I'm making an AI assistant interface website, and I've attempted to make a code attachment feature which allows you to attach code in bubbles inline with the text. However, I've had too many issues with it, so I'm shelving the feature for now. I want to instead just have an attachments field and a standard text input.


// 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  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 (

No active chat. Please start a new chat.

);
}
return (


{/* 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;


// 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 (
{attachment.fileName || 'Untitled'}  
); }
export default CodeAttachmentBubble;


// 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 (



{initialData ? 'Edit Code Attachment' : 'Attach Code'}

{error &&
{error}
}
<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"
/>


Cancel

<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'}




);
}
export default CodeModal;


// 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 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(/])>([\s\S]?)/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(/]>[^<]/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 (



);
}
export default InlineCodeEditor;


// 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;


// 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 && (

Model: {modelName}

)}

      <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;


// 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>
);
};


// 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>
);
};


// 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 --> ;
};