@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,7 +7,7 @@ import {
7
7
  Chip,
8
8
  IconButton,
9
9
  Card,
10
- LinearProgress,
10
+ CardContent,
11
11
  } from '@mui/material';
12
12
  import {
13
13
  Mouse as MouseIcon,
@@ -18,14 +18,14 @@ import {
18
18
  CheckBox as CheckBoxIcon,
19
19
  RadioButtonChecked as RadioButtonIcon,
20
20
  AccessTime as AccessTimeIcon,
21
- CheckCircle as CheckCircleIcon,
22
- Error as ErrorIcon,
23
21
  } from '@mui/icons-material';
24
22
  import { RiCloseLine } from 'react-icons/ri';
25
23
  import ReactJson from 'react-json-view';
24
+ import { useTranslation } from 'react-i18next';
26
25
  import { useDiagram } from '../../contexts/DiagramProvider';
27
26
  import { useSearch } from '../../contexts/SearchContext';
28
27
  import { NodeActionButtons } from './NodeActionButtons';
28
+ import { showNodeAIAssistantPopup } from './NodeAIAssistantPopup';
29
29
  import { getStatusColor } from './statusColors';
30
30
 
31
31
  // ========================
@@ -252,22 +252,29 @@ export const AutomationInteractionNode: React.FC<AutomationInteractionNodeProps>
252
252
  data,
253
253
  selected,
254
254
  }) => {
255
+ const { t } = useTranslation();
255
256
  const { highlightText } = useSearch();
256
257
  const [isJsonOpen, setIsJsonOpen] = useState(false);
257
258
  const rootRef = useRef<any>(null);
258
259
  const portalRef = useRef<HTMLDivElement | null>(null);
260
+ const nodeRef = useRef<HTMLDivElement | null>(null);
259
261
  const nodeId = useNodeId();
260
262
  const setSelectedNode = useDiagram((state) => state.setSelectedNode);
261
263
  const enableJson = useDiagram((state) => state.enableNodeJsonPopover ?? true);
264
+ const onNodesChange = useDiagram((state) => state.onNodesChange);
265
+ const nodes = useDiagram((state) => state.nodes);
266
+ const setNodes = useDiagram((state) => state.setNodes);
262
267
 
263
268
  // Get interaction configuration
264
- const interactionType = data.interactionType || data.formData?.interactionType || 'click';
265
- const interactionConfig = INTERACTION_CONFIG[interactionType] || INTERACTION_CONFIG['click'];
269
+ const interactionTypeRaw = data.interactionType || data.formData?.interactionType || 'click';
270
+ const interactionType = (Object.keys(INTERACTION_CONFIG).includes(interactionTypeRaw)
271
+ ? interactionTypeRaw
272
+ : 'click') as InteractionType;
273
+ const interactionConfig = INTERACTION_CONFIG[interactionType];
266
274
  const InteractionIcon = interactionConfig.icon;
267
275
 
268
276
  // Execution state
269
277
  const status = data.status || 'Ready';
270
- const executionProgress = status === 'Running' ? 50 : 0;
271
278
 
272
279
  // Status configuration - using centralized status colors
273
280
  const statusConfig = getStatusColor(status);
@@ -281,6 +288,7 @@ export const AutomationInteractionNode: React.FC<AutomationInteractionNodeProps>
281
288
 
282
289
  const handleClose = () => {
283
290
  setIsJsonOpen(false);
291
+ // Clean up portal
284
292
  if (rootRef.current) {
285
293
  rootRef.current.unmount();
286
294
  rootRef.current = null;
@@ -304,7 +312,6 @@ export const AutomationInteractionNode: React.FC<AutomationInteractionNodeProps>
304
312
  };
305
313
  }, [isJsonOpen]);
306
314
 
307
- // JSON popover portal
308
315
  useEffect(() => {
309
316
  if (isJsonOpen) {
310
317
  const portalRoot = document.createElement('div');
@@ -328,265 +335,352 @@ export const AutomationInteractionNode: React.FC<AutomationInteractionNodeProps>
328
335
  bgcolor: '#242424',
329
336
  color: '#fff',
330
337
  border: '1px solid #333',
338
+ '&::-webkit-scrollbar': {
339
+ width: '6px',
340
+ },
341
+ '&::-webkit-scrollbar-track': {
342
+ background: 'transparent',
343
+ },
344
+ '&::-webkit-scrollbar-thumb': {
345
+ background: '#444',
346
+ borderRadius: '3px',
347
+ '&:hover': {
348
+ background: '#666',
349
+ },
350
+ },
331
351
  }}
332
352
  >
333
- <Box
334
- sx={{
335
- p: 2,
336
- borderBottom: '1px solid #333',
337
- display: 'flex',
338
- justifyContent: 'space-between',
339
- alignItems: 'center',
340
- }}
341
- >
342
- <Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
343
- Interaction Configuration
344
- </Typography>
345
- <IconButton size="small" onClick={handleClose} sx={{ color: '#fff' }}>
353
+ <CardContent sx={{ bgcolor: '#242424', color: '#fff' }}>
354
+ <IconButton
355
+ aria-label="close"
356
+ onClick={handleClose}
357
+ sx={{
358
+ color: '#999',
359
+ '&:hover': {
360
+ color: '#fff',
361
+ bgcolor: 'rgba(255, 255, 255, 0.1)',
362
+ },
363
+ }}
364
+ >
346
365
  <RiCloseLine />
347
366
  </IconButton>
348
- </Box>
349
- <Box sx={{ p: 2 }}>
350
- <ReactJson
351
- src={data}
352
- theme="monokai"
353
- collapsed={1}
354
- displayDataTypes={false}
355
- enableClipboard={false}
356
- style={{
357
- backgroundColor: 'transparent',
358
- fontSize: '12px',
359
- }}
360
- />
361
- </Box>
367
+ {/* Show full node data */}
368
+ <Typography variant="h6" sx={{ color: '#fff', mb: 1 }}>
369
+ {t('automation.common.fullNodeData')}
370
+ </Typography>
371
+ <ReactJson theme={'monokai'} src={data.formData || data} collapsed={false} />
372
+ </CardContent>
362
373
  </Card>
363
374
  );
364
- }
365
- return () => {
375
+ } else {
376
+ // Clean up when closing
366
377
  if (rootRef.current) {
367
378
  rootRef.current.unmount();
368
379
  rootRef.current = null;
369
380
  }
370
- if (portalRef.current && portalRef.current.parentNode) {
381
+ if (portalRef.current) {
371
382
  document.body.removeChild(portalRef.current);
372
383
  portalRef.current = null;
373
384
  }
374
- };
385
+ }
375
386
  }, [isJsonOpen, data]);
376
387
 
377
- // Render status badge
378
- const renderStatusBadge = () => (
379
- <Chip
380
- size="small"
381
- label={
382
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
383
- {status === 'Completed' && <CheckCircleIcon sx={{ fontSize: 12 }} />}
384
- {status === 'Error' && <ErrorIcon sx={{ fontSize: 12 }} />}
385
- <span>{statusConfig.label}</span>
386
- </Box>
387
- }
388
- sx={{
389
- height: 20,
390
- fontSize: '10px',
391
- fontWeight: 600,
392
- color: statusConfig.color,
393
- bgcolor: statusConfig.bgColor,
394
- border: `1px solid ${statusConfig.color}20`,
395
- '& .MuiChip-label': { px: 1 },
396
- }}
397
- />
398
- );
399
-
400
- // ========================
401
- // Render
402
- // ========================
403
388
  return (
404
389
  <Box
405
390
  sx={{
406
- bgcolor: '#1E1E2E',
407
- borderRadius: 2,
408
- border: selected ? `2px solid ${interactionConfig.color}` : '1px solid #374151',
409
- minWidth: 280,
410
- maxWidth: 320,
411
- overflow: 'hidden',
412
- boxShadow: selected
413
- ? `0 0 20px ${interactionConfig.color}30`
414
- : '0 4px 12px rgba(0, 0, 0, 0.3)',
415
- transition: 'all 0.2s ease',
391
+ position: 'relative',
392
+ width: '336px',
393
+ overflow: 'visible',
416
394
  }}
417
- onClick={() => setSelectedNode(nodeId || '')}
418
395
  >
419
- {/* Input Handle */}
420
- <Handle
421
- type="target"
422
- position={Position.Left}
423
- style={{
424
- background: interactionConfig.color,
425
- width: 10,
426
- height: 10,
427
- border: '2px solid #1E1E2E',
428
- }}
429
- />
430
-
431
- {/* Header */}
432
396
  <Box
397
+ ref={nodeRef}
433
398
  sx={{
434
- bgcolor: `${interactionConfig.color}15`,
435
- borderBottom: `1px solid ${interactionConfig.color}30`,
436
- px: 2,
437
- py: 1.5,
438
- display: 'flex',
439
- alignItems: 'center',
440
- justifyContent: 'space-between',
399
+ width: '336px',
400
+ minHeight: '150px',
401
+ backgroundColor: '#181C25',
402
+ border: selected ? `2px solid ${interactionConfig.color}` : '1px solid #1e293b',
403
+ borderRadius: '12px',
404
+ color: '#ffffff',
405
+ position: 'relative',
406
+ boxShadow: selected ? `0 0 0 2px ${interactionConfig.color}50` : '0 4px 8px rgba(0, 0, 0, 0.3)',
407
+ transition: 'all 0.2s ease',
408
+ cursor: 'pointer',
409
+ overflow: 'hidden',
410
+ ...(status === 'Running' && {
411
+ animation: 'pulse-glow 2s ease-in-out infinite',
412
+ '@keyframes pulse-glow': {
413
+ '0%, 100%': {
414
+ boxShadow: `0 0 20px ${interactionConfig.color}40, 0 0 40px ${interactionConfig.color}20`,
415
+ borderColor: `${interactionConfig.color}60`,
416
+ },
417
+ '50%': {
418
+ boxShadow: `0 0 30px ${interactionConfig.color}60, 0 0 60px ${interactionConfig.color}30`,
419
+ borderColor: `${interactionConfig.color}90`,
420
+ },
421
+ },
422
+ }),
441
423
  }}
424
+ onClick={handleJsonClick}
442
425
  >
443
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
444
- <Box
445
- sx={{
446
- bgcolor: interactionConfig.color,
447
- borderRadius: 1,
448
- p: 0.75,
449
- display: 'flex',
450
- alignItems: 'center',
451
- justifyContent: 'center',
452
- }}
453
- >
454
- <InteractionIcon sx={{ fontSize: 18, color: '#fff' }} />
455
- </Box>
456
- <Box>
457
- <Typography
458
- variant="subtitle2"
459
- sx={{ color: '#fff', fontWeight: 600, fontSize: 13 }}
460
- >
461
- {highlightText(data.label || interactionConfig.label)}
462
- </Typography>
463
- <Typography
464
- variant="caption"
465
- sx={{ color: '#9CA3AF', fontSize: 10 }}
466
- >
467
- {data.description || interactionConfig.description}
468
- </Typography>
469
- </Box>
470
- </Box>
471
- {renderStatusBadge()}
472
- </Box>
473
-
474
- {/* Content */}
475
- <Box sx={{ px: 2, py: 1.5 }}>
476
- {/* Progress Bar (when running) */}
477
- {status === 'Running' && (
478
- <Box sx={{ mb: 1.5 }}>
479
- <LinearProgress
480
- variant="determinate"
481
- value={executionProgress}
482
- sx={{
483
- height: 4,
484
- borderRadius: 2,
485
- bgcolor: 'rgba(59, 130, 246, 0.1)',
486
- '& .MuiLinearProgress-bar': {
487
- bgcolor: interactionConfig.color,
488
- },
489
- }}
490
- />
491
- </Box>
492
- )}
493
-
494
- {/* Interaction Details */}
495
- <Box
496
- sx={{
497
- bgcolor: '#1F2937',
498
- borderRadius: 1,
499
- p: 1.5,
500
- mb: 1.5,
501
- border: '1px solid #374151',
502
- }}
503
- >
504
- <Typography
505
- variant="caption"
506
- sx={{
507
- color: '#9CA3AF',
508
- fontSize: 10,
509
- display: 'block',
510
- mb: 0.5,
511
- }}
512
- >
513
- {interactionConfig.label}
514
- </Typography>
515
- <Typography
516
- variant="body2"
517
- sx={{
518
- color: '#fff',
519
- fontSize: 11,
520
- fontWeight: 500,
521
- wordBreak: 'break-word',
522
- }}
523
- >
524
- {getInteractionDetails(data)}
426
+ {/* Top Header Section */}
427
+ <Box sx={{
428
+ backgroundColor: "rgba(67, 93, 132, 0.1)",
429
+ padding: '8px 16px',
430
+ borderRadius: '12px 12px 0 0'
431
+ }}>
432
+ <Typography variant="body2" sx={{
433
+ color: '#ffffff',
434
+ fontSize: '12px',
435
+ fontWeight: 500
436
+ }}>
437
+ {highlightText(data.description || interactionConfig.description)}
525
438
  </Typography>
526
439
  </Box>
527
440
 
528
- {/* Execution Result */}
529
- {data.executionResult && (
530
- <Box
531
- sx={{
532
- bgcolor: data.executionResult.success
533
- ? 'rgba(16, 185, 129, 0.1)'
534
- : 'rgba(239, 68, 68, 0.1)',
535
- borderRadius: 1,
536
- p: 1,
537
- mb: 1.5,
538
- border: `1px solid ${
539
- data.executionResult.success
540
- ? 'rgba(16, 185, 129, 0.3)'
541
- : 'rgba(239, 68, 68, 0.3)'
542
- }`,
543
- }}
544
- >
545
- <Typography
546
- variant="caption"
441
+ {/* Main Content */}
442
+ <Box sx={{ padding: '16px' }}>
443
+ {/* Title Section */}
444
+ <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
445
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
446
+ <Box
447
+ sx={{
448
+ width: '32px',
449
+ height: '32px',
450
+ minWidth: '32px',
451
+ backgroundColor: '#1E3A8A',
452
+ borderRadius: '50%',
453
+ display: 'flex',
454
+ alignItems: 'center',
455
+ justifyContent: 'center',
456
+ flexShrink: 0,
457
+ }}
458
+ >
459
+ <InteractionIcon sx={{ color: 'white', fontSize: '18px' }} />
460
+ </Box>
461
+ <Typography variant="h6" sx={{ fontWeight: 600, fontSize: '16px' }}>
462
+ {highlightText(data.label || interactionConfig.label)}
463
+ </Typography>
464
+ </Box>
465
+ <Chip
466
+ label={status}
467
+ size="small"
547
468
  sx={{
548
- color: data.executionResult.success ? '#10B981' : '#EF4444',
549
- fontSize: 10,
469
+ backgroundColor: statusConfig.bgColor,
470
+ color: statusConfig.color,
471
+ fontWeight: 500,
472
+ fontSize: '12px',
473
+ height: '24px',
474
+ borderRadius: '12px',
550
475
  }}
551
- >
552
- {data.executionResult.success
553
- ? '✓ Interaction completed'
554
- : `✗ ${data.executionResult.error || 'Interaction failed'}`}
555
- </Typography>
476
+ />
556
477
  </Box>
557
- )}
558
478
 
559
- {/* Last Run Info */}
560
- {data.lastRun && (
561
- <Box
562
- sx={{
479
+ {/* Description Box */}
480
+ <Box sx={{
481
+ backgroundColor: '#1F2937',
482
+ borderRadius: '8px',
483
+ padding: '12px',
484
+ mb: 2,
485
+ border: '1px solid #374151'
486
+ }}>
487
+ {/* Inner text boundary box */}
488
+ <Box sx={{
489
+ backgroundColor: 'transparent',
490
+ borderRadius: '4px',
491
+ padding: '8px',
492
+ border: '1px solid #4B5563',
493
+ minHeight: '40px',
563
494
  display: 'flex',
564
- alignItems: 'center',
565
- gap: 0.5,
566
- justifyContent: 'center',
567
- }}
568
- >
569
- <AccessTimeIcon sx={{ fontSize: 10, color: '#6B7280' }} />
570
- <Typography variant="caption" sx={{ color: '#6B7280', fontSize: 9 }}>
571
- Last run: {data.lastRun}
572
- {data.duration && ` · ${data.duration}`}
573
- </Typography>
495
+ alignItems: 'center'
496
+ }}>
497
+ <Typography variant="body2" sx={{
498
+ color: '#9CA3AF',
499
+ fontSize: '12px',
500
+ lineHeight: 1.4,
501
+ margin: 0
502
+ }}>
503
+ {getInteractionDetails(data)}
504
+ </Typography>
505
+ </Box>
574
506
  </Box>
575
- )}
576
- </Box>
577
507
 
578
- {/* Node Action Buttons */}
579
- <NodeActionButtons selected={selected} />
508
+ {/* Last Run Info */}
509
+ {data.lastRun && (
510
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mt: 1 }}>
511
+ <AccessTimeIcon sx={{ fontSize: '14px', color: '#9CA3AF' }} />
512
+ <Typography variant="body2" sx={{ color: '#9CA3AF', fontSize: '11px' }}>
513
+ {t('automation.common.lastRan')}: {data.lastRun}
514
+ {data.duration && ` · ${data.duration}`}
515
+ </Typography>
516
+ </Box>
517
+ )}
518
+ </Box>
519
+
520
+ {/* Connection Handles - Bidirectional (source + target at each position) */}
521
+ {/* Top - Source */}
522
+ <Handle
523
+ type="source"
524
+ position={Position.Top}
525
+ id="top-source"
526
+ className="connection-handle"
527
+ style={{
528
+ background: selected ? '#10B981' : '#1a1a2e',
529
+ width: '14px',
530
+ height: '14px',
531
+ border: '3px solid #10B981',
532
+ top: '-8px',
533
+ opacity: selected ? 1 : 0,
534
+ transition: 'all 0.2s ease-in-out',
535
+ cursor: 'crosshair',
536
+ zIndex: 10,
537
+ }}
538
+ />
539
+ {/* Top - Target (hidden but functional) */}
540
+ <Handle
541
+ type="target"
542
+ position={Position.Top}
543
+ id="top-target"
544
+ style={{
545
+ background: 'transparent',
546
+ width: '14px',
547
+ height: '14px',
548
+ border: 'none',
549
+ top: '-8px',
550
+ opacity: 0,
551
+ pointerEvents: selected ? 'all' : 'none',
552
+ }}
553
+ />
554
+ {/* Bottom - Source */}
555
+ <Handle
556
+ type="source"
557
+ position={Position.Bottom}
558
+ id="bottom-source"
559
+ className="connection-handle"
560
+ style={{
561
+ background: selected ? '#10B981' : '#1a1a2e',
562
+ width: '14px',
563
+ height: '14px',
564
+ border: '3px solid #10B981',
565
+ bottom: '-8px',
566
+ opacity: selected ? 1 : 0,
567
+ transition: 'all 0.2s ease-in-out',
568
+ cursor: 'crosshair',
569
+ zIndex: 10,
570
+ }}
571
+ />
572
+ {/* Bottom - Target (hidden but functional) */}
573
+ <Handle
574
+ type="target"
575
+ position={Position.Bottom}
576
+ id="bottom-target"
577
+ style={{
578
+ background: 'transparent',
579
+ width: '14px',
580
+ height: '14px',
581
+ border: 'none',
582
+ bottom: '-8px',
583
+ opacity: 0,
584
+ pointerEvents: selected ? 'all' : 'none',
585
+ }}
586
+ />
587
+ {/* Left - Source */}
588
+ <Handle
589
+ type="source"
590
+ position={Position.Left}
591
+ id="left-source"
592
+ className="connection-handle"
593
+ style={{
594
+ background: selected ? '#10B981' : '#1a1a2e',
595
+ width: '14px',
596
+ height: '14px',
597
+ border: '3px solid #10B981',
598
+ left: '-8px',
599
+ opacity: selected ? 1 : 0,
600
+ transition: 'all 0.2s ease-in-out',
601
+ cursor: 'crosshair',
602
+ zIndex: 10,
603
+ }}
604
+ />
605
+ {/* Left - Target (hidden but functional) */}
606
+ <Handle
607
+ type="target"
608
+ position={Position.Left}
609
+ id="left-target"
610
+ style={{
611
+ background: 'transparent',
612
+ width: '14px',
613
+ height: '14px',
614
+ border: 'none',
615
+ left: '-8px',
616
+ opacity: 0,
617
+ pointerEvents: selected ? 'all' : 'none',
618
+ }}
619
+ />
620
+ {/* Right - Source */}
621
+ <Handle
622
+ type="source"
623
+ position={Position.Right}
624
+ id="right-source"
625
+ className="connection-handle"
626
+ style={{
627
+ background: selected ? '#10B981' : '#1a1a2e',
628
+ width: '14px',
629
+ height: '14px',
630
+ border: '3px solid #10B981',
631
+ right: '-8px',
632
+ opacity: selected ? 1 : 0,
633
+ transition: 'all 0.2s ease-in-out',
634
+ cursor: 'crosshair',
635
+ zIndex: 10,
636
+ }}
637
+ />
638
+ {/* Right - Target (hidden but functional) */}
639
+ <Handle
640
+ type="target"
641
+ position={Position.Right}
642
+ id="right-target"
643
+ style={{
644
+ background: 'transparent',
645
+ width: '14px',
646
+ height: '14px',
647
+ border: 'none',
648
+ right: '-8px',
649
+ opacity: 0,
650
+ pointerEvents: selected ? 'all' : 'none',
651
+ }}
652
+ />
653
+ </Box>
580
654
 
581
- {/* Output Handle */}
582
- <Handle
583
- type="source"
584
- position={Position.Right}
585
- style={{
586
- background: interactionConfig.color,
587
- width: 10,
588
- height: 10,
589
- border: '2px solid #1E1E2E',
655
+ {/* Node Action Buttons - Shows when selected */}
656
+ <NodeActionButtons
657
+ selected={selected}
658
+ onOpenAIAssistant={(buttonElement) => {
659
+ if (nodeId) {
660
+ showNodeAIAssistantPopup(nodeId, 'Interaction Node', buttonElement);
661
+ }
662
+ }}
663
+ onDelete={() => {
664
+ if (nodeId && onNodesChange) {
665
+ onNodesChange([{ id: nodeId, type: 'remove' }]);
666
+ }
667
+ }}
668
+ onDuplicate={() => {
669
+ if (nodeId) {
670
+ const currentNode = nodes.find(n => n.id === nodeId);
671
+ if (currentNode) {
672
+ const newNode = {
673
+ ...currentNode,
674
+ id: `${currentNode.id}-copy-${Date.now()}`,
675
+ position: {
676
+ x: currentNode.position.x + 50,
677
+ y: currentNode.position.y + 50,
678
+ },
679
+ selected: false,
680
+ };
681
+ setNodes([...nodes, newNode]);
682
+ }
683
+ }
590
684
  }}
591
685
  />
592
686
  </Box>