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 { useI18n } from 'vue-i18n'
|
||||||
import { useUserStore } from '../stores/user'
|
import { useUserStore } from '../stores/user'
|
||||||
import { api } from '../api/client'
|
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 BaseButton from '../components/ui/BaseButton.vue'
|
||||||
import LoginDialog from '../components/ui/LoginDialog.vue'
|
import LoginDialog from '../components/ui/LoginDialog.vue'
|
||||||
import {
|
import {
|
||||||
@@ -28,6 +28,7 @@ function switchLocale(lang: 'en' | 'zh') {
|
|||||||
// ========== 登录对话框状态 ==========
|
// ========== 登录对话框状态 ==========
|
||||||
const showLoginDialog = ref(false)
|
const showLoginDialog = ref(false)
|
||||||
const activeVoiceSession = ref<VoiceSessionSummary | null>(null)
|
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() {
|
function scrollToFeatures() {
|
||||||
document.getElementById('features')?.scrollIntoView({ behavior: 'smooth' })
|
document.getElementById('features')?.scrollIntoView({ behavior: 'smooth' })
|
||||||
}
|
}
|
||||||
@@ -82,12 +95,14 @@ onMounted(async () => {
|
|||||||
await userStore.fetchSession()
|
await userStore.fetchSession()
|
||||||
}
|
}
|
||||||
await loadActiveVoiceSession()
|
await loadActiveVoiceSession()
|
||||||
|
await loadVoiceAnalytics()
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => userStore.user?.id,
|
() => userStore.user?.id,
|
||||||
() => {
|
() => {
|
||||||
void loadActiveVoiceSession()
|
void loadActiveVoiceSession()
|
||||||
|
void loadVoiceAnalytics()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -202,6 +217,55 @@ watch(
|
|||||||
了解更多功能
|
了解更多功能
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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 -->
|
<!-- Trust Indicators -->
|
||||||
<div class="mt-12 flex items-center gap-8 text-stone-500">
|
<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 EmptyState from '../components/ui/EmptyState.vue'
|
||||||
import LoadingSpinner from '../components/ui/LoadingSpinner.vue'
|
import LoadingSpinner from '../components/ui/LoadingSpinner.vue'
|
||||||
import type { GenerationOpsSummary, GenerationProviderAnalytics } from '../types/generation'
|
import type { GenerationOpsSummary, GenerationProviderAnalytics } from '../types/generation'
|
||||||
import type { VoiceSessionSummary } from '../types/voiceSession'
|
import type { VoiceSessionAnalytics, VoiceSessionSummary } from '../types/voiceSession'
|
||||||
import {
|
import {
|
||||||
getAssetStatusMeta,
|
getAssetStatusMeta,
|
||||||
getGenerationStatusMeta,
|
getGenerationStatusMeta,
|
||||||
@@ -42,6 +42,7 @@ const stories = ref<StoryItem[]>([])
|
|||||||
const providerAnalytics = ref<GenerationProviderAnalytics | null>(null)
|
const providerAnalytics = ref<GenerationProviderAnalytics | null>(null)
|
||||||
const opsSummary = ref<GenerationOpsSummary | null>(null)
|
const opsSummary = ref<GenerationOpsSummary | null>(null)
|
||||||
const activeVoiceSession = ref<VoiceSessionSummary | null>(null)
|
const activeVoiceSession = ref<VoiceSessionSummary | null>(null)
|
||||||
|
const voiceAnalytics = ref<VoiceSessionAnalytics | null>(null)
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
const showCreateModal = ref(false)
|
const showCreateModal = ref(false)
|
||||||
@@ -63,6 +64,14 @@ const providerSuccessRate = computed(() => {
|
|||||||
})
|
})
|
||||||
const topProvider = computed(() => providerAnalytics.value?.by_provider[0] ?? null)
|
const topProvider = computed(() => providerAnalytics.value?.by_provider[0] ?? null)
|
||||||
const topFailureReason = computed(() => providerAnalytics.value?.failure_reasons[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() {
|
function buildProviderAnalyticsPath() {
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
@@ -78,16 +87,18 @@ function buildProviderAnalyticsPath() {
|
|||||||
|
|
||||||
async function fetchStories() {
|
async function fetchStories() {
|
||||||
try {
|
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<StoryItem[]>('/api/stories'),
|
||||||
api.get<GenerationProviderAnalytics>(buildProviderAnalyticsPath()),
|
api.get<GenerationProviderAnalytics>(buildProviderAnalyticsPath()),
|
||||||
api.get<GenerationOpsSummary>('/api/generations/ops-summary'),
|
api.get<GenerationOpsSummary>('/api/generations/ops-summary'),
|
||||||
api.get<VoiceSessionSummary | null>('/api/voice-sessions/active').catch(() => null),
|
api.get<VoiceSessionSummary | null>('/api/voice-sessions/active').catch(() => null),
|
||||||
|
api.get<VoiceSessionAnalytics>('/api/voice-sessions/analytics?days=30').catch(() => null),
|
||||||
])
|
])
|
||||||
stories.value = storyList
|
stories.value = storyList
|
||||||
providerAnalytics.value = analytics
|
providerAnalytics.value = analytics
|
||||||
opsSummary.value = ops
|
opsSummary.value = ops
|
||||||
activeVoiceSession.value = activeSession
|
activeVoiceSession.value = activeSession
|
||||||
|
voiceAnalytics.value = voiceOverview
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : '加载失败'
|
error.value = e instanceof Error ? e.message : '加载失败'
|
||||||
} finally {
|
} finally {
|
||||||
@@ -221,6 +232,18 @@ watch([selectedWindow, selectedCapability], () => {
|
|||||||
{{ activeVoiceSession.working_title || '未命名语音会话' }},
|
{{ activeVoiceSession.working_title || '未命名语音会话' }},
|
||||||
当前状态 {{ activeVoiceSession.status }},已完成 {{ activeVoiceSession.total_turns }} 轮。
|
当前状态 {{ activeVoiceSession.status }},已完成 {{ activeVoiceSession.total_turns }} 轮。
|
||||||
</p>
|
</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>
|
</div>
|
||||||
<BaseButton @click="goToVoiceStudio">
|
<BaseButton @click="goToVoiceStudio">
|
||||||
<SparklesIcon class="h-5 w-5 mr-2" />
|
<SparklesIcon class="h-5 w-5 mr-2" />
|
||||||
@@ -229,6 +252,55 @@ watch([selectedWindow, selectedCapability], () => {
|
|||||||
</div>
|
</div>
|
||||||
</BaseCard>
|
</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">
|
<BaseCard class="mb-8" padding="lg">
|
||||||
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
<div class="text-center px-4 py-2">
|
<div class="text-center px-4 py-2">
|
||||||
|
|||||||
Reference in New Issue
Block a user