2026-07-10 16:45:55 +08:00
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
|
|
|
|
|
import { onBeforeRouteLeave, useRouter } from 'vue-router'
|
2026-07-11 14:49:10 +08:00
|
|
|
|
import { ElMessage, type UploadFile } from 'element-plus'
|
|
|
|
|
|
import AppConfirmDialog from '@/components/AppConfirmDialog.vue'
|
2026-07-10 16:45:55 +08:00
|
|
|
|
import TaskSetupStep from './create/TaskSetupStep.vue'
|
2026-07-12 15:39:43 +08:00
|
|
|
|
import SourceUploadStep from './create/SourceUploadStep.vue'
|
2026-07-10 16:45:55 +08:00
|
|
|
|
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'
|
2026-07-11 14:49:10 +08:00
|
|
|
|
import type {
|
|
|
|
|
|
ExternalDataSource,
|
|
|
|
|
|
GenerationState,
|
|
|
|
|
|
PreviewItem,
|
|
|
|
|
|
ProcessType,
|
|
|
|
|
|
ResultItem,
|
|
|
|
|
|
StepId,
|
|
|
|
|
|
StructuredProcessOptions,
|
|
|
|
|
|
UnstructuredProcessOptions,
|
|
|
|
|
|
} from './create/types'
|
2026-07-10 16:45:55 +08:00
|
|
|
|
|
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
|
const taskSetupRef = ref<InstanceType<typeof TaskSetupStep>>()
|
2026-07-11 14:49:10 +08:00
|
|
|
|
const confirmDialogRef = ref<InstanceType<typeof AppConfirmDialog>>()
|
2026-07-10 16:45:55 +08:00
|
|
|
|
const DRAFT_STORAGE_KEY = 'yg-data-process-create-draft'
|
2026-07-12 15:39:43 +08:00
|
|
|
|
const DRAFT_SCHEMA_VERSION = 4
|
2026-07-11 14:49:10 +08:00
|
|
|
|
const PREVIEW_MODEL_VERSION = 'document-chunk-v2'
|
2026-07-10 16:45:55 +08:00
|
|
|
|
|
|
|
|
|
|
const WIZARD_STEPS = [
|
2026-07-12 15:39:43 +08:00
|
|
|
|
{ id: 'create', title: '创建任务', desc: '填写任务信息与处理配置' },
|
|
|
|
|
|
{ id: 'upload', title: '上传文件', desc: '上传或接入待处理的源数据' },
|
2026-07-10 16:45:55 +08:00
|
|
|
|
{ id: 'preview', title: '数据预览', desc: '核对源文件与预览内容' },
|
|
|
|
|
|
{ id: 'generate', title: '开始生成', desc: '确认摘要并启动处理' },
|
|
|
|
|
|
{ id: 'results', title: '结果编辑与保存', desc: '检查、修改并保存结果' },
|
|
|
|
|
|
] as const satisfies ReadonlyArray<{ id: StepId; title: string; desc: string }>
|
2026-07-12 15:39:43 +08:00
|
|
|
|
const LEGACY_V3_STEP_IDS: ReadonlyArray<StepId> = ['create', 'preview', 'generate', 'results']
|
2026-07-10 16:45:55 +08:00
|
|
|
|
|
|
|
|
|
|
const currentStep = ref(0)
|
|
|
|
|
|
const currentStepId = computed<StepId>(() => WIZARD_STEPS[currentStep.value]?.id ?? 'create')
|
|
|
|
|
|
const task = reactive({ name: '', description: '' })
|
|
|
|
|
|
const processType = ref<ProcessType>('unstructured')
|
2026-07-11 14:49:10 +08:00
|
|
|
|
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,
|
|
|
|
|
|
datasetSplit: { train: 80, validation: 10, test: 10 },
|
|
|
|
|
|
})
|
2026-07-10 16:45:55 +08:00
|
|
|
|
interface UploadedDataFile {
|
|
|
|
|
|
uid: number | string
|
|
|
|
|
|
name: string
|
|
|
|
|
|
size: number
|
|
|
|
|
|
count: number
|
|
|
|
|
|
content: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const uploadedFiles = ref<UploadedDataFile[]>([])
|
|
|
|
|
|
|
2026-07-11 11:00:15 +08:00
|
|
|
|
const externalSource = reactive<ExternalDataSource>({
|
|
|
|
|
|
type: 'mysql',
|
|
|
|
|
|
url: '',
|
|
|
|
|
|
authMode: 'none',
|
|
|
|
|
|
username: '',
|
|
|
|
|
|
password: '',
|
|
|
|
|
|
token: '',
|
|
|
|
|
|
limit: 1000,
|
|
|
|
|
|
})
|
|
|
|
|
|
const externalPulling = ref(false)
|
|
|
|
|
|
const externalConnected = ref(false)
|
|
|
|
|
|
|
2026-07-10 16:45:55 +08:00
|
|
|
|
const fileName = computed(() => uploadedFiles.value.map(f => f.name).join(', '))
|
|
|
|
|
|
const fileSize = computed(() => uploadedFiles.value.reduce((sum, f) => sum + f.size, 0))
|
|
|
|
|
|
const fileCount = computed(() => uploadedFiles.value.reduce((sum, f) => sum + f.count, 0))
|
|
|
|
|
|
const previewSignature = ref('')
|
|
|
|
|
|
const previewItems = ref<PreviewItem[]>([])
|
|
|
|
|
|
const selectedPreviewFileId = ref<string | null>(null)
|
|
|
|
|
|
const selectedPreviewId = ref<string | null>(null)
|
|
|
|
|
|
const selectedPreviewIdsByFile = ref<Record<string, string>>({})
|
|
|
|
|
|
const results = ref<ResultItem[]>([])
|
|
|
|
|
|
const selectedResultId = ref<string | null>(null)
|
|
|
|
|
|
const dirty = ref(false)
|
|
|
|
|
|
const restoringDraft = ref(false)
|
|
|
|
|
|
let allowLeave = false
|
|
|
|
|
|
|
|
|
|
|
|
const generation = reactive<GenerationState>({
|
|
|
|
|
|
status: 'idle',
|
|
|
|
|
|
progress: 0,
|
|
|
|
|
|
message: '确认摘要后即可开始生成,过程中可查看实时进度。',
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
let generationTimer: ReturnType<typeof setInterval> | null = null
|
|
|
|
|
|
|
|
|
|
|
|
const modifiedPreviewCount = computed(() => previewItems.value.filter((item) => item.status !== 'original').length)
|
|
|
|
|
|
const activePreviewFile = computed(() => uploadedFiles.value.find((file) => String(file.uid) === selectedPreviewFileId.value))
|
|
|
|
|
|
const activePreviewItems = computed(() => previewItems.value.filter((item) => item.sourceFileId === selectedPreviewFileId.value))
|
|
|
|
|
|
const activeSourceText = computed(() => activePreviewFile.value?.content ?? '')
|
|
|
|
|
|
const previewFiles = computed(() => uploadedFiles.value.map((file) => {
|
|
|
|
|
|
const items = previewItems.value.filter((item) => item.sourceFileId === String(file.uid))
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: String(file.uid),
|
|
|
|
|
|
name: file.name,
|
|
|
|
|
|
count: items.length,
|
|
|
|
|
|
modifiedCount: items.filter((item) => item.status !== 'original').length,
|
|
|
|
|
|
}
|
|
|
|
|
|
}))
|
|
|
|
|
|
const primaryActionLabel = computed(() => {
|
2026-07-12 15:39:43 +08:00
|
|
|
|
if (currentStepId.value === 'create') return '继续:上传文件'
|
|
|
|
|
|
if (currentStepId.value === 'upload') return '继续:数据预览'
|
2026-07-10 16:45:55 +08:00
|
|
|
|
if (currentStepId.value === 'preview') return '确认预览并继续'
|
|
|
|
|
|
if (currentStepId.value === 'results') return '保存任务'
|
|
|
|
|
|
if (generation.status === 'running') return '正在生成'
|
|
|
|
|
|
if (generation.status === 'success') return '查看生成结果'
|
|
|
|
|
|
if (generation.status === 'failed') return '重新生成'
|
|
|
|
|
|
return '开始生成'
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const primaryActionIcon = computed(() => {
|
|
|
|
|
|
if (currentStepId.value === 'results') return 'fa-check'
|
|
|
|
|
|
if (currentStepId.value === 'generate' && generation.status !== 'success') return 'fa-play'
|
|
|
|
|
|
return 'fa-arrow-right'
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const previousStepLabel = computed(() => currentStep.value > 0
|
|
|
|
|
|
? WIZARD_STEPS[currentStep.value - 1].title
|
|
|
|
|
|
: '')
|
|
|
|
|
|
|
2026-07-12 15:39:43 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-10 16:45:55 +08:00
|
|
|
|
function draftSnapshot() {
|
|
|
|
|
|
return {
|
2026-07-11 14:49:10 +08:00
|
|
|
|
schemaVersion: DRAFT_SCHEMA_VERSION,
|
2026-07-10 16:45:55 +08:00
|
|
|
|
currentStep: currentStep.value,
|
2026-07-12 15:39:43 +08:00
|
|
|
|
currentStepId: currentStepId.value,
|
2026-07-10 16:45:55 +08:00
|
|
|
|
task: { ...task },
|
|
|
|
|
|
processType: processType.value,
|
2026-07-11 14:49:10 +08:00
|
|
|
|
structuredOptions: {
|
|
|
|
|
|
...structuredOptions.value,
|
|
|
|
|
|
preprocessOptions: [...structuredOptions.value.preprocessOptions],
|
|
|
|
|
|
datasetSplit: { ...structuredOptions.value.datasetSplit },
|
|
|
|
|
|
},
|
|
|
|
|
|
unstructuredOptions: {
|
|
|
|
|
|
...unstructuredOptions.value,
|
|
|
|
|
|
preprocessOptions: [...unstructuredOptions.value.preprocessOptions],
|
|
|
|
|
|
datasetSplit: { ...unstructuredOptions.value.datasetSplit },
|
|
|
|
|
|
},
|
2026-07-10 16:45:55 +08:00
|
|
|
|
uploadedFiles: uploadedFiles.value,
|
2026-07-11 11:00:15 +08:00
|
|
|
|
externalSource: { ...externalSource },
|
2026-07-10 16:45:55 +08:00
|
|
|
|
previewSignature: previewSignature.value,
|
|
|
|
|
|
previewItems: previewItems.value,
|
|
|
|
|
|
selectedPreviewFileId: selectedPreviewFileId.value,
|
|
|
|
|
|
selectedPreviewId: selectedPreviewId.value,
|
|
|
|
|
|
selectedPreviewIdsByFile: selectedPreviewIdsByFile.value,
|
|
|
|
|
|
results: results.value,
|
|
|
|
|
|
selectedResultId: selectedResultId.value,
|
|
|
|
|
|
generation: { ...generation },
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-11 14:49:10 +08:00
|
|
|
|
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,
|
|
|
|
|
|
datasetSplit,
|
|
|
|
|
|
} = unstructuredOptions.value
|
|
|
|
|
|
return {
|
|
|
|
|
|
semanticEnrichment,
|
|
|
|
|
|
qaPairsPerChunk,
|
|
|
|
|
|
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}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-10 16:45:55 +08:00
|
|
|
|
function persistDraft() {
|
|
|
|
|
|
if (restoringDraft.value) return
|
|
|
|
|
|
try {
|
|
|
|
|
|
localStorage.setItem(DRAFT_STORAGE_KEY, JSON.stringify(draftSnapshot()))
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
ElMessage.warning('草稿保存失败,请检查浏览器存储空间')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-11 14:49:10 +08:00
|
|
|
|
type DraftSnapshot = Partial<ReturnType<typeof draftSnapshot>> & {
|
2026-07-10 16:45:55 +08:00
|
|
|
|
fileName?: string
|
|
|
|
|
|
fileSize?: number
|
|
|
|
|
|
fileCount?: number
|
|
|
|
|
|
sourceText?: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function restoreDraft() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const raw = localStorage.getItem(DRAFT_STORAGE_KEY)
|
|
|
|
|
|
if (!raw) return
|
|
|
|
|
|
const snapshot = JSON.parse(raw) as DraftSnapshot
|
|
|
|
|
|
if (!snapshot?.uploadedFiles && (!snapshot?.fileName || !snapshot?.sourceText)) return
|
2026-07-12 15:39:43 +08:00
|
|
|
|
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'
|
2026-07-10 16:45:55 +08:00
|
|
|
|
|
|
|
|
|
|
restoringDraft.value = true
|
2026-07-12 15:39:43 +08:00
|
|
|
|
goToStep(requiresPreviewMigration ? 'create' : restoredStepId)
|
2026-07-10 16:45:55 +08:00
|
|
|
|
task.name = snapshot.task?.name || ''
|
|
|
|
|
|
task.description = snapshot.task?.description || ''
|
2026-07-11 11:00:15 +08:00
|
|
|
|
processType.value = snapshot.processType === 'structured' || snapshot.processType === 'external'
|
|
|
|
|
|
? snapshot.processType
|
|
|
|
|
|
: 'unstructured'
|
2026-07-11 14:49:10 +08:00
|
|
|
|
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) {
|
2026-07-12 15:39:43 +08:00
|
|
|
|
const restoredUnstructuredOptions = snapshot.unstructuredOptions
|
|
|
|
|
|
const restoredDatasetSplit = restoredUnstructuredOptions.datasetSplit
|
2026-07-11 14:49:10 +08:00
|
|
|
|
unstructuredOptions.value = {
|
|
|
|
|
|
...unstructuredOptions.value,
|
2026-07-12 15:39:43 +08:00
|
|
|
|
preprocessOptions: Array.isArray(restoredUnstructuredOptions.preprocessOptions)
|
|
|
|
|
|
? restoredUnstructuredOptions.preprocessOptions
|
2026-07-11 14:49:10 +08:00
|
|
|
|
: unstructuredOptions.value.preprocessOptions,
|
2026-07-12 15:39:43 +08:00
|
|
|
|
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,
|
2026-07-11 14:49:10 +08:00
|
|
|
|
datasetSplit: {
|
2026-07-12 15:39:43 +08:00
|
|
|
|
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,
|
2026-07-11 14:49:10 +08:00
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-10 16:45:55 +08:00
|
|
|
|
|
|
|
|
|
|
if (snapshot.uploadedFiles) {
|
|
|
|
|
|
uploadedFiles.value = Array.isArray(snapshot.uploadedFiles) ? snapshot.uploadedFiles : []
|
|
|
|
|
|
} else if (snapshot.fileName && snapshot.sourceText) {
|
|
|
|
|
|
// Migrate old draft
|
|
|
|
|
|
uploadedFiles.value = [{
|
|
|
|
|
|
uid: 'migrated-draft',
|
|
|
|
|
|
name: snapshot.fileName,
|
|
|
|
|
|
size: Number(snapshot.fileSize) || 0,
|
|
|
|
|
|
count: Number(snapshot.fileCount) || 0,
|
|
|
|
|
|
content: snapshot.sourceText
|
|
|
|
|
|
}]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-11 14:49:10 +08:00
|
|
|
|
previewSignature.value = requiresPreviewMigration ? '' : snapshot.previewSignature || ''
|
2026-07-11 11:00:15 +08:00
|
|
|
|
if (snapshot.externalSource) {
|
|
|
|
|
|
Object.assign(externalSource, snapshot.externalSource)
|
|
|
|
|
|
}
|
2026-07-10 16:45:55 +08:00
|
|
|
|
const defaultSourceFileId = String(uploadedFiles.value[0]?.uid ?? '')
|
2026-07-11 14:49:10 +08:00
|
|
|
|
previewItems.value = !requiresPreviewMigration && Array.isArray(snapshot.previewItems)
|
2026-07-10 16:45:55 +08:00
|
|
|
|
? snapshot.previewItems.map((item) => ({ ...item, sourceFileId: item.sourceFileId ?? defaultSourceFileId }))
|
|
|
|
|
|
: []
|
|
|
|
|
|
selectedPreviewFileId.value = snapshot.selectedPreviewFileId || defaultSourceFileId || null
|
2026-07-11 14:49:10 +08:00
|
|
|
|
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
|
2026-07-12 15:39:43 +08:00
|
|
|
|
if (!requiresPreviewMigration) {
|
|
|
|
|
|
const restoredGeneration = snapshot.generation || {}
|
|
|
|
|
|
Object.assign(generation, restoredGeneration)
|
|
|
|
|
|
if (generation.status === 'running') {
|
|
|
|
|
|
generation.status = 'idle'
|
|
|
|
|
|
generation.progress = 0
|
|
|
|
|
|
generation.message = '草稿已恢复,请重新开始生成。'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-10 16:45:55 +08:00
|
|
|
|
dirty.value = false
|
|
|
|
|
|
nextTick(() => { restoringDraft.value = false })
|
2026-07-11 14:49:10 +08:00
|
|
|
|
ElMessage.info(requiresPreviewMigration
|
|
|
|
|
|
? '已恢复任务信息,切分规则已更新,请重新生成预览'
|
|
|
|
|
|
: '已恢复上次保存的草稿')
|
2026-07-10 16:45:55 +08:00
|
|
|
|
} catch {
|
|
|
|
|
|
localStorage.removeItem(DRAFT_STORAGE_KEY)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
2026-07-11 14:49:10 +08:00
|
|
|
|
[() => task.name, () => task.description, processType, structuredOptions, unstructuredOptions, externalSource],
|
2026-07-10 16:45:55 +08:00
|
|
|
|
() => {
|
|
|
|
|
|
if (!restoringDraft.value) dirty.value = true
|
|
|
|
|
|
},
|
2026-07-11 11:00:15 +08:00
|
|
|
|
{ deep: true },
|
2026-07-10 16:45:55 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-07-12 15:39:43 +08:00
|
|
|
|
watch(processType, (nextType, previousType) => {
|
|
|
|
|
|
if (restoringDraft.value || nextType === previousType || uploadedFiles.value.length === 0) return
|
|
|
|
|
|
resetSourceDataForProcessTypeChange()
|
|
|
|
|
|
ElMessage.info('处理类型已变更,请重新上传或拉取匹配的源数据')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-07-11 14:49:10 +08:00
|
|
|
|
watch(generationOptionsSignature, (currentSignature, previousSignature) => {
|
|
|
|
|
|
if (restoringDraft.value || currentSignature === previousSignature) return
|
|
|
|
|
|
resetDownstream()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-07-10 16:45:55 +08:00
|
|
|
|
watch(
|
2026-07-11 14:49:10 +08:00
|
|
|
|
[currentStep, task, processType, structuredOptions, unstructuredOptions, uploadedFiles, externalSource, previewSignature,
|
2026-07-10 16:45:55 +08:00
|
|
|
|
previewItems, selectedPreviewFileId, selectedPreviewId, selectedPreviewIdsByFile,
|
|
|
|
|
|
results, selectedResultId, generation],
|
|
|
|
|
|
persistDraft,
|
|
|
|
|
|
{ deep: true },
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
function scrollToStepTop() {
|
|
|
|
|
|
document.querySelector<HTMLElement>('.layout-content')?.scrollTo({ top: 0, behavior: 'smooth' })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
watch(currentStep, () => nextTick(scrollToStepTop))
|
|
|
|
|
|
|
|
|
|
|
|
async function handleFileChange(uploadFile: UploadFile) {
|
|
|
|
|
|
const raw = uploadFile.raw
|
|
|
|
|
|
if (!raw) return
|
|
|
|
|
|
if (raw.size > 200 * 1024 * 1024) {
|
|
|
|
|
|
ElMessage.warning('单文件不能超过 200MB')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const extension = raw.name.split('.').pop()?.toLowerCase() ?? ''
|
|
|
|
|
|
const textExtensions = ['txt', 'md', 'json', 'jsonl', 'csv']
|
|
|
|
|
|
let content = ''
|
|
|
|
|
|
if (textExtensions.includes(extension)) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
content = await raw.text()
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
content = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const fileContent = content.trim() ? content : DEFAULT_SOURCE_TEXT
|
|
|
|
|
|
const linesCount = fileContent.split('\n').filter((line) => line.trim()).length
|
|
|
|
|
|
|
|
|
|
|
|
// Prevent duplicate upload of the same file
|
|
|
|
|
|
if (!uploadedFiles.value.some(f => f.name === raw.name && f.size === raw.size)) {
|
|
|
|
|
|
uploadedFiles.value.push({
|
|
|
|
|
|
uid: uploadFile.uid || Date.now() + Math.random(),
|
|
|
|
|
|
name: raw.name,
|
|
|
|
|
|
size: raw.size,
|
|
|
|
|
|
count: linesCount,
|
|
|
|
|
|
content: fileContent
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
dirty.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function useSampleFile() {
|
|
|
|
|
|
uploadedFiles.value = [{
|
|
|
|
|
|
uid: 'sample-1',
|
|
|
|
|
|
name: 'finance_qa.jsonl',
|
|
|
|
|
|
size: 128 * 1024 * 1024,
|
|
|
|
|
|
count: DEFAULT_SOURCE_TEXT.split('\n').filter((line) => line.trim()).length,
|
|
|
|
|
|
content: DEFAULT_SOURCE_TEXT
|
|
|
|
|
|
}]
|
|
|
|
|
|
dirty.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-11 11:00:15 +08:00
|
|
|
|
function updateExternalSource(value: ExternalDataSource) {
|
|
|
|
|
|
Object.assign(externalSource, value)
|
|
|
|
|
|
externalConnected.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleTestConnection() {
|
|
|
|
|
|
if (!externalSource.url.trim()) {
|
|
|
|
|
|
ElMessage.warning('请先填写数据源地址')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
externalPulling.value = true
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
externalPulling.value = false
|
|
|
|
|
|
externalConnected.value = true
|
|
|
|
|
|
ElMessage.success('数据源连接测试成功')
|
|
|
|
|
|
}, 1500)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handlePullData() {
|
|
|
|
|
|
if (!externalSource.url.trim()) {
|
|
|
|
|
|
ElMessage.warning('请先填写数据源地址')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
externalPulling.value = true
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
externalPulling.value = false
|
|
|
|
|
|
externalConnected.value = true
|
|
|
|
|
|
const typeName = externalSource.type.toUpperCase()
|
|
|
|
|
|
const id = `external-${Date.now()}`
|
|
|
|
|
|
uploadedFiles.value.push({
|
|
|
|
|
|
uid: id,
|
|
|
|
|
|
name: `${typeName} 拉取数据 ${new Date().toLocaleString('zh-CN')}`,
|
|
|
|
|
|
size: Math.min(externalSource.limit, 5000) * 64,
|
|
|
|
|
|
count: Math.min(externalSource.limit, DEFAULT_SOURCE_TEXT.split('\n').filter((line) => line.trim()).length),
|
|
|
|
|
|
content: DEFAULT_SOURCE_TEXT,
|
|
|
|
|
|
})
|
|
|
|
|
|
dirty.value = true
|
|
|
|
|
|
ElMessage.success(`已成功拉取 ${uploadedFiles.value[uploadedFiles.value.length - 1].count.toLocaleString()} 条数据`)
|
|
|
|
|
|
}, 2000)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-10 16:45:55 +08:00
|
|
|
|
function handleRemoveFile(uid: string | number) {
|
|
|
|
|
|
const index = uploadedFiles.value.findIndex(f => f.uid === uid)
|
|
|
|
|
|
if (index > -1) {
|
|
|
|
|
|
uploadedFiles.value.splice(index, 1)
|
|
|
|
|
|
previewSignature.value = ''
|
|
|
|
|
|
previewItems.value = []
|
|
|
|
|
|
selectedPreviewId.value = null
|
|
|
|
|
|
resetDownstream()
|
|
|
|
|
|
dirty.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-12 15:39:43 +08:00
|
|
|
|
function resetSourceDataForProcessTypeChange() {
|
|
|
|
|
|
uploadedFiles.value = []
|
|
|
|
|
|
previewSignature.value = ''
|
|
|
|
|
|
previewItems.value = []
|
|
|
|
|
|
selectedPreviewFileId.value = null
|
|
|
|
|
|
selectedPreviewId.value = null
|
|
|
|
|
|
selectedPreviewIdsByFile.value = {}
|
|
|
|
|
|
externalConnected.value = false
|
|
|
|
|
|
resetDownstream()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-10 16:45:55 +08:00
|
|
|
|
function resetDownstream() {
|
|
|
|
|
|
stopGenerationTimer()
|
|
|
|
|
|
generation.status = 'idle'
|
|
|
|
|
|
generation.progress = 0
|
|
|
|
|
|
generation.message = '确认摘要后即可开始生成,过程中可查看实时进度。'
|
|
|
|
|
|
results.value = []
|
|
|
|
|
|
selectedResultId.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function nextFromCreate() {
|
|
|
|
|
|
const valid = await taskSetupRef.value?.validate()
|
|
|
|
|
|
if (!valid) return
|
2026-07-12 15:39:43 +08:00
|
|
|
|
goToStep('upload')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function nextFromUpload() {
|
2026-07-10 16:45:55 +08:00
|
|
|
|
if (uploadedFiles.value.length === 0) {
|
2026-07-11 11:00:15 +08:00
|
|
|
|
ElMessage.warning(processType.value === 'external' ? '请先拉取至少一个数据源' : '请上传至少一个源数据文件')
|
2026-07-10 16:45:55 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-11 14:49:10 +08:00
|
|
|
|
const signature = buildPreviewSignature()
|
2026-07-10 16:45:55 +08:00
|
|
|
|
if (signature !== previewSignature.value) {
|
|
|
|
|
|
previewItems.value = uploadedFiles.value.flatMap((file) =>
|
2026-07-11 14:49:10 +08:00
|
|
|
|
buildPreviewItems(
|
|
|
|
|
|
file.content,
|
|
|
|
|
|
processType.value,
|
|
|
|
|
|
String(file.uid),
|
|
|
|
|
|
processType.value === 'unstructured' ? unstructuredOptions.value : undefined,
|
|
|
|
|
|
),
|
2026-07-10 16:45:55 +08:00
|
|
|
|
)
|
|
|
|
|
|
selectedPreviewFileId.value = String(uploadedFiles.value[0]?.uid ?? '') || null
|
|
|
|
|
|
selectedPreviewId.value = activePreviewItems.value[0]?.id ?? null
|
|
|
|
|
|
selectedPreviewIdsByFile.value = selectedPreviewId.value && selectedPreviewFileId.value
|
|
|
|
|
|
? { [selectedPreviewFileId.value]: selectedPreviewId.value }
|
|
|
|
|
|
: {}
|
|
|
|
|
|
previewSignature.value = signature
|
|
|
|
|
|
resetDownstream()
|
|
|
|
|
|
}
|
2026-07-12 15:39:43 +08:00
|
|
|
|
goToStep('preview')
|
2026-07-10 16:45:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function selectPreviewFile(fileId: string) {
|
|
|
|
|
|
if (selectedPreviewFileId.value && selectedPreviewId.value) {
|
|
|
|
|
|
selectedPreviewIdsByFile.value[selectedPreviewFileId.value] = selectedPreviewId.value
|
|
|
|
|
|
}
|
|
|
|
|
|
selectedPreviewFileId.value = fileId
|
|
|
|
|
|
selectedPreviewId.value = selectedPreviewIdsByFile.value[fileId]
|
|
|
|
|
|
|| activePreviewItems.value[0]?.id
|
|
|
|
|
|
|| null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function selectPreviewItem(id: string) {
|
|
|
|
|
|
selectedPreviewId.value = id
|
|
|
|
|
|
if (selectedPreviewFileId.value) selectedPreviewIdsByFile.value[selectedPreviewFileId.value] = id
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function updatePreviewContent(id: string, value: string) {
|
|
|
|
|
|
const item = previewItems.value.find((entry) => entry.id === id)
|
|
|
|
|
|
if (!item) return
|
|
|
|
|
|
item.editedContent = value
|
|
|
|
|
|
item.tokenCount = Math.max(1, Math.ceil(value.length / 2))
|
|
|
|
|
|
item.status = value === item.originalContent ? 'original' : item.sourceStart == null ? 'manual' : 'modified'
|
2026-07-11 14:49:10 +08:00
|
|
|
|
resetDownstream()
|
2026-07-10 16:45:55 +08:00
|
|
|
|
dirty.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function restorePreviewItem(id: string) {
|
|
|
|
|
|
const item = previewItems.value.find((entry) => entry.id === id)
|
|
|
|
|
|
if (!item || item.sourceStart == null) return
|
|
|
|
|
|
item.editedContent = item.originalContent
|
|
|
|
|
|
item.tokenCount = Math.max(1, Math.ceil(item.originalContent.length / 2))
|
|
|
|
|
|
item.status = 'original'
|
2026-07-11 14:49:10 +08:00
|
|
|
|
resetDownstream()
|
2026-07-10 16:45:55 +08:00
|
|
|
|
dirty.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function addPreviewItem() {
|
|
|
|
|
|
if (!selectedPreviewFileId.value) return
|
|
|
|
|
|
const id = `manual-${Date.now()}`
|
|
|
|
|
|
previewItems.value.push({
|
|
|
|
|
|
id,
|
|
|
|
|
|
sourceFileId: selectedPreviewFileId.value,
|
|
|
|
|
|
originalContent: '',
|
|
|
|
|
|
editedContent: '',
|
|
|
|
|
|
sourceStart: null,
|
|
|
|
|
|
sourceEnd: null,
|
|
|
|
|
|
sourceStartLine: null,
|
|
|
|
|
|
sourceEndLine: null,
|
|
|
|
|
|
tokenCount: 1,
|
|
|
|
|
|
status: 'manual',
|
|
|
|
|
|
})
|
|
|
|
|
|
selectPreviewItem(id)
|
2026-07-11 14:49:10 +08:00
|
|
|
|
resetDownstream()
|
2026-07-10 16:45:55 +08:00
|
|
|
|
dirty.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function removePreviewItem(id: string) {
|
2026-07-11 14:49:10 +08:00
|
|
|
|
const confirmed = await confirmDialogRef.value?.open({
|
|
|
|
|
|
title: '删除预览内容?',
|
|
|
|
|
|
message: '删除只影响本次处理,不会修改源文件。删除后可重新从源文件生成预览。',
|
|
|
|
|
|
confirmText: '删除',
|
|
|
|
|
|
cancelText: '取消',
|
|
|
|
|
|
tone: 'danger',
|
|
|
|
|
|
})
|
|
|
|
|
|
if (!confirmed) return
|
2026-07-10 16:45:55 +08:00
|
|
|
|
|
|
|
|
|
|
const index = previewItems.value.findIndex((item) => item.id === id)
|
|
|
|
|
|
if (index < 0) return
|
|
|
|
|
|
previewItems.value.splice(index, 1)
|
|
|
|
|
|
selectedPreviewId.value = activePreviewItems.value[Math.min(index, activePreviewItems.value.length - 1)]?.id ?? null
|
|
|
|
|
|
if (selectedPreviewFileId.value && selectedPreviewId.value) {
|
|
|
|
|
|
selectedPreviewIdsByFile.value[selectedPreviewFileId.value] = selectedPreviewId.value
|
|
|
|
|
|
}
|
2026-07-11 14:49:10 +08:00
|
|
|
|
resetDownstream()
|
2026-07-10 16:45:55 +08:00
|
|
|
|
dirty.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function stopGenerationTimer() {
|
|
|
|
|
|
if (generationTimer) clearInterval(generationTimer)
|
|
|
|
|
|
generationTimer = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function startGeneration() {
|
|
|
|
|
|
stopGenerationTimer()
|
|
|
|
|
|
generation.status = 'running'
|
|
|
|
|
|
generation.progress = 0
|
|
|
|
|
|
generation.message = '正在应用预览修改并生成标准化结果,请稍候。'
|
|
|
|
|
|
|
|
|
|
|
|
generationTimer = setInterval(() => {
|
|
|
|
|
|
generation.progress = Math.min(100, generation.progress + 8)
|
|
|
|
|
|
if (generation.progress < 100) return
|
|
|
|
|
|
|
|
|
|
|
|
stopGenerationTimer()
|
|
|
|
|
|
generation.status = 'success'
|
2026-07-11 14:49:10 +08:00
|
|
|
|
results.value = createResults(
|
|
|
|
|
|
previewItems.value,
|
|
|
|
|
|
processType.value === 'structured'
|
|
|
|
|
|
? structuredOptions.value
|
|
|
|
|
|
: processType.value === 'unstructured'
|
|
|
|
|
|
? unstructuredOptions.value
|
|
|
|
|
|
: undefined,
|
|
|
|
|
|
)
|
|
|
|
|
|
generation.message = `已完成 ${results.value.length.toLocaleString()} 条数据处理,可进入结果页检查。`
|
2026-07-10 16:45:55 +08:00
|
|
|
|
selectedResultId.value = results.value[0]?.id ?? null
|
|
|
|
|
|
dirty.value = true
|
|
|
|
|
|
ElMessage.success('数据处理完成')
|
|
|
|
|
|
}, 180)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function stopGeneration() {
|
|
|
|
|
|
stopGenerationTimer()
|
|
|
|
|
|
generation.status = 'failed'
|
|
|
|
|
|
generation.message = '任务已停止,预览修改仍然保留,可以重新生成。'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function updateResultField(id: string, field: 'instruction' | 'input' | 'output', value: string) {
|
|
|
|
|
|
const item = results.value.find((entry) => entry.id === id)
|
|
|
|
|
|
if (!item) return
|
|
|
|
|
|
item[field] = value
|
|
|
|
|
|
const valid = item.instruction.trim() && item.output.trim()
|
|
|
|
|
|
item.error = valid ? undefined : 'Instruction 和 Output 不能为空'
|
|
|
|
|
|
const changed = item.instruction !== item.originalInstruction
|
|
|
|
|
|
|| item.input !== item.originalInput
|
|
|
|
|
|
|| item.output !== item.originalOutput
|
|
|
|
|
|
item.status = item.error ? 'invalid' : changed ? 'modified' : 'valid'
|
|
|
|
|
|
dirty.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function restoreResult(id: string) {
|
|
|
|
|
|
const item = results.value.find((entry) => entry.id === id)
|
|
|
|
|
|
if (!item) return
|
|
|
|
|
|
item.instruction = item.originalInstruction
|
|
|
|
|
|
item.input = item.originalInput
|
|
|
|
|
|
item.output = item.originalOutput
|
|
|
|
|
|
item.error = undefined
|
|
|
|
|
|
item.status = 'valid'
|
|
|
|
|
|
dirty.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function validateResults() {
|
|
|
|
|
|
let firstInvalidId: string | null = null
|
|
|
|
|
|
for (const item of results.value) {
|
|
|
|
|
|
if (!item.instruction.trim() || !item.output.trim()) {
|
|
|
|
|
|
item.error = 'Instruction 和 Output 不能为空'
|
|
|
|
|
|
item.status = 'invalid'
|
|
|
|
|
|
firstInvalidId ??= item.id
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (firstInvalidId) selectedResultId.value = firstInvalidId
|
|
|
|
|
|
return firstInvalidId == null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function handlePrimaryAction() {
|
|
|
|
|
|
if (currentStepId.value === 'create') {
|
|
|
|
|
|
await nextFromCreate()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-07-12 15:39:43 +08:00
|
|
|
|
if (currentStepId.value === 'upload') {
|
|
|
|
|
|
nextFromUpload()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-07-10 16:45:55 +08:00
|
|
|
|
if (currentStepId.value === 'preview') {
|
|
|
|
|
|
if (!previewItems.value.length) {
|
|
|
|
|
|
ElMessage.warning('当前没有可生成的预览内容')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-07-12 15:39:43 +08:00
|
|
|
|
goToStep('generate')
|
2026-07-10 16:45:55 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (currentStepId.value === 'generate') {
|
|
|
|
|
|
if (generation.status === 'success') {
|
2026-07-12 15:39:43 +08:00
|
|
|
|
goToStep('results')
|
2026-07-10 16:45:55 +08:00
|
|
|
|
} else if (generation.status !== 'running') {
|
|
|
|
|
|
startGeneration()
|
|
|
|
|
|
}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
await saveTask()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleBack() {
|
|
|
|
|
|
if (generation.status === 'running') {
|
|
|
|
|
|
ElMessage.warning('请先停止当前生成任务')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (currentStep.value > 0) currentStep.value -= 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function saveTask() {
|
|
|
|
|
|
if (!validateResults()) {
|
|
|
|
|
|
ElMessage.warning('请先修正校验失败的结果')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
dirty.value = false
|
|
|
|
|
|
localStorage.removeItem(DRAFT_STORAGE_KEY)
|
|
|
|
|
|
allowLeave = true
|
|
|
|
|
|
ElMessage.success('数据处理任务已保存')
|
|
|
|
|
|
await router.push('/data-process')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function handleCancel() {
|
|
|
|
|
|
if (!dirty.value) {
|
|
|
|
|
|
allowLeave = true
|
|
|
|
|
|
router.back()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-07-11 14:49:10 +08:00
|
|
|
|
const confirmed = await confirmDialogRef.value?.open({
|
|
|
|
|
|
title: '确认离开当前页面?',
|
|
|
|
|
|
message: '当前存在未保存修改,离开后这些修改将不会保留。',
|
|
|
|
|
|
confirmText: '放弃修改',
|
|
|
|
|
|
cancelText: '继续编辑',
|
|
|
|
|
|
tone: 'danger',
|
|
|
|
|
|
})
|
|
|
|
|
|
if (!confirmed) return
|
|
|
|
|
|
allowLeave = true
|
|
|
|
|
|
router.back()
|
2026-07-10 16:45:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-11 14:49:10 +08:00
|
|
|
|
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)
|
2026-07-10 16:45:55 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
onBeforeUnmount(stopGenerationTimer)
|
|
|
|
|
|
onMounted(restoreDraft)
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<div class="create-wizard-layout">
|
|
|
|
|
|
<main class="wizard-main">
|
|
|
|
|
|
<div class="wizard-main-inner">
|
|
|
|
|
|
<div class="wizard-steps-container">
|
|
|
|
|
|
<div class="custom-wizard-steps">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="(step, index) in WIZARD_STEPS"
|
|
|
|
|
|
:key="step.id"
|
|
|
|
|
|
class="step-item"
|
|
|
|
|
|
:class="{
|
|
|
|
|
|
'is-active': currentStep === index,
|
|
|
|
|
|
'is-completed': currentStep > index
|
|
|
|
|
|
}"
|
2026-07-12 15:39:43 +08:00
|
|
|
|
:aria-current="currentStep === index ? 'step' : undefined"
|
2026-07-10 16:45:55 +08:00
|
|
|
|
>
|
|
|
|
|
|
<div v-if="index !== 0" class="step-connector"></div>
|
|
|
|
|
|
<div class="step-node">
|
|
|
|
|
|
<div class="step-icon">
|
|
|
|
|
|
<i v-if="currentStep > index" class="fa fa-check" />
|
|
|
|
|
|
<span v-else>{{ index + 1 }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="step-text">
|
|
|
|
|
|
<div class="step-title">{{ step.title }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="wizard-content">
|
|
|
|
|
|
<TaskSetupStep
|
|
|
|
|
|
v-if="currentStepId === 'create'"
|
|
|
|
|
|
ref="taskSetupRef"
|
|
|
|
|
|
v-model:name="task.name"
|
|
|
|
|
|
v-model:description="task.description"
|
|
|
|
|
|
v-model:process-type="processType"
|
2026-07-11 14:49:10 +08:00
|
|
|
|
v-model:structured-options="structuredOptions"
|
|
|
|
|
|
v-model:unstructured-options="unstructuredOptions"
|
2026-07-12 15:39:43 +08:00
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<SourceUploadStep
|
|
|
|
|
|
v-else-if="currentStepId === 'upload'"
|
|
|
|
|
|
:process-type="processType"
|
2026-07-10 16:45:55 +08:00
|
|
|
|
:uploaded-files="uploadedFiles"
|
2026-07-11 11:00:15 +08:00
|
|
|
|
:external-source="externalSource"
|
|
|
|
|
|
:external-pulling="externalPulling"
|
|
|
|
|
|
:external-connected="externalConnected"
|
|
|
|
|
|
@update:external-source="updateExternalSource"
|
2026-07-10 16:45:55 +08:00
|
|
|
|
@file-change="handleFileChange"
|
|
|
|
|
|
@remove-file="handleRemoveFile"
|
|
|
|
|
|
@use-sample="useSampleFile"
|
2026-07-11 11:00:15 +08:00
|
|
|
|
@test-connection="handleTestConnection"
|
|
|
|
|
|
@pull-data="handlePullData"
|
2026-07-10 16:45:55 +08:00
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<PreviewCompareStep
|
|
|
|
|
|
v-else-if="currentStepId === 'preview'"
|
|
|
|
|
|
:selected-id="selectedPreviewId"
|
|
|
|
|
|
:selected-file-id="selectedPreviewFileId"
|
|
|
|
|
|
:source-text="activeSourceText"
|
|
|
|
|
|
:items="activePreviewItems"
|
|
|
|
|
|
:process-type="processType"
|
|
|
|
|
|
:file-name="activePreviewFile?.name ?? ''"
|
|
|
|
|
|
:files="previewFiles"
|
|
|
|
|
|
@update:selected-id="selectPreviewItem"
|
|
|
|
|
|
@update:selected-file-id="selectPreviewFile"
|
|
|
|
|
|
@update:item-content="updatePreviewContent"
|
|
|
|
|
|
@restore:item="restorePreviewItem"
|
|
|
|
|
|
@add:item="addPreviewItem"
|
|
|
|
|
|
@remove:item="removePreviewItem"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<GenerationStep
|
|
|
|
|
|
v-else-if="currentStepId === 'generate'"
|
|
|
|
|
|
:task-name="task.name"
|
|
|
|
|
|
:process-type="processType"
|
|
|
|
|
|
:file-name="fileName"
|
|
|
|
|
|
:preview-count="previewItems.length"
|
|
|
|
|
|
:modified-count="modifiedPreviewCount"
|
|
|
|
|
|
:generation="generation"
|
|
|
|
|
|
@stop="stopGeneration"
|
|
|
|
|
|
@retry="startGeneration"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<ResultEditorStep
|
2026-07-12 15:39:43 +08:00
|
|
|
|
v-else-if="currentStepId === 'results'"
|
2026-07-10 16:45:55 +08:00
|
|
|
|
v-model:selected-id="selectedResultId"
|
|
|
|
|
|
:items="results"
|
|
|
|
|
|
@update:field="updateResultField"
|
|
|
|
|
|
@restore:item="restoreResult"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</main>
|
|
|
|
|
|
|
|
|
|
|
|
<footer class="wizard-footer">
|
|
|
|
|
|
<div class="footer-left">
|
|
|
|
|
|
<el-button v-if="currentStep > 0" @click="handleBack">
|
|
|
|
|
|
<i class="fa fa-arrow-left" style="margin-right: 6px;" /> 返回:{{ previousStepLabel }}
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
<el-button v-else @click="handleCancel">取消</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="footer-center">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="footer-right">
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
class="wizard-primary-action"
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
:loading="generation.status === 'running'"
|
|
|
|
|
|
:disabled="currentStepId === 'generate' && generation.status === 'running'"
|
|
|
|
|
|
@click="handlePrimaryAction"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ primaryActionLabel }} <i class="fa" :class="primaryActionIcon" style="margin-left: 6px;" />
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</footer>
|
|
|
|
|
|
</div>
|
2026-07-11 14:49:10 +08:00
|
|
|
|
|
|
|
|
|
|
<AppConfirmDialog ref="confirmDialogRef" />
|
2026-07-10 16:45:55 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
|
.create-wizard-layout {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
border: 1px solid #eef0f5;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.wizard-main {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
padding: 32px;
|
|
|
|
|
|
|
|
|
|
|
|
/* 自定义滚动条 */
|
|
|
|
|
|
&::-webkit-scrollbar {
|
|
|
|
|
|
width: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
&::-webkit-scrollbar-thumb {
|
|
|
|
|
|
background: #cbd5e1;
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.wizard-main-inner {
|
|
|
|
|
|
min-height: 100%;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.wizard-steps-container {
|
|
|
|
|
|
margin-bottom: 32px;
|
|
|
|
|
|
padding-bottom: 24px;
|
|
|
|
|
|
border-bottom: 1px dashed #e2e8f0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.custom-wizard-steps {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
max-width: 860px;
|
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
|
padding: 0 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
|
|
|
|
|
|
&:first-child {
|
|
|
|
|
|
flex: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-connector {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
height: 2px;
|
|
|
|
|
|
background-color: #e2e8f0;
|
|
|
|
|
|
margin: 0 16px;
|
|
|
|
|
|
transition: background-color 0.3s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-item.is-completed .step-connector,
|
|
|
|
|
|
.step-item.is-active .step-connector {
|
|
|
|
|
|
background-color: #5146e5;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-node {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-icon {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
width: 28px;
|
|
|
|
|
|
height: 28px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
border: 2px solid #cbd5e1;
|
|
|
|
|
|
background-color: #fff;
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
font-weight: 650;
|
|
|
|
|
|
transition: all 0.3s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-title {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* State: Active */
|
|
|
|
|
|
.step-item.is-active {
|
|
|
|
|
|
.step-icon {
|
|
|
|
|
|
border-color: #5146e5;
|
|
|
|
|
|
background-color: #eef2ff;
|
|
|
|
|
|
color: #5146e5;
|
|
|
|
|
|
}
|
|
|
|
|
|
.step-title {
|
|
|
|
|
|
color: #1e293b;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* State: Completed */
|
|
|
|
|
|
.step-item.is-completed {
|
|
|
|
|
|
.step-icon {
|
|
|
|
|
|
background-color: #5146e5;
|
|
|
|
|
|
border-color: #5146e5;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
.step-title {
|
|
|
|
|
|
color: #1e293b;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.wizard-content {
|
|
|
|
|
|
min-height: 400px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.wizard-footer {
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
height: 64px;
|
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
|
border-top: 1px solid #e2e8f0;
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 1fr auto 1fr;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
padding: 0 32px;
|
|
|
|
|
|
box-shadow: 0 -4px 6px -1px rgba(0, 0, 0, 0.02);
|
|
|
|
|
|
|
|
|
|
|
|
.footer-left {
|
|
|
|
|
|
justify-self: start;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.footer-center {
|
|
|
|
|
|
justify-self: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.footer-right {
|
|
|
|
|
|
justify-self: end;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.wizard-primary-action {
|
|
|
|
|
|
min-width: 160px;
|
|
|
|
|
|
}
|
2026-07-12 15:39:43 +08:00
|
|
|
|
|
|
|
|
|
|
@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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-10 16:45:55 +08:00
|
|
|
|
</style>
|