<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>NeoChat AI</title>

  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" />
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/atom-one-dark.min.css" />
  <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/4.2.12/marked.min.js"></script>
</head>

<body>
  <div class="container">
    <div class="app-container">
      <div class="sidebar">
        <div class="logo">
          <i class="fas fa-robot"></i>
          <span>NeoChat</span>
        </div>
        <button id="new-chat" class="new-chat-btn">
          <i class="fas fa-plus"></i>
          <span>New Chat</span>
        </button>
        <div class="history-container">
          <h3>Chat History</h3>
          <div id="chat-history"></div>
        </div>
        <div class="settings">
          <button id="clear-history">
            <i class="fas fa-trash"></i>
            <span>Clear History</span>
          </button>
          <button id="toggle-theme">
            <i class="fas fa-moon"></i>
            <span>Dark Mode</span>
          </button>
        </div>
      </div>
      <div class="chat-container">
        <div class="chat-header">
          <div class="current-chat-title" id="current-chat-title">
            New Conversation
          </div>
          <div class="header-actions">
            <button id="regenerate-response" title="Regenerate response">
              <i class="fas fa-sync"></i>
            </button>
            <button id="stop-response" title="Stop generating" style="display: none">
              <i class="fas fa-stop"></i>
            </button>
            <button id="export-chat" title="Export conversation">
              <i class="fas fa-download"></i>
            </button>
          </div>
        </div>
        <div class="messages" id="messages">
          <div class="intro-message">
            <h1>Welcome to NeoChat AI</h1>
            <p>Ask me anything. I'm powered by deepseek-r1.</p>
            <div class="suggestion-chips">
              <button class="suggestion-chip">Tell me a story</button>
              <button class="suggestion-chip">
                Explain quantum computing
              </button>
              <button class="suggestion-chip">Write a poem</button>
              <button class="suggestion-chip">
                Help me learn JavaScript
              </button>
            </div>
          </div>
        </div>
        <div class="input-area">
          <div class="input-container">
            <button id="file-upload-button" title="Upload File">
              <i class="fas fa-paperclip"></i>
            </button>
            <input type="file" id="file-upload" style="display: none" />
            <textarea id="user-input" placeholder="Type your message here..." rows="1"></textarea>
            <button id="send-button" title="Send message">
              <i class="fas fa-paper-plane"></i>
            </button>
          </div>
          <!-- Pending file preview container -->
          <div id="pending-file-preview"></div>
          <div class="disclaimer">
            NeoChat may produce inaccurate information. Messages are stored
            locally.
          </div>
        </div>
      </div>
    </div>
  </div>

</body>

</html>
:root {
  --primary-color: #7c4dff;
  --secondary-color: #b388ff;
  --text-primary: #333;
  --text-secondary: #666;
  --background-primary: #fff;
  --background-secondary: #f5f7fb;
  --background-tertiary: #edf0f7;
  --ai-message-bg: #f0f4ff;
  --user-message-bg: #7c4dff;
  --user-message-text: #fff;
  --border-color: #e0e0e0;
  --shadow-color: rgba(0, 0, 0, 0.1);
  --animation-speed: 0.3s;
  --border-radius: 12px;
  --typing-indicator-color: var(--primary-color);
}

.dark-mode {
  --primary-color: #9d74ff;
  --secondary-color: #b388ff;
  --text-primary: #e0e0e0;
  --text-secondary: #aaaaaa;
  --background-primary: #1a1a1a;
  --background-secondary: #252525;
  --background-tertiary: #333333;
  --ai-message-bg: #2d2d3d;
  --user-message-bg: #9d74ff;
  --user-message-text: #fff;
  --border-color: #444;
  --shadow-color: rgba(0, 0, 0, 0.6);
}

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
  transition: background-color var(--animation-speed),
    color var(--animation-speed);
}

body {
  background-color: var(--background-secondary);
  color: var(--text-primary);
  height: 100vh;
  overflow: hidden;
}

.code-copy-container {
  position: absolute;
  top: 8px;
  right: 8px;
  z-index: 10;
}

.code-copy-button {
  background-color: rgba(255, 255, 255, 0.1);
  color: #ddd;
  border: none;
  border-radius: 4px;
  width: 30px;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: all 0.2s;
}

.code-copy-button:hover {
  background-color: rgba(255, 255, 255, 0.2);
  color: white;
}

.code-copy-button.copied {
  background-color: #4caf50;
  color: white;
}

.container {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
  padding: 20px;
}

.app-container {
  display: flex;
  width: 100%;
  max-width: 1200px;
  height: 85vh;
  background-color: var(--background-primary);
  border-radius: var(--border-radius);
  box-shadow: 0 8px 30px var(--shadow-color);
  overflow: hidden;
  position: relative;
}

/* Sidebar Styles */
.sidebar {
  width: 260px;
  background-color: var(--background-tertiary);
  padding: 20px;
  display: flex;
  flex-direction: column;
  border: 1px solid var(--border-color);
}

.logo {
  display: flex;
  align-items: center;
  font-size: 22px;
  font-weight: bold;
  margin-bottom: 24px;
  color: var(--primary-color);
}

.logo i {
  margin-right: 10px;
  font-size: 24px;
}

.new-chat-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: var(--primary-color);
  color: white;
  border: none;
  padding: 12px;
  border-radius: var(--border-radius);
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  margin-bottom: 20px;
  transition: background-color 0.2s, transform 0.1s;
}

.new-chat-btn:hover {
  background-color: var(--secondary-color);
  transform: translateY(-2px);
}

.new-chat-btn i {
  margin-right: 8px;
}

.history-container {
  flex: 1;
  overflow-y: auto;
}

.history-container h3 {
  font-size: 14px;
  color: var(--text-secondary);
  margin-bottom: 10px;
  padding-left: 5px;
}

.chat-history-item {
  display: flex;
  align-items: center;
  padding: 10px 12px;
  border-radius: var(--border-radius);
  margin-bottom: 5px;
  cursor: pointer;
  transition: background-color 0.2s;
  font-size: 14px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  position: relative;
  overflow: visible;
}
#chat-history span {
  white-space: nowrap;
  width: 16ch;
  overflow: hidden;
  text-overflow: ellipsis;
}

.chat-history-item:hover {
  background-color: var(--background-secondary);
}

.chat-history-item.active {
  background-color: rgba(124, 77, 255, 0.1);
  font-weight: 500;
}

.chat-history-item i {
  margin-right: 10px;
  color: var(--text-secondary);
  font-size: 14px;
}

.settings {
  margin-top: 20px;
  border-top: 1px solid var(--border-color);
  padding-top: 15px;
}

.settings button {
  display: flex;
  align-items: center;
  background: none;
  border: none;
  color: var(--text-secondary);
  padding: 10px 5px;
  width: 100%;
  text-align: left;
  cursor: pointer;
  border-radius: var(--border-radius);
  font-size: 14px;
  transition: background-color 0.2s;
}

.settings button:hover {
  background-color: var(--background-secondary);
  color: var(--text-primary);
}

.settings button i {
  margin-right: 10px;
  width: 16px;
}

/* Chat Container Styles */
.chat-container {
  flex: 1;
  display: flex;
  flex-direction: column;
  background-color: var(--background-primary);
}

.chat-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 15px 25px;
  border-bottom: 1px solid var(--border-color);
}

.current-chat-title {
  font-weight: 600;
  font-size: 16px;
}

.header-actions button {
  background: none;
  border: none;
  color: var(--text-secondary);
  cursor: pointer;
  font-size: 16px;
  padding: 5px;
  border-radius: 5px;
  transition: color 0.2s, background-color 0.2s;
}

.header-actions button:hover {
  color: var(--primary-color);
  background-color: var(--background-tertiary);
}

/* Added regenerate, stop, and export button styling */
.header-actions button#regenerate-response {
  margin-right: 5px;
}

/* Chat messages */
.messages {
  flex: 1;
  overflow-y: auto;
  padding: 20px 0;
}

.message {
  display: flex;
  margin: 25px;
  animation: fadeIn 0.3s ease-out;
}

@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.message-content {
  max-width: 80%;
  padding: 12px 16px;
  border-radius: var(--border-radius);
  font-size: 15px;
  line-height: 1.5;
}

.message.ai {
  justify-content: flex-start;
}

.message.user {
  justify-content: flex-end;
}

.message.ai .message-content {
  background-color: var(--ai-message-bg);
  color: var(--text-primary);
  border-radius: var(--border-radius);
}

.message.user .message-content {
  background-color: var(--user-message-bg);
  color: var(--user-message-text);
  border-radius: var(--border-radius);
}

.message.ai .message-content h1 {
  font-size: 22px;
  margin: 16px 0 10px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--border-color);
  color: var(--primary-color);
}

.message.ai .message-content h2 {
  font-size: 18px;
  margin: 14px 0 8px;
  color: var(--text-primary);
}

.message.ai .message-content h3 {
  font-size: 16px;
  margin: 12px 0 6px;
  color: var(--text-primary);
}

/* List styles for better readability */
.message.ai .message-content ul,
.message.ai .message-content ol {
  margin: 8px 0;
  padding-left: 25px;
}

.message.ai .message-content li {
  margin-bottom: 5px;
}

/* Block quote styling */
.message.ai .message-content blockquote {
  border-left: 4px solid var(--primary-color);
  padding: 0 0 0 15px;
  margin: 10px 0;
  color: var(--text-secondary);
}

/* Table styling */
.message.ai .message-content table {
  border-collapse: collapse;
  width: 100%;
  margin: 15px 0;
}

.message.ai .message-content th,
.message.ai .message-content td {
  border: 1px solid var(--border-color);
  padding: 8px 12px;
  text-align: left;
}

.message.ai .message-content th {
  background-color: var(--background-tertiary);
  font-weight: 600;
}

/* Links styling */
.message.ai .message-content a {
  color: var(--primary-color);
  text-decoration: none;
  transition: color 0.2s;
}

.message.ai .message-content a:hover {
  text-decoration: underline;
}

.hljs-keyword,
.hljs-selector-tag,
.hljs-addition {
  color: #c792ea;
}

.hljs-number,
.hljs-string,
.hljs-doctag,
.hljs-regexp {
  color: #89ca78;
}

.hljs-title,
.hljs-section,
.hljs-built_in,
.hljs-name {
  color: #e2b93d;
}

.hljs-variable,
.hljs-template-variable,
.hljs-selector-id,
.hljs-class .hljs-title {
  color: #7fdbca;
}

.hljs-type,
.hljs-tag {
  color: #e06c75;
}

/* Language badge for code blocks */
pre::before {
  content: attr(class);
  position: absolute;
  top: 5px;
  left: 12px;
  font-size: 12px;
  color: #aaa;
  display: none;
}

/* Horizontal rule */
.message.ai .message-content hr {
  border: none;
  height: 1px;
  background-color: var(--border-color);
  margin: 15px 0;
}

.typing-indicator {
  display: flex;
  padding: 15px 25px;
}

.typing-dot {
  width: 8px;
  height: 8px;
  margin: 0 2px;
  background-color: var(--typing-indicator-color);
  border-radius: 50%;
  opacity: 0.6;
  animation: typingAnimation 1.5s infinite;
}

.typing-dot:nth-child(2) {
  animation-delay: 0.2s;
}

.typing-dot:nth-child(3) {
  animation-delay: 0.4s;
}

@keyframes typingAnimation {
  0% {
    transform: translateY(0px);
  }
  25% {
    transform: translateY(-5px);
  }
  50% {
    transform: translateY(0px);
  }
}

.input-area {
  padding: 15px 25px 20px;
  border-top: 1px solid var(--border-color);
}

.input-container {
  display: flex;
  position: relative;
  background-color: var(--background-tertiary);
  border-radius: var(--border-radius);
  overflow: hidden;
}

/* Style for file upload button */
#file-upload-button {
  background: none;
  border: none;
  color: var(--text-secondary);
  font-size: 18px;
  padding: 15px;
  cursor: pointer;
}

#file-upload-button:hover {
  color: var(--primary-color);
}

textarea {
  flex: 1;
  border: none;
  background: none;
  padding: 15px;
  font-size: 15px;
  resize: none;
  max-height: 150px;
  color: var(--text-primary);
  outline: none;
}

textarea::placeholder {
  color: var(--text-secondary);
}

#send-button {
  background-color: var(--primary-color);
  color: white;
  border: none;
  width: 40px;
  cursor: pointer;
  font-size: 16px;
  transition: background-color 0.2s;
}

#send-button:hover {
  background-color: var(--secondary-color);
}

.disclaimer {
  font-size: 12px;
  color: var(--text-secondary);
  text-align: center;
  margin-top: 10px;
}

/* Pending file preview (for selected file before send) */
#pending-file-preview {
  display: none;
  margin: 10px 0;
}

/* Intro Message Styles */
.intro-message {
  text-align: center;
  max-width: 600px;
  margin: 40px auto;
  padding: 30px;
  background-color: var(--background-primary);
  border-radius: var(--border-radius);
  box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
  animation: fadeIn 0.5s ease-out;
}

.intro-message h1 {
  color: var(--primary-color);
  font-size: 28px;
  margin-bottom: 15px;
}

.intro-message p {
  color: var(--text-secondary);
  margin-bottom: 25px;
  font-size: 16px;
}

.suggestion-chips {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px;
}

.suggestion-chip {
  background-color: var(--background-tertiary);
  color: var(--text-primary);
  border: 1px solid var(--border-color);
  border-radius: 20px;
  padding: 8px 16px;
  font-size: 14px;
  cursor: pointer;
  transition: all 0.2s;
}

.suggestion-chip:hover {
  background-color: var(--background-secondary);
  color: var(--primary-color);
  transform: translateY(-2px);
}

/* Code block styling */
pre {
  position: relative;
  background-color: #282c34;
  border-radius: 8px;
  padding: 12px;
  padding-top: 35px;
  overflow-x: auto;
  margin: 10px 0;
  border: 1px solid #3e4451;
  color: #edf0f7;
}

code {
  font-family: "Fira Code", "Courier New", Courier, monospace;
  font-size: 14px;
}

pre code {
  white-space: pre;
  font-size: 14px;
  line-height: 1.5;
}

:not(pre) > code {
  background-color: rgba(125, 125, 125, 0.1);
  padding: 2px 4px;
  border-radius: 4px;
  color: var(--primary-color);
}

/* New CSS for chat options button and menu */
.chat-options-button {
  background: none;
  border: none;
  color: var(--text-secondary);
  cursor: pointer;
  font-size: 16px;
  padding: 5px;
  position: absolute;
  right: 10px;
  top: 50%;
  transform: translateY(-50%);
}

.chat-options-menu {
  position: absolute;
  right: 10px;
  top: 100%;
  background-color: var(--background-primary);
  border: 1px solid var(--border-color);
  border-radius: var(--border-radius);
  box-shadow: 0 4px 8px var(--shadow-color);
  z-index: 100;
}

.chat-options-item {
  padding: 8px 12px;
  cursor: pointer;
  white-space: nowrap;
}

.chat-options-item:hover {
  background-color: var(--background-secondary);
}
document.addEventListener("DOMContentLoaded", () => {
  // DOM Elements
  const messagesContainer = document.getElementById("messages");
  const userInput = document.getElementById("user-input");
  const sendButton = document.getElementById("send-button");
  const newChatButton = document.getElementById("new-chat");
  const clearHistoryButton = document.getElementById("clear-history");
  const toggleThemeButton = document.getElementById("toggle-theme");
  const chatHistoryContainer = document.getElementById("chat-history");
  const currentChatTitle = document.getElementById("current-chat-title");
  const exportChatButton = document.getElementById("export-chat");
  const regenerateResponseButton = document.getElementById(
    "regenerate-response"
  );
  const stopResponseButton = document.getElementById("stop-response");
  const suggestionChips = document.querySelectorAll(".suggestion-chip");
  const fileUploadButton = document.getElementById("file-upload-button");
  const fileUploadInput = document.getElementById("file-upload");

  // OpenRouter API Key and Configuration
  const API_KEY = "paste-your-api-key-here";

  // State variables
  let currentChatId = null;
  let isTyping = false;
  let chatHistory = JSON.parse(localStorage.getItem("chatHistory")) || {};
  let currentTheme = localStorage.getItem("theme") || "light";
  let typingSpeed = 2; // reduced for faster letter-by-letter output
  let letterTimeout = null; // separate timeout for letter typing
  let pendingFile = null; // Store selected file until send
  let stopGeneration = false; // Flag to stop the typing effect

  // Helper function to get stable rendering:
  function getStableRendering(text) {
    // Split text on triple backticks.
    let parts = text.split("```");
    if (parts.length % 2 === 1) {
      // All code blocks are closed.
      return marked.parse(text);
    } else {
      // Code block is open; force rendering as a code block by artificially closing it.
      let closedPart = parts.slice(0, parts.length - 1).join("```");
      let openPart = parts[parts.length - 1];
      return (
        marked.parse(closedPart) + marked.parse("```" + openPart + "\n```")
      );
    }
  }

  // Initialize application
  init();

  // Function to initialize the application
  function init() {
    // Apply theme
    if (currentTheme === "dark") {
      document.body.classList.add("dark-mode");
      toggleThemeButton.innerHTML =
        '<i class="fas fa-sun"></i><span>Light Mode</span>';
    }

    // Set up textarea auto-resize
    userInput.addEventListener("input", autoResizeTextarea);

    // Set up event listeners
    sendButton.addEventListener("click", handleSendMessage);
    userInput.addEventListener("keydown", (e) => {
      if (e.key === "Enter" && !e.shiftKey) {
        e.preventDefault();
        handleSendMessage();
      }
    });

    newChatButton.addEventListener("click", createNewChat);
    clearHistoryButton.addEventListener("click", clearAllHistory);
    toggleThemeButton.addEventListener("click", toggleTheme);
    exportChatButton.addEventListener("click", exportCurrentChat);
    regenerateResponseButton.addEventListener("click", regenerateLastResponse);
    stopResponseButton.addEventListener("click", () => {
      stopGeneration = true;
      clearTimeout(letterTimeout);
      stopResponseButton.style.display = "none";
      regenerateResponseButton.style.display = "inline-block";
    });

    fileUploadButton.addEventListener("click", () => {
      fileUploadInput.click();
    });
    // Store pending file and show preview on selection
    fileUploadInput.addEventListener("change", (event) => {
      const file = event.target.files[0];
      if (file) {
        pendingFile = file;
        displayPendingFilePreview(file);
      }
    });

    suggestionChips.forEach((chip) => {
      chip.addEventListener("click", () => {
        userInput.value = chip.textContent;
        handleSendMessage();
      });
    });

    // Load chat history
    updateChatHistorySidebar();

    // Create new chat if none exists
    if (Object.keys(chatHistory).length === 0) {
      createNewChat();
    } else {
      // Load most recent chat
      const mostRecentChatId = Object.keys(chatHistory).sort((a, b) => {
        return chatHistory[b].timestamp - chatHistory[a].timestamp;
      })[0];

      loadChat(mostRecentChatId);
    }

    // Global click listener to close any open options menus
    window.addEventListener("click", () => {
      document.querySelectorAll(".chat-options-menu").forEach((menu) => {
        menu.style.display = "none";
      });
    });
  }

  // NEW: Display pending file preview immediately after selection
  function displayPendingFilePreview(file) {
    const previewContainer = document.getElementById("pending-file-preview");
    const reader = new FileReader();
    reader.onload = function (e) {
      let previewHTML = "";
      if (file.type.startsWith("image/")) {
        previewHTML = `<img src="${e.target.result}" alt="${file.name}" style="max-width: 100px; max-height: 100px;"/>`;
      } else if (
        file.type.startsWith("text/") ||
        file.type === "application/json"
      ) {
        let content = e.target.result;
        if (content.length > 200) {
          content = content.substring(0, 200) + "...";
        }
        previewHTML = `<pre style="white-space: pre-wrap; font-size: 12px;">${escapeHtml(
          content
        )}</pre>`;
      } else {
        previewHTML = `<div style="font-size: 12px;">${file.name}</div>`;
      }
      previewContainer.innerHTML = previewHTML;
      previewContainer.style.display = "block";
    };

    if (file.type.startsWith("image/")) {
      reader.readAsDataURL(file);
    } else {
      reader.readAsText(file);
    }
  }

  // NEW: Process pending file when sending message
  function processPendingFile() {
    return new Promise((resolve, reject) => {
      const file = pendingFile;
      if (!file) {
        resolve();
        return;
      }
      const reader = new FileReader();
      reader.onload = function (e) {
        let previewHTML = "";
        if (file.type.startsWith("image/")) {
          previewHTML = `<img src="${e.target.result}" alt="${file.name}" style="max-width:100%;"/>`;
        } else if (
          file.type.startsWith("text/") ||
          file.type === "application/json"
        ) {
          previewHTML = `<pre style="white-space: pre-wrap;">${escapeHtml(
            e.target.result
          )}</pre>`;
        } else {
          previewHTML = `<div>Uploaded file: ${file.name}</div>`;
        }
        // Append file preview as a user message
        addFileMessageToUI("user", previewHTML);
        // Save file data in chat history
        if (!chatHistory[currentChatId]) {
          createNewChat();
        }
        chatHistory[currentChatId].messages.push({
          role: "user",
          content: previewHTML,
          file: {
            name: file.name,
            type: file.type,
            content: e.target.result
          }
        });
        // Clear pending file preview
        const previewContainer = document.getElementById(
          "pending-file-preview"
        );
        previewContainer.style.display = "none";
        previewContainer.innerHTML = "";
        pendingFile = null;
        resolve();
      };

      if (file.type.startsWith("image/")) {
        reader.readAsDataURL(file);
      } else {
        reader.readAsText(file);
      }
    });
  }

  // Helper to escape HTML (for text files)
  function escapeHtml(text) {
    var map = {
      "&": "&amp;",
      "<": "&lt;",
      ">": "&gt;",
      '"': "&quot;",
      "'": "&#039;"
    };
    return text.replace(/[&<>"']/g, function (m) {
      return map[m];
    });
  }

  // Function to add file message (with HTML preview) to UI
  function addFileMessageToUI(sender, htmlContent) {
    const messageDiv = document.createElement("div");
    messageDiv.className = `message ${sender}`;
    const messageContent = document.createElement("div");
    messageContent.className = "message-content";
    messageContent.innerHTML = htmlContent;
    messageDiv.appendChild(messageContent);
    messagesContainer.appendChild(messageDiv);
    messagesContainer.scrollTop = messagesContainer.scrollHeight;
  }

  // Function to handle sending a message
  async function handleSendMessage() {
    if (isTyping) {
      alert("Please wait until the current response is completed.");
      return;
    }
    const message = userInput.value.trim();
    // Only proceed if there is text or a pending file
    if (!message && !pendingFile) return;

    // Clear input and reset height
    userInput.value = "";
    userInput.style.height = "auto";

    // If there's text, add it as a user message
    if (message) {
      addMessageToUI("user", message);

      if (!chatHistory[currentChatId]) {
        createNewChat();
      }

      chatHistory[currentChatId].messages.push({
        role: "user",
        content: message
      });

      if (chatHistory[currentChatId].messages.length === 1) {
        const title =
          message.split(" ").slice(0, 4).join(" ") +
          (message.split(" ").length > 4 ? "..." : "");
        chatHistory[currentChatId].title = title;
        currentChatTitle.textContent = title;
        updateChatHistorySidebar();
      }
    }

    // If a file was selected, process it now
    if (pendingFile) {
      await processPendingFile();
    }

    saveChatHistory();

    try {
      showTypingIndicator();
      // Stream the AI response and update UI letter-by-letter
      const response = await getAIResponse(currentChatId);
      // The UI is updated during streaming. Save the final text.
      chatHistory[currentChatId].messages.push({
        role: "assistant",
        content: response
      });

      saveChatHistory();
    } catch (error) {
      removeTypingIndicator();
      addMessageToUIWithTypingEffect(
        "ai",
        `Sorry, I encountered an error: ${error.message}`
      );
    }
  }

  // UPDATED: Function to get AI response from OpenRouter API with streaming and letter-by-letter effect.
  // While streaming, we update the message content using getStableRendering so that if a code block is open,
  // it is rendered as a code block immediately.
  async function getAIResponse(chatId) {
    isTyping = true;
    stopGeneration = false;
    regenerateResponseButton.style.display = "none";
    stopResponseButton.style.display = "inline-block";

    try {
      const response = await fetch(
        "https://openrouter.ai/api/v1/chat/completions",
        {
          method: "POST",
          headers: {
            Authorization: `Bearer ${API_KEY}`,
            "Content-Type": "application/json"
          },
          body: JSON.stringify({
            model: MODEL,
            messages: chatHistory[chatId].messages,
            stream: true
          })
        }
      );

      if (!response.ok) {
        const errorData = await response.json();
        throw new Error(errorData.error?.message || "Failed to get response");
      }

      // Create a message container for the streaming response
      const messageDiv = document.createElement("div");
      messageDiv.className = "message ai";
      const messageContent = document.createElement("div");
      messageContent.className = "message-content";
      messageDiv.appendChild(messageContent);
      messagesContainer.appendChild(messageDiv);
      messagesContainer.scrollTop = messagesContainer.scrollHeight;

      const reader = response.body.getReader();
      const decoder = new TextDecoder();
      let buffer = "";
      let typingBuffer = "";
      let accumulatedText = "";
      let isProcessingBuffer = false;

      function processBuffer() {
        if (typingBuffer.length > 0 && !stopGeneration) {
          accumulatedText += typingBuffer[0];
          typingBuffer = typingBuffer.slice(1);
          // Update message content using the fixed getStableRendering
          messageContent.innerHTML = getStableRendering(accumulatedText);
          messagesContainer.scrollTop = messagesContainer.scrollHeight;
          setTimeout(processBuffer, typingSpeed);
        } else {
          isProcessingBuffer = false;
        }
      }

      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        const chunk = decoder.decode(value, { stream: true });
        buffer += chunk;
        const lines = buffer.split("\n");
        buffer = lines.pop();
        for (const line of lines) {
          if (line.startsWith("data: ")) {
            const jsonStr = line.slice("data: ".length).trim();
            if (jsonStr === "[DONE]") break;
            try {
              const obj = JSON.parse(jsonStr);
              if (obj.choices && obj.choices[0] && obj.choices[0].delta) {
                const delta = obj.choices[0].delta;
                const text = (delta.content || "") + (delta.reasoning || "");
                if (text) {
                  typingBuffer += text;
                  if (!isProcessingBuffer) {
                    isProcessingBuffer = true;
                    processBuffer();
                  }
                }
              }
            } catch (e) {
              // Ignore JSON parse errors
            }
          }
        }
      }

      // Wait until typingBuffer is fully processed
      while (isProcessingBuffer) {
        await new Promise((resolve) => setTimeout(resolve, typingSpeed));
      }

      isTyping = false;
      removeTypingIndicator();
      stopResponseButton.style.display = "none";
      regenerateResponseButton.style.display = "inline-block";

      // Replace the streaming message with a fully formatted one for proper code block rendering and copy buttons.
      messageDiv.remove();
      addFormattedMessageToUI("ai", accumulatedText);
      return accumulatedText;
    } catch (error) {
      isTyping = false;
      removeTypingIndicator();
      console.error("API Error:", error);
      throw error;
    }
  }

  // Function to add message to UI with typing effect (used for non-streaming messages)
  function addMessageToUIWithTypingEffect(sender, content) {
    removeTypingIndicator();

    const messageDiv = document.createElement("div");
    messageDiv.className = `message ${sender}`;

    const messageContent = document.createElement("div");
    messageContent.className = "message-content";

    if (sender === "user") {
      messageContent.textContent = content;
      messageDiv.appendChild(messageContent);
      messagesContainer.appendChild(messageDiv);
      messagesContainer.scrollTop = messagesContainer.scrollHeight;
      return;
    }

    messageDiv.appendChild(messageContent);
    messagesContainer.appendChild(messageDiv);

    const processedContent = processMarkdownContent(content);

    // When AI response starts, reset the stop flag and show the stop button
    if (sender === "ai") {
      stopGeneration = false;
      stopResponseButton.style.display = "inline-block";
    }

    startTypingEffect(messageContent, processedContent, 0);
  }

  // Function to add user message to UI immediately
  function addMessageToUI(sender, content) {
    const messageDiv = document.createElement("div");
    messageDiv.className = `message ${sender}`;

    const messageContent = document.createElement("div");
    messageContent.className = "message-content";
    messageContent.textContent = content;

    messageDiv.appendChild(messageContent);
    messagesContainer.appendChild(messageDiv);

    messagesContainer.scrollTop = messagesContainer.scrollHeight;
  }

  // Function to process markdown and identify code blocks
  function processMarkdownContent(content) {
    const segments = [];
    let currentPos = 0;
    const codeBlockRegex = /```([\w]*)\n([\s\S]*?)\n```/g;

    let match;
    while ((match = codeBlockRegex.exec(content)) !== null) {
      if (match.index > currentPos) {
        segments.push({
          type: "text",
          content: content.substring(currentPos, match.index)
        });
      }

      segments.push({
        type: "code",
        language: match[1] || "plaintext",
        content: match[2]
      });

      currentPos = match.index + match[0].length;
    }

    if (currentPos < content.length) {
      segments.push({
        type: "text",
        content: content.substring(currentPos)
      });
    }

    return segments;
  }

  // Function to start typing effect (used by non-streaming messages)
  function startTypingEffect(messageContent, segments, segmentIndex) {
    if (segmentIndex >= segments.length) {
      isTyping = false;
      stopResponseButton.style.display = "none";
      return;
    }

    const segment = segments[segmentIndex];

    if (segment.type === "code") {
      const preElement = document.createElement("pre");
      const codeElement = document.createElement("code");

      if (segment.language) {
        codeElement.className = `language-${segment.language}`;
      }
      codeElement.classList.add("hljs");

      const copyButtonContainer = document.createElement("div");
      copyButtonContainer.className = "code-copy-container";

      const copyButton = document.createElement("button");
      copyButton.className = "code-copy-button";
      copyButton.innerHTML = '<i class="fas fa-copy"></i>';
      copyButton.title = "Copy code";

      copyButton.addEventListener("click", () => {
        navigator.clipboard.writeText(segment.content).then(() => {
          copyButton.innerHTML = '<i class="fas fa-check"></i>';
          copyButton.classList.add("copied");
          setTimeout(() => {
            copyButton.innerHTML = '<i class="fas fa-copy"></i>';
            copyButton.classList.remove("copied");
          }, 2000);
        });
      });

      copyButtonContainer.appendChild(copyButton);
      preElement.appendChild(copyButtonContainer);

      preElement.appendChild(codeElement);
      messageContent.appendChild(preElement);

      typeCodeContent(codeElement, segment.content, 0, () => {
        hljs.highlightElement(codeElement);
        startTypingEffect(messageContent, segments, segmentIndex + 1);
      });
    } else {
      const textDiv = document.createElement("div");
      messageContent.appendChild(textDiv);
      typeTextContent(textDiv, segment.content, 0, () => {
        startTypingEffect(messageContent, segments, segmentIndex + 1);
      });
    }
  }

  // Function to type code content letter by letter
  function typeCodeContent(element, content, index, callback) {
    if (stopGeneration) {
      callback();
      return;
    }
    if (index < content.length) {
      element.textContent += content[index];
      messagesContainer.scrollTop = messagesContainer.scrollHeight;
      letterTimeout = setTimeout(() => {
        typeCodeContent(element, content, index + 1, callback);
      }, typingSpeed);
    } else {
      callback();
    }
  }

  // Function to type text content letter by letter
  function typeTextContent(element, content, index, callback) {
    if (stopGeneration) {
      callback();
      return;
    }
    if (index < content.length) {
      let currentText = content.substring(0, index + 1);
      element.innerHTML = marked.parse(currentText);
      messagesContainer.scrollTop = messagesContainer.scrollHeight;
      letterTimeout = setTimeout(() => {
        typeTextContent(element, content, index + 1, callback);
      }, typingSpeed);
    } else {
      callback();
    }
  }

  // Function to show typing indicator
  function showTypingIndicator() {
    const typingDiv = document.createElement("div");
    typingDiv.className = "typing-indicator";
    typingDiv.id = "typing-indicator";

    for (let i = 0; i < 3; i++) {
      const dot = document.createElement("div");
      dot.className = "typing-dot";
      typingDiv.appendChild(dot);
    }

    messagesContainer.appendChild(typingDiv);
    messagesContainer.scrollTop = messagesContainer.scrollHeight;
  }

  // Function to remove typing indicator
  function removeTypingIndicator() {
    const typingIndicator = document.getElementById("typing-indicator");
    if (typingIndicator) {
      typingIndicator.remove();
    }
  }

  // Function to create a new chat
  function createNewChat() {
    const chatId = "chat_" + Date.now();

    chatHistory[chatId] = {
      id: chatId,
      title: "New Conversation",
      timestamp: Date.now(),
      messages: []
    };

    currentChatId = chatId;
    currentChatTitle.textContent = "New Conversation";

    messagesContainer.innerHTML = `
            <div class="intro-message">
                <h1>Welcome to NeoChat AI</h1>
                <p>Ask me anything. I'm powered by deepseek-r1.</p>
                <div class="suggestion-chips">
                    <button class="suggestion-chip">Tell me a story</button>
                    <button class="suggestion-chip">Explain quantum computing</button>
                    <button class="suggestion-chip">Write a poem</button>
                    <button class="suggestion-chip">Help me learn JavaScript</button>
                </div>
            </div>
          `;

    document.querySelectorAll(".suggestion-chip").forEach((chip) => {
      chip.addEventListener("click", () => {
        userInput.value = chip.textContent;
        handleSendMessage();
      });
    });

    saveChatHistory();
    updateChatHistorySidebar();
    // Clear pending file and its preview when a new chat is created
    pendingFile = null;
    const previewContainer = document.getElementById("pending-file-preview");
    if (previewContainer) {
      previewContainer.innerHTML = "";
      previewContainer.style.display = "none";
    }
  }

  // Function to load a chat
  function loadChat(chatId) {
    if (!chatHistory[chatId]) return;

    currentChatId = chatId;
    currentChatTitle.textContent = chatHistory[chatId].title;

    messagesContainer.innerHTML = "";

    chatHistory[chatId].messages.forEach((message) => {
      // If the user message has a file property, render using innerHTML
      if (message.role === "user") {
        if (message.file) {
          addFileMessageToUI("user", message.content);
        } else {
          addMessageToUI("user", message.content);
        }
      } else {
        addFormattedMessageToUI("ai", message.content);
      }
    });

    updateActiveChatInSidebar();
  }

  // Function to add formatted message to UI without typing effect
  function addFormattedMessageToUI(sender, content) {
    const messageDiv = document.createElement("div");
    messageDiv.className = `message ${sender}`;

    const messageContent = document.createElement("div");
    messageContent.className = "message-content";

    const processedContent = processMarkdownContent(content);

    processedContent.forEach((segment) => {
      if (segment.type === "code") {
        const preElement = document.createElement("pre");
        const codeElement = document.createElement("code");

        if (segment.language) {
          codeElement.className = `language-${segment.language}`;
        }
        codeElement.classList.add("hljs");

        const copyButtonContainer = document.createElement("div");
        copyButtonContainer.className = "code-copy-container";

        const copyButton = document.createElement("button");
        copyButton.className = "code-copy-button";
        copyButton.innerHTML = '<i class="fas fa-copy"></i>';
        copyButton.title = "Copy code";

        copyButton.addEventListener("click", () => {
          navigator.clipboard.writeText(segment.content).then(() => {
            copyButton.innerHTML = '<i class="fas fa-check"></i>';
            copyButton.classList.add("copied");
            setTimeout(() => {
              copyButton.innerHTML = '<i class="fas fa-copy"></i>';
              copyButton.classList.remove("copied");
            }, 2000);
          });
        });

        copyButtonContainer.appendChild(copyButton);
        preElement.appendChild(copyButtonContainer);

        codeElement.textContent = segment.content;
        preElement.appendChild(codeElement);
        messageContent.appendChild(preElement);

        hljs.highlightElement(codeElement);
      } else {
        const textDiv = document.createElement("div");
        textDiv.innerHTML = marked.parse(segment.content);

        while (textDiv.firstChild) {
          messageContent.appendChild(textDiv.firstChild);
        }
      }
    });

    messageDiv.appendChild(messageContent);
    messagesContainer.appendChild(messageDiv);

    messagesContainer.scrollTop = messagesContainer.scrollHeight;
  }

  // Function to save chat history to localStorage
  function saveChatHistory() {
    localStorage.setItem("chatHistory", JSON.stringify(chatHistory));
    updateChatHistorySidebar();
  }

  // Function to update chat history sidebar with options (delete/rename)
  function updateChatHistorySidebar() {
    chatHistoryContainer.innerHTML = "";

    const sortedChatIds = Object.keys(chatHistory).sort((a, b) => {
      return chatHistory[b].timestamp - chatHistory[a].timestamp;
    });

    sortedChatIds.forEach((chatId) => {
      const chat = chatHistory[chatId];
      const chatItem = document.createElement("div");
      chatItem.className = `chat-history-item ${
        chatId === currentChatId ? "active" : ""
      }`;
      chatItem.dataset.chatId = chatId;

      // Create icon element
      const icon = document.createElement("i");
      icon.className = "fas fa-comment";
      // Create span for chat title using textContent for safety
      const titleSpan = document.createElement("span");
      titleSpan.textContent = chat.title;

      chatItem.appendChild(icon);
      chatItem.appendChild(titleSpan);

      // When clicking the chat item (outside the options button) load the chat
      chatItem.addEventListener("click", () => {
        loadChat(chatId);
      });

      // Create the options button (three dots)
      const optionsButton = document.createElement("button");
      optionsButton.className = "chat-options-button";
      optionsButton.innerHTML = '<i class="fas fa-ellipsis-v"></i>';
      // Stop propagation so clicking this does not trigger the parent click event
      optionsButton.addEventListener("click", (e) => {
        e.stopPropagation();
        // Toggle the options menu
        optionsMenu.style.display =
          optionsMenu.style.display === "none" ? "block" : "none";
      });

      // Create the options menu (hidden by default)
      const optionsMenu = document.createElement("div");
      optionsMenu.className = "chat-options-menu";
      optionsMenu.style.display = "none";
      optionsMenu.innerHTML = `
              <div class="chat-options-item delete-chat">Delete</div>
              <div class="chat-options-item rename-chat">Rename</div>
            `;

      // Handle deletion of a chat
      optionsMenu
        .querySelector(".delete-chat")
        .addEventListener("click", (e) => {
          e.stopPropagation();
          if (confirm("Are you sure you want to delete this chat?")) {
            delete chatHistory[chatId];
            if (currentChatId === chatId) {
              createNewChat();
            }
            saveChatHistory();
            updateChatHistorySidebar();
          }
        });

      // Handle renaming a chat
      optionsMenu
        .querySelector(".rename-chat")
        .addEventListener("click", (e) => {
          e.stopPropagation();
          const newName = prompt("Enter new name for this chat:", chat.title);
          if (newName) {
            chat.title = newName;
            saveChatHistory();
            updateChatHistorySidebar();
            if (currentChatId === chatId) {
              currentChatTitle.textContent = newName;
            }
          }
        });

      // Append the options button and menu to the chat item
      chatItem.appendChild(optionsButton);
      chatItem.appendChild(optionsMenu);

      // Append the chat item to the history container
      chatHistoryContainer.appendChild(chatItem);
    });
  }

  // Function to update active chat in sidebar
  function updateActiveChatInSidebar() {
    document.querySelectorAll(".chat-history-item").forEach((item) => {
      item.classList.remove("active");
      if (item.dataset.chatId === currentChatId) {
        item.classList.add("active");
      }
    });
  }

  // Function to clear all chat history
  function clearAllHistory() {
    if (
      confirm(
        "Are you sure you want to clear all chat history? This cannot be undone."
      )
    ) {
      chatHistory = {};
      localStorage.removeItem("chatHistory");
      createNewChat();
    }
  }

  // Function to toggle theme
  function toggleTheme() {
    if (currentTheme === "light") {
      document.body.classList.add("dark-mode");
      currentTheme = "dark";
      toggleThemeButton.innerHTML =
        '<i class="fas fa-sun"></i><span>Light Mode</span>';
    } else {
      document.body.classList.remove("dark-mode");
      currentTheme = "light";
      toggleThemeButton.innerHTML =
        '<i class="fas fa-moon"></i><span>Dark Mode</span>';
    }

    localStorage.setItem("theme", currentTheme);
  }

  // Function to export current chat
  function exportCurrentChat() {
    if (!chatHistory[currentChatId]) return;

    const chat = chatHistory[currentChatId];
    let exportText = `# ${chat.title}\n\n`;

    chat.messages.forEach((message) => {
      const role = message.role === "user" ? "You" : "NeoChat AI";
      exportText += `## ${role}:\n${message.content}\n\n`;
    });

    const blob = new Blob([exportText], { type: "text/markdown" });
    const url = URL.createObjectURL(blob);

    const a = document.createElement("a");
    a.href = url;
    a.download = `${chat.title.replace(/[^\w\s]/gi, "")}.md`;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
  }

  // Function to regenerate the last AI response
  function regenerateLastResponse() {
    if (
      chatHistory[currentChatId] &&
      chatHistory[currentChatId].messages.length > 0
    ) {
      const lastMessage =
        chatHistory[currentChatId].messages[
          chatHistory[currentChatId].messages.length - 1
        ];
      if (lastMessage.role === "assistant") {
        chatHistory[currentChatId].messages.pop();
        const messageElements = document.querySelectorAll(".message.ai");
        if (messageElements.length > 0) {
          messageElements[messageElements.length - 1].remove();
        }
        saveChatHistory();
        showTypingIndicator();
        getAIResponse(currentChatId)
          .then((response) => {
            chatHistory[currentChatId].messages.push({
              role: "assistant",
              content: response
            });
            saveChatHistory();
          })
          .catch((error) => {
            removeTypingIndicator();
            addMessageToUIWithTypingEffect(
              "ai",
              `Sorry, I encountered an error: ${error.message}`
            );
          });
      }
    }
  }

  // Function to auto-resize textarea
  function autoResizeTextarea() {
    userInput.style.height = "auto";
    userInput.style.height = userInput.scrollHeight + "px";
  }
});

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.