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

@@ -4,7 +4,7 @@ import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useUserStore } from '../stores/user'
import { api } from '../api/client'
import type { VoiceSessionSummary } from '../types/voiceSession'
import type { VoiceSessionAnalytics, VoiceSessionSummary } from '../types/voiceSession'
import BaseButton from '../components/ui/BaseButton.vue'
import LoginDialog from '../components/ui/LoginDialog.vue'
import {
@@ -28,6 +28,7 @@ function switchLocale(lang: 'en' | 'zh') {
// ========== 登录对话框状态 ==========
const showLoginDialog = ref(false)
const activeVoiceSession = ref<VoiceSessionSummary | null>(null)
const voiceAnalytics = ref<VoiceSessionAnalytics | null>(null)
// ========== 创作入口 ==========
// 旧的创作变量已移除,现在只负责跳转
@@ -68,6 +69,18 @@ async function loadActiveVoiceSession() {
}
}
async function loadVoiceAnalytics() {
if (!userStore.user) {
voiceAnalytics.value = null
return
}
try {
voiceAnalytics.value = await api.get<VoiceSessionAnalytics>('/api/voice-sessions/analytics?days=30')
} catch {
voiceAnalytics.value = null
}
}
function scrollToFeatures() {
document.getElementById('features')?.scrollIntoView({ behavior: 'smooth' })
}
@@ -82,12 +95,14 @@ onMounted(async () => {
await userStore.fetchSession()
}
await loadActiveVoiceSession()
await loadVoiceAnalytics()
})
watch(
() => userStore.user?.id,
() => {
void loadActiveVoiceSession()
void loadVoiceAnalytics()
},
)
@@ -202,6 +217,55 @@ watch(
了解更多功能
</button>
</div>
<div
v-if="activeVoiceSession || (voiceAnalytics && voiceAnalytics.total_sessions)"
class="mt-6 rounded-2xl border border-stone-200 bg-white/80 p-4 shadow-sm backdrop-blur-sm"
>
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div class="space-y-2">
<p
v-if="activeVoiceSession"
class="text-sm text-stone-700"
>
最近的语音共创会话
<span class="font-semibold text-stone-900">
{{ activeVoiceSession.working_title || '未命名会话' }}
</span>
仍可继续当前已完成 {{ activeVoiceSession.total_turns }}
</p>
<p
v-if="activeVoiceSession?.latest_requires_confirmation"
class="text-sm text-amber-700"
>
上一轮仍在等待家长确认回到工作台后可以选择继续重说或切到文本输入
</p>
<p
v-else-if="activeVoiceSession?.latest_safety_message"
class="text-sm text-rose-700"
>
最近一轮触发了儿童内容安全兜底工作台里可以查看完整记录
</p>
<p
v-else-if="voiceAnalytics && voiceAnalytics.total_sessions"
class="text-sm text-stone-600"
>
最近 30 天语音共创 {{ voiceAnalytics.total_sessions }} 个会话
turn 成功率 {{ Math.round(voiceAnalytics.turn_success_rate * 100) }}%
finalize 转化率 {{ Math.round(voiceAnalytics.finalize_conversion_rate * 100) }}%
</p>
</div>
<BaseButton
v-if="activeVoiceSession"
size="sm"
variant="secondary"
@click="continueVoiceStudio"
>
<MicrophoneIcon class="h-4 w-4 mr-2" />
回到语音共创
</BaseButton>
</div>
</div>
<!-- Trust Indicators -->
<div class="mt-12 flex items-center gap-8 text-stone-500">

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">