feat: refine voice studio attention workflow

This commit is contained in:
2026-04-21 14:19:51 +08:00
parent 8b50674d04
commit 9f74a93274
7 changed files with 1025 additions and 48 deletions

View File

@@ -37,6 +37,9 @@ interface StoryItem {
last_error: string | null
}
type VoiceAttentionReason = 'pending_confirmation' | 'safety_intervention' | 'failed_turn'
type VoiceStudioFocusTarget = 'confirmation' | 'safety' | 'failed' | 'text'
const router = useRouter()
const stories = ref<StoryItem[]>([])
const providerAnalytics = ref<GenerationProviderAnalytics | null>(null)
@@ -47,6 +50,7 @@ const loading = ref(true)
const error = ref('')
const showCreateModal = ref(false)
const selectedWindow = ref<'7' | '30' | 'all'>('30')
const selectedVoiceWindow = ref<'7' | '30' | 'all'>('30')
const selectedCapability = ref<'all' | 'text' | 'image' | 'tts' | 'storybook'>('all')
const readableCount = computed(() =>
@@ -72,6 +76,9 @@ const voiceFinalizeRate = computed(() => {
if (!voiceAnalytics.value) return null
return Math.round(voiceAnalytics.value.finalize_conversion_rate * 100)
})
const voiceAnalyticsWindowLabel = computed(() =>
formatWindowLabel(voiceAnalytics.value?.window_days ?? null),
)
function buildProviderAnalyticsPath() {
const params = new URLSearchParams()
@@ -85,6 +92,13 @@ function buildProviderAnalyticsPath() {
return `/api/generations/provider-analytics${query ? `?${query}` : ''}`
}
function buildVoiceAnalyticsPath() {
if (selectedVoiceWindow.value === 'all') {
return '/api/voice-sessions/analytics'
}
return `/api/voice-sessions/analytics?days=${selectedVoiceWindow.value}`
}
async function fetchStories() {
try {
const [storyList, analytics, ops, activeSession, voiceOverview] = await Promise.all([
@@ -92,7 +106,7 @@ async function fetchStories() {
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),
api.get<VoiceSessionAnalytics>(buildVoiceAnalyticsPath()).catch(() => null),
])
stories.value = storyList
providerAnalytics.value = analytics
@@ -123,12 +137,66 @@ function formatDate(dateStr: string) {
})
}
function formatWindowLabel(windowDays: number | null | undefined) {
if (typeof windowDays === 'number') {
return `最近 ${windowDays}`
}
return '全部历史'
}
function goToCreate() {
showCreateModal.value = true
}
function goToVoiceStudio() {
router.push('/voice-studio')
function goToVoiceStudio(options?: {
reason?: VoiceAttentionReason
sessionId?: string
focus?: VoiceStudioFocusTarget
}) {
const query: Record<string, string> = {}
if (options?.reason) {
query.filter = 'attention'
query.reason = options.reason
}
if (options?.sessionId) {
query.session = options.sessionId
}
if (options?.focus) {
query.focus = options.focus
}
router.push({ path: '/voice-studio', query })
}
function continueActiveVoiceSession() {
if (!activeVoiceSession.value) {
goToVoiceStudio()
return
}
if (activeVoiceSession.value.latest_requires_confirmation) {
goToVoiceStudio({
reason: 'pending_confirmation',
sessionId: activeVoiceSession.value.id,
focus: 'confirmation',
})
return
}
if (activeVoiceSession.value.latest_safety_message) {
goToVoiceStudio({
reason: 'safety_intervention',
sessionId: activeVoiceSession.value.id,
focus: 'safety',
})
return
}
if (activeVoiceSession.value.attention_reasons.includes('failed_turn')) {
goToVoiceStudio({
reason: 'failed_turn',
sessionId: activeVoiceSession.value.id,
focus: 'failed',
})
return
}
goToVoiceStudio({ sessionId: activeVoiceSession.value.id })
}
function getStoryLink(story: StoryItem) {
@@ -160,6 +228,10 @@ function setWindow(value: '7' | '30' | 'all') {
selectedWindow.value = value
}
function setVoiceWindow(value: '7' | '30' | 'all') {
selectedVoiceWindow.value = value
}
function setCapability(value: 'all' | 'text' | 'image' | 'tts' | 'storybook') {
selectedCapability.value = value
}
@@ -173,7 +245,7 @@ onMounted(() => {
}
})
watch([selectedWindow, selectedCapability], () => {
watch([selectedWindow, selectedCapability, selectedVoiceWindow], () => {
void fetchStories()
})
</script>
@@ -245,7 +317,7 @@ watch([selectedWindow, selectedCapability], () => {
最近一轮触发了儿童内容安全兜底建议回到工作台查看详细记录
</p>
</div>
<BaseButton @click="goToVoiceStudio">
<BaseButton @click="continueActiveVoiceSession">
<SparklesIcon class="h-5 w-5 mr-2" />
继续语音共创
</BaseButton>
@@ -261,7 +333,7 @@ watch([selectedWindow, selectedCapability], () => {
<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 }} 你的语音共创已经累计
{{ voiceAnalyticsWindowLabel }}你的语音共创已经累计
{{ voiceAnalytics.total_sessions }} 个会话{{ voiceAnalytics.total_turns }} turn
</p>
<p
@@ -271,6 +343,49 @@ watch([selectedWindow, selectedCapability], () => {
低置信度确认 {{ voiceAnalytics.low_confidence_turns }}
安全介入 {{ voiceAnalytics.safety_interventions }}
</p>
<p
v-if="voiceAnalytics.attention_sessions"
class="mt-2 text-sm text-amber-700"
>
当前仍有 {{ voiceAnalytics.attention_sessions }} 个语音会话建议优先回到工作台处理
待确认 {{ voiceAnalytics.confirmation_attention_sessions }}
安全介入 {{ voiceAnalytics.safety_attention_sessions }}
失败待处理 {{ voiceAnalytics.failed_attention_sessions }}
</p>
<div
v-if="voiceAnalytics.attention_sessions"
class="mt-3 flex flex-wrap gap-2"
>
<button
v-if="voiceAnalytics.confirmation_attention_sessions"
type="button"
class="rounded-lg border border-amber-200 bg-amber-50 px-3 py-1.5 text-sm text-amber-700 transition-colors hover:border-amber-400"
@click="goToVoiceStudio({ reason: 'pending_confirmation', focus: 'confirmation' })"
>
查看待确认会话
</button>
<button
v-if="voiceAnalytics.safety_attention_sessions"
type="button"
class="rounded-lg border border-rose-200 bg-rose-50 px-3 py-1.5 text-sm text-rose-700 transition-colors hover:border-rose-400"
@click="goToVoiceStudio({ reason: 'safety_intervention', focus: 'safety' })"
>
查看安全介入会话
</button>
<button
v-if="voiceAnalytics.failed_attention_sessions"
type="button"
class="rounded-lg border border-slate-200 bg-slate-100 px-3 py-1.5 text-sm text-slate-700 transition-colors hover:border-slate-400"
@click="goToVoiceStudio({ reason: 'failed_turn', focus: 'failed' })"
>
查看失败待处理会话
</button>
</div>
<div class="mt-4 flex flex-wrap gap-2">
<button type="button" class="rounded-lg border px-3 py-1.5 text-sm transition-colors" :class="selectedVoiceWindow === '7' ? 'border-violet-700 bg-violet-700 text-white' : 'border-gray-200 bg-white text-gray-600 hover:border-gray-400'" @click="setVoiceWindow('7')">最近 7 </button>
<button type="button" class="rounded-lg border px-3 py-1.5 text-sm transition-colors" :class="selectedVoiceWindow === '30' ? 'border-violet-700 bg-violet-700 text-white' : 'border-gray-200 bg-white text-gray-600 hover:border-gray-400'" @click="setVoiceWindow('30')">最近 30 </button>
<button type="button" class="rounded-lg border px-3 py-1.5 text-sm transition-colors" :class="selectedVoiceWindow === 'all' ? 'border-violet-700 bg-violet-700 text-white' : 'border-gray-200 bg-white text-gray-600 hover:border-gray-400'" @click="setVoiceWindow('all')">全部</button>
</div>
</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">