Files
Momento/memento-note/components/agents/agent-templates.tsx
Antigravity 5f9b8a9a09
All checks were successful
CI / Lint, Unit Tests & Build (push) Successful in 7m7s
CI / Deploy production (on server) (push) Successful in 24s
fix(ui): thème des réglages et derniers textes de la revue
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>
2026-08-30 21:42:28 +00:00

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>
)
}