Newer
Older
cortex-hub / ui / client-app / src / components / ChatWindow.js
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;