1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8" /> 5 <title>NovaChat</title> 6 7 <!-- Bootstrap CSS --> 8 <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" /> 9 10 <!-- Animate.css --> 11 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css" /> 12 13 <!-- Highlight.js CSS --> 14 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.2.0/styles/atom-one-dark.min.css" /> 15 16 <!-- Custom styling with Dark/Light mode support --> 17 <style> 18 :root { 19 /* Dark mode variables (default) */ 20 --bg-color: #1a1a1a; 21 --sidebar-bg: #2d2d2d; 22 --sidebar-border: #444; 23 --text-color: #7ee787; 24 --text-muted: #9e9e9e; 25 --content-bg: #1a1a1a; 26 --topbar-bg: #212121; 27 --link-hover: #fff; 28 --assistant-msg-bg: #2f352f; /* slightly lighter */ 29 --assistant-msg-color: #9ef1a0; 30 --user-msg-bg: #15354f; 31 --user-msg-color: #a0f1f1; 32 --btn-newchat-bg: #0d6efd; 33 } 34 body.light-mode { 35 /* Light mode overrides */ 36 --bg-color: #f8f9fa; 37 --sidebar-bg: #e9ecef; 38 --sidebar-border: #ccc; 39 --text-color: #0d6efd; 40 --text-muted: #6c757d; 41 --content-bg: #f8f9fa; 42 --topbar-bg: #e9ecef; 43 --link-hover: #000; 44 --assistant-msg-bg: #e2ffe2; 45 --assistant-msg-color: #198754; 46 --user-msg-bg: #cce5ff; 47 --user-msg-color: #0d6efd; 48 --btn-newchat-bg: #0d6efd; 49 } 50 html, body { 51 margin: 0; 52 padding: 0; 53 height: 100%; 54 font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; 55 background-color: var(--bg-color); 56 color: var(--text-color); 57 transition: background-color 0.3s ease, color 0.3s ease; 58 } 59 /* Sidebar: using transform to slide off-screen */ 60 #sidebar { 61 position: fixed; 62 left: 0; 63 top: 0; 64 width: 340px; 65 height: 100%; 66 background-color: var(--sidebar-bg); 67 border-right: 1px solid var(--sidebar-border); 68 padding: 15px; 69 display: flex; 70 flex-direction: column; 71 transition: transform 0.3s ease; 72 transform: translateX(0); 73 overflow: hidden; 74 z-index: 999; 75 } 76 #sidebar.collapsed { 77 transform: translateX(-340px); 78 } 79 .sidebar-header { 80 margin-bottom: 20px; 81 } 82 .sidebar-header h4 { 83 margin: 0; 84 font-size: 1.3rem; 85 color: var(--text-color); 86 } 87 .sidebar-menu { 88 flex: 1; 89 display: flex; 90 flex-direction: column; 91 } 92 .btn-newchat { 93 background-color: var(--btn-newchat-bg); 94 color: #fff; 95 border: none; 96 width: 100%; 97 text-align: left; 98 padding: 10px; 99 margin-bottom: 10px; 100 cursor: pointer; 101 font-size: 1rem; 102 transition: background-color 0.2s ease; 103 } 104 .btn-newchat:hover { 105 background-color: #0b5ed7; 106 } 107 .sidebar-menu ul { 108 list-style: none; 109 padding-left: 0; 110 margin: 0; 111 } 112 .sidebar-menu ul li { 113 margin: 10px 0; 114 } 115 .sidebar-menu ul li a { 116 color: var(--text-muted); 117 text-decoration: none; 118 cursor: pointer; 119 font-size: 1rem; 120 } 121 .sidebar-menu ul li a:hover { 122 color: var(--link-hover); 123 } 124 .sidebar-footer { 125 margin-top: auto; 126 padding-top: 20px; 127 border-top: 1px solid var(--sidebar-border); 128 } 129 .sidebar-footer ul { 130 list-style: none; 131 padding-left: 0; 132 margin: 0; 133 } 134 .sidebar-footer li { 135 margin: 10px 0; 136 } 137 .sidebar-footer li a { 138 color: var(--text-muted); 139 text-decoration: none; 140 font-size: 1rem; 141 } 142 .sidebar-footer li a:hover { 143 color: var(--link-hover); 144 } 145 /* Main Content Area */ 146 #mainContent { 147 margin-left: 340px; 148 height: 100vh; 149 display: flex; 150 flex-direction: column; 151 transition: margin-left 0.3s ease; 152 } 153 #mainContent.expanded { 154 margin-left: 0; 155 } 156 .topbar { 157 background-color: var(--topbar-bg); 158 padding: 10px 20px; 159 border-bottom: 1px solid var(--sidebar-border); 160 display: flex; 161 align-items: center; 162 justify-content: space-between; 163 color: var(--text-color); 164 transition: background-color 0.3s ease, color 0.3s ease; 165 } 166 .toggle-btn { 167 background: none; 168 border: none; 169 color: var(--text-color); 170 font-size: 22px; 171 cursor: pointer; 172 margin-right: 20px; 173 } 174 .theme-toggle-btn { 175 background: none; 176 border: 1px solid var(--text-color); 177 color: var(--text-color); 178 border-radius: 20px; 179 padding: 5px 10px; 180 cursor: pointer; 181 font-size: 0.9rem; 182 display: flex; 183 align-items: center; 184 gap: 5px; 185 transition: background-color 0.3s ease, color 0.3s ease; 186 } 187 .theme-toggle-btn:hover { 188 background-color: rgba(255, 255, 255, 0.1); 189 } 190 .chat-container { 191 flex: 1; 192 padding: 15px; 193 overflow-y: auto; 194 background-color: var(--content-bg); 195 display: flex; 196 flex-direction: column; 197 align-items: center; 198 position: relative; 199 transition: background-color 0.3s ease; 200 } 201 .message-area { 202 width: 100%; 203 max-width: 1300px; 204 position: relative; 205 } 206 .message { 207 margin: 10px 0; 208 padding: 10px; 209 border-radius: 6px; 210 max-width: 70%; 211 } 212 .assistant-msg { 213 background-color: var(--assistant-msg-bg); 214 color: var(--assistant-msg-color); 215 align-self: flex-start; 216 } 217 .user-msg { 218 background-color: var(--user-msg-bg); 219 color: var(--user-msg-color); 220 margin-left: auto; 221 align-self: flex-end; 222 } 223 .system-msg { 224 display: none; 225 } 226 pre { 227 background-color: #1e1e1e; /* slightly lighter for contrast */ 228 color: #e8f2ff; /* brighter text */ 229 padding: 12px 16px; 230 border-radius: 6px; 231 font-size: 0.95rem; 232 overflow-x: auto; 233 border: 1px solid #444; 234 } 235 code { 236 color: #d0ffcc; 237 font-family: "Consolas", "Courier New", monospace; 238 font-size: 0.95rem; 239 } 240 /* Greeting block if there's no user or assistant content */ 241 #emptyState { 242 position: absolute; 243 top: 35%; 244 left: 50%; 245 transform: translate(-50%, -50%); 246 color: #bbb; 247 font-size: 2rem; 248 pointer-events: none; 249 text-align: center; 250 } 251 #emptyState h1 { 252 color: #fff; 253 margin-bottom: 1rem; 254 font-size: 2.2rem; 255 } 256 /* Input Area */ 257 .input-area { 258 background-color: var(--topbar-bg); 259 padding: 10px; 260 border-top: 1px solid var(--sidebar-border); 261 display: flex; 262 align-items: flex-start; 263 width: 100%; 264 max-width: 1300px; 265 margin: 0 auto; 266 transition: background-color 0.3s ease; 267 } 268 #message { 269 flex: 1; 270 background-color: #333; 271 color: #a8f1a8; 272 border: 1px solid #555; 273 resize: vertical; 274 min-height: 100px; 275 } 276 .btn-send { 277 background-color: #005f5f; 278 color: #fff; 279 border: none; 280 margin-left: 10px; 281 padding: 10px 15px; 282 } 283 .btn-send:hover { 284 background-color: #007f7f; 285 } 286 .btn-clear { 287 background-color: #7f0000; 288 color: #fff; 289 border: none; 290 margin-left: 10px; 291 padding: 10px 15px; 292 } 293 .btn-clear:hover { 294 background-color: #bf0000; 295 } 296 .btn-mic { 297 background-color: #4fc3f7; 298 color: #fff; 299 border: none; 300 margin-left: 10px; 301 border-radius: 50%; 302 width: 40px; 303 height: 40px; 304 } 305 .btn-mic:hover { 306 background-color: #29b6f6; 307 } 308 .typing-indicator { 309 font-style: italic; 310 color: #6aff6a; 311 } 312 /* Modal styling */ 313 .modal-content { 314 background: #212121; 315 color: #fff; 316 } 317 .copy-btn { 318 position: absolute; 319 top: 8px; 320 right: 10px; 321 background-color: #444; 322 color: #fff; 323 border: none; 324 border-radius: 4px; 325 padding: 2px 8px; 326 font-size: 0.8rem; 327 cursor: pointer; 328 z-index: 10; 329 opacity: 0.7; 330 transition: opacity 0.2s ease; 331 } 332 333 .copy-btn:hover { 334 opacity: 1; 335 } 336 337 pre { 338 position: relative; /* allows button positioning */ 339 } 340 341 </style> 342 </head> 343 <body> 344 <!-- Sidebar --> 345 <div id="sidebar"> 346 <div class="sidebar-header"> 347 <h4 id="sidebarTitle">NovaChat</h4> 348 </div> 349 <div class="sidebar-menu"> 350 <button class="btn-newchat" onclick="newChat()">+ New Chat</button> 351 <ul> 352 <li><a onclick="showChatHistory()">Chat History</a></li> 353 </ul> 354 </div> 355 <div class="sidebar-footer"> 356 <ul> 357 <li><a href="#">Settings</a></li> 358 <li><a href="#">About</a></li> 359 </ul> 360 </div> 361 </div> 362 363 <!-- Main Area --> 364 <div class="main" id="mainContent"> 365 <div class="topbar"> 366 <button class="toggle-btn" id="toggleSidebarBtn">☰</button> 367 <h5 class="mb-0" style="margin-left: 15px;">NovaChat</h5> 368 <button id="themeToggle" class="theme-toggle-btn"> 369 <span id="themeToggleIcon">☾</span> 370 <span id="themeToggleText">Dark Mode</span> 371 </button> 372 </div> 373 374 <div class="chat-container"> 375 <div id="emptyState" style="display:none;"> 376 <!-- New greeting here --> 377 <h1>What's your next big idea? I'm all ears.</h1> 378 </div> 379 <div class="message-area" id="chat"> 380 {% for msg in chat_history %} 381 {% if msg.role == "assistant" %} 382 <div class="message assistant-msg animate__animated animate__fadeIn"> 383 <strong>Assistant:</strong> 384 <div class="msg-content">{{ msg.content }}</div> 385 </div> 386 {% elif msg.role == "user" %} 387 <div class="message user-msg animate__animated animate__fadeIn"> 388 <strong>User:</strong> 389 <div class="msg-content">{{ msg.content }}</div> 390 </div> 391 {% endif %} 392 {% endfor %} 393 </div> 394 </div> 395 396 <!-- Input Area --> 397 <div class="input-area"> 398 <textarea class="form-control" id="message" rows="4" placeholder="Type your message or use the mic..."></textarea> 399 <button class="btn-mic" id="micButton" title="Record">🎤</button> 400 <button class="btn btn-send" onclick="sendMessage()">Send</button> 401 <!-- Clear button to reset chat --> 402 <button class="btn btn-clear" onclick="clearChat()">Clear</button> 403 </div> 404 </div> 405 406 <!-- Modal for Voice Recording --> 407 <div class="modal fade" id="recordingModal" tabindex="-1" role="dialog" aria-labelledby="recordingModalLabel" aria-hidden="true"> 408 <div class="modal-dialog modal-dialog-centered" role="document"> 409 <div class="modal-content"> 410 <div class="modal-header"> 411 <h5 class="modal-title" id="recordingModalLabel">Recording...</h5> 412 <button type="button" class="close" data-dismiss="modal" aria-label="Close" onclick="stopRecording()"> 413 <span aria-hidden="true">×</span> 414 </button> 415 </div> 416 <div class="modal-body text-center"> 417 <p id="modalTranscript">Please speak now.</p> 418 <div class="spinner-border text-light" role="status"> 419 <span class="sr-only">Recording...</span> 420 </div> 421 </div> 422 <div class="modal-footer"> 423 <button type="button" class="btn btn-primary" onclick="finishRecording()">Send</button> 424 </div> 425 </div> 426 </div> 427 </div> 428 429 <!-- Modal for Chat History --> 430 <div class="modal fade" id="chatHistoryModal" tabindex="-1" role="dialog" aria-labelledby="chatHistoryModalLabel" aria-hidden="true"> 431 <div class="modal-dialog modal-dialog-centered modal-lg" role="document"> 432 <div class="modal-content"> 433 <div class="modal-header"> 434 <h5 class="modal-title" id="chatHistoryModalLabel">Chat History</h5> 435 <button type="button" class="close" data-dismiss="modal" aria-label="Close" onclick="closeChatHistory()"> 436 <span aria-hidden="true">×</span> 437 </button> 438 </div> 439 <div class="modal-body"> 440 <div id="chatHistoryContent"></div> 441 </div> 442 <div class="modal-footer"> 443 <button type="button" class="btn btn-secondary" data-dismiss="modal" onclick="closeChatHistory()">Close</button> 444 </div> 445 </div> 446 </div> 447 </div> 448 449 <!-- jQuery & Bootstrap JS --> 450 <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> 451 <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script> 452 <!-- Marked.js for Markdown parsing --> 453 <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> 454 <!-- Highlight.js for code syntax highlighting --> 455 <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.2.0/highlight.min.js"></script> 456 457 <script> 458 // Global chat history from server 459 var chatHistory = {{ chat_history|tojson }}; 460 461 // Initialize theme from localStorage 462 function initTheme() { 463 const savedTheme = localStorage.getItem("theme"); 464 if (savedTheme === "light") { 465 document.body.classList.add("light-mode"); 466 document.getElementById("themeToggleIcon").textContent = "☀"; 467 document.getElementById("themeToggleText").textContent = "Light Mode"; 468 } else { 469 document.body.classList.remove("light-mode"); 470 document.getElementById("themeToggleIcon").textContent = "☾"; 471 document.getElementById("themeToggleText").textContent = "Dark Mode"; 472 } 473 } 474 document.addEventListener("DOMContentLoaded", function() { 475 initTheme(); 476 checkIfEmpty(); 477 }); 478 479 function checkIfEmpty() { 480 const hasNonSystem = chatHistory.some(msg => msg.role !== "system"); 481 document.getElementById("emptyState").style.display = hasNonSystem ? "none" : "block"; 482 } 483 484 // Update chat window 485 function updateChat(history) { 486 chatHistory = history; 487 const chatContainer = document.getElementById("chat"); 488 chatContainer.innerHTML = ""; 489 490 let hasNonSystemMsg = false; 491 chatHistory.forEach(function(msg) { 492 if (msg.role === "system") return; // Hide system prompts 493 hasNonSystemMsg = true; 494 const rendered = marked.parse(msg.content); 495 let msgClass = "", label = ""; 496 if (msg.role === "assistant") { 497 msgClass = "assistant-msg animate__animated animate__fadeIn"; 498 label = "Assistant"; 499 } else if (msg.role === "user") { 500 msgClass = "user-msg animate__animated animate__fadeIn"; 501 label = "User"; 502 } 503 const wrapper = document.createElement("div"); 504 wrapper.className = "message " + msgClass; 505 wrapper.innerHTML = "<strong>" + label + ":</strong><div class='msg-content'>" + rendered + "</div>"; 506 chatContainer.appendChild(wrapper); 507 }); 508 document.getElementById("emptyState").style.display = hasNonSystemMsg ? "none" : "block"; 509 chatContainer.scrollTop = chatContainer.scrollHeight; 510 addCopyButtons(); 511 } 512 513 // Sidebar toggle using topbar button 514 document.getElementById("toggleSidebarBtn").addEventListener("click", function() { 515 const sidebar = document.getElementById("sidebar"); 516 const mainContent = document.getElementById("mainContent"); 517 if (sidebar.classList.contains("collapsed")) { 518 sidebar.classList.remove("collapsed"); 519 mainContent.classList.remove("expanded"); 520 } else { 521 sidebar.classList.add("collapsed"); 522 mainContent.classList.add("expanded"); 523 } 524 }); 525 526 // New Chat: clear conversation 527 function newChat() { 528 console.log("Clearing chat (new chat)..."); 529 $.post("/clear", {}, function(data) { 530 updateChat(data.chat_history); 531 }); 532 } 533 534 // Show Chat History modal 535 function showChatHistory() { 536 let historyContent = ""; 537 chatHistory.forEach(function(msg) { 538 if (msg.role === "system") return; 539 historyContent += "<p><strong>" + (msg.role === "assistant" ? "Assistant" : "User") + ":</strong> " + msg.content + "</p>"; 540 }); 541 document.getElementById("chatHistoryContent").innerHTML = historyContent; 542 $('#chatHistoryModal').modal('show'); 543 } 544 function closeChatHistory() { 545 $('#chatHistoryModal').modal('hide'); 546 } 547 548 // Clear the chat (same /clear endpoint as newChat) 549 function clearChat() { 550 console.log("Clear button clicked..."); 551 $.post("/clear", {}, function(data) { 552 updateChat(data.chat_history); 553 }); 554 } 555 556 // Theme toggle 557 const themeToggleBtn = document.getElementById("themeToggle"); 558 themeToggleBtn.addEventListener("click", function() { 559 if (document.body.classList.contains("light-mode")) { 560 document.body.classList.remove("light-mode"); 561 document.getElementById("themeToggleIcon").textContent = "☾"; 562 document.getElementById("themeToggleText").textContent = "Dark Mode"; 563 localStorage.setItem("theme", "dark"); 564 } else { 565 document.body.classList.add("light-mode"); 566 document.getElementById("themeToggleIcon").textContent = "☀"; 567 document.getElementById("themeToggleText").textContent = "Light Mode"; 568 localStorage.setItem("theme", "light"); 569 } 570 }); 571 572 // Voice recognition with live transcription 573 var recordedTranscript = ""; 574 var recognition; 575 var SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; 576 if (SpeechRecognition) { 577 recognition = new SpeechRecognition(); 578 recognition.interimResults = true; 579 recognition.maxAlternatives = 1; 580 recognition.onresult = function(event) { 581 let transcript = ""; 582 for (let i = event.resultIndex; i < event.results.length; i++) { 583 transcript += event.results[i][0].transcript; 584 } 585 recordedTranscript = transcript; 586 document.getElementById("modalTranscript").textContent = transcript; 587 }; 588 recognition.onerror = function(event) { 589 console.error("Speech recognition error", event.error); 590 }; 591 } else { 592 document.getElementById("micButton").disabled = true; 593 document.getElementById("micButton").title = "Speech recognition not supported in this browser."; 594 } 595 596 // Recording Modal 597 document.getElementById("micButton").addEventListener("click", function() { 598 showRecordingModal(); 599 }); 600 function showRecordingModal() { 601 recordedTranscript = ""; 602 document.getElementById("modalTranscript").textContent = "Please speak now."; 603 $('#recordingModal').modal('show'); 604 if (recognition) { 605 recognition.start(); 606 } 607 } 608 function stopRecording() { 609 if (recognition) { 610 recognition.stop(); 611 } 612 $('#recordingModal').modal('hide'); 613 } 614 function finishRecording() { 615 if (recognition) { 616 recognition.stop(); 617 } 618 document.getElementById("modalTranscript").textContent = "Converting to text..."; 619 setTimeout(function() { 620 document.getElementById("message").value = recordedTranscript; 621 $('#recordingModal').modal('hide'); 622 sendMessage(); 623 }, 2000); 624 } 625 626 // Send message 627 function sendMessage() { 628 const inputEl = document.getElementById("message"); 629 const text = inputEl.value.trim(); 630 if (!text) return; 631 632 chatHistory.push({role: "user", content: text}); 633 updateChat(chatHistory); 634 635 const chatContainer = document.getElementById("chat"); 636 const typingIndicator = document.createElement("div"); 637 typingIndicator.className = "message assistant-msg animate__animated animate__pulse typing-indicator"; 638 typingIndicator.innerHTML = "<strong>Assistant:</strong><div class='msg-content'><em>Agent is typing...</em></div>"; 639 chatContainer.appendChild(typingIndicator); 640 chatContainer.scrollTop = chatContainer.scrollHeight; 641 642 inputEl.value = ""; 643 644 // Send to server 645 $.post("/chat", { message: text }, function(data) { 646 updateChat(data.chat_history); 647 }); 648 } 649 650 function addCopyButtons() { 651 document.querySelectorAll("pre").forEach((pre, index) => { 652 // Prevent duplicates 653 if (pre.querySelector(".copy-btn")) return; 654 655 const btn = document.createElement("button"); 656 btn.className = "copy-btn"; 657 btn.textContent = "📋 Copy"; 658 btn.onclick = function () { 659 const codeText = pre.innerText; 660 navigator.clipboard.writeText(codeText).then(() => { 661 btn.textContent = "✅ Copied!"; 662 setTimeout(() => (btn.textContent = "📋 Copy"), 1500); 663 }); 664 }; 665 666 pre.appendChild(btn); 667 }); 668 } 669 670 671 // Shift+Enter for newline, Enter to send 672 document.getElementById("message").addEventListener("keydown", function(e) { 673 if (e.key === "Enter" && !e.shiftKey) { 674 e.preventDefault(); 675 sendMessage(); 676 } 677 }); 678 </script> 679 </body> 680 </html>