diff --git a/src/components/ChatMessage.tsx b/src/components/ChatMessage.tsx index dc1c9e4..0f61cd9 100644 --- a/src/components/ChatMessage.tsx +++ b/src/components/ChatMessage.tsx @@ -1,4 +1,5 @@ +import { useState, useCallback } from 'react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import rehypeHighlight from 'rehype-highlight'; @@ -7,7 +8,7 @@ import { ThinkingBlock } from './ThinkingBlock'; import { CodeBlock } from './CodeBlock'; import { ToolCall } from './ToolCall'; import { ImageBlock, buildImageSrc } from './ImageBlock'; -import { Bot, User, Wrench } from 'lucide-react'; +import { Bot, User, Wrench, Copy, Check } from 'lucide-react'; import { t, getLocale } from '../lib/i18n'; import { useLocale } from '../hooks/useLocale'; // ChevronDown, ChevronRight, Wrench still used by InternalOnlyMessage @@ -212,6 +213,35 @@ function InternalOnlyMessage({ message }: { message: ChatMessageType }) { ); } +function CopyButton({ text }: { text: string }) { + const [copied, setCopied] = useState(false); + const handleCopy = useCallback(() => { + navigator.clipboard.writeText(text).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }); + }, [text]); + + return ( + + ); +} + +/** Extract plain text from message blocks for clipboard copy */ +function getPlainText(message: ChatMessageType): string { + if (message.blocks.length > 0) { + return getTextBlocks(message.blocks).map(b => (b as any).text).join('\n\n'); + } + return message.content; +} + export function ChatMessageComponent({ message }: { message: ChatMessageType }) { useLocale(); // re-render on locale change const isUser = message.role === 'user'; @@ -238,11 +268,15 @@ export function ChatMessageComponent({ message }: { message: ChatMessageType }) {/* Bubble */}