Format: text/html
⬇ Download File (12.7 KB)
   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">&times;</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">&times;</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