'use server' import { tool } from 'ai' import { z } from 'zod' import { toolRegistry } from './registry' import { prisma } from '@/lib/prisma' interface SlideSpec { title: string subtitle?: string content: string[] layout?: 'title' | 'content' | 'section' | 'two-column' | 'cards' | 'stats' | 'quote' | 'toc' | 'summary' | 'image' imageUrl?: string notes?: string } interface PresentationSpec { title: string slides: SlideSpec[] theme?: string style?: string author?: string } interface Palette { primary: string secondary: string accent: string light: string bg: string isDark: boolean } const PALETTES: Record = { modern_wellness: { primary: '#006d77', secondary: '#83c5be', accent: '#e29578', light: '#ffddd2', bg: '#edf6f9', isDark: false }, business_authority: { primary: '#2b2d42', secondary: '#8d99ae', accent: '#ef233c', light: '#edf2f4', bg: '#edf2f4', isDark: false }, nature_outdoors: { primary: '#606c38', secondary: '#283618', accent: '#dda15e', light: '#fefae0', bg: '#fefae0', isDark: false }, vintage_academic: { primary: '#780000', secondary: '#669bbc', accent: '#c1121f', light: '#fdf0d5', bg: '#fdf0d5', isDark: false }, soft_creative: { primary: '#7c6c8a', secondary: '#a89bbd', accent: '#d4a5c9', light: '#e8dff0', bg: '#f3eef8', isDark: false }, bohemian: { primary: '#8a7e5e', secondary: '#a89e72', accent: '#c4a06a', light: '#e9dcc0', bg: '#f5eed8', isDark: false }, vibrant_tech: { primary: '#023047', secondary: '#219ebc', accent: '#ffb703', light: '#8ecae6', bg: '#f8fbff', isDark: false }, craft_artisan: { primary: '#5e3e28', secondary: '#8a6548', accent: '#a68a64', light: '#d4c4a8', bg: '#ede0d4', isDark: false }, tech_night: { primary: '#e0e0e0', secondary: '#ffc300', accent: '#ffd60a', light: '#003566', bg: '#001d3d', isDark: true }, education_charts: { primary: '#264653', secondary: '#2a9d8f', accent: '#e76f51', light: '#e9c46a', bg: '#f4f1eb', isDark: false }, forest_eco: { primary: '#344e41', secondary: '#588157', accent: '#a3b18a', light: '#dad7cd', bg: '#eae8e3', isDark: false }, elegant_fashion: { primary: '#4a5759', secondary: '#8f9fa2', accent: '#b0c4b1', light: '#c9ada7', bg: '#f2e9e4', isDark: false }, art_food: { primary: '#335c67', secondary: '#5e8a6f', accent: '#e09f3e', light: '#f3d97a', bg: '#fff8e1', isDark: false }, luxury_mystery: { primary: '#22223b', secondary: '#4a4e69', accent: '#9a8c98', light: '#c9ada7', bg: '#f2e9e4', isDark: false }, pure_tech_blue: { primary: '#03045e', secondary: '#0077b6', accent: '#00b4d8', light: '#90e0ef', bg: '#caf0f8', isDark: false }, coastal_coral: { primary: '#0081a7', secondary: '#00afb9', accent: '#f07167', light: '#fed9b7', bg: '#fdfcdc', isDark: false }, vibrant_orange_mint: { primary: '#1a1a2e', secondary: '#2ec4b6', accent: '#ff9f1c', light: '#cbf3f0', bg: '#ffffff', isDark: false }, platinum_white_gold: { primary: '#0a0a0a', secondary: '#0070F3', accent: '#D4AF37', light: '#f5f5f5', bg: '#ffffff', isDark: false }, architectural_mono: { primary: '#1C1C1C', secondary: '#D4A373', accent: '#ACB995', light: '#F9F8F6', bg: '#F9F8F6', isDark: false }, minimal_silk: { primary: '#212529', secondary: '#6c757d', accent: '#dee2e6', light: '#f8f9fa', bg: '#ffffff', isDark: false }, } const PALETTE_ALIASES: Record = { modern: 'vibrant_tech', corporate: 'business_authority', minimal: 'elegant_fashion', dark: 'tech_night', midnight: 'luxury_mystery', forest: 'forest_eco', coral: 'coastal_coral', ocean: 'pure_tech_blue', charcoal: 'platinum_white_gold', teal: 'education_charts', berry: 'art_food', cherry: 'vintage_academic', clair: 'pure_tech_blue', light: 'modern_wellness', warm: 'bohemian', premium: 'platinum_white_gold', clean: 'vibrant_tech', architectural: 'architectural_mono', silk: 'minimal_silk', } const THEME_NAMES: Record = { modern_wellness: 'Modern & Wellness', business_authority: 'Business & Authority', nature_outdoors: 'Nature & Outdoors', vintage_academic: 'Vintage & Academic', soft_creative: 'Soft & Creative', bohemian: 'Bohemian', vibrant_tech: 'Vibrant & Tech', craft_artisan: 'Craft & Artisan', tech_night: 'Tech & Night', education_charts: 'Education & Charts', forest_eco: 'Forest & Eco', elegant_fashion: 'Elegant & Fashion', art_food: 'Art & Food', luxury_mystery: 'Luxury & Mystery', pure_tech_blue: 'Pure Tech Blue', coastal_coral: 'Coastal Coral', vibrant_orange_mint: 'Vibrant Orange Mint', platinum_white_gold: 'Platinum White Gold', architectural_mono: 'Architectural Mono', minimal_silk: 'Minimal Silk', } function resolvePalette(spec: PresentationSpec): { palette: Palette; key: string } { const name = (spec.theme || '').toLowerCase().replace(/[\s-]/g, '_') const key = PALETTE_ALIASES[name] || (PALETTES[name] ? name : 'vibrant_tech') return { palette: PALETTES[key]!, key } } function resolveRadius(style?: string): string { switch ((style || '').toLowerCase()) { case 'sharp': return '2px' case 'rounded': return '16px' case 'pill': return '24px' default: return '10px' } } function esc(str: string): string { return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"') } function safeHtml(str: string): string { return str .replace(/)<[^<]*)*<\/script>/gi, '') .replace(/)<[^<]*)*<\/iframe>/gi, '') .replace(/\son\w+\s*=\s*["'][^"']*["']/gi, '') .replace(/\son\w+\s*=\s*[^\s>]*/gi, '') .replace(/javascript\s*:/gi, '') } function buildThemeCSS(p: Palette, radius: string, key: string): string { const text = p.isDark ? '#f0f0f0' : '#1a1a1a' const muted = p.isDark ? '#999' : '#555' const heading = p.isDark ? '#ffffff' : p.primary const bgText = p.isDark ? '#e0e0e0' : '#ffffff' const shadowAlpha = p.isDark ? '0.35' : '0.08' const shadowAlphaSm = p.isDark ? '0.25' : '0.05' return `:root { --p-primary: ${p.primary}; --p-secondary: ${p.secondary}; --p-accent: ${p.accent}; --p-light: ${p.light}; --p-bg: ${p.bg}; --p-text: ${text}; --p-muted: ${muted}; --p-heading: ${heading}; --p-on-primary: ${bgText}; --p-radius: ${radius}; --p-shadow: 0 8px 32px rgba(0,0,0,${shadowAlpha}); --p-shadow-sm: 0 2px 12px rgba(0,0,0,${shadowAlphaSm}); --p-gradient: linear-gradient(135deg, ${p.primary} 0%, ${p.secondary} 100%); --p-border: ${p.isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.06)'}; --p-border-accent: ${p.isDark ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.1)'}; --r-background-color: ${p.bg}; --r-main-font: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; --r-heading-font: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; --r-main-font-size: 26px; --r-heading-font-weight: 700; --r-heading-color: ${heading}; --r-heading-line-height: 1.15; --r-heading-letter-spacing: -0.03em; --r-heading-text-transform: none; --r-heading-text-shadow: none; --r-heading1-size: 3.2em; --r-heading2-size: 2em; --r-heading3-size: 1.4em; --r-heading4-size: 1em; --r-main-color: ${text}; --r-block-margin: 16px; --r-link-color: ${p.accent}; --r-link-color-hover: ${p.secondary}; --r-selection-background-color: ${p.accent}; --r-selection-color: ${bgText}; } ${key === 'architectural_mono' ? ` .reveal-viewport { background-color: #F9F8F6 !important; background-image: linear-gradient(rgba(28, 28, 28, 0.08) 1px, transparent 1px), linear-gradient(90deg, rgba(28, 28, 28, 0.08) 1px, transparent 1px), linear-gradient(rgba(28, 28, 28, 0.04) 1px, transparent 1px), linear-gradient(90deg, rgba(28, 28, 28, 0.04) 1px, transparent 1px) !important; background-size: 100px 100px, 100px 100px, 20px 20px, 20px 20px !important; } .reveal { font-family: 'JetBrains Mono', monospace !important; } .reveal h1, .reveal h2, .reveal h3 { font-family: 'JetBrains Mono', monospace !important; text-transform: uppercase !important; letter-spacing: -0.02em !important; font-weight: 700 !important; } .reveal h1 { border-left: 12px solid #D4A373; padding-left: 40px; } .reveal section { text-align: left; padding: 60px; } .reveal p, .reveal li { font-weight: 300; font-family: 'JetBrains Mono', monospace !important; } ` : ''}` } function buildLayoutCSS(): string { return ` .reveal-viewport { background: var(--p-bg); background-image: linear-gradient(var(--p-border) 1px, transparent 1px), linear-gradient(90deg, var(--p-border) 1px, transparent 1px); background-size: 40px 40px; } .reveal { font-family: var(--r-main-font); font-weight: 400; letter-spacing: -0.01em; color: var(--p-text); } .reveal h1, .reveal h2, .reveal h3, .reveal h4 { font-family: var(--r-heading-font); font-weight: 700; text-transform: none; letter-spacing: -0.03em; line-height: 1.15; } .reveal h1 { font-size: var(--r-heading1-size); } .reveal h2 { font-size: var(--r-heading2-size); margin-bottom: 0.1em; } .reveal h3 { font-size: var(--r-heading3-size); } .reveal section { padding: 40px 60px; text-align: left; } .reveal a { color: var(--p-accent); text-decoration: none; border-bottom: 1px solid transparent; transition: border-color 0.2s; } .reveal a:hover { border-bottom-color: var(--p-accent); } .reveal .accent-bar { width: 48px; height: 3px; background: var(--p-accent); border-radius: 2px; margin-bottom: 1.4rem; flex-shrink: 0; } .reveal .accent-bar--center { margin-left: auto; margin-right: auto; } .reveal .accent-bar--wide { width: 80px; height: 4px; } /* ======= DECORATIVE FRAME ======= */ .reveal .frame-top, .reveal .frame-bottom, .reveal .frame-left, .reveal .frame-right { position: fixed; z-index: 10; background: var(--p-accent); pointer-events: none; } .reveal .frame-top { top: 0; left: 0; right: 0; height: 4px; } .reveal .frame-bottom { bottom: 0; left: 0; right: 0; height: 4px; } .reveal .frame-left { top: 0; bottom: 0; left: 0; width: 4px; } .reveal .frame-right { top: 0; bottom: 0; right: 0; width: 4px; } /* ======= TITLE ======= */ .reveal .s-title { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; text-align: center; padding: 0 80px; } .reveal .s-title::before { content: ''; position: absolute; inset: 0; z-index: -1; background: var(--p-gradient); opacity: 0.06; } .reveal .s-title h1 { color: var(--p-heading); margin: 0; line-height: 1.1; } .reveal .s-title .subtitle { color: var(--p-muted); font-size: 16pt; margin-top: 1.2rem; font-weight: 300; letter-spacing: 0.02em; } /* ======= SECTION DIVIDER ======= */ .reveal .s-section { display: flex; flex-direction: column; justify-content: center; align-items: center; height: 100%; text-align: center; } .reveal .s-section::before { content: ''; position: absolute; inset: 0; z-index: -1; background: var(--p-light); } .reveal .s-section .section-num { color: var(--p-accent); font-size: 120pt; font-weight: 800; opacity: 0.12; line-height: 1; margin-bottom: -0.3em; } .reveal .s-section h2 { color: var(--p-heading); } .reveal .s-section .subtitle { color: var(--p-muted); font-size: 14pt; margin-top: 0.5rem; } /* ======= TOC ======= */ .reveal .s-toc h2 { color: var(--p-heading); } .reveal .s-toc .toc-list { display: flex; flex-direction: column; gap: 2px; } .reveal .s-toc .toc-item { display: flex; align-items: center; gap: 16px; padding: 10px 16px; border-radius: var(--p-radius); transition: background 0.2s; } .reveal .s-toc .toc-item:nth-child(odd) { background: var(--p-border); } .reveal .s-toc .toc-num { color: var(--p-accent); font-size: 24pt; font-weight: 800; min-width: 50px; text-align: right; line-height: 1; } .reveal .s-toc .toc-label { color: var(--p-text); font-size: 14pt; padding-left: 12px; border-left: 3px solid var(--p-secondary); } /* ======= CONTENT ======= */ .reveal .s-content h2 { color: var(--p-heading); } .reveal .s-content ul { list-style: none; padding: 0; margin: 0; } .reveal .s-content li { color: var(--p-text); font-size: 14pt; padding: 8px 0; display: flex; align-items: flex-start; gap: 14px; line-height: 1.5; } .reveal .s-content li::before { content: ''; display: block; width: 8px; height: 8px; min-width: 8px; background: var(--p-accent); border-radius: 50%; margin-top: 0.5em; } /* ======= TWO COLUMN ======= */ .reveal .s-twocol h2 { color: var(--p-heading); } .reveal .s-twocol .cols { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; } .reveal .s-twocol .col { background: var(--p-border); border-radius: var(--p-radius); padding: 20px 24px; } .reveal .s-twocol .col--accent { border-left: 3px solid var(--p-accent); } .reveal .s-twocol .col p { color: var(--p-text); font-size: 13pt; margin: 8px 0; line-height: 1.55; } /* ======= CARDS ======= */ .reveal .s-cards h2 { color: var(--p-heading); } .reveal .s-cards .card-grid { display: grid; gap: 14px; } .reveal .s-cards .card-grid.g2 { grid-template-columns: repeat(2, 1fr); } .reveal .s-cards .card-grid.g3 { grid-template-columns: repeat(3, 1fr); } .reveal .s-cards .card { border-radius: var(--p-radius); padding: 22px 24px; display: flex; flex-direction: column; gap: 6px; border: 1px solid var(--p-border-accent); background: var(--p-border); transition: transform 0.2s, box-shadow 0.2s; } .reveal .s-cards .card:nth-child(odd) { background: var(--p-primary); border-color: transparent; box-shadow: 0 10px 20px rgba(0,0,0,0.15); } .reveal .s-cards .card:nth-child(odd) .card-num { color: rgba(255,255,255,0.4); } .reveal .s-cards .card:nth-child(odd) .card-text { color: #ffffff; font-weight: 300; } .reveal .s-cards .card:nth-child(even) { background: #ffffff; border: 1px solid var(--p-border-accent); } .reveal .s-cards .card:nth-child(even) .card-num { color: var(--p-accent); opacity: 0.6; } .reveal .s-cards .card:nth-child(even) .card-text { color: var(--p-text); } .reveal .s-cards .card-num { font-size: 18pt; font-weight: 800; line-height: 1; } .reveal .s-cards .card-text { font-size: 12pt; line-height: 1.5; } /* ======= STATS ======= */ .reveal .s-stats h2 { color: var(--p-heading); } .reveal .s-stats .stat-grid { display: grid; gap: 28px; margin-top: 1.2rem; } .reveal .s-stats .stat { text-align: center; padding-top: 16px; border-top: 4px solid var(--p-accent); } .reveal .s-stats .stat-value { color: var(--p-heading); font-size: 44pt; font-weight: 800; line-height: 1; } .reveal .s-stats .stat-label { color: var(--p-muted); font-size: 12pt; margin-top: 8px; text-transform: uppercase; letter-spacing: 0.06em; } /* ======= QUOTE ======= */ .reveal .s-quote { display: flex; flex-direction: column; justify-content: center; height: 100%; text-align: left; padding: 0 80px; } .reveal .s-quote::before { content: ''; position: absolute; inset: 0; z-index: -1; background: var(--p-gradient); opacity: 0.08; } .reveal .s-quote .q-mark { color: var(--p-accent); font-size: 100pt; font-weight: 700; line-height: 0.4; font-family: 'Playfair Display', Georgia, serif; opacity: 0.5; } .reveal .s-quote blockquote { color: var(--p-heading); font-size: 22pt; font-style: italic; line-height: 1.5; margin: 16px 0 24px; font-family: 'Playfair Display', Georgia, serif; border: none; box-shadow: none; padding: 0; background: none; width: 100%; text-align: left; } .reveal .s-quote cite { color: var(--p-accent); font-size: 12pt; font-style: normal; font-family: var(--r-main-font); } /* ======= SUMMARY ======= */ .reveal .s-summary h2 { color: var(--p-heading); } .reveal .s-summary::before { content: ''; position: absolute; inset: 0; z-index: -1; background: var(--p-light); } .reveal .s-summary .summary-list { display: flex; flex-direction: column; gap: 4px; } .reveal .s-summary .summary-item { display: flex; align-items: center; gap: 14px; padding: 10px 16px; border-radius: var(--p-radius); background: var(--p-border); } .reveal .s-summary .summary-dot { width: 10px; height: 10px; min-width: 10px; background: var(--p-accent); border-radius: 50%; } .reveal .s-summary .summary-text { color: var(--p-text); font-size: 14pt; } /* ======= IMAGE ======= */ .reveal .s-image h2 { color: var(--p-heading); } .reveal .s-image img { max-width: 85%; max-height: 55vh; border-radius: var(--p-radius); box-shadow: var(--p-shadow); display: block; margin: 1rem auto 0; } .reveal .s-image .caption { color: var(--p-muted); font-size: 11pt; text-align: center; margin-top: 12px; } /* ======= UI ======= */ .reveal .controls button { color: var(--p-accent); } .reveal .progress span { background: var(--p-accent); } .reveal .slide-number { color: var(--p-muted); font-size: 10pt; opacity: 0.7; } /* ======= PRINT ======= */ @page { size: 1219px 686px; margin: 0; } @media print { .reveal section { padding: 20px; } .reveal .frame-top, .reveal .frame-bottom, .reveal .frame-left, .reveal .frame-right { display: none; } }` } function renderSlide(slide: SlideSpec, index: number): string { const layout = slide.layout || (index === 0 ? 'title' : 'content') let html = '' switch (layout) { case 'title': html = `

${safeHtml(slide.title)}

${slide.subtitle ? `

${safeHtml(slide.subtitle)}

` : ''}
` break case 'toc': html = `

${safeHtml(slide.title || 'Sommaire')}

${slide.content.map((item, i) => `
${String(i + 1).padStart(2, '0')} ${safeHtml(item)}
`).join('\n ')}
` break case 'section': html = `
${safeHtml(slide.content[0] || String(index).padStart(2, '0'))}

${safeHtml(slide.title)}

${slide.subtitle ? `

${safeHtml(slide.subtitle)}

` : ''}
` break case 'content': html = `

${safeHtml(slide.title)}

    ${slide.content.map(item => `
  • ${safeHtml(item)}
  • `).join('\n ')}
` break case 'two-column': { const mid = Math.ceil(slide.content.length / 2) const left = slide.content.slice(0, mid) const right = slide.content.slice(mid) html = `

${safeHtml(slide.title)}

${left.map(item => `

${safeHtml(item)}

`).join('\n ')}
${right.map(item => `

${safeHtml(item)}

`).join('\n ')}
` break } case 'cards': { const items = slide.content.slice(0, 6) const gClass = items.length <= 3 ? `g${items.length}` : 'g2' html = `

${safeHtml(slide.title)}

${items.map((item, i) => `
${String(i + 1).padStart(2, '0')}

${safeHtml(item)}

`).join('\n ')}
` break } case 'stats': html = `

${safeHtml(slide.title)}

${slide.content.slice(0, 4).map(item => { const parts = item.split(/[-\u2013\u2014:]/) const stat = parts[0]?.trim() || item const label = parts.slice(1).join(':').trim() return `
${safeHtml(stat)}
${label ? `
${safeHtml(label)}
` : ''}
` }).join('\n ')}
` break case 'quote': html = `
\u201C
${safeHtml(slide.title)}
${slide.subtitle ? `\u2014 ${safeHtml(slide.subtitle)}` : ''}
` break case 'summary': html = `

${safeHtml(slide.title || 'En r\u00e9sum\u00e9')}

${slide.content.slice(0, 5).map(item => `
${safeHtml(item)}
`).join('\n ')}
` break case 'image': html = `

${safeHtml(slide.title)}

${slide.imageUrl ? `${esc(slide.title)}` : ''} ${slide.content[0] ? `

${safeHtml(slide.content[0])}

` : ''}
` break default: html = `

${safeHtml(slide.title)}

    ${slide.content.map(item => `
  • ${safeHtml(item)}
  • `).join('\n ')}
` } if (slide.notes) { html = html.replace('', `\n`) } return html } function buildRevealHtml(spec: PresentationSpec): string { const { palette, key } = resolvePalette(spec) const baseTheme = palette.isDark ? 'moon' : 'white' const radius = resolveRadius(spec.style) const slidesHtml = spec.slides.map((s, i) => renderSlide(s, i)).join('\n') return ` ${esc(spec.title)}
${slidesHtml}
` } function parseSlidesFromText(text: string): PresentationSpec { const lines = text.split('\n').filter(l => l.trim().length > 0) const title = lines[0]?.replace(/^#+\s*/, '').trim() || 'Presentation' const slides: SlideSpec[] = [] let current: SlideSpec | null = null for (const line of lines) { const t = line.trim() if (t.match(/^#{1,2}\s+/) || t.match(/^slide\s+\d+/i)) { if (current) slides.push(current) current = { title: t.replace(/^#{1,2}\s+/, '').replace(/^slide\s+\d+\s*[:-]?\s*/i, ''), content: [] } } else if (current && (t.match(/^[-*]\s+/) || t.match(/^\d+\.\s+/))) { current.content.push(t.replace(/^[-*]\s+/, '').replace(/^\d+\.\s+/, '')) } } if (current) slides.push(current) if (slides.length === 0) slides.push({ title, content: lines.slice(1, 8).map(l => l.replace(/^[-*]\s+/, '').replace(/^\d+\.\s+/, '')) }) return { title, slides } } toolRegistry.register({ name: 'generate_slides', description: 'Generate a beautiful HTML presentation with Reveal.js and save it for viewing.', isInternal: true, buildTool: (ctx) => tool({ description: `Generate a beautiful HTML presentation using Reveal.js and save it. Provide a JSON specification: { "title": "Presentation Title", "theme": "vibrant_tech", "slides": [ { "title": "Title", "subtitle": "Subtitle", "content": [], "layout": "title" }, { "title": "Sommaire", "content": ["Section 1", "Section 2"], "layout": "toc" }, { "title": "Key Points", "content": ["Point 1", "Point 2"], "layout": "content" }, { "title": "Features", "content": ["Feature A: desc", "Feature B: desc"], "layout": "cards" }, { "title": "Metrics", "content": ["99% - Uptime", "50K - Users"], "layout": "stats" }, { "title": "Introduction", "content": ["01"], "subtitle": "Topic", "layout": "section" }, { "title": "A great quote.", "subtitle": "- Author", "layout": "quote" }, { "title": "Summary", "content": ["Point 1", "Point 2"], "layout": "summary" } ] } THEMES: modern_wellness, business_authority, nature_outdoors, vintage_academic, soft_creative, bohemian, vibrant_tech, craft_artisan, tech_night, education_charts, forest_eco, elegant_fashion, art_food, luxury_mystery, pure_tech_blue, coastal_coral, vibrant_orange_mint, platinum_white_gold LAYOUTS: title, toc, content, section, two-column, cards, stats, quote, summary, image RULES: - First slide MUST be "title" - Second slide should be "toc" - Use "section" for dividers (content[0]=section number like "01") - Use "cards" for feature lists (3-6 items) - Use "stats" for numbers (format: "NUMBER - LABEL") - Use "quote" for quotes (title=quote, subtitle=attribution) - Use "summary" for closing summary - Use "two-column" for comparisons - 5-12 slides, vary layouts, no repeats consecutively`, inputSchema: z.object({ title: z.string().describe('Title for the presentation'), slides: z.string().describe('JSON presentation specification'), }), execute: async ({ title, slides }) => { try { console.log('[Slides Tool] INPUT title:', title) console.log('[Slides Tool] INPUT slides (first 500 chars):', slides?.substring(0, 500)) let spec: PresentationSpec try { const parsed = JSON.parse(slides) console.log('[Slides Tool] JSON parsed OK. slides count:', parsed.slides?.length, 'theme:', parsed.theme, 'title:', parsed.title) if (parsed.slides && Array.isArray(parsed.slides) && parsed.slides.length > 0) { spec = { title: parsed.title || title || 'Presentation', theme: parsed.theme || 'vibrant_tech', style: parsed.style, slides: parsed.slides.map((s: any) => ({ title: String(s.title || '').substring(0, 200), subtitle: s.subtitle ? String(s.subtitle).substring(0, 300) : undefined, content: Array.isArray(s.content) ? s.content.map((c: any) => String(c).substring(0, 500)).slice(0, 12) : [], layout: ['title', 'content', 'section', 'two-column', 'cards', 'stats', 'quote', 'toc', 'summary', 'image'].includes(s.layout) ? s.layout : undefined, imageUrl: s.imageUrl ? String(s.imageUrl).substring(0, 500) : undefined, notes: s.notes ? String(s.notes).substring(0, 1000) : undefined, })), } } else { console.log('[Slides Tool] No slides array in JSON, falling back to text parse') spec = parseSlidesFromText(slides) } } catch (parseErr) { console.log('[Slides Tool] JSON parse failed, falling back to text parse:', parseErr) spec = parseSlidesFromText(slides) } console.log('[Slides Tool] Spec:', JSON.stringify({ title: spec.title, theme: spec.theme, style: spec.style, slideCount: spec.slides.length, layouts: spec.slides.map(s => s.layout) })) if (spec.slides.length === 0) { console.log('[Slides Tool] ERROR: No slides provided') return { success: false, error: 'No slides provided' } } const html = buildRevealHtml(spec) console.log('[Slides Tool] HTML generated. Length:', html.length, '| Start:', html.substring(0, 120)) const canvas = await prisma.canvas.create({ data: { name: title || spec.title || 'Presentation', data: JSON.stringify({ type: 'slides', title: spec.title, theme: spec.theme, slideCount: spec.slides.length, html, }), userId: ctx.userId, }, }) console.log('[Slides Tool] Canvas created:', canvas.id, canvas.name) return { success: true, canvasId: canvas.id, canvasName: canvas.name, slideCount: spec.slides.length, theme: spec.theme, message: `Presentation created with ${spec.slides.length} slides. Open in browser to view.`, } } catch (e: any) { console.error('[Slides Tool] FATAL:', e) return { success: false, error: `Failed: ${e.message}` } } }, }), })