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

@@ -31,7 +31,7 @@ import {
import { catalogForPrompt } from '@/lib/simulators'
import thermoFixture from '@/lib/interactive-page/fixtures/thermo-page.json'
const MAX_ATTEMPTS = 2
const MAX_ATTEMPTS = 3
// ── Shared helpers ───────────────────────────────────────────────────────────
@@ -124,19 +124,19 @@ function fixtureDemo(panelType: string): string | null {
// ── 1. Page plan ─────────────────────────────────────────────────────────────
const DEMO_KINDS = ['svg-scene', 'chart', 'heatmap-matrix', 'simulation', 'none'] as const
const DEMO_KINDS = ['steps', 'svg-scene', 'chart', 'heatmap-matrix', 'simulation', 'none'] as const
export type PagePlanDemoKind = (typeof DEMO_KINDS)[number]
const planSectionSchema = z.object({
title: z.string().min(1),
goal: z.string().min(1),
demoKind: z.enum(DEMO_KINDS),
demoGoal: z.string().optional(),
demoGoal: z.string().nullish(),
})
const pagePlanSchema = z.object({
heroTitle: z.string().min(1),
heroSubtitle: z.string().optional(),
heroSubtitle: z.string().nullish(),
overviewLead: z.string().min(1),
overviewCards: z
.array(
@@ -144,7 +144,7 @@ const pagePlanSchema = z.object({
badge: z.string().min(1),
title: z.string().min(1),
body: z.string().min(1),
intent: z.enum(INTENT_IDS).optional(),
intent: z.enum(INTENT_IDS).nullish(),
})
)
.min(INTERACTIVE_PAGE_CAPS.minOverviewCards)
@@ -170,7 +170,7 @@ SCHEMA:
"heroTitle": string, // the SUBJECT of the note, never a generic title
"heroSubtitle": string, // one sentence, autoportant
"overviewLead": string, // the central idea in ONE self-contained paragraph (may use $KaTeX$ inline)
"overviewCards": [ { "badge": string, "title": string, "body": string, "intent?": ${JSON.stringify(INTENT_IDS)} } ], // 24 key concepts, small-caps badges (ex. PROBLEM / APPROACH / RESULT)
"overviewCards": [ { "badge": string, "title": string, "body": string, "intent?": ${JSON.stringify(INTENT_IDS)} } ], // 34 key concepts, small-caps badges (ex. PROBLEM / APPROACH / RESULT)
"sections": [ { "title": string, "goal": string, "demoKind": ${JSON.stringify(DEMO_KINDS)}, "demoGoal?": string } ] // 25
}
@@ -181,6 +181,7 @@ COUVERTURE (règle n°1):
- If the content does not benefit from an interactive page, REFUSE: return { "error": "unsuitable_content", "reason": "…" } instead.
MATCHING CONTENU → DÉMO (choose demoKind per section, "none" when no visual helps):
- Dérivation, démonstration, résolution d'équation, calcul pas à pas (maths, physique) → "steps" — JAMAIS de svg-scene pour du contenu mathématique
- Catégories × propriétés (comparatif, matrice, échanges) → "heatmap-matrix"
- Loi / relation / courbe / évolution chiffrée → "chart"
- Processus / flux / cycle / architecture → "svg-scene"
@@ -298,8 +299,14 @@ Block types (discriminated by "type"):
- { "type": "table", "columns": string[], "rows": string[][], "caption?": string } // every row.length === columns.length
- { "type": "demo", "demo": InteractiveDemoV1, "caption?": string }
- { "type": "sim", "sim": SimRef, "caption?": string } // interactive simulation with sliders
- { "type": "steps", "title?": string, "steps": [{ "tex": string, "rule?": string, "speak?": string }] (212), "caption?": string } // step-by-step derivation: tex = KaTeX of the equation state, rule = transformation applied (short, e.g. "on sépare les variables")
IntentId: ${JSON.stringify(INTENT_IDS)}
BLOCK "steps" (Symbolab-style derivation) — MANDATORY for math/derivation content:
- Each step = the equation state AFTER applying the rule; steps must chain logically (each follows from the previous).
- rule = the transformation applied to reach THIS state (short verb phrase). speak = 1 sentence teacher narration.
- FORBIDDEN: using "demo" svg-scene (boxes with arrows) for equations, derivations, proofs, or calculus content — always "steps" instead.
SimRef — TWO forms:
(A) CATALOG simulator (PREFERRED when the section matches one): { "simId": "<id from catalog>", "title?": string, "preset?": { "<paramId>": number }, "disclaimer?": string }
→ You ONLY pick the simId and preset values (from the note's real numbers within the allowed ranges). The app runs the simulation.
@@ -347,13 +354,16 @@ function buildSectionUserPrompt(
SECTION_GOAL: ${section.goal}
${
section.demoKind === 'simulation'
? `SIMULATION_REQUIRED: include ONE "sim" block. Prefer a catalog simulator if the section matches one (bind the note's real values into "preset"); otherwise "generic-formula" with the section's key relation.
section.demoKind === 'steps'
? `STEPS_REQUIRED: include ONE "steps" block — the step-by-step derivation of this section's key result. Real equations from the note, logically chained, a short "rule" per step.
STEPS_GOAL: ${section.demoGoal || section.goal}`
: section.demoKind === 'simulation'
? `SIMULATION_REQUIRED: include ONE "sim" block. Prefer a catalog simulator if the section matches one (bind the note's real values into "preset"); otherwise "generic-formula" with the section's key relation.
SIM_GOAL: ${section.demoGoal || section.goal}`
: section.demoKind !== 'none'
? `DEMO_REQUIRED: include ONE "demo" block of kind "${section.demoKind}".
: section.demoKind !== 'none'
? `DEMO_REQUIRED: include ONE "demo" block of kind "${section.demoKind}".
DEMO_GOAL: ${section.demoGoal || section.goal}`
: `NO demo/sim block for this section — rich prose/formula/callout/stats/table only.`
: `NO demo/sim/steps block for this section — rich prose/formula/callout/stats/table only.`
}
EXCERPT_START
@@ -396,8 +406,10 @@ function validateSectionCandidate(
hero: { kicker: 'CHECK', title: 'Section check' },
sections: [
typeof candidate === 'object' && candidate !== null
? { id: sectionId, ...(candidate as Record<string, unknown>) }
? { ...(candidate as Record<string, unknown>), id: sectionId }
: candidate,
// schema requires ≥2 sections — inert filler for the wrap check
{ id: 's99', title: '—', blocks: [{ type: 'prose', md: '—' }] },
],
}
const normalized = normalizeInteractivePageCandidate(wrapped, lang)