1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8" /> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"/> 6 <title>NovaChat</title> 7 <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> 8 </head> 9 <body> 10 <div class="chat-app"> 11 <header class="chat-header"> 12 <h1>NovaChat</h1> 13 <button id="clear-btn" title="Clear chat">🗑️</button> 14 </header> 15 <div id="chat-container"> 16 {% for msg in chat_history %} 17 {% if msg.role != 'system' %} 18 <div class="message {{ msg.role }}"> 19 <div class="bubble">{{ msg.content }}</div> 20 </div> 21 {% endif %} 22 {% endfor %} 23 </div> 24 <form id="chat-form"> 25 <input 26 type="text" 27 id="message-input" 28 name="message" 29 placeholder="Type your message…" 30 autocomplete="off" 31 /> 32 <button type="submit">Send</button> 33 </form> 34 </div> 35 36 <script> 37 const form = document.getElementById('chat-form'); 38 const input = document.getElementById('message-input'); 39 const chatContainer = document.getElementById('chat-container'); 40 const clearBtn = document.getElementById('clear-btn'); 41 42 function renderChat(history) { 43 chatContainer.innerHTML = ''; 44 history.forEach(msg => { 45 if (msg.role === 'system') return; 46 const wrapper = document.createElement('div'); 47 wrapper.className = `message ${msg.role}`; 48 const bubble = document.createElement('div'); 49 bubble.className = 'bubble'; 50 bubble.textContent = msg.content; 51 wrapper.appendChild(bubble); 52 chatContainer.appendChild(wrapper); 53 }); 54 chatContainer.scrollTop = chatContainer.scrollHeight; 55 } 56 57 function showTyping() { 58 const typingWrapper = document.createElement('div'); 59 typingWrapper.className = 'message assistant typing'; 60 const bubble = document.createElement('div'); 61 bubble.className = 'bubble'; 62 bubble.textContent = 'Nova is typing'; 63 typingWrapper.appendChild(bubble); 64 chatContainer.appendChild(typingWrapper); 65 chatContainer.scrollTop = chatContainer.scrollHeight; 66 return typingWrapper; 67 } 68 69 form.addEventListener('submit', async e => { 70 e.preventDefault(); 71 const text = input.value.trim(); 72 if (!text) return; 73 const formData = new FormData(); 74 formData.append('message', text); 75 76 renderChat([...JSON.parse(localStorage.getItem('chat') || '[]'), {role:'user',content:text}]); 77 input.value = ''; 78 79 const typingElem = showTyping(); 80 81 const res = await fetch('/chat', { method: 'POST', body: formData }); 82 const data = await res.json(); 83 if (data.chat_history) { 84 typingElem.remove(); 85 renderChat(data.chat_history); 86 localStorage.setItem('chat', JSON.stringify(data.chat_history.filter(m=>m.role!=='system'))); 87 } 88 }); 89 90 clearBtn.addEventListener('click', async () => { 91 const res = await fetch('/clear', { method: 'POST' }); 92 const data = await res.json(); 93 if (data.chat_history) { 94 renderChat(data.chat_history); 95 localStorage.removeItem('chat'); 96 } 97 }); 98 99 window.addEventListener('load', () => { 100 const initial = {{ chat_history|tojson }}.filter(m=>m.role!=='system'); 101 localStorage.setItem('chat', JSON.stringify(initial)); 102 renderChat(initial); 103 }); 104 </script> 105 </body> 106 </html>