openvoiceui 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/.env.example +104 -0
  2. package/Dockerfile +30 -0
  3. package/LICENSE +21 -0
  4. package/README.md +638 -0
  5. package/SETUP.md +360 -0
  6. package/app.py +232 -0
  7. package/auto-approve-devices.js +111 -0
  8. package/cli/index.js +372 -0
  9. package/config/__init__.py +4 -0
  10. package/config/default.yaml +43 -0
  11. package/config/flags.yaml +67 -0
  12. package/config/loader.py +203 -0
  13. package/config/providers.yaml +71 -0
  14. package/config/speech_normalization.yaml +182 -0
  15. package/config/theme.json +4 -0
  16. package/data/greetings.json +25 -0
  17. package/default-pages/ai-image-creator.html +915 -0
  18. package/default-pages/bulk-image-uploader.html +492 -0
  19. package/default-pages/desktop.html +2865 -0
  20. package/default-pages/file-explorer.html +854 -0
  21. package/default-pages/interactive-map.html +655 -0
  22. package/default-pages/style-guide.html +1005 -0
  23. package/default-pages/website-setup.html +1623 -0
  24. package/deploy/openclaw/Dockerfile +46 -0
  25. package/deploy/openvoiceui.service +30 -0
  26. package/deploy/setup-nginx.sh +50 -0
  27. package/deploy/setup-sudo.sh +306 -0
  28. package/deploy/skill-runner/Dockerfile +19 -0
  29. package/deploy/skill-runner/requirements.txt +14 -0
  30. package/deploy/skill-runner/server.py +269 -0
  31. package/deploy/supertonic/Dockerfile +22 -0
  32. package/deploy/supertonic/server.py +79 -0
  33. package/docker-compose.pinokio.yml +11 -0
  34. package/docker-compose.yml +59 -0
  35. package/greetings.json +25 -0
  36. package/index.html +65 -0
  37. package/inject-device-identity.js +142 -0
  38. package/package.json +82 -0
  39. package/profiles/default.json +114 -0
  40. package/profiles/manager.py +354 -0
  41. package/profiles/schema.json +337 -0
  42. package/prompts/voice-system-prompt.md +149 -0
  43. package/providers/__init__.py +39 -0
  44. package/providers/base.py +63 -0
  45. package/providers/llm/__init__.py +12 -0
  46. package/providers/llm/base.py +71 -0
  47. package/providers/llm/clawdbot_provider.py +112 -0
  48. package/providers/llm/zai_provider.py +115 -0
  49. package/providers/registry.py +320 -0
  50. package/providers/stt/__init__.py +12 -0
  51. package/providers/stt/base.py +58 -0
  52. package/providers/stt/webspeech_provider.py +49 -0
  53. package/providers/stt/whisper_provider.py +100 -0
  54. package/providers/tts/__init__.py +20 -0
  55. package/providers/tts/base.py +91 -0
  56. package/providers/tts/groq_provider.py +74 -0
  57. package/providers/tts/supertonic_provider.py +72 -0
  58. package/requirements.txt +38 -0
  59. package/routes/__init__.py +10 -0
  60. package/routes/admin.py +515 -0
  61. package/routes/canvas.py +1315 -0
  62. package/routes/chat.py +51 -0
  63. package/routes/conversation.py +2158 -0
  64. package/routes/elevenlabs_hybrid.py +306 -0
  65. package/routes/greetings.py +98 -0
  66. package/routes/icons.py +279 -0
  67. package/routes/image_gen.py +364 -0
  68. package/routes/instructions.py +190 -0
  69. package/routes/music.py +838 -0
  70. package/routes/onboarding.py +43 -0
  71. package/routes/pi.py +62 -0
  72. package/routes/profiles.py +215 -0
  73. package/routes/report_issue.py +68 -0
  74. package/routes/static_files.py +533 -0
  75. package/routes/suno.py +664 -0
  76. package/routes/theme.py +81 -0
  77. package/routes/transcripts.py +199 -0
  78. package/routes/vision.py +348 -0
  79. package/routes/workspace.py +288 -0
  80. package/server.py +1510 -0
  81. package/services/__init__.py +1 -0
  82. package/services/auth.py +143 -0
  83. package/services/canvas_versioning.py +239 -0
  84. package/services/db_pool.py +107 -0
  85. package/services/gateway.py +16 -0
  86. package/services/gateway_manager.py +333 -0
  87. package/services/gateways/__init__.py +12 -0
  88. package/services/gateways/base.py +110 -0
  89. package/services/gateways/compat.py +264 -0
  90. package/services/gateways/openclaw.py +1134 -0
  91. package/services/health.py +100 -0
  92. package/services/memory_client.py +455 -0
  93. package/services/paths.py +26 -0
  94. package/services/speech_normalizer.py +285 -0
  95. package/services/tts.py +270 -0
  96. package/setup-config.js +262 -0
  97. package/sounds/air_horn.mp3 +0 -0
  98. package/sounds/bruh.mp3 +0 -0
  99. package/sounds/crowd_cheer.mp3 +0 -0
  100. package/sounds/gunshot.mp3 +0 -0
  101. package/sounds/impact.mp3 +0 -0
  102. package/sounds/lets_go.mp3 +0 -0
  103. package/sounds/record_stop.mp3 +0 -0
  104. package/sounds/rewind.mp3 +0 -0
  105. package/sounds/sad_trombone.mp3 +0 -0
  106. package/sounds/scratch_long.mp3 +0 -0
  107. package/sounds/yeah.mp3 +0 -0
  108. package/src/adapters/ClawdBotAdapter.js +264 -0
  109. package/src/adapters/_template.js +133 -0
  110. package/src/adapters/elevenlabs-classic.js +841 -0
  111. package/src/adapters/elevenlabs-hybrid.js +812 -0
  112. package/src/adapters/hume-evi.js +676 -0
  113. package/src/admin.html +1339 -0
  114. package/src/app.js +8802 -0
  115. package/src/core/Config.js +173 -0
  116. package/src/core/EmotionEngine.js +307 -0
  117. package/src/core/EventBridge.js +180 -0
  118. package/src/core/EventBus.js +117 -0
  119. package/src/core/VoiceSession.js +607 -0
  120. package/src/face/BaseFace.js +259 -0
  121. package/src/face/EyeFace.js +208 -0
  122. package/src/face/HaloSmokeFace.js +509 -0
  123. package/src/face/manifest.json +27 -0
  124. package/src/face/previews/eyes.svg +16 -0
  125. package/src/face/previews/orb.svg +29 -0
  126. package/src/features/MusicPlayer.js +620 -0
  127. package/src/features/Soundboard.js +128 -0
  128. package/src/providers/DeepgramSTT.js +472 -0
  129. package/src/providers/DeepgramStreamingSTT.js +766 -0
  130. package/src/providers/GroqSTT.js +559 -0
  131. package/src/providers/TTSPlayer.js +323 -0
  132. package/src/providers/WebSpeechSTT.js +479 -0
  133. package/src/providers/tts/BaseTTSProvider.js +81 -0
  134. package/src/providers/tts/HumeProvider.js +77 -0
  135. package/src/providers/tts/SupertonicProvider.js +174 -0
  136. package/src/providers/tts/index.js +140 -0
  137. package/src/shell/adapter-registry.js +154 -0
  138. package/src/shell/caller-bridge.js +35 -0
  139. package/src/shell/camera-bridge.js +28 -0
  140. package/src/shell/canvas-bridge.js +32 -0
  141. package/src/shell/commercial-bridge.js +44 -0
  142. package/src/shell/face-bridge.js +44 -0
  143. package/src/shell/music-bridge.js +60 -0
  144. package/src/shell/orchestrator.js +233 -0
  145. package/src/shell/profile-discovery.js +303 -0
  146. package/src/shell/sounds-bridge.js +28 -0
  147. package/src/shell/transcript-bridge.js +61 -0
  148. package/src/shell/waveform-bridge.js +33 -0
  149. package/src/styles/base.css +2862 -0
  150. package/src/styles/face.css +417 -0
  151. package/src/styles/pi-overrides.css +89 -0
  152. package/src/styles/theme-dark.css +67 -0
  153. package/src/test-tts.html +175 -0
  154. package/src/ui/AppShell.js +544 -0
  155. package/src/ui/ProfileSwitcher.js +228 -0
  156. package/src/ui/SessionControl.js +240 -0
  157. package/src/ui/face/FacePicker.js +195 -0
  158. package/src/ui/face/FaceRenderer.js +309 -0
  159. package/src/ui/settings/PlaylistEditor.js +366 -0
  160. package/src/ui/settings/SettingsPanel.css +684 -0
  161. package/src/ui/settings/SettingsPanel.js +419 -0
  162. package/src/ui/settings/TTSVoicePreview.js +210 -0
  163. package/src/ui/themes/ThemeManager.js +213 -0
  164. package/src/ui/visualizers/BaseVisualizer.js +29 -0
  165. package/src/ui/visualizers/PartyFXVisualizer.css +291 -0
  166. package/src/ui/visualizers/PartyFXVisualizer.js +637 -0
  167. package/static/emulators/jsdos/js-dos.css +1 -0
  168. package/static/emulators/jsdos/js-dos.js +22 -0
  169. package/static/favicon.svg +55 -0
  170. package/static/icons/apple-touch-icon.png +0 -0
  171. package/static/icons/favicon-32.png +0 -0
  172. package/static/icons/icon-192.png +0 -0
  173. package/static/icons/icon-512.png +0 -0
  174. package/static/install.html +449 -0
  175. package/static/manifest.json +26 -0
  176. package/static/sw.js +21 -0
  177. package/tts_providers/__init__.py +136 -0
  178. package/tts_providers/base_provider.py +319 -0
  179. package/tts_providers/groq_provider.py +155 -0
  180. package/tts_providers/hume_provider.py +226 -0
  181. package/tts_providers/providers_config.json +119 -0
  182. package/tts_providers/qwen3_provider.py +371 -0
  183. package/tts_providers/resemble_provider.py +315 -0
  184. package/tts_providers/supertonic_provider.py +557 -0
  185. package/tts_providers/supertonic_tts.py +399 -0
@@ -0,0 +1,544 @@
1
+ /**
2
+ * AppShell — injects the application DOM structure into document.body
3
+ *
4
+ * Extracted from index.html (P3-T9: thin shell). Call inject() before
5
+ * any module that queries DOM elements by ID.
6
+ *
7
+ * Usage:
8
+ * import { inject } from './ui/AppShell.js';
9
+ * inject(); // must be first, before DOM queries
10
+ */
11
+ export function inject() {
12
+ document.body.insertAdjacentHTML('afterbegin', SHELL_HTML);
13
+ // Activity chip style — light grey on dark grey, no backdrop-filter
14
+ const chipStyle = document.createElement('style');
15
+ chipStyle.textContent = `.agent-activity-chip{background:rgba(50,55,65,0.95)!important;border:1px solid rgba(180,190,210,0.3)!important;color:#e8eaed!important;backdrop-filter:none!important;box-shadow:0 2px 12px rgba(0,0,0,0.6),inset 0 1px 0 rgba(255,255,255,0.07)!important;overflow:hidden!important;}`;
16
+ document.head.appendChild(chipStyle);
17
+ }
18
+
19
+ const SHELL_HTML = `
20
+ <!-- Canvas Menu Button - Top Left Corner (Desktop) -->
21
+ <button id="canvas-menu-button" title="Desktop"><svg viewBox="0 0 48 48" width="22" height="22" style="vertical-align:middle;filter:drop-shadow(0 0 2px rgba(0,200,255,0.4))"><circle cx="24" cy="24" r="20" fill="#1a8a4a"/><circle cx="24" cy="24" r="20" fill="url(#globe-g)" opacity="0.7"/><ellipse cx="24" cy="24" rx="10" ry="20" fill="none" stroke="rgba(255,255,255,0.45)" stroke-width="1.5"/><line x1="4" y1="24" x2="44" y2="24" stroke="rgba(255,255,255,0.45)" stroke-width="1.5"/><ellipse cx="24" cy="15" rx="17" ry="5" fill="none" stroke="rgba(255,255,255,0.25)" stroke-width="1"/><ellipse cx="24" cy="33" rx="17" ry="5" fill="none" stroke="rgba(255,255,255,0.25)" stroke-width="1"/><circle cx="24" cy="24" r="20" fill="none" stroke="#2dd4bf" stroke-width="1" opacity="0.5"/><defs><radialGradient id="globe-g" cx="35%" cy="35%"><stop offset="0%" stop-color="#3b82f6"/><stop offset="100%" stop-color="#059669"/></radialGradient></defs></svg></button>
22
+
23
+ <!-- Canvas Menu Modal -->
24
+ <div id="canvas-menu-modal" class="canvas-menu-modal" style="display: none;">
25
+ <div class="cmm-backdrop"></div>
26
+ <div class="cmm-content">
27
+ <div class="cmm-header">
28
+ <input type="text" id="canvas-search" placeholder="Search canvas pages...">
29
+ <button class="cmm-close" title="Close">×</button>
30
+ </div>
31
+ <div class="cmm-quick-actions">
32
+ <button class="cmm-qa active" data-filter="all" title="Show all pages">All</button>
33
+ <button class="cmm-qa" data-filter="recent" title="Recently viewed">🕐 Recent</button>
34
+ <button class="cmm-qa" data-filter="starred" title="Starred pages">⭐ Starred</button>
35
+ </div>
36
+ <div class="cmm-categories" id="canvas-categories">
37
+ <div class="cmm-loading">Loading canvas pages...</div>
38
+ </div>
39
+ <div class="cmm-footer">
40
+ <span id="cmm-page-count">0 pages</span>
41
+ <button id="cmm-edit-mode" title="Edit or archive pages">✏️ Edit</button>
42
+ </div>
43
+ </div>
44
+ </div>
45
+
46
+ <!-- Delete Confirmation Modal -->
47
+ <div id="cmm-confirm-modal" class="cmm-confirm-modal" style="display: none;">
48
+ <div class="cmm-confirm-box">
49
+ <div class="cmm-confirm-icon">🗑️</div>
50
+ <div class="cmm-confirm-title">Archive Canvas Page?</div>
51
+ <div class="cmm-confirm-message">This will remove the page from the menu and archive it. The file will be renamed to .bak for safety.</div>
52
+ <div class="cmm-confirm-page-name" id="cmm-confirm-page-name">Page Name</div>
53
+ <div class="cmm-confirm-buttons">
54
+ <button class="cmm-confirm-btn cancel" id="cmm-confirm-cancel" title="Cancel">Cancel</button>
55
+ <button class="cmm-confirm-btn delete" id="cmm-confirm-delete" title="Archive this page">Archive</button>
56
+ </div>
57
+ </div>
58
+ </div>
59
+
60
+ <!-- Canvas System - Full Screen Visual Display -->
61
+ <div id="canvas-container" style="display: none; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; z-index: 150; background: #000; touch-action: manipulation;">
62
+ <iframe
63
+ id="canvas-iframe"
64
+ src="about:blank"
65
+ data-canvas-src=""
66
+ sandbox="allow-same-origin allow-scripts allow-popups allow-popups-to-escape-sandbox allow-forms allow-top-navigation-by-user-activation allow-downloads"
67
+ style="width: 100vw; height: 100vh; border: none; display: block; touch-action: manipulation;"
68
+ allow="autoplay; fullscreen">
69
+ </iframe>
70
+ </div>
71
+
72
+ <!-- Action Console (bottom-left popup) -->
73
+ <button class="console-button" id="console-button" onclick="ActionConsole.toggle()" title="Action Console">
74
+ <span>&gt;_</span>
75
+ <div class="unread-dot" id="console-unread"></div>
76
+ </button>
77
+ <div id="action-console">
78
+ <div class="ac-header">
79
+ <span>Actions</span>
80
+ <div style="display:flex;gap:6px;align-items:center;">
81
+ <button class="ac-clear" onclick="ActionConsole.showSessionInfo()" title="Session Info" style="font-size:13px;">ℹ️ Session</button>
82
+ <button class="ac-clear" style="color:#f85149;" onclick="ActionConsole.resetSession()" title="Reset Session">🔄 Reset</button>
83
+ <button class="ac-clear" onclick="ActionConsole.clear()" title="Clear console">Clear</button>
84
+ <button class="ac-close" onclick="ActionConsole.hide()" title="Close console">&times;</button>
85
+ </div>
86
+ </div>
87
+ <div class="ac-entries" id="action-entries"></div>
88
+ </div>
89
+
90
+ <!-- Transcript Panel (bottom-right popup) -->
91
+ <button class="transcript-button" id="transcript-button" onclick="TranscriptPanel.toggle()" title="Transcript">
92
+ <span>💬</span>
93
+ <div class="unread-dot" id="transcript-unread"></div>
94
+ </button>
95
+ <div id="transcript-panel">
96
+ <div class="tp-header">
97
+ <span>Transcript</span>
98
+ <button class="tp-close" onclick="TranscriptPanel.hide()" title="Close transcript">&times;</button>
99
+ </div>
100
+ <div class="tp-messages" id="transcript-messages"></div>
101
+ <div class="tp-input-bar">
102
+ <label class="tp-upload-btn" title="Attach file">
103
+ 📎
104
+ <input type="file" id="tp-file-input" style="display:none" multiple
105
+ accept="image/*,.pdf,.docx,.xlsx,.pptx,.txt,.md,.json,.csv,.html,.js,.py,.ts,.css"
106
+ onchange="TranscriptPanel.handleUpload(this)">
107
+ </label>
108
+ <div class="tp-file-preview" id="tp-file-preview" style="display:none">
109
+ <span id="tp-file-name"></span>
110
+ <button class="tp-file-clear" onclick="TranscriptPanel.clearFile()" title="Remove attachment">✕</button>
111
+ </div>
112
+ <input type="text" class="tp-text-input" id="tp-text-input"
113
+ placeholder="Type a message..."
114
+ onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();TranscriptPanel.sendText()}">
115
+ <button class="tp-send-btn" onclick="TranscriptPanel.sendText()" title="Send">➤</button>
116
+ </div>
117
+ </div>
118
+
119
+ <!-- User Auth Section -->
120
+ <div id="auth-section">
121
+ <div id="user-button"></div>
122
+ <div id="sign-in-button" style="display: none;">
123
+ <button id="login-btn" title="Sign in to your account">Login</button>
124
+ </div>
125
+ </div>
126
+
127
+ <!-- Login Modal -->
128
+ <div id="clerk-modal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 2000; justify-content: center; align-items: center;">
129
+ <div id="sign-in-container" style="background: var(--panel-bg); padding: 20px; border-radius: 10px; border: 1px solid var(--blue);"></div>
130
+ </div>
131
+
132
+ <!-- Settings Drawer (top-center dropdown) -->
133
+ <div class="settings-drawer" id="settings-drawer">
134
+ <div class="settings-tab" onclick="document.getElementById('settings-drawer').classList.toggle('open')"><span class="status-dot" id="status-dot"></span> SETTINGS</div>
135
+ <div class="settings-panel">
136
+ <div class="settings-group">
137
+ <label>Agent</label>
138
+ <select id="voice-mode-select" onchange="window.QuickSettings?.switchAgent(this.value)">
139
+ <option value="" disabled>Loading agents…</option>
140
+ </select>
141
+ <div class="provider-status" id="agent-status"></div>
142
+ </div>
143
+ <div class="settings-divider"></div>
144
+ <div class="settings-group">
145
+ <label>TTS Provider</label>
146
+ <select id="voice-provider-select" onchange="window.providerManager?.switchProvider(this.value)">
147
+ <option value="supertonic">Supertonic (Free)</option>
148
+ <option value="groq">Groq Orpheus</option>
149
+ <option value="hume">Hume EVI</option>
150
+ </select>
151
+ <div class="provider-status" id="provider-status">✓ Active</div>
152
+ </div>
153
+ <div class="settings-group" id="voice-select-group">
154
+ <label>Voice</label>
155
+ <select id="voice-select" onchange="window.providerManager?.setVoice(this.value)">
156
+ </select>
157
+ </div>
158
+ <div class="settings-divider"></div>
159
+ <div class="settings-group">
160
+ <label>PTT Hotkey</label>
161
+ <div class="ptt-hotkey-row">
162
+ <span class="ptt-hotkey-label" id="ptt-hotkey-label">None</span>
163
+ <button class="ptt-hotkey-btn" id="ptt-hotkey-set" onclick="window.PTTHotkey?.capture()" title="Press a key to set as PTT hotkey">Set</button>
164
+ <button class="ptt-hotkey-btn ptt-hotkey-clear" id="ptt-hotkey-clear" onclick="window.PTTHotkey?.clear()" title="Remove PTT hotkey">✕</button>
165
+ </div>
166
+ <div class="provider-status" id="ptt-hotkey-status">Hold hotkey = hold PTT button</div>
167
+ </div>
168
+ </div>
169
+ </div>
170
+
171
+ <!-- Agent activity chip — top-center, below settings -->
172
+ <div class="agent-activity-chip" id="agent-activity-chip" style="display:none">
173
+ <span class="chip-icon" id="chip-icon">🔄</span>
174
+ <span class="chip-text" id="chip-text">Agent working...</span>
175
+ </div>
176
+
177
+ <!-- Clawdbot Chat UI (hidden - voice-only mode) -->
178
+ <div class="clawdbot-container" id="clawdbot-container" style="display: none !important;">
179
+ <div class="clawdbot-status">
180
+ <span class="clawdbot-status-dot" id="clawdbot-status-dot"></span>
181
+ <span id="clawdbot-status-text">Disconnected</span>
182
+ </div>
183
+ <div class="chat-history" id="chat-history">
184
+ <div class="message message-system">Welcome to Clawdbot mode! Select Clawdbot from the mode selector to connect.</div>
185
+ </div>
186
+ <div class="input-container">
187
+ <input type="text" id="clawdbot-text-input" placeholder="Type your message to Clawdbot..." disabled>
188
+ <button id="clawdbot-send-btn" disabled title="Send message">Send</button>
189
+ </div>
190
+ </div>
191
+
192
+ <!-- Status text (hidden, kept for JS compatibility) -->
193
+ <span id="status-text" style="display:none;">OFFLINE</span>
194
+
195
+ <!-- PARTY EFFECTS - Behind face, visible when music plays with FX enabled -->
196
+ <div class="party-effects-container" id="party-effects-container">
197
+ <div class="center-glow" id="center-glow"></div>
198
+ <div class="ripple-container" id="ripple-container"></div>
199
+ <div class="explosion-container" id="explosion-container"></div>
200
+ <div class="fireworks-container" id="fireworks-container"></div>
201
+ <div class="party-particle-container" id="party-particle-container"></div>
202
+ <div class="disco-container" id="disco-container"></div>
203
+ <div class="oscilloscope-container" id="oscilloscope-container">
204
+ <canvas id="oscilloscope-canvas"></canvas>
205
+ </div>
206
+ <div class="beat-flash" id="beat-flash"></div>
207
+ </div>
208
+
209
+ <!-- Main Face -->
210
+ <div class="face-container">
211
+ <div class="face-box" id="face-box">
212
+ <div class="eyes-container">
213
+ <!-- Left Eye -->
214
+ <div class="eye left-eye" id="left-eye">
215
+ <div class="eye-white">
216
+ <div class="pupil-container" id="left-pupil-container">
217
+ <div class="pupil"></div>
218
+ </div>
219
+ </div>
220
+ <div class="eyelid-top"></div>
221
+ <div class="eyelid-bottom"></div>
222
+ <div class="eye-cap-top"></div>
223
+ </div>
224
+
225
+ <!-- Right Eye -->
226
+ <div class="eye right-eye" id="right-eye">
227
+ <div class="eye-white">
228
+ <div class="pupil-container" id="right-pupil-container">
229
+ <div class="pupil"></div>
230
+ </div>
231
+ </div>
232
+ <div class="eyelid-top"></div>
233
+ <div class="eyelid-bottom"></div>
234
+ <div class="eye-cap-top"></div>
235
+ </div>
236
+
237
+ <!-- Thought Bubbles -->
238
+ <div class="thought-bubbles" id="thought-bubbles">
239
+ <div class="thought-bubble tb-1"></div>
240
+ <div class="thought-bubble tb-2"></div>
241
+ <div class="thought-bubble tb-3"></div>
242
+ </div>
243
+ </div>
244
+
245
+ <!-- Waveform Mouth -->
246
+ <div class="mouth-container">
247
+ <canvas id="waveform-canvas"></canvas>
248
+ </div>
249
+
250
+ <!-- Side visualizers - inside face box so they extend from sides -->
251
+ <div class="side-visualizer left" id="left-viz">
252
+ <!-- Bars added by JS -->
253
+ </div>
254
+ <div class="side-visualizer right" id="right-viz">
255
+ <!-- Bars added by JS -->
256
+ </div>
257
+
258
+ <!-- Top/Bottom visualizers - inside face box -->
259
+ <div class="visualizer-container top" id="top-viz">
260
+ <!-- Bars added by JS -->
261
+ </div>
262
+ <div class="visualizer-container bottom" id="bottom-viz">
263
+ <!-- Bars added by JS -->
264
+ </div>
265
+ </div>
266
+ </div>
267
+
268
+ <!-- Face notification -->
269
+ <div class="face-notification" id="face-notification"></div>
270
+
271
+ <!-- Error message -->
272
+ <div class="error-message" id="error-message"></div>
273
+
274
+ <!-- Control buttons — Edge Tabs -->
275
+ <div class="controls-left">
276
+ <button class="edge-tab left call-button" id="call-button" onclick="ModeManager.toggleVoice()" title="Start / end voice call">
277
+ <span id="call-icon">📞</span>
278
+ </button>
279
+ <button class="edge-tab left call-button" id="stop-button" onclick="ModeManager.stopAll()" title="Stop call" style="display: none; background: rgba(239,68,68,0.25); border-color: rgba(239,68,68,0.6);">
280
+ <span id="stop-icon">⏹️</span>
281
+ </button>
282
+ <button class="edge-tab left wake-button" id="wake-button" onclick="window.toggleWakeWord?.()" title="Wake word listener">
283
+ <span id="wake-icon">👂</span>
284
+ </button>
285
+ <button class="edge-tab left mode-button" id="mode-button" onclick="window.ModeSelector?.toggle(event)" title="Conversation mode">
286
+ <span id="mode-button-icon">🎛️</span>
287
+ </button>
288
+ <button class="edge-tab left ptt-button" id="ptt-button" title="Push to Talk — hold to speak">
289
+ <span id="ptt-icon">🎙️</span>
290
+ </button>
291
+ </div>
292
+
293
+ <div class="controls-right">
294
+ <button class="edge-tab right music-button" id="music-button" onclick="window.musicPlayer?.togglePanel()" title="Music player">
295
+ <span id="music-icon">🎵</span>
296
+ </button>
297
+ <button class="edge-tab right canvas-button" id="canvas-button" onclick="CanvasControl.toggle()" title="Canvas display">
298
+ <span id="canvas-icon">🖥️</span>
299
+ </button>
300
+ <button class="edge-tab right face-button" id="face-button" onclick="window.FacePanel?.toggle()" title="Face recognition">
301
+ <span id="face-icon">👥</span>
302
+ </button>
303
+ <button class="edge-tab right camera-button" id="camera-button" onclick="window.cameraModule?.toggle()" title="Camera">
304
+ <span class="camera-icon">📷</span>
305
+ <video id="camera-video" autoplay playsinline muted></video>
306
+ </button>
307
+ </div>
308
+
309
+ <!-- Face Recognition Panel — slide-out from right edge -->
310
+ <div class="face-panel" id="face-panel">
311
+ <div class="fp-header">
312
+ <span>👥 Recognized Faces</span>
313
+ <button class="fp-close" onclick="window.FacePanel?.hide()" title="Close">&times;</button>
314
+ </div>
315
+ <ul class="fp-face-list" id="fp-face-list">
316
+ <!-- Populated by JS -->
317
+ </ul>
318
+ <div class="fp-actions">
319
+ <button class="fp-btn" id="fp-identify-btn" onclick="window.FacePanel?.identify()" title="Identify who you are">📷 Identify Me</button>
320
+ <div class="fp-status" id="fp-status"></div>
321
+ </div>
322
+ <div class="fp-register" id="fp-register">
323
+ <input type="text" id="fp-name-input" placeholder="Enter your name..." maxlength="30">
324
+ <div class="fp-register-btns">
325
+ <button class="fp-btn" onclick="window.FacePanel?.capture()" title="Take a photo with camera">📷 Capture</button>
326
+ <button class="fp-btn fp-upload" onclick="document.getElementById('fp-file-input').click()" title="Upload a photo">📁 Upload</button>
327
+ </div>
328
+ <input type="file" id="fp-file-input" accept="image/*" multiple style="display:none"
329
+ onchange="window.FacePanel?.handleUpload(this)">
330
+ <div class="fp-photos" id="fp-photos"></div>
331
+ </div>
332
+ </div>
333
+
334
+ <!-- Mode Picker Popup — floats beside the mode button -->
335
+ <div class="mode-picker" id="mode-picker">
336
+ <div class="mode-picker-label">CONVERSATION MODE</div>
337
+ <button class="mode-option" id="mode-opt-normal" onclick="window.ModeSelector?.select('normal')" title="Voice auto-detects speech">
338
+ <span class="mode-opt-icon">🎙️</span>
339
+ <span class="mode-opt-info">
340
+ <span class="mode-opt-name">Normal</span>
341
+ <span class="mode-opt-desc">Auto-detect speech, 3s silence</span>
342
+ </span>
343
+ <span class="mode-opt-check" id="mode-check-normal">✓</span>
344
+ </button>
345
+ <button class="mode-option" id="mode-opt-listen" onclick="window.ModeSelector?.select('listen')" title="Transcribe speech, send manually">
346
+ <span class="mode-opt-icon">👂</span>
347
+ <span class="mode-opt-info">
348
+ <span class="mode-opt-name">Listen</span>
349
+ <span class="mode-opt-desc">Accumulate, send manually</span>
350
+ </span>
351
+ <span class="mode-opt-check" id="mode-check-listen"></span>
352
+ </button>
353
+ <button class="mode-option" id="mode-opt-a2a" onclick="window.ModeSelector?.select('a2a')" title="AI agents talk to each other">
354
+ <span class="mode-opt-icon">🤝</span>
355
+ <span class="mode-opt-info">
356
+ <span class="mode-opt-name">Agent to Agent</span>
357
+ <span class="mode-opt-desc">AI room — human can interject</span>
358
+ </span>
359
+ <span class="mode-opt-check" id="mode-check-a2a"></span>
360
+ </button>
361
+ </div>
362
+
363
+ <!-- Listen Mode Panel — slide-out from right edge (shown in Listen mode) -->
364
+ <div class="listen-panel" id="listen-panel">
365
+ <div class="listen-header">
366
+ <span>👂 Live Transcription</span>
367
+ <button class="listen-close" onclick="window.ModeSelector?.select('normal')" title="Close and return to normal mode">&times;</button>
368
+ </div>
369
+ <input class="listen-title-input" id="listen-title" type="text"
370
+ placeholder="Session title (optional)" maxlength="80" />
371
+ <div class="listen-meta">
372
+ <span id="listen-word-count">0 words</span>
373
+ <button class="listen-clear-btn" onclick="window.ListenPanel?.clear()" title="Clear transcript">Clear</button>
374
+ </div>
375
+ <div class="listen-transcript" id="listen-transcript">
376
+ <span id="listen-empty" style="color:#3d3d3d;font-style:italic">Start speaking — transcript will appear here</span>
377
+ </div>
378
+ <div class="listen-interim" id="listen-interim"></div>
379
+ <div class="listen-actions">
380
+ <button class="listen-action-btn listen-save-btn" id="listen-save-btn" onclick="window.ListenPanel?.save()" disabled title="Save to server">
381
+ 💾 Save
382
+ </button>
383
+ <button class="listen-action-btn listen-send-btn" id="listen-send-btn" onclick="window.ListenPanel?.sendOnly()" disabled title="Send to agent">
384
+ 📤 Send
385
+ </button>
386
+ <button class="listen-action-btn listen-talk-btn" id="listen-talk-btn" onclick="window.ListenPanel?.saveAndTalk()" disabled title="Save and start voice call">
387
+ 📞 Save+Talk
388
+ </button>
389
+ </div>
390
+ <div class="listen-save-status" id="listen-save-status"></div>
391
+ </div>
392
+
393
+ <!-- Agent-to-Agent Panel — slide-out from left edge (shown in A2A mode) -->
394
+ <div class="a2a-panel" id="a2a-panel">
395
+ <div class="a2a-header">
396
+ <span>🤝 Agent-to-Agent</span>
397
+ <button class="a2a-close" onclick="window.AgentToAgentPanel?.hide()" title="Close">&times;</button>
398
+ </div>
399
+ <div class="a2a-config">
400
+ <label class="a2a-label">This client is:</label>
401
+ <select id="a2a-role" onchange="window.AgentToAgentPanel?.setRole(this.value)">
402
+ <option value="default">Assistant</option>
403
+ <option value="pgai">PGAI</option>
404
+ <option value="observer">Observer (Human)</option>
405
+ </select>
406
+ </div>
407
+ <div class="a2a-status-row">
408
+ <div class="a2a-turn-indicator" id="a2a-turn-indicator">
409
+ <span class="a2a-dot" id="a2a-dot"></span>
410
+ <span id="a2a-turn-label">Idle</span>
411
+ </div>
412
+ <div class="a2a-room-id" id="a2a-room-id"></div>
413
+ </div>
414
+ <div class="a2a-controls">
415
+ <button class="a2a-btn a2a-start" id="a2a-start-btn" onclick="window.AgentToAgentRoom?.start()" title="Start agent-to-agent room">▶ Start Room</button>
416
+ <button class="a2a-btn a2a-stop" id="a2a-stop-btn" onclick="window.AgentToAgentRoom?.stop()" title="Stop room" style="display:none">⏹ Stop</button>
417
+ </div>
418
+ <div class="a2a-transcript" id="a2a-transcript">
419
+ <div class="a2a-transcript-empty">Conversation will appear here</div>
420
+ </div>
421
+ </div>
422
+
423
+ <!-- Music Panel — slide-up card -->
424
+ <div class="music-panel" id="music-panel">
425
+ <!-- FULL VIEW (Concept T — Waveform Bar) -->
426
+ <div class="mp-full" id="mp-full">
427
+ <div class="mp-waveform-bg" id="mp-waveform">
428
+ <div class="bar" style="height:12px;--d:0.6s"></div><div class="bar" style="height:22px;--d:0.9s"></div>
429
+ <div class="bar" style="height:18px;--d:0.7s"></div><div class="bar" style="height:28px;--d:1.1s"></div>
430
+ <div class="bar" style="height:14px;--d:0.5s"></div><div class="bar" style="height:24px;--d:0.8s"></div>
431
+ <div class="bar" style="height:10px;--d:1.0s"></div><div class="bar" style="height:30px;--d:0.65s"></div>
432
+ <div class="bar" style="height:20px;--d:0.85s"></div><div class="bar" style="height:16px;--d:0.75s"></div>
433
+ <div class="bar" style="height:26px;--d:0.55s"></div><div class="bar" style="height:12px;--d:0.95s"></div>
434
+ <div class="bar" style="height:22px;--d:0.7s"></div><div class="bar" style="height:18px;--d:1.05s"></div>
435
+ <div class="bar" style="height:28px;--d:0.6s"></div><div class="bar" style="height:14px;--d:0.9s"></div>
436
+ <div class="bar" style="height:24px;--d:0.8s"></div><div class="bar" style="height:10px;--d:0.55s"></div>
437
+ <div class="bar" style="height:20px;--d:1.1s"></div><div class="bar" style="height:16px;--d:0.75s"></div>
438
+ </div>
439
+ <button class="mp-collapse" onclick="window.musicPlayer?.collapsePanel()" title="Minimize">&#9660;</button>
440
+ <div class="mp-info-row">
441
+ <div class="mp-track-info">
442
+ <span class="track-label">NOW PLAYING</span>
443
+ <span class="track-name" id="track-name">-</span>
444
+ </div>
445
+ <select id="playlist-select" onchange="window.switchPlaylist?.(this.value)" title="Switch Playlist">
446
+ <option value="generated" selected>Generated Tracks</option>
447
+ <option value="music">Playlist 1</option>
448
+ </select>
449
+ </div>
450
+ <div class="mp-timeline-row">
451
+ <span class="mp-time" id="mp-time-cur">0:00</span>
452
+ <div class="mp-timeline-wrap">
453
+ <input type="range" class="mp-timeline" id="mp-timeline" min="0" max="100" value="0">
454
+ <div class="mp-timeline-fill" id="mp-timeline-fill" style="width:0%"></div>
455
+ </div>
456
+ <span class="mp-time" id="mp-time-dur">0:00</span>
457
+ </div>
458
+ <div class="mp-controls-row">
459
+ <button onclick="window.musicPlayer?.prev()" title="Previous track">&#9198;</button>
460
+ <button class="mp-play" onclick="window.musicPlayer?.togglePlay()" id="play-pause-btn" title="Play / Pause">&#9208;</button>
461
+ <button onclick="window.musicPlayer?.next()" title="Next track">&#9197;</button>
462
+ <input type="range" class="mp-vol" id="volume-slider" min="0" max="100" value="85"
463
+ onchange="window.musicPlayer?.setVolume(this.value)">
464
+ <div class="mp-toggle"><input type="checkbox" id="autoplay-checkbox"
465
+ onchange="window.toggleAutoplay?.(this.checked)"><label for="autoplay-checkbox">Auto</label></div>
466
+ <div class="mp-toggle"><input type="checkbox" id="visualizer-checkbox"
467
+ onchange="window.toggleVisualizer?.(this.checked)" checked><label for="visualizer-checkbox">FX</label></div>
468
+ </div>
469
+ </div>
470
+ <!-- MINI VIEW -->
471
+ <div class="mp-mini" id="mp-mini" style="display:none">
472
+ <button onclick="window.musicPlayer?.prev()" title="Previous track">&#9198;</button>
473
+ <button class="mp-play" onclick="window.musicPlayer?.togglePlay()" id="play-pause-btn-mini" title="Play / Pause">&#9208;</button>
474
+ <button onclick="window.musicPlayer?.next()" title="Next track">&#9197;</button>
475
+ <input type="range" class="mp-vol mp-mini-vol" min="0" max="100" value="85"
476
+ onchange="window.musicPlayer?.setVolume(this.value)">
477
+ <button class="mp-expand" onclick="window.musicPlayer?.expandPanel()" title="Expand">&#9650;</button>
478
+ </div>
479
+ </div>
480
+
481
+ <!-- TTS Provider Selector (moved to settings drawer) -->
482
+ <div class="voice-provider-selector" id="voice-provider-selector" style="display: none;"></div>
483
+
484
+ <!-- Voice-only UI - no text input, voice-to-voice only -->
485
+
486
+ <!-- Hidden elements -->
487
+ <audio id="music-player" style="display: none;" crossorigin="anonymous"></audio>
488
+ <audio id="music-player-2" style="display: none;" crossorigin="anonymous"></audio>
489
+ <canvas id="capture-canvas" style="display: none;"></canvas>
490
+
491
+ <!-- Issue Report Button — top, halfway between center and right edge -->
492
+ <button id="issue-report-btn" class="issue-report-btn" title="Report an issue" onclick="window.IssueReporter?.open()">
493
+ <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
494
+ <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
495
+ <line x1="12" y1="9" x2="12" y2="13"/>
496
+ <line x1="12" y1="17" x2="12.01" y2="17"/>
497
+ </svg>
498
+ </button>
499
+
500
+ <!-- Issue Report Modal -->
501
+ <div id="issue-report-modal" class="issue-report-modal" style="display:none;" onclick="if(event.target===this)window.IssueReporter?.close()">
502
+ <div class="irm-box">
503
+ <div class="irm-header">
504
+ <span class="irm-title">
505
+ <svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle;margin-right:6px;color:#f59e0b">
506
+ <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
507
+ <line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>
508
+ </svg>
509
+ Report an Issue
510
+ </span>
511
+ <button class="irm-close" onclick="window.IssueReporter?.close()" title="Close">&times;</button>
512
+ </div>
513
+ <div class="irm-body">
514
+ <div class="irm-field">
515
+ <label class="irm-label">Issue Type</label>
516
+ <select id="irm-type" class="irm-select">
517
+ <option value="stt">STT / Mic not working</option>
518
+ <option value="tts">TTS / Audio problem</option>
519
+ <option value="connection">Connection / Gateway error</option>
520
+ <option value="canvas">Canvas / Display issue</option>
521
+ <option value="slow">Slow / Unresponsive</option>
522
+ <option value="bug">Other bug</option>
523
+ <option value="feedback">Feedback / Request</option>
524
+ </select>
525
+ </div>
526
+ <div class="irm-field">
527
+ <label class="irm-label">Describe what happened</label>
528
+ <textarea id="irm-description" class="irm-textarea" placeholder="What were you doing? What went wrong?" rows="4" maxlength="2000"></textarea>
529
+ </div>
530
+ <div class="irm-context-row" id="irm-context-row">
531
+ <span class="irm-context-label">Auto-attached:</span>
532
+ <span class="irm-context-value" id="irm-context-preview"></span>
533
+ </div>
534
+ </div>
535
+ <div class="irm-footer">
536
+ <span class="irm-status" id="irm-status"></span>
537
+ <div class="irm-btns">
538
+ <button class="irm-btn irm-cancel" onclick="window.IssueReporter?.close()">Cancel</button>
539
+ <button class="irm-btn irm-submit" id="irm-submit-btn" onclick="window.IssueReporter?.submit()">Submit Report</button>
540
+ </div>
541
+ </div>
542
+ </div>
543
+ </div>
544
+ `;