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:
@@ -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.
|
||||
* 3–4 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)
|
||||
|
||||
Reference in New Issue
Block a user