feat(knowledge): restructure knowledge base pages and components
- Implement new setting and testing pages with breadcrumbs
This commit is contained in:
@@ -224,24 +224,12 @@ export const useKnowledgeOperations = () => {
|
||||
* 更新知识库基础信息
|
||||
* 包括名称、描述、语言等基本信息
|
||||
*/
|
||||
const updateKnowledgeBasicInfo = useCallback(async (data: {
|
||||
id: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
language?: string;
|
||||
avatar?: any;
|
||||
permission?: string;
|
||||
}) => {
|
||||
const updateKnowledgeBasicInfo = useCallback(async (data: IKnowledge) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const updateData = {
|
||||
kb_id: data.id,
|
||||
...data,
|
||||
};
|
||||
|
||||
const response = await knowledgeService.updateKnowledge(updateData);
|
||||
const response = await knowledgeService.updateKnowledge(data);
|
||||
|
||||
if (response.data.code === 0) {
|
||||
return response.data.data;
|
||||
|
||||
@@ -37,7 +37,7 @@ const parserOptions = [
|
||||
{ value: DOCUMENT_PARSER_TYPES.KnowledgeGraph, label: '知识图谱解析器', description: '构建知识图谱结构' },
|
||||
];
|
||||
|
||||
interface ConfigFormData {
|
||||
export interface ConfigFormData {
|
||||
parser_id: DocumentParserType;
|
||||
chunk_token_count?: number;
|
||||
layout_recognize?: boolean;
|
||||
|
||||
@@ -46,6 +46,12 @@ interface FileListComponentProps {
|
||||
onRefresh: () => void;
|
||||
rowSelectionModel: GridRowSelectionModel;
|
||||
onRowSelectionModelChange: (model: GridRowSelectionModel) => void;
|
||||
// 分页相关props
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
onPageChange: (page: number) => void;
|
||||
onPageSizeChange: (pageSize: number) => void;
|
||||
}
|
||||
|
||||
const getFileIcon = (type: string) => {
|
||||
@@ -104,6 +110,11 @@ const FileListComponent: React.FC<FileListComponentProps> = ({
|
||||
onRefresh,
|
||||
rowSelectionModel,
|
||||
onRowSelectionModelChange,
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
}) => {
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [selectedFileId, setSelectedFileId] = useState<string>('');
|
||||
@@ -141,10 +152,15 @@ const FileListComponent: React.FC<FileListComponentProps> = ({
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
// 过滤文件列表
|
||||
const filteredFiles = files.filter(file =>
|
||||
file.name.toLowerCase().includes(searchKeyword.toLowerCase())
|
||||
);
|
||||
// 处理分页变化
|
||||
const handlePaginationModelChange = (model: { page: number; pageSize: number }) => {
|
||||
if (model.page !== page - 1) { // DataGrid的page是0-based,我们的是1-based
|
||||
onPageChange(model.page + 1);
|
||||
}
|
||||
if (model.pageSize !== pageSize) {
|
||||
onPageSizeChange(model.pageSize);
|
||||
}
|
||||
};
|
||||
|
||||
// DataGrid 列定义
|
||||
const columns: GridColDef[] = [
|
||||
@@ -278,7 +294,7 @@ const FileListComponent: React.FC<FileListComponentProps> = ({
|
||||
{/* 文件列表 */}
|
||||
<Paper sx={{ height: 600, width: '100%' }}>
|
||||
<DataGrid
|
||||
rows={filteredFiles}
|
||||
rows={files}
|
||||
columns={columns}
|
||||
loading={loading}
|
||||
checkboxSelection
|
||||
@@ -286,11 +302,13 @@ const FileListComponent: React.FC<FileListComponentProps> = ({
|
||||
rowSelectionModel={rowSelectionModel}
|
||||
onRowSelectionModelChange={onRowSelectionModelChange}
|
||||
pageSizeOptions={[10, 25, 50, 100]}
|
||||
initialState={{
|
||||
pagination: {
|
||||
paginationModel: { page: 0, pageSize: 25 },
|
||||
},
|
||||
paginationMode="server"
|
||||
rowCount={total}
|
||||
paginationModel={{
|
||||
page: page - 1,
|
||||
pageSize: pageSize,
|
||||
}}
|
||||
onPaginationModelChange={handlePaginationModelChange}
|
||||
localeText={getDataGridLocale().components.MuiDataGrid.defaultProps.localeText}
|
||||
sx={{
|
||||
'& .MuiDataGrid-cell:focus': {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from 'react';
|
||||
import { SpeedDial, SpeedDialAction } from '@mui/material';
|
||||
import {
|
||||
Settings as SettingsIcon,
|
||||
Dashboard as DashboardIcon,
|
||||
Search as TestIcon,
|
||||
Settings as ConfigIcon,
|
||||
|
||||
} from '@mui/icons-material';
|
||||
|
||||
interface FloatingActionButtonsProps {
|
||||
@@ -42,7 +43,7 @@ const FloatingActionButtons: React.FC<FloatingActionButtonsProps> = ({
|
||||
},
|
||||
},
|
||||
}}
|
||||
icon={<SettingsIcon />}
|
||||
icon={<DashboardIcon />}
|
||||
>
|
||||
{actions.map((action) => (
|
||||
<SpeedDialAction
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
Save as SaveIcon,
|
||||
} from '@mui/icons-material';
|
||||
|
||||
interface BasicFormData {
|
||||
export interface BasicFormData {
|
||||
name: string;
|
||||
description: string;
|
||||
permission: string;
|
||||
|
||||
89
src/pages/knowledge/components/KnowledgeBreadcrumbs.tsx
Normal file
89
src/pages/knowledge/components/KnowledgeBreadcrumbs.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import React from 'react';
|
||||
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
import { Breadcrumbs, Link, Typography } from '@mui/material';
|
||||
import type { IKnowledge } from '@/interfaces/database/knowledge';
|
||||
|
||||
interface KnowledgeBreadcrumbsProps {
|
||||
knowledge?: IKnowledge | null;
|
||||
sx?: object;
|
||||
}
|
||||
|
||||
const KnowledgeBreadcrumbs: React.FC<KnowledgeBreadcrumbsProps> = ({ knowledge, sx }) => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
|
||||
// 解析当前路径
|
||||
const pathSegments = location.pathname.split('/').filter(Boolean);
|
||||
|
||||
// 生成面包屑项
|
||||
const breadcrumbItems = [];
|
||||
|
||||
// 第一层:知识库列表
|
||||
breadcrumbItems.push({
|
||||
label: '知识库',
|
||||
path: '/knowledge',
|
||||
isLast: false
|
||||
});
|
||||
|
||||
// 第二层:知识库详情(如果有id)
|
||||
if (id && knowledge) {
|
||||
const isDetailPage = pathSegments.length === 2; // /knowledge/:id
|
||||
breadcrumbItems.push({
|
||||
label: knowledge.name,
|
||||
path: `/knowledge/${id}`,
|
||||
isLast: isDetailPage
|
||||
});
|
||||
|
||||
// 第三层:设置或测试页面
|
||||
if (pathSegments.length === 3) {
|
||||
const lastSegment = pathSegments[2];
|
||||
let label = '';
|
||||
|
||||
switch (lastSegment) {
|
||||
case 'setting':
|
||||
label = '设置';
|
||||
break;
|
||||
case 'testing':
|
||||
label = '测试';
|
||||
break;
|
||||
default:
|
||||
label = lastSegment;
|
||||
}
|
||||
|
||||
breadcrumbItems.push({
|
||||
label,
|
||||
path: location.pathname,
|
||||
isLast: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Breadcrumbs sx={{ mb: 2, ...sx }}>
|
||||
{breadcrumbItems.map((item, index) => {
|
||||
if (item.isLast) {
|
||||
return (
|
||||
<Typography key={index} color="text.primary">
|
||||
{item.label}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={index}
|
||||
component="button"
|
||||
variant="body1"
|
||||
onClick={() => navigate(item.path)}
|
||||
sx={{ textDecoration: 'none' }}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</Breadcrumbs>
|
||||
);
|
||||
};
|
||||
|
||||
export default KnowledgeBreadcrumbs;
|
||||
@@ -22,6 +22,7 @@ import FileUploadDialog from '@/components/FileUploadDialog';
|
||||
import KnowledgeInfoCard from './components/KnowledgeInfoCard';
|
||||
import FileListComponent from './components/FileListComponent';
|
||||
import FloatingActionButtons from './components/FloatingActionButtons';
|
||||
import KnowledgeBreadcrumbs from './components/KnowledgeBreadcrumbs';
|
||||
import { useDocumentList, useDocumentOperations } from '@/hooks/document-hooks';
|
||||
|
||||
function KnowledgeBaseDetail() {
|
||||
@@ -45,10 +46,15 @@ function KnowledgeBaseDetail() {
|
||||
// 使用新的document hooks
|
||||
const {
|
||||
documents: files,
|
||||
total,
|
||||
loading: filesLoading,
|
||||
error: filesError,
|
||||
currentPage,
|
||||
pageSize,
|
||||
refresh: refreshFiles,
|
||||
setKeywords,
|
||||
setCurrentPage,
|
||||
setPageSize,
|
||||
} = useDocumentList(id || '');
|
||||
|
||||
const {
|
||||
@@ -89,6 +95,7 @@ function KnowledgeBaseDetail() {
|
||||
ids: new Set()
|
||||
});
|
||||
refreshFiles();
|
||||
fetchKnowledgeDetail();
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.message || err.message || '删除文件失败');
|
||||
}
|
||||
@@ -101,6 +108,7 @@ function KnowledgeBaseDetail() {
|
||||
try {
|
||||
await uploadDocuments(kb_id, uploadFiles);
|
||||
refreshFiles();
|
||||
fetchKnowledgeDetail();
|
||||
} catch (err: any) {
|
||||
throw new Error(err.response?.data?.message || err.message || '上传文件失败');
|
||||
}
|
||||
@@ -161,17 +169,7 @@ function KnowledgeBaseDetail() {
|
||||
return (
|
||||
<Box sx={{ p: 3 }}>
|
||||
{/* 面包屑导航 */}
|
||||
<Breadcrumbs sx={{ mb: 2 }}>
|
||||
<Link
|
||||
component="button"
|
||||
variant="body1"
|
||||
onClick={() => navigate('/knowledge')}
|
||||
sx={{ textDecoration: 'none' }}
|
||||
>
|
||||
知识库
|
||||
</Link>
|
||||
<Typography color="text.primary">{knowledgeBase.name}</Typography>
|
||||
</Breadcrumbs>
|
||||
<KnowledgeBreadcrumbs knowledge={knowledgeBase} />
|
||||
|
||||
{/* 知识库信息卡片 */}
|
||||
<KnowledgeInfoCard knowledgeBase={knowledgeBase} />
|
||||
@@ -202,12 +200,17 @@ function KnowledgeBaseDetail() {
|
||||
console.log('新的选择模型:', newModel);
|
||||
setRowSelectionModel(newModel);
|
||||
}}
|
||||
total={total}
|
||||
page={currentPage}
|
||||
pageSize={pageSize}
|
||||
onPageChange={setCurrentPage}
|
||||
onPageSizeChange={setPageSize}
|
||||
/>
|
||||
|
||||
{/* 浮动操作按钮 */}
|
||||
<FloatingActionButtons
|
||||
onTestClick={() => setTestingDialogOpen(true)}
|
||||
onConfigClick={() => setConfigDialogOpen(true)}
|
||||
onTestClick={() => navigate(`/knowledge/${id}/testing`)}
|
||||
onConfigClick={() => navigate(`/knowledge/${id}/setting`)}
|
||||
/>
|
||||
|
||||
{/* 上传文件对话框 */}
|
||||
|
||||
5
src/pages/knowledge/index.ts
Normal file
5
src/pages/knowledge/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export { default as KnowledgeBaseList } from './list';
|
||||
export { default as KnowledgeBaseCreate } from './create';
|
||||
export { default as KnowledgeBaseDetail } from './detail'
|
||||
export { default as KnowledgeBaseSetting } from './setting';
|
||||
export { default as KnowledgeBaseTesting } from './testing';
|
||||
225
src/pages/knowledge/setting.tsx
Normal file
225
src/pages/knowledge/setting.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { useForm, type UseFormReturn } from 'react-hook-form';
|
||||
import {
|
||||
Box,
|
||||
Container,
|
||||
Typography,
|
||||
Paper,
|
||||
Tabs,
|
||||
Tab,
|
||||
Fab,
|
||||
Snackbar,
|
||||
Alert,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
ArrowBack as ArrowBackIcon,
|
||||
} from '@mui/icons-material';
|
||||
import { useKnowledgeDetail, useKnowledgeOperations } from '@/hooks/knowledge-hooks';
|
||||
import GeneralForm, { type BasicFormData } from './components/GeneralForm';
|
||||
import ChunkMethodForm, { type ConfigFormData } from './components/ChunkMethodForm';
|
||||
import KnowledgeBreadcrumbs from './components/KnowledgeBreadcrumbs';
|
||||
import { useSnackbar } from '@/components/Provider/SnackbarProvider';
|
||||
|
||||
interface TabPanelProps {
|
||||
children?: React.ReactNode;
|
||||
index: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
function TabPanel(props: TabPanelProps) {
|
||||
const { children, value, index, ...other } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="tabpanel"
|
||||
hidden={value !== index}
|
||||
id={`setting-tabpanel-${index}`}
|
||||
aria-labelledby={`setting-tab-${index}`}
|
||||
{...other}
|
||||
>
|
||||
{value === index && (
|
||||
<Box sx={{ p: 3 }}>
|
||||
{children}
|
||||
</Box>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function a11yProps(index: number) {
|
||||
return {
|
||||
id: `setting-tab-${index}`,
|
||||
'aria-controls': `setting-tabpanel-${index}`,
|
||||
};
|
||||
}
|
||||
|
||||
function KnowledgeBaseSetting() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const [tabValue, setTabValue] = useState(0);
|
||||
|
||||
// 获取知识库详情
|
||||
const { knowledge, loading: detailLoading, refresh } = useKnowledgeDetail(id || '');
|
||||
const { showMessage } = useSnackbar();
|
||||
|
||||
// 知识库操作hooks
|
||||
const {
|
||||
updateKnowledgeBasicInfo,
|
||||
updateKnowledgeModelConfig,
|
||||
loading: operationLoading
|
||||
} = useKnowledgeOperations();
|
||||
|
||||
// 基础信息表单
|
||||
const basicForm = useForm<BasicFormData>({
|
||||
defaultValues: {
|
||||
name: '',
|
||||
description: '',
|
||||
permission: 'me',
|
||||
avatar: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
// 解析配置表单
|
||||
const configForm = useForm<ConfigFormData>({
|
||||
defaultValues: {
|
||||
parser_id: 'naive',
|
||||
chunk_token_count: 512,
|
||||
layout_recognize: false,
|
||||
task_page_size: 0,
|
||||
},
|
||||
});
|
||||
|
||||
// 当知识库数据加载完成时,更新表单默认值
|
||||
useEffect(() => {
|
||||
if (knowledge) {
|
||||
basicForm.reset({
|
||||
name: knowledge.name || '',
|
||||
description: knowledge.description || '',
|
||||
permission: knowledge.permission || 'me',
|
||||
avatar: knowledge.avatar,
|
||||
});
|
||||
|
||||
configForm.reset({
|
||||
// parser_id: knowledge.parser_id || 'naive',
|
||||
// chunk_token_count: knowledge.chunk_token_count || 512,
|
||||
// layout_recognize: knowledge.layout_recognize || false,
|
||||
// task_page_size: knowledge.task_page_size || 0,
|
||||
});
|
||||
}
|
||||
}, [knowledge, basicForm, configForm]);
|
||||
|
||||
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
||||
setTabValue(newValue);
|
||||
};
|
||||
|
||||
const handleBasicInfoSubmit = async (data: BasicFormData) => {
|
||||
if (!knowledge) return;
|
||||
|
||||
try {
|
||||
const kb = {
|
||||
...data,
|
||||
// parser_id: knowledge.parser_id,
|
||||
kb_id: knowledge.id,
|
||||
} as any;
|
||||
|
||||
await updateKnowledgeBasicInfo(kb);
|
||||
showMessage.success('基础信息更新成功');
|
||||
// 刷新知识库详情
|
||||
refresh();
|
||||
} catch (error) {
|
||||
// showMessage.error('基础信息更新失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfigSubmit = async (data: ConfigFormData) => {
|
||||
if (!id) return;
|
||||
|
||||
try {
|
||||
await updateKnowledgeModelConfig({
|
||||
id,
|
||||
parser_id: data.parser_id,
|
||||
// 可以根据需要添加更多配置字段
|
||||
});
|
||||
showMessage.success('解析配置更新成功');
|
||||
// 刷新知识库详情
|
||||
refresh();
|
||||
} catch (error) {
|
||||
// showMessage.error('解析配置更新失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleBackToDetail = () => {
|
||||
navigate(`/knowledge/${id}`);
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
<Paper sx={{ width: '100%' }}>
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
||||
<Tabs value={tabValue} onChange={handleTabChange} aria-label="设置选项卡">
|
||||
<Tab label="基础信息" {...a11yProps(0)} />
|
||||
<Tab label="解析配置" {...a11yProps(1)} />
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
<TabPanel value={tabValue} index={0}>
|
||||
<GeneralForm
|
||||
form={basicForm}
|
||||
onSubmit={handleBasicInfoSubmit}
|
||||
isSubmitting={operationLoading}
|
||||
submitButtonText="保存基础信息"
|
||||
disabled={detailLoading}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={tabValue} index={1}>
|
||||
<ChunkMethodForm
|
||||
form={configForm as any}
|
||||
onSubmit={handleConfigSubmit}
|
||||
isSubmitting={operationLoading}
|
||||
submitButtonText="保存解析配置"
|
||||
disabled={detailLoading}
|
||||
/>
|
||||
</TabPanel>
|
||||
</Paper>
|
||||
|
||||
{/* 返回按钮 */}
|
||||
<Fab
|
||||
color="primary"
|
||||
aria-label="返回知识库详情"
|
||||
onClick={handleBackToDetail}
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
bottom: 128,
|
||||
right: 64,
|
||||
}}
|
||||
>
|
||||
<ArrowBackIcon />
|
||||
</Fab>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default KnowledgeBaseSetting;
|
||||
454
src/pages/knowledge/testing.tsx
Normal file
454
src/pages/knowledge/testing.tsx
Normal 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;
|
||||
@@ -2,26 +2,29 @@ import { Routes, Route, Navigate } from 'react-router-dom';
|
||||
import MainLayout from '../components/Layout/MainLayout';
|
||||
import Login from '../pages/login/Login';
|
||||
import Home from '../pages/Home';
|
||||
import KnowledgeBaseList from '../pages/knowledge/list';
|
||||
import PipelineConfig from '../pages/PipelineConfig';
|
||||
import Dashboard from '../pages/Dashboard';
|
||||
import ModelsResources from '../pages/ModelsResources';
|
||||
import KnowledgeBaseCreate from '../pages/knowledge/create';
|
||||
import KnowledgeBaseDetail from '../pages/knowledge/detail';
|
||||
import { KnowledgeBaseList, KnowledgeBaseCreate, KnowledgeBaseDetail, KnowledgeBaseSetting, KnowledgeBaseTesting } from '../pages/knowledge';
|
||||
import MCP from '../pages/MCP';
|
||||
|
||||
const AppRoutes = () => {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
|
||||
|
||||
{/* 使用MainLayout作为受保护路由的布局 */}
|
||||
<Route path="/" element={<MainLayout />}>
|
||||
{/* <Route index element={<Home />} /> */}
|
||||
<Route path="knowledge">
|
||||
<Route index element={<KnowledgeBaseList />} />
|
||||
<Route path="create" element={<KnowledgeBaseCreate />} />
|
||||
<Route path=":id" element={<KnowledgeBaseDetail />} />
|
||||
{/* 详情通用一个Layout */}
|
||||
<Route path=':id'>
|
||||
<Route path="testing" element={<KnowledgeBaseTesting />} />
|
||||
<Route index element={<KnowledgeBaseDetail />} />
|
||||
<Route path="setting" element={<KnowledgeBaseSetting />} />
|
||||
</Route>
|
||||
</Route>
|
||||
<Route index element={<KnowledgeBaseList />} />
|
||||
<Route path="pipeline-config" element={<PipelineConfig />} />
|
||||
@@ -29,7 +32,7 @@ const AppRoutes = () => {
|
||||
<Route path="models-resources" element={<ModelsResources />} />
|
||||
<Route path="mcp" element={<MCP />} />
|
||||
</Route>
|
||||
|
||||
|
||||
{/* 处理未匹配的路由 */}
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
|
||||
@@ -132,10 +132,10 @@ request.interceptors.response.use(
|
||||
if (data?.code === 100) {
|
||||
snackbar.error(data?.message);
|
||||
} else if (data?.code === 401) {
|
||||
notification.error(data?.message);
|
||||
notification.error(data?.message, i18n.t('message.401'));
|
||||
redirectToLogin();
|
||||
} else if (data?.code !== 0) {
|
||||
notification.error(`${i18n.t('message.hint')} : ${data?.code}`, data?.message);
|
||||
snackbar.error(data?.message);
|
||||
}
|
||||
|
||||
return response;
|
||||
|
||||
Reference in New Issue
Block a user