- 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>
151 lines
5.2 KiB
TypeScript
151 lines
5.2 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* Agent Templates Gallery
|
|
* Pre-built agent configurations that users can install in one click.
|
|
*/
|
|
|
|
import { useState } from 'react'
|
|
import {
|
|
Globe,
|
|
Search,
|
|
Eye,
|
|
Settings,
|
|
Plus,
|
|
Loader2,
|
|
} from 'lucide-react'
|
|
import { toast } from 'sonner'
|
|
import { useLanguage } from '@/lib/i18n'
|
|
|
|
interface AgentTemplatesProps {
|
|
onInstalled: () => void
|
|
existingAgentNames: string[]
|
|
}
|
|
|
|
const templateConfig = [
|
|
{ id: 'veilleAI', type: 'scraper', roleKey: 'agents.defaultRoles.scraper', urls: [
|
|
'https://www.theverge.com/rss/ai-artificial-intelligence/index.xml',
|
|
'https://techcrunch.com/category/artificial-intelligence/feed/',
|
|
'https://feeds.arstechnica.com/arstechnica/technology-lab',
|
|
'https://www.technologyreview.com/feed/',
|
|
'https://www.wired.com/feed/',
|
|
'https://korben.info/feed',
|
|
], frequency: 'weekly' },
|
|
{ id: 'veilleTech', type: 'scraper', roleKey: 'agents.defaultRoles.scraper', urls: [
|
|
'https://news.ycombinator.com/rss',
|
|
'https://dev.to/feed',
|
|
'https://www.producthunt.com/feed',
|
|
], frequency: 'daily' },
|
|
{ id: 'veilleDev', type: 'scraper', roleKey: 'agents.defaultRoles.scraper', urls: [
|
|
'https://dev.to/feed/tag/javascript',
|
|
'https://dev.to/feed/tag/typescript',
|
|
'https://dev.to/feed/tag/react',
|
|
], frequency: 'weekly' },
|
|
{ id: 'surveillant', type: 'monitor', roleKey: 'agents.defaultRoles.monitor', urls: [], frequency: 'weekly' },
|
|
{ id: 'chercheur', type: 'researcher', roleKey: 'agents.defaultRoles.researcher', urls: [], frequency: 'manual' },
|
|
] as const
|
|
|
|
const typeIcons: Record<string, typeof Globe> = {
|
|
scraper: Globe,
|
|
researcher: Search,
|
|
monitor: Eye,
|
|
custom: Settings,
|
|
}
|
|
|
|
const typeColors: Record<string, string> = {
|
|
scraper: 'text-blue-600 bg-blue-50',
|
|
researcher: 'text-purple-600 bg-purple-50',
|
|
monitor: 'text-amber-600 bg-amber-50',
|
|
custom: 'text-green-600 bg-green-50',
|
|
}
|
|
|
|
export function AgentTemplates({ onInstalled, existingAgentNames }: AgentTemplatesProps) {
|
|
const { t } = useLanguage()
|
|
const [installingId, setInstallingId] = useState<string | null>(null)
|
|
|
|
const handleInstall = async (tpl: typeof templateConfig[number]) => {
|
|
setInstallingId(tpl.id)
|
|
try {
|
|
const { createAgent } = await import('@/app/actions/agent-actions')
|
|
const nameKey = `agents.templates.${tpl.id}.name` as const
|
|
const descKey = `agents.templates.${tpl.id}.description` as const
|
|
const baseName = t(nameKey)
|
|
let resolvedName = baseName
|
|
if (existingAgentNames.includes(baseName)) {
|
|
let n = 2
|
|
while (existingAgentNames.includes(`${baseName} ${n}`)) n++
|
|
resolvedName = `${baseName} ${n}`
|
|
}
|
|
await createAgent({
|
|
name: resolvedName,
|
|
description: t(descKey),
|
|
type: tpl.type,
|
|
role: t(tpl.roleKey),
|
|
sourceUrls: tpl.urls.length > 0 ? [...tpl.urls] : undefined,
|
|
frequency: tpl.frequency,
|
|
tools: tpl.type === 'scraper'
|
|
? ['web_scrape', 'note_create']
|
|
: tpl.type === 'researcher'
|
|
? ['web_search', 'web_scrape', 'note_search', 'note_create']
|
|
: tpl.type === 'monitor'
|
|
? ['note_search', 'note_read', 'note_create']
|
|
: [],
|
|
})
|
|
toast.success(t('agents.toasts.installSuccess', { name: resolvedName }))
|
|
onInstalled()
|
|
} catch {
|
|
toast.error(t('agents.toasts.installError'))
|
|
} finally {
|
|
setInstallingId(null)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<h3 className="text-sm font-semibold text-slate-500 uppercase tracking-wider mb-3">
|
|
{t('agents.templates.title')}
|
|
</h3>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
|
{templateConfig.map(tpl => {
|
|
const Icon = typeIcons[tpl.type] || Settings
|
|
const isInstalling = installingId === tpl.id
|
|
const nameKey = `agents.templates.${tpl.id}.name`
|
|
const descKey = `agents.templates.${tpl.id}.description`
|
|
|
|
return (
|
|
<div
|
|
key={tpl.id}
|
|
className="border-2 border-dashed border-slate-200 rounded-xl p-4 hover:border-primary/30 hover:bg-primary/[0.02] transition-all group"
|
|
>
|
|
<div className="flex items-center gap-2.5 mb-2">
|
|
<div className={`p-1.5 rounded-lg ${typeColors[tpl.type]}`}>
|
|
<Icon className="w-4 h-4" />
|
|
</div>
|
|
<h4 className="font-medium text-sm text-slate-700">{t(nameKey)}</h4>
|
|
</div>
|
|
<p className="text-xs text-slate-400 mb-3 line-clamp-2">{t(descKey)}</p>
|
|
<button
|
|
onClick={() => handleInstall(tpl)}
|
|
disabled={isInstalling}
|
|
className="flex items-center gap-1.5 text-xs font-medium text-primary hover:text-primary/80 transition-colors disabled:opacity-50"
|
|
>
|
|
{isInstalling ? (
|
|
<>
|
|
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
|
{t('agents.templates.installing')}
|
|
</>
|
|
) : (
|
|
<>
|
|
<Plus className="w-3.5 h-3.5" />
|
|
{t('agents.templates.install')}
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|