diff --git a/.cursor/hooks/state/continual-learning.json b/.cursor/hooks/state/continual-learning.json index 14662af..182d65e 100644 --- a/.cursor/hooks/state/continual-learning.json +++ b/.cursor/hooks/state/continual-learning.json @@ -1,8 +1,8 @@ { "version": 1, "lastRunAtMs": 1778916916450, - "turnsSinceLastRun": 6, + "turnsSinceLastRun": 9, "lastTranscriptMtimeMs": 1778916916347.422, - "lastProcessedGenerationId": "6a9dfdc8-438b-497e-831b-ad0079ebea2a", + "lastProcessedGenerationId": "309e0c67-c6f3-45de-b400-cc4455d26b28", "trialStartedAtMs": null } diff --git a/README.fr.md b/README.fr.md index aa08c13..173e690 100644 --- a/README.fr.md +++ b/README.fr.md @@ -2,7 +2,7 @@ Une application de prise de notes intelligente et powered by IA. Comme Google Keep, mais avec des notebooks, la recherche semantique, des agents IA et un serveur MCP integre. -**[Read in English](README.md)** | **[Guide complet](GUIDE.md)** +**[Read in English](README.md)** | **[Guide complet](GUIDE.md)** | **[Guide utilisateur (captures)](docs/guide-utilisateur/README.md)** --- diff --git a/docs/guide-utilisateur/README.md b/docs/guide-utilisateur/README.md new file mode 100644 index 0000000..ea8522a --- /dev/null +++ b/docs/guide-utilisateur/README.md @@ -0,0 +1,307 @@ +# Guide utilisateur Momento + +Documentation produit illustrée du SaaS **Momento** — second cerveau augmenté par l’IA (notes, recherche sémantique, agents, brainstorm collaboratif, BYOK). + +> **Sources internes** : ce guide synthétise le [PRD](../prd.md), les [fonctionnalités IA](../fonctionnalites-ia.md), la [doc brainstorm](../../memento-note/docs/brainstorm-documentation.md) et le [GUIDE technique](../../GUIDE.md) (installation / admin). + +--- + +## Sommaire + +1. [Vue d’ensemble](#1-vue-densemble) +2. [Premiers pas](#2-premiers-pas) +3. [Page d’accueil (marketing)](#3-page-daccueil-marketing) +4. [Espace de travail](#4-espace-de-travail) +5. [Intelligence artificielle](#5-intelligence-artificielle) +6. [Agents autonomes](#6-agents-autonomes) +7. [Brainstorm radial](#7-brainstorm-radial) +8. [Paramètres et facturation](#8-paramètres-et-facturation) +9. [Administration](#9-administration) +10. [Captures d’écran](#10-captures-décran) +11. [Aller plus loin](#11-aller-plus-loin) + +--- + +## 1. Vue d’ensemble + +Momento est une application de prise de notes qui combine : + +- **Organisation** : carnets, labels, grille masonry, archive, corbeille, partage. +- **Recherche sémantique** : trouver par idée, pas seulement par mot-clé (vecteurs + plein texte). +- **IA intégrée** : chat RAG sur vos notes, reformulation, tags et titres suggérés, Memory Echo. +- **Agents** : Scraper, Researcher, Monitor, générateur de slides/diagrammes, agents personnalisés. +- **Brainstorm** : canvas radial D3 en temps réel (vagues Variations / Analogies / Disruptions). +- **BYOK** : connecter vos propres clés API (OpenAI, Anthropic, Google, etc.) pour maîtriser les coûts. +- **Modèle commercial** : pack découverte IA, abonnements Pro / Business / Enterprise, facturation « host-pays » en session partagée. + +**Publics visés** : créateurs, consultants, équipes R&D, organisations qui veulent une mémoire de travail partagée et sécurisée. + +--- + +## 2. Premiers pas + +### Créer un compte + +1. Ouvrir l’URL de votre instance (ex. `https://votre-domaine.com`). +2. Cliquer sur **Get started** ou **Sign up**. +3. Renseigner email, nom et mot de passe. + +![Inscription](screenshots/08-register.png) + +### Se connecter + +![Connexion](screenshots/07-login.png) + +En cas d’oubli : **Forgot password?** envoie un lien de réinitialisation par email (SMTP configuré côté serveur). + +![Mot de passe oublié](screenshots/09-forgot-password.png) + +### Après connexion + +Vous arrivez sur **l’accueil notes** (`/home`) : sidebar (carnets, navigation), zone centrale (grille ou liste de notes), actions de création. + +> Les captures de l’application connectée (`10-app-*.png`, etc.) nécessitent une session active. Voir [Régénérer les captures](#régénérer-les-captures). + +--- + +## 3. Page d’accueil (marketing) + +La landing publique présente la proposition de valeur avant inscription. + +### Hero — second cerveau amplifié + +![Landing — hero](screenshots/01-landing-hero.png) + +Message clé : Momento relie, analyse et développe vos idées avec **6 types d’agents IA** et une **recherche sémantique** avancée. Exemple produit : *Memory Echo* qui signale un lien avec un projet passé. + +### Capacités IA + +![Landing — fonctionnalités IA](screenshots/02-landing-features.png) + +| Bloc | Ce que l’utilisateur y gagne | +|------|------------------------------| +| **Semantic Search** | Retrouver une note par le sens, pas seulement les mots exacts. | +| **Contextual RAG Chat** | Dialoguer avec tout votre corpus (notes + web + documents). | +| **Augmented Writing** | Reformulation, titres, tags et résumés en arrière-plan. | + +### Agents spécialisés + +![Landing — agents](screenshots/03-landing-agents.png) + +Six rôles mis en avant : **Scraper**, **Researcher**, **Slide Gen**, **Monitor**, **Diagram Gen**, et agents **Custom** (rôles et sources de données définis par vous). + +### Brainstorm — vagues de pensée + +![Landing — brainstorm](screenshots/04-landing-brainstorm.png) + +Brainstorming radial temps réel : génération par vagues, collaboration (curseurs, avatars), export sémantique vers des notes structurées. + +### Tarifs + +![Landing — tarifs](screenshots/05-landing-pricing.png) + +| Offre | Positionnement | +|-------|----------------| +| **Basic** | Découverte (notes limitées, crédits IA découverte). | +| **Pro** | Créateurs / consultants (BYOK OpenAI/Anthropic, agents, historique étendu). | +| **Business** | Équipes (collaborateurs, 13 fournisseurs BYOK, API). | +| **Enterprise** | SSO/SAML, agents illimités, audit, support dédié. | + +### BYOK — votre propre fournisseur IA + +![Landing — BYOK](screenshots/06-landing-byok.png) + +Si vous avez déjà des clés **OpenAI**, **Anthropic** ou **Google**, vous les connectez à Momento : pas de plafond de crédits imposé par la plateforme, facturation directe chez le fournisseur, changement de provider en un clic. + +--- + +## 4. Espace de travail + +### Notes et carnets + +- **Types de notes** : texte riche, checklist, Markdown, texte brut. +- **Carnets** : regroupement thématique ; **labels** contextuels (y compris suggérés par l’IA). +- **Vues** : grille masonry (drag-and-drop) ou onglets. +- **Cycle de vie** : archive, corbeille, historique des versions, partage avec permissions. + +### Recherche + +Barre de recherche globale : mode **plein texte** et mode **sémantique** (embeddings). Idéal pour « retrouver cette idée sur l’architecture modulaire » sans se souvenir du titre exact. + +### Chat IA (`/chat`) + +Conversations persistées, alimentées par vos notes (RAG), avec outils : recherche de notes, lecture, recherche web, scrape. Le système peut cibler un carnet ou la note ouverte (mode Copilot). + +### Lab (`/lab`) + +Tableau blanc **Excalidraw** intégré pour schémas, mindmaps et croquis libres. + +--- + +## 5. Intelligence artificielle + +Détail technique : [fonctionnalites-ia.md](../fonctionnalites-ia.md). + +### Trois fournisseurs configurables + +| Tier | Usage | +|------|--------| +| **Tags** | Tags, reformulation, suggestions de titre | +| **Embeddings** | Recherche sémantique, Memory Echo | +| **Chat** | Chat RAG, agents, brainstorm, vision | + +Chacun peut pointer vers un modèle différent (OpenAI, Ollama local, Anthropic, DeepSeek, OpenRouter, etc.). + +### Dans l’éditeur de note + +- **Suggestions de titre** (3 styles). +- **Tags contextuels** parmi les labels du carnet ou nouveaux. +- **Reformulation** : clarifier, raccourcir, style, grammaire, traduire. +- **Description d’images** (vision) pour notes illustrées. + +### Memory Echo + +Détection proactive de liens entre notes (similarité vectorielle) avec explication en une phrase ; feedback pouce haut/bas pour affiner la sensibilité. + +### Quotas et pack découverte + +Indicateur de consommation IA (sidebar). À l’épuisement : proposition d’**upgrade** ou d’ajout d’une **clé BYOK** — pas de blocage brutal sans issue. + +--- + +## 6. Agents autonomes + +Page **Agents** (`/agents`) : créer, planifier et suivre des agents. + +| Type | Rôle | +|------|------| +| **Scraper** | URLs + flux RSS → synthèse et note avec images | +| **Researcher** | Requêtes web → note de recherche structurée | +| **Monitor** | Surveillance d’un carnet → tendances et insights | +| **Custom** | Rôle libre + sources optionnelles | +| **Slide Generator** | PowerPoint ou slides HTML Reveal.js | +| **Diagram Gen** | Diagrammes Excalidraw (mindmap, flowchart, etc.) | + +Planification : manuel, horaire, quotidien, hebdomadaire, mensuel (fuseau IANA). + +Résultats : notification in-app et par email selon configuration. + +--- + +## 7. Brainstorm radial + +Documentation technique : [brainstorm-documentation.md](../../memento-note/docs/brainstorm-documentation.md). + +### Parcours type + +1. Saisir une **idée graine** (ou partir d’une note existante). +2. L’IA génère **3 vagues** : Variations → Analogies → Disruptions (9 idées). +3. Sur le **canvas D3** : zoom, drag, approfondir, rejeter, convertir en note. +4. **Partager** la session (lien invité ; l’hôte paie les tokens en mode host-pays). +5. **Exporter** en note structurée ou formats de présentation. + +### Collaboration + +Socket.io : curseurs en direct, déplacement de nœuds, présence des participants (host / editor / viewer). + +--- + +## 8. Paramètres et facturation + +Accessible via l’icône paramètres (profil utilisateur). + +| Section | Contenu | +|---------|---------| +| **Profil** | Identité, email, mot de passe | +| **Apparence** | Thème clair/sombre, langue (15 locales) | +| **IA** | Préférences utilisateur ; clés **BYOK** personnelles (chiffrées AES-256-GCM) | +| **Facturation** | Abonnement Stripe, usage, portail client | +| **Données** | Export / import JSON | +| **MCP** | Clés API pour le serveur MCP (Claude Desktop, N8N, etc.) | + +### BYOK côté utilisateur + +Coller la clé du fournisseur choisi → validation en direct → badge « mode BYOK » ; le routeur LLM bascule automatiquement sans choix manuel à chaque requête. + +### Facturation host-pays + +Dans une session brainstorm partagée, c’est l’**hôte** qui consomme le quota / la clé BYOK pour les invités — flux fluide pour les équipes. + +--- + +## 9. Administration + +Réservé aux comptes **ADMIN** (premier email `ADMIN_EMAIL` à l’inscription ou promotion manuelle). + +| Zone | Rôle | +|------|------| +| **Utilisateurs** | Gestion des comptes et rôles | +| **IA** | Fournisseurs système (tags, embeddings, chat), **fournisseur de secours** optionnel (erreurs 429/5xx) | +| **Tests IA** | Valider tags et embeddings | +| **Sécurité** | Inscription publique on/off | +| **SMTP** | Emails transactionnels | + +Guide déploiement et variables : [GUIDE.md](../../GUIDE.md). + +--- + +## 10. Captures d’écran + +| Fichier | Description | +|---------|-------------| +| `01-landing-hero.png` | Landing — accroche | +| `02-landing-features.png` | Capacités IA | +| `03-landing-agents.png` | Agents | +| `04-landing-brainstorm.png` | Brainstorm | +| `05-landing-pricing.png` | Tarifs | +| `06-landing-byok.png` | BYOK | +| `07-login.png` | Connexion | +| `08-register.png` | Inscription | +| `09-forgot-password.png` | Mot de passe oublié | +| `10-app-home.png` | Accueil (connecté) | +| `11-app-chat.png` | Chat IA | +| `12-app-agents.png` | Agents | +| `13-app-brainstorm.png` | Brainstorm app | +| `14-app-lab.png` | Lab Excalidraw | +| `15-settings-ai.png` | Paramètres IA / BYOK | +| `16-settings-billing.png` | Facturation | +| `17-settings-profile.png` | Profil *(si capture auth)* | +| `18-admin-dashboard.png` | Admin *(si capture auth)* | + +### Régénérer les captures + +Depuis le dossier **`memento-note/`** (Playwright y est installé) : + +```bash +cd memento-note +npx playwright install chromium # une seule fois + +# Pages publiques uniquement +node ../docs/guide-utilisateur/capture-screenshots.mjs + +# + application connectée (recommandé pour 10–18) +MOMENTO_DOC_EMAIL=votre@email.com \ +MOMENTO_DOC_PASSWORD='votre-mot-de-passe' \ +node ../docs/guide-utilisateur/capture-screenshots.mjs +``` + +- **URL** : par défaut lue depuis `memento-note/.env` → `NEXTAUTH_URL` (ex. `http://192.168.1.83:3000`). Surcharge : `MOMENTO_DOC_BASE_URL`. +- En cas d’échec de connexion, les captures **01–09** sont quand même produites ; **10–18** sont ignorées. + +--- + +## 11. Aller plus loin + +| Document | Public | Sujet | +|----------|--------|--------| +| [GUIDE.md](../../GUIDE.md) | Admin / DevOps | Installation, Docker, env, MCP | +| [fonctionnalites-ia.md](../fonctionnalites-ia.md) | Produit / Dev | Détail des capacités IA | +| [prd.md](../prd.md) | Produit | Vision, parcours, exigences | +| [ux-design-specification.md](../ux-design-specification.md) | Design | Quotas, BYOK, RGPD | +| [epics.md](../epics.md) | Engineering | Stories commerciales V3 | +| [brainstorm-documentation.md](../../memento-note/docs/brainstorm-documentation.md) | Dev | Architecture brainstorm | +| [gtm-pricing-strategy.md](../gtm-pricing-strategy.md) | Business | Tarification GTM | + +--- + +*Dernière mise à jour des captures : génération automatique sur instance locale. Pour une doc production, régénérer les PNG après connexion avec un compte de démonstration.* diff --git a/docs/guide-utilisateur/capture-screenshots.mjs b/docs/guide-utilisateur/capture-screenshots.mjs new file mode 100644 index 0000000..0c07bdc --- /dev/null +++ b/docs/guide-utilisateur/capture-screenshots.mjs @@ -0,0 +1,121 @@ +#!/usr/bin/env node +/** + * Capture les captures d'écran pour docs/guide-utilisateur/screenshots/ + * + * Pages publiques : sans identifiants. + * App connectée : définir MOMENTO_DOC_EMAIL et MOMENTO_DOC_PASSWORD + * + * Usage (depuis memento-note/ — Playwright est installé là) : + * node ../docs/guide-utilisateur/capture-screenshots.mjs + * MOMENTO_DOC_EMAIL=you@example.com MOMENTO_DOC_PASSWORD=secret node ../docs/guide-utilisateur/capture-screenshots.mjs + */ +import { createRequire } from 'module' +import path from 'path' +import { fileURLToPath } from 'url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const appRoot = path.resolve(__dirname, '../../memento-note') +const require = createRequire(path.join(appRoot, 'package.json')) +const { chromium } = require('playwright') +const OUT = path.join(__dirname, 'screenshots') + +function resolveBaseUrl() { + if (process.env.MOMENTO_DOC_BASE_URL) return process.env.MOMENTO_DOC_BASE_URL.replace(/\/$/, '') + try { + const envPath = path.join(appRoot, '.env') + const env = require('fs').readFileSync(envPath, 'utf8') + const m = env.match(/^NEXTAUTH_URL=["']?([^"'\n]+)["']?/m) + if (m) return m[1].replace(/\/$/, '') + } catch { + /* ignore */ + } + return 'http://localhost:3000' +} + +const BASE = resolveBaseUrl() + +async function shot(page, url, name, opts = {}) { + await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 }) + await page.waitForTimeout(opts.wait ?? 1000) + if (opts.scrollId) { + const el = page.locator(`#${opts.scrollId}`).first() + if (await el.count()) await el.scrollIntoViewIfNeeded() + } + await page.waitForTimeout(400) + await page.screenshot({ path: path.join(OUT, name), fullPage: !!opts.fullPage }) + console.log('✓', name) +} + +async function loginIfConfigured(page) { + const email = process.env.MOMENTO_DOC_EMAIL + const password = process.env.MOMENTO_DOC_PASSWORD + if (!email || !password) { + console.log('ℹ Connexion ignorée (MOMENTO_DOC_EMAIL / MOMENTO_DOC_PASSWORD non définis)') + return false + } + console.log(`ℹ Connexion sur ${BASE}/login`) + await page.goto(`${BASE}/login`, { waitUntil: 'networkidle' }) + await page.locator('#email').fill(email) + await page.locator('#password').fill(password) + await page.locator('form button[type="submit"], form button').first().click() + try { + await page.waitForURL((u) => !u.pathname.includes('/login'), { timeout: 20000 }) + } catch { + const err = await page.locator('.text-red-500, [class*="error"]').first().textContent().catch(() => '') + console.warn( + `⚠ Connexion échouée (${err || 'timeout'}). Vérifiez MOMENTO_DOC_EMAIL, MOMENTO_DOC_PASSWORD et NEXTAUTH_URL=${BASE}`, + ) + return false + } + console.log('✓ Session connectée →', page.url()) + return true +} + +console.log(`ℹ Base URL: ${BASE}`) +const browser = await chromium.launch({ headless: true }) +const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }) + +await page.goto(`${BASE}/`, { waitUntil: 'networkidle' }) +for (const [id, name] of [ + [null, '01-landing-hero.png'], + ['features', '02-landing-features.png'], + ['agents', '03-landing-agents.png'], + ['brainstorm', '04-landing-brainstorm.png'], + ['pricing', '05-landing-pricing.png'], + ['tech', '06-landing-byok.png'], +]) { + if (id) await page.goto(`${BASE}/#${id}`, { waitUntil: 'networkidle' }) + else await page.evaluate(() => window.scrollTo(0, 0)) + await page.waitForTimeout(600) + if (id) { + const el = page.locator(`#${id}`).first() + if (await el.count()) await el.scrollIntoViewIfNeeded() + } + await page.waitForTimeout(400) + await page.screenshot({ path: path.join(OUT, name) }) + console.log('✓', name) +} + +await shot(page, `${BASE}/login`, '07-login.png') +await shot(page, `${BASE}/register`, '08-register.png') +await shot(page, `${BASE}/forgot-password`, '09-forgot-password.png') + +const loggedIn = await loginIfConfigured(page) +if (loggedIn) { + for (const [route, name] of [ + ['/home', '10-app-home.png'], + ['/chat', '11-app-chat.png'], + ['/agents', '12-app-agents.png'], + ['/brainstorm', '13-app-brainstorm.png'], + ['/lab', '14-app-lab.png'], + ['/settings/ai', '15-settings-ai.png'], + ['/settings/billing', '16-settings-billing.png'], + ['/settings/profile', '17-settings-profile.png'], + ['/admin', '18-admin-dashboard.png'], + ]) { + await shot(page, `${BASE}${route}`, name, { wait: 1500 }) + } +} + +await browser.close() +console.log('\nCaptures enregistrées dans', OUT) diff --git a/docs/guide-utilisateur/screenshots/01-landing-hero.png b/docs/guide-utilisateur/screenshots/01-landing-hero.png new file mode 100644 index 0000000..278715c Binary files /dev/null and b/docs/guide-utilisateur/screenshots/01-landing-hero.png differ diff --git a/docs/guide-utilisateur/screenshots/02-landing-features.png b/docs/guide-utilisateur/screenshots/02-landing-features.png new file mode 100644 index 0000000..b00d27d Binary files /dev/null and b/docs/guide-utilisateur/screenshots/02-landing-features.png differ diff --git a/docs/guide-utilisateur/screenshots/03-landing-agents.png b/docs/guide-utilisateur/screenshots/03-landing-agents.png new file mode 100644 index 0000000..104ccfd Binary files /dev/null and b/docs/guide-utilisateur/screenshots/03-landing-agents.png differ diff --git a/docs/guide-utilisateur/screenshots/04-landing-brainstorm.png b/docs/guide-utilisateur/screenshots/04-landing-brainstorm.png new file mode 100644 index 0000000..521c1ad Binary files /dev/null and b/docs/guide-utilisateur/screenshots/04-landing-brainstorm.png differ diff --git a/docs/guide-utilisateur/screenshots/05-landing-pricing.png b/docs/guide-utilisateur/screenshots/05-landing-pricing.png new file mode 100644 index 0000000..e95c419 Binary files /dev/null and b/docs/guide-utilisateur/screenshots/05-landing-pricing.png differ diff --git a/docs/guide-utilisateur/screenshots/06-landing-byok.png b/docs/guide-utilisateur/screenshots/06-landing-byok.png new file mode 100644 index 0000000..7f5612f Binary files /dev/null and b/docs/guide-utilisateur/screenshots/06-landing-byok.png differ diff --git a/docs/guide-utilisateur/screenshots/07-login.png b/docs/guide-utilisateur/screenshots/07-login.png new file mode 100644 index 0000000..c6b5304 Binary files /dev/null and b/docs/guide-utilisateur/screenshots/07-login.png differ diff --git a/docs/guide-utilisateur/screenshots/08-register.png b/docs/guide-utilisateur/screenshots/08-register.png new file mode 100644 index 0000000..5d2ff7e Binary files /dev/null and b/docs/guide-utilisateur/screenshots/08-register.png differ diff --git a/docs/guide-utilisateur/screenshots/09-forgot-password.png b/docs/guide-utilisateur/screenshots/09-forgot-password.png new file mode 100644 index 0000000..7cb3654 Binary files /dev/null and b/docs/guide-utilisateur/screenshots/09-forgot-password.png differ diff --git a/docs/guide-utilisateur/screenshots/10-app-home.png b/docs/guide-utilisateur/screenshots/10-app-home.png new file mode 100644 index 0000000..e98e007 Binary files /dev/null and b/docs/guide-utilisateur/screenshots/10-app-home.png differ diff --git a/docs/guide-utilisateur/screenshots/11-app-chat.png b/docs/guide-utilisateur/screenshots/11-app-chat.png new file mode 100644 index 0000000..18eb92a Binary files /dev/null and b/docs/guide-utilisateur/screenshots/11-app-chat.png differ diff --git a/docs/guide-utilisateur/screenshots/12-app-agents.png b/docs/guide-utilisateur/screenshots/12-app-agents.png new file mode 100644 index 0000000..0e54bd3 Binary files /dev/null and b/docs/guide-utilisateur/screenshots/12-app-agents.png differ diff --git a/docs/guide-utilisateur/screenshots/13-app-brainstorm.png b/docs/guide-utilisateur/screenshots/13-app-brainstorm.png new file mode 100644 index 0000000..079c72c Binary files /dev/null and b/docs/guide-utilisateur/screenshots/13-app-brainstorm.png differ diff --git a/docs/guide-utilisateur/screenshots/14-app-lab.png b/docs/guide-utilisateur/screenshots/14-app-lab.png new file mode 100644 index 0000000..4f71916 Binary files /dev/null and b/docs/guide-utilisateur/screenshots/14-app-lab.png differ diff --git a/docs/guide-utilisateur/screenshots/15-settings-ai.png b/docs/guide-utilisateur/screenshots/15-settings-ai.png new file mode 100644 index 0000000..e10c7a3 Binary files /dev/null and b/docs/guide-utilisateur/screenshots/15-settings-ai.png differ diff --git a/docs/guide-utilisateur/screenshots/16-settings-billing.png b/docs/guide-utilisateur/screenshots/16-settings-billing.png new file mode 100644 index 0000000..af91cab Binary files /dev/null and b/docs/guide-utilisateur/screenshots/16-settings-billing.png differ diff --git a/docs/guide-utilisateur/screenshots/17-settings-profile.png b/docs/guide-utilisateur/screenshots/17-settings-profile.png new file mode 100644 index 0000000..1c3228b Binary files /dev/null and b/docs/guide-utilisateur/screenshots/17-settings-profile.png differ diff --git a/docs/guide-utilisateur/screenshots/18-admin-dashboard.png b/docs/guide-utilisateur/screenshots/18-admin-dashboard.png new file mode 100644 index 0000000..da49f37 Binary files /dev/null and b/docs/guide-utilisateur/screenshots/18-admin-dashboard.png differ diff --git a/memento-note/app/(admin)/admin/user-list.tsx b/memento-note/app/(admin)/admin/user-list.tsx index da08669..4d31130 100644 --- a/memento-note/app/(admin)/admin/user-list.tsx +++ b/memento-note/app/(admin)/admin/user-list.tsx @@ -1,21 +1,103 @@ 'use client' -import { useState } from 'react' +import { useState, useRef, useEffect, useCallback } from 'react' +import { createPortal } from 'react-dom' import { Button } from '@/components/ui/button' -import { deleteUser, updateUserRole } from '@/app/actions/admin' +import { deleteUser, updateUserRole, updateUserSubscription } from '@/app/actions/admin' import { toast } from 'sonner' -import { Trash2, Shield, ShieldOff } from 'lucide-react' +import { Trash2, Shield, ShieldOff, Crown, ChevronDown } from 'lucide-react' import { format } from 'date-fns' import { useLanguage } from '@/lib/i18n' +const TIERS = ['BASIC', 'PRO', 'BUSINESS', 'ENTERPRISE'] as const + +const tierColors: Record = { + BASIC: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300', + PRO: 'bg-brand-accent/10 text-brand-accent', + BUSINESS: 'bg-blue-50 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400', + ENTERPRISE: 'bg-purple-50 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400', +} + +function TierDropdown({ userId, tier, isOpen, onToggle, onClose, onChange }: { + userId: string + tier: string + isOpen: boolean + onToggle: () => void + onClose: () => void + onChange: (tier: string) => void +}) { + const btnRef = useRef(null) + const [pos, setPos] = useState({ top: 0, left: 0 }) + + const updatePos = useCallback(() => { + if (btnRef.current) { + const r = btnRef.current.getBoundingClientRect() + setPos({ top: r.bottom + 4, left: r.left }) + } + }, []) + + useEffect(() => { + if (isOpen) { + updatePos() + window.addEventListener('scroll', updatePos, true) + window.addEventListener('resize', updatePos) + } + return () => { + window.removeEventListener('scroll', updatePos, true) + window.removeEventListener('resize', updatePos) + } + }, [isOpen, updatePos]) + + return ( + + + {isOpen && typeof document !== 'undefined' && createPortal( + <> +
+
+ {TIERS.map((t) => ( + + ))} +
+ , + document.body + )} + + ) +} + export function UserList({ initialUsers }: { initialUsers: any[] }) { const { t } = useLanguage() + const [users, setUsers] = useState(initialUsers) + const [openDropdown, setOpenDropdown] = useState(null) const handleDelete = async (id: string) => { if (!confirm(t('admin.users.confirmDelete'))) return try { await deleteUser(id) toast.success(t('admin.users.deleteSuccess')) + setUsers(prev => prev.filter(u => u.id !== id)) } catch (e) { toast.error(t('admin.users.deleteFailed')) } @@ -26,11 +108,25 @@ export function UserList({ initialUsers }: { initialUsers: any[] }) { try { await updateUserRole(user.id, newRole) toast.success(t('admin.users.roleUpdateSuccess', { role: newRole })) + setUsers(prev => prev.map(u => u.id === user.id ? { ...u, role: newRole } : u)) } catch (e) { toast.error(t('admin.users.roleUpdateFailed')) } } + const handleTierChange = async (userId: string, tier: string) => { + setOpenDropdown(null) + try { + await updateUserSubscription(userId, tier) + toast.success(t('admin.users.tierUpdateSuccess', { tier })) + setUsers(prev => prev.map(u => u.id === userId ? { ...u, subscription: { tier, status: 'ACTIVE' } } : u)) + } catch (e) { + toast.error(t('admin.users.tierUpdateFailed')) + } + } + + const getUserTier = (user: any) => user.subscription?.tier || 'BASIC' + return (
@@ -39,43 +135,55 @@ export function UserList({ initialUsers }: { initialUsers: any[] }) { + - {initialUsers.map((user) => ( - - - - - - - - ))} + {users.map((user) => { + const tier = getUserTier(user) + return ( + + + + + setOpenDropdown(openDropdown === user.id ? null : user.id)} + onClose={() => setOpenDropdown(null)} + onChange={(t) => handleTierChange(user.id, t)} + /> + + + + ) + })}
{t('admin.users.table.name')} {t('admin.users.table.email')} {t('admin.users.table.role')}{t('admin.users.table.subscription') || 'Abonnement'} {t('admin.users.table.createdAt')} {t('admin.users.table.actions')}
{user.name || t('common.notAvailable')}{user.email} - - {user.role === 'ADMIN' ? t('admin.users.roles.admin') : t('admin.users.roles.user')} - - {format(new Date(user.createdAt), 'PP')} -
- - -
-
{user.name || t('common.notAvailable')}{user.email} + + {user.role === 'ADMIN' ? t('admin.users.roles.admin') : t('admin.users.roles.user')} + + {format(new Date(user.createdAt), 'PP')} +
+ + +
+
diff --git a/memento-note/app/(main)/agents/agents-page-client.tsx b/memento-note/app/(main)/agents/agents-page-client.tsx index c059bb0..131b299 100644 --- a/memento-note/app/(main)/agents/agents-page-client.tsx +++ b/memento-note/app/(main)/agents/agents-page-client.tsx @@ -2,7 +2,7 @@ import { useState, useCallback, useMemo, useEffect, useRef } from 'react' import { motion } from 'motion/react' -import { PageEntry } from '@/components/page-entry' + import { Plus, Bot, Search, LifeBuoy } from 'lucide-react' import { toast } from 'sonner' import { useLanguage } from '@/lib/i18n' @@ -205,7 +205,6 @@ export function AgentsPageClient({ const showDetail = selectedAgent !== null || isNewAgent return ( - <> {showDetail ? ( setShowHelp(false)} /> )} - ) } diff --git a/memento-note/app/(main)/page.tsx b/memento-note/app/(main)/home/page.tsx similarity index 100% rename from memento-note/app/(main)/page.tsx rename to memento-note/app/(main)/home/page.tsx diff --git a/memento-note/app/(main)/layout.tsx b/memento-note/app/(main)/layout.tsx index 8ed3780..149e5cc 100644 --- a/memento-note/app/(main)/layout.tsx +++ b/memento-note/app/(main)/layout.tsx @@ -7,7 +7,7 @@ import { detectUserLanguage, parseAcceptLanguage } from "@/lib/i18n/detect-user- import { loadTranslations } from "@/lib/i18n/load-translations"; import { getAISettings } from "@/app/actions/ai-settings"; import { AIChatLayoutBridge } from "@/components/ai-chat-layout-bridge"; -import { PageTransition } from "@/components/page-transition"; + export default async function MainLayout({ children, @@ -38,9 +38,7 @@ export default async function MainLayout({
- - {children} - + {children}
{showAIAssistant && } diff --git a/memento-note/app/(main)/settings/general/general-settings-client.tsx b/memento-note/app/(main)/settings/general/general-settings-client.tsx index ac00859..805d6cc 100644 --- a/memento-note/app/(main)/settings/general/general-settings-client.tsx +++ b/memento-note/app/(main)/settings/general/general-settings-client.tsx @@ -7,7 +7,7 @@ import { toast } from 'sonner' import { useRouter } from 'next/navigation' import { Globe, Bell } from 'lucide-react' import { motion } from 'motion/react' -import { PageEntry } from '@/components/page-entry' + interface GeneralSettingsClientProps { initialSettings: { diff --git a/memento-note/app/(main)/template.tsx b/memento-note/app/(main)/template.tsx new file mode 100644 index 0000000..315c136 --- /dev/null +++ b/memento-note/app/(main)/template.tsx @@ -0,0 +1,16 @@ +'use client' + +import { motion } from 'motion/react' + +export default function MainTemplate({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ) +} diff --git a/memento-note/app/(main)/trash/trash-client.tsx b/memento-note/app/(main)/trash/trash-client.tsx index b8993ee..4a0e1bf 100644 --- a/memento-note/app/(main)/trash/trash-client.tsx +++ b/memento-note/app/(main)/trash/trash-client.tsx @@ -3,7 +3,7 @@ import { useState, useMemo } from 'react' import { useRouter } from 'next/navigation' import { motion, AnimatePresence } from 'motion/react' -import { PageEntry } from '@/components/page-entry' + import { Trash2, RotateCcw, @@ -138,7 +138,6 @@ export function TrashClient({ } return ( -
@@ -289,6 +288,5 @@ export function TrashClient({

- ) } diff --git a/memento-note/app/(public)/layout.tsx b/memento-note/app/(public)/layout.tsx new file mode 100644 index 0000000..adf1f75 --- /dev/null +++ b/memento-note/app/(public)/layout.tsx @@ -0,0 +1,17 @@ +import { headers } from 'next/headers' +import { detectUserLanguage, parseAcceptLanguage } from '@/lib/i18n/detect-user-language' +import { loadTranslations } from '@/lib/i18n/load-translations' +import { PublicProviders } from '@/components/public-providers' + +export default async function PublicLayout({ children }: { children: React.ReactNode }) { + const headersList = await headers() + const browserLang = parseAcceptLanguage(headersList.get('accept-language')) + const initialLanguage = await detectUserLanguage(browserLang) + const initialTranslations = await loadTranslations(initialLanguage) + + return ( + + {children} + + ) +} diff --git a/memento-note/app/(public)/page.tsx b/memento-note/app/(public)/page.tsx new file mode 100644 index 0000000..eda6203 --- /dev/null +++ b/memento-note/app/(public)/page.tsx @@ -0,0 +1,5 @@ +import { LandingPage } from '@/components/landing-page' + +export default function PublicHomePage() { + return +} diff --git a/memento-note/app/actions/admin.ts b/memento-note/app/actions/admin.ts index ab0df91..7ad6268 100644 --- a/memento-note/app/actions/admin.ts +++ b/memento-note/app/actions/admin.ts @@ -5,6 +5,7 @@ import prisma from '@/lib/prisma' import { auth } from '@/auth' import bcrypt from 'bcryptjs' import { z } from 'zod' +import { SubscriptionTier, SubscriptionStatus } from '@prisma/client' // Schema pour la création d'utilisateur const CreateUserSchema = z.object({ @@ -33,6 +34,13 @@ export async function getUsers() { email: true, role: true, createdAt: true, + subscription: { + select: { + tier: true, + status: true, + currentPeriodEnd: true, + } + } } }) return users @@ -118,3 +126,39 @@ export async function updateUserRole(userId: string, newRole: string) { throw new Error('Failed to update role') } } + +export async function updateUserSubscription(userId: string, tier: string) { + await checkAdmin() + + const validTiers: string[] = ['BASIC', 'PRO', 'BUSINESS', 'ENTERPRISE'] + if (!validTiers.includes(tier)) { + throw new Error('Invalid tier') + } + + try { + const now = new Date() + const periodEnd = new Date(now) + periodEnd.setFullYear(periodEnd.getFullYear() + 1) + + await prisma.subscription.upsert({ + where: { userId }, + update: { + tier: tier as SubscriptionTier, + status: 'ACTIVE', + currentPeriodStart: now, + currentPeriodEnd: periodEnd, + }, + create: { + userId, + tier: tier as SubscriptionTier, + status: 'ACTIVE' as SubscriptionStatus, + currentPeriodStart: now, + currentPeriodEnd: periodEnd, + }, + }) + revalidatePath('/admin') + return { success: true } + } catch (error) { + throw new Error('Failed to update subscription') + } +} diff --git a/memento-note/app/actions/auth.ts b/memento-note/app/actions/auth.ts index 58f6700..d2c8540 100644 --- a/memento-note/app/actions/auth.ts +++ b/memento-note/app/actions/auth.ts @@ -11,7 +11,7 @@ export async function authenticate( await signIn('credentials', { email: formData.get('email'), password: formData.get('password'), - redirectTo: '/', + redirectTo: '/home', }); } catch (error) { if (error instanceof AuthError) { diff --git a/memento-note/app/actions/brainstorm.ts b/memento-note/app/actions/brainstorm.ts index 2636948..58ab3fb 100644 --- a/memento-note/app/actions/brainstorm.ts +++ b/memento-note/app/actions/brainstorm.ts @@ -128,7 +128,7 @@ export async function respondToBrainstormShare( }) } - revalidatePath('/') + revalidatePath('/home') return { success: true } } @@ -193,6 +193,6 @@ export async function removeBrainstormShare(sessionId: string) { data: { status: 'removed' }, }) - revalidatePath('/') + revalidatePath('/home') return { success: true } } diff --git a/memento-note/app/actions/note-illustration.ts b/memento-note/app/actions/note-illustration.ts index cef999f..5222df2 100644 --- a/memento-note/app/actions/note-illustration.ts +++ b/memento-note/app/actions/note-illustration.ts @@ -3,7 +3,7 @@ import DOMPurify from 'isomorphic-dompurify' import { auth } from '@/auth' import { prisma } from '@/lib/prisma' -import { getAIProvider } from '@/lib/ai/factory' +import { getChatProvider } from '@/lib/ai/factory' import { getSystemConfig } from '@/lib/config' import { getAISettings } from '@/app/actions/ai-settings' import { revalidatePath } from 'next/cache' @@ -58,18 +58,47 @@ export async function generateNoteIllustrationSvg(noteId: string): Promise<{ ok: } const config = await getSystemConfig() - const provider = getAIProvider(config) + const provider = getChatProvider(config) - const prompt = `Tu es un designer minimaliste. Produis UN SEUL document SVG valide pour une vignette de carte note. -Contraintes strictes: -- viewBox="0 0 224 168" (rapport 4:3), pas de width/height fixes en px sur la racine ou width="100%" height="100%" -- Style architectural / papier, 2–4 formes géométriques ou lignes, palette sobre (noir/gris/une couleur douce), pas de texte lisible -- AUCUN script, AUCUNE balise foreignObject, AUCUN lien externe, AUCUN attribut on* -- Réponds UNIQUEMENT avec le fragment SVG (commence par et finit par ), sans markdown ni commentaire. + const prompt = `Create a small SVG thumbnail that VISUALLY REPRESENTS this note's topic. -Thème à suggérer visuellement (abstrait, pas littéral): -Titre: ${plainTitle || '(sans titre)'} -Extrait: ${plainBody.slice(0, 400)}` +OUTPUT: Only raw SVG markup. No markdown, no code fences, no comments. Start with . + +SPECIFICATIONS: +- viewBox="0 0 224 168", NO fixed width/height attributes +- Maximum 1000 bytes +- Background: soft warm beige (#F5F0E8) or transparent +- Color palette (pick 2-3): warm charcoal (#2C2C2C), slate gray (#6B7280), soft sage (#A8B5A0), muted ochre (#C4A882), dusty rose (#C9A9A6), teal (#5F9EA0), burgundy (#8B4513) +- NO text, NO scripts, NO foreignObject, NO external links + +CRITICAL: The illustration MUST be recognizably related to the topic. +Think of it like an ICON or PICTOGRAM for the title. Not abstract random shapes. + +TOPIC: "${plainTitle || 'untitled'}" + +How to illustrate this topic (pick the BEST match): +- If the topic is about CODE/DEV: Show angle brackets <>, curly braces {}, a terminal window shape, or circuit-like lines +- If the topic is about MUSIC: Show sound waves, musical notes shapes, or speaker icon +- If the topic is about FOOD/COOKING: Show a pot shape, utensils, or plate +- If the topic is about TRAVEL: Show a path/road, mountain peaks, or compass +- If the topic is about SCIENCE: Show atom orbits, flask/beaker, or molecule bonds +- If the topic is about BUSINESS/FINANCE: Show ascending chart lines, coins, or briefcase +- If the topic is about HEALTH: Show heart shape, pulse line, or leaf +- If the topic is about EDUCATION: Show book shape, graduation cap, or pencil +- If the topic is about NATURE: Show tree, mountain, water wave, or sun +- If the topic is about DESIGN/ART: Show palette, brush stroke, or frame +- If the topic is about PEOPLE/TEAM: Show overlapping circles, handshake, or connected nodes +- If the topic is about ARCHITECTURE: Show building outline, blueprint grid, or columns +- Otherwise: Extract the KEY CONCEPT from the title and draw its SIMPLEST iconic representation + +TECHNICAL RULES: +- Use simple shapes: , , , , , , +- Keep it FLAT and MINIMAL — 2-4 elements max +- Use opacity for depth (0.3-0.8) +- The icon should be immediately recognizable even at small size + +Additional context from the note: +${plainBody.slice(0, 200)}` const raw = await provider.generateText(prompt) const extracted = extractSvgSnippet(raw) @@ -90,7 +119,7 @@ Extrait: ${plainBody.slice(0, 400)}` }, }) - revalidatePath('/') + revalidatePath('/home') return { ok: true } } catch (e) { console.error('[note-illustration]', e) diff --git a/memento-note/app/actions/notes.ts b/memento-note/app/actions/notes.ts index aac0108..4f22b9c 100644 --- a/memento-note/app/actions/notes.ts +++ b/memento-note/app/actions/notes.ts @@ -434,7 +434,7 @@ export async function restoreNoteVersion(noteId: string, historyEntryId: string) }, }) - revalidatePath('/') + revalidatePath('/home') return parseNote(restored) } @@ -603,7 +603,7 @@ export async function createNote(data: { if (!data.skipRevalidation) { // Revalidate main page (handles both inbox and notebook views via query params) - revalidatePath('/') + revalidatePath('/home') } // Fire-and-forget: run AI operations in background without blocking the response @@ -690,7 +690,7 @@ export async function createNote(data: { const merged = [...new Set([...existingNames, ...appliedLabels])] await syncNoteLabels(noteId, merged, notebookId ?? null, userId) if (!data.skipRevalidation) { - revalidatePath('/') + revalidatePath('/home') } } } @@ -853,7 +853,7 @@ export async function updateNote(id: string, data: { if (!options?.skipRevalidation) { try { revalidatePath(`/note/${id}`) } catch {} - try { revalidatePath('/') } catch {} + try { revalidatePath('/home') } catch {} } if (isStructuralChange) { @@ -895,7 +895,7 @@ export async function deleteNote(id: string, options?: { skipRevalidation?: bool }) if (!options?.skipRevalidation) { - revalidatePath('/') + revalidatePath('/home') } return { success: true } } catch (error) { @@ -915,7 +915,7 @@ export async function trashNote(id: string, options?: { skipRevalidation?: boole data: { trashedAt: new Date() } }) if (!options?.skipRevalidation) { - revalidatePath('/') + revalidatePath('/home') } return { success: true } } catch (error) { @@ -933,7 +933,7 @@ export async function restoreNote(id: string) { where: { id, userId: session.user.id }, data: { trashedAt: null } }) - revalidatePath('/') + revalidatePath('/home') revalidatePath('/trash') return { success: true } } catch (error) { @@ -984,7 +984,7 @@ export async function permanentDeleteNote(id: string) { await syncLabels(session.user.id, []) revalidatePath('/trash') - revalidatePath('/') + revalidatePath('/home') return { success: true } } catch (error) { console.error('Error permanently deleting note:', error) @@ -1028,7 +1028,7 @@ export async function emptyTrash() { await syncLabels(session.user.id, []) revalidatePath('/trash') - revalidatePath('/') + revalidatePath('/home') return { success: true } } catch (error) { console.error('Error emptying trash:', error) @@ -1182,7 +1182,7 @@ export async function reorderNotes(draggedId: string, targetId: string) { prisma.note.update({ where: { id: note.id }, data: { order: index } }) ) await prisma.$transaction(updates) - revalidatePath('/') + revalidatePath('/home') return { success: true } } catch (error) { throw new Error('Failed to reorder notes') @@ -1198,7 +1198,7 @@ export async function updateFullOrder(ids: string[]) { prisma.note.update({ where: { id, userId }, data: { order: index } }) ) await prisma.$transaction(updates) - revalidatePath('/') + revalidatePath('/home') return { success: true } } catch (error) { throw new Error('Failed to update order') @@ -1314,7 +1314,7 @@ export async function cleanupAllOrphans() { } } - revalidatePath('/') + revalidatePath('/home') revalidatePath('/settings') return { success: true, @@ -1487,7 +1487,7 @@ export async function dismissFromRecent(id: string) { data: { dismissedFromRecent: true } }) - // revalidatePath('/') // Removed to prevent immediate refill of the list + // revalidatePath('/home') // Removed to prevent immediate refill of the list return { success: true } } catch (error) { console.error('Error dismissing note from recent:', error) @@ -1895,7 +1895,7 @@ export async function respondToShareRequest(shareId: string, action: 'accept' | }); // Revalidate all relevant cache tags - revalidatePath('/'); + revalidatePath('/home'); return { success: true, share: updatedShare }; } catch (error: any) { @@ -1957,7 +1957,7 @@ export async function removeSharedNoteFromView(shareId: string) { } }); - revalidatePath('/'); + revalidatePath('/home'); return { success: true }; } catch (error: any) { console.error('Error removing shared note from view:', error); @@ -2018,7 +2018,7 @@ export async function leaveSharedNote(noteId: string) { } }); - revalidatePath('/'); + revalidatePath('/home'); return { success: true }; } catch (error: any) { console.error('Error leaving shared note:', error); diff --git a/memento-note/app/actions/organize-notebook.ts b/memento-note/app/actions/organize-notebook.ts index 574d1e4..47551e3 100644 --- a/memento-note/app/actions/organize-notebook.ts +++ b/memento-note/app/actions/organize-notebook.ts @@ -271,7 +271,7 @@ export async function executeNotebookOrganization(plan: OrganizationPlan): Promi } } - revalidatePath('/') + revalidatePath('/home') return { success: true, created, moved } } catch (err) { console.error('[organize-notebook] Execute error:', err) diff --git a/memento-note/app/actions/profile.ts b/memento-note/app/actions/profile.ts index 7b3a7ac..725f2e5 100644 --- a/memento-note/app/actions/profile.ts +++ b/memento-note/app/actions/profile.ts @@ -96,7 +96,7 @@ export async function updateTheme(theme: string) { where: { id: session.user.id }, data: { theme: normalized }, }) - revalidatePath('/') + revalidatePath('/home') revalidatePath('/settings/profile') return { success: true } } catch (error) { @@ -123,7 +123,7 @@ export async function updateLanguage(language: string) { // Note: The language will be applied on next page load // The client component should handle updating localStorage and reloading - revalidatePath('/') + revalidatePath('/home') revalidatePath('/settings/profile') return { success: true, language } } catch (error) { @@ -168,7 +168,7 @@ export async function updateFontSize(fontSize: string) { }) } - revalidatePath('/') + revalidatePath('/home') revalidatePath('/settings/profile') return { success: true, fontSize } } catch (error) { @@ -194,7 +194,7 @@ export async function updateShowRecentNotes(showRecentNotes: boolean) { }, }) - revalidatePath('/') + revalidatePath('/home') revalidatePath('/settings/profile') return { success: true, showRecentNotes } } catch (error) { diff --git a/memento-note/app/actions/title-suggestions.ts b/memento-note/app/actions/title-suggestions.ts index a55c58f..cb9f8ad 100644 --- a/memento-note/app/actions/title-suggestions.ts +++ b/memento-note/app/actions/title-suggestions.ts @@ -99,7 +99,7 @@ export async function applyTitleSuggestion( console.error('[HISTORY] Failed to create snapshot after title suggestion:', snapshotError) } - revalidatePath('/') + revalidatePath('/home') revalidatePath(`/note/${noteId}`) } catch (error) { console.error('Error applying title suggestion:', error) diff --git a/memento-note/app/api/ai/reformulate/route.ts b/memento-note/app/api/ai/reformulate/route.ts index eba19c0..9bbdecc 100644 --- a/memento-note/app/api/ai/reformulate/route.ts +++ b/memento-note/app/api/ai/reformulate/route.ts @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server' import { auth } from '@/auth' import { paragraphRefactorService } from '@/lib/ai/services/paragraph-refactor.service' import { getAISettings } from '@/app/actions/ai-settings' +import { checkEntitlementOrThrow, QuotaExceededError, incrementUsageAsync } from '@/lib/entitlements' export async function POST(request: NextRequest) { try { @@ -52,9 +53,27 @@ export async function POST(request: NextRequest) { }, { status: 400 }) } + // Check quota + try { + await checkEntitlementOrThrow(session.user.id, 'reformulate') + } catch (err) { + if (err instanceof QuotaExceededError) { + const isTierLocked = err.currentQuota === 0 + return NextResponse.json({ + error: isTierLocked ? 'feature_locked' : 'quota_exceeded', + errorKey: isTierLocked ? 'ai.featureLocked' : 'ai.quotaExceeded', + upgradeTier: err.upgradeTier, + quotaExceeded: true, + }, { status: 402 }) + } + throw err + } + // Use the ParagraphRefactorService const result = await paragraphRefactorService.refactor(text, mode, format === 'html' ? 'html' : 'markdown', language) + incrementUsageAsync(session.user.id, 'reformulate') + return NextResponse.json({ originalText: result.original, reformulatedText: result.refactored, diff --git a/memento-note/app/api/brainstorm/[sessionId]/expand/route.ts b/memento-note/app/api/brainstorm/[sessionId]/expand/route.ts index 4345fd8..06adc82 100644 --- a/memento-note/app/api/brainstorm/[sessionId]/expand/route.ts +++ b/memento-note/app/api/brainstorm/[sessionId]/expand/route.ts @@ -67,7 +67,7 @@ async function getParentContext( FROM "NoteEmbedding" e JOIN "Note" n ON n.id = e."noteId" WHERE n."userId" = $1 AND n."trashedAt" IS NULL AND n.id NOT IN (${excludeList}) - ORDER BY e.embedding <=> $2::vector + ORDER BY e.embedding::vector <=> $2::vector LIMIT 5`, hostUserId, vectorStr ) as any[] diff --git a/memento-note/app/api/brainstorm/[sessionId]/manual-idea/route.ts b/memento-note/app/api/brainstorm/[sessionId]/manual-idea/route.ts index 328b44a..97f81bf 100644 --- a/memento-note/app/api/brainstorm/[sessionId]/manual-idea/route.ts +++ b/memento-note/app/api/brainstorm/[sessionId]/manual-idea/route.ts @@ -131,7 +131,7 @@ export async function POST( FROM "NoteEmbedding" e JOIN "Note" n ON n.id = e."noteId" WHERE n.id IN (${idList}) AND n."trashedAt" IS NULL - ORDER BY e.embedding <=> $1::vector + ORDER BY e.embedding::vector <=> $1::vector LIMIT 3`, vectorStr ) as any[] @@ -142,7 +142,7 @@ export async function POST( FROM "NoteEmbedding" e JOIN "Note" n ON n.id = e."noteId" WHERE n."userId" = $1 AND n."trashedAt" IS NULL - ORDER BY e.embedding <=> $2::vector + ORDER BY e.embedding::vector <=> $2::vector LIMIT 3`, aiUserId, vectorStr ) as any[] diff --git a/memento-note/app/api/brainstorm/route.ts b/memento-note/app/api/brainstorm/route.ts index dd2ae03..9be488c 100644 --- a/memento-note/app/api/brainstorm/route.ts +++ b/memento-note/app/api/brainstorm/route.ts @@ -44,7 +44,7 @@ async function autoContextSearch( FROM "NoteEmbedding" e JOIN "Note" n ON n.id = e."noteId" WHERE n."userId" = $1 AND n."trashedAt" IS NULL - ORDER BY e.embedding <=> $2::vector + ORDER BY e.embedding::vector <=> $2::vector LIMIT 8`, userId, vectorStr ) as any[] diff --git a/memento-note/app/api/labels/[id]/route.ts b/memento-note/app/api/labels/[id]/route.ts index 0adc12c..b639438 100644 --- a/memento-note/app/api/labels/[id]/route.ts +++ b/memento-note/app/api/labels/[id]/route.ts @@ -146,7 +146,7 @@ export async function PUT( }) // Revalidate to refresh UI - revalidatePath('/') + revalidatePath('/home') return NextResponse.json({ success: true, @@ -253,7 +253,7 @@ export async function DELETE( }) // Revalidate to refresh UI - revalidatePath('/') + revalidatePath('/home') return NextResponse.json({ success: true, diff --git a/memento-note/app/api/manifest/route.ts b/memento-note/app/api/manifest/route.ts index bb69bc3..81f8001 100644 --- a/memento-note/app/api/manifest/route.ts +++ b/memento-note/app/api/manifest/route.ts @@ -6,7 +6,7 @@ export async function GET() { name: "Memento Notes", short_name: "Memento", description: "A smart, local-first note taking app with AI capabilities.", - start_url: "/", + start_url: "/home", display: "standalone", background_color: "#F2F0E9", theme_color: "#1C1C1C", diff --git a/memento-note/app/api/notebooks/[id]/route.ts b/memento-note/app/api/notebooks/[id]/route.ts index 35d4e71..1fc8f59 100644 --- a/memento-note/app/api/notebooks/[id]/route.ts +++ b/memento-note/app/api/notebooks/[id]/route.ts @@ -98,7 +98,7 @@ export async function PATCH( }) } - try { revalidatePath('/') } catch {} + try { revalidatePath('/home') } catch {} return NextResponse.json({ success: true }) } catch (error) { @@ -143,7 +143,7 @@ export async function DELETE( await prisma.notebook.delete({ where: { id } }) - try { revalidatePath('/') } catch {} + try { revalidatePath('/home') } catch {} return NextResponse.json({ success: true, diff --git a/memento-note/app/api/notebooks/reorder/route.ts b/memento-note/app/api/notebooks/reorder/route.ts index 5cbba0e..511b753 100644 --- a/memento-note/app/api/notebooks/reorder/route.ts +++ b/memento-note/app/api/notebooks/reorder/route.ts @@ -47,7 +47,7 @@ export async function POST(request: NextRequest) { await prisma.$transaction(updates) - revalidatePath('/') + revalidatePath('/home') return NextResponse.json({ success: true, diff --git a/memento-note/app/api/notebooks/route.ts b/memento-note/app/api/notebooks/route.ts index 791d967..42127ef 100644 --- a/memento-note/app/api/notebooks/route.ts +++ b/memento-note/app/api/notebooks/route.ts @@ -105,7 +105,7 @@ export async function POST(request: NextRequest) { } }) - try { revalidatePath('/') } catch {} + try { revalidatePath('/home') } catch {} return NextResponse.json({ success: true, diff --git a/memento-note/app/api/notes/[id]/move/route.ts b/memento-note/app/api/notes/[id]/move/route.ts index 8fe7078..5f0a113 100644 --- a/memento-note/app/api/notes/[id]/move/route.ts +++ b/memento-note/app/api/notes/[id]/move/route.ts @@ -90,7 +90,7 @@ export async function POST( console.error('[HISTORY] Failed to create snapshot after notebook move:', snapshotError) } - // No revalidatePath('/') here — the client-side triggerRefresh() in + // No revalidatePath('/home') here — the client-side triggerRefresh() in // notebooks-context.tsx handles the refresh. Avoiding server-side // revalidation prevents a double-refresh (server + client). diff --git a/memento-note/app/api/notes/delete-all/route.ts b/memento-note/app/api/notes/delete-all/route.ts index 76c4e63..9c7f087 100644 --- a/memento-note/app/api/notes/delete-all/route.ts +++ b/memento-note/app/api/notes/delete-all/route.ts @@ -50,7 +50,7 @@ export async function POST(req: NextRequest) { }) // Revalidate paths - revalidatePath('/') + revalidatePath('/home') revalidatePath('/settings/data') // Await cleanup in background (don't block response) diff --git a/memento-note/app/api/notes/import/route.ts b/memento-note/app/api/notes/import/route.ts index 19f6e42..0b97661 100644 --- a/memento-note/app/api/notes/import/route.ts +++ b/memento-note/app/api/notes/import/route.ts @@ -222,7 +222,7 @@ export async function POST(req: NextRequest) { } } - revalidatePath('/') + revalidatePath('/home') revalidatePath('/settings/data') return NextResponse.json({ success: true, ...stats }) diff --git a/memento-note/app/api/usage/current/route.ts b/memento-note/app/api/usage/current/route.ts index 0c7d1b4..61480f4 100644 --- a/memento-note/app/api/usage/current/route.ts +++ b/memento-note/app/api/usage/current/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from 'next/server'; import { auth } from '@/auth'; -import { getUserQuotas } from '@/lib/entitlements'; +import { getUserQuotas, getEffectiveTier } from '@/lib/entitlements'; export async function GET() { const session = await auth(); @@ -10,8 +10,11 @@ export async function GET() { } try { - const quotas = await getUserQuotas(session.user.id); - return NextResponse.json({ quotas }); + const [quotas, tier] = await Promise.all([ + getUserQuotas(session.user.id), + getEffectiveTier(session.user.id), + ]); + return NextResponse.json({ quotas, tier }); } catch (error) { console.error('[usage/current] Failed to fetch quotas:', error); return NextResponse.json( diff --git a/memento-note/app/api/users/search/route.ts b/memento-note/app/api/users/search/route.ts index 0d244c7..2098954 100644 --- a/memento-note/app/api/users/search/route.ts +++ b/memento-note/app/api/users/search/route.ts @@ -8,6 +8,10 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } + if ((session.user as any)?.role !== 'ADMIN') { + return NextResponse.json({ users: [] }) + } + const q = request.nextUrl.searchParams.get('q') || '' if (q.length < 2) { return NextResponse.json({ users: [] }) diff --git a/memento-note/auth.config.ts b/memento-note/auth.config.ts index b2ecb44..2ae007b 100644 --- a/memento-note/auth.config.ts +++ b/memento-note/auth.config.ts @@ -14,7 +14,7 @@ export const authConfig = { authorized({ auth, request: { nextUrl } }) { const isLoggedIn = !!auth?.user; const isAdmin = (auth?.user as any)?.role === 'ADMIN'; - const isDashboardPage = nextUrl.pathname === '/' || + const isDashboardPage = nextUrl.pathname === '/home' || nextUrl.pathname.startsWith('/reminders') || nextUrl.pathname.startsWith('/archive') || nextUrl.pathname.startsWith('/trash') || @@ -24,8 +24,14 @@ export const authConfig = { nextUrl.pathname.startsWith('/chat') || nextUrl.pathname.startsWith('/canvas') || nextUrl.pathname.startsWith('/notebooks') || - nextUrl.pathname.startsWith('/note/'); + nextUrl.pathname.startsWith('/note/') || + nextUrl.pathname.startsWith('/brainstorm'); const isAdminPage = nextUrl.pathname.startsWith('/admin'); + const isPublicPage = nextUrl.pathname === '/' || + nextUrl.pathname === '/login' || + nextUrl.pathname === '/register' || + nextUrl.pathname === '/forgot-password' || + nextUrl.pathname.startsWith('/reset-password'); if (isAdminPage) { return isLoggedIn && isAdmin; @@ -34,9 +40,12 @@ export const authConfig = { if (isDashboardPage) { if (isLoggedIn) return true; return false; - } else if (isLoggedIn && (nextUrl.pathname === '/login' || nextUrl.pathname === '/register')) { - return Response.redirect(new URL('/', nextUrl)); } + + if (isLoggedIn && (nextUrl.pathname === '/login' || nextUrl.pathname === '/register')) { + return Response.redirect(new URL('/home', nextUrl)); + } + return true; }, async jwt({ token, user }) { diff --git a/memento-note/components/admin-sidebar.tsx b/memento-note/components/admin-sidebar.tsx index e3e7de8..0e784a8 100644 --- a/memento-note/components/admin-sidebar.tsx +++ b/memento-note/components/admin-sidebar.tsx @@ -122,7 +122,7 @@ export function AdminSidebar({ className }: { className?: string }) {
-
+
@@ -224,7 +224,7 @@ export function AgentCard({ agent, onEdit, onRefresh, onToggle }: AgentCardProps >
{t('agents.status.lastStatus')} {lastAction ? ( {lastAction.status === 'success' && } diff --git a/memento-note/components/agents/agent-detail-view.tsx b/memento-note/components/agents/agent-detail-view.tsx index 13612a0..07e999e 100644 --- a/memento-note/components/agents/agent-detail-view.tsx +++ b/memento-note/components/agents/agent-detail-view.tsx @@ -353,7 +353,7 @@ export function AgentDetailView({
-
+
@@ -369,7 +369,7 @@ export function AgentDetailView({ {isNew ? t('agents.newBadge') : `ID: ${agent?.id?.slice(0, 8)}`} {!isNew && agent?.isEnabled && ( - + {t('agents.actions.toggleOn')} )} @@ -386,7 +386,7 @@ export function AgentDetailView({ {successRate !== null && (
Succès - {successRate}% + {successRate}%
)}
@@ -751,7 +751,7 @@ export function AgentDetailView({

{t('agents.form.frequency')}

-
+
diff --git a/memento-note/components/ai-chat.tsx b/memento-note/components/ai-chat.tsx index 20bf758..8045455 100644 --- a/memento-note/components/ai-chat.tsx +++ b/memento-note/components/ai-chat.tsx @@ -6,9 +6,8 @@ import { DefaultChatTransport } from 'ai' import type { UIMessage } from 'ai' import { cn } from '@/lib/utils' import { - X, Bot, Sparkles, History, Send, Globe, Briefcase, Palette, GraduationCap, Coffee, - Loader2, Layers, Square, Plus, ChevronRight, MessageSquare, FileCode, - Zap, Network, Clock, Scissors, Languages, Layout, ArrowRightLeft, BookOpen, + X, Bot, Sparkles, History, Send, Globe, + Loader2, Square, Plus, MessageSquare, FileCode, Maximize2, Minimize2 } from 'lucide-react' import { useLanguage } from '@/lib/i18n' @@ -28,14 +27,7 @@ function getTextContent(msg: UIMessage): string { return '' } -const TONE_IDS = [ - { id: 'professional', icon: Briefcase }, - { id: 'creative', icon: Palette }, - { id: 'academic', icon: GraduationCap }, - { id: 'casual', icon: Coffee }, -] as const - -type AITab = 'discussion' | 'actions' | 'explore' | 'resources' +type AITab = 'discussion' | 'history' export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: boolean } = {}) { const { t, language } = useLanguage() @@ -45,7 +37,6 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b const [isOpen, setIsOpen] = useState(false) const [isExpanded, setIsExpanded] = useState(false) const [aiTab, setAiTab] = useState('discussion') - const [selectedTone, setSelectedTone] = useState('professional') const [webSearch, setWebSearch] = useState(false) const [chatScope, setChatScope] = useState<'all' | string>('all') const [input, setInput] = useState('') @@ -55,9 +46,6 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b const [history, setHistory] = useState([]) const [historyLoading, setHistoryLoading] = useState(false) - const [insights, setInsights] = useState('') - const [insightsLoading, setInsightsLoading] = useState(false) - const messagesEndRef = useRef(null) const transport = useRef(new DefaultChatTransport({ api: '/api/chat' })).current @@ -65,6 +53,23 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b transport, onError: (error) => { console.error('Chat error:', error) + try { + const parsed = JSON.parse((error as Error).message || '{}') + if (parsed.error === 'QUOTA_EXCEEDED') { + const isBasic = (parsed.currentTier || 'BASIC') === 'BASIC' + toast.error( + language === 'fr' + ? isBasic + ? 'Le chat IA est réservé au plan PRO et supérieur.' + : `Limite mensuelle atteinte pour le plan ${parsed.currentTier}. Elle se réinitialise le mois prochain.` + : isBasic + ? 'AI Chat is available from the PRO plan onwards.' + : `Monthly quota reached for ${parsed.currentTier} plan. It will reset next month.`, + { duration: 8000 } + ) + return + } + } catch {} toast.error(t('chat.assistantError') || 'Chat error') } }) @@ -91,7 +96,6 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b { text }, { body: { - tone: selectedTone, chatScope, notebookId: chatScope !== 'all' ? chatScope : undefined, webSearch: webSearch && webSearchAvailable, @@ -115,24 +119,6 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b setHistoryLoading(false) } - const fetchInsights = async () => { - setInsightsLoading(true) - try { - const res = await fetch('/api/chat/insights') - if (res.ok) { - const data = await res.json() - setInsights(data.insight) - } - } catch (e) { console.error(e) } - setInsightsLoading(false) - } - - useEffect(() => { - if (aiTab === 'discussion') { - // history is loaded on demand via insights tab - } - }, [aiTab]) - useEffect(() => { if (messagesEndRef.current) { messagesEndRef.current.scrollIntoView({ behavior: 'smooth' }) @@ -203,6 +189,7 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b @@ -215,22 +202,28 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b {/* Tabs */}
- {(['discussion', 'actions', 'explore', 'resources'] as AITab[]).map((tab) => { + {(['discussion', 'history'] as AITab[]).map((tab) => { const labels: Record = { discussion: t('ai.chatTab') || 'Discussion', - actions: t('ai.actionsTab') || 'Actions', - explore: t('ai.exploreTab') || 'Explorer', - resources: t('ai.resourcesTab') || 'Ressources', + history: t('ai.historyTab') || 'Historique', + } + const icons: Record = { + discussion: , + history: , } return ( - ) - })} -
-
- + {/* Scope selector */} +
- ))} - -
- -
-
-
-

Generation Tools

-
-
- -
-
- -
-
-
-
-
-
{t('ai.slides') || 'Présentation'}
-

{t('ai.slidesDesc') || 'Convertir en slides interactives'}

-
-
- -
-
- -
-
- -
-
-
-
-
-
{t('ai.diagram') || 'Diagramme'}
-

{t('ai.diagramDesc') || 'Visualisation de structure'}

-
-
- -
-
-
- -
- - Auto-Save Enabled -
- - )} - - {aiTab === 'explore' && ( - -
-
-

Intelligence Modules

-
-
- -
- - - - - -
- -
-

- {t('ai.modulesHint') || 'Ces modules utilisent les embeddings pour analyser vos pensées.'} -

-
- - )} - - {aiTab === 'resources' && ( - -
- -
- - -
-
- -
- -