mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-02 22:32:27 +08:00
Compare commits
22 Commits
v0.4.1
...
feat/mcp-x
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3c9408dff | ||
|
|
958f9410df | ||
|
|
b8126bd98a | ||
|
|
378bef435e | ||
|
|
f087b54ee4 | ||
|
|
6bb33eeda2 | ||
|
|
a91bd9d1e8 | ||
|
|
81eb71e704 | ||
|
|
58b6b19526 | ||
|
|
f65ef548b2 | ||
|
|
741a00db89 | ||
|
|
bcc6684ecb | ||
|
|
a9415d24e7 | ||
|
|
439bdd4577 | ||
|
|
98b890bb06 | ||
|
|
f039e4a3c8 | ||
|
|
7857858074 | ||
|
|
f0919117eb | ||
|
|
cd76fa615e | ||
|
|
c527ce1520 | ||
|
|
44840d27b3 | ||
|
|
f175276872 |
47
.github/workflows/auto-format.yml
vendored
Normal file
47
.github/workflows/auto-format.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
name: Auto Format
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
format:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.head_ref }}
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Install Biome
|
||||||
|
run: npm install --save-dev @biomejs/biome
|
||||||
|
|
||||||
|
- name: Run Biome format
|
||||||
|
run: npx @biomejs/biome check --write --no-errors-on-unmatched .
|
||||||
|
|
||||||
|
- name: Check for changes
|
||||||
|
id: changes
|
||||||
|
run: |
|
||||||
|
if git diff --quiet; then
|
||||||
|
echo "has_changes=false" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "has_changes=true" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Commit changes
|
||||||
|
if: steps.changes.outputs.has_changes == 'true'
|
||||||
|
run: |
|
||||||
|
git config --global user.name "github-actions[bot]"
|
||||||
|
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git add .
|
||||||
|
git commit -m "style: auto-format with Biome"
|
||||||
|
git push
|
||||||
24
.github/workflows/docker-build.yml
vendored
24
.github/workflows/docker-build.yml
vendored
@@ -64,3 +64,27 @@ jobs:
|
|||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|
||||||
|
# Push to AWS ECR for App Runner auto-deploy
|
||||||
|
- name: Configure AWS credentials
|
||||||
|
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
||||||
|
uses: aws-actions/configure-aws-credentials@v4
|
||||||
|
with:
|
||||||
|
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||||
|
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
|
aws-region: ap-northeast-1
|
||||||
|
|
||||||
|
- name: Login to Amazon ECR
|
||||||
|
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
||||||
|
id: login-ecr
|
||||||
|
uses: aws-actions/amazon-ecr-login@v2
|
||||||
|
|
||||||
|
- name: Push to ECR (triggers App Runner auto-deploy)
|
||||||
|
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
||||||
|
env:
|
||||||
|
REPO_LOWER: ${{ github.repository }}
|
||||||
|
run: |
|
||||||
|
REPO_LOWER=$(echo "$REPO_LOWER" | tr '[:upper:]' '[:lower:]')
|
||||||
|
docker pull ghcr.io/${REPO_LOWER}:latest
|
||||||
|
docker tag ghcr.io/${REPO_LOWER}:latest ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-northeast-1.amazonaws.com/next-ai-draw-io:latest
|
||||||
|
docker push ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-northeast-1.amazonaws.com/next-ai-draw-io:latest
|
||||||
|
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
/node_modules
|
/node_modules
|
||||||
|
packages/*/node_modules
|
||||||
|
packages/*/dist
|
||||||
/.pnp
|
/.pnp
|
||||||
.pnp.*
|
.pnp.*
|
||||||
.yarn/*
|
.yarn/*
|
||||||
@@ -46,3 +48,5 @@ push-via-ec2.sh
|
|||||||
.dev.vars
|
.dev.vars
|
||||||
.open-next/
|
.open-next/
|
||||||
.wrangler/
|
.wrangler/
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,6 @@ EXPOSE 3000
|
|||||||
ENV PORT=3000
|
ENV PORT=3000
|
||||||
ENV HOSTNAME="0.0.0.0"
|
ENV HOSTNAME="0.0.0.0"
|
||||||
|
|
||||||
# Start the application
|
# Start the application (HOSTNAME override needed for AWS App Runner)
|
||||||
CMD ["node", "server.js"]
|
CMD ["sh", "-c", "HOSTNAME=0.0.0.0 exec node server.js"]
|
||||||
|
|
||||||
|
|||||||
31
README.md
31
README.md
@@ -30,6 +30,7 @@ https://github.com/user-attachments/assets/9d60a3e8-4a1c-4b5e-acbb-26af2d3eabd1
|
|||||||
- [Table of Contents](#table-of-contents)
|
- [Table of Contents](#table-of-contents)
|
||||||
- [Examples](#examples)
|
- [Examples](#examples)
|
||||||
- [Features](#features)
|
- [Features](#features)
|
||||||
|
- [MCP Server (Preview)](#mcp-server-preview)
|
||||||
- [Getting Started](#getting-started)
|
- [Getting Started](#getting-started)
|
||||||
- [Try it Online](#try-it-online)
|
- [Try it Online](#try-it-online)
|
||||||
- [Run with Docker (Recommended)](#run-with-docker-recommended)
|
- [Run with Docker (Recommended)](#run-with-docker-recommended)
|
||||||
@@ -92,6 +93,36 @@ Here are some example prompts and their generated diagrams:
|
|||||||
- **Cloud Architecture Diagram Support**: Specialized support for generating cloud architecture diagrams (AWS, GCP, Azure)
|
- **Cloud Architecture Diagram Support**: Specialized support for generating cloud architecture diagrams (AWS, GCP, Azure)
|
||||||
- **Animated Connectors**: Create dynamic and animated connectors between diagram elements for better visualization
|
- **Animated Connectors**: Create dynamic and animated connectors between diagram elements for better visualization
|
||||||
|
|
||||||
|
## MCP Server (Preview)
|
||||||
|
|
||||||
|
> **Preview Feature**: This feature is experimental and may not stable.
|
||||||
|
|
||||||
|
Use Next AI Draw.io with AI agents like Claude Desktop, Cursor, and VS Code via MCP (Model Context Protocol).
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"drawio": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["@next-ai-drawio/mcp-server@latest"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Claude Code CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude mcp add drawio -- npx @next-ai-drawio/mcp-server@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Then ask Claude to create diagrams:
|
||||||
|
> "Create a flowchart showing user authentication with login, MFA, and session management"
|
||||||
|
|
||||||
|
The diagram appears in your browser in real-time!
|
||||||
|
|
||||||
|
See the [MCP Server README](./packages/mcp-server/README.md) for VS Code, Cursor, and other client configurations.
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
### Try it Online
|
### Try it Online
|
||||||
|
|||||||
172
app/[lang]/layout.tsx
Normal file
172
app/[lang]/layout.tsx
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
import { GoogleAnalytics } from "@next/third-parties/google"
|
||||||
|
import type { Metadata, Viewport } from "next"
|
||||||
|
import { JetBrains_Mono, Plus_Jakarta_Sans } from "next/font/google"
|
||||||
|
import { notFound } from "next/navigation"
|
||||||
|
import { DiagramProvider } from "@/contexts/diagram-context"
|
||||||
|
import { DictionaryProvider } from "@/hooks/use-dictionary"
|
||||||
|
import type { Locale } from "@/lib/i18n/config"
|
||||||
|
import { i18n } from "@/lib/i18n/config"
|
||||||
|
import { getDictionary, hasLocale } from "@/lib/i18n/dictionaries"
|
||||||
|
|
||||||
|
import "../globals.css"
|
||||||
|
|
||||||
|
const plusJakarta = Plus_Jakarta_Sans({
|
||||||
|
variable: "--font-sans",
|
||||||
|
subsets: ["latin"],
|
||||||
|
weight: ["400", "500", "600", "700"],
|
||||||
|
})
|
||||||
|
|
||||||
|
const jetbrainsMono = JetBrains_Mono({
|
||||||
|
variable: "--font-mono",
|
||||||
|
subsets: ["latin"],
|
||||||
|
weight: ["400", "500"],
|
||||||
|
})
|
||||||
|
|
||||||
|
export const viewport: Viewport = {
|
||||||
|
width: "device-width",
|
||||||
|
initialScale: 1,
|
||||||
|
maximumScale: 1,
|
||||||
|
userScalable: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate static params for all locales
|
||||||
|
export async function generateStaticParams() {
|
||||||
|
return i18n.locales.map((locale) => ({ lang: locale }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate metadata per locale
|
||||||
|
export async function generateMetadata({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
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
|
||||||
|
|
||||||
|
// 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",
|
||||||
|
}
|
||||||
|
|
||||||
|
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.",
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: titles[lang],
|
||||||
|
description: descriptions[lang],
|
||||||
|
keywords: [
|
||||||
|
"AI diagram generator",
|
||||||
|
"AWS architecture",
|
||||||
|
"flowchart creator",
|
||||||
|
"draw.io",
|
||||||
|
"AI drawing tool",
|
||||||
|
"technical diagrams",
|
||||||
|
"diagram automation",
|
||||||
|
"free diagram generator",
|
||||||
|
"online diagram maker",
|
||||||
|
],
|
||||||
|
authors: [{ name: "Next AI Draw.io" }],
|
||||||
|
creator: "Next AI Draw.io",
|
||||||
|
publisher: "Next AI Draw.io",
|
||||||
|
metadataBase: new URL("https://next-ai-drawio.jiang.jp"),
|
||||||
|
openGraph: {
|
||||||
|
title: titles[lang],
|
||||||
|
description: descriptions[lang],
|
||||||
|
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",
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: "/architecture.png",
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: "Next AI Draw.io - AI-powered diagram creation tool",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
title: titles[lang],
|
||||||
|
description: descriptions[lang],
|
||||||
|
images: ["/architecture.png"],
|
||||||
|
},
|
||||||
|
robots: {
|
||||||
|
index: true,
|
||||||
|
follow: true,
|
||||||
|
googleBot: {
|
||||||
|
index: true,
|
||||||
|
follow: true,
|
||||||
|
"max-video-preview": -1,
|
||||||
|
"max-image-preview": "large",
|
||||||
|
"max-snippet": -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
icons: {
|
||||||
|
icon: "/favicon.ico",
|
||||||
|
},
|
||||||
|
alternates: {
|
||||||
|
languages: {
|
||||||
|
en: "/en",
|
||||||
|
zh: "/zh",
|
||||||
|
ja: "/ja",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function RootLayout({
|
||||||
|
children,
|
||||||
|
params,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode
|
||||||
|
params: Promise<{ lang: string }>
|
||||||
|
}>) {
|
||||||
|
const { lang } = await params
|
||||||
|
if (!hasLocale(lang)) notFound()
|
||||||
|
const validLang = lang as Locale
|
||||||
|
const dictionary = await getDictionary(validLang)
|
||||||
|
|
||||||
|
const jsonLd = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "SoftwareApplication",
|
||||||
|
name: "Next AI Draw.io",
|
||||||
|
applicationCategory: "DesignApplication",
|
||||||
|
operatingSystem: "Web Browser",
|
||||||
|
description:
|
||||||
|
"AI-powered diagram generator with targeted XML editing capabilities that integrates with draw.io for creating AWS architecture diagrams, flowcharts, and technical diagrams. Features diagram history, multi-provider AI support, and real-time collaboration.",
|
||||||
|
url: "https://next-ai-drawio.jiang.jp",
|
||||||
|
inLanguage: validLang,
|
||||||
|
offers: {
|
||||||
|
"@type": "Offer",
|
||||||
|
price: "0",
|
||||||
|
priceCurrency: "USD",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<html lang={validLang} suppressHydrationWarning>
|
||||||
|
<head>
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
<body
|
||||||
|
className={`${plusJakarta.variable} ${jetbrainsMono.variable} antialiased`}
|
||||||
|
>
|
||||||
|
<DictionaryProvider dictionary={dictionary}>
|
||||||
|
<DiagramProvider>{children}</DiagramProvider>
|
||||||
|
</DictionaryProvider>
|
||||||
|
</body>
|
||||||
|
{process.env.NEXT_PUBLIC_GA_ID && (
|
||||||
|
<GoogleAnalytics gaId={process.env.NEXT_PUBLIC_GA_ID} />
|
||||||
|
)}
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { useEffect, useRef, useState } from "react"
|
import { useCallback, useEffect, useRef, useState } from "react"
|
||||||
import { DrawIoEmbed } from "react-drawio"
|
import { DrawIoEmbed } from "react-drawio"
|
||||||
import type { ImperativePanelHandle } from "react-resizable-panels"
|
import type { ImperativePanelHandle } from "react-resizable-panels"
|
||||||
import ChatPanel from "@/components/chat-panel"
|
import ChatPanel from "@/components/chat-panel"
|
||||||
@@ -15,8 +15,15 @@ const drawioBaseUrl =
|
|||||||
process.env.NEXT_PUBLIC_DRAWIO_BASE_URL || "https://embed.diagrams.net"
|
process.env.NEXT_PUBLIC_DRAWIO_BASE_URL || "https://embed.diagrams.net"
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const { drawioRef, handleDiagramExport, onDrawioLoad, resetDrawioReady } =
|
const {
|
||||||
useDiagram()
|
drawioRef,
|
||||||
|
handleDiagramExport,
|
||||||
|
onDrawioLoad,
|
||||||
|
resetDrawioReady,
|
||||||
|
saveDiagramToStorage,
|
||||||
|
showSaveDialog,
|
||||||
|
setShowSaveDialog,
|
||||||
|
} = useDiagram()
|
||||||
const [isMobile, setIsMobile] = useState(false)
|
const [isMobile, setIsMobile] = useState(false)
|
||||||
const [isChatVisible, setIsChatVisible] = useState(true)
|
const [isChatVisible, setIsChatVisible] = useState(true)
|
||||||
const [drawioUi, setDrawioUi] = useState<"min" | "sketch">("min")
|
const [drawioUi, setDrawioUi] = useState<"min" | "sketch">("min")
|
||||||
@@ -25,6 +32,29 @@ export default function Home() {
|
|||||||
const [closeProtection, setCloseProtection] = useState(false)
|
const [closeProtection, setCloseProtection] = useState(false)
|
||||||
|
|
||||||
const chatPanelRef = useRef<ImperativePanelHandle>(null)
|
const chatPanelRef = useRef<ImperativePanelHandle>(null)
|
||||||
|
const isSavingRef = useRef(false)
|
||||||
|
const mouseOverDrawioRef = useRef(false)
|
||||||
|
const isMobileRef = useRef(false)
|
||||||
|
|
||||||
|
// Reset saving flag when dialog closes (with delay to ignore lingering save events from draw.io)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showSaveDialog) {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
isSavingRef.current = false
|
||||||
|
}, 1000)
|
||||||
|
return () => clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
}, [showSaveDialog])
|
||||||
|
|
||||||
|
// Handle save from draw.io's built-in save button
|
||||||
|
// Note: draw.io sends save events for various reasons (focus changes, etc.)
|
||||||
|
// We use mouse position to determine if the user is interacting with draw.io
|
||||||
|
const handleDrawioSave = useCallback(() => {
|
||||||
|
if (!mouseOverDrawioRef.current) return
|
||||||
|
if (isSavingRef.current) return
|
||||||
|
isSavingRef.current = true
|
||||||
|
setShowSaveDialog(true)
|
||||||
|
}, [setShowSaveDialog])
|
||||||
|
|
||||||
// Load preferences from localStorage after mount
|
// Load preferences from localStorage after mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -35,12 +65,10 @@ export default function Home() {
|
|||||||
|
|
||||||
const savedDarkMode = localStorage.getItem("next-ai-draw-io-dark-mode")
|
const savedDarkMode = localStorage.getItem("next-ai-draw-io-dark-mode")
|
||||||
if (savedDarkMode !== null) {
|
if (savedDarkMode !== null) {
|
||||||
// Use saved preference
|
|
||||||
const isDark = savedDarkMode === "true"
|
const isDark = savedDarkMode === "true"
|
||||||
setDarkMode(isDark)
|
setDarkMode(isDark)
|
||||||
document.documentElement.classList.toggle("dark", isDark)
|
document.documentElement.classList.toggle("dark", isDark)
|
||||||
} else {
|
} else {
|
||||||
// First visit: match browser preference
|
|
||||||
const prefersDark = window.matchMedia(
|
const prefersDark = window.matchMedia(
|
||||||
"(prefers-color-scheme: dark)",
|
"(prefers-color-scheme: dark)",
|
||||||
).matches
|
).matches
|
||||||
@@ -58,25 +86,44 @@ export default function Home() {
|
|||||||
setIsLoaded(true)
|
setIsLoaded(true)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const toggleDarkMode = () => {
|
const handleDarkModeChange = async () => {
|
||||||
|
await saveDiagramToStorage()
|
||||||
const newValue = !darkMode
|
const newValue = !darkMode
|
||||||
setDarkMode(newValue)
|
setDarkMode(newValue)
|
||||||
localStorage.setItem("next-ai-draw-io-dark-mode", String(newValue))
|
localStorage.setItem("next-ai-draw-io-dark-mode", String(newValue))
|
||||||
document.documentElement.classList.toggle("dark", newValue)
|
document.documentElement.classList.toggle("dark", newValue)
|
||||||
// Reset so onDrawioLoad fires again after remount
|
|
||||||
resetDrawioReady()
|
resetDrawioReady()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check mobile
|
const handleDrawioUiChange = async () => {
|
||||||
|
await saveDiagramToStorage()
|
||||||
|
const newUi = drawioUi === "min" ? "sketch" : "min"
|
||||||
|
localStorage.setItem("drawio-theme", newUi)
|
||||||
|
setDrawioUi(newUi)
|
||||||
|
resetDrawioReady()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check mobile - save diagram and reset draw.io before crossing breakpoint
|
||||||
|
const isInitialRenderRef = useRef(true)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkMobile = () => {
|
const checkMobile = () => {
|
||||||
setIsMobile(window.innerWidth < 768)
|
const newIsMobile = window.innerWidth < 768
|
||||||
|
if (
|
||||||
|
!isInitialRenderRef.current &&
|
||||||
|
newIsMobile !== isMobileRef.current
|
||||||
|
) {
|
||||||
|
saveDiagramToStorage().catch(() => {})
|
||||||
|
resetDrawioReady()
|
||||||
|
}
|
||||||
|
isMobileRef.current = newIsMobile
|
||||||
|
isInitialRenderRef.current = false
|
||||||
|
setIsMobile(newIsMobile)
|
||||||
}
|
}
|
||||||
|
|
||||||
checkMobile()
|
checkMobile()
|
||||||
window.addEventListener("resize", checkMobile)
|
window.addEventListener("resize", checkMobile)
|
||||||
return () => window.removeEventListener("resize", checkMobile)
|
return () => window.removeEventListener("resize", checkMobile)
|
||||||
}, [])
|
}, [saveDiagramToStorage, resetDrawioReady])
|
||||||
|
|
||||||
const toggleChatPanel = () => {
|
const toggleChatPanel = () => {
|
||||||
const panel = chatPanelRef.current
|
const panel = chatPanelRef.current
|
||||||
@@ -122,11 +169,9 @@ export default function Home() {
|
|||||||
<div className="h-screen bg-background relative overflow-hidden">
|
<div className="h-screen bg-background relative overflow-hidden">
|
||||||
<ResizablePanelGroup
|
<ResizablePanelGroup
|
||||||
id="main-panel-group"
|
id="main-panel-group"
|
||||||
key={isMobile ? "mobile" : "desktop"}
|
|
||||||
direction={isMobile ? "vertical" : "horizontal"}
|
direction={isMobile ? "vertical" : "horizontal"}
|
||||||
className="h-full"
|
className="h-full"
|
||||||
>
|
>
|
||||||
{/* Draw.io Canvas */}
|
|
||||||
<ResizablePanel
|
<ResizablePanel
|
||||||
id="drawio-panel"
|
id="drawio-panel"
|
||||||
defaultSize={isMobile ? 50 : 67}
|
defaultSize={isMobile ? 50 : 67}
|
||||||
@@ -136,6 +181,12 @@ export default function Home() {
|
|||||||
className={`h-full relative ${
|
className={`h-full relative ${
|
||||||
isMobile ? "p-1" : "p-2"
|
isMobile ? "p-1" : "p-2"
|
||||||
}`}
|
}`}
|
||||||
|
onMouseEnter={() => {
|
||||||
|
mouseOverDrawioRef.current = true
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
mouseOverDrawioRef.current = false
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="h-full rounded-xl overflow-hidden shadow-soft-lg border border-border/30">
|
<div className="h-full rounded-xl overflow-hidden shadow-soft-lg border border-border/30">
|
||||||
{isLoaded ? (
|
{isLoaded ? (
|
||||||
@@ -144,6 +195,7 @@ export default function Home() {
|
|||||||
ref={drawioRef}
|
ref={drawioRef}
|
||||||
onExport={handleDiagramExport}
|
onExport={handleDiagramExport}
|
||||||
onLoad={onDrawioLoad}
|
onLoad={onDrawioLoad}
|
||||||
|
onSave={handleDrawioSave}
|
||||||
baseUrl={drawioBaseUrl}
|
baseUrl={drawioBaseUrl}
|
||||||
urlParameters={{
|
urlParameters={{
|
||||||
ui: drawioUi,
|
ui: drawioUi,
|
||||||
@@ -167,6 +219,7 @@ export default function Home() {
|
|||||||
|
|
||||||
{/* Chat Panel */}
|
{/* Chat Panel */}
|
||||||
<ResizablePanel
|
<ResizablePanel
|
||||||
|
key={isMobile ? "mobile" : "desktop"}
|
||||||
id="chat-panel"
|
id="chat-panel"
|
||||||
ref={chatPanelRef}
|
ref={chatPanelRef}
|
||||||
defaultSize={isMobile ? 50 : 33}
|
defaultSize={isMobile ? 50 : 33}
|
||||||
@@ -182,15 +235,9 @@ export default function Home() {
|
|||||||
isVisible={isChatVisible}
|
isVisible={isChatVisible}
|
||||||
onToggleVisibility={toggleChatPanel}
|
onToggleVisibility={toggleChatPanel}
|
||||||
drawioUi={drawioUi}
|
drawioUi={drawioUi}
|
||||||
onToggleDrawioUi={() => {
|
onToggleDrawioUi={handleDrawioUiChange}
|
||||||
const newUi =
|
|
||||||
drawioUi === "min" ? "sketch" : "min"
|
|
||||||
localStorage.setItem("drawio-theme", newUi)
|
|
||||||
setDrawioUi(newUi)
|
|
||||||
resetDrawioReady()
|
|
||||||
}}
|
|
||||||
darkMode={darkMode}
|
darkMode={darkMode}
|
||||||
onToggleDarkMode={toggleDarkMode}
|
onToggleDarkMode={handleDarkModeChange}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
onCloseProtectionChange={setCloseProtection}
|
onCloseProtectionChange={setCloseProtection}
|
||||||
/>
|
/>
|
||||||
@@ -8,7 +8,9 @@ import {
|
|||||||
stepCountIs,
|
stepCountIs,
|
||||||
streamText,
|
streamText,
|
||||||
} from "ai"
|
} from "ai"
|
||||||
|
import fs from "fs/promises"
|
||||||
import { jsonrepair } from "jsonrepair"
|
import { jsonrepair } from "jsonrepair"
|
||||||
|
import path from "path"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { getAIModel, supportsPromptCaching } from "@/lib/ai-providers"
|
import { getAIModel, supportsPromptCaching } from "@/lib/ai-providers"
|
||||||
import { findCachedResponse } from "@/lib/cached-responses"
|
import { findCachedResponse } from "@/lib/cached-responses"
|
||||||
@@ -70,29 +72,41 @@ function isMinimalDiagram(xml: string): boolean {
|
|||||||
|
|
||||||
// Helper function to replace historical tool call XML with placeholders
|
// Helper function to replace historical tool call XML with placeholders
|
||||||
// This reduces token usage and forces LLM to rely on the current diagram XML (source of truth)
|
// This reduces token usage and forces LLM to rely on the current diagram XML (source of truth)
|
||||||
|
// Also fixes invalid/undefined inputs from interrupted streaming
|
||||||
function replaceHistoricalToolInputs(messages: any[]): any[] {
|
function replaceHistoricalToolInputs(messages: any[]): any[] {
|
||||||
return messages.map((msg) => {
|
return messages.map((msg) => {
|
||||||
if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
|
if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
|
||||||
return msg
|
return msg
|
||||||
}
|
}
|
||||||
const replacedContent = msg.content.map((part: any) => {
|
const replacedContent = msg.content
|
||||||
if (part.type === "tool-call") {
|
.map((part: any) => {
|
||||||
const toolName = part.toolName
|
if (part.type === "tool-call") {
|
||||||
if (
|
const toolName = part.toolName
|
||||||
toolName === "display_diagram" ||
|
// Fix invalid/undefined inputs from interrupted streaming
|
||||||
toolName === "edit_diagram"
|
if (
|
||||||
) {
|
!part.input ||
|
||||||
return {
|
typeof part.input !== "object" ||
|
||||||
...part,
|
Object.keys(part.input).length === 0
|
||||||
input: {
|
) {
|
||||||
placeholder:
|
// Skip tool calls with invalid inputs entirely
|
||||||
"[XML content replaced - see current diagram XML in system context]",
|
return null
|
||||||
},
|
}
|
||||||
|
if (
|
||||||
|
toolName === "display_diagram" ||
|
||||||
|
toolName === "edit_diagram"
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
...part,
|
||||||
|
input: {
|
||||||
|
placeholder:
|
||||||
|
"[XML content replaced - see current diagram XML in system context]",
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
return part
|
||||||
return part
|
})
|
||||||
})
|
.filter(Boolean) // Remove null entries (invalid tool calls)
|
||||||
return { ...msg, content: replacedContent }
|
return { ...msg, content: replacedContent }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -231,6 +245,36 @@ ${userInputText}
|
|||||||
// Convert UIMessages to ModelMessages and add system message
|
// Convert UIMessages to ModelMessages and add system message
|
||||||
const modelMessages = convertToModelMessages(messages)
|
const modelMessages = convertToModelMessages(messages)
|
||||||
|
|
||||||
|
// DEBUG: Log incoming messages structure
|
||||||
|
console.log("[route.ts] Incoming messages count:", messages.length)
|
||||||
|
messages.forEach((msg: any, idx: number) => {
|
||||||
|
console.log(
|
||||||
|
`[route.ts] Message ${idx} role:`,
|
||||||
|
msg.role,
|
||||||
|
"parts count:",
|
||||||
|
msg.parts?.length,
|
||||||
|
)
|
||||||
|
if (msg.parts) {
|
||||||
|
msg.parts.forEach((part: any, partIdx: number) => {
|
||||||
|
if (
|
||||||
|
part.type === "tool-invocation" ||
|
||||||
|
part.type === "tool-result"
|
||||||
|
) {
|
||||||
|
console.log(`[route.ts] Part ${partIdx}:`, {
|
||||||
|
type: part.type,
|
||||||
|
toolName: part.toolName,
|
||||||
|
hasInput: !!part.input,
|
||||||
|
inputType: typeof part.input,
|
||||||
|
inputKeys:
|
||||||
|
part.input && typeof part.input === "object"
|
||||||
|
? Object.keys(part.input)
|
||||||
|
: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Replace historical tool call XML with placeholders to reduce tokens
|
// Replace historical tool call XML with placeholders to reduce tokens
|
||||||
// Disabled by default - some models (e.g. minimax) copy placeholders instead of generating XML
|
// Disabled by default - some models (e.g. minimax) copy placeholders instead of generating XML
|
||||||
const enableHistoryReplace =
|
const enableHistoryReplace =
|
||||||
@@ -246,6 +290,63 @@ ${userInputText}
|
|||||||
msg.content && Array.isArray(msg.content) && msg.content.length > 0,
|
msg.content && Array.isArray(msg.content) && msg.content.length > 0,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Filter out tool-calls with invalid inputs (from failed repair or interrupted streaming)
|
||||||
|
// Bedrock API rejects messages where toolUse.input is not a valid JSON object
|
||||||
|
enhancedMessages = enhancedMessages
|
||||||
|
.map((msg: any) => {
|
||||||
|
if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
const filteredContent = msg.content.filter((part: any) => {
|
||||||
|
if (part.type === "tool-call") {
|
||||||
|
// Check if input is a valid object (not null, undefined, or empty)
|
||||||
|
if (
|
||||||
|
!part.input ||
|
||||||
|
typeof part.input !== "object" ||
|
||||||
|
Object.keys(part.input).length === 0
|
||||||
|
) {
|
||||||
|
console.warn(
|
||||||
|
`[route.ts] Filtering out tool-call with invalid input:`,
|
||||||
|
{ toolName: part.toolName, input: part.input },
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return { ...msg, content: filteredContent }
|
||||||
|
})
|
||||||
|
.filter((msg: any) => msg.content && msg.content.length > 0)
|
||||||
|
|
||||||
|
// DEBUG: Log modelMessages structure (what's being sent to AI)
|
||||||
|
console.log("[route.ts] Model messages count:", enhancedMessages.length)
|
||||||
|
enhancedMessages.forEach((msg: any, idx: number) => {
|
||||||
|
console.log(
|
||||||
|
`[route.ts] ModelMsg ${idx} role:`,
|
||||||
|
msg.role,
|
||||||
|
"content count:",
|
||||||
|
msg.content?.length,
|
||||||
|
)
|
||||||
|
if (msg.content) {
|
||||||
|
msg.content.forEach((part: any, partIdx: number) => {
|
||||||
|
if (part.type === "tool-call" || part.type === "tool-result") {
|
||||||
|
console.log(`[route.ts] Content ${partIdx}:`, {
|
||||||
|
type: part.type,
|
||||||
|
toolName: part.toolName,
|
||||||
|
hasInput: !!part.input,
|
||||||
|
inputType: typeof part.input,
|
||||||
|
inputValue:
|
||||||
|
part.input === undefined
|
||||||
|
? "undefined"
|
||||||
|
: part.input === null
|
||||||
|
? "null"
|
||||||
|
: "object",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Update the last message with user input only (XML moved to separate cached system message)
|
// Update the last message with user input only (XML moved to separate cached system message)
|
||||||
if (enhancedMessages.length >= 1) {
|
if (enhancedMessages.length >= 1) {
|
||||||
const lastModelMessage = enhancedMessages[enhancedMessages.length - 1]
|
const lastModelMessage = enhancedMessages[enhancedMessages.length - 1]
|
||||||
@@ -327,14 +428,30 @@ ${userInputText}
|
|||||||
stopWhen: stepCountIs(5),
|
stopWhen: stepCountIs(5),
|
||||||
// Repair truncated tool calls when maxOutputTokens is reached mid-JSON
|
// Repair truncated tool calls when maxOutputTokens is reached mid-JSON
|
||||||
experimental_repairToolCall: async ({ toolCall, error }) => {
|
experimental_repairToolCall: async ({ toolCall, error }) => {
|
||||||
|
// DEBUG: Log what we're trying to repair
|
||||||
|
console.log(`[repairToolCall] Tool: ${toolCall.toolName}`)
|
||||||
|
console.log(
|
||||||
|
`[repairToolCall] Error: ${error.name} - ${error.message}`,
|
||||||
|
)
|
||||||
|
console.log(`[repairToolCall] Input type: ${typeof toolCall.input}`)
|
||||||
|
console.log(`[repairToolCall] Input value:`, toolCall.input)
|
||||||
|
|
||||||
// Only attempt repair for invalid tool input (broken JSON from truncation)
|
// Only attempt repair for invalid tool input (broken JSON from truncation)
|
||||||
if (
|
if (
|
||||||
error instanceof InvalidToolInputError ||
|
error instanceof InvalidToolInputError ||
|
||||||
error.name === "AI_InvalidToolInputError"
|
error.name === "AI_InvalidToolInputError"
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
// Pre-process to fix common LLM JSON errors that jsonrepair can't handle
|
||||||
|
let inputToRepair = toolCall.input
|
||||||
|
if (typeof inputToRepair === "string") {
|
||||||
|
// Fix `:=` instead of `: ` (LLM sometimes generates this)
|
||||||
|
inputToRepair = inputToRepair.replace(/:=/g, ": ")
|
||||||
|
// Fix `= "` instead of `: "`
|
||||||
|
inputToRepair = inputToRepair.replace(/=\s*"/g, ': "')
|
||||||
|
}
|
||||||
// Use jsonrepair to fix truncated JSON
|
// Use jsonrepair to fix truncated JSON
|
||||||
const repairedInput = jsonrepair(toolCall.input)
|
const repairedInput = jsonrepair(inputToRepair)
|
||||||
console.log(
|
console.log(
|
||||||
`[repairToolCall] Repaired truncated JSON for tool: ${toolCall.toolName}`,
|
`[repairToolCall] Repaired truncated JSON for tool: ${toolCall.toolName}`,
|
||||||
)
|
)
|
||||||
@@ -344,6 +461,26 @@ ${userInputText}
|
|||||||
`[repairToolCall] Failed to repair JSON for tool: ${toolCall.toolName}`,
|
`[repairToolCall] Failed to repair JSON for tool: ${toolCall.toolName}`,
|
||||||
repairError,
|
repairError,
|
||||||
)
|
)
|
||||||
|
// Return a placeholder input to avoid API errors in multi-step
|
||||||
|
// The tool will fail gracefully on client side
|
||||||
|
if (toolCall.toolName === "edit_diagram") {
|
||||||
|
return {
|
||||||
|
...toolCall,
|
||||||
|
input: {
|
||||||
|
operations: [],
|
||||||
|
_error: "JSON repair failed - no operations to apply",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (toolCall.toolName === "display_diagram") {
|
||||||
|
return {
|
||||||
|
...toolCall,
|
||||||
|
input: {
|
||||||
|
xml: "",
|
||||||
|
_error: "JSON repair failed - empty diagram",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -408,33 +545,37 @@ Notes:
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
edit_diagram: {
|
edit_diagram: {
|
||||||
description: `Edit specific parts of the current diagram by replacing exact line matches. Use this tool to make targeted fixes without regenerating the entire XML.
|
description: `Edit the current diagram by ID-based operations (update/add/delete cells).
|
||||||
CRITICAL: Copy-paste the EXACT search pattern from the "Current diagram XML" in system context. Do NOT reorder attributes or reformat - the attribute order in draw.io XML varies and you MUST match it exactly.
|
|
||||||
IMPORTANT: Keep edits concise:
|
|
||||||
- COPY the exact mxCell line from the current XML (attribute order matters!)
|
|
||||||
- Only include the lines that are changing, plus 1-2 surrounding lines for context if needed
|
|
||||||
- Break large changes into multiple smaller edits
|
|
||||||
- Each search must contain complete lines (never truncate mid-line)
|
|
||||||
- First match only - be specific enough to target the right element
|
|
||||||
|
|
||||||
⚠️ JSON ESCAPING: Every " inside string values MUST be escaped as \\". Example: x=\\"100\\" y=\\"200\\" - BOTH quotes need backslashes!`,
|
Operations:
|
||||||
|
- 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.
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
⚠️ JSON ESCAPING: Every " inside new_xml MUST be escaped as \\". Example: id=\\"5\\" value=\\"Label\\"`,
|
||||||
inputSchema: z.object({
|
inputSchema: z.object({
|
||||||
edits: z
|
operations: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
search: z
|
type: z
|
||||||
|
.enum(["update", "add", "delete"])
|
||||||
|
.describe("Operation type"),
|
||||||
|
cell_id: z
|
||||||
.string()
|
.string()
|
||||||
.describe(
|
.describe(
|
||||||
"EXACT lines copied from current XML (preserve attribute order!)",
|
"The id of the mxCell. Must match the id attribute in new_xml.",
|
||||||
),
|
),
|
||||||
replace: z
|
new_xml: z
|
||||||
.string()
|
.string()
|
||||||
.describe("Replacement lines"),
|
.optional()
|
||||||
|
.describe(
|
||||||
|
"Complete mxCell XML element (required for update/add)",
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.describe(
|
.describe("Array of operations to apply"),
|
||||||
"Array of search/replace pairs to apply sequentially",
|
|
||||||
),
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
append_diagram: {
|
append_diagram: {
|
||||||
@@ -457,6 +598,69 @@ Example: If previous output ended with '<mxCell id="x" style="rounded=1', contin
|
|||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
get_shape_library: {
|
||||||
|
description: `Get draw.io shape/icon library documentation with style syntax and shape names.
|
||||||
|
|
||||||
|
Available libraries:
|
||||||
|
- Cloud: aws4, azure2, gcp2, alibaba_cloud, openstack, salesforce
|
||||||
|
- Networking: cisco19, network, kubernetes, vvd, rack
|
||||||
|
- Business: bpmn, lean_mapping
|
||||||
|
- General: flowchart, basic, arrows2, infographic, sitemap
|
||||||
|
- UI/Mockups: android
|
||||||
|
- Enterprise: citrix, sap, mscae, atlassian
|
||||||
|
- Engineering: fluidpower, electrical, pid, cabinets, floorplan
|
||||||
|
- Icons: webicons
|
||||||
|
|
||||||
|
Call this tool to get shape names and usage syntax for a specific library.`,
|
||||||
|
inputSchema: z.object({
|
||||||
|
library: z
|
||||||
|
.string()
|
||||||
|
.describe(
|
||||||
|
"Library name (e.g., 'aws4', 'kubernetes', 'flowchart')",
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
execute: async ({ library }) => {
|
||||||
|
// Sanitize input - prevent path traversal attacks
|
||||||
|
const sanitizedLibrary = library
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9_-]/g, "")
|
||||||
|
|
||||||
|
if (sanitizedLibrary !== library.toLowerCase()) {
|
||||||
|
return `Invalid library name "${library}". Use only letters, numbers, underscores, and hyphens.`
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseDir = path.join(
|
||||||
|
process.cwd(),
|
||||||
|
"docs/shape-libraries",
|
||||||
|
)
|
||||||
|
const filePath = path.join(
|
||||||
|
baseDir,
|
||||||
|
`${sanitizedLibrary}.md`,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify path stays within expected directory
|
||||||
|
const resolvedPath = path.resolve(filePath)
|
||||||
|
if (!resolvedPath.startsWith(path.resolve(baseDir))) {
|
||||||
|
return `Invalid library path.`
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = await fs.readFile(filePath, "utf-8")
|
||||||
|
return content
|
||||||
|
} catch (error) {
|
||||||
|
if (
|
||||||
|
(error as NodeJS.ErrnoException).code === "ENOENT"
|
||||||
|
) {
|
||||||
|
return `Library "${library}" not found. Available: aws4, azure2, gcp2, alibaba_cloud, cisco19, kubernetes, network, bpmn, flowchart, basic, arrows2, vvd, salesforce, citrix, sap, mscae, atlassian, fluidpower, electrical, pid, cabinets, floorplan, webicons, infographic, sitemap, android, lean_mapping, openstack, rack`
|
||||||
|
}
|
||||||
|
console.error(
|
||||||
|
`[get_shape_library] Error loading "${library}":`,
|
||||||
|
error,
|
||||||
|
)
|
||||||
|
return `Error loading library "${library}". Please try again.`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
...(process.env.TEMPERATURE !== undefined && {
|
...(process.env.TEMPERATURE !== undefined && {
|
||||||
temperature: parseFloat(process.env.TEMPERATURE),
|
temperature: parseFloat(process.env.TEMPERATURE),
|
||||||
|
|||||||
125
app/layout.tsx
125
app/layout.tsx
@@ -1,125 +0,0 @@
|
|||||||
import { GoogleAnalytics } from "@next/third-parties/google"
|
|
||||||
import type { Metadata, Viewport } from "next"
|
|
||||||
import { JetBrains_Mono, Plus_Jakarta_Sans } from "next/font/google"
|
|
||||||
import { DiagramProvider } from "@/contexts/diagram-context"
|
|
||||||
|
|
||||||
import "./globals.css"
|
|
||||||
|
|
||||||
const plusJakarta = Plus_Jakarta_Sans({
|
|
||||||
variable: "--font-sans",
|
|
||||||
subsets: ["latin"],
|
|
||||||
weight: ["400", "500", "600", "700"],
|
|
||||||
})
|
|
||||||
|
|
||||||
const jetbrainsMono = JetBrains_Mono({
|
|
||||||
variable: "--font-mono",
|
|
||||||
subsets: ["latin"],
|
|
||||||
weight: ["400", "500"],
|
|
||||||
})
|
|
||||||
|
|
||||||
export const viewport: Viewport = {
|
|
||||||
width: "device-width",
|
|
||||||
initialScale: 1,
|
|
||||||
maximumScale: 1,
|
|
||||||
userScalable: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "Next AI Draw.io - AI-Powered Diagram Generator",
|
|
||||||
description:
|
|
||||||
"Create AWS architecture diagrams, flowcharts, and technical diagrams using AI. Free online tool integrating draw.io with AI assistance for professional diagram creation.",
|
|
||||||
keywords: [
|
|
||||||
"AI diagram generator",
|
|
||||||
"AWS architecture",
|
|
||||||
"flowchart creator",
|
|
||||||
"draw.io",
|
|
||||||
"AI drawing tool",
|
|
||||||
"technical diagrams",
|
|
||||||
"diagram automation",
|
|
||||||
"free diagram generator",
|
|
||||||
"online diagram maker",
|
|
||||||
],
|
|
||||||
authors: [{ name: "Next AI Draw.io" }],
|
|
||||||
creator: "Next AI Draw.io",
|
|
||||||
publisher: "Next AI Draw.io",
|
|
||||||
metadataBase: new URL("https://next-ai-drawio.jiang.jp"),
|
|
||||||
openGraph: {
|
|
||||||
title: "Next AI Draw.io - AI Diagram Generator",
|
|
||||||
description:
|
|
||||||
"Create professional diagrams with AI assistance. Supports AWS architecture, flowcharts, and more.",
|
|
||||||
type: "website",
|
|
||||||
url: "https://next-ai-drawio.jiang.jp",
|
|
||||||
siteName: "Next AI Draw.io",
|
|
||||||
locale: "en_US",
|
|
||||||
images: [
|
|
||||||
{
|
|
||||||
url: "/architecture.png",
|
|
||||||
width: 1200,
|
|
||||||
height: 630,
|
|
||||||
alt: "Next AI Draw.io - AI-powered diagram creation tool",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
twitter: {
|
|
||||||
card: "summary_large_image",
|
|
||||||
title: "Next AI Draw.io - AI Diagram Generator",
|
|
||||||
description:
|
|
||||||
"Create professional diagrams with AI assistance. Free, no login required.",
|
|
||||||
images: ["/architecture.png"],
|
|
||||||
},
|
|
||||||
robots: {
|
|
||||||
index: true,
|
|
||||||
follow: true,
|
|
||||||
googleBot: {
|
|
||||||
index: true,
|
|
||||||
follow: true,
|
|
||||||
"max-video-preview": -1,
|
|
||||||
"max-image-preview": "large",
|
|
||||||
"max-snippet": -1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
icons: {
|
|
||||||
icon: "/favicon.ico",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function RootLayout({
|
|
||||||
children,
|
|
||||||
}: Readonly<{
|
|
||||||
children: React.ReactNode
|
|
||||||
}>) {
|
|
||||||
const jsonLd = {
|
|
||||||
"@context": "https://schema.org",
|
|
||||||
"@type": "SoftwareApplication",
|
|
||||||
name: "Next AI Draw.io",
|
|
||||||
applicationCategory: "DesignApplication",
|
|
||||||
operatingSystem: "Web Browser",
|
|
||||||
description:
|
|
||||||
"AI-powered diagram generator with targeted XML editing capabilities that integrates with draw.io for creating AWS architecture diagrams, flowcharts, and technical diagrams. Features diagram history, multi-provider AI support, and real-time collaboration.",
|
|
||||||
url: "https://next-ai-drawio.jiang.jp",
|
|
||||||
offers: {
|
|
||||||
"@type": "Offer",
|
|
||||||
price: "0",
|
|
||||||
priceCurrency: "USD",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<html lang="en" suppressHydrationWarning>
|
|
||||||
<head>
|
|
||||||
<script
|
|
||||||
type="application/ld+json"
|
|
||||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
|
||||||
/>
|
|
||||||
</head>
|
|
||||||
<body
|
|
||||||
className={`${plusJakarta.variable} ${jetbrainsMono.variable} antialiased`}
|
|
||||||
>
|
|
||||||
<DiagramProvider>{children}</DiagramProvider>
|
|
||||||
</body>
|
|
||||||
{process.env.NEXT_PUBLIC_GA_ID && (
|
|
||||||
<GoogleAnalytics gaId={process.env.NEXT_PUBLIC_GA_ID} />
|
|
||||||
)}
|
|
||||||
</html>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
28
app/manifest.ts
Normal file
28
app/manifest.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import type { MetadataRoute } from "next"
|
||||||
|
|
||||||
|
export default function manifest(): MetadataRoute.Manifest {
|
||||||
|
return {
|
||||||
|
name: "Next AI Draw.io",
|
||||||
|
short_name: "AIDraw.io",
|
||||||
|
description:
|
||||||
|
"Create AWS architecture diagrams, flowcharts, and technical diagrams using AI. Free online tool integrating draw.io with AI assistance for professional diagram creation.",
|
||||||
|
start_url: "/",
|
||||||
|
display: "standalone",
|
||||||
|
background_color: "#f9fafb",
|
||||||
|
theme_color: "#171d26",
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: "/favicon-192x192.png",
|
||||||
|
sizes: "192x192",
|
||||||
|
type: "image/png",
|
||||||
|
purpose: "any",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "/favicon-512x512.png",
|
||||||
|
sizes: "512x512",
|
||||||
|
type: "image/png",
|
||||||
|
purpose: "any",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,14 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Cloud, FileText, GitBranch, Palette, Zap } from "lucide-react"
|
import {
|
||||||
|
Cloud,
|
||||||
|
FileText,
|
||||||
|
GitBranch,
|
||||||
|
Palette,
|
||||||
|
Terminal,
|
||||||
|
Zap,
|
||||||
|
} from "lucide-react"
|
||||||
|
import { useDictionary } from "@/hooks/use-dictionary"
|
||||||
|
|
||||||
interface ExampleCardProps {
|
interface ExampleCardProps {
|
||||||
icon: React.ReactNode
|
icon: React.ReactNode
|
||||||
@@ -17,6 +25,8 @@ function ExampleCard({
|
|||||||
onClick,
|
onClick,
|
||||||
isNew,
|
isNew,
|
||||||
}: ExampleCardProps) {
|
}: ExampleCardProps) {
|
||||||
|
const dict = useDictionary()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
@@ -43,7 +53,7 @@ function ExampleCard({
|
|||||||
</h3>
|
</h3>
|
||||||
{isNew && (
|
{isNew && (
|
||||||
<span className="px-1.5 py-0.5 text-[10px] font-semibold bg-primary text-primary-foreground rounded">
|
<span className="px-1.5 py-0.5 text-[10px] font-semibold bg-primary text-primary-foreground rounded">
|
||||||
NEW
|
{dict.common.new}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -63,6 +73,8 @@ export default function ExamplePanel({
|
|||||||
setInput: (input: string) => void
|
setInput: (input: string) => void
|
||||||
setFiles: (files: File[]) => void
|
setFiles: (files: File[]) => void
|
||||||
}) {
|
}) {
|
||||||
|
const dict = useDictionary()
|
||||||
|
|
||||||
const handleReplicateFlowchart = async () => {
|
const handleReplicateFlowchart = async () => {
|
||||||
setInput("Replicate this flowchart.")
|
setInput("Replicate this flowchart.")
|
||||||
|
|
||||||
@@ -72,7 +84,7 @@ export default function ExamplePanel({
|
|||||||
const file = new File([blob], "example.png", { type: "image/png" })
|
const file = new File([blob], "example.png", { type: "image/png" })
|
||||||
setFiles([file])
|
setFiles([file])
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading example image:", error)
|
console.error(dict.errors.failedToLoadExample, error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +99,7 @@ export default function ExamplePanel({
|
|||||||
})
|
})
|
||||||
setFiles([file])
|
setFiles([file])
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading architecture image:", error)
|
console.error(dict.errors.failedToLoadExample, error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,42 +114,68 @@ export default function ExamplePanel({
|
|||||||
})
|
})
|
||||||
setFiles([file])
|
setFiles([file])
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading text file:", error)
|
console.error(dict.errors.failedToLoadExample, error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="py-6 px-2 animate-fade-in">
|
<div className="py-6 px-2 animate-fade-in">
|
||||||
|
{/* MCP Server Notice */}
|
||||||
|
<a
|
||||||
|
href="https://github.com/DayuanJiang/next-ai-draw-io/tree/main/packages/mcp-server"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="block mb-4 p-3 rounded-xl bg-gradient-to-r from-purple-500/10 to-blue-500/10 border border-purple-500/20 hover:border-purple-500/40 transition-colors group"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-8 h-8 rounded-lg bg-purple-500/20 flex items-center justify-center shrink-0">
|
||||||
|
<Terminal className="w-4 h-4 text-purple-500" />
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm font-medium text-foreground group-hover:text-purple-500 transition-colors">
|
||||||
|
{dict.examples.mcpServer}
|
||||||
|
</span>
|
||||||
|
<span className="px-1.5 py-0.5 text-[10px] font-semibold bg-purple-500 text-white rounded">
|
||||||
|
{dict.examples.preview}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{dict.examples.mcpDescription}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
{/* Welcome section */}
|
{/* Welcome section */}
|
||||||
<div className="text-center mb-6">
|
<div className="text-center mb-6">
|
||||||
<h2 className="text-lg font-semibold text-foreground mb-2">
|
<h2 className="text-lg font-semibold text-foreground mb-2">
|
||||||
Create diagrams with AI
|
{dict.examples.title}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm text-muted-foreground max-w-xs mx-auto">
|
<p className="text-sm text-muted-foreground max-w-xs mx-auto">
|
||||||
Describe what you want to create or upload an image to
|
{dict.examples.subtitle}
|
||||||
replicate
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Examples grid */}
|
{/* Examples grid */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wider px-1">
|
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wider px-1">
|
||||||
Quick Examples
|
{dict.examples.quickExamples}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<ExampleCard
|
<ExampleCard
|
||||||
icon={<FileText className="w-4 h-4 text-primary" />}
|
icon={<FileText className="w-4 h-4 text-primary" />}
|
||||||
title="Paper to Diagram"
|
title={dict.examples.paperToDiagram}
|
||||||
description="Upload .pdf, .txt, .md, .json, .csv, .py, .js, .ts and more"
|
description={dict.examples.paperDescription}
|
||||||
onClick={handlePdfExample}
|
onClick={handlePdfExample}
|
||||||
isNew
|
isNew
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ExampleCard
|
<ExampleCard
|
||||||
icon={<Zap className="w-4 h-4 text-primary" />}
|
icon={<Zap className="w-4 h-4 text-primary" />}
|
||||||
title="Animated Diagram"
|
title={dict.examples.animatedDiagram}
|
||||||
description="Draw a transformer architecture with animated connectors"
|
description={dict.examples.animatedDescription}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setInput(
|
setInput(
|
||||||
"Give me a **animated connector** diagram of transformer's architecture",
|
"Give me a **animated connector** diagram of transformer's architecture",
|
||||||
@@ -148,22 +186,22 @@ export default function ExamplePanel({
|
|||||||
|
|
||||||
<ExampleCard
|
<ExampleCard
|
||||||
icon={<Cloud className="w-4 h-4 text-primary" />}
|
icon={<Cloud className="w-4 h-4 text-primary" />}
|
||||||
title="AWS Architecture"
|
title={dict.examples.awsArchitecture}
|
||||||
description="Create a cloud architecture diagram with AWS icons"
|
description={dict.examples.awsDescription}
|
||||||
onClick={handleReplicateArchitecture}
|
onClick={handleReplicateArchitecture}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ExampleCard
|
<ExampleCard
|
||||||
icon={<GitBranch className="w-4 h-4 text-primary" />}
|
icon={<GitBranch className="w-4 h-4 text-primary" />}
|
||||||
title="Replicate Flowchart"
|
title={dict.examples.replicateFlowchart}
|
||||||
description="Upload and replicate an existing flowchart"
|
description={dict.examples.replicateDescription}
|
||||||
onClick={handleReplicateFlowchart}
|
onClick={handleReplicateFlowchart}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ExampleCard
|
<ExampleCard
|
||||||
icon={<Palette className="w-4 h-4 text-primary" />}
|
icon={<Palette className="w-4 h-4 text-primary" />}
|
||||||
title="Creative Drawing"
|
title={dict.examples.creativeDrawing}
|
||||||
description="Draw something fun and creative"
|
description={dict.examples.creativeDescription}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setInput("Draw a cat for me")
|
setInput("Draw a cat for me")
|
||||||
setFiles([])
|
setFiles([])
|
||||||
@@ -172,7 +210,7 @@ export default function ExamplePanel({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-[11px] text-muted-foreground/60 text-center mt-4">
|
<p className="text-[11px] text-muted-foreground/60 text-center mt-4">
|
||||||
Examples are cached for instant response
|
{dict.examples.cachedNote}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip"
|
} from "@/components/ui/tooltip"
|
||||||
import { useDiagram } from "@/contexts/diagram-context"
|
import { useDiagram } from "@/contexts/diagram-context"
|
||||||
|
import { useDictionary } from "@/hooks/use-dictionary"
|
||||||
|
import { formatMessage } from "@/lib/i18n/utils"
|
||||||
import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
|
import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
|
||||||
import { FilePreviewList } from "./file-preview-list"
|
import { FilePreviewList } from "./file-preview-list"
|
||||||
|
|
||||||
@@ -58,6 +60,7 @@ interface ValidationResult {
|
|||||||
function validateFiles(
|
function validateFiles(
|
||||||
newFiles: File[],
|
newFiles: File[],
|
||||||
existingCount: number,
|
existingCount: number,
|
||||||
|
dict: any,
|
||||||
): ValidationResult {
|
): ValidationResult {
|
||||||
const errors: string[] = []
|
const errors: string[] = []
|
||||||
const validFiles: File[] = []
|
const validFiles: File[] = []
|
||||||
@@ -65,17 +68,23 @@ function validateFiles(
|
|||||||
const availableSlots = MAX_FILES - existingCount
|
const availableSlots = MAX_FILES - existingCount
|
||||||
|
|
||||||
if (availableSlots <= 0) {
|
if (availableSlots <= 0) {
|
||||||
errors.push(`Maximum ${MAX_FILES} files allowed`)
|
errors.push(formatMessage(dict.errors.maxFiles, { max: MAX_FILES }))
|
||||||
return { validFiles, errors }
|
return { validFiles, errors }
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const file of newFiles) {
|
for (const file of newFiles) {
|
||||||
if (validFiles.length >= availableSlots) {
|
if (validFiles.length >= availableSlots) {
|
||||||
errors.push(`Only ${availableSlots} more file(s) allowed`)
|
errors.push(
|
||||||
|
formatMessage(dict.errors.onlyMoreAllowed, {
|
||||||
|
slots: availableSlots,
|
||||||
|
}),
|
||||||
|
)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if (!isValidFileType(file)) {
|
if (!isValidFileType(file)) {
|
||||||
errors.push(`"${file.name}" is not a supported file type`)
|
errors.push(
|
||||||
|
formatMessage(dict.errors.unsupportedType, { name: file.name }),
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Only check size for images (PDFs/text files are extracted client-side, so file size doesn't matter)
|
// Only check size for images (PDFs/text files are extracted client-side, so file size doesn't matter)
|
||||||
@@ -83,7 +92,11 @@ function validateFiles(
|
|||||||
if (!isExtractedFile && file.size > MAX_IMAGE_SIZE) {
|
if (!isExtractedFile && file.size > MAX_IMAGE_SIZE) {
|
||||||
const maxSizeMB = MAX_IMAGE_SIZE / 1024 / 1024
|
const maxSizeMB = MAX_IMAGE_SIZE / 1024 / 1024
|
||||||
errors.push(
|
errors.push(
|
||||||
`"${file.name}" is ${formatFileSize(file.size)} (exceeds ${maxSizeMB}MB)`,
|
formatMessage(dict.errors.fileExceeds, {
|
||||||
|
name: file.name,
|
||||||
|
size: formatFileSize(file.size),
|
||||||
|
max: maxSizeMB,
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
validFiles.push(file)
|
validFiles.push(file)
|
||||||
@@ -93,7 +106,7 @@ function validateFiles(
|
|||||||
return { validFiles, errors }
|
return { validFiles, errors }
|
||||||
}
|
}
|
||||||
|
|
||||||
function showValidationErrors(errors: string[]) {
|
function showValidationErrors(errors: string[], dict: any) {
|
||||||
if (errors.length === 0) return
|
if (errors.length === 0) return
|
||||||
|
|
||||||
if (errors.length === 1) {
|
if (errors.length === 1) {
|
||||||
@@ -104,14 +117,20 @@ function showValidationErrors(errors: string[]) {
|
|||||||
showErrorToast(
|
showErrorToast(
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{errors.length} files rejected:
|
{formatMessage(dict.errors.filesRejected, {
|
||||||
|
count: errors.length,
|
||||||
|
})}
|
||||||
</span>
|
</span>
|
||||||
<ul className="text-muted-foreground text-xs list-disc list-inside">
|
<ul className="text-muted-foreground text-xs list-disc list-inside">
|
||||||
{errors.slice(0, 3).map((err) => (
|
{errors.slice(0, 3).map((err) => (
|
||||||
<li key={err}>{err}</li>
|
<li key={err}>{err}</li>
|
||||||
))}
|
))}
|
||||||
{errors.length > 3 && (
|
{errors.length > 3 && (
|
||||||
<li>...and {errors.length - 3} more</li>
|
<li>
|
||||||
|
{formatMessage(dict.errors.andMore, {
|
||||||
|
count: errors.length - 3,
|
||||||
|
})}
|
||||||
|
</li>
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>,
|
</div>,
|
||||||
@@ -155,13 +174,17 @@ export function ChatInput({
|
|||||||
minimalStyle = false,
|
minimalStyle = false,
|
||||||
onMinimalStyleChange = () => {},
|
onMinimalStyleChange = () => {},
|
||||||
}: ChatInputProps) {
|
}: ChatInputProps) {
|
||||||
const { diagramHistory, saveDiagramToFile } = useDiagram()
|
const dict = useDictionary()
|
||||||
|
const {
|
||||||
|
diagramHistory,
|
||||||
|
saveDiagramToFile,
|
||||||
|
showSaveDialog,
|
||||||
|
setShowSaveDialog,
|
||||||
|
} = useDiagram()
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
const [isDragging, setIsDragging] = useState(false)
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
const [showClearDialog, setShowClearDialog] = useState(false)
|
const [showClearDialog, setShowClearDialog] = useState(false)
|
||||||
const [showSaveDialog, setShowSaveDialog] = useState(false)
|
|
||||||
|
|
||||||
// Allow retry when there's an error (even if status is still "streaming" or "submitted")
|
// Allow retry when there's an error (even if status is still "streaming" or "submitted")
|
||||||
const isDisabled =
|
const isDisabled =
|
||||||
(status === "streaming" || status === "submitted") && !error
|
(status === "streaming" || status === "submitted") && !error
|
||||||
@@ -173,7 +196,6 @@ export function ChatInput({
|
|||||||
textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`
|
textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Handle programmatic input changes (e.g., setInput("") after form submission)
|
// Handle programmatic input changes (e.g., setInput("") after form submission)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
adjustTextareaHeight()
|
adjustTextareaHeight()
|
||||||
@@ -220,8 +242,9 @@ export function ChatInput({
|
|||||||
const { validFiles, errors } = validateFiles(
|
const { validFiles, errors } = validateFiles(
|
||||||
imageFiles,
|
imageFiles,
|
||||||
files.length,
|
files.length,
|
||||||
|
dict,
|
||||||
)
|
)
|
||||||
showValidationErrors(errors)
|
showValidationErrors(errors, dict)
|
||||||
if (validFiles.length > 0) {
|
if (validFiles.length > 0) {
|
||||||
onFileChange([...files, ...validFiles])
|
onFileChange([...files, ...validFiles])
|
||||||
}
|
}
|
||||||
@@ -230,12 +253,16 @@ export function ChatInput({
|
|||||||
|
|
||||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const newFiles = Array.from(e.target.files || [])
|
const newFiles = Array.from(e.target.files || [])
|
||||||
const { validFiles, errors } = validateFiles(newFiles, files.length)
|
const { validFiles, errors } = validateFiles(
|
||||||
showValidationErrors(errors)
|
newFiles,
|
||||||
|
files.length,
|
||||||
|
dict,
|
||||||
|
)
|
||||||
|
showValidationErrors(errors, dict)
|
||||||
if (validFiles.length > 0) {
|
if (validFiles.length > 0) {
|
||||||
onFileChange([...files, ...validFiles])
|
onFileChange([...files, ...validFiles])
|
||||||
}
|
}
|
||||||
// Reset input so same file can be selected again
|
|
||||||
if (fileInputRef.current) {
|
if (fileInputRef.current) {
|
||||||
fileInputRef.current.value = ""
|
fileInputRef.current.value = ""
|
||||||
}
|
}
|
||||||
@@ -279,8 +306,9 @@ export function ChatInput({
|
|||||||
const { validFiles, errors } = validateFiles(
|
const { validFiles, errors } = validateFiles(
|
||||||
supportedFiles,
|
supportedFiles,
|
||||||
files.length,
|
files.length,
|
||||||
|
dict,
|
||||||
)
|
)
|
||||||
showValidationErrors(errors)
|
showValidationErrors(errors, dict)
|
||||||
if (validFiles.length > 0) {
|
if (validFiles.length > 0) {
|
||||||
onFileChange([...files, ...validFiles])
|
onFileChange([...files, ...validFiles])
|
||||||
}
|
}
|
||||||
@@ -313,8 +341,6 @@ export function ChatInput({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Input container */}
|
|
||||||
<div className="relative rounded-2xl border border-border bg-background shadow-sm focus-within:ring-2 focus-within:ring-primary/20 focus-within:border-primary/50 transition-all duration-200">
|
<div className="relative rounded-2xl border border-border bg-background shadow-sm focus-within:ring-2 focus-within:ring-primary/20 focus-within:border-primary/50 transition-all duration-200">
|
||||||
<Textarea
|
<Textarea
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
@@ -322,22 +348,20 @@ export function ChatInput({
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onPaste={handlePaste}
|
onPaste={handlePaste}
|
||||||
placeholder="Describe your diagram or upload a file..."
|
placeholder={dict.chat.placeholder}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
aria-label="Chat input"
|
aria-label="Chat input"
|
||||||
className="min-h-[60px] max-h-[200px] resize-none border-0 bg-transparent px-4 py-3 text-sm focus-visible:ring-0 focus-visible:ring-offset-0 placeholder:text-muted-foreground/60"
|
className="min-h-[60px] max-h-[200px] resize-none border-0 bg-transparent px-4 py-3 text-sm focus-visible:ring-0 focus-visible:ring-offset-0 placeholder:text-muted-foreground/60"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Action bar */}
|
|
||||||
<div className="flex items-center justify-between px-3 py-2 border-t border-border/50">
|
<div className="flex items-center justify-between px-3 py-2 border-t border-border/50">
|
||||||
{/* Left actions */}
|
<div className="flex items-center gap-1 overflow-x-hidden">
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<ButtonWithTooltip
|
<ButtonWithTooltip
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setShowClearDialog(true)}
|
onClick={() => setShowClearDialog(true)}
|
||||||
tooltipContent="Clear conversation"
|
tooltipContent={dict.chat.clearConversation}
|
||||||
className="h-8 w-8 p-0 text-muted-foreground hover:text-destructive hover:bg-destructive/10"
|
className="h-8 w-8 p-0 text-muted-foreground hover:text-destructive hover:bg-destructive/10"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
@@ -371,25 +395,26 @@ export function ChatInput({
|
|||||||
: "text-muted-foreground"
|
: "text-muted-foreground"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{minimalStyle ? "Minimal" : "Styled"}
|
{minimalStyle
|
||||||
|
? dict.chat.minimalStyle
|
||||||
|
: dict.chat.styledMode}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="top">
|
<TooltipContent side="top">
|
||||||
Use minimal for faster generation (no colors)
|
{dict.chat.minimalTooltip}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right actions */}
|
<div className="flex items-center gap-1 overflow-hidden justify-end">
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<ButtonWithTooltip
|
<ButtonWithTooltip
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => onToggleHistory(true)}
|
onClick={() => onToggleHistory(true)}
|
||||||
disabled={isDisabled || diagramHistory.length === 0}
|
disabled={isDisabled || diagramHistory.length === 0}
|
||||||
tooltipContent="Diagram history"
|
tooltipContent={dict.chat.diagramHistory}
|
||||||
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
|
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
|
||||||
>
|
>
|
||||||
<History className="h-4 w-4" />
|
<History className="h-4 w-4" />
|
||||||
@@ -401,7 +426,7 @@ export function ChatInput({
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setShowSaveDialog(true)}
|
onClick={() => setShowSaveDialog(true)}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
tooltipContent="Save diagram"
|
tooltipContent={dict.chat.saveDiagram}
|
||||||
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
|
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
|
||||||
>
|
>
|
||||||
<Download className="h-4 w-4" />
|
<Download className="h-4 w-4" />
|
||||||
@@ -424,7 +449,7 @@ export function ChatInput({
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={triggerFileInput}
|
onClick={triggerFileInput}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
tooltipContent="Upload file (image, PDF, text)"
|
tooltipContent={dict.chat.uploadFile}
|
||||||
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
|
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
|
||||||
>
|
>
|
||||||
<ImageIcon className="h-4 w-4" />
|
<ImageIcon className="h-4 w-4" />
|
||||||
@@ -448,7 +473,7 @@ export function ChatInput({
|
|||||||
size="sm"
|
size="sm"
|
||||||
className="h-8 px-4 rounded-xl font-medium shadow-sm"
|
className="h-8 px-4 rounded-xl font-medium shadow-sm"
|
||||||
aria-label={
|
aria-label={
|
||||||
isDisabled ? "Sending..." : "Send message"
|
isDisabled ? dict.chat.sending : dict.chat.send
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{isDisabled ? (
|
{isDisabled ? (
|
||||||
@@ -456,7 +481,7 @@ export function ChatInput({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Send className="h-4 w-4 mr-1.5" />
|
<Send className="h-4 w-4 mr-1.5" />
|
||||||
Send
|
{dict.chat.send}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -10,9 +10,7 @@ import {
|
|||||||
Cpu,
|
Cpu,
|
||||||
FileCode,
|
FileCode,
|
||||||
FileText,
|
FileText,
|
||||||
Minus,
|
|
||||||
Pencil,
|
Pencil,
|
||||||
Plus,
|
|
||||||
RotateCcw,
|
RotateCcw,
|
||||||
ThumbsDown,
|
ThumbsDown,
|
||||||
ThumbsUp,
|
ThumbsUp,
|
||||||
@@ -30,6 +28,7 @@ import {
|
|||||||
} 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 {
|
import {
|
||||||
|
applyDiagramOperations,
|
||||||
convertToLegalXml,
|
convertToLegalXml,
|
||||||
isMxCellXmlComplete,
|
isMxCellXmlComplete,
|
||||||
replaceNodes,
|
replaceNodes,
|
||||||
@@ -38,9 +37,27 @@ import {
|
|||||||
import ExamplePanel from "./chat-example-panel"
|
import ExamplePanel from "./chat-example-panel"
|
||||||
import { CodeBlock } from "./code-block"
|
import { CodeBlock } from "./code-block"
|
||||||
|
|
||||||
interface EditPair {
|
interface DiagramOperation {
|
||||||
search: string
|
type: "update" | "add" | "delete"
|
||||||
replace: string
|
cell_id: string
|
||||||
|
new_xml?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to extract complete operations from streaming input
|
||||||
|
function getCompleteOperations(
|
||||||
|
operations: DiagramOperation[] | undefined,
|
||||||
|
): DiagramOperation[] {
|
||||||
|
if (!operations || !Array.isArray(operations)) return []
|
||||||
|
return operations.filter(
|
||||||
|
(op) =>
|
||||||
|
op &&
|
||||||
|
typeof op.type === "string" &&
|
||||||
|
["update", "add", "delete"].includes(op.type) &&
|
||||||
|
typeof op.cell_id === "string" &&
|
||||||
|
op.cell_id.length > 0 &&
|
||||||
|
// delete doesn't need new_xml, update/add do
|
||||||
|
(op.type === "delete" || typeof op.new_xml === "string"),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tool part interface for type safety
|
// Tool part interface for type safety
|
||||||
@@ -48,49 +65,44 @@ interface ToolPartLike {
|
|||||||
type: string
|
type: string
|
||||||
toolCallId: string
|
toolCallId: string
|
||||||
state?: string
|
state?: string
|
||||||
input?: { xml?: string; edits?: EditPair[] } & Record<string, unknown>
|
input?: {
|
||||||
|
xml?: string
|
||||||
|
operations?: DiagramOperation[]
|
||||||
|
} & Record<string, unknown>
|
||||||
output?: string
|
output?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditDiffDisplay({ edits }: { edits: EditPair[] }) {
|
function OperationsDisplay({ operations }: { operations: DiagramOperation[] }) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{edits.map((edit, index) => (
|
{operations.map((op, index) => (
|
||||||
<div
|
<div
|
||||||
key={`${(edit.search || "").slice(0, 50)}-${(edit.replace || "").slice(0, 50)}-${index}`}
|
key={`${op.type}-${op.cell_id}-${index}`}
|
||||||
className="rounded-lg border border-border/50 overflow-hidden bg-background/50"
|
className="rounded-lg border border-border/50 overflow-hidden bg-background/50"
|
||||||
>
|
>
|
||||||
<div className="px-3 py-1.5 bg-muted/40 border-b border-border/30 flex items-center gap-2">
|
<div className="px-3 py-1.5 bg-muted/40 border-b border-border/30 flex items-center gap-2">
|
||||||
<span className="text-xs font-medium text-muted-foreground">
|
<span
|
||||||
Change {index + 1}
|
className={`text-[10px] font-medium uppercase tracking-wide ${
|
||||||
|
op.type === "delete"
|
||||||
|
? "text-red-600"
|
||||||
|
: op.type === "add"
|
||||||
|
? "text-green-600"
|
||||||
|
: "text-blue-600"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{op.type}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
cell_id: {op.cell_id}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="divide-y divide-border/30">
|
{op.new_xml && (
|
||||||
{/* Search (old) */}
|
|
||||||
<div className="px-3 py-2">
|
<div className="px-3 py-2">
|
||||||
<div className="flex items-center gap-1.5 mb-1.5">
|
<pre className="text-[11px] font-mono text-foreground/80 bg-muted/30 rounded px-2 py-1.5 overflow-x-auto whitespace-pre-wrap break-all">
|
||||||
<Minus className="w-3 h-3 text-red-500" />
|
{op.new_xml}
|
||||||
<span className="text-[10px] font-medium text-red-600 uppercase tracking-wide">
|
|
||||||
Remove
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<pre className="text-[11px] font-mono text-red-700 bg-red-50 rounded px-2 py-1.5 overflow-x-auto whitespace-pre-wrap break-all">
|
|
||||||
{edit.search}
|
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
{/* Replace (new) */}
|
)}
|
||||||
<div className="px-3 py-2">
|
|
||||||
<div className="flex items-center gap-1.5 mb-1.5">
|
|
||||||
<Plus className="w-3 h-3 text-green-500" />
|
|
||||||
<span className="text-[10px] font-medium text-green-600 uppercase tracking-wide">
|
|
||||||
Add
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<pre className="text-[11px] font-mono text-green-700 bg-green-50 rounded px-2 py-1.5 overflow-x-auto whitespace-pre-wrap break-all">
|
|
||||||
{edit.replace}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -173,6 +185,7 @@ interface ChatMessageDisplayProps {
|
|||||||
setInput: (input: string) => void
|
setInput: (input: string) => void
|
||||||
setFiles: (files: File[]) => void
|
setFiles: (files: File[]) => void
|
||||||
processedToolCallsRef: MutableRefObject<Set<string>>
|
processedToolCallsRef: MutableRefObject<Set<string>>
|
||||||
|
editDiagramOriginalXmlRef: MutableRefObject<Map<string, string>>
|
||||||
sessionId?: string
|
sessionId?: string
|
||||||
onRegenerate?: (messageIndex: number) => void
|
onRegenerate?: (messageIndex: number) => void
|
||||||
onEditMessage?: (messageIndex: number, newText: string) => void
|
onEditMessage?: (messageIndex: number, newText: string) => void
|
||||||
@@ -184,6 +197,7 @@ export function ChatMessageDisplay({
|
|||||||
setInput,
|
setInput,
|
||||||
setFiles,
|
setFiles,
|
||||||
processedToolCallsRef,
|
processedToolCallsRef,
|
||||||
|
editDiagramOriginalXmlRef,
|
||||||
sessionId,
|
sessionId,
|
||||||
onRegenerate,
|
onRegenerate,
|
||||||
onEditMessage,
|
onEditMessage,
|
||||||
@@ -201,6 +215,14 @@ export function ChatMessageDisplay({
|
|||||||
null,
|
null,
|
||||||
)
|
)
|
||||||
const STREAMING_DEBOUNCE_MS = 150 // Only update diagram every 150ms during streaming
|
const STREAMING_DEBOUNCE_MS = 150 // Only update diagram every 150ms during streaming
|
||||||
|
// Refs for edit_diagram streaming
|
||||||
|
const pendingEditRef = useRef<{
|
||||||
|
operations: DiagramOperation[]
|
||||||
|
toolCallId: string
|
||||||
|
} | null>(null)
|
||||||
|
const editDebounceTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
|
||||||
|
null,
|
||||||
|
)
|
||||||
const [expandedTools, setExpandedTools] = useState<Record<string, boolean>>(
|
const [expandedTools, setExpandedTools] = useState<Record<string, boolean>>(
|
||||||
{},
|
{},
|
||||||
)
|
)
|
||||||
@@ -292,15 +314,12 @@ export function ChatMessageDisplay({
|
|||||||
|
|
||||||
const handleDisplayChart = useCallback(
|
const handleDisplayChart = useCallback(
|
||||||
(xml: string, showToast = false) => {
|
(xml: string, showToast = false) => {
|
||||||
console.time("perf:handleDisplayChart")
|
|
||||||
const currentXml = xml || ""
|
const currentXml = xml || ""
|
||||||
const convertedXml = convertToLegalXml(currentXml)
|
const convertedXml = convertToLegalXml(currentXml)
|
||||||
if (convertedXml !== previousXML.current) {
|
if (convertedXml !== previousXML.current) {
|
||||||
// Parse and validate XML BEFORE calling replaceNodes
|
// Parse and validate XML BEFORE calling replaceNodes
|
||||||
console.time("perf:DOMParser")
|
|
||||||
const parser = new DOMParser()
|
const parser = new DOMParser()
|
||||||
const testDoc = parser.parseFromString(convertedXml, "text/xml")
|
const testDoc = parser.parseFromString(convertedXml, "text/xml")
|
||||||
console.timeEnd("perf:DOMParser")
|
|
||||||
const parseError = testDoc.querySelector("parsererror")
|
const parseError = testDoc.querySelector("parsererror")
|
||||||
|
|
||||||
if (parseError) {
|
if (parseError) {
|
||||||
@@ -316,7 +335,6 @@ export function ChatMessageDisplay({
|
|||||||
"AI generated invalid diagram XML. Please try regenerating.",
|
"AI generated invalid diagram XML. Please try regenerating.",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
console.timeEnd("perf:handleDisplayChart")
|
|
||||||
return // Skip this update
|
return // Skip this update
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,14 +344,10 @@ export function ChatMessageDisplay({
|
|||||||
const baseXML =
|
const baseXML =
|
||||||
chartXML ||
|
chartXML ||
|
||||||
`<mxfile><diagram name="Page-1" id="page-1"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel></diagram></mxfile>`
|
`<mxfile><diagram name="Page-1" id="page-1"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel></diagram></mxfile>`
|
||||||
console.time("perf:replaceNodes")
|
|
||||||
const replacedXML = replaceNodes(baseXML, convertedXml)
|
const replacedXML = replaceNodes(baseXML, convertedXml)
|
||||||
console.timeEnd("perf:replaceNodes")
|
|
||||||
|
|
||||||
// Validate and auto-fix the XML
|
// Validate and auto-fix the XML
|
||||||
console.time("perf:validateAndFixXml")
|
|
||||||
const validation = validateAndFixXml(replacedXML)
|
const validation = validateAndFixXml(replacedXML)
|
||||||
console.timeEnd("perf:validateAndFixXml")
|
|
||||||
if (validation.valid) {
|
if (validation.valid) {
|
||||||
previousXML.current = convertedXml
|
previousXML.current = convertedXml
|
||||||
// Use fixed XML if available, otherwise use original
|
// Use fixed XML if available, otherwise use original
|
||||||
@@ -370,9 +384,6 @@ export function ChatMessageDisplay({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.timeEnd("perf:handleDisplayChart")
|
|
||||||
} else {
|
|
||||||
console.timeEnd("perf:handleDisplayChart")
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[chartXML, onDisplayChart],
|
[chartXML, onDisplayChart],
|
||||||
@@ -391,11 +402,6 @@ export function ChatMessageDisplay({
|
|||||||
}, [editingMessageId])
|
}, [editingMessageId])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.time("perf:message-display-useEffect")
|
|
||||||
let processedCount = 0
|
|
||||||
let skippedCount = 0
|
|
||||||
let debouncedCount = 0
|
|
||||||
|
|
||||||
// Only process the last message for streaming performance
|
// Only process the last message for streaming performance
|
||||||
// Previous messages are already processed and won't change
|
// Previous messages are already processed and won't change
|
||||||
const messagesToProcess =
|
const messagesToProcess =
|
||||||
@@ -425,7 +431,6 @@ export function ChatMessageDisplay({
|
|||||||
const lastXml =
|
const lastXml =
|
||||||
lastProcessedXmlRef.current.get(toolCallId)
|
lastProcessedXmlRef.current.get(toolCallId)
|
||||||
if (lastXml === xml) {
|
if (lastXml === xml) {
|
||||||
skippedCount++
|
|
||||||
return // Skip redundant processing
|
return // Skip redundant processing
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -445,9 +450,6 @@ export function ChatMessageDisplay({
|
|||||||
debounceTimeoutRef.current = null
|
debounceTimeoutRef.current = null
|
||||||
pendingXmlRef.current = null
|
pendingXmlRef.current = null
|
||||||
if (pendingXml) {
|
if (pendingXml) {
|
||||||
console.log(
|
|
||||||
"perf:debounced-handleDisplayChart executing",
|
|
||||||
)
|
|
||||||
handleDisplayChart(
|
handleDisplayChart(
|
||||||
pendingXml,
|
pendingXml,
|
||||||
false,
|
false,
|
||||||
@@ -461,7 +463,6 @@ export function ChatMessageDisplay({
|
|||||||
STREAMING_DEBOUNCE_MS,
|
STREAMING_DEBOUNCE_MS,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
debouncedCount++
|
|
||||||
} else if (
|
} else if (
|
||||||
state === "output-available" &&
|
state === "output-available" &&
|
||||||
!processedToolCalls.current.has(toolCallId)
|
!processedToolCalls.current.has(toolCallId)
|
||||||
@@ -477,17 +478,129 @@ export function ChatMessageDisplay({
|
|||||||
processedToolCalls.current.add(toolCallId)
|
processedToolCalls.current.add(toolCallId)
|
||||||
// Clean up the ref entry - tool is complete, no longer needed
|
// Clean up the ref entry - tool is complete, no longer needed
|
||||||
lastProcessedXmlRef.current.delete(toolCallId)
|
lastProcessedXmlRef.current.delete(toolCallId)
|
||||||
processedCount++
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle edit_diagram streaming - apply operations incrementally for preview
|
||||||
|
// Uses shared editDiagramOriginalXmlRef to coordinate with tool handler
|
||||||
|
if (
|
||||||
|
part.type === "tool-edit_diagram" &&
|
||||||
|
input?.operations
|
||||||
|
) {
|
||||||
|
const completeOps = getCompleteOperations(
|
||||||
|
input.operations as DiagramOperation[],
|
||||||
|
)
|
||||||
|
|
||||||
|
if (completeOps.length === 0) return
|
||||||
|
|
||||||
|
// Capture original XML when streaming starts (store in shared ref)
|
||||||
|
if (
|
||||||
|
!editDiagramOriginalXmlRef.current.has(
|
||||||
|
toolCallId,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
if (!chartXML) {
|
||||||
|
console.warn(
|
||||||
|
"[edit_diagram streaming] No chart XML available",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
editDiagramOriginalXmlRef.current.set(
|
||||||
|
toolCallId,
|
||||||
|
chartXML,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalXml =
|
||||||
|
editDiagramOriginalXmlRef.current.get(
|
||||||
|
toolCallId,
|
||||||
|
)
|
||||||
|
if (!originalXml) return
|
||||||
|
|
||||||
|
// Skip if no change from last processed state
|
||||||
|
const lastCount = lastProcessedXmlRef.current.get(
|
||||||
|
toolCallId + "-opCount",
|
||||||
|
)
|
||||||
|
if (lastCount === String(completeOps.length)) return
|
||||||
|
|
||||||
|
if (
|
||||||
|
state === "input-streaming" ||
|
||||||
|
state === "input-available"
|
||||||
|
) {
|
||||||
|
// Queue the operations for debounced processing
|
||||||
|
pendingEditRef.current = {
|
||||||
|
operations: completeOps,
|
||||||
|
toolCallId,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!editDebounceTimeoutRef.current) {
|
||||||
|
editDebounceTimeoutRef.current = setTimeout(
|
||||||
|
() => {
|
||||||
|
const pending =
|
||||||
|
pendingEditRef.current
|
||||||
|
editDebounceTimeoutRef.current =
|
||||||
|
null
|
||||||
|
pendingEditRef.current = null
|
||||||
|
|
||||||
|
if (pending) {
|
||||||
|
const origXml =
|
||||||
|
editDiagramOriginalXmlRef.current.get(
|
||||||
|
pending.toolCallId,
|
||||||
|
)
|
||||||
|
if (!origXml) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
result: editedXml,
|
||||||
|
} = applyDiagramOperations(
|
||||||
|
origXml,
|
||||||
|
pending.operations,
|
||||||
|
)
|
||||||
|
handleDisplayChart(
|
||||||
|
editedXml,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
lastProcessedXmlRef.current.set(
|
||||||
|
pending.toolCallId +
|
||||||
|
"-opCount",
|
||||||
|
String(
|
||||||
|
pending.operations
|
||||||
|
.length,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(
|
||||||
|
`[edit_diagram streaming] Operation failed:`,
|
||||||
|
e instanceof Error
|
||||||
|
? e.message
|
||||||
|
: e,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
STREAMING_DEBOUNCE_MS,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
state === "output-available" &&
|
||||||
|
!processedToolCalls.current.has(toolCallId)
|
||||||
|
) {
|
||||||
|
// Final state - cleanup streaming refs (tool handler does final application)
|
||||||
|
if (editDebounceTimeoutRef.current) {
|
||||||
|
clearTimeout(editDebounceTimeoutRef.current)
|
||||||
|
editDebounceTimeoutRef.current = null
|
||||||
|
}
|
||||||
|
lastProcessedXmlRef.current.delete(
|
||||||
|
toolCallId + "-opCount",
|
||||||
|
)
|
||||||
|
processedToolCalls.current.add(toolCallId)
|
||||||
|
// Note: Don't delete editDiagramOriginalXmlRef here - tool handler needs it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
console.log(
|
|
||||||
`perf:message-display-useEffect processed=${processedCount} skipped=${skippedCount} debounced=${debouncedCount}`,
|
|
||||||
)
|
|
||||||
console.timeEnd("perf:message-display-useEffect")
|
|
||||||
|
|
||||||
// Cleanup: clear any pending debounce timeout on unmount
|
// Cleanup: clear any pending debounce timeout on unmount
|
||||||
return () => {
|
return () => {
|
||||||
@@ -495,8 +608,12 @@ export function ChatMessageDisplay({
|
|||||||
clearTimeout(debounceTimeoutRef.current)
|
clearTimeout(debounceTimeoutRef.current)
|
||||||
debounceTimeoutRef.current = null
|
debounceTimeoutRef.current = null
|
||||||
}
|
}
|
||||||
|
if (editDebounceTimeoutRef.current) {
|
||||||
|
clearTimeout(editDebounceTimeoutRef.current)
|
||||||
|
editDebounceTimeoutRef.current = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [messages, handleDisplayChart])
|
}, [messages, handleDisplayChart, chartXML])
|
||||||
|
|
||||||
const renderToolPart = (part: ToolPartLike) => {
|
const renderToolPart = (part: ToolPartLike) => {
|
||||||
const callId = part.toolCallId
|
const callId = part.toolCallId
|
||||||
@@ -517,6 +634,8 @@ export function ChatMessageDisplay({
|
|||||||
return "Generate Diagram"
|
return "Generate Diagram"
|
||||||
case "edit_diagram":
|
case "edit_diagram":
|
||||||
return "Edit Diagram"
|
return "Edit Diagram"
|
||||||
|
case "get_shape_library":
|
||||||
|
return "Get Shape Library"
|
||||||
default:
|
default:
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
@@ -582,9 +701,9 @@ export function ChatMessageDisplay({
|
|||||||
{typeof input === "object" && input.xml ? (
|
{typeof input === "object" && input.xml ? (
|
||||||
<CodeBlock code={input.xml} language="xml" />
|
<CodeBlock code={input.xml} language="xml" />
|
||||||
) : typeof input === "object" &&
|
) : typeof input === "object" &&
|
||||||
input.edits &&
|
input.operations &&
|
||||||
Array.isArray(input.edits) ? (
|
Array.isArray(input.operations) ? (
|
||||||
<EditDiffDisplay edits={input.edits} />
|
<OperationsDisplay operations={input.operations} />
|
||||||
) : typeof input === "object" &&
|
) : typeof input === "object" &&
|
||||||
Object.keys(input).length > 0 ? (
|
Object.keys(input).length > 0 ? (
|
||||||
<CodeBlock
|
<CodeBlock
|
||||||
@@ -611,6 +730,25 @@ export function ChatMessageDisplay({
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})()}
|
})()}
|
||||||
|
{/* Show get_shape_library output on success */}
|
||||||
|
{output &&
|
||||||
|
toolName === "get_shape_library" &&
|
||||||
|
state === "output-available" &&
|
||||||
|
isExpanded && (
|
||||||
|
<div className="px-4 py-3 border-t border-border/40">
|
||||||
|
<div className="text-xs text-muted-foreground mb-2">
|
||||||
|
Library loaded (
|
||||||
|
{typeof output === "string" ? output.length : 0}{" "}
|
||||||
|
chars)
|
||||||
|
</div>
|
||||||
|
<pre className="text-xs bg-muted/50 p-2 rounded-md overflow-auto max-h-32 whitespace-pre-wrap">
|
||||||
|
{typeof output === "string"
|
||||||
|
? output.substring(0, 800) +
|
||||||
|
(output.length > 800 ? "\n..." : "")
|
||||||
|
: String(output)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { ChatInput } from "@/components/chat-input"
|
|||||||
import { ResetWarningModal } from "@/components/reset-warning-modal"
|
import { ResetWarningModal } from "@/components/reset-warning-modal"
|
||||||
import { SettingsDialog } from "@/components/settings-dialog"
|
import { SettingsDialog } from "@/components/settings-dialog"
|
||||||
import { useDiagram } from "@/contexts/diagram-context"
|
import { useDiagram } from "@/contexts/diagram-context"
|
||||||
|
import { useDictionary } from "@/hooks/use-dictionary"
|
||||||
import { getAIConfig } from "@/lib/ai-config"
|
import { getAIConfig } from "@/lib/ai-config"
|
||||||
import { findCachedResponse } from "@/lib/cached-responses"
|
import { findCachedResponse } from "@/lib/cached-responses"
|
||||||
import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
|
import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
|
||||||
@@ -28,6 +29,7 @@ import { type FileData, useFileProcessor } from "@/lib/use-file-processor"
|
|||||||
import { useQuotaManager } from "@/lib/use-quota-manager"
|
import { useQuotaManager } from "@/lib/use-quota-manager"
|
||||||
import { formatXML, isMxCellXmlComplete, wrapWithMxFile } from "@/lib/utils"
|
import { formatXML, isMxCellXmlComplete, wrapWithMxFile } from "@/lib/utils"
|
||||||
import { ChatMessageDisplay } from "./chat-message-display"
|
import { ChatMessageDisplay } from "./chat-message-display"
|
||||||
|
import LanguageToggle from "./language-toggle"
|
||||||
|
|
||||||
// localStorage keys for persistence
|
// localStorage keys for persistence
|
||||||
const STORAGE_MESSAGES_KEY = "next-ai-draw-io-messages"
|
const STORAGE_MESSAGES_KEY = "next-ai-draw-io-messages"
|
||||||
@@ -35,6 +37,9 @@ const STORAGE_XML_SNAPSHOTS_KEY = "next-ai-draw-io-xml-snapshots"
|
|||||||
const STORAGE_SESSION_ID_KEY = "next-ai-draw-io-session-id"
|
const STORAGE_SESSION_ID_KEY = "next-ai-draw-io-session-id"
|
||||||
export const STORAGE_DIAGRAM_XML_KEY = "next-ai-draw-io-diagram-xml"
|
export const STORAGE_DIAGRAM_XML_KEY = "next-ai-draw-io-diagram-xml"
|
||||||
|
|
||||||
|
// sessionStorage keys
|
||||||
|
const SESSION_STORAGE_INPUT_KEY = "next-ai-draw-io-input"
|
||||||
|
|
||||||
// Type for message parts (tool calls and their states)
|
// Type for message parts (tool calls and their states)
|
||||||
interface MessagePart {
|
interface MessagePart {
|
||||||
type: string
|
type: string
|
||||||
@@ -106,9 +111,10 @@ export default function ChatPanel({
|
|||||||
resolverRef,
|
resolverRef,
|
||||||
chartXML,
|
chartXML,
|
||||||
clearDiagram,
|
clearDiagram,
|
||||||
isDrawioReady,
|
|
||||||
} = useDiagram()
|
} = useDiagram()
|
||||||
|
|
||||||
|
const dict = useDictionary()
|
||||||
|
|
||||||
const onFetchChart = (saveToHistory = true) => {
|
const onFetchChart = (saveToHistory = true) => {
|
||||||
return Promise.race([
|
return Promise.race([
|
||||||
new Promise<string>((resolve) => {
|
new Promise<string>((resolve) => {
|
||||||
@@ -148,6 +154,14 @@ export default function ChatPanel({
|
|||||||
const [showNewChatDialog, setShowNewChatDialog] = useState(false)
|
const [showNewChatDialog, setShowNewChatDialog] = useState(false)
|
||||||
const [minimalStyle, setMinimalStyle] = useState(false)
|
const [minimalStyle, setMinimalStyle] = useState(false)
|
||||||
|
|
||||||
|
// Restore input from sessionStorage on mount (when ChatPanel remounts due to key change)
|
||||||
|
useEffect(() => {
|
||||||
|
const savedInput = sessionStorage.getItem(SESSION_STORAGE_INPUT_KEY)
|
||||||
|
if (savedInput) {
|
||||||
|
setInput(savedInput)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
// Check config on mount
|
// Check config on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch("/api/config")
|
fetch("/api/config")
|
||||||
@@ -202,13 +216,14 @@ export default function ChatPanel({
|
|||||||
// Persist processed tool call IDs so collapsing the chat doesn't replay old tool outputs
|
// Persist processed tool call IDs so collapsing the chat doesn't replay old tool outputs
|
||||||
const processedToolCallsRef = useRef<Set<string>>(new Set())
|
const processedToolCallsRef = useRef<Set<string>>(new Set())
|
||||||
|
|
||||||
|
// Store original XML for edit_diagram streaming - shared between streaming preview and tool handler
|
||||||
|
// Key: toolCallId, Value: original XML before any operations applied
|
||||||
|
const editDiagramOriginalXmlRef = useRef<Map<string, string>>(new Map())
|
||||||
|
|
||||||
// Debounce timeout for localStorage writes (prevents blocking during streaming)
|
// Debounce timeout for localStorage writes (prevents blocking during streaming)
|
||||||
const localStorageDebounceRef = useRef<ReturnType<
|
const localStorageDebounceRef = useRef<ReturnType<
|
||||||
typeof setTimeout
|
typeof setTimeout
|
||||||
> | null>(null)
|
> | null>(null)
|
||||||
const xmlStorageDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(
|
|
||||||
null,
|
|
||||||
)
|
|
||||||
const LOCAL_STORAGE_DEBOUNCE_MS = 1000 // Save at most once per second
|
const LOCAL_STORAGE_DEBOUNCE_MS = 1000 // Save at most once per second
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -323,23 +338,68 @@ ${finalXml}
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (toolCall.toolName === "edit_diagram") {
|
} else if (toolCall.toolName === "edit_diagram") {
|
||||||
const { edits } = toolCall.input as {
|
const { operations } = toolCall.input as {
|
||||||
edits: Array<{ search: string; replace: string }>
|
operations: Array<{
|
||||||
|
type: "update" | "add" | "delete"
|
||||||
|
cell_id: string
|
||||||
|
new_xml?: string
|
||||||
|
}>
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentXml = ""
|
let currentXml = ""
|
||||||
try {
|
try {
|
||||||
// Use chartXML from ref directly - more reliable than export
|
// Use the original XML captured during streaming (shared with chat-message-display)
|
||||||
const cachedXML = chartXMLRef.current
|
// This ensures we apply operations to the same base XML that streaming used
|
||||||
if (cachedXML) {
|
const originalXml = editDiagramOriginalXmlRef.current.get(
|
||||||
currentXml = cachedXML
|
toolCall.toolCallId,
|
||||||
|
)
|
||||||
|
if (originalXml) {
|
||||||
|
currentXml = originalXml
|
||||||
} else {
|
} else {
|
||||||
// Fallback to export only if no cached XML
|
// Fallback: use chartXML from ref if streaming didn't capture original
|
||||||
currentXml = await onFetchChart(false)
|
const cachedXML = chartXMLRef.current
|
||||||
|
if (cachedXML) {
|
||||||
|
currentXml = cachedXML
|
||||||
|
} else {
|
||||||
|
// Last resort: export from iframe
|
||||||
|
currentXml = await onFetchChart(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { replaceXMLParts } = await import("@/lib/utils")
|
const { applyDiagramOperations } = await import(
|
||||||
const editedXml = replaceXMLParts(currentXml, edits)
|
"@/lib/utils"
|
||||||
|
)
|
||||||
|
const { result: editedXml, errors } =
|
||||||
|
applyDiagramOperations(currentXml, operations)
|
||||||
|
|
||||||
|
// Check for operation errors
|
||||||
|
if (errors.length > 0) {
|
||||||
|
const errorMessages = errors
|
||||||
|
.map(
|
||||||
|
(e) =>
|
||||||
|
`- ${e.type} on cell_id="${e.cellId}": ${e.message}`,
|
||||||
|
)
|
||||||
|
.join("\n")
|
||||||
|
|
||||||
|
addToolOutput({
|
||||||
|
tool: "edit_diagram",
|
||||||
|
toolCallId: toolCall.toolCallId,
|
||||||
|
state: "output-error",
|
||||||
|
errorText: `Some operations failed:\n${errorMessages}
|
||||||
|
|
||||||
|
Current diagram XML:
|
||||||
|
\`\`\`xml
|
||||||
|
${currentXml}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Please check the cell IDs and retry.`,
|
||||||
|
})
|
||||||
|
// Clean up the shared original XML ref
|
||||||
|
editDiagramOriginalXmlRef.current.delete(
|
||||||
|
toolCall.toolCallId,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// loadDiagram validates and returns error if invalid
|
// loadDiagram validates and returns error if invalid
|
||||||
const validationError = onDisplayChart(editedXml)
|
const validationError = onDisplayChart(editedXml)
|
||||||
@@ -359,23 +419,30 @@ Current diagram XML:
|
|||||||
${currentXml}
|
${currentXml}
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
Please fix the edit to avoid structural issues (e.g., duplicate IDs, invalid references).`,
|
Please fix the operations to avoid structural issues.`,
|
||||||
})
|
})
|
||||||
|
// Clean up the shared original XML ref
|
||||||
|
editDiagramOriginalXmlRef.current.delete(
|
||||||
|
toolCall.toolCallId,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
onExport()
|
onExport()
|
||||||
addToolOutput({
|
addToolOutput({
|
||||||
tool: "edit_diagram",
|
tool: "edit_diagram",
|
||||||
toolCallId: toolCall.toolCallId,
|
toolCallId: toolCall.toolCallId,
|
||||||
output: `Successfully applied ${edits.length} edit(s) to the diagram.`,
|
output: `Successfully applied ${operations.length} operation(s) to the diagram.`,
|
||||||
})
|
})
|
||||||
|
// Clean up the shared original XML ref
|
||||||
|
editDiagramOriginalXmlRef.current.delete(
|
||||||
|
toolCall.toolCallId,
|
||||||
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[edit_diagram] Failed:", error)
|
console.error("[edit_diagram] Failed:", error)
|
||||||
|
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
error instanceof Error ? error.message : String(error)
|
error instanceof Error ? error.message : String(error)
|
||||||
|
|
||||||
// Use addToolOutput with state: 'output-error' for proper error signaling
|
|
||||||
addToolOutput({
|
addToolOutput({
|
||||||
tool: "edit_diagram",
|
tool: "edit_diagram",
|
||||||
toolCallId: toolCall.toolCallId,
|
toolCallId: toolCall.toolCallId,
|
||||||
@@ -387,8 +454,12 @@ Current diagram XML:
|
|||||||
${currentXml || "No XML available"}
|
${currentXml || "No XML available"}
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
Please retry with an adjusted search pattern or use display_diagram if retries are exhausted.`,
|
Please check cell IDs and retry, or use display_diagram to regenerate.`,
|
||||||
})
|
})
|
||||||
|
// Clean up the shared original XML ref even on error
|
||||||
|
editDiagramOriginalXmlRef.current.delete(
|
||||||
|
toolCall.toolCallId,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else if (toolCall.toolName === "append_diagram") {
|
} else if (toolCall.toolName === "append_diagram") {
|
||||||
const { xml } = toolCall.input as { xml: string }
|
const { xml } = toolCall.input as { xml: string }
|
||||||
@@ -477,6 +548,32 @@ Continue from EXACTLY where you stopped.`,
|
|||||||
// Silence access code error in console since it's handled by UI
|
// Silence access code error in console since it's handled by UI
|
||||||
if (!error.message.includes("Invalid or missing access code")) {
|
if (!error.message.includes("Invalid or missing access code")) {
|
||||||
console.error("Chat error:", error)
|
console.error("Chat error:", error)
|
||||||
|
// Debug: Log messages structure when error occurs
|
||||||
|
console.log("[onError] messages count:", messages.length)
|
||||||
|
messages.forEach((msg, idx) => {
|
||||||
|
console.log(`[onError] Message ${idx}:`, {
|
||||||
|
role: msg.role,
|
||||||
|
partsCount: msg.parts?.length,
|
||||||
|
})
|
||||||
|
if (msg.parts) {
|
||||||
|
msg.parts.forEach((part: any, partIdx: number) => {
|
||||||
|
console.log(
|
||||||
|
`[onError] Part ${partIdx}:`,
|
||||||
|
JSON.stringify({
|
||||||
|
type: part.type,
|
||||||
|
toolName: part.toolName,
|
||||||
|
hasInput: !!part.input,
|
||||||
|
inputType: typeof part.input,
|
||||||
|
inputKeys:
|
||||||
|
part.input &&
|
||||||
|
typeof part.input === "object"
|
||||||
|
? Object.keys(part.input)
|
||||||
|
: null,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Translate technical errors into user-friendly messages
|
// Translate technical errors into user-friendly messages
|
||||||
@@ -639,47 +736,6 @@ Continue from EXACTLY where you stopped.`,
|
|||||||
}
|
}
|
||||||
}, [setMessages])
|
}, [setMessages])
|
||||||
|
|
||||||
// Restore diagram XML when DrawIO becomes ready
|
|
||||||
const hasDiagramRestoredRef = useRef(false)
|
|
||||||
const [canSaveDiagram, setCanSaveDiagram] = useState(false)
|
|
||||||
useEffect(() => {
|
|
||||||
// Reset restore flag when DrawIO is not ready (e.g., theme/UI change remounts it)
|
|
||||||
if (!isDrawioReady) {
|
|
||||||
hasDiagramRestoredRef.current = false
|
|
||||||
setCanSaveDiagram(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (hasDiagramRestoredRef.current) return
|
|
||||||
hasDiagramRestoredRef.current = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
const savedDiagramXml = localStorage.getItem(
|
|
||||||
STORAGE_DIAGRAM_XML_KEY,
|
|
||||||
)
|
|
||||||
console.log(
|
|
||||||
"[ChatPanel] Restoring diagram, has saved XML:",
|
|
||||||
!!savedDiagramXml,
|
|
||||||
)
|
|
||||||
if (savedDiagramXml) {
|
|
||||||
console.log(
|
|
||||||
"[ChatPanel] Loading saved diagram XML, length:",
|
|
||||||
savedDiagramXml.length,
|
|
||||||
)
|
|
||||||
// Skip validation for trusted saved diagrams
|
|
||||||
onDisplayChart(savedDiagramXml, true)
|
|
||||||
chartXMLRef.current = savedDiagramXml
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to restore diagram from localStorage:", error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow saving after restore is complete
|
|
||||||
setTimeout(() => {
|
|
||||||
console.log("[ChatPanel] Enabling diagram save")
|
|
||||||
setCanSaveDiagram(true)
|
|
||||||
}, 500)
|
|
||||||
}, [isDrawioReady, onDisplayChart])
|
|
||||||
|
|
||||||
// Save messages to localStorage whenever they change (debounced to prevent blocking during streaming)
|
// Save messages to localStorage whenever they change (debounced to prevent blocking during streaming)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hasRestoredRef.current) return
|
if (!hasRestoredRef.current) return
|
||||||
@@ -692,12 +748,10 @@ Continue from EXACTLY where you stopped.`,
|
|||||||
// Debounce: save after 1 second of no changes
|
// Debounce: save after 1 second of no changes
|
||||||
localStorageDebounceRef.current = setTimeout(() => {
|
localStorageDebounceRef.current = setTimeout(() => {
|
||||||
try {
|
try {
|
||||||
console.time("perf:localStorage-messages")
|
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
STORAGE_MESSAGES_KEY,
|
STORAGE_MESSAGES_KEY,
|
||||||
JSON.stringify(messages),
|
JSON.stringify(messages),
|
||||||
)
|
)
|
||||||
console.timeEnd("perf:localStorage-messages")
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to save messages to localStorage:", error)
|
console.error("Failed to save messages to localStorage:", error)
|
||||||
}
|
}
|
||||||
@@ -711,40 +765,14 @@ Continue from EXACTLY where you stopped.`,
|
|||||||
}
|
}
|
||||||
}, [messages])
|
}, [messages])
|
||||||
|
|
||||||
// Save diagram XML to localStorage whenever it changes (debounced)
|
|
||||||
useEffect(() => {
|
|
||||||
if (!canSaveDiagram) return
|
|
||||||
if (!chartXML || chartXML.length <= 300) return
|
|
||||||
|
|
||||||
// Clear any pending save
|
|
||||||
if (xmlStorageDebounceRef.current) {
|
|
||||||
clearTimeout(xmlStorageDebounceRef.current)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debounce: save after 1 second of no changes
|
|
||||||
xmlStorageDebounceRef.current = setTimeout(() => {
|
|
||||||
console.time("perf:localStorage-xml")
|
|
||||||
localStorage.setItem(STORAGE_DIAGRAM_XML_KEY, chartXML)
|
|
||||||
console.timeEnd("perf:localStorage-xml")
|
|
||||||
}, LOCAL_STORAGE_DEBOUNCE_MS)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (xmlStorageDebounceRef.current) {
|
|
||||||
clearTimeout(xmlStorageDebounceRef.current)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [chartXML, canSaveDiagram])
|
|
||||||
|
|
||||||
// Save XML snapshots to localStorage whenever they change
|
// Save XML snapshots to localStorage whenever they change
|
||||||
const saveXmlSnapshots = useCallback(() => {
|
const saveXmlSnapshots = useCallback(() => {
|
||||||
try {
|
try {
|
||||||
console.time("perf:localStorage-snapshots")
|
|
||||||
const snapshotsArray = Array.from(xmlSnapshotsRef.current.entries())
|
const snapshotsArray = Array.from(xmlSnapshotsRef.current.entries())
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
STORAGE_XML_SNAPSHOTS_KEY,
|
STORAGE_XML_SNAPSHOTS_KEY,
|
||||||
JSON.stringify(snapshotsArray),
|
JSON.stringify(snapshotsArray),
|
||||||
)
|
)
|
||||||
console.timeEnd("perf:localStorage-snapshots")
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error(
|
||||||
"Failed to save XML snapshots to localStorage:",
|
"Failed to save XML snapshots to localStorage:",
|
||||||
@@ -836,6 +864,7 @@ Continue from EXACTLY where you stopped.`,
|
|||||||
},
|
},
|
||||||
] as any)
|
] as any)
|
||||||
setInput("")
|
setInput("")
|
||||||
|
sessionStorage.removeItem(SESSION_STORAGE_INPUT_KEY)
|
||||||
setFiles([])
|
setFiles([])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -883,6 +912,7 @@ Continue from EXACTLY where you stopped.`,
|
|||||||
|
|
||||||
// Token count is tracked in onFinish with actual server usage
|
// Token count is tracked in onFinish with actual server usage
|
||||||
setInput("")
|
setInput("")
|
||||||
|
sessionStorage.removeItem(SESSION_STORAGE_INPUT_KEY)
|
||||||
setFiles([])
|
setFiles([])
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching chart data:", error)
|
console.error("Error fetching chart data:", error)
|
||||||
@@ -905,6 +935,7 @@ Continue from EXACTLY where you stopped.`,
|
|||||||
localStorage.removeItem(STORAGE_XML_SNAPSHOTS_KEY)
|
localStorage.removeItem(STORAGE_XML_SNAPSHOTS_KEY)
|
||||||
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)
|
||||||
toast.success("Started a fresh chat")
|
toast.success("Started a fresh chat")
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to clear localStorage:", error)
|
console.error("Failed to clear localStorage:", error)
|
||||||
@@ -919,9 +950,14 @@ Continue from EXACTLY where you stopped.`,
|
|||||||
const handleInputChange = (
|
const handleInputChange = (
|
||||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||||
) => {
|
) => {
|
||||||
|
saveInputToSessionStorage(e.target.value)
|
||||||
setInput(e.target.value)
|
setInput(e.target.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const saveInputToSessionStorage = (input: string) => {
|
||||||
|
sessionStorage.setItem(SESSION_STORAGE_INPUT_KEY, input)
|
||||||
|
}
|
||||||
|
|
||||||
// Helper functions for message actions (regenerate/edit)
|
// Helper functions for message actions (regenerate/edit)
|
||||||
// Extract previous XML snapshot before a given message index
|
// Extract previous XML snapshot before a given message index
|
||||||
const getPreviousXml = (beforeIndex: number): string => {
|
const getPreviousXml = (beforeIndex: number): string => {
|
||||||
@@ -1197,14 +1233,14 @@ Continue from EXACTLY where you stopped.`,
|
|||||||
className={`${isMobile ? "px-3 py-2" : "px-5 py-4"} border-b border-border/50`}
|
className={`${isMobile ? "px-3 py-2" : "px-5 py-4"} border-b border-border/50`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2 overflow-x-hidden">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Image
|
<Image
|
||||||
src="/favicon.ico"
|
src="/favicon.ico"
|
||||||
alt="Next AI Drawio"
|
alt="Next AI Drawio"
|
||||||
width={isMobile ? 24 : 28}
|
width={isMobile ? 24 : 28}
|
||||||
height={isMobile ? 24 : 28}
|
height={isMobile ? 24 : 28}
|
||||||
className="rounded"
|
className="rounded flex-shrink-0"
|
||||||
/>
|
/>
|
||||||
<h1
|
<h1
|
||||||
className={`${isMobile ? "text-sm" : "text-base"} font-semibold tracking-tight whitespace-nowrap`}
|
className={`${isMobile ? "text-sm" : "text-base"} font-semibold tracking-tight whitespace-nowrap`}
|
||||||
@@ -1239,9 +1275,9 @@ Continue from EXACTLY where you stopped.`,
|
|||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1 justify-end overflow-visible">
|
||||||
<ButtonWithTooltip
|
<ButtonWithTooltip
|
||||||
tooltipContent="Start fresh chat"
|
tooltipContent={dict.nav.newChat}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => setShowNewChatDialog(true)}
|
onClick={() => setShowNewChatDialog(true)}
|
||||||
@@ -1263,7 +1299,7 @@ Continue from EXACTLY where you stopped.`,
|
|||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
<ButtonWithTooltip
|
<ButtonWithTooltip
|
||||||
tooltipContent="Settings"
|
tooltipContent={dict.nav.settings}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => setShowSettingsDialog(true)}
|
onClick={() => setShowSettingsDialog(true)}
|
||||||
@@ -1273,17 +1309,20 @@ Continue from EXACTLY where you stopped.`,
|
|||||||
className={`${isMobile ? "h-4 w-4" : "h-5 w-5"} text-muted-foreground`}
|
className={`${isMobile ? "h-4 w-4" : "h-5 w-5"} text-muted-foreground`}
|
||||||
/>
|
/>
|
||||||
</ButtonWithTooltip>
|
</ButtonWithTooltip>
|
||||||
{!isMobile && (
|
<div className="hidden sm:flex items-center gap-2">
|
||||||
<ButtonWithTooltip
|
<LanguageToggle />
|
||||||
tooltipContent="Hide chat panel (Ctrl+B)"
|
{!isMobile && (
|
||||||
variant="ghost"
|
<ButtonWithTooltip
|
||||||
size="icon"
|
tooltipContent={dict.nav.hidePanel}
|
||||||
onClick={onToggleVisibility}
|
variant="ghost"
|
||||||
className="hover:bg-accent"
|
size="icon"
|
||||||
>
|
className="hover:bg-accent"
|
||||||
<PanelRightClose className="h-5 w-5 text-muted-foreground" />
|
onClick={onToggleVisibility}
|
||||||
</ButtonWithTooltip>
|
>
|
||||||
)}
|
<PanelRightClose className="h-5 w-5 text-muted-foreground" />
|
||||||
|
</ButtonWithTooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -1295,6 +1334,7 @@ Continue from EXACTLY where you stopped.`,
|
|||||||
setInput={setInput}
|
setInput={setInput}
|
||||||
setFiles={handleFileChange}
|
setFiles={handleFileChange}
|
||||||
processedToolCallsRef={processedToolCallsRef}
|
processedToolCallsRef={processedToolCallsRef}
|
||||||
|
editDiagramOriginalXmlRef={editDiagramOriginalXmlRef}
|
||||||
sessionId={sessionId}
|
sessionId={sessionId}
|
||||||
onRegenerate={handleRegenerate}
|
onRegenerate={handleRegenerate}
|
||||||
status={status}
|
status={status}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { FileCode, FileText, Loader2, X } from "lucide-react"
|
import { FileCode, FileText, Loader2, X } from "lucide-react"
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
import { useEffect, useRef, useState } from "react"
|
import { useEffect, useRef, useState } from "react"
|
||||||
|
import { useDictionary } from "@/hooks/use-dictionary"
|
||||||
import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
|
import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
|
||||||
|
|
||||||
function formatCharCount(count: number): string {
|
function formatCharCount(count: number): string {
|
||||||
@@ -26,10 +27,10 @@ export function FilePreviewList({
|
|||||||
onRemoveFile,
|
onRemoveFile,
|
||||||
pdfData = new Map(),
|
pdfData = new Map(),
|
||||||
}: FilePreviewListProps) {
|
}: FilePreviewListProps) {
|
||||||
|
const dict = useDictionary()
|
||||||
const [selectedImage, setSelectedImage] = useState<string | null>(null)
|
const [selectedImage, setSelectedImage] = useState<string | null>(null)
|
||||||
const [imageUrls, setImageUrls] = useState<Map<File, string>>(new Map())
|
const [imageUrls, setImageUrls] = useState<Map<File, string>>(new Map())
|
||||||
const imageUrlsRef = useRef<Map<File, string>>(new Map())
|
const imageUrlsRef = useRef<Map<File, string>>(new Map())
|
||||||
|
|
||||||
// Create and cleanup object URLs when files change
|
// Create and cleanup object URLs when files change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentUrls = imageUrlsRef.current
|
const currentUrls = imageUrlsRef.current
|
||||||
@@ -46,7 +47,6 @@ export function FilePreviewList({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Revoke URLs for files that are no longer in the list
|
// Revoke URLs for files that are no longer in the list
|
||||||
currentUrls.forEach((url, file) => {
|
currentUrls.forEach((url, file) => {
|
||||||
if (!newUrls.has(file)) {
|
if (!newUrls.has(file)) {
|
||||||
@@ -57,7 +57,6 @@ export function FilePreviewList({
|
|||||||
imageUrlsRef.current = newUrls
|
imageUrlsRef.current = newUrls
|
||||||
setImageUrls(newUrls)
|
setImageUrls(newUrls)
|
||||||
}, [files])
|
}, [files])
|
||||||
|
|
||||||
// Cleanup all URLs on unmount only
|
// Cleanup all URLs on unmount only
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@@ -68,7 +67,6 @@ export function FilePreviewList({
|
|||||||
imageUrlsRef.current = new Map()
|
imageUrlsRef.current = new Map()
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Clear selected image if its URL was revoked
|
// Clear selected image if its URL was revoked
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
@@ -126,14 +124,14 @@ export function FilePreviewList({
|
|||||||
</span>
|
</span>
|
||||||
{pdfInfo?.isExtracting ? (
|
{pdfInfo?.isExtracting ? (
|
||||||
<span className="text-[10px] text-muted-foreground">
|
<span className="text-[10px] text-muted-foreground">
|
||||||
Reading...
|
{dict.file.reading}
|
||||||
</span>
|
</span>
|
||||||
) : pdfInfo?.charCount ? (
|
) : pdfInfo?.charCount ? (
|
||||||
<span className="text-[10px] text-green-600 font-medium">
|
<span className="text-[10px] text-green-600 font-medium">
|
||||||
{formatCharCount(
|
{formatCharCount(
|
||||||
pdfInfo.charCount,
|
pdfInfo.charCount,
|
||||||
)}{" "}
|
)}{" "}
|
||||||
chars
|
{dict.file.chars}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
@@ -147,7 +145,7 @@ export function FilePreviewList({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => onRemoveFile(file)}
|
onClick={() => onRemoveFile(file)}
|
||||||
className="absolute -top-2 -right-2 bg-destructive rounded-full p-1 opacity-0 group-hover:opacity-100 transition-opacity"
|
className="absolute -top-2 -right-2 bg-destructive rounded-full p-1 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
aria-label="Remove file"
|
aria-label={dict.file.removeFile}
|
||||||
>
|
>
|
||||||
<X className="h-3 w-3" />
|
<X className="h-3 w-3" />
|
||||||
</button>
|
</button>
|
||||||
@@ -155,7 +153,6 @@ export function FilePreviewList({
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Image Modal/Lightbox */}
|
{/* Image Modal/Lightbox */}
|
||||||
{selectedImage && (
|
{selectedImage && (
|
||||||
<div
|
<div
|
||||||
@@ -165,7 +162,7 @@ export function FilePreviewList({
|
|||||||
<button
|
<button
|
||||||
className="absolute top-4 right-4 z-10 bg-white rounded-full p-2 hover:bg-gray-200 transition-colors"
|
className="absolute top-4 right-4 z-10 bg-white rounded-full p-2 hover:bg-gray-200 transition-colors"
|
||||||
onClick={() => setSelectedImage(null)}
|
onClick={() => setSelectedImage(null)}
|
||||||
aria-label="Close"
|
aria-label={dict.common.close}
|
||||||
>
|
>
|
||||||
<X className="h-6 w-6" />
|
<X className="h-6 w-6" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog"
|
} from "@/components/ui/dialog"
|
||||||
import { useDiagram } from "@/contexts/diagram-context"
|
import { useDiagram } from "@/contexts/diagram-context"
|
||||||
|
import { useDictionary } from "@/hooks/use-dictionary"
|
||||||
|
import { formatMessage } from "@/lib/i18n/utils"
|
||||||
|
|
||||||
interface HistoryDialogProps {
|
interface HistoryDialogProps {
|
||||||
showHistory: boolean
|
showHistory: boolean
|
||||||
@@ -22,6 +24,7 @@ export function HistoryDialog({
|
|||||||
showHistory,
|
showHistory,
|
||||||
onToggleHistory,
|
onToggleHistory,
|
||||||
}: HistoryDialogProps) {
|
}: HistoryDialogProps) {
|
||||||
|
const dict = useDictionary()
|
||||||
const { loadDiagram: onDisplayChart, diagramHistory } = useDiagram()
|
const { loadDiagram: onDisplayChart, diagramHistory } = useDiagram()
|
||||||
const [selectedIndex, setSelectedIndex] = useState<number | null>(null)
|
const [selectedIndex, setSelectedIndex] = useState<number | null>(null)
|
||||||
|
|
||||||
@@ -42,18 +45,15 @@ export function HistoryDialog({
|
|||||||
<Dialog open={showHistory} onOpenChange={onToggleHistory}>
|
<Dialog open={showHistory} onOpenChange={onToggleHistory}>
|
||||||
<DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto">
|
<DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Diagram History</DialogTitle>
|
<DialogTitle>{dict.history.title}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Here saved each diagram before AI modification.
|
{dict.history.description}
|
||||||
<br />
|
|
||||||
Click on a diagram to restore it
|
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
{diagramHistory.length === 0 ? (
|
{diagramHistory.length === 0 ? (
|
||||||
<div className="text-center p-4 text-gray-500">
|
<div className="text-center p-4 text-gray-500">
|
||||||
No history available yet. Send messages to create
|
{dict.history.noHistory}
|
||||||
diagram history.
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 py-4">
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 py-4">
|
||||||
@@ -70,14 +70,14 @@ export function HistoryDialog({
|
|||||||
<div className="aspect-video bg-white rounded overflow-hidden flex items-center justify-center">
|
<div className="aspect-video bg-white rounded overflow-hidden flex items-center justify-center">
|
||||||
<Image
|
<Image
|
||||||
src={item.svg}
|
src={item.svg}
|
||||||
alt={`Diagram version ${index + 1}`}
|
alt={`${dict.history.version} ${index + 1}`}
|
||||||
width={200}
|
width={200}
|
||||||
height={100}
|
height={100}
|
||||||
className="object-contain w-full h-full p-1"
|
className="object-contain w-full h-full p-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-center mt-1 text-gray-500">
|
<div className="text-xs text-center mt-1 text-gray-500">
|
||||||
Version {index + 1}
|
{dict.history.version} {index + 1}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -88,21 +88,23 @@ export function HistoryDialog({
|
|||||||
{selectedIndex !== null ? (
|
{selectedIndex !== null ? (
|
||||||
<>
|
<>
|
||||||
<div className="flex-1 text-sm text-muted-foreground">
|
<div className="flex-1 text-sm text-muted-foreground">
|
||||||
Restore to Version {selectedIndex + 1}?
|
{formatMessage(dict.history.restoreTo, {
|
||||||
|
version: selectedIndex + 1,
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setSelectedIndex(null)}
|
onClick={() => setSelectedIndex(null)}
|
||||||
>
|
>
|
||||||
Cancel
|
{dict.common.cancel}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleConfirmRestore}>
|
<Button onClick={handleConfirmRestore}>
|
||||||
Confirm
|
{dict.common.confirm}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Button variant="outline" onClick={handleClose}>
|
<Button variant="outline" onClick={handleClose}>
|
||||||
Close
|
{dict.common.close}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
108
components/language-toggle.tsx
Normal file
108
components/language-toggle.tsx
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { Globe } from "lucide-react"
|
||||||
|
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
||||||
|
import { Suspense, useEffect, useRef, useState } from "react"
|
||||||
|
import { i18n, type Locale } from "@/lib/i18n/config"
|
||||||
|
|
||||||
|
const LABELS: Record<string, string> = {
|
||||||
|
en: "EN",
|
||||||
|
zh: "中文",
|
||||||
|
ja: "日本語",
|
||||||
|
}
|
||||||
|
|
||||||
|
function LanguageToggleInner({ className = "" }: { className?: string }) {
|
||||||
|
const router = useRouter()
|
||||||
|
const pathname = usePathname() || "/"
|
||||||
|
const search = useSearchParams()
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [value, setValue] = useState<Locale>(i18n.defaultLocale)
|
||||||
|
const ref = useRef<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const seg = pathname.split("/").filter(Boolean)
|
||||||
|
const first = seg[0]
|
||||||
|
if (first && i18n.locales.includes(first as Locale))
|
||||||
|
setValue(first as Locale)
|
||||||
|
else setValue(i18n.defaultLocale)
|
||||||
|
}, [pathname])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function onDoc(e: MouseEvent) {
|
||||||
|
if (!ref.current) return
|
||||||
|
if (!ref.current.contains(e.target as Node)) setOpen(false)
|
||||||
|
}
|
||||||
|
if (open) document.addEventListener("mousedown", onDoc)
|
||||||
|
return () => document.removeEventListener("mousedown", onDoc)
|
||||||
|
}, [open])
|
||||||
|
|
||||||
|
const changeLocale = (lang: string) => {
|
||||||
|
const parts = pathname.split("/")
|
||||||
|
if (parts.length > 1 && i18n.locales.includes(parts[1] as Locale)) {
|
||||||
|
parts[1] = lang
|
||||||
|
} else {
|
||||||
|
parts.splice(1, 0, lang)
|
||||||
|
}
|
||||||
|
const newPath = parts.join("/") || "/"
|
||||||
|
const searchStr = search?.toString() ? `?${search.toString()}` : ""
|
||||||
|
setOpen(false)
|
||||||
|
router.push(newPath + searchStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`relative inline-flex ${className}`} ref={ref}>
|
||||||
|
<button
|
||||||
|
aria-haspopup="menu"
|
||||||
|
aria-expanded={open}
|
||||||
|
onClick={() => setOpen((s) => !s)}
|
||||||
|
className="p-2 rounded-full hover:bg-accent/20 transition-colors text-muted-foreground"
|
||||||
|
aria-label="Change language"
|
||||||
|
>
|
||||||
|
<Globe className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
{open && (
|
||||||
|
<div className="absolute right-0 top-full mt-2 w-40 bg-popover dark:bg-popover text-popover-foreground rounded-xl shadow-md border border-border/30 overflow-hidden z-50">
|
||||||
|
<div className="grid gap-0 divide-y divide-border/30">
|
||||||
|
{i18n.locales.map((loc) => (
|
||||||
|
<button
|
||||||
|
key={loc}
|
||||||
|
onClick={() => changeLocale(loc)}
|
||||||
|
className={`flex items-center gap-2 px-4 py-2 text-sm w-full text-left hover:bg-accent/10 transition-colors ${value === loc ? "bg-accent/10 font-semibold" : ""}`}
|
||||||
|
>
|
||||||
|
<span className="flex-1">
|
||||||
|
{LABELS[loc] ?? loc}
|
||||||
|
</span>
|
||||||
|
{value === loc && (
|
||||||
|
<span className="text-xs opacity-70">
|
||||||
|
✓
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LanguageToggle({
|
||||||
|
className = "",
|
||||||
|
}: {
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<button
|
||||||
|
className="p-2 rounded-full text-muted-foreground opacity-50"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<Globe className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<LanguageToggleInner className={className} />
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ import { Coffee, X } from "lucide-react"
|
|||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import type React from "react"
|
import type React from "react"
|
||||||
import { FaGithub } from "react-icons/fa"
|
import { FaGithub } from "react-icons/fa"
|
||||||
|
import { useDictionary } from "@/hooks/use-dictionary"
|
||||||
|
import { formatMessage } from "@/lib/i18n/utils"
|
||||||
|
|
||||||
interface QuotaLimitToastProps {
|
interface QuotaLimitToastProps {
|
||||||
type?: "request" | "token"
|
type?: "request" | "token"
|
||||||
@@ -18,9 +20,11 @@ export function QuotaLimitToast({
|
|||||||
limit,
|
limit,
|
||||||
onDismiss,
|
onDismiss,
|
||||||
}: QuotaLimitToastProps) {
|
}: QuotaLimitToastProps) {
|
||||||
|
const dict = useDictionary()
|
||||||
const isTokenLimit = type === "token"
|
const isTokenLimit = type === "token"
|
||||||
const formatNumber = (n: number) =>
|
const formatNumber = (n: number) =>
|
||||||
n >= 1000 ? `${(n / 1000).toFixed(1)}k` : n.toString()
|
n >= 1000 ? `${(n / 1000).toFixed(1)}k` : n.toString()
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
if (e.key === "Escape") {
|
if (e.key === "Escape") {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -44,7 +48,6 @@ export function QuotaLimitToast({
|
|||||||
>
|
>
|
||||||
<X className="w-4 h-4" />
|
<X className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Title row with icon */}
|
{/* Title row with icon */}
|
||||||
<div className="flex items-center gap-2.5 mb-3 pr-6">
|
<div className="flex items-center gap-2.5 mb-3 pr-6">
|
||||||
<div className="flex-shrink-0 w-8 h-8 rounded-lg bg-accent flex items-center justify-center">
|
<div className="flex-shrink-0 w-8 h-8 rounded-lg bg-accent flex items-center justify-center">
|
||||||
@@ -55,40 +58,26 @@ export function QuotaLimitToast({
|
|||||||
</div>
|
</div>
|
||||||
<h3 className="font-semibold text-foreground text-sm">
|
<h3 className="font-semibold text-foreground text-sm">
|
||||||
{isTokenLimit
|
{isTokenLimit
|
||||||
? "Daily Token Limit Reached"
|
? dict.quota.tokenLimit
|
||||||
: "Daily Quota Reached"}
|
: dict.quota.dailyLimit}
|
||||||
</h3>
|
</h3>
|
||||||
<span className="px-2 py-0.5 text-xs font-medium rounded-md bg-muted text-muted-foreground">
|
<span className="px-2 py-0.5 text-xs font-medium rounded-md bg-muted text-muted-foreground">
|
||||||
{isTokenLimit
|
{formatMessage(dict.quota.usedOf, {
|
||||||
? `${formatNumber(used)}/${formatNumber(limit)} tokens`
|
used: formatNumber(used),
|
||||||
: `${used}/${limit}`}
|
limit: formatNumber(limit),
|
||||||
|
})}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Message */}
|
{/* Message */}
|
||||||
<div className="text-sm text-muted-foreground leading-relaxed mb-4 space-y-2">
|
<div className="text-sm text-muted-foreground leading-relaxed mb-4 space-y-2">
|
||||||
<p>
|
<p>
|
||||||
Oops — you've reached the daily{" "}
|
{isTokenLimit
|
||||||
{isTokenLimit ? "token" : "API"} limit for this demo! As an
|
? dict.quota.messageToken
|
||||||
indie developer covering all the API costs myself, I have to
|
: dict.quota.messageApi}
|
||||||
set these limits to keep things sustainable.{" "}
|
|
||||||
<Link
|
|
||||||
href="/about"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="inline-flex items-center gap-1 text-amber-600 font-medium hover:text-amber-700 hover:underline"
|
|
||||||
>
|
|
||||||
Learn more →
|
|
||||||
</Link>
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p dangerouslySetInnerHTML={{ __html: dict.quota.tip }} />
|
||||||
<strong>Tip:</strong> You can use your own API key (click
|
<p>{dict.quota.reset}</p>
|
||||||
the Settings icon) or self-host the project to bypass these
|
</div>{" "}
|
||||||
limits.
|
|
||||||
</p>
|
|
||||||
<p>Your limit resets tomorrow. Thanks for understanding!</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Action buttons */}
|
{/* Action buttons */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<a
|
<a
|
||||||
@@ -98,7 +87,7 @@ export function QuotaLimitToast({
|
|||||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors"
|
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors"
|
||||||
>
|
>
|
||||||
<FaGithub className="w-3.5 h-3.5" />
|
<FaGithub className="w-3.5 h-3.5" />
|
||||||
Self-host
|
{dict.quota.selfHost}
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="https://github.com/sponsors/DayuanJiang"
|
href="https://github.com/sponsors/DayuanJiang"
|
||||||
@@ -107,7 +96,7 @@ export function QuotaLimitToast({
|
|||||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg border border-border text-foreground hover:bg-muted transition-colors"
|
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg border border-border text-foreground hover:bg-muted transition-colors"
|
||||||
>
|
>
|
||||||
<Coffee className="w-3.5 h-3.5" />
|
<Coffee className="w-3.5 h-3.5" />
|
||||||
Sponsor
|
{dict.quota.sponsor}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog"
|
} from "@/components/ui/dialog"
|
||||||
|
import { useDictionary } from "@/hooks/use-dictionary"
|
||||||
|
|
||||||
interface ResetWarningModalProps {
|
interface ResetWarningModalProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
@@ -21,14 +22,15 @@ export function ResetWarningModal({
|
|||||||
onOpenChange,
|
onOpenChange,
|
||||||
onClear,
|
onClear,
|
||||||
}: ResetWarningModalProps) {
|
}: ResetWarningModalProps) {
|
||||||
|
const dict = useDictionary()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Clear Everything?</DialogTitle>
|
<DialogTitle>{dict.dialogs.clearTitle}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
This will clear the current conversation and reset the
|
{dict.dialogs.clearDescription}
|
||||||
diagram. This action cannot be undone.
|
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
@@ -36,10 +38,10 @@ export function ResetWarningModal({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => onOpenChange(false)}
|
onClick={() => onOpenChange(false)}
|
||||||
>
|
>
|
||||||
Cancel
|
{dict.common.cancel}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="destructive" onClick={onClear}>
|
<Button variant="destructive" onClick={onClear}>
|
||||||
Clear Everything
|
{dict.dialogs.clearEverything}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button"
|
|||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
@@ -17,19 +18,10 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select"
|
} from "@/components/ui/select"
|
||||||
|
import { useDictionary } from "@/hooks/use-dictionary"
|
||||||
|
|
||||||
export type ExportFormat = "drawio" | "png" | "svg"
|
export type ExportFormat = "drawio" | "png" | "svg"
|
||||||
|
|
||||||
const FORMAT_OPTIONS: {
|
|
||||||
value: ExportFormat
|
|
||||||
label: string
|
|
||||||
extension: string
|
|
||||||
}[] = [
|
|
||||||
{ value: "drawio", label: "Draw.io XML", extension: ".drawio" },
|
|
||||||
{ value: "png", label: "PNG Image", extension: ".png" },
|
|
||||||
{ value: "svg", label: "SVG Image", extension: ".svg" },
|
|
||||||
]
|
|
||||||
|
|
||||||
interface SaveDialogProps {
|
interface SaveDialogProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
onOpenChange: (open: boolean) => void
|
onOpenChange: (open: boolean) => void
|
||||||
@@ -43,6 +35,7 @@ export function SaveDialog({
|
|||||||
onSave,
|
onSave,
|
||||||
defaultFilename,
|
defaultFilename,
|
||||||
}: SaveDialogProps) {
|
}: SaveDialogProps) {
|
||||||
|
const dict = useDictionary()
|
||||||
const [filename, setFilename] = useState(defaultFilename)
|
const [filename, setFilename] = useState(defaultFilename)
|
||||||
const [format, setFormat] = useState<ExportFormat>("drawio")
|
const [format, setFormat] = useState<ExportFormat>("drawio")
|
||||||
|
|
||||||
@@ -65,17 +58,40 @@ export function SaveDialog({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FORMAT_OPTIONS = [
|
||||||
|
{
|
||||||
|
value: "drawio" as const,
|
||||||
|
label: dict.save.formats.drawio,
|
||||||
|
extension: ".drawio",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "png" as const,
|
||||||
|
label: dict.save.formats.png,
|
||||||
|
extension: ".png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "svg" as const,
|
||||||
|
label: dict.save.formats.svg,
|
||||||
|
extension: ".svg",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
const currentFormat = FORMAT_OPTIONS.find((f) => f.value === format)
|
const currentFormat = FORMAT_OPTIONS.find((f) => f.value === format)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="sm:max-w-md">
|
<DialogContent className="sm:max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Save Diagram</DialogTitle>
|
<DialogTitle>{dict.save.title}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{dict.save.description}
|
||||||
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium">Format</label>
|
<label className="text-sm font-medium">
|
||||||
|
{dict.save.format}
|
||||||
|
</label>
|
||||||
<Select
|
<Select
|
||||||
value={format}
|
value={format}
|
||||||
onValueChange={(v) => setFormat(v as ExportFormat)}
|
onValueChange={(v) => setFormat(v as ExportFormat)}
|
||||||
@@ -96,13 +112,15 @@ export function SaveDialog({
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium">Filename</label>
|
<label className="text-sm font-medium">
|
||||||
|
{dict.save.filename}
|
||||||
|
</label>
|
||||||
<div className="flex items-stretch">
|
<div className="flex items-stretch">
|
||||||
<Input
|
<Input
|
||||||
value={filename}
|
value={filename}
|
||||||
onChange={(e) => setFilename(e.target.value)}
|
onChange={(e) => setFilename(e.target.value)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
placeholder="Enter filename"
|
placeholder={dict.save.filenamePlaceholder}
|
||||||
autoFocus
|
autoFocus
|
||||||
onFocus={(e) => e.target.select()}
|
onFocus={(e) => e.target.select()}
|
||||||
className="rounded-r-none border-r-0 focus-visible:z-10"
|
className="rounded-r-none border-r-0 focus-visible:z-10"
|
||||||
@@ -118,9 +136,9 @@ export function SaveDialog({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => onOpenChange(false)}
|
onClick={() => onOpenChange(false)}
|
||||||
>
|
>
|
||||||
Cancel
|
{dict.common.cancel}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleSave}>Save</Button>
|
<Button onClick={handleSave}>{dict.common.save}</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select"
|
} from "@/components/ui/select"
|
||||||
import { Switch } from "@/components/ui/switch"
|
import { Switch } from "@/components/ui/switch"
|
||||||
|
import { useDictionary } from "@/hooks/use-dictionary"
|
||||||
|
|
||||||
interface SettingsDialogProps {
|
interface SettingsDialogProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
@@ -55,6 +56,7 @@ export function SettingsDialog({
|
|||||||
darkMode,
|
darkMode,
|
||||||
onToggleDarkMode,
|
onToggleDarkMode,
|
||||||
}: SettingsDialogProps) {
|
}: SettingsDialogProps) {
|
||||||
|
const dict = useDictionary()
|
||||||
const [accessCode, setAccessCode] = useState("")
|
const [accessCode, setAccessCode] = useState("")
|
||||||
const [closeProtection, setCloseProtection] = useState(true)
|
const [closeProtection, setCloseProtection] = useState(true)
|
||||||
const [isVerifying, setIsVerifying] = useState(false)
|
const [isVerifying, setIsVerifying] = useState(false)
|
||||||
@@ -129,14 +131,14 @@ export function SettingsDialog({
|
|||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
|
|
||||||
if (!data.valid) {
|
if (!data.valid) {
|
||||||
setError(data.message || "Invalid access code")
|
setError(data.message || dict.errors.invalidAccessCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
localStorage.setItem(STORAGE_ACCESS_CODE_KEY, accessCode.trim())
|
localStorage.setItem(STORAGE_ACCESS_CODE_KEY, accessCode.trim())
|
||||||
onOpenChange(false)
|
onOpenChange(false)
|
||||||
} catch {
|
} catch {
|
||||||
setError("Failed to verify access code")
|
setError(dict.errors.networkError)
|
||||||
} finally {
|
} finally {
|
||||||
setIsVerifying(false)
|
setIsVerifying(false)
|
||||||
}
|
}
|
||||||
@@ -153,15 +155,17 @@ export function SettingsDialog({
|
|||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="sm:max-w-md">
|
<DialogContent className="sm:max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Settings</DialogTitle>
|
<DialogTitle>{dict.settings.title}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Configure your application settings.
|
{dict.settings.description}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4 py-2">
|
<div className="space-y-4 py-2">
|
||||||
{accessCodeRequired && (
|
{accessCodeRequired && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="access-code">Access Code</Label>
|
<Label htmlFor="access-code">
|
||||||
|
{dict.settings.accessCode}
|
||||||
|
</Label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
id="access-code"
|
id="access-code"
|
||||||
@@ -171,18 +175,20 @@ export function SettingsDialog({
|
|||||||
setAccessCode(e.target.value)
|
setAccessCode(e.target.value)
|
||||||
}
|
}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
placeholder="Enter access code"
|
placeholder={
|
||||||
|
dict.settings.accessCodePlaceholder
|
||||||
|
}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={isVerifying || !accessCode.trim()}
|
disabled={isVerifying || !accessCode.trim()}
|
||||||
>
|
>
|
||||||
{isVerifying ? "..." : "Save"}
|
{isVerifying ? "..." : dict.common.save}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[0.8rem] text-muted-foreground">
|
<p className="text-[0.8rem] text-muted-foreground">
|
||||||
Required to use this application.
|
{dict.settings.accessCodeDescription}
|
||||||
</p>
|
</p>
|
||||||
{error && (
|
{error && (
|
||||||
<p className="text-[0.8rem] text-destructive">
|
<p className="text-[0.8rem] text-destructive">
|
||||||
@@ -192,15 +198,15 @@ export function SettingsDialog({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>AI Provider Settings</Label>
|
<Label>{dict.settings.aiProvider}</Label>
|
||||||
<p className="text-[0.8rem] text-muted-foreground">
|
<p className="text-[0.8rem] text-muted-foreground">
|
||||||
Use your own API key to bypass usage limits. Your
|
{dict.settings.aiProviderDescription}
|
||||||
key is stored locally in your browser and is never
|
|
||||||
stored on the server.
|
|
||||||
</p>
|
</p>
|
||||||
<div className="space-y-3 pt-2">
|
<div className="space-y-3 pt-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="ai-provider">Provider</Label>
|
<Label htmlFor="ai-provider">
|
||||||
|
{dict.settings.provider}
|
||||||
|
</Label>
|
||||||
<Select
|
<Select
|
||||||
value={provider || "default"}
|
value={provider || "default"}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
@@ -214,32 +220,36 @@ export function SettingsDialog({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger id="ai-provider">
|
<SelectTrigger id="ai-provider">
|
||||||
<SelectValue placeholder="Use Server Default" />
|
<SelectValue
|
||||||
|
placeholder={
|
||||||
|
dict.settings.useServerDefault
|
||||||
|
}
|
||||||
|
/>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="default">
|
<SelectItem value="default">
|
||||||
Use Server Default
|
{dict.settings.useServerDefault}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="openai">
|
<SelectItem value="openai">
|
||||||
OpenAI
|
{dict.providers.openai}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="anthropic">
|
<SelectItem value="anthropic">
|
||||||
Anthropic
|
{dict.providers.anthropic}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="google">
|
<SelectItem value="google">
|
||||||
Google
|
{dict.providers.google}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="azure">
|
<SelectItem value="azure">
|
||||||
Azure OpenAI
|
{dict.providers.azure}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="openrouter">
|
<SelectItem value="openrouter">
|
||||||
OpenRouter
|
{dict.providers.openrouter}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="deepseek">
|
<SelectItem value="deepseek">
|
||||||
DeepSeek
|
{dict.providers.deepseek}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="siliconflow">
|
<SelectItem value="siliconflow">
|
||||||
SiliconFlow
|
{dict.providers.siliconflow}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
@@ -248,7 +258,7 @@ export function SettingsDialog({
|
|||||||
<>
|
<>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="ai-model">
|
<Label htmlFor="ai-model">
|
||||||
Model ID
|
{dict.settings.modelId}
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="ai-model"
|
id="ai-model"
|
||||||
@@ -270,13 +280,14 @@ export function SettingsDialog({
|
|||||||
: provider ===
|
: provider ===
|
||||||
"deepseek"
|
"deepseek"
|
||||||
? "e.g., deepseek-chat"
|
? "e.g., deepseek-chat"
|
||||||
: "Model ID"
|
: dict.settings
|
||||||
|
.modelId
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="ai-api-key">
|
<Label htmlFor="ai-api-key">
|
||||||
API Key
|
{dict.settings.apiKey}
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="ai-api-key"
|
id="ai-api-key"
|
||||||
@@ -289,11 +300,13 @@ export function SettingsDialog({
|
|||||||
e.target.value,
|
e.target.value,
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
placeholder="Your API key"
|
placeholder={
|
||||||
|
dict.settings.apiKeyPlaceholder
|
||||||
|
}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
<p className="text-[0.8rem] text-muted-foreground">
|
<p className="text-[0.8rem] text-muted-foreground">
|
||||||
Overrides{" "}
|
{dict.settings.overrides}{" "}
|
||||||
{provider === "openai"
|
{provider === "openai"
|
||||||
? "OPENAI_API_KEY"
|
? "OPENAI_API_KEY"
|
||||||
: provider === "anthropic"
|
: provider === "anthropic"
|
||||||
@@ -316,7 +329,7 @@ export function SettingsDialog({
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="ai-base-url">
|
<Label htmlFor="ai-base-url">
|
||||||
Base URL (optional)
|
{dict.settings.baseUrl}
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="ai-base-url"
|
id="ai-base-url"
|
||||||
@@ -333,7 +346,8 @@ export function SettingsDialog({
|
|||||||
? "https://api.anthropic.com/v1"
|
? "https://api.anthropic.com/v1"
|
||||||
: provider === "siliconflow"
|
: provider === "siliconflow"
|
||||||
? "https://api.siliconflow.com/v1"
|
? "https://api.siliconflow.com/v1"
|
||||||
: "Custom endpoint URL"
|
: dict.settings
|
||||||
|
.customEndpoint
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -360,7 +374,7 @@ export function SettingsDialog({
|
|||||||
setModelId("")
|
setModelId("")
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Clear Settings
|
{dict.settings.clearSettings}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -369,9 +383,11 @@ export function SettingsDialog({
|
|||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<Label htmlFor="theme-toggle">Theme</Label>
|
<Label htmlFor="theme-toggle">
|
||||||
|
{dict.settings.theme}
|
||||||
|
</Label>
|
||||||
<p className="text-[0.8rem] text-muted-foreground">
|
<p className="text-[0.8rem] text-muted-foreground">
|
||||||
Dark/Light mode for interface and DrawIO canvas.
|
{dict.settings.themeDescription}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
@@ -390,10 +406,14 @@ export function SettingsDialog({
|
|||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<Label htmlFor="drawio-ui">DrawIO Style</Label>
|
<Label htmlFor="drawio-ui">
|
||||||
|
{dict.settings.drawioStyle}
|
||||||
|
</Label>
|
||||||
<p className="text-[0.8rem] text-muted-foreground">
|
<p className="text-[0.8rem] text-muted-foreground">
|
||||||
Canvas style:{" "}
|
{dict.settings.drawioStyleDescription}{" "}
|
||||||
{drawioUi === "min" ? "Minimal" : "Sketch"}
|
{drawioUi === "min"
|
||||||
|
? dict.settings.minimal
|
||||||
|
: dict.settings.sketch}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
@@ -402,18 +422,20 @@ export function SettingsDialog({
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={onToggleDrawioUi}
|
onClick={onToggleDrawioUi}
|
||||||
>
|
>
|
||||||
Switch to{" "}
|
{dict.settings.switchTo}{" "}
|
||||||
{drawioUi === "min" ? "Sketch" : "Minimal"}
|
{drawioUi === "min"
|
||||||
|
? dict.settings.sketch
|
||||||
|
: dict.settings.minimal}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<Label htmlFor="close-protection">
|
<Label htmlFor="close-protection">
|
||||||
Close Protection
|
{dict.settings.closeProtection}
|
||||||
</Label>
|
</Label>
|
||||||
<p className="text-[0.8rem] text-muted-foreground">
|
<p className="text-[0.8rem] text-muted-foreground">
|
||||||
Show confirmation when leaving the page.
|
{dict.settings.closeProtectionDescription}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import type React from "react"
|
import type React from "react"
|
||||||
import { createContext, useContext, useRef, useState } from "react"
|
import { createContext, useContext, useEffect, useRef, useState } from "react"
|
||||||
import type { DrawIoEmbedRef } from "react-drawio"
|
import type { DrawIoEmbedRef } from "react-drawio"
|
||||||
import { STORAGE_DIAGRAM_XML_KEY } from "@/components/chat-panel"
|
import { STORAGE_DIAGRAM_XML_KEY } from "@/components/chat-panel"
|
||||||
import type { ExportFormat } from "@/components/save-dialog"
|
import type { ExportFormat } from "@/components/save-dialog"
|
||||||
@@ -23,9 +23,12 @@ interface DiagramContextType {
|
|||||||
format: ExportFormat,
|
format: ExportFormat,
|
||||||
sessionId?: string,
|
sessionId?: string,
|
||||||
) => void
|
) => void
|
||||||
|
saveDiagramToStorage: () => Promise<void>
|
||||||
isDrawioReady: boolean
|
isDrawioReady: boolean
|
||||||
onDrawioLoad: () => void
|
onDrawioLoad: () => void
|
||||||
resetDrawioReady: () => void
|
resetDrawioReady: () => void
|
||||||
|
showSaveDialog: boolean
|
||||||
|
setShowSaveDialog: (show: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const DiagramContext = createContext<DiagramContextType | undefined>(undefined)
|
const DiagramContext = createContext<DiagramContextType | undefined>(undefined)
|
||||||
@@ -37,11 +40,15 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
|||||||
{ svg: string; xml: string }[]
|
{ svg: string; xml: string }[]
|
||||||
>([])
|
>([])
|
||||||
const [isDrawioReady, setIsDrawioReady] = useState(false)
|
const [isDrawioReady, setIsDrawioReady] = useState(false)
|
||||||
|
const [canSaveDiagram, setCanSaveDiagram] = useState(false)
|
||||||
|
const [showSaveDialog, setShowSaveDialog] = useState(false)
|
||||||
const hasCalledOnLoadRef = useRef(false)
|
const hasCalledOnLoadRef = useRef(false)
|
||||||
const drawioRef = useRef<DrawIoEmbedRef | null>(null)
|
const drawioRef = useRef<DrawIoEmbedRef | null>(null)
|
||||||
const resolverRef = useRef<((value: string) => void) | null>(null)
|
const resolverRef = useRef<((value: string) => void) | null>(null)
|
||||||
// Track if we're expecting an export for history (user-initiated)
|
// Track if we're expecting an export for history (user-initiated)
|
||||||
const expectHistoryExportRef = useRef<boolean>(false)
|
const expectHistoryExportRef = useRef<boolean>(false)
|
||||||
|
// Track if diagram has been restored from localStorage
|
||||||
|
const hasDiagramRestoredRef = useRef<boolean>(false)
|
||||||
|
|
||||||
const onDrawioLoad = () => {
|
const onDrawioLoad = () => {
|
||||||
// Only set ready state once to prevent infinite loops
|
// Only set ready state once to prevent infinite loops
|
||||||
@@ -57,6 +64,48 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
|||||||
setIsDrawioReady(false)
|
setIsDrawioReady(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restore diagram XML when DrawIO becomes ready
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- loadDiagram uses refs internally and is stable
|
||||||
|
useEffect(() => {
|
||||||
|
// Reset restore flag when DrawIO is not ready (e.g., theme/UI change remounts it)
|
||||||
|
if (!isDrawioReady) {
|
||||||
|
hasDiagramRestoredRef.current = false
|
||||||
|
setCanSaveDiagram(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (hasDiagramRestoredRef.current) return
|
||||||
|
hasDiagramRestoredRef.current = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const savedDiagramXml = localStorage.getItem(
|
||||||
|
STORAGE_DIAGRAM_XML_KEY,
|
||||||
|
)
|
||||||
|
if (savedDiagramXml) {
|
||||||
|
// Skip validation for trusted saved diagrams
|
||||||
|
loadDiagram(savedDiagramXml, true)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to restore diagram from localStorage:", error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow saving after restore is complete
|
||||||
|
setTimeout(() => {
|
||||||
|
setCanSaveDiagram(true)
|
||||||
|
}, 500)
|
||||||
|
}, [isDrawioReady])
|
||||||
|
|
||||||
|
// Save diagram XML to localStorage whenever it changes (debounced)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!canSaveDiagram) return
|
||||||
|
if (!chartXML || chartXML.length <= 300) return
|
||||||
|
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
localStorage.setItem(STORAGE_DIAGRAM_XML_KEY, chartXML)
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
return () => clearTimeout(timeoutId)
|
||||||
|
}, [chartXML, canSaveDiagram])
|
||||||
|
|
||||||
// Track if we're expecting an export for file save (stores raw export data)
|
// Track if we're expecting an export for file save (stores raw export data)
|
||||||
const saveResolverRef = useRef<{
|
const saveResolverRef = useRef<{
|
||||||
resolver: ((data: string) => void) | null
|
resolver: ((data: string) => void) | null
|
||||||
@@ -82,24 +131,44 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save current diagram to localStorage (used before theme/UI changes)
|
||||||
|
const saveDiagramToStorage = async (): Promise<void> => {
|
||||||
|
if (!drawioRef.current) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const currentXml = await Promise.race([
|
||||||
|
new Promise<string>((resolve) => {
|
||||||
|
resolverRef.current = resolve
|
||||||
|
drawioRef.current?.exportDiagram({ format: "xmlsvg" })
|
||||||
|
}),
|
||||||
|
new Promise<string>((_, reject) =>
|
||||||
|
setTimeout(() => reject(new Error("Export timeout")), 2000),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
// Only save if diagram has meaningful content (not empty template)
|
||||||
|
if (currentXml && currentXml.length > 300) {
|
||||||
|
localStorage.setItem(STORAGE_DIAGRAM_XML_KEY, currentXml)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to save diagram to storage:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const loadDiagram = (
|
const loadDiagram = (
|
||||||
chart: string,
|
chart: string,
|
||||||
skipValidation?: boolean,
|
skipValidation?: boolean,
|
||||||
): string | null => {
|
): string | null => {
|
||||||
console.time("perf:loadDiagram")
|
|
||||||
let xmlToLoad = chart
|
let xmlToLoad = chart
|
||||||
|
|
||||||
// Validate XML structure before loading (unless skipped for internal use)
|
// Validate XML structure before loading (unless skipped for internal use)
|
||||||
if (!skipValidation) {
|
if (!skipValidation) {
|
||||||
console.time("perf:loadDiagram-validation")
|
|
||||||
const validation = validateAndFixXml(chart)
|
const validation = validateAndFixXml(chart)
|
||||||
console.timeEnd("perf:loadDiagram-validation")
|
|
||||||
if (!validation.valid) {
|
if (!validation.valid) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"[loadDiagram] Validation error:",
|
"[loadDiagram] Validation error:",
|
||||||
validation.error,
|
validation.error,
|
||||||
)
|
)
|
||||||
console.timeEnd("perf:loadDiagram")
|
|
||||||
return validation.error
|
return validation.error
|
||||||
}
|
}
|
||||||
// Use fixed XML if auto-fix was applied
|
// Use fixed XML if auto-fix was applied
|
||||||
@@ -116,14 +185,11 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
|||||||
setChartXML(xmlToLoad)
|
setChartXML(xmlToLoad)
|
||||||
|
|
||||||
if (drawioRef.current) {
|
if (drawioRef.current) {
|
||||||
console.time("perf:drawio-iframe-load")
|
|
||||||
drawioRef.current.load({
|
drawioRef.current.load({
|
||||||
xml: xmlToLoad,
|
xml: xmlToLoad,
|
||||||
})
|
})
|
||||||
console.timeEnd("perf:drawio-iframe-load")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.timeEnd("perf:loadDiagram")
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,9 +353,12 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
|||||||
handleDiagramExport,
|
handleDiagramExport,
|
||||||
clearDiagram,
|
clearDiagram,
|
||||||
saveDiagramToFile,
|
saveDiagramToFile,
|
||||||
|
saveDiagramToStorage,
|
||||||
isDrawioReady,
|
isDrawioReady,
|
||||||
onDrawioLoad,
|
onDrawioLoad,
|
||||||
resetDrawioReady,
|
resetDrawioReady,
|
||||||
|
showSaveDialog,
|
||||||
|
setShowSaveDialog,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
|||||||
- [目录](#目录)
|
- [目录](#目录)
|
||||||
- [示例](#示例)
|
- [示例](#示例)
|
||||||
- [功能特性](#功能特性)
|
- [功能特性](#功能特性)
|
||||||
|
- [MCP服务器(预览)](#mcp服务器预览)
|
||||||
- [快速开始](#快速开始)
|
- [快速开始](#快速开始)
|
||||||
- [在线试用](#在线试用)
|
- [在线试用](#在线试用)
|
||||||
- [使用Docker运行(推荐)](#使用docker运行推荐)
|
- [使用Docker运行(推荐)](#使用docker运行推荐)
|
||||||
@@ -88,6 +89,36 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
|||||||
- **云架构图支持**:专门支持生成云架构图(AWS、GCP、Azure)
|
- **云架构图支持**:专门支持生成云架构图(AWS、GCP、Azure)
|
||||||
- **动画连接器**:在图表元素之间创建动态动画连接器,实现更好的可视化效果
|
- **动画连接器**:在图表元素之间创建动态动画连接器,实现更好的可视化效果
|
||||||
|
|
||||||
|
## MCP服务器(预览)
|
||||||
|
|
||||||
|
> **预览功能**:此功能为实验性功能,可能会有变化。
|
||||||
|
|
||||||
|
通过MCP(模型上下文协议)在Claude Desktop、Cursor和VS Code等AI代理中使用Next AI Draw.io。
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"drawio": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["@next-ai-drawio/mcp-server@latest"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Claude Code CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude mcp add drawio -- npx @next-ai-drawio/mcp-server@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
然后让Claude创建图表:
|
||||||
|
> "创建一个展示用户认证流程的流程图,包含登录、MFA和会话管理"
|
||||||
|
|
||||||
|
图表会实时显示在浏览器中!
|
||||||
|
|
||||||
|
详情请参阅[MCP服务器README](../packages/mcp-server/README.md),了解VS Code、Cursor等客户端配置。
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
### 在线试用
|
### 在线试用
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
|||||||
- [目次](#目次)
|
- [目次](#目次)
|
||||||
- [例](#例)
|
- [例](#例)
|
||||||
- [機能](#機能)
|
- [機能](#機能)
|
||||||
|
- [MCPサーバー(プレビュー)](#mcpサーバープレビュー)
|
||||||
- [はじめに](#はじめに)
|
- [はじめに](#はじめに)
|
||||||
- [オンラインで試す](#オンラインで試す)
|
- [オンラインで試す](#オンラインで試す)
|
||||||
- [Dockerで実行(推奨)](#dockerで実行推奨)
|
- [Dockerで実行(推奨)](#dockerで実行推奨)
|
||||||
@@ -88,6 +89,36 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
|||||||
- **クラウドアーキテクチャダイアグラムサポート**:クラウドアーキテクチャダイアグラムの生成を専門的にサポート(AWS、GCP、Azure)
|
- **クラウドアーキテクチャダイアグラムサポート**:クラウドアーキテクチャダイアグラムの生成を専門的にサポート(AWS、GCP、Azure)
|
||||||
- **アニメーションコネクタ**:より良い可視化のためにダイアグラム要素間に動的でアニメーション化されたコネクタを作成
|
- **アニメーションコネクタ**:より良い可視化のためにダイアグラム要素間に動的でアニメーション化されたコネクタを作成
|
||||||
|
|
||||||
|
## MCPサーバー(プレビュー)
|
||||||
|
|
||||||
|
> **プレビュー機能**:この機能は実験的であり、変更される可能性があります。
|
||||||
|
|
||||||
|
MCP(Model Context Protocol)を介して、Claude Desktop、Cursor、VS CodeなどのAIエージェントでNext AI Draw.ioを使用できます。
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"drawio": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["@next-ai-drawio/mcp-server@latest"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Claude Code CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude mcp add drawio -- npx @next-ai-drawio/mcp-server@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Claudeにダイアグラムの作成を依頼:
|
||||||
|
> 「ログイン、MFA、セッション管理を含むユーザー認証のフローチャートを作成してください」
|
||||||
|
|
||||||
|
ダイアグラムがリアルタイムでブラウザに表示されます!
|
||||||
|
|
||||||
|
詳細は[MCPサーバーREADME](../packages/mcp-server/README.md)をご覧ください(VS Code、Cursorなどのクライアント設定も含む)。
|
||||||
|
|
||||||
## はじめに
|
## はじめに
|
||||||
|
|
||||||
### オンラインで試す
|
### オンラインで試す
|
||||||
|
|||||||
@@ -136,6 +136,42 @@ Optional custom URL:
|
|||||||
OLLAMA_BASE_URL=http://localhost:11434
|
OLLAMA_BASE_URL=http://localhost:11434
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Vercel AI Gateway
|
||||||
|
|
||||||
|
Vercel AI Gateway provides unified access to multiple AI providers through a single API key. This simplifies authentication and allows you to switch between providers without managing multiple API keys.
|
||||||
|
|
||||||
|
**Basic Usage (Vercel-hosted Gateway):**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AI_GATEWAY_API_KEY=your_gateway_api_key
|
||||||
|
AI_MODEL=openai/gpt-4o
|
||||||
|
```
|
||||||
|
|
||||||
|
**Custom Gateway URL (for local development or self-hosted Gateway):**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AI_GATEWAY_API_KEY=your_custom_api_key
|
||||||
|
AI_GATEWAY_BASE_URL=https://your-custom-gateway.com/v1/ai
|
||||||
|
AI_MODEL=openai/gpt-4o
|
||||||
|
```
|
||||||
|
|
||||||
|
Model format uses `provider/model` syntax:
|
||||||
|
|
||||||
|
- `openai/gpt-4o` - OpenAI GPT-4o
|
||||||
|
- `anthropic/claude-sonnet-4-5` - Anthropic Claude Sonnet 4.5
|
||||||
|
- `google/gemini-2.0-flash` - Google Gemini 2.0 Flash
|
||||||
|
|
||||||
|
**Configuration notes:**
|
||||||
|
|
||||||
|
- If `AI_GATEWAY_BASE_URL` is not set, the default Vercel Gateway URL (`https://ai-gateway.vercel.sh/v1/ai`) is used
|
||||||
|
- Custom base URL is useful for:
|
||||||
|
- Local development with a custom Gateway instance
|
||||||
|
- Self-hosted AI Gateway deployments
|
||||||
|
- Enterprise proxy configurations
|
||||||
|
- When using a custom base URL, you must also provide `AI_GATEWAY_API_KEY`
|
||||||
|
|
||||||
|
Get your API key from the [Vercel AI Gateway dashboard](https://vercel.com/ai-gateway).
|
||||||
|
|
||||||
## Auto-Detection
|
## Auto-Detection
|
||||||
|
|
||||||
If you only configure **one** provider's API key, the system will automatically detect and use that provider. No need to set `AI_PROVIDER`.
|
If you only configure **one** provider's API key, the system will automatically detect and use that provider. No need to set `AI_PROVIDER`.
|
||||||
@@ -143,7 +179,7 @@ If you only configure **one** provider's API key, the system will automatically
|
|||||||
If you configure **multiple** API keys, you must explicitly set `AI_PROVIDER`:
|
If you configure **multiple** API keys, you must explicitly set `AI_PROVIDER`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
AI_PROVIDER=google # or: openai, anthropic, deepseek, siliconflow, azure, bedrock, openrouter, ollama
|
AI_PROVIDER=google # or: openai, anthropic, deepseek, siliconflow, azure, bedrock, openrouter, ollama, gateway
|
||||||
```
|
```
|
||||||
|
|
||||||
## Model Capability Requirements
|
## Model Capability Requirements
|
||||||
|
|||||||
78
docs/shape-libraries/README.md
Normal file
78
docs/shape-libraries/README.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# Draw.io Shape Libraries
|
||||||
|
|
||||||
|
Reference: `style="shape=mxgraph.<library>.<shape_name>"`
|
||||||
|
|
||||||
|
## Cloud Providers
|
||||||
|
|
||||||
|
| Library | Shapes | Prefix | Description | File |
|
||||||
|
|---------|--------|--------|-------------|------|
|
||||||
|
| aws4 | 1031 | `mxgraph.aws4` | Amazon Web Services (2025) - EC2, S3, Lambda, RDS, etc. | [aws4.md](./aws4.md) |
|
||||||
|
| azure2 | 608 | `img/lib/azure2/` | Microsoft Azure (2024) - VMs, Storage, AI, Networking, etc. | [azure2.md](./azure2.md) |
|
||||||
|
| gcp2 | 297 | `mxgraph.gcp2` | Google Cloud Platform - Compute Engine, BigQuery, GKE, etc. | [gcp2.md](./gcp2.md) |
|
||||||
|
| alibaba_cloud | 273 | `mxgraph.alibaba_cloud` | Alibaba Cloud - ECS, OSS, RDS, SLB, VPC, etc. | [alibaba_cloud.md](./alibaba_cloud.md) |
|
||||||
|
| openstack | 18 | `mxgraph.openstack` | OpenStack cloud platform icons | [openstack.md](./openstack.md) |
|
||||||
|
| digitalocean | 74 | `mxgraph.digitalocean` | DigitalOcean - Droplets, Spaces, Kubernetes, etc. | [digitalocean.md](./digitalocean.md) |
|
||||||
|
| salesforce | 96 | `mxgraph.salesforce` | Salesforce platform icons | [salesforce.md](./salesforce.md) |
|
||||||
|
|
||||||
|
## Networking & Infrastructure
|
||||||
|
|
||||||
|
| Library | Shapes | Prefix | Description | File |
|
||||||
|
|---------|--------|--------|-------------|------|
|
||||||
|
| cisco19 | 232 | `mxgraph.cisco19` | Cisco network equipment - routers, switches, firewalls | [cisco19.md](./cisco19.md) |
|
||||||
|
| network | 58 | `mxgraph.networks` | General network diagram symbols | [network.md](./network.md) |
|
||||||
|
| arista | 45 | `mxgraph.arista` | Arista network switches and equipment | [arista.md](./arista.md) |
|
||||||
|
| kubernetes | 40 | `mxgraph.kubernetes` | Kubernetes - pods, services, deployments, nodes | [kubernetes.md](./kubernetes.md) |
|
||||||
|
| vvd | 93 | `mxgraph.vvd` | VMware Validated Design icons | [vvd.md](./vvd.md) |
|
||||||
|
| rack | 11 | `mxgraph.rack` | Server rack and data center equipment | [rack.md](./rack.md) |
|
||||||
|
|
||||||
|
## Business Process
|
||||||
|
|
||||||
|
| Library | Shapes | Prefix | Description | File |
|
||||||
|
|---------|--------|--------|-------------|------|
|
||||||
|
| bpmn | 39 | `mxgraph.bpmn` | Business Process Model and Notation - events, gateways, tasks | [bpmn.md](./bpmn.md) |
|
||||||
|
| eip | 36 | `mxgraph.eip` | Enterprise Integration Patterns - messaging, routing | [eip.md](./eip.md) |
|
||||||
|
| lean_mapping | 13 | `mxgraph.lean_mapping` | Lean/Value Stream Mapping symbols | [lean_mapping.md](./lean_mapping.md) |
|
||||||
|
|
||||||
|
## General Diagrams
|
||||||
|
|
||||||
|
| Library | Shapes | Prefix | Description | File |
|
||||||
|
|---------|--------|--------|-------------|------|
|
||||||
|
| flowchart | 34 | `mxgraph.flowchart` | Standard flowchart symbols - process, decision, data | [flowchart.md](./flowchart.md) |
|
||||||
|
| basic | 30 | `mxgraph.basic` | Basic shapes - stars, banners, callouts, hearts | [basic.md](./basic.md) |
|
||||||
|
| arrows2 | 34 | `mxgraph.arrows2` | Arrow shapes and connectors | [arrows2.md](./arrows2.md) |
|
||||||
|
| infographic | 29 | `mxgraph.infographic` | Infographic elements - charts, icons, badges | [infographic.md](./infographic.md) |
|
||||||
|
| sitemap | 50 | `mxgraph.sitemap` | Website sitemap icons - pages, forms, navigation | [sitemap.md](./sitemap.md) |
|
||||||
|
|
||||||
|
## UI/Mockups
|
||||||
|
|
||||||
|
| Library | Shapes | Prefix | Description | File |
|
||||||
|
|---------|--------|--------|-------------|------|
|
||||||
|
| android | 17 | `mxgraph.android` | Android UI mockup components | [android.md](./android.md) |
|
||||||
|
|
||||||
|
## Enterprise Software
|
||||||
|
|
||||||
|
| Library | Shapes | Prefix | Description | File |
|
||||||
|
|---------|--------|--------|-------------|------|
|
||||||
|
| citrix | 97 | `mxgraph.citrix` | Citrix virtualization - XenApp, XenDesktop, NetScaler | [citrix.md](./citrix.md) |
|
||||||
|
| sap | 98 | `mxgraph.sap` | SAP enterprise software icons | [sap.md](./sap.md) |
|
||||||
|
| mscae | 73 | `mxgraph.mscae` | Microsoft Cloud and Enterprise symbols | [mscae.md](./mscae.md) |
|
||||||
|
| atlassian | 26 | `mxgraph.atlassian` | Atlassian - Jira, Confluence issue types | [atlassian.md](./atlassian.md) |
|
||||||
|
|
||||||
|
## Engineering
|
||||||
|
|
||||||
|
| Library | Shapes | Prefix | Description | File |
|
||||||
|
|---------|--------|--------|-------------|------|
|
||||||
|
| fluidpower | 246 | `mxgraph.fluid_power` | Hydraulic/pneumatic engineering symbols | [fluidpower.md](./fluidpower.md) |
|
||||||
|
| electrical | 50 | `mxgraph.electrical` | Electrical circuit symbols - resistors, capacitors | [electrical.md](./electrical.md) |
|
||||||
|
| pid | 18 | `mxgraph.pid2` | Piping and Instrumentation Diagram symbols | [pid.md](./pid.md) |
|
||||||
|
| cabinets | 53 | `mxgraph.cabinets` | Electrical cabinet components - breakers, terminals | [cabinets.md](./cabinets.md) |
|
||||||
|
| floorplan | 44 | `mxgraph.floorplan` | Floor plan furniture and fixtures | [floorplan.md](./floorplan.md) |
|
||||||
|
|
||||||
|
## Icons & Graphics
|
||||||
|
|
||||||
|
| Library | Shapes | Prefix | Description | File |
|
||||||
|
|---------|--------|--------|-------------|------|
|
||||||
|
| webicons | 176 | `mxgraph.webicons` | Web/social media logos - GitHub, Twitter, AWS, etc. | [webicons.md](./webicons.md) |
|
||||||
|
| un-ocha-icons | 242 | `mxgraph.un-ocha-icons` | UN OCHA humanitarian icons | [un-ocha-icons.md](./un-ocha-icons.md) |
|
||||||
|
|
||||||
|
**Total: 33 libraries, 4,281 shapes**
|
||||||
328
docs/shape-libraries/alibaba_cloud.md
Normal file
328
docs/shape-libraries/alibaba_cloud.md
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
# alibaba_cloud
|
||||||
|
|
||||||
|
**Type:** mxgraph shapes
|
||||||
|
**Prefix:** `mxgraph.alibaba_cloud`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mxCell value="label" style="shape=mxgraph.alibaba_cloud.{shape};fillColor=#FF6A00;strokeColor=none;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Shapes (311)
|
||||||
|
|
||||||
|
- `abap_business_application_platform`
|
||||||
|
- `acms_application_configuration_manangement`
|
||||||
|
- `acr_cloud_container_registry`
|
||||||
|
- `actiontrail`
|
||||||
|
- `adam_advanced_database_and_application_migration`
|
||||||
|
- `adb_analyticdb_for_mysql`
|
||||||
|
- `address_purification`
|
||||||
|
- `afs_fraud_service`
|
||||||
|
- `agw_aligateway`
|
||||||
|
- `ahas_application_high_availability_service`
|
||||||
|
- `airec_artificial_intelligence_recommendation`
|
||||||
|
- `alb_application_load_balancer_01`
|
||||||
|
- `alb_application_load_balancer_02`
|
||||||
|
- `alibaba_cloud_logo`
|
||||||
|
- `alibaba_cloud_logo_chinese`
|
||||||
|
- `alibaba_cloud_logo_english`
|
||||||
|
- `alimail`
|
||||||
|
- `alimt_machine_translation`
|
||||||
|
- `aliyun_linux`
|
||||||
|
- `amqp_advanced_message_queuing_protocol`
|
||||||
|
- `amscloudapp`
|
||||||
|
- `analyticdb_for_postgresql`
|
||||||
|
- `antibot`
|
||||||
|
- `apigateway`
|
||||||
|
- `apsara_file_storage_for_hdfs`
|
||||||
|
- `apsaravideo_vod`
|
||||||
|
- `arms_application_real-time_monitoring_service`
|
||||||
|
- `ask_ack_container_service_for_kubernetes`
|
||||||
|
- `asm_service_mesh`
|
||||||
|
- `assettech`
|
||||||
|
- `avds_vulnerability_db_scanning`
|
||||||
|
- `baas_blockchain_as_a_service`
|
||||||
|
- `bandwidth_bag`
|
||||||
|
- `bastionhost`
|
||||||
|
- `batchcompute`
|
||||||
|
- `bccluster`
|
||||||
|
- `beebot`
|
||||||
|
- `beian`
|
||||||
|
- `bizdevops`
|
||||||
|
- `bizworks`
|
||||||
|
- `bpstudio`
|
||||||
|
- `cas_ssl_central_authentication_service`
|
||||||
|
- `cassandra_wide-column_database_01`
|
||||||
|
- `cassandra_wide-column_database_02`
|
||||||
|
- `ccc_cloud_call_center`
|
||||||
|
- `ccn_cloud_connect_network`
|
||||||
|
- `ccs_customer_service_01`
|
||||||
|
- `ccs_customer_service_02`
|
||||||
|
- `cddc_cloud_database_dedicated_cluster`
|
||||||
|
- `cdn_content_distribution_network`
|
||||||
|
- `cdp_cloudera_cdp`
|
||||||
|
- `cdt_cloud_datatransfer`
|
||||||
|
- `cen_cloud_enterprise_network`
|
||||||
|
- `cfw_cloud_firewall`
|
||||||
|
- `cityvisual`
|
||||||
|
- `clb_classic_load_balancer_01`
|
||||||
|
- `clb_classic_load_balancer_02`
|
||||||
|
- `clickhouse`
|
||||||
|
- `cloud_auth`
|
||||||
|
- `cloud_config`
|
||||||
|
- `cloud_display`
|
||||||
|
- `cloud_governance_center`
|
||||||
|
- `cloud_security_center`
|
||||||
|
- `cloud_shield`
|
||||||
|
- `cloudap`
|
||||||
|
- `cloudbox`
|
||||||
|
- `clouddesktop`
|
||||||
|
- `clouddev`
|
||||||
|
- `cloudphoto`
|
||||||
|
- `cloudproc`
|
||||||
|
- `cloudshell`
|
||||||
|
- `cmn_cloud_managed_network`
|
||||||
|
- `cmp_cloud_mobile_push`
|
||||||
|
- `cms_cloud_monitor_service`
|
||||||
|
- `codepipeline`
|
||||||
|
- `codestore`
|
||||||
|
- `companyreg`
|
||||||
|
- `computenest`
|
||||||
|
- `content_security`
|
||||||
|
- `coo`
|
||||||
|
- `cpns_cell_phone_number_service`
|
||||||
|
- `csas_cloud_security_access_service`
|
||||||
|
- `cvc_cloud_video_conferencing`
|
||||||
|
- `cwh_cloud_web_hosting`
|
||||||
|
- `das_database_autonomy_service`
|
||||||
|
- `databot`
|
||||||
|
- `datahub`
|
||||||
|
- `dataphin`
|
||||||
|
- `dataquotient`
|
||||||
|
- `datav`
|
||||||
|
- `dataworks_dataide`
|
||||||
|
- `dbaudit`
|
||||||
|
- `dbes_database_expert_service`
|
||||||
|
- `dbfs_database_file_system`
|
||||||
|
- `dbs_database_backup`
|
||||||
|
- `dcdn_dynamic_route_for_cdn`
|
||||||
|
- `ddh_dedicated_host`
|
||||||
|
- `ddos-bgp`
|
||||||
|
- `ddos-dip`
|
||||||
|
- `ddos-pro`
|
||||||
|
- `ddos_protection`
|
||||||
|
- `devops`
|
||||||
|
- `dg_database_gateway`
|
||||||
|
- `directmail`
|
||||||
|
- `disk_block_storage`
|
||||||
|
- `dlf_data_lake_formation`
|
||||||
|
- `dms_data_management_service`
|
||||||
|
- `dns_domain_name_system`
|
||||||
|
- `dns_privatezone_01`
|
||||||
|
- `dns_privatezone_02`
|
||||||
|
- `domain`
|
||||||
|
- `domain_and_website`
|
||||||
|
- `drds_distribute_relational_database_service`
|
||||||
|
- `dsi_data_security_insurance`
|
||||||
|
- `dts_data_transmission_service`
|
||||||
|
- `e-mapreduce`
|
||||||
|
- `eais_elastic_accelerated_computing_instances`
|
||||||
|
- `eci_elastic_container_instance`
|
||||||
|
- `ecs_elastic_compute_service`
|
||||||
|
- `edas_enterprise_distributed_application_service`
|
||||||
|
- `ehpc_elastic_high_performance_computing`
|
||||||
|
- `eip_elastic_ip_address`
|
||||||
|
- `elastic_web_hosting`
|
||||||
|
- `elasticsearch`
|
||||||
|
- `emas_enterprise_mobile_application_studio`
|
||||||
|
- `energyexpert`
|
||||||
|
- `ens_edge_node_service`
|
||||||
|
- `enterprise_website`
|
||||||
|
- `eprofile`
|
||||||
|
- `esign`
|
||||||
|
- `ess_elastic_scaling_service`
|
||||||
|
- `eventbridge`
|
||||||
|
- `express_connect`
|
||||||
|
- `face_recognition`
|
||||||
|
- `fc_function_compute`
|
||||||
|
- `flow_service`
|
||||||
|
- `flowbag`
|
||||||
|
- `fnf_serverless_function_flow`
|
||||||
|
- `fpga_field_programmable_gate_array`
|
||||||
|
- `fraud_detection`
|
||||||
|
- `ga_global_accelerator`
|
||||||
|
- `gameshield`
|
||||||
|
- `gdb_graph_database`
|
||||||
|
- `graphanalytics`
|
||||||
|
- `graphcompute`
|
||||||
|
- `gtm_global_traffic_manager`
|
||||||
|
- `gts_global_transaction_service`
|
||||||
|
- `gws_graphic_workstation`
|
||||||
|
- `havip_high-availability_virtual_ip_address`
|
||||||
|
- `hbase`
|
||||||
|
- `hbr_hybrid_backup_recovery`
|
||||||
|
- `hcs-hgw_hybrid_cloud_storage_array`
|
||||||
|
- `hcs-mgw_hybrid_cloud_storage_datatransport`
|
||||||
|
- `hcs-sgw_hybrid_cloud_storage_gateway`
|
||||||
|
- `hdr_hybrid_disaster_recovery`
|
||||||
|
- `hologres`
|
||||||
|
- `holowatcher`
|
||||||
|
- `hsm_hardware_security_module`
|
||||||
|
- `httpdns`
|
||||||
|
- `idrsservice`
|
||||||
|
- `image_recognition`
|
||||||
|
- `imagesearch`
|
||||||
|
- `imarketing`
|
||||||
|
- `imm_intelligent_media_management`
|
||||||
|
- `imp_intelligent_media_production`
|
||||||
|
- `imp_low_code_video_factory`
|
||||||
|
- `indvi_industrial_visual_intelligence`
|
||||||
|
- `intelligent_advisor`
|
||||||
|
- `iot_internet_of_things_platform`
|
||||||
|
- `iot_wireless_connection_service`
|
||||||
|
- `iotid_identity`
|
||||||
|
- `iov_iot_vehicle_cloud`
|
||||||
|
- `ipv6_gateway`
|
||||||
|
- `isoc_iot_security_operations_center`
|
||||||
|
- `isu_intelligent_semantic_understanding`
|
||||||
|
- `ivision`
|
||||||
|
- `ivpd_intelligent_visual_production`
|
||||||
|
- `kafka`
|
||||||
|
- `linkedmall`
|
||||||
|
- `linkwan`
|
||||||
|
- `live`
|
||||||
|
- `livinglink`
|
||||||
|
- `log_streaming`
|
||||||
|
- `logic_composer`
|
||||||
|
- `machine_learning`
|
||||||
|
- `man_mobile_analytics`
|
||||||
|
- `mariadb`
|
||||||
|
- `mas_mobile_acceleration_service`
|
||||||
|
- `maxcompute`
|
||||||
|
- `memcache`
|
||||||
|
- `miniappdev`
|
||||||
|
- `mns_message_service`
|
||||||
|
- `mobile_hotfix`
|
||||||
|
- `mobsec`
|
||||||
|
- `mongodb`
|
||||||
|
- `mps-ai`
|
||||||
|
- `mps-censor`
|
||||||
|
- `mps-cover`
|
||||||
|
- `mps-dna`
|
||||||
|
- `mps-multimod`
|
||||||
|
- `mps-produce`
|
||||||
|
- `mps_apsaravideo_media_processing`
|
||||||
|
- `mq_message_queue`
|
||||||
|
- `mqc_mobile_quality_center`
|
||||||
|
- `mse_microservices_engine`
|
||||||
|
- `multi-cloud_finops`
|
||||||
|
- `multi-mode_database_lindorm`
|
||||||
|
- `multimediaai`
|
||||||
|
- `mxgraph.alibaba_cloud`
|
||||||
|
- `mysql`
|
||||||
|
- `nas_network_attached_storage`
|
||||||
|
- `nat_gateway`
|
||||||
|
- `network_acl_access_control_list`
|
||||||
|
- `nlb_network_load_balancer_01`
|
||||||
|
- `nlb_network_load_balancer_02`
|
||||||
|
- `nlp-address`
|
||||||
|
- `nlp-automl`
|
||||||
|
- `nlp-ie_text_information_extraction`
|
||||||
|
- `nlp-ke_keyword_extraction`
|
||||||
|
- `nlp-ner_named_entity_recognition`
|
||||||
|
- `nlp-pos_part-of-speech_tagging`
|
||||||
|
- `nlp-ra_reflexive_anaphora`
|
||||||
|
- `nlp-sa_sentiment_analysis`
|
||||||
|
- `nlp-tc_text_categorization`
|
||||||
|
- `nlp-ws_word_segmentation`
|
||||||
|
- `nlp_natural_language_processing`
|
||||||
|
- `nls`
|
||||||
|
- `nls-asrbag`
|
||||||
|
- `nls-asrcustommodel`
|
||||||
|
- `nls-filebag`
|
||||||
|
- `nls-service`
|
||||||
|
- `nls-shortasrbag`
|
||||||
|
- `nls-ttsbag`
|
||||||
|
- `nodejs_performance_platform`
|
||||||
|
- `oceanbase`
|
||||||
|
- `ocr_optical_character_recognition`
|
||||||
|
- `onsmqtt_micro_message_queuing_telemetry_transport`
|
||||||
|
- `oos_operation_orchestration_service`
|
||||||
|
- `openanalytics`
|
||||||
|
- `openapi_explorer`
|
||||||
|
- `opensearch`
|
||||||
|
- `oss_object_storage_service`
|
||||||
|
- `ots_tablestore`
|
||||||
|
- `outboundbot`
|
||||||
|
- `pcdn_p2p_cdn`
|
||||||
|
- `petadata_hybriddb_for_mysql`
|
||||||
|
- `physical_connection`
|
||||||
|
- `pnvs_phone_number_verification_service`
|
||||||
|
- `polardb`
|
||||||
|
- `porana_portrait_analysis`
|
||||||
|
- `postgresql`
|
||||||
|
- `ppas_pay-as-you-go_database`
|
||||||
|
- `privatelink`
|
||||||
|
- `prometheus`
|
||||||
|
- `prophet`
|
||||||
|
- `pts_performance_test_service`
|
||||||
|
- `quickbi`
|
||||||
|
- `ram_resource_access_management`
|
||||||
|
- `re_recommendation_engine`
|
||||||
|
- `realtime_compute`
|
||||||
|
- `redis_kvstore`
|
||||||
|
- `region`
|
||||||
|
- `retailir`
|
||||||
|
- `ros_resource_orchestration_service`
|
||||||
|
- `route_table`
|
||||||
|
- `router`
|
||||||
|
- `rsimganalys`
|
||||||
|
- `rtc_real-time_communication`
|
||||||
|
- `sae_serverless_app_engine`
|
||||||
|
- `sag_smart_access_gateway_01`
|
||||||
|
- `sag_smart_access_gateway_02`
|
||||||
|
- `sas_situational_awareness`
|
||||||
|
- `sca_smart_conversation_analysis_01`
|
||||||
|
- `sca_smart_conversation_analysis_02`
|
||||||
|
- `scc_super_computing_cluster`
|
||||||
|
- `scdn_secure_cdn`
|
||||||
|
- `scu_storage_capacity_unit`
|
||||||
|
- `sddp_sensitive_data_protection`
|
||||||
|
- `shared_bandwidth`
|
||||||
|
- `shared_flow_bag`
|
||||||
|
- `shc_shield_hybrid_cloud`
|
||||||
|
- `slb_server_load_balancer_01`
|
||||||
|
- `slb_server_load_balancer_02`
|
||||||
|
- `slb_server_load_balancer_03`
|
||||||
|
- `sls_simple_log_service`
|
||||||
|
- `smc_server_migration_center`
|
||||||
|
- `sms_short_message_service`
|
||||||
|
- `sos`
|
||||||
|
- `spark_data_insights`
|
||||||
|
- `sppc`
|
||||||
|
- `sqlserver`
|
||||||
|
- `swas_simple_application_server`
|
||||||
|
- `tr_transit_router`
|
||||||
|
- `trademark_service`
|
||||||
|
- `uis_ultimate_internet_service`
|
||||||
|
- `user`
|
||||||
|
- `user_feedback_01`
|
||||||
|
- `user_feedback_02`
|
||||||
|
- `vbr_virtual_border_router`
|
||||||
|
- `vcs_visual_computing_service`
|
||||||
|
- `vms_voice_messaging_service`
|
||||||
|
- `voicebot_intelligent_voice_navigation`
|
||||||
|
- `vpc_virtual_private_cloud`
|
||||||
|
- `vpn_gateway`
|
||||||
|
- `vs_video_surveillance`
|
||||||
|
- `vswitch`
|
||||||
|
- `waf_web_application_firewall`
|
||||||
|
- `webplus_web_app_service`
|
||||||
|
- `xdragon_bare_metal_server`
|
||||||
|
- `xtrace`
|
||||||
|
- `yida`
|
||||||
62
docs/shape-libraries/android.md
Normal file
62
docs/shape-libraries/android.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# android
|
||||||
|
|
||||||
|
**Type:** mxgraph shapes
|
||||||
|
**Prefix:** `mxgraph.android`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mxCell value="label" style="shape=mxgraph.android.phone2;strokeColor=#c0c0c0;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="0" y="0" width="200" height="390" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Shapes (47)
|
||||||
|
|
||||||
|
- `action_bar`
|
||||||
|
- `action_bar_landscape`
|
||||||
|
- `anchor`
|
||||||
|
- `checkbox`
|
||||||
|
- `contact_badge_focused`
|
||||||
|
- `contextual_action_bar`
|
||||||
|
- `contextual_action_bar_landscape`
|
||||||
|
- `contextual_split_action_bar`
|
||||||
|
- `contextual_split_action_bar_landscape`
|
||||||
|
- `contextual_split_action_bar_landscape_white`
|
||||||
|
- `indeterminateSpinner`
|
||||||
|
- `indeterminate_progress_bar`
|
||||||
|
- `keyboard`
|
||||||
|
- `navigation_bar_1`
|
||||||
|
- `navigation_bar_1_landscape`
|
||||||
|
- `navigation_bar_1_vertical`
|
||||||
|
- `navigation_bar_2`
|
||||||
|
- `navigation_bar_3`
|
||||||
|
- `navigation_bar_3_landscape`
|
||||||
|
- `navigation_bar_4`
|
||||||
|
- `navigation_bar_5`
|
||||||
|
- `navigation_bar_5_vertical`
|
||||||
|
- `navigation_bar_6`
|
||||||
|
- `phone2`
|
||||||
|
- `progressBar`
|
||||||
|
- `progressScrubberDisabled`
|
||||||
|
- `progressScrubberFocused`
|
||||||
|
- `progressScrubberPressed`
|
||||||
|
- `quick_contact`
|
||||||
|
- `quickscroll2`
|
||||||
|
- `quickscroll3`
|
||||||
|
- `rect`
|
||||||
|
- `rrect`
|
||||||
|
- `scrollbars2`
|
||||||
|
- `spinner2`
|
||||||
|
- `split_action_bar`
|
||||||
|
- `split_action_bar_landscape`
|
||||||
|
- `statusBar`
|
||||||
|
- `switch_off`
|
||||||
|
- `switch_on`
|
||||||
|
- `tab2`
|
||||||
|
- `textSelHandles`
|
||||||
|
- `text_insertion_point`
|
||||||
|
- `textfield`
|
||||||
|
- `time_picker`
|
||||||
|
- `time_picker_dark`
|
||||||
|
- `transparent`
|
||||||
33
docs/shape-libraries/arrows2.md
Normal file
33
docs/shape-libraries/arrows2.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# arrows2
|
||||||
|
|
||||||
|
**Type:** mxgraph shapes
|
||||||
|
**Prefix:** `mxgraph.arrows2`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mxCell value="label" style="shape=mxgraph.arrows2.arrow;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="0" y="0" width="100" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Shapes (18)
|
||||||
|
|
||||||
|
- `arrow`
|
||||||
|
- `bendArrow`
|
||||||
|
- `bendDoubleArrow`
|
||||||
|
- `calloutArrow`
|
||||||
|
- `calloutDouble90Arrow`
|
||||||
|
- `calloutDoubleArrow`
|
||||||
|
- `calloutQuadArrow`
|
||||||
|
- `jumpInArrow`
|
||||||
|
- `quadArrow`
|
||||||
|
- `sharpArrow`
|
||||||
|
- `sharpArrow2`
|
||||||
|
- `stripedArrow`
|
||||||
|
- `stylisedArrow`
|
||||||
|
- `tailedArrow`
|
||||||
|
- `tailedNotchedArrow`
|
||||||
|
- `triadArrow`
|
||||||
|
- `twoWayArrow`
|
||||||
|
- `uTurnArrow`
|
||||||
32
docs/shape-libraries/atlassian.md
Normal file
32
docs/shape-libraries/atlassian.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# atlassian
|
||||||
|
|
||||||
|
**Type:** SVG images
|
||||||
|
**Path:** `img/lib/atlassian/`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mxCell value="label" style="image;aspect=fixed;image=img/lib/atlassian/Jira_Logo.svg;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Shapes (17)
|
||||||
|
|
||||||
|
- `Atlassian_Logo`
|
||||||
|
- `Bamboo_Logo`
|
||||||
|
- `Bitbucket_Logo`
|
||||||
|
- `Clover_Logo`
|
||||||
|
- `Confluence_Logo`
|
||||||
|
- `Crowd_Logo`
|
||||||
|
- `Crucible_Logo`
|
||||||
|
- `Fisheye_Logo`
|
||||||
|
- `Hipchat_Logo`
|
||||||
|
- `Jira_Core_Logo`
|
||||||
|
- `Jira_Logo`
|
||||||
|
- `Jira_Service_Desk_Logo`
|
||||||
|
- `Jira_Software_Logo`
|
||||||
|
- `Sourcetree_Logo`
|
||||||
|
- `Statuspage_Logo`
|
||||||
|
- `Stride_Logo`
|
||||||
|
- `Trello_Logo`
|
||||||
1049
docs/shape-libraries/aws4.md
Normal file
1049
docs/shape-libraries/aws4.md
Normal file
File diff suppressed because it is too large
Load Diff
431
docs/shape-libraries/azure2.md
Normal file
431
docs/shape-libraries/azure2.md
Normal file
@@ -0,0 +1,431 @@
|
|||||||
|
# azure2
|
||||||
|
|
||||||
|
**Type:** SVG images
|
||||||
|
**Path:** `img/lib/azure2/`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mxCell value="label" style="image;aspect=fixed;image=img/lib/azure2/compute/Virtual_Machine.svg;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Shapes (648)
|
||||||
|
|
||||||
|
Shapes are organized by category: `azure2/{category}/{shape}.svg`
|
||||||
|
|
||||||
|
### ai_machine_learning (30)
|
||||||
|
|
||||||
|
- `AI_Studio`
|
||||||
|
- `Anomaly_Detector`
|
||||||
|
- `Azure_Applied_AI`
|
||||||
|
- `Azure_Experimentation_Studio`
|
||||||
|
- `Azure_Object_Understanding`
|
||||||
|
- `Azure_OpenAI`
|
||||||
|
- `Batch_AI`
|
||||||
|
- `Bonsai`
|
||||||
|
- `Bot_Services`
|
||||||
|
- `Cognitive_Services`
|
||||||
|
- `Cognitive_Services_Decisions`
|
||||||
|
- `Computer_Vision`
|
||||||
|
- `Content_Moderators`
|
||||||
|
- `Content_Safety`
|
||||||
|
- `Custom_Vision`
|
||||||
|
- `Face_APIs`
|
||||||
|
- `Form_Recognizers`
|
||||||
|
- `Genomics`
|
||||||
|
- `Immersive_Readers`
|
||||||
|
- `Language_Services`
|
||||||
|
- `Language_Understanding`
|
||||||
|
- `Machine_Learning`
|
||||||
|
- `Machine_Learning_Studio_Classic_Web_Services`
|
||||||
|
- `Machine_Learning_Studio_Web_Service_Plans`
|
||||||
|
- `Machine_Learning_Studio_Workspaces`
|
||||||
|
- `Personalizers`
|
||||||
|
- `QnA_Makers`
|
||||||
|
- `Serverless_Search`
|
||||||
|
- `Speech_Services`
|
||||||
|
- `Translator_Text`
|
||||||
|
|
||||||
|
### analytics (14)
|
||||||
|
|
||||||
|
- `Analysis_Services`
|
||||||
|
- `Azure_Databricks`
|
||||||
|
- `Azure_Synapse_Analytics`
|
||||||
|
- `Azure_Workbooks`
|
||||||
|
- `Data_Lake_Analytics`
|
||||||
|
- `Data_Lake_Store_Gen1`
|
||||||
|
- `Endpoint_Analytics`
|
||||||
|
- `Event_Hub_Clusters`
|
||||||
|
- `Event_Hubs`
|
||||||
|
- `HD_Insight_Clusters`
|
||||||
|
- `Log_Analytics_Workspaces`
|
||||||
|
- `Power_BI_Embedded`
|
||||||
|
- `Power_Platform`
|
||||||
|
- `Stream_Analytics_Jobs`
|
||||||
|
|
||||||
|
### app_services (9)
|
||||||
|
|
||||||
|
- `API_Management_Services`
|
||||||
|
- `App_Service_Certificates`
|
||||||
|
- `App_Service_Domains`
|
||||||
|
- `App_Service_Environments`
|
||||||
|
- `App_Service_Plans`
|
||||||
|
- `App_Services`
|
||||||
|
- `CDN_Profiles`
|
||||||
|
- `Notification_Hubs`
|
||||||
|
- `Search_Services`
|
||||||
|
|
||||||
|
### compute (38)
|
||||||
|
|
||||||
|
- `App_Services`
|
||||||
|
- `Application_Group`
|
||||||
|
- `Automanaged_VM`
|
||||||
|
- `Availability_Sets`
|
||||||
|
- `Azure_Compute_Galleries`
|
||||||
|
- `Azure_Spring_Cloud`
|
||||||
|
- `Batch_Accounts`
|
||||||
|
- `Cloud_Services_Classic`
|
||||||
|
- `Container_Instances`
|
||||||
|
- `Container_Services_Deprecated`
|
||||||
|
- `Disk_Encryption_Sets`
|
||||||
|
- `Disks`
|
||||||
|
- `Disks_Classic`
|
||||||
|
- `Disks_Snapshots`
|
||||||
|
- `Function_Apps`
|
||||||
|
- `Host_Groups`
|
||||||
|
- `Host_Pools`
|
||||||
|
- `Hosts`
|
||||||
|
- `Image_Definitions`
|
||||||
|
- `Image_Templates`
|
||||||
|
- `Image_Versions`
|
||||||
|
- `Images`
|
||||||
|
- `Kubernetes_Services`
|
||||||
|
- `Maintenance_Configuration`
|
||||||
|
- `Managed_Service_Fabric`
|
||||||
|
- `Mesh_Applications`
|
||||||
|
- `Metrics_Advisor`
|
||||||
|
- `OS_Images_Classic`
|
||||||
|
- `Restore_Points`
|
||||||
|
- `Restore_Points_Collections`
|
||||||
|
- `Service_Fabric_Clusters`
|
||||||
|
- `Shared_Image_Galleries`
|
||||||
|
- `VM_Images_Classic`
|
||||||
|
- `VM_Scale_Sets`
|
||||||
|
- `Virtual_Machine`
|
||||||
|
- `Virtual_Machines_Classic`
|
||||||
|
- `Workspaces`
|
||||||
|
- `Workspaces2`
|
||||||
|
|
||||||
|
### containers (7)
|
||||||
|
|
||||||
|
- `App_Services`
|
||||||
|
- `Azure_Red_Hat_OpenShift`
|
||||||
|
- `Batch_Accounts`
|
||||||
|
- `Container_Instances`
|
||||||
|
- `Container_Registries`
|
||||||
|
- `Kubernetes_Services`
|
||||||
|
- `Service_Fabric_Clusters`
|
||||||
|
|
||||||
|
### databases (27)
|
||||||
|
|
||||||
|
- `Azure_Cosmos_DB`
|
||||||
|
- `Azure_Data_Explorer_Clusters`
|
||||||
|
- `Azure_Database_MariaDB_Server`
|
||||||
|
- `Azure_Database_Migration_Services`
|
||||||
|
- `Azure_Database_MySQL_Server`
|
||||||
|
- `Azure_Database_PostgreSQL_Server`
|
||||||
|
- `Azure_Database_PostgreSQL_Server_Group`
|
||||||
|
- `Azure_Purview_Accounts`
|
||||||
|
- `Azure_SQL`
|
||||||
|
- `Azure_SQL_Edge`
|
||||||
|
- `Azure_SQL_Server_Stretch_Databases`
|
||||||
|
- `Azure_SQL_VM`
|
||||||
|
- `Azure_Synapse_Analytics`
|
||||||
|
- `Cache_Redis`
|
||||||
|
- `Data_Factory`
|
||||||
|
- `Elastic_Job_Agents`
|
||||||
|
- `Instance_Pools`
|
||||||
|
- `Managed_Database`
|
||||||
|
- `Oracle_Database`
|
||||||
|
- `SQL_Data_Warehouses`
|
||||||
|
- `SQL_Database`
|
||||||
|
- `SQL_Elastic_Pools`
|
||||||
|
- `SQL_Managed_Instance`
|
||||||
|
- `SQL_Server`
|
||||||
|
- `SQL_Server_Registries`
|
||||||
|
- `SSIS_Lift_And_Shift_IR`
|
||||||
|
- `Virtual_Clusters`
|
||||||
|
|
||||||
|
### identity (35)
|
||||||
|
|
||||||
|
- `AAD_Licenses`
|
||||||
|
- `Active_Directory_Connect_Health`
|
||||||
|
- `Active_Directory_Connect_Health2`
|
||||||
|
- `Administrative_Units`
|
||||||
|
- `App_Registrations`
|
||||||
|
- `Azure_AD_B2C`
|
||||||
|
- `Azure_AD_B2C2`
|
||||||
|
- `Azure_AD_Domain_Services`
|
||||||
|
- `Azure_AD_Identity_Protection`
|
||||||
|
- `Azure_AD_Privilege_Identity_Management`
|
||||||
|
- `Azure_Active_Directory`
|
||||||
|
- `Azure_Information_Protection`
|
||||||
|
- `Custom_Azure_AD_Roles`
|
||||||
|
- `Enterprise_Applications`
|
||||||
|
- `Entra_Connect`
|
||||||
|
- `Entra_Domain_Services`
|
||||||
|
- `Entra_Global_Secure_Access`
|
||||||
|
- `Entra_ID_Protection`
|
||||||
|
- `Entra_Internet_Access`
|
||||||
|
- `Entra_Managed_Identities`
|
||||||
|
- `Entra_Private_Access`
|
||||||
|
- `Entra_Privileged_Identity_Management`
|
||||||
|
- `Entra_Verified_ID`
|
||||||
|
- `External_Identities`
|
||||||
|
- `Groups`
|
||||||
|
- `Identity_Governance`
|
||||||
|
- `Managed_Identities`
|
||||||
|
- `Multi_Factor_Authentication`
|
||||||
|
- `PIM`
|
||||||
|
- `Security`
|
||||||
|
- `Tenant_Properties`
|
||||||
|
- `User_Settings`
|
||||||
|
- `Users`
|
||||||
|
- `Verifiable_Credentials`
|
||||||
|
- `Verification_As_A_Service`
|
||||||
|
|
||||||
|
### networking (51)
|
||||||
|
|
||||||
|
- `ATM_Multistack`
|
||||||
|
- `Application_Gateway_Containers`
|
||||||
|
- `Application_Gateways`
|
||||||
|
- `Azure_Communications_Gateway`
|
||||||
|
- `Azure_Firewall_Manager`
|
||||||
|
- `Azure_Firewall_Policy`
|
||||||
|
- `Bastions`
|
||||||
|
- `CDN_Profiles`
|
||||||
|
- `Connections`
|
||||||
|
- `DDoS_Protection_Plans`
|
||||||
|
- `DNS_Multistack`
|
||||||
|
- `DNS_Private_Resolver`
|
||||||
|
- `DNS_Security_Policy`
|
||||||
|
- `DNS_Zones`
|
||||||
|
- `ExpressRoute_Circuits`
|
||||||
|
- `Firewalls`
|
||||||
|
- `Front_Doors`
|
||||||
|
- `IP_Address_manager`
|
||||||
|
- `IP_Groups`
|
||||||
|
- `Load_Balancer_Hub`
|
||||||
|
- `Load_Balancers`
|
||||||
|
- `Local_Network_Gateways`
|
||||||
|
- `NAT`
|
||||||
|
- `Network_Interfaces`
|
||||||
|
- `Network_Security_Groups`
|
||||||
|
- `Network_Watcher`
|
||||||
|
- `On_Premises_Data_Gateways`
|
||||||
|
- `Private_Endpoint`
|
||||||
|
- `Private_Link`
|
||||||
|
- `Private_Link_Hub`
|
||||||
|
- `Private_Link_Service`
|
||||||
|
- `Proximity_Placement_Groups`
|
||||||
|
- `Public_IP_Addresses`
|
||||||
|
- `Public_IP_Addresses_Classic`
|
||||||
|
- `Public_IP_Prefixes`
|
||||||
|
- `Reserved_IP_Addresses_Classic`
|
||||||
|
- `Resource_Management_Private_Link`
|
||||||
|
- `Route_Filters`
|
||||||
|
- `Route_Tables`
|
||||||
|
- `Service_Endpoint_Policies`
|
||||||
|
- `Spot_VM`
|
||||||
|
- `Spot_VMSS`
|
||||||
|
- `Subnet`
|
||||||
|
- `Traffic_Manager_Profiles`
|
||||||
|
- `Virtual_Network_Gateways`
|
||||||
|
- `Virtual_Networks`
|
||||||
|
- `Virtual_Networks_Classic`
|
||||||
|
- `Virtual_Router`
|
||||||
|
- `Virtual_WAN_Hub`
|
||||||
|
- `Virtual_WANs`
|
||||||
|
- `Web_Application_Firewall_Policies_WAF`
|
||||||
|
|
||||||
|
### security (14)
|
||||||
|
|
||||||
|
- `Application_Security_Groups`
|
||||||
|
- `Azure_AD_Risky_Signins`
|
||||||
|
- `Azure_AD_Risky_Users`
|
||||||
|
- `Azure_Defender`
|
||||||
|
- `Azure_Sentinel`
|
||||||
|
- `Conditional_Access`
|
||||||
|
- `Detonation`
|
||||||
|
- `ExtendedSecurityUpdates`
|
||||||
|
- `Identity_Secure_Score`
|
||||||
|
- `Key_Vaults`
|
||||||
|
- `Keys`
|
||||||
|
- `MS_Defender_EASM`
|
||||||
|
- `Multifactor_Authentication`
|
||||||
|
- `Security_Center`
|
||||||
|
|
||||||
|
### storage (17)
|
||||||
|
|
||||||
|
- `Azure_Fileshare`
|
||||||
|
- `Azure_HCP_Cache`
|
||||||
|
- `Azure_NetApp_Files`
|
||||||
|
- `Azure_Stack_Edge`
|
||||||
|
- `Data_Box`
|
||||||
|
- `Data_Box_Edge`
|
||||||
|
- `Data_Lake_Storage_Gen1`
|
||||||
|
- `Data_Share_Invitations`
|
||||||
|
- `Data_Shares`
|
||||||
|
- `Import_Export_Jobs`
|
||||||
|
- `Recovery_Services_Vaults`
|
||||||
|
- `StorSimple_Data_Managers`
|
||||||
|
- `StorSimple_Device_Managers`
|
||||||
|
- `Storage_Accounts`
|
||||||
|
- `Storage_Accounts_Classic`
|
||||||
|
- `Storage_Explorer`
|
||||||
|
- `Storage_Sync_Services`
|
||||||
|
|
||||||
|
### general (98)
|
||||||
|
|
||||||
|
- `All_Resources`
|
||||||
|
- `Backlog`
|
||||||
|
- `Biz_Talk`
|
||||||
|
- `Blob_Block`
|
||||||
|
- `Blob_Page`
|
||||||
|
- `Branch`
|
||||||
|
- `Browser`
|
||||||
|
- `Bug`
|
||||||
|
- `Builds`
|
||||||
|
- `Cache`
|
||||||
|
- `Code`
|
||||||
|
- `Commit`
|
||||||
|
- `Controls`
|
||||||
|
- `Controls_Horizontal`
|
||||||
|
- `Cost_Alerts`
|
||||||
|
- `Cost_Analysis`
|
||||||
|
- `Cost_Budgets`
|
||||||
|
- `Cost_Management`
|
||||||
|
- `Cost_Management_and_Billing`
|
||||||
|
- `Counter`
|
||||||
|
- `Cubes`
|
||||||
|
- `Dashboard`
|
||||||
|
- `Dashboard2`
|
||||||
|
- `Dev_Console`
|
||||||
|
- `Download`
|
||||||
|
- `Error`
|
||||||
|
- `Extensions`
|
||||||
|
- `FTP`
|
||||||
|
- `File`
|
||||||
|
- `Files`
|
||||||
|
- `Folder_Blank`
|
||||||
|
- `Folder_Website`
|
||||||
|
- `Free_Services`
|
||||||
|
- `Gear`
|
||||||
|
- `Globe`
|
||||||
|
- `Globe_Error`
|
||||||
|
- `Globe_Success`
|
||||||
|
- `Globe_Warning`
|
||||||
|
- `Guide`
|
||||||
|
- `Heart`
|
||||||
|
- `Help_and_Support`
|
||||||
|
- `Image`
|
||||||
|
- `Information`
|
||||||
|
- `Input_Output`
|
||||||
|
- `Journey_Hub`
|
||||||
|
- `Launch_Portal`
|
||||||
|
- `Learn`
|
||||||
|
- `Load_Test`
|
||||||
|
- `Location`
|
||||||
|
- `Log_Streaming`
|
||||||
|
- `Management_Groups`
|
||||||
|
- `Management_Portal`
|
||||||
|
- `Marketplace`
|
||||||
|
- `Media`
|
||||||
|
- `Media_File`
|
||||||
|
- `Mobile`
|
||||||
|
- `Mobile_Engagement`
|
||||||
|
- `Module`
|
||||||
|
- `Power`
|
||||||
|
- `Power_Up`
|
||||||
|
- `Powershell`
|
||||||
|
- `Preview`
|
||||||
|
- `Preview_Features`
|
||||||
|
- `Process_Explorer`
|
||||||
|
- `Production_Ready_Database`
|
||||||
|
- `Quickstart_Center`
|
||||||
|
- `Recent`
|
||||||
|
- `Reservations`
|
||||||
|
- `Resource_Explorer`
|
||||||
|
- `Resource_Group_List`
|
||||||
|
- `Resource_Groups`
|
||||||
|
- `Resource_Linked`
|
||||||
|
- `SSD`
|
||||||
|
- `Scale`
|
||||||
|
- `Scheduler`
|
||||||
|
- `Search`
|
||||||
|
- `Search_Grid`
|
||||||
|
- `Server_Farm`
|
||||||
|
- `Service_Bus`
|
||||||
|
- `Service_Health`
|
||||||
|
- `Storage_Azure_Files`
|
||||||
|
- `Storage_Container`
|
||||||
|
- `Storage_Queue`
|
||||||
|
- `Subscriptions`
|
||||||
|
- `TFS_VC_Repository`
|
||||||
|
- `Table`
|
||||||
|
- `Tag`
|
||||||
|
- `Tags`
|
||||||
|
- `Templates`
|
||||||
|
- `Toolbox`
|
||||||
|
- `Troubleshoot`
|
||||||
|
- `Versions`
|
||||||
|
- `Web_Slots`
|
||||||
|
- `Web_Test`
|
||||||
|
- `Website_Power`
|
||||||
|
- `Website_Staging`
|
||||||
|
- `Workbooks`
|
||||||
|
- `Workflow`
|
||||||
|
|
||||||
|
### other (149)
|
||||||
|
|
||||||
|
(See draw.io for complete list of 149 shapes in the "other" category)
|
||||||
|
|
||||||
|
Selected shapes:
|
||||||
|
- `Azure_Backup_Center`
|
||||||
|
- `Azure_Chaos_Studio`
|
||||||
|
- `Azure_Cloud_Shell`
|
||||||
|
- `Azure_Communication_Services`
|
||||||
|
- `Azure_Deployment_Environments`
|
||||||
|
- `Azure_Load_Testing`
|
||||||
|
- `Azure_Monitor_Dashboard`
|
||||||
|
- `Azure_Network_Manager`
|
||||||
|
- `Azure_Orbital`
|
||||||
|
- `Azure_Sphere`
|
||||||
|
- `Azure_Storage_Mover`
|
||||||
|
- `Grafana`
|
||||||
|
- `Kubernetes_Fleet_Manager`
|
||||||
|
- `SSH_Keys`
|
||||||
|
|
||||||
|
### Additional Categories
|
||||||
|
|
||||||
|
- **azure_ecosystem** (3): Applens, Azure_Hybrid_Center, Collaborative_Service
|
||||||
|
- **azure_stack** (8): Azure_Stack, Capacity, Infrastructure_Backup, Multi_Tenancy, Offers, Plans, Updates, User_Subscriptions
|
||||||
|
- **azure_vmware_solution** (1): AVS
|
||||||
|
- **blockchain** (6): ABS_Member, Azure_Blockchain_Service, Azure_Token_Service, Blockchain_Applications, Consortium, Outbound_Connection
|
||||||
|
- **cxp** (2): Elixir, Elixir_Purple
|
||||||
|
- **devops** (10): API_Connections, Application_Insights, Azure_DevOps, Change_Analysis, CloudTest, Code_Optimization, DevOps_Starter, DevTest_Labs, Lab_Accounts, Lab_Services
|
||||||
|
- **hybrid_multicloud** (5): Azure_Operator_5G_Core, Azure_Operator_Insights, Azure_Operator_Nexus, Azure_Operator_Service_Manager, Azure_Programmable_Connectivity
|
||||||
|
- **integration** (21): API_Management_Services, App_Configuration, Azure_API_for_FHIR, Azure_Data_Catalog, Event_Grid_Domains, Event_Grid_Subscriptions, Event_Grid_Topics, Integration_Accounts, Integration_Environments, Integration_Service_Environments, Logic_Apps, Logic_Apps_Custom_Connector, Partner_Namespace, Partner_Registration, Partner_Topic, Relays, SQL_Data_Warehouses, SendGrid_Accounts, Service_Bus, Software_as_a_Service, System_Topic
|
||||||
|
- **internet_of_things** (3): Digital_Twins, Logic_Apps, Time_Series_Insights_Access_Policies
|
||||||
|
- **intune** (17): Azure_AD_Roles_and_Administrators, Client_Apps, Device_Compliance, Device_Configuration, Device_Enrollment, Device_Security_Apple, Device_Security_Google, Device_Security_Windows, Devices, Exchange_Access, Intune, Intune_For_Education, Mindaro, Security_Baselines, Software_Updates, Tenant_Status, eBooks
|
||||||
|
- **iot** (19): Azure_IoT_Operations, Azure_Maps_Accounts, Azure_Stack_HCI_Sizer, Device_Provisioning_Services, Digital_Twins, Event_Hubs, Function_Apps, Industrial_IoT, IoT_Central_Applications, IoT_Edge, IoT_Hub, Logic_Apps, Notification_Hubs, Stack_HCI_Premium, Stream_Analytics_Jobs, Time_Series_Data_Sets, Time_Series_Insights_Environments, Time_Series_Insights_Event_Sources, Windows10_Core_Services
|
||||||
|
- **management_governance** (32): Activity_Log, Advisor, Alerts, Application_Insights, Arc_Machines, Automation_Accounts, Azure_Arc, Azure_Lighthouse, Blueprints, Compliance, Cost_Management_and_Billing, Customer_Lockbox_for_MS_Azure, Diagnostics_Settings, Education, Log_Analytics_Workspaces, MachinesAzureArc, Managed_Applications_Center, Managed_Desktop, Metrics, Monitor, My_Customers, Operation_Log_Classic, Policy, Recovery_Services_Vaults, Resource_Graph_Explorer, Resources_Provider, Scheduler_Job_Collections, Service_Catalog_MAD, Service_Providers, Solutions, Universal_Print, User_Privacy
|
||||||
|
- **menu** (1): Keys
|
||||||
|
- **migrate** (5): Azure_Migrate, Cost_Management_and_Billing, Data_Box, Data_Box_Edge, Recovery_Services_Vaults
|
||||||
|
- **mixed_reality** (2): Remote_Rendering, Spatial_Anchor_Accounts
|
||||||
|
- **monitor** (1): SAP_Azure_Monitor
|
||||||
|
- **power_platform** (9): AIBuilder, CopilotStudio, Dataverse, PowerApps, PowerAutomate, PowerBI, PowerFx, PowerPages, PowerPlatform
|
||||||
|
- **preview** (9): Azure_Cloud_Shell, Azure_Sphere, Azure_Workbooks, IoT_Edge, Private_Link_Hub, RTOS, Static_Apps, Time_Series_Data_Sets, Web_Environment
|
||||||
|
- **web** (5): API_Center, App_Space, Azure_Media_Service, Notification_Hub_Namespaces, SignalR
|
||||||
48
docs/shape-libraries/basic.md
Normal file
48
docs/shape-libraries/basic.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# basic
|
||||||
|
|
||||||
|
**Type:** mxgraph shapes
|
||||||
|
**Prefix:** `mxgraph.basic`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mxCell value="label" style="shape=mxgraph.basic.{shape};fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Shapes (31)
|
||||||
|
|
||||||
|
- `4_point_star`
|
||||||
|
- `6_point_star`
|
||||||
|
- `8_point_star`
|
||||||
|
- `banner`
|
||||||
|
- `cloud_callout`
|
||||||
|
- `cloud_rect`
|
||||||
|
- `cone`
|
||||||
|
- `cross`
|
||||||
|
- `document`
|
||||||
|
- `flash`
|
||||||
|
- `half_circle`
|
||||||
|
- `heart`
|
||||||
|
- `loud_callout`
|
||||||
|
- `moon`
|
||||||
|
- `mxgraph.basic`
|
||||||
|
- `no_symbol`
|
||||||
|
- `octagon`
|
||||||
|
- `orthogonal_triangle`
|
||||||
|
- `oval_callout`
|
||||||
|
- `parallelepiped`
|
||||||
|
- `pentagon`
|
||||||
|
- `pointed_oval`
|
||||||
|
- `rectangular_callout`
|
||||||
|
- `rounded_rectangular_callout`
|
||||||
|
- `smiley`
|
||||||
|
- `star`
|
||||||
|
- `sun`
|
||||||
|
- `tick`
|
||||||
|
- `trapezoid`
|
||||||
|
- `wave`
|
||||||
|
- `x`
|
||||||
60
docs/shape-libraries/bpmn.md
Normal file
60
docs/shape-libraries/bpmn.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# bpmn
|
||||||
|
|
||||||
|
**Type:** mxgraph shapes
|
||||||
|
**Prefix:** `mxgraph.bpmn`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mxCell value="label" style="shape=mxgraph.bpmn.shape;symbol=message;outline=throwing;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
- `outline` - Event type: `start`, `end`, `catching`, `throwing`, `none`
|
||||||
|
- `symbol` - Icon inside: `message`, `timer`, `error`, `cancel`, `compensation`, `link`, `terminate`, `general`, `multiple`, `rule`
|
||||||
|
|
||||||
|
## Shapes (40)
|
||||||
|
|
||||||
|
- `ad_hoc`
|
||||||
|
- `business_rule_task`
|
||||||
|
- `cancel_end`
|
||||||
|
- `cancel_intermediate`
|
||||||
|
- `compensation`
|
||||||
|
- `compensation_end`
|
||||||
|
- `compensation_intermediate`
|
||||||
|
- `error_end`
|
||||||
|
- `error_intermediate`
|
||||||
|
- `gateway`
|
||||||
|
- `gateway_and`
|
||||||
|
- `gateway_complex`
|
||||||
|
- `gateway_or`
|
||||||
|
- `gateway_xor_(data)`
|
||||||
|
- `gateway_xor_(event)`
|
||||||
|
- `general_end`
|
||||||
|
- `general_intermediate`
|
||||||
|
- `general_start`
|
||||||
|
- `link_end`
|
||||||
|
- `link_intermediate`
|
||||||
|
- `link_start`
|
||||||
|
- `loop`
|
||||||
|
- `loop_marker`
|
||||||
|
- `manual_task`
|
||||||
|
- `message_end`
|
||||||
|
- `message_intermediate`
|
||||||
|
- `message_start`
|
||||||
|
- `multiple_end`
|
||||||
|
- `multiple_instances`
|
||||||
|
- `multiple_intermediate`
|
||||||
|
- `multiple_start`
|
||||||
|
- `mxgraph.bpmn`
|
||||||
|
- `rule_intermediate`
|
||||||
|
- `rule_start`
|
||||||
|
- `script_task`
|
||||||
|
- `service_task`
|
||||||
|
- `terminate`
|
||||||
|
- `timer_intermediate`
|
||||||
|
- `timer_start`
|
||||||
|
- `user_task`
|
||||||
71
docs/shape-libraries/cabinets.md
Normal file
71
docs/shape-libraries/cabinets.md
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# cabinets
|
||||||
|
|
||||||
|
**Type:** mxgraph shapes
|
||||||
|
**Prefix:** `mxgraph.cabinets`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mxCell value="label" style="shape=mxgraph.cabinets.{shape};" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Shapes (54)
|
||||||
|
|
||||||
|
- `auxiliary_contact_contactor_1_32a`
|
||||||
|
- `auxiliary_contact_contactor_32_125a`
|
||||||
|
- `cb_1p`
|
||||||
|
- `cb_1p_x10`
|
||||||
|
- `cb_2p`
|
||||||
|
- `cb_2p_x10`
|
||||||
|
- `cb_3p`
|
||||||
|
- `cb_3p_x5`
|
||||||
|
- `cb_4p`
|
||||||
|
- `cb_4p_x5`
|
||||||
|
- `cb_auxiliary_contact`
|
||||||
|
- `contactor_125_400a`
|
||||||
|
- `contactor_1_32a`
|
||||||
|
- `contactor_32_125a`
|
||||||
|
- `din_rail`
|
||||||
|
- `distribution_block_4p_125a_11_connections`
|
||||||
|
- `distribution_block_4p_125a_11_connections_2`
|
||||||
|
- `mccb_25_63a_3p`
|
||||||
|
- `mccb_25_63a_4p`
|
||||||
|
- `mccb_63_250a_3p`
|
||||||
|
- `mccb_63_250a_4p`
|
||||||
|
- `motor_cb_125_400a`
|
||||||
|
- `motor_cb_1_32a`
|
||||||
|
- `motor_cb_32_125a`
|
||||||
|
- `motor_protection_cb`
|
||||||
|
- `motor_starter_125_400a`
|
||||||
|
- `motor_starter_1_32a`
|
||||||
|
- `motor_starter_32_125a`
|
||||||
|
- `motorized_switch_3p`
|
||||||
|
- `motorized_switch_4p`
|
||||||
|
- `mxgraph.cabinets`
|
||||||
|
- `overcurrent_relay_125_400a`
|
||||||
|
- `overcurrent_relay_1_32a`
|
||||||
|
- `overcurrent_relay_32_125a`
|
||||||
|
- `plugin_relay_1`
|
||||||
|
- `plugin_relay_2`
|
||||||
|
- `residual_current_device_2p`
|
||||||
|
- `residual_current_device_4p`
|
||||||
|
- `surge_protection_1p`
|
||||||
|
- `surge_protection_2p`
|
||||||
|
- `surge_protection_3p`
|
||||||
|
- `surge_protection_4p`
|
||||||
|
- `terminal_40mm2`
|
||||||
|
- `terminal_40mm2_x10`
|
||||||
|
- `terminal_4_6mm2`
|
||||||
|
- `terminal_4_6mm2_x10`
|
||||||
|
- `terminal_4mm2`
|
||||||
|
- `terminal_4mm2_x10`
|
||||||
|
- `terminal_50mm2`
|
||||||
|
- `terminal_50mm2_x10`
|
||||||
|
- `terminal_6_25mm2`
|
||||||
|
- `terminal_6_25mm2_x10`
|
||||||
|
- `terminal_75mm2`
|
||||||
|
- `terminal_75mm2_x10`
|
||||||
250
docs/shape-libraries/cisco19.md
Normal file
250
docs/shape-libraries/cisco19.md
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
# cisco19
|
||||||
|
|
||||||
|
**Type:** mxgraph shapes
|
||||||
|
**Prefix:** `mxgraph.cisco19`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mxCell value="label" style="shape=mxgraph.cisco19.rect;prIcon={shape};fillColor=#00bceb;strokeColor=none;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Shapes (233)
|
||||||
|
|
||||||
|
- `3g_4g_indicator`
|
||||||
|
- `6500_vss`
|
||||||
|
- `6500_vss2`
|
||||||
|
- `access_control_and_trustsec`
|
||||||
|
- `aci`
|
||||||
|
- `aci2`
|
||||||
|
- `acibg`
|
||||||
|
- `acs`
|
||||||
|
- `ad_decoder`
|
||||||
|
- `ad_encoder`
|
||||||
|
- `analysis_correlation`
|
||||||
|
- `anomaly_detection`
|
||||||
|
- `anti_malware`
|
||||||
|
- `anti_malware2`
|
||||||
|
- `appnav`
|
||||||
|
- `asa_5500`
|
||||||
|
- `asr_1000`
|
||||||
|
- `asr_9000`
|
||||||
|
- `avc_application_visibility_control`
|
||||||
|
- `avc_application_visibility_control2`
|
||||||
|
- `bg1`
|
||||||
|
- `bg10`
|
||||||
|
- `bg2`
|
||||||
|
- `bg3`
|
||||||
|
- `bg4`
|
||||||
|
- `bg5`
|
||||||
|
- `bg6`
|
||||||
|
- `bg7`
|
||||||
|
- `bg8`
|
||||||
|
- `bg9`
|
||||||
|
- `blade_server`
|
||||||
|
- `branch`
|
||||||
|
- `branch2`
|
||||||
|
- `camera`
|
||||||
|
- `camera2`
|
||||||
|
- `cell_phone`
|
||||||
|
- `cell_phone2`
|
||||||
|
- `cisco_15800`
|
||||||
|
- `cisco_dna`
|
||||||
|
- `cisco_dna_center`
|
||||||
|
- `cisco_meetingplace_express`
|
||||||
|
- `cisco_security_manager`
|
||||||
|
- `cisco_unified_contact_center_enterprise_and_hosted`
|
||||||
|
- `cisco_unified_presence_service`
|
||||||
|
- `clock`
|
||||||
|
- `cloud`
|
||||||
|
- `cloud2`
|
||||||
|
- `cognitive`
|
||||||
|
- `collab1`
|
||||||
|
- `collab2`
|
||||||
|
- `collab3`
|
||||||
|
- `collab4`
|
||||||
|
- `communications_manager`
|
||||||
|
- `contact_center_express`
|
||||||
|
- `content_recording_streaming_server`
|
||||||
|
- `content_router`
|
||||||
|
- `csr_1000v`
|
||||||
|
- `da_decoder`
|
||||||
|
- `da_encoder`
|
||||||
|
- `data_center`
|
||||||
|
- `data_center2`
|
||||||
|
- `database_relational`
|
||||||
|
- `dns_server`
|
||||||
|
- `dns_server2`
|
||||||
|
- `dual_mode_access_point`
|
||||||
|
- `email_security`
|
||||||
|
- `fabric_interconnect`
|
||||||
|
- `fibre_channel_director_mds_9000`
|
||||||
|
- `fibre_channel_fabric_switch`
|
||||||
|
- `firewall`
|
||||||
|
- `flow_analytics`
|
||||||
|
- `flow_analytics2`
|
||||||
|
- `flow_collector`
|
||||||
|
- `h323`
|
||||||
|
- `handheld`
|
||||||
|
- `handheld2`
|
||||||
|
- `hdtv`
|
||||||
|
- `hdtv2`
|
||||||
|
- `home_office`
|
||||||
|
- `home_office2`
|
||||||
|
- `host_based_security`
|
||||||
|
- `hypervisor`
|
||||||
|
- `immersive_telepresence_endpoint`
|
||||||
|
- `ip_ip_gateway`
|
||||||
|
- `ip_phone`
|
||||||
|
- `ip_phone2`
|
||||||
|
- `ip_telephone_router`
|
||||||
|
- `ips_ids`
|
||||||
|
- `ironport`
|
||||||
|
- `ise`
|
||||||
|
- `joystick_keyboard`
|
||||||
|
- `joystick_keyboard2`
|
||||||
|
- `key`
|
||||||
|
- `key2`
|
||||||
|
- `l2_modular`
|
||||||
|
- `l2_modular2`
|
||||||
|
- `l2_switch`
|
||||||
|
- `l2_switch_with_dual_supervisor`
|
||||||
|
- `l3_modular`
|
||||||
|
- `l3_modular2`
|
||||||
|
- `l3_modular3`
|
||||||
|
- `l3_switch`
|
||||||
|
- `l3_switch_with_dual_supervisor`
|
||||||
|
- `laptop`
|
||||||
|
- `laptop2`
|
||||||
|
- `laptop_video_client`
|
||||||
|
- `laptop_video_client2`
|
||||||
|
- `layer3_nexus_5k_switch`
|
||||||
|
- `ldap`
|
||||||
|
- `ldap2`
|
||||||
|
- `load_balancer`
|
||||||
|
- `lock`
|
||||||
|
- `lock2`
|
||||||
|
- `media_server`
|
||||||
|
- `meeting_scheduling_and_management_server`
|
||||||
|
- `mesh_access_point`
|
||||||
|
- `monitor`
|
||||||
|
- `monitoring`
|
||||||
|
- `multipoint_meeting_server`
|
||||||
|
- `mxgraph.cisco19`
|
||||||
|
- `nac_appliance`
|
||||||
|
- `nam_virtual_service_blade`
|
||||||
|
- `net_mgmt_appliance`
|
||||||
|
- `netflow_router`
|
||||||
|
- `netflow_router2`
|
||||||
|
- `netflow_router3`
|
||||||
|
- `next_generation_intrusion_prevention_system`
|
||||||
|
- `nexus_1010`
|
||||||
|
- `nexus_1k`
|
||||||
|
- `nexus_1kv_vsm`
|
||||||
|
- `nexus_2000_10ge`
|
||||||
|
- `nexus_2k`
|
||||||
|
- `nexus_3k`
|
||||||
|
- `nexus_4k`
|
||||||
|
- `nexus_5k`
|
||||||
|
- `nexus_5k_with_integrated_vsm`
|
||||||
|
- `nexus_7k`
|
||||||
|
- `nexus_9300`
|
||||||
|
- `nexus_9500`
|
||||||
|
- `operations_manager`
|
||||||
|
- `phone_polycom`
|
||||||
|
- `phone_polycom2`
|
||||||
|
- `policy_configuration`
|
||||||
|
- `pos`
|
||||||
|
- `pos2`
|
||||||
|
- `posture_assessment`
|
||||||
|
- `primary_codec`
|
||||||
|
- `printer`
|
||||||
|
- `printer2`
|
||||||
|
- `router`
|
||||||
|
- `router_with_firewall`
|
||||||
|
- `router_with_firewall2`
|
||||||
|
- `router_with_voice`
|
||||||
|
- `rps`
|
||||||
|
- `secondary_codec`
|
||||||
|
- `secure_catalyst_switch_color`
|
||||||
|
- `secure_catalyst_switch_color2`
|
||||||
|
- `secure_catalyst_switch_color3`
|
||||||
|
- `secure_catalyst_switch_subdued`
|
||||||
|
- `secure_catalyst_switch_subdued2`
|
||||||
|
- `secure_endpoint_pc`
|
||||||
|
- `secure_endpoint_pc2`
|
||||||
|
- `secure_endpoints`
|
||||||
|
- `secure_endpoints2`
|
||||||
|
- `secure_router`
|
||||||
|
- `secure_server`
|
||||||
|
- `secure_server2`
|
||||||
|
- `secure_switch`
|
||||||
|
- `security_management`
|
||||||
|
- `server`
|
||||||
|
- `server2`
|
||||||
|
- `service_ready_engine`
|
||||||
|
- `set_top`
|
||||||
|
- `set_top2`
|
||||||
|
- `shield`
|
||||||
|
- `ssl_terminator`
|
||||||
|
- `stealthwatch_management_console_smc`
|
||||||
|
- `stealthwatch_management_console_smc2`
|
||||||
|
- `storage`
|
||||||
|
- `surveillance_camera`
|
||||||
|
- `surveillance_camera2`
|
||||||
|
- `tablet`
|
||||||
|
- `tablet2`
|
||||||
|
- `telepresence_endpoint`
|
||||||
|
- `telepresence_endpoint_twin_data_display`
|
||||||
|
- `telepresence_exchange`
|
||||||
|
- `threat_intelligence`
|
||||||
|
- `transcoder`
|
||||||
|
- `ucs_5108_blade_chassis`
|
||||||
|
- `ucs_c_series_server`
|
||||||
|
- `ucs_express`
|
||||||
|
- `unity`
|
||||||
|
- `upc_unified_personal_communicator`
|
||||||
|
- `upc_unified_personal_communicator2`
|
||||||
|
- `ups`
|
||||||
|
- `user`
|
||||||
|
- `user2`
|
||||||
|
- `vbond`
|
||||||
|
- `video_analytics`
|
||||||
|
- `video_call_server`
|
||||||
|
- `video_gateway`
|
||||||
|
- `virtual_desktop_service`
|
||||||
|
- `virtual_matrix_switch`
|
||||||
|
- `virtual_private_network`
|
||||||
|
- `virtual_private_network2`
|
||||||
|
- `virtual_private_network_connector`
|
||||||
|
- `vmanage`
|
||||||
|
- `vpn_concentrator`
|
||||||
|
- `vsmart`
|
||||||
|
- `vts`
|
||||||
|
- `vts2`
|
||||||
|
- `web_application_firewall`
|
||||||
|
- `web_reputation_filtering`
|
||||||
|
- `web_reputation_filtering_2`
|
||||||
|
- `web_security`
|
||||||
|
- `web_security_services`
|
||||||
|
- `web_security_services2`
|
||||||
|
- `webex`
|
||||||
|
- `wifi_indicator`
|
||||||
|
- `wireless_access_point`
|
||||||
|
- `wireless_access_point2`
|
||||||
|
- `wireless_bridge`
|
||||||
|
- `wireless_bridge2`
|
||||||
|
- `wireless_connector`
|
||||||
|
- `wireless_intrusion_prevention`
|
||||||
|
- `wireless_lan_controller`
|
||||||
|
- `wireless_location_appliance`
|
||||||
|
- `wireless_router`
|
||||||
|
- `workgroup_switch`
|
||||||
|
- `workstation`
|
||||||
|
- `workstation2`
|
||||||
|
- `x509_certificate`
|
||||||
|
- `x509_certificate2`
|
||||||
115
docs/shape-libraries/citrix.md
Normal file
115
docs/shape-libraries/citrix.md
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
# citrix
|
||||||
|
|
||||||
|
**Type:** mxgraph shapes
|
||||||
|
**Prefix:** `mxgraph.citrix`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mxCell value="label" style="shape=mxgraph.citrix.{shape};fillColor=#00A4E4;strokeColor=none;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Shapes (98)
|
||||||
|
|
||||||
|
- `1u_2u_server`
|
||||||
|
- `access_card`
|
||||||
|
- `branch_repeater`
|
||||||
|
- `browser`
|
||||||
|
- `cache_server`
|
||||||
|
- `calendar`
|
||||||
|
- `cell_phone`
|
||||||
|
- `chassis`
|
||||||
|
- `citrix_hdx`
|
||||||
|
- `citrix_logo`
|
||||||
|
- `cloud`
|
||||||
|
- `command_center`
|
||||||
|
- `database`
|
||||||
|
- `database_server`
|
||||||
|
- `datacenter`
|
||||||
|
- `desktop`
|
||||||
|
- `desktop_web`
|
||||||
|
- `dhcp_server`
|
||||||
|
- `directory_server`
|
||||||
|
- `dns_server`
|
||||||
|
- `document`
|
||||||
|
- `edgesight_server`
|
||||||
|
- `file_server`
|
||||||
|
- `firewall`
|
||||||
|
- `ftp_server`
|
||||||
|
- `geolocation_database`
|
||||||
|
- `globe`
|
||||||
|
- `goto_meeting`
|
||||||
|
- `government`
|
||||||
|
- `home_office`
|
||||||
|
- `hq_enterprise`
|
||||||
|
- `inspection`
|
||||||
|
- `ip_phone`
|
||||||
|
- `kiosk`
|
||||||
|
- `laptop_1`
|
||||||
|
- `laptop_2`
|
||||||
|
- `license_server`
|
||||||
|
- `merchandising_server`
|
||||||
|
- `middleware`
|
||||||
|
- `mxgraph.citrix`
|
||||||
|
- `netscaler_gateway`
|
||||||
|
- `netscaler_mpx`
|
||||||
|
- `netscaler_sdx`
|
||||||
|
- `netscaler_vpx`
|
||||||
|
- `pbx_server`
|
||||||
|
- `pda`
|
||||||
|
- `podio`
|
||||||
|
- `printer`
|
||||||
|
- `process`
|
||||||
|
- `provisioning_server`
|
||||||
|
- `proxy_server`
|
||||||
|
- `radius_server`
|
||||||
|
- `remote_office`
|
||||||
|
- `reporting`
|
||||||
|
- `role_appcontroller`
|
||||||
|
- `role_applications`
|
||||||
|
- `role_cloudbridge`
|
||||||
|
- `role_desktops`
|
||||||
|
- `role_load_testing_controller`
|
||||||
|
- `role_load_testing_launcher`
|
||||||
|
- `role_receiver`
|
||||||
|
- `role_repeater`
|
||||||
|
- `role_secure_access`
|
||||||
|
- `role_security`
|
||||||
|
- `role_services`
|
||||||
|
- `role_storefront`
|
||||||
|
- `role_storefront_services`
|
||||||
|
- `role_synchronizer`
|
||||||
|
- `role_xenmobile`
|
||||||
|
- `role_xenmobile_device_manager`
|
||||||
|
- `router`
|
||||||
|
- `security`
|
||||||
|
- `sharefile`
|
||||||
|
- `site`
|
||||||
|
- `smtp_server`
|
||||||
|
- `storefront_services`
|
||||||
|
- `switch`
|
||||||
|
- `tablet_1`
|
||||||
|
- `tablet_2`
|
||||||
|
- `thin_client`
|
||||||
|
- `tower_server`
|
||||||
|
- `user_control`
|
||||||
|
- `users`
|
||||||
|
- `web_server`
|
||||||
|
- `web_service`
|
||||||
|
- `worxenroll`
|
||||||
|
- `worxhome`
|
||||||
|
- `worxmail`
|
||||||
|
- `worxweb`
|
||||||
|
- `xenapp_server`
|
||||||
|
- `xenapp_services`
|
||||||
|
- `xenapp_web`
|
||||||
|
- `xencenter`
|
||||||
|
- `xenclient`
|
||||||
|
- `xenclient_synchronizer`
|
||||||
|
- `xendesktop_server`
|
||||||
|
- `xenmobile`
|
||||||
|
- `xenserver`
|
||||||
50
docs/shape-libraries/electrical.md
Normal file
50
docs/shape-libraries/electrical.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# electrical
|
||||||
|
|
||||||
|
**Type:** mxgraph shapes
|
||||||
|
**Prefix:** `mxgraph.electrical`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mxCell value="label" style="shape=mxgraph.electrical.resistors.resistor_1;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="0" y="0" width="60" height="20" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
Shapes are organized by category: `mxgraph.electrical.{category}.{shape}`
|
||||||
|
|
||||||
|
## Categories
|
||||||
|
|
||||||
|
### resistors
|
||||||
|
- `resistor_1`
|
||||||
|
- `resistor_2`
|
||||||
|
|
||||||
|
### capacitors
|
||||||
|
- `capacitor_1`
|
||||||
|
- `capacitor_3`
|
||||||
|
|
||||||
|
### inductors
|
||||||
|
- `inductor_3`
|
||||||
|
- `transformer_1`
|
||||||
|
|
||||||
|
### diodes
|
||||||
|
- `diode`
|
||||||
|
- `zener_diode_1`
|
||||||
|
|
||||||
|
### transistors
|
||||||
|
- `npn_transistor_1`
|
||||||
|
- `pnp_transistor_1`
|
||||||
|
|
||||||
|
### mosfets1
|
||||||
|
- `n-channel_mosfet_1`
|
||||||
|
- `p-channel_mosfet_1`
|
||||||
|
|
||||||
|
### logic_gates
|
||||||
|
- `logic_gate`
|
||||||
|
- `dual_inline_ic`
|
||||||
|
|
||||||
|
### electro-mechanical
|
||||||
|
- `singleSwitch`
|
||||||
|
- `pushbutton`
|
||||||
|
|
||||||
|
(See draw.io Electrical shape library for complete list)
|
||||||
62
docs/shape-libraries/floorplan.md
Normal file
62
docs/shape-libraries/floorplan.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# floorplan
|
||||||
|
|
||||||
|
**Type:** mxgraph shapes
|
||||||
|
**Prefix:** `mxgraph.floorplan`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mxCell value="label" style="shape=mxgraph.floorplan.{shape};fillColor=#ffffff;strokeColor=#000000;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Shapes (45)
|
||||||
|
|
||||||
|
- `bathtub`
|
||||||
|
- `bathtub2`
|
||||||
|
- `bed_double`
|
||||||
|
- `bed_single`
|
||||||
|
- `bookcase`
|
||||||
|
- `chair`
|
||||||
|
- `copier`
|
||||||
|
- `couch`
|
||||||
|
- `crt_tv`
|
||||||
|
- `desk_corner`
|
||||||
|
- `desk_corner_2`
|
||||||
|
- `dresser`
|
||||||
|
- `drying_machine`
|
||||||
|
- `elevator`
|
||||||
|
- `fireplace`
|
||||||
|
- `flat_tv`
|
||||||
|
- `floor_lamp`
|
||||||
|
- `laptop`
|
||||||
|
- `mxgraph.floorplan`
|
||||||
|
- `office_chair`
|
||||||
|
- `piano`
|
||||||
|
- `plant`
|
||||||
|
- `printer`
|
||||||
|
- `range_1`
|
||||||
|
- `range_2`
|
||||||
|
- `refrigerator`
|
||||||
|
- `shower`
|
||||||
|
- `shower2`
|
||||||
|
- `sink_1`
|
||||||
|
- `sink_2`
|
||||||
|
- `sink_22`
|
||||||
|
- `sink_double`
|
||||||
|
- `sink_double2`
|
||||||
|
- `sofa`
|
||||||
|
- `spiral_stairs`
|
||||||
|
- `table`
|
||||||
|
- `table_1`
|
||||||
|
- `table_2`
|
||||||
|
- `table_3`
|
||||||
|
- `table_4`
|
||||||
|
- `table_5`
|
||||||
|
- `toilet`
|
||||||
|
- `washing_machine`
|
||||||
|
- `water_cooler`
|
||||||
|
- `workstation`
|
||||||
52
docs/shape-libraries/flowchart.md
Normal file
52
docs/shape-libraries/flowchart.md
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# flowchart
|
||||||
|
|
||||||
|
**Type:** mxgraph shapes
|
||||||
|
**Prefix:** `mxgraph.flowchart`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mxCell value="label" style="shape=mxgraph.flowchart.{shape};fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Shapes (35)
|
||||||
|
|
||||||
|
- `annotation_1`
|
||||||
|
- `annotation_2`
|
||||||
|
- `card`
|
||||||
|
- `collate`
|
||||||
|
- `data`
|
||||||
|
- `database`
|
||||||
|
- `decision`
|
||||||
|
- `delay`
|
||||||
|
- `direct_data`
|
||||||
|
- `display`
|
||||||
|
- `document`
|
||||||
|
- `extract_or_measurement`
|
||||||
|
- `internal_storage`
|
||||||
|
- `loop_limit`
|
||||||
|
- `manual_input`
|
||||||
|
- `manual_operation`
|
||||||
|
- `merge_or_storage`
|
||||||
|
- `multi-document`
|
||||||
|
- `mxgraph.flowchart`
|
||||||
|
- `off-page_reference`
|
||||||
|
- `on-page_reference`
|
||||||
|
- `or`
|
||||||
|
- `paper_tape`
|
||||||
|
- `parallel_mode`
|
||||||
|
- `predefined_process`
|
||||||
|
- `preparation`
|
||||||
|
- `process`
|
||||||
|
- `sequential_data`
|
||||||
|
- `sort`
|
||||||
|
- `start_1`
|
||||||
|
- `start_2`
|
||||||
|
- `stored_data`
|
||||||
|
- `summing_function`
|
||||||
|
- `terminator`
|
||||||
|
- `transfer`
|
||||||
264
docs/shape-libraries/fluidpower.md
Normal file
264
docs/shape-libraries/fluidpower.md
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
# fluidpower
|
||||||
|
|
||||||
|
**Type:** mxgraph shapes
|
||||||
|
**Prefix:** `mxgraph.fluid_power`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mxCell value="label" style="shape=mxgraph.fluid_power.{shape};fillColor=strokeColor;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
Shapes are named like x10010, x10020, etc.
|
||||||
|
|
||||||
|
## Shapes (247)
|
||||||
|
|
||||||
|
- `mxgraph.fluid_power`
|
||||||
|
- `x10010`
|
||||||
|
- `x10020`
|
||||||
|
- `x10030`
|
||||||
|
- `x10040`
|
||||||
|
- `x10050`
|
||||||
|
- `x10060`
|
||||||
|
- `x10070`
|
||||||
|
- `x10080`
|
||||||
|
- `x10090`
|
||||||
|
- `x10100`
|
||||||
|
- `x10110`
|
||||||
|
- `x10120`
|
||||||
|
- `x10130`
|
||||||
|
- `x10140`
|
||||||
|
- `x10150`
|
||||||
|
- `x10160`
|
||||||
|
- `x10170`
|
||||||
|
- `x10180`
|
||||||
|
- `x10190`
|
||||||
|
- `x10200`
|
||||||
|
- `x10210`
|
||||||
|
- `x10220`
|
||||||
|
- `x10230`
|
||||||
|
- `x10240`
|
||||||
|
- `x10250`
|
||||||
|
- `x10260`
|
||||||
|
- `x10270`
|
||||||
|
- `x10280`
|
||||||
|
- `x10290`
|
||||||
|
- `x10300`
|
||||||
|
- `x10310`
|
||||||
|
- `x10320`
|
||||||
|
- `x10330`
|
||||||
|
- `x10340`
|
||||||
|
- `x10350`
|
||||||
|
- `x10360`
|
||||||
|
- `x10370`
|
||||||
|
- `x10380`
|
||||||
|
- `x10390`
|
||||||
|
- `x10400`
|
||||||
|
- `x10410`
|
||||||
|
- `x10420`
|
||||||
|
- `x10430`
|
||||||
|
- `x10440`
|
||||||
|
- `x10441`
|
||||||
|
- `x10442`
|
||||||
|
- `x10450`
|
||||||
|
- `x10460`
|
||||||
|
- `x10470`
|
||||||
|
- `x10480`
|
||||||
|
- `x10490`
|
||||||
|
- `x10500`
|
||||||
|
- `x10510`
|
||||||
|
- `x10520`
|
||||||
|
- `x10530`
|
||||||
|
- `x10540`
|
||||||
|
- `x10550`
|
||||||
|
- `x10560`
|
||||||
|
- `x10570`
|
||||||
|
- `x10580`
|
||||||
|
- `x10590`
|
||||||
|
- `x10600`
|
||||||
|
- `x10610`
|
||||||
|
- `x10620`
|
||||||
|
- `x10630`
|
||||||
|
- `x10640`
|
||||||
|
- `x10650`
|
||||||
|
- `x10660`
|
||||||
|
- `x10670`
|
||||||
|
- `x10680`
|
||||||
|
- `x10690`
|
||||||
|
- `x10700`
|
||||||
|
- `x10710`
|
||||||
|
- `x10720`
|
||||||
|
- `x10730`
|
||||||
|
- `x10740`
|
||||||
|
- `x10750`
|
||||||
|
- `x10760`
|
||||||
|
- `x10770`
|
||||||
|
- `x10780`
|
||||||
|
- `x10790`
|
||||||
|
- `x10800`
|
||||||
|
- `x10810`
|
||||||
|
- `x10820`
|
||||||
|
- `x10830`
|
||||||
|
- `x10840`
|
||||||
|
- `x10850`
|
||||||
|
- `x10860`
|
||||||
|
- `x10870`
|
||||||
|
- `x10880`
|
||||||
|
- `x10890`
|
||||||
|
- `x10900`
|
||||||
|
- `x10910`
|
||||||
|
- `x10920`
|
||||||
|
- `x10930`
|
||||||
|
- `x10940`
|
||||||
|
- `x10950`
|
||||||
|
- `x10960`
|
||||||
|
- `x10970`
|
||||||
|
- `x10980`
|
||||||
|
- `x10990`
|
||||||
|
- `x11000`
|
||||||
|
- `x11010`
|
||||||
|
- `x11020`
|
||||||
|
- `x11030`
|
||||||
|
- `x11040`
|
||||||
|
- `x11050`
|
||||||
|
- `x11060`
|
||||||
|
- `x11070`
|
||||||
|
- `x11080`
|
||||||
|
- `x11090`
|
||||||
|
- `x11100`
|
||||||
|
- `x11110`
|
||||||
|
- `x11120`
|
||||||
|
- `x11130`
|
||||||
|
- `x11140`
|
||||||
|
- `x11150`
|
||||||
|
- `x11160`
|
||||||
|
- `x11170`
|
||||||
|
- `x11180`
|
||||||
|
- `x11190`
|
||||||
|
- `x11200`
|
||||||
|
- `x11210`
|
||||||
|
- `x11220`
|
||||||
|
- `x11230`
|
||||||
|
- `x11240`
|
||||||
|
- `x11250`
|
||||||
|
- `x11260`
|
||||||
|
- `x11270`
|
||||||
|
- `x11280`
|
||||||
|
- `x11290`
|
||||||
|
- `x11300`
|
||||||
|
- `x11310`
|
||||||
|
- `x11320`
|
||||||
|
- `x11330`
|
||||||
|
- `x11340`
|
||||||
|
- `x11350`
|
||||||
|
- `x11360`
|
||||||
|
- `x11370`
|
||||||
|
- `x11380`
|
||||||
|
- `x11390`
|
||||||
|
- `x11400`
|
||||||
|
- `x11410`
|
||||||
|
- `x11420`
|
||||||
|
- `x11430`
|
||||||
|
- `x11440`
|
||||||
|
- `x11450`
|
||||||
|
- `x11460`
|
||||||
|
- `x11470`
|
||||||
|
- `x11480`
|
||||||
|
- `x11490`
|
||||||
|
- `x11500`
|
||||||
|
- `x11510`
|
||||||
|
- `x11520`
|
||||||
|
- `x11530`
|
||||||
|
- `x11540`
|
||||||
|
- `x11550`
|
||||||
|
- `x11560`
|
||||||
|
- `x11570`
|
||||||
|
- `x11580`
|
||||||
|
- `x11590`
|
||||||
|
- `x11600`
|
||||||
|
- `x11610`
|
||||||
|
- `x11620`
|
||||||
|
- `x11630`
|
||||||
|
- `x11640`
|
||||||
|
- `x11650`
|
||||||
|
- `x11660`
|
||||||
|
- `x11670`
|
||||||
|
- `x11680`
|
||||||
|
- `x11690`
|
||||||
|
- `x11700`
|
||||||
|
- `x11710`
|
||||||
|
- `x11720`
|
||||||
|
- `x11730`
|
||||||
|
- `x11740`
|
||||||
|
- `x11750`
|
||||||
|
- `x11760`
|
||||||
|
- `x11770`
|
||||||
|
- `x11780`
|
||||||
|
- `x11790`
|
||||||
|
- `x11800`
|
||||||
|
- `x11810`
|
||||||
|
- `x11820`
|
||||||
|
- `x11830`
|
||||||
|
- `x11840`
|
||||||
|
- `x11850`
|
||||||
|
- `x11860`
|
||||||
|
- `x11870`
|
||||||
|
- `x11880`
|
||||||
|
- `x11890`
|
||||||
|
- `x11900`
|
||||||
|
- `x11910`
|
||||||
|
- `x11920`
|
||||||
|
- `x11930`
|
||||||
|
- `x11940`
|
||||||
|
- `x11950`
|
||||||
|
- `x11960`
|
||||||
|
- `x11970`
|
||||||
|
- `x11980`
|
||||||
|
- `x11990`
|
||||||
|
- `x12000`
|
||||||
|
- `x12010`
|
||||||
|
- `x12020`
|
||||||
|
- `x12030`
|
||||||
|
- `x12040`
|
||||||
|
- `x12050`
|
||||||
|
- `x12060`
|
||||||
|
- `x12070`
|
||||||
|
- `x12080`
|
||||||
|
- `x12090`
|
||||||
|
- `x12100`
|
||||||
|
- `x12110`
|
||||||
|
- `x12120`
|
||||||
|
- `x12130`
|
||||||
|
- `x12140`
|
||||||
|
- `x12150`
|
||||||
|
- `x12160_detailed`
|
||||||
|
- `x12160_simplified`
|
||||||
|
- `x12170`
|
||||||
|
- `x12180`
|
||||||
|
- `x12190`
|
||||||
|
- `x12200`
|
||||||
|
- `x12210`
|
||||||
|
- `x12220`
|
||||||
|
- `x12230`
|
||||||
|
- `x12240`
|
||||||
|
- `x12250`
|
||||||
|
- `x12260`
|
||||||
|
- `x12270`
|
||||||
|
- `x12280`
|
||||||
|
- `x12290`
|
||||||
|
- `x12300`
|
||||||
|
- `x12310`
|
||||||
|
- `x12320`
|
||||||
|
- `x12330`
|
||||||
|
- `x12340`
|
||||||
|
- `x12350`
|
||||||
|
- `x12360`
|
||||||
|
- `x12370`
|
||||||
|
- `x12380`
|
||||||
|
- `x12390`
|
||||||
|
- `x12400`
|
||||||
|
- `x12410`
|
||||||
|
- `x12420`
|
||||||
|
- `x12430`
|
||||||
315
docs/shape-libraries/gcp2.md
Normal file
315
docs/shape-libraries/gcp2.md
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
# gcp2
|
||||||
|
|
||||||
|
**Type:** mxgraph shapes
|
||||||
|
**Prefix:** `mxgraph.gcp2`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mxCell value="label" style="shape=mxgraph.gcp2.{shape};fillColor=#4285F4;strokeColor=none;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Shapes (298)
|
||||||
|
|
||||||
|
- `a7_power`
|
||||||
|
- `admin_connected`
|
||||||
|
- `admob`
|
||||||
|
- `advanced_solutions_lab`
|
||||||
|
- `ai_hub`
|
||||||
|
- `anomaly_detection`
|
||||||
|
- `api_analytics`
|
||||||
|
- `api_monetization`
|
||||||
|
- `apigee_api_platform`
|
||||||
|
- `apigee_sense`
|
||||||
|
- `app_engine`
|
||||||
|
- `app_engine_icon`
|
||||||
|
- `application`
|
||||||
|
- `application_system`
|
||||||
|
- `arrow_cycle`
|
||||||
|
- `arrows_system`
|
||||||
|
- `aspect_ratio`
|
||||||
|
- `automl_natural_language`
|
||||||
|
- `automl_tables`
|
||||||
|
- `automl_translation`
|
||||||
|
- `automl_video_intelligence`
|
||||||
|
- `automl_vision`
|
||||||
|
- `avere`
|
||||||
|
- `beacon`
|
||||||
|
- `beyondcorp`
|
||||||
|
- `big_query`
|
||||||
|
- `bigquery`
|
||||||
|
- `biomedical_beaker`
|
||||||
|
- `biomedical_test_tube`
|
||||||
|
- `biomedical_trio`
|
||||||
|
- `blank`
|
||||||
|
- `blue_hexagon`
|
||||||
|
- `bucket`
|
||||||
|
- `bucket_scale`
|
||||||
|
- `calculator`
|
||||||
|
- `campaign_manager`
|
||||||
|
- `capabilities`
|
||||||
|
- `certified_industry_standard`
|
||||||
|
- `check`
|
||||||
|
- `check_2`
|
||||||
|
- `check_available`
|
||||||
|
- `check_scale`
|
||||||
|
- `circuit_board`
|
||||||
|
- `clock`
|
||||||
|
- `cloud`
|
||||||
|
- `cloud_apis`
|
||||||
|
- `cloud_armor`
|
||||||
|
- `cloud_automl`
|
||||||
|
- `cloud_bigtable`
|
||||||
|
- `cloud_cdn`
|
||||||
|
- `cloud_checkmark`
|
||||||
|
- `cloud_code`
|
||||||
|
- `cloud_composer`
|
||||||
|
- `cloud_computer`
|
||||||
|
- `cloud_connected_insight`
|
||||||
|
- `cloud_data_catalog`
|
||||||
|
- `cloud_data_fusion`
|
||||||
|
- `cloud_dataflow`
|
||||||
|
- `cloud_dataflow_icon`
|
||||||
|
- `cloud_datalab`
|
||||||
|
- `cloud_dataprep`
|
||||||
|
- `cloud_dataproc`
|
||||||
|
- `cloud_dataproc_icon`
|
||||||
|
- `cloud_datastore`
|
||||||
|
- `cloud_deployment_manager`
|
||||||
|
- `cloud_dns`
|
||||||
|
- `cloud_endpoints`
|
||||||
|
- `cloud_external_ip_addresses`
|
||||||
|
- `cloud_filestore`
|
||||||
|
- `cloud_firestore`
|
||||||
|
- `cloud_firewall_rules`
|
||||||
|
- `cloud_functions`
|
||||||
|
- `cloud_iam`
|
||||||
|
- `cloud_inference_api`
|
||||||
|
- `cloud_information`
|
||||||
|
- `cloud_iot_core`
|
||||||
|
- `cloud_iot_edge`
|
||||||
|
- `cloud_jobs_api`
|
||||||
|
- `cloud_load_balancing`
|
||||||
|
- `cloud_machine_learning`
|
||||||
|
- `cloud_memorystore`
|
||||||
|
- `cloud_messaging`
|
||||||
|
- `cloud_monitoring`
|
||||||
|
- `cloud_nat`
|
||||||
|
- `cloud_natural_language_api`
|
||||||
|
- `cloud_network`
|
||||||
|
- `cloud_pubsub`
|
||||||
|
- `cloud_router`
|
||||||
|
- `cloud_routes`
|
||||||
|
- `cloud_run`
|
||||||
|
- `cloud_scheduler`
|
||||||
|
- `cloud_security`
|
||||||
|
- `cloud_security_command_center`
|
||||||
|
- `cloud_security_scanner`
|
||||||
|
- `cloud_server`
|
||||||
|
- `cloud_service_mesh`
|
||||||
|
- `cloud_spanner`
|
||||||
|
- `cloud_speech_api`
|
||||||
|
- `cloud_sql`
|
||||||
|
- `cloud_storage`
|
||||||
|
- `cloud_sub_pub`
|
||||||
|
- `cloud_tasks`
|
||||||
|
- `cloud_test_lab`
|
||||||
|
- `cloud_text_to_speech`
|
||||||
|
- `cloud_tools_for_powershell`
|
||||||
|
- `cloud_tpu`
|
||||||
|
- `cloud_translation_api`
|
||||||
|
- `cloud_video_intelligence_api`
|
||||||
|
- `cloud_vision_api`
|
||||||
|
- `cloud_vpn`
|
||||||
|
- `cluster`
|
||||||
|
- `compute_engine`
|
||||||
|
- `compute_engine_2`
|
||||||
|
- `compute_engine_icon`
|
||||||
|
- `connected`
|
||||||
|
- `container_builder`
|
||||||
|
- `container_engine`
|
||||||
|
- `container_engine_icon`
|
||||||
|
- `container_optimized_os`
|
||||||
|
- `container_registry`
|
||||||
|
- `cost`
|
||||||
|
- `cost_arrows`
|
||||||
|
- `cost_savings`
|
||||||
|
- `data_access`
|
||||||
|
- `data_increase`
|
||||||
|
- `data_loss_prevention_api`
|
||||||
|
- `data_storage_cost`
|
||||||
|
- `data_studio`
|
||||||
|
- `database`
|
||||||
|
- `database_2`
|
||||||
|
- `database_3`
|
||||||
|
- `database_cycle`
|
||||||
|
- `database_speed`
|
||||||
|
- `database_uploading`
|
||||||
|
- `debugger`
|
||||||
|
- `dedicated_game_server`
|
||||||
|
- `dedicated_interconnect`
|
||||||
|
- `desktop`
|
||||||
|
- `desktop_and_mobile`
|
||||||
|
- `developer_portal`
|
||||||
|
- `dialogflow_enterprise_edition`
|
||||||
|
- `enhance_ui`
|
||||||
|
- `enhance_ui_2`
|
||||||
|
- `error_reporting`
|
||||||
|
- `external_data_center`
|
||||||
|
- `external_data_resource`
|
||||||
|
- `external_payment_form`
|
||||||
|
- `fastly`
|
||||||
|
- `files`
|
||||||
|
- `firebase`
|
||||||
|
- `folders`
|
||||||
|
- `forseti_lockup`
|
||||||
|
- `forseti_logo`
|
||||||
|
- `frontend_platform_services`
|
||||||
|
- `game`
|
||||||
|
- `gateway`
|
||||||
|
- `gateway_icon`
|
||||||
|
- `gear`
|
||||||
|
- `gear_arrow`
|
||||||
|
- `gear_chain`
|
||||||
|
- `gear_load`
|
||||||
|
- `genomics`
|
||||||
|
- `gke_on_prem`
|
||||||
|
- `globe_world`
|
||||||
|
- `google_ad_manager`
|
||||||
|
- `google_ads`
|
||||||
|
- `google_analytics`
|
||||||
|
- `google_analytics_360`
|
||||||
|
- `google_cloud_platform`
|
||||||
|
- `google_cloud_platform_lockup`
|
||||||
|
- `google_network`
|
||||||
|
- `google_network_edge_cache`
|
||||||
|
- `google_play_game_service`
|
||||||
|
- `gpu`
|
||||||
|
- `half_cloud`
|
||||||
|
- `https_load_balancer`
|
||||||
|
- `identity_aware_proxy`
|
||||||
|
- `image_services`
|
||||||
|
- `increase_cost_arrows`
|
||||||
|
- `internal_payment_authorization`
|
||||||
|
- `internet_connection`
|
||||||
|
- `istio_logo`
|
||||||
|
- `key`
|
||||||
|
- `key_management_service`
|
||||||
|
- `kubernetes_logo`
|
||||||
|
- `kubernetes_name`
|
||||||
|
- `laptop`
|
||||||
|
- `legacy_cloud`
|
||||||
|
- `legacy_cloud_2`
|
||||||
|
- `lifecycle`
|
||||||
|
- `lightbulb`
|
||||||
|
- `list`
|
||||||
|
- `live`
|
||||||
|
- `load_balancing`
|
||||||
|
- `loading`
|
||||||
|
- `loading_2`
|
||||||
|
- `loading_3`
|
||||||
|
- `lock`
|
||||||
|
- `logging`
|
||||||
|
- `logs_api`
|
||||||
|
- `management_security`
|
||||||
|
- `maps_api`
|
||||||
|
- `mem_instances`
|
||||||
|
- `memcache`
|
||||||
|
- `memory_card`
|
||||||
|
- `mobile_devices`
|
||||||
|
- `modifiers_autoscaling`
|
||||||
|
- `modifiers_custom_virtual_machine`
|
||||||
|
- `modifiers_high_cpu_machine`
|
||||||
|
- `modifiers_high_memory_machine`
|
||||||
|
- `modifiers_preemptable_vm`
|
||||||
|
- `modifiers_shared_core_machine_f1`
|
||||||
|
- `modifiers_shared_core_machine_g1`
|
||||||
|
- `modifiers_standard_machine`
|
||||||
|
- `modifiers_storage`
|
||||||
|
- `monitor`
|
||||||
|
- `monitor_2`
|
||||||
|
- `mxgraph.gcp2`
|
||||||
|
- `nat`
|
||||||
|
- `network`
|
||||||
|
- `network_load_balancer`
|
||||||
|
- `node`
|
||||||
|
- `outline_blank_1`
|
||||||
|
- `outline_blank_2`
|
||||||
|
- `outline_blank_3`
|
||||||
|
- `outline_highcomp`
|
||||||
|
- `outline_highmem`
|
||||||
|
- `partner_interconnect`
|
||||||
|
- `payment`
|
||||||
|
- `people_security_management`
|
||||||
|
- `persistent_disk`
|
||||||
|
- `persistent_disk_snapshot`
|
||||||
|
- `phone`
|
||||||
|
- `phone_android`
|
||||||
|
- `placeholder`
|
||||||
|
- `play_gear`
|
||||||
|
- `play_start`
|
||||||
|
- `prediction_api`
|
||||||
|
- `premium_network_tier`
|
||||||
|
- `primary`
|
||||||
|
- `process`
|
||||||
|
- `profiler`
|
||||||
|
- `push_notification_service`
|
||||||
|
- `recommendations_ai`
|
||||||
|
- `record`
|
||||||
|
- `replication_controller`
|
||||||
|
- `replication_controller_2`
|
||||||
|
- `replication_controller_3`
|
||||||
|
- `report`
|
||||||
|
- `repository`
|
||||||
|
- `repository_2`
|
||||||
|
- `repository_3`
|
||||||
|
- `repository_primary`
|
||||||
|
- `retail`
|
||||||
|
- `safety`
|
||||||
|
- `save`
|
||||||
|
- `scale`
|
||||||
|
- `scheduled_tasks`
|
||||||
|
- `search`
|
||||||
|
- `search_api`
|
||||||
|
- `security_key_enforcement`
|
||||||
|
- `segments`
|
||||||
|
- `segments_2`
|
||||||
|
- `segments_overlap`
|
||||||
|
- `servers_stacked`
|
||||||
|
- `service`
|
||||||
|
- `service_discovery`
|
||||||
|
- `social_media_time`
|
||||||
|
- `solution`
|
||||||
|
- `speaker`
|
||||||
|
- `speed`
|
||||||
|
- `squid_proxy`
|
||||||
|
- `stackdriver`
|
||||||
|
- `stacked_ownership`
|
||||||
|
- `standard_network_tier`
|
||||||
|
- `storage`
|
||||||
|
- `stream`
|
||||||
|
- `swap`
|
||||||
|
- `systems_check`
|
||||||
|
- `tape_record`
|
||||||
|
- `task_queues`
|
||||||
|
- `task_queues_2`
|
||||||
|
- `tensorflow_lockup`
|
||||||
|
- `tensorflow_logo`
|
||||||
|
- `thumbs_up`
|
||||||
|
- `time_clock`
|
||||||
|
- `trace`
|
||||||
|
- `traffic_director`
|
||||||
|
- `transfer_appliance`
|
||||||
|
- `users`
|
||||||
|
- `view_list`
|
||||||
|
- `virtual_file_system`
|
||||||
|
- `virtual_private_cloud`
|
||||||
|
- `visibility`
|
||||||
|
- `vpn`
|
||||||
|
- `vpn_gateway`
|
||||||
|
- `webcam`
|
||||||
|
- `website`
|
||||||
24
docs/shape-libraries/infographic.md
Normal file
24
docs/shape-libraries/infographic.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# infographic
|
||||||
|
|
||||||
|
**Type:** mxgraph shapes
|
||||||
|
**Prefix:** `mxgraph.infographic`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mxCell value="label" style="html=1;shape=mxgraph.infographic.shadedCube;isoAngle=15;fillColor=#10739E;strokeColor=none;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Shapes
|
||||||
|
|
||||||
|
- `shadedCube` (needs `isoAngle=15;`)
|
||||||
|
- `ribbonSimple` (needs `notch1=20;notch2=20;`)
|
||||||
|
- `ribbonRolled`
|
||||||
|
- `ribbonDoubleFolded`
|
||||||
|
- `shadedTriangle`
|
||||||
|
- `shadedPyramid`
|
||||||
|
- `cylinder`
|
||||||
|
- `banner`
|
||||||
|
- `flag`
|
||||||
58
docs/shape-libraries/kubernetes.md
Normal file
58
docs/shape-libraries/kubernetes.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# kubernetes
|
||||||
|
|
||||||
|
**Type:** mxgraph shapes
|
||||||
|
**Prefix:** `mxgraph.kubernetes`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mxCell value="label" style="shape=mxgraph.kubernetes.icon;prIcon={shape};fillColor=#326CE5;strokeColor=none;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Shapes (41)
|
||||||
|
|
||||||
|
- `api`
|
||||||
|
- `c_c_m`
|
||||||
|
- `c_m`
|
||||||
|
- `c_role`
|
||||||
|
- `cm`
|
||||||
|
- `crb`
|
||||||
|
- `crd`
|
||||||
|
- `cronjob`
|
||||||
|
- `deploy`
|
||||||
|
- `ds`
|
||||||
|
- `ep`
|
||||||
|
- `etcd`
|
||||||
|
- `frame`
|
||||||
|
- `group`
|
||||||
|
- `hpa`
|
||||||
|
- `ing`
|
||||||
|
- `job`
|
||||||
|
- `k_proxy`
|
||||||
|
- `kubelet`
|
||||||
|
- `limits`
|
||||||
|
- `master`
|
||||||
|
- `mxgraph.kubernetes`
|
||||||
|
- `netpol`
|
||||||
|
- `node`
|
||||||
|
- `ns`
|
||||||
|
- `pod`
|
||||||
|
- `psp`
|
||||||
|
- `pv`
|
||||||
|
- `pvc`
|
||||||
|
- `quota`
|
||||||
|
- `rb`
|
||||||
|
- `role`
|
||||||
|
- `rs`
|
||||||
|
- `sa`
|
||||||
|
- `sc`
|
||||||
|
- `sched`
|
||||||
|
- `secret`
|
||||||
|
- `sts`
|
||||||
|
- `svc`
|
||||||
|
- `user`
|
||||||
|
- `vol`
|
||||||
31
docs/shape-libraries/lean_mapping.md
Normal file
31
docs/shape-libraries/lean_mapping.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# lean_mapping
|
||||||
|
|
||||||
|
**Type:** mxgraph shapes
|
||||||
|
**Prefix:** `mxgraph.lean_mapping`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mxCell value="label" style="shape=mxgraph.lean_mapping.{shape};strokeWidth=2;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Shapes (14)
|
||||||
|
|
||||||
|
- `airplane_7`
|
||||||
|
- `electronic_info_flow`
|
||||||
|
- `finished_goods_to_customer`
|
||||||
|
- `go_see_production_scheduling`
|
||||||
|
- `kaizen_lightening_burst`
|
||||||
|
- `kanban_post`
|
||||||
|
- `load_leveling`
|
||||||
|
- `manual_info_flow`
|
||||||
|
- `move_by_forklift`
|
||||||
|
- `mrp_erp`
|
||||||
|
- `mxgraph.lean_mapping`
|
||||||
|
- `operator`
|
||||||
|
- `quality_problem`
|
||||||
|
- `verbal`
|
||||||
22
docs/shape-libraries/mscae.md
Normal file
22
docs/shape-libraries/mscae.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# mscae
|
||||||
|
|
||||||
|
**Type:** mxgraph shapes
|
||||||
|
**Prefix:** `mxgraph.mscae`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mxCell value="label" style="shape=mxgraph.mscae.cloud.azure;fillColor=#0078D4;strokeColor=none;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Categories
|
||||||
|
|
||||||
|
Shapes are organized by category: `mscae.cloud`, `mscae.intune`, `mscae.oms`, `mscae.system_center`
|
||||||
|
|
||||||
|
- `conditional_access_exchange`
|
||||||
|
- `conditional_access_sharepoint`
|
||||||
|
- `primary_site`
|
||||||
|
|
||||||
|
(See draw.io for complete shape list within each category)
|
||||||
72
docs/shape-libraries/network.md
Normal file
72
docs/shape-libraries/network.md
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# network
|
||||||
|
|
||||||
|
**Type:** mxgraph shapes
|
||||||
|
**Prefix:** `mxgraph.networks`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mxCell value="label" style="shape=mxgraph.networks.server;fillColor=#29AAE1;strokeColor=none;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Shapes (57)
|
||||||
|
|
||||||
|
- `biometric_reader`
|
||||||
|
- `bus`
|
||||||
|
- `business_center`
|
||||||
|
- `cloud`
|
||||||
|
- `comm_link`
|
||||||
|
- `comm_link_edge`
|
||||||
|
- `community`
|
||||||
|
- `copier`
|
||||||
|
- `desktop_pc`
|
||||||
|
- `external_storage`
|
||||||
|
- `firewall`
|
||||||
|
- `gamepad`
|
||||||
|
- `hub`
|
||||||
|
- `laptop`
|
||||||
|
- `load_balancer`
|
||||||
|
- `mail_server`
|
||||||
|
- `mainframe`
|
||||||
|
- `mobile`
|
||||||
|
- `modem`
|
||||||
|
- `monitor`
|
||||||
|
- `nas_filer`
|
||||||
|
- `patch_panel`
|
||||||
|
- `phone_1`
|
||||||
|
- `phone_2`
|
||||||
|
- `printer`
|
||||||
|
- `proxy_server`
|
||||||
|
- `rack`
|
||||||
|
- `radio_tower`
|
||||||
|
- `router`
|
||||||
|
- `satellite`
|
||||||
|
- `satellite_dish`
|
||||||
|
- `scanner`
|
||||||
|
- `secured`
|
||||||
|
- `security_camera`
|
||||||
|
- `server`
|
||||||
|
- `server_storage`
|
||||||
|
- `storage`
|
||||||
|
- `supercomputer`
|
||||||
|
- `switch`
|
||||||
|
- `tablet`
|
||||||
|
- `tape_storage`
|
||||||
|
- `terminal`
|
||||||
|
- `unsecure`
|
||||||
|
- `ups_enterprise`
|
||||||
|
- `ups_small`
|
||||||
|
- `usb_stick`
|
||||||
|
- `user_female`
|
||||||
|
- `user_male`
|
||||||
|
- `users`
|
||||||
|
- `video_projector`
|
||||||
|
- `video_projector_screen`
|
||||||
|
- `virtual_pc`
|
||||||
|
- `virtual_server`
|
||||||
|
- `virus`
|
||||||
|
- `web_server`
|
||||||
|
- `wireless_hub`
|
||||||
|
- `wireless_modem`
|
||||||
36
docs/shape-libraries/openstack.md
Normal file
36
docs/shape-libraries/openstack.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# openstack
|
||||||
|
|
||||||
|
**Type:** mxgraph shapes
|
||||||
|
**Prefix:** `mxgraph.openstack`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mxCell value="label" style="shape=mxgraph.openstack.{shape};fillColor=#3F51B5;strokeColor=none;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Shapes (19)
|
||||||
|
|
||||||
|
- `cinder_volume`
|
||||||
|
- `cinder_volumeattachment`
|
||||||
|
- `designate_recordset`
|
||||||
|
- `designate_zone`
|
||||||
|
- `heat_autoscalinggroup`
|
||||||
|
- `heat_resourcegroup`
|
||||||
|
- `heat_scalingpolicy`
|
||||||
|
- `mxgraph.openstack`
|
||||||
|
- `neutron_floatingip`
|
||||||
|
- `neutron_floatingipassociation`
|
||||||
|
- `neutron_net`
|
||||||
|
- `neutron_port`
|
||||||
|
- `neutron_router`
|
||||||
|
- `neutron_routerinterface`
|
||||||
|
- `neutron_securitygroup`
|
||||||
|
- `neutron_subnet`
|
||||||
|
- `nova_keypair`
|
||||||
|
- `nova_server`
|
||||||
|
- `swift_container`
|
||||||
22
docs/shape-libraries/pid.md
Normal file
22
docs/shape-libraries/pid.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# pid
|
||||||
|
|
||||||
|
**Type:** mxgraph shapes
|
||||||
|
**Prefix:** `mxgraph.pid2valves`, `mxgraph.pid2inst`, `mxgraph.pid2misc`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mxCell value="label" style="shape=mxgraph.pid2valves.valve;valveType=gate;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Valve Types
|
||||||
|
|
||||||
|
For `mxgraph.pid2valves.valve`, use `valveType=` with:
|
||||||
|
- `gate`, `globe`, `needle`, `ball`, `butterfly`, `diaphragm`, `plug`, `check`
|
||||||
|
|
||||||
|
## Other Prefixes
|
||||||
|
|
||||||
|
- `mxgraph.pid2inst` - Instruments (discInst, sharedCont, compFunc)
|
||||||
|
- `mxgraph.pid2misc` - Miscellaneous
|
||||||
57
docs/shape-libraries/rack.md
Normal file
57
docs/shape-libraries/rack.md
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# rack
|
||||||
|
|
||||||
|
**Type:** mxgraph shapes
|
||||||
|
**Prefix:** `mxgraph.rack`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mxCell value="label" style="shape=mxgraph.rack.f5.arx_500;strokeColor=#666666;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="0" y="0" width="200" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
Shapes are organized by vendor: `mxgraph.rack.{vendor}.{model}`
|
||||||
|
|
||||||
|
## Vendors
|
||||||
|
|
||||||
|
### F5
|
||||||
|
|
||||||
|
- `arx_500`
|
||||||
|
- `big_ip_1600`
|
||||||
|
- `big_ip_2000`
|
||||||
|
- `big_ip_4000`
|
||||||
|
|
||||||
|
### Dell
|
||||||
|
|
||||||
|
- `dell_poweredge_1u`
|
||||||
|
- `poweredge_630`
|
||||||
|
- `poweredge_730`
|
||||||
|
|
||||||
|
### HPE Aruba
|
||||||
|
|
||||||
|
HPE Aruba shapes have subcategories: `mxgraph.rack.hpe_aruba.{category}.{model}`
|
||||||
|
|
||||||
|
**gateways_controllers:**
|
||||||
|
- `aruba_7010_mobility_controller_front`
|
||||||
|
- `aruba_7010_mobility_controller_rear`
|
||||||
|
- `aruba_7024_mobility_controller_front`
|
||||||
|
- `aruba_7205_mobility_controller_front`
|
||||||
|
|
||||||
|
**security:**
|
||||||
|
- `aruba_clearpass_c1000_front`
|
||||||
|
- `aruba_clearpass_c2000_front`
|
||||||
|
- `aruba_clearpass_c3000_front`
|
||||||
|
|
||||||
|
**switches:**
|
||||||
|
- `j9772a_2530_48g_poeplus_switch`
|
||||||
|
- `j9773a_2530_24g_poeplus_switch`
|
||||||
|
- `jl253a_aruba_2930f_24g_4sfpplus_switch`
|
||||||
|
|
||||||
|
### General (rackGeneral)
|
||||||
|
|
||||||
|
Use `mxgraph.rackGeneral.{shape}` for generic rack items:
|
||||||
|
- `rackCabinet3`
|
||||||
|
- `plate`
|
||||||
|
|
||||||
|
(See draw.io Rack shape library for complete list)
|
||||||
116
docs/shape-libraries/salesforce.md
Normal file
116
docs/shape-libraries/salesforce.md
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
# salesforce
|
||||||
|
|
||||||
|
**Type:** mxgraph shapes
|
||||||
|
**Prefix:** `mxgraph.salesforce`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mxCell value="label" style="shape=mxgraph.salesforce.analytics;fillColor=#7f8de1;strokeColor=none;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace `analytics` with any shape from the list below.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Shapes (97)
|
||||||
|
|
||||||
|
- `analytics`
|
||||||
|
- `analytics2`
|
||||||
|
- `apps`
|
||||||
|
- `apps2`
|
||||||
|
- `automation`
|
||||||
|
- `automation2`
|
||||||
|
- `automotive`
|
||||||
|
- `automotive2`
|
||||||
|
- `bots`
|
||||||
|
- `bots2`
|
||||||
|
- `builders`
|
||||||
|
- `builders2`
|
||||||
|
- `channels`
|
||||||
|
- `channels2`
|
||||||
|
- `commerce`
|
||||||
|
- `commerce2`
|
||||||
|
- `communications`
|
||||||
|
- `communications2`
|
||||||
|
- `consumer_goods`
|
||||||
|
- `consumer_goods2`
|
||||||
|
- `customer_360`
|
||||||
|
- `customer_3602`
|
||||||
|
- `data`
|
||||||
|
- `data2`
|
||||||
|
- `education`
|
||||||
|
- `education2`
|
||||||
|
- `employees`
|
||||||
|
- `employees2`
|
||||||
|
- `energy`
|
||||||
|
- `energy2`
|
||||||
|
- `field_service`
|
||||||
|
- `field_service2`
|
||||||
|
- `financial_services`
|
||||||
|
- `financial_services2`
|
||||||
|
- `government`
|
||||||
|
- `government2`
|
||||||
|
- `health`
|
||||||
|
- `health2`
|
||||||
|
- `heroku`
|
||||||
|
- `heroku2`
|
||||||
|
- `inbox`
|
||||||
|
- `inbox2`
|
||||||
|
- `industries`
|
||||||
|
- `industries2`
|
||||||
|
- `integration`
|
||||||
|
- `integration2`
|
||||||
|
- `iot`
|
||||||
|
- `iot2`
|
||||||
|
- `learning`
|
||||||
|
- `learning2`
|
||||||
|
- `loyalty`
|
||||||
|
- `loyalty2`
|
||||||
|
- `manufacturing`
|
||||||
|
- `manufacturing2`
|
||||||
|
- `marketing`
|
||||||
|
- `marketing2`
|
||||||
|
- `media`
|
||||||
|
- `media2`
|
||||||
|
- `mxgraph.salesforce`
|
||||||
|
- `non_profit`
|
||||||
|
- `non_profit2`
|
||||||
|
- `partners`
|
||||||
|
- `partners2`
|
||||||
|
- `personalization`
|
||||||
|
- `personalization2`
|
||||||
|
- `philantrophy`
|
||||||
|
- `philantrophy2`
|
||||||
|
- `platform`
|
||||||
|
- `platform2`
|
||||||
|
- `privacy`
|
||||||
|
- `privacy2`
|
||||||
|
- `retail`
|
||||||
|
- `retail2`
|
||||||
|
- `sales`
|
||||||
|
- `sales2`
|
||||||
|
- `segments`
|
||||||
|
- `segments2`
|
||||||
|
- `service`
|
||||||
|
- `service2`
|
||||||
|
- `smb`
|
||||||
|
- `smb2`
|
||||||
|
- `social_studio`
|
||||||
|
- `social_studio2`
|
||||||
|
- `stream`
|
||||||
|
- `stream2`
|
||||||
|
- `success`
|
||||||
|
- `success2`
|
||||||
|
- `sustainability`
|
||||||
|
- `sustainability2`
|
||||||
|
- `transportation_and_technology`
|
||||||
|
- `transportation_and_technology2`
|
||||||
|
- `web`
|
||||||
|
- `web2`
|
||||||
|
- `work_com`
|
||||||
|
- `work_com2`
|
||||||
|
- `workflow`
|
||||||
|
- `workflow2`
|
||||||
179
docs/shape-libraries/sap.md
Normal file
179
docs/shape-libraries/sap.md
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
# sap
|
||||||
|
|
||||||
|
**Type:** SVG images
|
||||||
|
**Path:** `img/lib/sap/`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mxCell value="label" style="image;aspect=fixed;image=img/lib/sap/SAP_Logo.svg;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Shapes (164)
|
||||||
|
|
||||||
|
- `1`
|
||||||
|
- `2`
|
||||||
|
- `3`
|
||||||
|
- `4`
|
||||||
|
- `5`
|
||||||
|
- `6`
|
||||||
|
- `7`
|
||||||
|
- `8`
|
||||||
|
- `9`
|
||||||
|
- `10`
|
||||||
|
- `11`
|
||||||
|
- `12`
|
||||||
|
- `13`
|
||||||
|
- `Adapter`
|
||||||
|
- `Admin`
|
||||||
|
- `Alert`
|
||||||
|
- `API`
|
||||||
|
- `API_Business_Hub_Enterprise`
|
||||||
|
- `App`
|
||||||
|
- `Application_Autoscaler`
|
||||||
|
- `Application_Frontend_Service`
|
||||||
|
- `Application_Vulnerability_Report`
|
||||||
|
- `Building`
|
||||||
|
- `Business_Application_Studio`
|
||||||
|
- `Business_Entity_Recognition`
|
||||||
|
- `Business_Process_Model_Connector_for_SAP_Signavio_Solutions`
|
||||||
|
- `Cloud`
|
||||||
|
- `Cloud_Connector`
|
||||||
|
- `Cloud_Connector2`
|
||||||
|
- `Cloud_Integration_Automation`
|
||||||
|
- `Cloud_Integration_Automation2`
|
||||||
|
- `Cloud_Transport_Management`
|
||||||
|
- `Data_Attribute_Recommendation`
|
||||||
|
- `Deploy`
|
||||||
|
- `Desktop`
|
||||||
|
- `Devices`
|
||||||
|
- `Document`
|
||||||
|
- `Document_Information_Extraction`
|
||||||
|
- `Documents`
|
||||||
|
- `Edge_Integration_Cell`
|
||||||
|
- `Event`
|
||||||
|
- `Extensibility_Service`
|
||||||
|
- `Factory`
|
||||||
|
- `Feature`
|
||||||
|
- `HTML5_App_Repository`
|
||||||
|
- `Identity_Authentication`
|
||||||
|
- `Identity_Authentication2`
|
||||||
|
- `Identity_Directory`
|
||||||
|
- `Identity_Directory2`
|
||||||
|
- `Identity_Provisioning`
|
||||||
|
- `Identity_Provisioning2`
|
||||||
|
- `Info`
|
||||||
|
- `Intelligent_Situation_Automation`
|
||||||
|
- `Invoice_Object_Recommendation`
|
||||||
|
- `Invoice_Object_Recommendation2`
|
||||||
|
- `Key`
|
||||||
|
- `Landscape_Portal_for_SAP_S4HANA_Cloud_ABAP_Environment`
|
||||||
|
- `Link`
|
||||||
|
- `Locked`
|
||||||
|
- `Machine`
|
||||||
|
- `Message`
|
||||||
|
- `Mobile`
|
||||||
|
- `OAuth_20`
|
||||||
|
- `Object_Store_on_SAP_BTP`
|
||||||
|
- `On-Premise`
|
||||||
|
- `Personalized_Recommendation`
|
||||||
|
- `SAP_AI_Core`
|
||||||
|
- `SAP_AI_Launchpad`
|
||||||
|
- `SAP_Alert_Notification_service_for_SAP_BTP`
|
||||||
|
- `SAP_Analytics_Cloud`
|
||||||
|
- `SAP_Analytics_Cloud_Embedded_Edition`
|
||||||
|
- `SAP_Application_Logging_service_for_SAP_BTP`
|
||||||
|
- `SAP_Asset_Performance_Management`
|
||||||
|
- `SAP_Audit_Log_Service`
|
||||||
|
- `SAP_Authorization_Management_Service`
|
||||||
|
- `SAP_Authorization_and_Trust_Management_service`
|
||||||
|
- `SAP_Automation_Pilot`
|
||||||
|
- `SAP_BTP,_ABAP_environment`
|
||||||
|
- `SAP_BTP,_Cloud_Foundry_runtime`
|
||||||
|
- `SAP_BTP,_Kyma_runtime`
|
||||||
|
- `SAP_Build`
|
||||||
|
- `SAP_Build_Apps`
|
||||||
|
- `SAP_Build_Apps_-_Copy`
|
||||||
|
- `SAP_Build_Code`
|
||||||
|
- `SAP_Build_Process_Automation`
|
||||||
|
- `SAP_Build_Process_Automation_-_Copy`
|
||||||
|
- `SAP_Build_Work_Zone_-_Advanced_Edition`
|
||||||
|
- `SAP_Build_Work_Zone_-_Standard_Edition`
|
||||||
|
- `SAP_Business_Accelerator_Hub`
|
||||||
|
- `SAP_Business_Data_Cloud`
|
||||||
|
- `SAP_Cloud_ALM`
|
||||||
|
- `SAP_Cloud_Application_Programming_Model`
|
||||||
|
- `SAP_Cloud_Identity,_SAP_Malware_Scanning_Service`
|
||||||
|
- `SAP_Cloud_Identity_Service`
|
||||||
|
- `SAP_Cloud_Logging`
|
||||||
|
- `SAP_Cloud_Management_Service`
|
||||||
|
- `SAP_Cloud_Transport_Management`
|
||||||
|
- `SAP_Collaborative_Demand_and_Capacity_Management`
|
||||||
|
- `SAP_Connectivity_Service`
|
||||||
|
- `SAP_Content_Agent_Service`
|
||||||
|
- `SAP_Continuous_Integration_and_Delivery`
|
||||||
|
- `SAP_Credential_Store`
|
||||||
|
- `SAP_Custom_Domain_service`
|
||||||
|
- `SAP_Data_Privacy_Integration`
|
||||||
|
- `SAP_Data_Retention_Manager`
|
||||||
|
- `SAP_Datasphere`
|
||||||
|
- `SAP_Destination_service`
|
||||||
|
- `SAP_Digital_Assistant`
|
||||||
|
- `SAP_Digital_Assistant_Service`
|
||||||
|
- `SAP_Digital_Manufacturing`
|
||||||
|
- `SAP_Document_Grounding`
|
||||||
|
- `SAP_Document_Management_Service`
|
||||||
|
- `SAP_Event_Broker_for_SAP_Cloud_Applications`
|
||||||
|
- `SAP_Green_Token`
|
||||||
|
- `SAP_HANA_Cloud`
|
||||||
|
- `SAP_HANA_Spatial_Services`
|
||||||
|
- `SAP_Health_Data_Services_for_FHIR`
|
||||||
|
- `SAP_Integration_Suite`
|
||||||
|
- `SAP_Integration_Suite_-_API_Managment`
|
||||||
|
- `SAP_Integration_Suite_-_Advanced_Event_Mesh`
|
||||||
|
- `SAP_Integration_Suite_-_Cloud_Integration`
|
||||||
|
- `SAP_Integration_Suite_-_Data_Space_Integration`
|
||||||
|
- `SAP_Integration_Suite_-_Event_Mesh`
|
||||||
|
- `SAP_Integration_Suite_-_Integration_Advisor`
|
||||||
|
- `SAP_Integration_Suite_-_Integration_Assessment`
|
||||||
|
- `SAP_Integration_Suite_-_Migration_Assessment`
|
||||||
|
- `SAP_Integration_Suite_-_Open_Connectors`
|
||||||
|
- `SAP_Integration_Suite_-_SAP_Graph`
|
||||||
|
- `SAP_Integration_Suite_-_Trading_Partner_Management`
|
||||||
|
- `SAP_Job_Scheduling_service`
|
||||||
|
- `SAP_Keystore_Service`
|
||||||
|
- `SAP_Landscape_Management_Cloud`
|
||||||
|
- `SAP_Logo`
|
||||||
|
- `SAP_Master_Data_Governance`
|
||||||
|
- `SAP_Master_Data_Integration`
|
||||||
|
- `SAP_Mobile_Services`
|
||||||
|
- `SAP_Monitoring_service_for_SAP_BTP`
|
||||||
|
- `SAP_Omnichannel_Promotion_Pricing`
|
||||||
|
- `SAP_PKI_Certificate_Service`
|
||||||
|
- `SAP_Persistence_Service_ASE`
|
||||||
|
- `SAP_Personal_Data_Manager`
|
||||||
|
- `SAP_Private_Link_service`
|
||||||
|
- `SAP_Project_and_Resource_Management`
|
||||||
|
- `SAP_Responsibility_Management_Service`
|
||||||
|
- `SAP_S4HANA_Cloud_for_Intelligent_Intercompany_Reconciliation`
|
||||||
|
- `SAP_S4HANA_for_MS_Teams`
|
||||||
|
- `SAP_Secure_Login_Service_for_SAP_GUI`
|
||||||
|
- `SAP_Service_Manager`
|
||||||
|
- `SAP_Software_as_a_Service_Provisioning_Service`
|
||||||
|
- `SAP_Solution_Lifecycle_Management_Service`
|
||||||
|
- `SAP_Sustainability_Data_Exchange`
|
||||||
|
- `SAP_Task_Center`
|
||||||
|
- `SAP_Translation_Hub`
|
||||||
|
- `SAP_Variant_Configuration_and_Pricing`
|
||||||
|
- `SAP_Watch_List_Screening`
|
||||||
|
- `Service_Ticket_Intelligence`
|
||||||
|
- `Service_Ticket_Intelligence2`
|
||||||
|
- `Settings`
|
||||||
|
- `Success`
|
||||||
|
- `Third_Party`
|
||||||
|
- `UI5_flexibility_for_key_users`
|
||||||
|
- `UI_Theme_Designer`
|
||||||
|
- `User`
|
||||||
|
- `Web`
|
||||||
68
docs/shape-libraries/sitemap.md
Normal file
68
docs/shape-libraries/sitemap.md
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# sitemap
|
||||||
|
|
||||||
|
**Type:** mxgraph shapes
|
||||||
|
**Prefix:** `mxgraph.sitemap`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mxCell value="label" style="shape=mxgraph.sitemap.{shape};fillColor=#7ea6e0;strokeColor=#6c8ebf;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Shapes (51)
|
||||||
|
|
||||||
|
- `about_us`
|
||||||
|
- `audio`
|
||||||
|
- `biography`
|
||||||
|
- `blog`
|
||||||
|
- `calendar`
|
||||||
|
- `chart`
|
||||||
|
- `chat`
|
||||||
|
- `cloud`
|
||||||
|
- `contact`
|
||||||
|
- `contact_us`
|
||||||
|
- `document`
|
||||||
|
- `download`
|
||||||
|
- `error`
|
||||||
|
- `faq`
|
||||||
|
- `form`
|
||||||
|
- `gallery`
|
||||||
|
- `game`
|
||||||
|
- `home`
|
||||||
|
- `info`
|
||||||
|
- `jobs`
|
||||||
|
- `log`
|
||||||
|
- `login`
|
||||||
|
- `mail`
|
||||||
|
- `map`
|
||||||
|
- `mxgraph.sitemap`
|
||||||
|
- `news`
|
||||||
|
- `page`
|
||||||
|
- `payment`
|
||||||
|
- `photo`
|
||||||
|
- `portfolio`
|
||||||
|
- `post`
|
||||||
|
- `pricing`
|
||||||
|
- `print`
|
||||||
|
- `products`
|
||||||
|
- `profile`
|
||||||
|
- `references`
|
||||||
|
- `script`
|
||||||
|
- `search`
|
||||||
|
- `security`
|
||||||
|
- `services`
|
||||||
|
- `settings`
|
||||||
|
- `shopping`
|
||||||
|
- `sitemap`
|
||||||
|
- `slideshow`
|
||||||
|
- `sports`
|
||||||
|
- `success`
|
||||||
|
- `text`
|
||||||
|
- `upload`
|
||||||
|
- `user`
|
||||||
|
- `video`
|
||||||
|
- `warning`
|
||||||
112
docs/shape-libraries/vvd.md
Normal file
112
docs/shape-libraries/vvd.md
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
# vvd
|
||||||
|
|
||||||
|
**Type:** mxgraph shapes
|
||||||
|
**Prefix:** `mxgraph.vvd`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mxCell value="label" style="shape=mxgraph.vvd.{shape};fillColor=#00AEEF;strokeColor=none;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Shapes (95)
|
||||||
|
|
||||||
|
- `administrator`
|
||||||
|
- `app`
|
||||||
|
- `app_volumes_manager`
|
||||||
|
- `appstack_volume`
|
||||||
|
- `array_manager`
|
||||||
|
- `blueprint`
|
||||||
|
- `business_continuity_data_protection`
|
||||||
|
- `cd`
|
||||||
|
- `cloud_computing`
|
||||||
|
- `collective_nsx_esg`
|
||||||
|
- `consumption_plane`
|
||||||
|
- `cpu`
|
||||||
|
- `datacenter`
|
||||||
|
- `datastore`
|
||||||
|
- `disk`
|
||||||
|
- `document`
|
||||||
|
- `edge_gateway`
|
||||||
|
- `endpoint`
|
||||||
|
- `ethernet_port`
|
||||||
|
- `external_networks`
|
||||||
|
- `flash_drive`
|
||||||
|
- `folder`
|
||||||
|
- `guest_agent_customization`
|
||||||
|
- `horizon`
|
||||||
|
- `infrastructure`
|
||||||
|
- `key`
|
||||||
|
- `keyboard`
|
||||||
|
- `laptop`
|
||||||
|
- `log_files`
|
||||||
|
- `logical_distribution`
|
||||||
|
- `logical_firewall`
|
||||||
|
- `machine`
|
||||||
|
- `memory`
|
||||||
|
- `monitor`
|
||||||
|
- `mouse`
|
||||||
|
- `mxgraph.vvd`
|
||||||
|
- `networking`
|
||||||
|
- `networks`
|
||||||
|
- `nfvo`
|
||||||
|
- `nsx`
|
||||||
|
- `nsx_controller`
|
||||||
|
- `nsx_dashboard`
|
||||||
|
- `nsx_edge_and_load_balancer`
|
||||||
|
- `nsx_esg`
|
||||||
|
- `nsx_manager`
|
||||||
|
- `nsx_public_cloud_gateway`
|
||||||
|
- `on_demand_self_service`
|
||||||
|
- `ovdc_networks`
|
||||||
|
- `pair_sites`
|
||||||
|
- `phone`
|
||||||
|
- `physical_network_adapter`
|
||||||
|
- `physical_storage`
|
||||||
|
- `physical_upstream_router`
|
||||||
|
- `platform_services_controller`
|
||||||
|
- `protection_group`
|
||||||
|
- `protection_group_config`
|
||||||
|
- `recovery_plan`
|
||||||
|
- `resource_pool`
|
||||||
|
- `scsi_controller`
|
||||||
|
- `security`
|
||||||
|
- `server`
|
||||||
|
- `service_provider_cloud_environment`
|
||||||
|
- `site`
|
||||||
|
- `site_container`
|
||||||
|
- `site_recovery`
|
||||||
|
- `site_recovery_functional_icon`
|
||||||
|
- `ssd`
|
||||||
|
- `storage`
|
||||||
|
- `switch`
|
||||||
|
- `telco_network`
|
||||||
|
- `template`
|
||||||
|
- `tenant_key`
|
||||||
|
- `user_group`
|
||||||
|
- `vapp_network`
|
||||||
|
- `vcenter_server`
|
||||||
|
- `vcloud_director`
|
||||||
|
- `virtual_appliance`
|
||||||
|
- `virtual_machine`
|
||||||
|
- `virtual_switch`
|
||||||
|
- `vm_group`
|
||||||
|
- `vnf_m`
|
||||||
|
- `volumes_agent`
|
||||||
|
- `vpn`
|
||||||
|
- `vrealize_automation`
|
||||||
|
- `vrealize_log_insight`
|
||||||
|
- `vrealize_operations`
|
||||||
|
- `vrealize_orchestrator`
|
||||||
|
- `vrops`
|
||||||
|
- `vsan`
|
||||||
|
- `vshield`
|
||||||
|
- `vxlan`
|
||||||
|
- `wavefront`
|
||||||
|
- `web_browser`
|
||||||
|
- `wi_fi`
|
||||||
|
- `writable_volume`
|
||||||
194
docs/shape-libraries/webicons.md
Normal file
194
docs/shape-libraries/webicons.md
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
# webicons
|
||||||
|
|
||||||
|
**Type:** mxgraph shapes
|
||||||
|
**Prefix:** `mxgraph.webicons`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mxCell value="label" style="shape=mxgraph.webicons.{shape};fillColor=#3b5998;strokeColor=none;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Shapes (177)
|
||||||
|
|
||||||
|
- `adfty`
|
||||||
|
- `adobe_pdf`
|
||||||
|
- `aim`
|
||||||
|
- `allvoices`
|
||||||
|
- `amazon`
|
||||||
|
- `amazon_2`
|
||||||
|
- `android`
|
||||||
|
- `apache`
|
||||||
|
- `apple`
|
||||||
|
- `apple_classic`
|
||||||
|
- `arduino`
|
||||||
|
- `ask`
|
||||||
|
- `atlassian`
|
||||||
|
- `audioboo`
|
||||||
|
- `aws`
|
||||||
|
- `aws_s3`
|
||||||
|
- `baidu`
|
||||||
|
- `bebo`
|
||||||
|
- `behance`
|
||||||
|
- `bing`
|
||||||
|
- `bitbucket`
|
||||||
|
- `blinklist`
|
||||||
|
- `blogger`
|
||||||
|
- `blogmarks`
|
||||||
|
- `bookmarks.fr`
|
||||||
|
- `box`
|
||||||
|
- `buddymarks`
|
||||||
|
- `buffer`
|
||||||
|
- `buzzfeed`
|
||||||
|
- `chrome`
|
||||||
|
- `citeulike`
|
||||||
|
- `confluence`
|
||||||
|
- `connotea`
|
||||||
|
- `dealsplus`
|
||||||
|
- `delicious`
|
||||||
|
- `designfloat`
|
||||||
|
- `deviantart`
|
||||||
|
- `digg`
|
||||||
|
- `diigo`
|
||||||
|
- `dopplr`
|
||||||
|
- `drawio1`
|
||||||
|
- `drawio2`
|
||||||
|
- `dribbble`
|
||||||
|
- `dropbox`
|
||||||
|
- `dropbox2`
|
||||||
|
- `drupal`
|
||||||
|
- `dzone`
|
||||||
|
- `ebay`
|
||||||
|
- `edmodo`
|
||||||
|
- `evernote`
|
||||||
|
- `facebook`
|
||||||
|
- `fancy`
|
||||||
|
- `fark`
|
||||||
|
- `fashiolista`
|
||||||
|
- `feed`
|
||||||
|
- `feedburner`
|
||||||
|
- `flickr`
|
||||||
|
- `folkd`
|
||||||
|
- `forrst`
|
||||||
|
- `fotolog`
|
||||||
|
- `freshbump`
|
||||||
|
- `fresqui`
|
||||||
|
- `friendfeed`
|
||||||
|
- `funp`
|
||||||
|
- `fwisp`
|
||||||
|
- `gabbr`
|
||||||
|
- `gamespot`
|
||||||
|
- `github`
|
||||||
|
- `gmail`
|
||||||
|
- `google`
|
||||||
|
- `google_drive`
|
||||||
|
- `google_hangout`
|
||||||
|
- `google_photos`
|
||||||
|
- `google_play`
|
||||||
|
- `google_play_light`
|
||||||
|
- `google_plus`
|
||||||
|
- `grooveshark`
|
||||||
|
- `hatena`
|
||||||
|
- `html5`
|
||||||
|
- `identi.ca`
|
||||||
|
- `instagram`
|
||||||
|
- `instapaper`
|
||||||
|
- `ios`
|
||||||
|
- `jamespot`
|
||||||
|
- `java`
|
||||||
|
- `joomla`
|
||||||
|
- `jquery`
|
||||||
|
- `json`
|
||||||
|
- `json_2`
|
||||||
|
- `last.fm`
|
||||||
|
- `linkagogo`
|
||||||
|
- `linkedin`
|
||||||
|
- `livejournal`
|
||||||
|
- `mail.ru`
|
||||||
|
- `meetup`
|
||||||
|
- `meneame`
|
||||||
|
- `messenger`
|
||||||
|
- `messenger_2`
|
||||||
|
- `messenger_3`
|
||||||
|
- `mind_body_green`
|
||||||
|
- `mongodb`
|
||||||
|
- `mxgraph.webicons`
|
||||||
|
- `myspace`
|
||||||
|
- `n4g`
|
||||||
|
- `netlog`
|
||||||
|
- `netvibes`
|
||||||
|
- `netvouz`
|
||||||
|
- `networkedblogs`
|
||||||
|
- `newsvine`
|
||||||
|
- `odnoklassniki`
|
||||||
|
- `oknotizie`
|
||||||
|
- `onedrive`
|
||||||
|
- `oracle`
|
||||||
|
- `paypal`
|
||||||
|
- `phone`
|
||||||
|
- `phonefavs`
|
||||||
|
- `pinterest`
|
||||||
|
- `plaxo`
|
||||||
|
- `playfire`
|
||||||
|
- `plurk`
|
||||||
|
- `pocket`
|
||||||
|
- `protopage`
|
||||||
|
- `readernaut`
|
||||||
|
- `reddit`
|
||||||
|
- `rss`
|
||||||
|
- `scoopit`
|
||||||
|
- `scribd`
|
||||||
|
- `segnalo`
|
||||||
|
- `sina`
|
||||||
|
- `sitejot`
|
||||||
|
- `skype`
|
||||||
|
- `skyrock`
|
||||||
|
- `slashdot`
|
||||||
|
- `sms`
|
||||||
|
- `socialvibe`
|
||||||
|
- `society6`
|
||||||
|
- `sonico`
|
||||||
|
- `soundcloud`
|
||||||
|
- `sourceforge`
|
||||||
|
- `sourceforge_2`
|
||||||
|
- `spring.me`
|
||||||
|
- `stackexchange`
|
||||||
|
- `stackoverflow`
|
||||||
|
- `startaid`
|
||||||
|
- `startlap`
|
||||||
|
- `steam`
|
||||||
|
- `stumbleupon`
|
||||||
|
- `stumpedia`
|
||||||
|
- `technorati`
|
||||||
|
- `translate`
|
||||||
|
- `tumblr`
|
||||||
|
- `tunein`
|
||||||
|
- `twitter`
|
||||||
|
- `two`
|
||||||
|
- `typepad`
|
||||||
|
- `viadeo`
|
||||||
|
- `viber`
|
||||||
|
- `viddler`
|
||||||
|
- `vimeo`
|
||||||
|
- `virb`
|
||||||
|
- `vkontakte`
|
||||||
|
- `wakoopa`
|
||||||
|
- `weheartit`
|
||||||
|
- `whatsapp`
|
||||||
|
- `wix`
|
||||||
|
- `wordpress`
|
||||||
|
- `wordpress_2`
|
||||||
|
- `xanga`
|
||||||
|
- `xerpi`
|
||||||
|
- `xing`
|
||||||
|
- `yahoo`
|
||||||
|
- `yahoo_2`
|
||||||
|
- `yammer`
|
||||||
|
- `yandex`
|
||||||
|
- `yelp`
|
||||||
|
- `yoolink`
|
||||||
|
- `youmob`
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# AI Provider Configuration
|
# AI Provider Configuration
|
||||||
# AI_PROVIDER: Which provider to use
|
# AI_PROVIDER: Which provider to use
|
||||||
# Options: bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow
|
# Options: bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow, gateway
|
||||||
# Default: bedrock
|
# Default: bedrock
|
||||||
AI_PROVIDER=bedrock
|
AI_PROVIDER=bedrock
|
||||||
|
|
||||||
@@ -68,6 +68,13 @@ AI_MODEL=global.anthropic.claude-sonnet-4-5-20250929-v1:0
|
|||||||
# SILICONFLOW_API_KEY=sk-...
|
# SILICONFLOW_API_KEY=sk-...
|
||||||
# SILICONFLOW_BASE_URL=https://api.siliconflow.com/v1 # Optional: switch to https://api.siliconflow.cn/v1 if needed
|
# SILICONFLOW_BASE_URL=https://api.siliconflow.com/v1 # Optional: switch to https://api.siliconflow.cn/v1 if needed
|
||||||
|
|
||||||
|
# Vercel AI Gateway Configuration
|
||||||
|
# Get your API key from: https://vercel.com/ai-gateway
|
||||||
|
# Model format: "provider/model" e.g., "openai/gpt-4o", "anthropic/claude-sonnet-4-5"
|
||||||
|
# AI_GATEWAY_API_KEY=...
|
||||||
|
# AI_GATEWAY_BASE_URL=https://your-custom-gateway.com/v1/ai # Optional: Custom Gateway URL (for local dev or self-hosted Gateway)
|
||||||
|
# # If not set, uses Vercel default: https://ai-gateway.vercel.sh/v1/ai
|
||||||
|
|
||||||
# Langfuse Observability (Optional)
|
# Langfuse Observability (Optional)
|
||||||
# Enable LLM tracing and analytics - https://langfuse.com
|
# Enable LLM tracing and analytics - https://langfuse.com
|
||||||
# LANGFUSE_PUBLIC_KEY=pk-lf-...
|
# LANGFUSE_PUBLIC_KEY=pk-lf-...
|
||||||
|
|||||||
29
hooks/use-dictionary.ts
Normal file
29
hooks/use-dictionary.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import React, { createContext, useContext } from "react"
|
||||||
|
import type { Dictionary } from "@/lib/i18n/dictionaries"
|
||||||
|
|
||||||
|
const DictionaryContext = createContext<Dictionary | null>(null)
|
||||||
|
|
||||||
|
export function DictionaryProvider({
|
||||||
|
children,
|
||||||
|
dictionary,
|
||||||
|
}: React.PropsWithChildren<{ dictionary: Dictionary }>) {
|
||||||
|
return React.createElement(
|
||||||
|
DictionaryContext.Provider,
|
||||||
|
{ value: dictionary },
|
||||||
|
children,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDictionary() {
|
||||||
|
const dict = useContext(DictionaryContext)
|
||||||
|
if (!dict) {
|
||||||
|
throw new Error(
|
||||||
|
"useDictionary must be used within a DictionaryProvider",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return dict
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useDictionary
|
||||||
@@ -2,6 +2,7 @@ import { createAmazonBedrock } from "@ai-sdk/amazon-bedrock"
|
|||||||
import { createAnthropic } from "@ai-sdk/anthropic"
|
import { createAnthropic } from "@ai-sdk/anthropic"
|
||||||
import { azure, createAzure } from "@ai-sdk/azure"
|
import { azure, createAzure } from "@ai-sdk/azure"
|
||||||
import { createDeepSeek, deepseek } from "@ai-sdk/deepseek"
|
import { createDeepSeek, deepseek } from "@ai-sdk/deepseek"
|
||||||
|
import { createGateway, gateway } from "@ai-sdk/gateway"
|
||||||
import { createGoogleGenerativeAI, google } from "@ai-sdk/google"
|
import { createGoogleGenerativeAI, google } from "@ai-sdk/google"
|
||||||
import { createOpenAI, openai } from "@ai-sdk/openai"
|
import { createOpenAI, openai } from "@ai-sdk/openai"
|
||||||
import { fromNodeProviderChain } from "@aws-sdk/credential-providers"
|
import { fromNodeProviderChain } from "@aws-sdk/credential-providers"
|
||||||
@@ -18,6 +19,7 @@ export type ProviderName =
|
|||||||
| "openrouter"
|
| "openrouter"
|
||||||
| "deepseek"
|
| "deepseek"
|
||||||
| "siliconflow"
|
| "siliconflow"
|
||||||
|
| "gateway"
|
||||||
|
|
||||||
interface ModelConfig {
|
interface ModelConfig {
|
||||||
model: any
|
model: any
|
||||||
@@ -42,6 +44,7 @@ const ALLOWED_CLIENT_PROVIDERS: ProviderName[] = [
|
|||||||
"openrouter",
|
"openrouter",
|
||||||
"deepseek",
|
"deepseek",
|
||||||
"siliconflow",
|
"siliconflow",
|
||||||
|
"gateway",
|
||||||
]
|
]
|
||||||
|
|
||||||
// Bedrock provider options for Anthropic beta features
|
// Bedrock provider options for Anthropic beta features
|
||||||
@@ -333,8 +336,10 @@ function buildProviderOptions(
|
|||||||
|
|
||||||
case "deepseek":
|
case "deepseek":
|
||||||
case "openrouter":
|
case "openrouter":
|
||||||
case "siliconflow": {
|
case "siliconflow":
|
||||||
|
case "gateway": {
|
||||||
// These providers don't have reasoning configs in AI SDK yet
|
// These providers don't have reasoning configs in AI SDK yet
|
||||||
|
// Gateway passes through to underlying providers which handle their own configs
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,6 +361,7 @@ const PROVIDER_ENV_VARS: Record<ProviderName, string | null> = {
|
|||||||
openrouter: "OPENROUTER_API_KEY",
|
openrouter: "OPENROUTER_API_KEY",
|
||||||
deepseek: "DEEPSEEK_API_KEY",
|
deepseek: "DEEPSEEK_API_KEY",
|
||||||
siliconflow: "SILICONFLOW_API_KEY",
|
siliconflow: "SILICONFLOW_API_KEY",
|
||||||
|
gateway: "AI_GATEWAY_API_KEY",
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -438,6 +444,16 @@ function validateProviderCredentials(provider: ProviderName): void {
|
|||||||
* - SILICONFLOW_BASE_URL: SiliconFlow endpoint (optional, defaults to https://api.siliconflow.com/v1)
|
* - SILICONFLOW_BASE_URL: SiliconFlow endpoint (optional, defaults to https://api.siliconflow.com/v1)
|
||||||
*/
|
*/
|
||||||
export function getAIModel(overrides?: ClientOverrides): ModelConfig {
|
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.
|
||||||
|
if (overrides?.baseUrl && !overrides?.apiKey) {
|
||||||
|
throw new Error(
|
||||||
|
`API key is required when using a custom base URL. ` +
|
||||||
|
`Please provide your own API key in Settings.`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Check if client is providing their own provider override
|
// Check if client is providing their own provider override
|
||||||
const isClientOverride = !!(overrides?.provider && overrides?.apiKey)
|
const isClientOverride = !!(overrides?.provider && overrides?.apiKey)
|
||||||
|
|
||||||
@@ -485,6 +501,7 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
|
|||||||
if (configured.length === 0) {
|
if (configured.length === 0) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`No AI provider configured. Please set one of the following API keys in your .env.local file:\n` +
|
`No AI provider configured. Please set one of the following API keys in your .env.local file:\n` +
|
||||||
|
`- AI_GATEWAY_API_KEY for Vercel AI Gateway\n` +
|
||||||
`- DEEPSEEK_API_KEY for DeepSeek\n` +
|
`- DEEPSEEK_API_KEY for DeepSeek\n` +
|
||||||
`- OPENAI_API_KEY for OpenAI\n` +
|
`- OPENAI_API_KEY for OpenAI\n` +
|
||||||
`- ANTHROPIC_API_KEY for Anthropic\n` +
|
`- ANTHROPIC_API_KEY for Anthropic\n` +
|
||||||
@@ -662,9 +679,30 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "gateway": {
|
||||||
|
// Vercel AI Gateway - unified access to multiple AI providers
|
||||||
|
// Model format: "provider/model" e.g., "openai/gpt-4o", "anthropic/claude-sonnet-4-5"
|
||||||
|
// See: https://vercel.com/ai-gateway
|
||||||
|
const apiKey = overrides?.apiKey || process.env.AI_GATEWAY_API_KEY
|
||||||
|
const baseURL =
|
||||||
|
overrides?.baseUrl || process.env.AI_GATEWAY_BASE_URL
|
||||||
|
// Only use custom configuration if explicitly set (local dev or custom Gateway)
|
||||||
|
// Otherwise undefined → AI SDK uses Vercel default (https://ai-gateway.vercel.sh/v1/ai) + OIDC
|
||||||
|
if (baseURL || overrides?.apiKey) {
|
||||||
|
const customGateway = createGateway({
|
||||||
|
apiKey,
|
||||||
|
...(baseURL && { baseURL }),
|
||||||
|
})
|
||||||
|
model = customGateway(modelId)
|
||||||
|
} else {
|
||||||
|
model = gateway(modelId)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unknown AI provider: ${provider}. Supported providers: bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow`,
|
`Unknown AI provider: ${provider}. Supported providers: bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow, gateway`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
6
lib/i18n/config.ts
Normal file
6
lib/i18n/config.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export const i18n = {
|
||||||
|
defaultLocale: "en",
|
||||||
|
locales: ["en", "zh", "ja"],
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type Locale = (typeof i18n)["locales"][number]
|
||||||
18
lib/i18n/dictionaries.ts
Normal file
18
lib/i18n/dictionaries.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import "server-only"
|
||||||
|
|
||||||
|
import type { Locale } from "./config"
|
||||||
|
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Dictionary = Awaited<ReturnType<(typeof dictionaries)["en"]>>
|
||||||
|
|
||||||
|
export const hasLocale = (locale: string): locale is Locale =>
|
||||||
|
locale in dictionaries
|
||||||
|
|
||||||
|
export async function getDictionary(locale: Locale): Promise<Dictionary> {
|
||||||
|
return dictionaries[locale]()
|
||||||
|
}
|
||||||
184
lib/i18n/dictionaries/en.json
Normal file
184
lib/i18n/dictionaries/en.json
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"save": "Save",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"close": "Close",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"clear": "Clear",
|
||||||
|
"edit": "Edit",
|
||||||
|
"delete": "Delete",
|
||||||
|
"loading": "Loading..",
|
||||||
|
"new": "NEW"
|
||||||
|
},
|
||||||
|
"nav": {
|
||||||
|
"about": "About",
|
||||||
|
"editor": "Editor",
|
||||||
|
"newChat": "Start fresh chat",
|
||||||
|
"settings": "Settings",
|
||||||
|
"hidePanel": "Hide chat panel (Ctrl+B)",
|
||||||
|
"showPanel": "Show chat panel (Ctrl+B)",
|
||||||
|
"aiChat": "AI Chat"
|
||||||
|
},
|
||||||
|
"providers": {
|
||||||
|
"useServerDefault": "Use Server Default",
|
||||||
|
"openai": "OpenAI",
|
||||||
|
"anthropic": "Anthropic",
|
||||||
|
"google": "Google",
|
||||||
|
"azure": "Azure OpenAI",
|
||||||
|
"openrouter": "OpenRouter",
|
||||||
|
"deepseek": "DeepSeek",
|
||||||
|
"siliconflow": "SiliconFlow"
|
||||||
|
},
|
||||||
|
"chat": {
|
||||||
|
"placeholder": "Describe your diagram or upload a file...",
|
||||||
|
"send": "Send",
|
||||||
|
"sending": "Sending...",
|
||||||
|
"sendMessage": "Send message",
|
||||||
|
"clearConversation": "Clear conversation",
|
||||||
|
"diagramHistory": "Diagram history",
|
||||||
|
"saveDiagram": "Save diagram",
|
||||||
|
"uploadFile": "Upload file (image, PDF, text)",
|
||||||
|
"minimalStyle": "Minimal",
|
||||||
|
"styledMode": "Styled",
|
||||||
|
"minimalTooltip": "Use minimal for faster generation (no colors)",
|
||||||
|
"regenerate": "Regenerate response",
|
||||||
|
"copyResponse": "Copy response",
|
||||||
|
"copied": "Copied!",
|
||||||
|
"failedToCopy": "Failed to copy",
|
||||||
|
"goodResponse": "Good response",
|
||||||
|
"badResponse": "Bad response",
|
||||||
|
"clickToEdit": "Click to edit",
|
||||||
|
"editMessage": "Edit message",
|
||||||
|
"saveAndSubmit": "Save & Submit"
|
||||||
|
},
|
||||||
|
"examples": {
|
||||||
|
"title": "Create diagrams with AI",
|
||||||
|
"subtitle": "Describe what you want to create or upload an image to replicate",
|
||||||
|
"quickExamples": "Quick Examples",
|
||||||
|
"paperToDiagram": "Paper to Diagram",
|
||||||
|
"paperDescription": "Upload .pdf, .txt, .md, .json, .csv, .py, .js, .ts and more",
|
||||||
|
"animatedDiagram": "Animated Diagram",
|
||||||
|
"animatedDescription": "Draw a transformer architecture with animated connectors",
|
||||||
|
"awsArchitecture": "AWS Architecture",
|
||||||
|
"awsDescription": "Create a cloud architecture diagram with AWS icons",
|
||||||
|
"replicateFlowchart": "Replicate Flowchart",
|
||||||
|
"replicateDescription": "Upload and replicate an existing flowchart",
|
||||||
|
"creativeDrawing": "Creative Drawing",
|
||||||
|
"creativeDescription": "Draw something fun and creative",
|
||||||
|
"cachedNote": "Examples are cached for instant response",
|
||||||
|
"mcpServer": "MCP Server",
|
||||||
|
"mcpDescription": "Use in Claude Desktop, VS Code & Cursor",
|
||||||
|
"preview": "PREVIEW"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "Settings",
|
||||||
|
"description": "Configure your application settings.",
|
||||||
|
"accessCode": "Access Code",
|
||||||
|
"accessCodePlaceholder": "Enter access code",
|
||||||
|
"accessCodeDescription": "Required to use this application.",
|
||||||
|
"aiProvider": "AI Provider Settings",
|
||||||
|
"aiProviderDescription": "Use your own API key to bypass usage limits. Your key is stored locally in your browser and is never stored on the server.",
|
||||||
|
"provider": "Provider",
|
||||||
|
"modelId": "Model ID",
|
||||||
|
"apiKey": "API Key",
|
||||||
|
"apiKeyPlaceholder": "Your API key",
|
||||||
|
"baseUrl": "Base URL (optional)",
|
||||||
|
"customEndpoint": "Custom endpoint URL",
|
||||||
|
"overrides": "Overrides",
|
||||||
|
"clearSettings": "Clear Settings",
|
||||||
|
"useServerDefault": "Use Server Default",
|
||||||
|
"theme": "Theme",
|
||||||
|
"themeDescription": "Dark/Light mode for interface and DrawIO canvas.",
|
||||||
|
"drawioStyle": "DrawIO Style",
|
||||||
|
"drawioStyleDescription": "Canvas style:",
|
||||||
|
"switchTo": "Switch to",
|
||||||
|
"minimal": "Minimal",
|
||||||
|
"sketch": "Sketch",
|
||||||
|
"closeProtection": "Close Protection",
|
||||||
|
"closeProtectionDescription": "Show confirmation when leaving the page."
|
||||||
|
},
|
||||||
|
"save": {
|
||||||
|
"title": "Save Diagram",
|
||||||
|
"description": "Choose a format and filename to save your diagram.",
|
||||||
|
"format": "Format",
|
||||||
|
"filename": "Filename",
|
||||||
|
"filenamePlaceholder": "Enter filename",
|
||||||
|
"formats": {
|
||||||
|
"drawio": "Draw.io XML",
|
||||||
|
"png": "PNG Image",
|
||||||
|
"svg": "SVG Image"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"history": {
|
||||||
|
"title": "Diagram History",
|
||||||
|
"description": "Here saved each diagram before AI modification.\nClick on a diagram to restore it",
|
||||||
|
"noHistory": "No history available yet. Send messages to create diagram history.",
|
||||||
|
"version": "Version",
|
||||||
|
"restoreTo": "Restore to Version {version}?"
|
||||||
|
},
|
||||||
|
"dialogs": {
|
||||||
|
"clearTitle": "Clear Everything?",
|
||||||
|
"clearDescription": "This will clear the current conversation and reset the diagram. This action cannot be undone.",
|
||||||
|
"clearEverything": "Clear Everything",
|
||||||
|
"clearSuccess": "Started a fresh chat"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"maxFiles": "Too many files. Maximum {max} allowed.",
|
||||||
|
"onlyMoreAllowed": "Only {slots} more file(s) allowed",
|
||||||
|
"fileExceeds": "\"{name}\" is {size} (exceeds {max}MB)",
|
||||||
|
"unsupportedType": "\"{name}\" is not a supported file type",
|
||||||
|
"filesRejected": "{count} files rejected:",
|
||||||
|
"andMore": "...and {count} more",
|
||||||
|
"invalidAccessCode": "Invalid or missing access code. Please configure it in Settings.",
|
||||||
|
"networkError": "Network error. Please check your connection.",
|
||||||
|
"retryLimit": "Auto-retry limit reached ({max}). Please try again manually.",
|
||||||
|
"validationFailed": "Diagram validation failed. Please try regenerating.",
|
||||||
|
"malformedXml": "AI generated invalid diagram XML. Please try regenerating.",
|
||||||
|
"failedToProcess": "Failed to process diagram. Please try regenerating.",
|
||||||
|
"sessionCorrupted": "Session data was corrupted. Starting fresh.",
|
||||||
|
"failedToSave": "Failed to save messages to localStorage",
|
||||||
|
"failedToRestore": "Failed to restore from localStorage",
|
||||||
|
"failedToPersist": "Failed to persist state before unload",
|
||||||
|
"failedToExport": "Error fetching chart data",
|
||||||
|
"failedToLoadExample": "Error loading example image"
|
||||||
|
},
|
||||||
|
"quota": {
|
||||||
|
"dailyLimit": "Daily Quota Reached",
|
||||||
|
"tokenLimit": "Daily Token Limit Reached",
|
||||||
|
"tpmLimit": "Rate Limit",
|
||||||
|
"tpmMessage": "Too many requests. Please wait a moment.",
|
||||||
|
"messageApi": "Oops — you've reached the daily API limit for this demo! As an indie developer covering all the API costs myself, I have to set these limits to keep things sustainable.",
|
||||||
|
"messageToken": "Oops — you've reached the daily token limit for this demo! As an indie developer covering all the API costs myself, I have to set these limits to keep things sustainable.",
|
||||||
|
"tip": "<strong>Tip:</strong> You can use your own API key (click the Settings icon) or self-host the project to bypass these limits.",
|
||||||
|
"reset": "Your limit resets tomorrow. Thanks for understanding!",
|
||||||
|
"selfHost": "Self-host",
|
||||||
|
"sponsor": "Sponsor",
|
||||||
|
"learnMore": "Learn more →",
|
||||||
|
"usedOf": "{used}/{limit}"
|
||||||
|
},
|
||||||
|
"tools": {
|
||||||
|
"generateDiagram": "Generate Diagram",
|
||||||
|
"editDiagram": "Edit Diagram",
|
||||||
|
"appendDiagram": "Continue Diagram",
|
||||||
|
"complete": "Complete",
|
||||||
|
"error": "Error",
|
||||||
|
"truncated": "Truncated"
|
||||||
|
},
|
||||||
|
"file": {
|
||||||
|
"reading": "Reading...",
|
||||||
|
"chars": "chars",
|
||||||
|
"removeFile": "Remove file"
|
||||||
|
},
|
||||||
|
"reasoning": {
|
||||||
|
"thinking": "Thinking...",
|
||||||
|
"thoughtFor": "Thought for {duration} seconds",
|
||||||
|
"thoughtBrief": "Thought for a few seconds"
|
||||||
|
},
|
||||||
|
"about": {
|
||||||
|
"modelChange": "Model Change & Usage Limits",
|
||||||
|
"walletCrying": "(Or: Why My Wallet is Crying)",
|
||||||
|
"seekingSponsorship": "Call for Sponsorship",
|
||||||
|
"contactMe": "Contact Me",
|
||||||
|
"usageNotice": "Due to high usage, I have changed the model from Claude to minimax-m2 and added some usage limits. See About page for details."
|
||||||
|
}
|
||||||
|
}
|
||||||
184
lib/i18n/dictionaries/ja.json
Normal file
184
lib/i18n/dictionaries/ja.json
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"save": "保存",
|
||||||
|
"cancel": "キャンセル",
|
||||||
|
"close": "閉じる",
|
||||||
|
"confirm": "確認",
|
||||||
|
"clear": "クリア",
|
||||||
|
"edit": "編集",
|
||||||
|
"delete": "削除",
|
||||||
|
"loading": "読み込み中..",
|
||||||
|
"new": "新規"
|
||||||
|
},
|
||||||
|
"nav": {
|
||||||
|
"about": "概要",
|
||||||
|
"editor": "エディタ",
|
||||||
|
"newChat": "新しいチャットを開始",
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"chat": {
|
||||||
|
"placeholder": "ダイアグラムを説明するか、ファイルをアップロード...",
|
||||||
|
"send": "送信",
|
||||||
|
"sending": "送信中...",
|
||||||
|
"sendMessage": "メッセージを送信",
|
||||||
|
"clearConversation": "会話をクリア",
|
||||||
|
"diagramHistory": "ダイアグラム履歴",
|
||||||
|
"saveDiagram": "ダイアグラムを保存",
|
||||||
|
"uploadFile": "ファイルをアップロード(画像、PDF、テキスト)",
|
||||||
|
"minimalStyle": "ミニマル",
|
||||||
|
"styledMode": "スタイル付き",
|
||||||
|
"minimalTooltip": "高速生成のためミニマルを使用(色なし)",
|
||||||
|
"regenerate": "応答を再生成",
|
||||||
|
"copyResponse": "応答をコピー",
|
||||||
|
"copied": "コピーしました!",
|
||||||
|
"failedToCopy": "コピーに失敗しました",
|
||||||
|
"goodResponse": "良い応答",
|
||||||
|
"badResponse": "悪い応答",
|
||||||
|
"clickToEdit": "クリックして編集",
|
||||||
|
"editMessage": "メッセージを編集",
|
||||||
|
"saveAndSubmit": "保存して送信"
|
||||||
|
},
|
||||||
|
"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": "アプリケーション設定を構成します。",
|
||||||
|
"accessCode": "アクセスコード",
|
||||||
|
"accessCodePlaceholder": "アクセスコードを入力",
|
||||||
|
"accessCodeDescription": "このアプリケーションを使用するために必要です。",
|
||||||
|
"aiProvider": "AI プロバイダー設定",
|
||||||
|
"aiProviderDescription": "独自の API キーを使用して使用制限を回避できます。キーはブラウザのローカルに保存され、サーバーには保存されません。",
|
||||||
|
"provider": "プロバイダー",
|
||||||
|
"modelId": "モデル ID",
|
||||||
|
"apiKey": "API キー",
|
||||||
|
"apiKeyPlaceholder": "あなたの API キー",
|
||||||
|
"baseUrl": "ベース URL(オプション)",
|
||||||
|
"customEndpoint": "カスタムエンドポイント URL",
|
||||||
|
"overrides": "上書き",
|
||||||
|
"clearSettings": "設定をクリア",
|
||||||
|
"useServerDefault": "サーバーデフォルトを使用",
|
||||||
|
"theme": "テーマ",
|
||||||
|
"themeDescription": "インターフェースと DrawIO キャンバスのダーク/ライトモード。",
|
||||||
|
"drawioStyle": "DrawIO スタイル",
|
||||||
|
"drawioStyleDescription": "キャンバススタイル:",
|
||||||
|
"switchTo": "切り替え",
|
||||||
|
"minimal": "ミニマル",
|
||||||
|
"sketch": "スケッチ",
|
||||||
|
"closeProtection": "ページ離脱確認",
|
||||||
|
"closeProtectionDescription": "ページを離れる際に確認を表示します。"
|
||||||
|
},
|
||||||
|
"save": {
|
||||||
|
"title": "ダイアグラムを保存",
|
||||||
|
"description": "形式とファイル名を選択してダイアグラムを保存します。",
|
||||||
|
"format": "形式",
|
||||||
|
"filename": "ファイル名",
|
||||||
|
"filenamePlaceholder": "ファイル名を入力",
|
||||||
|
"formats": {
|
||||||
|
"drawio": "Draw.io XML",
|
||||||
|
"png": "PNG 画像",
|
||||||
|
"svg": "SVG 画像"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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})。手動で再試行してください。",
|
||||||
|
"validationFailed": "ダイアグラムの検証に失敗しました。再生成してみてください。",
|
||||||
|
"malformedXml": "AI が無効なダイアグラム XML を生成しました。再生成してみてください。",
|
||||||
|
"failedToProcess": "ダイアグラムの処理に失敗しました。再生成してみてください。",
|
||||||
|
"sessionCorrupted": "セッションデータが破損しました。最初からやり直します。",
|
||||||
|
"failedToSave": "localStorage へのメッセージの保存に失敗しました",
|
||||||
|
"failedToRestore": "localStorage からの復元に失敗しました",
|
||||||
|
"failedToPersist": "アンロード前の状態の永続化に失敗しました",
|
||||||
|
"failedToExport": "チャートデータの取得エラー",
|
||||||
|
"failedToLoadExample": "例の画像の読み込みエラー"
|
||||||
|
},
|
||||||
|
"quota": {
|
||||||
|
"dailyLimit": "1日の割当量に達しました",
|
||||||
|
"tokenLimit": "1日のトークン制限に達しました",
|
||||||
|
"tpmLimit": "レート制限",
|
||||||
|
"tpmMessage": "リクエストが多すぎます。しばらくお待ちください。",
|
||||||
|
"messageApi": "おっと — このデモの1日の API 制限に達しました!個人開発者として API コストをすべて負担しているため、持続可能性を保つためにこれらの制限を設定する必要があります。",
|
||||||
|
"messageToken": "おっと — このデモの1日のトークン制限に達しました!個人開発者として API コストをすべて負担しているため、持続可能性を保つためにこれらの制限を設定する必要があります。",
|
||||||
|
"tip": "<strong>ヒント:</strong>独自の API キーを使用する(設定アイコンをクリック)か、プロジェクトをセルフホストしてこれらの制限を回避できます。",
|
||||||
|
"reset": "制限は明日リセットされます。ご理解ありがとうございます!",
|
||||||
|
"selfHost": "セルフホスト",
|
||||||
|
"sponsor": "スポンサー",
|
||||||
|
"learnMore": "詳細 →",
|
||||||
|
"usedOf": "{used}/{limit}"
|
||||||
|
},
|
||||||
|
"tools": {
|
||||||
|
"generateDiagram": "ダイアグラムを生成",
|
||||||
|
"editDiagram": "ダイアグラムを編集",
|
||||||
|
"appendDiagram": "ダイアグラムに追加",
|
||||||
|
"complete": "完了",
|
||||||
|
"error": "エラー",
|
||||||
|
"truncated": "切り捨て"
|
||||||
|
},
|
||||||
|
"file": {
|
||||||
|
"reading": "読み込み中...",
|
||||||
|
"chars": "文字",
|
||||||
|
"removeFile": "ファイルを削除"
|
||||||
|
},
|
||||||
|
"reasoning": {
|
||||||
|
"thinking": "考え中...",
|
||||||
|
"thoughtFor": "{duration} 秒考えました",
|
||||||
|
"thoughtBrief": "数秒考えました"
|
||||||
|
},
|
||||||
|
"about": {
|
||||||
|
"modelChange": "モデル変更と利用制限について",
|
||||||
|
"walletCrying": "(別名:お財布が悲鳴を上げています)",
|
||||||
|
"seekingSponsorship": "スポンサー募集",
|
||||||
|
"contactMe": "お問い合わせ",
|
||||||
|
"usageNotice": "利用量の増加に伴い、コスト削減のためモデルを Claude から minimax-m2 に変更し、いくつかの利用制限を設けました。詳細は概要ページをご覧ください。"
|
||||||
|
}
|
||||||
|
}
|
||||||
184
lib/i18n/dictionaries/zh.json
Normal file
184
lib/i18n/dictionaries/zh.json
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"save": "保存",
|
||||||
|
"cancel": "取消",
|
||||||
|
"close": "关闭",
|
||||||
|
"confirm": "确认",
|
||||||
|
"clear": "清除",
|
||||||
|
"edit": "编辑",
|
||||||
|
"delete": "删除",
|
||||||
|
"loading": "加载中...",
|
||||||
|
"new": "新建"
|
||||||
|
},
|
||||||
|
"nav": {
|
||||||
|
"about": "关于",
|
||||||
|
"editor": "编辑器",
|
||||||
|
"newChat": "开始新对话",
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"chat": {
|
||||||
|
"placeholder": "描述您的图表或上传文件...",
|
||||||
|
"send": "发送",
|
||||||
|
"sending": "发送中...",
|
||||||
|
"sendMessage": "发送消息",
|
||||||
|
"clearConversation": "清除对话",
|
||||||
|
"diagramHistory": "图表历史",
|
||||||
|
"saveDiagram": "保存图表",
|
||||||
|
"uploadFile": "上传文件(图片、PDF、文本)",
|
||||||
|
"minimalStyle": "简约",
|
||||||
|
"styledMode": "精致",
|
||||||
|
"minimalTooltip": "使用简约模式以加快生成速度(无颜色)",
|
||||||
|
"regenerate": "重新生成响应",
|
||||||
|
"copyResponse": "复制响应",
|
||||||
|
"copied": "已复制!",
|
||||||
|
"failedToCopy": "复制失败",
|
||||||
|
"goodResponse": "有帮助",
|
||||||
|
"badResponse": "无帮助",
|
||||||
|
"clickToEdit": "点击编辑",
|
||||||
|
"editMessage": "编辑消息",
|
||||||
|
"saveAndSubmit": "保存并提交"
|
||||||
|
},
|
||||||
|
"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": "配置您的应用程序设置。",
|
||||||
|
"accessCode": "访问码",
|
||||||
|
"accessCodePlaceholder": "输入访问码",
|
||||||
|
"accessCodeDescription": "使用此应用程序需要访问码。",
|
||||||
|
"aiProvider": "AI 提供商设置",
|
||||||
|
"aiProviderDescription": "使用您自己的 API 密钥来绕过使用限制。您的密钥仅存储在浏览器本地,不会存储在服务器上。",
|
||||||
|
"provider": "提供商",
|
||||||
|
"modelId": "模型 ID",
|
||||||
|
"apiKey": "API 密钥",
|
||||||
|
"apiKeyPlaceholder": "您的 API 密钥",
|
||||||
|
"baseUrl": "基础 URL(可选)",
|
||||||
|
"customEndpoint": "自定义端点 URL",
|
||||||
|
"overrides": "覆盖",
|
||||||
|
"clearSettings": "清除设置",
|
||||||
|
"useServerDefault": "使用服务器默认值",
|
||||||
|
"theme": "主题",
|
||||||
|
"themeDescription": "界面和 DrawIO 画布的深色/浅色模式。",
|
||||||
|
"drawioStyle": "DrawIO 样式",
|
||||||
|
"drawioStyleDescription": "画布样式:",
|
||||||
|
"switchTo": "切换到",
|
||||||
|
"minimal": "简约",
|
||||||
|
"sketch": "草图",
|
||||||
|
"closeProtection": "关闭确认",
|
||||||
|
"closeProtectionDescription": "离开页面时显示确认。"
|
||||||
|
},
|
||||||
|
"save": {
|
||||||
|
"title": "保存图表",
|
||||||
|
"description": "选择格式和文件名以保存您的图表。",
|
||||||
|
"format": "格式",
|
||||||
|
"filename": "文件名",
|
||||||
|
"filenamePlaceholder": "输入文件名",
|
||||||
|
"formats": {
|
||||||
|
"drawio": "Draw.io XML",
|
||||||
|
"png": "PNG 图片",
|
||||||
|
"svg": "SVG 图片"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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})。请手动重试。",
|
||||||
|
"validationFailed": "图表验证失败。请尝试重新生成。",
|
||||||
|
"malformedXml": "AI 生成的图表 XML 无效。请尝试重新生成。",
|
||||||
|
"failedToProcess": "无法处理图表。请尝试重新生成。",
|
||||||
|
"sessionCorrupted": "会话数据已损坏。重新开始。",
|
||||||
|
"failedToSave": "无法保存消息到 localStorage",
|
||||||
|
"failedToRestore": "无法从 localStorage 恢复",
|
||||||
|
"failedToPersist": "卸载前无法持久化状态",
|
||||||
|
"failedToExport": "获取图表数据时出错",
|
||||||
|
"failedToLoadExample": "加载示例图片时出错"
|
||||||
|
},
|
||||||
|
"quota": {
|
||||||
|
"dailyLimit": "已达每日配额",
|
||||||
|
"tokenLimit": "已达每日令牌限制",
|
||||||
|
"tpmLimit": "速率限制",
|
||||||
|
"tpmMessage": "请求过多。请稍等片刻。",
|
||||||
|
"messageApi": "糟糕 — 您已达到此演示的每日 API 限制!作为一名独立开发者,我自己承担所有 API 费用,因此必须设置这些限制以保持可持续性。",
|
||||||
|
"messageToken": "糟糕 — 您已达到此演示的每日令牌限制!作为一名独立开发者,我自己承担所有 API 费用,因此必须设置这些限制以保持可持续性。",
|
||||||
|
"tip": "<strong>提示:</strong>您可以使用自己的 API 密钥(点击设置图标)或自托管项目来绕过这些限制。",
|
||||||
|
"reset": "您的限制将在明天重置。感谢您的理解!",
|
||||||
|
"selfHost": "自托管",
|
||||||
|
"sponsor": "赞助",
|
||||||
|
"learnMore": "了解更多 →",
|
||||||
|
"usedOf": "{used}/{limit}"
|
||||||
|
},
|
||||||
|
"tools": {
|
||||||
|
"generateDiagram": "生成图表",
|
||||||
|
"editDiagram": "编辑图表",
|
||||||
|
"appendDiagram": "继续图表",
|
||||||
|
"complete": "完成",
|
||||||
|
"error": "错误",
|
||||||
|
"truncated": "已截断"
|
||||||
|
},
|
||||||
|
"file": {
|
||||||
|
"reading": "读取中...",
|
||||||
|
"chars": "字符",
|
||||||
|
"removeFile": "移除文件"
|
||||||
|
},
|
||||||
|
"reasoning": {
|
||||||
|
"thinking": "思考中...",
|
||||||
|
"thoughtFor": "思考了 {duration} 秒",
|
||||||
|
"thoughtBrief": "思考了几秒钟"
|
||||||
|
},
|
||||||
|
"about": {
|
||||||
|
"modelChange": "模型变更与用量限制",
|
||||||
|
"walletCrying": "(别名:我的钱包顶不住了)",
|
||||||
|
"seekingSponsorship": "寻求赞助(求大佬捞一把)",
|
||||||
|
"contactMe": "联系我",
|
||||||
|
"usageNotice": "由于使用量过高,我已将模型从 Claude 更换为 minimax-m2,并设置了一些用量限制。详情请查看关于页面。"
|
||||||
|
}
|
||||||
|
}
|
||||||
14
lib/i18n/utils.ts
Normal file
14
lib/i18n/utils.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export function formatMessage(
|
||||||
|
template: string | undefined,
|
||||||
|
vars?: Record<string, string | number | undefined>,
|
||||||
|
): string {
|
||||||
|
if (!template) return ""
|
||||||
|
if (!vars) return template
|
||||||
|
|
||||||
|
return template.replace(/\{(\w+)\}/g, (match, name) => {
|
||||||
|
const val = vars[name]
|
||||||
|
return val === undefined ? match : String(val)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default formatMessage
|
||||||
@@ -48,12 +48,19 @@ description: Continue generating diagram XML when display_diagram was truncated
|
|||||||
parameters: {
|
parameters: {
|
||||||
xml: string // Continuation fragment (NO wrapper tags like <mxGraphModel> or <root>)
|
xml: string // Continuation fragment (NO wrapper tags like <mxGraphModel> or <root>)
|
||||||
}
|
}
|
||||||
|
---Tool4---
|
||||||
|
tool name: get_shape_library
|
||||||
|
description: Get shape/icon library documentation. Use this to discover available icon shapes (AWS, Azure, GCP, Kubernetes, etc.) before creating diagrams with cloud/tech icons.
|
||||||
|
parameters: {
|
||||||
|
library: string // Library name: aws4, azure2, gcp2, kubernetes, cisco19, flowchart, bpmn, etc.
|
||||||
|
}
|
||||||
---End of tools---
|
---End of tools---
|
||||||
|
|
||||||
IMPORTANT: Choose the right tool:
|
IMPORTANT: Choose the right tool:
|
||||||
- Use display_diagram for: Creating new diagrams, major restructuring, or when the current diagram XML is empty
|
- Use display_diagram for: Creating new diagrams, major restructuring, or when the current diagram XML is empty
|
||||||
- Use edit_diagram for: Small modifications, adding/removing elements, changing text/colors, repositioning items
|
- Use edit_diagram for: Small modifications, adding/removing elements, changing text/colors, repositioning items
|
||||||
- Use append_diagram for: ONLY when display_diagram was truncated due to output length - continue generating from where you stopped
|
- Use append_diagram for: ONLY when display_diagram was truncated due to output length - continue generating from where you stopped
|
||||||
|
- Use get_shape_library for: Discovering available icons/shapes when creating cloud architecture or technical diagrams (call BEFORE display_diagram)
|
||||||
|
|
||||||
Core capabilities:
|
Core capabilities:
|
||||||
- Generate valid, well-formed XML strings for draw.io diagrams
|
- Generate valid, well-formed XML strings for draw.io diagrams
|
||||||
@@ -84,23 +91,19 @@ Note that:
|
|||||||
- When artistic drawings are requested, creatively compose them using standard diagram shapes and connectors while maintaining visual clarity.
|
- When artistic drawings are requested, creatively compose them using standard diagram shapes and connectors while maintaining visual clarity.
|
||||||
- Return XML only via tool calls, never in text responses.
|
- Return XML only via tool calls, never in text responses.
|
||||||
- If user asks you to replicate a diagram based on an image, remember to match the diagram style and layout as closely as possible. Especially, pay attention to the lines and shapes, for example, if the lines are straight or curved, and if the shapes are rounded or square.
|
- If user asks you to replicate a diagram based on an image, remember to match the diagram style and layout as closely as possible. Especially, pay attention to the lines and shapes, for example, if the lines are straight or curved, and if the shapes are rounded or square.
|
||||||
- Note that when you need to generate diagram about aws architecture, use **AWS 2025 icons**.
|
- For cloud/tech diagrams (AWS, Azure, GCP, K8s), call get_shape_library first to discover available icon shapes and their syntax.
|
||||||
- NEVER include XML comments (<!-- ... -->) in your generated XML. Draw.io strips comments, which breaks edit_diagram patterns.
|
- NEVER include XML comments (<!-- ... -->) in your generated XML. Draw.io strips comments, which breaks edit_diagram patterns.
|
||||||
|
|
||||||
When using edit_diagram tool:
|
When using edit_diagram tool:
|
||||||
- CRITICAL: Copy search patterns EXACTLY from the "Current diagram XML" in system context - attribute order matters!
|
- Use operations: update (modify cell by id), add (new cell), delete (remove cell by id)
|
||||||
- Always include the element's id attribute for unique targeting: {"search": "<mxCell id=\\"5\\"", ...}
|
- For update/add: provide cell_id and complete new_xml (full mxCell element including mxGeometry)
|
||||||
- Include complete elements (mxCell + mxGeometry) for reliable matching
|
- For delete: only cell_id is needed
|
||||||
- Preserve exact whitespace, indentation, and line breaks
|
- Find the cell_id from "Current diagram XML" in system context
|
||||||
- BAD: {"search": "value=\\"Label\\"", ...} - too vague, matches multiple elements
|
- Example update: {"operations": [{"type": "update", "cell_id": "3", "new_xml": "<mxCell id=\\"3\\" value=\\"New Label\\" style=\\"rounded=1;\\" vertex=\\"1\\" parent=\\"1\\">\\n <mxGeometry x=\\"100\\" y=\\"100\\" width=\\"120\\" height=\\"60\\" as=\\"geometry\\"/>\\n</mxCell>"}]}
|
||||||
- GOOD: {"search": "<mxCell id=\\"3\\" value=\\"Old\\" style=\\"...\\">", "replace": "<mxCell id=\\"3\\" value=\\"New\\" style=\\"...\\">"}
|
- Example delete: {"operations": [{"type": "delete", "cell_id": "5"}]}
|
||||||
- For multiple changes, use separate edits in array
|
- Example add: {"operations": [{"type": "add", "cell_id": "new1", "new_xml": "<mxCell id=\\"new1\\" value=\\"New Box\\" style=\\"rounded=1;\\" vertex=\\"1\\" parent=\\"1\\">\\n <mxGeometry x=\\"400\\" y=\\"200\\" width=\\"120\\" height=\\"60\\" as=\\"geometry\\"/>\\n</mxCell>"}]}
|
||||||
- RETRY POLICY: If pattern not found, retry up to 3 times with adjusted patterns. After 3 failures, use display_diagram instead.
|
|
||||||
|
|
||||||
⚠️ CRITICAL JSON ESCAPING: When outputting edit_diagram tool calls, you MUST escape ALL double quotes inside string values:
|
⚠️ JSON ESCAPING: Every " inside new_xml MUST be escaped as \\". Example: id=\\"5\\" value=\\"Label\\"
|
||||||
- CORRECT: "y=\\"119\\"" (both quotes escaped)
|
|
||||||
- WRONG: "y="119\\"" (missing backslash before first quote - causes JSON parse error!)
|
|
||||||
- Every " inside a JSON string value needs \\" - no exceptions!
|
|
||||||
|
|
||||||
## Draw.io XML Structure Reference
|
## Draw.io XML Structure Reference
|
||||||
|
|
||||||
@@ -268,69 +271,43 @@ const EXTENDED_ADDITIONS = `
|
|||||||
|
|
||||||
### edit_diagram Details
|
### edit_diagram Details
|
||||||
|
|
||||||
**CRITICAL RULES:**
|
edit_diagram uses ID-based operations to modify cells directly by their id attribute.
|
||||||
- Copy-paste the EXACT search pattern from the "Current diagram XML" in system context
|
|
||||||
- Do NOT reorder attributes or reformat - the attribute order in draw.io XML varies and you MUST match it exactly
|
**Operations:**
|
||||||
- Only include the lines that are changing, plus 1-2 surrounding lines for context if needed
|
- **update**: Replace an existing cell. Provide cell_id and new_xml.
|
||||||
- Break large changes into multiple smaller edits
|
- **add**: Add a new cell. Provide cell_id (new unique id) and new_xml.
|
||||||
- Each search must contain complete lines (never truncate mid-line)
|
- **delete**: Remove a cell. Only cell_id is needed.
|
||||||
- First match only - be specific enough to target the right element
|
|
||||||
|
|
||||||
**Input Format:**
|
**Input Format:**
|
||||||
\`\`\`json
|
\`\`\`json
|
||||||
{
|
{
|
||||||
"edits": [
|
"operations": [
|
||||||
{
|
{"type": "update", "cell_id": "3", "new_xml": "<mxCell ...complete element...>"},
|
||||||
"search": "EXACT lines copied from current XML (preserve attribute order!)",
|
{"type": "add", "cell_id": "new1", "new_xml": "<mxCell ...new element...>"},
|
||||||
"replace": "Replacement lines"
|
{"type": "delete", "cell_id": "5"}
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
## edit_diagram Best Practices
|
**Examples:**
|
||||||
|
|
||||||
### Core Principle: Unique & Precise Patterns
|
Change label:
|
||||||
Your search pattern MUST uniquely identify exactly ONE location in the XML. Before writing a search pattern:
|
|
||||||
1. Review the "Current diagram XML" in the system context
|
|
||||||
2. Identify the exact element(s) to modify by their unique id attribute
|
|
||||||
3. Include enough context to ensure uniqueness
|
|
||||||
|
|
||||||
### Pattern Construction Rules
|
|
||||||
|
|
||||||
**Rule 1: Always include the element's id attribute**
|
|
||||||
\`\`\`json
|
\`\`\`json
|
||||||
{"search": "<mxCell id=\\"node5\\"", "replace": "<mxCell id=\\"node5\\" value=\\"New Label\\""}
|
{"operations": [{"type": "update", "cell_id": "3", "new_xml": "<mxCell id=\\"3\\" value=\\"New Label\\" style=\\"rounded=1;\\" vertex=\\"1\\" parent=\\"1\\">\\n <mxGeometry x=\\"100\\" y=\\"100\\" width=\\"120\\" height=\\"60\\" as=\\"geometry\\"/>\\n</mxCell>"}]}
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
**Rule 2: Include complete XML elements when possible**
|
Add new shape:
|
||||||
\`\`\`json
|
\`\`\`json
|
||||||
{
|
{"operations": [{"type": "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>"}]}
|
||||||
"search": "<mxCell id=\\"3\\" value=\\"Old\\" style=\\"rounded=1;\\" vertex=\\"1\\" parent=\\"1\\">\\n <mxGeometry x=\\"100\\" y=\\"100\\" width=\\"120\\" height=\\"60\\" as=\\"geometry\\"/>\\n</mxCell>",
|
|
||||||
"replace": "<mxCell id=\\"3\\" value=\\"New\\" style=\\"rounded=1;\\" vertex=\\"1\\" parent=\\"1\\">\\n <mxGeometry x=\\"100\\" y=\\"100\\" width=\\"120\\" height=\\"60\\" as=\\"geometry\\"/>\\n</mxCell>"
|
|
||||||
}
|
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
**Rule 3: Preserve exact whitespace and formatting**
|
Delete cell:
|
||||||
Copy the search pattern EXACTLY from the current XML, including leading spaces, line breaks (\\n), and attribute order.
|
\`\`\`json
|
||||||
|
{"operations": [{"type": "delete", "cell_id": "5"}]}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
### Good vs Bad Patterns
|
**Error Recovery:**
|
||||||
|
If cell_id not found, check "Current diagram XML" for correct IDs. Use display_diagram if major restructuring is needed
|
||||||
**BAD:** \`{"search": "value=\\"Label\\""}\` - Too vague, matches multiple elements
|
|
||||||
**BAD:** \`{"search": "<mxCell value=\\"X\\" id=\\"5\\""}\` - Reordered attributes won't match
|
|
||||||
**GOOD:** \`{"search": "<mxCell id=\\"5\\" parent=\\"1\\" style=\\"...\\" value=\\"Old\\" vertex=\\"1\\">"}\` - Uses unique id with full context
|
|
||||||
|
|
||||||
### ⚠️ JSON Escaping (CRITICAL)
|
|
||||||
Every double quote inside JSON string values MUST be escaped with backslash:
|
|
||||||
- **CORRECT:** \`"x=\\"100\\" y=\\"200\\""\` - both quotes escaped
|
|
||||||
- **WRONG:** \`"x=\\"100\\" y="200\\""\` - missing backslash causes JSON parse error!
|
|
||||||
|
|
||||||
### Error Recovery
|
|
||||||
If edit_diagram fails with "pattern not found":
|
|
||||||
1. **First retry**: Check attribute order - copy EXACTLY from current XML
|
|
||||||
2. **Second retry**: Expand context - include more surrounding lines
|
|
||||||
3. **Third retry**: Try matching on just \`<mxCell id="X"\` prefix + full replacement
|
|
||||||
4. **After 3 failures**: Fall back to display_diagram to regenerate entire diagram
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
573
lib/utils.ts
573
lib/utils.ts
@@ -377,303 +377,223 @@ export function replaceNodes(currentXML: string, nodes: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// ============================================================================
|
||||||
* Create a character count dictionary from a string
|
// ID-based Diagram Operations
|
||||||
* Used for attribute-order agnostic comparison
|
// ============================================================================
|
||||||
*/
|
|
||||||
function charCountDict(str: string): Map<string, number> {
|
export interface DiagramOperation {
|
||||||
const dict = new Map<string, number>()
|
type: "update" | "add" | "delete"
|
||||||
for (const char of str) {
|
cell_id: string
|
||||||
dict.set(char, (dict.get(char) || 0) + 1)
|
new_xml?: string
|
||||||
}
|
}
|
||||||
return dict
|
|
||||||
|
export interface OperationError {
|
||||||
|
type: "update" | "add" | "delete"
|
||||||
|
cellId: string
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApplyOperationsResult {
|
||||||
|
result: string
|
||||||
|
errors: OperationError[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compare two strings by character frequency (order-agnostic)
|
* Apply diagram operations (update/add/delete) using ID-based lookup.
|
||||||
|
* This replaces the text-matching approach with direct DOM manipulation.
|
||||||
|
*
|
||||||
|
* @param xmlContent - The full mxfile XML content
|
||||||
|
* @param operations - Array of operations to apply
|
||||||
|
* @returns Object with result XML and any errors
|
||||||
*/
|
*/
|
||||||
function sameCharFrequency(a: string, b: string): boolean {
|
export function applyDiagramOperations(
|
||||||
const trimmedA = a.trim()
|
|
||||||
const trimmedB = b.trim()
|
|
||||||
if (trimmedA.length !== trimmedB.length) return false
|
|
||||||
|
|
||||||
const dictA = charCountDict(trimmedA)
|
|
||||||
const dictB = charCountDict(trimmedB)
|
|
||||||
|
|
||||||
if (dictA.size !== dictB.size) return false
|
|
||||||
|
|
||||||
for (const [char, count] of dictA) {
|
|
||||||
if (dictB.get(char) !== count) return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replace specific parts of XML content using search and replace pairs
|
|
||||||
* @param xmlContent - The original XML string
|
|
||||||
* @param searchReplacePairs - Array of {search: string, replace: string} objects
|
|
||||||
* @returns The updated XML string with replacements applied
|
|
||||||
*/
|
|
||||||
export function replaceXMLParts(
|
|
||||||
xmlContent: string,
|
xmlContent: string,
|
||||||
searchReplacePairs: Array<{ search: string; replace: string }>,
|
operations: DiagramOperation[],
|
||||||
): string {
|
): ApplyOperationsResult {
|
||||||
// Format the XML first to ensure consistent line breaks
|
const errors: OperationError[] = []
|
||||||
let result = formatXML(xmlContent)
|
|
||||||
|
|
||||||
for (const { search, replace } of searchReplacePairs) {
|
// Parse the XML
|
||||||
// Also format the search content for consistency
|
const parser = new DOMParser()
|
||||||
const formattedSearch = formatXML(search)
|
const doc = parser.parseFromString(xmlContent, "text/xml")
|
||||||
const searchLines = formattedSearch.split("\n")
|
|
||||||
|
|
||||||
// Split into lines for exact line matching
|
// Check for parse errors
|
||||||
const resultLines = result.split("\n")
|
const parseError = doc.querySelector("parsererror")
|
||||||
|
if (parseError) {
|
||||||
// Remove trailing empty line if exists (from the trailing \n in search content)
|
return {
|
||||||
if (searchLines[searchLines.length - 1] === "") {
|
result: xmlContent,
|
||||||
searchLines.pop()
|
errors: [
|
||||||
|
{
|
||||||
|
type: "update",
|
||||||
|
cellId: "",
|
||||||
|
message: `XML parse error: ${parseError.textContent}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always search from the beginning - pairs may not be in document order
|
|
||||||
const startLineNum = 0
|
|
||||||
|
|
||||||
// Try to find match using multiple strategies
|
|
||||||
let matchFound = false
|
|
||||||
let matchStartLine = -1
|
|
||||||
let matchEndLine = -1
|
|
||||||
|
|
||||||
// First try: exact match
|
|
||||||
for (
|
|
||||||
let i = startLineNum;
|
|
||||||
i <= resultLines.length - searchLines.length;
|
|
||||||
i++
|
|
||||||
) {
|
|
||||||
let matches = true
|
|
||||||
|
|
||||||
for (let j = 0; j < searchLines.length; j++) {
|
|
||||||
if (resultLines[i + j] !== searchLines[j]) {
|
|
||||||
matches = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matches) {
|
|
||||||
matchStartLine = i
|
|
||||||
matchEndLine = i + searchLines.length
|
|
||||||
matchFound = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Second try: line-trimmed match (fallback)
|
|
||||||
if (!matchFound) {
|
|
||||||
for (
|
|
||||||
let i = startLineNum;
|
|
||||||
i <= resultLines.length - searchLines.length;
|
|
||||||
i++
|
|
||||||
) {
|
|
||||||
let matches = true
|
|
||||||
|
|
||||||
for (let j = 0; j < searchLines.length; j++) {
|
|
||||||
const originalTrimmed = resultLines[i + j].trim()
|
|
||||||
const searchTrimmed = searchLines[j].trim()
|
|
||||||
|
|
||||||
if (originalTrimmed !== searchTrimmed) {
|
|
||||||
matches = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matches) {
|
|
||||||
matchStartLine = i
|
|
||||||
matchEndLine = i + searchLines.length
|
|
||||||
matchFound = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Third try: substring match as last resort (for single-line XML)
|
|
||||||
if (!matchFound) {
|
|
||||||
// Try to find as a substring in the entire content
|
|
||||||
const searchStr = search.trim()
|
|
||||||
const resultStr = result
|
|
||||||
const index = resultStr.indexOf(searchStr)
|
|
||||||
|
|
||||||
if (index !== -1) {
|
|
||||||
// Found as substring - replace it
|
|
||||||
result =
|
|
||||||
resultStr.substring(0, index) +
|
|
||||||
replace.trim() +
|
|
||||||
resultStr.substring(index + searchStr.length)
|
|
||||||
// Re-format after substring replacement
|
|
||||||
result = formatXML(result)
|
|
||||||
continue // Skip the line-based replacement below
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fourth try: character frequency match (attribute-order agnostic)
|
|
||||||
// This handles cases where the model generates XML with different attribute order
|
|
||||||
if (!matchFound) {
|
|
||||||
for (
|
|
||||||
let i = startLineNum;
|
|
||||||
i <= resultLines.length - searchLines.length;
|
|
||||||
i++
|
|
||||||
) {
|
|
||||||
let matches = true
|
|
||||||
|
|
||||||
for (let j = 0; j < searchLines.length; j++) {
|
|
||||||
if (
|
|
||||||
!sameCharFrequency(resultLines[i + j], searchLines[j])
|
|
||||||
) {
|
|
||||||
matches = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matches) {
|
|
||||||
matchStartLine = i
|
|
||||||
matchEndLine = i + searchLines.length
|
|
||||||
matchFound = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fifth try: Match by mxCell id attribute
|
|
||||||
// Extract id from search pattern and find the element with that id
|
|
||||||
if (!matchFound) {
|
|
||||||
const idMatch = search.match(/id="([^"]+)"/)
|
|
||||||
if (idMatch) {
|
|
||||||
const searchId = idMatch[1]
|
|
||||||
// Find lines that contain this id
|
|
||||||
for (let i = startLineNum; i < resultLines.length; i++) {
|
|
||||||
if (resultLines[i].includes(`id="${searchId}"`)) {
|
|
||||||
// Found the element with matching id
|
|
||||||
// Now find the extent of this element (it might span multiple lines)
|
|
||||||
let endLine = i + 1
|
|
||||||
const line = resultLines[i].trim()
|
|
||||||
|
|
||||||
// Check if it's a self-closing tag or has children
|
|
||||||
if (!line.endsWith("/>")) {
|
|
||||||
// Find the closing tag or the end of the mxCell block
|
|
||||||
let depth = 1
|
|
||||||
while (endLine < resultLines.length && depth > 0) {
|
|
||||||
const currentLine = resultLines[endLine].trim()
|
|
||||||
if (
|
|
||||||
currentLine.startsWith("<") &&
|
|
||||||
!currentLine.startsWith("</") &&
|
|
||||||
!currentLine.endsWith("/>")
|
|
||||||
) {
|
|
||||||
depth++
|
|
||||||
} else if (currentLine.startsWith("</")) {
|
|
||||||
depth--
|
|
||||||
}
|
|
||||||
endLine++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
matchStartLine = i
|
|
||||||
matchEndLine = endLine
|
|
||||||
matchFound = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sixth try: Match by value attribute (label text)
|
|
||||||
// Extract value from search pattern and find elements with that value
|
|
||||||
if (!matchFound) {
|
|
||||||
const valueMatch = search.match(/value="([^"]*)"/)
|
|
||||||
if (valueMatch) {
|
|
||||||
const searchValue = valueMatch[0] // Use full match like value="text"
|
|
||||||
for (let i = startLineNum; i < resultLines.length; i++) {
|
|
||||||
if (resultLines[i].includes(searchValue)) {
|
|
||||||
// Found element with matching value
|
|
||||||
let endLine = i + 1
|
|
||||||
const line = resultLines[i].trim()
|
|
||||||
|
|
||||||
if (!line.endsWith("/>")) {
|
|
||||||
let depth = 1
|
|
||||||
while (endLine < resultLines.length && depth > 0) {
|
|
||||||
const currentLine = resultLines[endLine].trim()
|
|
||||||
if (
|
|
||||||
currentLine.startsWith("<") &&
|
|
||||||
!currentLine.startsWith("</") &&
|
|
||||||
!currentLine.endsWith("/>")
|
|
||||||
) {
|
|
||||||
depth++
|
|
||||||
} else if (currentLine.startsWith("</")) {
|
|
||||||
depth--
|
|
||||||
}
|
|
||||||
endLine++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
matchStartLine = i
|
|
||||||
matchEndLine = endLine
|
|
||||||
matchFound = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Seventh try: Normalized whitespace match
|
|
||||||
// Collapse all whitespace and compare
|
|
||||||
if (!matchFound) {
|
|
||||||
const normalizeWs = (s: string) => s.replace(/\s+/g, " ").trim()
|
|
||||||
const normalizedSearch = normalizeWs(search)
|
|
||||||
|
|
||||||
for (
|
|
||||||
let i = startLineNum;
|
|
||||||
i <= resultLines.length - searchLines.length;
|
|
||||||
i++
|
|
||||||
) {
|
|
||||||
// Build a normalized version of the candidate lines
|
|
||||||
const candidateLines = resultLines.slice(
|
|
||||||
i,
|
|
||||||
i + searchLines.length,
|
|
||||||
)
|
|
||||||
const normalizedCandidate = normalizeWs(
|
|
||||||
candidateLines.join(" "),
|
|
||||||
)
|
|
||||||
|
|
||||||
if (normalizedCandidate === normalizedSearch) {
|
|
||||||
matchStartLine = i
|
|
||||||
matchEndLine = i + searchLines.length
|
|
||||||
matchFound = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!matchFound) {
|
|
||||||
throw new Error(
|
|
||||||
`Search pattern not found in the diagram. The pattern may not exist in the current structure.`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace the matched lines
|
|
||||||
const replaceLines = replace.split("\n")
|
|
||||||
|
|
||||||
// Remove trailing empty line if exists
|
|
||||||
if (replaceLines[replaceLines.length - 1] === "") {
|
|
||||||
replaceLines.pop()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perform the replacement
|
|
||||||
const newResultLines = [
|
|
||||||
...resultLines.slice(0, matchStartLine),
|
|
||||||
...replaceLines,
|
|
||||||
...resultLines.slice(matchEndLine),
|
|
||||||
]
|
|
||||||
|
|
||||||
result = newResultLines.join("\n")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
// Find the root element (inside mxGraphModel)
|
||||||
|
const root = doc.querySelector("root")
|
||||||
|
if (!root) {
|
||||||
|
return {
|
||||||
|
result: xmlContent,
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
type: "update",
|
||||||
|
cellId: "",
|
||||||
|
message: "Could not find <root> element in XML",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a map of cell IDs to elements
|
||||||
|
const cellMap = new Map<string, Element>()
|
||||||
|
root.querySelectorAll("mxCell").forEach((cell) => {
|
||||||
|
const id = cell.getAttribute("id")
|
||||||
|
if (id) cellMap.set(id, cell)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Process each operation
|
||||||
|
for (const op of operations) {
|
||||||
|
if (op.type === "update") {
|
||||||
|
const existingCell = cellMap.get(op.cell_id)
|
||||||
|
if (!existingCell) {
|
||||||
|
errors.push({
|
||||||
|
type: "update",
|
||||||
|
cellId: op.cell_id,
|
||||||
|
message: `Cell with id="${op.cell_id}" not found`,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!op.new_xml) {
|
||||||
|
errors.push({
|
||||||
|
type: "update",
|
||||||
|
cellId: op.cell_id,
|
||||||
|
message: "new_xml is required for update operation",
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the new XML
|
||||||
|
const newDoc = parser.parseFromString(
|
||||||
|
`<wrapper>${op.new_xml}</wrapper>`,
|
||||||
|
"text/xml",
|
||||||
|
)
|
||||||
|
const newCell = newDoc.querySelector("mxCell")
|
||||||
|
if (!newCell) {
|
||||||
|
errors.push({
|
||||||
|
type: "update",
|
||||||
|
cellId: op.cell_id,
|
||||||
|
message: "new_xml must contain an mxCell element",
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate ID matches
|
||||||
|
const newCellId = newCell.getAttribute("id")
|
||||||
|
if (newCellId !== op.cell_id) {
|
||||||
|
errors.push({
|
||||||
|
type: "update",
|
||||||
|
cellId: op.cell_id,
|
||||||
|
message: `ID mismatch: cell_id is "${op.cell_id}" but new_xml has id="${newCellId}"`,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import and replace the node
|
||||||
|
const importedNode = doc.importNode(newCell, true)
|
||||||
|
existingCell.parentNode?.replaceChild(importedNode, existingCell)
|
||||||
|
|
||||||
|
// Update the map with the new element
|
||||||
|
cellMap.set(op.cell_id, importedNode)
|
||||||
|
} else if (op.type === "add") {
|
||||||
|
// Check if ID already exists
|
||||||
|
if (cellMap.has(op.cell_id)) {
|
||||||
|
errors.push({
|
||||||
|
type: "add",
|
||||||
|
cellId: op.cell_id,
|
||||||
|
message: `Cell with id="${op.cell_id}" already exists`,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!op.new_xml) {
|
||||||
|
errors.push({
|
||||||
|
type: "add",
|
||||||
|
cellId: op.cell_id,
|
||||||
|
message: "new_xml is required for add operation",
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the new XML
|
||||||
|
const newDoc = parser.parseFromString(
|
||||||
|
`<wrapper>${op.new_xml}</wrapper>`,
|
||||||
|
"text/xml",
|
||||||
|
)
|
||||||
|
const newCell = newDoc.querySelector("mxCell")
|
||||||
|
if (!newCell) {
|
||||||
|
errors.push({
|
||||||
|
type: "add",
|
||||||
|
cellId: op.cell_id,
|
||||||
|
message: "new_xml must contain an mxCell element",
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate ID matches
|
||||||
|
const newCellId = newCell.getAttribute("id")
|
||||||
|
if (newCellId !== op.cell_id) {
|
||||||
|
errors.push({
|
||||||
|
type: "add",
|
||||||
|
cellId: op.cell_id,
|
||||||
|
message: `ID mismatch: cell_id is "${op.cell_id}" but new_xml has id="${newCellId}"`,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import and append the node
|
||||||
|
const importedNode = doc.importNode(newCell, true)
|
||||||
|
root.appendChild(importedNode)
|
||||||
|
|
||||||
|
// Add to map
|
||||||
|
cellMap.set(op.cell_id, importedNode)
|
||||||
|
} else if (op.type === "delete") {
|
||||||
|
const existingCell = cellMap.get(op.cell_id)
|
||||||
|
if (!existingCell) {
|
||||||
|
errors.push({
|
||||||
|
type: "delete",
|
||||||
|
cellId: op.cell_id,
|
||||||
|
message: `Cell with id="${op.cell_id}" not found`,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for edges referencing this cell (warning only, still delete)
|
||||||
|
const referencingEdges = root.querySelectorAll(
|
||||||
|
`mxCell[source="${op.cell_id}"], mxCell[target="${op.cell_id}"]`,
|
||||||
|
)
|
||||||
|
if (referencingEdges.length > 0) {
|
||||||
|
const edgeIds = Array.from(referencingEdges)
|
||||||
|
.map((e) => e.getAttribute("id"))
|
||||||
|
.join(", ")
|
||||||
|
console.warn(
|
||||||
|
`[applyDiagramOperations] Deleting cell "${op.cell_id}" which is referenced by edges: ${edgeIds}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the node
|
||||||
|
existingCell.parentNode?.removeChild(existingCell)
|
||||||
|
cellMap.delete(op.cell_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize back to string
|
||||||
|
const serializer = new XMLSerializer()
|
||||||
|
const result = serializer.serializeToString(doc)
|
||||||
|
|
||||||
|
return { result, errors }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -823,8 +743,6 @@ function checkNestedMxCells(xml: string): string | null {
|
|||||||
* @returns null if valid, error message string if invalid
|
* @returns null if valid, error message string if invalid
|
||||||
*/
|
*/
|
||||||
export function validateMxCellStructure(xml: string): string | null {
|
export function validateMxCellStructure(xml: string): string | null {
|
||||||
console.time("perf:validateMxCellStructure")
|
|
||||||
console.log(`perf:validateMxCellStructure XML size: ${xml.length} bytes`)
|
|
||||||
// Size check for performance
|
// Size check for performance
|
||||||
if (xml.length > MAX_XML_SIZE) {
|
if (xml.length > MAX_XML_SIZE) {
|
||||||
console.warn(
|
console.warn(
|
||||||
@@ -834,18 +752,10 @@ export function validateMxCellStructure(xml: string): string | null {
|
|||||||
|
|
||||||
// 0. First use DOM parser to catch syntax errors (most accurate)
|
// 0. First use DOM parser to catch syntax errors (most accurate)
|
||||||
try {
|
try {
|
||||||
console.time("perf:validate-DOMParser")
|
|
||||||
const parser = new DOMParser()
|
const parser = new DOMParser()
|
||||||
const doc = parser.parseFromString(xml, "text/xml")
|
const doc = parser.parseFromString(xml, "text/xml")
|
||||||
console.timeEnd("perf:validate-DOMParser")
|
|
||||||
const parseError = doc.querySelector("parsererror")
|
const parseError = doc.querySelector("parsererror")
|
||||||
if (parseError) {
|
if (parseError) {
|
||||||
const actualError = parseError.textContent || "Unknown parse error"
|
|
||||||
console.log(
|
|
||||||
"[validateMxCellStructure] DOMParser error:",
|
|
||||||
actualError,
|
|
||||||
)
|
|
||||||
console.timeEnd("perf:validateMxCellStructure")
|
|
||||||
return `Invalid XML: The XML contains syntax errors (likely unescaped special characters like <, >, & in attribute values). Please escape special characters: use < for <, > for >, & for &, " for ". Regenerate the diagram with properly escaped values.`
|
return `Invalid XML: The XML contains syntax errors (likely unescaped special characters like <, >, & in attribute values). Please escape special characters: use < for <, > for >, & for &, " for ". Regenerate the diagram with properly escaped values.`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -854,7 +764,6 @@ export function validateMxCellStructure(xml: string): string | null {
|
|||||||
for (const cell of allCells) {
|
for (const cell of allCells) {
|
||||||
if (cell.parentElement?.tagName === "mxCell") {
|
if (cell.parentElement?.tagName === "mxCell") {
|
||||||
const id = cell.getAttribute("id") || "unknown"
|
const id = cell.getAttribute("id") || "unknown"
|
||||||
console.timeEnd("perf:validateMxCellStructure")
|
|
||||||
return `Invalid XML: Found nested mxCell (id="${id}"). Cells should be siblings, not nested inside other mxCell elements.`
|
return `Invalid XML: Found nested mxCell (id="${id}"). Cells should be siblings, not nested inside other mxCell elements.`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -868,16 +777,12 @@ export function validateMxCellStructure(xml: string): string | null {
|
|||||||
|
|
||||||
// 1. Check for CDATA wrapper (invalid at document root)
|
// 1. Check for CDATA wrapper (invalid at document root)
|
||||||
if (/^\s*<!\[CDATA\[/.test(xml)) {
|
if (/^\s*<!\[CDATA\[/.test(xml)) {
|
||||||
console.timeEnd("perf:validateMxCellStructure")
|
|
||||||
return "Invalid XML: XML is wrapped in CDATA section - remove <![CDATA[ from start and ]]> from end"
|
return "Invalid XML: XML is wrapped in CDATA section - remove <![CDATA[ from start and ]]> from end"
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Check for duplicate structural attributes
|
// 2. Check for duplicate structural attributes
|
||||||
console.time("perf:checkDuplicateAttributes")
|
|
||||||
const dupAttrError = checkDuplicateAttributes(xml)
|
const dupAttrError = checkDuplicateAttributes(xml)
|
||||||
console.timeEnd("perf:checkDuplicateAttributes")
|
|
||||||
if (dupAttrError) {
|
if (dupAttrError) {
|
||||||
console.timeEnd("perf:validateMxCellStructure")
|
|
||||||
return dupAttrError
|
return dupAttrError
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -887,33 +792,25 @@ export function validateMxCellStructure(xml: string): string | null {
|
|||||||
while ((attrValMatch = attrValuePattern.exec(xml)) !== null) {
|
while ((attrValMatch = attrValuePattern.exec(xml)) !== null) {
|
||||||
const value = attrValMatch[1]
|
const value = attrValMatch[1]
|
||||||
if (/</.test(value) && !/</.test(value)) {
|
if (/</.test(value) && !/</.test(value)) {
|
||||||
console.timeEnd("perf:validateMxCellStructure")
|
|
||||||
return "Invalid XML: Unescaped < character in attribute values. Replace < with <"
|
return "Invalid XML: Unescaped < character in attribute values. Replace < with <"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Check for duplicate IDs
|
// 4. Check for duplicate IDs
|
||||||
console.time("perf:checkDuplicateIds")
|
|
||||||
const dupIdError = checkDuplicateIds(xml)
|
const dupIdError = checkDuplicateIds(xml)
|
||||||
console.timeEnd("perf:checkDuplicateIds")
|
|
||||||
if (dupIdError) {
|
if (dupIdError) {
|
||||||
console.timeEnd("perf:validateMxCellStructure")
|
|
||||||
return dupIdError
|
return dupIdError
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Check for tag mismatches
|
// 5. Check for tag mismatches
|
||||||
console.time("perf:checkTagMismatches")
|
|
||||||
const tagMismatchError = checkTagMismatches(xml)
|
const tagMismatchError = checkTagMismatches(xml)
|
||||||
console.timeEnd("perf:checkTagMismatches")
|
|
||||||
if (tagMismatchError) {
|
if (tagMismatchError) {
|
||||||
console.timeEnd("perf:validateMxCellStructure")
|
|
||||||
return tagMismatchError
|
return tagMismatchError
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Check invalid character references
|
// 6. Check invalid character references
|
||||||
const charRefError = checkCharacterReferences(xml)
|
const charRefError = checkCharacterReferences(xml)
|
||||||
if (charRefError) {
|
if (charRefError) {
|
||||||
console.timeEnd("perf:validateMxCellStructure")
|
|
||||||
return charRefError
|
return charRefError
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -922,7 +819,6 @@ export function validateMxCellStructure(xml: string): string | null {
|
|||||||
let commentMatch
|
let commentMatch
|
||||||
while ((commentMatch = commentPattern.exec(xml)) !== null) {
|
while ((commentMatch = commentPattern.exec(xml)) !== null) {
|
||||||
if (/--/.test(commentMatch[1])) {
|
if (/--/.test(commentMatch[1])) {
|
||||||
console.timeEnd("perf:validateMxCellStructure")
|
|
||||||
return "Invalid XML: Comment contains -- (double hyphen) which is not allowed"
|
return "Invalid XML: Comment contains -- (double hyphen) which is not allowed"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -930,24 +826,20 @@ export function validateMxCellStructure(xml: string): string | null {
|
|||||||
// 8. Check for unescaped entity references and invalid entity names
|
// 8. Check for unescaped entity references and invalid entity names
|
||||||
const entityError = checkEntityReferences(xml)
|
const entityError = checkEntityReferences(xml)
|
||||||
if (entityError) {
|
if (entityError) {
|
||||||
console.timeEnd("perf:validateMxCellStructure")
|
|
||||||
return entityError
|
return entityError
|
||||||
}
|
}
|
||||||
|
|
||||||
// 9. Check for empty id attributes on mxCell
|
// 9. Check for empty id attributes on mxCell
|
||||||
if (/<mxCell[^>]*\sid\s*=\s*["']\s*["'][^>]*>/g.test(xml)) {
|
if (/<mxCell[^>]*\sid\s*=\s*["']\s*["'][^>]*>/g.test(xml)) {
|
||||||
console.timeEnd("perf:validateMxCellStructure")
|
|
||||||
return "Invalid XML: Found mxCell element(s) with empty id attribute"
|
return "Invalid XML: Found mxCell element(s) with empty id attribute"
|
||||||
}
|
}
|
||||||
|
|
||||||
// 10. Check for nested mxCell tags
|
// 10. Check for nested mxCell tags
|
||||||
const nestedCellError = checkNestedMxCells(xml)
|
const nestedCellError = checkNestedMxCells(xml)
|
||||||
if (nestedCellError) {
|
if (nestedCellError) {
|
||||||
console.timeEnd("perf:validateMxCellStructure")
|
|
||||||
return nestedCellError
|
return nestedCellError
|
||||||
}
|
}
|
||||||
|
|
||||||
console.timeEnd("perf:validateMxCellStructure")
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1162,7 +1054,31 @@ export function autoFixXml(xml: string): { fixed: string; fixes: string[] } {
|
|||||||
fixes.push("Fixed <Cell> tags to <mxCell>")
|
fixes.push("Fixed <Cell> tags to <mxCell>")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8b. Remove non-draw.io tags (LLM sometimes includes Claude's function calling XML)
|
// 8b. Fix common closing tag typos (MUST run before foreign tag removal)
|
||||||
|
const tagTypos = [
|
||||||
|
{ wrong: /<\/mxElement>/gi, right: "</mxCell>", name: "</mxElement>" },
|
||||||
|
{ wrong: /<\/mxcell>/g, right: "</mxCell>", name: "</mxcell>" }, // case sensitivity
|
||||||
|
{
|
||||||
|
wrong: /<\/mxgeometry>/g,
|
||||||
|
right: "</mxGeometry>",
|
||||||
|
name: "</mxgeometry>",
|
||||||
|
},
|
||||||
|
{ wrong: /<\/mxpoint>/g, right: "</mxPoint>", name: "</mxpoint>" },
|
||||||
|
{
|
||||||
|
wrong: /<\/mxgraphmodel>/gi,
|
||||||
|
right: "</mxGraphModel>",
|
||||||
|
name: "</mxgraphmodel>",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
for (const { wrong, right, name } of tagTypos) {
|
||||||
|
const before = fixed
|
||||||
|
fixed = fixed.replace(wrong, right)
|
||||||
|
if (fixed !== before) {
|
||||||
|
fixes.push(`Fixed typo ${name} to ${right}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8c. Remove non-draw.io tags (after typo fixes so lowercase variants are fixed first)
|
||||||
// Valid draw.io tags: mxfile, diagram, mxGraphModel, root, mxCell, mxGeometry, mxPoint, Array, Object
|
// Valid draw.io tags: mxfile, diagram, mxGraphModel, root, mxCell, mxGeometry, mxPoint, Array, Object
|
||||||
const validDrawioTags = new Set([
|
const validDrawioTags = new Set([
|
||||||
"mxfile",
|
"mxfile",
|
||||||
@@ -1187,7 +1103,7 @@ export function autoFixXml(xml: string): { fixed: string; fixes: string[] } {
|
|||||||
}
|
}
|
||||||
if (foreignTags.size > 0) {
|
if (foreignTags.size > 0) {
|
||||||
console.log(
|
console.log(
|
||||||
"[autoFixXml] Step 8b: Found foreign tags:",
|
"[autoFixXml] Step 8c: Found foreign tags:",
|
||||||
Array.from(foreignTags),
|
Array.from(foreignTags),
|
||||||
)
|
)
|
||||||
for (const tag of foreignTags) {
|
for (const tag of foreignTags) {
|
||||||
@@ -1201,29 +1117,6 @@ export function autoFixXml(xml: string): { fixed: string; fixes: string[] } {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 9. Fix common closing tag typos
|
|
||||||
const tagTypos = [
|
|
||||||
{ wrong: /<\/mxElement>/gi, right: "</mxCell>", name: "</mxElement>" },
|
|
||||||
{ wrong: /<\/mxcell>/g, right: "</mxCell>", name: "</mxcell>" }, // case sensitivity
|
|
||||||
{
|
|
||||||
wrong: /<\/mxgeometry>/g,
|
|
||||||
right: "</mxGeometry>",
|
|
||||||
name: "</mxgeometry>",
|
|
||||||
},
|
|
||||||
{ wrong: /<\/mxpoint>/g, right: "</mxPoint>", name: "</mxpoint>" },
|
|
||||||
{
|
|
||||||
wrong: /<\/mxgraphmodel>/gi,
|
|
||||||
right: "</mxGraphModel>",
|
|
||||||
name: "</mxgraphmodel>",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
for (const { wrong, right, name } of tagTypos) {
|
|
||||||
if (wrong.test(fixed)) {
|
|
||||||
fixed = fixed.replace(wrong, right)
|
|
||||||
fixes.push(`Fixed typo ${name} to ${right}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 10. Fix unclosed tags by appending missing closing tags
|
// 10. Fix unclosed tags by appending missing closing tags
|
||||||
// Use parseXmlTags helper to track open tags
|
// Use parseXmlTags helper to track open tags
|
||||||
const tagStack: string[] = []
|
const tagStack: string[] = []
|
||||||
|
|||||||
154
package-lock.json
generated
154
package-lock.json
generated
@@ -1,22 +1,24 @@
|
|||||||
{
|
{
|
||||||
"name": "next-ai-draw-io",
|
"name": "next-ai-draw-io",
|
||||||
"version": "0.4.0",
|
"version": "0.4.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "next-ai-draw-io",
|
"name": "next-ai-draw-io",
|
||||||
"version": "0.4.0",
|
"version": "0.4.3",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/amazon-bedrock": "^3.0.70",
|
"@ai-sdk/amazon-bedrock": "^3.0.70",
|
||||||
"@ai-sdk/anthropic": "^2.0.44",
|
"@ai-sdk/anthropic": "^2.0.44",
|
||||||
"@ai-sdk/azure": "^2.0.69",
|
"@ai-sdk/azure": "^2.0.69",
|
||||||
"@ai-sdk/deepseek": "^1.0.30",
|
"@ai-sdk/deepseek": "^1.0.30",
|
||||||
|
"@ai-sdk/gateway": "^2.0.21",
|
||||||
"@ai-sdk/google": "^2.0.0",
|
"@ai-sdk/google": "^2.0.0",
|
||||||
"@ai-sdk/openai": "^2.0.19",
|
"@ai-sdk/openai": "^2.0.19",
|
||||||
"@ai-sdk/react": "^2.0.107",
|
"@ai-sdk/react": "^2.0.107",
|
||||||
"@aws-sdk/credential-providers": "^3.943.0",
|
"@aws-sdk/credential-providers": "^3.943.0",
|
||||||
|
"@formatjs/intl-localematcher": "^0.7.2",
|
||||||
"@langfuse/client": "^4.4.9",
|
"@langfuse/client": "^4.4.9",
|
||||||
"@langfuse/otel": "^4.4.4",
|
"@langfuse/otel": "^4.4.4",
|
||||||
"@langfuse/tracing": "^4.4.9",
|
"@langfuse/tracing": "^4.4.9",
|
||||||
@@ -43,6 +45,7 @@
|
|||||||
"jsonrepair": "^3.13.1",
|
"jsonrepair": "^3.13.1",
|
||||||
"lucide-react": "^0.483.0",
|
"lucide-react": "^0.483.0",
|
||||||
"motion": "^12.23.25",
|
"motion": "^12.23.25",
|
||||||
|
"negotiator": "^1.0.0",
|
||||||
"next": "^16.0.7",
|
"next": "^16.0.7",
|
||||||
"ollama-ai-provider-v2": "^1.5.4",
|
"ollama-ai-provider-v2": "^1.5.4",
|
||||||
"pako": "^2.1.0",
|
"pako": "^2.1.0",
|
||||||
@@ -54,6 +57,7 @@
|
|||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-resizable-panels": "^3.0.6",
|
"react-resizable-panels": "^3.0.6",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
|
"server-only": "^0.0.1",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.0.2",
|
"tailwind-merge": "^3.0.2",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
@@ -62,9 +66,10 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@anthropic-ai/tokenizer": "^0.0.4",
|
"@anthropic-ai/tokenizer": "^0.0.4",
|
||||||
"@biomejs/biome": "2.3.8",
|
"@biomejs/biome": "^2.3.10",
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
|
"@types/negotiator": "^0.6.4",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/pako": "^2.0.3",
|
"@types/pako": "^2.0.3",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
@@ -199,13 +204,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@ai-sdk/gateway": {
|
"node_modules/@ai-sdk/gateway": {
|
||||||
"version": "2.0.18",
|
"version": "2.0.21",
|
||||||
"resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-2.0.18.tgz",
|
"resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-2.0.21.tgz",
|
||||||
"integrity": "sha512-sDQcW+6ck2m0pTIHW6BPHD7S125WD3qNkx/B8sEzJp/hurocmJ5Cni0ybExg6sQMGo+fr/GWOwpHF1cmCdg5rQ==",
|
"integrity": "sha512-BwV7DU/lAm3Xn6iyyvZdWgVxgLu3SNXzl5y57gMvkW4nGhAOV5269IrJzQwGt03bb107sa6H6uJwWxc77zXoGA==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "2.0.0",
|
"@ai-sdk/provider": "2.0.0",
|
||||||
"@ai-sdk/provider-utils": "3.0.18",
|
"@ai-sdk/provider-utils": "3.0.19",
|
||||||
"@vercel/oidc": "3.0.5"
|
"@vercel/oidc": "3.0.5"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -215,6 +220,23 @@
|
|||||||
"zod": "^3.25.76 || ^4.1.8"
|
"zod": "^3.25.76 || ^4.1.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@ai-sdk/gateway/node_modules/@ai-sdk/provider-utils": {
|
||||||
|
"version": "3.0.19",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.19.tgz",
|
||||||
|
"integrity": "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@ai-sdk/provider": "2.0.0",
|
||||||
|
"@standard-schema/spec": "^1.0.0",
|
||||||
|
"eventsource-parser": "^3.0.6"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"zod": "^3.25.76 || ^4.1.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@ai-sdk/google": {
|
"node_modules/@ai-sdk/google": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@ai-sdk/google/-/google-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@ai-sdk/google/-/google-2.0.0.tgz",
|
||||||
@@ -1398,9 +1420,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/biome": {
|
"node_modules/@biomejs/biome": {
|
||||||
"version": "2.3.8",
|
"version": "2.3.10",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.10.tgz",
|
||||||
"integrity": "sha512-Qjsgoe6FEBxWAUzwFGFrB+1+M8y/y5kwmg5CHac+GSVOdmOIqsAiXM5QMVGZJ1eCUCLlPZtq4aFAQ0eawEUuUA==",
|
"integrity": "sha512-/uWSUd1MHX2fjqNLHNL6zLYWBbrJeG412/8H7ESuK8ewoRoMPUgHDebqKrPTx/5n6f17Xzqc9hdg3MEqA5hXnQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -1414,20 +1436,20 @@
|
|||||||
"url": "https://opencollective.com/biome"
|
"url": "https://opencollective.com/biome"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@biomejs/cli-darwin-arm64": "2.3.8",
|
"@biomejs/cli-darwin-arm64": "2.3.10",
|
||||||
"@biomejs/cli-darwin-x64": "2.3.8",
|
"@biomejs/cli-darwin-x64": "2.3.10",
|
||||||
"@biomejs/cli-linux-arm64": "2.3.8",
|
"@biomejs/cli-linux-arm64": "2.3.10",
|
||||||
"@biomejs/cli-linux-arm64-musl": "2.3.8",
|
"@biomejs/cli-linux-arm64-musl": "2.3.10",
|
||||||
"@biomejs/cli-linux-x64": "2.3.8",
|
"@biomejs/cli-linux-x64": "2.3.10",
|
||||||
"@biomejs/cli-linux-x64-musl": "2.3.8",
|
"@biomejs/cli-linux-x64-musl": "2.3.10",
|
||||||
"@biomejs/cli-win32-arm64": "2.3.8",
|
"@biomejs/cli-win32-arm64": "2.3.10",
|
||||||
"@biomejs/cli-win32-x64": "2.3.8"
|
"@biomejs/cli-win32-x64": "2.3.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-darwin-arm64": {
|
"node_modules/@biomejs/cli-darwin-arm64": {
|
||||||
"version": "2.3.8",
|
"version": "2.3.10",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.10.tgz",
|
||||||
"integrity": "sha512-HM4Zg9CGQ3txTPflxD19n8MFPrmUAjaC7PQdLkugeeC0cQ+PiVrd7i09gaBS/11QKsTDBJhVg85CEIK9f50Qww==",
|
"integrity": "sha512-M6xUjtCVnNGFfK7HMNKa593nb7fwNm43fq1Mt71kpLpb+4mE7odO8W/oWVDyBVO4ackhresy1ZYO7OJcVo/B7w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1442,9 +1464,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-darwin-x64": {
|
"node_modules/@biomejs/cli-darwin-x64": {
|
||||||
"version": "2.3.8",
|
"version": "2.3.10",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.10.tgz",
|
||||||
"integrity": "sha512-lUDQ03D7y/qEao7RgdjWVGCu+BLYadhKTm40HkpJIi6kn8LSv5PAwRlew/DmwP4YZ9ke9XXoTIQDO1vAnbRZlA==",
|
"integrity": "sha512-Vae7+V6t/Avr8tVbFNjnFSTKZogZHFYl7MMH62P/J1kZtr0tyRQ9Fe0onjqjS2Ek9lmNLmZc/VR5uSekh+p1fg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1459,9 +1481,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-linux-arm64": {
|
"node_modules/@biomejs/cli-linux-arm64": {
|
||||||
"version": "2.3.8",
|
"version": "2.3.10",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.10.tgz",
|
||||||
"integrity": "sha512-Uo1OJnIkJgSgF+USx970fsM/drtPcQ39I+JO+Fjsaa9ZdCN1oysQmy6oAGbyESlouz+rzEckLTF6DS7cWse95g==",
|
"integrity": "sha512-hhPw2V3/EpHKsileVOFynuWiKRgFEV48cLe0eA+G2wO4SzlwEhLEB9LhlSrVeu2mtSn205W283LkX7Fh48CaxA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1476,9 +1498,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-linux-arm64-musl": {
|
"node_modules/@biomejs/cli-linux-arm64-musl": {
|
||||||
"version": "2.3.8",
|
"version": "2.3.10",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.10.tgz",
|
||||||
"integrity": "sha512-PShR4mM0sjksUMyxbyPNMxoKFPVF48fU8Qe8Sfx6w6F42verbwRLbz+QiKNiDPRJwUoMG1nPM50OBL3aOnTevA==",
|
"integrity": "sha512-B9DszIHkuKtOH2IFeeVkQmSMVUjss9KtHaNXquYYWCjH8IstNgXgx5B0aSBQNr6mn4RcKKRQZXn9Zu1rM3O0/A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1493,9 +1515,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-linux-x64": {
|
"node_modules/@biomejs/cli-linux-x64": {
|
||||||
"version": "2.3.8",
|
"version": "2.3.10",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.10.tgz",
|
||||||
"integrity": "sha512-QDPMD5bQz6qOVb3kiBui0zKZXASLo0NIQ9JVJio5RveBEFgDgsvJFUvZIbMbUZT3T00M/1wdzwWXk4GIh0KaAw==",
|
"integrity": "sha512-wwAkWD1MR95u+J4LkWP74/vGz+tRrIQvr8kfMMJY8KOQ8+HMVleREOcPYsQX82S7uueco60L58Wc6M1I9WA9Dw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1510,9 +1532,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-linux-x64-musl": {
|
"node_modules/@biomejs/cli-linux-x64-musl": {
|
||||||
"version": "2.3.8",
|
"version": "2.3.10",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.10.tgz",
|
||||||
"integrity": "sha512-YGLkqU91r1276uwSjiUD/xaVikdxgV1QpsicT0bIA1TaieM6E5ibMZeSyjQ/izBn4tKQthUSsVZacmoJfa3pDA==",
|
"integrity": "sha512-QTfHZQh62SDFdYc2nfmZFuTm5yYb4eO1zwfB+90YxUumRCR171tS1GoTX5OD0wrv4UsziMPmrePMtkTnNyYG3g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1527,9 +1549,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-win32-arm64": {
|
"node_modules/@biomejs/cli-win32-arm64": {
|
||||||
"version": "2.3.8",
|
"version": "2.3.10",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.10.tgz",
|
||||||
"integrity": "sha512-H4IoCHvL1fXKDrTALeTKMiE7GGWFAraDwBYFquE/L/5r1927Te0mYIGseXi4F+lrrwhSWbSGt5qPFswNoBaCxg==",
|
"integrity": "sha512-o7lYc9n+CfRbHvkjPhm8s9FgbKdYZu5HCcGVMItLjz93EhgJ8AM44W+QckDqLA9MKDNFrR8nPbO4b73VC5kGGQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1544,9 +1566,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-win32-x64": {
|
"node_modules/@biomejs/cli-win32-x64": {
|
||||||
"version": "2.3.8",
|
"version": "2.3.10",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.10.tgz",
|
||||||
"integrity": "sha512-RguzimPoZWtBapfKhKjcWXBVI91tiSprqdBYu7tWhgN8pKRZhw24rFeNZTNf6UiBfjCYCi9eFQs/JzJZIhuK4w==",
|
"integrity": "sha512-pHEFgq7dUEsKnqG9mx9bXihxGI49X+ar+UBrEIj3Wqj3UCZp1rNgV+OoyjFgcXsjCWpuEAF4VJdkZr3TrWdCbQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1885,6 +1907,15 @@
|
|||||||
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
|
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@formatjs/intl-localematcher": {
|
||||||
|
"version": "0.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.7.2.tgz",
|
||||||
|
"integrity": "sha512-1cpFlw1omNn2/Uz/vAdAyovlh7qS/po7MWipH3JrShT/lVUh2+lbEAWquyh9yRa84fqlLulTt7oysGtjATujZQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@humanfs/core": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.1",
|
"version": "0.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||||
@@ -5483,6 +5514,13 @@
|
|||||||
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
|
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/negotiator": {
|
||||||
|
"version": "0.6.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/negotiator/-/negotiator-0.6.4.tgz",
|
||||||
|
"integrity": "sha512-elf6BsTq+AkyNsb2h5cGNst2Mc7dPliVoAPm1fXglC/BM3f2pFA40BaSSv3E5lyHteEawVKLP+8TwiY1DMNb3A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.17.30",
|
"version": "20.17.30",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.30.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.30.tgz",
|
||||||
@@ -6130,6 +6168,23 @@
|
|||||||
"zod": "^3.25.76 || ^4.1.8"
|
"zod": "^3.25.76 || ^4.1.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ai/node_modules/@ai-sdk/gateway": {
|
||||||
|
"version": "2.0.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-2.0.18.tgz",
|
||||||
|
"integrity": "sha512-sDQcW+6ck2m0pTIHW6BPHD7S125WD3qNkx/B8sEzJp/hurocmJ5Cni0ybExg6sQMGo+fr/GWOwpHF1cmCdg5rQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@ai-sdk/provider": "2.0.0",
|
||||||
|
"@ai-sdk/provider-utils": "3.0.18",
|
||||||
|
"@vercel/oidc": "3.0.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"zod": "^3.25.76 || ^4.1.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "6.12.6",
|
"version": "6.12.6",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||||
@@ -10681,6 +10736,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/negotiator": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/next": {
|
"node_modules/next": {
|
||||||
"version": "16.0.10",
|
"version": "16.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/next/-/next-16.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/next/-/next-16.0.10.tgz",
|
||||||
@@ -11724,6 +11788,12 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/server-only": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz",
|
||||||
|
"integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/set-function-length": {
|
"node_modules/set-function-length": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "next-ai-draw-io",
|
"name": "next-ai-draw-io",
|
||||||
"version": "0.4.1",
|
"version": "0.4.3",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -17,10 +17,12 @@
|
|||||||
"@ai-sdk/anthropic": "^2.0.44",
|
"@ai-sdk/anthropic": "^2.0.44",
|
||||||
"@ai-sdk/azure": "^2.0.69",
|
"@ai-sdk/azure": "^2.0.69",
|
||||||
"@ai-sdk/deepseek": "^1.0.30",
|
"@ai-sdk/deepseek": "^1.0.30",
|
||||||
|
"@ai-sdk/gateway": "^2.0.21",
|
||||||
"@ai-sdk/google": "^2.0.0",
|
"@ai-sdk/google": "^2.0.0",
|
||||||
"@ai-sdk/openai": "^2.0.19",
|
"@ai-sdk/openai": "^2.0.19",
|
||||||
"@ai-sdk/react": "^2.0.107",
|
"@ai-sdk/react": "^2.0.107",
|
||||||
"@aws-sdk/credential-providers": "^3.943.0",
|
"@aws-sdk/credential-providers": "^3.943.0",
|
||||||
|
"@formatjs/intl-localematcher": "^0.7.2",
|
||||||
"@langfuse/client": "^4.4.9",
|
"@langfuse/client": "^4.4.9",
|
||||||
"@langfuse/otel": "^4.4.4",
|
"@langfuse/otel": "^4.4.4",
|
||||||
"@langfuse/tracing": "^4.4.9",
|
"@langfuse/tracing": "^4.4.9",
|
||||||
@@ -47,6 +49,7 @@
|
|||||||
"jsonrepair": "^3.13.1",
|
"jsonrepair": "^3.13.1",
|
||||||
"lucide-react": "^0.483.0",
|
"lucide-react": "^0.483.0",
|
||||||
"motion": "^12.23.25",
|
"motion": "^12.23.25",
|
||||||
|
"negotiator": "^1.0.0",
|
||||||
"next": "^16.0.7",
|
"next": "^16.0.7",
|
||||||
"ollama-ai-provider-v2": "^1.5.4",
|
"ollama-ai-provider-v2": "^1.5.4",
|
||||||
"pako": "^2.1.0",
|
"pako": "^2.1.0",
|
||||||
@@ -58,6 +61,7 @@
|
|||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-resizable-panels": "^3.0.6",
|
"react-resizable-panels": "^3.0.6",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
|
"server-only": "^0.0.1",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.0.2",
|
"tailwind-merge": "^3.0.2",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
@@ -72,9 +76,10 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@anthropic-ai/tokenizer": "^0.0.4",
|
"@anthropic-ai/tokenizer": "^0.0.4",
|
||||||
"@biomejs/biome": "2.3.8",
|
"@biomejs/biome": "^2.3.10",
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
|
"@types/negotiator": "^0.6.4",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/pako": "^2.0.3",
|
"@types/pako": "^2.0.3",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
|
|||||||
162
packages/mcp-server/README.md
Normal file
162
packages/mcp-server/README.md
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
# Next AI Draw.io MCP Server
|
||||||
|
|
||||||
|
MCP (Model Context Protocol) server that enables AI agents like Claude Desktop and Cursor to generate and edit draw.io diagrams with **real-time browser preview**.
|
||||||
|
|
||||||
|
**Self-contained** - includes an embedded HTTP server, no external dependencies required.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"drawio": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["@next-ai-drawio/mcp-server@latest"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Claude Desktop
|
||||||
|
|
||||||
|
Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"drawio": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["@next-ai-drawio/mcp-server@latest"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### VS Code
|
||||||
|
|
||||||
|
Add to your VS Code settings (`.vscode/mcp.json` in workspace or user settings):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"drawio": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["@next-ai-drawio/mcp-server@latest"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cursor
|
||||||
|
|
||||||
|
Add to Cursor MCP config (`~/.cursor/mcp.json`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"drawio": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["@next-ai-drawio/mcp-server@latest"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Claude Code CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude mcp add drawio -- npx @next-ai-drawio/mcp-server@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Other MCP Clients
|
||||||
|
|
||||||
|
Use the standard MCP configuration with:
|
||||||
|
- **Command**: `npx`
|
||||||
|
- **Args**: `["@next-ai-drawio/mcp-server@latest"]`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Restart your MCP client after updating config
|
||||||
|
2. Ask the AI to create a diagram:
|
||||||
|
> "Create a flowchart showing user authentication with login, MFA, and session management"
|
||||||
|
3. The diagram appears in your browser in real-time!
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Real-time Preview**: Diagrams appear and update in your browser as the AI creates them
|
||||||
|
- **Natural Language**: Describe diagrams in plain text - flowcharts, architecture diagrams, etc.
|
||||||
|
- **Edit Support**: Modify existing diagrams with natural language instructions
|
||||||
|
- **Export**: Save diagrams as `.drawio` files
|
||||||
|
- **Self-contained**: Embedded server, works offline (except draw.io UI which loads from embed.diagrams.net)
|
||||||
|
|
||||||
|
## Available Tools
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `start_session` | Opens browser with real-time diagram preview |
|
||||||
|
| `display_diagram` | Create a new diagram from XML |
|
||||||
|
| `edit_diagram` | Edit diagram by ID-based operations (update/add/delete cells) |
|
||||||
|
| `get_diagram` | Get the current diagram XML |
|
||||||
|
| `export_diagram` | Save diagram to a `.drawio` file |
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐ stdio ┌─────────────────┐
|
||||||
|
│ Claude Desktop │ <───────────> │ MCP Server │
|
||||||
|
│ (AI Agent) │ │ (this package) │
|
||||||
|
└─────────────────┘ └────────┬────────┘
|
||||||
|
│
|
||||||
|
┌────────▼────────┐
|
||||||
|
│ Embedded HTTP │
|
||||||
|
│ Server (:6002) │
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
┌────────▼────────┐
|
||||||
|
│ User's Browser │
|
||||||
|
│ (draw.io embed) │
|
||||||
|
└─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
1. **MCP Server** receives tool calls from Claude via stdio
|
||||||
|
2. **Embedded HTTP Server** serves the draw.io UI and handles state
|
||||||
|
3. **Browser** shows real-time diagram updates via polling
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `PORT` | `6002` | Port for the embedded HTTP server |
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Port already in use
|
||||||
|
|
||||||
|
If port 6002 is in use, the server will automatically try the next available port (up to 6020).
|
||||||
|
|
||||||
|
Or set a custom port:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"drawio": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["@next-ai-drawio/mcp-server@latest"],
|
||||||
|
"env": { "PORT": "6003" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### "No active session"
|
||||||
|
|
||||||
|
Call `start_session` first to open the browser window.
|
||||||
|
|
||||||
|
### Browser not updating
|
||||||
|
|
||||||
|
Check that the browser URL has the `?mcp=` query parameter. The MCP session ID connects the browser to the server.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Apache-2.0
|
||||||
2044
packages/mcp-server/package-lock.json
generated
Normal file
2044
packages/mcp-server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
55
packages/mcp-server/package.json
Normal file
55
packages/mcp-server/package.json
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"name": "@next-ai-drawio/mcp-server",
|
||||||
|
"version": "0.1.3",
|
||||||
|
"description": "MCP server for Next AI Draw.io - AI-powered diagram generation with real-time browser preview",
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"bin": {
|
||||||
|
"next-ai-drawio-mcp": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"dev": "tsx watch src/index.ts",
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"prepublishOnly": "npm run build"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"mcp",
|
||||||
|
"drawio",
|
||||||
|
"diagram",
|
||||||
|
"ai",
|
||||||
|
"claude",
|
||||||
|
"model-context-protocol"
|
||||||
|
],
|
||||||
|
"author": "Biki-dev",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/Biki-dev/next-ai-draw-io",
|
||||||
|
"directory": "packages/mcp-server"
|
||||||
|
},
|
||||||
|
"homepage": "https://next-ai-drawio.jiang.jp",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/Biki-dev/next-ai-draw-io/issues"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||||
|
"linkedom": "^0.18.0",
|
||||||
|
"open": "^10.1.0",
|
||||||
|
"zod": "^3.24.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20",
|
||||||
|
"tsx": "^4.19.0",
|
||||||
|
"typescript": "^5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
]
|
||||||
|
}
|
||||||
219
packages/mcp-server/src/diagram-operations.ts
Normal file
219
packages/mcp-server/src/diagram-operations.ts
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
/**
|
||||||
|
* ID-based diagram operations
|
||||||
|
* Copied from lib/utils.ts to avoid cross-package imports
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface DiagramOperation {
|
||||||
|
type: "update" | "add" | "delete"
|
||||||
|
cell_id: string
|
||||||
|
new_xml?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OperationError {
|
||||||
|
type: "update" | "add" | "delete"
|
||||||
|
cellId: string
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApplyOperationsResult {
|
||||||
|
result: string
|
||||||
|
errors: OperationError[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply diagram operations (update/add/delete) using ID-based lookup.
|
||||||
|
* This replaces the text-matching approach with direct DOM manipulation.
|
||||||
|
*
|
||||||
|
* @param xmlContent - The full mxfile XML content
|
||||||
|
* @param operations - Array of operations to apply
|
||||||
|
* @returns Object with result XML and any errors
|
||||||
|
*/
|
||||||
|
export function applyDiagramOperations(
|
||||||
|
xmlContent: string,
|
||||||
|
operations: DiagramOperation[],
|
||||||
|
): ApplyOperationsResult {
|
||||||
|
const errors: OperationError[] = []
|
||||||
|
|
||||||
|
// Parse the XML
|
||||||
|
const parser = new DOMParser()
|
||||||
|
const doc = parser.parseFromString(xmlContent, "text/xml")
|
||||||
|
|
||||||
|
// Check for parse errors
|
||||||
|
const parseError = doc.querySelector("parsererror")
|
||||||
|
if (parseError) {
|
||||||
|
return {
|
||||||
|
result: xmlContent,
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
type: "update",
|
||||||
|
cellId: "",
|
||||||
|
message: `XML parse error: ${parseError.textContent}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the root element (inside mxGraphModel)
|
||||||
|
const root = doc.querySelector("root")
|
||||||
|
if (!root) {
|
||||||
|
return {
|
||||||
|
result: xmlContent,
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
type: "update",
|
||||||
|
cellId: "",
|
||||||
|
message: "Could not find <root> element in XML",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a map of cell IDs to elements
|
||||||
|
const cellMap = new Map<string, Element>()
|
||||||
|
root.querySelectorAll("mxCell").forEach((cell) => {
|
||||||
|
const id = cell.getAttribute("id")
|
||||||
|
if (id) cellMap.set(id, cell)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Process each operation
|
||||||
|
for (const op of operations) {
|
||||||
|
if (op.type === "update") {
|
||||||
|
const existingCell = cellMap.get(op.cell_id)
|
||||||
|
if (!existingCell) {
|
||||||
|
errors.push({
|
||||||
|
type: "update",
|
||||||
|
cellId: op.cell_id,
|
||||||
|
message: `Cell with id="${op.cell_id}" not found`,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!op.new_xml) {
|
||||||
|
errors.push({
|
||||||
|
type: "update",
|
||||||
|
cellId: op.cell_id,
|
||||||
|
message: "new_xml is required for update operation",
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the new XML
|
||||||
|
const newDoc = parser.parseFromString(
|
||||||
|
`<wrapper>${op.new_xml}</wrapper>`,
|
||||||
|
"text/xml",
|
||||||
|
)
|
||||||
|
const newCell = newDoc.querySelector("mxCell")
|
||||||
|
if (!newCell) {
|
||||||
|
errors.push({
|
||||||
|
type: "update",
|
||||||
|
cellId: op.cell_id,
|
||||||
|
message: "new_xml must contain an mxCell element",
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate ID matches
|
||||||
|
const newCellId = newCell.getAttribute("id")
|
||||||
|
if (newCellId !== op.cell_id) {
|
||||||
|
errors.push({
|
||||||
|
type: "update",
|
||||||
|
cellId: op.cell_id,
|
||||||
|
message: `ID mismatch: cell_id is "${op.cell_id}" but new_xml has id="${newCellId}"`,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import and replace the node
|
||||||
|
const importedNode = doc.importNode(newCell, true)
|
||||||
|
existingCell.parentNode?.replaceChild(importedNode, existingCell)
|
||||||
|
|
||||||
|
// Update the map with the new element
|
||||||
|
cellMap.set(op.cell_id, importedNode)
|
||||||
|
} else if (op.type === "add") {
|
||||||
|
// Check if ID already exists
|
||||||
|
if (cellMap.has(op.cell_id)) {
|
||||||
|
errors.push({
|
||||||
|
type: "add",
|
||||||
|
cellId: op.cell_id,
|
||||||
|
message: `Cell with id="${op.cell_id}" already exists`,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!op.new_xml) {
|
||||||
|
errors.push({
|
||||||
|
type: "add",
|
||||||
|
cellId: op.cell_id,
|
||||||
|
message: "new_xml is required for add operation",
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the new XML
|
||||||
|
const newDoc = parser.parseFromString(
|
||||||
|
`<wrapper>${op.new_xml}</wrapper>`,
|
||||||
|
"text/xml",
|
||||||
|
)
|
||||||
|
const newCell = newDoc.querySelector("mxCell")
|
||||||
|
if (!newCell) {
|
||||||
|
errors.push({
|
||||||
|
type: "add",
|
||||||
|
cellId: op.cell_id,
|
||||||
|
message: "new_xml must contain an mxCell element",
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate ID matches
|
||||||
|
const newCellId = newCell.getAttribute("id")
|
||||||
|
if (newCellId !== op.cell_id) {
|
||||||
|
errors.push({
|
||||||
|
type: "add",
|
||||||
|
cellId: op.cell_id,
|
||||||
|
message: `ID mismatch: cell_id is "${op.cell_id}" but new_xml has id="${newCellId}"`,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import and append the node
|
||||||
|
const importedNode = doc.importNode(newCell, true)
|
||||||
|
root.appendChild(importedNode)
|
||||||
|
|
||||||
|
// Add to map
|
||||||
|
cellMap.set(op.cell_id, importedNode)
|
||||||
|
} else if (op.type === "delete") {
|
||||||
|
const existingCell = cellMap.get(op.cell_id)
|
||||||
|
if (!existingCell) {
|
||||||
|
errors.push({
|
||||||
|
type: "delete",
|
||||||
|
cellId: op.cell_id,
|
||||||
|
message: `Cell with id="${op.cell_id}" not found`,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for edges referencing this cell (warning only, still delete)
|
||||||
|
const referencingEdges = root.querySelectorAll(
|
||||||
|
`mxCell[source="${op.cell_id}"], mxCell[target="${op.cell_id}"]`,
|
||||||
|
)
|
||||||
|
if (referencingEdges.length > 0) {
|
||||||
|
const edgeIds = Array.from(referencingEdges)
|
||||||
|
.map((e) => e.getAttribute("id"))
|
||||||
|
.join(", ")
|
||||||
|
console.warn(
|
||||||
|
`[applyDiagramOperations] Deleting cell "${op.cell_id}" which is referenced by edges: ${edgeIds}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the node
|
||||||
|
existingCell.parentNode?.removeChild(existingCell)
|
||||||
|
cellMap.delete(op.cell_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize back to string
|
||||||
|
const serializer = new XMLSerializer()
|
||||||
|
const result = serializer.serializeToString(doc)
|
||||||
|
|
||||||
|
return { result, errors }
|
||||||
|
}
|
||||||
384
packages/mcp-server/src/http-server.ts
Normal file
384
packages/mcp-server/src/http-server.ts
Normal file
@@ -0,0 +1,384 @@
|
|||||||
|
/**
|
||||||
|
* Embedded HTTP Server for MCP
|
||||||
|
*
|
||||||
|
* Serves a static HTML page with draw.io embed and handles state sync.
|
||||||
|
* This eliminates the need for an external Next.js app.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import http from "node:http"
|
||||||
|
import { log } from "./logger.js"
|
||||||
|
|
||||||
|
interface SessionState {
|
||||||
|
xml: string
|
||||||
|
version: number
|
||||||
|
lastUpdated: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
// In-memory state store (shared with MCP server in same process)
|
||||||
|
export const stateStore = new Map<string, SessionState>()
|
||||||
|
|
||||||
|
let server: http.Server | null = null
|
||||||
|
let serverPort: number = 6002
|
||||||
|
const MAX_PORT = 6020 // Don't retry beyond this port
|
||||||
|
const SESSION_TTL = 60 * 60 * 1000 // 1 hour
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get state for a session
|
||||||
|
*/
|
||||||
|
export function getState(sessionId: string): SessionState | undefined {
|
||||||
|
return stateStore.get(sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set state for a session
|
||||||
|
*/
|
||||||
|
export function setState(sessionId: string, xml: string): number {
|
||||||
|
const existing = stateStore.get(sessionId)
|
||||||
|
const newVersion = (existing?.version || 0) + 1
|
||||||
|
|
||||||
|
stateStore.set(sessionId, {
|
||||||
|
xml,
|
||||||
|
version: newVersion,
|
||||||
|
lastUpdated: new Date(),
|
||||||
|
})
|
||||||
|
|
||||||
|
log.debug(`State updated: session=${sessionId}, version=${newVersion}`)
|
||||||
|
return newVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the embedded HTTP server
|
||||||
|
*/
|
||||||
|
export function startHttpServer(port: number = 6002): Promise<number> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (server) {
|
||||||
|
resolve(serverPort)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
serverPort = port
|
||||||
|
server = http.createServer(handleRequest)
|
||||||
|
|
||||||
|
server.on("error", (err: NodeJS.ErrnoException) => {
|
||||||
|
if (err.code === "EADDRINUSE") {
|
||||||
|
if (port >= MAX_PORT) {
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
`No available ports in range 6002-${MAX_PORT}`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.info(`Port ${port} in use, trying ${port + 1}`)
|
||||||
|
server = null
|
||||||
|
startHttpServer(port + 1)
|
||||||
|
.then(resolve)
|
||||||
|
.catch(reject)
|
||||||
|
} else {
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
server.listen(port, () => {
|
||||||
|
serverPort = port
|
||||||
|
log.info(`Embedded HTTP server running on http://localhost:${port}`)
|
||||||
|
resolve(port)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the HTTP server
|
||||||
|
*/
|
||||||
|
export function stopHttpServer(): void {
|
||||||
|
if (server) {
|
||||||
|
server.close()
|
||||||
|
server = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up expired sessions
|
||||||
|
*/
|
||||||
|
function cleanupExpiredSessions(): void {
|
||||||
|
const now = Date.now()
|
||||||
|
for (const [sessionId, state] of stateStore) {
|
||||||
|
if (now - state.lastUpdated.getTime() > SESSION_TTL) {
|
||||||
|
stateStore.delete(sessionId)
|
||||||
|
log.info(`Cleaned up expired session: ${sessionId}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run cleanup every 5 minutes
|
||||||
|
setInterval(cleanupExpiredSessions, 5 * 60 * 1000)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current server port
|
||||||
|
*/
|
||||||
|
export function getServerPort(): number {
|
||||||
|
return serverPort
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle HTTP requests
|
||||||
|
*/
|
||||||
|
function handleRequest(
|
||||||
|
req: http.IncomingMessage,
|
||||||
|
res: http.ServerResponse,
|
||||||
|
): void {
|
||||||
|
const url = new URL(req.url || "/", `http://localhost:${serverPort}`)
|
||||||
|
|
||||||
|
// CORS headers for local development
|
||||||
|
res.setHeader("Access-Control-Allow-Origin", "*")
|
||||||
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||||
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type")
|
||||||
|
|
||||||
|
if (req.method === "OPTIONS") {
|
||||||
|
res.writeHead(204)
|
||||||
|
res.end()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route handling
|
||||||
|
if (url.pathname === "/" || url.pathname === "/index.html") {
|
||||||
|
serveHtml(req, res, url)
|
||||||
|
} else if (
|
||||||
|
url.pathname === "/api/state" ||
|
||||||
|
url.pathname === "/api/mcp/state"
|
||||||
|
) {
|
||||||
|
handleStateApi(req, res, url)
|
||||||
|
} else if (
|
||||||
|
url.pathname === "/api/health" ||
|
||||||
|
url.pathname === "/api/mcp/health"
|
||||||
|
) {
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json" })
|
||||||
|
res.end(JSON.stringify({ status: "ok", mcp: true }))
|
||||||
|
} else {
|
||||||
|
res.writeHead(404)
|
||||||
|
res.end("Not Found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serve the HTML page with draw.io embed
|
||||||
|
*/
|
||||||
|
function serveHtml(
|
||||||
|
req: http.IncomingMessage,
|
||||||
|
res: http.ServerResponse,
|
||||||
|
url: URL,
|
||||||
|
): void {
|
||||||
|
const sessionId = url.searchParams.get("mcp") || ""
|
||||||
|
|
||||||
|
res.writeHead(200, { "Content-Type": "text/html" })
|
||||||
|
res.end(getHtmlPage(sessionId))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle state API requests
|
||||||
|
*/
|
||||||
|
function handleStateApi(
|
||||||
|
req: http.IncomingMessage,
|
||||||
|
res: http.ServerResponse,
|
||||||
|
url: URL,
|
||||||
|
): void {
|
||||||
|
if (req.method === "GET") {
|
||||||
|
const sessionId = url.searchParams.get("sessionId")
|
||||||
|
if (!sessionId) {
|
||||||
|
res.writeHead(400, { "Content-Type": "application/json" })
|
||||||
|
res.end(JSON.stringify({ error: "sessionId required" }))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = stateStore.get(sessionId)
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json" })
|
||||||
|
res.end(
|
||||||
|
JSON.stringify({
|
||||||
|
xml: state?.xml || null,
|
||||||
|
version: state?.version || 0,
|
||||||
|
lastUpdated: state?.lastUpdated?.toISOString() || null,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
} else if (req.method === "POST") {
|
||||||
|
let body = ""
|
||||||
|
req.on("data", (chunk) => {
|
||||||
|
body += chunk
|
||||||
|
})
|
||||||
|
req.on("end", () => {
|
||||||
|
try {
|
||||||
|
const { sessionId, xml } = JSON.parse(body)
|
||||||
|
if (!sessionId) {
|
||||||
|
res.writeHead(400, { "Content-Type": "application/json" })
|
||||||
|
res.end(JSON.stringify({ error: "sessionId required" }))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const version = setState(sessionId, xml)
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json" })
|
||||||
|
res.end(JSON.stringify({ success: true, version }))
|
||||||
|
} catch {
|
||||||
|
res.writeHead(400, { "Content-Type": "application/json" })
|
||||||
|
res.end(JSON.stringify({ error: "Invalid JSON" }))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
res.writeHead(405)
|
||||||
|
res.end("Method Not Allowed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the HTML page with draw.io embed
|
||||||
|
*/
|
||||||
|
function getHtmlPage(sessionId: string): string {
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Draw.io MCP - ${sessionId || "No Session"}</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
html, body { width: 100%; height: 100%; overflow: hidden; }
|
||||||
|
#container { width: 100%; height: 100%; display: flex; flex-direction: column; }
|
||||||
|
#header {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: #1a1a2e;
|
||||||
|
color: #eee;
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
#header .session { color: #888; font-size: 12px; }
|
||||||
|
#header .status { font-size: 12px; }
|
||||||
|
#header .status.connected { color: #4ade80; }
|
||||||
|
#header .status.disconnected { color: #f87171; }
|
||||||
|
#drawio { flex: 1; border: none; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="container">
|
||||||
|
<div id="header">
|
||||||
|
<div>
|
||||||
|
<strong>Draw.io MCP</strong>
|
||||||
|
<span class="session">${sessionId ? `Session: ${sessionId}` : "No MCP session"}</span>
|
||||||
|
</div>
|
||||||
|
<div id="status" class="status disconnected">Connecting...</div>
|
||||||
|
</div>
|
||||||
|
<iframe id="drawio" src="https://embed.diagrams.net/?embed=1&proto=json&spin=1&libraries=1"></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const sessionId = "${sessionId}";
|
||||||
|
const iframe = document.getElementById('drawio');
|
||||||
|
const statusEl = document.getElementById('status');
|
||||||
|
|
||||||
|
let currentVersion = 0;
|
||||||
|
let isDrawioReady = false;
|
||||||
|
let pendingXml = null;
|
||||||
|
let lastLoadedXml = null;
|
||||||
|
|
||||||
|
// Listen for messages from draw.io
|
||||||
|
window.addEventListener('message', (event) => {
|
||||||
|
if (event.origin !== 'https://embed.diagrams.net') return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const msg = JSON.parse(event.data);
|
||||||
|
handleDrawioMessage(msg);
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore non-JSON messages
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleDrawioMessage(msg) {
|
||||||
|
if (msg.event === 'init') {
|
||||||
|
isDrawioReady = true;
|
||||||
|
statusEl.textContent = 'Ready';
|
||||||
|
statusEl.className = 'status connected';
|
||||||
|
|
||||||
|
// Load pending XML if any
|
||||||
|
if (pendingXml) {
|
||||||
|
loadDiagram(pendingXml);
|
||||||
|
pendingXml = null;
|
||||||
|
}
|
||||||
|
} else if (msg.event === 'save') {
|
||||||
|
// User saved - push to state
|
||||||
|
if (msg.xml && msg.xml !== lastLoadedXml) {
|
||||||
|
pushState(msg.xml);
|
||||||
|
}
|
||||||
|
} else if (msg.event === 'export') {
|
||||||
|
// Export completed
|
||||||
|
if (msg.data) {
|
||||||
|
pushState(msg.data);
|
||||||
|
}
|
||||||
|
} else if (msg.event === 'autosave') {
|
||||||
|
// Autosave - push to state
|
||||||
|
if (msg.xml && msg.xml !== lastLoadedXml) {
|
||||||
|
pushState(msg.xml);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadDiagram(xml) {
|
||||||
|
if (!isDrawioReady) {
|
||||||
|
pendingXml = xml;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastLoadedXml = xml;
|
||||||
|
iframe.contentWindow.postMessage(JSON.stringify({
|
||||||
|
action: 'load',
|
||||||
|
xml: xml,
|
||||||
|
autosave: 1
|
||||||
|
}), '*');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pushState(xml) {
|
||||||
|
if (!sessionId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/state', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ sessionId, xml })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json();
|
||||||
|
currentVersion = result.version;
|
||||||
|
lastLoadedXml = xml;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to push state:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pollState() {
|
||||||
|
if (!sessionId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/state?sessionId=' + encodeURIComponent(sessionId));
|
||||||
|
if (!response.ok) return;
|
||||||
|
|
||||||
|
const state = await response.json();
|
||||||
|
|
||||||
|
if (state.version && state.version > currentVersion && state.xml) {
|
||||||
|
currentVersion = state.version;
|
||||||
|
loadDiagram(state.xml);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to poll state:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start polling if we have a session
|
||||||
|
if (sessionId) {
|
||||||
|
pollState();
|
||||||
|
setInterval(pollState, 2000);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
}
|
||||||
518
packages/mcp-server/src/index.ts
Normal file
518
packages/mcp-server/src/index.ts
Normal file
@@ -0,0 +1,518 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* MCP Server for Next AI Draw.io
|
||||||
|
*
|
||||||
|
* Enables AI agents (Claude Desktop, Cursor, etc.) to generate and edit
|
||||||
|
* draw.io diagrams with real-time browser preview.
|
||||||
|
*
|
||||||
|
* Uses an embedded HTTP server - no external dependencies required.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Setup DOM polyfill for Node.js (required for XML operations)
|
||||||
|
import { DOMParser } from "linkedom"
|
||||||
|
;(globalThis as any).DOMParser = DOMParser
|
||||||
|
|
||||||
|
// Create XMLSerializer polyfill using outerHTML
|
||||||
|
class XMLSerializerPolyfill {
|
||||||
|
serializeToString(node: any): string {
|
||||||
|
if (node.outerHTML !== undefined) {
|
||||||
|
return node.outerHTML
|
||||||
|
}
|
||||||
|
if (node.documentElement) {
|
||||||
|
return node.documentElement.outerHTML
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
;(globalThis as any).XMLSerializer = XMLSerializerPolyfill
|
||||||
|
|
||||||
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
|
||||||
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
||||||
|
import open from "open"
|
||||||
|
import { z } from "zod"
|
||||||
|
import {
|
||||||
|
applyDiagramOperations,
|
||||||
|
type DiagramOperation,
|
||||||
|
} from "./diagram-operations.js"
|
||||||
|
import {
|
||||||
|
getServerPort,
|
||||||
|
getState,
|
||||||
|
setState,
|
||||||
|
startHttpServer,
|
||||||
|
} from "./http-server.js"
|
||||||
|
import { log } from "./logger.js"
|
||||||
|
import { validateAndFixXml } from "./xml-validation.js"
|
||||||
|
|
||||||
|
// Server configuration
|
||||||
|
const config = {
|
||||||
|
port: parseInt(process.env.PORT || "6002"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session state (single session for simplicity)
|
||||||
|
let currentSession: {
|
||||||
|
id: string
|
||||||
|
xml: string
|
||||||
|
version: number
|
||||||
|
} | null = null
|
||||||
|
|
||||||
|
// Create MCP server
|
||||||
|
const server = new McpServer({
|
||||||
|
name: "next-ai-drawio",
|
||||||
|
version: "0.1.2",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Register prompt with workflow guidance
|
||||||
|
server.prompt(
|
||||||
|
"diagram-workflow",
|
||||||
|
"Guidelines for creating and editing draw.io diagrams",
|
||||||
|
() => ({
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: {
|
||||||
|
type: "text",
|
||||||
|
text: `# Draw.io Diagram Workflow Guidelines
|
||||||
|
|
||||||
|
## Creating a New Diagram
|
||||||
|
1. Call start_session to open the browser preview
|
||||||
|
2. Use display_diagram with complete mxGraphModel XML to create a new diagram
|
||||||
|
|
||||||
|
## Adding Elements to Existing Diagram
|
||||||
|
1. Use edit_diagram with "add" operation
|
||||||
|
2. Provide a unique cell_id and complete mxCell XML
|
||||||
|
3. No need to call get_diagram first - the server fetches latest state automatically
|
||||||
|
|
||||||
|
## Modifying or Deleting Existing Elements
|
||||||
|
1. FIRST call get_diagram to see current cell IDs and structure
|
||||||
|
2. THEN call edit_diagram with "update" or "delete" operations
|
||||||
|
3. For update, provide the cell_id and complete new mxCell XML
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
- display_diagram REPLACES the entire diagram - only use for new diagrams
|
||||||
|
- 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")`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tool: start_session
|
||||||
|
server.registerTool(
|
||||||
|
"start_session",
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
"Start a new diagram session and open the browser for real-time preview. " +
|
||||||
|
"Starts an embedded server and opens a browser window with draw.io. " +
|
||||||
|
"The browser will show diagram updates as they happen.",
|
||||||
|
inputSchema: {},
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
// Start embedded HTTP server
|
||||||
|
const port = await startHttpServer(config.port)
|
||||||
|
|
||||||
|
// Create session
|
||||||
|
const sessionId = `mcp-${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 8)}`
|
||||||
|
currentSession = {
|
||||||
|
id: sessionId,
|
||||||
|
xml: "",
|
||||||
|
version: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open browser
|
||||||
|
const browserUrl = `http://localhost:${port}?mcp=${sessionId}`
|
||||||
|
await open(browserUrl)
|
||||||
|
|
||||||
|
log.info(`Started session ${sessionId}, browser at ${browserUrl}`)
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Session started successfully!\n\nSession ID: ${sessionId}\nBrowser URL: ${browserUrl}\n\nThe browser will now show real-time diagram updates.`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const message =
|
||||||
|
error instanceof Error ? error.message : String(error)
|
||||||
|
log.error("start_session failed:", message)
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Error: ${message}` }],
|
||||||
|
isError: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tool: display_diagram
|
||||||
|
server.registerTool(
|
||||||
|
"display_diagram",
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
"Display a NEW draw.io diagram from XML. REPLACES the entire diagram. " +
|
||||||
|
"Use this for creating new diagrams from scratch. " +
|
||||||
|
"To ADD elements to an existing diagram, use edit_diagram with 'add' operation instead. " +
|
||||||
|
"You should generate valid draw.io/mxGraph XML format.",
|
||||||
|
inputSchema: {
|
||||||
|
xml: z
|
||||||
|
.string()
|
||||||
|
.describe("The draw.io XML to display (mxGraphModel format)"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async ({ xml: inputXml }) => {
|
||||||
|
try {
|
||||||
|
if (!currentSession) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Error: No active session. Please call start_session first.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate and auto-fix XML
|
||||||
|
let xml = inputXml
|
||||||
|
const { valid, error, fixed, fixes } = validateAndFixXml(xml)
|
||||||
|
if (fixed) {
|
||||||
|
xml = fixed
|
||||||
|
log.info(`XML auto-fixed: ${fixes.join(", ")}`)
|
||||||
|
}
|
||||||
|
if (!valid && error) {
|
||||||
|
log.error(`XML validation failed: ${error}`)
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Error: XML validation failed - ${error}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(`Displaying diagram, ${xml.length} chars`)
|
||||||
|
|
||||||
|
// Update session state
|
||||||
|
currentSession.xml = xml
|
||||||
|
currentSession.version++
|
||||||
|
|
||||||
|
// Push to embedded server state
|
||||||
|
setState(currentSession.id, xml)
|
||||||
|
|
||||||
|
log.info(`Diagram displayed successfully`)
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Diagram displayed successfully!\n\nThe diagram is now visible in your browser.\n\nXML length: ${xml.length} characters`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const message =
|
||||||
|
error instanceof Error ? error.message : String(error)
|
||||||
|
log.error("display_diagram failed:", message)
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Error: ${message}` }],
|
||||||
|
isError: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tool: edit_diagram
|
||||||
|
server.registerTool(
|
||||||
|
"edit_diagram",
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
"Edit the current diagram by ID-based operations (update/add/delete cells). " +
|
||||||
|
"ALWAYS fetches the latest state from browser first, so user's manual changes are preserved.\n\n" +
|
||||||
|
"IMPORTANT workflow:\n" +
|
||||||
|
"- For ADD operations: Can use directly - just provide new unique cell_id and new_xml.\n" +
|
||||||
|
"- For UPDATE/DELETE: Call get_diagram FIRST to see current cell IDs, then edit.\n\n" +
|
||||||
|
"Operations:\n" +
|
||||||
|
"- add: Add a new cell. Provide cell_id (new unique id) and new_xml.\n" +
|
||||||
|
"- update: Replace an existing cell by its id. Provide cell_id and complete new_xml.\n" +
|
||||||
|
"- delete: Remove a cell by its id. Only cell_id is needed.\n\n" +
|
||||||
|
"For add/update, new_xml must be a complete mxCell element including mxGeometry.",
|
||||||
|
inputSchema: {
|
||||||
|
operations: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
type: z
|
||||||
|
.enum(["update", "add", "delete"])
|
||||||
|
.describe("Operation type"),
|
||||||
|
cell_id: z.string().describe("The id of the mxCell"),
|
||||||
|
new_xml: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe(
|
||||||
|
"Complete mxCell XML element (required for update/add)",
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.describe("Array of operations to apply"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async ({ operations }) => {
|
||||||
|
try {
|
||||||
|
if (!currentSession) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Error: No active session. Please call start_session first.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch latest state from browser
|
||||||
|
const browserState = getState(currentSession.id)
|
||||||
|
if (browserState?.xml) {
|
||||||
|
currentSession.xml = browserState.xml
|
||||||
|
log.info("Fetched latest diagram state from browser")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentSession.xml) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Error: No diagram to edit. Please create a diagram first with display_diagram.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(`Editing diagram with ${operations.length} operation(s)`)
|
||||||
|
|
||||||
|
// Validate and auto-fix new_xml for each operation
|
||||||
|
const validatedOps = operations.map((op) => {
|
||||||
|
if (op.new_xml) {
|
||||||
|
const { valid, error, fixed, fixes } = validateAndFixXml(
|
||||||
|
op.new_xml,
|
||||||
|
)
|
||||||
|
if (fixed) {
|
||||||
|
log.info(
|
||||||
|
`Operation ${op.type} ${op.cell_id}: XML auto-fixed: ${fixes.join(", ")}`,
|
||||||
|
)
|
||||||
|
return { ...op, new_xml: fixed }
|
||||||
|
}
|
||||||
|
if (!valid && error) {
|
||||||
|
log.warn(
|
||||||
|
`Operation ${op.type} ${op.cell_id}: XML validation failed: ${error}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return op
|
||||||
|
})
|
||||||
|
|
||||||
|
// Apply operations
|
||||||
|
const { result, errors } = applyDiagramOperations(
|
||||||
|
currentSession.xml,
|
||||||
|
validatedOps as DiagramOperation[],
|
||||||
|
)
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
const errorMessages = errors
|
||||||
|
.map((e) => `${e.type} ${e.cellId}: ${e.message}`)
|
||||||
|
.join("\n")
|
||||||
|
log.warn(`Edit had ${errors.length} error(s): ${errorMessages}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update state
|
||||||
|
currentSession.xml = result
|
||||||
|
currentSession.version++
|
||||||
|
|
||||||
|
// Push to embedded server
|
||||||
|
setState(currentSession.id, result)
|
||||||
|
|
||||||
|
log.info(`Diagram edited successfully`)
|
||||||
|
|
||||||
|
const successMsg = `Diagram edited successfully!\n\nApplied ${operations.length} operation(s).`
|
||||||
|
const errorMsg =
|
||||||
|
errors.length > 0
|
||||||
|
? `\n\nWarnings:\n${errors.map((e) => `- ${e.type} ${e.cellId}: ${e.message}`).join("\n")}`
|
||||||
|
: ""
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: successMsg + errorMsg,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const message =
|
||||||
|
error instanceof Error ? error.message : String(error)
|
||||||
|
log.error("edit_diagram failed:", message)
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Error: ${message}` }],
|
||||||
|
isError: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tool: get_diagram
|
||||||
|
server.registerTool(
|
||||||
|
"get_diagram",
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
"Get the current diagram XML (fetches latest from browser, including user's manual edits). " +
|
||||||
|
"Call this BEFORE edit_diagram if you need to update or delete existing elements, " +
|
||||||
|
"so you can see the current cell IDs and structure.",
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
if (!currentSession) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Error: No active session. Please call start_session first.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch latest state from browser
|
||||||
|
const browserState = getState(currentSession.id)
|
||||||
|
if (browserState?.xml) {
|
||||||
|
currentSession.xml = browserState.xml
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentSession.xml) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "No diagram exists yet. Use display_diagram to create one.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Current diagram XML:\n\n${currentSession.xml}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const message =
|
||||||
|
error instanceof Error ? error.message : String(error)
|
||||||
|
log.error("get_diagram failed:", message)
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Error: ${message}` }],
|
||||||
|
isError: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tool: export_diagram
|
||||||
|
server.registerTool(
|
||||||
|
"export_diagram",
|
||||||
|
{
|
||||||
|
description: "Export the current diagram to a .drawio file.",
|
||||||
|
inputSchema: {
|
||||||
|
path: z
|
||||||
|
.string()
|
||||||
|
.describe(
|
||||||
|
"File path to save the diagram (e.g., ./diagram.drawio)",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async ({ path }) => {
|
||||||
|
try {
|
||||||
|
if (!currentSession) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Error: No active session. Please call start_session first.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch latest state
|
||||||
|
const browserState = getState(currentSession.id)
|
||||||
|
if (browserState?.xml) {
|
||||||
|
currentSession.xml = browserState.xml
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentSession.xml) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Error: No diagram to export. Please create a diagram first.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fs = await import("node:fs/promises")
|
||||||
|
const nodePath = await import("node:path")
|
||||||
|
|
||||||
|
let filePath = path
|
||||||
|
if (!filePath.endsWith(".drawio")) {
|
||||||
|
filePath = `${filePath}.drawio`
|
||||||
|
}
|
||||||
|
|
||||||
|
const absolutePath = nodePath.resolve(filePath)
|
||||||
|
await fs.writeFile(absolutePath, currentSession.xml, "utf-8")
|
||||||
|
|
||||||
|
log.info(`Diagram exported to ${absolutePath}`)
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Diagram exported successfully!\n\nFile: ${absolutePath}\nSize: ${currentSession.xml.length} characters`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const message =
|
||||||
|
error instanceof Error ? error.message : String(error)
|
||||||
|
log.error("export_diagram failed:", message)
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Error: ${message}` }],
|
||||||
|
isError: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Start the MCP server
|
||||||
|
async function main() {
|
||||||
|
log.info("Starting MCP server for Next AI Draw.io (embedded mode)...")
|
||||||
|
|
||||||
|
const transport = new StdioServerTransport()
|
||||||
|
await server.connect(transport)
|
||||||
|
|
||||||
|
log.info("MCP server running on stdio")
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
log.error("Fatal error:", error)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
24
packages/mcp-server/src/logger.ts
Normal file
24
packages/mcp-server/src/logger.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* Logger for MCP server
|
||||||
|
*
|
||||||
|
* CRITICAL: MCP servers communicate via STDIO (stdin/stdout).
|
||||||
|
* Using console.log() will corrupt the JSON-RPC protocol messages.
|
||||||
|
* ALL logging MUST use console.error() which writes to stderr.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const log = {
|
||||||
|
info: (msg: string, ...args: unknown[]) => {
|
||||||
|
console.error(`[MCP-DrawIO] [INFO] ${msg}`, ...args)
|
||||||
|
},
|
||||||
|
error: (msg: string, ...args: unknown[]) => {
|
||||||
|
console.error(`[MCP-DrawIO] [ERROR] ${msg}`, ...args)
|
||||||
|
},
|
||||||
|
debug: (msg: string, ...args: unknown[]) => {
|
||||||
|
if (process.env.DEBUG === "true") {
|
||||||
|
console.error(`[MCP-DrawIO] [DEBUG] ${msg}`, ...args)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
warn: (msg: string, ...args: unknown[]) => {
|
||||||
|
console.error(`[MCP-DrawIO] [WARN] ${msg}`, ...args)
|
||||||
|
},
|
||||||
|
}
|
||||||
926
packages/mcp-server/src/xml-validation.ts
Normal file
926
packages/mcp-server/src/xml-validation.ts
Normal file
@@ -0,0 +1,926 @@
|
|||||||
|
/**
|
||||||
|
* XML Validation and Auto-Fix for draw.io diagrams
|
||||||
|
* Copied from lib/utils.ts to avoid cross-package imports
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Constants
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/** Maximum XML size to process (1MB) - larger XMLs may cause performance issues */
|
||||||
|
const MAX_XML_SIZE = 1_000_000
|
||||||
|
|
||||||
|
/** Maximum iterations for aggressive cell dropping to prevent infinite loops */
|
||||||
|
const MAX_DROP_ITERATIONS = 10
|
||||||
|
|
||||||
|
/** Structural attributes that should not be duplicated in draw.io */
|
||||||
|
const STRUCTURAL_ATTRS = [
|
||||||
|
"edge",
|
||||||
|
"parent",
|
||||||
|
"source",
|
||||||
|
"target",
|
||||||
|
"vertex",
|
||||||
|
"connectable",
|
||||||
|
]
|
||||||
|
|
||||||
|
/** Valid XML entity names */
|
||||||
|
const VALID_ENTITIES = new Set(["lt", "gt", "amp", "quot", "apos"])
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// XML Parsing Helpers
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
interface ParsedTag {
|
||||||
|
tag: string
|
||||||
|
tagName: string
|
||||||
|
isClosing: boolean
|
||||||
|
isSelfClosing: boolean
|
||||||
|
startIndex: number
|
||||||
|
endIndex: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse XML tags while properly handling quoted strings
|
||||||
|
*/
|
||||||
|
function parseXmlTags(xml: string): ParsedTag[] {
|
||||||
|
const tags: ParsedTag[] = []
|
||||||
|
let i = 0
|
||||||
|
|
||||||
|
while (i < xml.length) {
|
||||||
|
const tagStart = xml.indexOf("<", i)
|
||||||
|
if (tagStart === -1) break
|
||||||
|
|
||||||
|
// Find matching > by tracking quotes
|
||||||
|
let tagEnd = tagStart + 1
|
||||||
|
let inQuote = false
|
||||||
|
let quoteChar = ""
|
||||||
|
|
||||||
|
while (tagEnd < xml.length) {
|
||||||
|
const c = xml[tagEnd]
|
||||||
|
if (inQuote) {
|
||||||
|
if (c === quoteChar) inQuote = false
|
||||||
|
} else {
|
||||||
|
if (c === '"' || c === "'") {
|
||||||
|
inQuote = true
|
||||||
|
quoteChar = c
|
||||||
|
} else if (c === ">") {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tagEnd++
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tagEnd >= xml.length) break
|
||||||
|
|
||||||
|
const tag = xml.substring(tagStart, tagEnd + 1)
|
||||||
|
i = tagEnd + 1
|
||||||
|
|
||||||
|
const tagMatch = /^<(\/?)([a-zA-Z][a-zA-Z0-9:_-]*)/.exec(tag)
|
||||||
|
if (!tagMatch) continue
|
||||||
|
|
||||||
|
tags.push({
|
||||||
|
tag,
|
||||||
|
tagName: tagMatch[2],
|
||||||
|
isClosing: tagMatch[1] === "/",
|
||||||
|
isSelfClosing: tag.endsWith("/>"),
|
||||||
|
startIndex: tagStart,
|
||||||
|
endIndex: tagEnd,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return tags
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Validation Helper Functions
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/** Check for duplicate structural attributes in a tag */
|
||||||
|
function checkDuplicateAttributes(xml: string): string | null {
|
||||||
|
const structuralSet = new Set(STRUCTURAL_ATTRS)
|
||||||
|
const tagPattern = /<[^>]+>/g
|
||||||
|
let tagMatch
|
||||||
|
while ((tagMatch = tagPattern.exec(xml)) !== null) {
|
||||||
|
const tag = tagMatch[0]
|
||||||
|
const attrPattern = /\s([a-zA-Z_:][a-zA-Z0-9_:.-]*)\s*=/g
|
||||||
|
const attributes = new Map<string, number>()
|
||||||
|
let attrMatch
|
||||||
|
while ((attrMatch = attrPattern.exec(tag)) !== null) {
|
||||||
|
const attrName = attrMatch[1]
|
||||||
|
attributes.set(attrName, (attributes.get(attrName) || 0) + 1)
|
||||||
|
}
|
||||||
|
const duplicates = Array.from(attributes.entries())
|
||||||
|
.filter(([name, count]) => count > 1 && structuralSet.has(name))
|
||||||
|
.map(([name]) => name)
|
||||||
|
if (duplicates.length > 0) {
|
||||||
|
return `Invalid XML: Duplicate structural attribute(s): ${duplicates.join(", ")}. Remove duplicate attributes.`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check for duplicate IDs in XML */
|
||||||
|
function checkDuplicateIds(xml: string): string | null {
|
||||||
|
const idPattern = /\bid\s*=\s*["']([^"']+)["']/gi
|
||||||
|
const ids = new Map<string, number>()
|
||||||
|
let idMatch
|
||||||
|
while ((idMatch = idPattern.exec(xml)) !== null) {
|
||||||
|
const id = idMatch[1]
|
||||||
|
ids.set(id, (ids.get(id) || 0) + 1)
|
||||||
|
}
|
||||||
|
const duplicateIds = Array.from(ids.entries())
|
||||||
|
.filter(([, count]) => count > 1)
|
||||||
|
.map(([id, count]) => `'${id}' (${count}x)`)
|
||||||
|
if (duplicateIds.length > 0) {
|
||||||
|
return `Invalid XML: Found duplicate ID(s): ${duplicateIds.slice(0, 3).join(", ")}. All id attributes must be unique.`
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check for tag mismatches using parsed tags */
|
||||||
|
function checkTagMismatches(xml: string): string | null {
|
||||||
|
const xmlWithoutComments = xml.replace(/<!--[\s\S]*?-->/g, "")
|
||||||
|
const tags = parseXmlTags(xmlWithoutComments)
|
||||||
|
const tagStack: string[] = []
|
||||||
|
|
||||||
|
for (const { tagName, isClosing, isSelfClosing } of tags) {
|
||||||
|
if (isClosing) {
|
||||||
|
if (tagStack.length === 0) {
|
||||||
|
return `Invalid XML: Closing tag </${tagName}> without matching opening tag`
|
||||||
|
}
|
||||||
|
const expected = tagStack.pop()
|
||||||
|
if (expected?.toLowerCase() !== tagName.toLowerCase()) {
|
||||||
|
return `Invalid XML: Expected closing tag </${expected}> but found </${tagName}>`
|
||||||
|
}
|
||||||
|
} else if (!isSelfClosing) {
|
||||||
|
tagStack.push(tagName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tagStack.length > 0) {
|
||||||
|
return `Invalid XML: Document has ${tagStack.length} unclosed tag(s): ${tagStack.join(", ")}`
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check for invalid character references */
|
||||||
|
function checkCharacterReferences(xml: string): string | null {
|
||||||
|
const charRefPattern = /&#x?[^;]+;?/g
|
||||||
|
let charMatch
|
||||||
|
while ((charMatch = charRefPattern.exec(xml)) !== null) {
|
||||||
|
const ref = charMatch[0]
|
||||||
|
if (ref.startsWith("&#x")) {
|
||||||
|
if (!ref.endsWith(";")) {
|
||||||
|
return `Invalid XML: Missing semicolon after hex reference: ${ref}`
|
||||||
|
}
|
||||||
|
const hexDigits = ref.substring(3, ref.length - 1)
|
||||||
|
if (hexDigits.length === 0 || !/^[0-9a-fA-F]+$/.test(hexDigits)) {
|
||||||
|
return `Invalid XML: Invalid hex character reference: ${ref}`
|
||||||
|
}
|
||||||
|
} else if (ref.startsWith("&#")) {
|
||||||
|
if (!ref.endsWith(";")) {
|
||||||
|
return `Invalid XML: Missing semicolon after decimal reference: ${ref}`
|
||||||
|
}
|
||||||
|
const decDigits = ref.substring(2, ref.length - 1)
|
||||||
|
if (decDigits.length === 0 || !/^[0-9]+$/.test(decDigits)) {
|
||||||
|
return `Invalid XML: Invalid decimal character reference: ${ref}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check for invalid entity references */
|
||||||
|
function checkEntityReferences(xml: string): string | null {
|
||||||
|
const xmlWithoutComments = xml.replace(/<!--[\s\S]*?-->/g, "")
|
||||||
|
const bareAmpPattern = /&(?!(?:lt|gt|amp|quot|apos|#))/g
|
||||||
|
if (bareAmpPattern.test(xmlWithoutComments)) {
|
||||||
|
return "Invalid XML: Found unescaped & character(s). Replace & with &"
|
||||||
|
}
|
||||||
|
const invalidEntityPattern = /&([a-zA-Z][a-zA-Z0-9]*);/g
|
||||||
|
let entityMatch
|
||||||
|
while (
|
||||||
|
(entityMatch = invalidEntityPattern.exec(xmlWithoutComments)) !== null
|
||||||
|
) {
|
||||||
|
if (!VALID_ENTITIES.has(entityMatch[1])) {
|
||||||
|
return `Invalid XML: Invalid entity reference: &${entityMatch[1]}; - use only valid XML entities (lt, gt, amp, quot, apos)`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check for nested mxCell tags using regex */
|
||||||
|
function checkNestedMxCells(xml: string): string | null {
|
||||||
|
const cellTagPattern = /<\/?mxCell[^>]*>/g
|
||||||
|
const cellStack: number[] = []
|
||||||
|
let cellMatch
|
||||||
|
while ((cellMatch = cellTagPattern.exec(xml)) !== null) {
|
||||||
|
const tag = cellMatch[0]
|
||||||
|
if (tag.startsWith("</mxCell>")) {
|
||||||
|
if (cellStack.length > 0) cellStack.pop()
|
||||||
|
} else if (!tag.endsWith("/>")) {
|
||||||
|
const isLabelOrGeometry =
|
||||||
|
/\sas\s*=\s*["'](valueLabel|geometry)["']/.test(tag)
|
||||||
|
if (!isLabelOrGeometry) {
|
||||||
|
cellStack.push(cellMatch.index)
|
||||||
|
if (cellStack.length > 1) {
|
||||||
|
return "Invalid XML: Found nested mxCell tags. Cells should be siblings, not nested inside other mxCell elements."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Main Validation Function
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates draw.io XML structure for common issues
|
||||||
|
* Uses DOM parsing + additional regex checks for high accuracy
|
||||||
|
* @param xml - The XML string to validate
|
||||||
|
* @returns null if valid, error message string if invalid
|
||||||
|
*/
|
||||||
|
export function validateMxCellStructure(xml: string): string | null {
|
||||||
|
// Size check for performance
|
||||||
|
if (xml.length > MAX_XML_SIZE) {
|
||||||
|
console.warn(
|
||||||
|
`[validateMxCellStructure] XML size (${xml.length}) exceeds ${MAX_XML_SIZE} bytes, may cause performance issues`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0. First use DOM parser to catch syntax errors (most accurate)
|
||||||
|
try {
|
||||||
|
const parser = new DOMParser()
|
||||||
|
const doc = parser.parseFromString(xml, "text/xml")
|
||||||
|
const parseError = doc.querySelector("parsererror")
|
||||||
|
if (parseError) {
|
||||||
|
return `Invalid XML: The XML contains syntax errors (likely unescaped special characters like <, >, & in attribute values). Please escape special characters: use < for <, > for >, & for &, " for ". Regenerate the diagram with properly escaped values.`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DOM-based checks for nested mxCell
|
||||||
|
const allCells = doc.querySelectorAll("mxCell")
|
||||||
|
for (const cell of allCells) {
|
||||||
|
if (cell.parentElement?.tagName === "mxCell") {
|
||||||
|
const id = cell.getAttribute("id") || "unknown"
|
||||||
|
return `Invalid XML: Found nested mxCell (id="${id}"). Cells should be siblings, not nested inside other mxCell elements.`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(
|
||||||
|
"[validateMxCellStructure] DOMParser threw unexpected error, falling back to regex validation:",
|
||||||
|
error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Check for CDATA wrapper (invalid at document root)
|
||||||
|
if (/^\s*<!\[CDATA\[/.test(xml)) {
|
||||||
|
return "Invalid XML: XML is wrapped in CDATA section - remove <![CDATA[ from start and ]]> from end"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check for duplicate structural attributes
|
||||||
|
const dupAttrError = checkDuplicateAttributes(xml)
|
||||||
|
if (dupAttrError) {
|
||||||
|
return dupAttrError
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Check for unescaped < in attribute values
|
||||||
|
const attrValuePattern = /=\s*"([^"]*)"/g
|
||||||
|
let attrValMatch
|
||||||
|
while ((attrValMatch = attrValuePattern.exec(xml)) !== null) {
|
||||||
|
const value = attrValMatch[1]
|
||||||
|
if (/</.test(value) && !/</.test(value)) {
|
||||||
|
return "Invalid XML: Unescaped < character in attribute values. Replace < with <"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Check for duplicate IDs
|
||||||
|
const dupIdError = checkDuplicateIds(xml)
|
||||||
|
if (dupIdError) {
|
||||||
|
return dupIdError
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Check for tag mismatches
|
||||||
|
const tagMismatchError = checkTagMismatches(xml)
|
||||||
|
if (tagMismatchError) {
|
||||||
|
return tagMismatchError
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Check invalid character references
|
||||||
|
const charRefError = checkCharacterReferences(xml)
|
||||||
|
if (charRefError) {
|
||||||
|
return charRefError
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Check for invalid comment syntax (-- inside comments)
|
||||||
|
const commentPattern = /<!--([\s\S]*?)-->/g
|
||||||
|
let commentMatch
|
||||||
|
while ((commentMatch = commentPattern.exec(xml)) !== null) {
|
||||||
|
if (/--/.test(commentMatch[1])) {
|
||||||
|
return "Invalid XML: Comment contains -- (double hyphen) which is not allowed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. Check for unescaped entity references and invalid entity names
|
||||||
|
const entityError = checkEntityReferences(xml)
|
||||||
|
if (entityError) {
|
||||||
|
return entityError
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. Check for empty id attributes on mxCell
|
||||||
|
if (/<mxCell[^>]*\sid\s*=\s*["']\s*["'][^>]*>/g.test(xml)) {
|
||||||
|
return "Invalid XML: Found mxCell element(s) with empty id attribute"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 10. Check for nested mxCell tags
|
||||||
|
const nestedCellError = checkNestedMxCells(xml)
|
||||||
|
if (nestedCellError) {
|
||||||
|
return nestedCellError
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Auto-Fix Function
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to auto-fix common XML issues in draw.io diagrams
|
||||||
|
* @param xml - The XML string to fix
|
||||||
|
* @returns Object with fixed XML and list of fixes applied
|
||||||
|
*/
|
||||||
|
export function autoFixXml(xml: string): { fixed: string; fixes: string[] } {
|
||||||
|
let fixed = xml
|
||||||
|
const fixes: string[] = []
|
||||||
|
|
||||||
|
// 0. Fix JSON-escaped XML
|
||||||
|
if (/=\\"/.test(fixed)) {
|
||||||
|
fixed = fixed.replace(/\\"/g, '"')
|
||||||
|
fixed = fixed.replace(/\\n/g, "\n")
|
||||||
|
fixes.push("Fixed JSON-escaped XML")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Remove CDATA wrapper
|
||||||
|
if (/^\s*<!\[CDATA\[/.test(fixed)) {
|
||||||
|
fixed = fixed.replace(/^\s*<!\[CDATA\[/, "").replace(/\]\]>\s*$/, "")
|
||||||
|
fixes.push("Removed CDATA wrapper")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Remove text before XML declaration or root element
|
||||||
|
const xmlStart = fixed.search(/<(\?xml|mxGraphModel|mxfile)/i)
|
||||||
|
if (xmlStart > 0 && !/^<[a-zA-Z]/.test(fixed.trim())) {
|
||||||
|
fixed = fixed.substring(xmlStart)
|
||||||
|
fixes.push("Removed text before XML root")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fix duplicate attributes
|
||||||
|
let dupAttrFixed = false
|
||||||
|
fixed = fixed.replace(/<[^>]+>/g, (tag) => {
|
||||||
|
let newTag = tag
|
||||||
|
for (const attr of STRUCTURAL_ATTRS) {
|
||||||
|
const attrRegex = new RegExp(
|
||||||
|
`\\s${attr}\\s*=\\s*["'][^"']*["']`,
|
||||||
|
"gi",
|
||||||
|
)
|
||||||
|
const matches = tag.match(attrRegex)
|
||||||
|
if (matches && matches.length > 1) {
|
||||||
|
let firstKept = false
|
||||||
|
newTag = newTag.replace(attrRegex, (m) => {
|
||||||
|
if (!firstKept) {
|
||||||
|
firstKept = true
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
dupAttrFixed = true
|
||||||
|
return ""
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newTag
|
||||||
|
})
|
||||||
|
if (dupAttrFixed) {
|
||||||
|
fixes.push("Removed duplicate structural attributes")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Fix unescaped & characters
|
||||||
|
const ampersandPattern =
|
||||||
|
/&(?!(?:lt|gt|amp|quot|apos|#[0-9]+|#x[0-9a-fA-F]+);)/g
|
||||||
|
if (ampersandPattern.test(fixed)) {
|
||||||
|
fixed = fixed.replace(
|
||||||
|
/&(?!(?:lt|gt|amp|quot|apos|#[0-9]+|#x[0-9a-fA-F]+);)/g,
|
||||||
|
"&",
|
||||||
|
)
|
||||||
|
fixes.push("Escaped unescaped & characters")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Fix invalid entity names (double-escaping)
|
||||||
|
const invalidEntities = [
|
||||||
|
{ pattern: /&quot;/g, replacement: """, name: "&quot;" },
|
||||||
|
{ pattern: /&lt;/g, replacement: "<", name: "&lt;" },
|
||||||
|
{ pattern: /&gt;/g, replacement: ">", name: "&gt;" },
|
||||||
|
{ pattern: /&apos;/g, replacement: "'", name: "&apos;" },
|
||||||
|
{ pattern: /&amp;/g, replacement: "&", name: "&amp;" },
|
||||||
|
]
|
||||||
|
for (const { pattern, replacement, name } of invalidEntities) {
|
||||||
|
if (pattern.test(fixed)) {
|
||||||
|
fixed = fixed.replace(pattern, replacement)
|
||||||
|
fixes.push(`Fixed double-escaped entity ${name}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Fix malformed attribute quotes
|
||||||
|
const malformedQuotePattern = /(\s[a-zA-Z][a-zA-Z0-9_:-]*)="/
|
||||||
|
if (malformedQuotePattern.test(fixed)) {
|
||||||
|
fixed = fixed.replace(
|
||||||
|
/(\s[a-zA-Z][a-zA-Z0-9_:-]*)="([^&]*?)"/g,
|
||||||
|
'$1="$2"',
|
||||||
|
)
|
||||||
|
fixes.push("Fixed malformed attribute quotes")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Fix malformed closing tags
|
||||||
|
const malformedClosingTag = /<\/([a-zA-Z][a-zA-Z0-9]*)\s*\/>/g
|
||||||
|
if (malformedClosingTag.test(fixed)) {
|
||||||
|
fixed = fixed.replace(/<\/([a-zA-Z][a-zA-Z0-9]*)\s*\/>/g, "</$1>")
|
||||||
|
fixes.push("Fixed malformed closing tags")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. Fix missing space between attributes
|
||||||
|
const missingSpacePattern = /("[^"]*")([a-zA-Z][a-zA-Z0-9_:-]*=)/g
|
||||||
|
if (missingSpacePattern.test(fixed)) {
|
||||||
|
fixed = fixed.replace(/("[^"]*")([a-zA-Z][a-zA-Z0-9_:-]*=)/g, "$1 $2")
|
||||||
|
fixes.push("Added missing space between attributes")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. Fix unescaped quotes in style color values
|
||||||
|
const quotedColorPattern = /;([a-zA-Z]*[Cc]olor)="#/
|
||||||
|
if (quotedColorPattern.test(fixed)) {
|
||||||
|
fixed = fixed.replace(/;([a-zA-Z]*[Cc]olor)="#/g, ";$1=#")
|
||||||
|
fixes.push("Removed quotes around color values in style")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 10. Fix unescaped < in attribute values
|
||||||
|
const attrPattern = /(=\s*")([^"]*?)(<)([^"]*?)(")/g
|
||||||
|
let attrMatch
|
||||||
|
let hasUnescapedLt = false
|
||||||
|
while ((attrMatch = attrPattern.exec(fixed)) !== null) {
|
||||||
|
if (!attrMatch[3].startsWith("<")) {
|
||||||
|
hasUnescapedLt = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasUnescapedLt) {
|
||||||
|
fixed = fixed.replace(/=\s*"([^"]*)"/g, (_match, value) => {
|
||||||
|
const escaped = value.replace(/</g, "<")
|
||||||
|
return `="${escaped}"`
|
||||||
|
})
|
||||||
|
fixes.push("Escaped < characters in attribute values")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 11. Fix invalid hex character references
|
||||||
|
const invalidHexRefs: string[] = []
|
||||||
|
fixed = fixed.replace(/&#x([^;]*);/g, (match, hex) => {
|
||||||
|
if (/^[0-9a-fA-F]+$/.test(hex) && hex.length > 0) {
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
invalidHexRefs.push(match)
|
||||||
|
return ""
|
||||||
|
})
|
||||||
|
if (invalidHexRefs.length > 0) {
|
||||||
|
fixes.push(
|
||||||
|
`Removed ${invalidHexRefs.length} invalid hex character reference(s)`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 12. Fix invalid decimal character references
|
||||||
|
const invalidDecRefs: string[] = []
|
||||||
|
fixed = fixed.replace(/&#([^x][^;]*);/g, (match, dec) => {
|
||||||
|
if (/^[0-9]+$/.test(dec) && dec.length > 0) {
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
invalidDecRefs.push(match)
|
||||||
|
return ""
|
||||||
|
})
|
||||||
|
if (invalidDecRefs.length > 0) {
|
||||||
|
fixes.push(
|
||||||
|
`Removed ${invalidDecRefs.length} invalid decimal character reference(s)`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 13. Fix invalid comment syntax
|
||||||
|
fixed = fixed.replace(/<!--([\s\S]*?)-->/g, (match, content) => {
|
||||||
|
if (/--/.test(content)) {
|
||||||
|
let fixedContent = content
|
||||||
|
while (/--/.test(fixedContent)) {
|
||||||
|
fixedContent = fixedContent.replace(/--/g, "-")
|
||||||
|
}
|
||||||
|
fixes.push("Fixed invalid comment syntax")
|
||||||
|
return `<!--${fixedContent}-->`
|
||||||
|
}
|
||||||
|
return match
|
||||||
|
})
|
||||||
|
|
||||||
|
// 14. Fix <Cell> tags to <mxCell>
|
||||||
|
const hasCellTags = /<\/?Cell[\s>]/i.test(fixed)
|
||||||
|
if (hasCellTags) {
|
||||||
|
fixed = fixed.replace(/<Cell(\s)/gi, "<mxCell$1")
|
||||||
|
fixed = fixed.replace(/<Cell>/gi, "<mxCell>")
|
||||||
|
fixed = fixed.replace(/<\/Cell>/gi, "</mxCell>")
|
||||||
|
fixes.push("Fixed <Cell> tags to <mxCell>")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 15. Fix common closing tag typos (MUST run before foreign tag removal)
|
||||||
|
const tagTypos = [
|
||||||
|
{ wrong: /<\/mxElement>/gi, right: "</mxCell>", name: "</mxElement>" },
|
||||||
|
{ wrong: /<\/mxcell>/g, right: "</mxCell>", name: "</mxcell>" },
|
||||||
|
{
|
||||||
|
wrong: /<\/mxgeometry>/g,
|
||||||
|
right: "</mxGeometry>",
|
||||||
|
name: "</mxgeometry>",
|
||||||
|
},
|
||||||
|
{ wrong: /<\/mxpoint>/g, right: "</mxPoint>", name: "</mxpoint>" },
|
||||||
|
{
|
||||||
|
wrong: /<\/mxgraphmodel>/gi,
|
||||||
|
right: "</mxGraphModel>",
|
||||||
|
name: "</mxgraphmodel>",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
for (const { wrong, right, name } of tagTypos) {
|
||||||
|
const before = fixed
|
||||||
|
fixed = fixed.replace(wrong, right)
|
||||||
|
if (fixed !== before) {
|
||||||
|
fixes.push(`Fixed typo ${name} to ${right}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 16. Remove non-draw.io tags (after typo fixes so lowercase variants are fixed first)
|
||||||
|
const validDrawioTags = new Set([
|
||||||
|
"mxfile",
|
||||||
|
"diagram",
|
||||||
|
"mxGraphModel",
|
||||||
|
"root",
|
||||||
|
"mxCell",
|
||||||
|
"mxGeometry",
|
||||||
|
"mxPoint",
|
||||||
|
"Array",
|
||||||
|
"Object",
|
||||||
|
"mxRectangle",
|
||||||
|
])
|
||||||
|
const foreignTagPattern = /<\/?([a-zA-Z][a-zA-Z0-9_]*)[^>]*>/g
|
||||||
|
let foreignMatch
|
||||||
|
const foreignTags = new Set<string>()
|
||||||
|
while ((foreignMatch = foreignTagPattern.exec(fixed)) !== null) {
|
||||||
|
const tagName = foreignMatch[1]
|
||||||
|
if (!validDrawioTags.has(tagName)) {
|
||||||
|
foreignTags.add(tagName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (foreignTags.size > 0) {
|
||||||
|
for (const tag of foreignTags) {
|
||||||
|
fixed = fixed.replace(new RegExp(`<${tag}[^>]*>`, "gi"), "")
|
||||||
|
fixed = fixed.replace(new RegExp(`</${tag}>`, "gi"), "")
|
||||||
|
}
|
||||||
|
fixes.push(
|
||||||
|
`Removed foreign tags: ${Array.from(foreignTags).join(", ")}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 17. Fix unclosed tags
|
||||||
|
const tagStack: string[] = []
|
||||||
|
const parsedTags = parseXmlTags(fixed)
|
||||||
|
|
||||||
|
for (const { tagName, isClosing, isSelfClosing } of parsedTags) {
|
||||||
|
if (isClosing) {
|
||||||
|
const lastIdx = tagStack.lastIndexOf(tagName)
|
||||||
|
if (lastIdx !== -1) {
|
||||||
|
tagStack.splice(lastIdx, 1)
|
||||||
|
}
|
||||||
|
} else if (!isSelfClosing) {
|
||||||
|
tagStack.push(tagName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tagStack.length > 0) {
|
||||||
|
const tagsToClose: string[] = []
|
||||||
|
for (const tagName of tagStack.reverse()) {
|
||||||
|
const openCount = (
|
||||||
|
fixed.match(new RegExp(`<${tagName}[\\s>]`, "gi")) || []
|
||||||
|
).length
|
||||||
|
const closeCount = (
|
||||||
|
fixed.match(new RegExp(`</${tagName}>`, "gi")) || []
|
||||||
|
).length
|
||||||
|
if (openCount > closeCount) {
|
||||||
|
tagsToClose.push(tagName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tagsToClose.length > 0) {
|
||||||
|
const closingTags = tagsToClose.map((t) => `</${t}>`).join("\n")
|
||||||
|
fixed = fixed.trimEnd() + "\n" + closingTags
|
||||||
|
fixes.push(
|
||||||
|
`Closed ${tagsToClose.length} unclosed tag(s): ${tagsToClose.join(", ")}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 18. Remove extra closing tags
|
||||||
|
const tagCounts = new Map<
|
||||||
|
string,
|
||||||
|
{ opens: number; closes: number; selfClosing: number }
|
||||||
|
>()
|
||||||
|
const fullTagPattern = /<(\/?[a-zA-Z][a-zA-Z0-9]*)[^>]*>/g
|
||||||
|
let tagCountMatch
|
||||||
|
while ((tagCountMatch = fullTagPattern.exec(fixed)) !== null) {
|
||||||
|
const fullMatch = tagCountMatch[0]
|
||||||
|
const tagPart = tagCountMatch[1]
|
||||||
|
const isClosing = tagPart.startsWith("/")
|
||||||
|
const isSelfClosing = fullMatch.endsWith("/>")
|
||||||
|
const tagName = isClosing ? tagPart.slice(1) : tagPart
|
||||||
|
|
||||||
|
let counts = tagCounts.get(tagName)
|
||||||
|
if (!counts) {
|
||||||
|
counts = { opens: 0, closes: 0, selfClosing: 0 }
|
||||||
|
tagCounts.set(tagName, counts)
|
||||||
|
}
|
||||||
|
if (isClosing) {
|
||||||
|
counts.closes++
|
||||||
|
} else if (isSelfClosing) {
|
||||||
|
counts.selfClosing++
|
||||||
|
} else {
|
||||||
|
counts.opens++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [tagName, counts] of tagCounts) {
|
||||||
|
const extraCloses = counts.closes - counts.opens
|
||||||
|
if (extraCloses > 0) {
|
||||||
|
let removed = 0
|
||||||
|
const closeTagPattern = new RegExp(`</${tagName}>`, "g")
|
||||||
|
const matches = [...fixed.matchAll(closeTagPattern)]
|
||||||
|
for (
|
||||||
|
let i = matches.length - 1;
|
||||||
|
i >= 0 && removed < extraCloses;
|
||||||
|
i--
|
||||||
|
) {
|
||||||
|
const match = matches[i]
|
||||||
|
const idx = match.index ?? 0
|
||||||
|
fixed = fixed.slice(0, idx) + fixed.slice(idx + match[0].length)
|
||||||
|
removed++
|
||||||
|
}
|
||||||
|
if (removed > 0) {
|
||||||
|
fixes.push(
|
||||||
|
`Removed ${removed} extra </${tagName}> closing tag(s)`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 19. Remove trailing garbage after last XML tag
|
||||||
|
const closingTagPattern = /<\/[a-zA-Z][a-zA-Z0-9]*>|\/>/g
|
||||||
|
let lastValidTagEnd = -1
|
||||||
|
let closingMatch
|
||||||
|
while ((closingMatch = closingTagPattern.exec(fixed)) !== null) {
|
||||||
|
lastValidTagEnd = closingMatch.index + closingMatch[0].length
|
||||||
|
}
|
||||||
|
if (lastValidTagEnd > 0 && lastValidTagEnd < fixed.length) {
|
||||||
|
const trailing = fixed.slice(lastValidTagEnd).trim()
|
||||||
|
if (trailing) {
|
||||||
|
fixed = fixed.slice(0, lastValidTagEnd)
|
||||||
|
fixes.push("Removed trailing garbage after last XML tag")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 20. Fix nested mxCell by flattening
|
||||||
|
const lines = fixed.split("\n")
|
||||||
|
let newLines: string[] = []
|
||||||
|
let nestedFixed = 0
|
||||||
|
let extraClosingToRemove = 0
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i]
|
||||||
|
const nextLine = lines[i + 1]
|
||||||
|
|
||||||
|
if (
|
||||||
|
nextLine &&
|
||||||
|
/<mxCell\s/.test(line) &&
|
||||||
|
/<mxCell\s/.test(nextLine) &&
|
||||||
|
!line.includes("/>") &&
|
||||||
|
!nextLine.includes("/>")
|
||||||
|
) {
|
||||||
|
const id1 = line.match(/\bid\s*=\s*["']([^"']+)["']/)?.[1]
|
||||||
|
const id2 = nextLine.match(/\bid\s*=\s*["']([^"']+)["']/)?.[1]
|
||||||
|
|
||||||
|
if (id1 && id1 === id2) {
|
||||||
|
nestedFixed++
|
||||||
|
extraClosingToRemove++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extraClosingToRemove > 0 && /^\s*<\/mxCell>\s*$/.test(line)) {
|
||||||
|
extraClosingToRemove--
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
newLines.push(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nestedFixed > 0) {
|
||||||
|
fixed = newLines.join("\n")
|
||||||
|
fixes.push(`Flattened ${nestedFixed} duplicate-ID nested mxCell(s)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 21. Fix true nested mxCell (different IDs)
|
||||||
|
const lines2 = fixed.split("\n")
|
||||||
|
newLines = []
|
||||||
|
let trueNestedFixed = 0
|
||||||
|
let cellDepth = 0
|
||||||
|
let pendingCloseRemoval = 0
|
||||||
|
|
||||||
|
for (let i = 0; i < lines2.length; i++) {
|
||||||
|
const line = lines2[i]
|
||||||
|
const trimmed = line.trim()
|
||||||
|
|
||||||
|
const isOpenCell = /<mxCell\s/.test(trimmed) && !trimmed.endsWith("/>")
|
||||||
|
const isCloseCell = trimmed === "</mxCell>"
|
||||||
|
|
||||||
|
if (isOpenCell) {
|
||||||
|
if (cellDepth > 0) {
|
||||||
|
const indent = line.match(/^(\s*)/)?.[1] || ""
|
||||||
|
newLines.push(indent + "</mxCell>")
|
||||||
|
trueNestedFixed++
|
||||||
|
pendingCloseRemoval++
|
||||||
|
}
|
||||||
|
cellDepth = 1
|
||||||
|
newLines.push(line)
|
||||||
|
} else if (isCloseCell) {
|
||||||
|
if (pendingCloseRemoval > 0) {
|
||||||
|
pendingCloseRemoval--
|
||||||
|
} else {
|
||||||
|
cellDepth = Math.max(0, cellDepth - 1)
|
||||||
|
newLines.push(line)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newLines.push(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trueNestedFixed > 0) {
|
||||||
|
fixed = newLines.join("\n")
|
||||||
|
fixes.push(`Fixed ${trueNestedFixed} true nested mxCell(s)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 22. Fix duplicate IDs by appending suffix
|
||||||
|
const seenIds = new Map<string, number>()
|
||||||
|
const duplicateIds: string[] = []
|
||||||
|
|
||||||
|
const idPattern = /\bid\s*=\s*["']([^"']+)["']/gi
|
||||||
|
let idMatch
|
||||||
|
while ((idMatch = idPattern.exec(fixed)) !== null) {
|
||||||
|
const id = idMatch[1]
|
||||||
|
seenIds.set(id, (seenIds.get(id) || 0) + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [id, count] of seenIds) {
|
||||||
|
if (count > 1) duplicateIds.push(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (duplicateIds.length > 0) {
|
||||||
|
const idCounters = new Map<string, number>()
|
||||||
|
fixed = fixed.replace(/\bid\s*=\s*["']([^"']+)["']/gi, (match, id) => {
|
||||||
|
if (!duplicateIds.includes(id)) return match
|
||||||
|
|
||||||
|
const count = idCounters.get(id) || 0
|
||||||
|
idCounters.set(id, count + 1)
|
||||||
|
|
||||||
|
if (count === 0) return match
|
||||||
|
|
||||||
|
const newId = `${id}_dup${count}`
|
||||||
|
return match.replace(id, newId)
|
||||||
|
})
|
||||||
|
fixes.push(`Renamed ${duplicateIds.length} duplicate ID(s)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 23. Fix empty id attributes
|
||||||
|
let emptyIdCount = 0
|
||||||
|
fixed = fixed.replace(
|
||||||
|
/<mxCell([^>]*)\sid\s*=\s*["']\s*["']([^>]*)>/g,
|
||||||
|
(_match, before, after) => {
|
||||||
|
emptyIdCount++
|
||||||
|
const newId = `cell_${Date.now()}_${emptyIdCount}`
|
||||||
|
return `<mxCell${before} id="${newId}"${after}>`
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if (emptyIdCount > 0) {
|
||||||
|
fixes.push(`Generated ${emptyIdCount} missing ID(s)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 24. Aggressive: drop broken mxCell elements
|
||||||
|
if (typeof DOMParser !== "undefined") {
|
||||||
|
let droppedCells = 0
|
||||||
|
let maxIterations = MAX_DROP_ITERATIONS
|
||||||
|
while (maxIterations-- > 0) {
|
||||||
|
const parser = new DOMParser()
|
||||||
|
const doc = parser.parseFromString(fixed, "text/xml")
|
||||||
|
const parseError = doc.querySelector("parsererror")
|
||||||
|
if (!parseError) break
|
||||||
|
|
||||||
|
const errText = parseError.textContent || ""
|
||||||
|
const match = errText.match(/(\d+):\d+:/)
|
||||||
|
if (!match) break
|
||||||
|
|
||||||
|
const errLine = parseInt(match[1], 10) - 1
|
||||||
|
const lines = fixed.split("\n")
|
||||||
|
|
||||||
|
let cellStart = errLine
|
||||||
|
let cellEnd = errLine
|
||||||
|
|
||||||
|
while (cellStart > 0 && !lines[cellStart].includes("<mxCell")) {
|
||||||
|
cellStart--
|
||||||
|
}
|
||||||
|
|
||||||
|
while (cellEnd < lines.length - 1) {
|
||||||
|
if (
|
||||||
|
lines[cellEnd].includes("</mxCell>") ||
|
||||||
|
lines[cellEnd].trim().endsWith("/>")
|
||||||
|
) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
cellEnd++
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.splice(cellStart, cellEnd - cellStart + 1)
|
||||||
|
fixed = lines.join("\n")
|
||||||
|
droppedCells++
|
||||||
|
}
|
||||||
|
if (droppedCells > 0) {
|
||||||
|
fixes.push(`Dropped ${droppedCells} unfixable mxCell element(s)`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { fixed, fixes }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Combined Validation and Fix
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates XML and attempts to fix if invalid
|
||||||
|
* @param xml - The XML string to validate and potentially fix
|
||||||
|
* @returns Object with validation result, fixed XML if applicable, and fixes applied
|
||||||
|
*/
|
||||||
|
export function validateAndFixXml(xml: string): {
|
||||||
|
valid: boolean
|
||||||
|
error: string | null
|
||||||
|
fixed: string | null
|
||||||
|
fixes: string[]
|
||||||
|
} {
|
||||||
|
// First validation attempt
|
||||||
|
let error = validateMxCellStructure(xml)
|
||||||
|
|
||||||
|
if (!error) {
|
||||||
|
return { valid: true, error: null, fixed: null, fixes: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to fix
|
||||||
|
const { fixed, fixes } = autoFixXml(xml)
|
||||||
|
|
||||||
|
// Validate the fixed version
|
||||||
|
error = validateMxCellStructure(fixed)
|
||||||
|
|
||||||
|
if (!error) {
|
||||||
|
return { valid: true, error: null, fixed, fixes }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Still invalid after fixes
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error,
|
||||||
|
fixed: fixes.length > 0 ? fixed : null,
|
||||||
|
fixes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if mxCell XML output is complete (not truncated).
|
||||||
|
* @param xml - The XML string to check (can be undefined/null)
|
||||||
|
* @returns true if XML appears complete, false if truncated or empty
|
||||||
|
*/
|
||||||
|
export function isMxCellXmlComplete(xml: string | undefined | null): boolean {
|
||||||
|
let trimmed = xml?.trim() || ""
|
||||||
|
if (!trimmed) return false
|
||||||
|
|
||||||
|
// Strip wrapper tags if present
|
||||||
|
let prev = ""
|
||||||
|
while (prev !== trimmed) {
|
||||||
|
prev = trimmed
|
||||||
|
trimmed = trimmed
|
||||||
|
.replace(/<\/mxParameter>\s*$/i, "")
|
||||||
|
.replace(/<\/invoke>\s*$/i, "")
|
||||||
|
.replace(/<\/antml:parameter>\s*$/i, "")
|
||||||
|
.replace(/<\/antml:invoke>\s*$/i, "")
|
||||||
|
.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
return trimmed.endsWith("/>") || trimmed.endsWith("</mxCell>")
|
||||||
|
}
|
||||||
19
packages/mcp-server/tsconfig.json
Normal file
19
packages/mcp-server/tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "Node16",
|
||||||
|
"moduleResolution": "Node16",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"resolveJsonModule": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
63
proxy.ts
Normal file
63
proxy.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
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).*)"],
|
||||||
|
}
|
||||||
BIN
public/favicon-192x192.png
Normal file
BIN
public/favicon-192x192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
public/favicon-512x512.png
Normal file
BIN
public/favicon-512x512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
375
scripts/test-diagram-operations.mjs
Normal file
375
scripts/test-diagram-operations.mjs
Normal file
@@ -0,0 +1,375 @@
|
|||||||
|
/**
|
||||||
|
* Simple test script for applyDiagramOperations function
|
||||||
|
* Run with: node scripts/test-diagram-operations.mjs
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { JSDOM } from "jsdom"
|
||||||
|
|
||||||
|
// Set up DOMParser for Node.js environment
|
||||||
|
const dom = new JSDOM()
|
||||||
|
globalThis.DOMParser = dom.window.DOMParser
|
||||||
|
globalThis.XMLSerializer = dom.window.XMLSerializer
|
||||||
|
|
||||||
|
// Import the function (we'll inline it since it's not ESM exported)
|
||||||
|
function applyDiagramOperations(xmlContent, operations) {
|
||||||
|
const errors = []
|
||||||
|
const parser = new DOMParser()
|
||||||
|
const doc = parser.parseFromString(xmlContent, "text/xml")
|
||||||
|
|
||||||
|
const parseError = doc.querySelector("parsererror")
|
||||||
|
if (parseError) {
|
||||||
|
return {
|
||||||
|
result: xmlContent,
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
type: "update",
|
||||||
|
cellId: "",
|
||||||
|
message: `XML parse error: ${parseError.textContent}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = doc.querySelector("root")
|
||||||
|
if (!root) {
|
||||||
|
return {
|
||||||
|
result: xmlContent,
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
type: "update",
|
||||||
|
cellId: "",
|
||||||
|
message: "Could not find <root> element in XML",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cellMap = new Map()
|
||||||
|
root.querySelectorAll("mxCell").forEach((cell) => {
|
||||||
|
const id = cell.getAttribute("id")
|
||||||
|
if (id) cellMap.set(id, cell)
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const op of operations) {
|
||||||
|
if (op.type === "update") {
|
||||||
|
const existingCell = cellMap.get(op.cell_id)
|
||||||
|
if (!existingCell) {
|
||||||
|
errors.push({
|
||||||
|
type: "update",
|
||||||
|
cellId: op.cell_id,
|
||||||
|
message: `Cell with id="${op.cell_id}" not found`,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (!op.new_xml) {
|
||||||
|
errors.push({
|
||||||
|
type: "update",
|
||||||
|
cellId: op.cell_id,
|
||||||
|
message: "new_xml is required for update operation",
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const newDoc = parser.parseFromString(
|
||||||
|
`<wrapper>${op.new_xml}</wrapper>`,
|
||||||
|
"text/xml",
|
||||||
|
)
|
||||||
|
const newCell = newDoc.querySelector("mxCell")
|
||||||
|
if (!newCell) {
|
||||||
|
errors.push({
|
||||||
|
type: "update",
|
||||||
|
cellId: op.cell_id,
|
||||||
|
message: "new_xml must contain an mxCell element",
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const newCellId = newCell.getAttribute("id")
|
||||||
|
if (newCellId !== op.cell_id) {
|
||||||
|
errors.push({
|
||||||
|
type: "update",
|
||||||
|
cellId: op.cell_id,
|
||||||
|
message: `ID mismatch: cell_id is "${op.cell_id}" but new_xml has id="${newCellId}"`,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const importedNode = doc.importNode(newCell, true)
|
||||||
|
existingCell.parentNode?.replaceChild(importedNode, existingCell)
|
||||||
|
cellMap.set(op.cell_id, importedNode)
|
||||||
|
} else if (op.type === "add") {
|
||||||
|
if (cellMap.has(op.cell_id)) {
|
||||||
|
errors.push({
|
||||||
|
type: "add",
|
||||||
|
cellId: op.cell_id,
|
||||||
|
message: `Cell with id="${op.cell_id}" already exists`,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (!op.new_xml) {
|
||||||
|
errors.push({
|
||||||
|
type: "add",
|
||||||
|
cellId: op.cell_id,
|
||||||
|
message: "new_xml is required for add operation",
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const newDoc = parser.parseFromString(
|
||||||
|
`<wrapper>${op.new_xml}</wrapper>`,
|
||||||
|
"text/xml",
|
||||||
|
)
|
||||||
|
const newCell = newDoc.querySelector("mxCell")
|
||||||
|
if (!newCell) {
|
||||||
|
errors.push({
|
||||||
|
type: "add",
|
||||||
|
cellId: op.cell_id,
|
||||||
|
message: "new_xml must contain an mxCell element",
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const newCellId = newCell.getAttribute("id")
|
||||||
|
if (newCellId !== op.cell_id) {
|
||||||
|
errors.push({
|
||||||
|
type: "add",
|
||||||
|
cellId: op.cell_id,
|
||||||
|
message: `ID mismatch: cell_id is "${op.cell_id}" but new_xml has id="${newCellId}"`,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const importedNode = doc.importNode(newCell, true)
|
||||||
|
root.appendChild(importedNode)
|
||||||
|
cellMap.set(op.cell_id, importedNode)
|
||||||
|
} else if (op.type === "delete") {
|
||||||
|
const existingCell = cellMap.get(op.cell_id)
|
||||||
|
if (!existingCell) {
|
||||||
|
errors.push({
|
||||||
|
type: "delete",
|
||||||
|
cellId: op.cell_id,
|
||||||
|
message: `Cell with id="${op.cell_id}" not found`,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
existingCell.parentNode?.removeChild(existingCell)
|
||||||
|
cellMap.delete(op.cell_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const serializer = new XMLSerializer()
|
||||||
|
const result = serializer.serializeToString(doc)
|
||||||
|
return { result, errors }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test data
|
||||||
|
const sampleXml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<mxfile>
|
||||||
|
<diagram>
|
||||||
|
<mxGraphModel>
|
||||||
|
<root>
|
||||||
|
<mxCell id="0"/>
|
||||||
|
<mxCell id="1" parent="0"/>
|
||||||
|
<mxCell id="2" value="Box A" style="rounded=1;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="100" y="100" width="120" height="60" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="3" value="Box B" style="rounded=1;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="300" y="100" width="120" height="60" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="4" value="" style="edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="2" target="3">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
</root>
|
||||||
|
</mxGraphModel>
|
||||||
|
</diagram>
|
||||||
|
</mxfile>`
|
||||||
|
|
||||||
|
let passed = 0
|
||||||
|
let failed = 0
|
||||||
|
|
||||||
|
function test(name, fn) {
|
||||||
|
try {
|
||||||
|
fn()
|
||||||
|
console.log(`✓ ${name}`)
|
||||||
|
passed++
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`✗ ${name}`)
|
||||||
|
console.log(` Error: ${e.message}`)
|
||||||
|
failed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assert(condition, message) {
|
||||||
|
if (!condition) throw new Error(message || "Assertion failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests
|
||||||
|
test("Update operation changes cell value", () => {
|
||||||
|
const { result, errors } = applyDiagramOperations(sampleXml, [
|
||||||
|
{
|
||||||
|
type: "update",
|
||||||
|
cell_id: "2",
|
||||||
|
new_xml:
|
||||||
|
'<mxCell id="2" value="Updated Box A" style="rounded=1;" vertex="1" parent="1"><mxGeometry x="100" y="100" width="120" height="60" as="geometry"/></mxCell>',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
assert(
|
||||||
|
errors.length === 0,
|
||||||
|
`Expected no errors, got: ${JSON.stringify(errors)}`,
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
result.includes('value="Updated Box A"'),
|
||||||
|
"Updated value should be in result",
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
!result.includes('value="Box A"'),
|
||||||
|
"Old value should not be in result",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Update operation fails for non-existent cell", () => {
|
||||||
|
const { errors } = applyDiagramOperations(sampleXml, [
|
||||||
|
{
|
||||||
|
type: "update",
|
||||||
|
cell_id: "999",
|
||||||
|
new_xml: '<mxCell id="999" value="Test"/>',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
assert(errors.length === 1, "Should have one error")
|
||||||
|
assert(
|
||||||
|
errors[0].message.includes("not found"),
|
||||||
|
"Error should mention not found",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Update operation fails on ID mismatch", () => {
|
||||||
|
const { errors } = applyDiagramOperations(sampleXml, [
|
||||||
|
{
|
||||||
|
type: "update",
|
||||||
|
cell_id: "2",
|
||||||
|
new_xml: '<mxCell id="WRONG" value="Test"/>',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
assert(errors.length === 1, "Should have one error")
|
||||||
|
assert(
|
||||||
|
errors[0].message.includes("ID mismatch"),
|
||||||
|
"Error should mention ID mismatch",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Add operation creates new cell", () => {
|
||||||
|
const { result, errors } = applyDiagramOperations(sampleXml, [
|
||||||
|
{
|
||||||
|
type: "add",
|
||||||
|
cell_id: "new1",
|
||||||
|
new_xml:
|
||||||
|
'<mxCell id="new1" value="New Box" style="rounded=1;" vertex="1" parent="1"><mxGeometry x="500" y="100" width="120" height="60" as="geometry"/></mxCell>',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
assert(
|
||||||
|
errors.length === 0,
|
||||||
|
`Expected no errors, got: ${JSON.stringify(errors)}`,
|
||||||
|
)
|
||||||
|
assert(result.includes('id="new1"'), "New cell should be in result")
|
||||||
|
assert(
|
||||||
|
result.includes('value="New Box"'),
|
||||||
|
"New cell value should be in result",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Add operation fails for duplicate ID", () => {
|
||||||
|
const { errors } = applyDiagramOperations(sampleXml, [
|
||||||
|
{
|
||||||
|
type: "add",
|
||||||
|
cell_id: "2",
|
||||||
|
new_xml: '<mxCell id="2" value="Duplicate"/>',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
assert(errors.length === 1, "Should have one error")
|
||||||
|
assert(
|
||||||
|
errors[0].message.includes("already exists"),
|
||||||
|
"Error should mention already exists",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Add operation fails on ID mismatch", () => {
|
||||||
|
const { errors } = applyDiagramOperations(sampleXml, [
|
||||||
|
{
|
||||||
|
type: "add",
|
||||||
|
cell_id: "new1",
|
||||||
|
new_xml: '<mxCell id="WRONG" value="Test"/>',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
assert(errors.length === 1, "Should have one error")
|
||||||
|
assert(
|
||||||
|
errors[0].message.includes("ID mismatch"),
|
||||||
|
"Error should mention ID mismatch",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Delete operation removes cell", () => {
|
||||||
|
const { result, errors } = applyDiagramOperations(sampleXml, [
|
||||||
|
{ type: "delete", cell_id: "3" },
|
||||||
|
])
|
||||||
|
assert(
|
||||||
|
errors.length === 0,
|
||||||
|
`Expected no errors, got: ${JSON.stringify(errors)}`,
|
||||||
|
)
|
||||||
|
assert(!result.includes('id="3"'), "Deleted cell should not be in result")
|
||||||
|
assert(result.includes('id="2"'), "Other cells should remain")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Delete operation fails for non-existent cell", () => {
|
||||||
|
const { errors } = applyDiagramOperations(sampleXml, [
|
||||||
|
{ type: "delete", cell_id: "999" },
|
||||||
|
])
|
||||||
|
assert(errors.length === 1, "Should have one error")
|
||||||
|
assert(
|
||||||
|
errors[0].message.includes("not found"),
|
||||||
|
"Error should mention not found",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Multiple operations in sequence", () => {
|
||||||
|
const { result, errors } = applyDiagramOperations(sampleXml, [
|
||||||
|
{
|
||||||
|
type: "update",
|
||||||
|
cell_id: "2",
|
||||||
|
new_xml:
|
||||||
|
'<mxCell id="2" value="Updated" style="rounded=1;" vertex="1" parent="1"><mxGeometry x="100" y="100" width="120" height="60" as="geometry"/></mxCell>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "add",
|
||||||
|
cell_id: "new1",
|
||||||
|
new_xml:
|
||||||
|
'<mxCell id="new1" value="Added" style="rounded=1;" vertex="1" parent="1"><mxGeometry x="500" y="100" width="120" height="60" as="geometry"/></mxCell>',
|
||||||
|
},
|
||||||
|
{ type: "delete", cell_id: "3" },
|
||||||
|
])
|
||||||
|
assert(
|
||||||
|
errors.length === 0,
|
||||||
|
`Expected no errors, got: ${JSON.stringify(errors)}`,
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
result.includes('value="Updated"'),
|
||||||
|
"Updated value should be present",
|
||||||
|
)
|
||||||
|
assert(result.includes('id="new1"'), "Added cell should be present")
|
||||||
|
assert(!result.includes('id="3"'), "Deleted cell should not be present")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Invalid XML returns parse error", () => {
|
||||||
|
const { errors } = applyDiagramOperations("<not valid xml", [
|
||||||
|
{ type: "delete", cell_id: "1" },
|
||||||
|
])
|
||||||
|
assert(errors.length === 1, "Should have one error")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Missing root element returns error", () => {
|
||||||
|
const { errors } = applyDiagramOperations("<mxfile></mxfile>", [
|
||||||
|
{ type: "delete", cell_id: "1" },
|
||||||
|
])
|
||||||
|
assert(errors.length === 1, "Should have one error")
|
||||||
|
assert(
|
||||||
|
errors[0].message.includes("root"),
|
||||||
|
"Error should mention root element",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
console.log(`\n${passed} passed, ${failed} failed`)
|
||||||
|
process.exit(failed > 0 ? 1 : 0)
|
||||||
@@ -29,5 +29,5 @@
|
|||||||
".next/types/**/*.ts",
|
".next/types/**/*.ts",
|
||||||
".next/dev/types/**/*.ts"
|
".next/dev/types/**/*.ts"
|
||||||
],
|
],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules", "packages"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user