@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,600 +1,608 @@
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, Button } from '@mui/material';
5
- import { AccessTime as AccessTimeIcon, Visibility as VisibilityIcon, Save as SaveIcon, Send as SendIcon, Lightbulb as LightbulbIcon } 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 { AISuggestion } from './AISuggestionsModal';
12
- import { AISuggestionsPanel } from './AISuggestionsPanel';
13
- import { NodeActionButtons } from './NodeActionButtons';
14
-
15
- interface AutomationEndNodeProps {
16
- data: {
17
- label: string;
18
- description: string;
19
- status: 'Ready' | 'Running' | 'Completed' | 'Error';
20
- outputType: 'display' | 'store' | 'send';
21
- outputConfig?: {
22
- format: 'table' | 'json' | 'csv';
23
- destination?: string;
24
- showInConsole: boolean;
25
- showInUI: boolean;
26
- };
27
- lastRun: string;
28
- backgroundColor: string;
29
- textColor: string;
30
- borderColor: string;
31
- iconName?: string; // Add iconName to the interface
32
- formData?: {
33
- aiSuggestionsCount?: number; // Number of AI suggestions available
34
- [key: string]: any;
35
- }; // Include formData for configuration
36
- };
37
- selected?: boolean;
38
- }
39
-
40
- export const AutomationEndNode: React.FC<AutomationEndNodeProps> = ({ data, selected }) => {
41
- const { t } = useTranslation();
42
- const [isJsonOpen, setIsJsonOpen] = useState(false);
43
- const [showSuggestions, setShowSuggestions] = useState(false);
44
- const rootRef = useRef<any>(null);
45
- const portalRef = useRef<HTMLDivElement | null>(null);
46
- const nodeRef = useRef<HTMLDivElement | null>(null);
47
- const nodeId = useNodeId();
48
- const setSelectedNode = useDiagram((state) => state.setSelectedNode);
49
- const enableJson = useDiagram((state) => state.enableNodeJsonPopover ?? true);
50
- const onNodesChange = useDiagram((state) => state.onNodesChange);
51
- const nodes = useDiagram((state) => state.nodes);
52
- const setNodes = useDiagram((state) => state.setNodes);
53
-
54
- // Get the icon component based on the iconName
55
- const IconComponent = getIconByName(data.iconName);
56
-
57
- const handleJsonClick = () => {
58
- if (nodeId) setSelectedNode(nodeId);
59
- if (!enableJson) return;
60
- setIsJsonOpen(!isJsonOpen);
61
- };
62
-
63
- const handleClose = () => {
64
- setIsJsonOpen(false);
65
- // Clean up portal
66
- if (rootRef.current) {
67
- rootRef.current.unmount();
68
- rootRef.current = null;
69
- }
70
- if (portalRef.current) {
71
- document.body.removeChild(portalRef.current);
72
- portalRef.current = null;
73
- }
74
- };
75
-
76
- useEffect(() => {
77
- const handleClickOutside = (event: MouseEvent) => {
78
- if (isJsonOpen && !(event.target as Element).closest('#automation-json-popover')) {
79
- handleClose();
80
- }
81
- };
82
- document.addEventListener('mousedown', handleClickOutside);
83
- return () => {
84
- document.removeEventListener('mousedown', handleClickOutside);
85
- };
86
- }, [isJsonOpen]);
87
-
88
- // Debug logging for node dimensions
89
- useEffect(() => {
90
- if (nodeRef.current) {
91
- const rect = nodeRef.current.getBoundingClientRect();
92
- const computedStyle = window.getComputedStyle(nodeRef.current);
93
- // Debug information available but not logged
94
- }
95
- }, [data.label]);
96
-
97
- useEffect(() => {
98
- if (isJsonOpen) {
99
- const portalRoot = document.createElement('div');
100
- document.body.appendChild(portalRoot);
101
- portalRef.current = portalRoot;
102
-
103
- const root = createRoot(portalRoot);
104
- rootRef.current = root;
105
-
106
- root.render(
107
- <Card
108
- id="automation-json-popover"
109
- sx={{
110
- position: 'fixed',
111
- top: 0,
112
- right: 0,
113
- zIndex: 9999,
114
- width: '400px',
115
- height: '100vh',
116
- overflow: 'auto',
117
- bgcolor: '#242424',
118
- color: '#fff',
119
- border: '1px solid #333',
120
- '&::-webkit-scrollbar': {
121
- width: '6px',
122
- },
123
- '&::-webkit-scrollbar-track': {
124
- background: 'transparent',
125
- },
126
- '&::-webkit-scrollbar-thumb': {
127
- background: '#444',
128
- borderRadius: '3px',
129
- '&:hover': {
130
- background: '#666',
131
- },
132
- },
133
- }}
134
- >
135
- <CardContent sx={{ bgcolor: '#242424', color: '#fff' }}>
136
- <IconButton
137
- aria-label="close"
138
- onClick={handleClose}
139
- sx={{
140
- color: '#999',
141
- '&:hover': {
142
- color: '#fff',
143
- bgcolor: 'rgba(255, 255, 255, 0.1)',
144
- },
145
- }}
146
- >
147
- <RiCloseLine />
148
- </IconButton>
149
- {/* Show execution result prominently if available */}
150
- {data.formData?.executionResult && (
151
- <Box sx={{ mb: 2 }}>
152
- <Typography variant="h6" sx={{ color: '#fff', mb: 1 }}>
153
- {t('automation.common.executionResult')}
154
- </Typography>
155
- <Box sx={{
156
- bgcolor: data.formData.executionResult.success ? '#1e3a8a' : '#dc2626',
157
- p: 1,
158
- borderRadius: 1,
159
- mb: 1
160
- }}>
161
- <Typography variant="body2" sx={{ color: '#fff' }}>
162
- {t('automation.common.status')}: {data.formData.executionResult.success ? t('automation.common.success') : t('automation.common.failed')}
163
- </Typography>
164
- <Typography variant="body2" sx={{ color: '#fff' }}>
165
- {t('automation.common.timestamp')}: {new Date(data.formData.executionResult.timestamp).toLocaleString()}
166
- </Typography>
167
- {data.formData.executionResult.error && (
168
- <Typography variant="body2" sx={{ color: '#fff' }}>
169
- {t('automation.common.error')}: {data.formData.executionResult.error}
170
- </Typography>
171
- )}
172
- </Box>
173
- <Typography variant="h6" sx={{ color: '#fff', mb: 1 }}>
174
- {t('automation.endNode.finalOutput')}
175
- </Typography>
176
- <ReactJson
177
- theme={'monokai'}
178
- src={data.formData.executionResult.data}
179
- collapsed={false}
180
- />
181
- </Box>
182
- )}
183
-
184
- {/* Show full node data */}
185
- <Typography variant="h6" sx={{ color: '#fff', mb: 1 }}>
186
- {t('automation.common.fullNodeData')}
187
- </Typography>
188
- <ReactJson theme={'monokai'} src={data.formData || data} collapsed={false} />
189
- </CardContent>
190
- </Card>
191
- );
192
- } else {
193
- // Clean up when closing
194
- if (rootRef.current) {
195
- rootRef.current.unmount();
196
- rootRef.current = null;
197
- }
198
- if (portalRef.current) {
199
- document.body.removeChild(portalRef.current);
200
- portalRef.current = null;
201
- }
202
- }
203
- }, [isJsonOpen, data]);
204
-
205
- return (
206
- <Box
207
- sx={{
208
- position: 'relative',
209
- width: '336px',
210
- overflow: 'visible',
211
- }}
212
- >
213
- <Box
214
- ref={nodeRef}
215
- sx={{
216
- width: '336px',
217
- minHeight: '150px',
218
- backgroundColor: '#181C25', // New background color from image
219
- border: selected ? '2px solid #3b82f6' : '1px solid #1e293b',
220
- borderRadius: '12px',
221
- color: '#ffffff',
222
- position: 'relative',
223
- boxShadow: selected ? '0 0 0 2px rgba(59, 130, 246, 0.5)' : '0 4px 8px rgba(0, 0, 0, 0.3)',
224
- transition: 'all 0.2s ease',
225
- cursor: 'pointer',
226
- overflow: 'hidden',
227
- ...(data.status === 'Running' && {
228
- animation: 'pulse-glow 2s ease-in-out infinite',
229
- '@keyframes pulse-glow': {
230
- '0%, 100%': {
231
- boxShadow: '0 0 20px rgba(59, 130, 246, 0.4), 0 0 40px rgba(59, 130, 246, 0.2)',
232
- borderColor: 'rgba(59, 130, 246, 0.6)',
233
- },
234
- '50%': {
235
- boxShadow: '0 0 30px rgba(59, 130, 246, 0.6), 0 0 60px rgba(59, 130, 246, 0.3)',
236
- borderColor: 'rgba(59, 130, 246, 0.9)',
237
- },
238
- },
239
- }),
240
- }}
241
- onClick={handleJsonClick}
242
- >
243
- {/* Top Header Section */}
244
- <Box sx={{
245
- backgroundColor: "rgba(67, 93, 132, 0.1)",
246
- padding: '8px 16px',
247
- borderRadius: '12px 12px 0 0'
248
- }}>
249
- <Typography variant="body2" sx={{
250
- color: '#ffffff',
251
- fontSize: '12px',
252
- fontWeight: 500
253
- }}>
254
- {data.formData?.description || t('automation.endNode.headerDescription')}
255
- </Typography>
256
- </Box>
257
-
258
- {/* Main Content */}
259
- <Box sx={{ padding: '16px' }}>
260
- {/* Title Section */}
261
- <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
262
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
263
- <Box
264
- sx={{
265
- width: '32px',
266
- height: '32px',
267
- backgroundColor: '#0ea5e9', // Blue-green color like in image
268
- borderRadius: '50%',
269
- display: 'flex',
270
- alignItems: 'center',
271
- justifyContent: 'center',
272
- }}
273
- >
274
- <IconComponent sx={{ color: 'white', fontSize: '18px' }} />
275
- </Box>
276
- <Typography variant="h6" sx={{ fontWeight: 600, fontSize: '16px' }}>
277
- {data.label}
278
- </Typography>
279
- </Box>
280
- <Chip
281
- label={data.status || 'Ready'}
282
- size="small"
283
- sx={{
284
- backgroundColor: data.status === 'Completed'
285
- ? 'rgba(37, 99, 235, 0.1)'
286
- : data.status === 'Running'
287
- ? 'rgba(251, 191, 36, 0.1)'
288
- : data.status === 'Error'
289
- ? 'rgba(239, 68, 68, 0.1)'
290
- : 'rgba(16, 185, 129, 0.1)',
291
- color: data.status === 'Completed'
292
- ? '#93C5FD'
293
- : data.status === 'Running'
294
- ? '#FCD34D'
295
- : data.status === 'Error'
296
- ? '#FCA5A5'
297
- : '#86EFAC',
298
- fontWeight: 500,
299
- fontSize: '12px',
300
- height: '24px',
301
- borderRadius: '12px',
302
- }}
303
- />
304
- </Box>
305
-
306
- {/* Description Box */}
307
- <Box sx={{
308
- backgroundColor: '#1F2937',
309
- borderRadius: '8px',
310
- padding: '12px',
311
- mb: 2,
312
- border: '1px solid #374151'
313
- }}>
314
- {/* Inner text boundary box */}
315
- <Box sx={{
316
- backgroundColor: 'transparent',
317
- borderRadius: '4px',
318
- padding: '8px',
319
- border: '1px solid #4B5563', // Light grey border for inner box
320
- minHeight: '40px',
321
- display: 'flex',
322
- alignItems: 'center'
323
- }}>
324
- <Typography variant="body2" sx={{
325
- color: '#9CA3AF',
326
- fontSize: '12px',
327
- lineHeight: 1.4,
328
- margin: 0
329
- }}>
330
- {data.description}
331
- </Typography>
332
- </Box>
333
- </Box>
334
-
335
- {/* Last Run Info */}
336
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mt: 1 }}>
337
- <AccessTimeIcon sx={{ fontSize: '14px', color: '#9CA3AF' }} />
338
- <Typography variant="body2" sx={{ color: '#9CA3AF', fontSize: '11px' }}>
339
- {t('automation.common.lastRan')}: {data.lastRun}
340
- </Typography>
341
- </Box>
342
- </Box>
343
-
344
- {/* Connection Handles - Bidirectional (source + target at each position) */}
345
- {/* Top - Source */}
346
- <Handle
347
- type="source"
348
- position={Position.Top}
349
- id="top-source"
350
- className="connection-handle"
351
- style={{
352
- background: selected ? '#10B981' : '#1a1a2e',
353
- width: '14px',
354
- height: '14px',
355
- border: '3px solid #10B981',
356
- top: '-8px',
357
- opacity: selected ? 1 : 0,
358
- transition: 'all 0.2s ease-in-out',
359
- cursor: 'crosshair',
360
- zIndex: 10,
361
- }}
362
- />
363
- {/* Top - Target (hidden but functional) */}
364
- <Handle
365
- type="target"
366
- position={Position.Top}
367
- id="top-target"
368
- style={{
369
- background: 'transparent',
370
- width: '14px',
371
- height: '14px',
372
- border: 'none',
373
- top: '-8px',
374
- opacity: 0,
375
- pointerEvents: selected ? 'all' : 'none',
376
- }}
377
- />
378
- {/* Bottom - Source */}
379
- <Handle
380
- type="source"
381
- position={Position.Bottom}
382
- id="bottom-source"
383
- className="connection-handle"
384
- style={{
385
- background: selected ? '#10B981' : '#1a1a2e',
386
- width: '14px',
387
- height: '14px',
388
- border: '3px solid #10B981',
389
- bottom: '-8px',
390
- opacity: selected ? 1 : 0,
391
- transition: 'all 0.2s ease-in-out',
392
- cursor: 'crosshair',
393
- zIndex: 10,
394
- }}
395
- />
396
- {/* Bottom - Target (hidden but functional) */}
397
- <Handle
398
- type="target"
399
- position={Position.Bottom}
400
- id="bottom-target"
401
- style={{
402
- background: 'transparent',
403
- width: '14px',
404
- height: '14px',
405
- border: 'none',
406
- bottom: '-8px',
407
- opacity: 0,
408
- pointerEvents: selected ? 'all' : 'none',
409
- }}
410
- />
411
- {/* Left - Source */}
412
- <Handle
413
- type="source"
414
- position={Position.Left}
415
- id="left-source"
416
- className="connection-handle"
417
- style={{
418
- background: selected ? '#10B981' : '#1a1a2e',
419
- width: '14px',
420
- height: '14px',
421
- border: '3px solid #10B981',
422
- left: '-8px',
423
- opacity: selected ? 1 : 0,
424
- transition: 'all 0.2s ease-in-out',
425
- cursor: 'crosshair',
426
- zIndex: 10,
427
- }}
428
- />
429
- {/* Left - Target (hidden but functional) */}
430
- <Handle
431
- type="target"
432
- position={Position.Left}
433
- id="left-target"
434
- style={{
435
- background: 'transparent',
436
- width: '14px',
437
- height: '14px',
438
- border: 'none',
439
- left: '-8px',
440
- opacity: 0,
441
- pointerEvents: selected ? 'all' : 'none',
442
- }}
443
- />
444
- {/* Right - Source */}
445
- <Handle
446
- type="source"
447
- position={Position.Right}
448
- id="right-source"
449
- className="connection-handle"
450
- style={{
451
- background: selected ? '#10B981' : '#1a1a2e',
452
- width: '14px',
453
- height: '14px',
454
- border: '3px solid #10B981',
455
- right: '-8px',
456
- opacity: selected ? 1 : 0,
457
- transition: 'all 0.2s ease-in-out',
458
- cursor: 'crosshair',
459
- zIndex: 10,
460
- }}
461
- />
462
- {/* Right - Target (hidden but functional) */}
463
- <Handle
464
- type="target"
465
- position={Position.Right}
466
- id="right-target"
467
- style={{
468
- background: 'transparent',
469
- width: '14px',
470
- height: '14px',
471
- border: 'none',
472
- right: '-8px',
473
- opacity: 0,
474
- pointerEvents: selected ? 'all' : 'none',
475
- }}
476
- />
477
-
478
- </Box>
479
-
480
- {/* Node Action Buttons - Shows when selected */}
481
- <NodeActionButtons
482
- selected={selected}
483
- onDelete={() => {
484
- if (nodeId && onNodesChange) {
485
- onNodesChange([{ id: nodeId, type: 'remove' }]);
486
- }
487
- }}
488
- onDuplicate={() => {
489
- if (nodeId) {
490
- const currentNode = nodes.find(n => n.id === nodeId);
491
- if (currentNode) {
492
- const newNode = {
493
- ...currentNode,
494
- id: `${currentNode.id}-copy-${Date.now()}`,
495
- position: {
496
- x: currentNode.position.x + 50,
497
- y: currentNode.position.y + 50,
498
- },
499
- selected: false,
500
- };
501
- setNodes([...nodes, newNode]);
502
- }
503
- }
504
- }}
505
- />
506
-
507
- {/* AI Suggestions Button - Positioned below the node box */}
508
- {data.formData?.aiSuggestionsCount !== undefined && data.formData.aiSuggestionsCount > 0 && (
509
- <Box
510
- sx={{
511
- position: 'absolute',
512
- top: '100%',
513
- left: '50%',
514
- transform: 'translateX(-50%)',
515
- marginTop: '12px',
516
- zIndex: 10,
517
- whiteSpace: 'nowrap',
518
- }}
519
- onClick={(e) => {
520
- e.stopPropagation();
521
- // Toggle AI Suggestions panel
522
- setShowSuggestions(!showSuggestions);
523
- }}
524
- >
525
- <Button
526
- variant="contained"
527
- startIcon={<LightbulbIcon sx={{ fontSize: '12px' }} />}
528
- sx={{
529
- backgroundColor: '#2563EB',
530
- color: '#ffffff',
531
- borderRadius: '20px',
532
- textTransform: 'none',
533
- fontSize: '10px',
534
- fontWeight: 400,
535
- padding: '8px 16px',
536
- whiteSpace: 'nowrap',
537
- display: 'inline-flex',
538
- alignItems: 'center',
539
- boxShadow: '0 2px 8px rgba(0, 0, 0, 0.3)',
540
- '&:hover': {
541
- backgroundColor: '#2563eb',
542
- },
543
- '& .MuiButton-startIcon': {
544
- marginRight: '8px',
545
- }
546
- }}
547
- >
548
- AI Suggestions
549
- <Box
550
- component="span"
551
- sx={{
552
- marginLeft: '8px',
553
- backgroundColor: '#FFFFFF26',
554
- color: '#ffffff',
555
- fontSize: '10px',
556
- fontWeight: 400,
557
- minWidth: '18px',
558
- height: '18px',
559
- borderRadius: '9px',
560
- display: 'inline-flex',
561
- alignItems: 'center',
562
- justifyContent: 'center',
563
- padding: '0 6px',
564
- border: '1px solid rgba(255, 255, 255, 0.2)',
565
- }}
566
- >
567
- {data.formData.aiSuggestionsCount}
568
- </Box>
569
- </Button>
570
- </Box>
571
- )}
572
-
573
- {/* AI Suggestions Panel - Rendered on canvas below the button */}
574
- {showSuggestions && data.formData?.aiSuggestionsCount !== undefined && data.formData.aiSuggestionsCount > 0 && nodeId && (
575
- <AISuggestionsPanel
576
- suggestions={data.formData?.aiSuggestions || [
577
- {
578
- id: '1',
579
- title: 'Add Citation Extraction',
580
- description: 'Automatically extract and format citations from article content.',
581
- tags: ['classification', 'enhancement'],
582
- },
583
- {
584
- id: '2',
585
- title: 'Generate Bullet Summary',
586
- description: 'Create a concise bullet-point summary of the article\'s main points.',
587
- tags: ['classification', 'enhancement'],
588
- },
589
- ]}
590
- parentNodeId={nodeId}
591
- onSuggestionClick={(suggestion) => {
592
- console.log('Suggestion clicked:', suggestion);
593
- // Handle suggestion selection here
594
- }}
595
- onClose={() => setShowSuggestions(false)}
596
- />
597
- )}
598
- </Box>
599
- );
600
- };
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, Button } from '@mui/material';
5
+ import { AccessTime as AccessTimeIcon, Visibility as VisibilityIcon, Save as SaveIcon, Send as SendIcon, Lightbulb as LightbulbIcon } 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 { AISuggestion } from './AISuggestionsModal';
12
+ import { AISuggestionsPanel } from './AISuggestionsPanel';
13
+ import { NodeActionButtons } from './NodeActionButtons';
14
+ import { showNodeAIAssistantPopup } from './NodeAIAssistantPopup';
15
+ import { useSearch } from '../../contexts/SearchContext';
16
+
17
+ interface AutomationEndNodeProps {
18
+ data: {
19
+ label: string;
20
+ description: string;
21
+ status: 'Ready' | 'Running' | 'Completed' | 'Error';
22
+ outputType: 'display' | 'store' | 'send';
23
+ outputConfig?: {
24
+ format: 'table' | 'json' | 'csv';
25
+ destination?: string;
26
+ showInConsole: boolean;
27
+ showInUI: boolean;
28
+ };
29
+ lastRun: string;
30
+ backgroundColor: string;
31
+ textColor: string;
32
+ borderColor: string;
33
+ iconName?: string; // Add iconName to the interface
34
+ formData?: {
35
+ aiSuggestionsCount?: number; // Number of AI suggestions available
36
+ [key: string]: any;
37
+ }; // Include formData for configuration
38
+ };
39
+ selected?: boolean;
40
+ }
41
+
42
+ export const AutomationEndNode: React.FC<AutomationEndNodeProps> = ({ data, selected }) => {
43
+ const { t } = useTranslation();
44
+ const { highlightText } = useSearch();
45
+ const [isJsonOpen, setIsJsonOpen] = useState(false);
46
+ const [showSuggestions, setShowSuggestions] = useState(false);
47
+ const rootRef = useRef<any>(null);
48
+ const portalRef = useRef<HTMLDivElement | null>(null);
49
+ const nodeRef = useRef<HTMLDivElement | null>(null);
50
+ const nodeId = useNodeId();
51
+ const setSelectedNode = useDiagram((state) => state.setSelectedNode);
52
+ const enableJson = useDiagram((state) => state.enableNodeJsonPopover ?? true);
53
+ const onNodesChange = useDiagram((state) => state.onNodesChange);
54
+ const nodes = useDiagram((state) => state.nodes);
55
+ const setNodes = useDiagram((state) => state.setNodes);
56
+
57
+ // Get the icon component based on the iconName
58
+ const IconComponent = getIconByName(data.iconName);
59
+
60
+ const handleJsonClick = () => {
61
+ if (nodeId) setSelectedNode(nodeId);
62
+ if (!enableJson) return;
63
+ setIsJsonOpen(!isJsonOpen);
64
+ };
65
+
66
+ const handleClose = () => {
67
+ setIsJsonOpen(false);
68
+ // Clean up portal
69
+ if (rootRef.current) {
70
+ rootRef.current.unmount();
71
+ rootRef.current = null;
72
+ }
73
+ if (portalRef.current) {
74
+ document.body.removeChild(portalRef.current);
75
+ portalRef.current = null;
76
+ }
77
+ };
78
+
79
+ useEffect(() => {
80
+ const handleClickOutside = (event: MouseEvent) => {
81
+ if (isJsonOpen && !(event.target as Element).closest('#automation-json-popover')) {
82
+ handleClose();
83
+ }
84
+ };
85
+ document.addEventListener('mousedown', handleClickOutside);
86
+ return () => {
87
+ document.removeEventListener('mousedown', handleClickOutside);
88
+ };
89
+ }, [isJsonOpen]);
90
+
91
+ // Debug logging for node dimensions
92
+ useEffect(() => {
93
+ if (nodeRef.current) {
94
+ const rect = nodeRef.current.getBoundingClientRect();
95
+ const computedStyle = window.getComputedStyle(nodeRef.current);
96
+ // Debug information available but not logged
97
+ }
98
+ }, [data.label]);
99
+
100
+ useEffect(() => {
101
+ if (isJsonOpen) {
102
+ const portalRoot = document.createElement('div');
103
+ document.body.appendChild(portalRoot);
104
+ portalRef.current = portalRoot;
105
+
106
+ const root = createRoot(portalRoot);
107
+ rootRef.current = root;
108
+
109
+ root.render(
110
+ <Card
111
+ id="automation-json-popover"
112
+ sx={{
113
+ position: 'fixed',
114
+ top: 0,
115
+ right: 0,
116
+ zIndex: 9999,
117
+ width: '400px',
118
+ height: '100vh',
119
+ overflow: 'auto',
120
+ bgcolor: '#242424',
121
+ color: '#fff',
122
+ border: '1px solid #333',
123
+ '&::-webkit-scrollbar': {
124
+ width: '6px',
125
+ },
126
+ '&::-webkit-scrollbar-track': {
127
+ background: 'transparent',
128
+ },
129
+ '&::-webkit-scrollbar-thumb': {
130
+ background: '#444',
131
+ borderRadius: '3px',
132
+ '&:hover': {
133
+ background: '#666',
134
+ },
135
+ },
136
+ }}
137
+ >
138
+ <CardContent sx={{ bgcolor: '#242424', color: '#fff' }}>
139
+ <IconButton
140
+ aria-label="close"
141
+ onClick={handleClose}
142
+ sx={{
143
+ color: '#999',
144
+ '&:hover': {
145
+ color: '#fff',
146
+ bgcolor: 'rgba(255, 255, 255, 0.1)',
147
+ },
148
+ }}
149
+ >
150
+ <RiCloseLine />
151
+ </IconButton>
152
+ {/* Show execution result prominently if available */}
153
+ {data.formData?.executionResult && (
154
+ <Box sx={{ mb: 2 }}>
155
+ <Typography variant="h6" sx={{ color: '#fff', mb: 1 }}>
156
+ {t('automation.common.executionResult')}
157
+ </Typography>
158
+ <Box sx={{
159
+ bgcolor: data.formData.executionResult.success ? '#1e3a8a' : '#dc2626',
160
+ p: 1,
161
+ borderRadius: 1,
162
+ mb: 1
163
+ }}>
164
+ <Typography variant="body2" sx={{ color: '#fff' }}>
165
+ {t('automation.common.status')}: {data.formData.executionResult.success ? t('automation.common.success') : t('automation.common.failed')}
166
+ </Typography>
167
+ <Typography variant="body2" sx={{ color: '#fff' }}>
168
+ {t('automation.common.timestamp')}: {new Date(data.formData.executionResult.timestamp).toLocaleString()}
169
+ </Typography>
170
+ {data.formData.executionResult.error && (
171
+ <Typography variant="body2" sx={{ color: '#fff' }}>
172
+ {t('automation.common.error')}: {data.formData.executionResult.error}
173
+ </Typography>
174
+ )}
175
+ </Box>
176
+ <Typography variant="h6" sx={{ color: '#fff', mb: 1 }}>
177
+ {t('automation.endNode.finalOutput')}
178
+ </Typography>
179
+ <ReactJson
180
+ theme={'monokai'}
181
+ src={data.formData.executionResult.data}
182
+ collapsed={false}
183
+ />
184
+ </Box>
185
+ )}
186
+
187
+ {/* Show full node data */}
188
+ <Typography variant="h6" sx={{ color: '#fff', mb: 1 }}>
189
+ {t('automation.common.fullNodeData')}
190
+ </Typography>
191
+ <ReactJson theme={'monokai'} src={data.formData || data} collapsed={false} />
192
+ </CardContent>
193
+ </Card>
194
+ );
195
+ } else {
196
+ // Clean up when closing
197
+ if (rootRef.current) {
198
+ rootRef.current.unmount();
199
+ rootRef.current = null;
200
+ }
201
+ if (portalRef.current) {
202
+ document.body.removeChild(portalRef.current);
203
+ portalRef.current = null;
204
+ }
205
+ }
206
+ }, [isJsonOpen, data]);
207
+
208
+ return (
209
+ <Box
210
+ sx={{
211
+ position: 'relative',
212
+ width: '336px',
213
+ overflow: 'visible',
214
+ }}
215
+ >
216
+ <Box
217
+ ref={nodeRef}
218
+ sx={{
219
+ width: '336px',
220
+ minHeight: '150px',
221
+ backgroundColor: '#181C25', // New background color from image
222
+ border: selected ? '2px solid #3b82f6' : '1px solid #1e293b',
223
+ borderRadius: '12px',
224
+ color: '#ffffff',
225
+ position: 'relative',
226
+ boxShadow: selected ? '0 0 0 2px rgba(59, 130, 246, 0.5)' : '0 4px 8px rgba(0, 0, 0, 0.3)',
227
+ transition: 'all 0.2s ease',
228
+ cursor: 'pointer',
229
+ overflow: 'hidden',
230
+ ...(data.status === 'Running' && {
231
+ animation: 'pulse-glow 2s ease-in-out infinite',
232
+ '@keyframes pulse-glow': {
233
+ '0%, 100%': {
234
+ boxShadow: '0 0 20px rgba(59, 130, 246, 0.4), 0 0 40px rgba(59, 130, 246, 0.2)',
235
+ borderColor: 'rgba(59, 130, 246, 0.6)',
236
+ },
237
+ '50%': {
238
+ boxShadow: '0 0 30px rgba(59, 130, 246, 0.6), 0 0 60px rgba(59, 130, 246, 0.3)',
239
+ borderColor: 'rgba(59, 130, 246, 0.9)',
240
+ },
241
+ },
242
+ }),
243
+ }}
244
+ onClick={handleJsonClick}
245
+ >
246
+ {/* Top Header Section */}
247
+ <Box sx={{
248
+ backgroundColor: "rgba(67, 93, 132, 0.1)",
249
+ padding: '8px 16px',
250
+ borderRadius: '12px 12px 0 0'
251
+ }}>
252
+ <Typography variant="body2" sx={{
253
+ color: '#ffffff',
254
+ fontSize: '12px',
255
+ fontWeight: 500
256
+ }}>
257
+ {data.formData?.description || t('automation.endNode.headerDescription')}
258
+ </Typography>
259
+ </Box>
260
+
261
+ {/* Main Content */}
262
+ <Box sx={{ padding: '16px' }}>
263
+ {/* Title Section */}
264
+ <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
265
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
266
+ <Box
267
+ sx={{
268
+ width: '32px',
269
+ height: '32px',
270
+ backgroundColor: '#0ea5e9', // Blue-green color like in image
271
+ borderRadius: '50%',
272
+ display: 'flex',
273
+ alignItems: 'center',
274
+ justifyContent: 'center',
275
+ }}
276
+ >
277
+ <IconComponent sx={{ color: 'white', fontSize: '18px' }} />
278
+ </Box>
279
+ <Typography variant="h6" sx={{ fontWeight: 600, fontSize: '16px' }}>
280
+ {highlightText(data.label)}
281
+ </Typography>
282
+ </Box>
283
+ <Chip
284
+ label={data.status || 'Ready'}
285
+ size="small"
286
+ sx={{
287
+ backgroundColor: data.status === 'Completed'
288
+ ? 'rgba(37, 99, 235, 0.1)'
289
+ : data.status === 'Running'
290
+ ? 'rgba(251, 191, 36, 0.1)'
291
+ : data.status === 'Error'
292
+ ? 'rgba(239, 68, 68, 0.1)'
293
+ : 'rgba(16, 185, 129, 0.1)',
294
+ color: data.status === 'Completed'
295
+ ? '#93C5FD'
296
+ : data.status === 'Running'
297
+ ? '#FCD34D'
298
+ : data.status === 'Error'
299
+ ? '#FCA5A5'
300
+ : '#86EFAC',
301
+ fontWeight: 500,
302
+ fontSize: '12px',
303
+ height: '24px',
304
+ borderRadius: '12px',
305
+ }}
306
+ />
307
+ </Box>
308
+
309
+ {/* Description Box */}
310
+ <Box sx={{
311
+ backgroundColor: '#1F2937',
312
+ borderRadius: '8px',
313
+ padding: '12px',
314
+ mb: 2,
315
+ border: '1px solid #374151'
316
+ }}>
317
+ {/* Inner text boundary box */}
318
+ <Box sx={{
319
+ backgroundColor: 'transparent',
320
+ borderRadius: '4px',
321
+ padding: '8px',
322
+ border: '1px solid #4B5563', // Light grey border for inner box
323
+ minHeight: '40px',
324
+ display: 'flex',
325
+ alignItems: 'center'
326
+ }}>
327
+ <Typography variant="body2" sx={{
328
+ color: '#9CA3AF',
329
+ fontSize: '12px',
330
+ lineHeight: 1.4,
331
+ margin: 0
332
+ }}>
333
+ {data.description}
334
+ </Typography>
335
+ </Box>
336
+ </Box>
337
+
338
+ {/* Last Run Info */}
339
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mt: 1 }}>
340
+ <AccessTimeIcon sx={{ fontSize: '14px', color: '#9CA3AF' }} />
341
+ <Typography variant="body2" sx={{ color: '#9CA3AF', fontSize: '11px' }}>
342
+ {t('automation.common.lastRan')}: {data.lastRun}
343
+ </Typography>
344
+ </Box>
345
+ </Box>
346
+
347
+ {/* Connection Handles - Bidirectional (source + target at each position) */}
348
+ {/* Top - Source */}
349
+ <Handle
350
+ type="source"
351
+ position={Position.Top}
352
+ id="top-source"
353
+ className="connection-handle"
354
+ style={{
355
+ background: selected ? '#10B981' : '#1a1a2e',
356
+ width: '14px',
357
+ height: '14px',
358
+ border: '3px solid #10B981',
359
+ top: '-8px',
360
+ opacity: selected ? 1 : 0,
361
+ transition: 'all 0.2s ease-in-out',
362
+ cursor: 'crosshair',
363
+ zIndex: 10,
364
+ }}
365
+ />
366
+ {/* Top - Target (hidden but functional) */}
367
+ <Handle
368
+ type="target"
369
+ position={Position.Top}
370
+ id="top-target"
371
+ style={{
372
+ background: 'transparent',
373
+ width: '14px',
374
+ height: '14px',
375
+ border: 'none',
376
+ top: '-8px',
377
+ opacity: 0,
378
+ pointerEvents: selected ? 'all' : 'none',
379
+ }}
380
+ />
381
+ {/* Bottom - Source */}
382
+ <Handle
383
+ type="source"
384
+ position={Position.Bottom}
385
+ id="bottom-source"
386
+ className="connection-handle"
387
+ style={{
388
+ background: selected ? '#10B981' : '#1a1a2e',
389
+ width: '14px',
390
+ height: '14px',
391
+ border: '3px solid #10B981',
392
+ bottom: '-8px',
393
+ opacity: selected ? 1 : 0,
394
+ transition: 'all 0.2s ease-in-out',
395
+ cursor: 'crosshair',
396
+ zIndex: 10,
397
+ }}
398
+ />
399
+ {/* Bottom - Target (hidden but functional) */}
400
+ <Handle
401
+ type="target"
402
+ position={Position.Bottom}
403
+ id="bottom-target"
404
+ style={{
405
+ background: 'transparent',
406
+ width: '14px',
407
+ height: '14px',
408
+ border: 'none',
409
+ bottom: '-8px',
410
+ opacity: 0,
411
+ pointerEvents: selected ? 'all' : 'none',
412
+ }}
413
+ />
414
+ {/* Left - Source */}
415
+ <Handle
416
+ type="source"
417
+ position={Position.Left}
418
+ id="left-source"
419
+ className="connection-handle"
420
+ style={{
421
+ background: selected ? '#10B981' : '#1a1a2e',
422
+ width: '14px',
423
+ height: '14px',
424
+ border: '3px solid #10B981',
425
+ left: '-8px',
426
+ opacity: selected ? 1 : 0,
427
+ transition: 'all 0.2s ease-in-out',
428
+ cursor: 'crosshair',
429
+ zIndex: 10,
430
+ }}
431
+ />
432
+ {/* Left - Target (hidden but functional) */}
433
+ <Handle
434
+ type="target"
435
+ position={Position.Left}
436
+ id="left-target"
437
+ style={{
438
+ background: 'transparent',
439
+ width: '14px',
440
+ height: '14px',
441
+ border: 'none',
442
+ left: '-8px',
443
+ opacity: 0,
444
+ pointerEvents: selected ? 'all' : 'none',
445
+ }}
446
+ />
447
+ {/* Right - Source */}
448
+ <Handle
449
+ type="source"
450
+ position={Position.Right}
451
+ id="right-source"
452
+ className="connection-handle"
453
+ style={{
454
+ background: selected ? '#10B981' : '#1a1a2e',
455
+ width: '14px',
456
+ height: '14px',
457
+ border: '3px solid #10B981',
458
+ right: '-8px',
459
+ opacity: selected ? 1 : 0,
460
+ transition: 'all 0.2s ease-in-out',
461
+ cursor: 'crosshair',
462
+ zIndex: 10,
463
+ }}
464
+ />
465
+ {/* Right - Target (hidden but functional) */}
466
+ <Handle
467
+ type="target"
468
+ position={Position.Right}
469
+ id="right-target"
470
+ style={{
471
+ background: 'transparent',
472
+ width: '14px',
473
+ height: '14px',
474
+ border: 'none',
475
+ right: '-8px',
476
+ opacity: 0,
477
+ pointerEvents: selected ? 'all' : 'none',
478
+ }}
479
+ />
480
+
481
+ </Box>
482
+
483
+ {/* Node Action Buttons - Shows when selected */}
484
+ <NodeActionButtons
485
+ selected={selected}
486
+ onOpenAIAssistant={(buttonElement) => {
487
+ if (nodeId) {
488
+ showNodeAIAssistantPopup(nodeId, 'End Node', buttonElement);
489
+ }
490
+ }}
491
+ onDelete={() => {
492
+ if (nodeId && onNodesChange) {
493
+ onNodesChange([{ id: nodeId, type: 'remove' }]);
494
+ }
495
+ }}
496
+ onDuplicate={() => {
497
+ if (nodeId) {
498
+ const currentNode = nodes.find(n => n.id === nodeId);
499
+ if (currentNode) {
500
+ const newNode = {
501
+ ...currentNode,
502
+ id: `${currentNode.id}-copy-${Date.now()}`,
503
+ position: {
504
+ x: currentNode.position.x + 50,
505
+ y: currentNode.position.y + 50,
506
+ },
507
+ selected: false,
508
+ };
509
+ setNodes([...nodes, newNode]);
510
+ }
511
+ }
512
+ }}
513
+ />
514
+
515
+ {/* AI Suggestions Button - Positioned below the node box */}
516
+ {data.formData?.aiSuggestionsCount !== undefined && data.formData.aiSuggestionsCount > 0 && (
517
+ <Box
518
+ sx={{
519
+ position: 'absolute',
520
+ top: '100%',
521
+ left: '50%',
522
+ transform: 'translateX(-50%)',
523
+ marginTop: '12px',
524
+ zIndex: 10,
525
+ whiteSpace: 'nowrap',
526
+ }}
527
+ onClick={(e) => {
528
+ e.stopPropagation();
529
+ // Toggle AI Suggestions panel
530
+ setShowSuggestions(!showSuggestions);
531
+ }}
532
+ >
533
+ <Button
534
+ variant="contained"
535
+ startIcon={<LightbulbIcon sx={{ fontSize: '12px' }} />}
536
+ sx={{
537
+ backgroundColor: '#2563EB',
538
+ color: '#ffffff',
539
+ borderRadius: '20px',
540
+ textTransform: 'none',
541
+ fontSize: '10px',
542
+ fontWeight: 400,
543
+ padding: '8px 16px',
544
+ whiteSpace: 'nowrap',
545
+ display: 'inline-flex',
546
+ alignItems: 'center',
547
+ boxShadow: '0 2px 8px rgba(0, 0, 0, 0.3)',
548
+ '&:hover': {
549
+ backgroundColor: '#2563eb',
550
+ },
551
+ '& .MuiButton-startIcon': {
552
+ marginRight: '8px',
553
+ }
554
+ }}
555
+ >
556
+ AI Suggestions
557
+ <Box
558
+ component="span"
559
+ sx={{
560
+ marginLeft: '8px',
561
+ backgroundColor: '#FFFFFF26',
562
+ color: '#ffffff',
563
+ fontSize: '10px',
564
+ fontWeight: 400,
565
+ minWidth: '18px',
566
+ height: '18px',
567
+ borderRadius: '9px',
568
+ display: 'inline-flex',
569
+ alignItems: 'center',
570
+ justifyContent: 'center',
571
+ padding: '0 6px',
572
+ border: '1px solid rgba(255, 255, 255, 0.2)',
573
+ }}
574
+ >
575
+ {data.formData.aiSuggestionsCount}
576
+ </Box>
577
+ </Button>
578
+ </Box>
579
+ )}
580
+
581
+ {/* AI Suggestions Panel - Rendered on canvas below the button */}
582
+ {showSuggestions && data.formData?.aiSuggestionsCount !== undefined && data.formData.aiSuggestionsCount > 0 && nodeId && (
583
+ <AISuggestionsPanel
584
+ suggestions={data.formData?.aiSuggestions || [
585
+ {
586
+ id: '1',
587
+ title: 'Add Citation Extraction',
588
+ description: 'Automatically extract and format citations from article content.',
589
+ tags: ['classification', 'enhancement'],
590
+ },
591
+ {
592
+ id: '2',
593
+ title: 'Generate Bullet Summary',
594
+ description: 'Create a concise bullet-point summary of the article\'s main points.',
595
+ tags: ['classification', 'enhancement'],
596
+ },
597
+ ]}
598
+ parentNodeId={nodeId}
599
+ onSuggestionClick={(suggestion) => {
600
+ console.log('Suggestion clicked:', suggestion);
601
+ // Handle suggestion selection here
602
+ }}
603
+ onClose={() => setShowSuggestions(false)}
604
+ />
605
+ )}
606
+ </Box>
607
+ );
608
+ };