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