=== NEW === - studio/ — MCPEngine Studio scaffold (Next.js monorepo, build plan) - docs/FACTORY-V2.md — Factory v2 architecture doc - docs/CALENDLY_MCP_BUILD_SUMMARY.md — Calendly MCP build report === UPDATED SERVERS === - fieldedge: Added jobs-tools, UI build script, main entry update - lightspeed: Updated main + server entry points - squarespace: Added collection-browser + page-manager apps - toast: Added main + server entry points === INFRA === - infra/command-center/state.json — Updated pipeline state - infra/command-center/FACTORY-V2.md — Factory v2 operator playbook
207 lines
4.9 KiB
TypeScript
207 lines
4.9 KiB
TypeScript
import { create } from 'zustand';
|
|
import {
|
|
type Node,
|
|
type Edge,
|
|
type OnNodesChange,
|
|
type OnEdgesChange,
|
|
applyNodeChanges,
|
|
applyEdgeChanges,
|
|
} from '@xyflow/react';
|
|
|
|
import type { ToolDefinition, ToolGroup } from '@mcpengine/ai-pipeline/types';
|
|
import type { ToolNodeData } from '../components/canvas/ToolNode';
|
|
import type { GroupNodeData } from '../components/canvas/GroupNode';
|
|
|
|
interface CanvasState {
|
|
// State
|
|
nodes: Node[];
|
|
edges: Edge[];
|
|
selectedNodeId: string | null;
|
|
inspectorOpen: boolean;
|
|
|
|
// Node/Edge change handlers (for React Flow)
|
|
setNodes: OnNodesChange;
|
|
setEdges: OnEdgesChange;
|
|
|
|
// Actions
|
|
selectNode: (id: string | null) => void;
|
|
deselectAll: () => void;
|
|
addTool: (tool: ToolDefinition, position?: { x: number; y: number }) => void;
|
|
removeTool: (toolName: string) => void;
|
|
updateTool: (toolName: string, updates: Partial<ToolDefinition>) => void;
|
|
toggleInspector: () => void;
|
|
|
|
// Initialization
|
|
initializeFromTools: (tools: ToolDefinition[], groups?: ToolGroup[]) => void;
|
|
}
|
|
|
|
const GRID_COLS = 3;
|
|
const NODE_WIDTH = 300;
|
|
const NODE_HEIGHT = 160;
|
|
const GAP_X = 40;
|
|
const GAP_Y = 40;
|
|
|
|
function toolToNode(
|
|
tool: ToolDefinition,
|
|
index: number,
|
|
parentId?: string
|
|
): Node {
|
|
const col = index % GRID_COLS;
|
|
const row = Math.floor(index / GRID_COLS);
|
|
|
|
return {
|
|
id: tool.name,
|
|
type: 'tool',
|
|
position: {
|
|
x: col * (NODE_WIDTH + GAP_X) + (parentId ? 16 : 0),
|
|
y: row * (NODE_HEIGHT + GAP_Y) + (parentId ? 48 : 0),
|
|
},
|
|
data: {
|
|
tool,
|
|
selected: false,
|
|
enabled: true,
|
|
} satisfies ToolNodeData,
|
|
...(parentId ? { parentId, extent: 'parent' as const } : {}),
|
|
};
|
|
}
|
|
|
|
function groupToNode(
|
|
group: ToolGroup,
|
|
groupIndex: number,
|
|
toolStartIndex: number
|
|
): Node {
|
|
const rows = Math.ceil(group.tools.length / GRID_COLS);
|
|
const width = GRID_COLS * (NODE_WIDTH + GAP_X) + 32;
|
|
const height = rows * (NODE_HEIGHT + GAP_Y) + 64;
|
|
|
|
return {
|
|
id: `group-${group.name}`,
|
|
type: 'group',
|
|
position: {
|
|
x: 0,
|
|
y: groupIndex * (height + 60),
|
|
},
|
|
data: {
|
|
group,
|
|
childCount: group.tools.length,
|
|
} satisfies GroupNodeData,
|
|
style: { width, height },
|
|
};
|
|
}
|
|
|
|
export const useCanvasState = create<CanvasState>((set, get) => ({
|
|
nodes: [],
|
|
edges: [],
|
|
selectedNodeId: null,
|
|
inspectorOpen: false,
|
|
|
|
setNodes: (changes) => {
|
|
set({
|
|
nodes: applyNodeChanges(changes, get().nodes),
|
|
});
|
|
},
|
|
|
|
setEdges: (changes) => {
|
|
set({
|
|
edges: applyEdgeChanges(changes, get().edges),
|
|
});
|
|
},
|
|
|
|
selectNode: (id) => {
|
|
set((state) => ({
|
|
selectedNodeId: id,
|
|
inspectorOpen: id !== null,
|
|
nodes: state.nodes.map((n) => ({
|
|
...n,
|
|
data: {
|
|
...n.data,
|
|
selected: n.id === id,
|
|
},
|
|
})),
|
|
}));
|
|
},
|
|
|
|
deselectAll: () => {
|
|
set((state) => ({
|
|
selectedNodeId: null,
|
|
inspectorOpen: false,
|
|
nodes: state.nodes.map((n) => ({
|
|
...n,
|
|
data: { ...n.data, selected: false },
|
|
})),
|
|
}));
|
|
},
|
|
|
|
addTool: (tool, position) => {
|
|
const existingCount = get().nodes.filter((n) => n.type === 'tool').length;
|
|
const node = toolToNode(tool, existingCount);
|
|
if (position) {
|
|
node.position = position;
|
|
}
|
|
set((state) => ({
|
|
nodes: [...state.nodes, node],
|
|
}));
|
|
},
|
|
|
|
removeTool: (toolName) => {
|
|
set((state) => ({
|
|
nodes: state.nodes.filter((n) => n.id !== toolName),
|
|
edges: state.edges.filter(
|
|
(e) => e.source !== toolName && e.target !== toolName
|
|
),
|
|
selectedNodeId:
|
|
state.selectedNodeId === toolName ? null : state.selectedNodeId,
|
|
inspectorOpen:
|
|
state.selectedNodeId === toolName ? false : state.inspectorOpen,
|
|
}));
|
|
},
|
|
|
|
updateTool: (toolName, updates) => {
|
|
set((state) => ({
|
|
nodes: state.nodes.map((n) => {
|
|
if (n.id !== toolName || n.type !== 'tool') return n;
|
|
const currentData = n.data as ToolNodeData;
|
|
return {
|
|
...n,
|
|
// If name changed, update the node id
|
|
id: updates.name ?? n.id,
|
|
data: {
|
|
...currentData,
|
|
tool: { ...currentData.tool, ...updates },
|
|
},
|
|
};
|
|
}),
|
|
}));
|
|
},
|
|
|
|
toggleInspector: () => {
|
|
set((state) => ({ inspectorOpen: !state.inspectorOpen }));
|
|
},
|
|
|
|
initializeFromTools: (tools, groups) => {
|
|
const nodes: Node[] = [];
|
|
const edges: Edge[] = [];
|
|
|
|
if (groups && groups.length > 0) {
|
|
let toolIndex = 0;
|
|
groups.forEach((group, gi) => {
|
|
// Create group node
|
|
nodes.push(groupToNode(group, gi, toolIndex));
|
|
|
|
// Create tool nodes as children
|
|
group.tools.forEach((tool, ti) => {
|
|
nodes.push(toolToNode(tool, ti, `group-${group.name}`));
|
|
toolIndex++;
|
|
});
|
|
});
|
|
} else {
|
|
// Flat layout — no groups
|
|
tools.forEach((tool, i) => {
|
|
nodes.push(toolToNode(tool, i));
|
|
});
|
|
}
|
|
|
|
set({ nodes, edges, selectedNodeId: null, inspectorOpen: false });
|
|
},
|
|
}));
|