mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-09-01 17:10:24 +08:00
Compare commits
37 Commits
v0.4.12
...
fix/kimi-k
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d3a498306 | ||
|
|
2712f6d67c | ||
|
|
b84b156c8c | ||
|
|
c6eab07622 | ||
|
|
c382d3c0f4 | ||
|
|
e065d4b727 | ||
|
|
b2b9614404 | ||
|
|
1b96325fea | ||
|
|
19dd4e287f | ||
|
|
699767f392 | ||
|
|
feeb9ec0c5 | ||
|
|
f1a3044224 | ||
|
|
71a1d02e28 | ||
|
|
2d17963b2a | ||
|
|
3721b16d18 | ||
|
|
cbde35fb54 | ||
|
|
f098e557fc | ||
|
|
a245dcb150 | ||
|
|
f2f5ac907d | ||
|
|
0a79536002 | ||
|
|
f2c8fea58d | ||
|
|
31fffcc52d | ||
|
|
0a03ac9f45 | ||
|
|
6dad6ab147 | ||
|
|
8781520ebb | ||
|
|
67196225f0 | ||
|
|
09a5774cba | ||
|
|
fdd4be6463 | ||
|
|
f8a0ebd149 | ||
|
|
c9dd54dad7 | ||
|
|
1258f98478 | ||
|
|
f6f693ee8d | ||
|
|
9398117368 | ||
|
|
cb0c0fbcda | ||
|
|
e7c29fb410 | ||
|
|
f0dd199cd1 | ||
|
|
7656b64018 |
2
.github/workflows/electron-release.yml
vendored
2
.github/workflows/electron-release.yml
vendored
@@ -99,7 +99,7 @@ jobs:
|
||||
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||
organization-id: '880a211d-2cd3-4e7b-8d04-3d1f8eb39df5'
|
||||
project-slug: 'next-ai-draw-io'
|
||||
signing-policy-slug: 'test-signing'
|
||||
signing-policy-slug: 'release-signing'
|
||||
artifact-configuration-slug: 'windows-exe'
|
||||
github-artifact-id: ${{ steps.upload-unsigned.outputs.artifact-id }}
|
||||
wait-for-completion: true
|
||||
|
||||
@@ -19,7 +19,7 @@ English | [中文](./docs/cn/README_CN.md) | [日本語](./docs/ja/README_JA.md)
|
||||
|
||||
A Next.js web application that integrates AI capabilities with draw.io diagrams. Create, modify, and enhance diagrams through natural language commands and AI-assisted visualization.
|
||||
|
||||
> Note: Thanks to <img src="https://raw.githubusercontent.com/DayuanJiang/next-ai-draw-io/main/public/doubao-color.png" alt="" height="20" /> [ByteDance Doubao](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project) sponsorship, the demo site now uses the powerful K2-thinking model!
|
||||
> Note: Thanks to <img src="https://raw.githubusercontent.com/DayuanJiang/next-ai-draw-io/main/public/doubao-color.png" alt="" height="20" /> [ByteDance Doubao](https://www.volcengine.com/activity/newyear-referral?utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project) sponsorship, the demo site now uses the powerful K2-thinking model!
|
||||
|
||||
|
||||
https://github.com/user-attachments/assets/9d60a3e8-4a1c-4b5e-acbb-26af2d3eabd1
|
||||
@@ -202,7 +202,7 @@ See the [Next.js deployment documentation](https://nextjs.org/docs/app/building-
|
||||
|
||||
## Multi-Provider Support
|
||||
|
||||
- [ByteDance Doubao](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project)
|
||||
- [ByteDance Doubao](https://www.volcengine.com/activity/newyear-referral?utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project)
|
||||
- AWS Bedrock (default)
|
||||
- OpenAI
|
||||
- Anthropic
|
||||
@@ -244,7 +244,7 @@ Diagrams are represented as XML that can be rendered in draw.io. The AI processe
|
||||
|
||||
## Support & Contact
|
||||
|
||||
**Special thanks to [ByteDance Doubao](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project) for sponsoring the API token usage of the demo site!** Register on the ARK platform to get 500K free tokens for all models!
|
||||
**Special thanks to [ByteDance Doubao](https://www.volcengine.com/activity/newyear-referral?utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project) for sponsoring the API token usage of the demo site!** Register on the ARK platform to get 500K free tokens for all models!
|
||||
|
||||
If you find this project useful, please consider [sponsoring](https://github.com/sponsors/DayuanJiang) to help me host the live demo site!
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Metadata } from "next"
|
||||
import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
import { FaGithub } from "react-icons/fa"
|
||||
import Image from "@/components/image-with-basepath"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "关于 - Next AI Draw.io",
|
||||
@@ -78,7 +78,7 @@ export default function AboutCN() {
|
||||
<p>
|
||||
好消息!感谢{" "}
|
||||
<a
|
||||
href="https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
|
||||
href="https://www.volcengine.com/activity/newyear-referral?utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-semibold text-blue-600 hover:underline"
|
||||
@@ -277,7 +277,7 @@ export default function AboutCN() {
|
||||
<ul className="list-disc pl-6 text-gray-700 space-y-1">
|
||||
<li>
|
||||
<a
|
||||
href="https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
|
||||
href="https://www.volcengine.com/activity/newyear-referral?utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:underline"
|
||||
@@ -312,7 +312,7 @@ export default function AboutCN() {
|
||||
<p className="text-gray-700 mb-4 font-semibold">
|
||||
特别感谢{" "}
|
||||
<a
|
||||
href="https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
|
||||
href="https://www.volcengine.com/activity/newyear-referral?utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:underline"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Metadata } from "next"
|
||||
import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
import { FaGithub } from "react-icons/fa"
|
||||
import Image from "@/components/image-with-basepath"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "概要 - Next AI Draw.io",
|
||||
@@ -86,7 +86,7 @@ export default function AboutJA() {
|
||||
<p>
|
||||
朗報です!
|
||||
<a
|
||||
href="https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
|
||||
href="https://www.volcengine.com/activity/newyear-referral?utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-semibold text-blue-600 hover:underline"
|
||||
@@ -292,7 +292,7 @@ export default function AboutJA() {
|
||||
<ul className="list-disc pl-6 text-gray-700 space-y-1">
|
||||
<li>
|
||||
<a
|
||||
href="https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
|
||||
href="https://www.volcengine.com/activity/newyear-referral?utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:underline"
|
||||
@@ -327,7 +327,7 @@ export default function AboutJA() {
|
||||
<p className="text-gray-700 mb-4 font-semibold">
|
||||
デモサイトのAPIトークン使用を支援してくださった{" "}
|
||||
<a
|
||||
href="https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
|
||||
href="https://www.volcengine.com/activity/newyear-referral?utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:underline"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Metadata } from "next"
|
||||
import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
import { FaGithub } from "react-icons/fa"
|
||||
import Image from "@/components/image-with-basepath"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "About - Next AI Draw.io",
|
||||
@@ -87,7 +87,7 @@ export default function About() {
|
||||
Great news! Thanks to the generous
|
||||
sponsorship from{" "}
|
||||
<a
|
||||
href="https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
|
||||
href="https://www.volcengine.com/activity/newyear-referral?utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-semibold text-blue-600 hover:underline"
|
||||
@@ -311,7 +311,7 @@ export default function About() {
|
||||
<ul className="list-disc pl-6 text-gray-700 space-y-1">
|
||||
<li>
|
||||
<a
|
||||
href="https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
|
||||
href="https://www.volcengine.com/activity/newyear-referral?utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:underline"
|
||||
@@ -348,7 +348,7 @@ export default function About() {
|
||||
<p className="text-gray-700 mb-4 font-semibold">
|
||||
Special thanks to{" "}
|
||||
<a
|
||||
href="https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
|
||||
href="https://www.volcengine.com/activity/newyear-referral?utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:underline"
|
||||
|
||||
@@ -41,19 +41,24 @@ export async function generateMetadata({
|
||||
params: Promise<{ lang: string }>
|
||||
}): Promise<Metadata> {
|
||||
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
|
||||
const titles: Record<Locale, string> = {
|
||||
en: "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",
|
||||
"zh-Hant": "Next AI Draw.io - AI 驅動的圖表產生器",
|
||||
}
|
||||
|
||||
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.",
|
||||
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.",
|
||||
"zh-Hant":
|
||||
"使用 AI 建立 AWS 架構圖、流程圖和技術圖表。免費線上工具整合 draw.io 與 AI 輔助,輕鬆建立專業圖表。",
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -80,7 +85,14 @@ export async function generateMetadata({
|
||||
type: "website",
|
||||
url: "https://next-ai-drawio.jiang.jp",
|
||||
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: [
|
||||
{
|
||||
url: "/architecture.png",
|
||||
@@ -115,6 +127,7 @@ export async function generateMetadata({
|
||||
en: "/en",
|
||||
zh: "/zh",
|
||||
ja: "/ja",
|
||||
"zh-Hant": "/zh-Hant",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -449,6 +449,7 @@ ${userInputText}
|
||||
|
||||
const result = streamText({
|
||||
model,
|
||||
abortSignal: req.signal,
|
||||
...(process.env.MAX_OUTPUT_TOKENS && {
|
||||
maxOutputTokens: parseInt(process.env.MAX_OUTPUT_TOKENS, 10),
|
||||
}),
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Link,
|
||||
Loader2,
|
||||
Send,
|
||||
Square,
|
||||
} from "lucide-react"
|
||||
import type React from "react"
|
||||
import {
|
||||
@@ -154,6 +155,7 @@ interface ChatInputProps {
|
||||
status: "submitted" | "streaming" | "ready" | "error"
|
||||
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void
|
||||
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void
|
||||
onStop?: () => void
|
||||
files?: File[]
|
||||
onFileChange?: (files: File[]) => void
|
||||
pdfData?: Map<
|
||||
@@ -170,7 +172,6 @@ interface ChatInputProps {
|
||||
selectedModelId?: string
|
||||
onModelSelect?: (modelId: string | undefined) => void
|
||||
showUnvalidatedModels?: boolean
|
||||
onConfigureModels?: () => void
|
||||
// Focus control props
|
||||
shouldFocus?: boolean
|
||||
onFocused?: () => void
|
||||
@@ -183,6 +184,7 @@ export const ChatInput = forwardRef<ChatInputRef, ChatInputProps>(
|
||||
status,
|
||||
onSubmit,
|
||||
onChange,
|
||||
onStop,
|
||||
files = [],
|
||||
onFileChange = () => {},
|
||||
pdfData = new Map(),
|
||||
@@ -194,7 +196,6 @@ export const ChatInput = forwardRef<ChatInputRef, ChatInputProps>(
|
||||
selectedModelId,
|
||||
onModelSelect = () => {},
|
||||
showUnvalidatedModels = false,
|
||||
onConfigureModels = () => {},
|
||||
shouldFocus = false,
|
||||
onFocused,
|
||||
},
|
||||
@@ -550,29 +551,34 @@ export const ChatInput = forwardRef<ChatInputRef, ChatInputProps>(
|
||||
models={models}
|
||||
selectedModelId={selectedModelId}
|
||||
onSelect={onModelSelect}
|
||||
onConfigure={onConfigureModels}
|
||||
disabled={isDisabled}
|
||||
showUnvalidatedModels={showUnvalidatedModels}
|
||||
/>
|
||||
<div className="w-px h-5 bg-border mx-1" />
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isDisabled || !input.trim()}
|
||||
size="sm"
|
||||
className="h-8 px-4 rounded-xl font-medium shadow-sm"
|
||||
aria-label={
|
||||
isDisabled ? dict.chat.sending : dict.chat.send
|
||||
}
|
||||
>
|
||||
{isDisabled ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<Send className="h-4 w-4 mr-1.5" />
|
||||
{dict.chat.send}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
{(status === "streaming" || status === "submitted") &&
|
||||
onStop ? (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onStop}
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
className="h-8 w-8 p-0 rounded-xl shadow-sm"
|
||||
aria-label={dict.chat.stopGeneration}
|
||||
>
|
||||
<Square className="h-4 w-4" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isDisabled || !input.trim()}
|
||||
size="sm"
|
||||
className="h-8 px-4 rounded-xl font-medium shadow-sm"
|
||||
aria-label={dict.chat.send}
|
||||
>
|
||||
<Send className="h-4 w-4 mr-1.5" />
|
||||
{dict.chat.send}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<HistoryDialog
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
ThumbsUp,
|
||||
X,
|
||||
} from "lucide-react"
|
||||
import Image from "next/image"
|
||||
import type { MutableRefObject } from "react"
|
||||
import { useCallback, useEffect, useRef, useState } from "react"
|
||||
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 { ValidationState } 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 { useDictionary } from "@/hooks/use-dictionary"
|
||||
import { getApiEndpoint } from "@/lib/base-path"
|
||||
|
||||
@@ -8,8 +8,7 @@ import {
|
||||
PanelRightOpen,
|
||||
Settings,
|
||||
} from "lucide-react"
|
||||
import Image from "next/image"
|
||||
import { useRouter, useSearchParams } from "next/navigation"
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
||||
import type React from "react"
|
||||
import {
|
||||
useCallback,
|
||||
@@ -22,6 +21,7 @@ import { flushSync } from "react-dom"
|
||||
import { Toaster, toast } from "sonner"
|
||||
import { ButtonWithTooltip } from "@/components/button-with-tooltip"
|
||||
import { ChatInput } from "@/components/chat-input"
|
||||
import Image from "@/components/image-with-basepath"
|
||||
import { ModelConfigDialog } from "@/components/model-config-dialog"
|
||||
import { SettingsDialog } from "@/components/settings-dialog"
|
||||
import { useDiagram } from "@/contexts/diagram-context"
|
||||
@@ -131,6 +131,7 @@ export default function ChatPanel({
|
||||
|
||||
const dict = useDictionary()
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const searchParams = useSearchParams()
|
||||
const urlSessionId = searchParams.get("session")
|
||||
|
||||
@@ -339,151 +340,155 @@ export default function ChatPanel({
|
||||
onValidationStateChange: handleValidationStateChange,
|
||||
})
|
||||
|
||||
const { messages, sendMessage, addToolOutput, status, error, setMessages } =
|
||||
useChat({
|
||||
transport: new DefaultChatTransport({
|
||||
api: getApiEndpoint("/api/chat"),
|
||||
}),
|
||||
onToolCall: async ({ toolCall }) => {
|
||||
await handleToolCall({ toolCall }, addToolOutput)
|
||||
},
|
||||
onError: (error) => {
|
||||
// Handle server-side quota limit (429 response)
|
||||
// AI SDK puts the full response body in error.message for non-OK responses
|
||||
try {
|
||||
const data = JSON.parse(error.message)
|
||||
if (data.type === "request") {
|
||||
quotaManager.showQuotaLimitToast(data.used, data.limit)
|
||||
return
|
||||
}
|
||||
if (data.type === "token") {
|
||||
quotaManager.showTokenLimitToast(data.used, data.limit)
|
||||
return
|
||||
}
|
||||
if (data.type === "tpm") {
|
||||
quotaManager.showTPMLimitToast(data.limit)
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
// Not JSON, fall through to string matching for backwards compatibility
|
||||
const {
|
||||
messages,
|
||||
sendMessage,
|
||||
addToolOutput,
|
||||
status,
|
||||
error,
|
||||
setMessages,
|
||||
stop,
|
||||
} = useChat({
|
||||
transport: new DefaultChatTransport({
|
||||
api: getApiEndpoint("/api/chat"),
|
||||
}),
|
||||
onToolCall: async ({ toolCall }) => {
|
||||
await handleToolCall({ toolCall }, addToolOutput)
|
||||
},
|
||||
onError: (error) => {
|
||||
// Handle server-side quota limit (429 response)
|
||||
// AI SDK puts the full response body in error.message for non-OK responses
|
||||
try {
|
||||
const data = JSON.parse(error.message)
|
||||
if (data.type === "request") {
|
||||
quotaManager.showQuotaLimitToast(data.used, data.limit)
|
||||
return
|
||||
}
|
||||
if (data.type === "token") {
|
||||
quotaManager.showTokenLimitToast(data.used, data.limit)
|
||||
return
|
||||
}
|
||||
if (data.type === "tpm") {
|
||||
quotaManager.showTPMLimitToast(data.limit)
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
// Not JSON, fall through to string matching for backwards compatibility
|
||||
}
|
||||
|
||||
// Fallback to string matching
|
||||
if (error.message.includes("Daily request limit")) {
|
||||
quotaManager.showQuotaLimitToast()
|
||||
return
|
||||
}
|
||||
if (error.message.includes("Daily token limit")) {
|
||||
quotaManager.showTokenLimitToast()
|
||||
return
|
||||
// Fallback to string matching
|
||||
if (error.message.includes("Daily request limit")) {
|
||||
quotaManager.showQuotaLimitToast()
|
||||
return
|
||||
}
|
||||
if (error.message.includes("Daily token limit")) {
|
||||
quotaManager.showTokenLimitToast()
|
||||
return
|
||||
}
|
||||
if (
|
||||
error.message.includes("Rate limit exceeded") ||
|
||||
error.message.includes("tokens per minute")
|
||||
) {
|
||||
quotaManager.showTPMLimitToast()
|
||||
return
|
||||
}
|
||||
|
||||
// Silence access code error in console since it's handled by UI
|
||||
if (!error.message.includes("Invalid or missing access code")) {
|
||||
console.error("Chat error:", error)
|
||||
}
|
||||
|
||||
// Translate technical errors into user-friendly messages
|
||||
// The server now handles detailed error messages, so we can display them directly.
|
||||
// But we still handle connection/network errors that happen before reaching the server.
|
||||
let friendlyMessage = error.message
|
||||
|
||||
// Simple check for network errors if message is generic
|
||||
if (friendlyMessage === "Failed to fetch") {
|
||||
friendlyMessage = "Network error. Please check your connection."
|
||||
}
|
||||
|
||||
// Truncated tool input error (model output limit too low)
|
||||
if (friendlyMessage.includes("toolUse.input is invalid")) {
|
||||
friendlyMessage =
|
||||
"Output was truncated before the diagram could be generated. Try a simpler request or increase the maxOutputLength."
|
||||
}
|
||||
|
||||
// Translate image not supported error
|
||||
if (
|
||||
friendlyMessage.includes("image content block") ||
|
||||
friendlyMessage.toLowerCase().includes("image_url")
|
||||
) {
|
||||
friendlyMessage = "This model doesn't support image input."
|
||||
}
|
||||
|
||||
// Add system message for error so it can be cleared
|
||||
setMessages((currentMessages) => {
|
||||
const errorMessage = {
|
||||
id: `error-${Date.now()}`,
|
||||
role: "system" as const,
|
||||
content: friendlyMessage,
|
||||
parts: [{ type: "text" as const, text: friendlyMessage }],
|
||||
}
|
||||
return [...currentMessages, errorMessage]
|
||||
})
|
||||
|
||||
if (error.message.includes("Invalid or missing access code")) {
|
||||
// Show settings dialog to help user fix it
|
||||
setShowSettingsDialog(true)
|
||||
}
|
||||
},
|
||||
onFinish: () => {},
|
||||
sendAutomaticallyWhen: ({ messages }) => {
|
||||
const isInContinuationMode = partialXmlRef.current.length > 0
|
||||
|
||||
const shouldRetry = hasToolErrors(
|
||||
messages as unknown as ChatMessage[],
|
||||
)
|
||||
|
||||
if (!shouldRetry) {
|
||||
// No error, reset retry count and clear state
|
||||
autoRetryCountRef.current = 0
|
||||
continuationRetryCountRef.current = 0
|
||||
partialXmlRef.current = ""
|
||||
return false
|
||||
}
|
||||
|
||||
// Continuation mode: limited retries for truncation handling
|
||||
if (isInContinuationMode) {
|
||||
if (
|
||||
error.message.includes("Rate limit exceeded") ||
|
||||
error.message.includes("tokens per minute")
|
||||
continuationRetryCountRef.current >=
|
||||
MAX_CONTINUATION_RETRY_COUNT
|
||||
) {
|
||||
quotaManager.showTPMLimitToast()
|
||||
return
|
||||
}
|
||||
|
||||
// Silence access code error in console since it's handled by UI
|
||||
if (!error.message.includes("Invalid or missing access code")) {
|
||||
console.error("Chat error:", error)
|
||||
}
|
||||
|
||||
// Translate technical errors into user-friendly messages
|
||||
// The server now handles detailed error messages, so we can display them directly.
|
||||
// But we still handle connection/network errors that happen before reaching the server.
|
||||
let friendlyMessage = error.message
|
||||
|
||||
// Simple check for network errors if message is generic
|
||||
if (friendlyMessage === "Failed to fetch") {
|
||||
friendlyMessage =
|
||||
"Network error. Please check your connection."
|
||||
}
|
||||
|
||||
// Truncated tool input error (model output limit too low)
|
||||
if (friendlyMessage.includes("toolUse.input is invalid")) {
|
||||
friendlyMessage =
|
||||
"Output was truncated before the diagram could be generated. Try a simpler request or increase the maxOutputLength."
|
||||
}
|
||||
|
||||
// Translate image not supported error
|
||||
if (
|
||||
friendlyMessage.includes("image content block") ||
|
||||
friendlyMessage.toLowerCase().includes("image_url")
|
||||
) {
|
||||
friendlyMessage = "This model doesn't support image input."
|
||||
}
|
||||
|
||||
// Add system message for error so it can be cleared
|
||||
setMessages((currentMessages) => {
|
||||
const errorMessage = {
|
||||
id: `error-${Date.now()}`,
|
||||
role: "system" as const,
|
||||
content: friendlyMessage,
|
||||
parts: [
|
||||
{ type: "text" as const, text: friendlyMessage },
|
||||
],
|
||||
}
|
||||
return [...currentMessages, errorMessage]
|
||||
})
|
||||
|
||||
if (error.message.includes("Invalid or missing access code")) {
|
||||
// Show settings dialog to help user fix it
|
||||
setShowSettingsDialog(true)
|
||||
}
|
||||
},
|
||||
onFinish: () => {},
|
||||
sendAutomaticallyWhen: ({ messages }) => {
|
||||
const isInContinuationMode = partialXmlRef.current.length > 0
|
||||
|
||||
const shouldRetry = hasToolErrors(
|
||||
messages as unknown as ChatMessage[],
|
||||
)
|
||||
|
||||
if (!shouldRetry) {
|
||||
// No error, reset retry count and clear state
|
||||
autoRetryCountRef.current = 0
|
||||
toast.error(
|
||||
formatMessage(dict.errors.continuationRetryLimit, {
|
||||
max: MAX_CONTINUATION_RETRY_COUNT,
|
||||
}),
|
||||
)
|
||||
continuationRetryCountRef.current = 0
|
||||
partialXmlRef.current = ""
|
||||
return false
|
||||
}
|
||||
|
||||
// Continuation mode: limited retries for truncation handling
|
||||
if (isInContinuationMode) {
|
||||
if (
|
||||
continuationRetryCountRef.current >=
|
||||
MAX_CONTINUATION_RETRY_COUNT
|
||||
) {
|
||||
toast.error(
|
||||
formatMessage(dict.errors.continuationRetryLimit, {
|
||||
max: MAX_CONTINUATION_RETRY_COUNT,
|
||||
}),
|
||||
)
|
||||
continuationRetryCountRef.current = 0
|
||||
partialXmlRef.current = ""
|
||||
return false
|
||||
}
|
||||
continuationRetryCountRef.current++
|
||||
} else {
|
||||
// Regular error: check retry count limit
|
||||
if (autoRetryCountRef.current >= MAX_AUTO_RETRY_COUNT) {
|
||||
toast.error(
|
||||
formatMessage(dict.errors.retryLimit, {
|
||||
max: MAX_AUTO_RETRY_COUNT,
|
||||
}),
|
||||
)
|
||||
autoRetryCountRef.current = 0
|
||||
partialXmlRef.current = ""
|
||||
return false
|
||||
}
|
||||
// Increment retry count for actual errors
|
||||
autoRetryCountRef.current++
|
||||
continuationRetryCountRef.current++
|
||||
} else {
|
||||
// Regular error: check retry count limit
|
||||
if (autoRetryCountRef.current >= MAX_AUTO_RETRY_COUNT) {
|
||||
toast.error(
|
||||
formatMessage(dict.errors.retryLimit, {
|
||||
max: MAX_AUTO_RETRY_COUNT,
|
||||
}),
|
||||
)
|
||||
autoRetryCountRef.current = 0
|
||||
partialXmlRef.current = ""
|
||||
return false
|
||||
}
|
||||
// Increment retry count for actual errors
|
||||
autoRetryCountRef.current++
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
})
|
||||
return true
|
||||
},
|
||||
})
|
||||
|
||||
// Store sendMessage in ref for use in callbacks (like handleImproveWithSuggestions)
|
||||
useEffect(() => {
|
||||
@@ -901,10 +906,10 @@ export default function ChatPanel({
|
||||
if (result.wasCurrentSession) {
|
||||
// Deleted current session - clear UI and URL
|
||||
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 () => {
|
||||
@@ -937,7 +942,7 @@ export default function ChatPanel({
|
||||
toast.success(dict.dialogs.clearSuccess)
|
||||
|
||||
// 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
|
||||
setShouldFocusInput(true)
|
||||
@@ -952,6 +957,7 @@ export default function ChatPanel({
|
||||
dict.dialogs.clearSuccess,
|
||||
buildSessionData,
|
||||
setDiagramHistory,
|
||||
pathname,
|
||||
])
|
||||
|
||||
const handleInputChange = (
|
||||
@@ -991,6 +997,29 @@ export default function ChatPanel({
|
||||
}
|
||||
}
|
||||
|
||||
// Handle stop button click
|
||||
const handleStop = useCallback(() => {
|
||||
const lastMessage = messages[messages.length - 1]
|
||||
const toolParts = lastMessage?.parts?.filter(
|
||||
(part: any) =>
|
||||
part.type?.startsWith("tool-") &&
|
||||
part.state === "input-streaming",
|
||||
)
|
||||
|
||||
toolParts?.forEach((part: any) => {
|
||||
if (part.toolCallId) {
|
||||
addToolOutput({
|
||||
tool: part.type.replace("tool-", ""),
|
||||
toolCallId: part.toolCallId,
|
||||
state: "output-error",
|
||||
errorText: "Stopped by user",
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
stop()
|
||||
}, [messages, addToolOutput, stop])
|
||||
|
||||
// Send chat message with headers
|
||||
const sendChatMessage = (
|
||||
parts: any,
|
||||
@@ -1357,6 +1386,7 @@ export default function ChatPanel({
|
||||
status={status}
|
||||
onSubmit={onFormSubmit}
|
||||
onChange={handleInputChange}
|
||||
onStop={handleStop}
|
||||
files={files}
|
||||
onFileChange={handleFileChange}
|
||||
pdfData={pdfData}
|
||||
@@ -1368,7 +1398,6 @@ export default function ChatPanel({
|
||||
selectedModelId={modelConfig.selectedModelId}
|
||||
onModelSelect={modelConfig.setSelectedModelId}
|
||||
showUnvalidatedModels={modelConfig.showUnvalidatedModels}
|
||||
onConfigureModels={() => setShowModelConfigDialog(true)}
|
||||
shouldFocus={shouldFocusInput}
|
||||
onFocused={() => setShouldFocusInput(false)}
|
||||
/>
|
||||
@@ -1385,6 +1414,7 @@ export default function ChatPanel({
|
||||
onMinimalStyleChange={setMinimalStyle}
|
||||
vlmValidationEnabled={vlmValidationEnabled}
|
||||
onVlmValidationChange={handleVlmValidationChange}
|
||||
onOpenModelConfig={() => setShowModelConfigDialog(true)}
|
||||
/>
|
||||
|
||||
<ModelConfigDialog
|
||||
|
||||
@@ -8,9 +8,9 @@ import {
|
||||
Trash2,
|
||||
X,
|
||||
} from "lucide-react"
|
||||
import Image from "next/image"
|
||||
import { useState } from "react"
|
||||
import ExamplePanel from "@/components/chat-example-panel"
|
||||
import Image from "@/components/image-with-basepath"
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
RefreshCw,
|
||||
X,
|
||||
} from "lucide-react"
|
||||
import Image from "next/image"
|
||||
import { useState } from "react"
|
||||
import Image from "@/components/image-with-basepath"
|
||||
import { useDictionary } from "@/hooks/use-dictionary"
|
||||
import type { ValidationResult } from "@/lib/diagram-validator"
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import { FileCode, FileText, Link, Loader2, X } from "lucide-react"
|
||||
import Image from "next/image"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import Image from "@/components/image-with-basepath"
|
||||
import { useDictionary } from "@/hooks/use-dictionary"
|
||||
import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import Image from "next/image"
|
||||
import { useState } from "react"
|
||||
import Image from "@/components/image-with-basepath"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
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,
|
||||
Monitor,
|
||||
Server,
|
||||
Settings2,
|
||||
User,
|
||||
} from "lucide-react"
|
||||
import { useEffect, useMemo, useRef, useState } from "react"
|
||||
@@ -34,7 +33,6 @@ interface ModelSelectorProps {
|
||||
models: FlattenedModel[]
|
||||
selectedModelId: string | undefined
|
||||
onSelect: (modelId: string | undefined) => void
|
||||
onConfigure: () => void
|
||||
disabled?: boolean
|
||||
showUnvalidatedModels?: boolean
|
||||
}
|
||||
@@ -85,7 +83,6 @@ export function ModelSelector({
|
||||
models,
|
||||
selectedModelId,
|
||||
onSelect,
|
||||
onConfigure,
|
||||
disabled = false,
|
||||
showUnvalidatedModels = false,
|
||||
}: ModelSelectorProps) {
|
||||
@@ -126,9 +123,7 @@ export function ModelSelector({
|
||||
)
|
||||
|
||||
const handleSelect = (value: string) => {
|
||||
if (value === "__configure__") {
|
||||
onConfigure()
|
||||
} else if (value === "__server_default__") {
|
||||
if (value === "__server_default__") {
|
||||
onSelect(undefined)
|
||||
} else {
|
||||
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 */}
|
||||
<div className="px-3 py-2 text-xs text-muted-foreground border-t">
|
||||
{showUnvalidatedModels
|
||||
|
||||
@@ -79,7 +79,7 @@ export function QuotaLimitToast({
|
||||
<p
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: formatMessage(dict.quota.doubaoSponsorship, {
|
||||
link: "https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project",
|
||||
link: "https://www.volcengine.com/activity/newyear-referral?utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project",
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"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 { Suspense, useEffect, useState } from "react"
|
||||
import { toast } from "sonner"
|
||||
@@ -56,6 +56,7 @@ const LANGUAGE_LABELS: Record<Locale, string> = {
|
||||
en: "English",
|
||||
zh: "中文",
|
||||
ja: "日本語",
|
||||
"zh-Hant": "繁體中文",
|
||||
}
|
||||
|
||||
interface SettingsDialogProps {
|
||||
@@ -69,6 +70,7 @@ interface SettingsDialogProps {
|
||||
onMinimalStyleChange?: (value: boolean) => void
|
||||
vlmValidationEnabled?: boolean
|
||||
onVlmValidationChange?: (value: boolean) => void
|
||||
onOpenModelConfig?: () => void
|
||||
}
|
||||
|
||||
export const STORAGE_ACCESS_CODE_KEY = "next-ai-draw-io-access-code"
|
||||
@@ -92,6 +94,7 @@ function SettingsContent({
|
||||
onMinimalStyleChange = () => {},
|
||||
vlmValidationEnabled = false,
|
||||
onVlmValidationChange = () => {},
|
||||
onOpenModelConfig,
|
||||
}: SettingsDialogProps) {
|
||||
const dict = useDictionary()
|
||||
const router = useRouter()
|
||||
@@ -283,6 +286,27 @@ function SettingsContent({
|
||||
{/* Content */}
|
||||
<div className="px-6 pb-6">
|
||||
<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) */}
|
||||
{accessCodeRequired && (
|
||||
<div className="py-4 first:pt-0 space-y-3">
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
一个集成了AI功能的Next.js网页应用,与draw.io图表无缝结合。通过自然语言命令和AI辅助可视化来创建、修改和增强图表。
|
||||
|
||||
> 注:感谢 <img src="https://raw.githubusercontent.com/DayuanJiang/next-ai-draw-io/main/public/doubao-color.png" alt="" height="20" /> [字节跳动豆包](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project) 的赞助支持,本项目的 Demo 现已接入强大的 K2-thinking 模型!
|
||||
> 注:感谢 <img src="https://raw.githubusercontent.com/DayuanJiang/next-ai-draw-io/main/public/doubao-color.png" alt="" height="20" /> [字节跳动豆包](https://www.volcengine.com/activity/newyear-referral?utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project) 的赞助支持,本项目的 Demo 现已接入强大的 K2-thinking 模型!
|
||||
|
||||
https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
||||
|
||||
@@ -195,7 +195,7 @@ npm run dev
|
||||
|
||||
## 多提供商支持
|
||||
|
||||
- [字节跳动豆包](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project)
|
||||
- [字节跳动豆包](https://www.volcengine.com/activity/newyear-referral?utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project)
|
||||
- AWS Bedrock(默认)
|
||||
- OpenAI
|
||||
- Anthropic
|
||||
@@ -236,7 +236,7 @@ npm run dev
|
||||
|
||||
## 支持与联系
|
||||
|
||||
**特别感谢[字节跳动豆包](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project)赞助演示站点的 API Token 使用!** 注册火山引擎 ARK 平台即可获得50万免费Token!
|
||||
**特别感谢[字节跳动豆包](https://www.volcengine.com/activity/newyear-referral?utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project)赞助演示站点的 API Token 使用!** 注册火山引擎 ARK 平台即可获得50万免费Token!
|
||||
|
||||
如果您觉得这个项目有用,请考虑[赞助](https://github.com/sponsors/DayuanJiang)来帮助我托管在线演示站点!
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
### 豆包 (字节跳动火山引擎)
|
||||
|
||||
> **免费 Token**:在 [火山引擎 ARK 平台](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project) 注册,即可获得所有模型 50 万免费 Token!
|
||||
> **免费 Token**:在 [火山引擎 ARK 平台](https://www.volcengine.com/activity/newyear-referral?utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project) 注册,即可获得所有模型 50 万免费 Token!
|
||||
|
||||
```bash
|
||||
DOUBAO_API_KEY=your_api_key
|
||||
|
||||
@@ -13,7 +13,7 @@ This guide explains how to configure different AI model providers for next-ai-dr
|
||||
|
||||
### Doubao (ByteDance Volcengine)
|
||||
|
||||
> **Free tokens**: Register on the [Volcengine ARK platform](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project) to get 500K free tokens for all models!
|
||||
> **Free tokens**: Register on the [Volcengine ARK platform](https://www.volcengine.com/activity/newyear-referral?utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project) to get 500K free tokens for all models!
|
||||
|
||||
```bash
|
||||
DOUBAO_API_KEY=your_api_key
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
AI機能とdraw.ioダイアグラムを統合したNext.jsウェブアプリケーションです。自然言語コマンドとAI支援の可視化により、ダイアグラムを作成、修正、強化できます。
|
||||
|
||||
> 注:<img src="https://raw.githubusercontent.com/DayuanJiang/next-ai-draw-io/main/public/doubao-color.png" alt="" height="20" /> [ByteDance Doubao](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project) のご支援により、デモサイトに強力な K2-thinking モデルを導入しました!
|
||||
> 注:<img src="https://raw.githubusercontent.com/DayuanJiang/next-ai-draw-io/main/public/doubao-color.png" alt="" height="20" /> [ByteDance Doubao](https://www.volcengine.com/activity/newyear-referral?utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project) のご支援により、デモサイトに強力な K2-thinking モデルを導入しました!
|
||||
|
||||
https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
||||
|
||||
@@ -196,7 +196,7 @@ Next.jsアプリをデプロイする最も簡単な方法は、Next.jsの作成
|
||||
|
||||
## マルチプロバイダーサポート
|
||||
|
||||
- [ByteDance Doubao](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project)
|
||||
- [ByteDance Doubao](https://www.volcengine.com/activity/newyear-referral?utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project)
|
||||
- AWS Bedrock(デフォルト)
|
||||
- OpenAI
|
||||
- Anthropic
|
||||
@@ -237,7 +237,7 @@ AWS BedrockとOpenRouter以外のすべてのプロバイダーはカスタム
|
||||
|
||||
## サポート&お問い合わせ
|
||||
|
||||
**デモサイトのAPIトークン使用を支援してくださった[ByteDance Doubao](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project)に特別な感謝を申し上げます!** ARKプラットフォームに登録すると、50万トークンが無料でもらえます!
|
||||
**デモサイトのAPIトークン使用を支援してくださった[ByteDance Doubao](https://www.volcengine.com/activity/newyear-referral?utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project)に特別な感謝を申し上げます!** ARKプラットフォームに登録すると、50万トークンが無料でもらえます!
|
||||
|
||||
このプロジェクトが役に立ったら、ライブデモサイトのホスティングを支援するために[スポンサー](https://github.com/sponsors/DayuanJiang)をご検討ください!
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
### Doubao (ByteDance Volcengine)
|
||||
|
||||
> **無料トークン**: [Volcengine ARK プラットフォーム](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project)に登録すると、すべてのモデルで使える50万トークンが無料で入手できます!
|
||||
> **無料トークン**: [Volcengine ARK プラットフォーム](https://www.volcengine.com/activity/newyear-referral?utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project)に登録すると、すべてのモデルで使える50万トークンが無料で入手できます!
|
||||
|
||||
```bash
|
||||
DOUBAO_API_KEY=your_api_key
|
||||
|
||||
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) */
|
||||
setProxy: (config: ProxyConfig) => Promise<SetProxyResult>
|
||||
/** Get user's preferred locale */
|
||||
getUserLocale: () => Promise<"en" | "zh" | "ja" | undefined>
|
||||
getUserLocale: () => Promise<
|
||||
"en" | "zh" | "ja" | "zh-Hant" | undefined
|
||||
>
|
||||
/** Set user's preferred locale */
|
||||
setUserLocale: (locale: string) => Promise<SetUserLocaleResult>
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ interface ConfigPresetsFile {
|
||||
version: 1
|
||||
currentPresetId: string | null
|
||||
presets: ConfigPreset[]
|
||||
userLocale?: "en" | "zh" | "ja"
|
||||
userLocale?: "en" | "zh" | "ja" | "zh-Hant"
|
||||
}
|
||||
|
||||
const CONFIG_FILE_NAME = "config-presets.json"
|
||||
@@ -470,7 +470,7 @@ export function getCurrentPresetEnv(): Record<string, string> {
|
||||
* Get user's preferred locale from config
|
||||
* Returns undefined if not set
|
||||
*/
|
||||
export function getUserLocale(): "en" | "zh" | "ja" | undefined {
|
||||
export function getUserLocale(): "en" | "zh" | "ja" | "zh-Hant" | undefined {
|
||||
const data = loadPresets()
|
||||
return data.userLocale
|
||||
}
|
||||
@@ -478,7 +478,9 @@ export function getUserLocale(): "en" | "zh" | "ja" | undefined {
|
||||
/**
|
||||
* 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()
|
||||
data.userLocale = locale === null ? undefined : locale
|
||||
savePresets(data)
|
||||
|
||||
@@ -263,12 +263,12 @@ export function registerIpcHandlers(): void {
|
||||
|
||||
ipcMain.handle("set-user-locale", (_event, locale: string) => {
|
||||
// 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" }
|
||||
}
|
||||
|
||||
try {
|
||||
setUserLocale(locale as "en" | "zh" | "ja")
|
||||
setUserLocale(locale as "en" | "zh" | "ja" | "zh-Hant")
|
||||
// Rebuild the menu to reflect the new locale
|
||||
rebuildAppMenu()
|
||||
return { success: true }
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { getUserLocale } from "./config-manager"
|
||||
|
||||
export type MenuLocale = "en" | "zh" | "ja"
|
||||
export type MenuLocale = "en" | "zh" | "ja" | "zh-Hant"
|
||||
|
||||
export interface MenuTranslations {
|
||||
// App menu (macOS only)
|
||||
@@ -119,6 +119,34 @@ const translations: Record<MenuLocale, MenuTranslations> = {
|
||||
documentation: "ドキュメント",
|
||||
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
|
||||
*/
|
||||
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")
|
||||
const normalized = locale.toLowerCase().split("-")[0]
|
||||
|
||||
@@ -136,10 +174,21 @@ export function getMenuTranslations(locale: string): MenuTranslations {
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
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 === "ja") return "ja"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import { getApiEndpoint } from "@/lib/base-path"
|
||||
import type { FlattenedServerModel } from "@/lib/server-model-config"
|
||||
import { STORAGE_KEYS } from "@/lib/storage"
|
||||
import {
|
||||
@@ -147,7 +148,7 @@ export function useModelConfig(): UseModelConfigReturn {
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return
|
||||
|
||||
fetch("/api/server-models")
|
||||
fetch(getApiEndpoint("/api/server-models"))
|
||||
.then((res) => {
|
||||
if (!res.ok) {
|
||||
console.error(
|
||||
|
||||
@@ -39,7 +39,7 @@ export interface ClientOverrides {
|
||||
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[] = [
|
||||
"openai",
|
||||
"anthropic",
|
||||
@@ -53,6 +53,7 @@ const ALLOWED_CLIENT_PROVIDERS: ProviderName[] = [
|
||||
"sglang",
|
||||
"gateway",
|
||||
"edgeone",
|
||||
"ollama",
|
||||
"doubao",
|
||||
"modelscope",
|
||||
]
|
||||
@@ -572,12 +573,13 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
|
||||
// SECURITY: Prevent SSRF attacks (GHSA-9qf7-mprq-9qgm)
|
||||
// If a custom baseUrl is provided, an API key MUST also be provided.
|
||||
// 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 (
|
||||
overrides?.baseUrl &&
|
||||
!overrides?.apiKey &&
|
||||
!(overrides?.provider === "vertexai" && overrides?.vertexApiKey) &&
|
||||
overrides?.provider !== "edgeone"
|
||||
overrides?.provider !== "edgeone" &&
|
||||
overrides?.provider !== "ollama"
|
||||
) {
|
||||
throw new Error(
|
||||
`API key is required when using a custom base URL. ` +
|
||||
@@ -836,16 +838,16 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
|
||||
break
|
||||
}
|
||||
|
||||
case "ollama":
|
||||
if (process.env.OLLAMA_BASE_URL) {
|
||||
const customOllama = createOllama({
|
||||
baseURL: process.env.OLLAMA_BASE_URL,
|
||||
})
|
||||
case "ollama": {
|
||||
const baseURL = overrides?.baseUrl || process.env.OLLAMA_BASE_URL
|
||||
if (baseURL) {
|
||||
const customOllama = createOllama({ baseURL })
|
||||
model = customOllama(modelId)
|
||||
} else {
|
||||
model = ollama(modelId)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "openrouter": {
|
||||
const apiKey = resolveApiKey(overrides, "OPENROUTER_API_KEY")
|
||||
@@ -1159,8 +1161,15 @@ export function supportsImageInput(modelId: string): boolean {
|
||||
lowerModelId.includes("vision") || lowerModelId.includes("vl")
|
||||
|
||||
// Models that DON'T support image/vision input (unless vision variant)
|
||||
// Kimi K2 models don't support images
|
||||
if (lowerModelId.includes("kimi") && !hasVisionIndicator) {
|
||||
// Kimi K2 doesn't support images, but K2.5 does
|
||||
// Only block kimi-k2 specifically, not other Kimi models
|
||||
if (
|
||||
(lowerModelId.includes("kimi-k2") ||
|
||||
lowerModelId.includes("kimi_k2")) &&
|
||||
!hasVisionIndicator &&
|
||||
!lowerModelId.includes("2.5") &&
|
||||
!lowerModelId.includes("k2.5")
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export const i18n = {
|
||||
defaultLocale: "en",
|
||||
locales: ["en", "zh", "ja"],
|
||||
locales: ["en", "zh", "ja", "zh-Hant"],
|
||||
} as const
|
||||
|
||||
export type Locale = (typeof i18n)["locales"][number]
|
||||
|
||||
@@ -6,6 +6,8 @@ const dictionaries = {
|
||||
en: () => import("./dictionaries/en.json").then((m) => m.default),
|
||||
zh: () => import("./dictionaries/zh.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"]>>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"chat": {
|
||||
"placeholder": "Describe your diagram or upload a file...",
|
||||
"send": "Send",
|
||||
"sending": "Sending...",
|
||||
"stopGeneration": "Stop generation",
|
||||
"sendMessage": "Send message",
|
||||
"clearConversation": "Clear conversation",
|
||||
"diagramHistory": "Diagram history",
|
||||
@@ -77,6 +77,8 @@
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"description": "Configure your application settings.",
|
||||
"apiKeysModels": "API Keys & Models",
|
||||
"apiKeysModelsDescription": "Configure AI providers and API keys.",
|
||||
"accessCode": "Access Code",
|
||||
"accessCodePlaceholder": "Enter access code",
|
||||
"accessCodeDescription": "Required to use this application.",
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"chat": {
|
||||
"placeholder": "ダイアグラムを説明するか、ファイルをアップロード...",
|
||||
"send": "送信",
|
||||
"sending": "送信中...",
|
||||
"stopGeneration": "生成を停止",
|
||||
"sendMessage": "メッセージを送信",
|
||||
"clearConversation": "会話をクリア",
|
||||
"diagramHistory": "ダイアグラム履歴",
|
||||
@@ -77,6 +77,8 @@
|
||||
"settings": {
|
||||
"title": "設定",
|
||||
"description": "アプリケーション設定を構成します。",
|
||||
"apiKeysModels": "API キーとモデル",
|
||||
"apiKeysModelsDescription": "AI プロバイダーと API キーを設定します。",
|
||||
"accessCode": "アクセスコード",
|
||||
"accessCodePlaceholder": "アクセスコードを入力",
|
||||
"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": "傳送",
|
||||
"stopGeneration": "停止產生",
|
||||
"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": "伺服器預設模型"
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@
|
||||
"chat": {
|
||||
"placeholder": "描述您的图表或上传文件...",
|
||||
"send": "发送",
|
||||
"sending": "发送中...",
|
||||
"stopGeneration": "停止生成",
|
||||
"sendMessage": "发送消息",
|
||||
"clearConversation": "清除对话",
|
||||
"diagramHistory": "图表历史",
|
||||
@@ -77,6 +77,8 @@
|
||||
"settings": {
|
||||
"title": "设置",
|
||||
"description": "配置您的应用程序设置。",
|
||||
"apiKeysModels": "API 密钥和模型",
|
||||
"apiKeysModelsDescription": "配置 AI 提供商和 API 密钥。",
|
||||
"accessCode": "访问码",
|
||||
"accessCodePlaceholder": "输入访问码",
|
||||
"accessCodeDescription": "使用此应用程序需要访问码。",
|
||||
|
||||
@@ -11,6 +11,7 @@ export const DEFAULT_SYSTEM_PROMPT = `
|
||||
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.
|
||||
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.
|
||||
After generating or editing a diagram, you don't need to say anything. The user can see the diagram - no need to describe it.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { z } from "zod"
|
||||
import { getApiEndpoint } from "@/lib/base-path"
|
||||
|
||||
export interface UrlData {
|
||||
url: string
|
||||
@@ -15,7 +16,7 @@ const UrlResponseSchema = z.object({
|
||||
})
|
||||
|
||||
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",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ url }),
|
||||
|
||||
542
package-lock.json
generated
542
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -168,6 +168,11 @@ describe("supportsImageInput", () => {
|
||||
expect(supportsImageInput("moonshot/kimi-k2")).toBe(false)
|
||||
})
|
||||
|
||||
it("returns true for Kimi K2.5 models (supports vision)", () => {
|
||||
expect(supportsImageInput("kimi-k2.5")).toBe(true)
|
||||
expect(supportsImageInput("moonshotai/kimi-k2.5")).toBe(true)
|
||||
})
|
||||
|
||||
it("returns false for DeepSeek text models", () => {
|
||||
expect(supportsImageInput("deepseek-chat")).toBe(false)
|
||||
expect(supportsImageInput("deepseek-coder")).toBe(false)
|
||||
|
||||
Reference in New Issue
Block a user