| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>🦙 LLaMA Chat</title> |
| <style> |
| * { |
| margin: 0; |
| padding: 0; |
| box-sizing: border-box; |
| } |
| |
| body { |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; |
| background: #0f0f0f; |
| color: #e0e0e0; |
| height: 100vh; |
| display: flex; |
| flex-direction: column; |
| } |
| |
| .header { |
| padding: 16px 24px; |
| background: #1a1a2e; |
| border-bottom: 1px solid #333; |
| text-align: center; |
| } |
| |
| .header h1 { |
| font-size: 1.4rem; |
| color: #a78bfa; |
| } |
| |
| .header p { |
| font-size: 0.8rem; |
| color: #888; |
| margin-top: 4px; |
| } |
| |
| .chat-container { |
| flex: 1; |
| overflow-y: auto; |
| padding: 20px 24px; |
| display: flex; |
| flex-direction: column; |
| gap: 16px; |
| } |
| |
| .message { |
| max-width: 75%; |
| padding: 12px 16px; |
| border-radius: 16px; |
| line-height: 1.5; |
| font-size: 0.95rem; |
| white-space: pre-wrap; |
| word-wrap: break-word; |
| } |
| |
| .message.user { |
| align-self: flex-end; |
| background: #4338ca; |
| color: #fff; |
| border-bottom-right-radius: 4px; |
| } |
| |
| .message.assistant { |
| align-self: flex-start; |
| background: #1e1e2e; |
| color: #d4d4d4; |
| border: 1px solid #333; |
| border-bottom-left-radius: 4px; |
| } |
| |
| .message.assistant .typing-cursor { |
| display: inline-block; |
| width: 8px; |
| height: 16px; |
| background: #a78bfa; |
| animation: blink 0.7s infinite; |
| vertical-align: text-bottom; |
| margin-left: 2px; |
| } |
| |
| @keyframes blink { |
| 0%, 100% { opacity: 1; } |
| 50% { opacity: 0; } |
| } |
| |
| .input-area { |
| padding: 16px 24px; |
| background: #1a1a2e; |
| border-top: 1px solid #333; |
| display: flex; |
| gap: 12px; |
| } |
| |
| .input-area textarea { |
| flex: 1; |
| padding: 12px 16px; |
| border: 1px solid #444; |
| border-radius: 12px; |
| background: #0f0f0f; |
| color: #e0e0e0; |
| font-size: 0.95rem; |
| font-family: inherit; |
| resize: none; |
| outline: none; |
| max-height: 120px; |
| } |
| |
| .input-area textarea:focus { |
| border-color: #a78bfa; |
| } |
| |
| .input-area button { |
| padding: 12px 24px; |
| background: #7c3aed; |
| color: #fff; |
| border: none; |
| border-radius: 12px; |
| font-size: 0.95rem; |
| cursor: pointer; |
| transition: background 0.2s; |
| } |
| |
| .input-area button:hover { |
| background: #6d28d9; |
| } |
| |
| .input-area button:disabled { |
| background: #444; |
| cursor: not-allowed; |
| } |
| |
| .status { |
| text-align: center; |
| padding: 40px; |
| color: #888; |
| } |
| </style> |
| </head> |
| <body> |
| <div class="header"> |
| <h1>🦙 LLaMA Chat</h1> |
| <p>SmolLM2 1.7B Instruct • CPU • GGUF</p> |
| </div> |
|
|
| <div class="chat-container" id="chatContainer"> |
| <div class="status" id="welcome"> |
| Type a message below to start chatting! |
| </div> |
| </div> |
|
|
| <div class="input-area"> |
| <textarea |
| id="userInput" |
| rows="1" |
| placeholder="Type your message..." |
| onkeydown="handleKey(event)" |
| ></textarea> |
| <button id="sendBtn" onclick="sendMessage()">Send</button> |
| </div> |
|
|
| <script> |
| const chatContainer = document.getElementById('chatContainer'); |
| const userInput = document.getElementById('userInput'); |
| const sendBtn = document.getElementById('sendBtn'); |
| const welcome = document.getElementById('welcome'); |
| let isGenerating = false; |
| |
| |
| userInput.addEventListener('input', function () { |
| this.style.height = 'auto'; |
| this.style.height = Math.min(this.scrollHeight, 120) + 'px'; |
| }); |
| |
| function handleKey(e) { |
| if (e.key === 'Enter' && !e.shiftKey) { |
| e.preventDefault(); |
| sendMessage(); |
| } |
| } |
| |
| function addMessage(role, text) { |
| if (welcome) welcome.remove(); |
| |
| const div = document.createElement('div'); |
| div.className = `message ${role}`; |
| div.textContent = text; |
| chatContainer.appendChild(div); |
| chatContainer.scrollTop = chatContainer.scrollHeight; |
| return div; |
| } |
| |
| async function sendMessage() { |
| const msg = userInput.value.trim(); |
| if (!msg || isGenerating) return; |
| |
| isGenerating = true; |
| sendBtn.disabled = true; |
| userInput.value = ''; |
| userInput.style.height = 'auto'; |
| |
| addMessage('user', msg); |
| |
| |
| if (welcome) welcome.remove(); |
| const assistantDiv = document.createElement('div'); |
| assistantDiv.className = 'message assistant'; |
| const cursor = document.createElement('span'); |
| cursor.className = 'typing-cursor'; |
| assistantDiv.appendChild(cursor); |
| chatContainer.appendChild(assistantDiv); |
| chatContainer.scrollTop = chatContainer.scrollHeight; |
| |
| try { |
| const response = await fetch('/chat/stream', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ message: msg }), |
| }); |
| |
| const reader = response.body.getReader(); |
| const decoder = new TextDecoder(); |
| let fullText = ''; |
| 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: ')) { |
| const data = line.slice(6).trim(); |
| if (data === '[DONE]') continue; |
| try { |
| const parsed = JSON.parse(data); |
| if (parsed.content) { |
| fullText += parsed.content; |
| assistantDiv.textContent = fullText; |
| |
| const newCursor = document.createElement('span'); |
| newCursor.className = 'typing-cursor'; |
| assistantDiv.appendChild(newCursor); |
| chatContainer.scrollTop = chatContainer.scrollHeight; |
| } |
| if (parsed.error) { |
| fullText += `\n[Error: ${parsed.error}]`; |
| assistantDiv.textContent = fullText; |
| } |
| } catch (e) { } |
| } |
| } |
| } |
| |
| |
| assistantDiv.textContent = fullText || '(No response)'; |
| |
| } catch (err) { |
| assistantDiv.textContent = `Error: ${err.message}`; |
| } |
| |
| isGenerating = false; |
| sendBtn.disabled = false; |
| userInput.focus(); |
| } |
| </script> |
| </body> |
| </html> |