feat: surface voice session insights in entry views

This commit is contained in:
2026-04-20 16:50:23 +08:00
parent fab2094e34
commit 4d7072fb66
2 changed files with 139 additions and 3 deletions

View File

@@ -8,7 +8,7 @@ import BaseCard from '../components/ui/BaseCard.vue'
import EmptyState from '../components/ui/EmptyState.vue'
import LoadingSpinner from '../components/ui/LoadingSpinner.vue'
import type { GenerationOpsSummary, GenerationProviderAnalytics } from '../types/generation'
import type { VoiceSessionSummary } from '../types/voiceSession'
import type { VoiceSessionAnalytics, VoiceSessionSummary } from '../types/voiceSession'
import {
getAssetStatusMeta,
getGenerationStatusMeta,
@@ -42,6 +42,7 @@ const stories = ref<StoryItem[]>([])
const providerAnalytics = ref<GenerationProviderAnalytics | null>(null)
const opsSummary = ref<GenerationOpsSummary | null>(null)
const activeVoiceSession = ref<VoiceSessionSummary | null>(null)
const voiceAnalytics = ref<VoiceSessionAnalytics | null>(null)
const loading = ref(true)
const error = ref('')
const showCreateModal = ref(false)
@@ -63,6 +64,14 @@ const providerSuccessRate = computed(() => {
})
const topProvider = computed(() => providerAnalytics.value?.by_provider[0] ?? null)
const topFailureReason = computed(() => providerAnalytics.value?.failure_reasons[0] ?? null)
const voiceTurnSuccessRate = computed(() => {
if (!voiceAnalytics.value) return null
return Math.round(voiceAnalytics.value.turn_success_rate * 100)
})
const voiceFinalizeRate = computed(() => {
if (!voiceAnalytics.value) return null
return Math.round(voiceAnalytics.value.finalize_conversion_rate * 100)
})
function buildProviderAnalyticsPath() {
const params = new URLSearchParams()
@@ -78,16 +87,18 @@ function buildProviderAnalyticsPath() {
async function fetchStories() {
try {
const [storyList, analytics, ops, activeSession] = await Promise.all([
const [storyList, analytics, ops, activeSession, voiceOverview] = await Promise.all([
api.get<StoryItem[]>('/api/stories'),
api.get<GenerationProviderAnalytics>(buildProviderAnalyticsPath()),
api.get<GenerationOpsSummary>('/api/generations/ops-summary'),
api.get<VoiceSessionSummary | null>('/api/voice-sessions/active').catch(() => null),
api.get<VoiceSessionAnalytics>('/api/voice-sessions/analytics?days=30').catch(() => null),
])
stories.value = storyList
providerAnalytics.value = analytics
opsSummary.value = ops
activeVoiceSession.value = activeSession
voiceAnalytics.value = voiceOverview
} catch (e) {
error.value = e instanceof Error ? e.message : '加载失败'
} finally {
@@ -221,6 +232,18 @@ watch([selectedWindow, selectedCapability], () => {
{{ activeVoiceSession.working_title || '未命名语音会话' }}
当前状态 {{ activeVoiceSession.status }}已完成 {{ activeVoiceSession.total_turns }}
</p>
<p
v-if="activeVoiceSession.latest_requires_confirmation"
class="mt-2 text-sm text-amber-700"
>
上一轮仍在等待家长确认建议优先回到语音共创工作台处理
</p>
<p
v-else-if="activeVoiceSession.latest_safety_message"
class="mt-2 text-sm text-rose-700"
>
最近一轮触发了儿童内容安全兜底建议回到工作台查看详细记录
</p>
</div>
<BaseButton @click="goToVoiceStudio">
<SparklesIcon class="h-5 w-5 mr-2" />
@@ -229,6 +252,55 @@ watch([selectedWindow, selectedCapability], () => {
</div>
</BaseCard>
<BaseCard
v-if="voiceAnalytics && voiceAnalytics.total_sessions"
class="mb-8 border border-violet-100 bg-violet-50/40"
padding="lg"
>
<div class="flex flex-col gap-5 lg:flex-row lg:items-center lg:justify-between">
<div>
<h2 class="text-xl font-bold text-gray-800">语音共创运营摘要</h2>
<p class="mt-2 text-sm leading-6 text-gray-600">
最近 {{ voiceAnalytics.window_days ?? 30 }} 你的语音共创已经累计
{{ voiceAnalytics.total_sessions }} 个会话{{ voiceAnalytics.total_turns }} turn
</p>
<p
v-if="voiceAnalytics.low_confidence_turns || voiceAnalytics.safety_interventions"
class="mt-2 text-sm text-gray-500"
>
低置信度确认 {{ voiceAnalytics.low_confidence_turns }}
安全介入 {{ voiceAnalytics.safety_interventions }}
</p>
</div>
<div class="grid grid-cols-2 gap-3 sm:grid-cols-4 lg:min-w-[520px]">
<div class="rounded-lg border border-white/80 bg-white px-3 py-3">
<div class="text-xs text-gray-500">Turn 成功率</div>
<div class="mt-1 text-lg font-semibold text-gray-800">
{{ voiceTurnSuccessRate }}%
</div>
</div>
<div class="rounded-lg border border-white/80 bg-white px-3 py-3">
<div class="text-xs text-gray-500">Finalize 转化率</div>
<div class="mt-1 text-lg font-semibold text-emerald-700">
{{ voiceFinalizeRate }}%
</div>
</div>
<div class="rounded-lg border border-white/80 bg-white px-3 py-3">
<div class="text-xs text-gray-500">ASR / TTS 失败</div>
<div class="mt-1 text-lg font-semibold text-gray-800">
{{ voiceAnalytics.asr_failures }} / {{ voiceAnalytics.tts_failures }}
</div>
</div>
<div class="rounded-lg border border-white/80 bg-white px-3 py-3">
<div class="text-xs text-gray-500">已完成会话</div>
<div class="mt-1 text-lg font-semibold text-violet-700">
{{ voiceAnalytics.finalized_sessions }}
</div>
</div>
</div>
</div>
</BaseCard>
<BaseCard class="mb-8" padding="lg">
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4">
<div class="text-center px-4 py-2">