refactor: 数据处理向导拆分源数据上传步骤
将任务设置中的文件上传与外来数据源拉取抽离为独立 SourceUploadStep 组件,TaskSetupStep 聚焦任务信息与处理配置,CreateView 同步接入新步骤与草稿同步,回归脚本补充上传步骤断言。
This commit is contained in:
@@ -34,9 +34,14 @@ assert.match(viewSource, /onBeforeRouteLeave\(async \(\) =>/, '路由离开确
|
|||||||
assert.doesNotMatch(viewSource, /window\.confirm|ElMessageBox/, '创建页仍在使用系统或 Element Plus 确认框')
|
assert.doesNotMatch(viewSource, /window\.confirm|ElMessageBox/, '创建页仍在使用系统或 Element Plus 确认框')
|
||||||
|
|
||||||
assert.match(viewSource, /const WIZARD_STEPS = \[/, '向导步骤尚未改为固定常量')
|
assert.match(viewSource, /const WIZARD_STEPS = \[/, '向导步骤尚未改为固定常量')
|
||||||
for (const title of ['创建任务', '数据预览', '开始生成', '结果编辑与保存']) {
|
for (const title of ['创建任务', '上传文件', '数据预览', '开始生成', '结果编辑与保存']) {
|
||||||
assert.ok(viewSource.includes(`title: '${title}'`), `缺少固定步骤:${title}`)
|
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.doesNotMatch(viewSource, /steps\s*=\s*computed|all\.filter/, '步骤仍根据处理类型动态增减')
|
||||||
assert.match(viewSource, /localStorage\.setItem\(DRAFT_STORAGE_KEY/, '草稿没有持久化')
|
assert.match(viewSource, /localStorage\.setItem\(DRAFT_STORAGE_KEY/, '草稿没有持久化')
|
||||||
assert.match(viewSource, /localStorage\.getItem\(DRAFT_STORAGE_KEY\)/, '草稿没有恢复读取')
|
assert.match(viewSource, /localStorage\.getItem\(DRAFT_STORAGE_KEY\)/, '草稿没有恢复读取')
|
||||||
@@ -44,6 +49,7 @@ assert.match(viewSource, /restoreDraft\(\)/, '页面没有恢复草稿')
|
|||||||
|
|
||||||
const expectedComponents = [
|
const expectedComponents = [
|
||||||
'TaskSetupStep.vue',
|
'TaskSetupStep.vue',
|
||||||
|
'SourceUploadStep.vue',
|
||||||
'PreviewCompareStep.vue',
|
'PreviewCompareStep.vue',
|
||||||
'GenerationStep.vue',
|
'GenerationStep.vue',
|
||||||
'ResultEditorStep.vue',
|
'ResultEditorStep.vue',
|
||||||
@@ -68,6 +74,7 @@ for (const field of ['sourceStart', 'sourceEnd', 'originalContent', 'editedConte
|
|||||||
assert.ok(typesSource.includes(field), `PreviewItem 缺少字段:${field}`)
|
assert.ok(typesSource.includes(field), `PreviewItem 缺少字段:${field}`)
|
||||||
}
|
}
|
||||||
assert.match(typesSource, /sourceFileId/, 'PreviewItem 缺少来源文件标识')
|
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 buildPreviewItems/, '缺少切片来源映射生成函数')
|
||||||
assert.match(modelSource, /export function sourceLines/, '缺少源文件行偏移生成函数')
|
assert.match(modelSource, /export function sourceLines/, '缺少源文件行偏移生成函数')
|
||||||
assert.match(modelSource, /sourceFileId/, '切片生成没有写入来源文件标识')
|
assert.match(modelSource, /sourceFileId/, '切片生成没有写入来源文件标识')
|
||||||
@@ -87,10 +94,10 @@ for (const marker of [
|
|||||||
'preview-editor',
|
'preview-editor',
|
||||||
'scrollIntoView',
|
'scrollIntoView',
|
||||||
]) {
|
]) {
|
||||||
assert.ok(previewSource.includes(marker), `第二步缺少结构或行为:${marker}`)
|
assert.ok(previewSource.includes(marker), `第三步缺少结构或行为:${marker}`)
|
||||||
}
|
}
|
||||||
assert.match(previewSource, /sourceStart/, '第二步未使用来源起始偏移')
|
assert.match(previewSource, /sourceStart/, '第三步未使用来源起始偏移')
|
||||||
assert.match(previewSource, /sourceEnd/, '第二步未使用来源结束偏移')
|
assert.match(previewSource, /sourceEnd/, '第三步未使用来源结束偏移')
|
||||||
assert.match(previewSource, /filterable/, '文件选择器必须可搜索')
|
assert.match(previewSource, /filterable/, '文件选择器必须可搜索')
|
||||||
assert.match(previewSource, /当前文件/, '预览缺少当前文件切换器')
|
assert.match(previewSource, /当前文件/, '预览缺少当前文件切换器')
|
||||||
assert.doesNotMatch(previewSource, /located-badge|sync-label|已定位到/, '源文件栏不应显示冗余定位提示')
|
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.match(previewSource, /\.editor-actions\s*\{[\s\S]*?justify-content:\s*flex-end/, '取消和保存按钮必须在编辑器右侧对齐')
|
||||||
assert.doesNotMatch(previewSource, /item-token|item-status|modifiedOnly|仅看已修改/, '切片列表不应再显示 Token 或修改状态')
|
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, /\.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 taskSetupPath = path.join(createDir, 'TaskSetupStep.vue')
|
||||||
const taskSetupSource = await readFile(taskSetupPath, 'utf8')
|
const taskSetupSource = await readFile(taskSetupPath, 'utf8')
|
||||||
|
const sourceUploadPath = path.join(createDir, 'SourceUploadStep.vue')
|
||||||
|
const sourceUploadSource = await readFile(sourceUploadPath, 'utf8')
|
||||||
|
|
||||||
|
for (const marker of ['<el-upload', '源数据上传', '数据源配置', 'uploadedFiles']) {
|
||||||
|
assert.ok(!taskSetupSource.includes(marker), `第一步仍包含上传职责:${marker}`)
|
||||||
|
}
|
||||||
|
assert.match(viewSource, /<SourceUploadStep\s+[\s\S]*?v-else-if="currentStepId === 'upload'"/, '第二步没有挂载独立上传组件')
|
||||||
|
assert.match(viewSource, /if \(currentStepId\.value === 'create'\) return '继续:上传文件'/, '第一步主按钮没有指向上传文件')
|
||||||
|
assert.match(viewSource, /if \(currentStepId\.value === 'upload'\) return '继续:数据预览'/, '第二步主按钮没有指向数据预览')
|
||||||
|
assert.match(viewSource, /const DRAFT_SCHEMA_VERSION = 4/, '五步索引变更后必须升级草稿版本')
|
||||||
|
|
||||||
|
const nextFromCreateStart = viewSource.indexOf('async function nextFromCreate()')
|
||||||
|
const nextFromUploadStart = viewSource.indexOf('function nextFromUpload()', nextFromCreateStart)
|
||||||
|
const selectPreviewFileStart = viewSource.indexOf('function selectPreviewFile(', nextFromUploadStart)
|
||||||
|
assert.ok(nextFromCreateStart >= 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, /<ResultEditorStep\s+[\s\S]*?v-else-if="currentStepId === 'results'"/, '结果编辑器必须只在结果步骤渲染')
|
||||||
|
assert.match(viewSource, /watch\(processType,[\s\S]*?resetSourceDataForProcessTypeChange\(\)/, '切换处理类型后没有失效旧源数据')
|
||||||
|
assert.match(viewSource, /function resetSourceDataForProcessTypeChange\(\)[\s\S]*?uploadedFiles\.value = \[\][\s\S]*?selectedPreviewFileId\.value = null/, '旧源数据失效没有同步清理文件与预览选择')
|
||||||
|
|
||||||
assert.match(taskSetupSource, /v-if="processType === 'structured'"/, '结构化配置必须仅在结构化数据类型下显示')
|
assert.match(taskSetupSource, /v-if="processType === 'structured'"/, '结构化配置必须仅在结构化数据类型下显示')
|
||||||
for (const option of [
|
for (const option of [
|
||||||
@@ -169,58 +208,50 @@ for (const field of [
|
|||||||
'preserveLists',
|
'preserveLists',
|
||||||
'semanticEnrichment',
|
'semanticEnrichment',
|
||||||
'qaPairsPerChunk',
|
'qaPairsPerChunk',
|
||||||
'contextScope',
|
|
||||||
'generationTypes',
|
|
||||||
'skipUnanswerable',
|
|
||||||
'datasetSplit',
|
'datasetSplit',
|
||||||
]) {
|
]) {
|
||||||
assert.ok(typesSource.includes(field), `非结构化处理选项缺少字段:${field}`)
|
assert.ok(typesSource.includes(field), `非结构化处理选项缺少字段:${field}`)
|
||||||
}
|
}
|
||||||
|
for (const removedField of ['contextScope', 'generationTypes', 'skipUnanswerable']) {
|
||||||
|
assert.ok(!typesSource.includes(removedField), `简化后仍保留低频生成字段:${removedField}`)
|
||||||
|
}
|
||||||
|
|
||||||
assert.match(taskSetupSource, /v-if="processType === 'unstructured'"/, '非结构化配置必须仅在非结构化数据类型下显示')
|
assert.match(taskSetupSource, /v-if="processType === 'unstructured'"/, '非结构化配置必须仅在非结构化数据类型下显示')
|
||||||
for (const option of [
|
assert.ok(taskSetupSource.includes('智能预处理'), '简化后缺少智能预处理总开关')
|
||||||
'清理无效内容',
|
assert.ok(taskSetupSource.includes('敏感信息脱敏'), '简化后缺少脱敏开关')
|
||||||
'识别文档结构',
|
assert.match(taskSetupSource, /const smartPreprocessEnabled = computed/, '智能预处理没有映射到内部处理项')
|
||||||
'合并过短内容',
|
assert.match(taskSetupSource, /function updateSmartPreprocess/, '智能预处理开关缺少更新逻辑')
|
||||||
'过滤低质量内容',
|
assert.match(taskSetupSource, /function updateDesensitize/, '脱敏开关缺少更新逻辑')
|
||||||
'重复内容去重',
|
|
||||||
'敏感信息脱敏',
|
|
||||||
'保留上下文信息',
|
|
||||||
]) {
|
|
||||||
assert.ok(taskSetupSource.includes(option), `非结构化预处理缺少选项:${option}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.ok(taskSetupSource.includes('切分选项'), '非结构化配置缺少切分选项分类')
|
assert.ok(taskSetupSource.includes('切分选项'), '非结构化配置缺少切分选项分类')
|
||||||
for (const method of ['自动语义切分', '按标题和段落', '按固定长度', '自定义分隔符']) {
|
for (const method of ['自动语义切分', '按标题和段落', '按固定长度', '自定义分隔符']) {
|
||||||
assert.ok(taskSetupSource.includes(method), `切分方式缺少选项:${method}`)
|
assert.ok(taskSetupSource.includes(method), `切分方式缺少选项:${method}`)
|
||||||
}
|
}
|
||||||
for (const label of ['切片长度', '重叠长度', '最小切片长度', '完整保留表格', '完整保留代码块', '完整保留列表']) {
|
for (const label of ['切片长度', '重叠长度', '最小切片长度', '保护表格、代码和列表']) {
|
||||||
assert.ok(taskSetupSource.includes(label), `切分选项缺少配置:${label}`)
|
assert.ok(taskSetupSource.includes(label), `切分选项缺少配置:${label}`)
|
||||||
}
|
}
|
||||||
|
assert.ok(taskSetupSource.includes('高级设置'), '切分选项缺少渐进式高级设置入口')
|
||||||
|
assert.match(taskSetupSource, /:aria-expanded="advancedChunkSettingsOpen"/, '高级设置入口缺少展开状态的可访问性标记')
|
||||||
|
assert.match(taskSetupSource, /v-if="advancedChunkSettingsOpen"/, '高级设置内容没有默认收起')
|
||||||
|
assert.match(taskSetupSource, /watch\(\(\) => 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\.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\.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')
|
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}`)
|
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, /unstructuredOptions\.qaPairsPerChunk[\s\S]*?:min="1"[\s\S]*?:max="3"/, '每个切片生成数量必须限制在 1 到 3')
|
||||||
assert.match(taskSetupSource, /unstructuredSplitTotal\.value !== 100/, '非结构化数据集划分缺少总和 100% 校验')
|
assert.match(taskSetupSource, /unstructuredSplitTotal\.value !== 100/, '非结构化数据集划分缺少总和 100% 校验')
|
||||||
assert.match(taskSetupSource, /chunkOverlap \+ props\.unstructuredOptions\.minChunkSize[\s\S]*?> props\.unstructuredOptions\.chunkSize/, '切分配置未校验重叠长度与最小切片长度的组合边界')
|
assert.match(taskSetupSource, /chunkOverlap \+ props\.unstructuredOptions\.minChunkSize[\s\S]*?> props\.unstructuredOptions\.chunkSize/, '切分配置未校验重叠长度与最小切片长度的组合边界')
|
||||||
assert.ok(taskSetupSource.includes('Token 数为轻量估算值'), '切片长度缺少 Token 估算说明')
|
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<UnstructuredProcessOptions>/, '父页面缺少非结构化配置状态')
|
assert.match(viewSource, /const unstructuredOptions = ref<UnstructuredProcessOptions>/, '父页面缺少非结构化配置状态')
|
||||||
assert.match(viewSource, /chunkMethod:\s*'semantic'/, '非结构化默认切分方式必须为自动语义切分')
|
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, /chunkOverlap:\s*100/, '默认重叠长度必须为 100 Token')
|
||||||
assert.match(viewSource, /minChunkSize:\s*100/, '默认最小切片长度必须为 100 Token')
|
assert.match(viewSource, /minChunkSize:\s*100/, '默认最小切片长度必须为 100 Token')
|
||||||
assert.match(viewSource, /qaPairsPerChunk:\s*1/, '默认每个切片必须生成 1 个问答对')
|
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:\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, /v-model:unstructured-options="unstructuredOptions"/, '父页面没有双向绑定非结构化配置')
|
||||||
assert.match(viewSource, /const DRAFT_SCHEMA_VERSION = \d+/, '草稿缺少版本标识')
|
assert.match(viewSource, /const DRAFT_SCHEMA_VERSION = \d+/, '草稿缺少版本标识')
|
||||||
assert.match(viewSource, /schemaVersion:\s*DRAFT_SCHEMA_VERSION/, '草稿快照没有写入版本标识')
|
assert.match(viewSource, /schemaVersion:\s*DRAFT_SCHEMA_VERSION/, '草稿快照没有写入版本标识')
|
||||||
assert.match(viewSource, /requiresPreviewMigration/, '旧草稿没有失效旧切片预览')
|
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\(\)\)/, '影响切分的非结构化配置没有纳入预览失效判断')
|
assert.match(viewSource, /JSON\.stringify\(previewAffectingOptions\(\)\)/, '影响切分的非结构化配置没有纳入预览失效判断')
|
||||||
|
|
||||||
const previewOptionsStart = viewSource.indexOf('function previewAffectingOptions()')
|
const previewOptionsStart = viewSource.indexOf('function previewAffectingOptions()')
|
||||||
@@ -254,14 +290,7 @@ for (const field of [
|
|||||||
]) {
|
]) {
|
||||||
assert.ok(previewOptionsSource.includes(field), `预览签名缺少切分影响字段:${field}`)
|
assert.ok(previewOptionsSource.includes(field), `预览签名缺少切分影响字段:${field}`)
|
||||||
}
|
}
|
||||||
for (const field of [
|
for (const field of ['semanticEnrichment', 'qaPairsPerChunk', 'datasetSplit']) {
|
||||||
'semanticEnrichment',
|
|
||||||
'qaPairsPerChunk',
|
|
||||||
'contextScope',
|
|
||||||
'generationTypes',
|
|
||||||
'skipUnanswerable',
|
|
||||||
'datasetSplit',
|
|
||||||
]) {
|
|
||||||
assert.ok(!previewOptionsSource.includes(field), `生成字段 ${field} 不应导致预览重建并丢失编辑`)
|
assert.ok(!previewOptionsSource.includes(field), `生成字段 ${field} 不应导致预览重建并丢失编辑`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,14 +298,7 @@ const generationOptionsStart = viewSource.indexOf('function generationAffectingO
|
|||||||
const generationOptionsEnd = viewSource.indexOf('const generationOptionsSignature', generationOptionsStart)
|
const generationOptionsEnd = viewSource.indexOf('const generationOptionsSignature', generationOptionsStart)
|
||||||
assert.ok(generationOptionsStart >= 0 && generationOptionsEnd > generationOptionsStart, '缺少生成影响配置签名函数')
|
assert.ok(generationOptionsStart >= 0 && generationOptionsEnd > generationOptionsStart, '缺少生成影响配置签名函数')
|
||||||
const generationOptionsSource = viewSource.slice(generationOptionsStart, generationOptionsEnd)
|
const generationOptionsSource = viewSource.slice(generationOptionsStart, generationOptionsEnd)
|
||||||
for (const field of [
|
for (const field of ['semanticEnrichment', 'qaPairsPerChunk', 'datasetSplit']) {
|
||||||
'semanticEnrichment',
|
|
||||||
'qaPairsPerChunk',
|
|
||||||
'contextScope',
|
|
||||||
'generationTypes',
|
|
||||||
'skipUnanswerable',
|
|
||||||
'datasetSplit',
|
|
||||||
]) {
|
|
||||||
assert.ok(generationOptionsSource.includes(field), `生成签名缺少字段:${field}`)
|
assert.ok(generationOptionsSource.includes(field), `生成签名缺少字段:${field}`)
|
||||||
}
|
}
|
||||||
assert.match(
|
assert.match(
|
||||||
@@ -318,9 +340,6 @@ const baseUnstructuredOptions = {
|
|||||||
preserveLists: false,
|
preserveLists: false,
|
||||||
semanticEnrichment: false,
|
semanticEnrichment: false,
|
||||||
qaPairsPerChunk: 3,
|
qaPairsPerChunk: 3,
|
||||||
contextScope: 'adjacent',
|
|
||||||
generationTypes: ['factual'],
|
|
||||||
skipUnanswerable: true,
|
|
||||||
datasetSplit: { train: 80, validation: 10, test: 10 },
|
datasetSplit: { train: 80, validation: 10, test: 10 },
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -624,25 +643,25 @@ for (const marker of [
|
|||||||
'已添加 {{ uploadedFiles.length }} 个文件',
|
'已添加 {{ uploadedFiles.length }} 个文件',
|
||||||
':title="file.name"',
|
':title="file.name"',
|
||||||
]) {
|
]) {
|
||||||
assert.ok(taskSetupSource.includes(marker), `源数据文件列表缺少:${marker}`)
|
assert.ok(sourceUploadSource.includes(marker), `源数据文件列表缺少:${marker}`)
|
||||||
}
|
}
|
||||||
assert.match(
|
assert.match(
|
||||||
taskSetupSource,
|
sourceUploadSource,
|
||||||
/^const[ \t]+FILE_PAGE_SIZE[ \t]*=[ \t]*10[ \t]*;?[ \t]*$/m,
|
/^const[ \t]+FILE_PAGE_SIZE[ \t]*=[ \t]*10[ \t]*;?[ \t]*$/m,
|
||||||
'文件分页大小必须固定为整数 10',
|
'文件分页大小必须固定为整数 10',
|
||||||
)
|
)
|
||||||
assert.match(
|
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*\}\)/,
|
/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(
|
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]*?\}\)/,
|
/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(/<el-pagination\b[\s\S]*?\/>/g)]
|
const filePaginationTags = [...sourceUploadSource.matchAll(/<el-pagination\b[\s\S]*?\/>/g)]
|
||||||
assert.ok(filePaginationTags.length >= 1, '文件列表必须包含分页器')
|
assert.ok(filePaginationTags.length >= 1, '文件列表必须包含分页器')
|
||||||
for (const [filePaginationTag] of filePaginationTags) {
|
for (const [filePaginationTag] of filePaginationTags) {
|
||||||
for (const attribute of [
|
for (const attribute of [
|
||||||
@@ -655,8 +674,8 @@ for (const [filePaginationTag] of filePaginationTags) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { descriptor: taskSetupDescriptor } = parseSfc(taskSetupSource, { filename: taskSetupPath })
|
const { descriptor: sourceUploadDescriptor } = parseSfc(sourceUploadSource, { filename: sourceUploadPath })
|
||||||
const uploadedFileItemsStyles = taskSetupDescriptor.styles
|
const uploadedFileItemsStyles = sourceUploadDescriptor.styles
|
||||||
.flatMap(({ content }) => collectStyleRules(content))
|
.flatMap(({ content }) => collectStyleRules(content))
|
||||||
.filter(({ selector }) => /(?:^|[^\w-])\.uploaded-file-items(?![\w-])/.test(selector))
|
.filter(({ selector }) => /(?:^|[^\w-])\.uploaded-file-items(?![\w-])/.test(selector))
|
||||||
|
|
||||||
@@ -668,51 +687,48 @@ for (const { declarations } of uploadedFileItemsStyles) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
assert.match(
|
assert.match(
|
||||||
taskSetupSource,
|
sourceUploadSource,
|
||||||
/<el-upload\s+v-if="uploadedFiles\.length === 0"[\s\S]*?<\/el-upload>\s*<section\s+v-else\s+class="uploaded-file-list"\s+aria-label="已上传文件列表">/,
|
/<el-upload\s+v-if="uploadedFiles\.length === 0"[\s\S]*?<\/el-upload>\s*<section\s+v-else\s+class="uploaded-file-list"\s+aria-label="已上传文件列表">/,
|
||||||
'有文件状态缺少带 aria-label="已上传文件列表" 的语义列表容器',
|
'有文件状态缺少带 aria-label="已上传文件列表" 的语义列表容器',
|
||||||
)
|
)
|
||||||
assert.match(
|
assert.match(
|
||||||
taskSetupSource,
|
sourceUploadSource,
|
||||||
/<div\s+class="uploaded-file-list-header">\s*<span>已添加 \{\{ uploadedFiles\.length \}\} 个文件<\/span>/,
|
/<div\s+class="uploaded-file-list-header">\s*<span>已添加 \{\{ uploadedFiles\.length \}\} 个文件<\/span>/,
|
||||||
'文件列表标题结构或文件数量文案缺失',
|
'文件列表标题结构或文件数量文案缺失',
|
||||||
)
|
)
|
||||||
assert.match(
|
assert.match(
|
||||||
taskSetupSource,
|
sourceUploadSource,
|
||||||
/<div\s+class="uploaded-file-items">\s*<div\s+v-for="file in pagedUploadedFiles"[^>]*class="uploaded-file">/,
|
/<div\s+class="uploaded-file-items">\s*<div\s+v-for="file in pagedUploadedFiles"[^>]*class="uploaded-file">/,
|
||||||
'文件列表缺少分页后的文件行容器',
|
'文件列表缺少分页后的文件行容器',
|
||||||
)
|
)
|
||||||
assert.match(
|
assert.match(
|
||||||
taskSetupSource,
|
sourceUploadSource,
|
||||||
/<el-upload\s+v-if="uploadedFiles\.length === 0"\s+drag\s+multiple\s+:accept="uploadAccept"\s+:auto-upload="false"\s+:show-file-list="false"\s+:on-change="\(file: UploadFile\) => emit\('file-change', file\)"\s*>/,
|
/<el-upload\s+v-if="uploadedFiles\.length === 0"\s+drag\s+multiple\s+:accept="uploadAccept"\s+:auto-upload="false"\s+:show-file-list="false"\s+:on-change="\(file: UploadFile\) => emit\('file-change', file\)"[^>]*>/,
|
||||||
'无文件时未保留原有大拖拽上传区或上传配置',
|
'无文件时未保留原有大拖拽上传区或上传配置',
|
||||||
)
|
)
|
||||||
assert.match(
|
assert.match(
|
||||||
taskSetupSource,
|
sourceUploadSource,
|
||||||
/<el-upload\s+v-if="uploadedFiles\.length === 0"\s+drag\s+multiple\s+:accept="uploadAccept"\s+:auto-upload="false"\s+:show-file-list="false"\s+:on-change="\(file: UploadFile\) => emit\('file-change', file\)"\s*>[\s\S]*?<template\s+#tip>\s*<div\s+class="el-upload__tip">\s*\{\{\s*processType === 'unstructured'\s*\? '支持 TXT、Markdown、PDF、Word、JSON、JSONL,单文件不超过 200MB'\s*:\s*'支持 JSON、JSONL、CSV、Excel,单文件不超过 200MB'\s*\}\}/,
|
/<el-upload\s+v-if="uploadedFiles\.length === 0"\s+drag\s+multiple\s+:accept="uploadAccept"\s+:auto-upload="false"\s+:show-file-list="false"\s+:on-change="\(file: UploadFile\) => emit\('file-change', file\)"[^>]*>[\s\S]*?<template\s+#tip>\s*<div\s+class="el-upload__tip">\s*\{\{\s*processType === 'unstructured'\s*\? '支持 TXT、Markdown、PDF、Word、JSON、JSONL,单文件不超过 200MB'\s*:\s*'支持 JSON、JSONL、CSV、Excel,单文件不超过 200MB'\s*\}\}/,
|
||||||
'无文件时大拖拽上传区缺少格式提示槽、处理类型分支或完整格式提示',
|
'无文件时大拖拽上传区缺少格式提示槽、处理类型分支或完整格式提示',
|
||||||
)
|
)
|
||||||
assert.match(
|
assert.match(
|
||||||
taskSetupSource,
|
sourceUploadSource,
|
||||||
/<div\s+class="uploaded-file-list-header">\s*<span>已添加 \{\{ uploadedFiles\.length \}\} 个文件<\/span>\s*<div\s+class="continue-upload">\s*<el-upload\s+multiple\s+:accept="uploadAccept"\s+:auto-upload="false"\s+:show-file-list="false"\s+:on-change="\(file: UploadFile\) => emit\('file-change', file\)"\s*>\s*<el-button\s+size="small"\s+type="primary">继续上传<\/el-button>\s*<\/el-upload>\s*<\/div>\s*<\/div>/,
|
/<div\s+class="uploaded-file-list-header">\s*<span>已添加 \{\{ uploadedFiles\.length \}\} 个文件<\/span>\s*<div\s+class="continue-upload">\s*<el-upload\s+multiple\s+:accept="uploadAccept"\s+:auto-upload="false"\s+:show-file-list="false"\s+:on-change="\(file: UploadFile\) => emit\('file-change', file\)"[^>]*>\s*<el-button\s+size="small"\s+type="primary">继续上传<\/el-button>\s*<\/el-upload>\s*<\/div>\s*<\/div>/,
|
||||||
'有文件时缺少标题右侧的继续上传触发器或上传配置',
|
'有文件时缺少标题右侧的继续上传触发器或上传配置',
|
||||||
)
|
)
|
||||||
assert.match(
|
assert.match(
|
||||||
taskSetupSource,
|
sourceUploadSource,
|
||||||
/\.uploaded-file-list-header\s*\{[^}]*display:\s*flex[^}]*justify-content:\s*space-between/,
|
/\.uploaded-file-list-header\s*\{[^}]*display:\s*flex[^}]*justify-content:\s*space-between/,
|
||||||
'文件列表标题未布局为右侧继续上传按钮',
|
'文件列表标题未布局为右侧继续上传按钮',
|
||||||
)
|
)
|
||||||
assert.match(
|
assert.match(
|
||||||
taskSetupSource,
|
sourceUploadSource,
|
||||||
/\.continue-upload\s+:deep\(\.el-upload\)\s*\{[^}]*width:\s*auto;?[^}]*margin-top:\s*0;?/,
|
/\.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(sourceUploadSource, /\.uploaded-file\s*\{[^}]*min-height:\s*48px/, '文件行没有保持 48px 最小高度')
|
||||||
assert.match(
|
assert.match(sourceUploadSource, /<span class="file-status"><i class="fa fa-check-circle"[^>]*\/> 校验通过<\/span>/, '文件行缺少校验成功状态')
|
||||||
taskSetupSource,
|
assert.match(sourceUploadSource, /@click="emit\('remove-file', file\.uid\)"/, '文件行缺少 remove-file 删除动作')
|
||||||
/<section\s+v-else\s+class="uploaded-file-list"\s+aria-label="已上传文件列表">[\s\S]*?<div\s+class="uploaded-file-items">\s*<div\s+v-for="file in pagedUploadedFiles"[^>]*class="uploaded-file">[\s\S]*?<span\s+class="file-status"><i\s+class="fa fa-check-circle"\s*\/>\s*校验通过<\/span>\s*<el-button\s+link\s+type="danger"\s+@click="emit\('remove-file', file\.uid\)">删除<\/el-button>\s*<\/div>\s*<\/div>/,
|
|
||||||
'文件行没有将成功状态与对应 remove-file 删除按钮关联',
|
|
||||||
)
|
|
||||||
|
|
||||||
const { descriptor } = parseSfc(viewSource, { filename: viewPath })
|
const { descriptor } = parseSfc(viewSource, { filename: viewPath })
|
||||||
const template = descriptor.template?.content || ''
|
const template = descriptor.template?.content || ''
|
||||||
@@ -728,4 +744,4 @@ assert.match(
|
|||||||
)
|
)
|
||||||
assert.match(previewSource, /height:\s*clamp\(560px,\s*calc\(100vh - 370px\),\s*720px\)/, '对照预览高度不足以展示切片正文')
|
assert.match(previewSource, /height:\s*clamp\(560px,\s*calc\(100vh - 370px\),\s*720px\)/, '对照预览高度不足以展示切片正文')
|
||||||
|
|
||||||
console.log('数据处理四步向导回归检查通过')
|
console.log('数据处理五步向导回归检查通过')
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { onBeforeRouteLeave, useRouter } from 'vue-router'
|
|||||||
import { ElMessage, type UploadFile } from 'element-plus'
|
import { ElMessage, type UploadFile } from 'element-plus'
|
||||||
import AppConfirmDialog from '@/components/AppConfirmDialog.vue'
|
import AppConfirmDialog from '@/components/AppConfirmDialog.vue'
|
||||||
import TaskSetupStep from './create/TaskSetupStep.vue'
|
import TaskSetupStep from './create/TaskSetupStep.vue'
|
||||||
|
import SourceUploadStep from './create/SourceUploadStep.vue'
|
||||||
import PreviewCompareStep from './create/PreviewCompareStep.vue'
|
import PreviewCompareStep from './create/PreviewCompareStep.vue'
|
||||||
import GenerationStep from './create/GenerationStep.vue'
|
import GenerationStep from './create/GenerationStep.vue'
|
||||||
import ResultEditorStep from './create/ResultEditorStep.vue'
|
import ResultEditorStep from './create/ResultEditorStep.vue'
|
||||||
@@ -23,15 +24,17 @@ const router = useRouter()
|
|||||||
const taskSetupRef = ref<InstanceType<typeof TaskSetupStep>>()
|
const taskSetupRef = ref<InstanceType<typeof TaskSetupStep>>()
|
||||||
const confirmDialogRef = ref<InstanceType<typeof AppConfirmDialog>>()
|
const confirmDialogRef = ref<InstanceType<typeof AppConfirmDialog>>()
|
||||||
const DRAFT_STORAGE_KEY = 'yg-data-process-create-draft'
|
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 PREVIEW_MODEL_VERSION = 'document-chunk-v2'
|
||||||
|
|
||||||
const WIZARD_STEPS = [
|
const WIZARD_STEPS = [
|
||||||
{ id: 'create', title: '创建任务', desc: '填写任务信息与上传源数据' },
|
{ id: 'create', title: '创建任务', desc: '填写任务信息与处理配置' },
|
||||||
|
{ id: 'upload', title: '上传文件', desc: '上传或接入待处理的源数据' },
|
||||||
{ id: 'preview', title: '数据预览', desc: '核对源文件与预览内容' },
|
{ id: 'preview', title: '数据预览', desc: '核对源文件与预览内容' },
|
||||||
{ id: 'generate', title: '开始生成', desc: '确认摘要并启动处理' },
|
{ id: 'generate', title: '开始生成', desc: '确认摘要并启动处理' },
|
||||||
{ id: 'results', title: '结果编辑与保存', desc: '检查、修改并保存结果' },
|
{ id: 'results', title: '结果编辑与保存', desc: '检查、修改并保存结果' },
|
||||||
] as const satisfies ReadonlyArray<{ id: StepId; title: string; desc: string }>
|
] as const satisfies ReadonlyArray<{ id: StepId; title: string; desc: string }>
|
||||||
|
const LEGACY_V3_STEP_IDS: ReadonlyArray<StepId> = ['create', 'preview', 'generate', 'results']
|
||||||
|
|
||||||
const currentStep = ref(0)
|
const currentStep = ref(0)
|
||||||
const currentStepId = computed<StepId>(() => WIZARD_STEPS[currentStep.value]?.id ?? 'create')
|
const currentStepId = computed<StepId>(() => WIZARD_STEPS[currentStep.value]?.id ?? 'create')
|
||||||
@@ -62,9 +65,6 @@ const unstructuredOptions = ref<UnstructuredProcessOptions>({
|
|||||||
preserveLists: true,
|
preserveLists: true,
|
||||||
semanticEnrichment: false,
|
semanticEnrichment: false,
|
||||||
qaPairsPerChunk: 1,
|
qaPairsPerChunk: 1,
|
||||||
contextScope: 'adjacent',
|
|
||||||
generationTypes: ['factual', 'concept', 'comprehensive'],
|
|
||||||
skipUnanswerable: true,
|
|
||||||
datasetSplit: { train: 80, validation: 10, test: 10 },
|
datasetSplit: { train: 80, validation: 10, test: 10 },
|
||||||
})
|
})
|
||||||
interface UploadedDataFile {
|
interface UploadedDataFile {
|
||||||
@@ -125,7 +125,8 @@ const previewFiles = computed(() => uploadedFiles.value.map((file) => {
|
|||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
const primaryActionLabel = computed(() => {
|
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 === 'preview') return '确认预览并继续'
|
||||||
if (currentStepId.value === 'results') return '保存任务'
|
if (currentStepId.value === 'results') return '保存任务'
|
||||||
if (generation.status === 'running') return '正在生成'
|
if (generation.status === 'running') return '正在生成'
|
||||||
@@ -144,10 +145,20 @@ const previousStepLabel = computed(() => currentStep.value > 0
|
|||||||
? WIZARD_STEPS[currentStep.value - 1].title
|
? 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() {
|
function draftSnapshot() {
|
||||||
return {
|
return {
|
||||||
schemaVersion: DRAFT_SCHEMA_VERSION,
|
schemaVersion: DRAFT_SCHEMA_VERSION,
|
||||||
currentStep: currentStep.value,
|
currentStep: currentStep.value,
|
||||||
|
currentStepId: currentStepId.value,
|
||||||
task: { ...task },
|
task: { ...task },
|
||||||
processType: processType.value,
|
processType: processType.value,
|
||||||
structuredOptions: {
|
structuredOptions: {
|
||||||
@@ -158,7 +169,6 @@ function draftSnapshot() {
|
|||||||
unstructuredOptions: {
|
unstructuredOptions: {
|
||||||
...unstructuredOptions.value,
|
...unstructuredOptions.value,
|
||||||
preprocessOptions: [...unstructuredOptions.value.preprocessOptions],
|
preprocessOptions: [...unstructuredOptions.value.preprocessOptions],
|
||||||
generationTypes: [...unstructuredOptions.value.generationTypes],
|
|
||||||
datasetSplit: { ...unstructuredOptions.value.datasetSplit },
|
datasetSplit: { ...unstructuredOptions.value.datasetSplit },
|
||||||
},
|
},
|
||||||
uploadedFiles: uploadedFiles.value,
|
uploadedFiles: uploadedFiles.value,
|
||||||
@@ -216,17 +226,11 @@ function generationAffectingOptions() {
|
|||||||
const {
|
const {
|
||||||
semanticEnrichment,
|
semanticEnrichment,
|
||||||
qaPairsPerChunk,
|
qaPairsPerChunk,
|
||||||
contextScope,
|
|
||||||
generationTypes,
|
|
||||||
skipUnanswerable,
|
|
||||||
datasetSplit,
|
datasetSplit,
|
||||||
} = unstructuredOptions.value
|
} = unstructuredOptions.value
|
||||||
return {
|
return {
|
||||||
semanticEnrichment,
|
semanticEnrichment,
|
||||||
qaPairsPerChunk,
|
qaPairsPerChunk,
|
||||||
contextScope,
|
|
||||||
generationTypes,
|
|
||||||
skipUnanswerable,
|
|
||||||
datasetSplit,
|
datasetSplit,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -264,12 +268,21 @@ function restoreDraft() {
|
|||||||
if (!raw) return
|
if (!raw) return
|
||||||
const snapshot = JSON.parse(raw) as DraftSnapshot
|
const snapshot = JSON.parse(raw) as DraftSnapshot
|
||||||
if (!snapshot?.uploadedFiles && (!snapshot?.fileName || !snapshot?.sourceText)) return
|
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
|
restoringDraft.value = true
|
||||||
currentStep.value = requiresPreviewMigration
|
goToStep(requiresPreviewMigration ? 'create' : restoredStepId)
|
||||||
? 0
|
|
||||||
: Math.min(Math.max(Number(snapshot.currentStep) || 0, 0), WIZARD_STEPS.length - 1)
|
|
||||||
task.name = snapshot.task?.name || ''
|
task.name = snapshot.task?.name || ''
|
||||||
task.description = snapshot.task?.description || ''
|
task.description = snapshot.task?.description || ''
|
||||||
processType.value = snapshot.processType === 'structured' || snapshot.processType === 'external'
|
processType.value = snapshot.processType === 'structured' || snapshot.processType === 'external'
|
||||||
@@ -289,18 +302,51 @@ function restoreDraft() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (snapshot.unstructuredOptions) {
|
if (snapshot.unstructuredOptions) {
|
||||||
|
const restoredUnstructuredOptions = snapshot.unstructuredOptions
|
||||||
|
const restoredDatasetSplit = restoredUnstructuredOptions.datasetSplit
|
||||||
unstructuredOptions.value = {
|
unstructuredOptions.value = {
|
||||||
...unstructuredOptions.value,
|
...unstructuredOptions.value,
|
||||||
...snapshot.unstructuredOptions,
|
preprocessOptions: Array.isArray(restoredUnstructuredOptions.preprocessOptions)
|
||||||
preprocessOptions: Array.isArray(snapshot.unstructuredOptions.preprocessOptions)
|
? restoredUnstructuredOptions.preprocessOptions
|
||||||
? snapshot.unstructuredOptions.preprocessOptions
|
|
||||||
: unstructuredOptions.value.preprocessOptions,
|
: unstructuredOptions.value.preprocessOptions,
|
||||||
generationTypes: Array.isArray(snapshot.unstructuredOptions.generationTypes)
|
chunkMethod: restoredUnstructuredOptions.chunkMethod || unstructuredOptions.value.chunkMethod,
|
||||||
? snapshot.unstructuredOptions.generationTypes
|
chunkSize: Number.isFinite(restoredUnstructuredOptions.chunkSize)
|
||||||
: unstructuredOptions.value.generationTypes,
|
? 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: {
|
datasetSplit: {
|
||||||
...unstructuredOptions.value.datasetSplit,
|
train: Number.isFinite(restoredDatasetSplit?.train)
|
||||||
...snapshot.unstructuredOptions.datasetSplit,
|
? 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
|
|| null
|
||||||
results.value = !requiresPreviewMigration && Array.isArray(snapshot.results) ? snapshot.results : []
|
results.value = !requiresPreviewMigration && Array.isArray(snapshot.results) ? snapshot.results : []
|
||||||
selectedResultId.value = requiresPreviewMigration ? null : snapshot.selectedResultId || results.value[0]?.id || null
|
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
|
dirty.value = false
|
||||||
nextTick(() => { restoringDraft.value = false })
|
nextTick(() => { restoringDraft.value = false })
|
||||||
ElMessage.info(requiresPreviewMigration
|
ElMessage.info(requiresPreviewMigration
|
||||||
@@ -355,6 +409,12 @@ watch(
|
|||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watch(processType, (nextType, previousType) => {
|
||||||
|
if (restoringDraft.value || nextType === previousType || uploadedFiles.value.length === 0) return
|
||||||
|
resetSourceDataForProcessTypeChange()
|
||||||
|
ElMessage.info('处理类型已变更,请重新上传或拉取匹配的源数据')
|
||||||
|
})
|
||||||
|
|
||||||
watch(generationOptionsSignature, (currentSignature, previousSignature) => {
|
watch(generationOptionsSignature, (currentSignature, previousSignature) => {
|
||||||
if (restoringDraft.value || currentSignature === previousSignature) return
|
if (restoringDraft.value || currentSignature === previousSignature) return
|
||||||
resetDownstream()
|
resetDownstream()
|
||||||
@@ -418,8 +478,6 @@ function useSampleFile() {
|
|||||||
count: DEFAULT_SOURCE_TEXT.split('\n').filter((line) => line.trim()).length,
|
count: DEFAULT_SOURCE_TEXT.split('\n').filter((line) => line.trim()).length,
|
||||||
content: DEFAULT_SOURCE_TEXT
|
content: DEFAULT_SOURCE_TEXT
|
||||||
}]
|
}]
|
||||||
if (!task.name) task.name = '金融问答清洗任务'
|
|
||||||
if (!task.description) task.description = '清洗金融领域问答数据,统一格式并生成高质量训练数据。'
|
|
||||||
dirty.value = true
|
dirty.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -459,7 +517,6 @@ function handlePullData() {
|
|||||||
count: Math.min(externalSource.limit, DEFAULT_SOURCE_TEXT.split('\n').filter((line) => line.trim()).length),
|
count: Math.min(externalSource.limit, DEFAULT_SOURCE_TEXT.split('\n').filter((line) => line.trim()).length),
|
||||||
content: DEFAULT_SOURCE_TEXT,
|
content: DEFAULT_SOURCE_TEXT,
|
||||||
})
|
})
|
||||||
if (!task.name) task.name = `${typeName} 数据拉取任务`
|
|
||||||
dirty.value = true
|
dirty.value = true
|
||||||
ElMessage.success(`已成功拉取 ${uploadedFiles.value[uploadedFiles.value.length - 1].count.toLocaleString()} 条数据`)
|
ElMessage.success(`已成功拉取 ${uploadedFiles.value[uploadedFiles.value.length - 1].count.toLocaleString()} 条数据`)
|
||||||
}, 2000)
|
}, 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() {
|
function resetDownstream() {
|
||||||
stopGenerationTimer()
|
stopGenerationTimer()
|
||||||
generation.status = 'idle'
|
generation.status = 'idle'
|
||||||
@@ -489,6 +557,10 @@ function resetDownstream() {
|
|||||||
async function nextFromCreate() {
|
async function nextFromCreate() {
|
||||||
const valid = await taskSetupRef.value?.validate()
|
const valid = await taskSetupRef.value?.validate()
|
||||||
if (!valid) return
|
if (!valid) return
|
||||||
|
goToStep('upload')
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextFromUpload() {
|
||||||
if (uploadedFiles.value.length === 0) {
|
if (uploadedFiles.value.length === 0) {
|
||||||
ElMessage.warning(processType.value === 'external' ? '请先拉取至少一个数据源' : '请上传至少一个源数据文件')
|
ElMessage.warning(processType.value === 'external' ? '请先拉取至少一个数据源' : '请上传至少一个源数据文件')
|
||||||
return
|
return
|
||||||
@@ -512,7 +584,7 @@ async function nextFromCreate() {
|
|||||||
previewSignature.value = signature
|
previewSignature.value = signature
|
||||||
resetDownstream()
|
resetDownstream()
|
||||||
}
|
}
|
||||||
currentStep.value = 1
|
goToStep('preview')
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectPreviewFile(fileId: string) {
|
function selectPreviewFile(fileId: string) {
|
||||||
@@ -671,17 +743,21 @@ async function handlePrimaryAction() {
|
|||||||
await nextFromCreate()
|
await nextFromCreate()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (currentStepId.value === 'upload') {
|
||||||
|
nextFromUpload()
|
||||||
|
return
|
||||||
|
}
|
||||||
if (currentStepId.value === 'preview') {
|
if (currentStepId.value === 'preview') {
|
||||||
if (!previewItems.value.length) {
|
if (!previewItems.value.length) {
|
||||||
ElMessage.warning('当前没有可生成的预览内容')
|
ElMessage.warning('当前没有可生成的预览内容')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
currentStep.value = 2
|
goToStep('generate')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (currentStepId.value === 'generate') {
|
if (currentStepId.value === 'generate') {
|
||||||
if (generation.status === 'success') {
|
if (generation.status === 'success') {
|
||||||
currentStep.value = 3
|
goToStep('results')
|
||||||
} else if (generation.status !== 'running') {
|
} else if (generation.status !== 'running') {
|
||||||
startGeneration()
|
startGeneration()
|
||||||
}
|
}
|
||||||
@@ -760,6 +836,7 @@ onMounted(restoreDraft)
|
|||||||
'is-active': currentStep === index,
|
'is-active': currentStep === index,
|
||||||
'is-completed': currentStep > index
|
'is-completed': currentStep > index
|
||||||
}"
|
}"
|
||||||
|
:aria-current="currentStep === index ? 'step' : undefined"
|
||||||
>
|
>
|
||||||
<div v-if="index !== 0" class="step-connector"></div>
|
<div v-if="index !== 0" class="step-connector"></div>
|
||||||
<div class="step-node">
|
<div class="step-node">
|
||||||
@@ -784,6 +861,11 @@ onMounted(restoreDraft)
|
|||||||
v-model:process-type="processType"
|
v-model:process-type="processType"
|
||||||
v-model:structured-options="structuredOptions"
|
v-model:structured-options="structuredOptions"
|
||||||
v-model:unstructured-options="unstructuredOptions"
|
v-model:unstructured-options="unstructuredOptions"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SourceUploadStep
|
||||||
|
v-else-if="currentStepId === 'upload'"
|
||||||
|
:process-type="processType"
|
||||||
:uploaded-files="uploadedFiles"
|
:uploaded-files="uploadedFiles"
|
||||||
:external-source="externalSource"
|
:external-source="externalSource"
|
||||||
:external-pulling="externalPulling"
|
:external-pulling="externalPulling"
|
||||||
@@ -826,7 +908,7 @@ onMounted(restoreDraft)
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<ResultEditorStep
|
<ResultEditorStep
|
||||||
v-else
|
v-else-if="currentStepId === 'results'"
|
||||||
v-model:selected-id="selectedResultId"
|
v-model:selected-id="selectedResultId"
|
||||||
:items="results"
|
:items="results"
|
||||||
@update:field="updateResultField"
|
@update:field="updateResultField"
|
||||||
@@ -1013,4 +1095,47 @@ onMounted(restoreDraft)
|
|||||||
.wizard-primary-action {
|
.wizard-primary-action {
|
||||||
min-width: 160px;
|
min-width: 160px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.wizard-main {
|
||||||
|
padding: 24px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-wizard-steps {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-connector {
|
||||||
|
margin: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-node {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-title {
|
||||||
|
max-width: 72px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.35;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard-footer {
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.step-title {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-connector {
|
||||||
|
margin: 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard-primary-action {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
565
frontend/src/views/data-process/create/SourceUploadStep.vue
Normal file
565
frontend/src/views/data-process/create/SourceUploadStep.vue
Normal file
@@ -0,0 +1,565 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import type { UploadFile } from 'element-plus'
|
||||||
|
import type { ExternalDataSource, ProcessType } from './types'
|
||||||
|
|
||||||
|
interface UploadedSourceFile {
|
||||||
|
uid: string | number
|
||||||
|
name: string
|
||||||
|
size: number
|
||||||
|
count: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
processType: ProcessType
|
||||||
|
uploadedFiles: UploadedSourceFile[]
|
||||||
|
externalSource: ExternalDataSource
|
||||||
|
externalPulling: boolean
|
||||||
|
externalConnected: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:externalSource': [value: ExternalDataSource]
|
||||||
|
'file-change': [file: UploadFile]
|
||||||
|
'remove-file': [uid: string | number]
|
||||||
|
'use-sample': []
|
||||||
|
'test-connection': []
|
||||||
|
'pull-data': []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const DATA_SOURCE_TYPES = [
|
||||||
|
{ value: 'mysql', label: 'MySQL' },
|
||||||
|
{ value: 'postgresql', label: 'PostgreSQL' },
|
||||||
|
{ value: 'mongodb', label: 'MongoDB' },
|
||||||
|
{ value: 'api', label: 'REST API' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const AUTH_MODES = [
|
||||||
|
{ value: 'none', label: '免鉴权' },
|
||||||
|
{ value: 'basic', label: '账号密码' },
|
||||||
|
{ value: 'token', label: 'Token' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const FILE_PAGE_SIZE = 10
|
||||||
|
const currentFilePage = ref(1)
|
||||||
|
|
||||||
|
const isExternal = computed(() => props.processType === 'external')
|
||||||
|
|
||||||
|
const uploadAccept = computed(() => props.processType === 'unstructured'
|
||||||
|
? '.txt,.md,.pdf,.docx,.doc,.json,.jsonl'
|
||||||
|
: '.json,.jsonl,.csv,.xlsx,.xls')
|
||||||
|
|
||||||
|
const pagedUploadedFiles = computed(() => {
|
||||||
|
const start = (currentFilePage.value - 1) * FILE_PAGE_SIZE
|
||||||
|
return props.uploadedFiles.slice(start, start + FILE_PAGE_SIZE)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => props.uploadedFiles.length, (newLength, oldLength) => {
|
||||||
|
const totalPages = Math.max(1, Math.ceil(newLength / FILE_PAGE_SIZE))
|
||||||
|
if (newLength > oldLength) {
|
||||||
|
currentFilePage.value = totalPages
|
||||||
|
return
|
||||||
|
}
|
||||||
|
currentFilePage.value = Math.min(currentFilePage.value, totalPages)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => props.processType, () => {
|
||||||
|
currentFilePage.value = 1
|
||||||
|
})
|
||||||
|
|
||||||
|
function updateExternalField<K extends keyof ExternalDataSource>(field: K, value: ExternalDataSource[K]) {
|
||||||
|
emit('update:externalSource', { ...props.externalSource, [field]: value })
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSize(size: number) {
|
||||||
|
if (!size) return '0 KB'
|
||||||
|
if (size >= 1024 * 1024) return `${(size / 1024 / 1024).toFixed(1)} MB`
|
||||||
|
return `${(size / 1024).toFixed(1)} KB`
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="source-upload-step" aria-labelledby="source-upload-title">
|
||||||
|
<div v-if="isExternal" class="form-section external-section">
|
||||||
|
<div class="section-title-row">
|
||||||
|
<div>
|
||||||
|
<h3 id="source-upload-title">数据源配置</h3>
|
||||||
|
<p>配置并验证外部数据源,拉取成功后可在下一步预览数据内容</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="external-form">
|
||||||
|
<el-form label-position="top" class="external-grid">
|
||||||
|
<el-form-item label="数据源类型">
|
||||||
|
<el-select
|
||||||
|
:model-value="externalSource.type"
|
||||||
|
placeholder="请选择数据源类型"
|
||||||
|
aria-label="数据源类型"
|
||||||
|
@update:model-value="updateExternalField('type', $event)"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in DATA_SOURCE_TYPES"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="地址 / URL">
|
||||||
|
<el-input
|
||||||
|
:model-value="externalSource.url"
|
||||||
|
placeholder="例如:mysql://host:3306/db 或 https://api.example.com/data"
|
||||||
|
aria-label="数据源地址或 URL"
|
||||||
|
@update:model-value="updateExternalField('url', $event)"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="鉴权方式">
|
||||||
|
<el-select
|
||||||
|
:model-value="externalSource.authMode"
|
||||||
|
aria-label="鉴权方式"
|
||||||
|
@update:model-value="updateExternalField('authMode', $event)"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in AUTH_MODES"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="externalSource.authMode === 'basic'" label="账号">
|
||||||
|
<el-input
|
||||||
|
:model-value="externalSource.username"
|
||||||
|
autocomplete="username"
|
||||||
|
placeholder="请输入账号"
|
||||||
|
aria-label="数据源账号"
|
||||||
|
@update:model-value="updateExternalField('username', $event)"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="externalSource.authMode === 'basic'" label="密码">
|
||||||
|
<el-input
|
||||||
|
:model-value="externalSource.password"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
autocomplete="current-password"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
aria-label="数据源密码"
|
||||||
|
@update:model-value="updateExternalField('password', $event)"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="externalSource.authMode === 'token'" label="Token">
|
||||||
|
<el-input
|
||||||
|
:model-value="externalSource.token"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="请输入访问 Token"
|
||||||
|
aria-label="数据源访问 Token"
|
||||||
|
@update:model-value="updateExternalField('token', $event)"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="拉取条数">
|
||||||
|
<el-input-number
|
||||||
|
:model-value="externalSource.limit"
|
||||||
|
:min="1"
|
||||||
|
:max="100000"
|
||||||
|
:step="100"
|
||||||
|
controls-position="right"
|
||||||
|
aria-label="数据拉取条数"
|
||||||
|
@update:model-value="updateExternalField('limit', Number($event) || 0)"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<div class="external-actions">
|
||||||
|
<el-button
|
||||||
|
:loading="externalPulling && !externalConnected"
|
||||||
|
:disabled="externalPulling"
|
||||||
|
plain
|
||||||
|
@click="emit('test-connection')"
|
||||||
|
>
|
||||||
|
测试连接
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:loading="externalPulling"
|
||||||
|
:disabled="externalPulling"
|
||||||
|
@click="emit('pull-data')"
|
||||||
|
>
|
||||||
|
拉取数据
|
||||||
|
</el-button>
|
||||||
|
<span v-if="externalConnected" class="external-status is-connected" role="status">
|
||||||
|
<i class="fa fa-check-circle" aria-hidden="true" /> 连接正常
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section v-if="uploadedFiles.length" class="uploaded-file-list" aria-label="已拉取数据列表">
|
||||||
|
<div class="uploaded-file-list-header">
|
||||||
|
<span>已拉取 {{ uploadedFiles.length }} 个数据集</span>
|
||||||
|
</div>
|
||||||
|
<div class="uploaded-file-items">
|
||||||
|
<div v-for="file in pagedUploadedFiles" :key="file.uid" class="uploaded-file">
|
||||||
|
<span class="file-icon"><i class="fa fa-cloud-download" aria-hidden="true" /></span>
|
||||||
|
<div class="file-main">
|
||||||
|
<strong :title="file.name">{{ file.name }}</strong>
|
||||||
|
<span>
|
||||||
|
{{ formatSize(file.size) }}
|
||||||
|
<template v-if="file.count"> · {{ file.count.toLocaleString() }} 条</template>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="file-status"><i class="fa fa-check-circle" aria-hidden="true" /> 拉取成功</span>
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
type="danger"
|
||||||
|
:aria-label="`删除数据集 ${file.name}`"
|
||||||
|
@click="emit('remove-file', file.uid)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-pagination
|
||||||
|
v-if="uploadedFiles.length > FILE_PAGE_SIZE"
|
||||||
|
v-model:current-page="currentFilePage"
|
||||||
|
:page-size="FILE_PAGE_SIZE"
|
||||||
|
:total="uploadedFiles.length"
|
||||||
|
:pager-count="5"
|
||||||
|
small
|
||||||
|
background
|
||||||
|
layout="prev, pager, next"
|
||||||
|
class="uploaded-file-pagination"
|
||||||
|
aria-label="已拉取数据分页"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="form-section upload-section">
|
||||||
|
<div class="section-title-row">
|
||||||
|
<div>
|
||||||
|
<h3 id="source-upload-title">源数据上传</h3>
|
||||||
|
<p>上传后可在下一步检查内容和切分效果,支持同时添加多个文件</p>
|
||||||
|
</div>
|
||||||
|
<el-button v-if="uploadedFiles.length === 0" link type="primary" @click="emit('use-sample')">
|
||||||
|
使用示例数据
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-upload
|
||||||
|
v-if="uploadedFiles.length === 0"
|
||||||
|
drag
|
||||||
|
multiple
|
||||||
|
:accept="uploadAccept"
|
||||||
|
:auto-upload="false"
|
||||||
|
:show-file-list="false"
|
||||||
|
:on-change="(file: UploadFile) => emit('file-change', file)"
|
||||||
|
aria-label="选择或拖拽源数据文件"
|
||||||
|
>
|
||||||
|
<i class="fa fa-cloud-upload upload-icon" aria-hidden="true" />
|
||||||
|
<div class="el-upload__text">拖拽文件到此处,或<em>点击选择文件</em></div>
|
||||||
|
<template #tip>
|
||||||
|
<div class="el-upload__tip">
|
||||||
|
{{ processType === 'unstructured'
|
||||||
|
? '支持 TXT、Markdown、PDF、Word、JSON、JSONL,单文件不超过 200MB'
|
||||||
|
: '支持 JSON、JSONL、CSV、Excel,单文件不超过 200MB' }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-upload>
|
||||||
|
|
||||||
|
<section v-else class="uploaded-file-list" aria-label="已上传文件列表">
|
||||||
|
<div class="uploaded-file-list-header">
|
||||||
|
<span>已添加 {{ uploadedFiles.length }} 个文件</span>
|
||||||
|
<div class="continue-upload">
|
||||||
|
<el-upload
|
||||||
|
multiple
|
||||||
|
:accept="uploadAccept"
|
||||||
|
:auto-upload="false"
|
||||||
|
:show-file-list="false"
|
||||||
|
:on-change="(file: UploadFile) => emit('file-change', file)"
|
||||||
|
aria-label="继续添加源数据文件"
|
||||||
|
>
|
||||||
|
<el-button size="small" type="primary">继续上传</el-button>
|
||||||
|
</el-upload>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="uploaded-file-items">
|
||||||
|
<div v-for="file in pagedUploadedFiles" :key="file.uid" class="uploaded-file">
|
||||||
|
<span class="file-icon"><i class="fa fa-file-text-o" aria-hidden="true" /></span>
|
||||||
|
<div class="file-main">
|
||||||
|
<strong :title="file.name">{{ file.name }}</strong>
|
||||||
|
<span>
|
||||||
|
{{ formatSize(file.size) }}
|
||||||
|
<template v-if="file.count"> · {{ file.count.toLocaleString() }} 条</template>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="file-status"><i class="fa fa-check-circle" aria-hidden="true" /> 校验通过</span>
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
type="danger"
|
||||||
|
:aria-label="`删除文件 ${file.name}`"
|
||||||
|
@click="emit('remove-file', file.uid)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-pagination
|
||||||
|
v-if="uploadedFiles.length > FILE_PAGE_SIZE"
|
||||||
|
v-model:current-page="currentFilePage"
|
||||||
|
:page-size="FILE_PAGE_SIZE"
|
||||||
|
:total="uploadedFiles.length"
|
||||||
|
:pager-count="5"
|
||||||
|
small
|
||||||
|
background
|
||||||
|
layout="prev, pager, next"
|
||||||
|
class="uploaded-file-pagination"
|
||||||
|
aria-label="已上传文件分页"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.source-upload-step {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section {
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 5px;
|
||||||
|
color: #2f3747;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
color: #8a93a3;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.external-section {
|
||||||
|
.external-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 18px 20px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-form-item) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-select),
|
||||||
|
:deep(.el-input),
|
||||||
|
:deep(.el-input-number) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.external-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.external-status {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
&.is-connected {
|
||||||
|
color: #2ca66a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-section :deep(.el-upload) {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-section :deep(.el-upload-dragger) {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 154px;
|
||||||
|
padding: 32px 20px;
|
||||||
|
background: #fbfcfe;
|
||||||
|
border-color: #dfe3ea;
|
||||||
|
transition: border-color 0.18s ease, background-color 0.18s ease;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus-visible {
|
||||||
|
background: #fafaff;
|
||||||
|
border-color: #8b82f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid #5b50f2;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-icon {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: #5b50f2;
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploaded-file-list {
|
||||||
|
margin-top: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #dfe3ea;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploaded-file-list-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
color: #5f6878;
|
||||||
|
font-size: 12px;
|
||||||
|
background: #fbfcfe;
|
||||||
|
border-bottom: 1px solid #edf0f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.continue-upload {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.continue-upload :deep(.el-upload) {
|
||||||
|
width: auto;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploaded-file-pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-top: 1px solid #edf0f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploaded-file {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-height: 48px;
|
||||||
|
padding: 8px 14px;
|
||||||
|
border-bottom: 1px solid #edf0f5;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
color: #5b50f2;
|
||||||
|
font-size: 14px;
|
||||||
|
background: #f0efff;
|
||||||
|
border-radius: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-main {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
overflow: hidden;
|
||||||
|
color: #273142;
|
||||||
|
font-size: 14px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: #8a93a3;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-status {
|
||||||
|
color: #2ca66a;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploaded-file :deep(.el-button) {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.external-section .external-grid {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploaded-file {
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-status {
|
||||||
|
flex: 0 1 auto;
|
||||||
|
line-height: 1.4;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploaded-file-pagination {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 560px) {
|
||||||
|
.section-title-row {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploaded-file-list-header {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploaded-file {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-main {
|
||||||
|
min-width: calc(100% - 40px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-status {
|
||||||
|
margin-left: 38px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.upload-section :deep(.el-upload-dragger) {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
export type ProcessType = 'structured' | 'unstructured' | 'external'
|
export type ProcessType = 'structured' | 'unstructured' | 'external'
|
||||||
|
|
||||||
export type StepId = 'create' | 'preview' | 'generate' | 'results'
|
export type StepId = 'create' | 'upload' | 'preview' | 'generate' | 'results'
|
||||||
|
|
||||||
export type PreprocessOption =
|
export type PreprocessOption =
|
||||||
| 'clean_invalid'
|
| 'clean_invalid'
|
||||||
@@ -34,15 +34,6 @@ export type UnstructuredPreprocessOption =
|
|||||||
|
|
||||||
export type ChunkMethod = 'semantic' | 'heading' | 'fixed' | 'custom'
|
export type ChunkMethod = 'semantic' | 'heading' | 'fixed' | 'custom'
|
||||||
|
|
||||||
export type GenerationContextScope = 'current' | 'adjacent' | 'section'
|
|
||||||
|
|
||||||
export type QuestionGenerationType =
|
|
||||||
| 'factual'
|
|
||||||
| 'concept'
|
|
||||||
| 'procedure'
|
|
||||||
| 'reasoning'
|
|
||||||
| 'comprehensive'
|
|
||||||
|
|
||||||
export interface UnstructuredProcessOptions {
|
export interface UnstructuredProcessOptions {
|
||||||
preprocessOptions: UnstructuredPreprocessOption[]
|
preprocessOptions: UnstructuredPreprocessOption[]
|
||||||
chunkMethod: ChunkMethod
|
chunkMethod: ChunkMethod
|
||||||
@@ -55,9 +46,6 @@ export interface UnstructuredProcessOptions {
|
|||||||
preserveLists: boolean
|
preserveLists: boolean
|
||||||
semanticEnrichment: boolean
|
semanticEnrichment: boolean
|
||||||
qaPairsPerChunk: number
|
qaPairsPerChunk: number
|
||||||
contextScope: GenerationContextScope
|
|
||||||
generationTypes: QuestionGenerationType[]
|
|
||||||
skipUnanswerable: boolean
|
|
||||||
datasetSplit: DatasetSplitOptions
|
datasetSplit: DatasetSplitOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user