refactor(lib): reuse MCP helpers for streaming preview and SVG extraction

- lib/utils.ts: remove convertToLegalXml and replaceNodes; the streaming
  preview now goes through wrapCellsInModel, autoFixXml and
  replacePageModel like the final commit path does
- lib/utils.ts: extractDiagramXML reuses extractSvgEmbeddedXml,
  pageElement and decompressPageContent instead of hand-written SVG
  parsing and pako inflation
- lib/use-file-processor.tsx: messages parameter is required, drop the
  English default texts only tests used
- hooks/use-dictionary.ts, lib/i18n/utils.ts: drop duplicate default
  exports nobody imported
- tests updated accordingly
This commit is contained in:
dayuan.jiang
2026-10-12 08:05:13 +09:00
parent 07f3e3c2a3
commit a3664367ce
8 changed files with 73 additions and 316 deletions
+29
View File
@@ -103,6 +103,35 @@ describe("the streaming preview of display_diagram", () => {
expect(loaded).not.toContain("mxStyle")
expect(loaded).not.toContain('id="b"')
})
it("fixes an unescaped & and HTML in a label, as the final diagram does", () => {
mocks.loadDiagram.mockClear()
const xml = `<mxCell id="a" value="R & D <b>team</b>" vertex="1" parent="1"><mxGeometry x="0" y="0" width="80" height="40" as="geometry"/></mxCell>`
renderHook(() =>
useDiagramStreaming({
messages: [
{
id: "m1",
role: "assistant",
parts: [
{
type: "tool-display_diagram",
toolCallId: "draw-2",
state: "input-streaming",
input: { xml },
},
],
},
] as any,
processedToolCallsRef: { current: new Set() },
editDiagramOriginalXmlRef: { current: new Map() },
loadedMessageIdsRef: { current: new Set() },
}),
)
expect(mocks.loadDiagram).toHaveBeenCalledTimes(1)
const loaded = mocks.loadDiagram.mock.calls[0][0]
expect(loaded).toContain('value="R &amp; D &lt;b&gt;team&lt;/b&gt;"')
})
})
describe("the streaming preview of edit_diagram", () => {
+5 -3
View File
@@ -22,6 +22,8 @@ function deferred<T>() {
const pdfFile = () =>
new File(["%PDF"], "slow.pdf", { type: "application/pdf" })
const textFile = () => new File(["notes"], "notes.txt", { type: "text/plain" })
// The toast texts the hook formats; toast is mocked, so any texts do
const messages = { tooLong: "{name} too long", readFailed: "failed {name}" }
describe("useFileProcessor", () => {
beforeEach(() => {
@@ -35,7 +37,7 @@ describe("useFileProcessor", () => {
vi.mocked(extractTextFileContent).mockResolvedValue("notes")
const a = pdfFile()
const b = textFile()
const { result } = renderHook(() => useFileProcessor())
const { result } = renderHook(() => useFileProcessor(messages))
let done!: Promise<void>
act(() => {
@@ -59,7 +61,7 @@ describe("useFileProcessor", () => {
vi.mocked(extractTextFileContent).mockResolvedValue("notes")
const a = pdfFile()
const b = textFile()
const { result } = renderHook(() => useFileProcessor())
const { result } = renderHook(() => useFileProcessor(messages))
let first!: Promise<void>
act(() => {
@@ -82,7 +84,7 @@ describe("useFileProcessor", () => {
const pdf = deferred<string>()
vi.mocked(extractPdfText).mockReturnValue(pdf.promise)
const a = pdfFile()
const { result } = renderHook(() => useFileProcessor())
const { result } = renderHook(() => useFileProcessor(messages))
let first!: Promise<void>
act(() => {
-34
View File
@@ -5,7 +5,6 @@ import {
extractCompleteMxCells,
extractDiagramXML,
isRealDiagram,
replaceNodes,
} from "@/lib/utils"
import { BLANK_MXFILE } from "@/packages/mcp-server/src/pages.ts"
@@ -70,39 +69,6 @@ describe("extractCompleteMxCells", () => {
const pageBox = (id: string) => `<mxCell id="${id}" vertex="1" parent="1"/>`
const pageModel = (cells: string) =>
`<mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/>${cells}</root></mxGraphModel>`
const pageOf = (id: string, cells: string) =>
`<diagram id="${id}" name="${id}">${pageModel(cells)}</diagram>`
/** Ids of the shapes on a page (not the root cells) */
const shapesOn = (xml: string, pageId: string) =>
Array.from(
new DOMParser()
.parseFromString(xml, "text/xml")
.querySelectorAll(`diagram[id="${pageId}"] mxCell`),
)
.map((cell) => cell.getAttribute("id"))
.filter((id) => id !== "0" && id !== "1")
describe("replaceNodes", () => {
const two = `<mxfile>${pageOf("a", pageBox("A"))}${pageOf("b", pageBox("B"))}</mxfile>`
it("replaces the first page's cells by default", () => {
const out = replaceNodes(two, pageBox("N"))
expect(shapesOn(out, "a")).toEqual(["N"])
expect(shapesOn(out, "b")).toEqual(["B"])
})
it("replaces the given page's cells and keeps the others", () => {
const out = replaceNodes(two, pageBox("N"), "b")
expect(shapesOn(out, "a")).toEqual(["A"])
expect(shapesOn(out, "b")).toEqual(["N"])
})
it("uses the first page when the given page is not there", () => {
const out = replaceNodes(two, pageBox("N"), "zzz")
expect(shapesOn(out, "a")).toEqual(["N"])
expect(shapesOn(out, "b")).toEqual(["B"])
})
})
describe("extractDiagramXML", () => {
const packed = (model: string) =>