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.
152 lines
5.5 KiB
TypeScript
152 lines
5.5 KiB
TypeScript
/**
|
||
* 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')
|
||
}
|