'use client' /** * Agent Form Component * Simplified form for creating and editing agents. * Novice-friendly: hides system prompt and tools behind "Advanced mode". */ import { useState, useMemo, useRef } from 'react' import { X, Plus, Trash2, Globe, FileSearch, FilePlus, FileText, ExternalLink, Brain, ChevronDown, ChevronUp, HelpCircle, Mail, ImageIcon } from 'lucide-react' import { toast } from 'sonner' import { useLanguage } from '@/lib/i18n' import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip' // --- Types --- type AgentType = 'scraper' | 'researcher' | 'monitor' | 'custom' /** Small "?" tooltip shown next to form labels */ function FieldHelp({ tooltip }: { tooltip: string }) { return ( {tooltip} ) } interface AgentFormProps { agent?: { id: string name: string description?: string | null type?: string | null role: string sourceUrls?: string | null sourceNotebookId?: string | null targetNotebookId?: string | null frequency: string tools?: string | null maxSteps?: number notifyEmail?: boolean includeImages?: boolean } | null notebooks: { id: string; name: string; icon?: string | null }[] onSave: (data: FormData) => Promise onCancel: () => void } // --- Tool presets per type --- const TOOL_PRESETS: Record = { scraper: ['web_scrape', 'note_create', 'memory_search'], researcher: ['web_search', 'web_scrape', 'note_search', 'note_create', 'memory_search'], monitor: ['note_search', 'note_read', 'note_create', 'memory_search'], custom: ['memory_search'], } // --- Component --- export function AgentForm({ agent, notebooks, onSave, onCancel }: AgentFormProps) { const { t } = useLanguage() const [name, setName] = useState(agent?.name || '') const [description, setDescription] = useState(agent?.description || '') const [type, setType] = useState((agent?.type as AgentType) || 'scraper') const [role, setRole] = useState(agent?.role || '') const [urls, setUrls] = useState(() => { if (agent?.sourceUrls) { try { return JSON.parse(agent.sourceUrls) } catch { return [''] } } return [''] }) const [sourceNotebookId, setSourceNotebookId] = useState(agent?.sourceNotebookId || '') const [targetNotebookId, setTargetNotebookId] = useState(agent?.targetNotebookId || '') const [frequency, setFrequency] = useState(agent?.frequency || 'manual') const [selectedTools, setSelectedTools] = useState(() => { if (agent?.tools) { try { const parsed = JSON.parse(agent.tools) if (parsed.length > 0) return parsed } catch { /* fall through to presets */ } } // New agent or old agent with empty tools: use preset defaults const defaultType = (agent?.type as AgentType) || 'scraper' return TOOL_PRESETS[defaultType] || [] }) const [maxSteps, setMaxSteps] = useState(agent?.maxSteps || 10) const [notifyEmail, setNotifyEmail] = useState(agent?.notifyEmail || false) const [includeImages, setIncludeImages] = useState(agent?.includeImages || false) const [isSaving, setIsSaving] = useState(false) const [showAdvanced, setShowAdvanced] = useState(() => { // Auto-open advanced if editing an agent with custom tools or custom prompt if (agent?.tools) { try { const tools = JSON.parse(agent.tools) if (tools.length > 0) return true } catch { /* ignore */ } } // Also open if agent has a custom role (instructions) if (agent?.role && agent.role.trim().length > 0) return true return false }) // Tool definitions const availableTools = useMemo(() => [ { id: 'web_search', icon: Globe, labelKey: 'agents.tools.webSearch', external: true }, { id: 'web_scrape', icon: ExternalLink, labelKey: 'agents.tools.webScrape', external: true }, { id: 'note_search', icon: FileSearch, labelKey: 'agents.tools.noteSearch', external: false }, { id: 'note_read', icon: FileText, labelKey: 'agents.tools.noteRead', external: false }, { id: 'note_create', icon: FilePlus, labelKey: 'agents.tools.noteCreate', external: false }, { id: 'url_fetch', icon: ExternalLink, labelKey: 'agents.tools.urlFetch', external: false }, { id: 'memory_search', icon: Brain, labelKey: 'agents.tools.memorySearch', external: false }, ], []) // Track previous type to detect user-initiated type changes const prevTypeRef = useRef(type) // When user explicitly changes type (not on mount), reset tools to presets if (prevTypeRef.current !== type) { prevTypeRef.current = type // This is a user-initiated type change, not a mount // We queue the state update to happen after render setSelectedTools(TOOL_PRESETS[type] || []) setRole('') } const addUrl = () => setUrls([...urls, '']) const removeUrl = (index: number) => setUrls(urls.filter((_, i) => i !== index)) const updateUrl = (index: number, value: string) => { const newUrls = [...urls] newUrls[index] = value setUrls(newUrls) } const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() if (!name.trim()) { toast.error(t('agents.form.nameRequired')) return } setIsSaving(true) try { const formData = new FormData() formData.set('name', name.trim()) formData.set('description', description.trim()) formData.set('type', type) formData.set('role', role || t(`agents.defaultRoles.${type}`)) formData.set('frequency', frequency) formData.set('targetNotebookId', targetNotebookId) if (type === 'monitor') { formData.set('sourceNotebookId', sourceNotebookId) } const validUrls = urls.filter(u => u.trim()) if (validUrls.length > 0) { formData.set('sourceUrls', JSON.stringify(validUrls)) } formData.set('tools', JSON.stringify(selectedTools)) formData.set('maxSteps', String(maxSteps)) formData.set('notifyEmail', String(notifyEmail)) formData.set('includeImages', String(includeImages)) await onSave(formData) } catch { toast.error(t('agents.toasts.saveError')) } finally { setIsSaving(false) } } const showSourceNotebook = type === 'monitor' const agentTypes: { value: AgentType; labelKey: string; descKey: string }[] = [ { value: 'researcher', labelKey: 'agents.types.researcher', descKey: 'agents.typeDescriptions.researcher' }, { value: 'scraper', labelKey: 'agents.types.scraper', descKey: 'agents.typeDescriptions.scraper' }, { value: 'monitor', labelKey: 'agents.types.monitor', descKey: 'agents.typeDescriptions.monitor' }, { value: 'custom', labelKey: 'agents.types.custom', descKey: 'agents.typeDescriptions.custom' }, ] return (
{/* Header — editable agent name */}
setName(e.target.value)} className="text-lg font-semibold text-slate-800 bg-transparent border-none outline-none focus:ring-0 p-0 flex-1 placeholder:text-slate-300" placeholder={t('agents.form.namePlaceholder')} required />
{/* Agent Type */}
{agentTypes.map(at => ( ))}
{/* Research Topic (researcher only) — replaces Description for this type */} {type === 'researcher' && (
setDescription(e.target.value)} className="w-full px-3 py-2 text-sm border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary" placeholder={t('agents.form.researchTopicPlaceholder')} />
)} {/* Description (for non-researcher types) */} {type !== 'researcher' && (
setDescription(e.target.value)} className="w-full px-3 py-2 text-sm border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary" placeholder={t('agents.form.descriptionPlaceholder')} />
)} {/* URLs (scraper and custom only — researcher uses search, not URLs) */} {(type === 'scraper' || type === 'custom') && (
{urls.map((url, i) => (
updateUrl(i, e.target.value)} className="flex-1 px-3 py-2 text-sm border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary" placeholder="https://example.com" /> {urls.length > 1 && ( )}
))}
)} {/* Source Notebook (monitor only) */} {showSourceNotebook && (
)} {/* Target Notebook */}
{/* Frequency */}
{/* Email Notification */}
setNotifyEmail(!notifyEmail)} className={`flex items-center gap-3 p-3 rounded-lg border-2 cursor-pointer transition-all ${ notifyEmail ? 'border-primary bg-primary/5' : 'border-slate-200 hover:border-slate-300' }`} >
{t('agents.form.notifyEmail')}
{t('agents.form.notifyEmailHint')}
{/* Include Images */}
setIncludeImages(!includeImages)} className={`flex items-center gap-3 p-3 rounded-lg border-2 cursor-pointer transition-all ${ includeImages ? 'border-primary bg-primary/5' : 'border-slate-200 hover:border-slate-300' }`} >
{t('agents.form.includeImages')}
{t('agents.form.includeImagesHint')}
{/* Advanced mode toggle */} {/* Advanced: System Prompt */} {showAdvanced && ( <>