feat: 8 AI providers, rich text editor, agent notifications, UI contrast & font settings
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 1m25s

- Add DeepSeek, OpenRouter, Mistral, Z.AI, LM Studio as AI providers
  with editable model names via Combobox in admin settings
- Fix OpenRouter broken by normalizeProvider bug in config.ts
- Convert agent-created notes from Markdown to HTML (TipTap rich text)
- Add Notification model + in-app notifications for agent results
- Agent notification click opens the created note directly
- Add note count display on notebook and inbox headers
- Fix checklist toggle in card view (persist state via localCheckItems)
- Add checklist creation option in tabs/list view (dropdown on + button)
- Fix image description ENOENT error with HTTP fallback
- Improve UI contrast across all themes (input, border, checkbox visibility)
- Add font family setting (Inter vs System Default) in Appearance settings
- Fix CSS font-sans variable conflict (removed dead Geist references)
- Update README with new features and 8 providers

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Sepehr Ramezani
2026-05-01 16:14:07 +02:00
parent 1345403a31
commit dbd49d6fcb
64 changed files with 4124 additions and 1392 deletions

View File

@@ -1,5 +1,6 @@
import { AdminProvidersWrapper } from '@/components/admin-providers-wrapper'
import { detectUserLanguage } from '@/lib/i18n/detect-user-language'
import { headers } from 'next/headers'
import { detectUserLanguage, parseAcceptLanguage } from '@/lib/i18n/detect-user-language'
import { loadTranslations } from '@/lib/i18n/load-translations'
// No <Suspense> here intentionally: combining a Suspense boundary with <Link>
@@ -10,7 +11,9 @@ export default async function AdminGroupLayout({
}: {
children: React.ReactNode
}) {
const initialLanguage = await detectUserLanguage()
const headersList = await headers()
const browserLang = parseAcceptLanguage(headersList.get('accept-language'))
const initialLanguage = await detectUserLanguage(browserLang)
const initialTranslations = await loadTranslations(initialLanguage)
return (

View File

@@ -3,7 +3,8 @@ import { HeaderWrapper } from "@/components/header-wrapper";
import { Sidebar } from "@/components/sidebar";
import { ProvidersWrapper } from "@/components/providers-wrapper";
import { auth } from "@/auth";
import { detectUserLanguage } from "@/lib/i18n/detect-user-language";
import { headers } from "next/headers";
import { detectUserLanguage, parseAcceptLanguage } from "@/lib/i18n/detect-user-language";
import { loadTranslations } from "@/lib/i18n/load-translations";
import { getAISettings } from "@/app/actions/ai-settings";
@@ -14,10 +15,14 @@ export default async function MainLayout({
}: Readonly<{
children: React.ReactNode;
}>) {
// Read browser language hint from Accept-Language header
const headersList = await headers();
const browserLang = parseAcceptLanguage(headersList.get("accept-language"));
// Run auth + language detection + translation loading in parallel
const [session, initialLanguage] = await Promise.all([
auth(),
detectUserLanguage(),
detectUserLanguage(browserLang),
]);
// Load initial translations server-side to prevent hydration mismatch

View File

@@ -12,14 +12,16 @@ interface AppearanceSettingsClientProps {
initialTheme: string
initialNotesViewMode: 'masonry' | 'tabs'
initialCardSizeMode?: 'variable' | 'uniform'
initialFontFamily?: string
}
export function AppearanceSettingsClient({ initialFontSize, initialTheme, initialNotesViewMode, initialCardSizeMode = 'variable' }: AppearanceSettingsClientProps) {
export function AppearanceSettingsClient({ initialFontSize, initialTheme, initialNotesViewMode, initialCardSizeMode = 'variable', initialFontFamily = 'inter' }: AppearanceSettingsClientProps) {
const { t } = useLanguage()
const [theme, setTheme] = useState(initialTheme || 'light')
const [fontSize, setFontSize] = useState(initialFontSize || 'medium')
const [notesViewMode, setNotesViewMode] = useState<'masonry' | 'tabs'>(initialNotesViewMode)
const [cardSizeMode, setCardSizeMode] = useState<'variable' | 'uniform'>(initialCardSizeMode)
const [fontFamily, setFontFamily] = useState(initialFontFamily)
const handleThemeChange = async (value: string) => {
setTheme(value)
@@ -69,6 +71,20 @@ export function AppearanceSettingsClient({ initialFontSize, initialTheme, initia
toast.success(t('settings.settingsSaved') || 'Saved')
}
const handleFontFamilyChange = async (value: string) => {
const font = value === 'system' ? 'system' : 'inter'
setFontFamily(font)
localStorage.setItem('font-family', font)
const root = document.documentElement
if (font === 'system') {
root.classList.add('font-system')
} else {
root.classList.remove('font-system')
}
await updateAISettings({ fontFamily: font })
toast.success(t('settings.settingsSaved') || 'Saved')
}
return (
<div className="space-y-6">
<div>
@@ -116,6 +132,23 @@ export function AppearanceSettingsClient({ initialFontSize, initialTheme, initia
/>
</SettingsSection>
<SettingsSection
title={t('appearance.fontFamilyLabel') || 'Font Family'}
icon={<span className="text-2xl">🔤</span>}
description={t('appearance.fontFamilyDescription') || 'Choose the font used throughout the app'}
>
<SettingSelect
label={t('appearance.fontFamilyLabel') || 'Font Family'}
description={t('appearance.selectFontFamily') || 'Inter is optimized for readability, System uses your OS native font'}
value={fontFamily}
options={[
{ value: 'inter', label: 'Inter' },
{ value: 'system', label: t('appearance.fontSystem') || 'System Default' },
]}
onChange={handleFontFamilyChange}
/>
</SettingsSection>
<SettingsSection
title={t('appearance.notesViewLabel')}
icon={<span className="text-2xl">📋</span>}

View File

@@ -21,6 +21,7 @@ export default async function AppearanceSettingsPage() {
initialTheme={userSettings.theme}
initialNotesViewMode={aiSettings.notesViewMode === 'masonry' ? 'masonry' : 'tabs'}
initialCardSizeMode={userSettings.cardSizeMode}
initialFontFamily={aiSettings.fontFamily || 'inter'}
/>
)
}

View File

@@ -3,7 +3,6 @@
import prisma from '@/lib/prisma'
import { auth } from '@/auth'
import { sendEmail } from '@/lib/mail'
import { updateTag } from 'next/cache'
async function checkAdmin() {
const session = await auth()
@@ -62,9 +61,6 @@ export async function updateSystemConfig(data: Record<string, string>) {
await prisma.$transaction(operations)
// Invalidate cache after update
updateTag('system-config')
return { success: true }
} catch (error) {
console.error('Failed to update settings:', error)

View File

@@ -23,6 +23,7 @@ export type UserAISettingsData = {
autoLabeling?: boolean
noteHistory?: boolean
noteHistoryMode?: 'manual' | 'auto'
fontFamily?: 'inter' | 'system'
}
/** Only fields that exist on `UserAISettings` in Prisma (excludes e.g. `theme`, which lives on `User`). */
@@ -45,6 +46,7 @@ const USER_AI_SETTINGS_PRISMA_KEYS = [
'autoLabeling',
'noteHistory',
'noteHistoryMode',
'fontFamily',
] as const
type UserAISettingsPrismaKey = (typeof USER_AI_SETTINGS_PRISMA_KEYS)[number]
@@ -157,6 +159,7 @@ const getCachedAISettings = unstable_cache(
autoLabeling: true,
noteHistory: false,
noteHistoryMode: 'manual' as const,
fontFamily: 'inter' as const,
}
}
@@ -188,6 +191,7 @@ const getCachedAISettings = unstable_cache(
autoLabeling: settings.autoLabeling ?? true,
noteHistory: settings.noteHistory ?? false,
noteHistoryMode: (settings.noteHistoryMode ?? 'manual') as 'manual' | 'auto',
fontFamily: (settings.fontFamily || 'inter') as 'inter' | 'system',
}
} catch (error) {
console.error('Error getting AI settings:', error)
@@ -212,6 +216,7 @@ const getCachedAISettings = unstable_cache(
autoLabeling: true,
noteHistory: false,
noteHistoryMode: 'manual' as const,
fontFamily: 'inter' as const,
}
}
},
@@ -249,6 +254,7 @@ export async function getAISettings(userId?: string) {
autoLabeling: true,
noteHistory: false,
noteHistoryMode: 'manual' as const,
fontFamily: 'inter' as const,
}
}

View File

@@ -1,6 +1,7 @@
'use server'
import { detectUserLanguage } from '@/lib/i18n/detect-user-language'
import { headers } from 'next/headers'
import { detectUserLanguage, parseAcceptLanguage } from '@/lib/i18n/detect-user-language'
import { SupportedLanguage } from '@/lib/i18n/load-translations'
/**
@@ -8,5 +9,11 @@ import { SupportedLanguage } from '@/lib/i18n/load-translations'
* Called on app load to set initial language
*/
export async function getInitialLanguage(): Promise<SupportedLanguage> {
return await detectUserLanguage()
try {
const headersList = await headers()
const browserLang = parseAcceptLanguage(headersList.get('accept-language'))
return await detectUserLanguage(browserLang)
} catch {
return await detectUserLanguage()
}
}

View File

@@ -0,0 +1,76 @@
'use server'
import { prisma } from '@/lib/prisma'
import { auth } from '@/auth'
export interface AppNotification {
id: string
type: string
title: string
message: string | null
read: boolean
actionUrl: string | null
relatedId: string | null
createdAt: Date
}
export async function getUnreadNotifications(): Promise<AppNotification[]> {
const session = await auth()
if (!session?.user?.id) return []
try {
const notifications = await prisma.notification.findMany({
where: { userId: session.user.id, read: false },
orderBy: { createdAt: 'desc' },
take: 20,
})
return notifications
} catch {
return []
}
}
export async function markNotificationRead(id: string): Promise<void> {
const session = await auth()
if (!session?.user?.id) return
await prisma.notification.updateMany({
where: { id, userId: session.user.id },
data: { read: true },
})
}
export async function markAllNotificationsRead(): Promise<void> {
const session = await auth()
if (!session?.user?.id) return
await prisma.notification.updateMany({
where: { userId: session.user.id, read: false },
data: { read: true },
})
}
/** Create a notification (called from server-side code, not exposed to client) */
export async function createNotification(data: {
userId: string
type: string
title: string
message?: string
actionUrl?: string
relatedId?: string
}): Promise<void> {
try {
await prisma.notification.create({
data: {
userId: data.userId,
type: data.type,
title: data.title,
message: data.message || null,
actionUrl: data.actionUrl || null,
relatedId: data.relatedId || null,
},
})
} catch (e) {
console.error('[Notification] Failed to create:', e)
}
}

View File

@@ -10,13 +10,13 @@ async function requireAdmin() {
/**
* GET /api/admin/models?type=ollama&url=<base_url>
* GET /api/admin/models?type=custom&url=<base_url>&key=<api_key>&kind=tags|embeddings
* GET /api/admin/models?type=deepseek&key=<api_key>&kind=tags|embeddings
* GET /api/admin/models?type=openrouter&key=<api_key>&kind=tags|embeddings
* GET /api/admin/models?type=mistral&key=<api_key>&kind=tags|embeddings
* GET /api/admin/models?type=zai&key=<api_key>&kind=tags|embeddings
* GET /api/admin/models?type=lmstudio&url=<base_url>
*
* Route API (not a Server Action) for fetching AI model lists from Ollama or
* OpenAI-compatible providers. Using a Route Handler instead of a Server Action
* is the correct architecture for client-side GET requests: Server Actions are
* for data mutations, and calling them from useEffect pushes items into the
* App Router's internal action queue, which is drained during render (inside
* AppRouter's useMemo), triggering React Error #310 when multiple calls stack up.
* Route API for fetching AI model lists from providers.
*/
export async function GET(request: NextRequest) {
if (!(await requireAdmin())) {
@@ -29,14 +29,22 @@ export async function GET(request: NextRequest) {
const apiKey = searchParams.get('key') ?? undefined
const kind = searchParams.get('kind') ?? 'tags'
if (!rawUrl) {
return NextResponse.json({ success: false, models: [], error: 'url parameter is required' })
// Provider-specific base URLs (used when url param is empty)
const PROVIDER_URLS: Record<string, string> = {
deepseek: 'https://api.deepseek.com/v1',
openrouter: 'https://openrouter.ai/api/v1',
mistral: 'https://api.mistral.ai/v1',
zai: 'https://api.zukijourney.com/v1',
lmstudio: 'http://localhost:1234/v1',
}
const baseUrl = rawUrl.replace(/\/$/, '').replace(/\/v1$/, '')
try {
// Ollama: uses native /api/tags endpoint
if (type === 'ollama') {
if (!rawUrl) {
return NextResponse.json({ success: false, models: [], error: 'url parameter is required for Ollama' })
}
const baseUrl = rawUrl.replace(/\/$/, '').replace(/\/api$/, '')
const res = await fetch(`${baseUrl}/api/tags`, {
headers: { 'Content-Type': 'application/json' },
signal: AbortSignal.timeout(5000),
@@ -49,58 +57,69 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ success: true, models })
}
if (type === 'custom') {
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`
// All other providers: use OpenAI-compatible /v1/models endpoint
const baseUrl = rawUrl
? rawUrl.replace(/\/$/, '')
: (PROVIDER_URLS[type || ''] || '')
if (kind === 'embeddings') {
// Try provider-specific embeddings endpoint first (e.g. OpenRouter)
try {
const embRes = await fetch(`${baseUrl}/v1/embeddings/models`, {
headers,
signal: AbortSignal.timeout(8000),
})
if (embRes.ok) {
const embData = await embRes.json()
const embModels: string[] = (embData.data ?? [])
.map((m: { id: string }) => m.id)
.filter(Boolean)
.sort()
if (embModels.length > 0) {
return NextResponse.json({ success: true, models: embModels })
}
}
} catch {
// Fall through to /v1/models with keyword filter
}
}
const res = await fetch(`${baseUrl}/v1/models`, {
headers,
signal: AbortSignal.timeout(8000),
})
if (!res.ok) {
return NextResponse.json({ success: false, models: [], error: `Provider ${res.status}` })
}
const data = await res.json()
let models: string[] = (data.data ?? [])
.map((m: { id: string }) => m.id)
.filter(Boolean)
.sort()
if (kind === 'embeddings') {
const keywords = ['embed', 'embedding', 'ada', 'e5', 'bge', 'gte', 'minilm']
const filtered = models.filter((id) =>
keywords.some((kw) => id.toLowerCase().includes(kw))
)
if (filtered.length > 0) models = filtered
}
return NextResponse.json({ success: true, models })
if (!baseUrl) {
return NextResponse.json({ success: false, models: [], error: 'url parameter is required' })
}
return NextResponse.json({ success: false, models: [], error: `Unknown type: ${type}` })
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`
// For OpenRouter, add required headers
if (type === 'openrouter') {
headers['HTTP-Referer'] = 'https://localhost:3000'
headers['X-Title'] = 'Memento AI'
}
// Try provider-specific embeddings endpoint first for embeddings kind
if (kind === 'embeddings') {
try {
const embRes = await fetch(`${baseUrl}/embeddings/models`, {
headers,
signal: AbortSignal.timeout(8000),
})
if (embRes.ok) {
const embData = await embRes.json()
const embModels: string[] = (embData.data ?? [])
.map((m: { id: string }) => m.id)
.filter(Boolean)
.sort()
if (embModels.length > 0) {
return NextResponse.json({ success: true, models: embModels })
}
}
} catch {
// Fall through to /v1/models with keyword filter
}
}
const res = await fetch(`${baseUrl}/models`, {
headers,
signal: AbortSignal.timeout(8000),
})
if (!res.ok) {
return NextResponse.json({ success: false, models: [], error: `Provider ${res.status}` })
}
const data = await res.json()
let models: string[] = (data.data ?? [])
.map((m: { id: string }) => m.id)
.filter(Boolean)
.sort()
if (kind === 'embeddings') {
const keywords = ['embed', 'embedding', 'ada', 'e5', 'bge', 'gte', 'minilm']
const filtered = models.filter((id) =>
keywords.some((kw) => id.toLowerCase().includes(kw))
)
if (filtered.length > 0) models = filtered
}
return NextResponse.json({ success: true, models })
} catch (err: any) {
return NextResponse.json({ success: false, models: [], error: err.message ?? 'Request failed' })
}

View File

@@ -63,8 +63,8 @@
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--font-sans: var(--font-inter);
--font-mono: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Monaco, Consolas, monospace;
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
@@ -117,11 +117,11 @@
--secondary-foreground: #1e293b;
--muted: #f1f5f9;
--muted-foreground: #64748b;
--accent: #f8fafc;
--accent: #f1f5f9;
--accent-foreground: #0284c7;
--destructive: oklch(0.6 0.18 25); /* Rouge */
--border: #e2e8f0; /* Gris-bleu très clair */
--input: #ffffff;
--border: #cbd5e1; /* Gris-bleu visible */
--input: #cbd5e1; /* Bordure visible pour inputs/checkbox */
--ring: #0284c7;
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
@@ -151,11 +151,11 @@
--secondary-foreground: oklch(0.2 0.02 230);
--muted: oklch(0.92 0.005 230);
--muted-foreground: oklch(0.6 0.01 230);
--accent: oklch(0.94 0.005 230);
--accent: oklch(0.92 0.005 230);
--accent-foreground: oklch(0.2 0.02 230);
--destructive: oklch(0.6 0.18 25); /* Rouge */
--border: oklch(0.9 0.008 230); /* Gris-bleu très clair */
--input: oklch(0.98 0.003 230);
--border: oklch(0.85 0.008 230); /* Gris-bleu visible */
--input: oklch(0.85 0.008 230); /* Bordure visible */
--ring: oklch(0.7 0.005 230);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.2 0.02 230);
@@ -165,7 +165,7 @@
--sidebar-primary-foreground: oklch(0.99 0 0);
--sidebar-accent: oklch(0.94 0.005 230);
--sidebar-accent-foreground: oklch(0.2 0.02 230);
--sidebar-border: oklch(0.9 0.008 230);
--sidebar-border: oklch(0.85 0.008 230);
--sidebar-ring: oklch(0.7 0.005 230);
}
@@ -182,11 +182,11 @@
--secondary-foreground: oklch(0.97 0.003 230);
--muted: oklch(0.22 0.006 230);
--muted-foreground: oklch(0.55 0.01 230);
--accent: oklch(0.24 0.006 230);
--accent: oklch(0.26 0.008 230);
--accent-foreground: oklch(0.97 0.003 230);
--destructive: oklch(0.65 0.18 25);
--border: oklch(0.28 0.01 230);
--input: oklch(0.2 0.006 230);
--border: oklch(0.33 0.01 230);
--input: oklch(0.33 0.01 230);
--ring: oklch(0.6 0.01 230);
--popover: oklch(0.18 0.006 230);
--popover-foreground: oklch(0.97 0.003 230);
@@ -213,11 +213,11 @@
--secondary-foreground: oklch(0.97 0.003 230);
--muted: oklch(0.22 0.006 230);
--muted-foreground: oklch(0.55 0.01 230);
--accent: oklch(0.24 0.006 230);
--accent: oklch(0.26 0.008 230);
--accent-foreground: oklch(0.97 0.003 230);
--destructive: oklch(0.65 0.18 25);
--border: oklch(0.28 0.01 230);
--input: oklch(0.2 0.006 230);
--border: oklch(0.33 0.01 230);
--input: oklch(0.33 0.01 230);
--ring: oklch(0.6 0.01 230);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
@@ -248,8 +248,8 @@
--accent: oklch(0.25 0.015 250);
--accent-foreground: oklch(0.18 0.03 250);
--destructive: oklch(0.6 0.22 25);
--border: oklch(0.85 0.015 250);
--input: oklch(0.25 0.01 250);
--border: oklch(0.82 0.015 250);
--input: oklch(0.82 0.015 250);
--ring: oklch(0.65 0.015 250);
--popover: oklch(0.97 0.006 250);
--popover-foreground: oklch(0.18 0.03 250);
@@ -274,11 +274,11 @@
--secondary-foreground: oklch(0.96 0.005 250);
--muted: oklch(0.2 0.015 250);
--muted-foreground: oklch(0.5 0.02 250);
--accent: oklch(0.22 0.02 250);
--accent: oklch(0.26 0.02 250);
--accent-foreground: oklch(0.96 0.005 250);
--destructive: oklch(0.65 0.2 25);
--border: oklch(0.3 0.02 250);
--input: oklch(0.22 0.02 250);
--border: oklch(0.33 0.02 250);
--input: oklch(0.33 0.02 250);
--ring: oklch(0.55 0.02 250);
--popover: oklch(0.15 0.015 250);
--popover-foreground: oklch(0.96 0.005 250);
@@ -306,8 +306,8 @@
--accent: oklch(0.93 0.01 225);
--accent-foreground: oklch(0.18 0.035 225);
--destructive: oklch(0.6 0.2 25);
--border: oklch(0.87 0.012 225);
--input: oklch(0.95 0.01 225);
--border: oklch(0.83 0.012 225);
--input: oklch(0.83 0.012 225);
--ring: oklch(0.65 0.015 225);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.18 0.035 225);
@@ -332,11 +332,11 @@
--secondary-foreground: oklch(0.97 0.006 225);
--muted: oklch(0.25 0.02 225);
--muted-foreground: oklch(0.52 0.018 225);
--accent: oklch(0.25 0.025 225);
--accent: oklch(0.28 0.025 225);
--accent-foreground: oklch(0.97 0.006 225);
--destructive: oklch(0.65 0.22 25);
--border: oklch(0.32 0.018 225);
--input: oklch(0.25 0.02 225);
--border: oklch(0.35 0.018 225);
--input: oklch(0.35 0.018 225);
--ring: oklch(0.55 0.02 225);
--popover: oklch(0.17 0.01 225);
--popover-foreground: oklch(0.97 0.006 225);
@@ -364,8 +364,8 @@
--accent: oklch(0.93 0.01 45);
--accent-foreground: oklch(0.2 0.015 45);
--destructive: oklch(0.6 0.2 25);
--border: oklch(0.88 0.012 45);
--input: oklch(0.97 0.008 45);
--border: oklch(0.83 0.012 45);
--input: oklch(0.83 0.012 45);
--ring: oklch(0.68 0.01 45);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.2 0.015 45);
@@ -390,11 +390,11 @@
--secondary-foreground: oklch(0.97 0.005 45);
--muted: oklch(0.23 0.02 45);
--muted-foreground: oklch(0.55 0.012 45);
--accent: oklch(0.27 0.018 45);
--accent: oklch(0.29 0.018 45);
--accent-foreground: oklch(0.97 0.005 45);
--destructive: oklch(0.65 0.2 25);
--border: oklch(0.3 0.018 45);
--input: oklch(0.27 0.02 45);
--border: oklch(0.33 0.018 45);
--input: oklch(0.33 0.018 45);
--ring: oklch(0.58 0.02 45);
--popover: oklch(0.19 0.01 45);
--popover-foreground: oklch(0.97 0.005 45);
@@ -408,6 +408,16 @@
--sidebar-ring: oklch(0.58 0.02 45);
}
/* System font mode — override Inter with native OS fonts.
Must be outside @layer base to win over next/font's generated class. */
html.font-system {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif !important;
}
html.font-system body,
html.font-system * {
font-family: inherit !important;
}
@layer base {
* {
@apply border-border outline-ring/50;

View File

@@ -89,7 +89,7 @@ export default async function RootLayout({
<SessionProviderWrapper>
<ErrorReporter />
<DirectionInitializer />
<ThemeInitializer theme={userSettings.theme} fontSize={aiSettings.fontSize} />
<ThemeInitializer theme={userSettings.theme} fontSize={aiSettings.fontSize} fontFamily={aiSettings.fontFamily} />
{children}
<Toaster />
</SessionProviderWrapper>