Ashok75 commited on
Commit
8986cf8
·
verified ·
1 Parent(s): 3db39aa

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +2 -0
  2. index.html +800 -795
app.py CHANGED
@@ -185,6 +185,8 @@ async def stream_tokens(prompt: str, max_tokens: int, temperature: float, tools:
185
  # Yield SSE event immediately (no buffering)
186
  data = json.dumps({"type": "token", "content": new_text})
187
  yield f"data: {data}\n\n"
 
 
188
  logger.debug(f"Token {token_count}: {repr(new_text[:20])}...")
189
 
190
  # Log generation stats
 
185
  # Yield SSE event immediately (no buffering)
186
  data = json.dumps({"type": "token", "content": new_text})
187
  yield f"data: {data}\n\n"
188
+ # let the event loop schedule a send/flush so proxies don't buffer
189
+ await asyncio.sleep(0)
190
  logger.debug(f"Token {token_count}: {repr(new_text[:20])}...")
191
 
192
  # Log generation stats
index.html CHANGED
@@ -1,796 +1,801 @@
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>Nanbeige4.1-3B Chat</title>
7
- <style>
8
- * {
9
- margin: 0;
10
- padding: 0;
11
- box-sizing: border-box;
12
- }
13
-
14
- :root {
15
- --bg-primary: #0f0f0f;
16
- --bg-secondary: #1a1a1a;
17
- --bg-tertiary: #252525;
18
- --accent: #00d4aa;
19
- --accent-hover: #00b894;
20
- --text-primary: #ffffff;
21
- --text-secondary: #a0a0a0;
22
- --border: #333333;
23
- --user-msg: #1a3d35;
24
- --assistant-msg: #252525;
25
- --error: #ff4757;
26
- --shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
27
- }
28
-
29
- body {
30
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
31
- background: var(--bg-primary);
32
- color: var(--text-primary);
33
- height: 100vh;
34
- display: flex;
35
- flex-direction: column;
36
- overflow: hidden;
37
- }
38
-
39
- /* Header */
40
- .header {
41
- background: var(--bg-secondary);
42
- border-bottom: 1px solid var(--border);
43
- padding: 1rem 1.5rem;
44
- display: flex;
45
- align-items: center;
46
- justify-content: space-between;
47
- box-shadow: var(--shadow);
48
- z-index: 10;
49
- }
50
-
51
- .logo {
52
- display: flex;
53
- align-items: center;
54
- gap: 0.75rem;
55
- }
56
-
57
- .logo-icon {
58
- width: 36px;
59
- height: 36px;
60
- background: linear-gradient(135deg, var(--accent), var(--accent-hover));
61
- border-radius: 10px;
62
- display: flex;
63
- align-items: center;
64
- justify-content: center;
65
- font-weight: bold;
66
- font-size: 1.2rem;
67
- }
68
-
69
- .logo-text {
70
- font-size: 1.25rem;
71
- font-weight: 600;
72
- letter-spacing: -0.5px;
73
- }
74
-
75
- .logo-text span {
76
- color: var(--accent);
77
- }
78
-
79
- .status {
80
- display: flex;
81
- align-items: center;
82
- gap: 0.5rem;
83
- font-size: 0.875rem;
84
- color: var(--text-secondary);
85
- }
86
-
87
- .status-dot {
88
- width: 8px;
89
- height: 8px;
90
- border-radius: 50%;
91
- background: var(--text-secondary);
92
- transition: background 0.3s ease;
93
- }
94
-
95
- .status-dot.online {
96
- background: var(--accent);
97
- box-shadow: 0 0 10px var(--accent);
98
- }
99
-
100
- .status-dot.error {
101
- background: var(--error);
102
- box-shadow: 0 0 10px var(--error);
103
- }
104
-
105
- /* Main Chat Area */
106
- .chat-container {
107
- flex: 1;
108
- overflow-y: auto;
109
- padding: 2rem;
110
- display: flex;
111
- flex-direction: column;
112
- gap: 1.5rem;
113
- scroll-behavior: smooth;
114
- }
115
-
116
- .chat-container::-webkit-scrollbar {
117
- width: 8px;
118
- }
119
-
120
- .chat-container::-webkit-scrollbar-track {
121
- background: transparent;
122
- }
123
-
124
- .chat-container::-webkit-scrollbar-thumb {
125
- background: var(--border);
126
- border-radius: 4px;
127
- }
128
-
129
- .chat-container::-webkit-scrollbar-thumb:hover {
130
- background: var(--text-secondary);
131
- }
132
-
133
- .welcome-message {
134
- text-align: center;
135
- padding: 3rem 1rem;
136
- color: var(--text-secondary);
137
- }
138
-
139
- .welcome-message h2 {
140
- color: var(--text-primary);
141
- margin-bottom: 0.5rem;
142
- font-size: 1.5rem;
143
- }
144
-
145
- .message {
146
- display: flex;
147
- gap: 1rem;
148
- max-width: 900px;
149
- margin: 0 auto;
150
- width: 100%;
151
- animation: fadeIn 0.3s ease;
152
- }
153
-
154
- @keyframes fadeIn {
155
- from { opacity: 0; transform: translateY(10px); }
156
- to { opacity: 1; transform: translateY(0); }
157
- }
158
-
159
- .message-avatar {
160
- width: 36px;
161
- height: 36px;
162
- border-radius: 50%;
163
- display: flex;
164
- align-items: center;
165
- justify-content: center;
166
- font-size: 1rem;
167
- flex-shrink: 0;
168
- font-weight: 600;
169
- }
170
-
171
- .user-message .message-avatar {
172
- background: var(--accent);
173
- color: var(--bg-primary);
174
- }
175
-
176
- .assistant-message .message-avatar {
177
- background: var(--bg-tertiary);
178
- border: 1px solid var(--border);
179
- }
180
-
181
- .message-content {
182
- flex: 1;
183
- padding: 1rem 1.25rem;
184
- border-radius: 12px;
185
- line-height: 1.6;
186
- font-size: 0.95rem;
187
- white-space: pre-wrap;
188
- word-wrap: break-word;
189
- }
190
-
191
- .user-message .message-content {
192
- background: var(--user-msg);
193
- border: 1px solid rgba(0, 212, 170, 0.2);
194
- }
195
-
196
- .assistant-message .message-content {
197
- background: var(--assistant-msg);
198
- border: 1px solid var(--border);
199
- }
200
-
201
- .message-content.loading {
202
- display: flex;
203
- align-items: center;
204
- gap: 0.5rem;
205
- }
206
-
207
- .typing-indicator {
208
- display: flex;
209
- gap: 4px;
210
- }
211
-
212
- .typing-indicator span {
213
- width: 6px;
214
- height: 6px;
215
- background: var(--text-secondary);
216
- border-radius: 50%;
217
- animation: bounce 1.4s infinite ease-in-out both;
218
- }
219
-
220
- .typing-indicator span:nth-child(1) { animation-delay: -0.32s; }
221
- .typing-indicator span:nth-child(2) { animation-delay: -0.16s; }
222
-
223
- @keyframes bounce {
224
- 0%, 80%, 100% { transform: scale(0); }
225
- 40% { transform: scale(1); }
226
- }
227
-
228
- /* Input Area */
229
- .input-container {
230
- background: var(--bg-secondary);
231
- border-top: 1px solid var(--border);
232
- padding: 1.5rem;
233
- display: flex;
234
- justify-content: center;
235
- }
236
-
237
- .input-wrapper {
238
- max-width: 900px;
239
- width: 100%;
240
- position: relative;
241
- display: flex;
242
- gap: 0.75rem;
243
- align-items: flex-end;
244
- background: var(--bg-tertiary);
245
- border: 1px solid var(--border);
246
- border-radius: 16px;
247
- padding: 0.75rem;
248
- transition: border-color 0.2s ease, box-shadow 0.2s ease;
249
- }
250
-
251
- .input-wrapper:focus-within {
252
- border-color: var(--accent);
253
- box-shadow: 0 0 0 3px rgba(0, 212, 170, 0.1);
254
- }
255
-
256
- textarea {
257
- flex: 1;
258
- background: transparent;
259
- border: none;
260
- color: var(--text-primary);
261
- font-size: 0.95rem;
262
- resize: none;
263
- max-height: 200px;
264
- min-height: 24px;
265
- font-family: inherit;
266
- line-height: 1.5;
267
- padding: 0.25rem 0.5rem;
268
- }
269
-
270
- textarea:focus {
271
- outline: none;
272
- }
273
-
274
- textarea::placeholder {
275
- color: var(--text-secondary);
276
- }
277
-
278
- .send-btn {
279
- background: var(--accent);
280
- color: var(--bg-primary);
281
- border: none;
282
- width: 36px;
283
- height: 36px;
284
- border-radius: 10px;
285
- cursor: pointer;
286
- display: flex;
287
- align-items: center;
288
- justify-content: center;
289
- transition: all 0.2s ease;
290
- flex-shrink: 0;
291
- }
292
-
293
- .send-btn:hover:not(:disabled) {
294
- background: var(--accent-hover);
295
- transform: scale(1.05);
296
- }
297
-
298
- .send-btn:disabled {
299
- opacity: 0.5;
300
- cursor: not-allowed;
301
- }
302
-
303
- .send-btn svg {
304
- width: 18px;
305
- height: 18px;
306
- }
307
-
308
- /* Settings Panel */
309
- .settings-btn {
310
- background: transparent;
311
- border: 1px solid var(--border);
312
- color: var(--text-secondary);
313
- padding: 0.5rem 1rem;
314
- border-radius: 8px;
315
- cursor: pointer;
316
- font-size: 0.875rem;
317
- transition: all 0.2s ease;
318
- }
319
-
320
- .settings-btn:hover {
321
- border-color: var(--accent);
322
- color: var(--accent);
323
- }
324
-
325
- .settings-panel {
326
- position: fixed;
327
- top: 50%;
328
- left: 50%;
329
- transform: translate(-50%, -50%) scale(0.95);
330
- background: var(--bg-secondary);
331
- border: 1px solid var(--border);
332
- border-radius: 16px;
333
- padding: 1.5rem;
334
- width: 90%;
335
- max-width: 400px;
336
- box-shadow: var(--shadow);
337
- opacity: 0;
338
- visibility: hidden;
339
- transition: all 0.2s ease;
340
- z-index: 100;
341
- }
342
-
343
- .settings-panel.active {
344
- opacity: 1;
345
- visibility: visible;
346
- transform: translate(-50%, -50%) scale(1);
347
- }
348
-
349
- .settings-overlay {
350
- position: fixed;
351
- top: 0;
352
- left: 0;
353
- right: 0;
354
- bottom: 0;
355
- background: rgba(0, 0, 0, 0.7);
356
- opacity: 0;
357
- visibility: hidden;
358
- transition: all 0.2s ease;
359
- z-index: 99;
360
- }
361
-
362
- .settings-overlay.active {
363
- opacity: 1;
364
- visibility: visible;
365
- }
366
-
367
- .settings-header {
368
- display: flex;
369
- justify-content: space-between;
370
- align-items: center;
371
- margin-bottom: 1.5rem;
372
- }
373
-
374
- .settings-title {
375
- font-size: 1.25rem;
376
- font-weight: 600;
377
- }
378
-
379
- .close-settings {
380
- background: none;
381
- border: none;
382
- color: var(--text-secondary);
383
- cursor: pointer;
384
- font-size: 1.5rem;
385
- line-height: 1;
386
- }
387
-
388
- .setting-item {
389
- margin-bottom: 1.25rem;
390
- }
391
-
392
- .setting-label {
393
- display: block;
394
- font-size: 0.875rem;
395
- color: var(--text-secondary);
396
- margin-bottom: 0.5rem;
397
- }
398
-
399
- .setting-input {
400
- width: 100%;
401
- background: var(--bg-tertiary);
402
- border: 1px solid var(--border);
403
- color: var(--text-primary);
404
- padding: 0.75rem;
405
- border-radius: 8px;
406
- font-size: 0.9rem;
407
- }
408
-
409
- .setting-input:focus {
410
- outline: none;
411
- border-color: var(--accent);
412
- }
413
-
414
- /* Code blocks */
415
- pre {
416
- background: var(--bg-primary);
417
- border: 1px solid var(--border);
418
- border-radius: 8px;
419
- padding: 1rem;
420
- overflow-x: auto;
421
- margin: 0.75rem 0;
422
- }
423
-
424
- code {
425
- font-family: 'Courier New', monospace;
426
- font-size: 0.9em;
427
- }
428
-
429
- pre code {
430
- color: #e0e0e0;
431
- }
432
-
433
- /* Responsive */
434
- @media (max-width: 768px) {
435
- .chat-container {
436
- padding: 1rem;
437
- }
438
-
439
- .header {
440
- padding: 1rem;
441
- }
442
-
443
- .logo-text {
444
- font-size: 1rem;
445
- }
446
- }
447
-
448
- /* Empty state suggestions */
449
- .suggestions {
450
- display: grid;
451
- grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
452
- gap: 1rem;
453
- max-width: 800px;
454
- margin: 2rem auto;
455
- padding: 0 1rem;
456
- }
457
-
458
- .suggestion-card {
459
- background: var(--bg-secondary);
460
- border: 1px solid var(--border);
461
- border-radius: 12px;
462
- padding: 1rem;
463
- cursor: pointer;
464
- transition: all 0.2s ease;
465
- text-align: left;
466
- }
467
-
468
- .suggestion-card:hover {
469
- border-color: var(--accent);
470
- transform: translateY(-2px);
471
- box-shadow: 0 4px 12px rgba(0, 212, 170, 0.1);
472
- }
473
-
474
- .suggestion-card h4 {
475
- color: var(--accent);
476
- margin-bottom: 0.5rem;
477
- font-size: 0.9rem;
478
- }
479
-
480
- .suggestion-card p {
481
- color: var(--text-secondary);
482
- font-size: 0.85rem;
483
- line-height: 1.4;
484
- }
485
- </style>
486
- </head>
487
- <body>
488
- <header class="header">
489
- <div class="logo">
490
- <div class="logo-icon">N</div>
491
- <div class="logo-text">Nanbeige<span>4.1-3B</span></div>
492
- </div>
493
- <div style="display: flex; gap: 1rem; align-items: center;">
494
- <div class="status">
495
- <div class="status-dot" id="statusDot"></div>
496
- <span id="statusText">Checking...</span>
497
- </div>
498
- <button class="settings-btn" onclick="toggleSettings()">⚙️ Settings</button>
499
- <button class="settings-btn" onclick="clearChat()">🗑️ Clear</button>
500
- </div>
501
- </header>
502
-
503
- <div class="settings-overlay" id="settingsOverlay" onclick="toggleSettings()"></div>
504
- <div class="settings-panel" id="settingsPanel">
505
- <div class="settings-header">
506
- <h3 class="settings-title">Settings</h3>
507
- <button class="close-settings" onclick="toggleSettings()">×</button>
508
- </div>
509
- <div class="setting-item">
510
- <label class="setting-label">Temperature (0.0 - 2.0)</label>
511
- <input type="number" class="setting-input" id="temperature" min="0" max="2" step="0.1" value="0.6">
512
- </div>
513
- <div class="setting-item">
514
- <label class="setting-label">Max Tokens (1 - 4096)</label>
515
- <input type="number" class="setting-input" id="maxTokens" min="1" max="4096" step="1" value="2048">
516
- </div>
517
- <div class="setting-item">
518
- <label class="setting-label">System Prompt</label>
519
- <textarea class="setting-input" id="systemPrompt" rows="3" placeholder="Optional system instructions..."></textarea>
520
- </div>
521
- </div>
522
-
523
- <main class="chat-container" id="chatContainer">
524
- <div class="welcome-message" id="welcomeMessage">
525
- <h2>Welcome to Nanbeige4.1-3B</h2>
526
- <p>A powerful language model for conversation and assistance</p>
527
-
528
- <div class="suggestions">
529
- <div class="suggestion-card" onclick="sendSuggestion('Explain quantum computing in simple terms')">
530
- <h4>💡 Explain a concept</h4>
531
- <p>"Explain quantum computing in simple terms"</p>
532
- </div>
533
- <div class="suggestion-card" onclick="sendSuggestion('Write a Python function to calculate fibonacci numbers')">
534
- <h4>💻 Code assistance</h4>
535
- <p>"Write a Python function to calculate fibonacci numbers"</p>
536
- </div>
537
- <div class="suggestion-card" onclick="sendSuggestion('Help me brainstorm ideas for a sci-fi short story')">
538
- <h4> Creative writing</h4>
539
- <p>"Help me brainstorm ideas for a sci-fi short story"</p>
540
- </div>
541
- <div class="suggestion-card" onclick="sendSuggestion('What are the latest developments in AI?')">
542
- <h4>🤖 General knowledge</h4>
543
- <p>"What are the latest developments in AI?"</p>
544
- </div>
545
- </div>
546
- </div>
547
- </main>
548
-
549
- <div class="input-container">
550
- <div class="input-wrapper">
551
- <textarea
552
- id="messageInput"
553
- placeholder="Type your message..."
554
- rows="1"
555
- onkeydown="handleKeyDown(event)"
556
- oninput="autoResize(this)"
557
- ></textarea>
558
- <button class="send-btn" id="sendBtn" onclick="sendMessage()">
559
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
560
- <path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z"/>
561
- </svg>
562
- </button>
563
- </div>
564
- </div>
565
-
566
- <script>
567
- let messages = [];
568
- let isStreaming = false;
569
- const API_BASE = window.location.origin;
570
-
571
- // Check health on load
572
- checkHealth();
573
- setInterval(checkHealth, 30000);
574
-
575
- async function checkHealth() {
576
- try {
577
- const response = await fetch(`${API_BASE}/health`);
578
- const data = await response.json();
579
- const dot = document.getElementById('statusDot');
580
- const text = document.getElementById('statusText');
581
-
582
- if (data.model_loaded) {
583
- dot.className = 'status-dot online';
584
- text.textContent = 'Online';
585
- } else {
586
- dot.className = 'status-dot';
587
- text.textContent = 'Loading...';
588
- }
589
- } catch (error) {
590
- document.getElementById('statusDot').className = 'status-dot error';
591
- document.getElementById('statusText').textContent = 'Offline';
592
- }
593
- }
594
-
595
- function toggleSettings() {
596
- document.getElementById('settingsPanel').classList.toggle('active');
597
- document.getElementById('settingsOverlay').classList.toggle('active');
598
- }
599
-
600
- function clearChat() {
601
- messages = [];
602
- document.getElementById('chatContainer').innerHTML = `
603
- <div class="welcome-message" id="welcomeMessage">
604
- <h2>Welcome to Nanbeige4.1-3B</h2>
605
- <p>Start a new conversation</p>
606
- </div>
607
- `;
608
- }
609
-
610
- function autoResize(textarea) {
611
- textarea.style.height = 'auto';
612
- textarea.style.height = Math.min(textarea.scrollHeight, 200) + 'px';
613
- }
614
-
615
- function handleKeyDown(e) {
616
- if (e.key === 'Enter' && !e.shiftKey) {
617
- e.preventDefault();
618
- sendMessage();
619
- }
620
- }
621
-
622
- function sendSuggestion(text) {
623
- document.getElementById('messageInput').value = text;
624
- sendMessage();
625
- }
626
-
627
- async function sendMessage() {
628
- const input = document.getElementById('messageInput');
629
- const text = input.value.trim();
630
-
631
- if (!text || isStreaming) return;
632
-
633
- // Hide welcome message
634
- const welcome = document.getElementById('welcomeMessage');
635
- if (welcome) welcome.style.display = 'none';
636
-
637
- // Add user message
638
- addMessage('user', text);
639
- messages.push({ role: 'user', content: text });
640
-
641
- // Clear input
642
- input.value = '';
643
- input.style.height = 'auto';
644
-
645
- // Show loading
646
- isStreaming = true;
647
- document.getElementById('sendBtn').disabled = true;
648
- const loadingId = addLoadingMessage();
649
-
650
- try {
651
- const temperature = parseFloat(document.getElementById('temperature').value) || 0.6;
652
- const maxTokens = parseInt(document.getElementById('maxTokens').value) || 2048;
653
- const systemPrompt = document.getElementById('systemPrompt').value.trim();
654
-
655
- let chatMessages = [...messages];
656
- if (systemPrompt && messages.length === 1) {
657
- chatMessages.unshift({ role: 'system', content: systemPrompt });
658
- }
659
-
660
- const response = await fetch(`${API_BASE}/chat`, {
661
- method: 'POST',
662
- headers: { 'Content-Type': 'application/json' },
663
- body: JSON.stringify({
664
- messages: chatMessages,
665
- stream: true,
666
- temperature: temperature,
667
- max_tokens: maxTokens
668
- })
669
- });
670
-
671
- if (!response.ok) throw new Error('Failed to get response');
672
-
673
- // Remove loading, prepare for streaming
674
- removeLoadingMessage(loadingId);
675
- const assistantId = addMessage('assistant', '');
676
-
677
- const reader = response.body.getReader();
678
- const decoder = new TextDecoder();
679
- let buffer = '';
680
- let fullContent = '';
681
-
682
- while (true) {
683
- const { done, value } = await reader.read();
684
- if (done) break;
685
-
686
- buffer += decoder.decode(value, { stream: true });
687
- const lines = buffer.split('\n');
688
- buffer = lines.pop();
689
-
690
- for (const line of lines) {
691
- if (line.startsWith('data: ')) {
692
- try {
693
- const data = JSON.parse(line.slice(6));
694
- if (data.type === 'token') {
695
- fullContent += data.content;
696
- updateMessage(assistantId, fullContent);
697
- } else if (data.type === 'done') {
698
- messages.push({ role: 'assistant', content: fullContent });
699
- }
700
- } catch (e) {
701
- console.error('Parse error:', e);
702
- }
703
- }
704
- }
705
- }
706
-
707
- } catch (error) {
708
- removeLoadingMessage(loadingId);
709
- addMessage('assistant', `Error: ${error.message}. Please check if the API is running.`);
710
- } finally {
711
- isStreaming = false;
712
- document.getElementById('sendBtn').disabled = false;
713
- }
714
- }
715
-
716
- function addMessage(role, content) {
717
- const container = document.getElementById('chatContainer');
718
- const id = 'msg-' + Date.now();
719
-
720
- const div = document.createElement('div');
721
- div.className = `message ${role}-message`;
722
- div.id = id;
723
- div.innerHTML = `
724
- <div class="message-avatar">${role === 'user' ? 'U' : '🤖'}</div>
725
- <div class="message-content">${formatContent(content)}</div>
726
- `;
727
-
728
- container.appendChild(div);
729
- scrollToBottom();
730
- return id;
731
- }
732
-
733
- function updateMessage(id, content) {
734
- const msg = document.getElementById(id);
735
- if (msg) {
736
- msg.querySelector('.message-content').innerHTML = formatContent(content);
737
- scrollToBottom();
738
- }
739
- }
740
-
741
- function addLoadingMessage() {
742
- const id = 'loading-' + Date.now();
743
- const container = document.getElementById('chatContainer');
744
-
745
- const div = document.createElement('div');
746
- div.className = 'message assistant-message';
747
- div.id = id;
748
- div.innerHTML = `
749
- <div class="message-avatar">🤖</div>
750
- <div class="message-content loading">
751
- <div class="typing-indicator">
752
- <span></span>
753
- <span></span>
754
- <span></span>
755
- </div>
756
- </div>
757
- `;
758
-
759
- container.appendChild(div);
760
- scrollToBottom();
761
- return id;
762
- }
763
-
764
- function removeLoadingMessage(id) {
765
- const el = document.getElementById(id);
766
- if (el) el.remove();
767
- }
768
-
769
- function formatContent(text) {
770
- // Escape HTML
771
- text = text.replace(/&/g, '&amp;')
772
- .replace(/</g, '&lt;')
773
- .replace(/>/g, '&gt;');
774
-
775
- // Format code blocks
776
- text = text.replace(/```(\w+)?\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>');
777
- text = text.replace(/`([^`]+)`/g, '<code>$1</code>');
778
-
779
- // Format bold and italic
780
- text = text.replace(/\*\*\*(.*?)\*\*\*/g, '<strong><em>$1</em></strong>');
781
- text = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
782
- text = text.replace(/\*(.*?)\*/g, '<em>$1</em>');
783
-
784
- // Convert newlines to <br> outside of pre blocks
785
- text = text.replace(/\n/g, '<br>');
786
-
787
- return text;
788
- }
789
-
790
- function scrollToBottom() {
791
- const container = document.getElementById('chatContainer');
792
- container.scrollTop = container.scrollHeight;
793
- }
794
- </script>
795
- </body>
 
 
 
 
 
796
  </html>
 
1
+ <!-- this is a test chat for app.py in space to test the response and
2
+ streaming capabilities of the app.py return response and llm -->
3
+
4
+ <!DOCTYPE html>
5
+ <html lang="en">
6
+ <head>
7
+ <meta charset="UTF-8">
8
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
9
+ <title>Nanbeige4.1-3B Chat</title>
10
+ <style>
11
+ * {
12
+ margin: 0;
13
+ padding: 0;
14
+ box-sizing: border-box;
15
+ }
16
+
17
+ :root {
18
+ --bg-primary: #0f0f0f;
19
+ --bg-secondary: #1a1a1a;
20
+ --bg-tertiary: #252525;
21
+ --accent: #00d4aa;
22
+ --accent-hover: #00b894;
23
+ --text-primary: #ffffff;
24
+ --text-secondary: #a0a0a0;
25
+ --border: #333333;
26
+ --user-msg: #1a3d35;
27
+ --assistant-msg: #252525;
28
+ --error: #ff4757;
29
+ --shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
30
+ }
31
+
32
+ body {
33
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
34
+ background: var(--bg-primary);
35
+ color: var(--text-primary);
36
+ height: 100vh;
37
+ display: flex;
38
+ flex-direction: column;
39
+ overflow: hidden;
40
+ }
41
+
42
+ /* Header */
43
+ .header {
44
+ background: var(--bg-secondary);
45
+ border-bottom: 1px solid var(--border);
46
+ padding: 1rem 1.5rem;
47
+ display: flex;
48
+ align-items: center;
49
+ justify-content: space-between;
50
+ box-shadow: var(--shadow);
51
+ z-index: 10;
52
+ }
53
+
54
+ .logo {
55
+ display: flex;
56
+ align-items: center;
57
+ gap: 0.75rem;
58
+ }
59
+
60
+ .logo-icon {
61
+ width: 36px;
62
+ height: 36px;
63
+ background: linear-gradient(135deg, var(--accent), var(--accent-hover));
64
+ border-radius: 10px;
65
+ display: flex;
66
+ align-items: center;
67
+ justify-content: center;
68
+ font-weight: bold;
69
+ font-size: 1.2rem;
70
+ }
71
+
72
+ .logo-text {
73
+ font-size: 1.25rem;
74
+ font-weight: 600;
75
+ letter-spacing: -0.5px;
76
+ }
77
+
78
+ .logo-text span {
79
+ color: var(--accent);
80
+ }
81
+
82
+ .status {
83
+ display: flex;
84
+ align-items: center;
85
+ gap: 0.5rem;
86
+ font-size: 0.875rem;
87
+ color: var(--text-secondary);
88
+ }
89
+
90
+ .status-dot {
91
+ width: 8px;
92
+ height: 8px;
93
+ border-radius: 50%;
94
+ background: var(--text-secondary);
95
+ transition: background 0.3s ease;
96
+ }
97
+
98
+ .status-dot.online {
99
+ background: var(--accent);
100
+ box-shadow: 0 0 10px var(--accent);
101
+ }
102
+
103
+ .status-dot.error {
104
+ background: var(--error);
105
+ box-shadow: 0 0 10px var(--error);
106
+ }
107
+
108
+ /* Main Chat Area */
109
+ .chat-container {
110
+ flex: 1;
111
+ overflow-y: auto;
112
+ padding: 2rem;
113
+ display: flex;
114
+ flex-direction: column;
115
+ gap: 1.5rem;
116
+ scroll-behavior: smooth;
117
+ }
118
+
119
+ .chat-container::-webkit-scrollbar {
120
+ width: 8px;
121
+ }
122
+
123
+ .chat-container::-webkit-scrollbar-track {
124
+ background: transparent;
125
+ }
126
+
127
+ .chat-container::-webkit-scrollbar-thumb {
128
+ background: var(--border);
129
+ border-radius: 4px;
130
+ }
131
+
132
+ .chat-container::-webkit-scrollbar-thumb:hover {
133
+ background: var(--text-secondary);
134
+ }
135
+
136
+ .welcome-message {
137
+ text-align: center;
138
+ padding: 3rem 1rem;
139
+ color: var(--text-secondary);
140
+ }
141
+
142
+ .welcome-message h2 {
143
+ color: var(--text-primary);
144
+ margin-bottom: 0.5rem;
145
+ font-size: 1.5rem;
146
+ }
147
+
148
+ .message {
149
+ display: flex;
150
+ gap: 1rem;
151
+ max-width: 900px;
152
+ margin: 0 auto;
153
+ width: 100%;
154
+ animation: fadeIn 0.3s ease;
155
+ }
156
+
157
+ @keyframes fadeIn {
158
+ from { opacity: 0; transform: translateY(10px); }
159
+ to { opacity: 1; transform: translateY(0); }
160
+ }
161
+
162
+ .message-avatar {
163
+ width: 36px;
164
+ height: 36px;
165
+ border-radius: 50%;
166
+ display: flex;
167
+ align-items: center;
168
+ justify-content: center;
169
+ font-size: 1rem;
170
+ flex-shrink: 0;
171
+ font-weight: 600;
172
+ }
173
+
174
+ .user-message .message-avatar {
175
+ background: var(--accent);
176
+ color: var(--bg-primary);
177
+ }
178
+
179
+ .assistant-message .message-avatar {
180
+ background: var(--bg-tertiary);
181
+ border: 1px solid var(--border);
182
+ }
183
+
184
+ .message-content {
185
+ flex: 1;
186
+ padding: 1rem 1.25rem;
187
+ border-radius: 12px;
188
+ line-height: 1.6;
189
+ font-size: 0.95rem;
190
+ white-space: pre-wrap;
191
+ word-wrap: break-word;
192
+ }
193
+
194
+ .user-message .message-content {
195
+ background: var(--user-msg);
196
+ border: 1px solid rgba(0, 212, 170, 0.2);
197
+ }
198
+
199
+ .assistant-message .message-content {
200
+ background: var(--assistant-msg);
201
+ border: 1px solid var(--border);
202
+ }
203
+
204
+ .message-content.loading {
205
+ display: flex;
206
+ align-items: center;
207
+ gap: 0.5rem;
208
+ }
209
+
210
+ .typing-indicator {
211
+ display: flex;
212
+ gap: 4px;
213
+ }
214
+
215
+ .typing-indicator span {
216
+ width: 6px;
217
+ height: 6px;
218
+ background: var(--text-secondary);
219
+ border-radius: 50%;
220
+ animation: bounce 1.4s infinite ease-in-out both;
221
+ }
222
+
223
+ .typing-indicator span:nth-child(1) { animation-delay: -0.32s; }
224
+ .typing-indicator span:nth-child(2) { animation-delay: -0.16s; }
225
+
226
+ @keyframes bounce {
227
+ 0%, 80%, 100% { transform: scale(0); }
228
+ 40% { transform: scale(1); }
229
+ }
230
+
231
+ /* Input Area */
232
+ .input-container {
233
+ background: var(--bg-secondary);
234
+ border-top: 1px solid var(--border);
235
+ padding: 1.5rem;
236
+ display: flex;
237
+ justify-content: center;
238
+ }
239
+
240
+ .input-wrapper {
241
+ max-width: 900px;
242
+ width: 100%;
243
+ position: relative;
244
+ display: flex;
245
+ gap: 0.75rem;
246
+ align-items: flex-end;
247
+ background: var(--bg-tertiary);
248
+ border: 1px solid var(--border);
249
+ border-radius: 16px;
250
+ padding: 0.75rem;
251
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
252
+ }
253
+
254
+ .input-wrapper:focus-within {
255
+ border-color: var(--accent);
256
+ box-shadow: 0 0 0 3px rgba(0, 212, 170, 0.1);
257
+ }
258
+
259
+ textarea {
260
+ flex: 1;
261
+ background: transparent;
262
+ border: none;
263
+ color: var(--text-primary);
264
+ font-size: 0.95rem;
265
+ resize: none;
266
+ max-height: 200px;
267
+ min-height: 24px;
268
+ font-family: inherit;
269
+ line-height: 1.5;
270
+ padding: 0.25rem 0.5rem;
271
+ }
272
+
273
+ textarea:focus {
274
+ outline: none;
275
+ }
276
+
277
+ textarea::placeholder {
278
+ color: var(--text-secondary);
279
+ }
280
+
281
+ .send-btn {
282
+ background: var(--accent);
283
+ color: var(--bg-primary);
284
+ border: none;
285
+ width: 36px;
286
+ height: 36px;
287
+ border-radius: 10px;
288
+ cursor: pointer;
289
+ display: flex;
290
+ align-items: center;
291
+ justify-content: center;
292
+ transition: all 0.2s ease;
293
+ flex-shrink: 0;
294
+ }
295
+
296
+ .send-btn:hover:not(:disabled) {
297
+ background: var(--accent-hover);
298
+ transform: scale(1.05);
299
+ }
300
+
301
+ .send-btn:disabled {
302
+ opacity: 0.5;
303
+ cursor: not-allowed;
304
+ }
305
+
306
+ .send-btn svg {
307
+ width: 18px;
308
+ height: 18px;
309
+ }
310
+
311
+ /* Settings Panel */
312
+ .settings-btn {
313
+ background: transparent;
314
+ border: 1px solid var(--border);
315
+ color: var(--text-secondary);
316
+ padding: 0.5rem 1rem;
317
+ border-radius: 8px;
318
+ cursor: pointer;
319
+ font-size: 0.875rem;
320
+ transition: all 0.2s ease;
321
+ }
322
+
323
+ .settings-btn:hover {
324
+ border-color: var(--accent);
325
+ color: var(--accent);
326
+ }
327
+
328
+ .settings-panel {
329
+ position: fixed;
330
+ top: 50%;
331
+ left: 50%;
332
+ transform: translate(-50%, -50%) scale(0.95);
333
+ background: var(--bg-secondary);
334
+ border: 1px solid var(--border);
335
+ border-radius: 16px;
336
+ padding: 1.5rem;
337
+ width: 90%;
338
+ max-width: 400px;
339
+ box-shadow: var(--shadow);
340
+ opacity: 0;
341
+ visibility: hidden;
342
+ transition: all 0.2s ease;
343
+ z-index: 100;
344
+ }
345
+
346
+ .settings-panel.active {
347
+ opacity: 1;
348
+ visibility: visible;
349
+ transform: translate(-50%, -50%) scale(1);
350
+ }
351
+
352
+ .settings-overlay {
353
+ position: fixed;
354
+ top: 0;
355
+ left: 0;
356
+ right: 0;
357
+ bottom: 0;
358
+ background: rgba(0, 0, 0, 0.7);
359
+ opacity: 0;
360
+ visibility: hidden;
361
+ transition: all 0.2s ease;
362
+ z-index: 99;
363
+ }
364
+
365
+ .settings-overlay.active {
366
+ opacity: 1;
367
+ visibility: visible;
368
+ }
369
+
370
+ .settings-header {
371
+ display: flex;
372
+ justify-content: space-between;
373
+ align-items: center;
374
+ margin-bottom: 1.5rem;
375
+ }
376
+
377
+ .settings-title {
378
+ font-size: 1.25rem;
379
+ font-weight: 600;
380
+ }
381
+
382
+ .close-settings {
383
+ background: none;
384
+ border: none;
385
+ color: var(--text-secondary);
386
+ cursor: pointer;
387
+ font-size: 1.5rem;
388
+ line-height: 1;
389
+ }
390
+
391
+ .setting-item {
392
+ margin-bottom: 1.25rem;
393
+ }
394
+
395
+ .setting-label {
396
+ display: block;
397
+ font-size: 0.875rem;
398
+ color: var(--text-secondary);
399
+ margin-bottom: 0.5rem;
400
+ }
401
+
402
+ .setting-input {
403
+ width: 100%;
404
+ background: var(--bg-tertiary);
405
+ border: 1px solid var(--border);
406
+ color: var(--text-primary);
407
+ padding: 0.75rem;
408
+ border-radius: 8px;
409
+ font-size: 0.9rem;
410
+ }
411
+
412
+ .setting-input:focus {
413
+ outline: none;
414
+ border-color: var(--accent);
415
+ }
416
+
417
+ /* Code blocks */
418
+ pre {
419
+ background: var(--bg-primary);
420
+ border: 1px solid var(--border);
421
+ border-radius: 8px;
422
+ padding: 1rem;
423
+ overflow-x: auto;
424
+ margin: 0.75rem 0;
425
+ }
426
+
427
+ code {
428
+ font-family: 'Courier New', monospace;
429
+ font-size: 0.9em;
430
+ }
431
+
432
+ pre code {
433
+ color: #e0e0e0;
434
+ }
435
+
436
+ /* Responsive */
437
+ @media (max-width: 768px) {
438
+ .chat-container {
439
+ padding: 1rem;
440
+ }
441
+
442
+ .header {
443
+ padding: 1rem;
444
+ }
445
+
446
+ .logo-text {
447
+ font-size: 1rem;
448
+ }
449
+ }
450
+
451
+ /* Empty state suggestions */
452
+ .suggestions {
453
+ display: grid;
454
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
455
+ gap: 1rem;
456
+ max-width: 800px;
457
+ margin: 2rem auto;
458
+ padding: 0 1rem;
459
+ }
460
+
461
+ .suggestion-card {
462
+ background: var(--bg-secondary);
463
+ border: 1px solid var(--border);
464
+ border-radius: 12px;
465
+ padding: 1rem;
466
+ cursor: pointer;
467
+ transition: all 0.2s ease;
468
+ text-align: left;
469
+ }
470
+
471
+ .suggestion-card:hover {
472
+ border-color: var(--accent);
473
+ transform: translateY(-2px);
474
+ box-shadow: 0 4px 12px rgba(0, 212, 170, 0.1);
475
+ }
476
+
477
+ .suggestion-card h4 {
478
+ color: var(--accent);
479
+ margin-bottom: 0.5rem;
480
+ font-size: 0.9rem;
481
+ }
482
+
483
+ .suggestion-card p {
484
+ color: var(--text-secondary);
485
+ font-size: 0.85rem;
486
+ line-height: 1.4;
487
+ }
488
+ </style>
489
+ </head>
490
+ <body>
491
+ <header class="header">
492
+ <div class="logo">
493
+ <div class="logo-icon">N</div>
494
+ <div class="logo-text">Nanbeige<span>4.1-3B</span></div>
495
+ </div>
496
+ <div style="display: flex; gap: 1rem; align-items: center;">
497
+ <div class="status">
498
+ <div class="status-dot" id="statusDot"></div>
499
+ <span id="statusText">Checking...</span>
500
+ </div>
501
+ <button class="settings-btn" onclick="toggleSettings()">⚙️ Settings</button>
502
+ <button class="settings-btn" onclick="clearChat()">🗑️ Clear</button>
503
+ </div>
504
+ </header>
505
+
506
+ <div class="settings-overlay" id="settingsOverlay" onclick="toggleSettings()"></div>
507
+ <div class="settings-panel" id="settingsPanel">
508
+ <div class="settings-header">
509
+ <h3 class="settings-title">Settings</h3>
510
+ <button class="close-settings" onclick="toggleSettings()">×</button>
511
+ </div>
512
+ <div class="setting-item">
513
+ <label class="setting-label">Temperature (0.0 - 2.0)</label>
514
+ <input type="number" class="setting-input" id="temperature" min="0" max="2" step="0.1" value="0.6">
515
+ </div>
516
+ <div class="setting-item">
517
+ <label class="setting-label">Max Tokens (1 - 4096)</label>
518
+ <input type="number" class="setting-input" id="maxTokens" min="1" max="4096" step="1" value="2048">
519
+ </div>
520
+ <div class="setting-item">
521
+ <label class="setting-label">System Prompt</label>
522
+ <textarea class="setting-input" id="systemPrompt" rows="3" placeholder="Optional system instructions..."></textarea>
523
+ </div>
524
+ </div>
525
+
526
+ <main class="chat-container" id="chatContainer">
527
+ <div class="welcome-message" id="welcomeMessage">
528
+ <h2>Welcome to Nanbeige4.1-3B</h2>
529
+ <p>A powerful language model for conversation and assistance</p>
530
+
531
+ <div class="suggestions">
532
+ <div class="suggestion-card" onclick="sendSuggestion('Explain quantum computing in simple terms')">
533
+ <h4>💡 Explain a concept</h4>
534
+ <p>"Explain quantum computing in simple terms"</p>
535
+ </div>
536
+ <div class="suggestion-card" onclick="sendSuggestion('Write a Python function to calculate fibonacci numbers')">
537
+ <h4>💻 Code assistance</h4>
538
+ <p>"Write a Python function to calculate fibonacci numbers"</p>
539
+ </div>
540
+ <div class="suggestion-card" onclick="sendSuggestion('Help me brainstorm ideas for a sci-fi short story')">
541
+ <h4>✨ Creative writing</h4>
542
+ <p>"Help me brainstorm ideas for a sci-fi short story"</p>
543
+ </div>
544
+ <div class="suggestion-card" onclick="sendSuggestion('What are the latest developments in AI?')">
545
+ <h4>🤖 General knowledge</h4>
546
+ <p>"What are the latest developments in AI?"</p>
547
+ </div>
548
+ </div>
549
+ </div>
550
+ </main>
551
+
552
+ <div class="input-container">
553
+ <div class="input-wrapper">
554
+ <textarea
555
+ id="messageInput"
556
+ placeholder="Type your message..."
557
+ rows="1"
558
+ onkeydown="handleKeyDown(event)"
559
+ oninput="autoResize(this)"
560
+ ></textarea>
561
+ <button class="send-btn" id="sendBtn" onclick="sendMessage()">
562
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
563
+ <path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z"/>
564
+ </svg>
565
+ </button>
566
+ </div>
567
+ </div>
568
+
569
+ <script>
570
+ let messages = [];
571
+ let isStreaming = false;
572
+ const API_BASE = window.location.origin;
573
+
574
+ // Check health on load
575
+ checkHealth();
576
+ setInterval(checkHealth, 30000);
577
+
578
+ async function checkHealth() {
579
+ try {
580
+ const response = await fetch(`${API_BASE}/health`);
581
+ const data = await response.json();
582
+ const dot = document.getElementById('statusDot');
583
+ const text = document.getElementById('statusText');
584
+
585
+ if (data.model_loaded) {
586
+ dot.className = 'status-dot online';
587
+ text.textContent = 'Online';
588
+ } else {
589
+ dot.className = 'status-dot';
590
+ text.textContent = 'Loading...';
591
+ }
592
+ } catch (error) {
593
+ document.getElementById('statusDot').className = 'status-dot error';
594
+ document.getElementById('statusText').textContent = 'Offline';
595
+ }
596
+ }
597
+
598
+ function toggleSettings() {
599
+ document.getElementById('settingsPanel').classList.toggle('active');
600
+ document.getElementById('settingsOverlay').classList.toggle('active');
601
+ }
602
+
603
+ function clearChat() {
604
+ messages = [];
605
+ document.getElementById('chatContainer').innerHTML = `
606
+ <div class="welcome-message" id="welcomeMessage">
607
+ <h2>Welcome to Nanbeige4.1-3B</h2>
608
+ <p>Start a new conversation</p>
609
+ </div>
610
+ `;
611
+ }
612
+
613
+ function autoResize(textarea) {
614
+ textarea.style.height = 'auto';
615
+ textarea.style.height = Math.min(textarea.scrollHeight, 200) + 'px';
616
+ }
617
+
618
+ function handleKeyDown(e) {
619
+ if (e.key === 'Enter' && !e.shiftKey) {
620
+ e.preventDefault();
621
+ sendMessage();
622
+ }
623
+ }
624
+
625
+ function sendSuggestion(text) {
626
+ document.getElementById('messageInput').value = text;
627
+ sendMessage();
628
+ }
629
+
630
+ async function sendMessage() {
631
+ const input = document.getElementById('messageInput');
632
+ const text = input.value.trim();
633
+
634
+ if (!text || isStreaming) return;
635
+
636
+ // Hide welcome message
637
+ const welcome = document.getElementById('welcomeMessage');
638
+ if (welcome) welcome.style.display = 'none';
639
+
640
+ // Add user message
641
+ addMessage('user', text);
642
+ messages.push({ role: 'user', content: text });
643
+
644
+ // Clear input
645
+ input.value = '';
646
+ input.style.height = 'auto';
647
+
648
+ // Show loading
649
+ isStreaming = true;
650
+ document.getElementById('sendBtn').disabled = true;
651
+ const loadingId = addLoadingMessage();
652
+
653
+ try {
654
+ const temperature = parseFloat(document.getElementById('temperature').value) || 0.6;
655
+ const maxTokens = parseInt(document.getElementById('maxTokens').value) || 2048;
656
+ const systemPrompt = document.getElementById('systemPrompt').value.trim();
657
+
658
+ let chatMessages = [...messages];
659
+ if (systemPrompt && messages.length === 1) {
660
+ chatMessages.unshift({ role: 'system', content: systemPrompt });
661
+ }
662
+
663
+ const response = await fetch(`${API_BASE}/chat`, {
664
+ method: 'POST',
665
+ headers: { 'Content-Type': 'application/json' },
666
+ body: JSON.stringify({
667
+ messages: chatMessages,
668
+ stream: true,
669
+ temperature: temperature,
670
+ max_tokens: maxTokens
671
+ })
672
+ });
673
+
674
+ if (!response.ok) throw new Error('Failed to get response');
675
+
676
+ // Remove loading, prepare for streaming
677
+ removeLoadingMessage(loadingId);
678
+ const assistantId = addMessage('assistant', '');
679
+
680
+ const reader = response.body.getReader();
681
+ const decoder = new TextDecoder();
682
+ let buffer = '';
683
+ let fullContent = '';
684
+
685
+ while (true) {
686
+ const { done, value } = await reader.read();
687
+ if (done) break;
688
+
689
+ buffer += decoder.decode(value, { stream: true });
690
+ const lines = buffer.split('\n');
691
+ buffer = lines.pop();
692
+
693
+ for (const line of lines) {
694
+ if (line.startsWith('data: ')) {
695
+ try {
696
+ const data = JSON.parse(line.slice(6));
697
+ if (data.type === 'token') {
698
+ fullContent += data.content;
699
+ updateMessage(assistantId, fullContent);
700
+ // allow DOM to repaint between tokens in case chunks arrive together
701
+ await new Promise(r => setTimeout(r, 0));
702
+ } else if (data.type === 'done') {
703
+ messages.push({ role: 'assistant', content: fullContent });
704
+ }
705
+ } catch (e) {
706
+ console.error('Parse error:', e);
707
+ }
708
+ }
709
+ }
710
+ }
711
+
712
+ } catch (error) {
713
+ removeLoadingMessage(loadingId);
714
+ addMessage('assistant', `Error: ${error.message}. Please check if the API is running.`);
715
+ } finally {
716
+ isStreaming = false;
717
+ document.getElementById('sendBtn').disabled = false;
718
+ }
719
+ }
720
+
721
+ function addMessage(role, content) {
722
+ const container = document.getElementById('chatContainer');
723
+ const id = 'msg-' + Date.now();
724
+
725
+ const div = document.createElement('div');
726
+ div.className = `message ${role}-message`;
727
+ div.id = id;
728
+ div.innerHTML = `
729
+ <div class="message-avatar">${role === 'user' ? 'U' : '🤖'}</div>
730
+ <div class="message-content">${formatContent(content)}</div>
731
+ `;
732
+
733
+ container.appendChild(div);
734
+ scrollToBottom();
735
+ return id;
736
+ }
737
+
738
+ function updateMessage(id, content) {
739
+ const msg = document.getElementById(id);
740
+ if (msg) {
741
+ msg.querySelector('.message-content').innerHTML = formatContent(content);
742
+ scrollToBottom();
743
+ }
744
+ }
745
+
746
+ function addLoadingMessage() {
747
+ const id = 'loading-' + Date.now();
748
+ const container = document.getElementById('chatContainer');
749
+
750
+ const div = document.createElement('div');
751
+ div.className = 'message assistant-message';
752
+ div.id = id;
753
+ div.innerHTML = `
754
+ <div class="message-avatar">🤖</div>
755
+ <div class="message-content loading">
756
+ <div class="typing-indicator">
757
+ <span></span>
758
+ <span></span>
759
+ <span></span>
760
+ </div>
761
+ </div>
762
+ `;
763
+
764
+ container.appendChild(div);
765
+ scrollToBottom();
766
+ return id;
767
+ }
768
+
769
+ function removeLoadingMessage(id) {
770
+ const el = document.getElementById(id);
771
+ if (el) el.remove();
772
+ }
773
+
774
+ function formatContent(text) {
775
+ // Escape HTML
776
+ text = text.replace(/&/g, '&amp;')
777
+ .replace(/</g, '&lt;')
778
+ .replace(/>/g, '&gt;');
779
+
780
+ // Format code blocks
781
+ text = text.replace(/```(\w+)?\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>');
782
+ text = text.replace(/`([^`]+)`/g, '<code>$1</code>');
783
+
784
+ // Format bold and italic
785
+ text = text.replace(/\*\*\*(.*?)\*\*\*/g, '<strong><em>$1</em></strong>');
786
+ text = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
787
+ text = text.replace(/\*(.*?)\*/g, '<em>$1</em>');
788
+
789
+ // Convert newlines to <br> outside of pre blocks
790
+ text = text.replace(/\n/g, '<br>');
791
+
792
+ return text;
793
+ }
794
+
795
+ function scrollToBottom() {
796
+ const container = document.getElementById('chatContainer');
797
+ container.scrollTop = container.scrollHeight;
798
+ }
799
+ </script>
800
+ </body>
801
  </html>