"use client" import { useState } from "react" import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" import { Header } from "@/components/layout/header" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Badge } from "@/components/ui/badge" import { Separator } from "@/components/ui/separator" import { Key, Trash2, Check, Loader2, Shield, Sliders } from "lucide-react" import { BackupRestore } from "@/components/settings/backup-restore" interface ApiKeyInfo { id: string provider: string label?: string maskedKey: string isActive: boolean } interface ApiKeysResponse { keys: ApiKeyInfo[] gatewayUrl: string } export default function SettingsPage() { const queryClient = useQueryClient() // API Keys const { data: apiKeyData } = useQuery({ queryKey: ["api-keys"], queryFn: async () => { const res = await fetch("/api/settings/api-keys") if (!res.ok) throw new Error("Failed to fetch API keys") return res.json() }, }) const apiKeys = apiKeyData?.keys ?? [] const legacyKeys = apiKeys.filter( (k) => k.provider === "claude" || k.provider === "openai" ) // Preferences const { data: preferences, isLoading: prefsLoading } = useQuery({ queryKey: ["preferences"], queryFn: async () => { const res = await fetch("/api/settings/preferences") if (!res.ok) throw new Error("Failed to fetch preferences") return res.json() }, }) const savePreferences = useMutation({ mutationFn: async (prefs: Record) => { const res = await fetch("/api/settings/preferences", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(prefs), }) if (!res.ok) throw new Error("Failed to save preferences") return res.json() }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["preferences"] }) }, }) return (

Settings

Manage your AI providers and preferences

{/* API Keys Section */} AI Gateway AI features route through Switchboard, which picks the best model for each request. Add your gateway API key below — it is encrypted before storage. {apiKeyData?.gatewayUrl && ( <> {" "} This app is pointed at{" "} {apiKeyData.gatewayUrl}. )} k.provider === "switchboard")} /> {legacyKeys.length > 0 && ( <> )} {/* Preferences Section */} Drink Preferences Help the AI make better recommendations by telling it what you like savePreferences.mutate(prefs)} isSaving={savePreferences.isPending} /> {/* Backup & Restore Section */}
) } const LEGACY_PROVIDER_LABELS: Record = { claude: "Anthropic Claude", openai: "OpenAI", } /** * Keys left over from when the app called Claude and OpenAI directly. They are * already ignored when picking a provider, but they are shown here so a user who * still has one can see it is inert and remove it. */ function LegacyKeyNotice({ keys }: { keys: ApiKeyInfo[] }) { const queryClient = useQueryClient() const deleteKey = useMutation({ mutationFn: async (provider: string) => { const res = await fetch(`/api/settings/api-keys?provider=${provider}`, { method: "DELETE", }) if (!res.ok) throw new Error("Failed to delete API key") }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["api-keys"] }) }, }) return (

These keys are from an earlier version that called each AI provider directly. They are no longer used and can be removed.

{keys.map((key) => (

{LEGACY_PROVIDER_LABELS[key.provider] ?? key.provider}

{key.maskedKey}
))}
) } function ApiKeyForm({ provider, label, existingKey, }: { provider: string label: string existingKey?: ApiKeyInfo }) { const [apiKey, setApiKey] = useState("") const [isEditing, setIsEditing] = useState(false) const queryClient = useQueryClient() const saveKey = useMutation({ mutationFn: async () => { const res = await fetch("/api/settings/api-keys", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ provider, apiKey }), }) if (!res.ok) throw new Error("Failed to save API key") return res.json() }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["api-keys"] }) setApiKey("") setIsEditing(false) }, }) const deleteKey = useMutation({ mutationFn: async () => { const res = await fetch(`/api/settings/api-keys?provider=${provider}`, { method: "DELETE", }) if (!res.ok) throw new Error("Failed to delete API key") }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["api-keys"] }) }, }) if (existingKey && !isEditing) { return (

{label}

{existingKey.maskedKey} Active
) } return (

{label}

setApiKey(e.target.value)} /> {isEditing && ( )}

Your key is encrypted before storage and never exposed in full

) } function PreferencesForm({ preferences, isLoading, onSave, isSaving, }: { preferences: Record | undefined isLoading: boolean onSave: (prefs: Record) => void isSaving: boolean }) { const [preferredStyles, setPreferredStyles] = useState("") const [avoidedStyles, setAvoidedStyles] = useState("") const [minAbv, setMinAbv] = useState("") const [maxAbv, setMaxAbv] = useState("") const [initialized, setInitialized] = useState(false) if (preferences && !initialized) { const prefs = preferences as { preferredStyles?: string[]; avoidedStyles?: string[]; minAbv?: number; maxAbv?: number } setPreferredStyles(prefs.preferredStyles?.join(", ") || "") setAvoidedStyles(prefs.avoidedStyles?.join(", ") || "") setMinAbv(prefs.minAbv?.toString() || "") setMaxAbv(prefs.maxAbv?.toString() || "") setInitialized(true) } const handleSubmit = (e: React.FormEvent) => { e.preventDefault() onSave({ preferredStyles: preferredStyles .split(",") .map((s) => s.trim()) .filter(Boolean), avoidedStyles: avoidedStyles .split(",") .map((s) => s.trim()) .filter(Boolean), minAbv: minAbv ? parseFloat(minAbv) : null, maxAbv: maxAbv ? parseFloat(maxAbv) : null, }) } if (isLoading) return

Loading...

return (
setPreferredStyles(e.target.value)} />

Comma-separated list of styles you enjoy

setAvoidedStyles(e.target.value)} />

Comma-separated list of styles you want to avoid

setMinAbv(e.target.value)} />
setMaxAbv(e.target.value)} />
) }