Files
MemoryBear/web/src/views/KnowledgeBase/components/RecallTestResult.tsx
yujiangping 6e0407f404 style(web): translate Chinese comments to English in KnowledgeBase views
- Translate all Chinese comments to English in CreateDataset component
- Translate Chinese comments in DocumentDetails, Private, and Share pages
- Translate Chinese comments in all KnowledgeBase modal components (CreateContentModal, CreateDatasetModal, CreateFolderModal, etc.)
- Translate Chinese comments in KnowledgeGraph, RecallTest, and related components
- Translate Chinese comments in datasets and index files
- Improve code readability and maintain consistency with existing English codebase
- Ensure all inline comments and console logs use English for better maintainability
2026-02-03 17:08:22 +08:00

218 lines
7.7 KiB
TypeScript

/**
* @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';
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}`;
};
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]';
}
};
if (data.length === 0 && showEmpty) {
return (
<NoData
title={t('knowledgeBase.recallTestUnStart')}
subTitle={t('knowledgeBase.recallTestUnStartSubTitle')}
/>
);
}
if (data.length === 0) {
return null;
}
const renderContent = () => (
<div className='rb:flex rb:flex-col rb:mt-4'>
{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 (
<div
key={`${item.metadata?.sort_id || index}-${index}`}
className={`rb:flex rb:flex-col rb:mb-4 rb:rounded-lg rb:border rb:border-[#DFE4ED] rb:bg-[#FBFDFF] rb:p-4 rb:pt-2 rb:pb-3 rb:relative rb:group ${editable ? 'rb:cursor-pointer rb:transition-all hover:rb:border-[#155EEF] hover:rb:shadow-md' : ''}`}
onClick={(e) => handleItemClick(e, item, index)}
>
{editable && (
<div className='rb:absolute rb:top-2 rb:right-2 rb:opacity-0 group-hover:rb:opacity-100 rb:transition-opacity'>
<EditOutlined className='rb:text-[#155EEF] rb:text-base' />
</div>
)}
<div className='rb:flex rb:items-center rb:justify-between'>
{showScore && (
<span className={`${colorClass} rb:text-xl rb:font-semibold`}>
{scorePercentage.toFixed(1)}% {t('knowledgeBase.similarity')}
</span>
)}
<div className={`rb:flex rb:mt-2 rb:flex-col rb:items-end rb:justify-end rb:gap-1 ${!showScore ? 'rb:w-full' : ''}`}>
<span className='rb:text-gray-800'>
<FileOutlined /> {item.metadata?.file_name || '-'}
</span>
<span className='rb:text-gray-500 rb:text-xs rb:bg-[#F0F3F8] rb:px-1 rb:py-[2px] rb:rounded'>
chunk_{item.metadata?.sort_id || index}
</span>
</div>
</div>
<div className='rb:flex rb:text-left rb:px-4 rb:py-3 rb:bg-[#F0F3F8] rb:rounded-lg rb:mt-2'>
<div className='rb:text-gray-800 rb:text-sm rb:whitespace-pre-wrap rb:break-words rb:w-full'>
{(() => {
const qaContent = parseQAContent(item.page_content);
if (qaContent) {
const formattedContent = formatQAContent(qaContent.question, qaContent.answer);
return <RbMarkdown content={formattedContent} showHtmlComments={true} />;
}
return <RbMarkdown content={item.page_content} showHtmlComments={true} />;
})()}
</div>
</div>
{item.metadata?.file_created_at && (
<div className='rb:flex rb:items-center rb:justify-start rb:mt-3'>
<span className='rb:text-gray-500 rb:text-xs'>
<FieldTimeOutlined /> {formatDateTime(item.metadata.file_created_at)}
</span>
</div>
)}
</div>
);
})}
{loading && (
<div className='rb:mb-4'>
<Skeleton active paragraph={{ rows: 3 }} />
</div>
)}
</div>
);
// If loadMore and hasMore are provided, use InfiniteScroll
if (loadMore && hasMore !== undefined) {
return (
<div className='rb:flex rb:h-full rb:flex-col'>
<div className='rb:flex rb:items-center rb:justify-start rb:gap-2'>
<span className='rb:text-lg rb:font-medium'>{t('knowledgeBase.recallResult')}</span>
<span className='rb:text-gray-500 rb:text-xs rb:pt-[2px]'>
(<span className='rb:text-[#155EEF]'>{data.length}</span> results)
</span>
</div>
<InfiniteScroll
dataLength={data.length}
next={loadMore}
hasMore={hasMore}
loader={<Skeleton active paragraph={{ rows: 3 }} className='rb:mt-4' />}
scrollableTarget={scrollableTarget}
>
{renderContent()}
</InfiniteScroll>
</div>
);
}
// Otherwise use normal rendering
return (
<div className='rb:flex rb:flex-col'>
<div className='rb:flex rb:items-center rb:justify-start rb:gap-2'>
<span className='rb:text-lg rb:font-medium'>{t('knowledgeBase.recallResult')}</span>
<span className='rb:text-gray-500 rb:text-xs rb:pt-[2px]'>
(<span className='rb:text-[#155EEF]'>{data.length}</span> results)
</span>
</div>
{renderContent()}
</div>
);
};
export default RecallTestResult;