diff --git a/src/components/chat/ChatMessage.tsx b/src/components/chat/ChatMessage.tsx index f254f86fc2..4c1b76199c 100644 --- a/src/components/chat/ChatMessage.tsx +++ b/src/components/chat/ChatMessage.tsx @@ -21,13 +21,15 @@ import { formatDistanceToNow, format } from "date-fns"; import { useVersions } from "@/hooks/useVersions"; import { useAtomValue } from "jotai"; import { selectedAppIdAtom } from "@/atoms/appAtoms"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { useMemo } from "react"; import { useCopyToClipboard } from "@/hooks/useCopyToClipboard"; +import { calculateVersionNumber } from "./versionUtils"; import { Tooltip, TooltipTrigger, TooltipContent, } from "@/components/ui/tooltip"; +import { CopyButton } from "@/components/ui/copy-button"; import { unescapeXmlAttr } from "../../../shared/xmlEscape"; import { isCancelledResponseContent, @@ -123,16 +125,15 @@ const ChatMessage = ({ return null; }, [message.commitHash, message.role, liveVersions]); - // handle copy request id - const [copiedRequestId, setCopiedRequestId] = useState(false); - const copiedRequestIdTimeoutRef = useRef(null); - useEffect(() => { - return () => { - if (copiedRequestIdTimeoutRef.current) { - clearTimeout(copiedRequestIdTimeoutRef.current); - } - }; - }, []); + // Calculate version number (reverse index: newest = 1, older = 2, 3, etc.) + const versionNumber = useMemo(() => { + if (messageVersion && liveVersions.length) { + return calculateVersionNumber(messageVersion, liveVersions); + } + return null; + }, [messageVersion, liveVersions]); + + // handle copy request id and commit hash using CopyButton // Format the message timestamp const formatTimestamp = (timestamp: string | Date) => { @@ -284,9 +285,9 @@ const ChatMessage = ({ {formatTimestamp(message.createdAt)} {messageVersion && messageVersion.message && ( -
- - {messageVersion && messageVersion.message && ( +
+
+ +
+ {versionNumber && ( + )}
)} {message.requestId && ( - - { - if (!message.requestId) return; - navigator.clipboard - .writeText(message.requestId) - .then(() => { - setCopiedRequestId(true); - if (copiedRequestIdTimeoutRef.current) { - clearTimeout(copiedRequestIdTimeoutRef.current); - } - copiedRequestIdTimeoutRef.current = setTimeout( - () => setCopiedRequestId(false), - 2000, - ); - }) - .catch(() => { - // noop - }); - }} - aria-label="Copy Request ID" - className="flex items-center space-x-1 px-1 py-0.5 hover:bg-gray-100 dark:hover:bg-gray-800 rounded transition-colors duration-200 cursor-pointer" - /> - } - > - {copiedRequestId ? ( - - ) : ( - - )} - - {copiedRequestId ? "Copied" : "Request ID"} - - - - {copiedRequestId - ? "Copied!" - : `Copy Request ID: ${message.requestId.slice(0, 8)}...`} - - + )} {isLastMessage && message.totalTokens && (
Version{" "} {" "} ( diff --git a/src/components/chat/versionUtils.ts b/src/components/chat/versionUtils.ts new file mode 100644 index 0000000000..8ee8e53bba --- /dev/null +++ b/src/components/chat/versionUtils.ts @@ -0,0 +1,8 @@ +//Calculate the version number from a version object using reverse index. + +export function calculateVersionNumber( + version: { oid: string }, + versions: { oid: string }[], +): number { + return versions.length - versions.indexOf(version); +} diff --git a/src/components/ui/copy-button.tsx b/src/components/ui/copy-button.tsx new file mode 100644 index 0000000000..8efbf31006 --- /dev/null +++ b/src/components/ui/copy-button.tsx @@ -0,0 +1,60 @@ +import { Copy, Check } from "lucide-react"; +import { useCopyToClipboard } from "@/hooks/useCopyToClipboard"; +import { + Tooltip, + TooltipTrigger, + TooltipContent, +} from "@/components/ui/tooltip"; + +interface CopyButtonProps { + value: string; + ariaLabel: string; + displayText: string; + tooltipText: string; + mono?: boolean; + raw?: boolean; +} + +export function CopyButton({ + value, + ariaLabel, + displayText, + tooltipText, + mono = false, + raw = false, +}: CopyButtonProps) { + const { copyMessageContent, copyRawText, copied } = useCopyToClipboard(); + + const handleCopy = () => { + if (!value) return; + if (raw) { + copyRawText(value); + } else { + copyMessageContent(value); + } + }; + + return ( + + + } + > + {copied ? ( + + ) : ( + + )} + + {copied ? "Copied" : displayText} + + + {copied ? "Copied!" : tooltipText} + + ); +} diff --git a/src/hooks/useCopyToClipboard.ts b/src/hooks/useCopyToClipboard.ts index 10929ff2c4..dff301eecb 100644 --- a/src/hooks/useCopyToClipboard.ts +++ b/src/hooks/useCopyToClipboard.ts @@ -50,6 +50,25 @@ export const useCopyToClipboard = () => { } }; + const copyRawText = async (text: string) => { + try { + await navigator.clipboard.writeText(text); + + setCopied(true); + // Clear existing timeout if any + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + + // Set new timeout and store reference + timeoutRef.current = setTimeout(() => setCopied(false), 2000); + return true; + } catch (error) { + console.error("Failed to copy text:", error); + return false; + } + }; + // Convert Dyad content to clean markdown using the same parsing logic as DyadMarkdownParser const convertDyadContentToMarkdown = (content: string): string => { if (!content) return ""; @@ -273,5 +292,5 @@ export const useCopyToClipboard = () => { return { processedContent }; }; - return { copyMessageContent, copied }; + return { copyMessageContent, copyRawText, copied }; };