feat(knowledge): add document parser and metadata management
This commit is contained in:
@@ -38,7 +38,9 @@
|
||||
"react-is": "18.3.1",
|
||||
"react-router-dom": "^7.9.4",
|
||||
"uuid": "^13.0.0",
|
||||
"zustand": "^5.0.8"
|
||||
"zustand": "^5.0.8",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"monaco-editor": "^0.52.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.36.0",
|
||||
|
||||
18
pnpm-lock.yaml
generated
18
pnpm-lock.yaml
generated
@@ -14,6 +14,9 @@ importers:
|
||||
'@emotion/styled':
|
||||
specifier: ^11.14.1
|
||||
version: 11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@18.3.1))(@types/react@19.2.2)(react@18.3.1)
|
||||
'@monaco-editor/react':
|
||||
specifier: ^4.6.0
|
||||
version: 4.7.0(monaco-editor@0.52.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@mui/icons-material':
|
||||
specifier: ^7.3.4
|
||||
version: 7.3.4(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.2.2)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@18.3.1))(@types/react@19.2.2)(react@18.3.1))(@types/react@19.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@19.2.2)(react@18.3.1)
|
||||
@@ -59,6 +62,9 @@ importers:
|
||||
loglevel:
|
||||
specifier: ^1.9.2
|
||||
version: 1.9.2
|
||||
monaco-editor:
|
||||
specifier: ^0.52.2
|
||||
version: 0.52.2
|
||||
pdfjs-dist:
|
||||
specifier: ^5.4.394
|
||||
version: 5.4.394
|
||||
@@ -8785,6 +8791,9 @@ packages:
|
||||
moment@2.30.1:
|
||||
resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==}
|
||||
|
||||
monaco-editor@0.52.2:
|
||||
resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==}
|
||||
|
||||
monaco-editor@0.54.0:
|
||||
resolution: {integrity: sha512-hx45SEUoLatgWxHKCmlLJH81xBo0uXP4sRkESUpmDQevfi+e7K1VuiSprK6UpQ8u4zOcKNiH0pMvHvlMWA/4cw==}
|
||||
|
||||
@@ -14016,6 +14025,13 @@ snapshots:
|
||||
dependencies:
|
||||
state-local: 1.0.7
|
||||
|
||||
'@monaco-editor/react@4.7.0(monaco-editor@0.52.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@monaco-editor/loader': 1.6.1
|
||||
monaco-editor: 0.52.2
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
'@monaco-editor/react@4.7.0(monaco-editor@0.54.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@monaco-editor/loader': 1.6.1
|
||||
@@ -22253,6 +22269,8 @@ snapshots:
|
||||
|
||||
moment@2.30.1: {}
|
||||
|
||||
monaco-editor@0.52.2: {}
|
||||
|
||||
monaco-editor@0.54.0:
|
||||
dependencies:
|
||||
dompurify: 3.1.7
|
||||
|
||||
@@ -279,6 +279,10 @@ export interface IKnowledgeFile {
|
||||
name: string;
|
||||
/** 解析器ID */
|
||||
parser_id: string;
|
||||
/** 流水线ID,可选 */
|
||||
pipeline_id?: string;
|
||||
/** 流水线名称,可选 */
|
||||
pipeline_name?: string;
|
||||
/** 处理开始时间,可选 */
|
||||
process_begin_at?: any;
|
||||
/** 处理持续时间 */
|
||||
|
||||
@@ -599,6 +599,7 @@ export default {
|
||||
redo: 'Do you want to clear the existing {{chunkNum}} chunks?',
|
||||
setMetaData: 'Set Meta Data',
|
||||
pleaseInputJson: 'Please enter JSON',
|
||||
invalidJson: 'Invalid JSON format',
|
||||
documentMetaTips: `<p>The meta data is in Json format(it's not searchable). It will be added into prompt for LLM if any chunks of this document are included in the prompt.</p>
|
||||
<p>Examples:</p>
|
||||
<b>The meta data is:</b><br>
|
||||
|
||||
@@ -593,6 +593,7 @@ export default {
|
||||
redo: '是否清空已有 {{chunkNum}}个 chunk?',
|
||||
setMetaData: '设置元数据',
|
||||
pleaseInputJson: '请输入JSON',
|
||||
invalidJson: '无效的JSON格式',
|
||||
documentMetaTips: `<p>元数据为 Json 格式(不可搜索)。如果提示中包含此文档的任何块,它将被添加到 LLM 的提示中。</p>
|
||||
<p>示例:</p>
|
||||
<b>元数据为:</b><br>
|
||||
|
||||
@@ -58,6 +58,9 @@ import dayjs from 'dayjs';
|
||||
import logger from '@/utils/logger';
|
||||
import { LanguageAbbreviation } from '@/constants/common';
|
||||
import knowledgeService from '@/services/knowledge_service';
|
||||
import ParserContextMenu from './ParserContextMenu';
|
||||
import DocumentParserDialog from './DocumentParserDialog';
|
||||
import DocumentMetadataDialog from './DocumentMetadataDialog';
|
||||
|
||||
|
||||
interface DocumentListComponentProps {
|
||||
@@ -215,6 +218,15 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
|
||||
const [renameDialogOpen, setRenameDialogOpen] = useState(false);
|
||||
const [newFileName, setNewFileName] = useState('');
|
||||
|
||||
// parser 列右键菜单与对话框状态
|
||||
const [parserMenuAnchor, setParserMenuAnchor] = useState<null | HTMLElement>(null);
|
||||
const [parserMenuFile, setParserMenuFile] = useState<IKnowledgeFile | undefined>(undefined);
|
||||
const [parserDialogOpen, setParserDialogOpen] = useState(false);
|
||||
const [metadataDialogOpen, setMetadataDialogOpen] = useState(false);
|
||||
// 打开对话框时使用的文件,避免关闭菜单时清空导致对话框拿不到文件
|
||||
const [dialogFile, setDialogFile] = useState<IKnowledgeFile | undefined>(undefined);
|
||||
// 解析与元数据对话框提交状态由子组件内部管理
|
||||
|
||||
const { i18n, t } = useTranslation();
|
||||
|
||||
// 根据当前语言获取DataGrid的localeText
|
||||
@@ -346,6 +358,19 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
|
||||
setSelectedSuffix(typeof value === 'string' ? value.split(',') : value);
|
||||
};
|
||||
|
||||
// 打开 parser 列的上下文菜单
|
||||
const handleOpenParserMenu = (event: React.MouseEvent<HTMLElement>, file: IKnowledgeFile) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
setParserMenuAnchor(event.currentTarget);
|
||||
setParserMenuFile(file);
|
||||
};
|
||||
|
||||
const handleCloseParserMenu = () => {
|
||||
setParserMenuAnchor(null);
|
||||
setParserMenuFile(undefined);
|
||||
};
|
||||
|
||||
// 选中数量计算(强类型)
|
||||
const countSelected = (model: GridRowSelectionModel, totalCount: number): number => {
|
||||
const size = model.ids.size ?? 0;
|
||||
@@ -380,7 +405,7 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
|
||||
field: 'name',
|
||||
headerName: t('knowledge.fileName'),
|
||||
flex: 2,
|
||||
minWidth: 200,
|
||||
minWidth: 120,
|
||||
cellClassName: 'grid-center-cell',
|
||||
renderCell: (params) => (
|
||||
<Box
|
||||
@@ -408,6 +433,13 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'create_time',
|
||||
headerName: t('knowledge.uploadTime'),
|
||||
flex: 1,
|
||||
minWidth: 140,
|
||||
valueFormatter: (value) => dayjs(value).format('YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
headerName: t('knowledge.type'),
|
||||
@@ -446,11 +478,15 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
|
||||
renderCell: (params) => getRunStatusChip(params.value, params.row.progress),
|
||||
},
|
||||
{
|
||||
field: 'create_time',
|
||||
headerName: t('knowledge.uploadTime'),
|
||||
flex: 1,
|
||||
minWidth: 140,
|
||||
valueFormatter: (value) => dayjs(value).format('YYYY-MM-DD HH:mm:ss'),
|
||||
field: 'parser_id',
|
||||
headerName: t('knowledge.parser'),
|
||||
flex: 0.8,
|
||||
minWidth: 100,
|
||||
renderCell: (params) => (
|
||||
<Box onContextMenu={(e) => handleOpenParserMenu(e, params.row)} onClick={(e) => handleOpenParserMenu(e, params.row)}>
|
||||
<Chip label={params.value.toUpperCase()} size="small" variant="outlined" />
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'actions',
|
||||
@@ -680,6 +716,23 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
|
||||
/>
|
||||
</Paper>
|
||||
|
||||
{/* Parser 列上下文菜单 */}
|
||||
<ParserContextMenu
|
||||
anchorEl={parserMenuAnchor}
|
||||
open={Boolean(parserMenuAnchor)}
|
||||
onClose={handleCloseParserMenu}
|
||||
onOpenParser={() => {
|
||||
setDialogFile(parserMenuFile);
|
||||
setParserDialogOpen(true);
|
||||
handleCloseParserMenu();
|
||||
}}
|
||||
onOpenMetadata={() => {
|
||||
setDialogFile(parserMenuFile);
|
||||
setMetadataDialogOpen(true);
|
||||
handleCloseParserMenu();
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 菜单 */}
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
@@ -746,6 +799,22 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
|
||||
<Button onClick={handleRenameConfirm} variant="contained">{t('common.confirm')}</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* Document Parser Dialog */}
|
||||
<DocumentParserDialog
|
||||
open={parserDialogOpen}
|
||||
file={dialogFile}
|
||||
onClose={() => { setParserDialogOpen(false); setDialogFile(undefined); }}
|
||||
onSuccess={() => { setParserDialogOpen(false); setDialogFile(undefined); onRefresh(); }}
|
||||
/>
|
||||
|
||||
{/* Document Metadata Dialog */}
|
||||
<DocumentMetadataDialog
|
||||
open={metadataDialogOpen}
|
||||
file={dialogFile}
|
||||
onClose={() => { setMetadataDialogOpen(false); setDialogFile(undefined); }}
|
||||
onSuccess={() => { setMetadataDialogOpen(false); setDialogFile(undefined); onRefresh(); }}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
115
src/pages/knowledge/components/DocumentMetadataDialog.tsx
Normal file
115
src/pages/knowledge/components/DocumentMetadataDialog.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { IKnowledgeFile } from '@/interfaces/database/knowledge';
|
||||
import knowledgeService from '@/services/knowledge_service';
|
||||
import logger from '@/utils/logger';
|
||||
import Editor from '@monaco-editor/react';
|
||||
|
||||
interface DocumentMetadataDialogProps {
|
||||
open: boolean;
|
||||
file?: IKnowledgeFile;
|
||||
onClose: () => void;
|
||||
onSubmittingChange?: (submitting: boolean) => void;
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
export default function DocumentMetadataDialog({
|
||||
open,
|
||||
file,
|
||||
onClose,
|
||||
onSubmittingChange,
|
||||
onSuccess,
|
||||
}: DocumentMetadataDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const [value, setValue] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// 初始值:将 file.meta_fields 作为格式化 JSON 预填
|
||||
const initialJson = useMemo(() => {
|
||||
const meta = (file as any)?.meta_fields;
|
||||
if (meta && typeof meta === 'object') {
|
||||
try {
|
||||
return JSON.stringify(meta, null, 2);
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}, [file]);
|
||||
|
||||
useEffect(() => {
|
||||
// 每次打开针对不同文件重置输入为当前文件的 meta_fields
|
||||
if (open) {
|
||||
setValue(initialJson);
|
||||
setError(null);
|
||||
}
|
||||
}, [open, file?.id, initialJson]);
|
||||
|
||||
const validateJson = (text: string) => {
|
||||
try {
|
||||
if (!text.trim()) {
|
||||
setError(null);
|
||||
return {};
|
||||
}
|
||||
const parsed = JSON.parse(text);
|
||||
setError(null);
|
||||
return parsed;
|
||||
} catch (e: any) {
|
||||
setError(e?.message || 'Invalid JSON');
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!file) return;
|
||||
try {
|
||||
onSubmittingChange?.(true);
|
||||
const parsed = validateJson(value);
|
||||
if (parsed === null) {
|
||||
return;
|
||||
}
|
||||
const metaStr = JSON.stringify(parsed);
|
||||
await knowledgeService.setDocumentMetaData({ doc_id: file.id, meta: metaStr });
|
||||
onSuccess?.();
|
||||
onClose();
|
||||
} catch (err) {
|
||||
logger.error('设置元数据失败', err);
|
||||
} finally {
|
||||
onSubmittingChange?.(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>{t('knowledgeDetails.setMetaData')}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Editor
|
||||
height="240px"
|
||||
language="json"
|
||||
theme="vs-dark"
|
||||
value={value}
|
||||
onChange={(val) => setValue(val || '')}
|
||||
options={{
|
||||
minimap: { enabled: false },
|
||||
automaticLayout: true,
|
||||
formatOnPaste: true,
|
||||
formatOnType: true,
|
||||
tabSize: 2,
|
||||
}}
|
||||
/>
|
||||
{error ? (
|
||||
<Typography variant="caption" color="error" sx={{ mt: 1, display: 'block' }}>
|
||||
{t('knowledgeDetails.invalidJson')}: {error}
|
||||
</Typography>
|
||||
) : null}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>{t('common.cancel')}</Button>
|
||||
<Button variant="contained" onClick={handleSubmit} disabled={!!error}>
|
||||
{t('common.confirm')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
128
src/pages/knowledge/components/DocumentParseForm.tsx
Normal file
128
src/pages/knowledge/components/DocumentParseForm.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Box, Card, CardContent } from '@mui/material';
|
||||
import { useFormContext, useWatch, type UseFormReturn } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AutoKeywordsItem, AutoQuestionsItem, BasicConfigItems, ChunkTokenNumberItem, DelimiterItem, PipelineSelectorItem } from '../configuration/common-items';
|
||||
import { ChunkMethodItem } from '../configuration';
|
||||
import { RadioFormField } from '@/components/FormField';
|
||||
import { DocumentParserType, ParseType } from '@/constants/knowledge';
|
||||
import logger from '@/utils/logger';
|
||||
|
||||
|
||||
function ParserConfigurationItems({ parser_id }: { parser_id: DocumentParserType }) {
|
||||
|
||||
logger.info('ParserConfigurationItems ---- parser_id', parser_id);
|
||||
|
||||
const chunkNum_parserArr = [DocumentParserType.Naive]
|
||||
|
||||
const auto_parserArr = [DocumentParserType.Naive, DocumentParserType.Manual, DocumentParserType.Paper, DocumentParserType.Book, DocumentParserType.Laws, DocumentParserType.Presentation, DocumentParserType.One]
|
||||
|
||||
const itemsArr = []
|
||||
|
||||
if (chunkNum_parserArr.includes(parser_id)) {
|
||||
const item = (
|
||||
<Card sx={{ mb: 2, px: 2 }}>
|
||||
<CardContent>
|
||||
{/* 建议文本块大小 */}
|
||||
<ChunkTokenNumberItem />
|
||||
{/* 文本分段标识符 */}
|
||||
<DelimiterItem />
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
itemsArr.push(item)
|
||||
}
|
||||
|
||||
if (auto_parserArr.includes(parser_id)) {
|
||||
const item = (
|
||||
<Card sx={{ mb: 2, px: 2 }}>
|
||||
<CardContent>
|
||||
{/* 自动关键词提取 */}
|
||||
<AutoKeywordsItem />
|
||||
{/* 自动问题提取 */}
|
||||
<AutoQuestionsItem />
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
itemsArr.push(item)
|
||||
}
|
||||
return (
|
||||
<Box>
|
||||
{itemsArr}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
interface DocumentParseFormProps {
|
||||
form?: UseFormReturn;
|
||||
}
|
||||
|
||||
export default function DocumentParseForm({
|
||||
form: propForm,
|
||||
}: DocumentParseFormProps = {}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// 允许从 FormProvider 获取 form,也允许通过 props 传入
|
||||
let contextForm: UseFormReturn | null = null;
|
||||
try {
|
||||
contextForm = useFormContext();
|
||||
} catch (err) {
|
||||
contextForm = null;
|
||||
}
|
||||
const form = propForm || contextForm || null;
|
||||
|
||||
if (!form) {
|
||||
return (
|
||||
<Box sx={{ p: 2, textAlign: 'center' }}>
|
||||
{t('form.formConfigError')}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const { control } = form;
|
||||
// 同步 pipeline_id 选择影响解析模式默认值
|
||||
const pipeline_id = useWatch({ control, name: 'pipeline_id' });
|
||||
|
||||
// 同步 parser_id 选择影响解析模式默认值
|
||||
const parser_id = useWatch({ control, name: 'parser_id' });
|
||||
|
||||
const [parseType, setParseType] = useState<ParseType>(ParseType.BuildIn);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setParseType(pipeline_id ? ParseType.Pipeline : ParseType.BuildIn);
|
||||
}, [pipeline_id]);
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||
<Card sx={{ px: 3 }}>
|
||||
<CardContent>
|
||||
<RadioFormField
|
||||
name="parseType"
|
||||
label={t('knowledgeConfiguration.parseType')}
|
||||
defaultValue={parseType}
|
||||
options={[
|
||||
{ value: ParseType.BuildIn, label: 'Built-in' },
|
||||
{ value: ParseType.Pipeline, label: 'Pipeline' },
|
||||
]}
|
||||
onChangeValue={(v) => setParseType(v as ParseType)}
|
||||
/>
|
||||
|
||||
{/* 基于模式:内置显示切片方法,Pipeline 显示选择器 */}
|
||||
{parseType === ParseType.BuildIn ? (
|
||||
<ChunkMethodItem />
|
||||
) : (
|
||||
<PipelineSelectorItem />
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 动态配置内容:始终渲染,内部按 parseType 控制基础配置显示 */}
|
||||
{
|
||||
parseType === ParseType.BuildIn &&
|
||||
<ParserConfigurationItems parser_id={parser_id} />
|
||||
}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
100
src/pages/knowledge/components/DocumentParserDialog.tsx
Normal file
100
src/pages/knowledge/components/DocumentParserDialog.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useForm, FormProvider } from 'react-hook-form';
|
||||
import type { IKnowledgeFile, IKnowledgeFileParserConfig } from '@/interfaces/database/knowledge';
|
||||
import knowledgeService from '@/services/knowledge_service';
|
||||
import logger from '@/utils/logger';
|
||||
import DocumentParseForm from './DocumentParseForm';
|
||||
|
||||
interface DocumentParserDialogProps {
|
||||
open: boolean;
|
||||
file?: IKnowledgeFile;
|
||||
onClose: () => void;
|
||||
onSubmittingChange?: (submitting: boolean) => void;
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
export default function DocumentParserDialog({
|
||||
open,
|
||||
file,
|
||||
onClose,
|
||||
onSubmittingChange,
|
||||
onSuccess,
|
||||
}: DocumentParserDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
logger.info('DocumentParserDialog 组件渲染', { open, file });
|
||||
|
||||
// 统一构建默认表单值(与 BasicConfigItems 字段保持一致)
|
||||
const buildDefaultValues = (f?: IKnowledgeFile) => {
|
||||
const cfg: Partial<IKnowledgeFileParserConfig> = f?.parser_config || {};
|
||||
const defaultValues: IKnowledgeFile = {
|
||||
parser_id: f?.parser_id ?? '',
|
||||
pipeline_id: f?.pipeline_id ?? '',
|
||||
pipeline_name: f?.pipeline_name ?? '',
|
||||
parser_config: cfg,
|
||||
} as any;
|
||||
return defaultValues;
|
||||
};
|
||||
|
||||
const parserFormMethods = useForm({
|
||||
defaultValues: buildDefaultValues(file),
|
||||
});
|
||||
|
||||
// 当对话框打开或文件变更时,重置表单为最新数据;关闭时清空到默认值
|
||||
useEffect(() => {
|
||||
if (open && file) {
|
||||
parserFormMethods.reset(buildDefaultValues(file));
|
||||
}
|
||||
if (!open) {
|
||||
parserFormMethods.reset(buildDefaultValues(undefined));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open, file]);
|
||||
|
||||
const handleSubmit = async (data: {
|
||||
parser_id: string;
|
||||
pipeline_id?: string;
|
||||
parser_config: IKnowledgeFileParserConfig;
|
||||
}) => {
|
||||
try {
|
||||
onSubmittingChange?.(true);
|
||||
if (!file) return; // 无文件时不提交
|
||||
await knowledgeService.changeDocumentParser({
|
||||
doc_id: file.id,
|
||||
parser_config: data.parser_config,
|
||||
parser_id: data.parser_id,
|
||||
pipeline_id: data.pipeline_id || '',
|
||||
});
|
||||
onSuccess?.();
|
||||
onClose();
|
||||
} catch (err) {
|
||||
logger.error('更新文档解析器配置失败', err);
|
||||
} finally {
|
||||
onSubmittingChange?.(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
|
||||
<DialogTitle>{t('knowledgeDetails.dataPipeline')}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
{file ? (
|
||||
<FormProvider {...parserFormMethods}>
|
||||
<DocumentParseForm
|
||||
// @ts-ignore
|
||||
form={parserFormMethods}
|
||||
/>
|
||||
</FormProvider>
|
||||
) : null}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>{t('common.cancel')}</Button>
|
||||
<Button variant="contained" onClick={parserFormMethods.handleSubmit(handleSubmit)}>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
32
src/pages/knowledge/components/ParserContextMenu.tsx
Normal file
32
src/pages/knowledge/components/ParserContextMenu.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import { Menu, MenuItem, Typography } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface ParserContextMenuProps {
|
||||
anchorEl: HTMLElement | null;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onOpenParser: () => void;
|
||||
onOpenMetadata: () => void;
|
||||
}
|
||||
|
||||
export default function ParserContextMenu({
|
||||
anchorEl,
|
||||
open,
|
||||
onClose,
|
||||
onOpenParser,
|
||||
onOpenMetadata,
|
||||
}: ParserContextMenuProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Menu anchorEl={anchorEl} open={open} onClose={onClose}>
|
||||
<MenuItem onClick={() => { onOpenParser(); }}>
|
||||
<Typography>{t('knowledgeDetails.dataPipeline')}</Typography>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => { onOpenMetadata(); }}>
|
||||
<Typography>{t('knowledgeDetails.setMetaData')}</Typography>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
@@ -106,7 +106,7 @@ const knowledgeService = {
|
||||
|
||||
// 删除文档
|
||||
removeDocument: (data: { doc_id: string | Array<string | number> }) => {
|
||||
return post(api.document_rm, data);
|
||||
return request.post(api.document_rm, data);
|
||||
},
|
||||
|
||||
// 删除文档(DELETE方法)
|
||||
@@ -119,17 +119,26 @@ const knowledgeService = {
|
||||
* @param data 文档ID列表和状态 status 0 禁用 1 启用
|
||||
*/
|
||||
changeDocumentStatus: (data: { doc_ids: Array<string | number>; status: string | number }) => {
|
||||
return post(api.document_change_status, data);
|
||||
return request.post(api.document_change_status, data);
|
||||
},
|
||||
|
||||
// 运行文档处理
|
||||
runDocument: (data: IRunDocumentRequestBody) => {
|
||||
return post(api.document_run, data);
|
||||
return request.post(api.document_run, data);
|
||||
},
|
||||
|
||||
// 更改文档解析器配置
|
||||
changeDocumentParser: (data: { doc_id: string; parser_config: IKnowledgeFileParserConfig }) => {
|
||||
return post(api.document_change_parser, data);
|
||||
// 更改文档解析器配置(兼容可选的 parser_id 与 pipeline_id 字段)
|
||||
changeDocumentParser: (data: { doc_id: string; parser_config: IKnowledgeFileParserConfig; parser_id?: string; pipeline_id?: string }) => {
|
||||
return request.post(api.document_change_parser, data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置文档元数据
|
||||
* @param data 文档ID和元数据字符串
|
||||
* @param data.meta json string 文档元数据
|
||||
*/
|
||||
setDocumentMetaData: (data: { doc_id: string; meta: string }) => {
|
||||
return request.post(api.setMeta, data);
|
||||
},
|
||||
|
||||
// 获取文档缩略图
|
||||
|
||||
Reference in New Issue
Block a user