@flowuent-org/diagramming-core 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,831 +1,825 @@
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 {
6
- Psychology as PsychologyIcon,
7
- AccessTime as AccessTimeIcon,
8
- Visibility as VisibilityIcon,
9
- FilterList as FilterIcon,
10
- Send as SendIcon,
11
- Settings as SettingsIcon,
12
- Storage as StorageIcon,
13
- Lightbulb as LightbulbIcon
14
- } from '@mui/icons-material';
15
- import { RiCloseLine, RiUser2Line } from 'react-icons/ri';
16
- import ReactJson from 'react-json-view';
17
- import { getIconByName } from '../../utils/iconMapper';
18
- import { useTranslation } from 'react-i18next';
19
- import { useDiagram } from '../../contexts/DiagramProvider';
20
- import { AISuggestion } from './AISuggestionsModal';
21
- import { AISuggestionsPanel } from './AISuggestionsPanel';
22
- import { NodeActionButtons } from './NodeActionButtons';
23
- import { showNodeAIAssistantPopup } from './NodeAIAssistantPopup';
24
-
25
- interface AutomationFormattingNodeProps {
26
- data: {
27
- label: string;
28
- description: string;
29
- status: 'Ready' | 'Running' | 'Completed' | 'Error';
30
- inputVariable: string;
31
- formattingType: 'basic' | 'ai-powered';
32
- aiConfig?: {
33
- apiUrl: string;
34
- model: string;
35
- instruction: string;
36
- temperature: number;
37
- };
38
- outputFormat: string;
39
- outputVariable: string;
40
- lastRun: string;
41
- backgroundColor: string;
42
- textColor: string;
43
- borderColor: string;
44
- iconName?: string; // Add iconName to the interface
45
- formData?: {
46
- schemaMatch?: {
47
- matched: number;
48
- partial: number;
49
- ignored: number;
50
- };
51
- aiReasoning?: {
52
- text: string;
53
- };
54
- configurationCards?: Array<{
55
- icon: string;
56
- label: string;
57
- iconColor: string;
58
- }>;
59
- aiSuggestionsCount?: number; // Number of AI suggestions available
60
- [key: string]: any;
61
- }; // Include formData for configuration
62
- };
63
- selected?: boolean;
64
- }
65
-
66
- export const AutomationFormattingNode: React.FC<AutomationFormattingNodeProps> = ({ data, selected }) => {
67
- const { t } = useTranslation();
68
- const [isJsonOpen, setIsJsonOpen] = useState(false);
69
- const [showSuggestions, setShowSuggestions] = useState(false);
70
- const rootRef = useRef<any>(null);
71
- const portalRef = useRef<HTMLDivElement | null>(null);
72
- const nodeRef = useRef<HTMLDivElement | null>(null);
73
- const nodeId = useNodeId();
74
- const setSelectedNode = useDiagram((state) => state.setSelectedNode);
75
- const enableJson = useDiagram((state) => state.enableNodeJsonPopover ?? true);
76
- const onNodesChange = useDiagram((state) => state.onNodesChange);
77
- const nodes = useDiagram((state) => state.nodes);
78
- const setNodes = useDiagram((state) => state.setNodes);
79
-
80
- // Get the icon component based on the iconName
81
- const IconComponent = getIconByName(data.iconName);
82
-
83
- const handleJsonClick = () => {
84
- if (nodeId) setSelectedNode(nodeId);
85
- if (!enableJson) return;
86
- setIsJsonOpen(!isJsonOpen);
87
- };
88
-
89
- const handleClose = () => {
90
- setIsJsonOpen(false);
91
- // Clean up portal
92
- if (rootRef.current) {
93
- rootRef.current.unmount();
94
- rootRef.current = null;
95
- }
96
- if (portalRef.current) {
97
- document.body.removeChild(portalRef.current);
98
- portalRef.current = null;
99
- }
100
- };
101
-
102
- useEffect(() => {
103
- const handleClickOutside = (event: MouseEvent) => {
104
- if (isJsonOpen && !(event.target as Element).closest('#automation-json-popover')) {
105
- handleClose();
106
- }
107
- };
108
- document.addEventListener('mousedown', handleClickOutside);
109
- return () => {
110
- document.removeEventListener('mousedown', handleClickOutside);
111
- };
112
- }, [isJsonOpen]);
113
-
114
- // Debug logging for node dimensions
115
- useEffect(() => {
116
- if (nodeRef.current) {
117
- const rect = nodeRef.current.getBoundingClientRect();
118
- const computedStyle = window.getComputedStyle(nodeRef.current);
119
- // Debug information available but not logged
120
- }
121
- }, [data.label]);
122
-
123
- useEffect(() => {
124
- if (isJsonOpen) {
125
- const portalRoot = document.createElement('div');
126
- document.body.appendChild(portalRoot);
127
- portalRef.current = portalRoot;
128
-
129
- const root = createRoot(portalRoot);
130
- rootRef.current = root;
131
-
132
- root.render(
133
- <Card
134
- id="automation-json-popover"
135
- sx={{
136
- position: 'fixed',
137
- top: 0,
138
- right: 0,
139
- zIndex: 9999,
140
- width: '400px',
141
- height: '100vh',
142
- overflow: 'auto',
143
- bgcolor: '#242424',
144
- color: '#fff',
145
- border: '1px solid #333',
146
- '&::-webkit-scrollbar': {
147
- width: '6px',
148
- },
149
- '&::-webkit-scrollbar-track': {
150
- background: 'transparent',
151
- },
152
- '&::-webkit-scrollbar-thumb': {
153
- background: '#444',
154
- borderRadius: '3px',
155
- '&:hover': {
156
- background: '#666',
157
- },
158
- },
159
- }}
160
- >
161
- <CardContent sx={{ bgcolor: '#242424', color: '#fff' }}>
162
- <IconButton
163
- aria-label="close"
164
- onClick={handleClose}
165
- sx={{
166
- color: '#999',
167
- '&:hover': {
168
- color: '#fff',
169
- bgcolor: 'rgba(255, 255, 255, 0.1)',
170
- },
171
- }}
172
- >
173
- <RiCloseLine />
174
- </IconButton>
175
- {/* Show execution result prominently if available */}
176
- {data.formData?.executionResult && (
177
- <Box sx={{ mb: 2 }}>
178
- <Typography variant="h6" sx={{ color: '#fff', mb: 1 }}>
179
- {t('automation.common.executionResult')}
180
- </Typography>
181
- <Box sx={{
182
- bgcolor: data.formData.executionResult.success ? '#1e3a8a' : '#dc2626',
183
- p: 1,
184
- borderRadius: 1,
185
- mb: 1
186
- }}>
187
- <Typography variant="body2" sx={{ color: '#fff' }}>
188
- {t('automation.common.status')}: {data.formData.executionResult.success ? t('automation.common.success') : t('automation.common.failed')}
189
- </Typography>
190
- <Typography variant="body2" sx={{ color: '#fff' }}>
191
- {t('automation.common.timestamp')}: {new Date(data.formData.executionResult.timestamp).toLocaleString()}
192
- </Typography>
193
- {data.formData.executionResult.error && (
194
- <Typography variant="body2" sx={{ color: '#fff' }}>
195
- {t('automation.common.error')}: {data.formData.executionResult.error}
196
- </Typography>
197
- )}
198
- </Box>
199
- <Typography variant="h6" sx={{ color: '#fff', mb: 1 }}>
200
- {t('automation.formattingNode.formattedData')}
201
- </Typography>
202
- <ReactJson
203
- theme={'monokai'}
204
- src={data.formData.executionResult.data}
205
- collapsed={false}
206
- />
207
- </Box>
208
- )}
209
-
210
- {/* Show full node data */}
211
- <Typography variant="h6" sx={{ color: '#fff', mb: 1 }}>
212
- {t('automation.common.fullNodeData')}
213
- </Typography>
214
- <ReactJson theme={'monokai'} src={data.formData || data} collapsed={false} />
215
- </CardContent>
216
- </Card>
217
- );
218
- } else {
219
- // Clean up when closing
220
- if (rootRef.current) {
221
- rootRef.current.unmount();
222
- rootRef.current = null;
223
- }
224
- if (portalRef.current) {
225
- document.body.removeChild(portalRef.current);
226
- portalRef.current = null;
227
- }
228
- }
229
- }, [isJsonOpen, data]);
230
-
231
- // Card component for consistent styling
232
- const ProcessCard = ({ icon, label, description, iconColor }: {
233
- icon: React.ReactNode;
234
- label: string;
235
- description: string;
236
- iconColor: string;
237
- }) => (
238
- <Box sx={{
239
- backgroundColor: '#1e293b',
240
- borderRadius: '8px',
241
- padding: '12px',
242
- mb: 1,
243
- border: '1px solid #334155',
244
- display: 'flex',
245
- alignItems: 'center',
246
- gap: 2,
247
- }}>
248
- <Box
249
- sx={{
250
- width: '24px',
251
- height: '24px',
252
- display: 'flex',
253
- alignItems: 'center',
254
- justifyContent: 'center',
255
- flexShrink: 0,
256
- }}
257
- >
258
- {icon}
259
- </Box>
260
- <Box sx={{ flex: 1 }}>
261
- <Typography variant="body2" sx={{
262
- color: iconColor,
263
- fontSize: '12px',
264
- fontWeight: 600,
265
- mb: 0.5
266
- }}>
267
- {label}:
268
- </Typography>
269
- <Typography variant="body2" sx={{
270
- color: '#ffffff',
271
- fontSize: '11px',
272
- lineHeight: 1.3
273
- }}>
274
- {description}
275
- </Typography>
276
- </Box>
277
- </Box>
278
- );
279
-
280
- return (
281
- <Box
282
- sx={{
283
- position: 'relative',
284
- width: '336px',
285
- overflow: 'visible',
286
- }}
287
- >
288
- <Box
289
- ref={nodeRef}
290
- sx={{
291
- width: '336px',
292
- minHeight: '150px',
293
- backgroundColor: '#181C25', // New background color from image
294
- border: selected ? '2px solid #3b82f6' : '1px solid #1e293b',
295
- borderRadius: '12px',
296
- color: '#ffffff',
297
- position: 'relative',
298
- boxShadow: selected ? '0 0 0 2px rgba(59, 130, 246, 0.5)' : '0 4px 8px rgba(0, 0, 0, 0.3)',
299
- transition: 'all 0.2s ease',
300
- cursor: 'pointer',
301
- overflow: 'hidden',
302
- ...(data.status === 'Running' && {
303
- animation: 'pulse-glow 2s ease-in-out infinite',
304
- '@keyframes pulse-glow': {
305
- '0%, 100%': {
306
- boxShadow: '0 0 20px rgba(59, 130, 246, 0.4), 0 0 40px rgba(59, 130, 246, 0.2)',
307
- borderColor: 'rgba(59, 130, 246, 0.6)',
308
- },
309
- '50%': {
310
- boxShadow: '0 0 30px rgba(59, 130, 246, 0.6), 0 0 60px rgba(59, 130, 246, 0.3)',
311
- borderColor: 'rgba(59, 130, 246, 0.9)',
312
- },
313
- },
314
- }),
315
- }}
316
- onClick={handleJsonClick}
317
- >
318
- {/* Top Header Section */}
319
- <Box sx={{
320
- backgroundColor: "rgba(67, 93, 132, 0.1)",
321
- padding: '8px 16px',
322
- borderRadius: '12px 12px 0 0'
323
- }}>
324
- <Typography variant="body2" sx={{
325
- color: '#ffffff',
326
- fontSize: '12px',
327
- fontWeight: 500
328
- }}>
329
- {data.formData?.description || t('automation.formattingNode.headerDescription')}
330
- </Typography>
331
- </Box>
332
-
333
- {/* Main Content */}
334
- <Box sx={{ padding: '16px' }}>
335
- {/* Title Section */}
336
- <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
337
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
338
- <Box
339
- sx={{
340
- width: '32px',
341
- height: '32px',
342
- backgroundColor: '#8b5cf6', // Purple like in image
343
- borderRadius: '50%',
344
- display: 'flex',
345
- alignItems: 'center',
346
- justifyContent: 'center',
347
- }}
348
- >
349
- <IconComponent sx={{ color: 'white', fontSize: '18px' }} />
350
- </Box>
351
- <Typography variant="h6" sx={{ fontWeight: 600, fontSize: '16px' }}>
352
- {data.label}
353
- </Typography>
354
- </Box>
355
- <Chip
356
- label={data.status || 'Ready'}
357
- size="small"
358
- sx={{
359
- backgroundColor: data.status === 'Completed'
360
- ? 'rgba(37, 99, 235, 0.1)'
361
- : data.status === 'Running'
362
- ? 'rgba(251, 191, 36, 0.1)'
363
- : data.status === 'Error'
364
- ? 'rgba(239, 68, 68, 0.1)'
365
- : 'rgba(16, 185, 129, 0.1)',
366
- color: data.status === 'Completed'
367
- ? '#93C5FD'
368
- : data.status === 'Running'
369
- ? '#FCD34D'
370
- : data.status === 'Error'
371
- ? '#FCA5A5'
372
- : '#86EFAC',
373
- fontWeight: 500,
374
- fontSize: '12px',
375
- height: '24px',
376
- borderRadius: '12px',
377
- }}
378
- />
379
- </Box>
380
-
381
- {/* Description Box */}
382
- <Box sx={{
383
- backgroundColor: '#1F2937',
384
- borderRadius: '8px',
385
- padding: '12px',
386
- mb: 2,
387
- border: '1px solid #374151'
388
- }}>
389
- {/* Inner text boundary box */}
390
- <Box sx={{
391
- backgroundColor: 'transparent',
392
- borderRadius: '4px',
393
- padding: '8px',
394
- border: '1px solid #4B5563', // Light grey border for inner box
395
- minHeight: '40px',
396
- display: 'flex',
397
- alignItems: 'center'
398
- }}>
399
- <Typography variant="body2" sx={{
400
- color: '#9CA3AF',
401
- fontSize: '12px',
402
- lineHeight: 1.4,
403
- margin: 0
404
- }}>
405
- {data.description}
406
- </Typography>
407
- </Box>
408
- </Box>
409
-
410
- {/* AI Reasoning Section */}
411
- <Box sx={{
412
- backgroundColor: '#1F2937',
413
- borderRadius: '8px',
414
- padding: '12px',
415
- mb: 2,
416
- border: '1px solid #374151'
417
- }}>
418
- {/* AI Reasoning Header */}
419
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
420
- <Box sx={{ color: '#34D399', fontSize: '16px' }}>
421
- 🧠
422
- </Box>
423
- <Typography variant="body2" sx={{
424
- color: '#34D399',
425
- fontSize: '12px',
426
- fontWeight: 600
427
- }}>
428
- {t('automation.formattingNode.aiReasoning')}
429
- </Typography>
430
- </Box>
431
-
432
- {/* AI Reasoning Description */}
433
- <Box sx={{
434
- backgroundColor: 'transparent',
435
- borderRadius: '4px',
436
- padding: '8px',
437
- border: '1px solid #4B5563',
438
- minHeight: '40px',
439
- display: 'flex',
440
- alignItems: 'center'
441
- }}>
442
- <Typography variant="body2" sx={{
443
- color: '#9CA3AF',
444
- fontSize: '12px',
445
- lineHeight: 1.4,
446
- margin: 0
447
- }}>
448
- {data.formData?.aiReasoning?.text || t('automation.formattingNode.aiReasoningDefault')}
449
- </Typography>
450
- </Box>
451
- </Box>
452
-
453
- {data.status === 'Completed' && (data.formData?.executionResult || data.formData?.schemaMatch) && (
454
- <>
455
- {/* Schema Row */}
456
- <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
457
- <Typography variant="body2" sx={{ color: '#9CA3AF', fontSize: '11px' }}>
458
- {t('automation.common.schema')}
459
- </Typography>
460
- <Typography variant="body2" sx={{ color: '#34D399', fontSize: '11px', fontWeight: 500 }}>
461
- HeadlineSchema
462
- </Typography>
463
- </Box>
464
-
465
- {/* Content Data Boxes */}
466
- <Box sx={{ display: 'flex', gap: 1, mb: 2 }}>
467
- {/* Matched Box */}
468
- <Box sx={{
469
- flex: 1,
470
- backgroundColor: '#1A3038',
471
- borderRadius: '6px',
472
- padding: '8px',
473
- textAlign: 'center',
474
- border: '1px solid #374151'
475
- }}>
476
- <Typography variant="h6" sx={{
477
- color: '#5EEAD4',
478
- fontSize: '16px',
479
- fontWeight: 600,
480
- mb: 0.5
481
- }}>
482
- {data.formData?.schemaMatch?.matched ?? 0}
483
- </Typography>
484
- <Typography variant="body2" sx={{ color: '#9CA3AF', fontSize: '10px' }}>
485
- {t('automation.common.matched')}
486
- </Typography>
487
- </Box>
488
-
489
- {/* Partial Box */}
490
- <Box sx={{
491
- flex: 1,
492
- backgroundColor: '#312B2F',
493
- borderRadius: '6px',
494
- padding: '8px',
495
- textAlign: 'center',
496
- border: '1px solid #374151'
497
- }}>
498
- <Typography variant="h6" sx={{
499
- color: '#FBBF24',
500
- fontSize: '16px',
501
- fontWeight: 600,
502
- mb: 0.5
503
- }}>
504
- {data.formData?.schemaMatch?.partial ?? 0}
505
- </Typography>
506
- <Typography variant="body2" sx={{ color: '#9CA3AF', fontSize: '10px' }}>
507
- {t('automation.common.partial')}
508
- </Typography>
509
- </Box>
510
-
511
- {/* Ignored Box */}
512
- <Box sx={{
513
- flex: 1,
514
- backgroundColor: '#252932',
515
- borderRadius: '6px',
516
- padding: '8px',
517
- textAlign: 'center',
518
- border: '1px solid #374151'
519
- }}>
520
- <Typography variant="h6" sx={{
521
- color: '#9CA3AF',
522
- fontSize: '16px',
523
- fontWeight: 600,
524
- mb: 0.5
525
- }}>
526
- {data.formData?.schemaMatch?.ignored ?? 0}
527
- </Typography>
528
- <Typography variant="body2" sx={{ color: '#9CA3AF', fontSize: '10px' }}>
529
- {t('automation.common.ignored')}
530
- </Typography>
531
- </Box>
532
- </Box>
533
- </>
534
- )}
535
-
536
- {/* Configuration Cards */}
537
- {data.formData?.configurationCards?.map((card: any, index: number) => (
538
- <Box key={index} sx={{
539
- backgroundColor: '#1F2937',
540
- borderRadius: '6px',
541
- padding: '8px 12px',
542
- mb: 1,
543
- display: 'flex',
544
- alignItems: 'center',
545
- gap: 1,
546
- border: '1px solid #374151'
547
- }}>
548
- <Box sx={{ color: card.iconColor || '#ffffff', fontSize: '16px' }}>
549
- {card.icon}
550
- </Box>
551
- <Typography variant="body2" sx={{
552
- color: '#ffffff',
553
- fontSize: '11px',
554
- fontWeight: 500
555
- }}>
556
- {card.label}
557
- </Typography>
558
- </Box>
559
- ))}
560
-
561
- {/* Last Run Info */}
562
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mt: 1 }}>
563
- <AccessTimeIcon sx={{ fontSize: '14px', color: '#9CA3AF' }} />
564
- <Typography variant="body2" sx={{ color: '#9CA3AF', fontSize: '11px' }}>
565
- {t('automation.common.lastRan')}: {data.lastRun}
566
- </Typography>
567
- </Box>
568
- </Box>
569
-
570
- {/* Connection Handles - Bidirectional (source + target at each position) */}
571
- {/* Top - Source */}
572
- <Handle
573
- type="source"
574
- position={Position.Top}
575
- id="top-source"
576
- className="connection-handle"
577
- style={{
578
- background: selected ? '#10B981' : '#1a1a2e',
579
- width: '14px',
580
- height: '14px',
581
- border: '3px solid #10B981',
582
- top: '-8px',
583
- opacity: selected ? 1 : 0,
584
- transition: 'all 0.2s ease-in-out',
585
- cursor: 'crosshair',
586
- zIndex: 10,
587
- }}
588
- />
589
- {/* Top - Target (hidden but functional) */}
590
- <Handle
591
- type="target"
592
- position={Position.Top}
593
- id="top-target"
594
- style={{
595
- background: 'transparent',
596
- width: '14px',
597
- height: '14px',
598
- border: 'none',
599
- top: '-8px',
600
- opacity: 0,
601
- pointerEvents: selected ? 'all' : 'none',
602
- }}
603
- />
604
- {/* Bottom - Source */}
605
- <Handle
606
- type="source"
607
- position={Position.Bottom}
608
- id="bottom-source"
609
- className="connection-handle"
610
- style={{
611
- background: selected ? '#10B981' : '#1a1a2e',
612
- width: '14px',
613
- height: '14px',
614
- border: '3px solid #10B981',
615
- bottom: '-8px',
616
- opacity: selected ? 1 : 0,
617
- transition: 'all 0.2s ease-in-out',
618
- cursor: 'crosshair',
619
- zIndex: 10,
620
- }}
621
- />
622
- {/* Bottom - Target (hidden but functional) */}
623
- <Handle
624
- type="target"
625
- position={Position.Bottom}
626
- id="bottom-target"
627
- style={{
628
- background: 'transparent',
629
- width: '14px',
630
- height: '14px',
631
- border: 'none',
632
- bottom: '-8px',
633
- opacity: 0,
634
- pointerEvents: selected ? 'all' : 'none',
635
- }}
636
- />
637
- {/* Left - Source */}
638
- <Handle
639
- type="source"
640
- position={Position.Left}
641
- id="left-source"
642
- className="connection-handle"
643
- style={{
644
- background: selected ? '#10B981' : '#1a1a2e',
645
- width: '14px',
646
- height: '14px',
647
- border: '3px solid #10B981',
648
- left: '-8px',
649
- opacity: selected ? 1 : 0,
650
- transition: 'all 0.2s ease-in-out',
651
- cursor: 'crosshair',
652
- zIndex: 10,
653
- }}
654
- />
655
- {/* Left - Target (hidden but functional) */}
656
- <Handle
657
- type="target"
658
- position={Position.Left}
659
- id="left-target"
660
- style={{
661
- background: 'transparent',
662
- width: '14px',
663
- height: '14px',
664
- border: 'none',
665
- left: '-8px',
666
- opacity: 0,
667
- pointerEvents: selected ? 'all' : 'none',
668
- }}
669
- />
670
- {/* Right - Source */}
671
- <Handle
672
- type="source"
673
- position={Position.Right}
674
- id="right-source"
675
- className="connection-handle"
676
- style={{
677
- background: selected ? '#10B981' : '#1a1a2e',
678
- width: '14px',
679
- height: '14px',
680
- border: '3px solid #10B981',
681
- right: '-8px',
682
- opacity: selected ? 1 : 0,
683
- transition: 'all 0.2s ease-in-out',
684
- cursor: 'crosshair',
685
- zIndex: 10,
686
- }}
687
- />
688
- {/* Right - Target (hidden but functional) */}
689
- <Handle
690
- type="target"
691
- position={Position.Right}
692
- id="right-target"
693
- style={{
694
- background: 'transparent',
695
- width: '14px',
696
- height: '14px',
697
- border: 'none',
698
- right: '-8px',
699
- opacity: 0,
700
- pointerEvents: selected ? 'all' : 'none',
701
- }}
702
- />
703
-
704
- </Box>
705
-
706
- {/* Node Action Buttons - Shows when selected */}
707
- <NodeActionButtons
708
- selected={selected}
709
- onOpenAIAssistant={(buttonElement) => {
710
- if (nodeId) {
711
- showNodeAIAssistantPopup(nodeId, 'Formatting Node', buttonElement);
712
- }
713
- }}
714
- onDelete={() => {
715
- if (nodeId && onNodesChange) {
716
- onNodesChange([{ id: nodeId, type: 'remove' }]);
717
- }
718
- }}
719
- onDuplicate={() => {
720
- if (nodeId) {
721
- const currentNode = nodes.find(n => n.id === nodeId);
722
- if (currentNode) {
723
- const newNode = {
724
- ...currentNode,
725
- id: `${currentNode.id}-copy-${Date.now()}`,
726
- position: {
727
- x: currentNode.position.x + 50,
728
- y: currentNode.position.y + 50,
729
- },
730
- selected: false,
731
- };
732
- setNodes([...nodes, newNode]);
733
- }
734
- }
735
- }}
736
- />
737
-
738
- {/* AI Suggestions Button - Positioned below the node box */}
739
- {data.formData?.aiSuggestionsCount !== undefined && data.formData.aiSuggestionsCount > 0 && (
740
- <Box
741
- sx={{
742
- position: 'absolute',
743
- top: '100%',
744
- left: '50%',
745
- transform: 'translateX(-50%)',
746
- marginTop: '12px',
747
- zIndex: 10,
748
- whiteSpace: 'nowrap',
749
- }}
750
- onClick={(e) => {
751
- e.stopPropagation();
752
- // Toggle AI Suggestions panel
753
- setShowSuggestions(!showSuggestions);
754
- }}
755
- >
756
- <Button
757
- variant="contained"
758
- startIcon={<LightbulbIcon sx={{ fontSize: '12px' }} />}
759
- sx={{
760
- backgroundColor: '#2563EB',
761
- color: '#ffffff',
762
- borderRadius: '20px',
763
- textTransform: 'none',
764
- fontSize: '10px',
765
- fontWeight: 400,
766
- padding: '8px 16px',
767
- whiteSpace: 'nowrap',
768
- display: 'inline-flex',
769
- alignItems: 'center',
770
- boxShadow: '0 2px 8px rgba(0, 0, 0, 0.3)',
771
- '&:hover': {
772
- backgroundColor: '#2563eb',
773
- },
774
- '& .MuiButton-startIcon': {
775
- marginRight: '8px',
776
- }
777
- }}
778
- >
779
- AI Suggestions
780
- <Box
781
- component="span"
782
- sx={{
783
- marginLeft: '8px',
784
- backgroundColor: '#FFFFFF26',
785
- color: '#ffffff',
786
- fontSize: '10px',
787
- fontWeight: 400,
788
- minWidth: '18px',
789
- height: '18px',
790
- borderRadius: '9px',
791
- display: 'inline-flex',
792
- alignItems: 'center',
793
- justifyContent: 'center',
794
- padding: '0 6px',
795
- border: '1px solid rgba(255, 255, 255, 0.2)',
796
- }}
797
- >
798
- {data.formData.aiSuggestionsCount}
799
- </Box>
800
- </Button>
801
- </Box>
802
- )}
803
-
804
- {/* AI Suggestions Panel - Rendered on canvas below the button */}
805
- {showSuggestions && data.formData?.aiSuggestionsCount !== undefined && data.formData.aiSuggestionsCount > 0 && nodeId && (
806
- <AISuggestionsPanel
807
- suggestions={data.formData?.aiSuggestions || [
808
- {
809
- id: '1',
810
- title: 'Add Citation Extraction',
811
- description: 'Automatically extract and format citations from article content.',
812
- tags: ['classification', 'enhancement'],
813
- },
814
- {
815
- id: '2',
816
- title: 'Generate Bullet Summary',
817
- description: 'Create a concise bullet-point summary of the article\'s main points.',
818
- tags: ['classification', 'enhancement'],
819
- },
820
- ]}
821
- parentNodeId={nodeId}
822
- onSuggestionClick={(suggestion) => {
823
- console.log('Suggestion clicked:', suggestion);
824
- // Handle suggestion selection here
825
- }}
826
- onClose={() => setShowSuggestions(false)}
827
- />
828
- )}
829
- </Box>
830
- );
831
- };
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 {
6
+ Psychology as PsychologyIcon,
7
+ AccessTime as AccessTimeIcon,
8
+ Visibility as VisibilityIcon,
9
+ FilterList as FilterIcon,
10
+ Send as SendIcon,
11
+ Settings as SettingsIcon,
12
+ Storage as StorageIcon,
13
+ Lightbulb as LightbulbIcon
14
+ } from '@mui/icons-material';
15
+ import { RiCloseLine, RiUser2Line } from 'react-icons/ri';
16
+ import ReactJson from 'react-json-view';
17
+ import { getIconByName } from '../../utils/iconMapper';
18
+ import { useTranslation } from 'react-i18next';
19
+ import { useDiagram } from '../../contexts/DiagramProvider';
20
+ import { AISuggestion } from './AISuggestionsModal';
21
+ import { AISuggestionsPanel } from './AISuggestionsPanel';
22
+ import { NodeActionButtons } from './NodeActionButtons';
23
+
24
+ interface AutomationFormattingNodeProps {
25
+ data: {
26
+ label: string;
27
+ description: string;
28
+ status: 'Ready' | 'Running' | 'Completed' | 'Error';
29
+ inputVariable: string;
30
+ formattingType: 'basic' | 'ai-powered';
31
+ aiConfig?: {
32
+ apiUrl: string;
33
+ model: string;
34
+ instruction: string;
35
+ temperature: number;
36
+ };
37
+ outputFormat: string;
38
+ outputVariable: string;
39
+ lastRun: string;
40
+ backgroundColor: string;
41
+ textColor: string;
42
+ borderColor: string;
43
+ iconName?: string; // Add iconName to the interface
44
+ formData?: {
45
+ schemaMatch?: {
46
+ matched: number;
47
+ partial: number;
48
+ ignored: number;
49
+ };
50
+ aiReasoning?: {
51
+ text: string;
52
+ };
53
+ configurationCards?: Array<{
54
+ icon: string;
55
+ label: string;
56
+ iconColor: string;
57
+ }>;
58
+ aiSuggestionsCount?: number; // Number of AI suggestions available
59
+ [key: string]: any;
60
+ }; // Include formData for configuration
61
+ };
62
+ selected?: boolean;
63
+ }
64
+
65
+ export const AutomationFormattingNode: React.FC<AutomationFormattingNodeProps> = ({ data, selected }) => {
66
+ const { t } = useTranslation();
67
+ const [isJsonOpen, setIsJsonOpen] = useState(false);
68
+ const [showSuggestions, setShowSuggestions] = useState(false);
69
+ const rootRef = useRef<any>(null);
70
+ const portalRef = useRef<HTMLDivElement | null>(null);
71
+ const nodeRef = useRef<HTMLDivElement | null>(null);
72
+ const nodeId = useNodeId();
73
+ const setSelectedNode = useDiagram((state) => state.setSelectedNode);
74
+ const enableJson = useDiagram((state) => state.enableNodeJsonPopover ?? true);
75
+ const onNodesChange = useDiagram((state) => state.onNodesChange);
76
+ const nodes = useDiagram((state) => state.nodes);
77
+ const setNodes = useDiagram((state) => state.setNodes);
78
+
79
+ // Get the icon component based on the iconName
80
+ const IconComponent = getIconByName(data.iconName);
81
+
82
+ const handleJsonClick = () => {
83
+ if (nodeId) setSelectedNode(nodeId);
84
+ if (!enableJson) return;
85
+ setIsJsonOpen(!isJsonOpen);
86
+ };
87
+
88
+ const handleClose = () => {
89
+ setIsJsonOpen(false);
90
+ // Clean up portal
91
+ if (rootRef.current) {
92
+ rootRef.current.unmount();
93
+ rootRef.current = null;
94
+ }
95
+ if (portalRef.current) {
96
+ document.body.removeChild(portalRef.current);
97
+ portalRef.current = null;
98
+ }
99
+ };
100
+
101
+ useEffect(() => {
102
+ const handleClickOutside = (event: MouseEvent) => {
103
+ if (isJsonOpen && !(event.target as Element).closest('#automation-json-popover')) {
104
+ handleClose();
105
+ }
106
+ };
107
+ document.addEventListener('mousedown', handleClickOutside);
108
+ return () => {
109
+ document.removeEventListener('mousedown', handleClickOutside);
110
+ };
111
+ }, [isJsonOpen]);
112
+
113
+ // Debug logging for node dimensions
114
+ useEffect(() => {
115
+ if (nodeRef.current) {
116
+ const rect = nodeRef.current.getBoundingClientRect();
117
+ const computedStyle = window.getComputedStyle(nodeRef.current);
118
+ // Debug information available but not logged
119
+ }
120
+ }, [data.label]);
121
+
122
+ useEffect(() => {
123
+ if (isJsonOpen) {
124
+ const portalRoot = document.createElement('div');
125
+ document.body.appendChild(portalRoot);
126
+ portalRef.current = portalRoot;
127
+
128
+ const root = createRoot(portalRoot);
129
+ rootRef.current = root;
130
+
131
+ root.render(
132
+ <Card
133
+ id="automation-json-popover"
134
+ sx={{
135
+ position: 'fixed',
136
+ top: 0,
137
+ right: 0,
138
+ zIndex: 9999,
139
+ width: '400px',
140
+ height: '100vh',
141
+ overflow: 'auto',
142
+ bgcolor: '#242424',
143
+ color: '#fff',
144
+ border: '1px solid #333',
145
+ '&::-webkit-scrollbar': {
146
+ width: '6px',
147
+ },
148
+ '&::-webkit-scrollbar-track': {
149
+ background: 'transparent',
150
+ },
151
+ '&::-webkit-scrollbar-thumb': {
152
+ background: '#444',
153
+ borderRadius: '3px',
154
+ '&:hover': {
155
+ background: '#666',
156
+ },
157
+ },
158
+ }}
159
+ >
160
+ <CardContent sx={{ bgcolor: '#242424', color: '#fff' }}>
161
+ <IconButton
162
+ aria-label="close"
163
+ onClick={handleClose}
164
+ sx={{
165
+ color: '#999',
166
+ '&:hover': {
167
+ color: '#fff',
168
+ bgcolor: 'rgba(255, 255, 255, 0.1)',
169
+ },
170
+ }}
171
+ >
172
+ <RiCloseLine />
173
+ </IconButton>
174
+ {/* Show execution result prominently if available */}
175
+ {data.formData?.executionResult && (
176
+ <Box sx={{ mb: 2 }}>
177
+ <Typography variant="h6" sx={{ color: '#fff', mb: 1 }}>
178
+ {t('automation.common.executionResult')}
179
+ </Typography>
180
+ <Box sx={{
181
+ bgcolor: data.formData.executionResult.success ? '#1e3a8a' : '#dc2626',
182
+ p: 1,
183
+ borderRadius: 1,
184
+ mb: 1
185
+ }}>
186
+ <Typography variant="body2" sx={{ color: '#fff' }}>
187
+ {t('automation.common.status')}: {data.formData.executionResult.success ? t('automation.common.success') : t('automation.common.failed')}
188
+ </Typography>
189
+ <Typography variant="body2" sx={{ color: '#fff' }}>
190
+ {t('automation.common.timestamp')}: {new Date(data.formData.executionResult.timestamp).toLocaleString()}
191
+ </Typography>
192
+ {data.formData.executionResult.error && (
193
+ <Typography variant="body2" sx={{ color: '#fff' }}>
194
+ {t('automation.common.error')}: {data.formData.executionResult.error}
195
+ </Typography>
196
+ )}
197
+ </Box>
198
+ <Typography variant="h6" sx={{ color: '#fff', mb: 1 }}>
199
+ {t('automation.formattingNode.formattedData')}
200
+ </Typography>
201
+ <ReactJson
202
+ theme={'monokai'}
203
+ src={data.formData.executionResult.data}
204
+ collapsed={false}
205
+ />
206
+ </Box>
207
+ )}
208
+
209
+ {/* Show full node data */}
210
+ <Typography variant="h6" sx={{ color: '#fff', mb: 1 }}>
211
+ {t('automation.common.fullNodeData')}
212
+ </Typography>
213
+ <ReactJson theme={'monokai'} src={data.formData || data} collapsed={false} />
214
+ </CardContent>
215
+ </Card>
216
+ );
217
+ } else {
218
+ // Clean up when closing
219
+ if (rootRef.current) {
220
+ rootRef.current.unmount();
221
+ rootRef.current = null;
222
+ }
223
+ if (portalRef.current) {
224
+ document.body.removeChild(portalRef.current);
225
+ portalRef.current = null;
226
+ }
227
+ }
228
+ }, [isJsonOpen, data]);
229
+
230
+ // Card component for consistent styling
231
+ const ProcessCard = ({ icon, label, description, iconColor }: {
232
+ icon: React.ReactNode;
233
+ label: string;
234
+ description: string;
235
+ iconColor: string;
236
+ }) => (
237
+ <Box sx={{
238
+ backgroundColor: '#1e293b',
239
+ borderRadius: '8px',
240
+ padding: '12px',
241
+ mb: 1,
242
+ border: '1px solid #334155',
243
+ display: 'flex',
244
+ alignItems: 'center',
245
+ gap: 2,
246
+ }}>
247
+ <Box
248
+ sx={{
249
+ width: '24px',
250
+ height: '24px',
251
+ display: 'flex',
252
+ alignItems: 'center',
253
+ justifyContent: 'center',
254
+ flexShrink: 0,
255
+ }}
256
+ >
257
+ {icon}
258
+ </Box>
259
+ <Box sx={{ flex: 1 }}>
260
+ <Typography variant="body2" sx={{
261
+ color: iconColor,
262
+ fontSize: '12px',
263
+ fontWeight: 600,
264
+ mb: 0.5
265
+ }}>
266
+ {label}:
267
+ </Typography>
268
+ <Typography variant="body2" sx={{
269
+ color: '#ffffff',
270
+ fontSize: '11px',
271
+ lineHeight: 1.3
272
+ }}>
273
+ {description}
274
+ </Typography>
275
+ </Box>
276
+ </Box>
277
+ );
278
+
279
+ return (
280
+ <Box
281
+ sx={{
282
+ position: 'relative',
283
+ width: '336px',
284
+ overflow: 'visible',
285
+ }}
286
+ >
287
+ <Box
288
+ ref={nodeRef}
289
+ sx={{
290
+ width: '336px',
291
+ minHeight: '150px',
292
+ backgroundColor: '#181C25', // New background color from image
293
+ border: selected ? '2px solid #3b82f6' : '1px solid #1e293b',
294
+ borderRadius: '12px',
295
+ color: '#ffffff',
296
+ position: 'relative',
297
+ boxShadow: selected ? '0 0 0 2px rgba(59, 130, 246, 0.5)' : '0 4px 8px rgba(0, 0, 0, 0.3)',
298
+ transition: 'all 0.2s ease',
299
+ cursor: 'pointer',
300
+ overflow: 'hidden',
301
+ ...(data.status === 'Running' && {
302
+ animation: 'pulse-glow 2s ease-in-out infinite',
303
+ '@keyframes pulse-glow': {
304
+ '0%, 100%': {
305
+ boxShadow: '0 0 20px rgba(59, 130, 246, 0.4), 0 0 40px rgba(59, 130, 246, 0.2)',
306
+ borderColor: 'rgba(59, 130, 246, 0.6)',
307
+ },
308
+ '50%': {
309
+ boxShadow: '0 0 30px rgba(59, 130, 246, 0.6), 0 0 60px rgba(59, 130, 246, 0.3)',
310
+ borderColor: 'rgba(59, 130, 246, 0.9)',
311
+ },
312
+ },
313
+ }),
314
+ }}
315
+ onClick={handleJsonClick}
316
+ >
317
+ {/* Top Header Section */}
318
+ <Box sx={{
319
+ backgroundColor: "rgba(67, 93, 132, 0.1)",
320
+ padding: '8px 16px',
321
+ borderRadius: '12px 12px 0 0'
322
+ }}>
323
+ <Typography variant="body2" sx={{
324
+ color: '#ffffff',
325
+ fontSize: '12px',
326
+ fontWeight: 500
327
+ }}>
328
+ {data.formData?.description || t('automation.formattingNode.headerDescription')}
329
+ </Typography>
330
+ </Box>
331
+
332
+ {/* Main Content */}
333
+ <Box sx={{ padding: '16px' }}>
334
+ {/* Title Section */}
335
+ <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
336
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
337
+ <Box
338
+ sx={{
339
+ width: '32px',
340
+ height: '32px',
341
+ backgroundColor: '#8b5cf6', // Purple like in image
342
+ borderRadius: '50%',
343
+ display: 'flex',
344
+ alignItems: 'center',
345
+ justifyContent: 'center',
346
+ }}
347
+ >
348
+ <IconComponent sx={{ color: 'white', fontSize: '18px' }} />
349
+ </Box>
350
+ <Typography variant="h6" sx={{ fontWeight: 600, fontSize: '16px' }}>
351
+ {data.label}
352
+ </Typography>
353
+ </Box>
354
+ <Chip
355
+ label={data.status || 'Ready'}
356
+ size="small"
357
+ sx={{
358
+ backgroundColor: data.status === 'Completed'
359
+ ? 'rgba(37, 99, 235, 0.1)'
360
+ : data.status === 'Running'
361
+ ? 'rgba(251, 191, 36, 0.1)'
362
+ : data.status === 'Error'
363
+ ? 'rgba(239, 68, 68, 0.1)'
364
+ : 'rgba(16, 185, 129, 0.1)',
365
+ color: data.status === 'Completed'
366
+ ? '#93C5FD'
367
+ : data.status === 'Running'
368
+ ? '#FCD34D'
369
+ : data.status === 'Error'
370
+ ? '#FCA5A5'
371
+ : '#86EFAC',
372
+ fontWeight: 500,
373
+ fontSize: '12px',
374
+ height: '24px',
375
+ borderRadius: '12px',
376
+ }}
377
+ />
378
+ </Box>
379
+
380
+ {/* Description Box */}
381
+ <Box sx={{
382
+ backgroundColor: '#1F2937',
383
+ borderRadius: '8px',
384
+ padding: '12px',
385
+ mb: 2,
386
+ border: '1px solid #374151'
387
+ }}>
388
+ {/* Inner text boundary box */}
389
+ <Box sx={{
390
+ backgroundColor: 'transparent',
391
+ borderRadius: '4px',
392
+ padding: '8px',
393
+ border: '1px solid #4B5563', // Light grey border for inner box
394
+ minHeight: '40px',
395
+ display: 'flex',
396
+ alignItems: 'center'
397
+ }}>
398
+ <Typography variant="body2" sx={{
399
+ color: '#9CA3AF',
400
+ fontSize: '12px',
401
+ lineHeight: 1.4,
402
+ margin: 0
403
+ }}>
404
+ {data.description}
405
+ </Typography>
406
+ </Box>
407
+ </Box>
408
+
409
+ {/* AI Reasoning Section */}
410
+ <Box sx={{
411
+ backgroundColor: '#1F2937',
412
+ borderRadius: '8px',
413
+ padding: '12px',
414
+ mb: 2,
415
+ border: '1px solid #374151'
416
+ }}>
417
+ {/* AI Reasoning Header */}
418
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
419
+ <Box sx={{ color: '#34D399', fontSize: '16px' }}>
420
+ 🧠
421
+ </Box>
422
+ <Typography variant="body2" sx={{
423
+ color: '#34D399',
424
+ fontSize: '12px',
425
+ fontWeight: 600
426
+ }}>
427
+ {t('automation.formattingNode.aiReasoning')}
428
+ </Typography>
429
+ </Box>
430
+
431
+ {/* AI Reasoning Description */}
432
+ <Box sx={{
433
+ backgroundColor: 'transparent',
434
+ borderRadius: '4px',
435
+ padding: '8px',
436
+ border: '1px solid #4B5563',
437
+ minHeight: '40px',
438
+ display: 'flex',
439
+ alignItems: 'center'
440
+ }}>
441
+ <Typography variant="body2" sx={{
442
+ color: '#9CA3AF',
443
+ fontSize: '12px',
444
+ lineHeight: 1.4,
445
+ margin: 0
446
+ }}>
447
+ {data.formData?.aiReasoning?.text || t('automation.formattingNode.aiReasoningDefault')}
448
+ </Typography>
449
+ </Box>
450
+ </Box>
451
+
452
+ {data.status === 'Completed' && (data.formData?.executionResult || data.formData?.schemaMatch) && (
453
+ <>
454
+ {/* Schema Row */}
455
+ <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
456
+ <Typography variant="body2" sx={{ color: '#9CA3AF', fontSize: '11px' }}>
457
+ {t('automation.common.schema')}
458
+ </Typography>
459
+ <Typography variant="body2" sx={{ color: '#34D399', fontSize: '11px', fontWeight: 500 }}>
460
+ HeadlineSchema
461
+ </Typography>
462
+ </Box>
463
+
464
+ {/* Content Data Boxes */}
465
+ <Box sx={{ display: 'flex', gap: 1, mb: 2 }}>
466
+ {/* Matched Box */}
467
+ <Box sx={{
468
+ flex: 1,
469
+ backgroundColor: '#1A3038',
470
+ borderRadius: '6px',
471
+ padding: '8px',
472
+ textAlign: 'center',
473
+ border: '1px solid #374151'
474
+ }}>
475
+ <Typography variant="h6" sx={{
476
+ color: '#5EEAD4',
477
+ fontSize: '16px',
478
+ fontWeight: 600,
479
+ mb: 0.5
480
+ }}>
481
+ {data.formData?.schemaMatch?.matched ?? 0}
482
+ </Typography>
483
+ <Typography variant="body2" sx={{ color: '#9CA3AF', fontSize: '10px' }}>
484
+ {t('automation.common.matched')}
485
+ </Typography>
486
+ </Box>
487
+
488
+ {/* Partial Box */}
489
+ <Box sx={{
490
+ flex: 1,
491
+ backgroundColor: '#312B2F',
492
+ borderRadius: '6px',
493
+ padding: '8px',
494
+ textAlign: 'center',
495
+ border: '1px solid #374151'
496
+ }}>
497
+ <Typography variant="h6" sx={{
498
+ color: '#FBBF24',
499
+ fontSize: '16px',
500
+ fontWeight: 600,
501
+ mb: 0.5
502
+ }}>
503
+ {data.formData?.schemaMatch?.partial ?? 0}
504
+ </Typography>
505
+ <Typography variant="body2" sx={{ color: '#9CA3AF', fontSize: '10px' }}>
506
+ {t('automation.common.partial')}
507
+ </Typography>
508
+ </Box>
509
+
510
+ {/* Ignored Box */}
511
+ <Box sx={{
512
+ flex: 1,
513
+ backgroundColor: '#252932',
514
+ borderRadius: '6px',
515
+ padding: '8px',
516
+ textAlign: 'center',
517
+ border: '1px solid #374151'
518
+ }}>
519
+ <Typography variant="h6" sx={{
520
+ color: '#9CA3AF',
521
+ fontSize: '16px',
522
+ fontWeight: 600,
523
+ mb: 0.5
524
+ }}>
525
+ {data.formData?.schemaMatch?.ignored ?? 0}
526
+ </Typography>
527
+ <Typography variant="body2" sx={{ color: '#9CA3AF', fontSize: '10px' }}>
528
+ {t('automation.common.ignored')}
529
+ </Typography>
530
+ </Box>
531
+ </Box>
532
+ </>
533
+ )}
534
+
535
+ {/* Configuration Cards */}
536
+ {data.formData?.configurationCards?.map((card: any, index: number) => (
537
+ <Box key={index} sx={{
538
+ backgroundColor: '#1F2937',
539
+ borderRadius: '6px',
540
+ padding: '8px 12px',
541
+ mb: 1,
542
+ display: 'flex',
543
+ alignItems: 'center',
544
+ gap: 1,
545
+ border: '1px solid #374151'
546
+ }}>
547
+ <Box sx={{ color: card.iconColor || '#ffffff', fontSize: '16px' }}>
548
+ {card.icon}
549
+ </Box>
550
+ <Typography variant="body2" sx={{
551
+ color: '#ffffff',
552
+ fontSize: '11px',
553
+ fontWeight: 500
554
+ }}>
555
+ {card.label}
556
+ </Typography>
557
+ </Box>
558
+ ))}
559
+
560
+ {/* Last Run Info */}
561
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mt: 1 }}>
562
+ <AccessTimeIcon sx={{ fontSize: '14px', color: '#9CA3AF' }} />
563
+ <Typography variant="body2" sx={{ color: '#9CA3AF', fontSize: '11px' }}>
564
+ {t('automation.common.lastRan')}: {data.lastRun}
565
+ </Typography>
566
+ </Box>
567
+ </Box>
568
+
569
+ {/* Connection Handles - Bidirectional (source + target at each position) */}
570
+ {/* Top - Source */}
571
+ <Handle
572
+ type="source"
573
+ position={Position.Top}
574
+ id="top-source"
575
+ className="connection-handle"
576
+ style={{
577
+ background: selected ? '#10B981' : '#1a1a2e',
578
+ width: '14px',
579
+ height: '14px',
580
+ border: '3px solid #10B981',
581
+ top: '-8px',
582
+ opacity: selected ? 1 : 0,
583
+ transition: 'all 0.2s ease-in-out',
584
+ cursor: 'crosshair',
585
+ zIndex: 10,
586
+ }}
587
+ />
588
+ {/* Top - Target (hidden but functional) */}
589
+ <Handle
590
+ type="target"
591
+ position={Position.Top}
592
+ id="top-target"
593
+ style={{
594
+ background: 'transparent',
595
+ width: '14px',
596
+ height: '14px',
597
+ border: 'none',
598
+ top: '-8px',
599
+ opacity: 0,
600
+ pointerEvents: selected ? 'all' : 'none',
601
+ }}
602
+ />
603
+ {/* Bottom - Source */}
604
+ <Handle
605
+ type="source"
606
+ position={Position.Bottom}
607
+ id="bottom-source"
608
+ className="connection-handle"
609
+ style={{
610
+ background: selected ? '#10B981' : '#1a1a2e',
611
+ width: '14px',
612
+ height: '14px',
613
+ border: '3px solid #10B981',
614
+ bottom: '-8px',
615
+ opacity: selected ? 1 : 0,
616
+ transition: 'all 0.2s ease-in-out',
617
+ cursor: 'crosshair',
618
+ zIndex: 10,
619
+ }}
620
+ />
621
+ {/* Bottom - Target (hidden but functional) */}
622
+ <Handle
623
+ type="target"
624
+ position={Position.Bottom}
625
+ id="bottom-target"
626
+ style={{
627
+ background: 'transparent',
628
+ width: '14px',
629
+ height: '14px',
630
+ border: 'none',
631
+ bottom: '-8px',
632
+ opacity: 0,
633
+ pointerEvents: selected ? 'all' : 'none',
634
+ }}
635
+ />
636
+ {/* Left - Source */}
637
+ <Handle
638
+ type="source"
639
+ position={Position.Left}
640
+ id="left-source"
641
+ className="connection-handle"
642
+ style={{
643
+ background: selected ? '#10B981' : '#1a1a2e',
644
+ width: '14px',
645
+ height: '14px',
646
+ border: '3px solid #10B981',
647
+ left: '-8px',
648
+ opacity: selected ? 1 : 0,
649
+ transition: 'all 0.2s ease-in-out',
650
+ cursor: 'crosshair',
651
+ zIndex: 10,
652
+ }}
653
+ />
654
+ {/* Left - Target (hidden but functional) */}
655
+ <Handle
656
+ type="target"
657
+ position={Position.Left}
658
+ id="left-target"
659
+ style={{
660
+ background: 'transparent',
661
+ width: '14px',
662
+ height: '14px',
663
+ border: 'none',
664
+ left: '-8px',
665
+ opacity: 0,
666
+ pointerEvents: selected ? 'all' : 'none',
667
+ }}
668
+ />
669
+ {/* Right - Source */}
670
+ <Handle
671
+ type="source"
672
+ position={Position.Right}
673
+ id="right-source"
674
+ className="connection-handle"
675
+ style={{
676
+ background: selected ? '#10B981' : '#1a1a2e',
677
+ width: '14px',
678
+ height: '14px',
679
+ border: '3px solid #10B981',
680
+ right: '-8px',
681
+ opacity: selected ? 1 : 0,
682
+ transition: 'all 0.2s ease-in-out',
683
+ cursor: 'crosshair',
684
+ zIndex: 10,
685
+ }}
686
+ />
687
+ {/* Right - Target (hidden but functional) */}
688
+ <Handle
689
+ type="target"
690
+ position={Position.Right}
691
+ id="right-target"
692
+ style={{
693
+ background: 'transparent',
694
+ width: '14px',
695
+ height: '14px',
696
+ border: 'none',
697
+ right: '-8px',
698
+ opacity: 0,
699
+ pointerEvents: selected ? 'all' : 'none',
700
+ }}
701
+ />
702
+
703
+ </Box>
704
+
705
+ {/* Node Action Buttons - Shows when selected */}
706
+ <NodeActionButtons
707
+ selected={selected}
708
+ onDelete={() => {
709
+ if (nodeId && onNodesChange) {
710
+ onNodesChange([{ id: nodeId, type: 'remove' }]);
711
+ }
712
+ }}
713
+ onDuplicate={() => {
714
+ if (nodeId) {
715
+ const currentNode = nodes.find(n => n.id === nodeId);
716
+ if (currentNode) {
717
+ const newNode = {
718
+ ...currentNode,
719
+ id: `${currentNode.id}-copy-${Date.now()}`,
720
+ position: {
721
+ x: currentNode.position.x + 50,
722
+ y: currentNode.position.y + 50,
723
+ },
724
+ selected: false,
725
+ };
726
+ setNodes([...nodes, newNode]);
727
+ }
728
+ }
729
+ }}
730
+ />
731
+
732
+ {/* AI Suggestions Button - Positioned below the node box */}
733
+ {data.formData?.aiSuggestionsCount !== undefined && data.formData.aiSuggestionsCount > 0 && (
734
+ <Box
735
+ sx={{
736
+ position: 'absolute',
737
+ top: '100%',
738
+ left: '50%',
739
+ transform: 'translateX(-50%)',
740
+ marginTop: '12px',
741
+ zIndex: 10,
742
+ whiteSpace: 'nowrap',
743
+ }}
744
+ onClick={(e) => {
745
+ e.stopPropagation();
746
+ // Toggle AI Suggestions panel
747
+ setShowSuggestions(!showSuggestions);
748
+ }}
749
+ >
750
+ <Button
751
+ variant="contained"
752
+ startIcon={<LightbulbIcon sx={{ fontSize: '12px' }} />}
753
+ sx={{
754
+ backgroundColor: '#2563EB',
755
+ color: '#ffffff',
756
+ borderRadius: '20px',
757
+ textTransform: 'none',
758
+ fontSize: '10px',
759
+ fontWeight: 400,
760
+ padding: '8px 16px',
761
+ whiteSpace: 'nowrap',
762
+ display: 'inline-flex',
763
+ alignItems: 'center',
764
+ boxShadow: '0 2px 8px rgba(0, 0, 0, 0.3)',
765
+ '&:hover': {
766
+ backgroundColor: '#2563eb',
767
+ },
768
+ '& .MuiButton-startIcon': {
769
+ marginRight: '8px',
770
+ }
771
+ }}
772
+ >
773
+ AI Suggestions
774
+ <Box
775
+ component="span"
776
+ sx={{
777
+ marginLeft: '8px',
778
+ backgroundColor: '#FFFFFF26',
779
+ color: '#ffffff',
780
+ fontSize: '10px',
781
+ fontWeight: 400,
782
+ minWidth: '18px',
783
+ height: '18px',
784
+ borderRadius: '9px',
785
+ display: 'inline-flex',
786
+ alignItems: 'center',
787
+ justifyContent: 'center',
788
+ padding: '0 6px',
789
+ border: '1px solid rgba(255, 255, 255, 0.2)',
790
+ }}
791
+ >
792
+ {data.formData.aiSuggestionsCount}
793
+ </Box>
794
+ </Button>
795
+ </Box>
796
+ )}
797
+
798
+ {/* AI Suggestions Panel - Rendered on canvas below the button */}
799
+ {showSuggestions && data.formData?.aiSuggestionsCount !== undefined && data.formData.aiSuggestionsCount > 0 && nodeId && (
800
+ <AISuggestionsPanel
801
+ suggestions={data.formData?.aiSuggestions || [
802
+ {
803
+ id: '1',
804
+ title: 'Add Citation Extraction',
805
+ description: 'Automatically extract and format citations from article content.',
806
+ tags: ['classification', 'enhancement'],
807
+ },
808
+ {
809
+ id: '2',
810
+ title: 'Generate Bullet Summary',
811
+ description: 'Create a concise bullet-point summary of the article\'s main points.',
812
+ tags: ['classification', 'enhancement'],
813
+ },
814
+ ]}
815
+ parentNodeId={nodeId}
816
+ onSuggestionClick={(suggestion) => {
817
+ console.log('Suggestion clicked:', suggestion);
818
+ // Handle suggestion selection here
819
+ }}
820
+ onClose={() => setShowSuggestions(false)}
821
+ />
822
+ )}
823
+ </Box>
824
+ );
825
+ };