@flowuent-org/diagramming-core 1.2.0 → 1.2.1

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.
@@ -1,420 +1,414 @@
1
- import React, { useEffect, useRef, useState } from 'react';
2
- import { Handle, Position, useNodeId } from '@xyflow/react';
3
- import { Box, Typography, Button } from '@mui/material';
4
- import { Description as DescriptionIcon, Lightbulb as LightbulbIcon } from '@mui/icons-material';
5
- import { getIconByName } from '../../utils/iconMapper';
6
- import { AISuggestion } from './AISuggestionsModal';
7
- import { AISuggestionsPanel } from './AISuggestionsPanel';
8
- import { NodeActionButtons } from './NodeActionButtons';
9
- import { showNodeAIAssistantPopup } from './NodeAIAssistantPopup';
10
- import { useDiagram } from '../../contexts/DiagramProvider';
11
-
12
- interface AutomationNoteNodeProps {
13
- data: {
14
- label: string;
15
- description: string;
16
- backgroundColor?: string;
17
- textColor?: string;
18
- borderColor?: string;
19
- iconName?: string;
20
- noteType?: 'info' | 'warning' | 'note' | 'purpose';
21
- formData?: {
22
- aiSuggestionsCount?: number; // Number of AI suggestions available
23
- [key: string]: any;
24
- };
25
- };
26
- selected?: boolean;
27
- }
28
-
29
- export const AutomationNoteNode: React.FC<AutomationNoteNodeProps> = ({ data, selected }) => {
30
- const nodeRef = useRef<HTMLDivElement | null>(null);
31
- const [showSuggestions, setShowSuggestions] = useState(false);
32
- const nodeId = useNodeId();
33
- const onNodesChange = useDiagram((state) => state.onNodesChange);
34
- const nodes = useDiagram((state) => state.nodes);
35
- const setNodes = useDiagram((state) => state.setNodes);
36
-
37
- // Get the icon component based on the iconName, default to DescriptionIcon
38
- const IconComponent = getIconByName(data.iconName || 'Description');
39
-
40
- // Use consistent colors with other automation nodes
41
- const colors = {
42
- backgroundColor: '#181C25', // Default automation node background
43
- textColor: '#ffffff',
44
- borderColor: '#1e293b',
45
- iconBg: '#3b82f6' // Default blue for icon background
46
- };
47
-
48
- // Debug logging for node dimensions
49
- useEffect(() => {
50
- if (nodeRef.current) {
51
- const rect = nodeRef.current.getBoundingClientRect();
52
- const computedStyle = window.getComputedStyle(nodeRef.current);
53
- console.log('AutomationNoteNode Debug:', {
54
- nodeId: data.label,
55
- actualDimensions: {
56
- width: rect.width,
57
- height: rect.height,
58
- left: rect.left,
59
- top: rect.top
60
- },
61
- computedStyle: {
62
- width: computedStyle.width,
63
- height: computedStyle.height,
64
- padding: computedStyle.padding,
65
- border: computedStyle.border,
66
- boxShadow: computedStyle.boxShadow,
67
- margin: computedStyle.margin
68
- },
69
- handlePositions: {
70
- left: '-8px',
71
- right: '-8px'
72
- }
73
- });
74
- }
75
- }, [data.label]);
76
-
77
- return (
78
- <Box
79
- sx={{
80
- position: 'relative',
81
- width: '336px',
82
- overflow: 'visible',
83
- }}
84
- >
85
- <Box
86
- ref={nodeRef}
87
- sx={{
88
- width: '336px',
89
- minHeight: '150px',
90
- backgroundColor: colors.backgroundColor,
91
- border: selected ? `2px solid ${colors.borderColor}` : `1px solid ${colors.borderColor}`,
92
- borderRadius: '12px',
93
- padding: '0',
94
- color: colors.textColor,
95
- position: 'relative',
96
- boxShadow: selected ? `0 0 0 2px rgba(59, 130, 246, 0.5)` : '0 4px 8px rgba(0, 0, 0, 0.3)',
97
- transition: 'all 0.2s ease',
98
- cursor: 'pointer',
99
- overflow: 'hidden',
100
- }}
101
- >
102
- {/* Header */}
103
- <Box sx={{
104
- display: 'flex',
105
- alignItems: 'center',
106
- justifyContent: 'space-between',
107
- p: 2,
108
- borderBottom: '1px solid rgba(255, 255, 255, 0.1)'
109
- }}>
110
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
111
- <Box
112
- sx={{
113
- width: '32px',
114
- height: '32px',
115
- backgroundColor: colors.iconBg,
116
- borderRadius: '6px',
117
- display: 'flex',
118
- alignItems: 'center',
119
- justifyContent: 'center',
120
- }}
121
- >
122
- <IconComponent sx={{ color: 'white', fontSize: '18px' }} />
123
- </Box>
124
- <Typography variant="h6" sx={{ fontWeight: 600, fontSize: '16px' }}>
125
- {data.label}
126
- </Typography>
127
- </Box>
128
- </Box>
129
-
130
- {/* Content */}
131
- <Box sx={{ p: 2 }}>
132
- <Typography variant="body2" sx={{
133
- color: colors.textColor,
134
- fontSize: '14px',
135
- lineHeight: 1.5,
136
- opacity: 0.9
137
- }}>
138
- {data.description}
139
- </Typography>
140
- </Box>
141
-
142
- {/* Note Type Indicator */}
143
- <Box sx={{
144
- position: 'absolute',
145
- top: '8px',
146
- right: '8px',
147
- px: 1,
148
- py: 0.5,
149
- backgroundColor: 'rgba(255, 255, 255, 0.2)',
150
- borderRadius: '4px',
151
- fontSize: '10px',
152
- fontWeight: 500,
153
- textTransform: 'uppercase',
154
- letterSpacing: '0.5px'
155
- }}>
156
- {data.noteType || 'note'}
157
- </Box>
158
-
159
- {/* Connection Handles - Bidirectional (source + target at each position) */}
160
- {/* Top - Source */}
161
- <Handle
162
- type="source"
163
- position={Position.Top}
164
- id="top-source"
165
- className="connection-handle"
166
- style={{
167
- background: selected ? '#10B981' : '#1a1a2e',
168
- width: '14px',
169
- height: '14px',
170
- border: '3px solid #10B981',
171
- top: '-8px',
172
- opacity: selected ? 1 : 0,
173
- transition: 'all 0.2s ease-in-out',
174
- cursor: 'crosshair',
175
- zIndex: 10,
176
- }}
177
- />
178
- {/* Top - Target (hidden but functional) */}
179
- <Handle
180
- type="target"
181
- position={Position.Top}
182
- id="top-target"
183
- style={{
184
- background: 'transparent',
185
- width: '14px',
186
- height: '14px',
187
- border: 'none',
188
- top: '-8px',
189
- opacity: 0,
190
- pointerEvents: selected ? 'all' : 'none',
191
- }}
192
- />
193
- {/* Bottom - Source */}
194
- <Handle
195
- type="source"
196
- position={Position.Bottom}
197
- id="bottom-source"
198
- className="connection-handle"
199
- style={{
200
- background: selected ? '#10B981' : '#1a1a2e',
201
- width: '14px',
202
- height: '14px',
203
- border: '3px solid #10B981',
204
- bottom: '-8px',
205
- opacity: selected ? 1 : 0,
206
- transition: 'all 0.2s ease-in-out',
207
- cursor: 'crosshair',
208
- zIndex: 10,
209
- }}
210
- />
211
- {/* Bottom - Target (hidden but functional) */}
212
- <Handle
213
- type="target"
214
- position={Position.Bottom}
215
- id="bottom-target"
216
- style={{
217
- background: 'transparent',
218
- width: '14px',
219
- height: '14px',
220
- border: 'none',
221
- bottom: '-8px',
222
- opacity: 0,
223
- pointerEvents: selected ? 'all' : 'none',
224
- }}
225
- />
226
- {/* Left - Source */}
227
- <Handle
228
- type="source"
229
- position={Position.Left}
230
- id="left-source"
231
- className="connection-handle"
232
- style={{
233
- background: selected ? '#10B981' : '#1a1a2e',
234
- width: '14px',
235
- height: '14px',
236
- border: '3px solid #10B981',
237
- left: '-8px',
238
- opacity: selected ? 1 : 0,
239
- transition: 'all 0.2s ease-in-out',
240
- cursor: 'crosshair',
241
- zIndex: 10,
242
- }}
243
- />
244
- {/* Left - Target (hidden but functional) */}
245
- <Handle
246
- type="target"
247
- position={Position.Left}
248
- id="left-target"
249
- style={{
250
- background: 'transparent',
251
- width: '14px',
252
- height: '14px',
253
- border: 'none',
254
- left: '-8px',
255
- opacity: 0,
256
- pointerEvents: selected ? 'all' : 'none',
257
- }}
258
- />
259
- {/* Right - Source */}
260
- <Handle
261
- type="source"
262
- position={Position.Right}
263
- id="right-source"
264
- className="connection-handle"
265
- style={{
266
- background: selected ? '#10B981' : '#1a1a2e',
267
- width: '14px',
268
- height: '14px',
269
- border: '3px solid #10B981',
270
- right: '-8px',
271
- opacity: selected ? 1 : 0,
272
- transition: 'all 0.2s ease-in-out',
273
- cursor: 'crosshair',
274
- zIndex: 10,
275
- }}
276
- />
277
- {/* Right - Target (hidden but functional) */}
278
- <Handle
279
- type="target"
280
- position={Position.Right}
281
- id="right-target"
282
- style={{
283
- background: 'transparent',
284
- width: '14px',
285
- height: '14px',
286
- border: 'none',
287
- right: '-8px',
288
- opacity: 0,
289
- pointerEvents: selected ? 'all' : 'none',
290
- }}
291
- />
292
-
293
- </Box>
294
-
295
- {/* Node Action Buttons - Shows when selected */}
296
- <NodeActionButtons
297
- selected={selected}
298
- onOpenAIAssistant={(buttonElement) => {
299
- if (nodeId) {
300
- showNodeAIAssistantPopup(nodeId, 'Note Node', buttonElement);
301
- }
302
- }}
303
- onDelete={() => {
304
- if (nodeId && onNodesChange) {
305
- onNodesChange([{ id: nodeId, type: 'remove' }]);
306
- }
307
- }}
308
- onDuplicate={() => {
309
- if (nodeId) {
310
- const currentNode = nodes.find(n => n.id === nodeId);
311
- if (currentNode) {
312
- const newNode = {
313
- ...currentNode,
314
- id: `${currentNode.id}-copy-${Date.now()}`,
315
- position: {
316
- x: currentNode.position.x + 50,
317
- y: currentNode.position.y + 50,
318
- },
319
- selected: false,
320
- };
321
- setNodes([...nodes, newNode]);
322
- }
323
- }
324
- }}
325
- />
326
-
327
- {/* AI Suggestions Button - Positioned below the node box */}
328
- {data.formData?.aiSuggestionsCount !== undefined && data.formData.aiSuggestionsCount > 0 && (
329
- <Box
330
- sx={{
331
- position: 'absolute',
332
- top: '100%',
333
- left: '50%',
334
- transform: 'translateX(-50%)',
335
- marginTop: '12px',
336
- zIndex: 10,
337
- whiteSpace: 'nowrap',
338
- }}
339
- onClick={(e) => {
340
- e.stopPropagation();
341
- // Toggle AI Suggestions panel
342
- setShowSuggestions(!showSuggestions);
343
- }}
344
- >
345
- <Button
346
- variant="contained"
347
- startIcon={<LightbulbIcon sx={{ fontSize: '12px' }} />}
348
- sx={{
349
- backgroundColor: '#2563EB',
350
- color: '#ffffff',
351
- borderRadius: '20px',
352
- textTransform: 'none',
353
- fontSize: '10px',
354
- fontWeight: 400,
355
- padding: '8px 16px',
356
- whiteSpace: 'nowrap',
357
- display: 'inline-flex',
358
- alignItems: 'center',
359
- boxShadow: '0 2px 8px rgba(0, 0, 0, 0.3)',
360
- '&:hover': {
361
- backgroundColor: '#2563eb',
362
- },
363
- '& .MuiButton-startIcon': {
364
- marginRight: '8px',
365
- }
366
- }}
367
- >
368
- AI Suggestions
369
- <Box
370
- component="span"
371
- sx={{
372
- marginLeft: '8px',
373
- backgroundColor: '#FFFFFF26',
374
- color: '#ffffff',
375
- fontSize: '10px',
376
- fontWeight: 400,
377
- minWidth: '18px',
378
- height: '18px',
379
- borderRadius: '9px',
380
- display: 'inline-flex',
381
- alignItems: 'center',
382
- justifyContent: 'center',
383
- padding: '0 6px',
384
- border: '1px solid rgba(255, 255, 255, 0.2)',
385
- }}
386
- >
387
- {data.formData.aiSuggestionsCount}
388
- </Box>
389
- </Button>
390
- </Box>
391
- )}
392
-
393
- {/* AI Suggestions Panel - Rendered on canvas below the button */}
394
- {showSuggestions && data.formData?.aiSuggestionsCount !== undefined && data.formData.aiSuggestionsCount > 0 && nodeId && (
395
- <AISuggestionsPanel
396
- suggestions={data.formData?.aiSuggestions || [
397
- {
398
- id: '1',
399
- title: 'Add Citation Extraction',
400
- description: 'Automatically extract and format citations from article content.',
401
- tags: ['classification', 'enhancement'],
402
- },
403
- {
404
- id: '2',
405
- title: 'Generate Bullet Summary',
406
- description: 'Create a concise bullet-point summary of the article\'s main points.',
407
- tags: ['classification', 'enhancement'],
408
- },
409
- ]}
410
- parentNodeId={nodeId}
411
- onSuggestionClick={(suggestion) => {
412
- console.log('Suggestion clicked:', suggestion);
413
- // Handle suggestion selection here
414
- }}
415
- onClose={() => setShowSuggestions(false)}
416
- />
417
- )}
418
- </Box>
419
- );
420
- };
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { Handle, Position, useNodeId } from '@xyflow/react';
3
+ import { Box, Typography, Button } from '@mui/material';
4
+ import { Description as DescriptionIcon, Lightbulb as LightbulbIcon } from '@mui/icons-material';
5
+ import { getIconByName } from '../../utils/iconMapper';
6
+ import { AISuggestion } from './AISuggestionsModal';
7
+ import { AISuggestionsPanel } from './AISuggestionsPanel';
8
+ import { NodeActionButtons } from './NodeActionButtons';
9
+ import { useDiagram } from '../../contexts/DiagramProvider';
10
+
11
+ interface AutomationNoteNodeProps {
12
+ data: {
13
+ label: string;
14
+ description: string;
15
+ backgroundColor?: string;
16
+ textColor?: string;
17
+ borderColor?: string;
18
+ iconName?: string;
19
+ noteType?: 'info' | 'warning' | 'note' | 'purpose';
20
+ formData?: {
21
+ aiSuggestionsCount?: number; // Number of AI suggestions available
22
+ [key: string]: any;
23
+ };
24
+ };
25
+ selected?: boolean;
26
+ }
27
+
28
+ export const AutomationNoteNode: React.FC<AutomationNoteNodeProps> = ({ data, selected }) => {
29
+ const nodeRef = useRef<HTMLDivElement | null>(null);
30
+ const [showSuggestions, setShowSuggestions] = useState(false);
31
+ const nodeId = useNodeId();
32
+ const onNodesChange = useDiagram((state) => state.onNodesChange);
33
+ const nodes = useDiagram((state) => state.nodes);
34
+ const setNodes = useDiagram((state) => state.setNodes);
35
+
36
+ // Get the icon component based on the iconName, default to DescriptionIcon
37
+ const IconComponent = getIconByName(data.iconName || 'Description');
38
+
39
+ // Use consistent colors with other automation nodes
40
+ const colors = {
41
+ backgroundColor: '#181C25', // Default automation node background
42
+ textColor: '#ffffff',
43
+ borderColor: '#1e293b',
44
+ iconBg: '#3b82f6' // Default blue for icon background
45
+ };
46
+
47
+ // Debug logging for node dimensions
48
+ useEffect(() => {
49
+ if (nodeRef.current) {
50
+ const rect = nodeRef.current.getBoundingClientRect();
51
+ const computedStyle = window.getComputedStyle(nodeRef.current);
52
+ console.log('AutomationNoteNode Debug:', {
53
+ nodeId: data.label,
54
+ actualDimensions: {
55
+ width: rect.width,
56
+ height: rect.height,
57
+ left: rect.left,
58
+ top: rect.top
59
+ },
60
+ computedStyle: {
61
+ width: computedStyle.width,
62
+ height: computedStyle.height,
63
+ padding: computedStyle.padding,
64
+ border: computedStyle.border,
65
+ boxShadow: computedStyle.boxShadow,
66
+ margin: computedStyle.margin
67
+ },
68
+ handlePositions: {
69
+ left: '-8px',
70
+ right: '-8px'
71
+ }
72
+ });
73
+ }
74
+ }, [data.label]);
75
+
76
+ return (
77
+ <Box
78
+ sx={{
79
+ position: 'relative',
80
+ width: '336px',
81
+ overflow: 'visible',
82
+ }}
83
+ >
84
+ <Box
85
+ ref={nodeRef}
86
+ sx={{
87
+ width: '336px',
88
+ minHeight: '150px',
89
+ backgroundColor: colors.backgroundColor,
90
+ border: selected ? `2px solid ${colors.borderColor}` : `1px solid ${colors.borderColor}`,
91
+ borderRadius: '12px',
92
+ padding: '0',
93
+ color: colors.textColor,
94
+ position: 'relative',
95
+ boxShadow: selected ? `0 0 0 2px rgba(59, 130, 246, 0.5)` : '0 4px 8px rgba(0, 0, 0, 0.3)',
96
+ transition: 'all 0.2s ease',
97
+ cursor: 'pointer',
98
+ overflow: 'hidden',
99
+ }}
100
+ >
101
+ {/* Header */}
102
+ <Box sx={{
103
+ display: 'flex',
104
+ alignItems: 'center',
105
+ justifyContent: 'space-between',
106
+ p: 2,
107
+ borderBottom: '1px solid rgba(255, 255, 255, 0.1)'
108
+ }}>
109
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
110
+ <Box
111
+ sx={{
112
+ width: '32px',
113
+ height: '32px',
114
+ backgroundColor: colors.iconBg,
115
+ borderRadius: '6px',
116
+ display: 'flex',
117
+ alignItems: 'center',
118
+ justifyContent: 'center',
119
+ }}
120
+ >
121
+ <IconComponent sx={{ color: 'white', fontSize: '18px' }} />
122
+ </Box>
123
+ <Typography variant="h6" sx={{ fontWeight: 600, fontSize: '16px' }}>
124
+ {data.label}
125
+ </Typography>
126
+ </Box>
127
+ </Box>
128
+
129
+ {/* Content */}
130
+ <Box sx={{ p: 2 }}>
131
+ <Typography variant="body2" sx={{
132
+ color: colors.textColor,
133
+ fontSize: '14px',
134
+ lineHeight: 1.5,
135
+ opacity: 0.9
136
+ }}>
137
+ {data.description}
138
+ </Typography>
139
+ </Box>
140
+
141
+ {/* Note Type Indicator */}
142
+ <Box sx={{
143
+ position: 'absolute',
144
+ top: '8px',
145
+ right: '8px',
146
+ px: 1,
147
+ py: 0.5,
148
+ backgroundColor: 'rgba(255, 255, 255, 0.2)',
149
+ borderRadius: '4px',
150
+ fontSize: '10px',
151
+ fontWeight: 500,
152
+ textTransform: 'uppercase',
153
+ letterSpacing: '0.5px'
154
+ }}>
155
+ {data.noteType || 'note'}
156
+ </Box>
157
+
158
+ {/* Connection Handles - Bidirectional (source + target at each position) */}
159
+ {/* Top - Source */}
160
+ <Handle
161
+ type="source"
162
+ position={Position.Top}
163
+ id="top-source"
164
+ className="connection-handle"
165
+ style={{
166
+ background: selected ? '#10B981' : '#1a1a2e',
167
+ width: '14px',
168
+ height: '14px',
169
+ border: '3px solid #10B981',
170
+ top: '-8px',
171
+ opacity: selected ? 1 : 0,
172
+ transition: 'all 0.2s ease-in-out',
173
+ cursor: 'crosshair',
174
+ zIndex: 10,
175
+ }}
176
+ />
177
+ {/* Top - Target (hidden but functional) */}
178
+ <Handle
179
+ type="target"
180
+ position={Position.Top}
181
+ id="top-target"
182
+ style={{
183
+ background: 'transparent',
184
+ width: '14px',
185
+ height: '14px',
186
+ border: 'none',
187
+ top: '-8px',
188
+ opacity: 0,
189
+ pointerEvents: selected ? 'all' : 'none',
190
+ }}
191
+ />
192
+ {/* Bottom - Source */}
193
+ <Handle
194
+ type="source"
195
+ position={Position.Bottom}
196
+ id="bottom-source"
197
+ className="connection-handle"
198
+ style={{
199
+ background: selected ? '#10B981' : '#1a1a2e',
200
+ width: '14px',
201
+ height: '14px',
202
+ border: '3px solid #10B981',
203
+ bottom: '-8px',
204
+ opacity: selected ? 1 : 0,
205
+ transition: 'all 0.2s ease-in-out',
206
+ cursor: 'crosshair',
207
+ zIndex: 10,
208
+ }}
209
+ />
210
+ {/* Bottom - Target (hidden but functional) */}
211
+ <Handle
212
+ type="target"
213
+ position={Position.Bottom}
214
+ id="bottom-target"
215
+ style={{
216
+ background: 'transparent',
217
+ width: '14px',
218
+ height: '14px',
219
+ border: 'none',
220
+ bottom: '-8px',
221
+ opacity: 0,
222
+ pointerEvents: selected ? 'all' : 'none',
223
+ }}
224
+ />
225
+ {/* Left - Source */}
226
+ <Handle
227
+ type="source"
228
+ position={Position.Left}
229
+ id="left-source"
230
+ className="connection-handle"
231
+ style={{
232
+ background: selected ? '#10B981' : '#1a1a2e',
233
+ width: '14px',
234
+ height: '14px',
235
+ border: '3px solid #10B981',
236
+ left: '-8px',
237
+ opacity: selected ? 1 : 0,
238
+ transition: 'all 0.2s ease-in-out',
239
+ cursor: 'crosshair',
240
+ zIndex: 10,
241
+ }}
242
+ />
243
+ {/* Left - Target (hidden but functional) */}
244
+ <Handle
245
+ type="target"
246
+ position={Position.Left}
247
+ id="left-target"
248
+ style={{
249
+ background: 'transparent',
250
+ width: '14px',
251
+ height: '14px',
252
+ border: 'none',
253
+ left: '-8px',
254
+ opacity: 0,
255
+ pointerEvents: selected ? 'all' : 'none',
256
+ }}
257
+ />
258
+ {/* Right - Source */}
259
+ <Handle
260
+ type="source"
261
+ position={Position.Right}
262
+ id="right-source"
263
+ className="connection-handle"
264
+ style={{
265
+ background: selected ? '#10B981' : '#1a1a2e',
266
+ width: '14px',
267
+ height: '14px',
268
+ border: '3px solid #10B981',
269
+ right: '-8px',
270
+ opacity: selected ? 1 : 0,
271
+ transition: 'all 0.2s ease-in-out',
272
+ cursor: 'crosshair',
273
+ zIndex: 10,
274
+ }}
275
+ />
276
+ {/* Right - Target (hidden but functional) */}
277
+ <Handle
278
+ type="target"
279
+ position={Position.Right}
280
+ id="right-target"
281
+ style={{
282
+ background: 'transparent',
283
+ width: '14px',
284
+ height: '14px',
285
+ border: 'none',
286
+ right: '-8px',
287
+ opacity: 0,
288
+ pointerEvents: selected ? 'all' : 'none',
289
+ }}
290
+ />
291
+
292
+ </Box>
293
+
294
+ {/* Node Action Buttons - Shows when selected */}
295
+ <NodeActionButtons
296
+ selected={selected}
297
+ onDelete={() => {
298
+ if (nodeId && onNodesChange) {
299
+ onNodesChange([{ id: nodeId, type: 'remove' }]);
300
+ }
301
+ }}
302
+ onDuplicate={() => {
303
+ if (nodeId) {
304
+ const currentNode = nodes.find(n => n.id === nodeId);
305
+ if (currentNode) {
306
+ const newNode = {
307
+ ...currentNode,
308
+ id: `${currentNode.id}-copy-${Date.now()}`,
309
+ position: {
310
+ x: currentNode.position.x + 50,
311
+ y: currentNode.position.y + 50,
312
+ },
313
+ selected: false,
314
+ };
315
+ setNodes([...nodes, newNode]);
316
+ }
317
+ }
318
+ }}
319
+ />
320
+
321
+ {/* AI Suggestions Button - Positioned below the node box */}
322
+ {data.formData?.aiSuggestionsCount !== undefined && data.formData.aiSuggestionsCount > 0 && (
323
+ <Box
324
+ sx={{
325
+ position: 'absolute',
326
+ top: '100%',
327
+ left: '50%',
328
+ transform: 'translateX(-50%)',
329
+ marginTop: '12px',
330
+ zIndex: 10,
331
+ whiteSpace: 'nowrap',
332
+ }}
333
+ onClick={(e) => {
334
+ e.stopPropagation();
335
+ // Toggle AI Suggestions panel
336
+ setShowSuggestions(!showSuggestions);
337
+ }}
338
+ >
339
+ <Button
340
+ variant="contained"
341
+ startIcon={<LightbulbIcon sx={{ fontSize: '12px' }} />}
342
+ sx={{
343
+ backgroundColor: '#2563EB',
344
+ color: '#ffffff',
345
+ borderRadius: '20px',
346
+ textTransform: 'none',
347
+ fontSize: '10px',
348
+ fontWeight: 400,
349
+ padding: '8px 16px',
350
+ whiteSpace: 'nowrap',
351
+ display: 'inline-flex',
352
+ alignItems: 'center',
353
+ boxShadow: '0 2px 8px rgba(0, 0, 0, 0.3)',
354
+ '&:hover': {
355
+ backgroundColor: '#2563eb',
356
+ },
357
+ '& .MuiButton-startIcon': {
358
+ marginRight: '8px',
359
+ }
360
+ }}
361
+ >
362
+ AI Suggestions
363
+ <Box
364
+ component="span"
365
+ sx={{
366
+ marginLeft: '8px',
367
+ backgroundColor: '#FFFFFF26',
368
+ color: '#ffffff',
369
+ fontSize: '10px',
370
+ fontWeight: 400,
371
+ minWidth: '18px',
372
+ height: '18px',
373
+ borderRadius: '9px',
374
+ display: 'inline-flex',
375
+ alignItems: 'center',
376
+ justifyContent: 'center',
377
+ padding: '0 6px',
378
+ border: '1px solid rgba(255, 255, 255, 0.2)',
379
+ }}
380
+ >
381
+ {data.formData.aiSuggestionsCount}
382
+ </Box>
383
+ </Button>
384
+ </Box>
385
+ )}
386
+
387
+ {/* AI Suggestions Panel - Rendered on canvas below the button */}
388
+ {showSuggestions && data.formData?.aiSuggestionsCount !== undefined && data.formData.aiSuggestionsCount > 0 && nodeId && (
389
+ <AISuggestionsPanel
390
+ suggestions={data.formData?.aiSuggestions || [
391
+ {
392
+ id: '1',
393
+ title: 'Add Citation Extraction',
394
+ description: 'Automatically extract and format citations from article content.',
395
+ tags: ['classification', 'enhancement'],
396
+ },
397
+ {
398
+ id: '2',
399
+ title: 'Generate Bullet Summary',
400
+ description: 'Create a concise bullet-point summary of the article\'s main points.',
401
+ tags: ['classification', 'enhancement'],
402
+ },
403
+ ]}
404
+ parentNodeId={nodeId}
405
+ onSuggestionClick={(suggestion) => {
406
+ console.log('Suggestion clicked:', suggestion);
407
+ // Handle suggestion selection here
408
+ }}
409
+ onClose={() => setShowSuggestions(false)}
410
+ />
411
+ )}
412
+ </Box>
413
+ );
414
+ };