diff --git a/app/api/settings/route.ts b/app/api/settings/route.ts index a0c7883..cbf727f 100644 --- a/app/api/settings/route.ts +++ b/app/api/settings/route.ts @@ -1,6 +1,6 @@ import { auth } from '@/auth'; import { query } from '@/lib/db'; -import { DEFAULT_FONT_ID, isFontId, type FontId } from '@/lib/fonts'; +import { DEFAULT_FONT_ID, getStoredFontFamily, parseStoredFontFamily } from '@/lib/fonts'; import { logRouteOutcome, type LogFields } from '@/lib/log'; import { getRequestId, jsonResponse, PRIVATE_NO_STORE, readContentLength } from '@/lib/request'; import { LIMITS, validateSettingsRequest } from '@/lib/validation'; @@ -17,7 +17,7 @@ type SettingsPatch = { systemPrompt: string | null; saveHistory: boolean | null; girlMode: boolean | null; - font: FontId | null; + fontFamily: string | null; keyJwk: string | null; }; @@ -80,7 +80,7 @@ async function upsertSettingsRow(email: string, patch: SettingsPatch): Promise<{ key_jwk = COALESCE($4, user_settings.key_jwk), girl_mode = COALESCE($5, user_settings.girl_mode), font_family = COALESCE($6, user_settings.font_family)`, - [email, patch.systemPrompt, patch.saveHistory, patch.keyJwk, patch.girlMode, patch.font], + [email, patch.systemPrompt, patch.saveHistory, patch.keyJwk, patch.girlMode, patch.fontFamily], ); return { legacySchema: false, hasFontColumn: true }; } catch (error) { @@ -152,8 +152,7 @@ export async function GET(req: Request) { ctx.user = session.user.email; const { row, legacySchema, hasFontColumn } = await getSettingsRow(session.user.email); - const savedFont = row?.font_family ?? null; - const font = savedFont && isFontId(savedFont) ? savedFont : DEFAULT_FONT_ID; + const { font, customFontFamily } = parseStoredFontFamily(row?.font_family ?? null); ctx.status = 200; ctx.hasKey = !!row?.key_jwk; ctx.saveHistory = row?.save_history ?? false; @@ -166,6 +165,7 @@ export async function GET(req: Request) { systemPrompt: row?.system_prompt ?? '', saveHistory: row?.save_history ?? false, font, + customFontFamily, ...(legacySchema ? {} : { girlMode: row?.girl_mode ?? false }), keyJwk: row?.key_jwk ?? null, }, {}, { requestId, cacheControl: PRIVATE_NO_STORE }); @@ -242,7 +242,9 @@ export async function PUT(req: Request) { systemPrompt: hasOwn(input, 'systemPrompt') ? parsed.value.systemPrompt : null, saveHistory: hasOwn(input, 'saveHistory') ? parsed.value.saveHistory : null, girlMode: hasOwn(input, 'girlMode') ? parsed.value.girlMode : null, - font: hasOwn(input, 'font') ? parsed.value.font : null, + fontFamily: hasOwn(input, 'font') || hasOwn(input, 'customFontFamily') + ? getStoredFontFamily(parsed.value.font, parsed.value.customFontFamily) + : null, keyJwk: hasOwn(input, 'keyJwk') ? parsed.value.keyJwk : null, }; @@ -250,13 +252,13 @@ export async function PUT(req: Request) { ctx.keyJwkChars = patch.keyJwk === null ? ctx.keyJwkChars : patch.keyJwk.length; ctx.saveHistory = patch.saveHistory; ctx.girlMode = patch.girlMode; - ctx.font = patch.font; + ctx.font = patch.fontFamily; ctx.hasKey = patch.keyJwk === null ? null : patch.keyJwk.length > 0; const { legacySchema, hasFontColumn } = await upsertSettingsRow(session.user.email, patch); ctx.status = 204; ctx.girlMode = legacySchema ? null : patch.girlMode; - ctx.font = hasFontColumn ? patch.font : null; + ctx.font = hasFontColumn ? patch.fontFamily : null; ctx.hasFontColumn = hasFontColumn; ctx.legacySchema = legacySchema; return new Response(null, { diff --git a/app/layout.tsx b/app/layout.tsx index 740c804..ec81eb0 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,8 +1,15 @@ import type { Metadata, Viewport } from 'next'; +import { IBM_Plex_Mono, JetBrains_Mono } from 'next/font/google'; import './globals.css'; import 'highlight.js/styles/atom-one-dark.min.css'; +import { CUSTOM_FONT_ID, DEFAULT_FONT_FAMILY, FONTS } from '@/lib/fonts'; const GIRL_MODE_STORAGE_KEY = 'gippidy-girl-mode'; +const FONT_STORAGE_KEY = 'gippidy-font'; +const CUSTOM_FONT_STORAGE_KEY = 'gippidy-custom-font-family'; +const jetbrainsMono = JetBrains_Mono({ subsets: ['latin'], variable: '--font-jetbrains-mono', display: 'swap' }); +const ibmPlexMono = IBM_Plex_Mono({ subsets: ['latin'], weight: ['400', '500', '700'], variable: '--font-ibm-plex-mono', display: 'swap' }); +const FONT_BOOTSTRAP_MAP = Object.fromEntries(FONTS.map(font => [font.id, font.family])); const GIRL_MODE_BOOTSTRAP = ` try { if (localStorage.getItem('${GIRL_MODE_STORAGE_KEY}') === '1') { @@ -10,6 +17,21 @@ try { } } catch {} `; +const FONT_BOOTSTRAP = ` +try { + const fontMap = ${JSON.stringify(FONT_BOOTSTRAP_MAP)}; + const defaultFont = ${JSON.stringify(DEFAULT_FONT_FAMILY)}; + const font = localStorage.getItem('${FONT_STORAGE_KEY}'); + const custom = (localStorage.getItem('${CUSTOM_FONT_STORAGE_KEY}') || '') + .replace(/[{};]/g, ' ') + .replace(/\\s+/g, ' ') + .trim(); + const value = font === '${CUSTOM_FONT_ID}' + ? (custom ? custom + ', monospace' : defaultFont) + : (font && fontMap[font]) || defaultFont; + document.documentElement.style.setProperty('--app-font-family', value); +} catch {} +`; export const metadata: Metadata = { metadataBase: new URL(process.env.NEXT_PUBLIC_BASE_URL ?? 'https://www.gippidy.chat'), @@ -25,9 +47,9 @@ export const viewport: Viewport = { export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - +
- + {children} diff --git a/app/page.tsx b/app/page.tsx index 4fef68c..da9b604 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -5,7 +5,7 @@ import { signOut } from 'next-auth/react'; import RenderedMarkdown from './rendered-markdown'; import { renderMarkdown } from '@/lib/markdown'; import { getOrCreateKey, encrypt, decrypt } from '@/lib/crypto'; -import { DEFAULT_FONT_ID, FONTS, getFontFamily, isFontId, type FontId } from '@/lib/fonts'; +import { CUSTOM_FONT_ID, DEFAULT_FONT_ID, FONTS, getFontFamily, isFontId, normalizeCustomFontFamily, type FontId } from '@/lib/fonts'; import { DEFAULT_MODEL_ID, getModelLabel, MODELS, normalizeModelId } from '@/lib/models'; import { splitMessageFollowups, type Role, type Image, type Pdf, type Message } from '@/lib/chat'; import { LIMITS } from '@/lib/validation'; @@ -16,6 +16,7 @@ type PendingPdf = Pdf; const MODEL_KEY = 'gippidy-model'; const KEY_WARNED = 'gippidy-key-warned'; const FONT_KEY = 'gippidy-font'; +const CUSTOM_FONT_FAMILY_KEY = 'gippidy-custom-font-family'; const GIRL_MODE_KEY = 'gippidy-girl-mode'; const ACTIVE_HISTORY_CHAT_KEY = 'gippidy-active-history-chat'; const HISTORY_PREVIEW_CACHE_KEY = 'gippidy-history-preview-cache'; @@ -52,7 +53,14 @@ type HistoryRestoreResult = | { kind: 'ok'; item: HistoryItem } | { kind: 'missing' } | { kind: 'error' }; -type SettingsPatch = { systemPrompt?: string; saveHistory?: boolean; girlMode?: boolean; font?: FontId; keyJwk?: string | null }; +type SettingsPatch = { + systemPrompt?: string; + saveHistory?: boolean; + girlMode?: boolean; + font?: FontId; + customFontFamily?: string; + keyJwk?: string | null; +}; type PendingSettings = Omit