Add voice analytics filters and metrics
This commit is contained in:
@@ -80,6 +80,8 @@ const selectedUniverseId = ref('')
|
||||
const sessionFilter = ref<SessionFilter>('active')
|
||||
const attentionReasonFilter = ref<AttentionReasonFilter>('all')
|
||||
const analyticsWindow = ref<'7' | '30' | 'all'>('30')
|
||||
const analyticsProviderFilter = ref('')
|
||||
const analyticsStatusFilter = ref('')
|
||||
const textTurnInput = ref('')
|
||||
const uploadTranscriptHint = ref('')
|
||||
const loadingSessions = ref(false)
|
||||
@@ -113,10 +115,30 @@ const profileOptions = computed(() =>
|
||||
const universeOptions = computed(() =>
|
||||
universes.value.map((universe) => ({ value: universe.id, label: universe.name })),
|
||||
)
|
||||
const analyticsProviderOptions = [
|
||||
{ value: 'fallback', label: '文本 fallback' },
|
||||
{ value: 'demo', label: 'Demo ASR' },
|
||||
{ value: 'openai', label: 'OpenAI ASR' },
|
||||
{ value: 'openai_asr', label: 'OpenAI ASR Adapter' },
|
||||
]
|
||||
const analyticsStatusOptions = [
|
||||
{ value: 'draft', label: '草稿' },
|
||||
{ value: 'active', label: '进行中' },
|
||||
{ value: 'waiting_user', label: '等待用户' },
|
||||
{ value: 'completed', label: '已完成' },
|
||||
{ value: 'abandoned', label: '已放弃' },
|
||||
]
|
||||
const filteredSessions = computed(() => {
|
||||
return resolveDisplayedSessions(sessions.value)
|
||||
})
|
||||
|
||||
const getSessionInputModeSummary = (session: VoiceSessionSummary) => {
|
||||
if (session.latest_requires_confirmation) return '上一轮待确认'
|
||||
if (session.latest_safety_flags.length) return `安全介入 ${session.latest_safety_flags.length} 项`
|
||||
if (session.latest_detected_intent) return `最近意图:${formatIntent(session.latest_detected_intent)}`
|
||||
return '等待输入'
|
||||
}
|
||||
|
||||
const activeTurnList = computed(() => activeSession.value?.recent_turns ?? [])
|
||||
const hasPendingConfirmation = computed(() => activeSession.value?.latest_requires_confirmation ?? false)
|
||||
const latestPendingConfirmationTurn = computed(
|
||||
@@ -269,6 +291,50 @@ const finalizeConversionRateLabel = computed(() => {
|
||||
if (!voiceAnalytics.value) return '0%'
|
||||
return `${Math.round(voiceAnalytics.value.finalize_conversion_rate * 100)}%`
|
||||
})
|
||||
const confirmationRequestRateLabel = computed(() => {
|
||||
if (!voiceAnalytics.value) return '0%'
|
||||
return `${Math.round(voiceAnalytics.value.confirmation_request_rate * 100)}%`
|
||||
})
|
||||
const userAudioTurnRateLabel = computed(() => {
|
||||
if (!voiceAnalytics.value) return '0%'
|
||||
return `${Math.round(voiceAnalytics.value.user_audio_turn_rate * 100)}%`
|
||||
})
|
||||
const assistantAudioReadyRateLabel = computed(() => {
|
||||
if (!voiceAnalytics.value) return '0%'
|
||||
return `${Math.round(voiceAnalytics.value.assistant_audio_ready_rate * 100)}%`
|
||||
})
|
||||
const asrSuccessRateLabel = computed(() => {
|
||||
if (!voiceAnalytics.value) return '0%'
|
||||
return `${Math.round(voiceAnalytics.value.asr_success_rate * 100)}%`
|
||||
})
|
||||
const ttsSuccessRateLabel = computed(() => {
|
||||
if (!voiceAnalytics.value) return '0%'
|
||||
return `${Math.round(voiceAnalytics.value.tts_success_rate * 100)}%`
|
||||
})
|
||||
const avgConfidenceSummary = computed(() => {
|
||||
if (!voiceAnalytics.value) return '转写 0%,意图 0%'
|
||||
const transcript = Math.round(voiceAnalytics.value.avg_transcript_confidence * 100)
|
||||
const intent = Math.round(voiceAnalytics.value.avg_intent_confidence * 100)
|
||||
return `转写 ${transcript}%,意图 ${intent}%`
|
||||
})
|
||||
const avgUserAudioDurationLabel = computed(() => {
|
||||
if (!voiceAnalytics.value || !voiceAnalytics.value.avg_user_audio_duration_ms) return '0.0 秒'
|
||||
return `${(voiceAnalytics.value.avg_user_audio_duration_ms / 1000).toFixed(1)} 秒`
|
||||
})
|
||||
const avgAssistantAudioDurationLabel = computed(() => {
|
||||
if (!voiceAnalytics.value || !voiceAnalytics.value.avg_assistant_audio_duration_ms) return '0.0 秒'
|
||||
return `${(voiceAnalytics.value.avg_assistant_audio_duration_ms / 1000).toFixed(1)} 秒`
|
||||
})
|
||||
const formatDurationMs = (durationMs: number | null | undefined) => {
|
||||
if (!durationMs) return '0.0 秒'
|
||||
return `${(durationMs / 1000).toFixed(1)} 秒`
|
||||
}
|
||||
const transcriptionProviderSummary = computed(() => {
|
||||
const counts = voiceAnalytics.value?.transcription_provider_counts ?? {}
|
||||
const entries = Object.entries(counts).sort((left, right) => right[1] - left[1])
|
||||
if (!entries.length) return '暂无转写来源'
|
||||
return entries.map(([provider, count]) => `${provider} ${count} 次`).join(',')
|
||||
})
|
||||
const analyticsWindowLabel = computed(() =>
|
||||
formatAnalyticsWindowLabel(voiceAnalytics.value?.window_days ?? null),
|
||||
)
|
||||
@@ -616,10 +682,19 @@ async function syncVoiceStudioRouteState(options?: {
|
||||
}
|
||||
|
||||
function buildVoiceAnalyticsPath() {
|
||||
if (analyticsWindow.value === 'all') {
|
||||
return '/api/voice-sessions/analytics'
|
||||
const params = new URLSearchParams()
|
||||
if (analyticsWindow.value !== 'all') {
|
||||
params.set('days', analyticsWindow.value)
|
||||
}
|
||||
return `/api/voice-sessions/analytics?days=${analyticsWindow.value}`
|
||||
if (analyticsProviderFilter.value) {
|
||||
params.set('provider', analyticsProviderFilter.value)
|
||||
}
|
||||
if (analyticsStatusFilter.value) {
|
||||
params.set('session_status', analyticsStatusFilter.value)
|
||||
}
|
||||
const query = params.toString()
|
||||
const path = '/api/voice-sessions/analytics'
|
||||
return query ? `${path}?${query}` : path
|
||||
}
|
||||
|
||||
function buildVoiceSessionListPath() {
|
||||
@@ -1157,6 +1232,14 @@ function setAnalyticsWindow(value: '7' | '30' | 'all') {
|
||||
analyticsWindow.value = value
|
||||
}
|
||||
|
||||
function setAnalyticsProviderFilter(value: string | number) {
|
||||
analyticsProviderFilter.value = String(value)
|
||||
}
|
||||
|
||||
function setAnalyticsStatusFilter(value: string | number) {
|
||||
analyticsStatusFilter.value = String(value)
|
||||
}
|
||||
|
||||
function setSessionFilter(value: SessionFilter) {
|
||||
suppressAutoAdvanceNotice.value = true
|
||||
clearAttentionCompletionNotice()
|
||||
@@ -1258,6 +1341,10 @@ watch(analyticsWindow, () => {
|
||||
void loadVoiceAnalytics()
|
||||
})
|
||||
|
||||
watch([analyticsProviderFilter, analyticsStatusFilter], () => {
|
||||
void loadVoiceAnalytics()
|
||||
})
|
||||
|
||||
watch(sessionFilter, () => {
|
||||
void loadSessions()
|
||||
})
|
||||
@@ -1528,6 +1615,9 @@ onBeforeUnmount(() => {
|
||||
<div class="mt-1 text-xs text-gray-500">
|
||||
{{ formatSessionStatus(session.status) }} · {{ session.total_turns }} 轮
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-gray-400">
|
||||
{{ getSessionInputModeSummary(session) }}
|
||||
</div>
|
||||
<div
|
||||
class="mt-3 rounded-xl border px-3 py-2 text-xs leading-5"
|
||||
:class="getVoiceSessionNextStep(session).toneClass"
|
||||
@@ -1669,6 +1759,22 @@ onBeforeUnmount(() => {
|
||||
全部
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-3 grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<BaseSelect
|
||||
v-model="analyticsProviderFilter"
|
||||
label="转写来源筛选"
|
||||
:options="analyticsProviderOptions"
|
||||
placeholder="全部来源"
|
||||
@update:modelValue="setAnalyticsProviderFilter"
|
||||
/>
|
||||
<BaseSelect
|
||||
v-model="analyticsStatusFilter"
|
||||
label="会话状态筛选"
|
||||
:options="analyticsStatusOptions"
|
||||
placeholder="全部状态"
|
||||
@update:modelValue="setAnalyticsStatusFilter"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 grid grid-cols-2 gap-3 xl:grid-cols-4">
|
||||
<div class="rounded-xl border border-gray-100 bg-gray-50 px-4 py-3">
|
||||
<div class="text-xs text-gray-500">Turn 成功率</div>
|
||||
@@ -1677,6 +1783,7 @@ onBeforeUnmount(() => {
|
||||
<div class="rounded-xl border border-gray-100 bg-gray-50 px-4 py-3">
|
||||
<div class="text-xs text-gray-500">低置信度触发</div>
|
||||
<div class="mt-1 text-lg font-semibold text-amber-700">{{ voiceAnalytics.low_confidence_turns }}</div>
|
||||
<div class="mt-1 text-xs text-gray-400">确认率 {{ confirmationRequestRateLabel }}</div>
|
||||
</div>
|
||||
<div class="rounded-xl border border-gray-100 bg-gray-50 px-4 py-3">
|
||||
<div class="text-xs text-gray-500">安全介入</div>
|
||||
@@ -1686,12 +1793,36 @@ onBeforeUnmount(() => {
|
||||
<div class="text-xs text-gray-500">Finalize 转化率</div>
|
||||
<div class="mt-1 text-lg font-semibold text-emerald-700">{{ finalizeConversionRateLabel }}</div>
|
||||
</div>
|
||||
<div class="rounded-xl border border-gray-100 bg-gray-50 px-4 py-3">
|
||||
<div class="text-xs text-gray-500">上传语音占比</div>
|
||||
<div class="mt-1 text-lg font-semibold text-sky-700">{{ userAudioTurnRateLabel }}</div>
|
||||
<div class="mt-1 text-xs text-gray-400">上传 {{ voiceAnalytics.uploaded_audio_turns }} / 文本 {{ voiceAnalytics.text_fallback_turns }}</div>
|
||||
</div>
|
||||
<div class="rounded-xl border border-gray-100 bg-gray-50 px-4 py-3">
|
||||
<div class="text-xs text-gray-500">助手语音覆盖</div>
|
||||
<div class="mt-1 text-lg font-semibold text-purple-700">{{ assistantAudioReadyRateLabel }}</div>
|
||||
<div class="mt-1 text-xs text-gray-400">{{ voiceAnalytics.assistant_audio_ready_turns }} 轮有语音</div>
|
||||
</div>
|
||||
<div class="rounded-xl border border-gray-100 bg-gray-50 px-4 py-3">
|
||||
<div class="text-xs text-gray-500">ASR 成功率</div>
|
||||
<div class="mt-1 text-lg font-semibold text-indigo-700">{{ asrSuccessRateLabel }}</div>
|
||||
</div>
|
||||
<div class="rounded-xl border border-gray-100 bg-gray-50 px-4 py-3">
|
||||
<div class="text-xs text-gray-500">TTS 成功率</div>
|
||||
<div class="mt-1 text-lg font-semibold text-fuchsia-700">{{ ttsSuccessRateLabel }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-4 text-sm text-gray-500">
|
||||
ASR 失败 {{ voiceAnalytics.asr_failures }} 次,TTS 失败 {{ voiceAnalytics.tts_failures }} 次;
|
||||
当前共有 {{ voiceAnalytics.total_sessions }} 个会话,其中 {{ voiceAnalytics.attention_sessions }} 个仍需处理,
|
||||
已完成 {{ voiceAnalytics.finalized_sessions }} 个。
|
||||
</p>
|
||||
<p class="mt-2 text-sm text-gray-500">
|
||||
平均用户语音 {{ avgUserAudioDurationLabel }},平均助手语音 {{ avgAssistantAudioDurationLabel }},转写来源:{{ transcriptionProviderSummary }}。
|
||||
</p>
|
||||
<p class="mt-2 text-sm text-gray-500">
|
||||
平均置信度:{{ avgConfidenceSummary }};安全介入率 {{ Math.round(voiceAnalytics.safety_intervention_rate * 100) }}%。
|
||||
</p>
|
||||
<p
|
||||
v-if="voiceAnalytics.attention_sessions"
|
||||
class="mt-2 text-sm text-gray-500"
|
||||
@@ -2199,6 +2330,8 @@ onBeforeUnmount(() => {
|
||||
<span>·</span>
|
||||
<span>{{ formatIntent(turn.detected_intent) }}</span>
|
||||
<span v-if="turn.transcription_provider">· {{ turn.transcription_provider }}</span>
|
||||
<span v-if="turn.user_audio_duration_ms">· 用户语音 {{ formatDurationMs(turn.user_audio_duration_ms) }}</span>
|
||||
<span v-if="turn.assistant_audio_duration_ms">· 助手语音 {{ formatDurationMs(turn.assistant_audio_duration_ms) }}</span>
|
||||
</div>
|
||||
<div class="mt-3 text-sm text-gray-800">
|
||||
<span class="font-medium text-gray-900">孩子:</span>
|
||||
|
||||
Reference in New Issue
Block a user