"use client" import { Github, Info, Moon, Sun, Tag } from "lucide-react" import { usePathname, useRouter, useSearchParams } from "next/navigation" import { Suspense, useEffect, useState } from "react" 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 { useDictionary } from "@/hooks/use-dictionary" import { getApiEndpoint } from "@/lib/base-path" import { i18n, type Locale } from "@/lib/i18n/config" // 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: "日本語", } interface SettingsDialogProps { open: boolean onOpenChange: (open: boolean) => void onCloseProtectionChange?: (enabled: boolean) => void drawioUi: "min" | "sketch" onToggleDrawioUi: () => void darkMode: boolean onToggleDarkMode: () => void minimalStyle?: boolean onMinimalStyleChange?: (value: boolean) => void } export const STORAGE_ACCESS_CODE_KEY = "next-ai-draw-io-access-code" export const STORAGE_CLOSE_PROTECTION_KEY = "next-ai-draw-io-close-protection" 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, onCloseProtectionChange, drawioUi, onToggleDrawioUi, darkMode, onToggleDarkMode, minimalStyle = false, onMinimalStyleChange = () => {}, }: SettingsDialogProps) { const dict = useDictionary() const router = useRouter() const pathname = usePathname() || "/" const search = useSearchParams() const [accessCode, setAccessCode] = useState("") const [closeProtection, setCloseProtection] = useState(true) const [isVerifying, setIsVerifying] = useState(false) const [error, setError] = useState("") const [accessCodeRequired, setAccessCodeRequired] = useState( () => getStoredAccessCodeRequired() ?? false, ) const [currentLang, setCurrentLang] = useState("en") useEffect(() => { // Only fetch if not cached in localStorage if (getStoredAccessCodeRequired() !== null) 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(() => { // Don't cache on error - allow retry on next mount setAccessCodeRequired(false) }) }, []) // 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 storedCloseProtection = localStorage.getItem( STORAGE_CLOSE_PROTECTION_KEY, ) // Default to true if not set setCloseProtection(storedCloseProtection !== "false") setError("") } }, [open]) const changeLanguage = (lang: string) => { // Save locale to localStorage for persistence across restarts localStorage.setItem("next-ai-draw-io-locale", lang) 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() } } return ( {/* Header */} {dict.settings.title} {dict.settings.description} {/* Content */}
{/* 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 */} {/* Close Protection */} { setCloseProtection(checked) localStorage.setItem( STORAGE_CLOSE_PROTECTION_KEY, checked.toString(), ) onCloseProtectionChange?.(checked) }} /> {/* Diagram Style */}
{minimalStyle ? dict.chat.minimalStyle : dict.chat.styledMode}
{/* Footer */}
{process.env.APP_VERSION} · GitHub {process.env.NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE === "true" && ( <> · {dict.nav.about} )}
) } export function SettingsDialog(props: SettingsDialogProps) { return (
} >
) }