mukaddamzaid commited on
Commit
55947a0
·
1 Parent(s): 2517ed6

feat: add CopyButton component and integrate copy functionality into messages

Browse files

- Introduced a new CopyButton component for copying text to the clipboard.
- Implemented a custom hook, useCopy, to manage clipboard interactions and state.
- Integrated the CopyButton into the message component, allowing users to copy assistant messages easily.
- Enhanced message display logic to conditionally show the copy button based on message role and status.

app/api/chat/route.ts CHANGED
@@ -56,7 +56,7 @@ export async function POST(req: Request) {
56
 
57
  // Check if chat already exists for the given ID
58
  // If not, we'll create it in onFinish
59
- var isNewChat = false;
60
  if (chatId) {
61
  try {
62
  const existingChat = await db.query.chats.findFirst({
 
56
 
57
  // Check if chat already exists for the given ID
58
  // If not, we'll create it in onFinish
59
+ let isNewChat = false;
60
  if (chatId) {
61
  try {
62
  const existingChat = await db.query.chats.findFirst({
components/copy-button.tsx ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { CheckIcon, CopyIcon } from "lucide-react";
2
+ import { cn } from "@/lib/utils";
3
+ import { useCopy } from "@/lib/hooks/use-copy";
4
+ import { Button } from "./ui/button";
5
+
6
+ interface CopyButtonProps {
7
+ text: string;
8
+ className?: string;
9
+ }
10
+
11
+ export function CopyButton({ text, className }: CopyButtonProps) {
12
+ const { copied, copy } = useCopy();
13
+
14
+ return (
15
+ <Button
16
+ variant="ghost"
17
+ size="sm"
18
+ className={cn(
19
+ "transition-opacity opacity-0 group-hover/message:opacity-100 gap-1.5",
20
+ className
21
+ )}
22
+ onClick={() => copy(text)}
23
+ title="Copy to clipboard"
24
+ >
25
+ {copied ? (
26
+ <>
27
+ <CheckIcon className="h-4 w-4" />
28
+ <span className="text-xs">Copied!</span>
29
+ </>
30
+ ) : (
31
+ <>
32
+ <CopyIcon className="h-4 w-4" />
33
+ <span className="text-xs">Copy</span>
34
+ </>
35
+ )}
36
+ </Button>
37
+ );
38
+ }
components/message.tsx CHANGED
@@ -9,6 +9,7 @@ import { cn } from "@/lib/utils";
9
  import { ChevronDownIcon, ChevronUpIcon, LightbulbIcon } from "lucide-react";
10
  import { SpinnerIcon } from "./icons";
11
  import { ToolInvocation } from "./tool-invocation";
 
12
 
13
  interface ReasoningPart {
14
  type: "reasoning";
@@ -102,6 +103,18 @@ const PurePreviewMessage = ({
102
  status: "error" | "submitted" | "streaming" | "ready";
103
  isLatestMessage: boolean;
104
  }) => {
 
 
 
 
 
 
 
 
 
 
 
 
105
  return (
106
  <AnimatePresence key={message.id}>
107
  <motion.div
@@ -174,6 +187,11 @@ const PurePreviewMessage = ({
174
  return null;
175
  }
176
  })}
 
 
 
 
 
177
  </div>
178
  </div>
179
  </motion.div>
 
9
  import { ChevronDownIcon, ChevronUpIcon, LightbulbIcon } from "lucide-react";
10
  import { SpinnerIcon } from "./icons";
11
  import { ToolInvocation } from "./tool-invocation";
12
+ import { CopyButton } from "./copy-button";
13
 
14
  interface ReasoningPart {
15
  type: "reasoning";
 
103
  status: "error" | "submitted" | "streaming" | "ready";
104
  isLatestMessage: boolean;
105
  }) => {
106
+ // Create a string with all text parts for copy functionality
107
+ const getMessageText = () => {
108
+ if (!message.parts) return "";
109
+ return message.parts
110
+ .filter(part => part.type === "text")
111
+ .map(part => (part.type === "text" ? part.text : ""))
112
+ .join("\n\n");
113
+ };
114
+
115
+ // Only show copy button if the message is from the assistant and not currently streaming
116
+ const shouldShowCopyButton = message.role === "assistant" && (!isLatestMessage || status !== "streaming");
117
+
118
  return (
119
  <AnimatePresence key={message.id}>
120
  <motion.div
 
187
  return null;
188
  }
189
  })}
190
+ {shouldShowCopyButton && (
191
+ <div className="flex justify-start mt-2">
192
+ <CopyButton text={getMessageText()} />
193
+ </div>
194
+ )}
195
  </div>
196
  </div>
197
  </motion.div>
lib/hooks/use-copy.ts ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useCallback } from 'react';
2
+
3
+ export function useCopy(timeout = 2000) {
4
+ const [copied, setCopied] = useState(false);
5
+
6
+ const copy = useCallback(
7
+ async (text: string) => {
8
+ if (!navigator.clipboard) {
9
+ console.error('Clipboard API not available');
10
+ return false;
11
+ }
12
+
13
+ try {
14
+ await navigator.clipboard.writeText(text);
15
+ setCopied(true);
16
+
17
+ setTimeout(() => {
18
+ setCopied(false);
19
+ }, timeout);
20
+
21
+ return true;
22
+ } catch (error) {
23
+ console.error('Failed to copy text:', error);
24
+ return false;
25
+ }
26
+ },
27
+ [timeout]
28
+ );
29
+
30
+ return { copied, copy };
31
+ }