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