Files
Momento/memento-note/lib/ai/services/slide-intent.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

152 lines
5.5 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.
/**
* 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 38 */
slideCount?: number
/** Existing executive template id or auto */
template?: string
}
const PURPOSE_SET = new Set<string>([
'auto',
'course',
'board',
'project',
'strategy',
'pitch',
'summary',
])
const AUDIENCE_SET = new Set<string>([
'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<SlideIntent> | 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<string, SlidePurpose> = {
'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<SlidePurpose, string> = {
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<SlidePurpose, string> = {
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<SlideAudience, string> = {
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<SlideAudience, string> = {
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')
}