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

@@ -57,7 +57,7 @@ const props = withDefaults(
{
tone: 'light',
title: '生成轨迹',
description: '查看生成、资源补全和 Provider 调用事件,便于演示时解释状态来源与失败恢复。',
description: '查看生成、资源补全和供应商调用事件,便于演示时解释状态来源与失败恢复。',
},
)
@@ -123,9 +123,9 @@ function eventLabel(eventType: string) {
context_prepared: '上下文准备',
narrative_generated: '正文生成',
story_saved: '故事保存',
provider_call_started: 'Provider 调用',
provider_call_succeeded: 'Provider 成功',
provider_call_failed: 'Provider 失败',
provider_call_started: '供应商调用',
provider_call_succeeded: '供应商成功',
provider_call_failed: '供应商失败',
cover_image_started: '封面开始',
cover_image_succeeded: '封面就绪',
cover_image_failed: '封面失败',
@@ -314,7 +314,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="mutedClass">Provider 成功率</div>
<div class="text-xs" :class="mutedClass">供应商成功率</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">
@@ -33,13 +33,13 @@
<div class="flex flex-col gap-5 xl:flex-row xl:items-start xl:justify-between">
<div class="max-w-2xl">
<div class="flex flex-wrap items-center gap-3">
<h2 class="text-xl font-bold text-gray-900">当前环境 Provider 运营摘要</h2>
<h2 class="text-xl font-bold text-gray-900">当前环境供应商运营摘要</h2>
<span class="rounded-full bg-emerald-50 px-3 py-1 text-xs font-medium text-emerald-700">
跨用户 / 当前环境
</span>
</div>
<p class="mt-2 text-sm leading-6 text-gray-500">
这里展示的是当前部署环境内所有生成任务留下的 Provider 调用轨迹便于运营和排障
这里展示的是当前部署环境内所有生成任务留下的供应商调用轨迹便于运营和排障
跨环境对比仍需要后续独立汇聚层
</p>
<div class="mt-4 flex flex-wrap gap-2">
@@ -169,7 +169,7 @@
<div class="mt-1 text-lg font-semibold text-gray-900">{{ formatLatency(analytics.avg_latency_ms) }}</div>
</div>
<div class="rounded-xl border border-gray-100 bg-white px-4 py-3">
<div class="text-xs text-gray-500">配置中 Provider</div>
<div class="text-xs text-gray-500">配置中供应商</div>
<div class="mt-1 text-lg font-semibold text-gray-900">{{ enabledProviderCount }}/{{ providers.length }}</div>
</div>
</div>
@@ -178,8 +178,8 @@
<div class="rounded-2xl border border-gray-100 bg-white">
<div class="flex items-center justify-between border-b border-gray-100 px-5 py-4">
<div>
<h3 class="font-semibold text-gray-900">Provider 调用明细</h3>
<p class="mt-1 text-xs text-gray-500">按能力和 adapter 聚合的当前环境视图</p>
<h3 class="font-semibold text-gray-900">供应商调用明细</h3>
<p class="mt-1 text-xs text-gray-500">按能力和驱动聚合的当前环境视图</p>
</div>
<span class="text-xs text-gray-400">{{ analyticsProviderRows.length }} 个组合</span>
</div>
@@ -217,7 +217,7 @@
</div>
</div>
<div v-if="analyticsProviderRows.length === 0" class="px-5 py-8 text-sm text-gray-500">
当前筛选条件下还没有 Provider 调用样本
当前筛选条件下还没有供应商调用样本
</div>
</div>
</div>
@@ -308,7 +308,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">
@@ -410,21 +410,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>
<!-- MiniMax Specific Config -->

View File

@@ -280,7 +280,7 @@ watch([selectedWindow, selectedCapability], () => {
>
<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

@@ -2,6 +2,55 @@
这份记录用于演示前快速说明“当前本地 Docker 环境已经验证到什么程度”。新的验证记录按时间倒序追加。
## 2026-04-28 拉取后回归与 Voice Studio 文案收敛
- 已拉取远端 `main``55ca098 Add voice analytics filters and metrics` 后完成本地回归。
- 后端复用仓库内 Windows `.venv` 执行全量测试,`118 passed`
- 后端 `ruff check app/ tests/` 通过。
- 用户端与管理端 `npm run build` 均通过;依赖和文案收敛后再次构建通过,且不再出现 `baseline-browser-mapping` 数据偏旧提示。
- Voice Studio、生成轨迹、故事库和供应商管理页已将用户可见的 `session``turn``attention``fallback``Finalize``Provider` 等工程词收敛为中文表达,并补充转写来源、语音事件类型和事件状态的中文展示。
- 用户端与管理端执行依赖安全收敛后,`vite` 升至 `6.4.2``esbuild` 升至 `0.25.12``autoprefixer` 升至 `10.5.0``postcss` 升至 `8.5.12``baseline-browser-mapping` 升至 `2.10.23`
- 用户端与管理端完整 `npm audit --registry=https://registry.npmjs.org` 均为 0 vulnerabilities。
- Alembic 当前只有一个 head`0013_add_voice_sessions_phase_a`;迁移链从 `0012_story_text_status` 到 head 连续。
- `scripts/demo_smoke.sh` shell 语法检查通过,`curl``jq` 可用。
- 当前 WSL 发行版未启用 Docker Desktop 集成,且本地 `52000/52800/52080` 未监听;本轮无法执行完整 `SMOKE_VOICE=1 ./scripts/demo_smoke.sh`
验证命令:
```bash
cd backend && .venv/Scripts/python.exe -m pytest
cd backend && .venv/Scripts/python.exe -m ruff check app/ tests/
cd frontend && npm run build
cd admin-frontend && npm run build
cd frontend && npm audit fix --registry=https://registry.npmjs.org
cd admin-frontend && npm audit fix --registry=https://registry.npmjs.org
cd frontend && npm install autoprefixer@latest -D --registry=https://registry.npmjs.org
cd admin-frontend && npm install autoprefixer@latest -D --registry=https://registry.npmjs.org
cd frontend && npm install vite@^6.4.2 -D --registry=https://registry.npmjs.org
cd admin-frontend && npm install vite@^6.4.2 -D --registry=https://registry.npmjs.org
cd frontend && npm audit --omit=dev --registry=https://registry.npmjs.org
cd admin-frontend && npm audit --omit=dev --registry=https://registry.npmjs.org
cd frontend && npm audit --registry=https://registry.npmjs.org
cd admin-frontend && npm audit --registry=https://registry.npmjs.org
cd backend && .venv/Scripts/python.exe -m compileall -q app tests
cd backend && .venv/Scripts/python.exe -m alembic heads
cd backend && .venv/Scripts/python.exe -m alembic history --verbose -r 0012_story_text_status:head
bash -n scripts/demo_smoke.sh
git diff --check
docker compose config --quiet
curl -fsS --max-time 2 http://localhost:52000/health
curl -fsS --max-time 2 http://localhost:52800/health
curl -fsS --max-time 2 http://localhost:52080/health
```
结果:
- `pytest` 通过118 passed耗时约 4 分 25 秒。
- 后端 lint、Python compileall、用户端构建、管理端构建均通过代码/文档 diff 空白检查通过lockfile 保持仓库既有 CRLF 行尾风格。
- 用户端与管理端构建不再出现 `baseline-browser-mapping` 数据偏旧提示。
- 用户端与管理端完整 audit 均返回 0 vulnerabilities。
- `docker compose config --quiet` 因当前 WSL 找不到 `docker` 命令未执行成功;完整 Docker demo smoke 待启用 Docker Desktop WSL 集成后补跑。
## 2026-04-24
补充验证:

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>