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'
|
2026-07-13 11:47:33 +08:00
|
|
|
|
import { storeToRefs } from 'pinia'
|
2026-07-11 14:49:10 +08:00
|
|
|
|
import AppConfirmDialog from '@/components/AppConfirmDialog.vue'
|
2026-07-10 16:45:55 +08:00
|
|
|
|
import TaskSetupStep from './create/TaskSetupStep.vue'
|
2026-07-13 17:12:49 +08:00
|
|
|
|
import ModelSelectionStep from './create/ModelSelectionStep.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'
|
2026-07-30 16:53:54 +08:00
|
|
|
|
import { DEFAULT_SOURCE_TEXT, estimateTokenCount, isManualPreviewItem } from './create/previewModel'
|
2026-07-13 15:28:48 +08:00
|
|
|
|
import {
|
|
|
|
|
|
createDefaultStructuredOptions,
|
|
|
|
|
|
createDefaultUnstructuredOptions,
|
2026-07-25 22:41:06 +08:00
|
|
|
|
generationAffectingOptionsFor,
|
|
|
|
|
|
previewAffectingOptionsFor,
|
2026-07-13 15:28:48 +08:00
|
|
|
|
} from './create/dataProcessCreateState'
|
|
|
|
|
|
import { useDataProcessGeneration } from './create/useDataProcessGeneration'
|
2026-07-24 11:28:02 +08:00
|
|
|
|
import { useDataProcessPreviewBuild } from './create/useDataProcessPreviewBuild'
|
2026-07-25 22:41:06 +08:00
|
|
|
|
import { useDataProcessRegeneration } from './create/useDataProcessRegeneration'
|
2026-07-24 11:28:02 +08:00
|
|
|
|
import {
|
2026-07-30 16:53:54 +08:00
|
|
|
|
loadCanonicalSourceContent,
|
2026-07-24 11:28:02 +08:00
|
|
|
|
mapDataProcessSourceFile,
|
|
|
|
|
|
useDataProcessSourceUpload,
|
|
|
|
|
|
validateSourceFileSelection,
|
|
|
|
|
|
} from './create/useDataProcessSourceUpload'
|
2026-07-13 11:47:33 +08:00
|
|
|
|
import { useModelsStore } from '@/stores/models'
|
2026-07-23 15:10:13 +08:00
|
|
|
|
import {
|
2026-07-28 10:56:05 +08:00
|
|
|
|
confirmDataProcessResults,
|
2026-07-23 15:10:13 +08:00
|
|
|
|
createDataProcessPreview,
|
|
|
|
|
|
createDataProcessTask,
|
|
|
|
|
|
deleteDataProcessPreview,
|
|
|
|
|
|
deleteDataProcessSourceFile,
|
|
|
|
|
|
getDataProcessPreview,
|
|
|
|
|
|
pullDataProcessExternalSource,
|
|
|
|
|
|
testDataProcessExternalSource,
|
|
|
|
|
|
updateDataProcessPreview,
|
|
|
|
|
|
updateDataProcessTask,
|
2026-07-28 10:56:05 +08:00
|
|
|
|
updateDataProcessWorkflowStep,
|
2026-07-23 15:10:13 +08:00
|
|
|
|
type DataProcessExternalSourcePayload,
|
|
|
|
|
|
type DataProcessPreviewItem,
|
|
|
|
|
|
} from '@/api/modules/dataProcess'
|
2026-07-28 10:56:05 +08:00
|
|
|
|
import type {
|
|
|
|
|
|
DataProcessConfig,
|
|
|
|
|
|
DataProcessPreviewProgress,
|
|
|
|
|
|
DataProcessTask,
|
|
|
|
|
|
DataProcessWorkflowStep,
|
|
|
|
|
|
} from '@/types/dataProcess'
|
2026-07-11 14:49:10 +08:00
|
|
|
|
import type {
|
|
|
|
|
|
ExternalDataSource,
|
2026-07-13 17:12:49 +08:00
|
|
|
|
GenerationControlOptions,
|
2026-07-11 14:49:10 +08:00
|
|
|
|
PreviewItem,
|
|
|
|
|
|
ProcessType,
|
|
|
|
|
|
StepId,
|
|
|
|
|
|
StructuredProcessOptions,
|
|
|
|
|
|
UnstructuredProcessOptions,
|
2026-07-13 15:28:48 +08:00
|
|
|
|
UploadedDataFile,
|
2026-07-11 14:49:10 +08:00
|
|
|
|
} from './create/types'
|
2026-07-10 16:45:55 +08:00
|
|
|
|
|
|
|
|
|
|
const router = useRouter()
|
2026-07-13 11:47:33 +08:00
|
|
|
|
const modelsStore = useModelsStore()
|
2026-07-25 22:41:06 +08:00
|
|
|
|
const { list: modelList, loaded: modelsLoaded } = storeToRefs(modelsStore)
|
2026-07-24 11:28:02 +08:00
|
|
|
|
// 候选范围与模型管理保持一致,不在数据处理页面重复定义模型过滤规则。
|
|
|
|
|
|
const generationModels = computed(() => modelList.value)
|
2026-07-10 16:45:55 +08:00
|
|
|
|
const taskSetupRef = ref<InstanceType<typeof TaskSetupStep>>()
|
2026-07-13 17:12:49 +08:00
|
|
|
|
const modelSelectionRef = ref<InstanceType<typeof ModelSelectionStep>>()
|
2026-07-11 14:49:10 +08:00
|
|
|
|
const confirmDialogRef = ref<InstanceType<typeof AppConfirmDialog>>()
|
2026-07-24 15:06:01 +08:00
|
|
|
|
const PREVIEW_MODEL_VERSION = 'backend-pipeline-v4'
|
2026-07-10 16:45:55 +08:00
|
|
|
|
const WIZARD_STEPS = [
|
2026-07-12 15:39:43 +08:00
|
|
|
|
{ id: 'create', title: '创建任务', desc: '填写任务信息与处理配置' },
|
2026-07-13 17:12:49 +08:00
|
|
|
|
{ id: 'model', title: '大模型选择', desc: '选择生成模型并设置输出要求' },
|
2026-07-12 15:39:43 +08:00
|
|
|
|
{ id: 'upload', title: '上传文件', desc: '上传或接入待处理的源数据' },
|
2026-07-10 16:45:55 +08:00
|
|
|
|
{ id: 'preview', title: '数据预览', desc: '核对源文件与预览内容' },
|
|
|
|
|
|
{ id: 'generate', title: '开始生成', desc: '确认摘要并启动处理' },
|
2026-07-16 09:36:39 +08:00
|
|
|
|
{ id: 'results', title: '结果编辑与保存', desc: '检查、修改并保存结果' },
|
2026-07-10 16:45:55 +08:00
|
|
|
|
] as const satisfies ReadonlyArray<{ id: StepId; title: string; desc: string }>
|
|
|
|
|
|
const currentStep = ref(0)
|
|
|
|
|
|
const currentStepId = computed<StepId>(() => WIZARD_STEPS[currentStep.value]?.id ?? 'create')
|
|
|
|
|
|
const task = reactive({ name: '', description: '' })
|
2026-07-23 15:10:13 +08:00
|
|
|
|
const taskId = ref<string | null>(null)
|
2026-07-13 16:41:42 +08:00
|
|
|
|
const processType = ref<ProcessType>('structured')
|
2026-07-13 15:28:48 +08:00
|
|
|
|
const structuredOptions = ref<StructuredProcessOptions>(createDefaultStructuredOptions())
|
|
|
|
|
|
const unstructuredOptions = ref<UnstructuredProcessOptions>(createDefaultUnstructuredOptions())
|
2026-07-13 17:12:49 +08:00
|
|
|
|
const modelSelectionOptions = computed<GenerationControlOptions>(() => (
|
|
|
|
|
|
processType.value === 'unstructured' ? unstructuredOptions.value : structuredOptions.value
|
|
|
|
|
|
))
|
2026-07-10 16:45:55 +08:00
|
|
|
|
const uploadedFiles = ref<UploadedDataFile[]>([])
|
2026-07-24 11:28:02 +08:00
|
|
|
|
const previewBuilding = ref(false)
|
2026-07-28 10:56:05 +08:00
|
|
|
|
const {
|
|
|
|
|
|
resumePreviewBuild,
|
|
|
|
|
|
startPreviewBuild,
|
|
|
|
|
|
stopPreviewPolling,
|
|
|
|
|
|
} = useDataProcessPreviewBuild()
|
2026-07-11 11:00:15 +08:00
|
|
|
|
const externalSource = reactive<ExternalDataSource>({
|
2026-07-23 15:10:13 +08:00
|
|
|
|
type: 'postgresql',
|
2026-07-11 11:00:15 +08:00
|
|
|
|
url: '',
|
|
|
|
|
|
authMode: 'none',
|
|
|
|
|
|
username: '',
|
|
|
|
|
|
password: '',
|
|
|
|
|
|
limit: 1000,
|
2026-07-23 15:10:13 +08:00
|
|
|
|
query: '',
|
|
|
|
|
|
fileName: 'external-data.jsonl',
|
2026-07-11 11:00:15 +08:00
|
|
|
|
})
|
|
|
|
|
|
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 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 dirty = ref(false)
|
2026-07-25 22:41:06 +08:00
|
|
|
|
const modelSubmitLoading = ref(false)
|
2026-07-10 16:45:55 +08:00
|
|
|
|
let allowLeave = false
|
2026-07-13 15:28:48 +08:00
|
|
|
|
const {
|
2026-07-28 10:56:05 +08:00
|
|
|
|
bulkRegeneration,
|
2026-07-30 16:53:54 +08:00
|
|
|
|
canReturnFromGeneration,
|
2026-07-13 15:28:48 +08:00
|
|
|
|
generation,
|
2026-07-30 16:53:54 +08:00
|
|
|
|
generationStarting,
|
2026-07-28 10:56:05 +08:00
|
|
|
|
regeneratingResultId,
|
|
|
|
|
|
resultRegenerationBusy,
|
2026-07-13 15:28:48 +08:00
|
|
|
|
results,
|
|
|
|
|
|
selectedResultId,
|
2026-07-23 15:10:13 +08:00
|
|
|
|
persistResultChanges,
|
2026-07-13 15:28:48 +08:00
|
|
|
|
resetDownstream,
|
2026-07-28 10:56:05 +08:00
|
|
|
|
regenerateAllResults,
|
|
|
|
|
|
regenerateResult,
|
|
|
|
|
|
resumeGeneration,
|
2026-07-13 15:28:48 +08:00
|
|
|
|
startGeneration,
|
|
|
|
|
|
stopGenerationTimer,
|
|
|
|
|
|
updateResultField,
|
|
|
|
|
|
validateResults,
|
|
|
|
|
|
} = useDataProcessGeneration({
|
2026-07-23 15:10:13 +08:00
|
|
|
|
taskId,
|
2026-07-13 15:28:48 +08:00
|
|
|
|
dirty,
|
2026-07-27 10:43:48 +08:00
|
|
|
|
beforeGenerate: beforeStartGeneration,
|
2026-07-10 16:45:55 +08:00
|
|
|
|
})
|
2026-07-24 11:28:02 +08:00
|
|
|
|
const { enqueueSourceUpload, sourceUploading } = useDataProcessSourceUpload({
|
|
|
|
|
|
taskId,
|
|
|
|
|
|
uploadedFiles,
|
|
|
|
|
|
onUploaded(file) {
|
|
|
|
|
|
previewSignature.value = ''
|
|
|
|
|
|
resetDownstream()
|
|
|
|
|
|
dirty.value = true
|
|
|
|
|
|
ElMessage.success(`文件 ${file.name} 上传成功,等待切分`)
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
const hasUnfinishedUploads = computed(() => uploadedFiles.value.some((file) => (
|
|
|
|
|
|
file.status !== 'ready' || !file.sourceFileId
|
|
|
|
|
|
)))
|
2026-07-10 16:45:55 +08:00
|
|
|
|
const modifiedPreviewCount = computed(() => previewItems.value.filter((item) => item.status !== 'original').length)
|
2026-07-13 15:28:48 +08:00
|
|
|
|
const previewItemsByFile = computed(() => {
|
|
|
|
|
|
const grouped = new Map<string, PreviewItem[]>()
|
|
|
|
|
|
for (const item of previewItems.value) {
|
|
|
|
|
|
const items = grouped.get(item.sourceFileId) || []
|
|
|
|
|
|
items.push(item)
|
|
|
|
|
|
grouped.set(item.sourceFileId, items)
|
|
|
|
|
|
}
|
|
|
|
|
|
return grouped
|
|
|
|
|
|
})
|
2026-07-10 16:45:55 +08:00
|
|
|
|
const activePreviewFile = computed(() => uploadedFiles.value.find((file) => String(file.uid) === selectedPreviewFileId.value))
|
2026-07-13 15:28:48 +08:00
|
|
|
|
const activePreviewItems = computed(() => previewItemsByFile.value.get(selectedPreviewFileId.value || '') || [])
|
2026-07-10 16:45:55 +08:00
|
|
|
|
const activeSourceText = computed(() => activePreviewFile.value?.content ?? '')
|
|
|
|
|
|
const previewFiles = computed(() => uploadedFiles.value.map((file) => {
|
2026-07-13 15:28:48 +08:00
|
|
|
|
const items = previewItemsByFile.value.get(String(file.uid)) || []
|
2026-07-10 16:45:55 +08:00
|
|
|
|
return {
|
|
|
|
|
|
id: String(file.uid),
|
|
|
|
|
|
name: file.name,
|
|
|
|
|
|
count: items.length,
|
|
|
|
|
|
modifiedCount: items.filter((item) => item.status !== 'original').length,
|
|
|
|
|
|
}
|
|
|
|
|
|
}))
|
|
|
|
|
|
const primaryActionLabel = computed(() => {
|
2026-07-13 17:12:49 +08:00
|
|
|
|
if (currentStepId.value === 'create') return '继续:选择大模型'
|
|
|
|
|
|
if (currentStepId.value === 'model') return '继续:上传文件'
|
2026-07-24 11:28:02 +08:00
|
|
|
|
if (currentStepId.value === 'upload') {
|
|
|
|
|
|
if (sourceUploading.value) return '正在上传'
|
|
|
|
|
|
return previewBuilding.value ? '正在切分' : '继续:数据预览'
|
|
|
|
|
|
}
|
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 goToStep(stepId: StepId) {
|
|
|
|
|
|
const nextStepIndex = WIZARD_STEPS.findIndex((step) => step.id === stepId)
|
|
|
|
|
|
if (nextStepIndex >= 0) currentStep.value = nextStepIndex
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-13 17:12:49 +08:00
|
|
|
|
function updateModelSelectionOptions(value: GenerationControlOptions) {
|
|
|
|
|
|
if (processType.value === 'unstructured') {
|
|
|
|
|
|
unstructuredOptions.value = { ...unstructuredOptions.value, ...value }
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
structuredOptions.value = { ...structuredOptions.value, ...value }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-23 15:10:13 +08:00
|
|
|
|
function toBackendConfig(): DataProcessConfig {
|
|
|
|
|
|
const options = processType.value === 'unstructured'
|
|
|
|
|
|
? unstructuredOptions.value
|
|
|
|
|
|
: structuredOptions.value
|
|
|
|
|
|
const common = {
|
|
|
|
|
|
preprocess_options: [...options.preprocessOptions],
|
|
|
|
|
|
semantic_enrichment: options.semanticEnrichment,
|
|
|
|
|
|
dataset_split: { ...options.datasetSplit },
|
|
|
|
|
|
generation_model_id: options.generationModelId,
|
|
|
|
|
|
generation_prompt: options.generationPrompt,
|
2026-07-27 13:08:44 +08:00
|
|
|
|
output_type: options.outputType,
|
2026-07-27 14:41:38 +08:00
|
|
|
|
reasoning_detail: options.reasoningDetail,
|
2026-07-23 15:10:13 +08:00
|
|
|
|
temperature: options.temperature,
|
|
|
|
|
|
max_tokens: options.maxTokens,
|
|
|
|
|
|
json_mode: options.jsonMode,
|
|
|
|
|
|
quality_filter_enabled: options.qualityFilterEnabled,
|
|
|
|
|
|
filter_low_quality: options.filterLowQuality,
|
|
|
|
|
|
filter_short_content: options.filterShortContent,
|
|
|
|
|
|
min_output_length: options.minOutputLength,
|
|
|
|
|
|
}
|
|
|
|
|
|
if (processType.value === 'unstructured') {
|
|
|
|
|
|
return {
|
|
|
|
|
|
...common,
|
|
|
|
|
|
chunk_method: unstructuredOptions.value.chunkMethod,
|
|
|
|
|
|
chunk_size: unstructuredOptions.value.chunkSize,
|
|
|
|
|
|
chunk_overlap: unstructuredOptions.value.chunkOverlap,
|
|
|
|
|
|
min_chunk_size: unstructuredOptions.value.minChunkSize,
|
2026-07-25 18:00:44 +08:00
|
|
|
|
semantic_breakpoint_percentile: unstructuredOptions.value.semanticBreakpointPercentile,
|
2026-07-23 15:10:13 +08:00
|
|
|
|
preserve_tables: unstructuredOptions.value.preserveTables,
|
|
|
|
|
|
preserve_code_blocks: unstructuredOptions.value.preserveCodeBlocks,
|
|
|
|
|
|
preserve_lists: unstructuredOptions.value.preserveLists,
|
|
|
|
|
|
qa_pairs_per_chunk: unstructuredOptions.value.qaPairsPerChunk,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
...common,
|
|
|
|
|
|
qa_pairs_per_row: structuredOptions.value.qaPairsPerRow,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function taskPayload() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
name: task.name.trim(),
|
|
|
|
|
|
description: task.description.trim(),
|
2026-07-25 22:41:06 +08:00
|
|
|
|
process_type: originalProcessType.value || processType.value,
|
2026-07-23 15:10:13 +08:00
|
|
|
|
config: toBackendConfig(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-28 10:56:05 +08:00
|
|
|
|
async function persistWorkflowStep(step: StepId) {
|
|
|
|
|
|
if (!taskId.value) return
|
|
|
|
|
|
await updateDataProcessWorkflowStep(taskId.value, step as DataProcessWorkflowStep)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function saveTaskConfiguration() {
|
|
|
|
|
|
if (isRegeneration.value) {
|
|
|
|
|
|
const regenerated = await prepareRegeneration(taskPayload())
|
|
|
|
|
|
taskId.value = String(regenerated.task.id)
|
|
|
|
|
|
dirty.value = false
|
|
|
|
|
|
return regenerated.task
|
|
|
|
|
|
}
|
|
|
|
|
|
const saved = taskId.value
|
|
|
|
|
|
? await updateDataProcessTask(taskId.value, taskPayload())
|
|
|
|
|
|
: await createDataProcessTask(taskPayload())
|
|
|
|
|
|
taskId.value = String(saved.id)
|
|
|
|
|
|
dirty.value = false
|
|
|
|
|
|
return saved
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-23 15:10:13 +08:00
|
|
|
|
function externalPayload(): DataProcessExternalSourcePayload {
|
|
|
|
|
|
return {
|
2026-07-24 10:27:52 +08:00
|
|
|
|
type: 'postgresql',
|
2026-07-23 15:10:13 +08:00
|
|
|
|
url: externalSource.url.trim(),
|
|
|
|
|
|
auth_mode: externalSource.authMode,
|
|
|
|
|
|
username: externalSource.username || undefined,
|
|
|
|
|
|
password: externalSource.password || undefined,
|
|
|
|
|
|
limit: externalSource.limit,
|
|
|
|
|
|
query: externalSource.query?.trim() || undefined,
|
|
|
|
|
|
file_name: externalSource.fileName || 'external-data.jsonl',
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function mapPreviewItem(item: DataProcessPreviewItem): PreviewItem {
|
2026-07-30 16:53:54 +08:00
|
|
|
|
const sourceLocator = item.quality_score?.source_locator
|
2026-07-23 15:10:13 +08:00
|
|
|
|
return {
|
|
|
|
|
|
id: String(item.id),
|
|
|
|
|
|
sourceFileId: String(item.source_file_id),
|
|
|
|
|
|
originalContent: item.original_content,
|
|
|
|
|
|
editedContent: item.edited_content,
|
2026-07-28 10:56:05 +08:00
|
|
|
|
savedEditedContent: item.edited_content,
|
2026-07-30 16:53:54 +08:00
|
|
|
|
sourceStart: item.source_start ?? sourceLocator?.source_start ?? null,
|
|
|
|
|
|
sourceEnd: item.source_end ?? sourceLocator?.source_end ?? null,
|
|
|
|
|
|
sourceStartLine: item.source_start_line ?? sourceLocator?.start_line ?? null,
|
|
|
|
|
|
sourceEndLine: item.source_end_line ?? sourceLocator?.end_line ?? null,
|
2026-07-23 15:10:13 +08:00
|
|
|
|
tokenCount: item.token_count,
|
|
|
|
|
|
status: item.status,
|
2026-07-25 18:00:44 +08:00
|
|
|
|
sourcePages: Array.isArray(item.quality_score?.source_pages)
|
|
|
|
|
|
? item.quality_score.source_pages.filter((value): value is number => typeof value === 'number')
|
|
|
|
|
|
: [],
|
2026-07-30 16:53:54 +08:00
|
|
|
|
sourceLocator,
|
|
|
|
|
|
headingPath: Array.isArray(item.quality_score?.heading_path)
|
|
|
|
|
|
? item.quality_score.heading_path.filter((value): value is string => typeof value === 'string')
|
|
|
|
|
|
: [],
|
2026-07-23 15:10:13 +08:00
|
|
|
|
updatedAt: item.updated_at,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-28 10:56:05 +08:00
|
|
|
|
async function loadAllPreviewItems() {
|
|
|
|
|
|
if (!taskId.value) return []
|
|
|
|
|
|
const first = await getDataProcessPreview(taskId.value, { page: 1, page_size: 500 })
|
|
|
|
|
|
const items = [...first.items]
|
|
|
|
|
|
const pages = Math.ceil(first.total / first.page_size)
|
|
|
|
|
|
for (let page = 2; page <= pages; page += 1) {
|
|
|
|
|
|
const next = await getDataProcessPreview(taskId.value, { page, page_size: 500 })
|
|
|
|
|
|
items.push(...next.items)
|
|
|
|
|
|
}
|
|
|
|
|
|
return items.map(mapPreviewItem)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-11 14:49:10 +08:00
|
|
|
|
function previewAffectingOptions() {
|
2026-07-25 22:41:06 +08:00
|
|
|
|
return previewAffectingOptionsFor(processType.value, structuredOptions.value, unstructuredOptions.value)
|
2026-07-11 14:49:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
function generationAffectingOptions() {
|
2026-07-25 22:41:06 +08:00
|
|
|
|
return generationAffectingOptionsFor(processType.value, structuredOptions.value, unstructuredOptions.value)
|
2026-07-11 14:49:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
const generationOptionsSignature = computed(() => JSON.stringify(generationAffectingOptions()))
|
2026-07-24 11:28:02 +08:00
|
|
|
|
function buildPreviewConfigSignature() {
|
|
|
|
|
|
return `${PREVIEW_MODEL_VERSION}:${processType.value}:${JSON.stringify(previewAffectingOptions())}`
|
|
|
|
|
|
}
|
2026-07-11 14:49:10 +08:00
|
|
|
|
function buildPreviewSignature() {
|
|
|
|
|
|
const filesSignature = uploadedFiles.value
|
2026-07-23 15:10:13 +08:00
|
|
|
|
.map((file) => `${file.uid}:${file.name}:${file.size}:${file.checksumSha256 || file.count}`)
|
2026-07-11 14:49:10 +08:00
|
|
|
|
.join('|')
|
2026-07-24 11:28:02 +08:00
|
|
|
|
return `${buildPreviewConfigSignature()}:${filesSignature}`
|
2026-07-11 14:49:10 +08:00
|
|
|
|
}
|
2026-07-25 22:41:06 +08:00
|
|
|
|
const {
|
|
|
|
|
|
isRegeneration,
|
2026-07-28 10:56:05 +08:00
|
|
|
|
isWorkflowResume,
|
2026-07-25 22:41:06 +08:00
|
|
|
|
originalProcessType,
|
|
|
|
|
|
regenerationPrepared,
|
|
|
|
|
|
hydrating,
|
|
|
|
|
|
initializationError,
|
|
|
|
|
|
loadSource: loadRegenerationSource,
|
|
|
|
|
|
confirmPreviewConfigChange,
|
|
|
|
|
|
prepareRegeneration,
|
2026-07-27 10:43:48 +08:00
|
|
|
|
confirmStartGeneration,
|
2026-07-25 22:41:06 +08:00
|
|
|
|
} = useDataProcessRegeneration({
|
|
|
|
|
|
task,
|
|
|
|
|
|
processType,
|
|
|
|
|
|
structuredOptions,
|
|
|
|
|
|
unstructuredOptions,
|
|
|
|
|
|
uploadedFiles,
|
|
|
|
|
|
previewItems,
|
|
|
|
|
|
selectedPreviewFileId,
|
|
|
|
|
|
selectedPreviewId,
|
|
|
|
|
|
selectedPreviewIdsByFile,
|
|
|
|
|
|
previewSignature,
|
|
|
|
|
|
dirty,
|
|
|
|
|
|
buildPreviewConfigSignature,
|
|
|
|
|
|
buildPreviewSignature,
|
|
|
|
|
|
mapPreviewItem,
|
|
|
|
|
|
resetDownstream,
|
|
|
|
|
|
})
|
2026-07-27 10:43:48 +08:00
|
|
|
|
async function beforeStartGeneration() {
|
|
|
|
|
|
return confirmStartGeneration(
|
|
|
|
|
|
options => confirmDialogRef.value?.open(options) ?? Promise.resolve(false),
|
|
|
|
|
|
syncPreviewChanges,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
2026-07-10 16:45:55 +08:00
|
|
|
|
watch(
|
2026-07-11 14:49:10 +08:00
|
|
|
|
[() => task.name, () => task.description, processType, structuredOptions, unstructuredOptions, externalSource],
|
2026-07-25 22:41:06 +08:00
|
|
|
|
() => {
|
|
|
|
|
|
if (!hydrating.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) => {
|
2026-07-25 22:41:06 +08:00
|
|
|
|
if (hydrating.value) return
|
|
|
|
|
|
if (isRegeneration.value && originalProcessType.value && nextType !== originalProcessType.value) {
|
|
|
|
|
|
processType.value = originalProcessType.value
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-07-24 11:28:02 +08:00
|
|
|
|
if (nextType === previousType || uploadedFiles.value.length === 0) return
|
2026-07-12 15:39:43 +08:00
|
|
|
|
resetSourceDataForProcessTypeChange()
|
|
|
|
|
|
ElMessage.info('处理类型已变更,请重新上传或拉取匹配的源数据')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-07-11 14:49:10 +08:00
|
|
|
|
watch(generationOptionsSignature, (currentSignature, previousSignature) => {
|
2026-07-25 22:41:06 +08:00
|
|
|
|
if (hydrating.value) return
|
2026-07-24 11:28:02 +08:00
|
|
|
|
if (currentSignature === previousSignature) return
|
2026-07-11 14:49:10 +08:00
|
|
|
|
resetDownstream()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-07-10 16:45:55 +08:00
|
|
|
|
function scrollToStepTop() {
|
|
|
|
|
|
document.querySelector<HTMLElement>('.layout-content')?.scrollTo({ top: 0, behavior: 'smooth' })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
watch(currentStep, () => nextTick(scrollToStepTop))
|
|
|
|
|
|
|
2026-07-24 11:28:02 +08:00
|
|
|
|
function handleFileChange(uploadFile: UploadFile) {
|
2026-07-10 16:45:55 +08:00
|
|
|
|
const raw = uploadFile.raw
|
|
|
|
|
|
if (!raw) return
|
2026-07-23 15:10:13 +08:00
|
|
|
|
if (!taskId.value) {
|
|
|
|
|
|
ElMessage.error('任务尚未创建,请返回模型选择步骤后重试')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-07-24 11:28:02 +08:00
|
|
|
|
const validation = validateSourceFileSelection(raw, processType.value, uploadedFiles.value)
|
|
|
|
|
|
if (!validation.valid) {
|
|
|
|
|
|
ElMessage[validation.severity](validation.message)
|
2026-07-23 15:10:13 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-24 11:28:02 +08:00
|
|
|
|
// 必须先同步插入文件行,再交给队列;这样选择完成后页面会立即展示全部文件。
|
|
|
|
|
|
const localUid = `local-${uploadFile.uid}-${Date.now()}-${uploadedFiles.value.length}`
|
|
|
|
|
|
uploadedFiles.value.push({
|
|
|
|
|
|
uid: localUid,
|
|
|
|
|
|
name: raw.name,
|
|
|
|
|
|
size: raw.size,
|
|
|
|
|
|
count: 0,
|
|
|
|
|
|
content: '',
|
|
|
|
|
|
fileFormat: validation.extension,
|
|
|
|
|
|
status: 'queued',
|
|
|
|
|
|
uploadProgress: 0,
|
|
|
|
|
|
previewStatus: 'waiting',
|
|
|
|
|
|
previewProgress: 0,
|
|
|
|
|
|
})
|
|
|
|
|
|
dirty.value = true
|
2026-07-30 16:53:54 +08:00
|
|
|
|
enqueueSourceUpload({ uid: localUid, file: raw })
|
2026-07-23 15:10:13 +08:00
|
|
|
|
}
|
2026-07-10 16:45:55 +08:00
|
|
|
|
|
2026-07-23 15:10:13 +08:00
|
|
|
|
async function useSampleFile() {
|
|
|
|
|
|
if (!taskId.value) {
|
|
|
|
|
|
ElMessage.error('任务尚未创建,请返回模型选择步骤后重试')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const sample = new File([DEFAULT_SOURCE_TEXT], 'finance_qa.jsonl', { type: 'application/x-ndjson' })
|
2026-07-24 11:28:02 +08:00
|
|
|
|
handleFileChange({ raw: sample, uid: Date.now(), name: sample.name } as UploadFile)
|
2026-07-10 16:45:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-11 11:00:15 +08:00
|
|
|
|
function updateExternalSource(value: ExternalDataSource) {
|
|
|
|
|
|
Object.assign(externalSource, value)
|
|
|
|
|
|
externalConnected.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-23 15:10:13 +08:00
|
|
|
|
async function handleTestConnection() {
|
|
|
|
|
|
if (!taskId.value) {
|
|
|
|
|
|
ElMessage.error('任务尚未创建,请返回模型选择步骤后重试')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-07-11 11:00:15 +08:00
|
|
|
|
if (!externalSource.url.trim()) {
|
|
|
|
|
|
ElMessage.warning('请先填写数据源地址')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
externalPulling.value = true
|
2026-07-23 15:10:13 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const result = await testDataProcessExternalSource(taskId.value, externalPayload())
|
|
|
|
|
|
externalConnected.value = result.connected
|
|
|
|
|
|
if (result.connected) ElMessage.success(result.message || '数据源连接测试成功')
|
|
|
|
|
|
else ElMessage.warning(result.message || '数据源连接失败')
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
externalConnected.value = false
|
|
|
|
|
|
} finally {
|
2026-07-11 11:00:15 +08:00
|
|
|
|
externalPulling.value = false
|
2026-07-23 15:10:13 +08:00
|
|
|
|
}
|
2026-07-11 11:00:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-23 15:10:13 +08:00
|
|
|
|
async function handlePullData() {
|
|
|
|
|
|
if (!taskId.value) {
|
|
|
|
|
|
ElMessage.error('任务尚未创建,请返回模型选择步骤后重试')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-07-11 11:00:15 +08:00
|
|
|
|
if (!externalSource.url.trim()) {
|
|
|
|
|
|
ElMessage.warning('请先填写数据源地址')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-07-23 15:10:13 +08:00
|
|
|
|
if (!externalSource.query?.trim()) {
|
|
|
|
|
|
ElMessage.warning('请先填写只读 SELECT 查询语句')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-07-11 11:00:15 +08:00
|
|
|
|
externalPulling.value = true
|
2026-07-23 15:10:13 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const response = await pullDataProcessExternalSource(taskId.value, externalPayload())
|
|
|
|
|
|
const newFiles: UploadedDataFile[] = []
|
|
|
|
|
|
for (const file of response.files) {
|
2026-07-30 16:53:54 +08:00
|
|
|
|
const content = await loadCanonicalSourceContent(taskId.value, file.id)
|
|
|
|
|
|
newFiles.push(mapDataProcessSourceFile(file, content))
|
2026-07-23 15:10:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
uploadedFiles.value.push(...newFiles)
|
2026-07-11 11:00:15 +08:00
|
|
|
|
externalConnected.value = true
|
2026-07-23 15:10:13 +08:00
|
|
|
|
previewSignature.value = ''
|
|
|
|
|
|
resetDownstream()
|
2026-07-11 11:00:15 +08:00
|
|
|
|
dirty.value = true
|
2026-07-23 15:10:13 +08:00
|
|
|
|
ElMessage.success(`已成功登记 ${newFiles.length} 个外部源文件`)
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
externalConnected.value = false
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
externalPulling.value = false
|
|
|
|
|
|
}
|
2026-07-11 11:00:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-23 15:10:13 +08:00
|
|
|
|
async function handleRemoveFile(uid: string | number) {
|
2026-07-10 16:45:55 +08:00
|
|
|
|
const index = uploadedFiles.value.findIndex(f => f.uid === uid)
|
2026-07-23 15:10:13 +08:00
|
|
|
|
if (index < 0 || !taskId.value) return
|
2026-07-24 11:28:02 +08:00
|
|
|
|
const file = uploadedFiles.value[index]
|
|
|
|
|
|
if (!file) return
|
|
|
|
|
|
if (file.status === 'uploading') {
|
|
|
|
|
|
ElMessage.warning('当前文件正在上传,请等待上传结束后再删除')
|
2026-07-23 15:10:13 +08:00
|
|
|
|
return
|
2026-07-10 16:45:55 +08:00
|
|
|
|
}
|
2026-07-24 11:28:02 +08:00
|
|
|
|
if (file.sourceFileId) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await deleteDataProcessSourceFile(taskId.value, file.sourceFileId)
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-23 15:10:13 +08:00
|
|
|
|
uploadedFiles.value.splice(index, 1)
|
|
|
|
|
|
previewSignature.value = ''
|
|
|
|
|
|
previewItems.value = []
|
|
|
|
|
|
selectedPreviewId.value = null
|
|
|
|
|
|
resetDownstream()
|
|
|
|
|
|
dirty.value = true
|
2026-07-10 16:45:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
async function nextFromCreate() {
|
|
|
|
|
|
const valid = await taskSetupRef.value?.validate()
|
|
|
|
|
|
if (!valid) return
|
2026-07-25 22:41:06 +08:00
|
|
|
|
const confirmed = await confirmPreviewConfigChange((options) => (
|
|
|
|
|
|
confirmDialogRef.value?.open(options) ?? Promise.resolve(false)
|
|
|
|
|
|
))
|
|
|
|
|
|
if (!confirmed) return
|
2026-07-28 10:56:05 +08:00
|
|
|
|
if (!isRegeneration.value) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await saveTaskConfiguration()
|
|
|
|
|
|
await persistWorkflowStep('model')
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-13 17:12:49 +08:00
|
|
|
|
goToStep('model')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function nextFromModel() {
|
2026-07-25 22:41:06 +08:00
|
|
|
|
if (modelSubmitLoading.value) return
|
|
|
|
|
|
modelSubmitLoading.value = true
|
2026-07-23 15:10:13 +08:00
|
|
|
|
try {
|
2026-07-25 22:41:06 +08:00
|
|
|
|
await modelsStore.load(true)
|
|
|
|
|
|
if (!modelsLoaded.value) {
|
|
|
|
|
|
ElMessage.error('模型列表加载失败,未提交任何修改,请稍后重试')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const modelId = modelSelectionOptions.value.generationModelId
|
|
|
|
|
|
const modelExists = generationModels.value.some(model => String(model.id) === String(modelId))
|
|
|
|
|
|
if (modelId !== '' && !modelExists) {
|
|
|
|
|
|
ElMessage.error(isRegeneration.value ? '原任务使用的模型已删除,请重新选择模型' : '所选模型已删除,请重新选择')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const valid = await modelSelectionRef.value?.validate()
|
|
|
|
|
|
if (!valid) return
|
|
|
|
|
|
if (isRegeneration.value) {
|
|
|
|
|
|
const regenerated = await prepareRegeneration(taskPayload())
|
|
|
|
|
|
taskId.value = String(regenerated.task.id)
|
2026-07-28 10:56:05 +08:00
|
|
|
|
dirty.value = false
|
2026-07-25 22:41:06 +08:00
|
|
|
|
if (regenerated.preview_invalidated) {
|
2026-07-27 10:43:48 +08:00
|
|
|
|
ElMessage.info('切分配置已保存,后续将按新配置重新切分;点击“开始生成”前,原生成结果和已发布数据保持不变')
|
2026-07-25 22:41:06 +08:00
|
|
|
|
} else if (regenerated.published_outputs_preserved) {
|
2026-07-27 10:43:48 +08:00
|
|
|
|
ElMessage.info('重新生成配置已保存;点击“开始生成”前,原生成结果和已发布数据保持不变')
|
2026-07-25 22:41:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
2026-07-28 10:56:05 +08:00
|
|
|
|
await saveTaskConfiguration()
|
2026-07-25 22:41:06 +08:00
|
|
|
|
}
|
2026-07-28 10:56:05 +08:00
|
|
|
|
await persistWorkflowStep('upload')
|
|
|
|
|
|
dirty.value = false
|
2026-07-23 15:10:13 +08:00
|
|
|
|
goToStep('upload')
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// 请求层已展示名称冲突或配置非法等具体原因。
|
2026-07-25 22:41:06 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
modelSubmitLoading.value = false
|
2026-07-23 15:10:13 +08:00
|
|
|
|
}
|
2026-07-12 15:39:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-28 10:56:05 +08:00
|
|
|
|
function applyPreviewProgress(progress: DataProcessPreviewProgress) {
|
|
|
|
|
|
const active = progress.preview_status === 'queued' || progress.preview_status === 'running'
|
|
|
|
|
|
previewBuilding.value = active
|
|
|
|
|
|
for (const file of uploadedFiles.value) {
|
|
|
|
|
|
if (!file.sourceFileId || file.previewStatus === 'success') continue
|
|
|
|
|
|
if (active) {
|
|
|
|
|
|
file.previewStatus = 'processing'
|
|
|
|
|
|
file.previewProgress = Math.max(0, Math.min(99, Number(progress.preview_progress) || 0))
|
|
|
|
|
|
file.previewError = undefined
|
|
|
|
|
|
} else if (progress.preview_status === 'failed' || progress.preview_status === 'cancelled') {
|
|
|
|
|
|
file.previewStatus = 'failed'
|
|
|
|
|
|
file.previewProgress = 0
|
|
|
|
|
|
file.previewError = progress.preview_failure_reason || '切分失败,请重试'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function completePreviewWorkspace() {
|
|
|
|
|
|
previewItems.value = await loadAllPreviewItems()
|
|
|
|
|
|
const configSignature = buildPreviewConfigSignature()
|
|
|
|
|
|
const previewCounts = new Map<string, number>()
|
|
|
|
|
|
for (const item of previewItems.value) {
|
|
|
|
|
|
previewCounts.set(item.sourceFileId, (previewCounts.get(item.sourceFileId) || 0) + 1)
|
|
|
|
|
|
}
|
|
|
|
|
|
for (const file of uploadedFiles.value) {
|
|
|
|
|
|
const count = previewCounts.get(String(file.sourceFileId)) || 0
|
|
|
|
|
|
file.previewCount = count
|
|
|
|
|
|
file.previewStatus = count > 0 ? 'success' : 'failed'
|
|
|
|
|
|
file.previewProgress = count > 0 ? 100 : 0
|
|
|
|
|
|
file.previewError = count > 0 ? undefined : '该文件没有生成可用的预览条目'
|
|
|
|
|
|
file.previewConfigSignature = count > 0 ? configSignature : undefined
|
|
|
|
|
|
}
|
|
|
|
|
|
const failedCount = uploadedFiles.value.filter((file) => file.previewStatus === 'failed').length
|
|
|
|
|
|
if (failedCount || !previewItems.value.length) {
|
|
|
|
|
|
ElMessage.warning(`${failedCount || 1} 个文件切分失败;修正问题后可重新切分`)
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
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 = buildPreviewSignature()
|
|
|
|
|
|
resetDownstream()
|
|
|
|
|
|
await persistWorkflowStep('preview')
|
|
|
|
|
|
dirty.value = false
|
|
|
|
|
|
goToStep('preview')
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function monitorPreviewBuild(sourceFileIds: string[], resume = false) {
|
|
|
|
|
|
if (!taskId.value) return
|
|
|
|
|
|
previewBuilding.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const finalProgress = resume
|
|
|
|
|
|
? await resumePreviewBuild(taskId.value, applyPreviewProgress)
|
|
|
|
|
|
: await startPreviewBuild(taskId.value, sourceFileIds, applyPreviewProgress)
|
|
|
|
|
|
applyPreviewProgress(finalProgress)
|
|
|
|
|
|
if (finalProgress.preview_status === 'completed') {
|
|
|
|
|
|
await completePreviewWorkspace()
|
|
|
|
|
|
} else if (finalProgress.preview_status === 'failed') {
|
|
|
|
|
|
ElMessage.warning(finalProgress.preview_failure_reason || '文件切分失败,请修正后重试')
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// 请求层已经展示错误;后端运行状态可在重新进入任务后继续查询。
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
previewBuilding.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-23 15:10:13 +08:00
|
|
|
|
async function nextFromUpload() {
|
2026-07-24 11:28:02 +08:00
|
|
|
|
if (previewBuilding.value || sourceUploading.value) return
|
2026-07-23 15:10:13 +08:00
|
|
|
|
if (!taskId.value) {
|
|
|
|
|
|
ElMessage.error('任务尚未创建,请返回模型选择步骤后重试')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
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-24 11:28:02 +08:00
|
|
|
|
const failedUploads = uploadedFiles.value.filter((file) => file.status === 'failed')
|
|
|
|
|
|
if (failedUploads.length) {
|
|
|
|
|
|
ElMessage.warning(`${failedUploads.length} 个文件上传失败,请删除后重新选择`)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (hasUnfinishedUploads.value) {
|
|
|
|
|
|
ElMessage.warning('请等待所有文件上传完成后再开始切分')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-07-10 16:45:55 +08:00
|
|
|
|
|
2026-07-11 14:49:10 +08:00
|
|
|
|
const signature = buildPreviewSignature()
|
2026-07-24 11:28:02 +08:00
|
|
|
|
const configSignature = buildPreviewConfigSignature()
|
|
|
|
|
|
const allFilesSucceeded = () => uploadedFiles.value.every((file) => (
|
|
|
|
|
|
file.previewStatus === 'success' && file.previewConfigSignature === configSignature
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
if (signature === previewSignature.value && previewItems.value.length && allFilesSucceeded()) {
|
|
|
|
|
|
goToStep('preview')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for (const file of uploadedFiles.value) {
|
|
|
|
|
|
if (file.previewConfigSignature === configSignature) continue
|
|
|
|
|
|
file.previewStatus = 'waiting'
|
|
|
|
|
|
file.previewProgress = 0
|
|
|
|
|
|
file.previewError = undefined
|
|
|
|
|
|
file.previewCount = undefined
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-28 10:56:05 +08:00
|
|
|
|
const pendingFileIds = uploadedFiles.value
|
|
|
|
|
|
.filter((file) => file.previewStatus !== 'success' || file.previewConfigSignature !== configSignature)
|
|
|
|
|
|
.map((file) => file.sourceFileId)
|
|
|
|
|
|
.filter((fileId): fileId is string => Boolean(fileId))
|
|
|
|
|
|
await persistWorkflowStep('upload')
|
|
|
|
|
|
await monitorPreviewBuild(pendingFileIds)
|
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
|
2026-07-30 16:53:54 +08:00
|
|
|
|
const isManual = isManualPreviewItem(item)
|
2026-07-10 16:45:55 +08:00
|
|
|
|
item.editedContent = value
|
2026-07-23 15:10:13 +08:00
|
|
|
|
item.tokenCount = estimateTokenCount(value)
|
2026-07-30 16:53:54 +08:00
|
|
|
|
item.status = !value.trim()
|
|
|
|
|
|
? 'invalid'
|
|
|
|
|
|
: value === item.originalContent
|
|
|
|
|
|
? 'original'
|
|
|
|
|
|
: isManual ? 'manual' : 'modified'
|
2026-07-11 14:49:10 +08:00
|
|
|
|
resetDownstream()
|
2026-07-10 16:45:55 +08:00
|
|
|
|
dirty.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-23 15:10:13 +08:00
|
|
|
|
async function syncPreviewChanges() {
|
|
|
|
|
|
if (!taskId.value) throw new Error('任务尚未创建')
|
2026-07-28 10:56:05 +08:00
|
|
|
|
const changedItems = previewItems.value.filter((item) => (
|
|
|
|
|
|
item.editedContent !== item.savedEditedContent
|
|
|
|
|
|
))
|
2026-07-23 15:10:13 +08:00
|
|
|
|
for (const item of changedItems) {
|
|
|
|
|
|
const saved = await updateDataProcessPreview(taskId.value, item.id, {
|
|
|
|
|
|
edited_content: item.editedContent,
|
|
|
|
|
|
expected_updated_at: item.updatedAt,
|
|
|
|
|
|
})
|
|
|
|
|
|
const index = previewItems.value.findIndex((entry) => entry.id === item.id)
|
|
|
|
|
|
if (index >= 0) previewItems.value[index] = mapPreviewItem(saved)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-10 16:45:55 +08:00
|
|
|
|
function restorePreviewItem(id: string) {
|
|
|
|
|
|
const item = previewItems.value.find((entry) => entry.id === id)
|
2026-07-30 16:53:54 +08:00
|
|
|
|
if (!item || isManualPreviewItem(item)) return
|
2026-07-10 16:45:55 +08:00
|
|
|
|
item.editedContent = item.originalContent
|
2026-07-23 15:10:13 +08:00
|
|
|
|
item.tokenCount = estimateTokenCount(item.originalContent)
|
2026-07-10 16:45:55 +08:00
|
|
|
|
item.status = 'original'
|
2026-07-11 14:49:10 +08:00
|
|
|
|
resetDownstream()
|
2026-07-10 16:45:55 +08:00
|
|
|
|
dirty.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-23 15:10:13 +08:00
|
|
|
|
async function addPreviewItem() {
|
|
|
|
|
|
if (!selectedPreviewFileId.value || !taskId.value) return
|
|
|
|
|
|
try {
|
|
|
|
|
|
const created = await createDataProcessPreview(taskId.value, {
|
|
|
|
|
|
source_file_id: selectedPreviewFileId.value,
|
|
|
|
|
|
edited_content: '',
|
|
|
|
|
|
})
|
|
|
|
|
|
const item = mapPreviewItem(created)
|
|
|
|
|
|
previewItems.value.push(item)
|
|
|
|
|
|
selectPreviewItem(item.id)
|
|
|
|
|
|
resetDownstream()
|
|
|
|
|
|
dirty.value = true
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// 请求层已展示错误。
|
|
|
|
|
|
}
|
2026-07-10 16:45:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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
|
2026-07-23 15:10:13 +08:00
|
|
|
|
if (!taskId.value) return
|
|
|
|
|
|
try {
|
|
|
|
|
|
await deleteDataProcessPreview(taskId.value, id)
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-07-10 16:45:55 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function handlePrimaryAction() {
|
|
|
|
|
|
if (currentStepId.value === 'create') {
|
|
|
|
|
|
await nextFromCreate()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-07-13 17:12:49 +08:00
|
|
|
|
if (currentStepId.value === 'model') {
|
|
|
|
|
|
await nextFromModel()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-07-12 15:39:43 +08:00
|
|
|
|
if (currentStepId.value === 'upload') {
|
2026-07-23 15:10:13 +08:00
|
|
|
|
await nextFromUpload()
|
2026-07-12 15:39:43 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-07-10 16:45:55 +08:00
|
|
|
|
if (currentStepId.value === 'preview') {
|
|
|
|
|
|
if (!previewItems.value.length) {
|
|
|
|
|
|
ElMessage.warning('当前没有可生成的预览内容')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-07-28 10:56:05 +08:00
|
|
|
|
try {
|
|
|
|
|
|
await syncPreviewChanges()
|
|
|
|
|
|
await persistWorkflowStep('generate')
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
dirty.value = false
|
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-28 10:56:05 +08:00
|
|
|
|
try {
|
|
|
|
|
|
await persistWorkflowStep('results')
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
dirty.value = false
|
2026-07-12 15:39:43 +08:00
|
|
|
|
goToStep('results')
|
2026-07-10 16:45:55 +08:00
|
|
|
|
} else if (generation.status !== 'running') {
|
2026-07-28 10:56:05 +08:00
|
|
|
|
await handleStartGeneration()
|
2026-07-10 16:45:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
await saveTask()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-28 10:56:05 +08:00
|
|
|
|
async function handleStartGeneration() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await persistWorkflowStep('generate')
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const started = await startGeneration()
|
|
|
|
|
|
if (!started) return
|
|
|
|
|
|
dirty.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function persistWorkspaceForStep(targetStep: StepId) {
|
|
|
|
|
|
if (isRegeneration.value && !dirty.value) return
|
|
|
|
|
|
if (currentStepId.value === 'create') {
|
|
|
|
|
|
if (!taskId.value && !task.name.trim()) return
|
|
|
|
|
|
const valid = await taskSetupRef.value?.validate()
|
|
|
|
|
|
if (!valid) throw new Error('请先完善任务信息')
|
|
|
|
|
|
await saveTaskConfiguration()
|
|
|
|
|
|
} else if (currentStepId.value === 'model' && dirty.value) {
|
|
|
|
|
|
await saveTaskConfiguration()
|
|
|
|
|
|
} else if (currentStepId.value === 'preview') {
|
|
|
|
|
|
await syncPreviewChanges()
|
|
|
|
|
|
} else if (currentStepId.value === 'results') {
|
|
|
|
|
|
await persistResultChanges()
|
|
|
|
|
|
}
|
|
|
|
|
|
if (taskId.value) await persistWorkflowStep(targetStep)
|
|
|
|
|
|
dirty.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function persistWorkspaceBeforeLeave() {
|
|
|
|
|
|
await persistWorkspaceForStep(currentStepId.value)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function handleBack() {
|
2026-07-24 11:28:02 +08:00
|
|
|
|
if (sourceUploading.value) {
|
|
|
|
|
|
ElMessage.warning('请等待当前文件上传完成')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (previewBuilding.value) {
|
|
|
|
|
|
ElMessage.warning('请等待当前文件切分完成')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-07-30 16:53:54 +08:00
|
|
|
|
if (currentStepId.value === 'generate' && !canReturnFromGeneration.value) return
|
2026-07-28 10:56:05 +08:00
|
|
|
|
if (currentStep.value > 0) {
|
|
|
|
|
|
const targetStep = WIZARD_STEPS[currentStep.value - 1]?.id
|
|
|
|
|
|
if (!targetStep) return
|
|
|
|
|
|
try {
|
|
|
|
|
|
await persistWorkspaceForStep(targetStep)
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
currentStep.value -= 1
|
2026-07-10 16:45:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function saveTask() {
|
2026-07-28 10:56:05 +08:00
|
|
|
|
if (!taskId.value) {
|
|
|
|
|
|
ElMessage.error('任务尚未创建,请返回上一步重试')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-07-10 16:45:55 +08:00
|
|
|
|
if (!validateResults()) {
|
|
|
|
|
|
ElMessage.warning('请先修正校验失败的结果')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-07-23 15:10:13 +08:00
|
|
|
|
try {
|
|
|
|
|
|
await persistResultChanges()
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!validateResults()) {
|
|
|
|
|
|
ElMessage.warning('仍有结果未通过后端质量校验,请继续修正')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-07-28 10:56:05 +08:00
|
|
|
|
try {
|
|
|
|
|
|
await persistWorkflowStep('results')
|
|
|
|
|
|
await confirmDataProcessResults(taskId.value)
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-07-10 16:45:55 +08:00
|
|
|
|
dirty.value = false
|
|
|
|
|
|
allowLeave = true
|
2026-07-28 10:56:05 +08:00
|
|
|
|
ElMessage.success('生成结果已确认')
|
|
|
|
|
|
await router.replace({ name: 'data-process-detail', params: { id: taskId.value } })
|
2026-07-10 16:45:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function handleCancel() {
|
2026-07-27 10:43:48 +08:00
|
|
|
|
await returnToPreviousPage()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function returnToPreviousPage() {
|
2026-07-28 10:56:05 +08:00
|
|
|
|
await router.push({ name: 'data-process' })
|
2026-07-10 16:45:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-28 10:56:05 +08:00
|
|
|
|
onBeforeRouteLeave(async (to) => {
|
|
|
|
|
|
if (allowLeave) return true
|
|
|
|
|
|
if (to.name === 'data-process' && taskId.value) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await persistWorkspaceBeforeLeave()
|
|
|
|
|
|
allowLeave = true
|
|
|
|
|
|
return true
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
ElMessage.error('当前步骤自动保存失败,已停留在本页,请重试')
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
await persistWorkspaceBeforeLeave()
|
|
|
|
|
|
allowLeave = true
|
|
|
|
|
|
return true
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
ElMessage.error('当前步骤自动保存失败,已停留在本页,请重试')
|
2026-07-24 11:28:02 +08:00
|
|
|
|
return false
|
|
|
|
|
|
}
|
2026-07-10 16:45:55 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-07-28 10:56:05 +08:00
|
|
|
|
function inferWorkflowStep(sourceTask: DataProcessTask): StepId {
|
|
|
|
|
|
if (sourceTask.status === 'running' || sourceTask.status === 'failed' || sourceTask.status === 'stopped') {
|
|
|
|
|
|
return 'generate'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (sourceTask.status === 'completed' && sourceTask.results_confirmed === false) return 'generate'
|
|
|
|
|
|
if (Number(sourceTask.preview_count || previewItems.value.length) > 0) return 'preview'
|
|
|
|
|
|
if ((sourceTask.source_files || []).length > 0) return 'upload'
|
|
|
|
|
|
return 'model'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function initializeExistingWorkflow() {
|
|
|
|
|
|
const sourceTask = await loadRegenerationSource()
|
|
|
|
|
|
if (!sourceTask) return
|
|
|
|
|
|
taskId.value = String(sourceTask.id)
|
|
|
|
|
|
if (!isWorkflowResume.value) return
|
|
|
|
|
|
if (sourceTask.status === 'completed' && sourceTask.results_confirmed !== false) {
|
|
|
|
|
|
allowLeave = true
|
|
|
|
|
|
await router.replace({ name: 'data-process-detail', params: { id: sourceTask.id } })
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (sourceTask.preview_status === 'queued' || sourceTask.preview_status === 'running') {
|
|
|
|
|
|
goToStep('upload')
|
|
|
|
|
|
applyPreviewProgress({
|
|
|
|
|
|
task_id: sourceTask.id,
|
|
|
|
|
|
workflow_step: 'upload',
|
|
|
|
|
|
preview_status: sourceTask.preview_status,
|
|
|
|
|
|
preview_progress: Number(sourceTask.preview_progress) || 0,
|
|
|
|
|
|
preview_run_id: sourceTask.preview_run_id,
|
|
|
|
|
|
preview_failure_reason: sourceTask.preview_failure_reason,
|
|
|
|
|
|
preview_total_files: sourceTask.preview_total_files,
|
|
|
|
|
|
preview_completed_files: sourceTask.preview_completed_files,
|
|
|
|
|
|
})
|
|
|
|
|
|
const sourceFileIds = uploadedFiles.value
|
|
|
|
|
|
.map((file) => file.sourceFileId)
|
|
|
|
|
|
.filter((fileId): fileId is string => Boolean(fileId))
|
|
|
|
|
|
void monitorPreviewBuild(sourceFileIds, true)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
let resumeStep = (sourceTask.workflow_step || inferWorkflowStep(sourceTask)) as StepId
|
|
|
|
|
|
if (sourceTask.preview_status === 'failed') resumeStep = 'upload'
|
|
|
|
|
|
if (sourceTask.status === 'running') resumeStep = 'generate'
|
|
|
|
|
|
if (resumeStep === 'preview' && !previewItems.value.length) resumeStep = 'upload'
|
|
|
|
|
|
if (resumeStep === 'results' && sourceTask.status !== 'completed') resumeStep = 'generate'
|
2026-07-30 16:53:54 +08:00
|
|
|
|
if (resumeStep === 'generate' || resumeStep === 'results') {
|
|
|
|
|
|
const resume = resumeGeneration()
|
|
|
|
|
|
goToStep(resumeStep)
|
|
|
|
|
|
await resume
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-07-28 10:56:05 +08:00
|
|
|
|
goToStep(resumeStep)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-13 15:28:48 +08:00
|
|
|
|
onBeforeUnmount(() => {
|
2026-07-28 10:56:05 +08:00
|
|
|
|
stopPreviewPolling()
|
2026-07-13 15:28:48 +08:00
|
|
|
|
stopGenerationTimer()
|
|
|
|
|
|
})
|
2026-07-24 11:28:02 +08:00
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
localStorage.removeItem('yg-data-process-create-draft')
|
|
|
|
|
|
void modelsStore.load(true)
|
2026-07-28 10:56:05 +08:00
|
|
|
|
if (isRegeneration.value || isWorkflowResume.value) void initializeExistingWorkflow()
|
2026-07-13 11:47:33 +08:00
|
|
|
|
})
|
2026-07-10 16:45:55 +08:00
|
|
|
|
</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">
|
2026-07-13 17:12:49 +08:00
|
|
|
|
<template v-for="(step, index) in WIZARD_STEPS" :key="step.id">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="index !== 0"
|
|
|
|
|
|
class="step-connector"
|
|
|
|
|
|
:class="{ 'is-active': currentStep >= index }"
|
|
|
|
|
|
></div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="step-item"
|
|
|
|
|
|
:class="{
|
|
|
|
|
|
'is-active': currentStep === index,
|
|
|
|
|
|
'is-completed': currentStep > index
|
|
|
|
|
|
}"
|
|
|
|
|
|
:aria-current="currentStep === index ? 'step' : undefined"
|
|
|
|
|
|
>
|
|
|
|
|
|
<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>
|
2026-07-10 16:45:55 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-07-13 17:12:49 +08:00
|
|
|
|
</template>
|
2026-07-10 16:45:55 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-07-25 22:41:06 +08:00
|
|
|
|
<div class="wizard-content" v-loading="hydrating">
|
|
|
|
|
|
<div v-if="initializationError" class="initialization-error" role="alert">
|
|
|
|
|
|
<el-alert type="error" :closable="false" :title="initializationError" show-icon />
|
|
|
|
|
|
<el-button type="primary" :loading="hydrating" @click="loadRegenerationSource">重试加载原任务</el-button>
|
|
|
|
|
|
</div>
|
2026-07-10 16:45:55 +08:00
|
|
|
|
<TaskSetupStep
|
2026-07-25 22:41:06 +08:00
|
|
|
|
v-else-if="currentStepId === 'create'"
|
2026-07-10 16:45:55 +08:00
|
|
|
|
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-25 22:41:06 +08:00
|
|
|
|
:process-type-locked="isRegeneration"
|
2026-07-13 17:12:49 +08:00
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<ModelSelectionStep
|
|
|
|
|
|
v-else-if="currentStepId === 'model'"
|
|
|
|
|
|
ref="modelSelectionRef"
|
|
|
|
|
|
:options="modelSelectionOptions"
|
|
|
|
|
|
:models="generationModels"
|
|
|
|
|
|
@update:options="updateModelSelectionOptions"
|
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"
|
2026-07-24 11:28:02 +08:00
|
|
|
|
:preview-building="previewBuilding"
|
|
|
|
|
|
:source-uploading="sourceUploading"
|
2026-07-11 11:00:15 +08:00
|
|
|
|
@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 ?? ''"
|
2026-07-27 16:24:53 +08:00
|
|
|
|
:file-format="activePreviewFile?.fileFormat"
|
2026-07-24 11:28:02 +08:00
|
|
|
|
:task-id="taskId"
|
|
|
|
|
|
:source-file-id="activePreviewFile?.sourceFileId ?? activePreviewFile?.uid ?? null"
|
2026-07-10 16:45:55 +08:00
|
|
|
|
: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"
|
2026-07-28 10:56:05 +08:00
|
|
|
|
@retry="handleStartGeneration"
|
2026-07-10 16:45:55 +08:00
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<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"
|
2026-07-24 16:30:29 +08:00
|
|
|
|
:preview-items="previewItems"
|
2026-07-28 10:56:05 +08:00
|
|
|
|
:regenerating-result-id="regeneratingResultId"
|
|
|
|
|
|
:bulk-regeneration="bulkRegeneration"
|
2026-07-10 16:45:55 +08:00
|
|
|
|
@update:field="updateResultField"
|
2026-07-28 10:56:05 +08:00
|
|
|
|
@regenerate:all="regenerateAllResults"
|
|
|
|
|
|
@regenerate:item="regenerateResult"
|
2026-07-10 16:45:55 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</main>
|
|
|
|
|
|
|
|
|
|
|
|
<footer class="wizard-footer">
|
|
|
|
|
|
<div class="footer-left">
|
2026-07-28 10:56:05 +08:00
|
|
|
|
<el-button
|
|
|
|
|
|
v-if="currentStep > 0"
|
2026-07-30 16:53:54 +08:00
|
|
|
|
:disabled="(currentStepId === 'generate' && !canReturnFromGeneration) || previewBuilding || sourceUploading"
|
2026-07-28 10:56:05 +08:00
|
|
|
|
@click="handleBack"
|
|
|
|
|
|
>
|
2026-07-10 16:45:55 +08:00
|
|
|
|
<i class="fa fa-arrow-left" style="margin-right: 6px;" /> 返回:{{ previousStepLabel }}
|
|
|
|
|
|
</el-button>
|
2026-07-27 10:43:48 +08:00
|
|
|
|
<el-button v-else @click="handleCancel">{{ isRegeneration ? '返回详情' : '取消' }}</el-button>
|
2026-07-10 16:45:55 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="footer-center">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="footer-right">
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
class="wizard-primary-action"
|
|
|
|
|
|
type="primary"
|
2026-07-30 16:53:54 +08:00
|
|
|
|
:loading="modelSubmitLoading || generationStarting || generation.status === 'running' || resultRegenerationBusy || (currentStepId === 'upload' && (sourceUploading || previewBuilding))"
|
|
|
|
|
|
:disabled="hydrating || modelSubmitLoading || generationStarting || Boolean(initializationError) || resultRegenerationBusy || (currentStepId === 'generate' && generation.status === 'running') || previewBuilding || sourceUploading || (currentStepId === 'upload' && hasUnfinishedUploads)"
|
2026-07-10 16:45:55 +08:00
|
|
|
|
@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>
|
|
|
|
|
|
|
2026-07-13 15:28:48 +08:00
|
|
|
|
<style scoped lang="scss" src="./create/data-process-create.scss"></style>
|