mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-02 22:32:27 +08:00
move Language Selector into SettingDialog (#352)
* fix:custom model setting bug * refactor: consolidate aiProvider checks for cleaner code * fix:Integrated the language selection option into the `SettingsDialog` * fix:useSearchParams() should be wrapped in a suspense boundary at page * fix: improve semantic HTML and maintainability - Replace nested button>a with proper anchor element for GitHub link - Use i18n.locales.map() with LANGUAGE_LABELS for language options --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
This commit is contained in:
@@ -21,6 +21,11 @@ import { ChatInput } from "@/components/chat-input"
|
|||||||
import { ModelConfigDialog } from "@/components/model-config-dialog"
|
import { ModelConfigDialog } from "@/components/model-config-dialog"
|
||||||
import { ResetWarningModal } from "@/components/reset-warning-modal"
|
import { ResetWarningModal } from "@/components/reset-warning-modal"
|
||||||
import { SettingsDialog } from "@/components/settings-dialog"
|
import { SettingsDialog } from "@/components/settings-dialog"
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip"
|
||||||
import { useDiagram } from "@/contexts/diagram-context"
|
import { useDiagram } from "@/contexts/diagram-context"
|
||||||
import { useDictionary } from "@/hooks/use-dictionary"
|
import { useDictionary } from "@/hooks/use-dictionary"
|
||||||
import { getSelectedAIConfig, useModelConfig } from "@/hooks/use-model-config"
|
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 { useQuotaManager } from "@/lib/use-quota-manager"
|
||||||
import { formatXML, isMxCellXmlComplete, wrapWithMxFile } from "@/lib/utils"
|
import { formatXML, isMxCellXmlComplete, wrapWithMxFile } from "@/lib/utils"
|
||||||
import { ChatMessageDisplay } from "./chat-message-display"
|
import { ChatMessageDisplay } from "./chat-message-display"
|
||||||
import LanguageToggle from "./language-toggle"
|
|
||||||
|
|
||||||
// localStorage keys for persistence
|
// localStorage keys for persistence
|
||||||
const STORAGE_MESSAGES_KEY = "next-ai-draw-io-messages"
|
const STORAGE_MESSAGES_KEY = "next-ai-draw-io-messages"
|
||||||
@@ -1304,16 +1308,23 @@ Continue from EXACTLY where you stopped.`,
|
|||||||
/>
|
/>
|
||||||
</ButtonWithTooltip>
|
</ButtonWithTooltip>
|
||||||
<div className="w-px h-5 bg-border mx-1" />
|
<div className="w-px h-5 bg-border mx-1" />
|
||||||
<a
|
|
||||||
href="https://github.com/DayuanJiang/next-ai-draw-io"
|
<Tooltip>
|
||||||
target="_blank"
|
<TooltipTrigger asChild>
|
||||||
rel="noopener noreferrer"
|
<a
|
||||||
className="p-2 rounded-lg text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
|
href="https://github.com/DayuanJiang/next-ai-draw-io"
|
||||||
>
|
target="_blank"
|
||||||
<FaGithub
|
rel="noopener noreferrer"
|
||||||
className={`${isMobile ? "w-4 h-4" : "w-5 h-5"}`}
|
className="inline-flex items-center justify-center h-9 w-9 rounded-md text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
|
||||||
/>
|
>
|
||||||
</a>
|
<FaGithub
|
||||||
|
className={`${isMobile ? "w-4 h-4" : "w-5 h-5"}`}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{dict.nav.github}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<ButtonWithTooltip
|
<ButtonWithTooltip
|
||||||
tooltipContent={dict.nav.settings}
|
tooltipContent={dict.nav.settings}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -1326,7 +1337,6 @@ Continue from EXACTLY where you stopped.`,
|
|||||||
/>
|
/>
|
||||||
</ButtonWithTooltip>
|
</ButtonWithTooltip>
|
||||||
<div className="hidden sm:flex items-center gap-2">
|
<div className="hidden sm:flex items-center gap-2">
|
||||||
<LanguageToggle />
|
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
<ButtonWithTooltip
|
<ButtonWithTooltip
|
||||||
tooltipContent={dict.nav.hidePanel}
|
tooltipContent={dict.nav.hidePanel}
|
||||||
|
|||||||
@@ -1,108 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { Globe } from "lucide-react"
|
|
||||||
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
|
||||||
import { Suspense, useEffect, useRef, useState } from "react"
|
|
||||||
import { i18n, type Locale } from "@/lib/i18n/config"
|
|
||||||
|
|
||||||
const LABELS: Record<string, string> = {
|
|
||||||
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<Locale>(i18n.defaultLocale)
|
|
||||||
const ref = useRef<HTMLDivElement | null>(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 (
|
|
||||||
<div className={`relative inline-flex ${className}`} ref={ref}>
|
|
||||||
<button
|
|
||||||
aria-haspopup="menu"
|
|
||||||
aria-expanded={open}
|
|
||||||
onClick={() => setOpen((s) => !s)}
|
|
||||||
className="p-2 rounded-full hover:bg-accent/20 transition-colors text-muted-foreground"
|
|
||||||
aria-label="Change language"
|
|
||||||
>
|
|
||||||
<Globe className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
{open && (
|
|
||||||
<div className="absolute right-0 top-full mt-2 w-40 bg-popover dark:bg-popover text-popover-foreground rounded-xl shadow-md border border-border/30 overflow-hidden z-50">
|
|
||||||
<div className="grid gap-0 divide-y divide-border/30">
|
|
||||||
{i18n.locales.map((loc) => (
|
|
||||||
<button
|
|
||||||
key={loc}
|
|
||||||
onClick={() => changeLocale(loc)}
|
|
||||||
className={`flex items-center gap-2 px-4 py-2 text-sm w-full text-left hover:bg-accent/10 transition-colors ${value === loc ? "bg-accent/10 font-semibold" : ""}`}
|
|
||||||
>
|
|
||||||
<span className="flex-1">
|
|
||||||
{LABELS[loc] ?? loc}
|
|
||||||
</span>
|
|
||||||
{value === loc && (
|
|
||||||
<span className="text-xs opacity-70">
|
|
||||||
✓
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function LanguageToggle({
|
|
||||||
className = "",
|
|
||||||
}: {
|
|
||||||
className?: string
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Suspense
|
|
||||||
fallback={
|
|
||||||
<button
|
|
||||||
className="p-2 rounded-full text-muted-foreground opacity-50"
|
|
||||||
disabled
|
|
||||||
>
|
|
||||||
<Globe className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<LanguageToggleInner className={className} />
|
|
||||||
</Suspense>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Moon, Sun } from "lucide-react"
|
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 { Button } from "@/components/ui/button"
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -12,8 +13,22 @@ import {
|
|||||||
} from "@/components/ui/dialog"
|
} from "@/components/ui/dialog"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select"
|
||||||
import { Switch } from "@/components/ui/switch"
|
import { Switch } from "@/components/ui/switch"
|
||||||
import { useDictionary } from "@/hooks/use-dictionary"
|
import { useDictionary } from "@/hooks/use-dictionary"
|
||||||
|
import { i18n, type Locale } from "@/lib/i18n/config"
|
||||||
|
|
||||||
|
const LANGUAGE_LABELS: Record<Locale, string> = {
|
||||||
|
en: "English",
|
||||||
|
zh: "中文",
|
||||||
|
ja: "日本語",
|
||||||
|
}
|
||||||
|
|
||||||
interface SettingsDialogProps {
|
interface SettingsDialogProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
@@ -36,7 +51,7 @@ function getStoredAccessCodeRequired(): boolean | null {
|
|||||||
return stored === "true"
|
return stored === "true"
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SettingsDialog({
|
function SettingsContent({
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
onCloseProtectionChange,
|
onCloseProtectionChange,
|
||||||
@@ -46,6 +61,9 @@ export function SettingsDialog({
|
|||||||
onToggleDarkMode,
|
onToggleDarkMode,
|
||||||
}: SettingsDialogProps) {
|
}: SettingsDialogProps) {
|
||||||
const dict = useDictionary()
|
const dict = useDictionary()
|
||||||
|
const router = useRouter()
|
||||||
|
const pathname = usePathname() || "/"
|
||||||
|
const search = useSearchParams()
|
||||||
const [accessCode, setAccessCode] = useState("")
|
const [accessCode, setAccessCode] = useState("")
|
||||||
const [closeProtection, setCloseProtection] = useState(true)
|
const [closeProtection, setCloseProtection] = useState(true)
|
||||||
const [isVerifying, setIsVerifying] = useState(false)
|
const [isVerifying, setIsVerifying] = useState(false)
|
||||||
@@ -53,6 +71,7 @@ export function SettingsDialog({
|
|||||||
const [accessCodeRequired, setAccessCodeRequired] = useState(
|
const [accessCodeRequired, setAccessCodeRequired] = useState(
|
||||||
() => getStoredAccessCodeRequired() ?? false,
|
() => getStoredAccessCodeRequired() ?? false,
|
||||||
)
|
)
|
||||||
|
const [currentLang, setCurrentLang] = useState("en")
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Only fetch if not cached in localStorage
|
// 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(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
const storedCode =
|
const storedCode =
|
||||||
@@ -93,6 +123,18 @@ export function SettingsDialog({
|
|||||||
}
|
}
|
||||||
}, [open])
|
}, [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 () => {
|
const handleSave = async () => {
|
||||||
if (!accessCodeRequired) return
|
if (!accessCodeRequired) return
|
||||||
|
|
||||||
@@ -131,128 +173,166 @@ export function SettingsDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<DialogContent className="sm:max-w-md">
|
||||||
<DialogContent className="sm:max-w-md">
|
<DialogHeader>
|
||||||
<DialogHeader>
|
<DialogTitle>{dict.settings.title}</DialogTitle>
|
||||||
<DialogTitle>{dict.settings.title}</DialogTitle>
|
<DialogDescription>
|
||||||
<DialogDescription>
|
{dict.settings.description}
|
||||||
{dict.settings.description}
|
</DialogDescription>
|
||||||
</DialogDescription>
|
</DialogHeader>
|
||||||
</DialogHeader>
|
<div className="space-y-4 py-2">
|
||||||
<div className="space-y-4 py-2">
|
{accessCodeRequired && (
|
||||||
{accessCodeRequired && (
|
<div className="space-y-2">
|
||||||
<div className="space-y-2">
|
<Label htmlFor="access-code">
|
||||||
<Label htmlFor="access-code">
|
{dict.settings.accessCode}
|
||||||
{dict.settings.accessCode}
|
</Label>
|
||||||
</Label>
|
<div className="flex gap-2">
|
||||||
<div className="flex gap-2">
|
<Input
|
||||||
<Input
|
id="access-code"
|
||||||
id="access-code"
|
type="password"
|
||||||
type="password"
|
value={accessCode}
|
||||||
value={accessCode}
|
onChange={(e) => setAccessCode(e.target.value)}
|
||||||
onChange={(e) =>
|
onKeyDown={handleKeyDown}
|
||||||
setAccessCode(e.target.value)
|
placeholder={
|
||||||
}
|
dict.settings.accessCodePlaceholder
|
||||||
onKeyDown={handleKeyDown}
|
}
|
||||||
placeholder={
|
autoComplete="off"
|
||||||
dict.settings.accessCodePlaceholder
|
/>
|
||||||
}
|
<Button
|
||||||
autoComplete="off"
|
onClick={handleSave}
|
||||||
/>
|
disabled={isVerifying || !accessCode.trim()}
|
||||||
<Button
|
>
|
||||||
onClick={handleSave}
|
{isVerifying ? "..." : dict.common.save}
|
||||||
disabled={isVerifying || !accessCode.trim()}
|
</Button>
|
||||||
>
|
|
||||||
{isVerifying ? "..." : dict.common.save}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<p className="text-[0.8rem] text-muted-foreground">
|
|
||||||
{dict.settings.accessCodeDescription}
|
|
||||||
</p>
|
|
||||||
{error && (
|
|
||||||
<p className="text-[0.8rem] text-destructive">
|
|
||||||
{error}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<p className="text-[0.8rem] text-muted-foreground">
|
||||||
<div className="flex items-center justify-between">
|
{dict.settings.accessCodeDescription}
|
||||||
<div className="space-y-0.5">
|
</p>
|
||||||
<Label htmlFor="theme-toggle">
|
{error && (
|
||||||
{dict.settings.theme}
|
<p className="text-[0.8rem] text-destructive">
|
||||||
</Label>
|
{error}
|
||||||
<p className="text-[0.8rem] text-muted-foreground">
|
|
||||||
{dict.settings.themeDescription}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
)}
|
||||||
<Button
|
|
||||||
id="theme-toggle"
|
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
onClick={onToggleDarkMode}
|
|
||||||
>
|
|
||||||
{darkMode ? (
|
|
||||||
<Sun className="h-4 w-4" />
|
|
||||||
) : (
|
|
||||||
<Moon className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<Label htmlFor="drawio-ui">
|
<Label htmlFor="language-select">
|
||||||
{dict.settings.drawioStyle}
|
{dict.settings.language}
|
||||||
</Label>
|
</Label>
|
||||||
<p className="text-[0.8rem] text-muted-foreground">
|
<p className="text-[0.8rem] text-muted-foreground">
|
||||||
{dict.settings.drawioStyleDescription}{" "}
|
{dict.settings.languageDescription}
|
||||||
{drawioUi === "min"
|
</p>
|
||||||
? dict.settings.minimal
|
</div>
|
||||||
: dict.settings.sketch}
|
<Select value={currentLang} onValueChange={changeLanguage}>
|
||||||
</p>
|
<SelectTrigger id="language-select" className="w-32">
|
||||||
</div>
|
<SelectValue />
|
||||||
<Button
|
</SelectTrigger>
|
||||||
id="drawio-ui"
|
<SelectContent>
|
||||||
variant="outline"
|
{i18n.locales.map((locale) => (
|
||||||
size="sm"
|
<SelectItem key={locale} value={locale}>
|
||||||
onClick={onToggleDrawioUi}
|
{LANGUAGE_LABELS[locale]}
|
||||||
>
|
</SelectItem>
|
||||||
{dict.settings.switchTo}{" "}
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label htmlFor="theme-toggle">
|
||||||
|
{dict.settings.theme}
|
||||||
|
</Label>
|
||||||
|
<p className="text-[0.8rem] text-muted-foreground">
|
||||||
|
{dict.settings.themeDescription}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
id="theme-toggle"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={onToggleDarkMode}
|
||||||
|
>
|
||||||
|
{darkMode ? (
|
||||||
|
<Sun className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<Moon className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label htmlFor="drawio-ui">
|
||||||
|
{dict.settings.drawioStyle}
|
||||||
|
</Label>
|
||||||
|
<p className="text-[0.8rem] text-muted-foreground">
|
||||||
|
{dict.settings.drawioStyleDescription}{" "}
|
||||||
{drawioUi === "min"
|
{drawioUi === "min"
|
||||||
? dict.settings.sketch
|
? dict.settings.minimal
|
||||||
: dict.settings.minimal}
|
: dict.settings.sketch}
|
||||||
</Button>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<Button
|
||||||
|
id="drawio-ui"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={onToggleDrawioUi}
|
||||||
|
>
|
||||||
|
{dict.settings.switchTo}{" "}
|
||||||
|
{drawioUi === "min"
|
||||||
|
? dict.settings.sketch
|
||||||
|
: dict.settings.minimal}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<Label htmlFor="close-protection">
|
<Label htmlFor="close-protection">
|
||||||
{dict.settings.closeProtection}
|
{dict.settings.closeProtection}
|
||||||
</Label>
|
</Label>
|
||||||
<p className="text-[0.8rem] text-muted-foreground">
|
<p className="text-[0.8rem] text-muted-foreground">
|
||||||
{dict.settings.closeProtectionDescription}
|
{dict.settings.closeProtectionDescription}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
<Switch
|
|
||||||
id="close-protection"
|
|
||||||
checked={closeProtection}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
setCloseProtection(checked)
|
|
||||||
localStorage.setItem(
|
|
||||||
STORAGE_CLOSE_PROTECTION_KEY,
|
|
||||||
checked.toString(),
|
|
||||||
)
|
|
||||||
onCloseProtectionChange?.(checked)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<Switch
|
||||||
|
id="close-protection"
|
||||||
|
checked={closeProtection}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
setCloseProtection(checked)
|
||||||
|
localStorage.setItem(
|
||||||
|
STORAGE_CLOSE_PROTECTION_KEY,
|
||||||
|
checked.toString(),
|
||||||
|
)
|
||||||
|
onCloseProtectionChange?.(checked)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-4 border-t border-border/50">
|
</div>
|
||||||
<p className="text-[0.75rem] text-muted-foreground text-center">
|
<div className="pt-4 border-t border-border/50">
|
||||||
Version {process.env.APP_VERSION}
|
<p className="text-[0.75rem] text-muted-foreground text-center">
|
||||||
</p>
|
Version {process.env.APP_VERSION}
|
||||||
</div>
|
</p>
|
||||||
</DialogContent>
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingsDialog(props: SettingsDialogProps) {
|
||||||
|
return (
|
||||||
|
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<DialogContent className="sm:max-w-md">
|
||||||
|
<div className="h-64 flex items-center justify-center">
|
||||||
|
<div className="animate-spin h-8 w-8 border-4 border-primary border-t-transparent rounded-full" />
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SettingsContent {...props} />
|
||||||
|
</Suspense>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"about": "About",
|
"about": "About",
|
||||||
"editor": "Editor",
|
"editor": "Editor",
|
||||||
"newChat": "Start fresh chat",
|
"newChat": "Start fresh chat",
|
||||||
|
"github": "GitHub",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"hidePanel": "Hide chat panel (Ctrl+B)",
|
"hidePanel": "Hide chat panel (Ctrl+B)",
|
||||||
"showPanel": "Show chat panel (Ctrl+B)",
|
"showPanel": "Show chat panel (Ctrl+B)",
|
||||||
@@ -87,6 +88,8 @@
|
|||||||
"overrides": "Overrides",
|
"overrides": "Overrides",
|
||||||
"clearSettings": "Clear Settings",
|
"clearSettings": "Clear Settings",
|
||||||
"useServerDefault": "Use Server Default",
|
"useServerDefault": "Use Server Default",
|
||||||
|
"language": "Language",
|
||||||
|
"languageDescription": "Choose your interface language.",
|
||||||
"theme": "Theme",
|
"theme": "Theme",
|
||||||
"themeDescription": "Dark/Light mode for interface and DrawIO canvas.",
|
"themeDescription": "Dark/Light mode for interface and DrawIO canvas.",
|
||||||
"drawioStyle": "DrawIO Style",
|
"drawioStyle": "DrawIO Style",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"about": "概要",
|
"about": "概要",
|
||||||
"editor": "エディタ",
|
"editor": "エディタ",
|
||||||
"newChat": "新しいチャットを開始",
|
"newChat": "新しいチャットを開始",
|
||||||
|
"github": "GitHub",
|
||||||
"settings": "設定",
|
"settings": "設定",
|
||||||
"hidePanel": "チャットパネルを非表示 (Ctrl+B)",
|
"hidePanel": "チャットパネルを非表示 (Ctrl+B)",
|
||||||
"showPanel": "チャットパネルを表示 (Ctrl+B)",
|
"showPanel": "チャットパネルを表示 (Ctrl+B)",
|
||||||
@@ -87,6 +88,8 @@
|
|||||||
"overrides": "上書き",
|
"overrides": "上書き",
|
||||||
"clearSettings": "設定をクリア",
|
"clearSettings": "設定をクリア",
|
||||||
"useServerDefault": "サーバーデフォルトを使用",
|
"useServerDefault": "サーバーデフォルトを使用",
|
||||||
|
"language": "言語",
|
||||||
|
"languageDescription": "インターフェース言語を選択します。",
|
||||||
"theme": "テーマ",
|
"theme": "テーマ",
|
||||||
"themeDescription": "インターフェースと DrawIO キャンバスのダーク/ライトモード。",
|
"themeDescription": "インターフェースと DrawIO キャンバスのダーク/ライトモード。",
|
||||||
"drawioStyle": "DrawIO スタイル",
|
"drawioStyle": "DrawIO スタイル",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"about": "关于",
|
"about": "关于",
|
||||||
"editor": "编辑器",
|
"editor": "编辑器",
|
||||||
"newChat": "开始新对话",
|
"newChat": "开始新对话",
|
||||||
|
"github": "GitHub",
|
||||||
"settings": "设置",
|
"settings": "设置",
|
||||||
"hidePanel": "隐藏聊天面板 (Ctrl+B)",
|
"hidePanel": "隐藏聊天面板 (Ctrl+B)",
|
||||||
"showPanel": "显示聊天面板 (Ctrl+B)",
|
"showPanel": "显示聊天面板 (Ctrl+B)",
|
||||||
@@ -87,6 +88,8 @@
|
|||||||
"overrides": "覆盖",
|
"overrides": "覆盖",
|
||||||
"clearSettings": "清除设置",
|
"clearSettings": "清除设置",
|
||||||
"useServerDefault": "使用服务器默认值",
|
"useServerDefault": "使用服务器默认值",
|
||||||
|
"language": "语言",
|
||||||
|
"languageDescription": "选择界面语言。",
|
||||||
"theme": "主题",
|
"theme": "主题",
|
||||||
"themeDescription": "界面和 DrawIO 画布的深色/浅色模式。",
|
"themeDescription": "界面和 DrawIO 画布的深色/浅色模式。",
|
||||||
"drawioStyle": "DrawIO 样式",
|
"drawioStyle": "DrawIO 样式",
|
||||||
|
|||||||
Reference in New Issue
Block a user