@arbidocs/react 0.5.122 → 0.5.123

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,6 +8,7 @@ import rehypeSlug from "rehype-slug";
8
8
  import { Copy, Check } from "lucide-react";
9
9
  import { cn } from "../lib/utils.js";
10
10
  import { markdownBaseComponents } from "./markdown-base-components.js";
11
+ import { remarkAiCaption } from "./remark-ai-caption.js";
11
12
  import { MarkdownStreamingContext } from "./markdown-streaming-context.js";
12
13
  import { splitMarkdownBlocks } from "./split-markdown-blocks.js";
13
14
  import { closeOpenEmphasis, normalizeCitationMarkup, trimOpenCitation } from "./normalize-citations.js";
@@ -162,11 +163,13 @@ const defaultComponents = {
162
163
  };
163
164
  const markdownSanitizeSchema = {
164
165
  ...defaultSchema,
165
- tagNames: [...defaultSchema.tagNames ?? [], "mark"],
166
+ tagNames: [...defaultSchema.tagNames ?? [], "mark", "aside"],
166
167
  attributes: {
167
168
  ...defaultSchema.attributes,
168
169
  span: [...defaultSchema.attributes?.span ?? [], "className"],
169
- div: [...defaultSchema.attributes?.div ?? [], "className"],
170
+ div: [...defaultSchema.attributes?.div ?? [], "className", "dataTestid"],
171
+ // A figure's generated caption (`remark-ai-caption`): its box and label.
172
+ aside: ["className", "dataAiCaption", "ariaLabel"],
170
173
  code: [...defaultSchema.attributes?.code ?? [], "className"],
171
174
  pre: [...defaultSchema.attributes?.pre ?? [], "className"],
172
175
  ins: [...defaultSchema.attributes?.ins ?? [], "dataCid"],
@@ -211,7 +214,7 @@ const NO_PLUGINS = [];
211
214
  function usePlugins(allowHtml, mathReady, extra) {
212
215
  return useMemo(() => {
213
216
  const math = mathReady && loadedMath ? loadedMath : null;
214
- const remarkPlugins = math ? [remarkGfm, math.remarkMath] : [remarkGfm];
217
+ const remarkPlugins = math ? [remarkGfm, remarkAiCaption, math.remarkMath] : [remarkGfm, remarkAiCaption];
215
218
  const rehypePlugins = [
216
219
  // Text someone else wrote (a Word comment from any .docx): raw HTML stays text.
217
220
  ...allowHtml ? [rehypeRaw, [rehypeSanitize, markdownSanitizeSchema]] : [],
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/ai/MarkdownRenderer.tsx"],
4
- "sourcesContent": ["import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'\nimport Markdown, { type Components, type Options as MarkdownOptions } from 'react-markdown'\nimport remarkGfm from 'remark-gfm'\nimport rehypeRaw from 'rehype-raw'\nimport rehypeSanitize, { defaultSchema, type Options as SanitizeSchema } from 'rehype-sanitize'\nimport rehypeSlug from 'rehype-slug'\nimport { Copy, Check } from 'lucide-react'\nimport { cn } from '../lib/utils'\nimport { markdownBaseComponents } from './markdown-base-components'\nimport { MarkdownStreamingContext } from './markdown-streaming-context'\nimport { splitMarkdownBlocks } from './split-markdown-blocks'\nimport { closeOpenEmphasis, normalizeCitationMarkup, trimOpenCitation } from './normalize-citations'\n\n/** remark/rehype plugin lists, as react-markdown takes them. */\ntype PluggableList = NonNullable<MarkdownOptions['rehypePlugins']>\n\n/**\n * MarkdownRenderer \u2014 the single, full-featured markdown surface for the kit.\n *\n * Renders AI answers, chat/DM messages and parsed-document panes with one house\n * style: headings, paragraphs, bold/italic, ordered/unordered lists, GFM tables,\n * fenced code blocks (monospace + copy button), inline code, blockquotes, safe\n * links (`rel=\"noopener\"`, new tab) and horizontal rules.\n *\n * Styling is 100% semantic Tailwind tokens (`text-foreground`,\n * `text-muted-foreground`, `border-border`, `bg-muted`, `text-primary`) so it\n * themes per firm \u2014 no hardcoded brand colours. It sizes to its container, so it\n * looks right in a compact chat bubble and in a full-width document pane alike.\n *\n * Streaming: the content is split into block-level chunks (see\n * `splitMarkdownBlocks`) and each chunk is memoised, so streaming token updates\n * only re-parse the trailing block instead of the whole message. Pass\n * `isStreaming` to let heavy custom components defer via `MarkdownStreamingContext`.\n *\n * Safety: inline HTML (redlines, `<mark>` highlights, parser tables) is parsed and then sanitised\n * (`markdownSanitizeSchema`), so a model or a document cannot inject scripts, handlers or styles.\n * Headings get slug ids, so `#section` links and bookmarks land.\n *\n * Math: `$\u2026$` and `$$\u2026$$` are typeset with KaTeX. remark-math, rehype-katex and KaTeX's CSS\n * (~90 kB gzip) load only once some content actually contains math. A `$` before a digit is a\n * currency sign, never a formula delimiter.\n *\n * Extending: pass `components` to override any element (the kit's `AiMarkdown`\n * and `CitationRenderer` layer their accent links / citation popovers on top; a host composes\n * `MarkdownLink` / `MarkdownCode` into its own overrides), and `rehypePlugins` for trusted\n * transforms that run after sanitising (e.g. turning entity ids into pills).\n */\n\nconst EDGE_FADE = '1.5rem'\n\n/**\n * A GFM table in its own horizontal scroller. The table keeps a minimum width per column\n * (`min-w-[7rem]` cells, words never broken mid-word), so on a narrow screen it scrolls sideways\n * inside this wrapper instead of squeezing cells to a letter per line. The edge(s) with more\n * content to scroll to fade out, which is the scroll hint. Wide containers are unchanged: the\n * table is `w-full` and nothing overflows.\n */\nfunction MarkdownTable({ children, ...props }: React.ComponentPropsWithoutRef<'table'>) {\n const ref = useRef<HTMLDivElement>(null)\n const [edges, setEdges] = useState({ start: false, end: false })\n const measure = useCallback(() => {\n const el = ref.current\n if (!el) return\n const start = el.scrollLeft > 1\n const end = el.scrollLeft + el.clientWidth < el.scrollWidth - 1\n setEdges((prev) => (prev.start === start && prev.end === end ? prev : { start, end }))\n }, [])\n useEffect(() => {\n const el = ref.current\n if (!el) return\n measure()\n if (typeof ResizeObserver === 'undefined') return\n const observer = new ResizeObserver(measure)\n observer.observe(el)\n if (el.firstElementChild) observer.observe(el.firstElementChild)\n return () => observer.disconnect()\n }, [measure])\n const mask =\n edges.start || edges.end\n ? `linear-gradient(to right, ${edges.start ? 'transparent' : '#000'}, #000 ${EDGE_FADE}, #000 calc(100% - ${EDGE_FADE}), ${edges.end ? 'transparent' : '#000'})`\n : undefined\n return (\n <div\n ref={ref}\n onScroll={measure}\n data-testid=\"markdown-table-scroller\"\n data-scroll-start={edges.start || undefined}\n data-scroll-end={edges.end || undefined}\n className=\"my-3 overflow-x-auto rounded-lg border border-border\"\n style={mask ? { maskImage: mask, WebkitMaskImage: mask } : undefined}\n >\n <table\n data-testid=\"markdown-table\"\n className=\"w-full border-collapse text-sm [overflow-wrap:normal] [word-break:normal]\"\n {...props}\n >\n {children}\n </table>\n </div>\n )\n}\n\n/** Fenced code block with a language label and copy-to-clipboard button. */\nfunction CodeBlock({ language, code }: { language: string; code: string }) {\n const [copied, setCopied] = useState(false)\n\n const handleCopy = async () => {\n try {\n await navigator.clipboard.writeText(code)\n setCopied(true)\n setTimeout(() => setCopied(false), 2000)\n } catch {\n // Clipboard unavailable (insecure context / permissions) \u2014 no-op.\n }\n }\n\n return (\n <div\n className=\"code-block relative my-3 overflow-hidden rounded-md border border-current/20 bg-current/5\"\n data-testid=\"markdown-code-block\"\n >\n <div className=\"flex items-center justify-between border-b border-current/20 px-3 py-1.5\">\n <span className=\"font-mono text-xs text-muted-foreground\">{language || 'code'}</span>\n <button\n type=\"button\"\n onClick={handleCopy}\n className=\"flex items-center gap-1 text-xs text-muted-foreground transition-colors hover:text-foreground\"\n title={copied ? 'Copied!' : 'Copy code'}\n >\n {copied ? <Check size={12} /> : <Copy size={12} />}\n <span>{copied ? 'Copied' : 'Copy'}</span>\n </button>\n </div>\n <pre className=\"overflow-x-auto p-3 text-sm leading-relaxed\">\n <code className={cn('font-mono', language && `language-${language}`)}>{code}</code>\n </pre>\n </div>\n )\n}\n\n/**\n * Links. Internal hash/anchor links stay in-page; everything else opens safely in a new tab\n * (`rel=\"noopener noreferrer\"` prevents reverse-tabnabbing). A backend citation marker\n * (`#cite-N`) has no popover here: the cited words stay in the surface's own colour with a quiet\n * superscript number after them (no underline, so a cited sentence does not read as a link).\n * Pass `renderCitation` to `MarkdownRenderer` to swap in a real source popover.\n */\nexport function MarkdownLink({ href, children, ...props }: React.ComponentPropsWithoutRef<'a'>) {\n if (href?.startsWith('#cite-')) {\n return (\n <span\n className=\"cited-text after:ml-px after:align-super after:text-[max(0.8em,var(--text-xs))] after:font-semibold after:text-muted-foreground after:content-[attr(data-cite)]\"\n data-cite={href.slice(6)}\n >\n {children}\n </span>\n )\n }\n const isInternal = !href || href.startsWith('#') || href.startsWith('cite:')\n return (\n <a\n className=\"text-primary underline underline-offset-2\"\n href={href}\n // A `cite:` reference is not a place to go: the click must not navigate.\n onClick={href?.startsWith('cite:') ? (e) => e.preventDefault() : undefined}\n {...(isInternal ? {} : { target: '_blank', rel: 'noopener noreferrer' })}\n {...props}\n >\n {children}\n </a>\n )\n}\n\n/**\n * Inline code, and fenced code as a `CodeBlock` (language label + copy). Exported so a host's own\n * `code` override can handle its special cases (a diagram language, an id shown as a pill) and\n * hand everything else back here.\n */\nexport function MarkdownCode({\n className,\n children,\n ...props\n}: React.ComponentPropsWithoutRef<'code'>) {\n const match = /language-(\\w+)/.exec(className || '')\n if (!match) {\n return (\n <code\n className=\"rounded bg-current/10 px-1 py-0.5 font-mono text-[0.9em]\"\n data-testid=\"markdown-inline-code\"\n {...props}\n >\n {children}\n </code>\n )\n }\n return <CodeBlock language={match[1]} code={String(children).replace(/\\n$/, '')} />\n}\n\n/**\n * Default component overrides. Spreads the shared brand-neutral base\n * (`markdownBaseComponents`: headings, lists, paragraphs, emphasis, hr\u2026) and adds\n * the elements that carry behaviour or need self-contained styling: safe links,\n * code (inline + fenced), bordered tables and blockquotes. Module-scoped so the\n * reference is stable across renders and react-markdown can skip re-parsing.\n */\nconst defaultComponents: Partial<Components> = {\n ...markdownBaseComponents,\n a: MarkdownLink,\n em: ({ children, ...props }: React.ComponentPropsWithoutRef<'em'>) => (\n <em className=\"italic\" {...props}>\n {children}\n </em>\n ),\n code: MarkdownCode,\n // The CodeBlock renders its own <pre>; unwrap react-markdown's wrapper so we\n // don't nest <pre><pre>.\n pre: ({ children }: React.ComponentPropsWithoutRef<'pre'>) => <>{children}</>,\n blockquote: ({ children, ...props }: React.ComponentPropsWithoutRef<'blockquote'>) => (\n <blockquote className=\"border-l-2 border-border pl-4 italic text-muted-foreground\" {...props}>\n {children}\n </blockquote>\n ),\n table: MarkdownTable,\n thead: ({ children, ...props }: React.ComponentPropsWithoutRef<'thead'>) => (\n <thead className=\"bg-current/10\" {...props}>\n {children}\n </thead>\n ),\n th: ({ children, ...props }: React.ComponentPropsWithoutRef<'th'>) => (\n <th\n className=\"min-w-[7rem] border-b border-border px-3 py-2 text-left font-semibold\"\n {...props}\n >\n {children}\n </th>\n ),\n td: ({ children, ...props }: React.ComponentPropsWithoutRef<'td'>) => (\n <td className=\"min-w-[7rem] border-b border-border px-3 py-2 align-top\" {...props}>\n {children}\n </td>\n ),\n // General emphasis marks (GFM `~~strikethrough~~` \u2192 <del>, raw <ins>/<mark>).\n del: ({ children, ...props }: React.ComponentPropsWithoutRef<'del'>) => (\n <del className=\"text-muted-foreground line-through\" {...props}>\n {children}\n </del>\n ),\n ins: ({ children, ...props }: React.ComponentPropsWithoutRef<'ins'>) => (\n <ins className=\"underline decoration-primary/60\" {...props}>\n {children}\n </ins>\n ),\n mark: ({ children, ...props }: React.ComponentPropsWithoutRef<'mark'>) => (\n <mark className=\"rounded bg-primary/15 px-0.5 text-inherit\" {...props}>\n {children}\n </mark>\n ),\n}\n\n/**\n * What survives sanitising inline HTML: GitHub's schema, plus the classes the downstream steps and\n * renderers read (KaTeX's `math-*` placeholders, `language-*` on code), `<mark>` for search\n * highlights, and `data-cid` on `<ins>`/`<del>` (the redline's per-change id, minted from diff\n * positions, never from content). Scripts, event handlers, inline styles and `javascript:` URLs\n * stay out.\n */\nexport const markdownSanitizeSchema: SanitizeSchema = {\n ...defaultSchema,\n tagNames: [...(defaultSchema.tagNames ?? []), 'mark'],\n attributes: {\n ...defaultSchema.attributes,\n span: [...(defaultSchema.attributes?.span ?? []), 'className'],\n div: [...(defaultSchema.attributes?.div ?? []), 'className'],\n code: [...(defaultSchema.attributes?.code ?? []), 'className'],\n pre: [...(defaultSchema.attributes?.pre ?? []), 'className'],\n ins: [...(defaultSchema.attributes?.ins ?? []), 'dataCid'],\n del: [...(defaultSchema.attributes?.del ?? []), 'dataCid'],\n },\n}\n\n// \u2500\u2500 Lazy KaTeX (math) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Loaded once, module-wide, the first time content with math renders; mounted renderers that\n// were waiting re-render when it lands.\ntype MathPlugins = { remarkMath: PluggableList[number]; rehypeKatex: PluggableList[number] }\nlet loadedMath: MathPlugins | null = null\nlet mathLoadStarted = false\nconst mathSubscribers = new Set<() => void>()\n\nfunction loadMath() {\n if (mathLoadStarted) return\n mathLoadStarted = true\n void Promise.all([\n import('remark-math'),\n import('rehype-katex'),\n import('katex/dist/katex.min.css'),\n ])\n .then(([remarkMath, rehypeKatex]) => {\n loadedMath = { remarkMath: remarkMath.default, rehypeKatex: rehypeKatex.default }\n mathSubscribers.forEach((notify) => notify())\n })\n .catch(() => {\n // Retry on the next math-bearing render if the chunk failed to load.\n mathLoadStarted = false\n })\n}\n\n// remark-math's inline rule: `$\u2026$` with no whitespace just inside, or a `$$\u2026$$` block. A lone\n// price (\"$5,000\") never matches.\nconst MATH_RE = /\\$\\$[\\s\\S]+?\\$\\$|(?<!\\\\)\\$(?![\\s$])(?:\\\\\\$|[^$\\n])+?(?<![\\s\\\\])\\$/\n\n/**\n * A `$` straight before a digit is a currency sign (\"$2,000\", \"\u2248$50,000\"), not the start of inline\n * math: two in one paragraph would otherwise pair up and typeset everything between as a formula.\n * Escape them (outside code, where a backslash would show) so they stay text.\n */\nfunction escapeCurrencyDollars(markdown: string): string {\n return markdown\n .split(/(```[\\s\\S]*?```|`[^`\\n]*`)/)\n .map((part, i) => (i % 2 === 1 ? part : part.replace(/(?<!\\\\)\\$(?=\\d)/g, '\\\\$')))\n .join('')\n}\n\n/** True once the math plugins are loaded for content that contains math. */\nfunction useMathReady(content: string): boolean {\n const hasMath = useMemo(() => MATH_RE.test(content), [content])\n const [, rerender] = useReducer((n: number) => n + 1, 0)\n useEffect(() => {\n if (!hasMath || loadedMath) return\n mathSubscribers.add(rerender)\n loadMath()\n return () => {\n mathSubscribers.delete(rerender)\n }\n }, [hasMath])\n return hasMath && loadedMath !== null\n}\n\nconst NO_PLUGINS: PluggableList = []\n\n/**\n * The remark/rehype pipelines. Raw HTML (when allowed) is parsed and sanitised before the trusted\n * steps (KaTeX, slugs, the host's plugins), so their output is not stripped again. Memoised on\n * its inputs so react-markdown sees stable arrays and memoised blocks do not re-parse.\n */\nfunction usePlugins(allowHtml: boolean, mathReady: boolean, extra: PluggableList) {\n return useMemo(() => {\n const math = mathReady && loadedMath ? loadedMath : null\n const remarkPlugins: PluggableList = math ? [remarkGfm, math.remarkMath] : [remarkGfm]\n const rehypePlugins: PluggableList = [\n // Text someone else wrote (a Word comment from any .docx): raw HTML stays text.\n ...(allowHtml ? [rehypeRaw, [rehypeSanitize, markdownSanitizeSchema]] : []),\n ...(math ? [math.rehypeKatex] : []),\n rehypeSlug,\n ...extra,\n ] as PluggableList\n return { remarkPlugins, rehypePlugins }\n }, [allowHtml, mathReady, extra])\n}\n\n/**\n * One parsed markdown block, memoised on (content, components) so already-rendered\n * blocks don't re-parse when a later block streams new tokens. This is the core\n * of the streaming-rendering optimisation.\n */\nconst MemoizedBlock = React.memo(function MarkdownBlock({\n content,\n components,\n remarkPlugins,\n rehypePlugins,\n}: {\n content: string\n components: Partial<Components>\n remarkPlugins: PluggableList\n rehypePlugins: PluggableList\n}) {\n return (\n <Markdown remarkPlugins={remarkPlugins} rehypePlugins={rehypePlugins} components={components}>\n {content}\n </Markdown>\n )\n})\n\n/** Prevents a malformed-markdown render error from crashing the whole surface. */\nclass MarkdownErrorBoundary extends React.Component<\n { fallback: React.ReactNode; children: React.ReactNode },\n { hasError: boolean }\n> {\n state = { hasError: false }\n static getDerivedStateFromError() {\n return { hasError: true }\n }\n render() {\n return this.state.hasError ? this.props.fallback : this.props.children\n }\n}\n\nexport interface MarkdownRendererProps {\n /** Markdown content to render. */\n children: string\n /** Custom component overrides merged on top of the defaults. */\n components?: Partial<Components>\n /**\n * When true, signals heavy custom components (via `MarkdownStreamingContext`)\n * to defer their work while tokens are still arriving. Defaults to false.\n */\n isStreaming?: boolean\n /**\n * Renders a citation marker `[words](#cite-N)`: `children` are the cited words, `num` the N.\n * Keep it referentially stable (`useCallback`/`useMemo`), or every block re-parses.\n */\n renderCitation?: (citation: { num: string; children: React.ReactNode }) => React.ReactNode\n /** Extra classes on the wrapper (font-size, prose width, etc.). */\n className?: string\n /** Overrides the `data-testid` on the wrapper (default `markdown-renderer`). */\n testId?: string\n /**\n * Inline HTML in the markdown becomes elements, sanitised (default). False keeps it as text: for\n * content from outside the app, such as a comment in a Word file anyone could have written.\n */\n allowHtml?: boolean\n /**\n * Trusted rehype plugins run after sanitising, KaTeX and heading slugs (e.g. a pass that turns\n * entity ids into elements the host's `components` render). Keep the array referentially\n * stable (module scope), or every block re-parses.\n */\n rehypePlugins?: PluggableList\n}\n\nfunction MarkdownRendererComponent({\n children,\n components: customComponents,\n renderCitation,\n isStreaming = false,\n className,\n testId = 'markdown-renderer',\n allowHtml = true,\n rehypePlugins: extraRehypePlugins = NO_PLUGINS,\n}: MarkdownRendererProps) {\n const components = useMemo(() => {\n const base = customComponents\n ? { ...defaultComponents, ...customComponents }\n : defaultComponents\n if (!renderCitation) return base\n const Link = base.a ?? MarkdownLink\n return {\n ...base,\n a: (props: React.ComponentPropsWithoutRef<'a'>) =>\n props.href?.startsWith('#cite-')\n ? renderCitation({ num: props.href.slice(6), children: props.children })\n : React.createElement(Link as React.ElementType, props),\n }\n }, [customComponents, renderCitation])\n\n const text = useMemo(() => {\n const escaped = escapeCurrencyDollars(children)\n return normalizeCitationMarkup(\n isStreaming ? closeOpenEmphasis(trimOpenCitation(escaped)) : escaped\n )\n }, [children, isStreaming])\n const blocks = useMemo(() => splitMarkdownBlocks(text), [text])\n const mathReady = useMathReady(text)\n const { remarkPlugins, rehypePlugins } = usePlugins(allowHtml, mathReady, extraRehypePlugins)\n\n return (\n <MarkdownErrorBoundary\n fallback={<div className={cn('whitespace-pre-wrap break-words', className)}>{children}</div>}\n >\n <MarkdownStreamingContext.Provider value={isStreaming}>\n <div\n className={cn('markdown-content space-y-3 break-words leading-relaxed', className)}\n data-testid={testId}\n >\n {blocks.map((block, index) => (\n <MemoizedBlock\n key={index}\n content={block}\n components={components}\n remarkPlugins={remarkPlugins}\n rehypePlugins={rehypePlugins}\n />\n ))}\n </div>\n </MarkdownStreamingContext.Provider>\n </MarkdownErrorBoundary>\n )\n}\n\n/**\n * Memoised so a stable message in a long thread doesn't re-parse when unrelated\n * tokens arrive elsewhere; a streaming message gets a fresh `children` each flush\n * and re-renders (only its trailing block re-parses, thanks to `MemoizedBlock`).\n */\nexport const MarkdownRenderer = React.memo(MarkdownRendererComponent)\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'\nimport Markdown, { type Components, type Options as MarkdownOptions } from 'react-markdown'\nimport remarkGfm from 'remark-gfm'\nimport rehypeRaw from 'rehype-raw'\nimport rehypeSanitize, { defaultSchema, type Options as SanitizeSchema } from 'rehype-sanitize'\nimport rehypeSlug from 'rehype-slug'\nimport { Copy, Check } from 'lucide-react'\nimport { cn } from '../lib/utils'\nimport { markdownBaseComponents } from './markdown-base-components'\nimport { remarkAiCaption } from './remark-ai-caption'\nimport { MarkdownStreamingContext } from './markdown-streaming-context'\nimport { splitMarkdownBlocks } from './split-markdown-blocks'\nimport { closeOpenEmphasis, normalizeCitationMarkup, trimOpenCitation } from './normalize-citations'\n\n/** remark/rehype plugin lists, as react-markdown takes them. */\ntype PluggableList = NonNullable<MarkdownOptions['rehypePlugins']>\n\n/**\n * MarkdownRenderer \u2014 the single, full-featured markdown surface for the kit.\n *\n * Renders AI answers, chat/DM messages and parsed-document panes with one house\n * style: headings, paragraphs, bold/italic, ordered/unordered lists, GFM tables,\n * fenced code blocks (monospace + copy button), inline code, blockquotes, safe\n * links (`rel=\"noopener\"`, new tab) and horizontal rules.\n *\n * Styling is 100% semantic Tailwind tokens (`text-foreground`,\n * `text-muted-foreground`, `border-border`, `bg-muted`, `text-primary`) so it\n * themes per firm \u2014 no hardcoded brand colours. It sizes to its container, so it\n * looks right in a compact chat bubble and in a full-width document pane alike.\n *\n * Streaming: the content is split into block-level chunks (see\n * `splitMarkdownBlocks`) and each chunk is memoised, so streaming token updates\n * only re-parse the trailing block instead of the whole message. Pass\n * `isStreaming` to let heavy custom components defer via `MarkdownStreamingContext`.\n *\n * Safety: inline HTML (redlines, `<mark>` highlights, parser tables) is parsed and then sanitised\n * (`markdownSanitizeSchema`), so a model or a document cannot inject scripts, handlers or styles.\n * Headings get slug ids, so `#section` links and bookmarks land.\n *\n * Math: `$\u2026$` and `$$\u2026$$` are typeset with KaTeX. remark-math, rehype-katex and KaTeX's CSS\n * (~90 kB gzip) load only once some content actually contains math. A `$` before a digit is a\n * currency sign, never a formula delimiter.\n *\n * Extending: pass `components` to override any element (the kit's `AiMarkdown`\n * and `CitationRenderer` layer their accent links / citation popovers on top; a host composes\n * `MarkdownLink` / `MarkdownCode` into its own overrides), and `rehypePlugins` for trusted\n * transforms that run after sanitising (e.g. turning entity ids into pills).\n */\n\nconst EDGE_FADE = '1.5rem'\n\n/**\n * A GFM table in its own horizontal scroller. The table keeps a minimum width per column\n * (`min-w-[7rem]` cells, words never broken mid-word), so on a narrow screen it scrolls sideways\n * inside this wrapper instead of squeezing cells to a letter per line. The edge(s) with more\n * content to scroll to fade out, which is the scroll hint. Wide containers are unchanged: the\n * table is `w-full` and nothing overflows.\n */\nfunction MarkdownTable({ children, ...props }: React.ComponentPropsWithoutRef<'table'>) {\n const ref = useRef<HTMLDivElement>(null)\n const [edges, setEdges] = useState({ start: false, end: false })\n const measure = useCallback(() => {\n const el = ref.current\n if (!el) return\n const start = el.scrollLeft > 1\n const end = el.scrollLeft + el.clientWidth < el.scrollWidth - 1\n setEdges((prev) => (prev.start === start && prev.end === end ? prev : { start, end }))\n }, [])\n useEffect(() => {\n const el = ref.current\n if (!el) return\n measure()\n if (typeof ResizeObserver === 'undefined') return\n const observer = new ResizeObserver(measure)\n observer.observe(el)\n if (el.firstElementChild) observer.observe(el.firstElementChild)\n return () => observer.disconnect()\n }, [measure])\n const mask =\n edges.start || edges.end\n ? `linear-gradient(to right, ${edges.start ? 'transparent' : '#000'}, #000 ${EDGE_FADE}, #000 calc(100% - ${EDGE_FADE}), ${edges.end ? 'transparent' : '#000'})`\n : undefined\n return (\n <div\n ref={ref}\n onScroll={measure}\n data-testid=\"markdown-table-scroller\"\n data-scroll-start={edges.start || undefined}\n data-scroll-end={edges.end || undefined}\n className=\"my-3 overflow-x-auto rounded-lg border border-border\"\n style={mask ? { maskImage: mask, WebkitMaskImage: mask } : undefined}\n >\n <table\n data-testid=\"markdown-table\"\n className=\"w-full border-collapse text-sm [overflow-wrap:normal] [word-break:normal]\"\n {...props}\n >\n {children}\n </table>\n </div>\n )\n}\n\n/** Fenced code block with a language label and copy-to-clipboard button. */\nfunction CodeBlock({ language, code }: { language: string; code: string }) {\n const [copied, setCopied] = useState(false)\n\n const handleCopy = async () => {\n try {\n await navigator.clipboard.writeText(code)\n setCopied(true)\n setTimeout(() => setCopied(false), 2000)\n } catch {\n // Clipboard unavailable (insecure context / permissions) \u2014 no-op.\n }\n }\n\n return (\n <div\n className=\"code-block relative my-3 overflow-hidden rounded-md border border-current/20 bg-current/5\"\n data-testid=\"markdown-code-block\"\n >\n <div className=\"flex items-center justify-between border-b border-current/20 px-3 py-1.5\">\n <span className=\"font-mono text-xs text-muted-foreground\">{language || 'code'}</span>\n <button\n type=\"button\"\n onClick={handleCopy}\n className=\"flex items-center gap-1 text-xs text-muted-foreground transition-colors hover:text-foreground\"\n title={copied ? 'Copied!' : 'Copy code'}\n >\n {copied ? <Check size={12} /> : <Copy size={12} />}\n <span>{copied ? 'Copied' : 'Copy'}</span>\n </button>\n </div>\n <pre className=\"overflow-x-auto p-3 text-sm leading-relaxed\">\n <code className={cn('font-mono', language && `language-${language}`)}>{code}</code>\n </pre>\n </div>\n )\n}\n\n/**\n * Links. Internal hash/anchor links stay in-page; everything else opens safely in a new tab\n * (`rel=\"noopener noreferrer\"` prevents reverse-tabnabbing). A backend citation marker\n * (`#cite-N`) has no popover here: the cited words stay in the surface's own colour with a quiet\n * superscript number after them (no underline, so a cited sentence does not read as a link).\n * Pass `renderCitation` to `MarkdownRenderer` to swap in a real source popover.\n */\nexport function MarkdownLink({ href, children, ...props }: React.ComponentPropsWithoutRef<'a'>) {\n if (href?.startsWith('#cite-')) {\n return (\n <span\n className=\"cited-text after:ml-px after:align-super after:text-[max(0.8em,var(--text-xs))] after:font-semibold after:text-muted-foreground after:content-[attr(data-cite)]\"\n data-cite={href.slice(6)}\n >\n {children}\n </span>\n )\n }\n const isInternal = !href || href.startsWith('#') || href.startsWith('cite:')\n return (\n <a\n className=\"text-primary underline underline-offset-2\"\n href={href}\n // A `cite:` reference is not a place to go: the click must not navigate.\n onClick={href?.startsWith('cite:') ? (e) => e.preventDefault() : undefined}\n {...(isInternal ? {} : { target: '_blank', rel: 'noopener noreferrer' })}\n {...props}\n >\n {children}\n </a>\n )\n}\n\n/**\n * Inline code, and fenced code as a `CodeBlock` (language label + copy). Exported so a host's own\n * `code` override can handle its special cases (a diagram language, an id shown as a pill) and\n * hand everything else back here.\n */\nexport function MarkdownCode({\n className,\n children,\n ...props\n}: React.ComponentPropsWithoutRef<'code'>) {\n const match = /language-(\\w+)/.exec(className || '')\n if (!match) {\n return (\n <code\n className=\"rounded bg-current/10 px-1 py-0.5 font-mono text-[0.9em]\"\n data-testid=\"markdown-inline-code\"\n {...props}\n >\n {children}\n </code>\n )\n }\n return <CodeBlock language={match[1]} code={String(children).replace(/\\n$/, '')} />\n}\n\n/**\n * Default component overrides. Spreads the shared brand-neutral base\n * (`markdownBaseComponents`: headings, lists, paragraphs, emphasis, hr\u2026) and adds\n * the elements that carry behaviour or need self-contained styling: safe links,\n * code (inline + fenced), bordered tables and blockquotes. Module-scoped so the\n * reference is stable across renders and react-markdown can skip re-parsing.\n */\nconst defaultComponents: Partial<Components> = {\n ...markdownBaseComponents,\n a: MarkdownLink,\n em: ({ children, ...props }: React.ComponentPropsWithoutRef<'em'>) => (\n <em className=\"italic\" {...props}>\n {children}\n </em>\n ),\n code: MarkdownCode,\n // The CodeBlock renders its own <pre>; unwrap react-markdown's wrapper so we\n // don't nest <pre><pre>.\n pre: ({ children }: React.ComponentPropsWithoutRef<'pre'>) => <>{children}</>,\n blockquote: ({ children, ...props }: React.ComponentPropsWithoutRef<'blockquote'>) => (\n <blockquote className=\"border-l-2 border-border pl-4 italic text-muted-foreground\" {...props}>\n {children}\n </blockquote>\n ),\n table: MarkdownTable,\n thead: ({ children, ...props }: React.ComponentPropsWithoutRef<'thead'>) => (\n <thead className=\"bg-current/10\" {...props}>\n {children}\n </thead>\n ),\n th: ({ children, ...props }: React.ComponentPropsWithoutRef<'th'>) => (\n <th\n className=\"min-w-[7rem] border-b border-border px-3 py-2 text-left font-semibold\"\n {...props}\n >\n {children}\n </th>\n ),\n td: ({ children, ...props }: React.ComponentPropsWithoutRef<'td'>) => (\n <td className=\"min-w-[7rem] border-b border-border px-3 py-2 align-top\" {...props}>\n {children}\n </td>\n ),\n // General emphasis marks (GFM `~~strikethrough~~` \u2192 <del>, raw <ins>/<mark>).\n del: ({ children, ...props }: React.ComponentPropsWithoutRef<'del'>) => (\n <del className=\"text-muted-foreground line-through\" {...props}>\n {children}\n </del>\n ),\n ins: ({ children, ...props }: React.ComponentPropsWithoutRef<'ins'>) => (\n <ins className=\"underline decoration-primary/60\" {...props}>\n {children}\n </ins>\n ),\n mark: ({ children, ...props }: React.ComponentPropsWithoutRef<'mark'>) => (\n <mark className=\"rounded bg-primary/15 px-0.5 text-inherit\" {...props}>\n {children}\n </mark>\n ),\n}\n\n/**\n * What survives sanitising inline HTML: GitHub's schema, plus the classes the downstream steps and\n * renderers read (KaTeX's `math-*` placeholders, `language-*` on code), `<mark>` for search\n * highlights, the generated-caption `<aside>` (`remark-ai-caption`), and `data-cid` on `<ins>`/`<del>` (the redline's per-change id, minted from diff\n * positions, never from content). Scripts, event handlers, inline styles and `javascript:` URLs\n * stay out.\n */\nexport const markdownSanitizeSchema: SanitizeSchema = {\n ...defaultSchema,\n tagNames: [...(defaultSchema.tagNames ?? []), 'mark', 'aside'],\n attributes: {\n ...defaultSchema.attributes,\n span: [...(defaultSchema.attributes?.span ?? []), 'className'],\n div: [...(defaultSchema.attributes?.div ?? []), 'className', 'dataTestid'],\n // A figure's generated caption (`remark-ai-caption`): its box and label.\n aside: ['className', 'dataAiCaption', 'ariaLabel'],\n code: [...(defaultSchema.attributes?.code ?? []), 'className'],\n pre: [...(defaultSchema.attributes?.pre ?? []), 'className'],\n ins: [...(defaultSchema.attributes?.ins ?? []), 'dataCid'],\n del: [...(defaultSchema.attributes?.del ?? []), 'dataCid'],\n },\n}\n\n// \u2500\u2500 Lazy KaTeX (math) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Loaded once, module-wide, the first time content with math renders; mounted renderers that\n// were waiting re-render when it lands.\ntype MathPlugins = { remarkMath: PluggableList[number]; rehypeKatex: PluggableList[number] }\nlet loadedMath: MathPlugins | null = null\nlet mathLoadStarted = false\nconst mathSubscribers = new Set<() => void>()\n\nfunction loadMath() {\n if (mathLoadStarted) return\n mathLoadStarted = true\n void Promise.all([\n import('remark-math'),\n import('rehype-katex'),\n import('katex/dist/katex.min.css'),\n ])\n .then(([remarkMath, rehypeKatex]) => {\n loadedMath = { remarkMath: remarkMath.default, rehypeKatex: rehypeKatex.default }\n mathSubscribers.forEach((notify) => notify())\n })\n .catch(() => {\n // Retry on the next math-bearing render if the chunk failed to load.\n mathLoadStarted = false\n })\n}\n\n// remark-math's inline rule: `$\u2026$` with no whitespace just inside, or a `$$\u2026$$` block. A lone\n// price (\"$5,000\") never matches.\nconst MATH_RE = /\\$\\$[\\s\\S]+?\\$\\$|(?<!\\\\)\\$(?![\\s$])(?:\\\\\\$|[^$\\n])+?(?<![\\s\\\\])\\$/\n\n/**\n * A `$` straight before a digit is a currency sign (\"$2,000\", \"\u2248$50,000\"), not the start of inline\n * math: two in one paragraph would otherwise pair up and typeset everything between as a formula.\n * Escape them (outside code, where a backslash would show) so they stay text.\n */\nfunction escapeCurrencyDollars(markdown: string): string {\n return markdown\n .split(/(```[\\s\\S]*?```|`[^`\\n]*`)/)\n .map((part, i) => (i % 2 === 1 ? part : part.replace(/(?<!\\\\)\\$(?=\\d)/g, '\\\\$')))\n .join('')\n}\n\n/** True once the math plugins are loaded for content that contains math. */\nfunction useMathReady(content: string): boolean {\n const hasMath = useMemo(() => MATH_RE.test(content), [content])\n const [, rerender] = useReducer((n: number) => n + 1, 0)\n useEffect(() => {\n if (!hasMath || loadedMath) return\n mathSubscribers.add(rerender)\n loadMath()\n return () => {\n mathSubscribers.delete(rerender)\n }\n }, [hasMath])\n return hasMath && loadedMath !== null\n}\n\nconst NO_PLUGINS: PluggableList = []\n\n/**\n * The remark/rehype pipelines. Raw HTML (when allowed) is parsed and sanitised before the trusted\n * steps (KaTeX, slugs, the host's plugins), so their output is not stripped again. Memoised on\n * its inputs so react-markdown sees stable arrays and memoised blocks do not re-parse.\n */\nfunction usePlugins(allowHtml: boolean, mathReady: boolean, extra: PluggableList) {\n return useMemo(() => {\n const math = mathReady && loadedMath ? loadedMath : null\n const remarkPlugins: PluggableList = math\n ? [remarkGfm, remarkAiCaption, math.remarkMath]\n : [remarkGfm, remarkAiCaption]\n const rehypePlugins: PluggableList = [\n // Text someone else wrote (a Word comment from any .docx): raw HTML stays text.\n ...(allowHtml ? [rehypeRaw, [rehypeSanitize, markdownSanitizeSchema]] : []),\n ...(math ? [math.rehypeKatex] : []),\n rehypeSlug,\n ...extra,\n ] as PluggableList\n return { remarkPlugins, rehypePlugins }\n }, [allowHtml, mathReady, extra])\n}\n\n/**\n * One parsed markdown block, memoised on (content, components) so already-rendered\n * blocks don't re-parse when a later block streams new tokens. This is the core\n * of the streaming-rendering optimisation.\n */\nconst MemoizedBlock = React.memo(function MarkdownBlock({\n content,\n components,\n remarkPlugins,\n rehypePlugins,\n}: {\n content: string\n components: Partial<Components>\n remarkPlugins: PluggableList\n rehypePlugins: PluggableList\n}) {\n return (\n <Markdown remarkPlugins={remarkPlugins} rehypePlugins={rehypePlugins} components={components}>\n {content}\n </Markdown>\n )\n})\n\n/** Prevents a malformed-markdown render error from crashing the whole surface. */\nclass MarkdownErrorBoundary extends React.Component<\n { fallback: React.ReactNode; children: React.ReactNode },\n { hasError: boolean }\n> {\n state = { hasError: false }\n static getDerivedStateFromError() {\n return { hasError: true }\n }\n render() {\n return this.state.hasError ? this.props.fallback : this.props.children\n }\n}\n\nexport interface MarkdownRendererProps {\n /** Markdown content to render. */\n children: string\n /** Custom component overrides merged on top of the defaults. */\n components?: Partial<Components>\n /**\n * When true, signals heavy custom components (via `MarkdownStreamingContext`)\n * to defer their work while tokens are still arriving. Defaults to false.\n */\n isStreaming?: boolean\n /**\n * Renders a citation marker `[words](#cite-N)`: `children` are the cited words, `num` the N.\n * Keep it referentially stable (`useCallback`/`useMemo`), or every block re-parses.\n */\n renderCitation?: (citation: { num: string; children: React.ReactNode }) => React.ReactNode\n /** Extra classes on the wrapper (font-size, prose width, etc.). */\n className?: string\n /** Overrides the `data-testid` on the wrapper (default `markdown-renderer`). */\n testId?: string\n /**\n * Inline HTML in the markdown becomes elements, sanitised (default). False keeps it as text: for\n * content from outside the app, such as a comment in a Word file anyone could have written.\n */\n allowHtml?: boolean\n /**\n * Trusted rehype plugins run after sanitising, KaTeX and heading slugs (e.g. a pass that turns\n * entity ids into elements the host's `components` render). Keep the array referentially\n * stable (module scope), or every block re-parses.\n */\n rehypePlugins?: PluggableList\n}\n\nfunction MarkdownRendererComponent({\n children,\n components: customComponents,\n renderCitation,\n isStreaming = false,\n className,\n testId = 'markdown-renderer',\n allowHtml = true,\n rehypePlugins: extraRehypePlugins = NO_PLUGINS,\n}: MarkdownRendererProps) {\n const components = useMemo(() => {\n const base = customComponents\n ? { ...defaultComponents, ...customComponents }\n : defaultComponents\n if (!renderCitation) return base\n const Link = base.a ?? MarkdownLink\n return {\n ...base,\n a: (props: React.ComponentPropsWithoutRef<'a'>) =>\n props.href?.startsWith('#cite-')\n ? renderCitation({ num: props.href.slice(6), children: props.children })\n : React.createElement(Link as React.ElementType, props),\n }\n }, [customComponents, renderCitation])\n\n const text = useMemo(() => {\n const escaped = escapeCurrencyDollars(children)\n return normalizeCitationMarkup(\n isStreaming ? closeOpenEmphasis(trimOpenCitation(escaped)) : escaped\n )\n }, [children, isStreaming])\n const blocks = useMemo(() => splitMarkdownBlocks(text), [text])\n const mathReady = useMathReady(text)\n const { remarkPlugins, rehypePlugins } = usePlugins(allowHtml, mathReady, extraRehypePlugins)\n\n return (\n <MarkdownErrorBoundary\n fallback={<div className={cn('whitespace-pre-wrap break-words', className)}>{children}</div>}\n >\n <MarkdownStreamingContext.Provider value={isStreaming}>\n <div\n className={cn('markdown-content space-y-3 break-words leading-relaxed', className)}\n data-testid={testId}\n >\n {blocks.map((block, index) => (\n <MemoizedBlock\n key={index}\n content={block}\n components={components}\n remarkPlugins={remarkPlugins}\n rehypePlugins={rehypePlugins}\n />\n ))}\n </div>\n </MarkdownStreamingContext.Provider>\n </MarkdownErrorBoundary>\n )\n}\n\n/**\n * Memoised so a stable message in a long thread doesn't re-parse when unrelated\n * tokens arrive elsewhere; a streaming message gets a fresh `children` each flush\n * and re-renders (only its trailing block re-parses, thanks to `MemoizedBlock`).\n */\nexport const MarkdownRenderer = React.memo(MarkdownRendererComponent)\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,60 @@
1
+ const AI_CAPTION_MARKER = "[!ai-caption]";
2
+ const AI_CAPTION_LABEL = "Generated image caption";
3
+ function takeMarker(quote) {
4
+ const paragraph = quote.children?.[0];
5
+ const text = paragraph?.type === "paragraph" ? paragraph.children?.[0] : void 0;
6
+ if (text?.type !== "text" || !text.value?.trimStart().startsWith(AI_CAPTION_MARKER)) return false;
7
+ text.value = text.value.trimStart().slice(AI_CAPTION_MARKER.length).replace(/^\s+/, "");
8
+ const rest = paragraph.children;
9
+ if (!text.value) rest.shift();
10
+ if (rest[0]?.type === "break") rest.shift();
11
+ if (!rest.length) quote.children.shift();
12
+ return true;
13
+ }
14
+ function walk(node) {
15
+ for (const child of node.children ?? []) {
16
+ if (child.type === "blockquote" && takeMarker(child)) {
17
+ child.data = {
18
+ hName: "aside",
19
+ hProperties: {
20
+ className: [
21
+ "my-2",
22
+ "rounded-md",
23
+ "border",
24
+ "border-border",
25
+ "bg-muted/40",
26
+ "px-3",
27
+ "py-2"
28
+ ],
29
+ dataAiCaption: "",
30
+ ariaLabel: AI_CAPTION_LABEL
31
+ }
32
+ };
33
+ child.children = [
34
+ {
35
+ type: "paragraph",
36
+ data: {
37
+ hName: "div",
38
+ hProperties: {
39
+ className: ["mb-1", "text-xs", "font-medium", "text-muted-foreground"],
40
+ dataTestid: "ai-caption-label"
41
+ }
42
+ },
43
+ children: [{ type: "text", value: AI_CAPTION_LABEL }]
44
+ },
45
+ ...child.children ?? []
46
+ ];
47
+ } else {
48
+ walk(child);
49
+ }
50
+ }
51
+ }
52
+ function remarkAiCaption() {
53
+ return (tree) => walk(tree);
54
+ }
55
+ export {
56
+ AI_CAPTION_LABEL,
57
+ AI_CAPTION_MARKER,
58
+ remarkAiCaption
59
+ };
60
+ //# sourceMappingURL=remark-ai-caption.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/ai/remark-ai-caption.ts"],
4
+ "sourcesContent": ["/**\n * A figure passage's text is not the document's words: the backend stores the caption a vision\n * model wrote as a callout,\n *\n * > [!ai-caption]\n * > Bar chart of damages claimed by year ...\n *\n * and that callout is its only marker. This remark step renders it as a labelled aside\n * (\"Generated image caption\") with the marker removed, wherever a passage is shown.\n */\n\nexport const AI_CAPTION_MARKER = '[!ai-caption]'\nexport const AI_CAPTION_LABEL = 'Generated image caption'\n\ninterface MdNode {\n type: string\n value?: string\n children?: MdNode[]\n data?: { hName?: string; hProperties?: Record<string, unknown> }\n}\n\n/** Strip the marker from the blockquote's first text; true when it was there. */\nfunction takeMarker(quote: MdNode): boolean {\n const paragraph = quote.children?.[0]\n const text = paragraph?.type === 'paragraph' ? paragraph.children?.[0] : undefined\n if (text?.type !== 'text' || !text.value?.trimStart().startsWith(AI_CAPTION_MARKER)) return false\n text.value = text.value.trimStart().slice(AI_CAPTION_MARKER.length).replace(/^\\s+/, '')\n // The marker had a line of its own: drop the break or empty paragraph it leaves behind.\n const rest = paragraph!.children!\n if (!text.value) rest.shift()\n if (rest[0]?.type === 'break') rest.shift()\n if (!rest.length) quote.children!.shift()\n return true\n}\n\nfunction walk(node: MdNode) {\n for (const child of node.children ?? []) {\n if (child.type === 'blockquote' && takeMarker(child)) {\n child.data = {\n hName: 'aside',\n hProperties: {\n className: [\n 'my-2',\n 'rounded-md',\n 'border',\n 'border-border',\n 'bg-muted/40',\n 'px-3',\n 'py-2',\n ],\n dataAiCaption: '',\n ariaLabel: AI_CAPTION_LABEL,\n },\n }\n child.children = [\n {\n type: 'paragraph',\n data: {\n hName: 'div',\n hProperties: {\n className: ['mb-1', 'text-xs', 'font-medium', 'text-muted-foreground'],\n dataTestid: 'ai-caption-label',\n },\n },\n children: [{ type: 'text', value: AI_CAPTION_LABEL }],\n },\n ...(child.children ?? []),\n ]\n } else {\n walk(child)\n }\n }\n}\n\nexport function remarkAiCaption() {\n return (tree: MdNode) => walk(tree)\n}\n"],
5
+ "mappings": "AAWO,MAAM,oBAAoB;AAC1B,MAAM,mBAAmB;AAUhC,SAAS,WAAW,OAAwB;AAC1C,QAAM,YAAY,MAAM,WAAW,CAAC;AACpC,QAAM,OAAO,WAAW,SAAS,cAAc,UAAU,WAAW,CAAC,IAAI;AACzE,MAAI,MAAM,SAAS,UAAU,CAAC,KAAK,OAAO,UAAU,EAAE,WAAW,iBAAiB,EAAG,QAAO;AAC5F,OAAK,QAAQ,KAAK,MAAM,UAAU,EAAE,MAAM,kBAAkB,MAAM,EAAE,QAAQ,QAAQ,EAAE;AAEtF,QAAM,OAAO,UAAW;AACxB,MAAI,CAAC,KAAK,MAAO,MAAK,MAAM;AAC5B,MAAI,KAAK,CAAC,GAAG,SAAS,QAAS,MAAK,MAAM;AAC1C,MAAI,CAAC,KAAK,OAAQ,OAAM,SAAU,MAAM;AACxC,SAAO;AACT;AAEA,SAAS,KAAK,MAAc;AAC1B,aAAW,SAAS,KAAK,YAAY,CAAC,GAAG;AACvC,QAAI,MAAM,SAAS,gBAAgB,WAAW,KAAK,GAAG;AACpD,YAAM,OAAO;AAAA,QACX,OAAO;AAAA,QACP,aAAa;AAAA,UACX,WAAW;AAAA,YACT;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,UACF;AAAA,UACA,eAAe;AAAA,UACf,WAAW;AAAA,QACb;AAAA,MACF;AACA,YAAM,WAAW;AAAA,QACf;AAAA,UACE,MAAM;AAAA,UACN,MAAM;AAAA,YACJ,OAAO;AAAA,YACP,aAAa;AAAA,cACX,WAAW,CAAC,QAAQ,WAAW,eAAe,uBAAuB;AAAA,cACrE,YAAY;AAAA,YACd;AAAA,UACF;AAAA,UACA,UAAU,CAAC,EAAE,MAAM,QAAQ,OAAO,iBAAiB,CAAC;AAAA,QACtD;AAAA,QACA,GAAI,MAAM,YAAY,CAAC;AAAA,MACzB;AAAA,IACF,OAAO;AACL,WAAK,KAAK;AAAA,IACZ;AAAA,EACF;AACF;AAEO,SAAS,kBAAkB;AAChC,SAAO,CAAC,SAAiB,KAAK,IAAI;AACpC;",
6
+ "names": []
7
+ }
@@ -7,7 +7,7 @@ var chunkO32T7XRD_cjs = require('../chunk-O32T7XRD.cjs');
7
7
  require('../chunk-N73IMVFF.cjs');
8
8
  var chunkNDIKMOBJ_cjs = require('../chunk-NDIKMOBJ.cjs');
9
9
  var chunkVOFNX4P6_cjs = require('../chunk-VOFNX4P6.cjs');
10
- var chunkMXP4O4DI_cjs = require('../chunk-MXP4O4DI.cjs');
10
+ var chunkEM6DP62E_cjs = require('../chunk-EM6DP62E.cjs');
11
11
  require('../chunk-5NOZDH3F.cjs');
12
12
  require('../chunk-S6A6FSG7.cjs');
13
13
  var react = require('react');
@@ -484,7 +484,7 @@ function useArtifactSource(documentId, enabled = true) {
484
484
  });
485
485
  }
486
486
  function renderArtifactMarkdown(markdown, components) {
487
- return /* @__PURE__ */ jsxRuntime.jsx(chunkMXP4O4DI_cjs.MarkdownRenderer, { components, children: markdown });
487
+ return /* @__PURE__ */ jsxRuntime.jsx(chunkEM6DP62E_cjs.MarkdownRenderer, { components, children: markdown });
488
488
  }
489
489
  function useStreamedArtifact({
490
490
  artifacts,
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var chunkEEIYXVM4_cjs = require('./chunk-EEIYXVM4.cjs');
4
- var chunkMXP4O4DI_cjs = require('./chunk-MXP4O4DI.cjs');
4
+ var chunkEM6DP62E_cjs = require('./chunk-EM6DP62E.cjs');
5
5
  var chunkWYZJLZ67_cjs = require('./chunk-WYZJLZ67.cjs');
6
6
  var chunkKC474WVG_cjs = require('./chunk-KC474WVG.cjs');
7
7
  var chunkHJDR5HEO_cjs = require('./chunk-HJDR5HEO.cjs');
@@ -4577,7 +4577,7 @@ function AiMarkdown({
4577
4577
  allowHtml
4578
4578
  }) {
4579
4579
  return /* @__PURE__ */ jsxRuntime.jsx(
4580
- chunkMXP4O4DI_cjs.MarkdownRenderer,
4580
+ chunkEM6DP62E_cjs.MarkdownRenderer,
4581
4581
  {
4582
4582
  className: className ?? "text-sm",
4583
4583
  components: AI_COMPONENTS,
@@ -5322,5 +5322,5 @@ exports.toggleVariants = toggleVariants;
5322
5322
  exports.useAudioRecorder = useAudioRecorder;
5323
5323
  exports.useDictateContext = useDictateContext;
5324
5324
  exports.usesOriginalBytesForViewer = usesOriginalBytesForViewer;
5325
- //# sourceMappingURL=chunk-DLEHM4IA.cjs.map
5326
- //# sourceMappingURL=chunk-DLEHM4IA.cjs.map
5325
+ //# sourceMappingURL=chunk-AB2A6F6T.cjs.map
5326
+ //# sourceMappingURL=chunk-AB2A6F6T.cjs.map