Created
May 30, 2025 23:15
-
-
Save mathisve/c2cb67d725f94d230524863510608265 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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