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

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 })
}
}

View File

@@ -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<string, DemoNote[]> = {
fr: [
{
title: 'Réunion Q3 — Stratégie produit',
content: `<h2>Réunion Q3 — Stratégie produit</h2>
<p>Points clés abordés lors de la réunion trimestrielle :</p>
<ul>
<li><strong>Roadmap</strong> : priorité aux features de monétisation (Stripe, BYOK) avant les agents autonomes</li>
<li><strong>KPI Activation</strong> : objectif 40% d'utilisateurs actifs dans les 7 jours suivant l'inscription</li>
<li><strong>Acquisition PLG</strong> : brainstorm partageable = viral loop principale</li>
<li><strong>Pricing</strong> : Pro à 9,90 €/mois validé avec 3 personas cibles</li>
</ul>
<p>Prochaine étape : finaliser l'onboarding wizard pour atteindre le taux d'activation cible.</p>`,
},
{
title: 'Idées de projets secondaires',
content: `<h2>Idées de projets secondaires</h2>
<p>Liste de projets à explorer dans les prochains mois :</p>
<ul>
<li>📱 <strong>App de suivi d'habitudes</strong> — gamification, streaks, rappels intelligents</li>
<li>🎙️ <strong>Podcast "Tech Indépendant"</strong> — interviews de freelances tech, conseils pratiques</li>
<li>🤖 <strong>Bot Telegram de veille</strong> — scraping RSS + résumé IA chaque matin</li>
<li>📚 <strong>Newsletter hebdomadaire</strong> — curation de ressources pour développeurs</li>
<li>🎮 <strong>Jeu de stratégie minimaliste</strong> — WebGL, logique pure, pas de monétisation</li>
</ul>
<p>Critères de sélection : impact fort, temps minimal, apprentissage technique.</p>`,
},
{
title: 'Livres à lire — Recommandations',
content: `<h2>Livres à lire — Recommandations</h2>
<h3>Productivité & Pensée</h3>
<ul>
<li><strong>Building a Second Brain</strong> — Tiago Forte · Système de notes PKM, méthode CODE</li>
<li><strong>Deep Work</strong> — Cal Newport · Concentration profonde dans un monde de distractions</li>
<li><strong>Atomic Habits</strong> — James Clear · Les habitudes comme système de progression</li>
</ul>
<h3>Technique</h3>
<ul>
<li><strong>Designing Data-Intensive Applications</strong> — Martin Kleppmann · Architecture systèmes</li>
<li><strong>The Pragmatic Programmer</strong> — Hunt & Thomas · Bonnes pratiques développement</li>
</ul>
<p>Prochaine lecture : Building a Second Brain (lien avec Momento évident).</p>`,
},
{
title: 'Notes de formation React — Hooks avancés',
content: `<h2>Notes de formation React — Hooks avancés</h2>
<h3>useCallback vs useMemo</h3>
<ul>
<li><strong>useCallback</strong> : mémoïse une <em>fonction</em>, utile pour éviter re-renders des enfants</li>
<li><strong>useMemo</strong> : mémoïse une <em>valeur calculée</em>, utile pour calculs coûteux</li>
<li>⚠️ Ne pas abuser — le coût de la mémoïsation peut dépasser le gain</li>
</ul>
<h3>useReducer</h3>
<p>Préférer <code>useReducer</code> à <code>useState</code> quand :</p>
<ul>
<li>L'état a plusieurs sous-valeurs liées</li>
<li>La prochaine valeur dépend de la précédente</li>
<li>La logique de transition est complexe</li>
</ul>
<h3>Pattern : Context + useReducer</h3>
<p>Combine Context API avec useReducer pour un state management léger sans Redux.</p>`,
},
{
title: 'Objectifs personnels 2025',
content: `<h2>Objectifs personnels 2025</h2>
<h3>🎯 Professionnel</h3>
<ul>
<li>Lancer un produit SaaS avec les premiers utilisateurs payants</li>
<li>Contribuer à un projet open source significatif</li>
<li>Maîtriser l'architecture de systèmes distribués</li>
</ul>
<h3>📚 Apprentissage</h3>
<ul>
<li>Lire 12 livres techniques (1 par mois)</li>
<li>Compléter une formation en machine learning appliqué</li>
<li>Améliorer le niveau en algorithmique (LeetCode medium)</li>
</ul>
<h3>🌱 Personnel</h3>
<ul>
<li>Sport : 3x par semaine minimum</li>
<li>Méditation : 10 minutes par jour (habitude matinale)</li>
<li>Voyager dans 2 nouveaux pays</li>
</ul>
<p>Revue mensuelle le 1er de chaque mois pour ajuster les priorités.</p>`,
},
],
en: [
{
title: 'Q3 Meeting — Product Strategy',
content: `<h2>Q3 Meeting — Product Strategy</h2>
<p>Key points from the quarterly review:</p>
<ul>
<li><strong>Roadmap</strong>: monetization features first (Stripe, BYOK) before autonomous agents</li>
<li><strong>Activation KPI</strong>: target 40% active users within 7 days of signup</li>
<li><strong>PLG Acquisition</strong>: shareable brainstorm canvas = main viral loop</li>
<li><strong>Pricing</strong>: Pro at $9.90/month validated with 3 target personas</li>
</ul>
<p>Next step: finalize onboarding wizard to reach activation target.</p>`,
},
{
title: 'Side Project Ideas',
content: `<h2>Side Project Ideas</h2>
<p>Projects to explore in the coming months:</p>
<ul>
<li>📱 <strong>Habit tracking app</strong> — gamification, streaks, smart reminders</li>
<li>🎙️ <strong>Tech podcast</strong> — interviews with indie developers, practical advice</li>
<li>🤖 <strong>Telegram news bot</strong> — RSS scraping + AI summary every morning</li>
<li>📚 <strong>Weekly newsletter</strong> — curated resources for developers</li>
<li>🎮 <strong>Minimalist strategy game</strong> — WebGL, pure logic, no monetization</li>
</ul>
<p>Selection criteria: high impact, minimal time, technical learning.</p>`,
},
{
title: 'Books to Read — Recommendations',
content: `<h2>Books to Read — Recommendations</h2>
<h3>Productivity & Thinking</h3>
<ul>
<li><strong>Building a Second Brain</strong> — Tiago Forte · PKM note-taking system, CODE method</li>
<li><strong>Deep Work</strong> — Cal Newport · Deep focus in a distracted world</li>
<li><strong>Atomic Habits</strong> — James Clear · Habits as a system for progress</li>
</ul>
<h3>Technical</h3>
<ul>
<li><strong>Designing Data-Intensive Applications</strong> — Martin Kleppmann · Systems architecture</li>
<li><strong>The Pragmatic Programmer</strong> — Hunt & Thomas · Development best practices</li>
</ul>
<p>Next read: Building a Second Brain (obvious connection to Momento).</p>`,
},
{
title: 'React Training Notes — Advanced Hooks',
content: `<h2>React Training Notes — Advanced Hooks</h2>
<h3>useCallback vs useMemo</h3>
<ul>
<li><strong>useCallback</strong>: memoizes a <em>function</em>, useful to prevent child re-renders</li>
<li><strong>useMemo</strong>: memoizes a <em>computed value</em>, useful for expensive calculations</li>
<li>⚠️ Don't overuse — memoization cost can exceed the gain</li>
</ul>
<h3>useReducer</h3>
<p>Prefer <code>useReducer</code> over <code>useState</code> when:</p>
<ul>
<li>State has multiple related sub-values</li>
<li>Next state depends on previous state</li>
<li>Transition logic is complex</li>
</ul>`,
},
{
title: 'Personal Goals 2025',
content: `<h2>Personal Goals 2025</h2>
<h3>🎯 Professional</h3>
<ul>
<li>Launch a SaaS product with first paying users</li>
<li>Contribute to a significant open source project</li>
<li>Master distributed systems architecture</li>
</ul>
<h3>📚 Learning</h3>
<ul>
<li>Read 12 technical books (1 per month)</li>
<li>Complete an applied machine learning course</li>
<li>Improve algorithmic skills (LeetCode medium)</li>
</ul>
<h3>🌱 Personal</h3>
<ul>
<li>Exercise: 3x per week minimum</li>
<li>Meditation: 10 minutes per day (morning habit)</li>
<li>Travel to 2 new countries</li>
</ul>`,
},
],
fa: [
{
title: 'جلسه Q3 — استراتژی محصول',
content: `<h2>جلسه Q3 — استراتژی محصول</h2>
<p>نکات کلیدی جلسه فصلی:</p>
<ul>
<li><strong>نقشه راه</strong>: اول ویژگی‌های پولی‌سازی (Stripe، BYOK) و سپس عامل‌های خودمختار</li>
<li><strong>KPI فعال‌سازی</strong>: هدف ۴۰٪ کاربران فعال در ۷ روز اول</li>
<li><strong>رشد محصول‌محور</strong>: اشتراک‌گذاری بوم طوفان فکری = حلقه ویروسی اصلی</li>
</ul>
<p>قدم بعدی: نهایی کردن ویزارد آنبوردینگ برای رسیدن به هدف فعال‌سازی.</p>`,
},
{
title: 'ایده‌های پروژه‌های جانبی',
content: `<h2>ایده‌های پروژه‌های جانبی</h2>
<ul>
<li>📱 <strong>اپلیکیشن پیگیری عادت</strong> — گیمیفیکیشن، استریک، یادآوری هوشمند</li>
<li>🎙️ <strong>پادکست فناوری</strong> — مصاحبه با توسعه‌دهندگان مستقل</li>
<li>🤖 <strong>ربات تلگرام اخبار</strong> — خلاصه‌سازی روزانه با هوش مصنوعی</li>
</ul>`,
},
{
title: 'کتاب‌های پیشنهادی',
content: `<h2>کتاب‌های پیشنهادی</h2>
<ul>
<li><strong>ساختن مغز دوم</strong> — تیاگو فورتی · سیستم یادداشت‌برداری PKM</li>
<li><strong>کار عمیق</strong> — کال نیوپورت · تمرکز عمیق در دنیای پر از حواس‌پرتی</li>
<li><strong>عادت‌های اتمی</strong> — جیمز کلیر · عادت‌ها به عنوان سیستم پیشرفت</li>
</ul>`,
},
{
title: 'یادداشت‌های آموزش React',
content: `<h2>یادداشت‌های آموزش React</h2>
<h3>useCallback و useMemo</h3>
<ul>
<li><strong>useCallback</strong>: یک تابع را حفظ می‌کند</li>
<li><strong>useMemo</strong>: یک مقدار محاسبه‌شده را حفظ می‌کند</li>
</ul>`,
},
{
title: 'اهداف شخصی ۲۰۲۵',
content: `<h2>اهداف شخصی ۲۰۲۵</h2>
<h3>🎯 حرفه‌ای</h3>
<ul>
<li>راه‌اندازی یک محصول SaaS با اولین کاربران پرداخت‌کننده</li>
<li>مشارکت در یک پروژه متن‌باز مهم</li>
</ul>
<h3>📚 یادگیری</h3>
<ul>
<li>خواندن ۱۲ کتاب فناوری (یکی در ماه)</li>
<li>تکمیل یک دوره یادگیری ماشین کاربردی</li>
</ul>`,
},
],
}
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<T>(promise: Promise<T>, ms: number): Promise<T> {
const timeout = new Promise<never>((_, 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 })
}

View File

@@ -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<string, unknown>
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<Record<AllowedField, unknown>> = {}
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)
}

View File

@@ -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;
},

View File

@@ -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;
},

View File

@@ -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<string | null>(null)
const [pptxLoading, setPptxLoading] = useState(false)
const [pptxError, setPptxError] = useState<string | null>(null)
const [fitTrigger, setFitTrigger] = useState(0)
const [renamingSession, setRenamingSession] = useState<string | null>(null)
const [renameInput, setRenameInput] = useState('')
const canvasContainerRef = useRef<HTMLDivElement>(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}
/>
) : (
<div className="absolute inset-0 flex items-center justify-center pointer-events-none opacity-20 flex-col gap-6">
@@ -594,7 +639,7 @@ export function BrainstormPage() {
animate={{ opacity: 1, y: 0 }}
className="absolute bottom-6 left-6 flex gap-2"
>
<div className="px-4 py-2 bg-[#F4F1EA] dark:bg-black/60 backdrop-blur-xl border border-border shadow-xl rounded-full flex items-center gap-6">
<div className="px-4 py-2 bg-[#F4F1EA] dark:bg-black/60 backdrop-blur-xl border border-border shadow-xl rounded-full flex items-center gap-5">
{[1, 2, 3].map((w) => (
<div key={w} className="flex items-center gap-2">
<div
@@ -609,6 +654,20 @@ export function BrainstormPage() {
</span>
</div>
))}
{viewMode === 'canvas' && (
<>
<div className="w-px h-4 bg-border/60" />
<button
onClick={() => setFitTrigger((c) => c + 1)}
title={t('brainstorm.fitToScreen') || 'Recentrer'}
className="text-muted-foreground hover:text-foreground transition-colors"
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/>
</svg>
</button>
</>
)}
</div>
{canEdit && (
@@ -854,7 +913,10 @@ export function BrainstormPage() {
{sessions?.map((s) => (
<div key={s.id} className="relative group/session">
<button
onClick={() => setActiveSessionId(s.id)}
onClick={() => {
setActiveSessionId(s.id)
router.replace('/brainstorm?session=' + s.id)
}}
className={`w-10 h-10 min-h-[40px] rounded-xl flex items-center justify-center text-xs font-bold transition-all shrink-0 ${
activeSessionId === s.id
? 'bg-foreground text-background scale-110 shadow-lg'
@@ -864,7 +926,7 @@ export function BrainstormPage() {
}`}
title={s.seedIdea}
>
{s.seedIdea.charAt(0).toUpperCase()}
{(s.seedIdea.replace(/\p{Emoji}/gu, '').trim().charAt(0) || '?').toUpperCase()}
</button>
{activeSessionId === s.id && (
<button
@@ -943,14 +1005,29 @@ export function BrainstormPage() {
{t('brainstorm.regenerateSummary') || 'Regenerate'}
</button>
)}
<button
onClick={() => { setSummaryOpen(false); handleExport() }}
disabled={exportBrainstorm.isPending || summarize.isPending}
className="ms-auto flex items-center gap-2 px-5 py-2.5 bg-foreground text-background rounded-xl text-xs font-bold uppercase tracking-widest disabled:opacity-50 hover:opacity-80 transition-opacity"
>
<Download size={13} />
{t('brainstorm.exportAsNote') || 'Export as note'}
</button>
<div className="ms-auto flex items-center gap-2">
<button
onClick={handleExportPptx}
disabled={pptxLoading || summarize.isPending}
title={t('brainstorm.downloadPptxDesc') || 'Download as PowerPoint'}
className="flex items-center gap-2 px-4 py-2.5 border border-foreground/20 text-foreground rounded-xl text-xs font-bold uppercase tracking-widest disabled:opacity-50 hover:bg-foreground/5 transition-colors"
>
{pptxLoading ? (
<motion.div animate={{ rotate: 360 }} transition={{ duration: 1, repeat: Infinity, ease: 'linear' }} className="w-3 h-3 border-2 border-current border-t-transparent rounded-full" />
) : (
<Download size={13} />
)}
{t('brainstorm.downloadPptx') || 'PPTX'}
</button>
<button
onClick={() => { setSummaryOpen(false); handleExport() }}
disabled={exportBrainstorm.isPending || summarize.isPending}
className="flex items-center gap-2 px-5 py-2.5 bg-foreground text-background rounded-xl text-xs font-bold uppercase tracking-widest disabled:opacity-50 hover:opacity-80 transition-opacity"
>
<Download size={13} />
{t('brainstorm.exportAsNote') || 'Export as note'}
</button>
</div>
</div>
</motion.div>
</motion.div>
@@ -1033,6 +1110,20 @@ export function BrainstormPage() {
)}
</AnimatePresence>
<AnimatePresence>
{pptxError && (
<motion.div
initial={{ y: 100, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 100, opacity: 0 }}
className="fixed bottom-8 left-1/2 -translate-x-1/2 z-50 px-6 py-4 bg-rose-500 text-white rounded-2xl shadow-2xl flex items-center gap-4 text-sm font-medium"
>
<span>{pptxError}</span>
<button onClick={() => setPptxError(null)} className="text-white/60 hover:text-white"></button>
</motion.div>
)}
</AnimatePresence>
<AnimatePresence>
{exportToast && (
<motion.div

View File

@@ -14,6 +14,7 @@ interface WaveCanvasProps {
remoteMove?: { ideaId: string; x: number; y: number; _seq: number } | null
manualEditTrigger?: number
playbackIdeas?: any[] | null
fitTrigger?: number
}
const WAVE_COLORS: Record<number, string> = {
@@ -31,6 +32,7 @@ export const WaveCanvas: React.FC<WaveCanvasProps> = ({
remoteMove,
manualEditTrigger,
playbackIdeas,
fitTrigger,
}) => {
const { t, language } = useLanguage()
const svgRef = useRef<SVGSVGElement>(null)
@@ -39,6 +41,8 @@ export const WaveCanvas: React.FC<WaveCanvasProps> = ({
const linkRef = useRef<d3.Selection<SVGLineElement, any, SVGGElement, unknown> | null>(null)
const simulationRef = useRef<d3.Simulation<any, any> | null>(null)
const transformRef = useRef<d3.ZoomTransform>(d3.zoomIdentity)
const zoomRef = useRef<d3.ZoomBehavior<SVGSVGElement, unknown> | 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<WaveCanvasProps> = ({
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<WaveCanvasProps> = ({
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 (
<div
ref={containerRef}
@@ -524,6 +541,21 @@ export const WaveCanvas: React.FC<WaveCanvasProps> = ({
>
<svg ref={svgRef} className="w-full h-full" />
{/* Legend overlay — bottom right */}
<div className="absolute bottom-6 right-6 pointer-events-none flex flex-col gap-1 bg-white/80 dark:bg-[#1A1A1A]/80 backdrop-blur-sm rounded-xl px-3 py-2.5 shadow-sm border border-black/[0.06] dark:border-white/[0.06]">
{[
{ 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 }) => (
<div key={label} className="flex items-center gap-2">
<div className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: color }} />
<span className="text-[10px] text-foreground/50 font-medium">{label}</span>
</div>
))}
</div>
{editingNode && (
<div
className="absolute z-50 pointer-events-auto"

View File

@@ -11,6 +11,7 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { Button } from '@/components/ui/button'
@@ -18,7 +19,7 @@ import { Badge } from '@/components/ui/badge'
import {
X, Plus, Palette, Image as ImageIcon, Bell, Eye, Link as LinkIcon, Sparkles,
Maximize2, Copy, ArrowLeft, ChevronRight, PanelRight, Check, Loader2, Save, MoreHorizontal,
Trash2, LogOut, Wand2, Share2, Wind, Paperclip, GraduationCap
Trash2, LogOut, Wand2, Share2, Wind, Paperclip, GraduationCap, FileDown, FileUp
} from 'lucide-react'
import { FlashcardGenerateDialog } from '@/components/flashcards/flashcard-generate-dialog'
import { NoteShareDialog } from './note-share-dialog'
@@ -29,6 +30,7 @@ import { NOTE_COLORS, NoteColor, Note } from '@/lib/types'
import { cn } from '@/lib/utils'
import { toast } from 'sonner'
import { format } from 'date-fns'
import { tiptapHTMLToMarkdown, markdownToHTML, extractMarkdownTitle } from '@/lib/editor/markdown-export'
interface NoteEditorToolbarProps {
mode: 'fullPage' | 'dialog'
@@ -38,16 +40,70 @@ interface NoteEditorToolbarProps {
}
export function NoteEditorToolbar({ mode, onClose, onToggleAttachments, attachmentsCount }: NoteEditorToolbarProps) {
const { state, actions, note, readOnly, fullPage, notebooks, fileInputRef } = useNoteEditorContext()
const { state, actions, note, readOnly, fullPage, notebooks, fileInputRef, richTextEditorRef } = useNoteEditorContext()
const { t } = useLanguage()
const [isConverting, setIsConverting] = useState(false)
const [shareOpen, setShareOpen] = useState(false)
const [flashcardsOpen, setFlashcardsOpen] = useState(false)
const mdImportInputRef = useRef<HTMLInputElement>(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<HTMLInputElement>) => {
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
<MoreHorizontal size={16} />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuContent align="end" className="w-52">
<DropdownMenuItem onClick={handleExportMarkdown}>
<FileDown className="h-4 w-4 me-2" />
{t('richTextEditor.exportMarkdown')}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => mdImportInputRef.current?.click()}>
<FileUp className="h-4 w-4 me-2" />
{t('richTextEditor.importMarkdown')}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={async () => {
try {
@@ -304,6 +369,7 @@ export function NoteEditorToolbar({ mode, onClose, onToggleAttachments, attachme
}
return (
<>
<div className="flex items-center justify-between pt-3 border-t border-border/30">
<div className="flex items-center gap-0.5">
{!readOnly && (
@@ -432,5 +498,14 @@ export function NoteEditorToolbar({ mode, onClose, onToggleAttachments, attachme
)}
</div>
</div>
{/* Hidden file input for Markdown import */}
<input
ref={mdImportInputRef}
type="file"
accept=".md,text/markdown"
className="hidden"
onChange={handleImportMarkdownFile}
/>
</>
)
}

View File

@@ -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<string, RouteHintSet> = {
'/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<string | null>(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 (
<AnimatePresence>
{visible && (
<motion.div
key={`hints-${pathname}`}
initial={{ opacity: 0, x: 60 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 60 }}
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
className="fixed bottom-24 right-4 z-[150] w-72 rounded-2xl border border-border bg-background shadow-xl"
>
{/* Header */}
<div className="flex items-center justify-between px-4 pt-3 pb-2 border-b border-border/50">
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
{t('onboarding.editor_hints_title')}
</p>
<button
onClick={dismiss}
className="rounded p-0.5 text-muted-foreground/40 hover:text-muted-foreground transition-colors"
>
<X className="h-3.5 w-3.5" />
</button>
</div>
{/* Hint content */}
<AnimatePresence mode="wait">
<motion.div
key={hint.key}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -8 }}
transition={{ duration: 0.18 }}
className="px-4 py-3 flex items-start gap-3"
>
<span className={`mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ${hint.bg} ${hint.color}`}>
<Icon className="h-4 w-4" />
</span>
<div>
<p className="text-sm font-semibold text-foreground">
{t(`onboarding.hint_${hint.key}_title`)}
</p>
<p className="text-xs text-muted-foreground mt-0.5 leading-relaxed">
{t(`onboarding.hint_${hint.key}_desc`)}
</p>
</div>
</motion.div>
</AnimatePresence>
{/* Navigation */}
<div className="flex items-center justify-between px-4 pb-3">
{/* Dots */}
<div className="flex gap-1">
{hints.map((_, i) => (
<button
key={i}
onClick={() => setIndex(i)}
className={`h-1.5 rounded-full transition-all ${
i === index ? 'w-4 bg-violet-500' : 'w-1.5 bg-border'
}`}
/>
))}
</div>
{/* Arrows + dismiss */}
<div className="flex items-center gap-1">
<button
onClick={() => setIndex(i => Math.max(0, i - 1))}
disabled={index === 0}
className="flex h-6 w-6 items-center justify-center rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted disabled:opacity-30 transition-colors"
>
<ChevronLeft className="h-3.5 w-3.5" />
</button>
{index < hints.length - 1 ? (
<button
onClick={() => setIndex(i => i + 1)}
className="flex h-6 w-6 items-center justify-center rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
>
<ChevronRight className="h-3.5 w-3.5" />
</button>
) : (
<button
onClick={dismiss}
className="flex items-center gap-1 text-xs font-medium px-2.5 h-6 rounded-lg bg-violet-500/10 text-violet-500 hover:bg-violet-500/20 transition-colors"
>
{t('onboarding.editor_hints_got_it')}
</button>
)}
</div>
</div>
</motion.div>
)}
</AnimatePresence>
)
}

View File

@@ -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<string, string> = {
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<SearchResult[] | null>(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 (
<motion.div
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -16 }}
className="flex flex-col items-center gap-5 w-full"
>
<motion.div
initial={{ scale: 0.7, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ delay: 0.1, type: 'spring', stiffness: 200 }}
className="flex h-20 w-20 items-center justify-center rounded-2xl bg-violet-500/10 text-violet-500"
>
<Sparkles className="h-10 w-10" />
</motion.div>
<div className="space-y-2 text-center">
<h2 className="text-2xl font-bold tracking-tight text-foreground">
{t('onboarding.step_aha_title')}
</h2>
<p className="text-base text-muted-foreground max-w-xs mx-auto">
{t('onboarding.step_aha_subtitle')}
</p>
</div>
<motion.div
animate={searched ? {} : { boxShadow: ['0 0 0 0 rgba(139,92,246,0)', '0 0 0 6px rgba(139,92,246,0.15)', '0 0 0 0 rgba(139,92,246,0)'] }}
transition={{ duration: 2, repeat: searched ? 0 : Infinity }}
className="flex w-full max-w-sm rounded-xl border border-border bg-background overflow-hidden shadow-sm"
>
<input
type="text"
value={query}
onChange={e => 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')}
/>
<button
onClick={handleSearch}
disabled={loading || !query.trim()}
aria-label={t('onboarding.step_aha_search_button')}
className="flex items-center gap-1.5 px-4 text-sm font-medium text-muted-foreground hover:text-violet-500 transition-colors disabled:opacity-50"
>
{loading
? <span className="h-4 w-4 animate-spin border-2 border-violet-500/30 border-t-violet-500 rounded-full block" />
: <Search className="h-4 w-4" />
}
<span className="hidden sm:inline">{t('onboarding.step_aha_search_button')}</span>
</button>
</motion.div>
<AnimatePresence>
{searched && results !== null && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
className="w-full max-w-sm space-y-2"
>
{quotaExceeded ? (
<p className="text-sm text-center text-destructive py-2">
{t('onboarding.quota_exceeded')}
</p>
) : results.length === 0 ? (
<p className="text-sm text-center text-muted-foreground py-2">
{t('onboarding.no_results')}
</p>
) : (
<>
{results.slice(0, 3).map((r, i) => (
<motion.div
key={r.id}
initial={{ opacity: 0, x: -8 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: i * 0.07 }}
className="rounded-lg border border-border bg-muted/30 px-3 py-2"
>
<p className="text-sm font-medium text-foreground truncate">{r.title ?? 'Untitled'}</p>
{r.snippet && (
<p className="text-xs text-muted-foreground truncate mt-0.5">{r.snippet}</p>
)}
</motion.div>
))}
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.3 }}
className="flex items-center justify-center gap-1 text-xs text-violet-500 pt-1"
>
<span></span>
<span>{t('onboarding.search_credit_used')}</span>
</motion.p>
</>
)}
</motion.div>
)}
</AnimatePresence>
<Button
onClick={onDone}
size="lg"
className="w-full max-w-sm gap-2"
>
{t('onboarding.step_aha_cta')}
<ArrowRight className="h-4 w-4" />
</Button>
</motion.div>
)
}

View File

@@ -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<Set<string>>(new Set())
function handleTry(key: string, href: string) {
setChecked(prev => new Set([...prev, key]))
onTry(href)
}
const allChecked = checked.size === ACTIONS.length
return (
<motion.div
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -16 }}
className="flex flex-col items-center gap-5 w-full"
>
<motion.div
initial={{ scale: 0.7, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ delay: 0.1, type: 'spring', stiffness: 200 }}
className="flex h-20 w-20 items-center justify-center rounded-2xl bg-amber-500/10 text-amber-500"
>
<Sparkles className="h-10 w-10" />
</motion.div>
<div className="space-y-1 text-center">
<h2 className="text-2xl font-bold tracking-tight text-foreground">
{t('onboarding.step_features_title')}
</h2>
<p className="text-sm text-muted-foreground max-w-xs mx-auto">
{t('onboarding.step_features_subtitle')}
</p>
</div>
<div className="flex flex-col gap-2.5 w-full">
{ACTIONS.map(({ icon: Icon, color, bg, key, href }, i) => {
const done = checked.has(key)
return (
<motion.div
key={key}
initial={{ opacity: 0, x: -12 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.08 + i * 0.08 }}
className={`flex items-center gap-3 rounded-xl border p-3 transition-all ${
done ? 'border-emerald-500/30 bg-emerald-500/5' : 'border-border bg-muted/10 hover:border-border/80'
}`}
>
{/* Checkmark */}
<span className={`shrink-0 flex h-5 w-5 items-center justify-center rounded-full border-2 transition-colors ${
done ? 'border-emerald-500 bg-emerald-500' : 'border-border'
}`}>
<AnimatePresence>
{done && (
<motion.span initial={{ scale: 0 }} animate={{ scale: 1 }} exit={{ scale: 0 }}>
<Check className="h-3 w-3 text-white" />
</motion.span>
)}
</AnimatePresence>
</span>
{/* Icon */}
<span className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ${bg} ${color}`}>
<Icon className="h-4 w-4" />
</span>
{/* Text */}
<div className="flex-1 min-w-0">
<p className={`text-sm font-medium leading-tight ${done ? 'text-muted-foreground' : 'text-foreground'}`}>
{t(`onboarding.action_${key}_title`)}
</p>
<p className="text-xs text-muted-foreground leading-tight mt-0.5">
{t(`onboarding.action_${key}_desc`)}
</p>
</div>
{/* Essayer — minimise wizard + navigue */}
<button
onClick={() => handleTry(key, href)}
className={`shrink-0 flex items-center gap-1 text-xs font-medium px-2.5 py-1.5 rounded-lg transition-opacity ${color} ${bg} ${done ? 'opacity-40' : 'hover:opacity-80'}`}
>
{done ? t('onboarding.action_done') : t('onboarding.action_try')}
{!done && <ArrowRight className="h-3 w-3" />}
</button>
</motion.div>
)
})}
</div>
<Button onClick={onDone} size="lg" className="w-full gap-2 mt-1">
{allChecked ? t('onboarding.step_features_cta_all') : t('onboarding.step_features_cta')}
<ArrowRight className="h-4 w-4" />
</Button>
</motion.div>
)
}

View File

@@ -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<string | null>(null)
const [importedCount, setImportedCount] = useState(0)
const fileInputRef = useRef<HTMLInputElement>(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<HTMLInputElement>) {
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) : `<p>${text.replace(/\n\n+/g, '</p><p>').replace(/\n/g, '<br/>')}</p>`
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 (
<motion.div
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -16 }}
className="flex flex-col items-center gap-6 text-center"
>
<motion.div
initial={{ scale: 0.7, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ delay: 0.1, type: 'spring', stiffness: 200 }}
className="flex h-20 w-20 items-center justify-center rounded-2xl bg-blue-500/10 text-blue-500"
>
<FileText className="h-10 w-10" />
</motion.div>
<div className="space-y-2">
<h2 className="text-2xl font-bold tracking-tight text-foreground">
{t('onboarding.step_notes_title')}
</h2>
<p className="text-base text-muted-foreground max-w-xs">
{hasNotes
? t('onboarding.step_notes_has_notes', { count: noteCount })
: t('onboarding.step_notes_empty')}
</p>
{!hasNotes && (
<p className="text-xs text-violet-500/80 max-w-xs">
{t('onboarding.step_notes_hint')}
</p>
)}
</div>
{hasNotes ? (
<div className="flex flex-col gap-2 w-full max-w-xs">
<Button onClick={() => onNext(false)} size="lg" className="w-full">
{t('onboarding.step_notes_cta')}
</Button>
<button
onClick={onSkip}
className="text-xs text-muted-foreground/60 hover:text-muted-foreground transition-colors py-1"
>
{t('onboarding.skip')}
</button>
</div>
) : (
<div className="flex flex-col gap-3 w-full max-w-xs">
{created ? (
<motion.div
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
className="flex items-center justify-center gap-2 rounded-lg bg-green-500/10 py-3 text-green-600"
>
<CheckCircle className="h-5 w-5" />
<span className="text-sm font-medium">
{importedCount > 0
? t('onboarding.import_notes_ready', { count: importedCount })
: t('onboarding.demo_notes_ready')}
</span>
</motion.div>
) : (
<>
<Button
onClick={handleCreateDemo}
size="lg"
className="w-full gap-2"
disabled={loading}
>
{loading ? (
<>
<span className="animate-spin h-4 w-4 border-2 border-white/30 border-t-white rounded-full" />
{t('onboarding.creating_demo_notes')}
</>
) : (
<>
<Sparkles className="h-4 w-4" />
{t('onboarding.step_notes_demo')}
</>
)}
</Button>
{/* Hidden file input — accepts .md and .txt */}
<input
ref={fileInputRef}
type="file"
accept=".md,.txt"
multiple
className="hidden"
onChange={handleImportFiles}
/>
<Button
onClick={() => fileInputRef.current?.click()}
size="lg"
variant="outline"
className="w-full gap-2"
disabled={loading}
>
<Upload className="h-4 w-4" />
{t('onboarding.step_notes_import')}
</Button>
<p className="text-xs text-muted-foreground/50">
{t('onboarding.import_formats')}
</p>
</>
)}
{importError && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="flex items-center gap-2 rounded-lg bg-destructive/10 py-2 px-3 text-destructive text-sm"
>
<AlertCircle className="h-4 w-4 shrink-0" />
{importError}
</motion.div>
)}
<button
onClick={onSkip}
className="text-xs text-muted-foreground/60 hover:text-muted-foreground transition-colors py-1"
>
{t('onboarding.skip')}
</button>
</div>
)}
</motion.div>
)
}

View File

@@ -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 (
<motion.div
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -16 }}
className="flex flex-col items-center gap-6 text-center"
>
<motion.div
initial={{ scale: 0.7, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ delay: 0.1, type: 'spring', stiffness: 200 }}
className="flex h-20 w-20 items-center justify-center rounded-2xl bg-brand-accent/10 text-brand-accent"
>
<Brain className="h-10 w-10" />
</motion.div>
<div className="space-y-2">
<h2 className="text-2xl font-bold tracking-tight text-foreground">
{firstName
? t('onboarding.welcome_title_name', { name: firstName })
: t('onboarding.welcome_title')}
</h2>
<p className="text-base text-muted-foreground max-w-xs">
{t('onboarding.welcome_subtitle')}
</p>
</div>
<div className="flex flex-col gap-2 w-full max-w-xs">
<Button onClick={onNext} size="lg" className="w-full">
{t('onboarding.welcome_cta')}
</Button>
<button
onClick={onSkip}
className="text-xs text-muted-foreground/60 hover:text-muted-foreground transition-colors py-1"
>
{t('onboarding.skip')}
</button>
</div>
</motion.div>
)
}

View File

@@ -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<number> {
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 */}
<AnimatePresence>
{minimized && (
<motion.button
key="onboarding-pill"
initial={{ opacity: 0, y: 40, scale: 0.8 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 40, scale: 0.8 }}
transition={{ type: 'spring', stiffness: 400, damping: 28 }}
onClick={() => 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"
>
<Sparkles className="h-4 w-4" />
<span>{t('onboarding.pill_resume')}</span>
{triedCount > 0 && (
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-white/20 text-xs font-bold">
{triedCount}
</span>
)}
</motion.button>
)}
</AnimatePresence>
{/* Full wizard modal */}
<AnimatePresence>
{!minimized && (
<motion.div
key="onboarding-overlay"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 z-[200] flex items-end justify-center sm:items-center bg-black/50 backdrop-blur-sm p-4"
>
<motion.div
initial={{ y: 40, opacity: 0, scale: 0.97 }}
animate={{ y: 0, opacity: 1, scale: 1 }}
exit={{ y: 60, opacity: 0, scale: 0.95 }}
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
className={cn(
'relative w-full max-w-md rounded-2xl bg-background border border-border shadow-2xl p-8',
'sm:rounded-2xl rounded-t-2xl rounded-b-none sm:rounded-b-2xl'
)}
>
{/* Progress */}
<div className="flex flex-col items-center gap-1.5 mb-6">
<div className="flex items-center gap-2">
{Array.from({ length: TOTAL_STEPS }).map((_, i) => (
<motion.div
key={i}
animate={{
width: i + 1 === step ? 24 : 8,
backgroundColor: i + 1 <= step ? 'var(--brand-accent, #7c3aed)' : 'var(--border)',
}}
transition={{ duration: 0.3 }}
className="h-2 rounded-full bg-border"
/>
))}
</div>
<p className="text-xs text-muted-foreground/60">
{t('onboarding.progress', { current: step, total: TOTAL_STEPS })}
</p>
</div>
{/* Skip / close button (steps 1-3 only, step 4 has its own CTA) */}
{step < 4 && (
<button
onClick={handleSkip}
className="absolute top-4 right-4 rounded-lg p-1.5 text-muted-foreground/40 hover:text-muted-foreground hover:bg-muted/50 transition-colors"
aria-label="Fermer"
>
<X className="h-4 w-4" />
</button>
)}
{/* Step content */}
<AnimatePresence mode="wait">
{step === 1 && (
<OnboardingStepWelcome
key="step-1"
onNext={handleNext}
onSkip={handleSkip}
userName={user?.name}
/>
)}
{step === 2 && (
<OnboardingStepNotes
key="step-2"
noteCount={noteCount}
onNext={handleNotesNext}
onSkip={handleSkip}
locale={language}
/>
)}
{step === 3 && (
<OnboardingStepAha
key="step-3"
onDone={handleNext}
locale={language}
autoSearch={demoNotesJustCreated}
/>
)}
{step === 4 && (
<OnboardingStepFeatures
key="step-4"
onDone={handleDone}
onTry={handleTry}
/>
)}
</AnimatePresence>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</>
)
}

View File

@@ -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<string, QuotaData>; tier: string }
export function StarterPackBadge() {
const { t } = useLanguage()
const { data } = useQuery<UsageData>({
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 (
<Link
href="/pricing"
className={cn(
'flex items-center gap-2 rounded-lg px-3 py-2 text-xs font-medium transition-colors',
isCritical
? 'bg-destructive/10 text-destructive hover:bg-destructive/20'
: isWarning
? 'bg-orange-500/10 text-orange-500 hover:bg-orange-500/20'
: 'bg-muted text-muted-foreground hover:text-foreground hover:bg-muted/80'
)}
>
<Zap className={cn('h-3.5 w-3.5', isWarning || isCritical ? 'animate-pulse' : '')} />
<span>
{isCritical
? t('onboarding.badge_upgrade')
: t('onboarding.badge_credits', { count: remaining })
}
</span>
{isCritical && <ArrowUpRight className="h-3 w-3 ml-auto" />}
</Link>
)
}

View File

@@ -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({
<DirWrapper>
<SearchModalProvider>
{children}
<OnboardingWizard />
<OnboardingEditorHints />
</SearchModalProvider>
</DirWrapper>
</AiConsentProvider>

View File

@@ -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<RichTextEditorHandle, RichTextEditorPro
...globalDragHandleExtensions,
SmartPasteExtension,
BlockSelectionExtension,
MarkdownPasteExtension,
TurnIntoShortcutExtension,
UndoRedoFeedbackExtension,
LiveBlockExtension,

View File

@@ -59,6 +59,7 @@ import {
import { performSignOut } from '@/lib/auth-client'
import { useBrainstormSessions, useDeleteBrainstorm } from '@/hooks/use-brainstorm'
import { UsageMeter } from './usage-meter'
import { StarterPackBadge } from './onboarding/starter-pack-badge'
type NavigationView = 'notebooks' | 'agents' | 'reminders' | 'brainstorms' | 'revision' | 'insights'
type SortOrder = 'newest' | 'oldest' | 'alpha' | 'manual'
@@ -1535,8 +1536,9 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
</AnimatePresence>
</div>
{/* ── Usage meter en bas du panneau ── */}
<div className="border-t border-border/20 px-3 py-3 mt-auto">
{/* ── Usage meter + starter badge en bas du panneau ── */}
<div className="border-t border-border/20 px-3 py-3 mt-auto space-y-2">
<StarterPackBadge />
<UsageMeter />
</div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

View File

@@ -0,0 +1,322 @@
import type PptxGenJSModule from 'pptxgenjs'
let _PptxGenJS: (new () => PptxGenJSModule) | null = null
async function getPptxGenClass(): Promise<new () => 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<number, string> = {
1: '🔄 Variations',
2: '🔗 Analogies',
3: '💥 Disruptions',
}
const WAVE_COLORS: Record<number, string> = {
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 }
}

View File

@@ -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<!-- live-block: ${sourceNoteId}#${blockId} -->\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<string, string> = {}
for (const attr of Array.from(el.attributes)) {
if (attr.name !== 'data-structured-view-block') {
attrs[attr.name] = attr.value
}
}
return `\n\n<!-- structured-view: ${JSON.stringify(attrs)} -->\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: <div data-live-block="true" sourceNoteId="..." blockId="..."></div>
let result = html.replace(
/<div([^>]*?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: `<!-- live-block: ${snId}#${bId} -->` })
return `<p>${key}</p>`
}
)
// structuredViewBlock: <div data-structured-view-block="true" ...></div>
result = result.replace(
/<div([^>]*?data-structured-view-block[^>]*?)>\s*<\/div>/gi,
(_match, attrs) => {
const attrMap: Record<string, string> = {}
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: `<!-- structured-view: ${JSON.stringify(attrMap)} -->` })
return `<p>${key}</p>`
}
)
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
}

View File

@@ -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
},
},
}),
]
},
})

View File

@@ -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."
}
}
}

View File

@@ -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."
}
}
}

View File

@@ -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."
}
}

View File

@@ -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."
}
}
}

View File

@@ -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."
}
}
}

View File

@@ -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."
}
}

View File

@@ -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."
}
}
}

View File

@@ -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."
}
}
}

View File

@@ -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."
}
}
}

View File

@@ -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."
}
}
}

View File

@@ -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."
}
}
}

View File

@@ -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."
}
}
}

View File

@@ -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."
}
}
}

View File

@@ -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."
}
}
}

View File

@@ -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."
}
}
}

View File

@@ -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",

View File

@@ -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",

View File

@@ -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;

View File

@@ -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")?

View File

@@ -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('<h1>Hello</h1>')
expect(md).toBe('# Hello')
})
test('converts H2 to ## heading', () => {
const md = tiptapHTMLToMarkdown('<h2>Section</h2>')
expect(md).toBe('## Section')
})
test('converts H3 to ### heading', () => {
const md = tiptapHTMLToMarkdown('<h3>Sub</h3>')
expect(md).toBe('### Sub')
})
test('converts bold text', () => {
const md = tiptapHTMLToMarkdown('<p>This is <strong>bold</strong> text.</p>')
expect(md).toContain('**bold**')
})
test('converts italic text', () => {
const md = tiptapHTMLToMarkdown('<p>This is <em>italic</em> text.</p>')
expect(md).toContain('_italic_')
})
test('converts unordered list', () => {
const md = tiptapHTMLToMarkdown('<ul><li>Item 1</li><li>Item 2</li></ul>')
expect(md).toContain('Item 1')
expect(md).toContain('Item 2')
expect(md).toMatch(/^[-*+]\s/m)
})
test('converts ordered list', () => {
const md = tiptapHTMLToMarkdown('<ol><li>First</li><li>Second</li></ol>')
expect(md).toContain('1. First')
expect(md).toContain('2. Second')
})
test('converts code block', () => {
const md = tiptapHTMLToMarkdown('<pre><code>const x = 1;</code></pre>')
expect(md).toContain('```')
expect(md).toContain('const x = 1;')
})
test('converts blockquote', () => {
const md = tiptapHTMLToMarkdown('<blockquote><p>Quote text</p></blockquote>')
expect(md).toContain('> Quote text')
})
test('converts inline code', () => {
const md = tiptapHTMLToMarkdown('<p>Use <code>console.log()</code> here.</p>')
expect(md).toContain('`console.log()`')
})
test('converts hyperlink', () => {
const md = tiptapHTMLToMarkdown('<p><a href="https://example.com">Example</a></p>')
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 = '<div data-live-block="true" sourceNoteId="abc" blockId="def"></div>'
const md = tiptapHTMLToMarkdown(html)
expect(md).toContain('<!-- live-block:')
})
test('preserves structuredViewBlock as HTML comment', () => {
const html = '<div data-structured-view-block="true" data-sv-mode="table"></div>'
const md = tiptapHTMLToMarkdown(html)
expect(md).toContain('<!-- structured-view:')
})
})
describe('markdownToHTML', () => {
test('converts # H1 to h1 element', () => {
const html = markdownToHTML('# Hello')
expect(html.toLowerCase()).toContain('<h1>hello</h1>')
})
test('converts ## H2 to h2 element', () => {
const html = markdownToHTML('## Section')
expect(html.toLowerCase()).toContain('<h2>section</h2>')
})
test('converts bold markdown to <strong>', () => {
const html = markdownToHTML('This is **bold** text.')
expect(html).toContain('<strong>bold</strong>')
})
test('converts italic markdown to <em>', () => {
const html = markdownToHTML('This is *italic* text.')
expect(html).toContain('<em>italic</em>')
})
test('converts unordered list to <ul><li>', () => {
const html = markdownToHTML('- Item 1\n- Item 2')
expect(html).toContain('<ul>')
expect(html).toContain('<li>Item 1</li>')
})
test('converts ordered list to <ol><li>', () => {
const html = markdownToHTML('1. First\n2. Second')
expect(html).toContain('<ol>')
expect(html).toContain('<li>First</li>')
})
test('converts fenced code block to <pre><code>', () => {
const html = markdownToHTML('```\nconst x = 1;\n```')
expect(html).toContain('<pre>')
expect(html).toContain('<code>')
expect(html).toContain('const x = 1;')
})
test('converts GFM table to <table>', () => {
const md = '| Name | Age |\n|------|-----|\n| Alice | 30 |'
const html = markdownToHTML(md)
expect(html).toContain('<table>')
expect(html).toContain('<th>Name</th>')
expect(html).toContain('Alice')
})
test('handles empty markdown', () => {
expect(markdownToHTML('')).toBe('')
expect(markdownToHTML(' ')).toBe('')
})
})
describe('extractMarkdownTitle', () => {
test('extracts first H1', () => {
const md = '# My Note\n\nSome content'
expect(extractMarkdownTitle(md)).toBe('My Note')
})
test('extracts H1 even with preceding content', () => {
const md = 'Some preamble\n# The Title\n\nContent'
expect(extractMarkdownTitle(md)).toBe('The Title')
})
test('returns null if no H1', () => {
const md = '## Not an H1\n\nContent'
expect(extractMarkdownTitle(md)).toBeNull()
})
test('returns null for empty string', () => {
expect(extractMarkdownTitle('')).toBeNull()
})
test('handles H1 with leading spaces in title', () => {
const md = '# Spaced Title '
expect(extractMarkdownTitle(md)).toBe('Spaced Title')
})
})

View File

@@ -1,4 +1,13 @@
declare module 'web-animations-js';
declare module 'turndown-plugin-gfm' {
import TurndownService from 'turndown'
export function gfmHeadings(service: TurndownService): void
export function tables(service: TurndownService): void
export function taskListItems(service: TurndownService): void
export function strikethrough(service: TurndownService): void
export function gfm(service: TurndownService): void
}
declare module 'muuri';
declare module 'jalaali-js' {