ParthSadaria commited on
Commit
88fcb56
·
verified ·
1 Parent(s): bda7485

Update playground.html

Browse files
Files changed (1) hide show
  1. playground.html +660 -328
playground.html CHANGED
@@ -38,15 +38,26 @@
38
  card: '#1E293B', // Slate 800
39
  input: '#334155', // Slate 700
40
  },
 
 
 
 
 
 
41
  },
42
  animation: {
43
  'bounce-slow': 'bounce 1.5s infinite',
44
  'typing': 'typing 1s infinite',
 
45
  },
46
  keyframes: {
47
  typing: {
48
  '0%, 100%': { opacity: 0 },
49
  '50%': { opacity: 1 },
 
 
 
 
50
  }
51
  }
52
  }
@@ -73,14 +84,97 @@
73
  background: #64748B;
74
  }
75
 
76
- /* Markdown styles */
77
- .markdown-content h1 {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  font-size: 1.8rem;
79
  font-weight: 700;
80
  margin-top: 1.5rem;
81
  margin-bottom: 1rem;
82
  border-bottom: 1px solid #334155;
83
  padding-bottom: 0.5rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  }
85
 
86
  .markdown-content h2 {
@@ -88,6 +182,7 @@
88
  font-weight: 600;
89
  margin-top: 1.2rem;
90
  margin-bottom: 0.8rem;
 
91
  }
92
 
93
  .markdown-content h3 {
@@ -95,17 +190,20 @@
95
  font-weight: 600;
96
  margin-top: 1rem;
97
  margin-bottom: 0.7rem;
 
98
  }
99
 
100
  .markdown-content p {
101
  margin-bottom: 1rem;
102
  line-height: 1.6;
 
103
  }
104
 
105
  .markdown-content ul, .markdown-content ol {
106
  margin-top: 0.5rem;
107
  margin-bottom: 1rem;
108
  padding-left: 1.5rem;
 
109
  }
110
 
111
  .markdown-content ul {
@@ -121,14 +219,14 @@
121
  }
122
 
123
  .markdown-content blockquote {
124
- border-left: 4px solid #6EE7B7;
125
  padding-left: 1rem;
126
- margin-left: 0;
127
- margin-right: 0;
128
  font-style: italic;
129
- background-color: rgba(30, 41, 59, 0.5);
130
  padding: 0.5rem 1rem;
131
  border-radius: 0 4px 4px 0;
 
132
  }
133
 
134
  .markdown-content img {
@@ -144,22 +242,37 @@
144
  margin: 1rem 0;
145
  }
146
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  .markdown-content table th,
148
  .markdown-content table td {
149
  padding: 0.5rem;
150
- border: 1px solid #334155;
151
  }
152
 
153
  .markdown-content table th {
154
- background-color: #1E293B;
155
  font-weight: 600;
156
  }
157
 
158
  .markdown-content table tr:nth-child(even) {
159
- background-color: rgba(30, 41, 59, 0.5);
160
  }
161
 
162
- .markdown-content pre {
163
  margin: 1rem 0;
164
  padding: 1rem;
165
  border-radius: 8px;
@@ -167,7 +280,15 @@
167
  overflow-x: auto;
168
  }
169
 
170
- .markdown-content code {
 
 
 
 
 
 
 
 
171
  font-family: 'JetBrains Mono', monospace;
172
  font-size: 0.9rem;
173
  background-color: #334155;
@@ -175,6 +296,20 @@
175
  border-radius: 4px;
176
  }
177
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  .markdown-content pre code {
179
  background-color: transparent;
180
  padding: 0;
@@ -190,22 +325,127 @@
190
  .message-bubble {
191
  border-radius: 1rem;
192
  max-width: 90%;
 
 
193
  }
194
 
195
- .user-message {
 
 
 
 
196
  background-color: #334155;
197
  margin-left: auto;
198
  border-top-right-radius: 0.25rem;
199
  }
200
 
201
- .assistant-message {
 
 
 
 
 
 
 
202
  background-color: #0F172A;
203
  border-top-left-radius: 0.25rem;
204
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  </style>
206
  </head>
207
 
208
- <body class="dark:bg-dark dark:text-gray-200 min-h-screen transition-colors duration-300 font-sans">
209
  <!-- GitHub Link -->
210
  <a href="https://github.com/ParthSadaria" target="_blank" rel="noopener noreferrer"
211
  class="fixed top-4 right-4 text-gray-500 hover:text-primary-light transition-colors duration-300 z-10"
@@ -216,38 +456,48 @@
216
  </svg>
217
  </a>
218
 
 
 
 
219
  <!-- Chat Container -->
220
  <div class="max-w-4xl mx-auto py-8 px-4 sm:px-6 opacity-0 transform translate-y-4" id="chatWrapper">
221
  <!-- Header with Logo -->
222
- <div class="flex justify-between items-center mb-6 p-4 bg-dark-lighter rounded-xl shadow-lg border border-dark-input">
223
  <div class="flex items-center">
224
  <span class="text-xl font-mono font-bold bg-gradient-to-r from-primary to-primary-light bg-clip-text text-transparent">LOKI.AI</span>
225
- <span class="ml-2 text-sm bg-dark-input px-2 py-0.5 rounded-full text-gray-300">Playground</span>
226
  </div>
227
  <div class="flex space-x-2">
228
  <!-- Model Selector -->
229
  <div class="relative" id="modelSelector">
230
- <button class="px-3 py-2 bg-dark-input rounded-md text-sm flex items-center space-x-1 hover:bg-gray-700 transition-colors shadow-sm" id="modelSelectButton">
231
  <span id="modelSelectDisplay">Select Model</span>
232
  <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
233
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
234
  </svg>
235
  </button>
236
- <div class="absolute mt-1 w-56 rounded-md shadow-lg bg-dark-card ring-1 ring-black ring-opacity-5 hidden max-h-64 overflow-y-auto z-20" id="modelOptions">
237
- <div class="py-2 px-3 text-sm text-gray-400" id="modelLoader">Loading models...</div>
238
  </div>
239
  </div>
240
 
241
  <!-- Clear Chat Button -->
242
- <button id="clearChatButton" class="p-2 rounded-md hover:bg-gray-700 transition-colors bg-dark-input shadow-sm group">
243
- <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 group-hover:text-primary-light transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor">
244
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
245
  </svg>
246
  </button>
247
 
 
 
 
 
 
 
 
248
  <!-- Theme Toggle Button -->
249
- <button id="themeToggle" class="p-2 rounded-md hover:bg-gray-700 transition-colors bg-dark-input shadow-sm group">
250
- <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 group-hover:text-primary-light transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor">
251
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
252
  </svg>
253
  </button>
@@ -259,28 +509,35 @@
259
  <h2 class="text-2xl font-semibold bg-gradient-to-r from-primary to-primary-light bg-clip-text text-transparent">Welcome to LOKI.AI</h2>
260
  <div class="w-full max-w-2xl relative">
261
  <input type="text" id="initialChatInput"
262
- class="w-full p-4 pr-12 rounded-xl border-2 border-dark-input bg-dark-lighter focus:outline-none focus:ring-2 focus:ring-primary-light dark:text-white shadow-lg transition-all"
263
  placeholder="What can I help you with today?">
264
- <button id="initialSendIcon" class="absolute right-4 top-4 text-gray-400 hover:text-primary-light transition-colors">
265
  <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
266
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
267
  </svg>
268
  </button>
269
  </div>
 
 
 
 
 
 
 
270
  </div>
271
 
272
  <!-- Chat Area -->
273
- <div class="hidden flex-col h-[70vh] bg-dark-lighter rounded-xl shadow-lg overflow-hidden border border-dark-input" id="chatContainer">
274
  <!-- Messages Area -->
275
  <div class="flex-1 overflow-y-auto p-4 space-y-5" id="chatMessages"></div>
276
 
277
  <!-- Input Area -->
278
- <div class="border-t border-dark-input p-4">
279
  <div class="relative">
280
- <input type="text" id="chatInput"
281
- class="w-full p-3 pr-12 rounded-xl border border-dark-input bg-dark-input focus:outline-none focus:ring-2 focus:ring-primary-light dark:text-white transition-all shadow-inner"
282
- placeholder="Type your message...">
283
- <button id="sendButton" class="absolute right-3 top-3 text-gray-400 hover:text-primary-light transition-colors">
284
  <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" id="sendIcon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
285
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
286
  </svg>
@@ -289,7 +546,10 @@
289
  </div>
290
  </button>
291
  </div>
292
- <p class="text-center text-gray-500 text-xs mt-2">Models can make mistakes. Check important information.</p>
 
 
 
293
  </div>
294
  </div>
295
  </div>
@@ -313,11 +573,14 @@ document.addEventListener('DOMContentLoaded', function() {
313
  const sendIcon = document.getElementById('sendIcon');
314
  const sendLoader = document.getElementById('sendLoader');
315
  const clearChatButton = document.getElementById('clearChatButton');
 
316
  const modelSelectButton = document.getElementById('modelSelectButton');
317
  const modelSelectDisplay = document.getElementById('modelSelectDisplay');
318
  const modelOptions = document.getElementById('modelOptions');
319
  const modelLoader = document.getElementById('modelLoader');
320
  const themeToggle = document.getElementById('themeToggle');
 
 
321
 
322
  // State variables
323
  let currentStreamingMessage = null;
@@ -326,6 +589,8 @@ document.addEventListener('DOMContentLoaded', function() {
326
  let selectedModel = '';
327
  let modelsList = [];
328
  let isDarkMode = true;
 
 
329
 
330
  // Initialize with animation
331
  anime({
@@ -336,11 +601,92 @@ document.addEventListener('DOMContentLoaded', function() {
336
  duration: 800
337
  });
338
 
339
- // Fetch models from API
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
  async function fetchModels() {
341
  try {
342
  const response = await fetch('https://parthsadaria-lokiai.hf.space/models', {
343
- method: 'GET',
344
  headers: {
345
  'Authorization': 'playground'
346
  }
@@ -348,271 +694,270 @@ document.addEventListener('DOMContentLoaded', function() {
348
 
349
  if (response.ok) {
350
  const data = await response.json();
351
- modelsList = data;
352
- populateModels(data);
 
 
 
 
 
 
 
 
353
  } else {
354
- const fallbackModels = [
355
- { id: "gpt-4o", object: "model" },
356
- { id: "gpt-3.5-turbo", object: "model" },
357
- { id: "claude-3-5-sonnet", object: "model" },
358
- { id: "claude-3-haiku", object: "model" },
359
- { id: "llama-3.1-70b", object: "model" }
360
- ];
361
- populateModels(fallbackModels);
362
  }
363
  } catch (error) {
364
  console.error('Error fetching models:', error);
365
- const fallbackModels = [
366
- { id: "gpt-4o", object: "model" },
367
- { id: "gpt-3.5-turbo", object: "model" },
368
- { id: "claude-3-5-sonnet", object: "model" },
369
- { id: "claude-3-haiku", object: "model" },
370
- { id: "llama-3.1-70b", object: "model" }
371
- ];
372
- populateModels(fallbackModels);
373
  }
374
  }
375
-
376
- // Organize and display models
377
- function populateModels(models) {
378
- // Group models by provider
379
- const providers = {};
 
 
 
 
 
 
 
 
380
  models.forEach(model => {
381
- const modelName = model.id;
382
- let provider = 'Other';
 
383
 
384
- if (modelName.includes('gpt')) provider = 'OpenAI';
385
- else if (modelName.includes('claude')) provider = 'Anthropic';
386
- else if (modelName.includes('llama')) provider = 'Meta';
387
- else if (modelName.includes('gemini')) provider = 'Google';
388
- else if (modelName.includes('mistral')) provider = 'Mistral';
389
- else if (modelName.includes('yi')) provider = 'Yi';
390
 
391
- if (!providers[provider]) providers[provider] = [];
392
- providers[provider].push(model);
393
- });
394
-
395
- // Clear existing options
396
- modelOptions.innerHTML = '';
397
-
398
- // Add special option for web search
399
- const searchOption = document.createElement('div');
400
- searchOption.className = 'px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer';
401
- searchOption.textContent = 'SearchGPT (Web-access)';
402
- searchOption.dataset.value = 'searchgpt';
403
- modelOptions.appendChild(searchOption);
404
-
405
- // Add a separator
406
- const separator = document.createElement('div');
407
- separator.className = 'px-4 py-1 text-xs font-semibold text-gray-500 bg-gray-50 dark:bg-gray-800 dark:text-gray-400';
408
- separator.textContent = 'AI Models';
409
- modelOptions.appendChild(separator);
410
-
411
- // Create and append provider groups and their models
412
- Object.keys(providers).sort().forEach(provider => {
413
- const providerGroup = document.createElement('div');
414
- providerGroup.className = 'px-4 py-1 text-xs font-semibold text-gray-500 bg-gray-50 dark:bg-gray-800 dark:text-gray-400';
415
- providerGroup.textContent = provider;
416
- modelOptions.appendChild(providerGroup);
417
 
418
- providers[provider].sort((a, b) => a.id.localeCompare(b.id)).forEach(model => {
419
- const option = document.createElement('div');
420
- option.className = 'px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer';
421
- option.textContent = model.id;
422
- option.dataset.value = model.id;
423
- modelOptions.appendChild(option);
424
  });
 
 
425
  });
426
 
427
- // Set default model if none selected yet
428
- if (!selectedModel && models.length > 0) {
429
- const preferredModels = ['gpt-4o', 'gpt-4', 'claude-3-5-sonnet', 'gpt-3.5-turbo'];
430
-
431
- // Find the first preferred model that exists in our list
432
- for (const preferred of preferredModels) {
433
- const match = models.find(m => m.id.includes(preferred));
434
- if (match) {
435
- selectedModel = match.id;
436
- modelSelectDisplay.textContent = selectedModel;
437
- break;
438
- }
439
- }
440
-
441
- // If no preferred model found, use the first one in the list
442
- if (!selectedModel) {
443
- selectedModel = models[0].id;
444
- modelSelectDisplay.textContent = selectedModel;
445
- }
446
  }
447
-
448
- // Add event listeners to options
449
- document.querySelectorAll('#modelOptions > div').forEach(option => {
450
- if (option.dataset.value) {
451
- option.addEventListener('click', function() {
452
- selectedModel = this.dataset.value;
453
- modelSelectDisplay.textContent = this.textContent;
454
- modelOptions.classList.add('hidden');
455
- });
456
- }
457
- });
458
  }
459
-
460
- function scrollToBottom() {
461
- chatMessages.scrollTop = chatMessages.scrollHeight;
 
 
 
462
  }
463
- marked.setOptions({
464
- highlight: function(code, lang) {
465
- if (lang && hljs.getLanguage(lang)) {
466
- return hljs.highlight(code, { language: lang }).value;
467
- }
468
- return hljs.highlightAuto(code).value;
469
- },
470
- breaks: true,
471
- gfm: true
472
- });
473
-
474
- function createMessageElement(message, sender) {
475
- const messageEl = document.createElement('div');
476
- messageEl.className = `message-bubble p-4 ${sender === 'user' ? 'user-message' : 'assistant-message'}`;
477
-
478
- if (sender === 'assistant') {
479
- // Use markdown parsing for assistant messages
480
- messageEl.classList.add('markdown-content');
481
- messageEl.innerHTML = marked.parse(message);
 
 
 
 
 
 
 
 
 
482
 
483
  // Apply syntax highlighting to code blocks
484
- messageEl.querySelectorAll('pre code').forEach((block) => {
485
  hljs.highlightElement(block);
486
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
487
  } else {
488
- // For user messages, just use the plain text
489
- messageEl.textContent = message;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
490
  }
 
 
 
 
 
 
 
491
 
492
- const wrapperEl = document.createElement('div');
493
- wrapperEl.className = `flex ${sender === 'user' ? 'justify-end' : 'justify-start'}`;
494
- wrapperEl.appendChild(messageEl);
495
 
496
- return wrapperEl;
497
- }
498
-
499
- function appendMessage(content, type = 'bot', isStreaming = false) {
500
- if (type === 'bot' && isStreaming) {
501
- if (!currentStreamingMessage) {
502
- currentStreamingMessage = createMessageElement(type);
503
- chatMessages.appendChild(currentStreamingMessage);
504
-
505
- // Add to conversation history only at the start of streaming
506
- if (!isStreamingInProgress) {
507
- conversationHistory.push({
508
- role: 'assistant',
509
- content: ''
510
- });
511
- isStreamingInProgress = true;
512
- }
513
  }
514
-
515
- // Format content
516
- const formattedContent = content
517
- .replace(/```([\s\S]*?)```/g, (match, code) => `<pre class="bg-gray-900 text-gray-100 p-2 rounded my-2 overflow-x-auto">${code}</pre>`)
518
- .replace(/`([^`]+)`/g, '<code class="bg-gray-200 dark:bg-gray-700 px-1 rounded text-sm">$1</code>')
519
- .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
520
- .replace(/\*(.*?)\*/g, '<em>$1</em>')
521
- .replace(/\n/g, '<br>');
522
-
523
- currentStreamingMessage.innerHTML += formattedContent;
524
-
525
- // Update the last message in conversation history
526
- if (conversationHistory.length > 0) {
527
- conversationHistory[conversationHistory.length - 1].content += content;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
528
  }
529
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
530
  scrollToBottom();
531
- } else {
532
- if (type === 'bot' && currentStreamingMessage) {
533
- isStreamingInProgress = false;
534
- currentStreamingMessage = null;
535
- } else {
536
- const messageBox = createMessageElement(type);
537
-
538
- // Format content
539
- const formattedContent = content
540
- .replace(/```([\s\S]*?)```/g, (match, code) => `<pre class="bg-gray-900 text-gray-100 p-2 rounded my-2 overflow-x-auto">${code}</pre>`)
541
- .replace(/`([^`]+)`/g, '<code class="bg-gray-200 dark:bg-gray-700 px-1 rounded text-sm">$1</code>')
542
- .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
543
- .replace(/\*(.*?)\*/g, '<em>$1</em>')
544
- .replace(/\n/g, '<br>');
545
-
546
- messageBox.innerHTML = formattedContent;
547
- chatMessages.appendChild(messageBox);
548
-
549
- // Add to conversation history
550
- if (type === 'user') {
551
- conversationHistory.push({
552
- role: 'user',
553
- content: content
554
- });
555
- } else if (type === 'bot' && !isStreamingInProgress) {
556
- conversationHistory.push({
557
- role: 'assistant',
558
- content: content
559
- });
560
- }
561
-
562
- scrollToBottom();
563
- }
564
  }
565
  }
566
-
 
 
 
 
 
 
567
  function clearChat() {
568
- chatMessages.innerHTML = '';
569
- conversationHistory = [{ role: "system", content: "You are a helpful AI assistant. Assist the user effectively." }];
570
- initialInput.style.display = 'flex';
571
- chatContainer.style.display = 'none';
572
-
573
- // Animation for initial input
574
- anime({
575
- targets: '#initialInput',
576
- opacity: [0, 1],
577
- translateY: [20, 0],
578
- easing: 'easeOutExpo',
579
- duration: 500
580
- });
 
 
 
 
 
581
  }
582
-
583
- async function sendInitialMessage() {
 
584
  const userMessage = initialChatInput.value.trim();
585
- if (!userMessage || !selectedModel) return;
 
 
 
586
 
 
587
  initialInput.style.display = 'none';
588
  chatContainer.style.display = 'flex';
589
 
590
- // Animation for chat container
591
- anime({
592
- targets: '#chatContainer',
593
- opacity: [0, 1],
594
- easing: 'easeOutExpo',
595
- duration: 500
596
- });
597
-
598
- appendMessage(userMessage, 'user');
599
  initialChatInput.value = '';
600
-
601
- // Show loader, hide send icon
602
- sendLoader.style.display = 'block';
603
- sendIcon.style.display = 'none';
604
-
605
- try {
606
- await callApi(userMessage, selectedModel);
607
- } catch (error) {
608
- appendMessage("Oops! Something went wrong. Please try again.", 'bot');
609
- } finally {
610
- // Hide loader, show send icon
611
- sendLoader.style.display = 'none';
612
- sendIcon.style.display = 'block';
613
- }
614
  }
615
-
616
  async function sendMessage() {
617
  const userMessage = chatInput.value.trim();
618
  if (!userMessage || !selectedModel) return;
@@ -808,10 +1153,57 @@ document.addEventListener('DOMContentLoaded', function() {
808
 
809
  sendButton.addEventListener('click', sendMessage);
810
  chatInput.addEventListener('keypress', (event) => {
811
- if (event.key === 'Enter') sendMessage();
 
 
 
812
  });
813
 
814
  clearChatButton.addEventListener('click', clearChat);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
815
 
816
  // Model selector
817
  modelSelectButton.addEventListener('click', function(e) {
@@ -827,74 +1219,14 @@ document.addEventListener('DOMContentLoaded', function() {
827
  // Theme toggle
828
  themeToggle.addEventListener('click', toggleTheme);
829
 
 
 
 
 
 
 
830
  // Initialize
831
- // Initialize
832
  fetchModels();
833
-
834
- // Add event listeners to prevent propagation within model options
835
- modelOptions.addEventListener('click', function(e) {
836
- e.stopPropagation();
837
- });
838
-
839
- // Add responsive behavior
840
- function resizeChat() {
841
- if (window.innerWidth < 640) {
842
- chatContainer.style.height = 'calc(100vh - 180px)';
843
- } else {
844
- chatContainer.style.height = '70vh';
845
- }
846
- }
847
- function showTypingIndicator() {
848
- const typingEl = document.createElement('div');
849
- typingEl.className = 'message-bubble assistant-message p-3 typing-indicator flex';
850
- typingEl.innerHTML = `
851
- <span class="h-2 w-2 bg-primary-light rounded-full mx-0.5" style="--dot-index: 0"></span>
852
- <span class="h-2 w-2 bg-primary-light rounded-full mx-0.5" style="--dot-index: 1"></span>
853
- <span class="h-2 w-2 bg-primary-light rounded-full mx-0.5" style="--dot-index: 2"></span>
854
- `;
855
-
856
- const wrapperEl = document.createElement('div');
857
- wrapperEl.className = 'flex justify-start typing-wrapper';
858
- wrapperEl.appendChild(typingEl);
859
-
860
- document.getElementById('chatMessages').appendChild(wrapperEl);
861
- document.getElementById('chatMessages').scrollTop = document.getElementById('chatMessages').scrollHeight;
862
-
863
- return wrapperEl;
864
- }
865
-
866
- // Function to remove typing indicator
867
- function removeTypingIndicator() {
868
- const typingWrapper = document.querySelector('.typing-wrapper');
869
- if (typingWrapper) {
870
- typingWrapper.remove();
871
- }
872
- }
873
- window.addEventListener('resize', resizeChat);
874
- resizeChat();
875
-
876
- // Check for saved theme preference
877
- if (localStorage.getItem('darkMode') === 'false') {
878
- toggleTheme();
879
- }
880
-
881
- // Save theme preference when changed
882
- themeToggle.addEventListener('click', function() {
883
- localStorage.setItem('darkMode', isDarkMode);
884
- });
885
-
886
- // Keyboard shortcuts
887
- document.addEventListener('keydown', function(event) {
888
- // Ctrl+Enter to send in chat area
889
- if (event.ctrlKey && event.key === 'Enter' && document.activeElement === chatInput) {
890
- sendMessage();
891
- }
892
-
893
- // Escape to close model dropdown
894
- if (event.key === 'Escape') {
895
- modelOptions.classList.add('hidden');
896
- }
897
- });
898
  });
899
  </script>
900
  </body>
 
38
  card: '#1E293B', // Slate 800
39
  input: '#334155', // Slate 700
40
  },
41
+ light: {
42
+ DEFAULT: '#F8FAFC',
43
+ darker: '#E2E8F0',
44
+ card: '#FFFFFF',
45
+ input: '#F1F5F9',
46
+ }
47
  },
48
  animation: {
49
  'bounce-slow': 'bounce 1.5s infinite',
50
  'typing': 'typing 1s infinite',
51
+ 'fade-in': 'fadeIn 0.5s ease-in-out',
52
  },
53
  keyframes: {
54
  typing: {
55
  '0%, 100%': { opacity: 0 },
56
  '50%': { opacity: 1 },
57
+ },
58
+ fadeIn: {
59
+ '0%': { opacity: 0 },
60
+ '100%': { opacity: 1 },
61
  }
62
  }
63
  }
 
84
  background: #64748B;
85
  }
86
 
87
+ /* Custom scrollbar for light theme */
88
+ ::-webkit-scrollbar {
89
+ width: 8px;
90
+ height: 8px;
91
+ }
92
+
93
+ ::-webkit-scrollbar-track {
94
+ background: #E2E8F0;
95
+ }
96
+
97
+ ::-webkit-scrollbar-thumb {
98
+ background: #94A3B8;
99
+ border-radius: 4px;
100
+ }
101
+
102
+ ::-webkit-scrollbar-thumb:hover {
103
+ background: #64748B;
104
+ }
105
+
106
+ /* Markdown styles - Dark */
107
+ .dark .markdown-content h1 {
108
  font-size: 1.8rem;
109
  font-weight: 700;
110
  margin-top: 1.5rem;
111
  margin-bottom: 1rem;
112
  border-bottom: 1px solid #334155;
113
  padding-bottom: 0.5rem;
114
+ color: #F1F5F9;
115
+ }
116
+
117
+ .dark .markdown-content h2 {
118
+ font-size: 1.5rem;
119
+ font-weight: 600;
120
+ margin-top: 1.2rem;
121
+ margin-bottom: 0.8rem;
122
+ color: #F1F5F9;
123
+ }
124
+
125
+ .dark .markdown-content h3 {
126
+ font-size: 1.3rem;
127
+ font-weight: 600;
128
+ margin-top: 1rem;
129
+ margin-bottom: 0.7rem;
130
+ color: #F1F5F9;
131
+ }
132
+
133
+ .dark .markdown-content p {
134
+ margin-bottom: 1rem;
135
+ line-height: 1.6;
136
+ color: #E2E8F0;
137
+ }
138
+
139
+ .dark .markdown-content ul, .dark .markdown-content ol {
140
+ margin-top: 0.5rem;
141
+ margin-bottom: 1rem;
142
+ padding-left: 1.5rem;
143
+ color: #E2E8F0;
144
+ }
145
+
146
+ .dark .markdown-content ul {
147
+ list-style-type: disc;
148
+ }
149
+
150
+ .dark .markdown-content ol {
151
+ list-style-type: decimal;
152
+ }
153
+
154
+ .dark .markdown-content li {
155
+ margin-bottom: 0.5rem;
156
+ }
157
+
158
+ .dark .markdown-content blockquote {
159
+ border-left: 4px solid #6EE7B7;
160
+ padding-left: 1rem;
161
+ margin: 1rem 0;
162
+ font-style: italic;
163
+ background-color: rgba(30, 41, 59, 0.5);
164
+ padding: 0.5rem 1rem;
165
+ border-radius: 0 4px 4px 0;
166
+ color: #CBD5E1;
167
+ }
168
+
169
+ /* Markdown styles - Light */
170
+ .markdown-content h1 {
171
+ font-size: 1.8rem;
172
+ font-weight: 700;
173
+ margin-top: 1.5rem;
174
+ margin-bottom: 1rem;
175
+ border-bottom: 1px solid #E2E8F0;
176
+ padding-bottom: 0.5rem;
177
+ color: #0F172A;
178
  }
179
 
180
  .markdown-content h2 {
 
182
  font-weight: 600;
183
  margin-top: 1.2rem;
184
  margin-bottom: 0.8rem;
185
+ color: #0F172A;
186
  }
187
 
188
  .markdown-content h3 {
 
190
  font-weight: 600;
191
  margin-top: 1rem;
192
  margin-bottom: 0.7rem;
193
+ color: #0F172A;
194
  }
195
 
196
  .markdown-content p {
197
  margin-bottom: 1rem;
198
  line-height: 1.6;
199
+ color: #334155;
200
  }
201
 
202
  .markdown-content ul, .markdown-content ol {
203
  margin-top: 0.5rem;
204
  margin-bottom: 1rem;
205
  padding-left: 1.5rem;
206
+ color: #334155;
207
  }
208
 
209
  .markdown-content ul {
 
219
  }
220
 
221
  .markdown-content blockquote {
222
+ border-left: 4px solid #10B981;
223
  padding-left: 1rem;
224
+ margin: 1rem 0;
 
225
  font-style: italic;
226
+ background-color: rgba(241, 245, 249, 0.5);
227
  padding: 0.5rem 1rem;
228
  border-radius: 0 4px 4px 0;
229
+ color: #475569;
230
  }
231
 
232
  .markdown-content img {
 
242
  margin: 1rem 0;
243
  }
244
 
245
+ .dark .markdown-content table th,
246
+ .dark .markdown-content table td {
247
+ padding: 0.5rem;
248
+ border: 1px solid #334155;
249
+ }
250
+
251
+ .dark .markdown-content table th {
252
+ background-color: #1E293B;
253
+ font-weight: 600;
254
+ }
255
+
256
+ .dark .markdown-content table tr:nth-child(even) {
257
+ background-color: rgba(30, 41, 59, 0.5);
258
+ }
259
+
260
  .markdown-content table th,
261
  .markdown-content table td {
262
  padding: 0.5rem;
263
+ border: 1px solid #E2E8F0;
264
  }
265
 
266
  .markdown-content table th {
267
+ background-color: #F1F5F9;
268
  font-weight: 600;
269
  }
270
 
271
  .markdown-content table tr:nth-child(even) {
272
+ background-color: rgba(241, 245, 249, 0.5);
273
  }
274
 
275
+ .dark .markdown-content pre {
276
  margin: 1rem 0;
277
  padding: 1rem;
278
  border-radius: 8px;
 
280
  overflow-x: auto;
281
  }
282
 
283
+ .markdown-content pre {
284
+ margin: 1rem 0;
285
+ padding: 1rem;
286
+ border-radius: 8px;
287
+ background-color: #F1F5F9 !important;
288
+ overflow-x: auto;
289
+ }
290
+
291
+ .dark .markdown-content code {
292
  font-family: 'JetBrains Mono', monospace;
293
  font-size: 0.9rem;
294
  background-color: #334155;
 
296
  border-radius: 4px;
297
  }
298
 
299
+ .markdown-content code {
300
+ font-family: 'JetBrains Mono', monospace;
301
+ font-size: 0.9rem;
302
+ background-color: #E2E8F0;
303
+ padding: 0.2rem 0.4rem;
304
+ border-radius: 4px;
305
+ }
306
+
307
+ .dark .markdown-content pre code {
308
+ background-color: transparent;
309
+ padding: 0;
310
+ border-radius: 0;
311
+ }
312
+
313
  .markdown-content pre code {
314
  background-color: transparent;
315
  padding: 0;
 
325
  .message-bubble {
326
  border-radius: 1rem;
327
  max-width: 90%;
328
+ transition: all 0.3s ease;
329
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
330
  }
331
 
332
+ .dark .message-bubble {
333
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
334
+ }
335
+
336
+ .dark .user-message {
337
  background-color: #334155;
338
  margin-left: auto;
339
  border-top-right-radius: 0.25rem;
340
  }
341
 
342
+ .user-message {
343
+ background-color: #E2E8F0;
344
+ margin-left: auto;
345
+ border-top-right-radius: 0.25rem;
346
+ color: #0F172A;
347
+ }
348
+
349
+ .dark .assistant-message {
350
  background-color: #0F172A;
351
  border-top-left-radius: 0.25rem;
352
  }
353
+
354
+ .assistant-message {
355
+ background-color: #FFFFFF;
356
+ border-top-left-radius: 0.25rem;
357
+ color: #334155;
358
+ border: 1px solid #E2E8F0;
359
+ }
360
+
361
+ .copy-button {
362
+ opacity: 0;
363
+ transition: opacity 0.2s ease-in-out;
364
+ }
365
+
366
+ .message-bubble:hover .copy-button {
367
+ opacity: 1;
368
+ }
369
+
370
+ /* Toast notification */
371
+ .toast {
372
+ position: fixed;
373
+ bottom: 20px;
374
+ left: 50%;
375
+ transform: translateX(-50%);
376
+ padding: 0.5rem 1rem;
377
+ border-radius: 0.5rem;
378
+ background-color: #334155;
379
+ color: white;
380
+ z-index: 50;
381
+ opacity: 0;
382
+ transition: opacity 0.3s ease;
383
+ }
384
+
385
+ .toast.show {
386
+ opacity: 1;
387
+ }
388
+
389
+ /* Dropdown transition */
390
+ .dropdown-transition {
391
+ transition: opacity 0.2s ease, transform 0.2s ease;
392
+ }
393
+
394
+ /* Message timestamp */
395
+ .message-timestamp {
396
+ font-size: 0.7rem;
397
+ color: #64748B;
398
+ margin-top: 0.25rem;
399
+ }
400
+
401
+ .message-transition-enter {
402
+ opacity: 0;
403
+ transform: translateY(10px);
404
+ }
405
+
406
+ .message-transition-enter-active {
407
+ opacity: 1;
408
+ transform: translateY(0px);
409
+ transition: opacity 300ms, transform 300ms;
410
+ }
411
+
412
+ /* Mobile responsiveness */
413
+ @media (max-width: 640px) {
414
+ .message-bubble {
415
+ max-width: 85%;
416
+ }
417
+
418
+ .markdown-content pre {
419
+ max-width: 100%;
420
+ overflow-x: auto;
421
+ }
422
+ }
423
+
424
+ /* Focus states for accessibility */
425
+ button:focus, input:focus {
426
+ outline: 2px solid #10B981;
427
+ outline-offset: 2px;
428
+ }
429
+
430
+ /* Animation for message appearance */
431
+ @keyframes messageAppear {
432
+ from {
433
+ opacity: 0;
434
+ transform: translateY(10px);
435
+ }
436
+ to {
437
+ opacity: 1;
438
+ transform: translateY(0);
439
+ }
440
+ }
441
+
442
+ .message-appear {
443
+ animation: messageAppear 0.3s ease-out forwards;
444
+ }
445
  </style>
446
  </head>
447
 
448
+ <body class="bg-light-DEFAULT dark:bg-dark dark:text-gray-200 min-h-screen transition-colors duration-300 font-sans">
449
  <!-- GitHub Link -->
450
  <a href="https://github.com/ParthSadaria" target="_blank" rel="noopener noreferrer"
451
  class="fixed top-4 right-4 text-gray-500 hover:text-primary-light transition-colors duration-300 z-10"
 
456
  </svg>
457
  </a>
458
 
459
+ <!-- Toast Notification -->
460
+ <div id="toast" class="toast">Message copied to clipboard!</div>
461
+
462
  <!-- Chat Container -->
463
  <div class="max-w-4xl mx-auto py-8 px-4 sm:px-6 opacity-0 transform translate-y-4" id="chatWrapper">
464
  <!-- Header with Logo -->
465
+ <div class="flex justify-between items-center mb-6 p-4 bg-light-darker dark:bg-dark-lighter rounded-xl shadow-lg border border-light-darker dark:border-dark-input">
466
  <div class="flex items-center">
467
  <span class="text-xl font-mono font-bold bg-gradient-to-r from-primary to-primary-light bg-clip-text text-transparent">LOKI.AI</span>
468
+ <span class="ml-2 text-sm bg-light-input dark:bg-dark-input px-2 py-0.5 rounded-full text-gray-600 dark:text-gray-300">Playground</span>
469
  </div>
470
  <div class="flex space-x-2">
471
  <!-- Model Selector -->
472
  <div class="relative" id="modelSelector">
473
+ <button class="px-3 py-2 bg-light-input dark:bg-dark-input rounded-md text-sm flex items-center space-x-1 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors shadow-sm" id="modelSelectButton" aria-label="Select AI model">
474
  <span id="modelSelectDisplay">Select Model</span>
475
  <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
476
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
477
  </svg>
478
  </button>
479
+ <div class="absolute mt-1 w-56 rounded-md shadow-lg bg-light-card dark:bg-dark-card ring-1 ring-black ring-opacity-5 hidden max-h-64 overflow-y-auto z-20 dropdown-transition" id="modelOptions">
480
+ <div class="py-2 px-3 text-sm text-gray-600 dark:text-gray-400" id="modelLoader">Loading models...</div>
481
  </div>
482
  </div>
483
 
484
  <!-- Clear Chat Button -->
485
+ <button id="clearChatButton" class="p-2 rounded-md hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors bg-light-input dark:bg-dark-input shadow-sm group" aria-label="Clear chat history">
486
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-500 group-hover:text-primary dark:text-gray-400 dark:group-hover:text-primary-light transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor">
487
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
488
  </svg>
489
  </button>
490
 
491
+ <!-- Export Chat Button -->
492
+ <button id="exportChatButton" class="p-2 rounded-md hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors bg-light-input dark:bg-dark-input shadow-sm group" aria-label="Export conversation">
493
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-500 group-hover:text-primary dark:text-gray-400 dark:group-hover:text-primary-light transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor">
494
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
495
+ </svg>
496
+ </button>
497
+
498
  <!-- Theme Toggle Button -->
499
+ <button id="themeToggle" class="p-2 rounded-md hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors bg-light-input dark:bg-dark-input shadow-sm group" aria-label="Toggle theme">
500
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-500 group-hover:text-primary dark:text-gray-400 dark:group-hover:text-primary-light transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor">
501
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
502
  </svg>
503
  </button>
 
509
  <h2 class="text-2xl font-semibold bg-gradient-to-r from-primary to-primary-light bg-clip-text text-transparent">Welcome to LOKI.AI</h2>
510
  <div class="w-full max-w-2xl relative">
511
  <input type="text" id="initialChatInput"
512
+ class="w-full p-4 pr-12 rounded-xl border-2 border-light-darker dark:border-dark-input bg-light-input dark:bg-dark-lighter focus:outline-none focus:ring-2 focus:ring-primary-light dark:text-white text-gray-800 shadow-lg transition-all"
513
  placeholder="What can I help you with today?">
514
+ <button id="initialSendIcon" class="absolute right-4 top-4 text-gray-400 hover:text-primary transition-colors" aria-label="Send message">
515
  <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
516
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
517
  </svg>
518
  </button>
519
  </div>
520
+ <!-- Model suggestions -->
521
+ <div class="flex flex-wrap justify-center gap-2 max-w-2xl">
522
+ <button class="px-3 py-2 bg-light-input dark:bg-dark-input rounded-full text-sm hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors shadow-sm text-gray-600 dark:text-gray-300 quick-prompt">Explain quantum computing</button>
523
+ <button class="px-3 py-2 bg-light-input dark:bg-dark-input rounded-full text-sm hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors shadow-sm text-gray-600 dark:text-gray-300 quick-prompt">Write a poem about AI</button>
524
+ <button class="px-3 py-2 bg-light-input dark:bg-dark-input rounded-full text-sm hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors shadow-sm text-gray-600 dark:text-gray-300 quick-prompt">Help debug my code</button>
525
+ <button class="px-3 py-2 bg-light-input dark:bg-dark-input rounded-full text-sm hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors shadow-sm text-gray-600 dark:text-gray-300 quick-prompt">Recommend a book</button>
526
+ </div>
527
  </div>
528
 
529
  <!-- Chat Area -->
530
+ <div class="hidden flex-col h-[70vh] bg-light-card dark:bg-dark-lighter rounded-xl shadow-lg overflow-hidden border border-light-darker dark:border-dark-input" id="chatContainer">
531
  <!-- Messages Area -->
532
  <div class="flex-1 overflow-y-auto p-4 space-y-5" id="chatMessages"></div>
533
 
534
  <!-- Input Area -->
535
+ <div class="border-t border-light-darker dark:border-dark-input p-4 bg-light-card dark:bg-dark-lighter">
536
  <div class="relative">
537
+ <textarea id="chatInput"
538
+ class="w-full p-3 pr-12 rounded-xl border border-light-darker dark:border-dark-input bg-light-input dark:bg-dark-input focus:outline-none focus:ring-2 focus:ring-primary dark:focus:ring-primary-light text-gray-800 dark:text-white transition-all shadow-inner resize-none min-h-[50px] max-h-[150px]"
539
+ placeholder="Type your message..." rows="1"></textarea>
540
+ <button id="sendButton" class="absolute right-3 bottom-3 text-gray-400 hover:text-primary dark:hover:text-primary-light transition-colors" aria-label="Send message">
541
  <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" id="sendIcon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
542
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
543
  </svg>
 
546
  </div>
547
  </button>
548
  </div>
549
+ <div class="flex justify-between items-center mt-2">
550
+ <p class="text-gray-500 text-xs">Ctrl+Enter to send</p>
551
+ <p class="text-gray-500 text-xs">Models may make mistakes. Check important information.</p>
552
+ </div>
553
  </div>
554
  </div>
555
  </div>
 
573
  const sendIcon = document.getElementById('sendIcon');
574
  const sendLoader = document.getElementById('sendLoader');
575
  const clearChatButton = document.getElementById('clearChatButton');
576
+ const exportChatButton = document.getElementById('exportChatButton');
577
  const modelSelectButton = document.getElementById('modelSelectButton');
578
  const modelSelectDisplay = document.getElementById('modelSelectDisplay');
579
  const modelOptions = document.getElementById('modelOptions');
580
  const modelLoader = document.getElementById('modelLoader');
581
  const themeToggle = document.getElementById('themeToggle');
582
+ const toast = document.getElementById('toast');
583
+ const quickPromptButtons = document.querySelectorAll('.quick-prompt');
584
 
585
  // State variables
586
  let currentStreamingMessage = null;
 
589
  let selectedModel = '';
590
  let modelsList = [];
591
  let isDarkMode = true;
592
+ let autoScrollEnabled = true;
593
+ let lastMessageTime = null;
594
 
595
  // Initialize with animation
596
  anime({
 
601
  duration: 800
602
  });
603
 
604
+ // Auto-resize textarea
605
+ function autoResizeTextarea(textarea) {
606
+ textarea.style.height = 'auto';
607
+ const newHeight = Math.min(Math.max(textarea.scrollHeight, 50), 150);
608
+ textarea.style.height = newHeight + 'px';
609
+ }
610
+
611
+ chatInput.addEventListener('input', function() {
612
+ autoResizeTextarea(this);
613
+ });
614
+
615
+ // Quick prompt buttons
616
+ quickPromptButtons.forEach(button => {
617
+ button.addEventListener('click', function() {
618
+ initialChatInput.value = this.textContent;
619
+ initialChatInput.focus();
620
+ });
621
+ });
622
+
623
+ // Show toast notification
624
+ function showToast(message, duration = 2000) {
625
+ toast.textContent = message;
626
+ toast.classList.add('show');
627
+
628
+ setTimeout(() => {
629
+ toast.classList.remove('show');
630
+ }, duration);
631
+ }
632
+
633
+ // Copy text to clipboard
634
+ function copyToClipboard(text) {
635
+ navigator.clipboard.writeText(text).then(() => {
636
+ showToast('Copied to clipboard!');
637
+ }).catch(err => {
638
+ console.error('Failed to copy: ', err);
639
+ showToast('Failed to copy text');
640
+ });
641
+ }
642
+
643
+ // Export conversation as markdown or JSON
644
+ function exportConversation(format = 'markdown') {
645
+ if (conversationHistory.length <= 1) {
646
+ showToast('No conversation to export');
647
+ return;
648
+ }
649
+
650
+ let content = '';
651
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
652
+ let filename = '';
653
+
654
+ if (format === 'markdown') {
655
+ content = '# LOKI.AI Conversation\n\n';
656
+ content += `*Exported on: ${new Date().toLocaleString()}*\n\n`;
657
+
658
+ for (let i = 1; i < conversationHistory.length; i++) {
659
+ const message = conversationHistory[i];
660
+ if (message.role === 'user') {
661
+ content += `## User\n\n${message.content}\n\n`;
662
+ } else if (message.role === 'assistant') {
663
+ content += `## Assistant\n\n${message.content}\n\n`;
664
+ }
665
+ }
666
+
667
+ filename = `loki-ai-conversation-${timestamp}.md`;
668
+ } else if (format === 'json') {
669
+ content = JSON.stringify(conversationHistory, null, 2);
670
+ filename = `loki-ai-conversation-${timestamp}.json`;
671
+ }
672
+
673
+ const blob = new Blob([content], { type: 'text/plain' });
674
+ const url = URL.createObjectURL(blob);
675
+ const a = document.createElement('a');
676
+ a.href = url;
677
+ a.download = filename;
678
+ document.body.appendChild(a);
679
+ a.click();
680
+ document.body.removeChild(a);
681
+ URL.revokeObjectURL(url);
682
+
683
+ showToast(`Exported as ${format}`);
684
+ }
685
+
686
+ // Fetch available models
687
  async function fetchModels() {
688
  try {
689
  const response = await fetch('https://parthsadaria-lokiai.hf.space/models', {
 
690
  headers: {
691
  'Authorization': 'playground'
692
  }
 
694
 
695
  if (response.ok) {
696
  const data = await response.json();
697
+ modelsList = data.data || [];
698
+
699
+ // Add search GPT option
700
+ modelsList.push({
701
+ id: 'searchgpt',
702
+ displayName: 'SearchGPT (with internet)',
703
+ description: 'GPT model with internet search capability'
704
+ });
705
+
706
+ populateModelOptions(modelsList);
707
  } else {
708
+ modelLoader.textContent = 'Failed to load models';
 
 
 
 
 
 
 
709
  }
710
  } catch (error) {
711
  console.error('Error fetching models:', error);
712
+ modelLoader.textContent = 'Error loading models';
 
 
 
 
 
 
 
713
  }
714
  }
715
+
716
+ // Populate model dropdown
717
+ function populateModelOptions(models) {
718
+ modelOptions.innerHTML = '';
719
+
720
+ if (models.length === 0) {
721
+ const noModels = document.createElement('div');
722
+ noModels.className = 'py-2 px-4 text-sm text-gray-600 dark:text-gray-400';
723
+ noModels.textContent = 'No models available';
724
+ modelOptions.appendChild(noModels);
725
+ return;
726
+ }
727
+
728
  models.forEach(model => {
729
+ const option = document.createElement('div');
730
+ option.className = 'py-2 px-4 text-sm hover:bg-light-input dark:hover:bg-dark-input cursor-pointer';
731
+ option.dataset.modelId = model.id;
732
 
733
+ const modelName = document.createElement('div');
734
+ modelName.className = 'font-medium text-gray-800 dark:text-gray-200';
735
+ modelName.textContent = model.displayName || model.id;
 
 
 
736
 
737
+ const modelDesc = document.createElement('div');
738
+ modelDesc.className = 'text-xs text-gray-500 dark:text-gray-400';
739
+ modelDesc.textContent = model.description || '';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
740
 
741
+ option.appendChild(modelName);
742
+ option.appendChild(modelDesc);
743
+
744
+ option.addEventListener('click', function() {
745
+ selectModel(model);
 
746
  });
747
+
748
+ modelOptions.appendChild(option);
749
  });
750
 
751
+ // Default to first model if available
752
+ if (models.length > 0 && !selectedModel) {
753
+ selectModel(models[0]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
754
  }
 
 
 
 
 
 
 
 
 
 
 
755
  }
756
+
757
+ // Select a model
758
+ function selectModel(model) {
759
+ selectedModel = model.id;
760
+ modelSelectDisplay.textContent = model.displayName || model.id;
761
+ modelOptions.classList.add('hidden');
762
  }
763
+
764
+ // Format timestamp
765
+ function formatTimestamp(timestamp) {
766
+ const date = new Date(timestamp);
767
+ return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
768
+ }
769
+
770
+ // Append message to chat
771
+ function appendMessage(text, sender, isStreaming = false) {
772
+ const now = new Date();
773
+ const messageTime = formatTimestamp(now);
774
+
775
+ if (isStreaming) {
776
+ if (!currentStreamingMessage) {
777
+ // Create new message container for streaming
778
+ isStreamingInProgress = true;
779
+ lastMessageTime = now;
780
+
781
+ const messageDiv = document.createElement('div');
782
+ messageDiv.className = 'message-appear flex flex-col items-start';
783
+
784
+ const messageBubble = document.createElement('div');
785
+ messageBubble.className = `message-bubble p-3 rounded-lg ${sender === 'user' ? 'user-message bg-light-darker dark:bg-dark-input text-right ml-auto' : 'assistant-message bg-light-card dark:bg-dark-lighter'}`;
786
+
787
+ // Content div with markdown support
788
+ const contentDiv = document.createElement('div');
789
+ contentDiv.className = 'markdown-content break-words';
790
+ contentDiv.innerHTML = marked.parse(text);
791
 
792
  // Apply syntax highlighting to code blocks
793
+ contentDiv.querySelectorAll('pre code').forEach((block) => {
794
  hljs.highlightElement(block);
795
  });
796
+
797
+ // Add copy button for assistant messages
798
+ if (sender === 'bot') {
799
+ const copyButton = document.createElement('button');
800
+ copyButton.className = 'copy-button text-xs text-gray-500 hover:text-primary dark:hover:text-primary-light mt-2 flex items-center float-right';
801
+ copyButton.innerHTML = `
802
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
803
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-8M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" />
804
+ </svg>
805
+ Copy
806
+ `;
807
+ copyButton.addEventListener('click', () => {
808
+ copyToClipboard(contentDiv.textContent);
809
+ });
810
+ messageBubble.appendChild(copyButton);
811
+ }
812
+
813
+ messageBubble.appendChild(contentDiv);
814
+ messageDiv.appendChild(messageBubble);
815
+
816
+ // Timestamp
817
+ const timestampDiv = document.createElement('div');
818
+ timestampDiv.className = 'message-timestamp';
819
+ timestampDiv.textContent = messageTime;
820
+ messageDiv.appendChild(timestampDiv);
821
+
822
+ chatMessages.appendChild(messageDiv);
823
+ currentStreamingMessage = contentDiv;
824
+
825
+ // Update conversation history
826
+ conversationHistory.push({
827
+ role: sender === 'user' ? 'user' : 'assistant',
828
+ content: text
829
+ });
830
  } else {
831
+ // Append to existing streaming message
832
+ if (currentStreamingMessage) {
833
+ // Update content with latest text
834
+ const existingContent = conversationHistory[conversationHistory.length - 1].content;
835
+ const updatedContent = existingContent + text;
836
+
837
+ conversationHistory[conversationHistory.length - 1].content = updatedContent;
838
+
839
+ // Update display
840
+ currentStreamingMessage.innerHTML = marked.parse(updatedContent);
841
+
842
+ // Apply syntax highlighting to code blocks
843
+ currentStreamingMessage.querySelectorAll('pre code').forEach((block) => {
844
+ hljs.highlightElement(block);
845
+ });
846
+ }
847
  }
848
+ } else if (isStreamingInProgress) {
849
+ // End of streaming
850
+ isStreamingInProgress = false;
851
+ currentStreamingMessage = null;
852
+ } else {
853
+ // Regular message (not streaming)
854
+ lastMessageTime = now;
855
 
856
+ const messageDiv = document.createElement('div');
857
+ messageDiv.className = 'message-appear flex flex-col items-start';
 
858
 
859
+ if (sender === 'user') {
860
+ messageDiv.classList.add('items-end');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
861
  }
862
+
863
+ const messageBubble = document.createElement('div');
864
+ messageBubble.className = `message-bubble p-3 rounded-lg ${sender === 'user' ? 'user-message bg-light-darker dark:bg-dark-input text-right ml-auto' : 'assistant-message bg-light-card dark:bg-dark-lighter'}`;
865
+
866
+ // Content div with markdown support
867
+ const contentDiv = document.createElement('div');
868
+ contentDiv.className = 'markdown-content break-words';
869
+ contentDiv.innerHTML = marked.parse(text);
870
+
871
+ // Apply syntax highlighting to code blocks
872
+ contentDiv.querySelectorAll('pre code').forEach((block) => {
873
+ hljs.highlightElement(block);
874
+ });
875
+
876
+ // Add copy button for assistant messages
877
+ if (sender === 'bot') {
878
+ const copyButton = document.createElement('button');
879
+ copyButton.className = 'copy-button text-xs text-gray-500 hover:text-primary dark:hover:text-primary-light mt-2 flex items-center float-right';
880
+ copyButton.innerHTML = `
881
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
882
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-8M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" />
883
+ </svg>
884
+ Copy
885
+ `;
886
+ copyButton.addEventListener('click', () => {
887
+ copyToClipboard(contentDiv.textContent);
888
+ });
889
+ messageBubble.appendChild(copyButton);
890
  }
891
+
892
+ messageBubble.appendChild(contentDiv);
893
+ messageDiv.appendChild(messageBubble);
894
+
895
+ // Timestamp
896
+ const timestampDiv = document.createElement('div');
897
+ timestampDiv.className = 'message-timestamp';
898
+ timestampDiv.textContent = messageTime;
899
+ messageDiv.appendChild(timestampDiv);
900
+
901
+ chatMessages.appendChild(messageDiv);
902
+
903
+ // Update conversation history
904
+ conversationHistory.push({
905
+ role: sender === 'user' ? 'user' : 'assistant',
906
+ content: text
907
+ });
908
+ }
909
+
910
+ // Scroll to bottom if auto-scroll is enabled
911
+ if (autoScrollEnabled) {
912
  scrollToBottom();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
913
  }
914
  }
915
+
916
+ // Scroll chat to bottom
917
+ function scrollToBottom() {
918
+ chatMessages.scrollTop = chatMessages.scrollHeight;
919
+ }
920
+
921
+ // Clear chat history
922
  function clearChat() {
923
+ if (isStreamingInProgress) {
924
+ showToast('Cannot clear chat while message is streaming');
925
+ return;
926
+ }
927
+
928
+ if (conversationHistory.length <= 1) {
929
+ showToast('Chat is already empty');
930
+ return;
931
+ }
932
+
933
+ // Confirm before clearing
934
+ if (confirm('Are you sure you want to clear the chat history?')) {
935
+ chatMessages.innerHTML = '';
936
+ conversationHistory = [conversationHistory[0]]; // Keep system message
937
+ currentStreamingMessage = null;
938
+ isStreamingInProgress = false;
939
+ showToast('Chat cleared');
940
+ }
941
  }
942
+
943
+ // Handle initial message submission
944
+ function sendInitialMessage() {
945
  const userMessage = initialChatInput.value.trim();
946
+ if (!userMessage || !selectedModel) {
947
+ if (!selectedModel) showToast('Please select a model first');
948
+ return;
949
+ }
950
 
951
+ // Hide initial input and show chat container
952
  initialInput.style.display = 'none';
953
  chatContainer.style.display = 'flex';
954
 
955
+ // Copy message to chat input and send
956
+ chatInput.value = userMessage;
 
 
 
 
 
 
 
957
  initialChatInput.value = '';
958
+ sendMessage();
 
 
 
 
 
 
 
 
 
 
 
 
 
959
  }
960
+
961
  async function sendMessage() {
962
  const userMessage = chatInput.value.trim();
963
  if (!userMessage || !selectedModel) return;
 
1153
 
1154
  sendButton.addEventListener('click', sendMessage);
1155
  chatInput.addEventListener('keypress', (event) => {
1156
+ if ((event.key === 'Enter' && !event.shiftKey) || (event.ctrlKey && event.key === 'Enter')) {
1157
+ event.preventDefault();
1158
+ sendMessage();
1159
+ }
1160
  });
1161
 
1162
  clearChatButton.addEventListener('click', clearChat);
1163
+
1164
+ exportChatButton.addEventListener('click', function() {
1165
+ const formatOptions = document.createElement('div');
1166
+ formatOptions.className = 'absolute mt-1 w-40 rounded-md shadow-lg bg-light-card dark:bg-dark-card ring-1 ring-black ring-opacity-5 py-1 z-20';
1167
+ formatOptions.style.right = '0';
1168
+
1169
+ const markdownOption = document.createElement('div');
1170
+ markdownOption.className = 'px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-light-input dark:hover:bg-dark-input cursor-pointer';
1171
+ markdownOption.textContent = 'Export as Markdown';
1172
+ markdownOption.addEventListener('click', () => {
1173
+ exportConversation('markdown');
1174
+ formatOptions.remove();
1175
+ });
1176
+
1177
+ const jsonOption = document.createElement('div');
1178
+ jsonOption.className = 'px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-light-input dark:hover:bg-dark-input cursor-pointer';
1179
+ jsonOption.textContent = 'Export as JSON';
1180
+ jsonOption.addEventListener('click', () => {
1181
+ exportConversation('json');
1182
+ formatOptions.remove();
1183
+ });
1184
+
1185
+ formatOptions.appendChild(markdownOption);
1186
+ formatOptions.appendChild(jsonOption);
1187
+
1188
+ // Remove existing format options if any
1189
+ const existingOptions = document.querySelector('.export-format-options');
1190
+ if (existingOptions) existingOptions.remove();
1191
+
1192
+ formatOptions.classList.add('export-format-options');
1193
+ exportChatButton.parentNode.appendChild(formatOptions);
1194
+
1195
+ // Close when clicking outside
1196
+ const closeFormatOptions = (e) => {
1197
+ if (!formatOptions.contains(e.target) && !exportChatButton.contains(e.target)) {
1198
+ formatOptions.remove();
1199
+ document.removeEventListener('click', closeFormatOptions);
1200
+ }
1201
+ };
1202
+
1203
+ setTimeout(() => {
1204
+ document.addEventListener('click', closeFormatOptions);
1205
+ }, 0);
1206
+ });
1207
 
1208
  // Model selector
1209
  modelSelectButton.addEventListener('click', function(e) {
 
1219
  // Theme toggle
1220
  themeToggle.addEventListener('click', toggleTheme);
1221
 
1222
+ // Message scroll event to detect if user has scrolled up
1223
+ chatMessages.addEventListener('scroll', function() {
1224
+ const isScrolledToBottom = chatMessages.scrollHeight - chatMessages.clientHeight <= chatMessages.scrollTop + 10;
1225
+ autoScrollEnabled = isScrolledToBottom;
1226
+ });
1227
+
1228
  // Initialize
 
1229
  fetchModels();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1230
  });
1231
  </script>
1232
  </body>