feat(knowledge): add local upload translation and sync tab with URL
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -447,6 +447,7 @@ export default {
|
||||
datasetLog: '知识库日志',
|
||||
created: '创建于',
|
||||
learnMore: '了解更多',
|
||||
localUpload: '本地上传',
|
||||
general: '通用',
|
||||
chunkMethodTab: '切片方法',
|
||||
testResults: '测试结果',
|
||||
|
||||
@@ -38,10 +38,10 @@ const KnowledgeInfoCard: React.FC<KnowledgeInfoCardProps> = ({ knowledgeBase })
|
||||
{knowledgeBase.description || t('knowledge.noDescription')}
|
||||
</Typography>
|
||||
<Stack direction="row" spacing={2} flexWrap="wrap">
|
||||
<Chip label={t('knowledge.fileCount', { count: knowledgeBase.doc_num || 0 })} variant="outlined" />
|
||||
<Chip label={t('knowledge.chunkCount', { count: knowledgeBase.chunk_num || 0 })} variant="outlined" />
|
||||
<Chip label={t('knowledge.tokenCount', { count: knowledgeBase.token_num || 0 })} variant="outlined" />
|
||||
<Chip label={t('knowledge.size', { size: formatFileSize(knowledgeBase.size || 0) })} variant="outlined" />
|
||||
<Chip label={`${t('knowledge.fileCount')}: ${knowledgeBase.doc_num || 0}`} variant="outlined" />
|
||||
<Chip label={`${t('knowledge.chunkCount')}: ${knowledgeBase.chunk_num || 0}`} variant="outlined" />
|
||||
<Chip label={`${t('knowledge.tokenCount')}: ${knowledgeBase.token_num || 0}`} variant="outlined" />
|
||||
<Chip label={`${t('knowledge.size')}: ${formatFileSize(knowledgeBase.size || 0)}`} variant="outlined" />
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 4 }}>
|
||||
|
||||
@@ -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() {
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Tabs
|
||||
value={currentTab}
|
||||
onChange={(event, newValue) => setCurrentTab(newValue)}
|
||||
onChange={handleTabChange}
|
||||
sx={{ borderBottom: 1, borderColor: 'divider' }}
|
||||
>
|
||||
<Tab value="documents" label={t('knowledgeDetails.files')} />
|
||||
<Tab value="testing" label={t('knowledgeDetails.testing')} />
|
||||
<Tab value="overview" label={t('knowledgeDetails.datasetLogs')} />
|
||||
<Tab value="overview" label={t('knowledgeDetails.overview')} />
|
||||
{showKnowledgeGraph && (
|
||||
<Tab value="graph" label={t('knowledgeDetails.graph')} />
|
||||
)}
|
||||
@@ -540,12 +564,12 @@ function KnowledgeBaseDetail() {
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ p: 3, pt: 0 }}>
|
||||
<Button onClick={() => setProcessDetailsDialogOpen(false)} variant="contained">
|
||||
{t('common.close')}
|
||||
</Button>
|
||||
{t('common.close')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default KnowledgeBaseDetail;
|
||||
export default KnowledgeBaseDetail;
|
||||
|
||||
@@ -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) => (
|
||||
<Box sx={{ height: '100%', width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 1 }}>
|
||||
{params.row.avatar && (
|
||||
<Avatar src={params.row.avatar} alt={params.row.pipeline_title} sx={{ width: 18, height: 18 }} />
|
||||
)}
|
||||
<Typography variant="body2">
|
||||
{params.row.pipeline_title === 'naive' ? t('knowledgeDetails.general') : params.row.pipeline_title}
|
||||
</Typography>
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
{
|
||||
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<string, 'default' | 'info' | 'warning' | 'success' | 'error'> = {
|
||||
['0']: 'default',
|
||||
['1']: 'info',
|
||||
['2']: 'warning',
|
||||
['3']: 'success',
|
||||
['4']: 'error',
|
||||
};
|
||||
const chipColor = colorMap[status] || 'default';
|
||||
return <Chip label={label} color={chipColor} size="small" />;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'actions', headerName: t('knowledgeDetails.operations'), type: 'actions',
|
||||
flex: 0.8, minWidth: 160, sortable: false, filterable: false, align: 'center', headerAlign: 'center',
|
||||
getActions: (params) => [
|
||||
<IconButton
|
||||
key="view"
|
||||
color="primary"
|
||||
onClick={() => handleViewClick(params.row)}
|
||||
>
|
||||
<VisibilityIcon />
|
||||
</IconButton>,
|
||||
],
|
||||
}
|
||||
];
|
||||
|
||||
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<IFileLogItem | null>(null);
|
||||
const [detailLoading, setDetailLoading] = React.useState(false);
|
||||
const [detailError, setDetailError] = React.useState<string | null>(null);
|
||||
const [detailData, setDetailData] = React.useState<any>(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' },
|
||||
}}
|
||||
/>
|
||||
</Paper>
|
||||
|
||||
{/* 日志详情弹框 */}
|
||||
<Dialog open={detailDialogOpen} onClose={() => setDetailDialogOpen(false)} maxWidth="md" fullWidth>
|
||||
<DialogTitle>{t('knowledgeDetails.viewProcessDetails')}</DialogTitle>
|
||||
<DialogContent sx={{ pt: 1 }}>
|
||||
{selectedLog && (
|
||||
<>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Typography variant="subtitle2" color="text.secondary">{t('knowledgeDetails.file')}</Typography>
|
||||
<Typography variant="body1" sx={{ mt: 0.5 }}>
|
||||
{selectedLog.document_name || selectedLog.name}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Typography variant="subtitle2" color="text.secondary">{t('knowledgeDetails.task')}</Typography>
|
||||
<Typography variant="body1" sx={{ mt: 0.5 }}>
|
||||
{selectedLog.task_type || selectedLog.pipeline_title}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 3 }}>
|
||||
<Typography variant="subtitle2" color="text.secondary">{t('knowledgeDetails.status')}</Typography>
|
||||
<Box sx={{ mt: 0.5 }}>
|
||||
{(() => {
|
||||
const status: RunningStatus = (selectedLog.operation_status as RunningStatus) || (selectedLog.status as RunningStatus);
|
||||
const label = RunningStatusMap[status] || '';
|
||||
const colorMap: Record<string, 'default' | 'info' | 'warning' | 'success' | 'error'> = {
|
||||
['0']: 'default',
|
||||
['1']: 'info',
|
||||
['2']: 'warning',
|
||||
['3']: 'success',
|
||||
['4']: 'error',
|
||||
};
|
||||
const chipColor = colorMap[status] || 'default';
|
||||
return <Chip label={label} color={chipColor} size="small" />;
|
||||
})()}
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Divider sx={{ my: 2 }} />
|
||||
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Typography variant="subtitle2" color="text.secondary">{t('knowledgeDetails.startDate')}</Typography>
|
||||
<Typography variant="body1" sx={{ mt: 0.5 }}>
|
||||
{dayjs(selectedLog.process_begin_at || selectedLog.create_date).format('YYYY-MM-DD HH:mm:ss')}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Typography variant="subtitle2" color="text.secondary">{t('knowledgeDetails.duration')}</Typography>
|
||||
<Typography variant="body1" sx={{ mt: 0.5 }}>
|
||||
{typeof selectedLog.process_duration === 'number'
|
||||
? `${selectedLog.process_duration.toFixed(3)}s`
|
||||
: `${selectedLog.process_duration || '-'}s`}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Typography variant="subtitle2" color="text.secondary" sx={{ mb: 1 }}>{t('knowledgeDetails.details')}</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
p: 2,
|
||||
bgcolor: 'background.default',
|
||||
borderRadius: 1,
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
maxHeight: 320,
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
{detailLoading ? (
|
||||
<Typography color="text.secondary">{t('common.loading')}</Typography>
|
||||
) : detailError ? (
|
||||
<Typography color="error.main">{detailError}</Typography>
|
||||
) : (
|
||||
<Typography
|
||||
component="pre"
|
||||
sx={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '0.875rem',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
m: 0,
|
||||
}}
|
||||
>
|
||||
{detailData?.message || selectedLog.progress_msg || ''}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ p: 3, pt: 0 }}>
|
||||
<Button onClick={() => setDetailDialogOpen(false)} variant="contained">
|
||||
{t('common.close')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* 返回按钮 */}
|
||||
{!embedded && (
|
||||
<Fab
|
||||
|
||||
Reference in New Issue
Block a user