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:
@@ -8,7 +8,7 @@ export const INTERACTIVE_PAGE_CAPS = {
|
||||
maxDemosPerPage: 5,
|
||||
maxSimsPerPage: 3,
|
||||
maxOverviewCards: 4,
|
||||
minOverviewCards: 2,
|
||||
minOverviewCards: 3,
|
||||
maxStatsItems: 5,
|
||||
minStatsItems: 2,
|
||||
maxJsonBytes: 128 * 1024,
|
||||
@@ -30,8 +30,14 @@ export const PAGE_BLOCK_TYPES = [
|
||||
'table',
|
||||
'image',
|
||||
'sim',
|
||||
'steps',
|
||||
] as const
|
||||
|
||||
export const STEPS_CAPS = {
|
||||
minSteps: 2,
|
||||
maxSteps: 12,
|
||||
} as const
|
||||
|
||||
export const CALLOUT_KINDS = [
|
||||
'definition',
|
||||
'warning',
|
||||
@@ -69,6 +75,8 @@ export const PAGE_HUMAN_STRING_KEYS = [
|
||||
'intro',
|
||||
'xLabel',
|
||||
'yLabel',
|
||||
// steps blocks
|
||||
'rule',
|
||||
// inherited from demos (speak etc. scanned via demo validator)
|
||||
'speak',
|
||||
'text',
|
||||
|
||||
@@ -63,6 +63,33 @@
|
||||
"simId": "ts-diagram"
|
||||
},
|
||||
"caption": "Le même cycle sur le diagramme T–s : les aires sont les chaleurs échangées."
|
||||
},
|
||||
{
|
||||
"type": "steps",
|
||||
"title": "Le COP frigorifique, dérivé pas à pas",
|
||||
"steps": [
|
||||
{
|
||||
"tex": "\\eta_{\\text{Carnot}} = 1 - \\frac{T_c}{T_h}",
|
||||
"rule": "Point de départ — rendement de Carnot",
|
||||
"speak": "Le rendement maximal d'un moteur entre $T_h$ et $T_c$ ne dépend que des températures."
|
||||
},
|
||||
{
|
||||
"tex": "\\mathrm{COP}_{\\text{PAC}} = \\frac{1}{\\eta_{\\text{Carnot}}} = \\frac{T_h}{T_h - T_c}",
|
||||
"rule": "Inversion — pompe à chaleur",
|
||||
"speak": "La pompe à chaleur est l'inverse du moteur : son COP est l'inverse du rendement."
|
||||
},
|
||||
{
|
||||
"tex": "\\mathrm{COP}_{\\text{frigo}} = \\mathrm{COP}_{\\text{PAC}} - 1 = \\frac{T_c}{T_h - T_c}",
|
||||
"rule": "Soustraction de 1 — réfrigérateur",
|
||||
"speak": "Le frigo ne compte que la chaleur utile $Q_c$ : on retire 1 au COP de la PAC."
|
||||
},
|
||||
{
|
||||
"tex": "\\mathrm{COP}_{\\text{frigo}} = \\frac{260}{300 - 260} = 6{,}5",
|
||||
"rule": "Application numérique",
|
||||
"speak": "Avec $T_c = 260$ K et $T_h = 300$ K : le COP maximal vaut 6,5."
|
||||
}
|
||||
],
|
||||
"caption": "Chaque ligne découle de la précédente — la règle appliquée est en marge."
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@ export {
|
||||
PAGE_BLOCK_TYPES,
|
||||
CALLOUT_KINDS,
|
||||
PAGE_HUMAN_STRING_KEYS,
|
||||
STEPS_CAPS,
|
||||
isPageHumanStringKey,
|
||||
} from './constants'
|
||||
export { pageSpecV1Schema, pageBlockSchema } from './schema'
|
||||
@@ -22,4 +23,6 @@ export type {
|
||||
CatalogSimRef,
|
||||
GenericFormulaSim,
|
||||
SimBlock,
|
||||
StepsBlock,
|
||||
DerivationStep,
|
||||
} from './types'
|
||||
|
||||
@@ -189,6 +189,31 @@ function normalizeBlock(
|
||||
return out
|
||||
}
|
||||
|
||||
if (type === 'steps' || type === 'derivation' || type === 'walkthrough' || type === 'solution' || type === 'proof') {
|
||||
const rawSteps = Array.isArray(obj.steps) ? obj.steps : []
|
||||
const steps = rawSteps
|
||||
.map((st) => {
|
||||
const r = asRecord(st)
|
||||
if (!r) return null
|
||||
const tex = asString(r.tex) || asString(r.latex) || asString(r.equation) || asString(r.math)
|
||||
if (!tex) return null
|
||||
const out: Record<string, unknown> = { tex }
|
||||
const rule = asString(r.rule) || asString(r.transform) || asString(r.action) || asString(r.operation)
|
||||
const speak = asString(r.speak) || asString(r.note) || asString(r.comment)
|
||||
if (rule) out.rule = rule
|
||||
if (speak) out.speak = speak
|
||||
return out
|
||||
})
|
||||
.filter(Boolean)
|
||||
if (steps.length < 2) return null
|
||||
const out: Record<string, unknown> = { type: 'steps', steps }
|
||||
const title = asString(obj.title)
|
||||
if (title) out.title = title
|
||||
const caption = asString(obj.caption)
|
||||
if (caption) out.caption = caption
|
||||
return out
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,9 @@ import {
|
||||
INTERACTIVE_PAGE_SCHEMA_VERSION,
|
||||
PAGE_BLOCK_TYPES,
|
||||
SIM_CAPS,
|
||||
STEPS_CAPS,
|
||||
} from './constants'
|
||||
import { isValidSimParamId } from './sim-eval'
|
||||
|
||||
const intentSchema = z.enum(INTENT_IDS).optional()
|
||||
|
||||
@@ -90,6 +92,9 @@ const imageBlock = z.object({
|
||||
const simParamIdSchema = z
|
||||
.string()
|
||||
.regex(/^[A-Za-z_][A-Za-z0-9_]*$/, 'Invalid sim identifier')
|
||||
.refine(isValidSimParamId, {
|
||||
message: 'Identifier collides with a reserved constant/function',
|
||||
})
|
||||
|
||||
const genericSimParamSchema = z.object({
|
||||
id: simParamIdSchema,
|
||||
@@ -150,6 +155,22 @@ const simBlock = z.object({
|
||||
caption: z.string().optional(),
|
||||
})
|
||||
|
||||
const stepsBlock = z.object({
|
||||
type: z.literal('steps'),
|
||||
title: z.string().optional(),
|
||||
steps: z
|
||||
.array(
|
||||
z.object({
|
||||
tex: z.string().min(1),
|
||||
rule: z.string().optional(),
|
||||
speak: z.string().optional(),
|
||||
})
|
||||
)
|
||||
.min(STEPS_CAPS.minSteps)
|
||||
.max(STEPS_CAPS.maxSteps),
|
||||
caption: z.string().optional(),
|
||||
})
|
||||
|
||||
export const pageBlockSchema = z.discriminatedUnion('type', [
|
||||
proseBlock,
|
||||
formulaBlock,
|
||||
@@ -160,6 +181,7 @@ export const pageBlockSchema = z.discriminatedUnion('type', [
|
||||
tableBlock,
|
||||
imageBlock,
|
||||
simBlock,
|
||||
stepsBlock,
|
||||
])
|
||||
|
||||
const sectionSchema = z.object({
|
||||
@@ -199,7 +221,7 @@ export const pageSpecV1Schema = z.object({
|
||||
overview: overviewSchema.optional(),
|
||||
sections: z
|
||||
.array(sectionSchema)
|
||||
.min(1)
|
||||
.min(2)
|
||||
.max(INTERACTIVE_PAGE_CAPS.maxSections),
|
||||
footer: z.string().optional(),
|
||||
})
|
||||
|
||||
@@ -115,6 +115,27 @@ export type SimBlock = {
|
||||
caption?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Step-by-step derivation (Symbolab/Khan style): equation states revealed
|
||||
* line by line with the transformation rule used at each step. Fully
|
||||
* generic — the LLM writes KaTeX + rules, the app renders; no drawing.
|
||||
*/
|
||||
export type DerivationStep = {
|
||||
/** KaTeX of the equation state at this step. */
|
||||
tex: string
|
||||
/** Transformation rule applied to reach this state (e.g. "on sépare les variables"). */
|
||||
rule?: string
|
||||
/** Narration for the Play/Step player (falls back to rule). */
|
||||
speak?: string
|
||||
}
|
||||
|
||||
export type StepsBlock = {
|
||||
type: 'steps'
|
||||
title?: string
|
||||
steps: DerivationStep[]
|
||||
caption?: string
|
||||
}
|
||||
|
||||
export type PageBlock =
|
||||
| ProseBlock
|
||||
| FormulaBlock
|
||||
@@ -125,6 +146,7 @@ export type PageBlock =
|
||||
| TableBlock
|
||||
| ImageBlock
|
||||
| SimBlock
|
||||
| StepsBlock
|
||||
|
||||
export type PageSection = {
|
||||
id: string
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
isPageHumanStringKey,
|
||||
} from './constants'
|
||||
import { pageSpecV1Schema } from './schema'
|
||||
import { validateSimExprRefs } from './sim-eval'
|
||||
import { validateSimExprRefs, isValidSimParamId } from './sim-eval'
|
||||
import type {
|
||||
PageBlock,
|
||||
PageSpecV1,
|
||||
@@ -79,6 +79,13 @@ function validateSim(
|
||||
if (sim.simId === 'generic-formula') {
|
||||
const generic = sim as Extract<typeof sim, { simId: 'generic-formula' }>
|
||||
const paramIds = new Set(generic.params.map((p) => p.id))
|
||||
if (paramIds.size !== generic.params.length) {
|
||||
out.push(issue('sim_duplicate_id', `${path}.params`, 'Duplicate param id'))
|
||||
}
|
||||
const computedIds = new Set(generic.computed.map((c) => c.id))
|
||||
if (computedIds.size !== generic.computed.length) {
|
||||
out.push(issue('sim_duplicate_id', `${path}.computed`, 'Duplicate computed id'))
|
||||
}
|
||||
for (const p of generic.params) {
|
||||
if (p.min >= p.max) {
|
||||
out.push(issue('sim_param_range', `${path}.params`, `Param "${p.id}": min >= max`))
|
||||
|
||||
Reference in New Issue
Block a user