diff --git a/ui/desktop/src/components/ChatView.tsx b/ui/desktop/src/components/ChatView.tsx index c1143081..daf9e6d3 100644 --- a/ui/desktop/src/components/ChatView.tsx +++ b/ui/desktop/src/components/ChatView.tsx @@ -235,7 +235,7 @@ export default function ChatView({ {messages.length === 0 ? ( append(createUserMessage(text))} /> ) : ( - + {filteredMessages.map((message, index) => (
{isUserMessage(message) ? ( diff --git a/ui/desktop/src/components/GooseMessage.tsx b/ui/desktop/src/components/GooseMessage.tsx index f41159ad..618b0dbf 100644 --- a/ui/desktop/src/components/GooseMessage.tsx +++ b/ui/desktop/src/components/GooseMessage.tsx @@ -12,6 +12,7 @@ import { getToolConfirmationContent, } from '../types/message'; import ToolCallConfirmation from './ToolCallConfirmation'; +import CopyButton from './ui/CopyButton'; interface GooseMessageProps { message: Message; @@ -67,9 +68,17 @@ export default function GooseMessage({ message, metadata, messages, append }: Go {/* Always show the top content area if there are tool calls, even if textContent is empty */} {(textContent || toolRequests.length > 0) && (
0 ? 'rounded-b-none' : ''}`} + className={`goose-message-content bg-bgSubtle rounded-2xl px-4 py-2 ${toolRequests.length > 0 ? 'rounded-b-none' : ''} relative group`} > {textContent ? : null} + {/* Only show CopyButton if there's text content and no tool requests/responses */} + {textContent && message.content.every((content) => content.type === 'text') && ( + + )}
)} diff --git a/ui/desktop/src/components/MarkdownContent.tsx b/ui/desktop/src/components/MarkdownContent.tsx index ad691f5f..6288e3cd 100644 --- a/ui/desktop/src/components/MarkdownContent.tsx +++ b/ui/desktop/src/components/MarkdownContent.tsx @@ -1,12 +1,11 @@ -import React, { useState } from 'react'; +import React from 'react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import rehypeRaw from 'rehype-raw'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism'; -import { oneLight } from 'react-syntax-highlighter/dist/cjs/styles/prism'; -import { Check, Copy } from './icons'; import { visit } from 'unist-util-visit'; +import CopyButton from './ui/CopyButton'; const UrlTransform = { a: ({ node, ...props }) => , @@ -31,28 +30,14 @@ interface MarkdownContentProps { } const CodeBlock = ({ language, children }: { language: string; children: string }) => { - const [copied, setCopied] = useState(false); - const handleCopy = async () => { - try { - await navigator.clipboard.writeText(children); - setCopied(true); - setTimeout(() => setCopied(false), 2000); // Reset after 2 seconds - } catch (err) { - console.error('Failed to copy text: ', err); - } - }; - return (
- + />
-
+
+
{/* TODO(alexhancock): Re-enable link previews once styled well again */} diff --git a/ui/desktop/src/components/ui/CopyButton.tsx b/ui/desktop/src/components/ui/CopyButton.tsx new file mode 100644 index 00000000..0b6759e0 --- /dev/null +++ b/ui/desktop/src/components/ui/CopyButton.tsx @@ -0,0 +1,36 @@ +import React, { useState } from 'react'; +import { Check, Copy } from '../icons'; + +interface CopyButtonProps { + text: string; + className?: string; + iconClassName?: string; + lightIcon?: boolean; +} + +export default function CopyButton({ + text, + className = 'absolute bottom-2 right-2 p-1.5 rounded-lg bg-gray-700/50 text-gray-300 opacity-0 group-hover:opacity-100 transition-opacity duration-200 hover:bg-gray-600/50 hover:text-gray-100', + iconClassName = 'h-4 w-4', + lightIcon = false, +}: CopyButtonProps) { + const [copied, setCopied] = useState(false); + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 2000); // Reset after 2 seconds + } catch (err) { + console.error('Failed to copy text: ', err); + } + }; + + const Icon = copied ? Check : Copy; + + return ( + + ); +}