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