fix: make paragraph refactor service use configured AI provider

The paragraph-refactor service was using OLLAMA_BASE_URL directly from
environment variables instead of using the configured AI provider from
the database. This caused "OLLAMA error" even when OpenAI was configured
in the admin interface.

Changes:
- paragraph-refactor.service.ts: Now uses getSystemConfig() and
  getTagsProvider() from factory instead of direct Ollama calls
- factory.ts: Added proper error messages when API keys are missing
- .env.docker.example: Updated with new provider configuration
  variables (AI_PROVIDER_TAGS, AI_PROVIDER_EMBEDDING)

This fixes the issue where AI reformulation features (Clarify, Shorten,
Improve Style) would fail with OLLAMA errors even when OpenAI was
properly configured in the admin settings.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
sepehr 2026-01-12 22:51:24 +01:00
parent 58e486c68e
commit 5d315a6bdd
10 changed files with 3025 additions and 3072 deletions

View File

@ -14,19 +14,37 @@ NEXTAUTH_URL="http://YOUR_SERVER_IP:3000"
# ============================================
# AI Provider Configuration
# ============================================
# You can configure separate providers for tags and embeddings
# Options: ollama, openai, custom
# For local Ollama in Docker (service name):
AI_PROVIDER=ollama
OLLAMA_BASE_URL="http://ollama:11434"
OLLAMA_MODEL="granite4:latest"
# AI_PROVIDER_TAGS=ollama
# AI_PROVIDER_EMBEDDING=ollama
# OLLAMA_BASE_URL="http://ollama:11434"
# AI_MODEL_TAGS="granite4:latest"
# AI_MODEL_EMBEDDING="embeddinggemma:latest"
# For external Ollama (on host or different server):
# AI_PROVIDER=ollama
# AI_PROVIDER_TAGS=ollama
# AI_PROVIDER_EMBEDDING=ollama
# OLLAMA_BASE_URL="http://YOUR_SERVER_IP:11434"
# OLLAMA_MODEL="granite4:latest"
# AI_MODEL_TAGS="granite4:latest"
# AI_MODEL_EMBEDDING="embeddinggemma:latest"
# For OpenAI:
# AI_PROVIDER=openai
# For OpenAI (recommended for production):
# AI_PROVIDER_TAGS=openai
# AI_PROVIDER_EMBEDDING=openai
# OPENAI_API_KEY="sk-..."
# AI_MODEL_TAGS="gpt-4o-mini"
# AI_MODEL_EMBEDDING="text-embedding-3-small"
# Mixed setup (e.g., Ollama for tags, OpenAI for 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"
# ============================================
# Email Configuration (Optional)
@ -37,8 +55,3 @@ OLLAMA_MODEL="granite4:latest"
# SMTP_USER="your-email@gmail.com"
# SMTP_PASS="your-app-password"
# SMTP_FROM="noreply@memento.app"
# ============================================
# OpenAI (Optional - for GPT models)
# ============================================
# OPENAI_API_KEY="sk-..."

View File

@ -29,6 +29,7 @@ function createOpenAIProvider(config: Record<string, string>, modelName: string,
const apiKey = config?.OPENAI_API_KEY || process.env.OPENAI_API_KEY || '';
if (!apiKey) {
throw new Error('OPENAI_API_KEY is required when using OpenAI provider');
}
return new OpenAIProvider(apiKey, modelName, embeddingModelName);
@ -39,9 +40,11 @@ function createCustomOpenAIProvider(config: Record<string, string>, modelName: s
const baseUrl = config?.CUSTOM_OPENAI_BASE_URL || process.env.CUSTOM_OPENAI_BASE_URL || '';
if (!apiKey) {
throw new Error('CUSTOM_OPENAI_API_KEY is required when using Custom OpenAI provider');
}
if (!baseUrl) {
throw new Error('CUSTOM_OPENAI_BASE_URL is required when using Custom OpenAI provider');
}
return new CustomOpenAIProvider(apiKey, baseUrl, modelName, embeddingModelName);

View File

@ -7,6 +7,8 @@
*/
import { LanguageDetectionService } from './language-detection.service'
import { getTagsProvider } from '../factory'
import { getSystemConfig } from '@/lib/config'
export type RefactorMode = 'clarify' | 'shorten' | 'improveStyle'
@ -83,37 +85,13 @@ export class ParagraphRefactorService {
const systemPrompt = this.getSystemPrompt(mode)
const userPrompt = this.getUserPrompt(mode, content, language)
// Get AI provider response using fetch
let baseUrl = process.env.OLLAMA_BASE_URL
// Get AI provider from factory
const config = await getSystemConfig()
const provider = getTagsProvider(config)
if (!baseUrl) {
throw new Error('OLLAMA_BASE_URL environment variable is required')
}
// Remove /api suffix if present to avoid double /api/api/...
if (baseUrl.endsWith('/api')) {
baseUrl = baseUrl.slice(0, -4)
}
const modelName = process.env.OLLAMA_MODEL || 'granite4:latest'
const response = await fetch(`${baseUrl}/api/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: modelName,
system: systemPrompt,
prompt: userPrompt,
stream: false,
}),
})
if (!response.ok) {
throw new Error(`Provider error: ${response.statusText}`)
}
const data = await response.json()
const refactored = this.extractRefactoredText(data.response)
// Use provider's generateText method
const fullPrompt = `${systemPrompt}\n\n${userPrompt}`
const refactored = await provider.generateText(fullPrompt)
// Calculate word count change
const refactoredWordCount = refactored.split(/\s+/).length
@ -189,38 +167,16 @@ ${content}
Original language: ${language}
IMPORTANT: Provide all 3 versions in ${language}. No English, no explanations.`
// Get AI provider response using fetch
let baseUrl = process.env.OLLAMA_BASE_URL
// Get AI provider from factory
const config = await getSystemConfig()
const provider = getTagsProvider(config)
if (!baseUrl) {
throw new Error('OLLAMA_BASE_URL environment variable is required')
}
// Remove /api suffix if present to avoid double /api/api/...
if (baseUrl.endsWith('/api')) {
baseUrl = baseUrl.slice(0, -4)
}
const modelName = process.env.OLLAMA_MODEL || 'granite4:latest'
const response = await fetch(`${baseUrl}/api/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: modelName,
system: systemPrompt,
prompt: userPrompt,
stream: false,
}),
})
if (!response.ok) {
throw new Error(`Provider error: ${response.statusText}`)
}
const data = await response.json()
// Use provider's generateText method
const fullPrompt = `${systemPrompt}\n\n${userPrompt}`
const response = await provider.generateText(fullPrompt)
// Parse JSON response
const jsonResponse = JSON.parse(data.response)
const jsonResponse = JSON.parse(response)
const modes: RefactorMode[] = ['clarify', 'shorten', 'improveStyle']
const results: RefactorResult[] = []

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
{
"name": "prisma-client-62aaff0ff1302a5b1470021854f34bb9a9c1219fe39a7ee39aa626bd83e22eae",
"name": "prisma-client-46efe72656f1c393bbd99fdd6d2d34037b30f693f014757faf08aec1d9319858",
"main": "index.js",
"types": "index.d.ts",
"browser": "index-browser.js",

View File

@ -1,6 +1,3 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
output = "./client-generated"
@ -13,31 +10,28 @@ datasource db {
}
model User {
id String @id @default(cuid())
name String?
email String @unique
emailVerified DateTime?
password String? // Hashed password
role String @default("USER") // "USER" or "ADMIN"
image String?
theme String @default("light")
resetToken String? @unique
resetTokenExpiry DateTime?
accounts Account[]
sessions Session[]
notes Note[]
labels Label[]
notebooks Notebook[] // NEW: Relation to notebooks
receivedShares NoteShare[] @relation("ReceivedShares")
sentShares NoteShare[] @relation("SentShares")
// Phase 1 AI Relations
id String @id @default(cuid())
name String?
email String @unique
emailVerified DateTime?
password String?
role String @default("USER")
image String?
theme String @default("light")
resetToken String? @unique
resetTokenExpiry DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
accounts Account[]
aiFeedback AiFeedback[]
aiSettings UserAISettings?
labels Label[]
memoryEchoInsights MemoryEchoInsight[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
notes Note[]
sentShares NoteShare[] @relation("SentShares")
receivedShares NoteShare[] @relation("ReceivedShares")
notebooks Notebook[]
sessions Session[]
aiSettings UserAISettings?
}
model Account {
@ -52,11 +46,9 @@ model Account {
scope String?
id_token String?
session_state String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@id([provider, providerAccountId])
}
@ -65,10 +57,9 @@ model Session {
sessionToken String @unique
userId String
expires DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model VerificationToken {
@ -79,19 +70,18 @@ model VerificationToken {
@@id([identifier, token])
}
// NEW: Notebook model for organizing notes
model Notebook {
id String @id @default(cuid())
name String
icon String? // Emoji or icon name
color String? // Hex color for personalization
order Int // Manual order for drag & drop
icon String?
color String?
order Int
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
notes Note[] // Notes can belong to a notebook
labels Label[] // Labels are contextual to this notebook
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
labels Label[]
notes Note[]
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId, order])
@@index([userId])
@ -101,89 +91,80 @@ model Label {
id String @id @default(cuid())
name String
color String @default("gray")
notebookId String? // TEMPORARY: Optional for migration, will be required later
notebook Notebook? @relation(fields: [notebookId], references: [id], onDelete: Cascade)
notes Note[] // NEW: Many-to-many relation with notes
userId String? // DEPRECATED: Kept for migration, will be removed after migration
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
notebookId String?
userId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
notebook Notebook? @relation(fields: [notebookId], references: [id], onDelete: Cascade)
notes Note[] @relation("LabelToNote")
@@unique([notebookId, name]) // NEW: Labels are unique within a notebook (ignored if notebookId is null)
@@unique([notebookId, name])
@@index([notebookId])
@@index([userId]) // DEPRECATED: Keep for now, remove after migration
@@index([userId])
}
model Note {
id String @id @default(cuid())
id String @id @default(cuid())
title String?
content String
color String @default("default")
isPinned Boolean @default(false)
isArchived Boolean @default(false)
type String @default("text") // "text" or "checklist"
checkItems String? // For checklist items stored as JSON string
labels String? // Array of label names stored as JSON string (DEPRECATED)
images String? // Array of image URLs stored as JSON string
links String? // Array of link metadata stored as JSON string
reminder DateTime? // Reminder date and time
isReminderDone Boolean @default(false)
reminderRecurrence String? // "none", "daily", "weekly", "monthly", "custom"
reminderLocation String? // Location for location-based reminders
isMarkdown Boolean @default(false) // Whether content uses Markdown
size String @default("small") // "small", "medium", "large"
embedding String? // Vector embeddings stored as JSON string for semantic search
sharedWith String? // Array of user IDs (collaborators) stored as JSON string
userId String? // Owner of the note
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
shares NoteShare[] // All share records for this note
order Int @default(0)
// NEW: Notebook relation (optional - null = "Notes générales" / Inbox)
notebookId String? // NULL = note is in general notes
notebook Notebook? @relation(fields: [notebookId], references: [id], onDelete: SetNull)
// NEW: Many-to-many relation with labels
labelRelations Label[] // Uses implicit _NoteToLabel junction table
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Phase 1 AI Extensions
autoGenerated Boolean? // True if title/content was AI-generated
aiProvider String? // 'openai' | 'ollama'
aiConfidence Int? // 0-100 (collected Phase 1, UI Phase 3)
language String? // ISO 639-1: 'fr', 'en', 'es', 'de', 'fa'
languageConfidence Float? // 0.0-1.0
lastAiAnalysis DateTime? // Timestamp of last AI analysis
// Relations for Phase 1 AI
aiFeedback AiFeedback[]
memoryEchoAsNote1 MemoryEchoInsight[] @relation("EchoNote1")
memoryEchoAsNote2 MemoryEchoInsight[] @relation("EchoNote2")
color String @default("default")
isPinned Boolean @default(false)
isArchived Boolean @default(false)
type String @default("text")
checkItems String?
labels String?
images String?
links String?
reminder DateTime?
isReminderDone Boolean @default(false)
reminderRecurrence String?
reminderLocation String?
isMarkdown Boolean @default(false)
size String @default("small")
embedding String?
sharedWith String?
userId String?
order Int @default(0)
notebookId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
autoGenerated Boolean?
aiProvider String?
aiConfidence Int?
language String?
languageConfidence Float?
lastAiAnalysis DateTime?
aiFeedback AiFeedback[]
memoryEchoAsNote2 MemoryEchoInsight[] @relation("EchoNote2")
memoryEchoAsNote1 MemoryEchoInsight[] @relation("EchoNote1")
notebook Notebook? @relation(fields: [notebookId], references: [id])
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
shares NoteShare[]
labelRelations Label[] @relation("LabelToNote")
@@index([isPinned])
@@index([isArchived])
@@index([order])
@@index([reminder])
@@index([userId])
@@index([userId, notebookId]) // NEW: For filtering notes by notebook
@@index([userId, notebookId])
}
model NoteShare {
id String @id @default(cuid())
noteId String
note Note @relation(fields: [noteId], references: [id], onDelete: Cascade)
userId String
user User @relation("ReceivedShares", fields: [userId], references: [id], onDelete: Cascade)
sharedBy String // User ID who shared the note
sharer User @relation("SentShares", fields: [sharedBy], references: [id], onDelete: Cascade)
status String @default("pending") // "pending", "accepted", "declined", "removed"
permission String @default("view") // "view", "comment", "edit"
sharedBy String
status String @default("pending")
permission String @default("view")
notifiedAt DateTime?
respondedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
sharer User @relation("SentShares", fields: [sharedBy], references: [id], onDelete: Cascade)
user User @relation("ReceivedShares", fields: [userId], references: [id], onDelete: Cascade)
note Note @relation(fields: [noteId], references: [id], onDelete: Cascade)
@@unique([noteId, userId])
@@index([userId])
@ -196,21 +177,18 @@ model SystemConfig {
value String
}
// Phase 1 MVP AI Models
model AiFeedback {
id String @id @default(cuid())
noteId String
userId String?
feedbackType String // 'thumbs_up' | 'thumbs_down' | 'correction'
feature String // 'title_suggestion' | 'memory_echo' | 'semantic_search' | 'paragraph_refactor'
originalContent String // JSON string of AI-generated content
correctedContent String? // User's modified version
metadata String? // JSON string for additional data (provider, model, timestamp)
feedbackType String
feature String
originalContent String
correctedContent String?
metadata String?
createdAt DateTime @default(now())
note Note @relation(fields: [noteId], references: [id], onDelete: Cascade)
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
note Note @relation(fields: [noteId], references: [id], onDelete: Cascade)
@@index([noteId])
@@index([userId])
@ -223,41 +201,33 @@ model MemoryEchoInsight {
note1Id String
note2Id String
similarityScore Float
insight String // AI-generated explanation of the connection
insight String
insightDate DateTime @default(now())
viewed Boolean @default(false)
feedback String? // 'thumbs_up' | 'thumbs_down'
dismissed Boolean @default(false) // User dismissed this connection
note1 Note @relation("EchoNote1", fields: [note1Id], references: [id], onDelete: Cascade)
note2 Note @relation("EchoNote2", fields: [note2Id], references: [id], onDelete: Cascade)
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
feedback String?
dismissed Boolean @default(false)
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
note2 Note @relation("EchoNote2", fields: [note2Id], references: [id], onDelete: Cascade)
note1 Note @relation("EchoNote1", fields: [note1Id], references: [id], onDelete: Cascade)
@@unique([userId, insightDate])
@@index([userId, insightDate])
@@index([userId, dismissed]) // For filtering dismissed connections
@@index([userId, dismissed])
}
model UserAISettings {
userId String @id
userId String @id
titleSuggestions Boolean @default(true)
semanticSearch Boolean @default(true)
paragraphRefactor Boolean @default(true)
memoryEcho Boolean @default(true)
memoryEchoFrequency String @default("daily")
aiProvider String @default("auto")
preferredLanguage String @default("auto")
fontSize String @default("medium")
demoMode Boolean @default(false)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
// Feature Flags (granular ON/OFF)
titleSuggestions Boolean @default(true)
semanticSearch Boolean @default(true)
paragraphRefactor Boolean @default(true)
memoryEcho Boolean @default(true)
// Configuration
memoryEchoFrequency String @default("daily") // 'daily' | 'weekly' | 'custom'
aiProvider String @default("auto") // 'auto' | 'openai' | 'ollama'
preferredLanguage String @default("auto") // 'auto' | 'en' | 'fr' | 'es' | 'de' | 'fa' | 'it' | 'pt' | 'ru' | 'zh' | 'ja' | 'ko' | 'ar' | 'hi' | 'nl' | 'pl'
fontSize String @default("medium") // 'small' | 'medium' | 'large' | 'extra-large'
demoMode Boolean @default(false) // Demo mode for testing Memory Echo
// Relation
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
// Indexes for analytics
@@index([memoryEcho])
@@index([aiProvider])
@@index([memoryEchoFrequency])

View File

@ -1,9 +1,6 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
output = "./client-generated"
provider = "prisma-client-js"
output = "./client-generated"
binaryTargets = ["debian-openssl-1.1.x", "native"]
}
@ -13,31 +10,28 @@ datasource db {
}
model User {
id String @id @default(cuid())
name String?
email String @unique
emailVerified DateTime?
password String? // Hashed password
role String @default("USER") // "USER" or "ADMIN"
image String?
theme String @default("light")
resetToken String? @unique
resetTokenExpiry DateTime?
accounts Account[]
sessions Session[]
notes Note[]
labels Label[]
notebooks Notebook[] // NEW: Relation to notebooks
receivedShares NoteShare[] @relation("ReceivedShares")
sentShares NoteShare[] @relation("SentShares")
// Phase 1 AI Relations
aiFeedback AiFeedback[]
aiSettings UserAISettings?
id String @id @default(cuid())
name String?
email String @unique
emailVerified DateTime?
password String?
role String @default("USER")
image String?
theme String @default("light")
resetToken String? @unique
resetTokenExpiry DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
accounts Account[]
aiFeedback AiFeedback[]
labels Label[]
memoryEchoInsights MemoryEchoInsight[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
notes Note[]
sentShares NoteShare[] @relation("SentShares")
receivedShares NoteShare[] @relation("ReceivedShares")
notebooks Notebook[]
sessions Session[]
aiSettings UserAISettings?
}
model Account {
@ -52,11 +46,9 @@ model Account {
scope String?
id_token String?
session_state String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@id([provider, providerAccountId])
}
@ -65,10 +57,9 @@ model Session {
sessionToken String @unique
userId String
expires DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model VerificationToken {
@ -79,111 +70,101 @@ model VerificationToken {
@@id([identifier, token])
}
// NEW: Notebook model for organizing notes
model Notebook {
id String @id @default(cuid())
name String
icon String? // Emoji or icon name
color String? // Hex color for personalization
order Int // Manual order for drag & drop
icon String?
color String?
order Int
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
notes Note[] // Notes can belong to a notebook
labels Label[] // Labels are contextual to this notebook
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
labels Label[]
notes Note[]
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId, order])
@@index([userId])
}
model Label {
id String @id @default(cuid())
name String
color String @default("gray")
notebookId String? // TEMPORARY: Optional for migration, will be required later
notebook Notebook? @relation(fields: [notebookId], references: [id], onDelete: Cascade)
notes Note[] // NEW: Many-to-many relation with notes
userId String? // DEPRECATED: Kept for migration, will be removed after migration
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id String @id @default(cuid())
name String
color String @default("gray")
notebookId String?
userId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
notebook Notebook? @relation(fields: [notebookId], references: [id], onDelete: Cascade)
notes Note[] @relation("LabelToNote")
@@unique([notebookId, name]) // NEW: Labels are unique within a notebook (ignored if notebookId is null)
@@unique([notebookId, name])
@@index([notebookId])
@@index([userId]) // DEPRECATED: Keep for now, remove after migration
@@index([userId])
}
model Note {
id String @id @default(cuid())
title String?
content String
color String @default("default")
isPinned Boolean @default(false)
isArchived Boolean @default(false)
type String @default("text") // "text" or "checklist"
checkItems String? // For checklist items stored as JSON string
labels String? // Array of label names stored as JSON string (DEPRECATED)
images String? // Array of image URLs stored as JSON string
links String? // Array of link metadata stored as JSON string
reminder DateTime? // Reminder date and time
isReminderDone Boolean @default(false)
reminderRecurrence String? // "none", "daily", "weekly", "monthly", "custom"
reminderLocation String? // Location for location-based reminders
isMarkdown Boolean @default(false) // Whether content uses Markdown
size String @default("small") // "small", "medium", "large"
embedding String? // Vector embeddings stored as JSON string for semantic search
sharedWith String? // Array of user IDs (collaborators) stored as JSON string
userId String? // Owner of the note
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
shares NoteShare[] // All share records for this note
order Int @default(0)
// NEW: Notebook relation (optional - null = "Notes générales" / Inbox)
notebookId String? // NULL = note is in general notes
notebook Notebook? @relation(fields: [notebookId], references: [id], onDelete: SetNull)
// NEW: Many-to-many relation with labels
labelRelations Label[] // Uses implicit _NoteToLabel junction table
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Phase 1 AI Extensions
autoGenerated Boolean? // True if title/content was AI-generated
aiProvider String? // 'openai' | 'ollama'
aiConfidence Int? // 0-100 (collected Phase 1, UI Phase 3)
language String? // ISO 639-1: 'fr', 'en', 'es', 'de', 'fa'
languageConfidence Float? // 0.0-1.0
lastAiAnalysis DateTime? // Timestamp of last AI analysis
// Relations for Phase 1 AI
aiFeedback AiFeedback[]
memoryEchoAsNote1 MemoryEchoInsight[] @relation("EchoNote1")
memoryEchoAsNote2 MemoryEchoInsight[] @relation("EchoNote2")
id String @id @default(cuid())
title String?
content String
color String @default("default")
isPinned Boolean @default(false)
isArchived Boolean @default(false)
type String @default("text")
checkItems String?
labels String?
images String?
links String?
reminder DateTime?
isReminderDone Boolean @default(false)
reminderRecurrence String?
reminderLocation String?
isMarkdown Boolean @default(false)
size String @default("small")
embedding String?
sharedWith String?
userId String?
order Int @default(0)
notebookId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
autoGenerated Boolean?
aiProvider String?
aiConfidence Int?
language String?
languageConfidence Float?
lastAiAnalysis DateTime?
aiFeedback AiFeedback[]
memoryEchoAsNote2 MemoryEchoInsight[] @relation("EchoNote2")
memoryEchoAsNote1 MemoryEchoInsight[] @relation("EchoNote1")
notebook Notebook? @relation(fields: [notebookId], references: [id])
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
shares NoteShare[]
labelRelations Label[] @relation("LabelToNote")
@@index([isPinned])
@@index([isArchived])
@@index([order])
@@index([reminder])
@@index([userId])
@@index([userId, notebookId]) // NEW: For filtering notes by notebook
@@index([userId, notebookId])
}
model NoteShare {
id String @id @default(cuid())
noteId String
note Note @relation(fields: [noteId], references: [id], onDelete: Cascade)
userId String
user User @relation("ReceivedShares", fields: [userId], references: [id], onDelete: Cascade)
sharedBy String // User ID who shared the note
sharer User @relation("SentShares", fields: [sharedBy], references: [id], onDelete: Cascade)
status String @default("pending") // "pending", "accepted", "declined", "removed"
permission String @default("view") // "view", "comment", "edit"
sharedBy String
status String @default("pending")
permission String @default("view")
notifiedAt DateTime?
respondedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
sharer User @relation("SentShares", fields: [sharedBy], references: [id], onDelete: Cascade)
user User @relation("ReceivedShares", fields: [userId], references: [id], onDelete: Cascade)
note Note @relation(fields: [noteId], references: [id], onDelete: Cascade)
@@unique([noteId, userId])
@@index([userId])
@ -196,21 +177,18 @@ model SystemConfig {
value String
}
// Phase 1 MVP AI Models
model AiFeedback {
id String @id @default(cuid())
noteId String
userId String?
feedbackType String // 'thumbs_up' | 'thumbs_down' | 'correction'
feature String // 'title_suggestion' | 'memory_echo' | 'semantic_search' | 'paragraph_refactor'
originalContent String // JSON string of AI-generated content
correctedContent String? // User's modified version
metadata String? // JSON string for additional data (provider, model, timestamp)
createdAt DateTime @default(now())
note Note @relation(fields: [noteId], references: [id], onDelete: Cascade)
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
id String @id @default(cuid())
noteId String
userId String?
feedbackType String
feature String
originalContent String
correctedContent String?
metadata String?
createdAt DateTime @default(now())
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
note Note @relation(fields: [noteId], references: [id], onDelete: Cascade)
@@index([noteId])
@@index([userId])
@ -218,46 +196,38 @@ model AiFeedback {
}
model MemoryEchoInsight {
id String @id @default(cuid())
id String @id @default(cuid())
userId String?
note1Id String
note2Id String
similarityScore Float
insight String // AI-generated explanation of the connection
insightDate DateTime @default(now())
viewed Boolean @default(false)
feedback String? // 'thumbs_up' | 'thumbs_down'
dismissed Boolean @default(false) // User dismissed this connection
note1 Note @relation("EchoNote1", fields: [note1Id], references: [id], onDelete: Cascade)
note2 Note @relation("EchoNote2", fields: [note2Id], references: [id], onDelete: Cascade)
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
insight String
insightDate DateTime @default(now())
viewed Boolean @default(false)
feedback String?
dismissed Boolean @default(false)
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
note2 Note @relation("EchoNote2", fields: [note2Id], references: [id], onDelete: Cascade)
note1 Note @relation("EchoNote1", fields: [note1Id], references: [id], onDelete: Cascade)
@@unique([userId, insightDate])
@@index([userId, insightDate])
@@index([userId, dismissed]) // For filtering dismissed connections
@@index([userId, dismissed])
}
model UserAISettings {
userId String @id
userId String @id
titleSuggestions Boolean @default(true)
semanticSearch Boolean @default(true)
paragraphRefactor Boolean @default(true)
memoryEcho Boolean @default(true)
memoryEchoFrequency String @default("daily")
aiProvider String @default("auto")
preferredLanguage String @default("auto")
fontSize String @default("medium")
demoMode Boolean @default(false)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
// Feature Flags (granular ON/OFF)
titleSuggestions Boolean @default(true)
semanticSearch Boolean @default(true)
paragraphRefactor Boolean @default(true)
memoryEcho Boolean @default(true)
// Configuration
memoryEchoFrequency String @default("daily") // 'daily' | 'weekly' | 'custom'
aiProvider String @default("auto") // 'auto' | 'openai' | 'ollama'
preferredLanguage String @default("auto") // 'auto' | 'en' | 'fr' | 'es' | 'de' | 'fa' | 'it' | 'pt' | 'ru' | 'zh' | 'ja' | 'ko' | 'ar' | 'hi' | 'nl' | 'pl'
fontSize String @default("medium") // 'small' | 'medium' | 'large' | 'extra-large'
demoMode Boolean @default(false) // Demo mode for testing Memory Echo
// Relation
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
// Indexes for analytics
@@index([memoryEcho])
@@index([aiProvider])
@@index([memoryEchoFrequency])

View File

@ -0,0 +1,41 @@
import prisma from '../lib/prisma'
async function debugConfig() {
console.log('=== System Configuration Debug ===\n')
const configs = await prisma.systemConfig.findMany()
console.log(`Total configs in DB: ${configs.length}\n`)
// Group by category
const aiConfigs = configs.filter(c => c.key.startsWith('AI_'))
const ollamaConfigs = configs.filter(c => c.key.includes('OLLAMA'))
const openaiConfigs = configs.filter(c => c.key.includes('OPENAI'))
console.log('=== AI Provider Configs ===')
aiConfigs.forEach(c => {
console.log(`${c.key}: "${c.value}"`)
})
console.log('\n=== Ollama Configs ===')
ollamaConfigs.forEach(c => {
console.log(`${c.key}: "${c.value}"`)
})
console.log('\n=== OpenAI Configs ===')
openaiConfigs.forEach(c => {
console.log(`${c.key}: "${c.value}"`)
})
console.log('\n=== All Configs ===')
configs.forEach(c => {
console.log(`${c.key}: "${c.value}"`)
})
}
debugConfig()
.then(() => process.exit(0))
.catch((err) => {
console.error(err)
process.exit(1)
})