feat(knowledge-testing): add pagination and document filtering to testing interface
This commit is contained in:
@@ -5,3 +5,11 @@ export interface IPaginationRequestBody {
|
|||||||
orderby?: string;
|
orderby?: string;
|
||||||
desc?: string;
|
desc?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页请求参数
|
||||||
|
*/
|
||||||
|
export interface IPaginationBody {
|
||||||
|
page?: number;
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,18 @@
|
|||||||
export interface ITestRetrievalRequestBody {
|
/**
|
||||||
|
{"similarity_threshold":0.2,
|
||||||
|
"vector_similarity_weight":0.3,
|
||||||
|
"top_k":1024,"use_kg":true,
|
||||||
|
"cross_languages":["English","Chinese","Spanish","French","German",
|
||||||
|
"Japanese","Korean","Vietnamese"],"question":"123","kb_id":"dcc2871aa4cd11f08d4116ac85b1de0a",
|
||||||
|
"page":1,"size":10,"doc_ids":["92b37a3aa7de11f084d336b0b158b556"]}
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { IPaginationBody } from "./base";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检索测试请求体
|
||||||
|
*/
|
||||||
|
export interface ITestRetrievalRequestBody extends IPaginationBody {
|
||||||
question: string;
|
question: string;
|
||||||
similarity_threshold: number;
|
similarity_threshold: number;
|
||||||
vector_similarity_weight: number;
|
vector_similarity_weight: number;
|
||||||
@@ -6,13 +20,21 @@ export interface ITestRetrievalRequestBody {
|
|||||||
top_k?: number;
|
top_k?: number;
|
||||||
use_kg?: boolean;
|
use_kg?: boolean;
|
||||||
highlight?: boolean;
|
highlight?: boolean;
|
||||||
kb_id?: string[];
|
kb_id?: string;
|
||||||
|
doc_ids?: string[];
|
||||||
|
cross_languages?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取知识库列表请求体
|
||||||
|
*/
|
||||||
export interface IFetchKnowledgeListRequestBody {
|
export interface IFetchKnowledgeListRequestBody {
|
||||||
owner_ids?: string[];
|
owner_ids?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取知识库列表请求参数
|
||||||
|
*/
|
||||||
export interface IFetchKnowledgeListRequestParams {
|
export interface IFetchKnowledgeListRequestParams {
|
||||||
kb_id?: string;
|
kb_id?: string;
|
||||||
keywords?: string;
|
keywords?: string;
|
||||||
@@ -20,6 +42,9 @@ export interface IFetchKnowledgeListRequestParams {
|
|||||||
page_size?: number;
|
page_size?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文档列表请求体
|
||||||
|
*/
|
||||||
export interface IFetchDocumentListRequestBody {
|
export interface IFetchDocumentListRequestBody {
|
||||||
suffix?: string[];
|
suffix?: string[];
|
||||||
run_status?: string[];
|
run_status?: string[];
|
||||||
|
|||||||
249
src/pages/knowledge/components/TestChunkResult.tsx
Normal file
249
src/pages/knowledge/components/TestChunkResult.tsx
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Paper,
|
||||||
|
Typography,
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
Select,
|
||||||
|
MenuItem,
|
||||||
|
Grid,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
Chip,
|
||||||
|
Stack,
|
||||||
|
Checkbox,
|
||||||
|
ListItemText,
|
||||||
|
OutlinedInput,
|
||||||
|
} from '@mui/material';
|
||||||
|
import type { SelectChangeEvent } from '@mui/material/Select';
|
||||||
|
import { FilterList as FilterListIcon } from '@mui/icons-material';
|
||||||
|
import type { INextTestingResult, ITestingDocument } from '@/interfaces/database/knowledge';
|
||||||
|
|
||||||
|
interface TestChunkResultProps {
|
||||||
|
result: INextTestingResult | null;
|
||||||
|
loading: boolean;
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
onDocumentFilter: (docIds: string[]) => void;
|
||||||
|
selectedDocIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function TestChunkResult({ result, page, pageSize, onDocumentFilter, selectedDocIds }: TestChunkResultProps) {
|
||||||
|
if (!result) {
|
||||||
|
return (
|
||||||
|
<Paper sx={{ p: 3, textAlign: 'center' }}>
|
||||||
|
<Typography variant="h6" color="text.secondary">
|
||||||
|
请输入问题并点击"开始测试"来查看检索结果
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算分页数据 - 使用服务端分页,直接显示当前页数据
|
||||||
|
const chunks = result.chunks || [];
|
||||||
|
const totalPages = Math.ceil(result.total / pageSize);
|
||||||
|
|
||||||
|
const handleDocumentFilterChange = (event: SelectChangeEvent<string[]>) => {
|
||||||
|
const value = event.target.value as string[];
|
||||||
|
onDocumentFilter(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
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.doc_aggs?.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.doc_aggs && result.doc_aggs.length > 0 && (
|
||||||
|
<Paper sx={{ p: 3, mb: 3 }}>
|
||||||
|
<Typography variant="h6" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<FilterListIcon />
|
||||||
|
文档过滤
|
||||||
|
</Typography>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel>选择要显示的文档</InputLabel>
|
||||||
|
<Select
|
||||||
|
multiple
|
||||||
|
value={selectedDocIds}
|
||||||
|
onChange={handleDocumentFilterChange}
|
||||||
|
input={<OutlinedInput label="选择要显示的文档" />}
|
||||||
|
renderValue={(selected) => (
|
||||||
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
|
||||||
|
{selected.map((value) => {
|
||||||
|
const doc = result.doc_aggs?.find(d => d.doc_id === value);
|
||||||
|
return (
|
||||||
|
<Chip key={value} label={doc?.doc_name || value} size="small" />
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{result.doc_aggs.map((doc) => (
|
||||||
|
<MenuItem key={doc.doc_id} value={doc.doc_id}>
|
||||||
|
<Checkbox checked={selectedDocIds.indexOf(doc.doc_id) > -1} />
|
||||||
|
<ListItemText
|
||||||
|
primary={doc.doc_name}
|
||||||
|
secondary={`${doc.count} 个匹配块`}
|
||||||
|
/>
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 匹配的文档块 */}
|
||||||
|
{chunks && chunks.length > 0 && (
|
||||||
|
<Paper sx={{ p: 3, mb: 3 }}>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||||
|
<Typography variant="h6">
|
||||||
|
匹配的文档块 (第 {page} 页,共 {totalPages} 页)
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
共找到 {result.total} 个匹配块
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
{chunks.map((chunk, index) => (
|
||||||
|
<Grid size={12} key={chunk.chunk_id}>
|
||||||
|
<Card variant="outlined">
|
||||||
|
<CardContent>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2 }}>
|
||||||
|
<Typography variant="subtitle1" fontWeight="bold">
|
||||||
|
{chunk.doc_name}
|
||||||
|
</Typography>
|
||||||
|
<Stack direction="row" spacing={1}>
|
||||||
|
<Chip
|
||||||
|
label={`相似度: ${(chunk.similarity * 100).toFixed(1)}%`}
|
||||||
|
size="small"
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
{chunk.vector_similarity !== undefined && (
|
||||||
|
<Chip
|
||||||
|
label={`向量: ${(chunk.vector_similarity * 100).toFixed(1)}%`}
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{chunk.term_similarity !== undefined && (
|
||||||
|
<Chip
|
||||||
|
label={`词项: ${(chunk.term_similarity * 100).toFixed(1)}%`}
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
mb: 2,
|
||||||
|
'& mark': {
|
||||||
|
backgroundColor: 'yellow',
|
||||||
|
padding: '2px 4px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: chunk.content_with_weight || chunk.content_ltks || '无内容'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{chunk.important_kwd && chunk.important_kwd.length > 0 && (
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ mb: 1, display: 'block' }}>
|
||||||
|
关键词:
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
|
||||||
|
{chunk.important_kwd.map((keyword, kwdIndex) => (
|
||||||
|
<Chip
|
||||||
|
key={kwdIndex}
|
||||||
|
label={keyword}
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 相关文档统计 */}
|
||||||
|
{result.doc_aggs && result.doc_aggs.length > 0 && (
|
||||||
|
<Paper sx={{ p: 3 }}>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
相关文档统计
|
||||||
|
</Typography>
|
||||||
|
<Stack spacing={1}>
|
||||||
|
{result.doc_aggs.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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TestChunkResult;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm, Controller } from 'react-hook-form';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Container,
|
Container,
|
||||||
@@ -15,270 +15,206 @@ import {
|
|||||||
MenuItem,
|
MenuItem,
|
||||||
FormControlLabel,
|
FormControlLabel,
|
||||||
Switch,
|
Switch,
|
||||||
Fab,
|
|
||||||
Snackbar,
|
|
||||||
Alert,
|
|
||||||
Grid,
|
Grid,
|
||||||
Card,
|
Pagination,
|
||||||
CardContent,
|
Checkbox,
|
||||||
|
ListItemText,
|
||||||
|
OutlinedInput,
|
||||||
|
ListSubheader,
|
||||||
Chip,
|
Chip,
|
||||||
Divider,
|
|
||||||
Stack,
|
|
||||||
CircularProgress,
|
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import {
|
|
||||||
ArrowBack as ArrowBackIcon,
|
|
||||||
Search as SearchIcon,
|
|
||||||
Psychology as PsychologyIcon,
|
|
||||||
} from '@mui/icons-material';
|
|
||||||
import { useKnowledgeDetail } from '@/hooks/knowledge-hooks';
|
import { useKnowledgeDetail } from '@/hooks/knowledge-hooks';
|
||||||
|
import { useRerankModelOptions } from '@/hooks/llm-hooks';
|
||||||
import knowledgeService from '@/services/knowledge_service';
|
import knowledgeService from '@/services/knowledge_service';
|
||||||
import type { ITestRetrievalRequestBody } from '@/interfaces/request/knowledge';
|
import type { ITestRetrievalRequestBody } from '@/interfaces/request/knowledge';
|
||||||
import type { ITestingResult, ITestingChunk, ITestingDocument } from '@/interfaces/database/knowledge';
|
import type { INextTestingResult } from '@/interfaces/database/knowledge';
|
||||||
import KnowledgeBreadcrumbs from './components/KnowledgeBreadcrumbs';
|
import KnowledgeBreadcrumbs from './components/KnowledgeBreadcrumbs';
|
||||||
|
import TestChunkResult from './components/TestChunkResult';
|
||||||
|
import { useSnackbar } from '@/components/Provider/SnackbarProvider';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { toLower } from 'lodash';
|
||||||
|
import { t } from 'i18next';
|
||||||
|
|
||||||
|
// 语言选项常量
|
||||||
|
const Languages = [
|
||||||
|
'English',
|
||||||
|
'Chinese',
|
||||||
|
'Spanish',
|
||||||
|
'French',
|
||||||
|
'German',
|
||||||
|
'Japanese',
|
||||||
|
'Korean',
|
||||||
|
'Vietnamese',
|
||||||
|
];
|
||||||
|
|
||||||
|
const options = Languages.map((x) => ({
|
||||||
|
label: t('language.' + toLower(x)),
|
||||||
|
value: x,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 表单数据接口
|
||||||
interface TestFormData {
|
interface TestFormData {
|
||||||
question: string;
|
question: string;
|
||||||
similarity_threshold: number;
|
similarity_threshold: number;
|
||||||
vector_similarity_weight: number;
|
vector_similarity_weight: number;
|
||||||
rerank_id?: string;
|
rerank_id?: string;
|
||||||
top_k: number;
|
top_k?: number;
|
||||||
use_kg: boolean;
|
use_kg?: boolean;
|
||||||
highlight: boolean;
|
cross_languages?: string[];
|
||||||
}
|
doc_ids?: string[];
|
||||||
|
|
||||||
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() {
|
function KnowledgeBaseTesting() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [testResult, setTestResult] = useState<ITestingResult | null>(null);
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
// 状态管理
|
||||||
|
const [testResult, setTestResult] = useState<INextTestingResult | null>(null);
|
||||||
const [testing, setTesting] = useState(false);
|
const [testing, setTesting] = useState(false);
|
||||||
const [snackbar, setSnackbar] = useState<{
|
const [page, setPage] = useState(1);
|
||||||
open: boolean;
|
const [pageSize] = useState(10);
|
||||||
message: string;
|
const [selectedDocIds, setSelectedDocIds] = useState<string[]>([]);
|
||||||
severity: 'success' | 'error';
|
|
||||||
}>({
|
const { showMessage } = useSnackbar();
|
||||||
open: false,
|
|
||||||
message: '',
|
|
||||||
severity: 'success',
|
|
||||||
});
|
|
||||||
|
|
||||||
// 获取知识库详情
|
// 获取知识库详情
|
||||||
const { knowledge, loading: detailLoading } = useKnowledgeDetail(id || '');
|
const { knowledge: knowledgeDetail, loading: detailLoading } = useKnowledgeDetail(id || '');
|
||||||
|
|
||||||
// 测试表单
|
// 获取重排序模型选项
|
||||||
const form = useForm<TestFormData>({
|
const { options: rerankOptions, loading: rerankLoading } = useRerankModelOptions();
|
||||||
|
|
||||||
|
// 表单配置
|
||||||
|
const { control, handleSubmit, watch, register, setValue, getValues, formState: { errors } } = useForm<TestFormData>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
question: '',
|
question: '',
|
||||||
similarity_threshold: 0.2,
|
similarity_threshold: 0.2,
|
||||||
vector_similarity_weight: 0.3,
|
vector_similarity_weight: 0.3,
|
||||||
rerank_id: '',
|
rerank_id: '',
|
||||||
top_k: 6,
|
top_k: 1024,
|
||||||
use_kg: false,
|
use_kg: false,
|
||||||
highlight: true,
|
cross_languages: [],
|
||||||
|
doc_ids: [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { register, handleSubmit, watch, setValue, formState: { errors } } = form;
|
// 处理测试提交
|
||||||
|
|
||||||
const handleTestSubmit = async (data: TestFormData) => {
|
const handleTestSubmit = async (data: TestFormData) => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
||||||
try {
|
|
||||||
setTesting(true);
|
setTesting(true);
|
||||||
|
try {
|
||||||
const requestBody: ITestRetrievalRequestBody = {
|
const requestBody: ITestRetrievalRequestBody = {
|
||||||
question: data.question,
|
question: data.question,
|
||||||
similarity_threshold: data.similarity_threshold,
|
similarity_threshold: data.similarity_threshold,
|
||||||
vector_similarity_weight: data.vector_similarity_weight,
|
vector_similarity_weight: data.vector_similarity_weight,
|
||||||
top_k: data.top_k,
|
kb_id: id,
|
||||||
use_kg: data.use_kg,
|
page: page,
|
||||||
highlight: data.highlight,
|
size: pageSize,
|
||||||
kb_id: [id],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (data.rerank_id && data.rerank_id.trim()) {
|
// 只有当字段有值时才添加到请求体中
|
||||||
|
if (data.rerank_id) {
|
||||||
requestBody.rerank_id = data.rerank_id;
|
requestBody.rerank_id = data.rerank_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.top_k) {
|
||||||
|
requestBody.top_k = data.top_k;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.use_kg !== undefined) {
|
||||||
|
requestBody.use_kg = data.use_kg;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有选择的文档,添加到请求中
|
||||||
|
if (data.doc_ids && data.doc_ids.length > 0) {
|
||||||
|
requestBody.doc_ids = data.doc_ids;
|
||||||
|
} else {
|
||||||
|
if (selectedDocIds.length > 0) {
|
||||||
|
requestBody.doc_ids = selectedDocIds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.cross_languages && data.cross_languages.length > 0) {
|
||||||
|
requestBody.cross_languages = data.cross_languages;
|
||||||
|
}
|
||||||
|
|
||||||
const response = await knowledgeService.retrievalTest(requestBody);
|
const response = await knowledgeService.retrievalTest(requestBody);
|
||||||
|
|
||||||
if (response.data.code === 0) {
|
if (response.data.code === 0) {
|
||||||
setTestResult(response.data.data);
|
setTestResult(response.data.data);
|
||||||
setSnackbar({
|
setPage(1); // 重置到第一页
|
||||||
open: true,
|
showMessage.success('检索测试完成');
|
||||||
message: '检索测试完成',
|
|
||||||
severity: 'success',
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error(response.data.message || '检索测试失败');
|
throw new Error(response.data.message || '检索测试失败');
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setSnackbar({
|
showMessage.error(error.message || '检索测试失败');
|
||||||
open: true,
|
|
||||||
message: error.message || '检索测试失败',
|
|
||||||
severity: 'error',
|
|
||||||
});
|
|
||||||
} finally {
|
} finally {
|
||||||
setTesting(false);
|
setTesting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBackToDetail = () => {
|
// 处理分页变化
|
||||||
navigate(`/knowledge/${id}`);
|
const handlePageChange = async (event: React.ChangeEvent<unknown>, value: number) => {
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
setPage(value);
|
||||||
|
setTesting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = getValues();
|
||||||
|
const requestBody: ITestRetrievalRequestBody = {
|
||||||
|
question: formData.question,
|
||||||
|
similarity_threshold: formData.similarity_threshold,
|
||||||
|
vector_similarity_weight: formData.vector_similarity_weight,
|
||||||
|
kb_id: id,
|
||||||
|
page: value,
|
||||||
|
size: pageSize,
|
||||||
|
highlight: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCloseSnackbar = () => {
|
// 只有当字段有值时才添加到请求体中
|
||||||
setSnackbar(prev => ({ ...prev, open: false }));
|
if (formData.rerank_id) {
|
||||||
|
requestBody.rerank_id = formData.rerank_id;
|
||||||
|
}
|
||||||
|
if (formData.top_k) {
|
||||||
|
requestBody.top_k = formData.top_k;
|
||||||
|
}
|
||||||
|
if (formData.use_kg !== undefined) {
|
||||||
|
requestBody.use_kg = formData.use_kg;
|
||||||
|
}
|
||||||
|
if (selectedDocIds.length > 0) {
|
||||||
|
requestBody.doc_ids = selectedDocIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formData.cross_languages && formData.cross_languages.length > 0) {
|
||||||
|
requestBody.cross_languages = formData.cross_languages;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await knowledgeService.retrievalTest(requestBody);
|
||||||
|
if (response.data.code === 0) {
|
||||||
|
setTestResult(response.data.data);
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.message || '分页请求失败');
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
showMessage.error(error.message || '分页请求失败');
|
||||||
|
} finally {
|
||||||
|
setTesting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理文档过滤
|
||||||
|
const handleDocumentFilter = (docIds: string[]) => {
|
||||||
|
setSelectedDocIds(docIds);
|
||||||
|
setValue('doc_ids', docIds);
|
||||||
|
handleTestSubmit(getValues());
|
||||||
|
};
|
||||||
|
|
||||||
|
// 返回详情页
|
||||||
|
const handleBackToDetail = () => {
|
||||||
|
navigate(`/knowledge/${id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (detailLoading) {
|
if (detailLoading) {
|
||||||
@@ -291,21 +227,22 @@ function KnowledgeBaseTesting() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container maxWidth="lg" sx={{ py: 4 }}>
|
<Container maxWidth="lg" sx={{ py: 4 }}>
|
||||||
|
|
||||||
{/* 面包屑导航 */}
|
{/* 面包屑导航 */}
|
||||||
<KnowledgeBreadcrumbs knowledge={knowledge} />
|
<KnowledgeBreadcrumbs knowledge={knowledgeDetail} />
|
||||||
|
|
||||||
<Box sx={{ mb: 3 }}>
|
<Box sx={{ mb: 3 }}>
|
||||||
<Typography variant="h4" gutterBottom>
|
<Typography variant="h4" gutterBottom>
|
||||||
知识库测试
|
知识库测试
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="subtitle1" color="text.secondary">
|
<Typography variant="subtitle1" color="text.secondary">
|
||||||
{knowledge?.name}
|
{knowledgeDetail?.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Grid container spacing={3}>
|
<Grid container spacing={3} direction="row">
|
||||||
{/* 测试表单 */}
|
{/* 测试表单 */}
|
||||||
<Grid size={{xs: 12, sm: 4}}>
|
<Grid size={4}>
|
||||||
<Paper sx={{ p: 3, position: 'sticky', top: 20 }}>
|
<Paper sx={{ p: 3, position: 'sticky', top: 20 }}>
|
||||||
<Typography variant="h6" gutterBottom>
|
<Typography variant="h6" gutterBottom>
|
||||||
测试配置
|
测试配置
|
||||||
@@ -356,99 +293,136 @@ function KnowledgeBaseTesting() {
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
<FormControl fullWidth margin="normal">
|
||||||
|
<InputLabel>重排序模型 (可选)</InputLabel>
|
||||||
|
<Controller
|
||||||
|
name="rerank_id"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
label="重排序模型 (可选)"
|
||||||
|
disabled={rerankLoading}
|
||||||
|
>
|
||||||
|
<MenuItem value="">
|
||||||
|
<em>不使用重排序</em>
|
||||||
|
</MenuItem>
|
||||||
|
{rerankOptions.map((group) => [
|
||||||
|
<ListSubheader key={group.label}>{group.label}</ListSubheader>,
|
||||||
|
...group.options.map((option) => (
|
||||||
|
<MenuItem key={option.value} value={option.value} disabled={option.disabled}>
|
||||||
|
{option.label}
|
||||||
|
</MenuItem>
|
||||||
|
))
|
||||||
|
])}
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
{/* Top-K 字段 - 只有选择了rerank_id时才显示 */}
|
||||||
|
{watch('rerank_id') && (
|
||||||
<TextField
|
<TextField
|
||||||
{...register('top_k')}
|
{...register('top_k', {
|
||||||
label="返回结果数量"
|
required: '请输入返回结果数量',
|
||||||
|
min: { value: 1, message: '最小值为1' },
|
||||||
|
max: { value: 2048, message: '最大值为2048' }
|
||||||
|
})}
|
||||||
|
label="Top-K"
|
||||||
type="number"
|
type="number"
|
||||||
fullWidth
|
fullWidth
|
||||||
margin="normal"
|
margin="normal"
|
||||||
inputProps={{ min: 1, max: 50 }}
|
inputProps={{ min: 1, max: 2048 }}
|
||||||
|
error={!!errors.top_k}
|
||||||
|
helperText={errors.top_k?.message || '与Rerank模型配合使用'}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<TextField
|
<FormControl fullWidth margin="normal">
|
||||||
{...register('rerank_id')}
|
<InputLabel>跨语言搜索</InputLabel>
|
||||||
label="重排序模型ID (可选)"
|
<Controller
|
||||||
fullWidth
|
name="cross_languages"
|
||||||
margin="normal"
|
control={control}
|
||||||
placeholder="留空使用默认设置"
|
render={({ field }) => (
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
multiple
|
||||||
|
label="跨语言搜索"
|
||||||
|
input={<OutlinedInput label="跨语言搜索" />}
|
||||||
|
renderValue={(selected) => (
|
||||||
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
|
||||||
|
{selected.map((value) => {
|
||||||
|
const option = options.find(opt => opt.value === value);
|
||||||
|
return (
|
||||||
|
<Chip key={value} label={option?.label || value} size="small" />
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{options.map((option) => (
|
||||||
|
<MenuItem key={option.value} value={option.value}>
|
||||||
|
<Checkbox checked={(watch('cross_languages') ?? []).indexOf(option.value) > -1} />
|
||||||
|
<ListItemText primary={option.label} />
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
<Switch
|
<Switch
|
||||||
{...register('use_kg')}
|
{...register('use_kg')}
|
||||||
checked={watch('use_kg')}
|
checked={watch('use_kg')}
|
||||||
onChange={(e) => setValue('use_kg', e.target.checked)}
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label="使用知识图谱"
|
label="使用知识图谱"
|
||||||
sx={{ mt: 2 }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormControlLabel
|
|
||||||
control={
|
|
||||||
<Switch
|
|
||||||
{...register('highlight')}
|
|
||||||
checked={watch('highlight')}
|
|
||||||
onChange={(e) => setValue('highlight', e.target.checked)}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="高亮显示"
|
|
||||||
sx={{ mt: 1 }}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
fullWidth
|
fullWidth
|
||||||
size="large"
|
|
||||||
disabled={testing}
|
disabled={testing}
|
||||||
startIcon={testing ? <CircularProgress size={20} /> : <SearchIcon />}
|
sx={{ mt: 2 }}
|
||||||
sx={{ mt: 3 }}
|
|
||||||
>
|
>
|
||||||
{testing ? '测试中...' : '开始测试'}
|
{testing ? '测试中...' : '开始测试'}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
<Grid size={8}>
|
||||||
|
{testResult && (
|
||||||
|
<TestChunkResult
|
||||||
|
result={testResult}
|
||||||
|
loading={testing}
|
||||||
|
page={page}
|
||||||
|
pageSize={pageSize}
|
||||||
|
onDocumentFilter={handleDocumentFilter}
|
||||||
|
selectedDocIds={selectedDocIds}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 测试结果 */}
|
{/* 分页组件 */}
|
||||||
<Grid size={{xs: 12, md: 8}}>
|
{testResult && testResult.total > 10 && (
|
||||||
<ResultView result={testResult} loading={testing} />
|
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 3 }}>
|
||||||
</Grid>
|
<Pagination
|
||||||
</Grid>
|
count={Math.ceil(testResult.total / 10)}
|
||||||
|
page={page}
|
||||||
{/* 返回按钮 */}
|
onChange={handlePageChange}
|
||||||
<Fab
|
|
||||||
color="primary"
|
color="primary"
|
||||||
aria-label="返回知识库详情"
|
size="large"
|
||||||
onClick={handleBackToDetail}
|
showFirstButton
|
||||||
sx={{
|
showLastButton
|
||||||
position: 'fixed',
|
/>
|
||||||
bottom: 16,
|
</Box>
|
||||||
right: 16,
|
)}
|
||||||
}}
|
</Grid>
|
||||||
>
|
</Grid>
|
||||||
<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>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default KnowledgeBaseTesting;
|
export default KnowledgeBaseTesting;
|
||||||
Reference in New Issue
Block a user