783 lines
36 KiB
TypeScript
783 lines
36 KiB
TypeScript
'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>
|
|
<TooltipTrigger asChild>
|
|
<button type="button" className="inline-flex items-center ml-1 text-muted-foreground/40 hover:text-muted-foreground transition-colors">
|
|
<HelpCircle className="w-3.5 h-3.5" />
|
|
</button>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="right" className="max-w-xs text-balance">
|
|
{tooltip}
|
|
</TooltipContent>
|
|
</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<void>
|
|
onCancel: () => void
|
|
}
|
|
|
|
// --- Tool presets per type ---
|
|
const TOOL_PRESETS: Record<string, string[]> = {
|
|
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<AgentType>((agent?.type as AgentType) || 'scraper')
|
|
const [role, setRole] = useState(agent?.role || '')
|
|
const [urls, setUrls] = useState<string[]>(() => {
|
|
if (agent?.sourceUrls) {
|
|
try { return JSON.parse(agent.sourceUrls) } catch { return [''] }
|
|
}
|
|
return ['']
|
|
})
|
|
const [sourceNotebookId, setSourceNotebookId] = useState(agent?.sourceNotebookId || '')
|
|
const [sourceNoteIds, setSourceNoteIds] = useState<string[]>(() => {
|
|
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<number>(agent?.scheduledDay ?? 1)
|
|
const [timezone] = useState(() => {
|
|
try { return Intl.DateTimeFormat().resolvedOptions().timeZone } catch { return 'UTC' }
|
|
})
|
|
const [selectedTools, setSelectedTools] = useState<string[]>(() => {
|
|
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 (
|
|
<div className="fixed inset-0 bg-black/20 flex justify-end z-50" onClick={onCancel}>
|
|
<div
|
|
className="bg-card shadow-2xl w-full max-w-md h-full overflow-y-auto animate-in slide-in-from-right duration-300 flex flex-col border-l border-border/40"
|
|
onClick={e => e.stopPropagation()}
|
|
>
|
|
{/* Header — editable agent name */}
|
|
<div className="flex items-center justify-between px-6 py-4 border-b border-border">
|
|
<input
|
|
type="text"
|
|
value={name}
|
|
onChange={e => 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
|
|
/>
|
|
<button onClick={onCancel} className="p-1 rounded-md hover:bg-accent ml-3">
|
|
<X className="w-5 h-5 text-muted-foreground" />
|
|
</button>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="p-6 space-y-5">
|
|
{/* Agent Type */}
|
|
<div>
|
|
<label className={labelCls2}>{t('agents.form.agentType')}<FieldHelp tooltip={t('agents.help.tooltips.agentType')} /></label>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{agentTypes.map(at => (
|
|
<button
|
|
key={at.value}
|
|
type="button"
|
|
onClick={() => 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`}
|
|
`}
|
|
>
|
|
<div className="font-medium">{t(at.labelKey)}</div>
|
|
<div className="text-xs text-muted-foreground mt-0.5">{t(at.descKey)}</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Research Topic (researcher only) — replaces Description for this type */}
|
|
{type === 'researcher' && (
|
|
<div>
|
|
<label className={labelCls}>{t('agents.form.researchTopic')}<FieldHelp tooltip={t('agents.help.tooltips.researchTopic')} /></label>
|
|
<input
|
|
type="text"
|
|
value={description}
|
|
onChange={e => setDescription(e.target.value)}
|
|
className={inputCls}
|
|
placeholder={t('agents.form.researchTopicPlaceholder')}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Description (for non-researcher types) */}
|
|
{type !== 'researcher' && (
|
|
<div>
|
|
<label className={labelCls}>{t('agents.form.description')}<FieldHelp tooltip={t('agents.help.tooltips.description')} /></label>
|
|
<input
|
|
type="text"
|
|
value={description}
|
|
onChange={e => setDescription(e.target.value)}
|
|
className={inputCls}
|
|
placeholder={t('agents.form.descriptionPlaceholder')}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* URLs (scraper and custom only — researcher uses search, not URLs) */}
|
|
{(type === 'scraper' || type === 'custom') && (
|
|
<div>
|
|
<label className={labelCls}>
|
|
{t('agents.form.urlsLabel')}<FieldHelp tooltip={t('agents.help.tooltips.urls')} />
|
|
</label>
|
|
<div className="space-y-2">
|
|
{urls.map((url, i) => (
|
|
<div key={i} className="flex gap-2">
|
|
<input
|
|
type="url"
|
|
value={url}
|
|
onChange={e => updateUrl(i, e.target.value)}
|
|
className={inputCls}
|
|
placeholder="https://example.com"
|
|
/>
|
|
{urls.length > 1 && (
|
|
<button
|
|
type="button"
|
|
onClick={() => removeUrl(i)}
|
|
className="p-2 text-destructive/80 hover:text-destructive hover:bg-destructive/10 rounded-lg transition-colors"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
))}
|
|
<button
|
|
type="button"
|
|
onClick={addUrl}
|
|
className="flex items-center gap-1.5 text-xs text-primary hover:text-primary/80 font-medium"
|
|
>
|
|
<Plus className="w-3.5 h-3.5" />
|
|
{t('agents.form.addUrl')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Source Notebook (monitor, slide, excalidraw) */}
|
|
{showSourceNotebook && (
|
|
<div>
|
|
<label className={labelCls}>{t('agents.form.sourceNotebook')}<FieldHelp tooltip={t('agents.help.tooltips.sourceNotebook')} /></label>
|
|
<select
|
|
value={sourceNotebookId}
|
|
onChange={e => { setSourceNotebookId(e.target.value); setSourceNoteIds([]) }}
|
|
className={selectCls}
|
|
>
|
|
<option value="">{t('agents.form.selectNotebook')}</option>
|
|
{notebooks.map(nb => (
|
|
<option key={nb.id} value={nb.id}>
|
|
{nb.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
)}
|
|
|
|
{/* Note multi-select (slide-generator, excalidraw-generator only) */}
|
|
{(type === 'slide-generator' || type === 'excalidraw-generator') && sourceNotebookId && noteOptions.length > 0 && (
|
|
<div>
|
|
<label className={labelCls}>{t('agents.form.selectNotes')}<FieldHelp tooltip={t('agents.help.tooltips.selectNotes')} /></label>
|
|
<div className="border border-input rounded-lg max-h-48 overflow-y-auto bg-card">
|
|
{noteOptions.map(note => {
|
|
const isSelected = sourceNoteIds.includes(note.id)
|
|
return (
|
|
<button
|
|
key={note.id}
|
|
type="button"
|
|
onClick={() => {
|
|
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' : ''}`}
|
|
>
|
|
<div className={`w-4 h-4 rounded border-2 flex items-center justify-center flex-shrink-0 ${isSelected ? 'border-primary bg-primary' : 'border-input'}`}>
|
|
{isSelected && <Check className="w-3 h-3 text-primary-foreground" />}
|
|
</div>
|
|
<span className={isSelected ? 'text-primary font-medium' : 'text-foreground'}>{note.title}</span>
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
{sourceNoteIds.length > 0 && (
|
|
<p className="text-xs text-muted-foreground mt-1">{t('agents.form.notesSelected', { count: sourceNoteIds.length })}</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Theme selector — slide-generator only */}
|
|
{type === 'slide-generator' && (
|
|
<>
|
|
<div>
|
|
<label className={labelCls}>{t('agents.form.slideTheme')}<FieldHelp tooltip={t('agents.help.tooltips.slideTheme')} /></label>
|
|
<select
|
|
value={slideTheme}
|
|
onChange={e => setSlideTheme(e.target.value)}
|
|
className={selectCls}
|
|
>
|
|
<option value="">{t('agents.form.slideThemeDefault')}</option>
|
|
<option value="modern_wellness">Modern & Wellness</option>
|
|
<option value="business_authority">Business & Authority</option>
|
|
<option value="nature_outdoors">Nature & Outdoors</option>
|
|
<option value="vintage_academic">Vintage & Academic</option>
|
|
<option value="soft_creative">Soft & Creative</option>
|
|
<option value="bohemian">Bohemian</option>
|
|
<option value="vibrant_tech">Vibrant & Tech</option>
|
|
<option value="craft_artisan">Craft & Artisan</option>
|
|
<option value="tech_night">Tech & Night (dark)</option>
|
|
<option value="education_charts">Education & Charts</option>
|
|
<option value="forest_eco">Forest & Eco</option>
|
|
<option value="elegant_fashion">Elegant & Fashion</option>
|
|
<option value="art_food">Art & Food</option>
|
|
<option value="luxury_mystery">Luxury & Mystery</option>
|
|
<option value="pure_tech_blue">Pure Tech Blue</option>
|
|
<option value="coastal_coral">Coastal Coral</option>
|
|
<option value="vibrant_orange_mint">Vibrant Orange Mint</option>
|
|
<option value="platinum_white_gold">Platinum White Gold</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className={labelCls}>{t('agents.form.slideStyle')}<FieldHelp tooltip={t('agents.help.tooltips.slideStyle')} /></label>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{(['soft', 'sharp', 'rounded', 'pill'] as const).map(s => (
|
|
<button
|
|
key={s}
|
|
type="button"
|
|
onClick={() => 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)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Visual style selector — excalidraw-generator only */}
|
|
{type === 'excalidraw-generator' && (
|
|
<>
|
|
<div>
|
|
<label className={labelCls}>{t('agents.form.excalidrawDiagramType')}</label>
|
|
<div className="grid grid-cols-1 gap-2">
|
|
{([
|
|
{ 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) => (
|
|
<button
|
|
key={opt.id}
|
|
type="button"
|
|
onClick={() => 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)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className={labelCls}>{t('agents.form.excalidrawDiagramStyle')}</label>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{([
|
|
{ id: 'default', labelKey: 'agents.form.excalidrawDiagramStyleDefault' },
|
|
{ id: 'sketch-plus', labelKey: 'agents.form.excalidrawDiagramStyleSketchPlus' },
|
|
{ id: 'austere', labelKey: 'agents.form.excalidrawDiagramStyleAustere' },
|
|
] as const).map((opt) => (
|
|
<button
|
|
key={opt.id}
|
|
type="button"
|
|
onClick={() => 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)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Target Notebook — hidden for file generators (they never create notes) */}
|
|
{type !== 'slide-generator' && type !== 'excalidraw-generator' && (
|
|
<div>
|
|
<label className={labelCls}>{t('agents.form.targetNotebook')}<FieldHelp tooltip={t('agents.help.tooltips.targetNotebook')} /></label>
|
|
<select
|
|
value={targetNotebookId}
|
|
onChange={e => setTargetNotebookId(e.target.value)}
|
|
className={selectCls}
|
|
>
|
|
<option value="">{t('agents.form.inbox')}</option>
|
|
{notebooks.map(nb => (
|
|
<option key={nb.id} value={nb.id}>
|
|
{nb.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
)}
|
|
|
|
{/* Frequency */}
|
|
<div>
|
|
<label className={labelCls}>{t('agents.form.frequency')}<FieldHelp tooltip={t('agents.help.tooltips.frequency')} /></label>
|
|
<select
|
|
value={frequency}
|
|
onChange={e => setFrequency(e.target.value)}
|
|
className={selectCls}
|
|
>
|
|
<option value="manual">{t('agents.frequencies.manual')}</option>
|
|
<option value="hourly">{t('agents.frequencies.hourly')}</option>
|
|
<option value="daily">{t('agents.frequencies.daily')}</option>
|
|
<option value="weekly">{t('agents.frequencies.weekly')}</option>
|
|
<option value="monthly">{t('agents.frequencies.monthly')}</option>
|
|
</select>
|
|
</div>
|
|
|
|
{/* Schedule config: time + day pickers (hidden for manual/hourly) */}
|
|
{frequency !== 'manual' && frequency !== 'hourly' && (
|
|
<div className="flex gap-3">
|
|
{/* Day selector (weekly/monthly only) */}
|
|
{frequency === 'weekly' && (
|
|
<div className="flex-1">
|
|
<label className={labelCls}>{t('agents.schedule.dayOfWeek')}</label>
|
|
<select
|
|
value={scheduledDay}
|
|
onChange={e => 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 => (
|
|
<option key={d.value} value={d.value}>{d.label}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
)}
|
|
{frequency === 'monthly' && (
|
|
<div className="flex-1">
|
|
<label className={labelCls}>{t('agents.schedule.dayOfMonth')}</label>
|
|
<select
|
|
value={scheduledDay}
|
|
onChange={e => setScheduledDay(Number(e.target.value))}
|
|
className={selectCls}
|
|
>
|
|
{Array.from({ length: 31 }, (_, i) => i + 1).map(d => (
|
|
<option key={d} value={d}>{d}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
)}
|
|
{/* Time picker */}
|
|
<div className="flex-1">
|
|
<label className={labelCls}>{t('agents.schedule.time')}</label>
|
|
<input
|
|
type="time"
|
|
value={scheduledTime}
|
|
onChange={e => setScheduledTime(e.target.value)}
|
|
className={inputCls}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Email Notification — hidden for file generators */}
|
|
{type !== 'slide-generator' && type !== 'excalidraw-generator' && (
|
|
<div
|
|
onClick={() => 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
|
|
}`}
|
|
>
|
|
<Mail className={`w-4 h-4 flex-shrink-0 ${notifyEmail ? 'text-primary' : toggleOffIcon}`} />
|
|
<div className="flex-1 min-w-0">
|
|
<div className={toggleOffLabel}>{t('agents.form.notifyEmail')}</div>
|
|
<div className={toggleOffHint}>{t('agents.form.notifyEmailHint')}</div>
|
|
</div>
|
|
<div className={`w-9 h-5 rounded-full transition-colors flex-shrink-0 ${notifyEmail ? 'bg-primary' : 'bg-muted'}`}>
|
|
<div className={`w-4 h-4 bg-card rounded-full shadow-sm transition-transform mt-0.5 ${notifyEmail ? 'translate-x-4.5 ml-0.5' : 'ml-0.5'}`} />
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Include Images — hidden for file generators */}
|
|
{type !== 'slide-generator' && type !== 'excalidraw-generator' && (
|
|
<div
|
|
onClick={() => 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
|
|
}`}
|
|
>
|
|
<ImageIcon className={`w-4 h-4 flex-shrink-0 ${includeImages ? 'text-primary' : toggleOffIcon}`} />
|
|
<div className="flex-1 min-w-0">
|
|
<div className={toggleOffLabel}>{t('agents.form.includeImages')}</div>
|
|
<div className={toggleOffHint}>{t('agents.form.includeImagesHint')}</div>
|
|
</div>
|
|
<div className={`w-9 h-5 rounded-full transition-colors flex-shrink-0 ${includeImages ? 'bg-primary' : 'bg-muted'}`}>
|
|
<div className={`w-4 h-4 bg-card rounded-full shadow-sm transition-transform mt-0.5 ${includeImages ? 'translate-x-4.5 ml-0.5' : 'ml-0.5'}`} />
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Advanced mode toggle */}
|
|
<button
|
|
type="button"
|
|
onClick={() => 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 ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
|
|
{t('agents.form.advancedMode')}
|
|
</button>
|
|
|
|
{/* Advanced: System Prompt */}
|
|
{showAdvanced && (
|
|
<>
|
|
<div>
|
|
<label className="block text-sm font-medium text-foreground mb-1">
|
|
{t('agents.form.instructions')}
|
|
<FieldHelp tooltip={t('agents.help.tooltips.instructions')} />
|
|
<span className="text-xs text-muted-foreground font-normal ml-1">({t('agents.form.instructionsHint')})</span>
|
|
</label>
|
|
<textarea
|
|
value={role}
|
|
onChange={e => 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')}
|
|
/>
|
|
</div>
|
|
|
|
{/* Advanced: Tools */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-foreground mb-2">{t('agents.tools.title')}<FieldHelp tooltip={t('agents.help.tooltips.tools')} /></label>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{availableTools.map(at => {
|
|
const Icon = at.icon
|
|
const isSelected = selectedTools.includes(at.id)
|
|
return (
|
|
<button
|
|
key={at.id}
|
|
type="button"
|
|
onClick={() => {
|
|
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`}
|
|
`}
|
|
>
|
|
<Icon className="w-4 h-4 flex-shrink-0" />
|
|
<span>{t(at.labelKey)}</span>
|
|
{at.external && !isSelected && (
|
|
<span className="ml-auto text-[10px] text-muted-foreground bg-muted border border-border/60 px-1.5 py-0.5 rounded-full">{t('agents.tools.configNeeded')}</span>
|
|
)}
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
{selectedTools.length > 0 && (
|
|
<p className="text-xs text-muted-foreground mt-1.5">
|
|
{t('agents.tools.selected', { count: selectedTools.length })}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Advanced: Max Steps */}
|
|
{selectedTools.length > 0 && (
|
|
<div>
|
|
<label className="block text-sm font-medium text-foreground mb-1.5">
|
|
{t('agents.tools.maxSteps')}<FieldHelp tooltip={t('agents.help.tooltips.maxSteps')} />
|
|
<span className="text-muted-foreground font-normal ml-1">({maxSteps})</span>
|
|
</label>
|
|
<input
|
|
type="range"
|
|
min={3}
|
|
max={25}
|
|
value={maxSteps}
|
|
onChange={e => setMaxSteps(Number(e.target.value))}
|
|
className="w-full accent-primary"
|
|
/>
|
|
<div className="flex justify-between text-xs text-muted-foreground">
|
|
<span>3</span>
|
|
<span>25</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{/* Actions */}
|
|
<div className="flex items-center justify-end gap-3 pt-6 pb-4 mt-auto border-t border-border/40 bg-card sticky bottom-0">
|
|
<button
|
|
type="button"
|
|
onClick={onCancel}
|
|
className="px-4 py-2 text-sm font-medium text-muted-foreground bg-muted rounded-lg hover:bg-accent transition-colors"
|
|
>
|
|
{t('agents.form.cancel')}
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={isSaving}
|
|
className="px-4 py-2 text-sm font-medium text-primary-foreground bg-primary rounded-lg hover:bg-primary/90 transition-colors disabled:opacity-50 shadow-sm"
|
|
>
|
|
{isSaving ? t('agents.form.saving') : agent ? t('agents.form.save') : t('agents.form.create')}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|