From 6b4ed8514f1340f7bbbfbc82e01c47c8632d87ec Mon Sep 17 00:00:00 2001 From: Antigravity Date: Fri, 29 May 2026 11:24:56 +0000 Subject: [PATCH] Epic 6: Stories 6-2 (Markdown roundtrip) + 6-3 (Brainstorm PPTX + Canvas) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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> --- docs/6-3-brainstorm-canvas-finalize.md | 182 ++++++++++ docs/brief-markdown-roundtrip.md | 223 ++++++++++++ docs/sprint-status.yaml | 9 + docs/story-markdown-roundtrip.md | 239 +++++++++++++ docs/story-onboarding-activation.md | 315 +++++++++++++++++ docs/user-stories.md | 7 +- .../[sessionId]/export-pptx/route.ts | 65 ++++ .../api/onboarding/seed-demo-notes/route.ts | 300 ++++++++++++++++ memento-note/app/api/user/me/route.ts | 61 ++++ memento-note/auth.config.ts | 1 + memento-note/auth.ts | 11 +- .../components/brainstorm/brainstorm-page.tsx | 113 +++++- .../components/brainstorm/wave-canvas.tsx | 32 ++ .../note-editor/note-editor-toolbar.tsx | 81 ++++- .../onboarding/onboarding-editor-hints.tsx | 232 +++++++++++++ .../onboarding/onboarding-step-aha.tsx | 202 +++++++++++ .../onboarding/onboarding-step-features.tsx | 116 +++++++ .../onboarding/onboarding-step-notes.tsx | 203 +++++++++++ .../onboarding/onboarding-step-welcome.tsx | 58 ++++ .../onboarding/onboarding-wizard.tsx | 219 ++++++++++++ .../onboarding/starter-pack-badge.tsx | 59 ++++ memento-note/components/providers-wrapper.tsx | 4 + memento-note/components/rich-text-editor.tsx | 2 + memento-note/components/sidebar.tsx | 6 +- .../1698b7d6-de51-4d9e-8f2e-9492ef190abf.png | Bin 0 -> 182080 bytes memento-note/lib/brainstorm/export-pptx.ts | 322 ++++++++++++++++++ memento-note/lib/editor/markdown-export.ts | 209 ++++++++++++ .../lib/editor/markdown-paste-extension.ts | 50 +++ memento-note/locales/ar.json | 111 +++++- memento-note/locales/de.json | 111 +++++- memento-note/locales/en.json | 111 +++++- memento-note/locales/es.json | 111 +++++- memento-note/locales/fa.json | 111 +++++- memento-note/locales/fr.json | 109 +++++- memento-note/locales/hi.json | 111 +++++- memento-note/locales/it.json | 111 +++++- memento-note/locales/ja.json | 111 +++++- memento-note/locales/ko.json | 111 +++++- memento-note/locales/nl.json | 111 +++++- memento-note/locales/pl.json | 111 +++++- memento-note/locales/pt.json | 111 +++++- memento-note/locales/ru.json | 111 +++++- memento-note/locales/zh.json | 111 +++++- memento-note/package-lock.json | 53 +++ memento-note/package.json | 5 + .../migration.sql | 8 + memento-note/prisma/schema.prisma | 4 + .../tests/unit/markdown-export.test.ts | 218 ++++++++++++ memento-note/types/global.d.ts | 9 + 49 files changed, 5215 insertions(+), 66 deletions(-) create mode 100644 docs/6-3-brainstorm-canvas-finalize.md create mode 100644 docs/brief-markdown-roundtrip.md create mode 100644 docs/story-markdown-roundtrip.md create mode 100644 docs/story-onboarding-activation.md create mode 100644 memento-note/app/api/brainstorm/[sessionId]/export-pptx/route.ts create mode 100644 memento-note/app/api/onboarding/seed-demo-notes/route.ts create mode 100644 memento-note/app/api/user/me/route.ts create mode 100644 memento-note/components/onboarding/onboarding-editor-hints.tsx create mode 100644 memento-note/components/onboarding/onboarding-step-aha.tsx create mode 100644 memento-note/components/onboarding/onboarding-step-features.tsx create mode 100644 memento-note/components/onboarding/onboarding-step-notes.tsx create mode 100644 memento-note/components/onboarding/onboarding-step-welcome.tsx create mode 100644 memento-note/components/onboarding/onboarding-wizard.tsx create mode 100644 memento-note/components/onboarding/starter-pack-badge.tsx create mode 100644 memento-note/data/uploads/notes/1698b7d6-de51-4d9e-8f2e-9492ef190abf.png create mode 100644 memento-note/lib/brainstorm/export-pptx.ts create mode 100644 memento-note/lib/editor/markdown-export.ts create mode 100644 memento-note/lib/editor/markdown-paste-extension.ts create mode 100644 memento-note/prisma/migrations/20260529060000_add_onboarding_fields/migration.sql create mode 100644 memento-note/tests/unit/markdown-export.test.ts diff --git a/docs/6-3-brainstorm-canvas-finalize.md b/docs/6-3-brainstorm-canvas-finalize.md new file mode 100644 index 0000000..d1c75f2 --- /dev/null +++ b/docs/6-3-brainstorm-canvas-finalize.md @@ -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` + - 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 | diff --git a/docs/brief-markdown-roundtrip.md b/docs/brief-markdown-roundtrip.md new file mode 100644 index 0000000..0971083 --- /dev/null +++ b/docs/brief-markdown-roundtrip.md @@ -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 (2–5 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(``) + state.closeBlock(node) + }, + structuredViewBlock: (state, node) => { + state.write(``) + 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 `` 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** | diff --git a/docs/sprint-status.yaml b/docs/sprint-status.yaml index 1e03995..9afd826 100644 --- a/docs/sprint-status.yaml +++ b/docs/sprint-status.yaml @@ -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 + diff --git a/docs/story-markdown-roundtrip.md b/docs/story-markdown-roundtrip.md new file mode 100644 index 0000000..35e1882 --- /dev/null +++ b/docs/story-markdown-roundtrip.md @@ -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 `.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 `` +- [x] **AC-5** : Un `structuredViewBlock` est exporté en commentaire HTML `` + +--- + +### 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 `# ` 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 `` 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 `
` dans `getHTML()` +- `structuredViewBlock` génère `
` +- `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 `

MOMENTOBLOCKSNTINELXXX

`, post-process le MD résultant pour remplacer par ``. +- **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** : `` 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) | diff --git a/docs/story-onboarding-activation.md b/docs/story-onboarding-activation.md new file mode 100644 index 0000000..413d51c --- /dev/null +++ b/docs/story-onboarding-activation.md @@ -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 `` 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 `` conditionnel | +| `components/sidebar.tsx` | Modifier | Ajouter `` | +| `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 diff --git a/docs/user-stories.md b/docs/user-stories.md index 6a96b60..a075071 100644 --- a/docs/user-stories.md +++ b/docs/user-stories.md @@ -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** | — | --- diff --git a/memento-note/app/api/brainstorm/[sessionId]/export-pptx/route.ts b/memento-note/app/api/brainstorm/[sessionId]/export-pptx/route.ts new file mode 100644 index 0000000..ba62d07 --- /dev/null +++ b/memento-note/app/api/brainstorm/[sessionId]/export-pptx/route.ts @@ -0,0 +1,65 @@ +import { NextRequest, NextResponse } from 'next/server' +import prisma from '@/lib/prisma' +import { auth } from '@/auth' +import { generateBrainstormPptx } from '@/lib/brainstorm/export-pptx' + +export async function POST( + _request: NextRequest, + { params }: { params: Promise<{ sessionId: string }> } +) { + const session = await auth() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + try { + const { sessionId } = await params + + const brainstormSession = await prisma.brainstormSession.findFirst({ + where: { + id: sessionId, + OR: [ + { userId: session.user.id }, + { participants: { some: { userId: session.user.id } } }, + ], + }, + include: { + ideas: { + orderBy: [{ waveNumber: 'asc' }, { createdAt: 'asc' }], + }, + }, + }) + + if (!brainstormSession) { + return NextResponse.json({ error: 'Session not found' }, { status: 404 }) + } + + const sessionData = { + seedIdea: brainstormSession.seedIdea, + createdAt: brainstormSession.createdAt, + ideas: brainstormSession.ideas.map(idea => ({ + id: idea.id, + title: idea.title, + description: idea.description, + waveNumber: idea.waveNumber, + status: idea.status, + isStarred: idea.isStarred, + convertedToNoteId: idea.convertedToNoteId, + })), + } + + const { buffer, filename } = await generateBrainstormPptx(sessionData) + + return new NextResponse(new Uint8Array(buffer), { + status: 200, + headers: { + 'Content-Type': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'Content-Disposition': `attachment; filename="${filename}"`, + 'Content-Length': String(buffer.length), + }, + }) + } catch (err) { + console.error('[export-pptx]', err) + return NextResponse.json({ error: 'Export failed' }, { status: 500 }) + } +} diff --git a/memento-note/app/api/onboarding/seed-demo-notes/route.ts b/memento-note/app/api/onboarding/seed-demo-notes/route.ts new file mode 100644 index 0000000..1908a83 --- /dev/null +++ b/memento-note/app/api/onboarding/seed-demo-notes/route.ts @@ -0,0 +1,300 @@ +import { NextRequest, NextResponse } from 'next/server' +import { auth } from '@/auth' +import prisma from '@/lib/prisma' +import { embeddingService } from '@/lib/ai/services/embedding.service' +import { upsertNoteEmbedding } from '@/lib/embeddings' + +type DemoNote = { title: string; content: string } + +const DEMO_NOTES: Record = { + fr: [ + { + title: 'Réunion Q3 — Stratégie produit', + content: `

Réunion Q3 — Stratégie produit

+

Points clés abordés lors de la réunion trimestrielle :

+
    +
  • Roadmap : priorité aux features de monétisation (Stripe, BYOK) avant les agents autonomes
  • +
  • KPI Activation : objectif 40% d'utilisateurs actifs dans les 7 jours suivant l'inscription
  • +
  • Acquisition PLG : brainstorm partageable = viral loop principale
  • +
  • Pricing : Pro à 9,90 €/mois validé avec 3 personas cibles
  • +
+

Prochaine étape : finaliser l'onboarding wizard pour atteindre le taux d'activation cible.

`, + }, + { + title: 'Idées de projets secondaires', + content: `

Idées de projets secondaires

+

Liste de projets à explorer dans les prochains mois :

+
    +
  • 📱 App de suivi d'habitudes — gamification, streaks, rappels intelligents
  • +
  • 🎙️ Podcast "Tech Indépendant" — interviews de freelances tech, conseils pratiques
  • +
  • 🤖 Bot Telegram de veille — scraping RSS + résumé IA chaque matin
  • +
  • 📚 Newsletter hebdomadaire — curation de ressources pour développeurs
  • +
  • 🎮 Jeu de stratégie minimaliste — WebGL, logique pure, pas de monétisation
  • +
+

Critères de sélection : impact fort, temps minimal, apprentissage technique.

`, + }, + { + title: 'Livres à lire — Recommandations', + content: `

Livres à lire — Recommandations

+

Productivité & Pensée

+
    +
  • Building a Second Brain — Tiago Forte · Système de notes PKM, méthode CODE
  • +
  • Deep Work — Cal Newport · Concentration profonde dans un monde de distractions
  • +
  • Atomic Habits — James Clear · Les habitudes comme système de progression
  • +
+

Technique

+
    +
  • Designing Data-Intensive Applications — Martin Kleppmann · Architecture systèmes
  • +
  • The Pragmatic Programmer — Hunt & Thomas · Bonnes pratiques développement
  • +
+

Prochaine lecture : Building a Second Brain (lien avec Momento évident).

`, + }, + { + title: 'Notes de formation React — Hooks avancés', + content: `

Notes de formation React — Hooks avancés

+

useCallback vs useMemo

+
    +
  • useCallback : mémoïse une fonction, utile pour éviter re-renders des enfants
  • +
  • useMemo : mémoïse une valeur calculée, utile pour calculs coûteux
  • +
  • ⚠️ Ne pas abuser — le coût de la mémoïsation peut dépasser le gain
  • +
+

useReducer

+

Préférer useReducer à useState quand :

+
    +
  • L'état a plusieurs sous-valeurs liées
  • +
  • La prochaine valeur dépend de la précédente
  • +
  • La logique de transition est complexe
  • +
+

Pattern : Context + useReducer

+

Combine Context API avec useReducer pour un state management léger sans Redux.

`, + }, + { + title: 'Objectifs personnels 2025', + content: `

Objectifs personnels 2025

+

🎯 Professionnel

+
    +
  • Lancer un produit SaaS avec les premiers utilisateurs payants
  • +
  • Contribuer à un projet open source significatif
  • +
  • Maîtriser l'architecture de systèmes distribués
  • +
+

📚 Apprentissage

+
    +
  • Lire 12 livres techniques (1 par mois)
  • +
  • Compléter une formation en machine learning appliqué
  • +
  • Améliorer le niveau en algorithmique (LeetCode medium)
  • +
+

🌱 Personnel

+
    +
  • Sport : 3x par semaine minimum
  • +
  • Méditation : 10 minutes par jour (habitude matinale)
  • +
  • Voyager dans 2 nouveaux pays
  • +
+

Revue mensuelle le 1er de chaque mois pour ajuster les priorités.

`, + }, + ], + en: [ + { + title: 'Q3 Meeting — Product Strategy', + content: `

Q3 Meeting — Product Strategy

+

Key points from the quarterly review:

+
    +
  • Roadmap: monetization features first (Stripe, BYOK) before autonomous agents
  • +
  • Activation KPI: target 40% active users within 7 days of signup
  • +
  • PLG Acquisition: shareable brainstorm canvas = main viral loop
  • +
  • Pricing: Pro at $9.90/month validated with 3 target personas
  • +
+

Next step: finalize onboarding wizard to reach activation target.

`, + }, + { + title: 'Side Project Ideas', + content: `

Side Project Ideas

+

Projects to explore in the coming months:

+
    +
  • 📱 Habit tracking app — gamification, streaks, smart reminders
  • +
  • 🎙️ Tech podcast — interviews with indie developers, practical advice
  • +
  • 🤖 Telegram news bot — RSS scraping + AI summary every morning
  • +
  • 📚 Weekly newsletter — curated resources for developers
  • +
  • 🎮 Minimalist strategy game — WebGL, pure logic, no monetization
  • +
+

Selection criteria: high impact, minimal time, technical learning.

`, + }, + { + title: 'Books to Read — Recommendations', + content: `

Books to Read — Recommendations

+

Productivity & Thinking

+
    +
  • Building a Second Brain — Tiago Forte · PKM note-taking system, CODE method
  • +
  • Deep Work — Cal Newport · Deep focus in a distracted world
  • +
  • Atomic Habits — James Clear · Habits as a system for progress
  • +
+

Technical

+
    +
  • Designing Data-Intensive Applications — Martin Kleppmann · Systems architecture
  • +
  • The Pragmatic Programmer — Hunt & Thomas · Development best practices
  • +
+

Next read: Building a Second Brain (obvious connection to Momento).

`, + }, + { + title: 'React Training Notes — Advanced Hooks', + content: `

React Training Notes — Advanced Hooks

+

useCallback vs useMemo

+
    +
  • useCallback: memoizes a function, useful to prevent child re-renders
  • +
  • useMemo: memoizes a computed value, useful for expensive calculations
  • +
  • ⚠️ Don't overuse — memoization cost can exceed the gain
  • +
+

useReducer

+

Prefer useReducer over useState when:

+
    +
  • State has multiple related sub-values
  • +
  • Next state depends on previous state
  • +
  • Transition logic is complex
  • +
`, + }, + { + title: 'Personal Goals 2025', + content: `

Personal Goals 2025

+

🎯 Professional

+
    +
  • Launch a SaaS product with first paying users
  • +
  • Contribute to a significant open source project
  • +
  • Master distributed systems architecture
  • +
+

📚 Learning

+
    +
  • Read 12 technical books (1 per month)
  • +
  • Complete an applied machine learning course
  • +
  • Improve algorithmic skills (LeetCode medium)
  • +
+

🌱 Personal

+
    +
  • Exercise: 3x per week minimum
  • +
  • Meditation: 10 minutes per day (morning habit)
  • +
  • Travel to 2 new countries
  • +
`, + }, + ], + fa: [ + { + title: 'جلسه Q3 — استراتژی محصول', + content: `

جلسه Q3 — استراتژی محصول

+

نکات کلیدی جلسه فصلی:

+
    +
  • نقشه راه: اول ویژگی‌های پولی‌سازی (Stripe، BYOK) و سپس عامل‌های خودمختار
  • +
  • KPI فعال‌سازی: هدف ۴۰٪ کاربران فعال در ۷ روز اول
  • +
  • رشد محصول‌محور: اشتراک‌گذاری بوم طوفان فکری = حلقه ویروسی اصلی
  • +
+

قدم بعدی: نهایی کردن ویزارد آنبوردینگ برای رسیدن به هدف فعال‌سازی.

`, + }, + { + title: 'ایده‌های پروژه‌های جانبی', + content: `

ایده‌های پروژه‌های جانبی

+
    +
  • 📱 اپلیکیشن پیگیری عادت — گیمیفیکیشن، استریک، یادآوری هوشمند
  • +
  • 🎙️ پادکست فناوری — مصاحبه با توسعه‌دهندگان مستقل
  • +
  • 🤖 ربات تلگرام اخبار — خلاصه‌سازی روزانه با هوش مصنوعی
  • +
`, + }, + { + title: 'کتاب‌های پیشنهادی', + content: `

کتاب‌های پیشنهادی

+
    +
  • ساختن مغز دوم — تیاگو فورتی · سیستم یادداشت‌برداری PKM
  • +
  • کار عمیق — کال نیوپورت · تمرکز عمیق در دنیای پر از حواس‌پرتی
  • +
  • عادت‌های اتمی — جیمز کلیر · عادت‌ها به عنوان سیستم پیشرفت
  • +
`, + }, + { + title: 'یادداشت‌های آموزش React', + content: `

یادداشت‌های آموزش React

+

useCallback و useMemo

+
    +
  • useCallback: یک تابع را حفظ می‌کند
  • +
  • useMemo: یک مقدار محاسبه‌شده را حفظ می‌کند
  • +
`, + }, + { + title: 'اهداف شخصی ۲۰۲۵', + content: `

اهداف شخصی ۲۰۲۵

+

🎯 حرفه‌ای

+
    +
  • راه‌اندازی یک محصول SaaS با اولین کاربران پرداخت‌کننده
  • +
  • مشارکت در یک پروژه متن‌باز مهم
  • +
+

📚 یادگیری

+
    +
  • خواندن ۱۲ کتاب فناوری (یکی در ماه)
  • +
  • تکمیل یک دوره یادگیری ماشین کاربردی
  • +
`, + }, + ], +} + +function getNotesForLocale(locale: string): DemoNote[] { + const lang = locale.split('-')[0].toLowerCase() + return DEMO_NOTES[lang] ?? DEMO_NOTES.en +} + +/** Wraps a promise with a timeout — rejects after `ms` milliseconds. */ +function withTimeout(promise: Promise, ms: number): Promise { + const timeout = new Promise((_, reject) => + setTimeout(() => reject(new Error(`Operation timed out after ${ms}ms`)), ms) + ) + return Promise.race([promise, timeout]) +} + +export async function POST(req: NextRequest) { + const session = await auth() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const userId = session.user.id + let locale = 'en' + try { + const body = await req.json().catch(() => ({})) + if (body?.locale) locale = body.locale + } catch { /* ignore */ } + + // Idempotency check — if any demo notes already exist, return them (handles partial creation too) + const existing = await prisma.note.findMany({ + where: { userId, isDemo: true, trashedAt: null }, + select: { id: true, title: true }, + }) + if (existing.length > 0) { + return NextResponse.json({ created: false, notes: existing, message: 'Demo notes already exist' }) + } + + const demoNotes = getNotesForLocale(locale) + const created: { id: string; title: string | null }[] = [] + + for (const demo of demoNotes) { + const note = await prisma.note.create({ + data: { + userId, + title: demo.title, + content: demo.content, + isMarkdown: true, + isDemo: true, + type: 'richtext', + color: 'default', + }, + }) + created.push({ id: note.id, title: note.title }) + + // Synchronous embedding generation so semantic search works immediately (6s timeout per note) + try { + const { embedding } = await withTimeout( + embeddingService.generateNoteEmbedding(demo.title, demo.content), + 6000 + ) + if (embedding) { + await withTimeout(upsertNoteEmbedding(note.id, embedding), 3000) + } + } catch (e) { + console.error('[ONBOARDING] Embedding failed for demo note:', note.id, e) + } + } + + return NextResponse.json({ created: true, notes: created }) +} diff --git a/memento-note/app/api/user/me/route.ts b/memento-note/app/api/user/me/route.ts new file mode 100644 index 0000000..06fa533 --- /dev/null +++ b/memento-note/app/api/user/me/route.ts @@ -0,0 +1,61 @@ +import { NextRequest, NextResponse } from 'next/server' +import { auth } from '@/auth' +import prisma from '@/lib/prisma' + +/** + * GET /api/user/me + * Returns lightweight user profile including onboardingCompleted flag. + */ +export async function GET() { + const session = await auth() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { id: true, name: true, email: true, onboardingCompleted: true, onboardingStep: true }, + }) + if (!user) return NextResponse.json({ error: 'User not found' }, { status: 404 }) + return NextResponse.json(user) +} + +/** + * PATCH /api/user/me + * Partial update of user profile. Supported fields: onboardingCompleted, onboardingStep. + */ +export async function PATCH(req: NextRequest) { + const session = await auth() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + let body: Record + try { + body = await req.json() + } catch { + return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) + } + + const allowedFields = ['onboardingCompleted', 'onboardingStep'] as const + type AllowedField = typeof allowedFields[number] + const data: Partial> = {} + if ('onboardingCompleted' in body) { + data.onboardingCompleted = Boolean(body.onboardingCompleted) + } + if ('onboardingStep' in body) { + const val = parseInt(String(body.onboardingStep), 10) + if (Number.isInteger(val) && val >= 0 && val <= 10) { + data.onboardingStep = val + } + } + + if (Object.keys(data).length === 0) { + return NextResponse.json({ error: 'No valid fields to update' }, { status: 400 }) + } + + const updated = await prisma.user.update({ + where: { id: session.user.id }, + data: data as any, + select: { id: true, onboardingCompleted: true, onboardingStep: true }, + }) + return NextResponse.json(updated) +} diff --git a/memento-note/auth.config.ts b/memento-note/auth.config.ts index d74f42e..9621b25 100644 --- a/memento-note/auth.config.ts +++ b/memento-note/auth.config.ts @@ -67,6 +67,7 @@ export const authConfig = { (session.user as any).id = token.id; (session.user as any).role = token.role; session.aiSessionConsent = token.aiSessionConsent === true; + (session.user as any).onboardingCompleted = token.onboardingCompleted === true; } return session; }, diff --git a/memento-note/auth.ts b/memento-note/auth.ts index 4bf62e6..942f6d0 100644 --- a/memento-note/auth.ts +++ b/memento-note/auth.ts @@ -55,8 +55,9 @@ export const { auth, signIn, signOut, handlers } = NextAuth({ return true; }, async jwt({ token, user, trigger, session }) { - if (trigger === 'update' && session && 'aiSessionConsent' in session) { - token.aiSessionConsent = session.aiSessionConsent === true; + if (trigger === 'update' && session) { + if ('aiSessionConsent' in session) token.aiSessionConsent = session.aiSessionConsent === true; + if ('onboardingCompleted' in session) token.onboardingCompleted = session.onboardingCompleted === true; return token; } if (user?.id) { @@ -64,15 +65,16 @@ export const { auth, signIn, signOut, handlers } = NextAuth({ token.aiSessionConsent = false; const dbUser = await prisma.user.findUnique({ where: { id: user.id }, - select: { role: true, sessionVersion: true }, + select: { role: true, sessionVersion: true, onboardingCompleted: true }, }); if (!dbUser) return null; token.role = dbUser.role; token.sessionVersion = dbUser.sessionVersion; + token.onboardingCompleted = dbUser.onboardingCompleted; } else if (token.sub) { const dbUser = await prisma.user.findUnique({ where: { id: token.sub }, - select: { role: true, sessionVersion: true }, + select: { role: true, sessionVersion: true, onboardingCompleted: true }, }); if (!dbUser) return null; if ( @@ -84,6 +86,7 @@ export const { auth, signIn, signOut, handlers } = NextAuth({ token.id = token.sub; token.role = dbUser.role; token.sessionVersion = dbUser.sessionVersion; + token.onboardingCompleted = dbUser.onboardingCompleted; } return token; }, diff --git a/memento-note/components/brainstorm/brainstorm-page.tsx b/memento-note/components/brainstorm/brainstorm-page.tsx index 8be5093..1005a7e 100644 --- a/memento-note/components/brainstorm/brainstorm-page.tsx +++ b/memento-note/components/brainstorm/brainstorm-page.tsx @@ -94,12 +94,23 @@ export function BrainstormPage() { setActiveSessionId(urlSessionId) } }, [urlSessionId]) + const [showActivityFeed, setShowActivityFeed] = useState(false) const [showShareDialog, setShowShareDialog] = useState(false) const [manualEditCount, setManualEditCount] = useState(0) const [shareStatus, setShareStatus] = useState<'idle' | 'copying' | 'copied'>('idle') const { data: sessions, isLoading: sessionsLoading } = useBrainstormSessions() + + // Auto-sélectionner la dernière session si aucune session active + useEffect(() => { + if (!activeSessionId && !urlSessionId && !sessionsLoading && sessions && sessions.length > 0) { + const last = sessions[0] + setActiveSessionId(last.id) + router.replace('/brainstorm?session=' + last.id) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [sessions, sessionsLoading]) const { data: sessionResult, isLoading: sessionLoading } = useBrainstormSession(activeSessionId) const session = sessionResult?.session || null const sessionMeta = sessionResult?.meta @@ -128,6 +139,9 @@ export function BrainstormPage() { const [viewMode, setViewMode] = useState<'canvas' | 'list'>('canvas') const [summaryOpen, setSummaryOpen] = useState(false) const [summaryText, setSummaryText] = useState(null) + const [pptxLoading, setPptxLoading] = useState(false) + const [pptxError, setPptxError] = useState(null) + const [fitTrigger, setFitTrigger] = useState(0) const [renamingSession, setRenamingSession] = useState(null) const [renameInput, setRenameInput] = useState('') const canvasContainerRef = useRef(null) @@ -237,6 +251,36 @@ export function BrainstormPage() { } catch {} } + const handleExportPptx = async () => { + if (!activeSessionId) return + setPptxLoading(true) + setPptxError(null) + try { + const res = await fetch(`/api/brainstorm/${activeSessionId}/export-pptx`, { + method: 'POST', + credentials: 'include', + }) + if (!res.ok) throw new Error(t('brainstorm.pptxError') || 'Export PPTX failed') + const blob = await res.blob() + const disposition = res.headers.get('Content-Disposition') || '' + const match = disposition.match(/filename="([^"]+)"/) + const filename = match?.[1] || 'brainstorm.pptx' + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = filename + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + } catch (err: any) { + setPptxError(err?.message || t('brainstorm.pptxError') || 'Export PPTX failed') + setTimeout(() => setPptxError(null), 4000) + } finally { + setPptxLoading(false) + } + } + const handleExport = async () => { setExportError(null) try { @@ -577,6 +621,7 @@ export function BrainstormPage() { remoteMove={remoteMove} manualEditTrigger={manualEditCount} playbackIdeas={playbackIdeas} + fitTrigger={fitTrigger} /> ) : (
@@ -594,7 +639,7 @@ export function BrainstormPage() { animate={{ opacity: 1, y: 0 }} className="absolute bottom-6 left-6 flex gap-2" > -
+
{[1, 2, 3].map((w) => (
))} + {viewMode === 'canvas' && ( + <> +
+ + + )}
{canEdit && ( @@ -854,7 +913,10 @@ export function BrainstormPage() { {sessions?.map((s) => (
{activeSessionId === s.id && ( +
+ + +
@@ -1033,6 +1110,20 @@ export function BrainstormPage() { )} + + {pptxError && ( + + {pptxError} + + + )} + + {exportToast && ( = { @@ -31,6 +32,7 @@ export const WaveCanvas: React.FC = ({ remoteMove, manualEditTrigger, playbackIdeas, + fitTrigger, }) => { const { t, language } = useLanguage() const svgRef = useRef(null) @@ -39,6 +41,8 @@ export const WaveCanvas: React.FC = ({ const linkRef = useRef | null>(null) const simulationRef = useRef | null>(null) const transformRef = useRef(d3.zoomIdentity) + const zoomRef = useRef | null>(null) + const centerRef = useRef<{ x: number; y: number; scale: number }>({ x: 0, y: 0, scale: 0.8 }) const onNodeSelectRef = useRef(onNodeSelect) onNodeSelectRef.current = onNodeSelect @@ -167,6 +171,9 @@ export const WaveCanvas: React.FC = ({ transformRef.current = event.transform }) + zoomRef.current = zoom + centerRef.current = { x: centerX, y: centerY, scale: 0.8 } + svg.call(zoom) svg.call(zoom.transform, d3.zoomIdentity.translate(centerX, centerY).scale(0.8)) @@ -517,6 +524,16 @@ export const WaveCanvas: React.FC = ({ node.attr('transform', (d: any) => `translate(${d.x},${d.y})`) }, [remoteMove]) + // Fit-to-screen: re-center when fitTrigger increments + useEffect(() => { + if (!fitTrigger || !svgRef.current || !zoomRef.current) return + const { x, y, scale } = centerRef.current + d3.select(svgRef.current) + .transition() + .duration(500) + .call(zoomRef.current.transform, d3.zoomIdentity.translate(x, y).scale(scale)) + }, [fitTrigger]) + return (
= ({ > + {/* Legend overlay — bottom right */} +
+ {[ + { color: '#fb923c', label: t('brainstorm.legendWave1') || 'Variations' }, + { color: '#60a5fa', label: t('brainstorm.legendWave2') || 'Analogies' }, + { color: '#a78bfa', label: t('brainstorm.legendWave3') || 'Disruptions' }, + { color: '#10b981', label: t('brainstorm.legendConverted') || 'Convertie' }, + ].map(({ color, label }) => ( +
+
+ {label} +
+ ))} +
+ {editingNode && (
(null) const notebookName = notebooks.find(nb => nb.id === note.notebookId)?.name || null const undoSnapshotRef = useRef<{ content: string; isMarkdown: boolean } | null>(null) + // ── Markdown export ─────────────────────────────────────────────────────── + const handleExportMarkdown = () => { + try { + const editor = richTextEditorRef?.current?.getEditor() + if (!editor) { + toast.error(t('richTextEditor.markdownExportError')) + return + } + const html = editor.getHTML() + const title = state.title || note.title || 'note' + const titleLine = title ? `# ${title}\n\n` : '' + const markdown = titleLine + tiptapHTMLToMarkdown(html) + const blob = new Blob([markdown], { type: 'text/markdown;charset=utf-8' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `${title.replace(/[^a-z0-9\-_\s]/gi, '').trim().replace(/\s+/g, '-') || 'note'}.md` + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + toast.success(t('richTextEditor.markdownExportSuccess')) + } catch { + toast.error(t('richTextEditor.markdownExportError')) + } + } + + // ── Markdown import ─────────────────────────────────────────────────────── + const handleImportMarkdownFile = (e: React.ChangeEvent) => { + const file = e.target.files?.[0] + if (!file) return + const reader = new FileReader() + reader.onload = (ev) => { + try { + const md = ev.target?.result as string + const html = markdownToHTML(md) + const extractedTitle = extractMarkdownTitle(md) + const editor = richTextEditorRef?.current?.getEditor() + if (editor) { + editor.commands.setContent(html) + } + actions.setContent(html) + if (extractedTitle) actions.setTitle(extractedTitle) + toast.success(t('richTextEditor.markdownImportSuccess')) + } catch { + toast.error(t('richTextEditor.markdownExportError')) + } + } + reader.readAsText(file) + // Reset input so same file can be imported again + e.target.value = '' + } + const handleConvertToRichtext = async () => { if (isConverting || !state.content.trim()) return setIsConverting(true) @@ -240,7 +296,16 @@ export function NoteEditorToolbar({ mode, onClose, onToggleAttachments, attachme - + + + + {t('richTextEditor.exportMarkdown')} + + mdImportInputRef.current?.click()}> + + {t('richTextEditor.importMarkdown')} + + { try { @@ -304,6 +369,7 @@ export function NoteEditorToolbar({ mode, onClose, onToggleAttachments, attachme } return ( + <>
{!readOnly && ( @@ -432,5 +498,14 @@ export function NoteEditorToolbar({ mode, onClose, onToggleAttachments, attachme )}
+ {/* Hidden file input for Markdown import */} + + ) } diff --git a/memento-note/components/onboarding/onboarding-editor-hints.tsx b/memento-note/components/onboarding/onboarding-editor-hints.tsx new file mode 100644 index 0000000..4d6732c --- /dev/null +++ b/memento-note/components/onboarding/onboarding-editor-hints.tsx @@ -0,0 +1,232 @@ +'use client' + +import { useState, useEffect } from 'react' +import { motion, AnimatePresence } from 'motion/react' +import { useSession } from 'next-auth/react' +import { usePathname } from 'next/navigation' +import { + Slash, Sparkles, History, GraduationCap, Link2, + PenLine, FlipVertical, Keyboard, Lightbulb, ArrowRight, + FileDown, Network, Zap, RefreshCw, + X, ChevronLeft, ChevronRight, +} from 'lucide-react' +import { useLanguage } from '@/lib/i18n' +import type { LucideIcon } from 'lucide-react' + +// ── Types ────────────────────────────────────────────────────────────────── + +interface HintDef { + icon: LucideIcon + color: string + bg: string + key: string +} + +interface RouteHintSet { + hints: HintDef[] + storageKey: string +} + +// ── Hint definitions per route ───────────────────────────────────────────── +// Every item here maps to a real, verified UI element or gesture in the codebase. + +const ROUTE_HINTS: Record = { + '/home': { + storageKey: 'momento_hints_home', + hints: [ + { icon: PenLine, color: 'text-violet-500', bg: 'bg-violet-500/10', key: 'create_note' }, + { icon: Slash, color: 'text-indigo-500', bg: 'bg-indigo-500/10', key: 'slash' }, + { icon: Sparkles, color: 'text-amber-500', bg: 'bg-amber-500/10', key: 'ai' }, + { icon: History, color: 'text-blue-500', bg: 'bg-blue-500/10', key: 'version' }, + { icon: Link2, color: 'text-rose-500', bg: 'bg-rose-500/10', key: 'links' }, + { icon: GraduationCap, color: 'text-emerald-500',bg: 'bg-emerald-500/10',key: 'flashcards' }, + ], + }, + '/revision': { + storageKey: 'momento_hints_revision', + hints: [ + { icon: FlipVertical, color: 'text-violet-500', bg: 'bg-violet-500/10', key: 'flip' }, + { icon: Keyboard, color: 'text-blue-500', bg: 'bg-blue-500/10', key: 'rate_keys' }, + { icon: GraduationCap,color: 'text-emerald-500',bg: 'bg-emerald-500/10',key: 'generate_from_note' }, + ], + }, + '/brainstorm': { + storageKey: 'momento_hints_brainstorm', + hints: [ + { icon: Lightbulb, color: 'text-amber-500', bg: 'bg-amber-500/10', key: 'brainstorm_start' }, + { icon: ArrowRight, color: 'text-violet-500',bg: 'bg-violet-500/10', key: 'brainstorm_deepen' }, + { icon: FileDown, color: 'text-blue-500', bg: 'bg-blue-500/10', key: 'brainstorm_export' }, + ], + }, + '/insights': { + storageKey: 'momento_hints_insights', + hints: [ + { icon: Network, color: 'text-violet-500', bg: 'bg-violet-500/10', key: 'insights_clusters' }, + { icon: Zap, color: 'text-amber-500', bg: 'bg-amber-500/10', key: 'insights_bridge' }, + { icon: RefreshCw, color: 'text-blue-500', bg: 'bg-blue-500/10', key: 'insights_refresh' }, + ], + }, +} + +function getRouteSet(pathname: string): RouteHintSet | null { + // Exact match first + if (ROUTE_HINTS[pathname]) return ROUTE_HINTS[pathname] + // Prefix match (e.g. /brainstorm?session=xxx) + for (const route of Object.keys(ROUTE_HINTS)) { + if (pathname.startsWith(route)) return ROUTE_HINTS[route] + } + return null +} + +// ── Component ────────────────────────────────────────────────────────────── + +export function OnboardingEditorHints() { + const { data: session } = useSession() + const pathname = usePathname() + const { t } = useLanguage() + + const [visible, setVisible] = useState(false) + const [index, setIndex] = useState(0) + const [routeKey, setRouteKey] = useState(null) + + const user = session?.user as any + + // When route changes, check if we should show hints for the new page + useEffect(() => { + if (!session) return + if (user?.onboardingCompleted !== false) return + + const routeSet = getRouteSet(pathname) + if (!routeSet) { + setVisible(false) + return + } + + if (typeof window !== 'undefined' && localStorage.getItem(routeSet.storageKey)) { + setVisible(false) + return + } + + // Reset to first hint when page changes + setIndex(0) + setRouteKey(pathname) + + const timer = setTimeout(() => setVisible(true), 900) + return () => clearTimeout(timer) + }, [pathname, session, user?.onboardingCompleted]) + + // Hide when hints change (route switch) + useEffect(() => { + if (routeKey !== null && routeKey !== pathname) { + setVisible(false) + } + }, [pathname, routeKey]) + + function dismiss() { + const routeSet = getRouteSet(pathname) + if (routeSet && typeof window !== 'undefined') { + localStorage.setItem(routeSet.storageKey, '1') + } + setVisible(false) + } + + const routeSet = getRouteSet(pathname) + if (!routeSet) return null + + const hints = routeSet.hints + const hint = hints[Math.min(index, hints.length - 1)] + const Icon = hint.icon + + return ( + + {visible && ( + + {/* Header */} +
+

+ {t('onboarding.editor_hints_title')} +

+ +
+ + {/* Hint content */} + + + + + +
+

+ {t(`onboarding.hint_${hint.key}_title`)} +

+

+ {t(`onboarding.hint_${hint.key}_desc`)} +

+
+
+
+ + {/* Navigation */} +
+ {/* Dots */} +
+ {hints.map((_, i) => ( +
+ {/* Arrows + dismiss */} +
+ + {index < hints.length - 1 ? ( + + ) : ( + + )} +
+
+
+ )} +
+ ) +} diff --git a/memento-note/components/onboarding/onboarding-step-aha.tsx b/memento-note/components/onboarding/onboarding-step-aha.tsx new file mode 100644 index 0000000..22b4918 --- /dev/null +++ b/memento-note/components/onboarding/onboarding-step-aha.tsx @@ -0,0 +1,202 @@ +'use client' + +import { useState, useEffect, useRef } from 'react' +import { motion, AnimatePresence } from 'motion/react' +import { Search, Sparkles, ArrowRight } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { useLanguage } from '@/lib/i18n' +import { semanticSearch } from '@/app/actions/semantic-search' +import confetti from 'canvas-confetti' + +interface SearchResult { id: string; title: string | null; snippet?: string } + +interface Props { + onDone: () => void + locale: string + autoSearch?: boolean +} + +const SEARCH_PREFILL: Record = { + fr: 'notes sur ma productivité', + en: 'notes about productivity', + fa: 'یادداشت‌های بهره‌وری', + ar: 'ملاحظات حول الإنتاجية', + de: 'Notizen zur Produktivität', + es: 'notas sobre productividad', + it: 'note sulla produttività', + pt: 'notas sobre produtividade', + ru: 'заметки о продуктивности', + zh: '关于生产力的笔记', + ja: '生産性に関するメモ', + ko: '생산성에 관한 노트', + nl: 'notities over productiviteit', + pl: 'notatki o produktywności', + hi: 'उत्पादकता पर नोट्स', +} + +function fireConfetti() { + const end = Date.now() + 800 + const colors = ['#7c3aed', '#a78bfa', '#fbbf24', '#34d399'] + const frame = () => { + confetti({ particleCount: 3, angle: 60, spread: 55, origin: { x: 0 }, colors }) + confetti({ particleCount: 3, angle: 120, spread: 55, origin: { x: 1 }, colors }) + if (Date.now() < end) requestAnimationFrame(frame) + } + frame() +} + +export function OnboardingStepAha({ onDone, locale, autoSearch = false }: Props) { + const { t } = useLanguage() + const lang = locale.split('-')[0].toLowerCase() + const prefill = SEARCH_PREFILL[lang] ?? SEARCH_PREFILL.en + + const [query, setQuery] = useState(prefill) + const [results, setResults] = useState(null) + const [loading, setLoading] = useState(false) + const [searched, setSearched] = useState(false) + const [quotaExceeded, setQuotaExceeded] = useState(false) + const autoSearched = useRef(false) + + async function handleSearch() { + const q = query.trim() + if (!q) return + setLoading(true) + setQuotaExceeded(false) + try { + const res = await semanticSearch(q, { limit: 5 }) + const hits = res.results.map(r => ({ id: r.noteId, title: r.title, snippet: r.content?.slice(0, 80) })) + setResults(hits) + setSearched(true) + if (hits.length > 0) { + setTimeout(fireConfetti, 200) + } + } catch (err: unknown) { + const msg = err instanceof Error ? err.message.toLowerCase() : '' + if (msg.includes('quota') || msg.includes('limit') || msg.includes('exceeded') || msg.includes('upgrade')) { + setQuotaExceeded(true) + } + setResults([]) + setSearched(true) + } finally { + setLoading(false) + } + } + + useEffect(() => { + if (!autoSearch || autoSearched.current) return + autoSearched.current = true + const timer = setTimeout(() => { void handleSearch() }, 800) + return () => clearTimeout(timer) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [autoSearch]) + + return ( + + + + + +
+

+ {t('onboarding.step_aha_title')} +

+

+ {t('onboarding.step_aha_subtitle')} +

+
+ + + setQuery(e.target.value)} + onKeyDown={e => e.key === 'Enter' && handleSearch()} + className="flex-1 px-4 py-3 text-sm bg-transparent outline-none placeholder:text-muted-foreground" + dir={['fa', 'ar'].includes(lang) ? 'rtl' : 'ltr'} + aria-label={t('onboarding.step_aha_search_aria')} + /> + + + + + {searched && results !== null && ( + + {quotaExceeded ? ( +

+ {t('onboarding.quota_exceeded')} +

+ ) : results.length === 0 ? ( +

+ {t('onboarding.no_results')} +

+ ) : ( + <> + {results.slice(0, 3).map((r, i) => ( + +

{r.title ?? 'Untitled'}

+ {r.snippet && ( +

{r.snippet}

+ )} +
+ ))} + + + {t('onboarding.search_credit_used')} + + + )} +
+ )} +
+ + +
+ ) +} diff --git a/memento-note/components/onboarding/onboarding-step-features.tsx b/memento-note/components/onboarding/onboarding-step-features.tsx new file mode 100644 index 0000000..a32f22d --- /dev/null +++ b/memento-note/components/onboarding/onboarding-step-features.tsx @@ -0,0 +1,116 @@ +'use client' + +import { useState } from 'react' +import { motion, AnimatePresence } from 'motion/react' +import { Pencil, CreditCard, Lightbulb, Check, ArrowRight, Sparkles } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { useLanguage } from '@/lib/i18n' + +interface Props { + onDone: () => void + onTry: (href: string) => void +} + +const ACTIONS = [ + { icon: Pencil, color: 'text-violet-500', bg: 'bg-violet-500/10', key: 'write', href: '/home' }, + { icon: CreditCard, color: 'text-blue-500', bg: 'bg-blue-500/10', key: 'flashcards', href: '/revision' }, + { icon: Lightbulb, color: 'text-amber-500', bg: 'bg-amber-500/10', key: 'brainstorm', href: '/brainstorm' }, +] + +export function OnboardingStepFeatures({ onDone, onTry }: Props) { + const { t } = useLanguage() + const [checked, setChecked] = useState>(new Set()) + + function handleTry(key: string, href: string) { + setChecked(prev => new Set([...prev, key])) + onTry(href) + } + + const allChecked = checked.size === ACTIONS.length + + return ( + + + + + +
+

+ {t('onboarding.step_features_title')} +

+

+ {t('onboarding.step_features_subtitle')} +

+
+ +
+ {ACTIONS.map(({ icon: Icon, color, bg, key, href }, i) => { + const done = checked.has(key) + return ( + + {/* Checkmark */} + + + {done && ( + + + + )} + + + + {/* Icon */} + + + + + {/* Text */} +
+

+ {t(`onboarding.action_${key}_title`)} +

+

+ {t(`onboarding.action_${key}_desc`)} +

+
+ + {/* Essayer — minimise wizard + navigue */} + +
+ ) + })} +
+ + +
+ ) +} diff --git a/memento-note/components/onboarding/onboarding-step-notes.tsx b/memento-note/components/onboarding/onboarding-step-notes.tsx new file mode 100644 index 0000000..7e8b303 --- /dev/null +++ b/memento-note/components/onboarding/onboarding-step-notes.tsx @@ -0,0 +1,203 @@ +'use client' + +import { useRef, useState } from 'react' +import { motion } from 'motion/react' +import { FileText, Upload, Sparkles, CheckCircle, AlertCircle } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { useLanguage } from '@/lib/i18n' +import { markdownToHTML, extractMarkdownTitle } from '@/lib/editor/markdown-export' + +interface Props { + noteCount: number + onNext: (justCreated: boolean) => void + onSkip: () => void + locale: string +} + +async function createNote(title: string | null, content: string) { + const res = await fetch('/api/notes', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ title: title ?? 'Untitled', content }), + }) + if (!res.ok) throw new Error('Failed to create note') +} + +export function OnboardingStepNotes({ noteCount, onNext, onSkip, locale }: Props) { + const { t } = useLanguage() + const [loading, setLoading] = useState(false) + const [created, setCreated] = useState(false) + const [importError, setImportError] = useState(null) + const [importedCount, setImportedCount] = useState(0) + const fileInputRef = useRef(null) + + async function handleCreateDemo() { + setLoading(true) + try { + await fetch('/api/onboarding/seed-demo-notes', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ locale }), + }) + setCreated(true) + setTimeout(() => onNext(true), 1200) + } finally { + setLoading(false) + } + } + + async function handleImportFiles(e: React.ChangeEvent) { + const files = Array.from(e.target.files ?? []) + if (!files.length) return + setLoading(true) + setImportError(null) + let count = 0 + try { + for (const file of files) { + const text = await file.text() + const isMd = file.name.endsWith('.md') + const title = extractMarkdownTitle(text) ?? file.name.replace(/\.(md|txt)$/, '') + const content = isMd ? markdownToHTML(text) : `

${text.replace(/\n\n+/g, '

').replace(/\n/g, '
')}

` + await createNote(title, content) + count++ + } + setImportedCount(count) + setCreated(true) + setTimeout(() => onNext(false), 1200) + } catch { + setImportError(t('onboarding.import_error')) + } finally { + setLoading(false) + if (fileInputRef.current) fileInputRef.current.value = '' + } + } + + const hasNotes = noteCount > 0 + + return ( + + + + + +
+

+ {t('onboarding.step_notes_title')} +

+

+ {hasNotes + ? t('onboarding.step_notes_has_notes', { count: noteCount }) + : t('onboarding.step_notes_empty')} +

+ {!hasNotes && ( +

+ {t('onboarding.step_notes_hint')} +

+ )} +
+ + {hasNotes ? ( +
+ + +
+ ) : ( +
+ {created ? ( + + + + {importedCount > 0 + ? t('onboarding.import_notes_ready', { count: importedCount }) + : t('onboarding.demo_notes_ready')} + + + ) : ( + <> + + + {/* Hidden file input — accepts .md and .txt */} + + +

+ {t('onboarding.import_formats')} +

+ + )} + + {importError && ( + + + {importError} + + )} + + +
+ )} +
+ ) +} diff --git a/memento-note/components/onboarding/onboarding-step-welcome.tsx b/memento-note/components/onboarding/onboarding-step-welcome.tsx new file mode 100644 index 0000000..56629eb --- /dev/null +++ b/memento-note/components/onboarding/onboarding-step-welcome.tsx @@ -0,0 +1,58 @@ +'use client' + +import { motion } from 'motion/react' +import { Brain } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { useLanguage } from '@/lib/i18n' + +interface Props { + onNext: () => void + onSkip: () => void + userName?: string | null +} + +export function OnboardingStepWelcome({ onNext, onSkip, userName }: Props) { + const { t } = useLanguage() + const firstName = userName?.split(' ')[0] ?? null + + return ( + + + + + +
+

+ {firstName + ? t('onboarding.welcome_title_name', { name: firstName }) + : t('onboarding.welcome_title')} +

+

+ {t('onboarding.welcome_subtitle')} +

+
+ +
+ + +
+
+ ) +} diff --git a/memento-note/components/onboarding/onboarding-wizard.tsx b/memento-note/components/onboarding/onboarding-wizard.tsx new file mode 100644 index 0000000..183b63b --- /dev/null +++ b/memento-note/components/onboarding/onboarding-wizard.tsx @@ -0,0 +1,219 @@ +'use client' + +import { useState, useEffect, useCallback } from 'react' +import { motion, AnimatePresence } from 'motion/react' +import { useSession } from 'next-auth/react' +import { useRouter } from 'next/navigation' +import { Sparkles, X } from 'lucide-react' +import { cn } from '@/lib/utils' +import { useLanguage } from '@/lib/i18n' +import { OnboardingStepWelcome } from './onboarding-step-welcome' +import { OnboardingStepNotes } from './onboarding-step-notes' +import { OnboardingStepAha } from './onboarding-step-aha' +import { OnboardingStepFeatures } from './onboarding-step-features' + +const TOTAL_STEPS = 4 + +async function markOnboardingComplete() { + const res = await fetch('/api/user/me', { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ onboardingCompleted: true }), + }) + if (!res.ok) throw new Error('Failed to save onboarding state') +} + +async function getNoteCount(): Promise { + try { + const res = await fetch('/api/notes?limit=1') + if (!res.ok) return 0 + const data = await res.json() + if (data?.data && Array.isArray(data.data)) return data.data.length + if (Array.isArray(data)) return data.length + return 0 + } catch { + return 0 + } +} + +export function OnboardingWizard() { + const { data: session, update: updateSession } = useSession() + const { language, t } = useLanguage() + const router = useRouter() + + const [step, setStep] = useState(1) + const [visible, setVisible] = useState(false) + const [minimized, setMinimized] = useState(false) + const [noteCount, setNoteCount] = useState(0) + const [demoNotesJustCreated, setDemoNotesJustCreated] = useState(false) + const [triedCount, setTriedCount] = useState(0) + + const user = session?.user as any + + useEffect(() => { + if (!session) return + if (user?.onboardingCompleted === false) { + setVisible(true) + getNoteCount().then(setNoteCount) + } + }, [session, user?.onboardingCompleted]) + + const handleSkip = useCallback(async () => { + setVisible(false) + setMinimized(false) + try { + await markOnboardingComplete() + await updateSession({ onboardingCompleted: true }) + } catch (e) { + console.error('[Onboarding] skip failed:', e) + } + }, [updateSession]) + + const handleNext = useCallback(() => { + setStep(s => Math.min(s + 1, TOTAL_STEPS)) + }, []) + + const handleNotesNext = useCallback((justCreated: boolean) => { + setDemoNotesJustCreated(justCreated) + setStep(s => Math.min(s + 1, TOTAL_STEPS)) + }, []) + + const handleDone = useCallback(async () => { + setVisible(false) + setMinimized(false) + try { + await markOnboardingComplete() + await updateSession({ onboardingCompleted: true }) + } catch (e) { + console.error('[Onboarding] done failed:', e) + } + }, [updateSession]) + + // Called from step 4 when user clicks "Essayer" on a feature + const handleTry = useCallback((href: string) => { + setMinimized(true) + setTriedCount(c => c + 1) + router.push(href) + }, [router]) + + if (!visible) return null + + return ( + <> + {/* Minimized pill — always rendered when minimized so it shows on any page */} + + {minimized && ( + setMinimized(false)} + className="fixed bottom-6 right-6 z-[300] flex items-center gap-2.5 rounded-full bg-violet-600 text-white shadow-lg shadow-violet-500/30 px-4 py-3 text-sm font-medium hover:bg-violet-700 transition-colors" + > + + {t('onboarding.pill_resume')} + {triedCount > 0 && ( + + {triedCount} + + )} + + )} + + + {/* Full wizard modal */} + + {!minimized && ( + + + {/* Progress */} +
+
+ {Array.from({ length: TOTAL_STEPS }).map((_, i) => ( + + ))} +
+

+ {t('onboarding.progress', { current: step, total: TOTAL_STEPS })} +

+
+ + {/* Skip / close button (steps 1-3 only, step 4 has its own CTA) */} + {step < 4 && ( + + )} + + {/* Step content */} + + {step === 1 && ( + + )} + {step === 2 && ( + + )} + {step === 3 && ( + + )} + {step === 4 && ( + + )} + +
+
+ )} +
+ + ) +} diff --git a/memento-note/components/onboarding/starter-pack-badge.tsx b/memento-note/components/onboarding/starter-pack-badge.tsx new file mode 100644 index 0000000..cb7afb7 --- /dev/null +++ b/memento-note/components/onboarding/starter-pack-badge.tsx @@ -0,0 +1,59 @@ +'use client' + +import { useQuery } from '@tanstack/react-query' +import { Zap, ArrowUpRight } from 'lucide-react' +import { cn } from '@/lib/utils' +import { useLanguage } from '@/lib/i18n' +import Link from 'next/link' + +interface QuotaData { remaining: number; limit: number; used: number } +interface UsageData { quotas: Record; tier: string } + +export function StarterPackBadge() { + const { t } = useLanguage() + + const { data } = useQuery({ + queryKey: ['usage', 'current'], + queryFn: async () => { + const res = await fetch('/api/usage/current') + if (!res.ok) throw new Error('Failed') + return res.json() + }, + staleTime: 5000, + }) + + if (!data) return null + // Paid tiers do not need the badge; only BASIC (free) users see it + const PAID_TIERS = ['PRO', 'BUSINESS', 'ENTERPRISE'] + if (PAID_TIERS.includes(data.tier)) return null + + const semanticQuota = data.quotas?.['semantic_search'] + if (!semanticQuota) return null + + const remaining = semanticQuota.remaining ?? 0 + const isCritical = remaining === 0 + const isWarning = remaining > 0 && remaining < 5 + + return ( + + + + {isCritical + ? t('onboarding.badge_upgrade') + : t('onboarding.badge_credits', { count: remaining }) + } + + {isCritical && } + + ) +} diff --git a/memento-note/components/providers-wrapper.tsx b/memento-note/components/providers-wrapper.tsx index 215462a..04b9cca 100644 --- a/memento-note/components/providers-wrapper.tsx +++ b/memento-note/components/providers-wrapper.tsx @@ -9,6 +9,8 @@ import type { ReactNode } from 'react' import type { Translations } from '@/lib/i18n/load-translations' import { AiConsentProvider } from '@/components/legal/ai-consent-provider' import { SearchModalProvider } from '@/context/search-modal-context' +import { OnboardingWizard } from '@/components/onboarding/onboarding-wizard' +import { OnboardingEditorHints } from '@/components/onboarding/onboarding-editor-hints' const RTL_LANGUAGES = ['ar', 'fa'] @@ -42,6 +44,8 @@ export function ProvidersWrapper({ {children} + + diff --git a/memento-note/components/rich-text-editor.tsx b/memento-note/components/rich-text-editor.tsx index a6b1a29..ff4f5b5 100644 --- a/memento-note/components/rich-text-editor.tsx +++ b/memento-note/components/rich-text-editor.tsx @@ -41,6 +41,7 @@ import { parseBlockReferenceFromText, recallLastBlockReference, type ParsedBlock import { getEmptyParagraphAtSelection } from '@/lib/editor/empty-paragraph-at-selection' import { SmartPasteExtension } from '@/lib/editor/smart-paste-extension' import { BlockSelectionExtension } from '@/lib/editor/block-selection-extension' +import { MarkdownPasteExtension } from '@/lib/editor/markdown-paste-extension' import { TurnIntoShortcutExtension } from '@/lib/editor/turn-into-shortcut-extension' import { UndoRedoFeedbackExtension } from '@/lib/editor/undo-redo-feedback-extension' import type { Node as PMNode } from '@tiptap/pm/model' @@ -417,6 +418,7 @@ export const RichTextEditor = forwardRef
- {/* ── Usage meter en bas du panneau ── */} -
+ {/* ── Usage meter + starter badge en bas du panneau ── */} +
+
diff --git a/memento-note/data/uploads/notes/1698b7d6-de51-4d9e-8f2e-9492ef190abf.png b/memento-note/data/uploads/notes/1698b7d6-de51-4d9e-8f2e-9492ef190abf.png new file mode 100644 index 0000000000000000000000000000000000000000..aef58ad9c8cb651957df667c4eb5faba27ba6e3a GIT binary patch literal 182080 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

oEs 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

+ let result = html.replace( + /]*?data-live-block[^>]*?)>\s*<\/div>/gi, + (_match, attrs) => { + const snId = (attrs.match(/sourcenoteid="([^"]*)"/i) || attrs.match(/sourcenoteid='([^']*)'/i) || [])[1] || '' + const bId = (attrs.match(/blockid="([^"]*)"/i) || attrs.match(/blockid='([^']*)'/i) || [])[1] || '' + const key = `${SENTINEL_PREFIX}LIVEBLOCK${placeholders.length}` + placeholders.push({ key, comment: `` }) + return `

${key}

` + } + ) + + // structuredViewBlock:
+ result = result.replace( + /]*?data-structured-view-block[^>]*?)>\s*<\/div>/gi, + (_match, attrs) => { + const attrMap: Record = {} + const attrRe = /(data-[a-z-]+)="([^"]*)"/gi + let m: RegExpExecArray | null + while ((m = attrRe.exec(attrs)) !== null) { + if (m[1] !== 'data-structured-view-block') attrMap[m[1]] = m[2] + } + const key = `${SENTINEL_PREFIX}SVBLOCK${placeholders.length}` + placeholders.push({ key, comment: `` }) + return `

${key}

` + } + ) + + return { html: result, placeholders } +} + +/** + * Post-process the markdown output: replace sentinel placeholders with HTML comments. + */ +function postprocessPlaceholders(md: string, placeholders: BlockPlaceholder[]): string { + let result = md + for (const { key, comment } of placeholders) { + result = result.replace(key, `\n\n${comment}\n\n`) + } + return result +} + +// ── HTML → Markdown ────────────────────────────────────────────────────────── + +/** + * Convert a TipTap-generated HTML string to GitHub-Flavored Markdown. + * Custom nodes (liveBlock, structuredViewBlock) are serialised as HTML comments. + */ +export function tiptapHTMLToMarkdown(html: string): string { + if (!html || html.trim() === '') return '' + const { html: preprocessed, placeholders } = preprocessCustomNodes(html) + const td = getTurndownService() + const md = td.turndown(preprocessed).trim() + return postprocessPlaceholders(md, placeholders).trim() +} + +// ── Markdown → HTML ────────────────────────────────────────────────────────── + +/** + * Convert a Markdown string to HTML suitable for injection into TipTap via + * `editor.commands.setContent(html)`. + * + * Uses marked with GFM enabled (tables, task lists, line breaks). + */ +export function markdownToHTML(markdown: string): string { + if (!markdown || markdown.trim() === '') return '' + + // marked v18+ uses synchronous parse by default when no async tokens + const html = marked.parse(markdown, { + gfm: true, + breaks: false, + }) as string + + return html +} + +// ── Title extraction from Markdown ────────────────────────────────────────── + +/** + * Extract the first H1 title from a Markdown string. + * Returns null if no H1 is found. + */ +export function extractMarkdownTitle(markdown: string): string | null { + const match = markdown.match(/^#\s+(.+)/m) + return match ? match[1].trim() : null +} diff --git a/memento-note/lib/editor/markdown-paste-extension.ts b/memento-note/lib/editor/markdown-paste-extension.ts new file mode 100644 index 0000000..4fa8d47 --- /dev/null +++ b/memento-note/lib/editor/markdown-paste-extension.ts @@ -0,0 +1,50 @@ +/** + * markdown-paste-extension.ts + * + * TipTap extension that intercepts paste events. If the pasted plain text + * looks like Markdown, it converts it to HTML and inserts it into the editor + * as structured TipTap nodes — instead of inserting it as raw text. + */ + +import { Extension } from '@tiptap/core' +import { Plugin, PluginKey } from '@tiptap/pm/state' +import { looksLikeMarkdown, markdownToHTML } from './markdown-export' + +const MARKDOWN_PASTE_KEY = new PluginKey('markdownPaste') + +export const MarkdownPasteExtension = Extension.create({ + name: 'markdownPaste', + + addProseMirrorPlugins() { + const editor = this.editor + + return [ + new Plugin({ + key: MARKDOWN_PASTE_KEY, + props: { + handlePaste(_view, event) { + const text = event.clipboardData?.getData('text/plain') + if (!text || !looksLikeMarkdown(text)) return false + + event.preventDefault() + + try { + const html = markdownToHTML(text) + // Schedule after current event loop to avoid transaction conflicts + setTimeout(() => { + editor.commands.insertContent(html, { + parseOptions: { preserveWhitespace: 'full' }, + }) + }, 0) + } catch { + // Fallback: let TipTap handle the paste normally + return false + } + + return true + }, + }, + }), + ] + }, +}) diff --git a/memento-note/locales/ar.json b/memento-note/locales/ar.json index 6ae2f30..9d794de 100644 --- a/memento-note/locales/ar.json +++ b/memento-note/locales/ar.json @@ -2176,7 +2176,12 @@ "Italiano": "الإيطالية", "Chinois": "الصينية", "Japonais": "اليابانية" - } + }, + "exportMarkdown": "تصدير كـ Markdown", + "importMarkdown": "استيراد Markdown", + "markdownExportSuccess": "تم تصدير الملاحظة كـ Markdown", + "markdownExportError": "فشل تصدير الملاحظة", + "markdownImportSuccess": "تم استيراد Markdown بنجاح" }, "brainstorm": { "title": "Waves of Thought", @@ -2306,7 +2311,16 @@ "ownerBadge": "Owner", "waveBadge": "Wave {wave}", "quotaGuest": "استنفد مضيف الجلسة حدّ الذكاء الاصطناعي. اطلب منه ترقية خطته.", - "quotaHost": "لقد وصلت إلى حدّ الذكاء الاصطناعي لهذه الجلسة. رقِّ خطتك للمتابعة." + "quotaHost": "لقد وصلت إلى حدّ الذكاء الاصطناعي لهذه الجلسة. رقِّ خطتك للمتابعة.", + "downloadPptx": "PPTX", + "downloadPptxDesc": "تنزيل كـ PowerPoint", + "pptxSuccess": "تم تنزيل PPTX", + "pptxError": "فشل تصدير PPTX", + "fitToScreen": "إعادة التمركز", + "legendWave1": "التنويعات", + "legendWave2": "التشابهات", + "legendWave3": "الاضطرابات", + "legendConverted": "محوّل" }, "usageMeter": { "packName": "AI Discovery Pack", @@ -2762,5 +2776,96 @@ "uploadError": "خطأ في الرفع", "uploadFailed": "فشل الرفع", "uploading": "جاري الرفع..." + }, + "onboarding": { + "welcome_title": "ذاكرتك المعززة بالذكاء الاصطناعي", + "welcome_subtitle": "Momento يتذكر ما تنساه.", + "welcome_cta": "ابدأ", + "skip": "تخطي", + "step_notes_title": "ملاحظاتك", + "step_notes_empty": "ليس لديك ملاحظات بعد. استورد ملاحظاتك أو ابدأ بأمثلة.", + "step_notes_import": "استيراد ملاحظاتي", + "step_notes_demo": "إنشاء 5 ملاحظات تجريبية", + "step_notes_has_notes": "لديك بالفعل {count} ملاحظة. دعنا نكتشف السحر.", + "step_notes_cta": "ملاحظاتي جاهزة", + "step_aha_title": "اعثر على ما نسيته", + "step_aha_subtitle": "اطرح سؤالاً. اعثر على ملاحظة نسيتها.", + "step_aha_placeholder": "ملاحظات حول الإنتاجية...", + "step_aha_cta": "استكشف Momento", + "progress": "{current} من {total}", + "creating_demo_notes": "جارٍ إنشاء الملاحظات التجريبية...", + "demo_notes_ready": "تم إنشاء 5 ملاحظات تجريبية!", + "badge_credits": "⚡ {count} رصيد متبقٍ", + "badge_upgrade": "الترقية إلى Pro →", + "no_results": "لا نتائج — جرّب استعلامًا آخر.", + "search_credit_used": "تم استخدام بحث واحد", + "quota_exceeded": "تم استنفاد حصة البحث — انتقل إلى Pro.", + "step_aha_search_button": "بحث", + "step_aha_search_aria": "ابحث في ملاحظاتك", + "step_notes_hint": "💡 ستُغذِّي هذه الملاحظات عرض البحث بالذكاء الاصطناعي في الخطوة التالية.", + "step_features_title": "قدراتك الخارقة بالذكاء الاصطناعي", + "step_features_subtitle": "اختر من أين تبدأ.", + "step_features_cta": "لنبدأ!", + "feature_search_title": "البحث الدلالي", + "feature_search_desc": "ابحث عن أي ملاحظة بالمعنى، ليس فقط بالكلمات المفتاحية.", + "feature_flashcards_title": "بطاقات الذكاء الاصطناعي", + "feature_flashcards_desc": "أنشئ بطاقات مراجعة SRS من ملاحظاتك بنقرة واحدة.", + "feature_brainstorm_title": "العصف الذهني بالذكاء الاصطناعي", + "feature_brainstorm_desc": "جلسات عصف ذهني تعاوني مدعومة بالذكاء الاصطناعي.", + "feature_chat_title": "الدردشة مع ملاحظاتك", + "feature_chat_desc": "اطرح أسئلة على قاعدة معرفتك الشخصية.", + "feature_insights_title": "رؤى دلالية", + "feature_insights_desc": "اكتشف الروابط الخفية بين أفكارك.", + "feature_export_title": "تصدير Markdown", + "feature_export_desc": "استورد وصدِّر ملاحظاتك بتنسيق Markdown القياسي.", + "welcome_title_name": "مرحباً {name} 👋", + "import_formats": "الصيغ المقبولة: .md, .txt", + "import_error": "تعذر استيراد بعض الملفات. يرجى المحاولة مرة أخرى.", + "import_notes_ready": "تم استيراد {count} ملاحظة!", + "action_write_title": "اكتب ملاحظتك الأولى الحقيقية", + "action_write_desc": "أنشئ ملاحظة وابدأ في تدوين أفكارك.", + "action_flashcards_title": "أنشئ أول بطاقاتك التعليمية", + "action_flashcards_desc": "افتح ملاحظة وانقر على زر البطاقات.", + "action_brainstorm_title": "ابدأ عصفاً ذهنياً بالذكاء الاصطناعي", + "action_brainstorm_desc": "استكشف أفكارك مع وكيل الذكاء الاصطناعي.", + "action_try": "جرّب", + "step_features_cta_all": "كل شيء جاهز — لنبدأ!", + "action_write_where": "أغلق → انقر على \"+ ملاحظة جديدة\" في الشريط الجانبي", + "action_flashcards_where": "أغلق → افتح ملاحظة → زر 🃏 في شريط الأدوات", + "action_brainstorm_where": "أغلق → قسم \"Canvas\" في الشريط الجانبي", + "pill_resume": "✨ استئناف الجولة", + "action_done": "تم!", + "editor_hints_title": "Editor tips", + "editor_hints_got_it": "Got it!", + "hint_slash_title": "\"/\" command — insert blocks", + "hint_slash_desc": "In the editor, type \"/\" to open the block menu: heading, list, code block, table, to-do list, and AI commands (Clarify, Shorten, Improve, Expand).", + "hint_ai_title": "Built-in AI assistant", + "hint_ai_desc": "Click the ✨ button in the toolbar to open the AI panel — ask questions, summarize, rewrite, or brainstorm directly in your note.", + "hint_version_title": "Version history", + "hint_version_desc": "Click the ⓘ button in the toolbar → \"Versions\" tab. Enable versioning, then save and restore snapshots of your note at any time.", + "hint_flashcards_title": "Generate flashcards", + "hint_flashcards_desc": "Click the 🎓 button in the toolbar to auto-generate flashcards from your note for spaced repetition review.", + "hint_links_title": "Links between notes", + "hint_links_desc": "Type \"[[\" in the editor to search and link to another note. Linked notes appear as backlinks at the bottom of the note.", + "hint_create_note_title": "Create a note", + "hint_create_note_desc": "Click the \"+\" button in the sidebar or press Ctrl+N to create a new note. Then start writing.", + "hint_flip_title": "Flip the card", + "hint_flip_desc": "Press Space (or click the card) to flip it and reveal the answer.", + "hint_rate_keys_title": "Rate with keyboard", + "hint_rate_keys_desc": "After flipping, press 1 (Hard), 2 (Difficult), 3 (Good) or 4 (Easy) to rate the card. The SM-2 algorithm schedules your next review automatically.", + "hint_generate_from_note_title": "Generate from a note", + "hint_generate_from_note_desc": "Open any note and click the 🎓 button in the toolbar to automatically generate flashcards from its content.", + "hint_brainstorm_start_title": "Start with an idea", + "hint_brainstorm_start_desc": "Type any concept or question in the input field and press Enter. The AI will generate a set of ideas around it.", + "hint_brainstorm_deepen_title": "Deepen an idea", + "hint_brainstorm_deepen_desc": "Click on any idea card to expand it with sub-ideas and explore it further.", + "hint_brainstorm_export_title": "Export your session", + "hint_brainstorm_export_desc": "When done, export the entire brainstorm session as a structured note saved to your notebook.", + "hint_insights_clusters_title": "Note clusters", + "hint_insights_clusters_desc": "Your notes are automatically grouped into thematic clusters. Click a cluster to explore the notes it contains.", + "hint_insights_bridge_title": "Bridge notes", + "hint_insights_bridge_desc": "Bridge notes connect multiple clusters. They are highlighted because they hold your knowledge graph together.", + "hint_insights_refresh_title": "Refresh clusters", + "hint_insights_refresh_desc": "If you've added new notes, click the refresh button to recalculate the clusters with the latest content." } -} +} \ No newline at end of file diff --git a/memento-note/locales/de.json b/memento-note/locales/de.json index eae8eb2..a5a7ce8 100644 --- a/memento-note/locales/de.json +++ b/memento-note/locales/de.json @@ -2176,7 +2176,12 @@ "Italiano": "Italienisch", "Chinois": "Chinesisch", "Japonais": "Japanisch" - } + }, + "exportMarkdown": "Als Markdown exportieren", + "importMarkdown": "Markdown importieren", + "markdownExportSuccess": "Notiz als Markdown exportiert", + "markdownExportError": "Export der Notiz fehlgeschlagen", + "markdownImportSuccess": "Markdown erfolgreich importiert" }, "brainstorm": { "title": "Waves of Thought", @@ -2306,7 +2311,16 @@ "ownerBadge": "Owner", "waveBadge": "Wave {wave}", "quotaGuest": "Der Gastgeber der Sitzung hat sein KI-Kontingent aufgebraucht. Bitte ihn, seinen Tarif zu erweitern.", - "quotaHost": "Sie haben Ihr KI-Kontingent für dieses Brainstorming erreicht. Wechseln Sie den Tarif, um fortzufahren." + "quotaHost": "Sie haben Ihr KI-Kontingent für dieses Brainstorming erreicht. Wechseln Sie den Tarif, um fortzufahren.", + "downloadPptx": "PPTX", + "downloadPptxDesc": "Als PowerPoint herunterladen", + "pptxSuccess": "PPTX heruntergeladen", + "pptxError": "PPTX-Export fehlgeschlagen", + "fitToScreen": "Zentrieren", + "legendWave1": "Variationen", + "legendWave2": "Analogien", + "legendWave3": "Unterbrechungen", + "legendConverted": "Konvertiert" }, "usageMeter": { "packName": "AI Discovery Pack", @@ -2762,5 +2776,96 @@ "uploadError": "Upload-Fehler", "uploadFailed": "Upload fehlgeschlagen", "uploading": "Wird hochgeladen..." + }, + "onboarding": { + "welcome_title": "Ihr KI-erweitertes Gedächtnis", + "welcome_subtitle": "Momento erinnert sich an das, was Sie vergessen.", + "welcome_cta": "Loslegen", + "skip": "Überspringen", + "step_notes_title": "Ihre Notizen", + "step_notes_empty": "Sie haben noch keine Notizen. Importieren Sie Ihre oder beginnen Sie mit Beispielen.", + "step_notes_import": "Notizen importieren", + "step_notes_demo": "5 Beispielnotizen erstellen", + "step_notes_has_notes": "Sie haben bereits {count} Notizen. Entdecken wir die Magie.", + "step_notes_cta": "Meine Notizen sind bereit", + "step_aha_title": "Finden Sie, was Sie vergessen haben", + "step_aha_subtitle": "Stellen Sie eine Frage. Finden Sie eine vergessene Notiz.", + "step_aha_placeholder": "Notizen zur Produktivität...", + "step_aha_cta": "Momento erkunden", + "progress": "{current} von {total}", + "creating_demo_notes": "Beispielnotizen werden erstellt...", + "demo_notes_ready": "5 Beispielnotizen erstellt!", + "badge_credits": "⚡ Noch {count} Credits", + "badge_upgrade": "Auf Pro upgraden →", + "no_results": "Keine Ergebnisse — andere Anfrage versuchen.", + "search_credit_used": "1 Suche verwendet", + "quota_exceeded": "Suchlimit erreicht — auf Pro upgraden für unbegrenzte Suchen.", + "step_aha_search_button": "Suchen", + "step_aha_search_aria": "Notizen durchsuchen", + "step_notes_hint": "💡 Diese Notizen ermöglichen die KI-Suchdemo im nächsten Schritt.", + "step_features_title": "Ihre KI-Superkräfte", + "step_features_subtitle": "Wählen Sie Ihren Einstieg.", + "step_features_cta": "Los geht's!", + "feature_search_title": "Semantische Suche", + "feature_search_desc": "Finden Sie jede Notiz nach Bedeutung, nicht nur nach Schlüsselwörtern.", + "feature_flashcards_title": "KI-Karteikarten", + "feature_flashcards_desc": "SRS-Lernkarten aus Ihren Notizen in einem Klick erstellen.", + "feature_brainstorm_title": "KI-Brainstorming", + "feature_brainstorm_desc": "KI-gestützte kollaborative Brainstorming-Sitzungen.", + "feature_chat_title": "Mit Ihren Notizen chatten", + "feature_chat_desc": "Stellen Sie Ihrer persönlichen Wissensdatenbank Fragen.", + "feature_insights_title": "Semantische Einblicke", + "feature_insights_desc": "Entdecken Sie versteckte Verbindungen zwischen Ihren Ideen.", + "feature_export_title": "Markdown-Export", + "feature_export_desc": "Importieren und exportieren Sie Ihre Notizen im Markdown-Format.", + "welcome_title_name": "Hallo {name} 👋", + "import_formats": "Akzeptierte Formate: .md, .txt", + "import_error": "Einige Dateien konnten nicht importiert werden. Bitte erneut versuchen.", + "import_notes_ready": "{count} Notiz(en) importiert!", + "action_write_title": "Ihre erste echte Notiz schreiben", + "action_write_desc": "Erstellen Sie eine Notiz und beginnen Sie Ideen festzuhalten.", + "action_flashcards_title": "Erste Karteikarten erstellen", + "action_flashcards_desc": "Öffnen Sie eine Notiz und klicken Sie auf Karteikarten.", + "action_brainstorm_title": "KI-Brainstorming starten", + "action_brainstorm_desc": "Erkunden Sie Ihre Ideen mit einem KI-Agenten.", + "action_try": "Ausprobieren", + "step_features_cta_all": "Alles bereit — los geht's!", + "action_write_where": "Schließen → \"+ Neue Notiz\" in der Seitenleiste klicken", + "action_flashcards_where": "Schließen → Notiz öffnen → 🃏-Button in der Toolbar", + "action_brainstorm_where": "Schließen → \"Canvas\"-Bereich in der Seitenleiste", + "pill_resume": "✨ Tour fortsetzen", + "action_done": "Getestet!", + "editor_hints_title": "Editor tips", + "editor_hints_got_it": "Got it!", + "hint_slash_title": "\"/\" command — insert blocks", + "hint_slash_desc": "In the editor, type \"/\" to open the block menu: heading, list, code block, table, to-do list, and AI commands (Clarify, Shorten, Improve, Expand).", + "hint_ai_title": "Built-in AI assistant", + "hint_ai_desc": "Click the ✨ button in the toolbar to open the AI panel — ask questions, summarize, rewrite, or brainstorm directly in your note.", + "hint_version_title": "Version history", + "hint_version_desc": "Click the ⓘ button in the toolbar → \"Versions\" tab. Enable versioning, then save and restore snapshots of your note at any time.", + "hint_flashcards_title": "Generate flashcards", + "hint_flashcards_desc": "Click the 🎓 button in the toolbar to auto-generate flashcards from your note for spaced repetition review.", + "hint_links_title": "Links between notes", + "hint_links_desc": "Type \"[[\" in the editor to search and link to another note. Linked notes appear as backlinks at the bottom of the note.", + "hint_create_note_title": "Create a note", + "hint_create_note_desc": "Click the \"+\" button in the sidebar or press Ctrl+N to create a new note. Then start writing.", + "hint_flip_title": "Flip the card", + "hint_flip_desc": "Press Space (or click the card) to flip it and reveal the answer.", + "hint_rate_keys_title": "Rate with keyboard", + "hint_rate_keys_desc": "After flipping, press 1 (Hard), 2 (Difficult), 3 (Good) or 4 (Easy) to rate the card. The SM-2 algorithm schedules your next review automatically.", + "hint_generate_from_note_title": "Generate from a note", + "hint_generate_from_note_desc": "Open any note and click the 🎓 button in the toolbar to automatically generate flashcards from its content.", + "hint_brainstorm_start_title": "Start with an idea", + "hint_brainstorm_start_desc": "Type any concept or question in the input field and press Enter. The AI will generate a set of ideas around it.", + "hint_brainstorm_deepen_title": "Deepen an idea", + "hint_brainstorm_deepen_desc": "Click on any idea card to expand it with sub-ideas and explore it further.", + "hint_brainstorm_export_title": "Export your session", + "hint_brainstorm_export_desc": "When done, export the entire brainstorm session as a structured note saved to your notebook.", + "hint_insights_clusters_title": "Note clusters", + "hint_insights_clusters_desc": "Your notes are automatically grouped into thematic clusters. Click a cluster to explore the notes it contains.", + "hint_insights_bridge_title": "Bridge notes", + "hint_insights_bridge_desc": "Bridge notes connect multiple clusters. They are highlighted because they hold your knowledge graph together.", + "hint_insights_refresh_title": "Refresh clusters", + "hint_insights_refresh_desc": "If you've added new notes, click the refresh button to recalculate the clusters with the latest content." } -} +} \ No newline at end of file diff --git a/memento-note/locales/en.json b/memento-note/locales/en.json index aceb1b3..58fc15c 100644 --- a/memento-note/locales/en.json +++ b/memento-note/locales/en.json @@ -2429,7 +2429,12 @@ "Italiano": "Italian", "Chinois": "Chinese", "Japonais": "Japanese" - } + }, + "exportMarkdown": "Export as Markdown", + "importMarkdown": "Import Markdown", + "markdownExportSuccess": "Note exported as Markdown", + "markdownExportError": "Failed to export note", + "markdownImportSuccess": "Markdown imported successfully" }, "flashcards": { "generateTitle": "Generate flashcards", @@ -2495,7 +2500,7 @@ "retentionRate": "Retention rate", "masteredLabel": "{count}/{total} mastered", "retentionCurve": "Weekly success rate", - "retentionCurveHint": "Based on reviews with grade \u2265 Good (3 or 4)", + "retentionCurveHint": "Based on reviews with grade ≥ Good (3 or 4)", "retentionNoDataYet": "Review more cards across multiple weeks to see your retention curve.", "streak": "Current streak", "streakDays": "days", @@ -2792,7 +2797,16 @@ "ideasCount": "ideas", "star": "Star idea", "unstar": "Unstar idea", - "renameSession": "Rename session" + "renameSession": "Rename session", + "downloadPptx": "PPTX", + "downloadPptxDesc": "Download as PowerPoint", + "pptxSuccess": "PPTX downloaded", + "pptxError": "PPTX export failed", + "fitToScreen": "Re-center", + "legendWave1": "Variations", + "legendWave2": "Analogies", + "legendWave3": "Disruptions", + "legendConverted": "Converted" }, "byokSettings": { "title": "Your API keys (BYOK)", @@ -3461,5 +3475,96 @@ "notesLoadError": "Error loading notes", "defaultOption1": "Option 1", "defaultOption2": "Option 2" + }, + "onboarding": { + "welcome_title": "Your AI-augmented memory", + "welcome_subtitle": "Momento remembers what you forget.", + "welcome_cta": "Get started", + "skip": "Skip", + "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}", + "creating_demo_notes": "Creating example notes...", + "demo_notes_ready": "5 example notes created!", + "badge_credits": "⚡ {count} credits left", + "badge_upgrade": "Upgrade to Pro →", + "no_results": "No results — try another query.", + "search_credit_used": "1 search used", + "quota_exceeded": "Search quota reached — upgrade to Pro for unlimited.", + "step_aha_search_button": "Search", + "step_aha_search_aria": "Search your notes", + "step_notes_hint": "💡 These notes will power the AI search demo in the next step.", + "step_features_title": "Your AI superpowers", + "step_features_subtitle": "Choose where to start.", + "step_features_cta": "Let's go!", + "feature_search_title": "Semantic search", + "feature_search_desc": "Find any note by meaning, not just keywords.", + "feature_flashcards_title": "AI Flashcards", + "feature_flashcards_desc": "Generate SRS review cards from your notes in one click.", + "feature_brainstorm_title": "AI Brainstorm", + "feature_brainstorm_desc": "AI-powered collaborative brainstorming sessions.", + "feature_chat_title": "Chat with your notes", + "feature_chat_desc": "Ask questions to your personal knowledge base.", + "feature_insights_title": "Semantic insights", + "feature_insights_desc": "Discover hidden connections between your ideas.", + "feature_export_title": "Markdown export", + "feature_export_desc": "Import and export your notes in standard Markdown format.", + "welcome_title_name": "Hello {name} 👋", + "import_formats": "Accepted formats: .md, .txt", + "import_error": "Could not import some files. Please try again.", + "import_notes_ready": "{count} note(s) imported!", + "action_write_title": "Write your first real note", + "action_write_desc": "Create a note and start capturing your ideas.", + "action_flashcards_title": "Generate your first flashcards", + "action_flashcards_desc": "Open a note and click the flashcards button.", + "action_brainstorm_title": "Start an AI brainstorm", + "action_brainstorm_desc": "Explore your ideas with a dedicated AI agent.", + "action_try": "Try", + "step_features_cta_all": "All done — let's dive in!", + "action_write_where": "Close this → click \"+ New note\" in the sidebar", + "action_flashcards_where": "Close this → open a note → 🃏 button in the toolbar", + "action_brainstorm_where": "Close this → \"Canvas\" section in the sidebar", + "pill_resume": "✨ Resume tour", + "action_done": "Tried!", + "editor_hints_title": "Editor tips", + "editor_hints_got_it": "Got it!", + "hint_slash_title": "\"/\" command — insert blocks", + "hint_slash_desc": "In the editor, type \"/\" to open the block menu: heading, list, code block, table, to-do list, and AI commands (Clarify, Shorten, Improve, Expand).", + "hint_ai_title": "Built-in AI assistant", + "hint_ai_desc": "Click the ✨ button in the toolbar to open the AI panel — ask questions, summarize, rewrite, or brainstorm directly in your note.", + "hint_version_title": "Version history", + "hint_version_desc": "Click the ⓘ button in the toolbar → \"Versions\" tab. Enable versioning, then save and restore snapshots of your note at any time.", + "hint_flashcards_title": "Generate flashcards", + "hint_flashcards_desc": "Click the 🎓 button in the toolbar to auto-generate flashcards from your note for spaced repetition review.", + "hint_links_title": "Links between notes", + "hint_links_desc": "Type \"[[\" in the editor to search and link to another note. Linked notes appear as backlinks at the bottom of the note.", + "hint_create_note_title": "Create a note", + "hint_create_note_desc": "Click the \"+\" button in the sidebar or press Ctrl+N to create a new note. Then start writing.", + "hint_flip_title": "Flip the card", + "hint_flip_desc": "Press Space (or click the card) to flip it and reveal the answer.", + "hint_rate_keys_title": "Rate with keyboard", + "hint_rate_keys_desc": "After flipping, press 1 (Hard), 2 (Difficult), 3 (Good) or 4 (Easy) to rate the card. The SM-2 algorithm schedules your next review automatically.", + "hint_generate_from_note_title": "Generate from a note", + "hint_generate_from_note_desc": "Open any note and click the 🎓 button in the toolbar to automatically generate flashcards from its content.", + "hint_brainstorm_start_title": "Start with an idea", + "hint_brainstorm_start_desc": "Type any concept or question in the input field and press Enter. The AI will generate a set of ideas around it.", + "hint_brainstorm_deepen_title": "Deepen an idea", + "hint_brainstorm_deepen_desc": "Click on any idea card to expand it with sub-ideas and explore it further.", + "hint_brainstorm_export_title": "Export your session", + "hint_brainstorm_export_desc": "When done, export the entire brainstorm session as a structured note saved to your notebook.", + "hint_insights_clusters_title": "Note clusters", + "hint_insights_clusters_desc": "Your notes are automatically grouped into thematic clusters. Click a cluster to explore the notes it contains.", + "hint_insights_bridge_title": "Bridge notes", + "hint_insights_bridge_desc": "Bridge notes connect multiple clusters. They are highlighted because they hold your knowledge graph together.", + "hint_insights_refresh_title": "Refresh clusters", + "hint_insights_refresh_desc": "If you've added new notes, click the refresh button to recalculate the clusters with the latest content." } } \ No newline at end of file diff --git a/memento-note/locales/es.json b/memento-note/locales/es.json index cba1790..2d4d21c 100644 --- a/memento-note/locales/es.json +++ b/memento-note/locales/es.json @@ -2176,7 +2176,12 @@ "Italiano": "Italiano", "Chinois": "Chino", "Japonais": "Japonés" - } + }, + "exportMarkdown": "Exportar como Markdown", + "importMarkdown": "Importar Markdown", + "markdownExportSuccess": "Nota exportada como Markdown", + "markdownExportError": "Error al exportar la nota", + "markdownImportSuccess": "Markdown importado con éxito" }, "brainstorm": { "title": "Waves of Thought", @@ -2306,7 +2311,16 @@ "ownerBadge": "Owner", "waveBadge": "Wave {wave}", "quotaGuest": "El anfitrión de la sesión ha alcanzado su límite de IA. Pídele que mejore su plan.", - "quotaHost": "Has alcanzado tu límite de IA para este brainstorm. Mejora tu plan para continuar." + "quotaHost": "Has alcanzado tu límite de IA para este brainstorm. Mejora tu plan para continuar.", + "downloadPptx": "PPTX", + "downloadPptxDesc": "Descargar como PowerPoint", + "pptxSuccess": "PPTX descargado", + "pptxError": "Error al exportar PPTX", + "fitToScreen": "Recentrar", + "legendWave1": "Variaciones", + "legendWave2": "Analogías", + "legendWave3": "Disruptivas", + "legendConverted": "Convertida" }, "usageMeter": { "packName": "AI Discovery Pack", @@ -2762,5 +2776,96 @@ "uploadError": "Error al subir", "uploadFailed": "Error al subir", "uploading": "Subiendo..." + }, + "onboarding": { + "welcome_title": "Tu memoria aumentada por IA", + "welcome_subtitle": "Momento recuerda lo que olvidas.", + "welcome_cta": "Empezar", + "skip": "Omitir", + "step_notes_title": "Tus notas", + "step_notes_empty": "Aún no tienes notas. Importa las tuyas o empieza con ejemplos.", + "step_notes_import": "Importar mis notas", + "step_notes_demo": "Crear 5 notas de ejemplo", + "step_notes_has_notes": "Ya tienes {count} notas. ¡Descubramos la magia!", + "step_notes_cta": "Mis notas están listas", + "step_aha_title": "Encuentra lo que olvidaste", + "step_aha_subtitle": "Haz una pregunta. Encuentra una nota olvidada.", + "step_aha_placeholder": "notas sobre productividad...", + "step_aha_cta": "Explorar Momento", + "progress": "{current} de {total}", + "creating_demo_notes": "Creando notas de ejemplo...", + "demo_notes_ready": "¡5 notas de ejemplo creadas!", + "badge_credits": "⚡ {count} créditos restantes", + "badge_upgrade": "Mejorar a Pro →", + "no_results": "Sin resultados — intenta otra búsqueda.", + "search_credit_used": "1 búsqueda utilizada", + "quota_exceeded": "Cuota de búsqueda alcanzada — actualiza a Pro.", + "step_aha_search_button": "Buscar", + "step_aha_search_aria": "Buscar en tus notas", + "step_notes_hint": "💡 Estas notas alimentarán la demo de búsqueda IA en el siguiente paso.", + "step_features_title": "Tus superpoderes de IA", + "step_features_subtitle": "Elige por dónde empezar.", + "step_features_cta": "¡Vamos!", + "feature_search_title": "Búsqueda semántica", + "feature_search_desc": "Encuentra cualquier nota por significado, no solo por palabras clave.", + "feature_flashcards_title": "Tarjetas IA", + "feature_flashcards_desc": "Genera tarjetas de repaso SRS desde tus notas con un clic.", + "feature_brainstorm_title": "Brainstorming IA", + "feature_brainstorm_desc": "Sesiones de lluvia de ideas colaborativas con IA.", + "feature_chat_title": "Chatea con tus notas", + "feature_chat_desc": "Haz preguntas a tu base de conocimiento personal.", + "feature_insights_title": "Perspectivas semánticas", + "feature_insights_desc": "Descubre conexiones ocultas entre tus ideas.", + "feature_export_title": "Exportación Markdown", + "feature_export_desc": "Importa y exporta tus notas en formato Markdown estándar.", + "welcome_title_name": "¡Hola {name} 👋", + "import_formats": "Formatos aceptados: .md, .txt", + "import_error": "No se pudieron importar algunos archivos. Inténtalo de nuevo.", + "import_notes_ready": "¡{count} nota(s) importada(s)!", + "action_write_title": "Escribe tu primera nota real", + "action_write_desc": "Crea una nota y empieza a capturar tus ideas.", + "action_flashcards_title": "Genera tus primeras tarjetas", + "action_flashcards_desc": "Abre una nota y haz clic en el botón de tarjetas.", + "action_brainstorm_title": "Inicia un brainstorming IA", + "action_brainstorm_desc": "Explora tus ideas con un agente IA.", + "action_try": "Probar", + "step_features_cta_all": "¡Todo listo — ¡a sumergirnos!", + "action_write_where": "Cierra esto → haz clic en \"+ Nueva nota\" en la barra lateral", + "action_flashcards_where": "Cierra esto → abre una nota → botón 🃏 en la barra", + "action_brainstorm_where": "Cierra esto → sección \"Canvas\" en la barra lateral", + "pill_resume": "✨ Retomar visita", + "action_done": "¡Probado!", + "editor_hints_title": "Editor tips", + "editor_hints_got_it": "Got it!", + "hint_slash_title": "\"/\" command — insert blocks", + "hint_slash_desc": "In the editor, type \"/\" to open the block menu: heading, list, code block, table, to-do list, and AI commands (Clarify, Shorten, Improve, Expand).", + "hint_ai_title": "Built-in AI assistant", + "hint_ai_desc": "Click the ✨ button in the toolbar to open the AI panel — ask questions, summarize, rewrite, or brainstorm directly in your note.", + "hint_version_title": "Version history", + "hint_version_desc": "Click the ⓘ button in the toolbar → \"Versions\" tab. Enable versioning, then save and restore snapshots of your note at any time.", + "hint_flashcards_title": "Generate flashcards", + "hint_flashcards_desc": "Click the 🎓 button in the toolbar to auto-generate flashcards from your note for spaced repetition review.", + "hint_links_title": "Links between notes", + "hint_links_desc": "Type \"[[\" in the editor to search and link to another note. Linked notes appear as backlinks at the bottom of the note.", + "hint_create_note_title": "Create a note", + "hint_create_note_desc": "Click the \"+\" button in the sidebar or press Ctrl+N to create a new note. Then start writing.", + "hint_flip_title": "Flip the card", + "hint_flip_desc": "Press Space (or click the card) to flip it and reveal the answer.", + "hint_rate_keys_title": "Rate with keyboard", + "hint_rate_keys_desc": "After flipping, press 1 (Hard), 2 (Difficult), 3 (Good) or 4 (Easy) to rate the card. The SM-2 algorithm schedules your next review automatically.", + "hint_generate_from_note_title": "Generate from a note", + "hint_generate_from_note_desc": "Open any note and click the 🎓 button in the toolbar to automatically generate flashcards from its content.", + "hint_brainstorm_start_title": "Start with an idea", + "hint_brainstorm_start_desc": "Type any concept or question in the input field and press Enter. The AI will generate a set of ideas around it.", + "hint_brainstorm_deepen_title": "Deepen an idea", + "hint_brainstorm_deepen_desc": "Click on any idea card to expand it with sub-ideas and explore it further.", + "hint_brainstorm_export_title": "Export your session", + "hint_brainstorm_export_desc": "When done, export the entire brainstorm session as a structured note saved to your notebook.", + "hint_insights_clusters_title": "Note clusters", + "hint_insights_clusters_desc": "Your notes are automatically grouped into thematic clusters. Click a cluster to explore the notes it contains.", + "hint_insights_bridge_title": "Bridge notes", + "hint_insights_bridge_desc": "Bridge notes connect multiple clusters. They are highlighted because they hold your knowledge graph together.", + "hint_insights_refresh_title": "Refresh clusters", + "hint_insights_refresh_desc": "If you've added new notes, click the refresh button to recalculate the clusters with the latest content." } -} +} \ No newline at end of file diff --git a/memento-note/locales/fa.json b/memento-note/locales/fa.json index 6139b85..2883a1a 100644 --- a/memento-note/locales/fa.json +++ b/memento-note/locales/fa.json @@ -2211,7 +2211,12 @@ "Italiano": "ایتالیایی", "Chinois": "چینی", "Japonais": "ژاپنی" - } + }, + "exportMarkdown": "صدور به فرمت Markdown", + "importMarkdown": "وارد کردن Markdown", + "markdownExportSuccess": "یادداشت به فرمت Markdown صادر شد", + "markdownExportError": "خطا در صدور یادداشت", + "markdownImportSuccess": "Markdown با موفقیت وارد شد" }, "brainstorm": { "title": "Waves of Thought", @@ -2341,7 +2346,16 @@ "ownerBadge": "Owner", "waveBadge": "Wave {wave}", "quotaGuest": "میزبان جلسه به سقف هوش مصنوعی رسیده. از او بخواهید طرحش را ارتقا دهد.", - "quotaHost": "به سقف هوش مصنوعی این طوفان فکری رسیدید. برای ادامه، طرح خود را ارتقا دهید." + "quotaHost": "به سقف هوش مصنوعی این طوفان فکری رسیدید. برای ادامه، طرح خود را ارتقا دهید.", + "downloadPptx": "PPTX", + "downloadPptxDesc": "دانلود به‌صورت PowerPoint", + "pptxSuccess": "PPTX دانلود شد", + "pptxError": "خطا در خروجی PPTX", + "fitToScreen": "بازمرکزیابی", + "legendWave1": "تغییرات", + "legendWave2": "قیاس‌ها", + "legendWave3": "اختلالات", + "legendConverted": "تبدیل‌شده" }, "usageMeter": { "packName": "AI Discovery Pack", @@ -2801,5 +2815,96 @@ "uploadError": "خطای آپلود", "uploadFailed": "آپلود ناموفق", "uploading": "در حال آپلود..." + }, + "onboarding": { + "welcome_title": "حافظه شما با هوش مصنوعی", + "welcome_subtitle": "Momento آنچه فراموش می‌کنید را به یاد می‌آورد.", + "welcome_cta": "شروع", + "skip": "رد کردن", + "step_notes_title": "یادداشت‌های شما", + "step_notes_empty": "هنوز یادداشتی ندارید. یادداشت‌هایتان را وارد کنید یا با مثال‌ها شروع کنید.", + "step_notes_import": "وارد کردن یادداشت‌هایم", + "step_notes_demo": "ایجاد ۵ یادداشت نمونه", + "step_notes_has_notes": "شما از قبل {count} یادداشت دارید. بیایید جادو را کشف کنیم.", + "step_notes_cta": "یادداشت‌هایم آماده است", + "step_aha_title": "چیزی که فراموش کرده‌اید را پیدا کنید", + "step_aha_subtitle": "سوال بپرسید. یادداشت فراموش شده را پیدا کنید.", + "step_aha_placeholder": "یادداشت‌های بهره‌وری...", + "step_aha_cta": "کشف Momento", + "progress": "{current} از {total}", + "creating_demo_notes": "در حال ایجاد یادداشت‌های نمونه...", + "demo_notes_ready": "۵ یادداشت نمونه ایجاد شد!", + "badge_credits": "⚡ {count} اعتبار باقی", + "badge_upgrade": "ارتقاء به Pro →", + "no_results": "نتیجه‌ای یافت نشد — پرس‌وجوی دیگری امتحان کنید.", + "search_credit_used": "۱ جستجو استفاده شد", + "quota_exceeded": "سهمیه جستجو تمام شد — به Pro ارتقا دهید.", + "step_aha_search_button": "جستجو", + "step_aha_search_aria": "جستجو در یادداشت‌های شما", + "step_notes_hint": "💡 این یادداشت‌ها نمایش جستجوی هوش مصنوعی در مرحله بعد را تأمین می‌کنند.", + "step_features_title": "قدرت‌های فوق‌العاده هوش مصنوعی شما", + "step_features_subtitle": "انتخاب کنید از کجا شروع کنید.", + "step_features_cta": "بزن بریم!", + "feature_search_title": "جستجوی معنایی", + "feature_search_desc": "هر یادداشتی را بر اساس معنا پیدا کنید، نه فقط کلمات کلیدی.", + "feature_flashcards_title": "فلش‌کارت‌های هوش مصنوعی", + "feature_flashcards_desc": "کارت‌های مرور SRS را با یک کلیک از یادداشت‌هایتان بسازید.", + "feature_brainstorm_title": "طوفان فکری هوش مصنوعی", + "feature_brainstorm_desc": "جلسات طوفان فکری مشارکتی با پشتیبانی هوش مصنوعی.", + "feature_chat_title": "گفتگو با یادداشت‌ها", + "feature_chat_desc": "از پایگاه دانش شخصی خود سوال بپرسید.", + "feature_insights_title": "بینش‌های معنایی", + "feature_insights_desc": "ارتباط‌های پنهان بین ایده‌هایتان را کشف کنید.", + "feature_export_title": "خروجی Markdown", + "feature_export_desc": "یادداشت‌هایتان را با فرمت Markdown استاندارد وارد و صادر کنید.", + "welcome_title_name": "سلام {name} 👋", + "import_formats": "فرمت‌های پذیرفته‌شده: .md, .txt", + "import_error": "برخی فایل‌ها وارد نشدند. دوباره امتحان کنید.", + "import_notes_ready": "{count} یادداشت وارد شد!", + "action_write_title": "اولین یادداشت واقعی خود را بنویسید", + "action_write_desc": "یادداشتی بسازید و ایده‌هایتان را ثبت کنید.", + "action_flashcards_title": "اولین فلش‌کارت‌هایتان را بسازید", + "action_flashcards_desc": "یک یادداشت باز کنید و روی دکمه فلش‌کارت کلیک کنید.", + "action_brainstorm_title": "طوفان فکری هوش مصنوعی را شروع کنید", + "action_brainstorm_desc": "ایده‌هایتان را با یک عامل هوش مصنوعی کشف کنید.", + "action_try": "امتحان", + "step_features_cta_all": "همه چیز آماده است — بریم!", + "action_write_where": "ببند → روی \"+ یادداشت جدید\" در نوار کناری کلیک کنید", + "action_flashcards_where": "ببند → یادداشتی باز کنید → دکمه 🃏 در نوار ابزار", + "action_brainstorm_where": "ببند → بخش \"Canvas\" در نوار کناری", + "pill_resume": "✨ ادامه راهنما", + "action_done": "امتحان شد!", + "editor_hints_title": "Editor tips", + "editor_hints_got_it": "Got it!", + "hint_slash_title": "\"/\" command — insert blocks", + "hint_slash_desc": "In the editor, type \"/\" to open the block menu: heading, list, code block, table, to-do list, and AI commands (Clarify, Shorten, Improve, Expand).", + "hint_ai_title": "Built-in AI assistant", + "hint_ai_desc": "Click the ✨ button in the toolbar to open the AI panel — ask questions, summarize, rewrite, or brainstorm directly in your note.", + "hint_version_title": "Version history", + "hint_version_desc": "Click the ⓘ button in the toolbar → \"Versions\" tab. Enable versioning, then save and restore snapshots of your note at any time.", + "hint_flashcards_title": "Generate flashcards", + "hint_flashcards_desc": "Click the 🎓 button in the toolbar to auto-generate flashcards from your note for spaced repetition review.", + "hint_links_title": "Links between notes", + "hint_links_desc": "Type \"[[\" in the editor to search and link to another note. Linked notes appear as backlinks at the bottom of the note.", + "hint_create_note_title": "Create a note", + "hint_create_note_desc": "Click the \"+\" button in the sidebar or press Ctrl+N to create a new note. Then start writing.", + "hint_flip_title": "Flip the card", + "hint_flip_desc": "Press Space (or click the card) to flip it and reveal the answer.", + "hint_rate_keys_title": "Rate with keyboard", + "hint_rate_keys_desc": "After flipping, press 1 (Hard), 2 (Difficult), 3 (Good) or 4 (Easy) to rate the card. The SM-2 algorithm schedules your next review automatically.", + "hint_generate_from_note_title": "Generate from a note", + "hint_generate_from_note_desc": "Open any note and click the 🎓 button in the toolbar to automatically generate flashcards from its content.", + "hint_brainstorm_start_title": "Start with an idea", + "hint_brainstorm_start_desc": "Type any concept or question in the input field and press Enter. The AI will generate a set of ideas around it.", + "hint_brainstorm_deepen_title": "Deepen an idea", + "hint_brainstorm_deepen_desc": "Click on any idea card to expand it with sub-ideas and explore it further.", + "hint_brainstorm_export_title": "Export your session", + "hint_brainstorm_export_desc": "When done, export the entire brainstorm session as a structured note saved to your notebook.", + "hint_insights_clusters_title": "Note clusters", + "hint_insights_clusters_desc": "Your notes are automatically grouped into thematic clusters. Click a cluster to explore the notes it contains.", + "hint_insights_bridge_title": "Bridge notes", + "hint_insights_bridge_desc": "Bridge notes connect multiple clusters. They are highlighted because they hold your knowledge graph together.", + "hint_insights_refresh_title": "Refresh clusters", + "hint_insights_refresh_desc": "If you've added new notes, click the refresh button to recalculate the clusters with the latest content." } -} +} \ No newline at end of file diff --git a/memento-note/locales/fr.json b/memento-note/locales/fr.json index 86efca5..c6ccc36 100644 --- a/memento-note/locales/fr.json +++ b/memento-note/locales/fr.json @@ -2433,7 +2433,12 @@ "Italiano": "Italien", "Chinois": "Chinois", "Japonais": "Japonais" - } + }, + "exportMarkdown": "Exporter en Markdown", + "importMarkdown": "Importer un Markdown", + "markdownExportSuccess": "Note exportée en Markdown", + "markdownExportError": "Échec de l'export de la note", + "markdownImportSuccess": "Markdown importé avec succès" }, "flashcards": { "generateTitle": "Générer des flashcards", @@ -2796,7 +2801,16 @@ "ideasCount": "idées", "star": "Mettre en favori", "unstar": "Retirer des favoris", - "renameSession": "Renommer la session" + "renameSession": "Renommer la session", + "downloadPptx": "PPTX", + "downloadPptxDesc": "Télécharger en PowerPoint", + "pptxSuccess": "PPTX téléchargé", + "pptxError": "Échec de l'export PPTX", + "fitToScreen": "Recentrer", + "legendWave1": "Variations", + "legendWave2": "Analogies", + "legendWave3": "Disruptions", + "legendConverted": "Convertie" }, "byokSettings": { "title": "Vos clés API (BYOK)", @@ -3465,5 +3479,96 @@ "notesLoadError": "Erreur de chargement des notes", "defaultOption1": "Option 1", "defaultOption2": "Option 2" + }, + "onboarding": { + "welcome_title": "Votre mémoire augmentée par l'IA", + "welcome_subtitle": "Momento se souvient de ce que vous oubliez.", + "welcome_cta": "Commencer", + "skip": "Passer", + "step_notes_title": "Vos notes", + "step_notes_empty": "Vous n'avez pas encore de notes. Importez les vôtres ou commencez avec des exemples.", + "step_notes_import": "Importer mes notes", + "step_notes_demo": "Créer 5 notes d'exemple", + "step_notes_has_notes": "Vous avez déjà {count} notes ! Découvrons la magie.", + "step_notes_cta": "Mes notes sont prêtes", + "step_aha_title": "Retrouvez ce que vous avez oublié", + "step_aha_subtitle": "Tapez une question. Retrouvez une note oubliée.", + "step_aha_placeholder": "notes sur ma productivité...", + "step_aha_cta": "Explorer Momento", + "progress": "{current} sur {total}", + "creating_demo_notes": "Création des notes d'exemple...", + "demo_notes_ready": "5 notes d'exemple créées !", + "badge_credits": "⚡ {count} crédits restants", + "badge_upgrade": "Passer en Pro →", + "no_results": "Aucun résultat — essayez une autre requête.", + "search_credit_used": "1 recherche utilisée", + "quota_exceeded": "Quota de recherche atteint — passez en Pro pour illimité.", + "step_aha_search_button": "Chercher", + "step_aha_search_aria": "Rechercher dans vos notes", + "step_notes_hint": "💡 Ces notes alimenteront la démo de recherche IA à l'étape suivante.", + "step_features_title": "Vos super-pouvoirs IA", + "step_features_subtitle": "Choisissez par où commencer.", + "step_features_cta": "C'est parti !", + "feature_search_title": "Recherche sémantique", + "feature_search_desc": "Retrouvez n'importe quelle note par sens, pas seulement par mot-clé.", + "feature_flashcards_title": "Flashcards IA", + "feature_flashcards_desc": "Générez des cartes de révision SRS depuis vos notes en un clic.", + "feature_brainstorm_title": "Brainstorm IA", + "feature_brainstorm_desc": "Séances de brainstorming collaboratif alimentées par l'IA.", + "feature_chat_title": "Chat avec vos notes", + "feature_chat_desc": "Posez des questions à votre base de connaissances personnelle.", + "feature_insights_title": "Insights sémantiques", + "feature_insights_desc": "Découvrez les connexions cachées entre vos idées.", + "feature_export_title": "Export Markdown", + "feature_export_desc": "Importez et exportez vos notes au format Markdown standard.", + "welcome_title_name": "Bonjour {name} 👋", + "import_formats": "Formats acceptés : .md, .txt", + "import_error": "Impossible d'importer certains fichiers. Réessayez.", + "import_notes_ready": "{count} note(s) importée(s) !", + "action_write_title": "Écrire votre première vraie note", + "action_write_desc": "Créez une note et commencez à capturer vos idées.", + "action_flashcards_title": "Générer vos premières flashcards", + "action_flashcards_desc": "Ouvrez une note et cliquez sur le bouton flashcards.", + "action_brainstorm_title": "Lancer un brainstorm IA", + "action_brainstorm_desc": "Explorez vos idées avec un agent IA dédié.", + "action_try": "Essayer", + "step_features_cta_all": "Tout est prêt — on plonge !", + "action_write_where": "Fermez ce menu → cliquez sur \"+ Nouvelle note\" dans la barre latérale", + "action_flashcards_where": "Fermez ce menu → ouvrez une note → bouton 🃏 dans la toolbar", + "action_brainstorm_where": "Fermez ce menu → section \"Canvas\" dans la barre latérale", + "pill_resume": "✨ Reprendre la visite", + "action_done": "Testé !", + "editor_hints_title": "Astuces éditeur", + "editor_hints_got_it": "C'est compris !", + "hint_slash_title": "Commande \"/\" — insérer des blocs", + "hint_slash_desc": "Dans l'éditeur, tapez \"/\" pour ouvrir le menu de blocs : titre, liste, code, tableau, liste de tâches, et les commandes IA (Clarifier, Raccourcir, Améliorer, Développer).", + "hint_ai_title": "Assistant IA intégré", + "hint_ai_desc": "Cliquez sur le bouton ✨ dans la barre d'outils pour ouvrir le panneau IA — posez des questions, résumez, réécrivez ou brainstormez directement dans votre note.", + "hint_version_title": "Historique des versions", + "hint_version_desc": "Cliquez sur le bouton ⓘ dans la barre d'outils → onglet \"Versions\". Activez le versionnage, puis sauvegardez et restaurez des captures de votre note à tout moment.", + "hint_flashcards_title": "Générer des flashcards", + "hint_flashcards_desc": "Cliquez sur le bouton 🎓 dans la barre d'outils pour générer automatiquement des flashcards depuis votre note, pour une révision en répétition espacée.", + "hint_links_title": "Liens entre notes", + "hint_links_desc": "Tapez \"[[\" dans l'éditeur pour rechercher et lier une autre note. Les notes liées apparaissent comme rétroliens en bas de la note.", + "hint_create_note_title": "Créer une note", + "hint_create_note_desc": "Cliquez sur le bouton \"+\" dans la barre latérale pour créer une nouvelle note, puis commencez à écrire.", + "hint_flip_title": "Retourner la carte", + "hint_flip_desc": "Appuyez sur Espace (ou cliquez sur la carte) pour la retourner et révéler la réponse.", + "hint_rate_keys_title": "Noter au clavier", + "hint_rate_keys_desc": "Après avoir retourné la carte, appuyez sur 1 (Difficile), 2 (Laborieux), 3 (Bien) ou 4 (Facile) pour noter. L'algorithme SM-2 planifie automatiquement votre prochaine révision.", + "hint_generate_from_note_title": "Générer depuis une note", + "hint_generate_from_note_desc": "Ouvrez n'importe quelle note et cliquez sur le bouton 🎓 dans la barre d'outils pour générer automatiquement des flashcards depuis son contenu.", + "hint_brainstorm_start_title": "Démarrer avec une idée", + "hint_brainstorm_start_desc": "Tapez un concept ou une question dans le champ de saisie et appuyez sur Entrée. L'IA génère un ensemble d'idées autour de ce thème.", + "hint_brainstorm_deepen_title": "Approfondir une idée", + "hint_brainstorm_deepen_desc": "Cliquez sur n'importe quelle carte d'idée pour l'approfondir avec des sous-idées et l'explorer davantage.", + "hint_brainstorm_export_title": "Exporter la session", + "hint_brainstorm_export_desc": "Une fois terminé, exportez toute la session de brainstorm sous forme de note structurée sauvegardée dans votre carnet.", + "hint_insights_clusters_title": "Clusters de notes", + "hint_insights_clusters_desc": "Vos notes sont automatiquement regroupées en clusters thématiques. Cliquez sur un cluster pour explorer les notes qu'il contient.", + "hint_insights_bridge_title": "Notes ponts", + "hint_insights_bridge_desc": "Les notes ponts relient plusieurs clusters. Elles sont mises en avant car elles constituent les connexions clés de votre graphe de connaissances.", + "hint_insights_refresh_title": "Rafraîchir les clusters", + "hint_insights_refresh_desc": "Si vous avez ajouté de nouvelles notes, cliquez sur le bouton de rafraîchissement pour recalculer les clusters avec le contenu le plus récent." } } \ No newline at end of file diff --git a/memento-note/locales/hi.json b/memento-note/locales/hi.json index 469032e..b2cd98e 100644 --- a/memento-note/locales/hi.json +++ b/memento-note/locales/hi.json @@ -2176,7 +2176,12 @@ "Italiano": "इतालवी", "Chinois": "चीनी", "Japonais": "जापानी" - } + }, + "exportMarkdown": "Markdown के रूप में निर्यात करें", + "importMarkdown": "Markdown आयात करें", + "markdownExportSuccess": "नोट Markdown के रूप में निर्यात किया गया", + "markdownExportError": "नोट निर्यात करने में विफल", + "markdownImportSuccess": "Markdown सफलतापूर्वक आयात किया गया" }, "brainstorm": { "title": "Waves of Thought", @@ -2306,7 +2311,16 @@ "ownerBadge": "Owner", "waveBadge": "Wave {wave}", "quotaGuest": "सत्र के होस्ट की AI सीमा समाप्त हो गई है। उनसे अपना प्लान अपग्रेड करने को कहें।", - "quotaHost": "इस ब्रेनस्टॉर्म के लिए आपकी AI सीमा समाप्त हो गई है। जारी रखने के लिए प्लान अपग्रेड करें।" + "quotaHost": "इस ब्रेनस्टॉर्म के लिए आपकी AI सीमा समाप्त हो गई है। जारी रखने के लिए प्लान अपग्रेड करें।", + "downloadPptx": "PPTX", + "downloadPptxDesc": "PowerPoint के रूप में डाउनलोड करें", + "pptxSuccess": "PPTX डाउनलोड हो गया", + "pptxError": "PPTX निर्यात विफल", + "fitToScreen": "पुनः केन्द्रित करें", + "legendWave1": "विविधताएँ", + "legendWave2": "समानताएँ", + "legendWave3": "व्यवधान", + "legendConverted": "रूपांतरित" }, "usageMeter": { "packName": "AI Discovery Pack", @@ -2762,5 +2776,96 @@ "uploadError": "अपलोड त्रुटि", "uploadFailed": "अपलोड विफल", "uploading": "अपलोड हो रहा है..." + }, + "onboarding": { + "welcome_title": "AI-संवर्धित आपकी स्मृति", + "welcome_subtitle": "Momento वह याद रखता है जो आप भूल जाते हैं।", + "welcome_cta": "शुरू करें", + "skip": "छोड़ें", + "step_notes_title": "आपके नोट्स", + "step_notes_empty": "आपके पास अभी कोई नोट नहीं है। अपने नोट्स आयात करें या उदाहरणों से शुरू करें।", + "step_notes_import": "मेरे नोट्स आयात करें", + "step_notes_demo": "5 उदाहरण नोट्स बनाएं", + "step_notes_has_notes": "आपके पास पहले से {count} नोट्स हैं। आइए जादू खोजें।", + "step_notes_cta": "मेरे नोट्स तैयार हैं", + "step_aha_title": "वह खोजें जो आप भूल गए", + "step_aha_subtitle": "एक प्रश्न लिखें। भूला हुआ नोट खोजें।", + "step_aha_placeholder": "उत्पादकता पर नोट्स...", + "step_aha_cta": "Momento एक्सप्लोर करें", + "progress": "{total} में से {current}", + "creating_demo_notes": "उदाहरण नोट्स बना रहे हैं...", + "demo_notes_ready": "5 उदाहरण नोट्स बनाए गए!", + "badge_credits": "⚡ {count} क्रेडिट शेष", + "badge_upgrade": "Pro में अपग्रेड करें →", + "no_results": "कोई परिणाम नहीं — दूसरी क्वेरी आज़माएं।", + "search_credit_used": "1 खोज का उपयोग हुआ", + "quota_exceeded": "खोज कोटा समाप्त — Pro में अपग्रेड करें।", + "step_aha_search_button": "खोजें", + "step_aha_search_aria": "अपने नोट्स खोजें", + "step_notes_hint": "💡 ये नोट्स अगले चरण में AI खोज डेमो को शक्ति देंगे।", + "step_features_title": "आपकी AI महाशक्तियाँ", + "step_features_subtitle": "चुनें कहाँ से शुरू करना है।", + "step_features_cta": "चलिए शुरू करते हैं!", + "feature_search_title": "सिमेंटिक खोज", + "feature_search_desc": "केवल कीवर्ड से नहीं, अर्थ से कोई भी नोट खोजें।", + "feature_flashcards_title": "AI फ्लैशकार्ड", + "feature_flashcards_desc": "एक क्लिक में अपने नोट्स से SRS समीक्षा कार्ड बनाएं।", + "feature_brainstorm_title": "AI ब्रेनस्टॉर्म", + "feature_brainstorm_desc": "AI-संचालित सहयोगी ब्रेनस्टॉर्मिंग सत्र।", + "feature_chat_title": "नोट्स से चैट करें", + "feature_chat_desc": "अपने व्यक्तिगत ज्ञान आधार से प्रश्न पूछें।", + "feature_insights_title": "सिमेंटिक इनसाइट्स", + "feature_insights_desc": "अपने विचारों के बीच छुपे संबंध खोजें।", + "feature_export_title": "Markdown निर्यात", + "feature_export_desc": "अपने नोट्स को मानक Markdown प्रारूप में आयात और निर्यात करें।", + "welcome_title_name": "नमस्ते {name} 👋", + "import_formats": "स्वीकृत प्रारूप: .md, .txt", + "import_error": "कुछ फ़ाइलें आयात नहीं हो सकीं। पुनः प्रयास करें।", + "import_notes_ready": "{count} नोट(स) आयात किए!", + "action_write_title": "अपना पहला असली नोट लिखें", + "action_write_desc": "एक नोट बनाएं और अपने विचार लिखना शुरू करें।", + "action_flashcards_title": "अपने पहले फ्लैशकार्ड बनाएं", + "action_flashcards_desc": "एक नोट खोलें और फ्लैशकार्ड बटन पर क्लिक करें।", + "action_brainstorm_title": "AI ब्रेनस्टॉर्म शुरू करें", + "action_brainstorm_desc": "AI एजेंट के साथ अपने विचारों का अन्वेषण करें।", + "action_try": "कोशिश", + "step_features_cta_all": "सब तैयार — चलिए शुरू करते हैं!", + "action_write_where": "बंद करें → साइडबार में \"+ नया नोट\" क्लिक करें", + "action_flashcards_where": "बंद करें → नोट खोलें → टूलबार में 🃏 बटन", + "action_brainstorm_where": "बंद करें → साइडबार में \"Canvas\" अनुभाग", + "pill_resume": "✨ टूर जारी रखें", + "action_done": "आज़माया!", + "editor_hints_title": "Editor tips", + "editor_hints_got_it": "Got it!", + "hint_slash_title": "\"/\" command — insert blocks", + "hint_slash_desc": "In the editor, type \"/\" to open the block menu: heading, list, code block, table, to-do list, and AI commands (Clarify, Shorten, Improve, Expand).", + "hint_ai_title": "Built-in AI assistant", + "hint_ai_desc": "Click the ✨ button in the toolbar to open the AI panel — ask questions, summarize, rewrite, or brainstorm directly in your note.", + "hint_version_title": "Version history", + "hint_version_desc": "Click the ⓘ button in the toolbar → \"Versions\" tab. Enable versioning, then save and restore snapshots of your note at any time.", + "hint_flashcards_title": "Generate flashcards", + "hint_flashcards_desc": "Click the 🎓 button in the toolbar to auto-generate flashcards from your note for spaced repetition review.", + "hint_links_title": "Links between notes", + "hint_links_desc": "Type \"[[\" in the editor to search and link to another note. Linked notes appear as backlinks at the bottom of the note.", + "hint_create_note_title": "Create a note", + "hint_create_note_desc": "Click the \"+\" button in the sidebar or press Ctrl+N to create a new note. Then start writing.", + "hint_flip_title": "Flip the card", + "hint_flip_desc": "Press Space (or click the card) to flip it and reveal the answer.", + "hint_rate_keys_title": "Rate with keyboard", + "hint_rate_keys_desc": "After flipping, press 1 (Hard), 2 (Difficult), 3 (Good) or 4 (Easy) to rate the card. The SM-2 algorithm schedules your next review automatically.", + "hint_generate_from_note_title": "Generate from a note", + "hint_generate_from_note_desc": "Open any note and click the 🎓 button in the toolbar to automatically generate flashcards from its content.", + "hint_brainstorm_start_title": "Start with an idea", + "hint_brainstorm_start_desc": "Type any concept or question in the input field and press Enter. The AI will generate a set of ideas around it.", + "hint_brainstorm_deepen_title": "Deepen an idea", + "hint_brainstorm_deepen_desc": "Click on any idea card to expand it with sub-ideas and explore it further.", + "hint_brainstorm_export_title": "Export your session", + "hint_brainstorm_export_desc": "When done, export the entire brainstorm session as a structured note saved to your notebook.", + "hint_insights_clusters_title": "Note clusters", + "hint_insights_clusters_desc": "Your notes are automatically grouped into thematic clusters. Click a cluster to explore the notes it contains.", + "hint_insights_bridge_title": "Bridge notes", + "hint_insights_bridge_desc": "Bridge notes connect multiple clusters. They are highlighted because they hold your knowledge graph together.", + "hint_insights_refresh_title": "Refresh clusters", + "hint_insights_refresh_desc": "If you've added new notes, click the refresh button to recalculate the clusters with the latest content." } -} +} \ No newline at end of file diff --git a/memento-note/locales/it.json b/memento-note/locales/it.json index 747de95..1abe907 100644 --- a/memento-note/locales/it.json +++ b/memento-note/locales/it.json @@ -2176,7 +2176,12 @@ "Italiano": "Italiano", "Chinois": "Cinese", "Japonais": "Giapponese" - } + }, + "exportMarkdown": "Esporta come Markdown", + "importMarkdown": "Importa Markdown", + "markdownExportSuccess": "Nota esportata come Markdown", + "markdownExportError": "Esportazione nota fallita", + "markdownImportSuccess": "Markdown importato con successo" }, "brainstorm": { "title": "Waves of Thought", @@ -2306,7 +2311,16 @@ "ownerBadge": "Owner", "waveBadge": "Wave {wave}", "quotaGuest": "L'host della sessione ha raggiunto il limite IA. Chiedigli di aggiornare il piano.", - "quotaHost": "Hai raggiunto il limite IA per questo brainstorm. Passa a un piano superiore per continuare." + "quotaHost": "Hai raggiunto il limite IA per questo brainstorm. Passa a un piano superiore per continuare.", + "downloadPptx": "PPTX", + "downloadPptxDesc": "Scarica come PowerPoint", + "pptxSuccess": "PPTX scaricato", + "pptxError": "Esportazione PPTX fallita", + "fitToScreen": "Ri-centrare", + "legendWave1": "Variazioni", + "legendWave2": "Analogie", + "legendWave3": "Disruzioni", + "legendConverted": "Convertita" }, "usageMeter": { "packName": "AI Discovery Pack", @@ -2762,5 +2776,96 @@ "uploadError": "Errore di caricamento", "uploadFailed": "Caricamento non riuscito", "uploading": "Caricamento..." + }, + "onboarding": { + "welcome_title": "La tua memoria potenziata dall'IA", + "welcome_subtitle": "Momento ricorda ciò che dimentichi.", + "welcome_cta": "Inizia", + "skip": "Salta", + "step_notes_title": "Le tue note", + "step_notes_empty": "Non hai ancora note. Importa le tue o inizia con esempi.", + "step_notes_import": "Importa le mie note", + "step_notes_demo": "Crea 5 note di esempio", + "step_notes_has_notes": "Hai già {count} note. Scopriamo la magia.", + "step_notes_cta": "Le mie note sono pronte", + "step_aha_title": "Trova ciò che hai dimenticato", + "step_aha_subtitle": "Fai una domanda. Trova una nota dimenticata.", + "step_aha_placeholder": "note sulla produttività...", + "step_aha_cta": "Esplora Momento", + "progress": "{current} di {total}", + "creating_demo_notes": "Creazione note di esempio...", + "demo_notes_ready": "5 note di esempio create!", + "badge_credits": "⚡ {count} crediti rimasti", + "badge_upgrade": "Passa a Pro →", + "no_results": "Nessun risultato — prova un'altra query.", + "search_credit_used": "1 ricerca usata", + "quota_exceeded": "Quota di ricerca raggiunta — passa a Pro.", + "step_aha_search_button": "Cerca", + "step_aha_search_aria": "Cerca nelle tue note", + "step_notes_hint": "💡 Queste note alimenteranno la demo di ricerca IA nel passo successivo.", + "step_features_title": "I tuoi superpoteri IA", + "step_features_subtitle": "Scegli da dove iniziare.", + "step_features_cta": "Andiamo!", + "feature_search_title": "Ricerca semantica", + "feature_search_desc": "Trova qualsiasi nota per significato, non solo per parole chiave.", + "feature_flashcards_title": "Flashcard IA", + "feature_flashcards_desc": "Genera schede di ripasso SRS dalle tue note in un clic.", + "feature_brainstorm_title": "Brainstorming IA", + "feature_brainstorm_desc": "Sessioni di brainstorming collaborativo con IA.", + "feature_chat_title": "Chatta con le tue note", + "feature_chat_desc": "Fai domande alla tua base di conoscenza personale.", + "feature_insights_title": "Approfondimenti semantici", + "feature_insights_desc": "Scopri connessioni nascoste tra le tue idee.", + "feature_export_title": "Esportazione Markdown", + "feature_export_desc": "Importa ed esporta le tue note in formato Markdown.", + "welcome_title_name": "Ciao {name} 👋", + "import_formats": "Formati accettati: .md, .txt", + "import_error": "Impossibile importare alcuni file. Riprova.", + "import_notes_ready": "{count} nota/e importata/e!", + "action_write_title": "Scrivi la tua prima vera nota", + "action_write_desc": "Crea una nota e inizia a catturare le tue idee.", + "action_flashcards_title": "Genera le prime flashcard", + "action_flashcards_desc": "Apri una nota e clicca sul pulsante flashcard.", + "action_brainstorm_title": "Avvia un brainstorming IA", + "action_brainstorm_desc": "Esplora le tue idee con un agente IA.", + "action_try": "Prova", + "step_features_cta_all": "Tutto pronto — immergiamoci!", + "action_write_where": "Chiudi → clicca \"+ Nuova nota\" nella barra laterale", + "action_flashcards_where": "Chiudi → apri una nota → pulsante 🃏 nella toolbar", + "action_brainstorm_where": "Chiudi → sezione \"Canvas\" nella barra laterale", + "pill_resume": "✨ Riprendi tour", + "action_done": "Provato!", + "editor_hints_title": "Editor tips", + "editor_hints_got_it": "Got it!", + "hint_slash_title": "\"/\" command — insert blocks", + "hint_slash_desc": "In the editor, type \"/\" to open the block menu: heading, list, code block, table, to-do list, and AI commands (Clarify, Shorten, Improve, Expand).", + "hint_ai_title": "Built-in AI assistant", + "hint_ai_desc": "Click the ✨ button in the toolbar to open the AI panel — ask questions, summarize, rewrite, or brainstorm directly in your note.", + "hint_version_title": "Version history", + "hint_version_desc": "Click the ⓘ button in the toolbar → \"Versions\" tab. Enable versioning, then save and restore snapshots of your note at any time.", + "hint_flashcards_title": "Generate flashcards", + "hint_flashcards_desc": "Click the 🎓 button in the toolbar to auto-generate flashcards from your note for spaced repetition review.", + "hint_links_title": "Links between notes", + "hint_links_desc": "Type \"[[\" in the editor to search and link to another note. Linked notes appear as backlinks at the bottom of the note.", + "hint_create_note_title": "Create a note", + "hint_create_note_desc": "Click the \"+\" button in the sidebar or press Ctrl+N to create a new note. Then start writing.", + "hint_flip_title": "Flip the card", + "hint_flip_desc": "Press Space (or click the card) to flip it and reveal the answer.", + "hint_rate_keys_title": "Rate with keyboard", + "hint_rate_keys_desc": "After flipping, press 1 (Hard), 2 (Difficult), 3 (Good) or 4 (Easy) to rate the card. The SM-2 algorithm schedules your next review automatically.", + "hint_generate_from_note_title": "Generate from a note", + "hint_generate_from_note_desc": "Open any note and click the 🎓 button in the toolbar to automatically generate flashcards from its content.", + "hint_brainstorm_start_title": "Start with an idea", + "hint_brainstorm_start_desc": "Type any concept or question in the input field and press Enter. The AI will generate a set of ideas around it.", + "hint_brainstorm_deepen_title": "Deepen an idea", + "hint_brainstorm_deepen_desc": "Click on any idea card to expand it with sub-ideas and explore it further.", + "hint_brainstorm_export_title": "Export your session", + "hint_brainstorm_export_desc": "When done, export the entire brainstorm session as a structured note saved to your notebook.", + "hint_insights_clusters_title": "Note clusters", + "hint_insights_clusters_desc": "Your notes are automatically grouped into thematic clusters. Click a cluster to explore the notes it contains.", + "hint_insights_bridge_title": "Bridge notes", + "hint_insights_bridge_desc": "Bridge notes connect multiple clusters. They are highlighted because they hold your knowledge graph together.", + "hint_insights_refresh_title": "Refresh clusters", + "hint_insights_refresh_desc": "If you've added new notes, click the refresh button to recalculate the clusters with the latest content." } -} +} \ No newline at end of file diff --git a/memento-note/locales/ja.json b/memento-note/locales/ja.json index 521f231..a94143c 100644 --- a/memento-note/locales/ja.json +++ b/memento-note/locales/ja.json @@ -2176,7 +2176,12 @@ "Italiano": "イタリア語", "Chinois": "中国語", "Japonais": "日本語" - } + }, + "exportMarkdown": "Markdownとしてエクスポート", + "importMarkdown": "Markdownをインポート", + "markdownExportSuccess": "ノートをMarkdownとしてエクスポートしました", + "markdownExportError": "ノートのエクスポートに失敗しました", + "markdownImportSuccess": "Markdownのインポートに成功しました" }, "brainstorm": { "title": "Waves of Thought", @@ -2306,7 +2311,16 @@ "ownerBadge": "Owner", "waveBadge": "Wave {wave}", "quotaGuest": "セッションのホストがAI利用上限に達しました。プランのアップグレードを依頼してください。", - "quotaHost": "このブレインストームのAI上限に達しました。続けるにはプランをアップグレードしてください。" + "quotaHost": "このブレインストームのAI上限に達しました。続けるにはプランをアップグレードしてください。", + "downloadPptx": "PPTX", + "downloadPptxDesc": "PowerPointとしてダウンロード", + "pptxSuccess": "PPTXをダウンロードしました", + "pptxError": "PPTXのエクスポートに失敗しました", + "fitToScreen": "中央に戻す", + "legendWave1": "バリエーション", + "legendWave2": "類推", + "legendWave3": "破壊的変革", + "legendConverted": "変換済み" }, "usageMeter": { "packName": "AI Discovery Pack", @@ -2762,5 +2776,96 @@ "uploadError": "アップロードエラー", "uploadFailed": "アップロードに失敗しました", "uploading": "アップロード中..." + }, + "onboarding": { + "welcome_title": "AIで強化されたあなたの記憶", + "welcome_subtitle": "Momentoはあなたが忘れたことを覚えています。", + "welcome_cta": "始める", + "skip": "スキップ", + "step_notes_title": "あなたのノート", + "step_notes_empty": "まだノートがありません。インポートするか、例から始めましょう。", + "step_notes_import": "ノートをインポート", + "step_notes_demo": "5つの例ノートを作成", + "step_notes_has_notes": "すでに{count}件のノートがあります。魔法を発見しましょう。", + "step_notes_cta": "ノートの準備ができました", + "step_aha_title": "忘れたことを見つける", + "step_aha_subtitle": "質問を入力してください。忘れたノートを見つけます。", + "step_aha_placeholder": "生産性に関するメモ...", + "step_aha_cta": "Momentoを探索", + "progress": "{total}中{current}", + "creating_demo_notes": "サンプルノートを作成中...", + "demo_notes_ready": "5つのサンプルノートを作成しました!", + "badge_credits": "⚡ 残り {count} クレジット", + "badge_upgrade": "Proにアップグレード →", + "no_results": "結果なし — 別のクエリを試してください。", + "search_credit_used": "検索 1 回使用", + "quota_exceeded": "検索クォータに達しました — Proにアップグレード。", + "step_aha_search_button": "検索", + "step_aha_search_aria": "メモを検索", + "step_notes_hint": "💡 これらのノートは次のステップのAI検索デモに使用されます。", + "step_features_title": "あなたのAIスーパーパワー", + "step_features_subtitle": "どこから始めるか選んでください。", + "step_features_cta": "始めましょう!", + "feature_search_title": "セマンティック検索", + "feature_search_desc": "キーワードだけでなく意味でノートを検索。", + "feature_flashcards_title": "AIフラッシュカード", + "feature_flashcards_desc": "ノートからSRS復習カードをワンクリックで生成。", + "feature_brainstorm_title": "AIブレインストーミング", + "feature_brainstorm_desc": "AI搭載の共同ブレインストーミングセッション。", + "feature_chat_title": "ノートとチャット", + "feature_chat_desc": "個人ナレッジベースに質問する。", + "feature_insights_title": "セマンティックインサイト", + "feature_insights_desc": "アイデア間の隠れた関係を発見。", + "feature_export_title": "Markdownエクスポート", + "feature_export_desc": "標準Markdown形式でノートをインポート/エクスポート。", + "welcome_title_name": "こんにちは {name} 👋", + "import_formats": "対応形式:.md, .txt", + "import_error": "一部のファイルをインポートできませんでした。再試行してください。", + "import_notes_ready": "{count}件のノートをインポートしました!", + "action_write_title": "最初の本物のノートを書く", + "action_write_desc": "ノートを作成してアイデアを記録しましょう。", + "action_flashcards_title": "最初のフラッシュカードを生成", + "action_flashcards_desc": "ノートを開いてフラッシュカードボタンをクリック。", + "action_brainstorm_title": "AIブレインストーミングを開始", + "action_brainstorm_desc": "AIエージェントとアイデアを探索しましょう。", + "action_try": "試す", + "step_features_cta_all": "準備完了——さあ始めましょう!", + "action_write_where": "閉じる → サイドバーの「+ 新しいノート」をクリック", + "action_flashcards_where": "閉じる → ノートを開く → ツールバーの 🃏 ボタン", + "action_brainstorm_where": "閉じる → サイドバーの「Canvas」セクション", + "pill_resume": "✨ ツアーを再開", + "action_done": "試した!", + "editor_hints_title": "Editor tips", + "editor_hints_got_it": "Got it!", + "hint_slash_title": "\"/\" command — insert blocks", + "hint_slash_desc": "In the editor, type \"/\" to open the block menu: heading, list, code block, table, to-do list, and AI commands (Clarify, Shorten, Improve, Expand).", + "hint_ai_title": "Built-in AI assistant", + "hint_ai_desc": "Click the ✨ button in the toolbar to open the AI panel — ask questions, summarize, rewrite, or brainstorm directly in your note.", + "hint_version_title": "Version history", + "hint_version_desc": "Click the ⓘ button in the toolbar → \"Versions\" tab. Enable versioning, then save and restore snapshots of your note at any time.", + "hint_flashcards_title": "Generate flashcards", + "hint_flashcards_desc": "Click the 🎓 button in the toolbar to auto-generate flashcards from your note for spaced repetition review.", + "hint_links_title": "Links between notes", + "hint_links_desc": "Type \"[[\" in the editor to search and link to another note. Linked notes appear as backlinks at the bottom of the note.", + "hint_create_note_title": "Create a note", + "hint_create_note_desc": "Click the \"+\" button in the sidebar or press Ctrl+N to create a new note. Then start writing.", + "hint_flip_title": "Flip the card", + "hint_flip_desc": "Press Space (or click the card) to flip it and reveal the answer.", + "hint_rate_keys_title": "Rate with keyboard", + "hint_rate_keys_desc": "After flipping, press 1 (Hard), 2 (Difficult), 3 (Good) or 4 (Easy) to rate the card. The SM-2 algorithm schedules your next review automatically.", + "hint_generate_from_note_title": "Generate from a note", + "hint_generate_from_note_desc": "Open any note and click the 🎓 button in the toolbar to automatically generate flashcards from its content.", + "hint_brainstorm_start_title": "Start with an idea", + "hint_brainstorm_start_desc": "Type any concept or question in the input field and press Enter. The AI will generate a set of ideas around it.", + "hint_brainstorm_deepen_title": "Deepen an idea", + "hint_brainstorm_deepen_desc": "Click on any idea card to expand it with sub-ideas and explore it further.", + "hint_brainstorm_export_title": "Export your session", + "hint_brainstorm_export_desc": "When done, export the entire brainstorm session as a structured note saved to your notebook.", + "hint_insights_clusters_title": "Note clusters", + "hint_insights_clusters_desc": "Your notes are automatically grouped into thematic clusters. Click a cluster to explore the notes it contains.", + "hint_insights_bridge_title": "Bridge notes", + "hint_insights_bridge_desc": "Bridge notes connect multiple clusters. They are highlighted because they hold your knowledge graph together.", + "hint_insights_refresh_title": "Refresh clusters", + "hint_insights_refresh_desc": "If you've added new notes, click the refresh button to recalculate the clusters with the latest content." } -} +} \ No newline at end of file diff --git a/memento-note/locales/ko.json b/memento-note/locales/ko.json index 11eb035..35cdc11 100644 --- a/memento-note/locales/ko.json +++ b/memento-note/locales/ko.json @@ -2176,7 +2176,12 @@ "Italiano": "이탈리아어", "Chinois": "중국어", "Japonais": "일본어" - } + }, + "exportMarkdown": "Markdown으로 내보내기", + "importMarkdown": "Markdown 가져오기", + "markdownExportSuccess": "노트가 Markdown으로 내보내졌습니다", + "markdownExportError": "노트 내보내기 실패", + "markdownImportSuccess": "Markdown 가져오기 성공" }, "brainstorm": { "title": "Waves of Thought", @@ -2306,7 +2311,16 @@ "ownerBadge": "Owner", "waveBadge": "Wave {wave}", "quotaGuest": "세션 호스트의 AI 한도에 도달했습니다. 플랜 업그레이드를 요청하세요.", - "quotaHost": "이 브레인스토밍의 AI 한도에 도달했습니다. 계속하려면 플랜을 업그레이드하세요." + "quotaHost": "이 브레인스토밍의 AI 한도에 도달했습니다. 계속하려면 플랜을 업그레이드하세요.", + "downloadPptx": "PPTX", + "downloadPptxDesc": "PowerPoint로 다운로드", + "pptxSuccess": "PPTX 다운로드됨", + "pptxError": "PPTX 내보내기 실패", + "fitToScreen": "중앙으로", + "legendWave1": "변형", + "legendWave2": "유추", + "legendWave3": "파괴", + "legendConverted": "변환됨" }, "usageMeter": { "packName": "AI Discovery Pack", @@ -2762,5 +2776,96 @@ "uploadError": "업로드 오류", "uploadFailed": "업로드 실패", "uploading": "업로드 중..." + }, + "onboarding": { + "welcome_title": "AI로 강화된 당신의 기억", + "welcome_subtitle": "Momento는 당신이 잊은 것을 기억합니다.", + "welcome_cta": "시작하기", + "skip": "건너뛰기", + "step_notes_title": "당신의 노트", + "step_notes_empty": "아직 노트가 없습니다. 가져오거나 예제로 시작하세요.", + "step_notes_import": "내 노트 가져오기", + "step_notes_demo": "예제 노트 5개 만들기", + "step_notes_has_notes": "이미 {count}개의 노트가 있습니다. 마법을 발견해 봅시다.", + "step_notes_cta": "내 노트 준비 완료", + "step_aha_title": "잊은 것을 찾아보세요", + "step_aha_subtitle": "질문을 입력하세요. 잊었던 노트를 찾아보세요.", + "step_aha_placeholder": "생산성에 관한 노트...", + "step_aha_cta": "Momento 탐색", + "progress": "{total} 중 {current}", + "creating_demo_notes": "예제 노트 생성 중...", + "demo_notes_ready": "예제 노트 5개 생성 완료!", + "badge_credits": "⚡ {count}크레딧 남음", + "badge_upgrade": "Pro로 업그레이드 →", + "no_results": "결과 없음 — 다른 검색어를 시도하세요.", + "search_credit_used": "검색 1회 사용됨", + "quota_exceeded": "검색 할당량 초과 — Pro로 업그레이드하세요.", + "step_aha_search_button": "검색", + "step_aha_search_aria": "노트 검색", + "step_notes_hint": "💡 이 노트들은 다음 단계의 AI 검색 데모에 활용됩니다.", + "step_features_title": "당신의 AI 슈퍼파워", + "step_features_subtitle": "어디서 시작할지 선택하세요.", + "step_features_cta": "시작합시다!", + "feature_search_title": "시맨틱 검색", + "feature_search_desc": "키워드뿐만 아니라 의미로 노트를 찾아보세요.", + "feature_flashcards_title": "AI 플래시카드", + "feature_flashcards_desc": "노트에서 SRS 복습 카드를 한 번의 클릭으로 생성하세요.", + "feature_brainstorm_title": "AI 브레인스토밍", + "feature_brainstorm_desc": "AI 기반 협업 브레인스토밍 세션.", + "feature_chat_title": "노트와 채팅", + "feature_chat_desc": "개인 지식 베이스에 질문하세요.", + "feature_insights_title": "시맨틱 인사이트", + "feature_insights_desc": "아이디어 간의 숨겨진 연결을 발견하세요.", + "feature_export_title": "Markdown 내보내기", + "feature_export_desc": "표준 Markdown 형식으로 노트를 가져오거나 내보내세요.", + "welcome_title_name": "안녕하세요 {name} 👋", + "import_formats": "지원 형식: .md, .txt", + "import_error": "일부 파일을 가져올 수 없습니다. 다시 시도하세요.", + "import_notes_ready": "{count}개의 노트를 가져왔습니다!", + "action_write_title": "첫 번째 실제 노트 작성", + "action_write_desc": "노트를 만들고 아이디어를 기록하세요.", + "action_flashcards_title": "첫 번째 플래시카드 생성", + "action_flashcards_desc": "노트를 열고 플래시카드 버튼을 클릭하세요.", + "action_brainstorm_title": "AI 브레인스토밍 시작", + "action_brainstorm_desc": "AI 에이전트와 아이디어를 탐색하세요.", + "action_try": "해보기", + "step_features_cta_all": "모두 준비됨 — 시작합시다!", + "action_write_where": "닫기 → 사이드바의 \"+ 새 노트\" 클릭", + "action_flashcards_where": "닫기 → 노트 열기 → 툴바의 🃏 버튼", + "action_brainstorm_where": "닫기 → 사이드바의 \"Canvas\" 섹션", + "pill_resume": "✨ 투어 재개", + "action_done": "완료!", + "editor_hints_title": "Editor tips", + "editor_hints_got_it": "Got it!", + "hint_slash_title": "\"/\" command — insert blocks", + "hint_slash_desc": "In the editor, type \"/\" to open the block menu: heading, list, code block, table, to-do list, and AI commands (Clarify, Shorten, Improve, Expand).", + "hint_ai_title": "Built-in AI assistant", + "hint_ai_desc": "Click the ✨ button in the toolbar to open the AI panel — ask questions, summarize, rewrite, or brainstorm directly in your note.", + "hint_version_title": "Version history", + "hint_version_desc": "Click the ⓘ button in the toolbar → \"Versions\" tab. Enable versioning, then save and restore snapshots of your note at any time.", + "hint_flashcards_title": "Generate flashcards", + "hint_flashcards_desc": "Click the 🎓 button in the toolbar to auto-generate flashcards from your note for spaced repetition review.", + "hint_links_title": "Links between notes", + "hint_links_desc": "Type \"[[\" in the editor to search and link to another note. Linked notes appear as backlinks at the bottom of the note.", + "hint_create_note_title": "Create a note", + "hint_create_note_desc": "Click the \"+\" button in the sidebar or press Ctrl+N to create a new note. Then start writing.", + "hint_flip_title": "Flip the card", + "hint_flip_desc": "Press Space (or click the card) to flip it and reveal the answer.", + "hint_rate_keys_title": "Rate with keyboard", + "hint_rate_keys_desc": "After flipping, press 1 (Hard), 2 (Difficult), 3 (Good) or 4 (Easy) to rate the card. The SM-2 algorithm schedules your next review automatically.", + "hint_generate_from_note_title": "Generate from a note", + "hint_generate_from_note_desc": "Open any note and click the 🎓 button in the toolbar to automatically generate flashcards from its content.", + "hint_brainstorm_start_title": "Start with an idea", + "hint_brainstorm_start_desc": "Type any concept or question in the input field and press Enter. The AI will generate a set of ideas around it.", + "hint_brainstorm_deepen_title": "Deepen an idea", + "hint_brainstorm_deepen_desc": "Click on any idea card to expand it with sub-ideas and explore it further.", + "hint_brainstorm_export_title": "Export your session", + "hint_brainstorm_export_desc": "When done, export the entire brainstorm session as a structured note saved to your notebook.", + "hint_insights_clusters_title": "Note clusters", + "hint_insights_clusters_desc": "Your notes are automatically grouped into thematic clusters. Click a cluster to explore the notes it contains.", + "hint_insights_bridge_title": "Bridge notes", + "hint_insights_bridge_desc": "Bridge notes connect multiple clusters. They are highlighted because they hold your knowledge graph together.", + "hint_insights_refresh_title": "Refresh clusters", + "hint_insights_refresh_desc": "If you've added new notes, click the refresh button to recalculate the clusters with the latest content." } -} +} \ No newline at end of file diff --git a/memento-note/locales/nl.json b/memento-note/locales/nl.json index 6007746..999a132 100644 --- a/memento-note/locales/nl.json +++ b/memento-note/locales/nl.json @@ -2176,7 +2176,12 @@ "Italiano": "Italiaans", "Chinois": "Chinees", "Japonais": "Japans" - } + }, + "exportMarkdown": "Exporteren als Markdown", + "importMarkdown": "Markdown importeren", + "markdownExportSuccess": "Notitie geëxporteerd als Markdown", + "markdownExportError": "Exporteren van notitie mislukt", + "markdownImportSuccess": "Markdown succesvol geïmporteerd" }, "brainstorm": { "title": "Waves of Thought", @@ -2306,7 +2311,16 @@ "ownerBadge": "Owner", "waveBadge": "Wave {wave}", "quotaGuest": "De sessiehost heeft zijn AI-limiet bereikt. Vraag om een upgrade van het abonnement.", - "quotaHost": "Je hebt je AI-limiet voor deze brainstorm bereikt. Upgrade je abonnement om door te gaan." + "quotaHost": "Je hebt je AI-limiet voor deze brainstorm bereikt. Upgrade je abonnement om door te gaan.", + "downloadPptx": "PPTX", + "downloadPptxDesc": "Downloaden als PowerPoint", + "pptxSuccess": "PPTX gedownload", + "pptxError": "PPTX-export mislukt", + "fitToScreen": "Hercentreren", + "legendWave1": "Variaties", + "legendWave2": "Analogieën", + "legendWave3": "Verstoringen", + "legendConverted": "Geconverteerd" }, "usageMeter": { "packName": "AI Discovery Pack", @@ -2762,5 +2776,96 @@ "uploadError": "Uploadfout", "uploadFailed": "Upload mislukt", "uploading": "Uploaden..." + }, + "onboarding": { + "welcome_title": "Uw AI-versterkt geheugen", + "welcome_subtitle": "Momento onthoudt wat u vergeet.", + "welcome_cta": "Beginnen", + "skip": "Overslaan", + "step_notes_title": "Uw notities", + "step_notes_empty": "U heeft nog geen notities. Importeer uw eigen of begin met voorbeelden.", + "step_notes_import": "Mijn notities importeren", + "step_notes_demo": "5 voorbeeldnotities maken", + "step_notes_has_notes": "U heeft al {count} notities. Laten we de magie ontdekken.", + "step_notes_cta": "Mijn notities zijn klaar", + "step_aha_title": "Vind wat u vergeten bent", + "step_aha_subtitle": "Stel een vraag. Vind een vergeten notitie.", + "step_aha_placeholder": "notities over productiviteit...", + "step_aha_cta": "Momento verkennen", + "progress": "{current} van {total}", + "creating_demo_notes": "Voorbeeldnotities aanmaken...", + "demo_notes_ready": "5 voorbeeldnotities aangemaakt!", + "badge_credits": "⚡ Nog {count} credits", + "badge_upgrade": "Upgraden naar Pro →", + "no_results": "Geen resultaten — probeer een andere zoekopdracht.", + "search_credit_used": "1 zoekopdracht gebruikt", + "quota_exceeded": "Zoeklimiet bereikt — upgrade naar Pro.", + "step_aha_search_button": "Zoeken", + "step_aha_search_aria": "Zoek in je notities", + "step_notes_hint": "💡 Deze notities voeden de AI-zoekdemo in de volgende stap.", + "step_features_title": "Uw AI-superkrachten", + "step_features_subtitle": "Kies waar u wilt beginnen.", + "step_features_cta": "Aan de slag!", + "feature_search_title": "Semantisch zoeken", + "feature_search_desc": "Vind elke notitie op betekenis, niet alleen op trefwoorden.", + "feature_flashcards_title": "AI-flashcards", + "feature_flashcards_desc": "Genereer SRS-revisiekaarten uit uw notities met één klik.", + "feature_brainstorm_title": "AI-brainstormen", + "feature_brainstorm_desc": "AI-gestuurde collaboratieve brainstormsessies.", + "feature_chat_title": "Chat met uw notities", + "feature_chat_desc": "Stel vragen aan uw persoonlijke kennisbank.", + "feature_insights_title": "Semantische inzichten", + "feature_insights_desc": "Ontdek verborgen verbanden tussen uw ideeën.", + "feature_export_title": "Markdown-export", + "feature_export_desc": "Importeer en exporteer uw notities in Markdown-formaat.", + "welcome_title_name": "Hallo {name} 👋", + "import_formats": "Geaccepteerde formaten: .md, .txt", + "import_error": "Sommige bestanden konden niet worden geïmporteerd.", + "import_notes_ready": "{count} notitie(s) geïmporteerd!", + "action_write_title": "Schrijf uw eerste echte notitie", + "action_write_desc": "Maak een notitie en leg uw ideeën vast.", + "action_flashcards_title": "Maak uw eerste flashcards", + "action_flashcards_desc": "Open een notitie en klik op de flashcards-knop.", + "action_brainstorm_title": "Start een AI-brainstorm", + "action_brainstorm_desc": "Verken uw ideeën met een AI-agent.", + "action_try": "Proberen", + "step_features_cta_all": "Alles klaar — aan de slag!", + "action_write_where": "Sluit → klik op \"+ Nieuwe notitie\" in de zijbalk", + "action_flashcards_where": "Sluit → open een notitie → 🃏-knop in de werkbalk", + "action_brainstorm_where": "Sluit → \"Canvas\"-sectie in de zijbalk", + "pill_resume": "✨ Tour hervatten", + "action_done": "Geprobeerd!", + "editor_hints_title": "Editor tips", + "editor_hints_got_it": "Got it!", + "hint_slash_title": "\"/\" command — insert blocks", + "hint_slash_desc": "In the editor, type \"/\" to open the block menu: heading, list, code block, table, to-do list, and AI commands (Clarify, Shorten, Improve, Expand).", + "hint_ai_title": "Built-in AI assistant", + "hint_ai_desc": "Click the ✨ button in the toolbar to open the AI panel — ask questions, summarize, rewrite, or brainstorm directly in your note.", + "hint_version_title": "Version history", + "hint_version_desc": "Click the ⓘ button in the toolbar → \"Versions\" tab. Enable versioning, then save and restore snapshots of your note at any time.", + "hint_flashcards_title": "Generate flashcards", + "hint_flashcards_desc": "Click the 🎓 button in the toolbar to auto-generate flashcards from your note for spaced repetition review.", + "hint_links_title": "Links between notes", + "hint_links_desc": "Type \"[[\" in the editor to search and link to another note. Linked notes appear as backlinks at the bottom of the note.", + "hint_create_note_title": "Create a note", + "hint_create_note_desc": "Click the \"+\" button in the sidebar or press Ctrl+N to create a new note. Then start writing.", + "hint_flip_title": "Flip the card", + "hint_flip_desc": "Press Space (or click the card) to flip it and reveal the answer.", + "hint_rate_keys_title": "Rate with keyboard", + "hint_rate_keys_desc": "After flipping, press 1 (Hard), 2 (Difficult), 3 (Good) or 4 (Easy) to rate the card. The SM-2 algorithm schedules your next review automatically.", + "hint_generate_from_note_title": "Generate from a note", + "hint_generate_from_note_desc": "Open any note and click the 🎓 button in the toolbar to automatically generate flashcards from its content.", + "hint_brainstorm_start_title": "Start with an idea", + "hint_brainstorm_start_desc": "Type any concept or question in the input field and press Enter. The AI will generate a set of ideas around it.", + "hint_brainstorm_deepen_title": "Deepen an idea", + "hint_brainstorm_deepen_desc": "Click on any idea card to expand it with sub-ideas and explore it further.", + "hint_brainstorm_export_title": "Export your session", + "hint_brainstorm_export_desc": "When done, export the entire brainstorm session as a structured note saved to your notebook.", + "hint_insights_clusters_title": "Note clusters", + "hint_insights_clusters_desc": "Your notes are automatically grouped into thematic clusters. Click a cluster to explore the notes it contains.", + "hint_insights_bridge_title": "Bridge notes", + "hint_insights_bridge_desc": "Bridge notes connect multiple clusters. They are highlighted because they hold your knowledge graph together.", + "hint_insights_refresh_title": "Refresh clusters", + "hint_insights_refresh_desc": "If you've added new notes, click the refresh button to recalculate the clusters with the latest content." } -} +} \ No newline at end of file diff --git a/memento-note/locales/pl.json b/memento-note/locales/pl.json index 5cff893..0f0d18b 100644 --- a/memento-note/locales/pl.json +++ b/memento-note/locales/pl.json @@ -2176,7 +2176,12 @@ "Italiano": "Włoski", "Chinois": "Chiński", "Japonais": "Japoński" - } + }, + "exportMarkdown": "Eksportuj jako Markdown", + "importMarkdown": "Importuj Markdown", + "markdownExportSuccess": "Notatka wyeksportowana jako Markdown", + "markdownExportError": "Eksport notatki nie powiódł się", + "markdownImportSuccess": "Markdown zaimportowany pomyślnie" }, "brainstorm": { "title": "Waves of Thought", @@ -2306,7 +2311,16 @@ "ownerBadge": "Owner", "waveBadge": "Wave {wave}", "quotaGuest": "Gospodarz sesji wyczerpał limit AI. Poproś go o ulepszenie planu.", - "quotaHost": "Osiągnąłeś limit AI dla tego brainstormu. Ulepsz plan, aby kontynuować." + "quotaHost": "Osiągnąłeś limit AI dla tego brainstormu. Ulepsz plan, aby kontynuować.", + "downloadPptx": "PPTX", + "downloadPptxDesc": "Pobierz jako PowerPoint", + "pptxSuccess": "PPTX pobrany", + "pptxError": "Eksport PPTX nie powiódł się", + "fitToScreen": "Wycentruj", + "legendWave1": "Wariacje", + "legendWave2": "Analogie", + "legendWave3": "Zakłócenia", + "legendConverted": "Przekonwertowane" }, "usageMeter": { "packName": "AI Discovery Pack", @@ -2762,5 +2776,96 @@ "uploadError": "Błąd przesyłania", "uploadFailed": "Przesyłanie nie powiodło się", "uploading": "Przesyłanie..." + }, + "onboarding": { + "welcome_title": "Twoja pamięć wspomagana AI", + "welcome_subtitle": "Momento pamięta to, co zapominasz.", + "welcome_cta": "Zacznij", + "skip": "Pomiń", + "step_notes_title": "Twoje notatki", + "step_notes_empty": "Nie masz jeszcze notatek. Zaimportuj swoje lub zacznij od przykładów.", + "step_notes_import": "Importuj moje notatki", + "step_notes_demo": "Utwórz 5 przykładowych notatek", + "step_notes_has_notes": "Masz już {count} notatek. Odkryjmy magię.", + "step_notes_cta": "Moje notatki są gotowe", + "step_aha_title": "Znajdź to, co zapomniałeś", + "step_aha_subtitle": "Zadaj pytanie. Znajdź zapomnianą notatkę.", + "step_aha_placeholder": "notatki o produktywności...", + "step_aha_cta": "Eksploruj Momento", + "progress": "{current} z {total}", + "creating_demo_notes": "Tworzenie przykładowych notatek...", + "demo_notes_ready": "5 przykładowych notatek utworzonych!", + "badge_credits": "⚡ Pozostało {count} kredytów", + "badge_upgrade": "Przejdź na Pro →", + "no_results": "Brak wyników — spróbuj innego zapytania.", + "search_credit_used": "1 wyszukiwanie użyte", + "quota_exceeded": "Limit wyszukiwań osiągnięty — przejdź na Pro.", + "step_aha_search_button": "Szukaj", + "step_aha_search_aria": "Szukaj w notatkach", + "step_notes_hint": "💡 Te notatki zasilą demo wyszukiwania AI w następnym kroku.", + "step_features_title": "Twoje supermoce AI", + "step_features_subtitle": "Wybierz, od czego zacząć.", + "step_features_cta": "Zaczynamy!", + "feature_search_title": "Wyszukiwanie semantyczne", + "feature_search_desc": "Znajdź każdą notatkę według znaczenia, nie tylko słów kluczowych.", + "feature_flashcards_title": "Fiszki AI", + "feature_flashcards_desc": "Generuj karty powtórek SRS z notatek jednym kliknięciem.", + "feature_brainstorm_title": "Burza mózgów AI", + "feature_brainstorm_desc": "Sesje wspólnej burzy mózgów wspomaganej przez AI.", + "feature_chat_title": "Czatuj z notatkami", + "feature_chat_desc": "Zadawaj pytania swojej osobistej bazie wiedzy.", + "feature_insights_title": "Spostrzeżenia semantyczne", + "feature_insights_desc": "Odkryj ukryte powiązania między swoimi pomysłami.", + "feature_export_title": "Eksport Markdown", + "feature_export_desc": "Importuj i eksportuj notatki w formacie Markdown.", + "welcome_title_name": "Cześć {name} 👋", + "import_formats": "Akceptowane formaty: .md, .txt", + "import_error": "Nie można zaimportować niektórych plików. Spróbuj ponownie.", + "import_notes_ready": "{count} notatka/notatki zaimportowana/e!", + "action_write_title": "Napisz swoją pierwszą prawdziwą notatkę", + "action_write_desc": "Utwórz notatkę i zacznij zapisywać swoje pomysły.", + "action_flashcards_title": "Wygeneruj pierwsze fiszki", + "action_flashcards_desc": "Otwórz notatkę i kliknij przycisk fiszek.", + "action_brainstorm_title": "Rozpocznij burzę mózgów AI", + "action_brainstorm_desc": "Odkrywaj swoje pomysły z agentem AI.", + "action_try": "Spróbuj", + "step_features_cta_all": "Wszystko gotowe — zaczynamy!", + "action_write_where": "Zamknij → kliknij \"+ Nowa notatka\" na pasku bocznym", + "action_flashcards_where": "Zamknij → otwórz notatkę → przycisk 🃏 na pasku", + "action_brainstorm_where": "Zamknij → sekcja \"Canvas\" na pasku bocznym", + "pill_resume": "✨ Wznów tour", + "action_done": "Wypróbowano!", + "editor_hints_title": "Editor tips", + "editor_hints_got_it": "Got it!", + "hint_slash_title": "\"/\" command — insert blocks", + "hint_slash_desc": "In the editor, type \"/\" to open the block menu: heading, list, code block, table, to-do list, and AI commands (Clarify, Shorten, Improve, Expand).", + "hint_ai_title": "Built-in AI assistant", + "hint_ai_desc": "Click the ✨ button in the toolbar to open the AI panel — ask questions, summarize, rewrite, or brainstorm directly in your note.", + "hint_version_title": "Version history", + "hint_version_desc": "Click the ⓘ button in the toolbar → \"Versions\" tab. Enable versioning, then save and restore snapshots of your note at any time.", + "hint_flashcards_title": "Generate flashcards", + "hint_flashcards_desc": "Click the 🎓 button in the toolbar to auto-generate flashcards from your note for spaced repetition review.", + "hint_links_title": "Links between notes", + "hint_links_desc": "Type \"[[\" in the editor to search and link to another note. Linked notes appear as backlinks at the bottom of the note.", + "hint_create_note_title": "Create a note", + "hint_create_note_desc": "Click the \"+\" button in the sidebar or press Ctrl+N to create a new note. Then start writing.", + "hint_flip_title": "Flip the card", + "hint_flip_desc": "Press Space (or click the card) to flip it and reveal the answer.", + "hint_rate_keys_title": "Rate with keyboard", + "hint_rate_keys_desc": "After flipping, press 1 (Hard), 2 (Difficult), 3 (Good) or 4 (Easy) to rate the card. The SM-2 algorithm schedules your next review automatically.", + "hint_generate_from_note_title": "Generate from a note", + "hint_generate_from_note_desc": "Open any note and click the 🎓 button in the toolbar to automatically generate flashcards from its content.", + "hint_brainstorm_start_title": "Start with an idea", + "hint_brainstorm_start_desc": "Type any concept or question in the input field and press Enter. The AI will generate a set of ideas around it.", + "hint_brainstorm_deepen_title": "Deepen an idea", + "hint_brainstorm_deepen_desc": "Click on any idea card to expand it with sub-ideas and explore it further.", + "hint_brainstorm_export_title": "Export your session", + "hint_brainstorm_export_desc": "When done, export the entire brainstorm session as a structured note saved to your notebook.", + "hint_insights_clusters_title": "Note clusters", + "hint_insights_clusters_desc": "Your notes are automatically grouped into thematic clusters. Click a cluster to explore the notes it contains.", + "hint_insights_bridge_title": "Bridge notes", + "hint_insights_bridge_desc": "Bridge notes connect multiple clusters. They are highlighted because they hold your knowledge graph together.", + "hint_insights_refresh_title": "Refresh clusters", + "hint_insights_refresh_desc": "If you've added new notes, click the refresh button to recalculate the clusters with the latest content." } -} +} \ No newline at end of file diff --git a/memento-note/locales/pt.json b/memento-note/locales/pt.json index 283311e..7486f3f 100644 --- a/memento-note/locales/pt.json +++ b/memento-note/locales/pt.json @@ -2176,7 +2176,12 @@ "Italiano": "Italiano", "Chinois": "Chinês", "Japonais": "Japonês" - } + }, + "exportMarkdown": "Exportar como Markdown", + "importMarkdown": "Importar Markdown", + "markdownExportSuccess": "Nota exportada como Markdown", + "markdownExportError": "Falha ao exportar a nota", + "markdownImportSuccess": "Markdown importado com sucesso" }, "brainstorm": { "title": "Waves of Thought", @@ -2306,7 +2311,16 @@ "ownerBadge": "Owner", "waveBadge": "Wave {wave}", "quotaGuest": "O anfitrião da sessão atingiu o limite de IA. Peça-lhe para atualizar o plano.", - "quotaHost": "Atingiu o limite de IA deste brainstorm. Atualize o plano para continuar." + "quotaHost": "Atingiu o limite de IA deste brainstorm. Atualize o plano para continuar.", + "downloadPptx": "PPTX", + "downloadPptxDesc": "Baixar como PowerPoint", + "pptxSuccess": "PPTX baixado", + "pptxError": "Falha ao exportar PPTX", + "fitToScreen": "Recentralizar", + "legendWave1": "Variações", + "legendWave2": "Analogias", + "legendWave3": "Disrupções", + "legendConverted": "Convertida" }, "usageMeter": { "packName": "AI Discovery Pack", @@ -2762,5 +2776,96 @@ "uploadError": "Erro ao enviar", "uploadFailed": "Falha no envio", "uploading": "Enviando..." + }, + "onboarding": { + "welcome_title": "Sua memória aumentada por IA", + "welcome_subtitle": "Momento lembra o que você esquece.", + "welcome_cta": "Começar", + "skip": "Pular", + "step_notes_title": "Suas notas", + "step_notes_empty": "Você ainda não tem notas. Importe as suas ou comece com exemplos.", + "step_notes_import": "Importar minhas notas", + "step_notes_demo": "Criar 5 notas de exemplo", + "step_notes_has_notes": "Você já tem {count} notas. Vamos descobrir a magia.", + "step_notes_cta": "Minhas notas estão prontas", + "step_aha_title": "Encontre o que você esqueceu", + "step_aha_subtitle": "Faça uma pergunta. Encontre uma nota esquecida.", + "step_aha_placeholder": "notas sobre produtividade...", + "step_aha_cta": "Explorar Momento", + "progress": "{current} de {total}", + "creating_demo_notes": "Criando notas de exemplo...", + "demo_notes_ready": "5 notas de exemplo criadas!", + "badge_credits": "⚡ {count} créditos restantes", + "badge_upgrade": "Atualizar para Pro →", + "no_results": "Sem resultados — tente outra pesquisa.", + "search_credit_used": "1 pesquisa utilizada", + "quota_exceeded": "Cota de pesquisa atingida — atualize para Pro.", + "step_aha_search_button": "Pesquisar", + "step_aha_search_aria": "Pesquisar nas suas notas", + "step_notes_hint": "💡 Estas notas alimentarão a demonstração de busca IA no próximo passo.", + "step_features_title": "Seus superpoderes de IA", + "step_features_subtitle": "Escolha por onde começar.", + "step_features_cta": "Vamos lá!", + "feature_search_title": "Busca semântica", + "feature_search_desc": "Encontre qualquer nota por significado, não apenas por palavras-chave.", + "feature_flashcards_title": "Flashcards IA", + "feature_flashcards_desc": "Gere cartões de revisão SRS das suas notas com um clique.", + "feature_brainstorm_title": "Brainstorming IA", + "feature_brainstorm_desc": "Sessões de brainstorming colaborativo com IA.", + "feature_chat_title": "Converse com suas notas", + "feature_chat_desc": "Faça perguntas à sua base de conhecimento pessoal.", + "feature_insights_title": "Insights semânticos", + "feature_insights_desc": "Descubra conexões ocultas entre suas ideias.", + "feature_export_title": "Exportação Markdown", + "feature_export_desc": "Importe e exporte suas notas em formato Markdown padrão.", + "welcome_title_name": "Olá {name} 👋", + "import_formats": "Formatos aceites: .md, .txt", + "import_error": "Não foi possível importar alguns ficheiros. Tente novamente.", + "import_notes_ready": "{count} nota(s) importada(s)!", + "action_write_title": "Escreva sua primeira nota real", + "action_write_desc": "Crie uma nota e comece a capturar suas ideias.", + "action_flashcards_title": "Gere seus primeiros flashcards", + "action_flashcards_desc": "Abra uma nota e clique no botão flashcards.", + "action_brainstorm_title": "Inicie um brainstorm IA", + "action_brainstorm_desc": "Explore suas ideias com um agente IA.", + "action_try": "Tentar", + "step_features_cta_all": "Tudo pronto — vamos mergulhar!", + "action_write_where": "Feche → clique em \"+ Nova nota\" na barra lateral", + "action_flashcards_where": "Feche → abra uma nota → botão 🃏 na barra", + "action_brainstorm_where": "Feche → seção \"Canvas\" na barra lateral", + "pill_resume": "✨ Retomar visita", + "action_done": "Testado!", + "editor_hints_title": "Editor tips", + "editor_hints_got_it": "Got it!", + "hint_slash_title": "\"/\" command — insert blocks", + "hint_slash_desc": "In the editor, type \"/\" to open the block menu: heading, list, code block, table, to-do list, and AI commands (Clarify, Shorten, Improve, Expand).", + "hint_ai_title": "Built-in AI assistant", + "hint_ai_desc": "Click the ✨ button in the toolbar to open the AI panel — ask questions, summarize, rewrite, or brainstorm directly in your note.", + "hint_version_title": "Version history", + "hint_version_desc": "Click the ⓘ button in the toolbar → \"Versions\" tab. Enable versioning, then save and restore snapshots of your note at any time.", + "hint_flashcards_title": "Generate flashcards", + "hint_flashcards_desc": "Click the 🎓 button in the toolbar to auto-generate flashcards from your note for spaced repetition review.", + "hint_links_title": "Links between notes", + "hint_links_desc": "Type \"[[\" in the editor to search and link to another note. Linked notes appear as backlinks at the bottom of the note.", + "hint_create_note_title": "Create a note", + "hint_create_note_desc": "Click the \"+\" button in the sidebar or press Ctrl+N to create a new note. Then start writing.", + "hint_flip_title": "Flip the card", + "hint_flip_desc": "Press Space (or click the card) to flip it and reveal the answer.", + "hint_rate_keys_title": "Rate with keyboard", + "hint_rate_keys_desc": "After flipping, press 1 (Hard), 2 (Difficult), 3 (Good) or 4 (Easy) to rate the card. The SM-2 algorithm schedules your next review automatically.", + "hint_generate_from_note_title": "Generate from a note", + "hint_generate_from_note_desc": "Open any note and click the 🎓 button in the toolbar to automatically generate flashcards from its content.", + "hint_brainstorm_start_title": "Start with an idea", + "hint_brainstorm_start_desc": "Type any concept or question in the input field and press Enter. The AI will generate a set of ideas around it.", + "hint_brainstorm_deepen_title": "Deepen an idea", + "hint_brainstorm_deepen_desc": "Click on any idea card to expand it with sub-ideas and explore it further.", + "hint_brainstorm_export_title": "Export your session", + "hint_brainstorm_export_desc": "When done, export the entire brainstorm session as a structured note saved to your notebook.", + "hint_insights_clusters_title": "Note clusters", + "hint_insights_clusters_desc": "Your notes are automatically grouped into thematic clusters. Click a cluster to explore the notes it contains.", + "hint_insights_bridge_title": "Bridge notes", + "hint_insights_bridge_desc": "Bridge notes connect multiple clusters. They are highlighted because they hold your knowledge graph together.", + "hint_insights_refresh_title": "Refresh clusters", + "hint_insights_refresh_desc": "If you've added new notes, click the refresh button to recalculate the clusters with the latest content." } -} +} \ No newline at end of file diff --git a/memento-note/locales/ru.json b/memento-note/locales/ru.json index 4475987..b2524c1 100644 --- a/memento-note/locales/ru.json +++ b/memento-note/locales/ru.json @@ -2176,7 +2176,12 @@ "Italiano": "Итальянский", "Chinois": "Китайский", "Japonais": "Японский" - } + }, + "exportMarkdown": "Экспорт в Markdown", + "importMarkdown": "Импорт Markdown", + "markdownExportSuccess": "Заметка экспортирована в Markdown", + "markdownExportError": "Не удалось экспортировать заметку", + "markdownImportSuccess": "Markdown успешно импортирован" }, "brainstorm": { "title": "Waves of Thought", @@ -2306,7 +2311,16 @@ "ownerBadge": "Owner", "waveBadge": "Wave {wave}", "quotaGuest": "Организатор сессии исчерпал лимит ИИ. Попросите его обновить тариф.", - "quotaHost": "Вы исчерпали лимит ИИ для этого мозгового штурма. Обновите тариф, чтобы продолжить." + "quotaHost": "Вы исчерпали лимит ИИ для этого мозгового штурма. Обновите тариф, чтобы продолжить.", + "downloadPptx": "PPTX", + "downloadPptxDesc": "Скачать как PowerPoint", + "pptxSuccess": "PPTX скачан", + "pptxError": "Ошибка экспорта PPTX", + "fitToScreen": "Центрировать", + "legendWave1": "Вариации", + "legendWave2": "Аналогии", + "legendWave3": "Нарушения", + "legendConverted": "Конвертировано" }, "usageMeter": { "packName": "AI Discovery Pack", @@ -2762,5 +2776,96 @@ "uploadError": "Ошибка загрузки", "uploadFailed": "Загрузка не удалась", "uploading": "Загрузка..." + }, + "onboarding": { + "welcome_title": "Ваша память, усиленная ИИ", + "welcome_subtitle": "Momento помнит то, что вы забываете.", + "welcome_cta": "Начать", + "skip": "Пропустить", + "step_notes_title": "Ваши заметки", + "step_notes_empty": "У вас ещё нет заметок. Импортируйте свои или начните с примеров.", + "step_notes_import": "Импортировать заметки", + "step_notes_demo": "Создать 5 примеров заметок", + "step_notes_has_notes": "У вас уже {count} заметок. Давайте откроем магию.", + "step_notes_cta": "Мои заметки готовы", + "step_aha_title": "Найдите то, что забыли", + "step_aha_subtitle": "Задайте вопрос. Найдите забытую заметку.", + "step_aha_placeholder": "заметки о продуктивности...", + "step_aha_cta": "Исследовать Momento", + "progress": "{current} из {total}", + "creating_demo_notes": "Создание примеров заметок...", + "demo_notes_ready": "5 примеров заметок создано!", + "badge_credits": "⚡ Осталось {count} кредитов", + "badge_upgrade": "Перейти на Pro →", + "no_results": "Нет результатов — попробуйте другой запрос.", + "search_credit_used": "1 поиск использован", + "quota_exceeded": "Лимит поиска исчерпан — перейдите на Pro.", + "step_aha_search_button": "Искать", + "step_aha_search_aria": "Поиск по заметкам", + "step_notes_hint": "💡 Эти заметки обеспечат демонстрацию ИИ-поиска на следующем шаге.", + "step_features_title": "Ваши суперспособности ИИ", + "step_features_subtitle": "Выберите, с чего начать.", + "step_features_cta": "Поехали!", + "feature_search_title": "Семантический поиск", + "feature_search_desc": "Находите любую заметку по смыслу, а не только по ключевым словам.", + "feature_flashcards_title": "Карточки ИИ", + "feature_flashcards_desc": "Создавайте карточки для повторения SRS из заметок одним кликом.", + "feature_brainstorm_title": "Мозговой штурм ИИ", + "feature_brainstorm_desc": "Совместные сессии мозгового штурма с поддержкой ИИ.", + "feature_chat_title": "Чат с заметками", + "feature_chat_desc": "Задавайте вопросы своей личной базе знаний.", + "feature_insights_title": "Семантические инсайты", + "feature_insights_desc": "Откройте скрытые связи между вашими идеями.", + "feature_export_title": "Экспорт Markdown", + "feature_export_desc": "Импортируйте и экспортируйте заметки в формате Markdown.", + "welcome_title_name": "Привет, {name} 👋", + "import_formats": "Поддерживаемые форматы: .md, .txt", + "import_error": "Не удалось импортировать некоторые файлы. Попробуйте снова.", + "import_notes_ready": "{count} заметка(и) импортирована(ы)!", + "action_write_title": "Напишите первую настоящую заметку", + "action_write_desc": "Создайте заметку и начните фиксировать идеи.", + "action_flashcards_title": "Создайте первые карточки", + "action_flashcards_desc": "Откройте заметку и нажмите кнопку карточек.", + "action_brainstorm_title": "Запустите ИИ-мозговой штурм", + "action_brainstorm_desc": "Исследуйте идеи с ИИ-агентом.", + "action_try": "Попробовать", + "step_features_cta_all": "Всё готово — вперёд!", + "action_write_where": "Закройте → нажмите \"+ Новая заметка\" в боковой панели", + "action_flashcards_where": "Закройте → откройте заметку → кнопка 🃏 в панели", + "action_brainstorm_where": "Закройте → раздел \"Canvas\" в боковой панели", + "pill_resume": "✨ Продолжить тур", + "action_done": "Готово!", + "editor_hints_title": "Editor tips", + "editor_hints_got_it": "Got it!", + "hint_slash_title": "\"/\" command — insert blocks", + "hint_slash_desc": "In the editor, type \"/\" to open the block menu: heading, list, code block, table, to-do list, and AI commands (Clarify, Shorten, Improve, Expand).", + "hint_ai_title": "Built-in AI assistant", + "hint_ai_desc": "Click the ✨ button in the toolbar to open the AI panel — ask questions, summarize, rewrite, or brainstorm directly in your note.", + "hint_version_title": "Version history", + "hint_version_desc": "Click the ⓘ button in the toolbar → \"Versions\" tab. Enable versioning, then save and restore snapshots of your note at any time.", + "hint_flashcards_title": "Generate flashcards", + "hint_flashcards_desc": "Click the 🎓 button in the toolbar to auto-generate flashcards from your note for spaced repetition review.", + "hint_links_title": "Links between notes", + "hint_links_desc": "Type \"[[\" in the editor to search and link to another note. Linked notes appear as backlinks at the bottom of the note.", + "hint_create_note_title": "Create a note", + "hint_create_note_desc": "Click the \"+\" button in the sidebar or press Ctrl+N to create a new note. Then start writing.", + "hint_flip_title": "Flip the card", + "hint_flip_desc": "Press Space (or click the card) to flip it and reveal the answer.", + "hint_rate_keys_title": "Rate with keyboard", + "hint_rate_keys_desc": "After flipping, press 1 (Hard), 2 (Difficult), 3 (Good) or 4 (Easy) to rate the card. The SM-2 algorithm schedules your next review automatically.", + "hint_generate_from_note_title": "Generate from a note", + "hint_generate_from_note_desc": "Open any note and click the 🎓 button in the toolbar to automatically generate flashcards from its content.", + "hint_brainstorm_start_title": "Start with an idea", + "hint_brainstorm_start_desc": "Type any concept or question in the input field and press Enter. The AI will generate a set of ideas around it.", + "hint_brainstorm_deepen_title": "Deepen an idea", + "hint_brainstorm_deepen_desc": "Click on any idea card to expand it with sub-ideas and explore it further.", + "hint_brainstorm_export_title": "Export your session", + "hint_brainstorm_export_desc": "When done, export the entire brainstorm session as a structured note saved to your notebook.", + "hint_insights_clusters_title": "Note clusters", + "hint_insights_clusters_desc": "Your notes are automatically grouped into thematic clusters. Click a cluster to explore the notes it contains.", + "hint_insights_bridge_title": "Bridge notes", + "hint_insights_bridge_desc": "Bridge notes connect multiple clusters. They are highlighted because they hold your knowledge graph together.", + "hint_insights_refresh_title": "Refresh clusters", + "hint_insights_refresh_desc": "If you've added new notes, click the refresh button to recalculate the clusters with the latest content." } -} +} \ No newline at end of file diff --git a/memento-note/locales/zh.json b/memento-note/locales/zh.json index b7a53c5..e635af4 100644 --- a/memento-note/locales/zh.json +++ b/memento-note/locales/zh.json @@ -2176,7 +2176,12 @@ "Italiano": "意大利语", "Chinois": "中文", "Japonais": "日语" - } + }, + "exportMarkdown": "导出为 Markdown", + "importMarkdown": "导入 Markdown", + "markdownExportSuccess": "笔记已导出为 Markdown", + "markdownExportError": "笔记导出失败", + "markdownImportSuccess": "Markdown 导入成功" }, "brainstorm": { "title": "Waves of Thought", @@ -2306,7 +2311,16 @@ "ownerBadge": "Owner", "waveBadge": "Wave {wave}", "quotaGuest": "会话主持人已达到 AI 额度上限。请让对方升级套餐。", - "quotaHost": "您已达到此头脑风暴的 AI 额度上限。升级套餐以继续。" + "quotaHost": "您已达到此头脑风暴的 AI 额度上限。升级套餐以继续。", + "downloadPptx": "PPTX", + "downloadPptxDesc": "下载为 PowerPoint", + "pptxSuccess": "PPTX 已下载", + "pptxError": "PPTX 导出失败", + "fitToScreen": "重新居中", + "legendWave1": "变体", + "legendWave2": "类比", + "legendWave3": "颠覆", + "legendConverted": "已转换" }, "usageMeter": { "packName": "AI Discovery Pack", @@ -2762,5 +2776,96 @@ "uploadError": "上传错误", "uploadFailed": "上传失败", "uploading": "上传中..." + }, + "onboarding": { + "welcome_title": "您的AI增强记忆", + "welcome_subtitle": "Momento记住您忘记的事情。", + "welcome_cta": "开始", + "skip": "跳过", + "step_notes_title": "您的笔记", + "step_notes_empty": "您还没有笔记。导入您的笔记或从示例开始。", + "step_notes_import": "导入我的笔记", + "step_notes_demo": "创建5个示例笔记", + "step_notes_has_notes": "您已有{count}条笔记。让我们发现魔力。", + "step_notes_cta": "我的笔记已准备好", + "step_aha_title": "找到您忘记的内容", + "step_aha_subtitle": "提问。找到您遗忘的笔记。", + "step_aha_placeholder": "关于生产力的笔记...", + "step_aha_cta": "探索Momento", + "progress": "{current}/{total}", + "creating_demo_notes": "正在创建示例笔记...", + "demo_notes_ready": "已创建5个示例笔记!", + "badge_credits": "⚡ 剩余 {count} 积分", + "badge_upgrade": "升级到 Pro →", + "no_results": "无结果 — 请尝试其他查询。", + "search_credit_used": "已使用 1 次搜索", + "quota_exceeded": "搜索配额已用完 — 升级到 Pro。", + "step_aha_search_button": "搜索", + "step_aha_search_aria": "搜索您的笔记", + "step_notes_hint": "💡 这些笔记将在下一步为 AI 搜索演示提供支持。", + "step_features_title": "您的 AI 超能力", + "step_features_subtitle": "选择从哪里开始。", + "step_features_cta": "开始吧!", + "feature_search_title": "语义搜索", + "feature_search_desc": "按含义查找任何笔记,而不仅仅是关键词。", + "feature_flashcards_title": "AI 闪卡", + "feature_flashcards_desc": "一键从笔记生成 SRS 复习卡片。", + "feature_brainstorm_title": "AI 头脑风暴", + "feature_brainstorm_desc": "AI 驱动的协作头脑风暴会话。", + "feature_chat_title": "与笔记对话", + "feature_chat_desc": "向您的个人知识库提问。", + "feature_insights_title": "语义洞察", + "feature_insights_desc": "发现您想法之间隐藏的联系。", + "feature_export_title": "Markdown 导出", + "feature_export_desc": "以标准 Markdown 格式导入和导出笔记。", + "welcome_title_name": "你好 {name} 👋", + "import_formats": "支持格式:.md, .txt", + "import_error": "部分文件无法导入,请重试。", + "import_notes_ready": "已导入 {count} 篇笔记!", + "action_write_title": "写下您的第一篇真实笔记", + "action_write_desc": "创建笔记,开始记录您的想法。", + "action_flashcards_title": "生成您的第一批闪卡", + "action_flashcards_desc": "打开笔记,点击闪卡按钮。", + "action_brainstorm_title": "开始AI头脑风暴", + "action_brainstorm_desc": "与AI代理探索您的想法。", + "action_try": "试试", + "step_features_cta_all": "全部完成——开始吧!", + "action_write_where": "关闭此窗口 → 点击侧边栏\"+ 新建笔记\"", + "action_flashcards_where": "关闭 → 打开笔记 → 工具栏中的 🃏 按钮", + "action_brainstorm_where": "关闭 → 侧边栏\"Canvas\"区域", + "pill_resume": "✨ 继续引导", + "action_done": "已试用", + "editor_hints_title": "Editor tips", + "editor_hints_got_it": "Got it!", + "hint_slash_title": "\"/\" command — insert blocks", + "hint_slash_desc": "In the editor, type \"/\" to open the block menu: heading, list, code block, table, to-do list, and AI commands (Clarify, Shorten, Improve, Expand).", + "hint_ai_title": "Built-in AI assistant", + "hint_ai_desc": "Click the ✨ button in the toolbar to open the AI panel — ask questions, summarize, rewrite, or brainstorm directly in your note.", + "hint_version_title": "Version history", + "hint_version_desc": "Click the ⓘ button in the toolbar → \"Versions\" tab. Enable versioning, then save and restore snapshots of your note at any time.", + "hint_flashcards_title": "Generate flashcards", + "hint_flashcards_desc": "Click the 🎓 button in the toolbar to auto-generate flashcards from your note for spaced repetition review.", + "hint_links_title": "Links between notes", + "hint_links_desc": "Type \"[[\" in the editor to search and link to another note. Linked notes appear as backlinks at the bottom of the note.", + "hint_create_note_title": "Create a note", + "hint_create_note_desc": "Click the \"+\" button in the sidebar or press Ctrl+N to create a new note. Then start writing.", + "hint_flip_title": "Flip the card", + "hint_flip_desc": "Press Space (or click the card) to flip it and reveal the answer.", + "hint_rate_keys_title": "Rate with keyboard", + "hint_rate_keys_desc": "After flipping, press 1 (Hard), 2 (Difficult), 3 (Good) or 4 (Easy) to rate the card. The SM-2 algorithm schedules your next review automatically.", + "hint_generate_from_note_title": "Generate from a note", + "hint_generate_from_note_desc": "Open any note and click the 🎓 button in the toolbar to automatically generate flashcards from its content.", + "hint_brainstorm_start_title": "Start with an idea", + "hint_brainstorm_start_desc": "Type any concept or question in the input field and press Enter. The AI will generate a set of ideas around it.", + "hint_brainstorm_deepen_title": "Deepen an idea", + "hint_brainstorm_deepen_desc": "Click on any idea card to expand it with sub-ideas and explore it further.", + "hint_brainstorm_export_title": "Export your session", + "hint_brainstorm_export_desc": "When done, export the entire brainstorm session as a structured note saved to your notebook.", + "hint_insights_clusters_title": "Note clusters", + "hint_insights_clusters_desc": "Your notes are automatically grouped into thematic clusters. Click a cluster to explore the notes it contains.", + "hint_insights_bridge_title": "Bridge notes", + "hint_insights_bridge_desc": "Bridge notes connect multiple clusters. They are highlighted because they hold your knowledge graph together.", + "hint_insights_refresh_title": "Refresh clusters", + "hint_insights_refresh_desc": "If you've added new notes, click the refresh button to recalculate the clusters with the latest content." } -} +} \ No newline at end of file diff --git a/memento-note/package-lock.json b/memento-note/package-lock.json index 9a06150..b8f2fae 100644 --- a/memento-note/package-lock.json +++ b/memento-note/package-lock.json @@ -66,10 +66,12 @@ "@tiptap/y-tiptap": "^3.0.3", "@types/d3": "^7.4.3", "@types/jsdom": "^28.0.1", + "@types/turndown": "^5.0.6", "ai": "^6.0.23", "autoprefixer": "^10.4.23", "bcryptjs": "^3.0.3", "buffer": "^6.0.3", + "canvas-confetti": "^1.9.4", "cheerio": "^1.1.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -118,6 +120,8 @@ "tinyld": "^1.3.4", "tiptap-extension-auto-joiner": "^0.1.3", "tiptap-extension-global-drag-handle": "^0.1.18", + "turndown": "^7.2.4", + "turndown-plugin-gfm": "^1.0.2", "vazirmatn": "^33.0.3", "y-protocols": "^1.0.7", "yjs": "^13.6.30", @@ -128,6 +132,7 @@ "@tailwindcss/postcss": "^4", "@tailwindcss/typography": "^0.5.19", "@types/bcryptjs": "^2.4.6", + "@types/canvas-confetti": "^1.9.0", "@types/dagre": "^0.7.54", "@types/diff": "^7.0.2", "@types/ioredis": "^4.28.10", @@ -2728,6 +2733,12 @@ "langium": "3.3.1" } }, + "node_modules/@mixmark-io/domino": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz", + "integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==", + "license": "BSD-2-Clause" + }, "node_modules/@mozilla/readability": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@mozilla/readability/-/readability-0.6.0.tgz", @@ -8270,6 +8281,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/canvas-confetti": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@types/canvas-confetti/-/canvas-confetti-1.9.0.tgz", + "integrity": "sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -8738,6 +8756,12 @@ "license": "MIT", "optional": true }, + "node_modules/@types/turndown": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/turndown/-/turndown-5.0.6.tgz", + "integrity": "sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg==", + "license": "MIT" + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -10165,6 +10189,16 @@ "node": ">=12" } }, + "node_modules/canvas-confetti": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.9.4.tgz", + "integrity": "sha512-yxQbJkAVrFXWNbTUjPqjF7G+g6pDotOUHGbkZq2NELZUMDpiJ85rIEazVb8GTaAptNW2miJAXbs1BtioA251Pw==", + "license": "ISC", + "funding": { + "type": "donate", + "url": "https://www.paypal.me/kirilvatev" + } + }, "node_modules/canvas-roundrect-polyfill": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/canvas-roundrect-polyfill/-/canvas-roundrect-polyfill-0.0.1.tgz", @@ -19065,6 +19099,25 @@ "zustand": "^4.3.2" } }, + "node_modules/turndown": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.4.tgz", + "integrity": "sha512-I8yFsfRzmzK0WV1pNNOA4A7y4RDfFxPRxb3t+e3ui14qSGOxGtiSP6GjeX+Y6CHb7HYaFj7ECUD7VE5kQMZWGQ==", + "license": "MIT", + "dependencies": { + "@mixmark-io/domino": "^2.2.0" + }, + "engines": { + "node": ">=18", + "npm": ">=9" + } + }, + "node_modules/turndown-plugin-gfm": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/turndown-plugin-gfm/-/turndown-plugin-gfm-1.0.2.tgz", + "integrity": "sha512-vwz9tfvF7XN/jE0dGoBei3FXWuvll78ohzCZQuOb+ZjWrs3a0XhQVomJEb2Qh4VHTPNRO4GPZh0V7VRbiWwkRg==", + "license": "MIT" + }, "node_modules/tw-animate-css": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", diff --git a/memento-note/package.json b/memento-note/package.json index 46dca82..f222314 100644 --- a/memento-note/package.json +++ b/memento-note/package.json @@ -87,10 +87,12 @@ "@tiptap/y-tiptap": "^3.0.3", "@types/d3": "^7.4.3", "@types/jsdom": "^28.0.1", + "@types/turndown": "^5.0.6", "ai": "^6.0.23", "autoprefixer": "^10.4.23", "bcryptjs": "^3.0.3", "buffer": "^6.0.3", + "canvas-confetti": "^1.9.4", "cheerio": "^1.1.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -139,6 +141,8 @@ "tinyld": "^1.3.4", "tiptap-extension-auto-joiner": "^0.1.3", "tiptap-extension-global-drag-handle": "^0.1.18", + "turndown": "^7.2.4", + "turndown-plugin-gfm": "^1.0.2", "vazirmatn": "^33.0.3", "y-protocols": "^1.0.7", "yjs": "^13.6.30", @@ -149,6 +153,7 @@ "@tailwindcss/postcss": "^4", "@tailwindcss/typography": "^0.5.19", "@types/bcryptjs": "^2.4.6", + "@types/canvas-confetti": "^1.9.0", "@types/dagre": "^0.7.54", "@types/diff": "^7.0.2", "@types/ioredis": "^4.28.10", diff --git a/memento-note/prisma/migrations/20260529060000_add_onboarding_fields/migration.sql b/memento-note/prisma/migrations/20260529060000_add_onboarding_fields/migration.sql new file mode 100644 index 0000000..d1d05b0 --- /dev/null +++ b/memento-note/prisma/migrations/20260529060000_add_onboarding_fields/migration.sql @@ -0,0 +1,8 @@ +-- AddColumn: User.onboardingCompleted +ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "onboardingCompleted" BOOLEAN NOT NULL DEFAULT false; + +-- AddColumn: User.onboardingStep +ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "onboardingStep" INTEGER NOT NULL DEFAULT 0; + +-- AddColumn: Note.isDemo +ALTER TABLE "Note" ADD COLUMN IF NOT EXISTS "isDemo" BOOLEAN NOT NULL DEFAULT false; diff --git a/memento-note/prisma/schema.prisma b/memento-note/prisma/schema.prisma index dd39ce4..2974527 100644 --- a/memento-note/prisma/schema.prisma +++ b/memento-note/prisma/schema.prisma @@ -26,6 +26,8 @@ model User { updatedAt DateTime @updatedAt cardSizeMode String @default("variable") accentColor String @default("#A47148") + onboardingCompleted Boolean @default(false) + onboardingStep Int @default(0) accounts Account[] agents Agent[] aiFeedback AiFeedback[] @@ -175,6 +177,8 @@ model Note { historyEnabled Boolean @default(false) /// URL d'origine pour les clips web (Web Clipper) sourceUrl String? + /// Note de démonstration insérée lors de l'onboarding + isDemo Boolean @default(false) /// Illustration SVG (sanitized) for editorial feed thumbnail — optional, peut être généré par IA illustrationSvg String? tsv Unsupported("tsvector")? diff --git a/memento-note/tests/unit/markdown-export.test.ts b/memento-note/tests/unit/markdown-export.test.ts new file mode 100644 index 0000000..b504a41 --- /dev/null +++ b/memento-note/tests/unit/markdown-export.test.ts @@ -0,0 +1,218 @@ +import { describe, test, expect } from 'vitest' +import { + tiptapHTMLToMarkdown, + markdownToHTML, + looksLikeMarkdown, + extractMarkdownTitle, +} from '../../lib/editor/markdown-export' + +describe('looksLikeMarkdown', () => { + test('detects H1 heading', () => { + expect(looksLikeMarkdown('# Hello World')).toBe(true) + }) + + test('detects unordered list', () => { + expect(looksLikeMarkdown('- Item one\n- Item two')).toBe(true) + expect(looksLikeMarkdown('* Item one')).toBe(true) + }) + + test('detects ordered list', () => { + expect(looksLikeMarkdown('1. First\n2. Second')).toBe(true) + }) + + test('detects blockquote', () => { + expect(looksLikeMarkdown('> This is a quote')).toBe(true) + }) + + test('detects code fence', () => { + expect(looksLikeMarkdown('```\nconst x = 1\n```')).toBe(true) + }) + + test('detects inline code', () => { + expect(looksLikeMarkdown('Use `console.log()` for debugging')).toBe(true) + }) + + test('detects bold', () => { + expect(looksLikeMarkdown('This is **bold** text')).toBe(true) + }) + + test('detects italic', () => { + expect(looksLikeMarkdown('This is *italic* text')).toBe(true) + }) + + test('detects table', () => { + expect(looksLikeMarkdown('| Col1 | Col2 |\n|------|------|')).toBe(true) + }) + + test('detects link', () => { + expect(looksLikeMarkdown('See [TipTap docs](https://tiptap.dev)')).toBe(true) + }) + + test('does NOT flag plain prose as Markdown', () => { + expect(looksLikeMarkdown('This is a normal sentence without any markdown.')).toBe(false) + expect(looksLikeMarkdown('Hello world, this is plain text.')).toBe(false) + }) + + test('does NOT flag very short text', () => { + expect(looksLikeMarkdown('Hi')).toBe(false) + expect(looksLikeMarkdown('')).toBe(false) + }) +}) + +describe('tiptapHTMLToMarkdown', () => { + test('converts H1 to # heading', () => { + const md = tiptapHTMLToMarkdown('

Hello

') + expect(md).toBe('# Hello') + }) + + test('converts H2 to ## heading', () => { + const md = tiptapHTMLToMarkdown('

Section

') + expect(md).toBe('## Section') + }) + + test('converts H3 to ### heading', () => { + const md = tiptapHTMLToMarkdown('

Sub

') + expect(md).toBe('### Sub') + }) + + test('converts bold text', () => { + const md = tiptapHTMLToMarkdown('

This is bold text.

') + expect(md).toContain('**bold**') + }) + + test('converts italic text', () => { + const md = tiptapHTMLToMarkdown('

This is italic text.

') + expect(md).toContain('_italic_') + }) + + test('converts unordered list', () => { + const md = tiptapHTMLToMarkdown('
  • Item 1
  • Item 2
') + expect(md).toContain('Item 1') + expect(md).toContain('Item 2') + expect(md).toMatch(/^[-*+]\s/m) + }) + + test('converts ordered list', () => { + const md = tiptapHTMLToMarkdown('
  1. First
  2. Second
') + expect(md).toContain('1. First') + expect(md).toContain('2. Second') + }) + + test('converts code block', () => { + const md = tiptapHTMLToMarkdown('
const x = 1;
') + expect(md).toContain('```') + expect(md).toContain('const x = 1;') + }) + + test('converts blockquote', () => { + const md = tiptapHTMLToMarkdown('

Quote text

') + expect(md).toContain('> Quote text') + }) + + test('converts inline code', () => { + const md = tiptapHTMLToMarkdown('

Use console.log() here.

') + expect(md).toContain('`console.log()`') + }) + + test('converts hyperlink', () => { + const md = tiptapHTMLToMarkdown('
') + expect(md).toContain('[Example](https://example.com)') + }) + + test('handles empty HTML', () => { + expect(tiptapHTMLToMarkdown('')).toBe('') + expect(tiptapHTMLToMarkdown(' ')).toBe('') + }) + + test('preserves liveBlock as HTML comment', () => { + const html = '
' + const md = tiptapHTMLToMarkdown(html) + expect(md).toContain('

Example

1#KZlHv;l(Tvejj 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 qQf6K_r-8-@61!~d$lD@( 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 literal 0 HcmV?d00001 diff --git a/memento-note/lib/brainstorm/export-pptx.ts b/memento-note/lib/brainstorm/export-pptx.ts new file mode 100644 index 0000000..692c82a --- /dev/null +++ b/memento-note/lib/brainstorm/export-pptx.ts @@ -0,0 +1,322 @@ +import type PptxGenJSModule from 'pptxgenjs' + +let _PptxGenJS: (new () => PptxGenJSModule) | null = null +async function getPptxGenClass(): Promise PptxGenJSModule> { + if (!_PptxGenJS) { + const mod = await import('pptxgenjs') + _PptxGenJS = (mod.default ?? mod) as unknown as new () => PptxGenJSModule + } + return _PptxGenJS +} + +// ── Theme — cohérent avec l'identité visuelle Momento ─────────────────────── +const T = { + bg: 'F2F0E9', + primary: '1C1C1C', + accent: 'A47148', + secondary: 'D4A373', + muted: '9A8C87', + wave1: 'fb923c', + wave2: '60a5fa', + wave3: 'a78bfa', + green: '10b981', +} + +const WAVE_LABELS: Record = { + 1: '🔄 Variations', + 2: '🔗 Analogies', + 3: '💥 Disruptions', +} + +const WAVE_COLORS: Record = { + 1: T.wave1, + 2: T.wave2, + 3: T.wave3, +} + +// ── Types (minimal subset) ────────────────────────────────────────────────── + +interface IdeaLike { + id: string + title: string + description: string + waveNumber: number + status: string + isStarred: boolean + convertedToNoteId: string | null +} + +interface SessionLike { + seedIdea: string + createdAt: Date + ideas: IdeaLike[] +} + +// ── Helpers ───────────────────────────────────────────────────────────────── + +function slugify(text: string): string { + return text + .toLowerCase() + .replace(/[^a-z0-9\s-]/g, '') + .replace(/\s+/g, '-') + .slice(0, 40) +} + +function truncate(text: string, max: number): string { + return text.length > max ? text.slice(0, max - 1) + '…' : text +} + +// Add a consistent slide background +function addBg(slide: any) { + slide.background = { color: T.bg } +} + +// Accent bar at the top of a slide +function addTopBar(slide: any, color: string = T.accent) { + slide.addShape('rect', { x: 0, y: 0, w: '100%', h: 0.12, fill: { color } }) +} + +// ── Slide builders ─────────────────────────────────────────────────────────── + +function buildCoverSlide(pres: PptxGenJSModule, session: SessionLike, stats: { total: number; converted: number; starred: number }) { + const slide = pres.addSlide() + addBg(slide) + + // Left accent panel + slide.addShape('rect', { x: 0, y: 0, w: 3.6, h: '100%', fill: { color: T.primary } }) + + // Brand label on accent panel + slide.addText('MOMENTO', { + x: 0.3, y: 0.4, w: 3.0, h: 0.4, + fontSize: 9, fontFace: 'Arial', color: T.accent, bold: true, + charSpacing: 4, align: 'left', + }) + + // Seed idea (big) on right + slide.addText(truncate(session.seedIdea, 80), { + x: 4.0, y: 1.2, w: 5.6, h: 2.4, + fontSize: 26, fontFace: 'Georgia', color: T.primary, + bold: false, align: 'left', valign: 'middle', wrap: true, + }) + + // Date + slide.addText(session.createdAt.toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric' }), { + x: 4.0, y: 4.0, w: 5.6, h: 0.4, + fontSize: 10, fontFace: 'Arial', color: T.muted, align: 'left', + }) + + // Stats row + const statItems = [ + { label: 'idées', value: String(stats.total) }, + { label: 'converties', value: String(stats.converted) }, + { label: 'favorites', value: String(stats.starred) }, + ] + statItems.forEach((s, i) => { + const x = 4.0 + i * 2.0 + slide.addText(s.value, { x, y: 4.8, w: 1.8, h: 0.55, fontSize: 22, fontFace: 'Georgia', color: T.accent, bold: true, align: 'left' }) + slide.addText(s.label, { x, y: 5.35, w: 1.8, h: 0.3, fontSize: 9, fontFace: 'Arial', color: T.muted, align: 'left', charSpacing: 1 }) + }) + + // Vertical label "BRAINSTORM" on left panel + slide.addText('BRAINSTORM', { + x: 0.1, y: 2.0, w: 3.2, h: 0.5, + fontSize: 11, fontFace: 'Arial', color: 'FFFFFF', bold: true, + charSpacing: 6, align: 'center', + rotate: 270, + }) +} + +function buildWaveSlide(pres: PptxGenJSModule, wave: number, ideas: IdeaLike[]) { + const slide = pres.addSlide() + addBg(slide) + addTopBar(slide, WAVE_COLORS[wave] || T.accent) + + // Wave label + slide.addText(WAVE_LABELS[wave] || `Wave ${wave}`, { + x: 0.5, y: 0.25, w: 9.0, h: 0.5, + fontSize: 14, fontFace: 'Arial', color: WAVE_COLORS[wave] || T.accent, bold: true, charSpacing: 2, + }) + + // Count badge + slide.addShape('roundRect', { x: 9.0, y: 0.28, w: 0.6, h: 0.4, fill: { color: WAVE_COLORS[wave] || T.accent }, rectRadius: 0.1 }) + slide.addText(String(ideas.length), { x: 9.0, y: 0.28, w: 0.6, h: 0.4, fontSize: 11, fontFace: 'Arial', color: 'FFFFFF', bold: true, align: 'center', valign: 'middle' }) + + const maxPerSlide = 6 + const shown = ideas.slice(0, maxPerSlide) + + const colW = 4.5 + const rowH = 1.3 + const startY = 1.0 + + shown.forEach((idea, i) => { + const col = i % 2 + const row = Math.floor(i / 2) + const x = 0.4 + col * (colW + 0.3) + const y = startY + row * (rowH + 0.15) + + // Card background + slide.addShape('roundRect', { x, y, w: colW, h: rowH, fill: { color: 'FFFFFF' }, line: { color: 'E8E6E0', width: 0.75 }, rectRadius: 0.08 }) + + // Star / converted badge + if (idea.isStarred || idea.convertedToNoteId) { + const badge = idea.convertedToNoteId ? '✓' : '⭐' + const badgeColor = idea.convertedToNoteId ? T.green : T.wave1 + slide.addText(badge, { x: x + colW - 0.45, y: y + 0.08, w: 0.35, h: 0.35, fontSize: 10, fontFace: 'Arial', color: badgeColor, align: 'center' }) + } + + // Title + slide.addText(truncate(idea.title, 55), { + x: x + 0.18, y: y + 0.12, w: colW - 0.55, h: 0.4, + fontSize: 11, fontFace: 'Arial', color: T.primary, bold: true, wrap: true, + }) + + // Description + slide.addText(truncate(idea.description, 120), { + x: x + 0.18, y: y + 0.52, w: colW - 0.36, h: 0.65, + fontSize: 9, fontFace: 'Arial', color: T.muted, wrap: true, valign: 'top', + }) + }) + + if (ideas.length > maxPerSlide) { + slide.addText(`+ ${ideas.length - maxPerSlide} autres idées`, { + x: 0.4, y: 5.5, w: 9.2, h: 0.3, + fontSize: 9, fontFace: 'Arial', color: T.muted, align: 'center', italic: true, + }) + } +} + +function buildTopIdeasSlide(pres: PptxGenJSModule, starred: IdeaLike[], converted: IdeaLike[]) { + const slide = pres.addSlide() + addBg(slide) + addTopBar(slide, T.accent) + + slide.addText('Top Idées', { + x: 0.5, y: 0.25, w: 9.0, h: 0.5, + fontSize: 14, fontFace: 'Arial', color: T.accent, bold: true, charSpacing: 2, + }) + + const all = [ + ...starred.map(i => ({ ...i, badge: '⭐', badgeColor: T.wave1 })), + ...converted.filter(i => !i.isStarred).map(i => ({ ...i, badge: '✓', badgeColor: T.green })), + ].slice(0, 6) + + if (all.length === 0) { + slide.addText('Aucune idée favorite ou convertie.', { + x: 0.5, y: 3.0, w: 9.0, h: 0.5, + fontSize: 12, fontFace: 'Georgia', color: T.muted, align: 'center', italic: true, + }) + return + } + + const colW = 4.5 + const rowH = 1.25 + + all.forEach((idea, i) => { + const col = i % 2 + const row = Math.floor(i / 2) + const x = 0.4 + col * (colW + 0.3) + const y = 1.1 + row * (rowH + 0.15) + + const waveColor = WAVE_COLORS[idea.waveNumber] || T.muted + + slide.addShape('roundRect', { x, y, w: colW, h: rowH, fill: { color: 'FFFFFF' }, line: { color: waveColor, width: 1.5 }, rectRadius: 0.08 }) + + slide.addText(idea.badge, { x: x + 0.1, y: y + 0.1, w: 0.4, h: 0.4, fontSize: 14, fontFace: 'Arial', color: idea.badgeColor, align: 'center' }) + + slide.addText(truncate(idea.title, 55), { + x: x + 0.55, y: y + 0.1, w: colW - 0.7, h: 0.4, + fontSize: 11, fontFace: 'Arial', color: T.primary, bold: true, wrap: true, + }) + + slide.addText(truncate(idea.description, 110), { + x: x + 0.18, y: y + 0.55, w: colW - 0.36, h: 0.6, + fontSize: 9, fontFace: 'Arial', color: T.muted, wrap: true, valign: 'top', + }) + }) +} + +function buildSummarySlide(pres: PptxGenJSModule, session: SessionLike, stats: { total: number; converted: number; starred: number; dismissed: number }) { + const slide = pres.addSlide() + addBg(slide) + + // Dark left panel + slide.addShape('rect', { x: 0, y: 0, w: '100%', h: '100%', fill: { color: T.primary } }) + + slide.addText('BILAN DE SESSION', { + x: 0.8, y: 0.8, w: 8.4, h: 0.6, + fontSize: 10, fontFace: 'Arial', color: T.accent, bold: true, charSpacing: 5, align: 'left', + }) + + slide.addText(truncate(session.seedIdea, 70), { + x: 0.8, y: 1.55, w: 8.4, h: 1.0, + fontSize: 20, fontFace: 'Georgia', color: 'FFFFFF', align: 'left', wrap: true, + }) + + // Divider + slide.addShape('rect', { x: 0.8, y: 2.8, w: 8.4, h: 0.02, fill: { color: T.accent } }) + + // Stats grid + const statCols = [ + { label: 'IDÉES GÉNÉRÉES', value: String(stats.total), color: T.wave2 }, + { label: 'CONVERTIES EN NOTES', value: String(stats.converted), color: T.green }, + { label: 'FAVORITES', value: String(stats.starred), color: T.wave1 }, + { label: 'REJETÉES', value: String(stats.dismissed), color: T.muted }, + ] + + statCols.forEach((s, i) => { + const x = 0.8 + i * 2.3 + slide.addText(s.value, { x, y: 3.2, w: 2.2, h: 0.9, fontSize: 36, fontFace: 'Georgia', color: s.color, bold: true, align: 'left' }) + slide.addText(s.label, { x, y: 4.1, w: 2.2, h: 0.5, fontSize: 8, fontFace: 'Arial', color: T.muted, align: 'left', charSpacing: 1.5, wrap: true }) + }) + + slide.addText('Généré par Momento', { + x: 0.8, y: 5.5, w: 8.4, h: 0.3, + fontSize: 8, fontFace: 'Arial', color: T.muted, align: 'right', italic: true, + }) +} + +// ── Main export function ───────────────────────────────────────────────────── + +export async function generateBrainstormPptx(session: SessionLike): Promise<{ buffer: Buffer; filename: string }> { + const PptxGenJS = await getPptxGenClass() + const pres = new PptxGenJS() + + pres.layout = 'LAYOUT_WIDE' + pres.author = 'Momento' + pres.subject = `Brainstorm: ${session.seedIdea}` + + const activeIdeas = session.ideas.filter(i => i.status !== 'dismissed') + const dismissedCount = session.ideas.filter(i => i.status === 'dismissed').length + const converted = activeIdeas.filter(i => i.convertedToNoteId !== null) + const starred = activeIdeas.filter(i => i.isStarred) + + const stats = { + total: activeIdeas.length, + converted: converted.length, + starred: starred.length, + dismissed: dismissedCount, + } + + // Slide 1 — Cover + buildCoverSlide(pres, session, stats) + + // Slides 2-4 — One per active wave + for (const wave of [1, 2, 3]) { + const waveIdeas = activeIdeas.filter(i => i.waveNumber === wave) + if (waveIdeas.length === 0) continue + buildWaveSlide(pres, wave, waveIdeas) + } + + // Slide N — Top ideas (starred + converted) + if (starred.length > 0 || converted.length > 0) { + buildTopIdeasSlide(pres, starred, converted) + } + + // Last slide — Summary + buildSummarySlide(pres, session, stats) + + const buffer = (await pres.write({ outputType: 'nodebuffer' })) as unknown as Buffer + const filename = `brainstorm-${slugify(session.seedIdea)}.pptx` + + return { buffer, filename } +} diff --git a/memento-note/lib/editor/markdown-export.ts b/memento-note/lib/editor/markdown-export.ts new file mode 100644 index 0000000..64d00e4 --- /dev/null +++ b/memento-note/lib/editor/markdown-export.ts @@ -0,0 +1,209 @@ +/** + * markdown-export.ts + * Utilities for TipTap HTML ↔ Markdown conversion. + * + * Uses: + * - turndown (+ turndown-plugin-gfm) : HTML → Markdown + * - marked : Markdown → HTML + */ + +import TurndownService from 'turndown' +import { tables, taskListItems, strikethrough } from 'turndown-plugin-gfm' +import { marked } from 'marked' + +// ── Markdown heuristic detection ──────────────────────────────────────────── + +const MARKDOWN_PATTERNS = [ + /^#{1,6}\s/m, // headings + /^\s*[-*+]\s/m, // unordered list + /^\s*\d+\.\s/m, // ordered list + /^\s*>\s/m, // blockquote + /^```/m, // code fence + /`[^`]+`/, // inline code + /\*\*[^*]+\*\*/, // bold + /\*[^*]+\*/, // italic + /^[|].+[|]/m, // table + /\[.+\]\(.+\)/, // link + /!\[.+\]\(.+\)/, // image + /~~[^~]+~~/, // strikethrough +] + +/** + * Returns true if the given plain text looks like it contains Markdown syntax. + * Used by the paste handler to decide whether to convert before inserting. + */ +export function looksLikeMarkdown(text: string): boolean { + if (!text || text.trim().length < 3) return false + return MARKDOWN_PATTERNS.some((re) => re.test(text)) +} + +// ── Turndown service factory ───────────────────────────────────────────────── + +function createTurndownService(): TurndownService { + const td = new TurndownService({ + headingStyle: 'atx', + hr: '---', + bulletListMarker: '-', + codeBlockStyle: 'fenced', + fence: '```', + emDelimiter: '_', + strongDelimiter: '**', + linkStyle: 'inlined', + linkReferenceStyle: 'full', + }) + + // GFM plugins: tables + task lists + strikethrough + td.use([tables, taskListItems, strikethrough]) + + // Custom rule: liveBlock → HTML comment + td.addRule('liveBlock', { + filter(node) { + return ( + node.nodeName === 'DIV' && + (node as HTMLElement).hasAttribute('data-live-block') + ) + }, + replacement(_content, node) { + const el = node as HTMLElement + const sourceNoteId = el.getAttribute('sourcenoteId') || el.getAttribute('sourcenoteId') || el.getAttribute('sourcenoteid') || '' + const blockId = el.getAttribute('blockId') || el.getAttribute('blockid') || '' + return `\n\n\n\n` + }, + }) + + // Custom rule: structuredViewBlock → HTML comment + td.addRule('structuredViewBlock', { + filter(node) { + return ( + node.nodeName === 'DIV' && + (node as HTMLElement).hasAttribute('data-structured-view-block') + ) + }, + replacement(_content, node) { + const el = node as HTMLElement + const attrs: Record = {} + for (const attr of Array.from(el.attributes)) { + if (attr.name !== 'data-structured-view-block') { + attrs[attr.name] = attr.value + } + } + return `\n\n\n\n` + }, + }) + + return td +} + +// Singleton (lazy-init) — safe for server + client usage +let _tdService: TurndownService | null = null +function getTurndownService(): TurndownService { + if (!_tdService) _tdService = createTurndownService() + return _tdService +} + +// ── Custom node pre-processor ───────────────────────────────────────────── + +// Sentinel prefix — alphanumeric only to avoid Markdown escaping by turndown +const SENTINEL_PREFIX = 'MOMENTOBLOCKSENTINEL' + +interface BlockPlaceholder { + key: string + comment: string +} + +/** + * Pre-process HTML before passing to turndown: + * - Replace empty custom node divs (liveBlock, structuredViewBlock) with text + * placeholders so they survive turndown processing (turndown drops blank nodes + * and strips HTML comments). + * - Return the modified HTML and a map of placeholder → HTML comment. + */ +function preprocessCustomNodes(html: string): { html: string; placeholders: BlockPlaceholder[] } { + const placeholders: BlockPlaceholder[] = [] + + // liveBlock: