fix: 优化界面展 及引用关系
This commit is contained in:
177
apps/desktop/src/components/EnhancedMarkdownRenderer.tsx
Normal file
177
apps/desktop/src/components/EnhancedMarkdownRenderer.tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { GroundingMetadata, GroundingSupport, GroundingSource } from '../types/ragGrounding';
|
||||
import { ReferenceFootnote } from './ReferenceFootnote';
|
||||
|
||||
/**
|
||||
* 文本片段与引用信息
|
||||
*/
|
||||
interface TextSegmentWithReference {
|
||||
text: string;
|
||||
startIndex: number;
|
||||
endIndex: number;
|
||||
groundingSupport?: GroundingSupport;
|
||||
sources?: GroundingSource[];
|
||||
referenceIndex?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 增强Markdown渲染器属性接口
|
||||
*/
|
||||
interface EnhancedMarkdownRendererProps {
|
||||
/** 要渲染的文本内容 */
|
||||
content: string;
|
||||
/** Grounding元数据 */
|
||||
groundingMetadata?: GroundingMetadata;
|
||||
/** 是否启用角标引用 */
|
||||
enableReferences?: boolean;
|
||||
/** 是否启用Markdown渲染 */
|
||||
enableMarkdown?: boolean;
|
||||
/** 自定义样式类名 */
|
||||
className?: string;
|
||||
/** 引用点击回调 */
|
||||
onReferenceClick?: (sources: GroundingSource[], index: number) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 增强Markdown渲染器组件
|
||||
* 支持角标引用和grounding高亮
|
||||
*/
|
||||
export const EnhancedMarkdownRenderer: React.FC<EnhancedMarkdownRendererProps> = ({
|
||||
content,
|
||||
groundingMetadata,
|
||||
enableReferences = true,
|
||||
enableMarkdown = true,
|
||||
className = '',
|
||||
onReferenceClick
|
||||
}) => {
|
||||
const [selectedReference, setSelectedReference] = useState<number | null>(null);
|
||||
|
||||
// 解析文本片段和引用信息
|
||||
const textSegments = useMemo(() => {
|
||||
if (!groundingMetadata?.grounding_supports?.length) {
|
||||
return [{ text: content, startIndex: 0, endIndex: content.length }];
|
||||
}
|
||||
|
||||
const segments: TextSegmentWithReference[] = [];
|
||||
const supports = [...groundingMetadata.grounding_supports].sort(
|
||||
(a, b) => a.start_index - b.start_index
|
||||
);
|
||||
|
||||
let currentIndex = 0;
|
||||
let referenceCounter = 1;
|
||||
|
||||
supports.forEach((support) => {
|
||||
// 添加支持前的文本
|
||||
if (currentIndex < support.start_index) {
|
||||
segments.push({
|
||||
text: content.slice(currentIndex, support.start_index),
|
||||
startIndex: currentIndex,
|
||||
endIndex: support.start_index
|
||||
});
|
||||
}
|
||||
|
||||
// 获取相关来源
|
||||
const sources = groundingMetadata.sources?.filter(source =>
|
||||
support.grounding_chunk_indices?.includes(source.chunk_index || 0)
|
||||
) || [];
|
||||
|
||||
// 添加支持的文本片段
|
||||
segments.push({
|
||||
text: content.slice(support.start_index, support.end_index),
|
||||
startIndex: support.start_index,
|
||||
endIndex: support.end_index,
|
||||
groundingSupport: support,
|
||||
sources,
|
||||
referenceIndex: sources.length > 0 ? referenceCounter++ : undefined
|
||||
});
|
||||
|
||||
currentIndex = support.end_index;
|
||||
});
|
||||
|
||||
// 添加剩余文本
|
||||
if (currentIndex < content.length) {
|
||||
segments.push({
|
||||
text: content.slice(currentIndex),
|
||||
startIndex: currentIndex,
|
||||
endIndex: content.length
|
||||
});
|
||||
}
|
||||
|
||||
return segments;
|
||||
}, [content, groundingMetadata]);
|
||||
|
||||
// 处理引用点击
|
||||
const handleReferenceClick = (sources: GroundingSource[], index: number) => {
|
||||
setSelectedReference(selectedReference === index ? null : index);
|
||||
onReferenceClick?.(sources, index);
|
||||
};
|
||||
|
||||
// 渲染文本片段
|
||||
const renderSegment = (segment: TextSegmentWithReference, index: number) => {
|
||||
const hasReference = segment.referenceIndex && segment.sources?.length;
|
||||
const isHighlighted = hasReference && selectedReference === segment.referenceIndex;
|
||||
|
||||
return (
|
||||
<span
|
||||
key={index}
|
||||
className={`
|
||||
${hasReference ? 'relative text-highlight' : ''}
|
||||
${isHighlighted ? 'bg-blue-100 rounded px-1' : ''}
|
||||
`}
|
||||
>
|
||||
{enableMarkdown ? (
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
p: ({ children }) => <>{children}</>,
|
||||
h1: ({ children }) => <strong className="text-lg font-bold">{children}</strong>,
|
||||
h2: ({ children }) => <strong className="text-base font-semibold">{children}</strong>,
|
||||
h3: ({ children }) => <strong className="font-medium">{children}</strong>,
|
||||
ul: ({ children }) => <div className="ml-4">{children}</div>,
|
||||
ol: ({ children }) => <div className="ml-4">{children}</div>,
|
||||
li: ({ children }) => <div className="flex items-start"><span className="mr-2">•</span><span>{children}</span></div>,
|
||||
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">
|
||||
{children}
|
||||
</blockquote>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{segment.text}
|
||||
</ReactMarkdown>
|
||||
) : (
|
||||
segment.text
|
||||
)}
|
||||
|
||||
{/* 角标引用 */}
|
||||
{enableReferences && hasReference && (
|
||||
<ReferenceFootnote
|
||||
index={segment.referenceIndex!}
|
||||
sources={segment.sources!}
|
||||
className="ml-1 align-super"
|
||||
onClick={(sources) => handleReferenceClick(sources, segment.referenceIndex!)}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`enhanced-markdown-renderer ${className}`}>
|
||||
<div className="prose prose-sm max-w-none leading-relaxed">
|
||||
{textSegments.map(renderSegment)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnhancedMarkdownRenderer;
|
||||
Reference in New Issue
Block a user