Spaces:
Running
Running
| <html lang="en" class="dark"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>LOKI.AI Playground</title> | |
| <meta name="description" content="π LOKI AI IS BACK AND FASTER THAN EVER! Test 200+ models including GPT-4o, O1-preview, Claude-sonnet, O3-mini. Ultra-fast, zero cost, total dominance!"> | |
| <!-- Favicon --> | |
| <link rel="icon" href="https://parthsadaria-lokiai.hf.space/favicon.ico" type="image/x-icon"> | |
| <!-- Open Graph / Facebook --> | |
| <meta property="og:title" content="Loki AI Playground - 200+ Free Models"> | |
| <meta property="og:description" content="Test the latest AI models with Loki AI. Explore GPT-4o, O1-preview, Claude-sonnet, O3-mini, and more."> | |
| <meta property="og:image" content="https://parthsadaria-lokiai.hf.space/favicon.ico"> | |
| <meta property="og:url" content="https://parthsadaria-lokiai.hf.space"> | |
| <meta property="og:type" content="website"> | |
| <!-- Twitter --> | |
| <meta name="twitter:card" content="summary_large_image"> | |
| <meta name="twitter:title" content="Loki AI Playground - 200+ Free Models"> | |
| <meta name="twitter:description" content="Ultra-fast responses, zero cost, and total dominance. Test Loki AI's models like GPT-4o and more!"> | |
| <meta name="twitter:image" content="https://parthsadaria-lokiai.hf.space/favicon.ico"> | |
| <!-- DM Sans font --> | |
| <link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&display=swap" rel="stylesheet"> | |
| <!-- JetBrains Mono for code blocks --> | |
| <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet"> | |
| <!-- Tailwind CSS --> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <!-- Anime.js for animations --> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js"></script> | |
| <!-- Marked.js for Markdown support --> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/4.3.0/marked.min.js"></script> | |
| <!-- Highlight.js for code syntax highlighting --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/atom-one-dark.min.css"> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script> | |
| <script> | |
| tailwind.config = { | |
| darkMode: 'class', | |
| theme: { | |
| extend: { | |
| fontFamily: { | |
| sans: ['"DM Sans"', 'sans-serif'], | |
| mono: ['"JetBrains Mono"', 'monospace'], | |
| }, | |
| colors: { | |
| primary: { | |
| DEFAULT: '#AAAAAA', // straight up black | |
| dark: '#111111', // a shade lighter | |
| light: '#444444', // pure white | |
| }, | |
| dark: { | |
| DEFAULT: '#000000', // deep black background | |
| lighter: '#1C1C1C', // just a tad lighter | |
| card: '#222222', // card background | |
| input: '#333333', // input background | |
| accent: '#555555', // chill gray for accents | |
| }, | |
| light: { | |
| DEFAULT: '#FFFFFF', // clean white background | |
| darker: '#F5F5F5', // subtle off-white contrast | |
| card: '#EFEFEF', // light card vibe | |
| input: '#F7F7F7', // smooth input bg | |
| accent: '#CCCCCC', // soft gray accents | |
| }, | |
| }, | |
| animation: { | |
| 'bounce-slow': 'bounce 1.5s infinite', | |
| 'typing': 'typing 1s infinite', | |
| 'fade-in': 'fadeIn 0.5s ease-in-out', | |
| }, | |
| keyframes: { | |
| typing: { | |
| '0%, 100%': { opacity: 0 }, | |
| '50%': { opacity: 1 }, | |
| }, | |
| fadeIn: { | |
| '0%': { opacity: 0 }, | |
| '100%': { opacity: 1 }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| } | |
| </script> | |
| <style> | |
| /* Custom scrollbar for dark theme */ | |
| .dark ::-webkit-scrollbar { | |
| width: 8px; | |
| height: 8px; | |
| } | |
| .dark ::-webkit-scrollbar-track { | |
| background: #1E293B; | |
| } | |
| .dark ::-webkit-scrollbar-thumb { | |
| background: #475569; | |
| border-radius: 4px; | |
| } | |
| .dark ::-webkit-scrollbar-thumb:hover { | |
| background: #64748B; | |
| } | |
| /* Custom scrollbar for light theme */ | |
| ::-webkit-scrollbar { | |
| width: 8px; | |
| height: 8px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: #E2E8F0; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: #94A3B8; | |
| border-radius: 4px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: #64748B; | |
| } | |
| /* Markdown styles - Dark */ | |
| .dark .markdown-content h1 { | |
| font-size: 1.8rem; | |
| font-weight: 700; | |
| margin-top: 1.5rem; | |
| margin-bottom: 1rem; | |
| border-bottom: 1px solid #334155; | |
| padding-bottom: 0.5rem; | |
| color: #F1F5F9; | |
| } | |
| .dark .markdown-content h2 { | |
| font-size: 1.5rem; | |
| font-weight: 600; | |
| margin-top: 1.2rem; | |
| margin-bottom: 0.8rem; | |
| color: #F1F5F9; | |
| } | |
| .dark .markdown-content h3 { | |
| font-size: 1.3rem; | |
| font-weight: 600; | |
| margin-top: 1rem; | |
| margin-bottom: 0.7rem; | |
| color: #F1F5F9; | |
| } | |
| .dark .markdown-content p { | |
| margin-bottom: 1rem; | |
| line-height: 1.6; | |
| color: #E2E8F0; | |
| } | |
| .dark .markdown-content ul, .dark .markdown-content ol { | |
| margin-top: 0.5rem; | |
| margin-bottom: 1rem; | |
| padding-left: 1.5rem; | |
| color: #E2E8F0; | |
| } | |
| .dark .markdown-content ul { | |
| list-style-type: disc; | |
| } | |
| .dark .markdown-content ol { | |
| list-style-type: decimal; | |
| } | |
| .dark .markdown-content li { | |
| margin-bottom: 0.5rem; | |
| } | |
| .dark .markdown-content blockquote { | |
| border-left: 4px solid #6EE7B7; | |
| padding-left: 1rem; | |
| margin: 1rem 0; | |
| font-style: italic; | |
| background-color: rgba(30, 41, 59, 0.5); | |
| padding: 0.5rem 1rem; | |
| border-radius: 0 4px 4px 0; | |
| color: #CBD5E1; | |
| } | |
| /* Markdown styles - Light */ | |
| .markdown-content h1 { | |
| font-size: 1.8rem; | |
| font-weight: 700; | |
| margin-top: 1.5rem; | |
| margin-bottom: 1rem; | |
| border-bottom: 1px solid #E2E8F0; | |
| padding-bottom: 0.5rem; | |
| color: #0F172A; | |
| } | |
| .markdown-content h2 { | |
| font-size: 1.5rem; | |
| font-weight: 600; | |
| margin-top: 1.2rem; | |
| margin-bottom: 0.8rem; | |
| color: #0F172A; | |
| } | |
| .markdown-content h3 { | |
| font-size: 1.3rem; | |
| font-weight: 600; | |
| margin-top: 1rem; | |
| margin-bottom: 0.7rem; | |
| color: #0F172A; | |
| } | |
| .markdown-content p { | |
| margin-bottom: 1rem; | |
| line-height: 1.6; | |
| color: #334155; | |
| } | |
| .markdown-content ul, .markdown-content ol { | |
| margin-top: 0.5rem; | |
| margin-bottom: 1rem; | |
| padding-left: 1.5rem; | |
| color: #334155; | |
| } | |
| .markdown-content ul { | |
| list-style-type: disc; | |
| } | |
| .markdown-content ol { | |
| list-style-type: decimal; | |
| } | |
| .markdown-content li { | |
| margin-bottom: 0.5rem; | |
| } | |
| .markdown-content blockquote { | |
| border-left: 4px solid #10B981; | |
| padding-left: 1rem; | |
| margin: 1rem 0; | |
| font-style: italic; | |
| background-color: rgba(241, 245, 249, 0.5); | |
| padding: 0.5rem 1rem; | |
| border-radius: 0 4px 4px 0; | |
| color: #475569; | |
| } | |
| .markdown-content img { | |
| max-width: 100%; | |
| margin: 1rem 0; | |
| border-radius: 8px; | |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); | |
| } | |
| .markdown-content table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| margin: 1rem 0; | |
| } | |
| .dark .markdown-content table th, | |
| .dark .markdown-content table td { | |
| padding: 0.5rem; | |
| border: 1px solid #334155; | |
| } | |
| .dark .markdown-content table th { | |
| background-color: #1E293B; | |
| font-weight: 600; | |
| } | |
| .dark .markdown-content table tr:nth-child(even) { | |
| background-color: rgba(30, 41, 59, 0.5); | |
| } | |
| .markdown-content table th, | |
| .markdown-content table td { | |
| padding: 0.5rem; | |
| border: 1px solid #E2E8F0; | |
| } | |
| .markdown-content table th { | |
| background-color: #F1F5F9; | |
| font-weight: 600; | |
| } | |
| .markdown-content table tr:nth-child(even) { | |
| background-color: rgba(241, 245, 249, 0.5); | |
| } | |
| .dark .markdown-content pre { | |
| margin: 1rem 0; | |
| padding: 1rem; | |
| border-radius: 8px; | |
| background-color: #1E293B ; | |
| overflow-x: auto; | |
| } | |
| .markdown-content pre { | |
| margin: 1rem 0; | |
| padding: 1rem; | |
| border-radius: 8px; | |
| background-color: #F1F5F9 ; | |
| overflow-x: auto; | |
| } | |
| .dark .markdown-content code { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 0.9rem; | |
| background-color: #334155; | |
| padding: 0.2rem 0.4rem; | |
| border-radius: 4px; | |
| } | |
| .markdown-content code { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 0.9rem; | |
| background-color: #E2E8F0; | |
| padding: 0.2rem 0.4rem; | |
| border-radius: 4px; | |
| } | |
| .dark .markdown-content pre code { | |
| background-color: transparent; | |
| padding: 0; | |
| border-radius: 0; | |
| } | |
| .markdown-content pre code { | |
| background-color: transparent; | |
| padding: 0; | |
| border-radius: 0; | |
| } | |
| .typing-indicator span { | |
| animation: typing 1s infinite; | |
| animation-delay: calc(var(--dot-index) * 0.3s); | |
| } | |
| /* Message bubbles with improved styling */ | |
| .message-bubble { | |
| border-radius: 1rem; | |
| max-width: 90%; | |
| transition: all 0.3s ease; | |
| box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); | |
| } | |
| .dark .message-bubble { | |
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); | |
| } | |
| .dark .user-message { | |
| background-color: #334155; | |
| margin-left: auto; | |
| border-top-right-radius: 0.25rem; | |
| } | |
| .user-message { | |
| background-color: #E2E8F0; | |
| margin-left: auto; | |
| border-top-right-radius: 0.25rem; | |
| color: #0F172A; | |
| } | |
| .dark .assistant-message { | |
| background-color: #0F172A; | |
| border-top-left-radius: 0.25rem; | |
| } | |
| .assistant-message { | |
| background-color: #FFFFFF; | |
| border-top-left-radius: 0.25rem; | |
| color: #334155; | |
| border: 1px solid #E2E8F0; | |
| } | |
| .copy-button { | |
| opacity: 0; | |
| transition: opacity 0.2s ease-in-out; | |
| } | |
| .message-bubble:hover .copy-button { | |
| opacity: 1; | |
| } | |
| /* Toast notification */ | |
| .toast { | |
| position: fixed; | |
| bottom: 20px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| padding: 0.5rem 1rem; | |
| border-radius: 0.5rem; | |
| background-color: #334155; | |
| color: white; | |
| z-index: 50; | |
| opacity: 0; | |
| transition: opacity 0.3s ease; | |
| } | |
| .toast.show { | |
| opacity: 1; | |
| } | |
| /* Dropdown transition */ | |
| .dropdown-transition { | |
| transition: opacity 0.2s ease, transform 0.2s ease; | |
| } | |
| /* Message timestamp */ | |
| .message-timestamp { | |
| font-size: 0.7rem; | |
| color: #64748B; | |
| margin-top: 0.25rem; | |
| } | |
| .message-transition-enter { | |
| opacity: 0; | |
| transform: translateY(10px); | |
| } | |
| .message-transition-enter-active { | |
| opacity: 1; | |
| transform: translateY(0px); | |
| transition: opacity 300ms, transform 300ms; | |
| } | |
| /* Mobile responsiveness */ | |
| @media (max-width: 640px) { | |
| .message-bubble { | |
| max-width: 85%; | |
| } | |
| .markdown-content pre { | |
| max-width: 100%; | |
| overflow-x: auto; | |
| } | |
| } | |
| /* Focus states for accessibility */ | |
| button:focus, input:focus { | |
| outline: 2px solid #10B981; | |
| outline-offset: 2px; | |
| } | |
| /* Animation for message appearance */ | |
| @keyframes messageAppear { | |
| from { | |
| opacity: 0; | |
| transform: translateY(10px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| .message-appear { | |
| animation: messageAppear 0.3s ease-out forwards; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-light-DEFAULT dark:bg-dark dark:text-gray-200 min-h-screen transition-colors duration-300 font-sans"> | |
| <!-- GitHub Link --> | |
| <a href="https://github.com/ParthSadaria" target="_blank" rel="noopener noreferrer" | |
| class="fixed top-4 right-4 text-gray-500 hover:text-primary-light transition-colors duration-300 z-10" | |
| aria-label="Visit Parth Sadaria's GitHub profile"> | |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" | |
| class="w-6 h-6" stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path> | |
| </svg> | |
| </a> | |
| <!-- Toast Notification --> | |
| <div id="toast" class="toast">Message copied to clipboard!</div> | |
| <!-- Chat Container --> | |
| <div class="max-w-4xl mx-auto py-8 px-4 sm:px-6 opacity-0 transform translate-y-4" id="chatWrapper"> | |
| <!-- Header with Logo --> | |
| <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"> | |
| <div class="flex items-center"> | |
| <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> | |
| <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> | |
| </div> | |
| <div class="flex space-x-2"> | |
| <!-- Model Selector --> | |
| <div class="relative" id="modelSelector"> | |
| <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"> | |
| <span id="modelSelectDisplay">Select Model</span> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /> | |
| </svg> | |
| </button> | |
| <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"> | |
| <div class="py-2 px-3 text-sm text-gray-600 dark:text-gray-400" id="modelLoader">Loading models...</div> | |
| </div> | |
| </div> | |
| <!-- Clear Chat Button --> | |
| <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"> | |
| <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"> | |
| <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" /> | |
| </svg> | |
| </button> | |
| <!-- Export Chat Button --> | |
| <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"> | |
| <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"> | |
| <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" /> | |
| </svg> | |
| </button> | |
| <!-- Theme Toggle Button --> | |
| <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"> | |
| <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"> | |
| <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" /> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Initial Input --> | |
| <div class="flex flex-col items-center justify-center space-y-6 py-12" id="initialInput"> | |
| <h2 class="text-2xl font-bold bg-gradient-to-r from-primary to-primary-light bg-clip-text text-transparent">Welcome to LOKI.AI</h2> | |
| <div class="w-full max-w-2xl relative"> | |
| <input type="text" id="initialChatInput" | |
| 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" | |
| placeholder="What can I help you with today?"> | |
| <button id="initialSendIcon" class="absolute right-4 top-4 text-gray-400 hover:text-primary transition-colors" aria-label="Send message"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" /> | |
| </svg> | |
| </button> | |
| </div> | |
| <!-- Model suggestions --> | |
| <div class="flex flex-wrap justify-center gap-2 max-w-2xl"> | |
| <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> | |
| <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> | |
| <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> | |
| <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> | |
| </div> | |
| </div> | |
| <!-- Chat Area --> | |
| <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"> | |
| <!-- Messages Area --> | |
| <div class="flex-1 overflow-y-auto p-4 space-y-5" id="chatMessages"></div> | |
| <!-- Input Area --> | |
| <div class="border-t border-light-darker dark:border-dark-input p-4 bg-light-card dark:bg-dark-lighter"> | |
| <div class="relative"> | |
| <textarea id="chatInput" | |
| 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]" | |
| placeholder="Type your message..." rows="1"></textarea> | |
| <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"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" /> | |
| </svg> | |
| <div id="sendLoader" class="hidden"> | |
| <div class="w-5 h-5 border-2 border-primary-light border-t-transparent rounded-full animate-spin"></div> | |
| </div> | |
| </button> | |
| </div> | |
| <div class="flex justify-between items-center mt-2"> | |
| <p class="text-gray-500 text-xs">Ctrl+Enter to send</p> | |
| <p class="text-gray-500 text-xs">Models may make mistakes. Check important information.</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Watermark --> | |
| <div class="fixed bottom-2 right-4 text-sm text-gray-500 font-mono font-bold"> | |
| Built By <span class="text-primary-light">π₯</span> Parth Sadaria | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // DOM Elements | |
| const chatWrapper = document.getElementById('chatWrapper'); | |
| const initialInput = document.getElementById('initialInput'); | |
| const chatContainer = document.getElementById('chatContainer'); | |
| const initialChatInput = document.getElementById('initialChatInput'); | |
| const initialSendIcon = document.getElementById('initialSendIcon'); | |
| const chatMessages = document.getElementById('chatMessages'); | |
| const chatInput = document.getElementById('chatInput'); | |
| const sendButton = document.getElementById('sendButton'); | |
| const sendIcon = document.getElementById('sendIcon'); | |
| const sendLoader = document.getElementById('sendLoader'); | |
| const clearChatButton = document.getElementById('clearChatButton'); | |
| const exportChatButton = document.getElementById('exportChatButton'); | |
| const modelSelectButton = document.getElementById('modelSelectButton'); | |
| const modelSelectDisplay = document.getElementById('modelSelectDisplay'); | |
| const modelOptions = document.getElementById('modelOptions'); | |
| const modelLoader = document.getElementById('modelLoader'); | |
| const themeToggle = document.getElementById('themeToggle'); | |
| const toast = document.getElementById('toast'); | |
| const quickPromptButtons = document.querySelectorAll('.quick-prompt'); | |
| // State variables | |
| let currentStreamingMessage = null; | |
| let isStreamingInProgress = false; | |
| let conversationHistory = [{ role: "system", content: "You are a helpful AI assistant. Assist the user effectively." }]; | |
| let selectedModel = ''; | |
| let modelsList = []; | |
| let isDarkMode = true; | |
| let autoScrollEnabled = true; | |
| let lastMessageTime = null; | |
| // Initialize with animation | |
| anime({ | |
| targets: '#chatWrapper', | |
| opacity: [0, 1], | |
| translateY: [20, 0], | |
| easing: 'easeOutExpo', | |
| duration: 800 | |
| }); | |
| // Auto-resize textarea | |
| function autoResizeTextarea(textarea) { | |
| textarea.style.height = 'auto'; | |
| const newHeight = Math.min(Math.max(textarea.scrollHeight, 50), 150); | |
| textarea.style.height = newHeight + 'px'; | |
| } | |
| chatInput.addEventListener('input', function() { | |
| autoResizeTextarea(this); | |
| }); | |
| // Quick prompt buttons | |
| quickPromptButtons.forEach(button => { | |
| button.addEventListener('click', function() { | |
| initialChatInput.value = this.textContent; | |
| initialChatInput.focus(); | |
| }); | |
| }); | |
| // Show toast notification | |
| function showToast(message, duration = 2000) { | |
| toast.textContent = message; | |
| toast.classList.add('show'); | |
| setTimeout(() => { | |
| toast.classList.remove('show'); | |
| }, duration); | |
| } | |
| // Copy text to clipboard | |
| function copyToClipboard(text) { | |
| navigator.clipboard.writeText(text).then(() => { | |
| showToast('Copied to clipboard!'); | |
| }).catch(err => { | |
| console.error('Failed to copy: ', err); | |
| showToast('Failed to copy text'); | |
| }); | |
| } | |
| // Export conversation as markdown or JSON | |
| function exportConversation(format = 'markdown') { | |
| if (conversationHistory.length <= 1) { | |
| showToast('No conversation to export'); | |
| return; | |
| } | |
| let content = ''; | |
| const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); | |
| let filename = ''; | |
| if (format === 'markdown') { | |
| content = '# LOKI.AI Conversation\n\n'; | |
| content += `*Exported on: ${new Date().toLocaleString()}*\n\n`; | |
| for (let i = 1; i < conversationHistory.length; i++) { | |
| const message = conversationHistory[i]; | |
| if (message.role === 'user') { | |
| content += `## User\n\n${message.content}\n\n`; | |
| } else if (message.role === 'assistant') { | |
| content += `## Assistant\n\n${message.content}\n\n`; | |
| } | |
| } | |
| filename = `loki-ai-conversation-${timestamp}.md`; | |
| } else if (format === 'json') { | |
| content = JSON.stringify(conversationHistory, null, 2); | |
| filename = `loki-ai-conversation-${timestamp}.json`; | |
| } | |
| const blob = new Blob([content], { type: 'text/plain' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = filename; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| showToast(`Exported as ${format}`); | |
| } | |
| // Fetch available models | |
| async function fetchModels() { | |
| try { | |
| const response = await fetch('https://parthsadaria-lokiai.hf.space/models', { | |
| method: 'GET', | |
| headers: { 'Authorization': 'playground' } | |
| }); | |
| if (response.ok) { | |
| const data = await response.json(); | |
| modelsList = data; | |
| populateModels(data); | |
| } else { | |
| const fallbackModels = [ | |
| { id: "gpt-4o", object: "model" }, | |
| { id: "gpt-3.5-turbo", object: "model" }, | |
| { id: "claude-3-5-sonnet", object: "model" }, | |
| { id: "claude-3-haiku", object: "model" }, | |
| { id: "llama-3.1-70b", object: "model" } | |
| ]; | |
| populateModels(fallbackModels); | |
| } | |
| } catch (error) { | |
| console.error('Error fetching models:', error); | |
| const fallbackModels = [ | |
| { id: "gpt-4o", object: "model" }, | |
| { id: "gpt-3.5-turbo", object: "model" }, | |
| { id: "claude-3-5-sonnet", object: "model" }, | |
| { id: "claude-3-haiku", object: "model" }, | |
| { id: "llama-3.1-70b", object: "model" } | |
| ]; | |
| populateModels(fallbackModels); | |
| } | |
| } | |
| function populateModels(models) { | |
| // Group models by provider | |
| const providers = {}; | |
| models.forEach(model => { | |
| const modelName = model.id; | |
| let provider = 'Other'; | |
| if (modelName.includes('gpt')) provider = 'OpenAI'; | |
| else if (modelName.includes('claude')) provider = 'Anthropic'; | |
| else if (modelName.includes('llama')) provider = 'Meta'; | |
| else if (modelName.includes('gemini')) provider = 'Google'; | |
| else if (modelName.includes('mistral')) provider = 'Mistral'; | |
| else if (modelName.includes('yi')) provider = 'Yi'; | |
| if (!providers[provider]) providers[provider] = []; | |
| providers[provider].push(model); | |
| }); | |
| // Clear existing options | |
| modelOptions.innerHTML = ''; | |
| // Add special option for web search | |
| const searchOption = document.createElement('div'); | |
| searchOption.className = 'px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer'; | |
| searchOption.textContent = 'SearchGPT (Web-access)'; | |
| searchOption.dataset.value = 'searchgpt'; | |
| modelOptions.appendChild(searchOption); | |
| // Add a separator | |
| const separator = document.createElement('div'); | |
| separator.className = 'px-4 py-1 text-xs font-semibold text-gray-500 bg-gray-50 dark:bg-gray-800 dark:text-gray-400'; | |
| separator.textContent = 'AI Models'; | |
| modelOptions.appendChild(separator); | |
| // Create and append provider groups and their models | |
| Object.keys(providers).sort().forEach(provider => { | |
| const providerGroup = document.createElement('div'); | |
| providerGroup.className = 'px-4 py-1 text-xs font-semibold text-gray-500 bg-gray-50 dark:bg-gray-800 dark:text-gray-400'; | |
| providerGroup.textContent = provider; | |
| modelOptions.appendChild(providerGroup); | |
| providers[provider].sort((a, b) => a.id.localeCompare(b.id)).forEach(model => { | |
| const option = document.createElement('div'); | |
| option.className = 'px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer'; | |
| option.textContent = model.id; | |
| option.dataset.value = model.id; | |
| modelOptions.appendChild(option); | |
| }); | |
| }); | |
| // Set default model if none selected | |
| if (!selectedModel && models.length > 0) { | |
| const preferredModels = ['gpt-4o', 'gpt-4', 'claude-3-5-sonnet', 'gpt-3.5-turbo']; | |
| for (const preferred of preferredModels) { | |
| const match = models.find(m => m.id.includes(preferred)); | |
| if (match) { | |
| selectedModel = match.id; | |
| modelSelectDisplay.textContent = selectedModel; | |
| break; | |
| } | |
| } | |
| if (!selectedModel) { | |
| selectedModel = models[0].id; | |
| modelSelectDisplay.textContent = selectedModel; | |
| } | |
| } | |
| // Add click event listeners | |
| document.querySelectorAll('#modelOptions > div').forEach(option => { | |
| if (option.dataset.value) { | |
| option.addEventListener('click', function() { | |
| selectedModel = this.dataset.value; | |
| modelSelectDisplay.textContent = this.textContent; | |
| modelOptions.classList.add('hidden'); | |
| }); | |
| } | |
| }); | |
| } | |
| // Select a model | |
| function selectModel(model) { | |
| selectedModel = model.id; | |
| modelSelectDisplay.textContent = model.displayName || model.id; | |
| modelOptions.classList.add('hidden'); | |
| } | |
| // Format timestamp | |
| function formatTimestamp(timestamp) { | |
| const date = new Date(timestamp); | |
| return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); | |
| } | |
| // Append message to chat | |
| // Append message to chat | |
| function appendMessage(text, sender, isStreaming = false) { | |
| const now = new Date(); | |
| const messageTime = formatTimestamp(now); | |
| // For streaming message logic | |
| if (isStreaming) { | |
| if (!currentStreamingMessage) { | |
| // First chunk of a streaming message - create the new message container | |
| isStreamingInProgress = true; | |
| lastMessageTime = now; | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = `message-appear flex flex-col ${sender === 'user' ? 'items-end' : 'items-start'} gap-1`; | |
| const messageBubble = document.createElement('div'); | |
| messageBubble.className = `message-bubble px-3 py-2 rounded-2xl tracking-tight [word-spacing:-0.02em] shadow-sm ${ | |
| sender === 'user' | |
| ? 'user-message bg-light-darker dark:bg-dark-input text-right' | |
| : 'assistant-message bg-light-card dark:bg-dark-lighter' | |
| }`; | |
| const contentDiv = document.createElement('div'); | |
| contentDiv.className = 'markdown-content break-words'; | |
| // Apply markdown to first chunk immediately | |
| contentDiv.innerHTML = marked.parse(text); | |
| // Add message to DOM immediately | |
| messageBubble.appendChild(contentDiv); | |
| messageDiv.appendChild(messageBubble); | |
| // Timestamp styling | |
| const timestampDiv = document.createElement('div'); | |
| timestampDiv.className = 'message-timestamp text-xs text-gray-400 mt-1'; | |
| timestampDiv.textContent = messageTime; | |
| messageDiv.appendChild(timestampDiv); | |
| chatMessages.appendChild(messageDiv); | |
| scrollToBottom(); // Force scroll to show new message | |
| // Store reference to content div for future updates | |
| currentStreamingMessage = contentDiv; | |
| // Update conversation history | |
| conversationHistory.push({ | |
| role: sender === 'user' ? 'user' : 'assistant', | |
| content: text | |
| }); | |
| } else { | |
| // Append new text to existing streaming message | |
| const lastIndex = conversationHistory.length - 1; | |
| conversationHistory[lastIndex].content += text; | |
| // Update the displayed content with new text | |
| currentStreamingMessage.innerHTML = marked.parse(conversationHistory[lastIndex].content); | |
| // Highlight any code blocks in the updated content | |
| currentStreamingMessage.querySelectorAll('pre code').forEach((block) => { | |
| hljs.highlightElement(block); | |
| }); | |
| // Auto-scroll if enabled | |
| if (autoScrollEnabled) scrollToBottom(); | |
| } | |
| } else if (isStreamingInProgress) { | |
| // End streaming mode | |
| isStreamingInProgress = false; | |
| // Add copy button now that streaming is complete | |
| if (sender === 'bot' && currentStreamingMessage) { | |
| const parentBubble = currentStreamingMessage.parentElement; | |
| const copyButton = document.createElement('button'); | |
| copyButton.className = 'copy-button text-xs text-gray-500 hover:text-primary dark:hover:text-primary-light mt-2 flex items-center float-right'; | |
| copyButton.innerHTML = ` | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <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" /> | |
| </svg> | |
| Copy`; | |
| copyButton.addEventListener('click', () => copyToClipboard(currentStreamingMessage.textContent)); | |
| parentBubble.appendChild(copyButton); | |
| } | |
| currentStreamingMessage = null; | |
| } else { | |
| // Regular (non-streaming) message | |
| lastMessageTime = now; | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = `message-appear flex flex-col ${sender === 'user' ? 'items-end' : 'items-start'} gap-1`; | |
| const messageBubble = document.createElement('div'); | |
| messageBubble.className = `message-bubble px-3 py-2 rounded-2xl tracking-tight [word-spacing:-0.02em] shadow-sm ${ | |
| sender === 'user' | |
| ? 'user-message bg-light-darker dark:bg-dark-input text-right' | |
| : 'assistant-message bg-light-card dark:bg-dark-lighter' | |
| }`; | |
| const contentDiv = document.createElement('div'); | |
| contentDiv.className = 'markdown-content break-words'; | |
| contentDiv.innerHTML = marked.parse(text); | |
| contentDiv.querySelectorAll('pre code').forEach((block) => { | |
| hljs.highlightElement(block); | |
| }); | |
| if (sender === 'bot') { | |
| const copyButton = document.createElement('button'); | |
| copyButton.className = 'copy-button text-xs text-gray-500 hover:text-primary dark:hover:text-primary-light mt-2 flex items-center float-right'; | |
| copyButton.innerHTML = ` | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <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" /> | |
| </svg> | |
| Copy`; | |
| copyButton.addEventListener('click', () => copyToClipboard(contentDiv.textContent)); | |
| messageBubble.appendChild(copyButton); | |
| } | |
| messageBubble.appendChild(contentDiv); | |
| messageDiv.appendChild(messageBubble); | |
| const timestampDiv = document.createElement('div'); | |
| timestampDiv.className = 'message-timestamp text-xs text-gray-400 mt-1'; | |
| timestampDiv.textContent = messageTime; | |
| messageDiv.appendChild(timestampDiv); | |
| chatMessages.appendChild(messageDiv); | |
| conversationHistory.push({ | |
| role: sender === 'user' ? 'user' : 'assistant', | |
| content: text | |
| }); | |
| if (autoScrollEnabled) scrollToBottom(); | |
| } | |
| } | |
| // Scroll chat to bottom | |
| function scrollToBottom() { | |
| chatMessages.scrollTop = chatMessages.scrollHeight; | |
| } | |
| // Clear chat history | |
| function clearChat() { | |
| if (isStreamingInProgress) { | |
| showToast('Cannot clear chat while message is streaming'); | |
| return; | |
| } | |
| if (conversationHistory.length <= 1) { | |
| showToast('Chat is already empty'); | |
| return; | |
| } | |
| // Confirm before clearing | |
| if (confirm('Are you sure you want to clear the chat history?')) { | |
| chatMessages.innerHTML = ''; | |
| conversationHistory = [conversationHistory[0]]; // Keep system message | |
| currentStreamingMessage = null; | |
| isStreamingInProgress = false; | |
| showToast('Chat cleared'); | |
| } | |
| } | |
| // Handle initial message submission | |
| function sendInitialMessage() { | |
| const userMessage = initialChatInput.value.trim(); | |
| if (!userMessage || !selectedModel) { | |
| if (!selectedModel) showToast('Please select a model first'); | |
| return; | |
| } | |
| // Hide initial input and show chat container | |
| initialInput.style.display = 'none'; | |
| chatContainer.style.display = 'flex'; | |
| // Copy message to chat input and send | |
| chatInput.value = userMessage; | |
| initialChatInput.value = ''; | |
| sendMessage(); | |
| } | |
| async function sendMessage() { | |
| const userMessage = chatInput.value.trim(); | |
| if (!userMessage || !selectedModel) return; | |
| appendMessage(userMessage, 'user'); | |
| chatInput.value = ''; | |
| // Show loader, hide send icon | |
| sendLoader.style.display = 'block'; | |
| sendIcon.style.display = 'none'; | |
| try { | |
| await callApi(userMessage, selectedModel); | |
| } catch (error) { | |
| appendMessage("Oops! Something went wrong. Please try again.", 'bot'); | |
| } finally { | |
| // Hide loader, show send icon | |
| sendLoader.style.display = 'none'; | |
| sendIcon.style.display = 'block'; | |
| } | |
| } | |
| async function callApi(userMessage, model) { | |
| let typingIndicator = null; | |
| try { | |
| // Add typing indicator | |
| typingIndicator = document.createElement('div'); | |
| typingIndicator.className = 'p-3 rounded-lg bg-gray-100 dark:bg-gray-800 mr-8 flex space-x-1'; | |
| typingIndicator.innerHTML = ` | |
| <div class="w-2 h-2 bg-gray-500 rounded-full animate-typing"></div> | |
| <div class="w-2 h-2 bg-gray-500 rounded-full animate-typing" style="animation-delay: 0.2s"></div> | |
| <div class="w-2 h-2 bg-gray-500 rounded-full animate-typing" style="animation-delay: 0.4s"></div> | |
| `; | |
| chatMessages.appendChild(typingIndicator); | |
| scrollToBottom(); | |
| if (model === "searchgpt") { | |
| 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`; | |
| const response = await fetch(url, { | |
| headers: { | |
| 'Authorization': 'playground' | |
| } | |
| }); | |
| if (response.ok) { | |
| // Remove typing indicator | |
| if (typingIndicator) { | |
| typingIndicator.remove(); | |
| typingIndicator = null; | |
| } | |
| const reader = response.body.getReader(); | |
| const decoder = new TextDecoder("utf-8"); | |
| let done = false; | |
| let isFirstChunk = true; | |
| while (!done) { | |
| const { value, done: streamDone } = await reader.read(); | |
| done = streamDone; | |
| if (value) { | |
| const chunk = decoder.decode(value); | |
| const cleanedChunk = chunk.trim(); | |
| if (cleanedChunk.startsWith('data:')) { | |
| const jsonChunks = cleanedChunk.split("data:").filter(Boolean); | |
| for (const jsonString of jsonChunks) { | |
| try { | |
| const cleanJson = jsonString.trim(); | |
| if (cleanJson === '[DONE]') continue; | |
| const jsonData = JSON.parse(cleanJson); | |
| const content = jsonData.choices?.[0]?.message?.content || | |
| jsonData.choices?.[0]?.delta?.content || ""; | |
| if (content) { | |
| if (isFirstChunk) { | |
| appendMessage(content, 'bot', true); | |
| isFirstChunk = false; | |
| } else { | |
| appendMessage(content, 'bot', true); | |
| } | |
| } | |
| } catch (err) { | |
| console.warn("Parsing error:", err); | |
| } | |
| } | |
| } else if (cleanedChunk) { | |
| if (isFirstChunk) { | |
| appendMessage(cleanedChunk, 'bot', true); | |
| isFirstChunk = false; | |
| } else { | |
| appendMessage(cleanedChunk, 'bot', true); | |
| } | |
| } | |
| } | |
| } | |
| } else { | |
| throw new Error(`API responded with status ${response.status}`); | |
| } | |
| } else { | |
| const url = "https://parthsadaria-lokiai.hf.space/chat/completions"; | |
| const payload = { | |
| model: model, | |
| messages: [...conversationHistory], | |
| stream: true | |
| }; | |
| const response = await fetch(url, { | |
| method: "POST", | |
| headers: { | |
| "Content-Type": "application/json", | |
| "Authorization": "playground" | |
| }, | |
| body: JSON.stringify(payload) | |
| }); | |
| if (response.ok) { | |
| // Remove typing indicator | |
| if (typingIndicator) { | |
| typingIndicator.remove(); | |
| typingIndicator = null; | |
| } | |
| const reader = response.body.getReader(); | |
| const decoder = new TextDecoder("utf-8"); | |
| let done = false; | |
| let buffer = ""; | |
| let isFirstChunk = true; | |
| while (!done) { | |
| const { value, done: streamDone } = await reader.read(); | |
| done = streamDone; | |
| if (value) { | |
| const chunk = decoder.decode(value); | |
| buffer += chunk; | |
| const lines = buffer.split('\n'); | |
| buffer = lines.pop() || ""; | |
| for (const line of lines) { | |
| if (line.trim() === '') continue; | |
| const cleanedLine = line.replace(/^data:\s*/, '').trim(); | |
| if (cleanedLine === '[DONE]') continue; | |
| try { | |
| const jsonData = JSON.parse(cleanedLine); | |
| const content = jsonData.choices?.[0]?.delta?.content || ""; | |
| if (content) { | |
| if (isFirstChunk) { | |
| appendMessage(content, 'bot', true); | |
| isFirstChunk = false; | |
| } else { | |
| appendMessage(content, 'bot', true); | |
| } | |
| } | |
| } catch (err) { | |
| console.warn("Parsing error:", err); | |
| } | |
| } | |
| } | |
| } | |
| } else { | |
| throw new Error(`API responded with status ${response.status}`); | |
| } | |
| } | |
| // End streaming after successful completion | |
| appendMessage("", 'bot', false); | |
| } catch (error) { | |
| console.error("API call error:", error); | |
| if (typingIndicator) { | |
| typingIndicator.remove(); | |
| } | |
| throw error; | |
| } | |
| } | |
| // Toggle theme | |
| function toggleTheme() { | |
| isDarkMode = !isDarkMode; | |
| document.documentElement.classList.toggle('dark'); | |
| // Update icon based on current theme | |
| if (isDarkMode) { | |
| themeToggle.innerHTML = ` | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <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" /> | |
| </svg>`; | |
| } else { | |
| themeToggle.innerHTML = ` | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" /> | |
| </svg>`; | |
| } | |
| } | |
| // Event Listeners | |
| initialSendIcon.addEventListener('click', sendInitialMessage); | |
| initialChatInput.addEventListener('keypress', (event) => { | |
| if (event.key === 'Enter') sendInitialMessage(); | |
| }); | |
| sendButton.addEventListener('click', sendMessage); | |
| chatInput.addEventListener('keypress', (event) => { | |
| if ((event.key === 'Enter' && !event.shiftKey) || (event.ctrlKey && event.key === 'Enter')) { | |
| event.preventDefault(); | |
| sendMessage(); | |
| } | |
| }); | |
| clearChatButton.addEventListener('click', clearChat); | |
| exportChatButton.addEventListener('click', function() { | |
| const formatOptions = document.createElement('div'); | |
| 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'; | |
| formatOptions.style.right = '0'; | |
| const markdownOption = document.createElement('div'); | |
| 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'; | |
| markdownOption.textContent = 'Export as Markdown'; | |
| markdownOption.addEventListener('click', () => { | |
| exportConversation('markdown'); | |
| formatOptions.remove(); | |
| }); | |
| const jsonOption = document.createElement('div'); | |
| 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'; | |
| jsonOption.textContent = 'Export as JSON'; | |
| jsonOption.addEventListener('click', () => { | |
| exportConversation('json'); | |
| formatOptions.remove(); | |
| }); | |
| formatOptions.appendChild(markdownOption); | |
| formatOptions.appendChild(jsonOption); | |
| // Remove existing format options if any | |
| const existingOptions = document.querySelector('.export-format-options'); | |
| if (existingOptions) existingOptions.remove(); | |
| formatOptions.classList.add('export-format-options'); | |
| exportChatButton.parentNode.appendChild(formatOptions); | |
| // Close when clicking outside | |
| const closeFormatOptions = (e) => { | |
| if (!formatOptions.contains(e.target) && !exportChatButton.contains(e.target)) { | |
| formatOptions.remove(); | |
| document.removeEventListener('click', closeFormatOptions); | |
| } | |
| }; | |
| setTimeout(() => { | |
| document.addEventListener('click', closeFormatOptions); | |
| }, 0); | |
| }); | |
| // Model selector | |
| modelSelectButton.addEventListener('click', function(e) { | |
| e.stopPropagation(); | |
| modelOptions.classList.toggle('hidden'); | |
| }); | |
| // Close dropdown when clicking outside | |
| document.addEventListener('click', function() { | |
| modelOptions.classList.add('hidden'); | |
| }); | |
| // Theme toggle | |
| themeToggle.addEventListener('click', toggleTheme); | |
| // Message scroll event to detect if user has scrolled up | |
| chatMessages.addEventListener('scroll', function() { | |
| const isScrolledToBottom = chatMessages.scrollHeight - chatMessages.clientHeight <= chatMessages.scrollTop + 10; | |
| autoScrollEnabled = isScrolledToBottom; | |
| }); | |
| // Initialize | |
| fetchModels(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |