fix(agents): empêcher le déplacement des cartes lors du toggle
Le tri par `updatedAt` provoquait un saut de position quand on toggait un agent car Prisma mettait à jour `updatedAt` automatiquement. - Tri stable par `createdAt` au lieu de `updatedAt` - Mise à jour optimiste locale via `onToggle` au lieu d'un re-fetch complet - Rollback automatique en cas d'erreur serveur - Désactivation du bouton toggle pendant l'opération (anti double-clic) - Suppression du `revalidatePath` superflu dans `toggleAgent` Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
197
keep-notes/app/(main)/agents/agents-page-client.tsx
Normal file
197
keep-notes/app/(main)/agents/agents-page-client.tsx
Normal file
@@ -0,0 +1,197 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* Agents Page Client
|
||||
* Main client component for the agents page.
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from 'react'
|
||||
import { Plus, Bot } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
|
||||
import { AgentCard } from '@/components/agents/agent-card'
|
||||
import { AgentForm } from '@/components/agents/agent-form'
|
||||
import { AgentTemplates } from '@/components/agents/agent-templates'
|
||||
import { AgentRunLog } from '@/components/agents/agent-run-log'
|
||||
import {
|
||||
createAgent,
|
||||
updateAgent,
|
||||
getAgents,
|
||||
} from '@/app/actions/agent-actions'
|
||||
|
||||
// --- Types ---
|
||||
|
||||
interface Notebook {
|
||||
id: string
|
||||
name: string
|
||||
icon?: string | null
|
||||
}
|
||||
|
||||
interface AgentItem {
|
||||
id: string
|
||||
name: string
|
||||
description?: string | null
|
||||
type?: string | null
|
||||
role: string
|
||||
sourceUrls?: string | null
|
||||
sourceNotebookId?: string | null
|
||||
targetNotebookId?: string | null
|
||||
frequency: string
|
||||
isEnabled: boolean
|
||||
lastRun: string | Date | null
|
||||
updatedAt: string | Date
|
||||
_count: { actions: number }
|
||||
actions: { id: string; status: string; createdAt: string | Date }[]
|
||||
notebook?: { id: string; name: string; icon?: string | null } | null
|
||||
}
|
||||
|
||||
interface AgentsPageClientProps {
|
||||
agents: AgentItem[]
|
||||
notebooks: Notebook[]
|
||||
}
|
||||
|
||||
// --- Component ---
|
||||
|
||||
export function AgentsPageClient({
|
||||
agents: initialAgents,
|
||||
notebooks,
|
||||
}: AgentsPageClientProps) {
|
||||
const { t } = useLanguage()
|
||||
const [agents, setAgents] = useState(initialAgents)
|
||||
const [showForm, setShowForm] = useState(false)
|
||||
const [editingAgent, setEditingAgent] = useState<AgentItem | null>(null)
|
||||
const [logAgent, setLogAgent] = useState<{ id: string; name: string } | null>(null)
|
||||
|
||||
const refreshAgents = useCallback(async () => {
|
||||
try {
|
||||
const updated = await getAgents()
|
||||
setAgents(updated)
|
||||
} catch {
|
||||
// Silent
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleToggle = useCallback((id: string, isEnabled: boolean) => {
|
||||
setAgents(prev => prev.map(a => a.id === id ? { ...a, isEnabled } : a))
|
||||
}, [])
|
||||
|
||||
const handleCreate = useCallback(() => {
|
||||
setEditingAgent(null)
|
||||
setShowForm(true)
|
||||
}, [])
|
||||
|
||||
const handleEdit = useCallback((id: string) => {
|
||||
const agent = agents.find(a => a.id === id)
|
||||
if (agent) {
|
||||
setEditingAgent(agent)
|
||||
setShowForm(true)
|
||||
}
|
||||
}, [agents])
|
||||
|
||||
const handleSave = useCallback(async (formData: FormData) => {
|
||||
const data = {
|
||||
name: formData.get('name') as string,
|
||||
description: (formData.get('description') as string) || undefined,
|
||||
type: formData.get('type') as string,
|
||||
role: formData.get('role') as string,
|
||||
sourceUrls: formData.get('sourceUrls') ? JSON.parse(formData.get('sourceUrls') as string) : undefined,
|
||||
sourceNotebookId: (formData.get('sourceNotebookId') as string) || undefined,
|
||||
targetNotebookId: (formData.get('targetNotebookId') as string) || undefined,
|
||||
frequency: formData.get('frequency') as string,
|
||||
}
|
||||
|
||||
if (editingAgent) {
|
||||
await updateAgent(editingAgent.id, data)
|
||||
toast.success(t('agents.toasts.updated'))
|
||||
} else {
|
||||
await createAgent(data)
|
||||
toast.success(t('agents.toasts.created'))
|
||||
}
|
||||
|
||||
setShowForm(false)
|
||||
setEditingAgent(null)
|
||||
refreshAgents()
|
||||
}, [editingAgent, refreshAgents, t])
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between gap-4 mb-8">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="p-3 bg-primary/10 rounded-2xl shadow-sm border border-primary/20">
|
||||
<Bot className="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">{t('agents.title')}</h1>
|
||||
<p className="text-muted-foreground text-sm">{t('agents.subtitle')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Create button */}
|
||||
<div className="flex justify-end mb-6">
|
||||
<button
|
||||
onClick={handleCreate}
|
||||
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-primary rounded-lg hover:bg-primary/90 transition-colors"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
{t('agents.newAgent')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Agents grid */}
|
||||
{agents.length > 0 && (
|
||||
<div className="mb-10">
|
||||
<h3 className="text-sm font-semibold text-slate-500 uppercase tracking-wider mb-3">
|
||||
{t('agents.myAgents')}
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{agents.map(agent => (
|
||||
<AgentCard
|
||||
key={agent.id}
|
||||
agent={agent}
|
||||
onEdit={handleEdit}
|
||||
onRefresh={refreshAgents}
|
||||
onToggle={handleToggle}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty state */}
|
||||
{agents.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-center mb-10">
|
||||
<Bot className="w-16 h-16 text-slate-300 mb-4" />
|
||||
<h3 className="text-lg font-medium text-slate-500 mb-2">{t('agents.noAgents')}</h3>
|
||||
<p className="text-sm text-slate-400 max-w-sm">
|
||||
{t('agents.noAgentsDescription')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Templates */}
|
||||
<AgentTemplates onInstalled={refreshAgents} />
|
||||
|
||||
{/* Form modal */}
|
||||
{showForm && (
|
||||
<AgentForm
|
||||
agent={editingAgent}
|
||||
notebooks={notebooks}
|
||||
onSave={handleSave}
|
||||
onCancel={() => { setShowForm(false); setEditingAgent(null) }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Run log modal */}
|
||||
{logAgent && (
|
||||
<AgentRunLog
|
||||
agentId={logAgent.id}
|
||||
agentName={logAgent.name}
|
||||
onClose={() => setLogAgent(null)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user