@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,974 +1,1118 @@
1
- import React, { useEffect, useState, useRef } from 'react';
2
- import { createRoot } from 'react-dom/client';
3
- import { Handle, Position, useNodeId } from '@xyflow/react';
4
- import { Box, Typography, Chip, IconButton, Card, CardContent, Button } from '@mui/material';
5
- import {
6
- TableChart as TableChartIcon,
7
- AccessTime as AccessTimeIcon,
8
- Save as SaveIcon,
9
- Send as SendIcon,
10
- Settings as SettingsIcon,
11
- TableRows as TableRowsIcon,
12
- Email as EmailIcon,
13
- Description as DescriptionIcon,
14
- Drafts as DraftsIcon,
15
- Add as AddIcon,
16
- CheckCircle as CheckCircleIcon,
17
- Settings as ConfigureIcon,
18
- Error as ErrorIcon,
19
- ArrowForwardIos as ArrowForwardIcon,
20
- WhatsApp as WhatsAppIcon,
21
- Phone as PhoneIcon,
22
- Lightbulb as LightbulbIcon
23
- } from '@mui/icons-material';
24
- import { RiCloseLine } from 'react-icons/ri';
25
- import ReactJson from 'react-json-view';
26
- import { getIconByName } from '../../utils/iconMapper';
27
- import { useTranslation } from 'react-i18next';
28
- import { useDiagram } from '../../contexts/DiagramProvider';
29
- import { AISuggestion } from './AISuggestionsModal';
30
- import { AISuggestionsPanel } from './AISuggestionsPanel';
31
-
32
- interface AutomationSheetsNodeProps {
33
- data: {
34
- label: string;
35
- description: string;
36
- status: 'Ready' | 'Running' | 'Completed' | 'Error';
37
- inputVariable: string;
38
- lastRun?: string;
39
- iconName?: string;
40
- sheetsConfig: {
41
- spreadsheetId: string;
42
- sheetName: string;
43
- range: string;
44
- credentials: {
45
- type: 'service-account' | 'oauth' | 'api-key';
46
- serviceAccountKey?: string;
47
- oauthToken?: string;
48
- apiKey?: string;
49
- };
50
- };
51
- dataMapping: {
52
- headers: Array<{
53
- column: string;
54
- sourceField: string;
55
- dataType: 'string' | 'number' | 'date' | 'boolean';
56
- }>;
57
- appendMode: boolean;
58
- clearSheet: boolean;
59
- };
60
- exportOptions: {
61
- emailRecipients: string[];
62
- fileName: string;
63
- exportFormat: 'sheets' | 'excel' | 'both';
64
- includeHeaders: boolean;
65
- sendEmailNotification: boolean;
66
- // Optional fields for Gmail output configuration (may live under formData.exportOptions)
67
- emailSendEnabled?: boolean;
68
- emailSender?: string;
69
- emailSubject?: string;
70
- emailMessage?: string;
71
- // WhatsApp configuration
72
- whatsapp?: {
73
- enabled: boolean;
74
- phoneNumber: string;
75
- messageTemplate: string;
76
- includeDataMode: 'json' | 'summary' | 'custom';
77
- // Twilio configuration
78
- twilio?: {
79
- enabled: boolean;
80
- accountSid: string;
81
- authToken: string;
82
- fromNumber: string;
83
- isSandbox: boolean;
84
- templateSid?: string;
85
- templateVariables?: Record<string, string>;
86
- };
87
- };
88
- };
89
- formData?: {
90
- aiSuggestionsCount?: number; // Number of AI suggestions available
91
- [key: string]: any;
92
- };
93
- // Optional per-output-method statuses supplied by engine
94
- outputStatuses?: {
95
- googleSheets?: 'not-set' | 'configured' | 'running' | 'connected' | 'failed';
96
- gmail?: 'not-set' | 'configured' | 'running' | 'connected' | 'failed';
97
- whatsapp?: 'not-set' | 'configured' | 'running' | 'connected' | 'failed';
98
- };
99
- };
100
- selected?: boolean;
101
- }
102
-
103
- export const AutomationSheetsNode: React.FC<AutomationSheetsNodeProps> = ({ data, selected }) => {
104
- const { t } = useTranslation();
105
- const [isJsonOpen, setIsJsonOpen] = useState(false);
106
- const [showSuggestions, setShowSuggestions] = useState(false);
107
- const rootRef = useRef<any>(null);
108
- const portalRef = useRef<HTMLDivElement | null>(null);
109
- const nodeRef = useRef<HTMLDivElement | null>(null);
110
- const nodeId = useNodeId();
111
- const setSelectedNode = useDiagram((state) => state.setSelectedNode);
112
- const enableJson = useDiagram((state) => state.enableNodeJsonPopover ?? true);
113
-
114
- // Get the icon component based on the iconName
115
- const IconComponent = getIconByName(data.iconName || 'TableChart');
116
-
117
- // Resolve export options from formData or data
118
- const exportOptions = (data.formData?.exportOptions || (data as any).exportOptions) || {};
119
- const showSheets = Boolean(
120
- exportOptions?.exportFormat === 'sheets' || exportOptions?.exportFormat === 'both'
121
- );
122
- const showGmail = Boolean(
123
- exportOptions.emailSendEnabled &&
124
- exportOptions.emailSender &&
125
- (exportOptions.emailRecipients?.length || 0) > 0 &&
126
- exportOptions.emailSubject &&
127
- exportOptions.emailMessage
128
- );
129
- const showSlack = Boolean(
130
- exportOptions.slack?.enabled &&
131
- exportOptions.slack?.webhookUrl &&
132
- exportOptions.slack?.channel
133
- );
134
- const showWhatsApp = Boolean(
135
- exportOptions.whatsapp?.enabled &&
136
- exportOptions.whatsapp?.phoneNumber
137
- );
138
-
139
- // Determine Google Sheets status from provided data and execution result
140
- const determineGoogleSheetsStatus = (): 'not-set' | 'configured' | 'running' | 'connected' | 'failed' => {
141
- // Prefer status provided by engine
142
- if (data.outputStatuses?.googleSheets) return data.outputStatuses.googleSheets;
143
-
144
- // Required config check (accepts either spreadsheetId or OAuth clientId that can create one)
145
- const cfg = (data.formData?.sheetsConfig || data.sheetsConfig) || {};
146
- const creds = cfg.credentials || {};
147
- const hasRequired = Boolean((cfg.spreadsheetId || creds.clientId) && cfg.sheetName && creds.type);
148
- if (!hasRequired) return 'not-set';
149
-
150
- // Running inferred from node status
151
- if (data.status === 'Running') return 'running';
152
-
153
- // Execution result inference
154
- const exec = data.formData?.executionResult || (data as any).executionResult;
155
- if (exec?.success === true) return 'connected';
156
- if (exec?.success === false || data.status === 'Error') return 'failed';
157
- return 'configured';
158
- };
159
-
160
- // Determine Gmail status
161
- const determineGmailStatus = (): 'not-set' | 'configured' | 'running' | 'connected' | 'failed' => {
162
- // Prefer status provided by engine
163
- if (data.outputStatuses?.gmail) return data.outputStatuses.gmail;
164
-
165
- const ex = (data.formData?.exportOptions || data.exportOptions) || {};
166
- const hasRequired = Boolean(
167
- ex.emailSendEnabled && ex.emailSender && (ex.emailRecipients?.length || 0) > 0 && ex.emailSubject && ex.emailMessage
168
- );
169
- if (!hasRequired) return 'not-set';
170
-
171
- if (data.status === 'Running') return 'running';
172
-
173
- const exec = data.formData?.executionResult || (data as any).executionResult;
174
- if (exec?.success === true) return 'connected';
175
- if (exec?.success === false || data.status === 'Error') return 'failed';
176
- return 'configured';
177
- };
178
-
179
- // Determine Slack status
180
- const determineSlackStatus = (): 'not-set' | 'configured' | 'running' | 'connected' | 'failed' => {
181
- if ((data as any).outputStatuses?.slack) return (data as any).outputStatuses.slack;
182
- const slack = (data.formData?.exportOptions?.slack || (data as any).exportOptions?.slack) || {};
183
- if (!slack.enabled || !slack.webhookUrl || !slack.channel) return 'not-set';
184
- if (data.status === 'Running') return 'running';
185
- const exec = data.formData?.executionResult || (data as any).executionResult;
186
- if (exec?.success === true) return 'connected';
187
- if (exec?.success === false || data.status === 'Error') return 'failed';
188
- return 'configured';
189
- };
190
-
191
- // Determine WhatsApp status
192
- const determineWhatsAppStatus = (): 'not-set' | 'configured' | 'running' | 'connected' | 'failed' => {
193
- if (data.outputStatuses?.whatsapp) return data.outputStatuses.whatsapp;
194
-
195
- const whatsapp = (data.formData?.exportOptions?.whatsapp || data.exportOptions?.whatsapp) || {};
196
-
197
- // Check if Twilio is configured
198
- if (whatsapp.twilio?.enabled) {
199
- const twilioConfig = whatsapp.twilio;
200
- const hasRequiredTwilio = Boolean(
201
- twilioConfig.accountSid &&
202
- twilioConfig.authToken &&
203
- twilioConfig.fromNumber &&
204
- whatsapp.phoneNumber
205
- );
206
- if (!hasRequiredTwilio) return 'not-set';
207
- } else {
208
- // Fallback to basic WhatsApp Web check
209
- if (!whatsapp.enabled || !whatsapp.phoneNumber) return 'not-set';
210
- }
211
-
212
- if (data.status === 'Running') return 'running';
213
-
214
- const exec = data.formData?.executionResult || (data as any).executionResult;
215
- if (exec?.success === true) return 'connected';
216
- if (exec?.success === false || data.status === 'Error') return 'failed';
217
- return 'configured';
218
- };
219
-
220
- const getNodeStatus = (): string => {
221
- const gs = determineGoogleSheetsStatus();
222
- const gm = determineGmailStatus();
223
- const sl = determineSlackStatus();
224
- const wa = determineWhatsAppStatus();
225
- if (gs === 'failed' || gm === 'failed' || sl === 'failed' || wa === 'failed') return 'Failed';
226
- if (gs === 'running' || gm === 'running' || sl === 'running' || wa === 'running') return 'Running';
227
- if (
228
- (gs === 'connected' || gs === 'not-set') &&
229
- (gm === 'connected' || gm === 'not-set') &&
230
- (sl === 'connected' || sl === 'not-set') &&
231
- (wa === 'connected' || wa === 'not-set')
232
- ) return 'Success';
233
- if (
234
- (gs === 'configured' || gs === 'not-set') &&
235
- (gm === 'configured' || gm === 'not-set') &&
236
- (sl === 'configured' || sl === 'not-set') &&
237
- (wa === 'configured' || wa === 'not-set')
238
- ) return 'Ready';
239
- return 'Mixed Status';
240
- };
241
-
242
- // Status badge (shared)
243
- const StatusBadge = ({ status }: { status: 'not-set' | 'configured' | 'running' | 'connected' | 'failed' }) => {
244
- const cfg = (
245
- status === 'not-set' ? { icon: <ErrorIcon sx={{ fontSize: '12px' }} />, label: t('automation.sheetsNode.status.notSet'), bg: '#6b7280' } :
246
- status === 'configured' ? { icon: <ConfigureIcon sx={{ fontSize: '12px' }} />, label: t('automation.sheetsNode.status.configured'), bg: '#3b82f6' } :
247
- status === 'running' ? { icon: <SettingsIcon sx={{ fontSize: '12px' }} />, label: t('automation.sheetsNode.status.running'), bg: '#f59e0b' } :
248
- status === 'connected' ? { icon: <CheckCircleIcon sx={{ fontSize: '12px' }} />, label: t('automation.sheetsNode.status.connected'), bg: '#10b981' } :
249
- { icon: <ErrorIcon sx={{ fontSize: '12px' }} />, label: t('automation.sheetsNode.status.failed'), bg: '#ef4444' }
250
- );
251
- return (
252
- <Chip
253
- icon={cfg.icon}
254
- label={cfg.label}
255
- size="small"
256
- sx={{
257
- backgroundColor: cfg.bg,
258
- color: 'white',
259
- fontSize: '11px',
260
- height: '20px',
261
- '& .MuiChip-icon': { fontSize: '12px' },
262
- }}
263
- />
264
- );
265
- };
266
-
267
- // WhatsApp message sending function
268
- const sendWhatsAppMessage = (phoneNumber: string, data: any) => {
269
- const message = `
270
- Automation Result:
271
- ------------------
272
- Name: ${data.name || 'Automation Workflow'}
273
- Score: ${data.score || 'N/A'}
274
- Status: ${data.status || 'Completed'}
275
- Timestamp: ${new Date().toLocaleString()}
276
- Data: ${JSON.stringify(data, null, 2)}
277
- `;
278
- const encodedMsg = encodeURIComponent(message);
279
- const url = `https://wa.me/${phoneNumber}?text=${encodedMsg}`;
280
- window.open(url, '_blank');
281
- };
282
-
283
- const handleJsonClick = () => {
284
- if (nodeId) setSelectedNode(nodeId);
285
- if (!enableJson) return;
286
- setIsJsonOpen(!isJsonOpen);
287
- };
288
-
289
- const handleClose = () => {
290
- setIsJsonOpen(false);
291
- // Clean up portal
292
- if (rootRef.current) {
293
- rootRef.current.unmount();
294
- rootRef.current = null;
295
- }
296
- if (portalRef.current) {
297
- document.body.removeChild(portalRef.current);
298
- portalRef.current = null;
299
- }
300
- };
301
-
302
- useEffect(() => {
303
- const handleClickOutside = (event: MouseEvent) => {
304
- if (isJsonOpen && !(event.target as Element).closest('#automation-json-popover')) {
305
- handleClose();
306
- }
307
- };
308
- document.addEventListener('mousedown', handleClickOutside);
309
- return () => {
310
- document.removeEventListener('mousedown', handleClickOutside);
311
- };
312
- }, [isJsonOpen]);
313
-
314
-
315
- useEffect(() => {
316
- if (isJsonOpen) {
317
- const portalRoot = document.createElement('div');
318
- document.body.appendChild(portalRoot);
319
- portalRef.current = portalRoot;
320
-
321
- const root = createRoot(portalRoot);
322
- rootRef.current = root;
323
-
324
- root.render(
325
- <Card
326
- id="automation-json-popover"
327
- sx={{
328
- position: 'fixed',
329
- top: 0,
330
- right: 0,
331
- zIndex: 9999,
332
- width: '400px',
333
- height: '100vh',
334
- overflow: 'auto',
335
- bgcolor: '#242424',
336
- color: '#fff',
337
- border: '1px solid #333',
338
- '&::-webkit-scrollbar': {
339
- width: '6px',
340
- },
341
- '&::-webkit-scrollbar-track': {
342
- background: 'transparent',
343
- },
344
- '&::-webkit-scrollbar-thumb': {
345
- background: '#444',
346
- borderRadius: '3px',
347
- '&:hover': {
348
- background: '#666',
349
- },
350
- },
351
- }}
352
- >
353
- <CardContent sx={{ bgcolor: '#242424', color: '#fff' }}>
354
- <IconButton
355
- aria-label="close"
356
- onClick={handleClose}
357
- sx={{
358
- color: '#999',
359
- '&:hover': {
360
- color: '#fff',
361
- bgcolor: 'rgba(255, 255, 255, 0.1)',
362
- },
363
- }}
364
- >
365
- <RiCloseLine />
366
- </IconButton>
367
- {/* Show execution result prominently if available */}
368
- {data.formData?.executionResult && (
369
- <Box sx={{ mb: 2 }}>
370
- <Typography variant="h6" sx={{ color: '#fff', mb: 1 }}>
371
- {t('automation.common.executionResult')}
372
- </Typography>
373
- <Box sx={{
374
- bgcolor: data.formData.executionResult.success ? '#1e3a8a' : '#dc2626',
375
- p: 1,
376
- borderRadius: 1,
377
- mb: 1
378
- }}>
379
- <Typography variant="body2" sx={{ color: '#fff' }}>
380
- {t('automation.common.status')}: {data.formData.executionResult.success ? t('automation.common.success') : t('automation.common.failed')}
381
- </Typography>
382
- <Typography variant="body2" sx={{ color: '#fff' }}>
383
- {t('automation.common.timestamp')}: {new Date(data.formData.executionResult.timestamp).toLocaleString()}
384
- </Typography>
385
- {data.formData.executionResult.error && (
386
- <Typography variant="body2" sx={{ color: '#fff' }}>
387
- {t('automation.common.error')}: {data.formData.executionResult.error}
388
- </Typography>
389
- )}
390
- </Box>
391
- <Typography variant="h6" sx={{ color: '#fff', mb: 1 }}>
392
- Sheets Data
393
- </Typography>
394
- <ReactJson
395
- theme={'monokai'}
396
- src={data.formData.executionResult.data}
397
- collapsed={false}
398
- />
399
- </Box>
400
- )}
401
-
402
- {/* Show full node data */}
403
- <Typography variant="h6" sx={{ color: '#fff', mb: 1 }}>
404
- Full Node Data
405
- </Typography>
406
- <ReactJson theme={'monokai'} src={data.formData || data} collapsed={false} />
407
- </CardContent>
408
- </Card>
409
- );
410
- } else {
411
- // Clean up when closing
412
- if (rootRef.current) {
413
- rootRef.current.unmount();
414
- rootRef.current = null;
415
- }
416
- if (portalRef.current) {
417
- document.body.removeChild(portalRef.current);
418
- portalRef.current = null;
419
- }
420
- }
421
- }, [isJsonOpen, data]);
422
-
423
- // Output Method Component for Google Sheets
424
- const GoogleSheetsOutputMethod = () => (
425
- <Box sx={{
426
- display: 'flex',
427
- alignItems: 'center',
428
- justifyContent: 'space-between',
429
- py: 2,
430
- px: 2,
431
- borderBottom: '1px solid #334155',
432
- '&:hover': {
433
- backgroundColor: 'rgba(255, 255, 255, 0.05)',
434
- },
435
- cursor: 'pointer',
436
- }}>
437
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
438
- {/* Google Sheets Icon */}
439
- <Box sx={{
440
- width: '32px',
441
- height: '32px',
442
- backgroundColor: '#10b981',
443
- borderRadius: '6px',
444
- display: 'flex',
445
- alignItems: 'center',
446
- justifyContent: 'center',
447
- }}>
448
- <TableChartIcon sx={{ color: 'white', fontSize: '18px' }} />
449
- </Box>
450
-
451
- <Box>
452
- <Typography variant="body2" sx={{
453
- color: '#ffffff',
454
- fontSize: '14px',
455
- fontWeight: 600,
456
- mb: 0.5
457
- }}>
458
- {t('automation.sheetsNode.googleSheets')}
459
- </Typography>
460
- <Typography variant="body2" sx={{
461
- color: '#94a3b8',
462
- fontSize: '12px',
463
- mb: 0.5
464
- }}>
465
- {t('automation.sheetsNode.readWriteRows')}
466
- </Typography>
467
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
468
- <DescriptionIcon sx={{ fontSize: '12px', color: '#94a3b8' }} />
469
- <Typography variant="body2" sx={{
470
- color: '#94a3b8',
471
- fontSize: '11px'
472
- }}>
473
- {t('automation.sheetsNode.sheet')}: {data.sheetsConfig?.sheetName || data.formData?.sheetsConfig?.sheetName || 'Sheet'}
474
- </Typography>
475
- </Box>
476
- </Box>
477
- </Box>
478
-
479
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
480
- {/* Status Badge */}
481
- <StatusBadge status={determineGoogleSheetsStatus()} />
482
- <ArrowForwardIcon sx={{ fontSize: '14px', color: '#94a3b8' }} />
483
- </Box>
484
- </Box>
485
- );
486
-
487
- // Output Method Component for Gmail
488
- const GmailOutputMethod = () => (
489
- <Box sx={{
490
- display: 'flex',
491
- alignItems: 'center',
492
- justifyContent: 'space-between',
493
- py: 2,
494
- px: 2,
495
- borderBottom: '1px solid #334155',
496
- '&:hover': {
497
- backgroundColor: 'rgba(255, 255, 255, 0.05)',
498
- },
499
- cursor: 'pointer',
500
- }}>
501
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
502
- {/* Gmail Icon */}
503
- <Box sx={{
504
- width: '32px',
505
- height: '32px',
506
- backgroundColor: '#dc2626',
507
- borderRadius: '6px',
508
- display: 'flex',
509
- alignItems: 'center',
510
- justifyContent: 'center',
511
- }}>
512
- <EmailIcon sx={{ color: 'white', fontSize: '18px' }} />
513
- </Box>
514
-
515
- <Box>
516
- <Typography variant="body2" sx={{
517
- color: '#ffffff',
518
- fontSize: '14px',
519
- fontWeight: 600,
520
- mb: 0.5
521
- }}>
522
- {t('automation.sheetsNode.gmail')}
523
- </Typography>
524
- <Typography variant="body2" sx={{
525
- color: '#94a3b8',
526
- fontSize: '12px',
527
- mb: 0.5
528
- }}>
529
- {t('automation.sheetsNode.sendEmail')}
530
- </Typography>
531
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
532
- <DraftsIcon sx={{ fontSize: '12px', color: '#94a3b8' }} />
533
- <Typography variant="body2" sx={{
534
- color: '#94a3b8',
535
- fontSize: '11px'
536
- }}>
537
- {t('automation.sheetsNode.drafts')}: {data.exportOptions?.emailRecipients?.length || 0}
538
- </Typography>
539
- </Box>
540
- </Box>
541
- </Box>
542
-
543
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
544
- {/* Status Badge */}
545
- <StatusBadge status={determineGmailStatus()} />
546
- <ArrowForwardIcon sx={{ fontSize: '14px', color: '#94a3b8' }} />
547
- </Box>
548
- </Box>
549
- );
550
-
551
- // Output Method Component for Slack
552
- const SlackOutputMethod = () => {
553
- const slack = (data.formData?.exportOptions?.slack || (data as any).exportOptions?.slack) || {};
554
- return (
555
- <Box sx={{
556
- display: 'flex',
557
- alignItems: 'center',
558
- justifyContent: 'space-between',
559
- py: 2,
560
- px: 2,
561
- borderBottom: '1px solid #334155',
562
- '&:hover': {
563
- backgroundColor: 'rgba(255, 255, 255, 0.05)',
564
- },
565
- cursor: 'pointer',
566
- }}>
567
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
568
- {/* Slack Icon-like box */}
569
- <Box sx={{
570
- width: '32px',
571
- height: '32px',
572
- backgroundColor: '#4A154B',
573
- borderRadius: '6px',
574
- display: 'flex',
575
- alignItems: 'center',
576
- justifyContent: 'center',
577
- }}>
578
- <SendIcon sx={{ color: 'white', fontSize: '18px' }} />
579
- </Box>
580
-
581
- <Box>
582
- <Typography variant="body2" sx={{
583
- color: '#ffffff',
584
- fontSize: '14px',
585
- fontWeight: 600,
586
- mb: 0.5
587
- }}>
588
- {t('automation.sheetsNode.slack')}
589
- </Typography>
590
- <Typography variant="body2" sx={{
591
- color: '#94a3b8',
592
- fontSize: '12px',
593
- mb: 0.5
594
- }}>
595
- {t('automation.sheetsNode.sendMessage')}
596
- </Typography>
597
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
598
- <TableRowsIcon sx={{ fontSize: '12px', color: '#94a3b8' }} />
599
- <Typography variant="body2" sx={{
600
- color: '#94a3b8',
601
- fontSize: '11px'
602
- }}>
603
- {t('automation.sheetsNode.channel')}: {slack.channel ? `#${slack.channel}` : t('automation.sheetsNode.notSet')}
604
- </Typography>
605
- </Box>
606
- </Box>
607
- </Box>
608
-
609
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
610
- <StatusBadge status={determineSlackStatus()} />
611
- <ArrowForwardIcon sx={{ fontSize: '14px', color: '#94a3b8' }} />
612
- </Box>
613
- </Box>
614
- );
615
- };
616
-
617
- // Output Method Component for WhatsApp
618
- const WhatsAppOutputMethod = () => {
619
- const whatsapp = (data.formData?.exportOptions?.whatsapp || data.exportOptions?.whatsapp) || {};
620
- const isTwilioEnabled = whatsapp.twilio?.enabled;
621
-
622
- return (
623
- <Box sx={{
624
- display: 'flex',
625
- alignItems: 'center',
626
- justifyContent: 'space-between',
627
- py: 2,
628
- px: 2,
629
- borderBottom: '1px solid #334155',
630
- '&:hover': {
631
- backgroundColor: 'rgba(255, 255, 255, 0.05)',
632
- },
633
- cursor: 'pointer',
634
- }}
635
- onClick={() => {
636
- if (whatsapp.enabled && whatsapp.phoneNumber && !isTwilioEnabled) {
637
- const executionData = data.formData?.executionResult || {};
638
- sendWhatsAppMessage(whatsapp.phoneNumber, executionData);
639
- }
640
- }}>
641
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
642
- {/* WhatsApp Icon */}
643
- <Box sx={{
644
- width: '32px',
645
- height: '32px',
646
- backgroundColor: '#25D366',
647
- borderRadius: '6px',
648
- display: 'flex',
649
- alignItems: 'center',
650
- justifyContent: 'center',
651
- }}>
652
- <WhatsAppIcon sx={{ color: 'white', fontSize: '18px' }} />
653
- </Box>
654
-
655
- <Box>
656
- <Typography variant="body2" sx={{
657
- color: '#ffffff',
658
- fontSize: '14px',
659
- fontWeight: 600,
660
- mb: 0.5
661
- }}>
662
- {isTwilioEnabled ? t('automation.sheetsNode.whatsappTwilio') : t('automation.sheetsNode.whatsappWeb')}
663
- </Typography>
664
- <Typography variant="body2" sx={{
665
- color: '#94a3b8',
666
- fontSize: '12px',
667
- mb: 0.5
668
- }}>
669
- {isTwilioEnabled ? t('automation.sheetsNode.apiSend') : t('automation.sheetsNode.sendMessage')}
670
- </Typography>
671
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
672
- <PhoneIcon sx={{ fontSize: '12px', color: '#94a3b8' }} />
673
- <Typography variant="body2" sx={{
674
- color: '#94a3b8',
675
- fontSize: '11px'
676
- }}>
677
- {t('automation.sheetsNode.phone')}: {whatsapp.phoneNumber || t('automation.sheetsNode.notSet')}
678
- </Typography>
679
- </Box>
680
- {isTwilioEnabled && (
681
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mt: 0.5 }}>
682
- <Typography variant="body2" sx={{
683
- color: whatsapp.twilio?.isSandbox ? '#f59e0b' : '#10b981',
684
- fontSize: '10px',
685
- fontWeight: 500
686
- }}>
687
- {whatsapp.twilio?.isSandbox ? t('automation.sheetsNode.sandboxMode') : t('automation.sheetsNode.productionMode')}
688
- </Typography>
689
- </Box>
690
- )}
691
- </Box>
692
- </Box>
693
-
694
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
695
- <StatusBadge status={determineWhatsAppStatus()} />
696
- <ArrowForwardIcon sx={{ fontSize: '14px', color: '#94a3b8' }} />
697
- </Box>
698
- </Box>
699
- );
700
- };
701
-
702
- return (
703
- <Box
704
- sx={{
705
- position: 'relative',
706
- width: '380px',
707
- overflow: 'visible',
708
- }}
709
- >
710
- <Box
711
- ref={nodeRef}
712
- sx={{
713
- width: '380px',
714
- minHeight: '280px',
715
- backgroundColor: '#181C25', // Darker background like in image
716
- border: selected ? '2px solid #3b82f6' : '1px solid #1e293b',
717
- borderRadius: '12px',
718
- padding: '0',
719
- color: '#ffffff',
720
- position: 'relative',
721
- boxShadow: selected ? '0 0 0 2px rgba(59, 130, 246, 0.5)' : '0 4px 8px rgba(0, 0, 0, 0.3)',
722
- transition: 'all 0.2s ease',
723
- cursor: 'pointer',
724
- overflow: 'hidden',
725
- ...(data.status === 'Running' && {
726
- animation: 'pulse-glow 2s ease-in-out infinite',
727
- '@keyframes pulse-glow': {
728
- '0%, 100%': {
729
- boxShadow: '0 0 20px rgba(59, 130, 246, 0.4), 0 0 40px rgba(59, 130, 246, 0.2)',
730
- borderColor: 'rgba(59, 130, 246, 0.6)',
731
- },
732
- '50%': {
733
- boxShadow: '0 0 30px rgba(59, 130, 246, 0.6), 0 0 60px rgba(59, 130, 246, 0.3)',
734
- borderColor: 'rgba(59, 130, 246, 0.9)',
735
- },
736
- },
737
- }),
738
- }}
739
- onClick={handleJsonClick}
740
- >
741
- {/* Header */}
742
- <Box sx={{
743
- display: 'flex',
744
- alignItems: 'center',
745
- justifyContent: 'space-between',
746
- p: 3,
747
- borderBottom: '1px solid #334155'
748
- }}>
749
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
750
- <Box
751
- sx={{
752
- width: '32px',
753
- height: '32px',
754
- backgroundColor: '#3b82f6', // Blue color like in image
755
- borderRadius: '6px',
756
- display: 'flex',
757
- alignItems: 'center',
758
- justifyContent: 'center',
759
- }}
760
- >
761
- <IconComponent sx={{ color: 'white', fontSize: '18px' }} />
762
- </Box>
763
- <Typography variant="h6" sx={{ fontWeight: 600, fontSize: '16px' }}>
764
- {data.label}
765
- </Typography>
766
- </Box>
767
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
768
- {/* Status Chip */}
769
- <Chip
770
- label={(() => {
771
- // Map node status to standard status values
772
- const nodeStatus = getNodeStatus();
773
- if (nodeStatus === 'Failed' || data.status === 'Error') return 'Error';
774
- if (nodeStatus === 'Running' || data.status === 'Running') return 'Running';
775
- if (nodeStatus === 'Success' || data.status === 'Completed') return 'Completed';
776
- return data.status || 'Ready';
777
- })()}
778
- size="small"
779
- sx={{
780
- backgroundColor: (() => {
781
- const status = data.status || 'Ready';
782
- return status === 'Completed'
783
- ? 'rgba(37, 99, 235, 0.1)'
784
- : status === 'Running'
785
- ? 'rgba(251, 191, 36, 0.1)'
786
- : status === 'Error'
787
- ? 'rgba(239, 68, 68, 0.1)'
788
- : 'rgba(16, 185, 129, 0.1)';
789
- })(),
790
- color: (() => {
791
- const status = data.status || 'Ready';
792
- return status === 'Completed'
793
- ? '#93C5FD'
794
- : status === 'Running'
795
- ? '#FCD34D'
796
- : status === 'Error'
797
- ? '#FCA5A5'
798
- : '#86EFAC';
799
- })(),
800
- fontWeight: 500,
801
- fontSize: '12px',
802
- height: '24px',
803
- borderRadius: '12px',
804
- }}
805
- />
806
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
807
- <AccessTimeIcon sx={{ fontSize: '12px', color: '#94a3b8' }} />
808
- <Typography variant="body2" sx={{ color: '#94a3b8', fontSize: '11px' }}>
809
- {t('automation.common.lastRun')}: {data.lastRun || '2m ago'}
810
- </Typography>
811
- </Box>
812
- </Box>
813
- </Box>
814
-
815
- {/* Output Methods */}
816
- <Box>
817
- {showSheets && <GoogleSheetsOutputMethod />}
818
- {showGmail && <GmailOutputMethod />}
819
- {showSlack && <SlackOutputMethod />}
820
- {showWhatsApp && <WhatsAppOutputMethod />}
821
- {!showGmail && !showSlack && !showWhatsApp && (
822
- <Typography variant="body2" sx={{ color: '#94a3b8', p: 2 }}>
823
- {t('automation.sheetsNode.noOutputMethodsConfigured')}
824
- </Typography>
825
- )}
826
- </Box>
827
-
828
- {/* Add Configuration Button */}
829
- <Box sx={{
830
- p: 2,
831
- display: 'flex',
832
- justifyContent: 'center',
833
- borderTop: '1px solid #334155'
834
- }}>
835
- <Button
836
- startIcon={<AddIcon sx={{ fontSize: '16px' }} />}
837
- sx={{
838
- color: '#94a3b8',
839
- fontSize: '12px',
840
- textTransform: 'none',
841
- '&:hover': {
842
- backgroundColor: 'rgba(255, 255, 255, 0.05)',
843
- color: '#ffffff',
844
- },
845
- }}
846
- >
847
- {t('automation.sheetsNode.addConfiguration')}
848
- </Button>
849
- </Box>
850
-
851
- {/* Handles - Hidden but functional */}
852
- <Handle
853
- type="target"
854
- position={Position.Left}
855
- id="left"
856
- style={{
857
- background: '#3b82f6',
858
- width: '12px',
859
- height: '12px',
860
- border: '2px solid white',
861
- left: '-8px',
862
- opacity: 0, // Hidden but functional
863
- }}
864
- />
865
- <Handle
866
- type="source"
867
- position={Position.Right}
868
- id="right"
869
- style={{
870
- background: '#3b82f6',
871
- width: '12px',
872
- height: '12px',
873
- border: '2px solid white',
874
- right: '-8px',
875
- opacity: 0, // Hidden but functional
876
- }}
877
- />
878
- </Box>
879
-
880
- {/* AI Suggestions Button - Positioned below the node box */}
881
- {data.formData?.aiSuggestionsCount !== undefined && data.formData.aiSuggestionsCount > 0 && (
882
- <Box
883
- sx={{
884
- position: 'absolute',
885
- top: '100%',
886
- left: '50%',
887
- transform: 'translateX(-50%)',
888
- marginTop: '12px',
889
- zIndex: 10,
890
- whiteSpace: 'nowrap',
891
- }}
892
- onClick={(e) => {
893
- e.stopPropagation();
894
- // Toggle AI Suggestions panel
895
- setShowSuggestions(!showSuggestions);
896
- }}
897
- >
898
- <Button
899
- variant="contained"
900
- startIcon={<LightbulbIcon sx={{ fontSize: '12px' }} />}
901
- sx={{
902
- backgroundColor: '#2563EB',
903
- color: '#ffffff',
904
- borderRadius: '20px',
905
- textTransform: 'none',
906
- fontSize: '10px',
907
- fontWeight: 400,
908
- padding: '8px 16px',
909
- whiteSpace: 'nowrap',
910
- display: 'inline-flex',
911
- alignItems: 'center',
912
- boxShadow: '0 2px 8px rgba(0, 0, 0, 0.3)',
913
- '&:hover': {
914
- backgroundColor: '#2563eb',
915
- },
916
- '& .MuiButton-startIcon': {
917
- marginRight: '8px',
918
- }
919
- }}
920
- >
921
- AI Suggestions
922
- <Box
923
- component="span"
924
- sx={{
925
- marginLeft: '8px',
926
- backgroundColor: '#FFFFFF26',
927
- color: '#ffffff',
928
- fontSize: '10px',
929
- fontWeight: 400,
930
- minWidth: '18px',
931
- height: '18px',
932
- borderRadius: '9px',
933
- display: 'inline-flex',
934
- alignItems: 'center',
935
- justifyContent: 'center',
936
- padding: '0 6px',
937
- border: '1px solid rgba(255, 255, 255, 0.2)',
938
- }}
939
- >
940
- {data.formData.aiSuggestionsCount}
941
- </Box>
942
- </Button>
943
- </Box>
944
- )}
945
-
946
- {/* AI Suggestions Panel - Rendered on canvas below the button */}
947
- {showSuggestions && data.formData?.aiSuggestionsCount !== undefined && data.formData.aiSuggestionsCount > 0 && nodeId && (
948
- <AISuggestionsPanel
949
- suggestions={data.formData?.aiSuggestions || [
950
- {
951
- id: '1',
952
- title: 'Add Citation Extraction',
953
- description: 'Automatically extract and format citations from article content.',
954
- tags: ['classification', 'enhancement'],
955
- },
956
- {
957
- id: '2',
958
- title: 'Generate Bullet Summary',
959
- description: 'Create a concise bullet-point summary of the article\'s main points.',
960
- tags: ['classification', 'enhancement'],
961
- },
962
- ]}
963
- parentNodeId={nodeId}
964
- onSuggestionClick={(suggestion) => {
965
- console.log('Suggestion clicked:', suggestion);
966
- // Handle suggestion selection here
967
- }}
968
- onClose={() => setShowSuggestions(false)}
969
- />
970
- )}
971
- </Box>
972
- );
973
- };
974
-
1
+ import React, { useEffect, useState, useRef } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import { Handle, Position, useNodeId } from '@xyflow/react';
4
+ import { Box, Typography, Chip, IconButton, Card, CardContent, Button } from '@mui/material';
5
+ import {
6
+ TableChart as TableChartIcon,
7
+ AccessTime as AccessTimeIcon,
8
+ Save as SaveIcon,
9
+ Send as SendIcon,
10
+ Settings as SettingsIcon,
11
+ TableRows as TableRowsIcon,
12
+ Email as EmailIcon,
13
+ Description as DescriptionIcon,
14
+ Drafts as DraftsIcon,
15
+ Add as AddIcon,
16
+ CheckCircle as CheckCircleIcon,
17
+ Settings as ConfigureIcon,
18
+ Error as ErrorIcon,
19
+ ArrowForwardIos as ArrowForwardIcon,
20
+ WhatsApp as WhatsAppIcon,
21
+ Phone as PhoneIcon,
22
+ Lightbulb as LightbulbIcon
23
+ } from '@mui/icons-material';
24
+ import { RiCloseLine } from 'react-icons/ri';
25
+ import ReactJson from 'react-json-view';
26
+ import { getIconByName } from '../../utils/iconMapper';
27
+ import { useTranslation } from 'react-i18next';
28
+ import { useDiagram } from '../../contexts/DiagramProvider';
29
+ import { AISuggestion } from './AISuggestionsModal';
30
+ import { AISuggestionsPanel } from './AISuggestionsPanel';
31
+ import { NodeActionButtons } from './NodeActionButtons';
32
+ import { showNodeAIAssistantPopup } from './NodeAIAssistantPopup';
33
+
34
+ interface AutomationSheetsNodeProps {
35
+ data: {
36
+ label: string;
37
+ description: string;
38
+ status: 'Ready' | 'Running' | 'Completed' | 'Error';
39
+ inputVariable: string;
40
+ lastRun?: string;
41
+ iconName?: string;
42
+ sheetsConfig: {
43
+ spreadsheetId: string;
44
+ sheetName: string;
45
+ range: string;
46
+ credentials: {
47
+ type: 'service-account' | 'oauth' | 'api-key';
48
+ serviceAccountKey?: string;
49
+ oauthToken?: string;
50
+ apiKey?: string;
51
+ };
52
+ };
53
+ dataMapping: {
54
+ headers: Array<{
55
+ column: string;
56
+ sourceField: string;
57
+ dataType: 'string' | 'number' | 'date' | 'boolean';
58
+ }>;
59
+ appendMode: boolean;
60
+ clearSheet: boolean;
61
+ };
62
+ exportOptions: {
63
+ emailRecipients: string[];
64
+ fileName: string;
65
+ exportFormat: 'sheets' | 'excel' | 'both';
66
+ includeHeaders: boolean;
67
+ sendEmailNotification: boolean;
68
+ // Optional fields for Gmail output configuration (may live under formData.exportOptions)
69
+ emailSendEnabled?: boolean;
70
+ emailSender?: string;
71
+ emailSubject?: string;
72
+ emailMessage?: string;
73
+ // WhatsApp configuration
74
+ whatsapp?: {
75
+ enabled: boolean;
76
+ phoneNumber: string;
77
+ messageTemplate: string;
78
+ includeDataMode: 'json' | 'summary' | 'custom';
79
+ // Twilio configuration
80
+ twilio?: {
81
+ enabled: boolean;
82
+ accountSid: string;
83
+ authToken: string;
84
+ fromNumber: string;
85
+ isSandbox: boolean;
86
+ templateSid?: string;
87
+ templateVariables?: Record<string, string>;
88
+ };
89
+ };
90
+ };
91
+ formData?: {
92
+ aiSuggestionsCount?: number; // Number of AI suggestions available
93
+ [key: string]: any;
94
+ };
95
+ // Optional per-output-method statuses supplied by engine
96
+ outputStatuses?: {
97
+ googleSheets?: 'not-set' | 'configured' | 'running' | 'connected' | 'failed';
98
+ gmail?: 'not-set' | 'configured' | 'running' | 'connected' | 'failed';
99
+ whatsapp?: 'not-set' | 'configured' | 'running' | 'connected' | 'failed';
100
+ };
101
+ };
102
+ selected?: boolean;
103
+ }
104
+
105
+ export const AutomationSheetsNode: React.FC<AutomationSheetsNodeProps> = ({ data, selected }) => {
106
+ const { t } = useTranslation();
107
+ const [isJsonOpen, setIsJsonOpen] = useState(false);
108
+ const [showSuggestions, setShowSuggestions] = useState(false);
109
+ const rootRef = useRef<any>(null);
110
+ const portalRef = useRef<HTMLDivElement | null>(null);
111
+ const nodeRef = useRef<HTMLDivElement | null>(null);
112
+ const nodeId = useNodeId();
113
+ const setSelectedNode = useDiagram((state) => state.setSelectedNode);
114
+ const enableJson = useDiagram((state) => state.enableNodeJsonPopover ?? true);
115
+ const onNodesChange = useDiagram((state) => state.onNodesChange);
116
+ const nodes = useDiagram((state) => state.nodes);
117
+ const setNodes = useDiagram((state) => state.setNodes);
118
+
119
+ // Get the icon component based on the iconName
120
+ const IconComponent = getIconByName(data.iconName || 'TableChart');
121
+
122
+ // Resolve export options from formData or data
123
+ const exportOptions = (data.formData?.exportOptions || (data as any).exportOptions) || {};
124
+ const showSheets = Boolean(
125
+ exportOptions?.exportFormat === 'sheets' || exportOptions?.exportFormat === 'both'
126
+ );
127
+ const showGmail = Boolean(
128
+ exportOptions.emailSendEnabled &&
129
+ exportOptions.emailSender &&
130
+ (exportOptions.emailRecipients?.length || 0) > 0 &&
131
+ exportOptions.emailSubject &&
132
+ exportOptions.emailMessage
133
+ );
134
+ const showSlack = Boolean(
135
+ exportOptions.slack?.enabled &&
136
+ exportOptions.slack?.webhookUrl &&
137
+ exportOptions.slack?.channel
138
+ );
139
+ const showWhatsApp = Boolean(
140
+ exportOptions.whatsapp?.enabled &&
141
+ exportOptions.whatsapp?.phoneNumber
142
+ );
143
+
144
+ // Determine Google Sheets status from provided data and execution result
145
+ const determineGoogleSheetsStatus = (): 'not-set' | 'configured' | 'running' | 'connected' | 'failed' => {
146
+ // Prefer status provided by engine
147
+ if (data.outputStatuses?.googleSheets) return data.outputStatuses.googleSheets;
148
+
149
+ // Required config check (accepts either spreadsheetId or OAuth clientId that can create one)
150
+ const cfg = (data.formData?.sheetsConfig || data.sheetsConfig) || {};
151
+ const creds = cfg.credentials || {};
152
+ const hasRequired = Boolean((cfg.spreadsheetId || creds.clientId) && cfg.sheetName && creds.type);
153
+ if (!hasRequired) return 'not-set';
154
+
155
+ // Running inferred from node status
156
+ if (data.status === 'Running') return 'running';
157
+
158
+ // Execution result inference
159
+ const exec = data.formData?.executionResult || (data as any).executionResult;
160
+ if (exec?.success === true) return 'connected';
161
+ if (exec?.success === false || data.status === 'Error') return 'failed';
162
+ return 'configured';
163
+ };
164
+
165
+ // Determine Gmail status
166
+ const determineGmailStatus = (): 'not-set' | 'configured' | 'running' | 'connected' | 'failed' => {
167
+ // Prefer status provided by engine
168
+ if (data.outputStatuses?.gmail) return data.outputStatuses.gmail;
169
+
170
+ const ex = (data.formData?.exportOptions || data.exportOptions) || {};
171
+ const hasRequired = Boolean(
172
+ ex.emailSendEnabled && ex.emailSender && (ex.emailRecipients?.length || 0) > 0 && ex.emailSubject && ex.emailMessage
173
+ );
174
+ if (!hasRequired) return 'not-set';
175
+
176
+ if (data.status === 'Running') return 'running';
177
+
178
+ const exec = data.formData?.executionResult || (data as any).executionResult;
179
+ if (exec?.success === true) return 'connected';
180
+ if (exec?.success === false || data.status === 'Error') return 'failed';
181
+ return 'configured';
182
+ };
183
+
184
+ // Determine Slack status
185
+ const determineSlackStatus = (): 'not-set' | 'configured' | 'running' | 'connected' | 'failed' => {
186
+ if ((data as any).outputStatuses?.slack) return (data as any).outputStatuses.slack;
187
+ const slack = (data.formData?.exportOptions?.slack || (data as any).exportOptions?.slack) || {};
188
+ if (!slack.enabled || !slack.webhookUrl || !slack.channel) return 'not-set';
189
+ if (data.status === 'Running') return 'running';
190
+ const exec = data.formData?.executionResult || (data as any).executionResult;
191
+ if (exec?.success === true) return 'connected';
192
+ if (exec?.success === false || data.status === 'Error') return 'failed';
193
+ return 'configured';
194
+ };
195
+
196
+ // Determine WhatsApp status
197
+ const determineWhatsAppStatus = (): 'not-set' | 'configured' | 'running' | 'connected' | 'failed' => {
198
+ if (data.outputStatuses?.whatsapp) return data.outputStatuses.whatsapp;
199
+
200
+ const whatsapp = (data.formData?.exportOptions?.whatsapp || data.exportOptions?.whatsapp) || {};
201
+
202
+ // Check if Twilio is configured
203
+ if (whatsapp.twilio?.enabled) {
204
+ const twilioConfig = whatsapp.twilio;
205
+ const hasRequiredTwilio = Boolean(
206
+ twilioConfig.accountSid &&
207
+ twilioConfig.authToken &&
208
+ twilioConfig.fromNumber &&
209
+ whatsapp.phoneNumber
210
+ );
211
+ if (!hasRequiredTwilio) return 'not-set';
212
+ } else {
213
+ // Fallback to basic WhatsApp Web check
214
+ if (!whatsapp.enabled || !whatsapp.phoneNumber) return 'not-set';
215
+ }
216
+
217
+ if (data.status === 'Running') return 'running';
218
+
219
+ const exec = data.formData?.executionResult || (data as any).executionResult;
220
+ if (exec?.success === true) return 'connected';
221
+ if (exec?.success === false || data.status === 'Error') return 'failed';
222
+ return 'configured';
223
+ };
224
+
225
+ const getNodeStatus = (): string => {
226
+ const gs = determineGoogleSheetsStatus();
227
+ const gm = determineGmailStatus();
228
+ const sl = determineSlackStatus();
229
+ const wa = determineWhatsAppStatus();
230
+ if (gs === 'failed' || gm === 'failed' || sl === 'failed' || wa === 'failed') return 'Failed';
231
+ if (gs === 'running' || gm === 'running' || sl === 'running' || wa === 'running') return 'Running';
232
+ if (
233
+ (gs === 'connected' || gs === 'not-set') &&
234
+ (gm === 'connected' || gm === 'not-set') &&
235
+ (sl === 'connected' || sl === 'not-set') &&
236
+ (wa === 'connected' || wa === 'not-set')
237
+ ) return 'Success';
238
+ if (
239
+ (gs === 'configured' || gs === 'not-set') &&
240
+ (gm === 'configured' || gm === 'not-set') &&
241
+ (sl === 'configured' || sl === 'not-set') &&
242
+ (wa === 'configured' || wa === 'not-set')
243
+ ) return 'Ready';
244
+ return 'Mixed Status';
245
+ };
246
+
247
+ // Status badge (shared)
248
+ const StatusBadge = ({ status }: { status: 'not-set' | 'configured' | 'running' | 'connected' | 'failed' }) => {
249
+ const cfg = (
250
+ status === 'not-set' ? { icon: <ErrorIcon sx={{ fontSize: '12px' }} />, label: t('automation.sheetsNode.status.notSet'), bg: '#6b7280' } :
251
+ status === 'configured' ? { icon: <ConfigureIcon sx={{ fontSize: '12px' }} />, label: t('automation.sheetsNode.status.configured'), bg: '#3b82f6' } :
252
+ status === 'running' ? { icon: <SettingsIcon sx={{ fontSize: '12px' }} />, label: t('automation.sheetsNode.status.running'), bg: '#f59e0b' } :
253
+ status === 'connected' ? { icon: <CheckCircleIcon sx={{ fontSize: '12px' }} />, label: t('automation.sheetsNode.status.connected'), bg: '#10b981' } :
254
+ { icon: <ErrorIcon sx={{ fontSize: '12px' }} />, label: t('automation.sheetsNode.status.failed'), bg: '#ef4444' }
255
+ );
256
+ return (
257
+ <Chip
258
+ icon={cfg.icon}
259
+ label={cfg.label}
260
+ size="small"
261
+ sx={{
262
+ backgroundColor: cfg.bg,
263
+ color: 'white',
264
+ fontSize: '11px',
265
+ height: '20px',
266
+ '& .MuiChip-icon': { fontSize: '12px' },
267
+ }}
268
+ />
269
+ );
270
+ };
271
+
272
+ // WhatsApp message sending function
273
+ const sendWhatsAppMessage = (phoneNumber: string, data: any) => {
274
+ const message = `
275
+ Automation Result:
276
+ ------------------
277
+ Name: ${data.name || 'Automation Workflow'}
278
+ Score: ${data.score || 'N/A'}
279
+ Status: ${data.status || 'Completed'}
280
+ Timestamp: ${new Date().toLocaleString()}
281
+ Data: ${JSON.stringify(data, null, 2)}
282
+ `;
283
+ const encodedMsg = encodeURIComponent(message);
284
+ const url = `https://wa.me/${phoneNumber}?text=${encodedMsg}`;
285
+ window.open(url, '_blank');
286
+ };
287
+
288
+ const handleJsonClick = () => {
289
+ if (nodeId) setSelectedNode(nodeId);
290
+ if (!enableJson) return;
291
+ setIsJsonOpen(!isJsonOpen);
292
+ };
293
+
294
+ const handleClose = () => {
295
+ setIsJsonOpen(false);
296
+ // Clean up portal
297
+ if (rootRef.current) {
298
+ rootRef.current.unmount();
299
+ rootRef.current = null;
300
+ }
301
+ if (portalRef.current) {
302
+ document.body.removeChild(portalRef.current);
303
+ portalRef.current = null;
304
+ }
305
+ };
306
+
307
+ useEffect(() => {
308
+ const handleClickOutside = (event: MouseEvent) => {
309
+ if (isJsonOpen && !(event.target as Element).closest('#automation-json-popover')) {
310
+ handleClose();
311
+ }
312
+ };
313
+ document.addEventListener('mousedown', handleClickOutside);
314
+ return () => {
315
+ document.removeEventListener('mousedown', handleClickOutside);
316
+ };
317
+ }, [isJsonOpen]);
318
+
319
+
320
+ useEffect(() => {
321
+ if (isJsonOpen) {
322
+ const portalRoot = document.createElement('div');
323
+ document.body.appendChild(portalRoot);
324
+ portalRef.current = portalRoot;
325
+
326
+ const root = createRoot(portalRoot);
327
+ rootRef.current = root;
328
+
329
+ root.render(
330
+ <Card
331
+ id="automation-json-popover"
332
+ sx={{
333
+ position: 'fixed',
334
+ top: 0,
335
+ right: 0,
336
+ zIndex: 9999,
337
+ width: '400px',
338
+ height: '100vh',
339
+ overflow: 'auto',
340
+ bgcolor: '#242424',
341
+ color: '#fff',
342
+ border: '1px solid #333',
343
+ '&::-webkit-scrollbar': {
344
+ width: '6px',
345
+ },
346
+ '&::-webkit-scrollbar-track': {
347
+ background: 'transparent',
348
+ },
349
+ '&::-webkit-scrollbar-thumb': {
350
+ background: '#444',
351
+ borderRadius: '3px',
352
+ '&:hover': {
353
+ background: '#666',
354
+ },
355
+ },
356
+ }}
357
+ >
358
+ <CardContent sx={{ bgcolor: '#242424', color: '#fff' }}>
359
+ <IconButton
360
+ aria-label="close"
361
+ onClick={handleClose}
362
+ sx={{
363
+ color: '#999',
364
+ '&:hover': {
365
+ color: '#fff',
366
+ bgcolor: 'rgba(255, 255, 255, 0.1)',
367
+ },
368
+ }}
369
+ >
370
+ <RiCloseLine />
371
+ </IconButton>
372
+ {/* Show execution result prominently if available */}
373
+ {data.formData?.executionResult && (
374
+ <Box sx={{ mb: 2 }}>
375
+ <Typography variant="h6" sx={{ color: '#fff', mb: 1 }}>
376
+ {t('automation.common.executionResult')}
377
+ </Typography>
378
+ <Box sx={{
379
+ bgcolor: data.formData.executionResult.success ? '#1e3a8a' : '#dc2626',
380
+ p: 1,
381
+ borderRadius: 1,
382
+ mb: 1
383
+ }}>
384
+ <Typography variant="body2" sx={{ color: '#fff' }}>
385
+ {t('automation.common.status')}: {data.formData.executionResult.success ? t('automation.common.success') : t('automation.common.failed')}
386
+ </Typography>
387
+ <Typography variant="body2" sx={{ color: '#fff' }}>
388
+ {t('automation.common.timestamp')}: {new Date(data.formData.executionResult.timestamp).toLocaleString()}
389
+ </Typography>
390
+ {data.formData.executionResult.error && (
391
+ <Typography variant="body2" sx={{ color: '#fff' }}>
392
+ {t('automation.common.error')}: {data.formData.executionResult.error}
393
+ </Typography>
394
+ )}
395
+ </Box>
396
+ <Typography variant="h6" sx={{ color: '#fff', mb: 1 }}>
397
+ Sheets Data
398
+ </Typography>
399
+ <ReactJson
400
+ theme={'monokai'}
401
+ src={data.formData.executionResult.data}
402
+ collapsed={false}
403
+ />
404
+ </Box>
405
+ )}
406
+
407
+ {/* Show full node data */}
408
+ <Typography variant="h6" sx={{ color: '#fff', mb: 1 }}>
409
+ Full Node Data
410
+ </Typography>
411
+ <ReactJson theme={'monokai'} src={data.formData || data} collapsed={false} />
412
+ </CardContent>
413
+ </Card>
414
+ );
415
+ } else {
416
+ // Clean up when closing
417
+ if (rootRef.current) {
418
+ rootRef.current.unmount();
419
+ rootRef.current = null;
420
+ }
421
+ if (portalRef.current) {
422
+ document.body.removeChild(portalRef.current);
423
+ portalRef.current = null;
424
+ }
425
+ }
426
+ }, [isJsonOpen, data]);
427
+
428
+ // Output Method Component for Google Sheets
429
+ const GoogleSheetsOutputMethod = () => (
430
+ <Box sx={{
431
+ display: 'flex',
432
+ alignItems: 'center',
433
+ justifyContent: 'space-between',
434
+ py: 2,
435
+ px: 2,
436
+ borderBottom: '1px solid #334155',
437
+ '&:hover': {
438
+ backgroundColor: 'rgba(255, 255, 255, 0.05)',
439
+ },
440
+ cursor: 'pointer',
441
+ }}>
442
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
443
+ {/* Google Sheets Icon */}
444
+ <Box sx={{
445
+ width: '32px',
446
+ height: '32px',
447
+ backgroundColor: '#10b981',
448
+ borderRadius: '6px',
449
+ display: 'flex',
450
+ alignItems: 'center',
451
+ justifyContent: 'center',
452
+ }}>
453
+ <TableChartIcon sx={{ color: 'white', fontSize: '18px' }} />
454
+ </Box>
455
+
456
+ <Box>
457
+ <Typography variant="body2" sx={{
458
+ color: '#ffffff',
459
+ fontSize: '14px',
460
+ fontWeight: 600,
461
+ mb: 0.5
462
+ }}>
463
+ {t('automation.sheetsNode.googleSheets')}
464
+ </Typography>
465
+ <Typography variant="body2" sx={{
466
+ color: '#94a3b8',
467
+ fontSize: '12px',
468
+ mb: 0.5
469
+ }}>
470
+ {t('automation.sheetsNode.readWriteRows')}
471
+ </Typography>
472
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
473
+ <DescriptionIcon sx={{ fontSize: '12px', color: '#94a3b8' }} />
474
+ <Typography variant="body2" sx={{
475
+ color: '#94a3b8',
476
+ fontSize: '11px'
477
+ }}>
478
+ {t('automation.sheetsNode.sheet')}: {data.sheetsConfig?.sheetName || data.formData?.sheetsConfig?.sheetName || 'Sheet'}
479
+ </Typography>
480
+ </Box>
481
+ </Box>
482
+ </Box>
483
+
484
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
485
+ {/* Status Badge */}
486
+ <StatusBadge status={determineGoogleSheetsStatus()} />
487
+ <ArrowForwardIcon sx={{ fontSize: '14px', color: '#94a3b8' }} />
488
+ </Box>
489
+ </Box>
490
+ );
491
+
492
+ // Output Method Component for Gmail
493
+ const GmailOutputMethod = () => (
494
+ <Box sx={{
495
+ display: 'flex',
496
+ alignItems: 'center',
497
+ justifyContent: 'space-between',
498
+ py: 2,
499
+ px: 2,
500
+ borderBottom: '1px solid #334155',
501
+ '&:hover': {
502
+ backgroundColor: 'rgba(255, 255, 255, 0.05)',
503
+ },
504
+ cursor: 'pointer',
505
+ }}>
506
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
507
+ {/* Gmail Icon */}
508
+ <Box sx={{
509
+ width: '32px',
510
+ height: '32px',
511
+ backgroundColor: '#dc2626',
512
+ borderRadius: '6px',
513
+ display: 'flex',
514
+ alignItems: 'center',
515
+ justifyContent: 'center',
516
+ }}>
517
+ <EmailIcon sx={{ color: 'white', fontSize: '18px' }} />
518
+ </Box>
519
+
520
+ <Box>
521
+ <Typography variant="body2" sx={{
522
+ color: '#ffffff',
523
+ fontSize: '14px',
524
+ fontWeight: 600,
525
+ mb: 0.5
526
+ }}>
527
+ {t('automation.sheetsNode.gmail')}
528
+ </Typography>
529
+ <Typography variant="body2" sx={{
530
+ color: '#94a3b8',
531
+ fontSize: '12px',
532
+ mb: 0.5
533
+ }}>
534
+ {t('automation.sheetsNode.sendEmail')}
535
+ </Typography>
536
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
537
+ <DraftsIcon sx={{ fontSize: '12px', color: '#94a3b8' }} />
538
+ <Typography variant="body2" sx={{
539
+ color: '#94a3b8',
540
+ fontSize: '11px'
541
+ }}>
542
+ {t('automation.sheetsNode.drafts')}: {data.exportOptions?.emailRecipients?.length || 0}
543
+ </Typography>
544
+ </Box>
545
+ </Box>
546
+ </Box>
547
+
548
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
549
+ {/* Status Badge */}
550
+ <StatusBadge status={determineGmailStatus()} />
551
+ <ArrowForwardIcon sx={{ fontSize: '14px', color: '#94a3b8' }} />
552
+ </Box>
553
+ </Box>
554
+ );
555
+
556
+ // Output Method Component for Slack
557
+ const SlackOutputMethod = () => {
558
+ const slack = (data.formData?.exportOptions?.slack || (data as any).exportOptions?.slack) || {};
559
+ return (
560
+ <Box sx={{
561
+ display: 'flex',
562
+ alignItems: 'center',
563
+ justifyContent: 'space-between',
564
+ py: 2,
565
+ px: 2,
566
+ borderBottom: '1px solid #334155',
567
+ '&:hover': {
568
+ backgroundColor: 'rgba(255, 255, 255, 0.05)',
569
+ },
570
+ cursor: 'pointer',
571
+ }}>
572
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
573
+ {/* Slack Icon-like box */}
574
+ <Box sx={{
575
+ width: '32px',
576
+ height: '32px',
577
+ backgroundColor: '#4A154B',
578
+ borderRadius: '6px',
579
+ display: 'flex',
580
+ alignItems: 'center',
581
+ justifyContent: 'center',
582
+ }}>
583
+ <SendIcon sx={{ color: 'white', fontSize: '18px' }} />
584
+ </Box>
585
+
586
+ <Box>
587
+ <Typography variant="body2" sx={{
588
+ color: '#ffffff',
589
+ fontSize: '14px',
590
+ fontWeight: 600,
591
+ mb: 0.5
592
+ }}>
593
+ {t('automation.sheetsNode.slack')}
594
+ </Typography>
595
+ <Typography variant="body2" sx={{
596
+ color: '#94a3b8',
597
+ fontSize: '12px',
598
+ mb: 0.5
599
+ }}>
600
+ {t('automation.sheetsNode.sendMessage')}
601
+ </Typography>
602
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
603
+ <TableRowsIcon sx={{ fontSize: '12px', color: '#94a3b8' }} />
604
+ <Typography variant="body2" sx={{
605
+ color: '#94a3b8',
606
+ fontSize: '11px'
607
+ }}>
608
+ {t('automation.sheetsNode.channel')}: {slack.channel ? `#${slack.channel}` : t('automation.sheetsNode.notSet')}
609
+ </Typography>
610
+ </Box>
611
+ </Box>
612
+ </Box>
613
+
614
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
615
+ <StatusBadge status={determineSlackStatus()} />
616
+ <ArrowForwardIcon sx={{ fontSize: '14px', color: '#94a3b8' }} />
617
+ </Box>
618
+ </Box>
619
+ );
620
+ };
621
+
622
+ // Output Method Component for WhatsApp
623
+ const WhatsAppOutputMethod = () => {
624
+ const whatsapp = (data.formData?.exportOptions?.whatsapp || data.exportOptions?.whatsapp) || {};
625
+ const isTwilioEnabled = whatsapp.twilio?.enabled;
626
+
627
+ return (
628
+ <Box sx={{
629
+ display: 'flex',
630
+ alignItems: 'center',
631
+ justifyContent: 'space-between',
632
+ py: 2,
633
+ px: 2,
634
+ borderBottom: '1px solid #334155',
635
+ '&:hover': {
636
+ backgroundColor: 'rgba(255, 255, 255, 0.05)',
637
+ },
638
+ cursor: 'pointer',
639
+ }}
640
+ onClick={() => {
641
+ if (whatsapp.enabled && whatsapp.phoneNumber && !isTwilioEnabled) {
642
+ const executionData = data.formData?.executionResult || {};
643
+ sendWhatsAppMessage(whatsapp.phoneNumber, executionData);
644
+ }
645
+ }}>
646
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
647
+ {/* WhatsApp Icon */}
648
+ <Box sx={{
649
+ width: '32px',
650
+ height: '32px',
651
+ backgroundColor: '#25D366',
652
+ borderRadius: '6px',
653
+ display: 'flex',
654
+ alignItems: 'center',
655
+ justifyContent: 'center',
656
+ }}>
657
+ <WhatsAppIcon sx={{ color: 'white', fontSize: '18px' }} />
658
+ </Box>
659
+
660
+ <Box>
661
+ <Typography variant="body2" sx={{
662
+ color: '#ffffff',
663
+ fontSize: '14px',
664
+ fontWeight: 600,
665
+ mb: 0.5
666
+ }}>
667
+ {isTwilioEnabled ? t('automation.sheetsNode.whatsappTwilio') : t('automation.sheetsNode.whatsappWeb')}
668
+ </Typography>
669
+ <Typography variant="body2" sx={{
670
+ color: '#94a3b8',
671
+ fontSize: '12px',
672
+ mb: 0.5
673
+ }}>
674
+ {isTwilioEnabled ? t('automation.sheetsNode.apiSend') : t('automation.sheetsNode.sendMessage')}
675
+ </Typography>
676
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
677
+ <PhoneIcon sx={{ fontSize: '12px', color: '#94a3b8' }} />
678
+ <Typography variant="body2" sx={{
679
+ color: '#94a3b8',
680
+ fontSize: '11px'
681
+ }}>
682
+ {t('automation.sheetsNode.phone')}: {whatsapp.phoneNumber || t('automation.sheetsNode.notSet')}
683
+ </Typography>
684
+ </Box>
685
+ {isTwilioEnabled && (
686
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mt: 0.5 }}>
687
+ <Typography variant="body2" sx={{
688
+ color: whatsapp.twilio?.isSandbox ? '#f59e0b' : '#10b981',
689
+ fontSize: '10px',
690
+ fontWeight: 500
691
+ }}>
692
+ {whatsapp.twilio?.isSandbox ? t('automation.sheetsNode.sandboxMode') : t('automation.sheetsNode.productionMode')}
693
+ </Typography>
694
+ </Box>
695
+ )}
696
+ </Box>
697
+ </Box>
698
+
699
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
700
+ <StatusBadge status={determineWhatsAppStatus()} />
701
+ <ArrowForwardIcon sx={{ fontSize: '14px', color: '#94a3b8' }} />
702
+ </Box>
703
+ </Box>
704
+ );
705
+ };
706
+
707
+ return (
708
+ <Box
709
+ sx={{
710
+ position: 'relative',
711
+ width: '380px',
712
+ overflow: 'visible',
713
+ }}
714
+ >
715
+ <Box
716
+ ref={nodeRef}
717
+ sx={{
718
+ width: '380px',
719
+ minHeight: '280px',
720
+ backgroundColor: '#181C25', // Darker background like in image
721
+ border: selected ? '2px solid #3b82f6' : '1px solid #1e293b',
722
+ borderRadius: '12px',
723
+ padding: '0',
724
+ color: '#ffffff',
725
+ position: 'relative',
726
+ boxShadow: selected ? '0 0 0 2px rgba(59, 130, 246, 0.5)' : '0 4px 8px rgba(0, 0, 0, 0.3)',
727
+ transition: 'all 0.2s ease',
728
+ cursor: 'pointer',
729
+ overflow: 'hidden',
730
+ ...(data.status === 'Running' && {
731
+ animation: 'pulse-glow 2s ease-in-out infinite',
732
+ '@keyframes pulse-glow': {
733
+ '0%, 100%': {
734
+ boxShadow: '0 0 20px rgba(59, 130, 246, 0.4), 0 0 40px rgba(59, 130, 246, 0.2)',
735
+ borderColor: 'rgba(59, 130, 246, 0.6)',
736
+ },
737
+ '50%': {
738
+ boxShadow: '0 0 30px rgba(59, 130, 246, 0.6), 0 0 60px rgba(59, 130, 246, 0.3)',
739
+ borderColor: 'rgba(59, 130, 246, 0.9)',
740
+ },
741
+ },
742
+ }),
743
+ }}
744
+ onClick={handleJsonClick}
745
+ >
746
+ {/* Header */}
747
+ <Box sx={{
748
+ display: 'flex',
749
+ alignItems: 'center',
750
+ justifyContent: 'space-between',
751
+ p: 3,
752
+ borderBottom: '1px solid #334155'
753
+ }}>
754
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
755
+ <Box
756
+ sx={{
757
+ width: '32px',
758
+ height: '32px',
759
+ backgroundColor: '#3b82f6', // Blue color like in image
760
+ borderRadius: '6px',
761
+ display: 'flex',
762
+ alignItems: 'center',
763
+ justifyContent: 'center',
764
+ }}
765
+ >
766
+ <IconComponent sx={{ color: 'white', fontSize: '18px' }} />
767
+ </Box>
768
+ <Typography variant="h6" sx={{ fontWeight: 600, fontSize: '16px' }}>
769
+ {data.label}
770
+ </Typography>
771
+ </Box>
772
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
773
+ {/* Status Chip */}
774
+ <Chip
775
+ label={(() => {
776
+ // Map node status to standard status values
777
+ const nodeStatus = getNodeStatus();
778
+ if (nodeStatus === 'Failed' || data.status === 'Error') return 'Error';
779
+ if (nodeStatus === 'Running' || data.status === 'Running') return 'Running';
780
+ if (nodeStatus === 'Success' || data.status === 'Completed') return 'Completed';
781
+ return data.status || 'Ready';
782
+ })()}
783
+ size="small"
784
+ sx={{
785
+ backgroundColor: (() => {
786
+ const status = data.status || 'Ready';
787
+ return status === 'Completed'
788
+ ? 'rgba(37, 99, 235, 0.1)'
789
+ : status === 'Running'
790
+ ? 'rgba(251, 191, 36, 0.1)'
791
+ : status === 'Error'
792
+ ? 'rgba(239, 68, 68, 0.1)'
793
+ : 'rgba(16, 185, 129, 0.1)';
794
+ })(),
795
+ color: (() => {
796
+ const status = data.status || 'Ready';
797
+ return status === 'Completed'
798
+ ? '#93C5FD'
799
+ : status === 'Running'
800
+ ? '#FCD34D'
801
+ : status === 'Error'
802
+ ? '#FCA5A5'
803
+ : '#86EFAC';
804
+ })(),
805
+ fontWeight: 500,
806
+ fontSize: '12px',
807
+ height: '24px',
808
+ borderRadius: '12px',
809
+ }}
810
+ />
811
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
812
+ <AccessTimeIcon sx={{ fontSize: '12px', color: '#94a3b8' }} />
813
+ <Typography variant="body2" sx={{ color: '#94a3b8', fontSize: '11px' }}>
814
+ {t('automation.common.lastRun')}: {data.lastRun || '2m ago'}
815
+ </Typography>
816
+ </Box>
817
+ </Box>
818
+ </Box>
819
+
820
+ {/* Output Methods */}
821
+ <Box>
822
+ {showSheets && <GoogleSheetsOutputMethod />}
823
+ {showGmail && <GmailOutputMethod />}
824
+ {showSlack && <SlackOutputMethod />}
825
+ {showWhatsApp && <WhatsAppOutputMethod />}
826
+ {!showGmail && !showSlack && !showWhatsApp && (
827
+ <Typography variant="body2" sx={{ color: '#94a3b8', p: 2 }}>
828
+ {t('automation.sheetsNode.noOutputMethodsConfigured')}
829
+ </Typography>
830
+ )}
831
+ </Box>
832
+
833
+ {/* Add Configuration Button */}
834
+ <Box sx={{
835
+ p: 2,
836
+ display: 'flex',
837
+ justifyContent: 'center',
838
+ borderTop: '1px solid #334155'
839
+ }}>
840
+ <Button
841
+ startIcon={<AddIcon sx={{ fontSize: '16px' }} />}
842
+ sx={{
843
+ color: '#94a3b8',
844
+ fontSize: '12px',
845
+ textTransform: 'none',
846
+ '&:hover': {
847
+ backgroundColor: 'rgba(255, 255, 255, 0.05)',
848
+ color: '#ffffff',
849
+ },
850
+ }}
851
+ >
852
+ {t('automation.sheetsNode.addConfiguration')}
853
+ </Button>
854
+ </Box>
855
+
856
+ {/* Connection Handles - Bidirectional (source + target at each position) */}
857
+ {/* Top - Source */}
858
+ <Handle
859
+ type="source"
860
+ position={Position.Top}
861
+ id="top-source"
862
+ className="connection-handle"
863
+ style={{
864
+ background: selected ? '#10B981' : '#1a1a2e',
865
+ width: '14px',
866
+ height: '14px',
867
+ border: '3px solid #10B981',
868
+ top: '-8px',
869
+ opacity: selected ? 1 : 0,
870
+ transition: 'all 0.2s ease-in-out',
871
+ cursor: 'crosshair',
872
+ zIndex: 10,
873
+ }}
874
+ />
875
+ {/* Top - Target (hidden but functional) */}
876
+ <Handle
877
+ type="target"
878
+ position={Position.Top}
879
+ id="top-target"
880
+ style={{
881
+ background: 'transparent',
882
+ width: '14px',
883
+ height: '14px',
884
+ border: 'none',
885
+ top: '-8px',
886
+ opacity: 0,
887
+ pointerEvents: selected ? 'all' : 'none',
888
+ }}
889
+ />
890
+ {/* Bottom - Source */}
891
+ <Handle
892
+ type="source"
893
+ position={Position.Bottom}
894
+ id="bottom-source"
895
+ className="connection-handle"
896
+ style={{
897
+ background: selected ? '#10B981' : '#1a1a2e',
898
+ width: '14px',
899
+ height: '14px',
900
+ border: '3px solid #10B981',
901
+ bottom: '-8px',
902
+ opacity: selected ? 1 : 0,
903
+ transition: 'all 0.2s ease-in-out',
904
+ cursor: 'crosshair',
905
+ zIndex: 10,
906
+ }}
907
+ />
908
+ {/* Bottom - Target (hidden but functional) */}
909
+ <Handle
910
+ type="target"
911
+ position={Position.Bottom}
912
+ id="bottom-target"
913
+ style={{
914
+ background: 'transparent',
915
+ width: '14px',
916
+ height: '14px',
917
+ border: 'none',
918
+ bottom: '-8px',
919
+ opacity: 0,
920
+ pointerEvents: selected ? 'all' : 'none',
921
+ }}
922
+ />
923
+ {/* Left - Source */}
924
+ <Handle
925
+ type="source"
926
+ position={Position.Left}
927
+ id="left-source"
928
+ className="connection-handle"
929
+ style={{
930
+ background: selected ? '#10B981' : '#1a1a2e',
931
+ width: '14px',
932
+ height: '14px',
933
+ border: '3px solid #10B981',
934
+ left: '-8px',
935
+ opacity: selected ? 1 : 0,
936
+ transition: 'all 0.2s ease-in-out',
937
+ cursor: 'crosshair',
938
+ zIndex: 10,
939
+ }}
940
+ />
941
+ {/* Left - Target (hidden but functional) */}
942
+ <Handle
943
+ type="target"
944
+ position={Position.Left}
945
+ id="left-target"
946
+ style={{
947
+ background: 'transparent',
948
+ width: '14px',
949
+ height: '14px',
950
+ border: 'none',
951
+ left: '-8px',
952
+ opacity: 0,
953
+ pointerEvents: selected ? 'all' : 'none',
954
+ }}
955
+ />
956
+ {/* Right - Source */}
957
+ <Handle
958
+ type="source"
959
+ position={Position.Right}
960
+ id="right-source"
961
+ className="connection-handle"
962
+ style={{
963
+ background: selected ? '#10B981' : '#1a1a2e',
964
+ width: '14px',
965
+ height: '14px',
966
+ border: '3px solid #10B981',
967
+ right: '-8px',
968
+ opacity: selected ? 1 : 0,
969
+ transition: 'all 0.2s ease-in-out',
970
+ cursor: 'crosshair',
971
+ zIndex: 10,
972
+ }}
973
+ />
974
+ {/* Right - Target (hidden but functional) */}
975
+ <Handle
976
+ type="target"
977
+ position={Position.Right}
978
+ id="right-target"
979
+ style={{
980
+ background: 'transparent',
981
+ width: '14px',
982
+ height: '14px',
983
+ border: 'none',
984
+ right: '-8px',
985
+ opacity: 0,
986
+ pointerEvents: selected ? 'all' : 'none',
987
+ }}
988
+ />
989
+
990
+ </Box>
991
+
992
+ {/* Node Action Buttons - Shows when selected */}
993
+ <NodeActionButtons
994
+ selected={selected}
995
+ onOpenAIAssistant={(buttonElement) => {
996
+ if (nodeId) {
997
+ showNodeAIAssistantPopup(nodeId, 'Sheets Node', buttonElement);
998
+ }
999
+ }}
1000
+ onDelete={() => {
1001
+ if (nodeId && onNodesChange) {
1002
+ onNodesChange([{ id: nodeId, type: 'remove' }]);
1003
+ }
1004
+ }}
1005
+ onDuplicate={() => {
1006
+ if (nodeId) {
1007
+ const currentNode = nodes.find(n => n.id === nodeId);
1008
+ if (currentNode) {
1009
+ const newNode = {
1010
+ ...currentNode,
1011
+ id: `${currentNode.id}-copy-${Date.now()}`,
1012
+ position: {
1013
+ x: currentNode.position.x + 50,
1014
+ y: currentNode.position.y + 50,
1015
+ },
1016
+ selected: false,
1017
+ };
1018
+ setNodes([...nodes, newNode]);
1019
+ }
1020
+ }
1021
+ }}
1022
+ />
1023
+
1024
+ {/* AI Suggestions Button - Positioned below the node box */}
1025
+ {data.formData?.aiSuggestionsCount !== undefined && data.formData.aiSuggestionsCount > 0 && (
1026
+ <Box
1027
+ sx={{
1028
+ position: 'absolute',
1029
+ top: '100%',
1030
+ left: '50%',
1031
+ transform: 'translateX(-50%)',
1032
+ marginTop: '12px',
1033
+ zIndex: 10,
1034
+ whiteSpace: 'nowrap',
1035
+ }}
1036
+ onClick={(e) => {
1037
+ e.stopPropagation();
1038
+ // Toggle AI Suggestions panel
1039
+ setShowSuggestions(!showSuggestions);
1040
+ }}
1041
+ >
1042
+ <Button
1043
+ variant="contained"
1044
+ startIcon={<LightbulbIcon sx={{ fontSize: '12px' }} />}
1045
+ sx={{
1046
+ backgroundColor: '#2563EB',
1047
+ color: '#ffffff',
1048
+ borderRadius: '20px',
1049
+ textTransform: 'none',
1050
+ fontSize: '10px',
1051
+ fontWeight: 400,
1052
+ padding: '8px 16px',
1053
+ whiteSpace: 'nowrap',
1054
+ display: 'inline-flex',
1055
+ alignItems: 'center',
1056
+ boxShadow: '0 2px 8px rgba(0, 0, 0, 0.3)',
1057
+ '&:hover': {
1058
+ backgroundColor: '#2563eb',
1059
+ },
1060
+ '& .MuiButton-startIcon': {
1061
+ marginRight: '8px',
1062
+ }
1063
+ }}
1064
+ >
1065
+ AI Suggestions
1066
+ <Box
1067
+ component="span"
1068
+ sx={{
1069
+ marginLeft: '8px',
1070
+ backgroundColor: '#FFFFFF26',
1071
+ color: '#ffffff',
1072
+ fontSize: '10px',
1073
+ fontWeight: 400,
1074
+ minWidth: '18px',
1075
+ height: '18px',
1076
+ borderRadius: '9px',
1077
+ display: 'inline-flex',
1078
+ alignItems: 'center',
1079
+ justifyContent: 'center',
1080
+ padding: '0 6px',
1081
+ border: '1px solid rgba(255, 255, 255, 0.2)',
1082
+ }}
1083
+ >
1084
+ {data.formData.aiSuggestionsCount}
1085
+ </Box>
1086
+ </Button>
1087
+ </Box>
1088
+ )}
1089
+
1090
+ {/* AI Suggestions Panel - Rendered on canvas below the button */}
1091
+ {showSuggestions && data.formData?.aiSuggestionsCount !== undefined && data.formData.aiSuggestionsCount > 0 && nodeId && (
1092
+ <AISuggestionsPanel
1093
+ suggestions={data.formData?.aiSuggestions || [
1094
+ {
1095
+ id: '1',
1096
+ title: 'Add Citation Extraction',
1097
+ description: 'Automatically extract and format citations from article content.',
1098
+ tags: ['classification', 'enhancement'],
1099
+ },
1100
+ {
1101
+ id: '2',
1102
+ title: 'Generate Bullet Summary',
1103
+ description: 'Create a concise bullet-point summary of the article\'s main points.',
1104
+ tags: ['classification', 'enhancement'],
1105
+ },
1106
+ ]}
1107
+ parentNodeId={nodeId}
1108
+ onSuggestionClick={(suggestion) => {
1109
+ console.log('Suggestion clicked:', suggestion);
1110
+ // Handle suggestion selection here
1111
+ }}
1112
+ onClose={() => setShowSuggestions(false)}
1113
+ />
1114
+ )}
1115
+ </Box>
1116
+ );
1117
+ };
1118
+