/**
* @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;