feat: implement Stories 3.4, 3.5, 3.6 — AI proposals, wizard, hover & palette
Some checks failed
CI / Tests / 🧪 Test (push) Has been cancelled

Story 3.4: AI semantic suggestions with accept/reject workflow
- ProposalBar overlay with visual diff
- Accept/reject flow with graph snapshot restore
- useProposalDiff hook for change summary
- System prompt scoping for selected elements

Story 3.5: New diagram wizard with AI type inference
- CreateDiagramDialog with AI type inference (Haiku)
- initialDescription prop for chat-first flow
- Auto-send on mount with hasSentInitial ref guard
- DB migration for diagram description column

Story 3.6: Hover affordances and command palette
- HoverAffordances toolbar (5 AI actions, debounced)
- CommandPalette (Cmd+K) with AI, nav, Go to Node
- prefillChat/fitViewRequested/focusNodeId actions
- Code review: getNodesBounds, onOpenRightPanel,
  timer cleanup, test count fix

374 tests passing (251 web + 123 AI).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-03-01 08:55:06 +00:00
parent 6591d6385a
commit c4379afe1f
32 changed files with 5828 additions and 81 deletions

View File

@@ -23,6 +23,41 @@ interface GraphPatchData {
groups?: GraphData["groups"];
}
function patchToGraphData(patch: GraphPatchData, fallbackType: DiagramType): GraphData {
const effectiveDiagramType =
(patch.meta.diagramType as DiagramType) ?? fallbackType;
return {
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,
};
}
export function persistGraphData(diagramId: string, graphData: GraphData) {
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");
});
}
export function useGraphMutation(diagramId: string, diagramType: DiagramType) {
const setNodes = useGraphStore((s) => s.setNodes);
const setEdges = useGraphStore((s) => s.setEdges);
@@ -32,23 +67,7 @@ export function useGraphMutation(diagramId: string, diagramType: DiagramType) {
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 graphData = patchToGraphData(patch, diagramType);
const { nodes, edges } = graphToFlow(graphData);
setNodes(nodes);
@@ -62,21 +81,7 @@ export function useGraphMutation(diagramId: string, diagramType: DiagramType) {
}
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");
});
persistGraphData(diagramId, graphData);
},
[
diagramId,
@@ -89,5 +94,13 @@ export function useGraphMutation(diagramId: string, diagramType: DiagramType) {
],
);
return { applyGraphPatch };
const proposeGraphPatch = useCallback(
(patch: GraphPatchData) => {
const graphData = patchToGraphData(patch, diagramType);
useGraphStore.getState().proposeChanges(graphData);
},
[diagramType],
);
return { applyGraphPatch, proposeGraphPatch };
}