Skip to content

Commit 19a7a17

Browse files
committed
Add Toggle AI features, set agents as default, faster autoselect #951
1 parent ba4f8eb commit 19a7a17

File tree

10 files changed

+233
-96
lines changed

10 files changed

+233
-96
lines changed

browser/data-browser/src/components/AI/AgentConfig.tsx

Lines changed: 35 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { useEffect, useState } from 'react';
2-
import { styled } from 'styled-components';
2+
import { styled, useTheme } from 'styled-components';
33
import { Row, Column } from '../Row';
4-
import { FaPencil, FaPlus, FaTrash } from 'react-icons/fa6';
4+
import { FaPencil, FaPlus, FaTrash, FaStar } from 'react-icons/fa6';
55
import { IconButton } from '../IconButton/IconButton';
66
import { ModelSelect } from './ModelSelect';
7-
import type { AIAgent, MCPServer } from './types';
7+
import type { AIAgent } from './types';
88
import {
99
Dialog,
1010
DialogTitle,
@@ -17,9 +17,6 @@ import { Button } from '../Button';
1717
import { SkeletonButton } from '../SkeletonButton';
1818
import { useSettings } from '../../helpers/AppSettings';
1919
import { Checkbox, CheckboxLabel } from '../forms/Checkbox';
20-
import { generateObject } from 'ai';
21-
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
22-
import { z } from 'zod';
2320

2421
// Helper function to generate a unique ID
2522
const generateId = () => {
@@ -86,6 +83,10 @@ export const useAIAgentConfig = () => {
8683
'atomic.ai.autoAgentSelect',
8784
true,
8885
);
86+
const [defaultAgentId, setDefaultAgentId] = useLocalStorage<string>(
87+
'atomic.ai.defaultAgentId',
88+
agents[0]?.id || '',
89+
);
8990

9091
// Save agents to settings
9192
const saveAgents = (newAgents: AIAgent[]) => {
@@ -97,57 +98,11 @@ export const useAIAgentConfig = () => {
9798
autoAgentSelectEnabled,
9899
setAutoAgentSelectEnabled,
99100
saveAgents,
101+
defaultAgentId,
102+
setDefaultAgentId,
100103
};
101104
};
102105

103-
function agentToText(agent: AIAgent, mcpServers: MCPServer[]) {
104-
return `ID: ${agent.id} Name: ${agent.name} Description: ${agent.description} Tools: ${agent.availableTools.map(t => mcpServers.find(s => s.id === t)?.name).join(', ')}`;
105-
}
106-
107-
export const useAutoAgentSelect = () => {
108-
const { mcpServers, openRouterApiKey } = useSettings();
109-
const { agents } = useAIAgentConfig();
110-
111-
const openrouter = createOpenRouter({
112-
apiKey: openRouterApiKey,
113-
compatibility: 'strict',
114-
});
115-
116-
const basePrompt = `You are a tool that determines what agent to use to answer the users question.
117-
These are the agents to choose from
118-
119-
${agents.map(agent => agentToText(agent, mcpServers)).join('\n')}
120-
121-
Answer with only the ID of the agent you pick
122-
123-
User question: `;
124-
125-
const pickAgent = async (question: string): Promise<AIAgent> => {
126-
const prompt = basePrompt + question.trim();
127-
128-
const { object } = await generateObject({
129-
// model: openrouter('google/gemma-3-27b-it:free'),
130-
model: openrouter('google/gemini-2.0-flash-lite-preview-02-05:free'),
131-
schemaName: 'Agent',
132-
schemaDescription: 'The agent to use for the question.',
133-
schema: z.object({
134-
agentId: z.string(),
135-
}),
136-
prompt,
137-
});
138-
139-
const agent = agents.find(a => a.id === object.agentId);
140-
141-
if (!agent) {
142-
throw new Error('Agent not found');
143-
}
144-
145-
return agent;
146-
};
147-
148-
return pickAgent;
149-
};
150-
151106
export const AgentConfig = ({
152107
open,
153108
onOpenChange,
@@ -159,10 +114,12 @@ export const AgentConfig = ({
159114
autoAgentSelectEnabled,
160115
setAutoAgentSelectEnabled,
161116
saveAgents,
117+
defaultAgentId,
118+
setDefaultAgentId,
162119
} = useAIAgentConfig();
163120
const [editingAgent, setEditingAgent] = useState<AIAgent | null>(null);
164121
const [isCreating, setIsCreating] = useState(false);
165-
122+
const theme = useTheme();
166123
const [dialogProps, show, close, isOpen] = useDialog({
167124
bindShow: onOpenChange,
168125
});
@@ -262,7 +219,29 @@ export const AgentConfig = ({
262219
onClick={() => onSelectAgent(agent)}
263220
>
264221
<Column>
265-
<AgentName>{agent.name}</AgentName>
222+
<Row gap='0.2ch' center>
223+
<IconButton
224+
onClick={e => {
225+
e.stopPropagation();
226+
setDefaultAgentId(agent.id);
227+
}}
228+
title={
229+
defaultAgentId === agent.id
230+
? 'Default agent'
231+
: 'Set as default'
232+
}
233+
edgeAlign='start'
234+
>
235+
<FaStar
236+
color={
237+
defaultAgentId === agent.id
238+
? theme.colors.main
239+
: theme.colors.bg2
240+
}
241+
/>
242+
</IconButton>
243+
<AgentName>{agent.name}</AgentName>
244+
</Row>
266245
<AgentDescription>{agent.description}</AgentDescription>
267246
</Column>
268247
<Row gap='0.5rem'>
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import styled from 'styled-components';
2+
import { useSettings } from '../../helpers/AppSettings';
3+
import { paths } from '../../routes/paths';
4+
import { AtomicLink } from '../AtomicLink';
5+
import { Column } from '../Row';
6+
import { FaGear } from 'react-icons/fa6';
7+
8+
export const NoKeyOverlay: React.FC = () => {
9+
const { openRouterApiKey } = useSettings();
10+
11+
if (openRouterApiKey) {
12+
return null;
13+
}
14+
15+
return (
16+
<Overlay>
17+
<Column gap='0.5rem' center>
18+
<p>
19+
No{' '}
20+
<a href='https://openrouter.ai/' target='_blank' rel='noreferrer'>
21+
OpenRouter
22+
</a>{' '}
23+
API key configured.
24+
</p>
25+
<ButtonLink clean path={paths.appSettings}>
26+
<FaGear /> Settings
27+
</ButtonLink>
28+
</Column>
29+
</Overlay>
30+
);
31+
};
32+
33+
const Overlay = styled.div`
34+
position: absolute;
35+
inset: 0;
36+
backdrop-filter: blur(4px);
37+
background-color: ${p =>
38+
p.theme.darkMode ? 'rgba(0, 0, 0, 0.8)' : 'rgba(255, 255, 255, 0.8)'};
39+
border-radius: ${p => p.theme.radius};
40+
z-index: 1000;
41+
display: grid;
42+
place-items: center;
43+
`;
44+
45+
const ButtonLink = styled(AtomicLink)`
46+
display: flex;
47+
align-items: center;
48+
gap: 0.5rem;
49+
border-radius: ${p => p.theme.radius};
50+
border: 1px solid ${p => p.theme.colors.bg2};
51+
padding: 0.3rem 1rem;
52+
background-color: ${p => p.theme.colors.bg};
53+
color: ${p => p.theme.colors.textLight};
54+
55+
&:hover,
56+
&:focus-visible {
57+
color: ${p => p.theme.colors.main};
58+
border-color: ${p => p.theme.colors.main};
59+
box-shadow: ${p => p.theme.boxShadowSoft};
60+
}
61+
`;

browser/data-browser/src/components/AI/SimpleAIChat.tsx

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,12 @@ import {
3636
type AIChatDisplayMessage,
3737
type AIMessageContext,
3838
} from './types';
39-
import {
40-
AgentConfig,
41-
useAIAgentConfig,
42-
useAutoAgentSelect,
43-
} from './AgentConfig';
39+
import { AgentConfig, useAIAgentConfig } from './AgentConfig';
4440
import { Button } from '../Button';
4541
import { MessageContextItem } from './MessageContextItem';
4642
import { useProcessMessages } from './useProcessMessages';
43+
import { NoKeyOverlay } from './NoKeyOverlay';
44+
import { useAutoAgentSelect } from './useAgentAutoSelect';
4745
type OngoingMessagePart = {
4846
type: 'reasoning' | 'text';
4947
text: string;
@@ -95,11 +93,13 @@ export const SimpleAIChat: React.FC<
9593
const abortSignalRef = useRef<AbortController>(null);
9694
const [aiState, setAiState] = useState<AIState>(AIState.Stopped);
9795
const [editedResources, setEditedResources] = useState<string[]>([]);
98-
const { agents, autoAgentSelectEnabled } = useAIAgentConfig();
96+
const { agents, autoAgentSelectEnabled, defaultAgentId } = useAIAgentConfig();
9997
const fileInputRef = useRef<HTMLInputElement>(null);
10098
const [attachedFile, setAttachedFile] = useState<FileAttachment | null>(null);
10199
const store = useStore();
102-
const [selectedAgent, setSelectedAgent] = useState<AIAgent>(agents[0]);
100+
const [selectedAgent, setSelectedAgent] = useState<AIAgent>(
101+
agents.find(a => a.id === defaultAgentId) || agents[0],
102+
);
103103
const [userInput, setUserInput] = useState('');
104104
const [userSelectedContextItems, setUserSelectedContextItems] = useState<
105105
AIMessageContext[]
@@ -108,6 +108,9 @@ export const SimpleAIChat: React.FC<
108108
const openrouter = createOpenRouter({
109109
apiKey: openRouterApiKey,
110110
compatibility: 'strict',
111+
extraBody: {
112+
transforms: ['middle-out'],
113+
},
111114
});
112115
const [ongoingMessage, setOngoingMessage] = useState<OngoingMessagePart>({
113116
type: 'text',
@@ -617,6 +620,7 @@ export const SimpleAIChat: React.FC<
617620
</IconButton>
618621
</Row>
619622
</Column>
623+
<NoKeyOverlay />
620624
</ChatInputWrapper>
621625
<TokensUsed>
622626
Tokens used: {tokensUsed[0]} input, {tokensUsed[1]} output
@@ -736,6 +740,7 @@ const AttachmentPreview = styled.div`
736740

737741
const ChatWindow = styled.div<{ fullView?: boolean }>`
738742
padding-top: ${p => (p.fullView ? p.theme.size(2) : 0)};
743+
position: relative;
739744
display: grid;
740745
grid-template-rows: auto 1fr auto;
741746
height: 90vh;
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
2+
import { useAIAgentConfig } from './AgentConfig';
3+
import type { AIAgent, MCPServer } from './types';
4+
import { useSettings } from '../../helpers/AppSettings';
5+
import { generateObject } from 'ai';
6+
import { z } from 'zod';
7+
8+
function agentToText(agent: AIAgent, mcpServers: MCPServer[]) {
9+
return `ID: ${agent.id} Name: ${agent.name} Description: ${agent.description} Tools: ${agent.availableTools.map(t => mcpServers.find(s => s.id === t)?.name).join(', ')}`;
10+
}
11+
12+
export const useAutoAgentSelect = () => {
13+
const { mcpServers, openRouterApiKey } = useSettings();
14+
const { agents } = useAIAgentConfig();
15+
16+
const openrouter = createOpenRouter({
17+
apiKey: openRouterApiKey,
18+
compatibility: 'strict',
19+
});
20+
21+
const basePrompt = `You are a tool that determines what agent to use to answer the users question.
22+
These are the agents to choose from
23+
24+
${agents.map(agent => agentToText(agent, mcpServers)).join('\n')}
25+
26+
Answer with only the ID of the agent you pick
27+
28+
User question: `;
29+
30+
const pickAgent = async (question: string): Promise<AIAgent> => {
31+
const prompt = basePrompt + question.trim();
32+
33+
const { object } = await generateObject({
34+
model: openrouter('google/gemma-3-4b-it:free'),
35+
schemaName: 'Agent',
36+
schemaDescription: 'The agent to use for the question.',
37+
schema: z.object({
38+
agentId: z.string(),
39+
}),
40+
prompt,
41+
});
42+
43+
const agent = agents.find(a => a.id === object.agentId);
44+
45+
if (!agent) {
46+
throw new Error('Agent not found');
47+
}
48+
49+
return agent;
50+
};
51+
52+
return pickAgent;
53+
};

browser/data-browser/src/components/AI/useGenerativeData.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export const useGenerativeData = () => {
3939
return cleaned;
4040
}
4141

42-
return null;
42+
return undefined;
4343
};
4444

4545
return { generateTitleFromConversation };

browser/data-browser/src/components/Parent.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type ParentProps = {
2828
/** Breadcrumb list. Recursively renders parents. */
2929
function Parent({ resource }: ParentProps): JSX.Element {
3030
const [parent] = useString(resource, core.properties.parent);
31+
const { enableAI } = useSettings();
3132
const { setIsOpen } = useAISidebar();
3233

3334
return (
@@ -41,12 +42,14 @@ function Parent({ resource }: ParentProps): JSX.Element {
4142
<BreadCrumbCurrent>{resource.title}</BreadCrumbCurrent>
4243
<Spacer />
4344
<ButtonArea>
44-
<IconButton
45-
title='Toggle AI panel'
46-
onClick={() => setIsOpen(prev => !prev)}
47-
>
48-
<AIIcon />
49-
</IconButton>
45+
{enableAI && (
46+
<IconButton
47+
title='Toggle AI panel'
48+
onClick={() => setIsOpen(prev => !prev)}
49+
>
50+
<AIIcon />
51+
</IconButton>
52+
)}
5053
<ResourceContextMenu
5154
isMainMenu
5255
subject={resource.subject}

browser/data-browser/src/helpers/AppSettings.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ export const AppSettingsContextProvider = (
5858
const [baseURL, setBaseURL] = useServerURL();
5959
const [drive, innerSetDrive] = useLocalStorage('drive', baseURL);
6060

61+
const [enableAI, setEnableAI] = useLocalStorage('atomic.ai.enabled', true);
62+
6163
const setDrive = useCallback(
6264
(newDrive: string) => {
6365
const url = new URL(newDrive);
@@ -108,6 +110,8 @@ export const AppSettingsContextProvider = (
108110
setOpenRouterApiKey,
109111
mcpServers,
110112
setMcpServers,
113+
enableAI,
114+
setEnableAI,
111115
}),
112116
[
113117
drive,
@@ -135,6 +139,8 @@ export const AppSettingsContextProvider = (
135139
setOpenRouterApiKey,
136140
mcpServers,
137141
setMcpServers,
142+
enableAI,
143+
setEnableAI,
138144
],
139145
);
140146

@@ -186,6 +192,9 @@ export interface AppSettings {
186192
mcpServers: MCPServer[];
187193
/** Update the list of MCP servers */
188194
setMcpServers: (servers: MCPServer[]) => void;
195+
/** Enable all AI features in the app */
196+
enableAI: boolean;
197+
setEnableAI: (b: boolean) => void;
189198
}
190199

191200
const initialState: AppSettings = {
@@ -214,6 +223,8 @@ const initialState: AppSettings = {
214223
setOpenRouterApiKey: () => undefined,
215224
mcpServers: [],
216225
setMcpServers: () => undefined,
226+
enableAI: true,
227+
setEnableAI: () => undefined,
217228
};
218229

219230
/** Hook for using App Settings, such as theme and darkmode */

0 commit comments

Comments
 (0)