From c0f5f4a30aac50d54bf981ded695c958b64acd02 Mon Sep 17 00:00:00 2001 From: caoxiaozhu Date: Sun, 12 Jul 2026 15:39:43 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=95=B0=E6=8D=AE=E5=A4=84?= =?UTF-8?q?=E7=90=86=E5=90=91=E5=AF=BC=E6=8B=86=E5=88=86=E6=BA=90=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E4=B8=8A=E4=BC=A0=E6=AD=A5=E9=AA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将任务设置中的文件上传与外来数据源拉取抽离为独立 SourceUploadStep 组件,TaskSetupStep 聚焦任务信息与处理配置,CreateView 同步接入新步骤与草稿同步,回归脚本补充上传步骤断言。 --- .../regression-data-process-wizard.mjs | 176 ++-- .../data-process/DataProcessCreateView.vue | 191 +++- .../data-process/create/SourceUploadStep.vue | 565 ++++++++++++ .../data-process/create/TaskSetupStep.vue | 861 +++++------------- .../src/views/data-process/create/types.ts | 14 +- 5 files changed, 1057 insertions(+), 750 deletions(-) create mode 100644 frontend/src/views/data-process/create/SourceUploadStep.vue diff --git a/frontend/scripts/regression-data-process-wizard.mjs b/frontend/scripts/regression-data-process-wizard.mjs index 57d2320..4ca2013 100644 --- a/frontend/scripts/regression-data-process-wizard.mjs +++ b/frontend/scripts/regression-data-process-wizard.mjs @@ -34,9 +34,14 @@ assert.match(viewSource, /onBeforeRouteLeave\(async \(\) =>/, '路由离开确 assert.doesNotMatch(viewSource, /window\.confirm|ElMessageBox/, '创建页仍在使用系统或 Element Plus 确认框') assert.match(viewSource, /const WIZARD_STEPS = \[/, '向导步骤尚未改为固定常量') -for (const title of ['创建任务', '数据预览', '开始生成', '结果编辑与保存']) { +for (const title of ['创建任务', '上传文件', '数据预览', '开始生成', '结果编辑与保存']) { assert.ok(viewSource.includes(`title: '${title}'`), `缺少固定步骤:${title}`) } +assert.match( + viewSource, + /\{ id: 'create',[\s\S]*?\{ id: 'upload',[\s\S]*?\{ id: 'preview',[\s\S]*?\{ id: 'generate',[\s\S]*?\{ id: 'results'/, + '五步向导顺序必须为创建任务、上传文件、数据预览、开始生成、结果编辑与保存', +) assert.doesNotMatch(viewSource, /steps\s*=\s*computed|all\.filter/, '步骤仍根据处理类型动态增减') assert.match(viewSource, /localStorage\.setItem\(DRAFT_STORAGE_KEY/, '草稿没有持久化') assert.match(viewSource, /localStorage\.getItem\(DRAFT_STORAGE_KEY\)/, '草稿没有恢复读取') @@ -44,6 +49,7 @@ assert.match(viewSource, /restoreDraft\(\)/, '页面没有恢复草稿') const expectedComponents = [ 'TaskSetupStep.vue', + 'SourceUploadStep.vue', 'PreviewCompareStep.vue', 'GenerationStep.vue', 'ResultEditorStep.vue', @@ -68,6 +74,7 @@ for (const field of ['sourceStart', 'sourceEnd', 'originalContent', 'editedConte assert.ok(typesSource.includes(field), `PreviewItem 缺少字段:${field}`) } assert.match(typesSource, /sourceFileId/, 'PreviewItem 缺少来源文件标识') +assert.match(typesSource, /export type StepId = 'create' \| 'upload' \| 'preview' \| 'generate' \| 'results'/, '步骤类型缺少独立上传步骤') assert.match(modelSource, /export function buildPreviewItems/, '缺少切片来源映射生成函数') assert.match(modelSource, /export function sourceLines/, '缺少源文件行偏移生成函数') assert.match(modelSource, /sourceFileId/, '切片生成没有写入来源文件标识') @@ -87,10 +94,10 @@ for (const marker of [ 'preview-editor', 'scrollIntoView', ]) { - assert.ok(previewSource.includes(marker), `第二步缺少结构或行为:${marker}`) + assert.ok(previewSource.includes(marker), `第三步缺少结构或行为:${marker}`) } -assert.match(previewSource, /sourceStart/, '第二步未使用来源起始偏移') -assert.match(previewSource, /sourceEnd/, '第二步未使用来源结束偏移') +assert.match(previewSource, /sourceStart/, '第三步未使用来源起始偏移') +assert.match(previewSource, /sourceEnd/, '第三步未使用来源结束偏移') assert.match(previewSource, /filterable/, '文件选择器必须可搜索') assert.match(previewSource, /当前文件/, '预览缺少当前文件切换器') assert.doesNotMatch(previewSource, /located-badge|sync-label|已定位到/, '源文件栏不应显示冗余定位提示') @@ -115,10 +122,42 @@ assert.match(previewSource, />保存修改<\/el-button>/, '编辑器缺少保存 assert.match(previewSource, /\.editor-actions\s*\{[\s\S]*?justify-content:\s*flex-end/, '取消和保存按钮必须在编辑器右侧对齐') assert.doesNotMatch(previewSource, /item-token|item-status|modifiedOnly|仅看已修改/, '切片列表不应再显示 Token 或修改状态') assert.match(previewSource, /\.preview-editor\s*\{[\s\S]*?flex:\s*1 1 auto[\s\S]*?overflow-y:\s*auto/, '编辑模式必须占据右侧剩余区域并可滚动') -assert.match(previewSource, /@media \(max-width: 900px\)/, '第二步缺少窄屏上下布局') +assert.match(previewSource, /@media \(max-width: 900px\)/, '第三步缺少窄屏上下布局') const taskSetupPath = path.join(createDir, 'TaskSetupStep.vue') const taskSetupSource = await readFile(taskSetupPath, 'utf8') +const sourceUploadPath = path.join(createDir, 'SourceUploadStep.vue') +const sourceUploadSource = await readFile(sourceUploadPath, 'utf8') + +for (const marker of ['= 0 && nextFromUploadStart > nextFromCreateStart, '缺少创建步骤与上传步骤的独立跳转函数') +const nextFromCreateSource = viewSource.slice(nextFromCreateStart, nextFromUploadStart) +const nextFromUploadSource = viewSource.slice(nextFromUploadStart, selectPreviewFileStart) +assert.match(nextFromCreateSource, /taskSetupRef\.value\?\.validate\(\)/, '创建步骤继续前没有校验任务配置') +assert.match(nextFromCreateSource, /goToStep\('upload'\)/, '创建步骤校验通过后没有进入上传文件') +assert.doesNotMatch(nextFromCreateSource, /uploadedFiles|buildPreviewItems/, '创建步骤仍在校验文件或提前生成预览') +assert.match(nextFromUploadSource, /uploadedFiles\.value\.length === 0/, '上传步骤继续前没有校验源数据') +assert.match(nextFromUploadSource, /buildPreviewItems\(/, '上传步骤没有在进入预览前生成预览数据') +assert.match(nextFromUploadSource, /goToStep\('preview'\)/, '上传步骤完成后没有进入数据预览') +assert.match(viewSource, /function goToStep\(stepId: StepId\)[\s\S]*?WIZARD_STEPS\.findIndex/, '向导跳转没有使用稳定步骤标识') +assert.match(viewSource, /currentStepId\.value === 'preview'[\s\S]*?goToStep\('generate'\)/, '数据预览步骤没有进入开始生成') +assert.match(viewSource, /generation\.status === 'success'[\s\S]*?goToStep\('results'\)/, '生成成功后没有进入结果编辑与保存') +assert.match(viewSource, /currentStepId:\s*currentStepId\.value/, '草稿没有保存稳定步骤标识') +assert.match(viewSource, /const LEGACY_V3_STEP_IDS:[^=]*= \['create', 'preview', 'generate', 'results'\]/, 'v3 草稿缺少旧步骤索引映射') +assert.match(viewSource, /snapshotSchemaVersion === 3[\s\S]*?LEGACY_V3_STEP_IDS/, 'v3 草稿没有迁移到五步索引') +assert.match(viewSource, / props\.unstructuredOptions\.chunkMethod,[\s\S]*?method === 'custom'[\s\S]*?advancedChunkSettingsOpen\.value = true/, '选择自定义分隔符时没有自动展开高级设置') +assert.match(taskSetupSource, /if \(chunkValidationMessage\.value\) \{[\s\S]*?advancedChunkSettingsOpen\.value = true[\s\S]*?return false/, '高级切分配置校验失败时没有重新展开定位') +assert.match(taskSetupSource, /const preserveSpecialContentEnabled = computed/, '特殊内容保护没有合并为单一开关') +assert.match(taskSetupSource, /function updateSpecialContentProtection/, '特殊内容保护开关缺少更新逻辑') assert.match(taskSetupSource, /unstructuredOptions\.chunkSize[\s\S]*?:min="200"[\s\S]*?:max="2000"/, '切片长度必须限制在 200 到 2000 Token') assert.match(taskSetupSource, /unstructuredOptions\.chunkOverlap[\s\S]*?:min="0"[\s\S]*?:max="500"/, '重叠长度必须限制在 0 到 500 Token') assert.match(taskSetupSource, /unstructuredOptions\.minChunkSize[\s\S]*?:min="20"[\s\S]*?:max="500"/, '最小切片长度必须限制在 20 到 500 Token') -for (const label of [ - '每个切片生成数量', - '上下文范围', - '当前切片', - '相邻切片', - '当前章节', - '问题类型', - '事实问答', - '概念解释', - '操作步骤', - '原因分析', - '综合问答', - '跳过无法回答的内容', -]) { +for (const label of ['语义丰富表达', '每个切片生成数量', '数据集划分']) { assert.ok(taskSetupSource.includes(label), `非结构化生成选项缺少:${label}`) } +for (const removedLabel of ['上下文范围', '问题类型', '跳过无法回答的内容']) { + assert.ok(!taskSetupSource.includes(removedLabel), `简化后仍显示低频选项:${removedLabel}`) +} assert.match(taskSetupSource, /unstructuredOptions\.qaPairsPerChunk[\s\S]*?:min="1"[\s\S]*?:max="3"/, '每个切片生成数量必须限制在 1 到 3') assert.match(taskSetupSource, /unstructuredSplitTotal\.value !== 100/, '非结构化数据集划分缺少总和 100% 校验') assert.match(taskSetupSource, /chunkOverlap \+ props\.unstructuredOptions\.minChunkSize[\s\S]*?> props\.unstructuredOptions\.chunkSize/, '切分配置未校验重叠长度与最小切片长度的组合边界') assert.ok(taskSetupSource.includes('Token 数为轻量估算值'), '切片长度缺少 Token 估算说明') +assert.match(taskSetupSource, /\.chunk-settings-grid\s*\{[\s\S]*?grid-template-columns:\s*repeat\(3,\s*minmax\(0,\s*1fr\)\)/, '核心切分参数没有收紧为三列布局') assert.match(viewSource, /const unstructuredOptions = ref/, '父页面缺少非结构化配置状态') assert.match(viewSource, /chunkMethod:\s*'semantic'/, '非结构化默认切分方式必须为自动语义切分') @@ -228,13 +259,18 @@ assert.match(viewSource, /chunkSize:\s*800/, '默认切片长度必须为 800 To assert.match(viewSource, /chunkOverlap:\s*100/, '默认重叠长度必须为 100 Token') assert.match(viewSource, /minChunkSize:\s*100/, '默认最小切片长度必须为 100 Token') assert.match(viewSource, /qaPairsPerChunk:\s*1/, '默认每个切片必须生成 1 个问答对') -assert.match(viewSource, /contextScope:\s*'adjacent'/, '默认上下文范围必须为相邻切片') assert.match(viewSource, /unstructuredOptions:\s*\{[\s\S]*\.\.\.unstructuredOptions\.value/, '非结构化配置没有写入草稿') -assert.match(viewSource, /unstructuredOptions\.value = \{[\s\S]*\.\.\.snapshot\.unstructuredOptions/, '非结构化配置没有从草稿恢复') +assert.match(viewSource, /unstructuredOptions\.value = \{[\s\S]*restoredUnstructuredOptions\.chunkSize/, '非结构化配置没有从草稿恢复') assert.match(viewSource, /v-model:unstructured-options="unstructuredOptions"/, '父页面没有双向绑定非结构化配置') assert.match(viewSource, /const DRAFT_SCHEMA_VERSION = \d+/, '草稿缺少版本标识') assert.match(viewSource, /schemaVersion:\s*DRAFT_SCHEMA_VERSION/, '草稿快照没有写入版本标识') assert.match(viewSource, /requiresPreviewMigration/, '旧草稿没有失效旧切片预览') +assert.doesNotMatch( + viewSource, + /snapshot\.unstructuredOptions\s*&&\s*!requiresPreviewMigration/, + '草稿版本迁移不应丢失仍受支持的非结构化配置', +) +assert.match(viewSource, /const restoredUnstructuredOptions = snapshot\.unstructuredOptions/, '旧草稿没有通过白名单恢复仍受支持的配置') assert.match(viewSource, /JSON\.stringify\(previewAffectingOptions\(\)\)/, '影响切分的非结构化配置没有纳入预览失效判断') const previewOptionsStart = viewSource.indexOf('function previewAffectingOptions()') @@ -254,14 +290,7 @@ for (const field of [ ]) { assert.ok(previewOptionsSource.includes(field), `预览签名缺少切分影响字段:${field}`) } -for (const field of [ - 'semanticEnrichment', - 'qaPairsPerChunk', - 'contextScope', - 'generationTypes', - 'skipUnanswerable', - 'datasetSplit', -]) { +for (const field of ['semanticEnrichment', 'qaPairsPerChunk', 'datasetSplit']) { assert.ok(!previewOptionsSource.includes(field), `生成字段 ${field} 不应导致预览重建并丢失编辑`) } @@ -269,14 +298,7 @@ const generationOptionsStart = viewSource.indexOf('function generationAffectingO const generationOptionsEnd = viewSource.indexOf('const generationOptionsSignature', generationOptionsStart) assert.ok(generationOptionsStart >= 0 && generationOptionsEnd > generationOptionsStart, '缺少生成影响配置签名函数') const generationOptionsSource = viewSource.slice(generationOptionsStart, generationOptionsEnd) -for (const field of [ - 'semanticEnrichment', - 'qaPairsPerChunk', - 'contextScope', - 'generationTypes', - 'skipUnanswerable', - 'datasetSplit', -]) { +for (const field of ['semanticEnrichment', 'qaPairsPerChunk', 'datasetSplit']) { assert.ok(generationOptionsSource.includes(field), `生成签名缺少字段:${field}`) } assert.match( @@ -318,9 +340,6 @@ const baseUnstructuredOptions = { preserveLists: false, semanticEnrichment: false, qaPairsPerChunk: 3, - contextScope: 'adjacent', - generationTypes: ['factual'], - skipUnanswerable: true, datasetSplit: { train: 80, validation: 10, test: 10 }, } @@ -624,25 +643,25 @@ for (const marker of [ '已添加 {{ uploadedFiles.length }} 个文件', ':title="file.name"', ]) { - assert.ok(taskSetupSource.includes(marker), `源数据文件列表缺少:${marker}`) + assert.ok(sourceUploadSource.includes(marker), `源数据文件列表缺少:${marker}`) } assert.match( - taskSetupSource, + sourceUploadSource, /^const[ \t]+FILE_PAGE_SIZE[ \t]*=[ \t]*10[ \t]*;?[ \t]*$/m, '文件分页大小必须固定为整数 10', ) assert.match( - taskSetupSource, + sourceUploadSource, /const pagedUploadedFiles\s*=\s*computed\(\(\)\s*=>\s*\{\s*const start = \(currentFilePage\.value - 1\) \* FILE_PAGE_SIZE\s*return props\.uploadedFiles\.slice\(start, start \+ FILE_PAGE_SIZE\)\s*\}\)/, '文件分页必须按当前页偏移切片完整文件列表', ) assert.match( - taskSetupSource, + sourceUploadSource, /watch\(\(\)\s*=>\s*props\.uploadedFiles\.length,\s*\(newLength, oldLength\)\s*=>\s*\{[\s\S]*?if \(newLength > oldLength\)\s*\{\s*currentFilePage\.value = totalPages[\s\S]*?\}[\s\S]*?currentFilePage\.value = Math\.min\(currentFilePage\.value, totalPages\)[\s\S]*?\}\)/, '文件数变化时必须新增跳至末页、删除回退到有效页', ) -const filePaginationTags = [...taskSetupSource.matchAll(//g)] +const filePaginationTags = [...sourceUploadSource.matchAll(//g)] assert.ok(filePaginationTags.length >= 1, '文件列表必须包含分页器') for (const [filePaginationTag] of filePaginationTags) { for (const attribute of [ @@ -655,8 +674,8 @@ for (const [filePaginationTag] of filePaginationTags) { } } -const { descriptor: taskSetupDescriptor } = parseSfc(taskSetupSource, { filename: taskSetupPath }) -const uploadedFileItemsStyles = taskSetupDescriptor.styles +const { descriptor: sourceUploadDescriptor } = parseSfc(sourceUploadSource, { filename: sourceUploadPath }) +const uploadedFileItemsStyles = sourceUploadDescriptor.styles .flatMap(({ content }) => collectStyleRules(content)) .filter(({ selector }) => /(?:^|[^\w-])\.uploaded-file-items(?![\w-])/.test(selector)) @@ -668,51 +687,48 @@ for (const { declarations } of uploadedFileItemsStyles) { ) } assert.match( - taskSetupSource, + sourceUploadSource, /\s*/, '有文件状态缺少带 aria-label="已上传文件列表" 的语义列表容器', ) assert.match( - taskSetupSource, + sourceUploadSource, /\s*已添加 \{\{ uploadedFiles\.length \}\} 个文件<\/span>/, '文件列表标题结构或文件数量文案缺失', ) assert.match( - taskSetupSource, + sourceUploadSource, /\s*]*class="uploaded-file">/, '文件列表缺少分页后的文件行容器', ) assert.match( - taskSetupSource, - //, + sourceUploadSource, + /]*>/, '无文件时未保留原有大拖拽上传区或上传配置', ) assert.match( - taskSetupSource, - /[\s\S]*?\s*\s*\{\{\s*processType === 'unstructured'\s*\? '支持 TXT、Markdown、PDF、Word、JSON、JSONL,单文件不超过 200MB'\s*:\s*'支持 JSON、JSONL、CSV、Excel,单文件不超过 200MB'\s*\}\}/, + sourceUploadSource, + /]*>[\s\S]*?\s*\s*\{\{\s*processType === 'unstructured'\s*\? '支持 TXT、Markdown、PDF、Word、JSON、JSONL,单文件不超过 200MB'\s*:\s*'支持 JSON、JSONL、CSV、Excel,单文件不超过 200MB'\s*\}\}/, '无文件时大拖拽上传区缺少格式提示槽、处理类型分支或完整格式提示', ) assert.match( - taskSetupSource, - /\s*已添加 \{\{ uploadedFiles\.length \}\} 个文件<\/span>\s*\s* emit\('file-change', file\)"\s*>\s*继续上传<\/el-button>\s*<\/el-upload>\s*<\/div>\s*<\/div>/, + sourceUploadSource, + /\s*已添加 \{\{ uploadedFiles\.length \}\} 个文件<\/span>\s*\s* emit\('file-change', file\)"[^>]*>\s*继续上传<\/el-button>\s*<\/el-upload>\s*<\/div>\s*<\/div>/, '有文件时缺少标题右侧的继续上传触发器或上传配置', ) assert.match( - taskSetupSource, + sourceUploadSource, /\.uploaded-file-list-header\s*\{[^}]*display:\s*flex[^}]*justify-content:\s*space-between/, '文件列表标题未布局为右侧继续上传按钮', ) assert.match( - taskSetupSource, + sourceUploadSource, /\.continue-upload\s+:deep\(\.el-upload\)\s*\{[^}]*width:\s*auto;?[^}]*margin-top:\s*0;?/, '继续上传未覆盖内层上传节点的宽度和顶部间距', ) -assert.match(taskSetupSource, /\.uploaded-file\s*\{[^}]*min-height:\s*48px/, '文件行没有保持 48px 最小高度') -assert.match( - taskSetupSource, - /[\s\S]*?\s*]*class="uploaded-file">[\s\S]*?\s*校验通过<\/span>\s*删除<\/el-button>\s*<\/div>\s*<\/div>/, - '文件行没有将成功状态与对应 remove-file 删除按钮关联', -) +assert.match(sourceUploadSource, /\.uploaded-file\s*\{[^}]*min-height:\s*48px/, '文件行没有保持 48px 最小高度') +assert.match(sourceUploadSource, /]*\/> 校验通过<\/span>/, '文件行缺少校验成功状态') +assert.match(sourceUploadSource, /@click="emit\('remove-file', file\.uid\)"/, '文件行缺少 remove-file 删除动作') const { descriptor } = parseSfc(viewSource, { filename: viewPath }) const template = descriptor.template?.content || '' @@ -728,4 +744,4 @@ assert.match( ) assert.match(previewSource, /height:\s*clamp\(560px,\s*calc\(100vh - 370px\),\s*720px\)/, '对照预览高度不足以展示切片正文') -console.log('数据处理四步向导回归检查通过') +console.log('数据处理五步向导回归检查通过') diff --git a/frontend/src/views/data-process/DataProcessCreateView.vue b/frontend/src/views/data-process/DataProcessCreateView.vue index 9d559bc..7ee04a3 100644 --- a/frontend/src/views/data-process/DataProcessCreateView.vue +++ b/frontend/src/views/data-process/DataProcessCreateView.vue @@ -4,6 +4,7 @@ import { onBeforeRouteLeave, useRouter } from 'vue-router' import { ElMessage, type UploadFile } from 'element-plus' import AppConfirmDialog from '@/components/AppConfirmDialog.vue' import TaskSetupStep from './create/TaskSetupStep.vue' +import SourceUploadStep from './create/SourceUploadStep.vue' import PreviewCompareStep from './create/PreviewCompareStep.vue' import GenerationStep from './create/GenerationStep.vue' import ResultEditorStep from './create/ResultEditorStep.vue' @@ -23,15 +24,17 @@ const router = useRouter() const taskSetupRef = ref>() const confirmDialogRef = ref>() const DRAFT_STORAGE_KEY = 'yg-data-process-create-draft' -const DRAFT_SCHEMA_VERSION = 2 +const DRAFT_SCHEMA_VERSION = 4 const PREVIEW_MODEL_VERSION = 'document-chunk-v2' const WIZARD_STEPS = [ - { id: 'create', title: '创建任务', desc: '填写任务信息与上传源数据' }, + { id: 'create', title: '创建任务', desc: '填写任务信息与处理配置' }, + { id: 'upload', title: '上传文件', desc: '上传或接入待处理的源数据' }, { id: 'preview', title: '数据预览', desc: '核对源文件与预览内容' }, { id: 'generate', title: '开始生成', desc: '确认摘要并启动处理' }, { id: 'results', title: '结果编辑与保存', desc: '检查、修改并保存结果' }, ] as const satisfies ReadonlyArray<{ id: StepId; title: string; desc: string }> +const LEGACY_V3_STEP_IDS: ReadonlyArray = ['create', 'preview', 'generate', 'results'] const currentStep = ref(0) const currentStepId = computed(() => WIZARD_STEPS[currentStep.value]?.id ?? 'create') @@ -62,9 +65,6 @@ const unstructuredOptions = ref({ preserveLists: true, semanticEnrichment: false, qaPairsPerChunk: 1, - contextScope: 'adjacent', - generationTypes: ['factual', 'concept', 'comprehensive'], - skipUnanswerable: true, datasetSplit: { train: 80, validation: 10, test: 10 }, }) interface UploadedDataFile { @@ -125,7 +125,8 @@ const previewFiles = computed(() => uploadedFiles.value.map((file) => { } })) const primaryActionLabel = computed(() => { - if (currentStepId.value === 'create') return '继续:数据预览' + if (currentStepId.value === 'create') return '继续:上传文件' + if (currentStepId.value === 'upload') return '继续:数据预览' if (currentStepId.value === 'preview') return '确认预览并继续' if (currentStepId.value === 'results') return '保存任务' if (generation.status === 'running') return '正在生成' @@ -144,10 +145,20 @@ const previousStepLabel = computed(() => currentStep.value > 0 ? WIZARD_STEPS[currentStep.value - 1].title : '') +function isStepId(value: unknown): value is StepId { + return typeof value === 'string' && WIZARD_STEPS.some((step) => step.id === value) +} + +function goToStep(stepId: StepId) { + const nextStepIndex = WIZARD_STEPS.findIndex((step) => step.id === stepId) + if (nextStepIndex >= 0) currentStep.value = nextStepIndex +} + function draftSnapshot() { return { schemaVersion: DRAFT_SCHEMA_VERSION, currentStep: currentStep.value, + currentStepId: currentStepId.value, task: { ...task }, processType: processType.value, structuredOptions: { @@ -158,7 +169,6 @@ function draftSnapshot() { unstructuredOptions: { ...unstructuredOptions.value, preprocessOptions: [...unstructuredOptions.value.preprocessOptions], - generationTypes: [...unstructuredOptions.value.generationTypes], datasetSplit: { ...unstructuredOptions.value.datasetSplit }, }, uploadedFiles: uploadedFiles.value, @@ -216,17 +226,11 @@ function generationAffectingOptions() { const { semanticEnrichment, qaPairsPerChunk, - contextScope, - generationTypes, - skipUnanswerable, datasetSplit, } = unstructuredOptions.value return { semanticEnrichment, qaPairsPerChunk, - contextScope, - generationTypes, - skipUnanswerable, datasetSplit, } } @@ -264,12 +268,21 @@ function restoreDraft() { if (!raw) return const snapshot = JSON.parse(raw) as DraftSnapshot if (!snapshot?.uploadedFiles && (!snapshot?.fileName || !snapshot?.sourceText)) return - const requiresPreviewMigration = snapshot.schemaVersion !== DRAFT_SCHEMA_VERSION + const snapshotSchemaVersion = Number(snapshot.schemaVersion) || 0 + const requiresPreviewMigration = snapshotSchemaVersion < 3 + const legacyStepId = snapshotSchemaVersion === 3 + ? LEGACY_V3_STEP_IDS[Math.min(Math.max(Number(snapshot.currentStep) || 0, 0), LEGACY_V3_STEP_IDS.length - 1)] + : undefined + const indexedStepId = WIZARD_STEPS[Math.min( + Math.max(Number(snapshot.currentStep) || 0, 0), + WIZARD_STEPS.length - 1, + )]?.id + const restoredStepId = isStepId(snapshot.currentStepId) + ? snapshot.currentStepId + : legacyStepId || indexedStepId || 'create' restoringDraft.value = true - currentStep.value = requiresPreviewMigration - ? 0 - : Math.min(Math.max(Number(snapshot.currentStep) || 0, 0), WIZARD_STEPS.length - 1) + goToStep(requiresPreviewMigration ? 'create' : restoredStepId) task.name = snapshot.task?.name || '' task.description = snapshot.task?.description || '' processType.value = snapshot.processType === 'structured' || snapshot.processType === 'external' @@ -289,18 +302,51 @@ function restoreDraft() { } } if (snapshot.unstructuredOptions) { + const restoredUnstructuredOptions = snapshot.unstructuredOptions + const restoredDatasetSplit = restoredUnstructuredOptions.datasetSplit unstructuredOptions.value = { ...unstructuredOptions.value, - ...snapshot.unstructuredOptions, - preprocessOptions: Array.isArray(snapshot.unstructuredOptions.preprocessOptions) - ? snapshot.unstructuredOptions.preprocessOptions + preprocessOptions: Array.isArray(restoredUnstructuredOptions.preprocessOptions) + ? restoredUnstructuredOptions.preprocessOptions : unstructuredOptions.value.preprocessOptions, - generationTypes: Array.isArray(snapshot.unstructuredOptions.generationTypes) - ? snapshot.unstructuredOptions.generationTypes - : unstructuredOptions.value.generationTypes, + chunkMethod: restoredUnstructuredOptions.chunkMethod || unstructuredOptions.value.chunkMethod, + chunkSize: Number.isFinite(restoredUnstructuredOptions.chunkSize) + ? restoredUnstructuredOptions.chunkSize + : unstructuredOptions.value.chunkSize, + chunkOverlap: Number.isFinite(restoredUnstructuredOptions.chunkOverlap) + ? restoredUnstructuredOptions.chunkOverlap + : unstructuredOptions.value.chunkOverlap, + minChunkSize: Number.isFinite(restoredUnstructuredOptions.minChunkSize) + ? restoredUnstructuredOptions.minChunkSize + : unstructuredOptions.value.minChunkSize, + customDelimiter: typeof restoredUnstructuredOptions.customDelimiter === 'string' + ? restoredUnstructuredOptions.customDelimiter + : unstructuredOptions.value.customDelimiter, + preserveTables: typeof restoredUnstructuredOptions.preserveTables === 'boolean' + ? restoredUnstructuredOptions.preserveTables + : unstructuredOptions.value.preserveTables, + preserveCodeBlocks: typeof restoredUnstructuredOptions.preserveCodeBlocks === 'boolean' + ? restoredUnstructuredOptions.preserveCodeBlocks + : unstructuredOptions.value.preserveCodeBlocks, + preserveLists: typeof restoredUnstructuredOptions.preserveLists === 'boolean' + ? restoredUnstructuredOptions.preserveLists + : unstructuredOptions.value.preserveLists, + semanticEnrichment: typeof restoredUnstructuredOptions.semanticEnrichment === 'boolean' + ? restoredUnstructuredOptions.semanticEnrichment + : unstructuredOptions.value.semanticEnrichment, + qaPairsPerChunk: Number.isFinite(restoredUnstructuredOptions.qaPairsPerChunk) + ? restoredUnstructuredOptions.qaPairsPerChunk + : unstructuredOptions.value.qaPairsPerChunk, datasetSplit: { - ...unstructuredOptions.value.datasetSplit, - ...snapshot.unstructuredOptions.datasetSplit, + train: Number.isFinite(restoredDatasetSplit?.train) + ? restoredDatasetSplit.train + : unstructuredOptions.value.datasetSplit.train, + validation: Number.isFinite(restoredDatasetSplit?.validation) + ? restoredDatasetSplit.validation + : unstructuredOptions.value.datasetSplit.validation, + test: Number.isFinite(restoredDatasetSplit?.test) + ? restoredDatasetSplit.test + : unstructuredOptions.value.datasetSplit.test, }, } } @@ -336,7 +382,15 @@ function restoreDraft() { || null results.value = !requiresPreviewMigration && Array.isArray(snapshot.results) ? snapshot.results : [] selectedResultId.value = requiresPreviewMigration ? null : snapshot.selectedResultId || results.value[0]?.id || null - if (!requiresPreviewMigration) Object.assign(generation, snapshot.generation || {}) + if (!requiresPreviewMigration) { + const restoredGeneration = snapshot.generation || {} + Object.assign(generation, restoredGeneration) + if (generation.status === 'running') { + generation.status = 'idle' + generation.progress = 0 + generation.message = '草稿已恢复,请重新开始生成。' + } + } dirty.value = false nextTick(() => { restoringDraft.value = false }) ElMessage.info(requiresPreviewMigration @@ -355,6 +409,12 @@ watch( { deep: true }, ) +watch(processType, (nextType, previousType) => { + if (restoringDraft.value || nextType === previousType || uploadedFiles.value.length === 0) return + resetSourceDataForProcessTypeChange() + ElMessage.info('处理类型已变更,请重新上传或拉取匹配的源数据') +}) + watch(generationOptionsSignature, (currentSignature, previousSignature) => { if (restoringDraft.value || currentSignature === previousSignature) return resetDownstream() @@ -418,8 +478,6 @@ function useSampleFile() { count: DEFAULT_SOURCE_TEXT.split('\n').filter((line) => line.trim()).length, content: DEFAULT_SOURCE_TEXT }] - if (!task.name) task.name = '金融问答清洗任务' - if (!task.description) task.description = '清洗金融领域问答数据,统一格式并生成高质量训练数据。' dirty.value = true } @@ -459,7 +517,6 @@ function handlePullData() { count: Math.min(externalSource.limit, DEFAULT_SOURCE_TEXT.split('\n').filter((line) => line.trim()).length), content: DEFAULT_SOURCE_TEXT, }) - if (!task.name) task.name = `${typeName} 数据拉取任务` dirty.value = true ElMessage.success(`已成功拉取 ${uploadedFiles.value[uploadedFiles.value.length - 1].count.toLocaleString()} 条数据`) }, 2000) @@ -477,6 +534,17 @@ function handleRemoveFile(uid: string | number) { } } +function resetSourceDataForProcessTypeChange() { + uploadedFiles.value = [] + previewSignature.value = '' + previewItems.value = [] + selectedPreviewFileId.value = null + selectedPreviewId.value = null + selectedPreviewIdsByFile.value = {} + externalConnected.value = false + resetDownstream() +} + function resetDownstream() { stopGenerationTimer() generation.status = 'idle' @@ -489,6 +557,10 @@ function resetDownstream() { async function nextFromCreate() { const valid = await taskSetupRef.value?.validate() if (!valid) return + goToStep('upload') +} + +function nextFromUpload() { if (uploadedFiles.value.length === 0) { ElMessage.warning(processType.value === 'external' ? '请先拉取至少一个数据源' : '请上传至少一个源数据文件') return @@ -512,7 +584,7 @@ async function nextFromCreate() { previewSignature.value = signature resetDownstream() } - currentStep.value = 1 + goToStep('preview') } function selectPreviewFile(fileId: string) { @@ -671,17 +743,21 @@ async function handlePrimaryAction() { await nextFromCreate() return } + if (currentStepId.value === 'upload') { + nextFromUpload() + return + } if (currentStepId.value === 'preview') { if (!previewItems.value.length) { ElMessage.warning('当前没有可生成的预览内容') return } - currentStep.value = 2 + goToStep('generate') return } if (currentStepId.value === 'generate') { if (generation.status === 'success') { - currentStep.value = 3 + goToStep('results') } else if (generation.status !== 'running') { startGeneration() } @@ -760,6 +836,7 @@ onMounted(restoreDraft) 'is-active': currentStep === index, 'is-completed': currentStep > index }" + :aria-current="currentStep === index ? 'step' : undefined" >
@@ -784,6 +861,11 @@ onMounted(restoreDraft) v-model:process-type="processType" v-model:structured-options="structuredOptions" v-model:unstructured-options="unstructuredOptions" + /> + + diff --git a/frontend/src/views/data-process/create/SourceUploadStep.vue b/frontend/src/views/data-process/create/SourceUploadStep.vue new file mode 100644 index 0000000..74bbf99 --- /dev/null +++ b/frontend/src/views/data-process/create/SourceUploadStep.vue @@ -0,0 +1,565 @@ + + + + + diff --git a/frontend/src/views/data-process/create/TaskSetupStep.vue b/frontend/src/views/data-process/create/TaskSetupStep.vue index e1e5b5c..f03f6b2 100644 --- a/frontend/src/views/data-process/create/TaskSetupStep.vue +++ b/frontend/src/views/data-process/create/TaskSetupStep.vue @@ -1,14 +1,11 @@