mirror of
https://github.com/aljazceru/goose.git
synced 2026-01-02 14:04:27 +01:00
feat: copy message content (#1511)
Co-authored-by: Nahiyan Khan <nahiyan@squareup.com>
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
"license": {
|
||||
"name": "Apache-2.0"
|
||||
},
|
||||
"version": "1.0.11"
|
||||
"version": "1.0.12"
|
||||
},
|
||||
"paths": {
|
||||
"/config": {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useMemo, useRef } from 'react';
|
||||
import LinkPreview from './LinkPreview';
|
||||
import GooseResponseForm from './GooseResponseForm';
|
||||
import { extractUrls } from '../utils/urlUtils';
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
getToolConfirmationContent,
|
||||
} from '../types/message';
|
||||
import ToolCallConfirmation from './ToolCallConfirmation';
|
||||
import MessageCopyLink from './MessageCopyLink';
|
||||
|
||||
interface GooseMessageProps {
|
||||
message: Message;
|
||||
@@ -21,6 +22,8 @@ interface GooseMessageProps {
|
||||
}
|
||||
|
||||
export default function GooseMessage({ message, metadata, messages, append }: GooseMessageProps) {
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Extract text content from the message
|
||||
let textContent = getTextContent(message);
|
||||
|
||||
@@ -66,10 +69,20 @@ export default function GooseMessage({ message, metadata, messages, append }: Go
|
||||
<div className="flex flex-col w-full">
|
||||
{/* Always show the top content area if there are tool calls, even if textContent is empty */}
|
||||
{(textContent || toolRequests.length > 0) && (
|
||||
<div
|
||||
className={`goose-message-content bg-bgSubtle rounded-2xl px-4 py-2 ${toolRequests.length > 0 ? 'rounded-b-none' : ''}`}
|
||||
>
|
||||
{textContent ? <MarkdownContent content={textContent} /> : null}
|
||||
<div className="flex flex-col group">
|
||||
<div
|
||||
className={`goose-message-content bg-bgSubtle rounded-2xl px-4 py-2 ${toolRequests.length > 0 ? 'rounded-b-none' : ''}`}
|
||||
>
|
||||
<div ref={contentRef}>
|
||||
{textContent ? <MarkdownContent content={textContent} /> : null}
|
||||
</div>
|
||||
</div>
|
||||
{/* Only show MessageCopyLink if there's text content and no tool requests/responses */}
|
||||
{textContent && message.content.every((content) => content.type === 'text') && (
|
||||
<div className="flex justify-end mr-2">
|
||||
<MessageCopyLink text={textContent} contentRef={contentRef} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
60
ui/desktop/src/components/MessageCopyLink.tsx
Normal file
60
ui/desktop/src/components/MessageCopyLink.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
/* global Blob, ClipboardItem */
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Copy } from './icons';
|
||||
|
||||
interface MessageCopyLinkProps {
|
||||
text: string;
|
||||
contentRef: React.RefObject<HTMLElement>;
|
||||
}
|
||||
|
||||
export default function MessageCopyLink({ text, contentRef }: MessageCopyLinkProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
if (contentRef?.current) {
|
||||
// Create a temporary container to handle HTML content
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = contentRef.current.innerHTML;
|
||||
|
||||
// Clean up any copy buttons from the content
|
||||
const copyButtons = container.querySelectorAll('button');
|
||||
copyButtons.forEach((button) => button.remove());
|
||||
|
||||
// Create the clipboard data
|
||||
const clipboardData = new ClipboardItem({
|
||||
'text/plain': new Blob([text], { type: 'text/plain' }),
|
||||
'text/html': new Blob([container.innerHTML], { type: 'text/html' }),
|
||||
});
|
||||
|
||||
await navigator.clipboard.write([clipboardData]);
|
||||
} else {
|
||||
await navigator.clipboard.writeText(text);
|
||||
}
|
||||
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000); // Reset after 2 seconds
|
||||
} catch (err) {
|
||||
console.error('Failed to copy text: ', err);
|
||||
// Fallback to plain text if HTML copy fails
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch (fallbackErr) {
|
||||
console.error('Failed to copy text (fallback): ', fallbackErr);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="flex items-center gap-1 text-xs text-textSubtle hover:text-textProminent transition-all duration-200 mt-1 opacity-0 transform -translate-y-2 group-hover:translate-y-0 group-hover:opacity-100"
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
<span>{copied ? 'Copied!' : 'Copy'}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
import React from 'react';
|
||||
import React, { useRef } from 'react';
|
||||
import LinkPreview from './LinkPreview';
|
||||
import { extractUrls } from '../utils/urlUtils';
|
||||
import MarkdownContent from './MarkdownContent';
|
||||
import { Message, getTextContent } from '../types/message';
|
||||
import MessageCopyLink from './MessageCopyLink';
|
||||
|
||||
interface UserMessageProps {
|
||||
message: Message;
|
||||
}
|
||||
|
||||
export default function UserMessage({ message }: UserMessageProps) {
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Extract text content from the message
|
||||
const textContent = getTextContent(message);
|
||||
|
||||
@@ -18,8 +21,15 @@ export default function UserMessage({ message }: UserMessageProps) {
|
||||
return (
|
||||
<div className="flex justify-end mt-[16px] w-full opacity-0 animate-[appear_150ms_ease-in_forwards]">
|
||||
<div className="flex-col max-w-[85%]">
|
||||
<div className="flex bg-slate text-white rounded-xl rounded-br-none py-2 px-3">
|
||||
<MarkdownContent content={textContent} className="text-white" />
|
||||
<div className="flex flex-col group">
|
||||
<div className="flex bg-slate text-white rounded-xl rounded-br-none py-2 px-3">
|
||||
<div ref={contentRef}>
|
||||
<MarkdownContent content={textContent} className="text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<MessageCopyLink text={textContent} contentRef={contentRef} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TODO(alexhancock): Re-enable link previews once styled well again */}
|
||||
|
||||
Reference in New Issue
Block a user