@flowuent-org/diagramming-core 1.2.0 → 1.2.1

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