From 73c7d3efed6f309646fc3863b8aece8b23eed84e Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Wed, 14 Jan 2026 21:04:58 -0700 Subject: [PATCH] Add gemini key ability --- authentik/blueprints/kaboot-setup.yaml | 13 +- components/ApiKeyModal.tsx | 136 +++++++ components/AuthButton.tsx | 70 +++- components/Landing.tsx | 472 ++++++++++++++----------- components/PreferencesModal.tsx | 130 +++++++ hooks/useUserPreferences.ts | 94 +++++ server/src/db/connection.ts | 12 + server/src/db/schema.sql | 4 +- server/src/middleware/auth.ts | 2 + server/src/routes/users.ts | 60 +++- types.ts | 28 ++ 11 files changed, 793 insertions(+), 228 deletions(-) create mode 100644 components/ApiKeyModal.tsx create mode 100644 components/PreferencesModal.tsx create mode 100644 hooks/useUserPreferences.ts diff --git a/authentik/blueprints/kaboot-setup.yaml b/authentik/blueprints/kaboot-setup.yaml index 8aee45f..2c6eaf2 100644 --- a/authentik/blueprints/kaboot-setup.yaml +++ b/authentik/blueprints/kaboot-setup.yaml @@ -36,6 +36,13 @@ entries: attrs: name: kaboot-users + - id: kaboot-ai-access-group + model: authentik_core.group + identifiers: + name: kaboot-ai-access + attrs: + name: kaboot-ai-access + # ═══════════════════════════════════════════════════════════════════════════════ # OAUTH2/OIDC PROVIDER # ═══════════════════════════════════════════════════════════════════════════════ @@ -234,8 +241,7 @@ entries: is_active: true groups: - !KeyOf kaboot-users-group - # Note: Password must be set manually via UI or API after blueprint import - # Run: docker compose exec authentik-server ak setpassword kaboottest + - !KeyOf kaboot-ai-access-group # ═══════════════════════════════════════════════════════════════════════════════ # SERVICE ACCOUNT (for API/automated testing) @@ -253,5 +259,4 @@ entries: is_active: true groups: - !KeyOf kaboot-users-group - # Note: App password must be created via UI or API after blueprint import - # See docs/AUTHENTIK_SETUP.md for instructions + - !KeyOf kaboot-ai-access-group diff --git a/components/ApiKeyModal.tsx b/components/ApiKeyModal.tsx new file mode 100644 index 0000000..28cf06b --- /dev/null +++ b/components/ApiKeyModal.tsx @@ -0,0 +1,136 @@ +import React, { useState, useEffect } from 'react'; +import { motion } from 'framer-motion'; +import { X, Key, Eye, EyeOff, Loader2 } from 'lucide-react'; +import { useBodyScrollLock } from '../hooks/useBodyScrollLock'; + +interface ApiKeyModalProps { + isOpen: boolean; + onClose: () => void; + apiKey: string | undefined; + onSave: (key: string | undefined) => Promise; + saving: boolean; + hasAIAccess: boolean; +} + +export const ApiKeyModal: React.FC = ({ + isOpen, + onClose, + apiKey, + onSave, + saving, + hasAIAccess, +}) => { + useBodyScrollLock(isOpen); + const [localApiKey, setLocalApiKey] = useState(apiKey || ''); + const [showApiKey, setShowApiKey] = useState(false); + + useEffect(() => { + if (isOpen) { + setLocalApiKey(apiKey || ''); + } + }, [isOpen, apiKey]); + + if (!isOpen) return null; + + const handleSave = async () => { + await onSave(localApiKey || undefined); + onClose(); + }; + + return ( + + e.stopPropagation()} + > +
+
+
+ +
+
+

Account Settings

+

Manage your API access

+
+
+ +
+ +
+ {hasAIAccess ? ( +
+

+ + Custom Gemini API Key +

+

+ Use your own API key for quiz generation. Leave empty to use the system key. +

+
+ setLocalApiKey(e.target.value)} + placeholder="AIza..." + className="w-full px-4 py-3 pr-12 rounded-xl border-2 border-gray-200 focus:border-theme-primary focus:outline-none text-gray-800" + /> + +
+
+ ) : ( +
+ +

API access not available

+

Contact an administrator for access

+
+ )} +
+ +
+ + {hasAIAccess && ( + + )} +
+
+
+ ); +}; diff --git a/components/AuthButton.tsx b/components/AuthButton.tsx index 6af93a8..39e1473 100644 --- a/components/AuthButton.tsx +++ b/components/AuthButton.tsx @@ -1,9 +1,31 @@ -import React from 'react'; +import React, { useState, useRef, useEffect } from 'react'; import { useAuth } from 'react-oidc-context'; -import { LogIn, LogOut, User, Loader2 } from 'lucide-react'; +import { LogIn, LogOut, User, Loader2, Key, ChevronDown } from 'lucide-react'; -export const AuthButton: React.FC = () => { +interface AuthButtonProps { + onAccountSettingsClick?: () => void; +} + +export const AuthButton: React.FC = ({ onAccountSettingsClick }) => { const auth = useAuth(); + const [dropdownOpen, setDropdownOpen] = useState(false); + const dropdownRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setDropdownOpen(false); + } + }; + + if (dropdownOpen) { + document.addEventListener('mousedown', handleClickOutside); + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [dropdownOpen]); if (auth.isLoading) { return ( @@ -24,20 +46,44 @@ export const AuthButton: React.FC = () => { if (auth.isAuthenticated) { return ( -
-
+
+
- + + {dropdownOpen && ( +
+ {onAccountSettingsClick && ( + + )} + +
+ )}
); } diff --git a/components/Landing.tsx b/components/Landing.tsx index 80628cf..f8c52d2 100644 --- a/components/Landing.tsx +++ b/components/Landing.tsx @@ -1,12 +1,15 @@ import React, { useState, useEffect } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; -import { BrainCircuit, Loader2, Play, PenTool, BookOpen, Upload, X, FileText, Image, ScanText, Sparkles, Settings } from 'lucide-react'; +import { BrainCircuit, Loader2, Play, PenTool, BookOpen, Upload, X, FileText, Image, ScanText, Sparkles, Settings, Palette, Lock } from 'lucide-react'; import { useAuth } from 'react-oidc-context'; import { AuthButton } from './AuthButton'; import { QuizLibrary } from './QuizLibrary'; import { DefaultConfigModal } from './DefaultConfigModal'; +import { PreferencesModal } from './PreferencesModal'; +import { ApiKeyModal } from './ApiKeyModal'; import { useQuizLibrary } from '../hooks/useQuizLibrary'; import { useUserConfig } from '../hooks/useUserConfig'; +import { useUserPreferences } from '../hooks/useUserPreferences'; import type { Quiz, GameConfig } from '../types'; type GenerateMode = 'topic' | 'document'; @@ -22,7 +25,7 @@ interface LandingProps { export const Landing: React.FC = ({ onGenerate, onCreateManual, onLoadQuiz, onJoin, isLoading, error }) => { const auth = useAuth(); - const [mode, setMode] = useState<'HOST' | 'JOIN'>('HOST'); + const [mode, setMode] = useState<'HOST' | 'JOIN'>('JOIN'); const [generateMode, setGenerateMode] = useState('topic'); const [topic, setTopic] = useState(''); const [pin, setPin] = useState(''); @@ -35,12 +38,16 @@ export const Landing: React.FC = ({ onGenerate, onCreateManual, on const [useOcr, setUseOcr] = useState(false); const [defaultConfigOpen, setDefaultConfigOpen] = useState(false); const [editingDefaultConfig, setEditingDefaultConfig] = useState(null); + const [preferencesOpen, setPreferencesOpen] = useState(false); + const [accountSettingsOpen, setAccountSettingsOpen] = useState(false); const hasImageFile = selectedFiles.some(f => f.type.startsWith('image/')); const hasDocumentFile = selectedFiles.some(f => !f.type.startsWith('image/') && !['application/pdf', 'text/plain', 'text/markdown', 'text/csv', 'text/html'].includes(f.type)); const showOcrOption = hasImageFile || hasDocumentFile; const { defaultConfig, saving: savingConfig, saveDefaultConfig } = useUserConfig(); + const { preferences, hasAIAccess, saving: savingPrefs, savePreferences, applyColorScheme } = useUserPreferences(); + const canUseAI = auth.isAuthenticated && (hasAIAccess || preferences.geminiApiKey); const { quizzes, @@ -104,7 +111,8 @@ export const Landing: React.FC = ({ onGenerate, onCreateManual, on setUseOcr(false); }; - const canGenerate = generateMode === 'topic' ? topic.trim() : selectedFiles.length > 0; + const canGenerateInput = generateMode === 'topic' ? topic.trim() : selectedFiles.length > 0; + const canGenerate = canUseAI && canGenerateInput; const handleHostSubmit = (e: React.FormEvent) => { e.preventDefault(); @@ -140,18 +148,27 @@ export const Landing: React.FC = ({ onGenerate, onCreateManual, on
{auth.isAuthenticated && ( - + <> + + + )} - + setAccountSettingsOpen(true)} />
= ({ onGenerate, onCreateManual, on {mode === 'HOST' ? (
-
-
- -
- - - {generateMode === 'topic' ? ( - - setTopic(e.target.value)} - className="w-full p-4 text-xl font-bold border-2 border-gray-200 rounded-2xl focus:border-theme-primary focus:ring-4 focus:ring-theme-primary/20 outline-none transition-all placeholder:font-medium text-center" - disabled={isLoading} - /> - - ) : ( - -
document.getElementById('file-upload')?.click()} - className={`border-2 border-dashed rounded-2xl p-4 text-center cursor-pointer transition-all ${ - isDragging - ? 'border-theme-primary bg-theme-primary/5 scale-[1.02]' - : selectedFiles.length > 0 - ? 'border-theme-primary/50 bg-theme-primary/5' - : 'border-gray-300 hover:border-gray-400 hover:bg-gray-50' - }`} - > - - -
- -

- {isDragging ? 'Drop files here' : 'Drop files or click to browse'} -

-

PDF, DOCX, PPTX, XLSX, TXT, Images

-
-
- - {selectedFiles.length > 0 && ( -
-
- {selectedFiles.length} file{selectedFiles.length > 1 ? 's' : ''} selected - -
-
- {selectedFiles.map((file, index) => ( -
-
-
- {file.type.startsWith('image/') ? ( - - ) : ( - - )} -
- {file.name} -
- -
- ))} -
-
- )} - - {showOcrOption && ( - - )} -
- )} -
- -
-
- Questions - {questionCount} -
- setQuestionCount(Number(e.target.value))} - className="w-full accent-theme-primary h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer" - /> -
- - + +
+ + + {generateMode === 'topic' ? ( + + setTopic(e.target.value)} + className="w-full p-4 text-xl font-bold border-2 border-gray-200 rounded-2xl focus:border-theme-primary focus:ring-4 focus:ring-theme-primary/20 outline-none transition-all placeholder:font-medium text-center" + disabled={isLoading} + /> + + ) : ( + +
document.getElementById('file-upload')?.click()} + className={`border-2 border-dashed rounded-2xl p-4 text-center cursor-pointer transition-all ${ + isDragging + ? 'border-theme-primary bg-theme-primary/5 scale-[1.02]' + : selectedFiles.length > 0 + ? 'border-theme-primary/50 bg-theme-primary/5' + : 'border-gray-300 hover:border-gray-400 hover:bg-gray-50' + }`} + > + + +
+ +

+ {isDragging ? 'Drop files here' : 'Drop files or click to browse'} +

+

PDF, DOCX, PPTX, XLSX, TXT, Images

+
+
+ + {selectedFiles.length > 0 && ( +
+
+ {selectedFiles.length} file{selectedFiles.length > 1 ? 's' : ''} selected + +
+
+ {selectedFiles.map((file, index) => ( +
+
+
+ {file.type.startsWith('image/') ? ( + + ) : ( + + )} +
+ {file.name} +
+ +
+ ))} +
+
+ )} + + {showOcrOption && ( + + )} +
+ )} +
+ +
+
+ Questions + {questionCount} +
+ setQuestionCount(Number(e.target.value))} + className="w-full accent-theme-primary h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer" + /> +
+ + + + +
+
+ OR +
+
)} - - - -
-
- OR -
-
- + - {auth.isAuthenticated && ( - + + )}
) : ( @@ -452,6 +486,26 @@ export const Landing: React.FC = ({ onGenerate, onCreateManual, on }} saving={savingConfig} /> + + setPreferencesOpen(false)} + preferences={preferences} + onSave={savePreferences} + onPreview={applyColorScheme} + saving={savingPrefs} + /> + + setAccountSettingsOpen(false)} + apiKey={preferences.geminiApiKey} + onSave={async (key) => { + await savePreferences({ ...preferences, geminiApiKey: key }); + }} + saving={savingPrefs} + hasAIAccess={hasAIAccess} + />
); }; diff --git a/components/PreferencesModal.tsx b/components/PreferencesModal.tsx new file mode 100644 index 0000000..871e6e1 --- /dev/null +++ b/components/PreferencesModal.tsx @@ -0,0 +1,130 @@ +import React, { useState } from 'react'; +import { motion } from 'framer-motion'; +import { X, Palette, Loader2, Check } from 'lucide-react'; +import { useBodyScrollLock } from '../hooks/useBodyScrollLock'; +import type { UserPreferences } from '../types'; +import { COLOR_SCHEMES } from '../types'; + +interface PreferencesModalProps { + isOpen: boolean; + onClose: () => void; + preferences: UserPreferences; + onSave: (prefs: UserPreferences) => Promise; + onPreview: (schemeId: string) => void; + saving: boolean; +} + +export const PreferencesModal: React.FC = ({ + isOpen, + onClose, + preferences, + onSave, + onPreview, + saving, +}) => { + useBodyScrollLock(isOpen); + const [localPrefs, setLocalPrefs] = useState(preferences); + + if (!isOpen) return null; + + const handleColorSelect = (schemeId: string) => { + setLocalPrefs(prev => ({ ...prev, colorScheme: schemeId })); + onPreview(schemeId); + }; + + const handleSave = async () => { + await onSave(localPrefs); + onClose(); + }; + + const handleClose = () => { + onPreview(preferences.colorScheme); + onClose(); + }; + + return ( + + e.stopPropagation()} + > +
+
+
+ +
+
+

Color Scheme

+

Customize your theme

+
+
+ +
+ +
+
+ {COLOR_SCHEMES.map((scheme) => ( + + ))} +
+

+ {COLOR_SCHEMES.find(s => s.id === localPrefs.colorScheme)?.name || 'Select a color'} +

+
+ +
+ + +
+
+
+ ); +}; diff --git a/hooks/useUserPreferences.ts b/hooks/useUserPreferences.ts new file mode 100644 index 0000000..af188b6 --- /dev/null +++ b/hooks/useUserPreferences.ts @@ -0,0 +1,94 @@ +import { useState, useCallback, useEffect } from 'react'; +import toast from 'react-hot-toast'; +import { useAuthenticatedFetch } from './useAuthenticatedFetch'; +import type { UserPreferences } from '../types'; +import { COLOR_SCHEMES } from '../types'; + +const DEFAULT_PREFERENCES: UserPreferences = { + colorScheme: 'blue', +}; + +export const applyColorScheme = (schemeId: string) => { + const scheme = COLOR_SCHEMES.find(s => s.id === schemeId) || COLOR_SCHEMES[0]; + document.documentElement.style.setProperty('--theme-primary', scheme.primary); + document.documentElement.style.setProperty('--theme-primary-dark', scheme.primaryDark); + document.documentElement.style.setProperty('--theme-primary-darker', scheme.primaryDarker); +}; + +interface UseUserPreferencesReturn { + preferences: UserPreferences; + hasAIAccess: boolean; + loading: boolean; + saving: boolean; + fetchPreferences: () => Promise; + savePreferences: (prefs: UserPreferences) => Promise; + applyColorScheme: (schemeId: string) => void; +} + +export const useUserPreferences = (): UseUserPreferencesReturn => { + const { authFetch, isAuthenticated } = useAuthenticatedFetch(); + const [preferences, setPreferences] = useState(DEFAULT_PREFERENCES); + const [hasAIAccess, setHasAIAccess] = useState(false); + const [loading, setLoading] = useState(false); + const [saving, setSaving] = useState(false); + + const fetchPreferences = useCallback(async () => { + if (!isAuthenticated) return; + + setLoading(true); + try { + const response = await authFetch('/api/users/me/preferences'); + if (response.ok) { + const data = await response.json(); + const prefs: UserPreferences = { + colorScheme: data.colorScheme || 'blue', + geminiApiKey: data.geminiApiKey || undefined, + }; + setPreferences(prefs); + setHasAIAccess(data.hasAIAccess || false); + applyColorScheme(prefs.colorScheme); + } + } catch { + } finally { + setLoading(false); + } + }, [authFetch, isAuthenticated]); + + const savePreferences = useCallback(async (prefs: UserPreferences) => { + setSaving(true); + try { + const response = await authFetch('/api/users/me/preferences', { + method: 'PUT', + body: JSON.stringify(prefs), + }); + + if (!response.ok) { + throw new Error('Failed to save preferences'); + } + + setPreferences(prefs); + applyColorScheme(prefs.colorScheme); + toast.success('Preferences saved!'); + } catch (err) { + const message = err instanceof Error ? err.message : 'Failed to save preferences'; + toast.error(message); + throw err; + } finally { + setSaving(false); + } + }, [authFetch]); + + useEffect(() => { + fetchPreferences(); + }, [fetchPreferences]); + + return { + preferences, + hasAIAccess, + loading, + saving, + fetchPreferences, + savePreferences, + applyColorScheme, + }; +}; diff --git a/server/src/db/connection.ts b/server/src/db/connection.ts index 8f654e3..ce70dfd 100644 --- a/server/src/db/connection.ts +++ b/server/src/db/connection.ts @@ -60,6 +60,18 @@ const runMigrations = () => { db.exec("ALTER TABLE game_sessions ADD COLUMN first_correct_player_id TEXT"); console.log("Migration: Added first_correct_player_id to game_sessions"); } + + const userTableInfo2 = db.prepare("PRAGMA table_info(users)").all() as { name: string }[]; + const hasColorScheme = userTableInfo2.some(col => col.name === "color_scheme"); + if (!hasColorScheme) { + db.exec("ALTER TABLE users ADD COLUMN color_scheme TEXT DEFAULT 'blue'"); + console.log("Migration: Added color_scheme to users"); + } + const hasGeminiKey = userTableInfo2.some(col => col.name === "gemini_api_key"); + if (!hasGeminiKey) { + db.exec("ALTER TABLE users ADD COLUMN gemini_api_key TEXT"); + console.log("Migration: Added gemini_api_key to users"); + } }; runMigrations(); diff --git a/server/src/db/schema.sql b/server/src/db/schema.sql index 86b3d49..f1e2cd6 100644 --- a/server/src/db/schema.sql +++ b/server/src/db/schema.sql @@ -5,7 +5,9 @@ CREATE TABLE IF NOT EXISTS users ( display_name TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, last_login DATETIME, - default_game_config TEXT + default_game_config TEXT, + color_scheme TEXT DEFAULT 'blue', + gemini_api_key TEXT ); CREATE TABLE IF NOT EXISTS quizzes ( diff --git a/server/src/middleware/auth.ts b/server/src/middleware/auth.ts index a5aa677..bbf6c14 100644 --- a/server/src/middleware/auth.ts +++ b/server/src/middleware/auth.ts @@ -34,6 +34,7 @@ export interface AuthenticatedUser { preferred_username: string; email?: string; name?: string; + groups?: string[]; } export interface AuthenticatedRequest extends Request { @@ -74,6 +75,7 @@ export function requireAuth( preferred_username: payload.preferred_username || payload.sub!, email: payload.email, name: payload.name, + groups: payload.groups || [], }; next(); diff --git a/server/src/routes/users.ts b/server/src/routes/users.ts index 24925b5..0860ea7 100644 --- a/server/src/routes/users.ts +++ b/server/src/routes/users.ts @@ -8,11 +8,16 @@ router.use(requireAuth); router.get('/me', (req: AuthenticatedRequest, res: Response) => { const user = db.prepare(` - SELECT id, username, email, display_name as displayName, default_game_config as defaultGameConfig, created_at as createdAt, last_login as lastLogin + SELECT id, username, email, display_name as displayName, default_game_config as defaultGameConfig, + color_scheme as colorScheme, gemini_api_key as geminiApiKey, + created_at as createdAt, last_login as lastLogin FROM users WHERE id = ? `).get(req.user!.sub) as Record | undefined; + const groups = req.user!.groups || []; + const hasAIAccess = groups.includes('kaboot-ai-access'); + if (!user) { res.json({ id: req.user!.sub, @@ -20,6 +25,9 @@ router.get('/me', (req: AuthenticatedRequest, res: Response) => { email: req.user!.email, displayName: req.user!.name, defaultGameConfig: null, + colorScheme: 'blue', + geminiApiKey: null, + hasAIAccess, createdAt: null, lastLogin: null, isNew: true, @@ -36,7 +44,12 @@ router.get('/me', (req: AuthenticatedRequest, res: Response) => { } } - res.json({ ...user, defaultGameConfig: parsedConfig, isNew: false }); + res.json({ + ...user, + defaultGameConfig: parsedConfig, + hasAIAccess, + isNew: false + }); }); router.put('/me/default-config', (req: AuthenticatedRequest, res: Response) => { @@ -64,4 +77,47 @@ router.put('/me/default-config', (req: AuthenticatedRequest, res: Response) => { res.json({ success: true }); }); +router.get('/me/preferences', (req: AuthenticatedRequest, res: Response) => { + const user = db.prepare(` + SELECT color_scheme as colorScheme, gemini_api_key as geminiApiKey + FROM users + WHERE id = ? + `).get(req.user!.sub) as { colorScheme: string | null; geminiApiKey: string | null } | undefined; + + const groups = req.user!.groups || []; + const hasAIAccess = groups.includes('kaboot-ai-access'); + + res.json({ + colorScheme: user?.colorScheme || 'blue', + geminiApiKey: user?.geminiApiKey || null, + hasAIAccess, + }); +}); + +router.put('/me/preferences', (req: AuthenticatedRequest, res: Response) => { + const { colorScheme, geminiApiKey } = req.body; + + const upsertUser = db.prepare(` + INSERT INTO users (id, username, email, display_name, color_scheme, gemini_api_key, last_login) + VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) + ON CONFLICT(id) DO UPDATE SET + color_scheme = ?, + gemini_api_key = ?, + last_login = CURRENT_TIMESTAMP + `); + + upsertUser.run( + req.user!.sub, + req.user!.preferred_username, + req.user!.email || null, + req.user!.name || null, + colorScheme || 'blue', + geminiApiKey || null, + colorScheme || 'blue', + geminiApiKey || null + ); + + res.json({ success: true }); +}); + export default router; diff --git a/types.ts b/types.ts index 26378a7..8e375f3 100644 --- a/types.ts +++ b/types.ts @@ -13,6 +13,34 @@ export type GameState = | 'WAITING_TO_REJOIN' | 'HOST_RECONNECTED'; +export interface ColorScheme { + id: string; + name: string; + primary: string; + primaryDark: string; + primaryDarker: string; +} + +export const COLOR_SCHEMES: ColorScheme[] = [ + { id: 'blue', name: 'Ocean Blue', primary: '#2563eb', primaryDark: '#1e40af', primaryDarker: '#1e3a5f' }, + { id: 'purple', name: 'Royal Purple', primary: '#7c3aed', primaryDark: '#5b21b6', primaryDarker: '#3b1a5f' }, + { id: 'pink', name: 'Hot Pink', primary: '#db2777', primaryDark: '#9d174d', primaryDarker: '#5f1a3b' }, + { id: 'red', name: 'Ruby Red', primary: '#dc2626', primaryDark: '#991b1b', primaryDarker: '#5f1a1a' }, + { id: 'orange', name: 'Sunset Orange', primary: '#ea580c', primaryDark: '#c2410c', primaryDarker: '#5f2a1a' }, + { id: 'yellow', name: 'Golden Yellow', primary: '#ca8a04', primaryDark: '#a16207', primaryDarker: '#5f4a1a' }, + { id: 'green', name: 'Emerald Green', primary: '#16a34a', primaryDark: '#15803d', primaryDarker: '#1a5f3b' }, + { id: 'teal', name: 'Teal', primary: '#0d9488', primaryDark: '#0f766e', primaryDarker: '#1a5f5a' }, + { id: 'cyan', name: 'Cyan', primary: '#0891b2', primaryDark: '#0e7490', primaryDarker: '#1a4a5f' }, + { id: 'indigo', name: 'Indigo', primary: '#4f46e5', primaryDark: '#4338ca', primaryDarker: '#2a2a5f' }, + { id: 'slate', name: 'Slate', primary: '#475569', primaryDark: '#334155', primaryDarker: '#1e293b' }, + { id: 'rose', name: 'Rose', primary: '#e11d48', primaryDark: '#be123c', primaryDarker: '#5f1a2a' }, +]; + +export interface UserPreferences { + colorScheme: string; + geminiApiKey?: string; +} + export type GameRole = 'HOST' | 'CLIENT'; export interface AnswerOption {