import { Github, Info, KeyRound, Monitor, Moon, Palette, SlidersHorizontal, Sun, Terminal, } from "lucide-react" import { usePathname, useRouter, useSearchParams } from "next/navigation" import { useEffect, useState } from "react" import { toast } from "sonner" import { useChatEngine } from "@/components/chat/chat-engine" import { ModelConfigDialog } from "@/components/model-config-dialog" import { Button } from "@/components/ui/button" import { Dialog, DialogContent, DialogDescription, DialogTitle, } from "@/components/ui/dialog" import { Input } from "@/components/ui/input" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { Switch } from "@/components/ui/switch" import { Textarea } from "@/components/ui/textarea" import { BrandMark } from "@/components/workspace/brand-mark" import { useDictionary } from "@/hooks/use-dictionary" import { getApiEndpoint } from "@/lib/base-path" import { i18n, type Locale } from "@/lib/i18n/config" import { STORAGE_KEYS } from "@/lib/storage" import { cn } from "@/lib/utils" import { type SendShortcut, type ThemePreference, useSettingsStore, } from "@/stores/settings-store" import { type SettingsTab, useUiStore } from "@/stores/ui-store" const LANGUAGE_LABELS: Record = { en: "English", zh: "中文", ja: "日本語", "zh-Hant": "繁體中文", } function Row({ label, description, htmlFor, children, stacked = false, }: { label: string description?: string htmlFor?: string children: React.ReactNode stacked?: boolean }) { return (
{description && (

{description}

)}
{children}
) } function Segmented({ value, options, onChange, label, }: { value: T options: { value: T; label: string; icon?: React.ReactNode }[] onChange: (value: T) => void label: string }) { return (
{options.map((option) => ( // biome-ignore lint/a11y/useSemanticElements: styled segmented control ))}
) } function AppearanceTab() { const dict = useDictionary() const t = dict.settings const engine = useChatEngine() const router = useRouter() const pathname = usePathname() || "/" const search = useSearchParams() const theme = useSettingsStore((s) => s.theme) const setTheme = useSettingsStore((s) => s.setTheme) const minimalStyle = useSettingsStore((s) => s.minimalStyle) const setMinimalStyle = useSettingsStore((s) => s.setMinimalStyle) const currentLang = (pathname.split("/").filter(Boolean)[0] as Locale) || i18n.defaultLocale // The page mounts anew in the other language: the chat is saved first // (it may not be yet), and its session goes into the new URL const changeLanguage = (lang: string) => engine.leavePage((sessionId) => { localStorage.setItem(STORAGE_KEYS.locale, lang) // The page reloads in the new language; close settings as before useUiStore.getState().setSettingsOpen(false) // Keep the desktop app's menu language in sync window.electronAPI ?.setUserLocale?.(lang) .catch((error: unknown) => { console.error("Failed to sync locale with Electron:", error) }) const parts = pathname.split("/") if (parts.length > 1 && i18n.locales.includes(parts[1] as Locale)) { parts[1] = lang } else { parts.splice(1, 0, lang) } const params = new URLSearchParams(search?.toString()) if (sessionId) params.set("session", sessionId) const query = params.toString() ? `?${params.toString()}` : "" router.push((parts.join("/") || "/") + query) }) return (
label={t.theme} value={theme} onChange={setTheme} options={[ { value: "light", label: t.themeLight, icon: }, { value: "dark", label: t.themeDarkMode, icon: , }, { value: "system", label: t.themeSystem, icon: , }, ]} /> label={t.diagramStyle} value={minimalStyle ? "minimal" : "styled"} onChange={(value) => setMinimalStyle(value === "minimal")} options={[ { value: "styled", label: dict.chat.styledMode }, { value: "minimal", label: dict.chat.minimalStyle }, ]} />
) } function AdvancedTab({ open }: { open: boolean }) { const dict = useDictionary() const t = dict.settings const settings = useSettingsStore() const [accessCode, setAccessCode] = useState("") const [accessCodeRequired, setAccessCodeRequired] = useState( () => localStorage.getItem(STORAGE_KEYS.accessCodeRequired) === "true", ) const [isVerifying, setIsVerifying] = useState(false) const [accessError, setAccessError] = useState("") const [httpProxy, setHttpProxy] = useState("") const [httpsProxy, setHttpsProxy] = useState("") const [isApplyingProxy, setIsApplyingProxy] = useState(false) const isElectron = typeof window !== "undefined" && !!window.electronAPI?.isElectron useEffect(() => { if (!open) return setAccessCode(localStorage.getItem(STORAGE_KEYS.accessCode) || "") setAccessError("") // Re-check on every open: a stale cached value would hide the field fetch(getApiEndpoint("/api/config")) .then((res) => { if (!res.ok) throw new Error(`HTTP ${res.status}`) return res.json() }) .then((data) => { const required = data?.accessCodeRequired === true localStorage.setItem( STORAGE_KEYS.accessCodeRequired, String(required), ) setAccessCodeRequired(required) }) .catch(() => {}) window.electronAPI?.getProxy?.().then((config: any) => { setHttpProxy(config.httpProxy || "") setHttpsProxy(config.httpsProxy || "") }) }, [open]) const saveAccessCode = async () => { setAccessError("") setIsVerifying(true) try { const response = await fetch( getApiEndpoint("/api/verify-access-code"), { method: "POST", headers: { "x-access-code": accessCode.trim() }, }, ) const data = await response.json() if (!data.valid) { setAccessError(data.message || dict.errors.invalidAccessCode) return } localStorage.setItem(STORAGE_KEYS.accessCode, accessCode.trim()) toast.success(t.accessCodeSaved) } catch { setAccessError(dict.errors.networkError) } finally { setIsVerifying(false) } } const applyProxy = async () => { if (!window.electronAPI?.setProxy) return const isValid = (url: string) => !url || url.startsWith("http://") || url.startsWith("https://") const http = httpProxy.trim() const https = httpsProxy.trim() if (!isValid(http) || !isValid(https)) { toast.error(t.proxyInvalid) return } setIsApplyingProxy(true) try { const result = await window.electronAPI.setProxy({ httpProxy: http || undefined, httpsProxy: https || undefined, }) if (result.success) toast.success(t.proxyApplied) else toast.error(result.error || t.proxyFailed) } catch { toast.error(t.proxyFailed) } finally { setIsApplyingProxy(false) } } return (
{accessCodeRequired && (
setAccessCode(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault() saveAccessCode() } }} placeholder={t.accessCodePlaceholder} autoComplete="off" className="h-9" />
{accessError && (

{accessError}

)}
)} settings.setMaxOutputTokens(e.target.value) } placeholder="64000" className="h-8 w-28 text-sm" />