mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-09-01 17:10:24 +08:00
Compare commits
23 Commits
fix/idb-cl
...
pr-657-upd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43161977a5 | ||
|
|
f09d2c100a | ||
|
|
03e2d9da9d | ||
|
|
bd1e5cc237 | ||
|
|
edc10803e4 | ||
|
|
b074cee95c | ||
|
|
121b6fc99f | ||
|
|
0e1bed39a4 | ||
|
|
d5e2885623 | ||
|
|
7662b00bb3 | ||
|
|
8a93d4ea9f | ||
|
|
add9111c82 | ||
|
|
1c1c6a4d38 | ||
|
|
31fffcc52d | ||
|
|
0a03ac9f45 | ||
|
|
6dad6ab147 | ||
|
|
8781520ebb | ||
|
|
67196225f0 | ||
|
|
09a5774cba | ||
|
|
fdd4be6463 | ||
|
|
1258f98478 | ||
|
|
f6f693ee8d | ||
|
|
9398117368 |
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,6 +131,7 @@ 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")
|
||||||
|
|
||||||
@@ -901,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 () => {
|
||||||
@@ -937,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)
|
||||||
@@ -952,6 +953,7 @@ export default function ChatPanel({
|
|||||||
dict.dialogs.clearSuccess,
|
dict.dialogs.clearSuccess,
|
||||||
buildSessionData,
|
buildSessionData,
|
||||||
setDiagramHistory,
|
setDiagramHistory,
|
||||||
|
pathname,
|
||||||
])
|
])
|
||||||
|
|
||||||
const handleInputChange = (
|
const handleInputChange = (
|
||||||
@@ -1368,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)}
|
||||||
/>
|
/>
|
||||||
@@ -1385,6 +1386,7 @@ export default function ChatPanel({
|
|||||||
onMinimalStyleChange={setMinimalStyle}
|
onMinimalStyleChange={setMinimalStyle}
|
||||||
vlmValidationEnabled={vlmValidationEnabled}
|
vlmValidationEnabled={vlmValidationEnabled}
|
||||||
onVlmValidationChange={handleVlmValidationChange}
|
onVlmValidationChange={handleVlmValidationChange}
|
||||||
|
onOpenModelConfig={() => setShowModelConfigDialog(true)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ModelConfigDialog
|
<ModelConfigDialog
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
16
components/image-with-basepath.tsx
Normal file
16
components/image-with-basepath.tsx
Normal 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} />
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
4
electron/electron.d.ts
vendored
4
electron/electron.d.ts
vendored
@@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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"]>>
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
|||||||
@@ -77,6 +77,8 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"title": "設定",
|
"title": "設定",
|
||||||
"description": "アプリケーション設定を構成します。",
|
"description": "アプリケーション設定を構成します。",
|
||||||
|
"apiKeysModels": "API キーとモデル",
|
||||||
|
"apiKeysModelsDescription": "AI プロバイダーと API キーを設定します。",
|
||||||
"accessCode": "アクセスコード",
|
"accessCode": "アクセスコード",
|
||||||
"accessCodePlaceholder": "アクセスコードを入力",
|
"accessCodePlaceholder": "アクセスコードを入力",
|
||||||
"accessCodeDescription": "このアプリケーションを使用するために必要です。",
|
"accessCodeDescription": "このアプリケーションを使用するために必要です。",
|
||||||
|
|||||||
342
lib/i18n/dictionaries/zh-Hant.json
Normal file
342
lib/i18n/dictionaries/zh-Hant.json
Normal 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": "伺服器預設模型"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -77,6 +77,8 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"title": "设置",
|
"title": "设置",
|
||||||
"description": "配置您的应用程序设置。",
|
"description": "配置您的应用程序设置。",
|
||||||
|
"apiKeysModels": "API 密钥和模型",
|
||||||
|
"apiKeysModelsDescription": "配置 AI 提供商和 API 密钥。",
|
||||||
"accessCode": "访问码",
|
"accessCode": "访问码",
|
||||||
"accessCodePlaceholder": "输入访问码",
|
"accessCodePlaceholder": "输入访问码",
|
||||||
"accessCodeDescription": "使用此应用程序需要访问码。",
|
"accessCodeDescription": "使用此应用程序需要访问码。",
|
||||||
|
|||||||
@@ -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
542
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user