Files
Momento/memento-note/app/(main)/agents/agents-page-client.tsx
sepehr 9779dd7a79
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 42s
fix: refresh agents on tab focus + hide stale nextRun + add cron logging
- Add visibilitychange listener: refreshes agent data immediately when
  user returns to the tab (more reliable than interval-only polling)
- Only show toast for actions created within last 5 minutes to avoid
  false positives on page reload
- Hide "Prochaine exécution" line entirely if nextRun is in the past
  instead of showing confusing "En attente de déclenchement"
- Add detailed logging to cron endpoint for debugging

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 13:16:32 +02:00

340 lines
12 KiB
TypeScript

'use client'
/**
* Agents Page Client
* Main client component for the agents page.
*/
import { useState, useCallback, useMemo, useEffect, useRef } from 'react'
import { Plus, Bot, LifeBuoy, Search } 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 { AgentHelp } from '@/components/agents/agent-help'
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
createdAt: string | Date
updatedAt: string | Date
tools?: string | null
maxSteps?: number
notifyEmail?: boolean
includeImages?: boolean
_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[]
}
const typeFilterOptions = [
{ value: '', labelKey: 'agents.filterAll' },
{ value: 'scraper', labelKey: 'agents.types.scraper' },
{ value: 'researcher', labelKey: 'agents.types.researcher' },
{ value: 'monitor', labelKey: 'agents.types.monitor' },
{ value: 'custom', labelKey: 'agents.types.custom' },
] as const
// --- 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 [showHelp, setShowHelp] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
const [typeFilter, setTypeFilter] = useState('')
const refreshAgents = useCallback(async () => {
try {
const updated = await getAgents()
setAgents(updated)
return updated
} catch {
return null
}
}, [])
// Track latest action ID per agent to detect new executions
const prevActionsRef = useRef<Record<string, string | null>>({})
// Check for new actions and show toast notifications
const checkForNewActions = useCallback((updated: AgentItem[]) => {
for (const agent of updated) {
const lastAction = agent.actions[0]
if (!lastAction) continue
const prevId = prevActionsRef.current[agent.id]
if (prevId === undefined) continue // Not tracked
if (prevId !== lastAction.id) {
// Only toast for recently created actions (within 5 min)
const age = Date.now() - new Date(lastAction.createdAt).getTime()
if (age < 5 * 60 * 1000) {
if (lastAction.status === 'success') {
toast.success(t('agents.toasts.autoRunSuccess', { name: agent.name }))
} else if (lastAction.status === 'failure') {
toast.error(t('agents.toasts.autoRunError', { name: agent.name }))
}
}
}
prevActionsRef.current[agent.id] = lastAction.id
}
}, [t])
useEffect(() => {
// Initialize tracking from initial data
for (const agent of initialAgents) {
prevActionsRef.current[agent.id] = agent.actions[0]?.id ?? null
}
// Interval polling every 30s
const interval = setInterval(async () => {
const updated = await refreshAgents()
if (updated) checkForNewActions(updated)
}, 30000)
// Refresh immediately when user comes back to the tab
const onVisible = async () => {
if (document.visibilityState === 'visible') {
const updated = await refreshAgents()
if (updated) checkForNewActions(updated)
}
}
document.addEventListener('visibilitychange', onVisible)
return () => {
clearInterval(interval)
document.removeEventListener('visibilitychange', onVisible)
}
}, [refreshAgents, checkForNewActions])
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,
tools: formData.get('tools') ? JSON.parse(formData.get('tools') as string) : undefined,
maxSteps: formData.get('maxSteps') ? Number(formData.get('maxSteps')) : undefined,
notifyEmail: formData.get('notifyEmail') === 'true',
includeImages: formData.get('includeImages') === 'true',
scheduledTime: (formData.get('scheduledTime') as string) || undefined,
scheduledDay: formData.get('scheduledDay') ? Number(formData.get('scheduledDay')) : undefined,
timezone: (formData.get('timezone') as string) || undefined,
}
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)
await refreshAgents()
}, [editingAgent, refreshAgents, t])
const filteredAgents = useMemo(() => {
return agents.filter(agent => {
const matchesType = !typeFilter || (agent.type || 'scraper') === typeFilter
if (!searchQuery.trim()) return matchesType
const q = searchQuery.toLowerCase()
const matchesSearch =
(agent.name || '').toLowerCase().includes(q) ||
(agent.description && agent.description.toLowerCase().includes(q))
return matchesType && matchesSearch
})
}, [agents, searchQuery, typeFilter])
const existingAgentNames = useMemo(() => agents.map(a => a.name), [agents])
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>
{/* Action buttons */}
<div className="flex items-center justify-between mb-6">
<button
onClick={() => setShowHelp(true)}
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-primary bg-primary/10 border border-primary/20 rounded-lg hover:bg-primary/15 transition-colors"
>
<LifeBuoy className="w-4 h-4" />
{t('agents.help.btnLabel')}
</button>
<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-muted-foreground uppercase tracking-wider mb-3">
{t('agents.myAgents')}
</h3>
{/* Search and filter */}
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-3 mb-4">
<div className="relative flex-1 w-full sm:max-w-xs">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<input
type="text"
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
placeholder={t('agents.searchPlaceholder')}
className="w-full pl-9 pr-3 py-2 text-sm bg-card border border-border rounded-lg outline-none focus:border-primary/40 focus:ring-2 focus:ring-primary/10 transition-all"
/>
</div>
<div className="flex items-center gap-1.5 flex-wrap">
{typeFilterOptions.map(opt => (
<button
key={opt.value}
onClick={() => setTypeFilter(opt.value)}
className={`px-3 py-1.5 text-xs font-medium rounded-full transition-colors ${
typeFilter === opt.value
? 'bg-primary text-white'
: 'bg-muted text-muted-foreground hover:bg-accent'
}`}
>
{t(opt.labelKey)}
</button>
))}
</div>
</div>
{filteredAgents.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredAgents.map(agent => (
<AgentCard
key={agent.id}
agent={agent}
onEdit={handleEdit}
onRefresh={refreshAgents}
onToggle={handleToggle}
/>
))}
</div>
) : (
<div className="flex flex-col items-center justify-center py-12 text-center">
<Search className="w-10 h-10 text-muted-foreground/30 mb-3" />
<p className="text-sm text-muted-foreground">{t('agents.noResults')}</p>
</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-muted-foreground/30 mb-4" />
<h3 className="text-lg font-medium text-muted-foreground mb-2">{t('agents.noAgents')}</h3>
<p className="text-sm text-muted-foreground max-w-sm">
{t('agents.noAgentsDescription')}
</p>
</div>
)}
{/* Templates */}
<AgentTemplates onInstalled={refreshAgents} existingAgentNames={existingAgentNames} />
{/* 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)}
/>
)}
{/* Help modal */}
{showHelp && (
<AgentHelp onClose={() => setShowHelp(false)} />
)}
</>
)
}