- {referencedFileSources.length > 0 ? (
-
+ {(referencedFileSources.length > 0 || diagrams.length > 0) ? (
+ sourcesView
) : isNetworkActive ? (
{Array.from({ length: 3 }).map((_, index) => (
@@ -466,6 +505,7 @@ const ChatThreadListItemComponent = forwardRef
+
)
});
diff --git a/packages/web/src/ee/features/chat/components/chatThread/diagramPanelListItem.tsx b/packages/web/src/ee/features/chat/components/chatThread/diagramPanelListItem.tsx
new file mode 100644
index 000000000..621c2c6ef
--- /dev/null
+++ b/packages/web/src/ee/features/chat/components/chatThread/diagramPanelListItem.tsx
@@ -0,0 +1,75 @@
+'use client';
+
+import { Button } from "@/components/ui/button";
+import { cn } from "@/lib/utils";
+import { ExtractedDiagram } from "@/ee/features/chat/useExtractPanelItems";
+import { ChevronDown, ChevronRight, CornerUpLeft, Workflow } from "lucide-react";
+import { MermaidDiagram } from "./mermaidDiagram";
+import { getDiagramTitle } from "@/ee/features/chat/diagramUtils";
+
+interface DiagramPanelListItemProps {
+ diagram: ExtractedDiagram;
+ index: number;
+ isExpanded: boolean;
+ isHighlighted: boolean;
+ isHovered: boolean;
+ onToggle: () => void;
+ onJumpToInline: () => void;
+}
+
+export const DiagramPanelListItem = ({
+ diagram,
+ index,
+ isExpanded,
+ isHighlighted,
+ isHovered,
+ onToggle,
+ onJumpToInline,
+}: DiagramPanelListItemProps) => {
+ const label = getDiagramTitle(diagram.code) ?? `Diagram ${index + 1}`;
+
+ return (
+
+
+
+ {isExpanded ? (
+
+ ) : (
+
+ )}
+
+ {label}
+
+
+
+
+
+
+ {isExpanded && (
+
+ )}
+
+ );
+};
diff --git a/packages/web/src/ee/features/chat/components/chatThread/diagramReferenceChip.tsx b/packages/web/src/ee/features/chat/components/chatThread/diagramReferenceChip.tsx
new file mode 100644
index 000000000..c3ae4d88e
--- /dev/null
+++ b/packages/web/src/ee/features/chat/components/chatThread/diagramReferenceChip.tsx
@@ -0,0 +1,94 @@
+'use client';
+
+import { cn } from '@/lib/utils';
+import { Workflow } from 'lucide-react';
+import { useCallback, useEffect, useMemo, useRef } from 'react';
+import { getDiagramAnchorId, getDiagramId, getDiagramTitle } from '@/ee/features/chat/diagramUtils';
+import { usePanelContext } from '@/ee/features/chat/panelContext';
+import { TextShimmer } from '@/components/ui/textShimmer';
+import useCaptureEvent from '@/hooks/useCaptureEvent';
+
+interface DiagramReferenceChipProps {
+ code: string;
+}
+
+/**
+ * Inline, in-answer reference to a diagram. The full diagram is rendered in the
+ * right "evidence" panel; here we render a compact button (mirroring the
+ * file-reference chips) that scrolls to and focuses the panel diagram on click,
+ * and highlights it on hover. The raw mermaid fence in the answer text is
+ * untouched, so copying the answer still yields a valid mermaid code block.
+ */
+export const DiagramReferenceChip = ({ code }: DiagramReferenceChipProps) => {
+ const panel = usePanelContext();
+ const captureEvent = useCaptureEvent();
+ const containerRef = useRef
(null);
+
+ const diagramId = useMemo(() => getDiagramId(code), [code]);
+ const anchorId = useMemo(() => getDiagramAnchorId(code), [code]);
+
+ const index = panel?.getDiagramIndex(diagramId) ?? -1;
+
+ // While streaming, a chip whose source has not yet resolved to a (closed)
+ // panel diagram is the one currently being written: show a shimmer so the
+ // turn doesn't look stalled.
+ const isGenerating = (panel?.isStreaming ?? false) && index < 0;
+
+ const label = useMemo(() => {
+ if (isGenerating) {
+ return 'Generating diagram…';
+ }
+ const title = getDiagramTitle(code);
+ if (title) {
+ return title;
+ }
+ return index >= 0 ? `Diagram ${index + 1}` : 'Diagram';
+ }, [code, index, isGenerating]);
+
+ const reveal = useCallback(() => {
+ if (panel?.chatId) {
+ captureEvent('wa_chat_diagram_reference_clicked', { chatId: panel.chatId, diagramId });
+ }
+ panel?.revealDiagram(diagramId);
+ }, [panel, diagramId, captureEvent]);
+
+ // Shared in-thread deep links target the inline anchor (`#diagram-`).
+ // When the hash matches, scroll the chip into view and reveal the full
+ // diagram in the panel.
+ useEffect(() => {
+ const checkHash = () => {
+ if (typeof window === 'undefined' || window.location.hash !== `#${anchorId}`) {
+ return;
+ }
+ containerRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ panel?.revealDiagram(diagramId);
+ };
+
+ checkHash();
+ window.addEventListener('hashchange', checkHash);
+ return () => window.removeEventListener('hashchange', checkHash);
+ }, [anchorId, diagramId, panel]);
+
+ return (
+ panel?.setHoveredDiagram(diagramId)}
+ onMouseLeave={() => panel?.setHoveredDiagram(undefined)}
+ title={isGenerating ? undefined : 'Click to view diagram'}
+ >
+
+ {isGenerating ? (
+ {label}
+ ) : (
+ {label}
+ )}
+
+ );
+};
diff --git a/packages/web/src/ee/features/chat/components/chatThread/markdownRenderer.tsx b/packages/web/src/ee/features/chat/components/chatThread/markdownRenderer.tsx
index 5ef177dc1..012e1e682 100644
--- a/packages/web/src/ee/features/chat/components/chatThread/markdownRenderer.tsx
+++ b/packages/web/src/ee/features/chat/components/chatThread/markdownRenderer.tsx
@@ -18,6 +18,7 @@ import type { PluggableList, Plugin } from "unified";
import { visit } from 'unist-util-visit';
import { CodeBlock } from './codeBlock';
import { LinearIssueCard } from './linearIssueCard';
+import { DiagramReferenceChip } from './diagramReferenceChip';
import { FILE_REFERENCE_REGEX } from '@/features/chat/constants';
import { createFileReference } from '@/features/chat/utils';
import isEqual from "fast-deep-equal/react";
@@ -129,9 +130,14 @@ interface MarkdownRendererProps {
* instead of being parsed as HTML. File references (@file:{...}) are unaffected.
*/
escapeHtml?: boolean;
+ /**
+ * When true, fenced ```mermaid blocks are rendered as diagrams instead of
+ * code. Enabled only on the answer body (see answerCard.tsx).
+ */
+ enableDiagrams?: boolean;
}
-const MarkdownRendererComponent = forwardRef(({ content, className, escapeHtml = false }, ref) => {
+const MarkdownRendererComponent = forwardRef(({ content, className, escapeHtml = false, enableDiagrams = false }, ref) => {
const router = useRouter();
const remarkPlugins = useMemo((): PluggableList => {
@@ -204,6 +210,14 @@ const MarkdownRendererComponent = forwardRef
+ )
+ }
+
return (
)
- }, [router]);
+ }, [router, enableDiagrams]);
return (
| null = null;
+const loadMermaid = async () => {
+ if (!mermaidModulePromise) {
+ mermaidModulePromise = import('mermaid').then((mod) => mod.default);
+ }
+ return mermaidModulePromise;
+};
+
+let renderCounter = 0;
+
+const renderMermaidToSvg = async (rawCode: string, theme: 'dark' | 'default'): Promise
=> {
+ const code = sanitizeMermaidCode(rawCode);
+ const mermaid = await loadMermaid();
+ mermaid.initialize({
+ startOnLoad: false,
+ // `strict` sanitizes mermaid's own SVG output (via DOMPurify) and
+ // disables click bindings / arbitrary HTML in labels.
+ securityLevel: 'strict',
+ suppressErrorRendering: true,
+ // Render labels as native SVG rather than HTML .
+ // foreignObject content taints the canvas, which breaks PNG export
+ // (`toBlob` throws a SecurityError on a tainted canvas).
+ htmlLabels: false,
+ flowchart: { htmlLabels: false },
+ theme,
+ });
+
+ // Validate first so partial / invalid input throws before we attempt a
+ // render (this is what drives the fallback-to-code behavior).
+ await mermaid.parse(code);
+
+ const id = `sb-mermaid-${Date.now()}-${renderCounter++}`;
+ const { svg } = await mermaid.render(id, code);
+ return svg;
+};
+
+const triggerDownload = (blob: Blob, filename: string) => {
+ const url = URL.createObjectURL(blob);
+ const anchor = document.createElement('a');
+ anchor.href = url;
+ anchor.download = filename;
+ document.body.appendChild(anchor);
+ anchor.click();
+ document.body.removeChild(anchor);
+ URL.revokeObjectURL(url);
+};
+
+const PNG_TARGET_LONGEST_SIDE = 2400;
+const PNG_MAX_CANVAS_DIM = 8000;
+
+const getSvgBaseSize = (svgEl: Element): { width: number; height: number } => {
+ const viewBox = svgEl.getAttribute('viewBox');
+ if (viewBox) {
+ const parts = viewBox.split(/[\s,]+/).map(Number).filter((n) => !Number.isNaN(n));
+ if (parts.length === 4 && parts[2] > 0 && parts[3] > 0) {
+ return { width: parts[2], height: parts[3] };
+ }
+ }
+
+ const width = parseFloat(svgEl.getAttribute('width') || '');
+ const height = parseFloat(svgEl.getAttribute('height') || '');
+ if (width > 0 && height > 0) {
+ return { width, height };
+ }
+
+ return { width: 1024, height: 768 };
+};
+
+/**
+ * Rasterize a mermaid SVG to a high-resolution PNG. We bake explicit pixel
+ * dimensions (scaled up from the SVG's viewBox) into the SVG before drawing so
+ * the browser rasterizes the vector at full resolution instead of upscaling a
+ * small default bitmap.
+ */
+const svgToPngBlob = (svg: string, background: string): Promise => {
+ return new Promise((resolve) => {
+ const doc = new DOMParser().parseFromString(svg, 'image/svg+xml');
+ const svgEl = doc.documentElement;
+ const { width: baseWidth, height: baseHeight } = getSvgBaseSize(svgEl);
+
+ const longestSide = Math.max(baseWidth, baseHeight);
+ let scale = Math.max(2, PNG_TARGET_LONGEST_SIDE / longestSide);
+ if (baseWidth * scale > PNG_MAX_CANVAS_DIM || baseHeight * scale > PNG_MAX_CANVAS_DIM) {
+ scale = Math.min(PNG_MAX_CANVAS_DIM / baseWidth, PNG_MAX_CANVAS_DIM / baseHeight);
+ }
+
+ const outWidth = Math.round(baseWidth * scale);
+ const outHeight = Math.round(baseHeight * scale);
+
+ svgEl.setAttribute('width', String(outWidth));
+ svgEl.setAttribute('height', String(outHeight));
+ const serialized = new XMLSerializer().serializeToString(svgEl);
+
+ const url = URL.createObjectURL(new Blob([serialized], { type: 'image/svg+xml;charset=utf-8' }));
+ const image = new Image();
+
+ image.onload = () => {
+ const canvas = document.createElement('canvas');
+ canvas.width = outWidth;
+ canvas.height = outHeight;
+
+ const ctx = canvas.getContext('2d');
+ if (!ctx) {
+ URL.revokeObjectURL(url);
+ resolve(null);
+ return;
+ }
+
+ // Mermaid SVGs are transparent; paint a theme-matched background so
+ // the exported PNG is readable on any viewer.
+ ctx.fillStyle = background;
+ ctx.fillRect(0, 0, outWidth, outHeight);
+ ctx.drawImage(image, 0, 0, outWidth, outHeight);
+ URL.revokeObjectURL(url);
+
+ canvas.toBlob((pngBlob) => resolve(pngBlob), 'image/png');
+ };
+
+ image.onerror = () => {
+ URL.revokeObjectURL(url);
+ resolve(null);
+ };
+
+ image.src = url;
+ });
+};
+
+// Breathing room so the fitted diagram doesn't touch the viewport edges.
+const FIT_MARGIN = 0.95;
+
+// Panel sizing (see `computeFit`): the viewport grows to the diagram instead of
+// a fixed box, clamped to the readable area.
+const PANEL_MIN_HEIGHT = 240;
+// Gap below a full-height diagram (avoids scroll jitter, keeps neighbours reachable).
+const PANEL_VIEWPORT_MARGIN = 24;
+// Fallback when no scroll-area ancestor can be measured.
+const PANEL_FALLBACK_MAX_HEIGHT = 720;
+// Cap on enlarging a wide diagram: at most this many column-widths wide.
+const PANEL_MAX_OVERFLOW_FACTOR = 2.5;
+
+// Zoom limits and the button step are relative to the fitted ("100%") scale, so
+// the usable range and ±25% readout steps are consistent across diagram sizes.
+const ZOOM_OUT_FACTOR = 0.25;
+const ZOOM_IN_FACTOR = 32;
+const ZOOM_STEP_FACTOR = 0.25;
+
+interface IntrinsicSize {
+ width: number;
+ height: number;
+}
+
+// Diagram aspect ratio, from the SVG's viewBox. The SVG is rendered responsively
+// (fills the width), so we only need the ratio for the fit math.
+const parseSvgSize = (svg: string): IntrinsicSize => {
+ const viewBoxMatch = svg.match(/viewBox\s*=\s*["']([^"']+)["']/);
+ if (viewBoxMatch) {
+ const parts = viewBoxMatch[1].split(/[\s,]+/).map(Number).filter((n) => !Number.isNaN(n));
+ if (parts.length === 4 && parts[2] > 0 && parts[3] > 0) {
+ return { width: parts[2], height: parts[3] };
+ }
+ }
+ return { width: 1024, height: 768 };
+};
+
+interface DiagramFit {
+ // Initial transform scale (the 100% baseline); 1 = "fills the available width".
+ fitScale: number;
+ // Panel viewport height in px; null for inline / fullscreen (fixed / filled).
+ panelHeight: number | null;
+}
+
+// Pure fit, as a transform scale where 1 = "fills the available width". Inline
+// and fullscreen contain-fit the whole diagram; the panel contain-fits unless
+// the diagram is wider than the column, in which case it's enlarged to use the
+// readable height and overflows horizontally (bounded so it can't explode).
+const computeFit = (
+ { width: iw, height: ih }: IntrinsicSize,
+ availWidth: number,
+ availHeight: number,
+ autoHeight: boolean,
+): DiagramFit => {
+ // How tall the diagram is when it fills the available width (i.e. at scale 1).
+ const widthFitHeight = availWidth * (ih / iw);
+ const heightFitScale = availHeight / widthFitHeight;
+
+ if (!autoHeight) {
+ // Contain-fit: never wider than the box, shrink to fit the height if tall.
+ const fitScale = Math.min(1, heightFitScale) * FIT_MARGIN;
+ return { fitScale, panelHeight: null };
+ }
+
+ const isWiderThanPanel = iw > availWidth;
+ const fitScale = (isWiderThanPanel
+ // Enlarge into the readable height and overflow horizontally (drag to
+ // pan); never below filling the column width, capped at N column-widths.
+ ? Math.min(Math.max(heightFitScale, 1), PANEL_MAX_OVERFLOW_FACTOR)
+ // Contain-fit: the whole diagram stays visible.
+ : Math.min(1, heightFitScale)) * FIT_MARGIN;
+
+ const panelHeight = Math.round(Math.min(availHeight, Math.max(PANEL_MIN_HEIGHT, widthFitHeight * fitScale)));
+ return { fitScale, panelHeight };
+};
+
+/**
+ * Interactive diagram surface: the SVG with drag-to-pan + zoom (via
+ * react-zoom-pan-pinch) and hover controls. Shared by the right-panel and
+ * fullscreen views. The SVG fills the available width and the transform handles
+ * zoom/overflow; `computeFit` picks the initial scale (treated as 100%), and the
+ * zoom limits and button step are relative to it.
+ *
+ * `fill` is the fullscreen mode (contain-fit into the dialog); otherwise this is
+ * the panel diagram, whose height grows to the diagram (clamped to the readable
+ * area).
+ */
+const DiagramViewport = ({ svg, className, controlsClassName, actions, fill, forceControlsVisible, onPan }: { svg: string; className?: string; controlsClassName?: string; actions?: ReactNode; fill?: boolean; forceControlsVisible?: boolean; onPan?: () => void }) => {
+ const intrinsic = useMemo(() => parseSvgSize(svg), [svg]);
+ const rootRef = useRef(null);
+ const apiRef = useRef(null);
+ // Fit (initial scale + panel height), measured from the container. Null until
+ // measured; we gate the transform wrapper on it so the first paint is fitted.
+ const [fit, setFit] = useState(null);
+ // Live transform scale, driven by the library, for the relative zoom readout.
+ const [scale, setScale] = useState(0);
+
+ // Measure the available area and (re)compute the fit. Runs before paint and
+ // on container / scroll-viewport resize.
+ useLayoutEffect(() => {
+ const root = rootRef.current;
+ if (!root) {
+ return;
+ }
+
+ const measure = () => {
+ const availWidth = root.clientWidth;
+ if (!availWidth) {
+ return;
+ }
+ let availHeight: number;
+ if (fill) {
+ availHeight = root.clientHeight;
+ } else {
+ const viewport = root.closest('[data-radix-scroll-area-viewport]') as HTMLElement | null;
+ availHeight = viewport
+ ? Math.max(PANEL_MIN_HEIGHT, viewport.clientHeight - PANEL_VIEWPORT_MARGIN)
+ : PANEL_FALLBACK_MAX_HEIGHT;
+ }
+ if (!availHeight) {
+ return;
+ }
+ // Fullscreen contain-fits the whole diagram; the panel grows its
+ // height to the diagram (auto-height).
+ const next = computeFit(intrinsic, availWidth, availHeight, !fill);
+ setFit((prev) => (prev && prev.fitScale === next.fitScale && prev.panelHeight === next.panelHeight ? prev : next));
+ };
+
+ measure();
+ const observer = new ResizeObserver(measure);
+ observer.observe(root);
+ const viewport = fill ? null : root.closest('[data-radix-scroll-area-viewport]');
+ if (viewport) {
+ observer.observe(viewport);
+ }
+ return () => observer.disconnect();
+ }, [intrinsic, fill]);
+
+ const fitScale = fit?.fitScale ?? 1;
+ const zoomStep = fitScale * ZOOM_STEP_FACTOR;
+ const zoomLabel = `${Math.round(((scale || fitScale) / fitScale) * 100)}%`;
+
+ const rootStyle: CSSProperties | undefined = fill
+ ? undefined
+ : { height: fit?.panelHeight ?? PANEL_MIN_HEIGHT };
+
+ return (
+
+ {fit && (
+
setScale(ref.state.scale)}
+ onTransform={(_ref, state) => setScale(state.scale)}
+ // Fires on actual drag movement (not a bare click); parent dedupes.
+ onPanning={onPan}
+ >
+
+
+
+
+ )}
+
+
+ {actions}
+ {actions &&
}
+
apiRef.current?.zoomOut(zoomStep)}
+ aria-label="Zoom out"
+ >
+
+
+
{zoomLabel}
+
apiRef.current?.zoomIn(zoomStep)}
+ aria-label="Zoom in"
+ >
+
+
+
apiRef.current?.resetTransform()}
+ aria-label="Reset view"
+ >
+
+
+
+
+ );
+};
+
+interface MermaidDiagramProps {
+ code: string;
+ /** Optional override classes for the root container (e.g. to drop the default margin). */
+ className?: string;
+}
+
+/**
+ * Renders a mermaid source block as an interactive diagram (pan/zoom, copy,
+ * export, fullscreen). Used in the right "evidence" panel; inline, the answer
+ * renders a {@link DiagramReferenceChip} that reveals this panel diagram.
+ */
+export const MermaidDiagram = ({
+ code,
+ className,
+}: MermaidDiagramProps) => {
+ const { theme } = useThemeNormalized();
+ const mermaidTheme = theme === 'dark' ? 'dark' : 'default';
+ const pngBackground = mermaidTheme === 'dark' ? '#1e1e1e' : '#ffffff';
+ const { toast } = useToast();
+ const captureEvent = useCaptureEvent();
+
+ const panel = usePanelContext();
+ const chatId = panel?.chatId;
+ const isStreaming = panel?.isStreaming ?? false;
+ const diagramId = useMemo(() => getDiagramId(code), [code]);
+ const diagramType = useMemo(() => getDiagramType(code), [code]);
+
+ const [svg, setSvg] = useState(null);
+ const [renderError, setRenderError] = useState(false);
+ const [isFullscreen, setIsFullscreen] = useState(false);
+ // Keep the hover controls visible while a dropdown is open (the open menu
+ // lives in a portal, so moving to it would otherwise drop the hover state).
+ const [isCopyMenuOpen, setIsCopyMenuOpen] = useState(false);
+ const [isExportMenuOpen, setIsExportMenuOpen] = useState(false);
+ const isAnyMenuOpen = isCopyMenuOpen || isExportMenuOpen;
+
+ // Stable anchor used by "Copy link to diagram": the link targets the inline
+ // diagram reference (`#diagram-`) in the answer, which scrolls into
+ // view and reveals this panel diagram on load.
+ const canonicalAnchorId = useMemo(() => getDiagramAnchorId(code), [code]);
+
+ // Render (debounced) whenever the source or theme changes. A try/catch
+ // drives the fallback: partial or invalid mermaid leaves `svg` null and
+ // sets `renderError`, so we render the raw code instead.
+ useEffect(() => {
+ let cancelled = false;
+ const handle = setTimeout(async () => {
+ try {
+ const result = await renderMermaidToSvg(code, mermaidTheme);
+ if (!cancelled) {
+ setSvg(result);
+ setRenderError(false);
+ }
+ } catch {
+ if (!cancelled) {
+ setSvg(null);
+ setRenderError(true);
+ }
+ }
+ }, 150);
+
+ return () => {
+ cancelled = true;
+ clearTimeout(handle);
+ };
+ }, [code, mermaidTheme]);
+
+ const diagramReady = svg !== null && !renderError;
+
+ // Gates the render-outcome event below to diagrams generated live this
+ // session, so it doesn't fire when a chat is revisited / viewed from history.
+ const observedStreamingRef = useRef(false);
+ useEffect(() => {
+ if (isStreaming) {
+ observedStreamingRef.current = true;
+ }
+ }, [isStreaming]);
+
+ // Report the final render outcome (success vs invalid) once per diagram,
+ // only after streaming settles (so transient mid-stream parse failures
+ // aren't counted). Keyed on `code` so a theme re-render doesn't re-fire.
+ const reportedRenderCodeRef = useRef(null);
+ useEffect(() => {
+ if (!chatId || isStreaming || !observedStreamingRef.current || reportedRenderCodeRef.current === code) {
+ return;
+ }
+ // Wait for the render to reach a terminal state.
+ if (svg === null && !renderError) {
+ return;
+ }
+ reportedRenderCodeRef.current = code;
+ captureEvent('wa_chat_diagram_rendered', {
+ chatId,
+ diagramId,
+ outcome: renderError ? 'error' : 'success',
+ diagramType,
+ });
+ }, [chatId, isStreaming, code, svg, renderError, diagramId, diagramType, captureEvent]);
+
+ // Click-and-drag pan on the inline diagram, reported once per diagram.
+ const hasReportedPanRef = useRef(false);
+ useEffect(() => {
+ hasReportedPanRef.current = false;
+ }, [code]);
+ const onPanInteraction = useCallback(() => {
+ if (hasReportedPanRef.current || !chatId) {
+ return;
+ }
+ hasReportedPanRef.current = true;
+ captureEvent('wa_chat_diagram_panned', { chatId, diagramId });
+ }, [chatId, diagramId, captureEvent]);
+
+ const onCopyLink = useCallback(async () => {
+ if (chatId) {
+ captureEvent('wa_chat_diagram_copied', { chatId, diagramId, format: 'link' });
+ }
+ try {
+ const url = new URL(window.location.href);
+ url.hash = canonicalAnchorId;
+ await navigator.clipboard.writeText(url.toString());
+ toast({ description: '✅ Copied link to diagram' });
+ } catch {
+ toast({ description: '❌ Failed to copy link', variant: 'destructive' });
+ }
+ }, [canonicalAnchorId, toast, chatId, diagramId, captureEvent]);
+
+ const onCopySource = useCallback(async () => {
+ if (chatId) {
+ captureEvent('wa_chat_diagram_copied', { chatId, diagramId, format: 'source' });
+ }
+ try {
+ await navigator.clipboard.writeText(code);
+ toast({ description: '✅ Copied diagram source' });
+ } catch {
+ toast({ description: '❌ Failed to copy source', variant: 'destructive' });
+ }
+ }, [code, toast, chatId, diagramId, captureEvent]);
+
+ const onCopyImage = useCallback(async () => {
+ if (!svg) {
+ return;
+ }
+ if (chatId) {
+ captureEvent('wa_chat_diagram_copied', { chatId, diagramId, format: 'image' });
+ }
+ try {
+ const blob = await svgToPngBlob(svg, pngBackground);
+ if (!blob) {
+ throw new Error('Failed to rasterize diagram');
+ }
+ await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]);
+ toast({ description: '✅ Copied diagram image' });
+ } catch {
+ toast({ description: '❌ Failed to copy image', variant: 'destructive' });
+ }
+ }, [svg, pngBackground, toast, chatId, diagramId, captureEvent]);
+
+ const onExportSvg = useCallback(() => {
+ if (!svg) {
+ return;
+ }
+ if (chatId) {
+ captureEvent('wa_chat_diagram_exported', { chatId, diagramId, format: 'svg' });
+ }
+ triggerDownload(new Blob([svg], { type: 'image/svg+xml;charset=utf-8' }), 'diagram.svg');
+ }, [svg, chatId, diagramId, captureEvent]);
+
+ const onExportPng = useCallback(async () => {
+ if (!svg) {
+ return;
+ }
+ if (chatId) {
+ captureEvent('wa_chat_diagram_exported', { chatId, diagramId, format: 'png' });
+ }
+ const blob = await svgToPngBlob(svg, pngBackground);
+ if (blob) {
+ triggerDownload(blob, 'diagram.png');
+ }
+ }, [svg, pngBackground, chatId, diagramId, captureEvent]);
+
+ // On-hover controls overlaid on the diagram.
+ const actions = (
+ <>
+
+
+
+
+
+
+
+ Copy link to diagram
+ Copy source
+ Copy as image
+
+
+
+
+
+
+
+
+
+ Export as SVG
+ Export as PNG
+
+
+ {
+ if (chatId) {
+ captureEvent('wa_chat_diagram_fullscreen_opened', { chatId, diagramId });
+ }
+ setIsFullscreen(true);
+ }}
+ aria-label="Open fullscreen"
+ >
+
+
+ >
+ );
+
+ return (
+
+ {diagramReady ? (
+
+ ) : renderError ? (
+ // Invalid / mid-stream source: show it as code until it renders cleanly.
+
+ ) : (
+ // Initial render in flight: show a neutral placeholder rather than the source.
+
+
+
+ )}
+
+
+ {/* pt-12 reserves a top strip for the close (X) button so the
+ viewport's hover controls sit clearly below it. */}
+
+ Diagram
+ {svg && (
+
+ )}
+
+
+
+ );
+};
diff --git a/packages/web/src/ee/features/chat/components/chatThread/mermaidSanitize.test.ts b/packages/web/src/ee/features/chat/components/chatThread/mermaidSanitize.test.ts
new file mode 100644
index 000000000..3b9da1440
--- /dev/null
+++ b/packages/web/src/ee/features/chat/components/chatThread/mermaidSanitize.test.ts
@@ -0,0 +1,112 @@
+import { expect, test, describe } from 'vitest';
+import { sanitizeMermaidCode } from './mermaidSanitize';
+
+describe('sanitizeMermaidCode - styling directives', () => {
+ test('strips style, classDef, and linkStyle lines', () => {
+ const input = [
+ 'flowchart TD',
+ ' a --> b',
+ ' style a fill:#f00',
+ ' classDef warning fill:#ff0',
+ ' linkStyle 0 stroke:#0f0',
+ ].join('\n');
+
+ expect(sanitizeMermaidCode(input)).toBe(['flowchart TD', ' a --> b'].join('\n'));
+ });
+
+ test('strips styling directives regardless of leading whitespace', () => {
+ const input = ['flowchart TD', 'style a fill:#f00', '\t\tlinkStyle 0 stroke:#0f0'].join('\n');
+
+ expect(sanitizeMermaidCode(input)).toBe('flowchart TD');
+ });
+
+ test('does not strip node IDs or labels that merely start with a styling word', () => {
+ const input = [
+ 'flowchart TD',
+ ' styleGuide["Style Guide"]',
+ ' a["classDef is a keyword"]',
+ ].join('\n');
+
+ // No line is removed: the keywords are part of an id / label, not a directive.
+ expect(sanitizeMermaidCode(input)).toBe(input);
+ });
+
+ test('leaves classDiagram member definitions untouched', () => {
+ const input = [
+ 'classDiagram',
+ ' class Animal {',
+ ' +int age',
+ ' }',
+ ].join('\n');
+
+ expect(sanitizeMermaidCode(input)).toBe(input);
+ });
+});
+
+describe('sanitizeMermaidCode - hallucinated subgraph opener repair', () => {
+ test('rewrites a bare `subgbox["Label"]` into a valid subgraph opener', () => {
+ const input = ' subgbox["KV v2 Secrets Engine"]';
+
+ expect(sanitizeMermaidCode(input)).toBe(' subgraph subgbox["KV v2 Secrets Engine"]');
+ });
+
+ test('preserves the numeric suffix so repaired ids stay unique', () => {
+ const input = [
+ ' subgbox["One"]',
+ ' subgbox2["Two"]',
+ ' subgbox3["Three"]',
+ ].join('\n');
+
+ expect(sanitizeMermaidCode(input)).toBe(
+ [
+ ' subgraph subgbox["One"]',
+ ' subgraph subgbox2["Two"]',
+ ' subgraph subgbox3["Three"]',
+ ].join('\n'),
+ );
+ });
+
+ test('repairs the `(` label form as well as the `[` form', () => {
+ const input = ' subgbox("Rounded")';
+
+ expect(sanitizeMermaidCode(input)).toBe(' subgraph subgbox("Rounded")');
+ });
+
+ test('leaves an already-valid `subgraph id["Label"]` opener untouched', () => {
+ const input = ' subgraph operations["Version Operations"]';
+
+ expect(sanitizeMermaidCode(input)).toBe(input);
+ });
+
+ test('does not touch `subgbox` when it is not a subgraph opener', () => {
+ // No following `[` or `(`, so it reads as an ordinary token, not an opener.
+ const input = ' a --> subgbox';
+
+ expect(sanitizeMermaidCode(input)).toBe(input);
+ });
+
+ test('repairs openers and strips styling within the same diagram', () => {
+ const input = [
+ 'flowchart TB',
+ ' subgbox["Group A"]',
+ ' a1 --> a2',
+ ' end',
+ ' subgraph two["Group B"]',
+ ' b1 --> b2',
+ ' end',
+ ' style a1 fill:#f00',
+ ].join('\n');
+
+ expect(sanitizeMermaidCode(input)).toBe(
+ [
+ 'flowchart TB',
+ ' subgraph subgbox["Group A"]',
+ ' a1 --> a2',
+ ' end',
+ ' subgraph two["Group B"]',
+ ' b1 --> b2',
+ ' end',
+ ].join('\n'),
+ );
+ });
+});
diff --git a/packages/web/src/ee/features/chat/components/chatThread/mermaidSanitize.ts b/packages/web/src/ee/features/chat/components/chatThread/mermaidSanitize.ts
new file mode 100644
index 000000000..470878326
--- /dev/null
+++ b/packages/web/src/ee/features/chat/components/chatThread/mermaidSanitize.ts
@@ -0,0 +1,23 @@
+// Defensive cleanup applied to model-generated mermaid source before it is
+// handed to `mermaid.parse()` / `mermaid.render()`. Kept as a standalone,
+// dependency-free module so it can be unit tested without pulling in the
+// (heavy, client-only) diagram component.
+
+// Strip model-emitted custom styling so it can't override the auto-applied
+// theme. Line-anchored on the keyword, so it leaves node IDs, labels, and
+// `class X { ... }` member definitions untouched.
+const STYLING_DIRECTIVE_RE = /^\s*(?:style|classDef|linkStyle)\s/;
+
+// The model intermittently hallucinates the keyword `subgbox` (sometimes with a
+// numeric suffix: `subgbox2`, `subgbox3`, ...) in place of a `subgraph `
+// opener, e.g. `subgbox["KV v2 Secrets Engine"]`. This single malformed token
+// fails the entire parse. Rewrite it into a valid opener, reusing the garbled
+// token as the (unique) subgraph id: `subgraph subgbox["KV v2 Secrets Engine"]`.
+const HALLUCINATED_SUBGRAPH_OPENER_RE = /^(\s*)(subgbox\w*)(\s*[[(])/;
+
+export const sanitizeMermaidCode = (code: string): string =>
+ code
+ .split('\n')
+ .filter((line) => !STYLING_DIRECTIVE_RE.test(line))
+ .map((line) => line.replace(HALLUCINATED_SUBGRAPH_OPENER_RE, '$1subgraph $2$3'))
+ .join('\n');
diff --git a/packages/web/src/ee/features/chat/components/chatThread/referencedFileSourceListItem.tsx b/packages/web/src/ee/features/chat/components/chatThread/referencedFileSourceListItem.tsx
index e2e4f68fc..4d7664827 100644
--- a/packages/web/src/ee/features/chat/components/chatThread/referencedFileSourceListItem.tsx
+++ b/packages/web/src/ee/features/chat/components/chatThread/referencedFileSourceListItem.tsx
@@ -125,12 +125,12 @@ const ReferencedFileSourceListItemComponent = ({
}, [references, selectedReference?.id, selectedReference?.range]);
return (
-
+
{/* Sentinel element to scroll to when collapsing a file */}
{/* Sticky header outside the bordered container */}
-
onExpandedChanged(!isExpanded)} />
@@ -149,7 +149,7 @@ const ReferencedFileSourceListItemComponent = ({
{/* Code container */}
{/* @note: don't conditionally render here since we want to maintain state */}
-
diff --git a/packages/web/src/ee/features/chat/components/chatThread/referencedSourcesListView.tsx b/packages/web/src/ee/features/chat/components/chatThread/referencedSourcesListView.tsx
index 9e7438faf..ec69c075f 100644
--- a/packages/web/src/ee/features/chat/components/chatThread/referencedSourcesListView.tsx
+++ b/packages/web/src/ee/features/chat/components/chatThread/referencedSourcesListView.tsx
@@ -4,35 +4,76 @@ import { ScrollArea } from "@/components/ui/scroll-area";
import { ReactCodeMirrorRef } from "@uiw/react-codemirror";
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
import scrollIntoView from 'scroll-into-view-if-needed';
-import { FileReference, FileSource, Reference } from "@/features/chat/types";
+import { FileReference, FileSource } from "@/features/chat/types";
import { tryResolveFileReference } from '@/features/chat/utils';
import { ReferencedFileSourceListItemContainer } from "./referencedFileSourceListItemContainer";
+import { DiagramPanelListItem } from "./diagramPanelListItem";
+import { PanelItem } from "@/ee/features/chat/useExtractPanelItems";
+import { PanelSelection, usePanelContext } from "@/ee/features/chat/panelContext";
import isEqual from 'fast-deep-equal/react';
interface ReferencedSourcesListViewProps {
references: FileReference[];
sources: FileSource[];
index: number;
- hoveredReference?: Reference;
- onHoveredReferenceChanged: (reference?: Reference) => void;
- selectedReference?: Reference;
- onSelectedReferenceChanged: (reference?: Reference) => void;
style: React.CSSProperties;
+ orderedItems?: PanelItem[];
+ selected?: PanelSelection;
+ hovered?: PanelSelection;
}
const ReferencedSourcesListViewComponent = ({
references,
sources,
index,
- hoveredReference,
- selectedReference,
style,
- onHoveredReferenceChanged,
- onSelectedReferenceChanged,
+ orderedItems = [],
+ selected,
+ hovered,
}: ReferencedSourcesListViewProps) => {
+ const panel = usePanelContext();
+ const noop = useCallback(() => {}, []);
+ // Reference selection/hover is driven through the unified panel context; the
+ // file source items still call these the same way (e.g. from the CodeMirror
+ // reference-highlight extension).
+ const onSelectedReferenceChanged = panel?.setSelectedReference ?? noop;
+ const onHoveredReferenceChanged = panel?.setHoveredReference ?? noop;
+
+ const selectedReference = selected?.kind === 'reference' ? selected.reference : undefined;
+ const hoveredReference = hovered?.kind === 'reference' ? hovered.reference : undefined;
+ const selectedDiagramId = selected?.kind === 'diagram' ? selected.diagramId : undefined;
+ const hoveredDiagramId = hovered?.kind === 'diagram' ? hovered.diagramId : undefined;
+
const scrollAreaRef = useRef
(null);
const editorRefsMap = useRef>(new Map());
const [collapsedFileIds, setCollapsedFileIds] = useState([]);
+ // Diagrams render expanded by default (the panel is the canonical view);
+ // track the ids the user has explicitly collapsed, mirroring collapsedFileIds.
+ const [collapsedDiagramIds, setCollapsedDiagramIds] = useState([]);
+ // Transient highlight applied when a diagram is revealed from the answer.
+ const [highlightedDiagramId, setHighlightedDiagramId] = useState(undefined);
+
+ // When a diagram is revealed from the answer, ensure it is expanded, scroll
+ // it into view, and briefly highlight it.
+ useEffect(() => {
+ if (!selectedDiagramId) {
+ return;
+ }
+ setCollapsedDiagramIds((prev) => prev.filter((id) => id !== selectedDiagramId));
+ const element = document.getElementById(`diagram-panel-${selectedDiagramId}`);
+ if (element) {
+ scrollIntoView(element, { scrollMode: 'if-needed', block: 'center', behavior: 'smooth' });
+ }
+ setHighlightedDiagramId(selectedDiagramId);
+ const timeout = window.setTimeout(() => setHighlightedDiagramId(undefined), 2000);
+ return () => window.clearTimeout(timeout);
+ }, [selectedDiagramId]);
+
+ const onToggleDiagram = useCallback((diagramId: string) => {
+ setCollapsedDiagramIds((prev) => (
+ prev.includes(diagramId) ? prev.filter((id) => id !== diagramId) : [...prev, diagramId]
+ ));
+ }, []);
const getFileId = useCallback((fileSource: FileSource) => {
// @note: we include the index to ensure that the file id is unique
@@ -174,10 +215,10 @@ const ReferencedSourcesListViewComponent = ({
}
}, []);
- if (sources.length === 0) {
+ if (orderedItems.length === 0) {
return (
- No file references found
+ No references found
);
}
@@ -187,8 +228,24 @@ const ReferencedSourcesListViewComponent = ({
ref={scrollAreaRef}
style={style}
>
-
- {sources.map((fileSource) => {
+
+ {orderedItems.map((item) => {
+ if (item.kind === 'diagram') {
+ return (
+
onToggleDiagram(item.diagram.id)}
+ onJumpToInline={() => panel?.jumpToInlineDiagram(item.diagram.id)}
+ />
+ );
+ }
+
+ const fileSource = item.source;
const fileId = getFileId(fileSource);
const referencesInFile = referencesGroupedByFile.get(fileId) || [];
const hoveredReferenceInFile = referencesInFile.some(r => r.id === hoveredReference?.id) ? hoveredReference : undefined;
diff --git a/packages/web/src/ee/features/chat/diagramUtils.ts b/packages/web/src/ee/features/chat/diagramUtils.ts
new file mode 100644
index 000000000..bc4915ff6
--- /dev/null
+++ b/packages/web/src/ee/features/chat/diagramUtils.ts
@@ -0,0 +1,77 @@
+// Matches fenced ```mermaid code blocks in an answer's markdown.
+export const MERMAID_BLOCK_REGEX = /```mermaid\s*\n([\s\S]*?)```/g;
+
+// Stable, deterministic hash of the diagram source so a diagram keeps the same
+// id across reloads (the source is persisted in the message) and so the inline
+// and right-panel instances of the same diagram resolve to the same id.
+const hashString = (value: string): string => {
+ let hash = 0;
+ for (let i = 0; i < value.length; i++) {
+ hash = (hash * 31 + value.charCodeAt(i)) | 0;
+ }
+ return Math.abs(hash).toString(36);
+};
+
+export const getDiagramId = (code: string): string => hashString(code.trim());
+
+// Canonical DOM id / deep-link anchor for the inline instance of a diagram.
+export const getDiagramAnchorId = (code: string): string => `diagram-${getDiagramId(code)}`;
+
+const DIAGRAM_TITLE_MAX_LENGTH = 60;
+
+// Extracts the human-readable title the model assigned to a diagram via a
+// mermaid YAML frontmatter block at the top of the source, e.g.
+//
+// ---
+// title: Authentication Flow
+// ---
+// flowchart TD
+// ...
+//
+// Tolerant of surrounding quotes and other frontmatter keys (e.g. `config:`).
+// Returns undefined when no usable title is present so callers can fall back
+// to a generic "Diagram N" label.
+export const getDiagramTitle = (code: string): string | undefined => {
+ const frontmatterMatch = /^\s*---\s*\n([\s\S]*?)\n---/.exec(code);
+ if (!frontmatterMatch) {
+ return undefined;
+ }
+
+ const titleMatch = /^[ \t]*title:[ \t]*(.+?)[ \t]*$/m.exec(frontmatterMatch[1]);
+ if (!titleMatch) {
+ return undefined;
+ }
+
+ // Strip a single pair of surrounding quotes, if present.
+ const title = titleMatch[1].replace(/^(['"])(.*)\1$/, '$2').trim();
+ if (!title) {
+ return undefined;
+ }
+
+ return title.length > DIAGRAM_TITLE_MAX_LENGTH
+ ? `${title.slice(0, DIAGRAM_TITLE_MAX_LENGTH - 1).trimEnd()}…`
+ : title;
+};
+
+// Best-effort detection of the mermaid diagram type (e.g. 'flowchart',
+// 'sequenceDiagram', 'gantt') for analytics. Returns the first keyword of the
+// first meaningful line, skipping frontmatter and `%%` comments/directives.
+export const getDiagramType = (code: string): string | undefined => {
+ let body = code.trim();
+
+ const frontmatterMatch = /^\s*---\s*\n[\s\S]*?\n---\s*\n?/.exec(body);
+ if (frontmatterMatch) {
+ body = body.slice(frontmatterMatch[0].length);
+ }
+
+ for (const rawLine of body.split('\n')) {
+ const line = rawLine.trim();
+ if (!line || line.startsWith('%%')) {
+ continue;
+ }
+ const keyword = line.split(/[\s({:;]/)[0];
+ return keyword || undefined;
+ }
+
+ return undefined;
+};
diff --git a/packages/web/src/ee/features/chat/panelContext.tsx b/packages/web/src/ee/features/chat/panelContext.tsx
new file mode 100644
index 000000000..2f8492742
--- /dev/null
+++ b/packages/web/src/ee/features/chat/panelContext.tsx
@@ -0,0 +1,41 @@
+'use client';
+
+import { createContext, useContext } from "react";
+import { Reference } from "@/features/chat/types";
+
+// A selectable / hoverable entry that appears in the answer and is mirrored in
+// the right "evidence" panel. Unifies file-reference citations and diagrams so
+// the panel has a single selection/hover model. New panel item types plug in
+// here as additional members.
+export type PanelSelection =
+ | { kind: 'reference'; reference: Reference }
+ | { kind: 'diagram'; diagramId: string };
+
+export interface PanelContextValue {
+ // Id of the chat these items belong to, used for analytics.
+ chatId: string;
+ // Whether the answer is still streaming. A diagram chip whose source has not
+ // yet resolved to a (closed) panel diagram while streaming is the one
+ // currently being written, so it shows a shimmer.
+ isStreaming: boolean;
+
+ // Inline file-reference citations drive selection/hover through these.
+ setSelectedReference: (reference?: Reference) => void;
+ setHoveredReference: (reference?: Reference) => void;
+
+ // Inline diagram chips drive selection/hover through these. `revealDiagram`
+ // re-triggers even when the same diagram is reselected, so re-clicking a
+ // chip re-scrolls its panel mirror into view.
+ revealDiagram: (diagramId: string) => void;
+ setHoveredDiagram: (diagramId?: string) => void;
+
+ // Zero-based position of a diagram in order of appearance, used as a
+ // labeling fallback ("Diagram N"). Returns -1 if unknown.
+ getDiagramIndex: (diagramId: string) => number;
+ // Scroll the inline chip for a diagram into view (panel -> answer).
+ jumpToInlineDiagram: (diagramId: string) => void;
+}
+
+export const PanelContext = createContext(null);
+
+export const usePanelContext = () => useContext(PanelContext);
diff --git a/packages/web/src/ee/features/chat/useExtractPanelItems.ts b/packages/web/src/ee/features/chat/useExtractPanelItems.ts
new file mode 100644
index 000000000..434ba7215
--- /dev/null
+++ b/packages/web/src/ee/features/chat/useExtractPanelItems.ts
@@ -0,0 +1,114 @@
+'use client';
+
+import { TextUIPart } from "ai";
+import { useMemo } from "react";
+import { FileReference, FileSource, Source } from "@/features/chat/types";
+import { FILE_REFERENCE_REGEX } from "@/features/chat/constants";
+import { createFileReference, tryResolveFileReference } from "@/features/chat/utils";
+import { MERMAID_BLOCK_REGEX, getDiagramId } from "./diagramUtils";
+
+export interface ExtractedDiagram {
+ id: string;
+ code: string;
+}
+
+// An ordered entry in the right panel: either a referenced file source or a
+// diagram, interleaved by their order of appearance in the answer.
+export type PanelItem =
+ | { kind: 'source'; source: FileSource }
+ | { kind: 'diagram'; diagram: ExtractedDiagram; diagramIndex: number };
+
+export interface ExtractedPanelItems {
+ // De-duplicated diagrams, in order of appearance.
+ diagrams: ExtractedDiagram[];
+ // File sources cited by the answer that resolve against `sources`, deduped.
+ referencedFileSources: FileSource[];
+ // `referencedFileSources` and `diagrams` interleaved by order of appearance.
+ orderedItems: PanelItem[];
+}
+
+/**
+ * Single-pass extraction of everything the right "evidence" panel renders from
+ * an answer's text: the diagrams the model authored and the file sources it
+ * cited, interleaved by order of appearance. Consolidates what were previously
+ * three separate scans (diagram extraction, referenced-source resolution, and
+ * the interleaving scan).
+ *
+ * @note The diagram id is the content hash (`getDiagramId`); keep it that way —
+ * the diagram PostHog events are keyed on it and rely on it being stable across
+ * reloads.
+ */
+export const useExtractPanelItems = (
+ part: TextUIPart | undefined,
+ references: FileReference[],
+ sources: Source[],
+): ExtractedPanelItems => {
+ return useMemo(() => {
+ const text = part?.text ?? '';
+
+ const fileSources = sources.filter((source): source is FileSource => source.type === 'file');
+
+ // The file sources actually cited by the answer, de-duplicated.
+ const referencedFileSources = references
+ .filter((reference) => reference.type === 'file')
+ .map((reference) => tryResolveFileReference(reference, fileSources))
+ .filter((file): file is FileSource => file !== undefined)
+ .filter((file, index, self) =>
+ index === self.findIndex((other) =>
+ other.path === file.path
+ && other.repo === file.repo
+ && other.revision === file.revision
+ )
+ );
+
+ const diagrams: ExtractedDiagram[] = [];
+ const orderedItems: PanelItem[] = [];
+ const seenSources = new Set();
+ const seenDiagrams = new Set();
+ const sourceKey = (source: FileSource) => `${source.repo}::${source.path}::${source.revision}`;
+
+ const combined = new RegExp(`${MERMAID_BLOCK_REGEX.source}|${FILE_REFERENCE_REGEX.source}`, 'g');
+ let match: RegExpExecArray | null;
+ while ((match = combined.exec(text)) !== null) {
+ // match[1]: mermaid body. match[2..5]: file reference repo/path/start/end.
+ if (match[1] !== undefined) {
+ const code = match[1].trim();
+ if (!code) {
+ continue;
+ }
+ const id = getDiagramId(code);
+ if (seenDiagrams.has(id)) {
+ continue;
+ }
+ seenDiagrams.add(id);
+ const diagram = { id, code };
+ const diagramIndex = diagrams.length;
+ diagrams.push(diagram);
+ orderedItems.push({ kind: 'diagram', diagram, diagramIndex });
+ } else if (match[2] !== undefined && match[3] !== undefined) {
+ const reference = createFileReference({ repo: match[2], path: match[3], startLine: match[4], endLine: match[5] });
+ const source = tryResolveFileReference(reference, referencedFileSources);
+ if (!source) {
+ continue;
+ }
+ const key = sourceKey(source);
+ if (seenSources.has(key)) {
+ continue;
+ }
+ seenSources.add(key);
+ orderedItems.push({ kind: 'source', source });
+ }
+ }
+
+ // Safety net: append any resolved source not matched in the scan.
+ for (const source of referencedFileSources) {
+ const key = sourceKey(source);
+ if (!seenSources.has(key)) {
+ seenSources.add(key);
+ orderedItems.push({ kind: 'source', source });
+ }
+ }
+
+ return { diagrams, referencedFileSources, orderedItems };
+ }, [part, references, sources]);
+};
diff --git a/packages/web/src/lib/posthogEvents.ts b/packages/web/src/lib/posthogEvents.ts
index 3ad283254..b6b84f592 100644
--- a/packages/web/src/lib/posthogEvents.ts
+++ b/packages/web/src/lib/posthogEvents.ts
@@ -335,6 +335,35 @@ export type PosthogEventMap = {
chatId: string,
isExpanded: boolean,
},
+ wa_chat_diagram_rendered: {
+ chatId: string,
+ diagramId: string,
+ outcome: 'success' | 'error',
+ /** Mermaid diagram type (e.g. 'flowchart', 'sequenceDiagram'), if detectable. */
+ diagramType?: string,
+ },
+ wa_chat_diagram_fullscreen_opened: {
+ chatId: string,
+ diagramId: string,
+ },
+ wa_chat_diagram_copied: {
+ chatId: string,
+ diagramId: string,
+ format: 'link' | 'source' | 'image',
+ },
+ wa_chat_diagram_exported: {
+ chatId: string,
+ diagramId: string,
+ format: 'svg' | 'png',
+ },
+ wa_chat_diagram_panned: {
+ chatId: string,
+ diagramId: string,
+ },
+ wa_chat_diagram_reference_clicked: {
+ chatId: string,
+ diagramId: string,
+ },
wa_user_created: {
userId: string,
},
diff --git a/yarn.lock b/yarn.lock
index be48b2e71..ce21f594b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -226,6 +226,16 @@ __metadata:
languageName: node
linkType: hard
+"@antfu/install-pkg@npm:^1.1.0":
+ version: 1.1.0
+ resolution: "@antfu/install-pkg@npm:1.1.0"
+ dependencies:
+ package-manager-detector: "npm:^1.3.0"
+ tinyexec: "npm:^1.0.1"
+ checksum: 10c0/140d5994c76fd3d0e824c88f1ce91b3370e8066a8bc2f5729ae133bf768caa239f7915e29c78f239b7ead253113ace51293e95127fafe2b786b88eb615b3be47
+ languageName: node
+ linkType: hard
+
"@antfu/ni@npm:^0.23.0":
version: 0.23.2
resolution: "@antfu/ni@npm:0.23.2"
@@ -1323,6 +1333,13 @@ __metadata:
languageName: node
linkType: hard
+"@braintree/sanitize-url@npm:^7.1.2":
+ version: 7.1.2
+ resolution: "@braintree/sanitize-url@npm:7.1.2"
+ checksum: 10c0/62f2aa0cf58626e3880b2dc1025c42064b4639abd157ae4e1c35f4c2f5031e9273772046a423979845069c814e27ff818e8e669280dc53585e6f033d5b7a59cb
+ languageName: node
+ linkType: hard
+
"@cfworker/json-schema@npm:^4.0.2":
version: 4.1.1
resolution: "@cfworker/json-schema@npm:4.1.1"
@@ -1330,6 +1347,13 @@ __metadata:
languageName: node
linkType: hard
+"@chevrotain/types@npm:~11.1.2":
+ version: 11.1.2
+ resolution: "@chevrotain/types@npm:11.1.2"
+ checksum: 10c0/c0c4679a3d407df34e18d5adfa7ac599b4a2bfddbf68da6e43678b9b3e16ab911de7766b37b9fc466261c3dead3db1b620e2e344f800fa9f0f381720475eda8f
+ languageName: node
+ linkType: hard
+
"@codemirror/autocomplete@npm:^6.0.0, @codemirror/autocomplete@npm:^6.16.2, @codemirror/autocomplete@npm:^6.3.2, @codemirror/autocomplete@npm:^6.7.1":
version: 6.18.6
resolution: "@codemirror/autocomplete@npm:6.18.6"
@@ -2493,6 +2517,17 @@ __metadata:
languageName: node
linkType: hard
+"@iconify/utils@npm:^3.0.2":
+ version: 3.1.3
+ resolution: "@iconify/utils@npm:3.1.3"
+ dependencies:
+ "@antfu/install-pkg": "npm:^1.1.0"
+ "@iconify/types": "npm:^2.0.0"
+ import-meta-resolve: "npm:^4.2.0"
+ checksum: 10c0/6d01196172ef062a9cd0ee299dff3a405221e65057d6cca9ac9fb4bacd7d78168d54b0837b75d5f163038d770a3a7e4449d68ea2fc7a577c01b9ab51bfd53c98
+ languageName: node
+ linkType: hard
+
"@iizukak/codemirror-lang-wgsl@npm:^0.3.0":
version: 0.3.0
resolution: "@iizukak/codemirror-lang-wgsl@npm:0.3.0"
@@ -3688,6 +3723,15 @@ __metadata:
languageName: node
linkType: hard
+"@mermaid-js/parser@npm:^1.2.0":
+ version: 1.2.0
+ resolution: "@mermaid-js/parser@npm:1.2.0"
+ dependencies:
+ "@chevrotain/types": "npm:~11.1.2"
+ checksum: 10c0/907d41167b23160c88f292b0dd79162d2543445ecf1d784ed09747467705666669f818e3ab91e1182da2127720fb40cb707e6fd56e8a445020fd3e7b8b16f013
+ languageName: node
+ linkType: hard
+
"@modelcontextprotocol/sdk@npm:^1.25.0":
version: 1.27.1
resolution: "@modelcontextprotocol/sdk@npm:1.27.1"
@@ -9445,8 +9489,10 @@ __metadata:
langfuse-vercel: "npm:^3.38.4"
linguist-languages: "npm:^9.3.1"
lucide-react: "npm:^1.7.0"
+ mermaid: "npm:^11.16.0"
micromatch: "npm:^4.0.8"
minidenticons: "npm:^4.2.1"
+ motion: "npm:^12.42.0"
next: "npm:^16.2.6"
next-auth: "npm:^5.0.0-beta.30"
next-navigation-guard: "npm:^0.2.0"
@@ -9474,6 +9520,7 @@ __metadata:
react-markdown: "npm:^10.1.0"
react-resizable-panels: "npm:^2.1.1"
react-scan: "npm:^0.5.3"
+ react-zoom-pan-pinch: "npm:^4.0.3"
recharts: "npm:^2.15.3"
rehype-raw: "npm:^7.0.0"
rehype-sanitize: "npm:^6.0.0"
@@ -9774,6 +9821,13 @@ __metadata:
languageName: node
linkType: hard
+"@types/d3-array@npm:*":
+ version: 3.2.2
+ resolution: "@types/d3-array@npm:3.2.2"
+ checksum: 10c0/6137cb97302f8a4f18ca22c0560c585cfcb823f276b23d89f2c0c005d72697ec13bca671c08e68b4b0cabd622e3f0e91782ee221580d6774074050be96dd7028
+ languageName: node
+ linkType: hard
+
"@types/d3-array@npm:^3.0.3":
version: 3.2.1
resolution: "@types/d3-array@npm:3.2.1"
@@ -9781,6 +9835,31 @@ __metadata:
languageName: node
linkType: hard
+"@types/d3-axis@npm:*":
+ version: 3.0.6
+ resolution: "@types/d3-axis@npm:3.0.6"
+ dependencies:
+ "@types/d3-selection": "npm:*"
+ checksum: 10c0/d756d42360261f44d8eefd0950c5bb0a4f67a46dd92069da3f723ac36a1e8cb2b9ce6347d836ef19d5b8aef725dbcf8fdbbd6cfbff676ca4b0642df2f78b599a
+ languageName: node
+ linkType: hard
+
+"@types/d3-brush@npm:*":
+ version: 3.0.6
+ resolution: "@types/d3-brush@npm:3.0.6"
+ dependencies:
+ "@types/d3-selection": "npm:*"
+ checksum: 10c0/fd6e2ac7657a354f269f6b9c58451ffae9d01b89ccb1eb6367fd36d635d2f1990967215ab498e0c0679ff269429c57fad6a2958b68f4d45bc9f81d81672edc01
+ languageName: node
+ linkType: hard
+
+"@types/d3-chord@npm:*":
+ version: 3.0.6
+ resolution: "@types/d3-chord@npm:3.0.6"
+ checksum: 10c0/c5a25eb5389db01e63faec0c5c2ec7cc41c494e9b3201630b494c4e862a60f1aa83fabbc33a829e7e1403941e3c30d206c741559b14406ac2a4239cfdf4b4c17
+ languageName: node
+ linkType: hard
+
"@types/d3-color@npm:*":
version: 3.1.3
resolution: "@types/d3-color@npm:3.1.3"
@@ -9788,14 +9867,93 @@ __metadata:
languageName: node
linkType: hard
-"@types/d3-ease@npm:^3.0.0":
+"@types/d3-contour@npm:*":
+ version: 3.0.6
+ resolution: "@types/d3-contour@npm:3.0.6"
+ dependencies:
+ "@types/d3-array": "npm:*"
+ "@types/geojson": "npm:*"
+ checksum: 10c0/e7d83e94719af4576ceb5ac7f277c5806f83ba6c3631744ae391cffc3641f09dfa279470b83053cd0b2acd6784e8749c71141d05bdffa63ca58ffb5b31a0f27c
+ languageName: node
+ linkType: hard
+
+"@types/d3-delaunay@npm:*":
+ version: 6.0.4
+ resolution: "@types/d3-delaunay@npm:6.0.4"
+ checksum: 10c0/d154a8864f08c4ea23ecb9bdabcef1c406a25baa8895f0cb08a0ed2799de0d360e597552532ce7086ff0cdffa8f3563f9109d18f0191459d32bb620a36939123
+ languageName: node
+ linkType: hard
+
+"@types/d3-dispatch@npm:*":
+ version: 3.0.7
+ resolution: "@types/d3-dispatch@npm:3.0.7"
+ checksum: 10c0/38c6605ebf0bf0099dfb70eafe0dd4ae8213368b40b8f930b72a909ff2e7259d2bd8a54d100bb5a44eb4b36f4f2a62dcb37f8be59613ca6b507c7a2f910b3145
+ languageName: node
+ linkType: hard
+
+"@types/d3-drag@npm:*":
+ version: 3.0.7
+ resolution: "@types/d3-drag@npm:3.0.7"
+ dependencies:
+ "@types/d3-selection": "npm:*"
+ checksum: 10c0/65e29fa32a87c72d26c44b5e2df3bf15af21cd128386bcc05bcacca255927c0397d0cd7e6062aed5f0abd623490544a9d061c195f5ed9f018fe0b698d99c079d
+ languageName: node
+ linkType: hard
+
+"@types/d3-dsv@npm:*":
+ version: 3.0.7
+ resolution: "@types/d3-dsv@npm:3.0.7"
+ checksum: 10c0/c0f01da862465594c8a28278b51c850af3b4239cc22b14fd1a19d7a98f93d94efa477bf59d8071beb285dca45bf614630811451e18e7c52add3a0abfee0a1871
+ languageName: node
+ linkType: hard
+
+"@types/d3-ease@npm:*, @types/d3-ease@npm:^3.0.0":
version: 3.0.2
resolution: "@types/d3-ease@npm:3.0.2"
checksum: 10c0/aff5a1e572a937ee9bff6465225d7ba27d5e0c976bd9eacdac2e6f10700a7cb0c9ea2597aff6b43a6ed850a3210030870238894a77ec73e309b4a9d0333f099c
languageName: node
linkType: hard
-"@types/d3-interpolate@npm:^3.0.1":
+"@types/d3-fetch@npm:*":
+ version: 3.0.7
+ resolution: "@types/d3-fetch@npm:3.0.7"
+ dependencies:
+ "@types/d3-dsv": "npm:*"
+ checksum: 10c0/3d147efa52a26da1a5d40d4d73e6cebaaa964463c378068062999b93ea3731b27cc429104c21ecbba98c6090e58ef13429db6399238c5e3500162fb3015697a0
+ languageName: node
+ linkType: hard
+
+"@types/d3-force@npm:*":
+ version: 3.0.10
+ resolution: "@types/d3-force@npm:3.0.10"
+ checksum: 10c0/c82b459079a106b50e346c9b79b141f599f2fc4f598985a5211e72c7a2e20d35bd5dc6e91f306b323c8bfa325c02c629b1645f5243f1c6a55bd51bc85cccfa92
+ languageName: node
+ linkType: hard
+
+"@types/d3-format@npm:*":
+ version: 3.0.4
+ resolution: "@types/d3-format@npm:3.0.4"
+ checksum: 10c0/3ac1600bf9061a59a228998f7cd3f29e85cbf522997671ba18d4d84d10a2a1aff4f95aceb143fa9960501c3ec351e113fc75884e6a504ace44dc1744083035ee
+ languageName: node
+ linkType: hard
+
+"@types/d3-geo@npm:*":
+ version: 3.1.0
+ resolution: "@types/d3-geo@npm:3.1.0"
+ dependencies:
+ "@types/geojson": "npm:*"
+ checksum: 10c0/3745a93439038bb5b0b38facf435f7079812921d46406f5d38deaee59e90084ff742443c7ea0a8446df81a0d81eaf622fe7068cf4117a544bd4aa3b2dc182f88
+ languageName: node
+ linkType: hard
+
+"@types/d3-hierarchy@npm:*":
+ version: 3.1.7
+ resolution: "@types/d3-hierarchy@npm:3.1.7"
+ checksum: 10c0/873711737d6b8e7b6f1dda0bcd21294a48f75024909ae510c5d2c21fad2e72032e0958def4d9f68319d3aaac298ad09c49807f8bfc87a145a82693b5208613c7
+ languageName: node
+ linkType: hard
+
+"@types/d3-interpolate@npm:*, @types/d3-interpolate@npm:^3.0.1":
version: 3.0.4
resolution: "@types/d3-interpolate@npm:3.0.4"
dependencies:
@@ -9811,7 +9969,35 @@ __metadata:
languageName: node
linkType: hard
-"@types/d3-scale@npm:^4.0.2":
+"@types/d3-polygon@npm:*":
+ version: 3.0.2
+ resolution: "@types/d3-polygon@npm:3.0.2"
+ checksum: 10c0/f46307bb32b6c2aef8c7624500e0f9b518de8f227ccc10170b869dc43e4c542560f6c8d62e9f087fac45e198d6e4b623e579c0422e34c85baf56717456d3f439
+ languageName: node
+ linkType: hard
+
+"@types/d3-quadtree@npm:*":
+ version: 3.0.6
+ resolution: "@types/d3-quadtree@npm:3.0.6"
+ checksum: 10c0/7eaa0a4d404adc856971c9285e1c4ab17e9135ea669d847d6db7e0066126a28ac751864e7ce99c65d526e130f56754a2e437a1617877098b3bdcc3ef23a23616
+ languageName: node
+ linkType: hard
+
+"@types/d3-random@npm:*":
+ version: 3.0.3
+ resolution: "@types/d3-random@npm:3.0.3"
+ checksum: 10c0/5f4fea40080cd6d4adfee05183d00374e73a10c530276a6455348983dda341003a251def28565a27c25d9cf5296a33e870e397c9d91ff83fb7495a21c96b6882
+ languageName: node
+ linkType: hard
+
+"@types/d3-scale-chromatic@npm:*":
+ version: 3.1.0
+ resolution: "@types/d3-scale-chromatic@npm:3.1.0"
+ checksum: 10c0/93c564e02d2e97a048e18fe8054e4a935335da6ab75a56c3df197beaa87e69122eef0dfbeb7794d4a444a00e52e3123514ee27cec084bd21f6425b7037828cc2
+ languageName: node
+ linkType: hard
+
+"@types/d3-scale@npm:*, @types/d3-scale@npm:^4.0.2":
version: 4.0.9
resolution: "@types/d3-scale@npm:4.0.9"
dependencies:
@@ -9820,6 +10006,22 @@ __metadata:
languageName: node
linkType: hard
+"@types/d3-selection@npm:*":
+ version: 3.0.11
+ resolution: "@types/d3-selection@npm:3.0.11"
+ checksum: 10c0/0c512956c7503ff5def4bb32e0c568cc757b9a2cc400a104fc0f4cfe5e56d83ebde2a97821b6f2cb26a7148079d3b86a2f28e11d68324ed311cf35c2ed980d1d
+ languageName: node
+ linkType: hard
+
+"@types/d3-shape@npm:*":
+ version: 3.1.8
+ resolution: "@types/d3-shape@npm:3.1.8"
+ dependencies:
+ "@types/d3-path": "npm:*"
+ checksum: 10c0/49ec2172b9eb66fc1d036e2a23966216bb972e9af51ddbed134df24bd71aedf207bb1ef81903a119eb4e1f5e927cf44beacaf64c9af86474e5548594b102b574
+ languageName: node
+ linkType: hard
+
"@types/d3-shape@npm:^3.1.0":
version: 3.1.7
resolution: "@types/d3-shape@npm:3.1.7"
@@ -9829,6 +10031,13 @@ __metadata:
languageName: node
linkType: hard
+"@types/d3-time-format@npm:*":
+ version: 4.0.3
+ resolution: "@types/d3-time-format@npm:4.0.3"
+ checksum: 10c0/9ef5e8e2b96b94799b821eed5d61a3d432c7903247966d8ad951b8ce5797fe46554b425cb7888fa5bf604b4663c369d7628c0328ffe80892156671c58d1a7f90
+ languageName: node
+ linkType: hard
+
"@types/d3-time@npm:*, @types/d3-time@npm:^3.0.0":
version: 3.0.4
resolution: "@types/d3-time@npm:3.0.4"
@@ -9836,13 +10045,70 @@ __metadata:
languageName: node
linkType: hard
-"@types/d3-timer@npm:^3.0.0":
+"@types/d3-timer@npm:*, @types/d3-timer@npm:^3.0.0":
version: 3.0.2
resolution: "@types/d3-timer@npm:3.0.2"
checksum: 10c0/c644dd9571fcc62b1aa12c03bcad40571553020feeb5811f1d8a937ac1e65b8a04b759b4873aef610e28b8714ac71c9885a4d6c127a048d95118f7e5b506d9e1
languageName: node
linkType: hard
+"@types/d3-transition@npm:*":
+ version: 3.0.9
+ resolution: "@types/d3-transition@npm:3.0.9"
+ dependencies:
+ "@types/d3-selection": "npm:*"
+ checksum: 10c0/4f68b9df7ac745b3491216c54203cbbfa0f117ae4c60e2609cdef2db963582152035407fdff995b10ee383bae2f05b7743493f48e1b8e46df54faa836a8fb7b5
+ languageName: node
+ linkType: hard
+
+"@types/d3-zoom@npm:*":
+ version: 3.0.8
+ resolution: "@types/d3-zoom@npm:3.0.8"
+ dependencies:
+ "@types/d3-interpolate": "npm:*"
+ "@types/d3-selection": "npm:*"
+ checksum: 10c0/1dbdbcafddcae12efb5beb6948546963f29599e18bc7f2a91fb69cc617c2299a65354f2d47e282dfb86fec0968406cd4fb7f76ba2d2fb67baa8e8d146eb4a547
+ languageName: node
+ linkType: hard
+
+"@types/d3@npm:^7.4.3":
+ version: 7.4.3
+ resolution: "@types/d3@npm:7.4.3"
+ dependencies:
+ "@types/d3-array": "npm:*"
+ "@types/d3-axis": "npm:*"
+ "@types/d3-brush": "npm:*"
+ "@types/d3-chord": "npm:*"
+ "@types/d3-color": "npm:*"
+ "@types/d3-contour": "npm:*"
+ "@types/d3-delaunay": "npm:*"
+ "@types/d3-dispatch": "npm:*"
+ "@types/d3-drag": "npm:*"
+ "@types/d3-dsv": "npm:*"
+ "@types/d3-ease": "npm:*"
+ "@types/d3-fetch": "npm:*"
+ "@types/d3-force": "npm:*"
+ "@types/d3-format": "npm:*"
+ "@types/d3-geo": "npm:*"
+ "@types/d3-hierarchy": "npm:*"
+ "@types/d3-interpolate": "npm:*"
+ "@types/d3-path": "npm:*"
+ "@types/d3-polygon": "npm:*"
+ "@types/d3-quadtree": "npm:*"
+ "@types/d3-random": "npm:*"
+ "@types/d3-scale": "npm:*"
+ "@types/d3-scale-chromatic": "npm:*"
+ "@types/d3-selection": "npm:*"
+ "@types/d3-shape": "npm:*"
+ "@types/d3-time": "npm:*"
+ "@types/d3-time-format": "npm:*"
+ "@types/d3-timer": "npm:*"
+ "@types/d3-transition": "npm:*"
+ "@types/d3-zoom": "npm:*"
+ checksum: 10c0/a9c6d65b13ef3b42c87f2a89ea63a6d5640221869f97d0657b0cb2f1dac96a0f164bf5605643c0794e0de3aa2bf05df198519aaf15d24ca135eb0e8bd8a9d879
+ languageName: node
+ linkType: hard
+
"@types/debug@npm:^4.0.0":
version: 4.1.12
resolution: "@types/debug@npm:4.1.12"
@@ -9905,6 +10171,13 @@ __metadata:
languageName: node
linkType: hard
+"@types/geojson@npm:*":
+ version: 7946.0.16
+ resolution: "@types/geojson@npm:7946.0.16"
+ checksum: 10c0/1ff24a288bd5860b766b073ead337d31d73bdc715e5b50a2cee5cb0af57a1ed02cc04ef295f5fa68dc40fe3e4f104dd31282b2b818a5ba3231bc1001ba084e3c
+ languageName: node
+ linkType: hard
+
"@types/glob-to-regexp@npm:^0.4.4":
version: 0.4.4
resolution: "@types/glob-to-regexp@npm:0.4.4"
@@ -10748,6 +11021,21 @@ __metadata:
languageName: node
linkType: hard
+"@upsetjs/venn.js@npm:^2.0.0":
+ version: 2.0.0
+ resolution: "@upsetjs/venn.js@npm:2.0.0"
+ dependencies:
+ d3-selection: "npm:^3.0.0"
+ d3-transition: "npm:^3.0.1"
+ dependenciesMeta:
+ d3-selection:
+ optional: true
+ d3-transition:
+ optional: true
+ checksum: 10c0/b12014d94708ab4df7f5a4b6205c6f23ff235cca2ffe91df3314862b109b826e52f9020c2a2f7527d3712d21c578d6db9cdb60ce46a528739cc18e58d111f724
+ languageName: node
+ linkType: hard
+
"@vercel/oidc@npm:3.1.0":
version: 3.1.0
resolution: "@vercel/oidc@npm:3.1.0"
@@ -12276,6 +12564,13 @@ __metadata:
languageName: node
linkType: hard
+"commander@npm:7":
+ version: 7.2.0
+ resolution: "commander@npm:7.2.0"
+ checksum: 10c0/8d690ff13b0356df7e0ebbe6c59b4712f754f4b724d4f473d3cc5b3fdcf978e3a5dc3078717858a2ceb50b0f84d0660a7f22a96cdc50fb877d0c9bb31593d23a
+ languageName: node
+ linkType: hard
+
"commander@npm:^13.0.0":
version: 13.1.0
resolution: "commander@npm:13.1.0"
@@ -12304,6 +12599,13 @@ __metadata:
languageName: node
linkType: hard
+"commander@npm:^8.3.0":
+ version: 8.3.0
+ resolution: "commander@npm:8.3.0"
+ checksum: 10c0/8b043bb8322ea1c39664a1598a95e0495bfe4ca2fad0d84a92d7d1d8d213e2a155b441d2470c8e08de7c4a28cf2bc6e169211c49e1b21d9f7edc6ae4d9356060
+ languageName: node
+ linkType: hard
+
"commondir@npm:^1.0.1":
version: 1.0.1
resolution: "commondir@npm:1.0.1"
@@ -12436,6 +12738,24 @@ __metadata:
languageName: node
linkType: hard
+"cose-base@npm:^1.0.0":
+ version: 1.0.3
+ resolution: "cose-base@npm:1.0.3"
+ dependencies:
+ layout-base: "npm:^1.0.0"
+ checksum: 10c0/a6e400b1d101393d6af0967c1353355777c1106c40417c5acaef6ca8bdda41e2fc9398f466d6c85be30290943ad631f2590569f67b3fd5368a0d8318946bd24f
+ languageName: node
+ linkType: hard
+
+"cose-base@npm:^2.2.0":
+ version: 2.2.0
+ resolution: "cose-base@npm:2.2.0"
+ dependencies:
+ layout-base: "npm:^2.0.0"
+ checksum: 10c0/14b9f8100ac322a00777ffb1daeb3321af368bbc9cabe3103943361273baee2003202ffe38e4ab770960b600214224e9c196195a78d589521540aa694df7cdec
+ languageName: node
+ linkType: hard
+
"crelt@npm:^1.0.5":
version: 1.0.6
resolution: "crelt@npm:1.0.6"
@@ -12540,7 +12860,45 @@ __metadata:
languageName: node
linkType: hard
-"d3-array@npm:2 - 3, d3-array@npm:2.10.0 - 3, d3-array@npm:^3.1.6":
+"cytoscape-cose-bilkent@npm:^4.1.0":
+ version: 4.1.0
+ resolution: "cytoscape-cose-bilkent@npm:4.1.0"
+ dependencies:
+ cose-base: "npm:^1.0.0"
+ peerDependencies:
+ cytoscape: ^3.2.0
+ checksum: 10c0/5e2480ddba9da1a68e700ed2c674cbfd51e9efdbd55788f1971a68de4eb30708e3b3a5e808bf5628f7a258680406bbe6586d87a9133e02a9bdc1ab1a92f512f2
+ languageName: node
+ linkType: hard
+
+"cytoscape-fcose@npm:^2.2.0":
+ version: 2.2.0
+ resolution: "cytoscape-fcose@npm:2.2.0"
+ dependencies:
+ cose-base: "npm:^2.2.0"
+ peerDependencies:
+ cytoscape: ^3.2.0
+ checksum: 10c0/ce472c9f85b9057e75c5685396f8e1f2468895e71b184913e05ad56dcf3092618fe59a1054f29cb0995051ba8ebe566ad0dd49a58d62845145624bd60cd44917
+ languageName: node
+ linkType: hard
+
+"cytoscape@npm:^3.33.3":
+ version: 3.34.0
+ resolution: "cytoscape@npm:3.34.0"
+ checksum: 10c0/cb17b3dcacb9492f058cff653debf7b5a713e7532a9866cc72ae79d207757e8a9b68430874fe986f2a6404a8161b4fb5c1a2f1ca9ac5b1a21e316f57ed9d1243
+ languageName: node
+ linkType: hard
+
+"d3-array@npm:1 - 2":
+ version: 2.12.1
+ resolution: "d3-array@npm:2.12.1"
+ dependencies:
+ internmap: "npm:^1.0.0"
+ checksum: 10c0/7eca10427a9f113a4ca6a0f7301127cab26043fd5e362631ef5a0edd1c4b2dd70c56ed317566700c31e4a6d88b55f3951aaba192291817f243b730cb2352882e
+ languageName: node
+ linkType: hard
+
+"d3-array@npm:2 - 3, d3-array@npm:2.10.0 - 3, d3-array@npm:2.5.0 - 3, d3-array@npm:3, d3-array@npm:^3.1.6, d3-array@npm:^3.2.0":
version: 3.2.4
resolution: "d3-array@npm:3.2.4"
dependencies:
@@ -12549,20 +12907,125 @@ __metadata:
languageName: node
linkType: hard
-"d3-color@npm:1 - 3":
+"d3-axis@npm:3":
+ version: 3.0.0
+ resolution: "d3-axis@npm:3.0.0"
+ checksum: 10c0/a271e70ba1966daa5aaf6a7f959ceca3e12997b43297e757c7b945db2e1ead3c6ee226f2abcfa22abbd4e2e28bd2b71a0911794c4e5b911bbba271328a582c78
+ languageName: node
+ linkType: hard
+
+"d3-brush@npm:3":
+ version: 3.0.0
+ resolution: "d3-brush@npm:3.0.0"
+ dependencies:
+ d3-dispatch: "npm:1 - 3"
+ d3-drag: "npm:2 - 3"
+ d3-interpolate: "npm:1 - 3"
+ d3-selection: "npm:3"
+ d3-transition: "npm:3"
+ checksum: 10c0/07baf00334c576da2f68a91fc0da5732c3a5fa19bd3d7aed7fd24d1d674a773f71a93e9687c154176f7246946194d77c48c2d8fed757f5dcb1a4740067ec50a8
+ languageName: node
+ linkType: hard
+
+"d3-chord@npm:3":
+ version: 3.0.1
+ resolution: "d3-chord@npm:3.0.1"
+ dependencies:
+ d3-path: "npm:1 - 3"
+ checksum: 10c0/baa6013914af3f4fe1521f0d16de31a38eb8a71d08ff1dec4741f6f45a828661e5cd3935e39bd14e3032bdc78206c283ca37411da21d46ec3cfc520be6e7a7ce
+ languageName: node
+ linkType: hard
+
+"d3-color@npm:1 - 3, d3-color@npm:3":
version: 3.1.0
resolution: "d3-color@npm:3.1.0"
checksum: 10c0/a4e20e1115fa696fce041fbe13fbc80dc4c19150fa72027a7c128ade980bc0eeeba4bcf28c9e21f0bce0e0dbfe7ca5869ef67746541dcfda053e4802ad19783c
languageName: node
linkType: hard
-"d3-ease@npm:^3.0.1":
+"d3-contour@npm:4":
+ version: 4.0.2
+ resolution: "d3-contour@npm:4.0.2"
+ dependencies:
+ d3-array: "npm:^3.2.0"
+ checksum: 10c0/98bc5fbed6009e08707434a952076f39f1cd6ed8b9288253cc3e6a3286e4e80c63c62d84954b20e64bf6e4ededcc69add54d3db25e990784a59c04edd3449032
+ languageName: node
+ linkType: hard
+
+"d3-delaunay@npm:6":
+ version: 6.0.4
+ resolution: "d3-delaunay@npm:6.0.4"
+ dependencies:
+ delaunator: "npm:5"
+ checksum: 10c0/57c3aecd2525664b07c4c292aa11cf49b2752c0cf3f5257f752999399fe3c592de2d418644d79df1f255471eec8057a9cc0c3062ed7128cb3348c45f69597754
+ languageName: node
+ linkType: hard
+
+"d3-dispatch@npm:1 - 3, d3-dispatch@npm:3":
+ version: 3.0.1
+ resolution: "d3-dispatch@npm:3.0.1"
+ checksum: 10c0/6eca77008ce2dc33380e45d4410c67d150941df7ab45b91d116dbe6d0a3092c0f6ac184dd4602c796dc9e790222bad3ff7142025f5fd22694efe088d1d941753
+ languageName: node
+ linkType: hard
+
+"d3-drag@npm:2 - 3, d3-drag@npm:3":
+ version: 3.0.0
+ resolution: "d3-drag@npm:3.0.0"
+ dependencies:
+ d3-dispatch: "npm:1 - 3"
+ d3-selection: "npm:3"
+ checksum: 10c0/d2556e8dc720741a443b595a30af403dd60642dfd938d44d6e9bfc4c71a962142f9a028c56b61f8b4790b65a34acad177d1263d66f103c3c527767b0926ef5aa
+ languageName: node
+ linkType: hard
+
+"d3-dsv@npm:1 - 3, d3-dsv@npm:3":
+ version: 3.0.1
+ resolution: "d3-dsv@npm:3.0.1"
+ dependencies:
+ commander: "npm:7"
+ iconv-lite: "npm:0.6"
+ rw: "npm:1"
+ bin:
+ csv2json: bin/dsv2json.js
+ csv2tsv: bin/dsv2dsv.js
+ dsv2dsv: bin/dsv2dsv.js
+ dsv2json: bin/dsv2json.js
+ json2csv: bin/json2dsv.js
+ json2dsv: bin/json2dsv.js
+ json2tsv: bin/json2dsv.js
+ tsv2csv: bin/dsv2dsv.js
+ tsv2json: bin/dsv2json.js
+ checksum: 10c0/10e6af9e331950ed258f34ab49ac1b7060128ef81dcf32afc790bd1f7e8c3cc2aac7f5f875250a83f21f39bb5925fbd0872bb209f8aca32b3b77d32bab8a65ab
+ languageName: node
+ linkType: hard
+
+"d3-ease@npm:1 - 3, d3-ease@npm:3, d3-ease@npm:^3.0.1":
version: 3.0.1
resolution: "d3-ease@npm:3.0.1"
checksum: 10c0/fec8ef826c0cc35cda3092c6841e07672868b1839fcaf556e19266a3a37e6bc7977d8298c0fcb9885e7799bfdcef7db1baaba9cd4dcf4bc5e952cf78574a88b0
languageName: node
linkType: hard
+"d3-fetch@npm:3":
+ version: 3.0.1
+ resolution: "d3-fetch@npm:3.0.1"
+ dependencies:
+ d3-dsv: "npm:1 - 3"
+ checksum: 10c0/4f467a79bf290395ac0cbb5f7562483f6a18668adc4c8eb84c9d3eff048b6f6d3b6f55079ba1ebf1908dabe000c941d46be447f8d78453b2dad5fb59fb6aa93b
+ languageName: node
+ linkType: hard
+
+"d3-force@npm:3":
+ version: 3.0.0
+ resolution: "d3-force@npm:3.0.0"
+ dependencies:
+ d3-dispatch: "npm:1 - 3"
+ d3-quadtree: "npm:1 - 3"
+ d3-timer: "npm:1 - 3"
+ checksum: 10c0/220a16a1a1ac62ba56df61028896e4b52be89c81040d20229c876efc8852191482c233f8a52bb5a4e0875c321b8e5cb6413ef3dfa4d8fe79eeb7d52c587f52cf
+ languageName: node
+ linkType: hard
+
"d3-format@npm:1 - 3":
version: 3.1.0
resolution: "d3-format@npm:3.1.0"
@@ -12570,7 +13033,30 @@ __metadata:
languageName: node
linkType: hard
-"d3-interpolate@npm:1.2.0 - 3, d3-interpolate@npm:^3.0.1":
+"d3-format@npm:3":
+ version: 3.1.2
+ resolution: "d3-format@npm:3.1.2"
+ checksum: 10c0/0de452ae07585238e7f01607a7e0066665c34609652188b6ac7dc9f424f69465a425e07d16d79bd0e5955202ac7f241c66d0c76f68a79fc6f4857c94cf420652
+ languageName: node
+ linkType: hard
+
+"d3-geo@npm:3":
+ version: 3.1.1
+ resolution: "d3-geo@npm:3.1.1"
+ dependencies:
+ d3-array: "npm:2.5.0 - 3"
+ checksum: 10c0/d32270dd2dc8ac3ea63e8805d63239c4c8ec6c0d339d73b5e5a30a87f8f54db22a78fb434369799465eae169503b25f9a107c642c8a16c32a3285bc0e6d8e8c1
+ languageName: node
+ linkType: hard
+
+"d3-hierarchy@npm:3":
+ version: 3.1.2
+ resolution: "d3-hierarchy@npm:3.1.2"
+ checksum: 10c0/6dcdb480539644aa7fc0d72dfc7b03f99dfbcdf02714044e8c708577e0d5981deb9d3e99bbbb2d26422b55bcc342ac89a0fa2ea6c9d7302e2fc0951dd96f89cf
+ languageName: node
+ linkType: hard
+
+"d3-interpolate@npm:1 - 3, d3-interpolate@npm:1.2.0 - 3, d3-interpolate@npm:3, d3-interpolate@npm:^3.0.1":
version: 3.0.1
resolution: "d3-interpolate@npm:3.0.1"
dependencies:
@@ -12579,14 +13065,62 @@ __metadata:
languageName: node
linkType: hard
-"d3-path@npm:^3.1.0":
+"d3-path@npm:1":
+ version: 1.0.9
+ resolution: "d3-path@npm:1.0.9"
+ checksum: 10c0/e35e84df5abc18091f585725b8235e1fa97efc287571585427d3a3597301e6c506dea56b11dfb3c06ca5858b3eb7f02c1bf4f6a716aa9eade01c41b92d497eb5
+ languageName: node
+ linkType: hard
+
+"d3-path@npm:1 - 3, d3-path@npm:3, d3-path@npm:^3.1.0":
version: 3.1.0
resolution: "d3-path@npm:3.1.0"
checksum: 10c0/dc1d58ec87fa8319bd240cf7689995111a124b141428354e9637aa83059eb12e681f77187e0ada5dedfce346f7e3d1f903467ceb41b379bfd01cd8e31721f5da
languageName: node
linkType: hard
-"d3-scale@npm:^4.0.2":
+"d3-polygon@npm:3":
+ version: 3.0.1
+ resolution: "d3-polygon@npm:3.0.1"
+ checksum: 10c0/e236aa7f33efa9a4072907af7dc119f85b150a0716759d4fe5f12f62573018264a6cbde8617fbfa6944a7ae48c1c0c8d3f39ae72e11f66dd471e9b5e668385df
+ languageName: node
+ linkType: hard
+
+"d3-quadtree@npm:1 - 3, d3-quadtree@npm:3":
+ version: 3.0.1
+ resolution: "d3-quadtree@npm:3.0.1"
+ checksum: 10c0/18302d2548bfecaef788152397edec95a76400fd97d9d7f42a089ceb68d910f685c96579d74e3712d57477ed042b056881b47cd836a521de683c66f47ce89090
+ languageName: node
+ linkType: hard
+
+"d3-random@npm:3":
+ version: 3.0.1
+ resolution: "d3-random@npm:3.0.1"
+ checksum: 10c0/987a1a1bcbf26e6cf01fd89d5a265b463b2cea93560fc17d9b1c45e8ed6ff2db5924601bcceb808de24c94133f000039eb7fa1c469a7a844ccbf1170cbb25b41
+ languageName: node
+ linkType: hard
+
+"d3-sankey@npm:^0.12.3":
+ version: 0.12.3
+ resolution: "d3-sankey@npm:0.12.3"
+ dependencies:
+ d3-array: "npm:1 - 2"
+ d3-shape: "npm:^1.2.0"
+ checksum: 10c0/261debb01a13269f6fc53b9ebaef174a015d5ad646242c23995bf514498829ab8b8f920a7873724a7494288b46bea3ce7ebc5a920b745bc8ae4caa5885cf5204
+ languageName: node
+ linkType: hard
+
+"d3-scale-chromatic@npm:3":
+ version: 3.1.0
+ resolution: "d3-scale-chromatic@npm:3.1.0"
+ dependencies:
+ d3-color: "npm:1 - 3"
+ d3-interpolate: "npm:1 - 3"
+ checksum: 10c0/9a3f4671ab0b971f4a411b42180d7cf92bfe8e8584e637ce7e698d705e18d6d38efbd20ec64f60cc0dfe966c20d40fc172565bc28aaa2990c0a006360eed91af
+ languageName: node
+ linkType: hard
+
+"d3-scale@npm:4, d3-scale@npm:^4.0.2":
version: 4.0.2
resolution: "d3-scale@npm:4.0.2"
dependencies:
@@ -12599,7 +13133,14 @@ __metadata:
languageName: node
linkType: hard
-"d3-shape@npm:^3.1.0":
+"d3-selection@npm:2 - 3, d3-selection@npm:3, d3-selection@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "d3-selection@npm:3.0.0"
+ checksum: 10c0/e59096bbe8f0cb0daa1001d9bdd6dbc93a688019abc97d1d8b37f85cd3c286a6875b22adea0931b0c88410d025563e1643019161a883c516acf50c190a11b56b
+ languageName: node
+ linkType: hard
+
+"d3-shape@npm:3, d3-shape@npm:^3.1.0":
version: 3.2.0
resolution: "d3-shape@npm:3.2.0"
dependencies:
@@ -12608,7 +13149,16 @@ __metadata:
languageName: node
linkType: hard
-"d3-time-format@npm:2 - 4":
+"d3-shape@npm:^1.2.0":
+ version: 1.3.7
+ resolution: "d3-shape@npm:1.3.7"
+ dependencies:
+ d3-path: "npm:1"
+ checksum: 10c0/548057ce59959815decb449f15632b08e2a1bdce208f9a37b5f98ec7629dda986c2356bc7582308405ce68aedae7d47b324df41507404df42afaf352907577ae
+ languageName: node
+ linkType: hard
+
+"d3-time-format@npm:2 - 4, d3-time-format@npm:4":
version: 4.1.0
resolution: "d3-time-format@npm:4.1.0"
dependencies:
@@ -12617,7 +13167,7 @@ __metadata:
languageName: node
linkType: hard
-"d3-time@npm:1 - 3, d3-time@npm:2.1.1 - 3, d3-time@npm:^3.0.0":
+"d3-time@npm:1 - 3, d3-time@npm:2.1.1 - 3, d3-time@npm:3, d3-time@npm:^3.0.0":
version: 3.1.0
resolution: "d3-time@npm:3.1.0"
dependencies:
@@ -12626,13 +13176,89 @@ __metadata:
languageName: node
linkType: hard
-"d3-timer@npm:^3.0.1":
+"d3-timer@npm:1 - 3, d3-timer@npm:3, d3-timer@npm:^3.0.1":
version: 3.0.1
resolution: "d3-timer@npm:3.0.1"
checksum: 10c0/d4c63cb4bb5461d7038aac561b097cd1c5673969b27cbdd0e87fa48d9300a538b9e6f39b4a7f0e3592ef4f963d858c8a9f0e92754db73116770856f2fc04561a
languageName: node
linkType: hard
+"d3-transition@npm:2 - 3, d3-transition@npm:3, d3-transition@npm:^3.0.1":
+ version: 3.0.1
+ resolution: "d3-transition@npm:3.0.1"
+ dependencies:
+ d3-color: "npm:1 - 3"
+ d3-dispatch: "npm:1 - 3"
+ d3-ease: "npm:1 - 3"
+ d3-interpolate: "npm:1 - 3"
+ d3-timer: "npm:1 - 3"
+ peerDependencies:
+ d3-selection: 2 - 3
+ checksum: 10c0/4e74535dda7024aa43e141635b7522bb70cf9d3dfefed975eb643b36b864762eca67f88fafc2ca798174f83ca7c8a65e892624f824b3f65b8145c6a1a88dbbad
+ languageName: node
+ linkType: hard
+
+"d3-zoom@npm:3":
+ version: 3.0.0
+ resolution: "d3-zoom@npm:3.0.0"
+ dependencies:
+ d3-dispatch: "npm:1 - 3"
+ d3-drag: "npm:2 - 3"
+ d3-interpolate: "npm:1 - 3"
+ d3-selection: "npm:2 - 3"
+ d3-transition: "npm:2 - 3"
+ checksum: 10c0/ee2036479049e70d8c783d594c444fe00e398246048e3f11a59755cd0e21de62ece3126181b0d7a31bf37bcf32fd726f83ae7dea4495ff86ec7736ce5ad36fd3
+ languageName: node
+ linkType: hard
+
+"d3@npm:^7.9.0":
+ version: 7.9.0
+ resolution: "d3@npm:7.9.0"
+ dependencies:
+ d3-array: "npm:3"
+ d3-axis: "npm:3"
+ d3-brush: "npm:3"
+ d3-chord: "npm:3"
+ d3-color: "npm:3"
+ d3-contour: "npm:4"
+ d3-delaunay: "npm:6"
+ d3-dispatch: "npm:3"
+ d3-drag: "npm:3"
+ d3-dsv: "npm:3"
+ d3-ease: "npm:3"
+ d3-fetch: "npm:3"
+ d3-force: "npm:3"
+ d3-format: "npm:3"
+ d3-geo: "npm:3"
+ d3-hierarchy: "npm:3"
+ d3-interpolate: "npm:3"
+ d3-path: "npm:3"
+ d3-polygon: "npm:3"
+ d3-quadtree: "npm:3"
+ d3-random: "npm:3"
+ d3-scale: "npm:4"
+ d3-scale-chromatic: "npm:3"
+ d3-selection: "npm:3"
+ d3-shape: "npm:3"
+ d3-time: "npm:3"
+ d3-time-format: "npm:4"
+ d3-timer: "npm:3"
+ d3-transition: "npm:3"
+ d3-zoom: "npm:3"
+ checksum: 10c0/3dd9c08c73cfaa69c70c49e603c85e049c3904664d9c79a1a52a0f52795828a1ff23592dc9a7b2257e711d68a615472a13103c212032f38e016d609796e087e8
+ languageName: node
+ linkType: hard
+
+"dagre-d3-es@npm:7.0.14":
+ version: 7.0.14
+ resolution: "dagre-d3-es@npm:7.0.14"
+ dependencies:
+ d3: "npm:^7.9.0"
+ lodash-es: "npm:^4.17.21"
+ checksum: 10c0/0dc91fc79300eb0a4eab5a48a76c2baf3ce439c389d19e2f015729bb57dafd75e1e9a4c2880daf016e81ee45caca7b21745c13b23b6cd2a786ce84767e88323e
+ languageName: node
+ linkType: hard
+
"damerau-levenshtein@npm:^1.0.8":
version: 1.0.8
resolution: "damerau-levenshtein@npm:1.0.8"
@@ -12704,6 +13330,13 @@ __metadata:
languageName: node
linkType: hard
+"dayjs@npm:^1.11.20":
+ version: 1.11.21
+ resolution: "dayjs@npm:1.11.21"
+ checksum: 10c0/bd97dfdc4bfea3c66268635690313828b386faa040fbc1f829ff42a2bd748b72c9d9b3c8f9616ce9e61fcb78923f1461a462c969c54b1084458ae1b715898fb0
+ languageName: node
+ linkType: hard
+
"debounce-fn@npm:^6.0.0":
version: 6.0.0
resolution: "debounce-fn@npm:6.0.0"
@@ -12871,6 +13504,15 @@ __metadata:
languageName: node
linkType: hard
+"delaunator@npm:5":
+ version: 5.1.0
+ resolution: "delaunator@npm:5.1.0"
+ dependencies:
+ robust-predicates: "npm:^3.0.2"
+ checksum: 10c0/6489e3598212ab8759575e30f3ac26063471846e25c779048f441f2ccefd25efb9a52275e7e8e3dcc5bf5bc5c35169cf1de27f985d359fd5a24057be88fd1817
+ languageName: node
+ linkType: hard
+
"delayed-stream@npm:~1.0.0":
version: 1.0.0
resolution: "delayed-stream@npm:1.0.0"
@@ -13029,7 +13671,7 @@ __metadata:
languageName: node
linkType: hard
-"dompurify@npm:^3.3.2":
+"dompurify@npm:^3.3.2, dompurify@npm:^3.3.3":
version: 3.4.11
resolution: "dompurify@npm:3.4.11"
dependencies:
@@ -13561,6 +14203,20 @@ __metadata:
languageName: node
linkType: hard
+"es-toolkit@npm:^1.45.1":
+ version: 1.48.1
+ resolution: "es-toolkit@npm:1.48.1"
+ dependenciesMeta:
+ "@trivago/prettier-plugin-sort-imports@4.3.0":
+ unplugged: true
+ prettier-plugin-sort-re-exports@0.0.1:
+ unplugged: true
+ vitepress-plugin-sandpack@1.1.4:
+ unplugged: true
+ checksum: 10c0/29bfccb8aaf088f27ea91b5bf6448b8a46ef5d809139ad9335bea6919d0bc3aadc3206c957ab6c72d65864906fd75cf9ab41fbd936aaf69e58c6259d2821187d
+ languageName: node
+ linkType: hard
+
"esbuild-register@npm:3.6.0":
version: 3.6.0
resolution: "esbuild-register@npm:3.6.0"
@@ -14594,6 +15250,28 @@ __metadata:
languageName: node
linkType: hard
+"framer-motion@npm:^12.42.0":
+ version: 12.42.0
+ resolution: "framer-motion@npm:12.42.0"
+ dependencies:
+ motion-dom: "npm:^12.42.0"
+ motion-utils: "npm:^12.39.0"
+ tslib: "npm:^2.4.0"
+ peerDependencies:
+ "@emotion/is-prop-valid": "*"
+ react: ^18.0.0 || ^19.0.0
+ react-dom: ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ "@emotion/is-prop-valid":
+ optional: true
+ react:
+ optional: true
+ react-dom:
+ optional: true
+ checksum: 10c0/c425fa953615f3a54a0a3bb1dd7030d68613436aec1357bdb8b4752e9c46e0c9afe3c6d7ec2c2e730ff1978900575060480cb57ac1957baecd35093108e711bc
+ languageName: node
+ linkType: hard
+
"fresh@npm:0.5.2":
version: 0.5.2
resolution: "fresh@npm:0.5.2"
@@ -15056,6 +15734,13 @@ __metadata:
languageName: node
linkType: hard
+"hachure-fill@npm:^0.5.2":
+ version: 0.5.2
+ resolution: "hachure-fill@npm:0.5.2"
+ checksum: 10c0/307e3b6f9f2d3c11a82099c3f71eecbb9c440c00c1f896ac1732c23e6dbff16a92bb893d222b8b721b89cf11e58649ca60b4c24e5663f705f877cefd40153429
+ languageName: node
+ linkType: hard
+
"has-bigints@npm:^1.0.2":
version: 1.1.0
resolution: "has-bigints@npm:1.1.0"
@@ -15445,7 +16130,7 @@ __metadata:
languageName: node
linkType: hard
-"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2":
+"iconv-lite@npm:0.6, iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2":
version: 0.6.3
resolution: "iconv-lite@npm:0.6.3"
dependencies:
@@ -15525,6 +16210,13 @@ __metadata:
languageName: node
linkType: hard
+"import-meta-resolve@npm:^4.2.0":
+ version: 4.2.0
+ resolution: "import-meta-resolve@npm:4.2.0"
+ checksum: 10c0/3ee8aeecb61d19b49d2703987f977e9d1c7d4ba47db615a570eaa02fe414f40dfa63f7b953e842cbe8470d26df6371332bfcf21b2fd92b0112f9fea80dde2c4c
+ languageName: node
+ linkType: hard
+
"imurmurhash@npm:^0.1.4":
version: 0.1.4
resolution: "imurmurhash@npm:0.1.4"
@@ -15594,6 +16286,13 @@ __metadata:
languageName: node
linkType: hard
+"internmap@npm:^1.0.0":
+ version: 1.0.1
+ resolution: "internmap@npm:1.0.1"
+ checksum: 10c0/60942be815ca19da643b6d4f23bd0bf4e8c97abbd080fb963fe67583b60bdfb3530448ad4486bae40810e92317bded9995cc31411218acc750d72cd4e8646eee
+ languageName: node
+ linkType: hard
+
"ioredis@npm:^5.4.1, ioredis@npm:^5.4.2":
version: 5.6.0
resolution: "ioredis@npm:5.6.0"
@@ -16350,6 +17049,17 @@ __metadata:
languageName: node
linkType: hard
+"katex@npm:^0.16.45":
+ version: 0.16.47
+ resolution: "katex@npm:0.16.47"
+ dependencies:
+ commander: "npm:^8.3.0"
+ bin:
+ katex: cli.js
+ checksum: 10c0/b10f4d0651c60771a48444879e4227255e26e2b2ec061b1ee4b08934863ad2324ba8dbb772455f7768aeb14dfcc13bcd309174a0ddd5ef954a607f644a197710
+ languageName: node
+ linkType: hard
+
"keyv@npm:^4.5.4":
version: 4.5.4
resolution: "keyv@npm:4.5.4"
@@ -16359,6 +17069,13 @@ __metadata:
languageName: node
linkType: hard
+"khroma@npm:^2.1.0":
+ version: 2.1.0
+ resolution: "khroma@npm:2.1.0"
+ checksum: 10c0/634d98753ff5d2540491cafeb708fc98de0d43f4e6795256d5c8f6e3ad77de93049ea41433928fda3697adf7bbe6fe27351858f6d23b78f8b5775ef314c59891
+ languageName: node
+ linkType: hard
+
"kleur@npm:^3.0.3":
version: 3.0.3
resolution: "kleur@npm:3.0.3"
@@ -16460,6 +17177,20 @@ __metadata:
languageName: node
linkType: hard
+"layout-base@npm:^1.0.0":
+ version: 1.0.2
+ resolution: "layout-base@npm:1.0.2"
+ checksum: 10c0/2a55d0460fd9f6ed53d7e301b9eb3dea19bda03815d616a40665ce6dc75c1f4d62e1ca19a897da1cfaf6de1b91de59cd6f2f79ba1258f3d7fccc7d46ca7f3337
+ languageName: node
+ linkType: hard
+
+"layout-base@npm:^2.0.0":
+ version: 2.0.1
+ resolution: "layout-base@npm:2.0.1"
+ checksum: 10c0/a44df9ef3cbff9916a10f616635e22b5787c89fa62b2fec6f99e8e6ee512c7cebd22668ce32dab5a83c934ba0a309c51a678aa0b40d70853de6c357893c0a88b
+ languageName: node
+ linkType: hard
+
"leac@npm:^0.6.0":
version: 0.6.0
resolution: "leac@npm:0.6.0"
@@ -16688,6 +17419,13 @@ __metadata:
languageName: node
linkType: hard
+"lodash-es@npm:^4.17.21":
+ version: 4.18.1
+ resolution: "lodash-es@npm:4.18.1"
+ checksum: 10c0/35d4dcf87ef07f8d090f409447575800108057e360b445f590d0d25d09e3d1e33a163d2fc100d4d072b0f901d5e2fc533cd7c4bfd8eeb38a06abec693823c8b8
+ languageName: node
+ linkType: hard
+
"lodash.camelcase@npm:^4.3.0":
version: 4.3.0
resolution: "lodash.camelcase@npm:4.3.0"
@@ -16943,6 +17681,15 @@ __metadata:
languageName: node
linkType: hard
+"marked@npm:^16.3.0":
+ version: 16.4.2
+ resolution: "marked@npm:16.4.2"
+ bin:
+ marked: bin/marked.js
+ checksum: 10c0/fc6051142172454f2023f3d6b31cca92879ec8e1b96457086a54c70354c74b00e1b6543a76a1fad6d399366f52b90a848f6ffb8e1d65a5baff87f3ba9b8f1847
+ languageName: node
+ linkType: hard
+
"math-intrinsics@npm:^1.1.0":
version: 1.1.0
resolution: "math-intrinsics@npm:1.1.0"
@@ -17223,6 +17970,35 @@ __metadata:
languageName: node
linkType: hard
+"mermaid@npm:^11.16.0":
+ version: 11.16.0
+ resolution: "mermaid@npm:11.16.0"
+ dependencies:
+ "@braintree/sanitize-url": "npm:^7.1.2"
+ "@iconify/utils": "npm:^3.0.2"
+ "@mermaid-js/parser": "npm:^1.2.0"
+ "@types/d3": "npm:^7.4.3"
+ "@upsetjs/venn.js": "npm:^2.0.0"
+ cytoscape: "npm:^3.33.3"
+ cytoscape-cose-bilkent: "npm:^4.1.0"
+ cytoscape-fcose: "npm:^2.2.0"
+ d3: "npm:^7.9.0"
+ d3-sankey: "npm:^0.12.3"
+ dagre-d3-es: "npm:7.0.14"
+ dayjs: "npm:^1.11.20"
+ dompurify: "npm:^3.3.3"
+ es-toolkit: "npm:^1.45.1"
+ katex: "npm:^0.16.45"
+ khroma: "npm:^2.1.0"
+ marked: "npm:^16.3.0"
+ roughjs: "npm:^4.6.6"
+ stylis: "npm:^4.3.6"
+ ts-dedent: "npm:^2.2.0"
+ uuid: "npm:^11.1.0 || ^12 || ^13 || ^14.0.0"
+ checksum: 10c0/a14f9bc9db7f1dea65b0d6c0b920236d12ad2812f2a1b11c8b39b3dfb3c22bfce39c77f6a69493203b85880d8008d345c539efe7ae6a31d3dad512833ccfb517
+ languageName: node
+ linkType: hard
+
"methods@npm:~1.1.2":
version: 1.1.2
resolution: "methods@npm:1.1.2"
@@ -17786,6 +18562,43 @@ __metadata:
languageName: node
linkType: hard
+"motion-dom@npm:^12.42.0":
+ version: 12.42.0
+ resolution: "motion-dom@npm:12.42.0"
+ dependencies:
+ motion-utils: "npm:^12.39.0"
+ checksum: 10c0/c6cfe46ecce0bf2ffb4dcbc5d8b92777f0507b273bddc0a7da8cf2e043ee3db3082034138d801bbd581d1111a8d12465df9d7bb21ab0ea3e35cb2aac43e08d4e
+ languageName: node
+ linkType: hard
+
+"motion-utils@npm:^12.39.0":
+ version: 12.39.0
+ resolution: "motion-utils@npm:12.39.0"
+ checksum: 10c0/6d7a2a2cc0797b72410a666a9cc1c201c8e39bf9669670464e433fe1e72af5f0217154c869867b34fbadf3664cf222c0d022bbc4eed7927f201ae971918e7440
+ languageName: node
+ linkType: hard
+
+"motion@npm:^12.42.0":
+ version: 12.42.0
+ resolution: "motion@npm:12.42.0"
+ dependencies:
+ framer-motion: "npm:^12.42.0"
+ tslib: "npm:^2.4.0"
+ peerDependencies:
+ "@emotion/is-prop-valid": "*"
+ react: ^18.0.0 || ^19.0.0
+ react-dom: ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ "@emotion/is-prop-valid":
+ optional: true
+ react:
+ optional: true
+ react-dom:
+ optional: true
+ checksum: 10c0/5589172986a3a0bd3c11295d29190a01ef82a3a8b995b3db5cfe10a95f83a57ca72d5180034082c7ffee6c1389514342c330b147369ef6570b71d02cb482825c
+ languageName: node
+ linkType: hard
+
"ms@npm:2.0.0":
version: 2.0.0
resolution: "ms@npm:2.0.0"
@@ -18663,6 +19476,13 @@ __metadata:
languageName: node
linkType: hard
+"package-manager-detector@npm:^1.3.0":
+ version: 1.6.0
+ resolution: "package-manager-detector@npm:1.6.0"
+ checksum: 10c0/6419d0b840be64fd45bcdcb7a19f09b81b65456d5e7f7a3daac305a4c90643052122f6ac0308afe548ffee75e36148532a2002ea9d292754f1e385aa2e1ea03b
+ languageName: node
+ linkType: hard
+
"parent-module@npm:^1.0.0":
version: 1.0.1
resolution: "parent-module@npm:1.0.1"
@@ -18758,6 +19578,13 @@ __metadata:
languageName: node
linkType: hard
+"path-data-parser@npm:0.1.0, path-data-parser@npm:^0.1.0":
+ version: 0.1.0
+ resolution: "path-data-parser@npm:0.1.0"
+ checksum: 10c0/ba22d54669a8bc4a3df27431fe667900685585d1196085b803d0aa4066b83e709bbf2be7c1d2b56e706b49cc698231d55947c22abbfc4843ca424bbf8c985745
+ languageName: node
+ linkType: hard
+
"path-exists@npm:^4.0.0":
version: 4.0.0
resolution: "path-exists@npm:4.0.0"
@@ -19006,6 +19833,23 @@ __metadata:
languageName: node
linkType: hard
+"points-on-curve@npm:0.2.0, points-on-curve@npm:^0.2.0":
+ version: 0.2.0
+ resolution: "points-on-curve@npm:0.2.0"
+ checksum: 10c0/f0d92343fcc2ad1f48334633e580574c1e0e28038a756133e171e537f270d6d64203feada5ee556e36f448a1b46e0306dee07b30f589f4e3ad720f6ee38ef48c
+ languageName: node
+ linkType: hard
+
+"points-on-path@npm:^0.2.1":
+ version: 0.2.1
+ resolution: "points-on-path@npm:0.2.1"
+ dependencies:
+ path-data-parser: "npm:0.1.0"
+ points-on-curve: "npm:0.2.0"
+ checksum: 10c0/a7010340f9f196976f61838e767bb7b0b7f6273ab4fb9eb37c61001fe26fbfc3fcd63c96d5e85b9a4ab579213ab366f2ddaaf60e2a9253e2b91a62db33f395ba
+ languageName: node
+ linkType: hard
+
"possible-typed-array-names@npm:^1.0.0":
version: 1.1.0
resolution: "possible-typed-array-names@npm:1.1.0"
@@ -19916,6 +20760,16 @@ __metadata:
languageName: node
linkType: hard
+"react-zoom-pan-pinch@npm:^4.0.3":
+ version: 4.0.3
+ resolution: "react-zoom-pan-pinch@npm:4.0.3"
+ peerDependencies:
+ react: "*"
+ react-dom: "*"
+ checksum: 10c0/611bc498891550c5e59da5ee94996ff9c31eae533affa10f2fa0b0cb7b5333b51c1e7aa1bb918dcfff2a103c42de0b1963e1fdfe4fa87fcae36b046c37a822b1
+ languageName: node
+ linkType: hard
+
"react@npm:19.2.4":
version: 19.2.4
resolution: "react@npm:19.2.4"
@@ -20324,6 +21178,13 @@ __metadata:
languageName: node
linkType: hard
+"robust-predicates@npm:^3.0.2":
+ version: 3.0.3
+ resolution: "robust-predicates@npm:3.0.3"
+ checksum: 10c0/ae23a0318be809545f091a076818747848f144495491e24e6df5d767f2bad0a1501bbeac34e78b74681d1437ecff0585f593ebfe6c8d49a50e79c5053b962693
+ languageName: node
+ linkType: hard
+
"rolldown@npm:1.0.3":
version: 1.0.3
resolution: "rolldown@npm:1.0.3"
@@ -20482,6 +21343,18 @@ __metadata:
languageName: unknown
linkType: soft
+"roughjs@npm:^4.6.6":
+ version: 4.6.6
+ resolution: "roughjs@npm:4.6.6"
+ dependencies:
+ hachure-fill: "npm:^0.5.2"
+ path-data-parser: "npm:^0.1.0"
+ points-on-curve: "npm:^0.2.0"
+ points-on-path: "npm:^0.2.1"
+ checksum: 10c0/68c11bf4516aa014cef2fe52426a9bab237c2f500d13e1a4f13b523cb5723667bf2d92b9619325efdc5bc2a193588ff5af8d51683df17cfb8720e96fe2b92b0c
+ languageName: node
+ linkType: hard
+
"router@npm:^2.2.0":
version: 2.2.0
resolution: "router@npm:2.2.0"
@@ -20560,6 +21433,13 @@ __metadata:
languageName: node
linkType: hard
+"rw@npm:1":
+ version: 1.3.3
+ resolution: "rw@npm:1.3.3"
+ checksum: 10c0/b1e1ef37d1e79d9dc7050787866e30b6ddcb2625149276045c262c6b4d53075ddc35f387a856a8e76f0d0df59f4cd58fe24707e40797ebee66e542b840ed6a53
+ languageName: node
+ linkType: hard
+
"rxjs@npm:7.8.2":
version: 7.8.2
resolution: "rxjs@npm:7.8.2"
@@ -21810,6 +22690,13 @@ __metadata:
languageName: node
linkType: hard
+"stylis@npm:^4.3.6":
+ version: 4.4.0
+ resolution: "stylis@npm:4.4.0"
+ checksum: 10c0/259be096d90dfbfe903c8656dcb7591e52a421e577e950ef42ebd9ca02f387623a1165dd08761492fb6e92a7a562d62a53a694a10b0a2f6dcd7a0db107b4bf55
+ languageName: node
+ linkType: hard
+
"sucrase@npm:^3.35.0":
version: 3.35.0
resolution: "sucrase@npm:3.35.0"
@@ -22061,6 +22948,13 @@ __metadata:
languageName: node
linkType: hard
+"tinyexec@npm:^1.0.1":
+ version: 1.2.4
+ resolution: "tinyexec@npm:1.2.4"
+ checksum: 10c0/153b8db6b080194b558ff145b9cffc36b80a6e07babd644dcfbe49c807eee668c876049d28bdee90b96304476f883352f2dad91b3f86bc23832532f4363e66ff
+ languageName: node
+ linkType: hard
+
"tinyexec@npm:^1.0.2":
version: 1.1.1
resolution: "tinyexec@npm:1.1.1"
@@ -22242,6 +23136,13 @@ __metadata:
languageName: node
linkType: hard
+"ts-dedent@npm:^2.2.0":
+ version: 2.3.0
+ resolution: "ts-dedent@npm:2.3.0"
+ checksum: 10c0/bfc3331e0740191c0134fb526e7f01e16657e50955c9395de14703c6ef17119c91651fa044cf558dc9c1dbb0fe1b2a74e303aeebcbd5e3b31acd14f72f545a54
+ languageName: node
+ linkType: hard
+
"ts-essentials@npm:>=10.0.0":
version: 10.1.1
resolution: "ts-essentials@npm:10.1.1"