feat: add generation trace and partial-ready workflow status
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user