naimulislam commited on
Commit
1cfa2ac
Β·
verified Β·
1 Parent(s): d334391

Create templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +1434 -0
templates/index.html ADDED
@@ -0,0 +1,1434 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Qwen3-0.6B Chat</title>
7
+ <style>
8
+ :root {
9
+ --bg-primary: #0f0f0f;
10
+ --bg-secondary: #1a1a1a;
11
+ --bg-tertiary: #252525;
12
+ --bg-hover: #2a2a2a;
13
+ --text-primary: #e8e8e8;
14
+ --text-secondary: #a0a0a0;
15
+ --text-muted: #666;
16
+ --accent: #7c6fe0;
17
+ --accent-hover: #6b5ed4;
18
+ --accent-light: rgba(124, 111, 224, 0.1);
19
+ --border: #2a2a2a;
20
+ --thinking-bg: #1a1a2e;
21
+ --thinking-border: #4a4a8a;
22
+ --tool-bg: #1a2e1a;
23
+ --tool-border: #4a8a4a;
24
+ --user-bg: #2a2540;
25
+ --assistant-bg: transparent;
26
+ --code-bg: #1e1e1e;
27
+ --success: #4ade80;
28
+ --warning: #fbbf24;
29
+ --error: #f87171;
30
+ --radius: 12px;
31
+ --radius-sm: 8px;
32
+ --shadow: 0 2px 8px rgba(0,0,0,0.3);
33
+ }
34
+
35
+ * {
36
+ margin: 0;
37
+ padding: 0;
38
+ box-sizing: border-box;
39
+ }
40
+
41
+ body {
42
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
43
+ background: var(--bg-primary);
44
+ color: var(--text-primary);
45
+ height: 100vh;
46
+ display: flex;
47
+ flex-direction: column;
48
+ overflow: hidden;
49
+ }
50
+
51
+ /* Header */
52
+ .header {
53
+ background: var(--bg-secondary);
54
+ border-bottom: 1px solid var(--border);
55
+ padding: 12px 24px;
56
+ display: flex;
57
+ align-items: center;
58
+ justify-content: space-between;
59
+ flex-shrink: 0;
60
+ z-index: 100;
61
+ }
62
+
63
+ .header-left {
64
+ display: flex;
65
+ align-items: center;
66
+ gap: 12px;
67
+ }
68
+
69
+ .logo {
70
+ width: 32px;
71
+ height: 32px;
72
+ background: var(--accent);
73
+ border-radius: var(--radius-sm);
74
+ display: flex;
75
+ align-items: center;
76
+ justify-content: center;
77
+ font-weight: 700;
78
+ font-size: 16px;
79
+ color: white;
80
+ }
81
+
82
+ .header-title {
83
+ font-size: 16px;
84
+ font-weight: 600;
85
+ }
86
+
87
+ .header-subtitle {
88
+ font-size: 12px;
89
+ color: var(--text-secondary);
90
+ }
91
+
92
+ .header-right {
93
+ display: flex;
94
+ align-items: center;
95
+ gap: 12px;
96
+ }
97
+
98
+ /* Controls */
99
+ .controls {
100
+ display: flex;
101
+ align-items: center;
102
+ gap: 8px;
103
+ flex-wrap: wrap;
104
+ }
105
+
106
+ .control-group {
107
+ display: flex;
108
+ align-items: center;
109
+ gap: 6px;
110
+ background: var(--bg-tertiary);
111
+ padding: 4px 10px;
112
+ border-radius: 20px;
113
+ font-size: 12px;
114
+ }
115
+
116
+ .control-label {
117
+ color: var(--text-secondary);
118
+ font-size: 11px;
119
+ text-transform: uppercase;
120
+ letter-spacing: 0.5px;
121
+ }
122
+
123
+ select, input[type="number"] {
124
+ background: var(--bg-primary);
125
+ border: 1px solid var(--border);
126
+ color: var(--text-primary);
127
+ padding: 4px 8px;
128
+ border-radius: 6px;
129
+ font-size: 12px;
130
+ outline: none;
131
+ cursor: pointer;
132
+ }
133
+
134
+ select:focus, input[type="number"]:focus {
135
+ border-color: var(--accent);
136
+ }
137
+
138
+ input[type="number"] {
139
+ width: 70px;
140
+ }
141
+
142
+ .btn {
143
+ padding: 6px 14px;
144
+ border-radius: 8px;
145
+ border: none;
146
+ cursor: pointer;
147
+ font-size: 12px;
148
+ font-weight: 500;
149
+ transition: all 0.15s ease;
150
+ display: flex;
151
+ align-items: center;
152
+ gap: 6px;
153
+ }
154
+
155
+ .btn-ghost {
156
+ background: transparent;
157
+ color: var(--text-secondary);
158
+ border: 1px solid var(--border);
159
+ }
160
+
161
+ .btn-ghost:hover {
162
+ background: var(--bg-hover);
163
+ color: var(--text-primary);
164
+ }
165
+
166
+ .btn-danger {
167
+ background: transparent;
168
+ color: var(--error);
169
+ border: 1px solid transparent;
170
+ }
171
+
172
+ .btn-danger:hover {
173
+ background: rgba(248, 113, 113, 0.1);
174
+ }
175
+
176
+ /* Chat Container */
177
+ .chat-container {
178
+ flex: 1;
179
+ overflow-y: auto;
180
+ padding: 20px 0;
181
+ scroll-behavior: smooth;
182
+ }
183
+
184
+ .chat-container::-webkit-scrollbar {
185
+ width: 6px;
186
+ }
187
+
188
+ .chat-container::-webkit-scrollbar-track {
189
+ background: transparent;
190
+ }
191
+
192
+ .chat-container::-webkit-scrollbar-thumb {
193
+ background: var(--border);
194
+ border-radius: 3px;
195
+ }
196
+
197
+ .chat-container::-webkit-scrollbar-thumb:hover {
198
+ background: var(--text-muted);
199
+ }
200
+
201
+ /* Messages */
202
+ .message-wrapper {
203
+ max-width: 800px;
204
+ margin: 0 auto;
205
+ padding: 0 24px;
206
+ }
207
+
208
+ .message {
209
+ padding: 16px 0;
210
+ animation: fadeIn 0.3s ease;
211
+ }
212
+
213
+ @keyframes fadeIn {
214
+ from { opacity: 0; transform: translateY(8px); }
215
+ to { opacity: 1; transform: translateY(0); }
216
+ }
217
+
218
+ .message-header {
219
+ display: flex;
220
+ align-items: center;
221
+ gap: 8px;
222
+ margin-bottom: 8px;
223
+ }
224
+
225
+ .message-avatar {
226
+ width: 28px;
227
+ height: 28px;
228
+ border-radius: 50%;
229
+ display: flex;
230
+ align-items: center;
231
+ justify-content: center;
232
+ font-size: 13px;
233
+ font-weight: 600;
234
+ flex-shrink: 0;
235
+ }
236
+
237
+ .message-avatar.user {
238
+ background: var(--accent);
239
+ color: white;
240
+ }
241
+
242
+ .message-avatar.assistant {
243
+ background: var(--bg-tertiary);
244
+ color: var(--accent);
245
+ border: 1px solid var(--border);
246
+ }
247
+
248
+ .message-role {
249
+ font-size: 13px;
250
+ font-weight: 600;
251
+ }
252
+
253
+ .message-content {
254
+ padding-left: 36px;
255
+ line-height: 1.7;
256
+ font-size: 14px;
257
+ color: var(--text-primary);
258
+ }
259
+
260
+ .message-content p {
261
+ margin-bottom: 12px;
262
+ }
263
+
264
+ .message-content p:last-child {
265
+ margin-bottom: 0;
266
+ }
267
+
268
+ /* Thinking Block */
269
+ .thinking-block {
270
+ background: var(--thinking-bg);
271
+ border: 1px solid var(--thinking-border);
272
+ border-radius: var(--radius-sm);
273
+ margin-bottom: 12px;
274
+ overflow: hidden;
275
+ }
276
+
277
+ .thinking-header {
278
+ display: flex;
279
+ align-items: center;
280
+ justify-content: space-between;
281
+ padding: 8px 14px;
282
+ cursor: pointer;
283
+ user-select: none;
284
+ transition: background 0.15s;
285
+ }
286
+
287
+ .thinking-header:hover {
288
+ background: rgba(74, 74, 138, 0.15);
289
+ }
290
+
291
+ .thinking-header-left {
292
+ display: flex;
293
+ align-items: center;
294
+ gap: 8px;
295
+ font-size: 12px;
296
+ color: #8888cc;
297
+ font-weight: 500;
298
+ }
299
+
300
+ .thinking-icon {
301
+ animation: pulse 2s infinite;
302
+ }
303
+
304
+ .thinking-icon.done {
305
+ animation: none;
306
+ }
307
+
308
+ @keyframes pulse {
309
+ 0%, 100% { opacity: 1; }
310
+ 50% { opacity: 0.4; }
311
+ }
312
+
313
+ .thinking-toggle {
314
+ font-size: 10px;
315
+ color: var(--text-muted);
316
+ transition: transform 0.2s;
317
+ }
318
+
319
+ .thinking-toggle.collapsed {
320
+ transform: rotate(-90deg);
321
+ }
322
+
323
+ .thinking-content {
324
+ padding: 10px 14px;
325
+ font-size: 13px;
326
+ color: #9999bb;
327
+ line-height: 1.6;
328
+ border-top: 1px solid rgba(74, 74, 138, 0.3);
329
+ max-height: 300px;
330
+ overflow-y: auto;
331
+ white-space: pre-wrap;
332
+ }
333
+
334
+ .thinking-content.hidden {
335
+ display: none;
336
+ }
337
+
338
+ /* Tool Call Block */
339
+ .tool-block {
340
+ background: var(--tool-bg);
341
+ border: 1px solid var(--tool-border);
342
+ border-radius: var(--radius-sm);
343
+ margin-bottom: 12px;
344
+ padding: 10px 14px;
345
+ }
346
+
347
+ .tool-header {
348
+ font-size: 12px;
349
+ color: var(--success);
350
+ font-weight: 500;
351
+ display: flex;
352
+ align-items: center;
353
+ gap: 6px;
354
+ margin-bottom: 6px;
355
+ }
356
+
357
+ .tool-content {
358
+ font-family: 'SF Mono', 'Fira Code', monospace;
359
+ font-size: 12px;
360
+ color: #88cc88;
361
+ white-space: pre-wrap;
362
+ word-break: break-all;
363
+ }
364
+
365
+ /* Code blocks */
366
+ .message-content pre {
367
+ background: var(--code-bg);
368
+ border: 1px solid var(--border);
369
+ border-radius: var(--radius-sm);
370
+ padding: 14px;
371
+ margin: 10px 0;
372
+ overflow-x: auto;
373
+ font-size: 13px;
374
+ line-height: 1.5;
375
+ }
376
+
377
+ .message-content code {
378
+ font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
379
+ }
380
+
381
+ .message-content :not(pre) > code {
382
+ background: var(--bg-tertiary);
383
+ padding: 2px 6px;
384
+ border-radius: 4px;
385
+ font-size: 13px;
386
+ }
387
+
388
+ /* Lists */
389
+ .message-content ul, .message-content ol {
390
+ padding-left: 24px;
391
+ margin-bottom: 12px;
392
+ }
393
+
394
+ .message-content li {
395
+ margin-bottom: 4px;
396
+ }
397
+
398
+ /* Cursor */
399
+ .cursor {
400
+ display: inline-block;
401
+ width: 2px;
402
+ height: 16px;
403
+ background: var(--accent);
404
+ margin-left: 2px;
405
+ animation: blink 1s step-end infinite;
406
+ vertical-align: text-bottom;
407
+ }
408
+
409
+ @keyframes blink {
410
+ 50% { opacity: 0; }
411
+ }
412
+
413
+ /* Empty State */
414
+ .empty-state {
415
+ display: flex;
416
+ flex-direction: column;
417
+ align-items: center;
418
+ justify-content: center;
419
+ height: 100%;
420
+ color: var(--text-muted);
421
+ gap: 16px;
422
+ padding: 40px;
423
+ }
424
+
425
+ .empty-icon {
426
+ font-size: 48px;
427
+ opacity: 0.3;
428
+ }
429
+
430
+ .empty-title {
431
+ font-size: 20px;
432
+ font-weight: 600;
433
+ color: var(--text-secondary);
434
+ }
435
+
436
+ .empty-desc {
437
+ font-size: 14px;
438
+ text-align: center;
439
+ max-width: 400px;
440
+ line-height: 1.6;
441
+ }
442
+
443
+ .quick-actions {
444
+ display: flex;
445
+ gap: 8px;
446
+ flex-wrap: wrap;
447
+ justify-content: center;
448
+ margin-top: 8px;
449
+ }
450
+
451
+ .quick-action {
452
+ padding: 8px 16px;
453
+ background: var(--bg-tertiary);
454
+ border: 1px solid var(--border);
455
+ border-radius: 20px;
456
+ color: var(--text-secondary);
457
+ font-size: 13px;
458
+ cursor: pointer;
459
+ transition: all 0.15s;
460
+ }
461
+
462
+ .quick-action:hover {
463
+ background: var(--bg-hover);
464
+ color: var(--text-primary);
465
+ border-color: var(--accent);
466
+ }
467
+
468
+ /* Input Area */
469
+ .input-area {
470
+ border-top: 1px solid var(--border);
471
+ background: var(--bg-secondary);
472
+ padding: 16px 24px;
473
+ flex-shrink: 0;
474
+ }
475
+
476
+ .input-wrapper {
477
+ max-width: 800px;
478
+ margin: 0 auto;
479
+ }
480
+
481
+ .input-box {
482
+ display: flex;
483
+ align-items: flex-end;
484
+ gap: 10px;
485
+ background: var(--bg-tertiary);
486
+ border: 1px solid var(--border);
487
+ border-radius: var(--radius);
488
+ padding: 10px 14px;
489
+ transition: border-color 0.15s;
490
+ }
491
+
492
+ .input-box:focus-within {
493
+ border-color: var(--accent);
494
+ }
495
+
496
+ .input-box textarea {
497
+ flex: 1;
498
+ background: transparent;
499
+ border: none;
500
+ color: var(--text-primary);
501
+ font-size: 14px;
502
+ font-family: inherit;
503
+ resize: none;
504
+ outline: none;
505
+ line-height: 1.5;
506
+ max-height: 200px;
507
+ min-height: 24px;
508
+ }
509
+
510
+ .input-box textarea::placeholder {
511
+ color: var(--text-muted);
512
+ }
513
+
514
+ .send-btn {
515
+ width: 36px;
516
+ height: 36px;
517
+ border-radius: 8px;
518
+ border: none;
519
+ background: var(--accent);
520
+ color: white;
521
+ cursor: pointer;
522
+ display: flex;
523
+ align-items: center;
524
+ justify-content: center;
525
+ transition: all 0.15s;
526
+ flex-shrink: 0;
527
+ }
528
+
529
+ .send-btn:hover:not(:disabled) {
530
+ background: var(--accent-hover);
531
+ transform: scale(1.05);
532
+ }
533
+
534
+ .send-btn:disabled {
535
+ opacity: 0.4;
536
+ cursor: not-allowed;
537
+ }
538
+
539
+ .send-btn.stop {
540
+ background: var(--error);
541
+ }
542
+
543
+ .input-footer {
544
+ display: flex;
545
+ justify-content: space-between;
546
+ align-items: center;
547
+ margin-top: 8px;
548
+ font-size: 11px;
549
+ color: var(--text-muted);
550
+ }
551
+
552
+ /* Status indicator */
553
+ .status-dot {
554
+ width: 8px;
555
+ height: 8px;
556
+ border-radius: 50%;
557
+ background: var(--success);
558
+ display: inline-block;
559
+ margin-right: 4px;
560
+ }
561
+
562
+ .status-dot.loading {
563
+ background: var(--warning);
564
+ animation: pulse 1s infinite;
565
+ }
566
+
567
+ /* Tools panel */
568
+ .tools-panel {
569
+ display: none;
570
+ background: var(--bg-secondary);
571
+ border-top: 1px solid var(--border);
572
+ padding: 12px 24px;
573
+ max-height: 200px;
574
+ overflow-y: auto;
575
+ }
576
+
577
+ .tools-panel.visible {
578
+ display: block;
579
+ }
580
+
581
+ .tools-panel-header {
582
+ display: flex;
583
+ justify-content: space-between;
584
+ align-items: center;
585
+ margin-bottom: 8px;
586
+ }
587
+
588
+ .tools-panel-title {
589
+ font-size: 12px;
590
+ font-weight: 600;
591
+ color: var(--text-secondary);
592
+ text-transform: uppercase;
593
+ letter-spacing: 0.5px;
594
+ }
595
+
596
+ .tool-editor {
597
+ width: 100%;
598
+ max-width: 800px;
599
+ margin: 0 auto;
600
+ }
601
+
602
+ .tool-editor textarea {
603
+ width: 100%;
604
+ background: var(--bg-primary);
605
+ border: 1px solid var(--border);
606
+ color: var(--text-primary);
607
+ border-radius: var(--radius-sm);
608
+ padding: 10px;
609
+ font-family: 'SF Mono', monospace;
610
+ font-size: 12px;
611
+ resize: vertical;
612
+ min-height: 80px;
613
+ outline: none;
614
+ }
615
+
616
+ .tool-editor textarea:focus {
617
+ border-color: var(--accent);
618
+ }
619
+
620
+ /* System prompt */
621
+ .system-prompt-area {
622
+ display: none;
623
+ background: var(--bg-secondary);
624
+ border-top: 1px solid var(--border);
625
+ padding: 12px 24px;
626
+ }
627
+
628
+ .system-prompt-area.visible {
629
+ display: block;
630
+ }
631
+
632
+ .system-prompt-area textarea {
633
+ width: 100%;
634
+ max-width: 800px;
635
+ margin: 0 auto;
636
+ display: block;
637
+ background: var(--bg-primary);
638
+ border: 1px solid var(--border);
639
+ color: var(--text-primary);
640
+ border-radius: var(--radius-sm);
641
+ padding: 10px;
642
+ font-family: inherit;
643
+ font-size: 13px;
644
+ resize: vertical;
645
+ min-height: 60px;
646
+ outline: none;
647
+ }
648
+
649
+ /* Responsive */
650
+ @media (max-width: 768px) {
651
+ .header {
652
+ padding: 10px 16px;
653
+ flex-wrap: wrap;
654
+ gap: 8px;
655
+ }
656
+
657
+ .controls {
658
+ width: 100%;
659
+ justify-content: flex-start;
660
+ }
661
+
662
+ .message-wrapper {
663
+ padding: 0 16px;
664
+ }
665
+
666
+ .input-area {
667
+ padding: 12px 16px;
668
+ }
669
+ }
670
+
671
+ .markdown-rendered h1, .markdown-rendered h2, .markdown-rendered h3 {
672
+ margin-top: 16px;
673
+ margin-bottom: 8px;
674
+ font-weight: 600;
675
+ }
676
+
677
+ .markdown-rendered h1 { font-size: 1.4em; }
678
+ .markdown-rendered h2 { font-size: 1.2em; }
679
+ .markdown-rendered h3 { font-size: 1.1em; }
680
+
681
+ .markdown-rendered blockquote {
682
+ border-left: 3px solid var(--accent);
683
+ padding-left: 12px;
684
+ color: var(--text-secondary);
685
+ margin: 10px 0;
686
+ }
687
+
688
+ .markdown-rendered table {
689
+ border-collapse: collapse;
690
+ margin: 10px 0;
691
+ width: 100%;
692
+ }
693
+
694
+ .markdown-rendered th, .markdown-rendered td {
695
+ border: 1px solid var(--border);
696
+ padding: 8px 12px;
697
+ text-align: left;
698
+ }
699
+
700
+ .markdown-rendered th {
701
+ background: var(--bg-tertiary);
702
+ font-weight: 600;
703
+ }
704
+
705
+ .markdown-rendered hr {
706
+ border: none;
707
+ border-top: 1px solid var(--border);
708
+ margin: 16px 0;
709
+ }
710
+
711
+ .markdown-rendered a {
712
+ color: var(--accent);
713
+ text-decoration: none;
714
+ }
715
+
716
+ .markdown-rendered a:hover {
717
+ text-decoration: underline;
718
+ }
719
+
720
+ .markdown-rendered strong {
721
+ font-weight: 600;
722
+ color: var(--text-primary);
723
+ }
724
+
725
+ .markdown-rendered em {
726
+ font-style: italic;
727
+ color: var(--text-secondary);
728
+ }
729
+ </style>
730
+ </head>
731
+ <body>
732
+ <!-- Header -->
733
+ <div class="header">
734
+ <div class="header-left">
735
+ <div class="logo">Q</div>
736
+ <div>
737
+ <div class="header-title">Qwen3-0.6B</div>
738
+ <div class="header-subtitle">OpenAI-Compatible API</div>
739
+ </div>
740
+ </div>
741
+ <div class="header-right">
742
+ <div class="controls">
743
+ <div class="control-group">
744
+ <span class="control-label">Thinking</span>
745
+ <select id="thinkingMode">
746
+ <option value="auto">Auto</option>
747
+ <option value="true">Enabled</option>
748
+ <option value="false">Disabled</option>
749
+ </select>
750
+ </div>
751
+ <div class="control-group">
752
+ <span class="control-label">Temp</span>
753
+ <input type="number" id="temperature" value="0.7" min="0" max="2" step="0.1">
754
+ </div>
755
+ <div class="control-group">
756
+ <span class="control-label">Max Tokens</span>
757
+ <input type="number" id="maxTokens" value="4096" min="1" max="16384" step="256">
758
+ </div>
759
+ <button class="btn btn-ghost" onclick="toggleSystemPrompt()">
760
+ πŸ“ System
761
+ </button>
762
+ <button class="btn btn-ghost" onclick="toggleToolsPanel()">
763
+ πŸ”§ Tools
764
+ </button>
765
+ <button class="btn btn-danger" onclick="clearChat()">
766
+ πŸ—‘οΈ Clear
767
+ </button>
768
+ </div>
769
+ </div>
770
+ </div>
771
+
772
+ <!-- System Prompt -->
773
+ <div class="system-prompt-area" id="systemPromptArea">
774
+ <textarea id="systemPrompt" placeholder="Enter system prompt (optional)...">You are a helpful, intelligent assistant. Answer concisely and accurately.</textarea>
775
+ </div>
776
+
777
+ <!-- Tools Panel -->
778
+ <div class="tools-panel" id="toolsPanel">
779
+ <div class="tool-editor">
780
+ <div class="tools-panel-header">
781
+ <span class="tools-panel-title">πŸ”§ Tool Definitions (JSON Array)</span>
782
+ <button class="btn btn-ghost" onclick="loadSampleTools()">Load Sample</button>
783
+ </div>
784
+ <textarea id="toolsDefinition" placeholder='[{"type":"function","function":{"name":"get_weather","description":"Get weather info","parameters":{"type":"object","properties":{"city":{"type":"string"}}}}}]'></textarea>
785
+ </div>
786
+ </div>
787
+
788
+ <!-- Chat Container -->
789
+ <div class="chat-container" id="chatContainer">
790
+ <div class="message-wrapper">
791
+ <div class="empty-state" id="emptyState">
792
+ <div class="empty-icon">πŸ€–</div>
793
+ <div class="empty-title">Qwen3-0.6B Chat</div>
794
+ <div class="empty-desc">
795
+ A lightweight yet capable model with reasoning and tool calling support.
796
+ Choose a thinking mode and start chatting!
797
+ </div>
798
+ <div class="quick-actions">
799
+ <div class="quick-action" onclick="sendQuick('Explain quantum computing in simple terms')">
800
+ πŸ”¬ Quantum Computing
801
+ </div>
802
+ <div class="quick-action" onclick="sendQuick('Write a Python function to find prime numbers')">
803
+ πŸ’» Prime Numbers
804
+ </div>
805
+ <div class="quick-action" onclick="sendQuick('What are the pros and cons of microservices?')">
806
+ πŸ—οΈ Microservices
807
+ </div>
808
+ <div class="quick-action" onclick="sendQuick('Hello! What can you do?')">
809
+ πŸ‘‹ Say Hello
810
+ </div>
811
+ </div>
812
+ </div>
813
+ </div>
814
+ </div>
815
+
816
+ <!-- Input Area -->
817
+ <div class="input-area">
818
+ <div class="input-wrapper">
819
+ <div class="input-box">
820
+ <textarea id="userInput" placeholder="Type your message..." rows="1"
821
+ onkeydown="handleKeydown(event)" oninput="autoResize(this)"></textarea>
822
+ <button class="send-btn" id="sendBtn" onclick="handleSend()">
823
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
824
+ <path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z"/>
825
+ </svg>
826
+ </button>
827
+ </div>
828
+ <div class="input-footer">
829
+ <span><span class="status-dot" id="statusDot"></span><span id="statusText">Ready</span></span>
830
+ <span>Qwen3-0.6B Β· 32.8K context Β· Shift+Enter for new line</span>
831
+ </div>
832
+ </div>
833
+ </div>
834
+
835
+ <script>
836
+ // ─── State ──────────────────────────────────────────────────────
837
+ let chatHistory = [];
838
+ let isGenerating = false;
839
+ let abortController = null;
840
+ let currentStreamEl = null;
841
+
842
+ // ─── DOM refs ───────────────────────────────────────────────────
843
+ const chatContainer = document.getElementById('chatContainer');
844
+ const userInput = document.getElementById('userInput');
845
+ const sendBtn = document.getElementById('sendBtn');
846
+ const emptyState = document.getElementById('emptyState');
847
+ const statusDot = document.getElementById('statusDot');
848
+ const statusText = document.getElementById('statusText');
849
+
850
+ // ─── Markdown Rendering ─────────────────────────────────────────
851
+ function renderMarkdown(text) {
852
+ if (!text) return '';
853
+ let html = text;
854
+
855
+ // Escape HTML first (but not in code blocks)
856
+ // We'll handle code blocks separately
857
+
858
+ // Code blocks with language
859
+ html = html.replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) => {
860
+ const escaped = escapeHtml(code.trim());
861
+ return `<pre><code class="language-${lang || 'text'}">${escaped}</code></pre>`;
862
+ });
863
+
864
+ // Inline code
865
+ html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
866
+
867
+ // Bold
868
+ html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
869
+
870
+ // Italic
871
+ html = html.replace(/\*([^*]+)\*/g, '<em>$1</em>');
872
+
873
+ // Headers
874
+ html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
875
+ html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
876
+ html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
877
+
878
+ // Blockquotes
879
+ html = html.replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>');
880
+
881
+ // Unordered lists
882
+ html = html.replace(/^[\*\-] (.+)$/gm, '<li>$1</li>');
883
+ html = html.replace(/(<li>.*<\/li>)/gs, '<ul>$1</ul>');
884
+ // Remove nested ul tags
885
+ html = html.replace(/<\/ul>\s*<ul>/g, '');
886
+
887
+ // Ordered lists
888
+ html = html.replace(/^\d+\. (.+)$/gm, '<li>$1</li>');
889
+
890
+ // Horizontal rule
891
+ html = html.replace(/^---$/gm, '<hr>');
892
+
893
+ // Links
894
+ html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
895
+
896
+ // Paragraphs - convert double newlines
897
+ html = html.replace(/\n\n/g, '</p><p>');
898
+
899
+ // Single newlines to <br> (but not inside pre/code)
900
+ html = html.replace(/(?<!<\/?\w[^>]*)\n(?![<])/g, '<br>');
901
+
902
+ // Wrap in paragraph if not already wrapped
903
+ if (!html.startsWith('<')) {
904
+ html = '<p>' + html + '</p>';
905
+ }
906
+
907
+ return html;
908
+ }
909
+
910
+ function escapeHtml(text) {
911
+ const div = document.createElement('div');
912
+ div.textContent = text;
913
+ return div.innerHTML;
914
+ }
915
+
916
+ // ─── UI Functions ───────────────────────────────────────────────
917
+ function autoResize(el) {
918
+ el.style.height = 'auto';
919
+ el.style.height = Math.min(el.scrollHeight, 200) + 'px';
920
+ }
921
+
922
+ function handleKeydown(e) {
923
+ if (e.key === 'Enter' && !e.shiftKey) {
924
+ e.preventDefault();
925
+ handleSend();
926
+ }
927
+ }
928
+
929
+ function setStatus(text, loading = false) {
930
+ statusText.textContent = text;
931
+ statusDot.className = 'status-dot' + (loading ? ' loading' : '');
932
+ }
933
+
934
+ function toggleSystemPrompt() {
935
+ const area = document.getElementById('systemPromptArea');
936
+ area.classList.toggle('visible');
937
+ }
938
+
939
+ function toggleToolsPanel() {
940
+ const panel = document.getElementById('toolsPanel');
941
+ panel.classList.toggle('visible');
942
+ }
943
+
944
+ function loadSampleTools() {
945
+ const sample = [
946
+ {
947
+ "type": "function",
948
+ "function": {
949
+ "name": "get_weather",
950
+ "description": "Get the current weather for a location",
951
+ "parameters": {
952
+ "type": "object",
953
+ "properties": {
954
+ "city": {
955
+ "type": "string",
956
+ "description": "The city name"
957
+ },
958
+ "unit": {
959
+ "type": "string",
960
+ "enum": ["celsius", "fahrenheit"],
961
+ "description": "Temperature unit"
962
+ }
963
+ },
964
+ "required": ["city"]
965
+ }
966
+ }
967
+ },
968
+ {
969
+ "type": "function",
970
+ "function": {
971
+ "name": "calculate",
972
+ "description": "Perform a mathematical calculation",
973
+ "parameters": {
974
+ "type": "object",
975
+ "properties": {
976
+ "expression": {
977
+ "type": "string",
978
+ "description": "The math expression to evaluate"
979
+ }
980
+ },
981
+ "required": ["expression"]
982
+ }
983
+ }
984
+ }
985
+ ];
986
+ document.getElementById('toolsDefinition').value = JSON.stringify(sample, null, 2);
987
+ }
988
+
989
+ function clearChat() {
990
+ if (isGenerating) stopGeneration();
991
+ chatHistory = [];
992
+ const wrapper = chatContainer.querySelector('.message-wrapper');
993
+ wrapper.innerHTML = `
994
+ <div class="empty-state" id="emptyState">
995
+ <div class="empty-icon">πŸ€–</div>
996
+ <div class="empty-title">Qwen3-0.6B Chat</div>
997
+ <div class="empty-desc">
998
+ A lightweight yet capable model with reasoning and tool calling support.
999
+ Choose a thinking mode and start chatting!
1000
+ </div>
1001
+ <div class="quick-actions">
1002
+ <div class="quick-action" onclick="sendQuick('Explain quantum computing in simple terms')">πŸ”¬ Quantum Computing</div>
1003
+ <div class="quick-action" onclick="sendQuick('Write a Python function to find prime numbers')">πŸ’» Prime Numbers</div>
1004
+ <div class="quick-action" onclick="sendQuick('What are the pros and cons of microservices?')">πŸ—οΈ Microservices</div>
1005
+ <div class="quick-action" onclick="sendQuick('Hello! What can you do?')">πŸ‘‹ Say Hello</div>
1006
+ </div>
1007
+ </div>`;
1008
+ setStatus('Ready');
1009
+ }
1010
+
1011
+ function sendQuick(text) {
1012
+ userInput.value = text;
1013
+ autoResize(userInput);
1014
+ handleSend();
1015
+ }
1016
+
1017
+ function scrollToBottom() {
1018
+ chatContainer.scrollTop = chatContainer.scrollHeight;
1019
+ }
1020
+
1021
+ function removeEmptyState() {
1022
+ const es = document.getElementById('emptyState');
1023
+ if (es) es.remove();
1024
+ }
1025
+
1026
+ // ─── Message Rendering ──────────────────────────────────────────
1027
+ function addMessage(role, content, extras = {}) {
1028
+ removeEmptyState();
1029
+ const wrapper = chatContainer.querySelector('.message-wrapper');
1030
+
1031
+ const msgEl = document.createElement('div');
1032
+ msgEl.className = 'message';
1033
+
1034
+ const avatarClass = role === 'user' ? 'user' : 'assistant';
1035
+ const avatarText = role === 'user' ? 'U' : 'Q';
1036
+ const roleName = role === 'user' ? 'You' : 'Qwen3';
1037
+
1038
+ let html = `
1039
+ <div class="message-header">
1040
+ <div class="message-avatar ${avatarClass}">${avatarText}</div>
1041
+ <div class="message-role">${roleName}</div>
1042
+ </div>
1043
+ <div class="message-content markdown-rendered">`;
1044
+
1045
+ if (extras.thinking) {
1046
+ html += createThinkingBlock(extras.thinking, true);
1047
+ }
1048
+
1049
+ if (extras.toolCalls) {
1050
+ for (const tc of extras.toolCalls) {
1051
+ html += createToolCallBlock(tc);
1052
+ }
1053
+ }
1054
+
1055
+ if (content) {
1056
+ html += renderMarkdown(content);
1057
+ }
1058
+
1059
+ html += `</div>`;
1060
+ msgEl.innerHTML = html;
1061
+ wrapper.appendChild(msgEl);
1062
+ scrollToBottom();
1063
+ return msgEl;
1064
+ }
1065
+
1066
+ function createThinkingBlock(content, done = false) {
1067
+ const id = 'think-' + Math.random().toString(36).substr(2, 9);
1068
+ return `
1069
+ <div class="thinking-block">
1070
+ <div class="thinking-header" onclick="toggleThinking('${id}')">
1071
+ <div class="thinking-header-left">
1072
+ <span class="thinking-icon ${done ? 'done' : ''}">πŸ’­</span>
1073
+ <span>${done ? 'Thought process' : 'Thinking...'}</span>
1074
+ </div>
1075
+ <span class="thinking-toggle" id="${id}-toggle">β–Ό</span>
1076
+ </div>
1077
+ <div class="thinking-content ${done ? 'hidden' : ''}" id="${id}">${escapeHtml(content)}</div>
1078
+ </div>`;
1079
+ }
1080
+
1081
+ function createToolCallBlock(tc) {
1082
+ return `
1083
+ <div class="tool-block">
1084
+ <div class="tool-header">πŸ”§ Tool Call: ${escapeHtml(tc.function.name)}</div>
1085
+ <div class="tool-content">${escapeHtml(JSON.stringify(JSON.parse(tc.function.arguments), null, 2))}</div>
1086
+ </div>`;
1087
+ }
1088
+
1089
+ function toggleThinking(id) {
1090
+ const content = document.getElementById(id);
1091
+ const toggle = document.getElementById(id + '-toggle');
1092
+ if (content) {
1093
+ content.classList.toggle('hidden');
1094
+ toggle.classList.toggle('collapsed');
1095
+ }
1096
+ }
1097
+
1098
+ // ─── Streaming Assistant Message ────────────────────────────────
1099
+ function createStreamMessage() {
1100
+ removeEmptyState();
1101
+ const wrapper = chatContainer.querySelector('.message-wrapper');
1102
+
1103
+ const msgEl = document.createElement('div');
1104
+ msgEl.className = 'message';
1105
+ msgEl.innerHTML = `
1106
+ <div class="message-header">
1107
+ <div class="message-avatar assistant">Q</div>
1108
+ <div class="message-role">Qwen3</div>
1109
+ </div>
1110
+ <div class="message-content markdown-rendered" id="streamContent"></div>`;
1111
+ wrapper.appendChild(msgEl);
1112
+ currentStreamEl = document.getElementById('streamContent');
1113
+ return msgEl;
1114
+ }
1115
+
1116
+ // ─── Core Send Function ─────────────────────────────────────────
1117
+ async function handleSend() {
1118
+ if (isGenerating) {
1119
+ stopGeneration();
1120
+ return;
1121
+ }
1122
+
1123
+ const text = userInput.value.trim();
1124
+ if (!text) return;
1125
+
1126
+ userInput.value = '';
1127
+ autoResize(userInput);
1128
+
1129
+ // Add user message
1130
+ addMessage('user', text);
1131
+ chatHistory.push({ role: 'user', content: text });
1132
+
1133
+ // Prepare request
1134
+ const thinkingMode = document.getElementById('thinkingMode').value;
1135
+ const temperature = parseFloat(document.getElementById('temperature').value) || 0.7;
1136
+ const maxTokens = parseInt(document.getElementById('maxTokens').value) || 4096;
1137
+ const systemPrompt = document.getElementById('systemPrompt').value.trim();
1138
+ const toolsJson = document.getElementById('toolsDefinition').value.trim();
1139
+
1140
+ let messages = [];
1141
+ if (systemPrompt) {
1142
+ messages.push({ role: 'system', content: systemPrompt });
1143
+ }
1144
+ messages.push(...chatHistory);
1145
+
1146
+ let tools = null;
1147
+ if (toolsJson) {
1148
+ try {
1149
+ tools = JSON.parse(toolsJson);
1150
+ } catch (e) {
1151
+ console.warn('Invalid tools JSON:', e);
1152
+ }
1153
+ }
1154
+
1155
+ // Determine enable_thinking value
1156
+ let enableThinking;
1157
+ if (thinkingMode === 'true') enableThinking = true;
1158
+ else if (thinkingMode === 'false') enableThinking = false;
1159
+ else enableThinking = 'auto';
1160
+
1161
+ const reqBody = {
1162
+ model: 'qwen3-0.6b',
1163
+ messages: messages,
1164
+ temperature: temperature,
1165
+ max_tokens: maxTokens,
1166
+ stream: true,
1167
+ enable_thinking: enableThinking
1168
+ };
1169
+
1170
+ if (tools && tools.length > 0) {
1171
+ reqBody.tools = tools;
1172
+ }
1173
+
1174
+ // Start streaming
1175
+ isGenerating = true;
1176
+ abortController = new AbortController();
1177
+ sendBtn.classList.add('stop');
1178
+ sendBtn.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>`;
1179
+ setStatus('Generating...', true);
1180
+
1181
+ createStreamMessage();
1182
+
1183
+ let fullResponse = '';
1184
+ let thinkingContent = '';
1185
+ let mainContent = '';
1186
+ let inThinking = false;
1187
+ let thinkingDone = false;
1188
+ let toolCallsReceived = [];
1189
+ let buffer = '';
1190
+
1191
+ try {
1192
+ const response = await fetch('/v1/chat/completions', {
1193
+ method: 'POST',
1194
+ headers: { 'Content-Type': 'application/json' },
1195
+ body: JSON.stringify(reqBody),
1196
+ signal: abortController.signal
1197
+ });
1198
+
1199
+ if (!response.ok) {
1200
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1201
+ }
1202
+
1203
+ const reader = response.body.getReader();
1204
+ const decoder = new TextDecoder();
1205
+
1206
+ while (true) {
1207
+ const { done, value } = await reader.read();
1208
+ if (done) break;
1209
+
1210
+ buffer += decoder.decode(value, { stream: true });
1211
+ const lines = buffer.split('\n');
1212
+ buffer = lines.pop() || '';
1213
+
1214
+ for (const line of lines) {
1215
+ const trimmed = line.trim();
1216
+ if (!trimmed || !trimmed.startsWith('data:')) continue;
1217
+
1218
+ const data = trimmed.slice(5).trim();
1219
+ if (data === '[DONE]') continue;
1220
+
1221
+ try {
1222
+ const chunk = JSON.parse(data);
1223
+ const delta = chunk.choices?.[0]?.delta;
1224
+ const finishReason = chunk.choices?.[0]?.finish_reason;
1225
+
1226
+ if (delta?.tool_calls) {
1227
+ for (const tc of delta.tool_calls) {
1228
+ toolCallsReceived.push(tc);
1229
+ }
1230
+ }
1231
+
1232
+ if (delta?.content) {
1233
+ const content = delta.content;
1234
+ fullResponse += content;
1235
+
1236
+ // Process thinking tags
1237
+ processStreamContent(content);
1238
+ }
1239
+
1240
+ if (finishReason) {
1241
+ finishStream(finishReason, toolCallsReceived);
1242
+ }
1243
+ } catch (e) {
1244
+ // Skip malformed chunks
1245
+ }
1246
+ }
1247
+ }
1248
+ } catch (e) {
1249
+ if (e.name === 'AbortError') {
1250
+ appendToStream('\n\n*[Generation stopped]*');
1251
+ } else {
1252
+ appendToStream(`\n\n**Error:** ${e.message}`);
1253
+ }
1254
+ }
1255
+
1256
+ // Finalize
1257
+ isGenerating = false;
1258
+ abortController = null;
1259
+ sendBtn.classList.remove('stop');
1260
+ sendBtn.innerHTML = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z"/></svg>`;
1261
+ setStatus('Ready');
1262
+
1263
+ // Remove cursor
1264
+ const cursor = document.querySelector('.cursor');
1265
+ if (cursor) cursor.remove();
1266
+
1267
+ // Save to history
1268
+ if (mainContent || fullResponse) {
1269
+ chatHistory.push({
1270
+ role: 'assistant',
1271
+ content: mainContent || extractMainContent(fullResponse)
1272
+ });
1273
+ }
1274
+ }
1275
+
1276
+ // Stream content processing state
1277
+ let streamBuffer = '';
1278
+ let streamInThinking = false;
1279
+ let streamThinkingContent = '';
1280
+ let streamMainContent = '';
1281
+ let thinkingBlockCreated = false;
1282
+
1283
+ function resetStreamState() {
1284
+ streamBuffer = '';
1285
+ streamInThinking = false;
1286
+ streamThinkingContent = '';
1287
+ streamMainContent = '';
1288
+ thinkingBlockCreated = false;
1289
+ }
1290
+
1291
+ // Reset on new message
1292
+ function createStreamMessageWrapped() {
1293
+ resetStreamState();
1294
+ return createStreamMessage();
1295
+ }
1296
+
1297
+ // Override createStreamMessage
1298
+ const origCreateStreamMessage = createStreamMessage;
1299
+ createStreamMessage = function() {
1300
+ resetStreamState();
1301
+ return origCreateStreamMessage();
1302
+ };
1303
+
1304
+ function processStreamContent(newContent) {
1305
+ streamBuffer += newContent;
1306
+ renderStreamBuffer();
1307
+ }
1308
+
1309
+ function renderStreamBuffer() {
1310
+ if (!currentStreamEl) return;
1311
+
1312
+ let text = streamBuffer;
1313
+ let html = '';
1314
+
1315
+ // Check for <think> tags
1316
+ const thinkStartIdx = text.indexOf('<think>');
1317
+ const thinkEndIdx = text.indexOf('</think>');
1318
+
1319
+ if (thinkStartIdx !== -1 && thinkEndIdx !== -1) {
1320
+ // Complete thinking block
1321
+ const beforeThink = text.substring(0, thinkStartIdx);
1322
+ const thinkContent = text.substring(thinkStartIdx + 7, thinkEndIdx);
1323
+ const afterThink = text.substring(thinkEndIdx + 8);
1324
+
1325
+ streamThinkingContent = thinkContent;
1326
+ streamMainContent = afterThink;
1327
+
1328
+ html = createThinkingBlock(thinkContent, true);
1329
+ if (afterThink.trim()) {
1330
+ html += renderMarkdown(afterThink);
1331
+ }
1332
+ html += '<span class="cursor"></span>';
1333
+
1334
+ } else if (thinkStartIdx !== -1 && thinkEndIdx === -1) {
1335
+ // Thinking in progress
1336
+ const thinkContent = text.substring(thinkStartIdx + 7);
1337
+ streamThinkingContent = thinkContent;
1338
+
1339
+ const id = 'stream-think';
1340
+ html = `
1341
+ <div class="thinking-block">
1342
+ <div class="thinking-header" onclick="toggleThinking('${id}')">
1343
+ <div class="thinking-header-left">
1344
+ <span class="thinking-icon">πŸ’­</span>
1345
+ <span>Thinking...</span>
1346
+ </div>
1347
+ <span class="thinking-toggle" id="${id}-toggle">β–Ό</span>
1348
+ </div>
1349
+ <div class="thinking-content" id="${id}">${escapeHtml(thinkContent)}<span class="cursor"></span></div>
1350
+ </div>`;
1351
+
1352
+ } else {
1353
+ // No thinking tags, render as markdown
1354
+ streamMainContent = text;
1355
+ html = renderMarkdown(text) + '<span class="cursor"></span>';
1356
+ }
1357
+
1358
+ currentStreamEl.innerHTML = html;
1359
+ scrollToBottom();
1360
+ }
1361
+
1362
+ function appendToStream(content) {
1363
+ if (currentStreamEl) {
1364
+ streamBuffer += content;
1365
+ renderStreamBuffer();
1366
+ }
1367
+ }
1368
+
1369
+ function extractMainContent(text) {
1370
+ // Remove thinking blocks
1371
+ let result = text.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
1372
+ // Remove tool call blocks
1373
+ result = result.replace(/<tool_call>[\s\S]*?<\/tool_call>/g, '').trim();
1374
+ return result;
1375
+ }
1376
+
1377
+ function finishStream(reason, toolCalls) {
1378
+ if (!currentStreamEl) return;
1379
+
1380
+ // Remove cursor
1381
+ const cursor = currentStreamEl.querySelector('.cursor');
1382
+ if (cursor) cursor.remove();
1383
+
1384
+ // Update thinking block to show "done"
1385
+ const thinkingIcon = currentStreamEl.querySelector('.thinking-icon');
1386
+ if (thinkingIcon) {
1387
+ thinkingIcon.classList.add('done');
1388
+ const label = thinkingIcon.nextElementSibling;
1389
+ if (label) label.textContent = 'Thought process';
1390
+ }
1391
+
1392
+ // Collapse thinking by default when done
1393
+ const thinkContent = currentStreamEl.querySelector('.thinking-content');
1394
+ if (thinkContent && !thinkContent.classList.contains('hidden')) {
1395
+ thinkContent.classList.add('hidden');
1396
+ const toggleEl = document.getElementById(thinkContent.id + '-toggle');
1397
+ if (toggleEl) toggleEl.classList.add('collapsed');
1398
+ }
1399
+
1400
+ // Add tool calls
1401
+ if (toolCalls && toolCalls.length > 0) {
1402
+ for (const tc of toolCalls) {
1403
+ const toolHtml = createToolCallBlock(tc);
1404
+ currentStreamEl.insertAdjacentHTML('beforeend', toolHtml);
1405
+ }
1406
+ }
1407
+
1408
+ mainContent = streamMainContent;
1409
+ scrollToBottom();
1410
+ }
1411
+
1412
+ function stopGeneration() {
1413
+ if (abortController) {
1414
+ abortController.abort();
1415
+ }
1416
+ }
1417
+
1418
+ // ─── Initialize ─────────────────────────────────────────────────
1419
+ document.addEventListener('DOMContentLoaded', () => {
1420
+ userInput.focus();
1421
+
1422
+ // Check API health
1423
+ fetch('/health')
1424
+ .then(r => r.json())
1425
+ .then(d => {
1426
+ setStatus(`Connected Β· ${d.model}`);
1427
+ })
1428
+ .catch(() => {
1429
+ setStatus('Connecting...', true);
1430
+ });
1431
+ });
1432
+ </script>
1433
+ </body>
1434
+ </html>