lokiai / playground.html
ParthSadaria's picture
Update playground.html
b286547 verified
raw
history blame
55.7 kB
<!DOCTYPE html>
<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 !important;
overflow-x: auto;
}
.markdown-content pre {
margin: 1rem 0;
padding: 1rem;
border-radius: 8px;
background-color: #F1F5F9 !important;
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>