ParthSadaria commited on
Commit
28061b6
·
verified ·
1 Parent(s): 189656a

Update playground.html

Browse files
Files changed (1) hide show
  1. playground.html +1108 -610
playground.html CHANGED
@@ -1,299 +1,718 @@
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>LOKI.AI Playground</title>
7
  <link rel="icon" type="image/x-icon" href="favicon.ico">
8
- <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&family=Fira+Code:wght@300;400;500;600&display=swap" rel="stylesheet">
 
 
9
  <style>
10
  :root {
11
- --bg-primary: #121212;
12
- --bg-secondary: #1e1e1e;
13
- --text-primary: #f5f5f5;
14
- --text-secondary: #a0a0a0;
15
- --accent: #6b46c1;
16
- --accent-hover: #805ad5;
17
- --border: #333333;
18
- --message-user: #2d3748;
19
- --message-bot: #1a202c;
20
- }
21
- body.light-mode {
22
- --bg-primary: #f5f5f5;
23
- --bg-secondary: #e5e5e5;
24
- --text-primary: #1a1a1a;
25
- --text-secondary: #4a4a4a;
26
- --message-user: #e2e8f0;
27
- --message-bot: #edf2f7;
28
- }
29
- * { margin: 0; padding: 0; box-sizing: border-box; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  body {
31
- font-family: 'Montserrat', sans-serif;
32
- background-color: var(--bg-primary);
33
- color: var(--text-primary);
34
- min-height: 100vh;
35
  display: flex;
36
  justify-content: center;
37
  align-items: center;
38
- transition: background-color 0.3s ease;
39
- }
40
- .github-link {
41
- position: fixed;
42
- top: 1rem;
43
- right: 1rem;
44
- z-index: 100;
45
- color: var(--text-primary);
46
  }
47
- .github-link:hover { color: var(--accent); }
48
  .chat-wrapper {
49
- width: 95%;
50
- max-width: 900px;
 
51
  height: 90vh;
52
- background-color: var(--bg-secondary);
53
- border-radius: 12px;
54
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
55
  overflow: hidden;
 
56
  display: flex;
57
  flex-direction: column;
 
 
 
 
58
  }
 
59
  .chat-header {
 
 
60
  display: flex;
61
  justify-content: space-between;
62
  align-items: center;
63
- padding: 1rem;
64
- border-bottom: 1px solid var(--border);
 
65
  }
66
- .chat-header h2 {
67
- font-family: 'Fira Code', monospace;
68
- font-weight: 600;
69
- margin: 0;
 
 
70
  }
71
- .header-actions {
 
 
 
 
72
  display: flex;
73
- gap: 0.75rem;
 
 
 
 
 
74
  }
75
- button {
76
- background: none;
77
- border: none;
78
- color: var(--text-primary);
79
- cursor: pointer;
80
- padding: 0.5rem;
81
- border-radius: 6px;
 
 
 
82
  display: flex;
 
 
83
  align-items: center;
84
- justify-content: center;
85
  }
86
- button:hover { background-color: rgba(255, 255, 255, 0.1); }
87
-
88
- .custom-select-wrapper {
89
  position: relative;
90
- min-width: 180px;
 
 
91
  }
92
- .custom-select {
93
- border: 1px solid var(--border);
94
- padding: 0.5rem;
95
- border-radius: 6px;
96
- cursor: pointer;
97
- white-space: nowrap;
98
- overflow: hidden;
99
- text-overflow: ellipsis;
 
 
 
 
100
  }
101
- .custom-select-options {
102
- display: none;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  position: absolute;
104
- top: 100%;
105
  left: 0;
106
  right: 0;
107
- max-height: 300px;
108
- overflow-y: auto;
109
- background-color: var(--bg-secondary);
110
- border: 1px solid var(--border);
111
- border-radius: 6px;
112
- z-index: 100;
113
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
114
  }
115
- .model-group {
116
- padding: 0.5rem;
117
- font-weight: 500;
118
- color: var(--text-secondary);
119
- border-bottom: 1px solid var(--border);
120
- }
121
- .custom-option {
122
- padding: 0.5rem;
123
- cursor: pointer;
124
  }
125
- .custom-option:hover { background-color: rgba(255, 255, 255, 0.1); }
126
-
127
  .initial-input {
 
128
  display: flex;
129
  flex-direction: column;
130
- align-items: center;
131
  justify-content: center;
132
- flex: 1;
133
- gap: 2rem;
134
- padding: 2rem;
 
135
  }
 
 
 
 
 
 
 
 
 
136
  .input-container {
137
  width: 100%;
138
- max-width: 600px;
139
  position: relative;
140
  }
141
- .input-container input, .chat-input input {
 
142
  width: 100%;
143
- padding: 1rem;
144
- padding-right: 3rem;
145
- background-color: var(--bg-primary);
146
- color: var(--text-primary);
147
- border: 1px solid var(--border);
148
- border-radius: 6px;
149
- font-size: 1rem;
150
- font-family: 'Montserrat', sans-serif;
151
- }
152
- .input-container input:focus, .chat-input input:focus {
153
  outline: none;
154
- border-color: var(--accent);
155
  }
 
156
  .send-icon {
157
  position: absolute;
158
- right: 0.5rem;
159
  top: 50%;
160
  transform: translateY(-50%);
 
 
 
 
 
 
 
161
  cursor: pointer;
162
- padding: 0.5rem;
163
- color: var(--text-primary);
164
  }
165
- .send-icon:hover { color: var(--accent); }
166
-
167
- .chat-container {
168
- display: none;
169
- flex-direction: column;
170
- flex: 1;
171
- overflow: hidden;
172
  }
173
- .chat-messages {
174
- flex: 1;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  overflow-y: auto;
176
- padding: 1rem;
177
- scroll-behavior: smooth;
 
 
 
 
 
178
  }
179
- .message {
180
- margin-bottom: 1rem;
181
- padding: 1rem;
 
 
 
 
 
 
 
 
182
  border-radius: 8px;
183
- max-width: 80%;
184
- word-wrap: break-word;
 
 
 
 
 
 
185
  }
186
- .message.user {
187
- background-color: var(--message-user);
188
- margin-left: auto;
189
  }
190
- .message.bot {
191
- background-color: var(--message-bot);
192
- margin-right: auto;
 
193
  }
194
- .chat-input {
195
- padding: 1rem;
196
- border-top: 1px solid var(--border);
 
 
 
 
 
 
 
197
  }
198
- .chat-input-container {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  position: relative;
 
200
  }
201
-
202
- .typing-indicator {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  display: flex;
204
  align-items: center;
205
- justify-content: flex-start;
206
- gap: 0.25rem;
207
- padding: 0.75rem;
 
208
  }
209
- .typing-indicator span {
210
- width: 8px;
211
- height: 8px;
212
- background-color: var(--text-secondary);
213
- border-radius: 50%;
214
- display: inline-block;
215
- animation: typing 1.5s infinite ease-in-out;
216
  }
217
- .typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
218
- .typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
219
-
220
- @keyframes typing {
221
- 0%, 60%, 100% { transform: translateY(0); }
222
- 30% { transform: translateY(-6px); }
 
 
 
 
 
 
 
 
 
 
223
  }
224
-
225
- pre {
226
- background-color: rgba(0, 0, 0, 0.2);
227
- padding: 1rem;
228
- border-radius: 4px;
 
229
  overflow-x: auto;
230
- margin: 0.5rem 0;
 
231
  }
232
- code {
233
- font-family: 'Fira Code', monospace;
234
- font-size: 0.9rem;
 
 
 
 
 
 
 
 
 
 
235
  }
236
- a { color: var(--accent); text-decoration: none; }
237
- a:hover { text-decoration: underline; }
238
 
239
- .loader {
240
- display: none;
241
- width: 18px;
242
- height: 18px;
243
- border: 2px solid transparent;
244
- border-top-color: var(--text-primary);
 
 
 
 
 
 
245
  border-radius: 50%;
246
- animation: spin 1s linear infinite;
 
247
  }
248
- @keyframes spin {
249
- 0% { transform: rotate(0deg); }
250
- 100% { transform: rotate(360deg); }
251
  }
252
 
253
- .watermark {
254
- position: fixed;
255
- bottom: 0.5rem;
256
- font-size: 0.8rem;
257
- color: var(--text-secondary);
258
- text-align: center;
259
- width: 100%;
260
  }
261
 
262
- @media (max-width: 768px) {
263
- .chat-wrapper { width: 100%; height: 100vh; border-radius: 0; }
264
- .chat-header h2 { font-size: 1.2rem; }
265
- .message { max-width: 90%; }
266
- .custom-select-wrapper { min-width: 150px; }
267
- .github-link { top: 0.5rem; right: 0.5rem; }
 
 
 
268
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
  </style>
270
  </head>
 
271
  <body>
272
  <a href="https://github.com/ParthSadaria" target="_blank" rel="noopener noreferrer" class="github-link" aria-label="Visit Parth Sadaria's GitHub profile">
273
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
274
  <path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path>
275
  </svg>
276
  </a>
277
-
278
  <div class="chat-wrapper">
279
  <div class="chat-header">
280
- <h2>LOKI.AI Playground</h2>
281
  <div class="header-actions">
282
  <div class="custom-select-wrapper">
283
  <div class="custom-select" id="modelSelectDisplay">Select Model</div>
284
  <div class="custom-select-options" id="modelOptions">
285
- <div class="loader" id="modelLoader"></div>
 
286
  </div>
287
  </div>
288
- <button id="clearChatButton" title="Clear Chat">
289
- <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
 
290
  <polyline points="3 6 5 6 21 6"></polyline>
291
  <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
292
  <line x1="10" y1="11" x2="10" y2="17"></line>
293
  <line x1="14" y1="11" x2="14" y2="17"></line>
294
  </svg>
295
  </button>
296
- <button id="themeToggle" title="Toggle Theme">
297
  <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
298
  <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
299
  </svg>
@@ -306,7 +725,8 @@
306
  <div class="input-container">
307
  <input type="text" id="initialChatInput" placeholder="What can I help you with today?">
308
  <div class="send-icon" id="initialSendIcon">
309
- <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
 
310
  <line x1="22" y1="2" x2="11" y2="13"></line>
311
  <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
312
  </svg>
@@ -320,7 +740,8 @@
320
  <div class="chat-input-container">
321
  <input type="text" id="chatInput" placeholder="Type your message...">
322
  <div class="send-icon" id="sendButtonIcon">
323
- <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
 
324
  <line x1="22" y1="2" x2="11" y2="13"></line>
325
  <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
326
  </svg>
@@ -328,468 +749,545 @@
328
  </div>
329
  </div>
330
  </div>
331
- <p style="text-align:center; color:var(--text-secondary); font-size:9px; padding-bottom:5px;">
332
  Models can make mistakes. Check important info.
333
  </p>
334
  </div>
335
  </div>
336
-
337
  <div class="watermark">
338
  Made with ❤️ by Parth Sadaria
339
  </div>
340
-
341
  <script>
342
- document.addEventListener('DOMContentLoaded', () => {
343
- // DOM elements
344
- const elements = {
345
- chatWrapper: document.querySelector('.chat-wrapper'),
346
- initialInput: document.querySelector('.initial-input'),
347
- chatContainer: document.getElementById('chatContainer'),
348
- initialChatInput: document.getElementById('initialChatInput'),
349
- initialSendIcon: document.getElementById('initialSendIcon'),
350
- chatMessages: document.getElementById('chatMessages'),
351
- chatInput: document.getElementById('chatInput'),
352
- sendButtonIcon: document.getElementById('sendButtonIcon'),
353
- sendLoader: document.getElementById('sendLoader'),
354
- clearChatButton: document.getElementById('clearChatButton'),
355
- modelSelectDisplay: document.getElementById('modelSelectDisplay'),
356
- modelOptions: document.getElementById('modelOptions'),
357
- modelLoader: document.getElementById('modelLoader'),
358
- themeToggle: document.getElementById('themeToggle')
359
- };
360
-
361
- // State variables
362
- let state = {
363
- currentStreamingMessage: null,
364
- isStreamingInProgress: false,
365
- conversationHistory: [{ role: "system", content: "You are a helpful AI assistant. Assist the user effectively." }],
366
- selectedModel: '',
367
- modelsList: [],
368
- isDarkMode: true
369
- };
370
-
371
- // Helper functions
372
- const helpers = {
373
- scrollToBottom: () => {
374
- elements.chatMessages.scrollTop = elements.chatMessages.scrollHeight;
375
- },
376
 
377
- formatMessage: (content) => {
378
- return content
379
- .replace(/```([\s\S]*?)```/g, (match, code) => `<pre><code>${code}</code></pre>`)
380
- .replace(/`([^`]+)`/g, '<code>$1</code>')
381
- .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
382
- .replace(/\*(.*?)\*/g, '<em>$1</em>')
383
- .replace(/~~(.*?)~~/g, '<del>$1</del>')
384
- .replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>')
385
- .replace(/^#{3}\s+(.*?)$/gm, '<h3>$1</h3>')
386
- .replace(/^#{2}\s+(.*?)$/gm, '<h2>$1</h2>')
387
- .replace(/^#{1}\s+(.*?)$/gm, '<h1>$1</h1>')
388
- .replace(/^\>\s+(.*?)$/gm, '<blockquote>$1</blockquote>')
389
- .replace(/^(\d+)\.\s+(.*?)$/gm, '<ol start="$1"><li>$2</li></ol>')
390
- .replace(/^[-*]\s+(.*?)$/gm, '<ul><li>$1</li></ul>')
391
- .replace(/\n/g, '<br>');
392
  }
393
- };
394
-
395
- // Core functionality
396
- const app = {
397
- // Fetch models from API
398
- async fetchModels() {
399
- elements.modelLoader.style.display = 'block';
400
- try {
401
- const response = await fetch('https://parthsadaria-lokiai.hf.space/models', {
402
- headers: { 'Authorization': 'playground' }
403
- });
404
-
405
- if (response.ok) {
406
- state.modelsList = await response.json();
407
- app.populateModels(state.modelsList);
408
- } else {
409
- console.error('Failed to fetch models:', response.status);
410
- app.populateModels([
411
- { id: "gpt-4o", object: "model" },
412
- { id: "gpt-3.5-turbo", object: "model" },
413
- { id: "claude-3-5-sonnet", object: "model" },
414
- { id: "claude-3-haiku", object: "model" },
415
- { id: "llama-3.1-70b", object: "model" }
416
- ]);
417
- }
418
- } catch (error) {
419
- console.error('Error fetching models:', error);
420
- app.populateModels([
421
- { id: "gpt-4o", object: "model" },
422
- { id: "gpt-3.5-turbo", object: "model" },
423
- { id: "claude-3-5-sonnet", object: "model" },
424
- { id: "claude-3-haiku", object: "model" },
425
- { id: "llama-3.1-70b", object: "model" }
426
- ]);
427
- } finally {
428
- elements.modelLoader.style.display = 'none';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
429
  }
430
- },
431
 
432
- // Organize and display models
433
- populateModels(models) {
434
- // Group models by provider
435
- const providers = {};
436
- models.forEach(model => {
437
- const modelName = model.id;
438
- let provider = 'Other';
439
-
440
- if (modelName.includes('gpt')) provider = 'OpenAI';
441
- else if (modelName.includes('claude')) provider = 'Anthropic';
442
- else if (modelName.includes('llama')) provider = 'Meta';
443
- else if (modelName.includes('gemini')) provider = 'Google';
444
- else if (modelName.includes('mistral')) provider = 'Mistral';
445
- else if (modelName.includes('yi')) provider = 'Yi';
446
-
447
- if (!providers[provider]) providers[provider] = [];
448
- providers[provider].push(model);
449
- });
450
-
451
- // Clear existing options
452
- elements.modelOptions.innerHTML = '';
453
-
454
- // Add special option for web search
455
- const webSearchOption = document.createElement('div');
456
- webSearchOption.className = 'custom-option';
457
- webSearchOption.textContent = 'SearchGPT (Web-access)';
458
- webSearchOption.dataset.value = 'searchgpt';
459
- elements.modelOptions.appendChild(webSearchOption);
460
-
461
- // Add a separator
462
- const separator = document.createElement('div');
463
- separator.className = 'model-group';
464
- separator.textContent = 'AI Models';
465
- elements.modelOptions.appendChild(separator);
466
-
467
- // Create and append provider groups and their models
468
- Object.keys(providers).sort().forEach(provider => {
469
- const providerGroup = document.createElement('div');
470
- providerGroup.className = 'model-group';
471
- providerGroup.textContent = provider;
472
- elements.modelOptions.appendChild(providerGroup);
473
-
474
- providers[provider].sort((a, b) => a.id.localeCompare(b.id)).forEach(model => {
475
- const option = document.createElement('div');
476
- option.className = 'custom-option';
477
- option.textContent = model.id;
478
- option.dataset.value = model.id;
479
- elements.modelOptions.appendChild(option);
480
  });
481
- });
482
-
483
- // Set default model
484
- if (!state.selectedModel && models.length > 0) {
485
- const preferredModels = ['gpt-4o', 'gpt-4', 'claude-3-5-sonnet', 'gpt-3.5-turbo'];
486
-
487
- for (const preferred of preferredModels) {
488
- const match = models.find(m => m.id.includes(preferred));
489
- if (match) {
490
- state.selectedModel = match.id;
491
- elements.modelSelectDisplay.textContent = state.selectedModel;
492
- break;
493
- }
494
- }
495
-
496
- if (!state.selectedModel) {
497
- state.selectedModel = models[0].id;
498
- elements.modelSelectDisplay.textContent = state.selectedModel;
499
- }
500
  }
501
-
502
- // Add event listeners to options
503
- document.querySelectorAll('.custom-option').forEach(option => {
504
- option.addEventListener('click', function() {
505
- state.selectedModel = this.dataset.value;
506
- elements.modelSelectDisplay.textContent = this.textContent;
507
- elements.modelOptions.style.display = 'none';
508
- });
509
- });
510
- },
511
-
512
- // Message handling
513
- appendMessage(content, type = 'bot', isStreaming = false) {
514
- if (type === 'bot' && isStreaming) {
515
- if (!state.currentStreamingMessage) {
516
- state.currentStreamingMessage = document.createElement('div');
517
- state.currentStreamingMessage.className = `message ${type}`;
518
- elements.chatMessages.appendChild(state.currentStreamingMessage);
519
-
520
- if (!state.isStreamingInProgress) {
521
- state.conversationHistory.push({
522
- role: 'assistant',
523
- content: ''
524
- });
525
- state.isStreamingInProgress = true;
526
- }
527
- }
528
 
529
- state.currentStreamingMessage.innerHTML += helpers.formatMessage(content);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
530
 
531
- if (state.conversationHistory.length > 0) {
532
- state.conversationHistory[state.conversationHistory.length - 1].content += content;
533
- }
534
 
535
- helpers.scrollToBottom();
536
- } else {
537
- if (type === 'bot' && state.currentStreamingMessage) {
538
- state.isStreamingInProgress = false;
539
- state.currentStreamingMessage = null;
540
- } else {
541
- const messageBox = document.createElement('div');
542
- messageBox.className = `message ${type}`;
543
- messageBox.innerHTML = helpers.formatMessage(content);
544
- elements.chatMessages.appendChild(messageBox);
545
-
546
- if (type === 'user') {
547
- state.conversationHistory.push({
548
- role: 'user',
549
- content: content
550
- });
551
- } else if (type === 'bot' && !state.isStreamingInProgress) {
552
- state.conversationHistory.push({
553
- role: 'assistant',
554
- content: content
555
- });
556
- }
557
 
558
- helpers.scrollToBottom();
559
- }
560
- }
561
- },
562
-
563
- // Chat operations
564
- clearChat() {
565
- elements.chatMessages.innerHTML = '';
566
- state.conversationHistory = [{ role: "system", content: "You are a helpful AI assistant. Assist the user effectively." }];
567
- elements.initialInput.style.display = 'flex';
568
- elements.chatContainer.style.display = 'none';
569
- },
570
-
571
- async sendInitialMessage() {
572
- const userMessage = elements.initialChatInput.value.trim();
573
- if (!userMessage || !state.selectedModel) return;
574
-
575
- elements.initialInput.style.display = 'none';
576
- elements.chatContainer.style.display = 'flex';
577
-
578
- app.appendMessage(userMessage, 'user');
579
- elements.initialChatInput.value = '';
580
- helpers.scrollToBottom();
581
-
582
- elements.sendLoader.style.display = 'block';
583
- elements.sendButtonIcon.querySelector('svg').style.display = 'none';
584
-
585
- try {
586
- await app.callApi(userMessage, state.selectedModel);
587
- } catch (error) {
588
- app.appendMessage("Oops! Something went wrong. Please try again.", 'bot');
589
- console.error("API Error:", error);
590
- } finally {
591
- elements.sendLoader.style.display = 'none';
592
- elements.sendButtonIcon.querySelector('svg').style.display = 'block';
593
  }
594
- },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
595
 
596
- async sendMessage() {
597
- const userMessage = elements.chatInput.value.trim();
598
- if (!userMessage || !state.selectedModel) return;
599
-
600
- app.appendMessage(userMessage, 'user');
601
- elements.chatInput.value = '';
602
 
603
- elements.sendLoader.style.display = 'block';
604
- elements.sendButtonIcon.querySelector('svg').style.display = 'none';
605
-
606
- try {
607
- await app.callApi(userMessage, state.selectedModel);
608
- } catch (error) {
609
- app.appendMessage("Oops! Something went wrong. Please try again.", 'bot');
610
- console.error("API Error:", error);
611
- } finally {
612
- elements.sendLoader.style.display = 'none';
613
- elements.sendButtonIcon.querySelector('svg').style.display = 'block';
614
- }
615
- },
616
-
617
- async callApi(userMessage, model) {
618
- let typingIndicator = null;
619
 
620
- try {
621
- typingIndicator = document.createElement('div');
622
- typingIndicator.className = 'message bot typing-indicator';
623
- typingIndicator.innerHTML = '<span></span><span></span><span></span>';
624
- elements.chatMessages.appendChild(typingIndicator);
625
- helpers.scrollToBottom();
626
 
627
- if (model === "searchgpt") {
628
- const url = `https://parthsadaria-lokiai.hf.space/searchgpt?q=${encodeURIComponent(userMessage)}&stream=true&systemprompt=You are **SearchGPT**, an AI with internet access. Reply directly and accurately to user requests. Mention Sources at end`;
629
-
630
- const response = await fetch(url, {
631
- headers: { }
632
- });
 
633
 
634
- if (response.ok) {
635
- if (typingIndicator) {
636
- typingIndicator.remove();
637
- typingIndicator = null;
638
- }
639
-
640
- const reader = response.body.getReader();
641
- const decoder = new TextDecoder("utf-8");
642
- let done = false;
643
 
644
- while (!done) {
645
- const { value, done: streamDone } = await reader.read();
646
- done = streamDone;
647
 
648
- if (value) {
649
- const chunk = decoder.decode(value);
650
- const cleanedChunk = chunk.trim();
651
-
652
- if (cleanedChunk.startsWith('data:')) {
653
- const jsonChunks = cleanedChunk.split("data:").filter(Boolean);
654
 
655
- for (const jsonString of jsonChunks) {
656
- try {
657
- const cleanJson = jsonString.trim();
658
- if (cleanJson === '[DONE]') continue;
659
-
660
- const jsonData = JSON.parse(cleanJson);
661
- const content = jsonData.choices?.[0]?.message?.content ||
662
- jsonData.choices?.[0]?.delta?.content || "";
663
-
664
- if (content) {
665
- app.appendMessage(content, 'bot', true);
666
- }
667
- } catch (err) {
668
- console.warn("Parsing error:", err);
669
- }
670
  }
671
- } else {
672
- app.appendMessage(cleanedChunk, 'bot', true);
673
  }
674
  }
 
 
 
675
  }
676
- } else {
677
- throw new Error(`API responded with status ${response.status}`);
678
  }
679
- } else {
680
- const url = "https://parthsadaria-lokiai.hf.space/chat/completions";
681
- const payload = {
682
- model: model,
683
- messages: [...state.conversationHistory],
684
- stream: true
685
- };
686
-
687
- const response = await fetch(url, {
688
- method: "POST",
689
- headers: {
690
- "Content-Type": "application/json"
691
- },
692
- body: JSON.stringify(payload)
693
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
694
 
695
- if (response.ok) {
696
- if (typingIndicator) {
697
- typingIndicator.remove();
698
- typingIndicator = null;
699
- }
700
 
701
- const reader = response.body.getReader();
702
- const decoder = new TextDecoder("utf-8");
703
- let done = false;
704
- let buffer = "";
705
 
706
- while (!done) {
707
- const { value, done: streamDone } = await reader.read();
708
- done = streamDone;
709
- if (value) {
710
- buffer += decoder.decode(value, { stream: true });
711
-
712
- let boundary = buffer.indexOf('\n');
713
- while (boundary !== -1) {
714
- let chunk = buffer.slice(0, boundary);
715
- buffer = buffer.slice(boundary + 1);
716
-
717
- if (chunk.startsWith('data:')) {
718
- const jsonChunks = chunk.split("data:").filter(Boolean);
719
-
720
- for (const jsonString of jsonChunks) {
721
- try {
722
- const cleanJson = jsonString.trim();
723
- if (cleanJson === '[DONE]') continue;
724
-
725
- const jsonData = JSON.parse(cleanJson);
726
- const content = jsonData.choices?.[0]?.message?.content ||
727
- jsonData.choices?.[0]?.delta?.content || "";
728
-
729
- if (content) {
730
- app.appendMessage(content, 'bot', true);
731
- }
732
- } catch (err) {
733
- console.warn("Parsing error:", err);
734
- }
735
- }
736
- } else {
737
- app.appendMessage(chunk, 'bot', true);
738
- }
739
-
740
- boundary = buffer.indexOf('\n');
741
  }
 
 
742
  }
743
  }
744
- } else {
745
- throw new Error(`API responded with status ${response.status}`);
746
  }
747
  }
748
- } catch (error) {
749
- if (typingIndicator) {
750
- typingIndicator.remove();
 
 
 
 
 
 
 
 
 
 
 
 
 
751
  }
752
- throw error;
 
753
  }
754
- },
755
-
756
- // Event listeners
757
- setupEventListeners() {
758
- elements.initialSendIcon.addEventListener('click', app.sendInitialMessage);
759
- elements.initialChatInput.addEventListener('keydown', (event) => {
760
- if (event.key === 'Enter') {
761
- app.sendInitialMessage();
762
- }
763
- });
 
 
 
 
764
 
765
- elements.sendButtonIcon.addEventListener('click', app.sendMessage);
766
- elements.chatInput.addEventListener('keydown', (event) => {
767
- if (event.key === 'Enter') {
768
- app.sendMessage();
769
- }
770
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
771
 
772
- elements.clearChatButton.addEventListener('click', app.clearChat);
 
 
 
773
 
774
- elements.modelSelectDisplay.addEventListener('click', () => {
775
- elements.modelOptions.style.display = elements.modelOptions.style.display === 'block' ? 'none' : 'block';
776
- });
777
 
778
- elements.themeToggle.addEventListener('click', () => {
779
- document.body.classList.toggle('light-mode', !state.isDarkMode);
780
- state.isDarkMode = !state.isDarkMode;
781
- });
782
- }
783
- };
 
 
784
 
785
- // Initialize the app
786
- const init = async () => {
787
- await app.fetchModels();
788
- app.setupEventListeners();
789
- };
 
 
 
 
790
 
791
- init();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
792
  });
793
- </script>
 
794
  </body>
795
- </html>
 
1
  <!DOCTYPE html>
2
  <html lang="en">
3
+
4
  <head>
5
  <meta charset="UTF-8">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Loki.AI Playground</title>
8
  <link rel="icon" type="image/x-icon" href="favicon.ico">
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@300;400;500;600&display=swap" rel="stylesheet">
10
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js"></script>
11
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.js"></script>
12
  <style>
13
  :root {
14
+ --bg-dark: #0a0a0f;
15
+ --bg-darker: #040409;
16
+ --bg-deepest: #020205;
17
+ --primary-blue: #4a6cf7;
18
+ --secondary-blue: #6678e3;
19
+ --accent-color: #7e57c2;
20
+ --text-light: #e0e0e8;
21
+ --text-muted: #8a8a9b;
22
+ --border-color: #1a1a2e;
23
+ --hover-color: rgba(78, 108, 247, 0.1);
24
+ --delete-red: #ff4d4d;
25
+ --header-height: 60px;
26
+ --input-height: 80px;
27
+ }
28
+
29
+ * {
30
+ margin: 0;
31
+ padding: 0;
32
+ box-sizing: border-box;
33
+ scrollbar-width: thin;
34
+ scrollbar-color: var(--primary-blue) transparent;
35
+ }
36
+
37
+ *::-webkit-scrollbar {
38
+ width: 8px;
39
+ }
40
+
41
+ *::-webkit-scrollbar-track {
42
+ background: transparent;
43
+ }
44
+
45
+ *::-webkit-scrollbar-thumb {
46
+ background-color: var(--primary-blue);
47
+ border-radius: 20px;
48
+ }
49
+
50
  body {
51
+ font-family: 'Inter', sans-serif;
52
+ background-color: var(--bg-dark);
53
+ color: var(--text-light);
 
54
  display: flex;
55
  justify-content: center;
56
  align-items: center;
57
+ min-height: 100vh;
58
+ overflow: hidden;
59
+ perspective: 2000px;
 
 
 
 
 
60
  }
61
+
62
  .chat-wrapper {
63
+ position: relative;
64
+ width: 100%;
65
+ max-width: 1000px;
66
  height: 90vh;
67
+ background-color: var(--bg-darker);
68
+ border-radius: 24px;
 
69
  overflow: hidden;
70
+ box-shadow: 0 20px 50px rgba(5, 5, 10, 0.7);
71
  display: flex;
72
  flex-direction: column;
73
+ opacity: 1;
74
+ transform: scale(1);
75
+ transition: all 0.6s cubic-bezier(0.23, 1, 0.32, 1);
76
+ border: 1px solid var(--border-color);
77
  }
78
+
79
  .chat-header {
80
+ background-color: var(--bg-deepest);
81
+ padding: 15px 20px;
82
  display: flex;
83
  justify-content: space-between;
84
  align-items: center;
85
+ border-bottom: 1px solid var(--border-color);
86
+ flex-shrink: 0;
87
+ height: var(--header-height);
88
  }
89
+
90
+ .chat-container {
91
+ display: none;
92
+ flex-direction: column;
93
+ height: calc(100% - var(--header-height));
94
+ position: relative;
95
  }
96
+
97
+ .chat-messages {
98
+ flex: 1;
99
+ overflow-y: auto;
100
+ padding: 20px;
101
  display: flex;
102
+ flex-direction: column;
103
+ gap: 16px;
104
+ background-color: var(--bg-dark);
105
+ font-family: 'JetBrains Mono', monospace;
106
+ height: calc(100% - var(--input-height));
107
+ margin-bottom: var(--input-height);
108
  }
109
+
110
+ .chat-input {
111
+ position: absolute;
112
+ bottom: 0;
113
+ left: 0;
114
+ right: 0;
115
+ background-color: var(--bg-darker);
116
+ border-top: 1px solid var(--border-color);
117
+ padding: 16px;
118
+ height: var(--input-height);
119
  display: flex;
120
+ gap: 12px;
121
+ z-index: 10;
122
  align-items: center;
 
123
  }
124
+
125
+ .chat-input-container {
 
126
  position: relative;
127
+ flex: 1;
128
+ display: flex;
129
+ align-items: center;
130
  }
131
+
132
+ .chat-input input {
133
+ width: 100%;
134
+ padding: 12px 16px;
135
+ padding-right: 50px;
136
+ background-color: rgba(30, 30, 50, 0.8);
137
+ border: 1px solid var(--border-color);
138
+ border-radius: 12px;
139
+ color: var(--text-light);
140
+ font-size: 14px;
141
+ font-family: 'JetBrains Mono', monospace;
142
+ outline: none;
143
  }
144
+
145
+ .chat-input input:focus {
146
+ border-color: var(--primary-blue);
147
+ box-shadow: 0 0 0 3px rgba(74, 108, 247, 0.2);
148
+ }
149
+
150
+ .message {
151
+ max-width: 80%;
152
+ width: fit-content;
153
+ padding: 12px 18px;
154
+ border-radius: 12px;
155
+ font-size: 14px;
156
+ line-height: 1.5;
157
+ position: relative;
158
+ animation: fadeIn 0.4s forwards;
159
+ margin-bottom: 8px;
160
+ }
161
+
162
+ .message.user {
163
+ align-self: flex-end;
164
+ background-color: var(--primary-blue);
165
+ color: white;
166
+ }
167
+
168
+ .message.bot {
169
+ align-self: flex-start;
170
+ background-color: rgba(40, 40, 70, 0.8);
171
+ color: var(--text-light);
172
+ border: 1px solid var(--border-color);
173
+ }
174
+
175
+ .message::before {
176
+ content: '';
177
  position: absolute;
178
+ top: 0;
179
  left: 0;
180
  right: 0;
181
+ bottom: 0;
182
+ background: linear-gradient(to right, transparent, rgba(255, 255, 255, 0.1));
183
+ opacity: 0;
184
+ transition: opacity 0.3s ease;
 
 
 
185
  }
186
+
187
+ .message:hover::before {
188
+ opacity: 1;
 
 
 
 
 
 
189
  }
190
+
 
191
  .initial-input {
192
+ flex: 1;
193
  display: flex;
194
  flex-direction: column;
 
195
  justify-content: center;
196
+ align-items: center;
197
+ padding: 24px;
198
+ text-align: center;
199
+ background: linear-gradient(145deg, var(--bg-dark), var(--bg-darker));
200
  }
201
+
202
+ .initial-input h2 {
203
+ font-size: 24px;
204
+ margin-bottom: 16px;
205
+ color: var(--text-light);
206
+ font-weight: 600;
207
+ font-family: 'JetBrains Mono', monospace;
208
+ }
209
+
210
  .input-container {
211
  width: 100%;
212
+ max-width: 400px;
213
  position: relative;
214
  }
215
+
216
+ .initial-input input {
217
  width: 100%;
218
+ padding: 16px 24px;
219
+ background-color: rgba(30, 30, 50, 0.8);
220
+ border: 1px solid var(--border-color);
221
+ border-radius: 12px;
222
+ color: var(--text-light);
223
+ font-size: 16px;
224
+ font-family: 'JetBrains Mono', monospace;
 
 
 
225
  outline: none;
226
+ transition: all 0.3s ease;
227
  }
228
+
229
  .send-icon {
230
  position: absolute;
231
+ right: 12px;
232
  top: 50%;
233
  transform: translateY(-50%);
234
+ background-color: var(--primary-blue);
235
+ border-radius: 8px;
236
+ width: 40px;
237
+ height: 40px;
238
+ display: flex;
239
+ justify-content: center;
240
+ align-items: center;
241
  cursor: pointer;
242
+ transition: all 0.3s ease;
 
243
  }
244
+
245
+ .send-icon:hover {
246
+ background-color: var(--accent-color);
247
+ transform: translateY(-50%) scale(1.05);
248
+ border-radius: 15px;
 
 
249
  }
250
+
251
+ @keyframes fadeIn {
252
+ from {
253
+ opacity: 0;
254
+ transform: translateY(20px);
255
+ }
256
+
257
+ to {
258
+ opacity: 1;
259
+ transform: translateY(0);
260
+ }
261
+ }
262
+
263
+ /* Header actions */
264
+ .header-actions {
265
+ display: flex;
266
+ align-items: center;
267
+ gap: 10px;
268
+ justify-content: center;
269
+ }
270
+
271
+ .model-select {
272
+ background-color: black;
273
+ color: var(--text-light);
274
+ border: 1px solid var(--border-color);
275
+ border-radius: 8px;
276
+ padding: 8px 12px;
277
+ font-family: 'JetBrains Mono', monospace;
278
+ font-size: 14px;
279
+ cursor: pointer;
280
+ transition: all 0.3s ease;
281
+ max-width: 240px;
282
  overflow-y: auto;
283
+ -webkit-appearance: none;
284
+ -moz-appearance: none;
285
+ appearance: none;
286
+ background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2214%22%20height%3D%2214%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22%23ffffff%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpolyline%20points%3D%226%209%2012%2015%2018%209%22%3E%3C%2Fpolyline%3E%3C%2Fsvg%3E");
287
+ background-repeat: no-repeat;
288
+ background-position: right 8px center;
289
+ padding-right: 28px;
290
  }
291
+
292
+ .model-select option {
293
+ background-color: var(--bg-dark);
294
+ color: var(--text-light);
295
+ padding: 8px;
296
+ }
297
+
298
+ .clear-chat {
299
+ background-color: var(--delete-red);
300
+ color: white;
301
+ border: none;
302
  border-radius: 8px;
303
+ width: 36px;
304
+ height: 36px;
305
+ padding: 8px;
306
+ display: flex;
307
+ align-items: center;
308
+ justify-content: center;
309
+ cursor: pointer;
310
+ transition: all 0.3s ease;
311
  }
312
+
313
+ .clear-chat:hover {
314
+ background-color: #ff6666;
315
  }
316
+
317
+ .clear-chat svg {
318
+ width: 18px;
319
+ height: 18px;
320
  }
321
+
322
+ .watermark {
323
+ position: absolute;
324
+ bottom: 10px;
325
+ right: 15px;
326
+ color: var(--text-muted);
327
+ font-size: 10px;
328
+ opacity: 0.5;
329
+ transition: opacity 0.3s ease;
330
+ z-index: 11;
331
  }
332
+
333
+ .watermark:hover {
334
+ opacity: 0.8;
335
+ }
336
+
337
+ /* Loading spinner */
338
+ .loader {
339
+ display: none;
340
+ width: 24px;
341
+ height: 24px;
342
+ border: 3px solid rgba(255, 255, 255, 0.3);
343
+ border-radius: 50%;
344
+ border-top-color: var(--primary-blue);
345
+ animation: spin 1s ease-in-out infinite;
346
+ }
347
+
348
+ @keyframes spin {
349
+ to {
350
+ transform: rotate(360deg);
351
+ }
352
+ }
353
+
354
+ .github-link {
355
+ display: flex;
356
+ align-items: center;
357
+ justify-content: center;
358
+ width: 36px;
359
+ height: 36px;
360
+ z-index: 999;
361
+ border-radius: 8px;
362
+ background-color: transparent;
363
+ border: 1px solid var(--border-color);
364
+ transition: all 0.3s ease;
365
+ color: var(--text-light);
366
+ position: absolute;
367
+ bottom: 0;
368
+ left: 0;
369
+ margin: 10px;
370
+ }
371
+
372
+ .github-link:hover {
373
+ background-color: var(--hover-color);
374
+ transform: translateY(-2px);
375
+ }
376
+
377
+ .github-link svg {
378
+ width: 22px;
379
+ height: 22px;
380
+ }
381
+
382
+ /* Enhanced select dropdown */
383
+ .custom-select-wrapper {
384
  position: relative;
385
+ min-width: 180px;
386
  }
387
+
388
+ .custom-select {
389
+ position: relative;
390
+ background-color: rgba(20, 20, 40, 0.9);
391
+ color: var(--text-light);
392
+ border: 1px solid var(--border-color);
393
+ border-radius: 8px;
394
+ padding: 8px 35px 8px 12px;
395
+ font-family: 'JetBrains Mono', monospace;
396
+ font-size: 14px;
397
+ cursor: pointer;
398
+ transition: all 0.3s ease;
399
+ text-overflow: ellipsis;
400
+ overflow: hidden;
401
+ white-space: nowrap;
402
+ max-width: 240px;
403
+ }
404
+
405
+ .custom-select:after {
406
+ content: '';
407
+ position: absolute;
408
+ right: 10px;
409
+ top: 50%;
410
+ transform: translateY(-50%);
411
+ width: 0;
412
+ height: 0;
413
+ border-left: 5px solid transparent;
414
+ border-right: 5px solid transparent;
415
+ border-top: 5px solid var(--text-light);
416
+ }
417
+
418
+ .custom-select-options {
419
+ position: absolute;
420
+ top: 100%;
421
+ left: 0;
422
+ right: 0;
423
+ background-color: rgba(15, 15, 30, 0.95);
424
+ border: 1px solid var(--border-color);
425
+ border-radius: 8px;
426
+ margin-top: 4px;
427
+ max-height: 300px;
428
+ overflow-y: auto;
429
+ z-index: 1000;
430
+ display: none;
431
+ backdrop-filter: blur(10px);
432
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
433
+ }
434
+
435
+ .custom-option {
436
+ padding: 8px 12px;
437
+ cursor: pointer;
438
+ transition: all 0.2s ease;
439
+ }
440
+
441
+ .custom-option:hover {
442
+ background-color: rgba(74, 108, 247, 0.2);
443
+ }
444
+
445
+ .model-group {
446
+ padding: 5px 10px;
447
+ font-size: 10px;
448
+ color: var(--text-muted);
449
+ text-transform: uppercase;
450
+ letter-spacing: 1px;
451
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
452
+ margin-top: 5px;
453
+ }
454
+
455
+ /* Dark mode toggle */
456
+ .theme-toggle {
457
+ background-color: transparent;
458
+ border: 1px solid var(--border-color);
459
+ border-radius: 8px;
460
+ width: 36px;
461
+ height: 36px;
462
  display: flex;
463
  align-items: center;
464
+ justify-content: center;
465
+ cursor: pointer;
466
+ transition: all 0.3s ease;
467
+ color: var(--text-light);
468
  }
469
+
470
+ .theme-toggle:hover {
471
+ background-color: var(--hover-color);
 
 
 
 
472
  }
473
+
474
+ /* Mobile optimizations */
475
+ @media screen and (max-width: 768px) {
476
+ .chat-wrapper {
477
+ height: 100vh;
478
+ border-radius: 0;
479
+ }
480
+
481
+ .custom-select {
482
+ font-size: 12px;
483
+ max-width: 140px;
484
+ }
485
+
486
+ .chat-input {
487
+ padding: 10px;
488
+ }
489
  }
490
+
491
+ /* Markdown styling */
492
+ .bot pre {
493
+ background-color: rgba(15, 15, 30, 0.6);
494
+ padding: 10px;
495
+ border-radius: 6px;
496
  overflow-x: auto;
497
+ margin: 10px 0;
498
+ border: 1px solid rgba(255, 255, 255, 0.1);
499
  }
500
+
501
+ .bot code {
502
+ font-family: 'JetBrains Mono', monospace;
503
+ font-size: 12px;
504
+ }
505
+
506
+ .bot p {
507
+ margin-bottom: 8px;
508
+ }
509
+
510
+ .bot ul, .bot ol {
511
+ margin-left: 20px;
512
+ margin-bottom: 8px;
513
  }
 
 
514
 
515
+ /* Typing indicator */
516
+ .typing-indicator {
517
+ display: inline-flex;
518
+ align-items: center;
519
+ gap: 5px;
520
+ padding: 5px 10px;
521
+ }
522
+
523
+ .typing-indicator span {
524
+ width: 6px;
525
+ height: 6px;
526
+ background-color: var(--text-muted);
527
  border-radius: 50%;
528
+ display: inline-block;
529
+ animation: pulse 1.5s infinite ease-in-out;
530
  }
531
+
532
+ .typing-indicator span:nth-child(2) {
533
+ animation-delay: 0.2s;
534
  }
535
 
536
+ .typing-indicator span:nth-child(3) {
537
+ animation-delay: 0.4s;
 
 
 
 
 
538
  }
539
 
540
+ @keyframes pulse {
541
+ 0%, 100% {
542
+ transform: scale(0.8);
543
+ opacity: 0.5;
544
+ }
545
+ 50% {
546
+ transform: scale(1.2);
547
+ opacity: 1;
548
+ }
549
  }
550
+ /* markdown */
551
+ /* Add this to your style section */
552
+ .message pre {
553
+ background-color: rgba(15, 15, 30, 0.6);
554
+ padding: 10px;
555
+ border-radius: 6px;
556
+ overflow-x: auto;
557
+ margin: 10px 0;
558
+ border: 1px solid rgba(255, 255, 255, 0.1);
559
+ }
560
+
561
+ .message code {
562
+ font-family: 'JetBrains Mono', monospace;
563
+ font-size: 12px;
564
+ background-color: rgba(30, 30, 50, 0.3);
565
+ padding: 2px 4px;
566
+ border-radius: 3px;
567
+ }
568
+
569
+ .message pre code {
570
+ background-color: transparent;
571
+ padding: 0;
572
+ }
573
+
574
+ .message blockquote {
575
+ border-left: 3px solid var(--primary-blue);
576
+ padding-left: 10px;
577
+ margin: 10px 0;
578
+ color: var(--text-muted);
579
+ }
580
+
581
+ .message h1, .message h2, .message h3 {
582
+ margin: 15px 0 5px 0;
583
+ }
584
+
585
+ .message ul, .message ol {
586
+ margin-left: 20px;
587
+ margin-bottom: 8px;
588
+ }
589
+
590
+ .message a {
591
+ color: var(--primary-blue);
592
+ text-decoration: underline;
593
+ }
594
+
595
+ .message p {
596
+ margin-bottom: 8px;
597
+ }
598
+
599
+ .message img {
600
+ max-width: 100%;
601
+ border-radius: 8px;
602
+ margin: 10px 0;
603
+ }
604
+
605
+ .message table {
606
+ border-collapse: collapse;
607
+ width: 100%;
608
+ margin: 15px 0;
609
+ }
610
+
611
+ .message th, .message td {
612
+ border: 1px solid var(--border-color);
613
+ padding: 8px;
614
+ text-align: left;
615
+ }
616
+
617
+ .message th {
618
+ background-color: rgba(15, 15, 30, 0.6);
619
+ }
620
+
621
+ /* Tablet adjustments */
622
+ @media screen and (min-width: 769px) and (max-width: 1024px) {
623
+ .chat-wrapper {
624
+ max-width: 90%;
625
+ }
626
+ .chat-header {
627
+ padding: 12px 18px;
628
+ height: calc(var(--header-height) - 10px);
629
+ }
630
+ .chat-input {
631
+ padding: 12px;
632
+ height: calc(var(--input-height) - 10px);
633
+ }
634
+ }
635
+
636
+ /* Mobile adjustments */
637
+ @media screen and (max-width: 768px) {
638
+ .chat-wrapper {
639
+ height: 100vh;
640
+ border-radius: 0;
641
+ }
642
+ .chat-header {
643
+ padding: 10px 15px;
644
+ height: 50px;
645
+ }
646
+ .chat-messages {
647
+ padding: 10px;
648
+ font-size: 14px;
649
+ margin-bottom: calc(var(--input-height) - 20px);
650
+ }
651
+ .chat-input {
652
+ padding: 8px;
653
+ height: 60px;
654
+ }
655
+ .chat-input input {
656
+ font-size: 14px;
657
+ padding: 10px;
658
+ }
659
+ .model-select {
660
+ font-size: 12px;
661
+ max-width: 140px;
662
+ }
663
+ }
664
+
665
+ /* Extra small devices */
666
+ @media screen and (max-width: 480px) {
667
+ .chat-header {
668
+ font-size: 13px;
669
+ padding: 8px 10px;
670
+ height: 45px;
671
+ }
672
+ .chat-messages {
673
+ padding: 8px;
674
+ font-size: 12px;
675
+ }
676
+ .chat-input {
677
+ padding: 6px;
678
+ height: 50px;
679
+ }
680
+ .chat-input input {
681
+ font-size: 12px;
682
+ padding: 8px;
683
+ }
684
+ }
685
+
686
  </style>
687
  </head>
688
+
689
  <body>
690
  <a href="https://github.com/ParthSadaria" target="_blank" rel="noopener noreferrer" class="github-link" aria-label="Visit Parth Sadaria's GitHub profile">
691
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
692
  <path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path>
693
  </svg>
694
  </a>
 
695
  <div class="chat-wrapper">
696
  <div class="chat-header">
697
+ <h2 style="font-family: 'JetBrains Mono', monospace;">LOKI.AI Playground</h2>
698
  <div class="header-actions">
699
  <div class="custom-select-wrapper">
700
  <div class="custom-select" id="modelSelectDisplay">Select Model</div>
701
  <div class="custom-select-options" id="modelOptions">
702
+ <!-- Models will be loaded here -->
703
+ <div class="loader" id="modelLoader" style="margin: 10px auto;"></div>
704
  </div>
705
  </div>
706
+ <button id="clearChatButton" class="clear-chat">
707
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
708
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
709
  <polyline points="3 6 5 6 21 6"></polyline>
710
  <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
711
  <line x1="10" y1="11" x2="10" y2="17"></line>
712
  <line x1="14" y1="11" x2="14" y2="17"></line>
713
  </svg>
714
  </button>
715
+ <button id="themeToggle" class="theme-toggle">
716
  <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
717
  <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
718
  </svg>
 
725
  <div class="input-container">
726
  <input type="text" id="initialChatInput" placeholder="What can I help you with today?">
727
  <div class="send-icon" id="initialSendIcon">
728
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
729
+ stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
730
  <line x1="22" y1="2" x2="11" y2="13"></line>
731
  <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
732
  </svg>
 
740
  <div class="chat-input-container">
741
  <input type="text" id="chatInput" placeholder="Type your message...">
742
  <div class="send-icon" id="sendButtonIcon">
743
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
744
+ stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
745
  <line x1="22" y1="2" x2="11" y2="13"></line>
746
  <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
747
  </svg>
 
749
  </div>
750
  </div>
751
  </div>
752
+ <p style="display: block; text-align: center; color: grey; font-size: 9px; z-index: 99; padding-bottom:5px;">
753
  Models can make mistakes. Check important info.
754
  </p>
755
  </div>
756
  </div>
 
757
  <div class="watermark">
758
  Made with ❤️ by Parth Sadaria
759
  </div>
 
760
  <script>
761
+ document.addEventListener('DOMContentLoaded', function() {
762
+ const chatWrapper = document.querySelector('.chat-wrapper');
763
+ const initialInput = document.querySelector('.initial-input');
764
+ const chatContainer = document.getElementById('chatContainer');
765
+ const initialChatInput = document.getElementById('initialChatInput');
766
+ const initialSendIcon = document.getElementById('initialSendIcon');
767
+ const chatMessages = document.getElementById('chatMessages');
768
+ const chatInput = document.getElementById('chatInput');
769
+ const sendButtonIcon = document.getElementById('sendButtonIcon');
770
+ const sendLoader = document.getElementById('sendLoader');
771
+ const clearChatButton = document.getElementById('clearChatButton');
772
+ const modelSelectDisplay = document.getElementById('modelSelectDisplay');
773
+ const modelOptions = document.getElementById('modelOptions');
774
+ const modelLoader = document.getElementById('modelLoader');
775
+ const themeToggle = document.getElementById('themeToggle');
776
+
777
+ let currentStreamingMessage = null;
778
+ let isStreamingInProgress = false;
779
+ let conversationHistory = [{ role: "system", content: "You are a helpful AI assistant. Assist the user effectively." }];
780
+ let selectedModel = '';
781
+ let modelsList = [];
782
+ let isDarkMode = true;
783
+
784
+ // Fetch models from API
785
+ async function fetchModels() {
786
+ modelLoader.style.display = 'block';
787
+ try {
788
+ const response = await fetch('https://parthsadaria-lokiai.hf.space/models', {
789
+ method: 'GET',
790
+ headers: {
791
+ 'Authorization': 'playground'
792
+ }
793
+ });
 
794
 
795
+ if (response.ok) {
796
+ const data = await response.json();
797
+ modelsList = data;
798
+ populateModels(data);
799
+ } else {
800
+ console.error('Failed to fetch models:', response.status);
801
+ // Fallback to some default models
802
+ const fallbackModels = [
803
+ { id: "gpt-4o", object: "model" },
804
+ { id: "gpt-3.5-turbo", object: "model" },
805
+ { id: "claude-3-5-sonnet", object: "model" },
806
+ { id: "claude-3-haiku", object: "model" },
807
+ { id: "llama-3.1-70b", object: "model" }
808
+ ];
809
+ populateModels(fallbackModels);
810
  }
811
+ } catch (error) {
812
+ console.error('Error fetching models:', error);
813
+ // Use the same fallback
814
+ const fallbackModels = [
815
+ { id: "gpt-4o", object: "model" },
816
+ { id: "gpt-3.5-turbo", object: "model" },
817
+ { id: "claude-3-5-sonnet", object: "model" },
818
+ { id: "claude-3-haiku", object: "model" },
819
+ { id: "llama-3.1-70b", object: "model" }
820
+ ];
821
+ populateModels(fallbackModels);
822
+ } finally {
823
+ modelLoader.style.display = 'none';
824
+ }
825
+ }
826
+
827
+ // Organize and display models
828
+ function populateModels(models) {
829
+ // Group models by provider
830
+ const providers = {};
831
+ models.forEach(model => {
832
+ const modelName = model.id;
833
+ let provider = 'Other';
834
+
835
+ if (modelName.includes('gpt')) provider = 'OpenAI';
836
+ else if (modelName.includes('claude')) provider = 'Anthropic';
837
+ else if (modelName.includes('llama')) provider = 'Meta';
838
+ else if (modelName.includes('gemini')) provider = 'Google';
839
+ else if (modelName.includes('mistral')) provider = 'Mistral';
840
+ else if (modelName.includes('yi')) provider = 'Yi';
841
+
842
+ if (!providers[provider]) providers[provider] = [];
843
+ providers[provider].push(model);
844
+ });
845
+
846
+ // Clear existing options
847
+ modelOptions.innerHTML = '';
848
+
849
+ // Add special option for web search
850
+ const webSearchOption = document.createElement('div');
851
+ webSearchOption.className = 'custom-option';
852
+ webSearchOption.textContent = 'SearchGPT (Web-access)';
853
+ webSearchOption.dataset.value = 'searchgpt';
854
+ modelOptions.appendChild(webSearchOption);
855
+
856
+ // Add a separator
857
+ const separator = document.createElement('div');
858
+ separator.className = 'model-group';
859
+ separator.textContent = 'AI Models';
860
+ modelOptions.appendChild(separator);
861
+
862
+ // Create and append provider groups and their models
863
+ Object.keys(providers).sort().forEach(provider => {
864
+ const providerGroup = document.createElement('div');
865
+ providerGroup.className = 'model-group';
866
+ providerGroup.textContent = provider;
867
+ modelOptions.appendChild(providerGroup);
868
+
869
+ providers[provider].sort((a, b) => a.id.localeCompare(b.id)).forEach(model => {
870
+ const option = document.createElement('div');
871
+ option.className = 'custom-option';
872
+ option.textContent = model.id;
873
+ option.dataset.value = model.id;
874
+ modelOptions.appendChild(option);
875
+ });
876
+ });
877
+
878
+ // Set default model if none selected yet
879
+ if (!selectedModel && models.length > 0) {
880
+ const preferredModels = ['gpt-4o', 'gpt-4', 'claude-3-5-sonnet', 'gpt-3.5-turbo'];
881
+
882
+ // Find the first preferred model that exists in our list
883
+ for (const preferred of preferredModels) {
884
+ const match = models.find(m => m.id.includes(preferred));
885
+ if (match) {
886
+ selectedModel = match.id;
887
+ modelSelectDisplay.textContent = selectedModel;
888
+ break;
889
  }
890
+ }
891
 
892
+ // If no preferred model found, use the first one in the list
893
+ if (!selectedModel) {
894
+ selectedModel = models[0].id;
895
+ modelSelectDisplay.textContent = selectedModel;
896
+ }
897
+ }
898
+
899
+ // Add event listeners to options
900
+ document.querySelectorAll('.custom-option').forEach(option => {
901
+ option.addEventListener('click', function() {
902
+ selectedModel = this.dataset.value;
903
+ modelSelectDisplay.textContent = this.textContent;
904
+ modelOptions.style.display = 'none';
905
+ });
906
+ });
907
+ }
908
+
909
+ function scrollToBottom() {
910
+ chatMessages.scrollTop = chatMessages.scrollHeight;
911
+ }
912
+
913
+ function appendMessage(content, type = 'bot', isStreaming = false) {
914
+ if (type === 'bot' && isStreaming) {
915
+ if (!currentStreamingMessage) {
916
+ currentStreamingMessage = document.createElement('div');
917
+ currentStreamingMessage.className = `message ${type}`;
918
+ chatMessages.appendChild(currentStreamingMessage);
919
+
920
+ // Add to conversation history only at the start of streaming
921
+ if (!isStreamingInProgress) {
922
+ conversationHistory.push({
923
+ role: 'assistant',
924
+ content: ''
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
925
  });
926
+ isStreamingInProgress = true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
927
  }
928
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
929
 
930
+ // Replace the existing formatting code in the appendMessage function
931
+ const formattedContent = content
932
+ .replace(/```([\s\S]*?)```/g, (match, code) => `<pre><code>${code}</code></pre>`)
933
+ .replace(/`([^`]+)`/g, '<code>$1</code>')
934
+ .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
935
+ .replace(/\*(.*?)\*/g, '<em>$1</em>')
936
+ .replace(/~~(.*?)~~/g, '<del>$1</del>')
937
+ .replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>')
938
+ .replace(/^#{3}\s+(.*?)$/gm, '<h3>$1</h3>')
939
+ .replace(/^#{2}\s+(.*?)$/gm, '<h2>$1</h2>')
940
+ .replace(/^#{1}\s+(.*?)$/gm, '<h1>$1</h1>')
941
+ .replace(/^\>\s+(.*?)$/gm, '<blockquote>$1</blockquote>')
942
+ .replace(/^(\d+)\.\s+(.*?)$/gm, '<ol start="$1"><li>$2</li></ol>')
943
+ .replace(/^[-*]\s+(.*?)$/gm, '<ul><li>$1</li></ul>')
944
+ .replace(/\n/g, '<br>');
945
 
946
+ currentStreamingMessage.innerHTML += formattedContent;
 
 
947
 
948
+ // Update the last message in conversation history
949
+ if (conversationHistory.length > 0) {
950
+ conversationHistory[conversationHistory.length - 1].content += content;
951
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
952
 
953
+ scrollToBottom();
954
+ } else {
955
+ if (type === 'bot' && currentStreamingMessage) {
956
+ isStreamingInProgress = false;
957
+ currentStreamingMessage = null;
958
+ } else {
959
+ const messageBox = document.createElement('div');
960
+ messageBox.className = `message ${type}`;
961
+
962
+ // Apply markdown-like formatting
963
+ const formattedContent = content
964
+ .replace(/```([\s\S]*?)```/g, (match, code) => `<pre><code>${code}</code></pre>`)
965
+ .replace(/(\d+)\.\s*/g, '<br>$1. ')
966
+ .replace(/\n/g, '<br>')
967
+ .replace(/\*\*(.*?)\*\*/g, (match, p1) => '<strong>' + p1 + '</strong>');
968
+
969
+ messageBox.innerHTML = formattedContent;
970
+ chatMessages.appendChild(messageBox);
971
+
972
+ // Add to conversation history only for new messages
973
+ if (type === 'user') {
974
+ conversationHistory.push({
975
+ role: 'user',
976
+ content: content
977
+ });
978
+ } else if (type === 'bot' && !isStreamingInProgress) {
979
+ conversationHistory.push({
980
+ role: 'assistant',
981
+ content: content
982
+ });
 
 
 
 
 
983
  }
984
+
985
+ scrollToBottom();
986
+ }
987
+ }
988
+ }
989
+
990
+ function clearChat() {
991
+ chatMessages.innerHTML = '';
992
+ conversationHistory = [{ role: "system", content: "You are a helpful AI assistant. Assist the user effectively." }];
993
+ initialInput.style.display = 'flex';
994
+ chatContainer.style.display = 'none';
995
+ }
996
+
997
+ async function sendInitialMessage() {
998
+ const userMessage = initialChatInput.value.trim();
999
+ if (!userMessage || !selectedModel) return;
1000
+
1001
+ initialInput.style.display = 'none';
1002
+ chatContainer.style.display = 'flex';
1003
+
1004
+ appendMessage(userMessage, 'user');
1005
+ initialChatInput.value = '';
1006
+ scrollToBottom();
1007
+
1008
+ // Show the loader, hide the send icon
1009
+ sendLoader.style.display = 'block';
1010
+ sendButtonIcon.querySelector('svg').style.display = 'none';
1011
+
1012
+ try {
1013
+ await callApi(userMessage, selectedModel);
1014
+ } catch (error) {
1015
+ appendMessage("Oops! Something went wrong. Please try again.", 'bot');
1016
+ console.error("API Error:", error);
1017
+ } finally {
1018
+ // Hide loader, show send icon
1019
+ sendLoader.style.display = 'none';
1020
+ sendButtonIcon.querySelector('svg').style.display = 'block';
1021
+ }
1022
+ }
1023
+
1024
+ async function sendMessage() {
1025
+ const userMessage = chatInput.value.trim();
1026
+ if (!userMessage || !selectedModel) return;
1027
+
1028
+ appendMessage(userMessage, 'user');
1029
+ chatInput.value = '';
1030
+
1031
+ // Show the loader, hide the send icon
1032
+ sendLoader.style.display = 'block';
1033
+ sendButtonIcon.querySelector('svg').style.display = 'none';
1034
+
1035
+ try {
1036
+ await callApi(userMessage, selectedModel);
1037
+ } catch (error) {
1038
+ appendMessage("Oops! Something went wrong. Please try again.", 'bot');
1039
+ console.error("API Error:", error);
1040
+ } finally {
1041
+ // Hide loader, show send icon
1042
+ sendLoader.style.display = 'none';
1043
+ sendButtonIcon.querySelector('svg').style.display = 'block';
1044
+ }
1045
+ }
1046
+
1047
+ async function callApi(userMessage, model) {
1048
+ let typingIndicator = null;
1049
+
1050
+ try {
1051
+ // Add typing indicator before any API call
1052
+ typingIndicator = document.createElement('div');
1053
+ typingIndicator.className = 'message bot typing-indicator';
1054
+ typingIndicator.innerHTML = '<span></span><span></span><span></span>';
1055
+ chatMessages.appendChild(typingIndicator);
1056
+ scrollToBottom();
1057
 
1058
+ if (model === "searchgpt") {
1059
+ const url = `https://parthsadaria-lokiai.hf.space/searchgpt?q=${encodeURIComponent(userMessage)}&stream=true&systemprompt=You are **SearchGPT**, an AI with internet access. Reply directly and accurately to user requests. Mention Sources at end`;
 
 
 
 
1060
 
1061
+ const response = await fetch(url, {
1062
+ headers: {
1063
+ 'Authorization': 'playground'
1064
+ }
1065
+ });
 
 
 
 
 
 
 
 
 
 
 
1066
 
1067
+ if (response.ok) {
1068
+ // Remove typing indicator
1069
+ if (typingIndicator) {
1070
+ typingIndicator.remove();
1071
+ typingIndicator = null;
1072
+ }
1073
 
1074
+ const reader = response.body.getReader();
1075
+ const decoder = new TextDecoder("utf-8");
1076
+ let done = false;
1077
+
1078
+ while (!done) {
1079
+ const { value, done: streamDone } = await reader.read();
1080
+ done = streamDone;
1081
 
1082
+ if (value) {
1083
+ const chunk = decoder.decode(value);
1084
+ const cleanedChunk = chunk.trim();
 
 
 
 
 
 
1085
 
1086
+ // Handle different API response formats
1087
+ if (cleanedChunk.startsWith('data:')) {
1088
+ const jsonChunks = cleanedChunk.split("data:").filter(Boolean);
1089
 
1090
+ for (const jsonString of jsonChunks) {
1091
+ try {
1092
+ const cleanJson = jsonString.trim();
1093
+ if (cleanJson === '[DONE]') continue;
 
 
1094
 
1095
+ const jsonData = JSON.parse(cleanJson);
1096
+ const content = jsonData.choices?.[0]?.message?.content ||
1097
+ jsonData.choices?.[0]?.delta?.content || "";
1098
+
1099
+ if (content) {
1100
+ appendMessage(content, 'bot', true);
 
 
 
 
 
 
 
 
 
1101
  }
1102
+ } catch (err) {
1103
+ console.warn("Parsing error:", err, "for chunk:", jsonString);
1104
  }
1105
  }
1106
+ } else {
1107
+ // Handle plain text response
1108
+ appendMessage(cleanedChunk, 'bot', true);
1109
  }
 
 
1110
  }
1111
+ }
1112
+ } else {
1113
+ throw new Error(`API responded with status ${response.status}`);
1114
+ }
1115
+ } else {
1116
+ const url = "https://parthsadaria-lokiai.hf.space/chat/completions";
1117
+ const payload = {
1118
+ model: model,
1119
+ messages: [...conversationHistory],
1120
+ stream: true
1121
+ };
1122
+
1123
+ const headers = {
1124
+ "Content-Type": "application/json",
1125
+ "Authorization": "playground"
1126
+ };
1127
+
1128
+ const response = await fetch(url, {
1129
+ method: "POST",
1130
+ headers: headers,
1131
+ body: JSON.stringify(payload)
1132
+ });
1133
+
1134
+ if (response.ok) {
1135
+ // Remove typing indicator
1136
+ if (typingIndicator) {
1137
+ typingIndicator.remove();
1138
+ typingIndicator = null;
1139
+ }
1140
+
1141
+ const reader = response.body.getReader();
1142
+ const decoder = new TextDecoder("utf-8");
1143
+ let done = false;
1144
+ let buffer = "";
1145
+
1146
+ while (!done) {
1147
+ const { value, done: streamDone } = await reader.read();
1148
+ done = streamDone;
1149
 
1150
+ if (value) {
1151
+ const chunk = decoder.decode(value);
1152
+ buffer += chunk;
 
 
1153
 
1154
+ // Process complete data chunks
1155
+ const lines = buffer.split('\n');
1156
+ buffer = lines.pop() || ""; // Keep the last incomplete line in buffer
 
1157
 
1158
+ for (const line of lines) {
1159
+ if (line.trim() === '') continue;
1160
+
1161
+ const cleanedLine = line.replace(/^data:\s*/, '').trim();
1162
+ if (cleanedLine === '[DONE]') continue;
1163
+
1164
+ try {
1165
+ const jsonData = JSON.parse(cleanedLine);
1166
+ const content = jsonData.choices?.[0]?.delta?.content || "";
1167
+
1168
+ if (content) {
1169
+ appendMessage(content, 'bot', true);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1170
  }
1171
+ } catch (err) {
1172
+ console.warn("Parsing error:", err, "for line:", cleanedLine);
1173
  }
1174
  }
 
 
1175
  }
1176
  }
1177
+
1178
+ // Handle any remaining data in buffer
1179
+ if (buffer.trim() !== '') {
1180
+ try {
1181
+ const cleanedBuffer = buffer.replace(/^data:\s*/, '').trim();
1182
+ if (cleanedBuffer !== '[DONE]') {
1183
+ const jsonData = JSON.parse(cleanedBuffer);
1184
+ const content = jsonData.choices?.[0]?.delta?.content || "";
1185
+
1186
+ if (content) {
1187
+ appendMessage(content, 'bot', true);
1188
+ }
1189
+ }
1190
+ } catch (err) {
1191
+ console.warn("Parsing error for remaining buffer:", err);
1192
+ }
1193
  }
1194
+ } else {
1195
+ throw new Error(`API responded with status ${response.status}`);
1196
  }
1197
+ }
1198
+
1199
+ // End streaming after successful completion
1200
+ appendMessage("", 'bot', false);
1201
+
1202
+ } catch (error) {
1203
+ console.error("API call error:", error);
1204
+ // Remove typing indicator if still present
1205
+ if (typingIndicator) {
1206
+ typingIndicator.remove();
1207
+ }
1208
+ throw error;
1209
+ }
1210
+ }
1211
 
1212
+ // Toggle theme
1213
+ function toggleTheme() {
1214
+ isDarkMode = !isDarkMode;
1215
+ document.body.classList.toggle('light-mode');
1216
+
1217
+ // Update icon based on current theme
1218
+ if (isDarkMode) {
1219
+ themeToggle.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1220
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
1221
+ </svg>`;
1222
+ } else {
1223
+ themeToggle.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1224
+ <circle cx="12" cy="12" r="5"></circle>
1225
+ <line x1="12" y1="1" x2="12" y2="3"></line>
1226
+ <line x1="12" y1="21" x2="12" y2="23"></line>
1227
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
1228
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
1229
+ <line x1="1" y1="12" x2="3" y2="12"></line>
1230
+ <line x1="21" y1="12" x2="23" y2="12"></line>
1231
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
1232
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
1233
+ </svg>`;
1234
+ }
1235
+ }
1236
+
1237
+ // Event Listeners
1238
+ initialSendIcon.addEventListener('click', sendInitialMessage);
1239
+ initialChatInput.addEventListener('keypress', (event) => {
1240
+ if (event.key === 'Enter') sendInitialMessage();
1241
+ });
1242
 
1243
+ sendButtonIcon.addEventListener('click', sendMessage);
1244
+ chatInput.addEventListener('keypress', (event) => {
1245
+ if (event.key === 'Enter') sendMessage();
1246
+ });
1247
 
1248
+ // Clear Chat Button Event Listener
1249
+ clearChatButton.addEventListener('click', clearChat);
 
1250
 
1251
+ // Model selector dropdown event listeners
1252
+ modelSelectDisplay.addEventListener('click', function() {
1253
+ if (modelOptions.style.display === 'block') {
1254
+ modelOptions.style.display = 'none';
1255
+ } else {
1256
+ modelOptions.style.display = 'block';
1257
+ }
1258
+ });
1259
 
1260
+ // Close dropdown when clicking outside
1261
+ document.addEventListener('click', function(event) {
1262
+ if (!event.target.closest('.custom-select-wrapper') && modelOptions.style.display === 'block') {
1263
+ modelOptions.style.display = 'none';
1264
+ }
1265
+ });
1266
+
1267
+ // Theme toggle event listener
1268
+ themeToggle.addEventListener('click', toggleTheme);
1269
 
1270
+ // Initialize the app
1271
+ fetchModels();
1272
+
1273
+ // Add an animation to the chat wrapper
1274
+ if (typeof anime !== 'undefined') {
1275
+ anime({
1276
+ targets: '.chat-wrapper',
1277
+ translateY: [50, 0],
1278
+ opacity: [0, 1],
1279
+ easing: 'easeOutExpo',
1280
+ duration: 1000
1281
+ });
1282
+ }
1283
+
1284
+ // Detect escape key to close model selector
1285
+ document.addEventListener('keydown', function(event) {
1286
+ if (event.key === 'Escape' && modelOptions.style.display === 'block') {
1287
+ modelOptions.style.display = 'none';
1288
+ }
1289
  });
1290
+ });
1291
+ </script>
1292
  </body>
1293
+ </html>