Skip to content

Commit

Permalink
Merge pull request #32 from iamgreggarcia/copy-to-clipboard
Browse files Browse the repository at this point in the history
Add: copy-to-clipboard, download
  • Loading branch information
iamgreggarcia authored Jul 16, 2023
2 parents 6b7ea6b + 96e94b8 commit 702899f
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 3 deletions.
27 changes: 26 additions & 1 deletion frontend/src/components/chat-message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ type MessageProps = {
lastMessage: Message;
streamingMessageIndex?: number;
isCurrentMessage: boolean;
messageIndex: number;
hoveredMessageIndex: number | null;
setHoveredMessageIndex: React.Dispatch<React.SetStateAction<number | null>>;
};

type AvatarProps = {
Expand Down Expand Up @@ -87,7 +90,16 @@ function checkContent(content: string) {
return content.includes('{"function_call":') || content.includes('result:');
}

const ChatMessage: React.FC<MessageProps> = ({ message, isStreaming, isCurrentMessage, streamingMessageIndex, lastMessage }) => {
const ChatMessage: React.FC<MessageProps> = ({ message, isStreaming, isCurrentMessage, streamingMessageIndex, lastMessage, messageIndex, hoveredMessageIndex, setHoveredMessageIndex }) => {
const [copySuccess, setCopySuccess] = useState(false);
const copyToClipboard = () => {
if (message.content) {
navigator.clipboard.writeText(message.content).then(() => {
setCopySuccess(true);
setTimeout(() => setCopySuccess(false), 2000);
});
}
};
let content = message.content;
if (isStreaming && isCurrentMessage && message.role !== 'user') {
// Append the cursor character at the end of the streaming text
Expand All @@ -101,6 +113,8 @@ const ChatMessage: React.FC<MessageProps> = ({ message, isStreaming, isCurrentMe
: 'border-0 border-black/15 bg-slate-100 dark:border-gray-900/50 dark:bg-gray-700 dark:text-white'
}`}
style={{ overflowWrap: 'anywhere' }}
onMouseEnter={() => setHoveredMessageIndex(messageIndex)}
onMouseLeave={() => setHoveredMessageIndex(null)}
>
<div className="relative m-auto flex p-4 md:max-w-2xl md:gap-6 md:py-6 lg:max-w-2xl lg:px-0 xl:max-w-3xl">
<div className="min-w-[40px] text-right font-bold mr-5 sm:mr-4">
Expand All @@ -110,6 +124,17 @@ const ChatMessage: React.FC<MessageProps> = ({ message, isStreaming, isCurrentMe
<Avatar role={message.role} lastMessageRole={lastMessage.role} />}
</div>

<button onClick={copyToClipboard} className="w-5 h-5 hover:text-gray-200 text-gray-300 absolute right-1 top-1 transition-all duration-200" style={{ opacity: hoveredMessageIndex === messageIndex ? 1 : 0, visibility: hoveredMessageIndex === messageIndex ? 'visible' : 'hidden' }}>
{copySuccess ? (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5 text-green-500">
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184" />
</svg>
)}
</button>

<div className=" mt-[-2px] w-full">
{content && (
Expand Down
37 changes: 36 additions & 1 deletion frontend/src/components/codeblock.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FC, memo } from 'react'
import { FC, memo, useState } from 'react'
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
import { coldarkDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'

Expand Down Expand Up @@ -50,11 +50,46 @@ export const generateRandomString = (length: number, lowercase = false) => {
}

const CodeBlock: FC<Props> = memo(({ language, value, isStreaming, isCurrentMessage }) => {
const [copySuccess, setCopySuccess] = useState(false);
const copyToClipboard = () => {
navigator.clipboard.writeText(value).then(() => {
setCopySuccess(true);
setTimeout(() => setCopySuccess(false), 2000);
});
};


const downloadFile = () => {
const element = document.createElement("a");
const file = new Blob([value], { type: 'text/plain' });
element.href = URL.createObjectURL(file);
element.download = `${generateRandomString(10)}${programmingLanguages[language]}`;
document.body.appendChild(element); // Required for this to work in FireFox
element.click();
};

return (
<div className="codeblock relative w-full bg-zinc-950 font-sans shadow-lg">
<div className="flex w-full items-center justify-between bg-zinc-800 px-6 py-2 pr-4 text-zinc-50">
<span className="text-xs lowercase">{language}</span>
<div className="flex space-x-2">
<button onClick={downloadFile} className="w-5 h-5 hover:text-gray-300 text-gray-500 absolute top-1 right-8 transition-all duration-200">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
</button>
<button onClick={copyToClipboard} className="w-5 h-5 hover:text-gray-300 text-gray-500 absolute right-1 top-1 transition-all duration-200">
{copySuccess ? (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5 text-green-500">
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184" />
</svg>
)}
</button>
</div>
</div>
<SyntaxHighlighter
language={language}
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/pages/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export default function Chat() {
const [uploadedFileName, setUploadedFileName] = useState<string | null>(null);
const [fileIsAttached, setFileIsAttached] = useState<boolean>(false);
const [resubmitLastMessage, setResubmitLastMessage] = useState(false);
const [hoveredMessageIndex, setHoveredMessageIndex] = useState<number | null>(null);

const isMobile = () => {
const userAgent =
Expand Down Expand Up @@ -213,7 +214,7 @@ export default function Chat() {
const functionCallIndex = assistantMessageContent.indexOf('{"function_call":');
if (functionCallIndex !== -1) {
const functionCallStr = assistantMessageContent.slice(functionCallIndex);
console.log('functionCallStr: ', functionCallStr )
console.log('functionCallStr: ', functionCallStr)
const parsed = JSON.parse(functionCallStr);
let functionName = parsed.function_call.name
let functionArgumentsStr = parsed.function_call.arguments;
Expand Down Expand Up @@ -333,6 +334,9 @@ export default function Chat() {
streamingMessageIndex={messages.length - 1}
isCurrentMessage={index === messages.length - 1}
lastMessage={lastMessage as Message}
messageIndex={index}
hoveredMessageIndex={hoveredMessageIndex}
setHoveredMessageIndex={setHoveredMessageIndex}
/>
)
);
Expand Down

0 comments on commit 702899f

Please sign in to comment.