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/banner.jpg"> | |
<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/banner.jpg"> | |
<!-- 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; | |
} | |
@keyframes fullscreenIn { | |
from { transform: scale(0.95); opacity: 0; } | |
to { transform: scale(1); opacity: 1; } | |
} | |
.fullscreen-animate { | |
animation: fullscreenIn 0.3s ease-out; | |
} | |
/* Resize handle style */ | |
#resizeHandle { | |
width: 1.5rem; | |
height: 1.5rem; | |
background-color: #9CA3AF; /* gray-400 */ | |
cursor: se-resize; | |
position: absolute; | |
right: 0.5rem; | |
bottom: 0.5rem; | |
z-index: 50; | |
} | |
</style> | |
</head> | |
<body class="bg-light-DEFAULT dark:bg-dark dark:text-gray-200 min-h-screen transition-colors duration-300 font-sans"> | |
<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> | |
<div id="toast" class="toast">Message copied to clipboard!</div> | |
<div class="max-w-4xl mx-auto py-8 px-4 sm:px-6 opacity-0 transform translate-y-4 relative" id="chatWrapper"> | |
<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 hidden sm:inline-block">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 hidden sm:inline-block">Playground</span> | |
</div> | |
<div class="flex flex-wrap items-center justify-between gap-2"> | |
<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 min-w-0" 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> | |
<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 min-w-0" 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> | |
<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 min-w-0" 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> | |
<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 min-w-0" 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> | |
<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> | |
<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> | |
<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"> | |
<button id="fullscreenButton" class="absolute right-4 top-4 z-10 text-gray-400 hover:text-primary dark:hover:text-primary-light transition-colors" aria-label="Fullscreen"> | |
<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="M4 4h6M4 4v6M4 4l6 6M20 20h-6M20 20v-6M20 20l-6-6" /> | |
</svg> | |
</button> | |
<div class="flex-1 overflow-y-auto p-4 space-y-5" id="chatMessages"></div> | |
<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" id="sendIcon" 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 id="resizeHandle"></div> | |
</div> | |
<script> | |
const chatContainer = document.getElementById('chatContainer'); | |
const fullscreenButton = document.getElementById('fullscreenButton'); | |
const chatWrapper = document.getElementById('chatWrapper'); | |
const resizeHandle = document.getElementById('resizeHandle'); | |
// Fullscreen animation | |
fullscreenButton.addEventListener('click', () => { | |
if (!document.fullscreenElement) { | |
chatContainer.requestFullscreen().then(() => { | |
chatContainer.classList.add('fullscreen-animate'); | |
setTimeout(() => { | |
chatContainer.classList.remove('fullscreen-animate'); | |
}, 300); | |
}); | |
} else { | |
document.exitFullscreen(); | |
} | |
}); | |
// Resize functionality | |
let isResizing = false; | |
let lastDownX, lastDownY; | |
resizeHandle.addEventListener('mousedown', (e) => { | |
isResizing = true; | |
lastDownX = e.clientX; | |
lastDownY = e.clientY; | |
e.preventDefault(); | |
}); | |
document.addEventListener('mousemove', (e) => { | |
if (!isResizing) return; | |
const dx = e.clientX - lastDownX; | |
const dy = e.clientY - lastDownY; | |
const newWidth = chatWrapper.offsetWidth + dx; | |
const newHeight = chatWrapper.offsetHeight + dy; | |
chatWrapper.style.width = newWidth + 'px'; | |
chatWrapper.style.height = newHeight + 'px'; | |
lastDownX = e.clientX; | |
lastDownY = e.clientY; | |
}); | |
document.addEventListener('mouseup', () => { | |
isResizing = false; | |
}); | |
</script> | |
<!-- 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 (/gpt|openai/i.test(modelName)) 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> |