Files
Momento/Momento-main/momento/memento-note/app/actions/ai-settings.ts
Sepehr Ramezani ed807d3b2a
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 5s
Add safe database migration workflow and note history infrastructure.
This introduces guarded migrations with automatic backups, fixes note creation after DB reset, and wires snapshot/restore history across notes surfaces.
2026-04-28 17:14:26 +02:00

281 lines
8.4 KiB
TypeScript

'use server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
import { revalidatePath, updateTag } from 'next/cache'
export type UserAISettingsData = {
titleSuggestions?: boolean
semanticSearch?: boolean
paragraphRefactor?: boolean
memoryEcho?: boolean
memoryEchoFrequency?: 'daily' | 'weekly' | 'custom'
aiProvider?: 'auto' | 'openai' | 'ollama'
preferredLanguage?: 'auto' | 'en' | 'fr' | 'es' | 'de' | 'fa' | 'it' | 'pt' | 'ru' | 'zh' | 'ja' | 'ko' | 'ar' | 'hi' | 'nl' | 'pl'
demoMode?: boolean
showRecentNotes?: boolean
notesViewMode?: 'masonry' | 'tabs' | 'list'
emailNotifications?: boolean
desktopNotifications?: boolean
anonymousAnalytics?: boolean
fontSize?: 'small' | 'medium' | 'large'
languageDetection?: boolean
autoLabeling?: boolean
noteHistory?: boolean
}
/** Only fields that exist on `UserAISettings` in Prisma (excludes e.g. `theme`, which lives on `User`). */
const USER_AI_SETTINGS_PRISMA_KEYS = [
'titleSuggestions',
'semanticSearch',
'paragraphRefactor',
'memoryEcho',
'memoryEchoFrequency',
'aiProvider',
'preferredLanguage',
'fontSize',
'demoMode',
'showRecentNotes',
'notesViewMode',
'emailNotifications',
'desktopNotifications',
'anonymousAnalytics',
'languageDetection',
'autoLabeling',
'noteHistory',
] as const
type UserAISettingsPrismaKey = (typeof USER_AI_SETTINGS_PRISMA_KEYS)[number]
function pickUserAISettingsForDb(input: UserAISettingsData): Partial<Record<UserAISettingsPrismaKey, unknown>> {
const out: Partial<Record<UserAISettingsPrismaKey, unknown>> = {}
for (const key of USER_AI_SETTINGS_PRISMA_KEYS) {
const v = input[key]
if (v !== undefined) {
out[key] = v
}
}
if (out.notesViewMode === 'list') {
out.notesViewMode = 'tabs'
}
if (
out.notesViewMode != null &&
out.notesViewMode !== 'masonry' &&
out.notesViewMode !== 'tabs'
) {
delete out.notesViewMode
}
return out
}
/**
* Update AI settings for the current user
*/
export async function updateAISettings(settings: UserAISettingsData) {
const session = await auth()
if (!session?.user?.id) {
console.error('[updateAISettings] Unauthorized: No session or user ID')
throw new Error('Unauthorized')
}
try {
const data = pickUserAISettingsForDb(settings)
if (Object.keys(data).length === 0) {
return { success: true }
}
// Valeurs scalaires uniquement (pickUserAISettingsForDb) — cast pour éviter UpdateOperations vs create.
const payload = data as Record<string, string | boolean | undefined>
// Upsert settings (create if not exists, update if exists)
await prisma.userAISettings.upsert({
where: { userId: session.user.id },
create: {
userId: session.user.id,
...payload,
},
update: payload,
})
revalidatePath('/settings/ai', 'page')
revalidatePath('/settings/appearance', 'page')
revalidatePath('/', 'layout')
updateTag('ai-settings')
return { success: true }
} catch (error) {
console.error('Error updating AI settings:', error)
const raw = error instanceof Error ? error.message : String(error)
const isSchema =
/no such column|notesViewMode|Unknown column|does not exist/i.test(raw) ||
(typeof raw === 'string' && raw.includes('UserAISettings') && raw.includes('column'))
if (isSchema) {
throw new Error(
'Schéma base de données obsolète : colonne notesViewMode manquante. Dans le dossier memento-note, exécutez : npx prisma db push (ou appliquez les migrations Prisma).'
)
}
throw new Error('Failed to update AI settings')
}
}
/**
* Get AI settings for the current user (Cached)
*/
import { unstable_cache } from 'next/cache'
// Internal cached function to fetch settings from DB
const getCachedAISettings = unstable_cache(
async (userId: string) => {
try {
const settings = await prisma.userAISettings.findUnique({
where: { userId }
})
if (!settings) {
return {
titleSuggestions: true,
semanticSearch: true,
paragraphRefactor: true,
memoryEcho: true,
memoryEchoFrequency: 'daily' as const,
aiProvider: 'auto' as const,
preferredLanguage: 'auto' as const,
demoMode: false,
showRecentNotes: false,
notesViewMode: 'masonry' as const,
emailNotifications: false,
desktopNotifications: false,
anonymousAnalytics: false,
theme: 'light' as const,
fontSize: 'medium' as const,
languageDetection: true,
autoLabeling: true,
noteHistory: false,
}
}
const raw = settings.notesViewMode
const viewMode =
raw === 'masonry'
? ('masonry' as const)
: raw === 'list' || raw === 'tabs'
? ('tabs' as const)
: ('masonry' as const)
return {
titleSuggestions: settings.titleSuggestions,
semanticSearch: settings.semanticSearch,
paragraphRefactor: settings.paragraphRefactor,
memoryEcho: settings.memoryEcho,
memoryEchoFrequency: (settings.memoryEchoFrequency || 'daily') as 'daily' | 'weekly' | 'custom',
aiProvider: (settings.aiProvider || 'auto') as 'auto' | 'openai' | 'ollama',
preferredLanguage: (settings.preferredLanguage || 'auto') as 'auto' | 'en' | 'fr' | 'es' | 'de' | 'fa' | 'it' | 'pt' | 'ru' | 'zh' | 'ja' | 'ko' | 'ar' | 'hi' | 'nl' | 'pl',
demoMode: settings.demoMode,
showRecentNotes: settings.showRecentNotes,
notesViewMode: viewMode,
emailNotifications: settings.emailNotifications,
desktopNotifications: settings.desktopNotifications,
anonymousAnalytics: settings.anonymousAnalytics,
// theme: 'light' as const, // REMOVED: Should not be handled here or hardcoded
fontSize: (settings.fontSize || 'medium') as 'small' | 'medium' | 'large',
languageDetection: settings.languageDetection ?? true,
autoLabeling: settings.autoLabeling ?? true,
noteHistory: settings.noteHistory ?? false,
}
} catch (error) {
console.error('Error getting AI settings:', error)
// Return defaults on error
return {
titleSuggestions: true,
semanticSearch: true,
paragraphRefactor: true,
memoryEcho: true,
memoryEchoFrequency: 'daily' as const,
aiProvider: 'auto' as const,
preferredLanguage: 'auto' as const,
demoMode: false,
showRecentNotes: false,
notesViewMode: 'masonry' as const,
emailNotifications: false,
desktopNotifications: false,
anonymousAnalytics: false,
theme: 'light' as const,
fontSize: 'medium' as const,
languageDetection: true,
autoLabeling: true,
noteHistory: false,
}
}
},
['user-ai-settings'],
{ tags: ['ai-settings'] }
)
export async function getAISettings(userId?: string) {
let id = userId
if (!id) {
const session = await auth()
id = session?.user?.id
}
// Return defaults for non-logged-in users
if (!id) {
return {
titleSuggestions: true,
semanticSearch: true,
paragraphRefactor: true,
memoryEcho: true,
memoryEchoFrequency: 'daily' as const,
aiProvider: 'auto' as const,
preferredLanguage: 'auto' as const,
demoMode: false,
showRecentNotes: false,
notesViewMode: 'masonry' as const,
emailNotifications: false,
desktopNotifications: false,
anonymousAnalytics: false,
theme: 'light' as const,
fontSize: 'medium' as const,
languageDetection: true,
autoLabeling: true,
noteHistory: false,
}
}
return getCachedAISettings(id)
}
/**
* Get user's preferred AI provider
*/
export async function getUserAIPreference(): Promise<'auto' | 'openai' | 'ollama'> {
const settings = await getAISettings()
return settings.aiProvider
}
/**
* Check if a specific AI feature is enabled for the user
*/
export async function isAIFeatureEnabled(feature: keyof UserAISettingsData): Promise<boolean> {
const settings = await getAISettings()
switch (feature) {
case 'titleSuggestions':
return settings.titleSuggestions
case 'semanticSearch':
return settings.semanticSearch
case 'paragraphRefactor':
return settings.paragraphRefactor
case 'memoryEcho':
return settings.memoryEcho
case 'noteHistory':
return settings.noteHistory
default:
return true
}
}