mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-02 14:22:28 +08:00
Compare commits
2 Commits
7bdc1fe612
...
493ee168b1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
493ee168b1 | ||
|
|
037f32973a |
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.3.8/schema.json",
|
||||
"$schema": "https://biomejs.dev/schemas/2.3.10/schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
|
||||
@@ -283,7 +283,7 @@ export function ChatMessageDisplay({
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
setCopyState(messageId, isToolCall, true)
|
||||
} catch (err) {
|
||||
} catch (_err) {
|
||||
// Fallback for non-secure contexts (HTTP) or permission denied
|
||||
const textarea = document.createElement("textarea")
|
||||
textarea.value = text
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
Zap,
|
||||
} from "lucide-react"
|
||||
import { useCallback, useEffect, useRef, useState } from "react"
|
||||
import { toast } from "sonner"
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -517,7 +516,6 @@ export function ModelConfigDialog({
|
||||
{/* Provider Details (Right Panel) */}
|
||||
<div className="flex-1 min-w-0 flex flex-col overflow-auto [&::-webkit-scrollbar]:hidden ">
|
||||
{selectedProvider ? (
|
||||
<>
|
||||
<ScrollArea className="flex-1" ref={scrollRef}>
|
||||
<div className="p-6 space-y-8">
|
||||
{/* Provider Header */}
|
||||
@@ -559,10 +557,7 @@ export function ModelConfigDialog({
|
||||
<div className="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-success-muted text-success">
|
||||
<Check className="h-3.5 w-3.5 animate-check-pop" />
|
||||
<span className="text-xs font-medium">
|
||||
{
|
||||
dict.modelConfig
|
||||
.verified
|
||||
}
|
||||
{dict.modelConfig.verified}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -575,18 +570,13 @@ export function ModelConfigDialog({
|
||||
className="text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-1.5" />
|
||||
{
|
||||
dict.modelConfig
|
||||
.deleteProvider
|
||||
}
|
||||
{dict.modelConfig.deleteProvider}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Configuration Section */}
|
||||
<ConfigSection
|
||||
title={
|
||||
dict.modelConfig.configuration
|
||||
}
|
||||
title={dict.modelConfig.configuration}
|
||||
icon={Settings2}
|
||||
>
|
||||
<ConfigCard>
|
||||
@@ -636,8 +626,7 @@ export function ModelConfigDialog({
|
||||
>
|
||||
<Key className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
{
|
||||
dict
|
||||
.modelConfig
|
||||
dict.modelConfig
|
||||
.awsAccessKeyId
|
||||
}
|
||||
</Label>
|
||||
@@ -672,8 +661,7 @@ export function ModelConfigDialog({
|
||||
>
|
||||
<Key className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
{
|
||||
dict
|
||||
.modelConfig
|
||||
dict.modelConfig
|
||||
.awsSecretAccessKey
|
||||
}
|
||||
</Label>
|
||||
@@ -689,13 +677,10 @@ export function ModelConfigDialog({
|
||||
selectedProvider.awsSecretAccessKey ||
|
||||
""
|
||||
}
|
||||
onChange={(
|
||||
e,
|
||||
) =>
|
||||
onChange={(e) =>
|
||||
handleProviderUpdate(
|
||||
"awsSecretAccessKey",
|
||||
e
|
||||
.target
|
||||
e.target
|
||||
.value,
|
||||
)
|
||||
}
|
||||
@@ -737,8 +722,7 @@ export function ModelConfigDialog({
|
||||
>
|
||||
<Link2 className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
{
|
||||
dict
|
||||
.modelConfig
|
||||
dict.modelConfig
|
||||
.awsRegion
|
||||
}
|
||||
</Label>
|
||||
@@ -817,8 +801,7 @@ export function ModelConfigDialog({
|
||||
</SelectItem>
|
||||
<SelectItem value="sa-east-1">
|
||||
sa-east-1
|
||||
(São
|
||||
Paulo)
|
||||
(São Paulo)
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -865,8 +848,7 @@ export function ModelConfigDialog({
|
||||
}
|
||||
</>
|
||||
) : (
|
||||
dict
|
||||
.modelConfig
|
||||
dict.modelConfig
|
||||
.test
|
||||
)}
|
||||
</Button>
|
||||
@@ -922,8 +904,7 @@ export function ModelConfigDialog({
|
||||
}
|
||||
</>
|
||||
) : (
|
||||
dict
|
||||
.modelConfig
|
||||
dict.modelConfig
|
||||
.test
|
||||
)}
|
||||
</Button>
|
||||
@@ -949,8 +930,7 @@ export function ModelConfigDialog({
|
||||
>
|
||||
<Key className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
{
|
||||
dict
|
||||
.modelConfig
|
||||
dict.modelConfig
|
||||
.apiKey
|
||||
}
|
||||
</Label>
|
||||
@@ -1067,8 +1047,7 @@ export function ModelConfigDialog({
|
||||
>
|
||||
<Link2 className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
{
|
||||
dict
|
||||
.modelConfig
|
||||
dict.modelConfig
|
||||
.baseUrl
|
||||
}
|
||||
<span className="text-muted-foreground font-normal">
|
||||
@@ -1098,8 +1077,7 @@ export function ModelConfigDialog({
|
||||
.provider
|
||||
]
|
||||
.defaultBaseUrl ||
|
||||
dict
|
||||
.modelConfig
|
||||
dict.modelConfig
|
||||
.customEndpoint
|
||||
}
|
||||
className="h-9 rounded-xl font-mono text-xs"
|
||||
@@ -1122,13 +1100,10 @@ export function ModelConfigDialog({
|
||||
dict.modelConfig
|
||||
.customModelId
|
||||
}
|
||||
value={
|
||||
customModelInput
|
||||
}
|
||||
value={customModelInput}
|
||||
onChange={(e) => {
|
||||
setCustomModelInput(
|
||||
e.target
|
||||
.value,
|
||||
e.target.value,
|
||||
)
|
||||
if (
|
||||
duplicateError
|
||||
@@ -1148,9 +1123,7 @@ export function ModelConfigDialog({
|
||||
handleAddModel(
|
||||
customModelInput.trim(),
|
||||
)
|
||||
if (
|
||||
success
|
||||
) {
|
||||
if (success) {
|
||||
setCustomModelInput(
|
||||
"",
|
||||
)
|
||||
@@ -1195,9 +1168,7 @@ export function ModelConfigDialog({
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Select
|
||||
onValueChange={(
|
||||
value,
|
||||
) => {
|
||||
onValueChange={(value) => {
|
||||
if (value) {
|
||||
handleAddModel(
|
||||
value,
|
||||
@@ -1233,9 +1204,7 @@ export function ModelConfigDialog({
|
||||
}
|
||||
className="font-mono text-xs"
|
||||
>
|
||||
{
|
||||
modelId
|
||||
}
|
||||
{modelId}
|
||||
</SelectItem>
|
||||
),
|
||||
)}
|
||||
@@ -1246,8 +1215,8 @@ export function ModelConfigDialog({
|
||||
>
|
||||
{/* Model List */}
|
||||
<div className="rounded-2xl border border-border-subtle bg-surface-2/30 overflow-hidden min-h-[120px]">
|
||||
{selectedProvider.models
|
||||
.length === 0 ? (
|
||||
{selectedProvider.models.length ===
|
||||
0 ? (
|
||||
<div className="p-6 text-center h-full flex flex-col items-center justify-center">
|
||||
<div className="inline-flex items-center justify-center w-10 h-10 rounded-full bg-surface-2 mb-3">
|
||||
<Sparkles className="h-5 w-5 text-muted-foreground" />
|
||||
@@ -1264,9 +1233,7 @@ export function ModelConfigDialog({
|
||||
{selectedProvider.models.map(
|
||||
(model, index) => (
|
||||
<div
|
||||
key={
|
||||
model.id
|
||||
}
|
||||
key={model.id}
|
||||
className={cn(
|
||||
"transition-colors duration-150 hover:bg-interactive-hover/50",
|
||||
)}
|
||||
@@ -1500,7 +1467,6 @@ export function ModelConfigDialog({
|
||||
</ConfigSection>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</>
|
||||
) : (
|
||||
<div className="h-full flex flex-col items-center justify-center p-8 text-center">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-surface-2 mb-4">
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
flattenModels,
|
||||
type ModelConfig,
|
||||
type MultiModelConfig,
|
||||
PROVIDER_INFO,
|
||||
type ProviderConfig,
|
||||
type ProviderName,
|
||||
} from "@/lib/types/model-config"
|
||||
|
||||
@@ -786,7 +786,7 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
|
||||
`data: ${JSON.stringify(data)}\n\n`,
|
||||
),
|
||||
)
|
||||
} catch (e) {
|
||||
} catch (_e) {
|
||||
// If parsing fails, forward the original message to avoid breaking the stream.
|
||||
controller.enqueue(
|
||||
new TextEncoder().encode(
|
||||
|
||||
@@ -90,7 +90,7 @@ Use the standard MCP configuration with:
|
||||
- **Natural Language**: Describe diagrams in plain text - flowcharts, architecture diagrams, etc.
|
||||
- **Edit Support**: Modify existing diagrams with natural language instructions
|
||||
- **Export**: Save diagrams as `.drawio` files
|
||||
- **Self-contained**: Embedded server, works offline (except draw.io UI which loads from embed.diagrams.net)
|
||||
- **Self-contained**: Embedded server, works offline (except draw.io UI which loads from `embed.diagrams.net` by default, configurable via `DRAWIO_BASE_URL`)
|
||||
|
||||
## Available Tools
|
||||
|
||||
@@ -130,6 +130,33 @@ Use the standard MCP configuration with:
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `PORT` | `6002` | Port for the embedded HTTP server |
|
||||
| `DRAWIO_BASE_URL` | `https://embed.diagrams.net` | Base URL for the draw.io embed. Set this to use a self-hosted draw.io instance for private deployments. |
|
||||
|
||||
### Private Deployment (Self-hosted draw.io)
|
||||
|
||||
For security-sensitive environments that require private deployment of draw.io:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"drawio": {
|
||||
"command": "npx",
|
||||
"args": ["@next-ai-drawio/mcp-server@latest"],
|
||||
"env": {
|
||||
"DRAWIO_BASE_URL": "https://drawio.your-company.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can deploy your own draw.io instance using the official Docker image:
|
||||
|
||||
```bash
|
||||
docker run -d -p 8080:8080 jgraph/drawio
|
||||
```
|
||||
|
||||
Then set `DRAWIO_BASE_URL=http://localhost:8080` (or your server's URL).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -13,6 +13,28 @@ import {
|
||||
} from "./history.js"
|
||||
import { log } from "./logger.js"
|
||||
|
||||
// Configurable draw.io embed URL for private deployments
|
||||
const DRAWIO_BASE_URL =
|
||||
process.env.DRAWIO_BASE_URL || "https://embed.diagrams.net"
|
||||
|
||||
// Extract origin (scheme + host + port) from URL for postMessage security check
|
||||
function getOrigin(url: string): string {
|
||||
try {
|
||||
const parsed = new URL(url)
|
||||
return `${parsed.protocol}//${parsed.host}`
|
||||
} catch {
|
||||
return url // Fallback if parsing fails
|
||||
}
|
||||
}
|
||||
|
||||
const DRAWIO_ORIGIN = getOrigin(DRAWIO_BASE_URL)
|
||||
|
||||
// Normalize URL for iframe src - ensure no double slashes
|
||||
function normalizeUrl(url: string): string {
|
||||
// Remove trailing slash to avoid double slashes
|
||||
return url.replace(/\/$/, '')
|
||||
}
|
||||
|
||||
interface SessionState {
|
||||
xml: string
|
||||
version: number
|
||||
@@ -403,7 +425,7 @@ function getHtmlPage(sessionId: string): string {
|
||||
</div>
|
||||
<div id="status" class="status disconnected">Connecting...</div>
|
||||
</div>
|
||||
<iframe id="drawio" src="https://embed.diagrams.net/?embed=1&proto=json&spin=1&libraries=1"></iframe>
|
||||
<iframe id="drawio" src="${normalizeUrl(DRAWIO_BASE_URL)}/?embed=1&proto=json&spin=1&libraries=1"></iframe>
|
||||
</div>
|
||||
<button id="history-btn" title="History" ${sessionId ? "" : "disabled"}>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
@@ -433,7 +455,7 @@ function getHtmlPage(sessionId: string): string {
|
||||
let pendingAiSvg = false;
|
||||
|
||||
window.addEventListener('message', (e) => {
|
||||
if (e.origin !== 'https://embed.diagrams.net') return;
|
||||
if (e.origin !== '${DRAWIO_ORIGIN}') return;
|
||||
try {
|
||||
const msg = JSON.parse(e.data);
|
||||
if (msg.event === 'init') {
|
||||
|
||||
@@ -48,7 +48,7 @@ import { validateAndFixXml } from "./xml-validation.js"
|
||||
|
||||
// Server configuration
|
||||
const config = {
|
||||
port: parseInt(process.env.PORT || "6002"),
|
||||
port: parseInt(process.env.PORT || "6002", 10),
|
||||
}
|
||||
|
||||
// Session state (single session for simplicity)
|
||||
|
||||
@@ -253,7 +253,7 @@ async function main() {
|
||||
},
|
||||
)
|
||||
console.log("👀 Watching for preset configuration changes...")
|
||||
} catch (err) {
|
||||
} catch (_err) {
|
||||
// File might not exist yet, that's ok
|
||||
setTimeout(setupConfigWatcher, 5000)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user