- server.py: FastAPI + WebSocket server wrapping Brain, TTS, Actions
- WS /ws/chat: streaming chat with token-by-token delivery
- GET /api/audio/{filename}: serve generated TTS audio
- GET /api/health: server status check
- Serves static web UI from web/ directory
- web/: self-contained HTML/CSS/JS frontend
- Responsive chat interface with message bubbles
- WebSocket client for real-time streaming
- Voice input via Web Speech API (mic button)
- TTS audio playback (auto + manual replay)
- Conversation sidebar with history
- Settings panel (voice, TTS, sidebar toggles)
- Dark mode support via prefers-color-scheme
- Updated requirements.txt with fastapi, uvicorn, websockets
- Updated .env.example with SERVER_HOST/PORT config
185 lines
8.4 KiB
HTML
185 lines
8.4 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Echo — Voice Assistant</title>
|
|
<link rel="stylesheet" href="style.css">
|
|
</head>
|
|
<body>
|
|
<div id="app">
|
|
<!-- Sidebar -->
|
|
<aside id="sidebar" class="sidebar">
|
|
<div class="sidebar-header">
|
|
<div class="logo">
|
|
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/>
|
|
<path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
|
|
<line x1="12" y1="19" x2="12" y2="23"/>
|
|
<line x1="8" y1="23" x2="16" y2="23"/>
|
|
</svg>
|
|
<span>Echo</span>
|
|
</div>
|
|
<button id="sidebar-close" class="icon-btn" onclick="toggleSidebar()">
|
|
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M11 19l-7-7 7-7"/><path d="M18 5l-7 7 7 7"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<button id="new-chat-btn" class="new-chat-btn" onclick="newChat()">
|
|
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2">
|
|
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
|
</svg>
|
|
New Conversation
|
|
</button>
|
|
<div id="history-list" class="history-list">
|
|
<p class="empty-state">No conversations yet</p>
|
|
</div>
|
|
<div class="sidebar-footer">
|
|
<div id="status-indicator" class="status-dot offline" title="Disconnected"></div>
|
|
<span id="status-text" class="status-text">Connecting...</span>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- Main -->
|
|
<main class="main">
|
|
<!-- Header -->
|
|
<header class="header">
|
|
<button id="sidebar-toggle" class="icon-btn" onclick="toggleSidebar()">
|
|
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2">
|
|
<line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/>
|
|
</svg>
|
|
</button>
|
|
<div class="header-center">
|
|
<h1 id="header-title">Echo</h1>
|
|
<p id="header-subtitle" class="header-subtitle">Ready</p>
|
|
</div>
|
|
<div class="header-right">
|
|
<button id="mic-badge" class="badge" onclick="toggleMic()">
|
|
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/>
|
|
<path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
|
|
</svg>
|
|
<span>Mic</span>
|
|
</button>
|
|
<button id="tts-badge" class="badge active" onclick="toggleTTS()">
|
|
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
|
|
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/>
|
|
<path d="M15.54 8.46a5 5 0 0 1 0 7.07"/>
|
|
<path d="M19.07 4.93a10 10 0 0 1 0 14.14"/>
|
|
</svg>
|
|
<span>TTS</span>
|
|
</button>
|
|
<button class="icon-btn" onclick="toggleSettings()">
|
|
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2">
|
|
<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Chat area -->
|
|
<div id="chat-area" class="chat-area">
|
|
<div id="welcome" class="welcome">
|
|
<div class="welcome-icon">
|
|
<svg viewBox="0 0 24 24" width="40" height="40" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/>
|
|
<path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
|
|
<line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/>
|
|
</svg>
|
|
</div>
|
|
<h2>Echo Voice Assistant</h2>
|
|
<p>Type a message, or click the microphone to use your voice.</p>
|
|
</div>
|
|
<div id="messages" class="messages"></div>
|
|
</div>
|
|
|
|
<!-- Input area -->
|
|
<div class="input-area">
|
|
<form id="chat-form" class="chat-form" onsubmit="sendMessage(event)">
|
|
<button type="button" id="mic-btn" class="mic-btn" onclick="toggleMic()">
|
|
<svg id="mic-icon" viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/>
|
|
<path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
|
|
</svg>
|
|
<svg id="mic-stop" class="hidden" viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
|
|
<rect x="6" y="6" width="12" height="12" rx="2"/>
|
|
</svg>
|
|
</button>
|
|
<div class="input-wrapper">
|
|
<textarea
|
|
id="message-input"
|
|
placeholder="Type a message or use the mic..."
|
|
rows="1"
|
|
onkeydown="handleKeyDown(event)"
|
|
oninput="autoResize(this)"
|
|
></textarea>
|
|
</div>
|
|
<button type="submit" id="send-btn" class="send-btn" disabled>
|
|
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2">
|
|
<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>
|
|
</svg>
|
|
</button>
|
|
</form>
|
|
<div id="recording-indicator" class="recording-indicator hidden">
|
|
<span class="rec-dot"></span>
|
|
<span>Listening...</span>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<!-- Settings panel -->
|
|
<div id="settings-overlay" class="overlay hidden" onclick="toggleSettings()"></div>
|
|
<div id="settings-panel" class="settings-panel hidden">
|
|
<div class="settings-header">
|
|
<h2>Settings</h2>
|
|
<button class="icon-btn" onclick="toggleSettings()">
|
|
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2">
|
|
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<div class="settings-body">
|
|
<div class="setting-group">
|
|
<h3>Voice</h3>
|
|
<label class="setting-row">
|
|
<div>
|
|
<span class="setting-label">Voice Input</span>
|
|
<span class="setting-desc">Enable microphone for speech recognition</span>
|
|
</div>
|
|
<input type="checkbox" id="setting-voice" checked onchange="settings.voiceEnabled=this.checked">
|
|
</label>
|
|
<label class="setting-row">
|
|
<div>
|
|
<span class="setting-label">Auto-Speak Responses</span>
|
|
<span class="setting-desc">Play TTS audio when Echo responds</span>
|
|
</div>
|
|
<input type="checkbox" id="setting-tts" checked onchange="settings.autoSpeak=this.checked">
|
|
</label>
|
|
</div>
|
|
<hr class="divider">
|
|
<div class="setting-group">
|
|
<h3>Interface</h3>
|
|
<label class="setting-row">
|
|
<div>
|
|
<span class="setting-label">Show Sidebar</span>
|
|
<span class="setting-desc">Display conversation history</span>
|
|
</div>
|
|
<input type="checkbox" id="setting-sidebar" checked onchange="toggleSidebar(this.checked)">
|
|
</label>
|
|
</div>
|
|
<hr class="divider">
|
|
<div class="setting-group">
|
|
<h3>About</h3>
|
|
<p class="about-text">
|
|
Echo is a modular, private-first voice assistant using Vosk for local listening, OpenRouter for reasoning, and Qwen3-TTS for speech.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="app.js"></script>
|
|
</body>
|
|
</html>
|