@flowuent-org/diagramming-core 1.2.1 → 1.2.2
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/apps/diagramming/src/AutomationDiagramData.ts +65 -1
- package/package.json +1 -1
- package/packages/diagrams/src/lib/components/CanvasSearchBar.tsx +281 -0
- package/packages/diagrams/src/lib/components/automation/AutomationApiNode.tsx +796 -788
- package/packages/diagrams/src/lib/components/automation/AutomationEndNode.tsx +608 -600
- package/packages/diagrams/src/lib/components/automation/AutomationFormattingNode.tsx +833 -825
- package/packages/diagrams/src/lib/components/automation/AutomationNavigationNode.tsx +584 -0
- package/packages/diagrams/src/lib/components/automation/AutomationNoteNode.tsx +422 -414
- package/packages/diagrams/src/lib/components/automation/AutomationSheetsNode.tsx +1120 -1112
- package/packages/diagrams/src/lib/components/automation/AutomationStartNode.tsx +511 -503
- package/packages/diagrams/src/lib/components/automation/NodeAIAssistantPopup.tsx +504 -0
- package/packages/diagrams/src/lib/components/automation/NodeActionButtons.tsx +146 -145
- package/packages/diagrams/src/lib/components/automation/index.ts +21 -12
- package/packages/diagrams/src/lib/contexts/SearchContext.tsx +78 -0
- package/packages/diagrams/src/lib/templates/DiagramContainer.tsx +79 -76
- package/packages/diagrams/src/lib/templates/DiagramContent.tsx +276 -61
- package/packages/diagrams/src/lib/types/automation-node-data-types.ts +22 -2
- package/packages/diagrams/src/lib/types/node-types.ts +2 -0
- package/packages/diagrams/src/lib/utils/highlightText.tsx +93 -0
- package/packages/diagrams/src/lib/utils/nodeAIAssistantConfig.ts +54 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React, { useEffect, useState, useMemo, useCallback } from 'react';
|
|
2
|
-
import { ConnectionMode, ReactFlow, ReactFlowProps, NodeChange, applyNodeChanges } from '@xyflow/react';
|
|
1
|
+
import React, { useEffect, useState, useMemo, useCallback, useRef } from 'react';
|
|
2
|
+
import { ConnectionMode, ReactFlow, ReactFlowProps, NodeChange, applyNodeChanges, useReactFlow, Node } from '@xyflow/react';
|
|
3
3
|
import { useTriggerEvent } from '../utils/event-hooks';
|
|
4
4
|
import { IBlock, ICardNodeData } from '../types/card-node';
|
|
5
5
|
import {
|
|
@@ -45,6 +45,7 @@ import CollaborationDiagram from './collaborationDiagram/CollaborationDiagram';
|
|
|
45
45
|
import Header from '../components/Header';
|
|
46
46
|
import { PaneMouseHandler } from '../utils/event-store';
|
|
47
47
|
import { AutomationExecutionPanel } from '../components/automation/AutomationExecutionPanel';
|
|
48
|
+
import { CanvasSearchBar } from '../components/CanvasSearchBar';
|
|
48
49
|
|
|
49
50
|
interface DiagramContentProps
|
|
50
51
|
extends Omit<ReactFlowProps, 'nodes' | 'edges' | 'onChange'> {
|
|
@@ -85,7 +86,117 @@ export const DiagramContent: React.FC<DiagramContentProps> = ({
|
|
|
85
86
|
const canUndo = useCanUndo();
|
|
86
87
|
const canRedo = useCanRedo();
|
|
87
88
|
|
|
88
|
-
//
|
|
89
|
+
// Zoom hooks - get zoom functions from ReactFlow
|
|
90
|
+
const { zoomIn, zoomOut, fitView, setViewport, getViewport, getNodes } = useReactFlow();
|
|
91
|
+
|
|
92
|
+
// Search bar state
|
|
93
|
+
const [showSearchBar, setShowSearchBar] = useState(false);
|
|
94
|
+
|
|
95
|
+
// Clipboard state for copy/paste
|
|
96
|
+
const [copiedNode, setCopiedNode] = useState<Node | null>(null);
|
|
97
|
+
|
|
98
|
+
// Rename state
|
|
99
|
+
const [renamingNodeId, setRenamingNodeId] = useState<string | null>(null);
|
|
100
|
+
const [renameValue, setRenameValue] = useState('');
|
|
101
|
+
|
|
102
|
+
// Filter out hidden nodes for ReactFlow rendering
|
|
103
|
+
const visibleNodes = useMemo(() => {
|
|
104
|
+
return nodes.filter(node => !node.data?.isHidden);
|
|
105
|
+
}, [nodes]);
|
|
106
|
+
|
|
107
|
+
// Search canvas functionality - opens search bar
|
|
108
|
+
const handleSearchCanvas = useCallback(() => {
|
|
109
|
+
setShowSearchBar(true);
|
|
110
|
+
}, []);
|
|
111
|
+
|
|
112
|
+
// Copy selected node
|
|
113
|
+
const handleCopyNode = useCallback(() => {
|
|
114
|
+
const nodeToCopy = visibleNodes.find(node => node.id === selectedNode || node.selected);
|
|
115
|
+
if (nodeToCopy) {
|
|
116
|
+
setCopiedNode(nodeToCopy);
|
|
117
|
+
}
|
|
118
|
+
}, [visibleNodes, selectedNode]);
|
|
119
|
+
|
|
120
|
+
// Paste node
|
|
121
|
+
const handlePasteNode = useCallback(() => {
|
|
122
|
+
if (!copiedNode) return;
|
|
123
|
+
|
|
124
|
+
const newNode = {
|
|
125
|
+
...copiedNode,
|
|
126
|
+
id: `${copiedNode.id}-copy-${Date.now()}`,
|
|
127
|
+
position: {
|
|
128
|
+
x: copiedNode.position.x + 50,
|
|
129
|
+
y: copiedNode.position.y + 50,
|
|
130
|
+
},
|
|
131
|
+
selected: false,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
setNodes([...nodes, newNode as any]);
|
|
135
|
+
setSelectedNode(newNode.id);
|
|
136
|
+
}, [copiedNode, nodes, setNodes, setSelectedNode]);
|
|
137
|
+
|
|
138
|
+
// Delete selected node
|
|
139
|
+
const handleDeleteNode = useCallback(() => {
|
|
140
|
+
const nodeToDelete = visibleNodes.find(node => node.id === selectedNode || node.selected);
|
|
141
|
+
if (nodeToDelete && onNodesChange) {
|
|
142
|
+
onNodesChange([{ id: nodeToDelete.id, type: 'remove' }]);
|
|
143
|
+
setSelectedNode(null);
|
|
144
|
+
}
|
|
145
|
+
}, [visibleNodes, selectedNode, onNodesChange, setSelectedNode]);
|
|
146
|
+
|
|
147
|
+
// Rename node
|
|
148
|
+
const handleRenameNode = useCallback(() => {
|
|
149
|
+
const nodeToRename = visibleNodes.find(node => node.id === selectedNode || node.selected);
|
|
150
|
+
if (nodeToRename) {
|
|
151
|
+
setRenamingNodeId(nodeToRename.id);
|
|
152
|
+
setRenameValue((nodeToRename.data as any)?.label || nodeToRename.id);
|
|
153
|
+
}
|
|
154
|
+
}, [visibleNodes, selectedNode]);
|
|
155
|
+
|
|
156
|
+
// Save rename
|
|
157
|
+
const handleSaveRename = useCallback(() => {
|
|
158
|
+
if (!renamingNodeId || !renameValue.trim()) {
|
|
159
|
+
setRenamingNodeId(null);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const updatedNodes = nodes.map(node => {
|
|
164
|
+
if (node.id === renamingNodeId) {
|
|
165
|
+
return {
|
|
166
|
+
...node,
|
|
167
|
+
data: {
|
|
168
|
+
...node.data,
|
|
169
|
+
label: renameValue.trim(),
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
return node;
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
setNodes(updatedNodes);
|
|
177
|
+
setRenamingNodeId(null);
|
|
178
|
+
setRenameValue('');
|
|
179
|
+
}, [renamingNodeId, renameValue, nodes, setNodes]);
|
|
180
|
+
|
|
181
|
+
// Cancel rename
|
|
182
|
+
const handleCancelRename = useCallback(() => {
|
|
183
|
+
setRenamingNodeId(null);
|
|
184
|
+
setRenameValue('');
|
|
185
|
+
}, []);
|
|
186
|
+
|
|
187
|
+
// Edit node (open node configuration/form)
|
|
188
|
+
const handleEditNode = useCallback(() => {
|
|
189
|
+
const nodeToEdit = visibleNodes.find(node => node.id === selectedNode || node.selected);
|
|
190
|
+
if (nodeToEdit) {
|
|
191
|
+
// For automation nodes, we can trigger a double-click or open a form
|
|
192
|
+
// This will depend on the specific node type implementation
|
|
193
|
+
// For now, we'll just ensure the node is selected and could trigger a modal
|
|
194
|
+
setSelectedNode(nodeToEdit.id);
|
|
195
|
+
// You can add specific edit logic here based on node type
|
|
196
|
+
}
|
|
197
|
+
}, [visibleNodes, selectedNode, setSelectedNode]);
|
|
198
|
+
|
|
199
|
+
// Handle keyboard shortcuts for undo/redo and zoom
|
|
89
200
|
useEffect(() => {
|
|
90
201
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
91
202
|
// Don't trigger if user is typing in an input field
|
|
@@ -115,18 +226,116 @@ export const DiagramContent: React.FC<DiagramContentProps> = ({
|
|
|
115
226
|
redo();
|
|
116
227
|
}
|
|
117
228
|
}
|
|
229
|
+
|
|
230
|
+
// + or = for Zoom In (handles both + key and = key with shift)
|
|
231
|
+
if (event.key === '+' || event.key === '=') {
|
|
232
|
+
event.preventDefault();
|
|
233
|
+
zoomIn();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// - for Zoom Out
|
|
237
|
+
if (event.key === '-') {
|
|
238
|
+
event.preventDefault();
|
|
239
|
+
zoomOut();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// 0 for Reset Zoom (reset to zoom level 1)
|
|
243
|
+
if (event.key === '0' && !event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
244
|
+
event.preventDefault();
|
|
245
|
+
const viewport = getViewport();
|
|
246
|
+
setViewport({ x: viewport.x, y: viewport.y, zoom: 1 });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 1 for Fit to View
|
|
250
|
+
if (event.key === '1' && !event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
251
|
+
event.preventDefault();
|
|
252
|
+
fitView();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Ctrl+G or Cmd+G for Search Canvas (toggle search bar)
|
|
256
|
+
if (
|
|
257
|
+
(event.ctrlKey || event.metaKey) &&
|
|
258
|
+
event.key === 'g' &&
|
|
259
|
+
!event.shiftKey &&
|
|
260
|
+
!event.altKey
|
|
261
|
+
) {
|
|
262
|
+
event.preventDefault();
|
|
263
|
+
if (showSearchBar) {
|
|
264
|
+
// Close search bar if already open
|
|
265
|
+
setShowSearchBar(false);
|
|
266
|
+
} else {
|
|
267
|
+
// Open search bar
|
|
268
|
+
handleSearchCanvas();
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Ctrl+C or Cmd+C for Copy selected node
|
|
273
|
+
if (
|
|
274
|
+
(event.ctrlKey || event.metaKey) &&
|
|
275
|
+
event.key === 'c' &&
|
|
276
|
+
!event.shiftKey &&
|
|
277
|
+
!event.altKey
|
|
278
|
+
) {
|
|
279
|
+
event.preventDefault();
|
|
280
|
+
handleCopyNode();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Ctrl+V or Cmd+V for Paste node
|
|
284
|
+
if (
|
|
285
|
+
(event.ctrlKey || event.metaKey) &&
|
|
286
|
+
event.key === 'v' &&
|
|
287
|
+
!event.shiftKey &&
|
|
288
|
+
!event.altKey
|
|
289
|
+
) {
|
|
290
|
+
event.preventDefault();
|
|
291
|
+
handlePasteNode();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Delete key for Delete selected node
|
|
295
|
+
if (event.key === 'Delete' || event.key === 'Backspace') {
|
|
296
|
+
if (selectedNode || visibleNodes.some(node => node.selected)) {
|
|
297
|
+
event.preventDefault();
|
|
298
|
+
handleDeleteNode();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// F2 for Rename node
|
|
303
|
+
if (event.key === 'F2') {
|
|
304
|
+
event.preventDefault();
|
|
305
|
+
handleRenameNode();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Enter for Edit node
|
|
309
|
+
if (event.key === 'Enter' && !event.shiftKey) {
|
|
310
|
+
if (selectedNode || visibleNodes.some(node => node.selected)) {
|
|
311
|
+
event.preventDefault();
|
|
312
|
+
handleEditNode();
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Escape to cancel rename
|
|
317
|
+
if (event.key === 'Escape' && renamingNodeId) {
|
|
318
|
+
event.preventDefault();
|
|
319
|
+
handleCancelRename();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Enter to save rename
|
|
323
|
+
if (event.key === 'Enter' && renamingNodeId) {
|
|
324
|
+
event.preventDefault();
|
|
325
|
+
handleSaveRename();
|
|
326
|
+
}
|
|
118
327
|
};
|
|
119
328
|
|
|
120
329
|
document.addEventListener('keydown', handleKeyDown);
|
|
121
330
|
return () => {
|
|
122
331
|
document.removeEventListener('keydown', handleKeyDown);
|
|
123
332
|
};
|
|
124
|
-
}, [
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
333
|
+
}, [
|
|
334
|
+
undo, redo, canUndo, canRedo, zoomIn, zoomOut, fitView, setViewport, getViewport,
|
|
335
|
+
handleSearchCanvas, showSearchBar, handleCopyNode, handlePasteNode, handleDeleteNode,
|
|
336
|
+
handleRenameNode, handleEditNode, handleSaveRename, handleCancelRename,
|
|
337
|
+
selectedNode, visibleNodes, renamingNodeId
|
|
338
|
+
]);
|
|
130
339
|
|
|
131
340
|
// Filter out edges connected to hidden nodes
|
|
132
341
|
const visibleEdges = useMemo(() => {
|
|
@@ -214,58 +423,6 @@ export const DiagramContent: React.FC<DiagramContentProps> = ({
|
|
|
214
423
|
setActiveCard,
|
|
215
424
|
});
|
|
216
425
|
|
|
217
|
-
// Handle keyboard deletion of selected nodes (only for automation diagram)
|
|
218
|
-
useEffect(() => {
|
|
219
|
-
if (diagramType !== 'automation') return;
|
|
220
|
-
|
|
221
|
-
const handleKeyDown = (event: KeyboardEvent) => {
|
|
222
|
-
// Check if Delete or Backspace key is pressed
|
|
223
|
-
if (event.key === 'Delete' || event.key === 'Backspace') {
|
|
224
|
-
// Don't trigger deletion if user is typing in an input field
|
|
225
|
-
if (
|
|
226
|
-
event.target instanceof HTMLInputElement ||
|
|
227
|
-
event.target instanceof HTMLTextAreaElement ||
|
|
228
|
-
(event.target as any)?.isContentEditable
|
|
229
|
-
) {
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Check if a node is selected (either from store or ReactFlow's selection)
|
|
234
|
-
const selectedNodeId = selectedNode || visibleNodes.find(node => node.selected)?.id;
|
|
235
|
-
|
|
236
|
-
if (selectedNodeId) {
|
|
237
|
-
event.preventDefault();
|
|
238
|
-
event.stopPropagation();
|
|
239
|
-
|
|
240
|
-
// Find the node to delete
|
|
241
|
-
const nodeToDelete = nodes.find((node) => node.id === selectedNodeId);
|
|
242
|
-
if (!nodeToDelete) return;
|
|
243
|
-
|
|
244
|
-
// Create a remove change for the node
|
|
245
|
-
const nodeChanges: NodeChange[] = [
|
|
246
|
-
{
|
|
247
|
-
id: selectedNodeId,
|
|
248
|
-
type: 'remove',
|
|
249
|
-
},
|
|
250
|
-
];
|
|
251
|
-
|
|
252
|
-
// Apply the changes through onNodesChange (which handles edge cleanup)
|
|
253
|
-
onNodesChange(nodeChanges);
|
|
254
|
-
|
|
255
|
-
// Clear the selected node
|
|
256
|
-
setSelectedNode(null);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
// Add event listener
|
|
262
|
-
document.addEventListener('keydown', handleKeyDown);
|
|
263
|
-
|
|
264
|
-
// Cleanup on unmount
|
|
265
|
-
return () => {
|
|
266
|
-
document.removeEventListener('keydown', handleKeyDown);
|
|
267
|
-
};
|
|
268
|
-
}, [diagramType, selectedNode, nodes, visibleNodes, onNodesChange, setSelectedNode]);
|
|
269
426
|
|
|
270
427
|
const renderDynamicDiagram = () => {
|
|
271
428
|
if (diagramType === 'sequence') {
|
|
@@ -540,6 +697,64 @@ export const DiagramContent: React.FC<DiagramContentProps> = ({
|
|
|
540
697
|
onDragCancel={onDragCancel}
|
|
541
698
|
sensors={sensors}
|
|
542
699
|
>
|
|
700
|
+
{/* Canvas Search Bar - Available for all diagram types */}
|
|
701
|
+
{showSearchBar && (
|
|
702
|
+
<CanvasSearchBar
|
|
703
|
+
nodes={visibleNodes}
|
|
704
|
+
onClose={() => setShowSearchBar(false)}
|
|
705
|
+
onNodeSelect={setSelectedNode}
|
|
706
|
+
onViewportChange={(nodeId) => {
|
|
707
|
+
// Viewport change is handled inside CanvasSearchBar
|
|
708
|
+
}}
|
|
709
|
+
getViewport={getViewport}
|
|
710
|
+
setViewport={setViewport}
|
|
711
|
+
/>
|
|
712
|
+
)}
|
|
713
|
+
|
|
714
|
+
{/* Rename Input - Appears when F2 is pressed */}
|
|
715
|
+
{renamingNodeId && (() => {
|
|
716
|
+
const nodeToRename = visibleNodes.find(n => n.id === renamingNodeId);
|
|
717
|
+
if (!nodeToRename) return null;
|
|
718
|
+
const viewport = getViewport();
|
|
719
|
+
return (
|
|
720
|
+
<Box
|
|
721
|
+
sx={{
|
|
722
|
+
position: 'fixed',
|
|
723
|
+
top: `${nodeToRename.position.y * viewport.zoom + viewport.y + 20}px`,
|
|
724
|
+
left: `${nodeToRename.position.x * viewport.zoom + viewport.x + 20}px`,
|
|
725
|
+
zIndex: 1000,
|
|
726
|
+
backgroundColor: '#1e293b',
|
|
727
|
+
border: '2px solid #3b82f6',
|
|
728
|
+
borderRadius: '8px',
|
|
729
|
+
padding: '8px 12px',
|
|
730
|
+
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.5)',
|
|
731
|
+
}}
|
|
732
|
+
>
|
|
733
|
+
<input
|
|
734
|
+
type="text"
|
|
735
|
+
value={renameValue}
|
|
736
|
+
onChange={(e) => setRenameValue(e.target.value)}
|
|
737
|
+
onKeyDown={(e) => {
|
|
738
|
+
if (e.key === 'Enter') {
|
|
739
|
+
handleSaveRename();
|
|
740
|
+
} else if (e.key === 'Escape') {
|
|
741
|
+
handleCancelRename();
|
|
742
|
+
}
|
|
743
|
+
}}
|
|
744
|
+
onBlur={handleSaveRename}
|
|
745
|
+
autoFocus
|
|
746
|
+
style={{
|
|
747
|
+
backgroundColor: 'transparent',
|
|
748
|
+
border: 'none',
|
|
749
|
+
color: '#fff',
|
|
750
|
+
fontSize: '14px',
|
|
751
|
+
outline: 'none',
|
|
752
|
+
width: '200px',
|
|
753
|
+
}}
|
|
754
|
+
/>
|
|
755
|
+
</Box>
|
|
756
|
+
);
|
|
757
|
+
})()}
|
|
543
758
|
{renderDynamicDiagram()}
|
|
544
759
|
</DndContext>
|
|
545
760
|
);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
export interface AutomationNodeFormData {
|
|
3
3
|
nodeId: string;
|
|
4
4
|
title: string;
|
|
5
|
-
type: 'start' | 'api' | 'formatting' | 'sheets' | 'end';
|
|
5
|
+
type: 'start' | 'api' | 'formatting' | 'sheets' | 'end' | 'navigation';
|
|
6
6
|
isPinned?: boolean;
|
|
7
7
|
isBlock?: boolean;
|
|
8
8
|
blocks?: Array<{
|
|
@@ -245,12 +245,32 @@ export interface AutomationEndNodeForm extends AutomationNodeFormData {
|
|
|
245
245
|
};
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
+
export interface AutomationNavigationNodeForm extends AutomationNodeFormData {
|
|
249
|
+
type: 'navigation';
|
|
250
|
+
navigationType: 'navigate' | 'click' | 'scroll' | 'wait' | 'extract';
|
|
251
|
+
url?: string;
|
|
252
|
+
selector?: string;
|
|
253
|
+
action?: string;
|
|
254
|
+
waitTime?: number;
|
|
255
|
+
scrollDirection?: 'up' | 'down' | 'to-element';
|
|
256
|
+
extractSelector?: string;
|
|
257
|
+
outputVariable?: string;
|
|
258
|
+
timeout?: number;
|
|
259
|
+
retryCount?: number;
|
|
260
|
+
errorHandling?: {
|
|
261
|
+
onError: 'stop' | 'retry' | 'continue';
|
|
262
|
+
maxRetries?: number;
|
|
263
|
+
fallbackAction?: 'skip' | 'log' | 'notify';
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
248
267
|
export type AutomationNodeForm =
|
|
249
268
|
| AutomationStartNodeForm
|
|
250
269
|
| AutomationApiNodeForm
|
|
251
270
|
| AutomationFormattingNodeForm
|
|
252
271
|
| AutomationSheetsNodeForm
|
|
253
|
-
| AutomationEndNodeForm
|
|
272
|
+
| AutomationEndNodeForm
|
|
273
|
+
| AutomationNavigationNodeForm;
|
|
254
274
|
|
|
255
275
|
// Automation Workflow Data Structure
|
|
256
276
|
export interface AutomationWorkflowData {
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
AutomationSheetsNode,
|
|
11
11
|
AutomationEndNode,
|
|
12
12
|
AutomationNoteNode,
|
|
13
|
+
AutomationNavigationNode,
|
|
13
14
|
AutomationAISuggestionNode,
|
|
14
15
|
} from '../components/automation';
|
|
15
16
|
|
|
@@ -25,5 +26,6 @@ export default {
|
|
|
25
26
|
AutomationSheetsNode,
|
|
26
27
|
AutomationEndNode,
|
|
27
28
|
AutomationNoteNode,
|
|
29
|
+
AutomationNavigationNode,
|
|
28
30
|
AutomationAISuggestionNode,
|
|
29
31
|
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Utility function to highlight search terms in text
|
|
5
|
+
* @param text - The text to highlight
|
|
6
|
+
* @param searchQuery - The search query to highlight
|
|
7
|
+
* @returns React node with highlighted text
|
|
8
|
+
*/
|
|
9
|
+
export const highlightText = (text: string, searchQuery: string): React.ReactNode => {
|
|
10
|
+
if (!searchQuery.trim() || !text) {
|
|
11
|
+
return text;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const searchLower = searchQuery.toLowerCase();
|
|
15
|
+
const textLower = text.toLowerCase();
|
|
16
|
+
const index = textLower.indexOf(searchLower);
|
|
17
|
+
|
|
18
|
+
if (index === -1) {
|
|
19
|
+
return text;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const beforeMatch = text.substring(0, index);
|
|
23
|
+
const match = text.substring(index, index + searchQuery.length);
|
|
24
|
+
const afterMatch = text.substring(index + searchQuery.length);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<>
|
|
28
|
+
{beforeMatch}
|
|
29
|
+
<mark
|
|
30
|
+
style={{
|
|
31
|
+
backgroundColor: '#FFD700',
|
|
32
|
+
color: '#000',
|
|
33
|
+
padding: '2px 4px',
|
|
34
|
+
borderRadius: '3px',
|
|
35
|
+
fontWeight: 'bold',
|
|
36
|
+
}}
|
|
37
|
+
>
|
|
38
|
+
{match}
|
|
39
|
+
</mark>
|
|
40
|
+
{afterMatch}
|
|
41
|
+
</>
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Highlight all occurrences of search query in text
|
|
47
|
+
*/
|
|
48
|
+
export const highlightAllOccurrences = (text: string, searchQuery: string): React.ReactNode => {
|
|
49
|
+
if (!searchQuery.trim() || !text) {
|
|
50
|
+
return text;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const searchLower = searchQuery.toLowerCase();
|
|
54
|
+
const textLower = text.toLowerCase();
|
|
55
|
+
const parts: React.ReactNode[] = [];
|
|
56
|
+
let lastIndex = 0;
|
|
57
|
+
let index = textLower.indexOf(searchLower, lastIndex);
|
|
58
|
+
|
|
59
|
+
while (index !== -1) {
|
|
60
|
+
// Add text before match
|
|
61
|
+
if (index > lastIndex) {
|
|
62
|
+
parts.push(text.substring(lastIndex, index));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Add highlighted match
|
|
66
|
+
parts.push(
|
|
67
|
+
<mark
|
|
68
|
+
key={index}
|
|
69
|
+
style={{
|
|
70
|
+
backgroundColor: '#FFD700',
|
|
71
|
+
color: '#000',
|
|
72
|
+
padding: '2px 4px',
|
|
73
|
+
borderRadius: '3px',
|
|
74
|
+
fontWeight: 'bold',
|
|
75
|
+
}}
|
|
76
|
+
>
|
|
77
|
+
{text.substring(index, index + searchQuery.length)}
|
|
78
|
+
</mark>
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
lastIndex = index + searchQuery.length;
|
|
82
|
+
index = textLower.indexOf(searchLower, lastIndex);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Add remaining text
|
|
86
|
+
if (lastIndex < text.length) {
|
|
87
|
+
parts.push(text.substring(lastIndex));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return <>{parts}</>;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration utility for Node AI Assistant
|
|
3
|
+
* This allows setting a global API endpoint and headers for all node AI assistants
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
let globalApiEndpoint: string | undefined = undefined;
|
|
7
|
+
let globalApiHeaders: Record<string, string> = {};
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Set the global API endpoint for Node AI Assistant
|
|
11
|
+
* @param endpoint - The API endpoint URL (e.g., 'https://api.example.com/chat')
|
|
12
|
+
*/
|
|
13
|
+
export const setNodeAIAssistantEndpoint = (endpoint: string | undefined) => {
|
|
14
|
+
globalApiEndpoint = endpoint;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Set global API headers for Node AI Assistant requests
|
|
19
|
+
* @param headers - Headers object (e.g., { 'Authorization': 'Bearer token' })
|
|
20
|
+
*/
|
|
21
|
+
export const setNodeAIAssistantHeaders = (headers: Record<string, string>) => {
|
|
22
|
+
globalApiHeaders = headers;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get the global API endpoint
|
|
27
|
+
*/
|
|
28
|
+
export const getNodeAIAssistantEndpoint = (): string | undefined => {
|
|
29
|
+
return globalApiEndpoint;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get the global API headers
|
|
34
|
+
*/
|
|
35
|
+
export const getNodeAIAssistantHeaders = (): Record<string, string> => {
|
|
36
|
+
return globalApiHeaders;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Configure Node AI Assistant with endpoint and headers
|
|
41
|
+
* @param config - Configuration object
|
|
42
|
+
*/
|
|
43
|
+
export const configureNodeAIAssistant = (config: {
|
|
44
|
+
endpoint?: string;
|
|
45
|
+
headers?: Record<string, string>;
|
|
46
|
+
}) => {
|
|
47
|
+
if (config.endpoint !== undefined) {
|
|
48
|
+
globalApiEndpoint = config.endpoint;
|
|
49
|
+
}
|
|
50
|
+
if (config.headers) {
|
|
51
|
+
globalApiHeaders = { ...globalApiHeaders, ...config.headers };
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|