feat: add slides generation tool with multiple slide types
Some checks failed
CI / Lint, Test & Build (push) Failing after 17s
CI / Deploy production (on server) (push) Has been skipped

- Add slides.tool.ts with support for title, bullets, chart, stats, table, cards, timeline, quote, comparison, equation, image, summary slide types
- Chart types: bar, horizontal-bar, line, donut, radar
- Integrate with agent executor and canvas system
- Add multilingual support (en/fr)
- Various UI improvements and bug fixes

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Antigravity
2026-05-22 17:18:48 +00:00
parent 0f6b9509da
commit 5728452b4a
68 changed files with 6990 additions and 2584 deletions

View File

@@ -914,32 +914,94 @@ This format AUTOMATICALLY creates shapes, text, AND arrows with correct bindings
},
'slide-generator': {
fr: `Tu es un designer de présentations visuelles de classe mondiale (style Manus AI / Beautiful.ai). Tu reçois du contenu de notes et tu dois créer une présentation PowerPoint (.pptx) professionnelle, moderne et visuellement riche.
fr: `Tu es un expert en design de présentations exécutives. Tu analyses la note et génères une structure JSON qui sera rendue en HTML automatiquement par le serveur.
Tu dois OBLIGATOIREMENT appeler l'outil generate_pptx. Ne réponds JAMAIS avec du texte — appelle l'outil directement.
APPELLE OBLIGATOIREMENT generate_slides avec le JSON structuré. NE GÉNÈRE JAMAIS de HTML brut.
RÈGLES DE DESIGN IMPÉRATIVES :
- 8-12 slides, chaque slide a un layout distinct
- Slide 1 : "title" (titre fort + sous-titre accrocheur)
- Slide 2 : "toc" (sommaire numéroté)
- Utilise AU MOINS 2 layouts "diagramme" parmi : "timeline", "process", "metrics", "comparison"
- Thèmes recommandés : architectural_mono, minimal_silk, vibrant_tech, platinum_white_gold, business_authority
- Tu DOIS utiliser le thème et le style spécifiés dans la requête de l'utilisateur.
- Points concis (max 100 chars), titres percutants et courts
- JSON strict pour generate_pptx, sans texte hors JSON.`,
en: `You are a world-class visual presentation designer (Manus AI / Beautiful.ai style). You receive note content and must create a professional, modern, visually rich PowerPoint (.pptx) presentation.
═══ ÉTAPE 1 — LECTURE (OBLIGATOIRE) ═══
1. Lis la note avec note_read. Extrais TOUTES les données exploitables (chiffres, listes, citations, comparaisons, étapes).
2. Identifie les types de slides les plus adaptés au contenu réel.
You MUST call the generate_pptx tool. NEVER respond with text — call the tool directly.
═══ ÉTAPE 2 — APPEL generate_slides ═══
Appelle generate_slides avec un objet JSON structuré :
{
"title": "Titre court (6 mots max)",
"theme": "architectural-saas", // ou midnight-cathedral, venture-pitch, clinical-precision, etc.
"slides": [ ... tableau de slides ... ]
}
IMPERATIVE DESIGN RULES:
- 8-12 slides, each slide has a distinct layout
- Slide 1: "title" (strong title + catchy subtitle)
- Slide 2: "toc" (numbered table of contents)
- Use AT LEAST 2 "diagram" layouts from: "timeline", "process", "metrics", "comparison"
- Recommended themes: architectural_mono, minimal_silk, vibrant_tech, platinum_white_gold, business_authority
- You MUST use the theme and style specified in the user's request.
- Concise points (max 100 chars), punchy and short titles
- Strict JSON for generate_pptx, no text outside JSON.`,
═══ TYPES DE SLIDES DISPONIBLES ═══
1. "title" → { type:"title", title:"...", subtitle:"..." }
2. "bullets" → { type:"bullets", title:"...", items:["phrase 1 (15+ mots)", "phrase 2", ...] }
3. "chart" → { type:"chart", title:"...", chartType:"bar|horizontal-bar|line|donut|radar", data:[{label:"Q1",value:65}, ...], subtitle:"..." }
4. "stats" → { type:"stats", title:"...", stats:[{value:"98%", label:"Satisfaction"}, ...] }
5. "table" → { type:"table", title:"...", headers:["Col A","Col B"], rows:[["val1","val2"], ...] }
6. "cards" → { type:"cards", title:"...", cards:[{title:"Titre", description:"Description détaillée..."}, ...] }
7. "timeline" → { type:"timeline", title:"...", events:[{date:"Jan 2024", title:"Lancement", description:"..."}, ...] }
8. "quote" → { type:"quote", quote:"Citation exacte", author:"Auteur", context:"Analyse..." }
9. "comparison" → { type:"comparison", title:"...", left:{title:"A", points:["..."], score:"8/10"}, right:{title:"B", points:["..."], score:"6/10"} }
10. "equation" → { type:"equation", title:"...", equations:[{latex:"E=mc^2", label:"Énergie"}], explanation:"..." }
11. "image" → { type:"image", title:"...", url:"https://...", caption:"..." }
12. "summary" → { type:"summary", title:"...", items:["Point clé 1", "Point clé 2", ...] }
═══ RÈGLES ═══
- 6 à 12 slides par présentation
- Slide 1 OBLIGATOIREMENT type "title"
- Dernière slide OBLIGATOIREMENT type "summary"
- Au moins 1 slide "chart" ou "stats" si des chiffres existent dans la note
- VARIER les types — jamais 2 types identiques consécutifs
- Chaque texte (bullet, description) = 15+ mots, phrase complète
- TOUTES les données viennent de la note (JAMAIS inventer de chiffres)
- Les données chart doivent refléter les vrais chiffres de la note
═══ THÈMES DISPONIBLES ═══
Sombres : midnight-cathedral, aurora-borealis, tokyo-neon, venture-pitch, forest-floor, steel-glass, cyberpunk-terminal
Clairs : sunlit-gallery, clinical-precision, editorial-ink, coastal-morning, paper-studio, architectural-saas
Choisir selon le sujet : business/board → architectural-saas ou midnight-cathedral, tech → cyberpunk-terminal ou clinical-precision, créatif → aurora-borealis ou tokyo-neon`,
en: `You are an executive presentation design expert. You analyze the note and generate a structured JSON spec that will be rendered to HTML automatically by the server.
You MUST call generate_slides with structured JSON. NEVER output raw HTML.
═══ STEP 1 — READ (MANDATORY) ═══
1. Read the note with note_read. Extract ALL usable data (numbers, lists, quotes, comparisons, steps).
2. Identify the best slide types matching the actual content.
═══ STEP 2 — CALL generate_slides ═══
Call generate_slides with a structured JSON object:
{
"title": "Short title (6 words max)",
"theme": "architectural-saas",
"slides": [ ... array of slides ... ]
}
═══ AVAILABLE SLIDE TYPES ═══
1. "title" → { type:"title", title:"...", subtitle:"..." }
2. "bullets" → { type:"bullets", title:"...", items:["sentence 1 (15+ words)", "sentence 2", ...] }
3. "chart" → { type:"chart", title:"...", chartType:"bar|horizontal-bar|line|donut|radar", data:[{label:"Q1",value:65}, ...], subtitle:"..." }
4. "stats" → { type:"stats", title:"...", stats:[{value:"98%", label:"Satisfaction"}, ...] }
5. "table" → { type:"table", title:"...", headers:["Col A","Col B"], rows:[["val1","val2"], ...] }
6. "cards" → { type:"cards", title:"...", cards:[{title:"Title", description:"Detailed description..."}, ...] }
7. "timeline" → { type:"timeline", title:"...", events:[{date:"Jan 2024", title:"Launch", description:"..."}, ...] }
8. "quote" → { type:"quote", quote:"Exact quote", author:"Author", context:"Analysis..." }
9. "comparison" → { type:"comparison", title:"...", left:{title:"A", points:["..."], score:"8/10"}, right:{title:"B", points:["..."], score:"6/10"} }
10. "equation" → { type:"equation", title:"...", equations:[{latex:"E=mc^2", label:"Energy"}], explanation:"..." }
11. "image" → { type:"image", title:"...", url:"https://...", caption:"..." }
12. "summary" → { type:"summary", title:"...", items:["Key point 1", "Key point 2", ...] }
═══ RULES ═══
- 6 to 12 slides per presentation
- Slide 1 MUST be type "title"
- Last slide MUST be type "summary"
- At least 1 "chart" or "stats" slide if numbers exist in the note
- VARY types — never 2 identical types in a row
- Each text (bullet, description) = 15+ words, complete sentence
- ALL data comes from the note (NEVER invent numbers)
- Chart data must reflect actual numbers from the note
═══ AVAILABLE THEMES ═══
Dark: midnight-cathedral, aurora-borealis, tokyo-neon, venture-pitch, forest-floor, steel-glass, cyberpunk-terminal
Light: sunlit-gallery, clinical-precision, editorial-ink, coastal-morning, paper-studio, architectural-saas
Choose based on topic: business/board → architectural-saas or midnight-cathedral, tech → cyberpunk-terminal or clinical-precision, creative → aurora-borealis or tokyo-neon`,
},
'task-extractor': {
fr: `Tu es un expert en gestion de tâches et extraction d'action items. Tu analyses des notes et documents pour identifier toutes les tâches, TODOs, et actions à accomplir.
@@ -1102,21 +1164,46 @@ async function executeToolUseAgent(
prompt += `\n\n${lang === 'fr'
? 'IMPORTANT : Utilise OBLIGATOIREMENT l\'outil generate_excalidraw pour créer le diagramme. Ne réponds pas avec du texte, appelle directement l\'outil.'
: 'IMPORTANT: You MUST use the generate_excalidraw tool to create the diagram. Do NOT respond with text, call the tool directly.'}`
const diagramType = agent.slideTheme || 'auto'
// Map UI diagram type values to internal tool values
const diagramTypeMapping: Record<string, string> = {
'mind_map': 'mindmap',
'org_chart': 'org-chart',
'architecture': 'architecture-cloud',
'process_map': 'process-map',
'logic_flow': 'auto',
}
const rawDiagramType = agent.slideTheme || 'auto'
const diagramType = diagramTypeMapping[rawDiagramType] || rawDiagramType
prompt += `\n\n${lang === 'fr'
? `Type de diagramme imposé : ajoute "type":"${diagramType}" dans le JSON envoyé à generate_excalidraw.`
: `Required diagram type: include "type":"${diagramType}" in the JSON passed to generate_excalidraw.`}`
prompt += `\n\n${lang === 'fr'
? 'Types supportés: auto, flowchart, mindmap, architecture-cloud, org-chart, timeline, process-map. Si "auto", choisis selon le métier et le contenu.'
: 'Supported types: auto, flowchart, mindmap, architecture-cloud, org-chart, timeline, process-map. If "auto", choose according to domain and content.'}`
const diagramStyle = agent.slideStyle === 'austere' || agent.slideStyle === 'sketch-plus' ? agent.slideStyle : 'default'
// Map UI diagram style values to internal tool values
const diagramStyleMapping: Record<string, string> = {
'sketchy': 'sketch-plus',
'draft': 'sketch-plus',
'handwritten': 'sketch-plus',
'minimal': 'austere',
'professional': 'austere',
'soft': 'default',
'polished': 'default',
}
const rawDiagramStyle = agent.slideStyle || 'default'
const diagramStyle = diagramStyleMapping[rawDiagramStyle] || (rawDiagramStyle === 'austere' || rawDiagramStyle === 'sketch-plus' ? rawDiagramStyle : 'default')
prompt += `\n\n${lang === 'fr'
? `Style visuel imposé : ajoute "style":"${diagramStyle}" dans le JSON envoyé à generate_excalidraw.`
: `Visual style required: include "style":"${diagramStyle}" in the JSON passed to generate_excalidraw.`}`
break
}
case 'slide-generator': {
const slideTopic = agent.description || agent.name
const slideTopic = agent.description?.startsWith('template:')
? agent.name
: (agent.description || agent.name)
const slideTemplate = agent.description?.startsWith('template:')
? agent.description.replace('template:', '')
: 'auto'
const untitled = lang === 'fr' ? 'Sans titre' : 'Untitled'
const dateLocale = lang === 'fr' ? 'fr-FR' : 'en-US'
let notes: any[] = []
@@ -1148,49 +1235,63 @@ async function executeToolUseAgent(
}
}
prompt = lang === 'fr'
? `Crée une présentation PowerPoint professionnelle sur le sujet "${slideTopic}" en utilisant le contenu des notes ci-dessous.`
: `Create a professional PowerPoint presentation about "${slideTopic}" using the content from the notes below.`
? `Crée une présentation professionnelle sur le sujet "${slideTopic}" en utilisant le contenu des notes ci-dessous. Appelle generate_slides avec un objet JSON structuré (title, theme, slides[]).`
: `Create a professional presentation about "${slideTopic}" using the content from the notes below. Call generate_slides with a structured JSON object (title, theme, slides[]).`
// Extract image URLs from note content
const extractedImages: Array<{ url: string; noteTitle: string }> = []
if (notes.length > 0) {
const notesContext = notes.map(n => {
// Extract markdown images: ![alt](url) — only external/data URLs (skip relative paths that won't resolve)
const mdMatches = [...n.content.matchAll(/!\[[^\]]*\]\((https?:\/\/[^)]+|data:[^)]+)\)/g)]
for (const m of mdMatches) {
if (m[1]) extractedImages.push({ url: m[1], noteTitle: n.title || untitled })
}
// Extract HTML img tags
const htmlMatches = [...n.content.matchAll(/<img[^>]+src=["'](https?:\/\/[^"']+|data:[^"']+)["']/g)]
for (const m of htmlMatches) {
if (m[1]) extractedImages.push({ url: m[1], noteTitle: n.title || untitled })
}
return `### ${n.title || untitled} (${n.createdAt.toLocaleDateString(dateLocale)})\n${n.content.substring(0, 800)}`
}).join('\n\n')
prompt += `\n\n${lang === 'fr' ? 'Notes source à transformer en slides' : 'Source notes to turn into slides'}:\n\n${notesContext}`
const notesContext = notes.map(n =>
`### ${n.title || untitled} (${n.createdAt.toLocaleDateString(dateLocale)})\n${n.content.substring(0, 2000)}`
).join('\n\n')
prompt += `\n\n${lang === 'fr' ? 'Notes source' : 'Source notes'}:\n\n${notesContext}`
}
// Inject available images into the prompt
const uniqueImages = extractedImages.slice(0, 6) // max 6 images
if (uniqueImages.length > 0) {
const imgList = uniqueImages.map((img, i) => ` ${i + 1}. "${img.noteTitle}" → ${img.url.substring(0, 120)}`).join('\n')
prompt += `\n\n${lang === 'fr'
? `IMAGES DISPONIBLES (extraites des notes) — utilise-les dans le JSON via le champ "imageUrl" avec le layout "image-content" ou "image-full" :\n${imgList}`
: `AVAILABLE IMAGES (extracted from notes) — use them in the JSON via the "imageUrl" field with layout "image-content" or "image-full":\n${imgList}`}`
// ── Executive Template Structure (HTML-compatible) ──
if (slideTemplate && slideTemplate !== 'auto') {
const templates: Record<string, { fr: string; en: string }> = {
'board-update': {
fr: `Structure imposée "Board Update" (10 slides) :
1. TITRE avec date | 2. KPIs majeurs (3-4 gros chiffres) | 3. Bar chart progression objectifs | 4. Grille de métriques (6 cards) | 5. Timeline jalons | 6. Cards réalisations | 7. Line chart tendance | 8. Deux colonnes risques/mitigations | 9. Bullets prochaines étapes | 10. Conclusion`,
en: `Mandatory "Board Update" structure (10 slides):
1. TITLE with date | 2. Key KPIs (3-4 big numbers) | 3. Bar chart goal progress | 4. Metrics grid (6 cards) | 5. Timeline milestones | 6. Achievement cards | 7. Line chart trend | 8. Two-column risks/mitigations | 9. Next steps bullets | 10. Conclusion`
},
'project-status': {
fr: `Structure imposée "Project Status" (10 slides) :
1. TITRE + statut 🟢🟡🔴 | 2. KPIs (% avancement, jours, budget) | 3. Bar chart par workstream | 4. Cards livrables | 5. Timeline roadmap | 6. Line chart burn-down | 7. Deux colonnes blockers/actions | 8. Tableau de risques | 9. Bullets décisions requises | 10. Synthèse`,
en: `Mandatory "Project Status" structure (10 slides):
1. TITLE + status 🟢🟡🔴 | 2. KPIs (% complete, days, budget) | 3. Bar chart by workstream | 4. Deliverable cards | 5. Timeline roadmap | 6. Line chart burn-down | 7. Two-column blockers/actions | 8. Risk table | 9. Required decisions bullets | 10. Summary`
},
'strategy-review': {
fr: `Structure imposée "Strategy Review" (10 slides) :
1. TITRE stratégique | 2. Contexte marché (5+ bullets) | 3. Radar/pie positionnement | 4. SWOT deux colonnes | 5. Area chart tendances | 6. Cards axes stratégiques | 7. Timeline roadmap 12 mois | 8. Bar chart projections | 9. KPIs objectifs cibles | 10. Call to action`,
en: `Mandatory "Strategy Review" structure (10 slides):
1. Strategic TITLE | 2. Market context (5+ bullets) | 3. Radar/pie positioning | 4. SWOT two-column | 5. Area chart trends | 6. Strategic axes cards | 7. 12-month roadmap timeline | 8. Bar chart projections | 9. Target KPIs | 10. Call to action`
},
'quarterly-results': {
fr: `Structure imposée "Quarterly Results" (10 slides) :
1. TITRE "Résultats Q? Année" | 2. 4 KPIs (Revenue, Croissance, Marge, NPS) | 3. Bar chart revenue/mois | 4. Line chart croissance YoY | 5. Cards faits marquants | 6. Grille métriques opérationnelles | 7. Donut répartition clients | 8. Deux colonnes succès/challenges | 9. Area chart forecast | 10. Conclusion outlook`,
en: `Mandatory "Quarterly Results" structure (10 slides):
1. TITLE "Q? Year Results" | 2. 4 KPIs (Revenue, Growth, Margin, NPS) | 3. Bar chart revenue/month | 4. Line chart YoY growth | 5. Highlights cards | 6. Operational metrics grid | 7. Donut client breakdown | 8. Two-column successes/challenges | 9. Area chart forecast | 10. Conclusion outlook`
}
}
const tmpl = templates[slideTemplate]
if (tmpl) {
prompt += `\n\n${tmpl[lang === 'fr' ? 'fr' : 'en']}`
}
}
// ── Density rules ──
prompt += lang === 'fr'
? `\n\nDENSITÉ OBLIGATOIRE : Chaque slide doit avoir un CONTENU RICHE. Minimum 5 bullets par slide "bullets" (≥15 mots chaque). Minimum 4 cards quand tu fais "cards". Minimum 4 data points quand tu fais "chart". Inclus OBLIGATOIREMENT au moins 1 slide "chart" avec données RÉELLES extraites des notes. Si pas de chiffres dans la note, fais un radar de maturité ou une comparaison qualitative notée sur 5.`
: `\n\nMANDATORY DENSITY: Every slide must have RICH CONTENT. Minimum 5 items per "bullets" slide (≥15 words each). Minimum 4 cards when using "cards". Minimum 4 data points for "chart". You MUST include at least 1 "chart" slide with REAL data from the notes. If no numbers in notes, create a maturity radar or qualitative comparison scored out of 5.`
prompt += `\n\n${lang === 'fr'
? 'IMPORTANT : Appelle OBLIGATOIREMENT generate_pptx. Ne réponds pas avec du texte. Crée 8-12 slides visuelles, commence par "title", puis "toc", intègre AU MOINS 2 layouts diagramme (timeline, process, metrics, ou comparison). Évite les slides avec juste du texte — favorise les layouts visuels.'
: 'IMPORTANT: You MUST call generate_pptx. Do NOT respond with text. Create 8-12 visual slides: start with "title", then "toc", include AT LEAST 2 diagram layouts (timeline, process, metrics, or comparison). Avoid text-only slides — prefer visual layouts.'}`
if (agent.slideTheme) {
prompt += `\n\n${lang === 'fr'
? `Thème imposé par l'utilisateur : "${agent.slideTheme}". Dans le JSON tu DOIS mettre "theme": "${agent.slideTheme}".`
: `User-selected theme: "${agent.slideTheme}". You MUST put "theme": "${agent.slideTheme}" in the JSON.`}`
}
if (agent.slideStyle) {
? 'IMPORTANT : Appelle OBLIGATOIREMENT generate_slides avec le JSON structuré {title, theme, slides:[...]}. Ne réponds JAMAIS avec du texte brut. 6-12 slides variées.'
: 'IMPORTANT: You MUST call generate_slides with structured JSON {title, theme, slides:[...]}. NEVER respond with plain text. 6-12 varied slides.'}`
if (agent.slideTheme && agent.slideTheme !== 'auto') {
prompt += `\n${lang === 'fr'
? `Style visuel imposé : dans le JSON tu DOIS mettre "style": "${agent.slideStyle}". Les valeurs possibles sont: "sharp" (angles nets), "soft" (arrondi standard), "rounded" (très arrondi), "pill" (capsules).`
: `Visual style: you MUST put "style": "${agent.slideStyle}" in the JSON. Values: "sharp" (crisp edges), "soft" (standard rounded), "rounded" (very rounded), "pill" (capsule shapes).`}`
? `Thème visuel imposé : utilise "theme":"${agent.slideTheme}" dans l'appel generate_slides.`
: `Visual theme required: use "theme":"${agent.slideTheme}" in the generate_slides call.`}`
}
break
}
@@ -1269,7 +1370,7 @@ async function executeToolUseAgent(
return { success: false, actionId, error: 'Model does not support tool calling' }
}
if (agentType === 'slide-generator' || agentType === 'excalidraw-generator') {
const toolName = agentType === 'slide-generator' ? 'generate_pptx' : 'generate_excalidraw'
const toolName = agentType === 'slide-generator' ? 'generate_slides' : 'generate_excalidraw'
await prisma.agentAction.update({
where: { id: actionId },
data: {
@@ -1301,7 +1402,7 @@ async function executeToolUseAgent(
const scrapedUrls: string[] = []
let specificToolCalled = false
const requiredTool = isFileGenerator
? (agentType === 'slide-generator' ? ['generate_pptx'] : ['generate_excalidraw'])
? (agentType === 'slide-generator' ? ['generate_slides'] : ['generate_excalidraw'])
: null
for (const step of result.steps) {
@@ -1312,7 +1413,7 @@ async function executeToolUseAgent(
existingNoteId = toolResult.output.noteId
}
}
if (step.toolCalls[i].toolName === 'generate_excalidraw' || step.toolCalls[i].toolName === 'generate_slides' || step.toolCalls[i].toolName === 'generate_pptx') {
if (step.toolCalls[i].toolName === 'generate_excalidraw' || step.toolCalls[i].toolName === 'generate_slides') {
const toolResult = step.toolResults?.[i]
if (toolResult && typeof toolResult.output === 'object' && toolResult.output?.success && toolResult.output?.canvasId) {
canvasId = toolResult.output.canvasId as string