From 88a7d2ad0aa4f90c2ca40424bba5d7b778e019d6 Mon Sep 17 00:00:00 2001
From: Antigravity
Date: Fri, 17 Jul 2026 18:47:44 +0000
Subject: [PATCH] =?UTF-8?q?fix(audit):=20nettoyage=20acc=C3=A8s,=20garde-f?=
=?UTF-8?q?ous=20build,=20unification=20quotas?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Semaine 1 — fuites et accès :
- /archive rendu accessible via sidebar (était invisible)
- Suppression pages orphelines : /chat, /graph, /support, test-title-suggestions
- Fixes i18n : search-modal (clé non interpolée), sidebar tooltips, export PDF
- 15 locales mises à jour
Semaine 2 — garde-fous :
- 8 erreurs TS fixees → ignoreBuildErrors: false (build type-safe)
- reactStrictMode: true (dev-only, zero impact prod)
- reserveUsageOrThrow → wrapper deprecie vers reserveAiUsageOrThrow
- auth-guard.ts : requireAuthOrResponse() + requireAdminOrResponse()
Tests 212/212 | Lint 0 erreur | Build OK | tsc 0 erreur
---
AGENTS.md | 6 +-
memento-note/app/(main)/chat/page.tsx | 44 -
memento-note/app/(main)/graph/page.tsx | 17 -
memento-note/app/(main)/insights/page.tsx | 5 -
memento-note/app/(main)/support/page.tsx | 156 ----
memento-note/app/actions/chat-actions.ts | 2 -
memento-note/app/api/ai/auto-labels/route.ts | 5 +-
.../app/api/ai/suggest-notebook/route.ts | 5 +-
memento-note/app/api/graph/route.ts | 195 -----
memento-note/app/api/graph/sync-all/route.ts | 84 --
memento-note/app/layout.tsx | 2 +-
.../app/test-title-suggestions/page.tsx | 61 --
memento-note/auth.config.ts | 5 +-
.../components/chat/chat-container.tsx | 222 -----
memento-note/components/chat/chat-input.tsx | 159 ----
.../components/chat/chat-messages.tsx | 84 --
memento-note/components/chat/chat-sidebar.tsx | 127 ---
.../note-editor/note-editor-toolbar.tsx | 8 +-
memento-note/components/note-graph-view.tsx | 789 ------------------
memento-note/components/search-modal.tsx | 4 +-
memento-note/components/sidebar.tsx | 20 +-
.../ai/services/notebook-wizard.service.ts | 4 +-
memento-note/lib/auth-guard.ts | 60 +-
memento-note/lib/credits.ts | 1 +
memento-note/lib/entitlements.ts | 23 +-
memento-note/locales/ar.json | 7 +-
memento-note/locales/de.json | 7 +-
memento-note/locales/en.json | 7 +-
memento-note/locales/es.json | 7 +-
memento-note/locales/fa.json | 7 +-
memento-note/locales/fr.json | 7 +-
memento-note/locales/hi.json | 7 +-
memento-note/locales/it.json | 7 +-
memento-note/locales/ja.json | 7 +-
memento-note/locales/ko.json | 7 +-
memento-note/locales/nl.json | 7 +-
memento-note/locales/pl.json | 7 +-
memento-note/locales/pt.json | 7 +-
memento-note/locales/ru.json | 7 +-
memento-note/locales/zh.json | 7 +-
memento-note/next.config.ts | 12 +-
.../tests/unit/brainstorm-billing.test.ts | 92 +-
42 files changed, 216 insertions(+), 2081 deletions(-)
delete mode 100644 memento-note/app/(main)/chat/page.tsx
delete mode 100644 memento-note/app/(main)/graph/page.tsx
delete mode 100644 memento-note/app/(main)/support/page.tsx
delete mode 100644 memento-note/app/api/graph/route.ts
delete mode 100644 memento-note/app/api/graph/sync-all/route.ts
delete mode 100644 memento-note/app/test-title-suggestions/page.tsx
delete mode 100644 memento-note/components/chat/chat-container.tsx
delete mode 100644 memento-note/components/chat/chat-input.tsx
delete mode 100644 memento-note/components/chat/chat-messages.tsx
delete mode 100644 memento-note/components/chat/chat-sidebar.tsx
delete mode 100644 memento-note/components/note-graph-view.tsx
diff --git a/AGENTS.md b/AGENTS.md
index 85d58cc..d01e2f2 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -10,8 +10,8 @@
- Flux Excalidraw / diagrammes générés : accès via notification en plus d'une simple redirection ; priorité à la mise en page et au texte contenu dans les formes ; proposer des modes visuels (ex. coloré vs plus austère) tout en visant un rendu proche du style Excalidraw (polices, look).
- **Interdiction d'écrire des tests** sauf demande explicite ; en CI, seul `npm run test:unit` (`tests/unit/**`) — pas `tests/migration/` ; ne jamais générer de code superflu.
- Déploiement : privilégier le chemin rapide (artifact Next.js en CI + `Dockerfile.prebuilt`) ; CI/CD très robuste (pas d'image Docker obsolète en prod, pas de migrations/schéma DB via le workflow deploy) ; éviter les rebuild Docker complets inutiles (~15 min par itération) ; **ne pas pousser un déploiement quand des features clés sont inachevées** ; ne pas insister sur le déploiement tant que le produit n'est pas fini ou meilleur ; **ne jamais hardcoder d'IP/URL** dans le repo ou les défauts CI — uniquement variables d'env (ex. `GRAFANA_BIND`, `GRAFANA_URL`) ; la config runtime reste sur le serveur dans `.env.docker`. **Machines** : agent/dev = **192.168.1.83** (devSanbox) ; prod = **192.168.1.190** (`/opt/memento`) — **ne jamais confondre** les deux au diagnostic services. **CI/CD Gitea spécifique** : (1) CI `ubuntu-24.04` = lint + tests + build (validation) ; deploy `docker-host` = build sur le serveur → `/tmp/web-artifact.tgz` → `deploy-prod.sh` — **sans** upload/download-artifact inter-runners (stockage Gitea instable ; si artifacts utilisés, **@v3** uniquement — @v4 → `GHESNotSupportedError`) ; (2) `Dockerfile.socket.prebuilt` doit utiliser `--legacy-peer-deps` dans `npm install` (conflit TipTap 3.22.5 vs 3.23.6) ; (3) le serveur de prod (192.168.1.190) **ne peut pas pull Docker Hub** (DNS cassé) — le build Docker complet échoue, seul le chemin prebuilt artifact fonctionne ; (4) `docker-entrypoint.sh` applique les migrations Prisma **avant** de démarrer le serveur Next.js (ordre correct) ; (5) rollback d'urgence : `docker tag memento-memento-note:rollback memento-memento-note:latest && docker compose up -d --force-recreate memento-note` ; (6) `scripts/deploy-prod.sh` charge `/opt/memento/.env.docker` via un **parseur robuste ligne-par-ligne** (jamais `source` — crash `unexpected EOF` sur guillemet non fermé) et teste Postgres avec `pg_isready` + credentials **du conteneur** (`$POSTGRES_USER` interne) ; (7) lint CI = `eslint --max-warnings 9999` : les **warnings ne bloquent pas**, seules les **erreurs** bloquent (ex. `no-html-link-for-pages` → `` obligatoire pour la navigation interne, pas ``) ; **TRAVAILLER SUR UNE BRANCHE** pendant le dev, ne push sur `main` que quand le code est testé et fonctionnel — **avant push `main`** : exécuter localement `npm run lint`, `npm run test:unit`, `npm run build` (même enchaînement que CI) ; chaque push sur `main` déclenche un déploiement automatique en production.
-- Authentification : priorité à l'inscription/connexion via **Google OAuth** (plutôt qu'un compte email/mot de passe) ; exiger une vraie déconnexion (invalidation session/cookies — pas de reconnexion implicite, y compris en navigation privée) ; prod : `AUTH_GOOGLE_ID`/`AUTH_GOOGLE_SECRET` dans `/opt/memento/.env.docker` **et** variables/secrets Gitea Actions (workflow deploy n'écrit pas les valeurs vides) — absence → `invalid_client` ; redirect URI exacte `https://memento-note.com/api/auth/callback/google` ; config via **Google Auth Platform** (Branding → Audience **In production**, pas Testing → Clients Web) ; `ALLOW_REGISTRATION=true` requis pour inscription email (env ou `SystemConfig` en base — la DB peut primer sur l'env).
-- Priorité absolue à la qualité UX, même si l'implémentation est complexe (« je m'en fous si c'est complexe ») ; préférer les **solutions long terme** aux rustines ; si l'utilisateur demande explicitement d'**analyser sans coder**, expliquer d'abord et **ne pas modifier le code** tant qu'il n'a pas validé ; pour les **tâches ops** (config OAuth prod, serveur), préfère un **guidage interactif étape par étape** plutôt qu'un bloc d'instructions monolithique ; **ne jamais affirmer qu'un correctif ou une feature est fait sans vérification réelle** (app, prototype `architectural-grid`, ou test), **notamment navigation recherche/liste notes et vue `/insights` vs fichiers prototype** — l'utilisateur sanctionne fermement les fausses déclarations ; **ouverture note liée depuis l'éditeur** (ex. bloc live « Ouvrir ») : **split peek inline** animé (`lib/note-peek-sync.ts`, `note-editor-split-peek.tsx` — éditeur courant à **gauche**, note liée en lecture seule à **droite** en LTR ; **inversé en RTL** `fa`/`ar`), **pas nouvel onglet** ; **ne jamais annuler du code non commité** (`git checkout`, reset fichier) **sans demande explicite** (perte de travail documentée, ex. drag handle éditeur) ; **ne jamais remettre du code que l'utilisateur a explicitement retiré sans demander d'abord** (ex. `reserveUsageOrThrow` retiré intentionnellement de `organize-notebook.ts` — agent l'a remis sans demander → user mécontent) ; **correction i18n ou spec doc** : **ne pas refondre logique/UI** hors scope (ex. US-4 `structuredViewBlock` — garder le dual-mode base locale + lien carnet, pas de suppression du mode local) ; en frustration ou pour déléguer, **prévoir des prompts / briefs d'implémentation détaillés** (autre modèle ou dev), en plus des briefs outil de design.
+- Authentification : priorité à l'inscription/connexion via **Google OAuth** (plutôt qu'un compte email/mot de passe) ; exiger une vraie déconnexion (invalidation session/cookies — pas de reconnexion implicite, y compris en navigation privée) ; prod : `AUTH_GOOGLE_ID`/`AUTH_GOOGLE_SECRET` dans `/opt/memento/.env.docker` **et** variables/secrets Gitea Actions (workflow deploy n'écrit pas les valeurs vides) — absence → `invalid_client` ; redirect URI exacte `https://memento-note.com/api/auth/callback/google` ; config via **Google Auth Platform** (Branding → Audience **In production**, pas Testing → Clients Web) ; `ALLOW_REGISTRATION=true` requis pour inscription email (env ou `SystemConfig` en base — la DB peut primer sur l'env) ; **navigateur intégré Cursor peu fiable pour le login** (focus volé, cookies isolés, OAuth Google souvent bloqué) — préférer un vrai navigateur pour la connexion manuelle, et pour l'automation (démos/Playwright) une **session JWT/cookies stockée** plutôt que de rejouer le login dans le browser Cursor.
+- Priorité absolue à la qualité UX, même si l'implémentation est complexe (« je m'en fous si c'est complexe ») ; préférer les **solutions long terme** aux rustines ; si l'utilisateur demande explicitement d'**analyser sans coder**, expliquer d'abord et **ne pas modifier le code** tant qu'il n'a pas validé ; pour les **tâches ops** (config OAuth prod, serveur), préfère un **guidage interactif étape par étape** plutôt qu'un bloc d'instructions monolithique ; **ne jamais affirmer qu'un correctif ou une feature est fait sans vérification réelle** (app, prototype `architectural-grid`, ou test), **notamment navigation recherche/liste notes et vue `/insights` vs fichiers prototype** — l'utilisateur sanctionne fermement les fausses déclarations ; **ouverture note liée depuis l'éditeur** (ex. bloc live « Ouvrir ») : **split peek inline** animé (`lib/note-peek-sync.ts`, `note-editor-split-peek.tsx` — éditeur courant à **gauche**, note liée en lecture seule à **droite** en LTR ; **inversé en RTL** `fa`/`ar`), **pas nouvel onglet** ; **ne jamais annuler du code non commité** (`git checkout`, reset fichier) **sans demande explicite** (perte de travail documentée, ex. drag handle éditeur) ; **ne jamais remettre du code que l'utilisateur a explicitement retiré sans demander d'abord** (ex. `reserveUsageOrThrow` retiré intentionnellement de `organize-notebook.ts` — agent l'a remis sans demander → user mécontent) ; **correction i18n ou spec doc** : **ne pas refondre logique/UI** hors scope (ex. US-4 `structuredViewBlock` — garder le dual-mode base locale + lien carnet, pas de suppression du mode local) ; en frustration ou pour déléguer, **prévoir des prompts / briefs d'implémentation détaillés** (autre modèle ou dev), en plus des briefs outil de design ; **vidéos promo** : VO **anglaise** ; montrer de **vraies features authentifiées** (dashboard, Memory Echo, Insights, Revision, Agents…) — pas un slideshow landing seul ; **interdit Ken Burns / `zoompan`** (surtout sur fonds pointillés → moiré / tremblement) — images fixes, fond uni stable à la capture.
- Livraison : **une user story à la fois**, tester et valider avec l'utilisateur avant la suivante (pas d'auto-validation ni d'enchaînement de code non demandé) ; suivi dans `docs/user-stories.md` ; briefs pour outil de design externe sur demande ; **avant de développer une story, vérifier d'abord dans le code si la feature existe déjà** — les docs de stories (`docs/story-nextgen-editor.md`, `docs/user-stories.md`) sont **souvent périmés** (features livrées mais marquées « à faire ») ; **dashboard Second Brain** : même rythme — **une feature à la fois**, validation explicite avant la suivante.
- **Facturation & quotas IA** : limites mensuelles, tiers (BASIC/PRO/BUSINESS/ENTERPRISE) et Price IDs Stripe via **Admin > Facturation & quotas** (`/admin/billing`) — pas via `.env` pour le métier ; secrets Stripe (`STRIPE_SECRET_KEY`, webhook) restent en env serveur ; doc `memento-note/docs/admin-billing-quotas-guide.md` ; **tier BASIC sans accès MCP** ; chaque usage IA (dashboard, agents, MCP si autorisé, etc.) doit **décompter le quota** — mécanisme = **réservation atomique upfront** (Redis/Postgres) **avant** l'appel IA, sans rollback si l'appel échoue ensuite ; BYOK peut contourner selon config — ne pas affirmer un déploiement prod sans vérif réelle (ex. travail MCP/quotas encore local).
@@ -28,4 +28,4 @@
- Migrations prebuilt + vérif deploy : `docker compose exec memento-note node ./node_modules/prisma/build/index.js migrate deploy` (pas `npx prisma`) ; helper `scripts/migrate-docker.sh` ; `GET /api/build-info` (SHA Git) ; comparer `127.0.0.1:3000` et domaine Cloudflare — purger cache si versions divergent ; 403 `/api/manifest` côté domaine = souvent Cloudflare.
- Éditeur riche : `rich-text-editor.tsx` — `immediatelyRender: false` ; activer **`shouldRerenderOnTransaction: false`** (quick win perf TipTap 2.5) ; **drag handle / menu bloc** via **`@tiptap/extension-drag-handle-react`** (spec officielle — pas de double plugin `DragHandleExtension` + composant React, pas de repositionnement maison) ; poignée dans **colonne gutter fixe** du wrapper (padding + `getReferencedVirtualElement`), pas sur le bord des listes/numéros ; CSS : **pas `opacity:0` sur `.drag-handle`** (visibilité gérée par le plugin) ; config/callbacks **stables hors composant** ; fondation blocs : `tiptap-unique-id-extension.ts` / **`data-id` persisté à la sauvegarde** (références « Copier la référence ») ; **Smart Paste** : `lib/editor/smart-paste-extension.ts` ; **peek split** note source : `lib/note-peek-sync.ts` + `note-editor-split-peek.tsx` ; **conversion markdown → texte enrichi** : un **seul** convertisseur `lib/markdown-to-html.ts` (`markdownToHtml`, gfm+breaks) + une **action atomique** `convertToRichText(html)` dans `note-editor-context.tsx` (applique le HTML **immédiatement** via `setContentImmediate` — pas le `setContent` débouncé 800ms — et bascule `isMarkdown=false`) ; toolbar (`handleConvertToRichtext`) **et** chat IA contextuel (`contextual-ai-chat.tsx`, action `toRichText` → `/api/ai/convert-markdown`) passent par cette action unique (ne pas dupliquer la conversion ni oublier de basculer `isMarkdown`) ; **US-4 `structuredViewBlock`** (`tiptap-structured-view-block-extension.tsx`, `structured-view-block-embed.tsx`) : **dual-mode** — base locale autonome par défaut (`/database`, `/vue`, `isLocal: true`) + option « Lier à un carnet » (Structured Views) ; i18n `structuredViewBlock.*` ; **rejeté** : ancien `databaseBlock` « Auteurs & Œuvres » et spec embed-only `docs/story-nextgen-editor-us4-redesign.md` ; epic active `docs/story-nextgen-editor.md` — priorité **PERF > NEXTGEN > UX > MOBILE > MARKDOWN**.
- Sync mutations notes entre composants : `memento-note/lib/note-change-sync.ts` (`emitNoteChange`, événement `NOTE_CHANGE_EVENT`) ; tracking quotas IA : événement `ai-usage-changed` (window) — doit être dispatché depuis **tous** les points d'appel IA (recherche sémantique, auto-tag, auto-title, chat, reformulation, analyse carnet, etc.) ; UI quotas : `usage-meter.tsx` (polling 5s + écoute événement).
-- Roadmap / écart prototype vs prod : Web Clipper — **`ClipperSimulator.tsx` = référence design uniquement** (pas de simulateur en prod) ; extension **`memento-note/extension/`** v0.3 **Side Panel** (clip page/sélection/lien ; popup Chrome se ferme au clic page — Side Panel pour la sélection) ; i18n extension **15 langues** (`_locales/`, détection locale navigateur ; script `extension/i18n/generate-translations.cjs`) ; **`host_permissions`** incl. LAN ; **URL serveur configurable en dev**, adresse prod figée en release ; cookies/session alignés avec l'instance cible ; **Flashcards IA SM-2 livrées** : `/revision`, `/api/flashcards/*`, génération depuis l'éditeur (GraduationCap) — réf. prototype `RevisionView.tsx` ; **Structured Views partiellement livrées** : schéma par carnet, Table/Kanban, champs partagés et valeurs par note (`/home` + toolbar carnet) — **suivi de tâches par carnet via Kanban structuré** (pas de vue agrégée Notes/Tâches sur la home ; cases à cocher inline dans les notes) ; **Éditeur Next-Gen livré** (doc `docs/story-nextgen-editor.md` **périmé** — vérifier le code) : US-1 poignée gutter unique (`editor-block-drag-handle.tsx` + `lib/editor/global-drag-handle-extension.ts`, extension Novel `tiptap-extension-global-drag-handle`), US-2 menu action bloc (`block-action-menu.tsx`), US-3 Smart Paste transclusion (`smart-paste-extension.ts` + `smart-paste-menu.tsx`), US-4 database inline (`structuredViewBlock`), plus `data-id` / `liveBlock` / peek split ; **`/insights` livré** : `app/(main)/insights/page.tsx` + `network-graph.tsx` (clusters sémantiques, bridge notes ; **≠ `/graph`**) ; **US-TEMPORAL reporté** ; encore en gap : transclusion bidirectionnelle complète, graphe knowledge enrichi (`GraphKnowledgeMap.tsx`) ; publication **Chrome Web Store** : icônes 16/48/128, privacy policy, `host_permissions` prod restreints vs build dev ; **Wizards** : `NotebookOrganizerDialog` (tags/doublons) branché via bouton "Tags IA" dans `home-client.tsx` — `StructuredViewsWizard` encore **orphelin** (pas de point d'entrée UI) ; **Publication web** (`note-editor-toolbar.tsx`) : deux modes — simple (copie directe) + IA (2-3 templates **visuellement distincts**, reformulation/mise en page **adaptée au contenu** — exercices, toggles, callouts — **images incluses**, KaTeX pour équations) — quota IA consommé uniquement sur publication IA ; l'utilisateur rejette les rendus « copie du texte » / mise en page nulle ; **champs DB publication** sur modèle `Note` : `publicSlug` (pas `publishedSlug`), `publishedContent` (pas `publishedHtml`) — `publishMode` est paramètre API uniquement (`mode: 'simple' | 'ai'`), **pas** un champ en base ; **Second Brain dashboard** (`/home`, `dashboard-view.tsx`) **livré** : layout v5 configurable (~15 widgets, `/api/dashboard/layout`), briefing agrégé (`/api/briefing`), pistes fast+enrich (`/api/briefing/paths`, `paths-fast.ts`), suggestions agents, scan Gmail (`GmailScanHistory`), navigation sidebar `/home` ; bento interactif — `MindMapCard` (clusters), suggestions agents (`AgentSuggestion`, cron), Memory Echo, sentiment, inbox/révisions ; **Gmail OAuth** (`/api/integrations/gmail/*`, pattern Calendar) — scan vols/colis/abonnements → notes + rappels ; crons entrypoint : agents, agent-suggestions, clusters, reminders, gmail-scan, sync-usage.
+- Roadmap / écart prototype vs prod : Web Clipper — **`ClipperSimulator.tsx` = référence design uniquement** (pas de simulateur en prod) ; extension **`memento-note/extension/`** v0.3 **Side Panel** (clip page/sélection/lien ; popup Chrome se ferme au clic page — Side Panel pour la sélection) ; i18n extension **15 langues** (`_locales/`, détection locale navigateur ; script `extension/i18n/generate-translations.cjs`) ; **`host_permissions`** incl. LAN ; **URL serveur configurable en dev**, adresse prod figée en release ; cookies/session alignés avec l'instance cible ; **Flashcards IA SM-2 livrées** : `/revision`, `/api/flashcards/*`, génération depuis l'éditeur (GraduationCap) — réf. prototype `RevisionView.tsx` ; **Structured Views partiellement livrées** : schéma par carnet, Table/Kanban, champs partagés et valeurs par note (`/home` + toolbar carnet) — **suivi de tâches par carnet via Kanban structuré** (pas de vue agrégée Notes/Tâches sur la home ; cases à cocher inline dans les notes) ; **Éditeur Next-Gen livré** (doc `docs/story-nextgen-editor.md` **périmé** — vérifier le code) : US-1 poignée gutter unique (`editor-block-drag-handle.tsx` + `lib/editor/global-drag-handle-extension.ts`, extension Novel `tiptap-extension-global-drag-handle`), US-2 menu action bloc (`block-action-menu.tsx`), US-3 Smart Paste transclusion (`smart-paste-extension.ts` + `smart-paste-menu.tsx`), US-4 database inline (`structuredViewBlock`), plus `data-id` / `liveBlock` / peek split ; **`/insights` livré** : `app/(main)/insights/page.tsx` + `network-graph.tsx` (clusters sémantiques, bridge notes ; **≠ `/graph`**) ; **US-TEMPORAL reporté** ; encore en gap : transclusion bidirectionnelle complète, graphe knowledge enrichi (`GraphKnowledgeMap.tsx`) ; publication **Chrome Web Store** : icônes 16/48/128, privacy policy, `host_permissions` prod restreints vs build dev ; **Wizards** : `NotebookOrganizerDialog` (tags/doublons) branché via bouton "Tags IA" dans `home-client.tsx` — `StructuredViewsWizard` encore **orphelin** (pas de point d'entrée UI) ; **Publication web** (`note-editor-toolbar.tsx`) : deux modes — simple (copie directe) + IA (2-3 templates **visuellement distincts**, reformulation/mise en page **adaptée au contenu** — exercices, toggles, callouts — **images incluses**, KaTeX pour équations) — quota IA consommé uniquement sur publication IA ; l'utilisateur rejette les rendus « copie du texte » / mise en page nulle ; **champs DB publication** sur modèle `Note` : `publicSlug` (pas `publishedSlug`), `publishedContent` (pas `publishedHtml`) — `publishMode` est paramètre API uniquement (`mode: 'simple' | 'ai'`), **pas** un champ en base ; **Second Brain dashboard** (`/home`, `dashboard-view.tsx`) **livré** : layout v5 configurable (~15 widgets, `/api/dashboard/layout`), briefing agrégé (`/api/briefing`), pistes fast+enrich (`/api/briefing/paths`, `paths-fast.ts`), suggestions agents, scan Gmail (`GmailScanHistory`), navigation sidebar `/home` ; bento interactif — `MindMapCard` (clusters), suggestions agents (`AgentSuggestion`, cron), Memory Echo, sentiment, inbox/révisions ; **Gmail OAuth** (`/api/integrations/gmail/*`, pattern Calendar) — scan vols/colis/abonnements → notes + rappels ; crons entrypoint : agents, agent-suggestions, clusters, reminders, gmail-scan, sync-usage ; **vidéos promo** : pipeline `promo-video/` (`make_promo.py`, `make_promo_features.py`, `SCENARIO.md`) — captures app authentifiée (Playwright + session JWT), VO EN (edge-tts), montage stable sans zoom.
diff --git a/memento-note/app/(main)/chat/page.tsx b/memento-note/app/(main)/chat/page.tsx
deleted file mode 100644
index 510d251..0000000
--- a/memento-note/app/(main)/chat/page.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import { Metadata } from 'next'
-import { auth } from '@/auth'
-import { redirect } from 'next/navigation'
-import { prisma } from '@/lib/prisma'
-import { ChatContainer } from '@/components/chat/chat-container'
-import { getConversations } from '@/app/actions/chat-actions'
-import { getSystemConfig } from '@/lib/config'
-
-export const metadata: Metadata = {
- title: 'Chat IA | Memento',
- description: 'Discutez avec vos notes et vos agents IA',
-}
-
-export default async function ChatPage() {
- const session = await auth()
- if (!session?.user?.id) redirect('/login')
-
- const userId = session.user.id
-
- // Fetch initial data
- const [conversations, notebooks, config] = await Promise.all([
- getConversations(),
- prisma.notebook.findMany({
- where: { userId },
- orderBy: { order: 'asc' }
- }),
- getSystemConfig(),
- ])
-
- // Check if web search tools are configured
- const webSearchAvailable = !!(
- config.WEB_SEARCH_PROVIDER || config.BRAVE_SEARCH_API_KEY || config.SEARXNG_URL || config.JINA_API_KEY
- )
-
- return (
-
-
-
- )
-}
diff --git a/memento-note/app/(main)/graph/page.tsx b/memento-note/app/(main)/graph/page.tsx
deleted file mode 100644
index 4519ffa..0000000
--- a/memento-note/app/(main)/graph/page.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-'use client'
-
-import dynamic from 'next/dynamic'
-
-// D3 uses browser APIs — must be loaded client-side only
-const NoteGraphView = dynamic(
- () => import('@/components/note-graph-view').then(m => m.NoteGraphView),
- { ssr: false }
-)
-
-export default function GraphPage() {
- return (
-
-
-
- )
-}
diff --git a/memento-note/app/(main)/insights/page.tsx b/memento-note/app/(main)/insights/page.tsx
index 3435f0a..cda27c7 100644
--- a/memento-note/app/(main)/insights/page.tsx
+++ b/memento-note/app/(main)/insights/page.tsx
@@ -19,7 +19,6 @@ import {
ChevronRight,
ChevronDown,
Database,
- ArrowRight,
Menu,
Network,
List,
@@ -28,7 +27,6 @@ import {
X,
} from 'lucide-react'
import { toast } from 'sonner'
-import Link from 'next/link'
import { useNotePeek, NotePeekPanel } from '@/components/note-peek'
import { createNote } from '@/app/actions/notes'
import { emitNoteChange } from '@/lib/note-change-sync'
@@ -455,9 +453,6 @@ export default function InsightsPage() {
{t('insightsView.semanticGraphLegend')}
-
- {t('insightsView.openGraphMap')}
-
diff --git a/memento-note/app/(main)/support/page.tsx b/memento-note/app/(main)/support/page.tsx
deleted file mode 100644
index 36050d0..0000000
--- a/memento-note/app/(main)/support/page.tsx
+++ /dev/null
@@ -1,156 +0,0 @@
-'use client'
-
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
-import { Button } from '@/components/ui/button';
-import { useLanguage } from '@/lib/i18n';
-
-export default function SupportPage() {
- const { t } = useLanguage();
-
- return (
-
-
-
- {t('support.title')}
-
-
- {t('support.description')}
-
-
-
-
-
-
-
- ☕
- {t('support.buyMeACoffee')}
-
-
-
-
- {t('support.donationDescription')}
-
-
-
- {t('support.kofiDescription')}
-
-
-
-
-
-
-
- 💚
- {t('support.sponsorOnGithub')}
-
-
-
-
- {t('support.sponsorDescription')}
-
-
-
- {t('support.githubDescription')}
-
-
-
-
-
-
-
- {t('support.howSupportHelps')}
-
-
-
-
-
💰 {t('support.directImpact')}
-
- - ☕ Keeps me fueled with coffee
- - 🐛 Covers hosting and server costs
- - ✨ Funds development of new features
- - 📚 Improves documentation
- - 🌍 Keeps Memento 100% open-source
-
-
-
-
🎁 {t('support.sponsorPerks')}
-
- - 🥉 $5/month - Bronze: Name in supporters list
- - 🥈 $15/month - Silver: Priority feature requests
- - 🥇 $50/month - Gold: Logo in footer, priority support
- - 💎 $100/month - Platinum: Custom features, consulting
-
-
-
-
-
-
-
-
- 💡 {t('support.transparency')}
-
-
-
- {t('support.transparencyDescription')}
-
-
-
- {t('support.hostingServers')}
- ~$20/month
-
-
- {t('support.domainSSL')}
- ~$15/year
-
-
- {t('support.aiApiCosts')}
- ~$30/month
-
-
- {t('support.totalExpenses')}
- ~$50/month
-
-
-
- Any amount beyond these costs goes directly into improving Memento
- and funding new features. Thank you for your support! 💚
-
-
-
-
-
-
{t('support.otherWaysTitle')}
-
-
-
- );
-}
diff --git a/memento-note/app/actions/chat-actions.ts b/memento-note/app/actions/chat-actions.ts
index d26eb9a..6530840 100644
--- a/memento-note/app/actions/chat-actions.ts
+++ b/memento-note/app/actions/chat-actions.ts
@@ -2,7 +2,6 @@
import { chatService } from '@/lib/ai/services'
import { auth } from '@/auth'
-import { revalidatePath } from 'next/cache'
import { prisma } from '@/lib/prisma'
/**
@@ -38,7 +37,6 @@ export async function sendChatMessage(
try {
const result = await chatService.chat(message, conversationId, notebookId)
- revalidatePath('/chat')
return { success: true, ...result }
} catch (error: any) {
console.error('[ChatAction] Error:', error)
diff --git a/memento-note/app/api/ai/auto-labels/route.ts b/memento-note/app/api/ai/auto-labels/route.ts
index 09db4a5..08e20f9 100644
--- a/memento-note/app/api/ai/auto-labels/route.ts
+++ b/memento-note/app/api/ai/auto-labels/route.ts
@@ -19,6 +19,7 @@ export async function POST(request: NextRequest) {
{ status: 401 }
)
}
+ const userId = session.user.id
// GDPR AI Consent check
if (!(await hasUserAiConsent())) {
@@ -61,12 +62,12 @@ export async function POST(request: NextRequest) {
}
const suggestions = await withAiQuota(
- session.user.id,
+ userId,
'auto_tag',
() =>
autoLabelCreationService.suggestLabels(
notebookId,
- session.user!.id,
+ userId,
language,
),
{ lane: 'tags' },
diff --git a/memento-note/app/api/ai/suggest-notebook/route.ts b/memento-note/app/api/ai/suggest-notebook/route.ts
index c8296c0..d7c207f 100644
--- a/memento-note/app/api/ai/suggest-notebook/route.ts
+++ b/memento-note/app/api/ai/suggest-notebook/route.ts
@@ -10,6 +10,7 @@ export async function POST(req: NextRequest) {
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const userId = session.user.id
if (!(await hasUserAiConsent())) {
return NextResponse.json({ error: 'ai_consent_required' }, { status: 403 })
@@ -32,11 +33,11 @@ export async function POST(req: NextRequest) {
}
const suggestedNotebook = await withAiQuota(
- session.user.id,
+ userId,
'auto_tag',
() => notebookSuggestionService.suggestNotebook(
noteContent,
- session.user!.id,
+ userId,
language
),
{ lane: 'tags' },
diff --git a/memento-note/app/api/graph/route.ts b/memento-note/app/api/graph/route.ts
deleted file mode 100644
index 28a077a..0000000
--- a/memento-note/app/api/graph/route.ts
+++ /dev/null
@@ -1,195 +0,0 @@
-import { NextRequest, NextResponse } from 'next/server'
-import prisma from '@/lib/prisma'
-import { auth } from '@/auth'
-
-// ── Stopwords FR + EN ─────────────────────────────────────────────────────────
-const STOPWORDS = new Set([
- 'le','la','les','de','du','des','un','une','et','en','au','aux','ce','se',
- 'sa','son','ses','mon','ma','mes','ton','ta','tes','que','qui','quoi','dont',
- 'il','elle','ils','elles','nous','vous','je','tu','on','par','pour','sur',
- 'sous','avec','dans','est','sont','pas','ne','plus','très','tout','comme',
- 'mais','donc','car','cet','cette','ces','leur','leurs','note','notes',
- 'the','a','an','and','or','but','in','on','at','to','for','of','with','by',
- 'from','is','are','was','were','be','been','have','has','had','do','does',
- 'did','will','would','could','should','may','might','this','that','these',
- 'those','it','its','they','them','their','he','she','we','you','not','no',
- 'so','if','as','up','out','about','also','just','can','all','any','get',
-])
-
-function stripHtml(html: string): string {
- return html.replace(/<[^>]+>/g, ' ').replace(/&\w+;/g, ' ')
-}
-
-function extractKeywords(text: string): Set {
- return new Set(
- stripHtml(text)
- .toLowerCase()
- .split(/[\s\p{P}]+/u)
- .filter(w => w.length >= 3 && !STOPWORDS.has(w) && !/^\d+$/.test(w))
- )
-}
-
-function jaccardSimilarity(a: Set, b: Set): number {
- if (a.size === 0 || b.size === 0) return 0
- let intersection = 0
- for (const w of a) if (b.has(w)) intersection++
- return intersection / (a.size + b.size - intersection)
-}
-
-type EdgeType = 'title_mention' | 'shared_label' | 'jaccard' | 'explicit_link' | 'semantic_echo'
-
-interface GraphEdge { source: string; target: string; weight: number; type: EdgeType }
-
-// GET /api/graph — connexions automatiques à 3 niveaux
-export async function GET(request: NextRequest) {
- const session = await auth()
- if (!session?.user?.id) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
-
- const userId = session.user.id
- const { searchParams } = new URL(request.url)
- const notebookId = searchParams.get('notebookId') || undefined
-
- const notes = await prisma.note.findMany({
- where: { userId, trashedAt: null, ...(notebookId ? { notebookId } : {}) },
- select: {
- id: true, title: true, content: true, notebookId: true, createdAt: true,
- labelRelations: { select: { id: true } },
- notebook: { select: { id: true, name: true } },
- },
- take: 500,
- orderBy: { updatedAt: 'desc' },
- })
-
- if (notes.length === 0) return NextResponse.json({ nodes: [], edges: [] })
-
- const ids = notes.map(n => n.id)
-
- // Query NoteLink manually created relationships
- const noteLinks = await (prisma as any).noteLink.findMany({
- where: {
- sourceNoteId: { in: ids },
- targetNoteId: { in: ids }
- },
- select: {
- sourceNoteId: true,
- targetNoteId: true,
- contextSnippet: true
- }
- })
-
- // Query MemoryEchoInsight semantic relationships
- const echoInsights = await (prisma as any).memoryEchoInsight.findMany({
- where: {
- userId,
- dismissed: false,
- note1Id: { in: ids },
- note2Id: { in: ids }
- },
- select: {
- note1Id: true,
- note2Id: true,
- similarityScore: true,
- insight: true
- }
- })
-
- // Pré-calcul
- const keywordsMap = new Map>()
- const labelMap = new Map>()
- for (const note of notes) {
- keywordsMap.set(note.id, extractKeywords(`${note.title ?? ''} ${note.content}`))
- labelMap.set(note.id, new Set(note.labelRelations.map((l: any) => l.id)))
- }
-
- const EDGE_TYPE_PRIORITY: Record = {
- explicit_link: 5,
- semantic_echo: 4,
- title_mention: 3,
- shared_label: 2,
- jaccard: 1,
- }
-
- const edgeMap = new Map()
- function upsertEdge(a: string, b: string, weight: number, type: EdgeType) {
- const key = a < b ? `${a}--${b}` : `${b}--${a}`
- const ex = edgeMap.get(key)
- if (!ex) {
- edgeMap.set(key, { source: a < b ? a : b, target: a < b ? b : a, weight, type })
- } else {
- const exPriority = EDGE_TYPE_PRIORITY[ex.type] || 0
- const curPriority = EDGE_TYPE_PRIORITY[type] || 0
- if (curPriority > exPriority || (curPriority === exPriority && weight > ex.weight)) {
- edgeMap.set(key, { source: a < b ? a : b, target: a < b ? b : a, weight, type })
- }
- }
- }
-
- // ── Niveau 1 : Title Mention (comme Obsidian "unlinked mentions") ──────────
- for (const noteA of notes) {
- const title = (noteA.title ?? '').trim().toLowerCase()
- if (title.length < 3) continue
- const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
- const re = new RegExp(`(? lb.has(l)).length
- if (shared > 0) upsertEdge(ids[i], ids[j], Math.min(0.5 + shared * 0.15, 0.9), 'shared_label')
- }
- }
-
- // ── Niveau 3 : Jaccard (désactivé > 500 notes) ───────────────────────────
- if (notes.length <= 500) {
- for (let i = 0; i < ids.length; i++) {
- const kwI = keywordsMap.get(ids[i])!
- const candidates: { j: number; score: number }[] = []
- for (let j = i + 1; j < ids.length; j++) {
- const score = jaccardSimilarity(kwI, keywordsMap.get(ids[j])!)
- if (score >= 0.12) candidates.push({ j, score })
- }
- candidates.sort((a, b) => b.score - a.score).slice(0, 10)
- .forEach(({ j, score }) => upsertEdge(ids[i], ids[j], score * 0.8, 'jaccard'))
- }
- }
-
- // ── Niveau 4 : WikiLinks explicites (NoteLink) ─────────────────────────────
- for (const link of noteLinks) {
- upsertEdge(link.sourceNoteId, link.targetNoteId, 1.0, 'explicit_link')
- }
-
- // ── Niveau 5 : Échos sémantiques IA (MemoryEchoInsight) ────────────────────
- for (const echo of echoInsights) {
- upsertEdge(echo.note1Id, echo.note2Id, echo.similarityScore, 'semantic_echo')
- }
-
- const degreeMap = new Map()
- for (const e of edgeMap.values()) {
- degreeMap.set(e.source, (degreeMap.get(e.source) ?? 0) + 1)
- degreeMap.set(e.target, (degreeMap.get(e.target) ?? 0) + 1)
- }
-
- const nodes = notes.map(n => ({
- id: n.id,
- title: n.title || 'Sans titre',
- notebookId: n.notebookId,
- createdAt: n.createdAt,
- degree: degreeMap.get(n.id) ?? 0,
- }))
-
- // Build clusters (notebooks)
- const notebookMap = new Map()
- for (const n of notes) {
- if (n.notebook) notebookMap.set(n.notebook.id, n.notebook.name)
- }
- const clusters = [...notebookMap.entries()].map(([id, name]) => ({ id, name }))
-
- return NextResponse.json({ nodes, edges: Array.from(edgeMap.values()), clusters })
-}
diff --git a/memento-note/app/api/graph/sync-all/route.ts b/memento-note/app/api/graph/sync-all/route.ts
deleted file mode 100644
index 8b719d4..0000000
--- a/memento-note/app/api/graph/sync-all/route.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-import { NextResponse } from 'next/server'
-import prisma from '@/lib/prisma'
-import { auth } from '@/auth'
-
-const WIKILINK_RE = /\[\[([^\]|#]+?)(?:[|#][^\]]+)?\]\]/g
-
-function extractWikilinks(content: string): { title: string; snippet: string }[] {
- const plain = content.replace(/<[^>]+>/g, ' ')
- const results: { title: string; snippet: string }[] = []
- const seen = new Set()
- let match: RegExpExecArray | null
-
- WIKILINK_RE.lastIndex = 0
- while ((match = WIKILINK_RE.exec(plain)) !== null) {
- const title = match[1].trim()
- if (!title || seen.has(title.toLowerCase())) continue
- seen.add(title.toLowerCase())
- const start = Math.max(0, match.index - 50)
- const end = Math.min(plain.length, match.index + match[0].length + 50)
- const snippet = plain.slice(start, end).replace(/\s+/g, ' ').trim()
- results.push({ title, snippet })
- }
-
- return results
-}
-
-/**
- * POST /api/graph/sync-all
- * Batch-sync [[wikilinks]] for ALL notes of the authenticated user.
- * Call once to populate the NoteLink table from existing notes.
- */
-export async function POST() {
- const session = await auth()
- if (!session?.user?.id) {
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
- }
- const userId = session.user.id
-
- // Get all non-trashed notes with content
- const notes = await prisma.note.findMany({
- where: { userId, trashedAt: null, content: { not: '' } },
- select: { id: true, content: true, notebookId: true },
- })
-
- let totalLinks = 0
-
- for (const note of notes) {
- if (!note.content.includes('[[')) continue
-
- const wikilinks = extractWikilinks(note.content)
- if (wikilinks.length === 0) continue
-
- for (const { title, snippet } of wikilinks) {
- const targetNote = await prisma.note.findFirst({
- where: {
- userId,
- title: { equals: title, mode: 'insensitive' },
- trashedAt: null,
- },
- select: { id: true },
- })
-
- if (!targetNote) {
- // Skip stubs in batch sync — we only link existing notes
- continue
- }
-
- if (targetNote.id === note.id) continue
-
- try {
- await (prisma as any).noteLink.upsert({
- where: { sourceNoteId_targetNoteId: { sourceNoteId: note.id, targetNoteId: targetNote.id } },
- update: { contextSnippet: snippet },
- create: { sourceNoteId: note.id, targetNoteId: targetNote.id, contextSnippet: snippet },
- })
- totalLinks++
- } catch {
- // ignore duplicate constraint errors
- }
- }
- }
-
- return NextResponse.json({ synced: notes.length, links: totalLinks })
-}
diff --git a/memento-note/app/layout.tsx b/memento-note/app/layout.tsx
index 7483568..57143ad 100644
--- a/memento-note/app/layout.tsx
+++ b/memento-note/app/layout.tsx
@@ -101,7 +101,7 @@ export default async function RootLayout({
const htmlStyle = {
'--color-brand-accent': serverAccent,
// Aide le navigateur à peindre scrollbars/inputs dans le bon mode
- colorScheme: wantsDarkClass || resolvedTheme === 'dark' || resolvedTheme === 'midnight' ? 'dark' : resolvedTheme === 'light' ? 'light' : undefined,
+ colorScheme: wantsDarkClass ? 'dark' : resolvedTheme === 'light' ? 'light' : undefined,
} as CSSProperties
return (
diff --git a/memento-note/app/test-title-suggestions/page.tsx b/memento-note/app/test-title-suggestions/page.tsx
deleted file mode 100644
index 6956af3..0000000
--- a/memento-note/app/test-title-suggestions/page.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-'use client'
-
-import { useState } from 'react'
-import { useTitleSuggestions } from '@/hooks/use-title-suggestions'
-
-export default function TestTitleSuggestionsPage() {
- const [content, setContent] = useState('')
-
- const { suggestions, isAnalyzing, error } = useTitleSuggestions({
- content,
- enabled: true // Always enabled for testing
- })
-
- const wordCount = content.split(/\s+/).filter(w => w.length > 0).length
-
- return (
-
-
Test Title Suggestions
-
-
-
-
-
-
-
Word count: {wordCount} / 50
-
Status: {isAnalyzing ? 'Analyzing...' : 'Idle'}
- {error &&
Error: {error}
}
-
-
-
-
Suggestions ({suggestions.length}):
- {suggestions.length > 0 ? (
-
- {suggestions.map((s, i) => (
- -
- {s.title} (confidence: {s.confidence}%)
- {s.reasoning &&
→ {s.reasoning}
}
-
- ))}
-
- ) : (
-
No suggestions yet. Type 50+ words and wait 2 seconds.
- )}
-
-
-
-
-
-
- )
-}
diff --git a/memento-note/auth.config.ts b/memento-note/auth.config.ts
index 305c359..17c61e3 100644
--- a/memento-note/auth.config.ts
+++ b/memento-note/auth.config.ts
@@ -23,15 +23,12 @@ export const authConfig = {
nextUrl.pathname.startsWith('/settings') ||
nextUrl.pathname.startsWith('/lab') ||
nextUrl.pathname.startsWith('/agents') ||
- nextUrl.pathname.startsWith('/chat') ||
nextUrl.pathname.startsWith('/canvas') ||
nextUrl.pathname.startsWith('/notebooks') ||
nextUrl.pathname.startsWith('/note/') ||
nextUrl.pathname.startsWith('/brainstorm') ||
nextUrl.pathname.startsWith('/insights') ||
- nextUrl.pathname.startsWith('/graph') ||
- nextUrl.pathname.startsWith('/revision') ||
- nextUrl.pathname.startsWith('/support');
+ nextUrl.pathname.startsWith('/revision');
const isAdminPage = nextUrl.pathname.startsWith('/admin');
const isPublicPage = nextUrl.pathname === '/' ||
nextUrl.pathname === '/login' ||
diff --git a/memento-note/components/chat/chat-container.tsx b/memento-note/components/chat/chat-container.tsx
deleted file mode 100644
index 04b3c96..0000000
--- a/memento-note/components/chat/chat-container.tsx
+++ /dev/null
@@ -1,222 +0,0 @@
-'use client'
-
-import { useState, useEffect, useRef, useCallback } from 'react'
-import { useChat } from '@ai-sdk/react'
-import { DefaultChatTransport } from 'ai'
-import { ChatSidebar } from './chat-sidebar'
-import { ChatMessages } from './chat-messages'
-import { ChatInput } from './chat-input'
-import { createConversation, getConversationDetails, getConversations, deleteConversation } from '@/app/actions/chat-actions'
-import { toast } from 'sonner'
-import type { UIMessage } from 'ai'
-import { useLanguage } from '@/lib/i18n'
-
-interface ChatContainerProps {
- initialConversations: any[]
- notebooks: any[]
- webSearchAvailable?: boolean
-}
-
-export function ChatContainer({ initialConversations, notebooks, webSearchAvailable }: ChatContainerProps) {
- const { t, language } = useLanguage()
- const [conversations, setConversations] = useState(initialConversations)
- const [currentId, setCurrentId] = useState(null)
- const [selectedNotebook, setSelectedNotebook] = useState(undefined)
- const [webSearchEnabled, setWebSearchEnabled] = useState(false)
- const [historyMessages, setHistoryMessages] = useState([])
- const [isLoadingHistory, setIsLoadingHistory] = useState(false)
-
- // Prevents the useEffect from loading an empty conversation
- // when we just created one via createConversation()
- const skipHistoryLoad = useRef(false)
- const scrollRef = useRef(null)
-
- const transport = useRef(new DefaultChatTransport({
- api: '/api/chat',
- })).current
-
- const {
- messages,
- sendMessage,
- status,
- setMessages,
- stop,
- } = useChat({
- transport,
- onError: (error) => {
- toast.error(error.message || t('chat.assistantError'))
- },
- })
-
- const isLoading = status === 'submitted' || status === 'streaming'
-
- const refreshConversations = useCallback(async () => {
- try {
- const updated = await getConversations()
- setConversations(updated)
- } catch {}
- }, [])
-
- // Timeout warning: show toast if response takes > 30s
- useEffect(() => {
- if (!isLoading) return
- const timer = setTimeout(() => {
- toast.warning(t('chat.timeoutWarning') || 'Response is taking longer than expected...')
- }, 30000)
- return () => clearTimeout(timer)
- }, [isLoading, t])
-
- // Sync historyMessages after each completed streaming response
- // so the display doesn't revert to stale history.
- // Also refresh sidebar so new conversation appears.
- useEffect(() => {
- if (status === 'ready' && messages.length > 0) {
- setHistoryMessages([...messages])
- refreshConversations()
- }
- }, [status, messages, refreshConversations])
-
- // Load conversation details when the user selects a different conversation
- useEffect(() => {
- // Skip if we just created the conversation — useChat already has the messages
- if (skipHistoryLoad.current) {
- skipHistoryLoad.current = false
- return
- }
-
- if (currentId) {
- const loadMessages = async () => {
- setIsLoadingHistory(true)
- try {
- const details = await getConversationDetails(currentId)
- if (details) {
- const loaded: UIMessage[] = details.messages.map((m: any, i: number) => ({
- id: m.id || `hist-${i}`,
- role: m.role as 'user' | 'assistant',
- parts: [{ type: 'text' as const, text: m.content }],
- }))
- setHistoryMessages(loaded)
- setMessages(loaded)
- }
- } catch (error) {
- toast.error(t('chat.loadError'))
- } finally {
- setIsLoadingHistory(false)
- }
- }
- loadMessages()
- } else {
- setMessages([])
- setHistoryMessages([])
- }
-
- }, [currentId])
-
- const handleSendMessage = async (content: string, notebookId?: string) => {
- if (notebookId) {
- setSelectedNotebook(notebookId)
- }
-
- // If no active conversation, create one BEFORE streaming
- let convId = currentId
- if (!convId) {
- try {
- const result = await createConversation(content, notebookId || selectedNotebook)
- convId = result.id
- // Tell the useEffect to skip — we don't want to load an empty conversation
- skipHistoryLoad.current = true
- setCurrentId(convId)
- setHistoryMessages([])
- setConversations((prev) => [
- { id: result.id, title: result.title, updatedAt: new Date() },
- ...prev,
- ])
- } catch {
- toast.error(t('chat.createError'))
- return
- }
- }
-
- try {
- await sendMessage(
- { text: content },
- {
- body: {
- conversationId: convId,
- notebookId: notebookId || selectedNotebook || undefined,
- language,
- webSearch: webSearchEnabled,
- },
- }
- )
- } catch (error) {
- console.error('Chat send error:', error)
- toast.error(t('chat.assistantError') || 'Failed to send message')
- }
- }
-
- const handleNewChat = () => {
- setCurrentId(null)
- setMessages([])
- setHistoryMessages([])
- setSelectedNotebook(undefined)
- setWebSearchEnabled(false)
- }
-
- const handleDeleteConversation = async (id: string) => {
- try {
- await deleteConversation(id)
- if (currentId === id) {
- handleNewChat()
- }
- await refreshConversations()
- } catch {
- toast.error(t('chat.deleteError'))
- }
- }
-
- // During streaming or if useChat has more messages than history, prefer useChat
- const displayMessages = isLoading || messages.length > historyMessages.length
- ? messages
- : historyMessages
-
- // Auto-scroll to bottom when messages change
- useEffect(() => {
- if (scrollRef.current) {
- scrollRef.current.scrollTop = scrollRef.current.scrollHeight
- }
- }, [displayMessages])
-
- return (
-
-
-
-
-
-
-
-
-
-
- setWebSearchEnabled(prev => !prev)}
- webSearchAvailable={webSearchAvailable}
- />
-
-
-
-
- )
-}
diff --git a/memento-note/components/chat/chat-input.tsx b/memento-note/components/chat/chat-input.tsx
deleted file mode 100644
index c847655..0000000
--- a/memento-note/components/chat/chat-input.tsx
+++ /dev/null
@@ -1,159 +0,0 @@
-'use client'
-
-import { useState, useRef, useEffect } from 'react'
-import { Send, BookOpen, X, Globe, Square } from 'lucide-react'
-import { getNotebookIcon } from '@/lib/notebook-icon'
-import { Button } from '@/components/ui/button'
-import { Textarea } from '@/components/ui/textarea'
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select"
-import { cn } from '@/lib/utils'
-import { Badge } from '@/components/ui/badge'
-import { useLanguage } from '@/lib/i18n'
-
-interface ChatInputProps {
- onSend: (message: string, notebookId?: string) => void
- isLoading?: boolean
- onStop?: () => void
- notebooks: any[]
- currentNotebookId?: string | null
- webSearchEnabled?: boolean
- onToggleWebSearch?: () => void
- webSearchAvailable?: boolean
-}
-
-export function ChatInput({ onSend, isLoading, onStop, notebooks, currentNotebookId, webSearchEnabled, onToggleWebSearch, webSearchAvailable }: ChatInputProps) {
- const { t } = useLanguage()
- const [input, setInput] = useState('')
- const [selectedNotebook, setSelectedNotebook] = useState(currentNotebookId || undefined)
- const textareaRef = useRef(null)
-
- useEffect(() => {
- if (currentNotebookId) {
- setSelectedNotebook(currentNotebookId)
- }
- }, [currentNotebookId])
-
- const handleSend = () => {
- if (!input.trim() || isLoading) return
- onSend(input, selectedNotebook)
- setInput('')
- if (textareaRef.current) {
- textareaRef.current.style.height = 'auto'
- }
- }
-
- const handleKeyDown = (e: React.KeyboardEvent) => {
- if (e.key === 'Enter' && !e.shiftKey) {
- e.preventDefault()
- handleSend()
- }
- }
-
- useEffect(() => {
- if (textareaRef.current) {
- textareaRef.current.style.height = 'auto'
- textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`
- }
- }, [input])
-
- return (
-
-
-
-
-
- {t('chat.disclaimer')}
-
-
-
- )
-}
diff --git a/memento-note/components/chat/chat-messages.tsx b/memento-note/components/chat/chat-messages.tsx
deleted file mode 100644
index 18a1393..0000000
--- a/memento-note/components/chat/chat-messages.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-'use client'
-
-import { User, Bot, Loader2 } from 'lucide-react'
-import { cn } from '@/lib/utils'
-import ReactMarkdown from 'react-markdown'
-import remarkGfm from 'remark-gfm'
-import { Avatar, AvatarFallback } from '@/components/ui/avatar'
-import { useLanguage } from '@/lib/i18n'
-
-interface ChatMessagesProps {
- messages: any[]
- isLoading?: boolean
-}
-
-function getMessageContent(msg: any): string {
- if (typeof msg.content === 'string') return msg.content
- if (msg.parts && Array.isArray(msg.parts)) {
- return msg.parts
- .filter((p: any) => p.type === 'text')
- .map((p: any) => p.text)
- .join('')
- }
- return ''
-}
-
-export function ChatMessages({ messages, isLoading }: ChatMessagesProps) {
- const { t } = useLanguage()
-
- return (
-
- {messages.length === 0 && !isLoading && (
-
-
-
-
-
- {t('chat.welcome')}
-
-
- )}
-
- {messages.map((msg, index) => {
- const content = getMessageContent(msg)
- const isLastAssistant = msg.role === 'assistant' && index === messages.length - 1 && isLoading
-
- return (
-
- {msg.role === 'user' ? (
-
- ) : (
-
-
-
-
-
- {content ? (
-
- {content}
-
- ) : isLastAssistant ? (
-
-
- {t('chat.searching')}
-
- ) : null}
-
-
- )}
-
- )
- })}
-
- )
-}
diff --git a/memento-note/components/chat/chat-sidebar.tsx b/memento-note/components/chat/chat-sidebar.tsx
deleted file mode 100644
index 9241144..0000000
--- a/memento-note/components/chat/chat-sidebar.tsx
+++ /dev/null
@@ -1,127 +0,0 @@
-'use client'
-
-import { useState } from 'react'
-import { formatDistanceToNow } from 'date-fns'
-import { fr } from 'date-fns/locale/fr'
-import { enUS } from 'date-fns/locale/en-US'
-import { MessageSquare, Trash2, Plus, X } from 'lucide-react'
-import { Button } from '@/components/ui/button'
-import { cn } from '@/lib/utils'
-import { useLanguage } from '@/lib/i18n'
-
-interface ChatSidebarProps {
- conversations: any[]
- currentId?: string | null
- onSelect: (id: string) => void
- onNew: () => void
- onDelete?: (id: string) => void
-}
-
-export function ChatSidebar({
- conversations,
- currentId,
- onSelect,
- onNew,
- onDelete,
-}: ChatSidebarProps) {
- const { t, language } = useLanguage()
- const dateLocale = language === 'fr' ? fr : enUS
- const [pendingDelete, setPendingDelete] = useState(null)
-
- const confirmDelete = (id: string) => {
- setPendingDelete(id)
- }
-
- const cancelDelete = (e: React.MouseEvent) => {
- e.stopPropagation()
- setPendingDelete(null)
- }
-
- const executeDelete = async (e: React.MouseEvent, id: string) => {
- e.stopPropagation()
- setPendingDelete(null)
- if (onDelete) {
- await onDelete(id)
- }
- }
-
- return (
-
-
-
-
-
-
- {conversations.length === 0 ? (
-
- {t('chat.noHistory')}
-
- ) : (
- conversations.map((chat) => (
-
onSelect(chat.id)}
- className={cn(
- "relative cursor-pointer rounded-lg transition-all group",
- currentId === chat.id
- ? "bg-primary/10 text-primary dark:bg-primary/20"
- : "hover:bg-muted/50 text-muted-foreground"
- )}
- >
-
-
-
-
- {chat.title || t('chat.untitled')}
-
-
-
- {formatDistanceToNow(new Date(chat.updatedAt), { addSuffix: true, locale: dateLocale })}
-
-
-
- {/* Delete button — visible on hover or when confirming */}
- {pendingDelete !== chat.id && (
-
- )}
-
- {/* Inline confirmation banner */}
- {pendingDelete === chat.id && (
-
e.stopPropagation()}
- >
- {t('chat.deleteConfirm')}
-
-
-
- )}
-
- ))
- )}
-
-
- )
-}
diff --git a/memento-note/components/note-editor/note-editor-toolbar.tsx b/memento-note/components/note-editor/note-editor-toolbar.tsx
index 0347bc6..26cb911 100644
--- a/memento-note/components/note-editor/note-editor-toolbar.tsx
+++ b/memento-note/components/note-editor/note-editor-toolbar.tsx
@@ -165,11 +165,11 @@ export function NoteEditorToolbar({ mode, onClose, onToggleAttachments, attachme
// Remove all action buttons, toolbars, and UI elements
clone.querySelectorAll('button, .drag-handle, [contenteditable="false"], .opacity-0, .group-hover\\:opacity-100').forEach(el => el.remove())
+ // Remove remaining UI controls (block action icons with tooltips) regardless of
+ // UI language — keep user content that may carry a title (links, abbreviations).
clone.querySelectorAll('[title]').forEach(el => {
- const title = el.getAttribute('title')
- if (title && (title.includes('Supprimer') || title.includes('Delete') || title.includes('Désactiver') || title.includes('Disable') || title.includes('Modifier') || title.includes('Edit'))) {
- el.remove()
- }
+ const tag = el.tagName.toLowerCase()
+ if (tag !== 'a' && tag !== 'abbr') el.remove()
})
// Render KaTeX equations properly
diff --git a/memento-note/components/note-graph-view.tsx b/memento-note/components/note-graph-view.tsx
deleted file mode 100644
index 9ef1262..0000000
--- a/memento-note/components/note-graph-view.tsx
+++ /dev/null
@@ -1,789 +0,0 @@
-'use client'
-
-import { useEffect, useState, useMemo, useCallback, useRef } from 'react'
-import dynamic from 'next/dynamic'
-import { useRouter } from 'next/navigation'
-import { useNotebooks } from '@/context/notebooks-context'
-import { openNotePath } from '@/lib/navigation/open-note'
-import {
- Loader2,
- Network,
- Filter,
- X,
- ExternalLink,
- Maximize2,
- Calendar,
- Clock,
- Link2,
- FileText,
- Check,
- Tag,
- Sparkles,
- ChevronRight,
- BookOpen
-} from 'lucide-react'
-import DOMPurify from 'isomorphic-dompurify'
-import { markdownToHtml } from '@/lib/markdown-to-html'
-import { useLanguage } from '@/lib/i18n'
-import { LabelBadge } from './label-badge'
-import { NoteChecklist } from './note-checklist'
-
-const ForceGraph2D = dynamic(() => import('react-force-graph-2d'), { ssr: false }) as any
-const MarkdownContent = dynamic(() => import('./markdown-content').then(m => ({ default: m.MarkdownContent })), {
- ssr: false,
- loading: () =>
-})
-
-interface GraphNode { id: string; title: string; notebookId: string | null; createdAt: string; degree: number }
-interface GraphEdge { source: string; target: string; weight: number; type: string }
-interface Cluster { id: string; name: string }
-interface RawData { nodes: GraphNode[]; edges: GraphEdge[]; clusters: Cluster[] }
-interface NotePreview {
- id: string
- title: string | null
- content: string
- createdAt: string | Date
- updatedAt?: string | Date
- labels?: string[] | null
- type?: 'text' | 'markdown' | 'richtext' | 'checklist'
- checkItems?: { id: string; text: string; checked: boolean }[] | null
- isMarkdown?: boolean
-}
-
-const PALETTE = ['#6366f1', '#10b981', '#f59e0b', '#ec4899', '#14b8a6', '#8b5cf6', '#ef4444', '#3b82f6', '#84cc16', '#A47148']
-
-type EdgeTypeKey = 'explicit_link' | 'semantic_echo' | 'title_mention' | 'shared_label' | 'jaccard'
-
-const DEFAULT_EDGE_FILTERS: Record = {
- explicit_link: true,
- semantic_echo: true,
- title_mention: true,
- shared_label: true,
- jaccard: false,
-}
-
-export function NoteGraphView({ embedded = false }: { embedded?: boolean }) {
- const router = useRouter()
- const { notebooks } = useNotebooks()
- const containerRef = useRef(null)
- const graphRef = useRef(null)
- const existingNodesRef = useRef