feat: refine voice studio attention workflow
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user