import React, { useEffect, useRef } from 'react'; import ReactMarkdown from 'react-markdown'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { atomDark } from 'react-syntax-highlighter/dist/esm/styles/prism'; import remarkGfm from 'remark-gfm'; import rehypeRaw from 'rehype-raw'; import './Streaming.css'; import './SourceRef.css'; // Helper function to normalize various citation formats (e.g., [1,2], [1, 2]) into the standard [1][2] format const normalizeCitations = (text) => { if (!text) return ''; const citationRegex = /\[(\d+(?:,\s*\d+)+)\]/g; return text.replace(citationRegex, (match, capturedNumbers) => { const numbers = capturedNumbers .split(/,\s*/) .map(numStr => numStr.trim()) .filter(Boolean); if (numbers.length <= 1) { return match; } return numbers.map(num => `[${num}]`).join(''); }); }; // Streaming component for rendering markdown content const Streaming = ({ content, isStreaming, onContentRef, showSourcePopup, hideSourcePopup }) => { const contentRef = useRef(null); useEffect(() => { if (contentRef.current && onContentRef) { onContentRef(contentRef.current); } }, [content, onContentRef]); const displayContent = isStreaming ? `${content}▌` : (content || ''); const normalizedContent = normalizeCitations(displayContent); // Custom renderer for text nodes to handle source references const renderWithSourceRefs = (elementType) => { const ElementComponent = elementType; // e.g., 'p', 'li' // Helper to gather plain text const getFullText = (something) => { if (typeof something === 'string') return something; if (Array.isArray(something)) return something.map(getFullText).join(''); if (React.isValidElement(something) && something.props?.children) return getFullText(React.Children.toArray(something.props.children)); return ''; }; return (props) => { // Plain‑text version of this block (paragraph / list‑item) const fullText = getFullText(props.children); // Same regex the backend used const sentenceRegex = /[^.!?\n]+[.!?]+[\])'"`’”]*|[^.!?\n]+$/g; const sentencesArr = fullText.match(sentenceRegex) || [fullText]; // Helper function to find the sentence that contains position `pos` const sentenceByPos = (pos) => { let run = 0; for (const s of sentencesArr) { const end = run + s.length; if (pos >= run && pos < end) return s.trim(); run = end; } return fullText.trim(); }; // Cursor that advances through fullText so each subsequent // indexOf search starts AFTER the previous match let searchCursor = 0; // Recursive renderer that preserves existing markup const processNode = (node, keyPrefix = 'node') => { if (typeof node === 'string') { const citationRegex = /\[(\d+)\]/g; let last = 0; let parts = []; let m; while ((m = citationRegex.exec(node))) { const sliceBefore = node.slice(last, m.index); if (sliceBefore) parts.push(sliceBefore); const localIdx = m.index; const num = parseInt(m[1], 10); const citStr = m[0]; // Find this specific occurrence in fullText, starting at searchCursor const absIdx = fullText.indexOf(citStr, searchCursor); if (absIdx !== -1) searchCursor = absIdx + citStr.length; const sentenceForPopup = sentenceByPos(absIdx); parts.push( showSourcePopup && showSourcePopup(num - 1, e.target, sentenceForPopup) } onMouseLeave={hideSourcePopup} > {num} ); last = localIdx + citStr.length; } if (last < node.length) parts.push(node.slice(last)); return parts; } // For non‑string children, recurse (preserves , , links, etc.) if (React.isValidElement(node) && node.props?.children) { const processed = React.Children.map(node.props.children, (child, i) => processNode(child, `${keyPrefix}-${i}`) ); return React.cloneElement(node, { children: processed }); } return node; // element without children or unknown type }; const processedChildren = React.Children.map(props.children, (child, i) => processNode(child, `root-${i}`) ); // Render original element (p, li, …) with processed children return {processedChildren}; }; }; return ( {match ? match[1] : 'code'} {String(children).replace(/\n$/, '')} ) : ( {children} ); }, table({node, ...props}) { return ( ); }, a({node, children, href, ...props}) { return ( {children} ); }, blockquote({node, ...props}) { return ( ); } }} > {normalizedContent} ); }; export default Streaming;
{children}