1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8" /> 5 <meta http-equiv="X-UA-Compatible" content="IE=edge"/> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0"/> 7 <title>NovaChat</title> 8 <!-- Bootstrap CSS --> 9 <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" /> 10 <!-- Animate.css --> 11 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css" /> 12 <!-- Highlight.js CSS --> 13 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.2.0/styles/atom-one-dark.min.css" /> 14 <!-- Custom Stylesheet (override) --> 15 <link rel="stylesheet" href="/static/style.css" /> 16 </head> 17 <body> 18 <!-- Sidebar --> 19 <div id="sidebar"> 20 <div class="sidebar-header"> 21 <h4 id="sidebarTitle">NovaChat</h4> 22 </div> 23 <div class="sidebar-menu"> 24 <button class="btn-newchat">+ New Chat</button> 25 <ul> 26 <li><a href="#" id="historyBtn">Chat History</a></li> 27 </ul> 28 </div> 29 <div class="sidebar-footer"> 30 <ul> 31 <li><a href="#">Settings</a></li> 32 <li><a href="#">About</a></li> 33 </ul> 34 </div> 35 </div> 36 37 <!-- Main Area --> 38 <div class="main" id="mainContent"> 39 <div class="topbar"> 40 <button class="toggle-btn" id="toggleSidebarBtn">☰</button> 41 <h5 class="mb-0">NovaChat</h5> 42 <button id="themeToggle" class="theme-toggle-btn"> 43 <span id="themeToggleIcon">☾</span> 44 <span id="themeToggleText">Dark Mode</span> 45 </button> 46 </div> 47 48 <div class="chat-container"> 49 <div id="emptyState" style="display:none;"> 50 <h1>What's your next big idea? I'm all ears.</h1> 51 </div> 52 <div class="message-area" id="chat"> 53 {% for msg in chat_history %} 54 {% if msg.role == "assistant" %} 55 <div class="message assistant-msg animate__animated animate__fadeIn"> 56 <strong>Assistant:</strong> 57 <div class="msg-content">{{ msg.content }}</div> 58 </div> 59 {% elif msg.role == "user" %} 60 <div class="message user-msg animate__animated animate__fadeIn"> 61 <strong>User:</strong> 62 <div class="msg-content">{{ msg.content }}</div> 63 </div> 64 {% endif %} 65 {% endfor %} 66 </div> 67 </div> 68 69 <!-- Input Area (fixed footer) --> 70 <div class="input-area"> 71 <textarea class="form-control" id="message" rows="3" placeholder="Type your message or use the mic..."></textarea> 72 <button class="btn-mic" id="micButton" title="Record">🎤</button> 73 <button class="btn-send" id="sendBtn">Send</button> 74 <button class="btn-clear" id="clearBtn">Clear</button> 75 </div> 76 </div> 77 78 <!-- Recording Modal --> 79 <div class="modal fade" id="recordingModal" tabindex="-1" aria-labelledby="recordingModalLabel" aria-hidden="true"> 80 <div class="modal-dialog modal-dialog-centered"> 81 <div class="modal-content"> 82 <div class="modal-header"> 83 <h5 class="modal-title" id="recordingModalLabel">Recording...</h5> 84 <button type="button" class="close" data-dismiss="modal" aria-label="Close" id="stopRecBtn"> 85 <span aria-hidden="true">×</span> 86 </button> 87 </div> 88 <div class="modal-body text-center"> 89 <p id="modalTranscript">Please speak now.</p> 90 <div class="spinner-border text-light" role="status"> 91 <span class="sr-only">Recording...</span> 92 </div> 93 </div> 94 <div class="modal-footer"> 95 <button type="button" class="btn btn-primary" id="finishRecBtn">Send</button> 96 </div> 97 </div> 98 </div> 99 </div> 100 101 <!-- Chat History Modal --> 102 <div class="modal fade" id="chatHistoryModal" tabindex="-1" aria-labelledby="chatHistoryModalLabel" aria-hidden="true"> 103 <div class="modal-dialog modal-dialog-centered modal-lg"> 104 <div class="modal-content"> 105 <div class="modal-header"> 106 <h5 class="modal-title" id="chatHistoryModalLabel">Chat History</h5> 107 <button type="button" class="close" data-dismiss="modal" aria-label="Close" id="closeHistoryBtn"> 108 <span aria-hidden="true">×</span> 109 </button> 110 </div> 111 <div class="modal-body"> 112 <div id="chatHistoryContent"></div> 113 </div> 114 <div class="modal-footer"> 115 <button type="button" class="btn btn-secondary" id="closeHistoryFooterBtn">Close</button> 116 </div> 117 </div> 118 </div> 119 </div> 120 121 <!-- JS Libraries --> 122 <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> 123 <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.bundle.min.js"></script> 124 <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> 125 <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.2.0/highlight.min.js"></script> 126 127 <script> 128 // Global 129 var chatHistory = {{ chat_history|tojson }}; 130 131 // Initialize theme & editor once DOM loaded 132 document.addEventListener('DOMContentLoaded', () => { 133 initTheme(); 134 checkIfEmpty(); 135 bindButtons(); 136 // Render existing messages from server as Markdown 137 document.querySelectorAll('#chat .msg-content').forEach(div => { 138 div.innerHTML = marked.parse(div.textContent); 139 }); 140 // Inject copy buttons into any <pre> blocks 141 addCopyButtons(); 142 // Apply syntax highlighting 143 hljs.highlightAll(); 144 }); 145 146 function initTheme() { 147 const saved = localStorage.getItem('theme'); 148 if (saved === 'light') { 149 document.body.classList.add('light-mode'); 150 document.getElementById('themeToggleIcon').textContent = '☀'; 151 document.getElementById('themeToggleText').textContent = 'Light Mode'; 152 } 153 } 154 155 function bindButtons() { 156 document.getElementById('toggleSidebarBtn') 157 .addEventListener('click', toggleSidebar); 158 159 document.querySelector('.btn-newchat') 160 .addEventListener('click', clearChat); 161 162 document.getElementById('historyBtn') 163 .addEventListener('click', showChatHistory); 164 165 document.getElementById('closeHistoryBtn') 166 .addEventListener('click', () => $('#chatHistoryModal').modal('hide')); 167 document.getElementById('closeHistoryFooterBtn') 168 .addEventListener('click', () => $('#chatHistoryModal').modal('hide')); 169 170 document.getElementById('themeToggle') 171 .addEventListener('click', () => { 172 document.body.classList.toggle('light-mode'); 173 const isLight = document.body.classList.contains('light-mode'); 174 localStorage.setItem('theme', isLight ? 'light' : 'dark'); 175 document.getElementById('themeToggleIcon').textContent = isLight ? '☀' : '☾'; 176 document.getElementById('themeToggleText').textContent = isLight ? 'Light Mode' : 'Dark Mode'; 177 }); 178 179 document.getElementById('sendBtn') 180 .addEventListener('click', sendMessage); 181 182 document.getElementById('clearBtn') 183 .addEventListener('click', clearChat); 184 185 // Voice modal buttons 186 document.getElementById('micButton') 187 .addEventListener('click', startRecording); 188 document.getElementById('stopRecBtn') 189 .addEventListener('click', stopRecording); 190 document.getElementById('finishRecBtn') 191 .addEventListener('click', finishRecording); 192 193 // Enter to send 194 document.getElementById('message') 195 .addEventListener('keydown', e => { 196 if (e.key === 'Enter' && !e.shiftKey) { 197 e.preventDefault(); 198 sendMessage(); 199 } 200 }); 201 } 202 203 function toggleSidebar() { 204 document.getElementById('sidebar').classList.toggle('collapsed'); 205 document.getElementById('mainContent').classList.toggle('expanded'); 206 } 207 208 function checkIfEmpty() { 209 const has = chatHistory.some(m => m.role !== 'system'); 210 document.getElementById('emptyState').style.display = has ? 'none' : 'block'; 211 } 212 213 function updateChat(history) { 214 chatHistory = history; 215 const chatEl = document.getElementById('chat'); 216 chatEl.innerHTML = ''; 217 let has = false; 218 history.forEach(msg => { 219 // skip internal tool call entries 220 if (msg.role === 'system' || msg.role === 'tool') return; 221 has = true; 222 const wrapper = document.createElement('div'); 223 wrapper.className = 'message ' + 224 (msg.role === 'assistant' ? 'assistant-msg animate__fadeIn' : 'user-msg animate__fadeIn'); 225 wrapper.innerHTML = `<strong>${msg.role==='assistant'?'Assistant':'User'}:</strong> 226 <div class="msg-content">${marked.parse(msg.content)}</div>`; 227 chatEl.appendChild(wrapper); 228 }); 229 document.getElementById('emptyState').style.display = has ? 'none' : 'block'; 230 chatEl.scrollTop = chatEl.scrollHeight; 231 // re-inject copy buttons for all new <pre> blocks 232 addCopyButtons(); 233 // re-apply syntax highlighting via Highlight.js 234 hljs.highlightAll(); 235 } 236 237 function newSession() { 238 chatHistory = []; 239 updateChat([]); 240 clearChat(); 241 } 242 243 function clearChat() { 244 $.post('/clear', {}, res => updateChat(res.chat_history)); 245 } 246 247 function showChatHistory() { 248 let html = ''; 249 chatHistory.forEach(m => { 250 if (m.role !== 'system') { 251 html += `<p><strong>${m.role==='assistant'?'Assistant':'User'}:</strong> ${m.content}</p>`; 252 } 253 }); 254 document.getElementById('chatHistoryContent').innerHTML = html; 255 $('#chatHistoryModal').modal('show'); 256 } 257 258 // Voice recording 259 let recognition, recorded = ''; 260 const SpeechRec = window.SpeechRecognition || window.webkitSpeechRecognition; 261 if (SpeechRec) { 262 recognition = new SpeechRec(); 263 recognition.interimResults = true; 264 recognition.onresult = e => { 265 recorded = ''; 266 for (let i=e.resultIndex; i<e.results.length; i++) { 267 recorded += e.results[i][0].transcript; 268 } 269 document.getElementById('modalTranscript').textContent = recorded; 270 }; 271 } else { 272 document.getElementById('micButton').disabled = true; 273 } 274 275 function startRecording() { 276 recorded = ''; 277 document.getElementById('modalTranscript').textContent = 'Please speak now.'; 278 $('#recordingModal').modal('show'); 279 recognition && recognition.start(); 280 } 281 function stopRecording() { 282 recognition && recognition.stop(); 283 $('#recordingModal').modal('hide'); 284 } 285 function finishRecording() { 286 recognition && recognition.stop(); 287 setTimeout(() => { 288 document.getElementById('message').value = recorded; 289 $('#recordingModal').modal('hide'); 290 sendMessage(); 291 }, 300); 292 } 293 294 // Copy button injector 295 function addCopyButtons() { 296 document.querySelectorAll("pre").forEach(pre => { 297 // Skip if already added 298 if (pre.querySelector(".copy-btn")) return; 299 300 const btn = document.createElement("button"); 301 btn.className = "copy-btn"; 302 btn.textContent = "📋 Copy"; 303 btn.addEventListener("click", () => { 304 const codeText = pre.innerText; 305 navigator.clipboard.writeText(codeText).then(() => { 306 btn.textContent = "✅ Copied!"; 307 setTimeout(() => btn.textContent = "📋 Copy", 1500); 308 }); 309 }); 310 311 pre.appendChild(btn); 312 }); 313 } 314 315 // Send message 316 function sendMessage() { 317 const inp = document.getElementById('message'); 318 const txt = inp.value.trim(); 319 if (!txt) return; 320 chatHistory.push({ role: 'user', content: txt }); 321 updateChat(chatHistory); 322 inp.value = ''; 323 324 const chatEl = document.getElementById('chat'); 325 const indicator = document.createElement('div'); 326 indicator.className = 'message assistant-msg animate__pulse typing-indicator'; 327 indicator.innerHTML = `<strong>Assistant:</strong> 328 <div class="msg-content"><em>Agent is typing...</em></div>`; 329 chatEl.appendChild(indicator); 330 chatEl.scrollTop = chatEl.scrollHeight; 331 // re-inject copy buttons for all new <pre> blocks 332 addCopyButtons(); 333 // re-apply syntax highlighting via Highlight.js 334 hljs.highlightAll(); 335 336 $.post('/chat', { message: txt }) 337 .done(res => updateChat(res.chat_history)) 338 .fail(err => { 339 alert('Error: ' + err.statusText); 340 chatHistory.pop(); 341 updateChat(chatHistory); 342 }); 343 } 344 </script> 345 </body> 346 </html> 347