import React, { useEffect, useRef, useState } from "react"; import ReactMarkdown from 'react-markdown'; import './ChatWindow.css'; import FileListComponent from "./FileList"; import DiffViewer from "./DiffViewer"; import CodeChangePlan from "./CodeChangePlan"; import { FaRegCopy,FaCopy } from 'react-icons/fa'; // Import the copy icon // Individual message component const ChatMessage = ({ message }) => { const [selectedFile, setSelectedFile] = useState(null); const [isReasoningExpanded, setIsReasoningExpanded] = useState(false); const toggleReasoning = () => { setIsReasoningExpanded(!isReasoningExpanded); }; const handleCloseDiff = () => { setSelectedFile(null); }; const handleFileClick = (file) => { setSelectedFile(file); }; // Function to copy text to clipboard const handleCopy = async () => { if (message.text) { try { await navigator.clipboard.writeText(message.text); // Optional: Add a state or a toast notification to show "Copied!" } catch (err) { console.error('Failed to copy text: ', err); } } }; const assistantMessageClasses = `p-4 rounded-lg shadow-md w-full bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-gray-100 assistant-message`; const userMessageClasses = `max-w-md p-4 rounded-lg shadow-md bg-indigo-500 text-white ml-auto`; return ( <div className={message.isUser ? userMessageClasses : assistantMessageClasses}> {message.reasoning && ( <div className="mb-2"> <button onClick={toggleReasoning} className="text-sm font-semibold text-indigo-700 hover:text-indigo-800 dark:text-gray-300 dark:hover:text-gray-100 focus:outline-none" > {isReasoningExpanded ? "Hide Reasoning ▲" : "Show Reasoning ▼"} </button> <div className={`mt-2 text-xs transition-max-h duration-500 ease-in-out overflow-hidden ${ isReasoningExpanded ? "max-h-96" : "max-h-0" } text-gray-700 dark:text-gray-300`} > <ReactMarkdown>{message.reasoning}</ReactMarkdown> </div> </div> )} <ReactMarkdown>{message.text}</ReactMarkdown> {message.isPureAnswer && ( <div className="justify-end items-center mt-2"> {/* Horizontal line */} <div className="border-b border-gray-400 dark:border-gray-600"></div> <div className="flex justify-end items-center mt-2"> {/* Copy Icon - positioned above the bottom line */} <button onClick={handleCopy} className="relative p-2 rounded-right-2 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 transition-colors z-10 group" aria-label="Copy message text" > {/* Outline icon */} <FaRegCopy size={18} className="transition-opacity opacity-100 group-hover:opacity-0 absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2" /> {/* Solid icon (initially hidden) */} <FaCopy size={18} className="transition-opacity opacity-0 group-hover:opacity-100 absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2" /> </button> </div> </div> )} {message.code_changes && ( <FileListComponent code_changes={message.code_changes} onFileClick={handleFileClick} /> )} {message.steps && ( <CodeChangePlan steps={message.steps} /> )} {selectedFile && <DiffViewer oldContent={selectedFile.old} newContent={selectedFile.new} filePath={selectedFile.filepath} onClose={handleCloseDiff} />} </div> ); }; // Main ChatWindow component with dynamic height calculation const ChatWindow = ({ chatHistory, maxHeight }) => { const containerRef = useRef(null); useEffect(() => { if (containerRef.current) { containerRef.current.scrollTop = containerRef.current.scrollHeight; } }, [chatHistory]); return ( <div ref={containerRef} style={{ maxHeight: maxHeight, overflowY: 'auto' }} className="px-2 py-4 space-y-4 bg-transparent" > {chatHistory.map((message, index) => ( <div key={index} className={`flex ${message.isUser ? "justify-end" : "justify-start"} w-full`} > <ChatMessage message={message} /> </div> ))} </div> ); }; export default ChatWindow;