Compare commits

...

2 Commits

Author SHA1 Message Date
github-actions[bot]
68f74f74e0 style: auto-format with Biome 2025-12-18 14:03:12 +00:00
dayuan.jiang
4ad505f432 chore: add auto-format workflow and fix formatting
- Add GitHub Action to auto-format PRs with Biome
- Fix formatting in app/manifest.ts and scripts/test-diagram-operations.mjs
2025-12-18 23:02:19 +09:00
5 changed files with 239 additions and 67 deletions

47
.github/workflows/auto-format.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
name: Auto Format
on:
pull_request:
types: [opened, synchronize]
permissions:
contents: write
jobs:
format:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install Biome
run: npm install --save-dev @biomejs/biome
- name: Run Biome format
run: npx @biomejs/biome check --write --no-errors-on-unmatched .
- name: Check for changes
id: changes
run: |
if git diff --quiet; then
echo "has_changes=false" >> $GITHUB_OUTPUT
else
echo "has_changes=true" >> $GITHUB_OUTPUT
fi
- name: Commit changes
if: steps.changes.outputs.has_changes == 'true'
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git add .
git commit -m "style: auto-format with Biome"
git push

View File

@@ -1,27 +1,28 @@
import type { MetadataRoute } from "next";
import type { MetadataRoute } from "next"
export default function manifest(): MetadataRoute.Manifest {
return {
name: 'Next AI Draw.io',
short_name: 'AIDraw.io',
description: 'Create AWS architecture diagrams, flowcharts, and technical diagrams using AI. Free online tool integrating draw.io with AI assistance for professional diagram creation.',
start_url: '/',
display: 'standalone',
background_color: '#f9fafb',
theme_color: '#171d26',
icons: [
{
src: '/favicon-192x192.png',
sizes: '192x192',
type: 'image/png',
purpose: 'any',
},
{
src: '/favicon-512x512.png',
sizes: '512x512',
type: 'image/png',
purpose: 'any',
},
],
}
return {
name: "Next AI Draw.io",
short_name: "AIDraw.io",
description:
"Create AWS architecture diagrams, flowcharts, and technical diagrams using AI. Free online tool integrating draw.io with AI assistance for professional diagram creation.",
start_url: "/",
display: "standalone",
background_color: "#f9fafb",
theme_color: "#171d26",
icons: [
{
src: "/favicon-192x192.png",
sizes: "192x192",
type: "image/png",
purpose: "any",
},
{
src: "/favicon-512x512.png",
sizes: "512x512",
type: "image/png",
purpose: "any",
},
],
}
}

6
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "next-ai-draw-io",
"version": "0.4.2",
"version": "0.4.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "next-ai-draw-io",
"version": "0.4.2",
"version": "0.4.3",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/amazon-bedrock": "^3.0.70",
@@ -63,7 +63,7 @@
},
"devDependencies": {
"@anthropic-ai/tokenizer": "^0.0.4",
"@biomejs/biome": "2.3.8",
"@biomejs/biome": "^2.3.8",
"@tailwindcss/postcss": "^4",
"@tailwindcss/typography": "^0.5.19",
"@types/node": "^20",

View File

@@ -73,7 +73,7 @@
},
"devDependencies": {
"@anthropic-ai/tokenizer": "^0.0.4",
"@biomejs/biome": "2.3.8",
"@biomejs/biome": "^2.3.8",
"@tailwindcss/postcss": "^4",
"@tailwindcss/typography": "^0.5.19",
"@types/node": "^20",

View File

@@ -20,7 +20,13 @@ function applyDiagramOperations(xmlContent, operations) {
if (parseError) {
return {
result: xmlContent,
errors: [{ type: "update", cellId: "", message: `XML parse error: ${parseError.textContent}` }],
errors: [
{
type: "update",
cellId: "",
message: `XML parse error: ${parseError.textContent}`,
},
],
}
}
@@ -28,7 +34,13 @@ function applyDiagramOperations(xmlContent, operations) {
if (!root) {
return {
result: xmlContent,
errors: [{ type: "update", cellId: "", message: "Could not find <root> element in XML" }],
errors: [
{
type: "update",
cellId: "",
message: "Could not find <root> element in XML",
},
],
}
}
@@ -42,22 +54,41 @@ function applyDiagramOperations(xmlContent, operations) {
if (op.type === "update") {
const existingCell = cellMap.get(op.cell_id)
if (!existingCell) {
errors.push({ type: "update", cellId: op.cell_id, message: `Cell with id="${op.cell_id}" not found` })
errors.push({
type: "update",
cellId: op.cell_id,
message: `Cell with id="${op.cell_id}" not found`,
})
continue
}
if (!op.new_xml) {
errors.push({ type: "update", cellId: op.cell_id, message: "new_xml is required for update operation" })
errors.push({
type: "update",
cellId: op.cell_id,
message: "new_xml is required for update operation",
})
continue
}
const newDoc = parser.parseFromString(`<wrapper>${op.new_xml}</wrapper>`, "text/xml")
const newDoc = parser.parseFromString(
`<wrapper>${op.new_xml}</wrapper>`,
"text/xml",
)
const newCell = newDoc.querySelector("mxCell")
if (!newCell) {
errors.push({ type: "update", cellId: op.cell_id, message: "new_xml must contain an mxCell element" })
errors.push({
type: "update",
cellId: op.cell_id,
message: "new_xml must contain an mxCell element",
})
continue
}
const newCellId = newCell.getAttribute("id")
if (newCellId !== op.cell_id) {
errors.push({ type: "update", cellId: op.cell_id, message: `ID mismatch: cell_id is "${op.cell_id}" but new_xml has id="${newCellId}"` })
errors.push({
type: "update",
cellId: op.cell_id,
message: `ID mismatch: cell_id is "${op.cell_id}" but new_xml has id="${newCellId}"`,
})
continue
}
const importedNode = doc.importNode(newCell, true)
@@ -65,22 +96,41 @@ function applyDiagramOperations(xmlContent, operations) {
cellMap.set(op.cell_id, importedNode)
} else if (op.type === "add") {
if (cellMap.has(op.cell_id)) {
errors.push({ type: "add", cellId: op.cell_id, message: `Cell with id="${op.cell_id}" already exists` })
errors.push({
type: "add",
cellId: op.cell_id,
message: `Cell with id="${op.cell_id}" already exists`,
})
continue
}
if (!op.new_xml) {
errors.push({ type: "add", cellId: op.cell_id, message: "new_xml is required for add operation" })
errors.push({
type: "add",
cellId: op.cell_id,
message: "new_xml is required for add operation",
})
continue
}
const newDoc = parser.parseFromString(`<wrapper>${op.new_xml}</wrapper>`, "text/xml")
const newDoc = parser.parseFromString(
`<wrapper>${op.new_xml}</wrapper>`,
"text/xml",
)
const newCell = newDoc.querySelector("mxCell")
if (!newCell) {
errors.push({ type: "add", cellId: op.cell_id, message: "new_xml must contain an mxCell element" })
errors.push({
type: "add",
cellId: op.cell_id,
message: "new_xml must contain an mxCell element",
})
continue
}
const newCellId = newCell.getAttribute("id")
if (newCellId !== op.cell_id) {
errors.push({ type: "add", cellId: op.cell_id, message: `ID mismatch: cell_id is "${op.cell_id}" but new_xml has id="${newCellId}"` })
errors.push({
type: "add",
cellId: op.cell_id,
message: `ID mismatch: cell_id is "${op.cell_id}" but new_xml has id="${newCellId}"`,
})
continue
}
const importedNode = doc.importNode(newCell, true)
@@ -89,7 +139,11 @@ function applyDiagramOperations(xmlContent, operations) {
} else if (op.type === "delete") {
const existingCell = cellMap.get(op.cell_id)
if (!existingCell) {
errors.push({ type: "delete", cellId: op.cell_id, message: `Cell with id="${op.cell_id}" not found` })
errors.push({
type: "delete",
cellId: op.cell_id,
message: `Cell with id="${op.cell_id}" not found`,
})
continue
}
existingCell.parentNode?.removeChild(existingCell)
@@ -149,28 +203,52 @@ test("Update operation changes cell value", () => {
{
type: "update",
cell_id: "2",
new_xml: '<mxCell id="2" value="Updated Box A" style="rounded=1;" vertex="1" parent="1"><mxGeometry x="100" y="100" width="120" height="60" as="geometry"/></mxCell>',
new_xml:
'<mxCell id="2" value="Updated Box A" style="rounded=1;" vertex="1" parent="1"><mxGeometry x="100" y="100" width="120" height="60" as="geometry"/></mxCell>',
},
])
assert(errors.length === 0, `Expected no errors, got: ${JSON.stringify(errors)}`)
assert(result.includes('value="Updated Box A"'), "Updated value should be in result")
assert(!result.includes('value="Box A"'), "Old value should not be in result")
assert(
errors.length === 0,
`Expected no errors, got: ${JSON.stringify(errors)}`,
)
assert(
result.includes('value="Updated Box A"'),
"Updated value should be in result",
)
assert(
!result.includes('value="Box A"'),
"Old value should not be in result",
)
})
test("Update operation fails for non-existent cell", () => {
const { errors } = applyDiagramOperations(sampleXml, [
{ type: "update", cell_id: "999", new_xml: '<mxCell id="999" value="Test"/>' },
{
type: "update",
cell_id: "999",
new_xml: '<mxCell id="999" value="Test"/>',
},
])
assert(errors.length === 1, "Should have one error")
assert(errors[0].message.includes("not found"), "Error should mention not found")
assert(
errors[0].message.includes("not found"),
"Error should mention not found",
)
})
test("Update operation fails on ID mismatch", () => {
const { errors } = applyDiagramOperations(sampleXml, [
{ type: "update", cell_id: "2", new_xml: '<mxCell id="WRONG" value="Test"/>' },
{
type: "update",
cell_id: "2",
new_xml: '<mxCell id="WRONG" value="Test"/>',
},
])
assert(errors.length === 1, "Should have one error")
assert(errors[0].message.includes("ID mismatch"), "Error should mention ID mismatch")
assert(
errors[0].message.includes("ID mismatch"),
"Error should mention ID mismatch",
)
})
test("Add operation creates new cell", () => {
@@ -178,41 +256,72 @@ test("Add operation creates new cell", () => {
{
type: "add",
cell_id: "new1",
new_xml: '<mxCell id="new1" value="New Box" style="rounded=1;" vertex="1" parent="1"><mxGeometry x="500" y="100" width="120" height="60" as="geometry"/></mxCell>',
new_xml:
'<mxCell id="new1" value="New Box" style="rounded=1;" vertex="1" parent="1"><mxGeometry x="500" y="100" width="120" height="60" as="geometry"/></mxCell>',
},
])
assert(errors.length === 0, `Expected no errors, got: ${JSON.stringify(errors)}`)
assert(
errors.length === 0,
`Expected no errors, got: ${JSON.stringify(errors)}`,
)
assert(result.includes('id="new1"'), "New cell should be in result")
assert(result.includes('value="New Box"'), "New cell value should be in result")
assert(
result.includes('value="New Box"'),
"New cell value should be in result",
)
})
test("Add operation fails for duplicate ID", () => {
const { errors } = applyDiagramOperations(sampleXml, [
{ type: "add", cell_id: "2", new_xml: '<mxCell id="2" value="Duplicate"/>' },
{
type: "add",
cell_id: "2",
new_xml: '<mxCell id="2" value="Duplicate"/>',
},
])
assert(errors.length === 1, "Should have one error")
assert(errors[0].message.includes("already exists"), "Error should mention already exists")
assert(
errors[0].message.includes("already exists"),
"Error should mention already exists",
)
})
test("Add operation fails on ID mismatch", () => {
const { errors } = applyDiagramOperations(sampleXml, [
{ type: "add", cell_id: "new1", new_xml: '<mxCell id="WRONG" value="Test"/>' },
{
type: "add",
cell_id: "new1",
new_xml: '<mxCell id="WRONG" value="Test"/>',
},
])
assert(errors.length === 1, "Should have one error")
assert(errors[0].message.includes("ID mismatch"), "Error should mention ID mismatch")
assert(
errors[0].message.includes("ID mismatch"),
"Error should mention ID mismatch",
)
})
test("Delete operation removes cell", () => {
const { result, errors } = applyDiagramOperations(sampleXml, [{ type: "delete", cell_id: "3" }])
assert(errors.length === 0, `Expected no errors, got: ${JSON.stringify(errors)}`)
const { result, errors } = applyDiagramOperations(sampleXml, [
{ type: "delete", cell_id: "3" },
])
assert(
errors.length === 0,
`Expected no errors, got: ${JSON.stringify(errors)}`,
)
assert(!result.includes('id="3"'), "Deleted cell should not be in result")
assert(result.includes('id="2"'), "Other cells should remain")
})
test("Delete operation fails for non-existent cell", () => {
const { errors } = applyDiagramOperations(sampleXml, [{ type: "delete", cell_id: "999" }])
const { errors } = applyDiagramOperations(sampleXml, [
{ type: "delete", cell_id: "999" },
])
assert(errors.length === 1, "Should have one error")
assert(errors[0].message.includes("not found"), "Error should mention not found")
assert(
errors[0].message.includes("not found"),
"Error should mention not found",
)
})
test("Multiple operations in sequence", () => {
@@ -220,30 +329,45 @@ test("Multiple operations in sequence", () => {
{
type: "update",
cell_id: "2",
new_xml: '<mxCell id="2" value="Updated" style="rounded=1;" vertex="1" parent="1"><mxGeometry x="100" y="100" width="120" height="60" as="geometry"/></mxCell>',
new_xml:
'<mxCell id="2" value="Updated" style="rounded=1;" vertex="1" parent="1"><mxGeometry x="100" y="100" width="120" height="60" as="geometry"/></mxCell>',
},
{
type: "add",
cell_id: "new1",
new_xml: '<mxCell id="new1" value="Added" style="rounded=1;" vertex="1" parent="1"><mxGeometry x="500" y="100" width="120" height="60" as="geometry"/></mxCell>',
new_xml:
'<mxCell id="new1" value="Added" style="rounded=1;" vertex="1" parent="1"><mxGeometry x="500" y="100" width="120" height="60" as="geometry"/></mxCell>',
},
{ type: "delete", cell_id: "3" },
])
assert(errors.length === 0, `Expected no errors, got: ${JSON.stringify(errors)}`)
assert(result.includes('value="Updated"'), "Updated value should be present")
assert(
errors.length === 0,
`Expected no errors, got: ${JSON.stringify(errors)}`,
)
assert(
result.includes('value="Updated"'),
"Updated value should be present",
)
assert(result.includes('id="new1"'), "Added cell should be present")
assert(!result.includes('id="3"'), "Deleted cell should not be present")
})
test("Invalid XML returns parse error", () => {
const { errors } = applyDiagramOperations("<not valid xml", [{ type: "delete", cell_id: "1" }])
const { errors } = applyDiagramOperations("<not valid xml", [
{ type: "delete", cell_id: "1" },
])
assert(errors.length === 1, "Should have one error")
})
test("Missing root element returns error", () => {
const { errors } = applyDiagramOperations("<mxfile></mxfile>", [{ type: "delete", cell_id: "1" }])
const { errors } = applyDiagramOperations("<mxfile></mxfile>", [
{ type: "delete", cell_id: "1" },
])
assert(errors.length === 1, "Should have one error")
assert(errors[0].message.includes("root"), "Error should mention root element")
assert(
errors[0].message.includes("root"),
"Error should mention root element",
)
})
// Summary