'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 { incrementUsageAsync } from '@/lib/entitlements' const slideSchema = z.discriminatedUnion('type', [ z.object({ type: z.literal('title'), title: z.string(), subtitle: z.string().optional() }), z.object({ type: z.literal('bullets'), title: z.string(), items: z.array(z.string()) }), z.object({ type: z.literal('chart'), title: z.string(), chartType: z.enum(['bar', 'horizontal-bar', 'line', 'donut', 'radar']), data: z.array(z.object({ label: z.string(), value: z.number() })), subtitle: z.string().optional() }), z.object({ type: z.literal('stats'), title: z.string(), stats: z.array(z.object({ value: z.string(), label: z.string() })) }), z.object({ type: z.literal('table'), title: z.string(), headers: z.array(z.string()), rows: z.array(z.array(z.string())) }), z.object({ type: z.literal('cards'), title: z.string(), cards: z.array(z.object({ title: z.string(), description: z.string() })) }), z.object({ type: z.literal('timeline'), title: z.string(), events: z.array(z.object({ date: z.string(), title: z.string(), description: z.string().optional() })) }), z.object({ type: z.literal('quote'), quote: z.string(), author: z.string().optional(), context: z.string().optional() }), z.object({ type: z.literal('comparison'), title: z.string(), left: z.object({ title: z.string(), points: z.array(z.string()), score: z.string().optional() }), right: z.object({ title: z.string(), points: z.array(z.string()), score: z.string().optional() }) }), z.object({ type: z.literal('equation'), title: z.string(), equations: z.array(z.object({ latex: z.string(), label: z.string().optional() })), explanation: z.string().optional() }), z.object({ type: z.literal('image'), title: z.string(), url: z.string().optional(), caption: z.string().optional() }), z.object({ type: z.literal('summary'), title: z.string(), items: z.array(z.string()) }), ]) toolRegistry.register({ name: 'generate_slides', description: 'Renders a structured presentation from JSON data into a full animated HTML file and saves it.', isInternal: true, buildTool: (ctx) => tool({ description: `Create a presentation from structured slide data. Each slide has a type and corresponding content. Available slide types: - "title": title, subtitle (opening slide with particles) - "bullets": title, items[] (bullet list with 4-6 items of 15+ words each) - "chart": title, chartType (bar|horizontal-bar|line|donut|radar), data[{label,value}], subtitle - "stats": title, stats[{value:"98%", label:"KPI name"}] (3-4 animated KPIs) - "table": title, headers[], rows[][] (data table) - "cards": title, cards[{title, description}] (3-6 info cards) - "timeline": title, events[{date, title, description}] (chronological events) - "quote": quote, author, context (citation with analysis) - "comparison": title, left{title, points[], score}, right{...} (A vs B) - "equation": title, equations[{latex, label}], explanation (math formulas) - "image": title, url, caption (image slide) - "summary": title, items[] (conclusion with checkmarks) RULES: - Count the plain-text words in the note (ignore markdown, URLs, metadata, headers) - <50 words → 2-3 slides MAXIMUM (title + 1 content + summary) - 50-150 words → 3-4 slides max - 150-400 words → 5-6 slides max - >400 words → 7-8 slides max — HARD LIMIT, NEVER exceed 8 - First slide MUST be type "title" - Last slide MUST be type "summary" - Include "chart" ONLY if real numeric data exists in the note — otherwise FORBIDDEN - Use VARIED types — never 2 identical types in a row - All text must come from the source note — never invent data`, inputSchema: z.object({ title: z.string().describe('Short presentation title (6 words max)'), 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(8).describe('Array of slide objects, 3-8 slides MAX'), }), execute: async ({ title, theme, slides }) => { try { // Hard cap: never more than 8 slides regardless of what the model outputs const cappedSlides = slides.slice(0, 8) console.log('[Slides Tool] Building presentation:', title, '| Slides:', cappedSlides.length, '| Theme:', theme) const html = buildPresentationHTML({ title, theme, slides: cappedSlides as any }) const canvas = await prisma.canvas.create({ data: { name: title || 'Présentation', data: JSON.stringify({ type: 'slides', title: title || 'Présentation', html, slideCount: cappedSlides.length, theme: theme || 'architectural-saas', spec: { title, theme, slides: cappedSlides }, }), userId: ctx.userId, }, }) console.log('[Slides Tool] Canvas created:', canvas.id, '| Slides:', cappedSlides.length, '| Size:', Math.round(html.length / 1024), 'KB') // Decrement slide_generate quota after successful canvas creation incrementUsageAsync(ctx.userId, 'slide_generate') if (ctx.actionId) { await prisma.agentAction.update({ where: { id: ctx.actionId }, data: { status: 'success', result: canvas.id, log: `Slides generated: ${cappedSlides.length} slides, ${Math.round(html.length / 1024)}KB`, }, }).catch(err => console.error('[Slides Tool] Failed to update action status:', err)) } return { success: true, canvasId: canvas.id, canvasName: canvas.name, slideCount: cappedSlides.length, message: `Presentation "${canvas.name}" created with ${cappedSlides.length} slides.`, } } catch (e: any) { console.error('[Slides Tool] FATAL:', e) return { success: false, error: `Failed: ${e.message}` } } }, }), })