mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-02 14:22:28 +08:00
refactor: replace text-based edit_diagram with ID-based operations (#267)
* refactor: replace text-based edit_diagram with ID-based operations - Add applyDiagramOperations() function using DOMParser for ID lookup - New schema: operations array with type (update/add/delete), cell_id, new_xml - Update chat-panel.tsx handler for new operations format - Update OperationsDisplay component to show operation type and cell_id - Simplify system prompts with new ID-based examples - Add ID validation for add operations - Add warning for edges referencing deleted cells * fix: add ID validation to update operation and remove dead code - Add ID mismatch validation to update operation (consistency with add) - Remove orphaned replaceXMLParts function (~300 lines of dead code) - Update cell_id schema description for clarity - Add unit tests for applyDiagramOperations (11 tests)
This commit is contained in:
@@ -10,9 +10,7 @@ import {
|
||||
Cpu,
|
||||
FileCode,
|
||||
FileText,
|
||||
Minus,
|
||||
Pencil,
|
||||
Plus,
|
||||
RotateCcw,
|
||||
ThumbsDown,
|
||||
ThumbsUp,
|
||||
@@ -38,9 +36,10 @@ import {
|
||||
import ExamplePanel from "./chat-example-panel"
|
||||
import { CodeBlock } from "./code-block"
|
||||
|
||||
interface EditPair {
|
||||
search: string
|
||||
replace: string
|
||||
interface DiagramOperation {
|
||||
type: "update" | "add" | "delete"
|
||||
cell_id: string
|
||||
new_xml?: string
|
||||
}
|
||||
|
||||
// Tool part interface for type safety
|
||||
@@ -48,49 +47,44 @@ interface ToolPartLike {
|
||||
type: string
|
||||
toolCallId: string
|
||||
state?: string
|
||||
input?: { xml?: string; edits?: EditPair[] } & Record<string, unknown>
|
||||
input?: {
|
||||
xml?: string
|
||||
operations?: DiagramOperation[]
|
||||
} & Record<string, unknown>
|
||||
output?: string
|
||||
}
|
||||
|
||||
function EditDiffDisplay({ edits }: { edits: EditPair[] }) {
|
||||
function OperationsDisplay({ operations }: { operations: DiagramOperation[] }) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{edits.map((edit, index) => (
|
||||
{operations.map((op, index) => (
|
||||
<div
|
||||
key={`${(edit.search || "").slice(0, 50)}-${(edit.replace || "").slice(0, 50)}-${index}`}
|
||||
key={`${op.type}-${op.cell_id}-${index}`}
|
||||
className="rounded-lg border border-border/50 overflow-hidden bg-background/50"
|
||||
>
|
||||
<div className="px-3 py-1.5 bg-muted/40 border-b border-border/30 flex items-center gap-2">
|
||||
<span className="text-xs font-medium text-muted-foreground">
|
||||
Change {index + 1}
|
||||
<span
|
||||
className={`text-[10px] font-medium uppercase tracking-wide ${
|
||||
op.type === "delete"
|
||||
? "text-red-600"
|
||||
: op.type === "add"
|
||||
? "text-green-600"
|
||||
: "text-blue-600"
|
||||
}`}
|
||||
>
|
||||
{op.type}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
cell_id: {op.cell_id}
|
||||
</span>
|
||||
</div>
|
||||
<div className="divide-y divide-border/30">
|
||||
{/* Search (old) */}
|
||||
{op.new_xml && (
|
||||
<div className="px-3 py-2">
|
||||
<div className="flex items-center gap-1.5 mb-1.5">
|
||||
<Minus className="w-3 h-3 text-red-500" />
|
||||
<span className="text-[10px] font-medium text-red-600 uppercase tracking-wide">
|
||||
Remove
|
||||
</span>
|
||||
</div>
|
||||
<pre className="text-[11px] font-mono text-red-700 bg-red-50 rounded px-2 py-1.5 overflow-x-auto whitespace-pre-wrap break-all">
|
||||
{edit.search}
|
||||
<pre className="text-[11px] font-mono text-foreground/80 bg-muted/30 rounded px-2 py-1.5 overflow-x-auto whitespace-pre-wrap break-all">
|
||||
{op.new_xml}
|
||||
</pre>
|
||||
</div>
|
||||
{/* Replace (new) */}
|
||||
<div className="px-3 py-2">
|
||||
<div className="flex items-center gap-1.5 mb-1.5">
|
||||
<Plus className="w-3 h-3 text-green-500" />
|
||||
<span className="text-[10px] font-medium text-green-600 uppercase tracking-wide">
|
||||
Add
|
||||
</span>
|
||||
</div>
|
||||
<pre className="text-[11px] font-mono text-green-700 bg-green-50 rounded px-2 py-1.5 overflow-x-auto whitespace-pre-wrap break-all">
|
||||
{edit.replace}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -582,9 +576,9 @@ export function ChatMessageDisplay({
|
||||
{typeof input === "object" && input.xml ? (
|
||||
<CodeBlock code={input.xml} language="xml" />
|
||||
) : typeof input === "object" &&
|
||||
input.edits &&
|
||||
Array.isArray(input.edits) ? (
|
||||
<EditDiffDisplay edits={input.edits} />
|
||||
input.operations &&
|
||||
Array.isArray(input.operations) ? (
|
||||
<OperationsDisplay operations={input.operations} />
|
||||
) : typeof input === "object" &&
|
||||
Object.keys(input).length > 0 ? (
|
||||
<CodeBlock
|
||||
|
||||
@@ -323,8 +323,12 @@ ${finalXml}
|
||||
}
|
||||
}
|
||||
} else if (toolCall.toolName === "edit_diagram") {
|
||||
const { edits } = toolCall.input as {
|
||||
edits: Array<{ search: string; replace: string }>
|
||||
const { operations } = toolCall.input as {
|
||||
operations: Array<{
|
||||
type: "update" | "add" | "delete"
|
||||
cell_id: string
|
||||
new_xml?: string
|
||||
}>
|
||||
}
|
||||
|
||||
let currentXml = ""
|
||||
@@ -338,8 +342,36 @@ ${finalXml}
|
||||
currentXml = await onFetchChart(false)
|
||||
}
|
||||
|
||||
const { replaceXMLParts } = await import("@/lib/utils")
|
||||
const editedXml = replaceXMLParts(currentXml, edits)
|
||||
const { applyDiagramOperations } = await import(
|
||||
"@/lib/utils"
|
||||
)
|
||||
const { result: editedXml, errors } =
|
||||
applyDiagramOperations(currentXml, operations)
|
||||
|
||||
// Check for operation errors
|
||||
if (errors.length > 0) {
|
||||
const errorMessages = errors
|
||||
.map(
|
||||
(e) =>
|
||||
`- ${e.type} on cell_id="${e.cellId}": ${e.message}`,
|
||||
)
|
||||
.join("\n")
|
||||
|
||||
addToolOutput({
|
||||
tool: "edit_diagram",
|
||||
toolCallId: toolCall.toolCallId,
|
||||
state: "output-error",
|
||||
errorText: `Some operations failed:\n${errorMessages}
|
||||
|
||||
Current diagram XML:
|
||||
\`\`\`xml
|
||||
${currentXml}
|
||||
\`\`\`
|
||||
|
||||
Please check the cell IDs and retry.`,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// loadDiagram validates and returns error if invalid
|
||||
const validationError = onDisplayChart(editedXml)
|
||||
@@ -359,7 +391,7 @@ Current diagram XML:
|
||||
${currentXml}
|
||||
\`\`\`
|
||||
|
||||
Please fix the edit to avoid structural issues (e.g., duplicate IDs, invalid references).`,
|
||||
Please fix the operations to avoid structural issues.`,
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -367,7 +399,7 @@ Please fix the edit to avoid structural issues (e.g., duplicate IDs, invalid ref
|
||||
addToolOutput({
|
||||
tool: "edit_diagram",
|
||||
toolCallId: toolCall.toolCallId,
|
||||
output: `Successfully applied ${edits.length} edit(s) to the diagram.`,
|
||||
output: `Successfully applied ${operations.length} operation(s) to the diagram.`,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("[edit_diagram] Failed:", error)
|
||||
@@ -375,7 +407,6 @@ Please fix the edit to avoid structural issues (e.g., duplicate IDs, invalid ref
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error)
|
||||
|
||||
// Use addToolOutput with state: 'output-error' for proper error signaling
|
||||
addToolOutput({
|
||||
tool: "edit_diagram",
|
||||
toolCallId: toolCall.toolCallId,
|
||||
@@ -387,7 +418,7 @@ Current diagram XML:
|
||||
${currentXml || "No XML available"}
|
||||
\`\`\`
|
||||
|
||||
Please retry with an adjusted search pattern or use display_diagram if retries are exhausted.`,
|
||||
Please check cell IDs and retry, or use display_diagram to regenerate.`,
|
||||
})
|
||||
}
|
||||
} else if (toolCall.toolName === "append_diagram") {
|
||||
|
||||
Reference in New Issue
Block a user