Xenova HF Staff commited on
Commit
91c9bfd
·
verified ·
1 Parent(s): 2d701af

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +710 -19
index.html CHANGED
@@ -1,19 +1,710 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>NanoChat WebGPU</title>
8
+ <link rel="icon"
9
+ href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>⚡</text></svg>">
10
+ <style>
11
+ :root {
12
+ color-scheme: light;
13
+ }
14
+
15
+ * {
16
+ box-sizing: border-box;
17
+ }
18
+
19
+ body {
20
+ font-family: ui-sans-serif, -apple-system, system-ui, "Segoe UI", Helvetica, "Apple Color Emoji", Arial, sans-serif, "Segoe UI Emoji", "Segoe UI Symbol";
21
+ background-color: #ffffff;
22
+ color: #111827;
23
+ min-height: 100vh;
24
+ margin: 0;
25
+ display: flex;
26
+ flex-direction: column;
27
+ max-height: 100vh;
28
+ }
29
+
30
+ /* Loader Styles */
31
+ .loader-overlay {
32
+ position: fixed;
33
+ inset: 0;
34
+ background-color: rgba(255, 255, 255, 0.8);
35
+ backdrop-filter: blur(8px);
36
+ display: flex;
37
+ flex-direction: column;
38
+ align-items: center;
39
+ justify-content: center;
40
+ z-index: 1000;
41
+ color: #374151;
42
+ }
43
+
44
+ .loader {
45
+ border: 5px solid #f3f3f3;
46
+ border-top: 5px solid #3498db;
47
+ border-radius: 50%;
48
+ width: 50px;
49
+ height: 50px;
50
+ animation: spin 1s linear infinite;
51
+ }
52
+
53
+ #loader-text {
54
+ margin: 1rem 0 0.5rem 0;
55
+ font-weight: 500;
56
+ }
57
+
58
+ @keyframes spin {
59
+ 0% {
60
+ transform: rotate(0deg);
61
+ }
62
+
63
+ 100% {
64
+ transform: rotate(360deg);
65
+ }
66
+ }
67
+
68
+ .header {
69
+ background-color: #ffffff;
70
+ padding: 1.25rem 1.5rem;
71
+ }
72
+
73
+ .header-left {
74
+ display: flex;
75
+ align-items: center;
76
+ gap: 0.75rem;
77
+ }
78
+
79
+ .header-logo {
80
+ height: 32px;
81
+ width: auto;
82
+ }
83
+
84
+ .header h1 {
85
+ font-size: 1.25rem;
86
+ font-weight: 600;
87
+ margin: 0;
88
+ color: #111827;
89
+ }
90
+
91
+ .new-conversation-btn {
92
+ width: 32px;
93
+ height: 32px;
94
+ padding: 0;
95
+ border: 1px solid #e5e7eb;
96
+ border-radius: 0.5rem;
97
+ background-color: #ffffff;
98
+ color: #6b7280;
99
+ cursor: pointer;
100
+ display: flex;
101
+ align-items: center;
102
+ justify-content: center;
103
+ transition: all 0.2s ease;
104
+ }
105
+
106
+ .new-conversation-btn:hover {
107
+ background-color: #f3f4f6;
108
+ border-color: #d1d5db;
109
+ color: #374151;
110
+ }
111
+
112
+ .chat-container {
113
+ flex: 1;
114
+ overflow-y: auto;
115
+ background-color: #ffffff;
116
+ }
117
+
118
+ .chat-wrapper {
119
+ max-width: 48rem;
120
+ margin: 0 auto;
121
+ padding: 2rem 1.5rem 3rem;
122
+ display: flex;
123
+ flex-direction: column;
124
+ gap: 0.75rem;
125
+ }
126
+
127
+ .message {
128
+ display: flex;
129
+ justify-content: flex-start;
130
+ margin-bottom: 0.5rem;
131
+ color: #0d0d0d;
132
+ }
133
+
134
+ .message.assistant {
135
+ justify-content: flex-start;
136
+ }
137
+
138
+ .message.user {
139
+ justify-content: flex-end;
140
+ }
141
+
142
+ .message-content {
143
+ white-space: pre-wrap;
144
+ line-height: 1.6;
145
+ max-width: 100%;
146
+ }
147
+
148
+ .message.assistant .message-content {
149
+ background: transparent;
150
+ border: none;
151
+ padding: 0.25rem 0;
152
+ cursor: pointer;
153
+ border-radius: 0.5rem;
154
+ padding: 0.5rem;
155
+ margin-left: -0.5rem;
156
+ transition: background-color 0.2s ease;
157
+ }
158
+
159
+ .message.assistant .message-content:hover {
160
+ background-color: #f9fafb;
161
+ }
162
+
163
+ .message.user .message-content {
164
+ background-color: #f3f4f6;
165
+ border-radius: 1.25rem;
166
+ padding: 0.8rem 1rem;
167
+ max-width: 65%;
168
+ cursor: pointer;
169
+ transition: background-color 0.2s ease;
170
+ }
171
+
172
+ .message.user .message-content:hover {
173
+ background-color: #e5e7eb;
174
+ }
175
+
176
+ .message.console .message-content {
177
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'Courier New', monospace;
178
+ font-size: 0.875rem;
179
+ background-color: #fafafa;
180
+ padding: 0.75rem 1rem;
181
+ color: #374151;
182
+ max-width: 80%;
183
+ }
184
+
185
+ .input-container {
186
+ background-color: #ffffff;
187
+ padding: 1rem;
188
+ }
189
+
190
+ .input-wrapper {
191
+ max-width: 48rem;
192
+ margin: 0 auto;
193
+ display: flex;
194
+ gap: 0.75rem;
195
+ align-items: flex-end;
196
+ }
197
+
198
+ .chat-input {
199
+ flex: 1;
200
+ padding: 0.8rem 1rem;
201
+ border: 1px solid #d1d5db;
202
+ border-radius: 0.75rem;
203
+ background-color: #ffffff;
204
+ color: #111827;
205
+ font-size: 1rem;
206
+ line-height: 1.5;
207
+ resize: none;
208
+ outline: none;
209
+ min-height: 54px;
210
+ max-height: 200px;
211
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
212
+ }
213
+
214
+ .chat-input::placeholder {
215
+ color: #9ca3af;
216
+ }
217
+
218
+ .chat-input:focus {
219
+ border-color: #2563eb;
220
+ box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
221
+ }
222
+
223
+ .send-button {
224
+ flex-shrink: 0;
225
+ padding: 0;
226
+ width: 54px;
227
+ height: 54px;
228
+ border: 1px solid #111827;
229
+ border-radius: 0.75rem;
230
+ background-color: #111827;
231
+ color: #ffffff;
232
+ display: flex;
233
+ align-items: center;
234
+ justify-content: center;
235
+ cursor: pointer;
236
+ transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease;
237
+ }
238
+
239
+ .send-button:hover:not(:disabled) {
240
+ background-color: #2563eb;
241
+ border-color: #2563eb;
242
+ }
243
+
244
+ .send-button:disabled {
245
+ cursor: not-allowed;
246
+ border-color: #d1d5db;
247
+ background-color: #e5e7eb;
248
+ color: #9ca3af;
249
+ }
250
+
251
+ .error-message {
252
+ background-color: #fee2e2;
253
+ border: 1px solid #fecaca;
254
+ color: #b91c1c;
255
+ padding: 0.75rem 1rem;
256
+ border-radius: 0.75rem;
257
+ margin-top: 0.5rem;
258
+ }
259
+
260
+ .progress {
261
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'Courier New', monospace;
262
+ text-align: center;
263
+ }
264
+ </style>
265
+ </head>
266
+
267
+ <body>
268
+ <div id="loader" class="loader-overlay">
269
+ <div class="loader"></div>
270
+ <p id="loader-text">Loading model...</p>
271
+ <span class="progress">(0.00%)</span>
272
+ </div>
273
+
274
+ <div class="header">
275
+ <div class="header-left">
276
+ <button id="newConversationBtn" class="new-conversation-btn" title="New Conversation (Ctrl+Shift+N)">
277
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
278
+ stroke-linecap="round" stroke-linejoin="round">
279
+ <path d="M12 5v14"></path>
280
+ <path d="M5 12h14"></path>
281
+ </svg>
282
+ </button>
283
+ <h1>nanochat webgpu</h1>
284
+ </div>
285
+ </div>
286
+
287
+ <div class="chat-container" id="chatContainer">
288
+ <div class="chat-wrapper" id="chatWrapper">
289
+ </div>
290
+ </div>
291
+
292
+ <div class="input-container">
293
+ <div class="input-wrapper">
294
+ <textarea id="chatInput" class="chat-input" placeholder="Ask anything" rows="1" disabled></textarea>
295
+ <button id="sendButton" class="send-button" disabled>
296
+ <svg data-icon="send" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor"
297
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
298
+ <path d="M22 2L11 13"></path>
299
+ <path d="M22 2l-7 20-4-9-9-4 20-7z"></path>
300
+ </svg>
301
+ <svg data-icon="stop" width="22" height="22" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor"
302
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display: none;">
303
+ <rect x="6" y="6" width="12" height="12"></rect>
304
+ </svg>
305
+ </button>
306
+ </div>
307
+ </div>
308
+
309
+ <script type="module">
310
+ import { pipeline, TextStreamer, InterruptableStoppingCriteria } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.7.6';
311
+
312
+ /**
313
+ * Encapsulates the entire chat application logic.
314
+ */
315
+ class ChatApp {
316
+ constructor() {
317
+ // Application State
318
+ this.generator = null;
319
+ this.messages = [];
320
+ this.isGenerating = false;
321
+ this.stoppingCriteria = new InterruptableStoppingCriteria();
322
+
323
+ // Generation Settings
324
+ this.settings = {
325
+ temperature: 0.05,
326
+ topK: 5,
327
+ };
328
+ this.fileProgress = new Map();
329
+
330
+ // UI Element References
331
+ this.ui = {};
332
+
333
+ this.cacheDOMElements();
334
+ this.bindEventListeners();
335
+ this.initialize();
336
+ }
337
+
338
+ /**
339
+ * Caches all required DOM elements for easy access.
340
+ */
341
+ cacheDOMElements() {
342
+ this.ui.chatContainer = document.getElementById('chatContainer');
343
+ this.ui.chatWrapper = document.getElementById('chatWrapper');
344
+ this.ui.chatInput = document.getElementById('chatInput');
345
+ this.ui.sendButton = document.getElementById('sendButton');
346
+ this.ui.newConversationBtn = document.getElementById('newConversationBtn');
347
+ this.ui.loader = document.getElementById('loader');
348
+ this.ui.loaderText = document.getElementById('loader-text');
349
+ this.ui.progressSpan = this.ui.loader.querySelector('.progress');
350
+ this.ui.sendIcon = this.ui.sendButton.querySelector('[data-icon="send"]');
351
+ this.ui.stopIcon = this.ui.sendButton.querySelector('[data-icon="stop"]');
352
+ }
353
+
354
+ /**
355
+ * Binds all event listeners for user interactions.
356
+ */
357
+ bindEventListeners() {
358
+ this.ui.sendButton.addEventListener('click', () => this.handlePrimaryButtonClick());
359
+ this.ui.newConversationBtn.addEventListener('click', () => this.handleNewConversation());
360
+
361
+ this.ui.chatInput.addEventListener('keydown', (event) => {
362
+ if (event.key === 'Enter' && !event.shiftKey) {
363
+ event.preventDefault();
364
+ this.handlePrimaryButtonClick();
365
+ }
366
+ });
367
+
368
+ this.ui.chatInput.addEventListener('input', () => {
369
+ // Auto-resize textarea
370
+ this.ui.chatInput.style.height = 'auto';
371
+ this.ui.chatInput.style.height = Math.min(this.ui.chatInput.scrollHeight, 200) + 'px';
372
+ this.updateSendButtonState();
373
+ });
374
+
375
+ document.addEventListener('keydown', (event) => {
376
+ if (event.ctrlKey && event.shiftKey && event.key === 'N') {
377
+ event.preventDefault();
378
+ if (!this.isGenerating) {
379
+ this.handleNewConversation();
380
+ }
381
+ }
382
+ });
383
+ }
384
+
385
+ /**
386
+ * Starts the application by setting the initial state and loading the model.
387
+ */
388
+ initialize() {
389
+ this.messages.push({ role: 'system', content: 'You are a helpful assistant.' });
390
+ this.initializeModel();
391
+ }
392
+
393
+ /**
394
+ * Loads the text-generation model using the transformers.js pipeline.
395
+ */
396
+ async initializeModel() {
397
+ try {
398
+ this.fileProgress.clear();
399
+ this.generator = await pipeline(
400
+ "text-generation",
401
+ "onnx-community/nanochat-d32-ONNX",
402
+ {
403
+ dtype: "q4",
404
+ device: "webgpu",
405
+ progress_callback: (data) => {
406
+ if (data.status !== 'progress') return;
407
+ this.fileProgress.set(data.file, { loaded: data.loaded, total: data.total });
408
+ if (this.fileProgress.size >= 7) {
409
+ const aggregate = [...this.fileProgress.values()].reduce((acc, { loaded, total }) => {
410
+ acc.loaded += loaded;
411
+ acc.total += total;
412
+ return acc;
413
+ }, { loaded: 0, total: 0 });
414
+ const percent = aggregate.total > 0 ? ((aggregate.loaded / aggregate.total) * 100).toFixed(2) : '0.00';
415
+ this.ui.progressSpan.textContent = `(${percent}%)`;
416
+ }
417
+ }
418
+ }
419
+ );
420
+ this.ui.loader.style.display = 'none';
421
+ this.ui.chatInput.disabled = false;
422
+ this.ui.chatInput.focus();
423
+ } catch (error) {
424
+ console.error("Error initializing model:", error);
425
+ this.ui.loaderText.textContent = `Error loading model: ${error.message}.`;
426
+ const loaderSpinner = this.ui.loader.querySelector('.loader');
427
+ if (loaderSpinner) loaderSpinner.style.display = 'none';
428
+ }
429
+ }
430
+
431
+ /**
432
+ * Handles the primary button click, which can be either "Send" or "Stop".
433
+ */
434
+ handlePrimaryButtonClick() {
435
+ if (this.isGenerating) {
436
+ this.handleStopGeneration();
437
+ } else {
438
+ this.handleSendMessage();
439
+ }
440
+ }
441
+
442
+ /**
443
+ * Interrupts the current text generation.
444
+ */
445
+ handleStopGeneration() {
446
+ this.stoppingCriteria.interrupt();
447
+ }
448
+
449
+ /**
450
+ * Handles sending a message, including slash commands.
451
+ */
452
+ handleSendMessage() {
453
+ const message = this.ui.chatInput.value.trim();
454
+ if (!message || this.isGenerating) return;
455
+
456
+ if (message.startsWith('/')) {
457
+ if (!this.handleSlashCommand(message)) {
458
+ this.renderMessage('console', `Unknown command: ${message}`);
459
+ }
460
+ } else {
461
+ const userMessageIndex = this.messages.length;
462
+ this.messages.push({ role: 'user', content: message });
463
+ this.renderMessage('user', message, userMessageIndex);
464
+ this.generateAssistantResponse();
465
+ }
466
+
467
+ this.ui.chatInput.value = '';
468
+ this.ui.chatInput.style.height = 'auto';
469
+ this.updateSendButtonState();
470
+ }
471
+
472
+ /**
473
+ * Handles the regeneration of an assistant's response.
474
+ * @param {number} messageIndex - The index of the message to regenerate.
475
+ */
476
+ handleRegenerate(messageIndex) {
477
+ if (this.isGenerating || messageIndex < 1 || messageIndex >= this.messages.length) return;
478
+ const messageToRegenerate = this.messages[messageIndex];
479
+ if (messageToRegenerate.role !== 'assistant') return;
480
+
481
+ this.messages = this.messages.slice(0, messageIndex);
482
+
483
+ const allMessages = this.ui.chatWrapper.querySelectorAll('.message');
484
+ // DOM has one less message than the array (no system prompt)
485
+ for (let i = messageIndex - 1; i < allMessages.length; i++) {
486
+ allMessages[i].remove();
487
+ }
488
+
489
+ this.generateAssistantResponse();
490
+ }
491
+
492
+ /**
493
+ * Allows editing a user message and continuing the conversation from there.
494
+ * @param {number} messageIndex - The index of the user message to edit.
495
+ */
496
+ handleEdit(messageIndex) {
497
+ if (this.isGenerating || messageIndex < 1 || messageIndex >= this.messages.length) return;
498
+ const messageToEdit = this.messages[messageIndex];
499
+ if (messageToEdit.role !== 'user') return;
500
+
501
+ this.ui.chatInput.value = messageToEdit.content;
502
+ this.ui.chatInput.style.height = 'auto';
503
+ this.ui.chatInput.style.height = Math.min(this.ui.chatInput.scrollHeight, 200) + 'px';
504
+
505
+ this.messages = this.messages.slice(0, messageIndex);
506
+
507
+ const allMessages = this.ui.chatWrapper.querySelectorAll('.message');
508
+ for (let i = messageIndex - 1; i < allMessages.length; i++) {
509
+ allMessages[i].remove();
510
+ }
511
+
512
+ this.updateSendButtonState();
513
+ this.ui.chatInput.focus();
514
+ }
515
+
516
+ /**
517
+ * Clears the conversation and resets the state.
518
+ */
519
+ handleNewConversation() {
520
+ if (this.isGenerating) return;
521
+ this.messages = [{ role: 'system', content: 'You are a helpful assistant.' }];
522
+ this.ui.chatWrapper.innerHTML = '';
523
+ this.ui.chatInput.value = '';
524
+ this.ui.chatInput.style.height = 'auto';
525
+ this.updateSendButtonState();
526
+ this.ui.chatInput.focus();
527
+ }
528
+
529
+ /**
530
+ * Parses and executes slash commands.
531
+ * @param {string} command - The full command string from the user.
532
+ * @returns {boolean} - True if the command was valid, false otherwise.
533
+ */
534
+ handleSlashCommand(command) {
535
+ const parts = command.trim().split(/\s+/);
536
+ const cmd = parts[0].toLowerCase();
537
+ const arg = parts[1];
538
+
539
+ switch (cmd) {
540
+ case '/temperature':
541
+ if (arg === undefined) {
542
+ this.renderMessage('console', `Current temperature: ${this.settings.temperature}`);
543
+ } else {
544
+ const temp = parseFloat(arg);
545
+ if (isNaN(temp) || temp < 0 || temp > 2) {
546
+ this.renderMessage('console', 'Invalid temperature. Must be between 0.0 and 2.0');
547
+ } else {
548
+ this.settings.temperature = temp;
549
+ this.renderMessage('console', `Temperature set to ${this.settings.temperature}`);
550
+ }
551
+ }
552
+ return true;
553
+ case '/topk':
554
+ if (arg === undefined) {
555
+ this.renderMessage('console', `Current top-k: ${this.settings.topK}`);
556
+ } else {
557
+ const topk = parseInt(arg);
558
+ if (isNaN(topk) || topk < 1 || topk > 200) {
559
+ this.renderMessage('console', 'Invalid top-k. Must be between 1 and 200');
560
+ } else {
561
+ this.settings.topK = topk;
562
+ this.renderMessage('console', `Top-k set to ${this.settings.topK}`);
563
+ }
564
+ }
565
+ return true;
566
+ case '/clear':
567
+ this.handleNewConversation();
568
+ return true;
569
+ case '/help':
570
+ this.renderMessage('console',
571
+ 'Available commands:\n' +
572
+ '/temperature - Show current temperature\n' +
573
+ '/temperature <value> - Set temperature (0.0-2.0)\n' +
574
+ '/topk - Show current top-k\n' +
575
+ '/topk <value> - Set top-k (1-200)\n' +
576
+ '/clear - Clear conversation\n' +
577
+ '/help - Show this help message'
578
+ );
579
+ return true;
580
+ default:
581
+ return false;
582
+ }
583
+ }
584
+
585
+ /**
586
+ * Generates a response from the model and streams it to the UI.
587
+ */
588
+ async generateAssistantResponse() {
589
+ this.stoppingCriteria.reset();
590
+ this.isGenerating = true;
591
+ this.setButtonToStop();
592
+
593
+ const assistantContentDiv = this.renderMessage('assistant', '');
594
+ let fullResponse = '';
595
+
596
+ const streamer = new TextStreamer(this.generator.tokenizer, {
597
+ skip_prompt: true,
598
+ skip_special_tokens: true,
599
+ callback_function: (token) => {
600
+ fullResponse += token;
601
+ assistantContentDiv.textContent = fullResponse;
602
+ this.ui.chatContainer.scrollTop = this.ui.chatContainer.scrollHeight;
603
+ }
604
+ });
605
+
606
+ try {
607
+ await this.generator(this.messages, {
608
+ max_new_tokens: 512,
609
+ do_sample: this.settings.temperature > 0,
610
+ temperature: this.settings.temperature,
611
+ top_k: this.settings.topK,
612
+ repetition_penalty: 1.2,
613
+ streamer,
614
+ stopping_criteria: this.stoppingCriteria,
615
+ });
616
+
617
+ const assistantMessageIndex = this.messages.length;
618
+ this.messages.push({ role: 'assistant', content: fullResponse });
619
+
620
+ // Add click handler for regeneration after the message is complete
621
+ assistantContentDiv.setAttribute('data-message-index', assistantMessageIndex);
622
+ assistantContentDiv.setAttribute('title', 'Click to regenerate this response');
623
+ assistantContentDiv.addEventListener('click', () => this.handleRegenerate(assistantMessageIndex));
624
+ } catch (error) {
625
+ // Don't show an error if it was a user interruption
626
+ if (!error.message.includes('interrupted')) {
627
+ console.error('Error during generation:', error);
628
+ assistantContentDiv.textContent = '';
629
+ const errorDiv = document.createElement('div');
630
+ errorDiv.className = 'error-message';
631
+ errorDiv.textContent = `Error: ${error.message}`;
632
+ assistantContentDiv.appendChild(errorDiv);
633
+ } else {
634
+ // If interrupted, save the partial response
635
+ const assistantMessageIndex = this.messages.length;
636
+ this.messages.push({ role: 'assistant', content: fullResponse });
637
+ assistantContentDiv.setAttribute('data-message-index', assistantMessageIndex);
638
+ assistantContentDiv.setAttribute('title', 'Click to regenerate this response');
639
+ assistantContentDiv.addEventListener('click', () => this.handleRegenerate(assistantMessageIndex));
640
+ }
641
+ } finally {
642
+ this.isGenerating = false;
643
+ this.setButtonToSend();
644
+ this.ui.chatInput.focus();
645
+ }
646
+ }
647
+
648
+ /**
649
+ * Renders a message in the chat window.
650
+ * @param {string} role - 'user', 'assistant', or 'console'.
651
+ * @param {string} content - The message text.
652
+ * @param {number|null} messageIndex - The index for user/assistant messages for edit/regenerate.
653
+ * @returns {HTMLElement} - The content div of the new message.
654
+ */
655
+ renderMessage(role, content, messageIndex = null) {
656
+ const messageDiv = document.createElement('div');
657
+ messageDiv.className = `message ${role}`;
658
+
659
+ const contentDiv = document.createElement('div');
660
+ contentDiv.className = 'message-content';
661
+ contentDiv.textContent = content;
662
+
663
+ if (role === 'user' && messageIndex !== null) {
664
+ contentDiv.setAttribute('data-message-index', messageIndex);
665
+ contentDiv.setAttribute('title', 'Click to edit and restart from here');
666
+ contentDiv.addEventListener('click', () => this.handleEdit(messageIndex));
667
+ }
668
+
669
+ messageDiv.appendChild(contentDiv);
670
+ this.ui.chatWrapper.appendChild(messageDiv);
671
+
672
+ this.ui.chatContainer.scrollTop = this.ui.chatContainer.scrollHeight;
673
+ return contentDiv;
674
+ }
675
+
676
+ /**
677
+ * Updates the enabled/disabled state of the send button.
678
+ */
679
+ updateSendButtonState() {
680
+ this.ui.sendButton.disabled = !this.isGenerating && !this.ui.chatInput.value.trim();
681
+ }
682
+
683
+ /**
684
+ * Changes the primary button to its "Stop" state.
685
+ */
686
+ setButtonToStop() {
687
+ this.ui.sendIcon.style.display = 'none';
688
+ this.ui.stopIcon.style.display = '';
689
+ this.ui.sendButton.disabled = false;
690
+ this.ui.sendButton.title = 'Stop generating';
691
+ }
692
+
693
+ /**
694
+ * Changes the primary button back to its "Send" state.
695
+ */
696
+ setButtonToSend() {
697
+ this.ui.sendIcon.style.display = '';
698
+ this.ui.stopIcon.style.display = 'none';
699
+ this.ui.sendButton.title = '';
700
+ this.updateSendButtonState();
701
+ }
702
+ }
703
+
704
+ // Initialize the application once the DOM is ready.
705
+ new ChatApp();
706
+
707
+ </script>
708
+ </body>
709
+
710
+ </html>