feat(agents): refonte complète slide-generator + excalidraw-generator
Some checks failed
Deploy to Production / Build and Deploy (push) Failing after 3s

Slide generator (generate_pptx):
- Pivot vers génération PowerPoint natif (pptxgenjs) au lieu de Reveal.js HTML
- 4 nouveaux layouts diagramme : timeline, process, comparison, metrics
- 2 nouveaux layouts image : image-content (texte + image), image-full (plein cadre)
- Redesign visuel de tous les layouts (cover split, section full-bleed, header band)
- Palettes corrigées : bg blanc sur toutes les palettes, contrastes réels
- fit:shrink systématique sur tous les textes pour éviter les débordements
- Extraction automatique des images des notes (Markdown/HTML) et injection dans le prompt IA
- Prompt IA renforcé : impose "style" et "theme" explicitement dans le JSON, impose ≥2 layouts diagramme
- Fix overlap timeline : zones de texte calculées sans collision avec les cercles
- Notification agent mise à jour : bouton download .pptx au lieu d'ouvrir HTML

Excalidraw generator:
- Layout Dagre/ELK pour graphes auto-positionnés
- Styles visuels : coloré, austère, sketch-plus (Virgil font)
- Zones/containers pour architecture-cloud
- Sanitisation du graphe et métriques de qualité de layout

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Antigravity
2026-05-05 20:55:15 +00:00
parent 21fb56de3f
commit 129d5541e6
11 changed files with 7441 additions and 3543 deletions

View File

@@ -23,12 +23,13 @@ import '../tools'
// --- Types ---
export type AgentType = 'scraper' | 'researcher' | 'monitor' | 'custom'
export type AgentType = 'scraper' | 'researcher' | 'monitor' | 'custom' | 'slide-generator' | 'excalidraw-generator'
export interface AgentExecutionResult {
success: boolean
actionId: string
noteId?: string
canvasId?: string
error?: string
}
@@ -640,6 +641,7 @@ async function executeCustomAgent(
return { success: true, actionId, noteId: note.id }
}
// --- System Prompts (bilingual) ---
const SYSTEM_PROMPTS: Record<string, Record<Lang, string>> = {
@@ -792,6 +794,181 @@ RULES:
- Respond in English.
- Cite sources if you have scraped web pages.`,
},
'excalidraw-generator': {
fr: `Tu es un architecte visuel expert en diagrammes Excalidraw. Tu reçois du contenu (notes, documents) et tu dois créer un diagramme qui ARGUE visuellement — pas juste des boîtes avec du texte.
Appelle DIRECTEMENT generate_excalidraw. Ne réponds PAS avec du texte.
## PHILOSOPHIE
Un diagramme doit MONTRER des relations que le texte seul ne peut pas exprimer.
- Le test de l'isomorphisme : si tu enlèves tout le texte, la structure seule doit-elle communiquer le concept ?
- Chaque concept doit avoir une forme qui reflète son COMPORTEMENT, pas juste son nom.
- Les flèches sont OBLIGATOIRES — un diagramme sans flèches est inutile.
## FORMAT (simplifié — auto-layout automatique)
{
"title": "Titre du Diagramme",
"nodes": [
{"id":"c","label":"Concept Central","type":"ellipse"},
{"id":"n1","label":"Sous-concept 1"},
{"id":"n2","label":"Processus","type":"rect"},
{"id":"n3","label":"Décision ?","type":"diamond"},
{"id":"n4","label":"Résultat","type":"ellipse"}
],
"edges": [
{"from":"c","to":"n1","label":"déclenche"},
{"from":"n1","to":"n2"},
{"from":"n2","to":"n3","label":"produit"},
{"from":"n3","to":"n4","label":"oui"},
{"from":"n3","to":"n2","label":"non"}
]
}
Ce format crée AUTOMATIQUEMENT formes, textes ET flèches avec bindings corrects.
## TYPES DE FORMES (utilise le bon type pour chaque concept)
| Type | Quand l'utiliser |
|------|-----------------|
| "ellipse" | Départ, arrivée, concept abstrait, origine — le premier nœud EST TOUJOURS une ellipse |
| "rect" (défaut) | Processus, action, étape, élément concret |
| "diamond" | Décision, condition, choix, point de bifurcation |
## PATTERNS VISUELS (varie selon le contenu)
- **Flux séquentiel** : A → B → C → D (lineaire, utilise des rect)
- **Hub & spoke** : Centre (ellipse) rayonne vers sous-concepts (fan-out)
- **Cycle/boucle** : A → B → C → A (processus itératif, feedback)
- **Arbre hiérarchique** : Parent → enfants (structure, organisation)
- **Convergence** : Plusieurs sources → un résultat (synthèse, agrégation)
- **Comparaison** : Deux branches parallèles qui se rejoignent
## RÈGLES STRICTES
1. **4 à 10 nœuds** — pas moins, pas plus
2. **Premier nœud = ellipse** (point d'entrée)
3. **TOUS les nœuds connectés** — chaque nœud doit avoir au moins 1 edge entrant ou sortant
4. **Labels courts** — max 40 caractères par label
5. **Edge labels** — ajoute des labels sur les flèches importantes pour expliquer la relation
6. **Utilise "diamond"** pour au moins un nœud si le contenu implique une décision
7. **Pas de nœuds orphelins** — chaque nœud doit être atteignable depuis le nœud central
8. **Analyse le contenu d'abord** — identifie les concepts clés et leurs relations AVANT de créer le JSON
9. **Appelle generate_excalidraw DIRECTEMENT**, ne réponds pas avec du texte`,
en: `You are a visual architect expert in Excalidraw diagrams. You receive content (notes, documents) and must create a diagram that ARGUES visually — not just boxes with text.
Call generate_excalidraw DIRECTLY. Do NOT respond with text.
## PHILOSOPHY
A diagram must SHOW relationships that text alone cannot express.
- The Isomorphism Test: If you removed all text, would the structure alone communicate the concept?
- Each concept must have a shape that reflects its BEHAVIOR, not just its name.
- Arrows are MANDATORY — a diagram without arrows is useless.
## FORMAT (simplified — auto-layout automatic)
{
"title": "Diagram Title",
"nodes": [
{"id":"c","label":"Central Concept","type":"ellipse"},
{"id":"n1","label":"Sub-concept 1"},
{"id":"n2","label":"Process","type":"rect"},
{"id":"n3","label":"Decision?","type":"diamond"},
{"id":"n4","label":"Result","type":"ellipse"}
],
"edges": [
{"from":"c","to":"n1","label":"triggers"},
{"from":"n1","to":"n2"},
{"from":"n2","to":"n3","label":"produces"},
{"from":"n3","to":"n4","label":"yes"},
{"from":"n3","to":"n2","label":"no"}
]
}
This format AUTOMATICALLY creates shapes, text, AND arrows with correct bindings.
## SHAPE TYPES (use the right type for each concept)
| Type | When to use |
|------|------------|
| "ellipse" | Start, end, abstract concept, origin — first node is ALWAYS an ellipse |
| "rect" (default) | Process, action, step, concrete element |
| "diamond" | Decision, condition, choice, branching point |
## VISUAL PATTERNS (vary based on content)
- **Sequential flow**: A → B → C → D (linear, use rects)
- **Hub & spoke**: Center (ellipse) radiates to sub-concepts (fan-out)
- **Cycle/loop**: A → B → C → A (iterative process, feedback)
- **Hierarchical tree**: Parent → children (structure, organization)
- **Convergence**: Multiple sources → one result (synthesis, aggregation)
- **Comparison**: Two parallel branches that merge
## STRICT RULES
1. **4 to 10 nodes** — no less, no more
2. **First node = ellipse** (entry point)
3. **ALL nodes connected** — every node must have at least 1 incoming or outgoing edge
4. **Short labels** — max 40 chars per label
5. **Edge labels** — add labels on important arrows to explain the relationship
6. **Use "diamond"** for at least one node if the content involves a decision
7. **No orphan nodes** — every node must be reachable from the central node
8. **Analyze content first** — identify key concepts and their relationships BEFORE creating JSON
9. **Call generate_excalidraw DIRECTLY**, do not respond with text`,
},
'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.
Tu dois OBLIGATOIREMENT appeler l'outil generate_pptx. Ne réponds JAMAIS avec du texte — appelle l'outil directement.
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"
- "timeline" : étapes chronologiques ou roadmap (content items : "Étape: description")
- "process" : étapes numérotées avec détails (content items : "Action: explication")
- "metrics" : KPIs visuels avec grandes valeurs colorées (content items : "VALEUR: libellé")
- "comparison" : deux colonnes contrastées (subtitle="Avant | Après" ou "Option A | Option B")
- "cards" : fonctionnalités en grille 2-3 colonnes (3-6 items)
- "section" : séparateur de section (title=titre, content=[] — le numéro est auto-généré)
- "quote" : citation impactante (title=texte, subtitle=auteur)
- "summary" : récapitulatif final
- "content" : liste de points SEULEMENT si aucun layout visuel n'est adapté (max 7 points)
- Ne JAMAIS répéter le même layout consécutivement
- Pour "section" : ne pas mettre le numéro dans content, laisser content=[]
- Thèmes recommandés pour un rendu moderne : vibrant_tech, platinum_white_gold, business_authority, pure_tech_blue, tech_night
- 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.
You MUST call the generate_pptx tool. NEVER respond with text — call the tool directly.
MANDATORY DESIGN RULES:
- 8-12 slides, each slide has a distinct layout
- Slide 1: "title" (strong title + punchy subtitle)
- Slide 2: "toc" (numbered table of contents)
- Use AT LEAST 2 "diagram" layouts from: "timeline", "process", "metrics", "comparison"
- "timeline": chronological steps or roadmap (content items: "Step: description")
- "process": numbered steps with details (content items: "Action: explanation")
- "metrics": visual KPIs with large colored values (content items: "VALUE: label")
- "comparison": two contrasting columns (subtitle="Before | After" or "Option A | Option B")
- "cards": feature grid 2-3 columns (3-6 items)
- "section": section divider (title=heading, content=[] — number is auto-generated)
- "quote": impactful quote (title=text, subtitle=author)
- "summary": closing recap
- "content": bullet list ONLY if no visual layout fits (max 7 points)
- NEVER repeat the same layout consecutively
- For "section": do NOT put numbers in content, leave content=[]
- Recommended themes for modern look: vibrant_tech, platinum_white_gold, business_authority, pure_tech_blue, tech_night
- Concise points (max 100 chars), short impactful titles
- Strict JSON for generate_pptx, no text outside JSON.`,
},
}
// --- Tool-Use Agent ---
@@ -800,8 +977,9 @@ async function executeToolUseAgent(
agent: {
id: string; name: string; description?: string | null; type?: string | null
role: string; sourceUrls?: string | null; sourceNotebookId?: string | null
targetNotebookId?: string | null; userId: string; tools?: string | null; maxSteps?: number
includeImages?: boolean
sourceNoteIds?: string | null; targetNotebookId?: string | null; userId: string
tools?: string | null; maxSteps?: number; includeImages?: boolean
slideTheme?: string | null; slideStyle?: string | null
},
actionId: string,
lang: Lang,
@@ -837,6 +1015,7 @@ async function executeToolUseAgent(
// Build system prompt: use localized prompt for type, with optional user custom role
const agentType = (agent.type || 'custom') as AgentType
const isFileGenerator = agentType === 'slide-generator' || agentType === 'excalidraw-generator'
const promptsForType = SYSTEM_PROMPTS[agentType] || SYSTEM_PROMPTS.custom
const baseSystemPrompt = promptsForType[lang]
const toolList = toolNames.map(n => `- ${n}`).join('\n')
@@ -886,6 +1065,141 @@ async function executeToolUseAgent(
}
break
}
case 'excalidraw-generator': {
const untitled = lang === 'fr' ? 'Sans titre' : 'Untitled'
const dateLocale = lang === 'fr' ? 'fr-FR' : 'en-US'
let notes: any[] = []
const specificNoteIds: string[] = agent.sourceNoteIds ? JSON.parse(agent.sourceNoteIds) : []
if (specificNoteIds.length > 0) {
notes = await prisma.note.findMany({
where: { id: { in: specificNoteIds }, userId: agent.userId, isArchived: false, trashedAt: null },
select: { id: true, title: true, content: true, createdAt: true }
})
} else if (agent.sourceNotebookId) {
notes = await prisma.note.findMany({
where: { notebookId: agent.sourceNotebookId, userId: agent.userId, isArchived: false, trashedAt: null },
orderBy: { createdAt: 'desc' }, take: 10,
select: { id: true, title: true, content: true, createdAt: true }
})
} else {
const notebooks = await prisma.notebook.findMany({
where: { userId: agent.userId },
include: { _count: { select: { notes: { where: { trashedAt: null } } } } },
orderBy: { createdAt: 'desc' }
})
const best = notebooks.find(n => n._count.notes > 0)
if (best) {
notes = await prisma.note.findMany({
where: { notebookId: best.id, userId: agent.userId, isArchived: false, trashedAt: null },
orderBy: { createdAt: 'desc' }, take: 10,
select: { id: true, title: true, content: true, createdAt: true }
})
}
}
prompt = lang === 'fr'
? `Crée un diagramme Excalidraw (mind map ou flowchart) représentant visuellement les concepts clés et leurs relations.`
: `Create an Excalidraw diagram (mind map or flowchart) that visually represents the key concepts and their relationships.`
if (notes.length > 0) {
const notesContext = notes.map(n =>
`### ${n.title || untitled} (${n.createdAt.toLocaleDateString(dateLocale)})\n${n.content.substring(0, 800)}`
).join('\n\n')
prompt += `\n\n${lang === 'fr' ? 'Notes source à analyser' : 'Source notes to analyze'}:\n\n${notesContext}`
}
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'
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'
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 untitled = lang === 'fr' ? 'Sans titre' : 'Untitled'
const dateLocale = lang === 'fr' ? 'fr-FR' : 'en-US'
let notes: any[] = []
const specificNoteIds: string[] = agent.sourceNoteIds ? JSON.parse(agent.sourceNoteIds) : []
if (specificNoteIds.length > 0) {
notes = await prisma.note.findMany({
where: { id: { in: specificNoteIds }, userId: agent.userId, isArchived: false, trashedAt: null },
select: { id: true, title: true, content: true, createdAt: true }
})
} else if (agent.sourceNotebookId) {
notes = await prisma.note.findMany({
where: { notebookId: agent.sourceNotebookId, userId: agent.userId, isArchived: false, trashedAt: null },
orderBy: { createdAt: 'desc' }, take: 15,
select: { id: true, title: true, content: true, createdAt: true }
})
} else {
const notebooks = await prisma.notebook.findMany({
where: { userId: agent.userId },
include: { _count: { select: { notes: { where: { trashedAt: null } } } } },
orderBy: { createdAt: 'desc' }
})
const best = notebooks.find(n => n._count.notes > 0)
if (best) {
notes = await prisma.note.findMany({
where: { notebookId: best.id, userId: agent.userId, isArchived: false, trashedAt: null },
orderBy: { createdAt: 'desc' }, take: 15,
select: { id: true, title: true, content: true, createdAt: true }
})
}
}
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.`
// 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}`
}
// 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}`}`
}
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) {
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).`}`
}
break
}
default: {
const urls: string[] = agent.sourceUrls ? JSON.parse(agent.sourceUrls) : []
prompt = agent.role || (lang === 'fr' ? 'Accomplis la tâche demandée en utilisant les outils disponibles.' : 'Accomplish the requested task using available tools.')
@@ -928,6 +1242,19 @@ 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'
await prisma.agentAction.update({
where: { id: actionId },
data: {
status: 'failure',
log: lang === 'fr'
? `L'IA n'a pas appelé l'outil ${toolName}. Le modèle a répondu avec du texte au lieu de générer le fichier. Modèle: "${sysConfig.AI_MODEL_CHAT}". Essayez un modèle compatible avec le function calling.`
: `The AI did not call the ${toolName} tool. The model responded with text instead of generating the file. Model: "${sysConfig.AI_MODEL_CHAT}". Try a model that supports function calling.`,
}
})
return { success: false, actionId, error: `AI did not call ${toolName} tool` }
}
}
// Build tool log trace
@@ -942,8 +1269,15 @@ async function executeToolUseAgent(
}))
// Check if AI already created a note via note_create tool
// Or if excalidraw/slide generator created a canvas
let existingNoteId: string | null = null
let canvasId: string | null = null
const scrapedUrls: string[] = []
let specificToolCalled = false
const requiredTool = isFileGenerator
? (agentType === 'slide-generator' ? ['generate_pptx'] : ['generate_excalidraw'])
: null
for (const step of result.steps) {
for (let i = 0; i < step.toolCalls.length; i++) {
if (step.toolCalls[i].toolName === 'note_create') {
@@ -952,6 +1286,13 @@ 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') {
const toolResult = step.toolResults?.[i]
if (toolResult && typeof toolResult.output === 'object' && toolResult.output?.success && toolResult.output?.canvasId) {
canvasId = toolResult.output.canvasId as string
specificToolCalled = true
}
}
if (step.toolCalls[i].toolName === 'web_scrape') {
const toolResult = step.toolResults?.[i]
if (toolResult && typeof toolResult.output === 'object' && toolResult.output?.url) {
@@ -961,10 +1302,32 @@ async function executeToolUseAgent(
}
}
// For file generators: if the specific tool was NOT called, fail immediately
if (isFileGenerator && !specificToolCalled) {
const toolName = requiredTool!.join(' or ')
const toolLogStr = JSON.stringify(toolLog)
await prisma.agentAction.update({
where: { id: actionId },
data: {
status: 'failure',
log: lang === 'fr'
? `L'IA n'a pas appelé l'outil ${toolName}. Le modèle a peut-être répondu avec du texte. Modèle: "${sysConfig.AI_MODEL_CHAT}". Essayez un modèle compatible avec le function calling.`
: `The AI did not call the ${toolName} tool. The model may have responded with text. Model: "${sysConfig.AI_MODEL_CHAT}". Try a model that supports function calling.`,
toolLog: toolLogStr,
}
})
return { success: false, actionId, error: `AI did not call ${toolName} tool` }
}
const totalToolCalls = result.steps.reduce((acc, s) => acc + s.toolCalls.length, 0)
let noteId: string
if (existingNoteId) {
let noteId: string | undefined
if (isFileGenerator) {
// File generators NEVER create notes — only canvases
noteId = undefined
} else if (canvasId) {
noteId = undefined
} else if (existingNoteId) {
if (agent.targetNotebookId) {
await prisma.note.update({
where: { id: existingNoteId },
@@ -1010,17 +1373,23 @@ async function executeToolUseAgent(
console.log(`[AgentExecutor] includeImages enabled but no scraped URLs found in tool results`)
}
const resultId = canvasId || noteId
const resultType = canvasId ? 'Canvas' : 'Note'
await prisma.agentAction.update({
where: { id: actionId },
data: {
status: 'success',
result: noteId,
log: `Tool-use: ${totalToolCalls} calls, ${Math.round(duration / 1000)}s. Note: ${noteId}${imageCount > 0 ? `, ${imageCount} images` : ''}`,
result: resultId,
log: `Tool-use: ${totalToolCalls} calls, ${Math.round(duration / 1000)}s. ${resultType}: ${resultId}${imageCount > 0 ? `, ${imageCount} images` : ''}`,
toolLog: JSON.stringify(toolLog),
}
})
return { success: true, actionId, noteId }
if (canvasId) {
return { success: true, actionId, canvasId }
}
return { success: true, actionId, noteId: resultId }
}
// --- Agent Email Notification ---
@@ -1097,6 +1466,10 @@ export async function executeAgent(agentId: string, userId: string, promptOverri
case 'custom':
result = await executeCustomAgent(agent, action.id, lang)
break
case 'slide-generator':
case 'excalidraw-generator':
result = await executeToolUseAgent(agent, action.id, lang, promptOverride)
break
default:
result = await executeScraperAgent(agent, action.id, lang)
}
@@ -1117,7 +1490,7 @@ export async function executeAgent(agentId: string, userId: string, promptOverri
data: { lastRun: new Date(), ...nextRunUpdate }
})
if (result.success && agent.notifyEmail) {
if (result.success && agent.notifyEmail && !result.canvasId) {
const note = result.noteId
? await prisma.note.findUnique({ where: { id: result.noteId }, select: { content: true } })
: null
@@ -1128,15 +1501,26 @@ export async function executeAgent(agentId: string, userId: string, promptOverri
// Create in-app notification for agent result
if (result.success) {
const isCanvas = !!result.canvasId
const resultId = result.canvasId || result.noteId
const isSlides = isCanvas && (agent.type === 'slide-generator')
let message: string
if (isSlides) {
message = lang === 'fr' ? `Présentation PowerPoint prête — cliquez pour télécharger le fichier .pptx.` : `PowerPoint presentation ready — click to download the .pptx file.`
} else if (isCanvas) {
message = lang === 'fr' ? `Diagramme Excalidraw créé — cliquez pour ouvrir dans le Lab.` : `Excalidraw diagram created — click to open in Lab.`
} else if (result.noteId) {
message = lang === 'fr' ? `L'agent a terminé avec succès — note créée.` : `Agent completed successfully — note created.`
} else {
message = lang === 'fr' ? `L'agent a terminé avec succès.` : `Agent completed successfully.`
}
await createNotification({
userId,
type: 'agent_success',
type: isSlides ? 'agent_slides_ready' : isCanvas ? 'agent_canvas_ready' : 'agent_success',
title: agent.name,
message: result.noteId
? (lang === 'fr' ? `L'agent a terminé avec succès — note créée.` : `Agent completed successfully — note created.`)
: (lang === 'fr' ? `L'agent a terminé avec succès.` : `Agent completed successfully.`),
actionUrl: result.noteId ? `/?openNote=${result.noteId}` : '/agents',
relatedId: result.noteId || agentId,
message,
actionUrl: isCanvas ? `/lab?id=${result.canvasId}` : result.noteId ? `/?openNote=${result.noteId}` : '/agents',
relatedId: resultId || agentId,
})
}