fix: 优化markdown解析器
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
|
||||
{/* 显示引用来源信息(如果有的话) */}
|
||||
|
||||
Reference in New Issue
Block a user