feat(knowledge): restructure knowledge base pages and components

- Implement new setting and testing pages with breadcrumbs
This commit is contained in:
2025-10-14 18:06:12 +08:00
parent 7384ae36d0
commit 9f6785672f
12 changed files with 834 additions and 48 deletions

View File

@@ -0,0 +1,454 @@
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;