@iblai/web-containers 1.52.0 → 1.52.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/components/chat/loading-message.js +13 -0
- package/dist/cjs/components/chat/loading-message.js.map +1 -0
- package/dist/cjs/components/mentors/mentor-selection-grid.js +4 -2
- package/dist/cjs/components/mentors/mentor-selection-grid.js.map +1 -1
- package/dist/cjs/components/modals/edit-mentor-modal/tabs/mcp-tab/connector-management-content.js +9 -5
- package/dist/cjs/components/modals/edit-mentor-modal/tabs/mcp-tab/connector-management-content.js.map +1 -1
- package/dist/cjs/components/workflows/action-tooltip.js +11 -0
- package/dist/cjs/components/workflows/action-tooltip.js.map +1 -0
- package/dist/cjs/components/workflows/node-config-panel.js +576 -0
- package/dist/cjs/components/workflows/node-config-panel.js.map +1 -0
- package/dist/cjs/components/workflows/tool-dialogs.js +95 -60
- package/dist/cjs/components/workflows/tool-dialogs.js.map +1 -1
- package/dist/cjs/components/workflows/workflow-canvas.js +1028 -0
- package/dist/cjs/components/workflows/workflow-canvas.js.map +1 -0
- package/dist/cjs/components/workflows/workflow-definition.js +369 -0
- package/dist/cjs/components/workflows/workflow-definition.js.map +1 -0
- package/dist/cjs/components/workflows/workflow-editor.js +523 -0
- package/dist/cjs/components/workflows/workflow-editor.js.map +1 -0
- package/dist/cjs/components/workflows/workflow-preview-chat.js +57 -0
- package/dist/cjs/components/workflows/workflow-preview-chat.js.map +1 -0
- package/dist/cjs/components/workflows/workflow-sidebar.js +2 -2
- package/dist/cjs/components/workflows/workflow-sidebar.js.map +1 -1
- package/dist/cjs/components/workflows/workflows-list.js +115 -0
- package/dist/cjs/components/workflows/workflows-list.js.map +1 -0
- package/dist/cjs/i18n/messages/en.json.js +207 -18
- package/dist/cjs/i18n/messages/en.json.js.map +1 -1
- package/dist/cjs/i18n/messages/es.json.js +201 -12
- package/dist/cjs/i18n/messages/es.json.js.map +1 -1
- package/dist/cjs/i18n/messages/fr.json.js +201 -12
- package/dist/cjs/i18n/messages/fr.json.js.map +1 -1
- package/dist/cjs/i18n/messages/zh.json.js +201 -12
- package/dist/cjs/i18n/messages/zh.json.js.map +1 -1
- package/dist/cjs/index.js +17 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/next/index.js +10 -0
- package/dist/cjs/next/index.js.map +1 -1
- package/dist/cjs/web-containers/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/components/chat/loading-message.js +11 -0
- package/dist/esm/components/chat/loading-message.js.map +1 -0
- package/dist/esm/components/mentors/mentor-selection-grid.js +4 -2
- package/dist/esm/components/mentors/mentor-selection-grid.js.map +1 -1
- package/dist/esm/components/modals/edit-mentor-modal/tabs/mcp-tab/connector-management-content.js +9 -5
- package/dist/esm/components/modals/edit-mentor-modal/tabs/mcp-tab/connector-management-content.js.map +1 -1
- package/dist/esm/components/workflows/action-tooltip.js +9 -0
- package/dist/esm/components/workflows/action-tooltip.js.map +1 -0
- package/dist/esm/components/workflows/node-config-panel.js +574 -0
- package/dist/esm/components/workflows/node-config-panel.js.map +1 -0
- package/dist/esm/components/workflows/tool-dialogs.js +96 -61
- package/dist/esm/components/workflows/tool-dialogs.js.map +1 -1
- package/dist/esm/components/workflows/workflow-canvas.js +1026 -0
- package/dist/esm/components/workflows/workflow-canvas.js.map +1 -0
- package/dist/esm/components/workflows/workflow-definition.js +352 -0
- package/dist/esm/components/workflows/workflow-definition.js.map +1 -0
- package/dist/esm/components/workflows/workflow-editor.js +521 -0
- package/dist/esm/components/workflows/workflow-editor.js.map +1 -0
- package/dist/esm/components/workflows/workflow-preview-chat.js +55 -0
- package/dist/esm/components/workflows/workflow-preview-chat.js.map +1 -0
- package/dist/esm/components/workflows/workflow-sidebar.js +2 -2
- package/dist/esm/components/workflows/workflow-sidebar.js.map +1 -1
- package/dist/esm/components/workflows/workflows-list.js +113 -0
- package/dist/esm/components/workflows/workflows-list.js.map +1 -0
- package/dist/esm/i18n/messages/en.json.js +203 -19
- package/dist/esm/i18n/messages/en.json.js.map +1 -1
- package/dist/esm/i18n/messages/es.json.js +197 -13
- package/dist/esm/i18n/messages/es.json.js.map +1 -1
- package/dist/esm/i18n/messages/fr.json.js +197 -13
- package/dist/esm/i18n/messages/fr.json.js.map +1 -1
- package/dist/esm/i18n/messages/zh.json.js +197 -13
- package/dist/esm/i18n/messages/zh.json.js.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/next/index.js +5 -0
- package/dist/esm/next/index.js.map +1 -1
- package/dist/esm/web-containers/tsconfig.tsbuildinfo +1 -1
- package/dist/index.d.ts +164 -19
- package/dist/next/index.d.ts +215 -3
- package/dist/package.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,1026 @@
|
|
|
1
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
|
+
import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
|
|
3
|
+
import { useLazyGetMentorSettingsQuery } from '@iblai/data-layer';
|
|
4
|
+
import { useT } from '../../i18n/context.js';
|
|
5
|
+
import '../../packages/i18n/dist/index.esm.js';
|
|
6
|
+
import { MentorSelectionGrid } from '../mentors/mentor-selection-grid.js';
|
|
7
|
+
import { Button } from '../ui/button.js';
|
|
8
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../ui/dialog.js';
|
|
9
|
+
import { Textarea } from '../ui/textarea.js';
|
|
10
|
+
import { ActionTooltip } from './action-tooltip.js';
|
|
11
|
+
import { NodeConfigPanel } from './node-config-panel.js';
|
|
12
|
+
import { canReceiveConnection, edgeIdFor, collectWorkflowVariables, collectVariableUsages, WHILE_LOOP_HANDLE, WHILE_DONE_HANDLE, getConditionCount, reindexConditionEdges, getConditions } from './workflow-definition.js';
|
|
13
|
+
import X from '../../node_modules/.pnpm/lucide-react@0.528.0_react@19.1.0/node_modules/lucide-react/dist/esm/icons/x.js';
|
|
14
|
+
import Trash2 from '../../node_modules/.pnpm/lucide-react@0.528.0_react@19.1.0/node_modules/lucide-react/dist/esm/icons/trash-2.js';
|
|
15
|
+
import Play from '../../node_modules/.pnpm/lucide-react@0.528.0_react@19.1.0/node_modules/lucide-react/dist/esm/icons/play.js';
|
|
16
|
+
import RefreshCw from '../../node_modules/.pnpm/lucide-react@0.528.0_react@19.1.0/node_modules/lucide-react/dist/esm/icons/refresh-cw.js';
|
|
17
|
+
import GitBranch from '../../node_modules/.pnpm/lucide-react@0.528.0_react@19.1.0/node_modules/lucide-react/dist/esm/icons/git-branch.js';
|
|
18
|
+
import ThumbsUp from '../../node_modules/.pnpm/lucide-react@0.528.0_react@19.1.0/node_modules/lucide-react/dist/esm/icons/thumbs-up.js';
|
|
19
|
+
import Bot from '../../node_modules/.pnpm/lucide-react@0.528.0_react@19.1.0/node_modules/lucide-react/dist/esm/icons/bot.js';
|
|
20
|
+
import Pencil from '../../node_modules/.pnpm/lucide-react@0.528.0_react@19.1.0/node_modules/lucide-react/dist/esm/icons/pencil.js';
|
|
21
|
+
import Hand from '../../node_modules/.pnpm/lucide-react@0.528.0_react@19.1.0/node_modules/lucide-react/dist/esm/icons/hand.js';
|
|
22
|
+
import MousePointer from '../../node_modules/.pnpm/lucide-react@0.528.0_react@19.1.0/node_modules/lucide-react/dist/esm/icons/mouse-pointer.js';
|
|
23
|
+
import Undo from '../../node_modules/.pnpm/lucide-react@0.528.0_react@19.1.0/node_modules/lucide-react/dist/esm/icons/undo.js';
|
|
24
|
+
import Redo from '../../node_modules/.pnpm/lucide-react@0.528.0_react@19.1.0/node_modules/lucide-react/dist/esm/icons/redo.js';
|
|
25
|
+
import ZoomOut from '../../node_modules/.pnpm/lucide-react@0.528.0_react@19.1.0/node_modules/lucide-react/dist/esm/icons/zoom-out.js';
|
|
26
|
+
import ZoomIn from '../../node_modules/.pnpm/lucide-react@0.528.0_react@19.1.0/node_modules/lucide-react/dist/esm/icons/zoom-in.js';
|
|
27
|
+
|
|
28
|
+
const DEFAULT_MENTOR_LABELS = new Set([
|
|
29
|
+
'my agent',
|
|
30
|
+
'agent',
|
|
31
|
+
'agent node',
|
|
32
|
+
'my mentor',
|
|
33
|
+
'mentor',
|
|
34
|
+
'mentor node',
|
|
35
|
+
]);
|
|
36
|
+
const isDefaultMentorLabel = (label) => {
|
|
37
|
+
if (!label)
|
|
38
|
+
return true;
|
|
39
|
+
return DEFAULT_MENTOR_LABELS.has(label.trim().toLowerCase());
|
|
40
|
+
};
|
|
41
|
+
// API may use 'conditional' as the node type key for if-else nodes
|
|
42
|
+
const isConditionalType = (type) => type === 'if-else' || type === 'conditional';
|
|
43
|
+
// Cards with one labelled row per outgoing branch.
|
|
44
|
+
const isBranchingType = (type) => isConditionalType(type) || type === 'while' || type === 'user-approval';
|
|
45
|
+
// Branch rows sit under a 68px header, 44px tall with an 8px gap.
|
|
46
|
+
const branchRowCenter = (index) => 90 + index * 52;
|
|
47
|
+
const branchCardHeight = (rows) => 76 + rows * 52;
|
|
48
|
+
// Rendered card sizes, for placing new steps and for box selection.
|
|
49
|
+
const nodeSize = (node) => {
|
|
50
|
+
if (isConditionalType(node.type)) {
|
|
51
|
+
const conditions = node.data ? getConditionCount(node.data) : 1;
|
|
52
|
+
return { width: 280, height: branchCardHeight(conditions + 1) };
|
|
53
|
+
}
|
|
54
|
+
if (isBranchingType(node.type)) {
|
|
55
|
+
return { width: 280, height: branchCardHeight(2) };
|
|
56
|
+
}
|
|
57
|
+
if (node.type === 'note') {
|
|
58
|
+
return { width: node.width || 200, height: node.height || 120 };
|
|
59
|
+
}
|
|
60
|
+
return { width: 140, height: 50 };
|
|
61
|
+
};
|
|
62
|
+
const SIDE_HANDLES = new Set(['left', 'right', 'top', 'bottom']);
|
|
63
|
+
// If/Else rows, While Loop/Done and Approve/Reject, as opposed to a plain
|
|
64
|
+
// step's side points.
|
|
65
|
+
const isBranchHandle = (handle) => !SIDE_HANDLES.has(handle);
|
|
66
|
+
// The only steps the backend runs that write to step_results.
|
|
67
|
+
const STEPS_WITH_RESULTS = new Set(['mentor', 'transform', 'set-state', 'while']);
|
|
68
|
+
const NODE_PANEL_SELECTOR = '[data-testid="workflow-node-panel"]';
|
|
69
|
+
// The floating zoom/undo toolbar covers the bottom of the canvas.
|
|
70
|
+
const TOOLBAR_CLEARANCE = 80;
|
|
71
|
+
const isDeletableNode = (node) => node.type !== 'start' && node.deletable !== false;
|
|
72
|
+
// Helper to strip UI-only properties from nodes for comparison/saving
|
|
73
|
+
function stripUiProperties(nodes) {
|
|
74
|
+
return nodes.map(({ selected, dragging, hidden, ...rest }) => rest);
|
|
75
|
+
}
|
|
76
|
+
// Helper to strip UI-only properties from edges for comparison/saving
|
|
77
|
+
function stripEdgeUiProperties(edges) {
|
|
78
|
+
return edges.map(({ selected, hidden, ...rest }) => rest);
|
|
79
|
+
}
|
|
80
|
+
// Default nodes for new workflows
|
|
81
|
+
const DEFAULT_NODES = [
|
|
82
|
+
{
|
|
83
|
+
id: 'start',
|
|
84
|
+
type: 'start',
|
|
85
|
+
position: { x: 300, y: 250 },
|
|
86
|
+
data: { label: 'Start' },
|
|
87
|
+
draggable: true,
|
|
88
|
+
selectable: true,
|
|
89
|
+
connectable: true,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: 'mentor-1',
|
|
93
|
+
type: 'mentor',
|
|
94
|
+
position: { x: 550, y: 250 },
|
|
95
|
+
data: { label: 'My agent', subtitle: 'Agent', color: 'chart-1' },
|
|
96
|
+
draggable: true,
|
|
97
|
+
selectable: true,
|
|
98
|
+
connectable: true,
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
const DEFAULT_EDGES = [
|
|
102
|
+
{
|
|
103
|
+
id: 'e-start-mentor-1',
|
|
104
|
+
source: 'start',
|
|
105
|
+
target: 'mentor-1',
|
|
106
|
+
sourceHandle: 'right',
|
|
107
|
+
targetHandle: 'left',
|
|
108
|
+
},
|
|
109
|
+
];
|
|
110
|
+
function WorkflowCanvas({ onDraggedItem: _onDraggedItem, onClickedItem, previewMode = false, initialNodes, initialEdges, initialViewport, onStateChange, org, defaultMentorId, username, onOpenAgentSettings, agentSettings, }) {
|
|
111
|
+
var _a;
|
|
112
|
+
const t = useT();
|
|
113
|
+
// Use initialNodes if provided (from API), otherwise use defaults for new workflows
|
|
114
|
+
const [nodes, setNodes] = useState(initialNodes && initialNodes.length > 0 ? initialNodes : DEFAULT_NODES);
|
|
115
|
+
const [edges, setEdges] = useState(initialEdges ? initialEdges : DEFAULT_EDGES);
|
|
116
|
+
const [tool, setTool] = useState('hand');
|
|
117
|
+
const [zoom, setZoom] = useState((initialViewport === null || initialViewport === void 0 ? void 0 : initialViewport.zoom) || 1);
|
|
118
|
+
const [history, setHistory] = useState([
|
|
119
|
+
{ nodes: [...nodes], edges: [...edges] },
|
|
120
|
+
]);
|
|
121
|
+
const [historyIndex, setHistoryIndex] = useState(0);
|
|
122
|
+
// Saves can run after an await (mentor prefill); a closed-over index would
|
|
123
|
+
// cut off entries added in the meantime.
|
|
124
|
+
const historyIndexRef = useRef(0);
|
|
125
|
+
const [draggedNode, setDraggedNode] = useState(null);
|
|
126
|
+
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
|
|
127
|
+
const [isPanning, setIsPanning] = useState(false);
|
|
128
|
+
const [panOffset, setPanOffset] = useState(initialViewport ? { x: initialViewport.x, y: initialViewport.y } : { x: 0, y: 0 });
|
|
129
|
+
const [panStart, setPanStart] = useState({ x: 0, y: 0 });
|
|
130
|
+
const [hoveredNode, setHoveredNode] = useState(null);
|
|
131
|
+
const [connectingFrom, setConnectingFrom] = useState(null);
|
|
132
|
+
const [tempConnectionPos, setTempConnectionPos] = useState(null);
|
|
133
|
+
const [editingNote, setEditingNote] = useState(null);
|
|
134
|
+
const [resizingNote, setResizingNote] = useState(null);
|
|
135
|
+
const [selectionBox, setSelectionBox] = useState(null);
|
|
136
|
+
const [selectedNodeForConfig, setSelectedNodeForConfig] = useState(null);
|
|
137
|
+
const [selectedEdgeId, setSelectedEdgeId] = useState(null);
|
|
138
|
+
const [hoveredEdgeId, setHoveredEdgeId] = useState(null);
|
|
139
|
+
const processedClickRef = useRef(null);
|
|
140
|
+
const [showMentorModal, setShowMentorModal] = useState(false);
|
|
141
|
+
const [mentorSearchQuery, setMentorSearchQuery] = useState('');
|
|
142
|
+
const [activeMentorNodeId, setActiveMentorNodeId] = useState(null);
|
|
143
|
+
const rootRef = useRef(null);
|
|
144
|
+
// False once the user clicks into a settings panel or a dialog, so a
|
|
145
|
+
// Delete meant for a dropdown or button there never removes the step.
|
|
146
|
+
const canvasHasPointerRef = useRef(true);
|
|
147
|
+
const canvasRef = useRef(null);
|
|
148
|
+
// Read by callbacks that run after an await (mentor prefill), which must
|
|
149
|
+
// not write back a stale copy of the graph.
|
|
150
|
+
const nodesRef = useRef(nodes);
|
|
151
|
+
const edgesRef = useRef(edges);
|
|
152
|
+
nodesRef.current = nodes;
|
|
153
|
+
edgesRef.current = edges;
|
|
154
|
+
const dragStartRef = useRef(null);
|
|
155
|
+
const dragMovedRef = useRef(false);
|
|
156
|
+
const suppressMentorClickRef = useRef(false);
|
|
157
|
+
// Track if we've loaded initial data from props
|
|
158
|
+
const hasLoadedInitialData = useRef(false);
|
|
159
|
+
// Track last emitted state to avoid duplicate onStateChange calls
|
|
160
|
+
const lastEmittedStateRef = useRef(null);
|
|
161
|
+
const [fetchMentorSettings] = useLazyGetMentorSettingsQuery();
|
|
162
|
+
// Sync nodes and edges when initialNodes/initialEdges change
|
|
163
|
+
// This handles both preview mode updates and initial data load
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
// In preview mode, always sync when props change
|
|
166
|
+
if (previewMode) {
|
|
167
|
+
if (initialNodes && initialNodes.length > 0) {
|
|
168
|
+
setNodes(initialNodes);
|
|
169
|
+
}
|
|
170
|
+
if (initialEdges) {
|
|
171
|
+
setEdges(initialEdges);
|
|
172
|
+
}
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
// In edit mode, only sync on first load (when data comes from API)
|
|
176
|
+
if (!hasLoadedInitialData.current && initialNodes && initialNodes.length > 0) {
|
|
177
|
+
setNodes(initialNodes);
|
|
178
|
+
if (initialEdges) {
|
|
179
|
+
setEdges(initialEdges);
|
|
180
|
+
}
|
|
181
|
+
// Reset history with loaded data
|
|
182
|
+
setHistory([{ nodes: initialNodes, edges: initialEdges || [] }]);
|
|
183
|
+
historyIndexRef.current = 0;
|
|
184
|
+
setHistoryIndex(0);
|
|
185
|
+
hasLoadedInitialData.current = true;
|
|
186
|
+
}
|
|
187
|
+
}, [previewMode, initialNodes, initialEdges]);
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
if (previewMode && initialViewport) {
|
|
190
|
+
setZoom(initialViewport.zoom);
|
|
191
|
+
setPanOffset({ x: initialViewport.x, y: initialViewport.y });
|
|
192
|
+
}
|
|
193
|
+
}, [previewMode, initialViewport]);
|
|
194
|
+
const saveToHistory = useCallback((newNodes, newEdges) => {
|
|
195
|
+
const index = historyIndexRef.current;
|
|
196
|
+
setHistory((prev) => {
|
|
197
|
+
const newHistory = prev.slice(0, index + 1);
|
|
198
|
+
newHistory.push({ nodes: [...newNodes], edges: [...newEdges] });
|
|
199
|
+
return newHistory.slice(-50); // Keep last 50 states
|
|
200
|
+
});
|
|
201
|
+
historyIndexRef.current = Math.min(index + 1, 49);
|
|
202
|
+
setHistoryIndex(historyIndexRef.current);
|
|
203
|
+
}, []);
|
|
204
|
+
const restoreHistory = (newIndex) => {
|
|
205
|
+
historyIndexRef.current = newIndex;
|
|
206
|
+
setHistoryIndex(newIndex);
|
|
207
|
+
setNodes(history[newIndex].nodes);
|
|
208
|
+
setEdges(history[newIndex].edges);
|
|
209
|
+
setSelectedEdgeId(null);
|
|
210
|
+
setHoveredEdgeId(null);
|
|
211
|
+
};
|
|
212
|
+
const handleUndo = () => {
|
|
213
|
+
if (historyIndex > 0)
|
|
214
|
+
restoreHistory(historyIndex - 1);
|
|
215
|
+
};
|
|
216
|
+
const handleRedo = () => {
|
|
217
|
+
if (historyIndex < history.length - 1)
|
|
218
|
+
restoreHistory(historyIndex + 1);
|
|
219
|
+
};
|
|
220
|
+
const handleZoomIn = () => {
|
|
221
|
+
setZoom((prev) => Math.min(prev + 0.1, 2));
|
|
222
|
+
};
|
|
223
|
+
const handleZoomOut = () => {
|
|
224
|
+
setZoom((prev) => Math.max(prev - 0.1, 0.5));
|
|
225
|
+
};
|
|
226
|
+
const handleUpdateNode = useCallback((nodeId, updates) => {
|
|
227
|
+
const target = nodesRef.current.find((node) => node.id === nodeId);
|
|
228
|
+
if (!target)
|
|
229
|
+
return;
|
|
230
|
+
const shouldSyncMentorId = target.type === 'mentor' &&
|
|
231
|
+
updates.entry_mentor_id !== undefined &&
|
|
232
|
+
updates.mentor_id === undefined;
|
|
233
|
+
const normalizedUpdates = shouldSyncMentorId
|
|
234
|
+
? { ...updates, mentor_id: updates.entry_mentor_id }
|
|
235
|
+
: updates;
|
|
236
|
+
const applyUpdate = (node) => node.id === nodeId ? { ...node, data: { ...node.data, ...normalizedUpdates } } : node;
|
|
237
|
+
const { conditions } = updates;
|
|
238
|
+
const moveBranchEdges = (current) => isConditionalType(target.type) && conditions
|
|
239
|
+
? reindexConditionEdges(current, nodeId, getConditions(target.data), conditions)
|
|
240
|
+
: current;
|
|
241
|
+
const newNodes = nodesRef.current.map(applyUpdate);
|
|
242
|
+
const newEdges = moveBranchEdges(edgesRef.current);
|
|
243
|
+
nodesRef.current = newNodes;
|
|
244
|
+
edgesRef.current = newEdges;
|
|
245
|
+
setNodes((prev) => prev.map(applyUpdate));
|
|
246
|
+
setEdges(moveBranchEdges);
|
|
247
|
+
saveToHistory(newNodes, newEdges);
|
|
248
|
+
}, [saveToHistory]);
|
|
249
|
+
const prefillMentorNode = useCallback(async (nodeId, mentorId, existingData) => {
|
|
250
|
+
if (!mentorId || !org || !username)
|
|
251
|
+
return;
|
|
252
|
+
try {
|
|
253
|
+
const settings = (await fetchMentorSettings({
|
|
254
|
+
mentor: mentorId,
|
|
255
|
+
org,
|
|
256
|
+
}).unwrap());
|
|
257
|
+
const mentorLabel = settings.display_name || settings.mentor_name;
|
|
258
|
+
const mentorInstructions = settings.system_prompt;
|
|
259
|
+
const mentorModel = settings.llm_name;
|
|
260
|
+
const updates = {};
|
|
261
|
+
if (mentorLabel && isDefaultMentorLabel(existingData === null || existingData === void 0 ? void 0 : existingData.label)) {
|
|
262
|
+
updates.label = mentorLabel;
|
|
263
|
+
}
|
|
264
|
+
if (mentorInstructions && !(existingData === null || existingData === void 0 ? void 0 : existingData.instructions)) {
|
|
265
|
+
updates.instructions = mentorInstructions;
|
|
266
|
+
}
|
|
267
|
+
if (mentorModel && !(existingData === null || existingData === void 0 ? void 0 : existingData.model)) {
|
|
268
|
+
updates.model = mentorModel;
|
|
269
|
+
}
|
|
270
|
+
if (Object.keys(updates).length > 0) {
|
|
271
|
+
handleUpdateNode(nodeId, updates);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
console.error('Failed to prefill mentor node data:', error);
|
|
276
|
+
}
|
|
277
|
+
}, [fetchMentorSettings, handleUpdateNode, org, username]);
|
|
278
|
+
useEffect(() => {
|
|
279
|
+
if (!defaultMentorId || previewMode)
|
|
280
|
+
return;
|
|
281
|
+
const mentorNodesMissingId = nodes.filter((node) => node.type === 'mentor' && !node.data.entry_mentor_id && !node.data.mentor_id);
|
|
282
|
+
if (mentorNodesMissingId.length === 0)
|
|
283
|
+
return;
|
|
284
|
+
setNodes((prev) => prev.map((node) => {
|
|
285
|
+
if (node.type !== 'mentor')
|
|
286
|
+
return node;
|
|
287
|
+
if (node.data.entry_mentor_id || node.data.mentor_id)
|
|
288
|
+
return node;
|
|
289
|
+
return {
|
|
290
|
+
...node,
|
|
291
|
+
data: {
|
|
292
|
+
...node.data,
|
|
293
|
+
entry_mentor_id: defaultMentorId,
|
|
294
|
+
mentor_id: defaultMentorId,
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
}));
|
|
298
|
+
mentorNodesMissingId.forEach((node) => {
|
|
299
|
+
prefillMentorNode(node.id, defaultMentorId, node.data);
|
|
300
|
+
});
|
|
301
|
+
}, [defaultMentorId, previewMode, nodes, prefillMentorNode]);
|
|
302
|
+
const closeMentorModal = useCallback(() => {
|
|
303
|
+
setShowMentorModal(false);
|
|
304
|
+
setMentorSearchQuery('');
|
|
305
|
+
setActiveMentorNodeId(null);
|
|
306
|
+
}, []);
|
|
307
|
+
const handleMentorSelect = useCallback(async (mentor) => {
|
|
308
|
+
var _a;
|
|
309
|
+
if (!activeMentorNodeId)
|
|
310
|
+
return;
|
|
311
|
+
const mentorId = mentor.unique_id;
|
|
312
|
+
if (!mentorId)
|
|
313
|
+
return;
|
|
314
|
+
const activeNode = nodes.find((node) => node.id === activeMentorNodeId);
|
|
315
|
+
const currentData = activeNode === null || activeNode === void 0 ? void 0 : activeNode.data;
|
|
316
|
+
let updates = {
|
|
317
|
+
entry_mentor_id: mentorId,
|
|
318
|
+
mentor_id: mentorId,
|
|
319
|
+
subtitle: (_a = currentData === null || currentData === void 0 ? void 0 : currentData.subtitle) !== null && _a !== void 0 ? _a : 'Agent',
|
|
320
|
+
};
|
|
321
|
+
try {
|
|
322
|
+
if (org && username) {
|
|
323
|
+
const settings = (await fetchMentorSettings({
|
|
324
|
+
mentor: mentorId,
|
|
325
|
+
org,
|
|
326
|
+
}).unwrap());
|
|
327
|
+
updates = {
|
|
328
|
+
...updates,
|
|
329
|
+
label: settings.display_name ||
|
|
330
|
+
settings.mentor_name ||
|
|
331
|
+
mentor.name ||
|
|
332
|
+
(currentData === null || currentData === void 0 ? void 0 : currentData.label) ||
|
|
333
|
+
'Agent',
|
|
334
|
+
instructions: settings.system_prompt || (currentData === null || currentData === void 0 ? void 0 : currentData.instructions),
|
|
335
|
+
model: settings.llm_name || (currentData === null || currentData === void 0 ? void 0 : currentData.model),
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
else if (mentor.name) {
|
|
339
|
+
updates = { ...updates, label: mentor.name };
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
console.error('Failed to load mentor settings for workflow node:', error);
|
|
344
|
+
if (mentor.name) {
|
|
345
|
+
updates = { ...updates, label: mentor.name };
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
handleUpdateNode(activeMentorNodeId, updates);
|
|
349
|
+
closeMentorModal();
|
|
350
|
+
}, [
|
|
351
|
+
activeMentorNodeId,
|
|
352
|
+
closeMentorModal,
|
|
353
|
+
fetchMentorSettings,
|
|
354
|
+
handleUpdateNode,
|
|
355
|
+
nodes,
|
|
356
|
+
org,
|
|
357
|
+
username,
|
|
358
|
+
]);
|
|
359
|
+
const handleMentorChangeClick = useCallback((nodeId) => {
|
|
360
|
+
if (previewMode)
|
|
361
|
+
return;
|
|
362
|
+
setActiveMentorNodeId(nodeId);
|
|
363
|
+
setMentorSearchQuery('');
|
|
364
|
+
setShowMentorModal(true);
|
|
365
|
+
}, [previewMode]);
|
|
366
|
+
const findNonOverlappingPosition = useCallback((baseX, baseY, nodeType, view) => {
|
|
367
|
+
var _a, _b, _c;
|
|
368
|
+
const { width: nodeWidth, height: nodeHeight } = nodeSize({
|
|
369
|
+
type: nodeType,
|
|
370
|
+
});
|
|
371
|
+
const SPACING = 20; // Minimum spacing between nodes
|
|
372
|
+
const hasOverlap = ({ x, y }) => nodes.some((node) => {
|
|
373
|
+
const existing = nodeSize(node);
|
|
374
|
+
return !(x + nodeWidth + SPACING < node.position.x ||
|
|
375
|
+
x > node.position.x + existing.width + SPACING ||
|
|
376
|
+
y + nodeHeight + SPACING < node.position.y ||
|
|
377
|
+
y > node.position.y + existing.height + SPACING);
|
|
378
|
+
});
|
|
379
|
+
// A step placed past the canvas edge hides its connection points.
|
|
380
|
+
const isVisible = ({ x, y }) => !view ||
|
|
381
|
+
(x >= view.left &&
|
|
382
|
+
y >= view.top &&
|
|
383
|
+
x + nodeWidth <= view.right &&
|
|
384
|
+
y + nodeHeight <= view.bottom);
|
|
385
|
+
const stepX = nodeWidth + SPACING * 2;
|
|
386
|
+
const stepY = nodeHeight + SPACING * 2;
|
|
387
|
+
const candidates = [
|
|
388
|
+
{ x: baseX, y: baseY },
|
|
389
|
+
{ x: baseX + stepX, y: baseY }, // Right
|
|
390
|
+
{ x: baseX, y: baseY + stepY }, // Below
|
|
391
|
+
{ x: baseX - stepX, y: baseY }, // Left
|
|
392
|
+
{ x: baseX, y: baseY - stepY }, // Above
|
|
393
|
+
{ x: baseX + stepX, y: baseY + stepY }, // Bottom-right
|
|
394
|
+
{ x: baseX - stepX, y: baseY + stepY }, // Bottom-left
|
|
395
|
+
{ x: baseX + stepX, y: baseY - stepY }, // Top-right
|
|
396
|
+
{ x: baseX - stepX, y: baseY - stepY }, // Top-left
|
|
397
|
+
];
|
|
398
|
+
// The top-left corner wins when the card is bigger than the view.
|
|
399
|
+
const clampToView = ({ x, y }) => view
|
|
400
|
+
? {
|
|
401
|
+
x: Math.max(view.left, Math.min(x, view.right - nodeWidth)),
|
|
402
|
+
y: Math.max(view.top, Math.min(y, view.bottom - nodeHeight)),
|
|
403
|
+
}
|
|
404
|
+
: { x, y };
|
|
405
|
+
return ((_c = (_b = (_a = candidates.find((pos) => !hasOverlap(pos) && isVisible(pos))) !== null && _a !== void 0 ? _a : candidates.map(clampToView).find((pos) => !hasOverlap(pos))) !== null && _b !== void 0 ? _b : candidates.find((pos) => !hasOverlap(pos))) !== null && _c !== void 0 ? _c : {
|
|
406
|
+
x: baseX + (nodeWidth + SPACING) * 2,
|
|
407
|
+
y: baseY + (nodeHeight + SPACING) * 2,
|
|
408
|
+
});
|
|
409
|
+
}, [nodes]);
|
|
410
|
+
const visibleBounds = (rect) => {
|
|
411
|
+
// `|| 0` keeps an unpanned edge at 0 rather than -0.
|
|
412
|
+
const left = -panOffset.x / zoom || 0;
|
|
413
|
+
const top = -panOffset.y / zoom || 0;
|
|
414
|
+
return {
|
|
415
|
+
left,
|
|
416
|
+
top,
|
|
417
|
+
right: left + rect.width / zoom,
|
|
418
|
+
bottom: top + Math.max(0, rect.height - TOOLBAR_CLEARANCE) / zoom,
|
|
419
|
+
};
|
|
420
|
+
};
|
|
421
|
+
useEffect(() => {
|
|
422
|
+
if (previewMode)
|
|
423
|
+
return; // Don't add nodes in preview mode
|
|
424
|
+
if (onClickedItem && canvasRef.current) {
|
|
425
|
+
// Prevent duplicate additions - skip if we already processed this exact click object
|
|
426
|
+
if (processedClickRef.current === onClickedItem) {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
processedClickRef.current = onClickedItem;
|
|
430
|
+
const rect = canvasRef.current.getBoundingClientRect();
|
|
431
|
+
const centerX = (rect.width / 2 - panOffset.x) / zoom;
|
|
432
|
+
const centerY = (rect.height / 2 - panOffset.y) / zoom;
|
|
433
|
+
const size = nodeSize({ type: onClickedItem.id });
|
|
434
|
+
const { x, y } = findNonOverlappingPosition(centerX - size.width / 2, centerY - size.height / 2, onClickedItem.id, visibleBounds(rect));
|
|
435
|
+
const newNode = {
|
|
436
|
+
id: `${onClickedItem.id}-${Date.now()}`,
|
|
437
|
+
type: onClickedItem.id,
|
|
438
|
+
position: { x, y },
|
|
439
|
+
data: {
|
|
440
|
+
label: onClickedItem.label,
|
|
441
|
+
...(onClickedItem.id === 'mentor' && {
|
|
442
|
+
subtitle: 'Agent',
|
|
443
|
+
color: 'chart-1',
|
|
444
|
+
entry_mentor_id: defaultMentorId,
|
|
445
|
+
mentor_id: defaultMentorId,
|
|
446
|
+
}),
|
|
447
|
+
...(onClickedItem.id === 'note' && { content: 'Sticky Note' }),
|
|
448
|
+
},
|
|
449
|
+
...(onClickedItem.id === 'note' && { width: 200, height: 120 }),
|
|
450
|
+
draggable: true,
|
|
451
|
+
selectable: true,
|
|
452
|
+
connectable: true,
|
|
453
|
+
};
|
|
454
|
+
const newNodes = [...nodes, newNode];
|
|
455
|
+
setNodes(newNodes);
|
|
456
|
+
saveToHistory(newNodes, edges);
|
|
457
|
+
if (onClickedItem.id === 'mentor' && defaultMentorId) {
|
|
458
|
+
prefillMentorNode(newNode.id, defaultMentorId, newNode.data);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}, [onClickedItem, defaultMentorId, edges, prefillMentorNode]);
|
|
462
|
+
const handleNodeMouseDown = (e, nodeId) => {
|
|
463
|
+
if (previewMode)
|
|
464
|
+
return; // Prevent node dragging in preview mode
|
|
465
|
+
if (e.button !== 0)
|
|
466
|
+
return;
|
|
467
|
+
e.stopPropagation();
|
|
468
|
+
const node = nodes.find((n) => n.id === nodeId);
|
|
469
|
+
if (!node)
|
|
470
|
+
return;
|
|
471
|
+
setSelectedEdgeId(null);
|
|
472
|
+
if (e.ctrlKey || e.metaKey) {
|
|
473
|
+
setNodes((prev) => prev.map((n) => (n.id === nodeId ? { ...n, selected: !n.selected } : n)));
|
|
474
|
+
if (node.type === 'mentor') {
|
|
475
|
+
setSelectedNodeForConfig(null);
|
|
476
|
+
}
|
|
477
|
+
suppressMentorClickRef.current = true;
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
setNodes((prev) => prev.map((n) => ({ ...n, selected: n.id === nodeId })));
|
|
481
|
+
if (node.type !== 'mentor') {
|
|
482
|
+
setSelectedNodeForConfig(nodeId);
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
setSelectedNodeForConfig(null);
|
|
486
|
+
}
|
|
487
|
+
suppressMentorClickRef.current = false;
|
|
488
|
+
}
|
|
489
|
+
dragStartRef.current = { x: e.clientX, y: e.clientY };
|
|
490
|
+
dragMovedRef.current = false;
|
|
491
|
+
setDraggedNode(nodeId);
|
|
492
|
+
setDragOffset({
|
|
493
|
+
x: e.clientX / zoom - node.position.x,
|
|
494
|
+
y: e.clientY / zoom - node.position.y,
|
|
495
|
+
});
|
|
496
|
+
};
|
|
497
|
+
const handleConnectionStart = (e, nodeId, handle) => {
|
|
498
|
+
if (previewMode)
|
|
499
|
+
return; // Prevent connection creation in preview mode
|
|
500
|
+
e.stopPropagation();
|
|
501
|
+
setConnectingFrom({ nodeId, handle });
|
|
502
|
+
};
|
|
503
|
+
const handleResizeStart = (e, nodeId, corner) => {
|
|
504
|
+
if (previewMode)
|
|
505
|
+
return; // Prevent resizing in preview mode
|
|
506
|
+
e.stopPropagation();
|
|
507
|
+
const node = nodes.find((n) => n.id === nodeId);
|
|
508
|
+
if (!node)
|
|
509
|
+
return;
|
|
510
|
+
setResizingNote({
|
|
511
|
+
nodeId,
|
|
512
|
+
corner,
|
|
513
|
+
startX: e.clientX,
|
|
514
|
+
startY: e.clientY,
|
|
515
|
+
startWidth: node.width || 200,
|
|
516
|
+
startHeight: node.height || 120,
|
|
517
|
+
});
|
|
518
|
+
};
|
|
519
|
+
const handleCanvasMouseDown = (e) => {
|
|
520
|
+
var _a, _b;
|
|
521
|
+
if (e.button !== 0 || draggedNode || connectingFrom)
|
|
522
|
+
return;
|
|
523
|
+
// Dialogs render in portals, so their backdrop's mousedown still bubbles
|
|
524
|
+
// here through the React tree.
|
|
525
|
+
if (!((_a = canvasRef.current) === null || _a === void 0 ? void 0 : _a.contains(e.target)))
|
|
526
|
+
return;
|
|
527
|
+
const target = e.target;
|
|
528
|
+
const isCanvasClick = target === e.currentTarget || target.tagName === 'svg' || target.tagName === 'rect';
|
|
529
|
+
if (isCanvasClick && !previewMode) {
|
|
530
|
+
setSelectedNodeForConfig(null);
|
|
531
|
+
setSelectedEdgeId(null);
|
|
532
|
+
}
|
|
533
|
+
if (tool === 'hand') {
|
|
534
|
+
setIsPanning(true);
|
|
535
|
+
setPanStart({ x: e.clientX - panOffset.x, y: e.clientY - panOffset.y });
|
|
536
|
+
}
|
|
537
|
+
else if (tool === 'pointer' && !previewMode) {
|
|
538
|
+
const rect = (_b = canvasRef.current) === null || _b === void 0 ? void 0 : _b.getBoundingClientRect();
|
|
539
|
+
if (!rect)
|
|
540
|
+
return;
|
|
541
|
+
const x = (e.clientX - rect.left - panOffset.x) / zoom;
|
|
542
|
+
const y = (e.clientY - rect.top - panOffset.y) / zoom;
|
|
543
|
+
setSelectionBox({ startX: x, startY: y, currentX: x, currentY: y });
|
|
544
|
+
setNodes((prev) => prev.map((n) => ({ ...n, selected: false })));
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
const handleMouseMove = useCallback((e) => {
|
|
548
|
+
if (draggedNode) {
|
|
549
|
+
if (dragStartRef.current) {
|
|
550
|
+
const movedX = Math.abs(e.clientX - dragStartRef.current.x);
|
|
551
|
+
const movedY = Math.abs(e.clientY - dragStartRef.current.y);
|
|
552
|
+
if (movedX > 3 || movedY > 3) {
|
|
553
|
+
dragMovedRef.current = true;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
const draggedNodeData = nodes.find((n) => n.id === draggedNode);
|
|
557
|
+
if (!draggedNodeData)
|
|
558
|
+
return;
|
|
559
|
+
const deltaX = e.clientX / zoom - dragOffset.x - draggedNodeData.position.x;
|
|
560
|
+
const deltaY = e.clientY / zoom - dragOffset.y - draggedNodeData.position.y;
|
|
561
|
+
setNodes((prev) => prev.map((node) => {
|
|
562
|
+
if (node.selected) {
|
|
563
|
+
return {
|
|
564
|
+
...node,
|
|
565
|
+
position: {
|
|
566
|
+
x: node.position.x + deltaX,
|
|
567
|
+
y: node.position.y + deltaY,
|
|
568
|
+
},
|
|
569
|
+
dragging: true,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
return node;
|
|
573
|
+
}));
|
|
574
|
+
}
|
|
575
|
+
else if (isPanning) {
|
|
576
|
+
setPanOffset({
|
|
577
|
+
x: e.clientX - panStart.x,
|
|
578
|
+
y: e.clientY - panStart.y,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
else if (connectingFrom && canvasRef.current) {
|
|
582
|
+
const rect = canvasRef.current.getBoundingClientRect();
|
|
583
|
+
setTempConnectionPos({
|
|
584
|
+
x: (e.clientX - rect.left - panOffset.x) / zoom,
|
|
585
|
+
y: (e.clientY - rect.top - panOffset.y) / zoom,
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
else if (resizingNote) {
|
|
589
|
+
const deltaX = (e.clientX - resizingNote.startX) / zoom;
|
|
590
|
+
const deltaY = (e.clientY - resizingNote.startY) / zoom;
|
|
591
|
+
setNodes((prev) => prev.map((node) => {
|
|
592
|
+
if (node.id === resizingNote.nodeId) {
|
|
593
|
+
let newWidth = resizingNote.startWidth;
|
|
594
|
+
let newHeight = resizingNote.startHeight;
|
|
595
|
+
if (resizingNote.corner.includes('right')) {
|
|
596
|
+
newWidth = Math.max(150, resizingNote.startWidth + deltaX);
|
|
597
|
+
}
|
|
598
|
+
if (resizingNote.corner.includes('bottom')) {
|
|
599
|
+
newHeight = Math.max(80, resizingNote.startHeight + deltaY);
|
|
600
|
+
}
|
|
601
|
+
return { ...node, width: newWidth, height: newHeight };
|
|
602
|
+
}
|
|
603
|
+
return node;
|
|
604
|
+
}));
|
|
605
|
+
}
|
|
606
|
+
else if (selectionBox && canvasRef.current) {
|
|
607
|
+
const rect = canvasRef.current.getBoundingClientRect();
|
|
608
|
+
const currentX = (e.clientX - rect.left - panOffset.x) / zoom;
|
|
609
|
+
const currentY = (e.clientY - rect.top - panOffset.y) / zoom;
|
|
610
|
+
setSelectionBox((prev) => (prev ? { ...prev, currentX, currentY } : null));
|
|
611
|
+
const minX = Math.min(selectionBox.startX, currentX);
|
|
612
|
+
const maxX = Math.max(selectionBox.startX, currentX);
|
|
613
|
+
const minY = Math.min(selectionBox.startY, currentY);
|
|
614
|
+
const maxY = Math.max(selectionBox.startY, currentY);
|
|
615
|
+
setNodes((prev) => prev.map((node) => {
|
|
616
|
+
const { width, height } = nodeSize(node);
|
|
617
|
+
const nodeInBox = node.position.x + width > minX &&
|
|
618
|
+
node.position.x < maxX &&
|
|
619
|
+
node.position.y + height > minY &&
|
|
620
|
+
node.position.y < maxY;
|
|
621
|
+
return { ...node, selected: nodeInBox };
|
|
622
|
+
}));
|
|
623
|
+
}
|
|
624
|
+
}, [
|
|
625
|
+
draggedNode,
|
|
626
|
+
dragOffset,
|
|
627
|
+
isPanning,
|
|
628
|
+
panStart,
|
|
629
|
+
connectingFrom,
|
|
630
|
+
panOffset,
|
|
631
|
+
resizingNote,
|
|
632
|
+
selectionBox,
|
|
633
|
+
nodes,
|
|
634
|
+
zoom,
|
|
635
|
+
]);
|
|
636
|
+
const handleMouseUp = useCallback(() => {
|
|
637
|
+
var _a;
|
|
638
|
+
const targetNode = hoveredNode ? nodes.find((n) => n.id === hoveredNode) : undefined;
|
|
639
|
+
if (connectingFrom &&
|
|
640
|
+
targetNode &&
|
|
641
|
+
connectingFrom.nodeId !== targetNode.id &&
|
|
642
|
+
canReceiveConnection(targetNode.type)) {
|
|
643
|
+
const { nodeId, handle } = connectingFrom;
|
|
644
|
+
const fromSameOutput = (edge) => edge.source === nodeId && (edge.sourceHandle || 'right') === handle;
|
|
645
|
+
const existing = edges.find((edge) => fromSameOutput(edge) && edge.target === targetNode.id);
|
|
646
|
+
const connection = existing !== null && existing !== void 0 ? existing : {
|
|
647
|
+
id: edgeIdFor(nodeId, handle, targetNode.id),
|
|
648
|
+
source: nodeId,
|
|
649
|
+
target: targetNode.id,
|
|
650
|
+
sourceHandle: handle,
|
|
651
|
+
targetHandle: 'left',
|
|
652
|
+
};
|
|
653
|
+
// The backend routes each branch output to a single step and keeps
|
|
654
|
+
// only the last connection, so drawing one replaces the others - even
|
|
655
|
+
// when it repeats a connection an older save already had.
|
|
656
|
+
const newEdges = isBranchHandle(handle)
|
|
657
|
+
? [...edges.filter((edge) => !fromSameOutput(edge)), connection]
|
|
658
|
+
: existing
|
|
659
|
+
? edges
|
|
660
|
+
: [...edges, connection];
|
|
661
|
+
if (existing ? newEdges.length !== edges.length : true) {
|
|
662
|
+
setEdges(newEdges);
|
|
663
|
+
saveToHistory(nodes, newEdges);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
if (draggedNode) {
|
|
667
|
+
setNodes((prev) => prev.map((n) => ({ ...n, dragging: false })));
|
|
668
|
+
saveToHistory(nodes, edges);
|
|
669
|
+
if (!previewMode && !dragMovedRef.current && !suppressMentorClickRef.current) {
|
|
670
|
+
const clickedNode = nodes.find((node) => node.id === draggedNode);
|
|
671
|
+
if ((clickedNode === null || clickedNode === void 0 ? void 0 : clickedNode.type) === 'mentor') {
|
|
672
|
+
onOpenAgentSettings === null || onOpenAgentSettings === void 0 ? void 0 : onOpenAgentSettings((_a = clickedNode.data.mentor_id) !== null && _a !== void 0 ? _a : clickedNode.data.entry_mentor_id);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
dragMovedRef.current = false;
|
|
677
|
+
dragStartRef.current = null;
|
|
678
|
+
suppressMentorClickRef.current = false;
|
|
679
|
+
setDraggedNode(null);
|
|
680
|
+
setIsPanning(false);
|
|
681
|
+
setConnectingFrom(null);
|
|
682
|
+
setTempConnectionPos(null);
|
|
683
|
+
setResizingNote(null);
|
|
684
|
+
setSelectionBox(null);
|
|
685
|
+
}, [
|
|
686
|
+
connectingFrom,
|
|
687
|
+
hoveredNode,
|
|
688
|
+
draggedNode,
|
|
689
|
+
nodes,
|
|
690
|
+
edges,
|
|
691
|
+
saveToHistory,
|
|
692
|
+
onOpenAgentSettings,
|
|
693
|
+
previewMode,
|
|
694
|
+
defaultMentorId,
|
|
695
|
+
]);
|
|
696
|
+
useEffect(() => {
|
|
697
|
+
if (draggedNode || isPanning || connectingFrom || resizingNote || selectionBox) {
|
|
698
|
+
window.addEventListener('mousemove', handleMouseMove);
|
|
699
|
+
window.addEventListener('mouseup', handleMouseUp);
|
|
700
|
+
return () => {
|
|
701
|
+
window.removeEventListener('mousemove', handleMouseMove);
|
|
702
|
+
window.removeEventListener('mouseup', handleMouseUp);
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
}, [
|
|
706
|
+
draggedNode,
|
|
707
|
+
isPanning,
|
|
708
|
+
connectingFrom,
|
|
709
|
+
resizingNote,
|
|
710
|
+
selectionBox,
|
|
711
|
+
handleMouseMove,
|
|
712
|
+
handleMouseUp,
|
|
713
|
+
]);
|
|
714
|
+
const handleDrop = (e) => {
|
|
715
|
+
if (previewMode)
|
|
716
|
+
return; // Prevent dropping in preview mode
|
|
717
|
+
e.preventDefault();
|
|
718
|
+
if (!canvasRef.current)
|
|
719
|
+
return;
|
|
720
|
+
const rect = canvasRef.current.getBoundingClientRect();
|
|
721
|
+
const baseX = (e.clientX - rect.left - panOffset.x) / zoom;
|
|
722
|
+
const baseY = (e.clientY - rect.top - panOffset.y) / zoom;
|
|
723
|
+
try {
|
|
724
|
+
const data = JSON.parse(e.dataTransfer.getData('application/json'));
|
|
725
|
+
const { x, y } = findNonOverlappingPosition(baseX, baseY, data.id, visibleBounds(rect));
|
|
726
|
+
const newNode = {
|
|
727
|
+
id: `${data.id}-${Date.now()}`,
|
|
728
|
+
type: data.id,
|
|
729
|
+
position: { x, y },
|
|
730
|
+
data: {
|
|
731
|
+
label: data.label,
|
|
732
|
+
...(data.id === 'mentor' && {
|
|
733
|
+
subtitle: 'Agent',
|
|
734
|
+
color: 'chart-1',
|
|
735
|
+
entry_mentor_id: defaultMentorId,
|
|
736
|
+
mentor_id: defaultMentorId,
|
|
737
|
+
}),
|
|
738
|
+
...(data.id === 'note' && { content: 'Sticky Note' }),
|
|
739
|
+
},
|
|
740
|
+
...(data.id === 'note' && { width: 200, height: 120 }),
|
|
741
|
+
draggable: true,
|
|
742
|
+
selectable: true,
|
|
743
|
+
connectable: true,
|
|
744
|
+
};
|
|
745
|
+
const newNodes = [...nodes, newNode];
|
|
746
|
+
setNodes(newNodes);
|
|
747
|
+
saveToHistory(newNodes, edges);
|
|
748
|
+
if (data.id === 'mentor' && defaultMentorId) {
|
|
749
|
+
prefillMentorNode(newNode.id, defaultMentorId, newNode.data);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
catch (error) {
|
|
753
|
+
console.error('Failed to parse dropped data:', error);
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
const handleDragOver = (e) => {
|
|
757
|
+
e.preventDefault();
|
|
758
|
+
};
|
|
759
|
+
const getHandlePosition = (node, handle) => {
|
|
760
|
+
let nodeWidth = 140;
|
|
761
|
+
let nodeHeight = 50;
|
|
762
|
+
if (node.type === 'start') {
|
|
763
|
+
nodeWidth = 100;
|
|
764
|
+
nodeHeight = 50;
|
|
765
|
+
}
|
|
766
|
+
else if (node.type === 'while' || node.type === 'user-approval') {
|
|
767
|
+
nodeWidth = 280;
|
|
768
|
+
nodeHeight = branchCardHeight(2);
|
|
769
|
+
}
|
|
770
|
+
else if (isConditionalType(node.type)) {
|
|
771
|
+
nodeWidth = 280;
|
|
772
|
+
nodeHeight = branchCardHeight(getConditionCount(node.data) + 1);
|
|
773
|
+
}
|
|
774
|
+
else if (node.type === 'note') {
|
|
775
|
+
nodeWidth = node.width || 200;
|
|
776
|
+
nodeHeight = node.height || 120;
|
|
777
|
+
}
|
|
778
|
+
const HANDLE_OFFSET = 8;
|
|
779
|
+
switch (handle) {
|
|
780
|
+
case 'left':
|
|
781
|
+
return {
|
|
782
|
+
x: node.position.x - HANDLE_OFFSET,
|
|
783
|
+
y: node.position.y + nodeHeight / 2,
|
|
784
|
+
};
|
|
785
|
+
case 'right':
|
|
786
|
+
return {
|
|
787
|
+
x: node.position.x + nodeWidth + HANDLE_OFFSET,
|
|
788
|
+
y: node.position.y + nodeHeight / 2,
|
|
789
|
+
};
|
|
790
|
+
case 'top':
|
|
791
|
+
return {
|
|
792
|
+
x: node.position.x + nodeWidth / 2,
|
|
793
|
+
y: node.position.y - HANDLE_OFFSET,
|
|
794
|
+
};
|
|
795
|
+
case 'bottom':
|
|
796
|
+
return {
|
|
797
|
+
x: node.position.x + nodeWidth / 2,
|
|
798
|
+
y: node.position.y + nodeHeight + HANDLE_OFFSET,
|
|
799
|
+
};
|
|
800
|
+
case 'else':
|
|
801
|
+
return {
|
|
802
|
+
x: node.position.x + nodeWidth + HANDLE_OFFSET,
|
|
803
|
+
y: node.position.y + branchRowCenter(getConditionCount(node.data)),
|
|
804
|
+
};
|
|
805
|
+
case WHILE_LOOP_HANDLE:
|
|
806
|
+
case 'approve':
|
|
807
|
+
return {
|
|
808
|
+
x: node.position.x + nodeWidth + HANDLE_OFFSET,
|
|
809
|
+
y: node.position.y + branchRowCenter(0),
|
|
810
|
+
};
|
|
811
|
+
case WHILE_DONE_HANDLE:
|
|
812
|
+
case 'reject':
|
|
813
|
+
return {
|
|
814
|
+
x: node.position.x + nodeWidth + HANDLE_OFFSET,
|
|
815
|
+
y: node.position.y + branchRowCenter(1),
|
|
816
|
+
};
|
|
817
|
+
default:
|
|
818
|
+
if (handle.startsWith('condition-')) {
|
|
819
|
+
const index = Number.parseInt(handle.split('-')[1]);
|
|
820
|
+
return {
|
|
821
|
+
x: node.position.x + nodeWidth + HANDLE_OFFSET,
|
|
822
|
+
y: node.position.y + branchRowCenter(index),
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
return { x: node.position.x, y: node.position.y };
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
const getConnectionPath = (from, to) => {
|
|
829
|
+
const dx = to.x - from.x;
|
|
830
|
+
const dy = to.y - from.y;
|
|
831
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
832
|
+
const controlPointOffset = Math.min(distance * 0.5, 100);
|
|
833
|
+
return `M ${from.x} ${from.y} C ${from.x + controlPointOffset} ${from.y}, ${to.x - controlPointOffset} ${to.y}, ${to.x} ${to.y}`;
|
|
834
|
+
};
|
|
835
|
+
const deleteNodes = useCallback((nodeIds) => {
|
|
836
|
+
const ids = new Set(nodes
|
|
837
|
+
.filter((node) => nodeIds.includes(node.id) && isDeletableNode(node))
|
|
838
|
+
.map((node) => node.id));
|
|
839
|
+
if (ids.size === 0)
|
|
840
|
+
return;
|
|
841
|
+
const newNodes = nodes.filter((node) => !ids.has(node.id));
|
|
842
|
+
const newEdges = edges.filter((edge) => !ids.has(edge.source) && !ids.has(edge.target));
|
|
843
|
+
setNodes(newNodes);
|
|
844
|
+
setEdges(newEdges);
|
|
845
|
+
setSelectedNodeForConfig((current) => (current && ids.has(current) ? null : current));
|
|
846
|
+
const keepsEdge = (current) => current && newEdges.some((edge) => edge.id === current) ? current : null;
|
|
847
|
+
setSelectedEdgeId(keepsEdge);
|
|
848
|
+
setHoveredEdgeId(keepsEdge);
|
|
849
|
+
saveToHistory(newNodes, newEdges);
|
|
850
|
+
}, [nodes, edges, saveToHistory]);
|
|
851
|
+
const deleteEdge = useCallback((edgeId) => {
|
|
852
|
+
const newEdges = edges.filter((edge) => edge.id !== edgeId);
|
|
853
|
+
if (newEdges.length === edges.length)
|
|
854
|
+
return;
|
|
855
|
+
setEdges(newEdges);
|
|
856
|
+
setSelectedEdgeId(null);
|
|
857
|
+
setHoveredEdgeId(null);
|
|
858
|
+
saveToHistory(nodes, newEdges);
|
|
859
|
+
}, [nodes, edges, saveToHistory]);
|
|
860
|
+
const selectEdge = (edgeId) => {
|
|
861
|
+
setSelectedEdgeId(edgeId);
|
|
862
|
+
setSelectedNodeForConfig(null);
|
|
863
|
+
setNodes((prev) => prev.map((n) => ({ ...n, selected: false })));
|
|
864
|
+
};
|
|
865
|
+
const handleDeleteSelection = useCallback(() => {
|
|
866
|
+
if (selectedEdgeId && edges.some((edge) => edge.id === selectedEdgeId)) {
|
|
867
|
+
deleteEdge(selectedEdgeId);
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
deleteNodes(nodes.filter((node) => node.selected).map((node) => node.id));
|
|
871
|
+
}, [selectedEdgeId, edges, deleteEdge, deleteNodes, nodes]);
|
|
872
|
+
useEffect(() => {
|
|
873
|
+
if (previewMode)
|
|
874
|
+
return;
|
|
875
|
+
const handleKeyDown = (e) => {
|
|
876
|
+
var _a, _b;
|
|
877
|
+
if (e.key !== 'Delete' && e.key !== 'Backspace')
|
|
878
|
+
return;
|
|
879
|
+
const target = e.target;
|
|
880
|
+
if (target.tagName === 'INPUT' ||
|
|
881
|
+
target.tagName === 'TEXTAREA' ||
|
|
882
|
+
target.isContentEditable ||
|
|
883
|
+
((_a = target.closest) === null || _a === void 0 ? void 0 : _a.call(target, NODE_PANEL_SELECTOR)) ||
|
|
884
|
+
!canvasHasPointerRef.current) {
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
// Dialogs and menus (Edit Agent, Add State Variable, selects) render in
|
|
888
|
+
// portals; a key pressed there must not delete the step behind them.
|
|
889
|
+
const active = document.activeElement;
|
|
890
|
+
if (active && active !== document.body && !((_b = rootRef.current) === null || _b === void 0 ? void 0 : _b.contains(active))) {
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
e.preventDefault();
|
|
894
|
+
handleDeleteSelection();
|
|
895
|
+
};
|
|
896
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
897
|
+
return () => {
|
|
898
|
+
window.removeEventListener('keydown', handleKeyDown);
|
|
899
|
+
};
|
|
900
|
+
}, [previewMode, handleDeleteSelection]);
|
|
901
|
+
useEffect(() => {
|
|
902
|
+
if (onStateChange && !previewMode) {
|
|
903
|
+
// Strip UI-only properties for comparison to avoid triggering on selection changes
|
|
904
|
+
const strippedNodes = stripUiProperties(nodes);
|
|
905
|
+
const strippedEdges = stripEdgeUiProperties(edges);
|
|
906
|
+
const stateKey = JSON.stringify({
|
|
907
|
+
nodes: strippedNodes,
|
|
908
|
+
edges: strippedEdges,
|
|
909
|
+
});
|
|
910
|
+
// Only trigger onStateChange if actual data changed (not just selection/dragging)
|
|
911
|
+
if (stateKey !== lastEmittedStateRef.current) {
|
|
912
|
+
lastEmittedStateRef.current = stateKey;
|
|
913
|
+
const reactFlowData = {
|
|
914
|
+
nodes: strippedNodes,
|
|
915
|
+
edges: strippedEdges,
|
|
916
|
+
viewport: { x: panOffset.x, y: panOffset.y, zoom },
|
|
917
|
+
};
|
|
918
|
+
onStateChange(reactFlowData);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}, [nodes, edges, zoom, panOffset, previewMode, onStateChange]);
|
|
922
|
+
const workflowVariables = useMemo(() => collectWorkflowVariables(nodes), [nodes]);
|
|
923
|
+
const variableUsages = useMemo(() => collectVariableUsages(nodes), [nodes]);
|
|
924
|
+
const edgeGeometry = edges.flatMap((edge) => {
|
|
925
|
+
const fromNode = nodes.find((n) => n.id === edge.source);
|
|
926
|
+
const toNode = nodes.find((n) => n.id === edge.target);
|
|
927
|
+
if (!fromNode || !toNode)
|
|
928
|
+
return [];
|
|
929
|
+
const from = getHandlePosition(fromNode, edge.sourceHandle || 'right');
|
|
930
|
+
const to = getHandlePosition(toNode, edge.targetHandle || 'left');
|
|
931
|
+
// getConnectionPath's control points mirror each other, so the curve
|
|
932
|
+
// passes through the plain midpoint of its ends.
|
|
933
|
+
const midpoint = { x: (from.x + to.x) / 2, y: (from.y + to.y) / 2 };
|
|
934
|
+
return [{ edge, path: getConnectionPath(from, to), midpoint }];
|
|
935
|
+
});
|
|
936
|
+
const activeMentorNode = activeMentorNodeId
|
|
937
|
+
? nodes.find((node) => node.id === activeMentorNodeId)
|
|
938
|
+
: undefined;
|
|
939
|
+
const activeMentorId = (_a = activeMentorNode === null || activeMentorNode === void 0 ? void 0 : activeMentorNode.data.mentor_id) !== null && _a !== void 0 ? _a : activeMentorNode === null || activeMentorNode === void 0 ? void 0 : activeMentorNode.data.entry_mentor_id;
|
|
940
|
+
const selectedMentorIds = activeMentorId ? [activeMentorId] : [];
|
|
941
|
+
return (jsxs("div", { ref: rootRef, onMouseDownCapture: (e) => {
|
|
942
|
+
var _a;
|
|
943
|
+
const target = e.target;
|
|
944
|
+
canvasHasPointerRef.current =
|
|
945
|
+
!!((_a = rootRef.current) === null || _a === void 0 ? void 0 : _a.contains(target)) && !target.closest(NODE_PANEL_SELECTOR);
|
|
946
|
+
}, "data-testid": "workflow-canvas", className: "bg-background relative h-full w-full overflow-hidden select-none", children: [jsxs("div", { ref: canvasRef, className: `absolute inset-0 ${tool === 'hand' ? (isPanning ? 'cursor-grabbing' : 'cursor-grab') : 'cursor-default'}`, onMouseDown: handleCanvasMouseDown, onDrop: handleDrop, onDragOver: handleDragOver, children: [jsxs("svg", { className: "pointer-events-none h-full w-full", children: [jsx("defs", { children: jsx("pattern", { id: "grid", width: "20", height: "20", patternUnits: "userSpaceOnUse", children: jsx("circle", { cx: "1", cy: "1", r: "0.5", fill: "hsl(var(--muted-foreground) / 0.2)" }) }) }), jsx("rect", { width: "100%", height: "100%", fill: "url(#grid)" }), jsxs("g", { transform: `translate(${panOffset.x}, ${panOffset.y}) scale(${zoom})`, children: [edgeGeometry.map(({ edge, path }) => {
|
|
947
|
+
const isActive = !previewMode && (edge.id === selectedEdgeId || edge.id === hoveredEdgeId);
|
|
948
|
+
return (jsxs("g", { children: [jsx("path", { d: path, stroke: isActive ? '#1D7FC0' : '#38A1E5', strokeWidth: isActive ? 3 : 2, fill: "none", strokeLinecap: "round" }), !previewMode && (jsx("path", { "data-testid": `workflow-edge-${edge.id}`, d: path, stroke: "transparent", strokeWidth: 16, fill: "none", className: "cursor-pointer", style: { pointerEvents: 'stroke' }, onMouseDown: (e) => {
|
|
949
|
+
if (e.button !== 0)
|
|
950
|
+
return;
|
|
951
|
+
e.stopPropagation();
|
|
952
|
+
selectEdge(edge.id);
|
|
953
|
+
}, onMouseEnter: () => setHoveredEdgeId(edge.id), onMouseLeave: () => setHoveredEdgeId(null) }))] }, edge.id));
|
|
954
|
+
}), connectingFrom &&
|
|
955
|
+
tempConnectionPos &&
|
|
956
|
+
(() => {
|
|
957
|
+
const fromNode = nodes.find((n) => n.id === connectingFrom.nodeId);
|
|
958
|
+
if (!fromNode)
|
|
959
|
+
return null;
|
|
960
|
+
const fromPos = getHandlePosition(fromNode, connectingFrom.handle);
|
|
961
|
+
return (jsx("path", { d: getConnectionPath(fromPos, tempConnectionPos), stroke: "#38A1E5", strokeWidth: "2", strokeDasharray: "5,5", fill: "none", strokeLinecap: "round" }));
|
|
962
|
+
})(), selectionBox && (jsx("rect", { x: Math.min(selectionBox.startX, selectionBox.currentX), y: Math.min(selectionBox.startY, selectionBox.currentY), width: Math.abs(selectionBox.currentX - selectionBox.startX), height: Math.abs(selectionBox.currentY - selectionBox.startY), fill: "rgba(56, 161, 229, 0.1)", stroke: "#38A1E5", strokeWidth: "1", strokeDasharray: "4,4" }))] })] }), jsxs("div", { className: "pointer-events-none absolute inset-0", style: {
|
|
963
|
+
transform: `translate(${panOffset.x}px, ${panOffset.y}px) scale(${zoom})`,
|
|
964
|
+
transformOrigin: '0 0',
|
|
965
|
+
}, children: [!previewMode &&
|
|
966
|
+
edgeGeometry.map(({ edge, midpoint }) => edge.id === selectedEdgeId || edge.id === hoveredEdgeId ? (jsx(ActionTooltip, { label: t('workflowsWorkflowCanvas.deleteConnection'), children: jsx("button", { type: "button", "aria-label": t('workflowsWorkflowCanvas.deleteConnection'), "data-testid": `workflow-edge-delete-${edge.id}`, className: "bg-card border-border text-muted-foreground hover:text-destructive pointer-events-auto absolute z-10 flex h-6 w-6 -translate-x-1/2 -translate-y-1/2 cursor-pointer items-center justify-center rounded-full border shadow-sm", style: { left: `${midpoint.x}px`, top: `${midpoint.y}px` }, onMouseDown: (e) => e.stopPropagation(), onMouseEnter: () => setHoveredEdgeId(edge.id), onMouseLeave: () => setHoveredEdgeId(null), onClick: (e) => {
|
|
967
|
+
e.stopPropagation();
|
|
968
|
+
deleteEdge(edge.id);
|
|
969
|
+
}, children: jsx(X, { className: "h-3.5 w-3.5" }) }) }, edge.id)) : null), nodes.map((node) => (jsxs("div", { "data-testid": `workflow-node-${node.id}`, "data-node-type": node.type, className: `group pointer-events-auto absolute ${node.selected ? 'ring-offset-background rounded-xl ring-2 ring-[#38A1E5] ring-offset-2' : ''}`, style: {
|
|
970
|
+
left: `${node.position.x}px`,
|
|
971
|
+
top: `${node.position.y}px`,
|
|
972
|
+
}, onMouseDown: (e) => {
|
|
973
|
+
if (node.type !== 'note' || !editingNote) {
|
|
974
|
+
handleNodeMouseDown(e, node.id);
|
|
975
|
+
}
|
|
976
|
+
}, onMouseEnter: () => setHoveredNode(node.id), onMouseLeave: () => setHoveredNode(null), children: [!previewMode && isDeletableNode(node) && (jsx(ActionTooltip, { label: t('workflowsWorkflowCanvas.deleteNode', { label: node.data.label }), children: jsx("button", { type: "button", "aria-label": t('workflowsWorkflowCanvas.deleteNode', { label: node.data.label }), className: `bg-card border-border text-muted-foreground hover:text-destructive absolute -top-3 -right-3 z-10 flex h-6 w-6 cursor-pointer items-center justify-center rounded-full border shadow-sm transition-opacity group-hover:opacity-100 focus-visible:opacity-100 ${node.selected ? 'opacity-100' : 'opacity-0'}`, onMouseDown: (e) => e.stopPropagation(), onClick: (e) => {
|
|
977
|
+
e.stopPropagation();
|
|
978
|
+
deleteNodes([node.id]);
|
|
979
|
+
}, children: jsx(Trash2, { className: "h-3.5 w-3.5" }) }) })), node.type === 'start' ? (jsxs("div", { className: "bg-card border-border relative flex cursor-grab items-center gap-3 rounded-xl border px-4 py-3 transition-colors hover:border-[#38A1E5] active:cursor-grabbing", children: [jsx("div", { className: "flex h-6 w-6 items-center justify-center rounded", style: { backgroundColor: 'rgba(56, 161, 229, 0.2)' }, children: jsx(Play, { className: "h-4 w-4", style: { color: '#38A1E5' } }) }), jsx("span", { className: "text-foreground font-medium", children: node.data.label }), jsx("div", { className: "border-background absolute top-1/2 -right-2 h-3 w-3 -translate-y-1/2 cursor-crosshair rounded-full border-2 opacity-0 transition-opacity group-hover:opacity-100", style: { backgroundColor: '#38A1E5' }, onMouseDown: (e) => handleConnectionStart(e, node.id, 'right') })] })) : node.type === 'while' ? (jsxs("div", { className: "border-border bg-card relative cursor-grab rounded-2xl border transition-colors hover:border-[#38A1E5] active:cursor-grabbing", style: { width: '280px' }, children: [jsxs("div", { className: "flex items-center gap-3 p-4 pb-3", children: [jsx("div", { className: "flex h-10 w-10 items-center justify-center rounded-lg", style: { backgroundColor: 'rgba(56, 161, 229, 0.2)' }, children: jsx(RefreshCw, { className: "h-5 w-5", style: { color: '#38A1E5' } }) }), jsx("span", { className: "text-foreground font-medium", children: node.data.label })] }), jsx("div", { className: "space-y-2 px-4 pb-4", children: [
|
|
980
|
+
{ handle: WHILE_LOOP_HANDLE, label: t('workflowsWorkflowCanvas.whileLoop') },
|
|
981
|
+
{ handle: WHILE_DONE_HANDLE, label: t('workflowsWorkflowCanvas.whileDone') },
|
|
982
|
+
].map(({ handle, label }) => (jsxs("div", { className: "bg-muted/50 relative flex items-center justify-between rounded-lg px-4 py-3", children: [jsx("span", { className: "text-muted-foreground text-sm", children: label }), jsx("div", { "data-testid": `workflow-handle-${node.id}-${handle}`, className: "border-background absolute top-1/2 -right-[18px] h-3 w-3 -translate-y-1/2 cursor-crosshair rounded-full border-2 opacity-0 transition-opacity group-hover:opacity-100", style: { backgroundColor: '#38A1E5' }, onMouseDown: (e) => handleConnectionStart(e, node.id, handle) })] }, handle))) }), jsx("div", { className: "border-background absolute top-1/2 -left-2 h-3 w-3 -translate-y-1/2 rounded-full border-2 opacity-0 transition-opacity group-hover:opacity-100", style: { backgroundColor: '#38A1E5' } })] })) : isConditionalType(node.type) ? ((() => {
|
|
983
|
+
const conditionCount = getConditionCount(node.data);
|
|
984
|
+
const nodeHeight = branchCardHeight(conditionCount + 1);
|
|
985
|
+
return (jsxs("div", { className: "border-border bg-card relative cursor-grab rounded-2xl border transition-colors hover:border-[#38A1E5] active:cursor-grabbing", style: { width: '280px', minHeight: `${nodeHeight}px` }, children: [jsxs("div", { className: "flex items-center gap-3 p-4 pb-3", children: [jsx("div", { className: "flex h-10 w-10 items-center justify-center rounded-lg", style: { backgroundColor: 'rgba(56, 161, 229, 0.2)' }, children: jsx(GitBranch, { className: "h-5 w-5", style: { color: '#38A1E5' } }) }), jsx("span", { className: "text-foreground font-medium", children: node.data.label })] }), jsxs("div", { className: "space-y-2 px-4 pb-4", children: [Array.from({ length: conditionCount }).map((_, index) => (jsxs("div", { className: "bg-muted/50 relative flex items-center justify-between rounded-lg px-4 py-3", children: [jsx("span", { className: "text-muted-foreground text-sm", children: index === 0
|
|
986
|
+
? t('workflowsWorkflowCanvas.ifCondition')
|
|
987
|
+
: t('workflowsWorkflowCanvas.elseIf', { index }) }), jsx("div", { "data-testid": `workflow-handle-${node.id}-condition-${index}`, className: "border-background absolute top-1/2 -right-[18px] h-3 w-3 -translate-y-1/2 cursor-crosshair rounded-full border-2 opacity-0 transition-opacity group-hover:opacity-100", style: { backgroundColor: '#38A1E5' }, onMouseDown: (e) => handleConnectionStart(e, node.id, `condition-${index}`) })] }, index))), jsxs("div", { className: "bg-muted/50 relative flex items-center justify-between rounded-lg px-4 py-3", children: [jsx("span", { className: "text-muted-foreground text-sm", children: t('workflowsWorkflowCanvas.elseCondition') }), jsx("div", { "data-testid": `workflow-handle-${node.id}-else`, className: "border-background absolute top-1/2 -right-[18px] h-3 w-3 -translate-y-1/2 cursor-crosshair rounded-full border-2 opacity-0 transition-opacity group-hover:opacity-100", style: { backgroundColor: '#38A1E5' }, onMouseDown: (e) => handleConnectionStart(e, node.id, 'else') })] })] }), jsx("div", { className: "border-background absolute top-1/2 -left-2 h-3 w-3 -translate-y-1/2 rounded-full border-2 opacity-0 transition-opacity group-hover:opacity-100", style: { backgroundColor: '#38A1E5' } })] }));
|
|
988
|
+
})()) : node.type === 'user-approval' ? (jsxs("div", { className: "border-border bg-card relative cursor-grab rounded-2xl border transition-colors hover:border-[#38A1E5] active:cursor-grabbing", style: { width: '280px' }, children: [jsxs("div", { className: "flex items-center gap-3 p-4 pb-3", children: [jsx("div", { className: "flex h-10 w-10 items-center justify-center rounded-lg", style: { backgroundColor: 'rgba(56, 161, 229, 0.2)' }, children: jsx(ThumbsUp, { className: "h-5 w-5", style: { color: '#38A1E5' } }) }), jsx("span", { className: "text-foreground font-medium", children: node.data.label })] }), jsxs("div", { className: "space-y-2 px-4 pb-4", children: [jsxs("div", { className: "bg-muted/50 relative flex items-center justify-between rounded-lg px-4 py-3", children: [jsx("span", { className: "text-muted-foreground text-sm", children: t('workflowsWorkflowCanvas.approve') }), jsx("div", { className: "border-background absolute top-1/2 -right-[18px] h-3 w-3 -translate-y-1/2 cursor-crosshair rounded-full border-2 opacity-0 transition-opacity group-hover:opacity-100", style: { backgroundColor: '#38A1E5' }, onMouseDown: (e) => handleConnectionStart(e, node.id, 'approve') })] }), jsxs("div", { className: "bg-muted/50 relative flex items-center justify-between rounded-lg px-4 py-3", children: [jsx("span", { className: "text-muted-foreground text-sm", children: t('workflowsWorkflowCanvas.reject') }), jsx("div", { className: "border-background absolute top-1/2 -right-[18px] h-3 w-3 -translate-y-1/2 cursor-crosshair rounded-full border-2 opacity-0 transition-opacity group-hover:opacity-100", style: { backgroundColor: '#38A1E5' }, onMouseDown: (e) => handleConnectionStart(e, node.id, 'reject') })] })] }), jsx("div", { className: "border-background absolute top-1/2 -left-2 h-3 w-3 -translate-y-1/2 rounded-full border-2 opacity-0 transition-opacity group-hover:opacity-100", style: { backgroundColor: '#38A1E5' } })] })) : node.type === 'transform' ||
|
|
989
|
+
node.type === 'set-state' ||
|
|
990
|
+
node.type === 'guardrails' ||
|
|
991
|
+
node.type === 'file-search' ||
|
|
992
|
+
node.type === 'mcp' ? (jsxs("div", { className: "bg-card border-border relative min-w-[140px] cursor-grab rounded-xl border px-4 py-3 transition-colors hover:border-[#38A1E5] active:cursor-grabbing", children: [jsxs("div", { className: "flex items-center gap-2", children: [jsx("div", { className: "flex h-5 w-5 items-center justify-center rounded", style: { backgroundColor: 'rgba(56, 161, 229, 0.2)' }, children: jsx(Bot, { className: "h-3 w-3", style: { color: '#38A1E5' } }) }), jsx("span", { className: "text-foreground text-sm font-medium", children: node.data.label })] }), jsx("div", { className: "border-background absolute top-1/2 -left-2 h-3 w-3 -translate-y-1/2 rounded-full border-2 opacity-0 transition-opacity group-hover:opacity-100", style: { backgroundColor: '#38A1E5' } }), jsx("div", { className: "border-background absolute top-1/2 -right-2 h-3 w-3 -translate-y-1/2 cursor-crosshair rounded-full border-2 opacity-0 transition-opacity group-hover:opacity-100", style: { backgroundColor: '#38A1E5' }, onMouseDown: (e) => handleConnectionStart(e, node.id, 'right') }), jsx("div", { className: "border-background absolute -top-2 left-1/2 h-3 w-3 -translate-x-1/2 cursor-crosshair rounded-full border-2 opacity-0 transition-opacity group-hover:opacity-100", style: { backgroundColor: '#38A1E5' }, onMouseDown: (e) => handleConnectionStart(e, node.id, 'top') }), jsx("div", { className: "border-background absolute -bottom-2 left-1/2 h-3 w-3 -translate-x-1/2 cursor-crosshair rounded-full border-2 opacity-0 transition-opacity group-hover:opacity-100", style: { backgroundColor: '#38A1E5' }, onMouseDown: (e) => handleConnectionStart(e, node.id, 'bottom') })] })) : node.type === 'note' ? (jsxs("div", { className: "border-border relative cursor-grab rounded-lg border bg-amber-50 transition-colors hover:border-[#38A1E5] active:cursor-grabbing dark:bg-amber-950/20", style: {
|
|
993
|
+
width: `${node.width || 200}px`,
|
|
994
|
+
height: `${node.height || 120}px`,
|
|
995
|
+
}, onClick: (e) => {
|
|
996
|
+
e.stopPropagation();
|
|
997
|
+
setEditingNote(node.id);
|
|
998
|
+
}, children: [editingNote === node.id ? (jsx(Textarea, { autoFocus: true, className: "h-full w-full resize-none border-0 bg-transparent text-sm focus-visible:ring-0", value: node.data.content || '', onChange: (e) => {
|
|
999
|
+
setNodes((prev) => prev.map((n) => n.id === node.id
|
|
1000
|
+
? {
|
|
1001
|
+
...n,
|
|
1002
|
+
data: { ...n.data, content: e.target.value },
|
|
1003
|
+
}
|
|
1004
|
+
: n));
|
|
1005
|
+
}, onBlur: () => setEditingNote(null) })) : (jsx("div", { className: "text-foreground p-3 text-sm whitespace-pre-wrap", children: node.data.content || t('workflowsWorkflowCanvas.stickyNote') })), jsx("div", { className: "border-background absolute -right-1 -bottom-1 h-4 w-4 cursor-nwse-resize rounded-full border-2 opacity-0 transition-opacity group-hover:opacity-100", style: { backgroundColor: '#38A1E5' }, onMouseDown: (e) => handleResizeStart(e, node.id, 'bottom-right') })] })) : (jsxs("div", { className: "bg-card border-border relative min-w-[140px] cursor-grab rounded-xl border px-4 py-3 transition-colors hover:border-[#38A1E5] active:cursor-grabbing", children: [jsxs("div", { className: "mb-1 flex items-center gap-2", children: [jsx("div", { className: "flex h-5 w-5 items-center justify-center rounded", style: { backgroundColor: 'rgba(56, 161, 229, 0.2)' }, children: jsx(Bot, { className: "h-3 w-3", style: { color: '#38A1E5' } }) }), jsx("span", { className: "text-foreground text-sm font-medium", children: node.data.label }), node.type === 'mentor' && !previewMode && (jsx(ActionTooltip, { label: t('workflowsWorkflowCanvas.changeAgent'), children: jsx("button", { type: "button", "aria-label": t('workflowsWorkflowCanvas.changeAgent'), className: "text-muted-foreground hover:text-foreground ml-auto cursor-pointer p-1 transition-colors", onMouseDown: (e) => e.stopPropagation(), onClick: (e) => {
|
|
1006
|
+
e.stopPropagation();
|
|
1007
|
+
handleMentorChangeClick(node.id);
|
|
1008
|
+
}, children: jsx(Pencil, { className: "h-3.5 w-3.5" }) }) }))] }), node.data.subtitle && (jsx("p", { className: "text-muted-foreground ml-7 text-xs", children: node.data.subtitle })), jsx("div", { className: "border-background absolute top-1/2 -left-2 h-3 w-3 -translate-y-1/2 rounded-full border-2 opacity-0 transition-opacity group-hover:opacity-100", style: { backgroundColor: '#38A1E5' } }), jsx("div", { className: "border-background absolute top-1/2 -right-2 h-3 w-3 -translate-y-1/2 cursor-crosshair rounded-full border-2 opacity-0 transition-opacity group-hover:opacity-100", style: { backgroundColor: '#38A1E5' }, onMouseDown: (e) => handleConnectionStart(e, node.id, 'right') }), jsx("div", { className: "border-background absolute -top-2 left-1/2 h-3 w-3 -translate-x-1/2 cursor-crosshair rounded-full border-2 opacity-0 transition-opacity group-hover:opacity-100", style: { backgroundColor: '#38A1E5' }, onMouseDown: (e) => handleConnectionStart(e, node.id, 'top') }), jsx("div", { className: "border-background absolute -bottom-2 left-1/2 h-3 w-3 -translate-x-1/2 cursor-crosshair rounded-full border-2 opacity-0 transition-opacity group-hover:opacity-100", style: { backgroundColor: '#38A1E5' }, onMouseDown: (e) => handleConnectionStart(e, node.id, 'bottom') })] }))] }, node.id)))] }), !previewMode &&
|
|
1009
|
+
selectedNodeForConfig &&
|
|
1010
|
+
(() => {
|
|
1011
|
+
const node = nodes.find((n) => n.id === selectedNodeForConfig);
|
|
1012
|
+
return node && node.type !== 'mentor' ? (jsx(NodeConfigPanel, { nodeId: node.id, nodeType: node.type || '', nodeData: node.data, onClose: () => setSelectedNodeForConfig(null), onUpdateNode: handleUpdateNode, onDeleteNode: isDeletableNode(node) ? () => deleteNodes([node.id]) : undefined, workflowVariables: workflowVariables, variableUsages: variableUsages, stepReferences: nodes
|
|
1013
|
+
.filter((other) => { var _a; return other.id !== node.id && STEPS_WITH_RESULTS.has((_a = other.type) !== null && _a !== void 0 ? _a : ''); })
|
|
1014
|
+
.map((other) => ({ id: other.id, label: other.data.label })), org: org, defaultMentorId: defaultMentorId, username: username, agentSettings: agentSettings }, node.id)) : null;
|
|
1015
|
+
})(), !previewMode && (jsx(Dialog, { open: showMentorModal, onOpenChange: (open) => {
|
|
1016
|
+
if (open) {
|
|
1017
|
+
setShowMentorModal(true);
|
|
1018
|
+
}
|
|
1019
|
+
else {
|
|
1020
|
+
closeMentorModal();
|
|
1021
|
+
}
|
|
1022
|
+
}, children: jsxs(DialogContent, { className: "w-[calc(100%-2rem)] max-w-3xl rounded-lg", children: [jsx(DialogHeader, { children: jsx(DialogTitle, { children: t('workflowsWorkflowCanvas.changeAgent') }) }), jsx(MentorSelectionGrid, { tenantKey: org !== null && org !== void 0 ? org : '', selectedMentorIds: selectedMentorIds, onMentorSelect: handleMentorSelect, searchQuery: mentorSearchQuery, onSearchChange: setMentorSearchQuery, minHeight: "360px" })] }) }))] }), !previewMode && (jsx("div", { className: "pointer-events-auto absolute bottom-6 left-1/2 -translate-x-1/2 transform", children: jsxs("div", { className: "bg-card border-border flex items-center gap-2 rounded-full border px-3 py-2 shadow-lg", children: [jsx(ActionTooltip, { label: t('workflowsWorkflowCanvas.panTool'), children: jsx(Button, { variant: tool === 'hand' ? 'default' : 'ghost', "aria-label": t('workflowsWorkflowCanvas.panTool'), size: "icon", className: `h-8 w-8 ${tool === 'hand' ? 'bg-[#38A1E5] text-white hover:bg-[#38A1E5]' : ''}`, onClick: () => setTool('hand'), children: jsx(Hand, { className: "h-4 w-4" }) }) }), jsx(ActionTooltip, { label: t('workflowsWorkflowCanvas.selectTool'), children: jsx(Button, { variant: tool === 'pointer' ? 'default' : 'ghost', "aria-label": t('workflowsWorkflowCanvas.selectTool'), size: "icon", className: `h-8 w-8 ${tool === 'pointer' ? 'bg-[#38A1E5] text-white hover:bg-[#38A1E5]' : ''}`, onClick: () => setTool('pointer'), children: jsx(MousePointer, { className: "h-4 w-4" }) }) }), jsx("div", { className: "bg-border mx-1 h-6 w-px" }), jsx(ActionTooltip, { label: t('workflowsWorkflowCanvas.undo'), children: jsx(Button, { variant: "ghost", size: "icon", className: "text-muted-foreground hover:text-foreground h-8 w-8 disabled:opacity-30", onClick: handleUndo, "aria-label": t('workflowsWorkflowCanvas.undo'), disabled: historyIndex <= 0, children: jsx(Undo, { className: "h-4 w-4" }) }) }), jsx(ActionTooltip, { label: t('workflowsWorkflowCanvas.redo'), children: jsx(Button, { variant: "ghost", size: "icon", className: "text-muted-foreground hover:text-foreground h-8 w-8 disabled:opacity-30", onClick: handleRedo, "aria-label": t('workflowsWorkflowCanvas.redo'), disabled: historyIndex >= history.length - 1, children: jsx(Redo, { className: "h-4 w-4" }) }) }), jsx("div", { className: "bg-border mx-1 h-6 w-px" }), jsx(ActionTooltip, { label: t('workflowsWorkflowCanvas.zoomOut'), children: jsx(Button, { variant: "ghost", size: "icon", className: "text-muted-foreground hover:text-foreground h-8 w-8", onClick: handleZoomOut, "aria-label": t('workflowsWorkflowCanvas.zoomOut'), children: jsx(ZoomOut, { className: "h-4 w-4" }) }) }), jsxs("span", { className: "text-muted-foreground min-w-[3ch] text-center text-xs", children: [Math.round(zoom * 100), "%"] }), jsx(ActionTooltip, { label: t('workflowsWorkflowCanvas.zoomIn'), children: jsx(Button, { variant: "ghost", size: "icon", className: "text-muted-foreground hover:text-foreground h-8 w-8", onClick: handleZoomIn, "aria-label": t('workflowsWorkflowCanvas.zoomIn'), children: jsx(ZoomIn, { className: "h-4 w-4" }) }) })] }) }))] }));
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
export { WorkflowCanvas };
|
|
1026
|
+
//# sourceMappingURL=workflow-canvas.js.map
|