feat: implement Story 3.2 — AI diagram generation from natural language

Add complete AI-powered diagram generation pipeline: natural language input
→ type inference → graph patch generation → validated canvas render with
ELK.js layout animation. Includes adversarial code review fixes for
diagramType enum validation, duplicate ID detection, tool part history
preservation, PATCH error handling, and graphData structural validation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-02-28 13:34:46 +00:00
parent 26215d9060
commit 6dcb4dcd6f
13 changed files with 1577 additions and 44 deletions

View File

@@ -0,0 +1,93 @@
"use client";
import { useCallback } from "react";
import { toast } from "sonner";
import { api } from "~/lib/api/client";
import { useGraphStore } from "~/modules/diagram/stores/useGraphStore";
import { graphToFlow } from "~/modules/diagram/lib/graph-converter";
import type { DiagramType, GraphData } from "~/modules/diagram/types/graph";
interface GraphPatchData {
meta: {
diagramType: string;
title: string;
version?: string;
layoutDirection?: "DOWN" | "RIGHT" | "LEFT" | "UP";
edgeRouting?: "ORTHOGONAL" | "SPLINES" | "POLYLINE";
};
nodes: GraphData["nodes"];
edges: GraphData["edges"];
pools?: GraphData["pools"];
groups?: GraphData["groups"];
}
export function useGraphMutation(diagramId: string, diagramType: DiagramType) {
const setNodes = useGraphStore((s) => s.setNodes);
const setEdges = useGraphStore((s) => s.setEdges);
const setLayoutDirection = useGraphStore((s) => s.setLayoutDirection);
const setEdgeRouting = useGraphStore((s) => s.setEdgeRouting);
const requestLayout = useGraphStore((s) => s.requestLayout);
const applyGraphPatch = useCallback(
(patch: GraphPatchData) => {
const effectiveDiagramType =
(patch.meta.diagramType as DiagramType) ?? diagramType;
const graphData: GraphData = {
meta: {
version: patch.meta.version ?? "1",
title: patch.meta.title,
diagramType: effectiveDiagramType,
layoutDirection: patch.meta.layoutDirection,
edgeRouting: patch.meta.edgeRouting,
},
nodes: patch.nodes,
edges: patch.edges,
pools: patch.pools,
groups: patch.groups,
};
const { nodes, edges } = graphToFlow(graphData);
setNodes(nodes);
setEdges(edges);
if (graphData.meta?.layoutDirection) {
setLayoutDirection(graphData.meta.layoutDirection);
}
if (graphData.meta?.edgeRouting) {
setEdgeRouting(graphData.meta.edgeRouting);
}
requestLayout();
// Persist graphData to database (fire-and-forget with error reporting)
api.diagrams[":id"]
.$patch({
param: { id: diagramId },
json: { graphData: graphData as unknown as Record<string, unknown> },
})
.then((res) => {
if (!res.ok) {
toast.error("Failed to save diagram — changes may be lost on reload");
}
})
.catch(() => {
toast.error("Failed to save diagram — changes may be lost on reload");
});
},
[
diagramId,
diagramType,
setNodes,
setEdges,
setLayoutDirection,
setEdgeRouting,
requestLayout,
],
);
return { applyGraphPatch };
}