moxieTalking/web/index.html
Echo Assistant fee5e4d17b feat: add web server (FastAPI) and web UI on port 8001
- 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
2026-03-31 00:49:38 +00:00

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>