diff --git a/.github/renovate.json b/.github/renovate.json index 437ea5d6..6d7eb58b 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -33,6 +33,11 @@ "matchPackagePatterns": ["@ai-sdk/*", "ai", "next"], "groupName": "Core framework packages", "automerge": false + }, + { + "matchPackageNames": ["@biomejs/biome"], + "groupName": "Biome", + "automerge": false } ], "vulnerabilityAlerts": { diff --git a/.github/workflows/auto-format.yml b/.github/workflows/auto-format.yml index 3ca6ee41..ada9eba8 100644 --- a/.github/workflows/auto-format.yml +++ b/.github/workflows/auto-format.yml @@ -25,7 +25,7 @@ jobs: - name: Run Biome format # Pin to the version in package.json so CI matches local/pre-commit # (npx @latest drifts — e.g. 2.5.0 broke this job on unrelated PRs). - run: npx @biomejs/biome@2.4.13 check --write --no-errors-on-unmatched . + run: npx @biomejs/biome@2.5.7 check --write --no-errors-on-unmatched . - name: Check for changes id: changes diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 59d90b63..66ac4e7d 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -58,6 +58,8 @@ jobs: with: context: . push: ${{ github.event_name != 'pull_request' }} + provenance: mode=max + sbom: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha @@ -89,4 +91,3 @@ jobs: docker pull ghcr.io/${REPO_LOWER}:latest docker tag ghcr.io/${REPO_LOWER}:latest ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-northeast-1.amazonaws.com/next-ai-draw-io:latest docker push ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-northeast-1.amazonaws.com/next-ai-draw-io:latest - diff --git a/.github/workflows/publish-mcp.yml b/.github/workflows/publish-mcp.yml new file mode 100644 index 00000000..6b816c45 --- /dev/null +++ b/.github/workflows/publish-mcp.yml @@ -0,0 +1,71 @@ +name: Publish MCP Server + +# Publishes @next-ai-drawio/mcp-server to npm via OIDC trusted publishing +# (no token, no OTP). Triggers when packages/mcp-server changes on main; +# skips silently if the package.json version is already on npm — so a +# release is just "bump the version in a PR and merge". +on: + push: + branches: + - main + paths: + - "packages/mcp-server/**" + workflow_dispatch: + +permissions: + contents: read + id-token: write # OIDC token for npm trusted publishing + +concurrency: + group: publish-mcp + cancel-in-progress: false + +jobs: + publish: + runs-on: ubuntu-latest + defaults: + run: + working-directory: packages/mcp-server + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 24 + cache: "npm" + cache-dependency-path: packages/mcp-server/package-lock.json + registry-url: "https://registry.npmjs.org" + + # Trusted publishing requires npm >= 11.5.1 + - name: Update npm + run: npm install -g npm@latest + + - name: Check if version is already published + id: version + run: | + LOCAL=$(node -p "require('./package.json').version") + if npm view "@next-ai-drawio/mcp-server@${LOCAL}" version >/dev/null 2>&1; then + echo "Version ${LOCAL} already on npm - nothing to publish" + echo "publish=false" >> "$GITHUB_OUTPUT" + else + echo "Version ${LOCAL} not on npm - publishing" + echo "publish=true" >> "$GITHUB_OUTPUT" + fi + + - name: Install dependencies + if: steps.version.outputs.publish == 'true' + run: npm ci + + - name: Test + if: steps.version.outputs.publish == 'true' + run: npm test + + - name: Build and check package contents + if: steps.version.outputs.publish == 'true' + run: npm run build && npm run check-package + + - name: Publish to npm + if: steps.version.outputs.publish == 'true' + run: npm publish diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d8be1c4b..bc8376c2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -38,6 +38,10 @@ jobs: - name: Run MCP server unit tests run: npm --prefix packages/mcp-server test + # Tests run from src/, so check the built npm package separately + - name: Build MCP server and check package contents + run: npm --prefix packages/mcp-server run build && npm --prefix packages/mcp-server run check-package + e2e: name: E2E Tests runs-on: ubuntu-latest diff --git a/README.md b/README.md index 7362bb90..5c94015a 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,17 @@ A Next.js web application that integrates AI capabilities with draw.io diagrams. > Note: Thanks to [ByteDance Doubao](https://www.volcengine.com/activity/codingplan?ac=MMAP8JTTCAQ2&rc=Z9Z3LDTJ&utm_campaign=drawio&utm_content=drawio&utm_medium=devrel&utm_source=OWO&utm_term=drawio) sponsorship, the demo site now uses the powerful glm-4.7 model! +

+ + + + Atlas Cloud + + +

+ +> 🎁 Thanks to **[Atlas Cloud](https://www.atlascloud.ai/?utm_source=github&utm_medium=link&utm_campaign=next-ai-draw-io)** for sponsoring next-ai-draw-io. Its OpenAI-compatible API gives diagram workflows one provider connection for DeepSeek, Qwen, GLM, Kimi, MiniMax, and more. Budget-friendly access is available through the [Coding Plan](https://www.atlascloud.ai/console/coding-plan). + https://github.com/user-attachments/assets/9d60a3e8-4a1c-4b5e-acbb-26af2d3eabd1 @@ -127,6 +138,13 @@ Then ask Claude to create diagrams: The diagram appears in your browser in real-time! +The MCP server includes most of the web app's drawing features: + +- The same drawing rules and shape libraries (AWS, Azure, GCP, Kubernetes and more) +- A screenshot tool, so the AI can check the rendered diagram and fix it +- Version history, multi-page diagrams, and download as `.drawio`, `.png`, `.svg`, or `.drawio.svg` +- Auto-save to `~/.next-ai-drawio/`, so you can continue a diagram after a restart + See the [MCP Server README](./packages/mcp-server/README.md) for VS Code, Cursor, and other client configurations. ## Getting Started @@ -217,6 +235,7 @@ See the [Next.js deployment documentation](https://nextjs.org/docs/app/building- - ModelScope - SGLang - Vercel AI Gateway +- [Atlas Cloud](https://www.atlascloud.ai/?utm_source=github&utm_medium=link&utm_campaign=next-ai-draw-io) All providers except AWS Bedrock and OpenRouter support custom endpoints. @@ -253,6 +272,8 @@ Diagrams are represented as XML that can be rendered in draw.io. The AI processe **Special thanks to [ByteDance Doubao](https://www.volcengine.com/activity/codingplan?ac=MMAP8JTTCAQ2&rc=Z9Z3LDTJ&utm_campaign=drawio&utm_content=drawio&utm_medium=devrel&utm_source=OWO&utm_term=drawio) for sponsoring the API token usage of the demo site!** Register on the ARK platform to get 500K free tokens for all models! +**Special thanks to [Atlas Cloud](https://www.atlascloud.ai/?utm_source=github&utm_medium=link&utm_campaign=next-ai-draw-io) for sponsoring next-ai-draw-io and supporting its multi-provider ecosystem!** Try its OpenAI-compatible LLM API through the [Atlas Cloud Coding Plan](https://www.atlascloud.ai/console/coding-plan). + If you find this project useful, please consider [sponsoring](https://github.com/sponsors/DayuanJiang) to help me host the live demo site! For support or inquiries, please open an issue on the GitHub repository or contact the maintainer at: diff --git a/app/[lang]/admin/models-section.tsx b/app/[lang]/admin/models-section.tsx index 4f293c70..242d3ab8 100644 --- a/app/[lang]/admin/models-section.tsx +++ b/app/[lang]/admin/models-section.tsx @@ -33,8 +33,10 @@ import { import { Switch } from "@/components/ui/switch" import { useDictionary } from "@/hooks/use-dictionary" import { formatMessage } from "@/lib/i18n/utils" +import { STORAGE_KEYS } from "@/lib/storage" import { FIXED_CRED_PROVIDERS, + generateId, PROVIDER_INFO, type ProviderName, SUGGESTED_MODELS, @@ -87,6 +89,11 @@ function ProviderDetail({ try { const data = await adminFetch("/api/admin/test-model", password, { method: "POST", + // EdgeOne's function also checks the access code + headers: { + "x-access-code": + localStorage.getItem(STORAGE_KEYS.accessCode) || "", + }, body: JSON.stringify({ provider, modelId }), }) setTestResults((prev) => ({ @@ -225,6 +232,7 @@ function ProviderDetail({ {suggestions.length > 0 && ( addProvider(v as ProviderName)} > diff --git a/app/[lang]/admin/page.tsx b/app/[lang]/admin/page.tsx index cbf5bba5..d66924d2 100644 --- a/app/[lang]/admin/page.tsx +++ b/app/[lang]/admin/page.tsx @@ -37,6 +37,19 @@ import { SettingField } from "./setting-field" const NAV_GROUP_IDS = ["models", ...SETTING_GROUPS.map((g) => g.id)] +// For each toggleable group, whether any of its settings has a value (from +// the settings file or the environment) +function groupsWithValues(map: SettingsMap): Record { + const result: Record = {} + for (const group of SETTING_GROUPS) { + if (!group.toggleable) continue + result[group.id] = !!SETTINGS_BY_GROUP.get(group.id)?.some( + (d) => map[d.key]?.source !== "default", + ) + } + return result +} + export default function AdminPage() { const dict = useDictionary() // Localized group title/description, keyed by group id @@ -62,6 +75,8 @@ export default function AdminPage() { // Models section state const [providers, setProviders] = useState([]) const [envProviders, setEnvProviders] = useState([]) + // Whether .env itself sets AI_MODEL (a default the panel would override) + const [envHasDefaultModel, setEnvHasDefaultModel] = useState(false) const [savedProviders, setSavedProviders] = useState("[]") const providersDirty = JSON.stringify(providers) !== savedProviders @@ -88,15 +103,13 @@ export default function AdminPage() { const map: SettingsMap = {} for (const s of data.settings) map[s.key] = s setSettings(map) - // Seed each toggle once from whether the group has configured - // values; don't stomp a user's explicit toggle on later saves + // A group stays on while it still has values (e.g. from env vars + // that saving can't remove); a user's explicit "on" for a group + // with no values yet is kept across saves setEnabledGroups((prev) => { - const next = { ...prev } - for (const group of SETTING_GROUPS) { - if (!group.toggleable || group.id in next) continue - next[group.id] = !!SETTINGS_BY_GROUP.get(group.id)?.some( - (d) => map[d.key]?.source !== "default", - ) + const next = groupsWithValues(map) + for (const id of Object.keys(next)) { + next[id] = next[id] || !!prev[id] } return next }) @@ -108,10 +121,12 @@ export default function AdminPage() { (data: { providers: AdminProvider[] envProviders?: EnvProvider[] + envHasDefaultModel?: boolean }) => { setProviders(data.providers) setSavedProviders(JSON.stringify(data.providers)) setEnvProviders(data.envProviders ?? []) + setEnvHasDefaultModel(!!data.envHasDefaultModel) }, [], ) @@ -181,8 +196,9 @@ export default function AdminPage() { return () => observer.disconnect() }, [authedPassword]) + // value undefined drops the pending change (back to the saved value) const handleChange = useCallback( - (key: string, value: string | null) => { + (key: string, value: string | null | undefined) => { setSaveMessage(null) setErrors((prev) => { if (!(key in prev)) return prev @@ -201,7 +217,7 @@ export default function AdminPage() { value === "" && (!state || state.source !== "file") && !isSecretValue(state?.value) - if (isRevert || isNoop) { + if (value === undefined || isRevert || isNoop) { const next = { ...prev } delete next[key] return next @@ -225,9 +241,10 @@ export default function AdminPage() { const next = { ...prev } for (const key of keys) { if (!enabled) { - // Stage deletion only for values currently set - if (settings[key]?.source !== "default") - next[key] = null + // Stage deletion of saved values; drop unsaved input + if (settings[key]?.source === "default") + delete next[key] + else next[key] = null } else if (next[key] === null) { delete next[key] } @@ -447,6 +464,7 @@ export default function AdminPage() { { @@ -462,6 +480,11 @@ export default function AdminPage() { const defs = SETTINGS_BY_GROUP.get(group.id) ?? [] const groupOff = group.toggleable && !enabledGroups[group.id] + // Values from env vars can't be removed here, so the + // group can't be turned off from the panel + const envLocked = defs.some( + (d) => settings[d.key]?.source === "env", + ) const fieldsDisabled = !writable || saving || !!groupOff const gt = groupText(group.id) const title = gt?.title ?? group.title @@ -480,6 +503,11 @@ export default function AdminPage() { {group.toggleable && (