@flowuent-org/diagramming-core 1.2.1 → 1.2.2

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