'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 (1–2 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('3–5 short claims (8–18 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('2–4 cards preferred; short titles + 1–2 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('3–5 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: 3–5 max, short claims (≈8–18 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: 2–4 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}` } } }, }), })