diff --git a/components/chat-panel.tsx b/components/chat-panel.tsx index 7bfe807..941769f 100644 --- a/components/chat-panel.tsx +++ b/components/chat-panel.tsx @@ -21,6 +21,11 @@ import { ChatInput } from "@/components/chat-input" import { ModelConfigDialog } from "@/components/model-config-dialog" import { ResetWarningModal } from "@/components/reset-warning-modal" import { SettingsDialog } from "@/components/settings-dialog" +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip" import { useDiagram } from "@/contexts/diagram-context" import { useDictionary } from "@/hooks/use-dictionary" import { getSelectedAIConfig, useModelConfig } from "@/hooks/use-model-config" @@ -30,7 +35,6 @@ import { type FileData, useFileProcessor } from "@/lib/use-file-processor" import { useQuotaManager } from "@/lib/use-quota-manager" import { formatXML, isMxCellXmlComplete, wrapWithMxFile } from "@/lib/utils" import { ChatMessageDisplay } from "./chat-message-display" -import LanguageToggle from "./language-toggle" // localStorage keys for persistence const STORAGE_MESSAGES_KEY = "next-ai-draw-io-messages" @@ -1304,16 +1308,23 @@ Continue from EXACTLY where you stopped.`, />
- - - + + + + + + + + {dict.nav.github} + +
- {!isMobile && ( = { - en: "EN", - zh: "中文", - ja: "日本語", -} - -function LanguageToggleInner({ className = "" }: { className?: string }) { - const router = useRouter() - const pathname = usePathname() || "/" - const search = useSearchParams() - const [open, setOpen] = useState(false) - const [value, setValue] = useState(i18n.defaultLocale) - const ref = useRef(null) - - useEffect(() => { - const seg = pathname.split("/").filter(Boolean) - const first = seg[0] - if (first && i18n.locales.includes(first as Locale)) - setValue(first as Locale) - else setValue(i18n.defaultLocale) - }, [pathname]) - - useEffect(() => { - function onDoc(e: MouseEvent) { - if (!ref.current) return - if (!ref.current.contains(e.target as Node)) setOpen(false) - } - if (open) document.addEventListener("mousedown", onDoc) - return () => document.removeEventListener("mousedown", onDoc) - }, [open]) - - const changeLocale = (lang: string) => { - 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()}` : "" - setOpen(false) - router.push(newPath + searchStr) - } - - return ( -
- - {open && ( -
-
- {i18n.locales.map((loc) => ( - - ))} -
-
- )} -
- ) -} - -export default function LanguageToggle({ - className = "", -}: { - className?: string -}) { - return ( - - - - } - > - - - ) -} diff --git a/components/settings-dialog.tsx b/components/settings-dialog.tsx index 029fc8d..720e039 100644 --- a/components/settings-dialog.tsx +++ b/components/settings-dialog.tsx @@ -1,7 +1,8 @@ "use client" import { Moon, Sun } from "lucide-react" -import { useEffect, useState } from "react" +import { usePathname, useRouter, useSearchParams } from "next/navigation" +import { Suspense, useEffect, useState } from "react" import { Button } from "@/components/ui/button" import { Dialog, @@ -12,8 +13,22 @@ import { } 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 { i18n, type Locale } from "@/lib/i18n/config" + +const LANGUAGE_LABELS: Record = { + en: "English", + zh: "中文", + ja: "日本語", +} interface SettingsDialogProps { open: boolean @@ -36,7 +51,7 @@ function getStoredAccessCodeRequired(): boolean | null { return stored === "true" } -export function SettingsDialog({ +function SettingsContent({ open, onOpenChange, onCloseProtectionChange, @@ -46,6 +61,9 @@ export function SettingsDialog({ onToggleDarkMode, }: 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) @@ -53,6 +71,7 @@ export function SettingsDialog({ const [accessCodeRequired, setAccessCodeRequired] = useState( () => getStoredAccessCodeRequired() ?? false, ) + const [currentLang, setCurrentLang] = useState("en") useEffect(() => { // Only fetch if not cached in localStorage @@ -77,6 +96,17 @@ export function SettingsDialog({ }) }, []) + // 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 = @@ -93,6 +123,18 @@ export function SettingsDialog({ } }, [open]) + const changeLanguage = (lang: string) => { + 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 @@ -131,128 +173,166 @@ export function SettingsDialog({ } return ( - - - - {dict.settings.title} - - {dict.settings.description} - - -
- {accessCodeRequired && ( -
- -
- - setAccessCode(e.target.value) - } - onKeyDown={handleKeyDown} - placeholder={ - dict.settings.accessCodePlaceholder - } - autoComplete="off" - /> - -
-

- {dict.settings.accessCodeDescription} -

- {error && ( -

- {error} -

- )} + + + {dict.settings.title} + + {dict.settings.description} + + +
+ {accessCodeRequired && ( +
+ +
+ setAccessCode(e.target.value)} + onKeyDown={handleKeyDown} + placeholder={ + dict.settings.accessCodePlaceholder + } + autoComplete="off" + /> +
- )} -
-
- -

- {dict.settings.themeDescription} +

+ {dict.settings.accessCodeDescription} +

+ {error && ( +

+ {error}

-
- + )}
+ )} -
-
- -

- {dict.settings.drawioStyleDescription}{" "} - {drawioUi === "min" - ? dict.settings.minimal - : dict.settings.sketch} -

-
- +
+ +
+
+ +

+ {dict.settings.drawioStyleDescription}{" "} {drawioUi === "min" - ? dict.settings.sketch - : dict.settings.minimal} - + ? dict.settings.minimal + : dict.settings.sketch} +

+ +
-
-
- -

- {dict.settings.closeProtectionDescription} -

-
- { - setCloseProtection(checked) - localStorage.setItem( - STORAGE_CLOSE_PROTECTION_KEY, - checked.toString(), - ) - onCloseProtectionChange?.(checked) - }} - /> +
+
+ +

+ {dict.settings.closeProtectionDescription} +

+ { + setCloseProtection(checked) + localStorage.setItem( + STORAGE_CLOSE_PROTECTION_KEY, + checked.toString(), + ) + onCloseProtectionChange?.(checked) + }} + />
-
-

- Version {process.env.APP_VERSION} -

-
- +
+
+

+ Version {process.env.APP_VERSION} +

+
+ + ) +} + +export function SettingsDialog(props: SettingsDialogProps) { + return ( + + +
+
+
+ + } + > + +
) } diff --git a/lib/i18n/dictionaries/en.json b/lib/i18n/dictionaries/en.json index 44912cf..241c837 100644 --- a/lib/i18n/dictionaries/en.json +++ b/lib/i18n/dictionaries/en.json @@ -14,6 +14,7 @@ "about": "About", "editor": "Editor", "newChat": "Start fresh chat", + "github": "GitHub", "settings": "Settings", "hidePanel": "Hide chat panel (Ctrl+B)", "showPanel": "Show chat panel (Ctrl+B)", @@ -87,6 +88,8 @@ "overrides": "Overrides", "clearSettings": "Clear Settings", "useServerDefault": "Use Server Default", + "language": "Language", + "languageDescription": "Choose your interface language.", "theme": "Theme", "themeDescription": "Dark/Light mode for interface and DrawIO canvas.", "drawioStyle": "DrawIO Style", diff --git a/lib/i18n/dictionaries/ja.json b/lib/i18n/dictionaries/ja.json index 68001dd..f16b047 100644 --- a/lib/i18n/dictionaries/ja.json +++ b/lib/i18n/dictionaries/ja.json @@ -14,6 +14,7 @@ "about": "概要", "editor": "エディタ", "newChat": "新しいチャットを開始", + "github": "GitHub", "settings": "設定", "hidePanel": "チャットパネルを非表示 (Ctrl+B)", "showPanel": "チャットパネルを表示 (Ctrl+B)", @@ -87,6 +88,8 @@ "overrides": "上書き", "clearSettings": "設定をクリア", "useServerDefault": "サーバーデフォルトを使用", + "language": "言語", + "languageDescription": "インターフェース言語を選択します。", "theme": "テーマ", "themeDescription": "インターフェースと DrawIO キャンバスのダーク/ライトモード。", "drawioStyle": "DrawIO スタイル", diff --git a/lib/i18n/dictionaries/zh.json b/lib/i18n/dictionaries/zh.json index 43a8e7e..f287f7d 100644 --- a/lib/i18n/dictionaries/zh.json +++ b/lib/i18n/dictionaries/zh.json @@ -14,6 +14,7 @@ "about": "关于", "editor": "编辑器", "newChat": "开始新对话", + "github": "GitHub", "settings": "设置", "hidePanel": "隐藏聊天面板 (Ctrl+B)", "showPanel": "显示聊天面板 (Ctrl+B)", @@ -87,6 +88,8 @@ "overrides": "覆盖", "clearSettings": "清除设置", "useServerDefault": "使用服务器默认值", + "language": "语言", + "languageDescription": "选择界面语言。", "theme": "主题", "themeDescription": "界面和 DrawIO 画布的深色/浅色模式。", "drawioStyle": "DrawIO 样式",