From 9f6785672f3e078b175c118e97c569d4cacba705 Mon Sep 17 00:00:00 2001 From: "guangfei.zhao" Date: Tue, 14 Oct 2025 18:06:12 +0800 Subject: [PATCH] feat(knowledge): restructure knowledge base pages and components - Implement new setting and testing pages with breadcrumbs --- src/hooks/knowledge-hooks.ts | 16 +- .../knowledge/components/ChunkMethodForm.tsx | 2 +- .../components/FileListComponent.tsx | 36 +- .../components/FloatingActionButtons.tsx | 5 +- .../knowledge/components/GeneralForm.tsx | 2 +- .../components/KnowledgeBreadcrumbs.tsx | 89 ++++ src/pages/knowledge/detail.tsx | 29 +- src/pages/knowledge/index.ts | 5 + src/pages/knowledge/setting.tsx | 225 +++++++++ src/pages/knowledge/testing.tsx | 454 ++++++++++++++++++ src/routes/index.tsx | 15 +- src/utils/request.ts | 4 +- 12 files changed, 834 insertions(+), 48 deletions(-) create mode 100644 src/pages/knowledge/components/KnowledgeBreadcrumbs.tsx create mode 100644 src/pages/knowledge/index.ts create mode 100644 src/pages/knowledge/setting.tsx create mode 100644 src/pages/knowledge/testing.tsx diff --git a/src/hooks/knowledge-hooks.ts b/src/hooks/knowledge-hooks.ts index 2a0096e..92f795e 100644 --- a/src/hooks/knowledge-hooks.ts +++ b/src/hooks/knowledge-hooks.ts @@ -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; diff --git a/src/pages/knowledge/components/ChunkMethodForm.tsx b/src/pages/knowledge/components/ChunkMethodForm.tsx index ae55be0..90e4457 100644 --- a/src/pages/knowledge/components/ChunkMethodForm.tsx +++ b/src/pages/knowledge/components/ChunkMethodForm.tsx @@ -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; diff --git a/src/pages/knowledge/components/FileListComponent.tsx b/src/pages/knowledge/components/FileListComponent.tsx index 8806882..c184b94 100644 --- a/src/pages/knowledge/components/FileListComponent.tsx +++ b/src/pages/knowledge/components/FileListComponent.tsx @@ -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 = ({ onRefresh, rowSelectionModel, onRowSelectionModelChange, + total, + page, + pageSize, + onPageChange, + onPageSizeChange, }) => { const [anchorEl, setAnchorEl] = useState(null); const [selectedFileId, setSelectedFileId] = useState(''); @@ -141,10 +152,15 @@ const FileListComponent: React.FC = ({ 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 = ({ {/* 文件列表 */} = ({ 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': { diff --git a/src/pages/knowledge/components/FloatingActionButtons.tsx b/src/pages/knowledge/components/FloatingActionButtons.tsx index 670e312..73def20 100644 --- a/src/pages/knowledge/components/FloatingActionButtons.tsx +++ b/src/pages/knowledge/components/FloatingActionButtons.tsx @@ -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 = ({ }, }, }} - icon={} + icon={} > {actions.map((action) => ( = ({ 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 ( + + {breadcrumbItems.map((item, index) => { + if (item.isLast) { + return ( + + {item.label} + + ); + } + + return ( + navigate(item.path)} + sx={{ textDecoration: 'none' }} + > + {item.label} + + ); + })} + + ); +}; + +export default KnowledgeBreadcrumbs; \ No newline at end of file diff --git a/src/pages/knowledge/detail.tsx b/src/pages/knowledge/detail.tsx index 0114211..48f506e 100644 --- a/src/pages/knowledge/detail.tsx +++ b/src/pages/knowledge/detail.tsx @@ -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 ( {/* 面包屑导航 */} - - navigate('/knowledge')} - sx={{ textDecoration: 'none' }} - > - 知识库 - - {knowledgeBase.name} - + {/* 知识库信息卡片 */} @@ -202,12 +200,17 @@ function KnowledgeBaseDetail() { console.log('新的选择模型:', newModel); setRowSelectionModel(newModel); }} + total={total} + page={currentPage} + pageSize={pageSize} + onPageChange={setCurrentPage} + onPageSizeChange={setPageSize} /> {/* 浮动操作按钮 */} setTestingDialogOpen(true)} - onConfigClick={() => setConfigDialogOpen(true)} + onTestClick={() => navigate(`/knowledge/${id}/testing`)} + onConfigClick={() => navigate(`/knowledge/${id}/setting`)} /> {/* 上传文件对话框 */} diff --git a/src/pages/knowledge/index.ts b/src/pages/knowledge/index.ts new file mode 100644 index 0000000..eb84c9c --- /dev/null +++ b/src/pages/knowledge/index.ts @@ -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'; \ No newline at end of file diff --git a/src/pages/knowledge/setting.tsx b/src/pages/knowledge/setting.tsx new file mode 100644 index 0000000..e5cae02 --- /dev/null +++ b/src/pages/knowledge/setting.tsx @@ -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 ( + + ); +} + +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({ + defaultValues: { + name: '', + description: '', + permission: 'me', + avatar: undefined, + }, + }); + + // 解析配置表单 + const configForm = useForm({ + 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 ( + + 加载中... + + ); + } + + return ( + + {/* 面包屑导航 */} + + + + + 知识库设置 + + + {knowledge?.name} + + + + + + + + + + + + + + + + + + + + + {/* 返回按钮 */} + + + + + ); +} + +export default KnowledgeBaseSetting; \ No newline at end of file diff --git a/src/pages/knowledge/testing.tsx b/src/pages/knowledge/testing.tsx new file mode 100644 index 0000000..114e4d5 --- /dev/null +++ b/src/pages/knowledge/testing.tsx @@ -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 ( + + + + 正在检索测试... + + + ); + } + + if (!result) { + return ( + + + + 请输入测试问题并点击测试按钮 + + + ); + } + + return ( + + {/* 测试结果概览 */} + + + 测试结果概览 + + + + + + + {result.total} + + + 匹配的文档块 + + + + + + + + + {result.documents?.length || 0} + + + 相关文档 + + + + + + + + + {result.chunks?.length || 0} + + + 返回的块数 + + + + + + + + {/* 匹配的文档块 */} + {result.chunks && result.chunks.length > 0 && ( + + + 匹配的文档块 + + + {result.chunks.map((chunk: ITestingChunk, index: number) => ( + + + + + 文档: {chunk.document_name} + + + + {chunk.vector_similarity && ( + + )} + {chunk.term_similarity && ( + + )} + + + + {chunk.content || '内容不可用'} + + + + ))} + + + )} + + {/* 相关文档统计 */} + {result.documents && result.documents.length > 0 && ( + + + 相关文档统计 + + + {result.documents.map((doc: ITestingDocument, index: number) => ( + + + {doc.doc_name} + + + + ))} + + + )} + + ); +} + +function KnowledgeBaseTesting() { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const [testResult, setTestResult] = useState(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({ + 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 ( + + 加载中... + + ); + } + + return ( + + {/* 面包屑导航 */} + + + + + 知识库测试 + + + {knowledge?.name} + + + + + {/* 测试表单 */} + + + + 测试配置 + + + + + + + + 相似度阈值: {watch('similarity_threshold')} + + setValue('similarity_threshold', value as number)} + min={0} + max={1} + step={0.1} + marks + valueLabelDisplay="auto" + /> + + + + + 向量相似度权重: {watch('vector_similarity_weight')} + + setValue('vector_similarity_weight', value as number)} + min={0} + max={1} + step={0.1} + marks + valueLabelDisplay="auto" + /> + + + + + + + setValue('use_kg', e.target.checked)} + /> + } + label="使用知识图谱" + sx={{ mt: 2 }} + /> + + setValue('highlight', e.target.checked)} + /> + } + label="高亮显示" + sx={{ mt: 1 }} + /> + + + + + + + {/* 测试结果 */} + + + + + + {/* 返回按钮 */} + + + + + {/* 消息提示 */} + + + {snackbar.message} + + + + ); +} + +export default KnowledgeBaseTesting; \ No newline at end of file diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 0cc68d3..2fbfd67 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -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 ( } /> - + {/* 使用MainLayout作为受保护路由的布局 */} }> {/* } /> */} } /> } /> - } /> + {/* 详情通用一个Layout */} + + } /> + } /> + } /> + } /> } /> @@ -29,7 +32,7 @@ const AppRoutes = () => { } /> } /> - + {/* 处理未匹配的路由 */} } /> diff --git a/src/utils/request.ts b/src/utils/request.ts index 52cec7f..8936d96 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -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;