Spaces:
OrbitMC
/
Runtime error

OrbitMC commited on
Commit
0ca87ff
·
verified ·
1 Parent(s): 0e92a36

Create templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +223 -370
templates/index.html CHANGED
@@ -3,419 +3,272 @@
3
  <head>
4
  <meta charset="UTF-8">
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
  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; }
115
- @keyframes typing {
116
- 0%, 60%, 100% { opacity: 0.2; transform: scale(0.8); }
117
- 30% { opacity: 1; transform: scale(1.1); }
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>
168
  <body>
 
 
 
 
169
 
170
- <div class="header">
171
- <div class="header-left">
172
- <div class="arc-reactor"></div>
173
- <div class="header-title">
174
- <h1>J.A.R.V.I.S.</h1>
175
- <p>Just A Rather Very Intelligent System</p>
176
  </div>
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
-
216
- <div class="chat-container" id="chatContainer">
217
- <div class="welcome" id="welcome">
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
 
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" />
228
- <button id="sendBtn" onclick="sendMessage()">SEND</button>
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');
259
- btn.classList.toggle('active', ttsEnabled);
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
-
318
- try {
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) {
348
- const div = document.createElement('div');
349
- div.className = 'message assistant';
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
-
364
- function showTyping() {
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>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>🦙 LLaMA Chat</title>
7
  <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
  body {
15
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
16
+ background: #0f0f0f;
17
+ color: #e0e0e0;
18
+ height: 100vh;
19
+ display: flex;
20
+ flex-direction: column;
21
  }
22
 
23
  .header {
24
+ padding: 16px 24px;
25
+ background: #1a1a2e;
26
+ border-bottom: 1px solid #333;
27
+ text-align: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
+ .header h1 {
31
+ font-size: 1.4rem;
32
+ color: #a78bfa;
 
 
33
  }
34
+
35
+ .header p {
36
+ font-size: 0.8rem;
37
+ color: #888;
38
+ margin-top: 4px;
 
 
 
 
 
 
39
  }
40
 
 
41
  .chat-container {
42
+ flex: 1;
43
+ overflow-y: auto;
44
+ padding: 20px 24px;
45
+ display: flex;
46
+ flex-direction: column;
47
+ gap: 16px;
48
  }
49
+
 
50
  .message {
51
+ max-width: 75%;
52
+ padding: 12px 16px;
53
+ border-radius: 16px;
54
+ line-height: 1.5;
55
+ font-size: 0.95rem;
56
+ white-space: pre-wrap;
57
+ word-wrap: break-word;
58
  }
59
+
60
  .message.user {
61
+ align-self: flex-end;
62
+ background: #4338ca;
63
+ color: #fff;
64
+ border-bottom-right-radius: 4px;
65
  }
66
+
67
  .message.assistant {
68
+ align-self: flex-start;
69
+ background: #1e1e2e;
70
+ color: #d4d4d4;
71
+ border: 1px solid #333;
72
+ border-bottom-left-radius: 4px;
 
 
 
 
 
 
73
  }
 
 
 
74
 
75
+ .message.assistant .typing-cursor {
76
+ display: inline-block;
77
+ width: 8px;
78
+ height: 16px;
79
+ background: #a78bfa;
80
+ animation: blink 0.7s infinite;
81
+ vertical-align: text-bottom;
82
+ margin-left: 2px;
83
  }
84
+
85
+ @keyframes blink {
86
+ 0%, 100% { opacity: 1; }
87
+ 50% { opacity: 0; }
 
88
  }
89
 
90
+ .input-area {
91
+ padding: 16px 24px;
92
+ background: #1a1a2e;
93
+ border-top: 1px solid #333;
94
+ display: flex;
95
+ gap: 12px;
96
  }
97
+
98
+ .input-area textarea {
99
+ flex: 1;
100
+ padding: 12px 16px;
101
+ border: 1px solid #444;
102
+ border-radius: 12px;
103
+ background: #0f0f0f;
104
+ color: #e0e0e0;
105
+ font-size: 0.95rem;
106
+ font-family: inherit;
107
+ resize: none;
108
+ outline: none;
109
+ max-height: 120px;
110
  }
 
 
 
111
 
112
+ .input-area textarea:focus {
113
+ border-color: #a78bfa;
 
114
  }
115
+
116
+ .input-area button {
117
+ padding: 12px 24px;
118
+ background: #7c3aed;
119
+ color: #fff;
120
+ border: none;
121
+ border-radius: 12px;
122
+ font-size: 0.95rem;
123
+ cursor: pointer;
124
+ transition: background 0.2s;
125
  }
126
+
127
+ .input-area button:hover {
128
+ background: #6d28d9;
 
 
 
129
  }
130
+
131
+ .input-area button:disabled {
132
+ background: #444;
133
+ cursor: not-allowed;
 
134
  }
135
+
136
+ .status {
137
+ text-align: center;
138
+ padding: 40px;
139
+ color: #888;
 
 
 
 
140
  }
141
  </style>
142
  </head>
143
  <body>
144
+ <div class="header">
145
+ <h1>🦙 LLaMA Chat</h1>
146
+ <p>SmolLM2 1.7B Instruct • CPU • GGUF</p>
147
+ </div>
148
 
149
+ <div class="chat-container" id="chatContainer">
150
+ <div class="status" id="welcome">
151
+ Type a message below to start chatting!
 
 
 
152
  </div>
153
  </div>
 
 
 
 
 
 
 
154
 
155
+ <div class="input-area">
156
+ <textarea
157
+ id="userInput"
158
+ rows="1"
159
+ placeholder="Type your message..."
160
+ onkeydown="handleKey(event)"
161
+ ></textarea>
162
+ <button id="sendBtn" onclick="sendMessage()">Send</button>
 
163
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
 
165
+ <script>
166
+ const chatContainer = document.getElementById('chatContainer');
167
+ const userInput = document.getElementById('userInput');
168
+ const sendBtn = document.getElementById('sendBtn');
169
+ const welcome = document.getElementById('welcome');
170
+ let isGenerating = false;
171
+
172
+ // Auto-resize textarea
173
+ userInput.addEventListener('input', function () {
174
+ this.style.height = 'auto';
175
+ this.style.height = Math.min(this.scrollHeight, 120) + 'px';
176
+ });
177
+
178
+ function handleKey(e) {
179
+ if (e.key === 'Enter' && !e.shiftKey) {
180
+ e.preventDefault();
181
+ sendMessage();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  }
 
 
 
 
183
  }
184
 
185
+ function addMessage(role, text) {
186
+ if (welcome) welcome.remove();
187
+
188
+ const div = document.createElement('div');
189
+ div.className = `message ${role}`;
190
+ div.textContent = text;
191
+ chatContainer.appendChild(div);
192
+ chatContainer.scrollTop = chatContainer.scrollHeight;
193
+ return div;
194
+ }
195
+
196
+ async function sendMessage() {
197
+ const msg = userInput.value.trim();
198
+ if (!msg || isGenerating) return;
199
+
200
+ isGenerating = true;
201
+ sendBtn.disabled = true;
202
+ userInput.value = '';
203
+ userInput.style.height = 'auto';
204
+
205
+ addMessage('user', msg);
206
+
207
+ // Create assistant bubble with cursor
208
+ if (welcome) welcome.remove();
209
+ const assistantDiv = document.createElement('div');
210
+ assistantDiv.className = 'message assistant';
211
+ const cursor = document.createElement('span');
212
+ cursor.className = 'typing-cursor';
213
+ assistantDiv.appendChild(cursor);
214
+ chatContainer.appendChild(assistantDiv);
215
+ chatContainer.scrollTop = chatContainer.scrollHeight;
216
+
217
+ try {
218
+ const response = await fetch('/chat/stream', {
219
+ method: 'POST',
220
+ headers: { 'Content-Type': 'application/json' },
221
+ body: JSON.stringify({ message: msg }),
222
+ });
223
+
224
+ const reader = response.body.getReader();
225
+ const decoder = new TextDecoder();
226
+ let fullText = '';
227
+ let buffer = '';
228
+
229
+ while (true) {
230
+ const { done, value } = await reader.read();
231
+ if (done) break;
232
+
233
+ buffer += decoder.decode(value, { stream: true });
234
+ const lines = buffer.split('\n');
235
+ buffer = lines.pop(); // keep incomplete line
236
+
237
+ for (const line of lines) {
238
+ if (line.startsWith('data: ')) {
239
+ const data = line.slice(6).trim();
240
+ if (data === '[DONE]') continue;
241
+ try {
242
+ const parsed = JSON.parse(data);
243
+ if (parsed.content) {
244
+ fullText += parsed.content;
245
+ assistantDiv.textContent = fullText;
246
+ // Re-add cursor
247
+ const newCursor = document.createElement('span');
248
+ newCursor.className = 'typing-cursor';
249
+ assistantDiv.appendChild(newCursor);
250
+ chatContainer.scrollTop = chatContainer.scrollHeight;
251
+ }
252
+ if (parsed.error) {
253
+ fullText += `\n[Error: ${parsed.error}]`;
254
+ assistantDiv.textContent = fullText;
255
+ }
256
+ } catch (e) { }
257
+ }
258
+ }
259
+ }
260
+
261
+ // Remove cursor when done
262
+ assistantDiv.textContent = fullText || '(No response)';
263
+
264
+ } catch (err) {
265
+ assistantDiv.textContent = `Error: ${err.message}`;
266
  }
267
+
268
+ isGenerating = false;
269
+ sendBtn.disabled = false;
270
+ userInput.focus();
271
  }
272
+ </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  </body>
274
  </html>