454 lines
13 KiB
TypeScript
454 lines
13 KiB
TypeScript
|
|
import React, { useState } from 'react';
|
||
|
|
import { useParams, useNavigate } from 'react-router-dom';
|
||
|
|
import { useForm } from 'react-hook-form';
|
||
|
|
import {
|
||
|
|
Box,
|
||
|
|
Container,
|
||
|
|
Typography,
|
||
|
|
Paper,
|
||
|
|
TextField,
|
||
|
|
Button,
|
||
|
|
Slider,
|
||
|
|
FormControl,
|
||
|
|
InputLabel,
|
||
|
|
Select,
|
||
|
|
MenuItem,
|
||
|
|
FormControlLabel,
|
||
|
|
Switch,
|
||
|
|
Fab,
|
||
|
|
Snackbar,
|
||
|
|
Alert,
|
||
|
|
Grid,
|
||
|
|
Card,
|
||
|
|
CardContent,
|
||
|
|
Chip,
|
||
|
|
Divider,
|
||
|
|
Stack,
|
||
|
|
CircularProgress,
|
||
|
|
} from '@mui/material';
|
||
|
|
import {
|
||
|
|
ArrowBack as ArrowBackIcon,
|
||
|
|
Search as SearchIcon,
|
||
|
|
Psychology as PsychologyIcon,
|
||
|
|
} from '@mui/icons-material';
|
||
|
|
import { useKnowledgeDetail } from '@/hooks/knowledge-hooks';
|
||
|
|
import knowledgeService from '@/services/knowledge_service';
|
||
|
|
import type { ITestRetrievalRequestBody } from '@/interfaces/request/knowledge';
|
||
|
|
import type { ITestingResult, ITestingChunk, ITestingDocument } from '@/interfaces/database/knowledge';
|
||
|
|
import KnowledgeBreadcrumbs from './components/KnowledgeBreadcrumbs';
|
||
|
|
|
||
|
|
interface TestFormData {
|
||
|
|
question: string;
|
||
|
|
similarity_threshold: number;
|
||
|
|
vector_similarity_weight: number;
|
||
|
|
rerank_id?: string;
|
||
|
|
top_k: number;
|
||
|
|
use_kg: boolean;
|
||
|
|
highlight: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface ResultViewProps {
|
||
|
|
result: ITestingResult | null;
|
||
|
|
loading: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
function ResultView({ result, loading }: ResultViewProps) {
|
||
|
|
if (loading) {
|
||
|
|
return (
|
||
|
|
<Paper sx={{ p: 3, textAlign: 'center' }}>
|
||
|
|
<CircularProgress />
|
||
|
|
<Typography variant="body2" sx={{ mt: 2 }}>
|
||
|
|
正在检索测试...
|
||
|
|
</Typography>
|
||
|
|
</Paper>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!result) {
|
||
|
|
return (
|
||
|
|
<Paper sx={{ p: 3, textAlign: 'center' }}>
|
||
|
|
<PsychologyIcon sx={{ fontSize: 48, color: 'text.secondary', mb: 2 }} />
|
||
|
|
<Typography variant="h6" color="text.secondary">
|
||
|
|
请输入测试问题并点击测试按钮
|
||
|
|
</Typography>
|
||
|
|
</Paper>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Box>
|
||
|
|
{/* 测试结果概览 */}
|
||
|
|
<Paper sx={{ p: 3, mb: 3 }}>
|
||
|
|
<Typography variant="h6" gutterBottom>
|
||
|
|
测试结果概览
|
||
|
|
</Typography>
|
||
|
|
<Grid container spacing={2}>
|
||
|
|
<Grid size={{xs: 12, sm: 4}}>
|
||
|
|
<Card>
|
||
|
|
<CardContent>
|
||
|
|
<Typography variant="h4" color="primary">
|
||
|
|
{result.total}
|
||
|
|
</Typography>
|
||
|
|
<Typography variant="body2" color="text.secondary">
|
||
|
|
匹配的文档块
|
||
|
|
</Typography>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</Grid>
|
||
|
|
<Grid size={{xs: 12, sm: 4}}>
|
||
|
|
<Card>
|
||
|
|
<CardContent>
|
||
|
|
<Typography variant="h4" color="secondary">
|
||
|
|
{result.documents?.length || 0}
|
||
|
|
</Typography>
|
||
|
|
<Typography variant="body2" color="text.secondary">
|
||
|
|
相关文档
|
||
|
|
</Typography>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</Grid>
|
||
|
|
<Grid size={{xs: 12, sm: 4}}>
|
||
|
|
<Card>
|
||
|
|
<CardContent>
|
||
|
|
<Typography variant="h4" color="success.main">
|
||
|
|
{result.chunks?.length || 0}
|
||
|
|
</Typography>
|
||
|
|
<Typography variant="body2" color="text.secondary">
|
||
|
|
返回的块数
|
||
|
|
</Typography>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</Grid>
|
||
|
|
</Grid>
|
||
|
|
</Paper>
|
||
|
|
|
||
|
|
{/* 匹配的文档块 */}
|
||
|
|
{result.chunks && result.chunks.length > 0 && (
|
||
|
|
<Paper sx={{ p: 3, mb: 3 }}>
|
||
|
|
<Typography variant="h6" gutterBottom>
|
||
|
|
匹配的文档块
|
||
|
|
</Typography>
|
||
|
|
<Stack spacing={2}>
|
||
|
|
{result.chunks.map((chunk: ITestingChunk, index: number) => (
|
||
|
|
<Card key={chunk.id || index} variant="outlined">
|
||
|
|
<CardContent>
|
||
|
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2 }}>
|
||
|
|
<Typography variant="subtitle2" color="primary">
|
||
|
|
文档: {chunk.document_name}
|
||
|
|
</Typography>
|
||
|
|
<Stack direction="row" spacing={1}>
|
||
|
|
<Chip
|
||
|
|
label={`相似度: ${(chunk.similarity * 100).toFixed(1)}%`}
|
||
|
|
size="small"
|
||
|
|
color="primary"
|
||
|
|
variant="outlined"
|
||
|
|
/>
|
||
|
|
{chunk.vector_similarity && (
|
||
|
|
<Chip
|
||
|
|
label={`向量: ${(chunk.vector_similarity * 100).toFixed(1)}%`}
|
||
|
|
size="small"
|
||
|
|
color="secondary"
|
||
|
|
variant="outlined"
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
{chunk.term_similarity && (
|
||
|
|
<Chip
|
||
|
|
label={`词项: ${(chunk.term_similarity * 100).toFixed(1)}%`}
|
||
|
|
size="small"
|
||
|
|
color="info"
|
||
|
|
variant="outlined"
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
</Stack>
|
||
|
|
</Box>
|
||
|
|
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap' }}>
|
||
|
|
{chunk.content || '内容不可用'}
|
||
|
|
</Typography>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
))}
|
||
|
|
</Stack>
|
||
|
|
</Paper>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* 相关文档统计 */}
|
||
|
|
{result.documents && result.documents.length > 0 && (
|
||
|
|
<Paper sx={{ p: 3 }}>
|
||
|
|
<Typography variant="h6" gutterBottom>
|
||
|
|
相关文档统计
|
||
|
|
</Typography>
|
||
|
|
<Stack spacing={1}>
|
||
|
|
{result.documents.map((doc: ITestingDocument, index: number) => (
|
||
|
|
<Box key={doc.doc_id || index} sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||
|
|
<Typography variant="body2">
|
||
|
|
{doc.doc_name}
|
||
|
|
</Typography>
|
||
|
|
<Chip
|
||
|
|
label={`${doc.count} 个匹配块`}
|
||
|
|
size="small"
|
||
|
|
color="default"
|
||
|
|
/>
|
||
|
|
</Box>
|
||
|
|
))}
|
||
|
|
</Stack>
|
||
|
|
</Paper>
|
||
|
|
)}
|
||
|
|
</Box>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function KnowledgeBaseTesting() {
|
||
|
|
const { id } = useParams<{ id: string }>();
|
||
|
|
const navigate = useNavigate();
|
||
|
|
const [testResult, setTestResult] = useState<ITestingResult | null>(null);
|
||
|
|
const [testing, setTesting] = useState(false);
|
||
|
|
const [snackbar, setSnackbar] = useState<{
|
||
|
|
open: boolean;
|
||
|
|
message: string;
|
||
|
|
severity: 'success' | 'error';
|
||
|
|
}>({
|
||
|
|
open: false,
|
||
|
|
message: '',
|
||
|
|
severity: 'success',
|
||
|
|
});
|
||
|
|
|
||
|
|
// 获取知识库详情
|
||
|
|
const { knowledge, loading: detailLoading } = useKnowledgeDetail(id || '');
|
||
|
|
|
||
|
|
// 测试表单
|
||
|
|
const form = useForm<TestFormData>({
|
||
|
|
defaultValues: {
|
||
|
|
question: '',
|
||
|
|
similarity_threshold: 0.2,
|
||
|
|
vector_similarity_weight: 0.3,
|
||
|
|
rerank_id: '',
|
||
|
|
top_k: 6,
|
||
|
|
use_kg: false,
|
||
|
|
highlight: true,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
const { register, handleSubmit, watch, setValue, formState: { errors } } = form;
|
||
|
|
|
||
|
|
const handleTestSubmit = async (data: TestFormData) => {
|
||
|
|
if (!id) return;
|
||
|
|
|
||
|
|
try {
|
||
|
|
setTesting(true);
|
||
|
|
|
||
|
|
const requestBody: ITestRetrievalRequestBody = {
|
||
|
|
question: data.question,
|
||
|
|
similarity_threshold: data.similarity_threshold,
|
||
|
|
vector_similarity_weight: data.vector_similarity_weight,
|
||
|
|
top_k: data.top_k,
|
||
|
|
use_kg: data.use_kg,
|
||
|
|
highlight: data.highlight,
|
||
|
|
kb_id: [id],
|
||
|
|
};
|
||
|
|
|
||
|
|
if (data.rerank_id && data.rerank_id.trim()) {
|
||
|
|
requestBody.rerank_id = data.rerank_id;
|
||
|
|
}
|
||
|
|
|
||
|
|
const response = await knowledgeService.retrievalTest(requestBody);
|
||
|
|
|
||
|
|
if (response.data.code === 0) {
|
||
|
|
setTestResult(response.data.data);
|
||
|
|
setSnackbar({
|
||
|
|
open: true,
|
||
|
|
message: '检索测试完成',
|
||
|
|
severity: 'success',
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
throw new Error(response.data.message || '检索测试失败');
|
||
|
|
}
|
||
|
|
} catch (error: any) {
|
||
|
|
setSnackbar({
|
||
|
|
open: true,
|
||
|
|
message: error.message || '检索测试失败',
|
||
|
|
severity: 'error',
|
||
|
|
});
|
||
|
|
} finally {
|
||
|
|
setTesting(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleBackToDetail = () => {
|
||
|
|
navigate(`/knowledge/${id}`);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleCloseSnackbar = () => {
|
||
|
|
setSnackbar(prev => ({ ...prev, open: false }));
|
||
|
|
};
|
||
|
|
|
||
|
|
if (detailLoading) {
|
||
|
|
return (
|
||
|
|
<Container maxWidth="lg" sx={{ py: 4 }}>
|
||
|
|
<Typography>加载中...</Typography>
|
||
|
|
</Container>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Container maxWidth="lg" sx={{ py: 4 }}>
|
||
|
|
{/* 面包屑导航 */}
|
||
|
|
<KnowledgeBreadcrumbs knowledge={knowledge} />
|
||
|
|
|
||
|
|
<Box sx={{ mb: 3 }}>
|
||
|
|
<Typography variant="h4" gutterBottom>
|
||
|
|
知识库测试
|
||
|
|
</Typography>
|
||
|
|
<Typography variant="subtitle1" color="text.secondary">
|
||
|
|
{knowledge?.name}
|
||
|
|
</Typography>
|
||
|
|
</Box>
|
||
|
|
|
||
|
|
<Grid container spacing={3}>
|
||
|
|
{/* 测试表单 */}
|
||
|
|
<Grid size={{xs: 12, sm: 4}}>
|
||
|
|
<Paper sx={{ p: 3, position: 'sticky', top: 20 }}>
|
||
|
|
<Typography variant="h6" gutterBottom>
|
||
|
|
测试配置
|
||
|
|
</Typography>
|
||
|
|
|
||
|
|
<Box component="form" onSubmit={handleSubmit(handleTestSubmit)} sx={{ mt: 2 }}>
|
||
|
|
<TextField
|
||
|
|
{...register('question', { required: '请输入测试问题' })}
|
||
|
|
label="测试问题"
|
||
|
|
multiline
|
||
|
|
rows={3}
|
||
|
|
fullWidth
|
||
|
|
margin="normal"
|
||
|
|
error={!!errors.question}
|
||
|
|
helperText={errors.question?.message}
|
||
|
|
placeholder="请输入您想要测试的问题..."
|
||
|
|
/>
|
||
|
|
|
||
|
|
<Box sx={{ mt: 3 }}>
|
||
|
|
<Typography gutterBottom>
|
||
|
|
相似度阈值: {watch('similarity_threshold')}
|
||
|
|
</Typography>
|
||
|
|
<Slider
|
||
|
|
{...register('similarity_threshold')}
|
||
|
|
value={watch('similarity_threshold')}
|
||
|
|
onChange={(_, value) => setValue('similarity_threshold', value as number)}
|
||
|
|
min={0}
|
||
|
|
max={1}
|
||
|
|
step={0.1}
|
||
|
|
marks
|
||
|
|
valueLabelDisplay="auto"
|
||
|
|
/>
|
||
|
|
</Box>
|
||
|
|
|
||
|
|
<Box sx={{ mt: 3 }}>
|
||
|
|
<Typography gutterBottom>
|
||
|
|
向量相似度权重: {watch('vector_similarity_weight')}
|
||
|
|
</Typography>
|
||
|
|
<Slider
|
||
|
|
{...register('vector_similarity_weight')}
|
||
|
|
value={watch('vector_similarity_weight')}
|
||
|
|
onChange={(_, value) => setValue('vector_similarity_weight', value as number)}
|
||
|
|
min={0}
|
||
|
|
max={1}
|
||
|
|
step={0.1}
|
||
|
|
marks
|
||
|
|
valueLabelDisplay="auto"
|
||
|
|
/>
|
||
|
|
</Box>
|
||
|
|
|
||
|
|
<TextField
|
||
|
|
{...register('top_k')}
|
||
|
|
label="返回结果数量"
|
||
|
|
type="number"
|
||
|
|
fullWidth
|
||
|
|
margin="normal"
|
||
|
|
inputProps={{ min: 1, max: 50 }}
|
||
|
|
/>
|
||
|
|
|
||
|
|
<TextField
|
||
|
|
{...register('rerank_id')}
|
||
|
|
label="重排序模型ID (可选)"
|
||
|
|
fullWidth
|
||
|
|
margin="normal"
|
||
|
|
placeholder="留空使用默认设置"
|
||
|
|
/>
|
||
|
|
|
||
|
|
<FormControlLabel
|
||
|
|
control={
|
||
|
|
<Switch
|
||
|
|
{...register('use_kg')}
|
||
|
|
checked={watch('use_kg')}
|
||
|
|
onChange={(e) => setValue('use_kg', e.target.checked)}
|
||
|
|
/>
|
||
|
|
}
|
||
|
|
label="使用知识图谱"
|
||
|
|
sx={{ mt: 2 }}
|
||
|
|
/>
|
||
|
|
|
||
|
|
<FormControlLabel
|
||
|
|
control={
|
||
|
|
<Switch
|
||
|
|
{...register('highlight')}
|
||
|
|
checked={watch('highlight')}
|
||
|
|
onChange={(e) => setValue('highlight', e.target.checked)}
|
||
|
|
/>
|
||
|
|
}
|
||
|
|
label="高亮显示"
|
||
|
|
sx={{ mt: 1 }}
|
||
|
|
/>
|
||
|
|
|
||
|
|
<Button
|
||
|
|
type="submit"
|
||
|
|
variant="contained"
|
||
|
|
fullWidth
|
||
|
|
size="large"
|
||
|
|
disabled={testing}
|
||
|
|
startIcon={testing ? <CircularProgress size={20} /> : <SearchIcon />}
|
||
|
|
sx={{ mt: 3 }}
|
||
|
|
>
|
||
|
|
{testing ? '测试中...' : '开始测试'}
|
||
|
|
</Button>
|
||
|
|
</Box>
|
||
|
|
</Paper>
|
||
|
|
</Grid>
|
||
|
|
|
||
|
|
{/* 测试结果 */}
|
||
|
|
<Grid size={{xs: 12, md: 8}}>
|
||
|
|
<ResultView result={testResult} loading={testing} />
|
||
|
|
</Grid>
|
||
|
|
</Grid>
|
||
|
|
|
||
|
|
{/* 返回按钮 */}
|
||
|
|
<Fab
|
||
|
|
color="primary"
|
||
|
|
aria-label="返回知识库详情"
|
||
|
|
onClick={handleBackToDetail}
|
||
|
|
sx={{
|
||
|
|
position: 'fixed',
|
||
|
|
bottom: 16,
|
||
|
|
right: 16,
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<ArrowBackIcon />
|
||
|
|
</Fab>
|
||
|
|
|
||
|
|
{/* 消息提示 */}
|
||
|
|
<Snackbar
|
||
|
|
open={snackbar.open}
|
||
|
|
autoHideDuration={6000}
|
||
|
|
onClose={handleCloseSnackbar}
|
||
|
|
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
|
||
|
|
>
|
||
|
|
<Alert
|
||
|
|
onClose={handleCloseSnackbar}
|
||
|
|
severity={snackbar.severity}
|
||
|
|
sx={{ width: '100%' }}
|
||
|
|
>
|
||
|
|
{snackbar.message}
|
||
|
|
</Alert>
|
||
|
|
</Snackbar>
|
||
|
|
</Container>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export default KnowledgeBaseTesting;
|