diff --git a/src/locales/en.ts b/src/locales/en.ts index fc4fa0d..528e729 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -453,6 +453,7 @@ export default { datasetLog: 'Dataset Log', created: 'Created', learnMore: 'Learn More', + localUpload: 'Local Upload', general: 'General', chunkMethodTab: 'Chunk Method', testResults: 'Test Results', diff --git a/src/locales/zh.ts b/src/locales/zh.ts index 5ed8d76..e81f9c5 100644 --- a/src/locales/zh.ts +++ b/src/locales/zh.ts @@ -447,6 +447,7 @@ export default { datasetLog: '知识库日志', created: '创建于', learnMore: '了解更多', + localUpload: '本地上传', general: '通用', chunkMethodTab: '切片方法', testResults: '测试结果', diff --git a/src/pages/knowledge/components/KnowledgeInfoCard.tsx b/src/pages/knowledge/components/KnowledgeInfoCard.tsx index fcc0cf2..9109ee9 100644 --- a/src/pages/knowledge/components/KnowledgeInfoCard.tsx +++ b/src/pages/knowledge/components/KnowledgeInfoCard.tsx @@ -38,10 +38,10 @@ const KnowledgeInfoCard: React.FC = ({ knowledgeBase }) {knowledgeBase.description || t('knowledge.noDescription')} - - - - + + + + diff --git a/src/pages/knowledge/detail.tsx b/src/pages/knowledge/detail.tsx index ac8024f..717c4f2 100644 --- a/src/pages/knowledge/detail.tsx +++ b/src/pages/knowledge/detail.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useRef } from 'react'; -import { useParams, useNavigate } from 'react-router-dom'; +import { useParams, useNavigate, useLocation } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Box, @@ -42,6 +42,7 @@ import logger from '@/utils/logger'; function KnowledgeBaseDetail() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); + const location = useLocation(); const { t } = useTranslation(); // 状态管理 @@ -102,6 +103,29 @@ function KnowledgeBaseDetail() { console.log('showKnowledgeGraph:', showKnowledgeGraph, knowledgeGraph); + // 初次加载与 URL 变化时,从查询参数同步当前 Tab + useEffect(() => { + const params = new URLSearchParams(location.search); + const tab = params.get('tab') as 'documents' | 'testing' | 'overview' | 'graph' | null; + const validTabs: Array<'documents' | 'testing' | 'overview' | 'graph'> = ['documents', 'testing', 'overview', 'graph']; + if (tab && validTabs.includes(tab)) { + // 当 URL 指向 graph,但图不可用,则退回到 documents + const targetTab = tab === 'graph' && !showKnowledgeGraph ? 'documents' : tab; + if (targetTab !== currentTab) { + setCurrentTab(targetTab); + } + } + }, [location.search, showKnowledgeGraph]); + + const handleTabChange = (_: any, newValue: 'documents' | 'testing' | 'overview' | 'graph') => { + // 若选择 graph 但不可用,直接忽略并返回 + if (newValue === 'graph' && !showKnowledgeGraph) return; + setCurrentTab(newValue); + const params = new URLSearchParams(location.search); + params.set('tab', newValue); + navigate({ pathname: location.pathname, search: params.toString() }, { replace: false }); + }; + const refreshDetailData = async () => { await fetchKnowledgeDetail(); await refreshFiles(); @@ -303,12 +327,12 @@ function KnowledgeBaseDetail() { setCurrentTab(newValue)} + onChange={handleTabChange} sx={{ borderBottom: 1, borderColor: 'divider' }} > - + {showKnowledgeGraph && ( )} @@ -540,12 +564,12 @@ function KnowledgeBaseDetail() { + {t('common.close')} + ); } -export default KnowledgeBaseDetail; \ No newline at end of file +export default KnowledgeBaseDetail; diff --git a/src/pages/knowledge/overview.tsx b/src/pages/knowledge/overview.tsx index 1d07d07..8dc808c 100644 --- a/src/pages/knowledge/overview.tsx +++ b/src/pages/knowledge/overview.tsx @@ -1,9 +1,10 @@ import React from 'react'; -import { Box, Grid, Paper, Typography, IconButton, TextField, Tabs, Tab, Fab } from '@mui/material'; +import { Box, Grid, Paper, Typography, IconButton, TextField, Tabs, Tab, Fab, Avatar, Chip, Dialog, DialogTitle, DialogContent, DialogActions, Button, Card, CardContent, Divider } from '@mui/material'; import { DataGrid, type GridColDef } from '@mui/x-data-grid'; import { ArrowBack as ArrowBackIcon, Search as SearchIcon, + Visibility as VisibilityIcon } from '@mui/icons-material'; import { useTranslation } from 'react-i18next'; import { useNavigate, useParams } from 'react-router-dom'; @@ -13,6 +14,10 @@ import i18n from '@/locales'; import { enUS, zhCN } from '@mui/x-data-grid/locales'; import KnowledgeBreadcrumbs from './components/KnowledgeBreadcrumbs'; import { LanguageAbbreviation } from '@/constants/common'; +import dayjs from 'dayjs'; +import { RunningStatusMap, RunningStatus } from '@/constants/knowledge'; +import knowledgeService from '@/services/knowledge_service'; +import type { IFileLogItem } from '@/interfaces/database/knowledge'; const PROCESSING_TYPES = { @@ -69,27 +74,94 @@ function KnowledgeLogsPage({ embedded = false, kbId: kbIdProp }: KnowledgeLogsPa }, [currentPage, pageSize]); const columnsFile: GridColDef[] = [ - { field: 'id', headerName: 'ID', width: 100 }, + { field: 'id', headerName: 'ID', width: 100, headerAlign: 'center', align: 'center' }, { - field: 'document_name', headerName: t('knowledgeDetails.fileName'), flex: 1, minWidth: 160, valueGetter: (params) => { - logger.info('params', params) - return '' - } + field: 'document_name', + headerName: t('knowledgeDetails.fileName'), + flex: 1, + minWidth: 160, + headerAlign: 'center', }, - { field: 'source_from', headerName: t('knowledgeDetails.source'), flex: 1, minWidth: 140 }, - { field: 'pipeline_title', headerName: t('knowledgeDetails.ingestionPipeline'), flex: 1, minWidth: 180 }, - { field: 'create_date', headerName: t('knowledgeDetails.startDate') || 'Start Date', flex: 0.8, minWidth: 160, sortable: true }, - { field: 'task_type', headerName: t('knowledgeDetails.task') || 'Task', flex: 0.8, minWidth: 140 }, - { field: 'status', headerName: t('knowledgeDetails.status'), flex: 0.7, minWidth: 120 }, - { field: 'operations', headerName: t('knowledgeDetails.operations') || 'Operations', flex: 0.8, minWidth: 160, sortable: false, filterable: false, align: 'center', headerAlign: 'center' }, + { + field: 'source_from', + headerName: t('knowledgeDetails.source'), + flex: 1, + minWidth: 140, + headerAlign: 'center', + align: 'center', + valueGetter: (value) => value || t('knowledgeDetails.localUpload'), + }, + { + field: 'pipeline_title', + headerName: t('knowledgeDetails.dataPipeline'), + flex: 1, + minWidth: 180, + headerAlign: 'center', + align: 'center', + renderCell: (params) => ( + + {params.row.avatar && ( + + )} + + {params.row.pipeline_title === 'naive' ? t('knowledgeDetails.general') : params.row.pipeline_title} + + + ), + }, + { + field: 'process_begin_at', + headerName: t('knowledgeDetails.startDate'), + flex: 0.8, + minWidth: 160, + sortable: true, + headerAlign: 'center', + align: 'center', + valueGetter: (value) => dayjs(value).format('YYYY-MM-DD HH:mm:ss'), + }, + { field: 'task_type', headerName: t('knowledgeDetails.task'), flex: 0.8, minWidth: 140, headerAlign: 'center', align: 'center' }, + { + field: 'operation_status', + headerName: t('knowledgeDetails.status'), + flex: 0.7, + minWidth: 120, + headerAlign: 'center', + align: 'center', + renderCell: (params) => { + const status: RunningStatus = params.row.operation_status as RunningStatus; + const label = RunningStatusMap[status] || ''; + const colorMap: Record = { + ['0']: 'default', + ['1']: 'info', + ['2']: 'warning', + ['3']: 'success', + ['4']: 'error', + }; + const chipColor = colorMap[status] || 'default'; + return ; + }, + }, + { + field: 'actions', headerName: t('knowledgeDetails.operations'), type: 'actions', + flex: 0.8, minWidth: 160, sortable: false, filterable: false, align: 'center', headerAlign: 'center', + getActions: (params) => [ + handleViewClick(params.row)} + > + + , + ], + } ]; const columnsDataset: GridColDef[] = [ - { field: 'id', headerName: 'ID', width: 100 }, - { field: 'create_date', headerName: t('knowledgeDetails.startDate') || 'Start Date', flex: 1, minWidth: 160, sortable: true }, - { field: 'task_type', headerName: 'Processing Type', flex: 1, minWidth: 180 }, - { field: 'status', headerName: t('knowledgeDetails.status'), flex: 0.8, minWidth: 120 }, - { field: 'operations', headerName: t('knowledgeDetails.operations') || 'Operations', flex: 0.8, minWidth: 160, sortable: false, filterable: false, align: 'center', headerAlign: 'center' }, + { field: 'id', headerName: 'ID', width: 100, headerAlign: 'center', align: 'center' }, + { field: 'create_date', headerName: t('knowledgeDetails.startDate'), flex: 1, minWidth: 160, sortable: true, headerAlign: 'center', align: 'center' }, + { field: 'task_type', headerName: t('knowledgeDetails.task'), flex: 1, minWidth: 180, headerAlign: 'center', align: 'center' }, + { field: 'status', headerName: t('knowledgeDetails.status'), flex: 0.8, minWidth: 120, headerAlign: 'center', align: 'center' }, + { field: 'operations', headerName: t('knowledgeDetails.operations'), flex: 0.8, minWidth: 160, sortable: false, filterable: false, align: 'center', headerAlign: 'center' }, ]; const columns = React.useMemo(() => (activeTab === 'fileLogs' ? @@ -106,6 +178,37 @@ function KnowledgeLogsPage({ embedded = false, kbId: kbIdProp }: KnowledgeLogsPa [activeTab, fileLogs, datasetLogs]); const navigate = useNavigate(); + + // 日志详情弹框状态 + const [detailDialogOpen, setDetailDialogOpen] = React.useState(false); + const [selectedLog, setSelectedLog] = React.useState(null); + const [detailLoading, setDetailLoading] = React.useState(false); + const [detailError, setDetailError] = React.useState(null); + const [detailData, setDetailData] = React.useState(null); + + async function handleViewClick(row: IFileLogItem) { + setSelectedLog(row); + setDetailDialogOpen(true); + setDetailLoading(true); + setDetailError(null); + try { + if (row?.task_id) { + const resp = await knowledgeService.getPipelineDetail({ task_id: row.task_id }); + if (resp.data?.code === 0) { + setDetailData(resp.data.data || null); + } else { + setDetailError(resp.data?.message || '获取日志详情失败'); + } + } else { + // 没有 task_id 时使用列表中的 progress_msg 作为详情 + setDetailData({ message: row?.progress_msg || '' }); + } + } catch (e: any) { + setDetailError(e?.response?.data?.message || e?.message || '获取日志详情失败'); + } finally { + setDetailLoading(false); + } + } const handleNavigateBack = () => { navigate(`/knowledge/${kbId}`); }; @@ -187,9 +290,114 @@ function KnowledgeLogsPage({ embedded = false, kbId: kbIdProp }: KnowledgeLogsPa loading={loading} disableColumnMenu localeText={getDataGridLocale().components.MuiDataGrid.defaultProps.localeText} + sx={{ + '& .MuiDataGrid-columnHeader': { justifyContent: 'center' }, + '& .MuiDataGrid-cell': { justifyContent: 'center' }, + }} /> + {/* 日志详情弹框 */} + setDetailDialogOpen(false)} maxWidth="md" fullWidth> + {t('knowledgeDetails.viewProcessDetails')} + + {selectedLog && ( + <> + + + {t('knowledgeDetails.file')} + + {selectedLog.document_name || selectedLog.name} + + + + {t('knowledgeDetails.task')} + + {selectedLog.task_type || selectedLog.pipeline_title} + + + + {t('knowledgeDetails.status')} + + {(() => { + const status: RunningStatus = (selectedLog.operation_status as RunningStatus) || (selectedLog.status as RunningStatus); + const label = RunningStatusMap[status] || ''; + const colorMap: Record = { + ['0']: 'default', + ['1']: 'info', + ['2']: 'warning', + ['3']: 'success', + ['4']: 'error', + }; + const chipColor = colorMap[status] || 'default'; + return ; + })()} + + + + + + + + + {t('knowledgeDetails.startDate')} + + {dayjs(selectedLog.process_begin_at || selectedLog.create_date).format('YYYY-MM-DD HH:mm:ss')} + + + + {t('knowledgeDetails.duration')} + + {typeof selectedLog.process_duration === 'number' + ? `${selectedLog.process_duration.toFixed(3)}s` + : `${selectedLog.process_duration || '-'}s`} + + + + + + {t('knowledgeDetails.details')} + + {detailLoading ? ( + {t('common.loading')} + ) : detailError ? ( + {detailError} + ) : ( + + {detailData?.message || selectedLog.progress_msg || ''} + + )} + + + + )} + + + + + + {/* 返回按钮 */} {!embedded && (