Files
turbostarter/apps/web/src/modules/copilot/hooks/useGraphMutation.ts
Alejandro Gutiérrez c4379afe1f
Some checks failed
CI / Tests / 🧪 Test (push) Has been cancelled
feat: implement Stories 3.4, 3.5, 3.6 — AI proposals, wizard, hover & palette
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>
2026-03-01 08:55:06 +00:00

107 lines
3.0 KiB
TypeScript

"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"];
}
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);
const setLayoutDirection = useGraphStore((s) => s.setLayoutDirection);
const setEdgeRouting = useGraphStore((s) => s.setEdgeRouting);
const requestLayout = useGraphStore((s) => s.requestLayout);
const applyGraphPatch = useCallback(
(patch: GraphPatchData) => {
const graphData = patchToGraphData(patch, diagramType);
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();
persistGraphData(diagramId, graphData);
},
[
diagramId,
diagramType,
setNodes,
setEdges,
setLayoutDirection,
setEdgeRouting,
requestLayout,
],
);
const proposeGraphPatch = useCallback(
(patch: GraphPatchData) => {
const graphData = patchToGraphData(patch, diagramType);
useGraphStore.getState().proposeChanges(graphData);
},
[diagramType],
);
return { applyGraphPatch, proposeGraphPatch };
}