feat(markdown): enable citation rendering in MarkdownViewer and related components

- Added `enableCitations` prop to `MarkdownViewer` to support interactive citation badges.
- Updated instances of `MarkdownViewer` across various components to utilize the new citation feature.
- Enhanced citation processing in `PlateEditor` for read-only views, ensuring citations are rendered correctly without affecting markdown serialization.
- Refactored citation handling in `InlineCitation` and `MarkdownText` to improve citation context management.
This commit is contained in:
DESKTOP-RTLN3BA\$punk 2026-04-30 18:40:55 -07:00
parent d335e96ec2
commit 7aeb8bb0a8
14 changed files with 809 additions and 260 deletions

View file

@ -0,0 +1,79 @@
"use client";
import type { ReactNode } from "react";
import { InlineCitation, UrlCitation } from "@/components/assistant-ui/inline-citation";
import {
type CitationToken,
type CitationUrlMap,
parseTextWithCitations,
} from "@/lib/citations/citation-parser";
/**
* Render a single parsed citation token as JSX.
*
* `ordinalKey` should be a stable per-render counter so duplicate identical
* citations within the same parent don't collide on `key`. The previous
* implementation in `markdown-text.tsx` used the source string itself as
* the key, which produced React warnings when two segments rendered the
* same `[citation:N]` text.
*/
export function renderCitationToken(token: CitationToken, ordinalKey: number): ReactNode {
if (token.kind === "url") {
return <UrlCitation key={`citation-url-${ordinalKey}`} url={token.url} />;
}
return (
<InlineCitation
key={`citation-${token.isDocsChunk ? "doc-" : ""}${token.chunkId}-${ordinalKey}`}
chunkId={token.chunkId}
isDocsChunk={token.isDocsChunk}
/>
);
}
/**
* Walk a `ReactNode` (string, array, or arbitrary node) and replace any
* `[citation:...]` tokens inside string children with citation badges.
*
* Designed for use inside `Streamdown`/`react-markdown` `components`
* overrides where the renderer hands you `children`. Non-string children
* are returned untouched so block/phrasing structure is preserved.
*/
export function processChildrenWithCitations(
children: ReactNode,
urlMap: CitationUrlMap
): ReactNode {
if (typeof children === "string") {
const segments = parseTextWithCitations(children, urlMap);
if (segments.length === 1 && typeof segments[0] === "string") {
return children;
}
let ordinal = 0;
return segments.map((segment) =>
typeof segment === "string" ? segment : renderCitationToken(segment, ordinal++)
);
}
if (Array.isArray(children)) {
let ordinal = 0;
return children.map((child, childIndex) => {
if (typeof child === "string") {
const segments = parseTextWithCitations(child, urlMap);
if (segments.length === 1 && typeof segments[0] === "string") {
return child;
}
return (
<span key={`citation-seg-${childIndex}`}>
{segments.map((segment) =>
typeof segment === "string"
? segment
: renderCitationToken(segment, ordinal++)
)}
</span>
);
}
return child;
});
}
return children;
}