docs: add complete guide, env files, fix docker-compose
- Add GUIDE.md: complete user documentation covering installation, Docker deployment, AI providers, MCP server, N8N integration, email config, admin panel, env var reference, troubleshooting - Add mcp-server/.env.example with all MCP-specific variables - Update .env.docker.example with all 42 environment variables - Fix docker-compose.yml: parameterize PostgreSQL credentials, add missing env vars (CUSTOM_OPENAI, AI_PROVIDER_CHAT, ALLOW_REGISTRATION, RESEND_API_KEY) - Track memento-note/.env.example
This commit is contained in:
@@ -1,80 +1,101 @@
|
||||
# ============================================
|
||||
# =============================================================================
|
||||
# Memento - Docker Environment Configuration
|
||||
# ============================================
|
||||
# =============================================================================
|
||||
# Copy this file to .env.docker and update with your values.
|
||||
# This file is read by docker-compose.yml via env_file directive.
|
||||
# cp .env.docker.example .env.docker
|
||||
|
||||
# ============================================
|
||||
# Application URL
|
||||
# ============================================
|
||||
# =============================================================================
|
||||
# APPLICATION URL (REQUIRED)
|
||||
# =============================================================================
|
||||
# Change to your server IP or domain
|
||||
# Examples:
|
||||
# IP: http://192.168.1.190:3000
|
||||
# Domain: http://notes.yourdomain.com
|
||||
# HTTPS: https://notes.yourdomain.com
|
||||
NEXTAUTH_URL="http://localhost:3000"
|
||||
|
||||
# ============================================
|
||||
# Authentication Secret
|
||||
# ============================================
|
||||
# =============================================================================
|
||||
# AUTHENTICATION SECRET (REQUIRED)
|
||||
# =============================================================================
|
||||
# Generate with: openssl rand -base64 32
|
||||
NEXTAUTH_SECRET="changethisinproduction"
|
||||
|
||||
# ============================================
|
||||
# PostgreSQL Configuration
|
||||
# ============================================
|
||||
# =============================================================================
|
||||
# REGISTRATION
|
||||
# =============================================================================
|
||||
# Set to "false" to disable public registration (default: true)
|
||||
# ALLOW_REGISTRATION=true
|
||||
|
||||
# =============================================================================
|
||||
# POSTGRESQL CONFIGURATION
|
||||
# =============================================================================
|
||||
POSTGRES_PORT=5432
|
||||
POSTGRES_DB=memento
|
||||
POSTGRES_USER=memento
|
||||
POSTGRES_PASSWORD=memento
|
||||
|
||||
# ============================================
|
||||
# MCP Server Configuration
|
||||
# ============================================
|
||||
# =============================================================================
|
||||
# MCP SERVER CONFIGURATION
|
||||
# =============================================================================
|
||||
# Mode: 'stdio' (Claude Desktop, Cline) or 'sse' (N8N, HTTP)
|
||||
MCP_MODE="stdio"
|
||||
MCP_PORT="3001"
|
||||
|
||||
# ============================================
|
||||
# AI Provider - Tags Generation
|
||||
# ============================================
|
||||
# =============================================================================
|
||||
# AI PROVIDER - TAGS GENERATION
|
||||
# =============================================================================
|
||||
# Options: ollama, openai, custom
|
||||
AI_PROVIDER_TAGS=ollama
|
||||
AI_MODEL_TAGS="granite4:latest"
|
||||
|
||||
# ============================================
|
||||
# AI Provider - Embeddings
|
||||
# ============================================
|
||||
# =============================================================================
|
||||
# AI PROVIDER - EMBEDDINGS
|
||||
# =============================================================================
|
||||
# Options: ollama, openai, custom
|
||||
AI_PROVIDER_EMBEDDING=ollama
|
||||
AI_MODEL_EMBEDDING="embeddinggemma:latest"
|
||||
|
||||
# ============================================
|
||||
# Ollama Configuration
|
||||
# ============================================
|
||||
# =============================================================================
|
||||
# AI PROVIDER - CHAT (optional, falls back to AI_PROVIDER_TAGS)
|
||||
# =============================================================================
|
||||
# AI_PROVIDER_CHAT=ollama
|
||||
# AI_MODEL_CHAT="granite4:latest"
|
||||
|
||||
# =============================================================================
|
||||
# OLLAMA CONFIGURATION (if provider = ollama)
|
||||
# =============================================================================
|
||||
# Docker service: http://ollama:11434
|
||||
# Host machine: http://host.docker.internal:11434
|
||||
# Remote server: http://YOUR_SERVER_IP:11434
|
||||
OLLAMA_BASE_URL="http://ollama:11434"
|
||||
OLLAMA_MODEL="granite4:latest"
|
||||
|
||||
# ============================================
|
||||
# OpenAI Configuration
|
||||
# ============================================
|
||||
# =============================================================================
|
||||
# OPENAI CONFIGURATION (if provider = openai)
|
||||
# =============================================================================
|
||||
# OPENAI_API_KEY="sk-..."
|
||||
|
||||
# ============================================
|
||||
# Custom OpenAI-Compatible Provider
|
||||
# ============================================
|
||||
# OpenRouter, Groq, Together AI, Mistral, etc.
|
||||
# =============================================================================
|
||||
# CUSTOM OPENAI-COMPATIBLE PROVIDER (if provider = custom)
|
||||
# =============================================================================
|
||||
# Compatible with: OpenRouter, Groq, Together AI, Mistral, etc.
|
||||
# OpenRouter: https://openrouter.ai/api/v1
|
||||
# Groq: https://api.groq.com/openai/v1
|
||||
# Together: https://api.together.xyz/v1
|
||||
# Mistral: https://api.mistral.ai/v1
|
||||
# CUSTOM_OPENAI_API_KEY="your-api-key"
|
||||
# CUSTOM_OPENAI_BASE_URL="https://openrouter.ai/api/v1"
|
||||
|
||||
# ============================================
|
||||
# Email / SMTP Configuration
|
||||
# ============================================
|
||||
# =============================================================================
|
||||
# EMAIL / SMTP (optional, required for password reset)
|
||||
# =============================================================================
|
||||
# SMTP_HOST="smtp.gmail.com"
|
||||
# SMTP_PORT="587"
|
||||
# SMTP_USER="your-email@gmail.com"
|
||||
# SMTP_PASS="your-app-password"
|
||||
# SMTP_FROM="noreply@memento.app"
|
||||
|
||||
# ============================================
|
||||
# Application Settings
|
||||
# ============================================
|
||||
# ALLOW_REGISTRATION=true
|
||||
# =============================================================================
|
||||
# RESEND EMAIL (alternative to SMTP, optional)
|
||||
# =============================================================================
|
||||
# RESEND_API_KEY="re_..."
|
||||
|
||||
725
GUIDE.md
Normal file
725
GUIDE.md
Normal file
@@ -0,0 +1,725 @@
|
||||
# Memento - Guide d'utilisation complet
|
||||
|
||||
## Table des matieres
|
||||
|
||||
1. [Presentation](#presentation)
|
||||
2. [Architecture](#architecture)
|
||||
3. [Installation locale](#installation-locale)
|
||||
4. [Deploiement Docker](#deploiement-docker)
|
||||
5. [Configuration des providers IA](#configuration-des-providers-ia)
|
||||
6. [Serveur MCP (Model Context Protocol)](#serveur-mcp)
|
||||
7. [Integrations N8N](#integrations-n8n)
|
||||
8. [Configuration email (SMTP)](#configuration-email)
|
||||
9. [Administration](#administration)
|
||||
10. [Reference des variables d'environnement](#reference-des-variables-denvironnement)
|
||||
11. [Commandes utiles](#commandes-utiles)
|
||||
12. [Resolution des problemes](#resolution-des-problemes)
|
||||
|
||||
---
|
||||
|
||||
## Presentation
|
||||
|
||||
**Memento** est une application de prise de notes intelligente, inspiree de Google Keep, construite avec Next.js 16, TypeScript, Tailwind CSS 4, Prisma et PostgreSQL.
|
||||
|
||||
### Fonctionnalites principales
|
||||
|
||||
- **Notes texte et checklists** avec couleurs personnalisees (10 themes pastel)
|
||||
- **Grille masonry responsive** avec drag-and-drop
|
||||
- **Upload d'images** avec preservation de la taille originale
|
||||
- **Notebooks et labels contextuels** pour organiser les notes
|
||||
- **Recherche semantique** (embeddings IA) + recherche plein texte
|
||||
- **Generation automatique de tags** via IA
|
||||
- **Suggestions de titre** par IA
|
||||
- **Systeme de rappels** avec notifications
|
||||
- **Mode sombre/clair** avec preference systeme
|
||||
- **Authentification** NextAuth.js (credentials, inscription, reset mot de passe)
|
||||
- **Panneau admin** pour configurer les providers IA, SMTP, etc.
|
||||
- **Serveur MCP** pour integrer avec Claude Desktop, N8N, ou tout client MCP
|
||||
- **Support i18n** (FR/EN)
|
||||
- **Export/import de donnees** (JSON)
|
||||
- **PWA** (Progressive Web App)
|
||||
|
||||
### Stack technique
|
||||
|
||||
| Composant | Technologie |
|
||||
|-----------|-------------|
|
||||
| Frontend | Next.js 16, React 19, TypeScript, Tailwind CSS 4 |
|
||||
| UI | shadcn/ui, Lucide React |
|
||||
| Backend | Next.js Server Actions, API Routes |
|
||||
| Base de donnees | PostgreSQL 16 via Prisma ORM 5 |
|
||||
| Authentification | NextAuth.js v5 |
|
||||
| IA | Vercel AI SDK (OpenAI, Ollama, custom) |
|
||||
| MCP | @modelcontextprotocol/sdk |
|
||||
| Email | Nodemailer (SMTP) ou Resend |
|
||||
| Docker | Docker Compose (postgres + memento-note + mcp-server + ollama) |
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Momento/
|
||||
├── docker-compose.yml # Orchestration multi-conteneurs
|
||||
├── .env.docker.example # Template config Docker
|
||||
├── LICENSE # Apache 2.0 + Commons Clause
|
||||
├── memento-note/ # Application Next.js
|
||||
│ ├── app/ # App Router (pages, actions, API)
|
||||
│ ├── components/ # Composants React
|
||||
│ ├── lib/ # Logique metier
|
||||
│ │ ├── ai/ # Providers IA (factory pattern)
|
||||
│ │ ├── prisma.ts # Client DB
|
||||
│ │ ├── mail.ts # Envoi d'emails
|
||||
│ │ └── config.ts # Config depuis DB
|
||||
│ ├── prisma/ # Schema + migrations PostgreSQL
|
||||
│ ├── locales/ # Fichiers i18n (fr.json, en.json)
|
||||
│ ├── Dockerfile # Build multi-stage (node:22-bullseye)
|
||||
│ ├── docker-compose.yml # Standalone (postgres + app)
|
||||
│ └── .env.example # Template dev local
|
||||
├── mcp-server/ # Serveur MCP
|
||||
│ ├── index.js # Mode stdio (Claude Desktop)
|
||||
│ ├── index-sse.js # Mode SSE/HTTP (N8N, remote)
|
||||
│ ├── tools.js # Definitions des outils MCP
|
||||
│ ├── Dockerfile # Conteneur MCP (node:20-alpine)
|
||||
│ └── .env.example # Template MCP
|
||||
├── scripts/ # Scripts de migration
|
||||
└── n8n-memento-workflow.json # Workflow N8N preconfigure
|
||||
```
|
||||
|
||||
### Flux de donnees
|
||||
|
||||
```
|
||||
Navigateur → Next.js App Router
|
||||
├── Server Components (lecture)
|
||||
├── Server Actions (ecriture)
|
||||
└── API Routes (REST)
|
||||
↓
|
||||
Prisma ORM
|
||||
↓
|
||||
PostgreSQL 16
|
||||
↑
|
||||
MCP Server (stdio ou SSE)
|
||||
↑
|
||||
Claude Desktop / N8N / Client MCP
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Installation locale
|
||||
|
||||
### Prerequis
|
||||
|
||||
- Node.js 20+ (22 recommande)
|
||||
- PostgreSQL 16
|
||||
- npm
|
||||
|
||||
### Etapes
|
||||
|
||||
```bash
|
||||
# 1. Cloner le depot
|
||||
git clone https://github.com/votre-user/Momento.git
|
||||
cd Momento/memento-note
|
||||
|
||||
# 2. Installer les dependances
|
||||
npm install --legacy-peer-deps
|
||||
|
||||
# 3. Configurer l'environnement
|
||||
cp .env.example .env
|
||||
# Editer .env avec vos valeurs (DATABASE_URL, NEXTAUTH_SECRET, etc.)
|
||||
|
||||
# 4. Creer la base de donnees et appliquer les migrations
|
||||
npx prisma migrate dev
|
||||
|
||||
# 5. Lancer en developpement
|
||||
npm run dev
|
||||
|
||||
# 6. Acceder a l'application
|
||||
# http://localhost:3000
|
||||
```
|
||||
|
||||
### Premier lancement
|
||||
|
||||
1. Creez un compte via la page d'inscription
|
||||
2. Le premier utilisateur inscrit devient automatiquement admin
|
||||
3. Accedez au panneau admin : `http://localhost:3000/admin/settings`
|
||||
|
||||
---
|
||||
|
||||
## Deploiement Docker
|
||||
|
||||
### Quick Start
|
||||
|
||||
```bash
|
||||
# 1. Cloner le depot
|
||||
git clone https://github.com/votre-user/Momento.git
|
||||
cd Momento
|
||||
|
||||
# 2. Configurer l'environnement
|
||||
cp .env.docker.example .env.docker
|
||||
nano .env.docker
|
||||
# Modifier NEXTAUTH_URL et NEXTAUTH_SECRET (obligatoire)
|
||||
|
||||
# 3. Lancer les conteneurs
|
||||
docker compose up -d
|
||||
|
||||
# 4. Verifier que tout fonctionne
|
||||
docker compose ps
|
||||
docker compose logs -f memento-note
|
||||
```
|
||||
|
||||
### Avec Ollama (IA locale)
|
||||
|
||||
```bash
|
||||
# Ajouter le profile ollama
|
||||
docker compose --profile ollama up -d
|
||||
|
||||
# Tirer un modele
|
||||
docker exec memento-ollama ollama pull granite4
|
||||
docker exec memento-ollama ollama pull embeddinggemma
|
||||
```
|
||||
|
||||
### Variables Docker obligatoires
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `NEXTAUTH_URL` | URL publique de l'app (ex: `http://192.168.1.190:3000`) |
|
||||
| `NEXTAUTH_SECRET` | Secret JWT - generer avec `openssl rand -base64 32` |
|
||||
|
||||
### Ports utilises
|
||||
|
||||
| Service | Port | Description |
|
||||
|---------|------|-------------|
|
||||
| memento-note | 3000 | Application web |
|
||||
| PostgreSQL | 5432 | Base de donnees |
|
||||
| MCP Server | 3001 | SSE mode uniquement |
|
||||
| Ollama | 11434 | IA locale (optionnel) |
|
||||
|
||||
### Reverse Proxy (Nginx)
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name notes.yourdomain.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
client_max_body_size 10M;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration des providers IA
|
||||
|
||||
Memento supporte 3 providers IA, configurables independamment pour chaque fonctionnalite (tags, embeddings, chat).
|
||||
|
||||
### Provider 1 : Ollama (IA locale, gratuit)
|
||||
|
||||
```bash
|
||||
# .env.docker
|
||||
AI_PROVIDER_TAGS=ollama
|
||||
AI_PROVIDER_EMBEDDING=ollama
|
||||
OLLAMA_BASE_URL="http://ollama:11434"
|
||||
AI_MODEL_TAGS="granite4:latest"
|
||||
AI_MODEL_EMBEDDING="embeddinggemma:latest"
|
||||
```
|
||||
|
||||
Lancer avec le profile ollama :
|
||||
```bash
|
||||
docker compose --profile ollama up -d
|
||||
docker exec memento-ollama ollama pull granite4
|
||||
docker exec memento-ollama ollama pull embeddinggemma
|
||||
```
|
||||
|
||||
**Ressources recommandees** : 8 Go RAM, 4 CPU
|
||||
|
||||
### Provider 2 : OpenAI (cloud, payant)
|
||||
|
||||
```bash
|
||||
# .env.docker
|
||||
AI_PROVIDER_TAGS=openai
|
||||
AI_PROVIDER_EMBEDDING=openai
|
||||
OPENAI_API_KEY="sk-votre-cle-ici"
|
||||
AI_MODEL_TAGS="gpt-4o-mini"
|
||||
AI_MODEL_EMBEDDING="text-embedding-3-small"
|
||||
```
|
||||
|
||||
### Provider 3 : Custom (OpenRouter, Groq, Together, Mistral...)
|
||||
|
||||
```bash
|
||||
# .env.docker
|
||||
AI_PROVIDER_TAGS=custom
|
||||
AI_PROVIDER_EMBEDDING=custom
|
||||
CUSTOM_OPENAI_API_KEY="votre-cle-api"
|
||||
CUSTOM_OPENAI_BASE_URL="https://openrouter.ai/api/v1"
|
||||
AI_MODEL_TAGS="openai/gpt-4o-mini"
|
||||
AI_MODEL_EMBEDDING="openai/text-embedding-3-small"
|
||||
```
|
||||
|
||||
| Provider | URL de base |
|
||||
|----------|-------------|
|
||||
| OpenRouter | `https://openrouter.ai/api/v1` |
|
||||
| Groq | `https://api.groq.com/openai/v1` |
|
||||
| Together AI | `https://api.together.xyz/v1` |
|
||||
| Mistral | `https://api.mistral.ai/v1` |
|
||||
|
||||
### Configuration mixte
|
||||
|
||||
Vous pouvez utiliser des providers differents pour chaque fonctionnalite :
|
||||
|
||||
```bash
|
||||
# Ollama pour les tags, OpenAI pour les embeddings
|
||||
AI_PROVIDER_TAGS=ollama
|
||||
AI_PROVIDER_EMBEDDING=openai
|
||||
OLLAMA_BASE_URL="http://ollama:11434"
|
||||
OPENAI_API_KEY="sk-..."
|
||||
AI_MODEL_TAGS="granite4:latest"
|
||||
AI_MODEL_EMBEDDING="text-embedding-3-small"
|
||||
```
|
||||
|
||||
### Configuration via le panneau admin
|
||||
|
||||
Les providers IA peuvent aussi etre configures depuis l'interface :
|
||||
1. Se connecter en admin
|
||||
2. Aller sur `/admin/settings`
|
||||
3. Section "AI Settings"
|
||||
4. Choisir le provider, le modele, et entrer la cle API
|
||||
5. Sauvegarder
|
||||
|
||||
**Note** : Les parametres du panneau admin (stockes en DB) sont prioritaires sur les variables d'environnement.
|
||||
|
||||
---
|
||||
|
||||
## Serveur MCP
|
||||
|
||||
Le serveur MCP (Model Context Protocol) permet aux agents IA d'interagir avec vos notes via un protocole standardise.
|
||||
|
||||
### Outils disponibles (9 outils)
|
||||
|
||||
| Outil | Description |
|
||||
|-------|-------------|
|
||||
| `create_note` | Creer une nouvelle note |
|
||||
| `get_notes` | Recuperer toutes les notes |
|
||||
| `get_note` | Recuperer une note specifique |
|
||||
| `update_note` | Modifier une note existante |
|
||||
| `delete_note` | Supprimer une note |
|
||||
| `search_notes` | Rechercher des notes par contenu |
|
||||
| `get_labels` | Lister les labels |
|
||||
| `toggle_pin` | Epingler/Depingler une note |
|
||||
| `toggle_archive` | Archiver/Desarchiver une note |
|
||||
|
||||
### Mode stdio (Claude Desktop, Cline)
|
||||
|
||||
Communication via stdin/stdout, ideal pour les clients locaux.
|
||||
|
||||
**Configuration Claude Desktop** (`claude_desktop_config.json`) :
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"memento": {
|
||||
"command": "docker",
|
||||
"args": ["exec", "-i", "memento-mcp", "node", "index.js"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Mode SSE (N8N, HTTP)
|
||||
|
||||
Communication via HTTP Server-Sent Events, accessible sur le reseau.
|
||||
|
||||
```bash
|
||||
# .env.docker
|
||||
MCP_MODE="sse"
|
||||
MCP_PORT="3001"
|
||||
```
|
||||
|
||||
Le serveur sera accessible sur `http://localhost:3001`.
|
||||
|
||||
#### Endpoints SSE
|
||||
|
||||
| Endpoint | Methode | Description |
|
||||
|----------|---------|-------------|
|
||||
| `/` | GET | Health check |
|
||||
| `/sse` | GET/POST | Endpoint MCP principal |
|
||||
| `/message` | POST | Envoi de messages |
|
||||
| `/sessions` | GET | Sessions actives |
|
||||
|
||||
#### Verification
|
||||
|
||||
```bash
|
||||
curl http://localhost:3001/
|
||||
# {"name":"Memento MCP SSE Server","version":"1.0.0","status":"running"}
|
||||
```
|
||||
|
||||
### Configuration MCP avancee
|
||||
|
||||
```bash
|
||||
# .env.docker
|
||||
MCP_LOG_LEVEL=info # debug, info, warn, error
|
||||
MCP_REQUEST_TIMEOUT=30000 # Timeout en ms
|
||||
MCP_REQUIRE_AUTH=false # Activer l'authentification
|
||||
MCP_API_KEY="votre-cle" # Cle API si auth active
|
||||
APP_BASE_URL="http://localhost:3000" # URL de l'app pour les liens
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integrations N8N
|
||||
|
||||
### Configuration du noeud MCP Client
|
||||
|
||||
1. Ajouter un noeud **"MCP Client"** dans N8N
|
||||
2. Selectionner **"HTTP Streamable"** comme transport
|
||||
3. Configurer l'endpoint : `http://memento-mcp:3001/sse` (Docker) ou `http://VOTRE_IP:3001/sse`
|
||||
|
||||
### Workflows preconfigures
|
||||
|
||||
Le fichier `n8n-memento-workflow.json` contient un workflow pret a l'emploi pour :
|
||||
- Creer des notes automatiquement
|
||||
- Rechercher des notes
|
||||
- Archiver les notes anciennes
|
||||
|
||||
### Exemples d'utilisation
|
||||
|
||||
#### Creer une note via curl (SSE mode)
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3001/sse \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "create_note",
|
||||
"arguments": {
|
||||
"title": "Ma note",
|
||||
"content": "Contenu de la note"
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
#### Lister les outils disponibles
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3001/sse \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"method": "tools/list",
|
||||
"params": {}
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration email
|
||||
|
||||
L'email est necessaire pour la reinitialisation de mot de passe et les rappels.
|
||||
|
||||
### Option 1 : SMTP
|
||||
|
||||
```bash
|
||||
# .env.docker
|
||||
SMTP_HOST="smtp.gmail.com"
|
||||
SMTP_PORT="587"
|
||||
SMTP_USER="votre-email@gmail.com"
|
||||
SMTP_PASS="votre-mot-de-passe-app"
|
||||
SMTP_FROM="noreply@votre-domaine.com"
|
||||
```
|
||||
|
||||
| Provider | Host | Port |
|
||||
|----------|------|------|
|
||||
| Gmail | smtp.gmail.com | 587 |
|
||||
| Outlook | smtp.office365.com | 587 |
|
||||
| SendGrid | smtp.sendgrid.net | 587 |
|
||||
| Mailgun | smtp.mailgun.org | 587 |
|
||||
|
||||
### Option 2 : Resend
|
||||
|
||||
```bash
|
||||
# .env.docker
|
||||
RESEND_API_KEY="re_votre-cle"
|
||||
```
|
||||
|
||||
Les parametres `SMTP_SECURE` et `SMTP_IGNORE_CERT` sont configurables depuis le panneau admin.
|
||||
|
||||
---
|
||||
|
||||
## Administration
|
||||
|
||||
### Panneau admin
|
||||
|
||||
Accessibles uniquement par le premier utilisateur inscrit (ou les utilisateurs avec le role admin).
|
||||
|
||||
**URL** : `/admin/settings`
|
||||
|
||||
### Sections configurables
|
||||
|
||||
1. **AI Settings** - Provider, modele, cle API pour tags, embeddings et chat
|
||||
2. **Email Settings** - Configuration SMTP
|
||||
3. **General Settings** - Inscription publique activee/desactivee
|
||||
|
||||
### Gestion des utilisateurs
|
||||
|
||||
- Les utilisateurs sont geres via `/admin`
|
||||
- Le premier utilisateur inscrit est automatiquement admin
|
||||
- L'inscription peut etre desactivee avec `ALLOW_REGISTRATION=false`
|
||||
|
||||
---
|
||||
|
||||
## Reference des variables d'environnement
|
||||
|
||||
### Fichiers .env
|
||||
|
||||
| Fichier | Usage |
|
||||
|---------|-------|
|
||||
| `.env.docker` | Configuration Docker (lu par docker-compose via env_file) |
|
||||
| `.env.docker.example` | Template pour `.env.docker` |
|
||||
| `memento-note/.env` | Configuration dev local |
|
||||
| `memento-note/.env.example` | Template pour dev local |
|
||||
| `mcp-server/.env` | Configuration du serveur MCP |
|
||||
| `mcp-server/.env.example` | Template pour MCP |
|
||||
|
||||
### Variables completes
|
||||
|
||||
#### Core (obligatoire)
|
||||
|
||||
| Variable | Defaut | Description |
|
||||
|----------|--------|-------------|
|
||||
| `DATABASE_URL` | - | Connection string PostgreSQL |
|
||||
| `NEXTAUTH_SECRET` | - | Secret JWT (`openssl rand -base64 32`) |
|
||||
| `NEXTAUTH_URL` | `http://localhost:3000` | URL publique de l'app |
|
||||
|
||||
#### PostgreSQL (Docker)
|
||||
|
||||
| Variable | Defaut | Description |
|
||||
|----------|--------|-------------|
|
||||
| `POSTGRES_USER` | `memento` | Utilisateur PostgreSQL |
|
||||
| `POSTGRES_PASSWORD` | `memento` | Mot de passe PostgreSQL |
|
||||
| `POSTGRES_DB` | `memento` | Nom de la base |
|
||||
| `POSTGRES_PORT` | `5432` | Port expose |
|
||||
|
||||
#### IA - Tags
|
||||
|
||||
| Variable | Defaut | Description |
|
||||
|----------|--------|-------------|
|
||||
| `AI_PROVIDER_TAGS` | - | Provider pour tags (`ollama`, `openai`, `custom`) |
|
||||
| `AI_MODEL_TAGS` | `granite4:latest` | Modele pour tags |
|
||||
|
||||
#### IA - Embeddings
|
||||
|
||||
| Variable | Defaut | Description |
|
||||
|----------|--------|-------------|
|
||||
| `AI_PROVIDER_EMBEDDING` | - | Provider pour embeddings |
|
||||
| `AI_MODEL_EMBEDDING` | `embeddinggemma:latest` | Modele pour embeddings |
|
||||
|
||||
#### IA - Chat
|
||||
|
||||
| Variable | Defaut | Description |
|
||||
|----------|--------|-------------|
|
||||
| `AI_PROVIDER_CHAT` | fallback Tags | Provider pour chat |
|
||||
| `AI_MODEL_CHAT` | fallback Tags | Modele pour chat |
|
||||
|
||||
#### IA - OpenAI
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `OPENAI_API_KEY` | Cle API OpenAI (`sk-...`) |
|
||||
|
||||
#### IA - Ollama
|
||||
|
||||
| Variable | Defaut | Description |
|
||||
|----------|--------|-------------|
|
||||
| `OLLAMA_BASE_URL` | - | URL du service Ollama |
|
||||
|
||||
#### IA - Custom provider
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `CUSTOM_OPENAI_API_KEY` | Cle API du provider |
|
||||
| `CUSTOM_OPENAI_BASE_URL` | URL de base du provider |
|
||||
|
||||
#### MCP Server
|
||||
|
||||
| Variable | Defaut | Description |
|
||||
|----------|--------|-------------|
|
||||
| `MCP_MODE` | `stdio` | Mode (`stdio` ou `sse`) |
|
||||
| `MCP_PORT` | `3001` | Port SSE |
|
||||
| `MCP_LOG_LEVEL` | `info` | Niveau de log |
|
||||
| `MCP_REQUEST_TIMEOUT` | `30000` | Timeout (ms) |
|
||||
| `MCP_REQUIRE_AUTH` | `false` | Activer authentification |
|
||||
| `MCP_API_KEY` | - | Cle API pour auth |
|
||||
| `APP_BASE_URL` | `http://localhost:3000` | URL de l'app |
|
||||
| `USER_ID` | - | Filtrer par utilisateur |
|
||||
|
||||
#### Email
|
||||
|
||||
| Variable | Defaut | Description |
|
||||
|----------|--------|-------------|
|
||||
| `SMTP_HOST` | - | Serveur SMTP |
|
||||
| `SMTP_PORT` | `587` | Port SMTP |
|
||||
| `SMTP_USER` | - | Utilisateur SMTP |
|
||||
| `SMTP_PASS` | - | Mot de passe SMTP |
|
||||
| `SMTP_FROM` | `noreply@memento.app` | Email expediteur |
|
||||
| `RESEND_API_KEY` | - | Cle API Resend |
|
||||
|
||||
#### Application
|
||||
|
||||
| Variable | Defaut | Description |
|
||||
|----------|--------|-------------|
|
||||
| `ALLOW_REGISTRATION` | `true` | Autoriser l'inscription publique |
|
||||
| `NODE_ENV` | `development` | Environnement |
|
||||
| `PORT` | `3000` | Port de l'app |
|
||||
| `HOSTNAME` | `0.0.0.0` | Host d'ecoute |
|
||||
|
||||
---
|
||||
|
||||
## Commandes utiles
|
||||
|
||||
### Docker
|
||||
|
||||
```bash
|
||||
# Demarrer tous les services
|
||||
docker compose up -d
|
||||
|
||||
# Avec Ollama
|
||||
docker compose --profile ollama up -d
|
||||
|
||||
# Voir les logs
|
||||
docker compose logs -f
|
||||
docker compose logs -f memento-note # App seulement
|
||||
docker compose logs -f mcp-server # MCP seulement
|
||||
|
||||
# Statut des services
|
||||
docker compose ps
|
||||
|
||||
# Reconstruire apres modification
|
||||
docker compose down
|
||||
docker compose build --no-cache
|
||||
docker compose up -d
|
||||
|
||||
# Acceder a un conteneur
|
||||
docker compose exec memento-note sh
|
||||
docker compose exec mcp-server sh
|
||||
|
||||
# Arreter et supprimer les volumes (ATTENTION : perte de donnees)
|
||||
docker compose down -v
|
||||
```
|
||||
|
||||
### Base de donnees
|
||||
|
||||
```bash
|
||||
# Lancer Prisma Studio (GUI pour la DB)
|
||||
cd memento-note
|
||||
npx prisma studio
|
||||
|
||||
# Creer une migration
|
||||
npx prisma migrate dev --name nom_de_la_migration
|
||||
|
||||
# Appliquer les migrations en production
|
||||
npx prisma migrate deploy
|
||||
|
||||
# Regenerer le client Prisma
|
||||
npx prisma generate
|
||||
```
|
||||
|
||||
### Developpement
|
||||
|
||||
```bash
|
||||
cd memento-note
|
||||
|
||||
# Dev avec hot-reload
|
||||
npm run dev
|
||||
|
||||
# Build production
|
||||
npm run build
|
||||
npm start
|
||||
|
||||
# Linter
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### Deploy.sh (script de deploiement)
|
||||
|
||||
```bash
|
||||
cd memento-note
|
||||
chmod +x deploy.sh
|
||||
|
||||
./deploy.sh build # Construire l'image
|
||||
./deploy.sh start # Demarrer les conteneurs
|
||||
./deploy.sh logs # Voir les logs
|
||||
./deploy.sh backup # Sauvegarder la DB
|
||||
./deploy.sh update # Mettre a jour l'app
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Resolution des problemes
|
||||
|
||||
### Erreur : `ECONNREFUSED 127.0.0.1:11434`
|
||||
|
||||
**Cause** : L'app essaie d'acceder a Ollama via localhost au lieu du service Docker.
|
||||
|
||||
**Solution** : Verifier que `.env.docker` contient :
|
||||
```
|
||||
OLLAMA_BASE_URL="http://ollama:11434"
|
||||
```
|
||||
|
||||
### Le provider IA ne change pas dans l'admin
|
||||
|
||||
1. Sauvegarder les modifications dans l'admin
|
||||
2. Rafraichir la page (F5)
|
||||
3. Verifier la valeur affichee sous le dropdown
|
||||
|
||||
### Docker ne demarre pas
|
||||
|
||||
```bash
|
||||
# Verifier les logs
|
||||
docker compose logs memento-note
|
||||
|
||||
# Verifier la DB
|
||||
docker compose exec postgres pg_isready -U memento
|
||||
|
||||
# Reconstruire
|
||||
docker compose build --no-cache memento-note
|
||||
```
|
||||
|
||||
### Mot de passe oublie sans SMTP
|
||||
|
||||
Si vous n'avez pas configure l'email, vous pouvez reinitialiser le mot de passe manuellement :
|
||||
|
||||
```bash
|
||||
# Se connecter a la DB
|
||||
docker compose exec postgres psql -U memento -d memento
|
||||
|
||||
# ou en local
|
||||
cd memento-note
|
||||
npx prisma studio
|
||||
```
|
||||
|
||||
### Les embeddings ne se generent pas
|
||||
|
||||
1. Verifier que le provider d'embeddings est correctement configure
|
||||
2. Verifier que le modele d'embedding est disponible
|
||||
3. En admin, lancer la regeneration manuelle des embeddings
|
||||
|
||||
---
|
||||
|
||||
## Licence
|
||||
|
||||
Apache License 2.0 avec Commons Clause Restriction.
|
||||
Usage personnel et educatif libre. Usage commercial interdit sans autorisation ecrite de l'auteur.
|
||||
Voir le fichier [LICENSE](LICENSE) pour les details complets.
|
||||
@@ -7,15 +7,15 @@ services:
|
||||
container_name: memento-postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: keepnotes
|
||||
POSTGRES_PASSWORD: keepnotes
|
||||
POSTGRES_DB: keepnotes
|
||||
POSTGRES_USER: ${POSTGRES_USER:-memento}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-memento}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-memento}
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "5432:5432"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U keepnotes"]
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-memento}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
@@ -35,7 +35,7 @@ services:
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- DATABASE_URL=postgresql://keepnotes:keepnotes@postgres:5432/keepnotes
|
||||
- DATABASE_URL=postgresql://${POSTGRES_USER:-memento}:${POSTGRES_PASSWORD:-memento}@postgres:5432/${POSTGRES_DB:-memento}
|
||||
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET:-changethisinproduction}
|
||||
- NEXTAUTH_URL=${NEXTAUTH_URL:-http://localhost:3000}
|
||||
- NODE_ENV=production
|
||||
@@ -51,11 +51,16 @@ services:
|
||||
# AI Providers
|
||||
- AI_PROVIDER_TAGS=${AI_PROVIDER_TAGS}
|
||||
- AI_PROVIDER_EMBEDDING=${AI_PROVIDER_EMBEDDING}
|
||||
- AI_PROVIDER_CHAT=${AI_PROVIDER_CHAT}
|
||||
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
||||
- OLLAMA_BASE_URL=${OLLAMA_BASE_URL}
|
||||
- OLLAMA_MODEL=${OLLAMA_MODEL}
|
||||
- AI_MODEL_TAGS=${AI_MODEL_TAGS}
|
||||
- AI_MODEL_EMBEDDING=${AI_MODEL_EMBEDDING}
|
||||
- AI_MODEL_CHAT=${AI_MODEL_CHAT}
|
||||
- CUSTOM_OPENAI_API_KEY=${CUSTOM_OPENAI_API_KEY}
|
||||
- CUSTOM_OPENAI_BASE_URL=${CUSTOM_OPENAI_BASE_URL}
|
||||
- ALLOW_REGISTRATION=${ALLOW_REGISTRATION:-true}
|
||||
- RESEND_API_KEY=${RESEND_API_KEY}
|
||||
volumes:
|
||||
- uploads-data:/app/public/uploads
|
||||
depends_on:
|
||||
@@ -97,7 +102,7 @@ services:
|
||||
- MCP_MODE=${MCP_MODE:-stdio}
|
||||
- PORT=${MCP_PORT:-3001}
|
||||
# Database - connect to shared PostgreSQL
|
||||
- DATABASE_URL=postgresql://keepnotes:keepnotes@postgres:5432/keepnotes
|
||||
- DATABASE_URL=postgresql://${POSTGRES_USER:-memento}:${POSTGRES_PASSWORD:-memento}@postgres:5432/${POSTGRES_DB:-memento}
|
||||
- NODE_ENV=production
|
||||
depends_on:
|
||||
postgres:
|
||||
|
||||
50
mcp-server/.env.example
Normal file
50
mcp-server/.env.example
Normal file
@@ -0,0 +1,50 @@
|
||||
# =============================================================================
|
||||
# MCP Server - Environment Variables
|
||||
# =============================================================================
|
||||
# Copy this file to .env and fill in the values.
|
||||
# cp .env.example .env
|
||||
|
||||
# =============================================================================
|
||||
# DATABASE (REQUIRED)
|
||||
# =============================================================================
|
||||
# Local development (if sharing SQLite with memento-note):
|
||||
# DATABASE_URL="file:../memento-note/prisma/dev.db"
|
||||
# PostgreSQL:
|
||||
DATABASE_URL="postgresql://memento:memento@localhost:5432/memento"
|
||||
|
||||
# =============================================================================
|
||||
# MCP SERVER MODE
|
||||
# =============================================================================
|
||||
# 'stdio' = Standard I/O (Claude Desktop, Cline, etc.)
|
||||
# 'sse' = Server-Sent Events / HTTP (N8N, remote access, port 3001)
|
||||
MCP_MODE="stdio"
|
||||
|
||||
# =============================================================================
|
||||
# SSE MODE SETTINGS (only if MCP_MODE="sse")
|
||||
# =============================================================================
|
||||
# Port for HTTP/SSE mode
|
||||
# PORT=3001
|
||||
|
||||
# Request timeout in milliseconds (default: 30000)
|
||||
# MCP_REQUEST_TIMEOUT=30000
|
||||
|
||||
# Log level: debug, info, warn, error (default: info)
|
||||
# MCP_LOG_LEVEL=info
|
||||
|
||||
# =============================================================================
|
||||
# AUTHENTICATION (SSE mode only)
|
||||
# =============================================================================
|
||||
# Enable authentication (default: false in dev mode)
|
||||
# MCP_REQUIRE_AUTH=false
|
||||
|
||||
# Static API key for authentication (if MCP_REQUIRE_AUTH=true)
|
||||
# MCP_API_KEY="your-secret-api-key"
|
||||
|
||||
# =============================================================================
|
||||
# APPLICATION
|
||||
# =============================================================================
|
||||
# Base URL of the Memento web app (for generating links in responses)
|
||||
# APP_BASE_URL="http://localhost:3000"
|
||||
|
||||
# Filter data to a specific user ID (optional, null = all users)
|
||||
# USER_ID=""
|
||||
64
memento-note/.env.example
Normal file
64
memento-note/.env.example
Normal file
@@ -0,0 +1,64 @@
|
||||
# =============================================================================
|
||||
# Memento Note - Environment Variables
|
||||
# Copy this file to .env and fill in the values
|
||||
# =============================================================================
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Core (required)
|
||||
# -----------------------------------------------------------------------------
|
||||
DATABASE_URL="postgresql://user:password@localhost:5432/memento"
|
||||
NEXTAUTH_SECRET="generate-with-openssl-rand-base64-32"
|
||||
NEXTAUTH_URL="http://localhost:3000"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Registration
|
||||
# -----------------------------------------------------------------------------
|
||||
# Set to "false" to disable public registration (default: true)
|
||||
# ALLOW_REGISTRATION="true"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# AI Providers
|
||||
# -----------------------------------------------------------------------------
|
||||
# Main provider: "openai" | "ollama" | "deepseek" | "openrouter" | "custom-openai"
|
||||
# AI_PROVIDER="openai"
|
||||
|
||||
# Per-feature provider overrides (optional, falls back to AI_PROVIDER)
|
||||
# AI_PROVIDER_CHAT="openai"
|
||||
# AI_PROVIDER_TAGS="openai"
|
||||
# AI_PROVIDER_EMBEDDING="openai"
|
||||
|
||||
# Model names (optional, uses provider defaults)
|
||||
# AI_MODEL_CHAT="gpt-4o-mini"
|
||||
# AI_MODEL_TAGS="gpt-4o-mini"
|
||||
# AI_MODEL_EMBEDDING="text-embedding-3-small"
|
||||
|
||||
# OpenAI
|
||||
# OPENAI_API_KEY="sk-..."
|
||||
|
||||
# Ollama (local)
|
||||
# OLLAMA_BASE_URL="http://localhost:11434"
|
||||
|
||||
# Custom OpenAI-compatible endpoint
|
||||
# CUSTOM_OPENAI_API_KEY="..."
|
||||
# CUSTOM_OPENAI_BASE_URL="https://your-provider.com/v1"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Email (at least one provider required for password reset)
|
||||
# -----------------------------------------------------------------------------
|
||||
# Resend (https://resend.com)
|
||||
# RESEND_API_KEY="re_..."
|
||||
|
||||
# SMTP
|
||||
# SMTP_HOST="smtp.example.com"
|
||||
# SMTP_PORT="587"
|
||||
# SMTP_USER=""
|
||||
# SMTP_PASS=""
|
||||
# SMTP_FROM="noreply@example.com"
|
||||
# SMTP_SECURE="false"
|
||||
# SMTP_IGNORE_CERT="false"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# MCP (Model Context Protocol)
|
||||
# -----------------------------------------------------------------------------
|
||||
# MCP_SERVER_MODE="disabled"
|
||||
# MCP_SERVER_URL=""
|
||||
1
memento-note/.gitignore
vendored
1
memento-note/.gitignore
vendored
@@ -32,6 +32,7 @@ yarn-error.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
!.env.example
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { SettingsNav, SettingsSection, SettingToggle, SettingSelect, SettingInput } from '@/components/settings'
|
||||
import { updateAISettings } from '@/app/actions/ai-settings'
|
||||
import { toast } from 'sonner'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
|
||||
export default function AISettingsPage() {
|
||||
const { t } = useLanguage()
|
||||
const [apiKey, setApiKey] = useState('')
|
||||
|
||||
// Mock settings state - in real implementation, load from server
|
||||
const [settings, setSettings] = useState({
|
||||
titleSuggestions: true,
|
||||
semanticSearch: true,
|
||||
paragraphRefactor: true,
|
||||
memoryEcho: true,
|
||||
memoryEchoFrequency: 'daily' as 'daily' | 'weekly' | 'custom',
|
||||
aiProvider: 'auto' as 'auto' | 'openai' | 'ollama',
|
||||
preferredLanguage: 'auto' as 'auto' | 'en' | 'fr' | 'es' | 'de' | 'fa' | 'it' | 'pt' | 'ru' | 'zh' | 'ja' | 'ko' | 'ar' | 'hi' | 'nl' | 'pl',
|
||||
demoMode: false
|
||||
})
|
||||
|
||||
const handleToggle = async (feature: string, value: boolean) => {
|
||||
setSettings(prev => ({ ...prev, [feature]: value }))
|
||||
try {
|
||||
await updateAISettings({ [feature]: value })
|
||||
} catch (error) {
|
||||
toast.error(t('aiSettings.error'))
|
||||
setSettings(settings) // Revert on error
|
||||
}
|
||||
}
|
||||
|
||||
const handleFrequencyChange = async (value: string) => {
|
||||
setSettings(prev => ({ ...prev, memoryEchoFrequency: value as any }))
|
||||
try {
|
||||
await updateAISettings({ memoryEchoFrequency: value as any })
|
||||
} catch (error) {
|
||||
toast.error(t('aiSettings.error'))
|
||||
}
|
||||
}
|
||||
|
||||
const handleProviderChange = async (value: string) => {
|
||||
setSettings(prev => ({ ...prev, aiProvider: value as any }))
|
||||
try {
|
||||
await updateAISettings({ aiProvider: value as any })
|
||||
} catch (error) {
|
||||
toast.error(t('aiSettings.error'))
|
||||
}
|
||||
}
|
||||
|
||||
const handleApiKeyChange = async (value: string) => {
|
||||
setApiKey(value)
|
||||
// TODO: Implement API key persistence
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-10 px-4 max-w-6xl">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
||||
{/* Sidebar Navigation */}
|
||||
<aside className="lg:col-span-1">
|
||||
<SettingsNav />
|
||||
</aside>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="lg:col-span-3 space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold mb-2">{t('aiSettings.title')}</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
{t('aiSettings.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* AI Provider */}
|
||||
<SettingsSection
|
||||
title={t('aiSettings.provider')}
|
||||
icon={<span className="text-2xl">🤖</span>}
|
||||
description={t('aiSettings.providerDesc')}
|
||||
>
|
||||
<SettingSelect
|
||||
label={t('aiSettings.provider')}
|
||||
description={t('aiSettings.providerDesc')}
|
||||
value={settings.aiProvider}
|
||||
options={[
|
||||
{
|
||||
value: 'auto',
|
||||
label: t('aiSettings.providerAuto'),
|
||||
description: t('aiSettings.providerAutoDesc')
|
||||
},
|
||||
{
|
||||
value: 'ollama',
|
||||
label: t('aiSettings.providerOllama'),
|
||||
description: t('aiSettings.providerOllamaDesc')
|
||||
},
|
||||
{
|
||||
value: 'openai',
|
||||
label: t('aiSettings.providerOpenAI'),
|
||||
description: t('aiSettings.providerOpenAIDesc')
|
||||
},
|
||||
]}
|
||||
onChange={handleProviderChange}
|
||||
/>
|
||||
|
||||
{settings.aiProvider === 'openai' && (
|
||||
<SettingInput
|
||||
label={t('admin.ai.apiKey')}
|
||||
description={t('admin.ai.openAIKeyDescription')}
|
||||
value={apiKey}
|
||||
type="password"
|
||||
placeholder="sk-..."
|
||||
onChange={handleApiKeyChange}
|
||||
/>
|
||||
)}
|
||||
</SettingsSection>
|
||||
|
||||
{/* Feature Toggles */}
|
||||
<SettingsSection
|
||||
title={t('aiSettings.features')}
|
||||
icon={<span className="text-2xl">✨</span>}
|
||||
description={t('aiSettings.description')}
|
||||
>
|
||||
<SettingToggle
|
||||
label={t('titleSuggestions.available').replace('💡 ', '')}
|
||||
description={t('aiSettings.titleSuggestionsDesc')}
|
||||
checked={settings.titleSuggestions}
|
||||
onChange={(checked) => handleToggle('titleSuggestions', checked)}
|
||||
/>
|
||||
|
||||
<SettingToggle
|
||||
label={t('semanticSearch.exactMatch')}
|
||||
description={t('semanticSearch.searching')}
|
||||
checked={settings.semanticSearch}
|
||||
onChange={(checked) => handleToggle('semanticSearch', checked)}
|
||||
/>
|
||||
|
||||
<SettingToggle
|
||||
label={t('paragraphRefactor.title')}
|
||||
description={t('aiSettings.paragraphRefactorDesc')}
|
||||
checked={settings.paragraphRefactor}
|
||||
onChange={(checked) => handleToggle('paragraphRefactor', checked)}
|
||||
/>
|
||||
|
||||
<SettingToggle
|
||||
label={t('memoryEcho.title')}
|
||||
description={t('memoryEcho.dailyInsight')}
|
||||
checked={settings.memoryEcho}
|
||||
onChange={(checked) => handleToggle('memoryEcho', checked)}
|
||||
/>
|
||||
|
||||
{settings.memoryEcho && (
|
||||
<SettingSelect
|
||||
label={t('aiSettings.frequency')}
|
||||
description={t('aiSettings.frequencyDesc')}
|
||||
value={settings.memoryEchoFrequency}
|
||||
options={[
|
||||
{ value: 'daily', label: t('aiSettings.frequencyDaily') },
|
||||
{ value: 'weekly', label: t('aiSettings.frequencyWeekly') },
|
||||
{ value: 'custom', label: 'Custom' },
|
||||
]}
|
||||
onChange={handleFrequencyChange}
|
||||
/>
|
||||
)}
|
||||
</SettingsSection>
|
||||
|
||||
{/* Demo Mode */}
|
||||
<SettingsSection
|
||||
title={t('demoMode.title')}
|
||||
icon={<span className="text-2xl">🎭</span>}
|
||||
description={t('demoMode.description')}
|
||||
>
|
||||
<SettingToggle
|
||||
label={t('demoMode.title')}
|
||||
description={t('demoMode.description')}
|
||||
checked={settings.demoMode}
|
||||
onChange={(checked) => handleToggle('demoMode', checked)}
|
||||
/>
|
||||
</SettingsSection>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { SettingsNav, SettingsSection, SettingToggle, SettingInput, SettingSelect } from '@/components/settings'
|
||||
import { updateAISettings } from '@/app/actions/ai-settings'
|
||||
import { toast } from 'sonner'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
|
||||
export default function ProfileSettingsPage() {
|
||||
const { t } = useLanguage()
|
||||
|
||||
// Mock user data - in real implementation, load from server
|
||||
const [user, setUser] = useState({
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com'
|
||||
})
|
||||
|
||||
const [language, setLanguage] = useState('auto')
|
||||
const [showRecentNotes, setShowRecentNotes] = useState(false)
|
||||
|
||||
const handleNameChange = async (value: string) => {
|
||||
setUser(prev => ({ ...prev, name: value }))
|
||||
// TODO: Implement profile update
|
||||
|
||||
}
|
||||
|
||||
const handleEmailChange = async (value: string) => {
|
||||
setUser(prev => ({ ...prev, email: value }))
|
||||
// TODO: Implement email update
|
||||
|
||||
}
|
||||
|
||||
const handleLanguageChange = async (value: string) => {
|
||||
setLanguage(value)
|
||||
try {
|
||||
await updateAISettings({ preferredLanguage: value as any })
|
||||
} catch (error) {
|
||||
console.error('Error updating language:', error)
|
||||
toast.error(t('aiSettings.error'))
|
||||
}
|
||||
}
|
||||
|
||||
const handleRecentNotesChange = async (enabled: boolean) => {
|
||||
setShowRecentNotes(enabled)
|
||||
try {
|
||||
await updateAISettings({ showRecentNotes: enabled })
|
||||
} catch (error) {
|
||||
console.error('Error updating recent notes setting:', error)
|
||||
toast.error(t('aiSettings.error'))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-10 px-4 max-w-6xl">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
||||
{/* Sidebar Navigation */}
|
||||
<aside className="lg:col-span-1">
|
||||
<SettingsNav />
|
||||
</aside>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="lg:col-span-3 space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold mb-2">{t('profile.title')}</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
{t('profile.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Profile Information */}
|
||||
<SettingsSection
|
||||
title={t('profile.accountSettings')}
|
||||
icon={<span className="text-2xl">👤</span>}
|
||||
description={t('profile.description')}
|
||||
>
|
||||
<SettingInput
|
||||
label={t('profile.displayName')}
|
||||
description={t('profile.displayName')}
|
||||
value={user.name}
|
||||
onChange={handleNameChange}
|
||||
placeholder={t('auth.namePlaceholder')}
|
||||
/>
|
||||
|
||||
<SettingInput
|
||||
label={t('profile.email')}
|
||||
description={t('profile.email')}
|
||||
value={user.email}
|
||||
type="email"
|
||||
onChange={handleEmailChange}
|
||||
placeholder={t('auth.emailPlaceholder')}
|
||||
/>
|
||||
</SettingsSection>
|
||||
|
||||
{/* Preferences */}
|
||||
<SettingsSection
|
||||
title={t('settings.language')}
|
||||
icon={<span className="text-2xl">⚙️</span>}
|
||||
description={t('profile.languagePreferencesDescription')}
|
||||
>
|
||||
<SettingSelect
|
||||
label={t('profile.preferredLanguage')}
|
||||
description={t('profile.languageDescription')}
|
||||
value={language}
|
||||
options={[
|
||||
{ value: 'auto', label: t('profile.autoDetect') },
|
||||
{ value: 'en', label: 'English' },
|
||||
{ value: 'fr', label: 'Français' },
|
||||
{ value: 'es', label: 'Español' },
|
||||
{ value: 'de', label: 'Deutsch' },
|
||||
{ value: 'fa', label: 'فارسی' },
|
||||
{ value: 'it', label: 'Italiano' },
|
||||
{ value: 'pt', label: 'Português' },
|
||||
{ value: 'ru', label: 'Русский' },
|
||||
{ value: 'zh', label: '中文' },
|
||||
{ value: 'ja', label: '日本語' },
|
||||
{ value: 'ko', label: '한국어' },
|
||||
{ value: 'ar', label: 'العربية' },
|
||||
{ value: 'hi', label: 'हिन्दी' },
|
||||
{ value: 'nl', label: 'Nederlands' },
|
||||
{ value: 'pl', label: 'Polski' },
|
||||
]}
|
||||
onChange={handleLanguageChange}
|
||||
/>
|
||||
|
||||
<SettingToggle
|
||||
label={t('profile.showRecentNotes')}
|
||||
description={t('profile.showRecentNotesDescription')}
|
||||
checked={showRecentNotes}
|
||||
onChange={handleRecentNotesChange}
|
||||
/>
|
||||
</SettingsSection>
|
||||
|
||||
{/* AI Settings Link */}
|
||||
<div className="p-6 border rounded-lg bg-gradient-to-r from-purple-50 to-pink-50 dark:from-purple-950 dark:to-pink-950">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-4xl">✨</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-lg mb-1">{t('aiSettings.title')}</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{t('aiSettings.description')}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => window.location.href = '/settings/ai'}
|
||||
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
|
||||
>
|
||||
{t('nav.configureAI')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { getSystemConfig } from '@/lib/config'
|
||||
import bcrypt from 'bcryptjs'
|
||||
import { getEmailTemplate } from '@/lib/email-template'
|
||||
|
||||
// Helper simple pour générer un token sans dépendance externe lourde
|
||||
// Simple helper to generate a token without heavy external dependencies
|
||||
function generateToken() {
|
||||
const array = new Uint8Array(32);
|
||||
globalThis.crypto.getRandomValues(array);
|
||||
@@ -19,7 +19,7 @@ export async function forgotPassword(email: string) {
|
||||
try {
|
||||
const user = await prisma.user.findUnique({ where: { email: email.toLowerCase() } });
|
||||
if (!user) {
|
||||
// Pour des raisons de sécurité, on ne dit pas si l'email existe ou pas
|
||||
// For security reasons, don't reveal whether the email exists
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ export async function forgotPassword(email: string) {
|
||||
}
|
||||
});
|
||||
|
||||
const resetLink = `${process.env.NEXTAUTH_URL || 'http://localhost:3000'}/reset-password?token=${token}`;
|
||||
const resetLink = `${process.env.NEXTAUTH_URL}/reset-password?token=${token}`;
|
||||
|
||||
const html = getEmailTemplate(
|
||||
"Reset your Password",
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
'use server'
|
||||
|
||||
import { revalidatePath } from 'next/cache'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { auth } from '@/auth'
|
||||
import bcrypt from 'bcryptjs'
|
||||
import { z } from 'zod'
|
||||
|
||||
const ProfileSchema = z.object({
|
||||
name: z.string().min(2, "Name must be at least 2 characters"),
|
||||
email: z.string().email().optional(), // Email change might require verification logic, keeping it simple for now or read-only
|
||||
})
|
||||
|
||||
const PasswordSchema = z.object({
|
||||
currentPassword: z.string().min(1, "Current password is required"),
|
||||
newPassword: z.string().min(6, "New password must be at least 6 characters"),
|
||||
confirmPassword: z.string().min(6, "Confirm password must be at least 6 characters"),
|
||||
}).refine((data) => data.newPassword === data.confirmPassword, {
|
||||
message: "Passwords don't match",
|
||||
path: ["confirmPassword"],
|
||||
})
|
||||
|
||||
export async function updateProfile(data: { name: string }) {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) throw new Error('Unauthorized')
|
||||
|
||||
const validated = ProfileSchema.safeParse(data)
|
||||
if (!validated.success) {
|
||||
return { error: validated.error.flatten().fieldErrors }
|
||||
}
|
||||
|
||||
try {
|
||||
await prisma.user.update({
|
||||
where: { id: session.user.id },
|
||||
data: { name: validated.data.name },
|
||||
})
|
||||
revalidatePath('/settings/profile')
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
return { error: { _form: ['Failed to update profile'] } }
|
||||
}
|
||||
}
|
||||
|
||||
export async function changePassword(formData: FormData) {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) throw new Error('Unauthorized')
|
||||
|
||||
const rawData = {
|
||||
currentPassword: formData.get('currentPassword'),
|
||||
newPassword: formData.get('newPassword'),
|
||||
confirmPassword: formData.get('confirmPassword'),
|
||||
}
|
||||
|
||||
const validated = PasswordSchema.safeParse(rawData)
|
||||
if (!validated.success) {
|
||||
return { error: validated.error.flatten().fieldErrors }
|
||||
}
|
||||
|
||||
const { currentPassword, newPassword } = validated.data
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: session.user.id },
|
||||
})
|
||||
|
||||
if (!user || !user.password) {
|
||||
return { error: { _form: ['User not found'] } }
|
||||
}
|
||||
|
||||
const passwordsMatch = await bcrypt.compare(currentPassword, user.password)
|
||||
if (!passwordsMatch) {
|
||||
return { error: { currentPassword: ['Incorrect current password'] } }
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(newPassword, 10)
|
||||
|
||||
try {
|
||||
await prisma.user.update({
|
||||
where: { id: session.user.id },
|
||||
data: { password: hashedPassword },
|
||||
})
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
return { error: { _form: ['Failed to change password'] } }
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateTheme(theme: string) {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) return { error: 'Unauthorized' }
|
||||
|
||||
try {
|
||||
await prisma.user.update({
|
||||
where: { id: session.user.id },
|
||||
data: { theme },
|
||||
})
|
||||
revalidatePath('/')
|
||||
revalidatePath('/settings/profile')
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
return { error: 'Failed to update theme' }
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateLanguage(language: string) {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) return { error: 'Unauthorized' }
|
||||
|
||||
try {
|
||||
// Update or create UserAISettings with the preferred language
|
||||
await prisma.userAISettings.upsert({
|
||||
where: { userId: session.user.id },
|
||||
create: {
|
||||
userId: session.user.id,
|
||||
preferredLanguage: language,
|
||||
},
|
||||
update: {
|
||||
preferredLanguage: language,
|
||||
},
|
||||
})
|
||||
|
||||
// Note: The language will be applied on next page load
|
||||
// The client component should handle updating localStorage and reloading
|
||||
revalidatePath('/')
|
||||
revalidatePath('/settings/profile')
|
||||
return { success: true, language }
|
||||
} catch (error) {
|
||||
console.error('Failed to update language:', error)
|
||||
return { error: 'Failed to update language' }
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateFontSize(fontSize: string) {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) return { error: 'Unauthorized' }
|
||||
|
||||
try {
|
||||
// Check if UserAISettings exists
|
||||
const existing = await prisma.userAISettings.findUnique({
|
||||
where: { userId: session.user.id }
|
||||
})
|
||||
|
||||
let result
|
||||
if (existing) {
|
||||
// Update existing - only update fontSize field
|
||||
result = await prisma.userAISettings.update({
|
||||
where: { userId: session.user.id },
|
||||
data: { fontSize: fontSize }
|
||||
})
|
||||
} else {
|
||||
// Create new with all required fields
|
||||
result = await prisma.userAISettings.create({
|
||||
data: {
|
||||
userId: session.user.id,
|
||||
fontSize: fontSize,
|
||||
// Set default values for required fields
|
||||
titleSuggestions: true,
|
||||
semanticSearch: true,
|
||||
paragraphRefactor: true,
|
||||
memoryEcho: true,
|
||||
memoryEchoFrequency: 'daily',
|
||||
aiProvider: 'auto',
|
||||
preferredLanguage: 'auto',
|
||||
showRecentNotes: false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
revalidatePath('/')
|
||||
revalidatePath('/settings/profile')
|
||||
return { success: true, fontSize }
|
||||
} catch (error) {
|
||||
console.error('[updateFontSize] Failed to update font size:', error)
|
||||
return { error: 'Failed to update font size' }
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateShowRecentNotes(showRecentNotes: boolean) {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) return { error: 'Unauthorized' }
|
||||
|
||||
try {
|
||||
// Use EXACT same pattern as updateFontSize which works
|
||||
const existing = await prisma.userAISettings.findUnique({
|
||||
where: { userId: session.user.id }
|
||||
})
|
||||
|
||||
if (existing) {
|
||||
// Try Prisma client first, fallback to raw SQL if field doesn't exist in client
|
||||
try {
|
||||
await prisma.userAISettings.update({
|
||||
where: { userId: session.user.id },
|
||||
data: { showRecentNotes: showRecentNotes } as any
|
||||
})
|
||||
} catch (prismaError: any) {
|
||||
// If Prisma client doesn't know about showRecentNotes, use raw SQL
|
||||
if (prismaError?.message?.includes('Unknown argument') || prismaError?.code === 'P2009') {
|
||||
const value = showRecentNotes ? 1 : 0
|
||||
await prisma.$executeRaw`
|
||||
UPDATE UserAISettings
|
||||
SET showRecentNotes = ${value}
|
||||
WHERE userId = ${session.user.id}
|
||||
`
|
||||
} else {
|
||||
throw prismaError
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Create new - same as updateFontSize
|
||||
await prisma.userAISettings.create({
|
||||
data: {
|
||||
userId: session.user.id,
|
||||
titleSuggestions: true,
|
||||
semanticSearch: true,
|
||||
paragraphRefactor: true,
|
||||
memoryEcho: true,
|
||||
memoryEchoFrequency: 'daily',
|
||||
aiProvider: 'auto',
|
||||
preferredLanguage: 'auto',
|
||||
fontSize: 'medium',
|
||||
showRecentNotes: showRecentNotes
|
||||
} as any
|
||||
})
|
||||
}
|
||||
|
||||
revalidatePath('/')
|
||||
revalidatePath('/settings/profile')
|
||||
return { success: true, showRecentNotes }
|
||||
} catch (error) {
|
||||
console.error('[updateShowRecentNotes] Failed:', error)
|
||||
return { error: 'Failed to update show recent notes setting' }
|
||||
}
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
'use server'
|
||||
|
||||
import { revalidatePath } from 'next/cache'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { auth } from '@/auth'
|
||||
import bcrypt from 'bcryptjs'
|
||||
import { z } from 'zod'
|
||||
|
||||
const ProfileSchema = z.object({
|
||||
name: z.string().min(2, "Name must be at least 2 characters"),
|
||||
email: z.string().email().optional(), // Email change might require verification logic, keeping it simple for now or read-only
|
||||
})
|
||||
|
||||
const PasswordSchema = z.object({
|
||||
currentPassword: z.string().min(1, "Current password is required"),
|
||||
newPassword: z.string().min(6, "New password must be at least 6 characters"),
|
||||
confirmPassword: z.string().min(6, "Confirm password must be at least 6 characters"),
|
||||
}).refine((data) => data.newPassword === data.confirmPassword, {
|
||||
message: "Passwords don't match",
|
||||
path: ["confirmPassword"],
|
||||
})
|
||||
|
||||
export async function updateProfile(data: { name: string }) {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) throw new Error('Unauthorized')
|
||||
|
||||
const validated = ProfileSchema.safeParse(data)
|
||||
if (!validated.success) {
|
||||
return { error: validated.error.flatten().fieldErrors }
|
||||
}
|
||||
|
||||
try {
|
||||
await prisma.user.update({
|
||||
where: { id: session.user.id },
|
||||
data: { name: validated.data.name },
|
||||
})
|
||||
revalidatePath('/settings/profile')
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
return { error: { _form: ['Failed to update profile'] } }
|
||||
}
|
||||
}
|
||||
|
||||
export async function changePassword(formData: FormData) {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) throw new Error('Unauthorized')
|
||||
|
||||
const rawData = {
|
||||
currentPassword: formData.get('currentPassword'),
|
||||
newPassword: formData.get('newPassword'),
|
||||
confirmPassword: formData.get('confirmPassword'),
|
||||
}
|
||||
|
||||
const validated = PasswordSchema.safeParse(rawData)
|
||||
if (!validated.success) {
|
||||
return { error: validated.error.flatten().fieldErrors }
|
||||
}
|
||||
|
||||
const { currentPassword, newPassword } = validated.data
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: session.user.id },
|
||||
})
|
||||
|
||||
if (!user || !user.password) {
|
||||
return { error: { _form: ['User not found'] } }
|
||||
}
|
||||
|
||||
const passwordsMatch = await bcrypt.compare(currentPassword, user.password)
|
||||
if (!passwordsMatch) {
|
||||
return { error: { currentPassword: ['Incorrect current password'] } }
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(newPassword, 10)
|
||||
|
||||
try {
|
||||
await prisma.user.update({
|
||||
where: { id: session.user.id },
|
||||
data: { password: hashedPassword },
|
||||
})
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
return { error: { _form: ['Failed to change password'] } }
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateTheme(theme: string) {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) return { error: 'Unauthorized' }
|
||||
|
||||
try {
|
||||
await prisma.user.update({
|
||||
where: { id: session.user.id },
|
||||
data: { theme },
|
||||
})
|
||||
revalidatePath('/')
|
||||
revalidatePath('/settings/profile')
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
return { error: 'Failed to update theme' }
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateLanguage(language: string) {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) return { error: 'Unauthorized' }
|
||||
|
||||
try {
|
||||
// Update or create UserAISettings with the preferred language
|
||||
await prisma.userAISettings.upsert({
|
||||
where: { userId: session.user.id },
|
||||
create: {
|
||||
userId: session.user.id,
|
||||
preferredLanguage: language,
|
||||
},
|
||||
update: {
|
||||
preferredLanguage: language,
|
||||
},
|
||||
})
|
||||
|
||||
// Note: The language will be applied on next page load
|
||||
// The client component should handle updating localStorage and reloading
|
||||
revalidatePath('/')
|
||||
revalidatePath('/settings/profile')
|
||||
return { success: true, language }
|
||||
} catch (error) {
|
||||
console.error('Failed to update language:', error)
|
||||
return { error: 'Failed to update language' }
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateFontSize(fontSize: string) {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) return { error: 'Unauthorized' }
|
||||
|
||||
try {
|
||||
// Check if UserAISettings exists
|
||||
const existing = await prisma.userAISettings.findUnique({
|
||||
where: { userId: session.user.id }
|
||||
})
|
||||
|
||||
let result
|
||||
if (existing) {
|
||||
// Update existing - only update fontSize field
|
||||
result = await prisma.userAISettings.update({
|
||||
where: { userId: session.user.id },
|
||||
data: { fontSize: fontSize }
|
||||
})
|
||||
} else {
|
||||
// Create new with all required fields
|
||||
result = await prisma.userAISettings.create({
|
||||
data: {
|
||||
userId: session.user.id,
|
||||
fontSize: fontSize,
|
||||
// Set default values for required fields
|
||||
titleSuggestions: true,
|
||||
semanticSearch: true,
|
||||
paragraphRefactor: true,
|
||||
memoryEcho: true,
|
||||
memoryEchoFrequency: 'daily',
|
||||
aiProvider: 'auto',
|
||||
preferredLanguage: 'auto',
|
||||
showRecentNotes: false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
revalidatePath('/')
|
||||
revalidatePath('/settings/profile')
|
||||
return { success: true, fontSize }
|
||||
} catch (error) {
|
||||
console.error('[updateFontSize] Failed to update font size:', error)
|
||||
return { error: 'Failed to update font size' }
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateShowRecentNotes(showRecentNotes: boolean) {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) return { error: 'Unauthorized' }
|
||||
|
||||
try {
|
||||
// Use EXACT same pattern as updateFontSize which works
|
||||
const existing = await prisma.userAISettings.findUnique({
|
||||
where: { userId: session.user.id }
|
||||
})
|
||||
|
||||
if (existing) {
|
||||
// Try Prisma client first, fallback to raw SQL if field doesn't exist in client
|
||||
try {
|
||||
await prisma.userAISettings.update({
|
||||
where: { userId: session.user.id },
|
||||
data: { showRecentNotes: showRecentNotes } as any
|
||||
})
|
||||
} catch (prismaError: any) {
|
||||
// If Prisma client doesn't know about showRecentNotes, use raw SQL
|
||||
if (prismaError?.message?.includes('Unknown argument') || prismaError?.code === 'P2009') {
|
||||
const value = showRecentNotes ? 1 : 0
|
||||
await prisma.$executeRaw`
|
||||
UPDATE UserAISettings
|
||||
SET showRecentNotes = ${value}
|
||||
WHERE userId = ${session.user.id}
|
||||
`
|
||||
} else {
|
||||
throw prismaError
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Create new - same as updateFontSize
|
||||
await prisma.userAISettings.create({
|
||||
data: {
|
||||
userId: session.user.id,
|
||||
titleSuggestions: true,
|
||||
semanticSearch: true,
|
||||
paragraphRefactor: true,
|
||||
memoryEcho: true,
|
||||
memoryEchoFrequency: 'daily',
|
||||
aiProvider: 'auto',
|
||||
preferredLanguage: 'auto',
|
||||
fontSize: 'medium',
|
||||
showRecentNotes: showRecentNotes
|
||||
} as any
|
||||
})
|
||||
}
|
||||
|
||||
revalidatePath('/')
|
||||
revalidatePath('/settings/profile')
|
||||
return { success: true, showRecentNotes }
|
||||
} catch (error) {
|
||||
console.error('[updateShowRecentNotes] Failed:', error)
|
||||
return { error: 'Failed to update show recent notes setting' }
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ export const authConfig = {
|
||||
signIn: '/login',
|
||||
newUser: '/register',
|
||||
},
|
||||
secret: "csQFtfYvQ8YtatEYSUFyslXdk2vJhZFt9D5gav/RJQg=",
|
||||
secret: process.env.NEXTAUTH_SECRET,
|
||||
trustHost: true,
|
||||
session: {
|
||||
strategy: 'jwt',
|
||||
|
||||
@@ -113,25 +113,25 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
|
||||
const [comparisonNotes, setComparisonNotes] = useState<Array<Partial<Note>>>([])
|
||||
const [fusionNotes, setFusionNotes] = useState<Array<Partial<Note>>>([])
|
||||
|
||||
// Tags rejetés par l'utilisateur pour cette session
|
||||
// Tags dismissed by the user for this session
|
||||
const [dismissedTags, setDismissedTags] = useState<string[]>([])
|
||||
|
||||
const colorClasses = NOTE_COLORS[color as NoteColor] || NOTE_COLORS.default
|
||||
|
||||
const handleSelectGhostTag = async (tag: string) => {
|
||||
// Vérification insensible à la casse
|
||||
// Case-insensitive check
|
||||
const tagExists = labels.some(l => l.toLowerCase() === tag.toLowerCase())
|
||||
|
||||
if (!tagExists) {
|
||||
setLabels(prev => [...prev, tag])
|
||||
|
||||
// Créer le label globalement s'il n'existe pas
|
||||
// Create the label globally if it doesn't exist
|
||||
const globalExists = globalLabels.some(l => l.name.toLowerCase() === tag.toLowerCase())
|
||||
if (!globalExists) {
|
||||
try {
|
||||
await addLabel(tag)
|
||||
} catch (err) {
|
||||
console.error('Erreur création label auto:', err)
|
||||
console.error('Error creating auto-label:', err)
|
||||
}
|
||||
}
|
||||
toast.success(t('ai.tagAdded', { tag }))
|
||||
@@ -142,8 +142,8 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
|
||||
setDismissedTags(prev => [...prev, tag])
|
||||
}
|
||||
|
||||
// Filtrer les suggestions pour ne pas afficher celles rejetées par l'utilisateur
|
||||
// ni celles déjà présentes sur la note
|
||||
// Filter suggestions to exclude dismissed ones
|
||||
// and those already present on the note
|
||||
const existingLabelsLower = (note.labels || []).map((l) => l.toLowerCase())
|
||||
const filteredSuggestions = suggestions.filter(s => {
|
||||
if (!s || !s.tag) return false
|
||||
@@ -241,7 +241,7 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
|
||||
setTitleSuggestions(data.suggestions || [])
|
||||
toast.success(t('ai.titlesGenerated', { count: data.suggestions.length }))
|
||||
} catch (error: any) {
|
||||
console.error('Erreur génération titres:', error)
|
||||
console.error('Error generating titles:', error)
|
||||
toast.error(error.message || t('ai.titleGenerationFailed'))
|
||||
} finally {
|
||||
setIsGeneratingTitles(false)
|
||||
@@ -311,7 +311,7 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
|
||||
option: data.option
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error('Erreur reformulation:', error)
|
||||
console.error('Error reformulating:', error)
|
||||
toast.error(error.message || t('ai.reformulationFailed'))
|
||||
} finally {
|
||||
setIsReformulating(false)
|
||||
@@ -494,10 +494,10 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
|
||||
cleanupOrphanedImages(removedImageUrls, note.id).catch(() => {})
|
||||
}
|
||||
|
||||
// Rafraîchir les labels globaux pour refléter les suppressions éventuelles (orphans)
|
||||
// Refresh global labels to reflect any deletions (orphans)
|
||||
await refreshLabels()
|
||||
|
||||
// Rafraîchir la liste des notes
|
||||
// Refresh the notes list
|
||||
triggerRefresh()
|
||||
|
||||
onClose()
|
||||
|
||||
@@ -15,14 +15,14 @@ export function useAutoTagging({ content, notebookId, enabled = true }: UseAutoT
|
||||
const [isAnalyzing, setIsAnalyzing] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Debounce le contenu de 1.5s
|
||||
// Debounce content by 1.5s
|
||||
const debouncedContent = useDebounce(content, 1500);
|
||||
|
||||
// Track previous notebookId to detect when note is moved to a notebook
|
||||
const previousNotebookId = useRef<string | null | undefined>(notebookId);
|
||||
|
||||
const analyzeContent = async (contentToAnalyze: string) => {
|
||||
// CRITICAL: Don't suggest labels in "Notes générales" (notebookId is null)
|
||||
// CRITICAL: Don't suggest labels in "General Notes" (notebookId is null)
|
||||
// Labels should ONLY appear within notebooks, not in the general notes section
|
||||
if (!notebookId) {
|
||||
setSuggestions([]);
|
||||
@@ -49,13 +49,13 @@ export function useAutoTagging({ content, notebookId, enabled = true }: UseAutoT
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Erreur lors de l\'analyse');
|
||||
throw new Error('Error during analysis');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
setSuggestions(data.tags || []);
|
||||
} catch (err) {
|
||||
setError('Impossible de générer des suggestions');
|
||||
setError('Failed to generate suggestions');
|
||||
} finally {
|
||||
setIsAnalyzing(false);
|
||||
}
|
||||
@@ -78,7 +78,7 @@ export function useAutoTagging({ content, notebookId, enabled = true }: UseAutoT
|
||||
const prev = previousNotebookId.current;
|
||||
previousNotebookId.current = notebookId;
|
||||
|
||||
// Detect when note is moved FROM "Notes générales" (null) TO a notebook
|
||||
// Detect when note is moved FROM "General Notes" (null) TO a notebook
|
||||
const wasMovedToNotebook = (prev === null || prev === undefined) && notebookId;
|
||||
|
||||
if (wasMovedToNotebook && content && content.length >= 10) {
|
||||
|
||||
@@ -17,7 +17,7 @@ export function useTitleSuggestions({ content, enabled = true }: UseTitleSuggest
|
||||
const [isAnalyzing, setIsAnalyzing] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// Debounce le contenu de 2s pour éviter trop d'appels
|
||||
// Debounce content by 2s to avoid excessive API calls
|
||||
const debouncedContent = useDebounce(content, 2000)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -28,7 +28,7 @@ export function useTitleSuggestions({ content, enabled = true }: UseTitleSuggest
|
||||
|
||||
const wordCount = debouncedContent.split(/\s+/).length
|
||||
|
||||
// Il faut au moins 10 mots
|
||||
// Need at least 10 words
|
||||
if (wordCount < 10) {
|
||||
setSuggestions([])
|
||||
return
|
||||
@@ -49,14 +49,14 @@ export function useTitleSuggestions({ content, enabled = true }: UseTitleSuggest
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error || 'Erreur lors de la génération des titres')
|
||||
throw new Error(errorData.error || 'Error generating title suggestions')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
setSuggestions(data.suggestions || [])
|
||||
} catch (err) {
|
||||
console.error('❌ Title suggestions error:', err)
|
||||
setError('Impossible de générer des suggestions de titres')
|
||||
setError('Failed to generate title suggestions')
|
||||
} finally {
|
||||
setIsAnalyzing(false)
|
||||
}
|
||||
|
||||
@@ -40,17 +40,17 @@ export class CustomOpenAIProvider implements AIProvider {
|
||||
model: this.model,
|
||||
schema: z.object({
|
||||
tags: z.array(z.object({
|
||||
tag: z.string().describe('Le nom du tag, court et en minuscules'),
|
||||
confidence: z.number().min(0).max(1).describe('Le niveau de confiance entre 0 et 1')
|
||||
tag: z.string().describe('Short tag name in lowercase'),
|
||||
confidence: z.number().min(0).max(1).describe('Confidence level between 0 and 1')
|
||||
}))
|
||||
}),
|
||||
prompt: `Analyse la note suivante et suggère entre 1 et 5 tags pertinents.
|
||||
Contenu de la note: "${content}"`,
|
||||
prompt: `Analyze the following note and suggest 1 to 5 relevant tags.
|
||||
Note content: "${content}"`,
|
||||
});
|
||||
|
||||
return object.tags;
|
||||
} catch (e) {
|
||||
console.error('Erreur génération tags Custom OpenAI:', e);
|
||||
console.error('Error generating tags (Custom OpenAI):', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,7 @@ export class CustomOpenAIProvider implements AIProvider {
|
||||
});
|
||||
return embedding;
|
||||
} catch (e) {
|
||||
console.error('Erreur embeddings Custom OpenAI:', e);
|
||||
console.error('Error generating embeddings (Custom OpenAI):', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -74,8 +74,8 @@ export class CustomOpenAIProvider implements AIProvider {
|
||||
model: this.model,
|
||||
schema: z.object({
|
||||
titles: z.array(z.object({
|
||||
title: z.string().describe('Le titre suggéré'),
|
||||
confidence: z.number().min(0).max(1).describe('Le niveau de confiance entre 0 et 1')
|
||||
title: z.string().describe('Suggested title'),
|
||||
confidence: z.number().min(0).max(1).describe('Confidence level between 0 and 1')
|
||||
}))
|
||||
}),
|
||||
prompt: prompt,
|
||||
@@ -83,7 +83,7 @@ export class CustomOpenAIProvider implements AIProvider {
|
||||
|
||||
return object.titles;
|
||||
} catch (e) {
|
||||
console.error('Erreur génération titres Custom OpenAI:', e);
|
||||
console.error('Error generating titles (Custom OpenAI):', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -97,7 +97,7 @@ export class CustomOpenAIProvider implements AIProvider {
|
||||
|
||||
return text.trim();
|
||||
} catch (e) {
|
||||
console.error('Erreur génération texte Custom OpenAI:', e);
|
||||
console.error('Error generating text (Custom OpenAI):', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -112,7 +112,7 @@ export class CustomOpenAIProvider implements AIProvider {
|
||||
|
||||
return { text: text.trim() };
|
||||
} catch (e) {
|
||||
console.error('Erreur chat Custom OpenAI:', e);
|
||||
console.error('Error in chat (Custom OpenAI):', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,17 +24,17 @@ export class DeepSeekProvider implements AIProvider {
|
||||
model: this.model,
|
||||
schema: z.object({
|
||||
tags: z.array(z.object({
|
||||
tag: z.string().describe('Le nom du tag, court et en minuscules'),
|
||||
confidence: z.number().min(0).max(1).describe('Le niveau de confiance entre 0 et 1')
|
||||
tag: z.string().describe('Short tag name in lowercase'),
|
||||
confidence: z.number().min(0).max(1).describe('Confidence level between 0 and 1')
|
||||
}))
|
||||
}),
|
||||
prompt: `Analyse la note suivante et suggère entre 1 et 5 tags pertinents.
|
||||
Contenu de la note: "${content}"`,
|
||||
prompt: `Analyze the following note and suggest 1 to 5 relevant tags.
|
||||
Note content: "${content}"`,
|
||||
});
|
||||
|
||||
return object.tags;
|
||||
} catch (e) {
|
||||
console.error('Erreur génération tags DeepSeek:', e);
|
||||
console.error('Error generating tags (DeepSeek):', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ export class DeepSeekProvider implements AIProvider {
|
||||
});
|
||||
return embedding;
|
||||
} catch (e) {
|
||||
console.error('Erreur embeddings DeepSeek:', e);
|
||||
console.error('Error generating embeddings (DeepSeek):', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -58,8 +58,8 @@ export class DeepSeekProvider implements AIProvider {
|
||||
model: this.model,
|
||||
schema: z.object({
|
||||
titles: z.array(z.object({
|
||||
title: z.string().describe('Le titre suggéré'),
|
||||
confidence: z.number().min(0).max(1).describe('Le niveau de confiance entre 0 et 1')
|
||||
title: z.string().describe('Suggested title'),
|
||||
confidence: z.number().min(0).max(1).describe('Confidence level between 0 and 1')
|
||||
}))
|
||||
}),
|
||||
prompt: prompt,
|
||||
@@ -67,7 +67,7 @@ export class DeepSeekProvider implements AIProvider {
|
||||
|
||||
return object.titles;
|
||||
} catch (e) {
|
||||
console.error('Erreur génération titres DeepSeek:', e);
|
||||
console.error('Error generating titles (DeepSeek):', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -81,7 +81,7 @@ export class DeepSeekProvider implements AIProvider {
|
||||
|
||||
return text.trim();
|
||||
} catch (e) {
|
||||
console.error('Erreur génération texte DeepSeek:', e);
|
||||
console.error('Error generating text (DeepSeek):', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -96,7 +96,7 @@ export class DeepSeekProvider implements AIProvider {
|
||||
|
||||
return { text: text.trim() };
|
||||
} catch (e) {
|
||||
console.error('Erreur chat DeepSeek:', e);
|
||||
console.error('Error in chat (DeepSeek):', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ Note content: "${content}"`;
|
||||
return JSON.parse(jsonMatch[0]);
|
||||
}
|
||||
|
||||
// Support pour le format { "tags": [...] }
|
||||
// Support for { "tags": [...] } format
|
||||
const objectMatch = text.match(/\{\s*"tags"\s*:\s*(\[[\s\S]*\])\s*\}/);
|
||||
if (objectMatch && objectMatch[1]) {
|
||||
return JSON.parse(objectMatch[1]);
|
||||
@@ -83,7 +83,7 @@ Note content: "${content}"`;
|
||||
|
||||
return [];
|
||||
} catch (e) {
|
||||
console.error('Erreur API directe Ollama:', e);
|
||||
console.error('Error in Ollama API:', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -104,7 +104,7 @@ Note content: "${content}"`;
|
||||
const data = await response.json();
|
||||
return data.embedding;
|
||||
} catch (e) {
|
||||
console.error('Erreur embeddings directs Ollama:', e);
|
||||
console.error('Error generating embeddings (Ollama):', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -116,7 +116,7 @@ Note content: "${content}"`;
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: this.modelName,
|
||||
prompt: `${prompt}\n\nRéponds UNIQUEMENT sous forme de tableau JSON : [{"title": "string", "confidence": number}]`,
|
||||
prompt: `${prompt}\n\nRespond ONLY as a JSON array: [{"title": "string", "confidence": number}]`,
|
||||
stream: false,
|
||||
}),
|
||||
});
|
||||
@@ -126,7 +126,7 @@ Note content: "${content}"`;
|
||||
const data = await response.json();
|
||||
const text = data.response;
|
||||
|
||||
// Extraire le JSON de la réponse
|
||||
// Extract JSON from response
|
||||
const jsonMatch = text.match(/\[\s*\{[\s\S]*\}\s*\]/);
|
||||
if (jsonMatch) {
|
||||
return JSON.parse(jsonMatch[0]);
|
||||
@@ -134,7 +134,7 @@ Note content: "${content}"`;
|
||||
|
||||
return [];
|
||||
} catch (e) {
|
||||
console.error('Erreur génération titres Ollama:', e);
|
||||
console.error('Error generating titles (Ollama):', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -156,7 +156,7 @@ Note content: "${content}"`;
|
||||
const data = await response.json();
|
||||
return data.response.trim();
|
||||
} catch (e) {
|
||||
console.error('Erreur génération texte Ollama:', e);
|
||||
console.error('Error generating text (Ollama):', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -187,7 +187,7 @@ Note content: "${content}"`;
|
||||
const data = await response.json();
|
||||
return { text: data.message?.content?.trim() || '' };
|
||||
} catch (e) {
|
||||
console.error('Erreur chat Ollama:', e);
|
||||
console.error('Error in chat (Ollama):', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,17 +24,17 @@ export class OpenAIProvider implements AIProvider {
|
||||
model: this.model,
|
||||
schema: z.object({
|
||||
tags: z.array(z.object({
|
||||
tag: z.string().describe('Le nom du tag, court et en minuscules'),
|
||||
confidence: z.number().min(0).max(1).describe('Le niveau de confiance entre 0 et 1')
|
||||
tag: z.string().describe('Short tag name in lowercase'),
|
||||
confidence: z.number().min(0).max(1).describe('Confidence level between 0 and 1')
|
||||
}))
|
||||
}),
|
||||
prompt: `Analyse la note suivante et suggère entre 1 et 5 tags pertinents.
|
||||
Contenu de la note: "${content}"`,
|
||||
prompt: `Analyze the following note and suggest 1 to 5 relevant tags.
|
||||
Note content: "${content}"`,
|
||||
});
|
||||
|
||||
return object.tags;
|
||||
} catch (e) {
|
||||
console.error('Erreur génération tags OpenAI:', e);
|
||||
console.error('Error generating tags (OpenAI):', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ export class OpenAIProvider implements AIProvider {
|
||||
});
|
||||
return embedding;
|
||||
} catch (e) {
|
||||
console.error('Erreur embeddings OpenAI:', e);
|
||||
console.error('Error generating embeddings (OpenAI):', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -58,8 +58,8 @@ export class OpenAIProvider implements AIProvider {
|
||||
model: this.model,
|
||||
schema: z.object({
|
||||
titles: z.array(z.object({
|
||||
title: z.string().describe('Le titre suggéré'),
|
||||
confidence: z.number().min(0).max(1).describe('Le niveau de confiance entre 0 et 1')
|
||||
title: z.string().describe('Suggested title'),
|
||||
confidence: z.number().min(0).max(1).describe('Confidence level between 0 and 1')
|
||||
}))
|
||||
}),
|
||||
prompt: prompt,
|
||||
@@ -67,7 +67,7 @@ export class OpenAIProvider implements AIProvider {
|
||||
|
||||
return object.titles;
|
||||
} catch (e) {
|
||||
console.error('Erreur génération titres OpenAI:', e);
|
||||
console.error('Error generating titles (OpenAI):', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -81,7 +81,7 @@ export class OpenAIProvider implements AIProvider {
|
||||
|
||||
return text.trim();
|
||||
} catch (e) {
|
||||
console.error('Erreur génération texte OpenAI:', e);
|
||||
console.error('Error generating text (OpenAI):', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -96,7 +96,7 @@ export class OpenAIProvider implements AIProvider {
|
||||
|
||||
return { text: text.trim() };
|
||||
} catch (e) {
|
||||
console.error('Erreur chat OpenAI:', e);
|
||||
console.error('Error in chat (OpenAI):', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,17 +24,17 @@ export class OpenRouterProvider implements AIProvider {
|
||||
model: this.model,
|
||||
schema: z.object({
|
||||
tags: z.array(z.object({
|
||||
tag: z.string().describe('Le nom du tag, court et en minuscules'),
|
||||
confidence: z.number().min(0).max(1).describe('Le niveau de confiance entre 0 et 1')
|
||||
tag: z.string().describe('Short tag name in lowercase'),
|
||||
confidence: z.number().min(0).max(1).describe('Confidence level between 0 and 1')
|
||||
}))
|
||||
}),
|
||||
prompt: `Analyse la note suivante et suggère entre 1 et 5 tags pertinents.
|
||||
Contenu de la note: "${content}"`,
|
||||
prompt: `Analyze the following note and suggest 1 to 5 relevant tags.
|
||||
Note content: "${content}"`,
|
||||
});
|
||||
|
||||
return object.tags;
|
||||
} catch (e) {
|
||||
console.error('Erreur génération tags OpenRouter:', e);
|
||||
console.error('Error generating tags (OpenRouter):', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ export class OpenRouterProvider implements AIProvider {
|
||||
});
|
||||
return embedding;
|
||||
} catch (e) {
|
||||
console.error('Erreur embeddings OpenRouter:', e);
|
||||
console.error('Error generating embeddings (OpenRouter):', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -58,8 +58,8 @@ export class OpenRouterProvider implements AIProvider {
|
||||
model: this.model,
|
||||
schema: z.object({
|
||||
titles: z.array(z.object({
|
||||
title: z.string().describe('Le titre suggéré'),
|
||||
confidence: z.number().min(0).max(1).describe('Le niveau de confiance entre 0 et 1')
|
||||
title: z.string().describe('Suggested title'),
|
||||
confidence: z.number().min(0).max(1).describe('Confidence level between 0 and 1')
|
||||
}))
|
||||
}),
|
||||
prompt: prompt,
|
||||
@@ -67,7 +67,7 @@ export class OpenRouterProvider implements AIProvider {
|
||||
|
||||
return object.titles;
|
||||
} catch (e) {
|
||||
console.error('Erreur génération titres OpenRouter:', e);
|
||||
console.error('Error generating titles (OpenRouter):', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -81,7 +81,7 @@ export class OpenRouterProvider implements AIProvider {
|
||||
|
||||
return text.trim();
|
||||
} catch (e) {
|
||||
console.error('Erreur génération texte OpenRouter:', e);
|
||||
console.error('Error generating text (OpenRouter):', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -96,7 +96,7 @@ export class OpenRouterProvider implements AIProvider {
|
||||
|
||||
return { text: text.trim() };
|
||||
} catch (e) {
|
||||
console.error('Erreur chat OpenRouter:', e);
|
||||
console.error('Error in chat (OpenRouter):', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,32 +29,32 @@ export interface ToolCallResult {
|
||||
|
||||
export interface AIProvider {
|
||||
/**
|
||||
* Analyse le contenu et suggère des tags pertinents.
|
||||
* Analyze content and suggest relevant tags.
|
||||
*/
|
||||
generateTags(content: string, language?: string): Promise<TagSuggestion[]>;
|
||||
|
||||
/**
|
||||
* Génère un vecteur d'embeddings pour la recherche sémantique.
|
||||
* Generate an embedding vector for semantic search.
|
||||
*/
|
||||
getEmbeddings(text: string): Promise<number[]>;
|
||||
|
||||
/**
|
||||
* Génère des suggestions de titres basées sur le contenu.
|
||||
* Generate title suggestions based on content.
|
||||
*/
|
||||
generateTitles(prompt: string): Promise<TitleSuggestion[]>;
|
||||
|
||||
/**
|
||||
* Génère du texte basé sur un prompt.
|
||||
* Generate text based on a prompt.
|
||||
*/
|
||||
generateText(prompt: string): Promise<string>;
|
||||
|
||||
/**
|
||||
* Fournit une réponse de chat (utilisé pour le système agentique)
|
||||
* Provide a chat response (used for the agentic system)
|
||||
*/
|
||||
chat(messages: any[], systemPrompt?: string): Promise<any>;
|
||||
|
||||
/**
|
||||
* Retourne le modèle AI SDK pour le streaming direct (utilisé par l'API route)
|
||||
* Return the AI SDK model for direct streaming (used by API route)
|
||||
*/
|
||||
getModel(): any;
|
||||
|
||||
@@ -69,7 +69,7 @@ export type AIProviderType = 'openai' | 'ollama';
|
||||
export interface AIConfig {
|
||||
provider: AIProviderType;
|
||||
apiKey?: string;
|
||||
baseUrl?: string; // Utile pour Ollama
|
||||
baseUrl?: string; // Used for Ollama
|
||||
model?: string;
|
||||
embeddingModel?: string;
|
||||
}
|
||||
|
||||
@@ -3,8 +3,19 @@ import { prisma } from '../lib/prisma'
|
||||
import bcrypt from 'bcryptjs'
|
||||
|
||||
async function main() {
|
||||
const email = 'test@example.com'
|
||||
const newPassword = 'password123'
|
||||
const email = process.argv[2]
|
||||
const newPassword = process.argv[3]
|
||||
|
||||
if (!email || !newPassword) {
|
||||
console.error('Usage: npx tsx scripts/reset-password-auto.ts <email> <new-password>')
|
||||
console.error('Example: npx tsx scripts/reset-password-auto.ts user@example.com mynewpassword')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (newPassword.length < 6) {
|
||||
console.error('Password must be at least 6 characters')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(`Resetting password for ${email}...`)
|
||||
|
||||
@@ -19,9 +30,9 @@ async function main() {
|
||||
resetTokenExpiry: null
|
||||
},
|
||||
})
|
||||
console.log(`✅ Password successfully reset for ${user.email}`)
|
||||
console.log(`Password successfully reset for ${user.email}`)
|
||||
} catch (error) {
|
||||
console.error('❌ Error resetting password:', error)
|
||||
console.error('Error resetting password:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,12 @@
|
||||
/**
|
||||
* Script DIRECT de reset de mot de passe
|
||||
*
|
||||
* POURQUOI CE SCRIPT ?
|
||||
* -----------------
|
||||
* - Le compte `test@example.com` N'EXISTE PAS (vous avez raison !)
|
||||
* - L'envoi d'email nécessite une configuration SMTP complexe
|
||||
* - VOUS VOULEZ UNE SOLUTION DIRECTE, SANS PERDRE DE TEMPS
|
||||
*
|
||||
* CE QUE FAIT CE SCRIPT :
|
||||
* -------------------
|
||||
* - Réinitialise DIRECTEMENT le mot de passe d'un compte existant
|
||||
* - Sans avoir besoin d'email
|
||||
* - Sans avoir besoin d'interface graphique
|
||||
*
|
||||
* COMMENT UTILISER :
|
||||
* ---------------
|
||||
* 1. Ouvrez un terminal dans le dossier memento-note
|
||||
* 2. Exécutez: node scripts/reset-password.js
|
||||
* 3. Quand demandé, entrez l'email du compte à réinitialiser
|
||||
* 4. Entrez le nouveau mot de passe (2 fois pour confirmation)
|
||||
* 5. FINI ! Connectez-vous avec le nouveau mot de passe
|
||||
* Interactive password reset script
|
||||
*
|
||||
* USAGE:
|
||||
* 1. Open a terminal in the memento-note directory
|
||||
* 2. Run: node scripts/reset-password.js
|
||||
* 3. Enter the email of the account to reset
|
||||
* 4. Enter the new password (twice for confirmation)
|
||||
* 5. Done! Log in with the new password
|
||||
*/
|
||||
|
||||
const readline = require('readline');
|
||||
@@ -31,25 +18,25 @@ const rl = readline.createInterface({
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
console.log('╔══════════════════════════════════════════════════════════════════╗');
|
||||
console.log('║ RESET DE MOT DE PASSE DIRECT ║');
|
||||
console.log('║ ║');
|
||||
console.log('║ ⚠️ ATTENTION : Utilisez seulement pour VOTRE propre compte ! ║');
|
||||
console.log('║ ║');
|
||||
console.log('╚══════════════════════════════════════════════════════════════════════╝');
|
||||
console.log('================================================================');
|
||||
console.log(' INTERACTIVE PASSWORD RESET');
|
||||
console.log('');
|
||||
console.log(' WARNING: Use only for your own account!');
|
||||
console.log('');
|
||||
console.log('================================================================');
|
||||
console.log('');
|
||||
|
||||
rl.question('Entrez l\'EMAIL du compte à réinitialiser : ', async (email) => {
|
||||
rl.question('Enter the EMAIL of the account to reset: ', async (email) => {
|
||||
if (!email || !email.includes('@')) {
|
||||
console.log('❌ Erreur : Email invalide !');
|
||||
console.log('Error: Invalid email!');
|
||||
rl.close();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
email = email.toLowerCase().trim();
|
||||
|
||||
console.log(`🔍 Recherche du compte : ${email}...`);
|
||||
|
||||
|
||||
console.log(`Looking up account: ${email}...`);
|
||||
|
||||
try {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: email }
|
||||
@@ -57,56 +44,55 @@ rl.question('Entrez l\'EMAIL du compte à réinitialiser : ', async (email) => {
|
||||
|
||||
if (!user) {
|
||||
console.log('');
|
||||
console.log('❌ ERREUR : AUCUN compte trouvé avec cet email !');
|
||||
console.log('ERROR: No account found with this email!');
|
||||
console.log('');
|
||||
console.log('📋 COMPTES DISPONIBLES (si existants) :');
|
||||
console.log('─────────────────────────────────────────');
|
||||
|
||||
// Afficher tous les utilisateurs de la base de données
|
||||
console.log('AVAILABLE ACCOUNTS (if any):');
|
||||
console.log('-------------------------');
|
||||
|
||||
const allUsers = await prisma.user.findMany({
|
||||
select: { email: true, name: true, role: true, createdAt: true },
|
||||
orderBy: { createdAt: 'desc' }
|
||||
});
|
||||
|
||||
|
||||
if (allUsers.length > 0) {
|
||||
console.log('');
|
||||
allUsers.forEach((u, index) => {
|
||||
console.log(`${index + 1}. 📧 Email: ${u.email}`);
|
||||
console.log(` 👤 Nom: ${u.name || 'N/A'}`);
|
||||
console.log(` 🏷️ Rôle: ${u.role}`);
|
||||
console.log(` 📅 Créé: ${u.createdAt.toLocaleString('fr-FR')}`);
|
||||
console.log(`${index + 1}. Email: ${u.email}`);
|
||||
console.log(` Name: ${u.name || 'N/A'}`);
|
||||
console.log(` Role: ${u.role}`);
|
||||
console.log(` Created: ${u.createdAt.toLocaleString()}`);
|
||||
console.log('');
|
||||
});
|
||||
} else {
|
||||
console.log(' (Aucun compte dans la base de données)');
|
||||
console.log(' (No accounts in the database)');
|
||||
}
|
||||
|
||||
console.log('─────────────────────────────────────────');
|
||||
|
||||
console.log('-------------------------');
|
||||
console.log('');
|
||||
rl.close();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`✅ Compte trouvé : ${user.email} (${user.name})`);
|
||||
console.log(`Account found: ${user.email} (${user.name})`);
|
||||
console.log('');
|
||||
|
||||
rl.question('Entrez le NOUVEAU mot de passe (minimum 6 caractères) : ', async (newPassword) => {
|
||||
rl.question('Enter the NEW password (minimum 6 characters): ', async (newPassword) => {
|
||||
if (!newPassword || newPassword.length < 6) {
|
||||
console.log('❌ Erreur : Le mot de passe doit avoir au moins 6 caractères !');
|
||||
console.log('Error: Password must be at least 6 characters!');
|
||||
rl.close();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
rl.question('Confirmez le nouveau mot de passe : ', async (confirmPassword) => {
|
||||
rl.question('Confirm the new password: ', async (confirmPassword) => {
|
||||
if (newPassword !== confirmPassword) {
|
||||
console.log('❌ Erreur : Les mots de passe ne correspondent pas !');
|
||||
console.log('Error: Passwords do not match!');
|
||||
rl.close();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log('🔄 Réinitialisation du mot de passe en cours...');
|
||||
|
||||
console.log('Resetting password...');
|
||||
|
||||
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
||||
|
||||
await prisma.user.update({
|
||||
@@ -118,17 +104,15 @@ rl.question('Entrez l\'EMAIL du compte à réinitialiser : ', async (email) => {
|
||||
}
|
||||
});
|
||||
|
||||
console.log('✅ SUCCÈS ! Le mot de passe a été réinitialisé !');
|
||||
console.log('SUCCESS! Password has been reset!');
|
||||
console.log('');
|
||||
console.log('═══════════════════════════════════════════════════════════════════════');
|
||||
console.log('🎉 VOUS POUVEZ MAINTENANT VOUS CONNECTER !');
|
||||
console.log('═════════════════════════════════════════════════════════════════════');
|
||||
console.log('================================================================');
|
||||
console.log('YOU CAN NOW LOG IN!');
|
||||
console.log('================================================================');
|
||||
console.log('');
|
||||
console.log('📱 URL de connexion : http://localhost:3000/login');
|
||||
console.log('📧 Email :', email);
|
||||
console.log('🔑 Mot de passe :', newPassword);
|
||||
console.log('');
|
||||
console.log('⏩ Copiez ces informations et connectez-vous !');
|
||||
console.log('Login URL: http://localhost:3000/login');
|
||||
console.log('Email:', email);
|
||||
console.log('Password:', newPassword);
|
||||
console.log('');
|
||||
|
||||
rl.close();
|
||||
@@ -137,7 +121,7 @@ rl.question('Entrez l\'EMAIL du compte à réinitialiser : ', async (email) => {
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('');
|
||||
console.log('❌ ERREUR lors de la réinitialisation :');
|
||||
console.log('ERROR during password reset:');
|
||||
console.error(error);
|
||||
rl.close();
|
||||
process.exit(1);
|
||||
|
||||
Reference in New Issue
Block a user