feat: redesign agents page (architectural-grid style), add image description, fix AI limits, remove dead code
Some checks failed
Deploy to Production / Build and Deploy (push) Failing after 53s

- 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)
This commit is contained in:
Antigravity
2026-05-09 17:18:47 +00:00
parent 79381a4cc9
commit 2fd435df6f
33 changed files with 1441 additions and 745 deletions

View File

@@ -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<string, { icon: typeof Globe; color: string; bgColor: string }> = {
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<string, { icon: typeof Globe }> = {
scraper: { icon: Globe },
researcher: { icon: Search },
monitor: { icon: Eye },
custom: { icon: Settings },
'slide-generator': { icon: Presentation },
'excalidraw-generator': { icon: Pencil },
}
const frequencyKeys: Record<string, string> = {
@@ -73,8 +61,6 @@ const frequencyKeys: Record<string, string> = {
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<ReturnType<typeof setInterval> | 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 (
<div className={`
font-display group flex flex-col bg-card rounded-lg border transition-all duration-200
${agent.isEnabled
? 'border-border/40 hover:border-primary/25 hover:shadow-[0_2px_12px_color-mix(in_oklab,var(--foreground)_7%,transparent)]'
: 'border-border/30 opacity-60'
}
`}>
{/* Card body */}
<div className="p-4 flex flex-col gap-3 flex-1">
{/* Header row: icon + name/type + toggle */}
<div className="flex items-start justify-between gap-3">
<div className="flex items-center gap-3 min-w-0">
<div className={`w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0 ${config.bgColor}`}>
<Icon className={`w-5 h-5 ${config.color}`} />
</div>
<div className="min-w-0">
<div className="flex items-center gap-1.5">
<h3 className="font-semibold text-sm text-card-foreground truncate leading-tight">{agent.name}</h3>
{mounted && isNew && (
<span className="flex-shrink-0 px-1.5 py-0.5 text-[9px] font-bold uppercase tracking-wider bg-muted text-muted-foreground rounded border border-border/60">
{t('agents.newBadge')}
</span>
)}
</div>
<span className="text-[11px] font-bold uppercase tracking-wider text-muted-foreground">
{t(`agents.types.${agent.type || 'custom'}`)}
</span>
</div>
<div
className={`
bg-card border border-border rounded-2xl p-6 space-y-6
hover:border-foreground/20 transition-all group cursor-pointer
shadow-sm relative overflow-hidden
${!agent.isEnabled ? 'opacity-50' : ''}
`}
onClick={() => onEdit(agent.id)}
>
<div className="flex items-start justify-between">
<div className="flex items-center gap-4">
<div className="p-3 bg-muted rounded-xl group-hover:bg-foreground group-hover:text-background transition-all">
<Icon className="w-5 h-5" />
</div>
{/* Toggle */}
<div className="space-y-1">
<h4 className="text-[13px] font-bold text-foreground">{agent.name}</h4>
<p className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground opacity-60">
{t(`agents.types.${agent.type || 'custom'}`)}
</p>
</div>
</div>
<div className="flex items-center gap-2" onClick={(e) => e.stopPropagation()}>
<button
onClick={handleToggle}
disabled={isToggling}
className="flex-shrink-0 disabled:opacity-50"
className="disabled:opacity-50"
title={agent.isEnabled ? t('agents.actions.toggleOff') : t('agents.actions.toggleOn')}
>
<div className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${
agent.isEnabled ? 'bg-primary' : 'bg-muted-foreground/30'
}`}>
<span className={`inline-block h-4 w-4 transform rounded-full bg-white shadow-sm transition-transform ${
agent.isEnabled ? 'translate-x-4.5' : 'translate-x-0.5'
}`} />
<div className="relative inline-flex items-center cursor-pointer">
<div className={`w-8 h-4 rounded-full transition-colors ${
agent.isEnabled ? 'bg-primary' : 'bg-muted-foreground/30'
}`}>
<span className={`absolute top-0.5 left-[2px] bg-background border border-muted-foreground/30 rounded-full h-3 w-3 transition-all ${
agent.isEnabled ? 'translate-x-4' : ''
}`} />
</div>
</div>
</button>
</div>
{/* Description */}
{agent.description && (
<p className="text-xs text-muted-foreground line-clamp-2 leading-relaxed">{agent.description}</p>
)}
{/* Meta: frequency + executions */}
<div className="flex items-center gap-3 text-xs text-muted-foreground">
<span className="flex items-center gap-1">
<Clock className="w-3 h-3" />
{t(frequencyKeys[agent.frequency] || 'agents.frequencies.manual')}
</span>
<span>·</span>
<span>{t('agents.metadata.executions', { count: agent._count.actions })}</span>
{agent.notebook && (
<>
<span>·</span>
<span className="flex items-center gap-1">
{(() => {
const NbIcon = getNotebookIcon(agent.notebook!.icon)
return <NbIcon className="w-3 h-3" />
})()}
{agent.notebook.name}
</span>
</>
)}
</div>
</div>
{/* Footer: Next Run + Last Status */}
<div className="border-t border-border/30 grid grid-cols-2 divide-x divide-border/30">
<div className="px-4 py-2.5">
<p className="text-[10px] text-muted-foreground font-medium mb-0.5">{t('agents.status.nextRun')}</p>
<p className="text-xs font-semibold text-foreground flex items-center gap-1">
<Clock className="w-3 h-3 text-muted-foreground/60" />
{nextRunLabel}
</p>
</div>
<div className="px-4 py-2.5">
<p className="text-[10px] text-muted-foreground font-medium mb-0.5">{t('agents.status.lastStatus')}</p>
{lastAction ? (
<span className={`inline-flex items-center gap-1.5 text-xs font-semibold ${
lastAction.status === 'success' ? 'text-primary' :
lastAction.status === 'failure' ? 'text-destructive' :
lastAction.status === 'running' ? 'text-primary' :
'text-muted-foreground'
}`}>
{lastAction.status === 'success' && <CheckCircle2 className="w-3 h-3" />}
{lastAction.status === 'failure' && <XCircle className="w-3 h-3" />}
{lastAction.status === 'running' && <Loader2 className="w-3 h-3 animate-spin" />}
{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 && (
<p className="text-xs text-muted-foreground leading-relaxed line-clamp-3">
{agent.description}
</p>
)}
<div className="space-y-3">
<div className="flex items-center justify-between text-[10px] text-muted-foreground font-medium">
<div className="flex items-center gap-4">
<span className="flex items-center gap-1">
<Clock className="w-2.5 h-2.5" />
{t(frequencyKeys[agent.frequency] || 'agents.frequencies.manual')}
</span>
) : (
<span className="text-xs text-muted-foreground"></span>
)}
<span>{t('agents.metadata.executions', { count: agent._count.actions })}</span>
</div>
</div>
<div className="flex items-center justify-between text-[10px] text-muted-foreground font-medium">
<div className="flex items-center gap-2">
<span className="uppercase tracking-tight">{t('agents.status.nextRun')}</span>
<span className="text-foreground">{nextRunLabel}</span>
</div>
<div className="flex items-center gap-2">
<span className="uppercase tracking-tight">{t('agents.status.lastStatus')}</span>
{lastAction ? (
<span className={`flex items-center gap-1 ${
lastAction.status === 'success' ? 'text-primary'
: lastAction.status === 'failure' ? 'text-destructive'
: lastAction.status === 'running' ? 'text-primary'
: 'text-muted-foreground'
}`}>
{lastAction.status === 'success' && <Activity className="w-2 h-2" />}
{lastAction.status === 'failure' && <XCircle className="w-2 h-2" />}
{lastAction.status === 'running' && <Loader2 className="w-2 h-2 animate-spin" />}
{statusLabel}
</span>
) : (
<span className="text-muted-foreground"></span>
)}
</div>
</div>
</div>
{/* Actions row */}
<div className="border-t border-border/30 flex items-center px-4 py-2 gap-2">
<div className="grid grid-cols-3 gap-2 border-t border-border pt-4" onClick={(e) => e.stopPropagation()}>
<button
onClick={() => onEdit(agent.id)}
className="flex-1 flex items-center justify-center gap-1.5 px-3 py-1.5 text-xs font-medium text-muted-foreground bg-muted/40 hover:bg-muted/80 rounded-md transition-colors"
className="py-2 border border-border rounded-lg hover:bg-muted flex items-center justify-center transition-colors text-muted-foreground hover:text-foreground"
>
<Pencil className="w-3 h-3" />
{t('agents.actions.edit')}
<Pencil className="w-3.5 h-3.5" />
<span className="ml-2 text-[10px] font-bold uppercase">{t('agents.actions.edit')}</span>
</button>
<button
onClick={handleRun}
disabled={isRunning || !agent.isEnabled}
className="p-1.5 text-primary bg-primary/10 rounded-md hover:bg-primary/20 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
title={t('agents.actions.run')}
className="py-2 border border-border rounded-lg hover:bg-muted flex items-center justify-center transition-colors text-muted-foreground hover:text-foreground disabled:opacity-40 disabled:cursor-not-allowed"
>
{isRunning ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Play className="w-3.5 h-3.5" />}
{isRunning ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Play className="w-3.5 h-3.5 fill-current" />}
</button>
<button
onClick={handleDelete}
disabled={isDeleting}
className="p-1.5 text-destructive bg-destructive/10 rounded-md hover:bg-destructive/20 transition-colors disabled:opacity-40"
title={t('agents.actions.delete')}
className="py-2 border border-border rounded-lg hover:bg-destructive/10 hover:text-destructive hover:border-destructive/20 flex items-center justify-center transition-colors text-muted-foreground disabled:opacity-40"
>
{isDeleting ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Trash2 className="w-3.5 h-3.5" />}
</button>

View File

@@ -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>
<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>
)
}
const typeIcons: Record<string, typeof Globe> = {
scraper: Globe,
researcher: Search,
monitor: Eye,
custom: Settings,
'slide-generator': Presentation,
'excalidraw-generator': Pencil,
}
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'],
}
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<void>
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<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 */ }
}
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 (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="min-h-full flex flex-col"
>
<header className="px-12 py-10 border-b border-border bg-background/80 backdrop-blur-md sticky top-0 z-30">
<div className="flex items-center justify-between max-w-5xl mx-auto">
<button
onClick={onBack}
className="flex items-center gap-3 text-xs font-bold uppercase tracking-widest text-muted-foreground hover:text-foreground transition-colors"
>
<ArrowLeft className="w-4 h-4" />
{t('agents.form.back')}
</button>
<div className="flex items-center gap-3">
<button
onClick={onOpenHelp}
className="p-2 text-muted-foreground hover:text-foreground transition-colors"
title={t('agents.help.title')}
>
<LifeBuoy className="w-4 h-4" />
</button>
{!isNew && agent && (
<button
onClick={() => onOpenLogs(agent.id, agent.name)}
className="px-5 py-2 text-xs font-bold uppercase tracking-widest border border-border rounded-xl hover:bg-muted transition-all"
>
{t('agents.runLog.title')}
</button>
)}
<button
form="agent-config-form"
type="submit"
disabled={isSaving}
className="px-6 py-2 bg-foreground text-background text-xs font-bold uppercase tracking-widest rounded-xl hover:opacity-90 transition-all shadow-lg shadow-foreground/10 disabled:opacity-50 flex items-center gap-2"
>
{isSaving && <Loader2 className="w-3.5 h-3.5 animate-spin" />}
{isNew ? t('agents.form.create') : t('agents.form.save')}
</button>
</div>
</div>
</header>
<div className="flex-1 px-12 py-16 max-w-5xl mx-auto w-full space-y-16">
<section className="space-y-8">
<div className="flex items-end justify-between">
<div className="space-y-4">
<div className="flex items-center gap-3">
<div className="p-4 bg-foreground text-background rounded-2xl shadow-xl shadow-foreground/10">
<Icon className="w-8 h-8" />
</div>
<div className="space-y-1">
<input
type="text"
value={name}
onChange={e => 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')}
/>
<div className="flex items-center gap-3">
<span className="text-[10px] font-bold uppercase tracking-widest px-2 py-1 bg-muted text-muted-foreground rounded-md">
{isNew ? t('agents.newBadge') : `ID: ${agent?.id?.slice(0, 8)}`}
</span>
{!isNew && agent?.isEnabled && (
<span className="text-[10px] font-bold uppercase tracking-widest px-2 py-1 bg-primary/10 text-primary rounded-md">
{t('agents.actions.toggleOn')}
</span>
)}
</div>
</div>
</div>
</div>
{!isNew && agent && (
<div className="flex items-center gap-8 text-[12px] uppercase tracking-[0.2em] font-bold text-muted-foreground">
<div className="flex flex-col items-end gap-1">
<span className="opacity-40">{t('agents.status.nextRun')}</span>
<span className="text-foreground">{t('agents.metadata.executions', { count: agent._count.actions })}</span>
</div>
{successRate !== null && (
<div className="flex flex-col items-end gap-1">
<span className="opacity-40">Succès</span>
<span className="text-primary">{successRate}%</span>
</div>
)}
</div>
)}
</div>
</section>
<form id="agent-config-form" onSubmit={handleSubmit}>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-12">
<div className="lg:col-span-2 space-y-12">
<section className="space-y-6">
<h3 className={sectionTitleCls}>{t('agents.form.agentType')}<FieldHelp tooltip={t('agents.help.tooltips.agentType')} /></h3>
<div className={cardCls}>
<div className="p-8 space-y-8">
<div className="grid grid-cols-2 gap-4">
{[
{ 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 (
<button
key={at.value}
type="button"
onClick={() => setType(at.value)}
className={`p-4 rounded-2xl flex items-center justify-between transition-all cursor-pointer ${
type === at.value
? 'border-2 border-foreground bg-muted'
: 'border border-border hover:border-foreground/20'
}`}
>
<div className="flex items-center gap-3">
<TypeIcon className={`w-4 h-4 ${type === at.value ? 'text-foreground' : 'text-muted-foreground'}`} />
<div className="text-left">
<div className="text-sm font-medium text-foreground">{t(at.labelKey)}</div>
<div className="text-[10px] text-muted-foreground mt-0.5">{t(at.descKey)}</div>
</div>
</div>
<div className={`w-5 h-5 rounded-full flex items-center justify-center ${
type === at.value
? 'border-4 border-foreground'
: 'border border-border'
}`}>
{type === at.value && <div className="w-2 h-2 bg-foreground rounded-full" />}
</div>
</button>
)
})}
</div>
</div>
</div>
</section>
<section className="space-y-6">
<h3 className={sectionTitleCls}>{t('agents.form.configuration')}<FieldHelp tooltip={t('agents.help.tooltips.instructions')} /></h3>
<div className={cardCls}>
<div className="p-8 space-y-8">
{type === 'researcher' ? (
<div className="space-y-4">
<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>
) : (
<div className="space-y-4">
<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>
)}
{(type === 'scraper' || type === 'custom') && (
<div className="space-y-4">
<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-foreground hover:opacity-60 font-medium"
>
<Plus className="w-3.5 h-3.5" />
{t('agents.form.addUrl')}
</button>
</div>
</div>
)}
{showSourceNotebook && (
<div className="space-y-4">
<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>
)}
{(type === 'slide-generator' || type === 'excalidraw-generator') && sourceNotebookId && noteOptions.length > 0 && (
<div className="space-y-4">
<label className={labelCls}>{t('agents.form.selectNotes')}<FieldHelp tooltip={t('agents.help.tooltips.selectNotes')} /></label>
<div className="border border-border rounded-xl max-h-48 overflow-y-auto bg-muted/30">
{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-4 py-2.5 text-sm text-left hover:bg-muted/50 transition-colors ${isSelected ? 'bg-foreground/5' : ''}`}
>
<div className={`w-4 h-4 rounded border-2 flex items-center justify-center flex-shrink-0 ${isSelected ? 'border-foreground bg-foreground' : 'border-border'}`}>
{isSelected && <Check className="w-3 h-3 text-background" />}
</div>
<span className={isSelected ? 'text-foreground font-medium' : 'text-foreground'}>{note.title}</span>
</button>
)
})}
</div>
{sourceNoteIds.length > 0 && (
<p className="text-xs text-muted-foreground">{t('agents.form.notesSelected', { count: sourceNoteIds.length })}</p>
)}
</div>
)}
{type === 'slide-generator' && (
<>
<div className="space-y-4">
<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 className="space-y-4">
<label className={labelCls}>{t('agents.form.slideStyle')}<FieldHelp tooltip={t('agents.help.tooltips.slideStyle')} /></label>
<div className="grid grid-cols-2 gap-4">
{(['soft', 'sharp', 'rounded', 'pill'] as const).map(s => (
<button
key={s}
type="button"
onClick={() => setSlideStyle(s)}
className={`p-4 rounded-2xl flex items-center justify-between transition-all cursor-pointer ${
slideStyle === s
? 'border-2 border-foreground bg-muted'
: 'border border-border hover:border-foreground/20'
}`}
>
<span className={`text-sm font-medium ${slideStyle === s ? 'text-foreground' : 'text-muted-foreground'}`}>
{t(`agents.form.slideStyle${s.charAt(0).toUpperCase() + s.slice(1)}` as any)}
</span>
<div className={`w-5 h-5 rounded-full flex items-center justify-center ${slideStyle === s ? 'border-4 border-foreground' : 'border border-border'}`}>
{slideStyle === s && <div className="w-2 h-2 bg-foreground rounded-full" />}
</div>
</button>
))}
</div>
</div>
</>
)}
{type === 'excalidraw-generator' && (
<>
<div className="space-y-4">
<label className={labelCls}>{t('agents.form.excalidrawDiagramType')}</label>
<div className="grid grid-cols-2 gap-3">
{([
{ 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={`p-3 rounded-xl text-sm text-left transition-all ${
excalidrawType === opt.id
? 'border-2 border-foreground bg-muted text-foreground font-medium'
: 'border border-border text-muted-foreground hover:border-foreground/20'
}`}
>
{t(opt.labelKey)}
</button>
))}
</div>
</div>
<div className="space-y-4">
<label className={labelCls}>{t('agents.form.excalidrawDiagramStyle')}</label>
<div className="grid grid-cols-3 gap-3">
{([
{ 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={`p-3 rounded-xl text-sm text-left transition-all ${
excalidrawStyle === opt.id
? 'border-2 border-foreground bg-muted text-foreground font-medium'
: 'border border-border text-muted-foreground hover:border-foreground/20'
}`}
>
{t(opt.labelKey)}
</button>
))}
</div>
</div>
</>
)}
{type !== 'slide-generator' && type !== 'excalidraw-generator' && (
<div className="space-y-4">
<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>
)}
<div className="space-y-4">
<label className={labelCls}>{t('agents.form.instructions')}<FieldHelp tooltip={t('agents.help.tooltips.instructions')} /></label>
<textarea
value={role}
onChange={e => setRole(e.target.value)}
rows={6}
className="w-full bg-muted/50 border border-border rounded-2xl p-6 text-sm outline-none focus:ring-1 focus:ring-foreground/10 focus:border-foreground/20 transition-all font-light leading-relaxed resize-none text-foreground"
placeholder={t('agents.form.instructionsPlaceholder')}
/>
</div>
</div>
</div>
</section>
{type !== 'slide-generator' && type !== 'excalidraw-generator' && (
<section className="space-y-6">
<button
type="button"
onClick={() => setShowAdvanced(!showAdvanced)}
className="flex items-center gap-2 text-xs text-muted-foreground hover:text-foreground font-bold uppercase tracking-widest w-full pt-2"
>
{showAdvanced ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
{t('agents.form.advancedMode')}
</button>
{showAdvanced && (
<div className={cardCls}>
<div className="p-8 space-y-8">
<div className="space-y-4">
<label className={labelCls}>{t('agents.tools.title')}<FieldHelp tooltip={t('agents.help.tooltips.tools')} /></label>
<div className="grid grid-cols-2 gap-3">
{availableTools.map(at => {
const ToolIcon = 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.5 px-4 py-3 rounded-xl text-sm transition-all text-left ${
isSelected
? 'border-2 border-foreground bg-muted text-foreground font-medium'
: 'border border-border text-muted-foreground hover:border-foreground/20'
}`}
>
<ToolIcon className="w-4 h-4 flex-shrink-0" />
<span>{t(at.labelKey)}</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>
{selectedTools.length > 0 && (
<div className="space-y-4">
<label className={labelCls}>
{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={50}
value={maxSteps}
onChange={e => setMaxSteps(Number(e.target.value))}
className="w-full accent-foreground"
/>
<div className="flex justify-between text-xs text-muted-foreground">
<span>3</span>
<span>50</span>
</div>
</div>
)}
</div>
</div>
)}
</section>
)}
</div>
<div className="space-y-10">
<section className="space-y-6">
<h3 className={sectionTitleCls}>{t('agents.form.frequency')}<FieldHelp tooltip={t('agents.help.tooltips.frequency')} /></h3>
<div className="bg-foreground rounded-3xl p-8 space-y-8 text-background shadow-2xl shadow-foreground/20 relative overflow-hidden">
<div className="absolute top-0 right-0 p-4 opacity-10">
<Clock className="w-24 h-24" />
</div>
<div className="relative space-y-6">
<div className="space-y-2">
<span className="text-[10px] uppercase tracking-widest font-bold opacity-60">{t('agents.form.frequency')}</span>
<select
value={frequency}
onChange={e => setFrequency(e.target.value)}
className="w-full bg-white/10 border border-white/20 rounded-xl px-4 py-3 text-lg font-memento-serif italic text-background outline-none cursor-pointer appearance-none"
>
<option value="manual" className="text-foreground">{t('agents.frequencies.manual')}</option>
<option value="hourly" className="text-foreground">{t('agents.frequencies.hourly')}</option>
<option value="daily" className="text-foreground">{t('agents.frequencies.daily')}</option>
<option value="weekly" className="text-foreground">{t('agents.frequencies.weekly')}</option>
<option value="monthly" className="text-foreground">{t('agents.frequencies.monthly')}</option>
</select>
</div>
{frequency !== 'manual' && frequency !== 'hourly' && (
<>
<div className="h-px bg-white/10" />
<div className="space-y-4">
{frequency === 'weekly' && (
<div className="space-y-2">
<span className="text-[10px] uppercase tracking-widest font-bold opacity-60">{t('agents.schedule.dayOfWeek')}</span>
<select
value={scheduledDay}
onChange={e => setScheduledDay(Number(e.target.value))}
className="w-full bg-white/10 border border-white/20 rounded-xl px-4 py-3 text-sm font-bold text-background outline-none cursor-pointer appearance-none"
>
{[
{ 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} className="text-foreground">{d.label}</option>
))}
</select>
</div>
)}
{frequency === 'monthly' && (
<div className="space-y-2">
<span className="text-[10px] uppercase tracking-widest font-bold opacity-60">{t('agents.schedule.dayOfMonth')}</span>
<select
value={scheduledDay}
onChange={e => setScheduledDay(Number(e.target.value))}
className="w-full bg-white/10 border border-white/20 rounded-xl px-4 py-3 text-sm font-bold text-background outline-none cursor-pointer appearance-none"
>
{Array.from({ length: 31 }, (_, i) => i + 1).map(d => (
<option key={d} value={d} className="text-foreground">{d}</option>
))}
</select>
</div>
)}
<div className="space-y-2">
<span className="text-[10px] uppercase tracking-widest font-bold opacity-60">{t('agents.schedule.time')}</span>
<input
type="time"
value={scheduledTime}
onChange={e => setScheduledTime(e.target.value)}
className="w-full bg-white/10 border border-white/20 rounded-xl px-4 py-3 text-sm font-bold text-background outline-none"
/>
</div>
</div>
</>
)}
</div>
</div>
</section>
<section className="space-y-6">
<h3 className={sectionTitleCls}>{t('agents.form.options')}<FieldHelp tooltip={t('agents.help.tooltips.targetNotebook')} /></h3>
<div className="space-y-3">
{type !== 'slide-generator' && type !== 'excalidraw-generator' && (
<div
onClick={() => setNotifyEmail(!notifyEmail)}
className={`flex items-center justify-between p-4 rounded-2xl border cursor-pointer transition-all ${
notifyEmail ? 'bg-muted border-foreground/20' : 'bg-card border-border/50'
}`}
>
<div className="flex items-center gap-3">
<Mail className={`w-4 h-4 ${notifyEmail ? 'text-foreground' : 'text-muted-foreground'}`} />
<div>
<p className="text-xs font-bold text-foreground">{t('agents.form.notifyEmail')}</p>
<p className="text-[10px] text-muted-foreground">{t('agents.form.notifyEmailHint')}</p>
</div>
</div>
<div className={`relative inline-flex items-center`}>
<div className={`w-8 h-4 rounded-full transition-colors ${notifyEmail ? 'bg-foreground' : 'bg-muted-foreground/30'}`}>
<span className={`absolute top-0.5 left-[2px] bg-background border border-border rounded-full h-3 w-3 transition-all ${notifyEmail ? 'translate-x-4' : ''}`} />
</div>
</div>
</div>
)}
{type !== 'slide-generator' && type !== 'excalidraw-generator' && (
<div
onClick={() => setIncludeImages(!includeImages)}
className={`flex items-center justify-between p-4 rounded-2xl border cursor-pointer transition-all ${
includeImages ? 'bg-muted border-foreground/20' : 'bg-card border-border/50'
}`}
>
<div className="flex items-center gap-3">
<ImageIcon className={`w-4 h-4 ${includeImages ? 'text-foreground' : 'text-muted-foreground'}`} />
<div>
<p className="text-xs font-bold text-foreground">{t('agents.form.includeImages')}</p>
<p className="text-[10px] text-muted-foreground">{t('agents.form.includeImagesHint')}</p>
</div>
</div>
<div className={`relative inline-flex items-center`}>
<div className={`w-8 h-4 rounded-full transition-colors ${includeImages ? 'bg-foreground' : 'bg-muted-foreground/30'}`}>
<span className={`absolute top-0.5 left-[2px] bg-background border border-border rounded-full h-3 w-3 transition-all ${includeImages ? 'translate-x-4' : ''}`} />
</div>
</div>
</div>
)}
</div>
</section>
{!isNew && (
<section className="pt-6">
<button
type="button"
onClick={async () => {
if (!agent || !confirm(t('agents.actions.deleteConfirm', { name: agent.name }))) return
try {
const { deleteAgent } = await import('@/app/actions/agent-actions')
await deleteAgent(agent.id)
toast.success(t('agents.toasts.deleted', { name: agent.name }))
onBack()
} catch {
toast.error(t('agents.toasts.deleteError'))
}
}}
className="w-full py-4 border border-destructive/30 text-destructive bg-destructive/5 rounded-2xl text-xs font-bold uppercase tracking-widest hover:bg-destructive/10 transition-colors flex items-center justify-center gap-3"
>
<Trash2 className="w-4 h-4" />
{t('agents.actions.delete')}
</button>
</section>
)}
</div>
</div>
</form>
</div>
</motion.div>
)
}

View File

@@ -1,15 +1,11 @@
'use client'
/**
* Agent Run Log
* Shows execution history for an agent.
*/
import { useState, useEffect } from 'react'
import { X, CheckCircle2, XCircle, Loader2, Clock, ChevronDown, Wrench } from 'lucide-react'
import { X, CheckCircle2, XCircle, Loader2, Clock, ChevronDown, Wrench, Trash2 } from 'lucide-react'
import { formatDistanceToNow } from 'date-fns'
import { fr } from 'date-fns/locale/fr'
import { enUS } from 'date-fns/locale/en-US'
import { toast } from 'sonner'
import { useLanguage } from '@/lib/i18n'
interface AgentRunLogProps {
@@ -47,22 +43,38 @@ export function AgentRunLog({ agentId, agentName, onClose }: AgentRunLogProps) {
const { t, language } = useLanguage()
const [actions, setActions] = useState<Action[]>([])
const [loading, setLoading] = useState(true)
const [isDeleting, setIsDeleting] = useState(false)
const dateLocale = language === 'fr' ? fr : enUS
useEffect(() => {
async function load() {
try {
const { getAgentActions } = await import('@/app/actions/agent-actions')
const data = await getAgentActions(agentId)
setActions(data)
} catch {
// Silent fail
} finally {
setLoading(false)
}
const loadActions = async () => {
setLoading(true)
try {
const { getAgentActions } = await import('@/app/actions/agent-actions')
const data = await getAgentActions(agentId)
setActions(data)
} catch {
// Silent fail
} finally {
setLoading(false)
}
load()
}, [agentId])
}
useEffect(() => { loadActions() }, [agentId])
const handleClearHistory = async () => {
if (!confirm(t('agents.runLog.clearConfirm'))) return
setIsDeleting(true)
try {
const { deleteAgentHistory } = await import('@/app/actions/agent-actions')
await deleteAgentHistory(agentId)
setActions([])
toast.success(t('agents.runLog.cleared'))
} catch {
toast.error(t('agents.toasts.deleteError'))
} finally {
setIsDeleting(false)
}
}
return (
<div className="fixed inset-0 bg-black/20 flex justify-end z-50" onClick={onClose}>
@@ -70,18 +82,28 @@ export function AgentRunLog({ agentId, agentName, onClose }: AgentRunLogProps) {
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 */}
<div className="flex items-center justify-between px-5 py-4 border-b border-border">
<div>
<h3 className="font-semibold text-card-foreground">{t('agents.runLog.title')}</h3>
<p className="text-xs text-muted-foreground">{agentName}</p>
</div>
<button onClick={onClose} className="p-1 rounded-md hover:bg-accent">
<X className="w-5 h-5 text-muted-foreground" />
</button>
<div className="flex items-center gap-2">
{actions.length > 0 && (
<button
onClick={handleClearHistory}
disabled={isDeleting}
className="p-1.5 rounded-md hover:bg-destructive/10 text-muted-foreground hover:text-destructive transition-colors disabled:opacity-50"
title={t('agents.runLog.clearHistory')}
>
{isDeleting ? <Loader2 className="w-4 h-4 animate-spin" /> : <Trash2 className="w-4 h-4" />}
</button>
)}
<button onClick={onClose} className="p-1 rounded-md hover:bg-accent">
<X className="w-5 h-5 text-muted-foreground" />
</button>
</div>
</div>
{/* List */}
<div className="flex-1 overflow-y-auto p-4 space-y-2">
{loading && (
<div className="flex items-center justify-center py-8">
@@ -134,7 +156,6 @@ export function AgentRunLog({ agentId, agentName, onClose }: AgentRunLogProps) {
</div>
</div>
{/* Tool trace */}
{toolSteps.length > 0 && (
<details className="mt-2">
<summary className="flex items-center gap-1.5 text-xs text-primary cursor-pointer hover:text-primary/80 font-medium">
@@ -152,7 +173,7 @@ export function AgentRunLog({ agentId, agentName, onClose }: AgentRunLogProps) {
<div key={j} className="bg-muted rounded px-2 py-1">
<span className="font-mono text-primary">{tc.toolName}</span>
<span className="text-muted-foreground ml-1">
{JSON.stringify(tc.args).substring(0, 80)}
{JSON.stringify(tc.args ?? {}).substring(0, 80)}
</span>
</div>
))}

View File

@@ -1,10 +1,5 @@
'use client'
/**
* Agent Templates Gallery
* Pre-built agent configurations that users can install in one click.
*/
import { useState } from 'react'
import {
Globe,
@@ -58,8 +53,6 @@ const typeIcons: Record<string, typeof Globe> = {
'excalidraw-generator': Pencil,
}
const templateIconBox = 'bg-primary/10 text-primary dark:bg-primary/15'
export function AgentTemplates({ onInstalled, existingAgentNames }: AgentTemplatesProps) {
const { t } = useLanguage()
const [installingId, setInstallingId] = useState<string | null>(null)
@@ -106,50 +99,43 @@ export function AgentTemplates({ onInstalled, existingAgentNames }: AgentTemplat
}
return (
<div>
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">
{t('agents.templates.title')}
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
{templateConfig.map(tpl => {
const Icon = typeIcons[tpl.type] || Settings
const isInstalling = installingId === tpl.id
const nameKey = `agents.templates.${tpl.id}.name`
const descKey = `agents.templates.${tpl.id}.description`
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{templateConfig.map(tpl => {
const Icon = typeIcons[tpl.type] || Settings
const isInstalling = installingId === tpl.id
const nameKey = `agents.templates.${tpl.id}.name`
const descKey = `agents.templates.${tpl.id}.description`
return (
<div
key={tpl.id}
className="border-2 border-dashed border-border/70 rounded-xl p-4 hover:border-primary/35 hover:bg-primary/[0.03] transition-all group"
>
<div className="flex items-center gap-2.5 mb-2">
<div className={`p-1.5 rounded-lg ${templateIconBox}`}>
<Icon className="w-4 h-4" />
</div>
<h4 className="font-medium text-sm text-foreground">{t(nameKey)}</h4>
</div>
<p className="text-xs text-muted-foreground mb-3 line-clamp-2">{t(descKey)}</p>
<button
onClick={() => handleInstall(tpl)}
disabled={isInstalling}
className="flex items-center gap-1.5 text-xs font-medium text-primary hover:text-primary/80 transition-colors disabled:opacity-50"
>
{isInstalling ? (
<>
<Loader2 className="w-3.5 h-3.5 animate-spin" />
{t('agents.templates.installing')}
</>
) : (
<>
<Plus className="w-3.5 h-3.5" />
{t('agents.templates.install')}
</>
)}
</button>
return (
<div
key={tpl.id}
className="bg-card/40 border border-dashed border-border rounded-2xl p-6 group cursor-pointer hover:bg-card hover:border-foreground/20 transition-all"
>
<div className="w-8 h-8 rounded-lg bg-muted flex items-center justify-center text-muted-foreground group-hover:bg-foreground group-hover:text-background mb-4 transition-all">
<Icon className="w-4 h-4" />
</div>
)
})}
</div>
<h4 className="text-[13px] font-bold text-foreground mb-2">{t(nameKey)}</h4>
<p className="text-xs text-muted-foreground leading-relaxed mb-4">{t(descKey)}</p>
<button
onClick={() => handleInstall(tpl)}
disabled={isInstalling}
className="text-[11px] font-bold uppercase tracking-widest text-foreground hover:opacity-60 transition-opacity flex items-center gap-2 disabled:opacity-50"
>
{isInstalling ? (
<>
<Loader2 className="w-3.5 h-3.5 animate-spin" />
{t('agents.templates.installing')}
</>
) : (
<>
<Plus className="w-3.5 h-3.5" />
{t('agents.templates.install')}
</>
)}
</button>
</div>
)
})}
</div>
)
}