diff --git a/frontend/src/pages/AgentsPage.tsx b/frontend/src/pages/AgentsPage.tsx index dfdac0cf..c8adeb19 100644 --- a/frontend/src/pages/AgentsPage.tsx +++ b/frontend/src/pages/AgentsPage.tsx @@ -49,7 +49,11 @@ import { Send, RefreshCw, Wifi, + Database, } from 'lucide-react'; +import { SOURCE_CATALOG } from '../types/connectors'; +import type { ConnectRequest } from '../types/connectors'; +import { listConnectors, connectSource } from '../lib/connectors-api'; // --------------------------------------------------------------------------- // Status helpers @@ -994,47 +998,328 @@ function InteractTab({ agentId, agentStatus }: { agentId: string; agentStatus: s } // --------------------------------------------------------------------------- -// Channels tab component +// Channels tab component (data sources) // --------------------------------------------------------------------------- -const AVAILABLE_CHANNELS = [ +function ChannelsTab({ agentId }: { agentId: string }) { + const [connectors, setConnectors] = useState< + Array<{ connector_id: string; display_name: string; connected: boolean; chunks: number }> + >([]); + const [expandedId, setExpandedId] = useState(null); + const [loading, setLoading] = useState(false); + + // suppress unused var – agentId reserved for future per-agent source binding + void agentId; + + const loadConnectors = useCallback(() => { + listConnectors() + .then((list) => + setConnectors( + list.map((c) => ({ + connector_id: c.connector_id, + display_name: c.display_name, + connected: c.connected, + chunks: (c as any).chunks || 0, + })), + ), + ) + .catch(() => {}); + }, []); + + useEffect(() => { loadConnectors(); }, [loadConnectors]); + + const handleConnect = async (id: string, req: ConnectRequest) => { + setLoading(true); + try { + await connectSource(id, req); + setExpandedId(null); + loadConnectors(); + } catch { + // error handling + } finally { + setLoading(false); + } + }; + + const connected = connectors.filter((c) => c.connected); + const notConnected = connectors.filter((c) => !c.connected); + + // Merge with SOURCE_CATALOG for icons/descriptions + const getMeta = (id: string) => + SOURCE_CATALOG.find((s) => s.connector_id === id); + + const iconMap: Record = { + gmail: '\u2709\uFE0F', gmail_imap: '\u2709\uFE0F', slack: '#', + imessage: '\uD83D\uDCAC', gdrive: '\uD83D\uDCC1', notion: '\uD83D\uDCC4', + obsidian: '\uD83D\uDCC1', granola: '\uD83C\uDF99\uFE0F', gcalendar: '\uD83D\uDCC5', + gcontacts: '\uD83D\uDCC7', outlook: '\u2709\uFE0F', apple_notes: '\uD83C\uDF4E', + dropbox: '\uD83D\uDCE6', whatsapp: '\uD83D\uDCF1', + }; + + return ( +
+
+ Data sources your agent can search +
+ + {/* Connected sources grid */} + {connected.length > 0 && ( +
+ {connected.map((c) => ( +
+ {iconMap[c.connector_id] || '\uD83D\uDD17'} +
+
+ {c.display_name} +
+
+ {c.chunks.toLocaleString()} chunks +
+
+ {'\u2713'} +
+ ))} +
+ )} + + {/* Not connected grid */} + {notConnected.length > 0 && ( +
+ {notConnected.map((c) => { + const meta = getMeta(c.connector_id); + const isExpanded = expandedId === c.connector_id; + + return ( +
+
+ setExpandedId(isExpanded ? null : c.connector_id) + } + > + {iconMap[c.connector_id] || '\uD83D\uDD17'} +
+
+ {c.display_name} +
+
+ Not connected +
+
+ + {isExpanded ? '\u2715 Close' : '+ Add'} + +
+ + {/* Inline setup panel */} + {isExpanded && meta?.steps && ( +
+ {meta.steps.map((step, i) => ( +
+
+ STEP {i + 1} +
+
+ {step.label} +
+ {step.url && ( + + {step.urlLabel || 'Open'} {'\u2192'} + + )} +
+ ))} + {meta.inputFields && ( + + handleConnect(c.connector_id, req) + } + /> + )} +
+ {'\uD83D\uDD12'} Read-only access {'\u00B7'} No data leaves your device +
+
+ )} +
+ ); + })} +
+ )} +
+ ); +} + +function InlineConnectForm({ + fields, + loading, + onSubmit, +}: { + fields: Array<{ name: string; placeholder: string; type?: string }>; + loading: boolean; + onSubmit: (req: ConnectRequest) => void; +}) { + const [inputs, setInputs] = useState>({}); + + const update = (name: string, value: string) => + setInputs((p) => ({ ...p, [name]: value })); + + const allFilled = fields.every((f) => inputs[f.name]?.trim()); + + const submit = () => { + const req: ConnectRequest = {}; + for (const f of fields) { + if (f.name === 'email') req.email = inputs.email; + else if (f.name === 'password') req.password = inputs.password; + else if (f.name === 'token') req.token = inputs.token; + else if (f.name === 'path') req.path = inputs.path; + } + if (req.email && req.password) { + req.token = `${req.email}:${req.password}`; + req.code = req.token; + } + if (req.token && !req.code) req.code = req.token; + onSubmit(req); + }; + + return ( +
+ {fields.map((f) => ( + update(f.name, e.target.value)} + placeholder={f.placeholder} + type={f.type || 'text'} + style={{ + width: '100%', padding: '7px 10px', + background: 'var(--color-bg)', + border: '1px solid var(--color-border)', + borderRadius: 4, color: 'var(--color-text)', + fontSize: 12, marginBottom: 6, + boxSizing: 'border-box', + }} + /> + ))} + +
+ ); +} + +// --------------------------------------------------------------------------- +// Messaging tab component +// --------------------------------------------------------------------------- + +const MESSAGING_CHANNELS = [ { - type: 'imessage', - name: 'iMessage', - icon: '💬', + type: 'imessage', name: 'iMessage', icon: '\uD83D\uDCAC', description: 'Text from your iPhone, iPad, or Mac', - connectLabel: 'Phone number or contact to monitor', + activeTemplate: (id: string) => `Text ${id} from your iPhone`, + setupLabel: 'Phone number or email for the agent to use', placeholder: '+15551234567', }, { - type: 'slack', - name: 'Slack', - icon: '#', + type: 'slack', name: 'Slack', icon: '#', description: 'Message from any Slack workspace', - connectLabel: 'Slack bot token (xoxb-...)', + activeTemplate: (id: string) => `DM @jarvis in ${id}`, + setupLabel: 'Slack bot token (xoxb-...)', placeholder: 'xoxb-...', }, { - type: 'whatsapp', - name: 'WhatsApp', - icon: '📱', + type: 'whatsapp', name: 'WhatsApp', icon: '\uD83D\uDCF1', description: 'Message via WhatsApp', - connectLabel: 'WhatsApp access token', + activeTemplate: (id: string) => `Message ${id} on WhatsApp`, + setupLabel: 'WhatsApp access token', placeholder: 'Access token', }, { - type: 'twilio', - name: 'SMS (Twilio)', - icon: '📨', + type: 'twilio', name: 'SMS (Twilio)', icon: '\uD83D\uDCE8', description: 'Text from any phone via Twilio', - connectLabel: 'Twilio phone number', + activeTemplate: (id: string) => `Text ${id} from any phone`, + setupLabel: 'Twilio phone number', placeholder: '+15551234567', }, ]; -function ChannelsTab({ agentId }: { agentId: string }) { +function MessagingTab({ agentId }: { agentId: string }) { const [bindings, setBindings] = useState([]); - const [connectingType, setConnectingType] = useState(null); + const [setupType, setSetupType] = useState(null); const [inputValue, setInputValue] = useState(''); const [loading, setLoading] = useState(false); @@ -1044,152 +1329,115 @@ function ChannelsTab({ agentId }: { agentId: string }) { useEffect(() => { loadBindings(); }, [loadBindings]); - const handleConnect = async (channelType: string) => { + const handleSetup = async (channelType: string) => { if (!inputValue.trim()) return; setLoading(true); try { await bindAgentChannel(agentId, channelType, { identifier: inputValue.trim(), }); - setConnectingType(null); + setSetupType(null); setInputValue(''); loadBindings(); - } catch { - // Could show error toast - } finally { - setLoading(false); - } + } catch { /* */ } finally { setLoading(false); } }; - const handleDisconnect = async (bindingId: string) => { + const handleRemove = async (bindingId: string) => { try { await unbindAgentChannel(agentId, bindingId); loadBindings(); - } catch { - // Could show error toast - } + } catch { /* */ } }; return (
-

- Talk to this agent from -

+ Talk to your agent from your phone or other platforms
- {AVAILABLE_CHANNELS.map((ch) => { + {MESSAGING_CHANNELS.map((ch) => { const binding = bindings.find((b) => b.channel_type === ch.type); - const isConnecting = connectingType === ch.type; + const identifier = binding?.config?.identifier as string || binding?.session_id || ''; + const isSetup = setupType === ch.type; return (
-
- {ch.icon} -
+ {ch.icon}
-
{ch.name}
-
{ch.name}
+
- {ch.description} + {binding + ? ch.activeTemplate(identifier) + : 'Not set up'}
{binding ? ( - - Active - +
+ Active + +
) : ( )}
- {/* Expanded: connected details */} - {binding && ( + {isSetup && (
- {binding.config?.identifier - ? `Monitoring: ${binding.config.identifier}` - : `Session: ${binding.session_id}`} -
- -
- )} - - {/* Expanded: connect form */} - {isConnecting && ( -
-
- {ch.connectLabel} -
-
+ }}>{ch.setupLabel}
+
setInputValue(e.target.value)} @@ -1202,11 +1450,11 @@ function ChannelsTab({ agentId }: { agentId: string }) { fontSize: 12, }} onKeyDown={(e) => { - if (e.key === 'Enter') handleConnect(ch.type); + if (e.key === 'Enter') handleSetup(ch.type); }} />
@@ -1423,7 +1671,7 @@ export function AgentsPage() { const [channels, setChannels] = useState([]); const [templates, setTemplates] = useState([]); const [showWizard, setShowWizard] = useState(false); - const [detailTab, setDetailTab] = useState<'overview' | 'interact' | 'channels' | 'tasks' | 'memory' | 'learning' | 'logs'>('overview'); + const [detailTab, setDetailTab] = useState<'overview' | 'interact' | 'channels' | 'messaging' | 'tasks' | 'memory' | 'learning' | 'logs'>('overview'); const refresh = useCallback(async () => { try { @@ -1553,7 +1801,8 @@ export function AgentsPage() { const DETAIL_TABS = [ { id: 'overview', label: 'Overview', icon: Activity }, { id: 'interact', label: 'Interact', icon: MessageSquare }, - { id: 'channels', label: 'Channels', icon: Wifi }, + { id: 'channels', label: 'Channels', icon: Database }, + { id: 'messaging', label: 'Messaging', icon: Wifi }, { id: 'tasks', label: 'Tasks', icon: ListTodo }, { id: 'memory', label: 'Memory', icon: Brain }, { id: 'learning', label: 'Learning', icon: Settings }, @@ -1712,6 +1961,11 @@ export function AgentsPage() { )} + {/* Tab: Messaging */} + {detailTab === 'messaging' && ( + + )} + {/* Tab: Tasks */} {detailTab === 'tasks' && (
diff --git a/frontend/src/types/connectors.ts b/frontend/src/types/connectors.ts index 59673cbc..375f8751 100644 --- a/frontend/src/types/connectors.ts +++ b/frontend/src/types/connectors.ts @@ -27,6 +27,7 @@ export interface ConnectorInfo { connected: boolean; auth_url?: string; mcp_tools?: string[]; + chunks?: number; } export interface SyncStatus {