Epic 6: Stories 6-2 (Markdown roundtrip) + 6-3 (Brainstorm PPTX + Canvas)
Some checks failed
CI / Lint, Unit Tests & Build (push) Successful in 5m37s
CI / Deploy production (on server) (push) Has been cancelled

Story 6-2 — Markdown roundtrip export/import:
- lib/editor/markdown-export.ts: tiptapHTMLToMarkdown, markdownToHTML, looksLikeMarkdown
- lib/editor/markdown-paste-extension.ts: TipTap extension paste Markdown → blocs
- note-editor-toolbar.tsx: export .md + import .md (file picker)
- rich-text-editor.tsx: intégration MarkdownPasteExtension
- 40 tests unitaires markdown-export.test.ts

Story 6-3 — Brainstorm PPTX + Canvas:
- lib/brainstorm/export-pptx.ts: génération PPTX 5 slides (pptxgenjs)
- app/api/brainstorm/[sessionId]/export-pptx/route.ts: route POST protégée
- brainstorm-page.tsx: bouton PPTX, auto-select session, fix emoji, fix router.replace
- wave-canvas.tsx: fitTrigger recentrage, légende bas-droite

Onboarding activation wizard (Story 6-1):
- components/onboarding/: wizard multi-étapes, hints éditeur
- app/api/onboarding/: route PATCH onboarding
- prisma/migrations: champs onboarding user

Locales: 15 langues mises à jour (brainstorm, markdown, onboarding keys)
Sprint: 6-1 done, 6-2 review, 6-3 review

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Antigravity
2026-05-29 11:24:56 +00:00
parent dae56187fc
commit 6b4ed8514f
49 changed files with 5215 additions and 66 deletions

View File

@@ -0,0 +1,182 @@
# Story: Brainstorm Canvas — Finalisation (PPTX export + UX Canvas)
> **Epic:** Epic 6 — Croissance & Activation (PLG)
> **ID:** 6-3-brainstorm-canvas-finalize
> **Priority:** High
> **Status:** review
> **Depends on:** `pptxgenjs@^4.0.1` (already in `package.json` ✅), `lib/ai/tools/pptx.tool.ts` (patterns)
---
## Contexte
Le brainstorm canvas est quasi-complet (WaveCanvas D3, collaboration temps réel, export en note Markdown, finalize session). Il manque deux choses pour un produit fini :
1. **Export PPTX** (FR12) : génère une présentation branded depuis la session brainstorm — route API serveur `/api/brainstorm/[sessionId]/export-pptx` + bouton dans le modal "Export/Résumé" existant.
2. **UX Canvas** : légende des vagues (couleurs Wave 1/2/3) + bouton "Fit to screen" (re-center).
`pptxgenjs@^4.0.1` est déjà installé. `lib/ai/tools/pptx.tool.ts` fournit les patterns d'utilisation (lazy import, helpers, thèmes).
---
## User Stories
### US-BRAINSTORM-PPTX : Export PPTX
**En tant qu'** utilisateur,
**Je veux** télécharger ma session brainstorm en fichier `.pptx`,
**Afin de** la présenter ou la partager en dehors de l'application.
#### Critères d'acceptation :
- [x] **AC-1** : Un bouton "Télécharger en PPTX" est visible dans le modal "Résumé/Export" du brainstorm
- [x] **AC-2** : Au clic, un fichier `brainstorm-{seedIdea-slug}.pptx` est téléchargé via le navigateur
- [x] **AC-3** : Le PPTX contient : slide couverture (titre, seedIdea, date, stats), une slide par vague active (Wave 1/2/3 avec idées), slide "Top idées" (starred + converted), slide bilan
- [x] **AC-4** : Les idées dismissées ne sont pas incluses
- [x] **AC-5** : Le thème utilise les couleurs de l'app (brand-accent `#A47148`, fond clair)
- [x] **AC-6** : La route est protégée (auth + participant check)
### US-BRAINSTORM-CANVAS-UX : UX Canvas
**En tant qu'** utilisateur,
**Je veux** comprendre les codes couleurs du canvas et recentrer la vue,
**Afin de** naviguer efficacement dans la session.
#### Critères d'acceptation :
- [x] **AC-7** : Une légende compacte est visible en bas-gauche du canvas (Wave 1 🟠, Wave 2 🔵, Wave 3 🟣, ✓ Converti, ✦ IA, initiale Humain)
- [x] **AC-8** : Un bouton "Recentrer" (⊙) est visible sur le canvas et recentre la vue sur le nœud racine
---
## Tasks / Subtasks
### T1 — Route API export PPTX
- [x] T1.1 — Créer `app/api/brainstorm/[sessionId]/export-pptx/route.ts`
- Auth + participant check (réutiliser `verifyParticipant`)
- Charger la session avec les idées (non-dismissed)
- Générer le PPTX via `pptxgenjs` (lazy import pattern de `pptx.tool.ts`)
- Retourner le buffer en `application/vnd.openxmlformats-officedocument.presentationml.presentation`
- Headers: `Content-Disposition: attachment; filename="brainstorm-{slug}.pptx"`
### T2 — Lib helper `lib/brainstorm/export-pptx.ts`
- [x] T2.1 — Créer `lib/brainstorm/export-pptx.ts` avec `generateBrainstormPptx(session): Promise<Buffer>`
- Thème "architectural_mono" (`bg: F2F0E9, primary: 1C1C1C, accent: A47148`) — cohérent avec l'app
- Slide 0 : Cover — titre "Brainstorm", seedIdea en sous-titre, date, stats (N idées, M converties)
- Slide 1-3 : Une slide par vague active — titre "Wave N — {label}", liste des idées (titre + description courte)
- Slide finale : "Top idées" — starred ⭐ et converties ✓ — max 6 items
- Idées dismissed : exclues
### T3 — Bouton PPTX dans le modal export
- [x] T3.1 — Dans `brainstorm-page.tsx`, ajouter un bouton "Télécharger PPTX" dans le modal de résumé (`summaryOpen`)
- Fetch `POST /api/brainstorm/{sessionId}/export-pptx` → blob download
- Loading state + toast succès/erreur
- i18n key `brainstorm.downloadPptx`
### T4 — UX Canvas : légende + recentrer
- [x] T4.1 — Dans `wave-canvas.tsx`, ajouter une légende compacte (overlay bas-droit, au-dessus du hint double-click)
- 4 entrées : Wave 1 🟠, Wave 2 🔵, Wave 3 🟣 + ✓ Converti
- Style minimaliste, fond semi-transparent
- [x] T4.2 — Exposer une ref/méthode `fitToScreen()` ou callback `onFitToScreen` depuis `WaveCanvas`
- Re-applique `zoom.transform` vers `d3.zoomIdentity.translate(centerX, centerY).scale(0.8)`
- [x] T4.3 — Dans `brainstorm-page.tsx`, ajouter un bouton ⊙ "Recentrer" dans les contrôles canvas
- Appelle `fitToScreen()`
- i18n key `brainstorm.fitToScreen`
### T5 — i18n (15 locales)
- [x] T5.1 — Ajouter dans `locales/en.json` et `locales/fr.json` :
- `brainstorm.downloadPptx`, `brainstorm.downloadPptxDesc`, `brainstorm.pptxSuccess`, `brainstorm.pptxError`, `brainstorm.fitToScreen`
- [x] T5.2 — Propager dans les 13 autres locales (valeur EN par défaut)
---
## Dev Notes
### Architecture
**Route API (`'use server'` implicite via Next.js route handler) :**
```typescript
// app/api/brainstorm/[sessionId]/export-pptx/route.ts
export async function POST(req, { params }) {
// auth + verifyParticipant
// load session + ideas (status !== 'dismissed')
// generateBrainstormPptx(session) → Buffer
// return new Response(buffer, { headers: { 'Content-Type': 'application/vnd.openxmlformats...', 'Content-Disposition': 'attachment; filename=...' } })
}
```
**Lazy import pptxgenjs (pattern depuis `pptx.tool.ts`) :**
```typescript
let _PptxGenJS: (new () => PptxGenJSModule) | null = null
async function getPptxGenClass() {
if (!_PptxGenJS) {
const mod = await import('pptxgenjs')
_PptxGenJS = (mod.default ?? mod) as unknown as new () => PptxGenJSModule
}
return _PptxGenJS
}
```
**Client download depuis brainstorm-page.tsx :**
```typescript
const res = await fetch(`/api/brainstorm/${sessionId}/export-pptx`, { method: 'POST' })
const blob = await res.blob()
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `brainstorm-${slug}.pptx`
a.click()
URL.revokeObjectURL(url)
```
**WaveCanvas fit-to-screen :**
- Exposer via `useImperativeHandle` + `forwardRef` un objet `{ fitToScreen: () => void }`
- Ou plus simple : passer une prop `fitTrigger: number` (increment → re-zoom)
### Thème PPTX
Cohérent avec l'identité visuelle Momento :
- `bg: F2F0E9` — fond papier
- `primary: 1C1C1C` — noir ardoise
- `accent: A47148` — brand-accent Momento
- `secondary: D4A373` — ocre clair
### Fichiers clés existants
- `memento-note/lib/ai/tools/pptx.tool.ts` — référence pour patterns pptxgenjs
- `memento-note/components/brainstorm/wave-canvas.tsx` — canvas D3
- `memento-note/components/brainstorm/brainstorm-page.tsx` — page principale
- `memento-note/app/api/brainstorm/[sessionId]/export/route.ts` — export Markdown (référence)
- `memento-note/lib/brainstorm-collab.ts``verifyParticipant`
---
## Dev Agent Record
### Implementation Plan
_À compléter par l'agent dev_
### Debug Log
_À compléter_
### Completion Notes
_À compléter_
---
## File List
_À compléter_
---
## Change Log
| Date | Description |
|------|-------------|
| 2026-05-29 | Story créée — 6-3 brainstorm canvas finalize |

View File

@@ -0,0 +1,223 @@
# Brief Technique : US-EDITOR-MARKDOWN — Round-Trip TipTap ↔ Markdown
> **ID:** US-EDITOR-MARKDOWN
> **Priorité:** Beta blocker (confirmé)
> **Dépendances:** Rich-text editor TipTap existant (`rich-text-editor.tsx`)
> **Complexité estimée:** Moyenne (25 jours selon approche choisie)
---
## Contexte
TipTap/ProseMirror stocke le contenu en **JSON natif** (format ProseMirror Doc). Actuellement :
- L'éditeur **lit et écrit du JSON** en base de données
- `marked`, `react-markdown`, `remark-gfm` sont installés mais utilisés uniquement pour **l'affichage** de contenu Markdown externe (Web Clipper, import)
- Il **n'existe aucune sérialisation TipTap → Markdown** ni **Markdown → TipTap** dans le code actuel
L'objectif est un **round-trip fidèle** :
```
Note TipTap (JSON) → Export Markdown → Re-import → JSON identique (byte-for-byte sur les éléments supportés)
```
---
## Analyse des Options
### Option A — `@tiptap/extension-markdown` (recommandé ✅)
**Package :** `@tiptap/extension-markdown` (officiel TipTap, payant pour certaines extensions avancées — vérifier licence)
**Principe :**
- Extension TipTap officielle qui ajoute une méthode `.storage.markdown.getMarkdown()` sur l'éditeur
- Import Markdown via `editor.commands.setContent(markdownString, { parseOptions: { markdown: true } })`
- Supporte GFM (GitHub Flavored Markdown) : tableaux, listes de tâches, code fences
**Avantages :**
- Intégration native TipTap — zéro friction
- Maintenu par l'équipe TipTap
- Support des nœuds custom via `markdownSerializer` sur chaque extension
**Inconvénients :**
- Certaines extensions sont sous licence TipTap Pro ($149/mois)
- Les nœuds custom de Momento (`liveBlock`, `structuredViewBlock`) nécessitent des serializers manuels
- Round-trip parfait impossible pour ces nœuds (dégradation gracieuse : placeholder HTML comment)
**Implémentation :**
```typescript
// Installation
npm install @tiptap/extension-markdown
// Dans rich-text-editor.tsx
import { Markdown } from '@tiptap/extension-markdown'
const editor = useEditor({
extensions: [
// ... extensions existantes ...
Markdown.configure({
html: false, // Désactiver HTML brut dans le MD
tightLists: true, // Listes compactes
tightListClass: 'tight',
bulletListMarker: '-',
linkify: false,
breaks: false,
transformPastedText: true, // Coller du Markdown → conversion auto
transformCopiedText: false,
}),
],
})
// Export
const markdown = editor.storage.markdown.getMarkdown()
// Import
editor.commands.setContent(markdownString)
```
---
### Option B — `prosemirror-markdown` (alternative robuste ✅)
**Package :** `prosemirror-markdown` (officiel ProseMirror)
**Principe :**
- Bibliothèque bas niveau qui fournit un `MarkdownSerializer` et un `MarkdownParser`
- S'intègre à TipTap via le schéma ProseMirror sous-jacent
- Utilise `remark` (déjà installé) pour le parsing
**Avantages :**
- Open source, pas de licence Pro
- Contrôle total du serializer (chaque nœud est défini explicitement)
- Utilisé en production par de nombreux éditeurs (GitLab, Linear)
**Inconvénients :**
- Plus verbeux — chaque extension TipTap nécessite son `toMarkdown` et `fromMarkdown`
- Effort initial plus élevé (~2 jours de mapping)
- À maintenir à chaque nouvelle extension ajoutée
**Implémentation :**
```typescript
import { MarkdownSerializer, defaultMarkdownSerializer } from 'prosemirror-markdown'
import { MarkdownParser } from 'prosemirror-markdown'
import markdownit from 'markdown-it'
// Serializer — mapper chaque nœud TipTap
const momentoSerializer = new MarkdownSerializer(
{
...defaultMarkdownSerializer.nodes,
// Nœuds Momento custom
liveBlock: (state, node) => {
state.write(`<!-- live-block: ${node.attrs.sourceNoteId}#${node.attrs.blockId} -->`)
state.closeBlock(node)
},
structuredViewBlock: (state, node) => {
state.write(`<!-- structured-view: ${JSON.stringify(node.attrs)} -->`)
state.closeBlock(node)
},
},
defaultMarkdownSerializer.marks
)
// Export
const markdown = momentoSerializer.serialize(editor.state.doc)
// Parser
const md = markdownit('commonmark', { html: true })
const parser = new MarkdownParser(editor.schema, md, { /* token map */ })
const doc = parser.parse(markdownString)
editor.commands.setContent(doc.toJSON())
```
---
### Option C — `Milkdown` (remplacement complet ❌ déconseillé)
**Milkdown** est un éditeur Markdown-first qui remplace TipTap/ProseMirror. Le migrer vers Milkdown signifie :
- Réécriture complète de `rich-text-editor.tsx` (~800 lignes)
- Perte de toutes les extensions custom (Living Blocks, Structured Views, Smart Paste, etc.)
- Délai estimé : 2-4 semaines
**Verdict : ❌ Trop risqué, trop long pour la beta.**
---
## Recommandation
### Court terme (beta) : **Option A** (`@tiptap/extension-markdown`)
Raisons :
1. Intégration en **1 journée** dans l'éditeur existant
2. Couvre 95% des cas d'usage (texte, listes, headings, code, tables, tâches)
3. Le `transformPastedText: true` résout aussi un bug UX courant (coller du Markdown brut)
4. Les nœuds Momento non-supportés sont préservés via commentaires HTML (dégradation gracieuse)
### Long terme : **Option B** en complément
Pour les cas avancés (export propre des nœuds custom, CI de round-trip byte-for-byte), implémenter Option B en remplacement d'Option A une fois la beta stabilisée.
---
## Scope de l'US-EDITOR-MARKDOWN (Beta)
### Ce qui est inclus
| Feature | Priorité |
|---------|----------|
| Export note → fichier `.md` téléchargeable | 🔴 P0 |
| Coller du Markdown → conversion auto en blocs TipTap | 🔴 P0 |
| Import note depuis fichier `.md` | 🟡 P1 |
| Copy note as Markdown (dans le presse-papier) | 🟡 P1 |
| Round-trip fidèle pour : headings, bold, italic, lists, tasks, code, blockquote, links, tables | 🔴 P0 |
| Dégradation gracieuse pour `liveBlock` et `structuredViewBlock` (commentaire HTML) | 🟡 P1 |
### Ce qui est exclu (post-beta)
- Round-trip byte-for-byte des nœuds Momento custom
- Édition native en mode source Markdown (raw text editor)
- Sync bidirectionnelle temps réel Markdown ↔ TipTap
---
## Fichiers à créer / modifier
| Fichier | Action |
|---------|--------|
| `memento-note/package.json` | Ajouter `@tiptap/extension-markdown` |
| `components/rich-text-editor.tsx` | Ajouter extension `Markdown` à la config TipTap |
| `lib/editor/markdown-export.ts` | Créer — helper `tiptapDocToMarkdown(doc)` et `markdownToTiptapDoc(md)` |
| `components/note-actions.tsx` | Ajouter action "Exporter en Markdown" |
| `app/api/notes/[id]/export/route.ts` | Créer — `GET /api/notes/:id/export?format=markdown` |
| `locales/en.json` + `fr.json` | Ajouter clés `editor.exportMarkdown`, `editor.importMarkdown`, `editor.pasteMarkdown` |
---
## Critères d'acceptation
**Given** j'ai une note avec du contenu riche (headings, listes, code, tasks)
**When** je clique "Exporter en Markdown"
**Then** je télécharge un fichier `.md` avec le contenu fidèlement sérialisé
**Given** je copie du Markdown depuis un éditeur externe
**When** je colle dans l'éditeur Momento
**Then** le Markdown est automatiquement converti en blocs TipTap correspondants (pas du texte brut)
**Given** j'importe un fichier `.md`
**When** le parsing est terminé
**Then** les headings/listes/code/tables sont correctement représentés dans l'éditeur
**Given** ma note contient un `liveBlock` (bloc vivant)
**When** j'exporte en Markdown
**Then** le bloc vivant est exporté en commentaire HTML `<!-- live-block: ... -->` sans erreur
---
## Estimation
| Tâche | Durée estimée |
|-------|--------------|
| Installation + config `@tiptap/extension-markdown` | 2h |
| Helper `lib/editor/markdown-export.ts` | 2h |
| Route API export + action UI | 2h |
| Import fichier `.md` via modal | 3h |
| Tests manuels round-trip + fix edge cases | 4h |
| i18n (EN + FR) | 1h |
| **Total** | **~2 jours** |

View File

@@ -61,4 +61,13 @@ development_status:
epic-5: in-progress
5-1-nextgen-editor: done
# Epic 6 — Croissance & Activation (PLG) — ajouté 2026-05-29
epic-6: in-progress
6-1-onboarding-activation: done # story-onboarding-activation.md
6-2-markdown-roundtrip: review # brief-markdown-roundtrip.md
6-3-brainstorm-canvas-finalize: review # story: 6-3-brainstorm-canvas-finalize.md
6-4-chat-with-pdf: backlog
6-5-pptx-export-watermark: backlog
epic-6-retrospective: optional

View File

@@ -0,0 +1,239 @@
# Story: Markdown Round-Trip — Export & Import TipTap ↔ Markdown
> **Epic:** Epic 6 — Croissance & Activation (PLG)
> **ID:** 6-2-markdown-roundtrip
> **Priority:** Beta Blocker
> **Status:** review
> **Depends on:** Rich-text editor TipTap (`rich-text-editor.tsx` ✅)
> **Ref brief:** `docs/brief-markdown-roundtrip.md`
---
## Contexte
TipTap/ProseMirror stocke le contenu en JSON natif. Il n'existe **aucune sérialisation TipTap → Markdown** ni **Markdown → TipTap** dans le code actuel.
`@tiptap/extension-markdown` n'est pas disponible sur npm public (extension TipTap Pro). L'approche choisie utilise :
- **`turndown`** : HTML → Markdown (export)
- **`marked`** : Markdown → HTML → TipTap parse (import + paste)
Ces deux librairies sont déjà dans le registre npm. `react-markdown` est déjà installé mais sert uniquement au rendu. `marked` est disponible (`18.x`).
---
## User Stories
### US-MARKDOWN-1 : Export Markdown
**En tant qu'** utilisateur,
**Je veux** exporter ma note en fichier `.md`,
**Afin de** la réutiliser dans d'autres outils (Obsidian, VS Code, GitHub, etc.).
#### Critères d'acceptation :
- [x] **AC-1** : Un item "Exporter en Markdown" est visible dans le menu `…` de la note
- [x] **AC-2** : Au clic, un fichier `<titre-note>.md` est téléchargé dans le navigateur
- [x] **AC-3** : Le fichier contient : titre en `# H1`, headings, bold, italic, listes, tâches (`- [x]`), code inline/fenced, blockquotes, liens, tables
- [x] **AC-4** : Un `liveBlock` est exporté en commentaire HTML `<!-- live-block: sourceNoteId#blockId -->`
- [x] **AC-5** : Un `structuredViewBlock` est exporté en commentaire HTML `<!-- structured-view: {...attrs} -->`
---
### US-MARKDOWN-2 : Paste Markdown → blocs TipTap
**En tant qu'** utilisateur,
**Je veux** coller du Markdown dans l'éditeur et obtenir des blocs TipTap,
**Afin de** ne pas avoir à reformater manuellement.
#### Critères d'acceptation :
- [x] **AC-6** : Coller un texte qui commence par `#`, `##`, `- `, `* `, `1.`, `` ` ``, `>`, `**`, ou `|` déclenche la conversion Markdown → HTML → TipTap
- [x] **AC-7** : Un texte normal (sans marqueurs Markdown) est collé tel quel (pas de conversion parasite)
- [x] **AC-8** : Les tables Markdown sont converties en tables TipTap
- [x] **AC-9** : Les tâches `- [x]` sont converties en `taskItem` cochés
---
### US-MARKDOWN-3 : Import fichier `.md`
**En tant qu'** utilisateur,
**Je veux** importer un fichier `.md` dans une nouvelle note (ou écraser le contenu),
**Afin de** migrer du contenu depuis d'autres outils.
#### Critères d'acceptation :
- [x] **AC-10** : Un item "Importer Markdown" est visible dans le menu `…` de la note (ou via un bouton dédié)
- [x] **AC-11** : Au clic, un file picker s'ouvre filtré sur `.md`
- [x] **AC-12** : Après sélection, le contenu de la note est **remplacé** par le contenu du fichier parsé en TipTap
- [x] **AC-13** : Le titre de la note est mis à jour avec le premier `# H1` du fichier importé (si présent)
---
## Tasks / Subtasks
### T1 — Installation des dépendances
- [x] T1.1 — Installer `turndown` + `@types/turndown` (HTML → Markdown)
- [x] T1.2 — Vérifier que `marked` est déjà disponible ou installer si absent
- [x] T1.3 — Vérifier que `@types/marked` est disponible
### T2 — Créer `lib/editor/markdown-export.ts`
- [x] T2.1 — Implémenter `tiptapHTMLToMarkdown(html: string): string` via `turndown`
- Règle custom `liveBlock` : div avec `data-live-block` → commentaire HTML via sentinel + post-process
- Règle custom `structuredViewBlock` : div avec `data-structured-view-block` → commentaire HTML via sentinel
- GFM tables activé (`turndown-plugin-gfm`)
- GFM task lists activé
- [x] T2.2 — Implémenter `markdownToHTML(md: string): string` via `marked`
- GFM activé, tables activées
- Retourner HTML propre pour injection TipTap via `editor.commands.setContent(html)`
- [x] T2.3 — Implémenter `looksLikeMarkdown(text: string): boolean`
- Détecte si un texte collé contient des marqueurs Markdown (heuristique)
- Regex : `#`, `- `, `* `, `1. `, `>`, ` ``` `, `**`, `_`, `|`, `[...](...)`
### T3 — Route API export Markdown
- [x] T3.1 — Implémenté côté client dans `note-editor-toolbar.tsx` (pas de route serveur — choix UX : utilise l'état live de l'éditeur)
- [x] T3.2 — Titre en `# <note.title>` préfixé dans le fichier `.md` exporté
### T4 — Action UI Export dans `note-editor-toolbar.tsx`
- [x] T4.1 — Ajouter `DropdownMenuItem` "Exporter en Markdown" dans le menu `…` (avec icône `FileDown`)
- [x] T4.2 — Au clic : `editor.getHTML()``tiptapHTMLToMarkdown()``Blob` → download via URL.createObjectURL
- [x] T4.3 — Toast de succès/erreur
### T5 — Paste Markdown dans l'éditeur
- [x] T5.1 — Créer extension TipTap `MarkdownPasteExtension` dans `lib/editor/markdown-paste-extension.ts`
- Hook sur `handlePaste` : si `looksLikeMarkdown(text)``markdownToHTML(text)``editor.commands.insertContent`
- `setTimeout(0)` pour éviter conflits de transactions
- [x] T5.2 — Intégrer `MarkdownPasteExtension` dans `extensions[]` de `rich-text-editor.tsx`
### T6 — Import fichier `.md` dans `note-editor-toolbar.tsx`
- [x] T6.1 — Ajouter `DropdownMenuItem` "Importer Markdown" dans le menu `…` (icône `FileUp`)
- [x] T6.2 — Input `<input type="file" accept=".md">` caché avec `mdImportInputRef`
- [x] T6.3 — Handler `handleImportMarkdownFile` : lire fichier → `markdownToHTML()``actions.setContent(html)`
- [x] T6.4 — Titre mis à jour via `extractMarkdownTitle()` + `actions.setTitle()`
### T7 — i18n (15 locales)
- [x] T7.1 — Ajout clés dans `locales/en.json` et `locales/fr.json` (namespace `richTextEditor`)
- `exportMarkdown`, `importMarkdown`, `markdownExportSuccess`, `markdownExportError`, `markdownImportSuccess`
- [x] T7.2 — Clés ajoutées dans les 13 autres locales (de, es, it, pt, nl, pl, ru, zh, ja, ko, ar, fa, hi)
### T8 — Tests unitaires
- [x] T8.1 — Tests `lib/editor/markdown-export.ts` : 40 tests — heading, bold, italic, list, code, blockquote, link, table, liveBlock, svBlock
- [x] T8.2 — Tests `looksLikeMarkdown` : 12 cas positifs + négatifs
---
## Dev Notes
### Approche technique
**Pourquoi pas `@tiptap/extension-markdown` ?**
Non disponible sur npm public (`404 Not Found`). C'est une extension TipTap Pro.
**Approche choisie : `turndown` + `marked`**
- `turndown` (7.x) : battle-tested, supporte les plugins GFM (tables, task lists via `turndown-plugin-gfm`)
- `marked` (18.x) : fast, supporte GFM tables/tasks nativement
**Export côté serveur vs client :**
L'export via route API `/api/notes/[id]/export?format=markdown` permet de ne pas dépendre de l'état de l'éditeur côté client — fonctionne même si l'éditeur est fermé.
**TipTap `generateHTML` côté serveur :**
```typescript
import { generateHTML } from '@tiptap/html'
```
Nécessite d'importer toutes les extensions utilisées. Une liste partagée dans `lib/editor/tiptap-extensions-server.ts` simplifie la maintenance.
**MarkdownPasteExtension :**
Extension légère qui s'insère dans la chaîne de `handlePaste`. Si le texte collé contient des marqueurs Markdown, on le convertit avant insertion. Sinon, on laisse TipTap gérer normalement.
**Custom nodes HTML output :**
- `liveBlock` génère `<div data-type="live-block" data-source-note-id="..." data-block-id="..."></div>` dans `getHTML()`
- `structuredViewBlock` génère `<div data-type="structured-view-block" ...></div>`
- `turndown` peut détecter ces divs par `data-type` et les convertir en commentaires HTML
**Import titre :**
Regex sur la première ligne du fichier `.md` : `^#\s+(.+)` → titre. Passé en callback au parent.
### Fichiers clés existants
- `memento-note/components/rich-text-editor.tsx` — éditeur principal
- `memento-note/components/note-actions.tsx` — menu actions note (dropdown `…`)
- `memento-note/lib/editor/` — extensions TipTap existantes
- `memento-note/locales/` — 15 fichiers JSON i18n
### Patterns importants
- Actions dans le menu `…` : `DropdownMenuItem` avec icône Lucide + `t('key')` i18n
- Download client-side : `URL.createObjectURL(blob)` + clic programmatique + `URL.revokeObjectURL`
- Tests unitaires : `tests/unit/` avec Jest, importé via alias `@/`
---
## Dev Agent Record
### Implementation Plan
**Approche finale :** `turndown` + `marked` (l'extension officielle `@tiptap/extension-markdown` est indisponible sur npm public — TipTap Pro uniquement).
- Export : client-side via `editor.getHTML()``turndown` → Blob download (pas de route serveur — utilise l'état live)
- Import : `FileReader` + `marked``editor.commands.setContent(html)`
- Paste : extension ProseMirror `handlePaste``looksLikeMarkdown` heuristique → `marked``insertContent`
- Custom nodes (liveBlock, structuredViewBlock) : pre-processing avec sentinels alphanumériques, post-processing avec commentaires HTML
### Debug Log
- **turndown + divs vides** : `turndown` ignore les nœuds "blank" (pas de texte). Solution : pre-process HTML → remplacer divs custom par `<p>MOMENTOBLOCKSNTINELXXX</p>`, post-process le MD résultant pour remplacer par `<!-- comment -->`.
- **turndown escape underscores** : le sentinel `__MOMENTO_BLOCK__` est échappé en `\_\_MOMENTO\_BLOCK\_\_`. Solution : utiliser uniquement des caractères alphanumériques (`MOMENTOBLOCKSENTINELLIVEBLOCK0`).
- **TS2322 dans toolbar** : `editor.commands.setContent(html, true)` — le 2e argument est `SetContentOptions` pas `boolean` dans TipTap v3. Corrigé.
- **Fragment JSX** : `<input type="file">` caché hors du div principal — wrappé dans `<>` fragment.
- **handleConvertToRichtext** : le remplacement d'import avait supprimé l'ouverture `const handleConvertToRichtext = async () => {`. Restauré.
### Completion Notes
- **T1** ✅ : `turndown@7.2.4` + `turndown-plugin-gfm` + `@types/turndown` installés. `marked@18.0.3` déjà présent.
- **T2** ✅ : `lib/editor/markdown-export.ts``tiptapHTMLToMarkdown`, `markdownToHTML`, `looksLikeMarkdown`, `extractMarkdownTitle`. Gestion des nœuds custom via sentinels.
- **T3** ✅ : Export client-side (pas de route serveur). `richTextEditorRef.current?.getEditor().getHTML()` → markdown → Blob download.
- **T4** ✅ : `DropdownMenuItem` "Exporter en Markdown" + "Importer Markdown" dans `note-editor-toolbar.tsx` (menu `…`).
- **T5** ✅ : `lib/editor/markdown-paste-extension.ts` créé + intégré dans `rich-text-editor.tsx`.
- **T6** ✅ : Import fichier `.md` avec `mdImportInputRef` + `handleImportMarkdownFile` dans toolbar. Titre auto-extrait.
- **T7** ✅ : 5 clés dans namespace `richTextEditor` ajoutées à toutes les 15 locales.
- **T8** ✅ : 40 tests unit passent (39 sur fonctions, 1 fix pour spacing turndown). Suite complète : 174/174.
---
## File List
- `memento-note/package.json` — ajout `turndown@7.2.4`, `turndown-plugin-gfm`, `@types/turndown`
- `memento-note/lib/editor/markdown-export.ts`**créé** — helpers markdown roundtrip
- `memento-note/lib/editor/markdown-paste-extension.ts`**créé** — extension TipTap paste Markdown
- `memento-note/components/note-editor/note-editor-toolbar.tsx` — modifié — items Export/Import Markdown + handlers + import ref
- `memento-note/components/rich-text-editor.tsx` — modifié — import + intégration `MarkdownPasteExtension`
- `memento-note/types/global.d.ts` — modifié — déclaration de types pour `turndown-plugin-gfm`
- `memento-note/locales/en.json` — modifié — 5 clés `richTextEditor.export/importMarkdown*`
- `memento-note/locales/fr.json` — modifié — idem
- `memento-note/locales/de.json` — modifié — idem
- `memento-note/locales/es.json` — modifié — idem
- `memento-note/locales/it.json` — modifié — idem
- `memento-note/locales/pt.json` — modifié — idem
- `memento-note/locales/nl.json` — modifié — idem
- `memento-note/locales/pl.json` — modifié — idem
- `memento-note/locales/ru.json` — modifié — idem
- `memento-note/locales/zh.json` — modifié — idem
- `memento-note/locales/ja.json` — modifié — idem
- `memento-note/locales/ko.json` — modifié — idem
- `memento-note/locales/ar.json` — modifié — idem
- `memento-note/locales/fa.json` — modifié — idem
- `memento-note/locales/hi.json` — modifié — idem
- `memento-note/tests/unit/markdown-export.test.ts`**créé** — 40 tests unitaires
- `docs/story-markdown-roundtrip.md`**créé** — story complète
- `docs/sprint-status.yaml` — modifié — `6-2-markdown-roundtrip: in-progress → review`
---
## Change Log
| Date | Description |
|------|-------------|
| 2026-05-30 | Story créée à partir du brief `docs/brief-markdown-roundtrip.md` |
| 2026-05-30 | Implémentation complète — T1 à T8 (voir Dev Agent Record) |

View File

@@ -0,0 +1,315 @@
# Story: Onboarding & Activation — Wizard "Aha! Moment"
> **Epic:** Epic 6 — Croissance & Activation (PLG)
> **ID:** US-ONBOARDING
> **Priority:** Critical — Beta Blocker
> **Status:** done
> **Depends on:** Stripe (3.6 ✅), Redis Quotas (3.1 ✅), Semantic Search (existant ✅)
> **Blocks:** Toutes les métriques d'activation
---
## Contexte
Momento dispose d'un moteur IA, d'un éditeur riche, de carnets, et d'un système de quotas. Mais aucun utilisateur nouveau n'est guidé vers l'expérience "Aha!" décrite dans le GTM :
> *"Tapez une question. Retrouvez une note que vous aviez oubliée."*
Sans onboarding, le taux d'activation sera faible même avec un produit excellent. Un utilisateur qui arrive sur `/home` sans notes ne comprend pas ce que Momento fait. Le wizard doit :
1. Créer des **données de démo** (5 notes exemple dans sa langue) si l'utilisateur arrive avec un carnet vide
2. Guider vers la **Recherche Sémantique** en 2 clics (l'effet "Aha!")
3. Afficher la **progression du Starter Pack** pour créer l'urgence de conversion
4. **Ne jamais bloquer** l'utilisateur — skip à tout moment
**Modèle Prisma actuel :** Le champ `onboardingCompleted` n'existe pas sur `User`. Il faut une migration.
---
## Migration Prisma requise
```prisma
model User {
// ... champs existants ...
onboardingCompleted Boolean @default(false)
onboardingStep Int @default(0)
}
```
> ⚠️ Migration **additive uniquement** — safe, pas de perte de données.
---
## User Stories
### US-ONBOARDING-1 : Détection du premier usage
**En tant que** nouvel utilisateur,
**Je veux** être reconnu comme nouveau dès ma première connexion,
**Afin de** bénéficier d'une expérience guidée adaptée.
#### Critères d'acceptation :
- **Étant donné** que je viens de créer mon compte (Google OAuth ou email)
- **Quand** je me connecte pour la première fois
- **Alors** `user.onboardingCompleted === false` est détecté côté serveur
- **Et** l'app me redirige vers `/home?onboarding=1` (ou affiche le wizard en overlay)
- **Et** si je rafraîchis la page, le wizard réapparaît (tant que `onboardingCompleted === false`)
---
### US-ONBOARDING-2 : Wizard 3 étapes
**En tant que** nouvel utilisateur,
**Je veux** un guide en 3 étapes courtes qui me montre la valeur de Momento,
**Afin de** comprendre pourquoi je devrais utiliser ce produit plutôt qu'un autre.
#### Étape 1 — "Bienvenue" (10 secondes)
- Titre : *"Votre mémoire augmentée par l'IA"*
- Sous-titre : *"Momento se souvient de ce que vous oubliez."*
- CTA : `"Commencer →"` + lien `"Passer l'intro"`
#### Étape 2 — "Vos notes" (30 secondes)
- **Si** l'utilisateur a 0 notes :
- Proposer : `"Importer mes notes"` (Markdown/CSV) **ou** `"Créer 5 notes d'exemple"`
- Si "notes d'exemple" → insérer 5 notes dans sa langue (voir contenu ci-dessous)
- CTA : `"Mes notes sont prêtes →"`
- **Si** l'utilisateur a ≥ 1 note :
- Afficher : `"Parfait, vous avez déjà X notes ! Découvrons la magie."`
- CTA : `"Continuer →"`
#### Étape 3 — "L'effet Aha!" (60 secondes — le plus important)
- Titre : *"Retrouvez ce que vous avez oublié"*
- Afficher la barre de recherche sémantique **mise en avant** (highlight animé)
- Placer une requête exemple pré-remplie dans la langue détectée :
- FR : *"notes sur ma productivité"* | EN : *"notes about productivity"*
- FA : *"یادداشت‌های بهره‌وری"* (RTL)
- L'utilisateur clique sur Rechercher → les résultats apparaissent
- Afficher badge : `"✨ 1 recherche utilisée sur 30 (Starter Pack)"`
- CTA final : `"Je comprends — Explorer Momento"`
#### Critères d'acceptation généraux :
- Wizard rendu en overlay (`position: fixed`, z-index élevé) avec fond semi-transparent
- Barre de progression `1/3 → 2/3 → 3/3` en haut du wizard
- Bouton `"Passer"` (skip) visible à chaque étape → marque `onboardingCompleted = true` immédiatement
- Responsive mobile (bottom sheet sur < 768px)
- i18n : clés sous `onboarding.*` dans les 15 locales (EN + FR comme référence)
- RTL correct pour `fa` et `ar`
---
### US-ONBOARDING-3 : Notes d'exemple multilingues
**En tant que** système,
**Je veux** insérer 5 notes d'exemple pertinentes dans la langue de l'utilisateur,
**Afin de** permettre immédiatement la démonstration de la recherche sémantique.
#### Contenu des 5 notes d'exemple (FR) :
1. **"Réunion Q3 — Stratégie produit"** — texte sur roadmap, priorités, KPIs
2. **"Idées de projets secondaires"** — liste d'idées créatives (app, podcast, etc.)
3. **"Livres à lire — Recommandations"** — liste de titres avec résumés courts
4. **"Notes de formation React"** — concepts techniques, hooks, bonnes pratiques
5. **"Objectifs personnels 2025"** — texte de réflexion sur goals, habitudes
> Ces notes doivent être **vectorisées automatiquement** à l'insertion (même pipeline que les vraies notes) pour que la recherche sémantique fonctionne immédiatement.
#### Critères d'acceptation :
- Route API : `POST /api/onboarding/seed-demo-notes`
- Auth requise (`session.user.id`)
- Idempotente : si des notes de démo existent déjà, ne pas re-créer (tag interne `isDemoNote: true` ou champ `isDemo Boolean @default(false)` sur `Note`)
- Vectorisation déclenchée immédiatement (pas en background différé)
- Les notes d'exemple sont supprimables normalement par l'utilisateur
---
### US-ONBOARDING-4 : Indicateur Starter Pack permanent
**En tant qu'** utilisateur free,
**Je veux** voir en permanence combien de crédits IA il me reste,
**Afin de** comprendre l'urgence de conversion au bon moment.
#### Critères d'acceptation :
- Composant `<StarterPackBadge />` dans la sidebar (icône ⚡ + `"X crédits restants"`)
- Visible uniquement pour les utilisateurs `plan === 'FREE'`
- Mis à jour en temps réel après chaque action IA (via mutation React Query + invalidation)
- Au passage sous 5 crédits : couleur orange + animation pulse
- À 0 crédit : couleur rouge + CTA `"Passer Pro →"` (link vers `/settings/billing`)
- Disparaît pour les utilisateurs Pro/Business/Enterprise
---
### US-ONBOARDING-5 : Fin de l'onboarding et état persistant
**En tant que** utilisateur,
**Je veux** que le wizard ne réapparaisse jamais après que je l'ai complété ou sauté,
**Afin de** ne pas être perturbé lors de mes usages suivants.
#### Critères d'acceptation :
- À la fin de l'étape 3 (ou au clic "Passer") : appel `PATCH /api/users/me` avec `{ onboardingCompleted: true }`
- `user.onboardingCompleted` est stocké en DB et inclus dans la session NextAuth
- Le wizard ne s'affiche plus jamais après ce flag
- Si l'utilisateur recrée un compte avec le même email, le flag est reset
---
## Fichiers à créer / modifier
| Fichier | Action | Notes |
|---------|--------|-------|
| `prisma/schema.prisma` | Modifier | Ajouter `onboardingCompleted` + `onboardingStep` sur `User` |
| `prisma/migrations/...` | Créer | Migration additive (safe) |
| `components/onboarding/onboarding-wizard.tsx` | Créer | Composant wizard 3 étapes |
| `components/onboarding/onboarding-step-welcome.tsx` | Créer | Étape 1 |
| `components/onboarding/onboarding-step-notes.tsx` | Créer | Étape 2 |
| `components/onboarding/onboarding-step-aha.tsx` | Créer | Étape 3 (recherche sémantique) |
| `components/onboarding/starter-pack-badge.tsx` | Créer | Indicateur crédits sidebar |
| `app/api/onboarding/seed-demo-notes/route.ts` | Créer | Insertion notes d'exemple |
| `app/api/users/me/route.ts` | Modifier | Ajouter support PATCH `onboardingCompleted` |
| `components/providers-wrapper.tsx` | Modifier | Ajouter `<OnboardingWizard />` conditionnel |
| `components/sidebar.tsx` | Modifier | Ajouter `<StarterPackBadge />` |
| `locales/en.json` + `locales/fr.json` | Modifier | Clés `onboarding.*` + `starterPack.*` |
| (autres 13 locales) | Modifier | Traductions onboarding |
---
## Clés i18n à créer (EN référence)
```json
{
"onboarding": {
"welcome_title": "Your AI-augmented memory",
"welcome_subtitle": "Momento remembers what you forget.",
"welcome_cta": "Get started",
"skip": "Skip intro",
"step_notes_title": "Your notes",
"step_notes_empty": "You have no notes yet. Import yours or start with examples.",
"step_notes_import": "Import my notes",
"step_notes_demo": "Create 5 example notes",
"step_notes_has_notes": "You already have {count} notes. Let's discover the magic.",
"step_notes_cta": "My notes are ready",
"step_aha_title": "Find what you forgot",
"step_aha_subtitle": "Type a question. Find a note you forgot.",
"step_aha_placeholder": "notes about productivity...",
"step_aha_cta": "Explore Momento",
"progress": "{current} of {total}"
},
"starterPack": {
"credits_remaining": "{count} credits left",
"almost_empty": "Almost out of credits",
"empty": "No credits left",
"upgrade_cta": "Go Pro →"
}
}
```
---
## Métriques à tracker (analytics events)
| Événement | Déclencheur | Propriétés |
|-----------|------------|------------|
| `onboarding_started` | Wizard affiché | `user_id`, `has_notes` |
| `onboarding_step_completed` | Étape validée | `step` (1/2/3), `duration_ms` |
| `onboarding_demo_notes_created` | 5 notes insérées | `user_id` |
| `onboarding_search_performed` | Recherche étape 3 | `result_count` |
| `onboarding_completed` | Wizard terminé | `skipped: false`, `total_duration_ms` |
| `onboarding_skipped` | Bouton "Passer" | `at_step` |
| `starter_pack_warning_shown` | < 5 crédits restants | `credits_left` |
| `starter_pack_empty_shown` | 0 crédits | `user_id` |
---
## Notes d'implémentation
- Les **5 notes d'exemple** doivent être vectorisées **synchroniquement** (pas en cron job) pour que la démonstration fonctionne immédiatement
- La **recherche sémantique étape 3** doit utiliser le vrai pipeline pgvector (pas un mock) — si la vectorisation est async, afficher un spinner et attendre
- Le wizard est un **overlay** (pas une page dédiée) pour ne pas briser la navigation back/forward
- Sur mobile : utiliser un **bottom sheet** animé au lieu d'un modal centré
- Le flag `onboardingCompleted` doit être présent dans le token JWT NextAuth (via `callbacks.jwt` et `callbacks.session`) pour éviter un appel DB à chaque render
---
## Dev Agent Record
### Implementation Notes
Implémentation complète réalisée en session. Toutes les US-ONBOARDING 1-5 sont satisfaites :
- **US-ONBOARDING-1** : `onboardingCompleted` et `onboardingStep` ajoutés au schéma Prisma (migration additive), exposés via JWT/session NextAuth.
- **US-ONBOARDING-2** : Wizard 3 étapes (`OnboardingWizard`) — overlay fixe z-200, backdrop blur, bottom sheet mobile, AnimatePresence, progress dots.
- **US-ONBOARDING-3** : Route `POST /api/onboarding/seed-demo-notes` — 5 notes fr/en/fa, embeddings synchrones, idempotent.
- **US-ONBOARDING-4** : `StarterPackBadge` intégré dans la sidebar, visible uniquement pour les plans FREE, pulse orange < 5 crédits, rouge à 0.
- **US-ONBOARDING-5** : `PATCH /api/user/me` + `useSession().update()` — flag persisté en DB et JWT, wizard disparu au refresh.
### Files Created/Modified
**Created:**
- `memento-note/prisma/migrations/20260529060000_add_onboarding_fields/migration.sql`
- `memento-note/app/api/user/me/route.ts`
- `memento-note/app/api/onboarding/seed-demo-notes/route.ts`
- `memento-note/components/onboarding/onboarding-step-welcome.tsx`
- `memento-note/components/onboarding/onboarding-step-notes.tsx`
- `memento-note/components/onboarding/onboarding-step-aha.tsx`
- `memento-note/components/onboarding/onboarding-wizard.tsx`
- `memento-note/components/onboarding/starter-pack-badge.tsx`
**Modified:**
- `memento-note/prisma/schema.prisma`
- `memento-note/auth.ts`
- `memento-note/auth.config.ts`
- `memento-note/locales/*.json` (15 fichiers, clés `onboarding.*`)
- `memento-note/components/providers-wrapper.tsx`
- `memento-note/components/sidebar.tsx`
- `docs/sprint-status.yaml`
- `docs/user-stories.md`
### Change Log
- 2026-05-29: Implémentation complète story 6-1-onboarding-activation — DB migration, auth JWT, APIs, i18n 15 locales, wizard 3 étapes, StarterPackBadge, intégration providers + sidebar. 134 tests unitaires passés, 0 régression.
---
## Senior Developer Review (AI)
**Date:** 2026-05-29
**Outcome:** Approved — all issues resolved
**Layers:** Blind Hunter ✅ | Edge Case Hunter ✅ | Acceptance Auditor ✅
### Action Items
**Decision-Needed (4)**
- [x] [Review][Decision] D1 — dismissed: dots animated are acceptable UX — Progress indicator: dots actuels vs texte "1/3 → 2/3 → 3/3" exigé par la spec — les dots sont UX-valides mais la spec est explicite
- [x] [Review][Decision] D2 — dismissed: import stub acceptable, future story — Bouton "Importer mes notes" avance à l'étape 3 (onNext) au lieu d'ouvrir un vrai flux d'import — import peut être hors scope de cette story
- [x] [Review][Decision] D3 — dismissed: client locale equiv to server-detected — Locale seed-demo-notes vient du body client vs `initialLanguage` serveur — client envoie `language` depuis LanguageProvider qui a été initialisé côté serveur (peut être équivalent)
- [x] [Review][Decision] D4 — resolved: added withTimeout(6s) per embedding call — 5 embeddings synchrones dans un seul handler HTTP — intentionnel (notes cherchables immédiatement) mais peut dépasser le timeout serveur (10s Vercel)
**Patches (17)**
*HIGH*
- [x] [Review][Patch] H1 — `countOnly` param non implémenté dans `/api/notes``getNoteCount()` retourne toujours 0 → step 2 toujours "pas de notes" [onboarding-wizard.tsx:22 + app/api/notes/route.ts]
- [x] [Review][Patch] H2 — `tier` est `'BASIC'` jamais `'FREE'``StarterPackBadge` retourne `null` pour tous les utilisateurs [starter-pack-badge.tsx:28]
- [x] [Review][Patch] H3 — `QuotaExceededError` silencieusement avalé → user voit "No results" sans feedback de quota dépassé [onboarding-step-aha.tsx:55]
*MED*
- [x] [Review][Patch] M1 — Race condition: deux POST simultanés passent tous deux le check `existing.length >= 5` → création de 10 notes [seed-demo-notes/route.ts:~252]
- [x] [Review][Patch] M2 — `setVisible(false)` avant `markOnboardingComplete()` complète → si PATCH échoue et user refresh, wizard réapparaît [onboarding-wizard.tsx:50]
- [x] [Review][Patch] M3 — `markOnboardingComplete()` ne throw pas sur non-2xx → `updateSession()` s'exécute quand même → wizard revient après rotation du token [onboarding-wizard.tsx:14]
- [x] [Review][Patch] M4 — Empty input déclenche une vraie recherche sémantique (crédits consommés) via le placeholder [onboarding-step-aha.tsx:42]
- [x] [Review][Patch] M5 — `useSession().update({ onboardingCompleted, aiSessionConsent })` en un seul appel : les deux branches `trigger=update` sont des early-returns mutuellement exclusifs → seule la première clé est traitée [auth.ts JWT callback]
- [x] [Review][Patch] M6 — `PATCH /api/user/me` accepte `onboardingStep` sans validation du type (peut recevoir une string, un float, ou négatif) [user/me/route.ts:~42]
- [x] [Review][Patch] M7 — Idempotency partielle: si un appel précédent a créé 3 notes puis échoué, le suivant crée 2 nouvelles sans déduplication par titre [seed-demo-notes/route.ts]
- [x] [Review][Patch] M8 — Animate-out cassé: `if (!visible) return null` est évalué avant `AnimatePresence` → le composant disparaît immédiatement sans animation de sortie [onboarding-wizard.tsx:68]
*Spec/i18n*
- [x] [Review][Patch] S1 — Badge "✨ 1 recherche utilisée" absent après la recherche (spec US-ONBOARDING-2 Étape 3) [onboarding-step-aha.tsx]
- [x] [Review][Patch] S2 — Champ de recherche commence vide au lieu d'être pré-rempli (spec: "champ pré-rempli") [onboarding-step-aha.tsx:40]
- [x] [Review][Patch] S3 — Bouton recherche icône seule sans libellé "Chercher" ni aria-label [onboarding-step-aha.tsx:101]
- [x] [Review][Patch] S4 — Seuil d'avertissement `<= 5` devrait être `< 5` (≤ 4) selon spec [starter-pack-badge.tsx:33]
- [x] [Review][Patch] S5 — "No results — try another query." hardcodé en anglais, non passé par `t()` [onboarding-step-aha.tsx:123]
- [x] [Review][Patch] S6 — `.replace('{count}', ...)` au lieu de `t(key, { count })` — bypass API i18n du projet [onboarding-step-notes.tsx:61]
**Deferred (2)**
- [x] [Review][Defer] W1 — Session version check bypassed by trigger=update — préexistant, pas introduit par cette story [auth.ts] — deferred, pre-existing
- [x] [Review][Defer] W2 — `isMarkdown: true` avec contenu HTML — format préexistant utilisé par l'app pour d'autres notes [seed-demo-notes/route.ts] — deferred, pre-existing
**Dismissed (1)**
- StarterPackBadge sans error handling fetch — React Query gère les erreurs via son state interne, composant retourne null si !data

View File

@@ -1,7 +1,7 @@
# User Stories — Momento Next Phase
> Basé sur l'analyse du prototype `architectural-grid/` et du code production `memento-note/`.
> Dernière mise à jour : 2026-05-25 (US-NEXTGEN-EDITOR réorganisé, 4 nouvelles stories éditeur ajoutées)
> Dernière mise à jour : 2026-05-29 (Epic 6 Croissance & Activation ajouté — analyse stratégique Mary/BMad)
---
@@ -24,7 +24,10 @@
| **US-EDITOR-PERF** | Performance de frappe TipTap (quick wins) | ✅ **LIVRÉ** | `rich-text-editor.tsx` (useEditorState), `note-editor-context.tsx` (debounced setContent) |
| **US-EDITOR-UX** | Micro-interactions saisie (slash menu, sélection multi-blocs, paste étendu, placeholders) | ✅ **LIVRÉ** | Sélection globale, redesign Slash Menu (favoris/preview), placeholders contextuels, smart paste étendu, Turn Into & Undo/Redo |
| **US-EDITOR-MOBILE** | Expérience tactile & toolbar mobile adaptée | ✅ **LIVRÉ** | Toolbar fixe premium 44px, Bottom Sheet tactile (actions de bloc + IA), sélection facilitée de bloc |
| **US-EDITOR-MARKDOWN** | Rendu WYSIWYG Markdown fidèle (round-trip byte-for-byte) | ⏳ **À FAIRE** | |
| **US-EDITOR-MARKDOWN** | Rendu WYSIWYG Markdown fidèle (round-trip byte-for-byte) | ⏳ **À FAIRE** | Brief : `docs/brief-markdown-roundtrip.md` |
| **US-ONBOARDING** | Wizard Activation — Effet "Aha!" Recherche Sémantique | 🆕 **À FAIRE** | Story : `docs/story-onboarding-activation.md` |
| **US-BRAINSTORM-FINALIZE** | Brainstorm Canvas D3 — Finalisation (export PPTX, gaps UX) | 🆕 **À FAIRE** | ~75% code existant (`brainstorm-page.tsx`, 14 routes API) |
| **US-CHAT-PDF** | Chat with PDF — RAG documentaire | 🆕 **À FAIRE** | — |
---