Compare commits

..

16 Commits

Author SHA1 Message Date
dayuan.jiang
cef3a8de8f chore: add type check and unit test git hooks 2026-01-05 01:12:53 +09:00
dayuan.jiang
fc2086e408 docs: run both unit and e2e tests before PR 2026-01-05 01:06:40 +09:00
dayuan.jiang
6fc09d899c test: remove flaky XML paste test 2026-01-05 01:00:06 +09:00
dayuan.jiang
129b74a1b0 fix: make persistence tests more reliable
- Remove expectBeforeAndAfterReload from mocked API tests
- Add explicit test.step() for before/after reload checks
- Add retry config for flaky clipboard tests
- Add sleep after reload for language persistence test
2026-01-05 00:53:48 +09:00
dayuan.jiang
828bf43e31 refactor: add shared fixtures and test.step() patterns
- Add tests/e2e/lib/fixtures.ts with shared test helpers
- Add tests/e2e/fixtures/diagrams.ts with XML test data
- Add expectBeforeAndAfterReload() helper for persistence tests
- Add test.step() for better test reporting in complex tests
- Consolidate mock helpers into fixtures module
- Reduce code duplication across 17 test files
2026-01-05 00:37:40 +09:00
dayuan.jiang
948cc4666d fix: improve E2E test assertions and remove silent skips
- Replace silent test.skip() with explicit conditional skips
- Add actual persistence assertion after page reload
- Use data-testid selector for new chat button test
2026-01-04 23:05:31 +09:00
dayuan.jiang
d517268dbe fix: improve test infrastructure based on PR review
- Fix double build in CI: remove redundant build from playwright webServer
- Export chat helpers from shared module for proper unit testing
- Replace waitForTimeout with explicit waits in E2E tests
- Add data-testid attributes to settings and new chat buttons
- Add list reporter for CI to show failures in logs
- Add Playwright browser caching to speed up CI
- Add vitest coverage configuration
- Fix conditional test assertions to use test.skip() instead of silent pass
- Remove unused variables flagged by linter
2026-01-04 22:25:50 +09:00
dayuan.jiang
d1e5fc1440 docs: add testing section to CONTRIBUTING.md 2026-01-04 21:24:59 +09:00
dayuan.jiang
6ce7e3378b refactor: extract shared test helpers and improve error assertions
- Create tests/e2e/lib/helpers.ts with shared SSE mock functions
- Add proper error UI assertions to error-handling.spec.ts
- Remove waitForTimeout calls in favor of real assertions
- Update 6 test files to use shared helpers
2026-01-04 21:14:08 +09:00
dayuan.jiang
ddc35e1bb2 test: add comprehensive E2E tests for all major features
- Error handling tests (API errors, rate limits, network timeout, truncated XML)
- Multi-turn conversation tests (sequential requests, history preservation)
- File upload tests (upload button, file preview, sending with message)
- Theme switching tests (dark mode toggle, persistence, system preference)
- Language switching tests (EN/JA/ZH, persistence, locale URLs)
- Iframe interaction tests (draw.io loading, toolbar, diagram rendering)
- Copy/paste tests (chat input, XML input, special characters)
- History restore tests (new chat, persistence, browser navigation)
2026-01-04 20:47:31 +09:00
dayuan.jiang
92514ad6f5 test(e2e): add diagram generation tests with mocked AI responses
- Add tests for generate, edit, and append diagram operations
- Use SSE mocked responses matching AI SDK UI message stream format
- Generate mxCell XML directly in tests for deterministic assertions
- Tests verify tool card rendering and 'Complete' badge state
2026-01-04 20:28:42 +09:00
dayuan.jiang
c2fbfc1a9d fix: resolve lint errors for CI 2026-01-04 20:06:45 +09:00
github-actions[bot]
a80e0fca0a style: auto-format with Biome 2026-01-04 11:00:03 +00:00
dayuan.jiang
f415d457d8 test: fix E2E test issues from review
Fixes based on Gemini and Codex review:
- Remove brittle nth(1) selector in keyboard tests
- Remove waitForTimeout(500) race condition
- Remove if(isVisible) silent skip patterns
- Add proper assertions instead of no-op checks
- Remove expect(count >= 0) that always passes
- Remove unused hasProviderUI variable

All 14 E2E tests and 39 unit tests pass.
2026-01-04 19:58:48 +09:00
dayuan.jiang
ca86c9ebc6 test: add more E2E tests for UI components
- Chat panel tests (interactive elements, iframe)
- Settings tests (dark mode, language, draw.io theme)
- Save dialog tests (buttons exist)
- History dialog tests
- Model config tests
- Keyboard interaction tests
- Upload area tests

Total: 15 E2E tests, all passing
2026-01-04 19:46:37 +09:00
dayuan.jiang
74fbb629e7 test: add Vitest and Playwright testing infrastructure
- Add Vitest for unit tests (39 tests)
  - cached-responses.test.ts
  - ai-providers.test.ts
  - chat-helpers.test.ts
  - utils.test.ts
- Add Playwright for E2E tests (3 smoke tests)
  - Homepage load
  - Japanese locale
  - Settings dialog
- Add CI workflow (.github/workflows/test.yml)
- Add vitest.config.mts and playwright.config.ts
- Update .gitignore for test artifacts
2026-01-04 19:33:02 +09:00
33 changed files with 188 additions and 1127 deletions

View File

@@ -11,8 +11,7 @@ on:
required: false required: false
jobs: jobs:
# Mac and Linux: Build and publish directly (no signing needed) build:
build-mac-linux:
permissions: permissions:
contents: write contents: write
strategy: strategy:
@@ -21,9 +20,13 @@ jobs:
include: include:
- os: macos-latest - os: macos-latest
platform: mac platform: mac
- os: windows-latest
platform: win
- os: ubuntu-latest - os: ubuntu-latest
platform: linux platform: linux
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v6 uses: actions/checkout@v6
@@ -37,58 +40,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: npm install run: npm install
- name: Build and publish - name: Build and publish Electron app
run: npm run dist:${{ matrix.platform }} run: npm run dist:${{ matrix.platform }}
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Windows: Build, sign with SignPath, then publish
build-windows:
permissions:
contents: write
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 24
cache: "npm"
- name: Install dependencies
run: npm install
# Build WITHOUT publishing
- name: Build Windows app
run: npm run dist:win:build
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload unsigned artifacts for signing
uses: actions/upload-artifact@v4
id: upload-unsigned
with:
name: windows-unsigned
path: release/*.exe
retention-days: 1
- name: Sign with SignPath
uses: signpath/github-action-submit-signing-request@v2
with:
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
organization-id: '880a211d-2cd3-4e7b-8d04-3d1f8eb39df5'
project-slug: 'next-ai-draw-io'
signing-policy-slug: 'test-signing'
artifact-configuration-slug: 'windows-exe'
github-artifact-id: ${{ steps.upload-unsigned.outputs.artifact-id }}
wait-for-completion: true
output-artifact-directory: release-signed
- name: Upload signed artifacts to release
uses: softprops/action-gh-release@v2
with:
files: release-signed/*.exe
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -30,10 +30,6 @@ ENV NEXT_PUBLIC_DRAWIO_BASE_URL=${NEXT_PUBLIC_DRAWIO_BASE_URL}
ARG NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE=false ARG NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE=false
ENV NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE=${NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE} ENV NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE=${NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE}
# Build-time argument for subdirectory deployment (e.g., /nextaidrawio)
ARG NEXT_PUBLIC_BASE_PATH=""
ENV NEXT_PUBLIC_BASE_PATH=${NEXT_PUBLIC_BASE_PATH}
# Build Next.js application (standalone mode) # Build Next.js application (standalone mode)
RUN npm run build RUN npm run build

View File

@@ -40,7 +40,7 @@ https://github.com/user-attachments/assets/9d60a3e8-4a1c-4b5e-acbb-26af2d3eabd1
- [Installation](#installation) - [Installation](#installation)
- [Deployment](#deployment) - [Deployment](#deployment)
- [Deploy to EdgeOne Pages](#deploy-to-edgeone-pages) - [Deploy to EdgeOne Pages](#deploy-to-edgeone-pages)
- [Deploy on Vercel](#deploy-on-vercel) - [Deploy on Vercel (Recommended)](#deploy-on-vercel-recommended)
- [Deploy on Cloudflare Workers](#deploy-on-cloudflare-workers) - [Deploy on Cloudflare Workers](#deploy-on-cloudflare-workers)
- [Multi-Provider Support](#multi-provider-support) - [Multi-Provider Support](#multi-provider-support)
- [How It Works](#how-it-works) - [How It Works](#how-it-works)
@@ -185,7 +185,7 @@ Check out the [Tencent EdgeOne Pages documentation](https://pages.edgeone.ai/doc
Additionally, deploying through Tencent EdgeOne Pages will also grant you a [daily free quota for DeepSeek models](https://pages.edgeone.ai/document/edge-ai). Additionally, deploying through Tencent EdgeOne Pages will also grant you a [daily free quota for DeepSeek models](https://pages.edgeone.ai/document/edge-ai).
### Deploy on Vercel ### Deploy on Vercel (Recommended)
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FDayuanJiang%2Fnext-ai-draw-io) [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FDayuanJiang%2Fnext-ai-draw-io)
@@ -211,7 +211,6 @@ See the [Next.js deployment documentation](https://nextjs.org/docs/app/building-
- OpenRouter - OpenRouter
- DeepSeek - DeepSeek
- SiliconFlow - SiliconFlow
- ModelScope
- SGLang - SGLang
- Vercel AI Gateway - Vercel AI Gateway

View File

@@ -10,7 +10,18 @@ export const metadata: Metadata = {
keywords: ["AI图表", "draw.io", "AWS架构", "GCP图表", "Azure图表", "LLM"], keywords: ["AI图表", "draw.io", "AWS架构", "GCP图表", "Azure图表", "LLM"],
} }
function formatNumber(num: number): string {
if (num >= 1000) {
return `${num / 1000}k`
}
return num.toString()
}
export default function AboutCN() { export default function AboutCN() {
const dailyRequestLimit = Number(process.env.DAILY_REQUEST_LIMIT) || 20
const dailyTokenLimit = Number(process.env.DAILY_TOKEN_LIMIT) || 500000
const tpmLimit = Number(process.env.TPM_LIMIT) || 50000
return ( return (
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50">
{/* Navigation */} {/* Navigation */}
@@ -97,6 +108,42 @@ export default function AboutCN() {
</p> </p>
</div> </div>
{/* Usage Limits */}
<p className="text-sm text-gray-600 mb-3">
使
</p>
<div className="grid grid-cols-3 gap-3 mb-5">
<div className="text-center p-3 bg-white/60 rounded-lg">
<p className="text-lg font-bold text-amber-600">
{formatNumber(dailyRequestLimit)}
</p>
<p className="text-xs text-gray-500">
/
</p>
</div>
<div className="text-center p-3 bg-white/60 rounded-lg">
<p className="text-lg font-bold text-amber-600">
{formatNumber(dailyTokenLimit)}
</p>
<p className="text-xs text-gray-500">
Token/
</p>
</div>
<div className="text-center p-3 bg-white/60 rounded-lg">
<p className="text-lg font-bold text-amber-600">
{formatNumber(tpmLimit)}
</p>
<p className="text-xs text-gray-500">
Token/
</p>
</div>
</div>
{/* Divider */}
<div className="flex items-center gap-3 my-5">
<div className="flex-1 h-px bg-gradient-to-r from-transparent via-amber-300 to-transparent" />
</div>
{/* Bring Your Own Key */} {/* Bring Your Own Key */}
<div className="text-center"> <div className="text-center">
<h4 className="text-base font-bold text-gray-900 mb-2"> <h4 className="text-base font-bold text-gray-900 mb-2">
@@ -297,7 +344,6 @@ export default function AboutCN() {
<li>OpenRouter</li> <li>OpenRouter</li>
<li>DeepSeek</li> <li>DeepSeek</li>
<li>SiliconFlow</li> <li>SiliconFlow</li>
<li>ModelScope</li>
</ul> </ul>
<p className="text-gray-700 mt-4"> <p className="text-gray-700 mt-4">
<code>claude-sonnet-4-5</code>{" "} <code>claude-sonnet-4-5</code>{" "}

View File

@@ -17,7 +17,18 @@ export const metadata: Metadata = {
], ],
} }
function formatNumber(num: number): string {
if (num >= 1000) {
return `${num / 1000}k`
}
return num.toString()
}
export default function AboutJA() { export default function AboutJA() {
const dailyRequestLimit = Number(process.env.DAILY_REQUEST_LIMIT) || 20
const dailyTokenLimit = Number(process.env.DAILY_TOKEN_LIMIT) || 500000
const tpmLimit = Number(process.env.TPM_LIMIT) || 50000
return ( return (
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50">
{/* Navigation */} {/* Navigation */}
@@ -105,6 +116,42 @@ export default function AboutJA() {
</p> </p>
</div> </div>
{/* Usage Limits */}
<p className="text-sm text-gray-600 mb-3">
使
</p>
<div className="grid grid-cols-3 gap-3 mb-5">
<div className="text-center p-3 bg-white/60 rounded-lg">
<p className="text-lg font-bold text-amber-600">
{formatNumber(dailyRequestLimit)}
</p>
<p className="text-xs text-gray-500">
/
</p>
</div>
<div className="text-center p-3 bg-white/60 rounded-lg">
<p className="text-lg font-bold text-amber-600">
{formatNumber(dailyTokenLimit)}
</p>
<p className="text-xs text-gray-500">
/
</p>
</div>
<div className="text-center p-3 bg-white/60 rounded-lg">
<p className="text-lg font-bold text-amber-600">
{formatNumber(tpmLimit)}
</p>
<p className="text-xs text-gray-500">
/
</p>
</div>
</div>
{/* Divider */}
<div className="flex items-center gap-3 my-5">
<div className="flex-1 h-px bg-gradient-to-r from-transparent via-amber-300 to-transparent" />
</div>
{/* Bring Your Own Key */} {/* Bring Your Own Key */}
<div className="text-center"> <div className="text-center">
<h4 className="text-base font-bold text-gray-900 mb-2"> <h4 className="text-base font-bold text-gray-900 mb-2">
@@ -312,7 +359,6 @@ export default function AboutJA() {
<li>OpenRouter</li> <li>OpenRouter</li>
<li>DeepSeek</li> <li>DeepSeek</li>
<li>SiliconFlow</li> <li>SiliconFlow</li>
<li>ModelScope</li>
</ul> </ul>
<p className="text-gray-700 mt-4"> <p className="text-gray-700 mt-4">
<code>claude-sonnet-4-5</code> <code>claude-sonnet-4-5</code>

View File

@@ -17,7 +17,18 @@ export const metadata: Metadata = {
], ],
} }
function formatNumber(num: number): string {
if (num >= 1000) {
return `${num / 1000}k`
}
return num.toString()
}
export default function About() { export default function About() {
const dailyRequestLimit = Number(process.env.DAILY_REQUEST_LIMIT) || 20
const dailyTokenLimit = Number(process.env.DAILY_TOKEN_LIMIT) || 500000
const tpmLimit = Number(process.env.TPM_LIMIT) || 50000
return ( return (
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50">
{/* Navigation */} {/* Navigation */}
@@ -107,6 +118,42 @@ export default function About() {
</p> </p>
</div> </div>
{/* Usage Limits */}
<p className="text-sm text-gray-600 mb-3">
Please note the current usage limits:
</p>
<div className="grid grid-cols-3 gap-3 mb-5">
<div className="text-center p-3 bg-white/60 rounded-lg">
<p className="text-lg font-bold text-amber-600">
{formatNumber(dailyRequestLimit)}
</p>
<p className="text-xs text-gray-500">
requests/day
</p>
</div>
<div className="text-center p-3 bg-white/60 rounded-lg">
<p className="text-lg font-bold text-amber-600">
{formatNumber(dailyTokenLimit)}
</p>
<p className="text-xs text-gray-500">
tokens/day
</p>
</div>
<div className="text-center p-3 bg-white/60 rounded-lg">
<p className="text-lg font-bold text-amber-600">
{formatNumber(tpmLimit)}
</p>
<p className="text-xs text-gray-500">
tokens/min
</p>
</div>
</div>
{/* Divider */}
<div className="flex items-center gap-3 my-5">
<div className="flex-1 h-px bg-gradient-to-r from-transparent via-amber-300 to-transparent" />
</div>
{/* Bring Your Own Key */} {/* Bring Your Own Key */}
<div className="text-center"> <div className="text-center">
<h4 className="text-base font-bold text-gray-900 mb-2"> <h4 className="text-base font-bold text-gray-900 mb-2">
@@ -331,7 +378,6 @@ export default function About() {
<li>OpenRouter</li> <li>OpenRouter</li>
<li>DeepSeek</li> <li>DeepSeek</li>
<li>SiliconFlow</li> <li>SiliconFlow</li>
<li>ModelScope</li>
</ul> </ul>
<p className="text-gray-700 mt-4"> <p className="text-gray-700 mt-4">
Note that <code>claude-sonnet-4-5</code> has trained on Note that <code>claude-sonnet-4-5</code> has trained on

View File

@@ -1,154 +0,0 @@
import { extract } from "@extractus/article-extractor"
import { NextResponse } from "next/server"
import TurndownService from "turndown"
const MAX_CONTENT_LENGTH = 150000 // Match PDF limit
const EXTRACT_TIMEOUT_MS = 15000
// SSRF protection - block private/internal addresses
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)
if (a === 10) return true // 10.0.0.0/8
if (a === 172 && b >= 16 && b <= 31) return true // 172.16.0.0/12
if (a === 192 && b === 168) return true // 192.168.0.0/16
if (a === 169 && b === 254) return true // 169.254.0.0/16 (link-local)
if (a === 127) return true // 127.0.0.0/8 (loopback)
}
// Block common internal hostnames
if (
hostname.endsWith(".local") ||
hostname.endsWith(".internal") ||
hostname.endsWith(".localhost")
) {
return true
}
return false
} catch {
return true // Invalid URL - block it
}
}
export async function POST(req: Request) {
try {
const { url } = await req.json()
if (!url || typeof url !== "string") {
return NextResponse.json(
{ error: "URL is required" },
{ status: 400 },
)
}
// Validate URL format
try {
new URL(url)
} catch {
return NextResponse.json(
{ error: "Invalid URL format" },
{ status: 400 },
)
}
// SSRF protection
if (isPrivateUrl(url)) {
return NextResponse.json(
{ error: "Cannot access private/internal URLs" },
{ status: 400 },
)
}
// Extract article content with timeout to avoid tying up server resources
const controller = new AbortController()
const timeoutId = setTimeout(() => {
controller.abort()
}, EXTRACT_TIMEOUT_MS)
let article
try {
article = await extract(url, undefined, {
headers: {
"User-Agent": "Mozilla/5.0 (compatible; NextAIDrawio/1.0)",
},
signal: controller.signal,
})
} catch (err: any) {
if (err?.name === "AbortError") {
return NextResponse.json(
{ error: "Timed out while fetching URL content" },
{ status: 504 },
)
}
throw err
} finally {
clearTimeout(timeoutId)
}
if (!article || !article.content) {
return NextResponse.json(
{ error: "Could not extract content from URL" },
{ status: 400 },
)
}
// Convert HTML to Markdown
const turndownService = new TurndownService({
headingStyle: "atx",
codeBlockStyle: "fenced",
})
// Remove unwanted elements before conversion
turndownService.remove(["script", "style", "iframe", "noscript"])
const markdown = turndownService.turndown(article.content)
// Check content length
if (markdown.length > MAX_CONTENT_LENGTH) {
return NextResponse.json(
{
error: `Content exceeds ${MAX_CONTENT_LENGTH / 1000}k character limit (${(markdown.length / 1000).toFixed(1)}k chars)`,
},
{ status: 400 },
)
}
return NextResponse.json({
title: article.title || "Untitled",
content: markdown,
charCount: markdown.length,
})
} catch (error) {
console.error("URL extraction error:", error)
return NextResponse.json(
{ error: "Failed to fetch or parse URL content" },
{ status: 500 },
)
}
}

View File

@@ -251,98 +251,16 @@ export async function POST(req: Request) {
} }
case "doubao": { case "doubao": {
// ByteDance Doubao: use DeepSeek for DeepSeek/Kimi models, OpenAI for others // ByteDance Doubao uses DeepSeek-compatible API
const doubaoBaseUrl =
baseUrl || "https://ark.cn-beijing.volces.com/api/v3"
const lowerModelId = modelId.toLowerCase()
if (
lowerModelId.includes("deepseek") ||
lowerModelId.includes("kimi")
) {
const doubao = createDeepSeek({ const doubao = createDeepSeek({
apiKey, apiKey,
baseURL: doubaoBaseUrl, baseURL:
baseUrl || "https://ark.cn-beijing.volces.com/api/v3",
}) })
model = doubao(modelId) model = doubao(modelId)
} else {
const doubao = createOpenAI({
apiKey,
baseURL: doubaoBaseUrl,
})
model = doubao.chat(modelId)
}
break break
} }
case "modelscope": {
const baseURL =
baseUrl || "https://api-inference.modelscope.cn/v1"
const startTime = Date.now()
try {
// Initiate a streaming request (required for QwQ-32B and certain Qwen3 models)
const response = await fetch(
`${baseURL}/chat/completions`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
model: modelId,
messages: [
{ role: "user", content: "Say 'OK'" },
],
max_tokens: 20,
stream: true,
enable_thinking: false,
}),
},
)
if (!response.ok) {
const errorText = await response.text()
throw new Error(
`ModelScope API error (${response.status}): ${errorText}`,
)
}
const contentType =
response.headers.get("content-type") || ""
const isValidStreamingResponse =
response.status === 200 &&
(contentType.includes("text/event-stream") ||
contentType.includes("application/json"))
if (!isValidStreamingResponse) {
throw new Error(
`Unexpected response format: ${contentType}`,
)
}
const responseTime = Date.now() - startTime
if (response.body) {
response.body.cancel().catch(() => {
/* Ignore cancellation errors */
})
}
return NextResponse.json({
valid: true,
responseTime,
note: "ModelScope model validated (using streaming API)",
})
} catch (error) {
console.error(
"[validate-model] ModelScope validation failed:",
error,
)
throw error
}
}
default: default:
return NextResponse.json( return NextResponse.json(
{ valid: false, error: `Unknown provider: ${provider}` }, { valid: false, error: `Unknown provider: ${provider}` },

View File

@@ -4,7 +4,6 @@ import {
Download, Download,
History, History,
Image as ImageIcon, Image as ImageIcon,
Link,
Loader2, Loader2,
Send, Send,
} from "lucide-react" } from "lucide-react"
@@ -19,13 +18,11 @@ import { SaveDialog } from "@/components/save-dialog"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Textarea } from "@/components/ui/textarea" import { Textarea } from "@/components/ui/textarea"
import { UrlInputDialog } from "@/components/url-input-dialog"
import { useDiagram } from "@/contexts/diagram-context" import { useDiagram } from "@/contexts/diagram-context"
import { useDictionary } from "@/hooks/use-dictionary" import { useDictionary } from "@/hooks/use-dictionary"
import { formatMessage } from "@/lib/i18n/utils" import { formatMessage } from "@/lib/i18n/utils"
import { isPdfFile, isTextFile } from "@/lib/pdf-utils" import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
import type { FlattenedModel } from "@/lib/types/model-config" import type { FlattenedModel } from "@/lib/types/model-config"
import { extractUrlContent, type UrlData } from "@/lib/url-utils"
import { FilePreviewList } from "./file-preview-list" import { FilePreviewList } from "./file-preview-list"
const MAX_IMAGE_SIZE = 2 * 1024 * 1024 // 2MB const MAX_IMAGE_SIZE = 2 * 1024 * 1024 // 2MB
@@ -147,8 +144,6 @@ interface ChatInputProps {
File, File,
{ text: string; charCount: number; isExtracting: boolean } { text: string; charCount: number; isExtracting: boolean }
> >
urlData?: Map<string, UrlData>
onUrlChange?: (data: Map<string, UrlData>) => void
sessionId?: string sessionId?: string
error?: Error | null error?: Error | null
@@ -168,8 +163,6 @@ export function ChatInput({
files = [], files = [],
onFileChange = () => {}, onFileChange = () => {},
pdfData = new Map(), pdfData = new Map(),
urlData,
onUrlChange,
sessionId, sessionId,
error = null, error = null,
models = [], models = [],
@@ -190,8 +183,6 @@ export function ChatInput({
const fileInputRef = useRef<HTMLInputElement>(null) const fileInputRef = useRef<HTMLInputElement>(null)
const [isDragging, setIsDragging] = useState(false) const [isDragging, setIsDragging] = useState(false)
const [showHistory, setShowHistory] = useState(false) const [showHistory, setShowHistory] = useState(false)
const [showUrlDialog, setShowUrlDialog] = useState(false)
const [isExtractingUrl, setIsExtractingUrl] = useState(false)
// Allow retry when there's an error (even if status is still "streaming" or "submitted") // Allow retry when there's an error (even if status is still "streaming" or "submitted")
const isDisabled = const isDisabled =
(status === "streaming" || status === "submitted") && !error (status === "streaming" || status === "submitted") && !error
@@ -321,50 +312,6 @@ export function ChatInput({
} }
} }
const handleUrlExtract = async (url: string) => {
if (!onUrlChange) return
setIsExtractingUrl(true)
try {
const existing = urlData
? new Map(urlData)
: new Map<string, UrlData>()
existing.set(url, {
url,
title: url,
content: "",
charCount: 0,
isExtracting: true,
})
onUrlChange(existing)
const data = await extractUrlContent(url)
const newUrlData = new Map(existing)
newUrlData.set(url, data)
onUrlChange(newUrlData)
setShowUrlDialog(false)
} catch (error) {
// Remove the URL from the data map on error
const newUrlData = urlData
? new Map(urlData)
: new Map<string, UrlData>()
newUrlData.delete(url)
onUrlChange(newUrlData)
showErrorToast(
<span className="text-muted-foreground">
{error instanceof Error
? error.message
: "Failed to extract URL content"}
</span>,
)
} finally {
setIsExtractingUrl(false)
}
}
return ( return (
<form <form
onSubmit={onSubmit} onSubmit={onSubmit}
@@ -377,23 +324,13 @@ export function ChatInput({
onDragLeave={handleDragLeave} onDragLeave={handleDragLeave}
onDrop={handleDrop} onDrop={handleDrop}
> >
{/* File & URL previews */} {/* File previews */}
{(files.length > 0 || (urlData && urlData.size > 0)) && ( {files.length > 0 && (
<div className="mb-3"> <div className="mb-3">
<FilePreviewList <FilePreviewList
files={files} files={files}
onRemoveFile={handleRemoveFile} onRemoveFile={handleRemoveFile}
pdfData={pdfData} pdfData={pdfData}
urlData={urlData}
onRemoveUrl={
onUrlChange
? (url) => {
const next = new Map(urlData)
next.delete(url)
onUrlChange(next)
}
: undefined
}
/> />
</div> </div>
)} )}
@@ -448,20 +385,6 @@ export function ChatInput({
<ImageIcon className="h-4 w-4" /> <ImageIcon className="h-4 w-4" />
</ButtonWithTooltip> </ButtonWithTooltip>
{onUrlChange && (
<ButtonWithTooltip
type="button"
variant="ghost"
size="sm"
onClick={() => setShowUrlDialog(true)}
disabled={isDisabled}
tooltipContent={dict.chat.ExtractURL}
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
>
<Link className="h-4 w-4" />
</ButtonWithTooltip>
)}
<input <input
type="file" type="file"
ref={fileInputRef} ref={fileInputRef}
@@ -520,14 +443,6 @@ export function ChatInput({
.toISOString() .toISOString()
.slice(0, 10)}`} .slice(0, 10)}`}
/> />
{onUrlChange && (
<UrlInputDialog
open={showUrlDialog}
onOpenChange={setShowUrlDialog}
onSubmit={handleUrlExtract}
isExtracting={isExtractingUrl}
/>
)}
</form> </form>
) )
} }

View File

@@ -34,7 +34,6 @@ import { findCachedResponse } from "@/lib/cached-responses"
import { formatMessage } from "@/lib/i18n/utils" import { formatMessage } from "@/lib/i18n/utils"
import { isPdfFile, isTextFile } from "@/lib/pdf-utils" import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
import { sanitizeMessages } from "@/lib/session-storage" import { sanitizeMessages } from "@/lib/session-storage"
import type { UrlData } from "@/lib/url-utils"
import { type FileData, useFileProcessor } from "@/lib/use-file-processor" import { type FileData, useFileProcessor } from "@/lib/use-file-processor"
import { useQuotaManager } from "@/lib/use-quota-manager" import { useQuotaManager } from "@/lib/use-quota-manager"
import { cn, formatXML, isRealDiagram } from "@/lib/utils" import { cn, formatXML, isRealDiagram } from "@/lib/utils"
@@ -159,7 +158,6 @@ export default function ChatPanel({
// File processing using extracted hook // File processing using extracted hook
const { files, pdfData, handleFileChange, setFiles } = useFileProcessor() const { files, pdfData, handleFileChange, setFiles } = useFileProcessor()
const [urlData, setUrlData] = useState<Map<string, UrlData>>(new Map())
const [showSettingsDialog, setShowSettingsDialog] = useState(false) const [showSettingsDialog, setShowSettingsDialog] = useState(false)
const [showModelConfigDialog, setShowModelConfigDialog] = useState(false) const [showModelConfigDialog, setShowModelConfigDialog] = useState(false)
@@ -712,8 +710,6 @@ export default function ChatPanel({
input, input,
files, files,
pdfData, pdfData,
undefined,
urlData,
) )
setMessages([ setMessages([
@@ -739,7 +735,6 @@ export default function ChatPanel({
setInput("") setInput("")
sessionStorage.removeItem(SESSION_STORAGE_INPUT_KEY) sessionStorage.removeItem(SESSION_STORAGE_INPUT_KEY)
setFiles([]) setFiles([])
setUrlData(new Map())
return return
} }
} }
@@ -760,7 +755,6 @@ export default function ChatPanel({
files, files,
pdfData, pdfData,
parts, parts,
urlData,
) )
// Add the combined text as the first part // Add the combined text as the first part
@@ -785,7 +779,6 @@ export default function ChatPanel({
setInput("") setInput("")
sessionStorage.removeItem(SESSION_STORAGE_INPUT_KEY) sessionStorage.removeItem(SESSION_STORAGE_INPUT_KEY)
setFiles([]) setFiles([])
setUrlData(new Map())
} catch (error) { } catch (error) {
console.error("Error fetching chart data:", error) console.error("Error fetching chart data:", error)
} }
@@ -861,7 +854,6 @@ export default function ChatPanel({
clearDiagram() clearDiagram()
setDiagramHistory([]) setDiagramHistory([])
handleFileChange([]) // Use handleFileChange to also clear pdfData handleFileChange([]) // Use handleFileChange to also clear pdfData
setUrlData(new Map())
const newSessionId = `session-${Date.now()}-${Math.random() const newSessionId = `session-${Date.now()}-${Math.random()
.toString(36) .toString(36)
.slice(2, 9)}` .slice(2, 9)}`
@@ -980,7 +972,6 @@ export default function ChatPanel({
files: File[], files: File[],
pdfData: Map<File, FileData>, pdfData: Map<File, FileData>,
imageParts?: any[], imageParts?: any[],
urlDataParam?: Map<string, UrlData>,
): Promise<string> => { ): Promise<string> => {
let userText = baseText let userText = baseText
@@ -1011,14 +1002,6 @@ export default function ChatPanel({
} }
} }
if (urlDataParam) {
for (const [url, data] of urlDataParam) {
if (data.content) {
userText += `\n\n[URL: ${url}]\nTitle: ${data.title}\n\n${data.content}`
}
}
}
return userText return userText
} }
@@ -1281,8 +1264,6 @@ export default function ChatPanel({
files={files} files={files}
onFileChange={handleFileChange} onFileChange={handleFileChange}
pdfData={pdfData} pdfData={pdfData}
urlData={urlData}
onUrlChange={setUrlData}
sessionId={sessionId} sessionId={sessionId}
error={error} error={error}
models={modelConfig.models} models={modelConfig.models}

View File

@@ -1,6 +1,6 @@
"use client" "use client"
import { FileCode, FileText, Link, Loader2, X } from "lucide-react" import { FileCode, FileText, Loader2, X } from "lucide-react"
import Image from "next/image" import Image from "next/image"
import { useEffect, useRef, useState } from "react" import { useEffect, useRef, useState } from "react"
import { useDictionary } from "@/hooks/use-dictionary" import { useDictionary } from "@/hooks/use-dictionary"
@@ -20,19 +20,12 @@ interface FilePreviewListProps {
File, File,
{ text: string; charCount: number; isExtracting: boolean } { text: string; charCount: number; isExtracting: boolean }
> >
urlData?: Map<
string,
{ url: string; title: string; charCount: number; isExtracting: boolean }
>
onRemoveUrl?: (url: string) => void
} }
export function FilePreviewList({ export function FilePreviewList({
files, files,
onRemoveFile, onRemoveFile,
pdfData = new Map(), pdfData = new Map(),
urlData,
onRemoveUrl,
}: FilePreviewListProps) { }: FilePreviewListProps) {
const dict = useDictionary() const dict = useDictionary()
const [selectedImage, setSelectedImage] = useState<string | null>(null) const [selectedImage, setSelectedImage] = useState<string | null>(null)
@@ -84,7 +77,7 @@ export function FilePreviewList({
} }
}, [imageUrls, selectedImage]) }, [imageUrls, selectedImage])
if (files.length === 0 && (!urlData || urlData.size === 0)) return null if (files.length === 0) return null
return ( return (
<> <>
@@ -159,59 +152,6 @@ export function FilePreviewList({
</div> </div>
) )
})} })}
{/* URL previews */}
{urlData && urlData.size > 0 && (
<div className="flex flex-wrap gap-2">
{Array.from(urlData.entries()).map(
([url, data], index) => (
<div
key={url + index}
className="relative group"
>
<div className="w-20 h-20 border rounded-md overflow-hidden bg-muted">
<div className="flex flex-col items-center justify-center h-full p-1">
{data.isExtracting ? (
<>
<Loader2 className="h-6 w-6 text-blue-500 mb-1 animate-spin" />
<span className="text-[10px] text-muted-foreground">
{dict.file.reading}
</span>
</>
) : (
<>
<Link className="h-6 w-6 text-blue-500 mb-1" />
<span className="text-xs text-center truncate w-full px-1">
{data.title.length > 10
? `${data.title.slice(0, 7)}...`
: data.title}
</span>
{data.charCount && (
<span className="text-[10px] text-green-600 font-medium">
{formatCharCount(
data.charCount,
)}{" "}
{dict.file.chars}
</span>
)}
</>
)}
</div>
</div>
{onRemoveUrl && (
<button
type="button"
onClick={() => onRemoveUrl(url)}
className="absolute -top-2 -right-2 bg-destructive rounded-full p-1 opacity-0 group-hover:opacity-100 transition-opacity"
aria-label={dict.file.removeFile}
>
<X className="h-3 w-3" />
</button>
)}
</div>
),
)}
</div>
)}
</div> </div>
{/* Image Modal/Lightbox */} {/* Image Modal/Lightbox */}
{selectedImage && ( {selectedImage && (

View File

@@ -79,7 +79,6 @@ const PROVIDER_LOGO_MAP: Record<string, string> = {
gateway: "vercel", gateway: "vercel",
edgeone: "tencent-cloud", edgeone: "tencent-cloud",
doubao: "bytedance", doubao: "bytedance",
modelscope: "modelscope",
} }
// Provider logo component // Provider logo component

View File

@@ -50,7 +50,6 @@ const PROVIDER_LOGO_MAP: Record<string, string> = {
gateway: "vercel", gateway: "vercel",
edgeone: "tencent-cloud", edgeone: "tencent-cloud",
doubao: "bytedance", doubao: "bytedance",
modelscope: "modelscope",
} }
// Group models by providerLabel (handles duplicate providers) // Group models by providerLabel (handles duplicate providers)

View File

@@ -1,116 +0,0 @@
"use client"
import { Link, Loader2 } from "lucide-react"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { useDictionary } from "@/hooks/use-dictionary"
interface UrlInputDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
onSubmit: (url: string) => void
isExtracting: boolean
}
export function UrlInputDialog({
open,
onOpenChange,
onSubmit,
isExtracting,
}: UrlInputDialogProps) {
const dict = useDictionary()
const [url, setUrl] = useState("")
const [error, setError] = useState("")
const handleSubmit = () => {
setError("")
if (!url.trim()) {
setError(dict.url.enterUrl)
return
}
try {
new URL(url)
} catch {
setError(dict.url.invalidFormat)
return
}
onSubmit(url.trim())
}
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" && !isExtracting) {
e.preventDefault()
handleSubmit()
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{dict.url.title}</DialogTitle>
<DialogDescription>
{dict.url.description}
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Input
value={url}
onChange={(e) => {
setUrl(e.target.value)
setError("")
}}
onKeyDown={handleKeyDown}
placeholder="https://example.com/article"
disabled={isExtracting}
autoFocus
/>
{error && (
<p className="text-sm text-destructive">{error}</p>
)}
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => onOpenChange(false)}
disabled={isExtracting}
>
{dict.url.Cancel}
</Button>
<Button
onClick={handleSubmit}
disabled={isExtracting || !url.trim()}
>
{isExtracting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
{dict.url.Extracting}
</>
) : (
<>
<Link className="mr-2 h-4 w-4" />
{dict.url.extract}
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -37,7 +37,7 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
- [安装](#安装) - [安装](#安装)
- [部署](#部署) - [部署](#部署)
- [部署到腾讯云EdgeOne Pages](#部署到腾讯云edgeone-pages) - [部署到腾讯云EdgeOne Pages](#部署到腾讯云edgeone-pages)
- [部署到Vercel](#部署到vercel) - [部署到Vercel(推荐)](#部署到vercel推荐)
- [部署到Cloudflare Workers](#部署到cloudflare-workers) - [部署到Cloudflare Workers](#部署到cloudflare-workers)
- [多提供商支持](#多提供商支持) - [多提供商支持](#多提供商支持)
- [工作原理](#工作原理) - [工作原理](#工作原理)
@@ -179,7 +179,7 @@ npm run dev
同时通过腾讯云EdgeOne Pages部署也会获得[每日免费的DeepSeek模型额度](https://edgeone.cloud.tencent.com/pages/document/169925463311781888)。 同时通过腾讯云EdgeOne Pages部署也会获得[每日免费的DeepSeek模型额度](https://edgeone.cloud.tencent.com/pages/document/169925463311781888)。
### 部署到Vercel ### 部署到Vercel(推荐)
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FDayuanJiang%2Fnext-ai-draw-io) [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FDayuanJiang%2Fnext-ai-draw-io)
@@ -204,7 +204,6 @@ npm run dev
- OpenRouter - OpenRouter
- DeepSeek - DeepSeek
- SiliconFlow - SiliconFlow
- ModelScope
- SGLang - SGLang
- Vercel AI Gateway - Vercel AI Gateway

View File

@@ -152,19 +152,6 @@ AI_PROVIDER=ollama
AI_MODEL=llama3.2 AI_MODEL=llama3.2
``` ```
### ModelScope
```bash
MODELSCOPE_API_KEY=your_api_key
AI_MODEL=Qwen/Qwen3-235B-A22B-Instruct-2507
```
可选的自定义端点:
```bash
MODELSCOPE_BASE_URL=https://your-custom-endpoint
```
可选的自定义 URL 可选的自定义 URL
```bash ```bash

View File

@@ -158,19 +158,6 @@ Optional custom URL:
OLLAMA_BASE_URL=http://localhost:11434 OLLAMA_BASE_URL=http://localhost:11434
``` ```
### ModelScope
```bash
MODELSCOPE_API_KEY=your_api_key
AI_MODEL=Qwen/Qwen3-235B-A22B-Instruct-2507
```
Optional custom endpoint:
```bash
MODELSCOPE_BASE_URL=https://your-custom-endpoint
```
### Vercel AI Gateway ### 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. 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.
@@ -214,7 +201,7 @@ If you only configure **one** provider's API key, the system will automatically
If you configure **multiple** API keys, you must explicitly set `AI_PROVIDER`: If you configure **multiple** API keys, you must explicitly set `AI_PROVIDER`:
```bash ```bash
AI_PROVIDER=google # or: openai, anthropic, deepseek, siliconflow, doubao, azure, bedrock, openrouter, ollama, gateway, sglang, modelscope AI_PROVIDER=google # or: openai, anthropic, deepseek, siliconflow, doubao, azure, bedrock, openrouter, ollama, gateway, sglang
``` ```
## Model Capability Requirements ## Model Capability Requirements

View File

@@ -37,7 +37,7 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
- [インストール](#インストール) - [インストール](#インストール)
- [デプロイ](#デプロイ) - [デプロイ](#デプロイ)
- [EdgeOne Pagesへのデプロイ](#edgeone-pagesへのデプロイ) - [EdgeOne Pagesへのデプロイ](#edgeone-pagesへのデプロイ)
- [Vercelへのデプロイ](#vercelへのデプロイ) - [Vercelへのデプロイ(推奨)](#vercelへのデプロイ推奨)
- [Cloudflare Workersへのデプロイ](#cloudflare-workersへのデプロイ) - [Cloudflare Workersへのデプロイ](#cloudflare-workersへのデプロイ)
- [マルチプロバイダーサポート](#マルチプロバイダーサポート) - [マルチプロバイダーサポート](#マルチプロバイダーサポート)
- [仕組み](#仕組み) - [仕組み](#仕組み)
@@ -180,7 +180,7 @@ npm run dev
また、Tencent EdgeOne Pagesでデプロイすると、[DeepSeekモデルの毎日の無料クォータ](https://pages.edgeone.ai/document/edge-ai)が付与されます。 また、Tencent EdgeOne Pagesでデプロイすると、[DeepSeekモデルの毎日の無料クォータ](https://pages.edgeone.ai/document/edge-ai)が付与されます。
### Vercelへのデプロイ ### Vercelへのデプロイ(推奨)
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FDayuanJiang%2Fnext-ai-draw-io) [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FDayuanJiang%2Fnext-ai-draw-io)
@@ -205,7 +205,6 @@ Next.jsアプリをデプロイする最も簡単な方法は、Next.jsの作成
- OpenRouter - OpenRouter
- DeepSeek - DeepSeek
- SiliconFlow - SiliconFlow
- ModelScope
- SGLang - SGLang
- Vercel AI Gateway - Vercel AI Gateway

View File

@@ -158,19 +158,6 @@ AI_MODEL=llama3.2
OLLAMA_BASE_URL=http://localhost:11434 OLLAMA_BASE_URL=http://localhost:11434
``` ```
### ModelScope
```bash
MODELSCOPE_API_KEY=your_api_key
AI_MODEL=Qwen/Qwen3-235B-A22B-Instruct-2507
```
任意のカスタムエンドポイント:
```bash
MODELSCOPE_BASE_URL=https://your-custom-endpoint
```
### Vercel AI Gateway ### Vercel AI Gateway
Vercel AI Gateway は、単一の API キーで複数の AI プロバイダーへの統合アクセスを提供します。これにより認証が簡素化され、複数の API キーを管理することなくプロバイダーを切り替えることができます。 Vercel AI Gateway は、単一の API キーで複数の AI プロバイダーへの統合アクセスを提供します。これにより認証が簡素化され、複数の API キーを管理することなくプロバイダーを切り替えることができます。

View File

@@ -351,10 +351,6 @@ const PROVIDER_ENV_MAP: Record<string, { apiKey: string; baseUrl: string }> = {
apiKey: "SILICONFLOW_API_KEY", apiKey: "SILICONFLOW_API_KEY",
baseUrl: "SILICONFLOW_BASE_URL", baseUrl: "SILICONFLOW_BASE_URL",
}, },
modelscope: {
apiKey: "MODELSCOPE_API_KEY",
baseUrl: "MODELSCOPE_BASE_URL",
},
gateway: { apiKey: "AI_GATEWAY_API_KEY", baseUrl: "AI_GATEWAY_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 and ollama don't use API keys in the same way
bedrock: { apiKey: "", baseUrl: "" }, bedrock: { apiKey: "", baseUrl: "" },

View File

@@ -55,7 +55,6 @@
<option value="openrouter">OpenRouter</option> <option value="openrouter">OpenRouter</option>
<option value="deepseek">DeepSeek</option> <option value="deepseek">DeepSeek</option>
<option value="siliconflow">SiliconFlow</option> <option value="siliconflow">SiliconFlow</option>
<option value="modelscope">ModelScope</option>
<option value="ollama">Ollama (Local)</option> <option value="ollama">Ollama (Local)</option>
</select> </select>
</div> </div>

View File

@@ -288,7 +288,6 @@ function getProviderLabel(provider) {
openrouter: "OpenRouter", openrouter: "OpenRouter",
deepseek: "DeepSeek", deepseek: "DeepSeek",
siliconflow: "SiliconFlow", siliconflow: "SiliconFlow",
modelscope: "ModelScope",
ollama: "Ollama", ollama: "Ollama",
} }
return labels[provider] || provider return labels[provider] || provider

View File

@@ -72,10 +72,6 @@ AI_MODEL=global.anthropic.claude-sonnet-4-5-20250929-v1:0
# SGLANG_API_KEY=your-sglang-api-key # SGLANG_API_KEY=your-sglang-api-key
# SGLANG_BASE_URL=http://127.0.0.1:8000/v1 # Your SGLang endpoint # SGLANG_BASE_URL=http://127.0.0.1:8000/v1 # Your SGLang endpoint
# ModelScope Configuration
# MODELSCOPE_API_KEY=ms-...
# MODELSCOPE_BASE_URL=https://api-inference.modelscope.cn/v1 # Optional: Custom endpoint
# ByteDance Doubao Configuration (via Volcengine) # ByteDance Doubao Configuration (via Volcengine)
# DOUBAO_API_KEY=your-doubao-api-key # DOUBAO_API_KEY=your-doubao-api-key
# DOUBAO_BASE_URL=https://ark.cn-beijing.volces.com/api/v3 # ByteDance Volcengine endpoint # DOUBAO_BASE_URL=https://ark.cn-beijing.volces.com/api/v3 # ByteDance Volcengine endpoint

View File

@@ -23,7 +23,6 @@ export type ProviderName =
| "gateway" | "gateway"
| "edgeone" | "edgeone"
| "doubao" | "doubao"
| "modelscope"
interface ModelConfig { interface ModelConfig {
model: any model: any
@@ -60,7 +59,6 @@ const ALLOWED_CLIENT_PROVIDERS: ProviderName[] = [
"gateway", "gateway",
"edgeone", "edgeone",
"doubao", "doubao",
"modelscope",
] ]
// Bedrock provider options for Anthropic beta features // Bedrock provider options for Anthropic beta features
@@ -355,7 +353,6 @@ function buildProviderOptions(
case "siliconflow": case "siliconflow":
case "sglang": case "sglang":
case "gateway": case "gateway":
case "modelscope":
case "doubao": { case "doubao": {
// These providers don't have reasoning configs in AI SDK yet // These providers don't have reasoning configs in AI SDK yet
// Gateway passes through to underlying providers which handle their own configs // Gateway passes through to underlying providers which handle their own configs
@@ -384,7 +381,6 @@ const PROVIDER_ENV_VARS: Record<ProviderName, string | null> = {
gateway: "AI_GATEWAY_API_KEY", gateway: "AI_GATEWAY_API_KEY",
edgeone: null, // No credentials needed - uses EdgeOne Edge AI edgeone: null, // No credentials needed - uses EdgeOne Edge AI
doubao: "DOUBAO_API_KEY", doubao: "DOUBAO_API_KEY",
modelscope: "MODELSCOPE_API_KEY",
} }
/** /**
@@ -449,7 +445,7 @@ function validateProviderCredentials(provider: ProviderName): void {
* Get the AI model based on environment variables * Get the AI model based on environment variables
* *
* Environment variables: * Environment variables:
* - AI_PROVIDER: The provider to use (bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow, sglang, gateway, modelscope) * - 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 * - AI_MODEL: The model ID/name for the selected provider
* *
* Provider-specific env vars: * Provider-specific env vars:
@@ -467,8 +463,6 @@ function validateProviderCredentials(provider: ProviderName): void {
* - SILICONFLOW_BASE_URL: SiliconFlow endpoint (optional, defaults to https://api.siliconflow.com/v1) * - SILICONFLOW_BASE_URL: SiliconFlow endpoint (optional, defaults to https://api.siliconflow.com/v1)
* - SGLANG_API_KEY: SGLang API key * - SGLANG_API_KEY: SGLang API key
* - SGLANG_BASE_URL: SGLang endpoint (optional) * - SGLANG_BASE_URL: SGLang endpoint (optional)
* - MODELSCOPE_API_KEY: ModelScope API key
* - MODELSCOPE_BASE_URL: ModelScope endpoint (optional)
*/ */
export function getAIModel(overrides?: ClientOverrides): ModelConfig { export function getAIModel(overrides?: ClientOverrides): ModelConfig {
// SECURITY: Prevent SSRF attacks (GHSA-9qf7-mprq-9qgm) // SECURITY: Prevent SSRF attacks (GHSA-9qf7-mprq-9qgm)
@@ -543,7 +537,6 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
`- AZURE_API_KEY for Azure\n` + `- AZURE_API_KEY for Azure\n` +
`- SILICONFLOW_API_KEY for SiliconFlow\n` + `- SILICONFLOW_API_KEY for SiliconFlow\n` +
`- SGLANG_API_KEY for SGLang\n` + `- SGLANG_API_KEY for SGLang\n` +
`- MODELSCOPE_API_KEY for ModelScope\n` +
`Or set AI_PROVIDER=ollama for local Ollama.`, `Or set AI_PROVIDER=ollama for local Ollama.`,
) )
} else { } else {
@@ -878,44 +871,17 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
overrides?.baseUrl || overrides?.baseUrl ||
process.env.DOUBAO_BASE_URL || process.env.DOUBAO_BASE_URL ||
"https://ark.cn-beijing.volces.com/api/v3" "https://ark.cn-beijing.volces.com/api/v3"
const lowerModelId = modelId.toLowerCase()
// Use DeepSeek provider for DeepSeek/Kimi models, OpenAI for others (multimodal support)
if (
lowerModelId.includes("deepseek") ||
lowerModelId.includes("kimi")
) {
const doubaoProvider = createDeepSeek({ const doubaoProvider = createDeepSeek({
apiKey, apiKey,
baseURL, baseURL,
}) })
model = doubaoProvider(modelId) model = doubaoProvider(modelId)
} else {
const doubaoProvider = createOpenAI({
apiKey,
baseURL,
})
model = doubaoProvider.chat(modelId)
}
break
}
case "modelscope": {
const apiKey = overrides?.apiKey || process.env.MODELSCOPE_API_KEY
const baseURL =
overrides?.baseUrl ||
process.env.MODELSCOPE_BASE_URL ||
"https://api-inference.modelscope.cn/v1"
const modelscopeProvider = createOpenAI({
apiKey,
baseURL,
})
model = modelscopeProvider.chat(modelId)
break break
} }
default: default:
throw new Error( throw new Error(
`Unknown AI provider: ${provider}. Supported providers: bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow, sglang, gateway, edgeone, doubao, modelscope`, `Unknown AI provider: ${provider}. Supported providers: bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow, sglang, gateway, edgeone, doubao`,
) )
} }

View File

@@ -28,8 +28,7 @@
"azure": "Azure OpenAI", "azure": "Azure OpenAI",
"openrouter": "OpenRouter", "openrouter": "OpenRouter",
"deepseek": "DeepSeek", "deepseek": "DeepSeek",
"siliconflow": "SiliconFlow", "siliconflow": "SiliconFlow"
"modelscope": "ModelScope"
}, },
"chat": { "chat": {
"placeholder": "Describe your diagram or upload a file...", "placeholder": "Describe your diagram or upload a file...",
@@ -52,8 +51,7 @@
"badResponse": "Bad response", "badResponse": "Bad response",
"clickToEdit": "Click to edit", "clickToEdit": "Click to edit",
"editMessage": "Edit message", "editMessage": "Edit message",
"saveAndSubmit": "Save & Submit", "saveAndSubmit": "Save & Submit"
"ExtractURL": "Extract from URL"
}, },
"examples": { "examples": {
"title": "Create diagrams with AI", "title": "Create diagrams with AI",
@@ -188,15 +186,6 @@
"chars": "chars", "chars": "chars",
"removeFile": "Remove file" "removeFile": "Remove file"
}, },
"url": {
"title": "Extract Content from URL",
"description": "Paste a URL to extract and analyze its content",
"Extracting": "Extracting...",
"extract": "Extract",
"Cancel": "Cancel",
"enterUrl": "Please enter a URL",
"invalidFormat": "Invalid URL format"
},
"reasoning": { "reasoning": {
"thinking": "Thinking...", "thinking": "Thinking...",
"thoughtFor": "Thought for {duration} seconds", "thoughtFor": "Thought for {duration} seconds",

View File

@@ -28,8 +28,7 @@
"azure": "Azure OpenAI", "azure": "Azure OpenAI",
"openrouter": "OpenRouter", "openrouter": "OpenRouter",
"deepseek": "DeepSeek", "deepseek": "DeepSeek",
"siliconflow": "SiliconFlow", "siliconflow": "SiliconFlow"
"modelscope": "ModelScope"
}, },
"chat": { "chat": {
"placeholder": "ダイアグラムを説明するか、ファイルをアップロード...", "placeholder": "ダイアグラムを説明するか、ファイルをアップロード...",
@@ -52,8 +51,7 @@
"badResponse": "悪い応答", "badResponse": "悪い応答",
"clickToEdit": "クリックして編集", "clickToEdit": "クリックして編集",
"editMessage": "メッセージを編集", "editMessage": "メッセージを編集",
"saveAndSubmit": "保存して送信", "saveAndSubmit": "保存して送信"
"ExtractURL": "URLから抽出"
}, },
"examples": { "examples": {
"title": "AI でダイアグラムを作成", "title": "AI でダイアグラムを作成",
@@ -188,15 +186,6 @@
"chars": "文字", "chars": "文字",
"removeFile": "ファイルを削除" "removeFile": "ファイルを削除"
}, },
"url": {
"title": "URLからコンテンツを抽出",
"description": "URLを貼り付けてそのコンテンツを抽出および分析します",
"Extracting": "抽出中...",
"extract": "抽出",
"Cancel": "キャンセル",
"enterUrl": "URLを入力してください",
"invalidFormat": "無効なURL形式です"
},
"reasoning": { "reasoning": {
"thinking": "考え中...", "thinking": "考え中...",
"thoughtFor": "{duration} 秒考えました", "thoughtFor": "{duration} 秒考えました",

View File

@@ -28,8 +28,7 @@
"azure": "Azure OpenAI", "azure": "Azure OpenAI",
"openrouter": "OpenRouter", "openrouter": "OpenRouter",
"deepseek": "DeepSeek", "deepseek": "DeepSeek",
"siliconflow": "SiliconFlow", "siliconflow": "SiliconFlow"
"modelscope": "ModelScope"
}, },
"chat": { "chat": {
"placeholder": "描述您的图表或上传文件...", "placeholder": "描述您的图表或上传文件...",
@@ -52,8 +51,7 @@
"badResponse": "无帮助", "badResponse": "无帮助",
"clickToEdit": "点击编辑", "clickToEdit": "点击编辑",
"editMessage": "编辑消息", "editMessage": "编辑消息",
"saveAndSubmit": "保存并提交", "saveAndSubmit": "保存并提交"
"ExtractURL": "从 URL 提取"
}, },
"examples": { "examples": {
"title": "用 AI 创建图表", "title": "用 AI 创建图表",
@@ -188,15 +186,6 @@
"chars": "字符", "chars": "字符",
"removeFile": "移除文件" "removeFile": "移除文件"
}, },
"url": {
"title": "从 URL 提取内容",
"description": "粘贴 URL 以提取和分析其内容",
"Extracting": "提取中...",
"extract": "提取",
"Cancel": "取消",
"enterUrl": "请输入 URL",
"invalidFormat": "URL 格式无效"
},
"reasoning": { "reasoning": {
"thinking": "思考中...", "thinking": "思考中...",
"thoughtFor": "思考了 {duration} 秒", "thoughtFor": "思考了 {duration} 秒",

View File

@@ -13,7 +13,6 @@ export type ProviderName =
| "gateway" | "gateway"
| "edgeone" | "edgeone"
| "doubao" | "doubao"
| "modelscope"
// Individual model configuration // Individual model configuration
export interface ModelConfig { export interface ModelConfig {
@@ -92,10 +91,6 @@ export const PROVIDER_INFO: Record<
label: "Doubao (ByteDance)", label: "Doubao (ByteDance)",
defaultBaseUrl: "https://ark.cn-beijing.volces.com/api/v3", defaultBaseUrl: "https://ark.cn-beijing.volces.com/api/v3",
}, },
modelscope: {
label: "ModelScope",
defaultBaseUrl: "https://api-inference.modelscope.cn/v1",
},
} }
// Suggested models per provider for quick add // Suggested models per provider for quick add
@@ -236,17 +231,6 @@ export const SUGGESTED_MODELS: Record<ProviderName, string[]> = {
"doubao-pro-32k-241215", "doubao-pro-32k-241215",
"doubao-pro-256k-241215", "doubao-pro-256k-241215",
], ],
modelscope: [
// Qwen
"Qwen/Qwen2.5-72B-Instruct",
"Qwen/Qwen2.5-32B-Instruct",
"Qwen/Qwen3-235B-A22B-Instruct-2507",
"Qwen/Qwen3-VL-235B-A22B-Instruct",
"Qwen/Qwen3-32B",
// DeepSeek
"deepseek-ai/DeepSeek-R1-0528",
"deepseek-ai/DeepSeek-V3.2",
],
} }
// Helper to generate UUID // Helper to generate UUID

View File

@@ -1,49 +0,0 @@
import { z } from "zod"
export interface UrlData {
url: string
title: string
content: string
charCount: number
isExtracting: boolean
}
const UrlResponseSchema = z.object({
title: z.string().default("Untitled"),
content: z.string(),
charCount: z.number().int().nonnegative(),
})
export async function extractUrlContent(url: string): Promise<UrlData> {
const response = await fetch("/api/parse-url", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url }),
})
// Try to parse JSON once
const raw = await response
.json()
.catch(() => ({ error: "Unexpected non-JSON response" }))
if (!response.ok) {
const message =
typeof raw === "object" && raw && "error" in raw
? String((raw as any).error)
: "Failed to extract URL content"
throw new Error(message)
}
const parsed = UrlResponseSchema.safeParse(raw)
if (!parsed.success) {
throw new Error("Malformed response from URL extraction API")
}
return {
url,
title: parsed.data.title,
content: parsed.data.content,
charCount: parsed.data.charCount,
isExtracting: false,
}
}

322
package-lock.json generated
View File

@@ -19,7 +19,6 @@
"@ai-sdk/react": "^3.0.1", "@ai-sdk/react": "^3.0.1",
"@aws-sdk/client-dynamodb": "^3.957.0", "@aws-sdk/client-dynamodb": "^3.957.0",
"@aws-sdk/credential-providers": "^3.943.0", "@aws-sdk/credential-providers": "^3.943.0",
"@extractus/article-extractor": "^8.0.18",
"@formatjs/intl-localematcher": "^0.7.2", "@formatjs/intl-localematcher": "^0.7.2",
"@langfuse/client": "^4.4.9", "@langfuse/client": "^4.4.9",
"@langfuse/otel": "^4.4.4", "@langfuse/otel": "^4.4.4",
@@ -67,7 +66,6 @@
"sonner": "^2.0.7", "sonner": "^2.0.7",
"tailwind-merge": "^3.0.2", "tailwind-merge": "^3.0.2",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"turndown": "^7.2.0",
"unpdf": "^1.4.0", "unpdf": "^1.4.0",
"zod": "^4.1.12" "zod": "^4.1.12"
}, },
@@ -85,7 +83,6 @@
"@types/pako": "^2.0.3", "@types/pako": "^2.0.3",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"@types/turndown": "^5.0.6",
"@vitejs/plugin-react": "^5.1.2", "@vitejs/plugin-react": "^5.1.2",
"@vitest/coverage-v8": "^4.0.16", "@vitest/coverage-v8": "^4.0.16",
"concurrently": "^9.2.1", "concurrently": "^9.2.1",
@@ -6431,22 +6428,6 @@
} }
} }
}, },
"node_modules/@extractus/article-extractor": {
"version": "8.0.20",
"resolved": "https://registry.npmjs.org/@extractus/article-extractor/-/article-extractor-8.0.20.tgz",
"integrity": "sha512-oxHLZ3X5ctLVkQfFkOLf8afvQq6aJ2VBxwQhAaV6ZypaaMJboFz8uwpCGy7QBehmQIvzgWhCwuu8j4ayJFvPcg==",
"license": "MIT",
"dependencies": {
"@mozilla/readability": "^0.6.0",
"@ndaidong/bellajs": "^12.0.1",
"cross-fetch": "^4.1.0",
"linkedom": "^0.18.12",
"sanitize-html": "2.17.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@floating-ui/core": { "node_modules/@floating-ui/core": {
"version": "1.6.9", "version": "1.6.9",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
@@ -7357,21 +7338,6 @@
"node": ">= 10.0.0" "node": ">= 10.0.0"
} }
}, },
"node_modules/@mixmark-io/domino": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz",
"integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==",
"license": "BSD-2-Clause"
},
"node_modules/@mozilla/readability": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@mozilla/readability/-/readability-0.6.0.tgz",
"integrity": "sha512-juG5VWh4qAivzTAeMzvY9xs9HY5rAcr2E4I7tiSSCokRFi7XIZCAu92ZkSTsIj1OPceCifL3cpfteP3pDT9/QQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@napi-rs/wasm-runtime": { "node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.12", "version": "0.2.12",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
@@ -7385,12 +7351,6 @@
"@tybys/wasm-util": "^0.10.0" "@tybys/wasm-util": "^0.10.0"
} }
}, },
"node_modules/@ndaidong/bellajs": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/@ndaidong/bellajs/-/bellajs-12.0.1.tgz",
"integrity": "sha512-1iY42uiHz0cxNMbde7O3zVN+ZX1viOOUOBRt6ht6lkRZbSjwOnFV34Zv4URp3hGzEe6L9Byk7BOq/41H0PzAOQ==",
"license": "MIT"
},
"node_modules/@next/env": { "node_modules/@next/env": {
"version": "16.1.1", "version": "16.1.1",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.1.tgz", "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.1.tgz",
@@ -11483,13 +11443,6 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/turndown": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/@types/turndown/-/turndown-5.0.6.tgz",
"integrity": "sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/unist": { "node_modules/@types/unist": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
@@ -13030,12 +12983,6 @@
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
} }
}, },
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
"license": "ISC"
},
"node_modules/boolean": { "node_modules/boolean": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmmirror.com/boolean/-/boolean-3.2.0.tgz", "resolved": "https://registry.npmmirror.com/boolean/-/boolean-3.2.0.tgz",
@@ -14081,15 +14028,6 @@
"node": ">=20" "node": ">=20"
} }
}, },
"node_modules/cross-fetch": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz",
"integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==",
"license": "MIT",
"dependencies": {
"node-fetch": "^2.7.0"
}
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -14104,22 +14042,6 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/css-select": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
"integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0",
"css-what": "^6.1.0",
"domhandler": "^5.0.2",
"domutils": "^3.0.1",
"nth-check": "^2.0.1"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/css-tree": { "node_modules/css-tree": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz",
@@ -14134,18 +14056,6 @@
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
} }
}, },
"node_modules/css-what": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
"integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
"license": "BSD-2-Clause",
"engines": {
"node": ">= 6"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/cssesc": { "node_modules/cssesc": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@@ -14159,12 +14069,6 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/cssom": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz",
"integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==",
"license": "MIT"
},
"node_modules/cssstyle": { "node_modules/cssstyle": {
"version": "5.3.5", "version": "5.3.5",
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.5.tgz", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.5.tgz",
@@ -14334,15 +14238,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/defaults": { "node_modules/defaults": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmmirror.com/defaults/-/defaults-1.0.4.tgz", "resolved": "https://registry.npmmirror.com/defaults/-/defaults-1.0.4.tgz",
@@ -14579,73 +14474,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/dom-serializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.2",
"entities": "^4.2.0"
},
"funding": {
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
}
},
"node_modules/dom-serializer/node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/domelementtype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "BSD-2-Clause"
},
"node_modules/domhandler": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
"license": "BSD-2-Clause",
"dependencies": {
"domelementtype": "^2.3.0"
},
"engines": {
"node": ">= 4"
},
"funding": {
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/domutils": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
"license": "BSD-2-Clause",
"dependencies": {
"dom-serializer": "^2.0.0",
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3"
},
"funding": {
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
"node_modules/dotenv": { "node_modules/dotenv": {
"version": "16.6.1", "version": "16.6.1",
"resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.6.1.tgz", "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.6.1.tgz",
@@ -15022,6 +14850,7 @@
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
"dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"engines": { "engines": {
"node": ">=0.12" "node": ">=0.12"
@@ -17030,25 +16859,6 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
}, },
"node_modules/htmlparser2": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz",
"integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==",
"funding": [
"https://github.com/fb55/htmlparser2?sponsor=1",
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3",
"domutils": "^3.2.1",
"entities": "^6.0.0"
}
},
"node_modules/http-cache-semantics": { "node_modules/http-cache-semantics": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmmirror.com/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", "resolved": "https://registry.npmmirror.com/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
@@ -17790,15 +17600,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/is-plain-object": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-potential-custom-element-name": { "node_modules/is-potential-custom-element-name": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
@@ -18616,36 +18417,6 @@
"url": "https://opencollective.com/parcel" "url": "https://opencollective.com/parcel"
} }
}, },
"node_modules/linkedom": {
"version": "0.18.12",
"resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.18.12.tgz",
"integrity": "sha512-jalJsOwIKuQJSeTvsgzPe9iJzyfVaEJiEXl+25EkKevsULHvMJzpNqwvj1jOESWdmgKDiXObyjOYwlUqG7wo1Q==",
"license": "ISC",
"dependencies": {
"css-select": "^5.1.0",
"cssom": "^0.5.0",
"html-escaper": "^3.0.3",
"htmlparser2": "^10.0.0",
"uhyphen": "^0.2.0"
},
"engines": {
"node": ">=16"
},
"peerDependencies": {
"canvas": ">= 2"
},
"peerDependenciesMeta": {
"canvas": {
"optional": true
}
}
},
"node_modules/linkedom/node_modules/html-escaper": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
"integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==",
"license": "MIT"
},
"node_modules/lint-staged": { "node_modules/lint-staged": {
"version": "16.2.7", "version": "16.2.7",
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.7.tgz", "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.7.tgz",
@@ -20934,18 +20705,6 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0"
},
"funding": {
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
"node_modules/object-assign": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -21399,12 +21158,6 @@
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/parse-srcset": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz",
"integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==",
"license": "MIT"
},
"node_modules/parse5": { "node_modules/parse5": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz",
@@ -21640,6 +21393,7 @@
"version": "8.5.6", "version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true,
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -22679,63 +22433,6 @@
"truncate-utf8-bytes": "^1.0.0" "truncate-utf8-bytes": "^1.0.0"
} }
}, },
"node_modules/sanitize-html": {
"version": "2.17.0",
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.17.0.tgz",
"integrity": "sha512-dLAADUSS8rBwhaevT12yCezvioCA+bmUTPH/u57xKPT8d++voeYE6HeluA/bPbQ15TwDBG2ii+QZIEmYx8VdxA==",
"license": "MIT",
"dependencies": {
"deepmerge": "^4.2.2",
"escape-string-regexp": "^4.0.0",
"htmlparser2": "^8.0.0",
"is-plain-object": "^5.0.0",
"parse-srcset": "^1.0.2",
"postcss": "^8.3.11"
}
},
"node_modules/sanitize-html/node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/sanitize-html/node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/sanitize-html/node_modules/htmlparser2": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
"funding": [
"https://github.com/fb55/htmlparser2?sponsor=1",
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3",
"domutils": "^3.0.1",
"entities": "^4.4.0"
}
},
"node_modules/sax": { "node_modules/sax": {
"version": "1.4.3", "version": "1.4.3",
"resolved": "https://registry.npmmirror.com/sax/-/sax-1.4.3.tgz", "resolved": "https://registry.npmmirror.com/sax/-/sax-1.4.3.tgz",
@@ -24322,15 +24019,6 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD" "license": "0BSD"
}, },
"node_modules/turndown": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.2.tgz",
"integrity": "sha512-1F7db8BiExOKxjSMU2b7if62D/XOyQyZbPKq/nUwopfgnHlqXHqQ0lvfUTeUIr1lZJzOPFn43dODyMSIfvWRKQ==",
"license": "MIT",
"dependencies": {
"@mixmark-io/domino": "^2.2.0"
}
},
"node_modules/type-check": { "node_modules/type-check": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -24513,12 +24201,6 @@
"typescript": ">=4.8.4 <6.0.0" "typescript": ">=4.8.4 <6.0.0"
} }
}, },
"node_modules/uhyphen": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz",
"integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==",
"license": "ISC"
},
"node_modules/unbox-primitive": { "node_modules/unbox-primitive": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",

View File

@@ -24,7 +24,6 @@
"dist": "npm run electron:build && npm run electron:prepare && npx electron-builder --config electron/electron-builder.yml", "dist": "npm run electron:build && npm run electron:prepare && npx electron-builder --config electron/electron-builder.yml",
"dist:mac": "npm run electron:build && npm run electron:prepare && npx electron-builder --config electron/electron-builder.yml --mac", "dist:mac": "npm run electron:build && npm run electron:prepare && npx electron-builder --config electron/electron-builder.yml --mac",
"dist:win": "npm run electron:build && npm run electron:prepare && npx electron-builder --config electron/electron-builder.yml --win", "dist:win": "npm run electron:build && npm run electron:prepare && npx electron-builder --config electron/electron-builder.yml --win",
"dist:win:build": "npm run electron:build && npm run electron:prepare && npx electron-builder --config electron/electron-builder.yml --win --publish never",
"dist:linux": "npm run electron:build && npm run electron:prepare && npx electron-builder --config electron/electron-builder.yml --linux", "dist:linux": "npm run electron:build && npm run electron:prepare && npx electron-builder --config electron/electron-builder.yml --linux",
"dist:all": "npm run electron:build && npm run electron:prepare && npx electron-builder --config electron/electron-builder.yml --mac --win --linux", "dist:all": "npm run electron:build && npm run electron:prepare && npx electron-builder --config electron/electron-builder.yml --mac --win --linux",
"test": "vitest", "test": "vitest",
@@ -41,7 +40,6 @@
"@ai-sdk/react": "^3.0.1", "@ai-sdk/react": "^3.0.1",
"@aws-sdk/client-dynamodb": "^3.957.0", "@aws-sdk/client-dynamodb": "^3.957.0",
"@aws-sdk/credential-providers": "^3.943.0", "@aws-sdk/credential-providers": "^3.943.0",
"@extractus/article-extractor": "^8.0.18",
"@formatjs/intl-localematcher": "^0.7.2", "@formatjs/intl-localematcher": "^0.7.2",
"@langfuse/client": "^4.4.9", "@langfuse/client": "^4.4.9",
"@langfuse/otel": "^4.4.4", "@langfuse/otel": "^4.4.4",
@@ -73,7 +71,6 @@
"jsonrepair": "^3.13.1", "jsonrepair": "^3.13.1",
"lucide-react": "^0.562.0", "lucide-react": "^0.562.0",
"motion": "^12.23.25", "motion": "^12.23.25",
"nanoid": "^3.3.11",
"negotiator": "^1.0.0", "negotiator": "^1.0.0",
"next": "^16.0.7", "next": "^16.0.7",
"ollama-ai-provider-v2": "^2.0.0", "ollama-ai-provider-v2": "^2.0.0",
@@ -90,7 +87,6 @@
"sonner": "^2.0.7", "sonner": "^2.0.7",
"tailwind-merge": "^3.0.2", "tailwind-merge": "^3.0.2",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"turndown": "^7.2.0",
"unpdf": "^1.4.0", "unpdf": "^1.4.0",
"zod": "^4.1.12" "zod": "^4.1.12"
}, },
@@ -119,7 +115,6 @@
"@types/pako": "^2.0.3", "@types/pako": "^2.0.3",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"@types/turndown": "^5.0.6",
"@vitejs/plugin-react": "^5.1.2", "@vitejs/plugin-react": "^5.1.2",
"@vitest/coverage-v8": "^4.0.16", "@vitest/coverage-v8": "^4.0.16",
"concurrently": "^9.2.1", "concurrently": "^9.2.1",

View File

@@ -1,18 +1,18 @@
{ {
"name": "@next-ai-drawio/mcp-server", "name": "@next-ai-drawio/mcp-server",
"version": "0.1.12", "version": "0.1.6",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@next-ai-drawio/mcp-server", "name": "@next-ai-drawio/mcp-server",
"version": "0.1.12", "version": "0.1.6",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.0.4", "@modelcontextprotocol/sdk": "^1.0.4",
"linkedom": "^0.18.0", "linkedom": "^0.18.0",
"open": "^11.0.0", "open": "^11.0.0",
"zod": "^4.0.0" "zod": "^3.24.0"
}, },
"bin": { "bin": {
"next-ai-drawio-mcp": "dist/index.js" "next-ai-drawio-mcp": "dist/index.js"
@@ -2051,9 +2051,9 @@
} }
}, },
"node_modules/zod": { "node_modules/zod": {
"version": "4.3.5", "version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT", "license": "MIT",
"peer": true, "peer": true,
"funding": { "funding": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@next-ai-drawio/mcp-server", "name": "@next-ai-drawio/mcp-server",
"version": "0.1.12", "version": "0.1.11",
"description": "MCP server for Next AI Draw.io - AI-powered diagram generation with real-time browser preview", "description": "MCP server for Next AI Draw.io - AI-powered diagram generation with real-time browser preview",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
@@ -21,16 +21,16 @@
"claude", "claude",
"model-context-protocol" "model-context-protocol"
], ],
"author": "DayuanJiang", "author": "Biki-dev",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/DayuanJiang/next-ai-draw-io", "url": "https://github.com/Biki-dev/next-ai-draw-io",
"directory": "packages/mcp-server" "directory": "packages/mcp-server"
}, },
"homepage": "https://next-ai-drawio.jiang.jp", "homepage": "https://next-ai-drawio.jiang.jp",
"bugs": { "bugs": {
"url": "https://github.com/DayuanJiang/next-ai-draw-io/issues" "url": "https://github.com/Biki-dev/next-ai-draw-io/issues"
}, },
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"
@@ -39,7 +39,7 @@
"@modelcontextprotocol/sdk": "^1.0.4", "@modelcontextprotocol/sdk": "^1.0.4",
"linkedom": "^0.18.0", "linkedom": "^0.18.0",
"open": "^11.0.0", "open": "^11.0.0",
"zod": "^4.0.0" "zod": "^3.24.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^24.0.0", "@types/node": "^24.0.0",