@flowuent-org/diagramming-core 1.3.7 → 1.3.9

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.
@@ -7,10 +7,10 @@ import {
7
7
  Chip,
8
8
  IconButton,
9
9
  Card,
10
+ CardContent,
10
11
  Button,
11
12
  CircularProgress,
12
13
  Tooltip,
13
- LinearProgress,
14
14
  } from '@mui/material';
15
15
  import {
16
16
  Description as DocsIcon,
@@ -20,16 +20,15 @@ import {
20
20
  TableChart as SheetsIcon,
21
21
  CalendarMonth as CalendarIcon,
22
22
  AccessTime as AccessTimeIcon,
23
- CheckCircle as CheckCircleIcon,
24
- Error as ErrorIcon,
25
- LinkOff as LinkOffIcon,
26
23
  } from '@mui/icons-material';
27
24
  import { FcGoogle } from 'react-icons/fc';
28
25
  import { RiCloseLine } from 'react-icons/ri';
29
26
  import ReactJson from 'react-json-view';
27
+ import { useTranslation } from 'react-i18next';
30
28
  import { useDiagram } from '../../contexts/DiagramProvider';
31
29
  import { useSearch } from '../../contexts/SearchContext';
32
30
  import { NodeActionButtons } from './NodeActionButtons';
31
+ import { showNodeAIAssistantPopup } from './NodeAIAssistantPopup';
33
32
  import { getStatusColor } from './statusColors';
34
33
 
35
34
  // ========================
@@ -219,13 +218,18 @@ export const AutomationGoogleServicesNode: React.FC<AutomationGoogleServicesNode
219
218
  data,
220
219
  selected,
221
220
  }) => {
221
+ const { t } = useTranslation();
222
222
  const { highlightText } = useSearch();
223
223
  const [isJsonOpen, setIsJsonOpen] = useState(false);
224
224
  const rootRef = useRef<any>(null);
225
225
  const portalRef = useRef<HTMLDivElement | null>(null);
226
+ const nodeRef = useRef<HTMLDivElement | null>(null);
226
227
  const nodeId = useNodeId();
227
228
  const setSelectedNode = useDiagram((state) => state.setSelectedNode);
228
229
  const enableJson = useDiagram((state) => state.enableNodeJsonPopover ?? true);
230
+ const onNodesChange = useDiagram((state) => state.onNodesChange);
231
+ const nodes = useDiagram((state) => state.nodes);
232
+ const setNodes = useDiagram((state) => state.setNodes);
229
233
 
230
234
  // Get service configuration
231
235
  const serviceType = data.serviceType || 'docs';
@@ -240,7 +244,6 @@ export const AutomationGoogleServicesNode: React.FC<AutomationGoogleServicesNode
240
244
 
241
245
  // Execution state
242
246
  const status = data.status || 'Ready';
243
- const executionProgress = status === 'Running' ? 50 : 0;
244
247
 
245
248
  // Status configuration - using centralized status colors
246
249
  const statusConfig = getStatusColor(status, 'ready');
@@ -254,6 +257,7 @@ export const AutomationGoogleServicesNode: React.FC<AutomationGoogleServicesNode
254
257
 
255
258
  const handleClose = () => {
256
259
  setIsJsonOpen(false);
260
+ // Clean up portal
257
261
  if (rootRef.current) {
258
262
  rootRef.current.unmount();
259
263
  rootRef.current = null;
@@ -277,7 +281,6 @@ export const AutomationGoogleServicesNode: React.FC<AutomationGoogleServicesNode
277
281
  };
278
282
  }, [isJsonOpen]);
279
283
 
280
- // JSON popover portal
281
284
  useEffect(() => {
282
285
  if (isJsonOpen) {
283
286
  const portalRoot = document.createElement('div');
@@ -301,74 +304,56 @@ export const AutomationGoogleServicesNode: React.FC<AutomationGoogleServicesNode
301
304
  bgcolor: '#242424',
302
305
  color: '#fff',
303
306
  border: '1px solid #333',
307
+ '&::-webkit-scrollbar': {
308
+ width: '6px',
309
+ },
310
+ '&::-webkit-scrollbar-track': {
311
+ background: 'transparent',
312
+ },
313
+ '&::-webkit-scrollbar-thumb': {
314
+ background: '#444',
315
+ borderRadius: '3px',
316
+ '&:hover': {
317
+ background: '#666',
318
+ },
319
+ },
304
320
  }}
305
321
  >
306
- <Box
307
- sx={{
308
- p: 2,
309
- borderBottom: '1px solid #333',
310
- display: 'flex',
311
- justifyContent: 'space-between',
312
- alignItems: 'center',
313
- }}
314
- >
315
- <Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
316
- {serviceConfig.label} Configuration
317
- </Typography>
318
- <IconButton size="small" onClick={handleClose} sx={{ color: '#fff' }}>
322
+ <CardContent sx={{ bgcolor: '#242424', color: '#fff' }}>
323
+ <IconButton
324
+ aria-label="close"
325
+ onClick={handleClose}
326
+ sx={{
327
+ color: '#999',
328
+ '&:hover': {
329
+ color: '#fff',
330
+ bgcolor: 'rgba(255, 255, 255, 0.1)',
331
+ },
332
+ }}
333
+ >
319
334
  <RiCloseLine />
320
335
  </IconButton>
321
- </Box>
322
- <Box sx={{ p: 2 }}>
323
- <ReactJson
324
- src={data}
325
- theme="monokai"
326
- collapsed={1}
327
- displayDataTypes={false}
328
- enableClipboard={false}
329
- style={{
330
- backgroundColor: 'transparent',
331
- fontSize: '12px',
332
- }}
333
- />
334
- </Box>
336
+ {/* Show full node data */}
337
+ <Typography variant="h6" sx={{ color: '#fff', mb: 1 }}>
338
+ {t('automation.common.fullNodeData')}
339
+ </Typography>
340
+ <ReactJson theme={'monokai'} src={data.formData || data} collapsed={false} />
341
+ </CardContent>
335
342
  </Card>
336
343
  );
337
- }
338
- return () => {
344
+ } else {
345
+ // Clean up when closing
339
346
  if (rootRef.current) {
340
347
  rootRef.current.unmount();
341
348
  rootRef.current = null;
342
349
  }
343
- if (portalRef.current && portalRef.current.parentNode) {
350
+ if (portalRef.current) {
344
351
  document.body.removeChild(portalRef.current);
345
352
  portalRef.current = null;
346
353
  }
347
- };
354
+ }
348
355
  }, [isJsonOpen, data]);
349
356
 
350
- // Render status badge
351
- const renderStatusBadge = () => (
352
- <Chip
353
- size="small"
354
- label={
355
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
356
- {(status === 'Completed' || status === 'success') && <CheckCircleIcon sx={{ fontSize: 12 }} />}
357
- {(status === 'Failed' || status === 'error') && <ErrorIcon sx={{ fontSize: 12 }} />}
358
- <span>{statusConfig.label}</span>
359
- </Box>
360
- }
361
- sx={{
362
- height: 20,
363
- fontSize: '10px',
364
- fontWeight: 600,
365
- color: statusConfig.color,
366
- bgcolor: statusConfig.bgColor,
367
- border: `1px solid ${statusConfig.color}20`,
368
- '& .MuiChip-label': { px: 1 },
369
- }}
370
- />
371
- );
372
357
 
373
358
  // Render Google Auth Button
374
359
  const renderGoogleAuthButton = () => {
@@ -437,234 +422,349 @@ export const AutomationGoogleServicesNode: React.FC<AutomationGoogleServicesNode
437
422
  );
438
423
  };
439
424
 
440
- // ========================
441
- // Render
442
- // ========================
425
+ // Get service details for display
426
+ const getServiceDetails = (): string => {
427
+ if (serviceType === 'docs' && data.parameters?.documentTitle) {
428
+ return data.parameters.documentTitle;
429
+ }
430
+ if (serviceType === 'slides' && data.parameters?.presentationTitle) {
431
+ return data.parameters.presentationTitle;
432
+ }
433
+ if (serviceType === 'meet' && data.parameters?.meetingTitle) {
434
+ return data.parameters.meetingTitle;
435
+ }
436
+ if (serviceType === 'gmail' && data.parameters?.subject) {
437
+ return data.parameters.subject;
438
+ }
439
+ if (serviceType === 'sheets' && data.parameters?.sheetName) {
440
+ return data.parameters.sheetName;
441
+ }
442
+ if (serviceType === 'calendar' && data.parameters?.eventTitle) {
443
+ return data.parameters.eventTitle;
444
+ }
445
+ return serviceConfig.description;
446
+ };
447
+
443
448
  return (
444
449
  <Box
445
450
  sx={{
446
- bgcolor: '#1E1E2E',
447
- borderRadius: 2,
448
- border: selected ? `2px solid ${serviceConfig.color}` : '1px solid #374151',
449
- minWidth: 280,
450
- maxWidth: 320,
451
- overflow: 'hidden',
452
- boxShadow: selected
453
- ? `0 0 20px ${serviceConfig.color}30`
454
- : '0 4px 12px rgba(0, 0, 0, 0.3)',
455
- transition: 'all 0.2s ease',
451
+ position: 'relative',
452
+ width: '336px',
453
+ overflow: 'visible',
456
454
  }}
457
- onClick={() => setSelectedNode(nodeId || '')}
458
455
  >
459
- {/* Input Handle */}
460
- <Handle
461
- type="target"
462
- position={Position.Left}
463
- style={{
464
- background: serviceConfig.color,
465
- width: 10,
466
- height: 10,
467
- border: '2px solid #1E1E2E',
468
- }}
469
- />
470
-
471
- {/* Header */}
472
456
  <Box
457
+ ref={nodeRef}
473
458
  sx={{
474
- bgcolor: `${serviceConfig.color}15`,
475
- borderBottom: `1px solid ${serviceConfig.color}30`,
476
- px: 2,
477
- py: 1.5,
478
- display: 'flex',
479
- alignItems: 'center',
480
- justifyContent: 'space-between',
459
+ width: '336px',
460
+ minHeight: '150px',
461
+ backgroundColor: '#181C25',
462
+ border: selected ? `2px solid ${serviceConfig.color}` : '1px solid #1e293b',
463
+ borderRadius: '12px',
464
+ color: '#ffffff',
465
+ position: 'relative',
466
+ boxShadow: selected ? `0 0 0 2px ${serviceConfig.color}50` : '0 4px 8px rgba(0, 0, 0, 0.3)',
467
+ transition: 'all 0.2s ease',
468
+ cursor: 'pointer',
469
+ overflow: 'hidden',
470
+ ...(status === 'Running' && {
471
+ animation: 'pulse-glow 2s ease-in-out infinite',
472
+ '@keyframes pulse-glow': {
473
+ '0%, 100%': {
474
+ boxShadow: `0 0 20px ${serviceConfig.color}40, 0 0 40px ${serviceConfig.color}20`,
475
+ borderColor: `${serviceConfig.color}60`,
476
+ },
477
+ '50%': {
478
+ boxShadow: `0 0 30px ${serviceConfig.color}60, 0 0 60px ${serviceConfig.color}30`,
479
+ borderColor: `${serviceConfig.color}90`,
480
+ },
481
+ },
482
+ }),
481
483
  }}
484
+ onClick={handleJsonClick}
482
485
  >
483
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
484
- <Box
485
- sx={{
486
- bgcolor: serviceConfig.color,
487
- borderRadius: 1,
488
- p: 0.75,
489
- display: 'flex',
490
- alignItems: 'center',
491
- justifyContent: 'center',
492
- }}
493
- >
494
- <ServiceIcon sx={{ fontSize: 18, color: '#fff' }} />
495
- </Box>
496
- <Box>
497
- <Typography
498
- variant="subtitle2"
499
- sx={{ color: '#fff', fontWeight: 600, fontSize: 13 }}
500
- >
501
- {highlightText(data.label || serviceConfig.label)}
502
- </Typography>
503
- <Typography
504
- variant="caption"
505
- sx={{ color: '#9CA3AF', fontSize: 10 }}
506
- >
507
- {data.description || serviceConfig.description}
508
- </Typography>
509
- </Box>
486
+ {/* Top Header Section */}
487
+ <Box sx={{
488
+ backgroundColor: "rgba(67, 93, 132, 0.1)",
489
+ padding: '8px 16px',
490
+ borderRadius: '12px 12px 0 0'
491
+ }}>
492
+ <Typography variant="body2" sx={{
493
+ color: '#ffffff',
494
+ fontSize: '12px',
495
+ fontWeight: 500
496
+ }}>
497
+ {highlightText(data.description || serviceConfig.description)}
498
+ </Typography>
510
499
  </Box>
511
- {renderStatusBadge()}
512
- </Box>
513
500
 
514
- {/* Content */}
515
- <Box sx={{ px: 2, py: 1.5 }}>
516
- {/* Google Auth Button - Show on all Google nodes */}
517
- <Box sx={{ mb: 1.5 }}>
518
- {renderGoogleAuthButton()}
519
- {!isAuthenticated && (
520
- <Typography
521
- variant="caption"
522
- sx={{
523
- color: '#6B7280',
524
- fontSize: 10,
525
- display: 'block',
526
- textAlign: 'center',
527
- mt: 0.5,
528
- }}
529
- >
530
- Click to authenticate with Google
531
- </Typography>
532
- )}
533
- </Box>
534
-
535
- {/* Progress Bar (when running) */}
536
- {status === 'Running' && (
537
- <Box sx={{ mb: 1.5 }}>
538
- <LinearProgress
539
- variant="determinate"
540
- value={executionProgress}
501
+ {/* Main Content */}
502
+ <Box sx={{ padding: '16px' }}>
503
+ {/* Title Section */}
504
+ <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
505
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
506
+ <Box
507
+ sx={{
508
+ width: '32px',
509
+ height: '32px',
510
+ minWidth: '32px',
511
+ backgroundColor: '#1E3A8A',
512
+ borderRadius: '50%',
513
+ display: 'flex',
514
+ alignItems: 'center',
515
+ justifyContent: 'center',
516
+ flexShrink: 0,
517
+ }}
518
+ >
519
+ <ServiceIcon sx={{ color: 'white', fontSize: '18px' }} />
520
+ </Box>
521
+ <Typography variant="h6" sx={{ fontWeight: 600, fontSize: '16px' }}>
522
+ {highlightText(data.label || serviceConfig.label)}
523
+ </Typography>
524
+ </Box>
525
+ <Chip
526
+ label={status}
527
+ size="small"
541
528
  sx={{
542
- height: 4,
543
- borderRadius: 2,
544
- bgcolor: 'rgba(59, 130, 246, 0.1)',
545
- '& .MuiLinearProgress-bar': {
546
- bgcolor: serviceConfig.color,
547
- },
529
+ backgroundColor: statusConfig.bgColor,
530
+ color: statusConfig.color,
531
+ fontWeight: 500,
532
+ fontSize: '12px',
533
+ height: '24px',
534
+ borderRadius: '12px',
548
535
  }}
549
536
  />
550
537
  </Box>
551
- )}
552
538
 
553
- {/* Token Expiry Info */}
554
- {isAuthenticated && (
555
- <Box sx={{ mb: 1.5 }}>
556
- <Tooltip title={`Token expires in ${getTimeUntilExpiry(data.parameters?.googleTokenExpiresAt)}`}>
557
- <Chip
558
- size="small"
559
- label={getTimeUntilExpiry(data.parameters?.googleTokenExpiresAt)}
560
- sx={{
561
- height: 18,
562
- fontSize: '9px',
563
- backgroundColor: tokenExpired ? '#EF4444' : '#3B82F6',
564
- color: '#FFFFFF',
565
- fontWeight: 500,
566
- }}
567
- />
568
- </Tooltip>
539
+ {/* Google Auth Button */}
540
+ <Box sx={{ mb: 2 }}>
541
+ {renderGoogleAuthButton()}
569
542
  </Box>
570
- )}
571
543
 
572
- {/* Service-specific Parameters Display */}
573
- <Box sx={{ mb: 1.5 }}>
574
- {serviceType === 'docs' && data.parameters?.documentTitle && (
575
- <Typography variant="caption" sx={{ color: '#9CA3AF', fontSize: 10 }}>
576
- Document: {data.parameters.documentTitle}
577
- </Typography>
578
- )}
579
- {serviceType === 'slides' && data.parameters?.presentationTitle && (
580
- <Typography variant="caption" sx={{ color: '#9CA3AF', fontSize: 10 }}>
581
- Presentation: {data.parameters.presentationTitle}
582
- </Typography>
583
- )}
584
- {serviceType === 'meet' && data.parameters?.meetingTitle && (
585
- <Typography variant="caption" sx={{ color: '#9CA3AF', fontSize: 10 }}>
586
- Meeting: {data.parameters.meetingTitle}
587
- </Typography>
588
- )}
589
- {serviceType === 'gmail' && data.parameters?.subject && (
590
- <Typography variant="caption" sx={{ color: '#9CA3AF', fontSize: 10 }}>
591
- Subject: {data.parameters.subject}
592
- </Typography>
593
- )}
594
- {serviceType === 'sheets' && data.parameters?.sheetName && (
595
- <Typography variant="caption" sx={{ color: '#9CA3AF', fontSize: 10 }}>
596
- Sheet: {data.parameters.sheetName}
597
- </Typography>
544
+ {/* Description Box */}
545
+ <Box sx={{
546
+ backgroundColor: '#1F2937',
547
+ borderRadius: '8px',
548
+ padding: '12px',
549
+ mb: 2,
550
+ border: '1px solid #374151'
551
+ }}>
552
+ {/* Inner text boundary box */}
553
+ <Box sx={{
554
+ backgroundColor: 'transparent',
555
+ borderRadius: '4px',
556
+ padding: '8px',
557
+ border: '1px solid #4B5563',
558
+ minHeight: '40px',
559
+ display: 'flex',
560
+ alignItems: 'center'
561
+ }}>
562
+ <Typography variant="body2" sx={{
563
+ color: '#9CA3AF',
564
+ fontSize: '12px',
565
+ lineHeight: 1.4,
566
+ margin: 0
567
+ }}>
568
+ {getServiceDetails()}
569
+ </Typography>
570
+ </Box>
571
+ </Box>
572
+
573
+ {/* Token Expiry Info */}
574
+ {isAuthenticated && (
575
+ <Box sx={{ mb: 2 }}>
576
+ <Tooltip title={`Token expires in ${getTimeUntilExpiry(data.parameters?.googleTokenExpiresAt)}`}>
577
+ <Chip
578
+ size="small"
579
+ label={getTimeUntilExpiry(data.parameters?.googleTokenExpiresAt)}
580
+ sx={{
581
+ height: 18,
582
+ fontSize: '9px',
583
+ backgroundColor: tokenExpired ? '#EF4444' : '#3B82F6',
584
+ color: '#FFFFFF',
585
+ fontWeight: 500,
586
+ }}
587
+ />
588
+ </Tooltip>
589
+ </Box>
598
590
  )}
599
- {serviceType === 'calendar' && data.parameters?.eventTitle && (
600
- <Typography variant="caption" sx={{ color: '#9CA3AF', fontSize: 10 }}>
601
- Event: {data.parameters.eventTitle}
602
- </Typography>
591
+
592
+ {/* Last Run Info */}
593
+ {data.lastRun && (
594
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mt: 1 }}>
595
+ <AccessTimeIcon sx={{ fontSize: '14px', color: '#9CA3AF' }} />
596
+ <Typography variant="body2" sx={{ color: '#9CA3AF', fontSize: '11px' }}>
597
+ {t('automation.common.lastRan')}: {data.lastRun}
598
+ {data.duration && ` · ${data.duration}`}
599
+ </Typography>
600
+ </Box>
603
601
  )}
604
602
  </Box>
605
603
 
606
- {/* Execution Result */}
607
- {data.executionResult && (
608
- <Box
609
- sx={{
610
- bgcolor: data.executionResult.success
611
- ? 'rgba(16, 185, 129, 0.1)'
612
- : 'rgba(239, 68, 68, 0.1)',
613
- borderRadius: 1,
614
- p: 1,
615
- mb: 1.5,
616
- border: `1px solid ${
617
- data.executionResult.success
618
- ? 'rgba(16, 185, 129, 0.3)'
619
- : 'rgba(239, 68, 68, 0.3)'
620
- }`,
621
- }}
622
- >
623
- <Typography
624
- variant="caption"
625
- sx={{
626
- color: data.executionResult.success ? '#10B981' : '#EF4444',
627
- fontSize: 10,
628
- }}
629
- >
630
- {data.executionResult.success
631
- ? '✓ Operation completed'
632
- : `✗ ${data.executionResult.error || 'Operation failed'}`}
633
- </Typography>
634
- </Box>
635
- )}
636
-
637
- {/* Last Run Info */}
638
- {data.lastRun && (
639
- <Box
640
- sx={{
641
- display: 'flex',
642
- alignItems: 'center',
643
- gap: 0.5,
644
- justifyContent: 'center',
645
- }}
646
- >
647
- <AccessTimeIcon sx={{ fontSize: 10, color: '#6B7280' }} />
648
- <Typography variant="caption" sx={{ color: '#6B7280', fontSize: 9 }}>
649
- Last run: {data.lastRun}
650
- {data.duration && ` · ${data.duration}`}
651
- </Typography>
652
- </Box>
653
- )}
604
+ {/* Connection Handles - Bidirectional (source + target at each position) */}
605
+ {/* Top - Source */}
606
+ <Handle
607
+ type="source"
608
+ position={Position.Top}
609
+ id="top-source"
610
+ className="connection-handle"
611
+ style={{
612
+ background: selected ? '#10B981' : '#1a1a2e',
613
+ width: '14px',
614
+ height: '14px',
615
+ border: '3px solid #10B981',
616
+ top: '-8px',
617
+ opacity: selected ? 1 : 0,
618
+ transition: 'all 0.2s ease-in-out',
619
+ cursor: 'crosshair',
620
+ zIndex: 10,
621
+ }}
622
+ />
623
+ {/* Top - Target (hidden but functional) */}
624
+ <Handle
625
+ type="target"
626
+ position={Position.Top}
627
+ id="top-target"
628
+ style={{
629
+ background: 'transparent',
630
+ width: '14px',
631
+ height: '14px',
632
+ border: 'none',
633
+ top: '-8px',
634
+ opacity: 0,
635
+ pointerEvents: selected ? 'all' : 'none',
636
+ }}
637
+ />
638
+ {/* Bottom - Source */}
639
+ <Handle
640
+ type="source"
641
+ position={Position.Bottom}
642
+ id="bottom-source"
643
+ className="connection-handle"
644
+ style={{
645
+ background: selected ? '#10B981' : '#1a1a2e',
646
+ width: '14px',
647
+ height: '14px',
648
+ border: '3px solid #10B981',
649
+ bottom: '-8px',
650
+ opacity: selected ? 1 : 0,
651
+ transition: 'all 0.2s ease-in-out',
652
+ cursor: 'crosshair',
653
+ zIndex: 10,
654
+ }}
655
+ />
656
+ {/* Bottom - Target (hidden but functional) */}
657
+ <Handle
658
+ type="target"
659
+ position={Position.Bottom}
660
+ id="bottom-target"
661
+ style={{
662
+ background: 'transparent',
663
+ width: '14px',
664
+ height: '14px',
665
+ border: 'none',
666
+ bottom: '-8px',
667
+ opacity: 0,
668
+ pointerEvents: selected ? 'all' : 'none',
669
+ }}
670
+ />
671
+ {/* Left - Source */}
672
+ <Handle
673
+ type="source"
674
+ position={Position.Left}
675
+ id="left-source"
676
+ className="connection-handle"
677
+ style={{
678
+ background: selected ? '#10B981' : '#1a1a2e',
679
+ width: '14px',
680
+ height: '14px',
681
+ border: '3px solid #10B981',
682
+ left: '-8px',
683
+ opacity: selected ? 1 : 0,
684
+ transition: 'all 0.2s ease-in-out',
685
+ cursor: 'crosshair',
686
+ zIndex: 10,
687
+ }}
688
+ />
689
+ {/* Left - Target (hidden but functional) */}
690
+ <Handle
691
+ type="target"
692
+ position={Position.Left}
693
+ id="left-target"
694
+ style={{
695
+ background: 'transparent',
696
+ width: '14px',
697
+ height: '14px',
698
+ border: 'none',
699
+ left: '-8px',
700
+ opacity: 0,
701
+ pointerEvents: selected ? 'all' : 'none',
702
+ }}
703
+ />
704
+ {/* Right - Source */}
705
+ <Handle
706
+ type="source"
707
+ position={Position.Right}
708
+ id="right-source"
709
+ className="connection-handle"
710
+ style={{
711
+ background: selected ? '#10B981' : '#1a1a2e',
712
+ width: '14px',
713
+ height: '14px',
714
+ border: '3px solid #10B981',
715
+ right: '-8px',
716
+ opacity: selected ? 1 : 0,
717
+ transition: 'all 0.2s ease-in-out',
718
+ cursor: 'crosshair',
719
+ zIndex: 10,
720
+ }}
721
+ />
722
+ {/* Right - Target (hidden but functional) */}
723
+ <Handle
724
+ type="target"
725
+ position={Position.Right}
726
+ id="right-target"
727
+ style={{
728
+ background: 'transparent',
729
+ width: '14px',
730
+ height: '14px',
731
+ border: 'none',
732
+ right: '-8px',
733
+ opacity: 0,
734
+ pointerEvents: selected ? 'all' : 'none',
735
+ }}
736
+ />
654
737
  </Box>
655
738
 
656
- {/* Node Action Buttons */}
657
- <NodeActionButtons selected={selected} />
658
-
659
- {/* Output Handle */}
660
- <Handle
661
- type="source"
662
- position={Position.Right}
663
- style={{
664
- background: serviceConfig.color,
665
- width: 10,
666
- height: 10,
667
- border: '2px solid #1E1E2E',
739
+ {/* Node Action Buttons - Shows when selected */}
740
+ <NodeActionButtons
741
+ selected={selected}
742
+ onOpenAIAssistant={(buttonElement) => {
743
+ if (nodeId) {
744
+ showNodeAIAssistantPopup(nodeId, 'Google Services Node', buttonElement);
745
+ }
746
+ }}
747
+ onDelete={() => {
748
+ if (nodeId && onNodesChange) {
749
+ onNodesChange([{ id: nodeId, type: 'remove' }]);
750
+ }
751
+ }}
752
+ onDuplicate={() => {
753
+ if (nodeId) {
754
+ const currentNode = nodes.find(n => n.id === nodeId);
755
+ if (currentNode) {
756
+ const newNode = {
757
+ ...currentNode,
758
+ id: `${currentNode.id}-copy-${Date.now()}`,
759
+ position: {
760
+ x: currentNode.position.x + 50,
761
+ y: currentNode.position.y + 50,
762
+ },
763
+ selected: false,
764
+ };
765
+ setNodes([...nodes, newNode]);
766
+ }
767
+ }
668
768
  }}
669
769
  />
670
770
  </Box>