Compare commits

...

28 Commits

Author SHA1 Message Date
github-actions[bot]
c062584ed4 style: auto-format with Biome 2025-12-27 13:47:45 +00:00
renovate[bot]
0fa854747d fix(deps): update dependency react-resizable-panels to v4 2025-12-27 13:47:01 +00:00
Biki Kalita
9ac99a4690 [Feature] Add Cloudflare Worker as deployment option (#170)
* docs(cloudflare): add detailed Cloudflare Workers + R2 deploy guide

* separated cloudflare deploy guide from readme.md

* Missing R2 bucket binding for incremental cache

* docs: move Cloudflare guide to docs/ and improve documentation

- Move Cloudflare_Deploy.md to docs/ folder
- Add 'Deploy without R2' option for simple/free deployments
- Add workers.dev subdomain registration instructions
- Add missing global_fetch_strictly_public flag
- Add troubleshooting for common deployment issues
- Update README.md link to new location

* fix: conditional import for cloudflare dev and regenerate lockfile

- Use dynamic import for @opennextjs/cloudflare to avoid loading workerd during builds
- Regenerate package-lock.json with cross-platform dependencies

* fix: use main lockfile with cloudflare deps added

- Use main branch's package-lock.json as base to ensure cross-platform deps
- Add @opennextjs/cloudflare and wrangler

---------

Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2025-12-27 21:13:23 +09:00
Dayuan Jiang
6d84dade56 chore: add enhancement issue template (#434) 2025-12-27 18:16:38 +09:00
Dayuan Jiang
43f3fbb5ee Merge pull request #432 from DayuanJiang/renovate/aws-actions-configure-aws-credentials-5.x
chore(deps): update aws-actions/configure-aws-credentials action to v5
2025-12-27 14:52:41 +09:00
Dayuan Jiang
1915c817c3 Merge pull request #431 from DayuanJiang/renovate/actions-setup-node-6.x
chore(deps): update actions/setup-node action to v6
2025-12-27 14:52:18 +09:00
Dayuan Jiang
eeab1ba75d Merge pull request #425 from vansh-nagar/fix/theme-based-logo
fix: switch logo file based on dark mode
2025-12-27 14:48:55 +09:00
renovate[bot]
1f4eb02b0b chore(deps): update aws-actions/configure-aws-credentials action to v5 2025-12-27 05:17:32 +00:00
renovate[bot]
5d60ca74f7 chore(deps): update actions/setup-node action to v6 2025-12-27 05:17:29 +00:00
Dayuan Jiang
9fa1dd075b Merge pull request #430 from DayuanJiang/renovate/actions-checkout-6.x
chore(deps): update actions/checkout action to v6
2025-12-27 14:17:03 +09:00
Dayuan Jiang
743b317387 Merge pull request #429 from DayuanJiang/renovate/minor-and-patch-dependencies
fix(deps): update minor and patch dependencies
2025-12-27 14:16:37 +09:00
github-actions[bot]
5ed23784e7 style: auto-format with Biome 2025-12-27 04:35:31 +00:00
renovate[bot]
3a22e11651 chore(deps): update actions/checkout action to v6 2025-12-27 04:34:58 +00:00
renovate[bot]
eb89b9c052 fix(deps): update minor and patch dependencies 2025-12-27 04:34:47 +00:00
Dayuan Jiang
9c1117e8b0 Merge pull request #427 from DayuanJiang/renovate/core-framework-packages
chore(deps): update core framework packages
2025-12-27 13:33:41 +09:00
Dayuan Jiang
39bf3d6a49 Merge pull request #428 from DayuanJiang/renovate/radix-ui-packages
chore(deps): update radix ui packages
2025-12-27 13:33:32 +09:00
github-actions[bot]
ecd689162f style: auto-format with Biome 2025-12-27 01:26:41 +00:00
github-actions[bot]
7a03aec9be style: auto-format with Biome 2025-12-27 01:26:31 +00:00
renovate[bot]
95541dd284 chore(deps): update radix ui packages 2025-12-27 01:26:04 +00:00
renovate[bot]
49af6676b5 chore(deps): update core framework packages 2025-12-27 01:25:44 +00:00
Dayuan Jiang
18ab1bffa0 feat: migrate DynamoDB quota to composite key schema (#426)
- Change from single key (PK only) to composite key (PK + SK)
- PK = user ID, SK = date for per-day history tracking
- Remove two-step daily reset logic (SK handles day separation)
- Rename dailyReqCount/dailyTokenCount to reqCount/tokenCount
- Remove TTL (data never expires per user request)
- Simplify checkAndIncrementRequest to single atomic update
- Fix recordTokenUsage to handle new items explicitly

New table: next-ai-drawio-quota-v2
2025-12-27 10:24:43 +09:00
vansh-nagar
571ba3c6b0 fix: switch app logo based on theme 2025-12-26 17:16:12 +05:30
Divyesh
467561df47 docs(shape-libraries): add label positioning to shape library examples (#422)
- Add verticalLabelPosition=bottom, verticalAlign=top, and align=center to all shape library usage examples
- Update Alibaba Cloud shape library documentation
- Update Atlassian shape library documentation
- Update AWS4 shape library documentation
- Update Azure2 shape library documentation
- Update Cisco19 shape library documentation
- Update Citrix shape library documentation
- Update GCP2 shape library documentation
- Update Kubernetes shape library documentation
- Update MSCAE shape library documentation
- Update Network shape library documentation
- Update OpenStack shape library documentation
- Update Salesforce shape library documentation
- Update SAP shape library documentation
- Update VVD shape library documentation
- Update WebIcons shape library documentation
- Ensures consistent label positioning and alignment across all shape library examples for better visual consistency
2025-12-26 16:57:26 +09:00
Biki Kalita
e67ab37383 docs: fix cross-domain configuration to offline deployment docs (#405)
* docs: add cross-domain troubleshooting to offline deployment guide

* make it simple

* Remove common issues section from offline deployment docs

Removed common issues section regarding cross-domain configuration and rebuilding after configuration changes.
2025-12-26 16:52:56 +09:00
xunc lee
31644dbcd8 feat: add toggle to show unvalidated models in model selector (#413)
* feat: add toggle to show unvalidated models in model selector

Add a toggle switch in the model configuration dialog to allow users to
display models that haven't been validated. This helps users who work with
model providers that have disabled their verification endpoints.

Changes:
- Add showUnvalidatedModels field to MultiModelConfig type
- Add setShowUnvalidatedModels method to useModelConfig hook
- Add Switch toggle in model-config-dialog footer
- Update model-selector to filter based on showUnvalidatedModels setting
- Add warning icon for unvalidated models in the selector
- Add i18n translations for en/zh/ja

Closes #410

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: wrap AlertTriangle in span for title attribute

The AlertTriangle icon from lucide-react doesn't support the title prop directly.
Wrapped it in a span element to properly display the tooltip.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-26 12:19:59 +09:00
Dayuan Jiang
067d309927 fix: handle fork PRs in auto-format workflow (#419)
- Use head.sha instead of head_ref for checkout (works for forks)
- For fork PRs: fail with helpful message if formatting needed
- For same-repo PRs: auto-commit and push as before
2025-12-26 12:15:31 +09:00
Dayuan Jiang
d1d0de3dea chore: bump version to 0.4.7 (#416)
* chore: bump version to 0.4.7

* style: auto-format with Biome

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-12-25 22:30:48 +09:00
Dayuan Jiang
8c736cee0d fix: persist settings in Electron by using fixed port (#415)
- Use fixed port 61337 in production instead of random ports (10000-65535)
- localStorage is origin-specific, so random ports caused settings loss
- Add locale save/restore since language is URL-based
- Fixes #399
2025-12-25 22:20:59 +09:00
44 changed files with 7858 additions and 2809 deletions

24
.github/ISSUE_TEMPLATE/enhancement.md vendored Normal file
View File

@@ -0,0 +1,24 @@
---
name: Enhancement
about: Suggest an improvement to existing functionality
title: '[Enhancement] '
labels: enhancement
assignees: ''
---
> **Note**: This template is just a guide. Feel free to ignore the format entirely - any feedback is welcome! Don't let the template stop you from sharing your ideas.
## Current Behavior
Describe how the feature currently works.
## Proposed Enhancement
How you'd like this to be improved.
## Motivation
Why this enhancement would be beneficial.
## Screenshots / Mockups
If applicable, add screenshots or mockups to illustrate the proposed changes.
## Additional Context
Any other information about the enhancement request.

View File

@@ -12,13 +12,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
ref: ${{ github.head_ref }}
ref: ${{ github.event.pull_request.head.sha }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: '20'
@@ -37,11 +37,21 @@ jobs:
echo "has_changes=true" >> $GITHUB_OUTPUT
fi
# For fork PRs, just fail if formatting is needed (can't push to forks)
- name: Fail if fork PR needs formatting
if: steps.changes.outputs.has_changes == 'true' && github.event.pull_request.head.repo.full_name != github.repository
run: |
echo "::error::This PR has formatting issues. Please run 'npx @biomejs/biome check --write .' locally and push the changes."
git diff --stat
exit 1
# For same-repo PRs, commit and push the changes
- name: Commit changes
if: steps.changes.outputs.has_changes == 'true'
if: steps.changes.outputs.has_changes == 'true' && github.event.pull_request.head.repo.full_name == github.repository
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}
git add .
git commit -m "style: auto-format with Biome"
git push
git push origin HEAD:${{ github.head_ref }}

View File

@@ -20,10 +20,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: '20'
cache: 'npm'

View File

@@ -26,7 +26,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@@ -69,7 +69,7 @@ jobs:
# Push to AWS ECR for App Runner auto-deploy
- name: Configure AWS credentials
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
uses: aws-actions/configure-aws-credentials@v4
uses: aws-actions/configure-aws-credentials@v5
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

View File

@@ -29,10 +29,10 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: 20
cache: "npm"

View File

@@ -242,6 +242,11 @@ Or you can deploy by this button.
Be sure to **set the environment variables** in the Vercel dashboard as you did in your local `.env.local` file.
## Deploy on Cloudflare Workers
[Go to Cloudflare Deploy Guide](./docs/Cloudflare_Deploy.md)
## Multi-Provider Support

View File

@@ -1,4 +1,5 @@
"use client"
import { usePathname, useRouter } from "next/navigation"
import { useCallback, useEffect, useRef, useState } from "react"
import { DrawIoEmbed } from "react-drawio"
import type { ImperativePanelHandle } from "react-resizable-panels"
@@ -10,6 +11,7 @@ import {
ResizablePanelGroup,
} from "@/components/ui/resizable"
import { useDiagram } from "@/contexts/diagram-context"
import { i18n, type Locale } from "@/lib/i18n/config"
const drawioBaseUrl =
process.env.NEXT_PUBLIC_DRAWIO_BASE_URL || "https://embed.diagrams.net"
@@ -24,6 +26,8 @@ export default function Home() {
showSaveDialog,
setShowSaveDialog,
} = useDiagram()
const router = useRouter()
const pathname = usePathname()
const [isMobile, setIsMobile] = useState(false)
const [isChatVisible, setIsChatVisible] = useState(true)
const [drawioUi, setDrawioUi] = useState<"min" | "sketch">("min")
@@ -58,6 +62,18 @@ export default function Home() {
// Load preferences from localStorage after mount
useEffect(() => {
// Restore saved locale and redirect if needed
const savedLocale = localStorage.getItem("next-ai-draw-io-locale")
if (savedLocale && i18n.locales.includes(savedLocale as Locale)) {
const pathParts = pathname.split("/").filter(Boolean)
const currentLocale = pathParts[0]
if (currentLocale !== savedLocale) {
pathParts[0] = savedLocale
router.replace(`/${pathParts.join("/")}`)
return // Wait for redirect
}
}
const savedUi = localStorage.getItem("drawio-theme")
if (savedUi === "min" || savedUi === "sketch") {
setDrawioUi(savedUi)
@@ -84,7 +100,7 @@ export default function Home() {
}
setIsLoaded(true)
}, [])
}, [pathname, router])
const handleDarkModeChange = async () => {
await saveDiagramToStorage()

View File

@@ -162,6 +162,7 @@ interface ChatInputProps {
models?: FlattenedModel[]
selectedModelId?: string
onModelSelect?: (modelId: string | undefined) => void
showUnvalidatedModels?: boolean
onConfigureModels?: () => void
}
@@ -183,6 +184,7 @@ export function ChatInput({
models = [],
selectedModelId,
onModelSelect = () => {},
showUnvalidatedModels = false,
onConfigureModels = () => {},
}: ChatInputProps) {
const dict = useDictionary()
@@ -482,6 +484,7 @@ export function ChatInput({
onSelect={onModelSelect}
onConfigure={onConfigureModels}
disabled={isDisabled}
showUnvalidatedModels={showUnvalidatedModels}
/>
<div className="w-px h-5 bg-border mx-1" />

View File

@@ -943,7 +943,11 @@ export default function ChatPanel({
<div className="flex items-center gap-2 overflow-x-hidden">
<div className="flex items-center gap-2">
<Image
src="/favicon.ico"
src={
darkMode
? "/favicon-white.svg"
: "/favicon.ico"
}
alt="Next AI Drawio"
width={isMobile ? 24 : 28}
height={isMobile ? 24 : 28}
@@ -1071,6 +1075,7 @@ export default function ChatPanel({
models={modelConfig.models}
selectedModelId={modelConfig.selectedModelId}
onModelSelect={modelConfig.setSelectedModelId}
showUnvalidatedModels={modelConfig.showUnvalidatedModels}
onConfigureModels={() => setShowModelConfigDialog(true)}
/>
</footer>

View File

@@ -50,6 +50,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Switch } from "@/components/ui/switch"
import { useDictionary } from "@/hooks/use-dictionary"
import type { UseModelConfigReturn } from "@/hooks/use-model-config"
import { formatMessage } from "@/lib/i18n/utils"
@@ -1447,10 +1448,23 @@ export function ModelConfigDialog({
{/* Footer */}
<div className="px-6 py-3 border-t border-border-subtle bg-surface-1/30 shrink-0">
<p className="text-xs text-muted-foreground text-center flex items-center justify-center gap-1.5">
<Key className="h-3 w-3" />
{dict.modelConfig.apiKeyStored}
</p>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Switch
checked={modelConfig.showUnvalidatedModels}
onCheckedChange={
modelConfig.setShowUnvalidatedModels
}
/>
<Label className="text-xs text-muted-foreground cursor-pointer">
{dict.modelConfig.showUnvalidatedModels}
</Label>
</div>
<p className="text-xs text-muted-foreground flex items-center gap-1.5">
<Key className="h-3 w-3" />
{dict.modelConfig.apiKeyStored}
</p>
</div>
</div>
</DialogContent>

View File

@@ -1,6 +1,13 @@
"use client"
import { Bot, Check, ChevronDown, Server, Settings2 } from "lucide-react"
import {
AlertTriangle,
Bot,
Check,
ChevronDown,
Server,
Settings2,
} from "lucide-react"
import { useMemo, useState } from "react"
import {
ModelSelectorContent,
@@ -26,6 +33,7 @@ interface ModelSelectorProps {
onSelect: (modelId: string | undefined) => void
onConfigure: () => void
disabled?: boolean
showUnvalidatedModels?: boolean
}
// Map our provider names to models.dev logo names
@@ -67,17 +75,20 @@ export function ModelSelector({
onSelect,
onConfigure,
disabled = false,
showUnvalidatedModels = false,
}: ModelSelectorProps) {
const dict = useDictionary()
const [open, setOpen] = useState(false)
// Only show validated models in the selector
const validatedModels = useMemo(
() => models.filter((m) => m.validated === true),
[models],
)
// Filter models based on showUnvalidatedModels setting
const displayModels = useMemo(() => {
if (showUnvalidatedModels) {
return models
}
return models.filter((m) => m.validated === true)
}, [models, showUnvalidatedModels])
const groupedModels = useMemo(
() => groupModelsByProvider(validatedModels),
[validatedModels],
() => groupModelsByProvider(displayModels),
[displayModels],
)
// Find selected model for display
@@ -126,7 +137,7 @@ export function ModelSelector({
/>
<ModelSelectorList className="[&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]">
<ModelSelectorEmpty>
{validatedModels.length === 0 && models.length > 0
{displayModels.length === 0 && models.length > 0
? dict.modelConfig.noVerifiedModels
: dict.modelConfig.noModelsFound}
</ModelSelectorEmpty>
@@ -191,6 +202,16 @@ export function ModelSelector({
<ModelSelectorName>
{model.modelId}
</ModelSelectorName>
{model.validated !== true && (
<span
title={
dict.modelConfig
.unvalidatedModelWarning
}
>
<AlertTriangle className="ml-auto h-3 w-3 text-warning" />
</span>
)}
</ModelSelectorItem>
))}
</ModelSelectorGroup>
@@ -213,7 +234,9 @@ export function ModelSelector({
</ModelSelectorGroup>
{/* Info text */}
<div className="px-3 py-2 text-xs text-muted-foreground border-t">
{dict.modelConfig.onlyVerifiedShown}
{showUnvalidatedModels
? dict.modelConfig.allModelsShown
: dict.modelConfig.onlyVerifiedShown}
</div>
</ModelSelectorList>
</ModelSelectorContent>

View File

@@ -151,6 +151,9 @@ function SettingsContent({
}, [open])
const changeLanguage = (lang: string) => {
// Save locale to localStorage for persistence across restarts
localStorage.setItem("next-ai-draw-io-locale", lang)
const parts = pathname.split("/")
if (parts.length > 1 && i18n.locales.includes(parts[1] as Locale)) {
parts[1] = lang

267
docs/Cloudflare_Deploy.md Normal file
View File

@@ -0,0 +1,267 @@
# Deploy on Cloudflare Workers
This project can be deployed as a **Cloudflare Worker** using the **OpenNext adapter**, giving you:
- Global edge deployment
- Very low latency
- Free `workers.dev` hosting
- Full Next.js ISR support via R2 (optional)
> **Important Windows Note:** OpenNext and Wrangler are **not fully reliable on native Windows**. Recommended options:
>
> - Use **GitHub Codespaces** (works perfectly)
> - OR use **WSL (Linux)**
>
> Pure Windows builds may fail due to WASM file path issues.
---
## Prerequisites
1. A **Cloudflare account** (free tier works for basic deployment)
2. **Node.js 18+**
3. **Wrangler CLI** installed (dev dependency is fine):
```bash
npm install -D wrangler
```
4. Cloudflare login:
```bash
npx wrangler login
```
> **Note:** A payment method is only required if you want to enable R2 for ISR caching. Basic Workers deployment is free.
---
## Step 1 — Install dependencies
```bash
npm install
```
---
## Step 2 — Configure environment variables
Cloudflare uses a different file for local testing.
### 1) Create `.dev.vars` (for Cloudflare local + deploy)
```bash
cp env.example .dev.vars
```
Fill in your API keys and configuration.
### 2) Make sure `.env.local` also exists (for regular Next.js dev)
```bash
cp env.example .env.local
```
Fill in the same values there.
---
## Step 3 — Choose your deployment type
### Option A: Deploy WITHOUT R2 (Simple, Free)
If you don't need ISR caching, you can deploy without R2:
**1. Use simple `open-next.config.ts`:**
```ts
import { defineCloudflareConfig } from "@opennextjs/cloudflare/config"
export default defineCloudflareConfig({})
```
**2. Use simple `wrangler.jsonc` (without r2_buckets):**
```jsonc
{
"$schema": "node_modules/wrangler/config-schema.json",
"main": ".open-next/worker.js",
"name": "next-ai-draw-io-worker",
"compatibility_date": "2025-12-08",
"compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
"assets": {
"directory": ".open-next/assets",
"binding": "ASSETS"
},
"services": [
{
"binding": "WORKER_SELF_REFERENCE",
"service": "next-ai-draw-io-worker"
}
]
}
```
Skip to **Step 4**.
---
### Option B: Deploy WITH R2 (Full ISR Support)
R2 enables **Incremental Static Regeneration (ISR)** caching. Requires a payment method on your Cloudflare account.
**1. Create an R2 bucket** in the Cloudflare Dashboard:
- Go to **Storage & Databases → R2**
- Click **Create bucket**
- Name it: `next-inc-cache`
**2. Configure `open-next.config.ts`:**
```ts
import { defineCloudflareConfig } from "@opennextjs/cloudflare/config"
import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache"
export default defineCloudflareConfig({
incrementalCache: r2IncrementalCache,
})
```
**3. Configure `wrangler.jsonc` (with R2):**
```jsonc
{
"$schema": "node_modules/wrangler/config-schema.json",
"main": ".open-next/worker.js",
"name": "next-ai-draw-io-worker",
"compatibility_date": "2025-12-08",
"compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
"assets": {
"directory": ".open-next/assets",
"binding": "ASSETS"
},
"r2_buckets": [
{
"binding": "NEXT_INC_CACHE_R2_BUCKET",
"bucket_name": "next-inc-cache"
}
],
"services": [
{
"binding": "WORKER_SELF_REFERENCE",
"service": "next-ai-draw-io-worker"
}
]
}
```
> **Important:** The `bucket_name` must exactly match the name you created in the Cloudflare dashboard.
---
## Step 4 — Register a workers.dev subdomain (first-time only)
Before your first deployment, you need a workers.dev subdomain.
**Option 1: Via Cloudflare Dashboard (Recommended)**
Visit: https://dash.cloudflare.com → Workers & Pages → Overview → Set up a subdomain
**Option 2: During deploy**
When you run `npm run deploy`, Wrangler may prompt:
```
Would you like to register a workers.dev subdomain? (Y/n)
```
Type `Y` and choose a subdomain name.
> **Note:** In CI/CD or non-interactive environments, the prompt won't appear. Register via the dashboard first.
---
## Step 5 — Deploy to Cloudflare
```bash
npm run deploy
```
What the script does:
- Builds the Next.js app
- Converts it to a Cloudflare Worker via OpenNext
- Uploads static assets
- Publishes the Worker
Your app will be available at:
```
https://<worker-name>.<your-subdomain>.workers.dev
```
---
## Common issues & fixes
### `You need to register a workers.dev subdomain`
**Cause:** No workers.dev subdomain registered for your account.
**Fix:** Go to https://dash.cloudflare.com → Workers & Pages → Set up a subdomain.
---
### `Please enable R2 through the Cloudflare Dashboard`
**Cause:** R2 is configured in wrangler.jsonc but not enabled on your account.
**Fix:** Either enable R2 (requires payment method) or use Option A (deploy without R2).
---
### `No R2 binding "NEXT_INC_CACHE_R2_BUCKET" found`
**Cause:** `r2_buckets` is missing from `wrangler.jsonc`.
**Fix:** Add the `r2_buckets` section or switch to Option A (without R2).
---
### `Can't set compatibility date in the future`
**Cause:** `compatibility_date` in wrangler config is set to a future date.
**Fix:** Change `compatibility_date` to today or an earlier date.
---
### Windows error: `resvg.wasm?module` (ENOENT)
**Cause:** Windows filenames cannot include `?`, but a wasm asset uses `?module` in its filename.
**Fix:** Build/deploy on Linux (WSL, Codespaces, or CI).
---
## Optional: Preview locally
Preview the Worker locally before deploying:
```bash
npm run preview
```
---
## Summary
| Feature | Without R2 | With R2 |
|---------|------------|---------|
| Cost | Free | Requires payment method |
| ISR Caching | No | Yes |
| Static Pages | Yes | Yes |
| API Routes | Yes | Yes |
| Setup Complexity | Simple | Moderate |
Choose **without R2** for testing or simple apps. Choose **with R2** for production apps that need ISR caching.

View File

@@ -33,7 +33,7 @@ services:
| Scenario | URL Value |
|----------|-----------|
| Localhost | `http://localhost:8080` |
| Remote/Server | `http://YOUR_SERVER_IP:8080` or `https://drawio.your-domain.com` |
| Remote/Server | `http://YOUR_SERVER_IP:8080` |
**Do NOT use** internal Docker aliases like `http://drawio:8080`; the browser cannot resolve them.

View File

@@ -6,7 +6,7 @@
## Usage
```xml
<mxCell value="label" style="shape=mxgraph.alibaba_cloud.{shape};fillColor=#FF6A00;strokeColor=none;" vertex="1" parent="1">
<mxCell value="label" style="shape=mxgraph.alibaba_cloud.{shape};fillColor=#FF6A00;strokeColor=none;verticalLabelPosition=bottom;verticalAlign=top;align=center;" vertex="1" parent="1">
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
</mxCell>
```

View File

@@ -6,7 +6,7 @@
## Usage
```xml
<mxCell value="label" style="image;aspect=fixed;image=img/lib/atlassian/Jira_Logo.svg;" vertex="1" parent="1">
<mxCell value="label" style="image;aspect=fixed;image=img/lib/atlassian/Jira_Logo.svg;verticalLabelPosition=bottom;verticalAlign=top;align=center;" vertex="1" parent="1">
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
</mxCell>
```

View File

@@ -6,7 +6,7 @@
## Usage
```xml
<mxCell value="label" style="shape=mxgraph.aws4.resourceIcon;resIcon=mxgraph.aws4.{shape};fillColor=#ED7100;strokeColor=#ffffff;" vertex="1" parent="1">
<mxCell value="label" style="shape=mxgraph.aws4.resourceIcon;resIcon=mxgraph.aws4.{shape};fillColor=#ED7100;strokeColor=#ffffff;verticalLabelPosition=bottom;verticalAlign=top;align=center;" vertex="1" parent="1">
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
</mxCell>
```

View File

@@ -6,7 +6,7 @@
## Usage
```xml
<mxCell value="label" style="image;aspect=fixed;image=img/lib/azure2/compute/Virtual_Machine.svg;" vertex="1" parent="1">
<mxCell value="label" style="image;aspect=fixed;image=img/lib/azure2/compute/Virtual_Machine.svg;verticalLabelPosition=bottom;verticalAlign=top;align=center;" vertex="1" parent="1">
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
</mxCell>
```

View File

@@ -6,7 +6,7 @@
## Usage
```xml
<mxCell value="label" style="shape=mxgraph.cisco19.rect;prIcon={shape};fillColor=#00bceb;strokeColor=none;" vertex="1" parent="1">
<mxCell value="label" style="shape=mxgraph.cisco19.rect;prIcon={shape};fillColor=#00bceb;strokeColor=none;verticalLabelPosition=bottom;verticalAlign=top;align=center;" vertex="1" parent="1">
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
</mxCell>
```

View File

@@ -6,7 +6,7 @@
## Usage
```xml
<mxCell value="label" style="shape=mxgraph.citrix.{shape};fillColor=#00A4E4;strokeColor=none;" vertex="1" parent="1">
<mxCell value="label" style="shape=mxgraph.citrix.{shape};fillColor=#00A4E4;strokeColor=none;verticalLabelPosition=bottom;verticalAlign=top;align=center;" vertex="1" parent="1">
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
</mxCell>
```

View File

@@ -6,7 +6,7 @@
## Usage
```xml
<mxCell value="label" style="shape=mxgraph.gcp2.{shape};fillColor=#4285F4;strokeColor=none;" vertex="1" parent="1">
<mxCell value="label" style="shape=mxgraph.gcp2.{shape};fillColor=#4285F4;strokeColor=none;verticalLabelPosition=bottom;verticalAlign=top;align=center;" vertex="1" parent="1">
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
</mxCell>
```

View File

@@ -6,7 +6,7 @@
## Usage
```xml
<mxCell value="label" style="shape=mxgraph.kubernetes.icon;prIcon={shape};fillColor=#326CE5;strokeColor=none;" vertex="1" parent="1">
<mxCell value="label" style="shape=mxgraph.kubernetes.icon;prIcon={shape};fillColor=#326CE5;strokeColor=none;verticalLabelPosition=bottom;verticalAlign=top;align=center;" vertex="1" parent="1">
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
</mxCell>
```

View File

@@ -6,7 +6,7 @@
## Usage
```xml
<mxCell value="label" style="shape=mxgraph.mscae.cloud.azure;fillColor=#0078D4;strokeColor=none;" vertex="1" parent="1">
<mxCell value="label" style="shape=mxgraph.mscae.cloud.azure;fillColor=#0078D4;strokeColor=none;verticalLabelPosition=bottom;verticalAlign=top;align=center;" vertex="1" parent="1">
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
</mxCell>
```

View File

@@ -6,7 +6,7 @@
## Usage
```xml
<mxCell value="label" style="shape=mxgraph.networks.server;fillColor=#29AAE1;strokeColor=none;" vertex="1" parent="1">
<mxCell value="label" style="shape=mxgraph.networks.server;fillColor=#29AAE1;strokeColor=none;verticalLabelPosition=bottom;verticalAlign=top;align=center;" vertex="1" parent="1">
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
</mxCell>
```

View File

@@ -6,7 +6,7 @@
## Usage
```xml
<mxCell value="label" style="shape=mxgraph.openstack.{shape};fillColor=#3F51B5;strokeColor=none;" vertex="1" parent="1">
<mxCell value="label" style="shape=mxgraph.openstack.{shape};fillColor=#3F51B5;strokeColor=none;verticalLabelPosition=bottom;verticalAlign=top;align=center;" vertex="1" parent="1">
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
</mxCell>
```

View File

@@ -6,7 +6,7 @@
## Usage
```xml
<mxCell value="label" style="shape=mxgraph.salesforce.analytics;fillColor=#7f8de1;strokeColor=none;" vertex="1" parent="1">
<mxCell value="label" style="shape=mxgraph.salesforce.analytics;fillColor=#7f8de1;strokeColor=none;verticalLabelPosition=bottom;verticalAlign=top;align=center;" vertex="1" parent="1">
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
</mxCell>
```

View File

@@ -6,7 +6,7 @@
## Usage
```xml
<mxCell value="label" style="image;aspect=fixed;image=img/lib/sap/SAP_Logo.svg;" vertex="1" parent="1">
<mxCell value="label" style="image;aspect=fixed;image=img/lib/sap/SAP_Logo.svg;verticalLabelPosition=bottom;verticalAlign=top;align=center;" vertex="1" parent="1">
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
</mxCell>
```

View File

@@ -6,7 +6,7 @@
## Usage
```xml
<mxCell value="label" style="shape=mxgraph.vvd.{shape};fillColor=#00AEEF;strokeColor=none;" vertex="1" parent="1">
<mxCell value="label" style="shape=mxgraph.vvd.{shape};fillColor=#00AEEF;strokeColor=none;verticalLabelPosition=bottom;verticalAlign=top;align=center;" vertex="1" parent="1">
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
</mxCell>
```

View File

@@ -6,7 +6,7 @@
## Usage
```xml
<mxCell value="label" style="shape=mxgraph.webicons.{shape};fillColor=#3b5998;strokeColor=none;" vertex="1" parent="1">
<mxCell value="label" style="shape=mxgraph.webicons.{shape};fillColor=#3b5998;strokeColor=none;verticalLabelPosition=bottom;verticalAlign=top;align=center;" vertex="1" parent="1">
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
</mxCell>
```

View File

@@ -3,16 +3,16 @@ import { app } from "electron"
/**
* Port configuration
* Using fixed ports to preserve localStorage across restarts
* (localStorage is origin-specific, so changing ports loses all saved data)
*/
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
// Production mode uses fixed port (61337) to preserve localStorage
// Falls back to sequential ports if unavailable
production: 61337,
// Maximum attempts to find an available port (fallback)
maxAttempts: 100,
}
@@ -36,19 +36,11 @@ export function isPortAvailable(port: number): Promise<boolean> {
})
}
/**
* 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
* - In production: uses fixed port (61337) to preserve localStorage
* - Falls back to sequential ports if preferred port is unavailable
*
* @param reuseExisting If true, try to reuse the previously allocated port
* @returns Promise<number> The available port
@@ -56,6 +48,9 @@ function getRandomPort(): number {
*/
export async function findAvailablePort(reuseExisting = true): Promise<number> {
const isDev = !app.isPackaged
const preferredPort = isDev
? PORT_CONFIG.development
: PORT_CONFIG.production
// Try to reuse cached port if requested and available
if (reuseExisting && allocatedPort !== null) {
@@ -69,29 +64,22 @@ export async function findAvailablePort(reuseExisting = true): Promise<number> {
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...`,
)
// Try preferred port first
if (await isPortAvailable(preferredPort)) {
allocatedPort = preferredPort
return preferredPort
}
// 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()
console.warn(
`Preferred port ${preferredPort} is in use, finding alternative...`,
)
const available = await isPortAvailable(port)
if (available) {
// Fallback: try sequential ports starting from preferred + 1
for (let attempt = 1; attempt <= PORT_CONFIG.maxAttempts; attempt++) {
const port = preferredPort + attempt
if (await isPortAvailable(port)) {
allocatedPort = port
console.log(`Allocated port: ${port}`)
console.log(`Allocated fallback port: ${port}`)
return port
}
}

View File

@@ -109,9 +109,11 @@ export interface UseModelConfigReturn {
models: FlattenedModel[]
selectedModel: FlattenedModel | undefined
selectedModelId: string | undefined
showUnvalidatedModels: boolean
// Actions
setSelectedModelId: (modelId: string | undefined) => void
setShowUnvalidatedModels: (show: boolean) => void
addProvider: (provider: ProviderName) => ProviderConfig
updateProvider: (
providerId: string,
@@ -160,6 +162,13 @@ export function useModelConfig(): UseModelConfigReturn {
}))
}, [])
const setShowUnvalidatedModels = useCallback((show: boolean) => {
setConfig((prev) => ({
...prev,
showUnvalidatedModels: show,
}))
}, [])
const addProvider = useCallback(
(provider: ProviderName): ProviderConfig => {
const newProvider = createProviderConfig(provider)
@@ -278,7 +287,9 @@ export function useModelConfig(): UseModelConfigReturn {
models,
selectedModel,
selectedModelId: config.selectedModelId,
showUnvalidatedModels: config.showUnvalidatedModels ?? false,
setSelectedModelId,
setShowUnvalidatedModels,
addProvider,
updateProvider,
deleteProvider,

View File

@@ -27,6 +27,7 @@ try {
/**
* Get today's date string in the configured timezone (YYYY-MM-DD format)
* This is used as the Sort Key (SK) for per-day tracking
*/
function getTodayInTimezone(): string {
return new Intl.DateTimeFormat("en-CA", {
@@ -61,8 +62,8 @@ interface QuotaCheckResult {
/**
* Check all quotas and increment request count atomically.
* Uses ConditionExpression to prevent race conditions.
* Returns which limit was exceeded if any.
* Uses composite key (PK=user, SK=date) for per-day tracking.
* Each day automatically gets a new item - no explicit reset needed.
*/
export async function checkAndIncrementRequest(
ip: string,
@@ -73,77 +74,33 @@ export async function checkAndIncrementRequest(
return { allowed: true }
}
const today = getTodayInTimezone()
const pk = ip // User identifier (base64 IP)
const sk = getTodayInTimezone() // Date as sort key (YYYY-MM-DD)
const currentMinute = Math.floor(Date.now() / 60000).toString()
const ttl = Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60
try {
// First, try to reset counts if it's a new day (atomic day reset)
// This will succeed only if lastResetDate < today or doesn't exist
try {
await client.send(
new UpdateItemCommand({
TableName: TABLE,
Key: { PK: { S: `IP#${ip}` } },
// Reset all counts to 1/0 for the new day
UpdateExpression: `
SET lastResetDate = :today,
dailyReqCount = :one,
dailyTokenCount = :zero,
lastMinute = :minute,
tpmCount = :zero,
#ttl = :ttl
`,
// Only succeed if it's a new day (or new item)
ConditionExpression: `
attribute_not_exists(lastResetDate) OR lastResetDate < :today
`,
ExpressionAttributeNames: { "#ttl": "ttl" },
ExpressionAttributeValues: {
":today": { S: today },
":zero": { N: "0" },
":one": { N: "1" },
":minute": { S: currentMinute },
":ttl": { N: String(ttl) },
},
}),
)
// New day reset successful
return { allowed: true }
} catch (resetError: any) {
// If condition failed, it's the same day - continue to increment logic
if (!(resetError instanceof ConditionalCheckFailedException)) {
throw resetError // Re-throw unexpected errors
}
}
// Same day - increment request count with limit checks
// Single atomic update - handles creation AND increment
// New day automatically creates new item (different SK)
// Note: lastMinute/tpmCount are managed by recordTokenUsage only
await client.send(
new UpdateItemCommand({
TableName: TABLE,
Key: { PK: { S: `IP#${ip}` } },
// Increment request count, handle minute boundary for TPM
UpdateExpression: `
SET lastMinute = :minute,
tpmCount = if_not_exists(tpmCount, :zero),
#ttl = :ttl
ADD dailyReqCount :one
`,
Key: {
PK: { S: pk },
SK: { S: sk },
},
UpdateExpression: "ADD reqCount :one",
// Check all limits before allowing increment
// TPM check: allow if new minute OR under limit
ConditionExpression: `
lastResetDate = :today AND
(attribute_not_exists(dailyReqCount) OR dailyReqCount < :reqLimit) AND
(attribute_not_exists(dailyTokenCount) OR dailyTokenCount < :tokenLimit) AND
(attribute_not_exists(reqCount) OR reqCount < :reqLimit) AND
(attribute_not_exists(tokenCount) OR tokenCount < :tokenLimit) AND
(attribute_not_exists(lastMinute) OR lastMinute <> :minute OR
attribute_not_exists(tpmCount) OR tpmCount < :tpmLimit)
`,
ExpressionAttributeNames: { "#ttl": "ttl" },
ExpressionAttributeValues: {
":today": { S: today },
":zero": { N: "0" },
":one": { N: "1" },
":minute": { S: currentMinute },
":ttl": { N: String(ttl) },
":reqLimit": { N: String(limits.requests || 999999) },
":tokenLimit": { N: String(limits.tokens || 999999) },
":tpmLimit": { N: String(limits.tpm || 999999) },
@@ -160,42 +117,39 @@ export async function checkAndIncrementRequest(
const getResult = await client.send(
new GetItemCommand({
TableName: TABLE,
Key: { PK: { S: `IP#${ip}` } },
Key: {
PK: { S: pk },
SK: { S: sk },
},
}),
)
const item = getResult.Item
const storedDate = item?.lastResetDate?.S
const storedMinute = item?.lastMinute?.S
const isNewDay = !storedDate || storedDate < today
const dailyReqCount = isNewDay
? 0
: Number(item?.dailyReqCount?.N || 0)
const dailyTokenCount = isNewDay
? 0
: Number(item?.dailyTokenCount?.N || 0)
const reqCount = Number(item?.reqCount?.N || 0)
const tokenCount = Number(item?.tokenCount?.N || 0)
const tpmCount =
storedMinute !== currentMinute
? 0
: Number(item?.tpmCount?.N || 0)
// Determine which limit was exceeded
if (limits.requests > 0 && dailyReqCount >= limits.requests) {
if (limits.requests > 0 && reqCount >= limits.requests) {
return {
allowed: false,
type: "request",
error: "Daily request limit exceeded",
used: dailyReqCount,
used: reqCount,
limit: limits.requests,
}
}
if (limits.tokens > 0 && dailyTokenCount >= limits.tokens) {
if (limits.tokens > 0 && tokenCount >= limits.tokens) {
return {
allowed: false,
type: "token",
error: "Daily token limit exceeded",
used: dailyTokenCount,
used: tokenCount,
limit: limits.tokens,
}
}
@@ -210,7 +164,7 @@ export async function checkAndIncrementRequest(
}
// Condition failed but no limit clearly exceeded - race condition edge case
// Fail safe by allowing (could be a reset race)
// Fail safe by allowing (could be a TPM reset race)
console.warn(
`[quota] Condition failed but no limit exceeded for IP prefix: ${ip.slice(0, 8)}...`,
)
@@ -233,7 +187,7 @@ export async function checkAndIncrementRequest(
/**
* Record token usage after response completes.
* Uses atomic operations to update both daily token count and TPM count.
* Uses composite key (PK=user, SK=date) for per-day tracking.
* Handles minute boundaries atomically to prevent race conditions.
*/
export async function recordTokenUsage(
@@ -244,24 +198,27 @@ export async function recordTokenUsage(
if (!client || !TABLE) return
if (!Number.isFinite(tokens) || tokens <= 0) return
const pk = ip // User identifier (base64 IP)
const sk = getTodayInTimezone() // Date as sort key (YYYY-MM-DD)
const currentMinute = Math.floor(Date.now() / 60000).toString()
const ttl = Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60
try {
// Try to update assuming same minute (most common case)
// Uses condition to ensure we're in the same minute
// Try to update for same minute OR new item (most common cases)
// Handles: 1) new item (no lastMinute), 2) same minute (lastMinute matches)
await client.send(
new UpdateItemCommand({
TableName: TABLE,
Key: { PK: { S: `IP#${ip}` } },
Key: {
PK: { S: pk },
SK: { S: sk },
},
UpdateExpression:
"SET #ttl = :ttl ADD dailyTokenCount :tokens, tpmCount :tokens",
ConditionExpression: "lastMinute = :minute",
ExpressionAttributeNames: { "#ttl": "ttl" },
"SET lastMinute = if_not_exists(lastMinute, :minute) ADD tokenCount :tokens, tpmCount :tokens",
ConditionExpression:
"attribute_not_exists(lastMinute) OR lastMinute = :minute",
ExpressionAttributeValues: {
":minute": { S: currentMinute },
":tokens": { N: String(tokens) },
":ttl": { N: String(ttl) },
},
}),
)
@@ -272,14 +229,15 @@ export async function recordTokenUsage(
await client.send(
new UpdateItemCommand({
TableName: TABLE,
Key: { PK: { S: `IP#${ip}` } },
Key: {
PK: { S: pk },
SK: { S: sk },
},
UpdateExpression:
"SET lastMinute = :minute, tpmCount = :tokens, #ttl = :ttl ADD dailyTokenCount :tokens",
ExpressionAttributeNames: { "#ttl": "ttl" },
"SET lastMinute = :minute, tpmCount = :tokens ADD tokenCount :tokens",
ExpressionAttributeValues: {
":minute": { S: currentMinute },
":tokens": { N: String(tokens) },
":ttl": { N: String(ttl) },
},
}),
)

View File

@@ -243,6 +243,9 @@
"default": "Default",
"serverDefault": "Server Default",
"configureModels": "Configure Models...",
"onlyVerifiedShown": "Only verified models are shown"
"onlyVerifiedShown": "Only verified models are shown",
"showUnvalidatedModels": "Show unvalidated models",
"allModelsShown": "All models are shown (including unvalidated)",
"unvalidatedModelWarning": "This model has not been validated"
}
}

View File

@@ -243,6 +243,9 @@
"default": "デフォルト",
"serverDefault": "サーバーデフォルト",
"configureModels": "モデルを設定...",
"onlyVerifiedShown": "検証済みのモデルのみ表示"
"onlyVerifiedShown": "検証済みのモデルのみ表示",
"showUnvalidatedModels": "未検証のモデルを表示",
"allModelsShown": "すべてのモデルを表示(未検証を含む)",
"unvalidatedModelWarning": "このモデルは検証されていません"
}
}

View File

@@ -243,6 +243,9 @@
"default": "默认",
"serverDefault": "服务器默认",
"configureModels": "配置模型...",
"onlyVerifiedShown": "仅显示已验证的模型"
"onlyVerifiedShown": "仅显示已验证的模型",
"showUnvalidatedModels": "显示未验证的模型",
"allModelsShown": "显示所有模型(包括未验证的)",
"unvalidatedModelWarning": "此模型尚未验证"
}
}

View File

@@ -40,6 +40,7 @@ export interface MultiModelConfig {
version: 1
providers: ProviderConfig[]
selectedModelId?: string // Currently selected model's UUID
showUnvalidatedModels?: boolean // Show models that haven't been validated
}
// Flattened model for dropdown display

View File

@@ -17,3 +17,13 @@ const nextConfig: NextConfig = {
}
export default nextConfig
// Initialize OpenNext Cloudflare for local development only
// This must be a dynamic import to avoid loading workerd binary during builds
if (process.env.NODE_ENV === "development") {
import("@opennextjs/cloudflare").then(
({ initOpenNextCloudflareForDev }) => {
initOpenNextCloudflareForDev()
},
)
}

7
open-next.config.ts Normal file
View File

@@ -0,0 +1,7 @@
// default open-next.config.ts file created by @opennextjs/cloudflare
import { defineCloudflareConfig } from "@opennextjs/cloudflare/config"
import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache"
export default defineCloudflareConfig({
incrementalCache: r2IncrementalCache,
})

9887
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "next-ai-draw-io",
"version": "0.4.6",
"version": "0.4.7",
"license": "Apache-2.0",
"private": true,
"main": "dist-electron/main/index.js",
@@ -12,6 +12,10 @@
"format": "biome check --write .",
"check": "biome ci",
"prepare": "husky",
"preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
"deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
"upload": "opennextjs-cloudflare build && opennextjs-cloudflare upload",
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts",
"electron:dev": "node scripts/electron-dev.mjs",
"electron:build": "npm run build && npm run electron:compile",
"electron:compile": "npx esbuild electron/main/index.ts electron/preload/index.ts electron/preload/settings.ts --bundle --platform=node --outdir=dist-electron --external:electron --sourcemap --packages=external && npx shx cp -r electron/settings dist-electron/",
@@ -39,6 +43,7 @@
"@langfuse/otel": "^4.4.4",
"@langfuse/tracing": "^4.4.9",
"@next/third-parties": "^16.0.6",
"@opennextjs/cloudflare": "1.14.7",
"@openrouter/ai-sdk-provider": "^1.5.4",
"@opentelemetry/exporter-trace-otlp-http": "^0.208.0",
"@opentelemetry/sdk-trace-node": "^2.2.0",
@@ -62,7 +67,7 @@
"js-tiktoken": "^1.0.21",
"jsdom": "^26.0.0",
"jsonrepair": "^3.13.1",
"lucide-react": "^0.483.0",
"lucide-react": "^0.562.0",
"motion": "^12.23.25",
"negotiator": "^1.0.0",
"next": "^16.0.7",
@@ -74,7 +79,7 @@
"react-drawio": "^1.0.3",
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"react-resizable-panels": "^3.0.6",
"react-resizable-panels": "^4.0.0",
"remark-gfm": "^4.0.1",
"server-only": "^0.0.1",
"sonner": "^2.0.7",
@@ -104,14 +109,15 @@
"electron": "^39.2.7",
"electron-builder": "^26.0.12",
"esbuild": "^0.27.2",
"eslint": "9.39.1",
"eslint-config-next": "16.0.5",
"eslint": "9.39.2",
"eslint-config-next": "16.1.1",
"husky": "^9.1.7",
"lint-staged": "^16.2.7",
"shx": "^0.4.0",
"tailwindcss": "^4",
"typescript": "^5",
"wait-on": "^9.0.3"
"wait-on": "^9.0.3",
"wrangler": "4.54.0"
},
"overrides": {
"@openrouter/ai-sdk-provider": {

View File

@@ -1,12 +1,12 @@
{
"name": "@next-ai-drawio/mcp-server",
"version": "0.1.5",
"version": "0.1.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@next-ai-drawio/mcp-server",
"version": "0.1.5",
"version": "0.1.6",
"license": "Apache-2.0",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.4",
@@ -481,9 +481,9 @@
}
},
"node_modules/@modelcontextprotocol/sdk": {
"version": "1.25.0",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.0.tgz",
"integrity": "sha512-z0Zhn/LmQ3yz91dEfd5QgS7DpSjA4pk+3z2++zKgn5L6iDFM9QapsVoAQSbKLvlrFsZk9+ru6yHHWNq2lCYJKQ==",
"version": "1.25.1",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.1.tgz",
"integrity": "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ==",
"license": "MIT",
"dependencies": {
"@hono/node-server": "^1.19.7",
@@ -1034,6 +1034,7 @@
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"license": "MIT",
"peer": true,
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.1",
@@ -2027,6 +2028,7 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}

2
public/_headers Normal file
View File

@@ -0,0 +1,2 @@
/_next/static/*
Cache-Control: public,max-age=31536000,immutable

37
public/favicon-white.svg Normal file
View File

@@ -0,0 +1,37 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="1536.000000pt" height="1536.000000pt" viewBox="0 0 1536.000000 1536.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,1536.000000) scale(0.100000,-0.100000)"
fill="#ffffff" stroke="none">
<path d="M2765 14404 c-100 -29 -181 -58 -225 -82 -227 -125 -359 -296 -431
-560 -19 -70 -19 -108 -19 -1175 0 -1068 1 -1104 20 -1172 58 -206 159 -356
319 -474 71 -53 199 -121 226 -121 9 0 26 -5 38 -12 12 -6 62 -19 112 -29 85
-17 207 -18 2219 -19 1172 0 2133 -3 2138 -8 4 -4 7 -246 6 -538 l-3 -529
-2330 -5 c-2506 -6 -2373 -3 -2470 -54 -61 -31 -150 -113 -194 -178 -87 -128
-82 -77 -90 -1025 l-6 -838 -360 -6 c-292 -4 -368 -8 -405 -21 -194 -68 -303
-177 -373 -372 l-22 -61 1 -2887 c1 -2716 2 -2890 18 -2935 56 -153 161 -276
286 -334 126 -59 0 -54 1400 -54 1394 0 1290 -4 1410 53 95 45 198 148 242
241 62 133 58 -93 58 3026 0 2992 1 2883 -40 2990 -59 156 -183 272 -360 337
-25 9 -146 14 -440 18 l-405 5 0 540 0 540 2020 3 c1111 1 2030 0 2043 -3 l22
-5 -2 -538 -3 -537 -380 -6 c-312 -4 -388 -8 -426 -21 -195 -68 -326 -204
-383 -399 -15 -51 -16 -295 -16 -2921 0 -2778 1 -2867 19 -2920 36 -104 72
-167 134 -230 75 -78 115 -105 222 -151 l50 -22 1219 -3 c672 -1 1255 1 1300
6 109 12 217 63 298 140 73 69 107 118 144 208 l29 69 3 2880 c2 2687 1 2884
-15 2945 -48 183 -188 332 -373 398 -37 13 -114 17 -430 21 l-385 6 -3 534
c-2 421 0 536 10 543 7 4 925 8 2039 8 1718 0 2028 -2 2038 -14 8 -10 11 -154
11 -531 -1 -284 -4 -523 -7 -531 -4 -12 -69 -14 -392 -14 -354 0 -391 -2 -448
-20 -168 -52 -282 -148 -353 -295 -22 -45 -40 -91 -40 -103 0 -11 -5 -33 -10
-47 -7 -18 -10 -988 -10 -2875 0 -2393 2 -2858 14 -2902 43 -167 148 -298 293
-369 57 -27 107 -44 151 -50 88 -11 2429 -11 2508 0 210 31 416 238 445 450 6
39 8 1245 7 2926 -3 2713 -4 2862 -21 2900 -41 93 -74 150 -110 191 -46 52
-149 134 -169 134 -8 0 -19 5 -24 10 -6 6 -42 19 -80 30 -63 18 -100 20 -415
20 -307 0 -348 2 -353 16 -3 9 -6 390 -6 848 0 797 -1 834 -19 886 -31 87 -50
118 -111 183 -66 70 -141 119 -221 144 -50 16 -228 18 -2389 23 l-2335 5 0
535 0 535 2165 5 c1191 3 2170 8 2176 12 6 4 35 12 65 17 201 35 435 198 539
376 55 93 82 153 110 245 19 63 20 94 20 1167 0 1047 -1 1106 -19 1180 -70
290 -275 523 -539 613 -160 54 232 50 -5028 49 -4182 0 -4856 -2 -4899 -15z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

23
wrangler.jsonc Normal file
View File

@@ -0,0 +1,23 @@
{
"$schema": "node_modules/wrangler/config-schema.json",
"main": ".open-next/worker.js",
"name": "next-ai-draw-io-worker",
"compatibility_date": "2025-12-08", // must be a today or past compatibility_date
"compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
"assets": {
"directory": ".open-next/assets",
"binding": "ASSETS"
},
"r2_buckets": [
{
"binding": "NEXT_INC_CACHE_R2_BUCKET",
"bucket_name": "next-inc-cache"
}
],
"services": [
{
"binding": "WORKER_SELF_REFERENCE",
"service": "next-ai-draw-io-worker"
}
]
}