feat(knowledge): enhance knowledge base detail page with embedded views

This commit is contained in:
2025-11-18 15:53:54 +08:00
parent fc0b7b2cc9
commit 8ceff84776
8 changed files with 211 additions and 165 deletions

View File

@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
import ProfileForm, { type ProfileFormHandle } from '@/pages/setting/components/ProfileForm'; import ProfileForm, { type ProfileFormHandle } from '@/pages/setting/components/ProfileForm';
import { useProfileSetting } from '@/hooks/setting-hooks'; import { useProfileSetting } from '@/hooks/setting-hooks';
import type { IUserInfo } from '@/interfaces/database/user-setting'; import type { IUserInfo } from '@/interfaces/database/user-setting';
import { useUserData } from '@/hooks/useUserData';
interface ProfileFormDialogProps { interface ProfileFormDialogProps {
open: boolean; open: boolean;
@@ -17,12 +18,14 @@ interface ProfileFormDialogProps {
const ProfileFormDialog: React.FC<ProfileFormDialogProps> = ({ open, onClose }) => { const ProfileFormDialog: React.FC<ProfileFormDialogProps> = ({ open, onClose }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { userInfo, updateUserInfo } = useProfileSetting(); const { userInfo, updateUserInfo } = useProfileSetting();
const { refreshUserData } = useUserData();
const formRef = useRef<ProfileFormHandle>(null); const formRef = useRef<ProfileFormHandle>(null);
const handleSubmit = useCallback(async (data: Partial<IUserInfo>) => { const handleSubmit = useCallback(async (data: Partial<IUserInfo>) => {
await updateUserInfo(data); await updateUserInfo(data);
await refreshUserData();
onClose(); onClose();
}, [updateUserInfo, onClose]); }, [updateUserInfo, onClose, refreshUserData]);
return ( return (
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth> <Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>

View File

@@ -60,6 +60,7 @@ export default {
description: 'Description', description: 'Description',
confirm: 'Confirm', confirm: 'Confirm',
enabled: 'Enabled', enabled: 'Enabled',
disabled: 'Disabled',
clearFilter: 'Clear Filter', clearFilter: 'Clear Filter',
confirmFilter: 'Confirm Filter', confirmFilter: 'Confirm Filter',
private: 'Private', private: 'Private',
@@ -463,7 +464,7 @@ export default {
file: 'File', file: 'File',
dataset: 'Dataset', dataset: 'Dataset',
testing: 'Retrieval testing', testing: 'Retrieval testing',
files: 'files', files: 'Files List',
configuration: 'Configuration', configuration: 'Configuration',
knowledgeGraph: 'Knowledge Graph', knowledgeGraph: 'Knowledge Graph',
name: 'Name', name: 'Name',

View File

@@ -59,6 +59,7 @@ export default {
description: '描述', description: '描述',
confirm: '确认', confirm: '确认',
enabled: '已启用', enabled: '已启用',
disabled: '已禁用',
clearFilter: '清空筛选', clearFilter: '清空筛选',
confirmFilter: '确认筛选', confirmFilter: '确认筛选',
private: '私有', private: '私有',
@@ -459,7 +460,7 @@ export default {
testing: '检索测试', testing: '检索测试',
configuration: '配置', configuration: '配置',
knowledgeGraph: '知识图谱', knowledgeGraph: '知识图谱',
files: '文件', files: '文件列表',
name: '名称', name: '名称',
namePlaceholder: '请输入名称', namePlaceholder: '请输入名称',
doc: '文档', doc: '文档',

View File

@@ -47,6 +47,7 @@ import {
InsertDriveFile as FileIcon, InsertDriveFile as FileIcon,
Upload as UploadIcon, Upload as UploadIcon,
BugReportOutlined as ProcessIcon, BugReportOutlined as ProcessIcon,
Download as DownloadIcon,
} from '@mui/icons-material'; } from '@mui/icons-material';
import { DataGrid, type GridColDef, type GridRowSelectionModel } from '@mui/x-data-grid'; import { DataGrid, type GridColDef, type GridRowSelectionModel } from '@mui/x-data-grid';
import { zhCN, enUS } from '@mui/x-data-grid/locales'; import { zhCN, enUS } from '@mui/x-data-grid/locales';
@@ -56,6 +57,7 @@ import { RunningStatus } from '@/constants/knowledge';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import { LanguageAbbreviation } from '@/constants/common'; import { LanguageAbbreviation } from '@/constants/common';
import knowledgeService from '@/services/knowledge_service';
interface DocumentListComponentProps { interface DocumentListComponentProps {
@@ -172,6 +174,9 @@ const getRunStatusChip = (run: RunningStatus, progress: number) => {
return <Chip label={config.label} color={config.color} size="small" />; return <Chip label={config.label} color={config.color} size="small" />;
}; };
/**
* 文档列表组件
*/
const DocumentListComponent: React.FC<DocumentListComponentProps> = ({ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
files, files,
loading, loading,
@@ -305,6 +310,32 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
handleMenuClose(); handleMenuClose();
}; };
// 下载文件
const handleDownload = async () => {
try {
const docId = selectedFileIdRef.current || selectedFileId;
const fileName = selectedFileRef.current?.name || selectedFile?.name || 'document';
if (!docId) return;
const fileResponse: { data: Blob } = await knowledgeService.getDocumentFile({ doc_id: docId });
if (fileResponse?.data instanceof Blob) {
const url = URL.createObjectURL(fileResponse.data);
if (window?.document) {
const link = window.document.createElement('a');
link.href = url;
link.download = fileName;
window.document.body.appendChild(link);
link.click();
window.document.body.removeChild(link);
}
URL.revokeObjectURL(url);
}
} catch (err) {
logger.error('下载文件失败:', err);
} finally {
handleMenuClose();
}
};
const handleRunStatusChange = (event: SelectChangeEvent<string[]>) => { const handleRunStatusChange = (event: SelectChangeEvent<string[]>) => {
const value = event.target.value; const value = event.target.value;
setSelectedRunStatus(typeof value === 'string' ? value.split(',') : value); setSelectedRunStatus(typeof value === 'string' ? value.split(',') : value);
@@ -630,7 +661,7 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
/> />
</Paper> </Paper>
{/* 右键菜单 */} {/* 菜单 */}
<Menu <Menu
anchorEl={anchorEl} anchorEl={anchorEl}
open={Boolean(anchorEl)} open={Boolean(anchorEl)}
@@ -640,6 +671,10 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
<ViewIcon sx={{ mr: 1 }} /> <ViewIcon sx={{ mr: 1 }} />
<Typography>{t('knowledge.viewDetails')}</Typography> <Typography>{t('knowledge.viewDetails')}</Typography>
</MenuItem> </MenuItem>
<MenuItem onClick={handleDownload}>
<DownloadIcon sx={{ mr: 1 }} />
<Typography>{t('chunkPage.downloadFile')}</Typography>
</MenuItem>
<MenuItem onClick={handleRename}> <MenuItem onClick={handleRename}>
<EditIcon sx={{ mr: 1 }} /> <EditIcon sx={{ mr: 1 }} />
<Typography>{t('common.rename')}</Typography> <Typography>{t('common.rename')}</Typography>
@@ -674,7 +709,9 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
{/* 重命名对话框 */} {/* 重命名对话框 */}
<Dialog open={renameDialogOpen} onClose={() => setRenameDialogOpen(false)}> <Dialog open={renameDialogOpen} onClose={() => setRenameDialogOpen(false)}>
<DialogTitle>{t('knowledge.renameFile')}</DialogTitle> <DialogTitle>{t('knowledge.renameFile')}</DialogTitle>
<DialogContent> <DialogContent sx={{
minWidth: 200
}}>
<TextField <TextField
autoFocus autoFocus
margin="dense" margin="dense"

View File

@@ -21,6 +21,7 @@ import {
Chip, Chip,
Tabs, Tabs,
Tab, Tab,
Fab,
} from '@mui/material'; } from '@mui/material';
import { type GridRowSelectionModel } from '@mui/x-data-grid'; import { type GridRowSelectionModel } from '@mui/x-data-grid';
import type { IKnowledge, IKnowledgeFile } from '@/interfaces/database/knowledge'; import type { IKnowledge, IKnowledgeFile } from '@/interfaces/database/knowledge';
@@ -28,9 +29,11 @@ import type { IDocumentInfoFilter } from '@/interfaces/database/document';
import FileUploadDialog from '@/components/FileUploadDialog'; import FileUploadDialog from '@/components/FileUploadDialog';
import KnowledgeInfoCard from './components/KnowledgeInfoCard'; import KnowledgeInfoCard from './components/KnowledgeInfoCard';
import DocumentListComponent from './components/DocumentListComponent'; import DocumentListComponent from './components/DocumentListComponent';
import FloatingActionButtons from './components/FloatingActionButtons'; import KnowledgeBaseTesting from './testing';
import KnowledgeLogsPage from './overview';
import KnowledgeBreadcrumbs from './components/KnowledgeBreadcrumbs'; import KnowledgeBreadcrumbs from './components/KnowledgeBreadcrumbs';
import KnowledgeGraphView from './components/KnowledgeGraphView'; import KnowledgeGraphView from './components/KnowledgeGraphView';
import { Settings as SettingsIcon } from '@mui/icons-material';
import { useDocumentList, useDocumentOperations } from '@/hooks/document-hooks'; import { useDocumentList, useDocumentOperations } from '@/hooks/document-hooks';
import { RunningStatus } from '@/constants/knowledge'; import { RunningStatus } from '@/constants/knowledge';
import { useKnowledgeDetail } from '@/hooks/knowledge-hooks'; import { useKnowledgeDetail } from '@/hooks/knowledge-hooks';
@@ -54,8 +57,8 @@ function KnowledgeBaseDetail() {
const [processDetailsDialogOpen, setProcessDetailsDialogOpen] = useState(false); const [processDetailsDialogOpen, setProcessDetailsDialogOpen] = useState(false);
const [selectedFileDetails, setSelectedFileDetails] = useState<IKnowledgeFile | null>(null); const [selectedFileDetails, setSelectedFileDetails] = useState<IKnowledgeFile | null>(null);
// 标签页状态 // 标签页状态documents / testing / overview / graph
const [currentTab, setCurrentTab] = useState(0); const [currentTab, setCurrentTab] = useState<'documents' | 'testing' | 'overview' | 'graph'>('documents');
// 轮询相关状态 // 轮询相关状态
const pollingIntervalRef = useRef<any>(null); const pollingIntervalRef = useRef<any>(null);
@@ -296,117 +299,95 @@ function KnowledgeBaseDetail() {
{/* 知识库信息卡片 */} {/* 知识库信息卡片 */}
<KnowledgeInfoCard knowledgeBase={knowledgeBase} /> <KnowledgeInfoCard knowledgeBase={knowledgeBase} />
{/* 标签页组件 - 仅在showKnowledgeGraph为true时显示 */} {/* 标签页组件 - 默认显示 documents/testing/overviewGraph 仅在有数据时显示 */}
{showKnowledgeGraph ? ( <Box sx={{ mt: 3 }}>
<Box sx={{ mt: 3 }}> <Tabs
<Tabs value={currentTab}
value={currentTab} onChange={(event, newValue) => setCurrentTab(newValue)}
onChange={(event, newValue) => setCurrentTab(newValue)} sx={{ borderBottom: 1, borderColor: 'divider' }}
sx={{ borderBottom: 1, borderColor: 'divider' }} >
> <Tab value="documents" label={t('knowledgeDetails.files')} />
<Tab label={t('knowledgeDetails.documents')} /> <Tab value="testing" label={t('knowledgeDetails.testing')} />
<Tab label={t('knowledgeDetails.graph')} /> <Tab value="overview" label={t('knowledgeDetails.datasetLogs')} />
</Tabs> {showKnowledgeGraph && (
<Tab value="graph" label={t('knowledgeDetails.graph')} />
{/* Document List 标签页内容 */}
{currentTab === 0 && (
<Box sx={{ mt: 2 }}>
<DocumentListComponent
files={files}
loading={filesLoading}
searchKeyword={searchKeyword}
onSearchChange={setSearchKeyword}
onReparse={(fileIds) => handleReparse(fileIds)}
onDelete={(fileIds) => {
setRowSelectionModel({
type: 'include',
ids: new Set(fileIds)
});
setDeleteDialogOpen(true);
}}
onUpload={() => setUploadDialogOpen(true)}
onRefresh={() => {
refreshFiles();
fetchKnowledgeDetail();
}}
rowSelectionModel={rowSelectionModel}
onRowSelectionModelChange={(newModel) => {
setRowSelectionModel(newModel);
}}
total={total}
page={currentPage}
pageSize={pageSize}
documentFilter={documentsFilter}
onSelectedFilterChange={(filterBody) => {
fetchDocuments({}, filterBody);
}}
onPageChange={setCurrentPage}
onPageSizeChange={setPageSize}
onRename={handleRename}
onChangeStatus={handleChangeStatus}
onCancelRun={handleCancelRun}
onViewDetails={handleViewDetails}
onViewProcessDetails={handleViewProcessDetails}
/>
</Box>
)} )}
</Tabs>
{/* Graph 标签页内容 */} {/* Documents 标签页内容 */}
{currentTab === 1 && ( {currentTab === 'documents' && (
<Box sx={{ mt: 2 }}> <Box sx={{ mt: 2 }}>
<KnowledgeGraphView knowledgeGraph={knowledgeGraph} /> <DocumentListComponent
</Box> files={files}
)} loading={filesLoading}
</Box> searchKeyword={searchKeyword}
) : ( onSearchChange={setSearchKeyword}
/* 原有的文件列表组件 - 当showKnowledgeGraph为false时显示 */ onReparse={(fileIds) => handleReparse(fileIds)}
<DocumentListComponent onDelete={(fileIds) => {
files={files} setRowSelectionModel({
loading={filesLoading} type: 'include',
searchKeyword={searchKeyword} ids: new Set(fileIds)
onSearchChange={setSearchKeyword} });
onReparse={(fileIds) => handleReparse(fileIds)} setDeleteDialogOpen(true);
onDelete={(fileIds) => { }}
console.log(t('knowledgeDetails.deleteFiles'), fileIds); onUpload={() => setUploadDialogOpen(true)}
setRowSelectionModel({ onRefresh={() => {
type: 'include', refreshFiles();
ids: new Set(fileIds) fetchKnowledgeDetail();
}); }}
setDeleteDialogOpen(true); rowSelectionModel={rowSelectionModel}
}} onRowSelectionModelChange={(newModel) => {
onUpload={() => setUploadDialogOpen(true)} setRowSelectionModel(newModel);
onRefresh={() => { }}
refreshFiles(); total={total}
fetchKnowledgeDetail(); page={currentPage}
}} pageSize={pageSize}
rowSelectionModel={rowSelectionModel} documentFilter={documentsFilter}
onRowSelectionModelChange={(newModel) => { onSelectedFilterChange={(filterBody) => {
console.log(t('knowledgeDetails.newSelectionModel'), newModel); fetchDocuments({}, filterBody);
setRowSelectionModel(newModel); }}
}} onPageChange={setCurrentPage}
total={total} onPageSizeChange={setPageSize}
page={currentPage} onRename={handleRename}
pageSize={pageSize} onChangeStatus={handleChangeStatus}
documentFilter={documentsFilter} onCancelRun={handleCancelRun}
onSelectedFilterChange={(filterBody) => { onViewDetails={handleViewDetails}
fetchDocuments({}, filterBody); onViewProcessDetails={handleViewProcessDetails}
}} />
onPageChange={setCurrentPage} </Box>
onPageSizeChange={setPageSize} )}
onRename={handleRename}
onChangeStatus={handleChangeStatus}
onCancelRun={handleCancelRun}
onViewDetails={handleViewDetails}
onViewProcessDetails={handleViewProcessDetails}
/>
)}
{/* 浮动操作按钮 */} {/* Testing 标签页内容 */}
<FloatingActionButtons {currentTab === 'testing' && (
onTestClick={() => navigate(`/knowledge/${id}/testing`)} <Box sx={{ mt: 2 }}>
onConfigClick={() => navigate(`/knowledge/${id}/setting`)} <KnowledgeBaseTesting embedded kbId={id || ''} />
onOverviewClick={() => navigate(`/knowledge/${id}/overview`)} </Box>
/> )}
{/* Overview 标签页内容 */}
{currentTab === 'overview' && (
<Box sx={{ mt: 2 }}>
<KnowledgeLogsPage embedded kbId={id || ''} />
</Box>
)}
{/* Graph 标签页内容 */}
{currentTab === 'graph' && showKnowledgeGraph && (
<Box sx={{ mt: 2 }}>
<KnowledgeGraphView knowledgeGraph={knowledgeGraph} />
</Box>
)}
</Box>
{/* 设置按钮(替代原浮动操作按钮) */}
<Fab
color="primary"
aria-label={t('knowledgeSettings.knowledgeBaseSettings')}
onClick={() => navigate(`/knowledge/${id}/setting`)}
sx={{ position: 'fixed', bottom: 128, right: 64 }}
>
<SettingsIcon />
</Fab>
{/* 上传文件对话框 */} {/* 上传文件对话框 */}
<FileUploadDialog <FileUploadDialog

View File

@@ -27,7 +27,12 @@ const ProcessingTypeMap = {
[PROCESSING_TYPES.raptor]: 'RAPTOR', [PROCESSING_TYPES.raptor]: 'RAPTOR',
} as const } as const
function KnowledgeLogsPage() { interface KnowledgeLogsPageProps {
embedded?: boolean;
kbId?: string;
}
function KnowledgeLogsPage({ embedded = false, kbId: kbIdProp }: KnowledgeLogsPageProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const [paginationModel, setPaginationModel] = React.useState({ page: 0, pageSize: 50 }); const [paginationModel, setPaginationModel] = React.useState({ page: 0, pageSize: 50 });
@@ -38,7 +43,8 @@ function KnowledgeLogsPage() {
}; };
// 路由参数与数据Hook // 路由参数与数据Hook
const { id: kbId = '' } = useParams(); const { id: kbIdParam = '' } = useParams();
const kbId = kbIdProp || kbIdParam;
const { const {
overview, overview,
fileLogs, fileLogs,
@@ -105,23 +111,25 @@ function KnowledgeLogsPage() {
}; };
return ( return (
<Box sx={{ p: 3 }}> <Box sx={{ p: embedded ? 0 : 3 }}>
{/* 面包屑导航 */} {/* 面包屑导航 */}
<KnowledgeBreadcrumbs {!embedded && (
kbItems={[ <KnowledgeBreadcrumbs
{ kbItems={[
label: t('knowledgeSettings.knowledgeBase'), {
path: '/knowledge' label: t('knowledgeSettings.knowledgeBase'),
}, path: '/knowledge'
{ },
label: knowledge?.name || t('knowledgeSettings.knowledgeBaseDetail'), {
path: `/knowledge/${kbId}` label: knowledge?.name || t('knowledgeSettings.knowledgeBaseDetail'),
}, path: `/knowledge/${kbId}`
{ },
label: t('knowledgeSettings.fileLogs') {
} label: t('knowledgeSettings.fileLogs')
]} }
/> ]}
/>
)}
{/* 顶部统计卡片占位 */} {/* 顶部统计卡片占位 */}
<Grid container spacing={2} sx={{ mb: 2 }}> <Grid container spacing={2} sx={{ mb: 2 }}>
<Grid size={{ xs: 12, md: 4 }}> <Grid size={{ xs: 12, md: 4 }}>
@@ -183,18 +191,20 @@ function KnowledgeLogsPage() {
</Paper> </Paper>
{/* 返回按钮 */} {/* 返回按钮 */}
<Fab {!embedded && (
color="primary" <Fab
aria-label={t('knowledgeSettings.backToKnowledgeDetail')} color="primary"
onClick={handleNavigateBack} aria-label={t('knowledgeSettings.backToKnowledgeDetail')}
sx={{ onClick={handleNavigateBack}
position: 'fixed', sx={{
bottom: 128, position: 'fixed',
right: 64, bottom: 128,
}} right: 64,
> }}
<ArrowBackIcon /> >
</Fab> <ArrowBackIcon />
</Fab>
)}
</Box> </Box>
); );
} }

View File

@@ -61,9 +61,15 @@ interface TestFormData {
doc_ids?: string[]; doc_ids?: string[];
} }
function KnowledgeBaseTesting() { interface KnowledgeBaseTestingProps {
embedded?: boolean;
kbId?: string;
}
function KnowledgeBaseTesting({ embedded = false, kbId }: KnowledgeBaseTestingProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const { id } = useParams<{ id: string }>(); const { id: idParam } = useParams<{ id: string }>();
const id = kbId || idParam || '';
const navigate = useNavigate(); const navigate = useNavigate();
// 状态管理 // 状态管理
@@ -230,24 +236,26 @@ function KnowledgeBaseTesting() {
} }
return ( return (
<Container maxWidth="lg" sx={{ py: 4 }}> <Container maxWidth="lg" sx={{ py: embedded ? 0 : 4 }}>
{/* 面包屑导航 */} {/* 面包屑导航 */}
<KnowledgeBreadcrumbs {!embedded && (
kbItems={[ <KnowledgeBreadcrumbs
{ kbItems={[
label: t('knowledgeTesting.knowledgeBase'), {
path: '/knowledge' label: t('knowledgeTesting.knowledgeBase'),
}, path: '/knowledge'
{ },
label: knowledgeDetail?.name || t('knowledgeTesting.knowledgeBaseDetail'), {
path: `/knowledge/${id}` label: knowledgeDetail?.name || t('knowledgeTesting.knowledgeBaseDetail'),
}, path: `/knowledge/${id}`
{ },
label: t('knowledgeTesting.testing') {
} label: t('knowledgeTesting.testing')
]} }
/> ]}
/>
)}
<Box sx={{ mb: 3 }}> <Box sx={{ mb: 3 }}>
<Typography variant="h4" gutterBottom> <Typography variant="h4" gutterBottom>

View File

@@ -6,10 +6,12 @@ import ProfileForm from "./components/ProfileForm";
import ChangePasswordDialog from "./components/ChangePasswordDialog"; import ChangePasswordDialog from "./components/ChangePasswordDialog";
import { useProfileSetting } from "@/hooks/setting-hooks"; import { useProfileSetting } from "@/hooks/setting-hooks";
import logger from "@/utils/logger"; import logger from "@/utils/logger";
import { useUserData } from "@/hooks/useUserData";
function ProfileSetting() { function ProfileSetting() {
const { t } = useTranslation(); const { t } = useTranslation();
const { userInfo, updateUserInfo: updateUserInfoFunc, changeUserPassword: changeUserPasswordFunc } = useProfileSetting(); const { userInfo, updateUserInfo: updateUserInfoFunc, changeUserPassword: changeUserPasswordFunc } = useProfileSetting();
const { refreshUserData } = useUserData();
const [passwordDialogOpen, setPasswordDialogOpen] = useState(false); const [passwordDialogOpen, setPasswordDialogOpen] = useState(false);
logger.debug('userInfo', userInfo); logger.debug('userInfo', userInfo);
@@ -25,7 +27,10 @@ function ProfileSetting() {
return ( return (
<Box sx={{ maxWidth: 800, mx: 'auto', p: 3 }}> <Box sx={{ maxWidth: 800, mx: 'auto', p: 3 }}>
{/* 个人资料表单 */} {/* 个人资料表单 */}
<ProfileForm userInfo={userInfo} onSubmit={updateUserInfoFunc} /> <ProfileForm userInfo={userInfo} onSubmit={async (data) => {
await updateUserInfoFunc(data);
await refreshUserData();
}} />
{/* 分割线 */} {/* 分割线 */}
<Divider sx={{ my: 4 }} /> <Divider sx={{ my: 4 }} />