feat: surface voice session insights in entry views
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user