feat: dashboard Second Brain, essai 7 jours et vérification e-mail
All checks were successful
CI / Lint, Unit Tests & Build (push) Successful in 7m14s
CI / Deploy production (on server) (push) Successful in 1m25s

Rendre le dashboard actionnable (inbox, peek, carte mentale), aligner la facturation sur l’essai 7 jours, et bloquer le login e-mail tant que l’adresse n’est pas confirmée.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Antigravity
2026-08-30 07:19:36 +00:00
parent 69c99e4f4f
commit 80ccc1f6de
95 changed files with 4158 additions and 618 deletions

View File

@@ -47,131 +47,7 @@ function chunkSentences(text: string): string[] {
.filter((s) => s.length > 20)
}
const INTENT_CYCLE = ['compute', 'flow', 'output', 'cache'] as const
/**
* Instant Play/Step demo from note vocabulary — no LLM.
* 34 nodes + spotlight steps; formulas in speak when available.
*/
export function buildDeterministicDemo(
content: string,
lang: string,
assets: ReturnType<typeof extractSourceAssets>
): InteractiveDemoV1 | null {
const fr = lang.startsWith('fr')
const plain = stripToPlain(content)
const sentences = [
...assets.keySentences,
...chunkSentences(plain),
].filter((s, i, a) => a.indexOf(s) === i)
// Prefer short phrase labels from key sentences — never raw formula fragments
const labels: string[] = []
for (const s of sentences.slice(0, 8)) {
const words = s
.replace(/\$[^$]*\$/g, ' ')
.replace(/[\\{}]/g, ' ')
.split(/\s+/)
.filter(Boolean)
.slice(0, 4)
.join(' ')
.trim()
if (words.length >= 6 && words.length <= 40 && !labels.includes(words)) {
labels.push(words)
}
if (labels.length >= 4) break
}
// Clean formulas usable inside $…$ (KaTeX eats spaces → no prose, bounded)
const cleanFormulas = assets.formulas.filter((f) => f.length <= 80)
if (labels.length < 3) {
const fallbacks = fr
? ['Entrée', 'Transformation', 'Résultat', 'Retour']
: ['Input', 'Transform', 'Output', 'Loop']
for (const fb of fallbacks) {
if (labels.length >= 4) break
if (!labels.includes(fb)) labels.push(fb)
}
}
while (labels.length < 3) labels.push(`Étape ${labels.length + 1}`)
const nodeCount = Math.min(4, Math.max(3, labels.length))
const nodes = labels.slice(0, nodeCount).map((label, i) => ({
id: `n${i + 1}`,
label:
cleanFormulas[i] && i < 2
? `${i + 1} · ${label.split('\n')[0]}\n$${cleanFormulas[i]}$`
: `${i + 1} · ${label}`,
intent: INTENT_CYCLE[i % INTENT_CYCLE.length],
}))
const edges = nodes.map((n, i) => {
const next = nodes[(i + 1) % nodes.length]
return {
id: `e${i + 1}`,
from: n.id,
to: next.id,
style: 'solid' as const,
intent: 'flow' as const,
}
})
const steps = nodes.map((n, i) => {
const formula = cleanFormulas[i]
const speakBase =
sentences[i]?.slice(0, 100) ||
(fr ? `Étape **${i + 1}** du mécanisme.` : `Step **${i + 1}** of the mechanism.`)
const speak = formula
? `${speakBase.split('.')[0]}. $${formula}$`
: speakBase
const revealed = nodes.slice(0, i + 1).map((x) => x.id)
if (i > 0) revealed.push(edges[i - 1].id)
const isLast = i === nodes.length - 1
return {
id: `a1.s${i + 1}`,
speak: speak.slice(0, 160),
pattern: isLast ? ('overview' as const) : ('spotlightTour' as const),
pointTo: [n.id],
reveal: [{ ids: isLast ? nodes.map((x) => x.id).concat(edges.map((e) => e.id)) : revealed, scope: 'act' as const }],
}
})
const raw = {
schemaVersion: 1 as const,
id: 'demo.page-auto',
lang,
disclaimer: fr
? 'Schéma pédagogique généré depuis la note — valeurs illustratives.'
: 'Pedagogical diagram from your note — illustrative values.',
scene: {
id: 'scene.main',
panels: [
{
id: 'panel.main',
type: 'svg-scene' as const,
payload: { nodes, edges },
},
],
},
acts: [
{
id: 'a1',
title: fr ? 'Parcours' : 'Walkthrough',
pattern: 'flowTrace' as const,
steps,
},
],
}
const validated = validateInteractiveDemo(raw)
if (!validated.ok) {
console.warn(
'[interactive-page] deterministic demo invalid',
validated.issues.slice(0, 5)
)
return null
}
return validated.demo
}
export function buildPageFromNote(
content: string,
@@ -296,7 +172,7 @@ export function buildPageFromNote(
function injectDemo(
page: Record<string, unknown>,
demo: unknown
block: Record<string, unknown>
): Record<string, unknown> {
const sections = Array.isArray(page.sections)
? ([...page.sections] as Record<string, unknown>[])
@@ -307,7 +183,7 @@ function injectDemo(
const blocks = Array.isArray(target.blocks)
? [...(target.blocks as Record<string, unknown>[])]
: []
blocks.push({ type: 'demo', demo, caption: 'Démo interactive' })
blocks.push(block)
target.blocks = blocks
sections[targetIdx] = target
return { ...page, sections }
@@ -332,8 +208,43 @@ export type GenerateInteractivePageResult =
}
/**
* Instant reliable page: deterministic skeleton + deterministic Play/Step demo.
* No LLM round-trip for the page itself (LLM demos were timing out past client abort).
* Deterministic step-by-step block from extracted formulas (math notes).
* Replaces the old generic box-diagram fallback — boxes are banned.
*/
function buildDeterministicSteps(
content: string,
lang: string,
assets: ReturnType<typeof extractSourceAssets>
): Record<string, unknown> | null {
const fr = lang.startsWith('fr')
const formulas = assets.formulas.filter((f) => f.length <= 120).slice(0, 6)
if (formulas.length < 3) return null
const plain = stripToPlain(content)
const sentences = [
...assets.keySentences,
...chunkSentences(plain),
].filter((s, i, a) => a.indexOf(s) === i)
return {
type: 'steps',
title: fr ? 'Dérivation pas à pas' : 'Step-by-step derivation',
steps: formulas.map((tex, i) => ({
tex,
...(i === 0
? { rule: fr ? 'Point de départ' : 'Starting point' }
: {}),
speak:
sentences[i]?.slice(0, 120) ||
(fr ? `Étape **${i + 1}** de la dérivation.` : `Step **${i + 1}** of the derivation.`),
})),
caption: fr
? 'Formules extraites de la note, déroulées pas à pas.'
: 'Formulas extracted from the note, walked through step by step.',
}
}
/**
* Instant reliable page: deterministic skeleton + deterministic steps (math)
* or Play/Step demo (other content). No LLM round-trip for the page itself.
*/
export async function generateInteractivePageFromContent(
input: GenerateInteractivePageInput
@@ -351,9 +262,9 @@ export async function generateInteractivePageFromContent(
}
let pageObj = buildPageFromNote(input.content, lang, assets)
const demo = buildDeterministicDemo(input.content, lang, assets)
if (demo) {
pageObj = injectDemo(pageObj, demo)
const stepsBlock = buildDeterministicSteps(input.content, lang, assets)
if (stepsBlock) {
pageObj = injectDemo(pageObj, stepsBlock)
}
const normalized = normalizeInteractivePageCandidate(pageObj, lang)