| <!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; |
| } |
| |
| |
| 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> |
|
|