/** * Slide generation intent (Sprint A product layer). * Passed from UI → API → agent.description → generateSlideDeck. */ export type SlidePurpose = | 'auto' | 'course' | 'board' | 'project' | 'strategy' | 'pitch' | 'summary' export type SlideAudience = 'auto' | 'student' | 'board' | 'team' | 'client' | 'general' export interface SlideIntent { purpose?: SlidePurpose audience?: SlideAudience /** Target body slides including title+summary, clamped 3–8 */ slideCount?: number /** Existing executive template id or auto */ template?: string } const PURPOSE_SET = new Set([ 'auto', 'course', 'board', 'project', 'strategy', 'pitch', 'summary', ]) const AUDIENCE_SET = new Set([ 'auto', 'student', 'board', 'team', 'client', 'general', ]) export function clampSlideCount(n: unknown): number | undefined { const v = typeof n === 'number' ? n : typeof n === 'string' ? parseInt(n, 10) : NaN if (!Number.isFinite(v)) return undefined return Math.min(8, Math.max(3, Math.round(v))) } export function normalizeSlideIntent(raw: Partial | null | undefined): SlideIntent { if (!raw || typeof raw !== 'object') return {} const purpose = raw.purpose && PURPOSE_SET.has(raw.purpose) ? raw.purpose : 'auto' const audience = raw.audience && AUDIENCE_SET.has(raw.audience) ? raw.audience : 'auto' const slideCount = clampSlideCount(raw.slideCount) const template = typeof raw.template === 'string' && raw.template.trim() ? raw.template.trim() : 'auto' return { purpose, audience, slideCount, template } } /** Encode intent into agent.description (backward compatible with template:xxx). */ export function encodeSlideIntentDescription(intent: SlideIntent): string { const n = normalizeSlideIntent(intent) // Prefer structured JSON for the structured pipeline return `slide-intent:${JSON.stringify(n)}` } /** Decode agent.description → intent */ export function decodeSlideIntentDescription(description?: string | null): SlideIntent { if (!description) return {} if (description.startsWith('slide-intent:')) { try { return normalizeSlideIntent(JSON.parse(description.slice('slide-intent:'.length))) } catch { return {} } } if (description.startsWith('template:')) { const template = description.replace('template:', '').trim() || 'auto' // Map legacy templates to purpose const purposeMap: Record = { 'board-update': 'board', 'project-status': 'project', 'strategy-review': 'strategy', 'quarterly-results': 'board', } return normalizeSlideIntent({ template, purpose: purposeMap[template] || 'auto', }) } return {} } /** Human guidance injected into outline/expand prompts */ export function intentPromptHints(intent: SlideIntent, lang: 'fr' | 'en'): string { const n = normalizeSlideIntent(intent) const lines: string[] = [] if (n.purpose && n.purpose !== 'auto') { const mapFr: Record = { auto: '', course: 'Type: COURS / leçon — arc pédagogique (définitions → formules → exemples → résumé). Priorité aux slides equation si maths.', board: 'Type: COMITÉ / board — KPIs, décisions, risques, next steps.', project: 'Type: SUIVI PROJET — avancement, blockers, livrables, roadmap.', strategy: 'Type: STRATÉGIE — options, trade-offs, recommandations.', pitch: 'Type: PITCH — problème, solution, preuve, call to action.', summary: 'Type: SYNTHÈSE — peu de slides, dense, takeaways actionnables.', } const mapEn: Record = { auto: '', course: 'Type: LESSON — pedagogical arc (defs → formulas → examples → summary). Prefer equation slides for math.', board: 'Type: BOARD — KPIs, decisions, risks, next steps.', project: 'Type: PROJECT STATUS — progress, blockers, deliverables, roadmap.', strategy: 'Type: STRATEGY — options, trade-offs, recommendations.', pitch: 'Type: PITCH — problem, solution, proof, CTA.', summary: 'Type: SUMMARY — few dense slides, actionable takeaways.', } lines.push(lang === 'fr' ? mapFr[n.purpose] : mapEn[n.purpose]) } if (n.audience && n.audience !== 'auto') { const mapFr: Record = { auto: '', student: 'Audience: étudiants — clarté, définitions, exemples, formules explicites.', board: 'Audience: comité de direction — concis, chiffres, décisions.', team: 'Audience: équipe — opérationnel, actions, responsabilités.', client: 'Audience: client — bénéfices, preuves, sans jargon interne.', general: 'Audience: grand public — langage simple.', } const mapEn: Record = { auto: '', student: 'Audience: students — clarity, definitions, examples, explicit formulas.', board: 'Audience: board — concise, numbers, decisions.', team: 'Audience: team — operational, actions, ownership.', client: 'Audience: client — benefits, proof, no internal jargon.', general: 'Audience: general — plain language.', } lines.push(lang === 'fr' ? mapFr[n.audience] : mapEn[n.audience]) } if (n.slideCount) { lines.push( lang === 'fr' ? `Nombre de slides imposé: EXACTEMENT ${n.slideCount} (title + corps + summary inclus).` : `Slide count required: EXACTLY ${n.slideCount} (including title + body + summary).`, ) } return lines.filter(Boolean).join('\n') }