diff --git a/package.json b/package.json index 73826ce..6fbcc3d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3815f8d..aac5b90 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/interfaces/database/knowledge.ts b/src/interfaces/database/knowledge.ts index 1e3e98d..fde150c 100644 --- a/src/interfaces/database/knowledge.ts +++ b/src/interfaces/database/knowledge.ts @@ -279,6 +279,10 @@ export interface IKnowledgeFile { name: string; /** 解析器ID */ parser_id: string; + /** 流水线ID,可选 */ + pipeline_id?: string; + /** 流水线名称,可选 */ + pipeline_name?: string; /** 处理开始时间,可选 */ process_begin_at?: any; /** 处理持续时间 */ diff --git a/src/locales/en.ts b/src/locales/en.ts index 6b0632e..56f9b57 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -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: `

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.

Examples:

The meta data is:
diff --git a/src/locales/zh.ts b/src/locales/zh.ts index 7a4d380..f12da03 100644 --- a/src/locales/zh.ts +++ b/src/locales/zh.ts @@ -593,6 +593,7 @@ export default { redo: '是否清空已有 {{chunkNum}}个 chunk?', setMetaData: '设置元数据', pleaseInputJson: '请输入JSON', + invalidJson: '无效的JSON格式', documentMetaTips: `

元数据为 Json 格式(不可搜索)。如果提示中包含此文档的任何块,它将被添加到 LLM 的提示中。

示例:

元数据为:
diff --git a/src/pages/knowledge/components/DocumentListComponent.tsx b/src/pages/knowledge/components/DocumentListComponent.tsx index 4528ac1..01a8629 100644 --- a/src/pages/knowledge/components/DocumentListComponent.tsx +++ b/src/pages/knowledge/components/DocumentListComponent.tsx @@ -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 = ({ const [renameDialogOpen, setRenameDialogOpen] = useState(false); const [newFileName, setNewFileName] = useState(''); + // parser 列右键菜单与对话框状态 + const [parserMenuAnchor, setParserMenuAnchor] = useState(null); + const [parserMenuFile, setParserMenuFile] = useState(undefined); + const [parserDialogOpen, setParserDialogOpen] = useState(false); + const [metadataDialogOpen, setMetadataDialogOpen] = useState(false); + // 打开对话框时使用的文件,避免关闭菜单时清空导致对话框拿不到文件 + const [dialogFile, setDialogFile] = useState(undefined); + // 解析与元数据对话框提交状态由子组件内部管理 + const { i18n, t } = useTranslation(); // 根据当前语言获取DataGrid的localeText @@ -346,6 +358,19 @@ const DocumentListComponent: React.FC = ({ setSelectedSuffix(typeof value === 'string' ? value.split(',') : value); }; + // 打开 parser 列的上下文菜单 + const handleOpenParserMenu = (event: React.MouseEvent, 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 = ({ field: 'name', headerName: t('knowledge.fileName'), flex: 2, - minWidth: 200, + minWidth: 120, cellClassName: 'grid-center-cell', renderCell: (params) => ( = ({ ), }, + { + 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 = ({ 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) => ( + handleOpenParserMenu(e, params.row)} onClick={(e) => handleOpenParserMenu(e, params.row)}> + + + ), }, { field: 'actions', @@ -680,6 +716,23 @@ const DocumentListComponent: React.FC = ({ /> + {/* Parser 列上下文菜单 */} + { + setDialogFile(parserMenuFile); + setParserDialogOpen(true); + handleCloseParserMenu(); + }} + onOpenMetadata={() => { + setDialogFile(parserMenuFile); + setMetadataDialogOpen(true); + handleCloseParserMenu(); + }} + /> + {/* 菜单 */} = ({ + + {/* Document Parser Dialog */} + { setParserDialogOpen(false); setDialogFile(undefined); }} + onSuccess={() => { setParserDialogOpen(false); setDialogFile(undefined); onRefresh(); }} + /> + + {/* Document Metadata Dialog */} + { setMetadataDialogOpen(false); setDialogFile(undefined); }} + onSuccess={() => { setMetadataDialogOpen(false); setDialogFile(undefined); onRefresh(); }} + /> ); }; diff --git a/src/pages/knowledge/components/DocumentMetadataDialog.tsx b/src/pages/knowledge/components/DocumentMetadataDialog.tsx new file mode 100644 index 0000000..b2d2316 --- /dev/null +++ b/src/pages/knowledge/components/DocumentMetadataDialog.tsx @@ -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(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 ( + + {t('knowledgeDetails.setMetaData')} + + setValue(val || '')} + options={{ + minimap: { enabled: false }, + automaticLayout: true, + formatOnPaste: true, + formatOnType: true, + tabSize: 2, + }} + /> + {error ? ( + + {t('knowledgeDetails.invalidJson')}: {error} + + ) : null} + + + + + + + ); +} \ No newline at end of file diff --git a/src/pages/knowledge/components/DocumentParseForm.tsx b/src/pages/knowledge/components/DocumentParseForm.tsx new file mode 100644 index 0000000..a47df60 --- /dev/null +++ b/src/pages/knowledge/components/DocumentParseForm.tsx @@ -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 = ( + + + {/* 建议文本块大小 */} + + {/* 文本分段标识符 */} + + + + ) + itemsArr.push(item) + } + + if (auto_parserArr.includes(parser_id)) { + const item = ( + + + {/* 自动关键词提取 */} + + {/* 自动问题提取 */} + + + + ) + itemsArr.push(item) + } + return ( + + {itemsArr} + + ) +} + + +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 ( + + {t('form.formConfigError')} + + ); + } + + 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.BuildIn); + + + useEffect(() => { + setParseType(pipeline_id ? ParseType.Pipeline : ParseType.BuildIn); + }, [pipeline_id]); + + return ( + + + + setParseType(v as ParseType)} + /> + + {/* 基于模式:内置显示切片方法,Pipeline 显示选择器 */} + {parseType === ParseType.BuildIn ? ( + + ) : ( + + )} + + + + {/* 动态配置内容:始终渲染,内部按 parseType 控制基础配置显示 */} + { + parseType === ParseType.BuildIn && + + } + + ); +} \ No newline at end of file diff --git a/src/pages/knowledge/components/DocumentParserDialog.tsx b/src/pages/knowledge/components/DocumentParserDialog.tsx new file mode 100644 index 0000000..aa5149f --- /dev/null +++ b/src/pages/knowledge/components/DocumentParserDialog.tsx @@ -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 = 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 ( + + {t('knowledgeDetails.dataPipeline')} + + {file ? ( + + + + ) : null} + + + + + + + ); +} \ No newline at end of file diff --git a/src/pages/knowledge/components/ParserContextMenu.tsx b/src/pages/knowledge/components/ParserContextMenu.tsx new file mode 100644 index 0000000..aee9e87 --- /dev/null +++ b/src/pages/knowledge/components/ParserContextMenu.tsx @@ -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 ( + + { onOpenParser(); }}> + {t('knowledgeDetails.dataPipeline')} + + { onOpenMetadata(); }}> + {t('knowledgeDetails.setMetaData')} + + + ); +} \ No newline at end of file diff --git a/src/services/knowledge_service.ts b/src/services/knowledge_service.ts index 83f999d..d027c47 100644 --- a/src/services/knowledge_service.ts +++ b/src/services/knowledge_service.ts @@ -106,7 +106,7 @@ const knowledgeService = { // 删除文档 removeDocument: (data: { doc_id: string | Array }) => { - 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; 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); }, // 获取文档缩略图