@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.
Files changed (20) hide show
  1. package/apps/diagramming/src/AutomationDiagramData.ts +65 -1
  2. package/package.json +1 -1
  3. package/packages/diagrams/src/lib/components/CanvasSearchBar.tsx +281 -0
  4. package/packages/diagrams/src/lib/components/automation/AutomationApiNode.tsx +796 -788
  5. package/packages/diagrams/src/lib/components/automation/AutomationEndNode.tsx +608 -600
  6. package/packages/diagrams/src/lib/components/automation/AutomationFormattingNode.tsx +833 -825
  7. package/packages/diagrams/src/lib/components/automation/AutomationNavigationNode.tsx +584 -0
  8. package/packages/diagrams/src/lib/components/automation/AutomationNoteNode.tsx +422 -414
  9. package/packages/diagrams/src/lib/components/automation/AutomationSheetsNode.tsx +1120 -1112
  10. package/packages/diagrams/src/lib/components/automation/AutomationStartNode.tsx +511 -503
  11. package/packages/diagrams/src/lib/components/automation/NodeAIAssistantPopup.tsx +504 -0
  12. package/packages/diagrams/src/lib/components/automation/NodeActionButtons.tsx +146 -145
  13. package/packages/diagrams/src/lib/components/automation/index.ts +21 -12
  14. package/packages/diagrams/src/lib/contexts/SearchContext.tsx +78 -0
  15. package/packages/diagrams/src/lib/templates/DiagramContainer.tsx +79 -76
  16. package/packages/diagrams/src/lib/templates/DiagramContent.tsx +276 -61
  17. package/packages/diagrams/src/lib/types/automation-node-data-types.ts +22 -2
  18. package/packages/diagrams/src/lib/types/node-types.ts +2 -0
  19. package/packages/diagrams/src/lib/utils/highlightText.tsx +93 -0
  20. package/packages/diagrams/src/lib/utils/nodeAIAssistantConfig.ts +54 -0
@@ -1,503 +1,511 @@
1
- import React, { useEffect, useState, useRef } from 'react';
2
- import { createRoot } from 'react-dom/client';
3
- import { Handle, Position, useNodeId } from '@xyflow/react';
4
- import { Box, Typography, Chip, IconButton, Card, CardContent } from '@mui/material';
5
- import { AccessTime as AccessTimeIcon } from '@mui/icons-material';
6
- import { RiCloseLine, RiUser2Line } from 'react-icons/ri';
7
- import ReactJson from 'react-json-view';
8
- import { getIconByName } from '../../utils/iconMapper';
9
- import { useTranslation } from 'react-i18next';
10
- import { useDiagram } from '../../contexts/DiagramProvider';
11
- import { NodeActionButtons } from './NodeActionButtons';
12
-
13
- interface AutomationStartNodeProps {
14
- data: {
15
- label: string;
16
- description: string;
17
- status: 'Ready' | 'Running' | 'Completed' | 'Error';
18
- triggerType: 'manual' | 'scheduled';
19
- scheduleConfig?: {
20
- frequency: 'hourly' | 'daily' | 'weekly';
21
- time?: string;
22
- cron?: string;
23
- };
24
- lastRun: string;
25
- backgroundColor: string;
26
- textColor: string;
27
- borderColor: string;
28
- iconName?: string; // Add iconName to the interface
29
- formData?: any; // Include formData for configuration
30
- };
31
- selected?: boolean;
32
- }
33
-
34
- export const AutomationStartNode: React.FC<AutomationStartNodeProps> = ({ data, selected }) => {
35
- const { t } = useTranslation();
36
- const [isJsonOpen, setIsJsonOpen] = useState(false);
37
- const rootRef = useRef<any>(null);
38
- const portalRef = useRef<HTMLDivElement | null>(null);
39
- const nodeRef = useRef<HTMLDivElement | null>(null);
40
- const nodeId = useNodeId();
41
- const setSelectedNode = useDiagram((state) => state.setSelectedNode);
42
- const enableJson = useDiagram((state) => state.enableNodeJsonPopover ?? true);
43
- const onNodesChange = useDiagram((state) => state.onNodesChange);
44
- const nodes = useDiagram((state) => state.nodes);
45
- const setNodes = useDiagram((state) => state.setNodes);
46
-
47
- // Get the icon component based on the iconName
48
- const IconComponent = getIconByName(data.iconName);
49
-
50
-
51
-
52
- const handleJsonClick = () => {
53
- if (nodeId) setSelectedNode(nodeId);
54
- if (!enableJson) return;
55
- setIsJsonOpen(!isJsonOpen);
56
- };
57
-
58
- const handleClose = () => {
59
- setIsJsonOpen(false);
60
- // Clean up portal
61
- if (rootRef.current) {
62
- rootRef.current.unmount();
63
- rootRef.current = null;
64
- }
65
- if (portalRef.current) {
66
- document.body.removeChild(portalRef.current);
67
- portalRef.current = null;
68
- }
69
- };
70
-
71
- useEffect(() => {
72
- const handleClickOutside = (event: MouseEvent) => {
73
- if (isJsonOpen && !(event.target as Element).closest('#automation-json-popover')) {
74
- handleClose();
75
- }
76
- };
77
- document.addEventListener('mousedown', handleClickOutside);
78
- return () => {
79
- document.removeEventListener('mousedown', handleClickOutside);
80
- };
81
- }, [isJsonOpen]);
82
-
83
- // Debug logging for node dimensions
84
- useEffect(() => {
85
- if (nodeRef.current) {
86
- const rect = nodeRef.current.getBoundingClientRect();
87
- const computedStyle = window.getComputedStyle(nodeRef.current);
88
- // Debug information available but not logged
89
- }
90
- }, [data.label]);
91
-
92
- useEffect(() => {
93
- if (isJsonOpen) {
94
- const portalRoot = document.createElement('div');
95
- document.body.appendChild(portalRoot);
96
- portalRef.current = portalRoot;
97
-
98
- const root = createRoot(portalRoot);
99
- rootRef.current = root;
100
-
101
- root.render(
102
- <Card
103
- id="automation-json-popover"
104
- sx={{
105
- position: 'fixed',
106
- top: 0,
107
- right: 0,
108
- zIndex: 9999,
109
- width: '400px',
110
- height: '100vh',
111
- overflow: 'auto',
112
- bgcolor: '#242424',
113
- color: '#fff',
114
- border: '1px solid #333',
115
- '&::-webkit-scrollbar': {
116
- width: '6px',
117
- },
118
- '&::-webkit-scrollbar-track': {
119
- background: 'transparent',
120
- },
121
- '&::-webkit-scrollbar-thumb': {
122
- background: '#444',
123
- borderRadius: '3px',
124
- '&:hover': {
125
- background: '#666',
126
- },
127
- },
128
- }}
129
- >
130
- <CardContent sx={{ bgcolor: '#242424', color: '#fff' }}>
131
- <IconButton
132
- aria-label="close"
133
- onClick={handleClose}
134
- sx={{
135
- color: '#999',
136
- '&:hover': {
137
- color: '#fff',
138
- bgcolor: 'rgba(255, 255, 255, 0.1)',
139
- },
140
- }}
141
- >
142
- <RiCloseLine />
143
- </IconButton>
144
- {/* Show execution result prominently if available */}
145
- {data.formData?.executionResult && (
146
- <Box sx={{ mb: 2 }}>
147
- <Typography variant="h6" sx={{ color: '#fff', mb: 1 }}>
148
- {t('automation.common.executionResult')}
149
- </Typography>
150
- <Box sx={{
151
- bgcolor: data.formData.executionResult.success ? '#1e3a8a' : '#dc2626',
152
- p: 1,
153
- borderRadius: 1,
154
- mb: 1
155
- }}>
156
- <Typography variant="body2" sx={{ color: '#fff' }}>
157
- {t('automation.common.status')}: {data.formData.executionResult.success ? t('automation.common.success') : t('automation.common.failed')}
158
- </Typography>
159
- <Typography variant="body2" sx={{ color: '#fff' }}>
160
- {t('automation.common.timestamp')}: {new Date(data.formData.executionResult.timestamp).toLocaleString()}
161
- </Typography>
162
- {data.formData.executionResult.error && (
163
- <Typography variant="body2" sx={{ color: '#fff' }}>
164
- {t('automation.common.error')}: {data.formData.executionResult.error}
165
- </Typography>
166
- )}
167
- </Box>
168
- <Typography variant="h6" sx={{ color: '#fff', mb: 1 }}>
169
- {t('automation.startNode.initialContext')}
170
- </Typography>
171
- <ReactJson
172
- theme={'monokai'}
173
- src={data.formData.executionResult.data}
174
- collapsed={false}
175
- />
176
- </Box>
177
- )}
178
-
179
- {/* Show full node data */}
180
- <Typography variant="h6" sx={{ color: '#fff', mb: 1 }}>
181
- {t('automation.common.fullNodeData')}
182
- </Typography>
183
- <ReactJson theme={'monokai'} src={data.formData || data} collapsed={false} />
184
- </CardContent>
185
- </Card>
186
- );
187
- } else {
188
- // Clean up when closing
189
- if (rootRef.current) {
190
- rootRef.current.unmount();
191
- rootRef.current = null;
192
- }
193
- if (portalRef.current) {
194
- document.body.removeChild(portalRef.current);
195
- portalRef.current = null;
196
- }
197
- }
198
- }, [isJsonOpen, data]);
199
-
200
- return (
201
- <Box
202
- sx={{
203
- position: 'relative',
204
- width: '336px',
205
- overflow: 'visible',
206
- }}
207
- >
208
- <Box
209
- ref={nodeRef}
210
- sx={{
211
- width: '336px',
212
- minHeight: '150px',
213
- backgroundColor: '#181C25', // New background color from image
214
- border: selected ? '2px solid #3b82f6' : '1px solid #1e293b',
215
- borderRadius: '12px',
216
- color: '#ffffff',
217
- position: 'relative',
218
- boxShadow: selected ? '0 0 0 2px rgba(59, 130, 246, 0.5)' : '0 4px 8px rgba(0, 0, 0, 0.3)',
219
- transition: 'all 0.2s ease',
220
- cursor: 'pointer',
221
- overflow: 'hidden',
222
- ...(data.status === 'Running' && {
223
- animation: 'pulse-glow 2s ease-in-out infinite',
224
- '@keyframes pulse-glow': {
225
- '0%, 100%': {
226
- boxShadow: '0 0 20px rgba(59, 130, 246, 0.4), 0 0 40px rgba(59, 130, 246, 0.2)',
227
- borderColor: 'rgba(59, 130, 246, 0.6)',
228
- },
229
- '50%': {
230
- boxShadow: '0 0 30px rgba(59, 130, 246, 0.6), 0 0 60px rgba(59, 130, 246, 0.3)',
231
- borderColor: 'rgba(59, 130, 246, 0.9)',
232
- },
233
- },
234
- }),
235
- }}
236
- onClick={handleJsonClick}
237
- >
238
- {/* Top Header Section */}
239
- <Box sx={{
240
- backgroundColor: "rgba(67, 93, 132, 0.1)",
241
- padding: '8px 16px',
242
- borderRadius: '12px 12px 0 0'
243
- }}>
244
- <Typography variant="body2" sx={{
245
- color: '#ffffff',
246
- fontSize: '12px',
247
- fontWeight: 500
248
- }}>
249
- {data.formData?.description || t('automation.startNode.headerDescription')}
250
- </Typography>
251
- </Box>
252
-
253
- {/* Main Content */}
254
- <Box sx={{ padding: '16px' }}>
255
- {/* Title Section */}
256
- <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
257
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
258
- <Box
259
- sx={{
260
- width: '32px',
261
- height: '32px',
262
- backgroundColor: '#0ea5e9',
263
- borderRadius: '50%',
264
- display: 'flex',
265
- alignItems: 'center',
266
- justifyContent: 'center',
267
- }}
268
- >
269
- <IconComponent sx={{ color: 'white', fontSize: '18px' }} />
270
- </Box>
271
- <Typography variant="h6" sx={{ fontWeight: 600, fontSize: '16px' }}>
272
- {data.label}
273
- </Typography>
274
- </Box>
275
- <Chip
276
- label={data.status || 'Ready'}
277
- size="small"
278
- sx={{
279
- backgroundColor: data.status === 'Completed'
280
- ? 'rgba(37, 99, 235, 0.1)'
281
- : data.status === 'Running'
282
- ? 'rgba(251, 191, 36, 0.1)'
283
- : data.status === 'Error'
284
- ? 'rgba(239, 68, 68, 0.1)'
285
- : 'rgba(16, 185, 129, 0.1)',
286
- color: data.status === 'Completed'
287
- ? '#93C5FD'
288
- : data.status === 'Running'
289
- ? '#FCD34D'
290
- : data.status === 'Error'
291
- ? '#FCA5A5'
292
- : '#86EFAC',
293
- fontWeight: 500,
294
- fontSize: '12px',
295
- height: '24px',
296
- borderRadius: '12px',
297
- }}
298
- />
299
- </Box>
300
-
301
- {/* Description Box */}
302
- <Box sx={{
303
- backgroundColor: '#1F2937',
304
- borderRadius: '8px',
305
- padding: '12px',
306
- mb: 2,
307
- border: '1px solid #374151'
308
- }}>
309
- {/* Inner text boundary box */}
310
- <Box sx={{
311
- backgroundColor: 'transparent',
312
- borderRadius: '4px',
313
- padding: '8px',
314
- border: '1px solid #4B5563', // Light grey border for inner box
315
- minHeight: '40px',
316
- display: 'flex',
317
- alignItems: 'center'
318
- }}>
319
- <Typography variant="body2" sx={{
320
- color: '#9CA3AF',
321
- fontSize: '12px',
322
- lineHeight: 1.4,
323
- margin: 0
324
- }}>
325
- {data.description}
326
- </Typography>
327
- </Box>
328
- </Box>
329
-
330
- {/* Last Run Info */}
331
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mt: 1 }}>
332
- <AccessTimeIcon sx={{ fontSize: '14px', color: '#9CA3AF' }} />
333
- <Typography variant="body2" sx={{ color: '#9CA3AF', fontSize: '11px' }}>
334
- {t('automation.common.lastRan')}: {data.lastRun}
335
- </Typography>
336
- </Box>
337
- </Box>
338
-
339
- {/* Connection Handles - Bidirectional (source + target at each position) */}
340
- {/* Top - Source */}
341
- <Handle
342
- type="source"
343
- position={Position.Top}
344
- id="top-source"
345
- className="connection-handle"
346
- style={{
347
- background: selected ? '#10B981' : '#1a1a2e',
348
- width: '14px',
349
- height: '14px',
350
- border: '3px solid #10B981',
351
- top: '-8px',
352
- opacity: selected ? 1 : 0,
353
- transition: 'all 0.2s ease-in-out',
354
- cursor: 'crosshair',
355
- zIndex: 10,
356
- }}
357
- />
358
- {/* Top - Target (hidden but functional) */}
359
- <Handle
360
- type="target"
361
- position={Position.Top}
362
- id="top-target"
363
- style={{
364
- background: 'transparent',
365
- width: '14px',
366
- height: '14px',
367
- border: 'none',
368
- top: '-8px',
369
- opacity: 0,
370
- pointerEvents: selected ? 'all' : 'none',
371
- }}
372
- />
373
- {/* Bottom - Source */}
374
- <Handle
375
- type="source"
376
- position={Position.Bottom}
377
- id="bottom-source"
378
- className="connection-handle"
379
- style={{
380
- background: selected ? '#10B981' : '#1a1a2e',
381
- width: '14px',
382
- height: '14px',
383
- border: '3px solid #10B981',
384
- bottom: '-8px',
385
- opacity: selected ? 1 : 0,
386
- transition: 'all 0.2s ease-in-out',
387
- cursor: 'crosshair',
388
- zIndex: 10,
389
- }}
390
- />
391
- {/* Bottom - Target (hidden but functional) */}
392
- <Handle
393
- type="target"
394
- position={Position.Bottom}
395
- id="bottom-target"
396
- style={{
397
- background: 'transparent',
398
- width: '14px',
399
- height: '14px',
400
- border: 'none',
401
- bottom: '-8px',
402
- opacity: 0,
403
- pointerEvents: selected ? 'all' : 'none',
404
- }}
405
- />
406
- {/* Left - Source */}
407
- <Handle
408
- type="source"
409
- position={Position.Left}
410
- id="left-source"
411
- className="connection-handle"
412
- style={{
413
- background: selected ? '#10B981' : '#1a1a2e',
414
- width: '14px',
415
- height: '14px',
416
- border: '3px solid #10B981',
417
- left: '-8px',
418
- opacity: selected ? 1 : 0,
419
- transition: 'all 0.2s ease-in-out',
420
- cursor: 'crosshair',
421
- zIndex: 10,
422
- }}
423
- />
424
- {/* Left - Target (hidden but functional) */}
425
- <Handle
426
- type="target"
427
- position={Position.Left}
428
- id="left-target"
429
- style={{
430
- background: 'transparent',
431
- width: '14px',
432
- height: '14px',
433
- border: 'none',
434
- left: '-8px',
435
- opacity: 0,
436
- pointerEvents: selected ? 'all' : 'none',
437
- }}
438
- />
439
- {/* Right - Source */}
440
- <Handle
441
- type="source"
442
- position={Position.Right}
443
- id="right-source"
444
- className="connection-handle"
445
- style={{
446
- background: selected ? '#10B981' : '#1a1a2e',
447
- width: '14px',
448
- height: '14px',
449
- border: '3px solid #10B981',
450
- right: '-8px',
451
- opacity: selected ? 1 : 0,
452
- transition: 'all 0.2s ease-in-out',
453
- cursor: 'crosshair',
454
- zIndex: 10,
455
- }}
456
- />
457
- {/* Right - Target (hidden but functional) */}
458
- <Handle
459
- type="target"
460
- position={Position.Right}
461
- id="right-target"
462
- style={{
463
- background: 'transparent',
464
- width: '14px',
465
- height: '14px',
466
- border: 'none',
467
- right: '-8px',
468
- opacity: 0,
469
- pointerEvents: selected ? 'all' : 'none',
470
- }}
471
- />
472
-
473
- </Box>
474
-
475
- {/* Node Action Buttons - Shows when selected */}
476
- <NodeActionButtons
477
- selected={selected}
478
- onDelete={() => {
479
- if (nodeId && onNodesChange) {
480
- onNodesChange([{ id: nodeId, type: 'remove' }]);
481
- }
482
- }}
483
- onDuplicate={() => {
484
- if (nodeId) {
485
- const currentNode = nodes.find(n => n.id === nodeId);
486
- if (currentNode) {
487
- const newNode = {
488
- ...currentNode,
489
- id: `${currentNode.id}-copy-${Date.now()}`,
490
- position: {
491
- x: currentNode.position.x + 50,
492
- y: currentNode.position.y + 50,
493
- },
494
- selected: false,
495
- };
496
- setNodes([...nodes, newNode]);
497
- }
498
- }
499
- }}
500
- />
501
- </Box>
502
- );
503
- };
1
+ import React, { useEffect, useState, useRef } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import { Handle, Position, useNodeId } from '@xyflow/react';
4
+ import { Box, Typography, Chip, IconButton, Card, CardContent } from '@mui/material';
5
+ import { AccessTime as AccessTimeIcon } from '@mui/icons-material';
6
+ import { RiCloseLine, RiUser2Line } from 'react-icons/ri';
7
+ import ReactJson from 'react-json-view';
8
+ import { getIconByName } from '../../utils/iconMapper';
9
+ import { useTranslation } from 'react-i18next';
10
+ import { useDiagram } from '../../contexts/DiagramProvider';
11
+ import { NodeActionButtons } from './NodeActionButtons';
12
+ import { showNodeAIAssistantPopup } from './NodeAIAssistantPopup';
13
+ import { useSearch } from '../../contexts/SearchContext';
14
+
15
+ interface AutomationStartNodeProps {
16
+ data: {
17
+ label: string;
18
+ description: string;
19
+ status: 'Ready' | 'Running' | 'Completed' | 'Error';
20
+ triggerType: 'manual' | 'scheduled';
21
+ scheduleConfig?: {
22
+ frequency: 'hourly' | 'daily' | 'weekly';
23
+ time?: string;
24
+ cron?: string;
25
+ };
26
+ lastRun: string;
27
+ backgroundColor: string;
28
+ textColor: string;
29
+ borderColor: string;
30
+ iconName?: string; // Add iconName to the interface
31
+ formData?: any; // Include formData for configuration
32
+ };
33
+ selected?: boolean;
34
+ }
35
+
36
+ export const AutomationStartNode: React.FC<AutomationStartNodeProps> = ({ data, selected }) => {
37
+ const { t } = useTranslation();
38
+ const { highlightText } = useSearch();
39
+ const [isJsonOpen, setIsJsonOpen] = useState(false);
40
+ const rootRef = useRef<any>(null);
41
+ const portalRef = useRef<HTMLDivElement | null>(null);
42
+ const nodeRef = useRef<HTMLDivElement | null>(null);
43
+ const nodeId = useNodeId();
44
+ const setSelectedNode = useDiagram((state) => state.setSelectedNode);
45
+ const enableJson = useDiagram((state) => state.enableNodeJsonPopover ?? true);
46
+ const onNodesChange = useDiagram((state) => state.onNodesChange);
47
+ const nodes = useDiagram((state) => state.nodes);
48
+ const setNodes = useDiagram((state) => state.setNodes);
49
+
50
+ // Get the icon component based on the iconName
51
+ const IconComponent = getIconByName(data.iconName);
52
+
53
+
54
+
55
+ const handleJsonClick = () => {
56
+ if (nodeId) setSelectedNode(nodeId);
57
+ if (!enableJson) return;
58
+ setIsJsonOpen(!isJsonOpen);
59
+ };
60
+
61
+ const handleClose = () => {
62
+ setIsJsonOpen(false);
63
+ // Clean up portal
64
+ if (rootRef.current) {
65
+ rootRef.current.unmount();
66
+ rootRef.current = null;
67
+ }
68
+ if (portalRef.current) {
69
+ document.body.removeChild(portalRef.current);
70
+ portalRef.current = null;
71
+ }
72
+ };
73
+
74
+ useEffect(() => {
75
+ const handleClickOutside = (event: MouseEvent) => {
76
+ if (isJsonOpen && !(event.target as Element).closest('#automation-json-popover')) {
77
+ handleClose();
78
+ }
79
+ };
80
+ document.addEventListener('mousedown', handleClickOutside);
81
+ return () => {
82
+ document.removeEventListener('mousedown', handleClickOutside);
83
+ };
84
+ }, [isJsonOpen]);
85
+
86
+ // Debug logging for node dimensions
87
+ useEffect(() => {
88
+ if (nodeRef.current) {
89
+ const rect = nodeRef.current.getBoundingClientRect();
90
+ const computedStyle = window.getComputedStyle(nodeRef.current);
91
+ // Debug information available but not logged
92
+ }
93
+ }, [data.label]);
94
+
95
+ useEffect(() => {
96
+ if (isJsonOpen) {
97
+ const portalRoot = document.createElement('div');
98
+ document.body.appendChild(portalRoot);
99
+ portalRef.current = portalRoot;
100
+
101
+ const root = createRoot(portalRoot);
102
+ rootRef.current = root;
103
+
104
+ root.render(
105
+ <Card
106
+ id="automation-json-popover"
107
+ sx={{
108
+ position: 'fixed',
109
+ top: 0,
110
+ right: 0,
111
+ zIndex: 9999,
112
+ width: '400px',
113
+ height: '100vh',
114
+ overflow: 'auto',
115
+ bgcolor: '#242424',
116
+ color: '#fff',
117
+ border: '1px solid #333',
118
+ '&::-webkit-scrollbar': {
119
+ width: '6px',
120
+ },
121
+ '&::-webkit-scrollbar-track': {
122
+ background: 'transparent',
123
+ },
124
+ '&::-webkit-scrollbar-thumb': {
125
+ background: '#444',
126
+ borderRadius: '3px',
127
+ '&:hover': {
128
+ background: '#666',
129
+ },
130
+ },
131
+ }}
132
+ >
133
+ <CardContent sx={{ bgcolor: '#242424', color: '#fff' }}>
134
+ <IconButton
135
+ aria-label="close"
136
+ onClick={handleClose}
137
+ sx={{
138
+ color: '#999',
139
+ '&:hover': {
140
+ color: '#fff',
141
+ bgcolor: 'rgba(255, 255, 255, 0.1)',
142
+ },
143
+ }}
144
+ >
145
+ <RiCloseLine />
146
+ </IconButton>
147
+ {/* Show execution result prominently if available */}
148
+ {data.formData?.executionResult && (
149
+ <Box sx={{ mb: 2 }}>
150
+ <Typography variant="h6" sx={{ color: '#fff', mb: 1 }}>
151
+ {t('automation.common.executionResult')}
152
+ </Typography>
153
+ <Box sx={{
154
+ bgcolor: data.formData.executionResult.success ? '#1e3a8a' : '#dc2626',
155
+ p: 1,
156
+ borderRadius: 1,
157
+ mb: 1
158
+ }}>
159
+ <Typography variant="body2" sx={{ color: '#fff' }}>
160
+ {t('automation.common.status')}: {data.formData.executionResult.success ? t('automation.common.success') : t('automation.common.failed')}
161
+ </Typography>
162
+ <Typography variant="body2" sx={{ color: '#fff' }}>
163
+ {t('automation.common.timestamp')}: {new Date(data.formData.executionResult.timestamp).toLocaleString()}
164
+ </Typography>
165
+ {data.formData.executionResult.error && (
166
+ <Typography variant="body2" sx={{ color: '#fff' }}>
167
+ {t('automation.common.error')}: {data.formData.executionResult.error}
168
+ </Typography>
169
+ )}
170
+ </Box>
171
+ <Typography variant="h6" sx={{ color: '#fff', mb: 1 }}>
172
+ {t('automation.startNode.initialContext')}
173
+ </Typography>
174
+ <ReactJson
175
+ theme={'monokai'}
176
+ src={data.formData.executionResult.data}
177
+ collapsed={false}
178
+ />
179
+ </Box>
180
+ )}
181
+
182
+ {/* Show full node data */}
183
+ <Typography variant="h6" sx={{ color: '#fff', mb: 1 }}>
184
+ {t('automation.common.fullNodeData')}
185
+ </Typography>
186
+ <ReactJson theme={'monokai'} src={data.formData || data} collapsed={false} />
187
+ </CardContent>
188
+ </Card>
189
+ );
190
+ } else {
191
+ // Clean up when closing
192
+ if (rootRef.current) {
193
+ rootRef.current.unmount();
194
+ rootRef.current = null;
195
+ }
196
+ if (portalRef.current) {
197
+ document.body.removeChild(portalRef.current);
198
+ portalRef.current = null;
199
+ }
200
+ }
201
+ }, [isJsonOpen, data]);
202
+
203
+ return (
204
+ <Box
205
+ sx={{
206
+ position: 'relative',
207
+ width: '336px',
208
+ overflow: 'visible',
209
+ }}
210
+ >
211
+ <Box
212
+ ref={nodeRef}
213
+ sx={{
214
+ width: '336px',
215
+ minHeight: '150px',
216
+ backgroundColor: '#181C25', // New background color from image
217
+ border: selected ? '2px solid #3b82f6' : '1px solid #1e293b',
218
+ borderRadius: '12px',
219
+ color: '#ffffff',
220
+ position: 'relative',
221
+ boxShadow: selected ? '0 0 0 2px rgba(59, 130, 246, 0.5)' : '0 4px 8px rgba(0, 0, 0, 0.3)',
222
+ transition: 'all 0.2s ease',
223
+ cursor: 'pointer',
224
+ overflow: 'hidden',
225
+ ...(data.status === 'Running' && {
226
+ animation: 'pulse-glow 2s ease-in-out infinite',
227
+ '@keyframes pulse-glow': {
228
+ '0%, 100%': {
229
+ boxShadow: '0 0 20px rgba(59, 130, 246, 0.4), 0 0 40px rgba(59, 130, 246, 0.2)',
230
+ borderColor: 'rgba(59, 130, 246, 0.6)',
231
+ },
232
+ '50%': {
233
+ boxShadow: '0 0 30px rgba(59, 130, 246, 0.6), 0 0 60px rgba(59, 130, 246, 0.3)',
234
+ borderColor: 'rgba(59, 130, 246, 0.9)',
235
+ },
236
+ },
237
+ }),
238
+ }}
239
+ onClick={handleJsonClick}
240
+ >
241
+ {/* Top Header Section */}
242
+ <Box sx={{
243
+ backgroundColor: "rgba(67, 93, 132, 0.1)",
244
+ padding: '8px 16px',
245
+ borderRadius: '12px 12px 0 0'
246
+ }}>
247
+ <Typography variant="body2" sx={{
248
+ color: '#ffffff',
249
+ fontSize: '12px',
250
+ fontWeight: 500
251
+ }}>
252
+ {highlightText(data.formData?.description || t('automation.startNode.headerDescription'))}
253
+ </Typography>
254
+ </Box>
255
+
256
+ {/* Main Content */}
257
+ <Box sx={{ padding: '16px' }}>
258
+ {/* Title Section */}
259
+ <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
260
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
261
+ <Box
262
+ sx={{
263
+ width: '32px',
264
+ height: '32px',
265
+ backgroundColor: '#0ea5e9',
266
+ borderRadius: '50%',
267
+ display: 'flex',
268
+ alignItems: 'center',
269
+ justifyContent: 'center',
270
+ }}
271
+ >
272
+ <IconComponent sx={{ color: 'white', fontSize: '18px' }} />
273
+ </Box>
274
+ <Typography variant="h6" sx={{ fontWeight: 600, fontSize: '16px' }}>
275
+ {highlightText(data.label)}
276
+ </Typography>
277
+ </Box>
278
+ <Chip
279
+ label={data.status || 'Ready'}
280
+ size="small"
281
+ sx={{
282
+ backgroundColor: data.status === 'Completed'
283
+ ? 'rgba(37, 99, 235, 0.1)'
284
+ : data.status === 'Running'
285
+ ? 'rgba(251, 191, 36, 0.1)'
286
+ : data.status === 'Error'
287
+ ? 'rgba(239, 68, 68, 0.1)'
288
+ : 'rgba(16, 185, 129, 0.1)',
289
+ color: data.status === 'Completed'
290
+ ? '#93C5FD'
291
+ : data.status === 'Running'
292
+ ? '#FCD34D'
293
+ : data.status === 'Error'
294
+ ? '#FCA5A5'
295
+ : '#86EFAC',
296
+ fontWeight: 500,
297
+ fontSize: '12px',
298
+ height: '24px',
299
+ borderRadius: '12px',
300
+ }}
301
+ />
302
+ </Box>
303
+
304
+ {/* Description Box */}
305
+ <Box sx={{
306
+ backgroundColor: '#1F2937',
307
+ borderRadius: '8px',
308
+ padding: '12px',
309
+ mb: 2,
310
+ border: '1px solid #374151'
311
+ }}>
312
+ {/* Inner text boundary box */}
313
+ <Box sx={{
314
+ backgroundColor: 'transparent',
315
+ borderRadius: '4px',
316
+ padding: '8px',
317
+ border: '1px solid #4B5563', // Light grey border for inner box
318
+ minHeight: '40px',
319
+ display: 'flex',
320
+ alignItems: 'center'
321
+ }}>
322
+ <Typography variant="body2" sx={{
323
+ color: '#9CA3AF',
324
+ fontSize: '12px',
325
+ lineHeight: 1.4,
326
+ margin: 0
327
+ }}>
328
+ {data.description}
329
+ </Typography>
330
+ </Box>
331
+ </Box>
332
+
333
+ {/* Last Run Info */}
334
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mt: 1 }}>
335
+ <AccessTimeIcon sx={{ fontSize: '14px', color: '#9CA3AF' }} />
336
+ <Typography variant="body2" sx={{ color: '#9CA3AF', fontSize: '11px' }}>
337
+ {t('automation.common.lastRan')}: {data.lastRun}
338
+ </Typography>
339
+ </Box>
340
+ </Box>
341
+
342
+ {/* Connection Handles - Bidirectional (source + target at each position) */}
343
+ {/* Top - Source */}
344
+ <Handle
345
+ type="source"
346
+ position={Position.Top}
347
+ id="top-source"
348
+ className="connection-handle"
349
+ style={{
350
+ background: selected ? '#10B981' : '#1a1a2e',
351
+ width: '14px',
352
+ height: '14px',
353
+ border: '3px solid #10B981',
354
+ top: '-8px',
355
+ opacity: selected ? 1 : 0,
356
+ transition: 'all 0.2s ease-in-out',
357
+ cursor: 'crosshair',
358
+ zIndex: 10,
359
+ }}
360
+ />
361
+ {/* Top - Target (hidden but functional) */}
362
+ <Handle
363
+ type="target"
364
+ position={Position.Top}
365
+ id="top-target"
366
+ style={{
367
+ background: 'transparent',
368
+ width: '14px',
369
+ height: '14px',
370
+ border: 'none',
371
+ top: '-8px',
372
+ opacity: 0,
373
+ pointerEvents: selected ? 'all' : 'none',
374
+ }}
375
+ />
376
+ {/* Bottom - Source */}
377
+ <Handle
378
+ type="source"
379
+ position={Position.Bottom}
380
+ id="bottom-source"
381
+ className="connection-handle"
382
+ style={{
383
+ background: selected ? '#10B981' : '#1a1a2e',
384
+ width: '14px',
385
+ height: '14px',
386
+ border: '3px solid #10B981',
387
+ bottom: '-8px',
388
+ opacity: selected ? 1 : 0,
389
+ transition: 'all 0.2s ease-in-out',
390
+ cursor: 'crosshair',
391
+ zIndex: 10,
392
+ }}
393
+ />
394
+ {/* Bottom - Target (hidden but functional) */}
395
+ <Handle
396
+ type="target"
397
+ position={Position.Bottom}
398
+ id="bottom-target"
399
+ style={{
400
+ background: 'transparent',
401
+ width: '14px',
402
+ height: '14px',
403
+ border: 'none',
404
+ bottom: '-8px',
405
+ opacity: 0,
406
+ pointerEvents: selected ? 'all' : 'none',
407
+ }}
408
+ />
409
+ {/* Left - Source */}
410
+ <Handle
411
+ type="source"
412
+ position={Position.Left}
413
+ id="left-source"
414
+ className="connection-handle"
415
+ style={{
416
+ background: selected ? '#10B981' : '#1a1a2e',
417
+ width: '14px',
418
+ height: '14px',
419
+ border: '3px solid #10B981',
420
+ left: '-8px',
421
+ opacity: selected ? 1 : 0,
422
+ transition: 'all 0.2s ease-in-out',
423
+ cursor: 'crosshair',
424
+ zIndex: 10,
425
+ }}
426
+ />
427
+ {/* Left - Target (hidden but functional) */}
428
+ <Handle
429
+ type="target"
430
+ position={Position.Left}
431
+ id="left-target"
432
+ style={{
433
+ background: 'transparent',
434
+ width: '14px',
435
+ height: '14px',
436
+ border: 'none',
437
+ left: '-8px',
438
+ opacity: 0,
439
+ pointerEvents: selected ? 'all' : 'none',
440
+ }}
441
+ />
442
+ {/* Right - Source */}
443
+ <Handle
444
+ type="source"
445
+ position={Position.Right}
446
+ id="right-source"
447
+ className="connection-handle"
448
+ style={{
449
+ background: selected ? '#10B981' : '#1a1a2e',
450
+ width: '14px',
451
+ height: '14px',
452
+ border: '3px solid #10B981',
453
+ right: '-8px',
454
+ opacity: selected ? 1 : 0,
455
+ transition: 'all 0.2s ease-in-out',
456
+ cursor: 'crosshair',
457
+ zIndex: 10,
458
+ }}
459
+ />
460
+ {/* Right - Target (hidden but functional) */}
461
+ <Handle
462
+ type="target"
463
+ position={Position.Right}
464
+ id="right-target"
465
+ style={{
466
+ background: 'transparent',
467
+ width: '14px',
468
+ height: '14px',
469
+ border: 'none',
470
+ right: '-8px',
471
+ opacity: 0,
472
+ pointerEvents: selected ? 'all' : 'none',
473
+ }}
474
+ />
475
+
476
+ </Box>
477
+
478
+ {/* Node Action Buttons - Shows when selected */}
479
+ <NodeActionButtons
480
+ selected={selected}
481
+ onOpenAIAssistant={(buttonElement) => {
482
+ if (nodeId) {
483
+ showNodeAIAssistantPopup(nodeId, 'Start Node', buttonElement);
484
+ }
485
+ }}
486
+ onDelete={() => {
487
+ if (nodeId && onNodesChange) {
488
+ onNodesChange([{ id: nodeId, type: 'remove' }]);
489
+ }
490
+ }}
491
+ onDuplicate={() => {
492
+ if (nodeId) {
493
+ const currentNode = nodes.find(n => n.id === nodeId);
494
+ if (currentNode) {
495
+ const newNode = {
496
+ ...currentNode,
497
+ id: `${currentNode.id}-copy-${Date.now()}`,
498
+ position: {
499
+ x: currentNode.position.x + 50,
500
+ y: currentNode.position.y + 50,
501
+ },
502
+ selected: false,
503
+ };
504
+ setNodes([...nodes, newNode]);
505
+ }
506
+ }
507
+ }}
508
+ />
509
+ </Box>
510
+ );
511
+ };