fix: 优化markdown解析器

This commit is contained in:
imeepos
2025-07-22 15:04:37 +08:00
parent 31d97834dc
commit f92d9a7c39
24 changed files with 5702 additions and 703 deletions

View File

@@ -1,7 +1,12 @@
import React from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
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';
/**
* 增强Markdown渲染器属性接口
@@ -17,58 +22,265 @@ interface EnhancedMarkdownRendererProps {
enableMarkdown?: boolean;
/** 自定义样式类名 */
className?: string;
/** 是否显示解析统计信息 */
showStatistics?: boolean;
/** 是否启用实时解析 */
enableRealTimeParsing?: boolean;
}
/**
* 增强Markdown渲染器组件
* 暂时简化版本专注于正确渲染Markdown内容
* 使用基于Tree-sitter的MarkdownService替代ReactMarkdown
*/
export const EnhancedMarkdownRenderer: React.FC<EnhancedMarkdownRendererProps> = ({
content,
groundingMetadata,
enableReferences = true,
enableMarkdown = true,
className = ''
className = '',
showStatistics = false,
enableRealTimeParsing = false
}) => {
const [parseResult, setParseResult] = useState<MarkdownParseResult | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [validation, setValidation] = useState<ValidationResult | null>(null);
// 调试信息
console.log('📝 EnhancedMarkdownRenderer Debug:', {content, groundingMetadata});
// 解析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);
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]);
// 渲染单个Markdown节点
const renderNode = useCallback((node: MarkdownNode, depth: number = 0): React.ReactNode => {
const key = `${node.range?.start?.line || 0}-${node.range?.start?.column || 0}-${depth}`;
// 尝试使用 groundingMetadata 解析出来 当前节点引用的那部分资源 然后打印
switch (node.node_type) {
case MarkdownNodeType.Document:
return (
<div key={key} className="markdown-document">
{node.children.map((child) => renderNode(child, depth + 1))}
</div>
);
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 (+
<HeadingTag key={key} className={headingClasses[level as keyof typeof headingClasses] || headingClasses[3]}>
{node.children.map((child) => renderNode(child, depth + 1))}
</HeadingTag>
);
case MarkdownNodeType.Paragraph:
return (
<p key={key} className="mb-2">
{node.children.map((child) => renderNode(child, depth + 1))}
</p>
);
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 (
<ListTag key={key} className={listClass}>
{node.children.map((child) => renderNode(child, depth + 1))}
</ListTag>
);
case MarkdownNodeType.ListItem:
return (
<li key={key} className="mb-1">
{node.children.map((child) => renderNode(child, depth + 1))}
</li>
);
case MarkdownNodeType.CodeBlock:
const language = node.attributes.language || '';
return (
<pre key={key} className="bg-gray-100 p-3 rounded mb-2 overflow-x-auto">
<code className={`language-${language} text-sm font-mono`}>
{markdownService.extractTextContent(node)}
</code>
</pre>
);
case MarkdownNodeType.InlineCode:
return (
<code key={key} className="bg-gray-100 px-1 py-0.5 rounded text-sm font-mono">
{markdownService.extractTextContent(node)}
</code>
);
case MarkdownNodeType.Link:
const url = node.attributes.url || '#';
const title = node.attributes.title;
return (
<a
key={key}
href={url}
title={title}
className="text-blue-600 hover:text-blue-800 underline"
target="_blank"
rel="noopener noreferrer"
>
{node.children.map((child) => renderNode(child, depth + 1))}
</a>
);
case MarkdownNodeType.Image:
const src = node.attributes.src || '';
const alt = node.attributes.alt || '';
return (
<img
key={key}
src={src}
alt={alt}
className="max-w-full h-auto mb-2"
/>
);
case MarkdownNodeType.Strong:
return (
<strong key={key}>
{node.children.map((child) => renderNode(child, depth + 1))}
</strong>
);
case MarkdownNodeType.Emphasis:
return (
<em key={key}>
{node.children.map((child) => renderNode(child, depth + 1))}
</em>
);
case MarkdownNodeType.Blockquote:
return (
<blockquote key={key} className="border-l-4 border-gray-300 pl-4 italic text-gray-600 mb-2">
{node.children.map((child) => renderNode(child, depth + 1))}
</blockquote>
);
case MarkdownNodeType.Text:
return <span key={key}>{node.content}</span>;
case MarkdownNodeType.LineBreak:
return <br key={key} />;
default:
return (
<div key={key} className="unknown-node">
{node.children.map((child) => renderNode(child, depth + 1))}
</div>
);
}
}, []);
// 暂时禁用角标功能直接渲染Markdown以避免重复渲染问题
// TODO: 未来需要实现更复杂的Markdown + 角标集成方案
return (
<div className={`enhanced-markdown-renderer ${className}`}>
{enableMarkdown ? (
<div className="prose prose-sm max-w-none leading-relaxed">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
p: ({ children }) => <p className="mb-2">{children}</p>,
h1: ({ children }) => <h1 className="text-lg font-bold mb-2">{children}</h1>,
h2: ({ children }) => <h2 className="text-base font-semibold mb-2">{children}</h2>,
h3: ({ children }) => <h3 className="font-medium mb-1">{children}</h3>,
ul: ({ children }) => <ul className="list-disc ml-4 mb-2">{children}</ul>,
ol: ({ children }) => <ol className="list-decimal ml-4 mb-2">{children}</ol>,
li: ({ children }) => <li className="mb-1">{children}</li>,
strong: ({ children }) => <strong>{children}</strong>,
em: ({ children }) => <em>{children}</em>,
code: ({ children }) => (
<code className="bg-gray-100 px-1 py-0.5 rounded text-sm font-mono">
{children}
</code>
),
blockquote: ({ children }) => (
<blockquote className="border-l-4 border-gray-300 pl-4 italic text-gray-600 mb-2">
{children}
</blockquote>
),
}}
>
{content}
</ReactMarkdown>
{/* 加载状态 */}
{isLoading && (
<div className="flex items-center justify-center p-4">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600"></div>
<span className="ml-2 text-sm text-gray-600">...</span>
</div>
) : (
)}
{/* 错误状态 */}
{error && (
<div className="bg-red-50 border border-red-200 rounded p-3 mb-4">
<div className="text-red-800 font-medium text-sm"></div>
<div className="text-red-600 text-sm mt-1">{error}</div>
</div>
)}
{/* Markdown内容渲染 */}
{enableMarkdown && parseResult && !isLoading && !error ? (
<div className="prose prose-sm max-w-none leading-relaxed">
{renderNode(parseResult.root)}
</div>
) : !enableMarkdown ? (
<div className="leading-relaxed whitespace-pre-wrap">{content}</div>
) : null}
{/* 解析统计信息 */}
{showStatistics && parseResult && (
<div className="mt-4 p-3 bg-gray-50 rounded text-xs">
<div className="font-medium mb-1"></div>
<div className="grid grid-cols-2 gap-2">
<div>: {parseResult.statistics.total_nodes}</div>
<div>: {parseResult.statistics.error_nodes}</div>
<div>: {parseResult.statistics.parse_time_ms}ms</div>
<div>: {parseResult.statistics.max_depth}</div>
</div>
</div>
)}
{/* 验证结果 */}
{validation && !validation.is_valid && (
<div className="mt-4 p-3 bg-yellow-50 border border-yellow-200 rounded">
<div className="text-yellow-800 font-medium text-sm mb-2"></div>
<div className="space-y-1">
{validation.issues.slice(0, 3).map((issue, index) => (
<div key={index} className="text-yellow-700 text-xs">
{issue.message}
</div>
))}
{validation.issues.length > 3 && (
<div className="text-yellow-600 text-xs">
{validation.issues.length - 3} ...
</div>
)}
</div>
</div>
)}
{/* 显示引用来源信息(如果有的话) */}