@eventcatalog/core 3.8.2 → 3.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) 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-275AT7XV.js → chunk-4LOZDOZG.js} +1 -1
  6. package/dist/{chunk-KBMXUUXX.js → chunk-5S7Y7EII.js} +1 -1
  7. package/dist/{chunk-OCD75GFW.js → chunk-F2A3CZVC.js} +1 -1
  8. package/dist/{chunk-I7HRERRK.js → chunk-RC4O6Q6B.js} +1 -1
  9. package/dist/{chunk-MOBOWLEW.js → chunk-VUNGSK7U.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.config.d.cts +1 -0
  14. package/dist/eventcatalog.config.d.ts +1 -0
  15. package/dist/eventcatalog.js +5 -5
  16. package/dist/generate.cjs +1 -1
  17. package/dist/generate.js +3 -3
  18. package/dist/utils/cli-logger.cjs +1 -1
  19. package/dist/utils/cli-logger.js +2 -2
  20. package/eventcatalog/src/components/ChatPanel/ChatPanel.tsx +50 -0
  21. package/eventcatalog/src/components/MDX/Design/Design.astro +4 -1
  22. package/eventcatalog/src/components/MDX/EntityMap/EntityMap.astro +4 -0
  23. package/eventcatalog/src/components/MDX/Flow/Flow.astro +4 -1
  24. package/eventcatalog/src/components/MDX/NodeGraph/MermaidView.tsx +242 -0
  25. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +5 -0
  26. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.tsx +322 -189
  27. package/eventcatalog/src/components/MDX/NodeGraph/VisualizerDropdownContent.tsx +224 -0
  28. package/eventcatalog/src/content.config.ts +1 -1
  29. package/eventcatalog/src/enterprise/ai/chat-api.ts +23 -0
  30. package/eventcatalog/src/enterprise/tools/catalog-tools.ts +96 -0
  31. package/eventcatalog/src/pages/diagrams/[id]/[version]/embed.astro +1 -0
  32. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version].mermaid.ts +128 -0
  33. package/eventcatalog/src/pages/visualiser/designs/[id]/index.astro +4 -0
  34. package/eventcatalog/src/utils/clipboard.ts +22 -0
  35. package/eventcatalog/src/utils/mermaid-zoom.ts +1 -0
  36. package/eventcatalog/src/utils/node-graphs/export-mermaid.ts +299 -0
  37. 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,8 @@ 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 { ExternalLink, HistoryIcon, CheckIcon, ClipboardIcon, MoreVertical } from 'lucide-react';
22
+ import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
21
23
  import { toPng } from 'html-to-image';
22
24
  import { DocumentArrowDownIcon, PresentationChartLineIcon } from '@heroicons/react/24/outline';
23
25
  // Nodes and edges
@@ -47,11 +49,15 @@ import { navigate } from 'astro:transitions/client';
47
49
  import type { CollectionTypes } from '@types';
48
50
  import { buildUrl } from '@utils/url-builder';
49
51
  import ChannelNode from './Nodes/Channel';
50
- import { CogIcon } from '@heroicons/react/20/solid';
51
52
  import { useEventCatalogVisualiser } from 'src/hooks/eventcatalog-visualizer';
52
53
  import VisualiserSearch, { type VisualiserSearchRef } from './VisualiserSearch';
53
54
  import StepWalkthrough from './StepWalkthrough';
54
55
  import StudioModal from './StudioModal';
56
+ import MermaidView from './MermaidView';
57
+ import VisualizerDropdownContent from './VisualizerDropdownContent';
58
+ import { convertToMermaid } from '@utils/node-graphs/export-mermaid';
59
+ import { copyToClipboard } from '@utils/clipboard';
60
+
55
61
  interface Props {
56
62
  nodes: any;
57
63
  edges: any;
@@ -70,6 +76,8 @@ interface Props {
70
76
  designId?: string;
71
77
  isStudioModalOpen?: boolean;
72
78
  setIsStudioModalOpen?: (isOpen: boolean) => void;
79
+ isChatEnabled?: boolean;
80
+ maxTextSize?: number;
73
81
  }
74
82
 
75
83
  const getVisualiserUrlForCollection = (collectionItem: CollectionEntry<CollectionTypes>) => {
@@ -91,6 +99,8 @@ const NodeGraphBuilder = ({
91
99
  zoomOnScroll = false,
92
100
  isStudioModalOpen,
93
101
  setIsStudioModalOpen = () => {},
102
+ isChatEnabled = false,
103
+ maxTextSize,
94
104
  }: Props) => {
95
105
  const nodeTypes = useMemo(
96
106
  () =>
@@ -135,10 +145,14 @@ const NodeGraphBuilder = ({
135
145
  );
136
146
  const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
137
147
  const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
138
- const [isSettingsOpen, setIsSettingsOpen] = useState(false);
139
148
  const [animateMessages, setAnimateMessages] = useState(false);
140
149
  const [activeStepIndex, setActiveStepIndex] = useState<number | null>(null);
141
150
  const [isFullscreen, setIsFullscreen] = useState(false);
151
+ const [mermaidCode, setMermaidCode] = useState('');
152
+ const [isShareModalOpen, setIsShareModalOpen] = useState(false);
153
+ const [shareUrlCopySuccess, setShareUrlCopySuccess] = useState(false);
154
+ const [isMermaidView, setIsMermaidView] = useState(false);
155
+ const [showMinimap, setShowMinimap] = useState(false);
142
156
  // const [isStudioModalOpen, setIsStudioModalOpen] = useState(false);
143
157
 
144
158
  // Check if there are channels to determine if we need the visualizer functionality
@@ -234,6 +248,11 @@ const NodeGraphBuilder = ({
234
248
  localStorage.setItem('EventCatalog:animateMessages', JSON.stringify(!animateMessages));
235
249
  };
236
250
 
251
+ // Handle fit to view
252
+ const handleFitView = useCallback(() => {
253
+ fitView({ duration: 400, padding: 0.2 });
254
+ }, [fitView]);
255
+
237
256
  // animate messages, between views
238
257
  // URL parameter takes priority over localStorage
239
258
  useEffect(() => {
@@ -270,6 +289,17 @@ const NodeGraphBuilder = ({
270
289
  }, 150);
271
290
  }, []);
272
291
 
292
+ // Generate mermaid code from nodes and edges
293
+ useEffect(() => {
294
+ try {
295
+ const code = convertToMermaid(nodes, edges, { includeStyles: true, direction: 'LR' });
296
+ setMermaidCode(code);
297
+ } catch (error) {
298
+ console.error('Error generating mermaid code:', error);
299
+ setMermaidCode('');
300
+ }
301
+ }, [nodes, edges]);
302
+
273
303
  // Handle scroll wheel events to forward to page when no modifier keys are pressed
274
304
  // Only when zoomOnScroll is disabled
275
305
  // This is a fix for when we embed node graphs into pages, and users are scrolling the documentation pages
@@ -335,7 +365,6 @@ const NodeGraphBuilder = ({
335
365
  }, [zoomOnScroll]);
336
366
 
337
367
  const handlePaneClick = useCallback(() => {
338
- setIsSettingsOpen(false);
339
368
  searchRef.current?.hideSuggestions();
340
369
  resetNodesAndEdges();
341
370
  fitView({ duration: 800 });
@@ -364,6 +393,21 @@ const NodeGraphBuilder = ({
364
393
  setIsStudioModalOpen(true);
365
394
  };
366
395
 
396
+ const openChat = useCallback(() => {
397
+ window.dispatchEvent(new CustomEvent('eventcatalog:open-chat'));
398
+ }, []);
399
+
400
+ const handleCopyArchitectureCode = useCallback(async () => {
401
+ await copyToClipboard(mermaidCode);
402
+ }, [mermaidCode]);
403
+
404
+ const handleCopyShareUrl = useCallback(async () => {
405
+ const url = typeof window !== 'undefined' ? window.location.href : '';
406
+ await copyToClipboard(url);
407
+ setShareUrlCopySuccess(true);
408
+ setTimeout(() => setShareUrlCopySuccess(false), 2000);
409
+ }, []);
410
+
367
411
  const toggleFullScreen = useCallback(() => {
368
412
  if (!document.fullscreenElement) {
369
413
  reactFlowWrapperRef.current?.requestFullscreen().catch((err) => {
@@ -396,8 +440,7 @@ const NodeGraphBuilder = ({
396
440
  const height = imageHeight > nodesBounds.height ? imageHeight : nodesBounds.height;
397
441
  const viewport = getViewportForBounds(nodesBounds, width, height, 0.5, 2, 0);
398
442
 
399
- // Hide settings panel and controls during export
400
- setIsSettingsOpen(false);
443
+ // Hide controls during export
401
444
  const controls = document.querySelector('.react-flow__controls') as HTMLElement;
402
445
  if (controls) controls.style.display = 'none';
403
446
 
@@ -603,204 +646,288 @@ const NodeGraphBuilder = ({
603
646
  const isFlowVisualization = edges.some((edge: Edge) => edge.type === 'flow-edge');
604
647
 
605
648
  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">
649
+ <div ref={reactFlowWrapperRef} className="w-full h-full bg-gray-50 flex flex-col">
650
+ {isMermaidView ? (
651
+ <>
652
+ {/* Menu Bar for Mermaid View */}
653
+ <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
654
  <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
- )}
655
+ {/* Settings Dropdown Menu */}
656
+ <DropdownMenu.Root>
657
+ <DropdownMenu.Trigger asChild>
658
+ <button
659
+ 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"
660
+ aria-label="Open menu"
661
+ >
662
+ {title && (
663
+ <span className="text-base font-medium text-[rgb(var(--ec-page-text))] leading-tight">{title}</span>
664
+ )}
665
+ <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" />
666
+ </button>
667
+ </DropdownMenu.Trigger>
668
+ <DropdownMenu.Portal>
669
+ <DropdownMenu.Content
670
+ 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"
671
+ sideOffset={0}
672
+ align="end"
673
+ alignOffset={-180}
674
+ >
675
+ <DropdownMenu.Arrow className="fill-[rgb(var(--ec-page-bg))] stroke-[rgb(var(--ec-page-border))] stroke-1" />
676
+ <VisualizerDropdownContent
677
+ isMermaidView={isMermaidView}
678
+ setIsMermaidView={setIsMermaidView}
679
+ animateMessages={animateMessages}
680
+ toggleAnimateMessages={toggleAnimateMessages}
681
+ hideChannels={hideChannels}
682
+ toggleChannelsVisibility={toggleChannelsVisibility}
683
+ hasChannels={hasChannels}
684
+ showMinimap={showMinimap}
685
+ setShowMinimap={setShowMinimap}
686
+ handleFitView={handleFitView}
687
+ searchRef={searchRef}
688
+ isChatEnabled={isChatEnabled}
689
+ openChat={openChat}
690
+ handleCopyArchitectureCode={handleCopyArchitectureCode}
691
+ handleExportVisual={handleExportVisual}
692
+ setIsShareModalOpen={setIsShareModalOpen}
693
+ toggleFullScreen={toggleFullScreen}
694
+ openStudioModal={openStudioModal}
695
+ />
696
+ </DropdownMenu.Content>
697
+ </DropdownMenu.Portal>
698
+ </DropdownMenu.Root>
660
699
  </div>
661
700
  {mode === 'full' && showSearch && (
662
- <div className="flex justify-end space-x-2 w-96">
663
- <VisualiserSearch ref={searchRef} nodes={nodes} onNodeSelect={handleNodeSelect} onClear={handleSearchClear} />
701
+ <div className="flex justify-end items-center gap-2">
702
+ {!isMermaidView && (
703
+ <div className="w-96">
704
+ <VisualiserSearch ref={searchRef} nodes={nodes} onNodeSelect={handleNodeSelect} onClear={handleSearchClear} />
705
+ </div>
706
+ )}
664
707
  </div>
665
708
  )}
666
709
  </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>
710
+ {/* Mermaid View */}
711
+ <div className="flex-1 overflow-hidden">
712
+ <MermaidView nodes={nodes} edges={edges} maxTextSize={maxTextSize} />
713
+ </div>
714
+ </>
715
+ ) : (
716
+ <ReactFlow
717
+ nodeTypes={nodeTypes}
718
+ edgeTypes={edgeTypes}
719
+ minZoom={0.07}
720
+ nodes={nodes}
721
+ edges={edges}
722
+ fitView
723
+ onNodesChange={onNodesChange}
724
+ onEdgesChange={onEdgesChange}
725
+ connectionLineType={ConnectionLineType.SmoothStep}
726
+ nodeOrigin={[0.1, 0.1]}
727
+ onNodeClick={handleNodeClick}
728
+ onPaneClick={handlePaneClick}
729
+ zoomOnScroll={zoomOnScroll}
730
+ className="relative"
731
+ >
732
+ <Panel position="top-center" className="w-full pr-6 ">
733
+ <div className="flex space-x-2 justify-between items-center">
734
+ <div className="flex space-x-2 ml-4">
735
+ {/* Settings Dropdown Menu */}
736
+ <DropdownMenu.Root>
737
+ <DropdownMenu.Trigger asChild>
726
738
  <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`}
739
+ 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"
740
+ aria-label="Open menu"
732
741
  >
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
- />
742
+ {title && (
743
+ <span className="text-base font-medium text-[rgb(var(--ec-page-text))] leading-tight">{title}</span>
744
+ )}
745
+ <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
746
  </button>
739
- </div>
740
- <p className="text-[10px] text-gray-500">Show or hide channels in the visualizer.</p>
747
+ </DropdownMenu.Trigger>
748
+ <DropdownMenu.Portal>
749
+ <DropdownMenu.Content
750
+ 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"
751
+ sideOffset={0}
752
+ align="end"
753
+ alignOffset={-180}
754
+ >
755
+ <DropdownMenu.Arrow className="fill-[rgb(var(--ec-page-bg))] stroke-[rgb(var(--ec-page-border))] stroke-1" />
756
+ <VisualizerDropdownContent
757
+ isMermaidView={isMermaidView}
758
+ setIsMermaidView={setIsMermaidView}
759
+ animateMessages={animateMessages}
760
+ toggleAnimateMessages={toggleAnimateMessages}
761
+ hideChannels={hideChannels}
762
+ toggleChannelsVisibility={toggleChannelsVisibility}
763
+ hasChannels={hasChannels}
764
+ showMinimap={showMinimap}
765
+ setShowMinimap={setShowMinimap}
766
+ handleFitView={handleFitView}
767
+ searchRef={searchRef}
768
+ isChatEnabled={isChatEnabled}
769
+ openChat={openChat}
770
+ handleCopyArchitectureCode={handleCopyArchitectureCode}
771
+ handleExportVisual={handleExportVisual}
772
+ setIsShareModalOpen={setIsShareModalOpen}
773
+ toggleFullScreen={toggleFullScreen}
774
+ openStudioModal={openStudioModal}
775
+ />
776
+ </DropdownMenu.Content>
777
+ </DropdownMenu.Portal>
778
+ </DropdownMenu.Root>
779
+ </div>
780
+ {mode === 'full' && showSearch && (
781
+ <div className="flex justify-end items-center gap-2">
782
+ {!isMermaidView && (
783
+ <div className="w-96">
784
+ <VisualiserSearch
785
+ ref={searchRef}
786
+ nodes={nodes}
787
+ onNodeSelect={handleNodeSelect}
788
+ onClear={handleSearchClear}
789
+ />
790
+ </div>
791
+ )}
741
792
  </div>
742
793
  )}
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
794
  </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)}
795
+ {links.length > 0 && (
796
+ <div className="flex justify-end mt-3">
797
+ <div className="relative flex items-center -mt-1">
798
+ <span className="absolute left-2 pointer-events-none flex items-center h-full">
799
+ <HistoryIcon className="h-4 w-4 text-gray-600" />
800
+ </span>
801
+ <select
802
+ value={links.find((link) => window.location.href.includes(link.url))?.url || links[0].url}
803
+ onChange={(e) => navigate(e.target.value)}
804
+ 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))]"
805
+ style={{ minWidth: 120, height: '26px' }}
791
806
  >
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>
807
+ {links.map((link) => (
808
+ <option key={link.url} value={link.url}>
809
+ {link.label}
810
+ </option>
811
+ ))}
812
+ </select>
813
+ <span className="absolute right-2 pointer-events-none">
814
+ <svg className="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
815
+ <path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
816
+ </svg>
817
+ </span>
818
+ </div>
819
+ </div>
820
+ )}
800
821
  </Panel>
801
- )}
802
- </ReactFlow>
822
+
823
+ {includeBackground && <Background color="#bbb" gap={16} />}
824
+ {includeBackground && <Controls />}
825
+ {showMinimap && (
826
+ <MiniMap
827
+ nodeStrokeWidth={3}
828
+ zoomable
829
+ pannable
830
+ style={{
831
+ backgroundColor: 'rgb(var(--ec-page-bg))',
832
+ border: '1px solid rgb(var(--ec-page-border))',
833
+ borderRadius: '8px',
834
+ }}
835
+ />
836
+ )}
837
+ {isFlowVisualization && showFlowWalkthrough && (
838
+ <Panel position="bottom-left">
839
+ <StepWalkthrough
840
+ nodes={nodes}
841
+ edges={edges}
842
+ isFlowVisualization={isFlowVisualization}
843
+ onStepChange={handleStepChange}
844
+ mode={mode}
845
+ />
846
+ </Panel>
847
+ )}
848
+ {includeKey && (
849
+ <Panel position="bottom-right" style={showMinimap ? { marginRight: '230px' } : undefined}>
850
+ <div className=" bg-white font-light px-4 text-[12px] shadow-md py-1 rounded-md">
851
+ <ul className="m-0 p-0 ">
852
+ {Object.entries(legend).map(([key, { count, colorClass, groupId }]) => (
853
+ <li
854
+ key={key}
855
+ className="flex space-x-2 items-center text-[10px] cursor-pointer hover:text-[rgb(var(--ec-accent))] hover:underline"
856
+ onClick={() => handleLegendClick(key, groupId)}
857
+ >
858
+ <span className={`w-2 h-2 block ${colorClass}`} />
859
+ <span className="block capitalize">
860
+ {key} ({count})
861
+ </span>
862
+ </li>
863
+ ))}
864
+ </ul>
865
+ </div>
866
+ </Panel>
867
+ )}
868
+ </ReactFlow>
869
+ )}
803
870
  <StudioModal isOpen={isStudioModalOpen || false} onClose={() => setIsStudioModalOpen(false)} />
871
+
872
+ {/* Share Link Modal */}
873
+ {isShareModalOpen && (
874
+ <>
875
+ <div
876
+ className="fixed inset-0 bg-black/20 z-40"
877
+ onClick={() => setIsShareModalOpen(false)}
878
+ style={{ animation: 'fadeIn 150ms ease-out' }}
879
+ />
880
+ <div
881
+ 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))]"
882
+ style={{ animation: 'slideInCenter 250ms ease-out' }}
883
+ >
884
+ <style>{`
885
+ @keyframes fadeIn {
886
+ from { opacity: 0; }
887
+ to { opacity: 1; }
888
+ }
889
+ @keyframes slideInCenter {
890
+ from { opacity: 0; transform: translate(-50%, -48%); }
891
+ to { opacity: 1; transform: translate(-50%, -50%); }
892
+ }
893
+ `}</style>
894
+
895
+ <div className="flex justify-between items-start mb-4">
896
+ <h3 className="text-lg font-semibold text-[rgb(var(--ec-page-text))]">Share Link</h3>
897
+ <button
898
+ onClick={() => setIsShareModalOpen(false)}
899
+ className="text-[rgb(var(--ec-page-text-muted))] hover:text-[rgb(var(--ec-page-text))] transition-colors"
900
+ aria-label="Close modal"
901
+ >
902
+ <ExternalLink className="w-5 h-5 rotate-180" />
903
+ </button>
904
+ </div>
905
+
906
+ <p className="text-sm text-[rgb(var(--ec-page-text-muted))] mb-4">
907
+ Share this link with your team to let them view this visualization.
908
+ </p>
909
+
910
+ <div className="flex gap-2">
911
+ <input
912
+ type="text"
913
+ readOnly
914
+ value={typeof window !== 'undefined' ? window.location.href : ''}
915
+ 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))]"
916
+ />
917
+ <button
918
+ onClick={handleCopyShareUrl}
919
+ className={`px-4 py-2.5 rounded-md font-medium transition-all duration-200 flex items-center gap-2 ${
920
+ shareUrlCopySuccess ? 'bg-green-500 text-white' : 'bg-[rgb(var(--ec-accent))] text-white hover:opacity-90'
921
+ }`}
922
+ aria-label={shareUrlCopySuccess ? 'Copied!' : 'Copy link'}
923
+ >
924
+ {shareUrlCopySuccess ? <CheckIcon className="w-4 h-4" /> : <ClipboardIcon className="w-4 h-4" />}
925
+ <span>{shareUrlCopySuccess ? 'Copied!' : 'Copy'}</span>
926
+ </button>
927
+ </div>
928
+ </div>
929
+ </>
930
+ )}
804
931
  </div>
805
932
  );
806
933
  };
@@ -823,6 +950,8 @@ interface NodeGraphProps {
823
950
  showSearch?: boolean;
824
951
  zoomOnScroll?: boolean;
825
952
  designId?: string;
953
+ isChatEnabled?: boolean;
954
+ maxTextSize?: number;
826
955
  }
827
956
 
828
957
  const NodeGraph = ({
@@ -843,6 +972,8 @@ const NodeGraph = ({
843
972
  showSearch = true,
844
973
  zoomOnScroll = false,
845
974
  designId,
975
+ isChatEnabled = false,
976
+ maxTextSize,
846
977
  }: NodeGraphProps) => {
847
978
  const [elem, setElem] = useState(null);
848
979
  const [showFooter, setShowFooter] = useState(true);
@@ -888,6 +1019,8 @@ const NodeGraph = ({
888
1019
  designId={designId || id}
889
1020
  isStudioModalOpen={isStudioModalOpen}
890
1021
  setIsStudioModalOpen={setIsStudioModalOpen}
1022
+ isChatEnabled={isChatEnabled}
1023
+ maxTextSize={maxTextSize}
891
1024
  />
892
1025
 
893
1026
  {showFooter && (