I'm making an AI assistant interface website, and I've noticed an issue with my code attachment feature. If the code I paste into the attachment has characters that are the same as those used in Markdown or HTML (eg. <code> or backticks) then the code will spill out of the bubble. How can I fix this WITHOUT sending escaped characters to the AI or showing it to the user. Bear in mind, I plan on introducing an extended bubble system at some point incorporating more than just code bubbles: instructions, errors, documentation and even custom ones, as all these bubbles are meant to do is encase some text in tags for the AI to understand, such as <documentation></documentation> (though some bubbles like code do have additional information like file name). If it makes sense, perhaps it would be good to introduce that now?

<code>
// src/components/ChatInterface.js

import React, {
  useState,
  useContext,
  useRef,
  useEffect,
  useCallback,
} from "react";
import { ChatContext } from "../context/ChatContext";
import Message from "./Message";
import CodeModal from "./CodeModal";
import InlineCodeEditor from "./InlineCodeEditor";
import { escapeHtml } from "../utils/escapeHtml";
import { unescapeHtml } from "../utils/unescapeHtml"; // Import unescapeHtml

/**
 * Component representing the chat interface where users can send messages and receive responses.
 *
 * @returns {JSX.Element} The ChatInterface component.
 */
function ChatInterface() {
  const {
    chats,
    currentChatId,
    addMessage,
    updateChat,
    selectedModelId,
    models,
  } = useContext(ChatContext);

  const currentChat = chats.find((chat) => chat.id === currentChatId);
  const [message, setMessage] = useState("");
  const [isTyping, setIsTyping] = useState(false);
  const messagesEndRef = useRef(null);
  const [isCodeModalOpen, setIsCodeModalOpen] = useState(false);
  const [currentEditingCode, setCurrentEditingCode] = useState(null);
  const [isSystemPromptOpen, setIsSystemPromptOpen] = useState(false);
  // Removed unused state: codeAttachments

  /**
   * Adds a code attachment within the message by embedding it as a <code> tag.
   *
   * @param {object} params - Parameters for the code attachment.
   * @param {string} params.codeContent - The actual code content.
   * @param {string} [params.fileName='untitled'] - The name of the code file.
   * @param {string} [params.notes=''] - Optional notes or comments.
   */
  const addCodeAttachment = ({
    codeContent,
    fileName = "untitled",
    notes = "",
  }) => {
    const escapedFileName = escapeHtml(fileName);
    const escapedNotes = escapeHtml(notes);
    // Do not escape codeContent to preserve code readability
    const codeTag = `<code fileName="${escapedFileName}" notes="${escapedNotes}">${codeContent}</code>`;
    setMessage((prev) => prev + codeTag);
  };

  /**
   * Opens the code modal for attaching or editing code segments.
   *
   * @param {object} [codeSegment=null] - The code segment to edit, if any.
   */
  const handleAttachCode = useCallback((codeSegment = null) => {
    setCurrentEditingCode(codeSegment);
    setIsCodeModalOpen(true);
  }, []);

  /**
   * Handles saving of code attachments, either adding new or updating existing ones.
   *
   * @param {object} updatedCode - The updated code data.
   * @param {string} updatedCode.codeContent - The updated code content.
   * @param {string} updatedCode.fileName - The updated file name.
   * @param {string} updatedCode.notes - The updated notes.
   */
  const handleCodeSave = useCallback(
    (updatedCode) => {
      if (currentEditingCode) {
        // Update existing code
        const escapedFileName = escapeHtml(updatedCode.fileName || "untitled");
        const escapedNotes = escapeHtml(updatedCode.notes || "");

        // Use a more robust selector if possible to prevent regex issues
        const regex = new RegExp(
          `<code\\s+fileName="${escapeHtml(
            currentEditingCode.fileName
          )}"\\s+notes="${escapeHtml(
            currentEditingCode.notes
          )}">([\\s\\S]*?)<\\/code>`
        );

        const updatedMessage = message.replace(
          regex,
          `<code fileName="${escapedFileName}" notes="${escapedNotes}">${updatedCode.codeContent}</code>`
        );
        setMessage(updatedMessage);
      } else {
        // Add new code
        addCodeAttachment(updatedCode);
      }
      setIsCodeModalOpen(false);
      setCurrentEditingCode(null);
    },
    [currentEditingCode, message, escapeHtml, addCodeAttachment]
  );

  /**
   * Sends the user's message to the backend and handles the response.
   */
  const handleSend = useCallback(async () => {
    if (!message.trim() || !currentChatId || !selectedModelId) return;

    // Retrieve the system prompt from the current chat
    const systemPrompt = currentChat.systemPrompt || "";

    // Combine the system prompt with the user message
    const fullMessage = systemPrompt ? `${systemPrompt}\n${message}` : message;

    const userMessage = { sender: "user", text: fullMessage };
    addMessage(currentChatId, userMessage);
    setMessage("");
    scrollToBottom();

    // Set typing indicator
    setIsTyping(true);

    try {
      const response = await fetch(
        `${process.env.REACT_APP_BACKEND_URL}/api/chat`,
        {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            text: fullMessage, // Send the full message with <code> tags intact
            model_id: selectedModelId,
          }),
        }
      );

      if (!response.ok) {
        const errorData = await response.json();
        throw new Error(
          errorData.detail || "Failed to get response from the server."
        );
      }

      const data = await response.json();
      const responseText = data.response;
      const responseModelId = data.model_id;

      const botMessage = {
        sender: "bot",
        text: responseText,
        model_id: responseModelId,
      };
      addMessage(currentChatId, botMessage);
      scrollToBottom();
    } catch (error) {
      console.error("Error:", error);
      const errorMessage = {
        sender: "bot",
        text: "Sorry, something went wrong. Please try again.",
      };
      addMessage(currentChatId, errorMessage);
      scrollToBottom();
    } finally {
      // Remove typing indicator
      setIsTyping(false);
    }
  }, [message, currentChatId, selectedModelId, addMessage, currentChat]);

  /**
   * Handles the keydown event in the message input area.
   * Sends the message on Enter key without Shift.
   *
   * @param {object} e - The event object.
   */
  const handleKeyDown = useCallback(
    (e) => {
      if (e.key === "Enter" && !e.shiftKey) {
        e.preventDefault();
        handleSend();
      }
    },
    [handleSend]
  );

  const handleEditorKeyDown = useCallback((e) => {
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault();
      handleSend();
    }
  }, [handleSend]);

  /**
   * Scrolls the chat to the bottom.
   */
  const scrollToBottom = useCallback(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
  }, []);

  /**
   * Updates the system prompt in the current chat.
   *
   * @param {object} e - The event object.
   */
  const handleSystemPromptChange = useCallback(
    (e) => {
      const newPrompt = e.target.value;
      updateChat(currentChatId, { systemPrompt: newPrompt });
    },
    [updateChat, currentChatId]
  );

  /**
   * Toggles the visibility of the system prompt textarea.
   */
  const toggleSystemPrompt = useCallback(() => {
    setIsSystemPromptOpen((prev) => !prev);
  }, []);

  // Remove unused useState: codeAttachments

  if (!currentChat) {
    return (
      <div className="flex-1 p-4 text-gray-700 dark:text-gray-300">
        No active chat. Please start a new chat.
      </div>
    );
  }

  return (
    <div className="flex flex-col h-screen">
      <div className="flex-1 p-32 pt-8 pb-8 overflow-y-auto bg-[#eeeeee] dark:bg-[#161618]">
        {/* Removed Model Selector Dropdown */}

        {/* Chat Messages */}
        {currentChat.messages.map((msg, index) => (
          <Message
            key={index}
            chatId={currentChatId}
            messageIndex={index}
            sender={msg.sender}
            text={msg.text}
            model_id={msg.model_id} // Pass model_id
          />
        ))}
        {isTyping && (
          <div className="mb-4 flex justify-start">
            <div className="max-w-xs md:max-w-md lg:max-w-lg p-3 rounded-lg bg-gray-200 dark:bg-gray-800 text-gray-800 dark:text-gray-200 flex items-center">
              <div className="flex space-x-1 mr-2">
                <div className="w-2 h-2 bg-gray-600 rounded-full animate-bounce-custom animation-delay-0"></div>
                <div className="w-2 h-2 bg-gray-600 rounded-full animate-bounce-custom animation-delay-200"></div>
                <div className="w-2 h-2 bg-gray-600 rounded-full animate-bounce-custom animation-delay-400"></div>
              </div>
              <span className="text-sm">Echo is typing...</span>
            </div>
          </div>
        )}
        <div ref={messagesEndRef} />
      </div>
      <div
        id="message_area"
        className="p-4 border-t dark:border-gray-700 bg-white dark:bg-[#161618]"
      >
        <button
          onClick={toggleSystemPrompt}
          className="mb-2 text-sm text-blue-500 hover:underline focus:outline-none"
        >
          {isSystemPromptOpen ? "Hide System Prompt" : "Show System Prompt"}
        </button>
        {isSystemPromptOpen && (
          <textarea
            className="w-full p-2 border rounded-lg resize-none dark:bg-gray-800 dark:text-gray-200 mb-2"
            rows="3"
            placeholder="Enter system prompt..."
            value={currentChat.systemPrompt}
            onChange={handleSystemPromptChange}
          />
        )}
        <div className="flex flex-row">
          <div className="flex-grow">
            <InlineCodeEditor
              value={message}
              onChange={setMessage}
              onAttachCode={handleAttachCode}
              onKeyDown={handleEditorKeyDown} // Pass the keydown handler as a prop
            />
          </div>
          <div className="flex flex-col items-end pl-4">
            <button
              onClick={() => handleAttachCode(null)} // Passing null for new attachment
              className="bg-blue-500 text-white px-3 py-2 rounded-lg hover:bg-blue-600 flex-grow w-full"
            >
              Attach Code
            </button>
            <button
              className="bg-green-500 text-white px-4 py-2 rounded-lg hover:bg-green-600 flex-grow mt-2 w-full"
              onClick={handleSend}
            >
              Send
            </button>
          </div>
        </div>
      </div>
      {isCodeModalOpen && (
        <CodeModal
          onClose={() => {
            setIsCodeModalOpen(false);
            setCurrentEditingCode(null);
          }}
          onSave={handleCodeSave}
          initialData={currentEditingCode}
        />
      )}
    </div>
  );
}

export default ChatInterface;
</code>

<code>
// src/components/CodeAttachmentBubble.js

import React from 'react';
import { PencilIcon, TrashIcon } from '@heroicons/react/solid';

/**
 * Component representing a code attachment bubble with edit and delete options.
 *
 * @param {object} props - Component props.
 * @param {object} props.attachment - The code attachment details.
 * @param {function} props.onEdit - Function to call when editing the attachment.
 * @param {function} props.onDelete - Function to call when deleting the attachment.
 * @returns {JSX.Element} The CodeAttachmentBubble component.
 */
function CodeAttachmentBubble({ attachment, onEdit, onDelete }) {
  return (
    <div className="inline-flex items-center bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-300 px-3 py-1 rounded-full mr-2 mb-2">
      <span
        className="mr-2 text-xs md:text-sm truncate"
        title={attachment.fileName || 'Untitled'}
      >
        {attachment.fileName || 'Untitled'}
      </span>
      <button
        onClick={onEdit}
        className="hover:text-blue-600 dark:hover:text-blue-500 focus:outline-none"
        title="Edit Attachment"
        aria-label="Edit Attachment"
      >
        <PencilIcon className="h-4 w-4" />
      </button>
      <button
        onClick={onDelete}
        className="ml-1 hover:text-red-600 dark:hover:text-red-500 focus:outline-none"
        title="Delete Attachment"
        aria-label="Delete Attachment"
      >
        <TrashIcon className="h-4 w-4" />
      </button>
    </div>
  );
}

export default CodeAttachmentBubble;
</code>

<code>
// src/components/CodeModal.js

import React, { useState, useEffect, useCallback } from 'react';

/**
 * Modal component for attaching or editing code segments.
 *
 * @param {object} props - Component props.
 * @param {function} props.onClose - Function to call when closing the modal.
 * @param {function} props.onSave - Function to call when saving the attachment.
 * @param {object|null} props.initialData - Initial data for editing, if any.
 * @returns {JSX.Element|null} The CodeModal component or null if not visible.
 */
function CodeModal({ onClose, onSave, initialData = null }) {
  const [codeContent, setCodeContent] = useState('');
  const [fileName, setFileName] = useState('');
  const [notes, setNotes] = useState('');
  const [error, setError] = useState('');

  useEffect(() => {
    if (initialData) {
      setCodeContent(initialData.codeContent);
      setFileName(initialData.fileName);
      setNotes(initialData.notes);
    }
  }, [initialData]);

  /**
   * Handles the save action by validating inputs and invoking the onSave callback.
   */
  const handleSave = useCallback(() => {
    if (codeContent.trim() === '') {
      setError('Code content cannot be empty.');
      return;
    }
    setError('');
    onSave({ codeContent, fileName, notes });
    onClose();
  }, [codeContent, fileName, notes, onSave, onClose]);

  /**
   * Handles the key press event for accessibility (e.g., closing on Esc).
   *
   * @param {object} e - The event object.
   */
  const handleKeyDown = useCallback((e) => {
    if (e.key === 'Escape') {
      onClose();
    }
  }, [onClose]);

  return (
    <div
      className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50"
      onKeyDown={handleKeyDown}
      role="dialog"
      aria-modal="true"
      aria-labelledby="code-modal-title"
    >
      <div className="bg-white dark:bg-gray-800 p-6 rounded-lg w-11/12 md:w-1/2 lg:w-1/3">
        <h2 id="code-modal-title" className="text-xl mb-4 text-gray-800 dark:text-gray-200">
          {initialData ? 'Edit Code Attachment' : 'Attach Code'}
        </h2>
        {error && <div className="mb-2 text-red-500">{error}</div>}
        <textarea
          className="w-full p-2 border rounded-lg resize-none dark:bg-gray-700 dark:text-white mb-2"
          rows="6"
          placeholder="Enter your code here..."
          value={codeContent}
          onChange={(e) => setCodeContent(e.target.value)}
          aria-label="Code Content"
        />
        <input
          type="text"
          className="w-full p-2 border rounded-lg dark:bg-gray-700 dark:text-white mb-2"
          placeholder="File Name (optional)"
          value={fileName}
          onChange={(e) => setFileName(e.target.value)}
          aria-label="File Name"
        />
        <textarea
          className="w-full p-2 border rounded-lg resize-none dark:bg-gray-700 dark:text-white mb-2"
          rows="3"
          placeholder="Add notes (optional)"
          value={notes}
          onChange={(e) => setNotes(e.target.value)}
          aria-label="Notes"
        />
        <div className="flex justify-end">
          <button
            onClick={onClose}
            className="mr-2 px-4 py-2 bg-gray-300 dark:bg-gray-600 text-gray-800 dark:text-gray-200 rounded hover:bg-gray-400 dark:hover:bg-gray-500 focus:outline-none"
            aria-label="Cancel Attachment"
          >
            Cancel
          </button>
          <button
            onClick={handleSave}
            className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 focus:outline-none"
            aria-label={initialData ? "Update Attachment" : "Attach Code"}
          >
            {initialData ? 'Update' : 'Attach'}
          </button>
        </div>
      </div>
    </div>
  );
}

export default CodeModal;
</code>

<code>
// src/components/InlineCodeEditor.js

import React, { useRef, useEffect, useCallback } from 'react';
import CodeAttachmentBubble from './CodeAttachmentBubble';
import { renderCodeBubble } from '../utils/renderCodeBubble';
import { escapeHtml } from '../utils/escapeHtml';
import { unescapeHtml } from '../utils/unescapeHtml';

/**
 * Inline code editor component allowing users to input and attach code segments.
 *
 * @param {object} props - Component props.
 * @param {string} props.value - The current value of the editor.
 * @param {function} props.onChange - Function to call when the editor content changes.
 * @param {function} props.onAttachCode - Function to call when attaching code.
 * @returns {JSX.Element} The InlineCodeEditor component.
 */
function InlineCodeEditor({ value, onChange, onAttachCode }) {
  const editorRef = useRef(null);

  /**
   * Formats the content by replacing <code> tags with styled code bubbles.
   *
   * @param {string} content - The raw HTML content.
   * @returns {string} - The formatted HTML content.
   */
  const formatContent = useCallback((content) => {
    return content.replace(/<code([^>]*)>([\s\S]*?)<\/code>/g, (match, attributes, code) => {
      const fileName = attributes.match(/fileName="([^"]*)"/)?.[1] || 'Untitled';
      const notes = attributes.match(/notes="([^"]*)"/)?.[1] || '';
      // Use unescapeHtml to get actual code content
      const codeContent = unescapeHtml(code);
      return renderCodeBubble(fileName, notes, codeContent);
    });
  }, []);

  /**
   * Synchronizes the editor's HTML content with the internal state.
   */
  useEffect(() => {
    if (editorRef.current && editorRef.current.innerHTML !== formatContent(value)) {
      editorRef.current.innerHTML = formatContent(value);
    }
  }, [value, formatContent]);

  /**
   * Handles input events by decoding code bubbles and updating the state.
   */
  const handleInput = useCallback(() => {
    if (editorRef.current) {
      const content = editorRef.current.innerHTML;
      const plainText = content.replace(/<span class="code-bubble"[^>]*>[^<]*<\/span>/g, (match) => {
        const fileName = match.match(/data-filename="([^"]*)"/)?.[1];
        const notes = match.match(/data-notes="([^"]*)"/)?.[1];
        const codeContentEscaped = match.match(/data-codecontent="([^"]*)"/)?.[1];
        // Unescape the codeContent
        const codeContent = codeContentEscaped ? unescapeHtml(codeContentEscaped) : '';
        return `<code fileName="${fileName}" notes="${notes}">${codeContent}</code>`;
      });
      onChange(plainText);
    }
  }, [onChange]);

  /**
   * Handles keydown events, specifically for Enter and Shift+Enter.
   *
   * @param {object} e - The event object.
   */
  const handleKeyDown = useCallback((e) => {
    if (e.key === 'Enter') {
      if (e.shiftKey) {
        // Allow Shift+Enter to insert a newline
        // Let the event propagate naturally
      } else {
        // Prevent default behavior and emit a custom event or handle sending the message
        // Here, you might want to trigger the send action if needed
        // For InlineCodeEditor, we'll allow the parent to handle message sending
        // by not preventing default here
      }
    }
  }, []);

  /**
   * Handles paste events by stripping formatting and inserting plain text.
   *
   * @param {object} e - The event object.
   */
  const handlePaste = useCallback((e) => {
    e.preventDefault();
    const text = e.clipboardData.getData('text/plain');
    document.execCommand('insertText', false, text);
  }, []);

  /**
   * Handles clicks on code bubbles to initiate editing.
   *
   * @param {object} e - The event object.
   */
  const handleBubbleClick = useCallback((e) => {
    if (e.target.classList.contains('code-bubble')) {
      const fileName = e.target.getAttribute('data-filename');
      const notes = e.target.getAttribute('data-notes');
      const codeContentEscaped = e.target.getAttribute('data-codecontent');
      const codeContent = codeContentEscaped ? unescapeHtml(codeContentEscaped) : '';
      onAttachCode({ fileName, notes, codeContent });
    }
  }, [onAttachCode]);

  return (
    <div className="relative border rounded-lg p-2 bg-white dark:bg-gray-800 text-black dark:text-white">
      <div
        ref={editorRef}
        className="min-h-[100px] max-h-[300px] overflow-y-auto p-2 focus:outline-none"
        contentEditable
        onInput={handleInput}
        onKeyDown={handleKeyDown}
        onPaste={handlePaste}
        onClick={handleBubbleClick}
        role="textbox"
        aria-multiline="true"
        aria-label="Code Editor"
      />
    </div>
  );
}

export default InlineCodeEditor;
</code>

<code>
// src/components/MarkdownRenderer.js

import React, { useContext, useState, useCallback, useEffect } from 'react';
import ReactMarkdown from 'react-markdown';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { coldarkDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
import { CodeContext } from '../context/CodeContext';
import { ExternalLinkIcon, ClipboardCopyIcon } from '@heroicons/react/outline';
import { CopyToClipboard } from 'react-copy-to-clipboard';

/**
 * Component for rendering markdown content with syntax-highlighted code blocks.
 *
 * @param {object} props - Component props.
 * @param {string} props.text - The markdown text to render.
 * @returns {JSX.Element} The MarkdownRenderer component.
 */
const MarkdownRenderer = ({ text }) => {
  const { showCode } = useContext(CodeContext);
  const [copiedCode, setCopiedCode] = useState({}); // Track copied state per code block

  /**
   * Handles the copy action and updates the copied state.
   *
   * @param {number} key - Unique identifier for the code block.
   */
  const handleCopy = useCallback((key) => {
    setCopiedCode((prev) => ({ ...prev, [key]: true }));
  }, []);

  /**
   * Resets the copied state after a delay.
   */
  useEffect(() => {
    const timers = Object.keys(copiedCode).map((key) =>
      copiedCode[key] ? setTimeout(() => setCopiedCode((prev) => ({ ...prev, [key]: false })), 2000) : null
    );
    return () => {
      timers.forEach((timer) => timer && clearTimeout(timer));
    };
  }, [copiedCode]);

  return (
    <ReactMarkdown
      components={{
        code({ node, inline, className, children, ...props }) {
          const match = /language-(\w+)/.exec(className || '');
          if (!inline && match) {
            const language = match[1];
            const codeContent = String(children).replace(/\n$/, '');
            const key = node.position?.start.line || Math.random().toString(36).substr(2, 9); // Unique key per block

            const onCopy = () => handleCopy(key);

            return (
              <div className="relative my-4">
                <SyntaxHighlighter
                  style={coldarkDark}
                  language={language}
                  PreTag="div"
                  className="rounded-lg"
                >
                  {codeContent}
                </SyntaxHighlighter>
                <div className="absolute top-2 right-2 flex space-x-2">
                  <CopyToClipboard text={codeContent} onCopy={onCopy}>
                    <button
                      className="bg-gray-700 bg-opacity-50 text-white p-1 rounded hover:bg-opacity-75 flex items-center"
                      title="Copy Code"
                      aria-label="Copy Code"
                    >
                      <ClipboardCopyIcon className="h-4 w-4" />
                      {copiedCode[key] && <span className="ml-1 text-xs">Copied!</span>}
                    </button>
                  </CopyToClipboard>
                  <button
                    className="bg-gray-700 bg-opacity-50 text-white p-1 rounded hover:bg-opacity-75 flex items-center"
                    onClick={() =>
                      showCode({
                        codeContent: codeContent,
                        fileName: 'codeblock',
                        notes: '',
                      })
                    }
                    title="Pop Out Code"
                    aria-label="Pop Out Code"
                  >
                    <ExternalLinkIcon className="h-4 w-4" />
                  </button>
                </div>
              </div>
            );
          } else {
            return (
              <code className={className} {...props}>
                {children}
              </code>
            );
          }
        },
        h1: ({ node, ...props }) => <h1 className="text-2.5xl mb-4 mt-5" {...props} />,
        h2: ({ node, ...props }) => <h2 className="text-2xl mb-2 mt-3" {...props} />,
        h3: ({ node, ...props }) => <h3 className="text-1.75xl mb-2 mt-3" {...props} />,
        h4: ({ node, ...props }) => <h4 className="text-1.5xl mb-2 mt-3" {...props} />,
        h5: ({ node, ...props }) => <h5 className="text-1.25xl mb-2" {...props} />,
        h6: ({ node, ...props }) => <h6 className="text-xl mb-2" {...props} />,
        hr: ({ node, ...props }) => <hr className="my-6 border-gray-300 dark:border-gray-700" {...props} />,
        li: ({ node, ordered, ...props }) => <li className="ml-4 list-disc" {...props} />,
      }}
    >
      {text}
    </ReactMarkdown>
  );
};

export default MarkdownRenderer;
</code>

<code>
// src/components/Message.js

import React, { useState, useContext, useCallback } from "react";
import MarkdownRenderer from "./MarkdownRenderer";
import { CopyToClipboard } from "react-copy-to-clipboard";
import { ClipboardCopyIcon } from "@heroicons/react/solid";
import { CodeContext } from "../context/CodeContext";
import CodeAttachmentBubble from "./CodeAttachmentBubble";
import CodeModal from "./CodeModal";
import { ChatContext } from "../context/ChatContext";

/**
 * Component representing a single chat message.
 *
 * @param {object} props - Component props.
 * @param {string} props.chatId - The ID of the chat.
 * @param {number} props.messageIndex - The index of the message within the chat.
 * @param {string} props.sender - The sender of the message ('user' or 'bot').
 * @param {string} props.text - The message text.
 * @param {string|null} props.model_id - The ID of the model used for bot responses.
 * @returns {JSX.Element} The Message component.
 */
function Message({ chatId, messageIndex, sender, text, model_id }) {
  const [copied, setCopied] = useState(false);
  const { showCode } = useContext(CodeContext);
  const [isEditModalOpen, setIsEditModalOpen] = useState(false);
  const [currentAttachment, setCurrentAttachment] = useState(null);
  const { updateMessage, models } = useContext(ChatContext);

  const modelName = model_id
    ? models.find((model) => model.id === model_id)?.name || "Unknown Model"
    : null;

  /**
   * Handles the copy action and updates the copied state.
   */
  const handleCopy = useCallback(() => {
    setCopied(true);
  }, []);

  /**
   * Resets the copied state after a delay.
   */
  React.useEffect(() => {
    if (copied) {
      const timer = setTimeout(() => setCopied(false), 2000);
      return () => clearTimeout(timer);
    }
  }, [copied]);

  /**
   * Extracts code attachments from the message text.
   *
   * @param {string} text - The message text.
   * @returns {Array} An array of code attachment objects.
   */
  const extractCodeAttachments = useCallback((text) => {
    const codeRegex =
      /<code(?:\s+fileName="([^"]*)")?(?:\s+notes="([^"]*)")?>([\s\S]*?)<\/code>/g;
    let match;
    const attachments = [];
    while ((match = codeRegex.exec(text)) !== null) {
      attachments.push({
        fileName: match[1] || "Untitled",
        notes: match[2] || "",
        codeContent: match[3],
      });
    }
    return attachments;
  }, []);

  const codeAttachments = extractCodeAttachments(text);
  const cleanedText = text.replace(
    /<code(?:\s+fileName="[^"]*")?(?:\s+notes="[^"]*")?>[\s\S]*?<\/code>/g,
    ""
  );

  /**
   * Initiates editing of a code attachment.
   *
   * @param {object} attachment - The code attachment to edit.
   */
  const handleEdit = useCallback((attachment) => {
    setCurrentAttachment(attachment);
    setIsEditModalOpen(true);
  }, []);

  /**
   * Deletes a code attachment from the message.
   *
   * @param {object} attachment - The code attachment to delete.
   */
  const handleDelete = useCallback(
    (attachment) => {
      const updatedText = text.replace(
        `<code fileName="${attachment.fileName}" notes="${attachment.notes}">${attachment.codeContent}</code>`,
        ""
      );
      updateMessage(chatId, messageIndex, { text: updatedText });
    },
    [text, updateMessage, chatId, messageIndex]
  );

  /**
   * Updates a code attachment after editing.
   *
   * @param {object} updatedAttachment - The updated code attachment data.
   */
  const handleUpdateAttachment = useCallback(
    (updatedAttachment) => {
      const oldCodeBlockRegex = new RegExp(
        `<code\\s+fileName="${
          updatedAttachment.fileName || "Untitled"
        }"\\s+notes="${updatedAttachment.notes || ""}">[\\s\\S]*?<\\/code>`,
        "g"
      );
      const newCodeBlock = `<code fileName="${updatedAttachment.fileName}" notes="${updatedAttachment.notes}">${updatedAttachment.codeContent}</code>`;
      const updatedText = text.replace(oldCodeBlockRegex, newCodeBlock);
      updateMessage(chatId, messageIndex, { text: updatedText });
    },
    [text, updateMessage, chatId, messageIndex]
  );

  return (
    <>
      <div
        className={`mb-4 flex ${
          sender === "user" ? "justify-end" : "justify-start"
        }`}
      >
        <div
          className={`max-w-xs md:max-w-md lg:max-w-5xl p-5 rounded-xl ${
            sender === "user"
              ? "bg-zinc-950 text-white"
              : "bg-gray-200 dark:bg-[#282828] text-gray-800 dark:text-gray-200"
          }`}
          role={sender === "user" ? "article" : "document"}
          aria-label={`${sender === "user" ? "User" : "Bot"} message`}
        >
          {sender === "bot" && modelName && (
            <div className="mb-2 text-sm text-gray-500 dark:text-gray-400">
              <strong>Model:</strong> {modelName}
            </div>
          )}

          <MarkdownRenderer text={cleanedText} />
          <div className="mt-2 flex flex-wrap">
            {codeAttachments.map((attachment, index) => (
              <CodeAttachmentBubble
                key={index}
                attachment={attachment}
                onEdit={() => handleEdit(attachment)}
                onDelete={() => handleDelete(attachment)}
              />
            ))}
          </div>
          <CopyToClipboard text={text} onCopy={handleCopy}>
            <button
              className="mt-2 text-sm text-gray-500 hover:text-gray-700 flex items-center"
              aria-label="Copy Message"
            >
              <ClipboardCopyIcon className="h-5 w-5 mr-1" />{" "}
              {copied ? "Copied!" : "Copy"}
            </button>
          </CopyToClipboard>
        </div>
      </div>
      {isEditModalOpen && currentAttachment && (
        <CodeModal
          onClose={() => setIsEditModalOpen(false)}
          onSave={handleUpdateAttachment}
          initialData={currentAttachment}
        />
      )}
    </>
  );
}

export default Message;
</code>

<code>
// src/context/ChatContext.js

import React, { createContext, useState, useEffect, useCallback } from 'react';
import { v4 as uuidv4 } from 'uuid';

/**
 * Context for managing chat sessions, messages, and available AI models.
 */
export const ChatContext = createContext();

/**
 * Provider component that encapsulates chat-related state and functions.
 *
 * @param {React.ReactNode} children - Child components that consume the context.
 * @returns {JSX.Element} - The ChatContext provider wrapping children.
 */
export const ChatProvider = ({ children }) => {
  // Initialize chats from localStorage or as an empty array
  const [chats, setChats] = useState(() => {
    try {
      const saved = localStorage.getItem('chats');
      return saved ? JSON.parse(saved) : [];
    } catch (error) {
      console.error('Failed to parse chats from localStorage:', error);
      return [];
    }
  });

  // Current active chat ID
  const [currentChatId, setCurrentChatId] = useState(() => {
    try {
      const saved = localStorage.getItem('currentChatId');
      return saved ? JSON.parse(saved) : null;
    } catch (error) {
      console.error('Failed to parse currentChatId from localStorage:', error);
      return null;
    }
  });

  // Available AI models
  const [models, setModels] = useState([]);

  // Selected AI model ID
  const [selectedModelId, setSelectedModelId] = useState(() => {
    try {
      const saved = localStorage.getItem('selectedModelId');
      return saved ? JSON.parse(saved) : null;
    } catch (error) {
      console.error('Failed to parse selectedModelId from localStorage:', error);
      return null;
    }
  });

  /**
   * Fetches available AI models from the backend API.
   */
  const fetchModels = useCallback(async () => {
    try {
      const response = await fetch(`${process.env.REACT_APP_BACKEND_URL}/api/models`, {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
        },
      });

      if (!response.ok) {
        throw new Error(`HTTP error! Status: ${response.status}`);
      }

      const data = await response.json();

      if (Array.isArray(data) && data.length > 0 && !selectedModelId) {
        setSelectedModelId(data[0].id); // Select the first model by default
      }

      setModels(data);
    } catch (error) {
      console.error('Error fetching models:', error);
      setModels([]); // Clear models on error to prevent inconsistent state
    }
  }, [selectedModelId]);

  // Fetch models on component mount
  useEffect(() => {
    fetchModels();
  }, [fetchModels]);

  /**
   * Persist chats to localStorage whenever chats state changes.
   */
  useEffect(() => {
    try {
      localStorage.setItem('chats', JSON.stringify(chats));
    } catch (error) {
      console.error('Failed to save chats to localStorage:', error);
    }
  }, [chats]);

  /**
   * Persist currentChatId to localStorage whenever it changes.
   */
  useEffect(() => {
    try {
      localStorage.setItem('currentChatId', JSON.stringify(currentChatId));
    } catch (error) {
      console.error('Failed to save currentChatId to localStorage:', error);
    }
  }, [currentChatId]);

  /**
   * Persist selectedModelId to localStorage whenever it changes.
   */
  useEffect(() => {
    if (selectedModelId) {
      try {
        localStorage.setItem('selectedModelId', JSON.stringify(selectedModelId));
      } catch (error) {
        console.error('Failed to save selectedModelId to localStorage:', error);
      }
    }
  }, [selectedModelId]);

  /**
   * Adds a new chat session.
   *
   * @param {string} [name] - Optional name for the new chat. Defaults to "Chat X".
   */
  const addChat = (name = `Chat ${chats.length + 1}`) => {
    const newChat = {
      id: uuidv4(),
      name: name,
      messages: [],
      systemPrompt: '',
      model_id: selectedModelId, // Associate with selected model
    };
    setChats((prev) => [newChat, ...prev]);
    setCurrentChatId(newChat.id);
  };

  /**
   * Updates an existing chat's details.
   *
   * @param {string} id - The unique identifier of the chat to update.
   * @param {object} updatedChat - The updated chat data.
   */
  const updateChat = (id, updatedChat) => {
    setChats((prevChats) =>
      prevChats.map((chat) => (chat.id === id ? { ...chat, ...updatedChat } : chat))
    );
  };

  /**
   * Adds a new message to a specific chat.
   *
   * @param {string} chatId - The unique identifier of the chat.
   * @param {object} message - The message object to add.
   */
  const addMessage = (chatId, message) => {
    setChats((prevChats) =>
      prevChats.map((chat) =>
        chat.id === chatId
          ? { ...chat, messages: [...chat.messages, { ...message, text: String(message.text) }] }
          : chat
      )
    );
  };

  /**
   * Deletes a chat by its ID.
   *
   * @param {string} id - The unique identifier of the chat to delete.
   */
  const deleteChat = (id) => {
    setChats((prevChats) => prevChats.filter((chat) => chat.id !== id));
    // If the deleted chat was the current one, reset currentChatId
    if (id === currentChatId) {
      setCurrentChatId(null);
    }
  };

  /**
   * Clears all chat sessions.
   */
  const clearChats = () => {
    setChats([]);
    setCurrentChatId(null);
  };

  /**
   * Updates a specific message within a chat.
   *
   * @param {string} chatId - The unique identifier of the chat.
   * @param {number} messageIndex - The index of the message to update.
   * @param {object} updatedMessage - The updated message data.
   */
  const updateMessage = (chatId, messageIndex, updatedMessage) => {
    setChats((prevChats) =>
      prevChats.map((chat) =>
        chat.id === chatId
          ? {
              ...chat,
              messages: chat.messages.map((msg, idx) =>
                idx === messageIndex
                  ? { ...msg, text: String(updatedMessage.text) }
                  : msg
              ),
            }
          : chat
      )
    );
  };

  /**
   * Adds a new AI model to the available models list.
   *
   * @param {object} model - The model object to add.
   */
  const addModel = (model) => {
    setModels((prev) => [...prev, model]);
  };

  /**
   * Removes an AI model by its ID.
   *
   * @param {string} modelId - The unique identifier of the model to remove.
   */
  const removeModel = (modelId) => {
    setModels((prev) => prev.filter((model) => model.id !== modelId));
    // If the removed model was selected, select the first available model or null
    if (selectedModelId === modelId) {
      const remainingModels = models.filter((model) => model.id !== modelId);
      setSelectedModelId(remainingModels.length > 0 ? remainingModels[0].id : null);
    }
  };

  return (
    <ChatContext.Provider
      value={{
        chats,
        currentChatId,
        setCurrentChatId,
        addChat,
        updateChat,
        addMessage,
        updateMessage,
        deleteChat,
        clearChats,
        models,
        addModel,
        removeModel,
        selectedModelId,
        setSelectedModelId,
      }}
    >
      {children}
    </ChatContext.Provider>
  );
};
</code>

<code>
// src/context/CodeContext.js

import React, { createContext, useState, useCallback } from 'react';

/**
 * Context for managing the code-related sidebar, including visibility and current code content.
 */
export const CodeContext = createContext();

/**
 * Provider component that encapsulates code-related state and functions.
 *
 * @param {React.ReactNode} children - Child components that consume the context.
 * @returns {JSX.Element} - The CodeContext provider wrapping children.
 */
export const CodeProvider = ({ children }) => {
  // Indicates whether the code sidebar is open
  const [isSidebarOpen, setIsSidebarOpen] = useState(false);

  // Holds the current code attachment details
  const [currentCode, setCurrentCode] = useState(null);

  /**
   * Displays the code sidebar with the provided code attachment.
   *
   * @param {object} codeAttachment - The code data to display in the sidebar.
   */
  const showCode = useCallback((codeAttachment) => {
    setCurrentCode(codeAttachment);
    setIsSidebarOpen(true);
  }, []);

  /**
   * Hides the code sidebar and clears the current code content.
   */
  const hideCode = useCallback(() => {
    setIsSidebarOpen(false);
    setCurrentCode(null);
  }, []);

  return (
    <CodeContext.Provider value={{ isSidebarOpen, currentCode, showCode, hideCode }}>
      {children}
    </CodeContext.Provider>
  );
};
</code>

<code>
// src/utils/escapeHtml.js

/**
 * Escapes HTML special characters in a string to prevent XSS attacks.
 *
 * @param {string} unsafe - The string to be escaped.
 * @returns {string} - The escaped string safe for insertion into HTML.
 */
export const escapeHtml = (unsafe) => {
  if (typeof unsafe !== 'string') {
    console.warn('escapeHtml received non-string:', unsafe);
    unsafe = String(unsafe);
  }

  const entityMap = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#039;',
    '/': '&#x2F;',
    '`': '&#x60;',
    '=': '&#x3D;',
  };

  return unsafe.replace(/[&<>"'`=\/]/g, (char) => entityMap[char]);
};
</code>

<code>
// src/utils/unescapeHtml.js

/**
 * Reverses the escaping of HTML characters.
 *
 * @param {string} unsafe - The escaped HTML string.
 * @returns {string} - The unescaped HTML string.
 */
export const unescapeHtml = (unsafe) => {
    const doc = new DOMParser().parseFromString(unsafe, "text/html");
    return doc.documentElement.textContent;
  };
  </code>

<code>
// src/utils/renderCodeBubble.js

import { escapeHtml } from './escapeHtml';

/**
 * Generates an HTML string for a code bubble with given details.
 *
 * @param {string} fileName - Name of the file.
 * @param {string} [notes=''] - Optional notes or annotations.
 * @param {string} [codeContent=''] - The code content to be associated.
 * @returns {string} - The HTML string representing the code bubble.
 */
export const renderCodeBubble = (fileName, notes = '', codeContent = '') => {
  // Safely escape all dynamic content to prevent XSS
  const safeFileName = escapeHtml(fileName || 'Untitled');
  const safeNotes = escapeHtml(notes);
  const safeCodeContent = escapeHtml(codeContent); // Changed from encodeURIComponent to escapeHtml

  return `
    <span
      class="inline-flex items-center bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-300 px-3 py-1 rounded-full mr-2 mb-2 code-bubble cursor-pointer"
      data-filename="${safeFileName}"
      data-notes="${safeNotes}"
      data-codecontent="${safeCodeContent}" <!-- Removed encoding -->
    >
      ${safeFileName}
    </span>
    \u200B <!-- Zero-width space to ensure cursor moves after the bubble -->
  `;
};
</code>