Files
Momento/memento-note/components/agents/agent-templates.tsx
Antigravity c415d93945
Some checks failed
CI / Lint, Unit Tests & Build (push) Failing after 1m7s
CI / Deploy production (on server) (push) Has been skipped
feat: Tier 1 & 2 — Daily Note, Voice, Flashcard quota, Readwise, Calendar, Agent Gallery
Tier 1:
- BASIC tier: chat (10/mo) + reformulate (10/mo) désormais accessibles
- Nouveaux quotas: ai_flashcard + voice_transcribe dans tous les tiers
- /api/notes/daily : note du jour auto-créée (find or create)
- Bouton Note du Jour dans la sidebar (CalendarDays)
- Voice-to-Text dans l'éditeur (Web Speech API, bouton Mic toolbar)
- Flashcard generation → quota ai_flashcard (au lieu de reformulate)

Tier 2:
- Intégration Readwise: GET/POST/DELETE /api/integrations/readwise
- Intégration Google Calendar: OAuth flow + today's events + meeting notes
- /api/integrations/calendar + /callback
- Page /settings/integrations avec cards Calendar + Readwise
- SettingsNav: onglet Intégrations
- AgentTemplates: catégories + 4 nouveaux templates (Digest/Recap/AutoTagger/Synthesis)

Schema:
- UserAISettings.integrationTokens Json? (migration 20260529160000)
- prisma generate + migrate deploy appliqués

Fix:
- SpeechRecognition types (triple-slash @types/dom-speech-recognition)
- Notebook.create: suppression champ 'description' inexistant

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-29 15:14:01 +00:00

217 lines
9.2 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; label: string }[] = [
{ id: 'all', label: 'Tous' },
{ id: 'veille', label: '📡 Veille' },
{ id: 'digest', label: '📰 Digest' },
{ id: 'tools', label: '🔧 Outils' },
{ id: 'generate', label: '✨ Génération' },
]
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 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)
return (
<div className="space-y-5">
{/* Category filter */}
<div className="flex flex-wrap gap-2">
{CATEGORIES.map((cat) => (
<button
key={cat.id}
onClick={() => setActiveCategory(cat.id)}
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'
}`}
>
{cat.label}
</button>
))}
</div>
{/* Template grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filtered.map(tpl => {
const Icon = templateIcons[tpl.id as TemplateId] ?? 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="bg-card/40 border border-dashed border-border rounded-2xl p-6 group cursor-pointer 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 group-hover:bg-foreground group-hover:text-background mb-4 transition-all">
<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">
{tpl.frequency === 'daily' ? '📅 Quotidien' : '📆 Hebdo'}
</span>
)}
</div>
<p className="text-xs text-muted-foreground leading-relaxed mb-4">{t(descKey)}</p>
<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>
</div>
)
}