feat(i18n): add internationalization support across multiple components

This commit is contained in:
2025-10-29 16:40:20 +08:00
parent 184c232cc8
commit 9199ed7c29
34 changed files with 1455 additions and 761 deletions

View File

@@ -1,5 +1,6 @@
import React, { useMemo } from 'react';
import { useFormContext, useWatch, type UseFormReturn } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import {
Box,
Typography,
@@ -47,10 +48,11 @@ const ConfigurationComponentMap = {
// 空组件
function EmptyComponent() {
const { t } = useTranslation();
return (
<Box sx={{ textAlign: 'center', py: 4 }}>
<Typography color="text.secondary">
{t('knowledge.selectParserMethod')}
</Typography>
</Box>
);
@@ -74,9 +76,10 @@ function ChunkMethodForm({
onSubmit,
isSubmitting,
onCancel,
submitButtonText = '保存',
cancelButtonText = '取消',
submitButtonText,
cancelButtonText,
}: ChunkMethodFormProps = {}) {
const { t } = useTranslation();
// 优先使用props传递的form否则使用FormProvider的context
let contextForm;
try {
@@ -92,7 +95,7 @@ function ChunkMethodForm({
return (
<Box sx={{ p: 2, textAlign: 'center' }}>
<Typography color="error">
FormProvider中使用或传递form参数
{t('form.formConfigError')}
</Typography>
</Box>
);
@@ -127,7 +130,7 @@ function ChunkMethodForm({
onClick={onCancel}
disabled={isSubmitting}
>
{cancelButtonText}
{cancelButtonText || t('common.cancel')}
</Button>
)}
<Button
@@ -135,7 +138,7 @@ function ChunkMethodForm({
onClick={form ? form.handleSubmit(onSubmit) : undefined}
disabled={isSubmitting || !form}
>
{isSubmitting ? '提交中...' : submitButtonText}
{isSubmitting ? t('common.submitting') : (submitButtonText || t('common.save'))}
</Button>
</Box>
)}

View File

@@ -1,5 +1,6 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { t as translate } from 'i18next';
import {
Box,
Typography,
@@ -91,13 +92,13 @@ interface DocumentListComponentProps {
const getRunStatusLabel = (status: string) => {
const statusLabels = {
[RUNNING_STATUS_KEYS.UNSTART]: '未开始',
[RUNNING_STATUS_KEYS.RUNNING]: '运行中',
[RUNNING_STATUS_KEYS.CANCEL]: '已取消',
[RUNNING_STATUS_KEYS.DONE]: '完成',
[RUNNING_STATUS_KEYS.FAIL]: '失败',
[RUNNING_STATUS_KEYS.UNSTART]: translate('knowledge.runStatus.unstart'),
[RUNNING_STATUS_KEYS.RUNNING]: translate('knowledge.runStatus.running'),
[RUNNING_STATUS_KEYS.CANCEL]: translate('knowledge.runStatus.cancel'),
[RUNNING_STATUS_KEYS.DONE]: translate('knowledge.runStatus.done'),
[RUNNING_STATUS_KEYS.FAIL]: translate('knowledge.runStatus.fail'),
};
return statusLabels[status as keyof typeof statusLabels] || '未知';
return statusLabels[status as keyof typeof statusLabels] || translate('knowledge.runStatus.unknown');
};
const getFileIcon = (type: string, suffix?: string) => {
@@ -138,21 +139,21 @@ const formatFileSize = (bytes: number): string => {
};
const getStatusChip = (status: string) => {
return <Chip label={status === '1' ? '启用' : '禁用'}
return <Chip label={status === '1' ? translate('common.enabled') : translate('common.disabled')}
color={status === '1' ? 'success' : 'error'}
size="small" />;
};
const getRunStatusChip = (run: RunningStatus, progress: number) => {
const statusConfig = {
[RUNNING_STATUS_KEYS.UNSTART]: { label: '未开始', color: 'default' as const },
[RUNNING_STATUS_KEYS.RUNNING]: { label: `解析中`, color: 'info' as const },
[RUNNING_STATUS_KEYS.CANCEL]: { label: '已取消', color: 'warning' as const },
[RUNNING_STATUS_KEYS.DONE]: { label: '完成', color: 'success' as const },
[RUNNING_STATUS_KEYS.FAIL]: { label: '失败', color: 'error' as const },
const statusConfig = {
[RUNNING_STATUS_KEYS.UNSTART]: { label: translate('knowledge.runStatus.unstart'), color: 'default' as const },
[RUNNING_STATUS_KEYS.RUNNING]: { label: translate('knowledge.runStatus.parsing'), color: 'info' as const },
[RUNNING_STATUS_KEYS.CANCEL]: { label: translate('knowledge.runStatus.cancel'), color: 'warning' as const },
[RUNNING_STATUS_KEYS.DONE]: { label: translate('knowledge.runStatus.done'), color: 'success' as const },
[RUNNING_STATUS_KEYS.FAIL]: { label: translate('knowledge.runStatus.fail'), color: 'error' as const },
};
const config = statusConfig[run] || { label: '未知', color: 'default' as const };
const config = statusConfig[run] || { label: translate('knowledge.runStatus.unknown'), color: 'default' as const };
if (run === RUNNING_STATUS_KEYS.RUNNING) {
return (
@@ -209,7 +210,7 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
const [renameDialogOpen, setRenameDialogOpen] = useState(false);
const [newFileName, setNewFileName] = useState('');
const { i18n } = useTranslation();
const { i18n, t } = useTranslation();
// 根据当前语言获取DataGrid的localeText
const getDataGridLocale = () => {
@@ -328,7 +329,7 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
const columns: GridColDef[] = [
{
field: 'name',
headerName: '文件名',
headerName: t('knowledge.fileName'),
flex: 2,
minWidth: 200,
cellClassName: 'grid-center-cell',
@@ -349,7 +350,7 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
}
}}
>
<Tooltip title={`查看 ${params.value} 的详情`}>
<Tooltip title={t('knowledge.viewFileDetails', { fileName: params.value })}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{getFileIcon(params.row.type, params.row.suffix)}
<Typography variant="body2" noWrap>{params.value}</Typography>
@@ -360,7 +361,7 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
},
{
field: 'type',
headerName: '类型',
headerName: t('knowledge.type'),
flex: 0.5,
minWidth: 80,
renderCell: (params) => (
@@ -369,42 +370,42 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
},
{
field: 'size',
headerName: '大小',
headerName: t('knowledge.size'),
flex: 0.5,
minWidth: 80,
renderCell: (params) => formatFileSize(params.value),
},
{
field: 'chunk_num',
headerName: '分块数',
headerName: t('knowledge.chunkCount'),
flex: 0.5,
minWidth: 80,
type: 'number',
},
{
field: 'status',
headerName: '状态',
headerName: t('knowledge.status'),
flex: 0.8,
minWidth: 100,
renderCell: (params) => getStatusChip(params.value),
},
{
field: 'run',
headerName: '解析状态',
headerName: t('knowledge.parseStatus'),
flex: 0.8,
minWidth: 100,
renderCell: (params) => getRunStatusChip(params.value, params.row.progress),
},
{
field: 'create_time',
headerName: '上传时间',
headerName: t('knowledge.uploadTime'),
flex: 1,
minWidth: 140,
valueFormatter: (value) => dayjs(value).format('YYYY-MM-DD HH:mm:ss'),
},
{
field: 'actions',
headerName: '操作',
headerName: t('knowledge.actions'),
flex: 1.2,
minWidth: 200,
sortable: false,
@@ -453,7 +454,7 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
>
<ProcessIcon fontSize="small" />
</Box>
<Tooltip title="更多操作">
<Tooltip title={t('common.moreActions')}>
<IconButton
size="small"
onClick={(e) => handleMenuClick(e, params.row)}
@@ -471,16 +472,16 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
{/* 筛选器 */}
{documentFilter && (
<Paper sx={{ p: 2, mb: 2 }}>
<Typography variant="h6" sx={{ mb: 2 }}></Typography>
<Typography variant="h6" sx={{ mb: 2 }}>{t('knowledge.filter')}</Typography>
<Stack direction="row" spacing={2} alignItems="center">
{/* 运行状态筛选 */}
<FormControl sx={{ minWidth: 200 }}>
<InputLabel></InputLabel>
<InputLabel>{t('knowledge.runStatusFilter')}</InputLabel>
<Select
multiple
value={selectedRunStatus}
onChange={handleRunStatusChange}
input={<OutlinedInput label="运行状态" />}
input={<OutlinedInput label={t('knowledge.runStatusFilter')} />}
renderValue={(selected) => (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{selected.map((value) => (
@@ -499,12 +500,12 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
{/* 文件类型筛选 */}
<FormControl sx={{ minWidth: 200 }}>
<InputLabel></InputLabel>
<InputLabel>{t('knowledge.fileTypeFilter')}</InputLabel>
<Select
multiple
value={selectedSuffix}
onChange={handleSuffixChange}
input={<OutlinedInput label="文件类型" />}
input={<OutlinedInput label={t('knowledge.fileTypeFilter')} />}
renderValue={(selected) => (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{selected.map((value) => (
@@ -529,7 +530,7 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
});
handleFilterSubmit();
}}>
{t('common.clearFilter')}
</Button>
{/* submit filter */}
<Button variant="contained" onClick={async () => {
@@ -538,7 +539,7 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
});
handleFilterSubmit();
}}>
{t('common.confirmFilter')}
</Button>
</Stack>
</Paper>
@@ -548,7 +549,7 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
<Paper sx={{ p: 2, mb: 2 }}>
<Stack direction="row" spacing={2} alignItems="center" justifyContent="space-between">
<TextField
placeholder="搜索文件..."
placeholder={t('knowledge.searchFiles')}
value={searchKeyword}
onChange={(e) => onSearchChange(e.target.value)}
InputProps={{
@@ -568,7 +569,7 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
startIcon={<UploadIcon />}
onClick={onUpload}
>
{t('knowledge.uploadFile')}
</Button>
{rowSelectionModel.ids.size > 0 && (
@@ -578,7 +579,7 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
startIcon={<RefreshIcon />}
onClick={() => onReparse(Array.from(rowSelectionModel.ids).map((id) => id.toString()))}
>
({rowSelectionModel.ids.size})
{t('knowledge.reparse')} ({rowSelectionModel.ids.size})
</Button>
<Button
@@ -587,7 +588,7 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
startIcon={<DeleteIcon />}
onClick={() => onDelete(Array.from(rowSelectionModel.ids).map((id) => id.toString()))}
>
({rowSelectionModel.ids.size})
{t('common.delete')} ({rowSelectionModel.ids.size})
</Button>
</>
)}
@@ -637,47 +638,47 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
>
<MenuItem onClick={() => handleViewDetails(selectedFile)}>
<ViewIcon sx={{ mr: 1 }} />
<Typography></Typography>
<Typography>{t('knowledge.viewDetails')}</Typography>
</MenuItem>
<MenuItem onClick={handleRename}>
<EditIcon sx={{ mr: 1 }} />
<Typography></Typography>
<Typography>{t('common.rename')}</Typography>
</MenuItem>
<MenuItem onClick={handleReparse}>
<PlayIcon sx={{ mr: 1 }} />
<Typography></Typography>
<Typography>{t('knowledge.reparse')}</Typography>
</MenuItem>
{selectedFile?.run === RUNNING_STATUS_KEYS.RUNNING && (
<MenuItem onClick={handleCancelRun}>
<StopIcon sx={{ mr: 1 }} />
<Typography></Typography>
<Typography>{t('knowledge.cancelRun')}</Typography>
</MenuItem>
)}
{selectedFile?.status === '1' ? (
<MenuItem onClick={() => handleChangeStatus('0')}>
<DisableIcon sx={{ mr: 1 }} />
<Typography></Typography>
<Typography>{t('common.disable')}</Typography>
</MenuItem>
) : (
<MenuItem onClick={() => handleChangeStatus('1')}>
<EnableIcon sx={{ mr: 1 }} />
<Typography></Typography>
<Typography>{t('common.enable')}</Typography>
</MenuItem>
)}
<MenuItem onClick={handleDelete}>
<DeleteIcon sx={{ mr: 1 }} color="error" />
<Typography color="error"></Typography>
<Typography color="error">{t('common.delete')}</Typography>
</MenuItem>
</Menu>
{/* 重命名对话框 */}
<Dialog open={renameDialogOpen} onClose={() => setRenameDialogOpen(false)}>
<DialogTitle></DialogTitle>
<DialogTitle>{t('knowledge.renameFile')}</DialogTitle>
<DialogContent>
<TextField
autoFocus
margin="dense"
label="文件名"
label={t('knowledge.fileName')}
fullWidth
variant="outlined"
value={newFileName}
@@ -685,8 +686,8 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
/>
</DialogContent>
<DialogActions>
<Button onClick={() => setRenameDialogOpen(false)}></Button>
<Button onClick={handleRenameConfirm} variant="contained"></Button>
<Button onClick={() => setRenameDialogOpen(false)}>{t('common.cancel')}</Button>
<Button onClick={handleRenameConfirm} variant="contained">{t('common.confirm')}</Button>
</DialogActions>
</Dialog>
</Box>

View File

@@ -6,6 +6,7 @@ import {
Settings as ConfigIcon,
} from '@mui/icons-material';
import { useTranslation } from 'react-i18next';
interface FloatingActionButtonsProps {
onTestClick: () => void;
@@ -16,22 +17,24 @@ const FloatingActionButtons: React.FC<FloatingActionButtonsProps> = ({
onTestClick,
onConfigClick,
}) => {
const { t } = useTranslation();
const actions = [
{
icon: <TestIcon />,
name: '检索测试',
name: t('knowledge.retrievalTest'),
onClick: onTestClick,
},
{
icon: <ConfigIcon />,
name: '配置设置',
name: t('knowledge.configSettings'),
onClick: onConfigClick,
},
];
return (
<SpeedDial
ariaLabel="知识库操作"
ariaLabel={t('knowledge.knowledgeBaseActions')}
sx={{
position: 'fixed',
bottom: 128,

View File

@@ -17,6 +17,7 @@ import {
PhotoCamera as PhotoCameraIcon,
Delete as DeleteIcon,
} from '@mui/icons-material';
import { useTranslation } from 'react-i18next';
interface GeneralFormProps {
form?: UseFormReturn;
@@ -32,9 +33,13 @@ function GeneralForm({
onSubmit,
isSubmitting,
onCancel,
submitButtonText = '保存',
cancelButtonText = '取消',
submitButtonText,
cancelButtonText,
}: GeneralFormProps = {}) {
const { t } = useTranslation();
const defaultSubmitButtonText = submitButtonText || t('common.save');
const defaultCancelButtonText = cancelButtonText || t('common.cancel');
// 优先使用props传递的form否则使用FormProvider的context
let contextForm: UseFormReturn | null = null;
try {
@@ -50,7 +55,7 @@ function GeneralForm({
return (
<Box sx={{ p: 2, textAlign: 'center' }}>
<Typography color="error">
FormProvider中使用或传递form参数
{t('form.configurationError')}
</Typography>
</Box>
);
@@ -85,7 +90,7 @@ function GeneralForm({
return (
<Box sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom>
{t('knowledge.basicInfo')}
</Typography>
<Grid container spacing={3}>
@@ -106,7 +111,7 @@ function GeneralForm({
startIcon={<PhotoCameraIcon />}
onClick={handleAvatarClick}
>
{t('knowledge.uploadAvatar')}
</Button>
{avatar && (
<IconButton
@@ -136,11 +141,11 @@ function GeneralForm({
<Controller
name="name"
control={control}
rules={{ required: '知识库名称不能为空' }}
rules={{ required: t('knowledge.nameRequired') }}
render={({ field, fieldState: { error } }) => (
<TextField
{...field}
label="知识库名称"
label={t('knowledge.knowledgeBaseName')}
fullWidth
required
error={!!error}
@@ -157,11 +162,11 @@ function GeneralForm({
render={({ field }) => (
<TextField
{...field}
label="描述"
label={t('common.description')}
fullWidth
multiline
rows={3}
placeholder="请输入知识库描述..."
placeholder={t('knowledge.descriptionPlaceholder')}
/>
)}
/>
@@ -173,10 +178,10 @@ function GeneralForm({
control={control}
render={({ field }) => (
<FormControl fullWidth>
<InputLabel></InputLabel>
<Select {...field} label="权限设置">
<MenuItem value="me"></MenuItem>
<MenuItem value="team"></MenuItem>
<InputLabel>{t('knowledge.permissionSettings')}</InputLabel>
<Select {...field} label={t('knowledge.permissionSettings')}>
<MenuItem value="me">{t('knowledge.onlyMe')}</MenuItem>
<MenuItem value="team">{t('knowledge.teamMembers')}</MenuItem>
</Select>
</FormControl>
)}
@@ -195,7 +200,7 @@ function GeneralForm({
onClick={onCancel}
disabled={isSubmitting}
>
{cancelButtonText}
{defaultCancelButtonText}
</Button>
)}
<Button
@@ -203,7 +208,7 @@ function GeneralForm({
onClick={form ? form.handleSubmit(onSubmit) : undefined}
disabled={isSubmitting || !form}
>
{isSubmitting ? '提交中...' : submitButtonText}
{isSubmitting ? t('common.submitting') : defaultSubmitButtonText}
</Button>
</Box>
)}

View File

@@ -1,4 +1,5 @@
import React, { useMemo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import {
ReactFlow,
type Node,
@@ -39,6 +40,7 @@ const getNodeColor = (entityType?: string): string => {
// 自定义节点组件
const CustomNode = ({ data }: { data: any }) => {
const { t } = useTranslation();
const nodeColor = getNodeColor(data.entity_type);
const nodeSize = Math.max(80, Math.min(140, (data.pagerank || 0.1) * 500));
@@ -55,19 +57,19 @@ const CustomNode = ({ data }: { data: any }) => {
const tooltipContent = (
<Box sx={{ p: 1, maxWidth: 300 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', mb: 1 }}>
{data.label || data.name || 'Unknown'}
{data.label || data.name || t('knowledge.unknown')}
</Typography>
<Typography variant="body2" sx={{ mb: 0.5 }}>
<strong>:</strong> {data.entity_type || 'Unknown'}
<strong>{t('knowledge.type')}:</strong> {data.entity_type || t('knowledge.unknown')}
</Typography>
{data.description && (
<Typography variant="body2">
<strong>:</strong> {data.description}
<strong>{t('knowledge.description')}:</strong> {data.description}
</Typography>
)}
{data.pagerank && (
<Typography variant="caption" sx={{ display: 'block', mt: 1 }}>
PageRank: {data.pagerank.toFixed(4)}
{t('knowledge.pageRank')}: {data.pagerank.toFixed(4)}
</Typography>
)}
</Box>
@@ -127,24 +129,24 @@ const CustomNode = ({ data }: { data: any }) => {
}}
>
<Typography
variant="caption"
sx={{
color: '#fff',
fontWeight: 'bold',
fontSize: Math.max(10, nodeSize * 0.08),
textAlign: 'center',
lineHeight: 1.1,
wordBreak: 'break-word',
textShadow: '1px 1px 2px rgba(0,0,0,0.7)',
maxWidth: '90%',
overflow: 'hidden',
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
}}
>
{data.label || data.name || 'Unknown'}
</Typography>
variant="caption"
sx={{
color: '#fff',
fontWeight: 'bold',
fontSize: Math.max(10, nodeSize * 0.08),
textAlign: 'center',
lineHeight: 1.1,
wordBreak: 'break-word',
textShadow: '1px 1px 2px rgba(0,0,0,0.7)',
maxWidth: '90%',
overflow: 'hidden',
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
}}
>
{data.label || data.name || t('knowledge.unknown')}
</Typography>
</CardContent>
</Card>
</Tooltip>
@@ -156,6 +158,8 @@ const nodeTypes = {
};
const KnowledgeGraphView: React.FC<KnowledgeGraphProps> = ({ knowledgeGraph }) => {
const { t } = useTranslation();
// 转换数据格式为 React Flow 所需的格式
const { nodes: initialNodes, edges: initialEdges } = useMemo(() => {
const graphData = knowledgeGraph?.graph || {};
@@ -166,9 +170,6 @@ const KnowledgeGraphView: React.FC<KnowledgeGraphProps> = ({ knowledgeGraph }) =
// 转换节点数据
// @ts-ignore
const nodes: Node[] = graphData.nodes.map((node, index) => {
console.log(`节点 ${index}:`, node);
console.log(`节点ID: ${node.id}`);
const encodeId = encodeURIComponent(String(node.id));
const n: Node = {
@@ -192,11 +193,11 @@ const KnowledgeGraphView: React.FC<KnowledgeGraphProps> = ({ knowledgeGraph }) =
// 转换边数据
// @ts-ignore
const edges: Edge[] = graphData.edges.map((edge, index) => {
console.log(` ${index}:`, edge);
console.log(`${t('knowledge.edge')} ${index}:`, edge);
console.log(`src_id: ${edge.src_id}, tgt_id: ${edge.tgt_id}`);
// 检查source和target是否存在
if (!edge.src_id || !edge.tgt_id) {
console.warn(`${index} 缺少src_id或tgt_id:`, edge);
console.warn(`${t('knowledge.edge')} ${index} ${t('knowledge.missingIds')}:`, edge);
return null;
}
@@ -205,7 +206,7 @@ const KnowledgeGraphView: React.FC<KnowledgeGraphProps> = ({ knowledgeGraph }) =
const targetExists = nodes.some(node => node.id === encodeURIComponent(edge.tgt_id));
if (!sourceExists || !targetExists) {
console.warn(`${index} 的节点不存在: source=${sourceExists}, target=${targetExists}`, edge);
console.warn(`${t('knowledge.edge')} ${index} ${t('knowledge.nodeNotExists')}: source=${sourceExists}, target=${targetExists}`, edge);
return null;
}
@@ -261,7 +262,7 @@ const KnowledgeGraphView: React.FC<KnowledgeGraphProps> = ({ knowledgeGraph }) =
if (!knowledgeGraph || initialNodes.length === 0) {
return (
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', alignItems: 'center', height: 400 }}>
<Alert severity="info"></Alert>
<Alert severity="info">{t('knowledge.noGraphData')}</Alert>
</Box>
);
}
@@ -295,7 +296,7 @@ const KnowledgeGraphView: React.FC<KnowledgeGraphProps> = ({ knowledgeGraph }) =
boxShadow: '0 2px 8px rgba(0,0,0,0.1)'
}}>
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 'bold' }}>
{t('knowledge.legend')}
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
{['PERSON', 'ORGANIZATION', 'CATEGORY', 'TECHNOLOGY'].map((type) => (
@@ -325,17 +326,17 @@ const KnowledgeGraphView: React.FC<KnowledgeGraphProps> = ({ knowledgeGraph }) =
boxShadow: '0 2px 8px rgba(0,0,0,0.1)'
}}>
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 'bold' }}>
{t('knowledge.graphStats')}
</Typography>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
<Chip
label={`节点: ${nodes.length}`}
label={t('knowledge.nodeCount', { count: nodes.length })}
size="small"
color="primary"
variant="outlined"
/>
<Chip
label={`边: ${edges.length}`}
label={t('knowledge.edgeCount', { count: edges.length })}
size="small"
color="secondary"
variant="outlined"

View File

@@ -1,4 +1,5 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
Card,
CardContent,
@@ -23,6 +24,8 @@ const formatFileSize = (bytes: number): string => {
};
const KnowledgeInfoCard: React.FC<KnowledgeInfoCardProps> = ({ knowledgeBase }) => {
const { t } = useTranslation();
return (
<Card sx={{ mb: 3 }}>
<CardContent>
@@ -32,34 +35,34 @@ const KnowledgeInfoCard: React.FC<KnowledgeInfoCardProps> = ({ knowledgeBase })
{knowledgeBase.name}
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mb: 2 }}>
{knowledgeBase.description || '暂无描述'}
{knowledgeBase.description || t('knowledge.noDescription')}
</Typography>
<Stack direction="row" spacing={2} flexWrap="wrap">
<Chip label={`${knowledgeBase.doc_num || 0} 个文件`} variant="outlined" />
<Chip label={`${knowledgeBase.chunk_num || 0} 个分块`} variant="outlined" />
<Chip label={`${knowledgeBase.token_num || 0} 个令牌`} variant="outlined" />
<Chip label={`大小: ${formatFileSize(knowledgeBase.size || 0)}`} variant="outlined" />
<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" />
</Stack>
</Grid>
<Grid size={{ xs: 12, md: 4 }}>
<Stack spacing={1} alignItems="flex-end">
<Typography variant="body2" color="text.secondary">
: {dayjs(knowledgeBase.create_time).format('YYYY-MM-DD HH:mm:ss')}
{t('knowledge.createTime')}: {dayjs(knowledgeBase.create_time).format('YYYY-MM-DD HH:mm:ss')}
</Typography>
<Typography variant="body2" color="text.secondary">
: {dayjs(knowledgeBase.update_time).format('YYYY-MM-DD HH:mm:ss')}
{t('knowledge.updateTime')}: {dayjs(knowledgeBase.update_time).format('YYYY-MM-DD HH:mm:ss')}
</Typography>
<Typography variant="body2" color="text.secondary">
: {knowledgeBase.language || 'English'}
{t('knowledge.language')}: {knowledgeBase.language || 'English'}
</Typography>
<Typography variant="body2" color="text.secondary">
: {knowledgeBase.permission}
{t('knowledge.permission')}: {knowledgeBase.permission}
</Typography>
<Typography variant="body2" color="text.secondary">
: {knowledgeBase.embd_id}
{t('knowledge.embeddingModel')}: {knowledgeBase.embd_id}
</Typography>
<Typography variant="body2" color="text.secondary">
: {knowledgeBase.parser_id}
{t('knowledge.parser')}: {knowledgeBase.parser_id}
</Typography>
</Stack>
</Grid>

View File

@@ -1,4 +1,5 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
Box,
Paper,
@@ -31,11 +32,13 @@ interface TestChunkResultProps {
function TestChunkResult(props: TestChunkResultProps) {
const { result, loading, page, pageSize, onDocumentFilter, selectedDocIds } = props;
const { t } = useTranslation();
if (!result) {
return (
<Paper sx={{ p: 3, textAlign: 'center' }}>
<Typography variant="h6" color="text.secondary">
"开始测试"
{t('knowledge.testPrompt')}
</Typography>
</Paper>
);
@@ -55,7 +58,7 @@ function TestChunkResult(props: TestChunkResultProps) {
{/* 测试结果概览 */}
<Paper sx={{ p: 3, mb: 3 }}>
<Typography variant="h6" gutterBottom>
{t('knowledge.testResultOverview')}
</Typography>
<Grid container spacing={2}>
<Grid size={{ xs: 12, sm: 4 }}>
@@ -65,7 +68,7 @@ function TestChunkResult(props: TestChunkResultProps) {
{result.total}
</Typography>
<Typography variant="body2" color="text.secondary">
{t('knowledge.matchedChunks')}
</Typography>
</CardContent>
</Card>
@@ -77,7 +80,7 @@ function TestChunkResult(props: TestChunkResultProps) {
{result.doc_aggs?.length || 0}
</Typography>
<Typography variant="body2" color="text.secondary">
{t('knowledge.relatedDocuments')}
</Typography>
</CardContent>
</Card>
@@ -89,7 +92,7 @@ function TestChunkResult(props: TestChunkResultProps) {
{result.chunks?.length || 0}
</Typography>
<Typography variant="body2" color="text.secondary">
{t('knowledge.returnedChunks')}
</Typography>
</CardContent>
</Card>
@@ -102,15 +105,15 @@ function TestChunkResult(props: TestChunkResultProps) {
<Paper sx={{ p: 3, mb: 3 }}>
<Typography variant="h6" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<FilterListIcon />
{t('knowledge.documentFilter')}
</Typography>
<FormControl fullWidth>
<InputLabel></InputLabel>
<InputLabel>{t('knowledge.selectDocuments')}</InputLabel>
<Select
multiple
value={selectedDocIds}
onChange={handleDocumentFilterChange}
input={<OutlinedInput label="选择要显示的文档" />}
input={<OutlinedInput label={t('knowledge.selectDocuments')} />}
renderValue={(selected) => (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{selected.map((value) => {
@@ -127,7 +130,7 @@ function TestChunkResult(props: TestChunkResultProps) {
<Checkbox checked={selectedDocIds.indexOf(doc.doc_id) > -1} />
<ListItemText
primary={doc.doc_name}
secondary={`${doc.count} 个匹配块`}
secondary={t('knowledge.matchedChunksCount', { count: doc.count })}
/>
</MenuItem>
))}
@@ -141,10 +144,10 @@ function TestChunkResult(props: TestChunkResultProps) {
<Paper sx={{ p: 3, mb: 3 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h6">
( {page} {totalPages} )
{t('knowledge.matchedChunksTitle', { page, totalPages })}
</Typography>
<Typography variant="body2" color="text.secondary">
{result.total}
{t('knowledge.totalMatchedChunks', { total: result.total })}
</Typography>
</Box>
@@ -159,20 +162,20 @@ function TestChunkResult(props: TestChunkResultProps) {
</Typography>
<Stack direction="row" spacing={1}>
<Chip
label={`相似度: ${(chunk.similarity * 100).toFixed(1)}%`}
label={t('knowledge.similarity', { value: (chunk.similarity * 100).toFixed(1) })}
size="small"
color="primary"
/>
{chunk.vector_similarity !== undefined && (
<Chip
label={`向量: ${(chunk.vector_similarity * 100).toFixed(1)}%`}
label={t('knowledge.vectorSimilarity', { value: (chunk.vector_similarity * 100).toFixed(1) })}
size="small"
variant="outlined"
/>
)}
{chunk.term_similarity !== undefined && (
<Chip
label={`词项: ${(chunk.term_similarity * 100).toFixed(1)}%`}
label={t('knowledge.termSimilarity', { value: (chunk.term_similarity * 100).toFixed(1) })}
size="small"
variant="outlined"
/>
@@ -191,14 +194,14 @@ function TestChunkResult(props: TestChunkResultProps) {
}
}}
dangerouslySetInnerHTML={{
__html: chunk.content_with_weight || chunk.content_ltks || '无内容'
__html: chunk.content_with_weight || chunk.content_ltks || t('knowledge.noContent')
}}
/>
{chunk.important_kwd && chunk.important_kwd.length > 0 && (
<Box sx={{ mt: 2 }}>
<Typography variant="caption" color="text.secondary" sx={{ mb: 1, display: 'block' }}>
:
{t('knowledge.keywords')}:
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{chunk.important_kwd.map((keyword, kwdIndex) => (
@@ -225,7 +228,7 @@ function TestChunkResult(props: TestChunkResultProps) {
{result.doc_aggs && result.doc_aggs.length > 0 && (
<Paper sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom>
{t('knowledge.relatedDocumentStats')}
</Typography>
<Stack spacing={1}>
{result.doc_aggs.map((doc: ITestingDocument, index: number) => (
@@ -234,7 +237,7 @@ function TestChunkResult(props: TestChunkResultProps) {
{doc.doc_name}
</Typography>
<Chip
label={`${doc.count} 个匹配块`}
label={t('knowledge.matchedChunksCount', { count: doc.count })}
size="small"
color="default"
/>

View File

@@ -12,6 +12,8 @@ import {
} from '@mui/material';
import { Shuffle as ShuffleIcon } from '@mui/icons-material';
import { useFormContext, Controller } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { t as translate } from 'i18next';
import { DOCUMENT_PARSER_TYPES, LLM_MODEL_TYPES, type LlmModelType } from '@/constants/knowledge';
import { useSelectChunkMethodList } from '../hooks';
import { useEmbeddingModelOptions, useLlmOptionsByModelType } from '@/hooks/llm-hooks';
@@ -28,43 +30,48 @@ import {
// 解析器选项配置
const PARSER_OPTIONS = [
{ value: DOCUMENT_PARSER_TYPES.Naive, label: 'General', description: '通用解析器' },
{ value: DOCUMENT_PARSER_TYPES.Qa, label: 'Q&A', description: 'Q&A解析器' },
{ value: DOCUMENT_PARSER_TYPES.Resume, label: 'Resume', description: 'Resume解析器' },
{ value: DOCUMENT_PARSER_TYPES.Manual, label: 'Manual', description: 'Manual解析器' },
{ value: DOCUMENT_PARSER_TYPES.Table, label: 'Table', description: 'Table解析器' },
{ value: DOCUMENT_PARSER_TYPES.Paper, label: 'Paper', description: 'Paper解析器' },
{ value: DOCUMENT_PARSER_TYPES.Book, label: 'Book', description: 'Book解析器' },
{ value: DOCUMENT_PARSER_TYPES.Laws, label: 'Laws', description: 'Laws解析器' },
{ value: DOCUMENT_PARSER_TYPES.Presentation, label: 'Presentation', description: 'Presentation解析器' },
{ value: DOCUMENT_PARSER_TYPES.One, label: 'One', description: 'One解析器' },
{ value: DOCUMENT_PARSER_TYPES.Tag, label: 'Tag', description: 'Tag解析器' },
{ value: DOCUMENT_PARSER_TYPES.Naive, label: 'General', description: translate('knowledge.config.parser.general') },
{ value: DOCUMENT_PARSER_TYPES.Qa, label: 'Q&A', description: translate('knowledge.config.parser.qa') },
{ value: DOCUMENT_PARSER_TYPES.Resume, label: 'Resume', description: translate('knowledge.config.parser.resume') },
{ value: DOCUMENT_PARSER_TYPES.Manual, label: 'Manual', description: translate('knowledge.config.parser.manual') },
{ value: DOCUMENT_PARSER_TYPES.Table, label: 'Table', description: translate('knowledge.config.parser.table') },
{ value: DOCUMENT_PARSER_TYPES.Paper, label: 'Paper', description: translate('knowledge.config.parser.paper') },
{ value: DOCUMENT_PARSER_TYPES.Book, label: 'Book', description: translate('knowledge.config.parser.book') },
{ value: DOCUMENT_PARSER_TYPES.Laws, label: 'Laws', description: translate('knowledge.config.parser.laws') },
{ value: DOCUMENT_PARSER_TYPES.Presentation, label: 'Presentation', description: translate('knowledge.config.parser.presentation') },
{ value: DOCUMENT_PARSER_TYPES.One, label: 'One', description: translate('knowledge.config.parser.one') },
{ value: DOCUMENT_PARSER_TYPES.Tag, label: 'Tag', description: translate('knowledge.config.parser.tag') },
];
export function ChunkMethodItem() {
const { control, formState: { errors } } = useFormContext();
const parserIds = useSelectChunkMethodList();
const parserOptions = parserIds.map((x) => ({
value: x,
label: PARSER_OPTIONS.find((y) => y.value === x)?.label || x,
description: PARSER_OPTIONS.find((y) => y.value === x)?.description || x,
}));
const {t} = useTranslation();
const parserOptions = parserIds.map((x) => {
const parserOption = PARSER_OPTIONS.find((y) => y.value === x);
return {
value: x,
label: parserOption?.label || x,
description: parserOption?.description || x,
};
});
return (
<Box>
<Typography variant="h6" gutterBottom>
{t('knowledge.config.chunkMethod')}
</Typography>
<Controller
name="parser_id"
control={control}
render={({ field }) => (
<FormControl fullWidth error={!!errors.parser_id}>
<InputLabel></InputLabel>
<InputLabel>{t('knowledge.config.selectChunkMethod')}</InputLabel>
<Select
{...field}
label="选择切片方法"
label={t('knowledge.config.selectChunkMethod')}
>
{parserOptions.map((option) => (
<MenuItem key={option.value} value={option.value}>
@@ -91,10 +98,11 @@ export function ChunkMethodItem() {
// 分块token数量配置
export function ChunkTokenNumberItem() {
const { t } = useTranslation();
return (
<SliderInputFormField
name="parser_config.chunk_token_num"
label="建议文本块大小"
label={t('knowledge.config.chunkTokenSize')}
min={64}
max={2048}
step={64}
@@ -106,49 +114,53 @@ export function ChunkTokenNumberItem() {
// 页面排名配置
export function PageRankItem() {
const { t } = useTranslation();
return (
<NumberInputFormField
name="parser_config.page_rank"
label="页面排名"
label={t('knowledge.config.pageRank')}
defaultValue={0}
min={0}
placeholder="输入页面排名"
placeholder={t('knowledge.config.enterPageRank')}
/>
);
}
// 自动关键词数量配置
export function AutoKeywordsItem() {
const { t } = useTranslation();
return (
<NumberInputFormField
name="parser_config.auto_keywords"
label="自动关键词"
label={t('knowledge.config.autoKeywords')}
defaultValue={0}
min={0}
placeholder="输入关键词数量"
placeholder={t('knowledge.config.enterKeywordCount')}
/>
);
}
// 自动问题数量配置
export function AutoQuestionsItem() {
const { t } = useTranslation();
return (
<NumberInputFormField
name="parser_config.auto_questions"
label="自动问题"
label={t('knowledge.config.autoQuestions')}
defaultValue={0}
min={0}
placeholder="输入问题数量"
placeholder={t('knowledge.config.enterQuestionCount')}
/>
);
}
// 表格转HTML开关
export function HtmlForExcelItem() {
const { t } = useTranslation();
return (
<SwitchFormField
name="parser_config.html4excel"
label="表格转HTML"
label={t('knowledge.config.htmlForExcel')}
defaultValue={false}
/>
);
@@ -156,15 +168,16 @@ export function HtmlForExcelItem() {
// 标签集选择
export function TagsItem() {
const { t } = useTranslation();
const tagsOptions: SelectOption[] = [
{ value: '', label: '请选择' },
{ value: '', label: t('common.pleaseSelect') },
// 这里可以根据实际需求添加标签选项
];
return (
<SelectFormField
name="parser_config.tags"
label="标签集"
label={t('knowledge.config.tags')}
options={tagsOptions}
defaultValue=""
displayEmpty
@@ -174,10 +187,11 @@ export function TagsItem() {
// RAPTOR策略开关
export function UseRaptorItem() {
const { t } = useTranslation();
return (
<SwitchFormField
name="parser_config.raptor.use_raptor"
label="使用召回增强RAPTOR策略"
label={t('knowledge.config.useRaptorStrategy')}
defaultValue={false}
/>
);
@@ -185,11 +199,12 @@ export function UseRaptorItem() {
// RAPTOR提示词配置
export function RaptorPromptItem() {
const { t } = useTranslation();
return (
<MultilineTextFormField
name="parser_config.raptor.prompt"
label="提示词"
defaultValue="请总结以下段落。小心数字,不要编造。段落如下:\n{cluster_content}\n以上就是你需要总结的内容。"
label={t('knowledge.config.prompt')}
defaultValue={t('knowledge.config.raptorPromptDefault')}
rows={4}
/>
);
@@ -197,10 +212,11 @@ export function RaptorPromptItem() {
// RAPTOR最大token数配置
export function RaptorMaxTokenItem() {
const { t } = useTranslation();
return (
<SliderInputFormField
name="parser_config.raptor.max_token"
label="最大token数"
label={t('knowledge.config.maxTokens')}
min={64}
max={512}
step={32}
@@ -212,10 +228,11 @@ export function RaptorMaxTokenItem() {
// RAPTOR阈值配置
export function RaptorThresholdItem() {
const { t } = useTranslation();
return (
<SliderInputFormField
name="parser_config.raptor.threshold"
label="阈值"
label={t('knowledge.config.threshold')}
min={0}
max={1}
step={0.1}
@@ -227,10 +244,11 @@ export function RaptorThresholdItem() {
// RAPTOR最大聚类数配置
export function RaptorMaxClusterItem() {
const { t } = useTranslation();
return (
<SliderInputFormField
name="parser_config.raptor.max_cluster"
label="最大聚类数"
label={t('knowledge.config.maxClusterCount')}
min={16}
max={128}
step={16}
@@ -242,10 +260,11 @@ export function RaptorMaxClusterItem() {
// RAPTOR随机种子配置
export function RaptorRandomSeedItem() {
const { t } = useTranslation();
return (
<NumberInputFormField
name="parser_config.raptor.random_seed"
label="随机种子"
label={t('knowledge.config.randomSeed')}
defaultValue={0}
showRandomButton
/>
@@ -254,10 +273,11 @@ export function RaptorRandomSeedItem() {
// 知识图谱开关
export function UseGraphragItem() {
const { t } = useTranslation();
return (
<SwitchFormField
name="parser_config.graphrag.use_graphrag"
label="提取知识图谱"
label={t('knowledge.config.extractKnowledgeGraph')}
defaultValue={false}
/>
);
@@ -265,10 +285,11 @@ export function UseGraphragItem() {
// 实体类型配置
export function EntityTypesItem() {
const { t } = useTranslation();
return (
<ChipListFormField
name="parser_config.graphrag.entity_types"
label="*实体类型"
label={t('knowledge.config.entityTypes')}
defaultValue={['organization', 'person', 'geo', 'event', 'category']}
required
allowAdd
@@ -280,6 +301,7 @@ export function EntityTypesItem() {
// GraphRAG方法选择
export function GraphragMethodItem() {
const { t } = useTranslation();
const methodOptions: SelectOption[] = [
{ value: 'Light', label: 'Light' },
{ value: 'General', label: 'General' },
@@ -288,7 +310,7 @@ export function GraphragMethodItem() {
return (
<SelectFormField
name="parser_config.graphrag.method"
label="方法"
label={t('knowledge.config.method')}
options={methodOptions}
defaultValue="Light"
displayEmpty={false}
@@ -298,10 +320,11 @@ export function GraphragMethodItem() {
// 实体归一化开关
export function EntityNormalizeItem() {
const { t } = useTranslation();
return (
<SwitchFormField
name="parser_config.graphrag.entity_normalize"
label="实体归一化"
label={t('knowledge.config.entityNormalization')}
defaultValue={false}
/>
);
@@ -309,10 +332,11 @@ export function EntityNormalizeItem() {
// 社区报告生成开关
export function CommunityReportItem() {
const { t } = useTranslation();
return (
<SwitchFormField
name="parser_config.graphrag.community_report"
label="社区报告生成"
label={t('knowledge.config.communityReportGeneration')}
defaultValue={false}
/>
);
@@ -320,12 +344,13 @@ export function CommunityReportItem() {
export function EmbeddingModelItem() {
const { control } = useFormContext();
const { t } = useTranslation();
const { options } = useEmbeddingModelOptions();
return (
<Box>
<Typography variant="subtitle1" sx={{ minWidth: 120 }}>
{t('knowledge.config.embeddingModel')}
</Typography>
<Box sx={{ flex: 1 }}>
<Controller
@@ -356,13 +381,14 @@ export function EmbeddingModelItem() {
// PDF解析器配置
export function LayoutRecognizeItem() {
const { control, setValue, formState } = useFormContext();
const { t } = useTranslation();
const { getOptionsByModelType } = useLlmOptionsByModelType();
const options = useMemo(() => {
// 基础选项
const basicOptions = [
{ value: 'DeepDOC', label: 'DeepDOC' },
{ value: 'Plain Text', label: '纯文本' }
{ value: 'Plain Text', label: t('knowledge.config.plainText') }
];
// 获取图像转文本模型选项
@@ -372,12 +398,12 @@ export function LayoutRecognizeItem() {
const image2TextSelectOptions = image2TextOptions.flatMap(group =>
group.options.map(option => ({
value: option.value,
label: `${option.label} (实验性)`
label: `${option.label} (${t('knowledge.config.experimental')})`
}))
);
return [...basicOptions, ...image2TextSelectOptions];
}, [getOptionsByModelType]);
}, [getOptionsByModelType, t]);
return (
<Controller
@@ -393,7 +419,7 @@ export function LayoutRecognizeItem() {
return (
<SelectFormField
name="parser_config.layout_recognize"
label="PDF解析器"
label={t('knowledge.config.pdfParser')}
options={options}
defaultValue="DeepDOC"
/>
@@ -405,11 +431,12 @@ export function LayoutRecognizeItem() {
// 文本分段标识符配置
export function DelimiterItem() {
const { t } = useTranslation();
return (
<TextFormField
name="parser_config.delimiter"
label="分隔符"
placeholder="请输入分隔符"
label={t('knowledge.config.delimiter')}
placeholder={t('knowledge.config.enterDelimiter')}
defaultValue="\n!?。;!?"
/>
);

View File

@@ -1,8 +1,11 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { ChunkMethodItem, EmbeddingModelItem } from './common-items';
import { Box, Typography } from '@mui/material';
export function KnowledgeGraphConfiguration() {
const { t } = useTranslation();
return (
<>
<ChunkMethodItem />
@@ -10,25 +13,25 @@ export function KnowledgeGraphConfiguration() {
<Box sx={{ mb: 2 }}>
<Typography variant="body2" color="text.secondary">
PageRank配置 -
{t('knowledge.config.pageRankConfigTodo')}
</Typography>
</Box>
<Box sx={{ mb: 2 }}>
<Typography variant="body2" color="text.secondary">
-
{t('knowledge.config.entityTypeConfigTodo')}
</Typography>
</Box>
<Box sx={{ mb: 2 }}>
<Typography variant="body2" color="text.secondary">
Token数量配置 (最大: 16384) -
{t('knowledge.config.maxTokenConfigTodo')}
</Typography>
</Box>
<Box sx={{ mb: 2 }}>
<Typography variant="body2" color="text.secondary">
-
{t('knowledge.config.delimiterConfigTodo')}
</Typography>
</Box>
</>

View File

@@ -8,6 +8,7 @@ import {
} from '@mui/material';
import { ExpandMore as ExpandMoreIcon } from '@mui/icons-material';
import { useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { ConfigurationFormContainer, MainContainer } from './configuration-form-container';
import {
ChunkMethodItem,
@@ -35,6 +36,7 @@ import {
export function NaiveConfiguration() {
const { formState: { errors } } = useFormContext();
const { t } = useTranslation();
return (
<ConfigurationFormContainer>
@@ -42,7 +44,7 @@ export function NaiveConfiguration() {
{/* 第一部分:基础配置 */}
<Accordion defaultExpanded>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography variant="h6"></Typography>
<Typography variant="h6">{t('knowledge.config.basicConfig')}</Typography>
</AccordionSummary>
<AccordionDetails>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
@@ -67,7 +69,7 @@ export function NaiveConfiguration() {
{/* 第二部分:页面排名和自动提取 */}
<Accordion defaultExpanded>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography variant="h6"></Typography>
<Typography variant="h6">{t('knowledge.config.pageRankAndAutoExtract')}</Typography>
</AccordionSummary>
<AccordionDetails>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
@@ -92,7 +94,7 @@ export function NaiveConfiguration() {
{/* 第三部分RAPTOR策略 */}
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography variant="h6">RAPTOR策略</Typography>
<Typography variant="h6">{t('knowledge.config.raptorStrategy')}</Typography>
</AccordionSummary>
<AccordionDetails>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
@@ -120,7 +122,7 @@ export function NaiveConfiguration() {
{/* 第四部分:知识图谱 */}
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography variant="h6"></Typography>
<Typography variant="h6">{t('knowledge.config.knowledgeGraph')}</Typography>
</AccordionSummary>
<AccordionDetails>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>

View File

@@ -1,9 +1,12 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { ConfigurationFormContainer } from './configuration-form-container';
import { ChunkMethodItem, EmbeddingModelItem } from './common-items';
import { Box, Typography } from '@mui/material';
export function TagConfiguration() {
const { t } = useTranslation();
return (
<ConfigurationFormContainer>
<ChunkMethodItem />
@@ -11,7 +14,7 @@ export function TagConfiguration() {
<Box sx={{ mb: 2 }}>
<Typography variant="body2" color="text.secondary">
PageRank配置 -
{t('knowledge.config.pageRankConfigTodo')}
</Typography>
</Box>
</ConfigurationFormContainer>

View File

@@ -12,12 +12,14 @@ import {
Stepper,
Step,
StepLabel,
CircularProgress,
} from '@mui/material';
import {
ArrowBack as ArrowBackIcon,
CheckCircle as CheckCircleIcon,
SkipNext as SkipNextIcon,
} from '@mui/icons-material';
import { useTranslation } from 'react-i18next';
import { useKnowledgeOperations, useKnowledgeDetail } from '@/hooks/knowledge-hooks';
import GeneralForm from './components/GeneralForm';
import ChunkMethodForm from './components/ChunkMethodForm';
@@ -32,13 +34,14 @@ interface BaseFormData {
avatar?: string;
}
const steps = ['基础信息', '配置设置'];
function KnowledgeBaseCreate() {
const navigate = useNavigate();
const { t } = useTranslation();
const [activeStep, setActiveStep] = useState(0);
const [createdKbId, setCreatedKbId] = useState<string | null>(null);
const steps = [t('knowledgeConfiguration.basicInfo'), t('knowledgeConfiguration.configSettings')];
// 使用知识库操作 hooks
const {
loading: isSubmitting,
@@ -89,8 +92,6 @@ function KnowledgeBaseCreate() {
// 处理表单提交
const handleSubmit = async ({ data }: { data: any }) => {
clearError();
console.log('提交数据:', data);
try {
if (activeStep === 0) {
// 第一步:创建知识库基础信息
@@ -110,7 +111,7 @@ function KnowledgeBaseCreate() {
}, 500);
setActiveStep(1);
showMessage.success('知识库创建成功,请配置解析设置');
showMessage.success(t('knowledgeConfiguration.createSuccess'));
} else {
// 第二步:配置知识库解析设置
if (!createdKbId) return;
@@ -130,23 +131,23 @@ function KnowledgeBaseCreate() {
};
await updateKnowledgeModelConfig(configData);
showMessage.success('知识库配置完成');
showMessage.success(t('knowledgeConfiguration.configComplete'));
navigate('/knowledge');
}
} catch (err) {
console.error('操作失败:', err);
showMessage.error(activeStep === 0 ? '创建知识库失败' : '配置知识库失败');
showMessage.error(activeStep === 0 ? t('knowledgeConfiguration.createFailed') : t('knowledgeConfiguration.configFailed'));
}
};
// 跳过配置,直接完成创建
const handleSkipConfig = async () => {
try {
showMessage.success('知识库创建完成,您可以稍后在设置页面配置解析参数');
showMessage.success(t('knowledgeConfiguration.skipConfigSuccess'));
navigate('/knowledge');
} catch (err) {
console.error('跳过配置失败:', err);
showMessage.error('操作失败');
showMessage.error(t('common.operationFailed'));
}
};
@@ -159,10 +160,10 @@ function KnowledgeBaseCreate() {
onClick={() => navigate('/knowledge')}
sx={{ mr: 2 }}
>
{t('common.back')}
</Button>
<Typography variant="h4" component="h1">
{t('knowledgeList.createKnowledgeBase')}
</Typography>
</Box>
@@ -198,11 +199,11 @@ function KnowledgeBaseCreate() {
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<CheckCircleIcon color="success" sx={{ mr: 1 }} />
<Typography variant="h6">
{t('knowledgeConfiguration.createSuccessConfig')}
</Typography>
</Box>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
{t('knowledgeConfiguration.configLaterTip')}
</Typography>
<Divider sx={{ mb: 3 }} />
<ChunkMethodForm form={form} />
@@ -219,7 +220,7 @@ function KnowledgeBaseCreate() {
variant="outlined"
onClick={() => navigate('/knowledge')}
>
{t('common.cancel')}
</Button>
)}
@@ -231,7 +232,7 @@ function KnowledgeBaseCreate() {
disabled={isSubmitting}
startIcon={<SkipNextIcon />}
>
{t('knowledgeConfiguration.skipConfig')}
</Button>
)}
@@ -239,19 +240,12 @@ function KnowledgeBaseCreate() {
type="submit"
variant="contained"
disabled={isSubmitting}
startIcon={
activeStep === steps.length - 1 ? (
<CheckCircleIcon />
) : (
<SkipNextIcon />
)
}
startIcon={isSubmitting ? <CircularProgress size={20} /> : null}
>
{isSubmitting
? '处理中...'
: activeStep === steps.length - 1
? '完成创建'
: '下一步'}
{isSubmitting
? (activeStep === 0 ? t('knowledgeConfiguration.creating') : t('knowledgeConfiguration.configuring'))
: (activeStep === 0 ? t('knowledgeConfiguration.createAndNext') : t('knowledgeConfiguration.completeCreate'))
}
</Button>
</Box>
</Box>

View File

@@ -1,5 +1,6 @@
import React, { useState, useEffect, useRef } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import {
Box,
Typography,
@@ -38,6 +39,7 @@ import logger from '@/utils/logger';
function KnowledgeBaseDetail() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const { t } = useTranslation();
// 状态管理
const [error, setError] = useState<string | null>(null);
@@ -114,20 +116,20 @@ function KnowledgeBaseDetail() {
refreshFiles();
fetchKnowledgeDetail();
} catch (err: any) {
setError(err.response?.data?.message || err.message || '删除文件失败');
setError(err.response?.data?.message || err.message || t('knowledgeDetails.deleteFileFailed'));
}
};
// 上传文件处理
const handleUploadFiles = async (uploadFiles: File[]) => {
console.log('上传文件:', uploadFiles);
console.log(t('knowledgeDetails.uploadFiles'), uploadFiles);
const kb_id = knowledgeBase?.id || '';
try {
await uploadDocuments(kb_id, uploadFiles);
refreshFiles();
fetchKnowledgeDetail();
} catch (err: any) {
throw new Error(err.response?.data?.message || err.message || '上传文件失败');
throw new Error(err.response?.data?.message || err.message || t('knowledgeDetails.uploadFileFailed'));
}
};
@@ -138,7 +140,7 @@ function KnowledgeBaseDetail() {
refreshFiles(); // 刷新列表
startPolling(); // 开始轮询
} catch (err: any) {
setError(err.response?.data?.message || err.message || '重新解析失败');
setError(err.response?.data?.message || err.message || t('knowledgeDetails.reparseFailed'));
}
};
@@ -148,7 +150,7 @@ function KnowledgeBaseDetail() {
await renameDocument(fileId, newName);
refreshFiles();
} catch (err: any) {
setError(err.response?.data?.message || err.message || '重命名失败');
setError(err.response?.data?.message || err.message || t('knowledgeDetails.renameFailed'));
}
};
@@ -158,7 +160,7 @@ function KnowledgeBaseDetail() {
await changeDocumentStatus(fileIds, status);
refreshFiles();
} catch (err: any) {
setError(err.response?.data?.message || err.message || '更改状态失败');
setError(err.response?.data?.message || err.message || t('knowledgeDetails.changeStatusFailed'));
}
};
@@ -168,20 +170,20 @@ function KnowledgeBaseDetail() {
await cancelRunDocuments(fileIds);
refreshFiles();
} catch (err: any) {
setError(err.response?.data?.message || err.message || '取消运行失败');
setError(err.response?.data?.message || err.message || t('knowledgeDetails.cancelRunFailed'));
}
};
// 查看详情
const handleViewDetails = (file: IKnowledgeFile) => {
console.log("查看详情:", file);
console.log(t('knowledgeDetails.viewDetails'), file);
navigate(`/chunk/parsed-result?kb_id=${id}&doc_id=${file.id}`);
};
// 查看解析详情
const handleViewProcessDetails = (file: IKnowledgeFile) => {
console.log("查看解析详情:", file);
console.log(t('knowledgeDetails.viewProcessDetails'), file);
setSelectedFileDetails(file);
setProcessDetailsDialogOpen(true);
};
@@ -262,7 +264,7 @@ function KnowledgeBaseDetail() {
return (
<Box sx={{ p: 3 }}>
<LinearProgress />
<Typography sx={{ mt: 2 }}>...</Typography>
<Typography sx={{ mt: 2 }}>{t('common.loading')}</Typography>
</Box>
);
}
@@ -281,11 +283,11 @@ function KnowledgeBaseDetail() {
<KnowledgeBreadcrumbs
kbItems={[
{
label: '知识库',
label: t('knowledgeDetails.knowledgeBase'),
path: '/knowledge'
},
{
label: knowledgeBase?.name || '知识库详情',
label: knowledgeBase?.name || t('knowledgeDetails.knowledgeBaseDetail'),
path: `/knowledge/${id}`
}
]}
@@ -302,8 +304,8 @@ function KnowledgeBaseDetail() {
onChange={(event, newValue) => setCurrentTab(newValue)}
sx={{ borderBottom: 1, borderColor: 'divider' }}
>
<Tab label="Documents" />
<Tab label="Graph" />
<Tab label={t('knowledgeDetails.documents')} />
<Tab label={t('knowledgeDetails.graph')} />
</Tabs>
{/* Document List 标签页内容 */}
@@ -365,7 +367,7 @@ function KnowledgeBaseDetail() {
onSearchChange={setSearchKeyword}
onReparse={(fileIds) => handleReparse(fileIds)}
onDelete={(fileIds) => {
console.log('删除文件:', fileIds);
console.log(t('knowledgeDetails.deleteFiles'), fileIds);
setRowSelectionModel({
type: 'include',
ids: new Set(fileIds)
@@ -379,7 +381,7 @@ function KnowledgeBaseDetail() {
}}
rowSelectionModel={rowSelectionModel}
onRowSelectionModelChange={(newModel) => {
console.log('新的选择模型:', newModel);
console.log(t('knowledgeDetails.newSelectionModel'), newModel);
setRowSelectionModel(newModel);
}}
total={total}
@@ -410,7 +412,7 @@ function KnowledgeBaseDetail() {
open={uploadDialogOpen}
onClose={() => setUploadDialogOpen(false)}
onUpload={handleUploadFiles}
title="上传文件到知识库"
title={t('knowledgeDetails.uploadFilesToKnowledge')}
acceptedFileTypes={['.pdf', '.docx', '.txt', '.md', '.png', '.jpg', '.jpeg', '.mp4', '.wav']}
maxFileSize={100}
maxFiles={10}
@@ -418,15 +420,15 @@ function KnowledgeBaseDetail() {
{/* 删除确认对话框 */}
<Dialog open={deleteDialogOpen} onClose={() => setDeleteDialogOpen(false)}>
<DialogTitle></DialogTitle>
<DialogTitle>{t('knowledgeDetails.confirmDelete')}</DialogTitle>
<DialogContent>
<Typography>
{rowSelectionModel.ids.size}
{t('knowledgeDetails.confirmDeleteMessage', { count: rowSelectionModel.ids.size })}
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setDeleteDialogOpen(false)}></Button>
<Button color="error" onClick={handleDeleteFiles}></Button>
<Button onClick={() => setDeleteDialogOpen(false)}>{t('common.cancel')}</Button>
<Button color="error" onClick={handleDeleteFiles}>{t('common.delete')}</Button>
</DialogActions>
</Dialog>
@@ -437,7 +439,7 @@ function KnowledgeBaseDetail() {
maxWidth="md"
fullWidth
>
<DialogTitle></DialogTitle>
<DialogTitle>{t('knowledgeDetails.documentProcessDetails')}</DialogTitle>
<DialogContent sx={{ p: 3 }}>
{selectedFileDetails && (
<Stack spacing={3}>
@@ -445,12 +447,12 @@ function KnowledgeBaseDetail() {
<Card variant="outlined">
<CardContent>
<Typography variant="h6" gutterBottom color="primary">
{t('knowledgeDetails.basicInfo')}
</Typography>
<Stack spacing={2}>
<Box>
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
{t('knowledgeDetails.fileName')}
</Typography>
<Typography variant="body1" sx={{ fontWeight: 500 }}>
{selectedFileDetails.name}
@@ -459,10 +461,10 @@ function KnowledgeBaseDetail() {
<Divider />
<Box>
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
ID
{t('knowledgeDetails.parserId')}
</Typography>
<Typography variant="body1">
{selectedFileDetails.parser_id || '未指定'}
{selectedFileDetails.parser_id || t('knowledgeDetails.notSpecified')}
</Typography>
</Box>
</Stack>
@@ -473,30 +475,30 @@ function KnowledgeBaseDetail() {
<Card variant="outlined">
<CardContent>
<Typography variant="h6" gutterBottom color="primary">
{t('knowledgeDetails.processStatus')}
</Typography>
<Stack spacing={2}>
<Box>
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
{t('knowledgeDetails.startTime')}
</Typography>
<Typography variant="body1">
{selectedFileDetails.process_begin_at || '未开始'}
{selectedFileDetails.process_begin_at || t('knowledgeDetails.notStarted')}
</Typography>
</Box>
<Divider />
<Box>
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
{t('knowledgeDetails.processingTime')}
</Typography>
<Typography variant="body1">
{selectedFileDetails.process_duration ? `${selectedFileDetails.process_duration}` : '未完成'}
{selectedFileDetails.process_duration ? `${selectedFileDetails.process_duration}${t('knowledgeDetails.seconds')}` : t('knowledgeDetails.notCompleted')}
</Typography>
</Box>
<Divider />
<Box>
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
{t('knowledgeDetails.progress')}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Chip
@@ -520,7 +522,7 @@ function KnowledgeBaseDetail() {
<Card variant="outlined">
<CardContent>
<Typography variant="h6" gutterBottom color="primary">
{t('knowledgeDetails.processDetails')}
</Typography>
<Box
sx={{
@@ -556,8 +558,8 @@ function KnowledgeBaseDetail() {
</DialogContent>
<DialogActions sx={{ p: 3, pt: 0 }}>
<Button onClick={() => setProcessDetailsDialogOpen(false)} variant="contained">
</Button>
{t('common.close')}
</Button>
</DialogActions>
</Dialog>
</Box>

View File

@@ -25,8 +25,12 @@ import KnowledgeGridView from '@/components/knowledge/KnowledgeGridView';
import type { IKnowledge } from '@/interfaces/database/knowledge';
import { useDialog } from '@/hooks/useDialog';
import logger from '@/utils/logger';
import { useTranslation } from 'react-i18next';
const KnowledgeBaseList: React.FC = () => {
const {t} = useTranslation();
const navigate = useNavigate();
const { deleteKnowledge } = useKnowledgeOperations();
@@ -95,8 +99,8 @@ const KnowledgeBaseList: React.FC = () => {
const handleDeleteKnowledge = useCallback(async (kb: IKnowledge) => {
// 需要确认删除
dialog.confirm({
title: '确认删除',
content: `是否确认删除知识库 ${kb.name}`,
title: t('common.deleteModalTitle'),
content: `${t('knowledgeList.confirmDeleteKnowledge')} ${kb.name}`,
onConfirm: async () => {
try {
await deleteKnowledge(kb.id);
@@ -128,7 +132,7 @@ const KnowledgeBaseList: React.FC = () => {
// 构建团队筛选选项
const teamFilterOptions = useMemo(() => {
const options = [
{ value: 'all', label: '全部' },
{ value: 'all', label: t('common.all') },
];
// 添加租户选项
@@ -149,7 +153,7 @@ const KnowledgeBaseList: React.FC = () => {
{/* 页面标题 */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
<Typography variant="h4" fontWeight={600}>
{t('header.knowledgeBase')}
</Typography>
<Button
variant="contained"
@@ -157,14 +161,14 @@ const KnowledgeBaseList: React.FC = () => {
onClick={handleCreateKnowledge}
sx={{ borderRadius: 2 }}
>
{t('knowledgeList.createKnowledgeBase')}
</Button>
</Box>
{/* 搜索和筛选区域 */}
<Box sx={{ display: 'flex', gap: 2, mb: 3, alignItems: 'center' }}>
<TextField
placeholder="搜索知识库..."
placeholder={t('knowledgeList.searchKnowledgePlaceholder')}
value={searchTerm}
onChange={(e) => handleSearch(e.target.value)}
sx={{ flex: 1, maxWidth: 400 }}
@@ -178,10 +182,10 @@ const KnowledgeBaseList: React.FC = () => {
/>
<FormControl sx={{ minWidth: 120 }}>
<InputLabel></InputLabel>
<InputLabel>{t('knowledgeList.teamFilter')}</InputLabel>
<Select
value={teamFilter.length > 0 ? teamFilter[0] : 'all'}
label="团队筛选"
label={t('knowledgeList.teamFilter')}
defaultValue={'all'}
onChange={(e) => {
const value = e.target.value as string;
@@ -202,7 +206,7 @@ const KnowledgeBaseList: React.FC = () => {
onClick={handleRefresh}
disabled={loading}
>
{t('common.refresh')}
</Button>
</Box>
@@ -210,7 +214,7 @@ const KnowledgeBaseList: React.FC = () => {
{error && (
<Box sx={{ mb: 3, p: 2, bgcolor: 'error.light', borderRadius: 1 }}>
<Typography color="error">
: {error}
{t('knowledgeList.loadError')}: {error}
</Typography>
</Box>
)}
@@ -249,7 +253,11 @@ const KnowledgeBaseList: React.FC = () => {
showLastButton
/>
<Typography variant="body2" color="text.secondary" textAlign="center">
{knowledgeBases.length} {currentPage} {totalPages}
{t('knowledgeList.paginationInfo', {
total: knowledgeBases.length,
current: currentPage,
totalPages: totalPages
})}
</Typography>
</Stack>
</Box>

View File

@@ -2,6 +2,7 @@
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useForm, FormProvider, useWatch, Form } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import {
Box,
Typography,
@@ -33,6 +34,7 @@ interface BaseFormData {
function KnowledgeBaseSetting() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const { t } = useTranslation();
const [tabValue, setTabValue] = useState<'generalForm' | 'chunkMethodForm'>('generalForm');
// 获取知识库详情
@@ -103,7 +105,7 @@ function KnowledgeBaseSetting() {
const handleSubmit = async ({data}: {data: any}) => {
if (!knowledge) return;
console.log('提交数据:', data);
console.log(t('knowledgeSettings.submitData'), data);
try {
// 分别处理基础信息和配置信息
@@ -118,7 +120,7 @@ function KnowledgeBaseSetting() {
} as any;
await updateKnowledgeBasicInfo(basicData);
showMessage.success('基础信息更新成功');
showMessage.success(t('knowledgeSettings.basicInfoUpdateSuccess'));
} else {
const configData = {
kb_id: knowledge.id,
@@ -132,13 +134,15 @@ function KnowledgeBaseSetting() {
};
await updateKnowledgeModelConfig(configData);
showMessage.success('解析配置更新成功');
showMessage.success(t('knowledgeSettings.parseConfigUpdateSuccess'));
}
// 刷新知识库详情
refresh();
} catch (error) {
showMessage.error(`${tabValue === 'generalForm' ? '基础信息' : '解析配置'}更新失败`);
showMessage.error(t('knowledgeSettings.updateFailed', {
type: tabValue === 'generalForm' ? t('knowledgeSettings.basicInfo') : t('knowledgeSettings.parseConfig')
}));
}
};
@@ -149,7 +153,7 @@ function KnowledgeBaseSetting() {
if (detailLoading) {
return (
<MainContainer>
<Typography>...</Typography>
<Typography>{t('common.loading')}</Typography>
</MainContainer>
);
}
@@ -160,22 +164,22 @@ function KnowledgeBaseSetting() {
<KnowledgeBreadcrumbs
kbItems={[
{
label: '知识库',
label: t('knowledgeSettings.knowledgeBase'),
path: '/knowledge'
},
{
label: knowledge?.name || '知识库详情',
label: knowledge?.name || t('knowledgeSettings.knowledgeBaseDetail'),
path: `/knowledge/${id}`
},
{
label: '设置'
label: t('knowledgeSettings.settings')
}
]}
/>
<Box sx={{ mb: 3 }}>
<Typography variant="h4" gutterBottom>
{t('knowledgeSettings.knowledgeBaseSettings')}
</Typography>
<Typography variant="subtitle1" color="text.secondary">
{knowledge?.name}
@@ -185,9 +189,9 @@ function KnowledgeBaseSetting() {
<FormProvider {...form}>
<Form onSubmit={handleSubmit}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={tabValue} onChange={handleTabChange} aria-label="设置选项卡">
<Tab label="基础信息" value="generalForm" />
<Tab label="解析配置" value="chunkMethodForm" />
<Tabs value={tabValue} onChange={handleTabChange} aria-label={t('knowledgeSettings.settingsTabs')}>
<Tab label={t('knowledgeSettings.basicInfo')} value="generalForm" />
<Tab label={t('knowledgeSettings.parseConfig')} value="chunkMethodForm" />
</Tabs>
</Box>
@@ -202,7 +206,7 @@ function KnowledgeBaseSetting() {
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end' }}>
<Button type="submit" variant="contained">
{t('common.save')}
</Button>
</Box>
</Form>
@@ -211,7 +215,7 @@ function KnowledgeBaseSetting() {
{/* 返回按钮 */}
<Fab
color="primary"
aria-label="返回知识库详情"
aria-label={t('knowledgeSettings.backToKnowledgeDetail')}
onClick={handleNavigateBack}
sx={{
position: 'fixed',

View File

@@ -1,6 +1,7 @@
import React, { useState, useMemo } from 'react';
import React, { useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useForm, Controller } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import {
Box,
Container,
@@ -31,27 +32,21 @@ import type { INextTestingResult } from '@/interfaces/database/knowledge';
import KnowledgeBreadcrumbs from './components/KnowledgeBreadcrumbs';
import TestChunkResult from './components/TestChunkResult';
import { useSnackbar } from '@/components/Provider/SnackbarProvider';
import { useTranslation } from 'react-i18next';
import { toLower } from 'lodash';
import { t } from 'i18next';
// 语言选项常量
const Languages = [
'English',
'Chinese',
'Spanish',
'French',
'German',
'Japanese',
'Korean',
'Vietnamese',
const options = [
{ value: 'en', label: t('knowledgeTesting.languages.english') },
{ value: 'zh', label: t('knowledgeTesting.languages.chinese') },
{ value: 'ja', label: t('knowledgeTesting.languages.japanese') },
{ value: 'ko', label: t('knowledgeTesting.languages.korean') },
{ value: 'fr', label: t('knowledgeTesting.languages.french') },
{ value: 'de', label: t('knowledgeTesting.languages.german') },
{ value: 'es', label: t('knowledgeTesting.languages.spanish') },
{ value: 'vi', label: t('knowledgeTesting.languages.vietnamese') },
];
const options = Languages.map((x) => ({
label: t('language.' + toLower(x)),
value: x,
}));
// 表单数据接口
interface TestFormData {
question: string;
@@ -65,9 +60,9 @@ interface TestFormData {
}
function KnowledgeBaseTesting() {
const { t } = useTranslation();
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const { t } = useTranslation();
// 状态管理
const [testResult, setTestResult] = useState<INextTestingResult | null>(null);
@@ -151,12 +146,12 @@ function KnowledgeBaseTesting() {
if (response.data.code === 0) {
setTestResult(response.data.data);
setPage(1); // 重置到第一页
showMessage.success('检索测试完成');
showMessage.success(t('knowledgeTesting.retrievalTestComplete'));
} else {
throw new Error(response.data.message || '检索测试失败');
throw new Error(response.data.message || t('knowledgeTesting.retrievalTestFailed'));
}
} catch (error: any) {
showMessage.error(error.message || '检索测试失败');
showMessage.error(error.message || t('knowledgeTesting.retrievalTestFailed'));
} finally {
setTesting(false);
}
@@ -203,10 +198,10 @@ function KnowledgeBaseTesting() {
if (response.data.code === 0) {
setTestResult(response.data.data);
} else {
throw new Error(response.data.message || '分页请求失败');
throw new Error(response.data.message || t('knowledgeTesting.paginationRequestFailed'));
}
} catch (error: any) {
showMessage.error(error.message || '分页请求失败');
showMessage.error(error.message || t('knowledgeTesting.paginationRequestFailed'));
} finally {
setTesting(false);
}
@@ -227,7 +222,7 @@ function KnowledgeBaseTesting() {
if (detailLoading) {
return (
<Container maxWidth="lg" sx={{ py: 4 }}>
<Typography>...</Typography>
<Typography>{t('common.loading')}</Typography>
</Container>
);
}
@@ -239,22 +234,22 @@ function KnowledgeBaseTesting() {
<KnowledgeBreadcrumbs
kbItems={[
{
label: '知识库',
label: t('knowledgeTesting.knowledgeBase'),
path: '/knowledge'
},
{
label: knowledgeDetail?.name || '知识库详情',
label: knowledgeDetail?.name || t('knowledgeTesting.knowledgeBaseDetail'),
path: `/knowledge/${id}`
},
{
label: '测试'
label: t('knowledgeTesting.testing')
}
]}
/>
<Box sx={{ mb: 3 }}>
<Typography variant="h4" gutterBottom>
{t('knowledgeTesting.knowledgeBaseTesting')}
</Typography>
<Typography variant="subtitle1" color="text.secondary">
{knowledgeDetail?.name}
@@ -266,25 +261,25 @@ function KnowledgeBaseTesting() {
<Grid size={4}>
<Paper sx={{ p: 3, position: 'sticky', top: 20 }}>
<Typography variant="h6" gutterBottom>
{t('knowledgeTesting.testConfiguration')}
</Typography>
<Box component="form" onSubmit={handleSubmit(handleTestSubmit)} sx={{ mt: 2 }}>
<TextField
{...register('question', { required: '请输入测试问题' })}
label="测试问题"
{...register('question', { required: t('knowledgeTesting.pleaseEnterTestQuestion') })}
label={t('knowledgeTesting.testQuestion')}
multiline
rows={3}
fullWidth
margin="normal"
error={!!errors.question}
helperText={errors.question?.message}
placeholder="请输入您想要测试的问题..."
placeholder={t('knowledgeTesting.testQuestionPlaceholder')}
/>
<Box sx={{ mt: 3 }}>
<Typography gutterBottom>
: {watch('similarity_threshold')}
{t('knowledgeTesting.similarityThreshold')}: {watch('similarity_threshold')}
</Typography>
<Slider
{...register('similarity_threshold')}
@@ -300,7 +295,7 @@ function KnowledgeBaseTesting() {
<Box sx={{ mt: 3 }}>
<Typography gutterBottom>
: {watch('vector_similarity_weight')}
{t('knowledgeTesting.vectorSimilarityWeight')}: {watch('vector_similarity_weight')}
</Typography>
<Slider
{...register('vector_similarity_weight')}
@@ -315,18 +310,18 @@ function KnowledgeBaseTesting() {
</Box>
<FormControl fullWidth margin="normal">
<InputLabel> ()</InputLabel>
<InputLabel>{t('knowledgeTesting.rerankModel')}</InputLabel>
<Controller
name="rerank_id"
control={control}
render={({ field }) => (
<Select
{...field}
label="重排序模型 (可选)"
label={t('knowledgeTesting.rerankModel')}
disabled={rerankLoading}
>
<MenuItem value="">
<em>使</em>
<em>{t('knowledgeTesting.noRerank')}</em>
</MenuItem>
{rerankOptions.map((group) => [
<ListSubheader key={group.label}>{group.label}</ListSubheader>,
@@ -345,9 +340,9 @@ function KnowledgeBaseTesting() {
{watch('rerank_id') && (
<TextField
{...register('top_k', {
required: '请输入返回结果数量',
min: { value: 1, message: '最小值为1' },
max: { value: 2048, message: '最大值为2048' }
required: t('knowledgeTesting.pleaseEnterResultCount'),
min: { value: 1, message: t('knowledgeTesting.minValue1') },
max: { value: 2048, message: t('knowledgeTesting.maxValue2048') }
})}
label="Top-K"
type="number"
@@ -355,12 +350,12 @@ function KnowledgeBaseTesting() {
margin="normal"
inputProps={{ min: 1, max: 2048 }}
error={!!errors.top_k}
helperText={errors.top_k?.message || '与Rerank模型配合使用'}
helperText={errors.top_k?.message || t('knowledgeTesting.useWithRerankModel')}
/>
)}
<FormControl fullWidth margin="normal">
<InputLabel></InputLabel>
<InputLabel>{t('knowledgeTesting.crossLanguageSearch')}</InputLabel>
<Controller
name="cross_languages"
control={control}
@@ -368,8 +363,8 @@ function KnowledgeBaseTesting() {
<Select
{...field}
multiple
label="跨语言搜索"
input={<OutlinedInput label="跨语言搜索" />}
label={t('knowledgeTesting.crossLanguageSearch')}
input={<OutlinedInput label={t('knowledgeTesting.crossLanguageSearch')} />}
renderValue={(selected) => (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{selected.map((value) => {
@@ -399,7 +394,7 @@ function KnowledgeBaseTesting() {
checked={watch('use_kg')}
/>
}
label="使用知识图谱"
label={t('knowledgeTesting.useKnowledgeGraph')}
/>
<Button
@@ -409,7 +404,7 @@ function KnowledgeBaseTesting() {
disabled={testing}
sx={{ mt: 2 }}
>
{testing ? '测试中...' : '开始测试'}
{testing ? t('knowledgeTesting.testing') : t('knowledgeTesting.startTest')}
</Button>
</Box>
</Paper>

View File

@@ -1,265 +0,0 @@
# 参考Setting项目架构分析
## 项目概述
参考项目位于 `rag_web_core/src/pages/dataset/setting`,采用了基于 `react-hook-form``zod` 的现代表单管理架构,实现了高度模块化和可扩展的配置页面。
## 文件结构
```
setting/
├── index.tsx # 主页面入口
├── form-schema.ts # 表单数据结构定义
├── general-form.tsx # 通用表单组件
├── chunk-method-form.tsx # 动态解析方法表单
├── configuration-form-container.tsx # 表单容器组件
├── hooks.ts # 数据获取和状态管理
├── saving-button.tsx # 保存按钮组件
├── permission-form-field.tsx # 权限表单字段
├── tag-item.tsx # 标签项组件
├── configuration/ # 配置组件目录
│ ├── common-item.tsx # 通用配置项
│ ├── naive.tsx # 通用解析配置
│ ├── qa.tsx # Q&A解析配置
│ ├── paper.tsx # 论文解析配置
│ ├── book.tsx # 书籍解析配置
│ ├── table.tsx # 表格解析配置
│ ├── audio.tsx # 音频解析配置
│ ├── email.tsx # 邮件解析配置
│ ├── laws.tsx # 法律解析配置
│ ├── manual.tsx # 手册解析配置
│ ├── one.tsx # One解析配置
│ ├── picture.tsx # 图片解析配置
│ ├── presentation.tsx # 演示文稿解析配置
│ ├── resume.tsx # 简历解析配置
│ ├── knowledge-graph.tsx # 知识图谱配置
│ └── tag.tsx # 标签配置
└── tag-table/ # 标签表格相关组件
└── ...
```
## 核心架构模式
### 1. 主从架构模式 (Master-Slave)
- **主控制器**: `index.tsx` 作为主页面,管理整体状态和表单实例
- **从组件**: `general-form.tsx``chunk-method-form.tsx` 作为子表单组件
### 2. 策略模式 (Strategy Pattern)
- **配置映射**: `ConfigurationComponentMap` 根据 `parser_id` 动态选择配置组件
- **组件策略**: 每个解析类型对应一个独立的配置组件
### 3. 模块化设计
- **功能分离**: 通用配置与特定解析配置分离
- **组件复用**: 通过 `configuration-form-container.tsx` 提供统一的布局容器
## 关键技术实现
### 1. 表单管理系统
#### 数据结构 (form-schema.ts)
```typescript
export const formSchema = z.object({
name: z.string().min(1),
description: z.string().min(2),
avatar: z.any().nullish(),
permission: z.string().optional(),
parser_id: z.string(),
embd_id: z.string(),
parser_config: z.object({
layout_recognize: z.string(),
chunk_token_num: z.number(),
delimiter: z.string(),
auto_keywords: z.number().optional(),
auto_questions: z.number().optional(),
html4excel: z.boolean(),
raptor: z.object({...}),
graphrag: z.object({...}),
}).optional(),
pagerank: z.number(),
});
```
#### 表单初始化
```typescript
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
name: '',
parser_id: DocumentParserType.Naive,
permission: PermissionRole.Me,
parser_config: {
layout_recognize: DocumentType.DeepDOC,
chunk_token_num: 512,
delimiter: `\n`,
// ... 其他默认值
},
},
});
```
### 2. 动态表单系统
#### 配置组件映射
```typescript
const ConfigurationComponentMap = {
[DocumentParserType.Naive]: NaiveConfiguration,
[DocumentParserType.Qa]: QAConfiguration,
[DocumentParserType.Resume]: ResumeConfiguration,
// ... 其他映射
};
```
#### 动态组件渲染
```typescript
const ConfigurationComponent = useMemo(() => {
return finalParserId
? ConfigurationComponentMap[finalParserId]
: EmptyComponent;
}, [finalParserId]);
```
### 3. 状态管理
#### 表单上下文共享
- 使用 `useFormContext()` 在子组件中访问表单实例
- 通过 `useWatch()` 监听特定字段变化
#### 数据获取钩子
```typescript
// hooks.ts
export function useFetchKnowledgeConfigurationOnMount(form) {
// 获取知识库配置并初始化表单
}
```
### 4. 页面布局结构
#### 主页面布局 (index.tsx)
```typescript
<section className="p-5 h-full flex flex-col">
<TopTitle />
<div className="flex gap-14 flex-1 min-h-0">
<Form {...form}>
<form className="space-y-6 flex-1">
<Tabs>
<TabsList>
<TabsTrigger value="generalForm">通用</TabsTrigger>
<TabsTrigger value="chunkMethodForm">解析方法</TabsTrigger>
</TabsList>
<TabsContent value="generalForm">
<GeneralForm />
</TabsContent>
<TabsContent value="chunkMethodForm">
<ChunkMethodForm />
</TabsContent>
</Tabs>
</form>
</Form>
<ChunkMethodLearnMore />
</div>
</section>
```
#### 表单容器组件
```typescript
// configuration-form-container.tsx
export function ConfigurationFormContainer({ children, className }) {
return (
<FormContainer className={cn('p-10', className)}>
{children}
</FormContainer>
);
}
export function MainContainer({ children }) {
return <section className="space-y-5">{children}</section>;
}
```
## 组件设计模式
### 1. 通用表单组件 (general-form.tsx)
- 使用 `FormField` 组件统一表单字段样式
- 采用 1/4 和 3/4 的标签与输入框布局比例
- 集成头像上传、权限选择等通用功能
### 2. 配置组件模式
每个配置组件遵循统一的结构:
```typescript
export function NaiveConfiguration() {
return (
<ConfigurationFormContainer>
<MainContainer>
<LayoutRecognizeFormField />
<MaxTokenNumberFormField />
<DelimiterFormField />
<AutoKeywordsFormField />
<AutoQuestionsFormField />
<TagItems />
</MainContainer>
</ConfigurationFormContainer>
);
}
```
### 3. 表单字段组件
- 统一的 `FormField` 包装器
- 一致的错误处理和验证显示
- 响应式布局设计
## 技术特色
### 1. 类型安全
- 使用 TypeScript 和 Zod 确保类型安全
- 表单数据结构与后端API对齐
### 2. 性能优化
- 使用 `useMemo` 优化动态组件渲染
- `useWatch` 精确监听字段变化,避免不必要的重渲染
### 3. 可扩展性
- 新增解析类型只需添加配置组件和映射关系
- 模块化设计便于功能扩展
### 4. 用户体验
- Tab切换提供清晰的功能分区
- 右侧学习面板提供上下文帮助
- 统一的保存和重置操作
## 与用户项目的对比
### 用户当前架构
- 使用 Material-UI 组件库
- 基于 `react-hook-form` 但结构相对简单
- 单一的 Accordion 布局
- 配置逻辑集中在一个组件中
### 参考项目优势
- 更清晰的模块化分离
- 动态配置组件系统
- 更好的代码组织和可维护性
- 统一的设计语言和布局模式
## 适配建议
### 1. 保持现有样式
- 继续使用 Material-UI 组件
- 保持 Accordion 布局风格
- 适配现有的主题和设计规范
### 2. 采用核心架构
- 引入动态配置组件映射机制
- 分离通用配置和特定解析配置
- 使用 `useFormContext` 实现组件间状态共享
### 3. 数据结构对齐
- 参考 `form-schema.ts` 调整数据结构
- 使用 Zod 进行数据验证
- 统一默认值设置
### 4. 状态管理优化
- 使用 `useWatch` 监听字段变化
- 实现数据获取钩子
- 优化表单重置和提交逻辑
这种架构设计为大型表单应用提供了良好的可维护性和扩展性基础,值得在用户项目中借鉴和应用。