/** * Dialog for adding a new memory source. * * Step 1: pick a source kind (Composio / Folder / GitHub / RSS / Web / Twitter). * Step 2: fill in kind-specific fields and submit. * * For Composio, the dialog fetches the user's active connections and * presents them as a dropdown — the user picks an existing OAuth * connection rather than typing toolkit + connection_id. */ import { useCallback, useEffect, useState } from 'react'; import { listConnections } from '../../lib/composio/composioApi'; import type { ComposioConnection } from '../../lib/composio/types'; import { useT } from '../../lib/i18n/I18nContext'; import { addMemorySource, type MemorySourceEntry, SOURCE_KIND_ICONS, SOURCE_KIND_LABEL_KEYS, type SourceKind, } from '../../services/memorySourcesService'; interface AddMemorySourceDialogProps { open: boolean; onClose: () => void; onAdded: (source: MemorySourceEntry) => void; } const ALL_KINDS: SourceKind[] = [ 'composio', 'folder', 'github_repo', 'rss_feed', 'web_page', 'twitter_query', ]; export function AddMemorySourceDialog({ open, onClose, onAdded }: AddMemorySourceDialogProps) { const { t } = useT(); const [kind, setKind] = useState(null); const [label, setLabel] = useState(''); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); // Kind-specific fields const [path, setPath] = useState(''); const [glob, setGlob] = useState('**/*.md'); const [url, setUrl] = useState(''); const [branch, setBranch] = useState('main'); const [query, setQuery] = useState(''); const [selector, setSelector] = useState(''); const [connectionId, setConnectionId] = useState(''); const [toolkit, setToolkit] = useState(''); // Composio connection picker state const [connections, setConnections] = useState([]); const [loadingConnections, setLoadingConnections] = useState(false); // Fetch composio connections when user picks the composio kind. // setState calls live inside the spawned async closure (not the // synchronous effect body) to satisfy `react-hooks/set-state-in-effect`. useEffect(() => { if (kind !== 'composio') return undefined; let cancelled = false; void (async () => { if (cancelled) return; setLoadingConnections(true); try { const resp = await listConnections(); if (cancelled) return; setConnections(resp.connections); } catch (err) { if (cancelled) return; console.warn('[ui-flow][add-memory-source] listConnections failed', err); setError(t('memorySources.composioListFailed')); } finally { if (!cancelled) setLoadingConnections(false); } })(); return () => { cancelled = true; }; }, [kind, t]); const reset = useCallback(() => { setKind(null); setLabel(''); setPath(''); setGlob('**/*.md'); setUrl(''); setBranch('main'); setQuery(''); setSelector(''); setConnectionId(''); setToolkit(''); setError(null); }, []); const handleClose = useCallback(() => { reset(); onClose(); }, [onClose, reset]); const handleSubmit = useCallback(async () => { if (!kind || !label.trim()) return; setSubmitting(true); setError(null); try { const params: Record = { kind, label: label.trim(), enabled: true }; switch (kind) { case 'composio': params.toolkit = toolkit; params.connection_id = connectionId; break; case 'folder': params.path = path.trim(); params.glob = glob.trim() || '**/*.md'; break; case 'github_repo': params.url = url.trim(); params.branch = branch.trim() || 'main'; break; case 'rss_feed': params.url = url.trim(); break; case 'web_page': params.url = url.trim(); if (selector.trim()) params.selector = selector.trim(); break; case 'twitter_query': params.query = query.trim(); break; } const source = await addMemorySource(params as Omit); onAdded(source); handleClose(); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setSubmitting(false); } }, [ kind, label, path, glob, url, branch, query, selector, connectionId, toolkit, onAdded, handleClose, ]); if (!open) return null; const isValid = kind && label.trim() && isKindFieldsValid(kind, { path, url, query, connectionId }); return (

{t('memorySources.addSource')}

{!kind ? ( <>

{t('memorySources.pickKind')}

{ALL_KINDS.map(k => ( ))}
) : ( <>

{SOURCE_KIND_ICONS[kind]} {t(SOURCE_KIND_LABEL_KEYS[kind])}

{ setConnectionId(connId); setToolkit(tk); if (!label) setLabel(identityLabel); }} />
{error && (

{error}

)}
)}
); } function isKindFieldsValid( kind: SourceKind, fields: { path: string; url: string; query: string; connectionId: string } ): boolean { switch (kind) { case 'composio': return fields.connectionId.length > 0; case 'folder': return fields.path.trim().length > 0; case 'github_repo': case 'rss_feed': case 'web_page': return fields.url.trim().length > 0; case 'twitter_query': return fields.query.trim().length > 0; default: return true; } } interface FieldProps { label: string; value: string; onChange: (v: string) => void; placeholder?: string; type?: string; } interface FolderFieldProps { label: string; value: string; onChange: (v: string) => void; } function FolderField({ label, value, onChange }: FolderFieldProps) { const { t } = useT(); return ( ); } function Field({ label, value, onChange, placeholder, type = 'text' }: FieldProps) { return ( ); } interface KindFieldsProps { kind: SourceKind; path: string; setPath: (v: string) => void; glob: string; setGlob: (v: string) => void; url: string; setUrl: (v: string) => void; branch: string; setBranch: (v: string) => void; query: string; setQuery: (v: string) => void; selector: string; setSelector: (v: string) => void; connections: ComposioConnection[]; loadingConnections: boolean; connectionId: string; setConnection: (connectionId: string, toolkit: string, identityLabel: string) => void; } function KindFields(props: KindFieldsProps) { const { t } = useT(); switch (props.kind) { case 'composio': return ; case 'folder': return ( <> ); case 'github_repo': return ( <> ); case 'rss_feed': return ( ); case 'web_page': return ( <> ); case 'twitter_query': return ( ); default: return null; } } function ComposioPicker({ connections, loadingConnections, connectionId, setConnection, }: KindFieldsProps) { const { t } = useT(); if (loadingConnections) { return (

{t('memorySources.loadingConnections')}

); } if (connections.length === 0) { return (

{t('memorySources.noConnections')}

); } return ( ); }