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" /> 6 <title>NovaChat</title> 7 <!-- Bootstrap CSS --> 8 <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" /> 9 <!-- Animate.css --> 10 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css" /> 11 <!-- Highlight.js CSS --> 12 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.2.0/styles/atom-one-dark.min.css" /> 13 14 <style> 15 :root { 16 --bg-color: #1a1a1a; 17 --sidebar-bg: #2d2d2d; 18 --sidebar-border: #444; 19 --text-color: #7ee787; 20 --text-muted: #9e9e9e; 21 --content-bg: #1a1a1a; 22 --topbar-bg: #212121; 23 --link-hover: #fff; 24 --assistant-msg-bg: #262b26; 25 --assistant-msg-color: #9ef1a0; 26 --user-msg-bg: #15354f; 27 --user-msg-color: #a0f1f1; 28 --btn-newchat-bg: #0d6efd; 29 } 30 body.light-mode { /* light overrides omitted for brevity */ } 31 32 html, body { 33 margin: 0; padding: 0; height: 100%; font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; 34 background-color: var(--bg-color); color: var(--text-color); 35 transition: background-color 0.3s, color 0.3s; 36 } 37 38 /* Sidebar */ 39 #sidebar { 40 position: fixed; left: 0; top: 0; 41 width: 280px; height: 100%; background-color: var(--sidebar-bg); 42 border-right: 1px solid var(--sidebar-border); padding: 15px; 43 display: flex; flex-direction: column; 44 transition: transform 0.3s ease; 45 transform: translateX(0); 46 z-index: 1000; 47 } 48 #sidebar.collapsed { transform: translateX(-100%); } 49 50 /* Main Content */ 51 #mainContent { 52 margin-left: 280px; height: 100vh; display: flex; flex-direction: column; 53 transition: margin-left 0.3s ease; 54 } 55 #mainContent.expanded { margin-left: 0; } 56 57 .topbar { 58 background-color: var(--topbar-bg); padding: 10px; 59 border-bottom: 1px solid var(--sidebar-border); 60 display: flex; align-items: center; justify-content: space-between; 61 } 62 63 .toggle-btn { 64 background: none; border: none; color: var(--text-color); 65 font-size: 1.5rem; cursor: pointer; 66 } 67 68 /* Chat & Input */ 69 .chat-container { flex: 1; padding: 10px; overflow-y: auto; background-color: var(--content-bg); } 70 .message-area { width: 100%; max-width: 800px; margin: 0 auto; } 71 .input-area { background-color: var(--topbar-bg); padding: 8px; border-top: 1px solid var(--sidebar-border); display: flex; flex-wrap: wrap; } 72 #message { flex: 1 1 100%; margin-bottom: 8px; } 73 .input-area button { margin-left: 0.5rem; margin-bottom: 8px; } 74 75 /* Media Queries */ 76 @media (max-width: 767px) { 77 #sidebar { width: 100%; transform: translateX(-100%); height: auto; max-height: 80vh; } 78 #sidebar.show { transform: translateX(0); } 79 #mainContent, #mainContent.expanded { margin-left: 0; } 80 .toggle-btn { display: block; } 81 .message-area { max-width: 100%; padding: 0 5px; } 82 .chat-container { padding: 5px; } 83 .input-area { flex-direction: column; } 84 .input-area button { margin-left: 0; } 85 } 86 </style> 87 </head> 88 <body> 89 <!-- Sidebar --> 90 <div id="sidebar"> 91 <div class="sidebar-header"> 92 <h4>NovaChat</h4> 93 </div> 94 <button class="btn-newchat mb-2" onclick="newChat()">+ New Chat</button> 95 <ul class="sidebar-menu list-unstyled flex-grow-1"> 96 <li><a onclick="showChatHistory()">Chat History</a></li> 97 </ul> 98 <div class="sidebar-footer mt-auto pt-2 border-top"> 99 <ul class="list-unstyled"> 100 <li><a href="#">Settings</a></li> 101 <li><a href="#">About</a></li> 102 </ul> 103 </div> 104 </div> 105 106 <div id="mainContent"> 107 <div class="topbar"> 108 <button class="toggle-btn" id="toggleSidebarBtn">☰</button> 109 <h5 class="mb-0">NovaChat</h5> 110 <button id="themeToggle" class="theme-toggle-btn">☾ Dark Mode</button> 111 </div> 112 113 <div class="chat-container"> 114 <div id="emptyState" class="text-center text-muted" style="display:none;"> 115 <h1>What's your next big idea?</h1> 116 </div> 117 <div class="message-area" id="chat"> <!-- messages --> </div> 118 </div> 119 120 <div class="input-area"> 121 <textarea id="message" class="form-control" rows="3" placeholder="Type a message..."></textarea> 122 <div class="d-flex"> 123 <button class="btn btn-mic mr-1" id="micButton">🎤</button> 124 <button class="btn btn-send mr-1" onclick="sendMessage()">Send</button> 125 <button class="btn btn-clear" onclick="clearChat()">Clear</button> 126 </div> 127 </div> 128 </div> 129 130 <!-- Modal for Voice Recording --> 131 <div class="modal fade" id="recordingModal" tabindex="-1" role="dialog" aria-labelledby="recordingModalLabel" aria-hidden="true"> 132 <div class="modal-dialog modal-dialog-centered" role="document"> 133 <div class="modal-content"> 134 <div class="modal-header"> 135 <h5 class="modal-title" id="recordingModalLabel">Recording...</h5> 136 <button type="button" class="close" data-dismiss="modal" aria-label="Close" onclick="stopRecording()"> 137 <span aria-hidden="true">×</span> 138 </button> 139 </div> 140 <div class="modal-body text-center"> 141 <p id="modalTranscript">Please speak now.</p> 142 <div class="spinner-border text-light" role="status"> 143 <span class="sr-only">Recording...</span> 144 </div> 145 </div> 146 <div class="modal-footer"> 147 <button type="button" class="btn btn-primary" onclick="finishRecording()">Send</button> 148 </div> 149 </div> 150 </div> 151 </div> 152 153 <!-- Modal for Chat History --> 154 <div class="modal fade" id="chatHistoryModal" tabindex="-1" role="dialog" aria-labelledby="chatHistoryModalLabel" aria-hidden="true"> 155 <div class="modal-dialog modal-dialog-centered modal-lg" role="document"> 156 <div class="modal-content"> 157 <div class="modal-header"> 158 <h5 class="modal-title" id="chatHistoryModalLabel">Chat History</h5> 159 <button type="button" class="close" data-dismiss="modal" aria-label="Close" onclick="closeChatHistory()"> 160 <span aria-hidden="true">×</span> 161 </button> 162 </div> 163 <div class="modal-body"> 164 <div id="chatHistoryContent"></div> 165 </div> 166 <div class="modal-footer"> 167 <button type="button" class="btn btn-secondary" data-dismiss="modal" onclick="closeChatHistory()">Close</button> 168 </div> 169 </div> 170 </div> 171 </div> 172 173 <!-- jQuery & Bootstrap JS --> 174 <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> 175 <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script> 176 <!-- Marked.js for Markdown parsing --> 177 <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> 178 <!-- Highlight.js for code syntax highlighting --> 179 <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.2.0/highlight.min.js"></script> 180 181 <script> 182 // Global chat history from server 183 var chatHistory = {{ chat_history|tojson }}; 184 185 // Initialize theme from localStorage 186 function initTheme() { 187 const savedTheme = localStorage.getItem("theme"); 188 if (savedTheme === "light") { 189 document.body.classList.add("light-mode"); 190 document.getElementById("themeToggleIcon").textContent = "☀"; 191 document.getElementById("themeToggleText").textContent = "Light Mode"; 192 } else { 193 document.body.classList.remove("light-mode"); 194 document.getElementById("themeToggleIcon").textContent = "☾"; 195 document.getElementById("themeToggleText").textContent = "Dark Mode"; 196 } 197 } 198 document.addEventListener("DOMContentLoaded", function() { 199 initTheme(); 200 checkIfEmpty(); 201 }); 202 203 function checkIfEmpty() { 204 const hasNonSystem = chatHistory.some(msg => msg.role !== "system"); 205 document.getElementById("emptyState").style.display = hasNonSystem ? "none" : "block"; 206 } 207 208 // Update chat window 209 function updateChat(history) { 210 chatHistory = history; 211 const chatContainer = document.getElementById("chat"); 212 chatContainer.innerHTML = ""; 213 214 let hasNonSystemMsg = false; 215 chatHistory.forEach(function(msg) { 216 if (msg.role === "system") return; // Hide system prompts 217 hasNonSystemMsg = true; 218 const rendered = marked.parse(msg.content); 219 let msgClass = "", label = ""; 220 if (msg.role === "assistant") { 221 msgClass = "assistant-msg animate__animated animate__fadeIn"; 222 label = "Assistant"; 223 } else if (msg.role === "user") { 224 msgClass = "user-msg animate__animated animate__fadeIn"; 225 label = "User"; 226 } 227 const wrapper = document.createElement("div"); 228 wrapper.className = "message " + msgClass; 229 wrapper.innerHTML = "<strong>" + label + ":</strong><div class='msg-content'>" + rendered + "</div>"; 230 chatContainer.appendChild(wrapper); 231 }); 232 document.getElementById("emptyState").style.display = hasNonSystemMsg ? "none" : "block"; 233 chatContainer.scrollTop = chatContainer.scrollHeight; 234 } 235 236 // Sidebar toggle using topbar button 237 document.getElementById("toggleSidebarBtn").addEventListener("click", function() { 238 const sidebar = document.getElementById("sidebar"); 239 const mainContent = document.getElementById("mainContent"); 240 if (sidebar.classList.contains("collapsed")) { 241 sidebar.classList.remove("collapsed"); 242 mainContent.classList.remove("expanded"); 243 } else { 244 sidebar.classList.add("collapsed"); 245 mainContent.classList.add("expanded"); 246 } 247 }); 248 249 // New Chat: clear conversation 250 function newChat() { 251 console.log("Clearing chat (new chat)..."); 252 $.post("/clear", {}, function(data) { 253 updateChat(data.chat_history); 254 }); 255 } 256 257 // Show Chat History modal 258 function showChatHistory() { 259 let historyContent = ""; 260 chatHistory.forEach(function(msg) { 261 if (msg.role === "system") return; 262 historyContent += "<p><strong>" + (msg.role === "assistant" ? "Assistant" : "User") + ":</strong> " + msg.content + "</p>"; 263 }); 264 document.getElementById("chatHistoryContent").innerHTML = historyContent; 265 $('#chatHistoryModal').modal('show'); 266 } 267 function closeChatHistory() { 268 $('#chatHistoryModal').modal('hide'); 269 } 270 271 // Clear the chat (same /clear endpoint as newChat) 272 function clearChat() { 273 console.log("Clear button clicked..."); 274 $.post("/clear", {}, function(data) { 275 updateChat(data.chat_history); 276 }); 277 } 278 279 // Theme toggle 280 const themeToggleBtn = document.getElementById("themeToggle"); 281 themeToggleBtn.addEventListener("click", function() { 282 if (document.body.classList.contains("light-mode")) { 283 document.body.classList.remove("light-mode"); 284 document.getElementById("themeToggleIcon").textContent = "☾"; 285 document.getElementById("themeToggleText").textContent = "Dark Mode"; 286 localStorage.setItem("theme", "dark"); 287 } else { 288 document.body.classList.add("light-mode"); 289 document.getElementById("themeToggleIcon").textContent = "☀"; 290 document.getElementById("themeToggleText").textContent = "Light Mode"; 291 localStorage.setItem("theme", "light"); 292 } 293 }); 294 295 // Voice recognition with live transcription 296 var recordedTranscript = ""; 297 var recognition; 298 var SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; 299 if (SpeechRecognition) { 300 recognition = new SpeechRecognition(); 301 recognition.interimResults = true; 302 recognition.maxAlternatives = 1; 303 recognition.onresult = function(event) { 304 let transcript = ""; 305 for (let i = event.resultIndex; i < event.results.length; i++) { 306 transcript += event.results[i][0].transcript; 307 } 308 recordedTranscript = transcript; 309 document.getElementById("modalTranscript").textContent = transcript; 310 }; 311 recognition.onerror = function(event) { 312 console.error("Speech recognition error", event.error); 313 }; 314 } else { 315 document.getElementById("micButton").disabled = true; 316 document.getElementById("micButton").title = "Speech recognition not supported in this browser."; 317 } 318 319 // Recording Modal 320 document.getElementById("micButton").addEventListener("click", function() { 321 showRecordingModal(); 322 }); 323 function showRecordingModal() { 324 recordedTranscript = ""; 325 document.getElementById("modalTranscript").textContent = "Please speak now."; 326 $('#recordingModal').modal('show'); 327 if (recognition) { 328 recognition.start(); 329 } 330 } 331 function stopRecording() { 332 if (recognition) { 333 recognition.stop(); 334 } 335 $('#recordingModal').modal('hide'); 336 } 337 function finishRecording() { 338 if (recognition) { 339 recognition.stop(); 340 } 341 document.getElementById("modalTranscript").textContent = "Converting to text..."; 342 setTimeout(function() { 343 document.getElementById("message").value = recordedTranscript; 344 $('#recordingModal').modal('hide'); 345 sendMessage(); 346 }, 2000); 347 } 348 349 // Send message 350 function sendMessage() { 351 const inputEl = document.getElementById("message"); 352 const text = inputEl.value.trim(); 353 if (!text) return; 354 355 chatHistory.push({role: "user", content: text}); 356 updateChat(chatHistory); 357 358 const chatContainer = document.getElementById("chat"); 359 const typingIndicator = document.createElement("div"); 360 typingIndicator.className = "message assistant-msg animate__animated animate__pulse typing-indicator"; 361 typingIndicator.innerHTML = "<strong>Assistant:</strong><div class='msg-content'><em>Agent is typing...</em></div>"; 362 chatContainer.appendChild(typingIndicator); 363 chatContainer.scrollTop = chatContainer.scrollHeight; 364 365 inputEl.value = ""; 366 367 // Send to server 368 $.post("/chat", { message: text }, function(data) { 369 updateChat(data.chat_history); 370 }); 371 } 372 373 // Shift+Enter for newline, Enter to send 374 document.getElementById("message").addEventListener("keydown", function(e) { 375 if (e.key === "Enter" && !e.shiftKey) { 376 e.preventDefault(); 377 sendMessage(); 378 } 379 }); 380 </script> 381 </body> 382 </html>