Skip to content

Instantly share code, notes, and snippets.

@mathisve
Created May 30, 2025 23:15
Show Gist options
  • Save mathisve/c2cb67d725f94d230524863510608265 to your computer and use it in GitHub Desktop.
Save mathisve/c2cb67d725f94d230524863510608265 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ollama Chat</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/9.1.2/marked.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.js"></script>
<style>
@import url('https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css');
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.glass-morph {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.floating-orb {
position: absolute;
border-radius: 50%;
filter: blur(40px);
opacity: 0.3;
animation: float 20s infinite linear;
}
.orb-1 {
width: 200px;
height: 200px;
background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
top: 10%;
left: 10%;
}
.orb-2 {
width: 150px;
height: 150px;
background: linear-gradient(45deg, #a8edea, #fed6e3);
top: 60%;
right: 15%;
animation-delay: -10s;
}
.orb-3 {
width: 100px;
height: 100px;
background: linear-gradient(45deg, #ffecd2, #fcb69f);
bottom: 20%;
left: 20%;
animation-delay: -5s;
}
@keyframes float {
0%, 100% { transform: translateY(0px) rotate(0deg); }
33% { transform: translateY(-30px) rotate(120deg); }
66% { transform: translateY(15px) rotate(240deg); }
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(20px) scale(0.95); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
@keyframes typing {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.slide-in {
animation: slideIn 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.pulse-animation {
animation: pulse 2s infinite;
}
.typing-animation {
animation: typing 1.5s infinite;
}
.message-hover:hover {
transform: translateY(-1px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
}
.glow-effect {
box-shadow: 0 0 20px rgba(139, 92, 246, 0.3);
}
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(139, 92, 246, 0.3);
border-radius: 3px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: rgba(139, 92, 246, 0.5);
}
@keyframes shimmer {
0% { transform: translateX(-100%) skewX(-12deg); }
100% { transform: translateX(200%) skewX(-12deg); }
}
</style>
</head>
<body class="min-h-screen bg-gradient-to-br from-indigo-900 via-purple-900 to-pink-800 relative">
<!-- Floating Background Orbs -->
<div class="floating-orb orb-1"></div>
<div class="floating-orb orb-2"></div>
<div class="floating-orb orb-3"></div>
<div class="min-h-screen flex items-center justify-center p-4 relative z-10">
<div class="glass-morph rounded-3xl shadow-2xl w-full max-w-5xl h-[90vh] flex flex-col overflow-hidden">
<!-- Header -->
<div class="bg-gradient-to-r from-purple-600 via-pink-600 to-indigo-600 p-6 relative overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-r from-transparent via-white to-transparent opacity-10 transform -skew-x-12"></div>
<div class="relative flex items-center justify-center space-x-3">
<div class="w-10 h-10 bg-white bg-opacity-20 rounded-full flex items-center justify-center">
<span class="text-2xl">🦙</span>
</div>
<h1 class="text-3xl font-bold text-white tracking-tight">Ollama Chat</h1>
</div>
<p class="text-center text-purple-100 text-sm mt-2 opacity-90">Powered by AI • Real-time Conversations</p>
</div>
<!-- Configuration Panel -->
<div class="bg-white bg-opacity-5 backdrop-blur-sm border-b border-white border-opacity-10 p-5">
<div class="flex flex-wrap items-center justify-center gap-6">
<div class="flex items-center space-x-3">
<div class="w-3 h-3 bg-green-400 rounded-full pulse-animation"></div>
<label for="ollama-url" class="text-sm font-medium text-white opacity-90">Server URL:</label>
<input type="text" id="ollama-url" value="http://localhost:11434"
class="px-4 py-2 bg-white bg-opacity-10 border border-white border-opacity-20 rounded-xl text-white placeholder-white placeholder-opacity-60 text-sm focus:ring-2 focus:ring-purple-400 focus:border-transparent outline-none transition-all backdrop-blur-sm">
</div>
<div class="flex items-center space-x-3">
<div class="w-3 h-3 bg-blue-400 rounded-full pulse-animation" style="animation-delay: 0.5s;"></div>
<label for="model-select" class="text-sm font-medium text-white opacity-90">Model:</label>
<select id="model-select"
class="px-4 py-2 bg-white bg-opacity-10 border border-white border-opacity-20 rounded-xl text-white text-sm focus:ring-2 focus:ring-purple-400 focus:border-transparent outline-none transition-all backdrop-blur-sm">
<option value="" class="bg-gray-800 text-white">Loading models...</option>
</select>
<button id="refresh-models" title="Refresh Models"
class="w-8 h-8 bg-white bg-opacity-10 hover:bg-opacity-20 border border-white border-opacity-20 rounded-lg flex items-center justify-center transition-all text-white hover:scale-105">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
</button>
</div>
</div>
</div>
<!-- Messages Container -->
<div class="flex-1 overflow-y-auto p-6 space-y-6 custom-scrollbar" id="messages">
<!-- Messages will be inserted here -->
</div>
<!-- Input Section -->
<div class="bg-white bg-opacity-5 backdrop-blur-sm border-t border-white border-opacity-10 p-6">
<div class="flex items-end space-x-4">
<div class="flex-1 relative">
<input type="text" id="message-input" placeholder="Ask me anything..."
class="w-full px-6 py-4 bg-white bg-opacity-10 border border-white border-opacity-20 rounded-2xl text-white placeholder-white placeholder-opacity-60 text-lg focus:ring-2 focus:ring-purple-400 focus:border-transparent outline-none transition-all backdrop-blur-sm">
<div class="absolute right-4 top-1/2 transform -translate-y-1/2 text-white opacity-40">
<kbd class="px-2 py-1 text-xs bg-white bg-opacity-10 rounded">Enter</kbd>
</div>
</div>
<button id="send-button"
class="w-14 h-14 bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white rounded-2xl flex items-center justify-center hover:scale-105 hover:shadow-lg hover:shadow-purple-500/25 transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 glow-effect">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"></path>
</svg>
</button>
</div>
</div>
</div>
</div>
<script>
let isLoading = false;
const messagesDiv = document.getElementById('messages');
const messageInput = document.getElementById('message-input');
const sendButton = document.getElementById('send-button');
const ollamaUrlInput = document.getElementById('ollama-url');
const modelSelect = document.getElementById('model-select');
const refreshModelsBtn = document.getElementById('refresh-models');
// Load available models
async function loadModels() {
const ollamaUrl = ollamaUrlInput.value.trim();
if (!ollamaUrl) return;
try {
const response = await fetch(`${ollamaUrl}/api/tags`);
if (!response.ok) throw new Error('Failed to fetch models');
const data = await response.json();
modelSelect.innerHTML = '';
if (data.models && data.models.length > 0) {
data.models.forEach(model => {
const option = document.createElement('option');
option.value = model.name;
option.textContent = `${model.name} (${(model.size / 1e9).toFixed(1)}GB)`;
option.className = 'bg-gray-800 text-white';
modelSelect.appendChild(option);
});
} else {
const option = document.createElement('option');
option.value = '';
option.textContent = 'No models found';
option.className = 'bg-gray-800 text-white';
modelSelect.appendChild(option);
}
} catch (error) {
modelSelect.innerHTML = '';
const option = document.createElement('option');
option.value = '';
option.textContent = 'Error loading models';
option.className = 'bg-gray-800 text-white';
modelSelect.appendChild(option);
console.error('Error loading models:', error);
}
}
function addMessage(content, isUser = false) {
const messageDiv = document.createElement('div');
messageDiv.className = `slide-in flex ${isUser ? 'justify-end' : 'justify-start'} mb-4`;
const messageBubble = document.createElement('div');
messageBubble.className = isUser
? 'max-w-xs sm:max-w-md lg:max-w-lg xl:max-w-xl bg-gradient-to-r from-purple-500 to-pink-500 text-white px-6 py-4 rounded-3xl rounded-br-lg shadow-lg message-hover transition-all duration-300'
: 'max-w-xs sm:max-w-md lg:max-w-2xl bg-white bg-opacity-90 backdrop-blur-sm text-gray-800 px-6 py-4 rounded-3xl rounded-bl-lg shadow-lg border border-white border-opacity-50 message-hover transition-all duration-300';
if (isUser) {
messageBubble.textContent = content;
} else {
// Check if this is a DeepSeek model and handle thinking sections
const selectedModel = modelSelect.value.toLowerCase();
const isDeepSeek = selectedModel.includes('deepseek');
let processedContent = content;
// Handle DeepSeek thinking tags
if (isDeepSeek) {
processedContent = content.replace(
/<think>([\s\S]*?)<\/think>/g,
'<div class="deepseek-thinking">💭 **Thinking Process:**\n\n$1</div>'
);
}
// Render markdown for assistant messages
messageBubble.innerHTML = marked.parse(processedContent);
// Style DeepSeek thinking sections
const thinkingSections = messageBubble.querySelectorAll('.deepseek-thinking');
thinkingSections.forEach(section => {
section.className = 'deepseek-thinking bg-gradient-to-r from-blue-50 to-indigo-50 border-l-4 border-blue-400 p-4 rounded-r-xl my-4 text-blue-800 italic relative overflow-hidden';
// Add subtle animation background
const overlay = document.createElement('div');
overlay.className = 'absolute inset-0 bg-gradient-to-r from-transparent via-blue-100 to-transparent opacity-30 transform -skew-x-12';
overlay.style.animation = 'shimmer 3s infinite';
section.appendChild(overlay);
});
// Enhanced markdown styling
const headers = messageBubble.querySelectorAll('h1, h2, h3, h4, h5, h6');
headers.forEach(h => h.className = 'font-bold text-gray-900 mt-6 mb-3 first:mt-0');
const codeBlocks = messageBubble.querySelectorAll('pre');
codeBlocks.forEach(pre => {
pre.className = 'bg-gray-900 text-green-400 p-4 rounded-xl my-4 overflow-x-auto shadow-inner border border-gray-700';
const code = pre.querySelector('code');
if (code) code.className = 'text-sm font-mono leading-relaxed';
});
const inlineCode = messageBubble.querySelectorAll('code:not(pre code)');
inlineCode.forEach(code => code.className = 'bg-purple-100 text-purple-800 px-2 py-1 rounded-lg text-sm font-mono');
const lists = messageBubble.querySelectorAll('ul, ol');
lists.forEach(list => list.className = 'ml-6 my-3 space-y-1');
const listItems = messageBubble.querySelectorAll('li');
listItems.forEach(li => li.className = 'leading-relaxed');
const paragraphs = messageBubble.querySelectorAll('p');
paragraphs.forEach(p => p.className = 'mb-4 last:mb-0 leading-relaxed');
const blockquotes = messageBubble.querySelectorAll('blockquote');
blockquotes.forEach(bq => bq.className = 'border-l-4 border-purple-400 pl-4 italic my-4 text-gray-600 bg-purple-50 py-2 rounded-r-lg');
const links = messageBubble.querySelectorAll('a');
links.forEach(link => link.className = 'text-purple-600 hover:text-purple-800 underline transition-colors');
}
messageDiv.appendChild(messageBubble);
messagesDiv.appendChild(messageDiv);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
function addLoadingMessage() {
const loadingDiv = document.createElement('div');
loadingDiv.className = 'slide-in flex justify-start mb-4';
loadingDiv.id = 'loading-message';
const loadingBubble = document.createElement('div');
loadingBubble.className = 'max-w-xs bg-white bg-opacity-90 backdrop-blur-sm text-gray-600 px-6 py-4 rounded-3xl rounded-bl-lg shadow-lg border border-white border-opacity-50 flex items-center space-x-3';
const dotsContainer = document.createElement('div');
dotsContainer.className = 'flex space-x-1';
for (let i = 0; i < 3; i++) {
const dot = document.createElement('div');
dot.className = 'w-2 h-2 bg-purple-400 rounded-full typing-animation';
dot.style.animationDelay = `${i * 0.2}s`;
dotsContainer.appendChild(dot);
}
const loadingText = document.createElement('span');
loadingText.textContent = 'Thinking...';
loadingText.className = 'text-sm font-medium';
loadingBubble.appendChild(dotsContainer);
loadingBubble.appendChild(loadingText);
loadingDiv.appendChild(loadingBubble);
messagesDiv.appendChild(loadingDiv);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
function removeLoadingMessage() {
const loadingMsg = document.getElementById('loading-message');
if (loadingMsg) {
loadingMsg.remove();
}
}
function showError(message) {
const errorDiv = document.createElement('div');
errorDiv.className = 'slide-in bg-red-500 bg-opacity-10 backdrop-blur-sm border border-red-400 border-opacity-30 text-red-100 px-6 py-4 rounded-2xl mb-4 shadow-lg';
const errorContent = document.createElement('div');
errorContent.className = 'flex items-center space-x-3';
const errorIcon = document.createElement('div');
errorIcon.innerHTML = '⚠️';
errorIcon.className = 'text-xl';
const errorText = document.createElement('span');
errorText.textContent = message;
errorText.className = 'text-sm font-medium';
errorContent.appendChild(errorIcon);
errorContent.appendChild(errorText);
errorDiv.appendChild(errorContent);
messagesDiv.appendChild(errorDiv);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
async function sendMessage() {
const message = messageInput.value.trim();
if (!message || isLoading) return;
const ollamaUrl = ollamaUrlInput.value.trim();
const modelName = modelSelect.value;
if (!ollamaUrl || !modelName) {
showError('Please configure server URL and select a model');
return;
}
// Add user message
addMessage(message, true);
messageInput.value = '';
// Set loading state
isLoading = true;
sendButton.disabled = true;
addLoadingMessage();
try {
const response = await fetch(`${ollamaUrl}/api/generate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: modelName,
prompt: message,
stream: false
})
});
if (!response.ok) {
throw new Error(`Server responded with status ${response.status}`);
}
const data = await response.json();
removeLoadingMessage();
if (data.response) {
addMessage(data.response);
} else {
showError('No response received from the AI model');
}
} catch (error) {
removeLoadingMessage();
showError(`Connection failed: ${error.message}. Please check if Ollama is running and accessible.`);
} finally {
isLoading = false;
sendButton.disabled = false;
messageInput.focus();
}
}
// Event listeners
sendButton.addEventListener('click', sendMessage);
refreshModelsBtn.addEventListener('click', loadModels);
ollamaUrlInput.addEventListener('change', loadModels);
messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
// Auto-focus input and load models on startup
messageInput.focus();
loadModels();
// Welcome message with enhanced styling
setTimeout(() => {
addMessage('👋 **Welcome to Ollama Chat!**\n\nI\'m your AI assistant, ready to help with questions, coding, creative writing, and more. Make sure Ollama is running locally, then let\'s start our conversation!\n\n*Tip: You can press Enter to send messages quickly.*');
}, 500);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment