Les boutons suivent la couleur d’apparence, les libellés trop petits ou trop techniques sont clarifiés, et le catalogue des fournisseurs se met à jour tout seul. Co-authored-by: Cursor <cursoragent@cursor.com>
256 lines
10 KiB
TypeScript
256 lines
10 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import {
|
|
Globe,
|
|
Search,
|
|
Eye,
|
|
Settings,
|
|
Plus,
|
|
Loader2,
|
|
Presentation,
|
|
Pencil,
|
|
ListChecks,
|
|
Newspaper,
|
|
Youtube,
|
|
BookMarked,
|
|
FileText,
|
|
Tag,
|
|
Brain,
|
|
} from 'lucide-react'
|
|
import { toast } from 'sonner'
|
|
import { useLanguage } from '@/lib/i18n'
|
|
|
|
interface AgentTemplatesProps {
|
|
onInstalled: () => void
|
|
existingAgentNames: string[]
|
|
}
|
|
|
|
const templateConfig = [
|
|
// ── Scrapers & Veille ──────────────────────────────────────────────────
|
|
{ id: 'veilleAI', type: 'scraper', roleKey: 'agents.defaultRoles.scraper', category: 'veille', 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', category: 'veille', 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', category: 'veille', urls: [
|
|
'https://dev.to/feed/tag/javascript',
|
|
'https://dev.to/feed/tag/typescript',
|
|
'https://dev.to/feed/tag/react',
|
|
], frequency: 'weekly' },
|
|
// ── Digest & Résumés ──────────────────────────────────────────────────
|
|
{ id: 'dailyDigest', type: 'digest', roleKey: 'agents.defaultRoles.researcher', category: 'digest', urls: [], frequency: 'daily' },
|
|
{ id: 'weeklyRecap', type: 'monitor', roleKey: 'agents.defaultRoles.monitor', category: 'digest', urls: [], frequency: 'weekly' },
|
|
// ── Outils ────────────────────────────────────────────────────────────
|
|
{ id: 'surveillant', type: 'monitor', roleKey: 'agents.defaultRoles.monitor', category: 'tools', urls: [], frequency: 'weekly' },
|
|
{ id: 'chercheur', type: 'researcher', roleKey: 'agents.defaultRoles.researcher', category: 'tools', urls: [], frequency: 'manual' },
|
|
{ id: 'autoTagger', type: 'auto-tagger', roleKey: 'agents.defaultRoles.researcher', category: 'tools', urls: [], frequency: 'weekly' },
|
|
// ── Génération ────────────────────────────────────────────────────────
|
|
{ id: 'slideGenerator', type: 'slide-generator', roleKey: 'agents.defaultRoles.slideGenerator', category: 'generate', urls: [], frequency: 'manual' },
|
|
{ id: 'excalidrawGenerator', type: 'excalidraw-generator', roleKey: 'agents.defaultRoles.excalidrawGenerator', category: 'generate', urls: [], frequency: 'manual' },
|
|
{ id: 'taskExtractor', type: 'task-extractor', roleKey: 'agents.defaultRoles.taskExtractor', category: 'generate', urls: [], frequency: 'manual' },
|
|
{ id: 'knowledgeSynthesis', type: 'researcher', roleKey: 'agents.defaultRoles.researcher', category: 'generate', urls: [], frequency: 'weekly' },
|
|
] as const
|
|
|
|
type TemplateId = typeof templateConfig[number]['id']
|
|
type CategoryId = 'all' | 'veille' | 'digest' | 'tools' | 'generate'
|
|
|
|
const CATEGORIES: { id: CategoryId; labelKey: string }[] = [
|
|
{ id: 'all', labelKey: 'agents.templates.categoryAll' },
|
|
{ id: 'veille', labelKey: 'agents.templates.categoryWatch' },
|
|
{ id: 'digest', labelKey: 'agents.templates.categoryDigest' },
|
|
{ id: 'tools', labelKey: 'agents.templates.categoryTools' },
|
|
{ id: 'generate', labelKey: 'agents.templates.categoryGenerate' },
|
|
]
|
|
|
|
const PREVIEW_COUNT = 3
|
|
|
|
function templateNameKey(id: string) {
|
|
return `agents.templates.${id}.name`
|
|
}
|
|
|
|
function hasTranslatedName(t: (key: string) => string, id: string) {
|
|
const key = templateNameKey(id)
|
|
const name = t(key)
|
|
return Boolean(name.trim()) && name !== key
|
|
}
|
|
|
|
const typeIcons: Record<string, typeof Globe> = {
|
|
scraper: Globe,
|
|
researcher: Search,
|
|
monitor: Eye,
|
|
custom: Settings,
|
|
'slide-generator': Presentation,
|
|
'excalidraw-generator': Pencil,
|
|
'task-extractor': ListChecks,
|
|
digest: Newspaper,
|
|
'youtube-transcript': Youtube,
|
|
'readwise-sync': BookMarked,
|
|
'auto-tagger': Tag,
|
|
'knowledge-synthesis': Brain,
|
|
}
|
|
|
|
// Extra icons for specific template IDs
|
|
const templateIcons: Partial<Record<TemplateId, typeof Globe>> = {
|
|
dailyDigest: Newspaper,
|
|
weeklyRecap: FileText,
|
|
autoTagger: Tag,
|
|
knowledgeSynthesis: Brain,
|
|
}
|
|
|
|
export function AgentTemplates({ onInstalled, existingAgentNames }: AgentTemplatesProps) {
|
|
const { t } = useLanguage()
|
|
const [installingId, setInstallingId] = useState<string | null>(null)
|
|
const [activeCategory, setActiveCategory] = useState<CategoryId>('all')
|
|
const [showAll, setShowAll] = useState(false)
|
|
|
|
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}`
|
|
}
|
|
|
|
const toolMap: Record<string, string[]> = {
|
|
scraper: ['web_scrape', 'note_create'],
|
|
researcher: ['web_search', 'web_scrape', 'note_search', 'note_create'],
|
|
monitor: ['note_search', 'note_read', 'note_create'],
|
|
'slide-generator': ['note_search', 'note_read', 'generate_pptx'],
|
|
'excalidraw-generator': ['note_search', 'note_read', 'generate_excalidraw'],
|
|
'task-extractor': ['note_search', 'note_read', 'task_extract', 'note_create'],
|
|
digest: ['note_search', 'note_read', 'note_create'],
|
|
'auto-tagger': ['note_search', 'note_read', 'note_update'],
|
|
}
|
|
|
|
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: toolMap[tpl.type] ?? [],
|
|
})
|
|
toast.success(t('agents.toasts.installSuccess', { name: resolvedName }))
|
|
onInstalled()
|
|
} catch {
|
|
toast.error(t('agents.toasts.installError'))
|
|
} finally {
|
|
setInstallingId(null)
|
|
}
|
|
}
|
|
|
|
const filtered = (activeCategory === 'all'
|
|
? templateConfig
|
|
: templateConfig.filter((tpl) => tpl.category === activeCategory)
|
|
).filter((tpl) => hasTranslatedName(t, tpl.id))
|
|
|
|
const visible = showAll ? filtered : filtered.slice(0, PREVIEW_COUNT)
|
|
const hiddenCount = filtered.length - visible.length
|
|
|
|
return (
|
|
<div className="space-y-5">
|
|
<div className="flex flex-wrap gap-2">
|
|
{CATEGORIES.map((cat) => (
|
|
<button
|
|
key={cat.id}
|
|
type="button"
|
|
onClick={() => {
|
|
setActiveCategory(cat.id)
|
|
setShowAll(false)
|
|
}}
|
|
className={`px-3 py-1.5 rounded-full text-xs font-semibold transition-all border ${
|
|
activeCategory === cat.id
|
|
? 'bg-ink text-paper border-ink'
|
|
: 'bg-paper text-muted-ink border-border/40 hover:border-ink/30'
|
|
}`}
|
|
>
|
|
{t(cat.labelKey)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{visible.map(tpl => {
|
|
const Icon = templateIcons[tpl.id as TemplateId] ?? typeIcons[tpl.type] ?? Settings
|
|
const isInstalling = installingId === tpl.id
|
|
const nameKey = templateNameKey(tpl.id)
|
|
const descKey = `agents.templates.${tpl.id}.description`
|
|
|
|
return (
|
|
<div
|
|
key={tpl.id}
|
|
className="bg-card/40 border border-dashed border-border rounded-2xl p-6 hover:bg-card hover:border-foreground/20 transition-all"
|
|
>
|
|
<div className="w-8 h-8 rounded-lg bg-muted flex items-center justify-center text-muted-foreground mb-4">
|
|
<Icon className="w-4 h-4" />
|
|
</div>
|
|
<div className="flex items-start justify-between gap-2 mb-2">
|
|
<h4 className="text-[13px] font-bold text-foreground">{t(nameKey)}</h4>
|
|
{tpl.frequency !== 'manual' && (
|
|
<span className="text-[10px] font-semibold text-concrete bg-border/20 rounded-full px-2 py-0.5 shrink-0">
|
|
{t(`agents.frequencies.${tpl.frequency}`)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<p className="text-xs text-muted-foreground leading-relaxed mb-4">{t(descKey)}</p>
|
|
<button
|
|
type="button"
|
|
onClick={() => handleInstall(tpl)}
|
|
disabled={isInstalling}
|
|
className="text-[11px] font-bold uppercase tracking-widest text-foreground hover:opacity-60 transition-opacity flex items-center gap-2 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>
|
|
|
|
{hiddenCount > 0 && (
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowAll(true)}
|
|
className="text-[12px] font-semibold text-foreground hover:opacity-70 transition-opacity"
|
|
>
|
|
{t('agents.templates.seeAll')}
|
|
</button>
|
|
)}
|
|
{showAll && filtered.length > PREVIEW_COUNT && (
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowAll(false)}
|
|
className="text-[12px] font-semibold text-foreground hover:opacity-70 transition-opacity"
|
|
>
|
|
{t('agents.templates.showLess')}
|
|
</button>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|