|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>Local LLM Chat</title> |
|
|
<script src="https://cdn.tailwindcss.com"></script> |
|
|
<script src="https://unpkg.com/feather-icons"></script> |
|
|
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> |
|
|
<script src="https://cdn.jsdelivr.net/npm/animejs/lib/anime.iife.min.js"></script> |
|
|
<style> |
|
|
@keyframes fadeIn { |
|
|
from { opacity: 0; transform: translateY(10px); } |
|
|
to { opacity: 1; transform: translateY(0); } |
|
|
} |
|
|
.message-animation { |
|
|
animation: fadeIn 0.3s ease-out forwards; |
|
|
} |
|
|
.chat-container { |
|
|
height: calc(100vh - 160px); |
|
|
} |
|
|
.typing-indicator::after { |
|
|
content: '...'; |
|
|
animation: typing 1.5s infinite; |
|
|
} |
|
|
@keyframes typing { |
|
|
0% { content: '.'; } |
|
|
33% { content: '..'; } |
|
|
66% { content: '...'; } |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body class="bg-gray-50 text-gray-800 font-sans"> |
|
|
<div class="max-w-4xl mx-auto h-screen flex flex-col"> |
|
|
|
|
|
<header class="bg-white shadow-sm py-4 px-6 flex items-center justify-between"> |
|
|
<div class="flex items-center space-x-3"> |
|
|
<div class="w-10 h-10 rounded-full bg-gradient-to-r from-purple-500 to-blue-500 flex items-center justify-center"> |
|
|
<i data-feather="cpu" class="text-white"></i> |
|
|
</div> |
|
|
<h1 class="text-xl font-semibold">Local LLM Chat</h1> |
|
|
</div> |
|
|
<button class="p-2 rounded-full hover:bg-gray-100 transition-colors"> |
|
|
<i data-feather="settings" class="text-gray-500"></i> |
|
|
</button> |
|
|
</header> |
|
|
|
|
|
|
|
|
<div class="chat-container overflow-y-auto p-6 space-y-4 flex-1"> |
|
|
|
|
|
<div class="flex justify-start" data-aos="fade-right"> |
|
|
<div class="max-w-xs md:max-w-md lg:max-w-lg bg-white rounded-2xl p-4 shadow-sm"> |
|
|
<div class="flex items-start space-x-2"> |
|
|
<div class="w-8 h-8 rounded-full bg-gray-200 flex items-center justify-center"> |
|
|
<i data-feather="cpu" class="text-gray-600 w-4 h-4"></i> |
|
|
</div> |
|
|
<div> |
|
|
<p class="text-sm font-medium text-gray-700">Local LLM</p> |
|
|
<p class="text-gray-600 mt-1">Hello! I'm your local AI assistant. How can I help you today?</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="flex justify-end" data-aos="fade-left"> |
|
|
<div class="max-w-xs md:max-w-md lg:max-w-lg bg-blue-500 text-white rounded-2xl p-4 shadow-sm"> |
|
|
<p>What can you do?</p> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="flex justify-start"> |
|
|
<div class="max-w-xs md:max-w-md lg:max-w-lg bg-white rounded-2xl p-4 shadow-sm"> |
|
|
<div class="flex items-start space-x-2"> |
|
|
<div class="w-8 h-8 rounded-full bg-gray-200 flex items-center justify-center"> |
|
|
<i data-feather="cpu" class="text-gray-600 w-4 h-4"></i> |
|
|
</div> |
|
|
<div> |
|
|
<p class="text-sm font-medium text-gray-700">Local LLM</p> |
|
|
<p class="text-gray-600 mt-1">I can answer questions, help with creative writing, summarize text, and much more - all running locally on your machine for privacy.</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div id="typing-indicator" class="flex justify-start hidden"> |
|
|
<div class="max-w-xs md:max-w-md lg:max-w-lg bg-white rounded-2xl p-4 shadow-sm"> |
|
|
<div class="flex items-start space-x-2"> |
|
|
<div class="w-8 h-8 rounded-full bg-gray-200 flex items-center justify-center"> |
|
|
<i data-feather="cpu" class="text-gray-600 w-4 h-4"></i> |
|
|
</div> |
|
|
<div> |
|
|
<p class="text-sm font-medium text-gray-700">Local LLM</p> |
|
|
<p class="text-gray-600 mt-1 typing-indicator">Thinking</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="bg-white border-t border-gray-200 p-4"> |
|
|
<form id="chat-form" class="flex items-center space-x-2"> |
|
|
<div class="flex-1 relative"> |
|
|
<input |
|
|
id="message-input" |
|
|
type="text" |
|
|
placeholder="Type your message..." |
|
|
class="w-full px-4 py-3 rounded-full border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all" |
|
|
autocomplete="off" |
|
|
> |
|
|
<button type="button" class="absolute right-3 top-3 text-gray-400 hover:text-blue-500"> |
|
|
<i data-feather="paperclip"></i> |
|
|
</button> |
|
|
</div> |
|
|
<button |
|
|
type="submit" |
|
|
class="w-12 h-12 rounded-full bg-gradient-to-r from-purple-500 to-blue-500 text-white flex items-center justify-center hover:opacity-90 transition-opacity" |
|
|
> |
|
|
<i data-feather="send"></i> |
|
|
</button> |
|
|
</form> |
|
|
<p class="text-xs text-gray-500 mt-2 text-center">Your conversations stay on your device</p> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
feather.replace(); |
|
|
|
|
|
|
|
|
const chatForm = document.getElementById('chat-form'); |
|
|
const messageInput = document.getElementById('message-input'); |
|
|
const chatContainer = document.querySelector('.chat-container'); |
|
|
const typingIndicator = document.getElementById('typing-indicator'); |
|
|
|
|
|
chatForm.addEventListener('submit', function(e) { |
|
|
e.preventDefault(); |
|
|
const message = messageInput.value.trim(); |
|
|
if (!message) return; |
|
|
|
|
|
|
|
|
addMessage(message, 'user'); |
|
|
messageInput.value = ''; |
|
|
|
|
|
|
|
|
typingIndicator.classList.remove('hidden'); |
|
|
chatContainer.scrollTop = chatContainer.scrollHeight; |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
typingIndicator.classList.add('hidden'); |
|
|
addMessage(getRandomResponse(), 'ai'); |
|
|
}, 1500 + Math.random() * 2000); |
|
|
}); |
|
|
|
|
|
function addMessage(text, sender) { |
|
|
const messageDiv = document.createElement('div'); |
|
|
messageDiv.classList.add('flex', 'message-animation'); |
|
|
|
|
|
if (sender === 'user') { |
|
|
messageDiv.classList.add('justify-end'); |
|
|
messageDiv.innerHTML = ` |
|
|
<div class="max-w-xs md:max-w-md lg:max-w-lg bg-blue-500 text-white rounded-2xl p-4 shadow-sm"> |
|
|
<p>${text}</p> |
|
|
</div> |
|
|
`; |
|
|
} else { |
|
|
messageDiv.classList.add('justify-start'); |
|
|
messageDiv.innerHTML = ` |
|
|
<div class="max-w-xs md:max-w-md lg:max-w-lg bg-white rounded-2xl p-4 shadow-sm"> |
|
|
<div class="flex items-start space-x-2"> |
|
|
<div class="w-8 h-8 rounded-full bg-gray-200 flex items-center justify-center"> |
|
|
<i data-feather="cpu" class="text-gray-600 w-4 h-4"></i> |
|
|
</div> |
|
|
<div> |
|
|
<p class="text-sm font-medium text-gray-700">Local LLM</p> |
|
|
<p class="text-gray-600 mt-1">${text}</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
chatContainer.appendChild(messageDiv); |
|
|
chatContainer.scrollTop = chatContainer.scrollHeight; |
|
|
feather.replace(); |
|
|
} |
|
|
|
|
|
function getRandomResponse() { |
|
|
const responses = [ |
|
|
"That's an interesting question. Based on my training data, I would say...", |
|
|
"I can help with that. Here's what I know about this topic...", |
|
|
"Great question! The answer depends on several factors...", |
|
|
"I'm glad you asked. This is a complex topic, but in summary...", |
|
|
"Let me think about that for a moment. My analysis suggests..." |
|
|
]; |
|
|
return responses[Math.floor(Math.random() * responses.length)]; |
|
|
} |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
|