import React, { useMemo } from "react";
import ReactMarkdown from "react-markdown";
import rehypeRaw from "rehype-raw";
import rehypeSanitize from "rehype-sanitize";
import remarkGfm from "remark-gfm";
import { cn } from "@/lib/utils";
import { Citation } from "./chat/Citation";
import { Source } from "./chat/types";
interface MarkdownViewerProps {
content: string;
className?: string;
getCitationSource?: (id: number) => Source | null;
}
export function MarkdownViewer({ content, className, getCitationSource }: MarkdownViewerProps) {
// Memoize the markdown components to prevent unnecessary re-renders
const components = useMemo(() => {
return {
// Define custom components for markdown elements
p: ({node, children, ...props}: any) => {
// If there's no getCitationSource function, just render normally
if (!getCitationSource) {
return
{children}
;
}
// Process citations within paragraph content
return {processCitationsInReactChildren(children, getCitationSource)}
;
},
a: ({node, children, ...props}: any) => {
// Process citations within link content if needed
const processedChildren = getCitationSource
? processCitationsInReactChildren(children, getCitationSource)
: children;
return {processedChildren};
},
li: ({node, children, ...props}: any) => {
// Process citations within list item content
const processedChildren = getCitationSource
? processCitationsInReactChildren(children, getCitationSource)
: children;
return {processedChildren};
},
ul: ({node, ...props}: any) => ,
ol: ({node, ...props}: any) =>
,
h1: ({node, children, ...props}: any) => {
const processedChildren = getCitationSource
? processCitationsInReactChildren(children, getCitationSource)
: children;
return {processedChildren}
;
},
h2: ({node, children, ...props}: any) => {
const processedChildren = getCitationSource
? processCitationsInReactChildren(children, getCitationSource)
: children;
return {processedChildren}
;
},
h3: ({node, children, ...props}: any) => {
const processedChildren = getCitationSource
? processCitationsInReactChildren(children, getCitationSource)
: children;
return {processedChildren}
;
},
h4: ({node, children, ...props}: any) => {
const processedChildren = getCitationSource
? processCitationsInReactChildren(children, getCitationSource)
: children;
return {processedChildren}
;
},
blockquote: ({node, ...props}: any) => ,
hr: ({node, ...props}: any) =>
,
img: ({node, ...props}: any) =>
,
table: ({node, ...props}: any) => ,
th: ({node, ...props}: any) => | ,
td: ({node, ...props}: any) => | ,
code: ({node, className, children, ...props}: any) => {
const match = /language-(\w+)/.exec(className || '');
const isInline = !match;
return isInline
? {children}
: (
);
}
};
}, [getCitationSource]);
return (
{content}
);
}
// Helper function to process citations within React children
const processCitationsInReactChildren = (children: React.ReactNode, getCitationSource: (id: number) => Source | null): React.ReactNode => {
// If children is not an array or string, just return it
if (!children || (typeof children !== 'string' && !Array.isArray(children))) {
return children;
}
// Handle string content directly - this is where we process citation references
if (typeof children === 'string') {
return processCitationsInText(children, getCitationSource);
}
// Handle arrays of children recursively
if (Array.isArray(children)) {
return React.Children.map(children, child => {
if (typeof child === 'string') {
return processCitationsInText(child, getCitationSource);
}
return child;
});
}
return children;
};
// Process citation references in text content
const processCitationsInText = (text: string, getCitationSource: (id: number) => Source | null): React.ReactNode[] => {
// Use improved regex to catch citation numbers more reliably
// This will match patterns like [1], [42], etc. including when they appear at the end of a line or sentence
const citationRegex = /\[(\d+)\]/g;
const parts: React.ReactNode[] = [];
let lastIndex = 0;
let match;
let position = 0;
while ((match = citationRegex.exec(text)) !== null) {
// Add text before the citation
if (match.index > lastIndex) {
parts.push(text.substring(lastIndex, match.index));
}
// Add the citation component
const citationId = parseInt(match[1], 10);
const source = getCitationSource(citationId);
parts.push(
);
lastIndex = match.index + match[0].length;
position++;
}
// Add any remaining text after the last citation
if (lastIndex < text.length) {
parts.push(text.substring(lastIndex));
}
return parts;
};