diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index 3cec98e086..1ebc2d8465 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -772,6 +772,8 @@ Example: A 1 KB message with 1024-dim embedding uses ~9 KB. The 5 MB default lim parsingMode: 'Parsing mode', manualSetup: 'Custom ingestion pipeline', builtIn: 'Built-in parsing template', + parserSettingsChangeWarning: + 'Updated parser settings apply only to newly uploaded files. To apply these settings to existing files, update their parser settings and re-parse them.', noConfigChunkerHint: 'The built-in {{name}} parser handles chunking automatically, so no additional configuration is required.', titleDescription: diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index 8fb07a75b4..8ca3903b1b 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -708,6 +708,8 @@ export default { parsingMode: '解析方式', manualSetup: '自定义Ingestion pipeline', builtIn: '内置解析模板', + parserSettingsChangeWarning: + '更新后的解析设置仅对新上传的文件生效。如需对现有文件生效,请修改这些文件的解析设置并重新解析。', noConfigChunkerHint: '内置的 {{name}} 解析器会自动处理分块,因此无需额外配置。', titleDescription: '在这里更新知识库配置,特别是大语言模型和提示词。', diff --git a/web/src/pages/dataset/setting/index.tsx b/web/src/pages/dataset/setting/index.tsx index c7594e2b85..1d5c4fc68c 100644 --- a/web/src/pages/dataset/setting/index.tsx +++ b/web/src/pages/dataset/setting/index.tsx @@ -9,6 +9,7 @@ import { CardTitle, } from '@/components/ui/card'; import Divider from '@/components/ui/divider'; +import { Modal } from '@/components/ui/modal/modal'; import { Form, FormField, @@ -24,6 +25,7 @@ import { usePipelineOperatorNodes, useResetParserConfigOnPipelineChange, } from '@/hooks/use-pipeline-operator'; +import { buildParserConfigFromNodes } from '@/utils/pipeline-operator'; import { zodResolver } from '@hookform/resolvers/zod'; import { isEqual } from 'lodash'; import { useCallback, useEffect, useState } from 'react'; @@ -42,6 +44,7 @@ import { usePipelineDataList, useSaveDatasetSetting, } from './hooks'; +import { normalizeParserConfigForComparison } from './parser-config'; import { useRevealSubmitErrors } from './use-reveal-submit-errors'; export default function DatasetSetting() { @@ -146,9 +149,39 @@ export default function DatasetSetting() { const handleSubmit = useCallback( async (data: z.infer) => { - await handleSave(data); + const savedValues = form.formState.defaultValues; + const operatorDefaults = buildParserConfigFromNodes(operatorNodes); + const [savedParserConfig, parserConfig] = [ + savedValues?.parser_config, + data.parser_config, + ].map((config) => + normalizeParserConfigForComparison(config, operatorDefaults, t), + ); + const parserSettingsChanged = + data.parse_type !== savedValues?.parse_type || + (data.parser_id || '') !== (savedValues?.parser_id || '') || + (data.pipeline_id || '') !== (savedValues?.pipeline_id || '') || + !isEqual(parserConfig, savedParserConfig); + + if (knowledgeDetails.chunk_count > 0 && parserSettingsChanged) { + await new Promise((resolve) => { + Modal.warning({ + title: t('common.warn'), + content: t('knowledgeConfiguration.parserSettingsChangeWarning'), + okText: t('common.ok'), + closable: false, + showCancel: false, + onOk: resolve, + }); + }); + } + + const result = await handleSave(data); + if (result.code === 0) { + form.reset(data); + } }, - [handleSave], + [form, handleSave, knowledgeDetails.chunk_count, operatorNodes, t], ); const { errors } = useFormState({ @@ -294,6 +327,7 @@ export default function DatasetSetting() { loading={ datasetSettingLoading || saveLoading || + form.formState.isSubmitting || (operatorNodesLoading && operatorNodes.length === 0) } > diff --git a/web/src/pages/dataset/setting/parser-config.ts b/web/src/pages/dataset/setting/parser-config.ts new file mode 100644 index 0000000000..1b72e6960e --- /dev/null +++ b/web/src/pages/dataset/setting/parser-config.ts @@ -0,0 +1,85 @@ +import { Languages } from '@/components/cross-language-form-field'; +import { ParseDocumentType } from '@/components/layout-recognize-form-field'; +import { Operator } from '@/constants/agent'; +import { FileType } from '@/constants/file'; +import { normalizeParserFormValues } from '@/pages/agent/form/parser-form/utils'; +import { transformApiResponseToForm } from '@/pages/agent/form/title-chunker-form/hook'; +import { + getOperatorType, + normalizeOperatorForm, + transformExtractorConfigToForm, + transformFormConfigToApi, +} from '@/utils/pipeline-operator'; +import type { TFunction } from 'i18next'; + +export function normalizeParserConfigForComparison( + config: Record | undefined, + operatorDefaults: Record, + t: TFunction, +) { + const entries = Object.entries({ ...operatorDefaults, ...config }) + .filter(([operatorId]) => operatorId.includes(':')) + .map(([operatorId, values]) => { + const operatorType = getOperatorType(operatorId); + let normalized = normalizeOperatorForm(operatorId, values); + + // Mirror initialization performed inside the operator forms, including + // defaults that their mounted controls write back without user edits. + if (operatorType === Operator.Parser) { + normalized = normalizeParserFormValues(normalized); + normalized.setups = normalized.setups.map( + (setup: Record) => { + const next = { ...setup }; + if ( + setup.fileFormat === FileType.PDF && + setup.parse_method && + ![ + ParseDocumentType.DeepDOC, + ParseDocumentType.PlainText, + ParseDocumentType.TCADPParser, + ].includes(setup.parse_method) && + !setup.lang + ) { + next.lang = Languages[0]; + } + if ( + [ + FileType.PDF, + FileType.Spreadsheet, + FileType.PowerPoint, + ].includes(setup.fileFormat) && + setup.parse_method === ParseDocumentType.TCADPParser + ) { + next.table_result_type ||= '1'; + next.markdown_image_response_type ||= '1'; + } + return next; + }, + ); + } else if (operatorType === Operator.Extractor) { + normalized = transformExtractorConfigToForm(normalized); + for (const feature of ['keywords', 'questions', 'summary']) { + normalized[feature] = { + ...normalized[feature], + system_prompt: + normalized[feature]?.system_prompt || + t(`flow.prompts.system.${feature}`), + }; + } + } else if ( + operatorType === Operator.TitleChunker || + operatorType === Operator.ManualChunker + ) { + normalized = transformApiResponseToForm(normalized); + if (operatorType === Operator.ManualChunker) { + normalized.method = 'group'; + } + } + + return [operatorId, transformFormConfigToApi(operatorType, normalized)]; + }); + + // Registered optional controls can add undefined properties. Compare the + // serialized settings sent to the API, where those properties are omitted. + return JSON.parse(JSON.stringify(Object.fromEntries(entries))); +}