Files
Momento/memento-note/components/agents/agent-card.tsx
sepehr 153c921960
Some checks failed
Deploy to Production / Build and Deploy (push) Failing after 1m7s
fix: comprehensive i18n — replace hardcoded French/English strings with t() calls
Replaced ~100+ hardcoded French and English text strings across 30+ components
with proper i18n t() calls. Added 57 new translation keys to all 15 locale files
(ar, de, en, es, fa, fr, hi, it, ja, ko, nl, pl, pt, ru, zh).

Key changes:
- contextual-ai-chat.tsx: 30 French strings → t() (actions, toasts, labels, placeholders)
- ai-chat.tsx: 15 French/English strings → t() (header, tabs, welcome, insights, history)
- note-inline-editor.tsx: 20 French fallbacks removed (toolbar, save status, checklist)
- lab-skeleton.tsx: French loading text → t()
- admin-header.tsx, header.tsx, editor-connections-section.tsx: French fallbacks removed
- New AI chat component, agent cards, sidebar, settings panel i18n cleanup

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 21:14:45 +02:00

284 lines
11 KiB
TypeScript

'use client'
/**
* Agent Card Component
* Compact card matching the reference design — with a "Next Run / Status" footer.
*/
import { useState, useEffect } 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,
} 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 ---
const typeConfig: Record<string, { icon: typeof Globe; color: string; bgColor: string; label: string }> = {
scraper: { icon: Globe, color: 'text-blue-600 dark:text-blue-400', bgColor: 'bg-blue-50 dark:bg-blue-950', label: 'Veilleur' },
researcher: { icon: Search, color: 'text-violet-600 dark:text-violet-400', bgColor: 'bg-violet-50 dark:bg-violet-950', label: 'Chercheur' },
monitor: { icon: Eye, color: 'text-amber-600 dark:text-amber-400', bgColor: 'bg-amber-50 dark:bg-amber-950', label: 'Surveillant' },
custom: { icon: Settings, color: 'text-emerald-600 dark:text-emerald-400', bgColor: 'bg-emerald-50 dark:bg-emerald-950', label: 'Personnalisé' },
}
const frequencyKeys: Record<string, string> = {
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 handleRun = async () => {
setIsRunning(true)
try {
const { runAgent } = await import('@/app/actions/agent-actions')
const result = await runAgent(agent.id)
if (result.success) {
toast.success(t('agents.toasts.runSuccess', { name: agent.name }))
} else {
toast.error(t('agents.toasts.runError', { error: result.error || t('agents.toasts.runFailed') }))
}
} catch {
toast.error(t('agents.toasts.runGenericError'))
} finally {
setIsRunning(false)
onRefresh()
}
}
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 (
<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/30 hover:shadow-[0_2px_12px_rgba(0,91,193,0.08)]'
: '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-emerald-100 dark:bg-emerald-900 text-emerald-700 dark:text-emerald-300 rounded">
{t('agents.newBadge')}
</span>
)}
</div>
<span className={`text-[11px] font-bold uppercase tracking-wider ${config.color}`}>
{t(`agents.types.${agent.type || 'custom'}`)}
</span>
</div>
</div>
{/* Toggle */}
<button
onClick={handleToggle}
disabled={isToggling}
className="flex-shrink-0 disabled:opacity-50"
title={agent.isEnabled ? 'Désactiver' : 'Activer'}
>
<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>
</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">Prochaine exéc.</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">Dernier statut</p>
{lastAction ? (
<span className={`inline-flex items-center gap-1.5 text-xs font-semibold ${
lastAction.status === 'success' ? 'text-emerald-600 dark:text-emerald-400' :
lastAction.status === 'failure' ? 'text-red-600 dark:text-red-400' :
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' && 'Réussi'}
{lastAction.status === 'failure' && 'Échec'}
{lastAction.status === 'running' && 'En cours'}
{lastAction.status === 'pending' && 'En attente'}
</span>
) : (
<span className="text-xs text-muted-foreground"></span>
)}
</div>
</div>
{/* Actions row */}
<div className="border-t border-border/30 flex items-center px-4 py-2 gap-2">
<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"
>
<Pencil className="w-3 h-3" />
{t('agents.actions.edit')}
</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')}
>
{isRunning ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Play className="w-3.5 h-3.5" />}
</button>
<button
onClick={handleDelete}
disabled={isDeleting}
className="p-1.5 text-red-500 bg-red-50 dark:bg-red-950/20 rounded-md hover:bg-red-100 dark:hover:bg-red-900/40 transition-colors disabled:opacity-40"
title={t('agents.actions.delete')}
>
{isDeleting ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Trash2 className="w-3.5 h-3.5" />}
</button>
</div>
</div>
)
}