"use client" import { ChevronRight, Github, Info, Moon, Sun, Tag } from "lucide-react" import { usePathname, useRouter, useSearchParams } from "next/navigation" import { Suspense, useCallback, useEffect, useState } from "react" import { toast } from "sonner" import { Button } from "@/components/ui/button" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { Switch } from "@/components/ui/switch" import { Textarea } from "@/components/ui/textarea" import { useDictionary } from "@/hooks/use-dictionary" import { getApiEndpoint } from "@/lib/base-path" import type { DrawioTheme } from "@/lib/drawio-themes" import { i18n, type Locale } from "@/lib/i18n/config" import { STORAGE_KEYS } from "@/lib/storage" // Reusable setting item component for consistent layout function SettingItem({ label, description, children, }: { label: string description?: string children: React.ReactNode }) { return (
{description && (

{description}

)}
{children}
) } const LANGUAGE_LABELS: Record = { en: "English", zh: "中文", ja: "日本語", "zh-Hant": "繁體中文", } interface SettingsDialogProps { open: boolean onOpenChange: (open: boolean) => void drawioUi: DrawioTheme onDrawioUiChange: (theme: DrawioTheme) => void darkMode: boolean onToggleDarkMode: () => void minimalStyle?: boolean onMinimalStyleChange?: (value: boolean) => void vlmValidationEnabled?: boolean onVlmValidationChange?: (value: boolean) => void onOpenModelConfig?: () => void customSystemMessage?: string onCustomSystemMessageChange?: (value: string) => void } export const STORAGE_ACCESS_CODE_KEY = "next-ai-draw-io-access-code" const STORAGE_ACCESS_CODE_REQUIRED_KEY = "next-ai-draw-io-access-code-required" function getStoredAccessCodeRequired(): boolean | null { if (typeof window === "undefined") return null const stored = localStorage.getItem(STORAGE_ACCESS_CODE_REQUIRED_KEY) if (stored === null) return null return stored === "true" } function SettingsContent({ open, onOpenChange, drawioUi, onDrawioUiChange, darkMode, onToggleDarkMode, minimalStyle = false, onMinimalStyleChange = () => {}, vlmValidationEnabled = false, onVlmValidationChange = () => {}, onOpenModelConfig, customSystemMessage = "", onCustomSystemMessageChange = () => {}, }: SettingsDialogProps) { const dict = useDictionary() const router = useRouter() const pathname = usePathname() || "/" const search = useSearchParams() const [accessCode, setAccessCode] = useState("") const [isVerifying, setIsVerifying] = useState(false) const [error, setError] = useState("") const [accessCodeRequired, setAccessCodeRequired] = useState( () => getStoredAccessCodeRequired() ?? false, ) const [currentLang, setCurrentLang] = useState("en") const [sendShortcut, setSendShortcut] = useState("ctrl-enter") // Panel visibility state const [showRecentChats, setShowRecentChats] = useState(true) const [showMyTemplates, setShowMyTemplates] = useState(true) const [showQuickExamples, setShowQuickExamples] = useState(true) const handlePanelToggle = useCallback( (key: string, value: boolean, setter: (v: boolean) => void) => { setter(value) localStorage.setItem(key, String(value)) window.dispatchEvent(new CustomEvent("panelVisibilityChange")) }, [], ) // Proxy settings state (Electron only) const [httpProxy, setHttpProxy] = useState("") const [httpsProxy, setHttpsProxy] = useState("") const [isApplyingProxy, setIsApplyingProxy] = useState(false) useEffect(() => { // Re-fetch config whenever the dialog opens to ensure we always show // the access code input if the server requires it. This fixes the case // where a stale localStorage cache (from before ACCESS_CODE_LIST was // configured) would hide the access code input. if (!open) return 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_ACCESS_CODE_REQUIRED_KEY, String(required), ) setAccessCodeRequired(required) }) .catch(() => { // Keep existing cached value on error }) }, [open]) // Detect current language from pathname useEffect(() => { const seg = pathname.split("/").filter(Boolean) const first = seg[0] if (first && i18n.locales.includes(first as Locale)) { setCurrentLang(first) } else { setCurrentLang(i18n.defaultLocale) } }, [pathname]) useEffect(() => { if (open) { const storedCode = localStorage.getItem(STORAGE_ACCESS_CODE_KEY) || "" setAccessCode(storedCode) const storedSendShortcut = localStorage.getItem( STORAGE_KEYS.sendShortcut, ) setSendShortcut(storedSendShortcut || "ctrl-enter") setShowRecentChats( localStorage.getItem(STORAGE_KEYS.showRecentChats) !== "false", ) setShowMyTemplates( localStorage.getItem(STORAGE_KEYS.showMyTemplates) !== "false", ) setShowQuickExamples( localStorage.getItem(STORAGE_KEYS.showQuickExamples) !== "false", ) setError("") // Load proxy settings (Electron only) if (window.electronAPI?.getProxy) { window.electronAPI.getProxy().then((config) => { setHttpProxy(config.httpProxy || "") setHttpsProxy(config.httpsProxy || "") }) } } }, [open]) const changeLanguage = (lang: string) => { // Save locale to localStorage for persistence across restarts localStorage.setItem("next-ai-draw-io-locale", lang) // Notify Electron main process to update its menu language if (window.electronAPI?.setUserLocale) { window.electronAPI.setUserLocale(lang).catch((error) => { 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 newPath = parts.join("/") || "/" const searchStr = search?.toString() ? `?${search.toString()}` : "" router.push(newPath + searchStr) } const handleSave = async () => { if (!accessCodeRequired) return setError("") 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) { setError(data.message || dict.errors.invalidAccessCode) return } localStorage.setItem(STORAGE_ACCESS_CODE_KEY, accessCode.trim()) onOpenChange(false) } catch { setError(dict.errors.networkError) } finally { setIsVerifying(false) } } const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { e.preventDefault() handleSave() } } const handleApplyProxy = async () => { if (!window.electronAPI?.setProxy) return // Validate proxy URLs (must start with http:// or https://) const validateProxyUrl = (url: string): boolean => { if (!url) return true // Empty is OK return url.startsWith("http://") || url.startsWith("https://") } const trimmedHttp = httpProxy.trim() const trimmedHttps = httpsProxy.trim() if (trimmedHttp && !validateProxyUrl(trimmedHttp)) { toast.error("HTTP Proxy must start with http:// or https://") return } if (trimmedHttps && !validateProxyUrl(trimmedHttps)) { toast.error("HTTPS Proxy must start with http:// or https://") return } setIsApplyingProxy(true) try { const result = await window.electronAPI.setProxy({ httpProxy: trimmedHttp || undefined, httpsProxy: trimmedHttps || undefined, }) if (result.success) { toast.success(dict.settings.proxyApplied) } else { toast.error(result.error || "Failed to apply proxy settings") } } catch { toast.error("Failed to apply proxy settings") } finally { setIsApplyingProxy(false) } } return ( {/* Header */} {dict.settings.title} {dict.settings.description} {/* Content */}
{/* API Keys & Models */} {onOpenModelConfig && ( )} {/* Access Code (conditional) */} {accessCodeRequired && (

{dict.settings.accessCodeDescription}

setAccessCode(e.target.value) } onKeyDown={handleKeyDown} placeholder={ dict.settings.accessCodePlaceholder } autoComplete="off" className="h-9" />
{error && (

{error}

)}
)} {/* Language */} {/* Theme */} {/* Draw.io Style */} {/* Diagram Style */}
{minimalStyle ? dict.chat.minimalStyle : dict.chat.styledMode}
{/* Panel Visibility */}
{/* VLM Diagram Validation */}
{vlmValidationEnabled ? dict.settings.enabled : dict.settings.disabled}
{/* Custom System Message */}

{dict.settings.customSystemMessageDescription}