mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-03 06:42:27 +08:00
Compare commits
3 Commits
chore/clea
...
chore/clea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
950a8754fa | ||
|
|
76c0308ceb | ||
|
|
d40ab4114c |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -62,7 +62,4 @@ push-via-ec2.sh
|
|||||||
*.snap
|
*.snap
|
||||||
|
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
.spec-workflow
|
.spec-workflow
|
||||||
|
|
||||||
# edgeone
|
|
||||||
.edgeone
|
|
||||||
12
README.md
12
README.md
@@ -237,18 +237,6 @@ npm run dev
|
|||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
### Deploy to EdgeOne Pages
|
|
||||||
|
|
||||||
You can deploy with one click using [Tencent EdgeOne Pages](https://pages.edgeone.ai/).
|
|
||||||
|
|
||||||
Deploy by this button:
|
|
||||||
|
|
||||||
[](https://edgeone.ai/pages/new?repository-url=https%3A%2F%2Fgithub.com%2FDayuanJiang%2Fnext-ai-draw-io)
|
|
||||||
|
|
||||||
Check out the [Tencent EdgeOne Pages documentation](https://pages.edgeone.ai/document/deployment-overview) for more details.
|
|
||||||
|
|
||||||
Additionally, deploying through Tencent EdgeOne Pages will also grant you a [daily free quota for DeepSeek models](https://pages.edgeone.ai/document/edge-ai).
|
|
||||||
|
|
||||||
### Deploy on Vercel (Recommended)
|
### Deploy on Vercel (Recommended)
|
||||||
|
|
||||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FDayuanJiang%2Fnext-ai-draw-io)
|
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FDayuanJiang%2Fnext-ai-draw-io)
|
||||||
|
|||||||
@@ -72,6 +72,28 @@ export default function AboutCN() {
|
|||||||
<p className="text-xl text-gray-600 font-medium">
|
<p className="text-xl text-gray-600 font-medium">
|
||||||
AI驱动的图表创建工具 - 对话、绘制、可视化
|
AI驱动的图表创建工具 - 对话、绘制、可视化
|
||||||
</p>
|
</p>
|
||||||
|
<div className="flex justify-center gap-4 mt-4 text-sm">
|
||||||
|
<Link
|
||||||
|
href="/about"
|
||||||
|
className="text-gray-600 hover:text-blue-600"
|
||||||
|
>
|
||||||
|
English
|
||||||
|
</Link>
|
||||||
|
<span className="text-gray-400">|</span>
|
||||||
|
<Link
|
||||||
|
href="/about/cn"
|
||||||
|
className="text-blue-600 font-semibold"
|
||||||
|
>
|
||||||
|
中文
|
||||||
|
</Link>
|
||||||
|
<span className="text-gray-400">|</span>
|
||||||
|
<Link
|
||||||
|
href="/about/ja"
|
||||||
|
className="text-gray-600 hover:text-blue-600"
|
||||||
|
>
|
||||||
|
日本語
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative mb-8 rounded-2xl bg-gradient-to-br from-amber-50 via-orange-50 to-yellow-50 p-[1px] shadow-lg">
|
<div className="relative mb-8 rounded-2xl bg-gradient-to-br from-amber-50 via-orange-50 to-yellow-50 p-[1px] shadow-lg">
|
||||||
|
|||||||
@@ -80,6 +80,28 @@ export default function AboutJA() {
|
|||||||
AI搭載のダイアグラム作成ツール -
|
AI搭載のダイアグラム作成ツール -
|
||||||
チャット、描画、可視化
|
チャット、描画、可視化
|
||||||
</p>
|
</p>
|
||||||
|
<div className="flex justify-center gap-4 mt-4 text-sm">
|
||||||
|
<Link
|
||||||
|
href="/about"
|
||||||
|
className="text-gray-600 hover:text-blue-600"
|
||||||
|
>
|
||||||
|
English
|
||||||
|
</Link>
|
||||||
|
<span className="text-gray-400">|</span>
|
||||||
|
<Link
|
||||||
|
href="/about/cn"
|
||||||
|
className="text-gray-600 hover:text-blue-600"
|
||||||
|
>
|
||||||
|
中文
|
||||||
|
</Link>
|
||||||
|
<span className="text-gray-400">|</span>
|
||||||
|
<Link
|
||||||
|
href="/about/ja"
|
||||||
|
className="text-blue-600 font-semibold"
|
||||||
|
>
|
||||||
|
日本語
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative mb-8 rounded-2xl bg-gradient-to-br from-amber-50 via-orange-50 to-yellow-50 p-[1px] shadow-lg">
|
<div className="relative mb-8 rounded-2xl bg-gradient-to-br from-amber-50 via-orange-50 to-yellow-50 p-[1px] shadow-lg">
|
||||||
|
|||||||
@@ -80,6 +80,28 @@ export default function About() {
|
|||||||
AI-Powered Diagram Creation Tool - Chat, Draw,
|
AI-Powered Diagram Creation Tool - Chat, Draw,
|
||||||
Visualize
|
Visualize
|
||||||
</p>
|
</p>
|
||||||
|
<div className="flex justify-center gap-4 mt-4 text-sm">
|
||||||
|
<Link
|
||||||
|
href="/about"
|
||||||
|
className="text-blue-600 font-semibold"
|
||||||
|
>
|
||||||
|
English
|
||||||
|
</Link>
|
||||||
|
<span className="text-gray-400">|</span>
|
||||||
|
<Link
|
||||||
|
href="/about/cn"
|
||||||
|
className="text-gray-600 hover:text-blue-600"
|
||||||
|
>
|
||||||
|
中文
|
||||||
|
</Link>
|
||||||
|
<span className="text-gray-400">|</span>
|
||||||
|
<Link
|
||||||
|
href="/about/ja"
|
||||||
|
className="text-gray-600 hover:text-blue-600"
|
||||||
|
>
|
||||||
|
日本語
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative mb-8 rounded-2xl bg-gradient-to-br from-amber-50 via-orange-50 to-yellow-50 p-[1px] shadow-lg">
|
<div className="relative mb-8 rounded-2xl bg-gradient-to-br from-amber-50 via-orange-50 to-yellow-50 p-[1px] shadow-lg">
|
||||||
|
|||||||
@@ -244,22 +244,9 @@ async function handleChatRequest(req: Request): Promise<Response> {
|
|||||||
// === CACHE CHECK END ===
|
// === CACHE CHECK END ===
|
||||||
|
|
||||||
// Read client AI provider overrides from headers
|
// Read client AI provider overrides from headers
|
||||||
const provider = req.headers.get("x-ai-provider")
|
|
||||||
let baseUrl = req.headers.get("x-ai-base-url")
|
|
||||||
|
|
||||||
// For EdgeOne provider, construct full URL from request origin
|
|
||||||
// because createOpenAI needs absolute URL, not relative path
|
|
||||||
if (provider === "edgeone" && !baseUrl) {
|
|
||||||
const origin = req.headers.get("origin") || new URL(req.url).origin
|
|
||||||
baseUrl = `${origin}/api/edgeai`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get cookie header for EdgeOne authentication (eo_token, eo_time)
|
|
||||||
const cookieHeader = req.headers.get("cookie")
|
|
||||||
|
|
||||||
const clientOverrides = {
|
const clientOverrides = {
|
||||||
provider,
|
provider: req.headers.get("x-ai-provider"),
|
||||||
baseUrl,
|
baseUrl: req.headers.get("x-ai-base-url"),
|
||||||
apiKey: req.headers.get("x-ai-api-key"),
|
apiKey: req.headers.get("x-ai-api-key"),
|
||||||
modelId: req.headers.get("x-ai-model"),
|
modelId: req.headers.get("x-ai-model"),
|
||||||
// AWS Bedrock credentials
|
// AWS Bedrock credentials
|
||||||
@@ -267,11 +254,6 @@ async function handleChatRequest(req: Request): Promise<Response> {
|
|||||||
awsSecretAccessKey: req.headers.get("x-aws-secret-access-key"),
|
awsSecretAccessKey: req.headers.get("x-aws-secret-access-key"),
|
||||||
awsRegion: req.headers.get("x-aws-region"),
|
awsRegion: req.headers.get("x-aws-region"),
|
||||||
awsSessionToken: req.headers.get("x-aws-session-token"),
|
awsSessionToken: req.headers.get("x-aws-session-token"),
|
||||||
// Pass cookies for EdgeOne Pages authentication
|
|
||||||
...(provider === "edgeone" &&
|
|
||||||
cookieHeader && {
|
|
||||||
headers: { cookie: cookieHeader },
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read minimal style preference from header
|
// Read minimal style preference from header
|
||||||
@@ -623,7 +605,7 @@ Notes:
|
|||||||
Operations:
|
Operations:
|
||||||
- update: Replace an existing cell by its id. Provide cell_id and complete new_xml.
|
- update: Replace an existing cell by its id. Provide cell_id and complete new_xml.
|
||||||
- add: Add a new cell. Provide cell_id (new unique id) and new_xml.
|
- add: Add a new cell. Provide cell_id (new unique id) and new_xml.
|
||||||
- delete: Remove a cell. Cascade is automatic: children AND edges (source/target) are auto-deleted. Only specify ONE cell_id.
|
- delete: Remove a cell by its id. Only cell_id is needed.
|
||||||
|
|
||||||
For update/add, new_xml must be a complete mxCell element including mxGeometry.
|
For update/add, new_xml must be a complete mxCell element including mxGeometry.
|
||||||
|
|
||||||
@@ -632,8 +614,8 @@ For update/add, new_xml must be a complete mxCell element including mxGeometry.
|
|||||||
Example - Add a rectangle:
|
Example - Add a rectangle:
|
||||||
{"operations": [{"operation": "add", "cell_id": "rect-1", "new_xml": "<mxCell id=\\"rect-1\\" value=\\"Hello\\" style=\\"rounded=0;\\" vertex=\\"1\\" parent=\\"1\\"><mxGeometry x=\\"100\\" y=\\"100\\" width=\\"120\\" height=\\"60\\" as=\\"geometry\\"/></mxCell>"}]}
|
{"operations": [{"operation": "add", "cell_id": "rect-1", "new_xml": "<mxCell id=\\"rect-1\\" value=\\"Hello\\" style=\\"rounded=0;\\" vertex=\\"1\\" parent=\\"1\\"><mxGeometry x=\\"100\\" y=\\"100\\" width=\\"120\\" height=\\"60\\" as=\\"geometry\\"/></mxCell>"}]}
|
||||||
|
|
||||||
Example - Delete container (children & edges auto-deleted):
|
Example - Delete a cell:
|
||||||
{"operations": [{"operation": "delete", "cell_id": "2"}]}`,
|
{"operations": [{"operation": "delete", "cell_id": "rect-1"}]}`,
|
||||||
inputSchema: z.object({
|
inputSchema: z.object({
|
||||||
operations: z
|
operations: z
|
||||||
.array(
|
.array(
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export async function POST(req: Request) {
|
|||||||
{ status: 400 },
|
{ status: 400 },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else if (provider !== "ollama" && provider !== "edgeone" && !apiKey) {
|
} else if (provider !== "ollama" && !apiKey) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ valid: false, error: "API key is required" },
|
{ valid: false, error: "API key is required" },
|
||||||
{ status: 400 },
|
{ status: 400 },
|
||||||
@@ -225,21 +225,6 @@ export async function POST(req: Request) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
case "edgeone": {
|
|
||||||
// EdgeOne uses OpenAI-compatible API via Edge Functions
|
|
||||||
// Need to pass cookies for EdgeOne Pages authentication
|
|
||||||
const cookieHeader = req.headers.get("cookie") || ""
|
|
||||||
const edgeone = createOpenAI({
|
|
||||||
apiKey: "edgeone", // EdgeOne doesn't require API key
|
|
||||||
baseURL: baseUrl || "/api/edgeai",
|
|
||||||
headers: {
|
|
||||||
cookie: cookieHeader,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
model = edgeone.chat(modelId)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case "sglang": {
|
case "sglang": {
|
||||||
// SGLang is OpenAI-compatible
|
// SGLang is OpenAI-compatible
|
||||||
const sglang = createOpenAI({
|
const sglang = createOpenAI({
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import {
|
|||||||
ReasoningTrigger,
|
ReasoningTrigger,
|
||||||
} from "@/components/ai-elements/reasoning"
|
} from "@/components/ai-elements/reasoning"
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||||
import { useDictionary } from "@/hooks/use-dictionary"
|
|
||||||
import { getApiEndpoint } from "@/lib/base-path"
|
import { getApiEndpoint } from "@/lib/base-path"
|
||||||
import {
|
import {
|
||||||
applyDiagramOperations,
|
applyDiagramOperations,
|
||||||
@@ -206,7 +205,6 @@ export function ChatMessageDisplay({
|
|||||||
onEditMessage,
|
onEditMessage,
|
||||||
status = "idle",
|
status = "idle",
|
||||||
}: ChatMessageDisplayProps) {
|
}: ChatMessageDisplayProps) {
|
||||||
const dict = useDictionary()
|
|
||||||
const { chartXML, loadDiagram: onDisplayChart } = useDiagram()
|
const { chartXML, loadDiagram: onDisplayChart } = useDiagram()
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||||
const previousXML = useRef<string>("")
|
const previousXML = useRef<string>("")
|
||||||
@@ -270,7 +268,9 @@ export function ChatMessageDisplay({
|
|||||||
setTimeout(() => setCopiedMessageId(null), 2000)
|
setTimeout(() => setCopiedMessageId(null), 2000)
|
||||||
} catch (fallbackErr) {
|
} catch (fallbackErr) {
|
||||||
console.error("Failed to copy message:", fallbackErr)
|
console.error("Failed to copy message:", fallbackErr)
|
||||||
toast.error(dict.chat.failedToCopyDetail)
|
toast.error(
|
||||||
|
"Failed to copy message. Please copy manually or check clipboard permissions.",
|
||||||
|
)
|
||||||
setCopyFailedMessageId(messageId)
|
setCopyFailedMessageId(messageId)
|
||||||
setTimeout(() => setCopyFailedMessageId(null), 2000)
|
setTimeout(() => setCopyFailedMessageId(null), 2000)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -304,7 +304,7 @@ export function ChatMessageDisplay({
|
|||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to log feedback:", error)
|
console.error("Failed to log feedback:", error)
|
||||||
toast.error(dict.errors.failedToRecordFeedback)
|
toast.error("Failed to record your feedback. Please try again.")
|
||||||
// Revert optimistic UI update
|
// Revert optimistic UI update
|
||||||
setFeedback((prev) => {
|
setFeedback((prev) => {
|
||||||
const next = { ...prev }
|
const next = { ...prev }
|
||||||
@@ -349,7 +349,9 @@ export function ChatMessageDisplay({
|
|||||||
console.error(
|
console.error(
|
||||||
"[ChatMessageDisplay] Malformed XML detected in final output",
|
"[ChatMessageDisplay] Malformed XML detected in final output",
|
||||||
)
|
)
|
||||||
toast.error(dict.errors.malformedXml)
|
toast.error(
|
||||||
|
"AI generated invalid diagram XML. Please try regenerating.",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return // Skip this update
|
return // Skip this update
|
||||||
}
|
}
|
||||||
@@ -400,7 +402,9 @@ export function ChatMessageDisplay({
|
|||||||
"[ChatMessageDisplay] XML validation failed:",
|
"[ChatMessageDisplay] XML validation failed:",
|
||||||
validation.error,
|
validation.error,
|
||||||
)
|
)
|
||||||
toast.error(dict.errors.validationFailed)
|
toast.error(
|
||||||
|
"Diagram validation failed. Please try regenerating.",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error(
|
||||||
@@ -409,7 +413,9 @@ export function ChatMessageDisplay({
|
|||||||
)
|
)
|
||||||
// Only show toast if this is the final XML (not during streaming)
|
// Only show toast if this is the final XML (not during streaming)
|
||||||
if (showToast) {
|
if (showToast) {
|
||||||
toast.error(dict.errors.failedToProcess)
|
toast.error(
|
||||||
|
"Failed to process diagram. Please try regenerating.",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -826,10 +832,7 @@ export function ChatMessageDisplay({
|
|||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
className="p-1.5 rounded-lg text-muted-foreground/60 hover:text-muted-foreground hover:bg-muted transition-colors"
|
className="p-1.5 rounded-lg text-muted-foreground/60 hover:text-muted-foreground hover:bg-muted transition-colors"
|
||||||
title={
|
title="Edit message"
|
||||||
dict.chat
|
|
||||||
.editMessage
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Pencil className="h-3.5 w-3.5" />
|
<Pencil className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
@@ -846,13 +849,11 @@ export function ChatMessageDisplay({
|
|||||||
title={
|
title={
|
||||||
copiedMessageId ===
|
copiedMessageId ===
|
||||||
message.id
|
message.id
|
||||||
? dict.chat.copied
|
? "Copied!"
|
||||||
: copyFailedMessageId ===
|
: copyFailedMessageId ===
|
||||||
message.id
|
message.id
|
||||||
? dict.chat
|
? "Failed to copy"
|
||||||
.failedToCopy
|
: "Copy message"
|
||||||
: dict.chat
|
|
||||||
.copyResponse
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{copiedMessageId ===
|
{copiedMessageId ===
|
||||||
@@ -967,7 +968,7 @@ export function ChatMessageDisplay({
|
|||||||
}}
|
}}
|
||||||
className="px-3 py-1.5 text-xs rounded-lg bg-muted hover:bg-muted/80 transition-colors"
|
className="px-3 py-1.5 text-xs rounded-lg bg-muted hover:bg-muted/80 transition-colors"
|
||||||
>
|
>
|
||||||
{dict.common.cancel}
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -989,7 +990,7 @@ export function ChatMessageDisplay({
|
|||||||
disabled={!editText.trim()}
|
disabled={!editText.trim()}
|
||||||
className="px-3 py-1.5 text-xs rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50 transition-colors"
|
className="px-3 py-1.5 text-xs rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50 transition-colors"
|
||||||
>
|
>
|
||||||
{dict.chat.saveAndSubmit}
|
Save & Submit
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1122,8 +1123,7 @@ export function ChatMessageDisplay({
|
|||||||
"user" &&
|
"user" &&
|
||||||
isLastUserMessage &&
|
isLastUserMessage &&
|
||||||
onEditMessage
|
onEditMessage
|
||||||
? dict.chat
|
? "Click to edit"
|
||||||
.clickToEdit
|
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -1325,8 +1325,8 @@ export function ChatMessageDisplay({
|
|||||||
title={
|
title={
|
||||||
copiedMessageId ===
|
copiedMessageId ===
|
||||||
message.id
|
message.id
|
||||||
? dict.chat.copied
|
? "Copied!"
|
||||||
: dict.chat.copyResponse
|
: "Copy response"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{copiedMessageId ===
|
{copiedMessageId ===
|
||||||
@@ -1352,9 +1352,7 @@ export function ChatMessageDisplay({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
className="p-1.5 rounded-lg text-muted-foreground/60 hover:text-foreground hover:bg-muted transition-colors"
|
className="p-1.5 rounded-lg text-muted-foreground/60 hover:text-foreground hover:bg-muted transition-colors"
|
||||||
title={
|
title="Regenerate response"
|
||||||
dict.chat.regenerate
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<RotateCcw className="h-3.5 w-3.5" />
|
<RotateCcw className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
@@ -1376,7 +1374,7 @@ export function ChatMessageDisplay({
|
|||||||
? "text-green-600 bg-green-100"
|
? "text-green-600 bg-green-100"
|
||||||
: "text-muted-foreground/60 hover:text-green-600 hover:bg-green-50"
|
: "text-muted-foreground/60 hover:text-green-600 hover:bg-green-50"
|
||||||
}`}
|
}`}
|
||||||
title={dict.chat.goodResponse}
|
title="Good response"
|
||||||
>
|
>
|
||||||
<ThumbsUp className="h-3.5 w-3.5" />
|
<ThumbsUp className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
@@ -1395,7 +1393,7 @@ export function ChatMessageDisplay({
|
|||||||
? "text-red-600 bg-red-100"
|
? "text-red-600 bg-red-100"
|
||||||
: "text-muted-foreground/60 hover:text-red-600 hover:bg-red-50"
|
: "text-muted-foreground/60 hover:text-red-600 hover:bg-red-50"
|
||||||
}`}
|
}`}
|
||||||
title={dict.chat.badResponse}
|
title="Bad response"
|
||||||
>
|
>
|
||||||
<ThumbsDown className="h-3.5 w-3.5" />
|
<ThumbsDown className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -3,12 +3,14 @@
|
|||||||
import { useChat } from "@ai-sdk/react"
|
import { useChat } from "@ai-sdk/react"
|
||||||
import { DefaultChatTransport } from "ai"
|
import { DefaultChatTransport } from "ai"
|
||||||
import {
|
import {
|
||||||
|
AlertTriangle,
|
||||||
MessageSquarePlus,
|
MessageSquarePlus,
|
||||||
PanelRightClose,
|
PanelRightClose,
|
||||||
PanelRightOpen,
|
PanelRightOpen,
|
||||||
Settings,
|
Settings,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
|
import Link from "next/link"
|
||||||
import type React from "react"
|
import type React from "react"
|
||||||
import { useCallback, useEffect, useRef, useState } from "react"
|
import { useCallback, useEffect, useRef, useState } from "react"
|
||||||
import { flushSync } from "react-dom"
|
import { flushSync } from "react-dom"
|
||||||
@@ -24,7 +26,6 @@ import { useDictionary } from "@/hooks/use-dictionary"
|
|||||||
import { getSelectedAIConfig, useModelConfig } from "@/hooks/use-model-config"
|
import { getSelectedAIConfig, useModelConfig } from "@/hooks/use-model-config"
|
||||||
import { getApiEndpoint } from "@/lib/base-path"
|
import { getApiEndpoint } from "@/lib/base-path"
|
||||||
import { findCachedResponse } from "@/lib/cached-responses"
|
import { findCachedResponse } from "@/lib/cached-responses"
|
||||||
import { formatMessage } from "@/lib/i18n/utils"
|
|
||||||
import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
|
import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
|
||||||
import { type FileData, useFileProcessor } from "@/lib/use-file-processor"
|
import { type FileData, useFileProcessor } from "@/lib/use-file-processor"
|
||||||
import { useQuotaManager } from "@/lib/use-quota-manager"
|
import { useQuotaManager } from "@/lib/use-quota-manager"
|
||||||
@@ -388,9 +389,7 @@ export default function ChatPanel({
|
|||||||
MAX_CONTINUATION_RETRY_COUNT
|
MAX_CONTINUATION_RETRY_COUNT
|
||||||
) {
|
) {
|
||||||
toast.error(
|
toast.error(
|
||||||
formatMessage(dict.errors.continuationRetryLimit, {
|
`Continuation retry limit reached (${MAX_CONTINUATION_RETRY_COUNT}). The diagram may be too complex.`,
|
||||||
max: MAX_CONTINUATION_RETRY_COUNT,
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
continuationRetryCountRef.current = 0
|
continuationRetryCountRef.current = 0
|
||||||
partialXmlRef.current = ""
|
partialXmlRef.current = ""
|
||||||
@@ -401,9 +400,7 @@ export default function ChatPanel({
|
|||||||
// Regular error: check retry count limit
|
// Regular error: check retry count limit
|
||||||
if (autoRetryCountRef.current >= MAX_AUTO_RETRY_COUNT) {
|
if (autoRetryCountRef.current >= MAX_AUTO_RETRY_COUNT) {
|
||||||
toast.error(
|
toast.error(
|
||||||
formatMessage(dict.errors.retryLimit, {
|
`Auto-retry limit reached (${MAX_AUTO_RETRY_COUNT}). Please try again manually.`,
|
||||||
max: MAX_AUTO_RETRY_COUNT,
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
autoRetryCountRef.current = 0
|
autoRetryCountRef.current = 0
|
||||||
partialXmlRef.current = ""
|
partialXmlRef.current = ""
|
||||||
@@ -453,7 +450,7 @@ export default function ChatPanel({
|
|||||||
// On complete failure, clear storage to allow recovery
|
// On complete failure, clear storage to allow recovery
|
||||||
localStorage.removeItem(STORAGE_MESSAGES_KEY)
|
localStorage.removeItem(STORAGE_MESSAGES_KEY)
|
||||||
localStorage.removeItem(STORAGE_XML_SNAPSHOTS_KEY)
|
localStorage.removeItem(STORAGE_XML_SNAPSHOTS_KEY)
|
||||||
toast.error(dict.errors.sessionCorrupted)
|
toast.error("Session data was corrupted. Starting fresh.")
|
||||||
}
|
}
|
||||||
}, [setMessages])
|
}, [setMessages])
|
||||||
|
|
||||||
@@ -654,10 +651,12 @@ export default function ChatPanel({
|
|||||||
localStorage.removeItem(STORAGE_DIAGRAM_XML_KEY)
|
localStorage.removeItem(STORAGE_DIAGRAM_XML_KEY)
|
||||||
localStorage.setItem(STORAGE_SESSION_ID_KEY, newSessionId)
|
localStorage.setItem(STORAGE_SESSION_ID_KEY, newSessionId)
|
||||||
sessionStorage.removeItem(SESSION_STORAGE_INPUT_KEY)
|
sessionStorage.removeItem(SESSION_STORAGE_INPUT_KEY)
|
||||||
toast.success(dict.dialogs.clearSuccess)
|
toast.success("Started a fresh chat")
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to clear localStorage:", error)
|
console.error("Failed to clear localStorage:", error)
|
||||||
toast.warning(dict.errors.storageUpdateFailed)
|
toast.warning(
|
||||||
|
"Chat cleared but browser storage could not be updated",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
setShowNewChatDialog(false)
|
setShowNewChatDialog(false)
|
||||||
@@ -890,7 +889,7 @@ export default function ChatPanel({
|
|||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col items-center pt-4 bg-card border border-border/30 rounded-xl">
|
<div className="h-full flex flex-col items-center pt-4 bg-card border border-border/30 rounded-xl">
|
||||||
<ButtonWithTooltip
|
<ButtonWithTooltip
|
||||||
tooltipContent={dict.nav.showPanel}
|
tooltipContent="Show chat panel (Ctrl+B)"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={onToggleVisibility}
|
onClick={onToggleVisibility}
|
||||||
@@ -905,7 +904,7 @@ export default function ChatPanel({
|
|||||||
transform: "rotate(180deg)",
|
transform: "rotate(180deg)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{dict.nav.aiChat}
|
AI Chat
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -950,6 +949,32 @@ export default function ChatPanel({
|
|||||||
Next AI Drawio
|
Next AI Drawio
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
{!isMobile && (
|
||||||
|
<Link
|
||||||
|
href="/about"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-sm text-muted-foreground hover:text-foreground transition-colors ml-2"
|
||||||
|
>
|
||||||
|
About
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
{!isMobile && (
|
||||||
|
<Link
|
||||||
|
href="/about"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<ButtonWithTooltip
|
||||||
|
tooltipContent="Sponsored by ByteDance Doubao K2-thinking. See About page for details."
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-6 w-6 text-amber-500 hover:text-amber-600"
|
||||||
|
>
|
||||||
|
<AlertTriangle className="h-4 w-4" />
|
||||||
|
</ButtonWithTooltip>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1 justify-end overflow-visible">
|
<div className="flex items-center gap-1 justify-end overflow-visible">
|
||||||
<ButtonWithTooltip
|
<ButtonWithTooltip
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from "react"
|
import { useEffect, useRef, useState } from "react"
|
||||||
import { useDictionary } from "@/hooks/use-dictionary"
|
|
||||||
import { wrapWithMxFile } from "@/lib/utils"
|
import { wrapWithMxFile } from "@/lib/utils"
|
||||||
|
|
||||||
// Dev XML presets for streaming simulator
|
// Dev XML presets for streaming simulator
|
||||||
@@ -143,7 +142,6 @@ export function DevXmlSimulator({
|
|||||||
onDisplayChart,
|
onDisplayChart,
|
||||||
onShowQuotaToast,
|
onShowQuotaToast,
|
||||||
}: DevXmlSimulatorProps) {
|
}: DevXmlSimulatorProps) {
|
||||||
const dict = useDictionary()
|
|
||||||
const [devXml, setDevXml] = useState("")
|
const [devXml, setDevXml] = useState("")
|
||||||
const [isSimulating, setIsSimulating] = useState(false)
|
const [isSimulating, setIsSimulating] = useState(false)
|
||||||
const [devIntervalMs, setDevIntervalMs] = useState(1)
|
const [devIntervalMs, setDevIntervalMs] = useState(1)
|
||||||
@@ -180,7 +178,7 @@ export function DevXmlSimulator({
|
|||||||
parts: [
|
parts: [
|
||||||
{
|
{
|
||||||
type: "text" as const,
|
type: "text" as const,
|
||||||
text: dict.dev.simulatingMessage,
|
text: "[Dev] Simulating XML streaming",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -230,7 +228,7 @@ export function DevXmlSimulator({
|
|||||||
const lastMsg = updated[updated.length - 1] as any
|
const lastMsg = updated[updated.length - 1] as any
|
||||||
if (lastMsg?.role === "assistant" && lastMsg.parts?.[0]) {
|
if (lastMsg?.role === "assistant" && lastMsg.parts?.[0]) {
|
||||||
lastMsg.parts[0].state = "output-available"
|
lastMsg.parts[0].state = "output-available"
|
||||||
lastMsg.parts[0].output = dict.dev.successMessage
|
lastMsg.parts[0].output = "Successfully displayed the diagram."
|
||||||
lastMsg.parts[0].input = { xml }
|
lastMsg.parts[0].input = { xml }
|
||||||
}
|
}
|
||||||
return updated
|
return updated
|
||||||
@@ -247,12 +245,12 @@ export function DevXmlSimulator({
|
|||||||
<div className="border-t border-dashed border-orange-500/50 px-4 py-2 bg-orange-50/50 dark:bg-orange-950/30">
|
<div className="border-t border-dashed border-orange-500/50 px-4 py-2 bg-orange-50/50 dark:bg-orange-950/30">
|
||||||
<details>
|
<details>
|
||||||
<summary className="text-xs text-orange-600 dark:text-orange-400 cursor-pointer font-medium">
|
<summary className="text-xs text-orange-600 dark:text-orange-400 cursor-pointer font-medium">
|
||||||
{dict.dev.title}
|
Dev: XML Streaming Simulator
|
||||||
</summary>
|
</summary>
|
||||||
<div className="mt-2 space-y-2">
|
<div className="mt-2 space-y-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<label className="text-xs text-muted-foreground whitespace-nowrap">
|
<label className="text-xs text-muted-foreground whitespace-nowrap">
|
||||||
{dict.dev.preset}
|
Preset:
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -264,7 +262,7 @@ export function DevXmlSimulator({
|
|||||||
defaultValue=""
|
defaultValue=""
|
||||||
>
|
>
|
||||||
<option value="" disabled>
|
<option value="" disabled>
|
||||||
{dict.dev.selectPreset}
|
Select a preset...
|
||||||
</option>
|
</option>
|
||||||
{Object.keys(DEV_XML_PRESETS).map((name) => (
|
{Object.keys(DEV_XML_PRESETS).map((name) => (
|
||||||
<option key={name} value={name}>
|
<option key={name} value={name}>
|
||||||
@@ -277,19 +275,19 @@ export function DevXmlSimulator({
|
|||||||
onClick={() => setDevXml("")}
|
onClick={() => setDevXml("")}
|
||||||
className="px-2 py-1 text-xs text-muted-foreground hover:text-foreground border rounded"
|
className="px-2 py-1 text-xs text-muted-foreground hover:text-foreground border rounded"
|
||||||
>
|
>
|
||||||
{dict.dev.clear}
|
Clear
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
value={devXml}
|
value={devXml}
|
||||||
onChange={(e) => setDevXml(e.target.value)}
|
onChange={(e) => setDevXml(e.target.value)}
|
||||||
placeholder={dict.dev.placeholder}
|
placeholder="Paste mxCell XML here or select a preset..."
|
||||||
className="w-full h-24 text-xs font-mono p-2 border rounded bg-background"
|
className="w-full h-24 text-xs font-mono p-2 border rounded bg-background"
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="flex items-center gap-2 flex-1">
|
<div className="flex items-center gap-2 flex-1">
|
||||||
<label className="text-xs text-muted-foreground whitespace-nowrap">
|
<label className="text-xs text-muted-foreground whitespace-nowrap">
|
||||||
{dict.dev.interval}
|
Interval:
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
@@ -308,7 +306,7 @@ export function DevXmlSimulator({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<label className="text-xs text-muted-foreground whitespace-nowrap">
|
<label className="text-xs text-muted-foreground whitespace-nowrap">
|
||||||
{dict.dev.chars}
|
Chars:
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
@@ -332,8 +330,8 @@ export function DevXmlSimulator({
|
|||||||
className="px-3 py-1 text-xs bg-orange-500 text-white rounded hover:bg-orange-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
className="px-3 py-1 text-xs bg-orange-500 text-white rounded hover:bg-orange-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
{isSimulating
|
{isSimulating
|
||||||
? dict.dev.streaming
|
? "Streaming..."
|
||||||
: `${dict.dev.simulate} (${devChunkSize} chars/${devIntervalMs}ms)`}
|
: `Simulate (${devChunkSize} chars/${devIntervalMs}ms)`}
|
||||||
</button>
|
</button>
|
||||||
{isSimulating && (
|
{isSimulating && (
|
||||||
<button
|
<button
|
||||||
@@ -343,7 +341,7 @@ export function DevXmlSimulator({
|
|||||||
}}
|
}}
|
||||||
className="px-3 py-1 text-xs bg-red-500 text-white rounded hover:bg-red-600"
|
className="px-3 py-1 text-xs bg-red-500 text-white rounded hover:bg-red-600"
|
||||||
>
|
>
|
||||||
{dict.dev.stop}
|
Stop
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{onShowQuotaToast && (
|
{onShowQuotaToast && (
|
||||||
@@ -352,7 +350,7 @@ export function DevXmlSimulator({
|
|||||||
onClick={onShowQuotaToast}
|
onClick={onShowQuotaToast}
|
||||||
className="px-3 py-1 text-xs bg-purple-500 text-white rounded hover:bg-purple-600"
|
className="px-3 py-1 text-xs bg-purple-500 text-white rounded hover:bg-purple-600"
|
||||||
>
|
>
|
||||||
{dict.dev.testQuotaToast}
|
Test Quota Toast
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -78,7 +78,6 @@ const PROVIDER_LOGO_MAP: Record<string, string> = {
|
|||||||
siliconflow: "siliconflow",
|
siliconflow: "siliconflow",
|
||||||
sglang: "openai", // SGLang is OpenAI-compatible
|
sglang: "openai", // SGLang is OpenAI-compatible
|
||||||
gateway: "vercel",
|
gateway: "vercel",
|
||||||
edgeone: "tencent-cloud",
|
|
||||||
doubao: "bytedance",
|
doubao: "bytedance",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,7 +277,6 @@ export function ModelConfigDialog({
|
|||||||
|
|
||||||
// Check credentials based on provider type
|
// Check credentials based on provider type
|
||||||
const isBedrock = selectedProvider.provider === "bedrock"
|
const isBedrock = selectedProvider.provider === "bedrock"
|
||||||
const isEdgeOne = selectedProvider.provider === "edgeone"
|
|
||||||
if (isBedrock) {
|
if (isBedrock) {
|
||||||
if (
|
if (
|
||||||
!selectedProvider.awsAccessKeyId ||
|
!selectedProvider.awsAccessKeyId ||
|
||||||
@@ -287,7 +285,7 @@ export function ModelConfigDialog({
|
|||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else if (!isEdgeOne && !selectedProvider.apiKey) {
|
} else if (!selectedProvider.apiKey) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,18 +308,13 @@ export function ModelConfigDialog({
|
|||||||
setValidatingModelIndex(i)
|
setValidatingModelIndex(i)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// For EdgeOne, construct baseUrl from current origin
|
|
||||||
const baseUrl = isEdgeOne
|
|
||||||
? `${window.location.origin}/api/edgeai`
|
|
||||||
: selectedProvider.baseUrl
|
|
||||||
|
|
||||||
const response = await fetch("/api/validate-model", {
|
const response = await fetch("/api/validate-model", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
provider: selectedProvider.provider,
|
provider: selectedProvider.provider,
|
||||||
apiKey: selectedProvider.apiKey,
|
apiKey: selectedProvider.apiKey,
|
||||||
baseUrl,
|
baseUrl: selectedProvider.baseUrl,
|
||||||
modelId: model.modelId,
|
modelId: model.modelId,
|
||||||
// AWS Bedrock credentials
|
// AWS Bedrock credentials
|
||||||
awsAccessKeyId: selectedProvider.awsAccessKeyId,
|
awsAccessKeyId: selectedProvider.awsAccessKeyId,
|
||||||
@@ -329,6 +322,7 @@ export function ModelConfigDialog({
|
|||||||
awsRegion: selectedProvider.awsRegion,
|
awsRegion: selectedProvider.awsRegion,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
|
|
||||||
if (data.valid) {
|
if (data.valid) {
|
||||||
@@ -882,63 +876,6 @@ export function ModelConfigDialog({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : selectedProvider.provider ===
|
|
||||||
"edgeone" ? (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button
|
|
||||||
variant={
|
|
||||||
validationStatus ===
|
|
||||||
"success"
|
|
||||||
? "outline"
|
|
||||||
: "default"
|
|
||||||
}
|
|
||||||
size="sm"
|
|
||||||
onClick={
|
|
||||||
handleValidate
|
|
||||||
}
|
|
||||||
disabled={
|
|
||||||
validationStatus ===
|
|
||||||
"validating"
|
|
||||||
}
|
|
||||||
className={cn(
|
|
||||||
"h-9 px-4",
|
|
||||||
validationStatus ===
|
|
||||||
"success" &&
|
|
||||||
"text-success border-success/30 bg-success-muted hover:bg-success-muted",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{validationStatus ===
|
|
||||||
"validating" ? (
|
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
|
||||||
) : validationStatus ===
|
|
||||||
"success" ? (
|
|
||||||
<>
|
|
||||||
<Check className="h-4 w-4 mr-1.5" />
|
|
||||||
{
|
|
||||||
dict
|
|
||||||
.modelConfig
|
|
||||||
.verified
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
dict
|
|
||||||
.modelConfig
|
|
||||||
.test
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
{validationStatus ===
|
|
||||||
"error" &&
|
|
||||||
validationError && (
|
|
||||||
<p className="text-xs text-destructive flex items-center gap-1">
|
|
||||||
<X className="h-3 w-3" />
|
|
||||||
{
|
|
||||||
validationError
|
|
||||||
}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* API Key */}
|
{/* API Key */}
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ const PROVIDER_LOGO_MAP: Record<string, string> = {
|
|||||||
siliconflow: "siliconflow",
|
siliconflow: "siliconflow",
|
||||||
sglang: "openai", // SGLang is OpenAI-compatible, use OpenAI logo
|
sglang: "openai", // SGLang is OpenAI-compatible, use OpenAI logo
|
||||||
gateway: "vercel",
|
gateway: "vercel",
|
||||||
edgeone: "tencent-cloud",
|
|
||||||
doubao: "bytedance",
|
doubao: "bytedance",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -395,13 +395,13 @@ function SettingsContent({
|
|||||||
<>
|
<>
|
||||||
<span className="text-muted-foreground">·</span>
|
<span className="text-muted-foreground">·</span>
|
||||||
<a
|
<a
|
||||||
href={`/${currentLang}/about${currentLang === "zh" ? "/cn" : currentLang === "ja" ? "/ja" : ""}`}
|
href="/about"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-xs text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1"
|
className="text-xs text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1"
|
||||||
>
|
>
|
||||||
<Info className="h-3 w-3" />
|
<Info className="h-3 w-3" />
|
||||||
{dict.nav.about}
|
About
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -206,19 +206,6 @@ npm run dev
|
|||||||
|
|
||||||
## 部署
|
## 部署
|
||||||
|
|
||||||
### 部署到腾讯云EdgeOne Pages
|
|
||||||
|
|
||||||
您可以通过[腾讯云EdgeOne Pages](https://pages.edgeone.ai/zh)一键部署。
|
|
||||||
|
|
||||||
直接点击此按钮一键部署:
|
|
||||||
[](https://console.cloud.tencent.com/edgeone/pages/new?repository-url=https%3A%2F%2Fgithub.com%2FDayuanJiang%2Fnext-ai-draw-io)
|
|
||||||
|
|
||||||
查看[腾讯云EdgeOne Pages文档](https://pages.edgeone.ai/zh/document/product-introduction)了解更多详情。
|
|
||||||
|
|
||||||
同时,通过腾讯云EdgeOne Pages部署,也会获得[每日免费的DeepSeek模型额度](https://edgeone.cloud.tencent.com/pages/document/169925463311781888)。
|
|
||||||
|
|
||||||
### 部署到Vercel
|
|
||||||
|
|
||||||
部署Next.js应用最简单的方式是使用Next.js创建者提供的[Vercel平台](https://vercel.com/new)。
|
部署Next.js应用最简单的方式是使用Next.js创建者提供的[Vercel平台](https://vercel.com/new)。
|
||||||
|
|
||||||
查看[Next.js部署文档](https://nextjs.org/docs/app/building-your-application/deploying)了解更多详情。
|
查看[Next.js部署文档](https://nextjs.org/docs/app/building-your-application/deploying)了解更多详情。
|
||||||
|
|||||||
@@ -206,20 +206,6 @@ npm run dev
|
|||||||
|
|
||||||
## デプロイ
|
## デプロイ
|
||||||
|
|
||||||
### EdgeOne Pagesへのデプロイ
|
|
||||||
|
|
||||||
[Tencent EdgeOne Pages](https://pages.edgeone.ai/)を使用してワンクリックでデプロイできます。
|
|
||||||
|
|
||||||
このボタンでデプロイ:
|
|
||||||
|
|
||||||
[](https://edgeone.ai/pages/new?repository-url=https%3A%2F%2Fgithub.com%2FDayuanJiang%2Fnext-ai-draw-io)
|
|
||||||
|
|
||||||
詳細は[Tencent EdgeOne Pagesドキュメント](https://pages.edgeone.ai/document/deployment-overview)をご覧ください。
|
|
||||||
|
|
||||||
また、Tencent EdgeOne Pagesでデプロイすると、[DeepSeekモデルの毎日の無料クォータ](https://pages.edgeone.ai/document/edge-ai)が付与されます。
|
|
||||||
|
|
||||||
### Vercelへのデプロイ
|
|
||||||
|
|
||||||
Next.jsアプリをデプロイする最も簡単な方法は、Next.jsの作成者による[Vercelプラットフォーム](https://vercel.com/new)を使用することです。
|
Next.jsアプリをデプロイする最も簡単な方法は、Next.jsの作成者による[Vercelプラットフォーム](https://vercel.com/new)を使用することです。
|
||||||
|
|
||||||
詳細は[Next.jsデプロイメントドキュメント](https://nextjs.org/docs/app/building-your-application/deploying)をご覧ください。
|
詳細は[Next.jsデプロイメントドキュメント](https://nextjs.org/docs/app/building-your-application/deploying)をご覧ください。
|
||||||
|
|||||||
@@ -1,270 +0,0 @@
|
|||||||
/**
|
|
||||||
* EdgeOne Pages Edge Function for OpenAI-compatible Chat Completions API
|
|
||||||
*
|
|
||||||
* This endpoint provides an OpenAI-compatible API that can be used with
|
|
||||||
* AI SDK's createOpenAI({ baseURL: '/api/edgeai' })
|
|
||||||
*
|
|
||||||
* Uses EdgeOne Edge AI's AI.chatCompletions() which now supports native tool calling.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { z } from "zod"
|
|
||||||
|
|
||||||
// EdgeOne Pages global AI object
|
|
||||||
declare const AI: {
|
|
||||||
chatCompletions(options: {
|
|
||||||
model: string
|
|
||||||
messages: Array<{ role: string; content: string | null }>
|
|
||||||
stream?: boolean
|
|
||||||
max_tokens?: number
|
|
||||||
temperature?: number
|
|
||||||
tools?: any
|
|
||||||
tool_choice?: any
|
|
||||||
}): Promise<ReadableStream<Uint8Array> | any>
|
|
||||||
}
|
|
||||||
|
|
||||||
const messageItemSchema = z
|
|
||||||
.object({
|
|
||||||
role: z.enum(["user", "assistant", "system", "tool", "function"]),
|
|
||||||
content: z.string().nullable().optional(),
|
|
||||||
})
|
|
||||||
.passthrough()
|
|
||||||
|
|
||||||
const messageSchema = z
|
|
||||||
.object({
|
|
||||||
messages: z.array(messageItemSchema),
|
|
||||||
model: z.string().optional(),
|
|
||||||
stream: z.boolean().optional(),
|
|
||||||
tools: z.any().optional(),
|
|
||||||
tool_choice: z.any().optional(),
|
|
||||||
functions: z.any().optional(),
|
|
||||||
function_call: z.any().optional(),
|
|
||||||
temperature: z.number().optional(),
|
|
||||||
top_p: z.number().optional(),
|
|
||||||
max_tokens: z.number().optional(),
|
|
||||||
presence_penalty: z.number().optional(),
|
|
||||||
frequency_penalty: z.number().optional(),
|
|
||||||
stop: z.union([z.string(), z.array(z.string())]).optional(),
|
|
||||||
response_format: z.any().optional(),
|
|
||||||
seed: z.number().optional(),
|
|
||||||
user: z.string().optional(),
|
|
||||||
n: z.number().int().optional(),
|
|
||||||
logit_bias: z.record(z.string(), z.number()).optional(),
|
|
||||||
parallel_tool_calls: z.boolean().optional(),
|
|
||||||
stream_options: z.any().optional(),
|
|
||||||
})
|
|
||||||
.passthrough()
|
|
||||||
|
|
||||||
// Model configuration
|
|
||||||
const ALLOWED_MODELS = [
|
|
||||||
"@tx/deepseek-ai/deepseek-v32",
|
|
||||||
"@tx/deepseek-ai/deepseek-r1-0528",
|
|
||||||
"@tx/deepseek-ai/deepseek-v3-0324",
|
|
||||||
]
|
|
||||||
|
|
||||||
const MODEL_ALIASES: Record<string, string> = {
|
|
||||||
"deepseek-v3.2": "@tx/deepseek-ai/deepseek-v32",
|
|
||||||
"deepseek-r1-0528": "@tx/deepseek-ai/deepseek-r1-0528",
|
|
||||||
"deepseek-v3-0324": "@tx/deepseek-ai/deepseek-v3-0324",
|
|
||||||
}
|
|
||||||
|
|
||||||
const CORS_HEADERS = {
|
|
||||||
"Access-Control-Allow-Origin": "*",
|
|
||||||
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
||||||
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create standardized response with CORS headers
|
|
||||||
*/
|
|
||||||
function createResponse(body: any, status = 200, extraHeaders = {}): Response {
|
|
||||||
return new Response(JSON.stringify(body), {
|
|
||||||
status,
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
...CORS_HEADERS,
|
|
||||||
...extraHeaders,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle OPTIONS request for CORS preflight
|
|
||||||
*/
|
|
||||||
function handleOptionsRequest(): Response {
|
|
||||||
return new Response(null, {
|
|
||||||
headers: {
|
|
||||||
...CORS_HEADERS,
|
|
||||||
"Access-Control-Max-Age": "86400",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function onRequest({ request, env }: any) {
|
|
||||||
if (request.method === "OPTIONS") {
|
|
||||||
return handleOptionsRequest()
|
|
||||||
}
|
|
||||||
|
|
||||||
request.headers.delete("accept-encoding")
|
|
||||||
|
|
||||||
try {
|
|
||||||
const json = await request.clone().json()
|
|
||||||
const parseResult = messageSchema.safeParse(json)
|
|
||||||
|
|
||||||
if (!parseResult.success) {
|
|
||||||
return createResponse(
|
|
||||||
{
|
|
||||||
error: {
|
|
||||||
message: parseResult.error.message,
|
|
||||||
type: "invalid_request_error",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
400,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const { messages, model, stream, tools, tool_choice, ...extraParams } =
|
|
||||||
parseResult.data
|
|
||||||
|
|
||||||
// Validate messages
|
|
||||||
const userMessages = messages.filter(
|
|
||||||
(message) => message.role === "user",
|
|
||||||
)
|
|
||||||
if (!userMessages.length) {
|
|
||||||
return createResponse(
|
|
||||||
{
|
|
||||||
error: {
|
|
||||||
message: "No user message found",
|
|
||||||
type: "invalid_request_error",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
400,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve model
|
|
||||||
const requestedModel = model || ALLOWED_MODELS[0]
|
|
||||||
const selectedModel = MODEL_ALIASES[requestedModel] || requestedModel
|
|
||||||
|
|
||||||
if (!ALLOWED_MODELS.includes(selectedModel)) {
|
|
||||||
return createResponse(
|
|
||||||
{
|
|
||||||
error: {
|
|
||||||
message: `Invalid model: ${requestedModel}.`,
|
|
||||||
type: "invalid_request_error",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
429,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`[EdgeOne] Model: ${selectedModel}, Tools: ${tools?.length || 0}, Stream: ${stream ?? true}`,
|
|
||||||
)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const isStream = !!stream
|
|
||||||
|
|
||||||
// Non-streaming: return mock response for validation
|
|
||||||
// AI.chatCompletions doesn't support non-streaming mode
|
|
||||||
if (!isStream) {
|
|
||||||
const mockResponse = {
|
|
||||||
id: `chatcmpl-${Date.now()}`,
|
|
||||||
object: "chat.completion",
|
|
||||||
created: Math.floor(Date.now() / 1000),
|
|
||||||
model: selectedModel,
|
|
||||||
choices: [
|
|
||||||
{
|
|
||||||
index: 0,
|
|
||||||
message: {
|
|
||||||
role: "assistant",
|
|
||||||
content: "OK",
|
|
||||||
},
|
|
||||||
finish_reason: "stop",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
usage: {
|
|
||||||
prompt_tokens: 10,
|
|
||||||
completion_tokens: 1,
|
|
||||||
total_tokens: 11,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return createResponse(mockResponse)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build AI.chatCompletions options for streaming
|
|
||||||
const aiOptions: any = {
|
|
||||||
...extraParams,
|
|
||||||
model: selectedModel,
|
|
||||||
messages,
|
|
||||||
stream: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add tools if provided
|
|
||||||
if (tools && tools.length > 0) {
|
|
||||||
aiOptions.tools = tools
|
|
||||||
}
|
|
||||||
if (tool_choice !== undefined) {
|
|
||||||
aiOptions.tool_choice = tool_choice
|
|
||||||
}
|
|
||||||
|
|
||||||
const aiResponse = await AI.chatCompletions(aiOptions)
|
|
||||||
|
|
||||||
// Streaming response
|
|
||||||
return new Response(aiResponse, {
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "text/event-stream; charset=utf-8",
|
|
||||||
"Cache-Control": "no-cache, no-store, no-transform",
|
|
||||||
"X-Accel-Buffering": "no",
|
|
||||||
Connection: "keep-alive",
|
|
||||||
...CORS_HEADERS,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error: any) {
|
|
||||||
// Handle EdgeOne specific errors
|
|
||||||
try {
|
|
||||||
const message = JSON.parse(error.message)
|
|
||||||
if (message.code === 14020) {
|
|
||||||
return createResponse(
|
|
||||||
{
|
|
||||||
error: {
|
|
||||||
message:
|
|
||||||
"The daily public quota has been exhausted. After deployment, you can enjoy a personal daily exclusive quota.",
|
|
||||||
type: "rate_limit_error",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
429,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return createResponse(
|
|
||||||
{ error: { message: error.message, type: "api_error" } },
|
|
||||||
500,
|
|
||||||
)
|
|
||||||
} catch {
|
|
||||||
// Not a JSON error message
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error("[EdgeOne] AI error:", error.message)
|
|
||||||
return createResponse(
|
|
||||||
{
|
|
||||||
error: {
|
|
||||||
message: error.message || "AI service error",
|
|
||||||
type: "api_error",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
500,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error("[EdgeOne] Request error:", error.message)
|
|
||||||
return createResponse(
|
|
||||||
{
|
|
||||||
error: {
|
|
||||||
message: "Request processing failed",
|
|
||||||
type: "server_error",
|
|
||||||
details: error.message,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
500,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"nodeFunctionsConfig": {
|
|
||||||
"maxDuration": 120
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -21,7 +21,6 @@ export type ProviderName =
|
|||||||
| "siliconflow"
|
| "siliconflow"
|
||||||
| "sglang"
|
| "sglang"
|
||||||
| "gateway"
|
| "gateway"
|
||||||
| "edgeone"
|
|
||||||
| "doubao"
|
| "doubao"
|
||||||
|
|
||||||
interface ModelConfig {
|
interface ModelConfig {
|
||||||
@@ -41,8 +40,6 @@ export interface ClientOverrides {
|
|||||||
awsSecretAccessKey?: string | null
|
awsSecretAccessKey?: string | null
|
||||||
awsRegion?: string | null
|
awsRegion?: string | null
|
||||||
awsSessionToken?: string | null
|
awsSessionToken?: string | null
|
||||||
// Custom headers (e.g., for EdgeOne cookie auth)
|
|
||||||
headers?: Record<string, string>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Providers that can be used with client-provided API keys
|
// Providers that can be used with client-provided API keys
|
||||||
@@ -57,7 +54,6 @@ const ALLOWED_CLIENT_PROVIDERS: ProviderName[] = [
|
|||||||
"siliconflow",
|
"siliconflow",
|
||||||
"sglang",
|
"sglang",
|
||||||
"gateway",
|
"gateway",
|
||||||
"edgeone",
|
|
||||||
"doubao",
|
"doubao",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -379,7 +375,6 @@ const PROVIDER_ENV_VARS: Record<ProviderName, string | null> = {
|
|||||||
siliconflow: "SILICONFLOW_API_KEY",
|
siliconflow: "SILICONFLOW_API_KEY",
|
||||||
sglang: "SGLANG_API_KEY",
|
sglang: "SGLANG_API_KEY",
|
||||||
gateway: "AI_GATEWAY_API_KEY",
|
gateway: "AI_GATEWAY_API_KEY",
|
||||||
edgeone: null, // No credentials needed - uses EdgeOne Edge AI
|
|
||||||
doubao: "DOUBAO_API_KEY",
|
doubao: "DOUBAO_API_KEY",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -468,12 +463,7 @@ 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)
|
if (overrides?.baseUrl && !overrides?.apiKey) {
|
||||||
if (
|
|
||||||
overrides?.baseUrl &&
|
|
||||||
!overrides?.apiKey &&
|
|
||||||
overrides?.provider !== "edgeone"
|
|
||||||
) {
|
|
||||||
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. ` +
|
||||||
`Please provide your own API key in Settings.`,
|
`Please provide your own API key in Settings.`,
|
||||||
@@ -850,21 +840,6 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
case "edgeone": {
|
|
||||||
// EdgeOne Pages Edge AI - uses OpenAI-compatible API
|
|
||||||
// AI SDK appends /chat/completions to baseURL
|
|
||||||
// /api/edgeai + /chat/completions = /api/edgeai/chat/completions
|
|
||||||
const baseURL = overrides?.baseUrl || "/api/edgeai"
|
|
||||||
const edgeoneProvider = createOpenAI({
|
|
||||||
apiKey: "edgeone", // Dummy key - EdgeOne doesn't require API key
|
|
||||||
baseURL,
|
|
||||||
// Pass cookies for EdgeOne Pages authentication (eo_token, eo_time)
|
|
||||||
...(overrides?.headers && { headers: overrides.headers }),
|
|
||||||
})
|
|
||||||
model = edgeoneProvider.chat(modelId)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case "doubao": {
|
case "doubao": {
|
||||||
const apiKey = overrides?.apiKey || process.env.DOUBAO_API_KEY
|
const apiKey = overrides?.apiKey || process.env.DOUBAO_API_KEY
|
||||||
const baseURL =
|
const baseURL =
|
||||||
@@ -881,7 +856,7 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
|
|||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unknown AI provider: ${provider}. Supported providers: bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow, sglang, gateway, edgeone, doubao`,
|
`Unknown AI provider: ${provider}. Supported providers: bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow, sglang, gateway, doubao`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,6 @@
|
|||||||
"copyResponse": "Copy response",
|
"copyResponse": "Copy response",
|
||||||
"copied": "Copied!",
|
"copied": "Copied!",
|
||||||
"failedToCopy": "Failed to copy",
|
"failedToCopy": "Failed to copy",
|
||||||
"failedToCopyDetail": "Failed to copy message. Please copy manually or check clipboard permissions.",
|
|
||||||
"goodResponse": "Good response",
|
"goodResponse": "Good response",
|
||||||
"badResponse": "Bad response",
|
"badResponse": "Bad response",
|
||||||
"clickToEdit": "Click to edit",
|
"clickToEdit": "Click to edit",
|
||||||
@@ -142,7 +141,6 @@
|
|||||||
"invalidAccessCode": "Invalid or missing access code. Please configure it in Settings.",
|
"invalidAccessCode": "Invalid or missing access code. Please configure it in Settings.",
|
||||||
"networkError": "Network error. Please check your connection.",
|
"networkError": "Network error. Please check your connection.",
|
||||||
"retryLimit": "Auto-retry limit reached ({max}). Please try again manually.",
|
"retryLimit": "Auto-retry limit reached ({max}). Please try again manually.",
|
||||||
"continuationRetryLimit": "Continuation retry limit reached ({max}). The diagram may be too complex.",
|
|
||||||
"validationFailed": "Diagram validation failed. Please try regenerating.",
|
"validationFailed": "Diagram validation failed. Please try regenerating.",
|
||||||
"malformedXml": "AI generated invalid diagram XML. Please try regenerating.",
|
"malformedXml": "AI generated invalid diagram XML. Please try regenerating.",
|
||||||
"failedToProcess": "Failed to process diagram. Please try regenerating.",
|
"failedToProcess": "Failed to process diagram. Please try regenerating.",
|
||||||
@@ -151,9 +149,7 @@
|
|||||||
"failedToRestore": "Failed to restore from localStorage",
|
"failedToRestore": "Failed to restore from localStorage",
|
||||||
"failedToPersist": "Failed to persist state before unload",
|
"failedToPersist": "Failed to persist state before unload",
|
||||||
"failedToExport": "Error fetching chart data",
|
"failedToExport": "Error fetching chart data",
|
||||||
"failedToLoadExample": "Error loading example image",
|
"failedToLoadExample": "Error loading example image"
|
||||||
"failedToRecordFeedback": "Failed to record your feedback. Please try again.",
|
|
||||||
"storageUpdateFailed": "Chat cleared but browser storage could not be updated"
|
|
||||||
},
|
},
|
||||||
"quota": {
|
"quota": {
|
||||||
"dailyLimit": "Daily Quota Reached",
|
"dailyLimit": "Daily Quota Reached",
|
||||||
@@ -190,21 +186,6 @@
|
|||||||
"thoughtFor": "Thought for {duration} seconds",
|
"thoughtFor": "Thought for {duration} seconds",
|
||||||
"thoughtBrief": "Thought for a few seconds"
|
"thoughtBrief": "Thought for a few seconds"
|
||||||
},
|
},
|
||||||
"dev": {
|
|
||||||
"title": "Dev: XML Streaming Simulator",
|
|
||||||
"preset": "Preset:",
|
|
||||||
"selectPreset": "Select a preset...",
|
|
||||||
"clear": "Clear",
|
|
||||||
"placeholder": "Paste mxCell XML here or select a preset...",
|
|
||||||
"interval": "Interval:",
|
|
||||||
"chars": "Chars:",
|
|
||||||
"streaming": "Streaming...",
|
|
||||||
"simulate": "Simulate",
|
|
||||||
"stop": "Stop",
|
|
||||||
"testQuotaToast": "Test Quota Toast",
|
|
||||||
"simulatingMessage": "[Dev] Simulating XML streaming",
|
|
||||||
"successMessage": "Successfully displayed the diagram."
|
|
||||||
},
|
|
||||||
"about": {
|
"about": {
|
||||||
"modelChange": "Model Change & Usage Limits",
|
"modelChange": "Model Change & Usage Limits",
|
||||||
"walletCrying": "(Or: Why My Wallet is Crying)",
|
"walletCrying": "(Or: Why My Wallet is Crying)",
|
||||||
|
|||||||
@@ -46,7 +46,6 @@
|
|||||||
"copyResponse": "応答をコピー",
|
"copyResponse": "応答をコピー",
|
||||||
"copied": "コピーしました!",
|
"copied": "コピーしました!",
|
||||||
"failedToCopy": "コピーに失敗しました",
|
"failedToCopy": "コピーに失敗しました",
|
||||||
"failedToCopyDetail": "メッセージのコピーに失敗しました。手動でコピーするか、クリップボードの権限を確認してください。",
|
|
||||||
"goodResponse": "良い応答",
|
"goodResponse": "良い応答",
|
||||||
"badResponse": "悪い応答",
|
"badResponse": "悪い応答",
|
||||||
"clickToEdit": "クリックして編集",
|
"clickToEdit": "クリックして編集",
|
||||||
@@ -142,7 +141,6 @@
|
|||||||
"invalidAccessCode": "無効または欠落したアクセスコード。設定で入力してください。",
|
"invalidAccessCode": "無効または欠落したアクセスコード。設定で入力してください。",
|
||||||
"networkError": "ネットワークエラー。接続を確認してください。",
|
"networkError": "ネットワークエラー。接続を確認してください。",
|
||||||
"retryLimit": "自動再試行制限に達しました({max})。手動で再試行してください。",
|
"retryLimit": "自動再試行制限に達しました({max})。手動で再試行してください。",
|
||||||
"continuationRetryLimit": "継続再試行制限に達しました({max})。ダイアグラムが複雑すぎる可能性があります。",
|
|
||||||
"validationFailed": "ダイアグラムの検証に失敗しました。再生成してみてください。",
|
"validationFailed": "ダイアグラムの検証に失敗しました。再生成してみてください。",
|
||||||
"malformedXml": "AI が無効なダイアグラム XML を生成しました。再生成してみてください。",
|
"malformedXml": "AI が無効なダイアグラム XML を生成しました。再生成してみてください。",
|
||||||
"failedToProcess": "ダイアグラムの処理に失敗しました。再生成してみてください。",
|
"failedToProcess": "ダイアグラムの処理に失敗しました。再生成してみてください。",
|
||||||
@@ -151,9 +149,7 @@
|
|||||||
"failedToRestore": "localStorage からの復元に失敗しました",
|
"failedToRestore": "localStorage からの復元に失敗しました",
|
||||||
"failedToPersist": "アンロード前の状態の永続化に失敗しました",
|
"failedToPersist": "アンロード前の状態の永続化に失敗しました",
|
||||||
"failedToExport": "チャートデータの取得エラー",
|
"failedToExport": "チャートデータの取得エラー",
|
||||||
"failedToLoadExample": "例の画像の読み込みエラー",
|
"failedToLoadExample": "例の画像の読み込みエラー"
|
||||||
"failedToRecordFeedback": "フィードバックの記録に失敗しました。もう一度お試しください。",
|
|
||||||
"storageUpdateFailed": "チャットはクリアされましたが、ブラウザストレージを更新できませんでした"
|
|
||||||
},
|
},
|
||||||
"quota": {
|
"quota": {
|
||||||
"dailyLimit": "1日の割当量に達しました",
|
"dailyLimit": "1日の割当量に達しました",
|
||||||
@@ -190,21 +186,6 @@
|
|||||||
"thoughtFor": "{duration} 秒考えました",
|
"thoughtFor": "{duration} 秒考えました",
|
||||||
"thoughtBrief": "数秒考えました"
|
"thoughtBrief": "数秒考えました"
|
||||||
},
|
},
|
||||||
"dev": {
|
|
||||||
"title": "開発:XMLストリーミングシミュレーター",
|
|
||||||
"preset": "プリセット:",
|
|
||||||
"selectPreset": "プリセットを選択...",
|
|
||||||
"clear": "クリア",
|
|
||||||
"placeholder": "ここに mxCell XML を貼り付けるか、プリセットを選択...",
|
|
||||||
"interval": "間隔:",
|
|
||||||
"chars": "文字:",
|
|
||||||
"streaming": "ストリーミング中...",
|
|
||||||
"simulate": "シミュレート",
|
|
||||||
"stop": "停止",
|
|
||||||
"testQuotaToast": "クォータトーストをテスト",
|
|
||||||
"simulatingMessage": "[開発] XMLストリーミングをシミュレート中",
|
|
||||||
"successMessage": "ダイアグラムの表示に成功しました。"
|
|
||||||
},
|
|
||||||
"about": {
|
"about": {
|
||||||
"modelChange": "モデル変更と利用制限について",
|
"modelChange": "モデル変更と利用制限について",
|
||||||
"walletCrying": "(別名:お財布が悲鳴を上げています)",
|
"walletCrying": "(別名:お財布が悲鳴を上げています)",
|
||||||
|
|||||||
@@ -46,7 +46,6 @@
|
|||||||
"copyResponse": "复制响应",
|
"copyResponse": "复制响应",
|
||||||
"copied": "已复制!",
|
"copied": "已复制!",
|
||||||
"failedToCopy": "复制失败",
|
"failedToCopy": "复制失败",
|
||||||
"failedToCopyDetail": "复制消息失败。请手动复制或检查剪贴板权限。",
|
|
||||||
"goodResponse": "有帮助",
|
"goodResponse": "有帮助",
|
||||||
"badResponse": "无帮助",
|
"badResponse": "无帮助",
|
||||||
"clickToEdit": "点击编辑",
|
"clickToEdit": "点击编辑",
|
||||||
@@ -142,7 +141,6 @@
|
|||||||
"invalidAccessCode": "无效或缺少访问码。请在设置中配置。",
|
"invalidAccessCode": "无效或缺少访问码。请在设置中配置。",
|
||||||
"networkError": "网络错误。请检查您的连接。",
|
"networkError": "网络错误。请检查您的连接。",
|
||||||
"retryLimit": "已达到自动重试限制({max})。请手动重试。",
|
"retryLimit": "已达到自动重试限制({max})。请手动重试。",
|
||||||
"continuationRetryLimit": "已达到继续重试限制({max})。图表可能过于复杂。",
|
|
||||||
"validationFailed": "图表验证失败。请尝试重新生成。",
|
"validationFailed": "图表验证失败。请尝试重新生成。",
|
||||||
"malformedXml": "AI 生成的图表 XML 无效。请尝试重新生成。",
|
"malformedXml": "AI 生成的图表 XML 无效。请尝试重新生成。",
|
||||||
"failedToProcess": "无法处理图表。请尝试重新生成。",
|
"failedToProcess": "无法处理图表。请尝试重新生成。",
|
||||||
@@ -151,9 +149,7 @@
|
|||||||
"failedToRestore": "无法从 localStorage 恢复",
|
"failedToRestore": "无法从 localStorage 恢复",
|
||||||
"failedToPersist": "卸载前无法持久化状态",
|
"failedToPersist": "卸载前无法持久化状态",
|
||||||
"failedToExport": "获取图表数据时出错",
|
"failedToExport": "获取图表数据时出错",
|
||||||
"failedToLoadExample": "加载示例图片时出错",
|
"failedToLoadExample": "加载示例图片时出错"
|
||||||
"failedToRecordFeedback": "记录您的反馈失败。请重试。",
|
|
||||||
"storageUpdateFailed": "聊天已清除,但无法更新浏览器存储"
|
|
||||||
},
|
},
|
||||||
"quota": {
|
"quota": {
|
||||||
"dailyLimit": "已达每日配额",
|
"dailyLimit": "已达每日配额",
|
||||||
@@ -190,21 +186,6 @@
|
|||||||
"thoughtFor": "思考了 {duration} 秒",
|
"thoughtFor": "思考了 {duration} 秒",
|
||||||
"thoughtBrief": "思考了几秒钟"
|
"thoughtBrief": "思考了几秒钟"
|
||||||
},
|
},
|
||||||
"dev": {
|
|
||||||
"title": "开发:XML 流式模拟器",
|
|
||||||
"preset": "预设:",
|
|
||||||
"selectPreset": "选择预设...",
|
|
||||||
"clear": "清除",
|
|
||||||
"placeholder": "在此粘贴 mxCell XML 或选择预设...",
|
|
||||||
"interval": "间隔:",
|
|
||||||
"chars": "字符:",
|
|
||||||
"streaming": "流式传输中...",
|
|
||||||
"simulate": "模拟",
|
|
||||||
"stop": "停止",
|
|
||||||
"testQuotaToast": "测试配额提示",
|
|
||||||
"simulatingMessage": "[开发] 模拟 XML 流式传输",
|
|
||||||
"successMessage": "成功显示图表。"
|
|
||||||
},
|
|
||||||
"about": {
|
"about": {
|
||||||
"modelChange": "模型变更与用量限制",
|
"modelChange": "模型变更与用量限制",
|
||||||
"walletCrying": "(别名:我的钱包顶不住了)",
|
"walletCrying": "(别名:我的钱包顶不住了)",
|
||||||
|
|||||||
@@ -276,7 +276,7 @@ edit_diagram uses ID-based operations to modify cells directly by their id attri
|
|||||||
**Operations:**
|
**Operations:**
|
||||||
- **update**: Replace an existing cell. Provide cell_id and new_xml.
|
- **update**: Replace an existing cell. Provide cell_id and new_xml.
|
||||||
- **add**: Add a new cell. Provide cell_id (new unique id) and new_xml.
|
- **add**: Add a new cell. Provide cell_id (new unique id) and new_xml.
|
||||||
- **delete**: Remove a cell. **Cascade is automatic**: children AND edges (source/target) are auto-deleted. Only specify ONE cell_id.
|
- **delete**: Remove a cell. Only cell_id is needed.
|
||||||
|
|
||||||
**Input Format:**
|
**Input Format:**
|
||||||
\`\`\`json
|
\`\`\`json
|
||||||
@@ -301,9 +301,9 @@ Add new shape:
|
|||||||
{"operations": [{"operation": "add", "cell_id": "new1", "new_xml": "<mxCell id=\\"new1\\" value=\\"New Box\\" style=\\"rounded=1;fillColor=#dae8fc;\\" vertex=\\"1\\" parent=\\"1\\">\\n <mxGeometry x=\\"400\\" y=\\"200\\" width=\\"120\\" height=\\"60\\" as=\\"geometry\\"/>\\n</mxCell>"}]}
|
{"operations": [{"operation": "add", "cell_id": "new1", "new_xml": "<mxCell id=\\"new1\\" value=\\"New Box\\" style=\\"rounded=1;fillColor=#dae8fc;\\" vertex=\\"1\\" parent=\\"1\\">\\n <mxGeometry x=\\"400\\" y=\\"200\\" width=\\"120\\" height=\\"60\\" as=\\"geometry\\"/>\\n</mxCell>"}]}
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
Delete container (children & edges auto-deleted):
|
Delete cell:
|
||||||
\`\`\`json
|
\`\`\`json
|
||||||
{"operations": [{"operation": "delete", "cell_id": "2"}]}
|
{"operations": [{"operation": "delete", "cell_id": "5"}]}
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
**Error Recovery:**
|
**Error Recovery:**
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ export type ProviderName =
|
|||||||
| "siliconflow"
|
| "siliconflow"
|
||||||
| "sglang"
|
| "sglang"
|
||||||
| "gateway"
|
| "gateway"
|
||||||
| "edgeone"
|
|
||||||
| "doubao"
|
| "doubao"
|
||||||
|
|
||||||
// Individual model configuration
|
// Individual model configuration
|
||||||
@@ -86,7 +85,6 @@ export const PROVIDER_INFO: Record<
|
|||||||
defaultBaseUrl: "http://127.0.0.1:8000/v1",
|
defaultBaseUrl: "http://127.0.0.1:8000/v1",
|
||||||
},
|
},
|
||||||
gateway: { label: "AI Gateway" },
|
gateway: { label: "AI Gateway" },
|
||||||
edgeone: { label: "EdgeOne Pages" },
|
|
||||||
doubao: {
|
doubao: {
|
||||||
label: "Doubao (ByteDance)",
|
label: "Doubao (ByteDance)",
|
||||||
defaultBaseUrl: "https://ark.cn-beijing.volces.com/api/v3",
|
defaultBaseUrl: "https://ark.cn-beijing.volces.com/api/v3",
|
||||||
@@ -221,7 +219,6 @@ export const SUGGESTED_MODELS: Record<ProviderName, string[]> = {
|
|||||||
"anthropic/claude-3-5-sonnet",
|
"anthropic/claude-3-5-sonnet",
|
||||||
"google/gemini-2.0-flash",
|
"google/gemini-2.0-flash",
|
||||||
],
|
],
|
||||||
edgeone: ["@tx/deepseek-ai/deepseek-v32"],
|
|
||||||
doubao: [
|
doubao: [
|
||||||
// ByteDance Doubao models
|
// ByteDance Doubao models
|
||||||
"doubao-1.5-thinking-pro-250415",
|
"doubao-1.5-thinking-pro-250415",
|
||||||
|
|||||||
77
lib/utils.ts
77
lib/utils.ts
@@ -633,77 +633,32 @@ export function applyDiagramOperations(
|
|||||||
// Add to map
|
// Add to map
|
||||||
cellMap.set(op.cell_id, importedNode)
|
cellMap.set(op.cell_id, importedNode)
|
||||||
} else if (op.operation === "delete") {
|
} else if (op.operation === "delete") {
|
||||||
// Protect root cells from deletion
|
const existingCell = cellMap.get(op.cell_id)
|
||||||
if (op.cell_id === "0" || op.cell_id === "1") {
|
if (!existingCell) {
|
||||||
errors.push({
|
errors.push({
|
||||||
type: "delete",
|
type: "delete",
|
||||||
cellId: op.cell_id,
|
cellId: op.cell_id,
|
||||||
message: `Cannot delete root cell "${op.cell_id}"`,
|
message: `Cell with id="${op.cell_id}" not found`,
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingCell = cellMap.get(op.cell_id)
|
// Check for edges referencing this cell (warning only, still delete)
|
||||||
if (!existingCell) {
|
const referencingEdges = root.querySelectorAll(
|
||||||
// Cell not found - might have been cascade-deleted by a previous operation
|
`mxCell[source="${op.cell_id}"], mxCell[target="${op.cell_id}"]`,
|
||||||
// Skip silently instead of erroring (AI may redundantly list children/edges)
|
)
|
||||||
continue
|
if (referencingEdges.length > 0) {
|
||||||
}
|
const edgeIds = Array.from(referencingEdges)
|
||||||
|
.map((e) => e.getAttribute("id"))
|
||||||
// Cascade delete: collect all cells to delete (children + edges + self)
|
.join(", ")
|
||||||
const cellsToDelete = new Set<string>()
|
console.warn(
|
||||||
|
`[applyDiagramOperations] Deleting cell "${op.cell_id}" which is referenced by edges: ${edgeIds}`,
|
||||||
// Recursive function to find all descendants
|
|
||||||
const collectDescendants = (cellId: string) => {
|
|
||||||
if (cellsToDelete.has(cellId)) return
|
|
||||||
cellsToDelete.add(cellId)
|
|
||||||
|
|
||||||
// Find children (cells where parent === cellId)
|
|
||||||
const children = root.querySelectorAll(
|
|
||||||
`mxCell[parent="${cellId}"]`,
|
|
||||||
)
|
|
||||||
children.forEach((child) => {
|
|
||||||
const childId = child.getAttribute("id")
|
|
||||||
if (childId && childId !== "0" && childId !== "1") {
|
|
||||||
collectDescendants(childId)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect the target cell and all its descendants
|
|
||||||
collectDescendants(op.cell_id)
|
|
||||||
|
|
||||||
// Find edges referencing any of the cells to be deleted
|
|
||||||
// Also recursively collect children of those edges (e.g., edge labels)
|
|
||||||
for (const cellId of cellsToDelete) {
|
|
||||||
const referencingEdges = root.querySelectorAll(
|
|
||||||
`mxCell[source="${cellId}"], mxCell[target="${cellId}"]`,
|
|
||||||
)
|
|
||||||
referencingEdges.forEach((edge) => {
|
|
||||||
const edgeId = edge.getAttribute("id")
|
|
||||||
// Protect root cells from being added via edge references
|
|
||||||
if (edgeId && edgeId !== "0" && edgeId !== "1") {
|
|
||||||
// Recurse to collect edge's children (like labels)
|
|
||||||
collectDescendants(edgeId)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log what will be deleted
|
|
||||||
if (cellsToDelete.size > 1) {
|
|
||||||
console.log(
|
|
||||||
`[applyDiagramOperations] Cascade delete "${op.cell_id}" → deleting ${cellsToDelete.size} cells: ${Array.from(cellsToDelete).join(", ")}`,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete all collected cells
|
// Remove the node
|
||||||
for (const cellId of cellsToDelete) {
|
existingCell.parentNode?.removeChild(existingCell)
|
||||||
const cell = cellMap.get(cellId)
|
cellMap.delete(op.cell_id)
|
||||||
if (cell) {
|
|
||||||
cell.parentNode?.removeChild(cell)
|
|
||||||
cellMap.delete(cellId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -88,11 +88,6 @@
|
|||||||
"unpdf": "^1.4.0",
|
"unpdf": "^1.4.0",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
|
||||||
"lightningcss": "^1.30.2",
|
|
||||||
"lightningcss-linux-x64-gnu": "^1.30.2",
|
|
||||||
"@tailwindcss/oxide-linux-x64-gnu": "^4.1.18"
|
|
||||||
},
|
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{js,ts,jsx,tsx,json,css}": [
|
"*.{js,ts,jsx,tsx,json,css}": [
|
||||||
"biome check --write --no-errors-on-unmatched",
|
"biome check --write --no-errors-on-unmatched",
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ Use the standard MCP configuration with:
|
|||||||
| Tool | Description |
|
| Tool | Description |
|
||||||
|------|-------------|
|
|------|-------------|
|
||||||
| `start_session` | Opens browser with real-time diagram preview |
|
| `start_session` | Opens browser with real-time diagram preview |
|
||||||
| `create_new_diagram` | Create a new diagram from XML (requires `xml` argument) |
|
| `display_diagram` | Create a new diagram from XML |
|
||||||
| `edit_diagram` | Edit diagram by ID-based operations (update/add/delete cells) |
|
| `edit_diagram` | Edit diagram by ID-based operations (update/add/delete cells) |
|
||||||
| `get_diagram` | Get the current diagram XML |
|
| `get_diagram` | Get the current diagram XML |
|
||||||
| `export_diagram` | Save diagram to a `.drawio` file |
|
| `export_diagram` | Save diagram to a `.drawio` file |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@next-ai-drawio/mcp-server",
|
"name": "@next-ai-drawio/mcp-server",
|
||||||
"version": "0.1.10",
|
"version": "0.1.6",
|
||||||
"description": "MCP server for Next AI Draw.io - AI-powered diagram generation with real-time browser preview",
|
"description": "MCP server for Next AI Draw.io - AI-powered diagram generation with real-time browser preview",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
@@ -182,77 +182,32 @@ export function applyDiagramOperations(
|
|||||||
// Add to map
|
// Add to map
|
||||||
cellMap.set(op.cell_id, importedNode)
|
cellMap.set(op.cell_id, importedNode)
|
||||||
} else if (op.operation === "delete") {
|
} else if (op.operation === "delete") {
|
||||||
// Protect root cells from deletion
|
const existingCell = cellMap.get(op.cell_id)
|
||||||
if (op.cell_id === "0" || op.cell_id === "1") {
|
if (!existingCell) {
|
||||||
errors.push({
|
errors.push({
|
||||||
type: "delete",
|
type: "delete",
|
||||||
cellId: op.cell_id,
|
cellId: op.cell_id,
|
||||||
message: `Cannot delete root cell "${op.cell_id}"`,
|
message: `Cell with id="${op.cell_id}" not found`,
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingCell = cellMap.get(op.cell_id)
|
// Check for edges referencing this cell (warning only, still delete)
|
||||||
if (!existingCell) {
|
const referencingEdges = root.querySelectorAll(
|
||||||
// Cell not found - might have been cascade-deleted by a previous operation
|
`mxCell[source="${op.cell_id}"], mxCell[target="${op.cell_id}"]`,
|
||||||
// Skip silently instead of erroring (AI may redundantly list children/edges)
|
)
|
||||||
continue
|
if (referencingEdges.length > 0) {
|
||||||
}
|
const edgeIds = Array.from(referencingEdges)
|
||||||
|
.map((e) => e.getAttribute("id"))
|
||||||
// Cascade delete: collect all cells to delete (children + edges + self)
|
.join(", ")
|
||||||
const cellsToDelete = new Set<string>()
|
console.warn(
|
||||||
|
`[applyDiagramOperations] Deleting cell "${op.cell_id}" which is referenced by edges: ${edgeIds}`,
|
||||||
// Recursive function to find all descendants
|
|
||||||
const collectDescendants = (cellId: string) => {
|
|
||||||
if (cellsToDelete.has(cellId)) return
|
|
||||||
cellsToDelete.add(cellId)
|
|
||||||
|
|
||||||
// Find children (cells where parent === cellId)
|
|
||||||
const children = root.querySelectorAll(
|
|
||||||
`mxCell[parent="${cellId}"]`,
|
|
||||||
)
|
|
||||||
children.forEach((child) => {
|
|
||||||
const childId = child.getAttribute("id")
|
|
||||||
if (childId && childId !== "0" && childId !== "1") {
|
|
||||||
collectDescendants(childId)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect the target cell and all its descendants
|
|
||||||
collectDescendants(op.cell_id)
|
|
||||||
|
|
||||||
// Find edges referencing any of the cells to be deleted
|
|
||||||
// Also recursively collect children of those edges (e.g., edge labels)
|
|
||||||
for (const cellId of cellsToDelete) {
|
|
||||||
const referencingEdges = root.querySelectorAll(
|
|
||||||
`mxCell[source="${cellId}"], mxCell[target="${cellId}"]`,
|
|
||||||
)
|
|
||||||
referencingEdges.forEach((edge) => {
|
|
||||||
const edgeId = edge.getAttribute("id")
|
|
||||||
// Protect root cells from being added via edge references
|
|
||||||
if (edgeId && edgeId !== "0" && edgeId !== "1") {
|
|
||||||
// Recurse to collect edge's children (like labels)
|
|
||||||
collectDescendants(edgeId)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log what will be deleted
|
|
||||||
if (cellsToDelete.size > 1) {
|
|
||||||
console.log(
|
|
||||||
`[applyDiagramOperations] Cascade delete "${op.cell_id}" → deleting ${cellsToDelete.size} cells: ${Array.from(cellsToDelete).join(", ")}`,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete all collected cells
|
// Remove the node
|
||||||
for (const cellId of cellsToDelete) {
|
existingCell.parentNode?.removeChild(existingCell)
|
||||||
const cell = cellMap.get(cellId)
|
cellMap.delete(op.cell_id)
|
||||||
if (cell) {
|
|
||||||
cell.parentNode?.removeChild(cell)
|
|
||||||
cellMap.delete(cellId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ server.prompt(
|
|||||||
|
|
||||||
## Creating a New Diagram
|
## Creating a New Diagram
|
||||||
1. Call start_session to open the browser preview
|
1. Call start_session to open the browser preview
|
||||||
2. Use create_new_diagram with complete mxGraphModel XML to create a new diagram
|
2. Use display_diagram with complete mxGraphModel XML to create a new diagram
|
||||||
|
|
||||||
## Adding Elements to Existing Diagram
|
## Adding Elements to Existing Diagram
|
||||||
1. Use edit_diagram with "add" operation
|
1. Use edit_diagram with "add" operation
|
||||||
@@ -91,7 +91,7 @@ server.prompt(
|
|||||||
3. For update, provide the cell_id and complete new mxCell XML
|
3. For update, provide the cell_id and complete new mxCell XML
|
||||||
|
|
||||||
## Important Notes
|
## Important Notes
|
||||||
- create_new_diagram REPLACES the entire diagram - only use for new diagrams
|
- display_diagram REPLACES the entire diagram - only use for new diagrams
|
||||||
- edit_diagram PRESERVES user's manual changes (fetches browser state first)
|
- edit_diagram PRESERVES user's manual changes (fetches browser state first)
|
||||||
- Always use unique cell_ids when adding elements (e.g., "shape-1", "arrow-2")`,
|
- Always use unique cell_ids when adding elements (e.g., "shape-1", "arrow-2")`,
|
||||||
},
|
},
|
||||||
@@ -150,59 +150,19 @@ server.registerTool(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// Tool: create_new_diagram
|
// Tool: display_diagram
|
||||||
server.registerTool(
|
server.registerTool(
|
||||||
"create_new_diagram",
|
"display_diagram",
|
||||||
{
|
{
|
||||||
description: `Create a NEW diagram from mxGraphModel XML. Use this when creating a diagram from scratch or replacing the current diagram entirely.
|
description:
|
||||||
|
"Display a NEW draw.io diagram from XML. REPLACES the entire diagram. " +
|
||||||
CRITICAL: You MUST provide the 'xml' argument in EVERY call. Do NOT call this tool without xml.
|
"Use this for creating new diagrams from scratch. " +
|
||||||
|
"To ADD elements to an existing diagram, use edit_diagram with 'add' operation instead. " +
|
||||||
When to use this tool:
|
"You should generate valid draw.io/mxGraph XML format.",
|
||||||
- Creating a new diagram from scratch
|
|
||||||
- Replacing the current diagram with a completely different one
|
|
||||||
- Major structural changes that require regenerating the diagram
|
|
||||||
|
|
||||||
When to use edit_diagram instead:
|
|
||||||
- Small modifications to existing diagram
|
|
||||||
- Adding/removing individual elements
|
|
||||||
- Changing labels, colors, or positions
|
|
||||||
|
|
||||||
XML FORMAT - Full mxGraphModel structure:
|
|
||||||
<mxGraphModel>
|
|
||||||
<root>
|
|
||||||
<mxCell id="0"/>
|
|
||||||
<mxCell id="1" parent="0"/>
|
|
||||||
<mxCell id="2" value="Shape" style="rounded=1;" vertex="1" parent="1">
|
|
||||||
<mxGeometry x="100" y="100" width="120" height="60" as="geometry"/>
|
|
||||||
</mxCell>
|
|
||||||
</root>
|
|
||||||
</mxGraphModel>
|
|
||||||
|
|
||||||
LAYOUT CONSTRAINTS:
|
|
||||||
- Keep all elements within x=0-800, y=0-600 (single page viewport)
|
|
||||||
- Start from margins (x=40, y=40), keep elements grouped closely
|
|
||||||
- Use unique IDs starting from "2" (0 and 1 are reserved)
|
|
||||||
- Set parent="1" for top-level shapes
|
|
||||||
- Space shapes 150-200px apart for clear edge routing
|
|
||||||
|
|
||||||
EDGE ROUTING RULES:
|
|
||||||
- Never let multiple edges share the same path - use different exitY/entryY values
|
|
||||||
- For bidirectional connections (A↔B), use OPPOSITE sides
|
|
||||||
- Always specify exitX, exitY, entryX, entryY explicitly in edge style
|
|
||||||
- Route edges AROUND obstacles using waypoints (add 20-30px clearance)
|
|
||||||
- Use natural connection points based on flow (not corners)
|
|
||||||
|
|
||||||
COMMON STYLES:
|
|
||||||
- Shapes: rounded=1; fillColor=#hex; strokeColor=#hex
|
|
||||||
- Edges: endArrow=classic; edgeStyle=orthogonalEdgeStyle; curved=1
|
|
||||||
- Text: fontSize=14; fontStyle=1 (bold); align=center`,
|
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
xml: z
|
xml: z
|
||||||
.string()
|
.string()
|
||||||
.describe(
|
.describe("The draw.io XML to display (mxGraphModel format)"),
|
||||||
"REQUIRED: The complete mxGraphModel XML. Must always be provided.",
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async ({ xml: inputXml }) => {
|
async ({ xml: inputXml }) => {
|
||||||
@@ -239,7 +199,7 @@ COMMON STYLES:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(`Setting diagram content, ${xml.length} chars`)
|
log.info(`Displaying diagram, ${xml.length} chars`)
|
||||||
|
|
||||||
// Sync from browser state first
|
// Sync from browser state first
|
||||||
const browserState = getState(currentSession.id)
|
const browserState = getState(currentSession.id)
|
||||||
@@ -266,20 +226,20 @@ COMMON STYLES:
|
|||||||
// Save AI result (no SVG yet - will be captured by browser)
|
// Save AI result (no SVG yet - will be captured by browser)
|
||||||
addHistory(currentSession.id, xml, "")
|
addHistory(currentSession.id, xml, "")
|
||||||
|
|
||||||
log.info(`Diagram content set successfully`)
|
log.info(`Diagram displayed successfully`)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: `Diagram content set successfully!\n\nThe diagram is now visible in your browser.\n\nXML length: ${xml.length} characters`,
|
text: `Diagram displayed successfully!\n\nThe diagram is now visible in your browser.\n\nXML length: ${xml.length} characters`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message =
|
const message =
|
||||||
error instanceof Error ? error.message : String(error)
|
error instanceof Error ? error.message : String(error)
|
||||||
log.error("create_new_diagram failed:", message)
|
log.error("display_diagram failed:", message)
|
||||||
return {
|
return {
|
||||||
content: [{ type: "text", text: `Error: ${message}` }],
|
content: [{ type: "text", text: `Error: ${message}` }],
|
||||||
isError: true,
|
isError: true,
|
||||||
@@ -380,7 +340,7 @@ server.registerTool(
|
|||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: "Error: No diagram to edit. Please create a diagram first with create_new_diagram.",
|
text: "Error: No diagram to edit. Please create a diagram first with display_diagram.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
isError: true,
|
isError: true,
|
||||||
@@ -514,7 +474,7 @@ server.registerTool(
|
|||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: "No diagram exists yet. Use create_new_diagram to create one.",
|
text: "No diagram exists yet. Use display_diagram to create one.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
63
proxy.ts
63
proxy.ts
@@ -1,63 +0,0 @@
|
|||||||
import { match as matchLocale } from "@formatjs/intl-localematcher"
|
|
||||||
import Negotiator from "negotiator"
|
|
||||||
import type { NextRequest } from "next/server"
|
|
||||||
import { NextResponse } from "next/server"
|
|
||||||
import { i18n } from "./lib/i18n/config"
|
|
||||||
|
|
||||||
function getLocale(request: NextRequest): string | undefined {
|
|
||||||
// Negotiator expects plain object so we need to transform headers
|
|
||||||
const negotiatorHeaders: Record<string, string> = {}
|
|
||||||
request.headers.forEach((value, key) => {
|
|
||||||
negotiatorHeaders[key] = value
|
|
||||||
})
|
|
||||||
|
|
||||||
// @ts-expect-error locales are readonly
|
|
||||||
const locales: string[] = i18n.locales
|
|
||||||
|
|
||||||
// Use negotiator and intl-localematcher to get best locale
|
|
||||||
const languages = new Negotiator({ headers: negotiatorHeaders }).languages(
|
|
||||||
locales,
|
|
||||||
)
|
|
||||||
|
|
||||||
const locale = matchLocale(languages, locales, i18n.defaultLocale)
|
|
||||||
|
|
||||||
return locale
|
|
||||||
}
|
|
||||||
|
|
||||||
export function proxy(request: NextRequest) {
|
|
||||||
const pathname = request.nextUrl.pathname
|
|
||||||
|
|
||||||
// Skip API routes, static files, and Next.js internals
|
|
||||||
if (
|
|
||||||
pathname.startsWith("/api/") ||
|
|
||||||
pathname.startsWith("/_next/") ||
|
|
||||||
pathname.includes("/favicon") ||
|
|
||||||
/\.(.*)$/.test(pathname)
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if there is any supported locale in the pathname
|
|
||||||
const pathnameIsMissingLocale = i18n.locales.every(
|
|
||||||
(locale) =>
|
|
||||||
!pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Redirect if there is no locale
|
|
||||||
if (pathnameIsMissingLocale) {
|
|
||||||
const locale = getLocale(request)
|
|
||||||
|
|
||||||
// Redirect to localized path
|
|
||||||
return NextResponse.redirect(
|
|
||||||
new URL(
|
|
||||||
`/${locale}${pathname.startsWith("/") ? "" : "/"}${pathname}`,
|
|
||||||
request.url,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const config = {
|
|
||||||
// Matcher ignoring `/_next/` and `/api/`
|
|
||||||
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user