/** * @Description: Scroll List * @Version: 0.0.1 * @Author: yujiangping * @Date: 2025-11-18 16:19:58 * @LastEditors: yujiangping * @LastEditTime: 2025-12-22 13:47:53 */ import { FileOutlined, FieldTimeOutlined, EditOutlined } from '@ant-design/icons'; import { Skeleton } from 'antd'; import { useTranslation } from 'react-i18next'; import type { RecallTestData } from '@/views/KnowledgeBase/types'; import { NoData } from './noData'; import { formatDateTime } from '@/utils/format'; import InfiniteScroll from 'react-infinite-scroll-component'; import RbMarkdown from '@/components/Markdown'; import { useMemo } from 'react'; interface RecallTestResultProps { data: RecallTestData[]; showEmpty?: boolean; hasMore?: boolean; loadMore?: () => void; loading?: boolean; scrollableTarget?: string; editable?: boolean; // Whether editable onItemClick?: (item: RecallTestData, index: number) => void; // Click item callback parserMode?: number; // Parser mode, 1 means QA format } const RecallTestResult = ({ data, showEmpty = true, hasMore = false, loadMore, loading = false, scrollableTarget, editable = false, onItemClick, parserMode = 0, }: RecallTestResultProps) => { const { t } = useTranslation(); // Parse QA format content const parseQAContent = (content: string) => { if (!content || parserMode !== 1) return null; const qaRegex = /question:\s*(.*?)\s*answer:\s*(.*?)$/s; const match = content.match(qaRegex); if (match) { const question = match[1]?.trim() || ''; const answer = match[2]?.trim() || ''; return { question, answer }; } return null; }; // Format QA content for display const formatQAContent = (question: string, answer: string) => { return `**${t('knowledgeBase.question')}:** ${question}\n**${t('knowledgeBase.answer')}:** ${answer}`; }; // Check if content is valid HTML const isValidHTML = (content: string): boolean => { if (!content) return false; // Check if content contains HTML tags const htmlTagPattern = /<[^>]+>/; return htmlTagPattern.test(content); }; // Render content with HTML or Markdown fallback const renderTextContent = useMemo(() => { return (content: string) => { // Try to render as HTML first if (isValidHTML(content)) { try { return (
); } catch (error) { console.warn('HTML parsing failed, falling back to Markdown:', error); } } // Fallback to Markdown rendering return ; }; }, []); const handleItemClick = (e: React.MouseEvent, item: RecallTestData, index: number) => { // Check if the click is on an image or image-related element const target = e.target as HTMLElement; // Check if clicked on image itself, image container, preview layer, close button or SVG icon if ( target.tagName === 'IMG' || target.tagName === 'SVG' || // SVG icon target.tagName === 'PATH' || // SVG path target.closest('.ant-image') || target.closest('.ant-image-preview') || target.closest('.ant-image-preview-wrap') || target.closest('.ant-image-preview-operations') || target.closest('.anticon') || // Ant Design icon target.classList.contains('ant-image-img') || target.classList.contains('ant-image-mask') || target.classList.contains('ant-image-preview-close') || target.classList.contains('anticon') ) { return; } if (editable && onItemClick) { onItemClick(item, index); } }; // Get color class based on score const getScoreColorClass = (score: number): string => { const percentage = score * 100; if (percentage >= 90) { return 'rb:text-[#155EEF]'; } else if (percentage >= 80) { return 'rb:text-[#369F21]'; } else { return 'rb:text-[#FF5D34]'; } }; // Show skeleton when initial loading if (loading && data.length === 0) { return (
{t('knowledgeBase.recallResult')}
); } if (data.length === 0 && showEmpty) { return ( ); } if (data.length === 0) { return null; } const renderContent = () => (
{data.map((item, index) => { const score = item.metadata?.score ?? 1; const scorePercentage = score * 100; const colorClass = getScoreColorClass(score); const showScore = item.metadata?.score !== null && item.metadata?.score !== undefined; return (
handleItemClick(e, item, index)} > {editable && (
)}
{showScore && ( {scorePercentage.toFixed(1)}% {t('knowledgeBase.similarity')} )}
{item.metadata?.file_name || '-'} chunk_{item.metadata?.sort_id || index}
{(() => { const qaContent = parseQAContent(item.page_content); if (qaContent) { const formattedContent = formatQAContent(qaContent.question, qaContent.answer); return renderTextContent(formattedContent); } return renderTextContent(item.page_content); })()}
{item.metadata?.file_created_at && (
{formatDateTime(item.metadata.file_created_at)}
)}
); })} {loading && (
)}
); // If loadMore and hasMore are provided, use InfiniteScroll if (loadMore && hasMore !== undefined) { return (
{t('knowledgeBase.recallResult')} ({data.length} results)
} scrollableTarget={scrollableTarget} > {renderContent()}
); } // Otherwise use normal rendering return (
{t('knowledgeBase.recallResult')} ({data.length} results)
{renderContent()}
); }; export default RecallTestResult;