"use client"; import "@assistant-ui/react-markdown/styles/dot.css"; import { type CodeHeaderProps, MarkdownTextPrimitive, unstable_memoizeMarkdownComponents as memoizeMarkdownComponents, useIsMarkdownCodeBlock, } from "@assistant-ui/react-markdown"; import { CheckIcon, CopyIcon } from "lucide-react"; import { type FC, memo, type ReactNode, useState } from "react"; import remarkGfm from "remark-gfm"; import { InlineCitation } from "@/components/assistant-ui/inline-citation"; import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button"; import { cn } from "@/lib/utils"; // Citation pattern: [citation:CHUNK_ID] or [citation:doc-CHUNK_ID] // Also matches Chinese brackets 【】 and handles zero-width spaces that LLM sometimes inserts const CITATION_REGEX = /[[【]\u200B?citation:(doc-)?(\d+)\u200B?[\]】]/g; // Track chunk IDs to citation numbers mapping for consistent numbering // This map is reset when a new message starts rendering // Uses string keys to differentiate between doc and regular chunks (e.g., "doc-123" vs "123") let chunkIdToCitationNumber: Map = new Map(); let nextCitationNumber = 1; /** * Resets the citation counter - should be called at the start of each message */ export function resetCitationCounter() { chunkIdToCitationNumber = new Map(); nextCitationNumber = 1; } /** * Gets or assigns a citation number for a chunk ID * Uses string key to differentiate between doc and regular chunks */ function getCitationNumber(chunkId: number, isDocsChunk: boolean): number { const key = isDocsChunk ? `doc-${chunkId}` : String(chunkId); const existingNumber = chunkIdToCitationNumber.get(key); if (existingNumber === undefined) { chunkIdToCitationNumber.set(key, nextCitationNumber++); } return chunkIdToCitationNumber.get(key)!; } /** * Parses text and replaces [citation:XXX] patterns with InlineCitation components * Supports both regular chunks [citation:123] and docs chunks [citation:doc-123] */ function parseTextWithCitations(text: string): ReactNode[] { const parts: ReactNode[] = []; let lastIndex = 0; let match: RegExpExecArray | null; let instanceIndex = 0; // Reset regex state CITATION_REGEX.lastIndex = 0; while ((match = CITATION_REGEX.exec(text)) !== null) { // Add text before the citation if (match.index > lastIndex) { parts.push(text.substring(lastIndex, match.index)); } // Check if this is a docs chunk (has "doc-" prefix) const isDocsChunk = match[1] === "doc-"; const chunkId = Number.parseInt(match[2], 10); const citationNumber = getCitationNumber(chunkId, isDocsChunk); parts.push( ); lastIndex = match.index + match[0].length; instanceIndex++; } // Add any remaining text after the last citation if (lastIndex < text.length) { parts.push(text.substring(lastIndex)); } return parts.length > 0 ? parts : [text]; } const MarkdownTextImpl = () => { return ( ); }; export const MarkdownText = memo(MarkdownTextImpl); const CodeHeader: FC = ({ language, code }) => { const { isCopied, copyToClipboard } = useCopyToClipboard(); const onCopy = () => { if (!code || isCopied) return; copyToClipboard(code); }; return (
{language} {!isCopied && } {isCopied && }
); }; const useCopyToClipboard = ({ copiedDuration = 3000 }: { copiedDuration?: number } = {}) => { const [isCopied, setIsCopied] = useState(false); const copyToClipboard = (value: string) => { if (!value) return; navigator.clipboard.writeText(value).then(() => { setIsCopied(true); setTimeout(() => setIsCopied(false), copiedDuration); }); }; return { isCopied, copyToClipboard }; }; /** * Helper to process children and replace citation patterns with components */ function processChildrenWithCitations(children: ReactNode): ReactNode { if (typeof children === "string") { const parsed = parseTextWithCitations(children); return parsed.length === 1 && typeof parsed[0] === "string" ? children : <>{parsed}; } if (Array.isArray(children)) { return children.map((child, index) => { if (typeof child === "string") { const parsed = parseTextWithCitations(child); return parsed.length === 1 && typeof parsed[0] === "string" ? ( child ) : ( {parsed} ); } return child; }); } return children; } const defaultComponents = memoizeMarkdownComponents({ h1: ({ className, children, ...props }) => (

{processChildrenWithCitations(children)}

), h2: ({ className, children, ...props }) => (

{processChildrenWithCitations(children)}

), h3: ({ className, children, ...props }) => (

{processChildrenWithCitations(children)}

), h4: ({ className, children, ...props }) => (

{processChildrenWithCitations(children)}

), h5: ({ className, children, ...props }) => (
{processChildrenWithCitations(children)}
), h6: ({ className, children, ...props }) => (
{processChildrenWithCitations(children)}
), p: ({ className, children, ...props }) => (

{processChildrenWithCitations(children)}

), a: ({ className, children, ...props }) => ( {processChildrenWithCitations(children)} ), blockquote: ({ className, children, ...props }) => (
{processChildrenWithCitations(children)}
), ul: ({ className, ...props }) => (
    li]:mt-2", className)} {...props} /> ), ol: ({ className, ...props }) => (
      li]:mt-2", className)} {...props} /> ), li: ({ className, children, ...props }) => (
    1. {processChildrenWithCitations(children)}
    2. ), hr: ({ className, ...props }) => (
      ), table: ({ className, ...props }) => (
      ), th: ({ className, children, ...props }) => ( ), td: ({ className, children, ...props }) => ( ), tr: ({ className, ...props }) => ( td:first-child]:rounded-bl-lg [&:last-child>td:last-child]:rounded-br-lg", className )} {...props} /> ), sup: ({ className, ...props }) => ( a]:text-xs [&>a]:no-underline", className)} {...props} /> ), pre: ({ className, ...props }) => (
      	),
      	code: function Code({ className, ...props }) {
      		const isCodeBlock = useIsMarkdownCodeBlock();
      		return (
      			
      		);
      	},
      	strong: ({ className, children, ...props }) => (
      		
      			{processChildrenWithCitations(children)}
      		
      	),
      	em: ({ className, children, ...props }) => (
      		
      			{processChildrenWithCitations(children)}
      		
      	),
      	CodeHeader,
      });
      
      {processChildrenWithCitations(children)} {processChildrenWithCitations(children)}