feat(agents): add search/filter, "New" badge, and duplicate name resolution
- Add search bar with real-time filtering on agent name and description - Add type filter chips (All, Veilleur, Chercheur, Surveillant, Personnalisé) - Add "New" badge on agents created within the last 24h (hydration-safe) - Auto-increment template names on duplicate install (e.g. "Veille Tech 2") - Add i18n keys for new UI elements in both fr and en locales Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,8 +5,8 @@
|
||||
* Main client component for the agents page.
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from 'react'
|
||||
import { Plus, Bot } from 'lucide-react'
|
||||
import { useState, useCallback, useMemo } from 'react'
|
||||
import { Plus, Bot, LifeBuoy, Search } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
|
||||
@@ -14,6 +14,7 @@ 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,
|
||||
@@ -40,7 +41,11 @@ interface AgentItem {
|
||||
frequency: string
|
||||
isEnabled: boolean
|
||||
lastRun: string | Date | null
|
||||
createdAt: string | Date
|
||||
updatedAt: string | Date
|
||||
tools?: string | null
|
||||
maxSteps?: number
|
||||
notifyEmail?: boolean
|
||||
_count: { actions: number }
|
||||
actions: { id: string; status: string; createdAt: string | Date }[]
|
||||
notebook?: { id: string; name: string; icon?: string | null } | null
|
||||
@@ -51,6 +56,14 @@ interface AgentsPageClientProps {
|
||||
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({
|
||||
@@ -62,6 +75,9 @@ export function AgentsPageClient({
|
||||
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 {
|
||||
@@ -99,6 +115,9 @@ export function AgentsPageClient({
|
||||
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',
|
||||
}
|
||||
|
||||
if (editingAgent) {
|
||||
@@ -111,9 +130,23 @@ export function AgentsPageClient({
|
||||
|
||||
setShowForm(false)
|
||||
setEditingAgent(null)
|
||||
refreshAgents()
|
||||
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 */}
|
||||
@@ -129,8 +162,15 @@ export function AgentsPageClient({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Create button */}
|
||||
<div className="flex justify-end mb-6">
|
||||
{/* 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"
|
||||
@@ -146,17 +186,54 @@ export function AgentsPageClient({
|
||||
<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}
|
||||
|
||||
{/* 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-slate-400" />
|
||||
<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-white border border-slate-200 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-slate-100 text-slate-600 hover:bg-slate-200'
|
||||
}`}
|
||||
>
|
||||
{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-slate-300 mb-3" />
|
||||
<p className="text-sm text-slate-400">{t('agents.noResults')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -172,7 +249,7 @@ export function AgentsPageClient({
|
||||
)}
|
||||
|
||||
{/* Templates */}
|
||||
<AgentTemplates onInstalled={refreshAgents} />
|
||||
<AgentTemplates onInstalled={refreshAgents} existingAgentNames={existingAgentNames} />
|
||||
|
||||
{/* Form modal */}
|
||||
{showForm && (
|
||||
@@ -192,6 +269,11 @@ export function AgentsPageClient({
|
||||
onClose={() => setLogAgent(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Help modal */}
|
||||
{showHelp && (
|
||||
<AgentHelp onClose={() => setShowHelp(false)} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user