this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add copy action to chat messages

+78 -7
+17
apps/web/src/components/chat-message-actions.tsx
··· 1 + import { CopyButton } from "./copy-button"; 2 + import { Row } from "./layout"; 3 + 4 + interface ChatMessageActionsProps { 5 + text: string; 6 + } 7 + 8 + export function ChatMessageActions({ text }: ChatMessageActionsProps) { 9 + return ( 10 + <Row 11 + gap="xs" 12 + className="opacity-0 transition-opacity duration-150 group-focus-within:opacity-100 group-hover:opacity-100" 13 + > 14 + <CopyButton text={text} /> 15 + </Row> 16 + ); 17 + }
+19 -7
apps/web/src/components/chat-message.tsx
··· 1 1 import { cva, type VariantProps } from "class-variance-authority"; 2 2 import Markdown from "react-markdown"; 3 3 import { cn } from "#/lib/utils"; 4 + import { ChatMessageActions } from "./chat-message-actions"; 5 + import { Stack } from "./layout"; 4 6 import { TypingIndicator } from "./typing-indicator"; 5 7 6 8 const chatMessage = cva( ··· 27 29 const isTyping = author === "assistant" && text.length === 0; 28 30 29 31 return ( 30 - <div 32 + <Stack 33 + gap="xs" 31 34 className={cn( 32 - chatMessage({ author }), 33 - !isTyping && "markdown", 34 - author === "assistant" ? "self-start" : "self-end", 35 - className, 35 + "group w-full", 36 + author === "assistant" 37 + ? "items-start self-start" 38 + : "items-end self-end", 36 39 )} 37 40 > 38 - {isTyping ? <TypingIndicator /> : <Markdown>{text}</Markdown>} 39 - </div> 41 + <div 42 + className={cn( 43 + chatMessage({ author }), 44 + !isTyping && "markdown", 45 + className, 46 + )} 47 + > 48 + {isTyping ? <TypingIndicator /> : <Markdown>{text}</Markdown>} 49 + </div> 50 + {!isTyping && text && <ChatMessageActions text={text} />} 51 + </Stack> 40 52 ); 41 53 }
+42
apps/web/src/components/copy-button.tsx
··· 1 + import { CheckIcon, CopyIcon } from "lucide-react"; 2 + import { useEffect, useState } from "react"; 3 + import { Button } from "./ui/button"; 4 + 5 + interface CopyButtonProps { 6 + text: string; 7 + } 8 + 9 + export function CopyButton({ text }: CopyButtonProps) { 10 + const [isCopied, setIsCopied] = useState(false); 11 + 12 + useEffect(() => { 13 + if (!isCopied) return; 14 + 15 + const timeoutId = window.setTimeout(() => { 16 + setIsCopied(false); 17 + }, 1500); 18 + 19 + return () => window.clearTimeout(timeoutId); 20 + }, [isCopied]); 21 + 22 + async function handleCopy() { 23 + if (!text) return; 24 + 25 + await navigator.clipboard.writeText(text); 26 + setIsCopied(true); 27 + } 28 + 29 + return ( 30 + <Button 31 + type="button" 32 + size="icon" 33 + variant="ghost" 34 + onClick={handleCopy} 35 + className="text-muted-foreground hover:text-foreground" 36 + aria-label={isCopied ? "Copied message" : "Copy message"} 37 + title={isCopied ? "Copied" : "Copy message"} 38 + > 39 + {isCopied ? <CheckIcon /> : <CopyIcon />} 40 + </Button> 41 + ); 42 + }