feat: dashboard Second Brain, essai 7 jours et vérification e-mail
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:
@@ -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)} } ], // 2–4 key concepts, small-caps badges (ex. PROBLEM / APPROACH / RESULT)
|
||||
"overviewCards": [ { "badge": string, "title": string, "body": string, "intent?": ${JSON.stringify(INTENT_IDS)} } ], // 3–4 key concepts, small-caps badges (ex. PROBLEM / APPROACH / RESULT)
|
||||
"sections": [ { "title": string, "goal": string, "demoKind": ${JSON.stringify(DEMO_KINDS)}, "demoGoal?": string } ] // 2–5
|
||||
}
|
||||
|
||||
@@ -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 }] (2–12), "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)
|
||||
|
||||
Reference in New Issue
Block a user