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',
|
datasetLog: 'Dataset Log',
|
||||||
created: 'Created',
|
created: 'Created',
|
||||||
learnMore: 'Learn More',
|
learnMore: 'Learn More',
|
||||||
|
localUpload: 'Local Upload',
|
||||||
general: 'General',
|
general: 'General',
|
||||||
chunkMethodTab: 'Chunk Method',
|
chunkMethodTab: 'Chunk Method',
|
||||||
testResults: 'Test Results',
|
testResults: 'Test Results',
|
||||||
|
|||||||
@@ -447,6 +447,7 @@ export default {
|
|||||||
datasetLog: '知识库日志',
|
datasetLog: '知识库日志',
|
||||||
created: '创建于',
|
created: '创建于',
|
||||||
learnMore: '了解更多',
|
learnMore: '了解更多',
|
||||||
|
localUpload: '本地上传',
|
||||||
general: '通用',
|
general: '通用',
|
||||||
chunkMethodTab: '切片方法',
|
chunkMethodTab: '切片方法',
|
||||||
testResults: '测试结果',
|
testResults: '测试结果',
|
||||||
|
|||||||
@@ -38,10 +38,10 @@ const KnowledgeInfoCard: React.FC<KnowledgeInfoCardProps> = ({ knowledgeBase })
|
|||||||
{knowledgeBase.description || t('knowledge.noDescription')}
|
{knowledgeBase.description || t('knowledge.noDescription')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Stack direction="row" spacing={2} flexWrap="wrap">
|
<Stack direction="row" spacing={2} flexWrap="wrap">
|
||||||
<Chip label={t('knowledge.fileCount', { count: knowledgeBase.doc_num || 0 })} variant="outlined" />
|
<Chip label={`${t('knowledge.fileCount')}: ${knowledgeBase.doc_num || 0}`} variant="outlined" />
|
||||||
<Chip label={t('knowledge.chunkCount', { count: knowledgeBase.chunk_num || 0 })} variant="outlined" />
|
<Chip label={`${t('knowledge.chunkCount')}: ${knowledgeBase.chunk_num || 0}`} variant="outlined" />
|
||||||
<Chip label={t('knowledge.tokenCount', { count: knowledgeBase.token_num || 0 })} variant="outlined" />
|
<Chip label={`${t('knowledge.tokenCount')}: ${knowledgeBase.token_num || 0}`} variant="outlined" />
|
||||||
<Chip label={t('knowledge.size', { size: formatFileSize(knowledgeBase.size || 0) })} variant="outlined" />
|
<Chip label={`${t('knowledge.size')}: ${formatFileSize(knowledgeBase.size || 0)}`} variant="outlined" />
|
||||||
</Stack>
|
</Stack>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ xs: 12, md: 4 }}>
|
<Grid size={{ xs: 12, md: 4 }}>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
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 { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
@@ -42,6 +42,7 @@ import logger from '@/utils/logger';
|
|||||||
function KnowledgeBaseDetail() {
|
function KnowledgeBaseDetail() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
// 状态管理
|
// 状态管理
|
||||||
@@ -102,6 +103,29 @@ function KnowledgeBaseDetail() {
|
|||||||
|
|
||||||
console.log('showKnowledgeGraph:', showKnowledgeGraph, knowledgeGraph);
|
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 () => {
|
const refreshDetailData = async () => {
|
||||||
await fetchKnowledgeDetail();
|
await fetchKnowledgeDetail();
|
||||||
await refreshFiles();
|
await refreshFiles();
|
||||||
@@ -303,12 +327,12 @@ function KnowledgeBaseDetail() {
|
|||||||
<Box sx={{ mt: 3 }}>
|
<Box sx={{ mt: 3 }}>
|
||||||
<Tabs
|
<Tabs
|
||||||
value={currentTab}
|
value={currentTab}
|
||||||
onChange={(event, newValue) => setCurrentTab(newValue)}
|
onChange={handleTabChange}
|
||||||
sx={{ borderBottom: 1, borderColor: 'divider' }}
|
sx={{ borderBottom: 1, borderColor: 'divider' }}
|
||||||
>
|
>
|
||||||
<Tab value="documents" label={t('knowledgeDetails.files')} />
|
<Tab value="documents" label={t('knowledgeDetails.files')} />
|
||||||
<Tab value="testing" label={t('knowledgeDetails.testing')} />
|
<Tab value="testing" label={t('knowledgeDetails.testing')} />
|
||||||
<Tab value="overview" label={t('knowledgeDetails.datasetLogs')} />
|
<Tab value="overview" label={t('knowledgeDetails.overview')} />
|
||||||
{showKnowledgeGraph && (
|
{showKnowledgeGraph && (
|
||||||
<Tab value="graph" label={t('knowledgeDetails.graph')} />
|
<Tab value="graph" label={t('knowledgeDetails.graph')} />
|
||||||
)}
|
)}
|
||||||
@@ -540,12 +564,12 @@ function KnowledgeBaseDetail() {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions sx={{ p: 3, pt: 0 }}>
|
<DialogActions sx={{ p: 3, pt: 0 }}>
|
||||||
<Button onClick={() => setProcessDetailsDialogOpen(false)} variant="contained">
|
<Button onClick={() => setProcessDetailsDialogOpen(false)} variant="contained">
|
||||||
{t('common.close')}
|
{t('common.close')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default KnowledgeBaseDetail;
|
export default KnowledgeBaseDetail;
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import React from 'react';
|
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 { DataGrid, type GridColDef } from '@mui/x-data-grid';
|
||||||
import {
|
import {
|
||||||
ArrowBack as ArrowBackIcon,
|
ArrowBack as ArrowBackIcon,
|
||||||
Search as SearchIcon,
|
Search as SearchIcon,
|
||||||
|
Visibility as VisibilityIcon
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
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 { enUS, zhCN } from '@mui/x-data-grid/locales';
|
||||||
import KnowledgeBreadcrumbs from './components/KnowledgeBreadcrumbs';
|
import KnowledgeBreadcrumbs from './components/KnowledgeBreadcrumbs';
|
||||||
import { LanguageAbbreviation } from '@/constants/common';
|
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 = {
|
const PROCESSING_TYPES = {
|
||||||
@@ -69,27 +74,94 @@ function KnowledgeLogsPage({ embedded = false, kbId: kbIdProp }: KnowledgeLogsPa
|
|||||||
}, [currentPage, pageSize]);
|
}, [currentPage, pageSize]);
|
||||||
|
|
||||||
const columnsFile: GridColDef[] = [
|
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) => {
|
field: 'document_name',
|
||||||
logger.info('params', params)
|
headerName: t('knowledgeDetails.fileName'),
|
||||||
return ''
|
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: 'source_from',
|
||||||
{ field: 'create_date', headerName: t('knowledgeDetails.startDate') || 'Start Date', flex: 0.8, minWidth: 160, sortable: true },
|
headerName: t('knowledgeDetails.source'),
|
||||||
{ field: 'task_type', headerName: t('knowledgeDetails.task') || 'Task', flex: 0.8, minWidth: 140 },
|
flex: 1,
|
||||||
{ field: 'status', headerName: t('knowledgeDetails.status'), flex: 0.7, minWidth: 120 },
|
minWidth: 140,
|
||||||
{ field: 'operations', headerName: t('knowledgeDetails.operations') || 'Operations', flex: 0.8, minWidth: 160, sortable: false, filterable: false, align: 'center', headerAlign: 'center' },
|
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[] = [
|
const columnsDataset: GridColDef[] = [
|
||||||
{ field: 'id', headerName: 'ID', width: 100 },
|
{ field: 'id', headerName: 'ID', width: 100, headerAlign: 'center', align: 'center' },
|
||||||
{ field: 'create_date', headerName: t('knowledgeDetails.startDate') || 'Start Date', flex: 1, minWidth: 160, sortable: true },
|
{ field: 'create_date', headerName: t('knowledgeDetails.startDate'), flex: 1, minWidth: 160, sortable: true, headerAlign: 'center', align: 'center' },
|
||||||
{ field: 'task_type', headerName: 'Processing Type', flex: 1, minWidth: 180 },
|
{ 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 },
|
{ field: 'status', headerName: t('knowledgeDetails.status'), flex: 0.8, minWidth: 120, headerAlign: 'center', align: 'center' },
|
||||||
{ field: 'operations', headerName: t('knowledgeDetails.operations') || 'Operations', flex: 0.8, minWidth: 160, sortable: false, filterable: false, align: 'center', headerAlign: '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' ?
|
const columns = React.useMemo(() => (activeTab === 'fileLogs' ?
|
||||||
@@ -106,6 +178,37 @@ function KnowledgeLogsPage({ embedded = false, kbId: kbIdProp }: KnowledgeLogsPa
|
|||||||
[activeTab, fileLogs, datasetLogs]);
|
[activeTab, fileLogs, datasetLogs]);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
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 = () => {
|
const handleNavigateBack = () => {
|
||||||
navigate(`/knowledge/${kbId}`);
|
navigate(`/knowledge/${kbId}`);
|
||||||
};
|
};
|
||||||
@@ -187,9 +290,114 @@ function KnowledgeLogsPage({ embedded = false, kbId: kbIdProp }: KnowledgeLogsPa
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
disableColumnMenu
|
disableColumnMenu
|
||||||
localeText={getDataGridLocale().components.MuiDataGrid.defaultProps.localeText}
|
localeText={getDataGridLocale().components.MuiDataGrid.defaultProps.localeText}
|
||||||
|
sx={{
|
||||||
|
'& .MuiDataGrid-columnHeader': { justifyContent: 'center' },
|
||||||
|
'& .MuiDataGrid-cell': { justifyContent: 'center' },
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Paper>
|
</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 && (
|
{!embedded && (
|
||||||
<Fab
|
<Fab
|
||||||
|
|||||||
Reference in New Issue
Block a user