@flowuent-org/diagramming-core 1.1.8 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (20) hide show
  1. package/package.json +116 -116
  2. package/packages/diagrams/src/index.ts +1 -0
  3. package/packages/diagrams/src/lib/atoms/ConnectionPoints.tsx +149 -0
  4. package/packages/diagrams/src/lib/components/automation/AutomationApiNode.tsx +794 -650
  5. package/packages/diagrams/src/lib/components/automation/AutomationEndNode.tsx +606 -449
  6. package/packages/diagrams/src/lib/components/automation/AutomationFormattingNode.tsx +831 -687
  7. package/packages/diagrams/src/lib/components/automation/AutomationNoteNode.tsx +420 -275
  8. package/packages/diagrams/src/lib/components/automation/AutomationSheetsNode.tsx +1118 -974
  9. package/packages/diagrams/src/lib/components/automation/AutomationStartNode.tsx +509 -344
  10. package/packages/diagrams/src/lib/components/automation/NodeAIAssistantPopup.tsx +504 -0
  11. package/packages/diagrams/src/lib/components/automation/NodeActionButtons.tsx +146 -0
  12. package/packages/diagrams/src/lib/components/automation/index.ts +20 -11
  13. package/packages/diagrams/src/lib/molecules/SideHandles.tsx +177 -12
  14. package/packages/diagrams/src/lib/organisms/CustomEdge/custom-edge-generator.tsx +10 -5
  15. package/packages/diagrams/src/lib/styles.css +53 -0
  16. package/packages/diagrams/src/lib/templates/DiagramContainer.tsx +59 -0
  17. package/packages/diagrams/src/lib/templates/Diagramming.tsx +246 -204
  18. package/packages/diagrams/src/lib/types/edge-types.ts +17 -0
  19. package/packages/diagrams/src/lib/utils/generateEdgesFromNodeOrder.ts +113 -0
  20. package/packages/diagrams/src/lib/utils/nodeAIAssistantConfig.ts +54 -0
@@ -1,344 +1,509 @@
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 } from '@mui/material';
5
- import { AccessTime as AccessTimeIcon } 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
-
12
- interface AutomationStartNodeProps {
13
- data: {
14
- label: string;
15
- description: string;
16
- status: 'Ready' | 'Running' | 'Completed' | 'Error';
17
- triggerType: 'manual' | 'scheduled';
18
- scheduleConfig?: {
19
- frequency: 'hourly' | 'daily' | 'weekly';
20
- time?: string;
21
- cron?: string;
22
- };
23
- lastRun: string;
24
- backgroundColor: string;
25
- textColor: string;
26
- borderColor: string;
27
- iconName?: string; // Add iconName to the interface
28
- formData?: any; // Include formData for configuration
29
- };
30
- selected?: boolean;
31
- }
32
-
33
- export const AutomationStartNode: React.FC<AutomationStartNodeProps> = ({ data, selected }) => {
34
- const { t } = useTranslation();
35
- const [isJsonOpen, setIsJsonOpen] = useState(false);
36
- const rootRef = useRef<any>(null);
37
- const portalRef = useRef<HTMLDivElement | null>(null);
38
- const nodeRef = useRef<HTMLDivElement | null>(null);
39
- const nodeId = useNodeId();
40
- const setSelectedNode = useDiagram((state) => state.setSelectedNode);
41
- const enableJson = useDiagram((state) => state.enableNodeJsonPopover ?? true);
42
-
43
- // Get the icon component based on the iconName
44
- const IconComponent = getIconByName(data.iconName);
45
-
46
-
47
-
48
- const handleJsonClick = () => {
49
- if (nodeId) setSelectedNode(nodeId);
50
- if (!enableJson) return;
51
- setIsJsonOpen(!isJsonOpen);
52
- };
53
-
54
- const handleClose = () => {
55
- setIsJsonOpen(false);
56
- // Clean up portal
57
- if (rootRef.current) {
58
- rootRef.current.unmount();
59
- rootRef.current = null;
60
- }
61
- if (portalRef.current) {
62
- document.body.removeChild(portalRef.current);
63
- portalRef.current = null;
64
- }
65
- };
66
-
67
- useEffect(() => {
68
- const handleClickOutside = (event: MouseEvent) => {
69
- if (isJsonOpen && !(event.target as Element).closest('#automation-json-popover')) {
70
- handleClose();
71
- }
72
- };
73
- document.addEventListener('mousedown', handleClickOutside);
74
- return () => {
75
- document.removeEventListener('mousedown', handleClickOutside);
76
- };
77
- }, [isJsonOpen]);
78
-
79
- // Debug logging for node dimensions
80
- useEffect(() => {
81
- if (nodeRef.current) {
82
- const rect = nodeRef.current.getBoundingClientRect();
83
- const computedStyle = window.getComputedStyle(nodeRef.current);
84
- // Debug information available but not logged
85
- }
86
- }, [data.label]);
87
-
88
- useEffect(() => {
89
- if (isJsonOpen) {
90
- const portalRoot = document.createElement('div');
91
- document.body.appendChild(portalRoot);
92
- portalRef.current = portalRoot;
93
-
94
- const root = createRoot(portalRoot);
95
- rootRef.current = root;
96
-
97
- root.render(
98
- <Card
99
- id="automation-json-popover"
100
- sx={{
101
- position: 'fixed',
102
- top: 0,
103
- right: 0,
104
- zIndex: 9999,
105
- width: '400px',
106
- height: '100vh',
107
- overflow: 'auto',
108
- bgcolor: '#242424',
109
- color: '#fff',
110
- border: '1px solid #333',
111
- '&::-webkit-scrollbar': {
112
- width: '6px',
113
- },
114
- '&::-webkit-scrollbar-track': {
115
- background: 'transparent',
116
- },
117
- '&::-webkit-scrollbar-thumb': {
118
- background: '#444',
119
- borderRadius: '3px',
120
- '&:hover': {
121
- background: '#666',
122
- },
123
- },
124
- }}
125
- >
126
- <CardContent sx={{ bgcolor: '#242424', color: '#fff' }}>
127
- <IconButton
128
- aria-label="close"
129
- onClick={handleClose}
130
- sx={{
131
- color: '#999',
132
- '&:hover': {
133
- color: '#fff',
134
- bgcolor: 'rgba(255, 255, 255, 0.1)',
135
- },
136
- }}
137
- >
138
- <RiCloseLine />
139
- </IconButton>
140
- {/* Show execution result prominently if available */}
141
- {data.formData?.executionResult && (
142
- <Box sx={{ mb: 2 }}>
143
- <Typography variant="h6" sx={{ color: '#fff', mb: 1 }}>
144
- {t('automation.common.executionResult')}
145
- </Typography>
146
- <Box sx={{
147
- bgcolor: data.formData.executionResult.success ? '#1e3a8a' : '#dc2626',
148
- p: 1,
149
- borderRadius: 1,
150
- mb: 1
151
- }}>
152
- <Typography variant="body2" sx={{ color: '#fff' }}>
153
- {t('automation.common.status')}: {data.formData.executionResult.success ? t('automation.common.success') : t('automation.common.failed')}
154
- </Typography>
155
- <Typography variant="body2" sx={{ color: '#fff' }}>
156
- {t('automation.common.timestamp')}: {new Date(data.formData.executionResult.timestamp).toLocaleString()}
157
- </Typography>
158
- {data.formData.executionResult.error && (
159
- <Typography variant="body2" sx={{ color: '#fff' }}>
160
- {t('automation.common.error')}: {data.formData.executionResult.error}
161
- </Typography>
162
- )}
163
- </Box>
164
- <Typography variant="h6" sx={{ color: '#fff', mb: 1 }}>
165
- {t('automation.startNode.initialContext')}
166
- </Typography>
167
- <ReactJson
168
- theme={'monokai'}
169
- src={data.formData.executionResult.data}
170
- collapsed={false}
171
- />
172
- </Box>
173
- )}
174
-
175
- {/* Show full node data */}
176
- <Typography variant="h6" sx={{ color: '#fff', mb: 1 }}>
177
- {t('automation.common.fullNodeData')}
178
- </Typography>
179
- <ReactJson theme={'monokai'} src={data.formData || data} collapsed={false} />
180
- </CardContent>
181
- </Card>
182
- );
183
- } else {
184
- // Clean up when closing
185
- if (rootRef.current) {
186
- rootRef.current.unmount();
187
- rootRef.current = null;
188
- }
189
- if (portalRef.current) {
190
- document.body.removeChild(portalRef.current);
191
- portalRef.current = null;
192
- }
193
- }
194
- }, [isJsonOpen, data]);
195
-
196
- return (
197
- <Box
198
- ref={nodeRef}
199
- sx={{
200
- width: '336px',
201
- minHeight: '150px',
202
- backgroundColor: '#181C25', // New background color from image
203
- border: selected ? '2px solid #3b82f6' : '1px solid #1e293b',
204
- borderRadius: '12px',
205
- color: '#ffffff',
206
- position: 'relative',
207
- boxShadow: selected ? '0 0 0 2px rgba(59, 130, 246, 0.5)' : '0 4px 8px rgba(0, 0, 0, 0.3)',
208
- transition: 'all 0.2s ease',
209
- cursor: 'pointer',
210
- overflow: 'hidden',
211
- ...(data.status === 'Running' && {
212
- animation: 'pulse-glow 2s ease-in-out infinite',
213
- '@keyframes pulse-glow': {
214
- '0%, 100%': {
215
- boxShadow: '0 0 20px rgba(59, 130, 246, 0.4), 0 0 40px rgba(59, 130, 246, 0.2)',
216
- borderColor: 'rgba(59, 130, 246, 0.6)',
217
- },
218
- '50%': {
219
- boxShadow: '0 0 30px rgba(59, 130, 246, 0.6), 0 0 60px rgba(59, 130, 246, 0.3)',
220
- borderColor: 'rgba(59, 130, 246, 0.9)',
221
- },
222
- },
223
- }),
224
- }}
225
- onClick={handleJsonClick}
226
- >
227
- {/* Top Header Section */}
228
- <Box sx={{
229
- backgroundColor: "rgba(67, 93, 132, 0.1)",
230
- padding: '8px 16px',
231
- borderRadius: '12px 12px 0 0'
232
- }}>
233
- <Typography variant="body2" sx={{
234
- color: '#ffffff',
235
- fontSize: '12px',
236
- fontWeight: 500
237
- }}>
238
- {data.formData?.description || t('automation.startNode.headerDescription')}
239
- </Typography>
240
- </Box>
241
-
242
- {/* Main Content */}
243
- <Box sx={{ padding: '16px' }}>
244
- {/* Title Section */}
245
- <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
246
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
247
- <Box
248
- sx={{
249
- width: '32px',
250
- height: '32px',
251
- backgroundColor: '#0ea5e9',
252
- borderRadius: '50%',
253
- display: 'flex',
254
- alignItems: 'center',
255
- justifyContent: 'center',
256
- }}
257
- >
258
- <IconComponent sx={{ color: 'white', fontSize: '18px' }} />
259
- </Box>
260
- <Typography variant="h6" sx={{ fontWeight: 600, fontSize: '16px' }}>
261
- {data.label}
262
- </Typography>
263
- </Box>
264
- <Chip
265
- label={data.status || 'Ready'}
266
- size="small"
267
- sx={{
268
- backgroundColor: data.status === 'Completed'
269
- ? 'rgba(37, 99, 235, 0.1)'
270
- : data.status === 'Running'
271
- ? 'rgba(251, 191, 36, 0.1)'
272
- : data.status === 'Error'
273
- ? 'rgba(239, 68, 68, 0.1)'
274
- : 'rgba(16, 185, 129, 0.1)',
275
- color: data.status === 'Completed'
276
- ? '#93C5FD'
277
- : data.status === 'Running'
278
- ? '#FCD34D'
279
- : data.status === 'Error'
280
- ? '#FCA5A5'
281
- : '#86EFAC',
282
- fontWeight: 500,
283
- fontSize: '12px',
284
- height: '24px',
285
- borderRadius: '12px',
286
- }}
287
- />
288
- </Box>
289
-
290
- {/* Description Box */}
291
- <Box sx={{
292
- backgroundColor: '#1F2937',
293
- borderRadius: '8px',
294
- padding: '12px',
295
- mb: 2,
296
- border: '1px solid #374151'
297
- }}>
298
- {/* Inner text boundary box */}
299
- <Box sx={{
300
- backgroundColor: 'transparent',
301
- borderRadius: '4px',
302
- padding: '8px',
303
- border: '1px solid #4B5563', // Light grey border for inner box
304
- minHeight: '40px',
305
- display: 'flex',
306
- alignItems: 'center'
307
- }}>
308
- <Typography variant="body2" sx={{
309
- color: '#9CA3AF',
310
- fontSize: '12px',
311
- lineHeight: 1.4,
312
- margin: 0
313
- }}>
314
- {data.description}
315
- </Typography>
316
- </Box>
317
- </Box>
318
-
319
- {/* Last Run Info */}
320
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mt: 1 }}>
321
- <AccessTimeIcon sx={{ fontSize: '14px', color: '#9CA3AF' }} />
322
- <Typography variant="body2" sx={{ color: '#9CA3AF', fontSize: '11px' }}>
323
- {t('automation.common.lastRan')}: {data.lastRun}
324
- </Typography>
325
- </Box>
326
- </Box>
327
-
328
- {/* Output Handle - Hidden but functional */}
329
- <Handle
330
- type="source"
331
- position={Position.Right}
332
- id="right"
333
- style={{
334
- background: '#3b82f6',
335
- width: '12px',
336
- height: '12px',
337
- border: '2px solid white',
338
- right: '-8px',
339
- opacity: 0, // Hidden but functional
340
- }}
341
- />
342
- </Box>
343
- );
344
- };
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 } from '@mui/material';
5
+ import { AccessTime as AccessTimeIcon } 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 { NodeActionButtons } from './NodeActionButtons';
12
+ import { showNodeAIAssistantPopup } from './NodeAIAssistantPopup';
13
+
14
+ interface AutomationStartNodeProps {
15
+ data: {
16
+ label: string;
17
+ description: string;
18
+ status: 'Ready' | 'Running' | 'Completed' | 'Error';
19
+ triggerType: 'manual' | 'scheduled';
20
+ scheduleConfig?: {
21
+ frequency: 'hourly' | 'daily' | 'weekly';
22
+ time?: string;
23
+ cron?: string;
24
+ };
25
+ lastRun: string;
26
+ backgroundColor: string;
27
+ textColor: string;
28
+ borderColor: string;
29
+ iconName?: string; // Add iconName to the interface
30
+ formData?: any; // Include formData for configuration
31
+ };
32
+ selected?: boolean;
33
+ }
34
+
35
+ export const AutomationStartNode: React.FC<AutomationStartNodeProps> = ({ data, selected }) => {
36
+ const { t } = useTranslation();
37
+ const [isJsonOpen, setIsJsonOpen] = useState(false);
38
+ const rootRef = useRef<any>(null);
39
+ const portalRef = useRef<HTMLDivElement | null>(null);
40
+ const nodeRef = useRef<HTMLDivElement | null>(null);
41
+ const nodeId = useNodeId();
42
+ const setSelectedNode = useDiagram((state) => state.setSelectedNode);
43
+ const enableJson = useDiagram((state) => state.enableNodeJsonPopover ?? true);
44
+ const onNodesChange = useDiagram((state) => state.onNodesChange);
45
+ const nodes = useDiagram((state) => state.nodes);
46
+ const setNodes = useDiagram((state) => state.setNodes);
47
+
48
+ // Get the icon component based on the iconName
49
+ const IconComponent = getIconByName(data.iconName);
50
+
51
+
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.startNode.initialContext')}
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.startNode.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',
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
+ {/* Connection Handles - Bidirectional (source + target at each position) */}
341
+ {/* Top - Source */}
342
+ <Handle
343
+ type="source"
344
+ position={Position.Top}
345
+ id="top-source"
346
+ className="connection-handle"
347
+ style={{
348
+ background: selected ? '#10B981' : '#1a1a2e',
349
+ width: '14px',
350
+ height: '14px',
351
+ border: '3px solid #10B981',
352
+ top: '-8px',
353
+ opacity: selected ? 1 : 0,
354
+ transition: 'all 0.2s ease-in-out',
355
+ cursor: 'crosshair',
356
+ zIndex: 10,
357
+ }}
358
+ />
359
+ {/* Top - Target (hidden but functional) */}
360
+ <Handle
361
+ type="target"
362
+ position={Position.Top}
363
+ id="top-target"
364
+ style={{
365
+ background: 'transparent',
366
+ width: '14px',
367
+ height: '14px',
368
+ border: 'none',
369
+ top: '-8px',
370
+ opacity: 0,
371
+ pointerEvents: selected ? 'all' : 'none',
372
+ }}
373
+ />
374
+ {/* Bottom - Source */}
375
+ <Handle
376
+ type="source"
377
+ position={Position.Bottom}
378
+ id="bottom-source"
379
+ className="connection-handle"
380
+ style={{
381
+ background: selected ? '#10B981' : '#1a1a2e',
382
+ width: '14px',
383
+ height: '14px',
384
+ border: '3px solid #10B981',
385
+ bottom: '-8px',
386
+ opacity: selected ? 1 : 0,
387
+ transition: 'all 0.2s ease-in-out',
388
+ cursor: 'crosshair',
389
+ zIndex: 10,
390
+ }}
391
+ />
392
+ {/* Bottom - Target (hidden but functional) */}
393
+ <Handle
394
+ type="target"
395
+ position={Position.Bottom}
396
+ id="bottom-target"
397
+ style={{
398
+ background: 'transparent',
399
+ width: '14px',
400
+ height: '14px',
401
+ border: 'none',
402
+ bottom: '-8px',
403
+ opacity: 0,
404
+ pointerEvents: selected ? 'all' : 'none',
405
+ }}
406
+ />
407
+ {/* Left - Source */}
408
+ <Handle
409
+ type="source"
410
+ position={Position.Left}
411
+ id="left-source"
412
+ className="connection-handle"
413
+ style={{
414
+ background: selected ? '#10B981' : '#1a1a2e',
415
+ width: '14px',
416
+ height: '14px',
417
+ border: '3px solid #10B981',
418
+ left: '-8px',
419
+ opacity: selected ? 1 : 0,
420
+ transition: 'all 0.2s ease-in-out',
421
+ cursor: 'crosshair',
422
+ zIndex: 10,
423
+ }}
424
+ />
425
+ {/* Left - Target (hidden but functional) */}
426
+ <Handle
427
+ type="target"
428
+ position={Position.Left}
429
+ id="left-target"
430
+ style={{
431
+ background: 'transparent',
432
+ width: '14px',
433
+ height: '14px',
434
+ border: 'none',
435
+ left: '-8px',
436
+ opacity: 0,
437
+ pointerEvents: selected ? 'all' : 'none',
438
+ }}
439
+ />
440
+ {/* Right - Source */}
441
+ <Handle
442
+ type="source"
443
+ position={Position.Right}
444
+ id="right-source"
445
+ className="connection-handle"
446
+ style={{
447
+ background: selected ? '#10B981' : '#1a1a2e',
448
+ width: '14px',
449
+ height: '14px',
450
+ border: '3px solid #10B981',
451
+ right: '-8px',
452
+ opacity: selected ? 1 : 0,
453
+ transition: 'all 0.2s ease-in-out',
454
+ cursor: 'crosshair',
455
+ zIndex: 10,
456
+ }}
457
+ />
458
+ {/* Right - Target (hidden but functional) */}
459
+ <Handle
460
+ type="target"
461
+ position={Position.Right}
462
+ id="right-target"
463
+ style={{
464
+ background: 'transparent',
465
+ width: '14px',
466
+ height: '14px',
467
+ border: 'none',
468
+ right: '-8px',
469
+ opacity: 0,
470
+ pointerEvents: selected ? 'all' : 'none',
471
+ }}
472
+ />
473
+
474
+ </Box>
475
+
476
+ {/* Node Action Buttons - Shows when selected */}
477
+ <NodeActionButtons
478
+ selected={selected}
479
+ onOpenAIAssistant={(buttonElement) => {
480
+ if (nodeId) {
481
+ showNodeAIAssistantPopup(nodeId, 'Start Node', buttonElement);
482
+ }
483
+ }}
484
+ onDelete={() => {
485
+ if (nodeId && onNodesChange) {
486
+ onNodesChange([{ id: nodeId, type: 'remove' }]);
487
+ }
488
+ }}
489
+ onDuplicate={() => {
490
+ if (nodeId) {
491
+ const currentNode = nodes.find(n => n.id === nodeId);
492
+ if (currentNode) {
493
+ const newNode = {
494
+ ...currentNode,
495
+ id: `${currentNode.id}-copy-${Date.now()}`,
496
+ position: {
497
+ x: currentNode.position.x + 50,
498
+ y: currentNode.position.y + 50,
499
+ },
500
+ selected: false,
501
+ };
502
+ setNodes([...nodes, newNode]);
503
+ }
504
+ }
505
+ }}
506
+ />
507
+ </Box>
508
+ );
509
+ };