'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, useCallback, useEffect } from 'react' import { X, Plus, Trash2, Globe, FileSearch, FilePlus, FileText, ExternalLink, Brain, ChevronDown, ChevronUp, HelpCircle, Mail, ImageIcon, Presentation, Pencil, Check } 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' | 'slide-generator' | 'excalidraw-generator' /** 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 sourceNoteIds?: string | null targetNotebookId?: string | null frequency: string tools?: string | null maxSteps?: number notifyEmail?: boolean includeImages?: boolean scheduledTime?: string | null scheduledDay?: number | null timezone?: string | null slideTheme?: string | null slideStyle?: string | null } | 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'], 'slide-generator': ['generate_pptx'], 'excalidraw-generator': ['generate_excalidraw'], } // --- Shared class strings --- const labelCls = 'block text-sm font-medium text-foreground mb-1.5' const labelCls2 = 'block text-sm font-medium text-foreground mb-2' const inputCls = 'w-full px-3 py-2 text-sm border border-input rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary bg-card text-foreground' const selectCls = 'w-full px-3 py-2 text-sm border border-input rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary bg-card text-foreground' const toggleOffBorder = 'border-border hover:border-primary/30' const toggleOffIcon = 'text-muted-foreground' const toggleOffLabel = 'text-sm font-medium text-foreground' const toggleOffHint = 'text-xs text-muted-foreground' // --- 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 [sourceNoteIds, setSourceNoteIds] = useState(() => { if (agent?.sourceNoteIds) { try { return JSON.parse(agent.sourceNoteIds) } catch { return [] } } return [] }) const [noteOptions, setNoteOptions] = useState<{ id: string; title: string }[]>([]) const [targetNotebookId, setTargetNotebookId] = useState(agent?.targetNotebookId || '') const [frequency, setFrequency] = useState(agent?.frequency || 'manual') const [scheduledTime, setScheduledTime] = useState(agent?.scheduledTime || '08:00') const [scheduledDay, setScheduledDay] = useState(agent?.scheduledDay ?? 1) const [timezone] = useState(() => { try { return Intl.DateTimeFormat().resolvedOptions().timeZone } catch { return 'UTC' } }) 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 [slideTheme, setSlideTheme] = useState(agent?.slideTheme || '') const [slideStyle, setSlideStyle] = useState<'soft' | 'sharp' | 'rounded' | 'pill'>( (agent?.slideStyle as 'soft' | 'sharp' | 'rounded' | 'pill') || 'soft' ) const [excalidrawStyle, setExcalidrawStyle] = useState<'default' | 'austere' | 'sketch-plus'>(() => { if (agent?.slideStyle === 'austere') return 'austere' if (agent?.slideStyle === 'sketch-plus') return 'sketch-plus' return 'default' }) const [excalidrawType, setExcalidrawType] = useState<'auto' | 'architecture-cloud' | 'flowchart' | 'mindmap' | 'org-chart' | 'timeline' | 'process-map'>(() => { const value = (agent?.slideTheme || '').trim() if (value === 'auto' || value === 'architecture-cloud' || value === 'mindmap' || value === 'org-chart' || value === 'timeline' || value === 'process-map') return value return 'flowchart' }) const [isSaving, setIsSaving] = useState(false) useEffect(() => { if (!sourceNotebookId || (type !== 'slide-generator' && type !== 'excalidraw-generator' && type !== 'monitor')) { setNoteOptions([]) return } fetch(`/api/notes?notebookId=${sourceNotebookId}&limit=50`) .then(r => r.json()) .then(data => { const notes = Array.isArray(data.data) ? data.data : Array.isArray(data) ? data : [] setNoteOptions(notes.map((n: any) => ({ id: n.id, title: n.title || 'Sans titre' }))) }) .catch(() => setNoteOptions([])) }, [sourceNotebookId, type]) 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 }, { id: 'generate_pptx', icon: Presentation, labelKey: 'agents.tools.generatePptx', external: false }, { id: 'generate_slides', icon: Presentation, labelKey: 'agents.tools.generateSlides', external: false }, { id: 'generate_excalidraw', icon: Pencil, labelKey: 'agents.tools.generateExcalidraw', 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' || type === 'slide-generator' || type === 'excalidraw-generator') { formData.set('sourceNotebookId', sourceNotebookId) } if (sourceNoteIds.length > 0) { formData.set('sourceNoteIds', JSON.stringify(sourceNoteIds)) } 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)) formData.set('scheduledTime', scheduledTime) formData.set('scheduledDay', String(scheduledDay)) formData.set('timezone', timezone) if (type === 'slide-generator') { if (slideTheme) formData.set('slideTheme', slideTheme) formData.set('slideStyle', slideStyle) } if (type === 'excalidraw-generator') { formData.set('slideTheme', excalidrawType) formData.set('slideStyle', excalidrawStyle) } await onSave(formData) } catch { toast.error(t('agents.toasts.saveError')) } finally { setIsSaving(false) } } const showSourceNotebook = type === 'monitor' || type === 'slide-generator' || type === 'excalidraw-generator' 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: 'slide-generator', labelKey: 'agents.types.slideGenerator', descKey: 'agents.typeDescriptions.slideGenerator' }, { value: 'excalidraw-generator', labelKey: 'agents.types.excalidrawGenerator', descKey: 'agents.typeDescriptions.excalidrawGenerator' }, { value: 'custom', labelKey: 'agents.types.custom', descKey: 'agents.typeDescriptions.custom' }, ] return ( e.stopPropagation()} > {/* Header — editable agent name */} setName(e.target.value)} className="text-lg font-semibold text-card-foreground bg-transparent border-none outline-none focus:ring-0 p-0 flex-1 placeholder:text-muted-foreground/40" placeholder={t('agents.form.namePlaceholder')} required /> {/* Agent Type */} {t('agents.form.agentType')} {agentTypes.map(at => ( setType(at.value)} className={` text-left px-3 py-2.5 rounded-lg border-2 transition-all text-sm ${type === at.value ? 'border-primary bg-primary/5 text-primary font-medium' : `${toggleOffBorder} text-muted-foreground`} `} > {t(at.labelKey)} {t(at.descKey)} ))} {/* Research Topic (researcher only) — replaces Description for this type */} {type === 'researcher' && ( {t('agents.form.researchTopic')} setDescription(e.target.value)} className={inputCls} placeholder={t('agents.form.researchTopicPlaceholder')} /> )} {/* Description (for non-researcher types) */} {type !== 'researcher' && ( {t('agents.form.description')} setDescription(e.target.value)} className={inputCls} placeholder={t('agents.form.descriptionPlaceholder')} /> )} {/* URLs (scraper and custom only — researcher uses search, not URLs) */} {(type === 'scraper' || type === 'custom') && ( {t('agents.form.urlsLabel')} {urls.map((url, i) => ( updateUrl(i, e.target.value)} className={inputCls} placeholder="https://example.com" /> {urls.length > 1 && ( removeUrl(i)} className="p-2 text-destructive/80 hover:text-destructive hover:bg-destructive/10 rounded-lg transition-colors" > )} ))} {t('agents.form.addUrl')} )} {/* Source Notebook (monitor, slide, excalidraw) */} {showSourceNotebook && ( {t('agents.form.sourceNotebook')} { setSourceNotebookId(e.target.value); setSourceNoteIds([]) }} className={selectCls} > {t('agents.form.selectNotebook')} {notebooks.map(nb => ( {nb.name} ))} )} {/* Note multi-select (slide-generator, excalidraw-generator only) */} {(type === 'slide-generator' || type === 'excalidraw-generator') && sourceNotebookId && noteOptions.length > 0 && ( {t('agents.form.selectNotes')} {noteOptions.map(note => { const isSelected = sourceNoteIds.includes(note.id) return ( { setSourceNoteIds(prev => isSelected ? prev.filter(id => id !== note.id) : [...prev, note.id] ) }} className={`w-full flex items-center gap-2 px-3 py-2 text-sm text-left hover:bg-accent/50 transition-colors ${isSelected ? 'bg-primary/5' : ''}`} > {isSelected && } {note.title} ) })} {sourceNoteIds.length > 0 && ( {t('agents.form.notesSelected', { count: sourceNoteIds.length })} )} )} {/* Theme selector — slide-generator only */} {type === 'slide-generator' && ( <> {t('agents.form.slideTheme')} setSlideTheme(e.target.value)} className={selectCls} > {t('agents.form.slideThemeDefault')} Modern & Wellness Business & Authority Nature & Outdoors Vintage & Academic Soft & Creative Bohemian Vibrant & Tech Craft & Artisan Tech & Night (dark) Education & Charts Forest & Eco Elegant & Fashion Art & Food Luxury & Mystery Pure Tech Blue Coastal Coral Vibrant Orange Mint Platinum White Gold {t('agents.form.slideStyle')} {(['soft', 'sharp', 'rounded', 'pill'] as const).map(s => ( setSlideStyle(s)} className={`px-3 py-2 rounded-lg border-2 text-sm transition-all text-left ${ slideStyle === s ? 'border-primary bg-primary/5 text-primary font-medium' : `${toggleOffBorder} text-muted-foreground` }`} > {t(`agents.form.slideStyle${s.charAt(0).toUpperCase() + s.slice(1)}` as any)} ))} > )} {/* Visual style selector — excalidraw-generator only */} {type === 'excalidraw-generator' && ( <> {t('agents.form.excalidrawDiagramType')} {([ { id: 'auto', labelKey: 'agents.form.excalidrawDiagramTypeAuto' }, { id: 'flowchart', labelKey: 'agents.form.excalidrawDiagramTypeFlowchart' }, { id: 'mindmap', labelKey: 'agents.form.excalidrawDiagramTypeMindmap' }, { id: 'org-chart', labelKey: 'agents.form.excalidrawDiagramTypeOrgChart' }, { id: 'timeline', labelKey: 'agents.form.excalidrawDiagramTypeTimeline' }, { id: 'process-map', labelKey: 'agents.form.excalidrawDiagramTypeProcessMap' }, { id: 'architecture-cloud', labelKey: 'agents.form.excalidrawDiagramTypeArchitectureCloud' }, ] as const).map((opt) => ( setExcalidrawType(opt.id)} className={`px-3 py-2 rounded-lg border-2 text-sm transition-all text-left ${ excalidrawType === opt.id ? 'border-primary bg-primary/5 text-primary font-medium' : `${toggleOffBorder} text-muted-foreground` }`} > {t(opt.labelKey)} ))} {t('agents.form.excalidrawDiagramStyle')} {([ { id: 'default', labelKey: 'agents.form.excalidrawDiagramStyleDefault' }, { id: 'sketch-plus', labelKey: 'agents.form.excalidrawDiagramStyleSketchPlus' }, { id: 'austere', labelKey: 'agents.form.excalidrawDiagramStyleAustere' }, ] as const).map((opt) => ( setExcalidrawStyle(opt.id)} className={`px-3 py-2 rounded-lg border-2 text-sm transition-all text-left ${ excalidrawStyle === opt.id ? 'border-primary bg-primary/5 text-primary font-medium' : `${toggleOffBorder} text-muted-foreground` }`} > {t(opt.labelKey)} ))} > )} {/* Target Notebook — hidden for file generators (they never create notes) */} {type !== 'slide-generator' && type !== 'excalidraw-generator' && ( {t('agents.form.targetNotebook')} setTargetNotebookId(e.target.value)} className={selectCls} > {t('agents.form.inbox')} {notebooks.map(nb => ( {nb.name} ))} )} {/* Frequency */} {t('agents.form.frequency')} setFrequency(e.target.value)} className={selectCls} > {t('agents.frequencies.manual')} {t('agents.frequencies.hourly')} {t('agents.frequencies.daily')} {t('agents.frequencies.weekly')} {t('agents.frequencies.monthly')} {/* Schedule config: time + day pickers (hidden for manual/hourly) */} {frequency !== 'manual' && frequency !== 'hourly' && ( {/* Day selector (weekly/monthly only) */} {frequency === 'weekly' && ( {t('agents.schedule.dayOfWeek')} setScheduledDay(Number(e.target.value))} className={selectCls} > {[ { value: 0, label: t('agents.schedule.days.mon') }, { value: 1, label: t('agents.schedule.days.tue') }, { value: 2, label: t('agents.schedule.days.wed') }, { value: 3, label: t('agents.schedule.days.thu') }, { value: 4, label: t('agents.schedule.days.fri') }, { value: 5, label: t('agents.schedule.days.sat') }, { value: 6, label: t('agents.schedule.days.sun') }, ].map(d => ( {d.label} ))} )} {frequency === 'monthly' && ( {t('agents.schedule.dayOfMonth')} setScheduledDay(Number(e.target.value))} className={selectCls} > {Array.from({ length: 31 }, (_, i) => i + 1).map(d => ( {d} ))} )} {/* Time picker */} {t('agents.schedule.time')} setScheduledTime(e.target.value)} className={inputCls} /> )} {/* Email Notification — hidden for file generators */} {type !== 'slide-generator' && type !== 'excalidraw-generator' && ( setNotifyEmail(!notifyEmail)} className={`flex items-center gap-3 p-3 rounded-lg border-2 cursor-pointer transition-all ${ notifyEmail ? 'border-primary bg-primary/5' : toggleOffBorder }`} > {t('agents.form.notifyEmail')} {t('agents.form.notifyEmailHint')} )} {/* Include Images — hidden for file generators */} {type !== 'slide-generator' && type !== 'excalidraw-generator' && ( setIncludeImages(!includeImages)} className={`flex items-center gap-3 p-3 rounded-lg border-2 cursor-pointer transition-all ${ includeImages ? 'border-primary bg-primary/5' : toggleOffBorder }`} > {t('agents.form.includeImages')} {t('agents.form.includeImagesHint')} )} {/* Advanced mode toggle */} setShowAdvanced(!showAdvanced)} className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground font-medium w-full pt-2 border-t border-border" > {showAdvanced ? : } {t('agents.form.advancedMode')} {/* Advanced: System Prompt */} {showAdvanced && ( <> {t('agents.form.instructions')} ({t('agents.form.instructionsHint')}) setRole(e.target.value)} rows={3} className="w-full px-3 py-2 text-sm border border-input rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary resize-y min-h-[80px] bg-card text-foreground" placeholder={t('agents.form.instructionsPlaceholder')} /> {/* Advanced: Tools */} {t('agents.tools.title')} {availableTools.map(at => { const Icon = at.icon const isSelected = selectedTools.includes(at.id) return ( { setSelectedTools(prev => isSelected ? prev.filter(t => t !== at.id) : [...prev, at.id] ) }} className={` flex items-center gap-2 px-3 py-2 rounded-lg border text-sm transition-all text-left ${isSelected ? 'border-primary bg-primary/5 text-primary font-medium' : `${toggleOffBorder} text-muted-foreground`} `} > {t(at.labelKey)} {at.external && !isSelected && ( {t('agents.tools.configNeeded')} )} ) })} {selectedTools.length > 0 && ( {t('agents.tools.selected', { count: selectedTools.length })} )} {/* Advanced: Max Steps */} {selectedTools.length > 0 && ( {t('agents.tools.maxSteps')} ({maxSteps}) setMaxSteps(Number(e.target.value))} className="w-full accent-primary" /> 3 25 )} > )} {/* Actions */} {t('agents.form.cancel')} {isSaving ? t('agents.form.saving') : agent ? t('agents.form.save') : t('agents.form.create')} ) }
{t('agents.form.notesSelected', { count: sourceNoteIds.length })}
{t('agents.tools.selected', { count: selectedTools.length })}