@eventcatalog/core 3.8.2 → 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/analytics/analytics.cjs +1 -1
  2. package/dist/analytics/analytics.js +2 -2
  3. package/dist/analytics/log-build.cjs +1 -1
  4. package/dist/analytics/log-build.js +3 -3
  5. package/dist/{chunk-OCD75GFW.js → chunk-CZG5RQXY.js} +1 -1
  6. package/dist/{chunk-I7HRERRK.js → chunk-FM44RPBS.js} +1 -1
  7. package/dist/{chunk-MOBOWLEW.js → chunk-HJOMVPCL.js} +1 -1
  8. package/dist/{chunk-275AT7XV.js → chunk-OQYLI4YJ.js} +1 -1
  9. package/dist/{chunk-KBMXUUXX.js → chunk-UYBPI4UO.js} +1 -1
  10. package/dist/constants.cjs +1 -1
  11. package/dist/constants.js +1 -1
  12. package/dist/eventcatalog.cjs +1 -1
  13. package/dist/eventcatalog.js +5 -5
  14. package/dist/generate.cjs +1 -1
  15. package/dist/generate.js +3 -3
  16. package/dist/utils/cli-logger.cjs +1 -1
  17. package/dist/utils/cli-logger.js +2 -2
  18. package/eventcatalog/src/components/ChatPanel/ChatPanel.tsx +50 -0
  19. package/eventcatalog/src/components/MDX/Design/Design.astro +4 -1
  20. package/eventcatalog/src/components/MDX/EntityMap/EntityMap.astro +4 -0
  21. package/eventcatalog/src/components/MDX/Flow/Flow.astro +4 -1
  22. package/eventcatalog/src/components/MDX/NodeGraph/MermaidView.tsx +240 -0
  23. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +4 -0
  24. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.tsx +333 -189
  25. package/eventcatalog/src/components/MDX/NodeGraph/VisualizerDropdownContent.tsx +224 -0
  26. package/eventcatalog/src/content.config.ts +1 -1
  27. package/eventcatalog/src/enterprise/ai/chat-api.ts +23 -0
  28. package/eventcatalog/src/enterprise/tools/catalog-tools.ts +96 -0
  29. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version].mermaid.ts +128 -0
  30. package/eventcatalog/src/pages/visualiser/designs/[id]/index.astro +4 -0
  31. package/eventcatalog/src/utils/clipboard.ts +22 -0
  32. package/eventcatalog/src/utils/node-graphs/export-mermaid.ts +299 -0
  33. package/package.json +1 -1
@@ -6,6 +6,7 @@ import {
6
6
  ConnectionLineType,
7
7
  Controls,
8
8
  Panel,
9
+ MiniMap,
9
10
  ReactFlowProvider,
10
11
  useNodesState,
11
12
  useEdgesState,
@@ -17,7 +18,24 @@ import {
17
18
  type NodeTypes,
18
19
  } from '@xyflow/react';
19
20
  import '@xyflow/react/dist/style.css';
20
- import { ExternalLink, HistoryIcon } from 'lucide-react';
21
+ import {
22
+ ExternalLink,
23
+ HistoryIcon,
24
+ CheckIcon,
25
+ ClipboardIcon,
26
+ ChevronDownIcon,
27
+ MoreVertical,
28
+ Zap,
29
+ EyeOff,
30
+ Code,
31
+ Share2,
32
+ Search,
33
+ Grid3x3,
34
+ Maximize2,
35
+ Map,
36
+ Sparkles,
37
+ } from 'lucide-react';
38
+ import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
21
39
  import { toPng } from 'html-to-image';
22
40
  import { DocumentArrowDownIcon, PresentationChartLineIcon } from '@heroicons/react/24/outline';
23
41
  // Nodes and edges
@@ -47,11 +65,15 @@ import { navigate } from 'astro:transitions/client';
47
65
  import type { CollectionTypes } from '@types';
48
66
  import { buildUrl } from '@utils/url-builder';
49
67
  import ChannelNode from './Nodes/Channel';
50
- import { CogIcon } from '@heroicons/react/20/solid';
51
68
  import { useEventCatalogVisualiser } from 'src/hooks/eventcatalog-visualizer';
52
69
  import VisualiserSearch, { type VisualiserSearchRef } from './VisualiserSearch';
53
70
  import StepWalkthrough from './StepWalkthrough';
54
71
  import StudioModal from './StudioModal';
72
+ import MermaidView from './MermaidView';
73
+ import VisualizerDropdownContent from './VisualizerDropdownContent';
74
+ import { convertToMermaid } from '@utils/node-graphs/export-mermaid';
75
+ import { copyToClipboard } from '@utils/clipboard';
76
+
55
77
  interface Props {
56
78
  nodes: any;
57
79
  edges: any;
@@ -70,6 +92,7 @@ interface Props {
70
92
  designId?: string;
71
93
  isStudioModalOpen?: boolean;
72
94
  setIsStudioModalOpen?: (isOpen: boolean) => void;
95
+ isChatEnabled?: boolean;
73
96
  }
74
97
 
75
98
  const getVisualiserUrlForCollection = (collectionItem: CollectionEntry<CollectionTypes>) => {
@@ -91,6 +114,7 @@ const NodeGraphBuilder = ({
91
114
  zoomOnScroll = false,
92
115
  isStudioModalOpen,
93
116
  setIsStudioModalOpen = () => {},
117
+ isChatEnabled = false,
94
118
  }: Props) => {
95
119
  const nodeTypes = useMemo(
96
120
  () =>
@@ -135,10 +159,14 @@ const NodeGraphBuilder = ({
135
159
  );
136
160
  const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
137
161
  const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
138
- const [isSettingsOpen, setIsSettingsOpen] = useState(false);
139
162
  const [animateMessages, setAnimateMessages] = useState(false);
140
163
  const [activeStepIndex, setActiveStepIndex] = useState<number | null>(null);
141
164
  const [isFullscreen, setIsFullscreen] = useState(false);
165
+ const [mermaidCode, setMermaidCode] = useState('');
166
+ const [isShareModalOpen, setIsShareModalOpen] = useState(false);
167
+ const [shareUrlCopySuccess, setShareUrlCopySuccess] = useState(false);
168
+ const [isMermaidView, setIsMermaidView] = useState(false);
169
+ const [showMinimap, setShowMinimap] = useState(false);
142
170
  // const [isStudioModalOpen, setIsStudioModalOpen] = useState(false);
143
171
 
144
172
  // Check if there are channels to determine if we need the visualizer functionality
@@ -234,6 +262,11 @@ const NodeGraphBuilder = ({
234
262
  localStorage.setItem('EventCatalog:animateMessages', JSON.stringify(!animateMessages));
235
263
  };
236
264
 
265
+ // Handle fit to view
266
+ const handleFitView = useCallback(() => {
267
+ fitView({ duration: 400, padding: 0.2 });
268
+ }, [fitView]);
269
+
237
270
  // animate messages, between views
238
271
  // URL parameter takes priority over localStorage
239
272
  useEffect(() => {
@@ -270,6 +303,17 @@ const NodeGraphBuilder = ({
270
303
  }, 150);
271
304
  }, []);
272
305
 
306
+ // Generate mermaid code from nodes and edges
307
+ useEffect(() => {
308
+ try {
309
+ const code = convertToMermaid(nodes, edges, { includeStyles: true, direction: 'LR' });
310
+ setMermaidCode(code);
311
+ } catch (error) {
312
+ console.error('Error generating mermaid code:', error);
313
+ setMermaidCode('');
314
+ }
315
+ }, [nodes, edges]);
316
+
273
317
  // Handle scroll wheel events to forward to page when no modifier keys are pressed
274
318
  // Only when zoomOnScroll is disabled
275
319
  // This is a fix for when we embed node graphs into pages, and users are scrolling the documentation pages
@@ -335,7 +379,6 @@ const NodeGraphBuilder = ({
335
379
  }, [zoomOnScroll]);
336
380
 
337
381
  const handlePaneClick = useCallback(() => {
338
- setIsSettingsOpen(false);
339
382
  searchRef.current?.hideSuggestions();
340
383
  resetNodesAndEdges();
341
384
  fitView({ duration: 800 });
@@ -364,6 +407,21 @@ const NodeGraphBuilder = ({
364
407
  setIsStudioModalOpen(true);
365
408
  };
366
409
 
410
+ const openChat = useCallback(() => {
411
+ window.dispatchEvent(new CustomEvent('eventcatalog:open-chat'));
412
+ }, []);
413
+
414
+ const handleCopyArchitectureCode = useCallback(async () => {
415
+ await copyToClipboard(mermaidCode);
416
+ }, [mermaidCode]);
417
+
418
+ const handleCopyShareUrl = useCallback(async () => {
419
+ const url = typeof window !== 'undefined' ? window.location.href : '';
420
+ await copyToClipboard(url);
421
+ setShareUrlCopySuccess(true);
422
+ setTimeout(() => setShareUrlCopySuccess(false), 2000);
423
+ }, []);
424
+
367
425
  const toggleFullScreen = useCallback(() => {
368
426
  if (!document.fullscreenElement) {
369
427
  reactFlowWrapperRef.current?.requestFullscreen().catch((err) => {
@@ -396,8 +454,7 @@ const NodeGraphBuilder = ({
396
454
  const height = imageHeight > nodesBounds.height ? imageHeight : nodesBounds.height;
397
455
  const viewport = getViewportForBounds(nodesBounds, width, height, 0.5, 2, 0);
398
456
 
399
- // Hide settings panel and controls during export
400
- setIsSettingsOpen(false);
457
+ // Hide controls during export
401
458
  const controls = document.querySelector('.react-flow__controls') as HTMLElement;
402
459
  if (controls) controls.style.display = 'none';
403
460
 
@@ -603,204 +660,288 @@ const NodeGraphBuilder = ({
603
660
  const isFlowVisualization = edges.some((edge: Edge) => edge.type === 'flow-edge');
604
661
 
605
662
  return (
606
- <div ref={reactFlowWrapperRef} className="w-full h-full bg-gray-50">
607
- <ReactFlow
608
- nodeTypes={nodeTypes}
609
- edgeTypes={edgeTypes}
610
- minZoom={0.07}
611
- nodes={nodes}
612
- edges={edges}
613
- fitView
614
- onNodesChange={onNodesChange}
615
- onEdgesChange={onEdgesChange}
616
- connectionLineType={ConnectionLineType.SmoothStep}
617
- nodeOrigin={[0.1, 0.1]}
618
- onNodeClick={handleNodeClick}
619
- onPaneClick={handlePaneClick}
620
- zoomOnScroll={zoomOnScroll}
621
- className="relative"
622
- >
623
- <Panel position="top-center" className="w-full pr-6 ">
624
- <div className="flex space-x-2 justify-between items-center">
663
+ <div ref={reactFlowWrapperRef} className="w-full h-full bg-gray-50 flex flex-col">
664
+ {isMermaidView ? (
665
+ <>
666
+ {/* Menu Bar for Mermaid View */}
667
+ <div className="w-full pr-6 flex space-x-2 justify-between items-center bg-[rgb(var(--ec-page-bg))] border-b border-[rgb(var(--ec-page-border))] p-4">
625
668
  <div className="flex space-x-2 ml-4">
626
- <div className="relative group">
627
- <button
628
- onClick={() => setIsSettingsOpen(!isSettingsOpen)}
629
- className="py-2.5 px-3 bg-white rounded-md shadow-md hover:bg-[rgb(var(--ec-accent-subtle))] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[rgb(var(--ec-accent))]"
630
- aria-label="Open settings"
631
- >
632
- <CogIcon className="h-5 w-5 text-gray-600" />
633
- </button>
634
- <div className="absolute top-full left-0 mt-2 px-2 py-1 bg-gray-900 text-white text-xs rounded shadow-lg opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-50">
635
- Settings
636
- </div>
637
- </div>
638
- <div className="relative group">
639
- <button
640
- onClick={toggleFullScreen}
641
- className={`py-2.5 px-3 bg-white rounded-md shadow-md hover:bg-[rgb(var(--ec-accent-subtle))] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[rgb(var(--ec-accent))] ${
642
- isFullscreen ? 'bg-[rgb(var(--ec-accent-subtle))] text-[rgb(var(--ec-accent))]' : ''
643
- }`}
644
- aria-label={isFullscreen ? 'Exit presentation mode' : 'Enter presentation mode'}
645
- >
646
- <PresentationChartLineIcon
647
- className={`h-5 w-5 ${isFullscreen ? 'text-[rgb(var(--ec-accent))]' : 'text-gray-600'}`}
648
- />
649
- </button>
650
- <div className="absolute top-full left-1/2 -translate-x-1/2 mt-2 px-2 py-1 bg-gray-900 text-white text-xs rounded shadow-lg opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-50">
651
- {isFullscreen ? 'Exit Presentation Mode' : 'Presentation Mode'}
652
- </div>
653
- </div>
654
-
655
- {title && (
656
- <span className="block shadow-sm bg-white text-xl z-10 text-black px-4 py-1.5 border-gray-200 rounded-md border opacity-80">
657
- {title}
658
- </span>
659
- )}
669
+ {/* Settings Dropdown Menu */}
670
+ <DropdownMenu.Root>
671
+ <DropdownMenu.Trigger asChild>
672
+ <button
673
+ className="py-2.5 px-4 bg-[rgb(var(--ec-page-bg))] hover:bg-[rgb(var(--ec-accent-subtle)/0.4)] border border-[rgb(var(--ec-page-border))] rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[rgb(var(--ec-accent))] flex items-center gap-3 transition-all duration-200 hover:border-[rgb(var(--ec-accent)/0.3)] group whitespace-nowrap"
674
+ aria-label="Open menu"
675
+ >
676
+ {title && (
677
+ <span className="text-base font-medium text-[rgb(var(--ec-page-text))] leading-tight">{title}</span>
678
+ )}
679
+ <MoreVertical className="h-5 w-5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0 group-hover:text-[rgb(var(--ec-accent))] transition-colors duration-150" />
680
+ </button>
681
+ </DropdownMenu.Trigger>
682
+ <DropdownMenu.Portal>
683
+ <DropdownMenu.Content
684
+ className="min-w-56 bg-[rgb(var(--ec-page-bg))] border border-[rgb(var(--ec-page-border))] rounded-lg shadow-xl z-50 py-1.5 animate-in fade-in zoom-in-95 duration-200"
685
+ sideOffset={0}
686
+ align="end"
687
+ alignOffset={-180}
688
+ >
689
+ <DropdownMenu.Arrow className="fill-[rgb(var(--ec-page-bg))] stroke-[rgb(var(--ec-page-border))] stroke-1" />
690
+ <VisualizerDropdownContent
691
+ isMermaidView={isMermaidView}
692
+ setIsMermaidView={setIsMermaidView}
693
+ animateMessages={animateMessages}
694
+ toggleAnimateMessages={toggleAnimateMessages}
695
+ hideChannels={hideChannels}
696
+ toggleChannelsVisibility={toggleChannelsVisibility}
697
+ hasChannels={hasChannels}
698
+ showMinimap={showMinimap}
699
+ setShowMinimap={setShowMinimap}
700
+ handleFitView={handleFitView}
701
+ searchRef={searchRef}
702
+ isChatEnabled={isChatEnabled}
703
+ openChat={openChat}
704
+ handleCopyArchitectureCode={handleCopyArchitectureCode}
705
+ handleExportVisual={handleExportVisual}
706
+ setIsShareModalOpen={setIsShareModalOpen}
707
+ toggleFullScreen={toggleFullScreen}
708
+ openStudioModal={openStudioModal}
709
+ />
710
+ </DropdownMenu.Content>
711
+ </DropdownMenu.Portal>
712
+ </DropdownMenu.Root>
660
713
  </div>
661
714
  {mode === 'full' && showSearch && (
662
- <div className="flex justify-end space-x-2 w-96">
663
- <VisualiserSearch ref={searchRef} nodes={nodes} onNodeSelect={handleNodeSelect} onClear={handleSearchClear} />
715
+ <div className="flex justify-end items-center gap-2">
716
+ {!isMermaidView && (
717
+ <div className="w-96">
718
+ <VisualiserSearch ref={searchRef} nodes={nodes} onNodeSelect={handleNodeSelect} onClear={handleSearchClear} />
719
+ </div>
720
+ )}
664
721
  </div>
665
722
  )}
666
723
  </div>
667
- {links.length > 0 && (
668
- <div className="flex justify-end mt-3">
669
- <div className="relative flex items-center -mt-1">
670
- <span className="absolute left-2 pointer-events-none flex items-center h-full">
671
- <HistoryIcon className="h-4 w-4 text-gray-600" />
672
- </span>
673
- <select
674
- value={links.find((link) => window.location.href.includes(link.url))?.url || links[0].url}
675
- onChange={(e) => navigate(e.target.value)}
676
- className="appearance-none pl-7 pr-6 py-0 text-[14px] bg-white rounded-md border border-gray-200 hover:bg-gray-100/50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[rgb(var(--ec-accent))]"
677
- style={{ minWidth: 120, height: '26px' }}
678
- >
679
- {links.map((link) => (
680
- <option key={link.url} value={link.url}>
681
- {link.label}
682
- </option>
683
- ))}
684
- </select>
685
- <span className="absolute right-2 pointer-events-none">
686
- <svg className="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
687
- <path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
688
- </svg>
689
- </span>
690
- </div>
691
- </div>
692
- )}
693
- </Panel>
694
-
695
- {isSettingsOpen && (
696
- <div className="absolute top-[68px] left-5 w-72 p-4 bg-white rounded-lg shadow-lg z-30 border border-gray-200">
697
- <h3 className="text-lg font-semibold mb-4">Visualizer Settings</h3>
698
- <div className="space-y-4 ">
699
- <div>
700
- <div className="flex items-center justify-between">
701
- <label htmlFor="message-animation-toggle" className="text-sm font-medium text-gray-700">
702
- Simulate Messages
703
- </label>
704
- <button
705
- id="message-animation-toggle"
706
- onClick={toggleAnimateMessages}
707
- className={`${
708
- animateMessages ? 'bg-[rgb(var(--ec-accent))]' : 'bg-gray-200'
709
- } relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-[rgb(var(--ec-accent))] focus:ring-offset-2`}
710
- >
711
- <span
712
- className={`${
713
- animateMessages ? 'translate-x-6' : 'translate-x-1'
714
- } inline-block h-4 w-4 transform rounded-full bg-white transition-transform`}
715
- />
716
- </button>
717
- </div>
718
- <p className="text-[10px] text-gray-500">Animate events, queries and commands.</p>
719
- </div>
720
- {hasChannels && (
721
- <div>
722
- <div className="flex items-center justify-between">
723
- <label htmlFor="hide-channels-toggle" className="text-sm font-medium text-gray-700">
724
- Hide Channels
725
- </label>
724
+ {/* Mermaid View */}
725
+ <div className="flex-1 overflow-hidden">
726
+ <MermaidView nodes={nodes} edges={edges} />
727
+ </div>
728
+ </>
729
+ ) : (
730
+ <ReactFlow
731
+ nodeTypes={nodeTypes}
732
+ edgeTypes={edgeTypes}
733
+ minZoom={0.07}
734
+ nodes={nodes}
735
+ edges={edges}
736
+ fitView
737
+ onNodesChange={onNodesChange}
738
+ onEdgesChange={onEdgesChange}
739
+ connectionLineType={ConnectionLineType.SmoothStep}
740
+ nodeOrigin={[0.1, 0.1]}
741
+ onNodeClick={handleNodeClick}
742
+ onPaneClick={handlePaneClick}
743
+ zoomOnScroll={zoomOnScroll}
744
+ className="relative"
745
+ >
746
+ <Panel position="top-center" className="w-full pr-6 ">
747
+ <div className="flex space-x-2 justify-between items-center">
748
+ <div className="flex space-x-2 ml-4">
749
+ {/* Settings Dropdown Menu */}
750
+ <DropdownMenu.Root>
751
+ <DropdownMenu.Trigger asChild>
726
752
  <button
727
- id="hide-channels-toggle"
728
- onClick={toggleChannelsVisibility}
729
- className={`${
730
- hideChannels ? 'bg-[rgb(var(--ec-accent))]' : 'bg-gray-200'
731
- } relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-[rgb(var(--ec-accent))] focus:ring-offset-2`}
753
+ className="py-2.5 px-4 bg-[rgb(var(--ec-page-bg))] hover:bg-[rgb(var(--ec-accent-subtle)/0.4)] border border-[rgb(var(--ec-page-border))] rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[rgb(var(--ec-accent))] flex items-center gap-3 transition-all duration-200 hover:border-[rgb(var(--ec-accent)/0.3)] group whitespace-nowrap"
754
+ aria-label="Open menu"
732
755
  >
733
- <span
734
- className={`${
735
- hideChannels ? 'translate-x-6' : 'translate-x-1'
736
- } inline-block h-4 w-4 transform rounded-full bg-white transition-transform`}
737
- />
756
+ {title && (
757
+ <span className="text-base font-medium text-[rgb(var(--ec-page-text))] leading-tight">{title}</span>
758
+ )}
759
+ <MoreVertical className="h-5 w-5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0 group-hover:text-[rgb(var(--ec-accent))] transition-colors duration-150" />
738
760
  </button>
739
- </div>
740
- <p className="text-[10px] text-gray-500">Show or hide channels in the visualizer.</p>
761
+ </DropdownMenu.Trigger>
762
+ <DropdownMenu.Portal>
763
+ <DropdownMenu.Content
764
+ className="min-w-56 bg-[rgb(var(--ec-page-bg))] border border-[rgb(var(--ec-page-border))] rounded-lg shadow-xl z-50 py-1.5 animate-in fade-in zoom-in-95 duration-200"
765
+ sideOffset={0}
766
+ align="end"
767
+ alignOffset={-180}
768
+ >
769
+ <DropdownMenu.Arrow className="fill-[rgb(var(--ec-page-bg))] stroke-[rgb(var(--ec-page-border))] stroke-1" />
770
+ <VisualizerDropdownContent
771
+ isMermaidView={isMermaidView}
772
+ setIsMermaidView={setIsMermaidView}
773
+ animateMessages={animateMessages}
774
+ toggleAnimateMessages={toggleAnimateMessages}
775
+ hideChannels={hideChannels}
776
+ toggleChannelsVisibility={toggleChannelsVisibility}
777
+ hasChannels={hasChannels}
778
+ showMinimap={showMinimap}
779
+ setShowMinimap={setShowMinimap}
780
+ handleFitView={handleFitView}
781
+ searchRef={searchRef}
782
+ isChatEnabled={isChatEnabled}
783
+ openChat={openChat}
784
+ handleCopyArchitectureCode={handleCopyArchitectureCode}
785
+ handleExportVisual={handleExportVisual}
786
+ setIsShareModalOpen={setIsShareModalOpen}
787
+ toggleFullScreen={toggleFullScreen}
788
+ openStudioModal={openStudioModal}
789
+ />
790
+ </DropdownMenu.Content>
791
+ </DropdownMenu.Portal>
792
+ </DropdownMenu.Root>
793
+ </div>
794
+ {mode === 'full' && showSearch && (
795
+ <div className="flex justify-end items-center gap-2">
796
+ {!isMermaidView && (
797
+ <div className="w-96">
798
+ <VisualiserSearch
799
+ ref={searchRef}
800
+ nodes={nodes}
801
+ onNodeSelect={handleNodeSelect}
802
+ onClear={handleSearchClear}
803
+ />
804
+ </div>
805
+ )}
741
806
  </div>
742
807
  )}
743
- <div className="pt-4 border-t border-gray-200 space-y-2">
744
- <button
745
- onClick={openStudioModal}
746
- className="w-full flex items-center justify-center space-x-2 px-4 py-2 bg-black hover:bg-gray-800 text-white text-sm font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-[rgb(var(--ec-accent))] focus:ring-offset-2 transition-colors"
747
- >
748
- <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
749
- <path
750
- strokeLinecap="round"
751
- strokeLinejoin="round"
752
- strokeWidth="2"
753
- d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
754
- />
755
- </svg>
756
- <span>Open in EventCatalog Studio</span>
757
- </button>
758
- <button
759
- onClick={handleExportVisual}
760
- className="w-full flex items-center justify-center border border-gray-200 space-x-2 px-4 py-2 bg-white text-gray-800 text-sm font-medium rounded-md hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-[rgb(var(--ec-accent))] focus:ring-offset-2"
761
- >
762
- <DocumentArrowDownIcon className="w-4 h-4" />
763
- <span>Export as png</span>
764
- </button>
765
- </div>
766
808
  </div>
767
- </div>
768
- )}
769
- {includeBackground && <Background color="#bbb" gap={16} />}
770
- {includeBackground && <Controls />}
771
- {isFlowVisualization && showFlowWalkthrough && (
772
- <Panel position="bottom-left">
773
- <StepWalkthrough
774
- nodes={nodes}
775
- edges={edges}
776
- isFlowVisualization={isFlowVisualization}
777
- onStepChange={handleStepChange}
778
- mode={mode}
779
- />
780
- </Panel>
781
- )}
782
- {includeKey && (
783
- <Panel position="bottom-right">
784
- <div className=" bg-white font-light px-4 text-[12px] shadow-md py-1 rounded-md">
785
- <ul className="m-0 p-0 ">
786
- {Object.entries(legend).map(([key, { count, colorClass, groupId }]) => (
787
- <li
788
- key={key}
789
- className="flex space-x-2 items-center text-[10px] cursor-pointer hover:text-[rgb(var(--ec-accent))] hover:underline"
790
- onClick={() => handleLegendClick(key, groupId)}
809
+ {links.length > 0 && (
810
+ <div className="flex justify-end mt-3">
811
+ <div className="relative flex items-center -mt-1">
812
+ <span className="absolute left-2 pointer-events-none flex items-center h-full">
813
+ <HistoryIcon className="h-4 w-4 text-gray-600" />
814
+ </span>
815
+ <select
816
+ value={links.find((link) => window.location.href.includes(link.url))?.url || links[0].url}
817
+ onChange={(e) => navigate(e.target.value)}
818
+ className="appearance-none pl-7 pr-6 py-0 text-[14px] bg-white rounded-md border border-gray-200 hover:bg-gray-100/50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[rgb(var(--ec-accent))]"
819
+ style={{ minWidth: 120, height: '26px' }}
791
820
  >
792
- <span className={`w-2 h-2 block ${colorClass}`} />
793
- <span className="block capitalize">
794
- {key} ({count})
795
- </span>
796
- </li>
797
- ))}
798
- </ul>
799
- </div>
821
+ {links.map((link) => (
822
+ <option key={link.url} value={link.url}>
823
+ {link.label}
824
+ </option>
825
+ ))}
826
+ </select>
827
+ <span className="absolute right-2 pointer-events-none">
828
+ <svg className="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
829
+ <path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
830
+ </svg>
831
+ </span>
832
+ </div>
833
+ </div>
834
+ )}
800
835
  </Panel>
801
- )}
802
- </ReactFlow>
836
+
837
+ {includeBackground && <Background color="#bbb" gap={16} />}
838
+ {includeBackground && <Controls />}
839
+ {showMinimap && (
840
+ <MiniMap
841
+ nodeStrokeWidth={3}
842
+ zoomable
843
+ pannable
844
+ style={{
845
+ backgroundColor: 'rgb(var(--ec-page-bg))',
846
+ border: '1px solid rgb(var(--ec-page-border))',
847
+ borderRadius: '8px',
848
+ }}
849
+ />
850
+ )}
851
+ {isFlowVisualization && showFlowWalkthrough && (
852
+ <Panel position="bottom-left">
853
+ <StepWalkthrough
854
+ nodes={nodes}
855
+ edges={edges}
856
+ isFlowVisualization={isFlowVisualization}
857
+ onStepChange={handleStepChange}
858
+ mode={mode}
859
+ />
860
+ </Panel>
861
+ )}
862
+ {includeKey && (
863
+ <Panel position="bottom-right" style={showMinimap ? { marginRight: '230px' } : undefined}>
864
+ <div className=" bg-white font-light px-4 text-[12px] shadow-md py-1 rounded-md">
865
+ <ul className="m-0 p-0 ">
866
+ {Object.entries(legend).map(([key, { count, colorClass, groupId }]) => (
867
+ <li
868
+ key={key}
869
+ className="flex space-x-2 items-center text-[10px] cursor-pointer hover:text-[rgb(var(--ec-accent))] hover:underline"
870
+ onClick={() => handleLegendClick(key, groupId)}
871
+ >
872
+ <span className={`w-2 h-2 block ${colorClass}`} />
873
+ <span className="block capitalize">
874
+ {key} ({count})
875
+ </span>
876
+ </li>
877
+ ))}
878
+ </ul>
879
+ </div>
880
+ </Panel>
881
+ )}
882
+ </ReactFlow>
883
+ )}
803
884
  <StudioModal isOpen={isStudioModalOpen || false} onClose={() => setIsStudioModalOpen(false)} />
885
+
886
+ {/* Share Link Modal */}
887
+ {isShareModalOpen && (
888
+ <>
889
+ <div
890
+ className="fixed inset-0 bg-black/20 z-40"
891
+ onClick={() => setIsShareModalOpen(false)}
892
+ style={{ animation: 'fadeIn 150ms ease-out' }}
893
+ />
894
+ <div
895
+ className="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-[rgb(var(--ec-page-bg))] rounded-lg shadow-xl z-50 w-full max-w-md p-6 border border-[rgb(var(--ec-page-border))]"
896
+ style={{ animation: 'slideInCenter 250ms ease-out' }}
897
+ >
898
+ <style>{`
899
+ @keyframes fadeIn {
900
+ from { opacity: 0; }
901
+ to { opacity: 1; }
902
+ }
903
+ @keyframes slideInCenter {
904
+ from { opacity: 0; transform: translate(-50%, -48%); }
905
+ to { opacity: 1; transform: translate(-50%, -50%); }
906
+ }
907
+ `}</style>
908
+
909
+ <div className="flex justify-between items-start mb-4">
910
+ <h3 className="text-lg font-semibold text-[rgb(var(--ec-page-text))]">Share Link</h3>
911
+ <button
912
+ onClick={() => setIsShareModalOpen(false)}
913
+ className="text-[rgb(var(--ec-page-text-muted))] hover:text-[rgb(var(--ec-page-text))] transition-colors"
914
+ aria-label="Close modal"
915
+ >
916
+ <ExternalLink className="w-5 h-5 rotate-180" />
917
+ </button>
918
+ </div>
919
+
920
+ <p className="text-sm text-[rgb(var(--ec-page-text-muted))] mb-4">
921
+ Share this link with your team to let them view this visualization.
922
+ </p>
923
+
924
+ <div className="flex gap-2">
925
+ <input
926
+ type="text"
927
+ readOnly
928
+ value={typeof window !== 'undefined' ? window.location.href : ''}
929
+ className="flex-1 px-3 py-2.5 bg-[rgb(var(--ec-input-bg))] border border-[rgb(var(--ec-input-border))] rounded-md text-[rgb(var(--ec-input-text))] text-sm focus:outline-none focus:ring-2 focus:ring-[rgb(var(--ec-accent))]"
930
+ />
931
+ <button
932
+ onClick={handleCopyShareUrl}
933
+ className={`px-4 py-2.5 rounded-md font-medium transition-all duration-200 flex items-center gap-2 ${
934
+ shareUrlCopySuccess ? 'bg-green-500 text-white' : 'bg-[rgb(var(--ec-accent))] text-white hover:opacity-90'
935
+ }`}
936
+ aria-label={shareUrlCopySuccess ? 'Copied!' : 'Copy link'}
937
+ >
938
+ {shareUrlCopySuccess ? <CheckIcon className="w-4 h-4" /> : <ClipboardIcon className="w-4 h-4" />}
939
+ <span>{shareUrlCopySuccess ? 'Copied!' : 'Copy'}</span>
940
+ </button>
941
+ </div>
942
+ </div>
943
+ </>
944
+ )}
804
945
  </div>
805
946
  );
806
947
  };
@@ -823,6 +964,7 @@ interface NodeGraphProps {
823
964
  showSearch?: boolean;
824
965
  zoomOnScroll?: boolean;
825
966
  designId?: string;
967
+ isChatEnabled?: boolean;
826
968
  }
827
969
 
828
970
  const NodeGraph = ({
@@ -843,6 +985,7 @@ const NodeGraph = ({
843
985
  showSearch = true,
844
986
  zoomOnScroll = false,
845
987
  designId,
988
+ isChatEnabled = false,
846
989
  }: NodeGraphProps) => {
847
990
  const [elem, setElem] = useState(null);
848
991
  const [showFooter, setShowFooter] = useState(true);
@@ -888,6 +1031,7 @@ const NodeGraph = ({
888
1031
  designId={designId || id}
889
1032
  isStudioModalOpen={isStudioModalOpen}
890
1033
  setIsStudioModalOpen={setIsStudioModalOpen}
1034
+ isChatEnabled={isChatEnabled}
891
1035
  />
892
1036
 
893
1037
  {showFooter && (