Format: text/html
⬇ Download File (21.1 KB)
   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">&times;</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">&times;</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 = JSON.parse('{{ chat_history|tojson | safe }}');
 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>