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