import { useSWRConfig } from 'swr'; import { useCopyToClipboard } from 'usehooks-ts'; import type { Vote } from '@/lib/db/schema'; import { CopyIcon, ThumbDownIcon, ThumbUpIcon, PencilEditIcon } from './icons'; import { Actions, Action } from './elements/actions'; import { memo } from 'react'; import equal from 'fast-deep-equal'; import { toast } from 'sonner'; import type { ChatMessage } from '@/lib/types'; export function PureMessageActions({ chatId, message, vote, isLoading, setMode, }: { chatId: string; message: ChatMessage; vote: Vote | undefined; isLoading: boolean; setMode?: (mode: 'view' | 'edit') => void; }) { const { mutate } = useSWRConfig(); const [_, copyToClipboard] = useCopyToClipboard(); if (isLoading) return null; const textFromParts = message.parts ?.filter((part) => part.type === 'text') .map((part) => part.text) .join('\n') .trim(); const handleCopy = async () => { if (!textFromParts) { toast.error("There's no text to copy!"); return; } await copyToClipboard(textFromParts); toast.success('Copied to clipboard!'); }; // User messages get edit (on hover) and copy actions if (message.role === 'user') { return (
{setMode && ( setMode('edit')} className="-left-10 absolute top-0 opacity-0 transition-opacity group-hover/message:opacity-100" > )}
); } return ( { const upvote = fetch('/api/vote', { method: 'PATCH', body: JSON.stringify({ chatId, messageId: message.id, type: 'up', }), }); toast.promise(upvote, { loading: 'Upvoting Response...', success: () => { mutate>( `/api/vote?chatId=${chatId}`, (currentVotes) => { if (!currentVotes) return []; const votesWithoutCurrent = currentVotes.filter( (vote) => vote.messageId !== message.id, ); return [ ...votesWithoutCurrent, { chatId, messageId: message.id, isUpvoted: true, }, ]; }, { revalidate: false }, ); return 'Upvoted Response!'; }, error: 'Failed to upvote response.', }); }} > { const downvote = fetch('/api/vote', { method: 'PATCH', body: JSON.stringify({ chatId, messageId: message.id, type: 'down', }), }); toast.promise(downvote, { loading: 'Downvoting Response...', success: () => { mutate>( `/api/vote?chatId=${chatId}`, (currentVotes) => { if (!currentVotes) return []; const votesWithoutCurrent = currentVotes.filter( (vote) => vote.messageId !== message.id, ); return [ ...votesWithoutCurrent, { chatId, messageId: message.id, isUpvoted: false, }, ]; }, { revalidate: false }, ); return 'Downvoted Response!'; }, error: 'Failed to downvote response.', }); }} > ); } export const MessageActions = memo( PureMessageActions, (prevProps, nextProps) => { if (!equal(prevProps.vote, nextProps.vote)) return false; if (prevProps.isLoading !== nextProps.isLoading) return false; return true; }, );