import React, { useEffect, useState, useCallback } from 'react'; import { GroundingMetadata } from '../types/ragGrounding'; import { markdownService } from '../services/markdownService'; import { MarkdownParseResult, MarkdownNode, MarkdownNodeType, ValidationResult } from '../types/markdown'; import ImageCard from './ImageCard'; import ImagePreviewModal from './ImagePreviewModal'; /** * 增强Markdown渲染器属性接口 */ interface EnhancedMarkdownRendererProps { /** 要渲染的文本内容 */ content: string; /** Grounding元数据 */ groundingMetadata?: GroundingMetadata; /** 是否启用角标引用 */ enableReferences?: boolean; /** 是否启用Markdown渲染 */ enableMarkdown?: boolean; /** 自定义样式类名 */ className?: string; /** 是否显示解析统计信息 */ showStatistics?: boolean; /** 是否启用实时解析 */ enableRealTimeParsing?: boolean; } /** * 增强Markdown渲染器组件 * 使用基于Tree-sitter的MarkdownService替代ReactMarkdown */ export const EnhancedMarkdownRenderer: React.FC = ({ content, groundingMetadata, enableMarkdown = true, className = '', showStatistics = false, enableRealTimeParsing = false }) => { const [parseResult, setParseResult] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [validation, setValidation] = useState(null); const [expandedGrounding, setExpandedGrounding] = useState(null); const [previewSource, setPreviewSource] = useState(null); // 解析Markdown内容 const parseContent = useCallback(async () => { if (!content.trim() || !enableMarkdown) { setParseResult(null); setValidation(null); return; } setIsLoading(true); setError(null); try { // 解析Markdown const result = await markdownService.parseMarkdown(content); console.log({ content: content.length, result: result.source_text.length }) setParseResult(result); // 验证文档结构 const validationResult = await markdownService.validateMarkdown(content); setValidation(validationResult); } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred'; setError(errorMessage); console.error('📝 Markdown解析失败:', err); } finally { setIsLoading(false); } }, [content, enableMarkdown]); // 实时解析效果 useEffect(() => { if (enableRealTimeParsing) { const timeoutId = setTimeout(parseContent, 300); // 防抖 return () => clearTimeout(timeoutId); } }, [content, enableRealTimeParsing, parseContent]); // 初始解析 useEffect(() => { if (!enableRealTimeParsing) { parseContent(); } }, [parseContent, enableRealTimeParsing]); // 切换grounding详情展开状态 const toggleGroundingDetails = useCallback((groundingAnalysis: any) => { if (expandedGrounding && expandedGrounding === groundingAnalysis) { // 如果当前已展开相同的内容,则收起 setExpandedGrounding(null); } else { // 展开新的内容 setExpandedGrounding(groundingAnalysis); } }, [expandedGrounding]); // 查看大图 const handleViewLarge = useCallback((source: any) => { setPreviewSource(source); }, []); // 关闭图片预览 const handleClosePreview = useCallback(() => { setPreviewSource(null); }, []); // 分析节点与引用资源的关联 const analyzeNodeGrounding = useCallback((node: MarkdownNode) => { if (!groundingMetadata?.grounding_supports || !parseResult) { return null; } // 计算节点在原始文本中的字节偏移位置(与grounding数据的字节偏移匹配) const nodeStartOffset = node.range?.start?.byte_offset || 0; const nodeEndOffset = node.range?.end?.byte_offset || 0; // 查找与当前节点位置重叠的grounding支持信息 const relatedSupports = groundingMetadata.grounding_supports.filter(support => { // grounding数据使用字节偏移 const segmentStart = support.segment.startIndex; const segmentEnd = support.segment.endIndex; const hasOverlap = (nodeStartOffset <= segmentEnd && nodeEndOffset >= segmentStart); // 检查节点范围与grounding片段是否有重叠 return hasOverlap; }); if (relatedSupports.length > 0) { // 获取相关的来源信息 console.log('🔍 Related supports:', relatedSupports); const relatedSources = relatedSupports.flatMap(support => support.groundingChunkIndices.map(index => groundingMetadata.sources[index]) ).filter(Boolean); console.log('📚 Related sources:', relatedSources); console.log('🖼️ Source content examples:', relatedSources.map(s => ({ title: s.title, uri: s.uri, contentType: typeof s.content, contentKeys: s.content ? Object.keys(s.content) : null, contentSample: s.content }))); const analysisResult = { node: { type: node.node_type, content: node.content.substring(0, 100) + (node.content.length > 100 ? '...' : ''), position: { start: nodeStartOffset, end: nodeEndOffset, line: node.range?.start?.line, column: node.range?.start?.column } }, groundingInfo: { supportCount: relatedSupports.length, sourceCount: relatedSources.length, sources: relatedSources.map(source => { // 解析content字段中的图片信息 let imageData = null; if (source.content) { // 如果content是字符串,尝试解析为JSON if (typeof source.content === 'string') { try { imageData = JSON.parse(source.content); } catch { imageData = { description: source.content }; } } else if (source.content.text) { // 如果content有text字段,使用text字段 try { imageData = typeof source.content.text === 'string' ? JSON.parse(source.content.text) : source.content.text; } catch { imageData = { description: source.content.text }; } } else { // 直接使用content对象 imageData = source.content; } } return { title: source.title, uri: source.uri, content: imageData, snippet: imageData?.description || imageData?.snippet || 'No description available' }; }), segments: relatedSupports.map(support => ({ start: support.segment.startIndex, end: support.segment.endIndex, chunkIndices: support.groundingChunkIndices })) } }; console.log('🔗 节点引用分析:', analysisResult); return analysisResult; } return null; }, [groundingMetadata, parseResult]); // 渲染单个Markdown节点 const renderNode = useCallback((node: MarkdownNode, depth: number = 0, index: number = 0, showGroundingIndicator: boolean = true): React.ReactNode => { const key = `${node.range?.start?.line || 0}-${node.range?.start?.column || 0}-${node.range?.start?.offset || 0}-${depth}-${index}`; // 分析当前节点的引用关联 const groundingAnalysis = analyzeNodeGrounding(node); // 创建引用指示器组件 const isExpanded = expandedGrounding === groundingAnalysis; const GroundingIndicator = (groundingAnalysis && showGroundingIndicator) ? ( { console.log('📚 点击查看引用详情:', groundingAnalysis); // 切换详细的引用信息展示 toggleGroundingDetails(groundingAnalysis); }} > 🖼️ {groundingAnalysis.groundingInfo.sourceCount} ) : null; switch (node.node_type) { case MarkdownNodeType.Document: return (
{node.children.map((child, childIndex) => renderNode(child, depth + 1, childIndex))}
); case MarkdownNodeType.Heading: const level = parseInt(node.attributes.level || '1'); const HeadingTag = `h${Math.min(level, 6)}` as keyof JSX.IntrinsicElements; const headingClasses = { 1: 'text-lg font-bold mb-2', 2: 'text-base font-semibold mb-2', 3: 'font-medium mb-1', 4: 'font-medium mb-1 text-sm', 5: 'font-medium mb-1 text-sm', 6: 'font-medium mb-1 text-xs' }; return ( {node.children.map((child, childIndex) => renderNode(child, depth + 1, childIndex))} ); case MarkdownNodeType.Paragraph: return (

{node.children.map((child, childIndex) => renderNode(child, depth + 1, childIndex))}

); case MarkdownNodeType.List: const isOrdered = node.content.trim().match(/^\d+\./); const ListTag = isOrdered ? 'ol' : 'ul'; const listClass = isOrdered ? 'list-decimal ml-4 mb-2' : 'list-disc ml-4 mb-2'; return ( {node.children.map((child, childIndex) => renderNode(child, depth + 1, childIndex))} ); case MarkdownNodeType.ListItem: return (
  • {node.children.map((child, childIndex) => renderNode(child, depth + 1, childIndex))}
  • ); case MarkdownNodeType.CodeBlock: const language = node.attributes.language || ''; return (
                
                  {markdownService.extractTextContent(node)}
                
              
    ); case MarkdownNodeType.InlineCode: return ( {markdownService.extractTextContent(node)} ); case MarkdownNodeType.Link: const url = node.attributes.url || '#'; const title = node.attributes.title; return ( {node.children.map((child, childIndex) => renderNode(child, depth + 1, childIndex))} ); case MarkdownNodeType.Image: const src = node.attributes.src || ''; const alt = node.attributes.alt || ''; return ( {alt} ); case MarkdownNodeType.Strong: return ( {node.children.map((child, childIndex) => renderNode(child, depth + 1, childIndex, false))} ); case MarkdownNodeType.Emphasis: return ( {node.children.map((child, childIndex) => renderNode(child, depth + 1, childIndex))} ); case MarkdownNodeType.Blockquote: return (
    {node.children.map((child, childIndex) => renderNode(child, depth + 1, childIndex))}
    ); case MarkdownNodeType.Text: return {node.content} {GroundingIndicator}; case MarkdownNodeType.LineBreak: return
    ; default: return (
    {node.children.map((child, childIndex) => renderNode(child, depth + 1, childIndex))}
    ); } }, [analyzeNodeGrounding]); return (
    {/* 加载状态 */} {isLoading && (
    解析中...
    )} {/* 错误状态 */} {error && (
    解析错误
    {error}
    )} {/* Markdown内容渲染 */} {enableMarkdown && parseResult && !isLoading && !error ? (
    {renderNode(parseResult.root)}
    ) : !enableMarkdown ? (
    {content}
    ) : null} {/* 解析统计信息 */} {showStatistics && parseResult && (
    解析统计
    节点数: {parseResult.statistics.total_nodes}
    错误数: {parseResult.statistics.error_nodes}
    解析时间: {parseResult.statistics.parse_time_ms}ms
    最大深度: {parseResult.statistics.max_depth}
    )} {/* 验证结果 */} {validation && !validation.is_valid && (
    文档验证警告
    {validation.issues.slice(0, 3).map((issue, index) => (
    • {issue.message}
    ))} {validation.issues.length > 3 && (
    还有 {validation.issues.length - 3} 个问题...
    )}
    )} {/* Grounding详情内联展开 */} {expandedGrounding && (

    🖼️ 相关素材详情 ({expandedGrounding.groundingInfo?.sourceCount || 0})

    {expandedGrounding.groundingInfo?.sources && expandedGrounding.groundingInfo.sources.length > 0 ? (
    {expandedGrounding.groundingInfo.sources.map((source: any, index: number) => (
    ))}
    ) : (
    🖼️

    暂无相关图片素材

    该内容段落没有关联的图片资源

    )}
    )} {/* 图片预览模态框 */}
    ); }; export default EnhancedMarkdownRenderer;