@flowuent-org/diagramming-core 1.1.8 → 1.2.0

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