freeapi / templates /chat.html
xtom0's picture
Simplify to single-model chat app: llama3.1:8b only. Add /api/chat SSE streaming, chatbot UI, remove model selection/download UI, build-time model pull
946d512
Raw
History Blame Contribute Delete
9.23 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chat - Ollama</title>
<link rel="stylesheet" href="/static/style.css">
<style>
.chat-layout { display: flex; gap: 20px; min-height: 70vh; }
.chat-sidebar { width: 260px; flex-shrink: 0; }
.chat-main { flex: 1; display: flex; flex-direction: column; }
.chat-box { flex: 1; overflow-y: auto; padding: 16px; background: #f8f9fa; border-radius: 12px; margin-bottom: 12px; max-height: 60vh; }
.message { margin-bottom: 16px; display: flex; }
.message.user { justify-content: flex-end; }
.message.assistant { justify-content: flex-start; }
.message .bubble { max-width: 80%; padding: 10px 16px; border-radius: 16px; line-height: 1.5; white-space: pre-wrap; }
.message.user .bubble { background: #4361ee; color: white; border-bottom-right-radius: 4px; }
.message.assistant .bubble { background: white; color: #333; border: 1px solid #e0e0e0; border-bottom-left-radius: 4px; }
.chat-input-row { display: flex; gap: 8px; }
.chat-input-row input { flex: 1; padding: 12px 16px; border: 1px solid #ddd; border-radius: 24px; font-size: 1rem; }
.chat-input-row button { padding: 12px 24px; border-radius: 24px; }
.model-badge { display: inline-block; padding: 4px 12px; border-radius: 12px; font-size: 0.8rem; font-weight: 600; margin-bottom: 12px; }
.model-badge.ready { background: #d4edda; color: #155724; }
.model-badge.loading { background: #fff3cd; color: #856404; }
.model-badge.error { background: #f8d7da; color: #721c24; }
#status-msg { font-size: 0.9rem; color: #666; margin-top: 8px; }
.typing-indicator { display: none; padding: 10px 16px; background: white; border: 1px solid #e0e0e0; border-radius: 16px; border-bottom-left-radius: 4px; max-width: 80px; }
.typing-indicator.active { display: flex; gap: 4px; }
.typing-indicator span { width: 8px; height: 8px; background: #999; border-radius: 50%; animation: bounce 1.4s infinite; }
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
@keyframes bounce { 0%, 80%, 100% { transform: translateY(0); } 40% { transform: translateY(-8px); } }
@media (max-width: 768px) { .chat-layout { flex-direction: column; } .chat-sidebar { width: 100%; } }
</style>
</head>
<body>
<div class="container">
<header>
<h1>Llama 3.1 Chat</h1>
<div>
<span id="model-status" class="model-badge loading">Checking...</span>
<a href="/dashboard" class="btn btn-secondary">Dashboard</a>
<button class="btn btn-secondary" onclick="logout()">Logout</button>
</div>
</header>
<div class="chat-layout">
<div class="chat-sidebar">
<div class="card">
<h2>Info</h2>
<p><strong>Model:</strong> llama3.1:8b</p>
<p id="status-msg">Checking model status...</p>
<button class="btn btn-primary" style="width:100%;margin-top:12px" onclick="newChat()">New Chat</button>
</div>
</div>
<div class="chat-main">
<div class="chat-box" id="chat-box">
<div class="message assistant">
<div class="bubble">Hello! I'm Llama 3.1 (8B). Ask me anything.</div>
</div>
<div class="typing-indicator" id="typing"><span></span><span></span><span></span></div>
</div>
<form class="chat-input-row" onsubmit="sendMessage(event)">
<input type="text" id="message-input" placeholder="Type your message..." required autofocus>
<button type="submit" class="btn btn-primary">Send</button>
</form>
</div>
</div>
</div>
<script>
const token = localStorage.getItem('token');
if (!token) window.location.href = '/login';
let reading = false;
function logout() { localStorage.removeItem('token'); window.location.href = '/login'; }
function newChat() {
document.getElementById('chat-box').innerHTML = `
<div class="message assistant">
<div class="bubble">Hello! I'm Llama 3.1 (8B). Ask me anything.</div>
</div>
<div class="typing-indicator" id="typing"><span></span><span></span><span></span></div>
`;
}
function addMessage(role, content) {
const box = document.getElementById('chat-box');
const typing = document.getElementById('typing');
const div = document.createElement('div');
div.className = `message ${role}`;
div.innerHTML = `<div class="bubble">${content}</div>`;
box.insertBefore(div, typing);
box.scrollTop = box.scrollHeight;
}
// Check model status
async function checkStatus() {
const res = await fetch('/api/status', {headers: {'Authorization': `Bearer ${token}`}});
const data = await res.json();
const badge = document.getElementById('model-status');
const msg = document.getElementById('status-msg');
if (data.model) {
badge.className = 'model-badge ready';
badge.textContent = 'Model Ready';
msg.textContent = 'llama3.1:8b is ready to use';
} else if (data.ollama) {
badge.className = 'model-badge loading';
badge.textContent = 'Downloading...';
msg.textContent = data.message || 'Model is being downloaded in background. Please wait...';
setTimeout(checkStatus, 10000);
} else {
badge.className = 'model-badge error';
badge.textContent = 'Unavailable';
msg.textContent = data.message || 'Ollama not available';
}
}
checkStatus();
async function sendMessage(e) {
e.preventDefault();
if (reading) return;
const input = document.getElementById('message-input');
const text = input.value.trim();
if (!text) return;
input.value = '';
addMessage('user', text);
reading = true;
const typing = document.getElementById('typing');
typing.classList.add('active');
let assistantContent = '';
const bubbleDiv = document.createElement('div');
bubbleDiv.className = 'message assistant';
const bubble = document.createElement('div');
bubble.className = 'bubble';
bubbleDiv.appendChild(bubble);
const box = document.getElementById('chat-box');
box.insertBefore(bubbleDiv, typing);
try {
const res = await fetch('/api/chat', {
method: 'POST',
headers: {'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json'},
body: JSON.stringify({message: text, stream: true})
});
if (!res.ok) {
const err = await res.json();
bubble.textContent = err.detail || 'Error: Model not ready yet';
reading = false;
typing.classList.remove('active');
return;
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const {done, value} = await reader.read();
if (done) break;
buffer += decoder.decode(value, {stream: true});
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
try {
const data = JSON.parse(line.slice(6));
if (data.done) {
reading = false;
typing.classList.remove('active');
break;
}
if (data.content) {
assistantContent += data.content;
bubble.textContent = assistantContent;
box.scrollTop = box.scrollHeight;
}
} catch(e) {}
}
}
} catch(e) {
bubble.textContent = 'Error: Connection failed';
reading = false;
typing.classList.remove('active');
}
}
</script>
</body>
</html>