feat: Message copy button (#171)
* Add copy button * Fix copy button not copying the entire message * fix style * remove prewrap --------- Co-authored-by: Mckay Wrigley <mckaywrigley@gmail.com>
This commit is contained in:
		
							parent
							
								
									fffb729b34
								
							
						
					
					
						commit
						14fe29c03a
					
				|  | @ -1,10 +1,11 @@ | ||||||
| import { Message } from "@/types"; | import { Message } from "@/types"; | ||||||
| import { IconEdit } from "@tabler/icons-react"; | import { IconEdit } from "@tabler/icons-react"; | ||||||
| import { FC, useEffect, useRef, useState } from "react"; |  | ||||||
| import { useTranslation } from "next-i18next"; | import { useTranslation } from "next-i18next"; | ||||||
|  | import { FC, useEffect, useRef, useState } from "react"; | ||||||
| import ReactMarkdown from "react-markdown"; | import ReactMarkdown from "react-markdown"; | ||||||
| import remarkGfm from "remark-gfm"; | import remarkGfm from "remark-gfm"; | ||||||
| import { CodeBlock } from "../Markdown/CodeBlock"; | import { CodeBlock } from "../Markdown/CodeBlock"; | ||||||
|  | import { CopyButton } from "./CopyButton"; | ||||||
| 
 | 
 | ||||||
| interface Props { | interface Props { | ||||||
|   message: Message; |   message: Message; | ||||||
|  | @ -14,10 +15,11 @@ interface Props { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const ChatMessage: FC<Props> = ({ message, messageIndex, lightMode, onEditMessage }) => { | export const ChatMessage: FC<Props> = ({ message, messageIndex, lightMode, onEditMessage }) => { | ||||||
|   const { t } = useTranslation('chat'); |   const { t } = useTranslation("chat"); | ||||||
|   const [isEditing, setIsEditing] = useState<boolean>(false); |   const [isEditing, setIsEditing] = useState<boolean>(false); | ||||||
|   const [isHovering, setIsHovering] = useState<boolean>(false); |   const [isHovering, setIsHovering] = useState<boolean>(false); | ||||||
|   const [messageContent, setMessageContent] = useState(message.content); |   const [messageContent, setMessageContent] = useState(message.content); | ||||||
|  |   const [messagedCopied, setMessageCopied] = useState(false); | ||||||
| 
 | 
 | ||||||
|   const textareaRef = useRef<HTMLTextAreaElement>(null); |   const textareaRef = useRef<HTMLTextAreaElement>(null); | ||||||
| 
 | 
 | ||||||
|  | @ -47,6 +49,17 @@ export const ChatMessage: FC<Props> = ({ message, messageIndex, lightMode, onEdi | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|  |   const copyOnClick = () => { | ||||||
|  |     if (!navigator.clipboard) return; | ||||||
|  | 
 | ||||||
|  |     navigator.clipboard.writeText(message.content).then(() => { | ||||||
|  |       setMessageCopied(true); | ||||||
|  |       setTimeout(() => { | ||||||
|  |         setMessageCopied(false); | ||||||
|  |       }, 2000); | ||||||
|  |     }); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     if (textareaRef.current) { |     if (textareaRef.current) { | ||||||
|       textareaRef.current.style.height = "inherit"; |       textareaRef.current.style.height = "inherit"; | ||||||
|  | @ -119,7 +132,9 @@ export const ChatMessage: FC<Props> = ({ message, messageIndex, lightMode, onEdi | ||||||
|               )} |               )} | ||||||
|             </div> |             </div> | ||||||
|           ) : ( |           ) : ( | ||||||
|  |             <> | ||||||
|               <ReactMarkdown |               <ReactMarkdown | ||||||
|  |                 className="prose dark:prose-invert" | ||||||
|                 remarkPlugins={[remarkGfm]} |                 remarkPlugins={[remarkGfm]} | ||||||
|                 components={{ |                 components={{ | ||||||
|                   code({ node, inline, className, children, ...props }) { |                   code({ node, inline, className, children, ...props }) { | ||||||
|  | @ -154,6 +169,14 @@ export const ChatMessage: FC<Props> = ({ message, messageIndex, lightMode, onEdi | ||||||
|               > |               > | ||||||
|                 {message.content} |                 {message.content} | ||||||
|               </ReactMarkdown> |               </ReactMarkdown> | ||||||
|  | 
 | ||||||
|  |               {(isHovering || window.innerWidth < 640) && ( | ||||||
|  |                 <CopyButton | ||||||
|  |                   messagedCopied={messagedCopied} | ||||||
|  |                   copyOnClick={copyOnClick} | ||||||
|  |                 /> | ||||||
|  |               )} | ||||||
|  |             </> | ||||||
|           )} |           )} | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|  | @ -0,0 +1,24 @@ | ||||||
|  | import { IconCheck, IconCopy } from "@tabler/icons-react"; | ||||||
|  | import { FC } from "react"; | ||||||
|  | 
 | ||||||
|  | type Props = { | ||||||
|  |   messagedCopied: boolean; | ||||||
|  |   copyOnClick: () => void; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const CopyButton: FC<Props> = ({ messagedCopied, copyOnClick }) => ( | ||||||
|  |   <button className={`absolute ${window.innerWidth < 640 ? "right-3 bottom-1" : "right-[-20px] top-[26px] m-0"}`}> | ||||||
|  |     {messagedCopied ? ( | ||||||
|  |       <IconCheck | ||||||
|  |         size={20} | ||||||
|  |         className="text-green-500 dark:text-green-400" | ||||||
|  |       /> | ||||||
|  |     ) : ( | ||||||
|  |       <IconCopy | ||||||
|  |         size={20} | ||||||
|  |         className="text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300" | ||||||
|  |         onClick={copyOnClick} | ||||||
|  |       /> | ||||||
|  |     )} | ||||||
|  |   </button> | ||||||
|  | ); | ||||||
		Loading…
	
		Reference in New Issue