Les boutons des pages Agents, MCP, Données, Intégrations et Généraux suivent la couleur choisie. Les libellés encore techniques (recherche par le sens, notes proches, connexion unique) sont en langage courant. 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-brand-accent text-white border-brand-accent'
|
|
: '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>
|
|
)
|
|
}
|