Some checks failed
Deploy to Production / Build and Deploy (push) Failing after 1m7s
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>
284 lines
11 KiB
TypeScript
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>
|
|
)
|
|
}
|