From 0d06d7521327e05f285b309b0daf9c70b8c0fbd2 Mon Sep 17 00:00:00 2001 From: huangmin <2927933426@qq.com> Date: Mon, 1 Jun 2026 11:04:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E8=8A=82=E7=82=B9?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=93=8D=E4=BD=9C=E7=9A=84=E4=BD=8D=E7=BD=AE?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=E6=94=AF=E6=8C=81=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/chat/editOperations.test.ts | 43 +++++++++++++++++++++++++++++ src/lib/chat/editOperations.ts | 19 +++++++++++-- src/lib/chat/tools.ts | 4 +++ 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/src/lib/chat/editOperations.test.ts b/src/lib/chat/editOperations.test.ts index 7cabde19..2fd7d697 100644 --- a/src/lib/chat/editOperations.test.ts +++ b/src/lib/chat/editOperations.test.ts @@ -381,6 +381,49 @@ describe("applyEditOperations", () => { expect(updated!.data).toHaveProperty("status", "idle"); }); + it("updates node position from top-level position without requiring data changes", () => { + const ops: EditOperation[] = [ + { + type: "updateNode", + nodeId: "prompt-1", + position: { x: 420, y: 240 }, + data: {}, + }, + ]; + + const result = applyEditOperations(ops, { + nodes: baseNodes, + edges: baseEdges, + }); + + const updated = result.nodes.find((n) => n.id === "prompt-1"); + expect(updated!.position).toEqual({ x: 420, y: 240 }); + expect(updated!.data).toHaveProperty("prompt", "sunset photo"); + expect(result.applied).toBe(1); + expect(result.skipped).toEqual([]); + }); + + it("updates node position from data.position for backwards compatibility", () => { + const ops: EditOperation[] = [ + { + type: "updateNode", + nodeId: "prompt-1", + data: { position: { x: 320, y: 180 } }, + }, + ]; + + const result = applyEditOperations(ops, { + nodes: baseNodes, + edges: baseEdges, + }); + + const updated = result.nodes.find((n) => n.id === "prompt-1"); + expect(updated!.position).toEqual({ x: 320, y: 180 }); + expect(updated!.data).not.toHaveProperty("position"); + expect(result.applied).toBe(1); + expect(result.skipped).toEqual([]); + }); + it("skips with message when nodeId not found", () => { const ops: EditOperation[] = [ { diff --git a/src/lib/chat/editOperations.ts b/src/lib/chat/editOperations.ts index 27b4635b..64aa40cc 100644 --- a/src/lib/chat/editOperations.ts +++ b/src/lib/chat/editOperations.ts @@ -23,7 +23,12 @@ export type EditOperation = data?: Record; } | { type: "removeNode"; nodeId: string } - | { type: "updateNode"; nodeId: string; data: Record } + | { + type: "updateNode"; + nodeId: string; + position?: { x: number; y: number }; + data: Record; + } | { type: "addEdge"; source: string; @@ -137,19 +142,27 @@ export function applyEditOperations( } const node = nodes[nodeIndex]; + const rawPosition = + operation.position ?? + (operation.data.position as { x: number; y: number } | undefined); + const hasValidPosition = + rawPosition && + Number.isFinite(rawPosition.x) && + Number.isFinite(rawPosition.y); const safeData = sanitizeChatNodeData(node.type, operation.data); - if (Object.keys(safeData).length === 0) { + if (Object.keys(safeData).length === 0 && !hasValidPosition) { skipped.push( `updateNode: no editable fields provided for node "${operation.nodeId}"` ); break; } - + console.log("Updating node", operation.nodeId, "with data", safeData); // Update node data immutably nodes = nodes.map((n) => n.id === operation.nodeId ? { ...n, + position: hasValidPosition ? rawPosition : n.position, data: { ...n.data, ...safeData, diff --git a/src/lib/chat/tools.ts b/src/lib/chat/tools.ts index e068a6bc..ecdfd6af 100644 --- a/src/lib/chat/tools.ts +++ b/src/lib/chat/tools.ts @@ -206,6 +206,10 @@ export function createChatTools(nodeIds: string[]) { .record(z.string(), z.unknown()) .optional() .describe("Node data to set/merge for addNode/updateNode"), + position: z + .object({ x: z.number(), y: z.number() }) + .optional() + .describe("Canvas position for addNode/updateNode"), source: z .string() .optional()