feat(frontend): Add "Copy" Button to Chat Messages (#2619)

* Add "Copy" Button to Chat Messages

### PR Overview: Add "Copy" Button to Chat Messages

**Description:**
This PR introduces a "Copy" button to each chat message in the `ChatMessage` component. The button allows users to easily copy the content of a chat message to their clipboard. The implementation includes a button with a clipboard icon and the necessary functionality to copy the message content.

**Changes Made:**
1. **Imports:**
   - Added `FaClipboard` from `react-icons/fa` for the clipboard icon.
   - Added `toast` from `#utils/toast` for displaying notifications.

2. **New Functionality:**
   - Implemented `copyToClipboard` function using `navigator.clipboard.writeText` to copy the message content.
   - Added a button element with an `onClick` handler to trigger the `copyToClipboard` function.

3. **UI Enhancements:**
   - The button is styled to match the existing UI and is placed next to the message content.

**Code Changes:**
- Modified `frontend/src/components/chat/ChatMessage.tsx` to include the new button and functionality.

**Testing:**
- Verified that clicking the "Copy" button copies the message content to the clipboard.
- Confirmed that a toast notification appears upon successful copy or failure.

**Example Code:**
```tsx
import React from "react";
import Markdown from "react-markdown";
import { twMerge } from "tailwind-merge";
import { code } from "../markdown/code";
import { FaClipboard } from "react-icons/fa";
import toast from "#/utils/toast"; // Assuming you have a toast utility for notifications

interface MessageProps {
  message: Message;
}

function ChatMessage({ message }: MessageProps) {
  const className = twMerge(
    "markdown-body",
    "p-3 text-white max-w-[90%] overflow-y-auto rounded-lg",
    message.sender === "user" ? "bg-neutral-700 self-end" : "bg-neutral-500",
  );

  const copyToClipboard = () => {
    navigator.clipboard.writeText(message.content).then(() => {
      toast.info("Message copied to clipboard!");
    }).catch((error) => {
      toast.error(`Failed to copy message: ${error}`);
    });
  };

  return (
    <div data-testid="message" className={className}>
      <div className="flex justify-between items-center">
        <Markdown components={{ code }}>{message.content}</Markdown>
        <button
          onClick={copyToClipboard}
          className="ml-2 p-1 bg-neutral-600 rounded hover:bg-neutral-500"
          aria-label="Copy message"
        >
          <FaClipboard />
        </button>
      </div>
    </div>
  );
}

export default ChatMessage;
```

**Notes:**
- Ensure that the `react-icons` package is installed (`npm install react-icons` or `yarn add react-icons`).
- The toast utility is assumed to be available for notifications. If not, consider using an alternative notification method.

* layout enhancements; linting

---------

Co-authored-by: tobitege <tobitege@gmx.de>
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
This commit is contained in:
PierrunoYT
2024-06-25 08:15:44 +02:00
committed by GitHub
parent 7e78fde48f
commit c99b94dbe9

View File

@@ -1,23 +1,51 @@
import React from "react";
import React, { useState } from "react";
import Markdown from "react-markdown";
import { FaClipboard } from "react-icons/fa";
import { twMerge } from "tailwind-merge";
import { code } from "../markdown/code";
import toast from "#/utils/toast";
interface MessageProps {
message: Message;
}
function ChatMessage({ message }: MessageProps) {
// const text = useTyping(message.content);
const [isHovering, setIsHovering] = useState(false);
const className = twMerge(
"markdown-body",
"p-3 text-white max-w-[90%] overflow-y-auto rounded-lg",
"p-3 text-white max-w-[90%] overflow-y-auto rounded-lg relative",
message.sender === "user" ? "bg-neutral-700 self-end" : "bg-neutral-500",
);
const copyToClipboard = () => {
navigator.clipboard
.writeText(message.content)
.then(() => {
toast.info("Message copied to clipboard!");
})
.catch((error) => {
toast.error("copy-error", `Failed to copy message: ${error}`);
});
};
return (
<div data-testid="message" className={className}>
<div
data-testid="message"
className={className}
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
>
{isHovering && (
<button
onClick={copyToClipboard}
className="absolute top-1 right-1 p-1 bg-neutral-600 rounded hover:bg-neutral-500 transition-opacity opacity-75 hover:opacity-100"
aria-label="Copy message"
type="button"
>
<FaClipboard />
</button>
)}
<Markdown components={{ code }}>{message.content}</Markdown>
</div>
);