mihailik commited on
Commit
f85c67a
·
1 Parent(s): 07734b3

Patches and fixes.

Browse files
Files changed (2) hide show
  1. chat-copil2-web.html +717 -0
  2. chat-full.html +1 -1
chat-copil2-web.html ADDED
@@ -0,0 +1,717 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ <!doctype html>
3
+ <html lang="en">
4
+
5
+ <head>
6
+ <meta charset="utf-8" />
7
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover" />
8
+ <title>Hello World – On-Device Chat (Transformers.js)</title>
9
+ <style>
10
+ :root {
11
+ color-scheme: light dark;
12
+ --bg: #0b0c10;
13
+ --panel: #111318;
14
+ --panel-2: #151922;
15
+ --text: #e9eef3;
16
+ --muted: #9aa6b2;
17
+ --accent: #4f7cff;
18
+ --accent-2: #3a5fe0;
19
+ --danger: #ff4d67;
20
+ --ok: #3cc07a;
21
+ --shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
22
+ --radius: 14px;
23
+ --radius-sm: 10px;
24
+ --radius-lg: 22px;
25
+ --gap: 12px;
26
+ --gap-lg: 18px;
27
+ --gap-xl: 24px;
28
+ --font: system-ui, -apple-system, Segoe UI, Roboto, Inter, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
29
+ --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
30
+ }
31
+
32
+ * {
33
+ box-sizing: border-box;
34
+ }
35
+
36
+ html,
37
+ body {
38
+ margin: 0;
39
+ padding: 0;
40
+ height: 100%;
41
+ background: linear-gradient(180deg, #0b0c10, #0d1018 60%, #0b0c10);
42
+ color: var(--text);
43
+ font-family: var(--font);
44
+ }
45
+
46
+ .app {
47
+ display: grid;
48
+ grid-template-rows: auto 1fr auto;
49
+ height: 100dvh;
50
+ max-width: 900px;
51
+ margin: 0 auto;
52
+ }
53
+
54
+ header {
55
+ position: sticky;
56
+ top: 0;
57
+ z-index: 5;
58
+ display: flex;
59
+ gap: var(--gap);
60
+ padding: 12px;
61
+ align-items: center;
62
+ background: transparent;
63
+ backdrop-filter: saturate(140%) blur(10px);
64
+ }
65
+
66
+ .brand {
67
+ display: flex;
68
+ align-items: center;
69
+ gap: 10px;
70
+ padding: 10px 12px;
71
+ border-radius: var(--radius);
72
+ background: linear-gradient(180deg, #121623, #0e1220);
73
+ box-shadow: var(--shadow);
74
+ font-weight: 700;
75
+ letter-spacing: .2px;
76
+ }
77
+
78
+ .brand .dot {
79
+ width: 10px;
80
+ height: 10px;
81
+ border-radius: 50%;
82
+ background: radial-gradient(circle at 30% 30%, #8ab4ff, #4f7cff);
83
+ box-shadow: 0 0 12px rgba(79, 124, 255, .75), 0 0 28px rgba(79, 124, 255, .45);
84
+ }
85
+
86
+ .controls {
87
+ display: flex;
88
+ gap: var(--gap);
89
+ margin-left: auto;
90
+ align-items: center;
91
+ }
92
+
93
+ select,
94
+ button,
95
+ .status {
96
+ border: 1px solid #1f2533;
97
+ background: linear-gradient(180deg, #101523, #0f1320);
98
+ color: var(--text);
99
+ border-radius: var(--radius);
100
+ padding: 10px 12px;
101
+ font: inherit;
102
+ box-shadow: var(--shadow);
103
+ }
104
+
105
+ select {
106
+ padding-right: 36px;
107
+ }
108
+
109
+ button {
110
+ cursor: pointer;
111
+ border: 1px solid #2a375a;
112
+ }
113
+
114
+ button.primary {
115
+ background: linear-gradient(180deg, var(--accent), var(--accent-2));
116
+ border: none;
117
+ color: white;
118
+ font-weight: 600;
119
+ }
120
+
121
+ button.ghost {
122
+ background: transparent;
123
+ border: 1px dashed #2a375a;
124
+ opacity: .9;
125
+ }
126
+
127
+ button:disabled {
128
+ opacity: .5;
129
+ cursor: not-allowed;
130
+ filter: grayscale(.2);
131
+ }
132
+
133
+ .status {
134
+ display: flex;
135
+ align-items: center;
136
+ gap: 8px;
137
+ color: var(--muted);
138
+ padding: 10px 12px;
139
+ border-radius: var(--radius);
140
+ border-style: dashed;
141
+ white-space: nowrap;
142
+ max-width: 58vw;
143
+ overflow: hidden;
144
+ text-overflow: ellipsis;
145
+ }
146
+
147
+ .status .blink {
148
+ width: 8px;
149
+ height: 8px;
150
+ border-radius: 50%;
151
+ background: var(--muted);
152
+ animation: pulse 1.2s infinite ease-in-out;
153
+ box-shadow: 0 0 0 0 rgba(79, 124, 255, 0.6);
154
+ }
155
+
156
+ .status.loading .blink {
157
+ background: var(--accent);
158
+ box-shadow: 0 0 0 0 rgba(79, 124, 255, 0.6);
159
+ animation: pulse 1.2s infinite ease-in-out;
160
+ }
161
+
162
+ .status.generating .blink {
163
+ background: var(--ok);
164
+ animation: pulse-fast .8s infinite ease-in-out;
165
+ box-shadow: 0 0 0 0 rgba(60, 192, 122, 0.6);
166
+ }
167
+
168
+ @keyframes pulse {
169
+ 0% {
170
+ transform: scale(1);
171
+ box-shadow: 0 0 0 0 rgba(79, 124, 255, 0.5);
172
+ }
173
+
174
+ 70% {
175
+ transform: scale(1.12);
176
+ box-shadow: 0 0 0 10px rgba(79, 124, 255, 0);
177
+ }
178
+
179
+ 100% {
180
+ transform: scale(1);
181
+ box-shadow: 0 0 0 0 rgba(79, 124, 255, 0);
182
+ }
183
+ }
184
+
185
+ @keyframes pulse-fast {
186
+ 0% {
187
+ transform: scale(1);
188
+ box-shadow: 0 0 0 0 rgba(60, 192, 122, 0.5);
189
+ }
190
+
191
+ 70% {
192
+ transform: scale(1.12);
193
+ box-shadow: 0 0 0 10px rgba(60, 192, 122, 0);
194
+ }
195
+
196
+ 100% {
197
+ transform: scale(1);
198
+ box-shadow: 0 0 0 0 rgba(60, 192, 122, 0);
199
+ }
200
+ }
201
+
202
+ main {
203
+ padding: 10px 12px 16px;
204
+ overflow: auto;
205
+ display: flex;
206
+ flex-direction: column;
207
+ gap: var(--gap);
208
+ scroll-behavior: smooth;
209
+ }
210
+
211
+ .msg {
212
+ display: grid;
213
+ grid-template-columns: auto 1fr;
214
+ gap: 8px 10px;
215
+ align-items: start;
216
+ max-width: 100%;
217
+ word-wrap: break-word;
218
+ overflow-wrap: anywhere;
219
+ }
220
+
221
+ .msg .avatar {
222
+ width: 28px;
223
+ height: 28px;
224
+ border-radius: 8px;
225
+ display: grid;
226
+ place-items: center;
227
+ font-size: 14px;
228
+ font-weight: 700;
229
+ color: #0d1020;
230
+ background: #b8c8ff;
231
+ }
232
+
233
+ .msg.assistant .avatar {
234
+ background: #9be7c4;
235
+ }
236
+
237
+ .bubble {
238
+ padding: 12px 14px;
239
+ border-radius: var(--radius-lg);
240
+ line-height: 1.4;
241
+ box-shadow: var(--shadow);
242
+ background: linear-gradient(180deg, #111523, #0e1322);
243
+ border: 1px solid #1d2433;
244
+ white-space: pre-wrap;
245
+ }
246
+
247
+ .msg.user .bubble {
248
+ background: linear-gradient(180deg, #152037, #10192f);
249
+ border: 1px solid #243355;
250
+ }
251
+
252
+ .msg.error .bubble {
253
+ background: linear-gradient(180deg, #2a0f14, #220a0f);
254
+ border: 1px solid #5d1b26;
255
+ color: #ffd7de;
256
+ }
257
+
258
+ .meta {
259
+ grid-column: 2 / -1;
260
+ color: var(--muted);
261
+ font-size: 12px;
262
+ margin-top: -4px;
263
+ }
264
+
265
+ footer {
266
+ display: grid;
267
+ grid-template-columns: 1fr auto;
268
+ gap: var(--gap);
269
+ padding: 10px 12px 14px;
270
+ background: linear-gradient(180deg, #0b0f1a, #0b0f1a);
271
+ border-top: 1px solid #161c2a;
272
+ }
273
+
274
+ textarea {
275
+ resize: none;
276
+ width: 100%;
277
+ min-height: 52px;
278
+ max-height: 36dvh;
279
+ padding: 12px 14px;
280
+ border-radius: var(--radius-lg);
281
+ border: 1px solid #1f2738;
282
+ outline: none;
283
+ box-shadow: var(--shadow);
284
+ background: linear-gradient(180deg, #101426, #0e1221);
285
+ color: var(--text);
286
+ caret-color: var(--accent);
287
+ }
288
+
289
+ .actions {
290
+ display: flex;
291
+ gap: var(--gap);
292
+ align-items: stretch;
293
+ }
294
+
295
+ .hint {
296
+ padding: 8px 12px;
297
+ color: var(--muted);
298
+ font-size: 12px;
299
+ text-align: center;
300
+ }
301
+
302
+ @media (min-width: 720px) {
303
+ header {
304
+ padding: 16px;
305
+ }
306
+
307
+ main {
308
+ padding: 12px 16px 18px;
309
+ }
310
+
311
+ footer {
312
+ padding: 12px 16px 18px;
313
+ }
314
+
315
+ .status {
316
+ max-width: 420px;
317
+ }
318
+ }
319
+ </style>
320
+ </head>
321
+
322
+ <body>
323
+ <div class="app">
324
+ <header>
325
+ <div class="brand" title="On-device chat">
326
+ <div class="dot" aria-hidden="true"></div>
327
+ <div>Hello World</div>
328
+ </div>
329
+
330
+ <div class="controls" style="flex-wrap: wrap">
331
+ <label for="model" class="visually-hidden" style="position:absolute;left:-9999px">Model</label>
332
+ <select id="model" title="Select a model">
333
+ <option value="Xenova/distilgpt2">Xenova/distilgpt2</option>
334
+ <option value="Xenova/phi-3-mini-4k-instruct">Xenova/phi-3-mini-4k-instruct</option>
335
+ <option value="Xenova/t5-small">Xenova/t5-small</option>
336
+ <option value="Xenova/gemma-2b-it">Xenova/gemma-2b-it</option>
337
+ <option value="Xenova/llama-3-8b-instruct">Xenova/llama-3-8b-instruct</option>
338
+ <option value="Xenova/Mistral-7B-Instruct-v0.2">Xenova/Mistral-7B-Instruct-v0.2</option>
339
+ </select>
340
+
341
+ <div id="status" class="status" role="status" aria-live="polite">
342
+ <div class="blink" aria-hidden="true"></div>
343
+ <span class="label">Initializing…</span>
344
+ </div>
345
+ </div>
346
+ </header>
347
+
348
+ <main id="chat" aria-live="polite" aria-busy="false">
349
+ <div class="msg assistant">
350
+ <div class="avatar">AI</div>
351
+ <div class="bubble">Hello! I run fully in your browser using Transformers.js. Pick a model above and say hi.
352
+ </div>
353
+ <div class="meta">On-device, no server calls.</div>
354
+ </div>
355
+ </main>
356
+
357
+ <footer>
358
+ <textarea id="input" placeholder="Type a message… (Shift+Enter for newline)"></textarea>
359
+ <div class="actions">
360
+ <button id="send" class="primary">Send</button>
361
+ <button id="stop" class="ghost" disabled>Stop</button>
362
+ </div>
363
+ <div class="hint" style="grid-column: 1 / -1">
364
+ Tip: Switch models anytime. The first run will download weights into your browser cache.
365
+ </div>
366
+ </footer>
367
+ </div>
368
+
369
+ <script type="module">
370
+ // Wrap everything in an async IIFE and catch top-level errors.
371
+ (async () => {
372
+ 'use strict';
373
+
374
+ // UI elements
375
+ const chatEl = document.getElementById('chat');
376
+ const inputEl = document.getElementById('input');
377
+ const sendBtn = document.getElementById('send');
378
+ const stopBtn = document.getElementById('stop');
379
+ const modelSelect = document.getElementById('model');
380
+ const statusEl = document.getElementById('status');
381
+
382
+ // App state
383
+ const state = {
384
+ busy: false,
385
+ modelId: modelSelect.value,
386
+ task: null,
387
+ pipe: null,
388
+ messages: [{ role: 'system', content: 'You are a concise, friendly assistant.' }],
389
+ abortController: null,
390
+ };
391
+
392
+ // Helpers: UI status
393
+ function setStatus(mode, text) {
394
+ try {
395
+ statusEl.classList.remove('loading', 'generating');
396
+ if (mode) statusEl.classList.add(mode);
397
+ statusEl.querySelector('.label').textContent = text;
398
+ } catch (err) {
399
+ reportError(err);
400
+ }
401
+ }
402
+
403
+ function setBusy(isBusy) {
404
+ try {
405
+ state.busy = isBusy;
406
+ chatEl.setAttribute('aria-busy', String(isBusy));
407
+ sendBtn.disabled = isBusy;
408
+ modelSelect.disabled = isBusy;
409
+ stopBtn.disabled = !isBusy;
410
+ inputEl.readOnly = isBusy;
411
+ } catch (err) {
412
+ reportError(err);
413
+ }
414
+ }
415
+
416
+ // Helpers: chat UI
417
+ function addMessage(role, text, opts = {}) {
418
+ try {
419
+ const wrapper = document.createElement('div');
420
+ wrapper.className = `msg ${role}${opts.error ? ' error' : ''}`;
421
+ const avatar = document.createElement('div');
422
+ avatar.className = 'avatar';
423
+ avatar.textContent = role === 'user' ? 'You' : role === 'assistant' ? 'AI' : 'S';
424
+ const bubble = document.createElement('div');
425
+ bubble.className = 'bubble';
426
+ bubble.textContent = text ?? '';
427
+ const meta = document.createElement('div');
428
+ meta.className = 'meta';
429
+ meta.textContent = opts.meta ?? (role === 'assistant' ? ' ' : ' ');
430
+ wrapper.append(avatar, bubble, meta);
431
+ chatEl.appendChild(wrapper);
432
+ chatEl.scrollTop = chatEl.scrollHeight;
433
+ return { wrapper, bubble, meta };
434
+ } catch (err) {
435
+ reportError(err);
436
+ return null;
437
+ }
438
+ }
439
+
440
+ function updateLastAssistant(text) {
441
+ try {
442
+ const nodes = Array.from(chatEl.querySelectorAll('.msg.assistant .bubble'));
443
+ const last = nodes[nodes.length - 1];
444
+ if (last) {
445
+ last.textContent = text;
446
+ chatEl.scrollTop = chatEl.scrollHeight;
447
+ }
448
+ } catch (err) {
449
+ reportError(err);
450
+ }
451
+ }
452
+
453
+ function reportError(error) {
454
+ try {
455
+ const msg = (error && error.stack) ? error.stack : (error && error.message) ? error.message : String(error);
456
+ addMessage('assistant', `Error:\n\n${msg}`, { error: true, meta: 'stack trace' });
457
+ } catch (e) {
458
+ // Last-resort: console
459
+ console.error('Failed to report error:', e, error);
460
+ }
461
+ }
462
+
463
+ // Load Transformers.js dynamically (required)
464
+ let pipeline, env, AutoTokenizer;
465
+ try {
466
+ const mod = await import('https://cdn.jsdelivr.net/npm/@xenova/transformers/dist/transformers.min.js');
467
+ pipeline = mod.pipeline;
468
+ env = mod.env;
469
+ AutoTokenizer = mod.AutoTokenizer;
470
+ } catch (err) {
471
+ reportError(err);
472
+ throw err; // cannot proceed
473
+ }
474
+
475
+ // Configure environment (browser cache for model files)
476
+ try {
477
+ env.allowRemoteModels = true; // allow fetching from the Hugging Face Hub / CDN
478
+ env.useBrowserCache = true; // caches in IndexedDB/Cache Storage
479
+ } catch (err) {
480
+ reportError(err);
481
+ }
482
+
483
+ // Determine pipeline task based on model id
484
+ function resolveTask(modelId) {
485
+ try {
486
+ if (/t5/i.test(modelId)) return 'text2text-generation';
487
+ return 'text-generation';
488
+ } catch (err) {
489
+ reportError(err);
490
+ return 'text-generation';
491
+ }
492
+ }
493
+
494
+ // Progress UI during model load
495
+ function progressCallback(data) {
496
+ try {
497
+ const { status, name, file, loaded, total } = data || {};
498
+ const pct = (loaded && total) ? ` ${(100 * loaded / total).toFixed(1)}%` : '';
499
+ const fileName = (file && file.split('/').pop()) || name || '…';
500
+ setStatus('loading', `${status || 'Loading'} ${fileName}${pct}`);
501
+ } catch (err) {
502
+ reportError(err);
503
+ }
504
+ }
505
+
506
+ // Load/Reload selected model
507
+ async function loadModel(modelId) {
508
+ setBusy(true);
509
+ setStatus('loading', `Loading ${modelId}…`);
510
+ try {
511
+ state.task = resolveTask(modelId);
512
+ state.pipe = await pipeline(state.task, modelId, { progress_callback: progressCallback });
513
+ state.modelId = modelId;
514
+ setStatus('', `Ready • ${modelId}`);
515
+ } catch (err) {
516
+ reportError(err);
517
+ setStatus('', `Failed to load ${modelId}`);
518
+ } finally {
519
+ setBusy(false);
520
+ }
521
+ }
522
+
523
+ // Build input text from chat messages
524
+ function buildModelInput() {
525
+ try {
526
+ // If tokenizer supports chat templates, use them
527
+ const tok = state.pipe?.tokenizer;
528
+ const msgs = state.messages.map(m => ({ role: m.role, content: m.content }));
529
+ if (tok && typeof tok.apply_chat_template === 'function') {
530
+ try {
531
+ return tok.apply_chat_template(msgs, {
532
+ tokenize: false,
533
+ add_generation_prompt: true,
534
+ });
535
+ } catch (e) {
536
+ // Fall through to manual prompt
537
+ }
538
+ }
539
+
540
+ // Fallback prompt formatting (works for generic causal LMs)
541
+ let prompt = 'You are a helpful assistant.\n';
542
+ for (const m of msgs) {
543
+ if (m.role === 'system') {
544
+ prompt += `[System] ${m.content}\n`;
545
+ } else if (m.role === 'user') {
546
+ prompt += `User: ${m.content}\n`;
547
+ } else if (m.role === 'assistant') {
548
+ prompt += `Assistant: ${m.content}\n`;
549
+ }
550
+ }
551
+ prompt += 'Assistant:';
552
+ return prompt;
553
+ } catch (err) {
554
+ reportError(err);
555
+ return (state.messages[state.messages.length - 1]?.content) || '';
556
+ }
557
+ }
558
+
559
+ // Generate a response with streaming
560
+ async function generateResponse() {
561
+ if (!state.pipe) {
562
+ addMessage('assistant', 'Model is not loaded yet.');
563
+ return;
564
+ }
565
+ setBusy(true);
566
+ setStatus('generating', `Generating with ${state.modelId}…`);
567
+
568
+ // Create a placeholder assistant message to stream into
569
+ const assistant = addMessage('assistant', '…');
570
+
571
+ // Optional: support abort
572
+ state.abortController = new AbortController();
573
+
574
+ try {
575
+ const isT5 = state.task === 'text2text-generation';
576
+ const inputText = isT5 ? (state.messages[state.messages.length - 1]?.content || '') : buildModelInput();
577
+
578
+ const genOptions = {
579
+ max_new_tokens: 180,
580
+ temperature: 0.7,
581
+ top_p: 0.9,
582
+ repetition_penalty: 1.08,
583
+ stream: true,
584
+ // signal for cancellation
585
+ signal: state.abortController.signal,
586
+ };
587
+
588
+ // Stream tokens
589
+ let full = '';
590
+ for await (const out of state.pipe(inputText, genOptions)) {
591
+ const tokenText =
592
+ (out && out.token && typeof out.token.text === 'string')
593
+ ? out.token.text
594
+ : (out?.generated_text && out?.generated_text[0]?.token?.text)
595
+ ? out.generated_text[0].token.text
596
+ : (out?.text) ? out.text : '';
597
+ if (tokenText) {
598
+ full += tokenText;
599
+ updateLastAssistant(full);
600
+ }
601
+ }
602
+
603
+ // Some pipelines only return final output when not streaming; if empty, try non-stream as fallback
604
+ if (!full.trim()) {
605
+ const finalOut = await state.pipe(inputText, { ...genOptions, stream: false });
606
+ const text =
607
+ Array.isArray(finalOut) ? (finalOut[0]?.generated_text ?? finalOut[0]?.summary_text ?? finalOut[0]?.translation_text ?? '') :
608
+ (finalOut?.generated_text ?? finalOut?.text ?? '');
609
+ full = (text || '').toString();
610
+ updateLastAssistant(full || '(no output)');
611
+ }
612
+
613
+ // Save assistant turn
614
+ state.messages.push({ role: 'assistant', content: full || '' });
615
+ setStatus('', `Ready • ${state.modelId}`);
616
+ } catch (err) {
617
+ if (err && err.name === 'AbortError') {
618
+ updateLastAssistant('[stopped]');
619
+ addMessage('assistant', 'Generation stopped by user.', { meta: 'stopped' });
620
+ setStatus('', `Ready • ${state.modelId}`);
621
+ } else {
622
+ reportError(err);
623
+ setStatus('', `Ready • ${state.modelId}`);
624
+ }
625
+ } finally {
626
+ setBusy(false);
627
+ state.abortController = null;
628
+ }
629
+ }
630
+
631
+ // Send handler
632
+ async function onSend() {
633
+ try {
634
+ const text = (inputEl.value || '').trim();
635
+ if (!text || state.busy) return;
636
+ // Append user message
637
+ state.messages.push({ role: 'user', content: text });
638
+ addMessage('user', text);
639
+ inputEl.value = '';
640
+ inputEl.style.height = 'auto';
641
+ await generateResponse();
642
+ } catch (err) {
643
+ reportError(err);
644
+ }
645
+ }
646
+
647
+ // Autosize textarea
648
+ function autoGrow() {
649
+ try {
650
+ inputEl.style.height = 'auto';
651
+ inputEl.style.height = Math.min(inputEl.scrollHeight, window.innerHeight * 0.36) + 'px';
652
+ } catch (err) {
653
+ reportError(err);
654
+ }
655
+ }
656
+
657
+ // Wire up events (with error handling)
658
+ try {
659
+ sendBtn.addEventListener('click', async () => {
660
+ try { await onSend(); } catch (err) { reportError(err); }
661
+ });
662
+ stopBtn.addEventListener('click', () => {
663
+ try {
664
+ if (state.abortController) state.abortController.abort();
665
+ } catch (err) { reportError(err); }
666
+ });
667
+ inputEl.addEventListener('keydown', async (e) => {
668
+ try {
669
+ if (e.key === 'Enter' && !e.shiftKey) {
670
+ e.preventDefault();
671
+ await onSend();
672
+ }
673
+ } catch (err) { reportError(err); }
674
+ });
675
+ inputEl.addEventListener('input', () => {
676
+ try { autoGrow(); } catch (err) { reportError(err); }
677
+ });
678
+ modelSelect.addEventListener('change', async () => {
679
+ try {
680
+ if (state.busy) return;
681
+ await loadModel(modelSelect.value);
682
+ } catch (err) {
683
+ reportError(err);
684
+ }
685
+ });
686
+ } catch (err) {
687
+ reportError(err);
688
+ }
689
+
690
+ // Global error guards
691
+ window.addEventListener('error', (e) => {
692
+ reportError(e.error || e.message || e);
693
+ });
694
+ window.addEventListener('unhandledrejection', (e) => {
695
+ reportError(e.reason || e);
696
+ });
697
+
698
+ // Initial model load (default selection)
699
+ await loadModel(state.modelId);
700
+ setStatus('', `Ready • ${state.modelId}`);
701
+ })().catch((err) => {
702
+ // Top-level catch (final safety)
703
+ const pre = document.createElement('pre');
704
+ pre.textContent = (err && err.stack) ? err.stack : String(err);
705
+ pre.style.whiteSpace = 'pre-wrap';
706
+ pre.style.color = '#ffb4c7';
707
+ pre.style.padding = '12px';
708
+ pre.style.margin = '12px';
709
+ pre.style.border = '1px solid #5d1b26';
710
+ pre.style.background = '#220a0f';
711
+ document.body.appendChild(pre);
712
+ console.error(err);
713
+ });
714
+ </script>
715
+ </body>
716
+
717
+ </html>
chat-full.html CHANGED
@@ -209,7 +209,7 @@
209
  <body>
210
  <header>
211
  <div class="row">
212
- <h1>Локальний чат</h1>
213
  <label for="model" class="muted" style="display:none">Модель</label>
214
  <select id="model" title="Оберіть модель (Xenova)">
215
  <option value="Xenova/distilgpt2">Xenova/distilgpt2 (швидка, маленька)</option>
 
209
  <body>
210
  <header>
211
  <div class="row">
212
+ <h1>Локальний чат: 1</h1>
213
  <label for="model" class="muted" style="display:none">Модель</label>
214
  <select id="model" title="Оберіть модель (Xenova)">
215
  <option value="Xenova/distilgpt2">Xenova/distilgpt2 (швидка, маленька)</option>