feat: add generation trace and partial-ready workflow status

This commit is contained in:
2026-04-18 21:53:55 +08:00
parent 96dfc677e2
commit e99a7fbe14
36 changed files with 2597 additions and 144 deletions

View File

@@ -7,7 +7,12 @@ import BaseButton from '../components/ui/BaseButton.vue'
import BaseCard from '../components/ui/BaseCard.vue'
import EmptyState from '../components/ui/EmptyState.vue'
import LoadingSpinner from '../components/ui/LoadingSpinner.vue'
import { getAssetStatusMeta, getGenerationStatusMeta } from '../utils/storyStatus'
import {
getAssetStatusMeta,
getGenerationStatusMeta,
isReadableGenerationStatus,
needsGenerationAttention,
} from '../utils/storyStatus'
import {
BookOpenIcon,
ChevronRightIcon,
@@ -24,6 +29,7 @@ interface StoryItem {
created_at: string
mode: string
generation_status: string
text_status: string
image_status: string
audio_status: string
last_error: string | null
@@ -35,14 +41,12 @@ const loading = ref(true)
const error = ref('')
const showCreateModal = ref(false)
const completedCount = computed(() =>
stories.value.filter((story) => story.generation_status === 'completed').length,
const readableCount = computed(() =>
stories.value.filter((story) => isReadableGenerationStatus(story.generation_status)).length,
)
const attentionCount = computed(() =>
stories.value.filter((story) =>
['degraded_completed', 'failed'].includes(story.generation_status),
).length,
stories.value.filter((story) => needsGenerationAttention(story.generation_status)).length,
)
async function fetchStories() {
@@ -144,12 +148,12 @@ onMounted(() => {
<div class="text-gray-500 text-sm mt-1">绘本数量</div>
</div>
<div class="text-center px-4 py-2">
<div class="text-3xl font-bold text-gray-800">{{ completedCount }}</div>
<div class="text-gray-500 text-sm mt-1">完整可用</div>
<div class="text-3xl font-bold text-gray-800">{{ readableCount }}</div>
<div class="text-gray-500 text-sm mt-1">可阅读</div>
</div>
<div class="text-center px-4 py-2">
<div class="text-3xl font-bold text-gray-800">{{ attentionCount }}</div>
<div class="text-gray-500 text-sm mt-1">待补资源</div>
<div class="text-gray-500 text-sm mt-1">需关注</div>
</div>
</div>
</BaseCard>

View File

@@ -4,6 +4,7 @@ import { useRoute, useRouter } from 'vue-router'
import { api } from '../api/client'
import BaseButton from '../components/ui/BaseButton.vue'
import ConfirmModal from '../components/ui/ConfirmModal.vue'
import GenerationTrace from '../components/GenerationTrace.vue'
import LoadingSpinner from '../components/ui/LoadingSpinner.vue'
import { getAssetStatusMeta, getGenerationStatusMeta } from '../utils/storyStatus'
import {
@@ -23,6 +24,7 @@ interface Story {
image_url: string | null
mode: string
generation_status: string
text_status: string
image_status: string
audio_status: string
last_error: string | null
@@ -49,6 +51,7 @@ const audioProgress = ref(0)
const audioDuration = ref(0)
const error = ref('')
const showDeleteConfirm = ref(false)
const generationTraceRef = ref<InstanceType<typeof GenerationTrace> | null>(null)
const storyParagraphs = computed(() => story.value?.story_text?.split('\n\n') ?? [])
const generationMeta = computed(() => getGenerationStatusMeta(story.value?.generation_status))
@@ -62,6 +65,10 @@ const assetGuidance = computed(() => {
return '正文已经可读,失败的资源可以单独重试,不会覆盖当前故事。'
}
if (story.value?.generation_status === 'partial_ready') {
return '正文已经可读,仍可继续补全封面或音频。'
}
if (story.value?.generation_status === 'assets_generating') {
return '资源正在处理中,可以稍后刷新查看最新状态。'
}
@@ -103,6 +110,7 @@ async function generateImage() {
story.value = await api.post<Story>(`/api/generations/${story.value.id}/retry-assets`, {
assets: ['image'],
})
await generationTraceRef.value?.refresh()
} catch (e) {
error.value = e instanceof Error ? e.message : '图片生成失败'
await refreshStorySnapshot().catch(() => undefined)
@@ -151,6 +159,7 @@ async function retryAudio() {
audioUrl.value = null
await loadAudio()
}
await generationTraceRef.value?.refresh()
} catch (e) {
error.value = e instanceof Error ? e.message : '音频生成失败'
await refreshStorySnapshot().catch(() => undefined)
@@ -338,6 +347,12 @@ onUnmounted(() => {
<p>{{ assetGuidance }}</p>
</div>
<GenerationTrace
ref="generationTraceRef"
class="mb-10"
:story-id="story.id"
/>
<div class="prose prose-lg max-w-none mb-10">
<p
v-for="(paragraph, index) in storyParagraphs"

View File

@@ -4,6 +4,7 @@ import { useRoute, useRouter } from 'vue-router'
import { api } from '../api/client'
import { useStorybookStore } from '../stores/storybook'
import BaseButton from '../components/ui/BaseButton.vue'
import GenerationTrace from '../components/GenerationTrace.vue'
import LoadingSpinner from '../components/ui/LoadingSpinner.vue'
import { getAssetStatusMeta, getGenerationStatusMeta } from '../utils/storyStatus'
import {
@@ -30,6 +31,7 @@ interface StoryDetailResponse {
image_url: string | null
mode: string
generation_status: string
text_status: string
image_status: string
audio_status: string
last_error: string | null
@@ -45,6 +47,7 @@ const loading = ref(true)
const imageLoading = ref(false)
const error = ref('')
const currentPageIndex = ref(-1)
const generationTraceRef = ref<InstanceType<typeof GenerationTrace> | null>(null)
const totalPages = computed(() => storybook.value?.pages.length || 0)
const isCover = computed(() => currentPageIndex.value === -1)
@@ -83,6 +86,7 @@ const currentStoryId = computed(() => {
const parsed = Number(normalized)
return Number.isFinite(parsed) ? parsed : null
})
const storybookTraceId = computed(() => storybook.value?.id ?? currentStoryId.value)
function goHome() {
store.clearStorybook()
@@ -122,6 +126,7 @@ async function loadStorybook() {
if (cachedStorybook?.id === storyId) {
loading.value = false
await generationTraceRef.value?.refresh()
return
}
@@ -150,6 +155,7 @@ async function loadStorybook() {
last_error: detail.last_error,
retryable_assets: detail.retryable_assets,
})
await generationTraceRef.value?.refresh()
} catch (e) {
error.value = e instanceof Error ? e.message : '绘本加载失败'
} finally {
@@ -186,6 +192,7 @@ async function retryStorybookImages() {
last_error: detail.last_error,
retryable_assets: detail.retryable_assets,
})
await generationTraceRef.value?.refresh()
} catch (e) {
error.value = e instanceof Error ? e.message : '插图补全失败'
await loadStorybook().catch(() => undefined)
@@ -394,6 +401,17 @@ watch(
读完了再来一本
</BaseButton>
</div>
<section class="bg-[#0D0F1A] px-4 pb-10 md:px-8">
<GenerationTrace
ref="generationTraceRef"
:story-id="storybookTraceId"
tone="dark"
title="绘本生成轨迹"
description="绘本正文、封面和每页插图都会写入事件流;失败时可以看到资源重试和 Provider 调用结果。"
class="mx-auto max-w-5xl"
/>
</section>
</div>
</template>