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
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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user