diff --git a/CHANGELOG.md b/CHANGELOG.md index b450fef56..a451074d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added per-step token cost tracking and estimated tool call token usage to Ask Sourcebot chat history. [#1353](https://github.com/sourcebot-dev/sourcebot/pull/1353) +- [EE] Added mermaid diagram rendering to Ask Sourcebot answers, with pan/zoom, copy/export, in-thread deep links, and an interleaved right-panel view. [#1369](https://github.com/sourcebot-dev/sourcebot/pull/1369) - [EE] Added a context-window usage gauge to the Ask Sourcebot chat details, showing how much of the selected model's context window each turn occupies. Window sizes are resolved from the models.dev catalog. [#1370](https://github.com/sourcebot-dev/sourcebot/pull/1370) ### Fixed diff --git a/docs/docs/features/ask/ask-sourcebot.mdx b/docs/docs/features/ask/ask-sourcebot.mdx index 3fcf81576..7f89f6d59 100644 --- a/docs/docs/features/ask/ask-sourcebot.mdx +++ b/docs/docs/features/ask/ask-sourcebot.mdx @@ -5,7 +5,7 @@ title: Ask Sourcebot Ask Sourcebot gives you the ability to ask complex questions about your entire codebase in natural language. It uses Sourcebot’s existing [code search](/docs/features/search/code-search) and [navigation](/docs/features/code-navigation) tools to allow reasoning models to search your code, -follow code nav references, and provide an answer that’s rich with inline citations and navigable code snippets. +follow code nav references, and provide an answer that’s rich with inline citations, diagrams, and navigable code snippets. Ask Sourcebot **uses an LLM provider you configure with Sourcebot**, ensuring you have full control over where your data is sent. @@ -45,6 +45,7 @@ In this domain, these tools fall short: We built Ask Sourcebot to address these problems. With Ask Sourcebot, you can: - Ask questions about your teams entire codebase (even on repos you don't have locally) - Easily parse the response with side-by-side citations and code navigation +- Visualize architecture and flows with diagrams the model generates inline - Share answers with your team to spread the knowledge Being a web app is less convenient than being in your IDE, but it allows Sourcebot to provide responses in a richer UI that isn't constrained by the IDE. diff --git a/packages/web/package.json b/packages/web/package.json index 82543adbd..d6c1c4db6 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -157,8 +157,10 @@ "langfuse-vercel": "^3.38.4", "linguist-languages": "^9.3.1", "lucide-react": "^1.7.0", + "mermaid": "^11.16.0", "micromatch": "^4.0.8", "minidenticons": "^4.2.1", + "motion": "^12.42.0", "next": "^16.2.6", "next-auth": "^5.0.0-beta.30", "next-navigation-guard": "^0.2.0", @@ -180,6 +182,7 @@ "react-icons": "^5.6.0", "react-markdown": "^10.1.0", "react-resizable-panels": "^2.1.1", + "react-zoom-pan-pinch": "^4.0.3", "recharts": "^2.15.3", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", diff --git a/packages/web/src/components/ui/textShimmer.tsx b/packages/web/src/components/ui/textShimmer.tsx new file mode 100644 index 000000000..0c9692e92 --- /dev/null +++ b/packages/web/src/components/ui/textShimmer.tsx @@ -0,0 +1,61 @@ +'use client'; + +import { cn } from '@/lib/utils'; +import { motion } from 'motion/react'; +import React, { useMemo, type JSX } from 'react'; + +// Vendored from motion-primitives (https://motion-primitives.com/docs/text-shimmer). +// A shimmer sweeps across the text via an animated background-position on a +// background-clipped gradient; the spread scales with the content length. +export type TextShimmerProps = { + children: string; + as?: React.ElementType; + className?: string; + duration?: number; + spread?: number; +}; + +function TextShimmerComponent({ + children, + as: Component = 'p', + className, + duration = 2, + spread = 2, +}: TextShimmerProps) { + const MotionComponent = motion.create( + Component as keyof JSX.IntrinsicElements + ); + + const dynamicSpread = useMemo(() => { + return children.length * spread; + }, [children, spread]); + + return ( + + {children} + + ); +} + +export const TextShimmer = React.memo(TextShimmerComponent); diff --git a/packages/web/src/ee/features/chat/agent.ts b/packages/web/src/ee/features/chat/agent.ts index 3a300a08b..c0f1e45bc 100644 --- a/packages/web/src/ee/features/chat/agent.ts +++ b/packages/web/src/ee/features/chat/agent.ts @@ -658,6 +658,22 @@ const createPrompt = ({ - If you cannot provide a code reference for something you're discussing, do not mention that specific code element - Always prefer to use \`${FILE_REFERENCE_PREFIX}\` over \`\`\`code\`\`\` blocks. + **Diagrams:** + - Proactively include a diagram when a visual communicates the answer better than prose, e.g. architecture overviews, control/data flow, sequences of interactions, state machines, or entity relationships. Use your judgement, do not force a diagram for simple answers. + - Render diagrams as a \`\`\`mermaid fenced code block. This is an explicit exception to the rule above: it is OK to use a \`\`\`mermaid block even though you otherwise prefer \`${FILE_REFERENCE_PREFIX}\` over code blocks. Continue to use \`${FILE_REFERENCE_PREFIX}\` for code references in your prose. + - Give every diagram a short, descriptive, human-readable name via a mermaid YAML frontmatter \`title\` placed at the very top of the \`\`\`mermaid block, before the diagram type declaration. This name is shown as the diagram's label in the answer and the side panel (it falls back to a generic "Diagram N" if omitted). Keep the title plain text; if it must contain special characters such as a colon, wrap the value in double quotes so the frontmatter stays valid YAML (e.g. \`title: "Auth: login flow"\`). Invalid frontmatter will prevent the diagram from rendering. For example: + \`\`\`mermaid + --- + title: Authentication Flow + --- + flowchart TD + ... + \`\`\` + - Mermaid syntax rules: do NOT put spaces or special characters in node IDs (use camelCase or underscores), wrap node and edge labels that contain special characters (parentheses, commas, colons) in double quotes, avoid reserved keywords (\`end\`, \`graph\`, \`subgraph\`) as node IDs, and do NOT use \`click\` events or custom colors/styling (e.g. \`style\`, \`classDef\`, \`linkStyle\` lines — the theme is applied automatically and these directives are stripped before rendering). + - Do NOT use \`
\`/\`
\` tags or \`\\n\` for line breaks inside node or edge labels — they do not render reliably. Keep each label to a single short phrase; if you need more detail, split it into multiple connected nodes rather than wrapping text. + - You can group related nodes into a subgraph. Open it with the exact form \`subgraph someId["Label"]\` (the literal keyword \`subgraph\`, then a unique camelCase id, then the quoted label) and close it with \`end\`; the keyword and id are both required or the diagram will not render. + - Before emitting a \`\`\`mermaid block, self-check it once: every label containing a special character is double-quoted, no node ID is a reserved keyword, there are no \`
\`/\`\\n\` line breaks in labels, and there are no \`style\`/\`classDef\`/\`linkStyle\` directives. + **Example answer structure:** \`\`\`markdown ${ANSWER_TAG} diff --git a/packages/web/src/ee/features/chat/components/chatThread/answerCard.tsx b/packages/web/src/ee/features/chat/components/chatThread/answerCard.tsx index 2aeb2ac95..3ed22370e 100644 --- a/packages/web/src/ee/features/chat/components/chatThread/answerCard.tsx +++ b/packages/web/src/ee/features/chat/components/chatThread/answerCard.tsx @@ -155,6 +155,7 @@ const AnswerCardComponent = forwardRef(({ (null); const answerRef = useRef(null); - const [hoveredReference, setHoveredReference] = useState(undefined); - const [selectedReference, setSelectedReference] = useState(undefined); + // Unified panel selection/hover: a single selection model shared by inline + // file-reference citations and diagrams (see panelContext.ts). Only one + // thing is selected/hovered at a time. + const [selected, setSelected] = useState(undefined); + const [hovered, setHovered] = useState(undefined); + + const selectedReference = useMemo(() => (selected?.kind === 'reference' ? selected.reference : undefined), [selected]); + const hoveredReference = useMemo(() => (hovered?.kind === 'reference' ? hovered.reference : undefined), [hovered]); + + const setSelectedReference = useCallback((reference?: Reference) => { + setSelected(reference ? { kind: 'reference', reference } : undefined); + }, []); + const setHoveredReference = useCallback((reference?: Reference) => { + setHovered(reference ? { kind: 'reference', reference } : undefined); + }, []); + const [isDetailsPanelExpanded, _setIsDetailsPanelExpanded] = useState(isNetworkActive); const hasAutoCollapsed = useRef(false); const userHasManuallyExpanded = useRef(false); @@ -325,27 +341,59 @@ const ChatThreadListItemComponent = forwardRef { + return new Map(diagrams.map((diagram, i) => [diagram.id, i])); + }, [diagrams]); + + // Reveal a diagram in the right panel: the panel list expands it and scrolls + // it into view when the selection changes. Clearing first lets the same + // diagram be re-revealed (re-clicking a chip re-scrolls). + const revealDiagram = useCallback((diagramId: string) => { + setSelected(undefined); + requestAnimationFrame(() => setSelected({ kind: 'diagram', diagramId })); + }, []); - // Extract the file sources that are referenced by the answer part. - const referencedFileSources = useMemo(() => { - const fileSources = sources.filter((source) => source.type === 'file'); - - return references - .filter((reference) => reference.type === 'file') - .map((reference) => tryResolveFileReference(reference, fileSources)) - .filter((file) => file !== undefined) - // de-duplicate files - .filter((file, index, self) => - index === self.findIndex((t) => - t?.path === file?.path - && t?.repo === file?.repo - && t?.revision === file?.revision - ) - ); - }, [references, sources]); + const setHoveredDiagram = useCallback((diagramId?: string) => { + setHovered(diagramId ? { kind: 'diagram', diagramId } : undefined); + }, []); + + const jumpToInlineDiagram = useCallback((diagramId: string) => { + document.getElementById(`diagram-${diagramId}`)?.scrollIntoView({ behavior: 'smooth', block: 'center' }); + }, []); + const getDiagramIndex = useCallback((diagramId: string) => { + return diagramIndexById.get(diagramId) ?? -1; + }, [diagramIndexById]); + + const panelContextValue = useMemo(() => ({ + chatId, + isStreaming: isNetworkActive, + setSelectedReference, + setHoveredReference, + revealDiagram, + setHoveredDiagram, + getDiagramIndex, + jumpToInlineDiagram, + }), [chatId, isNetworkActive, setSelectedReference, setHoveredReference, revealDiagram, setHoveredDiagram, getDiagramIndex, jumpToInlineDiagram]); + + const sourcesView = ( + + ); return ( +
- {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 && ( + + )} +
+ ); +}; 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 ( + + ); +}; 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 &&
} + + {zoomLabel} + + +
+
+ ); +}; + +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 + + + + + ); + + 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"