mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-02 22:32:27 +08:00
Compare commits
42 Commits
feature/mc
...
v0.4.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4fc259de8 | ||
|
|
28f9a81e7b | ||
|
|
0f67884ead | ||
|
|
3521495ead | ||
|
|
6446454cd7 | ||
|
|
84959637db | ||
|
|
9e9ea10beb | ||
|
|
deae5c2c38 | ||
|
|
6e2d98e52d | ||
|
|
85cb441e26 | ||
|
|
b088a0653e | ||
|
|
b25b944600 | ||
|
|
4f07a5fafc | ||
|
|
fc5eca877a | ||
|
|
f58274bb84 | ||
|
|
e03b65328d | ||
|
|
14c1aa8e1c | ||
|
|
9e651a51e6 | ||
|
|
2871265362 | ||
|
|
9d13bd7451 | ||
|
|
b97f3ccda9 | ||
|
|
864375b8e4 | ||
|
|
b9bc2a72c6 | ||
|
|
c215d80688 | ||
|
|
74b9e38114 | ||
|
|
68ea4958b8 | ||
|
|
938faff6b2 | ||
|
|
378bef435e | ||
|
|
f087b54ee4 | ||
|
|
6bb33eeda2 | ||
|
|
a91bd9d1e8 | ||
|
|
81eb71e704 | ||
|
|
58b6b19526 | ||
|
|
f65ef548b2 | ||
|
|
741a00db89 | ||
|
|
bcc6684ecb | ||
|
|
a9415d24e7 | ||
|
|
439bdd4577 | ||
|
|
98b890bb06 | ||
|
|
f039e4a3c8 | ||
|
|
7857858074 | ||
|
|
f0919117eb |
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
|
||||
9
.github/workflows/docker-build.yml
vendored
9
.github/workflows/docker-build.yml
vendored
@@ -63,6 +63,8 @@ jobs:
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: |
|
||||
NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE=true
|
||||
|
||||
# Push to AWS ECR for App Runner auto-deploy
|
||||
- name: Configure AWS credentials
|
||||
@@ -80,8 +82,11 @@ jobs:
|
||||
|
||||
- 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: |
|
||||
docker pull ghcr.io/${{ github.repository }}:latest
|
||||
docker tag ghcr.io/${{ github.repository }}:latest ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-northeast-1.amazonaws.com/next-ai-draw-io:latest
|
||||
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
|
||||
|
||||
|
||||
46
.github/workflows/electron-release.yml
vendored
Normal file
46
.github/workflows/electron-release.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: Electron Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version tag (e.g., v0.4.5)"
|
||||
required: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: macos-latest
|
||||
platform: mac
|
||||
- os: windows-latest
|
||||
platform: win
|
||||
- os: ubuntu-latest
|
||||
platform: linux
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: "npm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build and publish Electron app
|
||||
run: npm run dist:${{ matrix.platform }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
16
.gitignore
vendored
16
.gitignore
vendored
@@ -2,6 +2,8 @@
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
packages/*/node_modules
|
||||
packages/*/dist
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
@@ -47,3 +49,17 @@ push-via-ec2.sh
|
||||
.open-next/
|
||||
.wrangler/
|
||||
.env*.local
|
||||
|
||||
# Electron
|
||||
/dist-electron/
|
||||
/release/
|
||||
/electron-standalone/
|
||||
*.dmg
|
||||
*.exe
|
||||
*.AppImage
|
||||
*.deb
|
||||
*.rpm
|
||||
*.snap
|
||||
|
||||
CLAUDE.md
|
||||
.spec-workflow
|
||||
@@ -26,6 +26,10 @@ ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ARG NEXT_PUBLIC_DRAWIO_BASE_URL=https://embed.diagrams.net
|
||||
ENV NEXT_PUBLIC_DRAWIO_BASE_URL=${NEXT_PUBLIC_DRAWIO_BASE_URL}
|
||||
|
||||
# Build-time argument to show About link and Notice icon
|
||||
ARG NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE=false
|
||||
ENV NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE=${NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE}
|
||||
|
||||
# Build Next.js application (standalone mode)
|
||||
RUN npm run build
|
||||
|
||||
|
||||
54
README.md
54
README.md
@@ -30,8 +30,10 @@ https://github.com/user-attachments/assets/9d60a3e8-4a1c-4b5e-acbb-26af2d3eabd1
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Examples](#examples)
|
||||
- [Features](#features)
|
||||
- [MCP Server (Preview)](#mcp-server-preview)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Try it Online](#try-it-online)
|
||||
- [Desktop Application](#desktop-application)
|
||||
- [Run with Docker (Recommended)](#run-with-docker-recommended)
|
||||
- [Installation](#installation)
|
||||
- [Deployment](#deployment)
|
||||
@@ -92,6 +94,36 @@ Here are some example prompts and their generated diagrams:
|
||||
- **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
|
||||
|
||||
## 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
|
||||
|
||||
### Try it Online
|
||||
@@ -104,6 +136,28 @@ No installation needed! Try the app directly on our demo site:
|
||||
|
||||
> **Bring Your Own API Key**: You can use your own API key to bypass usage limits on the demo site. Click the Settings icon in the chat panel to configure your provider and API key. Your key is stored locally in your browser and is never stored on the server.
|
||||
|
||||
### Desktop Application
|
||||
|
||||
Download the native desktop app for your platform from the [Releases page](https://github.com/DayuanJiang/next-ai-draw-io/releases):
|
||||
|
||||
| Platform | Download |
|
||||
|----------|----------|
|
||||
| macOS | `.dmg` (Intel & Apple Silicon) |
|
||||
| Windows | `.exe` installer (x64 & ARM64) |
|
||||
| Linux | `.AppImage` or `.deb` (x64 & ARM64) |
|
||||
|
||||
**Features:**
|
||||
- **Secure API key storage**: Credentials encrypted using OS keychain
|
||||
- **Configuration presets**: Save and switch between AI providers via menu
|
||||
- **Native file dialogs**: Open/save `.drawio` files directly
|
||||
- **Offline capable**: Works without internet after first launch
|
||||
|
||||
**Quick Setup:**
|
||||
1. Download and install for your platform
|
||||
2. Open the app → **Menu → Configuration → Manage Presets**
|
||||
3. Add your AI provider credentials
|
||||
4. Start creating diagrams!
|
||||
|
||||
### Run with Docker (Recommended)
|
||||
|
||||
If you just want to run it locally, the best way is to use Docker.
|
||||
|
||||
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"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { useCallback, useEffect, useRef, useState } from "react"
|
||||
import { DrawIoEmbed } from "react-drawio"
|
||||
import type { ImperativePanelHandle } from "react-resizable-panels"
|
||||
import ChatPanel from "@/components/chat-panel"
|
||||
@@ -15,8 +15,15 @@ const drawioBaseUrl =
|
||||
process.env.NEXT_PUBLIC_DRAWIO_BASE_URL || "https://embed.diagrams.net"
|
||||
|
||||
export default function Home() {
|
||||
const { drawioRef, handleDiagramExport, onDrawioLoad, resetDrawioReady } =
|
||||
useDiagram()
|
||||
const {
|
||||
drawioRef,
|
||||
handleDiagramExport,
|
||||
onDrawioLoad,
|
||||
resetDrawioReady,
|
||||
saveDiagramToStorage,
|
||||
showSaveDialog,
|
||||
setShowSaveDialog,
|
||||
} = useDiagram()
|
||||
const [isMobile, setIsMobile] = useState(false)
|
||||
const [isChatVisible, setIsChatVisible] = useState(true)
|
||||
const [drawioUi, setDrawioUi] = useState<"min" | "sketch">("min")
|
||||
@@ -25,6 +32,29 @@ export default function Home() {
|
||||
const [closeProtection, setCloseProtection] = useState(false)
|
||||
|
||||
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
|
||||
useEffect(() => {
|
||||
@@ -35,12 +65,10 @@ export default function Home() {
|
||||
|
||||
const savedDarkMode = localStorage.getItem("next-ai-draw-io-dark-mode")
|
||||
if (savedDarkMode !== null) {
|
||||
// Use saved preference
|
||||
const isDark = savedDarkMode === "true"
|
||||
setDarkMode(isDark)
|
||||
document.documentElement.classList.toggle("dark", isDark)
|
||||
} else {
|
||||
// First visit: match browser preference
|
||||
const prefersDark = window.matchMedia(
|
||||
"(prefers-color-scheme: dark)",
|
||||
).matches
|
||||
@@ -58,25 +86,44 @@ export default function Home() {
|
||||
setIsLoaded(true)
|
||||
}, [])
|
||||
|
||||
const toggleDarkMode = () => {
|
||||
const handleDarkModeChange = async () => {
|
||||
await saveDiagramToStorage()
|
||||
const newValue = !darkMode
|
||||
setDarkMode(newValue)
|
||||
localStorage.setItem("next-ai-draw-io-dark-mode", String(newValue))
|
||||
document.documentElement.classList.toggle("dark", newValue)
|
||||
// Reset so onDrawioLoad fires again after remount
|
||||
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(() => {
|
||||
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()
|
||||
window.addEventListener("resize", checkMobile)
|
||||
return () => window.removeEventListener("resize", checkMobile)
|
||||
}, [])
|
||||
}, [saveDiagramToStorage, resetDrawioReady])
|
||||
|
||||
const toggleChatPanel = () => {
|
||||
const panel = chatPanelRef.current
|
||||
@@ -122,11 +169,9 @@ export default function Home() {
|
||||
<div className="h-screen bg-background relative overflow-hidden">
|
||||
<ResizablePanelGroup
|
||||
id="main-panel-group"
|
||||
key={isMobile ? "mobile" : "desktop"}
|
||||
direction={isMobile ? "vertical" : "horizontal"}
|
||||
className="h-full"
|
||||
>
|
||||
{/* Draw.io Canvas */}
|
||||
<ResizablePanel
|
||||
id="drawio-panel"
|
||||
defaultSize={isMobile ? 50 : 67}
|
||||
@@ -136,6 +181,12 @@ export default function Home() {
|
||||
className={`h-full relative ${
|
||||
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">
|
||||
{isLoaded ? (
|
||||
@@ -144,6 +195,7 @@ export default function Home() {
|
||||
ref={drawioRef}
|
||||
onExport={handleDiagramExport}
|
||||
onLoad={onDrawioLoad}
|
||||
onSave={handleDrawioSave}
|
||||
baseUrl={drawioBaseUrl}
|
||||
urlParameters={{
|
||||
ui: drawioUi,
|
||||
@@ -167,6 +219,7 @@ export default function Home() {
|
||||
|
||||
{/* Chat Panel */}
|
||||
<ResizablePanel
|
||||
key={isMobile ? "mobile" : "desktop"}
|
||||
id="chat-panel"
|
||||
ref={chatPanelRef}
|
||||
defaultSize={isMobile ? 50 : 33}
|
||||
@@ -182,15 +235,9 @@ export default function Home() {
|
||||
isVisible={isChatVisible}
|
||||
onToggleVisibility={toggleChatPanel}
|
||||
drawioUi={drawioUi}
|
||||
onToggleDrawioUi={() => {
|
||||
const newUi =
|
||||
drawioUi === "min" ? "sketch" : "min"
|
||||
localStorage.setItem("drawio-theme", newUi)
|
||||
setDrawioUi(newUi)
|
||||
resetDrawioReady()
|
||||
}}
|
||||
onToggleDrawioUi={handleDrawioUiChange}
|
||||
darkMode={darkMode}
|
||||
onToggleDarkMode={toggleDarkMode}
|
||||
onToggleDarkMode={handleDarkModeChange}
|
||||
isMobile={isMobile}
|
||||
onCloseProtectionChange={setCloseProtection}
|
||||
/>
|
||||
@@ -8,7 +8,9 @@ import {
|
||||
stepCountIs,
|
||||
streamText,
|
||||
} from "ai"
|
||||
import fs from "fs/promises"
|
||||
import { jsonrepair } from "jsonrepair"
|
||||
import path from "path"
|
||||
import { z } from "zod"
|
||||
import { getAIModel, supportsPromptCaching } from "@/lib/ai-providers"
|
||||
import { findCachedResponse } from "@/lib/cached-responses"
|
||||
@@ -212,6 +214,11 @@ async function handleChatRequest(req: Request): Promise<Response> {
|
||||
baseUrl: req.headers.get("x-ai-base-url"),
|
||||
apiKey: req.headers.get("x-ai-api-key"),
|
||||
modelId: req.headers.get("x-ai-model"),
|
||||
// AWS Bedrock credentials
|
||||
awsAccessKeyId: req.headers.get("x-aws-access-key-id"),
|
||||
awsSecretAccessKey: req.headers.get("x-aws-secret-access-key"),
|
||||
awsRegion: req.headers.get("x-aws-region"),
|
||||
awsSessionToken: req.headers.get("x-aws-session-token"),
|
||||
}
|
||||
|
||||
// Read minimal style preference from header
|
||||
@@ -596,6 +603,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 && {
|
||||
temperature: parseFloat(process.env.TEMPERATURE),
|
||||
|
||||
281
app/api/validate-model/route.ts
Normal file
281
app/api/validate-model/route.ts
Normal file
@@ -0,0 +1,281 @@
|
||||
import { createAmazonBedrock } from "@ai-sdk/amazon-bedrock"
|
||||
import { createAnthropic } from "@ai-sdk/anthropic"
|
||||
import { createDeepSeek, deepseek } from "@ai-sdk/deepseek"
|
||||
import { createGateway } from "@ai-sdk/gateway"
|
||||
import { createGoogleGenerativeAI } from "@ai-sdk/google"
|
||||
import { createOpenAI } from "@ai-sdk/openai"
|
||||
import { createOpenRouter } from "@openrouter/ai-sdk-provider"
|
||||
import { generateText } from "ai"
|
||||
import { NextResponse } from "next/server"
|
||||
import { createOllama } from "ollama-ai-provider-v2"
|
||||
|
||||
export const runtime = "nodejs"
|
||||
|
||||
/**
|
||||
* SECURITY: Check if URL points to private/internal network (SSRF protection)
|
||||
* Blocks: localhost, private IPs, link-local, AWS metadata service
|
||||
*/
|
||||
function isPrivateUrl(urlString: string): boolean {
|
||||
try {
|
||||
const url = new URL(urlString)
|
||||
const hostname = url.hostname.toLowerCase()
|
||||
|
||||
// Block localhost
|
||||
if (
|
||||
hostname === "localhost" ||
|
||||
hostname === "127.0.0.1" ||
|
||||
hostname === "::1"
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Block AWS/cloud metadata endpoints
|
||||
if (
|
||||
hostname === "169.254.169.254" ||
|
||||
hostname === "metadata.google.internal"
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for private IPv4 ranges
|
||||
const ipv4Match = hostname.match(
|
||||
/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/,
|
||||
)
|
||||
if (ipv4Match) {
|
||||
const [, a, b] = ipv4Match.map(Number)
|
||||
// 10.0.0.0/8
|
||||
if (a === 10) return true
|
||||
// 172.16.0.0/12
|
||||
if (a === 172 && b >= 16 && b <= 31) return true
|
||||
// 192.168.0.0/16
|
||||
if (a === 192 && b === 168) return true
|
||||
// 169.254.0.0/16 (link-local)
|
||||
if (a === 169 && b === 254) return true
|
||||
// 127.0.0.0/8 (loopback)
|
||||
if (a === 127) return true
|
||||
}
|
||||
|
||||
// Block common internal hostnames
|
||||
if (
|
||||
hostname.endsWith(".local") ||
|
||||
hostname.endsWith(".internal") ||
|
||||
hostname.endsWith(".localhost")
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
} catch {
|
||||
// Invalid URL - block it
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
interface ValidateRequest {
|
||||
provider: string
|
||||
apiKey: string
|
||||
baseUrl?: string
|
||||
modelId: string
|
||||
// AWS Bedrock specific
|
||||
awsAccessKeyId?: string
|
||||
awsSecretAccessKey?: string
|
||||
awsRegion?: string
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const body: ValidateRequest = await req.json()
|
||||
const {
|
||||
provider,
|
||||
apiKey,
|
||||
baseUrl,
|
||||
modelId,
|
||||
awsAccessKeyId,
|
||||
awsSecretAccessKey,
|
||||
awsRegion,
|
||||
} = body
|
||||
|
||||
if (!provider || !modelId) {
|
||||
return NextResponse.json(
|
||||
{ valid: false, error: "Provider and model ID are required" },
|
||||
{ status: 400 },
|
||||
)
|
||||
}
|
||||
|
||||
// SECURITY: Block SSRF attacks via custom baseUrl
|
||||
if (baseUrl && isPrivateUrl(baseUrl)) {
|
||||
return NextResponse.json(
|
||||
{ valid: false, error: "Invalid base URL" },
|
||||
{ status: 400 },
|
||||
)
|
||||
}
|
||||
|
||||
// Validate credentials based on provider
|
||||
if (provider === "bedrock") {
|
||||
if (!awsAccessKeyId || !awsSecretAccessKey || !awsRegion) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
valid: false,
|
||||
error: "AWS credentials (Access Key ID, Secret Access Key, Region) are required",
|
||||
},
|
||||
{ status: 400 },
|
||||
)
|
||||
}
|
||||
} else if (provider !== "ollama" && !apiKey) {
|
||||
return NextResponse.json(
|
||||
{ valid: false, error: "API key is required" },
|
||||
{ status: 400 },
|
||||
)
|
||||
}
|
||||
|
||||
let model: any
|
||||
|
||||
switch (provider) {
|
||||
case "openai": {
|
||||
const openai = createOpenAI({
|
||||
apiKey,
|
||||
...(baseUrl && { baseURL: baseUrl }),
|
||||
})
|
||||
model = openai.chat(modelId)
|
||||
break
|
||||
}
|
||||
|
||||
case "anthropic": {
|
||||
const anthropic = createAnthropic({
|
||||
apiKey,
|
||||
baseURL: baseUrl || "https://api.anthropic.com/v1",
|
||||
})
|
||||
model = anthropic(modelId)
|
||||
break
|
||||
}
|
||||
|
||||
case "google": {
|
||||
const google = createGoogleGenerativeAI({
|
||||
apiKey,
|
||||
...(baseUrl && { baseURL: baseUrl }),
|
||||
})
|
||||
model = google(modelId)
|
||||
break
|
||||
}
|
||||
|
||||
case "azure": {
|
||||
const azure = createOpenAI({
|
||||
apiKey,
|
||||
baseURL: baseUrl,
|
||||
})
|
||||
model = azure.chat(modelId)
|
||||
break
|
||||
}
|
||||
|
||||
case "bedrock": {
|
||||
const bedrock = createAmazonBedrock({
|
||||
accessKeyId: awsAccessKeyId,
|
||||
secretAccessKey: awsSecretAccessKey,
|
||||
region: awsRegion,
|
||||
})
|
||||
model = bedrock(modelId)
|
||||
break
|
||||
}
|
||||
|
||||
case "openrouter": {
|
||||
const openrouter = createOpenRouter({
|
||||
apiKey,
|
||||
...(baseUrl && { baseURL: baseUrl }),
|
||||
})
|
||||
model = openrouter(modelId)
|
||||
break
|
||||
}
|
||||
|
||||
case "deepseek": {
|
||||
if (baseUrl || apiKey) {
|
||||
const ds = createDeepSeek({
|
||||
apiKey,
|
||||
...(baseUrl && { baseURL: baseUrl }),
|
||||
})
|
||||
model = ds(modelId)
|
||||
} else {
|
||||
model = deepseek(modelId)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "siliconflow": {
|
||||
const sf = createOpenAI({
|
||||
apiKey,
|
||||
baseURL: baseUrl || "https://api.siliconflow.com/v1",
|
||||
})
|
||||
model = sf.chat(modelId)
|
||||
break
|
||||
}
|
||||
|
||||
case "ollama": {
|
||||
const ollama = createOllama({
|
||||
baseURL: baseUrl || "http://localhost:11434",
|
||||
})
|
||||
model = ollama(modelId)
|
||||
break
|
||||
}
|
||||
|
||||
case "gateway": {
|
||||
const gw = createGateway({
|
||||
apiKey,
|
||||
...(baseUrl && { baseURL: baseUrl }),
|
||||
})
|
||||
model = gw(modelId)
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
return NextResponse.json(
|
||||
{ valid: false, error: `Unknown provider: ${provider}` },
|
||||
{ status: 400 },
|
||||
)
|
||||
}
|
||||
|
||||
// Make a minimal test request
|
||||
const startTime = Date.now()
|
||||
await generateText({
|
||||
model,
|
||||
prompt: "Say 'OK'",
|
||||
maxOutputTokens: 20,
|
||||
})
|
||||
const responseTime = Date.now() - startTime
|
||||
|
||||
return NextResponse.json({
|
||||
valid: true,
|
||||
responseTime,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("[validate-model] Error:", error)
|
||||
|
||||
let errorMessage = "Validation failed"
|
||||
if (error instanceof Error) {
|
||||
// Extract meaningful error message
|
||||
if (
|
||||
error.message.includes("401") ||
|
||||
error.message.includes("Unauthorized")
|
||||
) {
|
||||
errorMessage = "Invalid API key"
|
||||
} else if (
|
||||
error.message.includes("404") ||
|
||||
error.message.includes("not found")
|
||||
) {
|
||||
errorMessage = "Model not found"
|
||||
} else if (
|
||||
error.message.includes("429") ||
|
||||
error.message.includes("rate limit")
|
||||
) {
|
||||
errorMessage = "Rate limited - try again later"
|
||||
} else if (error.message.includes("ECONNREFUSED")) {
|
||||
errorMessage = "Cannot connect to server"
|
||||
} else {
|
||||
errorMessage = error.message.slice(0, 100)
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ valid: false, error: errorMessage },
|
||||
{ status: 200 }, // Return 200 so client can read error message
|
||||
)
|
||||
}
|
||||
}
|
||||
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"
|
||||
import { getAssetUrl } from "@/lib/base-path"
|
||||
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: getAssetUrl("/"),
|
||||
display: "standalone",
|
||||
background_color: "#f9fafb",
|
||||
theme_color: "#171d26",
|
||||
icons: [
|
||||
{
|
||||
src: getAssetUrl("/favicon-192x192.png"),
|
||||
sizes: "192x192",
|
||||
type: "image/png",
|
||||
purpose: "any",
|
||||
},
|
||||
{
|
||||
src: getAssetUrl("/favicon-512x512.png"),
|
||||
sizes: "512x512",
|
||||
type: "image/png",
|
||||
purpose: "any",
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
156
components/ai-elements/model-selector.tsx
Normal file
156
components/ai-elements/model-selector.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import { Cloud } from "lucide-react"
|
||||
import type { ComponentProps, ReactNode } from "react"
|
||||
import {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
CommandShortcut,
|
||||
} from "@/components/ui/command"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export type ModelSelectorProps = ComponentProps<typeof Dialog>
|
||||
|
||||
export const ModelSelector = (props: ModelSelectorProps) => (
|
||||
<Dialog {...props} />
|
||||
)
|
||||
|
||||
export type ModelSelectorTriggerProps = ComponentProps<typeof DialogTrigger>
|
||||
|
||||
export const ModelSelectorTrigger = (props: ModelSelectorTriggerProps) => (
|
||||
<DialogTrigger {...props} />
|
||||
)
|
||||
|
||||
export type ModelSelectorContentProps = ComponentProps<typeof DialogContent> & {
|
||||
title?: ReactNode
|
||||
}
|
||||
|
||||
export const ModelSelectorContent = ({
|
||||
className,
|
||||
children,
|
||||
title = "Model Selector",
|
||||
...props
|
||||
}: ModelSelectorContentProps) => (
|
||||
<DialogContent className={cn("p-0", className)} {...props}>
|
||||
<DialogTitle className="sr-only">{title}</DialogTitle>
|
||||
<Command className="**:data-[slot=command-input-wrapper]:h-auto">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
)
|
||||
|
||||
export type ModelSelectorDialogProps = ComponentProps<typeof CommandDialog>
|
||||
|
||||
export const ModelSelectorDialog = (props: ModelSelectorDialogProps) => (
|
||||
<CommandDialog {...props} />
|
||||
)
|
||||
|
||||
export type ModelSelectorInputProps = ComponentProps<typeof CommandInput>
|
||||
|
||||
export const ModelSelectorInput = ({
|
||||
className,
|
||||
...props
|
||||
}: ModelSelectorInputProps) => (
|
||||
<CommandInput className={cn("h-auto py-3.5", className)} {...props} />
|
||||
)
|
||||
|
||||
export type ModelSelectorListProps = ComponentProps<typeof CommandList>
|
||||
|
||||
export const ModelSelectorList = (props: ModelSelectorListProps) => (
|
||||
<CommandList {...props} />
|
||||
)
|
||||
|
||||
export type ModelSelectorEmptyProps = ComponentProps<typeof CommandEmpty>
|
||||
|
||||
export const ModelSelectorEmpty = (props: ModelSelectorEmptyProps) => (
|
||||
<CommandEmpty {...props} />
|
||||
)
|
||||
|
||||
export type ModelSelectorGroupProps = ComponentProps<typeof CommandGroup>
|
||||
|
||||
export const ModelSelectorGroup = (props: ModelSelectorGroupProps) => (
|
||||
<CommandGroup {...props} />
|
||||
)
|
||||
|
||||
export type ModelSelectorItemProps = ComponentProps<typeof CommandItem>
|
||||
|
||||
export const ModelSelectorItem = (props: ModelSelectorItemProps) => (
|
||||
<CommandItem {...props} />
|
||||
)
|
||||
|
||||
export type ModelSelectorShortcutProps = ComponentProps<typeof CommandShortcut>
|
||||
|
||||
export const ModelSelectorShortcut = (props: ModelSelectorShortcutProps) => (
|
||||
<CommandShortcut {...props} />
|
||||
)
|
||||
|
||||
export type ModelSelectorSeparatorProps = ComponentProps<
|
||||
typeof CommandSeparator
|
||||
>
|
||||
|
||||
export const ModelSelectorSeparator = (props: ModelSelectorSeparatorProps) => (
|
||||
<CommandSeparator {...props} />
|
||||
)
|
||||
|
||||
export type ModelSelectorLogoProps = Omit<
|
||||
ComponentProps<"img">,
|
||||
"src" | "alt"
|
||||
> & {
|
||||
provider: string
|
||||
}
|
||||
|
||||
export const ModelSelectorLogo = ({
|
||||
provider,
|
||||
className,
|
||||
...props
|
||||
}: ModelSelectorLogoProps) => {
|
||||
// Use Lucide icon for bedrock since models.dev doesn't have a good AWS icon
|
||||
if (provider === "amazon-bedrock") {
|
||||
return <Cloud className={cn("size-4", className)} />
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
{...props}
|
||||
alt={`${provider} logo`}
|
||||
className={cn("size-4 dark:invert", className)}
|
||||
height={16}
|
||||
src={`https://models.dev/logos/${provider}.svg`}
|
||||
width={16}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export type ModelSelectorLogoGroupProps = ComponentProps<"div">
|
||||
|
||||
export const ModelSelectorLogoGroup = ({
|
||||
className,
|
||||
...props
|
||||
}: ModelSelectorLogoGroupProps) => (
|
||||
<div
|
||||
className={cn(
|
||||
"-space-x-1 flex shrink-0 items-center [&>img]:rounded-full [&>img]:bg-background [&>img]:p-px [&>img]:ring-1 dark:[&>img]:bg-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
export type ModelSelectorNameProps = ComponentProps<"span">
|
||||
|
||||
export const ModelSelectorName = ({
|
||||
className,
|
||||
...props
|
||||
}: ModelSelectorNameProps) => (
|
||||
<span className={cn("flex-1 truncate text-left", className)} {...props} />
|
||||
)
|
||||
@@ -1,6 +1,15 @@
|
||||
"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"
|
||||
import { getAssetUrl } from "@/lib/base-path"
|
||||
|
||||
interface ExampleCardProps {
|
||||
icon: React.ReactNode
|
||||
@@ -17,6 +26,8 @@ function ExampleCard({
|
||||
onClick,
|
||||
isNew,
|
||||
}: ExampleCardProps) {
|
||||
const dict = useDictionary()
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
@@ -43,7 +54,7 @@ function ExampleCard({
|
||||
</h3>
|
||||
{isNew && (
|
||||
<span className="px-1.5 py-0.5 text-[10px] font-semibold bg-primary text-primary-foreground rounded">
|
||||
NEW
|
||||
{dict.common.new}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -63,16 +74,18 @@ export default function ExamplePanel({
|
||||
setInput: (input: string) => void
|
||||
setFiles: (files: File[]) => void
|
||||
}) {
|
||||
const dict = useDictionary()
|
||||
|
||||
const handleReplicateFlowchart = async () => {
|
||||
setInput("Replicate this flowchart.")
|
||||
|
||||
try {
|
||||
const response = await fetch("/example.png")
|
||||
const response = await fetch(getAssetUrl("/example.png"))
|
||||
const blob = await response.blob()
|
||||
const file = new File([blob], "example.png", { type: "image/png" })
|
||||
setFiles([file])
|
||||
} catch (error) {
|
||||
console.error("Error loading example image:", error)
|
||||
console.error(dict.errors.failedToLoadExample, error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,14 +93,14 @@ export default function ExamplePanel({
|
||||
setInput("Replicate this in aws style")
|
||||
|
||||
try {
|
||||
const response = await fetch("/architecture.png")
|
||||
const response = await fetch(getAssetUrl("/architecture.png"))
|
||||
const blob = await response.blob()
|
||||
const file = new File([blob], "architecture.png", {
|
||||
type: "image/png",
|
||||
})
|
||||
setFiles([file])
|
||||
} catch (error) {
|
||||
console.error("Error loading architecture image:", error)
|
||||
console.error(dict.errors.failedToLoadExample, error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,49 +108,75 @@ export default function ExamplePanel({
|
||||
setInput("Summarize this paper as a diagram")
|
||||
|
||||
try {
|
||||
const response = await fetch("/chain-of-thought.txt")
|
||||
const response = await fetch(getAssetUrl("/chain-of-thought.txt"))
|
||||
const blob = await response.blob()
|
||||
const file = new File([blob], "chain-of-thought.txt", {
|
||||
type: "text/plain",
|
||||
})
|
||||
setFiles([file])
|
||||
} catch (error) {
|
||||
console.error("Error loading text file:", error)
|
||||
console.error(dict.errors.failedToLoadExample, error)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<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 */}
|
||||
<div className="text-center mb-6">
|
||||
<h2 className="text-lg font-semibold text-foreground mb-2">
|
||||
Create diagrams with AI
|
||||
{dict.examples.title}
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground max-w-xs mx-auto">
|
||||
Describe what you want to create or upload an image to
|
||||
replicate
|
||||
{dict.examples.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Examples grid */}
|
||||
<div className="space-y-3">
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wider px-1">
|
||||
Quick Examples
|
||||
{dict.examples.quickExamples}
|
||||
</p>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<ExampleCard
|
||||
icon={<FileText className="w-4 h-4 text-primary" />}
|
||||
title="Paper to Diagram"
|
||||
description="Upload .pdf, .txt, .md, .json, .csv, .py, .js, .ts and more"
|
||||
title={dict.examples.paperToDiagram}
|
||||
description={dict.examples.paperDescription}
|
||||
onClick={handlePdfExample}
|
||||
isNew
|
||||
/>
|
||||
|
||||
<ExampleCard
|
||||
icon={<Zap className="w-4 h-4 text-primary" />}
|
||||
title="Animated Diagram"
|
||||
description="Draw a transformer architecture with animated connectors"
|
||||
title={dict.examples.animatedDiagram}
|
||||
description={dict.examples.animatedDescription}
|
||||
onClick={() => {
|
||||
setInput(
|
||||
"Give me a **animated connector** diagram of transformer's architecture",
|
||||
@@ -148,22 +187,22 @@ export default function ExamplePanel({
|
||||
|
||||
<ExampleCard
|
||||
icon={<Cloud className="w-4 h-4 text-primary" />}
|
||||
title="AWS Architecture"
|
||||
description="Create a cloud architecture diagram with AWS icons"
|
||||
title={dict.examples.awsArchitecture}
|
||||
description={dict.examples.awsDescription}
|
||||
onClick={handleReplicateArchitecture}
|
||||
/>
|
||||
|
||||
<ExampleCard
|
||||
icon={<GitBranch className="w-4 h-4 text-primary" />}
|
||||
title="Replicate Flowchart"
|
||||
description="Upload and replicate an existing flowchart"
|
||||
title={dict.examples.replicateFlowchart}
|
||||
description={dict.examples.replicateDescription}
|
||||
onClick={handleReplicateFlowchart}
|
||||
/>
|
||||
|
||||
<ExampleCard
|
||||
icon={<Palette className="w-4 h-4 text-primary" />}
|
||||
title="Creative Drawing"
|
||||
description="Draw something fun and creative"
|
||||
title={dict.examples.creativeDrawing}
|
||||
description={dict.examples.creativeDescription}
|
||||
onClick={() => {
|
||||
setInput("Draw a cat for me")
|
||||
setFiles([])
|
||||
@@ -172,7 +211,7 @@ export default function ExamplePanel({
|
||||
</div>
|
||||
|
||||
<p className="text-[11px] text-muted-foreground/60 text-center mt-4">
|
||||
Examples are cached for instant response
|
||||
{dict.examples.cachedNote}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { toast } from "sonner"
|
||||
import { ButtonWithTooltip } from "@/components/button-with-tooltip"
|
||||
import { ErrorToast } from "@/components/error-toast"
|
||||
import { HistoryDialog } from "@/components/history-dialog"
|
||||
import { ModelSelector } from "@/components/model-selector"
|
||||
import { ResetWarningModal } from "@/components/reset-warning-modal"
|
||||
import { SaveDialog } from "@/components/save-dialog"
|
||||
import { Button } from "@/components/ui/button"
|
||||
@@ -25,7 +26,10 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
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 type { FlattenedModel } from "@/lib/types/model-config"
|
||||
import { FilePreviewList } from "./file-preview-list"
|
||||
|
||||
const MAX_IMAGE_SIZE = 2 * 1024 * 1024 // 2MB
|
||||
@@ -58,6 +62,7 @@ interface ValidationResult {
|
||||
function validateFiles(
|
||||
newFiles: File[],
|
||||
existingCount: number,
|
||||
dict: any,
|
||||
): ValidationResult {
|
||||
const errors: string[] = []
|
||||
const validFiles: File[] = []
|
||||
@@ -65,17 +70,23 @@ function validateFiles(
|
||||
const availableSlots = MAX_FILES - existingCount
|
||||
|
||||
if (availableSlots <= 0) {
|
||||
errors.push(`Maximum ${MAX_FILES} files allowed`)
|
||||
errors.push(formatMessage(dict.errors.maxFiles, { max: MAX_FILES }))
|
||||
return { validFiles, errors }
|
||||
}
|
||||
|
||||
for (const file of newFiles) {
|
||||
if (validFiles.length >= availableSlots) {
|
||||
errors.push(`Only ${availableSlots} more file(s) allowed`)
|
||||
errors.push(
|
||||
formatMessage(dict.errors.onlyMoreAllowed, {
|
||||
slots: availableSlots,
|
||||
}),
|
||||
)
|
||||
break
|
||||
}
|
||||
if (!isValidFileType(file)) {
|
||||
errors.push(`"${file.name}" is not a supported file type`)
|
||||
errors.push(
|
||||
formatMessage(dict.errors.unsupportedType, { name: file.name }),
|
||||
)
|
||||
continue
|
||||
}
|
||||
// Only check size for images (PDFs/text files are extracted client-side, so file size doesn't matter)
|
||||
@@ -83,7 +94,11 @@ function validateFiles(
|
||||
if (!isExtractedFile && file.size > MAX_IMAGE_SIZE) {
|
||||
const maxSizeMB = MAX_IMAGE_SIZE / 1024 / 1024
|
||||
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 {
|
||||
validFiles.push(file)
|
||||
@@ -93,7 +108,7 @@ function validateFiles(
|
||||
return { validFiles, errors }
|
||||
}
|
||||
|
||||
function showValidationErrors(errors: string[]) {
|
||||
function showValidationErrors(errors: string[], dict: any) {
|
||||
if (errors.length === 0) return
|
||||
|
||||
if (errors.length === 1) {
|
||||
@@ -104,14 +119,20 @@ function showValidationErrors(errors: string[]) {
|
||||
showErrorToast(
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="font-medium">
|
||||
{errors.length} files rejected:
|
||||
{formatMessage(dict.errors.filesRejected, {
|
||||
count: errors.length,
|
||||
})}
|
||||
</span>
|
||||
<ul className="text-muted-foreground text-xs list-disc list-inside">
|
||||
{errors.slice(0, 3).map((err) => (
|
||||
<li key={err}>{err}</li>
|
||||
))}
|
||||
{errors.length > 3 && (
|
||||
<li>...and {errors.length - 3} more</li>
|
||||
<li>
|
||||
{formatMessage(dict.errors.andMore, {
|
||||
count: errors.length - 3,
|
||||
})}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>,
|
||||
@@ -137,6 +158,11 @@ interface ChatInputProps {
|
||||
error?: Error | null
|
||||
minimalStyle?: boolean
|
||||
onMinimalStyleChange?: (value: boolean) => void
|
||||
// Model selector props
|
||||
models?: FlattenedModel[]
|
||||
selectedModelId?: string
|
||||
onModelSelect?: (modelId: string | undefined) => void
|
||||
onConfigureModels?: () => void
|
||||
}
|
||||
|
||||
export function ChatInput({
|
||||
@@ -154,14 +180,22 @@ export function ChatInput({
|
||||
error = null,
|
||||
minimalStyle = false,
|
||||
onMinimalStyleChange = () => {},
|
||||
models = [],
|
||||
selectedModelId,
|
||||
onModelSelect = () => {},
|
||||
onConfigureModels = () => {},
|
||||
}: ChatInputProps) {
|
||||
const { diagramHistory, saveDiagramToFile } = useDiagram()
|
||||
const dict = useDictionary()
|
||||
const {
|
||||
diagramHistory,
|
||||
saveDiagramToFile,
|
||||
showSaveDialog,
|
||||
setShowSaveDialog,
|
||||
} = useDiagram()
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const [isDragging, setIsDragging] = 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")
|
||||
const isDisabled =
|
||||
(status === "streaming" || status === "submitted") && !error
|
||||
@@ -173,7 +207,6 @@ export function ChatInput({
|
||||
textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Handle programmatic input changes (e.g., setInput("") after form submission)
|
||||
useEffect(() => {
|
||||
adjustTextareaHeight()
|
||||
@@ -220,8 +253,9 @@ export function ChatInput({
|
||||
const { validFiles, errors } = validateFiles(
|
||||
imageFiles,
|
||||
files.length,
|
||||
dict,
|
||||
)
|
||||
showValidationErrors(errors)
|
||||
showValidationErrors(errors, dict)
|
||||
if (validFiles.length > 0) {
|
||||
onFileChange([...files, ...validFiles])
|
||||
}
|
||||
@@ -230,12 +264,16 @@ export function ChatInput({
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newFiles = Array.from(e.target.files || [])
|
||||
const { validFiles, errors } = validateFiles(newFiles, files.length)
|
||||
showValidationErrors(errors)
|
||||
const { validFiles, errors } = validateFiles(
|
||||
newFiles,
|
||||
files.length,
|
||||
dict,
|
||||
)
|
||||
showValidationErrors(errors, dict)
|
||||
if (validFiles.length > 0) {
|
||||
onFileChange([...files, ...validFiles])
|
||||
}
|
||||
// Reset input so same file can be selected again
|
||||
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = ""
|
||||
}
|
||||
@@ -279,8 +317,9 @@ export function ChatInput({
|
||||
const { validFiles, errors } = validateFiles(
|
||||
supportedFiles,
|
||||
files.length,
|
||||
dict,
|
||||
)
|
||||
showValidationErrors(errors)
|
||||
showValidationErrors(errors, dict)
|
||||
if (validFiles.length > 0) {
|
||||
onFileChange([...files, ...validFiles])
|
||||
}
|
||||
@@ -313,8 +352,6 @@ export function ChatInput({
|
||||
/>
|
||||
</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">
|
||||
<Textarea
|
||||
ref={textareaRef}
|
||||
@@ -322,22 +359,20 @@ export function ChatInput({
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
onPaste={handlePaste}
|
||||
placeholder="Describe your diagram or upload a file..."
|
||||
placeholder={dict.chat.placeholder}
|
||||
disabled={isDisabled}
|
||||
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"
|
||||
/>
|
||||
|
||||
{/* Action bar */}
|
||||
<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">
|
||||
<div className="flex items-center gap-1 overflow-x-hidden">
|
||||
<ButtonWithTooltip
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
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"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
@@ -371,25 +406,26 @@ export function ChatInput({
|
||||
: "text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
{minimalStyle ? "Minimal" : "Styled"}
|
||||
{minimalStyle
|
||||
? dict.chat.minimalStyle
|
||||
: dict.chat.styledMode}
|
||||
</label>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
Use minimal for faster generation (no colors)
|
||||
{dict.chat.minimalTooltip}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{/* Right actions */}
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex items-center gap-1 overflow-hidden justify-end">
|
||||
<ButtonWithTooltip
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onToggleHistory(true)}
|
||||
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"
|
||||
>
|
||||
<History className="h-4 w-4" />
|
||||
@@ -401,7 +437,7 @@ export function ChatInput({
|
||||
size="sm"
|
||||
onClick={() => setShowSaveDialog(true)}
|
||||
disabled={isDisabled}
|
||||
tooltipContent="Save diagram"
|
||||
tooltipContent={dict.chat.saveDiagram}
|
||||
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
@@ -424,7 +460,7 @@ export function ChatInput({
|
||||
size="sm"
|
||||
onClick={triggerFileInput}
|
||||
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"
|
||||
>
|
||||
<ImageIcon className="h-4 w-4" />
|
||||
@@ -440,6 +476,14 @@ export function ChatInput({
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
|
||||
<ModelSelector
|
||||
models={models}
|
||||
selectedModelId={selectedModelId}
|
||||
onSelect={onModelSelect}
|
||||
onConfigure={onConfigureModels}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
|
||||
<div className="w-px h-5 bg-border mx-1" />
|
||||
|
||||
<Button
|
||||
@@ -448,7 +492,7 @@ export function ChatInput({
|
||||
size="sm"
|
||||
className="h-8 px-4 rounded-xl font-medium shadow-sm"
|
||||
aria-label={
|
||||
isDisabled ? "Sending..." : "Send message"
|
||||
isDisabled ? dict.chat.sending : dict.chat.send
|
||||
}
|
||||
>
|
||||
{isDisabled ? (
|
||||
@@ -456,7 +500,7 @@ export function ChatInput({
|
||||
) : (
|
||||
<>
|
||||
<Send className="h-4 w-4 mr-1.5" />
|
||||
Send
|
||||
{dict.chat.send}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
ReasoningTrigger,
|
||||
} from "@/components/ai-elements/reasoning"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import { getApiEndpoint } from "@/lib/base-path"
|
||||
import {
|
||||
applyDiagramOperations,
|
||||
convertToLegalXml,
|
||||
@@ -291,7 +292,7 @@ export function ChatMessageDisplay({
|
||||
setFeedback((prev) => ({ ...prev, [messageId]: value }))
|
||||
|
||||
try {
|
||||
await fetch("/api/log-feedback", {
|
||||
await fetch(getApiEndpoint("/api/log-feedback"), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
@@ -634,6 +635,8 @@ export function ChatMessageDisplay({
|
||||
return "Generate Diagram"
|
||||
case "edit_diagram":
|
||||
return "Edit Diagram"
|
||||
case "get_shape_library":
|
||||
return "Get Shape Library"
|
||||
default:
|
||||
return name
|
||||
}
|
||||
@@ -728,6 +731,25 @@ export function ChatMessageDisplay({
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -18,10 +18,18 @@ import { FaGithub } from "react-icons/fa"
|
||||
import { Toaster, toast } from "sonner"
|
||||
import { ButtonWithTooltip } from "@/components/button-with-tooltip"
|
||||
import { ChatInput } from "@/components/chat-input"
|
||||
import { ModelConfigDialog } from "@/components/model-config-dialog"
|
||||
import { ResetWarningModal } from "@/components/reset-warning-modal"
|
||||
import { SettingsDialog } from "@/components/settings-dialog"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
import { useDiagram } from "@/contexts/diagram-context"
|
||||
import { getAIConfig } from "@/lib/ai-config"
|
||||
import { useDictionary } from "@/hooks/use-dictionary"
|
||||
import { getSelectedAIConfig, useModelConfig } from "@/hooks/use-model-config"
|
||||
import { getApiEndpoint } from "@/lib/base-path"
|
||||
import { findCachedResponse } from "@/lib/cached-responses"
|
||||
import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
|
||||
import { type FileData, useFileProcessor } from "@/lib/use-file-processor"
|
||||
@@ -35,6 +43,9 @@ const STORAGE_XML_SNAPSHOTS_KEY = "next-ai-draw-io-xml-snapshots"
|
||||
const STORAGE_SESSION_ID_KEY = "next-ai-draw-io-session-id"
|
||||
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)
|
||||
interface MessagePart {
|
||||
type: string
|
||||
@@ -106,9 +117,10 @@ export default function ChatPanel({
|
||||
resolverRef,
|
||||
chartXML,
|
||||
clearDiagram,
|
||||
isDrawioReady,
|
||||
} = useDiagram()
|
||||
|
||||
const dict = useDictionary()
|
||||
|
||||
const onFetchChart = (saveToHistory = true) => {
|
||||
return Promise.race([
|
||||
new Promise<string>((resolve) => {
|
||||
@@ -140,7 +152,10 @@ export default function ChatPanel({
|
||||
|
||||
const [showHistory, setShowHistory] = useState(false)
|
||||
const [showSettingsDialog, setShowSettingsDialog] = useState(false)
|
||||
const [, setAccessCodeRequired] = useState(false)
|
||||
const [showModelConfigDialog, setShowModelConfigDialog] = useState(false)
|
||||
|
||||
// Model configuration hook
|
||||
const modelConfig = useModelConfig()
|
||||
const [input, setInput] = useState("")
|
||||
const [dailyRequestLimit, setDailyRequestLimit] = useState(0)
|
||||
const [dailyTokenLimit, setDailyTokenLimit] = useState(0)
|
||||
@@ -148,17 +163,24 @@ export default function ChatPanel({
|
||||
const [showNewChatDialog, setShowNewChatDialog] = 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
|
||||
useEffect(() => {
|
||||
fetch("/api/config")
|
||||
fetch(getApiEndpoint("/api/config"))
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
setAccessCodeRequired(data.accessCodeRequired)
|
||||
setDailyRequestLimit(data.dailyRequestLimit || 0)
|
||||
setDailyTokenLimit(data.dailyTokenLimit || 0)
|
||||
setTpmLimit(data.tpmLimit || 0)
|
||||
})
|
||||
.catch(() => setAccessCodeRequired(false))
|
||||
.catch(() => {})
|
||||
}, [])
|
||||
|
||||
// Quota management using extracted hook
|
||||
@@ -210,9 +232,6 @@ export default function ChatPanel({
|
||||
const localStorageDebounceRef = useRef<ReturnType<
|
||||
typeof setTimeout
|
||||
> | null>(null)
|
||||
const xmlStorageDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(
|
||||
null,
|
||||
)
|
||||
const LOCAL_STORAGE_DEBOUNCE_MS = 1000 // Save at most once per second
|
||||
|
||||
const {
|
||||
@@ -225,7 +244,7 @@ export default function ChatPanel({
|
||||
setMessages,
|
||||
} = useChat({
|
||||
transport: new DefaultChatTransport({
|
||||
api: "/api/chat",
|
||||
api: getApiEndpoint("/api/chat"),
|
||||
}),
|
||||
async onToolCall({ toolCall }) {
|
||||
if (DEBUG) {
|
||||
@@ -598,8 +617,7 @@ Continue from EXACTLY where you stopped.`,
|
||||
})
|
||||
|
||||
if (error.message.includes("Invalid or missing access code")) {
|
||||
// Show settings button and open dialog to help user fix it
|
||||
setAccessCodeRequired(true)
|
||||
// Show settings dialog to help user fix it
|
||||
setShowSettingsDialog(true)
|
||||
}
|
||||
},
|
||||
@@ -725,47 +743,6 @@ Continue from EXACTLY where you stopped.`,
|
||||
}
|
||||
}, [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)
|
||||
useEffect(() => {
|
||||
if (!hasRestoredRef.current) return
|
||||
@@ -795,28 +772,6 @@ Continue from EXACTLY where you stopped.`,
|
||||
}
|
||||
}, [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(() => {
|
||||
localStorage.setItem(STORAGE_DIAGRAM_XML_KEY, chartXML)
|
||||
}, LOCAL_STORAGE_DEBOUNCE_MS)
|
||||
|
||||
return () => {
|
||||
if (xmlStorageDebounceRef.current) {
|
||||
clearTimeout(xmlStorageDebounceRef.current)
|
||||
}
|
||||
}
|
||||
}, [chartXML, canSaveDiagram])
|
||||
|
||||
// Save XML snapshots to localStorage whenever they change
|
||||
const saveXmlSnapshots = useCallback(() => {
|
||||
try {
|
||||
@@ -916,6 +871,7 @@ Continue from EXACTLY where you stopped.`,
|
||||
},
|
||||
] as any)
|
||||
setInput("")
|
||||
sessionStorage.removeItem(SESSION_STORAGE_INPUT_KEY)
|
||||
setFiles([])
|
||||
return
|
||||
}
|
||||
@@ -963,6 +919,7 @@ Continue from EXACTLY where you stopped.`,
|
||||
|
||||
// Token count is tracked in onFinish with actual server usage
|
||||
setInput("")
|
||||
sessionStorage.removeItem(SESSION_STORAGE_INPUT_KEY)
|
||||
setFiles([])
|
||||
} catch (error) {
|
||||
console.error("Error fetching chart data:", error)
|
||||
@@ -985,6 +942,7 @@ Continue from EXACTLY where you stopped.`,
|
||||
localStorage.removeItem(STORAGE_XML_SNAPSHOTS_KEY)
|
||||
localStorage.removeItem(STORAGE_DIAGRAM_XML_KEY)
|
||||
localStorage.setItem(STORAGE_SESSION_ID_KEY, newSessionId)
|
||||
sessionStorage.removeItem(SESSION_STORAGE_INPUT_KEY)
|
||||
toast.success("Started a fresh chat")
|
||||
} catch (error) {
|
||||
console.error("Failed to clear localStorage:", error)
|
||||
@@ -999,9 +957,14 @@ Continue from EXACTLY where you stopped.`,
|
||||
const handleInputChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
) => {
|
||||
saveInputToSessionStorage(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)
|
||||
// Extract previous XML snapshot before a given message index
|
||||
const getPreviousXml = (beforeIndex: number): string => {
|
||||
@@ -1063,7 +1026,7 @@ Continue from EXACTLY where you stopped.`,
|
||||
autoRetryCountRef.current = 0
|
||||
partialXmlRef.current = ""
|
||||
|
||||
const config = getAIConfig()
|
||||
const config = getSelectedAIConfig()
|
||||
|
||||
sendMessage(
|
||||
{ parts },
|
||||
@@ -1080,6 +1043,20 @@ Continue from EXACTLY where you stopped.`,
|
||||
"x-ai-api-key": config.aiApiKey,
|
||||
}),
|
||||
...(config.aiModel && { "x-ai-model": config.aiModel }),
|
||||
// AWS Bedrock credentials
|
||||
...(config.awsAccessKeyId && {
|
||||
"x-aws-access-key-id": config.awsAccessKeyId,
|
||||
}),
|
||||
...(config.awsSecretAccessKey && {
|
||||
"x-aws-secret-access-key":
|
||||
config.awsSecretAccessKey,
|
||||
}),
|
||||
...(config.awsRegion && {
|
||||
"x-aws-region": config.awsRegion,
|
||||
}),
|
||||
...(config.awsSessionToken && {
|
||||
"x-aws-session-token": config.awsSessionToken,
|
||||
}),
|
||||
}),
|
||||
...(minimalStyle && {
|
||||
"x-minimal-style": "true",
|
||||
@@ -1277,14 +1254,14 @@ Continue from EXACTLY where you stopped.`,
|
||||
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 gap-2">
|
||||
<div className="flex items-center gap-2 overflow-x-hidden">
|
||||
<div className="flex items-center gap-2">
|
||||
<Image
|
||||
src="/favicon.ico"
|
||||
alt="Next AI Drawio"
|
||||
width={isMobile ? 24 : 28}
|
||||
height={isMobile ? 24 : 28}
|
||||
className="rounded"
|
||||
className="rounded flex-shrink-0"
|
||||
/>
|
||||
<h1
|
||||
className={`${isMobile ? "text-sm" : "text-base"} font-semibold tracking-tight whitespace-nowrap`}
|
||||
@@ -1292,36 +1269,40 @@ Continue from EXACTLY where you stopped.`,
|
||||
Next AI Drawio
|
||||
</h1>
|
||||
</div>
|
||||
{!isMobile && (
|
||||
<Link
|
||||
href="/about"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors ml-2"
|
||||
>
|
||||
About
|
||||
</Link>
|
||||
)}
|
||||
{!isMobile && (
|
||||
<Link
|
||||
href="/about"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<ButtonWithTooltip
|
||||
tooltipContent="Due to high usage, I have changed the model to minimax-m2 and added some usage limits. See About page for details."
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 text-amber-500 hover:text-amber-600"
|
||||
{!isMobile &&
|
||||
process.env.NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE ===
|
||||
"true" && (
|
||||
<Link
|
||||
href="/about"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors ml-2"
|
||||
>
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
</ButtonWithTooltip>
|
||||
</Link>
|
||||
)}
|
||||
About
|
||||
</Link>
|
||||
)}
|
||||
{!isMobile &&
|
||||
process.env.NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE ===
|
||||
"true" && (
|
||||
<Link
|
||||
href="/about"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<ButtonWithTooltip
|
||||
tooltipContent="Due to high usage, I have changed the model to minimax-m2 and added some usage limits. See About page for details."
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 text-amber-500 hover:text-amber-600"
|
||||
>
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
</ButtonWithTooltip>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex items-center gap-1 justify-end overflow-visible">
|
||||
<ButtonWithTooltip
|
||||
tooltipContent="Start fresh chat"
|
||||
tooltipContent={dict.nav.newChat}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setShowNewChatDialog(true)}
|
||||
@@ -1332,18 +1313,25 @@ Continue from EXACTLY where you stopped.`,
|
||||
/>
|
||||
</ButtonWithTooltip>
|
||||
<div className="w-px h-5 bg-border mx-1" />
|
||||
<a
|
||||
href="https://github.com/DayuanJiang/next-ai-draw-io"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-2 rounded-lg text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
|
||||
>
|
||||
<FaGithub
|
||||
className={`${isMobile ? "w-4 h-4" : "w-5 h-5"}`}
|
||||
/>
|
||||
</a>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<a
|
||||
href="https://github.com/DayuanJiang/next-ai-draw-io"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center justify-center h-9 w-9 rounded-md text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
|
||||
>
|
||||
<FaGithub
|
||||
className={`${isMobile ? "w-4 h-4" : "w-5 h-5"}`}
|
||||
/>
|
||||
</a>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{dict.nav.github}</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<ButtonWithTooltip
|
||||
tooltipContent="Settings"
|
||||
tooltipContent={dict.nav.settings}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setShowSettingsDialog(true)}
|
||||
@@ -1353,17 +1341,19 @@ Continue from EXACTLY where you stopped.`,
|
||||
className={`${isMobile ? "h-4 w-4" : "h-5 w-5"} text-muted-foreground`}
|
||||
/>
|
||||
</ButtonWithTooltip>
|
||||
{!isMobile && (
|
||||
<ButtonWithTooltip
|
||||
tooltipContent="Hide chat panel (Ctrl+B)"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onToggleVisibility}
|
||||
className="hover:bg-accent"
|
||||
>
|
||||
<PanelRightClose className="h-5 w-5 text-muted-foreground" />
|
||||
</ButtonWithTooltip>
|
||||
)}
|
||||
<div className="hidden sm:flex items-center gap-2">
|
||||
{!isMobile && (
|
||||
<ButtonWithTooltip
|
||||
tooltipContent={dict.nav.hidePanel}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="hover:bg-accent"
|
||||
onClick={onToggleVisibility}
|
||||
>
|
||||
<PanelRightClose className="h-5 w-5 text-muted-foreground" />
|
||||
</ButtonWithTooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -1402,6 +1392,10 @@ Continue from EXACTLY where you stopped.`,
|
||||
error={error}
|
||||
minimalStyle={minimalStyle}
|
||||
onMinimalStyleChange={setMinimalStyle}
|
||||
models={modelConfig.models}
|
||||
selectedModelId={modelConfig.selectedModelId}
|
||||
onModelSelect={modelConfig.setSelectedModelId}
|
||||
onConfigureModels={() => setShowModelConfigDialog(true)}
|
||||
/>
|
||||
</footer>
|
||||
|
||||
@@ -1415,6 +1409,12 @@ Continue from EXACTLY where you stopped.`,
|
||||
onToggleDarkMode={onToggleDarkMode}
|
||||
/>
|
||||
|
||||
<ModelConfigDialog
|
||||
open={showModelConfigDialog}
|
||||
onOpenChange={setShowModelConfigDialog}
|
||||
modelConfig={modelConfig}
|
||||
/>
|
||||
|
||||
<ResetWarningModal
|
||||
open={showNewChatDialog}
|
||||
onOpenChange={setShowNewChatDialog}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { FileCode, FileText, Loader2, X } from "lucide-react"
|
||||
import Image from "next/image"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { useDictionary } from "@/hooks/use-dictionary"
|
||||
import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
|
||||
|
||||
function formatCharCount(count: number): string {
|
||||
@@ -26,10 +27,10 @@ export function FilePreviewList({
|
||||
onRemoveFile,
|
||||
pdfData = new Map(),
|
||||
}: FilePreviewListProps) {
|
||||
const dict = useDictionary()
|
||||
const [selectedImage, setSelectedImage] = useState<string | null>(null)
|
||||
const [imageUrls, setImageUrls] = useState<Map<File, string>>(new Map())
|
||||
const imageUrlsRef = useRef<Map<File, string>>(new Map())
|
||||
|
||||
// Create and cleanup object URLs when files change
|
||||
useEffect(() => {
|
||||
const currentUrls = imageUrlsRef.current
|
||||
@@ -46,7 +47,6 @@ export function FilePreviewList({
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Revoke URLs for files that are no longer in the list
|
||||
currentUrls.forEach((url, file) => {
|
||||
if (!newUrls.has(file)) {
|
||||
@@ -57,7 +57,6 @@ export function FilePreviewList({
|
||||
imageUrlsRef.current = newUrls
|
||||
setImageUrls(newUrls)
|
||||
}, [files])
|
||||
|
||||
// Cleanup all URLs on unmount only
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -68,7 +67,6 @@ export function FilePreviewList({
|
||||
imageUrlsRef.current = new Map()
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Clear selected image if its URL was revoked
|
||||
useEffect(() => {
|
||||
if (
|
||||
@@ -126,14 +124,14 @@ export function FilePreviewList({
|
||||
</span>
|
||||
{pdfInfo?.isExtracting ? (
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
Reading...
|
||||
{dict.file.reading}
|
||||
</span>
|
||||
) : pdfInfo?.charCount ? (
|
||||
<span className="text-[10px] text-green-600 font-medium">
|
||||
{formatCharCount(
|
||||
pdfInfo.charCount,
|
||||
)}{" "}
|
||||
chars
|
||||
{dict.file.chars}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -147,7 +145,7 @@ export function FilePreviewList({
|
||||
type="button"
|
||||
onClick={() => onRemoveFile(file)}
|
||||
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" />
|
||||
</button>
|
||||
@@ -155,7 +153,6 @@ export function FilePreviewList({
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Image Modal/Lightbox */}
|
||||
{selectedImage && (
|
||||
<div
|
||||
@@ -165,7 +162,7 @@ export function FilePreviewList({
|
||||
<button
|
||||
className="absolute top-4 right-4 z-10 bg-white rounded-full p-2 hover:bg-gray-200 transition-colors"
|
||||
onClick={() => setSelectedImage(null)}
|
||||
aria-label="Close"
|
||||
aria-label={dict.common.close}
|
||||
>
|
||||
<X className="h-6 w-6" />
|
||||
</button>
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import { useDiagram } from "@/contexts/diagram-context"
|
||||
import { useDictionary } from "@/hooks/use-dictionary"
|
||||
import { formatMessage } from "@/lib/i18n/utils"
|
||||
|
||||
interface HistoryDialogProps {
|
||||
showHistory: boolean
|
||||
@@ -22,6 +24,7 @@ export function HistoryDialog({
|
||||
showHistory,
|
||||
onToggleHistory,
|
||||
}: HistoryDialogProps) {
|
||||
const dict = useDictionary()
|
||||
const { loadDiagram: onDisplayChart, diagramHistory } = useDiagram()
|
||||
const [selectedIndex, setSelectedIndex] = useState<number | null>(null)
|
||||
|
||||
@@ -42,18 +45,15 @@ export function HistoryDialog({
|
||||
<Dialog open={showHistory} onOpenChange={onToggleHistory}>
|
||||
<DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Diagram History</DialogTitle>
|
||||
<DialogTitle>{dict.history.title}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Here saved each diagram before AI modification.
|
||||
<br />
|
||||
Click on a diagram to restore it
|
||||
{dict.history.description}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{diagramHistory.length === 0 ? (
|
||||
<div className="text-center p-4 text-gray-500">
|
||||
No history available yet. Send messages to create
|
||||
diagram history.
|
||||
{dict.history.noHistory}
|
||||
</div>
|
||||
) : (
|
||||
<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">
|
||||
<Image
|
||||
src={item.svg}
|
||||
alt={`Diagram version ${index + 1}`}
|
||||
alt={`${dict.history.version} ${index + 1}`}
|
||||
width={200}
|
||||
height={100}
|
||||
className="object-contain w-full h-full p-1"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs text-center mt-1 text-gray-500">
|
||||
Version {index + 1}
|
||||
{dict.history.version} {index + 1}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -88,21 +88,23 @@ export function HistoryDialog({
|
||||
{selectedIndex !== null ? (
|
||||
<>
|
||||
<div className="flex-1 text-sm text-muted-foreground">
|
||||
Restore to Version {selectedIndex + 1}?
|
||||
{formatMessage(dict.history.restoreTo, {
|
||||
version: selectedIndex + 1,
|
||||
})}
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setSelectedIndex(null)}
|
||||
>
|
||||
Cancel
|
||||
{dict.common.cancel}
|
||||
</Button>
|
||||
<Button onClick={handleConfirmRestore}>
|
||||
Confirm
|
||||
{dict.common.confirm}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button variant="outline" onClick={handleClose}>
|
||||
Close
|
||||
{dict.common.close}
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
|
||||
1492
components/model-config-dialog.tsx
Normal file
1492
components/model-config-dialog.tsx
Normal file
File diff suppressed because it is too large
Load Diff
216
components/model-selector.tsx
Normal file
216
components/model-selector.tsx
Normal file
@@ -0,0 +1,216 @@
|
||||
"use client"
|
||||
|
||||
import { Bot, Check, ChevronDown, Server, Settings2 } from "lucide-react"
|
||||
import { useMemo, useState } from "react"
|
||||
import {
|
||||
ModelSelectorContent,
|
||||
ModelSelectorEmpty,
|
||||
ModelSelectorGroup,
|
||||
ModelSelectorInput,
|
||||
ModelSelectorItem,
|
||||
ModelSelectorList,
|
||||
ModelSelectorLogo,
|
||||
ModelSelectorName,
|
||||
ModelSelector as ModelSelectorRoot,
|
||||
ModelSelectorSeparator,
|
||||
ModelSelectorTrigger,
|
||||
} from "@/components/ai-elements/model-selector"
|
||||
import { ButtonWithTooltip } from "@/components/button-with-tooltip"
|
||||
import type { FlattenedModel } from "@/lib/types/model-config"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
interface ModelSelectorProps {
|
||||
models: FlattenedModel[]
|
||||
selectedModelId: string | undefined
|
||||
onSelect: (modelId: string | undefined) => void
|
||||
onConfigure: () => void
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
// Map our provider names to models.dev logo names
|
||||
const PROVIDER_LOGO_MAP: Record<string, string> = {
|
||||
openai: "openai",
|
||||
anthropic: "anthropic",
|
||||
google: "google",
|
||||
azure: "azure",
|
||||
bedrock: "amazon-bedrock",
|
||||
openrouter: "openrouter",
|
||||
deepseek: "deepseek",
|
||||
siliconflow: "siliconflow",
|
||||
gateway: "vercel",
|
||||
}
|
||||
|
||||
// Group models by providerLabel (handles duplicate providers)
|
||||
function groupModelsByProvider(
|
||||
models: FlattenedModel[],
|
||||
): Map<string, { provider: string; models: FlattenedModel[] }> {
|
||||
const groups = new Map<
|
||||
string,
|
||||
{ provider: string; models: FlattenedModel[] }
|
||||
>()
|
||||
for (const model of models) {
|
||||
const key = model.providerLabel
|
||||
const existing = groups.get(key)
|
||||
if (existing) {
|
||||
existing.models.push(model)
|
||||
} else {
|
||||
groups.set(key, { provider: model.provider, models: [model] })
|
||||
}
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
export function ModelSelector({
|
||||
models,
|
||||
selectedModelId,
|
||||
onSelect,
|
||||
onConfigure,
|
||||
disabled = false,
|
||||
}: ModelSelectorProps) {
|
||||
const [open, setOpen] = useState(false)
|
||||
// Only show validated models in the selector
|
||||
const validatedModels = useMemo(
|
||||
() => models.filter((m) => m.validated === true),
|
||||
[models],
|
||||
)
|
||||
const groupedModels = useMemo(
|
||||
() => groupModelsByProvider(validatedModels),
|
||||
[validatedModels],
|
||||
)
|
||||
|
||||
// Find selected model for display
|
||||
const selectedModel = useMemo(
|
||||
() => models.find((m) => m.id === selectedModelId),
|
||||
[models, selectedModelId],
|
||||
)
|
||||
|
||||
const handleSelect = (value: string) => {
|
||||
if (value === "__configure__") {
|
||||
onConfigure()
|
||||
} else if (value === "__server_default__") {
|
||||
onSelect(undefined)
|
||||
} else {
|
||||
onSelect(value)
|
||||
}
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const tooltipContent = selectedModel
|
||||
? `${selectedModel.modelId} (click to change)`
|
||||
: "Using server default model (click to change)"
|
||||
|
||||
return (
|
||||
<ModelSelectorRoot open={open} onOpenChange={setOpen}>
|
||||
<ModelSelectorTrigger asChild>
|
||||
<ButtonWithTooltip
|
||||
tooltipContent={tooltipContent}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
className="hover:bg-accent gap-1.5 h-8 max-w-[180px] px-2"
|
||||
>
|
||||
<Bot className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
|
||||
<span className="text-xs truncate">
|
||||
{selectedModel ? selectedModel.modelId : "Default"}
|
||||
</span>
|
||||
<ChevronDown className="h-3 w-3 flex-shrink-0 text-muted-foreground" />
|
||||
</ButtonWithTooltip>
|
||||
</ModelSelectorTrigger>
|
||||
<ModelSelectorContent title="Select Model">
|
||||
<ModelSelectorInput placeholder="Search models..." />
|
||||
<ModelSelectorList>
|
||||
<ModelSelectorEmpty>
|
||||
{validatedModels.length === 0 && models.length > 0
|
||||
? "No verified models. Test your models first."
|
||||
: "No models found."}
|
||||
</ModelSelectorEmpty>
|
||||
|
||||
{/* Server Default Option */}
|
||||
<ModelSelectorGroup heading="Default">
|
||||
<ModelSelectorItem
|
||||
value="__server_default__"
|
||||
onSelect={handleSelect}
|
||||
className={cn(
|
||||
"cursor-pointer",
|
||||
!selectedModelId && "bg-accent",
|
||||
)}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
!selectedModelId
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
<Server className="mr-2 h-4 w-4 text-muted-foreground" />
|
||||
<ModelSelectorName>
|
||||
Server Default
|
||||
</ModelSelectorName>
|
||||
</ModelSelectorItem>
|
||||
</ModelSelectorGroup>
|
||||
|
||||
{/* Configured Models by Provider */}
|
||||
{Array.from(groupedModels.entries()).map(
|
||||
([
|
||||
providerLabel,
|
||||
{ provider, models: providerModels },
|
||||
]) => (
|
||||
<ModelSelectorGroup
|
||||
key={providerLabel}
|
||||
heading={providerLabel}
|
||||
>
|
||||
{providerModels.map((model) => (
|
||||
<ModelSelectorItem
|
||||
key={model.id}
|
||||
value={model.modelId}
|
||||
onSelect={() => handleSelect(model.id)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
selectedModelId === model.id
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
<ModelSelectorLogo
|
||||
provider={
|
||||
PROVIDER_LOGO_MAP[provider] ||
|
||||
provider
|
||||
}
|
||||
className="mr-2"
|
||||
/>
|
||||
<ModelSelectorName>
|
||||
{model.modelId}
|
||||
</ModelSelectorName>
|
||||
</ModelSelectorItem>
|
||||
))}
|
||||
</ModelSelectorGroup>
|
||||
),
|
||||
)}
|
||||
|
||||
{/* Configure Option */}
|
||||
<ModelSelectorSeparator />
|
||||
<ModelSelectorGroup>
|
||||
<ModelSelectorItem
|
||||
value="__configure__"
|
||||
onSelect={handleSelect}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<Settings2 className="mr-2 h-4 w-4" />
|
||||
<ModelSelectorName>
|
||||
Configure Models...
|
||||
</ModelSelectorName>
|
||||
</ModelSelectorItem>
|
||||
</ModelSelectorGroup>
|
||||
{/* Info text */}
|
||||
<div className="px-3 py-2 text-xs text-muted-foreground border-t">
|
||||
Only verified models are shown
|
||||
</div>
|
||||
</ModelSelectorList>
|
||||
</ModelSelectorContent>
|
||||
</ModelSelectorRoot>
|
||||
)
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import { Coffee, X } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import type React from "react"
|
||||
import { FaGithub } from "react-icons/fa"
|
||||
import { useDictionary } from "@/hooks/use-dictionary"
|
||||
import { formatMessage } from "@/lib/i18n/utils"
|
||||
|
||||
interface QuotaLimitToastProps {
|
||||
type?: "request" | "token"
|
||||
@@ -18,9 +20,11 @@ export function QuotaLimitToast({
|
||||
limit,
|
||||
onDismiss,
|
||||
}: QuotaLimitToastProps) {
|
||||
const dict = useDictionary()
|
||||
const isTokenLimit = type === "token"
|
||||
const formatNumber = (n: number) =>
|
||||
n >= 1000 ? `${(n / 1000).toFixed(1)}k` : n.toString()
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault()
|
||||
@@ -44,7 +48,6 @@ export function QuotaLimitToast({
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
{/* Title row with icon */}
|
||||
<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">
|
||||
@@ -55,40 +58,26 @@ export function QuotaLimitToast({
|
||||
</div>
|
||||
<h3 className="font-semibold text-foreground text-sm">
|
||||
{isTokenLimit
|
||||
? "Daily Token Limit Reached"
|
||||
: "Daily Quota Reached"}
|
||||
? dict.quota.tokenLimit
|
||||
: dict.quota.dailyLimit}
|
||||
</h3>
|
||||
<span className="px-2 py-0.5 text-xs font-medium rounded-md bg-muted text-muted-foreground">
|
||||
{isTokenLimit
|
||||
? `${formatNumber(used)}/${formatNumber(limit)} tokens`
|
||||
: `${used}/${limit}`}
|
||||
{formatMessage(dict.quota.usedOf, {
|
||||
used: formatNumber(used),
|
||||
limit: formatNumber(limit),
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Message */}
|
||||
<div className="text-sm text-muted-foreground leading-relaxed mb-4 space-y-2">
|
||||
<p>
|
||||
Oops — you've reached the daily{" "}
|
||||
{isTokenLimit ? "token" : "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.{" "}
|
||||
<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>
|
||||
{isTokenLimit
|
||||
? dict.quota.messageToken
|
||||
: dict.quota.messageApi}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Tip:</strong> You can use your own API key (click
|
||||
the Settings icon) or self-host the project to bypass these
|
||||
limits.
|
||||
</p>
|
||||
<p>Your limit resets tomorrow. Thanks for understanding!</p>
|
||||
</div>
|
||||
|
||||
<p dangerouslySetInnerHTML={{ __html: dict.quota.tip }} />
|
||||
<p>{dict.quota.reset}</p>
|
||||
</div>{" "}
|
||||
{/* Action buttons */}
|
||||
<div className="flex items-center gap-2">
|
||||
<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"
|
||||
>
|
||||
<FaGithub className="w-3.5 h-3.5" />
|
||||
Self-host
|
||||
{dict.quota.selfHost}
|
||||
</a>
|
||||
<a
|
||||
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"
|
||||
>
|
||||
<Coffee className="w-3.5 h-3.5" />
|
||||
Sponsor
|
||||
{dict.quota.sponsor}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import { useDictionary } from "@/hooks/use-dictionary"
|
||||
|
||||
interface ResetWarningModalProps {
|
||||
open: boolean
|
||||
@@ -21,14 +22,15 @@ export function ResetWarningModal({
|
||||
onOpenChange,
|
||||
onClear,
|
||||
}: ResetWarningModalProps) {
|
||||
const dict = useDictionary()
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Clear Everything?</DialogTitle>
|
||||
<DialogTitle>{dict.dialogs.clearTitle}</DialogTitle>
|
||||
<DialogDescription>
|
||||
This will clear the current conversation and reset the
|
||||
diagram. This action cannot be undone.
|
||||
{dict.dialogs.clearDescription}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
@@ -36,10 +38,10 @@ export function ResetWarningModal({
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
Cancel
|
||||
{dict.common.cancel}
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={onClear}>
|
||||
Clear Everything
|
||||
{dict.dialogs.clearEverything}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
@@ -17,19 +18,10 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import { useDictionary } from "@/hooks/use-dictionary"
|
||||
|
||||
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 {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
@@ -43,6 +35,7 @@ export function SaveDialog({
|
||||
onSave,
|
||||
defaultFilename,
|
||||
}: SaveDialogProps) {
|
||||
const dict = useDictionary()
|
||||
const [filename, setFilename] = useState(defaultFilename)
|
||||
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)
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Save Diagram</DialogTitle>
|
||||
<DialogTitle>{dict.save.title}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{dict.save.description}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Format</label>
|
||||
<label className="text-sm font-medium">
|
||||
{dict.save.format}
|
||||
</label>
|
||||
<Select
|
||||
value={format}
|
||||
onValueChange={(v) => setFormat(v as ExportFormat)}
|
||||
@@ -96,13 +112,15 @@ export function SaveDialog({
|
||||
</Select>
|
||||
</div>
|
||||
<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">
|
||||
<Input
|
||||
value={filename}
|
||||
onChange={(e) => setFilename(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Enter filename"
|
||||
placeholder={dict.save.filenamePlaceholder}
|
||||
autoFocus
|
||||
onFocus={(e) => e.target.select()}
|
||||
className="rounded-r-none border-r-0 focus-visible:z-10"
|
||||
@@ -118,9 +136,9 @@ export function SaveDialog({
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
Cancel
|
||||
{dict.common.cancel}
|
||||
</Button>
|
||||
<Button onClick={handleSave}>Save</Button>
|
||||
<Button onClick={handleSave}>{dict.common.save}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import { Moon, Sun } from "lucide-react"
|
||||
import { useEffect, useState } from "react"
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
||||
import { Suspense, useEffect, useState } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
@@ -20,6 +21,15 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { useDictionary } from "@/hooks/use-dictionary"
|
||||
import { getApiEndpoint } from "@/lib/base-path"
|
||||
import { i18n, type Locale } from "@/lib/i18n/config"
|
||||
|
||||
const LANGUAGE_LABELS: Record<Locale, string> = {
|
||||
en: "English",
|
||||
zh: "中文",
|
||||
ja: "日本語",
|
||||
}
|
||||
|
||||
interface SettingsDialogProps {
|
||||
open: boolean
|
||||
@@ -34,10 +44,6 @@ interface SettingsDialogProps {
|
||||
export const STORAGE_ACCESS_CODE_KEY = "next-ai-draw-io-access-code"
|
||||
export const STORAGE_CLOSE_PROTECTION_KEY = "next-ai-draw-io-close-protection"
|
||||
const STORAGE_ACCESS_CODE_REQUIRED_KEY = "next-ai-draw-io-access-code-required"
|
||||
export const STORAGE_AI_PROVIDER_KEY = "next-ai-draw-io-ai-provider"
|
||||
export const STORAGE_AI_BASE_URL_KEY = "next-ai-draw-io-ai-base-url"
|
||||
export const STORAGE_AI_API_KEY_KEY = "next-ai-draw-io-ai-api-key"
|
||||
export const STORAGE_AI_MODEL_KEY = "next-ai-draw-io-ai-model"
|
||||
|
||||
function getStoredAccessCodeRequired(): boolean | null {
|
||||
if (typeof window === "undefined") return null
|
||||
@@ -46,7 +52,7 @@ function getStoredAccessCodeRequired(): boolean | null {
|
||||
return stored === "true"
|
||||
}
|
||||
|
||||
export function SettingsDialog({
|
||||
function SettingsContent({
|
||||
open,
|
||||
onOpenChange,
|
||||
onCloseProtectionChange,
|
||||
@@ -55,6 +61,10 @@ export function SettingsDialog({
|
||||
darkMode,
|
||||
onToggleDarkMode,
|
||||
}: SettingsDialogProps) {
|
||||
const dict = useDictionary()
|
||||
const router = useRouter()
|
||||
const pathname = usePathname() || "/"
|
||||
const search = useSearchParams()
|
||||
const [accessCode, setAccessCode] = useState("")
|
||||
const [closeProtection, setCloseProtection] = useState(true)
|
||||
const [isVerifying, setIsVerifying] = useState(false)
|
||||
@@ -62,16 +72,13 @@ export function SettingsDialog({
|
||||
const [accessCodeRequired, setAccessCodeRequired] = useState(
|
||||
() => getStoredAccessCodeRequired() ?? false,
|
||||
)
|
||||
const [provider, setProvider] = useState("")
|
||||
const [baseUrl, setBaseUrl] = useState("")
|
||||
const [apiKey, setApiKey] = useState("")
|
||||
const [modelId, setModelId] = useState("")
|
||||
const [currentLang, setCurrentLang] = useState("en")
|
||||
|
||||
useEffect(() => {
|
||||
// Only fetch if not cached in localStorage
|
||||
if (getStoredAccessCodeRequired() !== null) return
|
||||
|
||||
fetch("/api/config")
|
||||
fetch(getApiEndpoint("/api/config"))
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return res.json()
|
||||
@@ -90,6 +97,17 @@ export function SettingsDialog({
|
||||
})
|
||||
}, [])
|
||||
|
||||
// Detect current language from pathname
|
||||
useEffect(() => {
|
||||
const seg = pathname.split("/").filter(Boolean)
|
||||
const first = seg[0]
|
||||
if (first && i18n.locales.includes(first as Locale)) {
|
||||
setCurrentLang(first)
|
||||
} else {
|
||||
setCurrentLang(i18n.defaultLocale)
|
||||
}
|
||||
}, [pathname])
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
const storedCode =
|
||||
@@ -102,16 +120,22 @@ export function SettingsDialog({
|
||||
// Default to true if not set
|
||||
setCloseProtection(storedCloseProtection !== "false")
|
||||
|
||||
// Load AI provider settings
|
||||
setProvider(localStorage.getItem(STORAGE_AI_PROVIDER_KEY) || "")
|
||||
setBaseUrl(localStorage.getItem(STORAGE_AI_BASE_URL_KEY) || "")
|
||||
setApiKey(localStorage.getItem(STORAGE_AI_API_KEY_KEY) || "")
|
||||
setModelId(localStorage.getItem(STORAGE_AI_MODEL_KEY) || "")
|
||||
|
||||
setError("")
|
||||
}
|
||||
}, [open])
|
||||
|
||||
const changeLanguage = (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()}` : ""
|
||||
router.push(newPath + searchStr)
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!accessCodeRequired) return
|
||||
|
||||
@@ -119,24 +143,27 @@ export function SettingsDialog({
|
||||
setIsVerifying(true)
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/verify-access-code", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-access-code": accessCode.trim(),
|
||||
const response = await fetch(
|
||||
getApiEndpoint("/api/verify-access-code"),
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-access-code": accessCode.trim(),
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.valid) {
|
||||
setError(data.message || "Invalid access code")
|
||||
setError(data.message || dict.errors.invalidAccessCode)
|
||||
return
|
||||
}
|
||||
|
||||
localStorage.setItem(STORAGE_ACCESS_CODE_KEY, accessCode.trim())
|
||||
onOpenChange(false)
|
||||
} catch {
|
||||
setError("Failed to verify access code")
|
||||
setError(dict.errors.networkError)
|
||||
} finally {
|
||||
setIsVerifying(false)
|
||||
}
|
||||
@@ -150,287 +177,166 @@ export function SettingsDialog({
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Settings</DialogTitle>
|
||||
<DialogDescription>
|
||||
Configure your application settings.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-2">
|
||||
{accessCodeRequired && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="access-code">Access Code</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="access-code"
|
||||
type="password"
|
||||
value={accessCode}
|
||||
onChange={(e) =>
|
||||
setAccessCode(e.target.value)
|
||||
}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Enter access code"
|
||||
autoComplete="off"
|
||||
/>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={isVerifying || !accessCode.trim()}
|
||||
>
|
||||
{isVerifying ? "..." : "Save"}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-[0.8rem] text-muted-foreground">
|
||||
Required to use this application.
|
||||
</p>
|
||||
{error && (
|
||||
<p className="text-[0.8rem] text-destructive">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{dict.settings.title}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{dict.settings.description}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-2">
|
||||
{accessCodeRequired && (
|
||||
<div className="space-y-2">
|
||||
<Label>AI Provider Settings</Label>
|
||||
<Label htmlFor="access-code">
|
||||
{dict.settings.accessCode}
|
||||
</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="access-code"
|
||||
type="password"
|
||||
value={accessCode}
|
||||
onChange={(e) => setAccessCode(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={
|
||||
dict.settings.accessCodePlaceholder
|
||||
}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={isVerifying || !accessCode.trim()}
|
||||
>
|
||||
{isVerifying ? "..." : dict.common.save}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-[0.8rem] text-muted-foreground">
|
||||
Use your own API key to bypass usage limits. Your
|
||||
key is stored locally in your browser and is never
|
||||
stored on the server.
|
||||
{dict.settings.accessCodeDescription}
|
||||
</p>
|
||||
<div className="space-y-3 pt-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="ai-provider">Provider</Label>
|
||||
<Select
|
||||
value={provider || "default"}
|
||||
onValueChange={(value) => {
|
||||
const actualValue =
|
||||
value === "default" ? "" : value
|
||||
setProvider(actualValue)
|
||||
localStorage.setItem(
|
||||
STORAGE_AI_PROVIDER_KEY,
|
||||
actualValue,
|
||||
)
|
||||
}}
|
||||
>
|
||||
<SelectTrigger id="ai-provider">
|
||||
<SelectValue placeholder="Use Server Default" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="default">
|
||||
Use Server Default
|
||||
</SelectItem>
|
||||
<SelectItem value="openai">
|
||||
OpenAI
|
||||
</SelectItem>
|
||||
<SelectItem value="anthropic">
|
||||
Anthropic
|
||||
</SelectItem>
|
||||
<SelectItem value="google">
|
||||
Google
|
||||
</SelectItem>
|
||||
<SelectItem value="azure">
|
||||
Azure OpenAI
|
||||
</SelectItem>
|
||||
<SelectItem value="openrouter">
|
||||
OpenRouter
|
||||
</SelectItem>
|
||||
<SelectItem value="deepseek">
|
||||
DeepSeek
|
||||
</SelectItem>
|
||||
<SelectItem value="siliconflow">
|
||||
SiliconFlow
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{provider && provider !== "default" && (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="ai-model">
|
||||
Model ID
|
||||
</Label>
|
||||
<Input
|
||||
id="ai-model"
|
||||
value={modelId}
|
||||
onChange={(e) => {
|
||||
setModelId(e.target.value)
|
||||
localStorage.setItem(
|
||||
STORAGE_AI_MODEL_KEY,
|
||||
e.target.value,
|
||||
)
|
||||
}}
|
||||
placeholder={
|
||||
provider === "openai"
|
||||
? "e.g., gpt-4o"
|
||||
: provider === "anthropic"
|
||||
? "e.g., claude-sonnet-4-5"
|
||||
: provider === "google"
|
||||
? "e.g., gemini-2.0-flash-exp"
|
||||
: provider ===
|
||||
"deepseek"
|
||||
? "e.g., deepseek-chat"
|
||||
: "Model ID"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="ai-api-key">
|
||||
API Key
|
||||
</Label>
|
||||
<Input
|
||||
id="ai-api-key"
|
||||
type="password"
|
||||
value={apiKey}
|
||||
onChange={(e) => {
|
||||
setApiKey(e.target.value)
|
||||
localStorage.setItem(
|
||||
STORAGE_AI_API_KEY_KEY,
|
||||
e.target.value,
|
||||
)
|
||||
}}
|
||||
placeholder="Your API key"
|
||||
autoComplete="off"
|
||||
/>
|
||||
<p className="text-[0.8rem] text-muted-foreground">
|
||||
Overrides{" "}
|
||||
{provider === "openai"
|
||||
? "OPENAI_API_KEY"
|
||||
: provider === "anthropic"
|
||||
? "ANTHROPIC_API_KEY"
|
||||
: provider === "google"
|
||||
? "GOOGLE_GENERATIVE_AI_API_KEY"
|
||||
: provider === "azure"
|
||||
? "AZURE_API_KEY"
|
||||
: provider ===
|
||||
"openrouter"
|
||||
? "OPENROUTER_API_KEY"
|
||||
: provider ===
|
||||
"deepseek"
|
||||
? "DEEPSEEK_API_KEY"
|
||||
: provider ===
|
||||
"siliconflow"
|
||||
? "SILICONFLOW_API_KEY"
|
||||
: "server API key"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="ai-base-url">
|
||||
Base URL (optional)
|
||||
</Label>
|
||||
<Input
|
||||
id="ai-base-url"
|
||||
value={baseUrl}
|
||||
onChange={(e) => {
|
||||
setBaseUrl(e.target.value)
|
||||
localStorage.setItem(
|
||||
STORAGE_AI_BASE_URL_KEY,
|
||||
e.target.value,
|
||||
)
|
||||
}}
|
||||
placeholder={
|
||||
provider === "anthropic"
|
||||
? "https://api.anthropic.com/v1"
|
||||
: provider === "siliconflow"
|
||||
? "https://api.siliconflow.com/v1"
|
||||
: "Custom endpoint URL"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
localStorage.removeItem(
|
||||
STORAGE_AI_PROVIDER_KEY,
|
||||
)
|
||||
localStorage.removeItem(
|
||||
STORAGE_AI_BASE_URL_KEY,
|
||||
)
|
||||
localStorage.removeItem(
|
||||
STORAGE_AI_API_KEY_KEY,
|
||||
)
|
||||
localStorage.removeItem(
|
||||
STORAGE_AI_MODEL_KEY,
|
||||
)
|
||||
setProvider("")
|
||||
setBaseUrl("")
|
||||
setApiKey("")
|
||||
setModelId("")
|
||||
}}
|
||||
>
|
||||
Clear Settings
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="theme-toggle">Theme</Label>
|
||||
<p className="text-[0.8rem] text-muted-foreground">
|
||||
Dark/Light mode for interface and DrawIO canvas.
|
||||
{error && (
|
||||
<p className="text-[0.8rem] text-destructive">
|
||||
{error}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
id="theme-toggle"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={onToggleDarkMode}
|
||||
>
|
||||
{darkMode ? (
|
||||
<Sun className="h-4 w-4" />
|
||||
) : (
|
||||
<Moon className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="drawio-ui">DrawIO Style</Label>
|
||||
<p className="text-[0.8rem] text-muted-foreground">
|
||||
Canvas style:{" "}
|
||||
{drawioUi === "min" ? "Minimal" : "Sketch"}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
id="drawio-ui"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onToggleDrawioUi}
|
||||
>
|
||||
Switch to{" "}
|
||||
{drawioUi === "min" ? "Sketch" : "Minimal"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="close-protection">
|
||||
Close Protection
|
||||
</Label>
|
||||
<p className="text-[0.8rem] text-muted-foreground">
|
||||
Show confirmation when leaving the page.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="close-protection"
|
||||
checked={closeProtection}
|
||||
onCheckedChange={(checked) => {
|
||||
setCloseProtection(checked)
|
||||
localStorage.setItem(
|
||||
STORAGE_CLOSE_PROTECTION_KEY,
|
||||
checked.toString(),
|
||||
)
|
||||
onCloseProtectionChange?.(checked)
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="language-select">
|
||||
{dict.settings.language}
|
||||
</Label>
|
||||
<p className="text-[0.8rem] text-muted-foreground">
|
||||
{dict.settings.languageDescription}
|
||||
</p>
|
||||
</div>
|
||||
<Select value={currentLang} onValueChange={changeLanguage}>
|
||||
<SelectTrigger id="language-select" className="w-32">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{i18n.locales.map((locale) => (
|
||||
<SelectItem key={locale} value={locale}>
|
||||
{LANGUAGE_LABELS[locale]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="theme-toggle">
|
||||
{dict.settings.theme}
|
||||
</Label>
|
||||
<p className="text-[0.8rem] text-muted-foreground">
|
||||
{dict.settings.themeDescription}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
id="theme-toggle"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={onToggleDarkMode}
|
||||
>
|
||||
{darkMode ? (
|
||||
<Sun className="h-4 w-4" />
|
||||
) : (
|
||||
<Moon className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="drawio-ui">
|
||||
{dict.settings.drawioStyle}
|
||||
</Label>
|
||||
<p className="text-[0.8rem] text-muted-foreground">
|
||||
{dict.settings.drawioStyleDescription}{" "}
|
||||
{drawioUi === "min"
|
||||
? dict.settings.minimal
|
||||
: dict.settings.sketch}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
id="drawio-ui"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onToggleDrawioUi}
|
||||
>
|
||||
{dict.settings.switchTo}{" "}
|
||||
{drawioUi === "min"
|
||||
? dict.settings.sketch
|
||||
: dict.settings.minimal}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="close-protection">
|
||||
{dict.settings.closeProtection}
|
||||
</Label>
|
||||
<p className="text-[0.8rem] text-muted-foreground">
|
||||
{dict.settings.closeProtectionDescription}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="close-protection"
|
||||
checked={closeProtection}
|
||||
onCheckedChange={(checked) => {
|
||||
setCloseProtection(checked)
|
||||
localStorage.setItem(
|
||||
STORAGE_CLOSE_PROTECTION_KEY,
|
||||
checked.toString(),
|
||||
)
|
||||
onCloseProtectionChange?.(checked)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-4 border-t border-border/50">
|
||||
<p className="text-[0.75rem] text-muted-foreground text-center">
|
||||
Version {process.env.APP_VERSION}
|
||||
</p>
|
||||
</div>
|
||||
</DialogContent>
|
||||
)
|
||||
}
|
||||
|
||||
export function SettingsDialog(props: SettingsDialogProps) {
|
||||
return (
|
||||
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
|
||||
<Suspense
|
||||
fallback={
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<div className="h-64 flex items-center justify-center">
|
||||
<div className="animate-spin h-8 w-8 border-4 border-primary border-t-transparent rounded-full" />
|
||||
</div>
|
||||
</DialogContent>
|
||||
}
|
||||
>
|
||||
<SettingsContent {...props} />
|
||||
</Suspense>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
157
components/ui/alert-dialog.tsx
Normal file
157
components/ui/alert-dialog.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
|
||||
function AlertDialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
||||
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
|
||||
}
|
||||
|
||||
function AlertDialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
data-slot="alert-dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
|
||||
return (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
data-slot="alert-dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogHeader({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogFooter({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Title
|
||||
data-slot="alert-dialog-title"
|
||||
className={cn("text-lg font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Description
|
||||
data-slot="alert-dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogAction({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Action
|
||||
className={cn(buttonVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogCancel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
className={cn(buttonVariants({ variant: "outline" }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
AlertDialogPortal,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogTrigger,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogFooter,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
}
|
||||
191
components/ui/command.tsx
Normal file
191
components/ui/command.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Command as CommandPrimitive } from "cmdk"
|
||||
import { SearchIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
|
||||
function Command({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive>) {
|
||||
return (
|
||||
<CommandPrimitive
|
||||
data-slot="command"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandDialog({
|
||||
title = "Command Palette",
|
||||
description = "Search for a command to run...",
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Dialog> & {
|
||||
title?: string
|
||||
description?: string
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogHeader className="sr-only">
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogContent className={cn("overflow-hidden p-0", className)}>
|
||||
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandInput({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="command-input-wrapper"
|
||||
className="flex h-9 items-center gap-2 border-b px-3"
|
||||
>
|
||||
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
data-slot="command-input"
|
||||
className={cn(
|
||||
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
||||
return (
|
||||
<CommandPrimitive.List
|
||||
data-slot="command-list"
|
||||
className={cn(
|
||||
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandEmpty({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
||||
return (
|
||||
<CommandPrimitive.Empty
|
||||
data-slot="command-empty"
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
||||
return (
|
||||
<CommandPrimitive.Group
|
||||
data-slot="command-group"
|
||||
className={cn(
|
||||
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
||||
return (
|
||||
<CommandPrimitive.Separator
|
||||
data-slot="command-separator"
|
||||
className={cn("bg-border -mx-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
||||
return (
|
||||
<CommandPrimitive.Item
|
||||
data-slot="command-item"
|
||||
className={cn(
|
||||
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
onMouseEnter={(e) => {
|
||||
// Ensure hover updates selection for visual feedback
|
||||
const item = e.currentTarget
|
||||
item.setAttribute("data-selected", "true")
|
||||
// Deselect siblings
|
||||
const siblings = item.parentElement?.querySelectorAll("[cmdk-item]")
|
||||
siblings?.forEach((sibling) => {
|
||||
if (sibling !== item) {
|
||||
sibling.setAttribute("data-selected", "false")
|
||||
}
|
||||
})
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="command-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
}
|
||||
48
components/ui/popover.tsx
Normal file
48
components/ui/popover.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Popover({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
||||
}
|
||||
|
||||
function PopoverTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
||||
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
||||
}
|
||||
|
||||
function PopoverContent({
|
||||
className,
|
||||
align = "center",
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
||||
return (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
data-slot="popover-content"
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function PopoverAnchor({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
||||
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
|
||||
}
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
||||
@@ -1,10 +1,11 @@
|
||||
"use client"
|
||||
|
||||
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 { STORAGE_DIAGRAM_XML_KEY } from "@/components/chat-panel"
|
||||
import type { ExportFormat } from "@/components/save-dialog"
|
||||
import { getApiEndpoint } from "@/lib/base-path"
|
||||
import { extractDiagramXML, validateAndFixXml } from "../lib/utils"
|
||||
|
||||
interface DiagramContextType {
|
||||
@@ -23,9 +24,12 @@ interface DiagramContextType {
|
||||
format: ExportFormat,
|
||||
sessionId?: string,
|
||||
) => void
|
||||
saveDiagramToStorage: () => Promise<void>
|
||||
isDrawioReady: boolean
|
||||
onDrawioLoad: () => void
|
||||
resetDrawioReady: () => void
|
||||
showSaveDialog: boolean
|
||||
setShowSaveDialog: (show: boolean) => void
|
||||
}
|
||||
|
||||
const DiagramContext = createContext<DiagramContextType | undefined>(undefined)
|
||||
@@ -37,11 +41,15 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
||||
{ svg: string; xml: string }[]
|
||||
>([])
|
||||
const [isDrawioReady, setIsDrawioReady] = useState(false)
|
||||
const [canSaveDiagram, setCanSaveDiagram] = useState(false)
|
||||
const [showSaveDialog, setShowSaveDialog] = useState(false)
|
||||
const hasCalledOnLoadRef = useRef(false)
|
||||
const drawioRef = useRef<DrawIoEmbedRef | null>(null)
|
||||
const resolverRef = useRef<((value: string) => void) | null>(null)
|
||||
// Track if we're expecting an export for history (user-initiated)
|
||||
const expectHistoryExportRef = useRef<boolean>(false)
|
||||
// Track if diagram has been restored from localStorage
|
||||
const hasDiagramRestoredRef = useRef<boolean>(false)
|
||||
|
||||
const onDrawioLoad = () => {
|
||||
// Only set ready state once to prevent infinite loops
|
||||
@@ -57,6 +65,48 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
||||
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)
|
||||
const saveResolverRef = useRef<{
|
||||
resolver: ((data: string) => void) | null
|
||||
@@ -82,6 +132,30 @@ 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 = (
|
||||
chart: string,
|
||||
skipValidation?: boolean,
|
||||
@@ -256,7 +330,7 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
||||
sessionId?: string,
|
||||
) => {
|
||||
try {
|
||||
await fetch("/api/log-save", {
|
||||
await fetch(getApiEndpoint("/api/log-save"), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ filename, format, sessionId }),
|
||||
@@ -280,9 +354,12 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
||||
handleDiagramExport,
|
||||
clearDiagram,
|
||||
saveDiagramToFile,
|
||||
saveDiagramToStorage,
|
||||
isDrawioReady,
|
||||
onDrawioLoad,
|
||||
resetDrawioReady,
|
||||
showSaveDialog,
|
||||
setShowSaveDialog,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -7,6 +7,11 @@ services:
|
||||
context: .
|
||||
args:
|
||||
- NEXT_PUBLIC_DRAWIO_BASE_URL=http://localhost:8080
|
||||
# Uncomment below for subdirectory deployment
|
||||
# - NEXT_PUBLIC_BASE_PATH=/nextaidrawio
|
||||
ports: ["3000:3000"]
|
||||
env_file: .env
|
||||
environment:
|
||||
# For subdirectory deployment, uncomment and set your path:
|
||||
# NEXT_PUBLIC_BASE_PATH: /nextaidrawio
|
||||
depends_on: [drawio]
|
||||
|
||||
@@ -26,6 +26,7 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
||||
- [目录](#目录)
|
||||
- [示例](#示例)
|
||||
- [功能特性](#功能特性)
|
||||
- [MCP服务器(预览)](#mcp服务器预览)
|
||||
- [快速开始](#快速开始)
|
||||
- [在线试用](#在线试用)
|
||||
- [使用Docker运行(推荐)](#使用docker运行推荐)
|
||||
@@ -88,6 +89,36 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
||||
- **云架构图支持**:专门支持生成云架构图(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で実行推奨)
|
||||
@@ -88,6 +89,36 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
||||
- **クラウドアーキテクチャダイアグラムサポート**:クラウドアーキテクチャダイアグラムの生成を専門的にサポート(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
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
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`:
|
||||
|
||||
```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
|
||||
|
||||
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`
|
||||
96
electron-builder.yml
Normal file
96
electron-builder.yml
Normal file
@@ -0,0 +1,96 @@
|
||||
appId: com.nextaidrawio.app
|
||||
productName: Next AI Draw.io
|
||||
copyright: Copyright © 2024 Next AI Draw.io
|
||||
electronVersion: 39.2.7
|
||||
|
||||
directories:
|
||||
output: release
|
||||
buildResources: resources
|
||||
|
||||
afterPack: ./scripts/afterPack.cjs
|
||||
|
||||
files:
|
||||
- dist-electron/**/*
|
||||
- "!node_modules"
|
||||
|
||||
asarUnpack:
|
||||
- "**/*.node"
|
||||
|
||||
extraResources:
|
||||
# Copy prepared standalone directory (includes node_modules)
|
||||
- from: electron-standalone/
|
||||
to: standalone/
|
||||
# Copy icon for runtime use (Windows/Linux)
|
||||
- from: resources/icon.png
|
||||
to: icon.png
|
||||
|
||||
# macOS configuration
|
||||
mac:
|
||||
category: public.app-category.productivity
|
||||
icon: resources/icon.png
|
||||
target:
|
||||
- target: dmg
|
||||
arch:
|
||||
- x64
|
||||
- arm64
|
||||
- target: zip
|
||||
arch:
|
||||
- x64
|
||||
- arm64
|
||||
hardenedRuntime: true
|
||||
gatekeeperAssess: false
|
||||
entitlements: resources/entitlements.mac.plist
|
||||
entitlementsInherit: resources/entitlements.mac.plist
|
||||
|
||||
dmg:
|
||||
contents:
|
||||
- x: 130
|
||||
y: 220
|
||||
- x: 410
|
||||
y: 220
|
||||
type: link
|
||||
path: /Applications
|
||||
window:
|
||||
width: 540
|
||||
height: 380
|
||||
|
||||
# Windows configuration
|
||||
win:
|
||||
icon: resources/icon.png
|
||||
target:
|
||||
- target: nsis
|
||||
arch:
|
||||
- x64
|
||||
- arm64
|
||||
- target: portable
|
||||
arch:
|
||||
- x64
|
||||
- arm64
|
||||
|
||||
nsis:
|
||||
oneClick: false
|
||||
perMachine: false
|
||||
allowToChangeInstallationDirectory: true
|
||||
deleteAppDataOnUninstall: false
|
||||
createDesktopShortcut: true
|
||||
createStartMenuShortcut: true
|
||||
|
||||
# Linux configuration
|
||||
linux:
|
||||
icon: resources/icon.png
|
||||
category: Office
|
||||
maintainer: Next AI Draw.io <nextaidrawio@users.noreply.github.com>
|
||||
target:
|
||||
- target: AppImage
|
||||
arch:
|
||||
- x64
|
||||
- arm64
|
||||
- target: deb
|
||||
arch:
|
||||
- x64
|
||||
- arm64
|
||||
|
||||
# Publish configuration (optional)
|
||||
publish:
|
||||
provider: github
|
||||
releaseType: release
|
||||
74
electron.d.ts
vendored
Normal file
74
electron.d.ts
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Type declarations for Electron API exposed via preload script
|
||||
*/
|
||||
|
||||
/** Configuration preset interface */
|
||||
interface ConfigPreset {
|
||||
id: string
|
||||
name: string
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
config: {
|
||||
AI_PROVIDER?: string
|
||||
AI_MODEL?: string
|
||||
AI_API_KEY?: string
|
||||
AI_BASE_URL?: string
|
||||
TEMPERATURE?: string
|
||||
[key: string]: string | undefined
|
||||
}
|
||||
}
|
||||
|
||||
/** Result of applying a preset */
|
||||
interface ApplyPresetResult {
|
||||
success: boolean
|
||||
error?: string
|
||||
env?: Record<string, string>
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
/** Main window Electron API */
|
||||
electronAPI?: {
|
||||
/** Current platform (darwin, win32, linux) */
|
||||
platform: NodeJS.Platform
|
||||
/** Whether running in Electron environment */
|
||||
isElectron: boolean
|
||||
/** Get application version */
|
||||
getVersion: () => Promise<string>
|
||||
/** Minimize the window */
|
||||
minimize: () => void
|
||||
/** Maximize/restore the window */
|
||||
maximize: () => void
|
||||
/** Close the window */
|
||||
close: () => void
|
||||
/** Open file dialog and return file path */
|
||||
openFile: () => Promise<string | null>
|
||||
/** Save data to file via save dialog */
|
||||
saveFile: (data: string) => Promise<boolean>
|
||||
}
|
||||
|
||||
/** Settings window Electron API */
|
||||
settingsAPI?: {
|
||||
/** Get all configuration presets */
|
||||
getPresets: () => Promise<ConfigPreset[]>
|
||||
/** Get current preset ID */
|
||||
getCurrentPresetId: () => Promise<string | null>
|
||||
/** Get current preset */
|
||||
getCurrentPreset: () => Promise<ConfigPreset | null>
|
||||
/** Save (create or update) a preset */
|
||||
savePreset: (preset: {
|
||||
id?: string
|
||||
name: string
|
||||
config: Record<string, string | undefined>
|
||||
}) => Promise<ConfigPreset>
|
||||
/** Delete a preset */
|
||||
deletePreset: (id: string) => Promise<boolean>
|
||||
/** Apply a preset (sets environment variables and restarts server) */
|
||||
applyPreset: (id: string) => Promise<ApplyPresetResult>
|
||||
/** Close settings window */
|
||||
close: () => void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { ConfigPreset, ApplyPresetResult }
|
||||
241
electron/main/app-menu.ts
Normal file
241
electron/main/app-menu.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
dialog,
|
||||
Menu,
|
||||
type MenuItemConstructorOptions,
|
||||
shell,
|
||||
} from "electron"
|
||||
import {
|
||||
applyPresetToEnv,
|
||||
getAllPresets,
|
||||
getCurrentPresetId,
|
||||
setCurrentPreset,
|
||||
} from "./config-manager"
|
||||
import { restartNextServer } from "./next-server"
|
||||
import { showSettingsWindow } from "./settings-window"
|
||||
|
||||
/**
|
||||
* Build and set the application menu
|
||||
*/
|
||||
export function buildAppMenu(): void {
|
||||
const template = getMenuTemplate()
|
||||
const menu = Menu.buildFromTemplate(template)
|
||||
Menu.setApplicationMenu(menu)
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the menu (call this when presets change)
|
||||
*/
|
||||
export function rebuildAppMenu(): void {
|
||||
buildAppMenu()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the menu template
|
||||
*/
|
||||
function getMenuTemplate(): MenuItemConstructorOptions[] {
|
||||
const isMac = process.platform === "darwin"
|
||||
|
||||
const template: MenuItemConstructorOptions[] = []
|
||||
|
||||
// macOS app menu
|
||||
if (isMac) {
|
||||
template.push({
|
||||
label: app.name,
|
||||
submenu: [
|
||||
{ role: "about" },
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "Settings...",
|
||||
accelerator: "CmdOrCtrl+,",
|
||||
click: () => {
|
||||
const win = BrowserWindow.getFocusedWindow()
|
||||
showSettingsWindow(win || undefined)
|
||||
},
|
||||
},
|
||||
{ type: "separator" },
|
||||
{ role: "services" },
|
||||
{ type: "separator" },
|
||||
{ role: "hide" },
|
||||
{ role: "hideOthers" },
|
||||
{ role: "unhide" },
|
||||
{ type: "separator" },
|
||||
{ role: "quit" },
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
// File menu
|
||||
template.push({
|
||||
label: "File",
|
||||
submenu: [
|
||||
...(isMac
|
||||
? []
|
||||
: [
|
||||
{
|
||||
label: "Settings",
|
||||
accelerator: "CmdOrCtrl+,",
|
||||
click: () => {
|
||||
const win = BrowserWindow.getFocusedWindow()
|
||||
showSettingsWindow(win || undefined)
|
||||
},
|
||||
},
|
||||
{ type: "separator" } as MenuItemConstructorOptions,
|
||||
]),
|
||||
isMac ? { role: "close" } : { role: "quit" },
|
||||
],
|
||||
})
|
||||
|
||||
// Edit menu
|
||||
template.push({
|
||||
label: "Edit",
|
||||
submenu: [
|
||||
{ role: "undo" },
|
||||
{ role: "redo" },
|
||||
{ type: "separator" },
|
||||
{ role: "cut" },
|
||||
{ role: "copy" },
|
||||
{ role: "paste" },
|
||||
...(isMac
|
||||
? [
|
||||
{
|
||||
role: "pasteAndMatchStyle",
|
||||
} as MenuItemConstructorOptions,
|
||||
{ role: "delete" } as MenuItemConstructorOptions,
|
||||
{ role: "selectAll" } as MenuItemConstructorOptions,
|
||||
]
|
||||
: [
|
||||
{ role: "delete" } as MenuItemConstructorOptions,
|
||||
{ type: "separator" } as MenuItemConstructorOptions,
|
||||
{ role: "selectAll" } as MenuItemConstructorOptions,
|
||||
]),
|
||||
],
|
||||
})
|
||||
|
||||
// View menu
|
||||
template.push({
|
||||
label: "View",
|
||||
submenu: [
|
||||
{ role: "reload" },
|
||||
{ role: "forceReload" },
|
||||
{ role: "toggleDevTools" },
|
||||
{ type: "separator" },
|
||||
{ role: "resetZoom" },
|
||||
{ role: "zoomIn" },
|
||||
{ role: "zoomOut" },
|
||||
{ type: "separator" },
|
||||
{ role: "togglefullscreen" },
|
||||
],
|
||||
})
|
||||
|
||||
// Configuration menu with presets
|
||||
template.push(buildConfigMenu())
|
||||
|
||||
// Window menu
|
||||
template.push({
|
||||
label: "Window",
|
||||
submenu: [
|
||||
{ role: "minimize" },
|
||||
{ role: "zoom" },
|
||||
...(isMac
|
||||
? [
|
||||
{ type: "separator" } as MenuItemConstructorOptions,
|
||||
{ role: "front" } as MenuItemConstructorOptions,
|
||||
]
|
||||
: [{ role: "close" } as MenuItemConstructorOptions]),
|
||||
],
|
||||
})
|
||||
|
||||
// Help menu
|
||||
template.push({
|
||||
label: "Help",
|
||||
submenu: [
|
||||
{
|
||||
label: "Documentation",
|
||||
click: async () => {
|
||||
await shell.openExternal(
|
||||
"https://github.com/dayuanjiang/next-ai-draw-io",
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Report Issue",
|
||||
click: async () => {
|
||||
await shell.openExternal(
|
||||
"https://github.com/dayuanjiang/next-ai-draw-io/issues",
|
||||
)
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
return template
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the Configuration menu with presets
|
||||
*/
|
||||
function buildConfigMenu(): MenuItemConstructorOptions {
|
||||
const presets = getAllPresets()
|
||||
const currentPresetId = getCurrentPresetId()
|
||||
|
||||
const presetItems: MenuItemConstructorOptions[] = presets.map((preset) => ({
|
||||
label: preset.name,
|
||||
type: "radio",
|
||||
checked: preset.id === currentPresetId,
|
||||
click: async () => {
|
||||
const previousPresetId = getCurrentPresetId()
|
||||
const env = applyPresetToEnv(preset.id)
|
||||
|
||||
if (env) {
|
||||
try {
|
||||
await restartNextServer()
|
||||
rebuildAppMenu() // Rebuild menu to update checkmarks
|
||||
} catch (error) {
|
||||
console.error("Failed to restart server:", error)
|
||||
|
||||
// Revert to previous preset on failure
|
||||
if (previousPresetId) {
|
||||
applyPresetToEnv(previousPresetId)
|
||||
} else {
|
||||
setCurrentPreset(null)
|
||||
}
|
||||
|
||||
// Rebuild menu to restore previous checkmark state
|
||||
rebuildAppMenu()
|
||||
|
||||
// Show error dialog to notify user
|
||||
dialog.showErrorBox(
|
||||
"Configuration Error",
|
||||
`Failed to apply preset "${preset.name}". The server could not be restarted.\n\nThe previous configuration has been restored.\n\nError: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
return {
|
||||
label: "Configuration",
|
||||
submenu: [
|
||||
...(presetItems.length > 0
|
||||
? [
|
||||
{ label: "Switch Preset", enabled: false },
|
||||
{ type: "separator" } as MenuItemConstructorOptions,
|
||||
...presetItems,
|
||||
{ type: "separator" } as MenuItemConstructorOptions,
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label:
|
||||
presetItems.length > 0
|
||||
? "Manage Presets..."
|
||||
: "Add Configuration Preset...",
|
||||
click: () => {
|
||||
const win = BrowserWindow.getFocusedWindow()
|
||||
showSettingsWindow(win || undefined)
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
460
electron/main/config-manager.ts
Normal file
460
electron/main/config-manager.ts
Normal file
@@ -0,0 +1,460 @@
|
||||
import { randomUUID } from "node:crypto"
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
|
||||
import path from "node:path"
|
||||
import { app, safeStorage } from "electron"
|
||||
|
||||
/**
|
||||
* Fields that contain sensitive data and should be encrypted
|
||||
*/
|
||||
const SENSITIVE_FIELDS = ["AI_API_KEY"] as const
|
||||
|
||||
/**
|
||||
* Prefix to identify encrypted values
|
||||
*/
|
||||
const ENCRYPTED_PREFIX = "encrypted:"
|
||||
|
||||
/**
|
||||
* Check if safeStorage encryption is available
|
||||
*/
|
||||
function isEncryptionAvailable(): boolean {
|
||||
return safeStorage.isEncryptionAvailable()
|
||||
}
|
||||
|
||||
/**
|
||||
* Track if we've already warned about plaintext storage
|
||||
*/
|
||||
let hasWarnedAboutPlaintext = false
|
||||
|
||||
/**
|
||||
* Encrypt a sensitive value using safeStorage
|
||||
* Warns if encryption is not available (API key stored in plaintext)
|
||||
*/
|
||||
function encryptValue(value: string): string {
|
||||
if (!value) {
|
||||
return value
|
||||
}
|
||||
|
||||
if (!isEncryptionAvailable()) {
|
||||
if (!hasWarnedAboutPlaintext) {
|
||||
console.warn(
|
||||
"⚠️ SECURITY WARNING: safeStorage not available. " +
|
||||
"API keys will be stored in PLAINTEXT. " +
|
||||
"On Linux, install gnome-keyring or similar for secure storage.",
|
||||
)
|
||||
hasWarnedAboutPlaintext = true
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
try {
|
||||
const encrypted = safeStorage.encryptString(value)
|
||||
return ENCRYPTED_PREFIX + encrypted.toString("base64")
|
||||
} catch (error) {
|
||||
console.error("Encryption failed:", error)
|
||||
// Fail secure: don't store if encryption fails
|
||||
throw new Error(
|
||||
"Failed to encrypt API key. Cannot securely store credentials.",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt a sensitive value using safeStorage
|
||||
* Returns the original value if it's not encrypted or decryption fails
|
||||
*/
|
||||
function decryptValue(value: string): string {
|
||||
if (!value || !value.startsWith(ENCRYPTED_PREFIX)) {
|
||||
return value
|
||||
}
|
||||
if (!isEncryptionAvailable()) {
|
||||
console.warn(
|
||||
"Cannot decrypt value: safeStorage encryption is not available",
|
||||
)
|
||||
return value
|
||||
}
|
||||
try {
|
||||
const base64Data = value.slice(ENCRYPTED_PREFIX.length)
|
||||
const buffer = Buffer.from(base64Data, "base64")
|
||||
return safeStorage.decryptString(buffer)
|
||||
} catch (error) {
|
||||
console.error("Failed to decrypt value:", error)
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt sensitive fields in a config object
|
||||
*/
|
||||
function encryptConfig(
|
||||
config: Record<string, string | undefined>,
|
||||
): Record<string, string | undefined> {
|
||||
const encrypted = { ...config }
|
||||
for (const field of SENSITIVE_FIELDS) {
|
||||
if (encrypted[field]) {
|
||||
encrypted[field] = encryptValue(encrypted[field] as string)
|
||||
}
|
||||
}
|
||||
return encrypted
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt sensitive fields in a config object
|
||||
*/
|
||||
function decryptConfig(
|
||||
config: Record<string, string | undefined>,
|
||||
): Record<string, string | undefined> {
|
||||
const decrypted = { ...config }
|
||||
for (const field of SENSITIVE_FIELDS) {
|
||||
if (decrypted[field]) {
|
||||
decrypted[field] = decryptValue(decrypted[field] as string)
|
||||
}
|
||||
}
|
||||
return decrypted
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration preset interface
|
||||
*/
|
||||
export interface ConfigPreset {
|
||||
id: string
|
||||
name: string
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
config: {
|
||||
AI_PROVIDER?: string
|
||||
AI_MODEL?: string
|
||||
AI_API_KEY?: string
|
||||
AI_BASE_URL?: string
|
||||
TEMPERATURE?: string
|
||||
[key: string]: string | undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration file structure
|
||||
*/
|
||||
interface ConfigPresetsFile {
|
||||
version: 1
|
||||
currentPresetId: string | null
|
||||
presets: ConfigPreset[]
|
||||
}
|
||||
|
||||
const CONFIG_FILE_NAME = "config-presets.json"
|
||||
|
||||
/**
|
||||
* Get the path to the config file
|
||||
*/
|
||||
function getConfigFilePath(): string {
|
||||
const userDataPath = app.getPath("userData")
|
||||
return path.join(userDataPath, CONFIG_FILE_NAME)
|
||||
}
|
||||
|
||||
/**
|
||||
* Load presets from the config file
|
||||
* Decrypts sensitive fields automatically
|
||||
*/
|
||||
export function loadPresets(): ConfigPresetsFile {
|
||||
const configPath = getConfigFilePath()
|
||||
|
||||
if (!existsSync(configPath)) {
|
||||
return {
|
||||
version: 1,
|
||||
currentPresetId: null,
|
||||
presets: [],
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const content = readFileSync(configPath, "utf-8")
|
||||
const data = JSON.parse(content) as ConfigPresetsFile
|
||||
|
||||
// Decrypt sensitive fields in each preset
|
||||
data.presets = data.presets.map((preset) => ({
|
||||
...preset,
|
||||
config: decryptConfig(preset.config) as ConfigPreset["config"],
|
||||
}))
|
||||
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error("Failed to load config presets:", error)
|
||||
return {
|
||||
version: 1,
|
||||
currentPresetId: null,
|
||||
presets: [],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save presets to the config file
|
||||
* Encrypts sensitive fields automatically
|
||||
*/
|
||||
export function savePresets(data: ConfigPresetsFile): void {
|
||||
const configPath = getConfigFilePath()
|
||||
const userDataPath = app.getPath("userData")
|
||||
|
||||
// Ensure the directory exists
|
||||
if (!existsSync(userDataPath)) {
|
||||
mkdirSync(userDataPath, { recursive: true })
|
||||
}
|
||||
|
||||
// Encrypt sensitive fields before saving
|
||||
const dataToSave: ConfigPresetsFile = {
|
||||
...data,
|
||||
presets: data.presets.map((preset) => ({
|
||||
...preset,
|
||||
config: encryptConfig(preset.config) as ConfigPreset["config"],
|
||||
})),
|
||||
}
|
||||
|
||||
try {
|
||||
writeFileSync(configPath, JSON.stringify(dataToSave, null, 2), "utf-8")
|
||||
} catch (error) {
|
||||
console.error("Failed to save config presets:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all presets
|
||||
*/
|
||||
export function getAllPresets(): ConfigPreset[] {
|
||||
const data = loadPresets()
|
||||
return data.presets
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current preset ID
|
||||
*/
|
||||
export function getCurrentPresetId(): string | null {
|
||||
const data = loadPresets()
|
||||
return data.currentPresetId
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current preset
|
||||
*/
|
||||
export function getCurrentPreset(): ConfigPreset | null {
|
||||
const data = loadPresets()
|
||||
if (!data.currentPresetId) {
|
||||
return null
|
||||
}
|
||||
return data.presets.find((p) => p.id === data.currentPresetId) || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new preset
|
||||
*/
|
||||
export function createPreset(
|
||||
preset: Omit<ConfigPreset, "id" | "createdAt" | "updatedAt">,
|
||||
): ConfigPreset {
|
||||
const data = loadPresets()
|
||||
const now = Date.now()
|
||||
|
||||
const newPreset: ConfigPreset = {
|
||||
id: randomUUID(),
|
||||
name: preset.name,
|
||||
config: preset.config,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}
|
||||
|
||||
data.presets.push(newPreset)
|
||||
savePresets(data)
|
||||
|
||||
return newPreset
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing preset
|
||||
*/
|
||||
export function updatePreset(
|
||||
id: string,
|
||||
updates: Partial<Omit<ConfigPreset, "id" | "createdAt">>,
|
||||
): ConfigPreset | null {
|
||||
const data = loadPresets()
|
||||
const index = data.presets.findIndex((p) => p.id === id)
|
||||
|
||||
if (index === -1) {
|
||||
return null
|
||||
}
|
||||
|
||||
const updatedPreset: ConfigPreset = {
|
||||
...data.presets[index],
|
||||
...updates,
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
|
||||
data.presets[index] = updatedPreset
|
||||
savePresets(data)
|
||||
|
||||
return updatedPreset
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a preset
|
||||
*/
|
||||
export function deletePreset(id: string): boolean {
|
||||
const data = loadPresets()
|
||||
const index = data.presets.findIndex((p) => p.id === id)
|
||||
|
||||
if (index === -1) {
|
||||
return false
|
||||
}
|
||||
|
||||
data.presets.splice(index, 1)
|
||||
|
||||
// Clear current preset if it was deleted
|
||||
if (data.currentPresetId === id) {
|
||||
data.currentPresetId = null
|
||||
}
|
||||
|
||||
savePresets(data)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current preset
|
||||
*/
|
||||
export function setCurrentPreset(id: string | null): boolean {
|
||||
const data = loadPresets()
|
||||
|
||||
if (id !== null) {
|
||||
const preset = data.presets.find((p) => p.id === id)
|
||||
if (!preset) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
data.currentPresetId = id
|
||||
savePresets(data)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Map generic AI_API_KEY and AI_BASE_URL to provider-specific environment variables
|
||||
*/
|
||||
const PROVIDER_ENV_MAP: Record<string, { apiKey: string; baseUrl: string }> = {
|
||||
openai: { apiKey: "OPENAI_API_KEY", baseUrl: "OPENAI_BASE_URL" },
|
||||
anthropic: { apiKey: "ANTHROPIC_API_KEY", baseUrl: "ANTHROPIC_BASE_URL" },
|
||||
google: {
|
||||
apiKey: "GOOGLE_GENERATIVE_AI_API_KEY",
|
||||
baseUrl: "GOOGLE_BASE_URL",
|
||||
},
|
||||
azure: { apiKey: "AZURE_API_KEY", baseUrl: "AZURE_BASE_URL" },
|
||||
openrouter: {
|
||||
apiKey: "OPENROUTER_API_KEY",
|
||||
baseUrl: "OPENROUTER_BASE_URL",
|
||||
},
|
||||
deepseek: { apiKey: "DEEPSEEK_API_KEY", baseUrl: "DEEPSEEK_BASE_URL" },
|
||||
siliconflow: {
|
||||
apiKey: "SILICONFLOW_API_KEY",
|
||||
baseUrl: "SILICONFLOW_BASE_URL",
|
||||
},
|
||||
gateway: { apiKey: "AI_GATEWAY_API_KEY", baseUrl: "AI_GATEWAY_BASE_URL" },
|
||||
// bedrock and ollama don't use API keys in the same way
|
||||
bedrock: { apiKey: "", baseUrl: "" },
|
||||
ollama: { apiKey: "", baseUrl: "OLLAMA_BASE_URL" },
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply preset environment variables to the current process
|
||||
* Returns the environment variables that were applied
|
||||
*/
|
||||
export function applyPresetToEnv(id: string): Record<string, string> | null {
|
||||
const data = loadPresets()
|
||||
const preset = data.presets.find((p) => p.id === id)
|
||||
|
||||
if (!preset) {
|
||||
return null
|
||||
}
|
||||
|
||||
const appliedEnv: Record<string, string> = {}
|
||||
const provider = preset.config.AI_PROVIDER?.toLowerCase()
|
||||
|
||||
for (const [key, value] of Object.entries(preset.config)) {
|
||||
if (value !== undefined && value !== "") {
|
||||
// Map generic AI_API_KEY to provider-specific key
|
||||
if (
|
||||
key === "AI_API_KEY" &&
|
||||
provider &&
|
||||
PROVIDER_ENV_MAP[provider]
|
||||
) {
|
||||
const providerApiKey = PROVIDER_ENV_MAP[provider].apiKey
|
||||
if (providerApiKey) {
|
||||
process.env[providerApiKey] = value
|
||||
appliedEnv[providerApiKey] = value
|
||||
}
|
||||
}
|
||||
// Map generic AI_BASE_URL to provider-specific key
|
||||
else if (
|
||||
key === "AI_BASE_URL" &&
|
||||
provider &&
|
||||
PROVIDER_ENV_MAP[provider]
|
||||
) {
|
||||
const providerBaseUrl = PROVIDER_ENV_MAP[provider].baseUrl
|
||||
if (providerBaseUrl) {
|
||||
process.env[providerBaseUrl] = value
|
||||
appliedEnv[providerBaseUrl] = value
|
||||
}
|
||||
}
|
||||
// Apply other env vars directly
|
||||
else {
|
||||
process.env[key] = value
|
||||
appliedEnv[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set as current preset
|
||||
data.currentPresetId = id
|
||||
savePresets(data)
|
||||
|
||||
return appliedEnv
|
||||
}
|
||||
|
||||
/**
|
||||
* Get environment variables from current preset
|
||||
* Maps generic AI_API_KEY/AI_BASE_URL to provider-specific keys
|
||||
*/
|
||||
export function getCurrentPresetEnv(): Record<string, string> {
|
||||
const preset = getCurrentPreset()
|
||||
if (!preset) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const env: Record<string, string> = {}
|
||||
const provider = preset.config.AI_PROVIDER?.toLowerCase()
|
||||
|
||||
for (const [key, value] of Object.entries(preset.config)) {
|
||||
if (value !== undefined && value !== "") {
|
||||
// Map generic AI_API_KEY to provider-specific key
|
||||
if (
|
||||
key === "AI_API_KEY" &&
|
||||
provider &&
|
||||
PROVIDER_ENV_MAP[provider]
|
||||
) {
|
||||
const providerApiKey = PROVIDER_ENV_MAP[provider].apiKey
|
||||
if (providerApiKey) {
|
||||
env[providerApiKey] = value
|
||||
}
|
||||
}
|
||||
// Map generic AI_BASE_URL to provider-specific key
|
||||
else if (
|
||||
key === "AI_BASE_URL" &&
|
||||
provider &&
|
||||
PROVIDER_ENV_MAP[provider]
|
||||
) {
|
||||
const providerBaseUrl = PROVIDER_ENV_MAP[provider].baseUrl
|
||||
if (providerBaseUrl) {
|
||||
env[providerBaseUrl] = value
|
||||
}
|
||||
}
|
||||
// Apply other env vars directly
|
||||
else {
|
||||
env[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
return env
|
||||
}
|
||||
67
electron/main/env-loader.ts
Normal file
67
electron/main/env-loader.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import fs from "node:fs"
|
||||
import path from "node:path"
|
||||
import { app } from "electron"
|
||||
|
||||
/**
|
||||
* Load environment variables from .env file
|
||||
* Searches multiple locations in priority order
|
||||
*/
|
||||
export function loadEnvFile(): void {
|
||||
const possiblePaths = [
|
||||
// Next to the executable (for portable installations)
|
||||
path.join(path.dirname(app.getPath("exe")), ".env"),
|
||||
// User data directory (persists across updates)
|
||||
path.join(app.getPath("userData"), ".env"),
|
||||
// Development: project root
|
||||
path.join(app.getAppPath(), ".env.local"),
|
||||
path.join(app.getAppPath(), ".env"),
|
||||
]
|
||||
|
||||
for (const envPath of possiblePaths) {
|
||||
if (fs.existsSync(envPath)) {
|
||||
console.log(`Loading environment from: ${envPath}`)
|
||||
loadEnvFromFile(envPath)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
console.log("No .env file found, using system environment variables")
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and load environment variables from a file
|
||||
*/
|
||||
function loadEnvFromFile(filePath: string): void {
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, "utf-8")
|
||||
const lines = content.split("\n")
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim()
|
||||
|
||||
// Skip comments and empty lines
|
||||
if (!trimmed || trimmed.startsWith("#")) continue
|
||||
|
||||
const equalIndex = trimmed.indexOf("=")
|
||||
if (equalIndex === -1) continue
|
||||
|
||||
const key = trimmed.slice(0, equalIndex).trim()
|
||||
let value = trimmed.slice(equalIndex + 1).trim()
|
||||
|
||||
// Remove surrounding quotes
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
value = value.slice(1, -1)
|
||||
}
|
||||
|
||||
// Don't override existing environment variables
|
||||
if (!(key in process.env)) {
|
||||
process.env[key] = value
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to load env file ${filePath}:`, error)
|
||||
}
|
||||
}
|
||||
105
electron/main/index.ts
Normal file
105
electron/main/index.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { app, BrowserWindow, dialog, shell } from "electron"
|
||||
import { buildAppMenu } from "./app-menu"
|
||||
import { getCurrentPresetEnv } from "./config-manager"
|
||||
import { loadEnvFile } from "./env-loader"
|
||||
import { registerIpcHandlers } from "./ipc-handlers"
|
||||
import { startNextServer, stopNextServer } from "./next-server"
|
||||
import { registerSettingsWindowHandlers } from "./settings-window"
|
||||
import { createWindow, getMainWindow } from "./window-manager"
|
||||
|
||||
// Single instance lock
|
||||
const gotTheLock = app.requestSingleInstanceLock()
|
||||
|
||||
if (!gotTheLock) {
|
||||
app.quit()
|
||||
} else {
|
||||
app.on("second-instance", () => {
|
||||
const mainWindow = getMainWindow()
|
||||
if (mainWindow) {
|
||||
if (mainWindow.isMinimized()) mainWindow.restore()
|
||||
mainWindow.focus()
|
||||
}
|
||||
})
|
||||
|
||||
// Load environment variables from .env files
|
||||
loadEnvFile()
|
||||
|
||||
// Apply saved preset environment variables (overrides .env)
|
||||
const presetEnv = getCurrentPresetEnv()
|
||||
for (const [key, value] of Object.entries(presetEnv)) {
|
||||
process.env[key] = value
|
||||
}
|
||||
|
||||
const isDev = process.env.NODE_ENV === "development"
|
||||
let serverUrl: string | null = null
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
// Register IPC handlers
|
||||
registerIpcHandlers()
|
||||
registerSettingsWindowHandlers()
|
||||
|
||||
// Build application menu
|
||||
buildAppMenu()
|
||||
|
||||
try {
|
||||
if (isDev) {
|
||||
// Development: use the dev server URL
|
||||
serverUrl =
|
||||
process.env.ELECTRON_DEV_URL || "http://localhost:6002"
|
||||
console.log(`Development mode: connecting to ${serverUrl}`)
|
||||
} else {
|
||||
// Production: start Next.js standalone server
|
||||
serverUrl = await startNextServer()
|
||||
}
|
||||
|
||||
// Create main window
|
||||
createWindow(serverUrl)
|
||||
} catch (error) {
|
||||
console.error("Failed to start application:", error)
|
||||
dialog.showErrorBox(
|
||||
"Startup Error",
|
||||
`Failed to start the application: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
)
|
||||
app.quit()
|
||||
}
|
||||
|
||||
app.on("activate", () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
if (serverUrl) {
|
||||
createWindow(serverUrl)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") {
|
||||
stopNextServer()
|
||||
app.quit()
|
||||
}
|
||||
})
|
||||
|
||||
app.on("before-quit", () => {
|
||||
stopNextServer()
|
||||
})
|
||||
|
||||
// Open external links in default browser
|
||||
app.on("web-contents-created", (_, contents) => {
|
||||
contents.setWindowOpenHandler(({ url }) => {
|
||||
// Allow diagrams.net iframe
|
||||
if (
|
||||
url.includes("diagrams.net") ||
|
||||
url.includes("draw.io") ||
|
||||
url.startsWith("http://localhost")
|
||||
) {
|
||||
return { action: "allow" }
|
||||
}
|
||||
// Open other links in external browser
|
||||
if (url.startsWith("http://") || url.startsWith("https://")) {
|
||||
shell.openExternal(url)
|
||||
return { action: "deny" }
|
||||
}
|
||||
return { action: "allow" }
|
||||
})
|
||||
})
|
||||
}
|
||||
212
electron/main/ipc-handlers.ts
Normal file
212
electron/main/ipc-handlers.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
import { app, BrowserWindow, dialog, ipcMain } from "electron"
|
||||
import {
|
||||
applyPresetToEnv,
|
||||
type ConfigPreset,
|
||||
createPreset,
|
||||
deletePreset,
|
||||
getAllPresets,
|
||||
getCurrentPreset,
|
||||
getCurrentPresetId,
|
||||
setCurrentPreset,
|
||||
updatePreset,
|
||||
} from "./config-manager"
|
||||
import { restartNextServer } from "./next-server"
|
||||
|
||||
/**
|
||||
* Allowed configuration keys for presets
|
||||
* This whitelist prevents arbitrary environment variable injection
|
||||
*/
|
||||
const ALLOWED_CONFIG_KEYS = new Set([
|
||||
"AI_PROVIDER",
|
||||
"AI_MODEL",
|
||||
"AI_API_KEY",
|
||||
"AI_BASE_URL",
|
||||
"TEMPERATURE",
|
||||
])
|
||||
|
||||
/**
|
||||
* Sanitize preset config to only include allowed keys
|
||||
*/
|
||||
function sanitizePresetConfig(
|
||||
config: Record<string, string | undefined>,
|
||||
): Record<string, string | undefined> {
|
||||
const sanitized: Record<string, string | undefined> = {}
|
||||
for (const key of ALLOWED_CONFIG_KEYS) {
|
||||
if (key in config && typeof config[key] === "string") {
|
||||
sanitized[key] = config[key]
|
||||
}
|
||||
}
|
||||
return sanitized
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all IPC handlers
|
||||
*/
|
||||
export function registerIpcHandlers(): void {
|
||||
// ==================== App Info ====================
|
||||
|
||||
ipcMain.handle("get-version", () => {
|
||||
return app.getVersion()
|
||||
})
|
||||
|
||||
// ==================== Window Controls ====================
|
||||
|
||||
ipcMain.on("window-minimize", (event) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender)
|
||||
win?.minimize()
|
||||
})
|
||||
|
||||
ipcMain.on("window-maximize", (event) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender)
|
||||
if (win?.isMaximized()) {
|
||||
win.unmaximize()
|
||||
} else {
|
||||
win?.maximize()
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.on("window-close", (event) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender)
|
||||
win?.close()
|
||||
})
|
||||
|
||||
// ==================== File Dialogs ====================
|
||||
|
||||
ipcMain.handle("dialog-open-file", async (event) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender)
|
||||
if (!win) return null
|
||||
|
||||
const result = await dialog.showOpenDialog(win, {
|
||||
properties: ["openFile"],
|
||||
filters: [
|
||||
{ name: "Draw.io Files", extensions: ["drawio", "xml"] },
|
||||
{ name: "All Files", extensions: ["*"] },
|
||||
],
|
||||
})
|
||||
|
||||
if (result.canceled || result.filePaths.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Read the file content
|
||||
const fs = await import("node:fs/promises")
|
||||
try {
|
||||
const content = await fs.readFile(result.filePaths[0], "utf-8")
|
||||
return content
|
||||
} catch (error) {
|
||||
console.error("Failed to read file:", error)
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle("dialog-save-file", async (event, data: string) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender)
|
||||
if (!win) return false
|
||||
|
||||
const result = await dialog.showSaveDialog(win, {
|
||||
filters: [
|
||||
{ name: "Draw.io Files", extensions: ["drawio"] },
|
||||
{ name: "XML Files", extensions: ["xml"] },
|
||||
],
|
||||
})
|
||||
|
||||
if (result.canceled || !result.filePath) {
|
||||
return false
|
||||
}
|
||||
|
||||
const fs = await import("node:fs/promises")
|
||||
try {
|
||||
await fs.writeFile(result.filePath, data, "utf-8")
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error("Failed to save file:", error)
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
// ==================== Config Presets ====================
|
||||
|
||||
ipcMain.handle("config-presets:get-all", () => {
|
||||
return getAllPresets()
|
||||
})
|
||||
|
||||
ipcMain.handle("config-presets:get-current", () => {
|
||||
return getCurrentPreset()
|
||||
})
|
||||
|
||||
ipcMain.handle("config-presets:get-current-id", () => {
|
||||
return getCurrentPresetId()
|
||||
})
|
||||
|
||||
ipcMain.handle(
|
||||
"config-presets:save",
|
||||
(
|
||||
_event,
|
||||
preset: Omit<ConfigPreset, "id" | "createdAt" | "updatedAt"> & {
|
||||
id?: string
|
||||
},
|
||||
) => {
|
||||
// Validate preset name
|
||||
if (typeof preset.name !== "string" || !preset.name.trim()) {
|
||||
throw new Error("Invalid preset name")
|
||||
}
|
||||
|
||||
// Sanitize config to only allow whitelisted keys
|
||||
const sanitizedConfig = sanitizePresetConfig(preset.config ?? {})
|
||||
|
||||
if (preset.id) {
|
||||
// Update existing preset
|
||||
return updatePreset(preset.id, {
|
||||
name: preset.name.trim(),
|
||||
config: sanitizedConfig,
|
||||
})
|
||||
}
|
||||
// Create new preset
|
||||
return createPreset({
|
||||
name: preset.name.trim(),
|
||||
config: sanitizedConfig,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
ipcMain.handle("config-presets:delete", (_event, id: string) => {
|
||||
return deletePreset(id)
|
||||
})
|
||||
|
||||
ipcMain.handle("config-presets:apply", async (_event, id: string) => {
|
||||
const env = applyPresetToEnv(id)
|
||||
if (!env) {
|
||||
return { success: false, error: "Preset not found" }
|
||||
}
|
||||
|
||||
const isDev = process.env.NODE_ENV === "development"
|
||||
|
||||
if (isDev) {
|
||||
// In development mode, the config file change will trigger
|
||||
// the file watcher in electron-dev.mjs to restart Next.js
|
||||
// We just need to save the preset (already done in applyPresetToEnv)
|
||||
return { success: true, env, devMode: true }
|
||||
}
|
||||
|
||||
// Production mode: restart the Next.js server to apply new environment variables
|
||||
try {
|
||||
await restartNextServer()
|
||||
return { success: true, env }
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to restart server",
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(
|
||||
"config-presets:set-current",
|
||||
(_event, id: string | null) => {
|
||||
return setCurrentPreset(id)
|
||||
},
|
||||
)
|
||||
}
|
||||
161
electron/main/next-server.ts
Normal file
161
electron/main/next-server.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { existsSync } from "node:fs"
|
||||
import path from "node:path"
|
||||
import { app, type UtilityProcess, utilityProcess } from "electron"
|
||||
import {
|
||||
findAvailablePort,
|
||||
getAllocatedPort,
|
||||
getServerUrl,
|
||||
isPortAvailable,
|
||||
} from "./port-manager"
|
||||
|
||||
let serverProcess: UtilityProcess | null = null
|
||||
|
||||
/**
|
||||
* Get the path to the standalone server resources
|
||||
* In packaged app: resources/standalone
|
||||
* In development: .next/standalone
|
||||
*/
|
||||
function getResourcePath(): string {
|
||||
if (app.isPackaged) {
|
||||
return path.join(process.resourcesPath, "standalone")
|
||||
}
|
||||
return path.join(app.getAppPath(), ".next", "standalone")
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the server to be ready by polling the health endpoint
|
||||
*/
|
||||
async function waitForServer(url: string, timeout = 30000): Promise<void> {
|
||||
const start = Date.now()
|
||||
while (Date.now() - start < timeout) {
|
||||
try {
|
||||
const response = await fetch(url)
|
||||
if (response.ok || response.status < 500) {
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
// Server not ready yet
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
}
|
||||
throw new Error(`Server startup timeout after ${timeout}ms`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the Next.js standalone server using Electron's utilityProcess
|
||||
* This API is designed for running Node.js code in the background
|
||||
*/
|
||||
export async function startNextServer(): Promise<string> {
|
||||
const resourcePath = getResourcePath()
|
||||
const serverPath = path.join(resourcePath, "server.js")
|
||||
|
||||
console.log(`Starting Next.js server from: ${resourcePath}`)
|
||||
console.log(`Server script path: ${serverPath}`)
|
||||
|
||||
// Verify server script exists before attempting to start
|
||||
if (!existsSync(serverPath)) {
|
||||
throw new Error(
|
||||
`Server script not found at ${serverPath}. ` +
|
||||
"Please ensure the app was built correctly with 'npm run build'.",
|
||||
)
|
||||
}
|
||||
|
||||
// Find an available port (random in production, fixed in development)
|
||||
const port = await findAvailablePort()
|
||||
console.log(`Using port: ${port}`)
|
||||
|
||||
// Set up environment variables
|
||||
const env: Record<string, string> = {
|
||||
NODE_ENV: "production",
|
||||
PORT: String(port),
|
||||
HOSTNAME: "localhost",
|
||||
}
|
||||
|
||||
// Set cache directory to a writable location (user's app data folder)
|
||||
// This is necessary because the packaged app might be on a read-only volume
|
||||
if (app.isPackaged) {
|
||||
const cacheDir = path.join(app.getPath("userData"), "cache")
|
||||
env.NEXT_CACHE_DIR = cacheDir
|
||||
}
|
||||
|
||||
// Copy existing environment variables
|
||||
for (const [key, value] of Object.entries(process.env)) {
|
||||
if (value !== undefined && !env[key]) {
|
||||
env[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
// Use Electron's utilityProcess API for running Node.js in background
|
||||
// This is the recommended way to run Node.js code in Electron
|
||||
serverProcess = utilityProcess.fork(serverPath, [], {
|
||||
cwd: resourcePath,
|
||||
env,
|
||||
stdio: "pipe",
|
||||
})
|
||||
|
||||
serverProcess.stdout?.on("data", (data) => {
|
||||
console.log(`[Next.js] ${data.toString().trim()}`)
|
||||
})
|
||||
|
||||
serverProcess.stderr?.on("data", (data) => {
|
||||
console.error(`[Next.js Error] ${data.toString().trim()}`)
|
||||
})
|
||||
|
||||
serverProcess.on("exit", (code) => {
|
||||
console.log(`Next.js server exited with code ${code}`)
|
||||
serverProcess = null
|
||||
})
|
||||
|
||||
const url = getServerUrl()
|
||||
await waitForServer(url)
|
||||
console.log(`Next.js server started at ${url}`)
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the Next.js server process
|
||||
*/
|
||||
export function stopNextServer(): void {
|
||||
if (serverProcess) {
|
||||
console.log("Stopping Next.js server...")
|
||||
serverProcess.kill()
|
||||
serverProcess = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the server to fully stop
|
||||
*/
|
||||
async function waitForServerStop(timeout = 5000): Promise<void> {
|
||||
const port = getAllocatedPort()
|
||||
if (port === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const start = Date.now()
|
||||
while (Date.now() - start < timeout) {
|
||||
const available = await isPortAvailable(port)
|
||||
if (available) {
|
||||
return
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
}
|
||||
console.warn("Server stop timeout, port may still be in use")
|
||||
}
|
||||
|
||||
/**
|
||||
* Restart the Next.js server with new environment variables
|
||||
*/
|
||||
export async function restartNextServer(): Promise<string> {
|
||||
console.log("Restarting Next.js server...")
|
||||
|
||||
// Stop the current server
|
||||
stopNextServer()
|
||||
|
||||
// Wait for the port to be released
|
||||
await waitForServerStop()
|
||||
|
||||
// Start the server again
|
||||
return startNextServer()
|
||||
}
|
||||
129
electron/main/port-manager.ts
Normal file
129
electron/main/port-manager.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import net from "node:net"
|
||||
import { app } from "electron"
|
||||
|
||||
/**
|
||||
* Port configuration
|
||||
*/
|
||||
const PORT_CONFIG = {
|
||||
// Development mode uses fixed port for hot reload compatibility
|
||||
development: 6002,
|
||||
// Production mode port range (will find first available)
|
||||
production: {
|
||||
min: 10000,
|
||||
max: 65535,
|
||||
},
|
||||
// Maximum attempts to find an available port
|
||||
maxAttempts: 100,
|
||||
}
|
||||
|
||||
/**
|
||||
* Currently allocated port (cached after first allocation)
|
||||
*/
|
||||
let allocatedPort: number | null = null
|
||||
|
||||
/**
|
||||
* Check if a specific port is available
|
||||
*/
|
||||
export function isPortAvailable(port: number): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const server = net.createServer()
|
||||
server.once("error", () => resolve(false))
|
||||
server.once("listening", () => {
|
||||
server.close()
|
||||
resolve(true)
|
||||
})
|
||||
server.listen(port, "127.0.0.1")
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random port within the production range
|
||||
*/
|
||||
function getRandomPort(): number {
|
||||
const { min, max } = PORT_CONFIG.production
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an available port
|
||||
* - In development: uses fixed port (6002)
|
||||
* - In production: finds a random available port
|
||||
* - If a port was previously allocated, verifies it's still available
|
||||
*
|
||||
* @param reuseExisting If true, try to reuse the previously allocated port
|
||||
* @returns Promise<number> The available port
|
||||
* @throws Error if no available port found after max attempts
|
||||
*/
|
||||
export async function findAvailablePort(reuseExisting = true): Promise<number> {
|
||||
const isDev = !app.isPackaged
|
||||
|
||||
// Try to reuse cached port if requested and available
|
||||
if (reuseExisting && allocatedPort !== null) {
|
||||
const available = await isPortAvailable(allocatedPort)
|
||||
if (available) {
|
||||
return allocatedPort
|
||||
}
|
||||
console.warn(
|
||||
`Previously allocated port ${allocatedPort} is no longer available`,
|
||||
)
|
||||
allocatedPort = null
|
||||
}
|
||||
|
||||
if (isDev) {
|
||||
// Development mode: use fixed port
|
||||
const port = PORT_CONFIG.development
|
||||
const available = await isPortAvailable(port)
|
||||
if (available) {
|
||||
allocatedPort = port
|
||||
return port
|
||||
}
|
||||
console.warn(
|
||||
`Development port ${port} is in use, finding alternative...`,
|
||||
)
|
||||
}
|
||||
|
||||
// Production mode or dev port unavailable: find random available port
|
||||
for (let attempt = 0; attempt < PORT_CONFIG.maxAttempts; attempt++) {
|
||||
const port = isDev
|
||||
? PORT_CONFIG.development + attempt + 1
|
||||
: getRandomPort()
|
||||
|
||||
const available = await isPortAvailable(port)
|
||||
if (available) {
|
||||
allocatedPort = port
|
||||
console.log(`Allocated port: ${port}`)
|
||||
return port
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Failed to find available port after ${PORT_CONFIG.maxAttempts} attempts`,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently allocated port
|
||||
* Returns null if no port has been allocated yet
|
||||
*/
|
||||
export function getAllocatedPort(): number | null {
|
||||
return allocatedPort
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the allocated port (useful for testing or restart scenarios)
|
||||
*/
|
||||
export function resetAllocatedPort(): void {
|
||||
allocatedPort = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the server URL with the allocated port
|
||||
*/
|
||||
export function getServerUrl(): string {
|
||||
if (allocatedPort === null) {
|
||||
throw new Error(
|
||||
"No port allocated yet. Call findAvailablePort() first.",
|
||||
)
|
||||
}
|
||||
return `http://localhost:${allocatedPort}`
|
||||
}
|
||||
78
electron/main/settings-window.ts
Normal file
78
electron/main/settings-window.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import path from "node:path"
|
||||
import { app, BrowserWindow, ipcMain } from "electron"
|
||||
|
||||
let settingsWindow: BrowserWindow | null = null
|
||||
|
||||
/**
|
||||
* Create and show the settings window
|
||||
*/
|
||||
export function showSettingsWindow(parentWindow?: BrowserWindow): void {
|
||||
// If settings window already exists, focus it
|
||||
if (settingsWindow && !settingsWindow.isDestroyed()) {
|
||||
settingsWindow.focus()
|
||||
return
|
||||
}
|
||||
|
||||
// Determine path to settings preload script
|
||||
// In compiled output: dist-electron/preload/settings.js
|
||||
const preloadPath = path.join(__dirname, "..", "preload", "settings.js")
|
||||
|
||||
// Determine path to settings HTML
|
||||
// In packaged app: app.asar/dist-electron/settings/index.html
|
||||
// In development: electron/settings/index.html
|
||||
const settingsHtmlPath = app.isPackaged
|
||||
? path.join(__dirname, "..", "settings", "index.html")
|
||||
: path.join(__dirname, "..", "..", "electron", "settings", "index.html")
|
||||
|
||||
settingsWindow = new BrowserWindow({
|
||||
width: 600,
|
||||
height: 700,
|
||||
minWidth: 500,
|
||||
minHeight: 500,
|
||||
parent: parentWindow,
|
||||
modal: false,
|
||||
show: false,
|
||||
title: "Settings - Next AI Draw.io",
|
||||
webPreferences: {
|
||||
preload: preloadPath,
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
sandbox: true,
|
||||
},
|
||||
})
|
||||
settingsWindow.loadFile(settingsHtmlPath)
|
||||
|
||||
settingsWindow.once("ready-to-show", () => {
|
||||
settingsWindow?.show()
|
||||
})
|
||||
|
||||
settingsWindow.on("closed", () => {
|
||||
settingsWindow = null
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the settings window if it exists
|
||||
*/
|
||||
export function closeSettingsWindow(): void {
|
||||
if (settingsWindow && !settingsWindow.isDestroyed()) {
|
||||
settingsWindow.close()
|
||||
settingsWindow = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if settings window is open
|
||||
*/
|
||||
export function isSettingsWindowOpen(): boolean {
|
||||
return settingsWindow !== null && !settingsWindow.isDestroyed()
|
||||
}
|
||||
|
||||
/**
|
||||
* Register settings window IPC handlers
|
||||
*/
|
||||
export function registerSettingsWindowHandlers(): void {
|
||||
ipcMain.on("settings:close", () => {
|
||||
closeSettingsWindow()
|
||||
})
|
||||
}
|
||||
84
electron/main/window-manager.ts
Normal file
84
electron/main/window-manager.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import path from "node:path"
|
||||
import { app, BrowserWindow, screen } from "electron"
|
||||
|
||||
let mainWindow: BrowserWindow | null = null
|
||||
|
||||
/**
|
||||
* Get the icon path based on platform
|
||||
* Note: electron-builder converts icon.png during packaging,
|
||||
* but at runtime we use PNG directly - Electron handles it
|
||||
*/
|
||||
function getIconPath(): string | undefined {
|
||||
// macOS doesn't need explicit icon - it's embedded in the app bundle
|
||||
if (process.platform === "darwin" && app.isPackaged) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const iconName = "icon.png"
|
||||
|
||||
if (app.isPackaged) {
|
||||
return path.join(process.resourcesPath, iconName)
|
||||
}
|
||||
|
||||
// Development: use icon.png from resources
|
||||
return path.join(__dirname, "../../resources/icon.png")
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the main application window
|
||||
*/
|
||||
export function createWindow(serverUrl: string): BrowserWindow {
|
||||
const { width, height } = screen.getPrimaryDisplay().workAreaSize
|
||||
|
||||
mainWindow = new BrowserWindow({
|
||||
width: Math.min(1400, Math.floor(width * 0.9)),
|
||||
height: Math.min(900, Math.floor(height * 0.9)),
|
||||
minWidth: 800,
|
||||
minHeight: 600,
|
||||
title: "Next AI Draw.io",
|
||||
icon: getIconPath(),
|
||||
show: false, // Don't show until ready
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, "../preload/index.js"),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
sandbox: true,
|
||||
webSecurity: true,
|
||||
},
|
||||
})
|
||||
|
||||
// Load the Next.js application
|
||||
mainWindow.loadURL(serverUrl)
|
||||
|
||||
// Show window when ready to prevent flashing
|
||||
mainWindow.once("ready-to-show", () => {
|
||||
mainWindow?.show()
|
||||
})
|
||||
|
||||
// Open DevTools in development
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
mainWindow.webContents.openDevTools()
|
||||
}
|
||||
|
||||
mainWindow.on("closed", () => {
|
||||
mainWindow = null
|
||||
})
|
||||
|
||||
// Handle page title updates
|
||||
mainWindow.webContents.on("page-title-updated", (event, title) => {
|
||||
if (title && !title.includes("localhost")) {
|
||||
mainWindow?.setTitle(title)
|
||||
} else {
|
||||
event.preventDefault()
|
||||
}
|
||||
})
|
||||
|
||||
return mainWindow
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the main window instance
|
||||
*/
|
||||
export function getMainWindow(): BrowserWindow | null {
|
||||
return mainWindow
|
||||
}
|
||||
24
electron/preload/index.ts
Normal file
24
electron/preload/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { contextBridge, ipcRenderer } from "electron"
|
||||
|
||||
/**
|
||||
* Expose safe APIs to the renderer process
|
||||
*/
|
||||
contextBridge.exposeInMainWorld("electronAPI", {
|
||||
// Platform information
|
||||
platform: process.platform,
|
||||
|
||||
// Check if running in Electron
|
||||
isElectron: true,
|
||||
|
||||
// Application version
|
||||
getVersion: () => ipcRenderer.invoke("get-version"),
|
||||
|
||||
// Window controls (optional, for custom title bar)
|
||||
minimize: () => ipcRenderer.send("window-minimize"),
|
||||
maximize: () => ipcRenderer.send("window-maximize"),
|
||||
close: () => ipcRenderer.send("window-close"),
|
||||
|
||||
// File operations
|
||||
openFile: () => ipcRenderer.invoke("dialog-open-file"),
|
||||
saveFile: (data: string) => ipcRenderer.invoke("dialog-save-file", data),
|
||||
})
|
||||
35
electron/preload/settings.ts
Normal file
35
electron/preload/settings.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Preload script for settings window
|
||||
* Exposes APIs for managing configuration presets
|
||||
*/
|
||||
import { contextBridge, ipcRenderer } from "electron"
|
||||
|
||||
// Expose settings API to the renderer process
|
||||
contextBridge.exposeInMainWorld("settingsAPI", {
|
||||
// Get all presets
|
||||
getPresets: () => ipcRenderer.invoke("config-presets:get-all"),
|
||||
|
||||
// Get current preset ID
|
||||
getCurrentPresetId: () =>
|
||||
ipcRenderer.invoke("config-presets:get-current-id"),
|
||||
|
||||
// Get current preset
|
||||
getCurrentPreset: () => ipcRenderer.invoke("config-presets:get-current"),
|
||||
|
||||
// Save (create or update) a preset
|
||||
savePreset: (preset: {
|
||||
id?: string
|
||||
name: string
|
||||
config: Record<string, string | undefined>
|
||||
}) => ipcRenderer.invoke("config-presets:save", preset),
|
||||
|
||||
// Delete a preset
|
||||
deletePreset: (id: string) =>
|
||||
ipcRenderer.invoke("config-presets:delete", id),
|
||||
|
||||
// Apply a preset (sets environment variables and restarts server)
|
||||
applyPreset: (id: string) => ipcRenderer.invoke("config-presets:apply", id),
|
||||
|
||||
// Close settings window
|
||||
close: () => ipcRenderer.send("settings:close"),
|
||||
})
|
||||
110
electron/settings/index.html
Normal file
110
electron/settings/index.html
Normal file
@@ -0,0 +1,110 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self';">
|
||||
<title>Settings - Next AI Draw.io</title>
|
||||
<link rel="stylesheet" href="./settings.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Configuration Presets</h1>
|
||||
|
||||
<div class="section">
|
||||
<h2>Presets</h2>
|
||||
<div id="preset-list" class="preset-list">
|
||||
<!-- Presets will be loaded here -->
|
||||
</div>
|
||||
<button id="add-preset-btn" class="btn btn-primary">
|
||||
+ Add New Preset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add/Edit Preset Modal -->
|
||||
<div id="preset-modal" class="modal-overlay">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h3 id="modal-title">Add Preset</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="preset-form">
|
||||
<input type="hidden" id="preset-id">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="preset-name">Preset Name *</label>
|
||||
<input type="text" id="preset-name" required placeholder="e.g., Work, Personal, Testing">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="ai-provider">AI Provider</label>
|
||||
<select id="ai-provider">
|
||||
<option value="">-- Select Provider --</option>
|
||||
<option value="openai">OpenAI</option>
|
||||
<option value="anthropic">Anthropic (Claude)</option>
|
||||
<option value="google">Google AI (Gemini)</option>
|
||||
<option value="azure">Azure OpenAI</option>
|
||||
<option value="bedrock">AWS Bedrock</option>
|
||||
<option value="openrouter">OpenRouter</option>
|
||||
<option value="deepseek">DeepSeek</option>
|
||||
<option value="siliconflow">SiliconFlow</option>
|
||||
<option value="ollama">Ollama (Local)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="ai-model">Model ID</label>
|
||||
<input type="text" id="ai-model" placeholder="e.g., gpt-4o, claude-sonnet-4-5">
|
||||
<div class="hint">The model identifier to use with the selected provider</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="ai-api-key">API Key</label>
|
||||
<input type="password" id="ai-api-key" placeholder="Your API key">
|
||||
<div class="hint">This will be stored locally on your device</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="ai-base-url">Base URL (Optional)</label>
|
||||
<input type="text" id="ai-base-url" placeholder="https://api.example.com/v1">
|
||||
<div class="hint">Custom API endpoint URL</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="temperature">Temperature (Optional)</label>
|
||||
<input type="text" id="temperature" placeholder="0.7">
|
||||
<div class="hint">Controls randomness (0.0 - 2.0)</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" id="cancel-btn" class="btn btn-secondary">Cancel</button>
|
||||
<button type="button" id="save-btn" class="btn btn-primary">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div id="delete-modal" class="modal-overlay">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h3>Delete Preset</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Are you sure you want to delete "<span id="delete-preset-name"></span>"?</p>
|
||||
<p class="delete-warning">This action cannot be undone.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" id="delete-cancel-btn" class="btn btn-secondary">Cancel</button>
|
||||
<button type="button" id="delete-confirm-btn" class="btn btn-danger">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast notification -->
|
||||
<div id="toast" class="toast"></div>
|
||||
|
||||
<script src="./settings.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
311
electron/settings/settings.css
Normal file
311
electron/settings/settings.css
Normal file
@@ -0,0 +1,311 @@
|
||||
:root {
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #f5f5f5;
|
||||
--bg-hover: #e8e8e8;
|
||||
--text-primary: #1a1a1a;
|
||||
--text-secondary: #666666;
|
||||
--border-color: #e0e0e0;
|
||||
--accent-color: #0066cc;
|
||||
--accent-hover: #0052a3;
|
||||
--danger-color: #dc3545;
|
||||
--success-color: #28a745;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg-primary: #1a1a1a;
|
||||
--bg-secondary: #2d2d2d;
|
||||
--bg-hover: #3d3d3d;
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #a0a0a0;
|
||||
--border-color: #404040;
|
||||
--accent-color: #4da6ff;
|
||||
--accent-hover: #66b3ff;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
|
||||
sans-serif;
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 560px;
|
||||
margin: 0 auto;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.preset-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.preset-card {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.preset-card:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.preset-card.active {
|
||||
border-color: var(--accent-color);
|
||||
box-shadow: 0 0 0 1px var(--accent-color);
|
||||
}
|
||||
|
||||
.preset-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.preset-name {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.preset-badge {
|
||||
background: var(--accent-color);
|
||||
color: white;
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.preset-info {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.preset-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--accent-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: var(--danger-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 100;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-overlay.show {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: var(--bg-primary);
|
||||
border-radius: 12px;
|
||||
width: 90%;
|
||||
max-width: 480px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-color);
|
||||
box-shadow: 0 0 0 2px rgba(0, 102, 204, 0.2);
|
||||
}
|
||||
|
||||
.form-group .hint {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid var(--border-color);
|
||||
border-top-color: var(--accent-color);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: var(--text-primary);
|
||||
color: var(--bg-primary);
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
z-index: 200;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.toast.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.toast.success {
|
||||
background: var(--success-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.toast.error {
|
||||
background: var(--danger-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Inline style replacements */
|
||||
.delete-warning {
|
||||
color: var(--text-secondary);
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
311
electron/settings/settings.js
Normal file
311
electron/settings/settings.js
Normal file
@@ -0,0 +1,311 @@
|
||||
// Settings page JavaScript
|
||||
// This file handles the UI interactions for the settings window
|
||||
|
||||
let presets = []
|
||||
let currentPresetId = null
|
||||
let editingPresetId = null
|
||||
let deletingPresetId = null
|
||||
|
||||
// DOM Elements
|
||||
const presetList = document.getElementById("preset-list")
|
||||
const addPresetBtn = document.getElementById("add-preset-btn")
|
||||
const presetModal = document.getElementById("preset-modal")
|
||||
const deleteModal = document.getElementById("delete-modal")
|
||||
const presetForm = document.getElementById("preset-form")
|
||||
const modalTitle = document.getElementById("modal-title")
|
||||
const toast = document.getElementById("toast")
|
||||
|
||||
// Form fields
|
||||
const presetIdField = document.getElementById("preset-id")
|
||||
const presetNameField = document.getElementById("preset-name")
|
||||
const aiProviderField = document.getElementById("ai-provider")
|
||||
const aiModelField = document.getElementById("ai-model")
|
||||
const aiApiKeyField = document.getElementById("ai-api-key")
|
||||
const aiBaseUrlField = document.getElementById("ai-base-url")
|
||||
const temperatureField = document.getElementById("temperature")
|
||||
|
||||
// Buttons
|
||||
const cancelBtn = document.getElementById("cancel-btn")
|
||||
const saveBtn = document.getElementById("save-btn")
|
||||
const deleteCancelBtn = document.getElementById("delete-cancel-btn")
|
||||
const deleteConfirmBtn = document.getElementById("delete-confirm-btn")
|
||||
|
||||
// Initialize
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
await loadPresets()
|
||||
setupEventListeners()
|
||||
})
|
||||
|
||||
// Load presets from main process
|
||||
async function loadPresets() {
|
||||
try {
|
||||
presets = await window.settingsAPI.getPresets()
|
||||
currentPresetId = await window.settingsAPI.getCurrentPresetId()
|
||||
renderPresets()
|
||||
} catch (error) {
|
||||
console.error("Failed to load presets:", error)
|
||||
showToast("Failed to load presets", "error")
|
||||
}
|
||||
}
|
||||
|
||||
// Render presets list
|
||||
function renderPresets() {
|
||||
if (presets.length === 0) {
|
||||
presetList.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<p>No presets configured yet.</p>
|
||||
<p>Add a preset to quickly switch between different AI configurations.</p>
|
||||
</div>
|
||||
`
|
||||
return
|
||||
}
|
||||
|
||||
presetList.innerHTML = presets
|
||||
.map((preset) => {
|
||||
const isActive = preset.id === currentPresetId
|
||||
const providerLabel = getProviderLabel(preset.config.AI_PROVIDER)
|
||||
|
||||
return `
|
||||
<div class="preset-card ${isActive ? "active" : ""}" data-id="${preset.id}">
|
||||
<div class="preset-header">
|
||||
<span class="preset-name">${escapeHtml(preset.name)}</span>
|
||||
${isActive ? '<span class="preset-badge">Active</span>' : ""}
|
||||
</div>
|
||||
<div class="preset-info">
|
||||
${providerLabel ? `Provider: ${providerLabel}` : "No provider configured"}
|
||||
${preset.config.AI_MODEL ? ` • Model: ${escapeHtml(preset.config.AI_MODEL)}` : ""}
|
||||
</div>
|
||||
<div class="preset-actions">
|
||||
${!isActive ? `<button class="btn btn-primary btn-sm apply-btn" data-id="${preset.id}">Apply</button>` : ""}
|
||||
<button class="btn btn-secondary btn-sm edit-btn" data-id="${preset.id}">Edit</button>
|
||||
<button class="btn btn-secondary btn-sm delete-btn" data-id="${preset.id}">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
.join("")
|
||||
|
||||
// Add event listeners to buttons
|
||||
presetList.querySelectorAll(".apply-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", (e) => {
|
||||
e.stopPropagation()
|
||||
applyPreset(btn.dataset.id)
|
||||
})
|
||||
})
|
||||
|
||||
presetList.querySelectorAll(".edit-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", (e) => {
|
||||
e.stopPropagation()
|
||||
openEditModal(btn.dataset.id)
|
||||
})
|
||||
})
|
||||
|
||||
presetList.querySelectorAll(".delete-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", (e) => {
|
||||
e.stopPropagation()
|
||||
openDeleteModal(btn.dataset.id)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Setup event listeners
|
||||
function setupEventListeners() {
|
||||
addPresetBtn.addEventListener("click", () => openAddModal())
|
||||
cancelBtn.addEventListener("click", () => closeModal())
|
||||
saveBtn.addEventListener("click", () => savePreset())
|
||||
deleteCancelBtn.addEventListener("click", () => closeDeleteModal())
|
||||
deleteConfirmBtn.addEventListener("click", () => confirmDelete())
|
||||
|
||||
// Close modal on overlay click
|
||||
presetModal.addEventListener("click", (e) => {
|
||||
if (e.target === presetModal) closeModal()
|
||||
})
|
||||
deleteModal.addEventListener("click", (e) => {
|
||||
if (e.target === deleteModal) closeDeleteModal()
|
||||
})
|
||||
|
||||
// Handle Enter key in form
|
||||
presetForm.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault()
|
||||
savePreset()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Open add modal
|
||||
function openAddModal() {
|
||||
editingPresetId = null
|
||||
modalTitle.textContent = "Add Preset"
|
||||
presetForm.reset()
|
||||
presetIdField.value = ""
|
||||
presetModal.classList.add("show")
|
||||
presetNameField.focus()
|
||||
}
|
||||
|
||||
// Open edit modal
|
||||
function openEditModal(id) {
|
||||
const preset = presets.find((p) => p.id === id)
|
||||
if (!preset) return
|
||||
|
||||
editingPresetId = id
|
||||
modalTitle.textContent = "Edit Preset"
|
||||
|
||||
presetIdField.value = preset.id
|
||||
presetNameField.value = preset.name
|
||||
aiProviderField.value = preset.config.AI_PROVIDER || ""
|
||||
aiModelField.value = preset.config.AI_MODEL || ""
|
||||
aiApiKeyField.value = preset.config.AI_API_KEY || ""
|
||||
aiBaseUrlField.value = preset.config.AI_BASE_URL || ""
|
||||
temperatureField.value = preset.config.TEMPERATURE || ""
|
||||
|
||||
presetModal.classList.add("show")
|
||||
presetNameField.focus()
|
||||
}
|
||||
|
||||
// Close modal
|
||||
function closeModal() {
|
||||
presetModal.classList.remove("show")
|
||||
editingPresetId = null
|
||||
}
|
||||
|
||||
// Open delete modal
|
||||
function openDeleteModal(id) {
|
||||
const preset = presets.find((p) => p.id === id)
|
||||
if (!preset) return
|
||||
|
||||
deletingPresetId = id
|
||||
document.getElementById("delete-preset-name").textContent = preset.name
|
||||
deleteModal.classList.add("show")
|
||||
}
|
||||
|
||||
// Close delete modal
|
||||
function closeDeleteModal() {
|
||||
deleteModal.classList.remove("show")
|
||||
deletingPresetId = null
|
||||
}
|
||||
|
||||
// Save preset
|
||||
async function savePreset() {
|
||||
const name = presetNameField.value.trim()
|
||||
if (!name) {
|
||||
showToast("Please enter a preset name", "error")
|
||||
presetNameField.focus()
|
||||
return
|
||||
}
|
||||
|
||||
const preset = {
|
||||
id: editingPresetId || undefined,
|
||||
name: name,
|
||||
config: {
|
||||
AI_PROVIDER: aiProviderField.value || undefined,
|
||||
AI_MODEL: aiModelField.value.trim() || undefined,
|
||||
AI_API_KEY: aiApiKeyField.value.trim() || undefined,
|
||||
AI_BASE_URL: aiBaseUrlField.value.trim() || undefined,
|
||||
TEMPERATURE: temperatureField.value.trim() || undefined,
|
||||
},
|
||||
}
|
||||
|
||||
// Remove undefined values
|
||||
Object.keys(preset.config).forEach((key) => {
|
||||
if (preset.config[key] === undefined) {
|
||||
delete preset.config[key]
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
saveBtn.disabled = true
|
||||
saveBtn.innerHTML = '<span class="loading"></span>'
|
||||
|
||||
await window.settingsAPI.savePreset(preset)
|
||||
await loadPresets()
|
||||
closeModal()
|
||||
showToast(
|
||||
editingPresetId ? "Preset updated" : "Preset created",
|
||||
"success",
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Failed to save preset:", error)
|
||||
showToast("Failed to save preset", "error")
|
||||
} finally {
|
||||
saveBtn.disabled = false
|
||||
saveBtn.textContent = "Save"
|
||||
}
|
||||
}
|
||||
|
||||
// Confirm delete
|
||||
async function confirmDelete() {
|
||||
if (!deletingPresetId) return
|
||||
|
||||
try {
|
||||
deleteConfirmBtn.disabled = true
|
||||
deleteConfirmBtn.innerHTML = '<span class="loading"></span>'
|
||||
|
||||
await window.settingsAPI.deletePreset(deletingPresetId)
|
||||
await loadPresets()
|
||||
closeDeleteModal()
|
||||
showToast("Preset deleted", "success")
|
||||
} catch (error) {
|
||||
console.error("Failed to delete preset:", error)
|
||||
showToast("Failed to delete preset", "error")
|
||||
} finally {
|
||||
deleteConfirmBtn.disabled = false
|
||||
deleteConfirmBtn.textContent = "Delete"
|
||||
}
|
||||
}
|
||||
|
||||
// Apply preset
|
||||
async function applyPreset(id) {
|
||||
try {
|
||||
const btn = presetList.querySelector(`.apply-btn[data-id="${id}"]`)
|
||||
if (btn) {
|
||||
btn.disabled = true
|
||||
btn.innerHTML = '<span class="loading"></span>'
|
||||
}
|
||||
|
||||
const result = await window.settingsAPI.applyPreset(id)
|
||||
if (result.success) {
|
||||
currentPresetId = id
|
||||
renderPresets()
|
||||
showToast("Preset applied, server restarting...", "success")
|
||||
} else {
|
||||
showToast(result.error || "Failed to apply preset", "error")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to apply preset:", error)
|
||||
showToast("Failed to apply preset", "error")
|
||||
}
|
||||
}
|
||||
|
||||
// Get provider display label
|
||||
function getProviderLabel(provider) {
|
||||
const labels = {
|
||||
openai: "OpenAI",
|
||||
anthropic: "Anthropic",
|
||||
google: "Google AI",
|
||||
azure: "Azure OpenAI",
|
||||
bedrock: "AWS Bedrock",
|
||||
openrouter: "OpenRouter",
|
||||
deepseek: "DeepSeek",
|
||||
siliconflow: "SiliconFlow",
|
||||
ollama: "Ollama",
|
||||
}
|
||||
return labels[provider] || provider
|
||||
}
|
||||
|
||||
// Show toast notification
|
||||
function showToast(message, type = "") {
|
||||
toast.textContent = message
|
||||
toast.className = "toast show" + (type ? ` ${type}` : "")
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.remove("show")
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
// Escape HTML to prevent XSS
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement("div")
|
||||
div.textContent = text
|
||||
return div.innerHTML
|
||||
}
|
||||
18
electron/tsconfig.json
Normal file
18
electron/tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "node",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "../dist-electron",
|
||||
"rootDir": ".",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": false,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["./**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
19
env.example
19
env.example
@@ -1,6 +1,6 @@
|
||||
# AI Provider Configuration
|
||||
# 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
|
||||
AI_PROVIDER=bedrock
|
||||
|
||||
@@ -68,6 +68,17 @@ AI_MODEL=global.anthropic.claude-sonnet-4-5-20250929-v1:0
|
||||
# SILICONFLOW_API_KEY=sk-...
|
||||
# SILICONFLOW_BASE_URL=https://api.siliconflow.com/v1 # Optional: switch to https://api.siliconflow.cn/v1 if needed
|
||||
|
||||
# SGLang Configuration (OpenAI-compatible)
|
||||
# SGLANG_API_KEY=your-sglang-api-key
|
||||
# SGLANG_BASE_URL=http://127.0.0.1:8000/v1 # Your SGLang endpoint
|
||||
|
||||
# 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)
|
||||
# Enable LLM tracing and analytics - https://langfuse.com
|
||||
# LANGFUSE_PUBLIC_KEY=pk-lf-...
|
||||
@@ -86,6 +97,12 @@ AI_MODEL=global.anthropic.claude-sonnet-4-5-20250929-v1:0
|
||||
# NEXT_PUBLIC_DRAWIO_BASE_URL=https://embed.diagrams.net # Default: https://embed.diagrams.net
|
||||
# Use this to point to a self-hosted draw.io instance
|
||||
|
||||
# Subdirectory Deployment (Optional)
|
||||
# For deploying to a subdirectory (e.g., https://example.com/nextaidrawio)
|
||||
# Set this to your subdirectory path with leading slash (e.g., /nextaidrawio)
|
||||
# Leave empty for root deployment (default)
|
||||
# NEXT_PUBLIC_BASE_PATH=/nextaidrawio
|
||||
|
||||
# PDF Input Feature (Optional)
|
||||
# Enable PDF file upload to extract text and generate diagrams
|
||||
# Enabled by default. Set to "false" to disable.
|
||||
|
||||
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
|
||||
373
hooks/use-model-config.ts
Normal file
373
hooks/use-model-config.ts
Normal file
@@ -0,0 +1,373 @@
|
||||
"use client"
|
||||
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import { STORAGE_KEYS } from "@/lib/storage"
|
||||
import {
|
||||
createEmptyConfig,
|
||||
createModelConfig,
|
||||
createProviderConfig,
|
||||
type FlattenedModel,
|
||||
findModelById,
|
||||
flattenModels,
|
||||
type ModelConfig,
|
||||
type MultiModelConfig,
|
||||
PROVIDER_INFO,
|
||||
type ProviderConfig,
|
||||
type ProviderName,
|
||||
} from "@/lib/types/model-config"
|
||||
|
||||
// Old storage keys for migration
|
||||
const OLD_KEYS = {
|
||||
aiProvider: "next-ai-draw-io-ai-provider",
|
||||
aiBaseUrl: "next-ai-draw-io-ai-base-url",
|
||||
aiApiKey: "next-ai-draw-io-ai-api-key",
|
||||
aiModel: "next-ai-draw-io-ai-model",
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate from old single-provider format to new multi-model format
|
||||
*/
|
||||
function migrateOldConfig(): MultiModelConfig | null {
|
||||
if (typeof window === "undefined") return null
|
||||
|
||||
const oldProvider = localStorage.getItem(OLD_KEYS.aiProvider)
|
||||
const oldApiKey = localStorage.getItem(OLD_KEYS.aiApiKey)
|
||||
const oldModel = localStorage.getItem(OLD_KEYS.aiModel)
|
||||
|
||||
// No old config to migrate
|
||||
if (!oldProvider || !oldApiKey || !oldModel) return null
|
||||
|
||||
const oldBaseUrl = localStorage.getItem(OLD_KEYS.aiBaseUrl)
|
||||
|
||||
// Create new config from old format
|
||||
const provider = createProviderConfig(oldProvider as ProviderName)
|
||||
provider.apiKey = oldApiKey
|
||||
if (oldBaseUrl) provider.baseUrl = oldBaseUrl
|
||||
|
||||
const model = createModelConfig(oldModel)
|
||||
provider.models.push(model)
|
||||
|
||||
const config: MultiModelConfig = {
|
||||
version: 1,
|
||||
providers: [provider],
|
||||
selectedModelId: model.id,
|
||||
}
|
||||
|
||||
// Clear old keys after migration
|
||||
localStorage.removeItem(OLD_KEYS.aiProvider)
|
||||
localStorage.removeItem(OLD_KEYS.aiBaseUrl)
|
||||
localStorage.removeItem(OLD_KEYS.aiApiKey)
|
||||
localStorage.removeItem(OLD_KEYS.aiModel)
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
/**
|
||||
* Load config from localStorage
|
||||
*/
|
||||
function loadConfig(): MultiModelConfig {
|
||||
if (typeof window === "undefined") return createEmptyConfig()
|
||||
|
||||
// First, check if new format exists
|
||||
const stored = localStorage.getItem(STORAGE_KEYS.modelConfigs)
|
||||
if (stored) {
|
||||
try {
|
||||
return JSON.parse(stored) as MultiModelConfig
|
||||
} catch {
|
||||
console.error("Failed to parse model config")
|
||||
}
|
||||
}
|
||||
|
||||
// Try migration from old format
|
||||
const migrated = migrateOldConfig()
|
||||
if (migrated) {
|
||||
// Save migrated config
|
||||
localStorage.setItem(
|
||||
STORAGE_KEYS.modelConfigs,
|
||||
JSON.stringify(migrated),
|
||||
)
|
||||
return migrated
|
||||
}
|
||||
|
||||
return createEmptyConfig()
|
||||
}
|
||||
|
||||
/**
|
||||
* Save config to localStorage
|
||||
*/
|
||||
function saveConfig(config: MultiModelConfig): void {
|
||||
if (typeof window === "undefined") return
|
||||
localStorage.setItem(STORAGE_KEYS.modelConfigs, JSON.stringify(config))
|
||||
}
|
||||
|
||||
export interface UseModelConfigReturn {
|
||||
// State
|
||||
config: MultiModelConfig
|
||||
isLoaded: boolean
|
||||
|
||||
// Getters
|
||||
models: FlattenedModel[]
|
||||
selectedModel: FlattenedModel | undefined
|
||||
selectedModelId: string | undefined
|
||||
|
||||
// Actions
|
||||
setSelectedModelId: (modelId: string | undefined) => void
|
||||
addProvider: (provider: ProviderName) => ProviderConfig
|
||||
updateProvider: (
|
||||
providerId: string,
|
||||
updates: Partial<ProviderConfig>,
|
||||
) => void
|
||||
deleteProvider: (providerId: string) => void
|
||||
addModel: (providerId: string, modelId: string) => ModelConfig
|
||||
updateModel: (
|
||||
providerId: string,
|
||||
modelConfigId: string,
|
||||
updates: Partial<ModelConfig>,
|
||||
) => void
|
||||
deleteModel: (providerId: string, modelConfigId: string) => void
|
||||
resetConfig: () => void
|
||||
}
|
||||
|
||||
export function useModelConfig(): UseModelConfigReturn {
|
||||
const [config, setConfig] = useState<MultiModelConfig>(createEmptyConfig)
|
||||
const [isLoaded, setIsLoaded] = useState(false)
|
||||
|
||||
// Load config on mount
|
||||
useEffect(() => {
|
||||
const loaded = loadConfig()
|
||||
setConfig(loaded)
|
||||
setIsLoaded(true)
|
||||
}, [])
|
||||
|
||||
// Save config whenever it changes (after initial load)
|
||||
useEffect(() => {
|
||||
if (isLoaded) {
|
||||
saveConfig(config)
|
||||
}
|
||||
}, [config, isLoaded])
|
||||
|
||||
// Derived state
|
||||
const models = flattenModels(config)
|
||||
const selectedModel = config.selectedModelId
|
||||
? findModelById(config, config.selectedModelId)
|
||||
: undefined
|
||||
|
||||
// Actions
|
||||
const setSelectedModelId = useCallback((modelId: string | undefined) => {
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
selectedModelId: modelId,
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const addProvider = useCallback(
|
||||
(provider: ProviderName): ProviderConfig => {
|
||||
const newProvider = createProviderConfig(provider)
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
providers: [...prev.providers, newProvider],
|
||||
}))
|
||||
return newProvider
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const updateProvider = useCallback(
|
||||
(providerId: string, updates: Partial<ProviderConfig>) => {
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
providers: prev.providers.map((p) =>
|
||||
p.id === providerId ? { ...p, ...updates } : p,
|
||||
),
|
||||
}))
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const deleteProvider = useCallback((providerId: string) => {
|
||||
setConfig((prev) => {
|
||||
const provider = prev.providers.find((p) => p.id === providerId)
|
||||
const modelIds = provider?.models.map((m) => m.id) || []
|
||||
|
||||
// Clear selected model if it belongs to deleted provider
|
||||
const newSelectedId =
|
||||
prev.selectedModelId && modelIds.includes(prev.selectedModelId)
|
||||
? undefined
|
||||
: prev.selectedModelId
|
||||
|
||||
return {
|
||||
...prev,
|
||||
providers: prev.providers.filter((p) => p.id !== providerId),
|
||||
selectedModelId: newSelectedId,
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
const addModel = useCallback(
|
||||
(providerId: string, modelId: string): ModelConfig => {
|
||||
const newModel = createModelConfig(modelId)
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
providers: prev.providers.map((p) =>
|
||||
p.id === providerId
|
||||
? { ...p, models: [...p.models, newModel] }
|
||||
: p,
|
||||
),
|
||||
}))
|
||||
return newModel
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const updateModel = useCallback(
|
||||
(
|
||||
providerId: string,
|
||||
modelConfigId: string,
|
||||
updates: Partial<ModelConfig>,
|
||||
) => {
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
providers: prev.providers.map((p) =>
|
||||
p.id === providerId
|
||||
? {
|
||||
...p,
|
||||
models: p.models.map((m) =>
|
||||
m.id === modelConfigId
|
||||
? { ...m, ...updates }
|
||||
: m,
|
||||
),
|
||||
}
|
||||
: p,
|
||||
),
|
||||
}))
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const deleteModel = useCallback(
|
||||
(providerId: string, modelConfigId: string) => {
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
providers: prev.providers.map((p) =>
|
||||
p.id === providerId
|
||||
? {
|
||||
...p,
|
||||
models: p.models.filter(
|
||||
(m) => m.id !== modelConfigId,
|
||||
),
|
||||
}
|
||||
: p,
|
||||
),
|
||||
// Clear selected model if it was deleted
|
||||
selectedModelId:
|
||||
prev.selectedModelId === modelConfigId
|
||||
? undefined
|
||||
: prev.selectedModelId,
|
||||
}))
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const resetConfig = useCallback(() => {
|
||||
setConfig(createEmptyConfig())
|
||||
}, [])
|
||||
|
||||
return {
|
||||
config,
|
||||
isLoaded,
|
||||
models,
|
||||
selectedModel,
|
||||
selectedModelId: config.selectedModelId,
|
||||
setSelectedModelId,
|
||||
addProvider,
|
||||
updateProvider,
|
||||
deleteProvider,
|
||||
addModel,
|
||||
updateModel,
|
||||
deleteModel,
|
||||
resetConfig,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the AI config for the currently selected model.
|
||||
* Returns format compatible with existing getAIConfig() usage.
|
||||
*/
|
||||
export function getSelectedAIConfig(): {
|
||||
accessCode: string
|
||||
aiProvider: string
|
||||
aiBaseUrl: string
|
||||
aiApiKey: string
|
||||
aiModel: string
|
||||
// AWS Bedrock credentials
|
||||
awsAccessKeyId: string
|
||||
awsSecretAccessKey: string
|
||||
awsRegion: string
|
||||
awsSessionToken: string
|
||||
} {
|
||||
const empty = {
|
||||
accessCode: "",
|
||||
aiProvider: "",
|
||||
aiBaseUrl: "",
|
||||
aiApiKey: "",
|
||||
aiModel: "",
|
||||
awsAccessKeyId: "",
|
||||
awsSecretAccessKey: "",
|
||||
awsRegion: "",
|
||||
awsSessionToken: "",
|
||||
}
|
||||
|
||||
if (typeof window === "undefined") return empty
|
||||
|
||||
// Get access code (separate from model config)
|
||||
const accessCode = localStorage.getItem(STORAGE_KEYS.accessCode) || ""
|
||||
|
||||
// Load multi-model config
|
||||
const stored = localStorage.getItem(STORAGE_KEYS.modelConfigs)
|
||||
if (!stored) {
|
||||
// Fallback to old format for backward compatibility
|
||||
return {
|
||||
accessCode,
|
||||
aiProvider: localStorage.getItem(OLD_KEYS.aiProvider) || "",
|
||||
aiBaseUrl: localStorage.getItem(OLD_KEYS.aiBaseUrl) || "",
|
||||
aiApiKey: localStorage.getItem(OLD_KEYS.aiApiKey) || "",
|
||||
aiModel: localStorage.getItem(OLD_KEYS.aiModel) || "",
|
||||
// Old format didn't support AWS
|
||||
awsAccessKeyId: "",
|
||||
awsSecretAccessKey: "",
|
||||
awsRegion: "",
|
||||
awsSessionToken: "",
|
||||
}
|
||||
}
|
||||
|
||||
let config: MultiModelConfig
|
||||
try {
|
||||
config = JSON.parse(stored)
|
||||
} catch {
|
||||
return { ...empty, accessCode }
|
||||
}
|
||||
|
||||
// No selected model = use server default
|
||||
if (!config.selectedModelId) {
|
||||
return { ...empty, accessCode }
|
||||
}
|
||||
|
||||
// Find selected model
|
||||
const model = findModelById(config, config.selectedModelId)
|
||||
if (!model) {
|
||||
return { ...empty, accessCode }
|
||||
}
|
||||
|
||||
return {
|
||||
accessCode,
|
||||
aiProvider: model.provider,
|
||||
aiBaseUrl: model.baseUrl || "",
|
||||
aiApiKey: model.apiKey,
|
||||
aiModel: model.modelId,
|
||||
// AWS Bedrock credentials
|
||||
awsAccessKeyId: model.awsAccessKeyId || "",
|
||||
awsSecretAccessKey: model.awsSecretAccessKey || "",
|
||||
awsRegion: model.awsRegion || "",
|
||||
awsSessionToken: model.awsSessionToken || "",
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { createAmazonBedrock } from "@ai-sdk/amazon-bedrock"
|
||||
import { createAnthropic } from "@ai-sdk/anthropic"
|
||||
import { azure, createAzure } from "@ai-sdk/azure"
|
||||
import { createDeepSeek, deepseek } from "@ai-sdk/deepseek"
|
||||
import { createGateway, gateway } from "@ai-sdk/gateway"
|
||||
import { createGoogleGenerativeAI, google } from "@ai-sdk/google"
|
||||
import { createOpenAI, openai } from "@ai-sdk/openai"
|
||||
import { fromNodeProviderChain } from "@aws-sdk/credential-providers"
|
||||
@@ -18,6 +19,8 @@ export type ProviderName =
|
||||
| "openrouter"
|
||||
| "deepseek"
|
||||
| "siliconflow"
|
||||
| "sglang"
|
||||
| "gateway"
|
||||
|
||||
interface ModelConfig {
|
||||
model: any
|
||||
@@ -31,6 +34,11 @@ export interface ClientOverrides {
|
||||
baseUrl?: string | null
|
||||
apiKey?: string | null
|
||||
modelId?: string | null
|
||||
// AWS Bedrock credentials
|
||||
awsAccessKeyId?: string | null
|
||||
awsSecretAccessKey?: string | null
|
||||
awsRegion?: string | null
|
||||
awsSessionToken?: string | null
|
||||
}
|
||||
|
||||
// Providers that can be used with client-provided API keys
|
||||
@@ -39,9 +47,12 @@ const ALLOWED_CLIENT_PROVIDERS: ProviderName[] = [
|
||||
"anthropic",
|
||||
"google",
|
||||
"azure",
|
||||
"bedrock",
|
||||
"openrouter",
|
||||
"deepseek",
|
||||
"siliconflow",
|
||||
"sglang",
|
||||
"gateway",
|
||||
]
|
||||
|
||||
// Bedrock provider options for Anthropic beta features
|
||||
@@ -333,8 +344,11 @@ function buildProviderOptions(
|
||||
|
||||
case "deepseek":
|
||||
case "openrouter":
|
||||
case "siliconflow": {
|
||||
case "siliconflow":
|
||||
case "sglang":
|
||||
case "gateway": {
|
||||
// These providers don't have reasoning configs in AI SDK yet
|
||||
// Gateway passes through to underlying providers which handle their own configs
|
||||
break
|
||||
}
|
||||
|
||||
@@ -356,6 +370,8 @@ const PROVIDER_ENV_VARS: Record<ProviderName, string | null> = {
|
||||
openrouter: "OPENROUTER_API_KEY",
|
||||
deepseek: "DEEPSEEK_API_KEY",
|
||||
siliconflow: "SILICONFLOW_API_KEY",
|
||||
sglang: "SGLANG_API_KEY",
|
||||
gateway: "AI_GATEWAY_API_KEY",
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -420,7 +436,7 @@ function validateProviderCredentials(provider: ProviderName): void {
|
||||
* Get the AI model based on environment variables
|
||||
*
|
||||
* Environment variables:
|
||||
* - AI_PROVIDER: The provider to use (bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow)
|
||||
* - AI_PROVIDER: The provider to use (bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow, sglang, gateway)
|
||||
* - AI_MODEL: The model ID/name for the selected provider
|
||||
*
|
||||
* Provider-specific env vars:
|
||||
@@ -436,6 +452,8 @@ function validateProviderCredentials(provider: ProviderName): void {
|
||||
* - DEEPSEEK_BASE_URL: DeepSeek endpoint (optional)
|
||||
* - SILICONFLOW_API_KEY: SiliconFlow API key
|
||||
* - SILICONFLOW_BASE_URL: SiliconFlow endpoint (optional, defaults to https://api.siliconflow.com/v1)
|
||||
* - SGLANG_API_KEY: SGLang API key
|
||||
* - SGLANG_BASE_URL: SGLang endpoint (optional)
|
||||
*/
|
||||
export function getAIModel(overrides?: ClientOverrides): ModelConfig {
|
||||
// SECURITY: Prevent SSRF attacks (GHSA-9qf7-mprq-9qgm)
|
||||
@@ -495,6 +513,7 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
|
||||
if (configured.length === 0) {
|
||||
throw new Error(
|
||||
`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` +
|
||||
`- OPENAI_API_KEY for OpenAI\n` +
|
||||
`- ANTHROPIC_API_KEY for Anthropic\n` +
|
||||
@@ -503,6 +522,7 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
|
||||
`- OPENROUTER_API_KEY for OpenRouter\n` +
|
||||
`- AZURE_API_KEY for Azure\n` +
|
||||
`- SILICONFLOW_API_KEY for SiliconFlow\n` +
|
||||
`- SGLANG_API_KEY for SGLang\n` +
|
||||
`Or set AI_PROVIDER=ollama for local Ollama.`,
|
||||
)
|
||||
} else {
|
||||
@@ -530,12 +550,25 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
|
||||
|
||||
switch (provider) {
|
||||
case "bedrock": {
|
||||
// Use credential provider chain for IAM role support (Lambda, EC2, etc.)
|
||||
// Falls back to env vars (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY) for local dev
|
||||
const bedrockProvider = createAmazonBedrock({
|
||||
region: process.env.AWS_REGION || "us-west-2",
|
||||
credentialProvider: fromNodeProviderChain(),
|
||||
})
|
||||
// Use client-provided credentials if available, otherwise fall back to IAM/env vars
|
||||
const hasClientCredentials =
|
||||
overrides?.awsAccessKeyId && overrides?.awsSecretAccessKey
|
||||
const bedrockRegion =
|
||||
overrides?.awsRegion || process.env.AWS_REGION || "us-west-2"
|
||||
|
||||
const bedrockProvider = hasClientCredentials
|
||||
? createAmazonBedrock({
|
||||
region: bedrockRegion,
|
||||
accessKeyId: overrides.awsAccessKeyId!,
|
||||
secretAccessKey: overrides.awsSecretAccessKey!,
|
||||
...(overrides?.awsSessionToken && {
|
||||
sessionToken: overrides.awsSessionToken,
|
||||
}),
|
||||
})
|
||||
: createAmazonBedrock({
|
||||
region: bedrockRegion,
|
||||
credentialProvider: fromNodeProviderChain(),
|
||||
})
|
||||
model = bedrockProvider(modelId)
|
||||
// Add Anthropic beta options if using Claude models via Bedrock
|
||||
if (modelId.includes("anthropic.claude")) {
|
||||
@@ -672,9 +705,136 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
|
||||
break
|
||||
}
|
||||
|
||||
case "sglang": {
|
||||
const apiKey = overrides?.apiKey || process.env.SGLANG_API_KEY
|
||||
const baseURL = overrides?.baseUrl || process.env.SGLANG_BASE_URL
|
||||
|
||||
const sglangProvider = createOpenAI({
|
||||
apiKey,
|
||||
baseURL,
|
||||
// Add a custom fetch wrapper to intercept and fix the stream from sglang
|
||||
fetch: async (url, options) => {
|
||||
const response = await fetch(url, options)
|
||||
if (!response.body) {
|
||||
return response
|
||||
}
|
||||
|
||||
// Create a transform stream to fix the non-compliant sglang stream
|
||||
let buffer = ""
|
||||
const decoder = new TextDecoder()
|
||||
|
||||
const transformStream = new TransformStream({
|
||||
transform(chunk, controller) {
|
||||
buffer += decoder.decode(chunk, { stream: true })
|
||||
// Process all complete messages in the buffer
|
||||
let messageEndPos
|
||||
while (
|
||||
(messageEndPos = buffer.indexOf("\n\n")) !== -1
|
||||
) {
|
||||
const message = buffer.substring(
|
||||
0,
|
||||
messageEndPos,
|
||||
)
|
||||
buffer = buffer.substring(messageEndPos + 2) // Move past the '\n\n'
|
||||
|
||||
if (message.startsWith("data: ")) {
|
||||
const jsonStr = message.substring(6).trim()
|
||||
if (jsonStr === "[DONE]") {
|
||||
controller.enqueue(
|
||||
new TextEncoder().encode(
|
||||
message + "\n\n",
|
||||
),
|
||||
)
|
||||
continue
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(jsonStr)
|
||||
const delta = data.choices?.[0]?.delta
|
||||
|
||||
if (delta) {
|
||||
// Fix 1: remove invalid empty role
|
||||
if (delta.role === "") {
|
||||
delete delta.role
|
||||
}
|
||||
// Fix 2: remove non-standard reasoning_content field
|
||||
if ("reasoning_content" in delta) {
|
||||
delete delta.reasoning_content
|
||||
}
|
||||
}
|
||||
|
||||
// Re-serialize and forward the corrected data with the correct SSE format
|
||||
controller.enqueue(
|
||||
new TextEncoder().encode(
|
||||
`data: ${JSON.stringify(data)}\n\n`,
|
||||
),
|
||||
)
|
||||
} catch (e) {
|
||||
// If parsing fails, forward the original message to avoid breaking the stream.
|
||||
controller.enqueue(
|
||||
new TextEncoder().encode(
|
||||
message + "\n\n",
|
||||
),
|
||||
)
|
||||
}
|
||||
} else if (message.trim() !== "") {
|
||||
// Pass through other message types (e.g., 'event: ...')
|
||||
controller.enqueue(
|
||||
new TextEncoder().encode(
|
||||
message + "\n\n",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
flush(controller) {
|
||||
// If there's anything left in the buffer, forward it.
|
||||
if (buffer.trim()) {
|
||||
controller.enqueue(
|
||||
new TextEncoder().encode(buffer),
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const transformedBody =
|
||||
response.body.pipeThrough(transformStream)
|
||||
|
||||
// Return a new response with the transformed body
|
||||
return new Response(transformedBody, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: response.headers,
|
||||
})
|
||||
},
|
||||
})
|
||||
model = sglangProvider.chat(modelId)
|
||||
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:
|
||||
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, sglang, gateway`,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
37
lib/base-path.ts
Normal file
37
lib/base-path.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Get the base path for API calls and static assets
|
||||
* This is used for subdirectory deployment support
|
||||
*
|
||||
* Example: If deployed at https://example.com/nextaidrawio, this returns "/nextaidrawio"
|
||||
* For root deployment, this returns ""
|
||||
*
|
||||
* Set NEXT_PUBLIC_BASE_PATH environment variable to your subdirectory path (e.g., /nextaidrawio)
|
||||
*/
|
||||
export function getBasePath(): string {
|
||||
// Read from environment variable (must start with NEXT_PUBLIC_ to be available on client)
|
||||
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || ""
|
||||
if (basePath && !basePath.startsWith("/")) {
|
||||
console.warn("NEXT_PUBLIC_BASE_PATH should start with /")
|
||||
}
|
||||
return basePath
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full API endpoint URL
|
||||
* @param endpoint - API endpoint path (e.g., "/api/chat", "/api/config")
|
||||
* @returns Full API path with base path prefix
|
||||
*/
|
||||
export function getApiEndpoint(endpoint: string): string {
|
||||
const basePath = getBasePath()
|
||||
return `${basePath}${endpoint}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full static asset URL
|
||||
* @param assetPath - Asset path (e.g., "/example.png", "/chain-of-thought.txt")
|
||||
* @returns Full asset path with base path prefix
|
||||
*/
|
||||
export function getAssetUrl(assetPath: string): string {
|
||||
const basePath = getBasePath()
|
||||
return `${basePath}${assetPath}`
|
||||
}
|
||||
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]()
|
||||
}
|
||||
207
lib/i18n/dictionaries/en.json
Normal file
207
lib/i18n/dictionaries/en.json
Normal file
@@ -0,0 +1,207 @@
|
||||
{
|
||||
"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",
|
||||
"github": "GitHub",
|
||||
"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",
|
||||
"language": "Language",
|
||||
"languageDescription": "Choose your interface language.",
|
||||
"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.",
|
||||
"tpmMessageDetailed": "Rate limit reached ({limit} tokens/min). Please wait {seconds} seconds before sending another request.",
|
||||
"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."
|
||||
},
|
||||
"modelConfig": {
|
||||
"title": "AI Model Configuration",
|
||||
"description": "Configure multiple AI providers and models",
|
||||
"configure": "Configure",
|
||||
"addProvider": "Add Provider",
|
||||
"addModel": "Add Model",
|
||||
"modelId": "Model ID",
|
||||
"modelLabel": "Display Label",
|
||||
"streaming": "Enable Streaming",
|
||||
"deleteProvider": "Delete Provider",
|
||||
"deleteModel": "Delete Model",
|
||||
"noModels": "No models configured. Add a model to get started.",
|
||||
"selectProvider": "Select a provider or add a new one",
|
||||
"configureMultiple": "Configure multiple AI providers and switch between them easily",
|
||||
"apiKeyStored": "API keys are stored locally in your browser",
|
||||
"test": "Test",
|
||||
"validationError": "Validation failed",
|
||||
"addModelFirst": "Add at least one model to validate"
|
||||
}
|
||||
}
|
||||
207
lib/i18n/dictionaries/ja.json
Normal file
207
lib/i18n/dictionaries/ja.json
Normal file
@@ -0,0 +1,207 @@
|
||||
{
|
||||
"common": {
|
||||
"save": "保存",
|
||||
"cancel": "キャンセル",
|
||||
"close": "閉じる",
|
||||
"confirm": "確認",
|
||||
"clear": "クリア",
|
||||
"edit": "編集",
|
||||
"delete": "削除",
|
||||
"loading": "読み込み中..",
|
||||
"new": "新規"
|
||||
},
|
||||
"nav": {
|
||||
"about": "概要",
|
||||
"editor": "エディタ",
|
||||
"newChat": "新しいチャットを開始",
|
||||
"github": "GitHub",
|
||||
"settings": "設定",
|
||||
"hidePanel": "チャットパネルを非表示 (Ctrl+B)",
|
||||
"showPanel": "チャットパネルを表示 (Ctrl+B)",
|
||||
"aiChat": "AI チャット"
|
||||
},
|
||||
"providers": {
|
||||
"useServerDefault": "サーバーデフォルトを使用",
|
||||
"openai": "OpenAI",
|
||||
"anthropic": "Anthropic",
|
||||
"google": "Google",
|
||||
"azure": "Azure OpenAI",
|
||||
"openrouter": "OpenRouter",
|
||||
"deepseek": "DeepSeek",
|
||||
"siliconflow": "SiliconFlow"
|
||||
},
|
||||
"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": "サーバーデフォルトを使用",
|
||||
"language": "言語",
|
||||
"languageDescription": "インターフェース言語を選択します。",
|
||||
"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": "リクエストが多すぎます。しばらくお待ちください。",
|
||||
"tpmMessageDetailed": "レート制限に達しました({limit}トークン/分)。{seconds}秒待ってからもう一度リクエストしてください。",
|
||||
"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 に変更し、いくつかの利用制限を設けました。詳細は概要ページをご覧ください。"
|
||||
},
|
||||
"modelConfig": {
|
||||
"title": "AIモデル設定",
|
||||
"description": "複数のAIプロバイダーとモデルを設定",
|
||||
"configure": "設定",
|
||||
"addProvider": "プロバイダーを追加",
|
||||
"addModel": "モデルを追加",
|
||||
"modelId": "モデルID",
|
||||
"modelLabel": "表示名",
|
||||
"streaming": "ストリーミングを有効",
|
||||
"deleteProvider": "プロバイダーを削除",
|
||||
"deleteModel": "モデルを削除",
|
||||
"noModels": "モデルが設定されていません。モデルを追加してください。",
|
||||
"selectProvider": "プロバイダーを選択または追加してください",
|
||||
"configureMultiple": "複数のAIプロバイダーを設定して簡単に切り替え",
|
||||
"apiKeyStored": "APIキーはブラウザにローカル保存されます",
|
||||
"test": "テスト",
|
||||
"validationError": "検証に失敗しました",
|
||||
"addModelFirst": "検証するには少なくとも1つのモデルを追加してください"
|
||||
}
|
||||
}
|
||||
207
lib/i18n/dictionaries/zh.json
Normal file
207
lib/i18n/dictionaries/zh.json
Normal file
@@ -0,0 +1,207 @@
|
||||
{
|
||||
"common": {
|
||||
"save": "保存",
|
||||
"cancel": "取消",
|
||||
"close": "关闭",
|
||||
"confirm": "确认",
|
||||
"clear": "清除",
|
||||
"edit": "编辑",
|
||||
"delete": "删除",
|
||||
"loading": "加载中...",
|
||||
"new": "新建"
|
||||
},
|
||||
"nav": {
|
||||
"about": "关于",
|
||||
"editor": "编辑器",
|
||||
"newChat": "开始新对话",
|
||||
"github": "GitHub",
|
||||
"settings": "设置",
|
||||
"hidePanel": "隐藏聊天面板 (Ctrl+B)",
|
||||
"showPanel": "显示聊天面板 (Ctrl+B)",
|
||||
"aiChat": "AI 聊天"
|
||||
},
|
||||
"providers": {
|
||||
"useServerDefault": "使用服务器默认值",
|
||||
"openai": "OpenAI",
|
||||
"anthropic": "Anthropic",
|
||||
"google": "Google",
|
||||
"azure": "Azure OpenAI",
|
||||
"openrouter": "OpenRouter",
|
||||
"deepseek": "DeepSeek",
|
||||
"siliconflow": "SiliconFlow"
|
||||
},
|
||||
"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": "使用服务器默认值",
|
||||
"language": "语言",
|
||||
"languageDescription": "选择界面语言。",
|
||||
"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": "请求过多。请稍等片刻。",
|
||||
"tpmMessageDetailed": "达到速率限制({limit} 令牌/分钟)。请等待 {seconds} 秒后再发送请求。",
|
||||
"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,并设置了一些用量限制。详情请查看关于页面。"
|
||||
},
|
||||
"modelConfig": {
|
||||
"title": "AI 模型配置",
|
||||
"description": "配置多个 AI 提供商和模型",
|
||||
"configure": "配置",
|
||||
"addProvider": "添加提供商",
|
||||
"addModel": "添加模型",
|
||||
"modelId": "模型 ID",
|
||||
"modelLabel": "显示名称",
|
||||
"streaming": "启用流式输出",
|
||||
"deleteProvider": "删除提供商",
|
||||
"deleteModel": "删除模型",
|
||||
"noModels": "尚未配置模型。添加模型以开始使用。",
|
||||
"selectProvider": "选择一个提供商或添加新的",
|
||||
"configureMultiple": "配置多个 AI 提供商并轻松切换",
|
||||
"apiKeyStored": "API 密钥存储在您的浏览器本地",
|
||||
"test": "测试",
|
||||
"validationError": "验证失败",
|
||||
"addModelFirst": "请先添加至少一个模型以进行验证"
|
||||
}
|
||||
}
|
||||
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
|
||||
@@ -24,4 +24,8 @@ export const STORAGE_KEYS = {
|
||||
aiBaseUrl: "next-ai-draw-io-ai-base-url",
|
||||
aiApiKey: "next-ai-draw-io-ai-api-key",
|
||||
aiModel: "next-ai-draw-io-ai-model",
|
||||
|
||||
// Multi-model configuration
|
||||
modelConfigs: "next-ai-draw-io-model-configs",
|
||||
selectedModelId: "next-ai-draw-io-selected-model-id",
|
||||
} as const
|
||||
|
||||
@@ -48,12 +48,19 @@ description: Continue generating diagram XML when display_diagram was truncated
|
||||
parameters: {
|
||||
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---
|
||||
|
||||
IMPORTANT: Choose the right tool:
|
||||
- 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 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:
|
||||
- Generate valid, well-formed XML strings for draw.io diagrams
|
||||
@@ -84,7 +91,7 @@ Note that:
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
When using edit_diagram tool:
|
||||
|
||||
277
lib/types/model-config.ts
Normal file
277
lib/types/model-config.ts
Normal file
@@ -0,0 +1,277 @@
|
||||
// Types for multi-provider model configuration
|
||||
|
||||
export type ProviderName =
|
||||
| "openai"
|
||||
| "anthropic"
|
||||
| "google"
|
||||
| "azure"
|
||||
| "bedrock"
|
||||
| "openrouter"
|
||||
| "deepseek"
|
||||
| "siliconflow"
|
||||
| "gateway"
|
||||
|
||||
// Individual model configuration
|
||||
export interface ModelConfig {
|
||||
id: string // UUID for this model
|
||||
modelId: string // e.g., "gpt-4o", "claude-sonnet-4-5"
|
||||
validated?: boolean // Has this model been validated
|
||||
validationError?: string // Error message if validation failed
|
||||
}
|
||||
|
||||
// Provider configuration
|
||||
export interface ProviderConfig {
|
||||
id: string // UUID for this provider config
|
||||
provider: ProviderName
|
||||
name?: string // Custom display name (e.g., "OpenAI Production")
|
||||
apiKey: string
|
||||
baseUrl?: string
|
||||
// AWS Bedrock specific fields
|
||||
awsAccessKeyId?: string
|
||||
awsSecretAccessKey?: string
|
||||
awsRegion?: string
|
||||
awsSessionToken?: string // Optional, for temporary credentials
|
||||
models: ModelConfig[]
|
||||
validated?: boolean // Has API key been validated
|
||||
}
|
||||
|
||||
// The complete multi-model configuration
|
||||
export interface MultiModelConfig {
|
||||
version: 1
|
||||
providers: ProviderConfig[]
|
||||
selectedModelId?: string // Currently selected model's UUID
|
||||
}
|
||||
|
||||
// Flattened model for dropdown display
|
||||
export interface FlattenedModel {
|
||||
id: string // Model config UUID
|
||||
modelId: string // Actual model ID
|
||||
provider: ProviderName
|
||||
providerLabel: string // Provider display name
|
||||
apiKey: string
|
||||
baseUrl?: string
|
||||
// AWS Bedrock specific fields
|
||||
awsAccessKeyId?: string
|
||||
awsSecretAccessKey?: string
|
||||
awsRegion?: string
|
||||
awsSessionToken?: string
|
||||
validated?: boolean // Has this model been validated
|
||||
}
|
||||
|
||||
// Provider metadata
|
||||
export const PROVIDER_INFO: Record<
|
||||
ProviderName,
|
||||
{ label: string; defaultBaseUrl?: string }
|
||||
> = {
|
||||
openai: { label: "OpenAI" },
|
||||
anthropic: {
|
||||
label: "Anthropic",
|
||||
defaultBaseUrl: "https://api.anthropic.com/v1",
|
||||
},
|
||||
google: { label: "Google" },
|
||||
azure: { label: "Azure OpenAI" },
|
||||
bedrock: { label: "Amazon Bedrock" },
|
||||
openrouter: { label: "OpenRouter" },
|
||||
deepseek: { label: "DeepSeek" },
|
||||
siliconflow: {
|
||||
label: "SiliconFlow",
|
||||
defaultBaseUrl: "https://api.siliconflow.com/v1",
|
||||
},
|
||||
gateway: { label: "AI Gateway" },
|
||||
}
|
||||
|
||||
// Suggested models per provider for quick add
|
||||
export const SUGGESTED_MODELS: Record<ProviderName, string[]> = {
|
||||
openai: [
|
||||
// GPT-4o series (latest)
|
||||
"gpt-4o",
|
||||
"gpt-4o-mini",
|
||||
"gpt-4o-2024-11-20",
|
||||
// GPT-4 Turbo
|
||||
"gpt-4-turbo",
|
||||
"gpt-4-turbo-preview",
|
||||
// o1/o3 reasoning models
|
||||
"o1",
|
||||
"o1-mini",
|
||||
"o1-preview",
|
||||
"o3-mini",
|
||||
// GPT-4
|
||||
"gpt-4",
|
||||
// GPT-3.5
|
||||
"gpt-3.5-turbo",
|
||||
],
|
||||
anthropic: [
|
||||
// Claude 4.5 series (latest)
|
||||
"claude-opus-4-5-20250514",
|
||||
"claude-sonnet-4-5-20250514",
|
||||
// Claude 4 series
|
||||
"claude-opus-4-20250514",
|
||||
"claude-sonnet-4-20250514",
|
||||
// Claude 3.7 series
|
||||
"claude-3-7-sonnet-20250219",
|
||||
// Claude 3.5 series
|
||||
"claude-3-5-sonnet-20241022",
|
||||
"claude-3-5-haiku-20241022",
|
||||
// Claude 3 series
|
||||
"claude-3-opus-20240229",
|
||||
"claude-3-sonnet-20240229",
|
||||
"claude-3-haiku-20240307",
|
||||
],
|
||||
google: [
|
||||
// Gemini 2.5 series
|
||||
"gemini-2.5-pro",
|
||||
"gemini-2.5-flash",
|
||||
"gemini-2.5-flash-preview-05-20",
|
||||
// Gemini 2.0 series
|
||||
"gemini-2.0-flash",
|
||||
"gemini-2.0-flash-exp",
|
||||
"gemini-2.0-flash-lite",
|
||||
// Gemini 1.5 series
|
||||
"gemini-1.5-pro",
|
||||
"gemini-1.5-flash",
|
||||
// Legacy
|
||||
"gemini-pro",
|
||||
],
|
||||
azure: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "gpt-4", "gpt-35-turbo"],
|
||||
bedrock: [
|
||||
// Anthropic Claude
|
||||
"anthropic.claude-opus-4-5-20250514-v1:0",
|
||||
"anthropic.claude-sonnet-4-5-20250514-v1:0",
|
||||
"anthropic.claude-opus-4-20250514-v1:0",
|
||||
"anthropic.claude-sonnet-4-20250514-v1:0",
|
||||
"anthropic.claude-3-7-sonnet-20250219-v1:0",
|
||||
"anthropic.claude-3-5-sonnet-20241022-v2:0",
|
||||
"anthropic.claude-3-5-haiku-20241022-v1:0",
|
||||
"anthropic.claude-3-opus-20240229-v1:0",
|
||||
"anthropic.claude-3-sonnet-20240229-v1:0",
|
||||
"anthropic.claude-3-haiku-20240307-v1:0",
|
||||
// Amazon Nova
|
||||
"amazon.nova-pro-v1:0",
|
||||
"amazon.nova-lite-v1:0",
|
||||
"amazon.nova-micro-v1:0",
|
||||
// Meta Llama
|
||||
"meta.llama3-3-70b-instruct-v1:0",
|
||||
"meta.llama3-1-405b-instruct-v1:0",
|
||||
"meta.llama3-1-70b-instruct-v1:0",
|
||||
// Mistral
|
||||
"mistral.mistral-large-2411-v1:0",
|
||||
"mistral.mistral-small-2503-v1:0",
|
||||
],
|
||||
openrouter: [
|
||||
// Anthropic
|
||||
"anthropic/claude-sonnet-4",
|
||||
"anthropic/claude-opus-4",
|
||||
"anthropic/claude-3.5-sonnet",
|
||||
"anthropic/claude-3.5-haiku",
|
||||
// OpenAI
|
||||
"openai/gpt-4o",
|
||||
"openai/gpt-4o-mini",
|
||||
"openai/o1",
|
||||
"openai/o3-mini",
|
||||
// Google
|
||||
"google/gemini-2.5-pro",
|
||||
"google/gemini-2.5-flash",
|
||||
"google/gemini-2.0-flash-exp:free",
|
||||
// Meta Llama
|
||||
"meta-llama/llama-3.3-70b-instruct",
|
||||
"meta-llama/llama-3.1-405b-instruct",
|
||||
"meta-llama/llama-3.1-70b-instruct",
|
||||
// DeepSeek
|
||||
"deepseek/deepseek-chat",
|
||||
"deepseek/deepseek-r1",
|
||||
// Qwen
|
||||
"qwen/qwen-2.5-72b-instruct",
|
||||
],
|
||||
deepseek: ["deepseek-chat", "deepseek-reasoner", "deepseek-coder"],
|
||||
siliconflow: [
|
||||
// DeepSeek
|
||||
"deepseek-ai/DeepSeek-V3",
|
||||
"deepseek-ai/DeepSeek-R1",
|
||||
"deepseek-ai/DeepSeek-V2.5",
|
||||
// Qwen
|
||||
"Qwen/Qwen2.5-72B-Instruct",
|
||||
"Qwen/Qwen2.5-32B-Instruct",
|
||||
"Qwen/Qwen2.5-Coder-32B-Instruct",
|
||||
"Qwen/Qwen2.5-7B-Instruct",
|
||||
"Qwen/Qwen2-VL-72B-Instruct",
|
||||
],
|
||||
gateway: [
|
||||
"openai/gpt-4o",
|
||||
"openai/gpt-4o-mini",
|
||||
"anthropic/claude-sonnet-4-5",
|
||||
"anthropic/claude-3-5-sonnet",
|
||||
"google/gemini-2.0-flash",
|
||||
],
|
||||
}
|
||||
|
||||
// Helper to generate UUID
|
||||
export function generateId(): string {
|
||||
return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
|
||||
}
|
||||
|
||||
// Create empty config
|
||||
export function createEmptyConfig(): MultiModelConfig {
|
||||
return {
|
||||
version: 1,
|
||||
providers: [],
|
||||
selectedModelId: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
// Create new provider config
|
||||
export function createProviderConfig(provider: ProviderName): ProviderConfig {
|
||||
return {
|
||||
id: generateId(),
|
||||
provider,
|
||||
apiKey: "",
|
||||
baseUrl: PROVIDER_INFO[provider].defaultBaseUrl,
|
||||
models: [],
|
||||
validated: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Create new model config
|
||||
export function createModelConfig(modelId: string): ModelConfig {
|
||||
return {
|
||||
id: generateId(),
|
||||
modelId,
|
||||
}
|
||||
}
|
||||
|
||||
// Get all models as flattened list for dropdown
|
||||
export function flattenModels(config: MultiModelConfig): FlattenedModel[] {
|
||||
const models: FlattenedModel[] = []
|
||||
|
||||
for (const provider of config.providers) {
|
||||
// Use custom name if provided, otherwise use default provider label
|
||||
const providerLabel =
|
||||
provider.name || PROVIDER_INFO[provider.provider].label
|
||||
|
||||
for (const model of provider.models) {
|
||||
models.push({
|
||||
id: model.id,
|
||||
modelId: model.modelId,
|
||||
provider: provider.provider,
|
||||
providerLabel,
|
||||
apiKey: provider.apiKey,
|
||||
baseUrl: provider.baseUrl,
|
||||
// AWS Bedrock fields
|
||||
awsAccessKeyId: provider.awsAccessKeyId,
|
||||
awsSecretAccessKey: provider.awsSecretAccessKey,
|
||||
awsRegion: provider.awsRegion,
|
||||
awsSessionToken: provider.awsSessionToken,
|
||||
validated: model.validated,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return models
|
||||
}
|
||||
|
||||
// Find model by ID
|
||||
export function findModelById(
|
||||
config: MultiModelConfig,
|
||||
modelId: string,
|
||||
): FlattenedModel | undefined {
|
||||
return flattenModels(config).find((m) => m.id === modelId)
|
||||
}
|
||||
@@ -3,6 +3,8 @@
|
||||
import { useCallback, useMemo } from "react"
|
||||
import { toast } from "sonner"
|
||||
import { QuotaLimitToast } from "@/components/quota-limit-toast"
|
||||
import { useDictionary } from "@/hooks/use-dictionary"
|
||||
import { formatMessage } from "@/lib/i18n/utils"
|
||||
import { STORAGE_KEYS } from "@/lib/storage"
|
||||
|
||||
export interface QuotaConfig {
|
||||
@@ -40,6 +42,8 @@ export function useQuotaManager(config: QuotaConfig): {
|
||||
} {
|
||||
const { dailyRequestLimit, dailyTokenLimit, tpmLimit } = config
|
||||
|
||||
const dict = useDictionary()
|
||||
|
||||
// Check if user has their own API key configured (bypass limits)
|
||||
const hasOwnApiKey = useCallback((): boolean => {
|
||||
const provider = localStorage.getItem(STORAGE_KEYS.aiProvider)
|
||||
@@ -221,11 +225,12 @@ export function useQuotaManager(config: QuotaConfig): {
|
||||
const showTPMLimitToast = useCallback(() => {
|
||||
const limitDisplay =
|
||||
tpmLimit >= 1000 ? `${tpmLimit / 1000}k` : String(tpmLimit)
|
||||
toast.error(
|
||||
`Rate limit reached (${limitDisplay} tokens/min). Please wait 60 seconds before sending another request.`,
|
||||
{ duration: 8000 },
|
||||
)
|
||||
}, [tpmLimit])
|
||||
const message = formatMessage(dict.quota.tpmMessageDetailed, {
|
||||
limit: limitDisplay,
|
||||
seconds: 60,
|
||||
})
|
||||
toast.error(message, { duration: 8000 })
|
||||
}, [tpmLimit, dict])
|
||||
|
||||
return {
|
||||
// Check functions
|
||||
|
||||
51
lib/utils.ts
51
lib/utils.ts
@@ -1054,7 +1054,31 @@ export function autoFixXml(xml: string): { fixed: string; fixes: string[] } {
|
||||
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
|
||||
const validDrawioTags = new Set([
|
||||
"mxfile",
|
||||
@@ -1079,7 +1103,7 @@ export function autoFixXml(xml: string): { fixed: string; fixes: string[] } {
|
||||
}
|
||||
if (foreignTags.size > 0) {
|
||||
console.log(
|
||||
"[autoFixXml] Step 8b: Found foreign tags:",
|
||||
"[autoFixXml] Step 8c: Found foreign tags:",
|
||||
Array.from(foreignTags),
|
||||
)
|
||||
for (const tag of foreignTags) {
|
||||
@@ -1093,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
|
||||
// Use parseXmlTags helper to track open tags
|
||||
const tagStack: string[] = []
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
import type { NextConfig } from "next"
|
||||
import packageJson from "./package.json"
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
output: "standalone",
|
||||
// Support for subdirectory deployment (e.g., https://example.com/nextaidrawio)
|
||||
// Set NEXT_PUBLIC_BASE_PATH environment variable to your subdirectory path (e.g., /nextaidrawio)
|
||||
basePath: process.env.NEXT_PUBLIC_BASE_PATH || "",
|
||||
env: {
|
||||
APP_VERSION: packageJson.version,
|
||||
},
|
||||
// Include instrumentation.ts in standalone build for Langfuse telemetry
|
||||
outputFileTracingIncludes: {
|
||||
"*": ["./instrumentation.ts"],
|
||||
},
|
||||
}
|
||||
|
||||
export default nextConfig
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user