mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-11 12:09:53 +08:00
* fix(chat): the AI reads and draws the page the user is viewing The model always got the first page and display_diagram replaced the whole file with one page, so drawing on page 3 of a multi-page file deleted the other pages (#241), and edits went to page 1 while the user looked at another page. Now a turn records the page the user is viewing when the message is sent. That page's model goes to the model (also as previousXml, on regenerate and in edit_diagram error messages), selected shapes are sent on any page, display_diagram and append_diagram replace only that page (a drawing that brings several pages still replaces the file), edit_diagram targets it, and the streaming preview draws on it. Compact cells and default styles take the layer and the edges of that page, in the preview and in editDiagram. The editor bridge replaces the current page in place when the other pages are unchanged, so Ctrl+Z undoes an AI change on a multi-page file in one step; when a full load is needed, the user's page is shown again afterwards. Version summaries and the compare dialog count the page the change was made on. * test: read the failed edit's output without unsafe optional chaining
126 lines
5.2 KiB
TypeScript
126 lines
5.2 KiB
TypeScript
import pako from "pako"
|
|
import { describe, expect, it } from "vitest"
|
|
import {
|
|
pageModelXml,
|
|
pageSelectorFor,
|
|
placeOnPage,
|
|
replacePageModel,
|
|
} from "@/lib/diagram-pages"
|
|
|
|
const box = (id: string) =>
|
|
`<mxCell id="${id}" value="${id}" vertex="1" parent="1"><mxGeometry x="0" y="0" width="80" height="40" as="geometry"/></mxCell>`
|
|
const model = (cells: string) =>
|
|
`<mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/>${cells}</root></mxGraphModel>`
|
|
const page = (id: string, cells: string) =>
|
|
`<diagram id="${id}" name="Page ${id}">${model(cells)}</diagram>`
|
|
const packedPage = (id: string, cells: string) => {
|
|
const packed = Buffer.from(
|
|
pako.deflateRaw(encodeURIComponent(model(cells))),
|
|
).toString("base64")
|
|
return `<diagram id="${id}" name="Page ${id}">${packed}</diagram>`
|
|
}
|
|
const TWO_PAGES = `<mxfile vars="{"x":"1"}">${page("a", box("A"))}${page("b", box("B"))}</mxfile>`
|
|
|
|
const pagesOf = (xml: string) =>
|
|
Array.from(
|
|
new DOMParser()
|
|
.parseFromString(xml, "text/xml")
|
|
.getElementsByTagName("diagram"),
|
|
).map((d) => ({
|
|
id: d.getAttribute("id"),
|
|
name: d.getAttribute("name"),
|
|
cells: Array.from(d.getElementsByTagName("mxCell"))
|
|
.map((c) => c.getAttribute("id"))
|
|
.filter((id) => id !== "0" && id !== "1"),
|
|
}))
|
|
|
|
describe("pageModelXml", () => {
|
|
it("gives the page with the id, and the first page without one", () => {
|
|
expect(pageModelXml(TWO_PAGES, "b")).toContain('id="B"')
|
|
expect(pageModelXml(TWO_PAGES, "b")).not.toContain('id="A"')
|
|
expect(pageModelXml(TWO_PAGES, null)).toContain('id="A"')
|
|
expect(pageModelXml(TWO_PAGES, "missing")).toContain('id="A"')
|
|
})
|
|
|
|
it("inflates a compressed page and passes a bare model through", () => {
|
|
const file = `<mxfile>${page("a", box("A"))}${packedPage("b", box("B"))}</mxfile>`
|
|
expect(pageModelXml(file, "b")).toContain('id="B"')
|
|
expect(pageModelXml(model(box("Z")), "b")).toBe(model(box("Z")))
|
|
expect(pageModelXml("<broken", "a")).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe("pageSelectorFor", () => {
|
|
it("selects the page only when the document has it", () => {
|
|
expect(pageSelectorFor(TWO_PAGES, "b")).toEqual({ page_id: "b" })
|
|
expect(pageSelectorFor(TWO_PAGES, "missing")).toEqual({})
|
|
expect(pageSelectorFor(TWO_PAGES, null)).toEqual({})
|
|
expect(pageSelectorFor(model(box("Z")), "b")).toEqual({})
|
|
})
|
|
})
|
|
|
|
describe("replacePageModel", () => {
|
|
it("replaces one page and keeps the others, the names and the variables", () => {
|
|
const result = replacePageModel(TWO_PAGES, "b", model(box("N")))
|
|
expect(pagesOf(result)).toEqual([
|
|
{ id: "a", name: "Page a", cells: ["A"] },
|
|
{ id: "b", name: "Page b", cells: ["N"] },
|
|
])
|
|
expect(result).toContain('vars="{"x":"1"}"')
|
|
})
|
|
|
|
it("falls back to the first page for an unknown or missing id", () => {
|
|
for (const id of ["missing", null]) {
|
|
const result = replacePageModel(TWO_PAGES, id, model(box("N")))
|
|
expect(pagesOf(result).map((p) => p.cells)).toEqual([["N"], ["B"]])
|
|
}
|
|
})
|
|
|
|
it("starts from the blank file when there is no document", () => {
|
|
for (const base of ["", "<mxGraphModel/>", "<broken"]) {
|
|
const result = replacePageModel(base, "any", model(box("N")))
|
|
expect(pagesOf(result)).toEqual([
|
|
{ id: "page-1", name: "Page-1", cells: ["N"] },
|
|
])
|
|
}
|
|
})
|
|
|
|
it("refuses anything but a model", () => {
|
|
expect(() => replacePageModel(TWO_PAGES, "a", "<root/>")).toThrow()
|
|
})
|
|
})
|
|
|
|
describe("placeOnPage", () => {
|
|
it("puts one page's worth of drawing on the model's page", () => {
|
|
const drawn = `<mxfile><diagram id="page-1" name="Page-1">${model(box("N"))}</diagram></mxfile>`
|
|
const result = placeOnPage(drawn, TWO_PAGES, "b")
|
|
expect(pagesOf(result)).toEqual([
|
|
{ id: "a", name: "Page a", cells: ["A"] },
|
|
{ id: "b", name: "Page b", cells: ["N"] },
|
|
])
|
|
})
|
|
|
|
it("applies the drawn file's variables, and keeps the canvas's otherwise", () => {
|
|
const withVars = `<mxfile vars="{"team":"New"}"><diagram id="p" name="P">${model(box("N"))}</diagram></mxfile>`
|
|
expect(placeOnPage(withVars, TWO_PAGES, "b")).toContain(
|
|
'vars="{"team":"New"}"',
|
|
)
|
|
const without = `<mxfile><diagram id="p" name="P">${model(box("N"))}</diagram></mxfile>`
|
|
expect(placeOnPage(without, TWO_PAGES, "b")).toContain(
|
|
'vars="{"x":"1"}"',
|
|
)
|
|
})
|
|
|
|
it("lets a drawing with several pages replace the document", () => {
|
|
const drawn = `<mxfile>${page("x", box("X"))}${page("y", box("Y"))}</mxfile>`
|
|
expect(placeOnPage(drawn, TWO_PAGES, "b")).toBe(drawn)
|
|
})
|
|
|
|
it("draws on the blank file when the canvas is empty", () => {
|
|
const drawn = `<mxfile><diagram id="page-1" name="Page-1">${model(box("N"))}</diagram></mxfile>`
|
|
expect(pagesOf(placeOnPage(drawn, "", null))).toEqual([
|
|
{ id: "page-1", name: "Page-1", cells: ["N"] },
|
|
])
|
|
})
|
|
})
|