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.
This commit is contained in:
151
memento-note/lib/ai/services/slide-intent.ts
Normal file
151
memento-note/lib/ai/services/slide-intent.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* 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<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')
|
||||
}
|
||||
Reference in New Issue
Block a user