@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.
- package/package.json +116 -116
- package/packages/diagrams/src/index.ts +1 -0
- package/packages/diagrams/src/lib/atoms/ConnectionPoints.tsx +149 -0
- package/packages/diagrams/src/lib/components/automation/AutomationApiNode.tsx +794 -650
- package/packages/diagrams/src/lib/components/automation/AutomationEndNode.tsx +606 -449
- package/packages/diagrams/src/lib/components/automation/AutomationFormattingNode.tsx +831 -687
- package/packages/diagrams/src/lib/components/automation/AutomationNoteNode.tsx +420 -275
- package/packages/diagrams/src/lib/components/automation/AutomationSheetsNode.tsx +1118 -974
- package/packages/diagrams/src/lib/components/automation/AutomationStartNode.tsx +509 -344
- package/packages/diagrams/src/lib/components/automation/NodeAIAssistantPopup.tsx +504 -0
- package/packages/diagrams/src/lib/components/automation/NodeActionButtons.tsx +146 -0
- package/packages/diagrams/src/lib/components/automation/index.ts +20 -11
- package/packages/diagrams/src/lib/molecules/SideHandles.tsx +177 -12
- package/packages/diagrams/src/lib/organisms/CustomEdge/custom-edge-generator.tsx +10 -5
- package/packages/diagrams/src/lib/styles.css +53 -0
- package/packages/diagrams/src/lib/templates/DiagramContainer.tsx +59 -0
- package/packages/diagrams/src/lib/templates/Diagramming.tsx +246 -204
- package/packages/diagrams/src/lib/types/edge-types.ts +17 -0
- package/packages/diagrams/src/lib/utils/generateEdgesFromNodeOrder.ts +113 -0
- package/packages/diagrams/src/lib/utils/nodeAIAssistantConfig.ts +54 -0
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import {
|
|
4
|
+
Box,
|
|
5
|
+
Card,
|
|
6
|
+
CardContent,
|
|
7
|
+
IconButton,
|
|
8
|
+
TextField,
|
|
9
|
+
Typography,
|
|
10
|
+
CircularProgress,
|
|
11
|
+
Alert,
|
|
12
|
+
} from '@mui/material';
|
|
13
|
+
import { RiCloseLine, RiSendPlaneFill } from 'react-icons/ri';
|
|
14
|
+
import { IconMessage } from '@tabler/icons-react';
|
|
15
|
+
import {
|
|
16
|
+
getNodeAIAssistantEndpoint,
|
|
17
|
+
getNodeAIAssistantHeaders,
|
|
18
|
+
} from '../../utils/nodeAIAssistantConfig';
|
|
19
|
+
|
|
20
|
+
interface Message {
|
|
21
|
+
id: string;
|
|
22
|
+
role: 'user' | 'assistant';
|
|
23
|
+
content: string;
|
|
24
|
+
timestamp: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface NodeAIAssistantPopupProps {
|
|
28
|
+
nodeId: string;
|
|
29
|
+
nodeType: string;
|
|
30
|
+
position: { x: number; y: number };
|
|
31
|
+
onClose: () => void;
|
|
32
|
+
apiEndpoint?: string;
|
|
33
|
+
apiHeaders?: Record<string, string>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const NodeAIAssistantPopup: React.FC<NodeAIAssistantPopupProps> = ({
|
|
37
|
+
nodeId,
|
|
38
|
+
nodeType,
|
|
39
|
+
position,
|
|
40
|
+
onClose,
|
|
41
|
+
apiEndpoint,
|
|
42
|
+
apiHeaders,
|
|
43
|
+
}) => {
|
|
44
|
+
// Use provided endpoint/headers or fall back to global configuration
|
|
45
|
+
const effectiveEndpoint = apiEndpoint || getNodeAIAssistantEndpoint();
|
|
46
|
+
const effectiveHeaders = apiHeaders || getNodeAIAssistantHeaders();
|
|
47
|
+
const [messages, setMessages] = useState<Message[]>([
|
|
48
|
+
{
|
|
49
|
+
id: '1',
|
|
50
|
+
role: 'assistant',
|
|
51
|
+
content: `Hello! I'm your AI assistant for this ${nodeType} node. How can I help you?`,
|
|
52
|
+
timestamp: new Date().toLocaleTimeString(),
|
|
53
|
+
},
|
|
54
|
+
]);
|
|
55
|
+
const [inputValue, setInputValue] = useState('');
|
|
56
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
57
|
+
const [error, setError] = useState<string | null>(null);
|
|
58
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
59
|
+
const popupRef = useRef<HTMLDivElement>(null);
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
63
|
+
}, [messages]);
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
67
|
+
const target = event.target as Element;
|
|
68
|
+
if (
|
|
69
|
+
popupRef.current &&
|
|
70
|
+
!popupRef.current.contains(target) &&
|
|
71
|
+
!target.closest('[data-node-ai-assistant-button]')
|
|
72
|
+
) {
|
|
73
|
+
onClose();
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
setTimeout(() => {
|
|
78
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
79
|
+
}, 0);
|
|
80
|
+
|
|
81
|
+
return () => {
|
|
82
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
83
|
+
};
|
|
84
|
+
}, [onClose]);
|
|
85
|
+
|
|
86
|
+
const handleSend = async () => {
|
|
87
|
+
if (!inputValue.trim() || isLoading) return;
|
|
88
|
+
|
|
89
|
+
const userMessage: Message = {
|
|
90
|
+
id: Date.now().toString(),
|
|
91
|
+
role: 'user',
|
|
92
|
+
content: inputValue,
|
|
93
|
+
timestamp: new Date().toLocaleTimeString(),
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
setMessages((prev) => [...prev, userMessage]);
|
|
97
|
+
const currentInput = inputValue;
|
|
98
|
+
setInputValue('');
|
|
99
|
+
setError(null);
|
|
100
|
+
setIsLoading(true);
|
|
101
|
+
|
|
102
|
+
// Add a loading message placeholder
|
|
103
|
+
const loadingMessageId = (Date.now() + 1).toString();
|
|
104
|
+
const loadingMessage: Message = {
|
|
105
|
+
id: loadingMessageId,
|
|
106
|
+
role: 'assistant',
|
|
107
|
+
content: '...',
|
|
108
|
+
timestamp: new Date().toLocaleTimeString(),
|
|
109
|
+
};
|
|
110
|
+
setMessages((prev) => [...prev, loadingMessage]);
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
if (effectiveEndpoint) {
|
|
114
|
+
// Send to backend API
|
|
115
|
+
const response = await fetch(effectiveEndpoint, {
|
|
116
|
+
method: 'POST',
|
|
117
|
+
headers: {
|
|
118
|
+
'Content-Type': 'application/json',
|
|
119
|
+
...effectiveHeaders,
|
|
120
|
+
},
|
|
121
|
+
body: JSON.stringify({
|
|
122
|
+
nodeId,
|
|
123
|
+
nodeType,
|
|
124
|
+
message: currentInput,
|
|
125
|
+
conversationHistory: messages.map((msg) => ({
|
|
126
|
+
role: msg.role,
|
|
127
|
+
content: msg.content,
|
|
128
|
+
})),
|
|
129
|
+
}),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (!response.ok) {
|
|
133
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const data = await response.json();
|
|
137
|
+
|
|
138
|
+
// Remove loading message and add actual response
|
|
139
|
+
setMessages((prev) => {
|
|
140
|
+
const filtered = prev.filter((msg) => msg.id !== loadingMessageId);
|
|
141
|
+
return [
|
|
142
|
+
...filtered,
|
|
143
|
+
{
|
|
144
|
+
id: Date.now().toString(),
|
|
145
|
+
role: 'assistant',
|
|
146
|
+
content: data.message || data.content || data.response || 'No response from AI',
|
|
147
|
+
timestamp: new Date().toLocaleTimeString(),
|
|
148
|
+
},
|
|
149
|
+
];
|
|
150
|
+
});
|
|
151
|
+
} else {
|
|
152
|
+
// Fallback: Simulate AI response if no API endpoint provided
|
|
153
|
+
setTimeout(() => {
|
|
154
|
+
setMessages((prev) => {
|
|
155
|
+
const filtered = prev.filter((msg) => msg.id !== loadingMessageId);
|
|
156
|
+
return [
|
|
157
|
+
...filtered,
|
|
158
|
+
{
|
|
159
|
+
id: Date.now().toString(),
|
|
160
|
+
role: 'assistant',
|
|
161
|
+
content: `I understand you're asking about: "${currentInput}". This is specific to your ${nodeType} node (ID: ${nodeId}). Please configure an API endpoint to get real AI responses.`,
|
|
162
|
+
timestamp: new Date().toLocaleTimeString(),
|
|
163
|
+
},
|
|
164
|
+
];
|
|
165
|
+
});
|
|
166
|
+
setIsLoading(false);
|
|
167
|
+
}, 500);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
} catch (err) {
|
|
171
|
+
// Remove loading message and show error
|
|
172
|
+
setMessages((prev) => {
|
|
173
|
+
const filtered = prev.filter((msg) => msg.id !== loadingMessageId);
|
|
174
|
+
return [
|
|
175
|
+
...filtered,
|
|
176
|
+
{
|
|
177
|
+
id: Date.now().toString(),
|
|
178
|
+
role: 'assistant',
|
|
179
|
+
content: `Sorry, I encountered an error: ${err instanceof Error ? err.message : 'Unknown error'}. Please check your API endpoint configuration.`,
|
|
180
|
+
timestamp: new Date().toLocaleTimeString(),
|
|
181
|
+
},
|
|
182
|
+
];
|
|
183
|
+
});
|
|
184
|
+
setError(err instanceof Error ? err.message : 'Failed to get AI response');
|
|
185
|
+
} finally {
|
|
186
|
+
setIsLoading(false);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const handleKeyPress = (e: React.KeyboardEvent) => {
|
|
191
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
192
|
+
e.preventDefault();
|
|
193
|
+
handleSend();
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<>
|
|
199
|
+
{/* Backdrop */}
|
|
200
|
+
<Box
|
|
201
|
+
data-backdrop
|
|
202
|
+
onClick={onClose}
|
|
203
|
+
sx={{
|
|
204
|
+
position: 'fixed',
|
|
205
|
+
top: 0,
|
|
206
|
+
left: 0,
|
|
207
|
+
right: 0,
|
|
208
|
+
bottom: 0,
|
|
209
|
+
bgcolor: 'rgba(0, 0, 0, 0.3)',
|
|
210
|
+
zIndex: 9998,
|
|
211
|
+
}}
|
|
212
|
+
/>
|
|
213
|
+
{/* Popup */}
|
|
214
|
+
<Card
|
|
215
|
+
ref={popupRef}
|
|
216
|
+
id="node-ai-assistant-popup"
|
|
217
|
+
sx={{
|
|
218
|
+
position: 'fixed',
|
|
219
|
+
left: `${position.x}px`,
|
|
220
|
+
top: `${position.y}px`,
|
|
221
|
+
zIndex: 9999,
|
|
222
|
+
width: '420px',
|
|
223
|
+
height: '600px',
|
|
224
|
+
display: 'flex',
|
|
225
|
+
flexDirection: 'column',
|
|
226
|
+
bgcolor: '#1F2937',
|
|
227
|
+
color: '#fff',
|
|
228
|
+
border: '1px solid #374151',
|
|
229
|
+
borderRadius: '12px',
|
|
230
|
+
boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 10px 10px -5px rgba(0, 0, 0, 0.2)',
|
|
231
|
+
overflow: 'hidden',
|
|
232
|
+
}}
|
|
233
|
+
>
|
|
234
|
+
{/* Header */}
|
|
235
|
+
<Box
|
|
236
|
+
sx={{
|
|
237
|
+
display: 'flex',
|
|
238
|
+
alignItems: 'center',
|
|
239
|
+
justifyContent: 'space-between',
|
|
240
|
+
p: 2,
|
|
241
|
+
borderBottom: '1px solid #374151',
|
|
242
|
+
bgcolor: '#111827',
|
|
243
|
+
}}
|
|
244
|
+
>
|
|
245
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
|
246
|
+
<IconMessage size={20} style={{ color: '#3b82f6' }} />
|
|
247
|
+
<Typography
|
|
248
|
+
variant="h6"
|
|
249
|
+
sx={{ color: '#fff', fontSize: '16px', fontWeight: 600 }}
|
|
250
|
+
>
|
|
251
|
+
Node AI Assistant
|
|
252
|
+
</Typography>
|
|
253
|
+
<Typography
|
|
254
|
+
variant="caption"
|
|
255
|
+
sx={{ color: '#9CA3AF', fontSize: '11px', ml: 1 }}
|
|
256
|
+
>
|
|
257
|
+
{nodeType}
|
|
258
|
+
</Typography>
|
|
259
|
+
</Box>
|
|
260
|
+
<IconButton
|
|
261
|
+
aria-label="close"
|
|
262
|
+
onClick={onClose}
|
|
263
|
+
sx={{
|
|
264
|
+
color: '#9CA3AF',
|
|
265
|
+
'&:hover': {
|
|
266
|
+
color: '#fff',
|
|
267
|
+
bgcolor: 'rgba(255, 255, 255, 0.1)',
|
|
268
|
+
},
|
|
269
|
+
}}
|
|
270
|
+
>
|
|
271
|
+
<RiCloseLine />
|
|
272
|
+
</IconButton>
|
|
273
|
+
</Box>
|
|
274
|
+
|
|
275
|
+
{/* Messages */}
|
|
276
|
+
<CardContent
|
|
277
|
+
sx={{
|
|
278
|
+
flex: 1,
|
|
279
|
+
overflow: 'auto',
|
|
280
|
+
p: 2,
|
|
281
|
+
bgcolor: '#1F2937',
|
|
282
|
+
'&::-webkit-scrollbar': {
|
|
283
|
+
width: '6px',
|
|
284
|
+
},
|
|
285
|
+
'&::-webkit-scrollbar-track': {
|
|
286
|
+
background: 'transparent',
|
|
287
|
+
},
|
|
288
|
+
'&::-webkit-scrollbar-thumb': {
|
|
289
|
+
background: '#4B5563',
|
|
290
|
+
borderRadius: '3px',
|
|
291
|
+
'&:hover': {
|
|
292
|
+
background: '#6B7280',
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
}}
|
|
296
|
+
>
|
|
297
|
+
{error && (
|
|
298
|
+
<Alert
|
|
299
|
+
severity="error"
|
|
300
|
+
sx={{
|
|
301
|
+
mb: 2,
|
|
302
|
+
bgcolor: '#7F1D1D',
|
|
303
|
+
color: '#FCA5A5',
|
|
304
|
+
'& .MuiAlert-icon': {
|
|
305
|
+
color: '#FCA5A5',
|
|
306
|
+
},
|
|
307
|
+
}}
|
|
308
|
+
onClose={() => setError(null)}
|
|
309
|
+
>
|
|
310
|
+
{error}
|
|
311
|
+
</Alert>
|
|
312
|
+
)}
|
|
313
|
+
{messages.map((message) => (
|
|
314
|
+
<Box
|
|
315
|
+
key={message.id}
|
|
316
|
+
sx={{
|
|
317
|
+
display: 'flex',
|
|
318
|
+
justifyContent: message.role === 'user' ? 'flex-end' : 'flex-start',
|
|
319
|
+
mb: 2,
|
|
320
|
+
}}
|
|
321
|
+
>
|
|
322
|
+
<Box
|
|
323
|
+
sx={{
|
|
324
|
+
maxWidth: '80%',
|
|
325
|
+
p: 1.5,
|
|
326
|
+
borderRadius: '8px',
|
|
327
|
+
bgcolor:
|
|
328
|
+
message.role === 'user' ? '#2563EB' : '#111827',
|
|
329
|
+
border:
|
|
330
|
+
message.role === 'assistant'
|
|
331
|
+
? '1px solid #374151'
|
|
332
|
+
: 'none',
|
|
333
|
+
display: 'flex',
|
|
334
|
+
alignItems: message.content === '...' ? 'center' : 'flex-start',
|
|
335
|
+
gap: 1,
|
|
336
|
+
}}
|
|
337
|
+
>
|
|
338
|
+
{message.content === '...' && isLoading ? (
|
|
339
|
+
<CircularProgress size={16} sx={{ color: '#3b82f6' }} />
|
|
340
|
+
) : (
|
|
341
|
+
<Typography
|
|
342
|
+
variant="body2"
|
|
343
|
+
sx={{
|
|
344
|
+
color: '#fff',
|
|
345
|
+
fontSize: '13px',
|
|
346
|
+
lineHeight: 1.5,
|
|
347
|
+
whiteSpace: 'pre-wrap',
|
|
348
|
+
wordBreak: 'break-word',
|
|
349
|
+
}}
|
|
350
|
+
>
|
|
351
|
+
{message.content}
|
|
352
|
+
</Typography>
|
|
353
|
+
)}
|
|
354
|
+
{message.content !== '...' && (
|
|
355
|
+
<Typography
|
|
356
|
+
variant="caption"
|
|
357
|
+
sx={{
|
|
358
|
+
color: 'rgba(255, 255, 255, 0.5)',
|
|
359
|
+
fontSize: '10px',
|
|
360
|
+
display: 'block',
|
|
361
|
+
mt: 0.5,
|
|
362
|
+
alignSelf: 'flex-end',
|
|
363
|
+
}}
|
|
364
|
+
>
|
|
365
|
+
{message.timestamp}
|
|
366
|
+
</Typography>
|
|
367
|
+
)}
|
|
368
|
+
</Box>
|
|
369
|
+
</Box>
|
|
370
|
+
))}
|
|
371
|
+
<div ref={messagesEndRef} />
|
|
372
|
+
</CardContent>
|
|
373
|
+
|
|
374
|
+
{/* Input */}
|
|
375
|
+
<Box
|
|
376
|
+
sx={{
|
|
377
|
+
p: 2,
|
|
378
|
+
borderTop: '1px solid #374151',
|
|
379
|
+
bgcolor: '#111827',
|
|
380
|
+
}}
|
|
381
|
+
>
|
|
382
|
+
<Box sx={{ display: 'flex', gap: 1 }}>
|
|
383
|
+
<TextField
|
|
384
|
+
fullWidth
|
|
385
|
+
placeholder="Ask your Node AI Assistant..."
|
|
386
|
+
value={inputValue}
|
|
387
|
+
onChange={(e) => setInputValue(e.target.value)}
|
|
388
|
+
onKeyPress={handleKeyPress}
|
|
389
|
+
multiline
|
|
390
|
+
maxRows={3}
|
|
391
|
+
sx={{
|
|
392
|
+
'& .MuiOutlinedInput-root': {
|
|
393
|
+
bgcolor: '#1F2937',
|
|
394
|
+
color: '#fff',
|
|
395
|
+
borderRadius: '8px',
|
|
396
|
+
'& fieldset': {
|
|
397
|
+
borderColor: '#374151',
|
|
398
|
+
},
|
|
399
|
+
'&:hover fieldset': {
|
|
400
|
+
borderColor: '#4B5563',
|
|
401
|
+
},
|
|
402
|
+
'&.Mui-focused fieldset': {
|
|
403
|
+
borderColor: '#3b82f6',
|
|
404
|
+
},
|
|
405
|
+
},
|
|
406
|
+
'& .MuiInputBase-input': {
|
|
407
|
+
color: '#fff',
|
|
408
|
+
fontSize: '13px',
|
|
409
|
+
'&::placeholder': {
|
|
410
|
+
color: '#9CA3AF',
|
|
411
|
+
opacity: 1,
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
}}
|
|
415
|
+
/>
|
|
416
|
+
<IconButton
|
|
417
|
+
onClick={handleSend}
|
|
418
|
+
disabled={!inputValue.trim() || isLoading}
|
|
419
|
+
sx={{
|
|
420
|
+
bgcolor: '#2563EB',
|
|
421
|
+
color: '#fff',
|
|
422
|
+
'&:hover': {
|
|
423
|
+
bgcolor: '#1d4ed8',
|
|
424
|
+
},
|
|
425
|
+
'&:disabled': {
|
|
426
|
+
bgcolor: '#374151',
|
|
427
|
+
color: '#6B7280',
|
|
428
|
+
},
|
|
429
|
+
}}
|
|
430
|
+
>
|
|
431
|
+
{isLoading ? (
|
|
432
|
+
<CircularProgress size={20} sx={{ color: '#fff' }} />
|
|
433
|
+
) : (
|
|
434
|
+
<RiSendPlaneFill />
|
|
435
|
+
)}
|
|
436
|
+
</IconButton>
|
|
437
|
+
</Box>
|
|
438
|
+
</Box>
|
|
439
|
+
</Card>
|
|
440
|
+
</>
|
|
441
|
+
);
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Show Node AI Assistant Popup at a specific position
|
|
446
|
+
* @param nodeId - The ID of the node
|
|
447
|
+
* @param nodeType - The type of the node (e.g., 'Start Node', 'API Node')
|
|
448
|
+
* @param buttonElement - The button element that triggered the popup (for positioning)
|
|
449
|
+
* @param apiEndpoint - Optional API endpoint (overrides global config)
|
|
450
|
+
* @param apiHeaders - Optional API headers (overrides global config)
|
|
451
|
+
*/
|
|
452
|
+
export const showNodeAIAssistantPopup = (
|
|
453
|
+
nodeId: string,
|
|
454
|
+
nodeType: string,
|
|
455
|
+
buttonElement: HTMLElement,
|
|
456
|
+
apiEndpoint?: string,
|
|
457
|
+
apiHeaders?: Record<string, string>
|
|
458
|
+
): (() => void) => {
|
|
459
|
+
const portalRoot = document.createElement('div');
|
|
460
|
+
document.body.appendChild(portalRoot);
|
|
461
|
+
|
|
462
|
+
const root = createRoot(portalRoot);
|
|
463
|
+
|
|
464
|
+
// Calculate position relative to button
|
|
465
|
+
const rect = buttonElement.getBoundingClientRect();
|
|
466
|
+
const position = {
|
|
467
|
+
x: rect.right + 12, // Position to the right of the button
|
|
468
|
+
y: rect.top - 50, // Align with button
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
// Adjust if popup would go off screen
|
|
472
|
+
const popupWidth = 420;
|
|
473
|
+
const popupHeight = 600;
|
|
474
|
+
if (position.x + popupWidth > window.innerWidth) {
|
|
475
|
+
position.x = rect.left - popupWidth - 12; // Position to the left instead
|
|
476
|
+
}
|
|
477
|
+
if (position.y + popupHeight > window.innerHeight) {
|
|
478
|
+
position.y = window.innerHeight - popupHeight - 20;
|
|
479
|
+
}
|
|
480
|
+
if (position.y < 0) {
|
|
481
|
+
position.y = 20;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const handleClose = () => {
|
|
485
|
+
root.unmount();
|
|
486
|
+
if (portalRoot.parentNode) {
|
|
487
|
+
document.body.removeChild(portalRoot);
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
root.render(
|
|
492
|
+
<NodeAIAssistantPopup
|
|
493
|
+
nodeId={nodeId}
|
|
494
|
+
nodeType={nodeType}
|
|
495
|
+
position={position}
|
|
496
|
+
onClose={handleClose}
|
|
497
|
+
apiEndpoint={apiEndpoint}
|
|
498
|
+
apiHeaders={apiHeaders}
|
|
499
|
+
/>
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
return handleClose;
|
|
503
|
+
};
|
|
504
|
+
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, IconButton, Tooltip } from '@mui/material';
|
|
3
|
+
import {
|
|
4
|
+
IconLayoutGrid,
|
|
5
|
+
IconMessage,
|
|
6
|
+
IconPlus,
|
|
7
|
+
IconCopy,
|
|
8
|
+
IconTrash,
|
|
9
|
+
} from '@tabler/icons-react';
|
|
10
|
+
|
|
11
|
+
interface NodeActionButtonsProps {
|
|
12
|
+
selected?: boolean;
|
|
13
|
+
onLayout?: () => void;
|
|
14
|
+
onOpenAIAssistant?: (buttonElement: HTMLElement) => void;
|
|
15
|
+
onAddToGroup?: () => void;
|
|
16
|
+
onDuplicate?: () => void;
|
|
17
|
+
onDelete?: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const NodeActionButtons: React.FC<NodeActionButtonsProps> = ({
|
|
21
|
+
selected,
|
|
22
|
+
onLayout,
|
|
23
|
+
onOpenAIAssistant,
|
|
24
|
+
onAddToGroup,
|
|
25
|
+
onDuplicate,
|
|
26
|
+
onDelete,
|
|
27
|
+
}) => {
|
|
28
|
+
if (!selected) return null;
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<Box
|
|
32
|
+
sx={{
|
|
33
|
+
position: 'absolute',
|
|
34
|
+
left: '100%',
|
|
35
|
+
top: '50%',
|
|
36
|
+
transform: 'translateY(-50%)',
|
|
37
|
+
marginLeft: '12px',
|
|
38
|
+
display: 'flex',
|
|
39
|
+
flexDirection: 'column',
|
|
40
|
+
gap: 0.5,
|
|
41
|
+
backgroundColor: 'rgba(15, 15, 35, 0.95)',
|
|
42
|
+
borderRadius: '12px',
|
|
43
|
+
border: '1px solid rgba(255, 255, 255, 0.1)',
|
|
44
|
+
p: 0.5,
|
|
45
|
+
zIndex: 1000,
|
|
46
|
+
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.3)',
|
|
47
|
+
}}
|
|
48
|
+
onClick={(e) => e.stopPropagation()}
|
|
49
|
+
>
|
|
50
|
+
<Tooltip title="Layout" placement="right">
|
|
51
|
+
<IconButton
|
|
52
|
+
size="small"
|
|
53
|
+
onClick={(e) => {
|
|
54
|
+
e.stopPropagation();
|
|
55
|
+
onLayout?.();
|
|
56
|
+
}}
|
|
57
|
+
sx={{
|
|
58
|
+
color: 'rgba(255, 255, 255, 0.6)',
|
|
59
|
+
'&:hover': {
|
|
60
|
+
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
|
61
|
+
color: '#fff',
|
|
62
|
+
},
|
|
63
|
+
}}
|
|
64
|
+
>
|
|
65
|
+
<IconLayoutGrid size={18} />
|
|
66
|
+
</IconButton>
|
|
67
|
+
</Tooltip>
|
|
68
|
+
<Tooltip title="Node AI Assistant" placement="right">
|
|
69
|
+
<IconButton
|
|
70
|
+
size="small"
|
|
71
|
+
data-node-ai-assistant-button
|
|
72
|
+
onClick={(e) => {
|
|
73
|
+
e.stopPropagation();
|
|
74
|
+
onOpenAIAssistant?.(e.currentTarget);
|
|
75
|
+
}}
|
|
76
|
+
sx={{
|
|
77
|
+
color: 'rgba(255, 255, 255, 0.6)',
|
|
78
|
+
'&:hover': {
|
|
79
|
+
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
|
80
|
+
color: '#fff',
|
|
81
|
+
},
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
<IconMessage size={18} />
|
|
85
|
+
</IconButton>
|
|
86
|
+
</Tooltip>
|
|
87
|
+
<Tooltip title="Add to Group" placement="right">
|
|
88
|
+
<IconButton
|
|
89
|
+
size="small"
|
|
90
|
+
onClick={(e) => {
|
|
91
|
+
e.stopPropagation();
|
|
92
|
+
onAddToGroup?.();
|
|
93
|
+
}}
|
|
94
|
+
sx={{
|
|
95
|
+
color: 'rgba(255, 255, 255, 0.6)',
|
|
96
|
+
'&:hover': {
|
|
97
|
+
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
|
98
|
+
color: '#fff',
|
|
99
|
+
},
|
|
100
|
+
}}
|
|
101
|
+
>
|
|
102
|
+
<IconPlus size={18} />
|
|
103
|
+
</IconButton>
|
|
104
|
+
</Tooltip>
|
|
105
|
+
<Tooltip title="Duplicate" placement="right">
|
|
106
|
+
<IconButton
|
|
107
|
+
size="small"
|
|
108
|
+
onClick={(e) => {
|
|
109
|
+
e.stopPropagation();
|
|
110
|
+
onDuplicate?.();
|
|
111
|
+
}}
|
|
112
|
+
sx={{
|
|
113
|
+
color: 'rgba(255, 255, 255, 0.6)',
|
|
114
|
+
'&:hover': {
|
|
115
|
+
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
|
116
|
+
color: '#fff',
|
|
117
|
+
},
|
|
118
|
+
}}
|
|
119
|
+
>
|
|
120
|
+
<IconCopy size={18} />
|
|
121
|
+
</IconButton>
|
|
122
|
+
</Tooltip>
|
|
123
|
+
<Tooltip title="Delete" placement="right">
|
|
124
|
+
<IconButton
|
|
125
|
+
size="small"
|
|
126
|
+
onClick={(e) => {
|
|
127
|
+
e.stopPropagation();
|
|
128
|
+
onDelete?.();
|
|
129
|
+
}}
|
|
130
|
+
sx={{
|
|
131
|
+
color: 'rgba(255, 255, 255, 0.6)',
|
|
132
|
+
'&:hover': {
|
|
133
|
+
backgroundColor: 'rgba(239, 68, 68, 0.2)',
|
|
134
|
+
color: '#EF4444',
|
|
135
|
+
},
|
|
136
|
+
}}
|
|
137
|
+
>
|
|
138
|
+
<IconTrash size={18} />
|
|
139
|
+
</IconButton>
|
|
140
|
+
</Tooltip>
|
|
141
|
+
</Box>
|
|
142
|
+
);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export default NodeActionButtons;
|
|
146
|
+
|
|
@@ -1,11 +1,20 @@
|
|
|
1
|
-
export { AutomationStartNode } from './AutomationStartNode';
|
|
2
|
-
export { AutomationApiNode } from './AutomationApiNode';
|
|
3
|
-
export { AutomationFormattingNode } from './AutomationFormattingNode';
|
|
4
|
-
export { AutomationSheetsNode } from './AutomationSheetsNode';
|
|
5
|
-
export { AutomationEndNode } from './AutomationEndNode';
|
|
6
|
-
export { AutomationNoteNode } from './AutomationNoteNode';
|
|
7
|
-
export { AutomationExecutionPanel } from './AutomationExecutionPanel';
|
|
8
|
-
export { AutomationAISuggestionNode } from './AutomationAISuggestionNode';
|
|
9
|
-
export { AISuggestionsModal, showAISuggestionsModal } from './AISuggestionsModal';
|
|
10
|
-
export { AISuggestionsPanel } from './AISuggestionsPanel';
|
|
11
|
-
export
|
|
1
|
+
export { AutomationStartNode } from './AutomationStartNode';
|
|
2
|
+
export { AutomationApiNode } from './AutomationApiNode';
|
|
3
|
+
export { AutomationFormattingNode } from './AutomationFormattingNode';
|
|
4
|
+
export { AutomationSheetsNode } from './AutomationSheetsNode';
|
|
5
|
+
export { AutomationEndNode } from './AutomationEndNode';
|
|
6
|
+
export { AutomationNoteNode } from './AutomationNoteNode';
|
|
7
|
+
export { AutomationExecutionPanel } from './AutomationExecutionPanel';
|
|
8
|
+
export { AutomationAISuggestionNode } from './AutomationAISuggestionNode';
|
|
9
|
+
export { AISuggestionsModal, showAISuggestionsModal } from './AISuggestionsModal';
|
|
10
|
+
export { AISuggestionsPanel } from './AISuggestionsPanel';
|
|
11
|
+
export { NodeActionButtons } from './NodeActionButtons';
|
|
12
|
+
export { NodeAIAssistantPopup, showNodeAIAssistantPopup } from './NodeAIAssistantPopup';
|
|
13
|
+
export type { AISuggestion } from './AISuggestionsModal';
|
|
14
|
+
export {
|
|
15
|
+
setNodeAIAssistantEndpoint,
|
|
16
|
+
setNodeAIAssistantHeaders,
|
|
17
|
+
getNodeAIAssistantEndpoint,
|
|
18
|
+
getNodeAIAssistantHeaders,
|
|
19
|
+
configureNodeAIAssistant,
|
|
20
|
+
} from '../../utils/nodeAIAssistantConfig';
|