'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' import { enUS } from 'date-fns/locale/en-US' import { Play, Trash2, Loader2, Globe, Search, Eye, Settings, CheckCircle2, XCircle, Clock, Pencil, 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 name: string description?: string | null type?: string | null isEnabled: boolean frequency: string lastRun: string | Date | null nextRun?: string | Date | null createdAt: string | Date updatedAt: string | Date _count: { actions: number } actions: { id: string; status: string; createdAt: string | Date }[] notebook?: { id: string; name: string; icon?: string | null } | null } onEdit: (id: string) => void onRefresh: () => void 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 frequencyKeys: Record = { manual: 'agents.frequencies.manual', hourly: 'agents.frequencies.hourly', daily: 'agents.frequencies.daily', weekly: 'agents.frequencies.weekly', monthly: 'agents.frequencies.monthly', } // --- Component --- export function AgentCard({ agent, onEdit, onRefresh, onToggle }: AgentCardProps) { const { t, language } = useLanguage() const [isRunning, setIsRunning] = useState(false) const [isDeleting, setIsDeleting] = useState(false) const [isToggling, setIsToggling] = useState(false) const [mounted, setMounted] = useState(false) useEffect(() => { setMounted(true) }, []) const config = typeConfig[agent.type || 'scraper'] || typeConfig.custom 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 () => { setIsRunning(true) const toastId = toast.loading( t('agents.toasts.running') || `Agent "${agent.name}" en cours…`, { description: t('agents.toasts.runningDesc') || 'La génération peut prendre quelques minutes.', duration: Infinity } ) try { const { runAgent } = await import('@/app/actions/agent-actions') const result = await runAgent(agent.id) if (!result.success) { toast.error(t('agents.toasts.runError', { error: result.error || t('agents.toasts.runFailed') }), { id: toastId }) setIsRunning(false) return } // Poll status every 3 s until terminal state if (pollRef.current) clearInterval(pollRef.current) pollRef.current = setInterval(async () => { try { const res = await fetch(`/api/agents/run-for-note?agentId=${agent.id}`) const data = await res.json() if (data.status === 'success') { clearInterval(pollRef.current!) pollRef.current = null setIsRunning(false) toast.success(t('agents.toasts.runSuccess', { name: agent.name }), { id: toastId, duration: 6000 }) onRefresh() } else if (data.status === 'failure') { clearInterval(pollRef.current!) pollRef.current = null setIsRunning(false) toast.error(t('agents.toasts.runError', { error: data.error || t('agents.toasts.runFailed') }), { id: toastId }) onRefresh() } } catch { /* network error — keep polling */ } }, 3000) } catch { toast.error(t('agents.toasts.runGenericError'), { id: toastId }) setIsRunning(false) } } const handleDelete = async () => { if (!confirm(t('agents.actions.deleteConfirm', { name: agent.name }))) return setIsDeleting(true) try { const { deleteAgent } = await import('@/app/actions/agent-actions') await deleteAgent(agent.id) toast.success(t('agents.toasts.deleted', { name: agent.name })) } catch { toast.error(t('agents.toasts.deleteError')) } finally { setIsDeleting(false) onRefresh() } } const handleToggle = async () => { const newEnabled = !agent.isEnabled setIsToggling(true) onToggle(agent.id, newEnabled) try { const { toggleAgent } = await import('@/app/actions/agent-actions') await toggleAgent(agent.id, newEnabled) toast.success(newEnabled ? t('agents.actions.toggleOn') : t('agents.actions.toggleOff')) } catch { onToggle(agent.id, !newEnabled) toast.error(t('agents.toasts.toggleError')) } finally { setIsToggling(false) } } // Derive "Next Run" label const nextRunLabel = (() => { if (!agent.isEnabled) return '—' if (agent.frequency === 'manual') return t('agents.frequencies.manual') if (agent.nextRun && new Date(agent.nextRun) > new Date()) { if (!mounted) return '...' return formatDistanceToNow(new Date(agent.nextRun), { addSuffix: true, locale: dateLocale }) } return t(frequencyKeys[agent.frequency] || 'agents.frequencies.manual') })() return (
{/* Card body */}
{/* Header row: icon + name/type + toggle */}

{agent.name}

{mounted && isNew && ( {t('agents.newBadge')} )}
{t(`agents.types.${agent.type || 'custom'}`)}
{/* Toggle */}
{/* 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')} ) : ( )}
{/* Actions row */}
) }