移除宿主机/conda 启动脚本与 dev 联调工具,后端仅通过 docker compose 部署并默认启用 GPU。模拟客户端与语音确认页迁入 clients/ 下自包含目录,切断对后端源码路径的依赖。 Co-authored-by: Cursor <cursoragent@cursor.com>
360 lines
15 KiB
HTML
Executable File
360 lines
15 KiB
HTML
Executable File
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>手术室耗材 — 语音确认</title>
|
||
<script src="https://cdn.tailwindcss.com"></script>
|
||
<script>
|
||
tailwind.config = {
|
||
theme: {
|
||
extend: {
|
||
colors: {
|
||
clinical: {
|
||
fg: "#0f172a",
|
||
muted: "#475569",
|
||
border: "#e2e8f0",
|
||
accent: "#1d4ed8",
|
||
},
|
||
},
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
<style>
|
||
@keyframes activityBarWave {
|
||
0%,
|
||
100% {
|
||
transform: scaleY(0.22);
|
||
}
|
||
50% {
|
||
transform: scaleY(1);
|
||
}
|
||
}
|
||
@keyframes activityDotPulse {
|
||
0%,
|
||
100% {
|
||
opacity: 1;
|
||
}
|
||
50% {
|
||
opacity: 0.4;
|
||
}
|
||
}
|
||
.activity-banner-active .activity-wave-bar {
|
||
display: inline-block;
|
||
width: 0.45rem;
|
||
min-height: 2.5rem;
|
||
height: 2.5rem;
|
||
transform-origin: bottom center;
|
||
will-change: transform;
|
||
border-radius: 0.2rem;
|
||
animation: activityBarWave 0.52s ease-in-out infinite;
|
||
}
|
||
.activity-banner-active .activity-wave-bar:nth-child(1) {
|
||
animation-delay: 0ms;
|
||
}
|
||
.activity-banner-active .activity-wave-bar:nth-child(2) {
|
||
animation-delay: 0.1s;
|
||
}
|
||
.activity-banner-active .activity-wave-bar:nth-child(3) {
|
||
animation-delay: 0.2s;
|
||
}
|
||
.activity-banner-active .activity-wave-bar:nth-child(4) {
|
||
animation-delay: 0.05s;
|
||
}
|
||
.activity-banner-active .activity-wave-bar:nth-child(5) {
|
||
animation-delay: 0.28s;
|
||
}
|
||
.activity-banner-active .activity-wave-bar:nth-child(6) {
|
||
animation-delay: 0.15s;
|
||
}
|
||
.activity-banner-active .activity-wave-bar:nth-child(7) {
|
||
animation-delay: 0.08s;
|
||
}
|
||
.activity-dot {
|
||
animation: activityDotPulse 0.9s ease-in-out infinite;
|
||
}
|
||
#advancedPanel summary {
|
||
list-style: none;
|
||
}
|
||
#advancedPanel summary::-webkit-details-marker {
|
||
display: none;
|
||
}
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.activity-banner-active .activity-wave-bar {
|
||
animation: none;
|
||
transform: scaleY(0.65);
|
||
}
|
||
.activity-dot {
|
||
animation: none;
|
||
opacity: 1;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body class="min-h-screen bg-slate-50 text-slate-900 antialiased">
|
||
<div class="mx-auto max-w-xl md:max-w-2xl px-4 py-6 md:px-8 md:py-10">
|
||
<header class="mb-8 border-b border-slate-200 pb-6">
|
||
<h1 id="pageTitle" class="text-xl font-semibold text-slate-900">语音确认</h1>
|
||
<p id="surgeryIdLine" class="mt-2 hidden text-base text-slate-600">
|
||
当前手术号:<span id="surgeryIdValue" class="font-medium text-slate-900"></span>
|
||
</p>
|
||
</header>
|
||
|
||
<main class="space-y-6">
|
||
<section class="space-y-4" aria-labelledby="status-heading">
|
||
<h2 id="status-heading" class="sr-only">当前状态</h2>
|
||
<p
|
||
id="statusClinical"
|
||
class="text-2xl font-semibold leading-snug text-slate-900 md:text-3xl"
|
||
role="status"
|
||
aria-live="polite"
|
||
>
|
||
等待下一个确认
|
||
</p>
|
||
<p id="queueClinical" class="text-sm leading-relaxed text-slate-600">暂无排队信息</p>
|
||
|
||
<div
|
||
id="recBanner"
|
||
class="activity-banner mb-3 hidden overflow-hidden rounded-xl border border-red-200 bg-red-50 px-4 py-4 text-center"
|
||
role="status"
|
||
aria-live="polite"
|
||
aria-label="语音流程状态"
|
||
>
|
||
<div
|
||
id="recWave"
|
||
class="mb-3 flex h-14 items-end justify-center gap-1.5 sm:gap-2"
|
||
aria-hidden="true"
|
||
>
|
||
<span class="activity-wave-bar bg-red-400/90"></span>
|
||
<span class="activity-wave-bar bg-red-400"></span>
|
||
<span class="activity-wave-bar bg-red-300"></span>
|
||
<span class="activity-wave-bar bg-red-200"></span>
|
||
<span class="activity-wave-bar bg-red-300"></span>
|
||
<span class="activity-wave-bar bg-red-400"></span>
|
||
<span class="activity-wave-bar bg-red-400/90"></span>
|
||
</div>
|
||
<p id="activityTitle" class="text-sm font-semibold text-red-900">
|
||
<span id="activityDot" class="activity-dot mr-1 inline-block text-red-600" aria-hidden="true">●</span>
|
||
正在采集语音
|
||
</p>
|
||
<p id="activityDesc" class="mt-1 text-xs leading-relaxed text-red-800">
|
||
请对着麦克风清楚回答
|
||
</p>
|
||
</div>
|
||
|
||
<div
|
||
id="resultClinical"
|
||
class="rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm leading-relaxed text-slate-700 shadow-sm"
|
||
role="region"
|
||
aria-label="最近一次确认结果"
|
||
>
|
||
尚无确认结果
|
||
</div>
|
||
|
||
<div class="flex flex-wrap gap-3 pt-1">
|
||
<button
|
||
type="button"
|
||
id="btnRetry"
|
||
class="min-h-11 rounded-lg bg-blue-700 px-4 py-2.5 text-sm font-medium text-white hover:bg-blue-800 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2"
|
||
>
|
||
重试
|
||
</button>
|
||
<button
|
||
type="button"
|
||
id="btnReplay"
|
||
class="min-h-11 rounded-lg border border-slate-300 bg-white px-4 py-2.5 text-sm font-medium text-slate-800 hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2"
|
||
>
|
||
重播提示
|
||
</button>
|
||
</div>
|
||
</section>
|
||
|
||
<details
|
||
id="advancedPanel"
|
||
class="rounded-xl border border-slate-200 bg-white shadow-sm"
|
||
>
|
||
<summary
|
||
class="cursor-pointer select-none px-4 py-3 text-sm font-medium text-slate-800 hover:bg-slate-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-blue-600 rounded-xl"
|
||
>
|
||
高级设置(运维 / 调试)
|
||
</summary>
|
||
<div class="space-y-5 border-t border-slate-200 px-4 py-4">
|
||
<p class="text-sm leading-relaxed text-slate-600">
|
||
以下供信息科或现场调试使用。术间日常使用请保持折叠。页面需用
|
||
<code class="text-xs text-slate-700">http://</code> 或
|
||
<code class="text-xs text-slate-700">https://</code> 打开(勿用本地文件协议);语音接口端口常与页面不同(例如页面
|
||
8080、接口 38080)。
|
||
</p>
|
||
<div>
|
||
<span class="text-xs font-medium uppercase tracking-wide text-slate-500"
|
||
>原始状态(调试)</span
|
||
>
|
||
<p
|
||
id="statusRaw"
|
||
class="mt-1 rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 font-mono text-xs text-slate-800"
|
||
>
|
||
待机
|
||
</p>
|
||
</div>
|
||
|
||
<section>
|
||
<h3 class="mb-3 text-sm font-medium text-slate-800">连接</h3>
|
||
<div class="space-y-3">
|
||
<div>
|
||
<label class="mb-1 block text-xs font-medium text-slate-600" for="baseUrl"
|
||
>服务端 Base URL</label
|
||
>
|
||
<input
|
||
id="baseUrl"
|
||
type="url"
|
||
class="w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 placeholder-slate-400 focus:border-blue-600 focus:outline-none focus:ring-1 focus:ring-blue-600"
|
||
placeholder="http://192.168.1.100:38080"
|
||
autocomplete="off"
|
||
/>
|
||
<p class="mt-1 text-xs leading-relaxed text-slate-500">
|
||
填监控 API 的局域网地址;若本页是 :8080,Base URL 通常应为同一主机的 :38080。
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<label class="mb-1 block text-xs font-medium text-slate-600" for="terminalId"
|
||
>本机语音终端 ID</label
|
||
>
|
||
<input
|
||
id="terminalId"
|
||
type="text"
|
||
class="w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900"
|
||
placeholder="与站点配置中 voice_terminal_id 一致"
|
||
autocomplete="off"
|
||
/>
|
||
</div>
|
||
<label class="flex cursor-pointer items-start gap-2 text-sm text-slate-700">
|
||
<input
|
||
id="autoAssign"
|
||
type="checkbox"
|
||
class="mt-1 rounded border-slate-400 text-blue-700"
|
||
/>
|
||
<span
|
||
>启用服务端自动指派(录制开始后通过长连接接收指派,
|
||
<code class="text-xs text-slate-600">voice_assignment</code>)</span
|
||
>
|
||
</label>
|
||
<div class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-3">
|
||
<label class="mb-1 block text-xs font-medium text-slate-600" for="manualSurgeryId"
|
||
>手动手术号(6 位,未开录或未指派时)</label
|
||
>
|
||
<div class="flex flex-wrap gap-2">
|
||
<input
|
||
id="manualSurgeryId"
|
||
type="text"
|
||
inputmode="numeric"
|
||
maxlength="6"
|
||
class="w-36 rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm tabular-nums text-slate-900"
|
||
placeholder="123456"
|
||
autocomplete="off"
|
||
/>
|
||
<button
|
||
type="button"
|
||
id="btnStartManual"
|
||
class="rounded-lg bg-blue-700 px-3 py-2 text-sm font-medium text-white hover:bg-blue-800 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2"
|
||
>
|
||
开始监控(手动)
|
||
</button>
|
||
</div>
|
||
<p class="mt-2 text-xs text-slate-500">
|
||
可与自动指派同时使用;若长连接暂无数据,会定时请求队首作为补充。
|
||
</p>
|
||
</div>
|
||
<div class="flex flex-wrap items-end gap-4">
|
||
<div>
|
||
<label class="mb-1 block text-xs font-medium text-slate-600" for="recordSec"
|
||
>TTS 播报结束后录制回答的秒数(默认 8)</label
|
||
>
|
||
<input
|
||
id="recordSec"
|
||
type="number"
|
||
min="0"
|
||
max="60"
|
||
step="0.5"
|
||
value="8"
|
||
class="w-32 rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm"
|
||
/>
|
||
</div>
|
||
<label class="flex cursor-pointer items-center gap-2 text-sm text-slate-600">
|
||
<input id="dryRun" type="checkbox" class="rounded border-slate-400" />
|
||
调试:只录音,不上传
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section>
|
||
<h3 class="mb-3 text-sm font-medium text-slate-800">本机操作</h3>
|
||
<div class="flex flex-wrap gap-2">
|
||
<button
|
||
type="button"
|
||
id="btnStop"
|
||
disabled
|
||
class="rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm font-medium text-slate-800 hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-50 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2"
|
||
>
|
||
停止监控(本机)
|
||
</button>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="grid gap-4 sm:grid-cols-2">
|
||
<div class="rounded-lg border border-slate-200 bg-slate-50 p-3">
|
||
<h3 class="text-xs font-medium text-slate-500">排队(原始字段)</h3>
|
||
<p id="queuePositionHint" class="mt-1 text-sm font-medium text-slate-800">无待确认</p>
|
||
<h3 class="mt-3 text-xs font-medium text-slate-500">本场累积序号</h3>
|
||
<p id="cumulativeHint" class="mt-1 text-sm font-medium text-slate-800">无待确认</p>
|
||
<p class="mt-2 text-xs text-slate-500">
|
||
<code class="text-slate-600">pending_queue_position</code> /
|
||
<code class="text-slate-600">pending_queue_length</code>;
|
||
<code class="text-slate-600">pending_cumulative_ordinal</code>
|
||
</p>
|
||
</div>
|
||
<div class="rounded-lg border border-slate-200 bg-slate-50 p-3">
|
||
<h3 class="text-xs font-medium text-slate-500">最近接口响应(JSON)</h3>
|
||
<pre
|
||
id="resolveResult"
|
||
class="mt-2 max-h-36 overflow-auto whitespace-pre-wrap break-words font-mono text-xs text-slate-700"
|
||
>—</pre>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid gap-4 sm:grid-cols-2">
|
||
<section class="min-h-[180px] rounded-lg border border-slate-200 bg-slate-50 p-3">
|
||
<h3 class="mb-2 text-xs font-medium text-slate-500">队首待确认(JSON)</h3>
|
||
<pre
|
||
id="pendingJson"
|
||
class="max-h-64 overflow-auto whitespace-pre-wrap break-words font-mono text-xs text-slate-700"
|
||
>
|
||
—</pre>
|
||
</section>
|
||
<section class="min-h-[180px] rounded-lg border border-slate-200 bg-slate-50 p-3">
|
||
<div class="mb-2 flex items-center justify-between gap-2">
|
||
<h3 class="text-xs font-medium text-slate-500">详细日志</h3>
|
||
<button
|
||
type="button"
|
||
id="btnClearLog"
|
||
class="rounded border border-slate-300 bg-white px-2 py-1 text-[11px] text-slate-700 hover:bg-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-600"
|
||
>
|
||
清空
|
||
</button>
|
||
</div>
|
||
<pre
|
||
id="log"
|
||
class="max-h-64 overflow-auto whitespace-pre-wrap font-mono text-xs text-slate-600"
|
||
></pre>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
</details>
|
||
</main>
|
||
</div>
|
||
|
||
<script src="voice_app.js" defer></script>
|
||
</body>
|
||
</html>
|