ParthSadaria commited on
Commit
258641e
·
verified ·
1 Parent(s): 7030cff

Update playground.html

Browse files
Files changed (1) hide show
  1. playground.html +667 -374
playground.html CHANGED
@@ -6,16 +6,10 @@
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
10
- href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@300;400;500;600&display=swap"
11
- rel="stylesheet">
12
- <link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/css/select2.min.css" rel="stylesheet" />
13
- <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
14
- <script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/js/select2.min.js"></script>
15
  <script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js"></script>
 
16
  <style>
17
- @import url('https://fonts.googleapis.com/css2?family=Encode+Sans:[email protected]&family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Protest+Revolution&display=swap');
18
-
19
  :root {
20
  --bg-dark: #0a0a0f;
21
  --bg-darker: #040409;
@@ -60,7 +54,7 @@
60
  display: flex;
61
  justify-content: center;
62
  align-items: center;
63
- height: 100vh;
64
  overflow: hidden;
65
  perspective: 2000px;
66
  }
@@ -139,7 +133,6 @@
139
  width: 100%;
140
  padding: 12px 16px;
141
  padding-right: 50px;
142
- /* Space for send icon */
143
  background-color: rgba(30, 30, 50, 0.8);
144
  border: 1px solid var(--border-color);
145
  border-radius: 12px;
@@ -285,7 +278,7 @@
285
  font-size: 14px;
286
  cursor: pointer;
287
  transition: all 0.3s ease;
288
- max-width: 150px;
289
  overflow-y: auto;
290
  -webkit-appearance: none;
291
  -moz-appearance: none;
@@ -341,176 +334,219 @@
341
  opacity: 0.8;
342
  }
343
 
344
- @media screen and (max-width: 768px) {
345
- .chat-header {
346
- padding: 8px 12px;
347
- height: auto;
348
- min-height: 50px;
349
- }
350
-
351
- .header-actions {
352
- gap: 8px;
353
- }
354
-
355
- .model-select {
356
- font-size: 12px;
357
- padding: 6px 24px 6px 8px;
358
- max-width: 120px;
359
- }
360
-
361
- .clear-chat {
362
- width: 32px;
363
- height: 32px;
364
- padding: 6px;
365
- }
366
-
367
- .clear-chat svg {
368
- width: 16px;
369
- height: 16px;
370
- }
371
-
372
- .chat-input {
373
- padding: 8px;
374
- height: auto;
375
- min-height: 60px;
376
- }
377
 
378
- .chat-input input {
379
- padding: 10px 12px;
380
- font-size: 14px;
381
- height: 40px;
382
  }
383
  }
384
 
385
- @media screen and (max-width: 480px) {
386
- .chat-header h2 {
387
- font-size: 14px;
388
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
389
 
390
- .message {
391
- font-size: 12px;
392
- }
 
393
 
394
- .header-actions {
395
- flex-direction: row;
396
- gap: 6px;
397
- }
398
 
399
- .model-select {
400
- max-width: 100px;
401
- font-size: 11px;
402
- }
 
403
 
404
- .clear-chat {
405
- width: 28px;
406
- height: 28px;
407
- padding: 5px;
408
- }
 
 
 
 
 
 
 
 
 
 
 
409
 
410
- .clear-chat svg {
411
- width: 14px;
412
- height: 14px;
413
- }
 
 
 
 
 
 
 
 
414
 
415
- .chat-input input {
416
- font-size: 13px;
417
- height: 36px;
418
- }
 
 
 
 
 
 
 
 
 
 
 
 
419
 
420
- .initial-input input {
421
- font-size: 15px;
422
- }
 
 
423
 
424
- .initial-input h2 {
425
- font-size: 20px;
426
- }
427
  }
428
 
429
- /* Additional responsive adjustments to existing CSS */
430
- * {
431
- -webkit-tap-highlight-color: transparent;
 
 
 
 
 
432
  }
433
 
434
- body {
435
- touch-action: manipulation;
 
 
 
 
 
 
 
 
 
 
 
436
  }
437
 
438
- input,
439
- button {
440
- -webkit-appearance: none;
441
- -moz-appearance: none;
442
- appearance: none;
443
  }
444
 
445
- @media (hover: hover) {
446
- .send-icon:hover {
447
- background-color: var(--accent-color);
448
- transform: translateY(-50%) scale(1.05);
449
- border-radius: 15px;
450
  }
451
- }
452
 
453
- @media (pointer: coarse) {
454
- .send-icon {
455
- width: 45px;
456
- height: 45px;
457
  }
458
 
459
- .chat-input input {
460
- font-size: 16px;
461
- /* Larger font for touch devices */
462
  }
463
  }
464
 
465
- .model-select::-webkit-scrollbar {
466
- width: 4px;
 
 
 
 
 
 
467
  }
468
 
469
- .model-select::-webkit-scrollbar-track {
470
- background: var(--bg-dark);
 
471
  }
472
 
473
- .model-select::-webkit-scrollbar-thumb {
474
- background-color: var(--primary-blue);
475
- border-radius: 4px;
476
  }
477
 
478
- .chat-input input:focus,
479
- .initial-input input:focus,
480
- .model-select:focus,
481
- .clear-chat:focus,
482
- .send-icon:focus {
483
- outline: 2px solid var(--primary-blue);
484
- outline-offset: 2px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
485
  }
486
- .github-link {
487
- display: flex;
488
- align-items: center;
489
- justify-content: center;
490
- width: 36px;
491
- height: 36px;
492
- z-index: 999;
493
- border-radius: 8px;
494
- background-color: transparent;
495
- border: 1px solid var(--border-color);
496
- transition: all 0.3s ease;
497
- color: var(--text-light);
498
- position: absolute; /* Allows placement in the corner */
499
- bottom: 0; /* Aligns to the top */
500
- left: 0; /* Aligns to the left */
501
- margin: 10px; /* Adds spacing from the edges */
502
- }
503
-
504
- .github-link:hover {
505
- background-color: var(--hover-color);
506
- transform: translateY(-2px);
507
- }
508
-
509
- .github-link svg {
510
- width: 22px;
511
- height: 22px;
512
- }
513
-
514
  </style>
515
  </head>
516
 
@@ -524,25 +560,13 @@
524
  <div class="chat-header">
525
  <h2 style="font-family: 'JetBrains Mono', monospace;">LOKI.AI Playground</h2>
526
  <div class="header-actions">
527
- <select id="modelSelect" class="model-select">
528
- <option value="gpt-4o-mini">GPT-4o Mini</option>
529
- <option value="o1-preview">O1-preview</option>
530
- <option value="gpt-4o">GPT-4o</option>
531
- <option value="gpt-3.5-turbo">GPT-3.5 Turbo</option>
532
- <option value="searchgpt">SearchGPT(Web-access)</option>
533
- <option value="claude-sonnet-3.5">Claude 3.5 Sonnet</option>
534
- <option value="claude-3-5-sonnet">Claude 3.5 Sonnet (2)</option>
535
- <option value="claude-3-haiku">Claude 3 Haiku</option>
536
- <option value="llama-3.1-8b">Llama 3.1 8B</option>
537
- <option value="llama-3.1-70b">Llama 3.1 70B</option>
538
- <option value="llama-3.1-405b">Llama 3.1 405b</option>
539
- <option value="nvidia/Llama-3.1-Nemotron-70B-Instruct">LLama 3.1 Nemotron 70B</option>
540
- <option value="gemini-1.5-flash">Gemini 1.5 Flash</option>
541
- <option value="gemini-pro">Gemini Pro</option>
542
- <option value="mistral">Mistral(Uncensored)</option>
543
- <option value="mistral-large">Mistral-large(Uncensored)</option>
544
- <option value="mixtral-8x7b">Mixtral 8x7b</option>
545
- </select>
546
  <button id="clearChatButton" class="clear-chat">
547
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
548
  stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@@ -552,6 +576,11 @@
552
  <line x1="14" y1="11" x2="14" y2="17"></line>
553
  </svg>
554
  </button>
 
 
 
 
 
555
  </div>
556
  </div>
557
 
@@ -572,244 +601,508 @@
572
  <div class="chat-container" id="chatContainer">
573
  <div class="chat-messages" id="chatMessages"></div>
574
  <div class="chat-input">
575
- <input type="text" id="chatInput" placeholder="Type your message...">
576
- <button id="sendButton"
577
- style="padding: 10px 10px; background-color: black;color: #e0e0e8; font-family: 'JetBrains Mono'; border: solid #1a1a2e 2px; border-radius: 15px;">Send</button>
 
 
 
 
 
 
 
 
578
  </div>
579
- <p style="display: block; text-align: center; color: grey; font-size: 9px; z-index: 99; padding-bottom:5px;">
580
- Models can make mistakes. Check important info.
581
- </p>
582
  </div>
583
  </div>
584
  <div class="watermark">
585
  Made with ❤️ by Parth Sadaria
586
  </div>
587
  <script>
588
- const chatWrapper = document.querySelector('.chat-wrapper');
589
- const initialInput = document.querySelector('.initial-input');
590
- const chatContainer = document.getElementById('chatContainer');
591
- const initialChatInput = document.getElementById('initialChatInput');
592
- const initialSendIcon = document.getElementById('initialSendIcon');
593
- const chatMessages = document.getElementById('chatMessages');
594
- const chatInput = document.getElementById('chatInput');
595
- const sendButton = document.getElementById('sendButton');
596
- const modelSelect = document.getElementById('modelSelect');
597
- const clearChatButton = document.getElementById('clearChatButton');
598
- let currentStreamingMessage = null;
599
- let isStreamingInProgress = false;
600
- let conversationHistory = [{ role: "system", content: "You are a AI assistant. Assist the user effectively." }]; //doesnt work idk
601
- function scrollToBottom() {
602
  const chatMessages = document.getElementById('chatMessages');
603
- chatMessages.scrollTop = chatMessages.scrollHeight;
604
- }
605
- function appendMessage(content, type = 'bot', isStreaming = false) {
606
- if (type === 'bot' && isStreaming) {
607
- if (!currentStreamingMessage) {
608
- currentStreamingMessage = document.createElement('div');
609
- currentStreamingMessage.className = `message ${type}`;
610
- chatMessages.appendChild(currentStreamingMessage);
611
-
612
- // Add to conversation history only at the start of streaming
613
- if (!isStreamingInProgress) {
614
- conversationHistory.push({
615
- role: 'assistant',
616
- content: ''
617
- });
618
- isStreamingInProgress = true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
619
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
620
  }
 
621
 
622
- const formattedContent = content
623
- .replace(/(\d+)\.\s*/g, '<br>$1. ')
624
- .replace(/\n/g, '<br>')
625
- .replace(/\*\*(.*?)\*\*/g, (match, p1) => '<strong>' + p1 + '</strong>');
626
-
627
- currentStreamingMessage.innerHTML += formattedContent;
628
-
629
- // Update the last message in conversation history
630
- if (conversationHistory.length > 0) {
631
- conversationHistory[conversationHistory.length - 1].content += content;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
632
  }
 
 
 
 
 
 
 
 
 
 
633
 
 
634
  chatMessages.scrollTop = chatMessages.scrollHeight;
635
- } else {
636
- if (currentStreamingMessage) {
637
- isStreamingInProgress = false;
638
- currentStreamingMessage = null;
639
- }
640
 
641
- const messageBox = document.createElement('div');
642
- messageBox.className = `message ${type}`;
 
 
 
 
 
 
 
 
 
 
 
 
 
643
 
644
- const formattedContent = content
645
- .replace(/(\d+)\.\s*/g, '<br>$1. ')
646
- .replace(/\n/g, '<br>')
647
- .replace(/\*\*(.*?)\*\*/g, (match, p1) => '<strong>' + p1 + '</strong>');
 
 
648
 
649
- messageBox.innerHTML = formattedContent;
650
- chatMessages.appendChild(messageBox);
651
 
652
- // Add to conversation history only for new messages
653
- if (type === 'user') {
654
- conversationHistory.push({
655
- role: 'user',
656
- content: content
657
- });
658
- }
659
-
660
- chatMessages.scrollTop = chatMessages.scrollHeight;
661
- }
662
  }
663
- function clearChat() {
664
- chatMessages.innerHTML = '';
665
- conversationHistory = [];
666
- initialInput.style.display = 'flex';
667
- chatContainer.style.display = 'none';
668
- chatWrapper.classList.remove('active');
669
- }
670
- async function sendInitialMessage() {
671
- const userMessage = initialChatInput.value.trim();
672
- const selectedModel = modelSelect.value;
673
- if (!userMessage) return;
674
- initialInput.style.display = 'none';
675
- chatContainer.style.display = 'flex';
676
- chatWrapper.classList.add('active');
677
- appendMessage(userMessage, 'user');
678
- initialChatInput.value = '';
679
- scrollToBottom();
680
- try {
681
- await callApi(userMessage, selectedModel);
682
- } catch (error) {
683
- appendMessage("Oops! Something went wrong.", 'bot');
684
- console.error("API Error:", error);
685
- }
686
  }
687
- async function sendMessage() {
688
- const userMessage = chatInput.value.trim();
689
- const selectedModel = modelSelect.value;
690
- if (!userMessage) return;
691
- appendMessage(userMessage, 'user');
692
- chatInput.value = '';
693
- try {
694
- await callApi(userMessage, selectedModel);
695
- } catch (error) {
696
- appendMessage("Oops! Something went wrong.", 'bot');
697
- console.error("API Error:", error);
698
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
699
  }
700
- async function callApi(userMessage, model) {
701
- let fullResponse = "";
702
- if (model === "searchgpt") {
703
- 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`;
704
- try {
705
- const response = await fetch(url);
706
- if (response.ok) {
707
- const reader = response.body.getReader();
708
- const decoder = new TextDecoder("utf-8");
709
- let done = false;
710
- while (!done) {
711
- const { value, done: streamDone } = await reader.read();
712
- done = streamDone;
713
- if (value) {
714
- const chunk = decoder.decode(value);
715
- const cleanedChunk = chunk.trim().replace(/^data:\s*/, '');
716
- const jsonChunks = cleanedChunk.split("data:").filter(Boolean);
717
- jsonChunks.forEach(jsonString => {
718
- try {
719
- const jsonData = JSON.parse(jsonString);
720
- const content = jsonData.choices?.[0]?.message?.content || "";
721
- if (content) {
722
- // Comprehensive newline conversion
723
- const formattedContent = content
724
- .replace(/\r\n/g, '<br>') // Windows-style newlines
725
- .replace(/\n/g, '<br>') // Unix/Linux-style newlines
726
- .replace(/\r/g, '<br>'); // Old Mac-style newlines
727
- appendMessage(formattedContent, 'bot', true);
728
- }
729
- } catch (err) {
730
- console.warn("Parsing error:", err);
731
- }
732
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
733
  }
734
  }
735
- } else {
736
- throw new Error(`API responded with status ${response.status}`);
737
  }
738
- } catch (error) {
739
- console.error("API call error:", error);
740
- throw error;
741
  }
742
  } else {
743
- const url = "https://parthsadaria-lokiai.hf.space/chat/completions";
744
- const payload = {
745
- model: model,
746
- messages: [
747
- ...conversationHistory
748
- ],
749
- stream: true
750
- };
751
- const headers = {
752
- "Content-Type": "application/json"
753
- };
754
- try {
755
- const response = await fetch(url, {
756
- method: "POST",
757
- headers: headers,
758
- body: JSON.stringify(payload)
759
- });
760
- if (response.ok) {
761
- const reader = response.body.getReader();
762
- const decoder = new TextDecoder("utf-8");
763
- let done = false;
764
- while (!done) {
765
- const { value, done: streamDone } = await reader.read();
766
- done = streamDone;
767
- if (value) {
768
- const chunk = decoder.decode(value);
769
- const cleanedChunk = chunk.trim().replace(/^data:\s*/, '');
770
- const jsonChunks = cleanedChunk.split("data:").filter(Boolean);
771
- jsonChunks.forEach(jsonString => {
772
- try {
773
- const jsonData = JSON.parse(jsonString);
774
- const delta = jsonData.choices?.[0]?.delta || {};
775
- let content = delta.content || "";
776
-
777
- // Comprehensive newline conversion
778
- content = content
779
- .replace(/\r\n/g, '<br>') // Windows-style newlines
780
- .replace(/\n/g, '<br>') // Unix/Linux-style newlines
781
- .replace(/\r/g, '<br>'); // Old Mac-style newlines
782
- if (content) {
783
- appendMessage(content, 'bot', true);
784
- }
785
- } catch (err) {
786
- console.warn("Parsing error:", err);
787
- }
788
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
789
  }
790
  }
791
- } else {
792
- throw new Error(`API responded with status ${response.status}`);
793
  }
794
- } catch (error) {
795
- console.error("API call error:", error);
796
- throw error;
797
  }
 
 
798
  }
799
- return fullResponse.trim();
800
- }
801
- // Event Listeners
802
- initialSendIcon.addEventListener('click', sendInitialMessage);
803
- initialChatInput.addEventListener('keypress', (event) => {
804
- if (event.key === 'Enter') sendInitialMessage();
805
- });
806
- sendButton.addEventListener('click', sendMessage);
807
- chatInput.addEventListener('keypress', (event) => {
808
- if (event.key === 'Enter') sendMessage();
809
- });
810
- // Clear Chat Button Event Listener
811
- clearChatButton.addEventListener('click', clearChat);
812
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
813
  </body>
814
- <!-- hehe go to networks tab to find cool stuff :) -->
815
  </html>
 
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;
 
54
  display: flex;
55
  justify-content: center;
56
  align-items: center;
57
+ min-height: 100vh;
58
  overflow: hidden;
59
  perspective: 2000px;
60
  }
 
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;
 
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;
 
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
  </style>
551
  </head>
552
 
 
560
  <div class="chat-header">
561
  <h2 style="font-family: 'JetBrains Mono', monospace;">LOKI.AI Playground</h2>
562
  <div class="header-actions">
563
+ <div class="custom-select-wrapper">
564
+ <div class="custom-select" id="modelSelectDisplay">Select Model</div>
565
+ <div class="custom-select-options" id="modelOptions">
566
+ <!-- Models will be loaded here -->
567
+ <div class="loader" id="modelLoader" style="margin: 10px auto;"></div>
568
+ </div>
569
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
570
  <button id="clearChatButton" class="clear-chat">
571
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
572
  stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
 
576
  <line x1="14" y1="11" x2="14" y2="17"></line>
577
  </svg>
578
  </button>
579
+ <button id="themeToggle" class="theme-toggle">
580
+ <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">
581
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
582
+ </svg>
583
+ </button>
584
  </div>
585
  </div>
586
 
 
601
  <div class="chat-container" id="chatContainer">
602
  <div class="chat-messages" id="chatMessages"></div>
603
  <div class="chat-input">
604
+ <div class="chat-input-container">
605
+ <input type="text" id="chatInput" placeholder="Type your message...">
606
+ <div class="send-icon" id="sendButtonIcon">
607
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
608
+ stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
609
+ <line x1="22" y1="2" x2="11" y2="13"></line>
610
+ <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
611
+ </svg>
612
+ <div class="loader" id="sendLoader"></div>
613
+ </div>
614
+ </div>
615
  </div>
616
+ <p style="display: block; text-align: center; color: grey; font-size: 9px; z-index: 99; padding-bottom:5px;">
617
+ Models can make mistakes. Check important info.
618
+ </p>
619
  </div>
620
  </div>
621
  <div class="watermark">
622
  Made with ❤️ by Parth Sadaria
623
  </div>
624
  <script>
625
+ document.addEventListener('DOMContentLoaded', function() {
626
+ const chatWrapper = document.querySelector('.chat-wrapper');
627
+ const initialInput = document.querySelector('.initial-input');
628
+ const chatContainer = document.getElementById('chatContainer');
629
+ const initialChatInput = document.getElementById('initialChatInput');
630
+ const initialSendIcon = document.getElementById('initialSendIcon');
 
 
 
 
 
 
 
 
631
  const chatMessages = document.getElementById('chatMessages');
632
+ const chatInput = document.getElementById('chatInput');
633
+ const sendButtonIcon = document.getElementById('sendButtonIcon');
634
+ const sendLoader = document.getElementById('sendLoader');
635
+ const clearChatButton = document.getElementById('clearChatButton');
636
+ const modelSelectDisplay = document.getElementById('modelSelectDisplay');
637
+ const modelOptions = document.getElementById('modelOptions');
638
+ const modelLoader = document.getElementById('modelLoader');
639
+ const themeToggle = document.getElementById('themeToggle');
640
+
641
+ let currentStreamingMessage = null;
642
+ let isStreamingInProgress = false;
643
+ let conversationHistory = [{ role: "system", content: "You are a helpful AI assistant. Assist the user effectively." }];
644
+ let selectedModel = '';
645
+ let modelsList = [];
646
+ let isDarkMode = true;
647
+
648
+ // Fetch models from API
649
+ async function fetchModels() {
650
+ modelLoader.style.display = 'block';
651
+ try {
652
+ const response = await fetch('https://parthsadaria-lokiai.hf.space/models', {
653
+ method: 'GET',
654
+ headers: {
655
+ 'Authorization': 'playground'
656
+ }
657
+ });
658
+
659
+ if (response.ok) {
660
+ const data = await response.json();
661
+ modelsList = data;
662
+ populateModels(data);
663
+ } else {
664
+ console.error('Failed to fetch models:', response.status);
665
+ // Fallback to some default models
666
+ const fallbackModels = [
667
+ { id: "gpt-4o", object: "model" },
668
+ { id: "gpt-3.5-turbo", object: "model" },
669
+ { id: "claude-3-5-sonnet", object: "model" },
670
+ { id: "claude-3-haiku", object: "model" },
671
+ { id: "llama-3.1-70b", object: "model" }
672
+ ];
673
+ populateModels(fallbackModels);
674
  }
675
+ } catch (error) {
676
+ console.error('Error fetching models:', error);
677
+ // Use the same fallback
678
+ const fallbackModels = [
679
+ { id: "gpt-4o", object: "model" },
680
+ { id: "gpt-3.5-turbo", object: "model" },
681
+ { id: "claude-3-5-sonnet", object: "model" },
682
+ { id: "claude-3-haiku", object: "model" },
683
+ { id: "llama-3.1-70b", object: "model" }
684
+ ];
685
+ populateModels(fallbackModels);
686
+ } finally {
687
+ modelLoader.style.display = 'none';
688
  }
689
+ }
690
 
691
+ // Organize and display models
692
+ function populateModels(models) {
693
+ // Group models by provider
694
+ const providers = {};
695
+ models.forEach(model => {
696
+ const modelName = model.id;
697
+ let provider = 'Other';
698
+
699
+ if (modelName.includes('gpt')) provider = 'OpenAI';
700
+ else if (modelName.includes('claude')) provider = 'Anthropic';
701
+ else if (modelName.includes('llama')) provider = 'Meta';
702
+ else if (modelName.includes('gemini')) provider = 'Google';
703
+ else if (modelName.includes('mistral')) provider = 'Mistral';
704
+ else if (modelName.includes('yi')) provider = 'Yi';
705
+
706
+ if (!providers[provider]) providers[provider] = [];
707
+ providers[provider].push(model);
708
+ });
709
+
710
+ // Clear existing options
711
+ modelOptions.innerHTML = '';
712
+
713
+ // Add special option for web search
714
+ const webSearchOption = document.createElement('div');
715
+ webSearchOption.className = 'custom-option';
716
+ webSearchOption.textContent = 'SearchGPT (Web-access)';
717
+ webSearchOption.dataset.value = 'searchgpt';
718
+ modelOptions.appendChild(webSearchOption);
719
+
720
+ // Add a separator
721
+ const separator = document.createElement('div');
722
+ separator.className = 'model-group';
723
+ separator.textContent = 'AI Models';
724
+ modelOptions.appendChild(separator);
725
+
726
+ // Create and append provider groups and their models
727
+ Object.keys(providers).sort().forEach(provider => {
728
+ const providerGroup = document.createElement('div');
729
+ providerGroup.className = 'model-group';
730
+ providerGroup.textContent = provider;
731
+ modelOptions.appendChild(providerGroup);
732
+
733
+ providers[provider].sort((a, b) => a.id.localeCompare(b.id)).forEach(model => {
734
+ const option = document.createElement('div');
735
+ option.className = 'custom-option';
736
+ option.textContent = model.id;
737
+ option.dataset.value = model.id;
738
+ modelOptions.appendChild(option);
739
+ });
740
+ });
741
+
742
+ // Set default model if none selected yet
743
+ if (!selectedModel && models.length > 0) {
744
+ const preferredModels = ['gpt-4o', 'gpt-4', 'claude-3-5-sonnet', 'gpt-3.5-turbo'];
745
+
746
+ // Find the first preferred model that exists in our list
747
+ for (const preferred of preferredModels) {
748
+ const match = models.find(m => m.id.includes(preferred));
749
+ if (match) {
750
+ selectedModel = match.id;
751
+ modelSelectDisplay.textContent = selectedModel;
752
+ break;
753
+ }
754
+ }
755
+
756
+ // If no preferred model found, use the first one in the list
757
+ if (!selectedModel) {
758
+ selectedModel = models[0].id;
759
+ modelSelectDisplay.textContent = selectedModel;
760
+ }
761
  }
762
+
763
+ // Add event listeners to options
764
+ document.querySelectorAll('.custom-option').forEach(option => {
765
+ option.addEventListener('click', function() {
766
+ selectedModel = this.dataset.value;
767
+ modelSelectDisplay.textContent = this.textContent;
768
+ modelOptions.style.display = 'none';
769
+ });
770
+ });
771
+ }
772
 
773
+ function scrollToBottom() {
774
  chatMessages.scrollTop = chatMessages.scrollHeight;
775
+ }
 
 
 
 
776
 
777
+ function appendMessage(content, type = 'bot', isStreaming = false) {
778
+ if (type === 'bot' && isStreaming) {
779
+ if (!currentStreamingMessage) {
780
+ currentStreamingMessage = document.createElement('div');
781
+ currentStreamingMessage.className = `message ${type}`;
782
+ chatMessages.appendChild(currentStreamingMessage);
783
+
784
+ // Add to conversation history only at the start of streaming
785
+ if (!isStreamingInProgress) {
786
+ conversationHistory.push({
787
+ role: 'assistant',
788
+ content: ''
789
+ });
790
+ isStreamingInProgress = true;
791
+ }
792
 
793
+ // Apply markdown-like formatting
794
+ const formattedContent = content
795
+ .replace(/```([\s\S]*?)```/g, (match, code) => `<pre><code>${code}</code></pre>`)
796
+ .replace(/(\d+)\.\s*/g, '<br>$1. ')
797
+ .replace(/\n/g, '<br>')
798
+ .replace(/\*\*(.*?)\*\*/g, (match, p1) => '<strong>' + p1 + '</strong>');
799
 
800
+ currentStreamingMessage.innerHTML += formattedContent;
 
801
 
802
+ // Update the last message in conversation history
803
+ if (conversationHistory.length > 0) {
804
+ conversationHistory[conversationHistory.length - 1].content += content;
 
 
 
 
 
 
 
805
  }
806
+
807
+ scrollToBottom();
808
+ } else {
809
+ if (currentStreamingMessage) {
810
+ isStreamingInProgress = false;
811
+ currentStreamingMessage = null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
812
  }
813
+
814
+ const messageBox = document.createElement('div');
815
+ messageBox.className = `message ${type}`;
816
+
817
+ // Apply markdown-like formatting
818
+ const formattedContent = content
819
+ .replace(/```([\s\S]*?)```/g, (match, code) => `<pre><code>${code}</code></pre>`)
820
+ .replace(/(\d+)\.\s*/g, '<br>$1. ')
821
+ .replace(/\n/g, '<br>')
822
+ .replace(/\*\*(.*?)\*\*/g, (match, p1) => '<strong>' + p1 + '</strong>');
823
+
824
+ messageBox.innerHTML = formattedContent;
825
+ chatMessages.appendChild(messageBox);
826
+
827
+ // Add to conversation history only for new messages
828
+ if (type === 'user') {
829
+ conversationHistory.push({
830
+ role: 'user',
831
+ content: content
832
+ });
833
+ } else if (type === 'bot' && !isStreamingInProgress) {
834
+ conversationHistory.push({
835
+ role: 'assistant',
836
+ content: content
837
+ });
838
  }
839
+
840
+ scrollToBottom();
841
+ }
842
+ }
843
+
844
+ function clearChat() {
845
+ chatMessages.innerHTML = '';
846
+ conversationHistory = [{ role: "system", content: "You are a helpful AI assistant. Assist the user effectively." }];
847
+ initialInput.style.display = 'flex';
848
+ chatContainer.style.display = 'none';
849
+ }
850
+
851
+ async function sendInitialMessage() {
852
+ const userMessage = initialChatInput.value.trim();
853
+ if (!userMessage || !selectedModel) return;
854
+
855
+ initialInput.style.display = 'none';
856
+ chatContainer.style.display = 'flex';
857
+
858
+ appendMessage(userMessage, 'user');
859
+ initialChatInput.value = '';
860
+ scrollToBottom();
861
+
862
+ // Show the loader, hide the send icon
863
+ sendLoader.style.display = 'block';
864
+ sendButtonIcon.querySelector('svg').style.display = 'none';
865
+
866
+ try {
867
+ await callApi(userMessage, selectedModel);
868
+ } catch (error) {
869
+ appendMessage("Oops! Something went wrong. Please try again.", 'bot');
870
+ console.error("API Error:", error);
871
+ } finally {
872
+ // Hide loader, show send icon
873
+ sendLoader.style.display = 'none';
874
+ sendButtonIcon.querySelector('svg').style.display = 'block';
875
+ }
876
+ }
877
+
878
+ async function sendMessage() {
879
+ const userMessage = chatInput.value.trim();
880
+ if (!userMessage || !selectedModel) return;
881
+
882
+ appendMessage(userMessage, 'user');
883
+ chatInput.value = '';
884
+
885
+ // Show the loader, hide the send icon
886
+ sendLoader.style.display = 'block';
887
+ sendButtonIcon.querySelector('svg').style.display = 'none';
888
+
889
+ try {
890
+ await callApi(userMessage, selectedModel);
891
+ } catch (error) {
892
+ appendMessage("Oops! Something went wrong. Please try again.", 'bot');
893
+ console.error("API Error:", error);
894
+ } finally {
895
+ // Hide loader, show send icon
896
+ sendLoader.style.display = 'none';
897
+ sendButtonIcon.querySelector('svg').style.display = 'block';
898
+ }
899
+ }
900
+
901
+ async function callApi(userMessage, model) {
902
+ if (model === "searchgpt") {
903
+ 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`;
904
+ try {
905
+ const response = await fetch(url, {
906
+ headers: {
907
+ 'Authorization': 'playground'
908
+ }
909
+ });
910
+
911
+ if (response.ok) {
912
+ const reader = response.body.getReader();
913
+ const decoder = new TextDecoder("utf-8");
914
+ let done = false;
915
+
916
+ // Add typing indicator
917
+ const typingIndicator = document.createElement('div');
918
+ typingIndicator.className = 'message bot typing-indicator';
919
+ typingIndicator.innerHTML = '<span></span><span></span><span></span>';
920
+ chatMessages.appendChild(typingIndicator);
921
+ scrollToBottom();
922
+
923
+ while (!done) {
924
+ const { value, done: streamDone } = await reader.read();
925
+ done = streamDone;
926
+
927
+ if (value) {
928
+ // Remove typing indicator when data starts coming
929
+ if (typingIndicator) {
930
+ typingIndicator.remove();
931
+ }
932
+
933
+ const chunk = decoder.decode(value);
934
+ const cleanedChunk = chunk.trim().replace(/^data:\s*/, '');
935
+ const jsonChunks = cleanedChunk.split("data:").filter(Boolean);
936
+
937
+ for (const jsonString of jsonChunks) {
938
+ try {
939
+ const jsonData = JSON.parse(jsonString);
940
+ const content = jsonData.choices?.[0]?.message?.content || "";
941
+ if (content) {
942
+ appendMessage(content, 'bot', true);
943
+ }
944
+ } catch (err) {
945
+ console.warn("Parsing error:", err);
946
  }
947
  }
 
 
948
  }
 
 
 
949
  }
950
  } else {
951
+ throw new Error(`API responded with status ${response.status}`);
952
+ }
953
+ } catch (error) {
954
+ console.error("API call error:", error);
955
+ throw error;
956
+ }
957
+ } else {
958
+ const url = "https://parthsadaria-lokiai.hf.space/chat/completions";
959
+ const payload = {
960
+ model: model,
961
+ messages: [...conversationHistory],
962
+ stream: true
963
+ };
964
+
965
+ const headers = {
966
+ "Content-Type": "application/json",
967
+ "Authorization": "playground"
968
+ };
969
+
970
+ try {
971
+ const response = await fetch(url, {
972
+ method: "POST",
973
+ headers: headers,
974
+ body: JSON.stringify(payload)
975
+ });
976
+
977
+ if (response.ok) {
978
+ const reader = response.body.getReader();
979
+ const decoder = new TextDecoder("utf-8");
980
+ let done = false;
981
+
982
+ // Add typing indicator
983
+ const typingIndicator = document.createElement('div');
984
+ typingIndicator.className = 'message bot typing-indicator';
985
+ typingIndicator.innerHTML = '<span></span><span></span><span></span>';
986
+ chatMessages.appendChild(typingIndicator);
987
+ scrollToBottom();
988
+
989
+ while (!done) {
990
+ const { value, done: streamDone } = await reader.read();
991
+ done = streamDone;
992
+
993
+ if (value) {
994
+ // Remove typing indicator when data starts coming
995
+ if (typingIndicator) {
996
+ typingIndicator.remove();
997
+ }
998
+
999
+ const chunk = decoder.decode(value);
1000
+ const cleanedChunk = chunk.trim().replace(/^data:\s*/, '');
1001
+ const jsonChunks = cleanedChunk.split("data:").filter(Boolean);
1002
+
1003
+ for (const jsonString of jsonChunks) {
1004
+ try {
1005
+ const jsonData = JSON.parse(jsonString);
1006
+ const delta = jsonData.choices?.[0]?.delta || {};
1007
+ const content = delta.content || "";
1008
+
1009
+ if (content) {
1010
+ appendMessage(content, 'bot', true);
1011
+ }
1012
+ } catch (err) {
1013
+ console.warn("Parsing error:", err);
1014
  }
1015
  }
 
 
1016
  }
 
 
 
1017
  }
1018
+ } else {
1019
+ throw new Error(`API responded with status ${response.status}`);
1020
  }
1021
+ } catch (error) {
1022
+ console.error("API call error:", error);
1023
+ throw error;
1024
+ }
1025
+ }
1026
+ }
1027
+
1028
+ // Toggle theme
1029
+ function toggleTheme() {
1030
+ isDarkMode = !isDarkMode;
1031
+ document.body.classList.toggle('light-mode');
1032
+
1033
+ // Update icon based on current theme
1034
+ if (isDarkMode) {
1035
+ 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">
1036
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
1037
+ </svg>`;
1038
+ } else {
1039
+ 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">
1040
+ <circle cx="12" cy="12" r="5"></circle>
1041
+ <line x1="12" y1="1" x2="12" y2="3"></line>
1042
+ <line x1="12" y1="21" x2="12" y2="23"></line>
1043
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
1044
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
1045
+ <line x1="1" y1="12" x2="3" y2="12"></line>
1046
+ <line x1="21" y1="12" x2="23" y2="12"></line>
1047
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
1048
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
1049
+ </svg>`;
1050
+ }
1051
+ }
1052
+
1053
+ // Event Listeners
1054
+ initialSendIcon.addEventListener('click', sendInitialMessage);
1055
+ initialChatInput.addEventListener('keypress', (event) => {
1056
+ if (event.key === 'Enter') sendInitialMessage();
1057
+ });
1058
+
1059
+ sendButtonIcon.addEventListener('click', sendMessage);
1060
+ chatInput.addEventListener('keypress', (event) => {
1061
+ if (event.key === 'Enter') sendMessage();
1062
+ });
1063
+
1064
+ // Clear Chat Button Event Listener
1065
+ clearChatButton.addEventListener('click', clearChat);
1066
+
1067
+ // Model selector dropdown event listeners
1068
+ modelSelectDisplay.addEventListener('click', function() {
1069
+ if (modelOptions.style.display === 'block') {
1070
+ modelOptions.style.display = 'none';
1071
+ } else {
1072
+ modelOptions.style.display = 'block';
1073
+ }
1074
+ });
1075
+
1076
+ // Close dropdown when clicking outside
1077
+ document.addEventListener('click', function(event) {
1078
+ if (!event.target.closest('.custom-select-wrapper') && modelOptions.style.display === 'block') {
1079
+ modelOptions.style.display = 'none';
1080
+ }
1081
+ });
1082
+
1083
+ // Theme toggle event listener
1084
+ themeToggle.addEventListener('click', toggleTheme);
1085
+
1086
+ // Initialize the app
1087
+ fetchModels();
1088
+
1089
+ // Add an animation to the chat wrapper
1090
+ anime({
1091
+ targets: '.chat-wrapper',
1092
+ translateY: [50, 0],
1093
+ opacity: [0, 1],
1094
+ easing: 'easeOutExpo',
1095
+ duration: 1000
1096
+ });
1097
+
1098
+ // Detect escape key to close model selector
1099
+ document.addEventListener('keydown', function(event) {
1100
+ if (event.key === 'Escape' && modelOptions.style.display === 'block') {
1101
+ modelOptions.style.display = 'none';
1102
+ }
1103
+ });
1104
+
1105
+ });
1106
+ </script>
1107
  </body>
 
1108
  </html>