feat: 数据处理向导配置体系扩展
新增结构化与非结构化处理选项类型,TaskSetupStep 增加预处理、切分方法、数据集划分等配置 UI,previewModel 实现语义切分与受保护区间算法,CreateView 接入配置状态与草稿持久化并替换为 AppConfirmDialog,回归脚本扩充配置与弹窗断言。
This commit is contained in:
@@ -1,17 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { onBeforeRouteLeave, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox, type UploadFile } from 'element-plus'
|
||||
import { ElMessage, type UploadFile } from 'element-plus'
|
||||
import AppConfirmDialog from '@/components/AppConfirmDialog.vue'
|
||||
import TaskSetupStep from './create/TaskSetupStep.vue'
|
||||
import PreviewCompareStep from './create/PreviewCompareStep.vue'
|
||||
import GenerationStep from './create/GenerationStep.vue'
|
||||
import ResultEditorStep from './create/ResultEditorStep.vue'
|
||||
import { buildPreviewItems, createResults, DEFAULT_SOURCE_TEXT } from './create/previewModel'
|
||||
import type { ExternalDataSource, GenerationState, PreviewItem, ProcessType, ResultItem, StepId } from './create/types'
|
||||
import type {
|
||||
ExternalDataSource,
|
||||
GenerationState,
|
||||
PreviewItem,
|
||||
ProcessType,
|
||||
ResultItem,
|
||||
StepId,
|
||||
StructuredProcessOptions,
|
||||
UnstructuredProcessOptions,
|
||||
} from './create/types'
|
||||
|
||||
const router = useRouter()
|
||||
const taskSetupRef = ref<InstanceType<typeof TaskSetupStep>>()
|
||||
const confirmDialogRef = ref<InstanceType<typeof AppConfirmDialog>>()
|
||||
const DRAFT_STORAGE_KEY = 'yg-data-process-create-draft'
|
||||
const DRAFT_SCHEMA_VERSION = 2
|
||||
const PREVIEW_MODEL_VERSION = 'document-chunk-v2'
|
||||
|
||||
const WIZARD_STEPS = [
|
||||
{ id: 'create', title: '创建任务', desc: '填写任务信息与上传源数据' },
|
||||
@@ -24,6 +37,36 @@ const currentStep = ref(0)
|
||||
const currentStepId = computed<StepId>(() => WIZARD_STEPS[currentStep.value]?.id ?? 'create')
|
||||
const task = reactive({ name: '', description: '' })
|
||||
const processType = ref<ProcessType>('unstructured')
|
||||
const structuredOptions = ref<StructuredProcessOptions>({
|
||||
preprocessOptions: ['clean_invalid', 'detect_structure', 'deduplicate', 'normalize_format'],
|
||||
semanticEnrichment: false,
|
||||
qaPairsPerRow: 1,
|
||||
datasetSplit: { train: 80, validation: 10, test: 10 },
|
||||
})
|
||||
const unstructuredOptions = ref<UnstructuredProcessOptions>({
|
||||
preprocessOptions: [
|
||||
'clean_invalid_content',
|
||||
'detect_document_structure',
|
||||
'merge_short_content',
|
||||
'filter_low_quality',
|
||||
'deduplicate_content',
|
||||
'preserve_context',
|
||||
],
|
||||
chunkMethod: 'semantic',
|
||||
chunkSize: 800,
|
||||
chunkOverlap: 100,
|
||||
minChunkSize: 100,
|
||||
customDelimiter: '',
|
||||
preserveTables: true,
|
||||
preserveCodeBlocks: true,
|
||||
preserveLists: true,
|
||||
semanticEnrichment: false,
|
||||
qaPairsPerChunk: 1,
|
||||
contextScope: 'adjacent',
|
||||
generationTypes: ['factual', 'concept', 'comprehensive'],
|
||||
skipUnanswerable: true,
|
||||
datasetSplit: { train: 80, validation: 10, test: 10 },
|
||||
})
|
||||
interface UploadedDataFile {
|
||||
uid: number | string
|
||||
name: string
|
||||
@@ -103,9 +146,21 @@ const previousStepLabel = computed(() => currentStep.value > 0
|
||||
|
||||
function draftSnapshot() {
|
||||
return {
|
||||
schemaVersion: DRAFT_SCHEMA_VERSION,
|
||||
currentStep: currentStep.value,
|
||||
task: { ...task },
|
||||
processType: processType.value,
|
||||
structuredOptions: {
|
||||
...structuredOptions.value,
|
||||
preprocessOptions: [...structuredOptions.value.preprocessOptions],
|
||||
datasetSplit: { ...structuredOptions.value.datasetSplit },
|
||||
},
|
||||
unstructuredOptions: {
|
||||
...unstructuredOptions.value,
|
||||
preprocessOptions: [...unstructuredOptions.value.preprocessOptions],
|
||||
generationTypes: [...unstructuredOptions.value.generationTypes],
|
||||
datasetSplit: { ...unstructuredOptions.value.datasetSplit },
|
||||
},
|
||||
uploadedFiles: uploadedFiles.value,
|
||||
externalSource: { ...externalSource },
|
||||
previewSignature: previewSignature.value,
|
||||
@@ -119,6 +174,74 @@ function draftSnapshot() {
|
||||
}
|
||||
}
|
||||
|
||||
function previewAffectingOptions() {
|
||||
if (processType.value === 'structured') {
|
||||
return {
|
||||
preprocessOptions: structuredOptions.value.preprocessOptions,
|
||||
}
|
||||
}
|
||||
if (processType.value === 'unstructured') {
|
||||
const {
|
||||
preprocessOptions,
|
||||
chunkMethod,
|
||||
chunkSize,
|
||||
chunkOverlap,
|
||||
minChunkSize,
|
||||
customDelimiter,
|
||||
preserveTables,
|
||||
preserveCodeBlocks,
|
||||
preserveLists,
|
||||
} = unstructuredOptions.value
|
||||
return {
|
||||
preprocessOptions,
|
||||
chunkMethod,
|
||||
chunkSize,
|
||||
chunkOverlap,
|
||||
minChunkSize,
|
||||
customDelimiter: chunkMethod === 'custom' ? customDelimiter : '',
|
||||
preserveTables,
|
||||
preserveCodeBlocks,
|
||||
preserveLists,
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function generationAffectingOptions() {
|
||||
if (processType.value === 'structured') {
|
||||
const { semanticEnrichment, qaPairsPerRow, datasetSplit } = structuredOptions.value
|
||||
return { semanticEnrichment, qaPairsPerRow, datasetSplit }
|
||||
}
|
||||
if (processType.value === 'unstructured') {
|
||||
const {
|
||||
semanticEnrichment,
|
||||
qaPairsPerChunk,
|
||||
contextScope,
|
||||
generationTypes,
|
||||
skipUnanswerable,
|
||||
datasetSplit,
|
||||
} = unstructuredOptions.value
|
||||
return {
|
||||
semanticEnrichment,
|
||||
qaPairsPerChunk,
|
||||
contextScope,
|
||||
generationTypes,
|
||||
skipUnanswerable,
|
||||
datasetSplit,
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const generationOptionsSignature = computed(() => JSON.stringify(generationAffectingOptions()))
|
||||
|
||||
function buildPreviewSignature() {
|
||||
const filesSignature = uploadedFiles.value
|
||||
.map((file) => `${file.uid}:${file.content}`)
|
||||
.join('|')
|
||||
return `${PREVIEW_MODEL_VERSION}:${processType.value}:${JSON.stringify(previewAffectingOptions())}:${filesSignature}`
|
||||
}
|
||||
|
||||
function persistDraft() {
|
||||
if (restoringDraft.value) return
|
||||
try {
|
||||
@@ -128,7 +251,7 @@ function persistDraft() {
|
||||
}
|
||||
}
|
||||
|
||||
type DraftSnapshot = ReturnType<typeof draftSnapshot> & {
|
||||
type DraftSnapshot = Partial<ReturnType<typeof draftSnapshot>> & {
|
||||
fileName?: string
|
||||
fileSize?: number
|
||||
fileCount?: number
|
||||
@@ -141,14 +264,46 @@ 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
|
||||
|
||||
restoringDraft.value = true
|
||||
currentStep.value = Math.min(Math.max(Number(snapshot.currentStep) || 0, 0), WIZARD_STEPS.length - 1)
|
||||
currentStep.value = requiresPreviewMigration
|
||||
? 0
|
||||
: Math.min(Math.max(Number(snapshot.currentStep) || 0, 0), WIZARD_STEPS.length - 1)
|
||||
task.name = snapshot.task?.name || ''
|
||||
task.description = snapshot.task?.description || ''
|
||||
processType.value = snapshot.processType === 'structured' || snapshot.processType === 'external'
|
||||
? snapshot.processType
|
||||
: 'unstructured'
|
||||
if (snapshot.structuredOptions) {
|
||||
structuredOptions.value = {
|
||||
...structuredOptions.value,
|
||||
...snapshot.structuredOptions,
|
||||
preprocessOptions: Array.isArray(snapshot.structuredOptions.preprocessOptions)
|
||||
? snapshot.structuredOptions.preprocessOptions
|
||||
: structuredOptions.value.preprocessOptions,
|
||||
datasetSplit: {
|
||||
...structuredOptions.value.datasetSplit,
|
||||
...snapshot.structuredOptions.datasetSplit,
|
||||
},
|
||||
}
|
||||
}
|
||||
if (snapshot.unstructuredOptions) {
|
||||
unstructuredOptions.value = {
|
||||
...unstructuredOptions.value,
|
||||
...snapshot.unstructuredOptions,
|
||||
preprocessOptions: Array.isArray(snapshot.unstructuredOptions.preprocessOptions)
|
||||
? snapshot.unstructuredOptions.preprocessOptions
|
||||
: unstructuredOptions.value.preprocessOptions,
|
||||
generationTypes: Array.isArray(snapshot.unstructuredOptions.generationTypes)
|
||||
? snapshot.unstructuredOptions.generationTypes
|
||||
: unstructuredOptions.value.generationTypes,
|
||||
datasetSplit: {
|
||||
...unstructuredOptions.value.datasetSplit,
|
||||
...snapshot.unstructuredOptions.datasetSplit,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (snapshot.uploadedFiles) {
|
||||
uploadedFiles.value = Array.isArray(snapshot.uploadedFiles) ? snapshot.uploadedFiles : []
|
||||
@@ -163,41 +318,50 @@ function restoreDraft() {
|
||||
}]
|
||||
}
|
||||
|
||||
previewSignature.value = snapshot.previewSignature || ''
|
||||
previewSignature.value = requiresPreviewMigration ? '' : snapshot.previewSignature || ''
|
||||
if (snapshot.externalSource) {
|
||||
Object.assign(externalSource, snapshot.externalSource)
|
||||
}
|
||||
const defaultSourceFileId = String(uploadedFiles.value[0]?.uid ?? '')
|
||||
previewItems.value = Array.isArray(snapshot.previewItems)
|
||||
previewItems.value = !requiresPreviewMigration && Array.isArray(snapshot.previewItems)
|
||||
? snapshot.previewItems.map((item) => ({ ...item, sourceFileId: item.sourceFileId ?? defaultSourceFileId }))
|
||||
: []
|
||||
selectedPreviewFileId.value = snapshot.selectedPreviewFileId || defaultSourceFileId || null
|
||||
selectedPreviewIdsByFile.value = snapshot.selectedPreviewIdsByFile || {}
|
||||
selectedPreviewId.value = snapshot.selectedPreviewId
|
||||
|| selectedPreviewIdsByFile.value[selectedPreviewFileId.value ?? '']
|
||||
|| activePreviewItems.value[0]?.id
|
||||
|| null
|
||||
results.value = Array.isArray(snapshot.results) ? snapshot.results : []
|
||||
selectedResultId.value = snapshot.selectedResultId || results.value[0]?.id || null
|
||||
Object.assign(generation, snapshot.generation || {})
|
||||
selectedPreviewIdsByFile.value = requiresPreviewMigration ? {} : snapshot.selectedPreviewIdsByFile || {}
|
||||
selectedPreviewId.value = requiresPreviewMigration
|
||||
? null
|
||||
: snapshot.selectedPreviewId
|
||||
|| selectedPreviewIdsByFile.value[selectedPreviewFileId.value ?? '']
|
||||
|| activePreviewItems.value[0]?.id
|
||||
|| 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 || {})
|
||||
dirty.value = false
|
||||
nextTick(() => { restoringDraft.value = false })
|
||||
ElMessage.info('已恢复上次保存的草稿')
|
||||
ElMessage.info(requiresPreviewMigration
|
||||
? '已恢复任务信息,切分规则已更新,请重新生成预览'
|
||||
: '已恢复上次保存的草稿')
|
||||
} catch {
|
||||
localStorage.removeItem(DRAFT_STORAGE_KEY)
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
[() => task.name, () => task.description, processType, externalSource],
|
||||
[() => task.name, () => task.description, processType, structuredOptions, unstructuredOptions, externalSource],
|
||||
() => {
|
||||
if (!restoringDraft.value) dirty.value = true
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
watch(generationOptionsSignature, (currentSignature, previousSignature) => {
|
||||
if (restoringDraft.value || currentSignature === previousSignature) return
|
||||
resetDownstream()
|
||||
})
|
||||
|
||||
watch(
|
||||
[currentStep, task, processType, uploadedFiles, externalSource, previewSignature,
|
||||
[currentStep, task, processType, structuredOptions, unstructuredOptions, uploadedFiles, externalSource, previewSignature,
|
||||
previewItems, selectedPreviewFileId, selectedPreviewId, selectedPreviewIdsByFile,
|
||||
results, selectedResultId, generation],
|
||||
persistDraft,
|
||||
@@ -330,10 +494,15 @@ async function nextFromCreate() {
|
||||
return
|
||||
}
|
||||
|
||||
const signature = `${processType.value}:${uploadedFiles.value.map((file) => `${file.uid}:${file.content}`).join('|')}`
|
||||
const signature = buildPreviewSignature()
|
||||
if (signature !== previewSignature.value) {
|
||||
previewItems.value = uploadedFiles.value.flatMap((file) =>
|
||||
buildPreviewItems(file.content, processType.value, String(file.uid)),
|
||||
buildPreviewItems(
|
||||
file.content,
|
||||
processType.value,
|
||||
String(file.uid),
|
||||
processType.value === 'unstructured' ? unstructuredOptions.value : undefined,
|
||||
),
|
||||
)
|
||||
selectedPreviewFileId.value = String(uploadedFiles.value[0]?.uid ?? '') || null
|
||||
selectedPreviewId.value = activePreviewItems.value[0]?.id ?? null
|
||||
@@ -367,6 +536,7 @@ function updatePreviewContent(id: string, value: string) {
|
||||
item.editedContent = value
|
||||
item.tokenCount = Math.max(1, Math.ceil(value.length / 2))
|
||||
item.status = value === item.originalContent ? 'original' : item.sourceStart == null ? 'manual' : 'modified'
|
||||
resetDownstream()
|
||||
dirty.value = true
|
||||
}
|
||||
|
||||
@@ -376,6 +546,7 @@ function restorePreviewItem(id: string) {
|
||||
item.editedContent = item.originalContent
|
||||
item.tokenCount = Math.max(1, Math.ceil(item.originalContent.length / 2))
|
||||
item.status = 'original'
|
||||
resetDownstream()
|
||||
dirty.value = true
|
||||
}
|
||||
|
||||
@@ -395,19 +566,19 @@ function addPreviewItem() {
|
||||
status: 'manual',
|
||||
})
|
||||
selectPreviewItem(id)
|
||||
resetDownstream()
|
||||
dirty.value = true
|
||||
}
|
||||
|
||||
async function removePreviewItem(id: string) {
|
||||
try {
|
||||
await ElMessageBox.confirm('删除只影响本次处理,不会修改源文件。确认删除吗?', '删除预览内容', {
|
||||
confirmButtonText: '删除',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
const confirmed = await confirmDialogRef.value?.open({
|
||||
title: '删除预览内容?',
|
||||
message: '删除只影响本次处理,不会修改源文件。删除后可重新从源文件生成预览。',
|
||||
confirmText: '删除',
|
||||
cancelText: '取消',
|
||||
tone: 'danger',
|
||||
})
|
||||
if (!confirmed) return
|
||||
|
||||
const index = previewItems.value.findIndex((item) => item.id === id)
|
||||
if (index < 0) return
|
||||
@@ -416,6 +587,7 @@ async function removePreviewItem(id: string) {
|
||||
if (selectedPreviewFileId.value && selectedPreviewId.value) {
|
||||
selectedPreviewIdsByFile.value[selectedPreviewFileId.value] = selectedPreviewId.value
|
||||
}
|
||||
resetDownstream()
|
||||
dirty.value = true
|
||||
}
|
||||
|
||||
@@ -436,8 +608,15 @@ function startGeneration() {
|
||||
|
||||
stopGenerationTimer()
|
||||
generation.status = 'success'
|
||||
generation.message = `已完成 ${previewItems.value.length.toLocaleString()} 条数据处理,可进入结果页检查。`
|
||||
results.value = createResults(previewItems.value)
|
||||
results.value = createResults(
|
||||
previewItems.value,
|
||||
processType.value === 'structured'
|
||||
? structuredOptions.value
|
||||
: processType.value === 'unstructured'
|
||||
? unstructuredOptions.value
|
||||
: undefined,
|
||||
)
|
||||
generation.message = `已完成 ${results.value.length.toLocaleString()} 条数据处理,可进入结果页检查。`
|
||||
selectedResultId.value = results.value[0]?.id ?? null
|
||||
dirty.value = true
|
||||
ElMessage.success('数据处理完成')
|
||||
@@ -538,25 +717,29 @@ async function handleCancel() {
|
||||
router.back()
|
||||
return
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm('当前存在未保存修改,确定离开吗?', '离开创建任务', {
|
||||
confirmButtonText: '放弃修改',
|
||||
cancelButtonText: '继续编辑',
|
||||
type: 'warning',
|
||||
})
|
||||
allowLeave = true
|
||||
router.back()
|
||||
} catch {
|
||||
// 用户继续编辑。
|
||||
}
|
||||
const confirmed = await confirmDialogRef.value?.open({
|
||||
title: '确认离开当前页面?',
|
||||
message: '当前存在未保存修改,离开后这些修改将不会保留。',
|
||||
confirmText: '放弃修改',
|
||||
cancelText: '继续编辑',
|
||||
tone: 'danger',
|
||||
})
|
||||
if (!confirmed) return
|
||||
allowLeave = true
|
||||
router.back()
|
||||
}
|
||||
|
||||
onBeforeRouteLeave((_to, _from, next) => {
|
||||
if (allowLeave || !dirty.value) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
next(window.confirm('当前存在未保存修改,确定离开吗?'))
|
||||
onBeforeRouteLeave(async () => {
|
||||
if (allowLeave || !dirty.value) return true
|
||||
const confirmed = await confirmDialogRef.value?.open({
|
||||
title: '确认离开当前页面?',
|
||||
message: '当前存在未保存修改,离开后这些修改将不会保留。',
|
||||
confirmText: '放弃修改',
|
||||
cancelText: '继续编辑',
|
||||
tone: 'danger',
|
||||
})
|
||||
if (confirmed) allowLeave = true
|
||||
return Boolean(confirmed)
|
||||
})
|
||||
|
||||
onBeforeUnmount(stopGenerationTimer)
|
||||
@@ -599,6 +782,8 @@ onMounted(restoreDraft)
|
||||
v-model:name="task.name"
|
||||
v-model:description="task.description"
|
||||
v-model:process-type="processType"
|
||||
v-model:structured-options="structuredOptions"
|
||||
v-model:unstructured-options="unstructuredOptions"
|
||||
:uploaded-files="uploadedFiles"
|
||||
:external-source="externalSource"
|
||||
:external-pulling="externalPulling"
|
||||
@@ -673,6 +858,8 @@ onMounted(restoreDraft)
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<AppConfirmDialog ref="confirmDialogRef" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
Reference in New Issue
Block a user