From 230e425fdf76aa1fe89949057a987a4e37515311 Mon Sep 17 00:00:00 2001 From: Manush Maheshwari Date: Tue, 14 Jul 2026 02:09:45 +0530 Subject: [PATCH 1/4] feat(ui): add shared className, humanize, and motion utilities Foundational frontend utilities for the staged chat UI/UX work: - cn(): clsx + tailwind-merge className composition so conflicting Tailwind utilities resolve deterministically. - titleCaseWords()/ACRONYMS: humanize snake_case and `__`-separated identifiers (tool and function names) into display labels, upper-casing common acronyms. - AppMotionConfig: a Motion provider that honors prefers-reduced-motion. Adds clsx, tailwind-merge, and motion. Covered 100% by unit tests; no UI behavior changes yet. Co-Authored-By: Claude Opus 4.8 Signed-off-by: Manush Maheshwari --- frontends/ui/package-lock.json | 93 +++++++++++++++++--- frontends/ui/package.json | 3 + frontends/ui/src/shared/lib/cn.spec.ts | 20 +++++ frontends/ui/src/shared/lib/cn.ts | 13 +++ frontends/ui/src/shared/lib/humanize.spec.ts | 27 ++++++ frontends/ui/src/shared/lib/humanize.ts | 26 ++++++ frontends/ui/src/shared/lib/motion.spec.tsx | 33 +++++++ frontends/ui/src/shared/lib/motion.tsx | 18 ++++ 8 files changed, 221 insertions(+), 12 deletions(-) create mode 100644 frontends/ui/src/shared/lib/cn.spec.ts create mode 100644 frontends/ui/src/shared/lib/cn.ts create mode 100644 frontends/ui/src/shared/lib/humanize.spec.ts create mode 100644 frontends/ui/src/shared/lib/humanize.ts create mode 100644 frontends/ui/src/shared/lib/motion.spec.tsx create mode 100644 frontends/ui/src/shared/lib/motion.tsx diff --git a/frontends/ui/package-lock.json b/frontends/ui/package-lock.json index 1945b0fa9..5cffdf964 100644 --- a/frontends/ui/package-lock.json +++ b/frontends/ui/package-lock.json @@ -11,14 +11,17 @@ "@nvidia/foundations-react-core": "^0.600.0", "@react-pdf/renderer": "^4.3.2", "@tailwindcss/postcss": "^4.1.18", + "clsx": "^2.1.1", "http-proxy": "^1.18.1", "marked": "^16.4.2", + "motion": "^12.42.2", "next": "^16.2.6", "next-auth": "^4.24.7", "react": "^18.2.0", "react-dom": "^18.2.0", "react-markdown": "^10.1.0", "remark-gfm": "^4.0.1", + "tailwind-merge": "^3.6.0", "typescript": "5.9.3", "zod": "^3.24.2", "zustand": "^5.0.0" @@ -1891,9 +1894,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1910,9 +1910,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1929,9 +1926,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1948,9 +1942,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7775,6 +7766,33 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/framer-motion": { + "version": "12.42.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.42.2.tgz", + "integrity": "sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.42.2", + "motion-utils": "^12.39.0", + "tslib": "^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 + } + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -10624,6 +10642,47 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/motion": { + "version": "12.42.2", + "resolved": "https://registry.npmjs.org/motion/-/motion-12.42.2.tgz", + "integrity": "sha512-Atvv11yUKIid41cVrRBDVX5m8tF8kNpExRSlbpt6APClhDjtwQssgFHhQzejxw7/7YYbjHSPKBVbHo05BuJT5Q==", + "license": "MIT", + "dependencies": { + "framer-motion": "^12.42.2", + "tslib": "^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 + } + } + }, + "node_modules/motion-dom": { + "version": "12.42.2", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.42.2.tgz", + "integrity": "sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.39.0" + } + }, + "node_modules/motion-utils": { + "version": "12.39.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz", + "integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -12957,6 +13016,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/tailwind-merge": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", + "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "4.1.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", diff --git a/frontends/ui/package.json b/frontends/ui/package.json index b114b57d5..7c7d5ede4 100644 --- a/frontends/ui/package.json +++ b/frontends/ui/package.json @@ -24,14 +24,17 @@ "@nvidia/foundations-react-core": "^0.600.0", "@react-pdf/renderer": "^4.3.2", "@tailwindcss/postcss": "^4.1.18", + "clsx": "^2.1.1", "http-proxy": "^1.18.1", "marked": "^16.4.2", + "motion": "^12.42.2", "next": "^16.2.6", "next-auth": "^4.24.7", "react": "^18.2.0", "react-dom": "^18.2.0", "react-markdown": "^10.1.0", "remark-gfm": "^4.0.1", + "tailwind-merge": "^3.6.0", "typescript": "5.9.3", "zod": "^3.24.2", "zustand": "^5.0.0" diff --git a/frontends/ui/src/shared/lib/cn.spec.ts b/frontends/ui/src/shared/lib/cn.spec.ts new file mode 100644 index 000000000..d40d41dd1 --- /dev/null +++ b/frontends/ui/src/shared/lib/cn.spec.ts @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, test } from 'vitest' +import { cn } from './cn' + +describe('cn', () => { + test('joins truthy class names and drops falsy values', () => { + expect(cn('a', false, 'b', undefined, null, 'c', 0 && 'd')).toBe('a b c') + }) + + test('later conflicting Tailwind utilities win', () => { + expect(cn('p-2', 'p-4')).toBe('p-4') + expect(cn('rounded-sm', 'rounded-lg')).toBe('rounded-lg') + }) + + test('supports conditional object and array inputs', () => { + expect(cn('base', { active: true, hidden: false }, ['x', 'y'])).toBe('base active x y') + }) +}) diff --git a/frontends/ui/src/shared/lib/cn.ts b/frontends/ui/src/shared/lib/cn.ts new file mode 100644 index 000000000..eb4aa03d7 --- /dev/null +++ b/frontends/ui/src/shared/lib/cn.ts @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { clsx, type ClassValue } from 'clsx' +import { twMerge } from 'tailwind-merge' + +/** + * Compose conditional class names with clsx and resolve conflicting Tailwind + * utilities with tailwind-merge so later classes win deterministically. + */ +export function cn(...inputs: ClassValue[]): string { + return twMerge(clsx(inputs)) +} diff --git a/frontends/ui/src/shared/lib/humanize.spec.ts b/frontends/ui/src/shared/lib/humanize.spec.ts new file mode 100644 index 000000000..ccf1faa9e --- /dev/null +++ b/frontends/ui/src/shared/lib/humanize.spec.ts @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, test } from 'vitest' +import { ACRONYMS, titleCaseWords } from './humanize' + +describe('titleCaseWords', () => { + test('title-cases ordinary tokens', () => { + expect(titleCaseWords('web_search_tool')).toBe('Web Search Tool') + expect(titleCaseWords('knowledge_search')).toBe('Knowledge Search') + }) + + test('upper-cases known acronyms', () => { + expect(titleCaseWords('api_gateway')).toBe('API Gateway') + expect(titleCaseWords('sql_query')).toBe('SQL Query') + expect(titleCaseWords('gpt_router')).toBe('GPT Router') + for (const acronym of ACRONYMS) { + expect(titleCaseWords(acronym)).toBe(acronym.toUpperCase()) + } + }) + + test('collapses repeated separators and whitespace, tolerates empty', () => { + expect(titleCaseWords('api__gateway')).toBe('API Gateway') + expect(titleCaseWords('web search tool')).toBe('Web Search Tool') + expect(titleCaseWords('')).toBe('') + }) +}) diff --git a/frontends/ui/src/shared/lib/humanize.ts b/frontends/ui/src/shared/lib/humanize.ts new file mode 100644 index 000000000..1da7d6074 --- /dev/null +++ b/frontends/ui/src/shared/lib/humanize.ts @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +/** + * Tokens that render fully upper-cased in humanized names (e.g. tool / function + * labels) rather than title-cased, so `api_gateway` becomes "API Gateway", not + * "Api Gateway". + */ +export const ACRONYMS = new Set(['ai', 'api', 'sql', 'gpt', 'llm', 'url', 'pdf', 'csv', 'json']) + +/** + * Humanize a snake_case / `__`-separated identifier into a display label, + * upper-casing known acronyms. Collapses runs of separators (so + * `web_search_tool` -> "Web Search Tool", `api__gateway` -> "API Gateway"). + */ +export function titleCaseWords(text: string): string { + return text + .split(/[_\s]+/) + .filter(Boolean) + .map((word) => + ACRONYMS.has(word.toLowerCase()) + ? word.toUpperCase() + : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase() + ) + .join(' ') +} diff --git a/frontends/ui/src/shared/lib/motion.spec.tsx b/frontends/ui/src/shared/lib/motion.spec.tsx new file mode 100644 index 000000000..fdc16b257 --- /dev/null +++ b/frontends/ui/src/shared/lib/motion.spec.tsx @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { afterEach, describe, expect, test, vi } from 'vitest' +import { render, screen } from '@/test-utils' +import { AppMotionConfig } from './motion' + +const useReducedMotion = vi.hoisted(() => vi.fn(() => false)) +vi.mock('@/hooks/use-reduced-motion', () => ({ useReducedMotion })) + +describe('AppMotionConfig', () => { + afterEach(() => vi.clearAllMocks()) + + test('renders its children when motion is allowed', () => { + useReducedMotion.mockReturnValue(false) + render( + + content + , + ) + expect(screen.getByText('content')).toBeInTheDocument() + }) + + test('renders its children when reduced motion is preferred', () => { + useReducedMotion.mockReturnValue(true) + render( + + reduced + , + ) + expect(screen.getByText('reduced')).toBeInTheDocument() + }) +}) diff --git a/frontends/ui/src/shared/lib/motion.tsx b/frontends/ui/src/shared/lib/motion.tsx new file mode 100644 index 000000000..a44000007 --- /dev/null +++ b/frontends/ui/src/shared/lib/motion.tsx @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +'use client' + +import type { ReactNode } from 'react' +import { MotionConfig } from 'motion/react' +import { useReducedMotion } from '@/hooks/use-reduced-motion' + +/** + * Centralizes Motion configuration so every animation honors the user's + * `prefers-reduced-motion` setting. Mount once near the application root. + */ +export function AppMotionConfig({ children }: { children: ReactNode }): ReactNode { + const reduce = useReducedMotion() + + return {children} +} From 3ed93d61db23a21bb8a071084cc96621e718830a Mon Sep 17 00:00:00 2001 From: Manush Maheshwari Date: Tue, 14 Jul 2026 05:24:48 +0530 Subject: [PATCH 2/4] feat(ui): add shared source, research-trace, and markdown primitives Adds the UI-only shared component library the chat redesign builds on: - Sources/*: source chips, list, and reference parsing (web + doc kinds). - research/*: StatusDot, ToolCallRow, and a generic tool-label engine for the live research trace. - Surface/Card, Actions/CopyButton, CollapsibleBlock, Mermaid. - MarkdownRenderer upgrade: an answer variant, superscript citations with hover popovers, KaTeX math, and Mermaid diagrams. Prod's artifact-URL handling is preserved verbatim; chart fences fall through unchanged. Adds mermaid, katex, rehype-katex, remark-math, unist-util-visit. No warehouse/predictive features are ported. Covered by unit and render tests. Co-Authored-By: Claude Opus 4.8 Signed-off-by: Manush Maheshwari --- frontends/ui/package-lock.json | 1611 +++++++++++++++-- frontends/ui/package.json | 5 + frontends/ui/src/app/globals.css | 127 ++ .../components/Actions/CopyButton.spec.tsx | 35 + .../shared/components/Actions/CopyButton.tsx | 46 + .../CollapsibleBlock/CollapsibleBlock.tsx | 90 + .../components/CollapsibleBlock/index.ts | 4 + .../MarkdownRenderer/Citation.spec.tsx | 59 + .../components/MarkdownRenderer/Citation.tsx | 73 + .../MarkdownRenderer.spec.tsx | 79 + .../MarkdownRenderer/MarkdownRenderer.tsx | 98 +- .../MarkdownRenderer/rehype-citations.spec.ts | 42 + .../MarkdownRenderer/rehype-citations.ts | 46 + .../components/MarkdownRenderer/types.ts | 10 + .../components/Mermaid/MermaidBlock.spec.tsx | 112 ++ .../components/Mermaid/MermaidBlock.tsx | 257 +++ .../ui/src/shared/components/Mermaid/index.ts | 4 + .../components/Sources/SourceKindIcon.tsx | 33 + .../shared/components/Sources/SourceList.tsx | 57 + .../components/Sources/SourceStrip.spec.tsx | 44 + .../shared/components/Sources/SourceStrip.tsx | 87 + .../Sources/parse-references.spec.ts | 203 +++ .../components/Sources/parse-references.ts | 295 +++ .../components/Sources/source-utils.spec.ts | 57 + .../shared/components/Sources/source-utils.ts | 58 + .../ui/src/shared/components/Sources/types.ts | 22 + .../shared/components/Surface/Card.spec.tsx | 45 + .../ui/src/shared/components/Surface/Card.tsx | 40 + .../shared/components/research/StatusDot.tsx | 20 + .../components/research/ToolCallRow.tsx | 50 + .../src/shared/components/research/index.ts | 19 + .../research/research-labels.spec.ts | 113 ++ .../components/research/research-labels.ts | 244 +++ 33 files changed, 3925 insertions(+), 160 deletions(-) create mode 100644 frontends/ui/src/shared/components/Actions/CopyButton.spec.tsx create mode 100644 frontends/ui/src/shared/components/Actions/CopyButton.tsx create mode 100644 frontends/ui/src/shared/components/CollapsibleBlock/CollapsibleBlock.tsx create mode 100644 frontends/ui/src/shared/components/CollapsibleBlock/index.ts create mode 100644 frontends/ui/src/shared/components/MarkdownRenderer/Citation.spec.tsx create mode 100644 frontends/ui/src/shared/components/MarkdownRenderer/Citation.tsx create mode 100644 frontends/ui/src/shared/components/MarkdownRenderer/rehype-citations.spec.ts create mode 100644 frontends/ui/src/shared/components/MarkdownRenderer/rehype-citations.ts create mode 100644 frontends/ui/src/shared/components/Mermaid/MermaidBlock.spec.tsx create mode 100644 frontends/ui/src/shared/components/Mermaid/MermaidBlock.tsx create mode 100644 frontends/ui/src/shared/components/Mermaid/index.ts create mode 100644 frontends/ui/src/shared/components/Sources/SourceKindIcon.tsx create mode 100644 frontends/ui/src/shared/components/Sources/SourceList.tsx create mode 100644 frontends/ui/src/shared/components/Sources/SourceStrip.spec.tsx create mode 100644 frontends/ui/src/shared/components/Sources/SourceStrip.tsx create mode 100644 frontends/ui/src/shared/components/Sources/parse-references.spec.ts create mode 100644 frontends/ui/src/shared/components/Sources/parse-references.ts create mode 100644 frontends/ui/src/shared/components/Sources/source-utils.spec.ts create mode 100644 frontends/ui/src/shared/components/Sources/source-utils.ts create mode 100644 frontends/ui/src/shared/components/Sources/types.ts create mode 100644 frontends/ui/src/shared/components/Surface/Card.spec.tsx create mode 100644 frontends/ui/src/shared/components/Surface/Card.tsx create mode 100644 frontends/ui/src/shared/components/research/StatusDot.tsx create mode 100644 frontends/ui/src/shared/components/research/ToolCallRow.tsx create mode 100644 frontends/ui/src/shared/components/research/index.ts create mode 100644 frontends/ui/src/shared/components/research/research-labels.spec.ts create mode 100644 frontends/ui/src/shared/components/research/research-labels.ts diff --git a/frontends/ui/package-lock.json b/frontends/ui/package-lock.json index 5cffdf964..919b95e87 100644 --- a/frontends/ui/package-lock.json +++ b/frontends/ui/package-lock.json @@ -13,16 +13,21 @@ "@tailwindcss/postcss": "^4.1.18", "clsx": "^2.1.1", "http-proxy": "^1.18.1", + "katex": "^0.17.0", "marked": "^16.4.2", + "mermaid": "^11.16.0", "motion": "^12.42.2", "next": "^16.2.6", "next-auth": "^4.24.7", "react": "^18.2.0", "react-dom": "^18.2.0", "react-markdown": "^10.1.0", + "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.1", + "remark-math": "^6.0.0", "tailwind-merge": "^3.6.0", "typescript": "5.9.3", + "unist-util-visit": "^5.1.0", "zod": "^3.24.2", "zustand": "^5.0.0" }, @@ -71,6 +76,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/@ariakit/core": { "version": "0.4.18", "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.18.tgz", @@ -429,6 +447,18 @@ "node": ">=18" } }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", + "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", + "license": "MIT" + }, + "node_modules/@chevrotain/types": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.2.tgz", + "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==", + "license": "Apache-2.0" + }, "node_modules/@csstools/color-helpers": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", @@ -1176,6 +1206,23 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.4.tgz", + "integrity": "sha512-b1S7B1k9ohZ+iNTi2ATxbRYG9fTrJmUT0rc46bvVnNxqNRGW7dyo/vRREwyniI5IRN2RSJHDcm+s3BjWrSAjHw==", + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@iconify/types": "^2.0.0", + "import-meta-resolve": "^4.2.0" + } + }, "node_modules/@img/colour": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", @@ -1775,6 +1822,15 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mermaid-js/parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.2.0.tgz", + "integrity": "sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA==", + "license": "MIT", + "dependencies": { + "@chevrotain/types": "~11.1.2" + } + }, "node_modules/@mswjs/data": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/@mswjs/data/-/data-0.16.2.tgz", @@ -4685,6 +4741,259 @@ "assertion-error": "^2.0.1" } }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.4.tgz", + "integrity": "sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -4716,6 +5025,12 @@ "@types/estree": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, "node_modules/@types/hast": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", @@ -4732,6 +5047,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/katex": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.8.tgz", + "integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==", + "license": "MIT" + }, "node_modules/@types/lodash": { "version": "4.17.23", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.23.tgz", @@ -5418,6 +5739,16 @@ "win32" ] }, + "node_modules/@upsetjs/venn.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", + "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==", + "license": "MIT", + "optionalDependencies": { + "d3-selection": "^3.0.0", + "d3-transition": "^3.0.1" + } + }, "node_modules/@vitejs/plugin-react": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.4.tgz", @@ -6399,6 +6730,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -6452,6 +6792,15 @@ "url": "https://opencollective.com/express" } }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -6509,132 +6858,646 @@ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, - "node_modules/damerau-levenshtein": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", - "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/data-urls": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", - "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "node_modules/cytoscape": { + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.0.tgz", + "integrity": "sha512-62rNSrioXw93uliKFBwjukeQyeWwH2PqDrTac31r2P6464u3AUvTk0xS4LVvT251g7IgkFunrI48ZEZGjywSOg==", "license": "MIT", - "dependencies": { - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.0.0" - }, "engines": { - "node": ">=18" + "node": ">=0.10" } }, - "node_modules/data-urls/node_modules/whatwg-mimetype": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", "license": "MIT", - "engines": { - "node": ">=18" + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" } }, - "node_modules/data-view-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", - "dev": true, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" + "cose-base": "^2.2.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "cytoscape": "^3.2.0" } }, - "node_modules/data-view-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", - "dev": true, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/inspect-js" + "layout-base": "^2.0.0" } }, - "node_modules/data-view-byte-offset": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", - "dev": true, - "license": "MIT", + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=12" } }, - "node_modules/date-fns": { - "version": "2.30.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", - "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", - "dev": true, - "license": "MIT", + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", "dependencies": { - "@babel/runtime": "^7.21.0" + "internmap": "1 - 2" }, "engines": { - "node": ">=0.11" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/date-fns" + "node": ">=12" } }, - "node_modules/date-fns-jalali": { - "version": "4.1.0-0", - "resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz", - "integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==", - "license": "MIT" + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", "dependencies": { - "ms": "^2.1.3" + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=12" } }, - "node_modules/decimal.js": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "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" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz", + "integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==", + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls/node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/date-fns-jalali": { + "version": "4.1.0-0", + "resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz", + "integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", "license": "MIT" }, @@ -6694,6 +7557,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delaunator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -7011,6 +7883,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-toolkit": { + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.49.0.tgz", + "integrity": "sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/esbuild": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", @@ -8069,6 +8951,12 @@ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "license": "MIT" + }, "node_modules/happy-dom": { "version": "20.9.0", "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.9.0.tgz", @@ -8136,62 +9024,157 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-proto": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", - "dev": true, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-from-dom": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz", + "integrity": "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==", + "license": "ISC", + "dependencies": { + "@types/hast": "^3.0.0", + "hastscript": "^9.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", + "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.1.0", + "hast-util-from-parse5": "^8.0.0", + "parse5": "^7.0.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html-isomorphic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hast-util-from-html-isomorphic/-/hast-util-from-html-isomorphic-2.0.0.tgz", + "integrity": "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==", "license": "MIT", "dependencies": { - "dunder-proto": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" + "@types/hast": "^3.0.0", + "hast-util-from-dom": "^5.0.0", + "hast-util-from-html": "^2.0.0", + "unist-util-remove-position": "^5.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", "license": "MIT", - "engines": { - "node": ">= 0.4" + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", "license": "MIT", "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" + "@types/hast": "^3.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", "license": "MIT", "dependencies": { - "function-bind": "^1.1.2" + "@types/hast": "^3.0.0" }, - "engines": { - "node": ">= 0.4" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, "node_modules/hast-util-to-html": { @@ -8244,6 +9227,22 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-to-text": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", + "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-whitespace": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", @@ -8257,6 +9256,23 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/headers-polyfill": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", @@ -8419,6 +9435,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -8478,6 +9504,15 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-alphabetical": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", @@ -9255,6 +10290,22 @@ "node": ">=4.0" } }, + "node_modules/katex": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.17.0.tgz", + "integrity": "sha512-Vdw0ATsQ9V+LuegM/BTwQqV/6cTl5lbGcIrU+BCgLxyf6bo38ybOr372tuSIxir3CN720flu1meYR6XzNMwQnw==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -9265,6 +10316,11 @@ "json-buffer": "3.0.1" } }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -9285,6 +10341,12 @@ "node": ">=0.10" } }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "license": "MIT" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -9590,6 +10652,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -9887,6 +10955,25 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-math": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-math/-/mdast-util-math-3.0.0.tgz", + "integrity": "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "longest-streak": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.1.0", + "unist-util-remove-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-mdx-expression": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", @@ -10032,6 +11119,64 @@ "node": ">= 8" } }, + "node_modules/mermaid": { + "version": "11.16.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.16.0.tgz", + "integrity": "sha512-Zvm3kbstgdpvIJPPItlL7fppIZ3kibvc1oZIGxdvk9t6UFz6flv+Jw7FtRGKwfcI8OckmH04LqG6LlS6X4B1pA==", + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "^7.1.2", + "@iconify/utils": "^3.0.2", + "@mermaid-js/parser": "^1.2.0", + "@types/d3": "^7.4.3", + "@upsetjs/venn.js": "^2.0.0", + "cytoscape": "^3.33.3", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.14", + "dayjs": "^1.11.20", + "dompurify": "^3.3.3", + "es-toolkit": "^1.45.1", + "katex": "^0.16.45", + "khroma": "^2.1.0", + "marked": "^16.3.0", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" + } + }, + "node_modules/mermaid/node_modules/katex": { + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/mermaid/node_modules/uuid": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz", + "integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/micromark": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", @@ -10222,6 +11367,41 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/micromark-extension-math": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz", + "integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==", + "license": "MIT", + "dependencies": { + "@types/katex": "^0.16.0", + "devlop": "^1.0.0", + "katex": "^0.16.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-math/node_modules/katex": { + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, "node_modules/micromark-factory-destination": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", @@ -11279,6 +12459,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-manager-detector": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.7.0.tgz", + "integrity": "sha512-xg1eHpwYL/D/HEdWw2goFZP6vV0FH7W+PZ5rFkGjdIDLtxq7EkzBUeT3m+lndYCt8wKbmofUu1MUdMCXkCk9ZQ==", + "license": "MIT" + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -11341,6 +12527,12 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -11421,6 +12613,22 @@ "node": ">=4" } }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "license": "MIT" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "license": "MIT", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -12028,6 +13236,41 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/rehype-katex": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/rehype-katex/-/rehype-katex-7.0.1.tgz", + "integrity": "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/katex": "^0.16.0", + "hast-util-from-html-isomorphic": "^2.0.0", + "hast-util-to-text": "^4.0.0", + "katex": "^0.16.0", + "unist-util-visit-parents": "^6.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-katex/node_modules/katex": { + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, "node_modules/remark-gfm": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", @@ -12046,6 +13289,22 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark-math": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/remark-math/-/remark-math-6.0.0.tgz", + "integrity": "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-math": "^3.0.0", + "micromark-extension-math": "^3.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-parse": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", @@ -12201,6 +13460,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", + "license": "Unlicense" + }, "node_modules/rollup": { "version": "4.60.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", @@ -12246,6 +13511,18 @@ "fsevents": "~2.3.2" } }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, "node_modules/rrweb-cssom": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", @@ -12276,6 +13553,12 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, "node_modules/rxjs": { "version": "7.8.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", @@ -12962,6 +14245,12 @@ } } }, + "node_modules/stylis": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", + "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -13069,7 +14358,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -13231,6 +14519,15 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-dedent": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.3.0.tgz", + "integrity": "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==", + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -13451,6 +14748,20 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-find-after": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unist-util-is": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", @@ -13477,6 +14788,20 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-remove-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", + "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unist-util-stringify-position": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", @@ -13686,6 +15011,20 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vfile-message": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", @@ -13923,6 +15262,16 @@ "node": ">=18" } }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", diff --git a/frontends/ui/package.json b/frontends/ui/package.json index 7c7d5ede4..fbc1148ef 100644 --- a/frontends/ui/package.json +++ b/frontends/ui/package.json @@ -26,16 +26,21 @@ "@tailwindcss/postcss": "^4.1.18", "clsx": "^2.1.1", "http-proxy": "^1.18.1", + "katex": "^0.17.0", "marked": "^16.4.2", + "mermaid": "^11.16.0", "motion": "^12.42.2", "next": "^16.2.6", "next-auth": "^4.24.7", "react": "^18.2.0", "react-dom": "^18.2.0", "react-markdown": "^10.1.0", + "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.1", + "remark-math": "^6.0.0", "tailwind-merge": "^3.6.0", "typescript": "5.9.3", + "unist-util-visit": "^5.1.0", "zod": "^3.24.2", "zustand": "^5.0.0" }, diff --git a/frontends/ui/src/app/globals.css b/frontends/ui/src/app/globals.css index 5c08a0977..84301beb6 100644 --- a/frontends/ui/src/app/globals.css +++ b/frontends/ui/src/app/globals.css @@ -312,3 +312,130 @@ body { *::-webkit-scrollbar-thumb:hover { background-color: var(--background-color-accent-gray-hover); } + +/* ============================================================================= + Shared chat-UI primitives (agent trace, answer prose, source cards). + Additive tokens + component classes referenced by shared/components. + ============================================================================= */ +:root { + --radius-card: 0.8rem; + --ease-premium: cubic-bezier(0.22, 1, 0.36, 1); + --color-agent-danger: #e5484d; + --text-answer: 0.975rem; + --text-answer-line-height: 1.65; + --spacing-section: 1.75rem; +} + +/* ---- Answer prose: generous reading scale for the final answer -------------- */ +.answer-prose { + font-size: var(--text-answer); + line-height: var(--text-answer-line-height); + color: var(--text-color-primary); +} +.answer-prose h2 { + font-size: 1.375rem; + font-weight: 600; + margin-top: var(--spacing-section); + margin-bottom: 0.5rem; +} +.answer-prose h3 { + font-size: 1.125rem; + font-weight: 600; + margin-top: 1.25rem; + margin-bottom: 0.4rem; +} +.answer-prose p { + margin: 0.75rem 0; +} +.answer-prose ul, +.answer-prose ol { + margin: 0.5rem 0 0.85rem; + padding-left: 1.25rem; +} +.answer-prose li { + margin: 0.2rem 0; +} + +/* ---- Status atom: one visual language for every agent-trace node ------------ */ +@keyframes dotBreathe { + 0%, + 100% { + box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-brand) 32%, transparent); + } + 50% { + box-shadow: 0 0 0 6px color-mix(in srgb, var(--color-brand) 6%, transparent); + } +} +.status-dot { + position: relative; + display: grid; + place-items: center; + height: 0.625rem; + width: 0.625rem; + border-radius: 999px; + flex-shrink: 0; +} +.status-dot-xs { + height: 0.5rem; + width: 0.5rem; +} +.status-dot-running { + background-color: var(--color-brand); + animation: dotBreathe 1.9s var(--ease-premium) infinite; +} +.status-dot-done { + background-color: var(--color-green-500); +} +.status-dot-pending { + background-color: transparent; + border: 1px solid color-mix(in srgb, var(--border-color-base) 72%, transparent); +} +.status-dot-waiting { + background-color: var(--color-brand); + animation: dotBreathe 2.4s ease-in-out infinite; +} +.status-dot-interrupted { + background-color: var(--color-agent-danger); +} +.status-dot-error { + background-color: var(--color-agent-danger); +} + +/* ---- Shared tool-call row (inline trace + research panel) -------------------- */ +/* Fixed-width dot column so the status dot centers on the trace rail. The height + matches the label's first line box (font-size x line-height) so the dot + centers on the first line of every row regardless of run-state or size. */ +.tool-call-row-dot { + width: 1.25rem; + height: calc(0.875rem * 1.3); +} +.tool-call-row-label { + letter-spacing: -0.005em; + line-height: 1.3; +} +.tool-call-row-args { + color: var(--text-color-secondary); + font-size: 0.8125rem; + line-height: 1.35; +} + +/* ---- KaTeX display math inside rendered markdown ---------------------------- */ +.markdown-content .katex-display { + margin: 0.35rem 0 0.6rem; + padding: 0.55rem 0.7rem; + overflow-x: auto; + overflow-y: hidden; + background: var(--background-color-surface-raised); + border: 1px solid var(--border-color-base); + border-radius: var(--radius-card); +} +.markdown-content .katex { + font-size: 1.02em; +} + +@media (prefers-reduced-motion: reduce) { + .status-dot-running, + .status-dot-waiting { + animation: none; + } +} diff --git a/frontends/ui/src/shared/components/Actions/CopyButton.spec.tsx b/frontends/ui/src/shared/components/Actions/CopyButton.spec.tsx new file mode 100644 index 000000000..52f8db213 --- /dev/null +++ b/frontends/ui/src/shared/components/Actions/CopyButton.spec.tsx @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { beforeEach, describe, expect, test, vi } from 'vitest' +import { CopyButton } from './CopyButton' + +const writeText = vi.fn().mockResolvedValue(undefined) + +describe('CopyButton', () => { + beforeEach(() => { + writeText.mockClear() + Object.defineProperty(navigator, 'clipboard', { value: { writeText }, configurable: true }) + }) + + test('copies the provided text and confirms with a checkmark', async () => { + render() + fireEvent.click(screen.getByRole('button', { name: 'Copy' })) + expect(writeText).toHaveBeenCalledWith('There are 11,463 users.') + await waitFor(() => expect(screen.getByRole('button', { name: 'Copied' })).toBeInTheDocument()) + }) + + test('renders a custom label', () => { + render() + expect(screen.getByRole('button', { name: 'Copy answer' })).toBeInTheDocument() + }) + + test('does not throw when the clipboard API rejects', async () => { + writeText.mockRejectedValueOnce(new Error('blocked')) + render() + fireEvent.click(screen.getByRole('button', { name: 'Copy' })) + await waitFor(() => expect(writeText).toHaveBeenCalled()) + expect(screen.getByRole('button', { name: 'Copy' })).toBeInTheDocument() + }) +}) diff --git a/frontends/ui/src/shared/components/Actions/CopyButton.tsx b/frontends/ui/src/shared/components/Actions/CopyButton.tsx new file mode 100644 index 000000000..9700d4722 --- /dev/null +++ b/frontends/ui/src/shared/components/Actions/CopyButton.tsx @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +'use client' + +import { type ReactNode, useCallback, useState } from 'react' +import { Button } from '@/adapters/ui' +import { Check, Copy } from '@/adapters/ui/icons' + +interface CopyButtonProps { + text: string + label?: string +} + +const RESET_MS = 1500 + +/** + * Tertiary icon button that copies `text` to the clipboard and briefly swaps to a + * check to confirm. Silently no-ops when the clipboard API is unavailable + * (e.g. an insecure context) so the answer surface never errors. + */ +export function CopyButton({ text, label = 'Copy' }: CopyButtonProps): ReactNode { + const [copied, setCopied] = useState(false) + + const handleCopy = useCallback(async () => { + try { + await navigator.clipboard.writeText(text) + setCopied(true) + setTimeout(() => setCopied(false), RESET_MS) + } catch { + setCopied(false) + } + }, [text]) + + return ( + + ) +} diff --git a/frontends/ui/src/shared/components/CollapsibleBlock/CollapsibleBlock.tsx b/frontends/ui/src/shared/components/CollapsibleBlock/CollapsibleBlock.tsx new file mode 100644 index 000000000..a8c4ef46a --- /dev/null +++ b/frontends/ui/src/shared/components/CollapsibleBlock/CollapsibleBlock.tsx @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +/** + * CollapsibleBlock + * + * The shared header + body disclosure used by agent surfaces (thinking trace, + * plan/approval blocks). A header button toggles an AnimatePresence height / + * opacity body so every block opens and closes with the same motion and reads + * uniform regardless of which surface renders it. Controlled: the parent owns + * the `open` state and is notified via `onOpenChange`. + */ + +'use client' + +import type { FC, ReactNode } from 'react' +import { AnimatePresence, motion } from 'motion/react' +import { Flex, AnimatedChevron } from '@/adapters/ui' + +export interface CollapsibleBlockProps { + /** Leading glyph rendered before the title (status atom, source icon, etc.). */ + icon?: ReactNode + /** Primary header label. */ + title: ReactNode + /** Trailing header content (counts, durations, the collapse affordance). */ + meta?: ReactNode + /** Controlled open state. */ + open: boolean + /** Notified when the header toggles (only when `collapsible`). */ + onOpenChange?: (open: boolean) => void + /** When false the block renders static (no toggle, body always shown). */ + collapsible?: boolean + children: ReactNode +} + +export const CollapsibleBlock: FC = ({ + icon, + title, + meta, + open, + onOpenChange, + collapsible = true, + children, +}) => { + const header = ( + <> + + {icon != null && {icon}} + {title} + + {(meta != null || collapsible) && ( + + {meta} + {collapsible && } + + )} + + ) + + return ( +
+ {collapsible ? ( + + ) : ( +
{header}
+ )} + + + {(open || !collapsible) && ( + + {children} + + )} + +
+ ) +} diff --git a/frontends/ui/src/shared/components/CollapsibleBlock/index.ts b/frontends/ui/src/shared/components/CollapsibleBlock/index.ts new file mode 100644 index 000000000..d81d44388 --- /dev/null +++ b/frontends/ui/src/shared/components/CollapsibleBlock/index.ts @@ -0,0 +1,4 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +export { CollapsibleBlock, type CollapsibleBlockProps } from './CollapsibleBlock' diff --git a/frontends/ui/src/shared/components/MarkdownRenderer/Citation.spec.tsx b/frontends/ui/src/shared/components/MarkdownRenderer/Citation.spec.tsx new file mode 100644 index 000000000..d60e13b96 --- /dev/null +++ b/frontends/ui/src/shared/components/MarkdownRenderer/Citation.spec.tsx @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { fireEvent, render, screen } from '@testing-library/react' +import { describe, expect, test } from 'vitest' +import { Citation } from './Citation' +import type { SourceRef } from '@/shared/components/Sources/types' + +const sources: SourceRef[] = [ + { + id: 's1', + title: 'NVIDIA shipped record volume.', + kind: 'web', + label: 'nvidia.com', + url: 'https://www.nvidia.com/news', + snippet: 'Record shipments across the fleet.', + }, +] + +describe('Citation', () => { + test('renders the citation number with an accessible label', () => { + render() + expect(screen.getByLabelText('Source 1: nvidia.com')).toHaveTextContent('1') + }) + + test('renders the marker as a superscript reference mark', () => { + render() + const link = screen.getByLabelText('Source 1: nvidia.com') + expect(link.querySelector('sup')).toHaveTextContent('1') + }) + + test('links to the source url', () => { + render() + expect(screen.getByLabelText('Source 1: nvidia.com')).toHaveAttribute( + 'href', + 'https://www.nvidia.com/news' + ) + }) + + test('a non-numeric value renders bracketed text and no chip', () => { + render() + expect(screen.queryByRole('link')).not.toBeInTheDocument() + expect(screen.queryByLabelText(/^Source/)).not.toBeInTheDocument() + }) + + test('an out-of-range index degrades to a bare chip', () => { + render() + expect(screen.getByLabelText('Source 9')).toBeInTheDocument() + expect(screen.queryByRole('tooltip')).not.toBeInTheDocument() + }) + + test('focus reveals a popover with the source label and title', () => { + render() + fireEvent.focus(screen.getByLabelText('Source 1: nvidia.com')) + const tip = screen.getByRole('tooltip') + expect(tip).toHaveTextContent('nvidia.com') + expect(tip).toHaveTextContent('NVIDIA shipped record volume.') + }) +}) diff --git a/frontends/ui/src/shared/components/MarkdownRenderer/Citation.tsx b/frontends/ui/src/shared/components/MarkdownRenderer/Citation.tsx new file mode 100644 index 000000000..1cc1d845d --- /dev/null +++ b/frontends/ui/src/shared/components/MarkdownRenderer/Citation.tsx @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +'use client' + +import { type ReactNode, useState } from 'react' +import { AnimatePresence, motion } from 'motion/react' +import { SourceKindIcon } from '@/shared/components/Sources/SourceKindIcon' +import type { SourceRef } from '@/shared/components/Sources/types' + +interface CitationProps { + n: number + sources: SourceRef[] +} + +const MARK = + 'text-secondary hover:text-[color:var(--color-green-500)] ml-px cursor-pointer align-super text-[0.8rem] font-medium leading-none no-underline transition-colors' + +/** + * Inline numbered citation, rendered as a true superscript reference mark. + * Resolves to a source via its 1-based index and, when found, reveals a + * hover/focus popover with the source kind, title, and snippet. A citation + * number with no matching source degrades to a bare mark; a non-numeric value + * renders the original bracketed text unchanged. + */ +export function Citation({ n, sources }: CitationProps): ReactNode { + const [open, setOpen] = useState(false) + + if (!Number.isFinite(n) || n < 1) return <>[{Number.isFinite(n) ? n : ''}] + + const src = sources[n - 1] + const chip = ( + setOpen(true)} + onMouseLeave={() => setOpen(false)} + onFocus={() => setOpen(true)} + onBlur={() => setOpen(false)} + aria-label={src ? `Source ${n}: ${src.label}` : `Source ${n}`} + > + {n} + + ) + + if (!src) return chip + + return ( + + {chip} + + {open && ( + + + {src.label} + + {src.title} + {src.snippet && {src.snippet}} + + )} + + + ) +} diff --git a/frontends/ui/src/shared/components/MarkdownRenderer/MarkdownRenderer.spec.tsx b/frontends/ui/src/shared/components/MarkdownRenderer/MarkdownRenderer.spec.tsx index 79af7ebe6..10d34178d 100644 --- a/frontends/ui/src/shared/components/MarkdownRenderer/MarkdownRenderer.spec.tsx +++ b/frontends/ui/src/shared/components/MarkdownRenderer/MarkdownRenderer.spec.tsx @@ -4,6 +4,7 @@ import { render, screen, fireEvent } from '@/test-utils' import { describe, test, expect, vi } from 'vitest' import { MarkdownRenderer } from './MarkdownRenderer' +import type { SourceRef } from '@/shared/components/Sources/types' describe('MarkdownRenderer', () => { describe('basic rendering', () => { @@ -348,3 +349,81 @@ Visit [our site](https://example.com) for more. }) }) }) + +const answerSources: SourceRef[] = [ + { + id: 's1', + title: 'NVIDIA shipped record volume.', + kind: 'web', + label: 'nvidia.com', + url: 'https://www.nvidia.com/news', + snippet: 'Record shipments across the fleet.', + }, +] + +describe('MarkdownRenderer answer variant', () => { + test('applies the answer-prose scale on the wrapper', () => { + const { container } = render() + expect(container.querySelector('.answer-prose')).toBeInTheDocument() + }) + + test('default variant does not apply the answer-prose scale', () => { + const { container } = render() + expect(container.querySelector('.answer-prose')).not.toBeInTheDocument() + }) + + test('renders an inline [n] marker as a citation chip resolving to the source', () => { + render( + + ) + const chip = screen.getByLabelText('Source 1: nvidia.com') + expect(chip.querySelector('sup')).toHaveTextContent('1') + expect(chip).toHaveAttribute('href', 'https://www.nvidia.com/news') + }) + + test('does not turn [n] markers into chips without sources (default variant)', () => { + render() + expect(screen.queryByLabelText(/^Source 1/)).not.toBeInTheDocument() + expect(screen.getByText(/A fact/)).toBeInTheDocument() + }) + + test('a chip focus reveals a popover with the source snippet', () => { + render( + + ) + fireEvent.focus(screen.getByLabelText('Source 1: nvidia.com')) + expect(screen.getByRole('tooltip')).toHaveTextContent('Record shipments across the fleet.') + }) +}) + +describe('MarkdownRenderer math (KaTeX)', () => { + test('renders display math from a $$...$$ block', () => { + const { container } = render() + expect(container.querySelector('.katex')).toBeInTheDocument() + }) + + test('leaves a single-dollar amount as plain text (no math parsing)', () => { + const { container } = render() + expect(container.querySelector('.katex')).not.toBeInTheDocument() + expect(container.textContent).toContain('$5') + }) +}) + +describe('MarkdownRenderer chart fences fall through to code', () => { + test('a ```chart block renders as a code block, not a chart', () => { + const spec = '{ "type": "bar", "data": [] }' + const { container } = render( + + ) + expect(container.querySelector('.research-chart')).toBeFalsy() + expect(container.textContent).toContain('"type": "bar"') + }) +}) diff --git a/frontends/ui/src/shared/components/MarkdownRenderer/MarkdownRenderer.tsx b/frontends/ui/src/shared/components/MarkdownRenderer/MarkdownRenderer.tsx index 222250ba2..bc4ddb0e5 100644 --- a/frontends/ui/src/shared/components/MarkdownRenderer/MarkdownRenderer.tsx +++ b/frontends/ui/src/shared/components/MarkdownRenderer/MarkdownRenderer.tsx @@ -6,9 +6,16 @@ import { type FC, type ReactNode, memo, useMemo } from 'react' import ReactMarkdown, { type Components, type ExtraProps, defaultUrlTransform } from 'react-markdown' import remarkGfm from 'remark-gfm' +import remarkMath from 'remark-math' +import rehypeKatex from 'rehype-katex' +import 'katex/dist/katex.min.css' import { Text, CodeSnippet, Anchor } from '@/adapters/ui' +import { cn } from '@/shared/lib/cn' import type { MarkdownRendererProps } from './types' import { getLanguageFromClassName } from './utils' +import { rehypeCitations } from './rehype-citations' +import { Citation } from './Citation' +import { MermaidBlock } from '@/shared/components/Mermaid' import { ARTIFACT_SCHEME, isArtifactRef, resolveArtifactUrl } from '@/shared/utils/artifact-url' // react-markdown's default sanitizer strips non-standard URL schemes, which would blank the @@ -43,34 +50,48 @@ function slugify(text: string): string { * @param isStreaming - Whether content is still streaming (disables memoization) * @param className - Additional CSS classes * @param compact - Use smaller text sizes for chat bubbles + * @param artifactJobId - Job id used to resolve `artifact://` image refs + * @param variant - `answer` applies the answer-prose scale and citation chips + * @param sources - Cited sources used to resolve inline `[n]` citation chips */ export const MarkdownRenderer: FC = memo( - ({ content, className = '', compact = false, artifactJobId }) => { - // Custom component mappings + ({ content, className = '', compact = false, artifactJobId, variant = 'default', sources }) => { + const isAnswer = variant === 'answer' + const hasCitations = isAnswer && Array.isArray(sources) && sources.length > 0 const components: Components = useMemo( () => ({ + cite: ({ children }) => ( + + ), code: ({ children, className: codeClassName, + node: _node, ...props }: React.ComponentPropsWithoutRef<'code'> & ExtraProps) => { - // Check if this is a block code (has language class) vs inline + // Block code (has a language class) vs inline const isBlock = codeClassName?.startsWith('language-') const codeContent = String(children).replace(/\n$/, '') if (isBlock) { - const language = getLanguageFromClassName(codeClassName) const lineCount = codeContent.split('\n').length - - return ( + const fallback = ( 15} rows={15} /> ) + + // A fenced ```mermaid block renders as an interactive diagram; every + // other fence (including ```chart) falls through to the code block. + if (/language-mermaid\b/.test(codeClassName ?? '')) { + return + } + + return fallback } // Inline code @@ -87,7 +108,7 @@ export const MarkdownRenderer: FC = memo( // Skip default pre rendering since CodeSnippet handles it pre: ({ children }) => <>{children}, - // Headings — include id for in-page anchor navigation + // Headings: include id for in-page anchor navigation h1: ({ children }) => { const id = slugify(getTextFromChildren(children)) return ( @@ -122,30 +143,38 @@ export const MarkdownRenderer: FC = memo( }, // Paragraphs - p: ({ children }) => ( - -

{children}

-
- ), + p: ({ children }) => + isAnswer ? ( +

{children}

+ ) : ( + +

{children}

+
+ ), // Lists ul: ({ children }) => (
    {children}
), - ol: ({ children }) => ( -
    {children}
+ ol: ({ children, start }) => ( +
    + {children} +
), - li: ({ children }) => ( - + li: ({ children }) => + isAnswer ? (
  • {children}
  • -
    - ), + ) : ( + +
  • {children}
  • +
    + ), - // Links — anchor hrefs scroll in-page; external hrefs open new tabs + // Links: anchor hrefs scroll in-page; external hrefs open new tabs a: ({ href, children }) => { if (href?.startsWith('#')) { return ( @@ -169,14 +198,14 @@ export const MarkdownRenderer: FC = memo( ) }, - // Images — resolve durable artifact:// refs to the content endpoint and render + // Images: resolve durable artifact:// refs to the content endpoint and render // as a captioned figure; pass other images through with responsive styling. img: ({ src, alt }) => { const rawSrc = typeof src === 'string' ? src : '' const resolved = isArtifactRef(rawSrc) ? resolveArtifactUrl(rawSrc, artifactJobId) : rawSrc - // An unresolved artifact ref (no job id) would be a broken image — skip it. + // An unresolved artifact ref (no job id) would be a broken image, so skip it. if (!resolved || isArtifactRef(resolved)) return null const caption = alt ?? '' return ( @@ -235,12 +264,23 @@ export const MarkdownRenderer: FC = memo( ), }), - [compact, artifactJobId] + [compact, isAnswer, sources, artifactJobId] ) return ( -
    *:last-child]:mb-0 ${className}`}> - +
    *:last-child]:mb-0', + isAnswer && 'answer-prose', + className, + )} + > + {content}
    diff --git a/frontends/ui/src/shared/components/MarkdownRenderer/rehype-citations.spec.ts b/frontends/ui/src/shared/components/MarkdownRenderer/rehype-citations.spec.ts new file mode 100644 index 000000000..7d3de32f9 --- /dev/null +++ b/frontends/ui/src/shared/components/MarkdownRenderer/rehype-citations.spec.ts @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, test } from 'vitest' +import type { Root } from 'hast' +import { rehypeCitations } from './rehype-citations' + +function paragraph(text: string): Root { + return { + type: 'root', + children: [{ type: 'element', tagName: 'p', properties: {}, children: [{ type: 'text', value: text }] }], + } +} + +function runPlugin(tree: Root): Root { + rehypeCitations()(tree) + return tree +} + +describe('rehypeCitations', () => { + test('splits a [n] marker into text + cite + text', () => { + const tree = runPlugin(paragraph('Users total 11,463 [1] today.')) + const p = tree.children[0] as { + children: Array<{ type: string; tagName?: string; value?: string; children?: Array<{ value: string }> }> + } + expect(p.children.map((c) => c.tagName ?? c.value)).toEqual(['Users total 11,463 ', 'cite', ' today.']) + expect(p.children[1].children?.[0].value).toBe('1') + }) + + test('handles multiple markers', () => { + const tree = runPlugin(paragraph('A [1] and B [12].')) + const p = tree.children[0] as { children: Array<{ tagName?: string }> } + expect(p.children.filter((c) => c.tagName === 'cite')).toHaveLength(2) + }) + + test('leaves text without markers untouched', () => { + const tree = runPlugin(paragraph('No citations here.')) + const p = tree.children[0] as { children: Array<{ value?: string }> } + expect(p.children).toHaveLength(1) + expect(p.children[0].value).toBe('No citations here.') + }) +}) diff --git a/frontends/ui/src/shared/components/MarkdownRenderer/rehype-citations.ts b/frontends/ui/src/shared/components/MarkdownRenderer/rehype-citations.ts new file mode 100644 index 000000000..ff771501a --- /dev/null +++ b/frontends/ui/src/shared/components/MarkdownRenderer/rehype-citations.ts @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { visit } from 'unist-util-visit' +import type { Element, Root, Text } from 'hast' + +const CITATION_RE = /\[(\d{1,3})\]/g + +/** + * Rehype plugin that rewrites inline `[n]` markers in text into `` elements + * whose sole child is the number. The renderer maps `` to a citation chip; + * the number is read back from the child so it survives react-markdown's + * prop handling without relying on custom attributes. + */ +export function rehypeCitations() { + return (tree: Root): void => { + visit(tree, 'text', (node: Text, index, parent) => { + if (parent == null || index == null || !node.value.includes('[')) return undefined + + const replacements: Array = [] + let lastIndex = 0 + let match: RegExpExecArray | null + CITATION_RE.lastIndex = 0 + while ((match = CITATION_RE.exec(node.value)) !== null) { + if (match.index > lastIndex) { + replacements.push({ type: 'text', value: node.value.slice(lastIndex, match.index) }) + } + replacements.push({ + type: 'element', + tagName: 'cite', + properties: {}, + children: [{ type: 'text', value: match[1] }], + }) + lastIndex = CITATION_RE.lastIndex + } + + if (replacements.length === 0) return undefined + if (lastIndex < node.value.length) { + replacements.push({ type: 'text', value: node.value.slice(lastIndex) }) + } + + parent.children.splice(index, 1, ...replacements) + return index + replacements.length + }) + } +} diff --git a/frontends/ui/src/shared/components/MarkdownRenderer/types.ts b/frontends/ui/src/shared/components/MarkdownRenderer/types.ts index 68f00074d..68a985b45 100644 --- a/frontends/ui/src/shared/components/MarkdownRenderer/types.ts +++ b/frontends/ui/src/shared/components/MarkdownRenderer/types.ts @@ -1,6 +1,8 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import type { SourceRef } from '@/shared/components/Sources/types' + export interface MarkdownRendererProps { /** Markdown content to render */ content: string @@ -15,6 +17,14 @@ export interface MarkdownRendererProps { * When omitted, artifact images are not resolved (rendered as-is). */ artifactJobId?: string + /** + * Reading mode. `answer` applies the generous answer-prose scale and renders + * inline `[n]` markers as citation chips when {@link MarkdownRendererProps.sources} + * are provided. Defaults to the existing KUI-`Text` rendering. + */ + variant?: 'default' | 'answer' + /** Cited sources used to resolve inline `[n]` citation chips (answer variant). */ + sources?: SourceRef[] } /** Supported languages for syntax highlighting */ diff --git a/frontends/ui/src/shared/components/Mermaid/MermaidBlock.spec.tsx b/frontends/ui/src/shared/components/Mermaid/MermaidBlock.spec.tsx new file mode 100644 index 000000000..d77aa2b5f --- /dev/null +++ b/frontends/ui/src/shared/components/Mermaid/MermaidBlock.spec.tsx @@ -0,0 +1,112 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { render, screen } from '@/test-utils' +import userEvent from '@testing-library/user-event' +import { vi, describe, test, expect, beforeEach, afterEach } from 'vitest' +import { MermaidBlock } from './MermaidBlock' +import { MarkdownRenderer } from '@/shared/components/MarkdownRenderer' + +const { initializeMock, renderMock } = vi.hoisted(() => ({ + initializeMock: vi.fn(), + renderMock: vi.fn(async () => ({ svg: '' })), +})) + +vi.mock('mermaid', () => ({ default: { initialize: initializeMock, render: renderMock } })) + +describe('MermaidBlock', () => { + beforeEach(() => { + initializeMock.mockClear() + renderMock.mockClear() + renderMock.mockResolvedValue({ svg: '' }) + }) + + afterEach(() => { + document.documentElement.classList.remove('nv-dark', 'nv-light') + }) + + test('renders the diagram SVG and not the fallback', async () => { + render(FALLBACK
    } />) + + const diagram = await screen.findByRole('img', { name: 'Diagram' }) + expect(diagram.innerHTML).toContain(' { + renderMock.mockRejectedValueOnce(new Error('parse error')) + render(FALLBACK} />) + + expect(await screen.findByText('FALLBACK')).toBeInTheDocument() + expect(screen.queryByRole('img', { name: 'Diagram' })).not.toBeInTheDocument() + }) + + test('initializes with the light theme by default', async () => { + render(} />) + await screen.findByRole('img', { name: 'Diagram' }) + + expect(initializeMock).toHaveBeenCalledWith( + expect.objectContaining({ theme: 'default', securityLevel: 'strict' }) + ) + }) + + test('initializes with the dark theme when the app is in dark mode', async () => { + document.documentElement.classList.add('nv-dark') + render(} />) + await screen.findByRole('img', { name: 'Diagram' }) + + expect(initializeMock).toHaveBeenCalledWith(expect.objectContaining({ theme: 'dark' })) + }) + + test('shows zoom controls and zooms in for a sized diagram', async () => { + renderMock.mockResolvedValueOnce({ svg: '' }) + render(} />) + await screen.findByRole('img', { name: 'Diagram' }) + + expect(screen.getByLabelText('Zoom in')).toBeInTheDocument() + expect(screen.getByLabelText('Reset zoom')).toHaveTextContent('100%') + + await userEvent.click(screen.getByLabelText('Zoom in')) + expect(screen.getByLabelText('Reset zoom')).toHaveTextContent('125%') + + await userEvent.click(screen.getByLabelText('Zoom out')) + expect(screen.getByLabelText('Reset zoom')).toHaveTextContent('100%') + }) + + test('expands to fullscreen and exits via the toggle and Escape', async () => { + renderMock.mockResolvedValueOnce({ svg: '' }) + render(} />) + await screen.findByRole('img', { name: 'Diagram' }) + + await userEvent.click(screen.getByLabelText('Expand to fullscreen')) + expect(screen.getByLabelText('Exit fullscreen')).toBeInTheDocument() + + await userEvent.keyboard('{Escape}') + expect(screen.getByLabelText('Expand to fullscreen')).toBeInTheDocument() + }) + + test('omits zoom controls when the diagram has no viewBox', async () => { + renderMock.mockResolvedValueOnce({ svg: '' }) + render(} />) + await screen.findByRole('img', { name: 'Diagram' }) + + expect(screen.queryByLabelText('Zoom in')).not.toBeInTheDocument() + }) +}) + +describe('MarkdownRenderer mermaid routing', () => { + beforeEach(() => { + renderMock.mockResolvedValue({ svg: '' }) + }) + + test('renders a ```mermaid fenced block as a diagram', async () => { + render() + expect(await screen.findByRole('img', { name: 'Diagram' })).toBeInTheDocument() + }) + + test('does not treat a non-mermaid code block as a diagram', () => { + render() + expect(screen.queryByRole('img', { name: 'Diagram' })).not.toBeInTheDocument() + }) +}) diff --git a/frontends/ui/src/shared/components/Mermaid/MermaidBlock.tsx b/frontends/ui/src/shared/components/Mermaid/MermaidBlock.tsx new file mode 100644 index 000000000..d1da690e6 --- /dev/null +++ b/frontends/ui/src/shared/components/Mermaid/MermaidBlock.tsx @@ -0,0 +1,257 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +'use client' + +import { + type FC, + type PointerEvent as ReactPointerEvent, + type ReactNode, + useCallback, + useEffect, + useId, + useLayoutEffect, + useRef, + useState, +} from 'react' + +interface MermaidBlockProps { + code: string + fallback: ReactNode +} + +const MIN_SCALE = 0.25 +const MAX_SCALE = 5 +const ZOOM_STEP = 0.25 + +const clamp = (v: number): number => Math.min(MAX_SCALE, Math.max(MIN_SCALE, v)) + +const isDarkTheme = (): boolean => + typeof document !== 'undefined' && document.documentElement.classList.contains('nv-dark') + +/** + * Renders a fenced ```mermaid block (e.g. the schema ER diagram) as an SVG diagram in the chat. + * + * Mermaid is heavy and DOM-bound, so it is dynamically imported on the client and only when a diagram is + * actually present. Wide schemas with many tables are unreadable when squeezed to the container width, so the + * diagram opens fit-to-width and is then zoomable (buttons, wheel-to-cursor) and pan-able (drag / scroll). The + * theme tracks the app's light/dark mode, and any parse or render failure falls back to the raw code block. + */ +export const MermaidBlock: FC = ({ code, fallback }) => { + const renderId = `mermaid-${useId().replace(/[^a-zA-Z0-9_-]/g, '')}` + const [svg, setSvg] = useState(null) + const [failed, setFailed] = useState(false) + const [dark, setDark] = useState(isDarkTheme) + + const [base, setBase] = useState<{ w: number; h: number } | null>(null) + const [scale, setScale] = useState(1) + const [fullscreen, setFullscreen] = useState(false) + const scrollRef = useRef(null) + const dragRef = useRef<{ x: number; y: number; left: number; top: number } | null>(null) + const anchorRef = useRef<{ ratio: number; contentX: number; contentY: number; cx: number; cy: number } | null>(null) + const scaleRef = useRef(scale) + scaleRef.current = scale + + useEffect(() => { + const observer = new MutationObserver(() => setDark(isDarkTheme())) + observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }) + return () => observer.disconnect() + }, []) + + useEffect(() => { + let active = true + setFailed(false) + + void (async () => { + try { + const mermaid = (await import('mermaid')).default + mermaid.initialize({ + startOnLoad: false, + securityLevel: 'strict', + theme: dark ? 'dark' : 'default', + er: { useMaxWidth: true }, + themeVariables: { fontFamily: 'ui-sans-serif, system-ui, sans-serif' }, + }) + const result = await mermaid.render(renderId, code) + if (active) setSvg(result.svg) + } catch { + if (active) { + setSvg(null) + setFailed(true) + } + } + })() + + return () => { + active = false + } + }, [code, dark, renderId]) + + useEffect(() => { + if (!svg) { + setBase(null) + return + } + const m = svg.match(/viewBox="0 0 ([\d.]+) ([\d.]+)"/) + setBase(m ? { w: parseFloat(m[1]), h: parseFloat(m[2]) } : null) + }, [svg]) + + const fit = useCallback(() => { + const el = scrollRef.current + if (!el || !base) return + const avail = el.clientWidth - 32 + if (avail > 0) setScale(clamp(avail / base.w)) + }, [base]) + + useEffect(() => { + fit() + }, [fit]) + + useEffect(() => { + fit() + }, [fullscreen, fit]) + + useEffect(() => { + if (!fullscreen) return + const onKey = (e: KeyboardEvent): void => { + if (e.key === 'Escape') setFullscreen(false) + } + window.addEventListener('keydown', onKey) + return () => window.removeEventListener('keydown', onKey) + }, [fullscreen]) + + useLayoutEffect(() => { + const a = anchorRef.current + const el = scrollRef.current + if (!a || !el) return + el.scrollLeft = a.contentX * a.ratio - a.cx + el.scrollTop = a.contentY * a.ratio - a.cy + anchorRef.current = null + }, [scale]) + + useEffect(() => { + const el = scrollRef.current + if (!el || !base) return + const onWheel = (e: WheelEvent): void => { + e.preventDefault() + const rect = el.getBoundingClientRect() + const cx = e.clientX - rect.left + const cy = e.clientY - rect.top + const prev = scaleRef.current + const next = clamp(prev * Math.exp(-e.deltaY * 0.0015)) + if (next === prev) return + anchorRef.current = { ratio: next / prev, contentX: el.scrollLeft + cx, contentY: el.scrollTop + cy, cx, cy } + setScale(next) + } + el.addEventListener('wheel', onWheel, { passive: false }) + return () => el.removeEventListener('wheel', onWheel) + }, [base]) + + const onPointerDown = (e: ReactPointerEvent): void => { + if (!base || e.button !== 0) return + const el = scrollRef.current + if (!el) return + dragRef.current = { x: e.clientX, y: e.clientY, left: el.scrollLeft, top: el.scrollTop } + el.setPointerCapture(e.pointerId) + } + const onPointerMove = (e: ReactPointerEvent): void => { + const d = dragRef.current + const el = scrollRef.current + if (!d || !el) return + el.scrollLeft = d.left - (e.clientX - d.x) + el.scrollTop = d.top - (e.clientY - d.y) + } + const endDrag = (): void => { + dragRef.current = null + } + + if (failed) return <>{fallback} + + if (!svg) { + return ( +
    + Rendering diagram… +
    + ) + } + + const zoomable = base !== null + + return ( +
    + {zoomable && ( +
    + + + + +
    + )} +
    +
    +
    +
    + ) +} diff --git a/frontends/ui/src/shared/components/Mermaid/index.ts b/frontends/ui/src/shared/components/Mermaid/index.ts new file mode 100644 index 000000000..da30635fe --- /dev/null +++ b/frontends/ui/src/shared/components/Mermaid/index.ts @@ -0,0 +1,4 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +export { MermaidBlock } from './MermaidBlock' diff --git a/frontends/ui/src/shared/components/Sources/SourceKindIcon.tsx b/frontends/ui/src/shared/components/Sources/SourceKindIcon.tsx new file mode 100644 index 000000000..4f99d0653 --- /dev/null +++ b/frontends/ui/src/shared/components/Sources/SourceKindIcon.tsx @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import type { ReactNode } from 'react' +import type { SourceKind } from './types' + +interface Props { + kind: SourceKind + className?: string +} + +/** + * Compact inline icon for a source's origin. Uses `currentColor` so callers + * control the accent. Self-contained SVGs to keep source typing free of external + * icon dependencies. + */ +export function SourceKindIcon({ kind, className = 'h-3.5 w-3.5' }: Props): ReactNode { + const common = { className, viewBox: '0 0 16 16', fill: 'none', stroke: 'currentColor', strokeWidth: 1.4 } + if (kind === 'web') { + return ( + + ) + } + return ( + + ) +} diff --git a/frontends/ui/src/shared/components/Sources/SourceList.tsx b/frontends/ui/src/shared/components/Sources/SourceList.tsx new file mode 100644 index 000000000..23bc3f55b --- /dev/null +++ b/frontends/ui/src/shared/components/Sources/SourceList.tsx @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import type { ReactNode } from 'react' +import { cn } from '@/shared/lib/cn' +import { SourceKindIcon } from './SourceKindIcon' +import { prettyDomain } from './source-utils' +import type { SourceRef } from './types' + +interface SourceListProps { + sources: SourceRef[] + className?: string + title?: string +} + +/** + * Compact, vertical references list for an agent answer: one {@link SourceRef} + * per line (origin icon, superscript-style index, label, and an optional domain + * link for web sources), rendered smaller than the body text so sources stay + * present but subordinate to the answer. + */ +export function SourceList({ sources, className, title = 'Sources' }: SourceListProps): ReactNode { + if (sources.length === 0) return null + + return ( +
    +

    {title}

    +
      + {sources.map((s, i) => { + const domain = s.url ? prettyDomain(s.url) : '' + const showDomain = Boolean(domain) && domain !== s.label + return ( +
    • + + {i + 1} + + + + + {s.label} + {s.url && showDomain && ( + + {domain} + + )} +
    • + ) + })} +
    +
    + ) +} diff --git a/frontends/ui/src/shared/components/Sources/SourceStrip.spec.tsx b/frontends/ui/src/shared/components/Sources/SourceStrip.spec.tsx new file mode 100644 index 000000000..2d61f99fb --- /dev/null +++ b/frontends/ui/src/shared/components/Sources/SourceStrip.spec.tsx @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { fireEvent, render, screen } from '@testing-library/react' +import { describe, expect, test } from 'vitest' +import { SourceStrip } from './SourceStrip' +import type { SourceRef } from './types' + +function makeSources(n: number): SourceRef[] { + return Array.from({ length: n }, (_, i) => ({ + id: `s${i}`, + title: `Source ${i}`, + kind: 'web' as const, + label: `site${i}.com`, + url: `https://site${i}.com`, + })) +} + +describe('SourceStrip', () => { + test('renders nothing when there are no sources', () => { + const { container } = render() + expect(container).toBeEmptyDOMElement() + }) + + test('renders a card per source when under the preview count', () => { + render() + expect(screen.getByText('Source 0')).toBeInTheDocument() + expect(screen.getByText('Source 2')).toBeInTheDocument() + expect(screen.queryByText(/more$/)).not.toBeInTheDocument() + }) + + test('collapses overflow behind a "+N more" expander', () => { + render() + expect(screen.queryByText('Source 5')).not.toBeInTheDocument() + const more = screen.getByText(/\+3 more/) + fireEvent.click(more) + expect(screen.getByText('Source 5')).toBeInTheDocument() + }) + + test('links cards that have a url', () => { + render() + expect(screen.getByRole('link')).toHaveAttribute('href', 'https://site0.com') + }) +}) diff --git a/frontends/ui/src/shared/components/Sources/SourceStrip.tsx b/frontends/ui/src/shared/components/Sources/SourceStrip.tsx new file mode 100644 index 000000000..1d73a277c --- /dev/null +++ b/frontends/ui/src/shared/components/Sources/SourceStrip.tsx @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +'use client' + +import { type ReactNode, useState } from 'react' +import { Card } from '@/shared/components/Surface/Card' +import { cn } from '@/shared/lib/cn' +import { SourceKindIcon } from './SourceKindIcon' +import type { SourceRef } from './types' + +interface SourceCardProps { + source: SourceRef + index: number +} + +/** + * One source rendered as a typed card: title, an origin icon + label, and the + * citation index. Becomes a link when the source has a real URL. + */ +export function SourceCard({ source, index }: SourceCardProps): ReactNode { + const body = ( + +

    {source.title}

    +
    + + + {source.label} + + +
    +
    + ) + if (source.url) { + return ( + + {body} + + ) + } + return body +} + +interface SourceStripProps { + sources: SourceRef[] + /** How many cards to show before the "+N more" expander. */ + previewCount?: number + className?: string +} + +/** + * Answer-attached strip of typed source cards. Shows a preview, then expands in + * place to reveal the rest, keeping sources present but subordinate to the answer. + */ +export function SourceStrip({ sources, previewCount = 4, className }: SourceStripProps): ReactNode { + const [expanded, setExpanded] = useState(false) + if (sources.length === 0) return null + + const hasMore = sources.length > previewCount + const visible = expanded || !hasMore ? sources : sources.slice(0, previewCount - 1) + + return ( +
    +

    + Sources +

    +
    + {visible.map((s, i) => ( + + ))} + {hasMore && !expanded && ( + + )} +
    +
    + ) +} diff --git a/frontends/ui/src/shared/components/Sources/parse-references.spec.ts b/frontends/ui/src/shared/components/Sources/parse-references.spec.ts new file mode 100644 index 000000000..3b2ab66eb --- /dev/null +++ b/frontends/ui/src/shared/components/Sources/parse-references.spec.ts @@ -0,0 +1,203 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, test } from 'vitest' +import { + splitReferences, + tabularizeEntityLines, + renumberOrderedLists, + nestBulletsUnderOrderedItems, + stripTrailingReferences, + extractSchemaTables, +} from './parse-references' + +describe('extractSchemaTables', () => { + const diagram = [ + 'Here is the schema:', + '', + '```mermaid', + 'erDiagram', + ' customers ||--o{ sales_orders : places', + ' sales_orders ||--|{ order_lines : contains', + ' customers {', + ' string customer_id', + ' string segment', + ' }', + ' sales_orders {', + ' string order_id', + ' }', + ' order_lines {', + ' string line_id', + ' }', + '```', + ].join('\n') + + test('pulls table names from entity blocks and relationships, sorted and de-duped', () => { + expect(extractSchemaTables(diagram)).toEqual(['customers', 'order_lines', 'sales_orders']) + }) + + test('returns empty for content with no mermaid ER diagram', () => { + expect(extractSchemaTables('just a normal answer with no diagram')).toEqual([]) + expect(extractSchemaTables('```mermaid\nflowchart TD\n A --> B\n```')).toEqual([]) + }) +}) + +describe('nestBulletsUnderOrderedItems', () => { + test('indents bullets that follow a numbered item so they nest under it', () => { + const md = '1. Top drivers:\n- Schumacher: 91\n- Prost: 51' + expect(nestBulletsUnderOrderedItems(md)).toBe('1. Top drivers:\n - Schumacher: 91\n - Prost: 51') + }) + + test('leaves top-level bullets with no preceding numbered item alone', () => { + const md = '- a\n- b' + expect(nestBulletsUnderOrderedItems(md)).toBe(md) + }) + + test('stops nesting at a heading', () => { + const md = '1. x\n- under x\n## Section\n- top level' + expect(nestBulletsUnderOrderedItems(md)).toBe('1. x\n - under x\n## Section\n- top level') + }) + + test('re-indents an under-indented (1-2 space) bullet to nest under the item', () => { + const md = '2. Categories:\n - Garment: $1\n - Swimwear: $2' + expect(nestBulletsUnderOrderedItems(md)).toBe('2. Categories:\n - Garment: $1\n - Swimwear: $2') + }) + + test('leaves an already-nested (3+ space) bullet untouched', () => { + const md = '2. Categories:\n - Garment: $1\n - deeper' + expect(nestBulletsUnderOrderedItems(md)).toBe(md) + }) +}) + +describe('stripTrailingReferences', () => { + test('strips a trailing `## Sources` section ([N] Title: URL format)', () => { + const md = + 'The report is complete [1].\n\n## Sources\n[1] Q4 report: https://x.com/r\n[2] Internal notes: knowledge_search' + expect(stripTrailingReferences(md)).toBe('The report is complete [1].') + }) + + test('strips a `### References` heading section', () => { + const md = 'Body text [1].\n\n### References\n[1] fleet_overview.pdf, p.4' + expect(stripTrailingReferences(md)).toBe('Body text [1].') + }) + + test('strips a bold `**Sources**` label section', () => { + const md = 'Body.\n\n**Sources**\n[1] thing - https://a.example.com' + expect(stripTrailingReferences(md)).toBe('Body.') + }) + + test('strips even when the listed lines are not [N]-parseable', () => { + const md = 'Body.\n\n## Sources\n1 Internal KB: schema\n7 Internal KB: knowledge_search' + expect(stripTrailingReferences(md)).toBe('Body.') + }) + + test('leaves a body with no references section unchanged', () => { + const md = 'Just a report.\n\n## Conclusion\nAll good.' + expect(stripTrailingReferences(md)).toBe(md) + }) +}) + +describe('renumberOrderedLists', () => { + test('renumbers top-level items that all restart at 1', () => { + const md = '1. Counts:\n- a: 1\n- b: 2\n1. Top drivers:\n- x\n1. Summary:' + expect(renumberOrderedLists(md)).toBe('1. Counts:\n- a: 1\n- b: 2\n2. Top drivers:\n- x\n3. Summary:') + }) + + test('resets numbering at each heading so sections keep 1-based order', () => { + const md = '## A\n1. one\n1. two\n## B\n1. three' + expect(renumberOrderedLists(md)).toBe('## A\n1. one\n2. two\n## B\n1. three') + }) + + test('leaves already-sequential lists and nested items untouched', () => { + const md = '1. one\n2. two\n 1. nested' + expect(renumberOrderedLists(md)).toBe(md) + }) +}) + +describe('splitReferences', () => { + test('returns the body unchanged when there is no references block', () => { + const content = 'The fleet has 11,463 GPUs across 12 clusters.' + const { body, sources } = splitReferences(content) + expect(body).toBe(content) + expect(sources).toEqual([]) + }) + + test('strips a **References:** block and parses web sources', () => { + const content = + 'NVIDIA shipped record volume [1].\n\n**References:**\n- [1] NVIDIA Q4 results - https://www.nvidia.com/news\n- [2] Industry brief - https://docs.example.ai/report' + const { body, sources } = splitReferences(content) + expect(body).toBe('NVIDIA shipped record volume [1].') + expect(sources).toHaveLength(2) + expect(sources[0]).toMatchObject({ + title: 'NVIDIA Q4 results', + url: 'https://www.nvidia.com/news', + kind: 'web', + label: 'nvidia.com', + }) + expect(sources[1]).toMatchObject({ url: 'https://docs.example.ai/report', kind: 'web', label: 'docs.example.ai' }) + }) + + test('parses file-with-page and bare tool references as documents', () => { + const content = + 'See attached docs [1] and the search [2].\n\n## Sources\n- [1] fleet_overview.pdf, p.4\n- [2] knowledge_search' + const { body, sources } = splitReferences(content) + expect(body).toBe('See attached docs [1] and the search [2].') + expect(sources[0]).toMatchObject({ kind: 'doc', title: 'fleet_overview.pdf, p.4' }) + expect(sources[1]).toMatchObject({ kind: 'doc', title: 'knowledge_search', label: 'knowledge_search' }) + }) + + test('orders sources by their [N] index regardless of line order', () => { + const content = + 'Body.\n\n**References**\n- [2] Second - https://b.example.com\n- [1] First - https://a.example.com' + const { sources } = splitReferences(content) + expect(sources.map((s) => s.title)).toEqual(['First', 'Second']) + }) + + test('leaves content intact when the block has no parseable lines', () => { + const content = 'Body.\n\n**References:**\nnothing structured here' + const { body, sources } = splitReferences(content) + expect(body).toBe(content) + expect(sources).toEqual([]) + }) +}) + +describe('tabularizeEntityLines', () => { + test('reflows 2+ consecutive label: value lines into a GFM table', () => { + const body = 'Top teams by job count:\n\nteam_051: ~11,285 jobs\nteam_009: ~8,210 jobs\nteam_044: ~6,140 jobs' + const out = tabularizeEntityLines(body) + expect(out).toContain('| Item | Value |') + expect(out).toContain('| --- | --- |') + expect(out).toContain('| team_051 | ~11,285 jobs |') + expect(out).toContain('| team_044 | ~6,140 jobs |') + }) + + test('leaves prose untouched', () => { + const body = 'The result is clear: the fleet is healthy. Utilization rose this quarter.' + expect(tabularizeEntityLines(body)).toBe(body) + }) + + test('does not tabularize a single label: value line', () => { + const body = 'Summary: the fleet is healthy' + expect(tabularizeEntityLines(body)).toBe(body) + }) + + test('does not touch markdown list items', () => { + const body = '- team_051: ~11,285 jobs\n- team_009: ~8,210 jobs' + expect(tabularizeEntityLines(body)).toBe(body) + }) + + test('copies fenced ```chart blocks through verbatim (no table from JSON)', () => { + const body = + 'Distribution by model:\n\n```chart\n{\n "type": "bar",\n "series": [{ "key": "count" }],\n "data": [{ "model": "H100", "count": 5120 }]\n}\n```\n\nH100 leads.' + const out = tabularizeEntityLines(body) + expect(out).toBe(body) + expect(out).not.toContain('| Item | Value |') + expect(out).toContain('"count": 5120') + }) + + test('renumber + nest-bullets leave fenced content untouched', () => { + const body = '1. one\n```chart\n1. not-a-list-item\n- not-a-bullet\n```\n1. two' + expect(renumberOrderedLists(body)).toBe('1. one\n```chart\n1. not-a-list-item\n- not-a-bullet\n```\n2. two') + expect(nestBulletsUnderOrderedItems(body)).toBe(body) + }) +}) diff --git a/frontends/ui/src/shared/components/Sources/parse-references.ts b/frontends/ui/src/shared/components/Sources/parse-references.ts new file mode 100644 index 000000000..65f2ae747 --- /dev/null +++ b/frontends/ui/src/shared/components/Sources/parse-references.ts @@ -0,0 +1,295 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { inferSourceKind, prettyDomain, sourceLabel } from './source-utils' +import type { SourceRef } from './types' + +/** + * Matches a trailing references block baked into a response by the backend, e.g. + * `\n\n**References:**\n- [1] Title - https://...`. Captures everything after the + * heading so each `- [N] ...` line can be parsed into a {@link SourceRef}. + */ +const REFERENCES_BLOCK_RE = + /\n{1,2}(?:\*\*References:?\*\*|#{2,3}\s+(?:References|Sources))\s*\n([\s\S]*)$/i + +/** A single `- [N] ...` reference line. */ +const REFERENCE_LINE_RE = /^\s*[-*]?\s*\[(\d{1,3})\]\s*(.+?)\s*$/ + +/** A trailing ` - URL` (web source) on a reference line. */ +const TRAILING_URL_RE = /^(.*?)\s*[-\u2013\u2014]\s*(https?:\/\/\S+)\s*$/i + +/** A `filename, p.N` / `filename p. N` file-with-page tail. */ +const FILE_PAGE_RE = /,?\s*p\.?\s*\d+\s*$/i + +export interface SplitReferencesResult { + /** The response body with the trailing references block removed. */ + body: string + /** Sources parsed from the references block, ordered by their `[N]` index. */ + sources: SourceRef[] +} + +/** + * Parse one `- [N] ...` reference line into a {@link SourceRef}. Supports three + * baked shapes: `Title - URL` (web), `filename, p.X` (document) and a bare + * `tool name`. The kind/label are inferred from the URL when present, else the + * text, reusing {@link inferSourceKind}/{@link sourceLabel}/{@link prettyDomain}. + */ +function parseReferenceLine(n: number, rest: string): SourceRef { + const urlMatch = rest.match(TRAILING_URL_RE) + if (urlMatch) { + const title = urlMatch[1].trim() + const url = urlMatch[2].trim() + const kind = inferSourceKind(url) + return { + id: `ref-${n}`, + title: title || prettyDomain(url) || url, + url, + kind, + label: sourceLabel(url, kind), + } + } + + const ref = rest.trim() + const kind = inferSourceKind(ref) + const isFile = FILE_PAGE_RE.test(ref) || kind === 'doc' + return { + id: `ref-${n}`, + title: ref || `Source ${n}`, + kind: isFile ? 'doc' : kind, + label: isFile ? ref || sourceLabel(ref, 'doc') : sourceLabel(ref, kind), + } +} + +/** + * Split a response into its prose `body` and the structured `sources` parsed + * from a trailing references block (`**References:**` / `## Sources`). When no + * such block exists, `body` is the unchanged input and `sources` is empty. + * Sources are ordered by their `[N]` index. + */ +export function splitReferences(content: string): SplitReferencesResult { + if (!content) return { body: content ?? '', sources: [] } + + const match = content.match(REFERENCES_BLOCK_RE) + if (!match) return { body: content, sources: [] } + + const block = match[1] + const byIndex = new Map() + for (const line of block.split('\n')) { + const lineMatch = line.match(REFERENCE_LINE_RE) + if (!lineMatch) continue + const n = parseInt(lineMatch[1], 10) + if (!Number.isFinite(n) || byIndex.has(n)) continue + byIndex.set(n, parseReferenceLine(n, lineMatch[2])) + } + + if (byIndex.size === 0) return { body: content, sources: [] } + + const sources = [...byIndex.entries()].sort((a, b) => a[0] - b[0]).map(([, s]) => s) + const body = content.slice(0, match.index).replace(/\s+$/, '') + return { body, sources } +} + +/** + * A trailing References/Sources section: a heading of any level (`# Sources` ... + * `###### Sources`) or a bold label (`**Sources**` / `**References:**`), through + * to the end of the document. + */ +const TRAILING_REFERENCES_RE = + /\n{1,2}(?:#{1,6}\s+(?:References|Sources)|\*\*\s*(?:References|Sources)\s*:?\s*\*\*)\s*:?\s*\n[\s\S]*$/i + +/** + * Remove a trailing References/Sources section from a body, unconditionally. + * Unlike {@link splitReferences}, this does not require the listed lines to be + * individually parseable. A report renders its citations as a structured source + * list, so the writer's text `## Sources` block at the end is redundant and is + * stripped here to avoid showing sources twice. + */ +export function stripTrailingReferences(body: string): string { + if (!body) return body + return body.replace(TRAILING_REFERENCES_RE, '').replace(/\s+$/, '') +} + +const HEADING_RE = /^\s{0,3}#{1,6}\s/ +const TOP_ORDERED_RE = /^(\d+)\.(\s)/ +/** A fenced code-block delimiter (```). */ +const FENCE_RE = /^\s*```/ + +/** + * Renumber top-level ordered-list items sequentially. Markdown restarts an + * ordered list whenever a sub-list or block interrupts it, so a model that emits + * `1.` for every section renders as "1. 1. 1." instead of "1. 2. 3.". This walks + * the body and renumbers top-level `N.` items in order, resetting at each heading + * (so distinct sections keep their own 1-based numbering). Indented / nested + * ordered items are left untouched, and already-sequential lists are unchanged. + */ +export function renumberOrderedLists(body: string): string { + if (!body) return body + const lines = body.split('\n') + let counter = 0 + let inFence = false + for (let i = 0; i < lines.length; i++) { + if (FENCE_RE.test(lines[i])) { + inFence = !inFence + continue + } + if (inFence) continue + if (HEADING_RE.test(lines[i])) { + counter = 0 + continue + } + const match = lines[i].match(TOP_ORDERED_RE) + if (match) { + counter += 1 + lines[i] = lines[i].replace(TOP_ORDERED_RE, `${counter}.${match[2]}`) + } + } + return lines.join('\n') +} + +const BULLET_TOP_RE = /^(\s*)[-*+]\s/ + +/** + * Indent bullet lists that follow a top-level ordered item so they render nested + * under it. A model often emits the bullets at (or near) the same left margin as + * the `N.` items, which reads as a flat list. While in an ordered list, an + * under-indented bullet (0-2 leading spaces) is normalized to a 3-space indent + * so CommonMark nests it as a sub-list of the preceding numbered item; bullets + * already indented >= 3 (correctly nested or deeper) are left untouched, and + * top-level bullets with no preceding numbered item are never moved. + */ +export function nestBulletsUnderOrderedItems(body: string): string { + if (!body) return body + const lines = body.split('\n') + let inOrdered = false + let inFence = false + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + if (FENCE_RE.test(line)) { + inFence = !inFence + continue + } + if (inFence) continue + if (HEADING_RE.test(line)) { + inOrdered = false + continue + } + if (TOP_ORDERED_RE.test(line)) { + inOrdered = true + continue + } + const bullet = line.match(BULLET_TOP_RE) + if (bullet) { + if (inOrdered && bullet[1].length < 3) lines[i] = ` ${line.replace(/^\s+/, '')}` + continue + } + if (line.trim() !== '' && !/^\s/.test(line)) inOrdered = false + } + return lines.join('\n') +} + +/** A short `label: value` line with no sentence punctuation. */ +const ENTITY_LINE_RE = /^\s*([^:\n]{1,60}?):\s+(\S.{0,80})$/ +/** Sentence-like punctuation that signals prose, not a metric line. */ +const SENTENCE_PUNCT_RE = /[.!?](\s|$)/ + +function isEntityLine(line: string): boolean { + const m = line.match(ENTITY_LINE_RE) + if (!m) return false + const [, label, value] = m + if (SENTENCE_PUNCT_RE.test(label) || SENTENCE_PUNCT_RE.test(value)) return false + if (label.includes('|') || value.includes('|')) return false + return true +} + +function escapeCell(text: string): string { + return text.trim().replace(/\|/g, '\\|') +} + +/** + * Conservatively reflow runs of 2+ consecutive `label: value` lines (entity / + * metric lists like `team_051: ~11,285 jobs`) into a 2-column GFM table so they + * render as a scannable grid instead of flat lines. Anything that looks like a + * sentence, a list item, or a heading is left untouched. Tightly gated to avoid + * mangling prose. + */ +export function tabularizeEntityLines(body: string): string { + if (!body || !body.includes(':')) return body + + const lines = body.split('\n') + const out: string[] = [] + let i = 0 + while (i < lines.length) { + // Copy fenced code blocks through verbatim so their JSON `"key": value` + // lines are never reflowed into a table. + if (FENCE_RE.test(lines[i])) { + out.push(lines[i]) + i++ + while (i < lines.length && !FENCE_RE.test(lines[i])) { + out.push(lines[i]) + i++ + } + if (i < lines.length) { + out.push(lines[i]) + i++ + } + continue + } + const isMarkup = + /^\s*[-*+]\s/.test(lines[i]) || /^\s*#{1,6}\s/.test(lines[i]) || /^\s*\d+\.\s/.test(lines[i]) + if (!isMarkup && isEntityLine(lines[i])) { + let j = i + while ( + j < lines.length && + !/^\s*[-*+]\s/.test(lines[j]) && + !/^\s*#{1,6}\s/.test(lines[j]) && + !/^\s*\d+\.\s/.test(lines[j]) && + isEntityLine(lines[j]) + ) { + j++ + } + const run = lines.slice(i, j) + if (run.length >= 2) { + out.push('| Item | Value |') + out.push('| --- | --- |') + for (const line of run) { + const m = line.match(ENTITY_LINE_RE) as RegExpMatchArray + out.push(`| ${escapeCell(m[1])} | ${escapeCell(m[2])} |`) + } + i = j + continue + } + } + out.push(lines[i]) + i++ + } + + return out.join('\n') +} + +const MERMAID_FENCE_RE = /```mermaid\s*([\s\S]*?)```/i +const ER_ENTITY_RE = /^[ \t]*([A-Za-z_][\w-]*)\s*\{/gm +const ER_RELATION_RE = /^[ \t]*([A-Za-z_][\w-]*)\s+[|}{o.\-]+\s+([A-Za-z_][\w-]*)\s*:/gm + +/** + * Pulls the table (entity) names out of a fenced mermaid `erDiagram`, from both + * the `TABLE { ... }` definition blocks and the `A ||--o{ B :` relationship + * lines. A schema answer is otherwise just a diagram with no `**References:**` + * block, so {@link splitReferences} finds nothing; these names let the answer + * still surface what it covers. Returns a sorted, de-duped list (empty when the + * content has no ER diagram). + */ +export function extractSchemaTables(content: string): string[] { + const fence = content.match(MERMAID_FENCE_RE) + if (!fence) return [] + const codeBlock = fence[1] + if (!/\berDiagram\b/i.test(codeBlock)) return [] + + const names = new Set() + for (const m of codeBlock.matchAll(ER_ENTITY_RE)) names.add(m[1]) + for (const m of codeBlock.matchAll(ER_RELATION_RE)) { + names.add(m[1]) + names.add(m[2]) + } + names.delete('erDiagram') + return [...names].sort((a, b) => a.localeCompare(b)) +} diff --git a/frontends/ui/src/shared/components/Sources/source-utils.spec.ts b/frontends/ui/src/shared/components/Sources/source-utils.spec.ts new file mode 100644 index 000000000..166af9cc2 --- /dev/null +++ b/frontends/ui/src/shared/components/Sources/source-utils.spec.ts @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, test } from 'vitest' +import { inferSourceKind, mapCitationSource, prettyDomain, sourceLabel } from './source-utils' + +describe('inferSourceKind', () => { + test('classifies web urls as web', () => { + expect(inferSourceKind('https://nvidia.com/blog')).toBe('web') + expect(inferSourceKind('http://docs.example.ai/x')).toBe('web') + }) + test('classifies everything else as doc', () => { + expect(inferSourceKind('knowledge_search')).toBe('doc') + expect(inferSourceKind('fleet_overview.pdf')).toBe('doc') + expect(inferSourceKind('mystery')).toBe('doc') + expect(inferSourceKind(undefined)).toBe('doc') + }) +}) + +describe('prettyDomain', () => { + test('strips protocol and www', () => { + expect(prettyDomain('https://www.nvidia.com/path?q=1')).toBe('nvidia.com') + expect(prettyDomain('http://docs.example.ai/x')).toBe('docs.example.ai') + }) + test('degrades for non-urls', () => { + expect(prettyDomain('knowledge_search')).toBe('knowledge_search') + expect(prettyDomain(undefined)).toBe('') + }) +}) + +describe('sourceLabel', () => { + test('domain for web, document label otherwise', () => { + expect(sourceLabel('https://www.example.com', 'web')).toBe('example.com') + expect(sourceLabel('knowledge_search', 'doc')).toBe('knowledge_search') + expect(sourceLabel(undefined, 'doc')).toBe('Document') + }) +}) + +describe('mapCitationSource', () => { + test('maps a web citation', () => { + const ref = mapCitationSource({ id: 'c1', url: 'https://www.nvidia.com', content: 'NVIDIA news\nmore' }, 0) + expect(ref).toMatchObject({ id: 'c1', kind: 'web', label: 'nvidia.com', title: 'NVIDIA news', url: 'https://www.nvidia.com' }) + expect(ref.snippet).toContain('NVIDIA news') + }) + test('maps a document citation with no real url', () => { + const ref = mapCitationSource({ id: 'c2', url: 'knowledge_search', content: 'There are 11,463 users.' }, 1) + expect(ref.kind).toBe('doc') + expect(ref.label).toBe('knowledge_search') + expect(ref.url).toBeUndefined() + expect(ref.title).toBe('There are 11,463 users.') + }) + test('synthesises an id and title when missing', () => { + const ref = mapCitationSource({ id: '', url: 'knowledge_search', content: '' }, 2) + expect(ref.id).toBe('src-2') + expect(ref.title).toBe('knowledge_search') + }) +}) diff --git a/frontends/ui/src/shared/components/Sources/source-utils.ts b/frontends/ui/src/shared/components/Sources/source-utils.ts new file mode 100644 index 000000000..2005565b2 --- /dev/null +++ b/frontends/ui/src/shared/components/Sources/source-utils.ts @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import type { SourceKind, SourceRef } from './types' + +/** + * Infer the {@link SourceKind} from a source reference (a URL or a file ref). + * Web URLs map to `web`; everything else (files, tool refs, bare text) to `doc`. + */ +export function inferSourceKind(ref: string | undefined): SourceKind { + if (!ref) return 'doc' + if (/^https?:\/\//i.test(ref)) return 'web' + return 'doc' +} + +/** + * Reduce a URL to a clean, scannable domain (`www.` and protocol stripped), + * falling back to the raw ref when it is not a parseable URL. + */ +export function prettyDomain(ref: string | undefined): string { + if (!ref) return '' + try { + return new URL(ref).hostname.replace(/^www\./, '') + } catch { + return ref.replace(/^[a-z]+:\/\//i, '').split('/')[0] + } +} + +/** + * Build the footer label shown on a source card: the domain for web sources, + * a friendly document label otherwise. + */ +export function sourceLabel(ref: string | undefined, kind: SourceKind): string { + if (kind === 'web') return prettyDomain(ref) + return prettyDomain(ref) || 'Document' +} + +/** + * Map a backend {@link CitationSource}-like record into a UI {@link SourceRef}. + * The first non-empty line of `content` becomes the title; the full content is + * kept as the hover snippet. + */ +export function mapCitationSource( + cs: { id: string; url: string; content?: string }, + index: number, +): SourceRef { + const kind = inferSourceKind(cs.url) + const firstLine = (cs.content ?? '').split('\n').map((l) => l.trim()).find(Boolean) + const label = sourceLabel(cs.url, kind) + return { + id: cs.id || `src-${index}`, + title: firstLine || label || cs.url || `Source ${index + 1}`, + url: /^https?:\/\//i.test(cs.url) ? cs.url : undefined, + snippet: cs.content || undefined, + kind, + label: label || `Source ${index + 1}`, + } +} diff --git a/frontends/ui/src/shared/components/Sources/types.ts b/frontends/ui/src/shared/components/Sources/types.ts new file mode 100644 index 000000000..d8b7209c9 --- /dev/null +++ b/frontends/ui/src/shared/components/Sources/types.ts @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +/** + * Origin of a cited source. Drives the icon and accent so users can tell + * document-backed claims from web ones at a glance. + */ +export type SourceKind = 'web' | 'doc' + +/** + * UI-facing representation of a cited source, mapped from the backend's + * {@link CitationSource}. Kept renderer-agnostic so both inline citation chips + * and the source-card strip consume the same shape. + */ +export interface SourceRef { + id: string + title: string + url?: string + snippet?: string + kind: SourceKind + label: string +} diff --git a/frontends/ui/src/shared/components/Surface/Card.spec.tsx b/frontends/ui/src/shared/components/Surface/Card.spec.tsx new file mode 100644 index 000000000..bec976f43 --- /dev/null +++ b/frontends/ui/src/shared/components/Surface/Card.spec.tsx @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { render, screen } from '@testing-library/react' +import { describe, expect, test } from 'vitest' +import { Card } from './Card' + +describe('Card', () => { + test('renders children', () => { + render(hello) + expect(screen.getByText('hello')).toBeInTheDocument() + }) + + test('applies the shared surface and default tone', () => { + const { container } = render(x) + const el = container.firstChild as HTMLElement + expect(el.className).toContain('bg-surface-raised') + expect(el.className).toContain('border-base') + }) + + test('source tones add an accent border without changing the base shape', () => { + const { container } = render(x) + const el = container.firstChild as HTMLElement + expect(el.className).toContain('border-l-[color:var(--text-color-subtle)]') + expect(el.className).toContain('bg-surface-raised') + }) + + test('the doc tone accents the left border', () => { + const { container } = render(x) + const el = container.firstChild as HTMLElement + expect(el.className).toContain('border-l-[color:var(--text-color-base)]') + }) + + test('merges a caller className and forwards arbitrary props', () => { + render( + + x + , + ) + const el = screen.getByTestId('card') + expect(el).toHaveAttribute('role', 'group') + expect(el.className).toContain('p-2') + expect(el.className).not.toContain('p-4') + }) +}) diff --git a/frontends/ui/src/shared/components/Surface/Card.tsx b/frontends/ui/src/shared/components/Surface/Card.tsx new file mode 100644 index 000000000..71a3bd52b --- /dev/null +++ b/frontends/ui/src/shared/components/Surface/Card.tsx @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import type { HTMLAttributes, ReactNode } from 'react' +import { cn } from '@/shared/lib/cn' + +/** + * Visual tone of a {@link Card}. Status tones (`active`/`success`/`error`) and + * source tones (`web`/`doc`) add an accent without changing the base surface, so + * every card in the chat UI shares one shape and radius. + */ +export type CardTone = 'default' | 'active' | 'success' | 'error' | 'web' | 'doc' + +const TONE_CLASS: Record = { + default: 'border-base', + active: 'border-[color:var(--color-brand)]', + success: 'border-[color:var(--color-green-500)]', + error: 'border-[color:var(--text-color-error,#d9534f)]', + web: 'border-base border-l-2 border-l-[color:var(--text-color-subtle)]', + doc: 'border-base border-l-2 border-l-[color:var(--text-color-base)]', +} + +interface CardProps extends HTMLAttributes { + tone?: CardTone +} + +/** + * Single surface primitive used across the chat UI. Replaces the ad-hoc + * `rounded-lg border bg-surface-*` blocks so cards are visually consistent. + */ +export function Card({ tone = 'default', className, children, ...rest }: CardProps): ReactNode { + return ( +
    + {children} +
    + ) +} diff --git a/frontends/ui/src/shared/components/research/StatusDot.tsx b/frontends/ui/src/shared/components/research/StatusDot.tsx new file mode 100644 index 000000000..3a507d284 --- /dev/null +++ b/frontends/ui/src/shared/components/research/StatusDot.tsx @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import type { FC } from 'react' +import { cn } from '@/shared/lib/cn' + +/** + * Lifecycle state of an agent-trace node. Drives the single status atom used by + * both the inline chat trace and the research panel, so a node looks identical + * wherever it appears. + */ +export type NodeState = 'running' | 'done' | 'pending' | 'waiting' | 'interrupted' | 'error' + +/** A single status atom: the only source of truth for every node's look. */ +export const StatusDot: FC<{ state: NodeState; size?: 'sm' | 'xs' }> = ({ state, size = 'sm' }) => ( +