OrbitMC commited on
Commit
a4d107b
·
verified ·
1 Parent(s): f7ae188

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +171 -191
templates/index.html CHANGED
@@ -6,133 +6,109 @@
6
  <title>J.A.R.V.I.S. AI</title>
7
  <style>
8
  * { margin: 0; padding: 0; box-sizing: border-box; }
9
-
10
  body {
11
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
12
- background: #0a0a1a;
13
- color: #e0e0e0;
14
- height: 100vh;
15
- display: flex;
16
- flex-direction: column;
17
- overflow: hidden;
18
  }
19
 
20
  .header {
21
  background: linear-gradient(135deg, #0d1b2a, #1b2838);
22
  border-bottom: 1px solid #00d4ff33;
23
- padding: 14px 24px;
24
- display: flex;
25
- align-items: center;
26
- justify-content: space-between;
27
- flex-shrink: 0;
28
  }
29
-
30
- .header-left { display: flex; align-items: center; gap: 14px; }
31
-
32
  .arc-reactor {
33
- width: 42px; height: 42px; border-radius: 50%;
34
  background: radial-gradient(circle, #00d4ff 0%, #0088aa 40%, #004466 70%, transparent 100%);
35
  box-shadow: 0 0 20px #00d4ff88, 0 0 40px #00d4ff44, inset 0 0 10px #00d4ff66;
36
- animation: pulse 2s ease-in-out infinite;
37
- position: relative;
38
  }
39
-
40
  .arc-reactor::after {
41
  content: ''; position: absolute; top: 50%; left: 50%;
42
- transform: translate(-50%, -50%);
43
- width: 14px; height: 14px; border-radius: 50%;
44
- background: #00d4ff; box-shadow: 0 0 8px #00d4ff;
45
  }
46
-
47
  @keyframes pulse {
48
  0%, 100% { box-shadow: 0 0 20px #00d4ff88, 0 0 40px #00d4ff44; }
49
  50% { box-shadow: 0 0 30px #00d4ffaa, 0 0 60px #00d4ff66; }
50
  }
 
 
51
 
52
- .header-title h1 {
53
- font-size: 1.3rem; font-weight: 600;
54
- color: #00d4ff; letter-spacing: 3px; text-transform: uppercase;
55
- }
56
- .header-title p { font-size: 0.7rem; color: #5a8a9a; letter-spacing: 1px; }
57
-
58
- .header-controls { display: flex; gap: 10px; align-items: center; }
59
-
60
- .toggle-btn {
61
  background: #0d1b2a; border: 1px solid #00d4ff44; color: #00d4ff;
62
- padding: 6px 14px; border-radius: 6px; cursor: pointer;
63
- font-size: 0.75rem; transition: all 0.3s; letter-spacing: 1px;
64
  }
65
- .toggle-btn:hover { background: #00d4ff22; border-color: #00d4ff88; }
66
- .toggle-btn.active { background: #00d4ff22; border-color: #00d4ff; box-shadow: 0 0 10px #00d4ff44; }
67
-
68
  .status-dot {
69
  width: 8px; height: 8px; border-radius: 50%;
70
  background: #00ff88; box-shadow: 0 0 6px #00ff88;
71
  }
72
  .status-dot.error { background: #ff4444; box-shadow: 0 0 6px #ff4444; }
73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  .chat-container {
75
- flex: 1; overflow-y: auto; padding: 20px 24px;
76
- display: flex; flex-direction: column; gap: 16px; scroll-behavior: smooth;
77
  }
78
- .chat-container::-webkit-scrollbar { width: 4px; }
79
- .chat-container::-webkit-scrollbar-track { background: transparent; }
80
  .chat-container::-webkit-scrollbar-thumb { background: #00d4ff33; border-radius: 2px; }
81
-
82
  .message {
83
- max-width: 78%; padding: 12px 18px; border-radius: 16px;
84
- font-size: 0.92rem; line-height: 1.6; animation: fadeIn 0.3s ease-out;
85
  }
86
-
87
  @keyframes fadeIn {
88
- from { opacity: 0; transform: translateY(10px); }
89
  to { opacity: 1; transform: translateY(0); }
90
  }
91
-
92
  .message.user {
93
- align-self: flex-end;
94
- background: linear-gradient(135deg, #1a3a5c, #0d2847);
95
- border: 1px solid #00d4ff33; color: #c8e6ff;
96
- border-bottom-right-radius: 4px;
97
  }
98
-
99
  .message.assistant {
100
- align-self: flex-start;
101
- background: linear-gradient(135deg, #141e30, #0f1923);
102
- border: 1px solid #00d4ff22; color: #e0e0e0;
103
- border-bottom-left-radius: 4px;
104
  }
105
-
106
- .message.assistant .label {
107
- font-size: 0.6rem; color: #00d4ff88;
108
- letter-spacing: 2px; margin-bottom: 6px; text-transform: uppercase;
109
- }
110
-
111
- .message .text-content { white-space: pre-wrap; }
112
-
113
- .message .audio-controls {
114
- margin-top: 8px; display: flex; align-items: center; gap: 8px;
115
- }
116
-
117
  .audio-btn {
118
  display: inline-flex; align-items: center; gap: 4px;
119
  background: #00d4ff15; border: 1px solid #00d4ff33; color: #00d4ff;
120
- padding: 4px 10px; border-radius: 12px; cursor: pointer;
121
- font-size: 0.7rem; transition: all 0.2s;
122
  }
123
  .audio-btn:hover { background: #00d4ff25; border-color: #00d4ff66; }
124
  .audio-btn:disabled { opacity: 0.3; cursor: wait; }
 
125
 
126
- .audio-status {
127
- font-size: 0.6rem; color: #5a8a9a; font-style: italic;
128
- }
129
-
130
- .typing-indicator {
131
- align-self: flex-start; display: flex; gap: 5px; padding: 16px 20px;
132
- }
133
  .typing-indicator span {
134
- width: 8px; height: 8px; border-radius: 50%;
135
- background: #00d4ff; animation: typing 1.4s infinite;
136
  }
137
  .typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
138
  .typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
@@ -142,58 +118,50 @@
142
  }
143
 
144
  .welcome {
145
- display: flex; flex-direction: column;
146
- align-items: center; justify-content: center;
147
- flex: 1; gap: 12px; opacity: 0.6;
148
  }
149
  .welcome .big-reactor {
150
- width: 80px; height: 80px; border-radius: 50%;
151
  background: radial-gradient(circle, #00d4ff 0%, #0088aa 35%, #004466 65%, transparent 100%);
152
- box-shadow: 0 0 40px #00d4ff66, 0 0 80px #00d4ff22;
153
- animation: pulse 2s ease-in-out infinite;
154
  }
155
- .welcome h2 { color: #00d4ff; font-size: 1.1rem; letter-spacing: 4px; }
156
- .welcome p { color: #5a8a9a; font-size: 0.8rem; }
 
157
 
158
  .input-container {
159
- padding: 16px 24px;
160
- background: linear-gradient(0deg, #0d1b2a, #0a0a1a);
161
  border-top: 1px solid #00d4ff22; flex-shrink: 0;
162
  }
163
- .input-wrapper {
164
- display: flex; gap: 10px; max-width: 900px; margin: 0 auto;
165
- }
166
  #messageInput {
167
- flex: 1; background: #0f1923; border: 1px solid #00d4ff33;
168
- border-radius: 12px; padding: 12px 18px; color: #e0e0e0;
169
- font-size: 0.92rem; outline: none; transition: border-color 0.3s;
170
- font-family: inherit;
171
  }
172
- #messageInput:focus { border-color: #00d4ff88; box-shadow: 0 0 15px #00d4ff22; }
173
  #messageInput::placeholder { color: #3a5a6a; }
174
-
175
  #sendBtn {
176
- background: linear-gradient(135deg, #00d4ff, #0088cc);
177
- border: none; border-radius: 12px; padding: 12px 24px;
178
- color: #0a0a1a; font-weight: 700; cursor: pointer;
179
- font-size: 0.85rem; letter-spacing: 1px; transition: all 0.3s;
180
- text-transform: uppercase;
181
  }
182
- #sendBtn:hover { box-shadow: 0 0 20px #00d4ff66; transform: translateY(-1px); }
183
  #sendBtn:disabled { opacity: 0.4; cursor: not-allowed; transform: none; }
184
-
185
  .input-footer {
186
- display: flex; justify-content: space-between;
187
- margin-top: 6px; max-width: 900px; margin-left: auto; margin-right: auto;
188
  }
189
- .input-footer span { font-size: 0.65rem; color: #3a5a6a; }
190
 
191
  @media (max-width: 640px) {
192
- .header { padding: 10px 14px; }
193
  .header-title h1 { font-size: 1rem; }
194
- .message { max-width: 90%; font-size: 0.85rem; }
195
- .chat-container { padding: 12px; }
196
- .input-container { padding: 12px; }
 
197
  }
198
  </style>
199
  </head>
@@ -209,8 +177,39 @@
209
  </div>
210
  <div class="header-controls">
211
  <div class="status-dot" id="statusDot"></div>
212
- <button class="toggle-btn active" id="ttsToggle" onclick="toggleTTS()">🔊 VOICE</button>
213
- <button class="toggle-btn" onclick="clearChat()">🗑 CLEAR</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  </div>
215
  </div>
216
 
@@ -219,6 +218,7 @@
219
  <div class="big-reactor"></div>
220
  <h2>SYSTEMS ONLINE</h2>
221
  <p>Type a message below to begin interaction</p>
 
222
  </div>
223
  </div>
224
 
@@ -229,26 +229,30 @@
229
  </div>
230
  <div class="input-footer">
231
  <span id="memoryCount">Memory: 0 turns</span>
232
- <span id="ttsStatus">KittenTTS · Kiki · CPU</span>
233
  </div>
234
  </div>
235
 
236
  <script>
237
- // ── STATE ──
238
  let sessionId = crypto.randomUUID ? crypto.randomUUID() : Date.now().toString(36) + Math.random().toString(36).slice(2);
239
  let ttsEnabled = true;
240
  let isProcessing = false;
241
- let messageCounter = 0;
 
242
 
243
- const chatContainer = document.getElementById('chatContainer');
244
- const messageInput = document.getElementById('messageInput');
245
  const sendBtn = document.getElementById('sendBtn');
246
  const welcome = document.getElementById('welcome');
247
 
248
- messageInput.addEventListener('keydown', (e) => {
249
  if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
250
  });
251
 
 
 
 
 
252
  function toggleTTS() {
253
  ttsEnabled = !ttsEnabled;
254
  const btn = document.getElementById('ttsToggle');
@@ -256,60 +260,58 @@
256
  btn.textContent = ttsEnabled ? '🔊 VOICE' : '🔇 MUTE';
257
  }
258
 
259
- // ── SEND MESSAGE (TWO-PHASE: text first, audio second) ──
 
 
 
 
 
 
260
  async function sendMessage() {
261
- const text = messageInput.value.trim();
262
  if (!text || isProcessing) return;
263
 
264
  if (welcome) welcome.style.display = 'none';
265
-
266
  addUserMessage(text);
267
- messageInput.value = '';
268
  isProcessing = true;
269
  sendBtn.disabled = true;
270
 
271
  const typingEl = showTyping();
272
- const msgId = ++messageCounter;
273
 
274
  try {
275
- // PHASE 1: Get text response (fast!)
276
  const res = await fetch('/chat', {
277
  method: 'POST',
278
  headers: { 'Content-Type': 'application/json' },
279
  body: JSON.stringify({ message: text, session_id: sessionId })
280
  });
281
-
282
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
283
-
284
  const data = await res.json();
285
  typingEl.remove();
286
 
287
- // Show text immediately
288
  const msgEl = addAssistantMessage(data.response, msgId);
289
-
290
  document.getElementById('memoryCount').textContent = `Memory: ${data.memory_length} turns`;
291
 
292
- // PHASE 2: Fetch TTS audio in background (non-blocking)
293
  if (ttsEnabled && data.tts_available) {
294
  fetchAndPlayAudio(data.response, msgId, msgEl);
295
  }
296
-
297
  } catch (err) {
298
  typingEl.remove();
299
- addAssistantMessage('System malfunction. Unable to process request. Please try again.', msgId);
300
- console.error('Chat error:', err);
301
  }
302
 
303
  isProcessing = false;
304
  sendBtn.disabled = false;
305
- messageInput.focus();
306
  }
307
 
308
- // ── FETCH AUDIO SEPARATELY (non-blocking) ──
309
  async function fetchAndPlayAudio(text, msgId, msgEl) {
310
  const statusEl = msgEl.querySelector('.audio-status');
311
  const playBtn = msgEl.querySelector('.audio-btn');
312
-
313
  if (statusEl) statusEl.textContent = '⏳ Generating voice...';
314
  if (playBtn) playBtn.disabled = true;
315
 
@@ -317,40 +319,29 @@
317
  const res = await fetch('/tts', {
318
  method: 'POST',
319
  headers: { 'Content-Type': 'application/json' },
320
- body: JSON.stringify({ text: text })
321
  });
322
-
323
  const data = await res.json();
324
-
325
  if (data.audio) {
326
- // Store audio data on the button
327
- if (playBtn) {
328
- playBtn.dataset.audio = data.audio;
329
- playBtn.disabled = false;
330
- playBtn.textContent = '▶ Play';
331
- }
332
  if (statusEl) statusEl.textContent = '✅ Ready';
333
-
334
- // Auto-play
335
  playAudioBase64(data.audio);
336
  } else {
337
  if (statusEl) statusEl.textContent = '⚠️ Voice unavailable';
338
  if (playBtn) playBtn.style.display = 'none';
339
  }
340
- } catch (err) {
341
- console.error('TTS error:', err);
342
  if (statusEl) statusEl.textContent = '⚠️ Voice error';
343
  if (playBtn) playBtn.style.display = 'none';
344
  }
345
  }
346
 
347
- // ── ADD MESSAGES ──
348
  function addUserMessage(text) {
349
  const div = document.createElement('div');
350
  div.className = 'message user';
351
- div.innerHTML = `<div class="text-content">${escapeHtml(text)}</div>`;
352
- chatContainer.appendChild(div);
353
- scrollToBottom();
354
  }
355
 
356
  function addAssistantMessage(text, msgId) {
@@ -359,15 +350,14 @@
359
  div.id = `msg-${msgId}`;
360
  div.innerHTML = `
361
  <div class="label">⟐ JARVIS</div>
362
- <div class="text-content">${escapeHtml(text)}</div>
363
  ${ttsEnabled ? `
364
  <div class="audio-controls">
365
  <button class="audio-btn" disabled onclick="replayAudio(this)">⏳</button>
366
  <span class="audio-status">Requesting voice...</span>
367
- </div>` : ''}
368
- `;
369
- chatContainer.appendChild(div);
370
- scrollToBottom();
371
  return div;
372
  }
373
 
@@ -375,67 +365,57 @@
375
  const div = document.createElement('div');
376
  div.className = 'typing-indicator';
377
  div.innerHTML = '<span></span><span></span><span></span>';
378
- chatContainer.appendChild(div);
379
- scrollToBottom();
380
  return div;
381
  }
382
 
383
- // ── AUDIO PLAYBACK ──
384
  function playAudioBase64(b64) {
385
  try {
386
- const binary = atob(b64);
387
- const bytes = new Uint8Array(binary.length);
388
- for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
389
  const blob = new Blob([bytes], { type: 'audio/wav' });
390
  const url = URL.createObjectURL(blob);
391
  const audio = new Audio(url);
392
  audio.play().catch(e => console.log('Autoplay blocked:', e));
393
  audio.onended = () => URL.revokeObjectURL(url);
394
- } catch (e) { console.error('Audio error:', e); }
395
  }
396
 
397
- function replayAudio(btn) {
398
- if (btn.dataset.audio) playAudioBase64(btn.dataset.audio);
399
- }
400
 
401
- // ── CLEAR ──
402
  async function clearChat() {
403
  await fetch('/clear', {
404
- method: 'POST',
405
- headers: { 'Content-Type': 'application/json' },
406
  body: JSON.stringify({ session_id: sessionId })
407
  });
408
- chatContainer.innerHTML = `
409
  <div class="welcome" id="welcome">
410
  <div class="big-reactor"></div>
411
  <h2>SYSTEMS ONLINE</h2>
412
- <p>Type a message below to begin interaction</p>
413
  </div>`;
414
  document.getElementById('memoryCount').textContent = 'Memory: 0 turns';
415
  sessionId = crypto.randomUUID ? crypto.randomUUID() : Date.now().toString(36) + Math.random().toString(36).slice(2);
416
  }
417
 
418
- // ── UTILS ──
419
- function escapeHtml(t) {
420
- const d = document.createElement('div');
421
- d.textContent = t;
422
- return d.innerHTML;
423
- }
424
- function scrollToBottom() {
425
- chatContainer.scrollTop = chatContainer.scrollHeight;
426
- }
427
 
428
- // ── HEALTH CHECK ──
429
  fetch('/health').then(r => r.json()).then(d => {
430
- const dot = document.getElementById('statusDot');
431
- const status = document.getElementById('ttsStatus');
432
- if (d.tts_model === 'DISABLED') {
433
- dot.classList.add('error');
434
- status.textContent = 'TTS Disabled · Text Only';
435
- }
 
 
436
  }).catch(() => {});
437
 
438
- messageInput.focus();
439
  </script>
440
  </body>
441
  </html>
 
6
  <title>J.A.R.V.I.S. AI</title>
7
  <style>
8
  * { margin: 0; padding: 0; box-sizing: border-box; }
 
9
  body {
10
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
11
+ background: #0a0a1a; color: #e0e0e0;
12
+ height: 100vh; display: flex; flex-direction: column; overflow: hidden;
 
 
 
 
13
  }
14
 
15
  .header {
16
  background: linear-gradient(135deg, #0d1b2a, #1b2838);
17
  border-bottom: 1px solid #00d4ff33;
18
+ padding: 12px 20px; display: flex; align-items: center;
19
+ justify-content: space-between; flex-shrink: 0;
 
 
 
20
  }
21
+ .header-left { display: flex; align-items: center; gap: 12px; }
 
 
22
  .arc-reactor {
23
+ width: 38px; height: 38px; border-radius: 50%;
24
  background: radial-gradient(circle, #00d4ff 0%, #0088aa 40%, #004466 70%, transparent 100%);
25
  box-shadow: 0 0 20px #00d4ff88, 0 0 40px #00d4ff44, inset 0 0 10px #00d4ff66;
26
+ animation: pulse 2s ease-in-out infinite; position: relative;
 
27
  }
 
28
  .arc-reactor::after {
29
  content: ''; position: absolute; top: 50%; left: 50%;
30
+ transform: translate(-50%, -50%); width: 12px; height: 12px;
31
+ border-radius: 50%; background: #00d4ff; box-shadow: 0 0 8px #00d4ff;
 
32
  }
 
33
  @keyframes pulse {
34
  0%, 100% { box-shadow: 0 0 20px #00d4ff88, 0 0 40px #00d4ff44; }
35
  50% { box-shadow: 0 0 30px #00d4ffaa, 0 0 60px #00d4ff66; }
36
  }
37
+ .header-title h1 { font-size: 1.2rem; color: #00d4ff; letter-spacing: 3px; text-transform: uppercase; }
38
+ .header-title p { font-size: 0.65rem; color: #5a8a9a; letter-spacing: 1px; }
39
 
40
+ .header-controls { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
41
+ .ctrl-btn {
 
 
 
 
 
 
 
42
  background: #0d1b2a; border: 1px solid #00d4ff44; color: #00d4ff;
43
+ padding: 5px 12px; border-radius: 6px; cursor: pointer;
44
+ font-size: 0.7rem; transition: all 0.3s; letter-spacing: 0.5px;
45
  }
46
+ .ctrl-btn:hover { background: #00d4ff22; border-color: #00d4ff88; }
47
+ .ctrl-btn.active { background: #00d4ff22; border-color: #00d4ff; box-shadow: 0 0 8px #00d4ff44; }
 
48
  .status-dot {
49
  width: 8px; height: 8px; border-radius: 50%;
50
  background: #00ff88; box-shadow: 0 0 6px #00ff88;
51
  }
52
  .status-dot.error { background: #ff4444; box-shadow: 0 0 6px #ff4444; }
53
 
54
+ /* Config panel */
55
+ .config-bar {
56
+ background: #0d1117; border-bottom: 1px solid #00d4ff15;
57
+ padding: 8px 20px; display: none; flex-wrap: wrap; gap: 12px;
58
+ align-items: center; flex-shrink: 0;
59
+ }
60
+ .config-bar.open { display: flex; }
61
+ .config-group { display: flex; align-items: center; gap: 6px; }
62
+ .config-group label { font-size: 0.65rem; color: #5a8a9a; text-transform: uppercase; letter-spacing: 1px; }
63
+ .config-group select {
64
+ background: #0f1923; border: 1px solid #00d4ff33; color: #00d4ff;
65
+ padding: 4px 8px; border-radius: 4px; font-size: 0.7rem; cursor: pointer;
66
+ }
67
+ .config-group select:focus { border-color: #00d4ff; outline: none; }
68
+ .config-tag {
69
+ font-size: 0.6rem; padding: 3px 8px; border-radius: 10px;
70
+ background: #00d4ff15; border: 1px solid #00d4ff33; color: #00d4ffaa;
71
+ }
72
+
73
+ /* Chat */
74
  .chat-container {
75
+ flex: 1; overflow-y: auto; padding: 16px 20px;
76
+ display: flex; flex-direction: column; gap: 14px; scroll-behavior: smooth;
77
  }
78
+ .chat-container::-webkit-scrollbar { width: 3px; }
 
79
  .chat-container::-webkit-scrollbar-thumb { background: #00d4ff33; border-radius: 2px; }
 
80
  .message {
81
+ max-width: 80%; padding: 12px 16px; border-radius: 14px;
82
+ font-size: 0.9rem; line-height: 1.6; animation: fadeIn 0.3s ease-out;
83
  }
 
84
  @keyframes fadeIn {
85
+ from { opacity: 0; transform: translateY(8px); }
86
  to { opacity: 1; transform: translateY(0); }
87
  }
 
88
  .message.user {
89
+ align-self: flex-end; background: linear-gradient(135deg, #1a3a5c, #0d2847);
90
+ border: 1px solid #00d4ff33; color: #c8e6ff; border-bottom-right-radius: 4px;
 
 
91
  }
 
92
  .message.assistant {
93
+ align-self: flex-start; background: linear-gradient(135deg, #141e30, #0f1923);
94
+ border: 1px solid #00d4ff22; color: #e0e0e0; border-bottom-left-radius: 4px;
 
 
95
  }
96
+ .message .label { font-size: 0.58rem; color: #00d4ff88; letter-spacing: 2px; margin-bottom: 5px; text-transform: uppercase; }
97
+ .message .text-content { white-space: pre-wrap; word-wrap: break-word; }
98
+ .message .audio-controls { margin-top: 8px; display: flex; align-items: center; gap: 8px; }
 
 
 
 
 
 
 
 
 
99
  .audio-btn {
100
  display: inline-flex; align-items: center; gap: 4px;
101
  background: #00d4ff15; border: 1px solid #00d4ff33; color: #00d4ff;
102
+ padding: 3px 10px; border-radius: 10px; cursor: pointer;
103
+ font-size: 0.65rem; transition: all 0.2s;
104
  }
105
  .audio-btn:hover { background: #00d4ff25; border-color: #00d4ff66; }
106
  .audio-btn:disabled { opacity: 0.3; cursor: wait; }
107
+ .audio-status { font-size: 0.58rem; color: #5a8a9a; }
108
 
109
+ .typing-indicator { align-self: flex-start; display: flex; gap: 5px; padding: 14px 18px; }
 
 
 
 
 
 
110
  .typing-indicator span {
111
+ width: 7px; height: 7px; border-radius: 50%; background: #00d4ff; animation: typing 1.4s infinite;
 
112
  }
113
  .typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
114
  .typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
 
118
  }
119
 
120
  .welcome {
121
+ display: flex; flex-direction: column; align-items: center;
122
+ justify-content: center; flex: 1; gap: 10px; opacity: 0.5;
 
123
  }
124
  .welcome .big-reactor {
125
+ width: 70px; height: 70px; border-radius: 50%;
126
  background: radial-gradient(circle, #00d4ff 0%, #0088aa 35%, #004466 65%, transparent 100%);
127
+ box-shadow: 0 0 40px #00d4ff66; animation: pulse 2s ease-in-out infinite;
 
128
  }
129
+ .welcome h2 { color: #00d4ff; font-size: 1rem; letter-spacing: 4px; }
130
+ .welcome p { color: #5a8a9a; font-size: 0.75rem; }
131
+ .welcome .model-info { font-size: 0.65rem; color: #3a5a6a; margin-top: 4px; }
132
 
133
  .input-container {
134
+ padding: 14px 20px; background: linear-gradient(0deg, #0d1b2a, #0a0a1a);
 
135
  border-top: 1px solid #00d4ff22; flex-shrink: 0;
136
  }
137
+ .input-wrapper { display: flex; gap: 8px; max-width: 900px; margin: 0 auto; }
 
 
138
  #messageInput {
139
+ flex: 1; background: #0f1923; border: 1px solid #00d4ff33; border-radius: 12px;
140
+ padding: 11px 16px; color: #e0e0e0; font-size: 0.9rem; outline: none;
141
+ transition: border-color 0.3s; font-family: inherit;
 
142
  }
143
+ #messageInput:focus { border-color: #00d4ff88; box-shadow: 0 0 12px #00d4ff22; }
144
  #messageInput::placeholder { color: #3a5a6a; }
 
145
  #sendBtn {
146
+ background: linear-gradient(135deg, #00d4ff, #0088cc); border: none; border-radius: 12px;
147
+ padding: 11px 22px; color: #0a0a1a; font-weight: 700; cursor: pointer;
148
+ font-size: 0.8rem; letter-spacing: 1px; transition: all 0.3s; text-transform: uppercase;
 
 
149
  }
150
+ #sendBtn:hover { box-shadow: 0 0 18px #00d4ff66; transform: translateY(-1px); }
151
  #sendBtn:disabled { opacity: 0.4; cursor: not-allowed; transform: none; }
 
152
  .input-footer {
153
+ display: flex; justify-content: space-between; margin-top: 5px;
154
+ max-width: 900px; margin-left: auto; margin-right: auto;
155
  }
156
+ .input-footer span { font-size: 0.6rem; color: #3a5a6a; }
157
 
158
  @media (max-width: 640px) {
159
+ .header { padding: 10px 12px; }
160
  .header-title h1 { font-size: 1rem; }
161
+ .message { max-width: 92%; font-size: 0.82rem; }
162
+ .chat-container { padding: 10px; }
163
+ .input-container { padding: 10px; }
164
+ .config-bar { padding: 6px 12px; }
165
  }
166
  </style>
167
  </head>
 
177
  </div>
178
  <div class="header-controls">
179
  <div class="status-dot" id="statusDot"></div>
180
+ <button class="ctrl-btn" id="configToggle" onclick="toggleConfig()"> CONFIG</button>
181
+ <button class="ctrl-btn active" id="ttsToggle" onclick="toggleTTS()">🔊 VOICE</button>
182
+ <button class="ctrl-btn" onclick="clearChat()">🗑 CLEAR</button>
183
+ </div>
184
+ </div>
185
+
186
+ <!-- Config Panel -->
187
+ <div class="config-bar" id="configPanel">
188
+ <div class="config-group">
189
+ <label>LLM:</label>
190
+ <span class="config-tag" id="llmTag">loading...</span>
191
+ </div>
192
+ <div class="config-group">
193
+ <label>TTS:</label>
194
+ <span class="config-tag" id="ttsTag">loading...</span>
195
+ </div>
196
+ <div class="config-group">
197
+ <label>Voice:</label>
198
+ <select id="voiceSelect">
199
+ <option value="Kiki">Kiki</option>
200
+ <option value="Bella">Bella</option>
201
+ <option value="Jasper">Jasper</option>
202
+ <option value="Luna">Luna</option>
203
+ <option value="Bruno">Bruno</option>
204
+ <option value="Rosie">Rosie</option>
205
+ <option value="Hugo">Hugo</option>
206
+ <option value="Leo">Leo</option>
207
+ </select>
208
+ </div>
209
+ <div class="config-group">
210
+ <label>Options (set via env vars):</label>
211
+ <span class="config-tag">LLM_MODE: gemma-3-270m-it | minilm-semantic</span>
212
+ <span class="config-tag">TTS_MODE: nano-fp32 | nano-int8 | micro | mini</span>
213
  </div>
214
  </div>
215
 
 
218
  <div class="big-reactor"></div>
219
  <h2>SYSTEMS ONLINE</h2>
220
  <p>Type a message below to begin interaction</p>
221
+ <div class="model-info" id="welcomeInfo">Initializing...</div>
222
  </div>
223
  </div>
224
 
 
229
  </div>
230
  <div class="input-footer">
231
  <span id="memoryCount">Memory: 0 turns</span>
232
+ <span id="modelInfo">Loading...</span>
233
  </div>
234
  </div>
235
 
236
  <script>
 
237
  let sessionId = crypto.randomUUID ? crypto.randomUUID() : Date.now().toString(36) + Math.random().toString(36).slice(2);
238
  let ttsEnabled = true;
239
  let isProcessing = false;
240
+ let msgCounter = 0;
241
+ let currentVoice = 'Kiki';
242
 
243
+ const chatEl = document.getElementById('chatContainer');
244
+ const inputEl = document.getElementById('messageInput');
245
  const sendBtn = document.getElementById('sendBtn');
246
  const welcome = document.getElementById('welcome');
247
 
248
+ inputEl.addEventListener('keydown', e => {
249
  if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
250
  });
251
 
252
+ document.getElementById('voiceSelect').addEventListener('change', function() {
253
+ currentVoice = this.value;
254
+ });
255
+
256
  function toggleTTS() {
257
  ttsEnabled = !ttsEnabled;
258
  const btn = document.getElementById('ttsToggle');
 
260
  btn.textContent = ttsEnabled ? '🔊 VOICE' : '🔇 MUTE';
261
  }
262
 
263
+ function toggleConfig() {
264
+ const panel = document.getElementById('configPanel');
265
+ const btn = document.getElementById('configToggle');
266
+ panel.classList.toggle('open');
267
+ btn.classList.toggle('active');
268
+ }
269
+
270
  async function sendMessage() {
271
+ const text = inputEl.value.trim();
272
  if (!text || isProcessing) return;
273
 
274
  if (welcome) welcome.style.display = 'none';
 
275
  addUserMessage(text);
276
+ inputEl.value = '';
277
  isProcessing = true;
278
  sendBtn.disabled = true;
279
 
280
  const typingEl = showTyping();
281
+ const msgId = ++msgCounter;
282
 
283
  try {
284
+ // Phase 1: Text response
285
  const res = await fetch('/chat', {
286
  method: 'POST',
287
  headers: { 'Content-Type': 'application/json' },
288
  body: JSON.stringify({ message: text, session_id: sessionId })
289
  });
 
290
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
 
291
  const data = await res.json();
292
  typingEl.remove();
293
 
 
294
  const msgEl = addAssistantMessage(data.response, msgId);
 
295
  document.getElementById('memoryCount').textContent = `Memory: ${data.memory_length} turns`;
296
 
297
+ // Phase 2: TTS async
298
  if (ttsEnabled && data.tts_available) {
299
  fetchAndPlayAudio(data.response, msgId, msgEl);
300
  }
 
301
  } catch (err) {
302
  typingEl.remove();
303
+ addAssistantMessage('System malfunction. Please try again.', msgId);
304
+ console.error(err);
305
  }
306
 
307
  isProcessing = false;
308
  sendBtn.disabled = false;
309
+ inputEl.focus();
310
  }
311
 
 
312
  async function fetchAndPlayAudio(text, msgId, msgEl) {
313
  const statusEl = msgEl.querySelector('.audio-status');
314
  const playBtn = msgEl.querySelector('.audio-btn');
 
315
  if (statusEl) statusEl.textContent = '⏳ Generating voice...';
316
  if (playBtn) playBtn.disabled = true;
317
 
 
319
  const res = await fetch('/tts', {
320
  method: 'POST',
321
  headers: { 'Content-Type': 'application/json' },
322
+ body: JSON.stringify({ text: text, voice: currentVoice })
323
  });
 
324
  const data = await res.json();
 
325
  if (data.audio) {
326
+ if (playBtn) { playBtn.dataset.audio = data.audio; playBtn.disabled = false; playBtn.textContent = '▶ Play'; }
 
 
 
 
 
327
  if (statusEl) statusEl.textContent = '✅ Ready';
 
 
328
  playAudioBase64(data.audio);
329
  } else {
330
  if (statusEl) statusEl.textContent = '⚠️ Voice unavailable';
331
  if (playBtn) playBtn.style.display = 'none';
332
  }
333
+ } catch (e) {
 
334
  if (statusEl) statusEl.textContent = '⚠️ Voice error';
335
  if (playBtn) playBtn.style.display = 'none';
336
  }
337
  }
338
 
 
339
  function addUserMessage(text) {
340
  const div = document.createElement('div');
341
  div.className = 'message user';
342
+ div.innerHTML = `<div class="text-content">${esc(text)}</div>`;
343
+ chatEl.appendChild(div);
344
+ scroll();
345
  }
346
 
347
  function addAssistantMessage(text, msgId) {
 
350
  div.id = `msg-${msgId}`;
351
  div.innerHTML = `
352
  <div class="label">⟐ JARVIS</div>
353
+ <div class="text-content">${esc(text)}</div>
354
  ${ttsEnabled ? `
355
  <div class="audio-controls">
356
  <button class="audio-btn" disabled onclick="replayAudio(this)">⏳</button>
357
  <span class="audio-status">Requesting voice...</span>
358
+ </div>` : ''}`;
359
+ chatEl.appendChild(div);
360
+ scroll();
 
361
  return div;
362
  }
363
 
 
365
  const div = document.createElement('div');
366
  div.className = 'typing-indicator';
367
  div.innerHTML = '<span></span><span></span><span></span>';
368
+ chatEl.appendChild(div);
369
+ scroll();
370
  return div;
371
  }
372
 
 
373
  function playAudioBase64(b64) {
374
  try {
375
+ const bin = atob(b64);
376
+ const bytes = new Uint8Array(bin.length);
377
+ for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
378
  const blob = new Blob([bytes], { type: 'audio/wav' });
379
  const url = URL.createObjectURL(blob);
380
  const audio = new Audio(url);
381
  audio.play().catch(e => console.log('Autoplay blocked:', e));
382
  audio.onended = () => URL.revokeObjectURL(url);
383
+ } catch (e) { console.error(e); }
384
  }
385
 
386
+ function replayAudio(btn) { if (btn.dataset.audio) playAudioBase64(btn.dataset.audio); }
 
 
387
 
 
388
  async function clearChat() {
389
  await fetch('/clear', {
390
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
 
391
  body: JSON.stringify({ session_id: sessionId })
392
  });
393
+ chatEl.innerHTML = `
394
  <div class="welcome" id="welcome">
395
  <div class="big-reactor"></div>
396
  <h2>SYSTEMS ONLINE</h2>
397
+ <p>Type a message below to begin</p>
398
  </div>`;
399
  document.getElementById('memoryCount').textContent = 'Memory: 0 turns';
400
  sessionId = crypto.randomUUID ? crypto.randomUUID() : Date.now().toString(36) + Math.random().toString(36).slice(2);
401
  }
402
 
403
+ function esc(t) { const d = document.createElement('div'); d.textContent = t; return d.innerHTML; }
404
+ function scroll() { chatEl.scrollTop = chatEl.scrollHeight; }
 
 
 
 
 
 
 
405
 
406
+ // Health check & populate config
407
  fetch('/health').then(r => r.json()).then(d => {
408
+ document.getElementById('llmTag').textContent = d.llm_mode;
409
+ document.getElementById('ttsTag').textContent = d.tts_mode + (d.tts_model === 'DISABLED' ? ' (OFF)' : '');
410
+ document.getElementById('modelInfo').textContent = `${d.llm_mode} · ${d.tts_mode} · ${d.tts_voice} · CPU`;
411
+ const wi = document.getElementById('welcomeInfo');
412
+ if (wi) wi.textContent = `LLM: ${d.llm_mode} | TTS: ${d.tts_mode} | Voice: ${d.tts_voice}`;
413
+ if (d.tts_model === 'DISABLED') document.getElementById('statusDot').classList.add('error');
414
+ // Set voice dropdown
415
+ if (d.tts_voice) { document.getElementById('voiceSelect').value = d.tts_voice; currentVoice = d.tts_voice; }
416
  }).catch(() => {});
417
 
418
+ inputEl.focus();
419
  </script>
420
  </body>
421
  </html>