OrbitMC commited on
Commit
3568f18
Β·
verified Β·
1 Parent(s): dcfaf67

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +179 -276
templates/index.html CHANGED
@@ -5,11 +5,7 @@
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>J.A.R.V.I.S. AI</title>
7
  <style>
8
- * {
9
- margin: 0;
10
- padding: 0;
11
- box-sizing: border-box;
12
- }
13
 
14
  body {
15
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
@@ -21,9 +17,8 @@
21
  overflow: hidden;
22
  }
23
 
24
- /* ── HEADER ── */
25
  .header {
26
- background: linear-gradient(135deg, #0d1b2a 0%, #1b2838 100%);
27
  border-bottom: 1px solid #00d4ff33;
28
  padding: 14px 24px;
29
  display: flex;
@@ -32,16 +27,10 @@
32
  flex-shrink: 0;
33
  }
34
 
35
- .header-left {
36
- display: flex;
37
- align-items: center;
38
- gap: 14px;
39
- }
40
 
41
  .arc-reactor {
42
- width: 42px;
43
- height: 42px;
44
- border-radius: 50%;
45
  background: radial-gradient(circle, #00d4ff 0%, #0088aa 40%, #004466 70%, transparent 100%);
46
  box-shadow: 0 0 20px #00d4ff88, 0 0 40px #00d4ff44, inset 0 0 10px #00d4ff66;
47
  animation: pulse 2s ease-in-out infinite;
@@ -49,16 +38,10 @@
49
  }
50
 
51
  .arc-reactor::after {
52
- content: '';
53
- position: absolute;
54
- top: 50%;
55
- left: 50%;
56
  transform: translate(-50%, -50%);
57
- width: 14px;
58
- height: 14px;
59
- border-radius: 50%;
60
- background: #00d4ff;
61
- box-shadow: 0 0 8px #00d4ff;
62
  }
63
 
64
  @keyframes pulse {
@@ -67,94 +50,38 @@
67
  }
68
 
69
  .header-title h1 {
70
- font-size: 1.3rem;
71
- font-weight: 600;
72
- color: #00d4ff;
73
- letter-spacing: 3px;
74
- text-transform: uppercase;
75
- }
76
-
77
- .header-title p {
78
- font-size: 0.7rem;
79
- color: #5a8a9a;
80
- letter-spacing: 1px;
81
  }
 
82
 
83
- .header-controls {
84
- display: flex;
85
- gap: 10px;
86
- align-items: center;
87
- }
88
 
89
  .toggle-btn {
90
- background: #0d1b2a;
91
- border: 1px solid #00d4ff44;
92
- color: #00d4ff;
93
- padding: 6px 14px;
94
- border-radius: 6px;
95
- cursor: pointer;
96
- font-size: 0.75rem;
97
- transition: all 0.3s;
98
- letter-spacing: 1px;
99
- }
100
-
101
- .toggle-btn:hover {
102
- background: #00d4ff22;
103
- border-color: #00d4ff88;
104
- }
105
-
106
- .toggle-btn.active {
107
- background: #00d4ff22;
108
- border-color: #00d4ff;
109
- box-shadow: 0 0 10px #00d4ff44;
110
  }
 
 
111
 
112
  .status-dot {
113
- width: 8px;
114
- height: 8px;
115
- border-radius: 50%;
116
- background: #00ff88;
117
- box-shadow: 0 0 6px #00ff88;
118
- animation: blink 3s infinite;
119
- }
120
-
121
- @keyframes blink {
122
- 0%, 90%, 100% { opacity: 1; }
123
- 95% { opacity: 0.3; }
124
  }
 
125
 
126
- /* ── CHAT AREA ── */
127
  .chat-container {
128
- flex: 1;
129
- overflow-y: auto;
130
- padding: 20px 24px;
131
- display: flex;
132
- flex-direction: column;
133
- gap: 16px;
134
- scroll-behavior: smooth;
135
- }
136
-
137
- .chat-container::-webkit-scrollbar {
138
- width: 4px;
139
- }
140
-
141
- .chat-container::-webkit-scrollbar-track {
142
- background: transparent;
143
- }
144
-
145
- .chat-container::-webkit-scrollbar-thumb {
146
- background: #00d4ff33;
147
- border-radius: 2px;
148
  }
 
 
 
149
 
150
  .message {
151
- max-width: 78%;
152
- padding: 12px 18px;
153
- border-radius: 16px;
154
- font-size: 0.92rem;
155
- line-height: 1.6;
156
- animation: fadeIn 0.3s ease-out;
157
- position: relative;
158
  }
159
 
160
  @keyframes fadeIn {
@@ -165,180 +92,102 @@
165
  .message.user {
166
  align-self: flex-end;
167
  background: linear-gradient(135deg, #1a3a5c, #0d2847);
168
- border: 1px solid #00d4ff33;
169
- color: #c8e6ff;
170
  border-bottom-right-radius: 4px;
171
  }
172
 
173
  .message.assistant {
174
  align-self: flex-start;
175
  background: linear-gradient(135deg, #141e30, #0f1923);
176
- border: 1px solid #00d4ff22;
177
- color: #e0e0e0;
178
  border-bottom-left-radius: 4px;
179
  }
180
 
181
- .message.assistant::before {
182
- content: '⟐ JARVIS';
183
- display: block;
184
- font-size: 0.6rem;
185
- color: #00d4ff88;
186
- letter-spacing: 2px;
187
- margin-bottom: 6px;
188
- text-transform: uppercase;
189
  }
190
 
191
- .message .audio-btn {
192
- display: inline-flex;
193
- align-items: center;
194
- gap: 4px;
195
- margin-top: 8px;
196
- background: #00d4ff15;
197
- border: 1px solid #00d4ff33;
198
- color: #00d4ff;
199
- padding: 4px 10px;
200
- border-radius: 12px;
201
- cursor: pointer;
202
- font-size: 0.7rem;
203
- transition: all 0.2s;
204
  }
205
 
206
- .message .audio-btn:hover {
207
- background: #00d4ff25;
208
- border-color: #00d4ff66;
 
 
209
  }
 
 
210
 
211
- .typing-indicator {
212
- align-self: flex-start;
213
- display: flex;
214
- gap: 5px;
215
- padding: 16px 20px;
216
  }
217
 
 
 
 
218
  .typing-indicator span {
219
- width: 8px;
220
- height: 8px;
221
- border-radius: 50%;
222
- background: #00d4ff;
223
- animation: typing 1.4s infinite;
224
  }
225
-
226
  .typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
227
  .typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
228
-
229
  @keyframes typing {
230
  0%, 60%, 100% { opacity: 0.2; transform: scale(0.8); }
231
  30% { opacity: 1; transform: scale(1.1); }
232
  }
233
 
234
- /* ── WELCOME ── */
235
  .welcome {
236
- display: flex;
237
- flex-direction: column;
238
- align-items: center;
239
- justify-content: center;
240
- flex: 1;
241
- gap: 12px;
242
- opacity: 0.6;
243
  }
244
-
245
  .welcome .big-reactor {
246
- width: 80px;
247
- height: 80px;
248
- border-radius: 50%;
249
  background: radial-gradient(circle, #00d4ff 0%, #0088aa 35%, #004466 65%, transparent 100%);
250
  box-shadow: 0 0 40px #00d4ff66, 0 0 80px #00d4ff22;
251
  animation: pulse 2s ease-in-out infinite;
252
  }
 
 
253
 
254
- .welcome h2 {
255
- color: #00d4ff;
256
- font-size: 1.1rem;
257
- letter-spacing: 4px;
258
- }
259
-
260
- .welcome p {
261
- color: #5a8a9a;
262
- font-size: 0.8rem;
263
- }
264
-
265
- /* ── INPUT ── */
266
  .input-container {
267
  padding: 16px 24px;
268
- background: linear-gradient(0deg, #0d1b2a 0%, #0a0a1a 100%);
269
- border-top: 1px solid #00d4ff22;
270
- flex-shrink: 0;
271
  }
272
-
273
  .input-wrapper {
274
- display: flex;
275
- gap: 10px;
276
- max-width: 900px;
277
- margin: 0 auto;
278
  }
279
-
280
  #messageInput {
281
- flex: 1;
282
- background: #0f1923;
283
- border: 1px solid #00d4ff33;
284
- border-radius: 12px;
285
- padding: 12px 18px;
286
- color: #e0e0e0;
287
- font-size: 0.92rem;
288
- outline: none;
289
- transition: border-color 0.3s;
290
  font-family: inherit;
291
  }
292
-
293
- #messageInput:focus {
294
- border-color: #00d4ff88;
295
- box-shadow: 0 0 15px #00d4ff22;
296
- }
297
-
298
- #messageInput::placeholder {
299
- color: #3a5a6a;
300
- }
301
 
302
  #sendBtn {
303
  background: linear-gradient(135deg, #00d4ff, #0088cc);
304
- border: none;
305
- border-radius: 12px;
306
- padding: 12px 24px;
307
- color: #0a0a1a;
308
- font-weight: 700;
309
- cursor: pointer;
310
- font-size: 0.85rem;
311
- letter-spacing: 1px;
312
- transition: all 0.3s;
313
  text-transform: uppercase;
314
  }
315
-
316
- #sendBtn:hover {
317
- box-shadow: 0 0 20px #00d4ff66;
318
- transform: translateY(-1px);
319
- }
320
-
321
- #sendBtn:disabled {
322
- opacity: 0.4;
323
- cursor: not-allowed;
324
- transform: none;
325
- }
326
 
327
  .input-footer {
328
- display: flex;
329
- justify-content: space-between;
330
- margin-top: 6px;
331
- max-width: 900px;
332
- margin-left: auto;
333
- margin-right: auto;
334
- }
335
-
336
- .input-footer span {
337
- font-size: 0.65rem;
338
- color: #3a5a6a;
339
  }
 
340
 
341
- /* ── RESPONSIVE ── */
342
  @media (max-width: 640px) {
343
  .header { padding: 10px 14px; }
344
  .header-title h1 { font-size: 1rem; }
@@ -350,7 +199,6 @@
350
  </head>
351
  <body>
352
 
353
- <!-- HEADER -->
354
  <div class="header">
355
  <div class="header-left">
356
  <div class="arc-reactor"></div>
@@ -360,13 +208,12 @@
360
  </div>
361
  </div>
362
  <div class="header-controls">
363
- <div class="status-dot"></div>
364
  <button class="toggle-btn active" id="ttsToggle" onclick="toggleTTS()">πŸ”Š VOICE</button>
365
  <button class="toggle-btn" onclick="clearChat()">πŸ—‘ CLEAR</button>
366
  </div>
367
  </div>
368
 
369
- <!-- CHAT -->
370
  <div class="chat-container" id="chatContainer">
371
  <div class="welcome" id="welcome">
372
  <div class="big-reactor"></div>
@@ -375,7 +222,6 @@
375
  </div>
376
  </div>
377
 
378
- <!-- INPUT -->
379
  <div class="input-container">
380
  <div class="input-wrapper">
381
  <input type="text" id="messageInput" placeholder="Talk to J.A.R.V.I.S..." autocomplete="off" />
@@ -383,7 +229,7 @@
383
  </div>
384
  <div class="input-footer">
385
  <span id="memoryCount">Memory: 0 turns</span>
386
- <span>KittenTTS Β· Kiki Β· CPU Inference</span>
387
  </div>
388
  </div>
389
 
@@ -392,21 +238,17 @@
392
  let sessionId = crypto.randomUUID ? crypto.randomUUID() : Date.now().toString(36) + Math.random().toString(36).slice(2);
393
  let ttsEnabled = true;
394
  let isProcessing = false;
 
395
 
396
  const chatContainer = document.getElementById('chatContainer');
397
  const messageInput = document.getElementById('messageInput');
398
  const sendBtn = document.getElementById('sendBtn');
399
  const welcome = document.getElementById('welcome');
400
 
401
- // ── ENTER KEY ──
402
  messageInput.addEventListener('keydown', (e) => {
403
- if (e.key === 'Enter' && !e.shiftKey) {
404
- e.preventDefault();
405
- sendMessage();
406
- }
407
  });
408
 
409
- // ── TTS TOGGLE ──
410
  function toggleTTS() {
411
  ttsEnabled = !ttsEnabled;
412
  const btn = document.getElementById('ttsToggle');
@@ -414,52 +256,48 @@
414
  btn.textContent = ttsEnabled ? 'πŸ”Š VOICE' : 'πŸ”‡ MUTE';
415
  }
416
 
417
- // ── SEND MESSAGE ──
418
  async function sendMessage() {
419
  const text = messageInput.value.trim();
420
  if (!text || isProcessing) return;
421
 
422
  if (welcome) welcome.style.display = 'none';
423
 
424
- // Add user message
425
- addMessage(text, 'user');
426
  messageInput.value = '';
427
  isProcessing = true;
428
  sendBtn.disabled = true;
429
 
430
- // Show typing indicator
431
  const typingEl = showTyping();
 
432
 
433
  try {
 
434
  const res = await fetch('/chat', {
435
  method: 'POST',
436
  headers: { 'Content-Type': 'application/json' },
437
- body: JSON.stringify({
438
- message: text,
439
- session_id: sessionId,
440
- tts: ttsEnabled
441
- })
442
  });
443
 
444
- const data = await res.json();
445
 
446
- // Remove typing indicator
447
  typingEl.remove();
448
 
449
- // Add assistant message
450
- addMessage(data.response, 'assistant', data.audio);
451
 
452
- // Update memory count
453
  document.getElementById('memoryCount').textContent = `Memory: ${data.memory_length} turns`;
454
 
455
- // Auto-play audio
456
- if (data.audio && ttsEnabled) {
457
- playAudio(data.audio);
458
  }
459
 
460
  } catch (err) {
461
  typingEl.remove();
462
- addMessage('System malfunction. Unable to process request. Please try again.', 'assistant');
 
463
  }
464
 
465
  isProcessing = false;
@@ -467,72 +305,137 @@
467
  messageInput.focus();
468
  }
469
 
470
- // ── ADD MESSAGE ──
471
- function addMessage(text, role, audioData) {
472
- const div = document.createElement('div');
473
- div.className = `message ${role}`;
 
 
 
474
 
475
- let html = text;
 
 
 
 
 
 
 
476
 
477
- if (role === 'assistant' && audioData) {
478
- html += `<br><button class="audio-btn" onclick="playAudio('${audioData}')">β–Ά Play Audio</button>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
479
  }
 
480
 
481
- div.innerHTML = html;
 
 
 
 
482
  chatContainer.appendChild(div);
483
- chatContainer.scrollTop = chatContainer.scrollHeight;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
484
  }
485
 
486
- // ── TYPING INDICATOR ──
487
  function showTyping() {
488
  const div = document.createElement('div');
489
  div.className = 'typing-indicator';
490
  div.innerHTML = '<span></span><span></span><span></span>';
491
  chatContainer.appendChild(div);
492
- chatContainer.scrollTop = chatContainer.scrollHeight;
493
  return div;
494
  }
495
 
496
- // ── PLAY AUDIO ──
497
- function playAudio(b64) {
498
  try {
499
  const binary = atob(b64);
500
  const bytes = new Uint8Array(binary.length);
501
- for (let i = 0; i < binary.length; i++) {
502
- bytes[i] = binary.charCodeAt(i);
503
- }
504
  const blob = new Blob([bytes], { type: 'audio/wav' });
505
  const url = URL.createObjectURL(blob);
506
  const audio = new Audio(url);
507
- audio.play();
508
  audio.onended = () => URL.revokeObjectURL(url);
509
- } catch (e) {
510
- console.error('Audio playback error:', e);
511
- }
 
 
512
  }
513
 
514
- // ── CLEAR CHAT ──
515
  async function clearChat() {
516
  await fetch('/clear', {
517
  method: 'POST',
518
  headers: { 'Content-Type': 'application/json' },
519
  body: JSON.stringify({ session_id: sessionId })
520
  });
521
-
522
  chatContainer.innerHTML = `
523
  <div class="welcome" id="welcome">
524
  <div class="big-reactor"></div>
525
  <h2>SYSTEMS ONLINE</h2>
526
  <p>Type a message below to begin interaction</p>
527
  </div>`;
528
-
529
  document.getElementById('memoryCount').textContent = 'Memory: 0 turns';
530
  sessionId = crypto.randomUUID ? crypto.randomUUID() : Date.now().toString(36) + Math.random().toString(36).slice(2);
531
  }
532
 
533
- // ── FOCUS INPUT ON LOAD ──
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
534
  messageInput.focus();
535
  </script>
536
-
537
  </body>
538
  </html>
 
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
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;
 
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;
 
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;
 
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 {
 
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 {
 
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; }
 
139
  @keyframes typing {
140
  0%, 60%, 100% { opacity: 0.2; transform: scale(0.8); }
141
  30% { opacity: 1; transform: scale(1.1); }
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; }
 
199
  </head>
200
  <body>
201
 
 
202
  <div class="header">
203
  <div class="header-left">
204
  <div class="arc-reactor"></div>
 
208
  </div>
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
 
 
217
  <div class="chat-container" id="chatContainer">
218
  <div class="welcome" id="welcome">
219
  <div class="big-reactor"></div>
 
222
  </div>
223
  </div>
224
 
 
225
  <div class="input-container">
226
  <div class="input-wrapper">
227
  <input type="text" id="messageInput" placeholder="Talk to J.A.R.V.I.S..." autocomplete="off" />
 
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
 
 
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
  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;
 
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
 
316
+ try {
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) {
357
+ const div = document.createElement('div');
358
+ div.className = 'message assistant';
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
 
 
374
  function showTyping() {
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>