@flowuent-org/diagramming-core 1.2.1 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (20) hide show
  1. package/apps/diagramming/src/AutomationDiagramData.ts +65 -1
  2. package/package.json +1 -1
  3. package/packages/diagrams/src/lib/components/CanvasSearchBar.tsx +281 -0
  4. package/packages/diagrams/src/lib/components/automation/AutomationApiNode.tsx +796 -788
  5. package/packages/diagrams/src/lib/components/automation/AutomationEndNode.tsx +608 -600
  6. package/packages/diagrams/src/lib/components/automation/AutomationFormattingNode.tsx +833 -825
  7. package/packages/diagrams/src/lib/components/automation/AutomationNavigationNode.tsx +584 -0
  8. package/packages/diagrams/src/lib/components/automation/AutomationNoteNode.tsx +422 -414
  9. package/packages/diagrams/src/lib/components/automation/AutomationSheetsNode.tsx +1120 -1112
  10. package/packages/diagrams/src/lib/components/automation/AutomationStartNode.tsx +511 -503
  11. package/packages/diagrams/src/lib/components/automation/NodeAIAssistantPopup.tsx +504 -0
  12. package/packages/diagrams/src/lib/components/automation/NodeActionButtons.tsx +146 -145
  13. package/packages/diagrams/src/lib/components/automation/index.ts +21 -12
  14. package/packages/diagrams/src/lib/contexts/SearchContext.tsx +78 -0
  15. package/packages/diagrams/src/lib/templates/DiagramContainer.tsx +79 -76
  16. package/packages/diagrams/src/lib/templates/DiagramContent.tsx +276 -61
  17. package/packages/diagrams/src/lib/types/automation-node-data-types.ts +22 -2
  18. package/packages/diagrams/src/lib/types/node-types.ts +2 -0
  19. package/packages/diagrams/src/lib/utils/highlightText.tsx +93 -0
  20. package/packages/diagrams/src/lib/utils/nodeAIAssistantConfig.ts +54 -0
@@ -1,788 +1,796 @@
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
- };
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
+ import { useSearch } from '../../contexts/SearchContext';
25
+
26
+ interface AutomationApiNodeProps {
27
+ data: {
28
+ label: string;
29
+ description: string;
30
+ status: 'Ready' | 'Running' | 'Completed' | 'Error';
31
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE';
32
+ url: string;
33
+ headers: Array<{ key: string; value: string; enabled: boolean }>;
34
+ queryParams: Array<{ key: string; value: string; enabled: boolean }>;
35
+ body?: string;
36
+ timeout: number;
37
+ retryCount: number;
38
+ lastRun: string;
39
+ backgroundColor: string;
40
+ textColor: string;
41
+ borderColor: string;
42
+ iconName?: string; // Add iconName to the interface
43
+ formData?: {
44
+ schemaMatch?: {
45
+ matched: number;
46
+ partial: number;
47
+ ignored: number;
48
+ };
49
+ configurationCards?: Array<{
50
+ icon: string;
51
+ label: string;
52
+ iconColor: string;
53
+ }>;
54
+ aiSuggestionsCount?: number; // Number of AI suggestions available
55
+ aiSuggestions?: AISuggestion[]; // AI suggestions data
56
+ [key: string]: any;
57
+ }; // Include formData for configuration
58
+ };
59
+ selected?: boolean;
60
+ }
61
+
62
+ export const AutomationApiNode: React.FC<AutomationApiNodeProps> = ({ data, selected }) => {
63
+ const { t } = useTranslation();
64
+ const { highlightText } = useSearch();
65
+ const [isJsonOpen, setIsJsonOpen] = useState(false);
66
+ const [showSuggestions, setShowSuggestions] = useState(false);
67
+ const rootRef = useRef<any>(null);
68
+ const portalRef = useRef<HTMLDivElement | null>(null);
69
+ const nodeRef = useRef<HTMLDivElement | null>(null);
70
+ const nodeId = useNodeId();
71
+ const setSelectedNode = useDiagram((state) => state.setSelectedNode);
72
+ const enableJson = useDiagram((state) => state.enableNodeJsonPopover ?? true);
73
+ const onNodesChange = useDiagram((state) => state.onNodesChange);
74
+ const nodes = useDiagram((state) => state.nodes);
75
+ const setNodes = useDiagram((state) => state.setNodes);
76
+
77
+ // Get the icon component based on the iconName
78
+ const IconComponent = getIconByName(data.iconName);
79
+
80
+
81
+
82
+ const handleJsonClick = () => {
83
+ if (nodeId) setSelectedNode(nodeId);
84
+ if (!enableJson) return; // host app disables built-in popover
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.apiNode.responseData')}
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.apiNode.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: '#0ea5e9',
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
+ {highlightText(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
+ {highlightText(data.description)}
405
+ </Typography>
406
+ </Box>
407
+ </Box>
408
+
409
+ {/* Additional Info Text */}
410
+ <Typography variant="body2" sx={{
411
+ color: '#9CA3AF',
412
+ fontSize: '11px',
413
+ mb: 1
414
+ }}>
415
+ {t('automation.apiNode.additionalInfo')}
416
+ </Typography>
417
+
418
+ {data.status === 'Completed' && (data.formData?.executionResult || data.formData?.schemaMatch) && (
419
+ <>
420
+ {/* Schema Row */}
421
+ <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
422
+ <Typography variant="body2" sx={{ color: '#9CA3AF', fontSize: '11px' }}>
423
+ {t('automation.common.schema')}
424
+ </Typography>
425
+ <Typography variant="body2" sx={{ color: '#34D399', fontSize: '11px', fontWeight: 500 }}>
426
+ HeadlineSchema
427
+ </Typography>
428
+ </Box>
429
+
430
+ {/* Content Data Boxes */}
431
+ <Box sx={{ display: 'flex', gap: 1, mb: 2 }}>
432
+ {/* Matched Box */}
433
+ <Box sx={{
434
+ flex: 1,
435
+ backgroundColor: '#1A3038',
436
+ borderRadius: '6px',
437
+ padding: '8px',
438
+ textAlign: 'center',
439
+ border: '1px solid #374151'
440
+ }}>
441
+ <Typography variant="h6" sx={{
442
+ color: '#5EEAD4',
443
+ fontSize: '16px',
444
+ fontWeight: 600,
445
+ mb: 0.5
446
+ }}>
447
+ {data.formData?.schemaMatch?.matched ?? 0}
448
+ </Typography>
449
+ <Typography variant="body2" sx={{ color: '#9CA3AF', fontSize: '10px' }}>
450
+ {t('automation.common.matched')}
451
+ </Typography>
452
+ </Box>
453
+
454
+ {/* Partial Box */}
455
+ <Box sx={{
456
+ flex: 1,
457
+ backgroundColor: '#312B2F',
458
+ borderRadius: '6px',
459
+ padding: '8px',
460
+ textAlign: 'center',
461
+ border: '1px solid #374151'
462
+ }}>
463
+ <Typography variant="h6" sx={{
464
+ color: '#FBBF24',
465
+ fontSize: '16px',
466
+ fontWeight: 600,
467
+ mb: 0.5
468
+ }}>
469
+ {data.formData?.schemaMatch?.partial ?? 0}
470
+ </Typography>
471
+ <Typography variant="body2" sx={{ color: '#9CA3AF', fontSize: '10px' }}>
472
+ {t('automation.common.partial')}
473
+ </Typography>
474
+ </Box>
475
+
476
+ {/* Ignored Box */}
477
+ <Box sx={{
478
+ flex: 1,
479
+ backgroundColor: '#252932',
480
+ borderRadius: '6px',
481
+ padding: '8px',
482
+ textAlign: 'center',
483
+ border: '1px solid #374151'
484
+ }}>
485
+ <Typography variant="h6" sx={{
486
+ color: '#9CA3AF',
487
+ fontSize: '16px',
488
+ fontWeight: 600,
489
+ mb: 0.5
490
+ }}>
491
+ {data.formData?.schemaMatch?.ignored ?? 0}
492
+ </Typography>
493
+ <Typography variant="body2" sx={{ color: '#9CA3AF', fontSize: '10px' }}>
494
+ {t('automation.common.ignored')}
495
+ </Typography>
496
+ </Box>
497
+ </Box>
498
+ </>
499
+ )}
500
+
501
+ {/* Configuration Cards */}
502
+ {data.formData?.configurationCards?.map((card: any, index: number) => (
503
+ <Box key={index} sx={{
504
+ backgroundColor: '#1F2937',
505
+ borderRadius: '6px',
506
+ padding: '8px 12px',
507
+ mb: 1,
508
+ display: 'flex',
509
+ alignItems: 'center',
510
+ gap: 1,
511
+ border: '1px solid #374151'
512
+ }}>
513
+ <Box sx={{ color: card.iconColor || '#ffffff', fontSize: '16px' }}>
514
+ {card.icon}
515
+ </Box>
516
+ <Typography variant="body2" sx={{
517
+ color: '#ffffff',
518
+ fontSize: '11px',
519
+ fontWeight: 500
520
+ }}>
521
+ {card.label}
522
+ </Typography>
523
+ </Box>
524
+ ))}
525
+
526
+ {/* Last Run Info */}
527
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mt: 1 }}>
528
+ <AccessTimeIcon sx={{ fontSize: '14px', color: '#9CA3AF' }} />
529
+ <Typography variant="body2" sx={{ color: '#9CA3AF', fontSize: '11px' }}>
530
+ {t('automation.common.lastRan')}: {data.lastRun}
531
+ </Typography>
532
+ </Box>
533
+ </Box>
534
+
535
+ {/* Connection Handles - Bidirectional (source + target at each position) */}
536
+ {/* Top - Source */}
537
+ <Handle
538
+ type="source"
539
+ position={Position.Top}
540
+ id="top-source"
541
+ className="connection-handle"
542
+ style={{
543
+ background: selected ? '#10B981' : '#1a1a2e',
544
+ width: '14px',
545
+ height: '14px',
546
+ border: '3px solid #10B981',
547
+ top: '-8px',
548
+ opacity: selected ? 1 : 0,
549
+ transition: 'all 0.2s ease-in-out',
550
+ cursor: 'crosshair',
551
+ zIndex: 10,
552
+ }}
553
+ />
554
+ {/* Top - Target (hidden but functional) */}
555
+ <Handle
556
+ type="target"
557
+ position={Position.Top}
558
+ id="top-target"
559
+ style={{
560
+ background: 'transparent',
561
+ width: '14px',
562
+ height: '14px',
563
+ border: 'none',
564
+ top: '-8px',
565
+ opacity: 0,
566
+ pointerEvents: selected ? 'all' : 'none',
567
+ }}
568
+ />
569
+ {/* Bottom - Source */}
570
+ <Handle
571
+ type="source"
572
+ position={Position.Bottom}
573
+ id="bottom-source"
574
+ className="connection-handle"
575
+ style={{
576
+ background: selected ? '#10B981' : '#1a1a2e',
577
+ width: '14px',
578
+ height: '14px',
579
+ border: '3px solid #10B981',
580
+ bottom: '-8px',
581
+ opacity: selected ? 1 : 0,
582
+ transition: 'all 0.2s ease-in-out',
583
+ cursor: 'crosshair',
584
+ zIndex: 10,
585
+ }}
586
+ />
587
+ {/* Bottom - Target (hidden but functional) */}
588
+ <Handle
589
+ type="target"
590
+ position={Position.Bottom}
591
+ id="bottom-target"
592
+ style={{
593
+ background: 'transparent',
594
+ width: '14px',
595
+ height: '14px',
596
+ border: 'none',
597
+ bottom: '-8px',
598
+ opacity: 0,
599
+ pointerEvents: selected ? 'all' : 'none',
600
+ }}
601
+ />
602
+ {/* Left - Source */}
603
+ <Handle
604
+ type="source"
605
+ position={Position.Left}
606
+ id="left-source"
607
+ className="connection-handle"
608
+ style={{
609
+ background: selected ? '#10B981' : '#1a1a2e',
610
+ width: '14px',
611
+ height: '14px',
612
+ border: '3px solid #10B981',
613
+ left: '-8px',
614
+ opacity: selected ? 1 : 0,
615
+ transition: 'all 0.2s ease-in-out',
616
+ cursor: 'crosshair',
617
+ zIndex: 10,
618
+ }}
619
+ />
620
+ {/* Left - Target (hidden but functional) */}
621
+ <Handle
622
+ type="target"
623
+ position={Position.Left}
624
+ id="left-target"
625
+ style={{
626
+ background: 'transparent',
627
+ width: '14px',
628
+ height: '14px',
629
+ border: 'none',
630
+ left: '-8px',
631
+ opacity: 0,
632
+ pointerEvents: selected ? 'all' : 'none',
633
+ }}
634
+ />
635
+ {/* Right - Source */}
636
+ <Handle
637
+ type="source"
638
+ position={Position.Right}
639
+ id="right-source"
640
+ className="connection-handle"
641
+ style={{
642
+ background: selected ? '#10B981' : '#1a1a2e',
643
+ width: '14px',
644
+ height: '14px',
645
+ border: '3px solid #10B981',
646
+ right: '-8px',
647
+ opacity: selected ? 1 : 0,
648
+ transition: 'all 0.2s ease-in-out',
649
+ cursor: 'crosshair',
650
+ zIndex: 10,
651
+ }}
652
+ />
653
+ {/* Right - Target (hidden but functional) */}
654
+ <Handle
655
+ type="target"
656
+ position={Position.Right}
657
+ id="right-target"
658
+ style={{
659
+ background: 'transparent',
660
+ width: '14px',
661
+ height: '14px',
662
+ border: 'none',
663
+ right: '-8px',
664
+ opacity: 0,
665
+ pointerEvents: selected ? 'all' : 'none',
666
+ }}
667
+ />
668
+
669
+ </Box>
670
+
671
+ {/* Node Action Buttons - Shows when selected */}
672
+ <NodeActionButtons
673
+ selected={selected}
674
+ onOpenAIAssistant={(buttonElement) => {
675
+ if (nodeId) {
676
+ showNodeAIAssistantPopup(nodeId, 'API Node', buttonElement);
677
+ }
678
+ }}
679
+ onDelete={() => {
680
+ if (nodeId && onNodesChange) {
681
+ onNodesChange([{ id: nodeId, type: 'remove' }]);
682
+ }
683
+ }}
684
+ onDuplicate={() => {
685
+ if (nodeId) {
686
+ const currentNode = nodes.find(n => n.id === nodeId);
687
+ if (currentNode) {
688
+ const newNode = {
689
+ ...currentNode,
690
+ id: `${currentNode.id}-copy-${Date.now()}`,
691
+ position: {
692
+ x: currentNode.position.x + 50,
693
+ y: currentNode.position.y + 50,
694
+ },
695
+ selected: false,
696
+ };
697
+ setNodes([...nodes, newNode]);
698
+ }
699
+ }
700
+ }}
701
+ />
702
+
703
+ {/* AI Suggestions Button - Positioned below the node box */}
704
+ {data.formData?.aiSuggestionsCount !== undefined && data.formData.aiSuggestionsCount > 0 && (
705
+ <Box
706
+ sx={{
707
+ position: 'absolute',
708
+ top: '100%',
709
+ left: '50%',
710
+ transform: 'translateX(-50%)',
711
+ marginTop: '12px',
712
+ zIndex: 10,
713
+ whiteSpace: 'nowrap',
714
+ }}
715
+ onClick={(e) => {
716
+ e.stopPropagation();
717
+ // Toggle AI Suggestions panel
718
+ setShowSuggestions(!showSuggestions);
719
+ }}
720
+ >
721
+ <Button
722
+ variant="contained"
723
+ startIcon={<LightbulbIcon sx={{ fontSize: '12px' }} />}
724
+ sx={{
725
+ backgroundColor: '#2563EB',
726
+ color: '#ffffff',
727
+ borderRadius: '20px',
728
+ textTransform: 'none',
729
+ fontSize: '10px',
730
+ fontWeight: 400,
731
+ padding: '8px 16px',
732
+ whiteSpace: 'nowrap',
733
+ display: 'inline-flex',
734
+ alignItems: 'center',
735
+ boxShadow: '0 2px 8px rgba(0, 0, 0, 0.3)',
736
+ '&:hover': {
737
+ backgroundColor: '#2563eb',
738
+ },
739
+ '& .MuiButton-startIcon': {
740
+ marginRight: '8px',
741
+ }
742
+ }}
743
+ >
744
+ AI Suggestions
745
+ <Box
746
+ component="span"
747
+ sx={{
748
+ marginLeft: '8px',
749
+ backgroundColor: '#FFFFFF26',
750
+ color: '#ffffff',
751
+ fontSize: '10px',
752
+ fontWeight: 400,
753
+ minWidth: '18px',
754
+ height: '18px',
755
+ borderRadius: '9px',
756
+ display: 'inline-flex',
757
+ alignItems: 'center',
758
+ justifyContent: 'center',
759
+ padding: '0 6px',
760
+ border: '1px solid rgba(255, 255, 255, 0.2)',
761
+ }}
762
+ >
763
+ {data.formData.aiSuggestionsCount}
764
+ </Box>
765
+ </Button>
766
+ </Box>
767
+ )}
768
+
769
+ {/* AI Suggestions Panel - Rendered on canvas below the button */}
770
+ {showSuggestions && data.formData?.aiSuggestionsCount !== undefined && data.formData.aiSuggestionsCount > 0 && nodeId && (
771
+ <AISuggestionsPanel
772
+ suggestions={data.formData?.aiSuggestions || [
773
+ {
774
+ id: '1',
775
+ title: 'Add Citation Extraction',
776
+ description: 'Automatically extract and format citations from article content.',
777
+ tags: ['classification', 'enhancement'],
778
+ },
779
+ {
780
+ id: '2',
781
+ title: 'Generate Bullet Summary',
782
+ description: 'Create a concise bullet-point summary of the article\'s main points.',
783
+ tags: ['classification', 'enhancement'],
784
+ },
785
+ ]}
786
+ parentNodeId={nodeId}
787
+ onSuggestionClick={(suggestion) => {
788
+ console.log('Suggestion clicked:', suggestion);
789
+ // Handle suggestion selection here
790
+ }}
791
+ onClose={() => setShowSuggestions(false)}
792
+ />
793
+ )}
794
+ </Box>
795
+ );
796
+ };