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>
356 lines
13 KiB
TypeScript
356 lines
13 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, LayoutTemplate, Search, HelpCircle } 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 [activeTab, setActiveTab] = useState<'dashboard' | 'templates'>('dashboard')
|
|
|
|
const refreshAgents = useCallback(async () => {
|
|
try {
|
|
const updated = await getAgents()
|
|
setAgents(updated)
|
|
return updated
|
|
} catch {
|
|
return null
|
|
}
|
|
}, [])
|
|
|
|
const prevActionsRef = useRef<Record<string, string | null>>({})
|
|
|
|
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
|
|
if (prevId !== lastAction.id) {
|
|
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(() => {
|
|
for (const agent of initialAgents) {
|
|
prevActionsRef.current[agent.id] = agent.actions[0]?.id ?? null
|
|
}
|
|
const interval = setInterval(async () => {
|
|
const updated = await refreshAgents()
|
|
if (updated) checkForNewActions(updated)
|
|
}, 30000)
|
|
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 (
|
|
/* Full-bleed layout: -m-4 cancels the p-4 of the parent <main> */
|
|
<div className="flex -m-4 h-[calc(100vh-4rem)] overflow-hidden">
|
|
|
|
{/* ── LEFT SIDEBAR ── */}
|
|
<aside className="w-60 flex-shrink-0 flex flex-col bg-muted/30 border-r border-border/40 h-full font-display">
|
|
{/* Brand */}
|
|
<div className="flex items-center gap-3 px-5 py-5 border-b border-border/40">
|
|
<div className="w-8 h-8 rounded-lg bg-primary flex items-center justify-center flex-shrink-0">
|
|
<Bot className="w-4 h-4 text-primary-foreground" />
|
|
</div>
|
|
<span className="font-bold text-base tracking-tight">{t('agents.title')}</span>
|
|
</div>
|
|
|
|
{/* Nav */}
|
|
<nav className="flex-1 p-3 space-y-0.5">
|
|
<button
|
|
onClick={() => setActiveTab('dashboard')}
|
|
className={`w-full flex items-center gap-2.5 px-3 py-2 text-sm font-medium rounded-lg transition-all ${
|
|
activeTab === 'dashboard'
|
|
? 'bg-primary/10 text-primary border-r-2 border-primary'
|
|
: 'text-muted-foreground hover:bg-background/70 hover:text-foreground'
|
|
}`}
|
|
>
|
|
<Bot className="w-4 h-4" />
|
|
{t('agents.myAgents')}
|
|
</button>
|
|
</nav>
|
|
|
|
{/* Footer: Help */}
|
|
<div className="p-3 border-t border-border/40">
|
|
<button
|
|
onClick={() => setShowHelp(true)}
|
|
className="w-full flex items-center gap-2.5 px-3 py-2 text-sm font-medium text-muted-foreground hover:bg-background/70 hover:text-foreground rounded-lg transition-all"
|
|
>
|
|
<HelpCircle className="w-4 h-4" />
|
|
{t('agents.help.btnLabel')}
|
|
</button>
|
|
</div>
|
|
</aside>
|
|
|
|
{/* ── MAIN CONTENT ── */}
|
|
<div className="flex-1 flex flex-col min-w-0 bg-background overflow-hidden">
|
|
|
|
{/* Top header bar */}
|
|
<header className="flex items-center justify-between px-8 py-4 border-b border-border/40 bg-background flex-shrink-0 font-display">
|
|
<div>
|
|
<h1 className="text-xl font-bold tracking-tight">
|
|
{t('agents.myAgents')}
|
|
</h1>
|
|
<p className="text-xs text-muted-foreground mt-0.5">
|
|
{t('agents.subtitle')}
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={handleCreate}
|
|
className="flex items-center gap-2 px-4 py-2 text-sm font-semibold text-primary-foreground bg-primary hover:bg-primary/90 rounded-lg shadow-sm hover:shadow-md hover:shadow-primary/20 transition-all"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
{t('agents.newAgent')}
|
|
</button>
|
|
</header>
|
|
|
|
{/* Scrollable content area */}
|
|
<main className="flex-1 overflow-y-auto p-8">
|
|
|
|
{/* Dashboard tab - agents + templates */}
|
|
{activeTab === 'dashboard' && (
|
|
<>
|
|
{agents.length > 0 && (
|
|
<>
|
|
{/* Filter pills + search */}
|
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-6">
|
|
<div className="flex items-center gap-1 bg-muted/40 p-1 rounded-lg border border-border/40">
|
|
{typeFilterOptions.map(opt => (
|
|
<button
|
|
key={opt.value}
|
|
onClick={() => setTypeFilter(opt.value)}
|
|
className={`px-3 py-1.5 text-[12px] font-semibold rounded-md transition-all ${
|
|
typeFilter === opt.value
|
|
? 'bg-background text-foreground shadow-sm'
|
|
: 'text-muted-foreground hover:text-foreground'
|
|
}`}
|
|
>
|
|
{t(opt.labelKey)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground/60" />
|
|
<input
|
|
type="text"
|
|
value={searchQuery}
|
|
onChange={e => setSearchQuery(e.target.value)}
|
|
placeholder={t('agents.searchPlaceholder')}
|
|
className="pl-9 pr-4 py-2 text-[13px] bg-card border border-border/50 rounded-lg outline-none focus:border-primary/50 focus:ring-2 focus:ring-primary/10 transition-all placeholder:text-muted-foreground/40 w-56"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{filteredAgents.length > 0 ? (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-3 mb-10">
|
|
{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 mb-10">
|
|
<Search className="w-10 h-10 text-muted-foreground/20 mb-3" />
|
|
<p className="text-sm text-muted-foreground">{t('agents.noResults')}</p>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{agents.length === 0 && (
|
|
<div className="flex flex-col items-center justify-center py-16 text-center bg-card rounded-xl border border-border/40 shadow-sm mb-10">
|
|
<Bot className="w-12 h-12 text-muted-foreground/20 mb-4" />
|
|
<h3 className="text-base font-semibold text-foreground mb-2">{t('agents.noAgents')}</h3>
|
|
<p className="text-sm text-muted-foreground max-w-sm mb-2">{t('agents.noAgentsDescription')}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Templates always visible on dashboard */}
|
|
<AgentTemplates onInstalled={refreshAgents} existingAgentNames={existingAgentNames} />
|
|
</>
|
|
)}
|
|
</main>
|
|
</div>
|
|
|
|
{/* Sliding panels */}
|
|
{showForm && (
|
|
<AgentForm
|
|
agent={editingAgent}
|
|
notebooks={notebooks}
|
|
onSave={handleSave}
|
|
onCancel={() => { setShowForm(false); setEditingAgent(null) }}
|
|
/>
|
|
)}
|
|
{logAgent && (
|
|
<AgentRunLog
|
|
agentId={logAgent.id}
|
|
agentName={logAgent.name}
|
|
onClose={() => setLogAgent(null)}
|
|
/>
|
|
)}
|
|
{showHelp && (
|
|
<AgentHelp onClose={() => setShowHelp(false)} />
|
|
)}
|
|
</div>
|
|
)
|
|
}
|