From 2fd435df6f39a9a9e6bbfc213e320a0e1f4dfaef Mon Sep 17 00:00:00 2001 From: Antigravity Date: Sat, 9 May 2026 17:18:47 +0000 Subject: [PATCH] feat: redesign agents page (architectural-grid style), add image description, fix AI limits, remove dead code - Redesign agents page with architectural-grid (8) design system: rounded-2xl cards, serif headings, motion tabs, dashed templates section - Replace agent form popup with full-page detail view (SettingsView style) with dark planning card, section tooltips, and help button - Hide advanced mode for slide/excalidraw generators - Add 'describe images' action to contextual AI assistant - Add copy button to action/resource preview with HTTP fallback - Add delete history button to agent run log panel - Increase AI word limit from 2000 to 5000 (reformulate + transform-markdown) - Increase max steps slider from 25 to 50 - Fix image description error with clear model compatibility message - Fix doubled execution count display in agent detail view - Remove dead files: notes-list-view.tsx, notes-view-toggle.tsx - Remove 'list' view mode from NotesViewMode type - Add missing i18n keys (back, configuration, options, copy, cleared) --- .../app/(main)/agents/agents-page-client.tsx | 265 +++--- .../appearance/appearance-settings-client.tsx | 46 +- .../app/(main)/settings/appearance/page.tsx | 8 - memento-note/app/actions/agent-actions.ts | 25 + .../app/api/ai/transform-markdown/route.ts | 4 +- memento-note/components/agents/agent-card.tsx | 224 ++--- .../components/agents/agent-detail-view.tsx | 900 ++++++++++++++++++ .../components/agents/agent-run-log.tsx | 73 +- .../components/agents/agent-templates.tsx | 84 +- .../components/contextual-ai-chat.tsx | 48 +- memento-note/components/home-client.tsx | 2 +- .../components/notes-editorial-view.tsx | 20 +- memento-note/components/notes-list-view.tsx | 166 ---- .../components/notes-main-section.tsx | 11 +- memento-note/components/notes-view-toggle.tsx | 109 --- .../components/recent-notes-section.tsx | 7 +- .../ai/services/image-description.service.ts | 67 +- .../ai/services/paragraph-refactor.service.ts | 2 +- memento-note/locales/ar.json | 7 +- memento-note/locales/de.json | 7 +- memento-note/locales/en.json | 17 +- memento-note/locales/es.json | 7 +- memento-note/locales/fa.json | 7 +- memento-note/locales/fr.json | 17 +- memento-note/locales/hi.json | 7 +- memento-note/locales/it.json | 7 +- memento-note/locales/ja.json | 7 +- memento-note/locales/ko.json | 7 +- memento-note/locales/nl.json | 7 +- memento-note/locales/pl.json | 7 +- memento-note/locales/pt.json | 7 +- memento-note/locales/ru.json | 7 +- memento-note/locales/zh.json | 7 +- 33 files changed, 1441 insertions(+), 745 deletions(-) create mode 100644 memento-note/components/agents/agent-detail-view.tsx delete mode 100644 memento-note/components/notes-list-view.tsx delete mode 100644 memento-note/components/notes-view-toggle.tsx diff --git a/memento-note/app/(main)/agents/agents-page-client.tsx b/memento-note/app/(main)/agents/agents-page-client.tsx index ccfff5d..83435e9 100644 --- a/memento-note/app/(main)/agents/agents-page-client.tsx +++ b/memento-note/app/(main)/agents/agents-page-client.tsx @@ -1,17 +1,13 @@ 'use client' -/** - * Agents Page Client - * Main client component for the agents page. - */ - import { useState, useCallback, useMemo, useEffect, useRef } from 'react' -import { Plus, Bot, LayoutTemplate, Search, HelpCircle } from 'lucide-react' +import { motion } from 'motion/react' +import { Plus, Bot, Search, LifeBuoy } from 'lucide-react' import { toast } from 'sonner' import { useLanguage } from '@/lib/i18n' import { AgentCard } from '@/components/agents/agent-card' -import { AgentForm } from '@/components/agents/agent-form' +import { AgentDetailView } from '@/components/agents/agent-detail-view' import { AgentTemplates } from '@/components/agents/agent-templates' import { AgentRunLog } from '@/components/agents/agent-run-log' import { AgentHelp } from '@/components/agents/agent-help' @@ -21,8 +17,6 @@ import { getAgents, } from '@/app/actions/agent-actions' -// --- Types --- - interface Notebook { id: string name: string @@ -37,16 +31,23 @@ interface AgentItem { role: string sourceUrls?: string | null sourceNotebookId?: string | null + sourceNoteIds?: string | null targetNotebookId?: string | null frequency: string isEnabled: boolean lastRun: string | Date | null + nextRun?: string | Date | null createdAt: string | Date updatedAt: string | Date tools?: string | null maxSteps?: number notifyEmail?: boolean includeImages?: boolean + scheduledTime?: string | null + scheduledDay?: number | null + timezone?: string | null + slideTheme?: string | null + slideStyle?: string | null _count: { actions: number } actions: { id: string; status: string; createdAt: string | Date }[] notebook?: { id: string; name: string; icon?: string | null } | null @@ -65,21 +66,18 @@ const typeFilterOptions = [ { value: 'custom', labelKey: 'agents.types.custom' }, ] as const -// --- Component --- - export function AgentsPageClient({ agents: initialAgents, notebooks, }: AgentsPageClientProps) { const { t } = useLanguage() const [agents, setAgents] = useState(initialAgents) - const [showForm, setShowForm] = useState(false) - const [editingAgent, setEditingAgent] = useState(null) + const [selectedAgent, setSelectedAgent] = useState(null) + const [isNewAgent, setIsNewAgent] = useState(false) const [logAgent, setLogAgent] = useState<{ id: string; name: string } | null>(null) const [showHelp, setShowHelp] = useState(false) const [searchQuery, setSearchQuery] = useState('') const [typeFilter, setTypeFilter] = useState('') - const [activeTab, setActiveTab] = useState<'dashboard' | 'templates'>('dashboard') const refreshAgents = useCallback(async () => { try { @@ -139,15 +137,15 @@ export function AgentsPageClient({ }, []) const handleCreate = useCallback(() => { - setEditingAgent(null) - setShowForm(true) + setIsNewAgent(true) + setSelectedAgent(null) }, []) const handleEdit = useCallback((id: string) => { const agent = agents.find(a => a.id === id) if (agent) { - setEditingAgent(agent) - setShowForm(true) + setIsNewAgent(false) + setSelectedAgent(agent) } }, [agents]) @@ -172,17 +170,22 @@ export function AgentsPageClient({ slideTheme: (formData.get('slideTheme') as string) || undefined, slideStyle: (formData.get('slideStyle') as string) || undefined, } - if (editingAgent) { - await updateAgent(editingAgent.id, data) + if (selectedAgent && !isNewAgent) { + await updateAgent(selectedAgent.id, data) toast.success(t('agents.toasts.updated')) } else { await createAgent(data) toast.success(t('agents.toasts.created')) } - setShowForm(false) - setEditingAgent(null) + setSelectedAgent(null) + setIsNewAgent(false) await refreshAgents() - }, [editingAgent, refreshAgents, t]) + }, [selectedAgent, isNewAgent, refreshAgents, t]) + + const handleBack = useCallback(() => { + setSelectedAgent(null) + setIsNewAgent(false) + }, []) const filteredAgents = useMemo(() => { return agents.filter(agent => { @@ -198,110 +201,126 @@ export function AgentsPageClient({ const existingAgentNames = useMemo(() => agents.map(a => a.name), [agents]) + const showDetail = selectedAgent !== null || isNewAgent + return ( - /* Full-bleed layout */ -
- - {/* ── Top header bar — architectural grid style ── */} -
-
-

- {t('agents.myAgents')} -

-

- {t('agents.subtitle')} -

-
- -
- - {/* ── Scrollable content area ── */} -
- - {/* Dashboard tab - agents + templates */} - {activeTab === 'dashboard' && ( - <> - {agents.length > 0 && ( - <> - {/* Filter pills + search */} -
-
- {typeFilterOptions.map(opt => ( - - ))} -
-
- - setSearchQuery(e.target.value)} - placeholder={t('agents.searchPlaceholder')} - className="pl-9 pr-4 py-2 text-[13px] bg-card border border-border/50 rounded-lg outline-none focus:border-primary/50 focus:ring-2 focus:ring-primary/10 transition-all placeholder:text-muted-foreground/40 w-56" - /> -
-
- - {filteredAgents.length > 0 ? ( -
- {filteredAgents.map(agent => ( - - ))} -
- ) : ( -
- -

{t('agents.noResults')}

-
- )} - - )} - - {agents.length === 0 && ( -
- -

{t('agents.noAgents')}

-

{t('agents.noAgentsDescription')}

-
- )} - - {/* Templates always visible on dashboard */} - - - )} -
- - {/* Sliding panels */} - {showForm && ( - + {showDetail ? ( + { setShowForm(false); setEditingAgent(null) }} + onBack={handleBack} + onOpenLogs={(id, name) => setLogAgent({ id, name })} + onOpenHelp={() => setShowHelp(true)} + isNew={isNewAgent} /> + ) : ( + <> +
+
+
+

+ {t('agents.myAgents')} +

+

+ {t('agents.subtitle')} +

+
+
+ + +
+
+ +
+
+ {typeFilterOptions.map((opt, i) => ( + + ))} +
+
+ + setSearchQuery(e.target.value)} + placeholder={t('agents.searchPlaceholder')} + className="pl-9 pr-4 py-2 text-[13px] bg-card border border-border/50 rounded-lg outline-none focus:border-foreground/20 transition-all placeholder:text-muted-foreground/40 w-48" + /> +
+
+
+ +
+ {agents.length === 0 ? ( +
+ +

{t('agents.noAgents')}

+

{t('agents.noAgentsDescription')}

+
+ ) : ( + <> + {filteredAgents.length > 0 ? ( +
+ {filteredAgents.map(agent => ( + + ))} +
+ ) : ( +
+ +

{t('agents.noResults')}

+
+ )} + + )} + +
+
+
+ {t('agents.templates.title')} +
+
+
+ +
+
+ )} + {logAgent && ( setShowHelp(false)} /> )} -
+ ) } diff --git a/memento-note/app/(main)/settings/appearance/appearance-settings-client.tsx b/memento-note/app/(main)/settings/appearance/appearance-settings-client.tsx index 39f7b60..ad8807e 100644 --- a/memento-note/app/(main)/settings/appearance/appearance-settings-client.tsx +++ b/memento-note/app/(main)/settings/appearance/appearance-settings-client.tsx @@ -5,29 +5,23 @@ import { updateAISettings } from '@/app/actions/ai-settings' import { updateUserSettings } from '@/app/actions/user-settings' import { useLanguage } from '@/lib/i18n' import { toast } from 'sonner' -import { Palette, Type, LayoutGrid, Maximize2 } from 'lucide-react' +import { Palette, Type } from 'lucide-react' import { applyDocumentTheme, normalizeThemeId, type ThemeId } from '@/lib/apply-document-theme' interface AppearanceSettingsClientProps { initialFontSize: string initialTheme: string - initialNotesViewMode: 'masonry' | 'tabs' | 'list' - initialCardSizeMode?: 'variable' | 'uniform' initialFontFamily?: string } export function AppearanceSettingsClient({ initialFontSize, initialTheme, - initialNotesViewMode, - initialCardSizeMode = 'variable', initialFontFamily = 'inter', }: AppearanceSettingsClientProps) { const { t } = useLanguage() const [theme, setTheme] = useState(normalizeThemeId(initialTheme || 'light')) const [fontSize, setFontSize] = useState(initialFontSize || 'medium') - const [notesViewMode, setNotesViewMode] = useState<'masonry' | 'tabs' | 'list'>(initialNotesViewMode) - const [cardSizeMode, setCardSizeMode] = useState<'variable' | 'uniform'>(initialCardSizeMode) const [fontFamily, setFontFamily] = useState(initialFontFamily) const handleThemeChange = async (value: string) => { @@ -47,21 +41,6 @@ export function AppearanceSettingsClient({ toast.success(t('settings.settingsSaved') || 'Saved') } - const handleNotesViewChange = async (value: string) => { - const mode = value === 'tabs' ? 'tabs' : value === 'list' ? 'list' : 'masonry' - setNotesViewMode(mode) - await updateAISettings({ notesViewMode: mode }) - toast.success(t('settings.settingsSaved') || 'Saved') - } - - const handleCardSizeModeChange = async (value: string) => { - const mode = value === 'uniform' ? 'uniform' : 'variable' - setCardSizeMode(mode) - localStorage.setItem('card-size-mode', mode) - await updateUserSettings({ cardSizeMode: mode }) - toast.success(t('settings.settingsSaved') || 'Saved') - } - const handleFontFamilyChange = async (value: string) => { const font = value === 'system' ? 'system' : value === 'playfair' ? 'playfair' @@ -206,30 +185,7 @@ export function AppearanceSettingsClient({ onChange={handleFontFamilyChange} /> - -
) diff --git a/memento-note/app/(main)/settings/appearance/page.tsx b/memento-note/app/(main)/settings/appearance/page.tsx index 35604a0..15a9868 100644 --- a/memento-note/app/(main)/settings/appearance/page.tsx +++ b/memento-note/app/(main)/settings/appearance/page.tsx @@ -19,14 +19,6 @@ export default async function AppearanceSettingsPage() { ) diff --git a/memento-note/app/actions/agent-actions.ts b/memento-note/app/actions/agent-actions.ts index 3632a4f..ada823c 100644 --- a/memento-note/app/actions/agent-actions.ts +++ b/memento-note/app/actions/agent-actions.ts @@ -287,6 +287,31 @@ export async function getAgentActions(agentId: string) { } } +export async function deleteAgentHistory(agentId: string) { + const session = await auth() + if (!session?.user?.id) { + throw new Error('Non autorise') + } + + try { + const agent = await prisma.agent.findFirst({ + where: { id: agentId, userId: session.user.id }, + select: { id: true } + }) + if (!agent) throw new Error('Agent non trouve') + + await prisma.agentAction.deleteMany({ + where: { agentId } + }) + + revalidatePath('/agents') + return { success: true } + } catch (error) { + console.error('Error deleting agent history:', error) + throw new Error('Impossible de supprimer l\'historique') + } +} + export async function toggleAgent(id: string, isEnabled: boolean) { const session = await auth() if (!session?.user?.id) { diff --git a/memento-note/app/api/ai/transform-markdown/route.ts b/memento-note/app/api/ai/transform-markdown/route.ts index 77f81b6..1c4cb44 100644 --- a/memento-note/app/api/ai/transform-markdown/route.ts +++ b/memento-note/app/api/ai/transform-markdown/route.ts @@ -27,9 +27,9 @@ export async function POST(request: NextRequest) { ) } - if (wordCount > 2000) { + if (wordCount > 5000) { return NextResponse.json( - { errorKey: 'ai.wordCountMax', params: { max: 2000, current: wordCount } }, + { errorKey: 'ai.wordCountMax', params: { max: 5000, current: wordCount } }, { status: 400 } ) } diff --git a/memento-note/components/agents/agent-card.tsx b/memento-note/components/agents/agent-card.tsx index b0039d0..0b1502f 100644 --- a/memento-note/components/agents/agent-card.tsx +++ b/memento-note/components/agents/agent-card.tsx @@ -1,10 +1,5 @@ 'use client' -/** - * Agent Card Component - * Compact card matching the reference design — with a "Next Run / Status" footer. - */ - import { useState, useEffect, useRef } from 'react' import { formatDistanceToNow } from 'date-fns' import { fr } from 'date-fns/locale/fr' @@ -21,14 +16,13 @@ import { XCircle, Clock, Pencil, + Activity, Presentation, } from 'lucide-react' import { toast } from 'sonner' import { useLanguage } from '@/lib/i18n' import { getNotebookIcon } from '@/lib/notebook-icon' -// --- Types --- - interface AgentCardProps { agent: { id: string @@ -50,19 +44,13 @@ interface AgentCardProps { onToggle: (id: string, isEnabled: boolean) => void } -// --- Config --- - -/** Icône par type — tons neutres alignés sur le thème (encre / papier). */ -const ICON_BOX = 'bg-primary/10 dark:bg-primary/15' -const ICON_MARK = 'text-primary' - -const typeConfig: Record = { - scraper: { icon: Globe, color: ICON_MARK, bgColor: ICON_BOX }, - researcher: { icon: Search, color: ICON_MARK, bgColor: ICON_BOX }, - monitor: { icon: Eye, color: ICON_MARK, bgColor: ICON_BOX }, - custom: { icon: Settings, color: ICON_MARK, bgColor: ICON_BOX }, - 'slide-generator': { icon: Presentation, color: ICON_MARK, bgColor: ICON_BOX }, - 'excalidraw-generator': { icon: Pencil, color: ICON_MARK, bgColor: ICON_BOX }, +const typeConfig: Record = { + scraper: { icon: Globe }, + researcher: { icon: Search }, + monitor: { icon: Eye }, + custom: { icon: Settings }, + 'slide-generator': { icon: Presentation }, + 'excalidraw-generator': { icon: Pencil }, } const frequencyKeys: Record = { @@ -73,8 +61,6 @@ const frequencyKeys: Record = { monthly: 'agents.frequencies.monthly', } -// --- Component --- - export function AgentCard({ agent, onEdit, onRefresh, onToggle }: AgentCardProps) { const { t, language } = useLanguage() const [isRunning, setIsRunning] = useState(false) @@ -88,11 +74,8 @@ export function AgentCard({ agent, onEdit, onRefresh, onToggle }: AgentCardProps const Icon = config.icon const lastAction = agent.actions[0] const dateLocale = language === 'fr' ? fr : enUS - const isNew = Date.now() - new Date(agent.createdAt).getTime() < 5 * 60 * 1000 const pollRef = useRef | null>(null) - - // Cleanup polling on unmount useEffect(() => () => { if (pollRef.current) clearInterval(pollRef.current) }, []) const handleRun = async () => { @@ -109,8 +92,6 @@ export function AgentCard({ agent, onEdit, onRefresh, onToggle }: AgentCardProps setIsRunning(false) return } - - // Poll status every 3 s until terminal state if (pollRef.current) clearInterval(pollRef.current) pollRef.current = setInterval(async () => { try { @@ -129,9 +110,8 @@ export function AgentCard({ agent, onEdit, onRefresh, onToggle }: AgentCardProps toast.error(t('agents.toasts.runError', { error: data.error || t('agents.toasts.runFailed') }), { id: toastId }) onRefresh() } - } catch { /* network error — keep polling */ } + } catch { /* keep polling */ } }, 3000) - } catch { toast.error(t('agents.toasts.runGenericError'), { id: toastId }) setIsRunning(false) @@ -169,7 +149,6 @@ export function AgentCard({ agent, onEdit, onRefresh, onToggle }: AgentCardProps } } - // Derive "Next Run" label const nextRunLabel = (() => { if (!agent.isEnabled) return '—' if (agent.frequency === 'manual') return t('agents.frequencies.manual') @@ -180,137 +159,116 @@ export function AgentCard({ agent, onEdit, onRefresh, onToggle }: AgentCardProps return t(frequencyKeys[agent.frequency] || 'agents.frequencies.manual') })() + const statusLabel = lastAction + ? lastAction.status === 'success' ? t('agents.status.success') + : lastAction.status === 'failure' ? t('agents.status.failure') + : lastAction.status === 'running' ? t('agents.status.running') + : t('agents.status.pending') + : '—' + return ( -
- {/* Card body */} -
- - {/* Header row: icon + name/type + toggle */} -
-
-
- -
-
-
-

{agent.name}

- {mounted && isNew && ( - - {t('agents.newBadge')} - - )} -
- - {t(`agents.types.${agent.type || 'custom'}`)} - -
+
onEdit(agent.id)} + > +
+
+
+
- - {/* Toggle */} +
+

{agent.name}

+

+ {t(`agents.types.${agent.type || 'custom'}`)} +

+
+
+
e.stopPropagation()}>
- - {/* Description */} - {agent.description && ( -

{agent.description}

- )} - - {/* Meta: frequency + executions */} -
- - - {t(frequencyKeys[agent.frequency] || 'agents.frequencies.manual')} - - · - {t('agents.metadata.executions', { count: agent._count.actions })} - {agent.notebook && ( - <> - · - - {(() => { - const NbIcon = getNotebookIcon(agent.notebook!.icon) - return - })()} - {agent.notebook.name} - - - )} -
- {/* Footer: Next Run + Last Status */} -
-
-

{t('agents.status.nextRun')}

-

- - {nextRunLabel} -

-
-
-

{t('agents.status.lastStatus')}

- {lastAction ? ( - - {lastAction.status === 'success' && } - {lastAction.status === 'failure' && } - {lastAction.status === 'running' && } - {lastAction.status === 'success' && t('agents.status.success')} - {lastAction.status === 'failure' && t('agents.status.failure')} - {lastAction.status === 'running' && t('agents.status.running')} - {lastAction.status === 'pending' && t('agents.status.pending')} + {agent.description && ( +

+ {agent.description} +

+ )} + +
+
+
+ + + {t(frequencyKeys[agent.frequency] || 'agents.frequencies.manual')} - ) : ( - - )} + {t('agents.metadata.executions', { count: agent._count.actions })} +
+
+
+
+ {t('agents.status.nextRun')} + {nextRunLabel} +
+
+ {t('agents.status.lastStatus')} + {lastAction ? ( + + {lastAction.status === 'success' && } + {lastAction.status === 'failure' && } + {lastAction.status === 'running' && } + {statusLabel} + + ) : ( + + )} +
- {/* Actions row */} -
+
e.stopPropagation()}> diff --git a/memento-note/components/agents/agent-detail-view.tsx b/memento-note/components/agents/agent-detail-view.tsx new file mode 100644 index 0000000..3c0b218 --- /dev/null +++ b/memento-note/components/agents/agent-detail-view.tsx @@ -0,0 +1,900 @@ +'use client' + +import { useState, useMemo, useRef, useCallback, useEffect } from 'react' +import { motion } from 'motion/react' +import { + ArrowLeft, + Plus, + Trash2, + Globe, + FileSearch, + FilePlus, + FileText, + ExternalLink, + Brain, + ChevronDown, + ChevronUp, + HelpCircle, + Mail, + ImageIcon, + Presentation, + Pencil, + Check, + Eye, + Search, + Settings, + Clock, + Activity, + Sparkles, + Loader2, + BookOpen, + LifeBuoy, +} from 'lucide-react' +import { toast } from 'sonner' +import { useLanguage } from '@/lib/i18n' +import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip' + +type AgentType = 'scraper' | 'researcher' | 'monitor' | 'custom' | 'slide-generator' | 'excalidraw-generator' + +function FieldHelp({ tooltip }: { tooltip: string }) { + return ( + + + + + + {tooltip} + + + ) +} + +const typeIcons: Record = { + scraper: Globe, + researcher: Search, + monitor: Eye, + custom: Settings, + 'slide-generator': Presentation, + 'excalidraw-generator': Pencil, +} + +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'], +} + +interface AgentDetailViewProps { + 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 + isEnabled: boolean + tools?: string | null + maxSteps?: number + notifyEmail?: boolean + includeImages?: boolean + scheduledTime?: string | null + scheduledDay?: number | null + timezone?: string | null + slideTheme?: string | null + slideStyle?: string | null + lastRun: string | Date | null + nextRun?: string | Date | null + createdAt: string | Date + _count: { actions: number } + actions: { id: string; status: string; createdAt: string | Date }[] + } | null + notebooks: { id: string; name: string; icon?: string | null }[] + onSave: (data: FormData) => Promise + onBack: () => void + onOpenLogs: (agentId: string, agentName: string) => void + onOpenHelp: () => void + isNew?: boolean +} + +export function AgentDetailView({ + agent, + notebooks, + onSave, + onBack, + onOpenLogs, + onOpenHelp, + isNew, +}: AgentDetailViewProps) { + 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 */ } + } + return TOOL_PRESETS[(agent?.type as AgentType) || 'scraper'] || [] + }) + 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) + const [showAdvanced, setShowAdvanced] = useState(() => { + if (agent?.tools) { + try { const tools = JSON.parse(agent.tools); if (tools.length > 0) return true } catch { /* */ } + } + if (agent?.role && agent.role.trim().length > 0) return true + return 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 availableTools = useMemo(() => [ + { id: 'web_search', icon: Globe, labelKey: 'agents.tools.webSearch' }, + { id: 'web_scrape', icon: ExternalLink, labelKey: 'agents.tools.webScrape' }, + { id: 'note_search', icon: FileSearch, labelKey: 'agents.tools.noteSearch' }, + { id: 'note_read', icon: FileText, labelKey: 'agents.tools.noteRead' }, + { id: 'note_create', icon: FilePlus, labelKey: 'agents.tools.noteCreate' }, + { id: 'url_fetch', icon: ExternalLink, labelKey: 'agents.tools.urlFetch' }, + { id: 'memory_search', icon: Brain, labelKey: 'agents.tools.memorySearch' }, + { id: 'generate_pptx', icon: Presentation, labelKey: 'agents.tools.generatePptx' }, + { id: 'generate_slides', icon: Presentation, labelKey: 'agents.tools.generateSlides' }, + { id: 'generate_excalidraw', icon: Pencil, labelKey: 'agents.tools.generateExcalidraw' }, + ], []) + + const prevTypeRef = useRef(type) + if (prevTypeRef.current !== type) { + prevTypeRef.current = type + 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 Icon = typeIcons[type] || Settings + + const frequencyLabel = t(`agents.frequencies.${frequency}`) || frequency + const successRate = agent?.actions?.length + ? Math.round((agent.actions.filter(a => a.status === 'success').length / agent.actions.length) * 100 * 10) / 10 + : null + + const sectionTitleCls = 'text-xs font-bold uppercase tracking-[0.3em] text-muted-foreground' + const cardCls = 'bg-card border border-border rounded-3xl overflow-hidden shadow-sm' + const labelCls = 'block text-[11px] uppercase tracking-widest font-bold text-muted-foreground' + const inputCls = 'w-full bg-muted/50 border border-border rounded-xl px-4 py-3 text-sm outline-none focus:ring-1 focus:ring-foreground/10 focus:border-foreground/20 transition-all text-foreground' + const selectCls = 'w-full bg-muted/50 border border-border rounded-xl px-4 py-3 text-sm outline-none focus:ring-1 focus:ring-foreground/10 focus:border-foreground/20 transition-all cursor-pointer font-medium text-foreground appearance-none' + + return ( + +
+
+ +
+ + {!isNew && agent && ( + + )} + +
+
+
+ +
+
+
+
+
+
+ +
+
+ setName(e.target.value)} + className="text-3xl font-memento-serif font-medium text-foreground bg-transparent border-none outline-none placeholder:text-muted-foreground/40 w-full" + placeholder={t('agents.form.namePlaceholder')} + /> +
+ + {isNew ? t('agents.newBadge') : `ID: ${agent?.id?.slice(0, 8)}`} + + {!isNew && agent?.isEnabled && ( + + {t('agents.actions.toggleOn')} + + )} +
+
+
+
+ {!isNew && agent && ( +
+
+ {t('agents.status.nextRun')} + {t('agents.metadata.executions', { count: agent._count.actions })} +
+ {successRate !== null && ( +
+ Succès + {successRate}% +
+ )} +
+ )} +
+
+ +
+
+
+ +
+

{t('agents.form.agentType')}

+
+
+
+ {[ + { value: 'researcher' as AgentType, labelKey: 'agents.types.researcher', descKey: 'agents.typeDescriptions.researcher', icon: Search }, + { value: 'scraper' as AgentType, labelKey: 'agents.types.scraper', descKey: 'agents.typeDescriptions.scraper', icon: Globe }, + { value: 'monitor' as AgentType, labelKey: 'agents.types.monitor', descKey: 'agents.typeDescriptions.monitor', icon: Eye }, + { value: 'custom' as AgentType, labelKey: 'agents.types.custom', descKey: 'agents.typeDescriptions.custom', icon: Settings }, + { value: 'slide-generator' as AgentType, labelKey: 'agents.types.slideGenerator', descKey: 'agents.typeDescriptions.slideGenerator', icon: Presentation }, + { value: 'excalidraw-generator' as AgentType, labelKey: 'agents.types.excalidrawGenerator', descKey: 'agents.typeDescriptions.excalidrawGenerator', icon: Pencil }, + ].map(at => { + const TypeIcon = at.icon + return ( + + ) + })} +
+
+
+
+ +
+

{t('agents.form.configuration')}

+
+
+ + {type === 'researcher' ? ( +
+ + setDescription(e.target.value)} + className={inputCls} + placeholder={t('agents.form.researchTopicPlaceholder')} + /> +
+ ) : ( +
+ + setDescription(e.target.value)} + className={inputCls} + placeholder={t('agents.form.descriptionPlaceholder')} + /> +
+ )} + + {(type === 'scraper' || type === 'custom') && ( +
+ +
+ {urls.map((url, i) => ( +
+ updateUrl(i, e.target.value)} + className={inputCls} + placeholder="https://example.com" + /> + {urls.length > 1 && ( + + )} +
+ ))} + +
+
+ )} + + {showSourceNotebook && ( +
+ + +
+ )} + + {(type === 'slide-generator' || type === 'excalidraw-generator') && sourceNotebookId && noteOptions.length > 0 && ( +
+ +
+ {noteOptions.map(note => { + const isSelected = sourceNoteIds.includes(note.id) + return ( + + ) + })} +
+ {sourceNoteIds.length > 0 && ( +

{t('agents.form.notesSelected', { count: sourceNoteIds.length })}

+ )} +
+ )} + + {type === 'slide-generator' && ( + <> +
+ + +
+
+ +
+ {(['soft', 'sharp', 'rounded', 'pill'] as const).map(s => ( + + ))} +
+
+ + )} + + {type === 'excalidraw-generator' && ( + <> +
+ +
+ {([ + { 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 => ( + + ))} +
+
+
+ +
+ {([ + { id: 'default', labelKey: 'agents.form.excalidrawDiagramStyleDefault' }, + { id: 'sketch-plus', labelKey: 'agents.form.excalidrawDiagramStyleSketchPlus' }, + { id: 'austere', labelKey: 'agents.form.excalidrawDiagramStyleAustere' }, + ] as const).map(opt => ( + + ))} +
+
+ + )} + + {type !== 'slide-generator' && type !== 'excalidraw-generator' && ( +
+ + +
+ )} + +
+ +