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:
133
memento-note/tests/unit/slide-source-assets.test.ts
Normal file
133
memento-note/tests/unit/slide-source-assets.test.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { extractFormulas, extractSourceAssets } from '@/lib/ai/services/slide-source-assets'
|
||||
import {
|
||||
assertDeckHasSubstance,
|
||||
injectEquationSlidesFromFormulas,
|
||||
normalizeSlideDeck,
|
||||
} from '@/lib/ai/services/slide-content-quality'
|
||||
import { buildPresentationHTML } from '@/lib/ai/tools/slides-html-builder'
|
||||
import { encodeSlideIntentDescription, decodeSlideIntentDescription } from '@/lib/ai/services/slide-intent'
|
||||
|
||||
describe('extractFormulas for STEM notes', () => {
|
||||
it('extracts display math $$...$$', () => {
|
||||
const text = 'On étudie $$\\frac{dy}{dx} + P(x)y = Q(x)$$ et la solution.'
|
||||
const f = extractFormulas(text)
|
||||
expect(f.some((x) => x.includes('dy') || x.includes('frac'))).toBe(true)
|
||||
})
|
||||
|
||||
it('extracts equation-like lines', () => {
|
||||
const text = 'La dérivée:\ny\' + 2y = e^x\nPuis on intègre.'
|
||||
const f = extractFormulas(text)
|
||||
expect(f.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('flags math domain for differential equations notes', () => {
|
||||
const assets = extractSourceAssets(
|
||||
'Cours sur les équations différentielles. $$y\' + P(x)y = Q(x)$$. Méthode du facteur intégrant.',
|
||||
)
|
||||
expect(assets.hasMath).toBe(true)
|
||||
expect(assets.formulas.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('equation slides render with katex hooks', () => {
|
||||
it('emits data-latex and katex CDN for equation slides', () => {
|
||||
const deck = normalizeSlideDeck({
|
||||
title: 'EDO',
|
||||
theme: 'clinical-precision',
|
||||
slides: [
|
||||
{ type: 'title', title: 'Équations différentielles', subtitle: 'Cours' },
|
||||
{
|
||||
type: 'equation',
|
||||
title: 'Forme générale',
|
||||
equations: [{ latex: "y' + P(x)y = Q(x)", label: 'Linéaire 1er ordre' }],
|
||||
explanation: 'Forme standard',
|
||||
},
|
||||
{
|
||||
type: 'bullets',
|
||||
title: 'Méthode',
|
||||
items: ['Identifier P et Q', 'Calculer le facteur intégrant', 'Intégrer les deux membres'],
|
||||
},
|
||||
{
|
||||
type: 'summary',
|
||||
title: 'À retenir',
|
||||
items: ['Forme standard', 'Facteur intégrant', 'Condition initiale'],
|
||||
},
|
||||
],
|
||||
})
|
||||
expect(assertDeckHasSubstance(deck).ok).toBe(true)
|
||||
const html = buildPresentationHTML({
|
||||
title: deck.title,
|
||||
theme: deck.theme,
|
||||
slides: deck.slides as any,
|
||||
})
|
||||
expect(html).toContain('katex')
|
||||
expect(html).toContain('data-latex')
|
||||
expect(html).toContain("y' + P(x)y = Q(x)")
|
||||
// cards grid must not force 2 empty columns for 1 card
|
||||
const oneCard = buildPresentationHTML({
|
||||
title: 't',
|
||||
theme: 'architectural-saas',
|
||||
slides: [
|
||||
{ type: 'title', title: 'T' },
|
||||
{
|
||||
type: 'cards',
|
||||
title: 'One',
|
||||
cards: [{ title: 'Seul', description: 'Plein largeur' }],
|
||||
},
|
||||
{ type: 'summary', title: 'S', items: ['a', 'b'] },
|
||||
] as any,
|
||||
})
|
||||
expect(oneCard).toContain('repeat(1,minmax(0,1fr))')
|
||||
})
|
||||
})
|
||||
|
||||
describe('STEM gate + model + intent', () => {
|
||||
it('fails substance when formulas required but no equation latex', () => {
|
||||
const deck = normalizeSlideDeck({
|
||||
title: 'EDO',
|
||||
slides: [
|
||||
{ type: 'title', title: 'EDO' },
|
||||
{ type: 'bullets', title: 'Intro', items: ['a', 'b', 'c'] },
|
||||
{ type: 'summary', title: 'Fin', items: ['x', 'y'] },
|
||||
],
|
||||
})
|
||||
const gate = assertDeckHasSubstance(deck, {
|
||||
requireFormulas: ["y' + P y = Q"],
|
||||
})
|
||||
expect(gate.ok).toBe(false)
|
||||
})
|
||||
|
||||
it('injects equation slides from formulas', () => {
|
||||
const thin = normalizeSlideDeck({
|
||||
title: 'EDO',
|
||||
theme: 'clinical-precision',
|
||||
slides: [
|
||||
{ type: 'title', title: 'EDO', subtitle: 'Cours' },
|
||||
{ type: 'bullets', title: 'Méthode', items: ['A', 'B', 'C'] },
|
||||
{ type: 'summary', title: 'Fin', items: ['1', '2'] },
|
||||
],
|
||||
})
|
||||
const filled = injectEquationSlidesFromFormulas(thin, ["y' + P(x)y = Q(x)", '\\mu(x) = e^{\\int P}'], {
|
||||
lang: 'fr',
|
||||
})
|
||||
expect(filled.slides.some((s) => s.type === 'equation')).toBe(true)
|
||||
const gate = assertDeckHasSubstance(filled, {
|
||||
requireFormulas: ["y' + P(x)y = Q(x)"],
|
||||
})
|
||||
expect(gate.ok).toBe(true)
|
||||
})
|
||||
|
||||
it('encodes and decodes slide intent', () => {
|
||||
const enc = encodeSlideIntentDescription({
|
||||
purpose: 'course',
|
||||
audience: 'student',
|
||||
slideCount: 6,
|
||||
template: 'auto',
|
||||
})
|
||||
const dec = decodeSlideIntentDescription(enc)
|
||||
expect(dec.purpose).toBe('course')
|
||||
expect(dec.audience).toBe('student')
|
||||
expect(dec.slideCount).toBe(6)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user