304 lines
12 KiB
TypeScript
304 lines
12 KiB
TypeScript
import { FileSearchOutlined, SearchOutlined } from '@ant-design/icons';
|
|
import { Button, Card, Flex, Input, InputNumber, Select, Slider, Spin, Switch, Tag, Tooltip } from 'antd';
|
|
import type { RetrieveRecord } from '~/api/dify-dataset/type';
|
|
import { useRetrieveTest } from '~/hooks/dify-dataset-manager/retrieve-test';
|
|
import type { RetrieveTestProps } from '~/types/dify-dataset-manager/retrieve-test';
|
|
|
|
// 颜色常量
|
|
const colors = {
|
|
bgContainer: '#fff',
|
|
bgLayout: '#f5f5f5',
|
|
bgElevated: '#fafafa',
|
|
border: '#e8e8e8',
|
|
text: '#262626',
|
|
textSecondary: '#8c8c8c',
|
|
textTertiary: '#bfbfbf',
|
|
textQuaternary: '#d9d9d9',
|
|
fillTertiary: '#f0f0f0',
|
|
};
|
|
|
|
/**
|
|
* 检索结果项组件
|
|
*/
|
|
function ResultItem({ record, index }: { record: RetrieveRecord; index: number }) {
|
|
const scorePercent = (record.score * 100).toFixed(1);
|
|
const scoreColor = record.score > 0.8 ? '#52c41a' : record.score > 0.5 ? '#faad14' : '#666';
|
|
|
|
return (
|
|
<div className="segment-item">
|
|
<div className="segment-header">
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
<Tag style={{ background: scoreColor, color: '#fff', border: 'none', margin: 0 }}>
|
|
{scorePercent}%
|
|
</Tag>
|
|
<span className="segment-index">#{index + 1}</span>
|
|
<span className="segment-chars">
|
|
{record.segment.word_count} 字 · 命中 {record.segment.hit_count} 次
|
|
</span>
|
|
</div>
|
|
{record.segment.document && (
|
|
<span className="segment-chars">
|
|
来源: {record.segment.document.name}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="segment-content">
|
|
{record.segment.content.length > 500
|
|
? record.segment.content.substring(0, 500) + '...'
|
|
: record.segment.content}
|
|
</div>
|
|
{record.segment.answer && (
|
|
<div style={{
|
|
padding: 12,
|
|
background: '#f0f0f0',
|
|
borderRadius: 6,
|
|
marginTop: 8,
|
|
}}>
|
|
<span style={{ color: '#8c8c8c', fontSize: 12, display: 'block', marginBottom: 4 }}>
|
|
答案:
|
|
</span>
|
|
<span style={{ color: '#262626', fontSize: 14 }}>
|
|
{record.segment.answer.length > 200
|
|
? record.segment.answer.substring(0, 200) + '...'
|
|
: record.segment.answer}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 召回测试组件
|
|
*/
|
|
export default function RetrieveTest({ datasetId }: RetrieveTestProps) {
|
|
const {
|
|
searchQuery,
|
|
setSearchQuery,
|
|
retrieveResults,
|
|
retrieving,
|
|
searchMethod,
|
|
setSearchMethod,
|
|
topK,
|
|
setTopK,
|
|
scoreThresholdEnabled,
|
|
setScoreThresholdEnabled,
|
|
scoreThreshold,
|
|
setScoreThreshold,
|
|
handleRetrieve,
|
|
} = useRetrieveTest(datasetId);
|
|
|
|
// 检索方式选项(只有3种)
|
|
const searchMethodOptions = [
|
|
{ label: '向量检索', value: 'semantic_search' },
|
|
{ label: '全文检索', value: 'full_text_search' },
|
|
{ label: '混合检索', value: 'hybrid_search' },
|
|
];
|
|
|
|
return (
|
|
<Flex
|
|
style={{
|
|
height: '100%',
|
|
minHeight: 'calc(100vh - 120px)',
|
|
}}
|
|
>
|
|
{/* 左侧面板 - 输入区域 */}
|
|
<Flex
|
|
vertical
|
|
gap={16}
|
|
style={{
|
|
width: 400,
|
|
minWidth: 400,
|
|
padding: 20,
|
|
background: colors.bgLayout,
|
|
borderRight: `1px solid ${colors.border}`,
|
|
}}
|
|
>
|
|
{/* 标题 */}
|
|
<Flex vertical gap={4}>
|
|
<h2 style={{
|
|
margin: 0,
|
|
fontSize: 18,
|
|
fontWeight: 600,
|
|
color: colors.text,
|
|
}}>
|
|
召回测试
|
|
</h2>
|
|
<span style={{
|
|
fontSize: 13,
|
|
color: colors.textSecondary,
|
|
}}>
|
|
根据给定的查询文本测试知识库召回效果
|
|
</span>
|
|
</Flex>
|
|
|
|
{/* 查询输入区 */}
|
|
<Flex vertical gap={8}>
|
|
<Flex justify="space-between" align="center">
|
|
<span style={{
|
|
fontSize: 13,
|
|
color: colors.text,
|
|
fontWeight: 500,
|
|
}}>
|
|
源文本
|
|
</span>
|
|
<Select
|
|
value={searchMethod}
|
|
onChange={(value) => setSearchMethod(value as any)}
|
|
options={searchMethodOptions}
|
|
style={{ width: 130 }}
|
|
size="small"
|
|
/>
|
|
</Flex>
|
|
<Input.TextArea
|
|
placeholder="请输入文本,建议使用简短的陈述句。"
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
onPressEnter={(e) => {
|
|
if (!e.shiftKey) {
|
|
e.preventDefault();
|
|
handleRetrieve();
|
|
}
|
|
}}
|
|
autoSize={{ minRows: 6, maxRows: 12 }}
|
|
style={{
|
|
background: colors.bgContainer,
|
|
resize: 'none',
|
|
}}
|
|
/>
|
|
<Flex justify="space-between" align="center">
|
|
<span style={{
|
|
fontSize: 12,
|
|
color: colors.textTertiary,
|
|
}}>
|
|
{searchQuery.length} / 200
|
|
</span>
|
|
<Button
|
|
type="primary"
|
|
icon={<SearchOutlined />}
|
|
onClick={handleRetrieve}
|
|
loading={retrieving}
|
|
>
|
|
测试
|
|
</Button>
|
|
</Flex>
|
|
</Flex>
|
|
|
|
{/* 检索设置 */}
|
|
<Flex vertical gap={12}>
|
|
<span style={{
|
|
fontSize: 13,
|
|
color: colors.text,
|
|
fontWeight: 500,
|
|
}}>
|
|
检索设置
|
|
</span>
|
|
<Flex align="center" gap={12}>
|
|
<span style={{
|
|
fontSize: 13,
|
|
color: colors.textSecondary,
|
|
whiteSpace: 'nowrap',
|
|
}}>
|
|
返回数量 (Top K):
|
|
</span>
|
|
<Slider
|
|
value={topK}
|
|
onChange={setTopK}
|
|
min={1}
|
|
max={20}
|
|
style={{ flex: 1 }}
|
|
/>
|
|
<span style={{
|
|
fontSize: 13,
|
|
color: colors.text,
|
|
minWidth: 24,
|
|
textAlign: 'right',
|
|
}}>
|
|
{topK}
|
|
</span>
|
|
</Flex>
|
|
|
|
{/* Score 阈值设置 */}
|
|
<Flex align="center" gap={12}>
|
|
<Tooltip title="开启后,只返回相似度分数高于阈值的结果">
|
|
<span style={{
|
|
fontSize: 13,
|
|
color: colors.textSecondary,
|
|
whiteSpace: 'nowrap',
|
|
cursor: 'help',
|
|
}}>
|
|
Score 阈值:
|
|
</span>
|
|
</Tooltip>
|
|
<Switch
|
|
size="small"
|
|
checked={scoreThresholdEnabled}
|
|
onChange={setScoreThresholdEnabled}
|
|
/>
|
|
{scoreThresholdEnabled && (
|
|
<>
|
|
<Slider
|
|
value={scoreThreshold}
|
|
onChange={setScoreThreshold}
|
|
min={0}
|
|
max={1}
|
|
step={0.01}
|
|
style={{ flex: 1 }}
|
|
/>
|
|
<InputNumber
|
|
value={scoreThreshold}
|
|
onChange={(value) => setScoreThreshold(value ?? 0.5)}
|
|
min={0}
|
|
max={1}
|
|
step={0.01}
|
|
size="small"
|
|
style={{ width: 70 }}
|
|
/>
|
|
</>
|
|
)}
|
|
</Flex>
|
|
</Flex>
|
|
</Flex>
|
|
|
|
{/* 右侧面板 - 结果展示 */}
|
|
<div className="preview-panel">
|
|
<Card
|
|
title={
|
|
<div className="preview-header">
|
|
<span>检索结果</span>
|
|
<span className="segment-count">
|
|
{retrieveResults.length > 0 ? `${retrieveResults.length} 条结果` : '0 条结果'}
|
|
</span>
|
|
</div>
|
|
}
|
|
className="preview-card"
|
|
>
|
|
{retrieving ? (
|
|
<div className="preview-loading">
|
|
<Spin size="large" />
|
|
<div className="loading-text">检索中...</div>
|
|
</div>
|
|
) : retrieveResults.length === 0 ? (
|
|
<div className="preview-empty">
|
|
<div className="empty-icon">
|
|
<FileSearchOutlined />
|
|
</div>
|
|
<p>召回测试结果将展示在这里</p>
|
|
</div>
|
|
) : (
|
|
<div className="preview-segments">
|
|
{retrieveResults.map((record, index) => (
|
|
<ResultItem
|
|
key={record.segment.id}
|
|
record={record}
|
|
index={index}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</Card>
|
|
</div>
|
|
</Flex>
|
|
);
|
|
}
|