Files
Momento/memento-note/lib/ai/tools/slides.tool.ts
Antigravity 556a0b2f3f feat(credits): solde IA unique (option M), packs Stripe et polish billing
Un pot de crédits global (BASIC 100 / PRO 1 000 / BUSINESS 4 000) remplace
les seaux par fonction côté débit. Packs one-shot S/M/L, admin sans seaux
legacy, usage multi-features, hints de coût, anti-flash thème et hydratation
admin. Inclut aussi le pipeline slides renforcé et la page publique polishée.
2026-07-16 20:43:18 +00:00

203 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use server'
import { tool } from 'ai'
import { z } from 'zod'
import { toolRegistry } from './registry'
import { prisma } from '@/lib/prisma'
import { buildPresentationHTML } from './slides-html-builder'
import { normalizeSlideDeck, SLIDE_HARD_CAP } from '@/lib/ai/services/slide-content-quality'
const notesField = z.string().optional().describe('Optional speaker notes (12 sentences for the presenter, not shown on slide)')
const slideSchema = z.discriminatedUnion('type', [
z.object({
type: z.literal('title'),
title: z.string().describe('Action title / deck name'),
subtitle: z.string().optional().describe('One-line context or audience promise'),
notes: notesField,
}),
z.object({
type: z.literal('bullets'),
title: z.string().describe('Action title = the insight, not a topic label'),
items: z.array(z.string()).max(5).describe('35 short claims (818 words), each distinct, from the note'),
notes: notesField,
}),
z.object({
type: z.literal('chart'),
title: z.string().describe('Action title stating what the chart proves'),
chartType: z.enum(['bar', 'horizontal-bar', 'line', 'donut', 'radar']),
data: z.array(z.object({ label: z.string(), value: z.number() })).min(2).max(8),
subtitle: z.string().optional(),
notes: notesField,
}),
z.object({
type: z.literal('stats'),
title: z.string(),
stats: z.array(z.object({ value: z.string(), label: z.string() })).min(2).max(4),
notes: notesField,
}),
z.object({
type: z.literal('table'),
title: z.string(),
headers: z.array(z.string()).max(6),
rows: z.array(z.array(z.string())).max(8),
notes: notesField,
}),
z.object({
type: z.literal('cards'),
title: z.string(),
cards: z
.array(z.object({ title: z.string(), description: z.string() }))
.min(2)
.max(6)
.describe('24 cards preferred; short titles + 12 sentence descriptions'),
notes: notesField,
}),
z.object({
type: z.literal('timeline'),
title: z.string(),
events: z
.array(z.object({ date: z.string(), title: z.string(), description: z.string().optional() }))
.min(2)
.max(6),
notes: notesField,
}),
z.object({
type: z.literal('quote'),
quote: z.string().describe('Exact or faithful quote from the note'),
author: z.string().optional(),
context: z.string().optional().describe('Why this quote matters'),
notes: notesField,
}),
z.object({
type: z.literal('comparison'),
title: z.string().describe('Action title of the comparison insight'),
left: z.object({
title: z.string(),
points: z.array(z.string()).max(4),
score: z.string().optional(),
}),
right: z.object({
title: z.string(),
points: z.array(z.string()).max(4),
score: z.string().optional(),
}),
notes: notesField,
}),
z.object({
type: z.literal('equation'),
title: z.string(),
equations: z.array(z.object({ latex: z.string(), label: z.string().optional() })).max(4),
explanation: z.string().optional(),
notes: notesField,
}),
z.object({
type: z.literal('image'),
title: z.string(),
url: z.string().optional().describe('Only if a real image URL exists in the note'),
caption: z.string().optional(),
notes: notesField,
}),
z.object({
type: z.literal('summary'),
title: z.string().describe('e.g. Next steps / Key takeaways'),
items: z.array(z.string()).max(5).describe('35 actionable takeaways'),
notes: notesField,
}),
])
toolRegistry.register({
name: 'generate_slides',
description: `Create an executive presentation from structured slide data (rendered to HTML by the server).
CONTENT RULES (quality > quantity):
- One idea per slide; titles are ACTION TITLES (insights), not topic labels like "Context" or "Analysis"
- Narrative: title → problem/context → insight → evidence → implications → summary
- Bullets: 35 max, short claims (≈818 words), no filler paragraphs
- chart/stats ONLY with real numbers from the source note — NEVER invent KPIs or radar scores
- First slide type "title", last type "summary"
- Hard limit: ${SLIDE_HARD_CAP} slides; fewer for short notes
- All facts from the note; optional "notes" field for speaker cues
Slide types: title | bullets | chart | stats | table | cards | timeline | quote | comparison | equation | image | summary`,
isInternal: true,
buildTool: (ctx) =>
tool({
description: `Create a high-quality executive deck. Prefer short action titles and sparse, high-signal content over dense filler.
Types:
- title: title, subtitle?
- bullets: title (insight), items[3-5] short claims
- chart: ONLY real numbers — chartType bar|horizontal-bar|line|donut|radar, data[{label,value}]
- stats: 24 real KPIs {value,label}
- table / cards / timeline / quote / comparison / equation / image / summary
Rules: first=title, last=summary, max ${SLIDE_HARD_CAP} slides, no invented data, no fake charts.`,
inputSchema: z.object({
title: z.string().describe('Short deck title (max ~8 words)'),
theme: z
.string()
.optional()
.describe(
'Visual recipe: architectural-saas, midnight-cathedral, aurora-borealis, venture-pitch, clinical-precision, coastal-morning, etc.',
),
slides: z.array(slideSchema).max(SLIDE_HARD_CAP).describe(`Slide array, 2${SLIDE_HARD_CAP} items`),
}),
execute: async ({ title, theme, slides }) => {
try {
const normalized = normalizeSlideDeck({ title, theme, slides: slides as unknown[] })
const html = buildPresentationHTML({
title: normalized.title,
theme: normalized.theme,
slides: normalized.slides as any,
})
const canvas = await prisma.canvas.create({
data: {
name: normalized.title || 'Présentation',
data: JSON.stringify({
type: 'slides',
title: normalized.title || 'Présentation',
html,
slideCount: normalized.slides.length,
theme: normalized.theme || 'architectural-saas',
spec: {
title: normalized.title,
theme: normalized.theme,
slides: normalized.slides,
},
}),
userId: ctx.userId,
},
})
if (ctx.actionId) {
await prisma.agentAction
.update({
where: { id: ctx.actionId },
data: {
status: 'success',
result: canvas.id,
log: `Slides generated: ${normalized.slides.length} slides, ${Math.round(html.length / 1024)}KB (quality-normalized)`,
},
})
.catch((err) => console.error('[Slides Tool] Failed to update action status:', err))
}
return {
success: true,
canvasId: canvas.id,
canvasName: canvas.name,
slideCount: normalized.slides.length,
message: `Presentation "${canvas.name}" created with ${normalized.slides.length} slides.`,
}
} catch (e: any) {
console.error('[Slides Tool] FATAL:', e)
return { success: false, error: `Failed: ${e.message}` }
}
},
}),
})