fix(chat): the AI reads and draws the page the user is viewing (#974)

* 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
This commit is contained in:
Dayuan Jiang
2026-10-10 23:30:51 +09:00
committed by GitHub
parent cd5352ca88
commit d1ee1594f1
21 changed files with 907 additions and 120 deletions
+22 -21
View File
@@ -1,6 +1,6 @@
import { decompressPageContent } from "@/packages/mcp-server/src/load-diagram.ts"
import { modelOfPage, pageElement } from "@/lib/diagram-pages"
/** What changed on the first page between two versions of a diagram */
/** What changed on one page between two versions of a diagram */
export interface ChangeSummary {
shapesAdded: number
shapesRemoved: number
@@ -32,27 +32,20 @@ interface CellInfo {
signature: string
}
/** A page's mxGraphModel element, inflated when the page is compressed */
function modelOfPage(diagram: Element): Element | null {
const model = diagram.querySelector("mxGraphModel")
if (model) return model
const inflated = decompressPageContent(diagram.textContent || "")
if (!inflated) return null
const inner = new DOMParser().parseFromString(inflated, "text/xml")
return inner.querySelector("mxGraphModel")
}
function parse(xml: string): Document | null {
if (!xml?.trim()) return null
const doc = new DOMParser().parseFromString(xml, "text/xml")
return doc.querySelector("parsererror") ? null : doc
}
/** The first page's mxGraphModel element, whatever wrapper the XML has */
function firstPageModel(xml: string): Element | null {
/**
* A page's mxGraphModel element, whatever wrapper the XML has: the page
* with this id, else the first
*/
function pageModel(xml: string, pageId?: string | null): Element | null {
const doc = parse(xml)
if (!doc) return null
const diagram = doc.querySelector("diagram")
const diagram = pageElement(doc, pageId)
if (diagram) return modelOfPage(diagram)
return doc.querySelector("mxGraphModel") ?? doc.documentElement
}
@@ -133,10 +126,13 @@ function cellsIn(model: Element | null) {
})
}
/** Shapes and connectors of the first page (not the root and its layers) */
function collectCells(xml: string): Map<string, CellInfo> {
/** Shapes and connectors of one page (not the root and its layers) */
function collectCells(
xml: string,
pageId?: string | null,
): Map<string, CellInfo> {
const cells = new Map<string, CellInfo>()
for (const { id, cell, node } of cellsIn(firstPageModel(xml))) {
for (const { id, cell, node } of cellsIn(pageModel(xml, pageId))) {
if (id === "0" || cell.getAttribute("parent") === "0") continue
cells.set(id, {
isEdge: cell.getAttribute("edge") === "1",
@@ -146,9 +142,14 @@ function collectCells(xml: string): Map<string, CellInfo> {
return cells
}
export function diffDiagrams(beforeXml: string, afterXml: string): DiagramDiff {
const before = collectCells(beforeXml)
const after = collectCells(afterXml)
/** What changed on one page (the first when no id is given) */
export function diffDiagrams(
beforeXml: string,
afterXml: string,
pageId?: string | null,
): DiagramDiff {
const before = collectCells(beforeXml, pageId)
const after = collectCells(afterXml, pageId)
const summary: ChangeSummary = { ...EMPTY_SUMMARY }
const touchedIds: string[] = []