chore: update frontend tooling and Chinese copy

This commit is contained in:
2026-04-28 14:52:18 +08:00
parent 55ca0985eb
commit 0ccfd00a23
12 changed files with 1000 additions and 614 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -18,11 +18,11 @@
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.1.0",
"autoprefixer": "^10.4.0",
"autoprefixer": "^10.5.0",
"postcss": "^8.4.0",
"tailwindcss": "^3.4.0",
"typescript": "^5.6.0",
"vite": "^5.4.0",
"vite": "^6.4.2",
"vue-tsc": "^2.1.0"
}
}

View File

@@ -20,7 +20,7 @@ const props = withDefaults(
{
tone: 'light',
title: '生成轨迹',
description: '每次生成和资源重试都会留下事件流,便于确认当前结果来自哪次任务、Provider 是否成功、失败是否可恢复。',
description: '每次生成和资源重试都会留下事件流,便于确认当前结果来自哪次任务、供应商是否成功、失败是否可恢复。',
},
)
@@ -119,9 +119,9 @@ function getEventLabel(eventType: string) {
audio_started: '音频开始',
audio_succeeded: '音频就绪',
audio_failed: '音频失败',
provider_call_started: 'Provider 调用',
provider_call_succeeded: 'Provider 成功',
provider_call_failed: 'Provider 失败',
provider_call_started: '供应商调用',
provider_call_succeeded: '供应商成功',
provider_call_failed: '供应商失败',
asset_retry_started: '资源重试开始',
asset_retry_completed: '资源重试完成',
asset_retry_failed: '资源重试失败',
@@ -301,7 +301,7 @@ defineExpose({ refresh })
class="grid gap-3 md:grid-cols-4"
>
<div class="rounded-lg border p-3" :class="panelClass">
<div class="text-xs" :class="mutedTextClass">Provider 成功率</div>
<div class="text-xs" :class="mutedTextClass">供应商成功率</div>
<div class="mt-1 text-xl font-semibold">{{ providerSuccessRate }}%</div>
</div>
<div class="rounded-lg border p-3" :class="panelClass">

View File

@@ -18,7 +18,7 @@
<header class="flex flex-col md:flex-row md:items-center justify-between gap-4 bg-white p-6 rounded-2xl shadow-sm border border-gray-100">
<div>
<h1 class="text-3xl font-bold gradient-text">引擎调度中心</h1>
<p class="text-sm text-gray-500 mt-1">Provider Orchestration & Strategy</p>
<p class="text-sm text-gray-500 mt-1">供应商编排与策略</p>
</div>
<div class="flex items-center gap-3">
<div class="bg-blue-50 text-blue-700 px-3 py-1 rounded-full text-xs font-medium flex items-center gap-1">
@@ -52,7 +52,7 @@
</div>
</BaseCard>
<BaseCard padding="md" title="可用驱动 (Adapters)">
<BaseCard padding="md" title="可用驱动">
<div class="flex flex-wrap gap-2">
<span v-for="adapter in availableAdapters" :key="adapter"
class="px-2 py-1 text-xs bg-indigo-50 text-indigo-700 rounded-full border border-indigo-100">
@@ -154,21 +154,21 @@
<BaseSelect
v-model="form.adapter"
label="驱动程序 (Adapter)"
label="驱动程序"
:options="adapterOptions"
required
description="选择底层的 API 驱动协议"
/>
<BaseInput v-model="form.model" label="模型名称 (Model)" placeholder="如: gpt-4o, minimax-v2" description="具体调用的模型ID" />
<BaseInput v-model="form.model" label="模型名称" placeholder="如: gpt-4o, minimax-v2" description="具体调用的模型 ID" />
<BaseInput v-model.number="form.priority" label="优先级 (0-100)" type="number" description="数字越大越优先" />
<div class="md:col-span-2 p-4 bg-gray-50 rounded-xl border border-gray-100 space-y-4">
<h3 class="text-sm font-bold text-gray-700">密钥与连接</h3>
<BaseInput v-model="form.api_key" label="API Key" type="password" placeholder="留空则使用 .env 配置" :required="!form.id && !form.config_ref" />
<BaseInput v-model="form.api_base" label="API Endpoint / Group ID" placeholder="https://... 或 Group ID" />
<BaseInput v-model="form.config_ref" label="Fallback Env Var" placeholder="如: OPENAI_API_KEY (高级)" />
<BaseInput v-model="form.api_key" label="API 密钥" type="password" placeholder="留空则使用 .env 配置" :required="!form.id && !form.config_ref" />
<BaseInput v-model="form.api_base" label="API 地址 / 分组 ID" placeholder="https://... 或 Group ID" />
<BaseInput v-model="form.config_ref" label="兜底环境变量" placeholder="如: OPENAI_API_KEY (高级)" />
</div>
<div class="md:col-span-2 flex justify-end gap-3 pt-4 border-t border-gray-100">

View File

@@ -395,13 +395,13 @@ watch([selectedWindow, selectedCapability, selectedVoiceWindow], () => {
</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="text-xs text-gray-500">保存转化率</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="text-xs text-gray-500">转写 / 语音失败</div>
<div class="mt-1 text-lg font-semibold text-gray-800">
{{ voiceAnalytics.asr_failures }} / {{ voiceAnalytics.tts_failures }}
</div>
@@ -446,7 +446,7 @@ watch([selectedWindow, selectedCapability, selectedVoiceWindow], () => {
>
<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">Provider 运营摘要</h2>
<h2 class="text-xl font-bold text-gray-800">供应商运营摘要</h2>
<p class="mt-2 text-sm leading-6 text-gray-500">
最近生成和资源补全留下的供应商调用轨迹
</p>

View File

@@ -116,10 +116,10 @@ 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' },
{ value: 'fallback', label: '文本兜底' },
{ value: 'demo', label: '演示转写' },
{ value: 'openai', label: 'OpenAI 转写' },
{ value: 'openai_asr', label: 'OpenAI 转写适配器' },
]
const analyticsStatusOptions = [
{ value: 'draft', label: '草稿' },
@@ -333,7 +333,7 @@ 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('')
return entries.map(([provider, count]) => `${formatTranscriptionProvider(provider)} ${count}`).join('')
})
const analyticsWindowLabel = computed(() =>
formatAnalyticsWindowLabel(voiceAnalytics.value?.window_days ?? null),
@@ -345,7 +345,7 @@ const transcriptionModeDescription = computed(() => {
case 'disabled':
return '当前环境禁用了真实语音转写,请先使用文本共创或填写开发转写提示。'
default:
return '当前默认是 demo 转写模式。若本地未接真实 ASR可在下方填写转写提示辅助开发验证。'
return '当前默认是演示转写模式。若本地未接真实 ASR可在下方填写转写提示辅助开发验证。'
}
})
const isSessionProcessing = computed(
@@ -438,6 +438,69 @@ function formatConfidence(value: number | null | undefined) {
return `${Math.round(value * 100)}%`
}
function formatTranscriptionProvider(provider: string | null | undefined) {
switch (provider) {
case 'fallback':
return '文本兜底'
case 'demo':
return '演示转写'
case 'openai':
return 'OpenAI 转写'
case 'openai_asr':
return 'OpenAI 转写适配器'
case 'unknown':
return '未知来源'
default:
return provider || '未知来源'
}
}
function formatVoiceEventType(eventType: string) {
const labels: Record<string, string> = {
assistant_audio_failed: '助手语音生成失败',
assistant_audio_ready: '助手语音已就绪',
assistant_audio_retry_failed: '助手语音重试失败',
assistant_audio_retry_succeeded: '助手语音重试成功',
assistant_text_ready: '助手文本已就绪',
intent_resolved: '意图已识别',
safety_intervention_requested: '触发安全介入',
session_abandoned: '会话已放弃',
session_cover_generation_failed: '封面补全失败',
session_cover_generation_queued: '封面补全已排队',
session_created: '会话已创建',
session_failed: '会话处理失败',
session_finalizing: '会话保存中',
session_saved_as_story: '会话已保存为故事',
story_patch_applied: '故事内容已更新',
turn_audio_uploaded: '用户录音已上传',
turn_confirmation_accepted: '家长已确认',
turn_confirmation_requested: '需要家长确认',
turn_confirmation_retry_recording: '选择重新录音',
turn_confirmation_switch_to_text: '选择改为文本',
turn_received: '已收到新回合',
turn_transcribed: '转写已完成',
turn_transcription_failed: '转写失败',
}
return labels[eventType] ?? eventType
}
function formatVoiceEventStatus(status: string) {
switch (status) {
case 'received':
return '已接收'
case 'succeeded':
return '成功'
case 'failed':
return '失败'
case 'blocked':
return '已拦截'
case 'processing':
return '处理中'
default:
return status
}
}
function truncateNarrative(value: string | null, maxLength = 90) {
if (!value) return null
const normalized = value.replace(/\s+/g, ' ').trim()
@@ -1467,7 +1530,7 @@ onBeforeUnmount(() => {
<div class="flex items-center justify-between">
<div>
<h2 class="text-lg font-semibold text-gray-900">最近会话</h2>
<p class="mt-1 text-sm text-gray-500">优先把需要家长确认或安全回看的 session 先拎出来处理</p>
<p class="mt-1 text-sm text-gray-500">优先把需要家长确认或安全回看的会话先拎出来处理</p>
</div>
<span class="text-xs text-gray-400">{{ filteredSessions.length }} </span>
</div>
@@ -1689,7 +1752,7 @@ onBeforeUnmount(() => {
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div class="text-sm leading-6">
{{ attentionCompletionNotice.completedReasonLabel }} 已处理完
你可以继续切到下一类 attention或先回到最近会话总览
你可以继续切到下一类关注事项或先回到最近会话总览
</div>
<div class="flex flex-wrap gap-2">
<button
@@ -1777,7 +1840,7 @@ onBeforeUnmount(() => {
</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>
<div class="text-xs text-gray-500">回合成功率</div>
<div class="mt-1 text-lg font-semibold text-gray-900">{{ turnSuccessRateLabel }}</div>
</div>
<div class="rounded-xl border border-gray-100 bg-gray-50 px-4 py-3">
@@ -1790,7 +1853,7 @@ onBeforeUnmount(() => {
<div class="mt-1 text-lg font-semibold text-rose-700">{{ voiceAnalytics.safety_interventions }}</div>
</div>
<div class="rounded-xl border border-gray-100 bg-gray-50 px-4 py-3">
<div class="text-xs text-gray-500">Finalize 转化率</div>
<div class="text-xs text-gray-500">保存转化率</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">
@@ -2216,7 +2279,7 @@ onBeforeUnmount(() => {
<div id="voice-text-composer" class="rounded-2xl border border-gray-100 bg-white p-4">
<div class="flex items-center justify-between">
<h3 class="font-semibold text-gray-900">文本共创回合</h3>
<span class="text-xs text-gray-400">最稳的 fallback 路径</span>
<span class="text-xs text-gray-400">最稳的文本兜底路径</span>
</div>
<div class="mt-4 space-y-4">
<BaseTextarea
@@ -2241,7 +2304,7 @@ onBeforeUnmount(() => {
<div class="rounded-2xl border border-gray-100 bg-white p-4">
<div class="flex items-center justify-between">
<h3 class="font-semibold text-gray-900">录音共创回合</h3>
<span class="text-xs text-gray-400">已支持上传音频 turn</span>
<span class="text-xs text-gray-400">已支持上传音频回合</span>
</div>
<p class="mt-2 text-sm text-gray-500">
{{ transcriptionModeDescription }}
@@ -2313,7 +2376,7 @@ onBeforeUnmount(() => {
<div class="rounded-2xl border border-gray-100 bg-white p-4">
<div class="flex items-center justify-between">
<h3 class="font-semibold text-gray-900">共创过程</h3>
<span class="text-xs text-gray-400">{{ activeTurnList.length }} 条最近 turn</span>
<span class="text-xs text-gray-400">{{ activeTurnList.length }} 条最近回合</span>
</div>
<div class="mt-4 space-y-4">
@@ -2329,7 +2392,7 @@ onBeforeUnmount(() => {
<span>{{ formatTurnStatus(turn.status) }}</span>
<span>·</span>
<span>{{ formatIntent(turn.detected_intent) }}</span>
<span v-if="turn.transcription_provider">· {{ turn.transcription_provider }}</span>
<span v-if="turn.transcription_provider">· {{ formatTranscriptionProvider(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>
@@ -2458,11 +2521,11 @@ onBeforeUnmount(() => {
class="rounded-xl border border-gray-100 bg-gray-50 px-3 py-3"
>
<div class="flex items-center justify-between gap-3 text-xs text-gray-400">
<span>{{ event.event_type }}</span>
<span>{{ formatVoiceEventType(event.event_type) }}</span>
<span>{{ formatDate(event.created_at) }}</span>
</div>
<div class="mt-1 text-sm font-medium text-gray-800">
{{ event.message || event.status }}
{{ event.message || formatVoiceEventStatus(event.status) }}
</div>
</div>
</div>
@@ -2475,7 +2538,7 @@ onBeforeUnmount(() => {
<EmptyState
:icon="SparklesIcon"
title="创建或恢复一个语音共创会话"
description="左侧可以直接创建新会话,也可以恢复最近仍在等待下一轮的 session。"
description="左侧可以直接创建新会话,也可以恢复最近仍在等待下一轮的会话。"
/>
</div>
</div>