279 lines
10 KiB
HTML
279 lines
10 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>VA Voice Chat — /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>WebSocket URL</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>Chat ID</span>
|
|
<div class="chat-id-control">
|
|
<input
|
|
id="chat-id"
|
|
type="text"
|
|
placeholder="optional chatId"
|
|
spellcheck="false"
|
|
autocomplete="off"
|
|
/>
|
|
<button
|
|
id="copy-chat-id-btn"
|
|
class="chat-id-control__copy"
|
|
type="button"
|
|
disabled
|
|
title="Copy Chat ID"
|
|
aria-label="Copy Chat 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">
|
|
Connect
|
|
</button>
|
|
</div>
|
|
|
|
<div class="status">
|
|
<span id="status-dot" class="status__dot status__dot--idle"></span>
|
|
<span id="status-text" class="status__text">Disconnected</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="Camera capture step"
|
|
aria-hidden="true"
|
|
>
|
|
<div class="camera-drawer__panel">
|
|
<div class="camera-drawer__header">
|
|
<div>
|
|
<p class="camera-drawer__eyebrow">Camera</p>
|
|
<h2>拍照步骤</h2>
|
|
</div>
|
|
<span id="camera-state" class="camera-drawer__state">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="Selected image preview"
|
|
/>
|
|
<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 class="camera-drawer__sources">
|
|
<button
|
|
id="camera-start-btn"
|
|
class="btn btn--ghost camera-drawer__source"
|
|
type="button"
|
|
>
|
|
使用摄像头
|
|
</button>
|
|
<button
|
|
id="camera-flip-btn"
|
|
class="btn btn--ghost camera-drawer__source"
|
|
type="button"
|
|
hidden
|
|
>
|
|
切换摄像头
|
|
</button>
|
|
<label class="btn btn--ghost camera-drawer__source">
|
|
上传图片
|
|
<input
|
|
id="camera-upload"
|
|
type="file"
|
|
accept="image/*"
|
|
hidden
|
|
/>
|
|
</label>
|
|
</div>
|
|
|
|
<div
|
|
id="camera-samples"
|
|
class="camera-drawer__samples"
|
|
aria-label="示例图片,点击选择"
|
|
></div>
|
|
|
|
<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="Conversation history">
|
|
<div id="chat-log" class="chat__log" role="log" aria-live="polite">
|
|
<div class="chat__empty">
|
|
<p>Connect to the engine, enable your mic, and start talking.</p>
|
|
<p class="chat__hint">
|
|
Audio is streamed as PCM16 mono @ 16 kHz over
|
|
<code>/ws-product</code>.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
<footer class="controls" aria-label="Chat controls">
|
|
<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="Type a message, or use the mic…"
|
|
disabled
|
|
></textarea>
|
|
<button
|
|
id="send-btn"
|
|
class="btn btn--primary composer__send"
|
|
type="submit"
|
|
disabled
|
|
title="Send message (Enter)"
|
|
>
|
|
Send
|
|
</button>
|
|
</form>
|
|
|
|
<div class="controls__row">
|
|
<label class="device-picker">
|
|
<span class="device-picker__label">Microphone</span>
|
|
<select id="mic-select" class="device-picker__select" disabled>
|
|
<option value="">Default microphone</option>
|
|
</select>
|
|
</label>
|
|
|
|
<button
|
|
id="mic-btn"
|
|
class="mic-btn"
|
|
type="button"
|
|
disabled
|
|
aria-pressed="false"
|
|
title="Mic is off"
|
|
>
|
|
<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">Enable mic</span>
|
|
</button>
|
|
|
|
<div class="indicators">
|
|
<span id="mic-indicator" class="indicator">
|
|
<span class="indicator__dot indicator__dot--mic"></span>
|
|
<span class="indicator__label">Mic</span>
|
|
</span>
|
|
<span id="bot-indicator" class="indicator">
|
|
<span class="indicator__dot indicator__dot--bot"></span>
|
|
<span class="indicator__label">Bot</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">State -</span>
|
|
</span>
|
|
</div>
|
|
|
|
<button id="clear-btn" class="btn btn--ghost" type="button">
|
|
Clear
|
|
</button>
|
|
</div>
|
|
|
|
<p class="hint">
|
|
Press <kbd>Enter</kbd> to send, <kbd>Shift</kbd>+<kbd>Enter</kbd>
|
|
for newline. Sending text will interrupt the bot if it's speaking.
|
|
Browser echo cancellation is on; use headphones if echo persists.
|
|
</p>
|
|
</footer>
|
|
</div>
|
|
|
|
<section class="ws-log" aria-label="WebSocket log">
|
|
<div class="ws-log__header">
|
|
<div class="ws-log__header-left">
|
|
<h2>WebSocket Log</h2>
|
|
<div class="ws-log__legend" aria-hidden="true">
|
|
<span class="ws-log__legend-item ws-log__legend-item--send">Send</span>
|
|
<span class="ws-log__legend-item ws-log__legend-item--recv">Recv</span>
|
|
</div>
|
|
</div>
|
|
<button id="clear-ws-log-btn" class="btn btn--ghost" type="button">
|
|
Clear log
|
|
</button>
|
|
</div>
|
|
<div id="ws-log" class="ws-log__body" role="log" aria-live="polite">
|
|
<div class="ws-log__empty">No websocket events yet.</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</main>
|
|
|
|
<script type="module" src="./app.js"></script>
|
|
</body>
|
|
</html>
|