Files
ZNJJ-api-server/static/voice-demo/index.html
2026-06-03 12:36:18 +08:00

289 lines
11 KiB
HTML

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>VA Voice Chat &mdash; /ws-product</title>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<main class="app">
<header class="app__header">
<div class="brand">
<span class="brand__dot" aria-hidden="true"></span>
<h1>VA Voice Chat</h1>
</div>
<div class="connection">
<label class="connection__field">
<span>服务器地址</span>
<input
id="ws-url"
type="text"
placeholder="ws://host/ws-product"
spellcheck="false"
autocomplete="off"
/>
</label>
<label class="connection__field connection__field--chat">
<span>会话 ID</span>
<div class="chat-id-control">
<input
id="chat-id"
type="text"
placeholder="可选"
spellcheck="false"
autocomplete="off"
/>
<button
id="copy-chat-id-btn"
class="chat-id-control__copy"
type="button"
disabled
title="复制会话 ID"
aria-label="复制会话 ID"
>
<svg class="copy-icon copy-icon--default" viewBox="0 0 16 16" width="14" height="14" fill="none" aria-hidden="true">
<rect x="5" y="5" width="8" height="9" rx="1.5" stroke="currentColor" stroke-width="1.4"/>
<path d="M3 11V3.5A1.5 1.5 0 0 1 4.5 2H11" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>
</svg>
<svg class="copy-icon copy-icon--check" viewBox="0 0 16 16" width="14" height="14" fill="none" aria-hidden="true">
<path d="M3 8.5l3.5 3.5 6.5-7" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</div>
</label>
<button id="connect-btn" class="btn btn--primary" type="button">
连接
</button>
</div>
<div class="status">
<span id="status-dot" class="status__dot status__dot--idle"></span>
<span id="status-text" class="status__text">未连接</span>
</div>
</header>
<div class="app__body">
<div class="app__main">
<div id="conversation" class="conversation">
<aside
id="camera-drawer"
class="camera-drawer"
aria-label="拍照步骤"
aria-hidden="true"
>
<div class="camera-drawer__panel">
<div class="camera-drawer__header">
<div>
<p class="camera-drawer__eyebrow">拍照</p>
<h2>拍照步骤</h2>
</div>
<span id="camera-state" class="camera-drawer__state">状态 -</span>
</div>
<div id="camera-preview" class="camera-drawer__preview">
<video
id="camera-video"
class="camera-drawer__video"
playsinline
muted
autoplay
></video>
<img
id="camera-photo"
class="camera-drawer__photo"
alt="已选择图片预览"
/>
<span class="camera-drawer__corner camera-drawer__corner--tl"></span>
<span class="camera-drawer__corner camera-drawer__corner--tr"></span>
<span class="camera-drawer__corner camera-drawer__corner--bl"></span>
<span class="camera-drawer__corner camera-drawer__corner--br"></span>
<span class="camera-drawer__lens"></span>
<span class="camera-drawer__scan"></span>
<span id="camera-placeholder" class="camera-drawer__placeholder">
打开摄像头实时拍摄,或从下方选择 / 上传图片
</span>
</div>
<p id="camera-question" class="camera-drawer__question"></p>
<div
id="camera-samples"
class="camera-drawer__samples"
aria-label="示例图片,点击选择"
></div>
<div class="camera-drawer__sources">
<label
class="btn btn--ghost camera-drawer__source"
>
上传图片
<input
id="camera-upload"
type="file"
accept="image/*"
hidden
/>
</label>
<button
id="camera-start-btn"
class="btn btn--ghost camera-drawer__source"
type="button"
title="打开摄像头"
>
使用摄像头
</button>
</div>
<label
id="camera-device-row"
class="device-picker camera-drawer__device-row"
hidden
>
<span class="device-picker__label">选择摄像头</span>
<select
id="camera-device-select"
class="device-picker__select"
disabled
>
<option value="">默认摄像头</option>
</select>
</label>
<button
id="camera-done-btn"
class="btn btn--primary camera-drawer__button"
type="button"
disabled
>
拍摄完成
</button>
<canvas id="camera-canvas" hidden></canvas>
</div>
</aside>
<section class="chat" aria-label="对话记录">
<div id="chat-log" class="chat__log" role="log" aria-live="polite">
<div class="chat__empty">
<p>连接服务、开启麦克风后即可开始对话。</p>
<p class="chat__hint">
音频通过 <code>/ws-product</code> 以 PCM16 单声道 16&nbsp;kHz
传输。
</p>
</div>
</div>
</section>
</div>
<footer class="controls" aria-label="操作栏">
<div class="meter" aria-hidden="true">
<div id="meter-fill" class="meter__fill"></div>
</div>
<form id="composer" class="composer" autocomplete="off">
<textarea
id="text-input"
class="composer__input"
rows="1"
placeholder="输入消息,或使用麦克风…"
disabled
></textarea>
<button
id="send-btn"
class="btn btn--primary composer__send"
type="submit"
disabled
title="发送消息 (Enter)"
>
发送
</button>
</form>
<div class="controls__row">
<label class="device-picker">
<span class="device-picker__label">麦克风</span>
<select id="mic-select" class="device-picker__select" disabled>
<option value="">默认麦克风</option>
</select>
</label>
<button
id="mic-btn"
class="mic-btn"
type="button"
disabled
aria-pressed="false"
title="麦克风已关闭"
>
<svg
class="mic-btn__icon"
viewBox="0 0 24 24"
width="24"
height="24"
aria-hidden="true"
>
<path
d="M12 14a3 3 0 0 0 3-3V6a3 3 0 1 0-6 0v5a3 3 0 0 0 3 3Z"
fill="currentColor"
/>
<path
d="M19 11a1 1 0 1 0-2 0 5 5 0 0 1-10 0 1 1 0 1 0-2 0 7 7 0 0 0 6 6.92V21a1 1 0 1 0 2 0v-3.08A7 7 0 0 0 19 11Z"
fill="currentColor"
/>
</svg>
<span class="mic-btn__label">开启麦克风</span>
</button>
<div class="indicators">
<span id="mic-indicator" class="indicator">
<span class="indicator__dot indicator__dot--mic"></span>
<span class="indicator__label">麦克风</span>
</span>
<span id="bot-indicator" class="indicator">
<span class="indicator__dot indicator__dot--bot"></span>
<span class="indicator__label">助手</span>
</span>
<span id="state-indicator" class="indicator indicator--state">
<span class="indicator__dot indicator__dot--state"></span>
<span id="state-label" class="indicator__label">状态 -</span>
</span>
</div>
<button id="clear-btn" class="btn btn--ghost" type="button">
清空
</button>
</div>
<p class="hint">
<kbd>Enter</kbd> 发送,<kbd>Shift</kbd>+<kbd>Enter</kbd>
换行。发送文字会打断正在说话的助手。
浏览器回声消除已开启,如有回音请使用耳机。
</p>
</footer>
</div>
<section class="ws-log" aria-label="WebSocket 日志">
<div class="ws-log__header">
<div class="ws-log__header-left">
<h2>WebSocket 日志</h2>
<div class="ws-log__legend" aria-hidden="true">
<span class="ws-log__legend-item ws-log__legend-item--send">发送</span>
<span class="ws-log__legend-item ws-log__legend-item--recv">接收</span>
</div>
</div>
<button id="clear-ws-log-btn" class="btn btn--ghost" type="button">
清空日志
</button>
</div>
<div id="ws-log" class="ws-log__body" role="log" aria-live="polite">
<div class="ws-log__empty">暂无 WebSocket 事件。</div>
</div>
</section>
</div>
</main>
<script type="module" src="./app.js"></script>
</body>
</html>