Compare commits

..

26 Commits

Author SHA1 Message Date
shibamudi
43161977a5 Adds pathname to effect dependencies
Ensures the effect hook updates correctly when the pathname changes,
improving synchronization with route-based state changes.
2026-01-29 23:55:12 +09:00
shibamudi
f09d2c100a Uses dynamic API endpoint for URL parsing
Replaces hardcoded API path with a dynamic endpoint resolver to support flexible deployments and ensure correct API routing in different environments.
2026-01-29 23:55:12 +09:00
shibamudi
03e2d9da9d Uses dynamic API base path for model fetch
Replaces the hardcoded API endpoint with a function that retrieves
the correct base path for server model requests. Improves compatibility
with deployments where the API is not served from the root path.
2026-01-29 23:55:12 +09:00
shibamudi
bd1e5cc237 Refactors image component to support ref forwarding
Enables ref forwarding for improved integration with parent components
and libraries that require direct DOM access. Enhances flexibility and
maintainability by switching to a forwardRef implementation.
2026-01-29 23:55:12 +09:00
十八亩地
edc10803e4 Update components/image-with-basepath.tsx
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-29 23:55:12 +09:00
十八亩地
b074cee95c Update components/chat-panel.tsx
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-29 23:55:12 +09:00
十八亩地
121b6fc99f Update components/image-with-basepath.tsx
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-29 23:55:12 +09:00
十八亩地
0e1bed39a4 Update components/image-with-basepath.tsx
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-29 23:55:12 +09:00
十八亩地
d5e2885623 Update import path for getAssetUrl function 2026-01-29 23:55:11 +09:00
十八亩地
7662b00bb3 Update components/image-with-basepath.tsx
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-29 23:55:11 +09:00
十八亩地
8a93d4ea9f Update components/image-with-basepath.tsx
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-29 23:55:11 +09:00
shibamudi
add9111c82 fix: update image imports to use custom Image component with base path handling 2026-01-29 23:55:11 +09:00
shibamudi
1c1c6a4d38 fix(chat-panel): update path handling with usePathname for URL management
When `basePath` is set, using `window.location.pathname` causes the `basePath` to be duplicated.
2026-01-29 23:55:11 +09:00
Dayuan Jiang
31fffcc52d Merge pull request #664 from DayuanJiang/fix/ollama-client-base-url
Supports client-provided base URL for Ollama
2026-01-29 21:57:05 +09:00
Dayuan Jiang
0a03ac9f45 Merge pull request #663 from DayuanJiang/fix/ollama-allowed-client-provider
Adds Ollama to allowed client providers list
2026-01-29 21:57:00 +09:00
Dayuan Jiang
6dad6ab147 Merge pull request #662 from DayuanJiang/fix/ollama-ssrf-exception
Adds Ollama to SSRF protection exception list
2026-01-29 21:56:54 +09:00
dayuan.jiang
8781520ebb Supports client-provided base URL for Ollama
Previously, Ollama only used the OLLAMA_BASE_URL environment variable.
Now client-provided base URL from settings takes priority, allowing
users to configure custom Ollama endpoints (e.g., remote servers).

Fixes #652
2026-01-29 21:25:20 +09:00
dayuan.jiang
67196225f0 Adds Ollama to allowed client providers list
Allows users to select Ollama as a provider from client settings.
Previously, Ollama was blocked with "Invalid provider" error even
though the UI supported it.

Fixes #652
2026-01-29 21:24:48 +09:00
dayuan.jiang
09a5774cba Adds Ollama to SSRF protection exception list
Ollama is a local/self-hosted model that doesn't require API keys.
The SSRF protection was incorrectly blocking Ollama connections
when users provided a custom base URL without an API key.

Fixes #652
2026-01-29 21:24:09 +09:00
broBinChen
fdd4be6463 fix(i18n): add missing translation keys in zh-Hant.json (#659)
* fix(i18n): add missing translation keys in zh-Hant.json

* fix(security): update next to fix high severity vulnerabilities

---------

Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-01-29 20:33:03 +09:00
Marvelous Ikponmwosa
1258f98478 feat: Add "API Keys & Models" link in Settings Dialog for better discoverability (#645)
* added api key and model in settings dialog

* added aria-label

* removed configure option from model-selector.tsx
2026-01-28 22:36:54 +09:00
yujinze
f6f693ee8d Merge pull request #650 from DayuanJiang/feat/add-zh-hant-locale
feat(i18n): add Traditional Chinese (zh-Hant) locale
2026-01-27 20:02:02 +09:00
Jinze Yu
9398117368 feat(i18n): add Traditional Chinese (zh-Hant) locale
Add full Traditional Chinese support for Hong Kong/Taiwan users by
creating a zh-Hant dictionary and registering the locale across the
web app, metadata, and Electron desktop menu system.
2026-01-27 13:22:33 +09:00
Dayuan Jiang
cb0c0fbcda Revert "fix(electron): prevent beforeunload prompt by using autosave (#642)" (#646)
This reverts commit e7c29fb410.

The PR introduced an IndexedDB error: 'Failed to execute transaction on IDBDatabase: The database connection is closing.'
2026-01-27 09:20:42 +09:00
Dayuan Jiang
e7c29fb410 fix(electron): prevent beforeunload prompt by using autosave (#642)
* fix(electron): prevent beforeunload prompt by using autosave

- Enable draw.io autosave and handle autosave events to update chartXML
- Clear modified state after autosave to avoid beforeunload prompts
- Disable confirmExit in draw.io configuration
- Set modified=false and keepmodified=false URL parameters
- Fix session save condition to also save when only diagram exists

* fix: persist diagram-only saves and ref typing

* fix: harden persistence checks and export timeout
2026-01-26 22:04:41 +09:00
Subhajeetch
f0dd199cd1 feat(prompt): add language-aware response rules with english fallback (#641)
* feat(prompt): add language-aware response rules with english fallback

Added language handling rules for user interactions.

* refactor(prompt): simplify language matching instruction
2026-01-26 15:32:07 +09:00
32 changed files with 761 additions and 433 deletions

View File

@@ -1,7 +1,7 @@
import type { Metadata } from "next" import type { Metadata } from "next"
import Image from "next/image"
import Link from "next/link" import Link from "next/link"
import { FaGithub } from "react-icons/fa" import { FaGithub } from "react-icons/fa"
import Image from "@/components/image-with-basepath"
export const metadata: Metadata = { export const metadata: Metadata = {
title: "关于 - Next AI Draw.io", title: "关于 - Next AI Draw.io",

View File

@@ -1,7 +1,7 @@
import type { Metadata } from "next" import type { Metadata } from "next"
import Image from "next/image"
import Link from "next/link" import Link from "next/link"
import { FaGithub } from "react-icons/fa" import { FaGithub } from "react-icons/fa"
import Image from "@/components/image-with-basepath"
export const metadata: Metadata = { export const metadata: Metadata = {
title: "概要 - Next AI Draw.io", title: "概要 - Next AI Draw.io",

View File

@@ -1,7 +1,7 @@
import type { Metadata } from "next" import type { Metadata } from "next"
import Image from "next/image"
import Link from "next/link" import Link from "next/link"
import { FaGithub } from "react-icons/fa" import { FaGithub } from "react-icons/fa"
import Image from "@/components/image-with-basepath"
export const metadata: Metadata = { export const metadata: Metadata = {
title: "About - Next AI Draw.io", title: "About - Next AI Draw.io",

View File

@@ -41,19 +41,24 @@ export async function generateMetadata({
params: Promise<{ lang: string }> params: Promise<{ lang: string }>
}): Promise<Metadata> { }): Promise<Metadata> {
const { lang: rawLang } = await params const { lang: rawLang } = await params
const lang = (rawLang in { en: 1, zh: 1, ja: 1 } ? rawLang : "en") as Locale const lang = (
rawLang in { en: 1, zh: 1, ja: 1, "zh-Hant": 1 } ? rawLang : "en"
) as Locale
// Default to English metadata // Default to English metadata
const titles: Record<Locale, string> = { const titles: Record<Locale, string> = {
en: "Next AI Draw.io - AI-Powered Diagram Generator", en: "Next AI Draw.io - AI-Powered Diagram Generator",
zh: "Next AI Draw.io - AI powered diagram generator", zh: "Next AI Draw.io - AI powered diagram generator",
ja: "Next AI Draw.io - AI-powered diagram generator", ja: "Next AI Draw.io - AI-powered diagram generator",
"zh-Hant": "Next AI Draw.io - AI 驅動的圖表產生器",
} }
const descriptions: Record<Locale, string> = { const descriptions: Record<Locale, string> = {
en: "Create AWS architecture diagrams, flowcharts, and technical diagrams using AI. Free online tool integrating draw.io with AI assistance for professional diagram creation.", en: "Create AWS architecture diagrams, flowcharts, and technical diagrams using AI. Free online tool integrating draw.io with AI assistance for professional diagram creation.",
zh: "Use AI to create AWS architecture diagrams, flowcharts, and technical diagrams. Free online tool integrated with draw.io and AI assistance for professional diagram creation.", zh: "Use AI to create AWS architecture diagrams, flowcharts, and technical diagrams. Free online tool integrated with draw.io and AI assistance for professional diagram creation.",
ja: "Create AWS architecture diagrams, flowcharts, and technical diagrams using AI. Create professional diagrams with a free online tool that integrates draw.io with an AI assistant.", ja: "Create AWS architecture diagrams, flowcharts, and technical diagrams using AI. Create professional diagrams with a free online tool that integrates draw.io with an AI assistant.",
"zh-Hant":
"使用 AI 建立 AWS 架構圖、流程圖和技術圖表。免費線上工具整合 draw.io 與 AI 輔助,輕鬆建立專業圖表。",
} }
return { return {
@@ -80,7 +85,14 @@ export async function generateMetadata({
type: "website", type: "website",
url: "https://next-ai-drawio.jiang.jp", url: "https://next-ai-drawio.jiang.jp",
siteName: "Next AI Draw.io", siteName: "Next AI Draw.io",
locale: lang === "zh" ? "zh_CN" : lang === "ja" ? "ja_JP" : "en_US", locale:
lang === "zh"
? "zh_CN"
: lang === "zh-Hant"
? "zh_HK"
: lang === "ja"
? "ja_JP"
: "en_US",
images: [ images: [
{ {
url: "/architecture.png", url: "/architecture.png",
@@ -115,6 +127,7 @@ export async function generateMetadata({
en: "/en", en: "/en",
zh: "/zh", zh: "/zh",
ja: "/ja", ja: "/ja",
"zh-Hant": "/zh-Hant",
}, },
}, },
} }

View File

@@ -11,16 +11,10 @@ import {
} from "@/components/ui/resizable" } from "@/components/ui/resizable"
import { useDiagram } from "@/contexts/diagram-context" import { useDiagram } from "@/contexts/diagram-context"
import { i18n, type Locale } from "@/lib/i18n/config" import { i18n, type Locale } from "@/lib/i18n/config"
import { isIndexedDBUsable } from "@/lib/session-storage"
export default function Home() { export default function Home() {
const { const { drawioRef, handleDiagramExport, onDrawioLoad, resetDrawioReady } =
drawioRef, useDiagram()
handleDiagramExport,
handleDiagramAutoSave,
onDrawioLoad,
resetDrawioReady,
} = useDiagram()
const router = useRouter() const router = useRouter()
const pathname = usePathname() const pathname = usePathname()
// Extract current language from pathname (e.g., "/zh/about" → "zh") // Extract current language from pathname (e.g., "/zh/about" → "zh")
@@ -32,7 +26,6 @@ export default function Home() {
const [isLoaded, setIsLoaded] = useState(false) const [isLoaded, setIsLoaded] = useState(false)
const [isDrawioReady, setIsDrawioReady] = useState(false) const [isDrawioReady, setIsDrawioReady] = useState(false)
const [isElectron, setIsElectron] = useState(false) const [isElectron, setIsElectron] = useState(false)
const [canPersist, setCanPersist] = useState(false)
const [drawioBaseUrl, setDrawioBaseUrl] = useState( const [drawioBaseUrl, setDrawioBaseUrl] = useState(
process.env.NEXT_PUBLIC_DRAWIO_BASE_URL || "https://embed.diagrams.net", process.env.NEXT_PUBLIC_DRAWIO_BASE_URL || "https://embed.diagrams.net",
) )
@@ -83,10 +76,6 @@ export default function Home() {
setDrawioBaseUrl(`${window.location.origin}/drawio/index.html`) setDrawioBaseUrl(`${window.location.origin}/drawio/index.html`)
} }
void (async () => {
const usable = await isIndexedDBUsable()
setCanPersist(usable)
})()
setIsLoaded(true) setIsLoaded(true)
}, [pathname, router]) }, [pathname, router])
@@ -95,17 +84,6 @@ export default function Home() {
onDrawioLoad() onDrawioLoad()
}, [onDrawioLoad]) }, [onDrawioLoad])
const handleDrawioAutoSave = useCallback(
(data: { xml?: string }) => {
handleDiagramAutoSave(data)
// Only suppress modified state when persistence is available
if (canPersist) {
drawioRef.current?.status({ message: "", modified: false })
}
},
[canPersist, drawioRef, handleDiagramAutoSave],
)
const handleDarkModeChange = () => { const handleDarkModeChange = () => {
const newValue = !darkMode const newValue = !darkMode
setDarkMode(newValue) setDarkMode(newValue)
@@ -196,25 +174,13 @@ export default function Home() {
<DrawIoEmbed <DrawIoEmbed
key={`${drawioUi}-${darkMode}-${currentLang}-${isElectron}`} key={`${drawioUi}-${darkMode}-${currentLang}-${isElectron}`}
ref={drawioRef} ref={drawioRef}
autosave
onAutoSave={handleDrawioAutoSave}
onExport={handleDiagramExport} onExport={handleDiagramExport}
onLoad={handleDrawioLoad} onLoad={handleDrawioLoad}
baseUrl={drawioBaseUrl} baseUrl={drawioBaseUrl}
configuration={
canPersist
? { confirmExit: false }
: undefined
}
urlParameters={{ urlParameters={{
ui: drawioUi, ui: drawioUi,
spin: false, spin: false,
libraries: false, libraries: false,
// Disable modified tracking only when persistence is available
...(canPersist && {
modified: false,
keepmodified: false,
}),
saveAndExit: false, saveAndExit: false,
noSaveBtn: true, noSaveBtn: true,
noExitBtn: true, noExitBtn: true,

View File

@@ -170,7 +170,6 @@ interface ChatInputProps {
selectedModelId?: string selectedModelId?: string
onModelSelect?: (modelId: string | undefined) => void onModelSelect?: (modelId: string | undefined) => void
showUnvalidatedModels?: boolean showUnvalidatedModels?: boolean
onConfigureModels?: () => void
// Focus control props // Focus control props
shouldFocus?: boolean shouldFocus?: boolean
onFocused?: () => void onFocused?: () => void
@@ -194,7 +193,6 @@ export const ChatInput = forwardRef<ChatInputRef, ChatInputProps>(
selectedModelId, selectedModelId,
onModelSelect = () => {}, onModelSelect = () => {},
showUnvalidatedModels = false, showUnvalidatedModels = false,
onConfigureModels = () => {},
shouldFocus = false, shouldFocus = false,
onFocused, onFocused,
}, },
@@ -550,7 +548,6 @@ export const ChatInput = forwardRef<ChatInputRef, ChatInputProps>(
models={models} models={models}
selectedModelId={selectedModelId} selectedModelId={selectedModelId}
onSelect={onModelSelect} onSelect={onModelSelect}
onConfigure={onConfigureModels}
disabled={isDisabled} disabled={isDisabled}
showUnvalidatedModels={showUnvalidatedModels} showUnvalidatedModels={showUnvalidatedModels}
/> />

View File

@@ -16,7 +16,6 @@ import {
ThumbsUp, ThumbsUp,
X, X,
} from "lucide-react" } from "lucide-react"
import Image from "next/image"
import type { MutableRefObject } from "react" import type { MutableRefObject } from "react"
import { useCallback, useEffect, useRef, useState } from "react" import { useCallback, useEffect, useRef, useState } from "react"
import ReactMarkdown from "react-markdown" import ReactMarkdown from "react-markdown"
@@ -31,6 +30,7 @@ import { ToolCallCard } from "@/components/chat/ToolCallCard"
import type { DiagramOperation, ToolPartLike } from "@/components/chat/types" import type { DiagramOperation, ToolPartLike } from "@/components/chat/types"
import type { ValidationState } from "@/components/chat/ValidationCard" import type { ValidationState } from "@/components/chat/ValidationCard"
import { ValidationCard } from "@/components/chat/ValidationCard" import { ValidationCard } from "@/components/chat/ValidationCard"
import Image from "@/components/image-with-basepath"
import { ScrollArea } from "@/components/ui/scroll-area" import { ScrollArea } from "@/components/ui/scroll-area"
import { useDictionary } from "@/hooks/use-dictionary" import { useDictionary } from "@/hooks/use-dictionary"
import { getApiEndpoint } from "@/lib/base-path" import { getApiEndpoint } from "@/lib/base-path"

View File

@@ -8,8 +8,7 @@ import {
PanelRightOpen, PanelRightOpen,
Settings, Settings,
} from "lucide-react" } from "lucide-react"
import Image from "next/image" import { usePathname, useRouter, useSearchParams } from "next/navigation"
import { useRouter, useSearchParams } from "next/navigation"
import type React from "react" import type React from "react"
import { import {
useCallback, useCallback,
@@ -22,6 +21,7 @@ import { flushSync } from "react-dom"
import { Toaster, toast } from "sonner" import { Toaster, toast } from "sonner"
import { ButtonWithTooltip } from "@/components/button-with-tooltip" import { ButtonWithTooltip } from "@/components/button-with-tooltip"
import { ChatInput } from "@/components/chat-input" import { ChatInput } from "@/components/chat-input"
import Image from "@/components/image-with-basepath"
import { ModelConfigDialog } from "@/components/model-config-dialog" import { ModelConfigDialog } from "@/components/model-config-dialog"
import { SettingsDialog } from "@/components/settings-dialog" import { SettingsDialog } from "@/components/settings-dialog"
import { useDiagram } from "@/contexts/diagram-context" import { useDiagram } from "@/contexts/diagram-context"
@@ -131,28 +131,33 @@ export default function ChatPanel({
const dict = useDictionary() const dict = useDictionary()
const router = useRouter() const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams() const searchParams = useSearchParams()
const urlSessionId = searchParams.get("session") const urlSessionId = searchParams.get("session")
const onFetchChart = (saveToHistory = true) => { const onFetchChart = (saveToHistory = true) => {
return Promise.race([ return Promise.race([
new Promise<string>((resolve) => { new Promise<string>((resolve) => {
resolverRef.current = resolve if (resolverRef && "current" in resolverRef) {
resolverRef.current = resolve
}
if (saveToHistory) { if (saveToHistory) {
onExport() onExport()
} else { } else {
handleExportWithoutHistory() handleExportWithoutHistory()
} }
}), }),
new Promise<string>((_, reject) => { new Promise<string>((_, reject) =>
const currentResolver = resolverRef.current setTimeout(
setTimeout(() => { () =>
if (resolverRef.current === currentResolver) { reject(
resolverRef.current = null new Error(
} "Chart export timed out after 10 seconds",
reject(new Error("Chart export timed out after 10 seconds")) ),
}, 10000) ),
}), 10000,
),
),
]) ])
} }
@@ -673,7 +678,7 @@ export default function ChatPanel({
// Debounce: save after 1 second of no changes // Debounce: save after 1 second of no changes
localStorageDebounceRef.current = setTimeout(async () => { localStorageDebounceRef.current = setTimeout(async () => {
try { try {
if (messages.length > 0 || hasDiagramNow) { if (messages.length > 0) {
const sessionData = await buildSessionData({ const sessionData = await buildSessionData({
// Only capture thumbnail if there was a diagram AND this isn't a no-diagram session // Only capture thumbnail if there was a diagram AND this isn't a no-diagram session
withThumbnail: hasDiagramNow && !isNodiagramSession, withThumbnail: hasDiagramNow && !isNodiagramSession,
@@ -695,7 +700,6 @@ export default function ChatPanel({
} }
} }
}, [ }, [
chartXML,
messages, messages,
status, status,
sessionIsAvailable, sessionIsAvailable,
@@ -726,8 +730,7 @@ export default function ChatPanel({
const handleVisibilityChange = async () => { const handleVisibilityChange = async () => {
if ( if (
document.visibilityState === "hidden" && document.visibilityState === "hidden" &&
(messagesRef.current.length > 0 || messagesRef.current.length > 0
isRealDiagram(chartXMLRef.current))
) { ) {
try { try {
// Attempt to save session - browser may not wait for completion // Attempt to save session - browser may not wait for completion
@@ -899,10 +902,10 @@ export default function ChatPanel({
if (result.wasCurrentSession) { if (result.wasCurrentSession) {
// Deleted current session - clear UI and URL // Deleted current session - clear UI and URL
syncUIWithSession(null) syncUIWithSession(null)
router.replace(window.location.pathname, { scroll: false }) router.replace(pathname, { scroll: false })
} }
}, },
[sessionManager, syncUIWithSession, router], [sessionManager, syncUIWithSession, router, pathname],
) )
const handleNewChat = useCallback(async () => { const handleNewChat = useCallback(async () => {
@@ -935,7 +938,7 @@ export default function ChatPanel({
toast.success(dict.dialogs.clearSuccess) toast.success(dict.dialogs.clearSuccess)
// Clear URL param to show blank state // Clear URL param to show blank state
router.replace(window.location.pathname, { scroll: false }) router.replace(pathname, { scroll: false })
// After starting a fresh chat, move focus back to the chat input // After starting a fresh chat, move focus back to the chat input
setShouldFocusInput(true) setShouldFocusInput(true)
@@ -950,6 +953,7 @@ export default function ChatPanel({
dict.dialogs.clearSuccess, dict.dialogs.clearSuccess,
buildSessionData, buildSessionData,
setDiagramHistory, setDiagramHistory,
pathname,
]) ])
const handleInputChange = ( const handleInputChange = (
@@ -1366,7 +1370,6 @@ export default function ChatPanel({
selectedModelId={modelConfig.selectedModelId} selectedModelId={modelConfig.selectedModelId}
onModelSelect={modelConfig.setSelectedModelId} onModelSelect={modelConfig.setSelectedModelId}
showUnvalidatedModels={modelConfig.showUnvalidatedModels} showUnvalidatedModels={modelConfig.showUnvalidatedModels}
onConfigureModels={() => setShowModelConfigDialog(true)}
shouldFocus={shouldFocusInput} shouldFocus={shouldFocusInput}
onFocused={() => setShouldFocusInput(false)} onFocused={() => setShouldFocusInput(false)}
/> />
@@ -1383,6 +1386,7 @@ export default function ChatPanel({
onMinimalStyleChange={setMinimalStyle} onMinimalStyleChange={setMinimalStyle}
vlmValidationEnabled={vlmValidationEnabled} vlmValidationEnabled={vlmValidationEnabled}
onVlmValidationChange={handleVlmValidationChange} onVlmValidationChange={handleVlmValidationChange}
onOpenModelConfig={() => setShowModelConfigDialog(true)}
/> />
<ModelConfigDialog <ModelConfigDialog

View File

@@ -8,9 +8,9 @@ import {
Trash2, Trash2,
X, X,
} from "lucide-react" } from "lucide-react"
import Image from "next/image"
import { useState } from "react" import { useState } from "react"
import ExamplePanel from "@/components/chat-example-panel" import ExamplePanel from "@/components/chat-example-panel"
import Image from "@/components/image-with-basepath"
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,

View File

@@ -10,8 +10,8 @@ import {
RefreshCw, RefreshCw,
X, X,
} from "lucide-react" } from "lucide-react"
import Image from "next/image"
import { useState } from "react" import { useState } from "react"
import Image from "@/components/image-with-basepath"
import { useDictionary } from "@/hooks/use-dictionary" import { useDictionary } from "@/hooks/use-dictionary"
import type { ValidationResult } from "@/lib/diagram-validator" import type { ValidationResult } from "@/lib/diagram-validator"

View File

@@ -1,8 +1,8 @@
"use client" "use client"
import { FileCode, FileText, Link, Loader2, X } from "lucide-react" import { FileCode, FileText, Link, Loader2, X } from "lucide-react"
import Image from "next/image"
import { useEffect, useRef, useState } from "react" import { useEffect, useRef, useState } from "react"
import Image from "@/components/image-with-basepath"
import { useDictionary } from "@/hooks/use-dictionary" import { useDictionary } from "@/hooks/use-dictionary"
import { isPdfFile, isTextFile } from "@/lib/pdf-utils" import { isPdfFile, isTextFile } from "@/lib/pdf-utils"

View File

@@ -1,7 +1,7 @@
"use client" "use client"
import Image from "next/image"
import { useState } from "react" import { useState } from "react"
import Image from "@/components/image-with-basepath"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { import {
Dialog, Dialog,

View File

@@ -0,0 +1,16 @@
import NextImage, { type ImageProps } from "next/image"
import { forwardRef } from "react"
import { getAssetUrl } from "@/lib/base-path"
export default forwardRef<HTMLImageElement, ImageProps>(
function Image(props, ref) {
const src =
typeof props.src === "string" &&
props.src.startsWith("/") &&
!props.src.startsWith("//")
? getAssetUrl(props.src)
: props.src
return <NextImage {...props} src={src} ref={ref} />
},
)

View File

@@ -7,7 +7,6 @@ import {
ChevronDown, ChevronDown,
Monitor, Monitor,
Server, Server,
Settings2,
User, User,
} from "lucide-react" } from "lucide-react"
import { useEffect, useMemo, useRef, useState } from "react" import { useEffect, useMemo, useRef, useState } from "react"
@@ -34,7 +33,6 @@ interface ModelSelectorProps {
models: FlattenedModel[] models: FlattenedModel[]
selectedModelId: string | undefined selectedModelId: string | undefined
onSelect: (modelId: string | undefined) => void onSelect: (modelId: string | undefined) => void
onConfigure: () => void
disabled?: boolean disabled?: boolean
showUnvalidatedModels?: boolean showUnvalidatedModels?: boolean
} }
@@ -85,7 +83,6 @@ export function ModelSelector({
models, models,
selectedModelId, selectedModelId,
onSelect, onSelect,
onConfigure,
disabled = false, disabled = false,
showUnvalidatedModels = false, showUnvalidatedModels = false,
}: ModelSelectorProps) { }: ModelSelectorProps) {
@@ -126,9 +123,7 @@ export function ModelSelector({
) )
const handleSelect = (value: string) => { const handleSelect = (value: string) => {
if (value === "__configure__") { if (value === "__server_default__") {
onConfigure()
} else if (value === "__server_default__") {
onSelect(undefined) onSelect(undefined)
} else { } else {
onSelect(value) onSelect(value)
@@ -386,20 +381,6 @@ export function ModelSelector({
</> </>
)} )}
{/* Configure Option */}
<ModelSelectorSeparator />
<ModelSelectorGroup>
<ModelSelectorItem
value="__configure__"
onSelect={handleSelect}
className="cursor-pointer text-muted-foreground hover:text-foreground"
>
<Settings2 className="mr-2 h-4 w-4" />
<ModelSelectorName>
{dict.modelConfig.configureModels}
</ModelSelectorName>
</ModelSelectorItem>
</ModelSelectorGroup>
{/* Info text */} {/* Info text */}
<div className="px-3 py-2 text-xs text-muted-foreground border-t"> <div className="px-3 py-2 text-xs text-muted-foreground border-t">
{showUnvalidatedModels {showUnvalidatedModels

View File

@@ -1,6 +1,6 @@
"use client" "use client"
import { Github, Info, Moon, Sun, Tag } from "lucide-react" import { ChevronRight, Github, Info, Moon, Sun, Tag } from "lucide-react"
import { usePathname, useRouter, useSearchParams } from "next/navigation" import { usePathname, useRouter, useSearchParams } from "next/navigation"
import { Suspense, useEffect, useState } from "react" import { Suspense, useEffect, useState } from "react"
import { toast } from "sonner" import { toast } from "sonner"
@@ -56,6 +56,7 @@ const LANGUAGE_LABELS: Record<Locale, string> = {
en: "English", en: "English",
zh: "中文", zh: "中文",
ja: "日本語", ja: "日本語",
"zh-Hant": "繁體中文",
} }
interface SettingsDialogProps { interface SettingsDialogProps {
@@ -69,6 +70,7 @@ interface SettingsDialogProps {
onMinimalStyleChange?: (value: boolean) => void onMinimalStyleChange?: (value: boolean) => void
vlmValidationEnabled?: boolean vlmValidationEnabled?: boolean
onVlmValidationChange?: (value: boolean) => void onVlmValidationChange?: (value: boolean) => void
onOpenModelConfig?: () => void
} }
export const STORAGE_ACCESS_CODE_KEY = "next-ai-draw-io-access-code" export const STORAGE_ACCESS_CODE_KEY = "next-ai-draw-io-access-code"
@@ -92,6 +94,7 @@ function SettingsContent({
onMinimalStyleChange = () => {}, onMinimalStyleChange = () => {},
vlmValidationEnabled = false, vlmValidationEnabled = false,
onVlmValidationChange = () => {}, onVlmValidationChange = () => {},
onOpenModelConfig,
}: SettingsDialogProps) { }: SettingsDialogProps) {
const dict = useDictionary() const dict = useDictionary()
const router = useRouter() const router = useRouter()
@@ -283,6 +286,27 @@ function SettingsContent({
{/* Content */} {/* Content */}
<div className="px-6 pb-6"> <div className="px-6 pb-6">
<div className="divide-y divide-border-subtle"> <div className="divide-y divide-border-subtle">
{/* API Keys & Models */}
{onOpenModelConfig && (
<SettingItem
label={dict.settings.apiKeysModels}
description={dict.settings.apiKeysModelsDescription}
>
<Button
variant="ghost"
size="sm"
className="h-9 w-9 p-0"
onClick={() => {
onOpenChange(false)
onOpenModelConfig()
}}
aria-label={dict.settings.apiKeysModels}
>
<ChevronRight className="h-4 w-4" />
</Button>
</SettingItem>
)}
{/* Access Code (conditional) */} {/* Access Code (conditional) */}
{accessCodeRequired && ( {accessCodeRequired && (
<div className="py-4 first:pt-0 space-y-3"> <div className="py-4 first:pt-0 space-y-3">

View File

@@ -20,10 +20,9 @@ interface DiagramContextType {
loadDiagram: (chart: string, skipValidation?: boolean) => string | null loadDiagram: (chart: string, skipValidation?: boolean) => string | null
handleExport: () => void handleExport: () => void
handleExportWithoutHistory: () => void handleExportWithoutHistory: () => void
resolverRef: React.MutableRefObject<((value: string) => void) | null> resolverRef: React.Ref<((value: string) => void) | null>
drawioRef: React.MutableRefObject<DrawIoEmbedRef | null> drawioRef: React.Ref<DrawIoEmbedRef | null>
handleDiagramExport: (data: any) => void handleDiagramExport: (data: any) => void
handleDiagramAutoSave: (data: { xml?: string }) => void
clearDiagram: () => void clearDiagram: () => void
saveDiagramToFile: ( saveDiagramToFile: (
filename: string, filename: string,
@@ -268,11 +267,6 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
} }
} }
const handleDiagramAutoSave = (data: { xml?: string }) => {
if (!data?.xml) return
setChartXML(data.xml)
}
const clearDiagram = () => { const clearDiagram = () => {
const emptyDiagram = `<mxfile><diagram name="Page-1" id="page-1"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel></diagram></mxfile>` const emptyDiagram = `<mxfile><diagram name="Page-1" id="page-1"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel></diagram></mxfile>`
// Skip validation for trusted internal template (loadDiagram also sets chartXML) // Skip validation for trusted internal template (loadDiagram also sets chartXML)
@@ -397,7 +391,6 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
resolverRef, resolverRef,
drawioRef, drawioRef,
handleDiagramExport, handleDiagramExport,
handleDiagramAutoSave,
clearDiagram, clearDiagram,
saveDiagramToFile, saveDiagramToFile,
getThumbnailSvg, getThumbnailSvg,

View File

@@ -69,7 +69,9 @@ declare global {
/** Set proxy configuration (saves and restarts server) */ /** Set proxy configuration (saves and restarts server) */
setProxy: (config: ProxyConfig) => Promise<SetProxyResult> setProxy: (config: ProxyConfig) => Promise<SetProxyResult>
/** Get user's preferred locale */ /** Get user's preferred locale */
getUserLocale: () => Promise<"en" | "zh" | "ja" | undefined> getUserLocale: () => Promise<
"en" | "zh" | "ja" | "zh-Hant" | undefined
>
/** Set user's preferred locale */ /** Set user's preferred locale */
setUserLocale: (locale: string) => Promise<SetUserLocaleResult> setUserLocale: (locale: string) => Promise<SetUserLocaleResult>
} }

View File

@@ -137,7 +137,7 @@ interface ConfigPresetsFile {
version: 1 version: 1
currentPresetId: string | null currentPresetId: string | null
presets: ConfigPreset[] presets: ConfigPreset[]
userLocale?: "en" | "zh" | "ja" userLocale?: "en" | "zh" | "ja" | "zh-Hant"
} }
const CONFIG_FILE_NAME = "config-presets.json" const CONFIG_FILE_NAME = "config-presets.json"
@@ -470,7 +470,7 @@ export function getCurrentPresetEnv(): Record<string, string> {
* Get user's preferred locale from config * Get user's preferred locale from config
* Returns undefined if not set * Returns undefined if not set
*/ */
export function getUserLocale(): "en" | "zh" | "ja" | undefined { export function getUserLocale(): "en" | "zh" | "ja" | "zh-Hant" | undefined {
const data = loadPresets() const data = loadPresets()
return data.userLocale return data.userLocale
} }
@@ -478,7 +478,9 @@ export function getUserLocale(): "en" | "zh" | "ja" | undefined {
/** /**
* Set user's preferred locale in config * Set user's preferred locale in config
*/ */
export function setUserLocale(locale: "en" | "zh" | "ja" | null): void { export function setUserLocale(
locale: "en" | "zh" | "ja" | "zh-Hant" | null,
): void {
const data = loadPresets() const data = loadPresets()
data.userLocale = locale === null ? undefined : locale data.userLocale = locale === null ? undefined : locale
savePresets(data) savePresets(data)

View File

@@ -263,12 +263,12 @@ export function registerIpcHandlers(): void {
ipcMain.handle("set-user-locale", (_event, locale: string) => { ipcMain.handle("set-user-locale", (_event, locale: string) => {
// Validate locale is one of the supported values // Validate locale is one of the supported values
if (!["en", "zh", "ja"].includes(locale)) { if (!["en", "zh", "ja", "zh-Hant"].includes(locale)) {
return { success: false, error: "Invalid locale" } return { success: false, error: "Invalid locale" }
} }
try { try {
setUserLocale(locale as "en" | "zh" | "ja") setUserLocale(locale as "en" | "zh" | "ja" | "zh-Hant")
// Rebuild the menu to reflect the new locale // Rebuild the menu to reflect the new locale
rebuildAppMenu() rebuildAppMenu()
return { success: true } return { success: true }

View File

@@ -5,7 +5,7 @@
import { getUserLocale } from "./config-manager" import { getUserLocale } from "./config-manager"
export type MenuLocale = "en" | "zh" | "ja" export type MenuLocale = "en" | "zh" | "ja" | "zh-Hant"
export interface MenuTranslations { export interface MenuTranslations {
// App menu (macOS only) // App menu (macOS only)
@@ -119,6 +119,34 @@ const translations: Record<MenuLocale, MenuTranslations> = {
documentation: "ドキュメント", documentation: "ドキュメント",
reportIssue: "問題を報告", reportIssue: "問題を報告",
}, },
"zh-Hant": {
// App menu
settings: "設定...",
// File menu
file: "檔案",
// Edit menu
edit: "編輯",
// View menu
view: "檢視",
// Configuration menu
configuration: "配置",
switchPreset: "切換預設",
managePresets: "管理預設...",
addConfigurationPreset: "新增配置預設...",
// Window menu
window: "視窗",
// Help menu
help: "說明",
documentation: "文件",
reportIssue: "回報問題",
},
} }
/** /**
@@ -126,6 +154,16 @@ const translations: Record<MenuLocale, MenuTranslations> = {
* Falls back to English if locale is not supported * Falls back to English if locale is not supported
*/ */
export function getMenuTranslations(locale: string): MenuTranslations { export function getMenuTranslations(locale: string): MenuTranslations {
// Check for zh-Hant before normalizing
if (
locale === "zh-Hant" ||
locale.toLowerCase().startsWith("zh-hant") ||
locale.toLowerCase().startsWith("zh-tw") ||
locale.toLowerCase().startsWith("zh-hk")
) {
return translations["zh-Hant"]
}
// Normalize locale (e.g., "zh-CN" -> "zh", "ja-JP" -> "ja") // Normalize locale (e.g., "zh-CN" -> "zh", "ja-JP" -> "ja")
const normalized = locale.toLowerCase().split("-")[0] const normalized = locale.toLowerCase().split("-")[0]
@@ -136,10 +174,21 @@ export function getMenuTranslations(locale: string): MenuTranslations {
/** /**
* Detect system locale from Electron app * Detect system locale from Electron app
* Returns one of: "en", "zh", "ja" * Returns one of: "en", "zh", "ja", "zh-Hant"
*/ */
export function detectSystemLocale(appLocale: string): MenuLocale { export function detectSystemLocale(appLocale: string): MenuLocale {
const normalized = appLocale.toLowerCase().split("-")[0] const lower = appLocale.toLowerCase()
// Distinguish Traditional Chinese locales (TW, HK, Hant) from Simplified
if (
lower.startsWith("zh-hant") ||
lower.startsWith("zh-tw") ||
lower.startsWith("zh-hk")
) {
return "zh-Hant"
}
const normalized = lower.split("-")[0]
if (normalized === "zh") return "zh" if (normalized === "zh") return "zh"
if (normalized === "ja") return "ja" if (normalized === "ja") return "ja"

View File

@@ -1,6 +1,7 @@
"use client" "use client"
import { useCallback, useEffect, useState } from "react" import { useCallback, useEffect, useState } from "react"
import { getApiEndpoint } from "@/lib/base-path"
import type { FlattenedServerModel } from "@/lib/server-model-config" import type { FlattenedServerModel } from "@/lib/server-model-config"
import { STORAGE_KEYS } from "@/lib/storage" import { STORAGE_KEYS } from "@/lib/storage"
import { import {
@@ -147,7 +148,7 @@ export function useModelConfig(): UseModelConfigReturn {
useEffect(() => { useEffect(() => {
if (typeof window === "undefined") return if (typeof window === "undefined") return
fetch("/api/server-models") fetch(getApiEndpoint("/api/server-models"))
.then((res) => { .then((res) => {
if (!res.ok) { if (!res.ok) {
console.error( console.error(

View File

@@ -39,7 +39,7 @@ export interface ClientOverrides {
baseUrlEnv?: string baseUrlEnv?: string
} }
// Providers that can be used with client-provided API keys // Providers that can be selected from client settings
const ALLOWED_CLIENT_PROVIDERS: ProviderName[] = [ const ALLOWED_CLIENT_PROVIDERS: ProviderName[] = [
"openai", "openai",
"anthropic", "anthropic",
@@ -53,6 +53,7 @@ const ALLOWED_CLIENT_PROVIDERS: ProviderName[] = [
"sglang", "sglang",
"gateway", "gateway",
"edgeone", "edgeone",
"ollama",
"doubao", "doubao",
"modelscope", "modelscope",
] ]
@@ -572,12 +573,13 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
// SECURITY: Prevent SSRF attacks (GHSA-9qf7-mprq-9qgm) // SECURITY: Prevent SSRF attacks (GHSA-9qf7-mprq-9qgm)
// If a custom baseUrl is provided, an API key MUST also be provided. // If a custom baseUrl is provided, an API key MUST also be provided.
// This prevents attackers from redirecting server API keys to malicious endpoints. // This prevents attackers from redirecting server API keys to malicious endpoints.
// Exception: EdgeOne provider doesn't require API key (uses Edge AI runtime) // Exception: EdgeOne and Ollama providers don't require API keys
if ( if (
overrides?.baseUrl && overrides?.baseUrl &&
!overrides?.apiKey && !overrides?.apiKey &&
!(overrides?.provider === "vertexai" && overrides?.vertexApiKey) && !(overrides?.provider === "vertexai" && overrides?.vertexApiKey) &&
overrides?.provider !== "edgeone" overrides?.provider !== "edgeone" &&
overrides?.provider !== "ollama"
) { ) {
throw new Error( throw new Error(
`API key is required when using a custom base URL. ` + `API key is required when using a custom base URL. ` +
@@ -836,16 +838,16 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
break break
} }
case "ollama": case "ollama": {
if (process.env.OLLAMA_BASE_URL) { const baseURL = overrides?.baseUrl || process.env.OLLAMA_BASE_URL
const customOllama = createOllama({ if (baseURL) {
baseURL: process.env.OLLAMA_BASE_URL, const customOllama = createOllama({ baseURL })
})
model = customOllama(modelId) model = customOllama(modelId)
} else { } else {
model = ollama(modelId) model = ollama(modelId)
} }
break break
}
case "openrouter": { case "openrouter": {
const apiKey = resolveApiKey(overrides, "OPENROUTER_API_KEY") const apiKey = resolveApiKey(overrides, "OPENROUTER_API_KEY")

View File

@@ -1,6 +1,6 @@
export const i18n = { export const i18n = {
defaultLocale: "en", defaultLocale: "en",
locales: ["en", "zh", "ja"], locales: ["en", "zh", "ja", "zh-Hant"],
} as const } as const
export type Locale = (typeof i18n)["locales"][number] export type Locale = (typeof i18n)["locales"][number]

View File

@@ -6,6 +6,8 @@ const dictionaries = {
en: () => import("./dictionaries/en.json").then((m) => m.default), en: () => import("./dictionaries/en.json").then((m) => m.default),
zh: () => import("./dictionaries/zh.json").then((m) => m.default), zh: () => import("./dictionaries/zh.json").then((m) => m.default),
ja: () => import("./dictionaries/ja.json").then((m) => m.default), ja: () => import("./dictionaries/ja.json").then((m) => m.default),
"zh-Hant": () =>
import("./dictionaries/zh-Hant.json").then((m) => m.default),
} }
export type Dictionary = Awaited<ReturnType<(typeof dictionaries)["en"]>> export type Dictionary = Awaited<ReturnType<(typeof dictionaries)["en"]>>

View File

@@ -77,6 +77,8 @@
"settings": { "settings": {
"title": "Settings", "title": "Settings",
"description": "Configure your application settings.", "description": "Configure your application settings.",
"apiKeysModels": "API Keys & Models",
"apiKeysModelsDescription": "Configure AI providers and API keys.",
"accessCode": "Access Code", "accessCode": "Access Code",
"accessCodePlaceholder": "Enter access code", "accessCodePlaceholder": "Enter access code",
"accessCodeDescription": "Required to use this application.", "accessCodeDescription": "Required to use this application.",

View File

@@ -77,6 +77,8 @@
"settings": { "settings": {
"title": "設定", "title": "設定",
"description": "アプリケーション設定を構成します。", "description": "アプリケーション設定を構成します。",
"apiKeysModels": "API キーとモデル",
"apiKeysModelsDescription": "AI プロバイダーと API キーを設定します。",
"accessCode": "アクセスコード", "accessCode": "アクセスコード",
"accessCodePlaceholder": "アクセスコードを入力", "accessCodePlaceholder": "アクセスコードを入力",
"accessCodeDescription": "このアプリケーションを使用するために必要です。", "accessCodeDescription": "このアプリケーションを使用するために必要です。",

View File

@@ -0,0 +1,342 @@
{
"common": {
"save": "儲存",
"cancel": "取消",
"close": "關閉",
"confirm": "確認",
"clear": "清除",
"edit": "編輯",
"delete": "刪除",
"loading": "載入中...",
"new": "新建"
},
"nav": {
"about": "關於",
"editor": "編輯器",
"newChat": "開始新對話",
"github": "GitHub",
"settings": "設定",
"hidePanel": "隱藏聊天面板 (Ctrl+B)",
"showPanel": "顯示聊天面板 (Ctrl+B)",
"aiChat": "AI 聊天"
},
"providers": {
"useServerDefault": "使用伺服器預設值",
"openai": "OpenAI",
"anthropic": "Anthropic",
"google": "Google",
"azure": "Azure OpenAI",
"openrouter": "OpenRouter",
"deepseek": "DeepSeek",
"siliconflow": "SiliconFlow",
"modelscope": "ModelScope"
},
"chat": {
"placeholder": "描述您的圖表或上傳檔案...",
"send": "傳送",
"sending": "傳送中...",
"sendMessage": "傳送訊息",
"clearConversation": "清除對話",
"diagramHistory": "圖表歷史",
"saveDiagram": "儲存圖表",
"uploadFile": "上傳檔案圖片、PDF、文字",
"minimalStyle": "簡約",
"styledMode": "精緻",
"minimalTooltip": "使用簡約模式以加快產生速度(無顏色)",
"regenerate": "重新產生回應",
"copyResponse": "複製回應",
"copied": "已複製!",
"failedToCopy": "複製失敗",
"failedToCopyDetail": "複製訊息失敗。請手動複製或檢查剪貼簿權限。",
"goodResponse": "有幫助",
"badResponse": "無幫助",
"clickToEdit": "點擊編輯",
"editMessage": "編輯訊息",
"saveAndSubmit": "儲存並提交",
"ExtractURL": "從 URL 擷取"
},
"examples": {
"title": "用 AI 建立圖表",
"subtitle": "描述您想要建立的內容或上傳圖片進行複製",
"quickExamples": "快速範例",
"paperToDiagram": "文件轉圖表",
"paperDescription": "上傳 .pdf, .txt, .md, .json, .csv, .py, .js, .ts 等檔案",
"animatedDiagram": "動畫圖表",
"animatedDescription": "繪製帶有動畫連接器的 Transformer 架構",
"awsArchitecture": "AWS 架構",
"awsDescription": "使用 AWS 圖示建立雲端架構圖",
"replicateFlowchart": "複製流程圖",
"replicateDescription": "上傳並複製現有流程圖",
"creativeDrawing": "創意繪圖",
"creativeDescription": "繪製有趣且富有創意的內容",
"cachedNote": "範例已快取,可即時回應",
"mcpServer": "MCP 伺服器",
"mcpDescription": "在 Claude Desktop、VS Code 和 Cursor 中使用",
"preview": "預覽"
},
"settings": {
"title": "設定",
"description": "配置您的應用程式設定。",
"apiKeysModels": "API 金鑰和模型",
"apiKeysModelsDescription": "配置 AI 提供商和 API 金鑰。",
"accessCode": "存取碼",
"accessCodePlaceholder": "輸入存取碼",
"accessCodeDescription": "使用此應用程式需要存取碼。",
"aiProvider": "AI 提供商設定",
"aiProviderDescription": "使用您自己的 API 金鑰來繞過使用限制。您的金鑰僅儲存在瀏覽器本機,不會儲存在伺服器上。",
"provider": "提供商",
"modelId": "模型 ID",
"apiKey": "API 金鑰",
"apiKeyPlaceholder": "您的 API 金鑰",
"baseUrl": "基礎 URL可選",
"customEndpoint": "自訂端點 URL",
"overrides": "覆寫",
"clearSettings": "清除設定",
"useServerDefault": "使用伺服器預設值",
"language": "語言",
"languageDescription": "選擇介面語言。",
"theme": "主題",
"themeDescription": "介面和 DrawIO 畫布的深色/淺色模式。",
"drawioStyle": "DrawIO 樣式",
"drawioStyleDescription": "畫布樣式:",
"switchTo": "切換到",
"minimal": "簡約",
"sketch": "草圖",
"diagramStyle": "圖表樣式",
"diagramStyleDescription": "切換簡約與精緻圖表輸出模式。",
"sendShortcut": "傳送快捷鍵",
"sendShortcutDescription": "選擇傳送訊息的方式。",
"enterToSend": "Enter 傳送",
"ctrlEnterToSend": "Cmd/Ctrl+Enter 傳送",
"diagramActions": "圖表操作",
"diagramActionsDescription": "管理圖表歷史紀錄和匯出",
"history": "歷史紀錄",
"download": "下載",
"proxy": "代理設定",
"proxyDescription": "配置 API 請求的 HTTP/HTTPS 代理(僅桌面版)",
"httpProxy": "HTTP 代理",
"httpsProxy": "HTTPS 代理",
"applyProxy": "套用",
"proxyApplied": "代理設定已套用",
"diagramValidation": "圖表驗證(實驗性)",
"diagramValidationDescription": "使用視覺語言模型驗證產生的圖表。需要支援視覺的模型,如 GPT-5.2 或 Sonnet-4.5。",
"enabled": "已啟用",
"disabled": "已停用"
},
"save": {
"title": "儲存圖表",
"description": "選擇格式和檔案名稱以儲存您的圖表。",
"format": "格式",
"filename": "檔案名稱",
"filenamePlaceholder": "輸入檔案名稱",
"formats": {
"drawio": "Draw.io XML",
"png": "PNG 圖片",
"svg": "SVG 圖片"
},
"savedSuccessfully": "儲存成功!"
},
"history": {
"title": "圖表歷史",
"description": "在 AI 修改之前儲存的每個圖表。\n點擊圖表以還原它",
"noHistory": "尚無歷史紀錄。傳送訊息以建立圖表歷史。",
"version": "版本",
"restoreTo": "還原到版本 {version}"
},
"dialogs": {
"clearTitle": "清除所有內容?",
"clearDescription": "這將清除目前對話並重設圖表。此操作無法復原。",
"clearEverything": "清除所有內容",
"clearSuccess": "已開始新對話"
},
"errors": {
"maxFiles": "檔案太多。最多允許 {max} 個。",
"onlyMoreAllowed": "只能再新增 {slots} 個檔案",
"fileExceeds": "「{name}」大小為 {size}(超過 {max}MB",
"unsupportedType": "「{name}」不是支援的檔案類型",
"filesRejected": "{count} 個檔案被拒絕:",
"andMore": "...還有 {count} 個",
"invalidAccessCode": "無效或缺少存取碼。請在設定中配置。",
"networkError": "網路錯誤。請檢查您的連線。",
"retryLimit": "已達自動重試限制({max})。請手動重試。",
"continuationRetryLimit": "已達繼續重試限制({max})。圖表可能過於複雜。",
"validationFailed": "圖表驗證失敗。請嘗試重新產生。",
"malformedXml": "AI 產生的圖表 XML 無效。請嘗試重新產生。",
"failedToProcess": "無法處理圖表。請嘗試重新產生。",
"sessionCorrupted": "工作階段資料已損壞。重新開始。",
"failedToSave": "無法儲存訊息到 localStorage",
"failedToRestore": "無法從 localStorage 還原",
"failedToPersist": "卸載前無法持久化狀態",
"failedToExport": "取得圖表資料時出錯",
"failedToLoadExample": "載入範例圖片時出錯",
"failedToRecordFeedback": "記錄您的回饋失敗。請重試。",
"storageUpdateFailed": "聊天已清除,但無法更新瀏覽器儲存空間"
},
"quota": {
"dailyLimit": "已達每日配額",
"tokenLimit": "已達每日令牌限制",
"tpmLimit": "速率限制",
"tpmMessage": "請求過多。請稍等片刻。",
"tpmMessageDetailed": "達到速率限制({limit} 令牌/分鐘)。請等待 {seconds} 秒後再傳送請求。",
"messageApi": "看來您今天的體驗次數已達上限。非常高興您玩得開心,雖然本專案由字節跳動豆包慷慨贊助,但為了確保大家都能公平使用,我們不得不對使用量做一點小小的限制。",
"messageToken": "看來您今天的 Token 用量已達上限。非常高興您玩得開心,雖然本專案由字節跳動豆包慷慨贊助,但為了確保大家都能公平使用,我們不得不對使用量做一點小小的限制。",
"tip": "<strong>提示:</strong>您可以使用自己的 API 金鑰(點擊設定圖示)或自行託管專案來繞過這些限制。",
"reset": "您的限制將在明天重設。感謝您的理解。",
"doubaoSponsorship": "<a href=\"{link}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"underline hover:text-foreground\">點此註冊</a>可獲得每個模型 50 萬免費 Token包括豆包、DeepSeek 和 Kimi然後在模型設定中配置您的 API Key。",
"configModel": "使用您的金鑰",
"selfHost": "自行託管",
"sponsor": "贊助",
"learnMore": "了解更多 →",
"usedOf": "{used}/{limit}"
},
"tools": {
"generateDiagram": "產生圖表",
"editDiagram": "編輯圖表",
"appendDiagram": "繼續圖表",
"complete": "完成",
"error": "錯誤",
"truncated": "已截斷"
},
"file": {
"reading": "讀取中...",
"chars": "字元",
"removeFile": "移除檔案"
},
"url": {
"title": "從 URL 擷取內容",
"description": "貼上 URL 以擷取和分析其內容",
"Extracting": "擷取中...",
"extract": "擷取",
"Cancel": "取消",
"enterUrl": "請輸入 URL",
"invalidFormat": "URL 格式無效"
},
"reasoning": {
"thinking": "思考中...",
"thoughtFor": "思考了 {duration} 秒",
"thoughtBrief": "思考了幾秒鐘"
},
"dev": {
"title": "開發XML 串流模擬器",
"preset": "預設:",
"selectPreset": "選擇預設...",
"clear": "清除",
"placeholder": "在此貼上 mxCell XML 或選擇預設...",
"interval": "間隔:",
"chars": "字元:",
"streaming": "串流傳輸中...",
"simulate": "模擬",
"stop": "停止",
"testQuotaToast": "測試配額提示",
"simulatingMessage": "[開發] 模擬 XML 串流傳輸",
"successMessage": "成功顯示圖表。"
},
"about": {
"modelChange": "模型變更與用量限制",
"walletCrying": "(別名:我的錢包頂不住了)",
"seekingSponsorship": "尋求贊助(求大佬撈一把)",
"contactMe": "聯絡我",
"usageNotice": "由於使用量過高,我已將模型從 Claude 更換為 minimax-m2並設定了一些用量限制。詳情請查看關於頁面。"
},
"sessionHistory": {
"tooltip": "聊天歷史",
"newChat": "新對話",
"empty": "暫無聊天紀錄",
"emptyHint": "開始對話吧",
"today": "今天",
"yesterday": "昨天",
"thisWeek": "本週",
"earlier": "更早",
"deleteTitle": "刪除此對話?",
"deleteDescription": "這將永久刪除此聊天工作階段及其圖表。此操作無法復原。",
"recentChats": "最近對話",
"justNow": "剛剛",
"searchPlaceholder": "搜尋對話...",
"noResults": "未找到對話"
},
"validation": {
"title": "驗證圖表",
"capturing": "截圖中",
"validating": "驗證中",
"validatingWithAttempt": "驗證中 ({attempt}/{max})",
"valid": "通過",
"validWithWarnings": "通過(有警告)",
"issuesFound": "發現問題",
"error": "錯誤",
"skipped": "已跳過",
"capturedScreenshot": "截圖預覽:",
"issuesFoundLabel": "發現的問題:",
"suggestions": "建議:",
"passedValidation": "圖表通過視覺驗證 - 未發現問題。",
"improvementRequested": "改進請求已傳送 - 請查看下方新圖表",
"improveWithSuggestions": "根據建議改進",
"regenerateWithFeedback": "使用驗證回饋重新產生圖表"
},
"modelConfig": {
"title": "AI 模型配置",
"description": "配置多個 AI 提供商和模型",
"configure": "配置",
"addProvider": "新增提供商",
"addModel": "新增模型",
"modelId": "模型 ID",
"modelLabel": "顯示名稱",
"streaming": "啟用串流輸出",
"deleteProvider": "刪除提供商",
"deleteModel": "刪除模型",
"noModels": "尚未配置模型。新增模型以開始使用。",
"selectProvider": "選擇一個提供商或新增",
"configureMultiple": "配置多個 AI 提供商並輕鬆切換",
"apiKeyStored": "API 金鑰儲存在您的瀏覽器本機",
"test": "測試",
"validationError": "驗證失敗",
"addModelFirst": "請先新增至少一個模型以進行驗證",
"providers": "提供商",
"addProviderHint": "新增提供商即可開始使用",
"verified": "已驗證",
"configuration": "配置",
"displayName": "顯示名稱",
"awsAccessKeyId": "AWS 存取金鑰 ID",
"awsSecretAccessKey": "AWS Secret Access Key",
"awsRegion": "AWS 區域",
"selectRegion": "選擇區域",
"apiKey": "API 金鑰",
"enterApiKey": "輸入您的 API 金鑰",
"enterSecretKey": "輸入您的 Secret Key",
"baseUrl": "基礎 URL",
"optional": "(可選)",
"baseUrlWithExample": "基礎 URL可選例如 {example}",
"customEndpoint": "自訂端點 URL",
"models": "模型",
"customModelId": "自訂模型 ID...",
"allAdded": "已全部新增",
"suggested": "推薦",
"noModelsConfigured": "尚未配置模型",
"modelIdEmpty": "模型 ID 不能為空",
"modelIdExists": "此模型 ID 已存在",
"configureProviders": "配置 AI 提供商",
"selectProviderHint": "從列表中選擇提供商或新增以配置 API 金鑰和模型",
"deleteConfirmDesc": "確定要刪除 {name} 嗎?這將移除所有配置的模型且無法復原。",
"typeToConfirm": "輸入「{name}」以確認",
"typeProviderName": "輸入提供商名稱...",
"modelsConfiguredCount": "已配置 {count} 個模型",
"validationFailedCount": "{count} 個模型驗證失敗",
"cancel": "取消",
"delete": "刪除",
"clickToChange": "(點擊變更)",
"usingServerDefault": "使用伺服器預設模型",
"selectModel": "選擇模型",
"searchModels": "搜尋模型...",
"noVerifiedModels": "沒有已驗證的模型。請先測試您的模型。",
"noModelsFound": "未找到模型。",
"default": "預設",
"serverDefault": "伺服器預設",
"serverModels": "伺服器模型",
"userModels": "使用者模型",
"configureModels": "配置模型...",
"onlyVerifiedShown": "僅顯示已驗證的模型",
"showUnvalidatedModels": "顯示未驗證的模型",
"allModelsShown": "顯示所有模型(包括未驗證的)",
"unvalidatedModelWarning": "此模型尚未驗證",
"serverDefaultModel": "伺服器預設模型"
}
}

View File

@@ -77,6 +77,8 @@
"settings": { "settings": {
"title": "设置", "title": "设置",
"description": "配置您的应用程序设置。", "description": "配置您的应用程序设置。",
"apiKeysModels": "API 密钥和模型",
"apiKeysModelsDescription": "配置 AI 提供商和 API 密钥。",
"accessCode": "访问码", "accessCode": "访问码",
"accessCodePlaceholder": "输入访问码", "accessCodePlaceholder": "输入访问码",
"accessCodeDescription": "使用此应用程序需要访问码。", "accessCodeDescription": "使用此应用程序需要访问码。",

View File

@@ -75,18 +75,6 @@ export function isIndexedDBAvailable(): boolean {
} }
} }
// Check if IndexedDB is actually usable (not just present).
export async function isIndexedDBUsable(): Promise<boolean> {
if (!isIndexedDBAvailable()) return false
try {
const db = await getDB()
db.close()
return true
} catch {
return false
}
}
// CRUD Operations // CRUD Operations
export async function getAllSessionMetadata(): Promise<SessionMetadata[]> { export async function getAllSessionMetadata(): Promise<SessionMetadata[]> {
if (!isIndexedDBAvailable()) return [] if (!isIndexedDBAvailable()) return []

View File

@@ -11,6 +11,7 @@ export const DEFAULT_SYSTEM_PROMPT = `
You are an expert diagram creation assistant specializing in draw.io XML generation. You are an expert diagram creation assistant specializing in draw.io XML generation.
Your primary function is chat with user and crafting clear, well-organized visual diagrams through precise XML specifications. Your primary function is chat with user and crafting clear, well-organized visual diagrams through precise XML specifications.
You can see images that users upload, and you can read the text content extracted from PDF documents they upload. You can see images that users upload, and you can read the text content extracted from PDF documents they upload.
ALWAYS respond in the same language as the user's last message.
When you are asked to create a diagram, briefly describe your plan about the layout and structure to avoid object overlapping or edge cross the objects. (2-3 sentences max), then use display_diagram tool to generate the XML. When you are asked to create a diagram, briefly describe your plan about the layout and structure to avoid object overlapping or edge cross the objects. (2-3 sentences max), then use display_diagram tool to generate the XML.
After generating or editing a diagram, you don't need to say anything. The user can see the diagram - no need to describe it. After generating or editing a diagram, you don't need to say anything. The user can see the diagram - no need to describe it.

View File

@@ -1,4 +1,5 @@
import { z } from "zod" import { z } from "zod"
import { getApiEndpoint } from "@/lib/base-path"
export interface UrlData { export interface UrlData {
url: string url: string
@@ -15,7 +16,7 @@ const UrlResponseSchema = z.object({
}) })
export async function extractUrlContent(url: string): Promise<UrlData> { export async function extractUrlContent(url: string): Promise<UrlData> {
const response = await fetch("/api/parse-url", { const response = await fetch(getApiEndpoint("/api/parse-url"), {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url }), body: JSON.stringify({ url }),

542
package-lock.json generated

File diff suppressed because it is too large Load Diff