/* eslint-disable @next/next/no-img-element */ 'use client'; import { cn } from '@/lib/utils'; import { CheckCheck, Copy as CopyIcon, Search, FileText, Globe, Settings, } from 'lucide-react'; import Markdown, { MarkdownToJSX } from 'markdown-to-jsx'; import { useState } from 'react'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { oneDark, oneLight, } from 'react-syntax-highlighter/dist/cjs/styles/prism'; import { useTheme } from 'next-themes'; import ThinkBox from './ThinkBox'; import { Document } from '@langchain/core/documents'; import CitationLink from './CitationLink'; // Helper functions for think overlay const extractThinkContent = (content: string): string | null => { const thinkRegex = /]*>([\s\S]*?)<\/think>/g; const matches = content.match(thinkRegex); if (!matches) return null; // Extract content between think tags and join if multiple const extractedContent = matches .map((match) => match.replace(/<\/?think[^>]*>/g, '')) .join('\n\n'); // Return null if content is empty or only whitespace return extractedContent.trim().length === 0 ? null : extractedContent; }; const removeThinkTags = (content: string): string => { return content.replace(/]*>[\s\S]*?<\/think>/g, '').trim(); }; // Add stable IDs to think tags if they don't already have them const addThinkBoxIds = (content: string): string => { let thinkCounter = 0; return content.replace(/]*\sid=)/g, () => { return `; onThinkBoxToggle?: ( messageId: string, thinkBoxId: string, expanded: boolean, ) => void; sources?: Document[]; } // Custom ToolCall component for markdown const ToolCall = ({ type, query, urls, count, children, }: { type?: string; query?: string; urls?: string; count?: string; children?: React.ReactNode; }) => { const getIcon = (toolType: string) => { switch (toolType) { case 'search': case 'web_search': return ( ); case 'file': case 'file_search': return ( ); case 'url': case 'url_summarization': return ( ); default: return ( ); } }; const formatToolMessage = () => { if (type === 'search' || type === 'web_search') { return ( <> {getIcon(type)} Web search: {query || children} ); } if (type === 'file' || type === 'file_search') { return ( <> {getIcon(type)} File search: {query || children} ); } if (type === 'url' || type === 'url_summarization') { const urlCount = count ? parseInt(count) : 1; return ( <> {getIcon(type)} Analyzing {urlCount} web page{urlCount === 1 ? '' : 's'} for additional details ); } // Fallback for unknown tool types return ( <> {getIcon(type || 'default')} Using tool: {type || 'unknown'} ); }; return (
{formatToolMessage()}
); }; const ThinkTagProcessor = ({ children, id, isExpanded, onToggle, }: { children: React.ReactNode; id?: string; isExpanded?: boolean; onToggle?: (thinkBoxId: string, expanded: boolean) => void; }) => { return ( { if (id && onToggle) { onToggle(id, !isExpanded); } }} /> ); }; const CodeBlock = ({ className, children, }: { className?: string; children: React.ReactNode; }) => { const { theme } = useTheme(); // Extract language from className (format could be "language-javascript" or "lang-javascript") let language = ''; if (className) { if (className.startsWith('language-')) { language = className.replace('language-', ''); } else if (className.startsWith('lang-')) { language = className.replace('lang-', ''); } } const content = children as string; const [isCopied, setIsCopied] = useState(false); const handleCopyCode = () => { navigator.clipboard.writeText(content); setIsCopied(true); setTimeout(() => setIsCopied(false), 2000); }; // Choose syntax highlighting style based on theme const syntaxStyle = theme === 'light' ? oneLight : oneDark; const backgroundStyle = theme === 'light' ? '#fafafa' : '#1c1c1c'; return (
{language}
1} useInlineStyles={true} PreTag="div" > {content}
); }; const MarkdownRenderer = ({ content, className, showThinking = true, messageId, expandedThinkBoxes, onThinkBoxToggle, sources, }: MarkdownRendererProps) => { // Preprocess content to add stable IDs to think tags const processedContent = addThinkBoxIds(content); // Check if a think box is expanded const isThinkBoxExpanded = (thinkBoxId: string) => { return expandedThinkBoxes?.has(thinkBoxId) || false; }; // Handle think box toggle const handleThinkBoxToggle = (thinkBoxId: string, expanded: boolean) => { if (messageId && onThinkBoxToggle) { onThinkBoxToggle(messageId, thinkBoxId, expanded); } }; // Determine what content to render based on showThinking parameter const contentToRender = showThinking ? processedContent : removeThinkTags(processedContent); // Markdown formatting options const markdownOverrides: MarkdownToJSX.Options = { overrides: { ToolCall: { component: ToolCall, }, think: { component: ({ children, id, ...props }) => { // Use the id from the HTML attribute const thinkBoxId = id || 'think-unknown'; const isExpanded = isThinkBoxExpanded(thinkBoxId); return ( {children} ); }, }, code: { component: ({ className, children }) => { // Check if it's an inline code block or a fenced code block if (className) { // This is a fenced code block (```code```) return {children}; } // This is an inline code block (`code`) return ( {children} ); }, }, strong: { component: ({ children }) => ( {children} ), }, pre: { component: ({ children }) => children, }, a: { component: (props) => { // Check if this is a citation link with data-citation attribute const citationNumber = props['data-citation']; if (sources && citationNumber) { const number = parseInt(citationNumber); const source = sources[number - 1]; if (source) { return ( ); } } // Default link behavior return ; }, }, // Prevent rendering of certain HTML elements for security iframe: () => null, // Don't render iframes script: () => null, // Don't render scripts object: () => null, // Don't render objects style: () => null, // Don't render styles }, }; return (
{contentToRender}
); }; export default MarkdownRenderer;