Files
MemoryBear/web/src/views/KnowledgeBase/components/RecallTestResult.tsx

188 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* @Description: 滚动列表
* @Version: 0.0.1
* @Author: yujiangping
* @Date: 2025-11-18 16:19:58
* @LastEditors: yujiangping
* @LastEditTime: 2025-11-29 19:08:40
*/
import { FileOutlined, FieldTimeOutlined, EditOutlined } from '@ant-design/icons';
import { Skeleton } from 'antd';
import { useTranslation } from 'react-i18next';
import type { RecallTestData } from '../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; // 是否可编辑
onItemClick?: (item: RecallTestData, index: number) => void; // 点击项的回调
}
const RecallTestResult = ({
data,
showEmpty = true,
hasMore = false,
loadMore,
loading = false,
scrollableTarget,
editable = false,
onItemClick,
}: RecallTestResultProps) => {
const { t } = useTranslation();
const handleItemClick = (e: React.MouseEvent, item: RecallTestData, index: number) => {
// 检查点击的是否是图片或图片相关元素
const target = e.target as HTMLElement;
// 检查是否点击了图片本身、图片的容器、预览层、关闭按钮或 SVG 图标
if (
target.tagName === 'IMG' ||
target.tagName === 'SVG' || // SVG 图标
target.tagName === 'PATH' || // SVG 路径
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 图标
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);
}
};
// 根据分数获取颜色类名
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'>
<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>
);
// 如果提供了 loadMore 和 hasMore使用 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>
);
}
// 否则使用普通渲染
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;