2024-11-15 12:18:17 -05:00
|
|
|
import type { Message } from 'ai';
|
2024-11-05 17:15:51 +03:00
|
|
|
import { useSWRConfig } from 'swr';
|
|
|
|
|
import { useCopyToClipboard } from 'usehooks-ts';
|
|
|
|
|
|
2024-11-15 12:18:17 -05:00
|
|
|
import type { Vote } from '@/lib/db/schema';
|
2024-11-05 17:15:51 +03:00
|
|
|
|
|
|
|
|
import { CopyIcon, ThumbDownIcon, ThumbUpIcon } from './icons';
|
2024-11-15 10:14:25 -05:00
|
|
|
import { Button } from './ui/button';
|
2024-11-05 17:15:51 +03:00
|
|
|
import {
|
|
|
|
|
Tooltip,
|
|
|
|
|
TooltipContent,
|
|
|
|
|
TooltipProvider,
|
|
|
|
|
TooltipTrigger,
|
2024-11-15 10:14:25 -05:00
|
|
|
} from './ui/tooltip';
|
2024-12-10 17:54:10 +05:30
|
|
|
import { memo } from 'react';
|
|
|
|
|
import equal from 'fast-deep-equal';
|
2025-03-11 14:39:36 -07:00
|
|
|
import { toast } from 'sonner';
|
2024-11-05 17:15:51 +03:00
|
|
|
|
2024-12-10 17:54:10 +05:30
|
|
|
export function PureMessageActions({
|
2024-11-05 17:15:51 +03:00
|
|
|
chatId,
|
|
|
|
|
message,
|
|
|
|
|
vote,
|
|
|
|
|
isLoading,
|
|
|
|
|
}: {
|
|
|
|
|
chatId: string;
|
|
|
|
|
message: Message;
|
|
|
|
|
vote: Vote | undefined;
|
|
|
|
|
isLoading: boolean;
|
|
|
|
|
}) {
|
|
|
|
|
const { mutate } = useSWRConfig();
|
|
|
|
|
const [_, copyToClipboard] = useCopyToClipboard();
|
|
|
|
|
|
|
|
|
|
if (isLoading) return null;
|
|
|
|
|
if (message.role === 'user') return null;
|
|
|
|
|
if (message.toolInvocations && message.toolInvocations.length > 0)
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<TooltipProvider delayDuration={0}>
|
|
|
|
|
<div className="flex flex-row gap-2">
|
|
|
|
|
<Tooltip>
|
|
|
|
|
<TooltipTrigger asChild>
|
|
|
|
|
<Button
|
|
|
|
|
className="py-1 px-2 h-fit text-muted-foreground"
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={async () => {
|
|
|
|
|
await copyToClipboard(message.content as string);
|
|
|
|
|
toast.success('Copied to clipboard!');
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<CopyIcon />
|
|
|
|
|
</Button>
|
|
|
|
|
</TooltipTrigger>
|
|
|
|
|
<TooltipContent>Copy</TooltipContent>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
|
|
|
|
<Tooltip>
|
|
|
|
|
<TooltipTrigger asChild>
|
|
|
|
|
<Button
|
2025-03-11 14:39:36 -07:00
|
|
|
data-testid="message-upvote"
|
2024-11-05 17:15:51 +03:00
|
|
|
className="py-1 px-2 h-fit text-muted-foreground !pointer-events-auto"
|
2024-11-15 12:18:17 -05:00
|
|
|
disabled={vote?.isUpvoted}
|
2024-11-05 17:15:51 +03:00
|
|
|
variant="outline"
|
|
|
|
|
onClick={async () => {
|
|
|
|
|
const upvote = fetch('/api/vote', {
|
|
|
|
|
method: 'PATCH',
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
chatId,
|
2025-01-28 18:51:04 +05:30
|
|
|
messageId: message.id,
|
2024-11-05 17:15:51 +03:00
|
|
|
type: 'up',
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
toast.promise(upvote, {
|
|
|
|
|
loading: 'Upvoting Response...',
|
|
|
|
|
success: () => {
|
|
|
|
|
mutate<Array<Vote>>(
|
|
|
|
|
`/api/vote?chatId=${chatId}`,
|
|
|
|
|
(currentVotes) => {
|
|
|
|
|
if (!currentVotes) return [];
|
|
|
|
|
|
|
|
|
|
const votesWithoutCurrent = currentVotes.filter(
|
2024-11-15 13:00:15 -05:00
|
|
|
(vote) => vote.messageId !== message.id,
|
2024-11-05 17:15:51 +03:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
...votesWithoutCurrent,
|
|
|
|
|
{
|
|
|
|
|
chatId,
|
|
|
|
|
messageId: message.id,
|
|
|
|
|
isUpvoted: true,
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
},
|
2024-11-15 13:00:15 -05:00
|
|
|
{ revalidate: false },
|
2024-11-05 17:15:51 +03:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return 'Upvoted Response!';
|
|
|
|
|
},
|
|
|
|
|
error: 'Failed to upvote response.',
|
|
|
|
|
});
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<ThumbUpIcon />
|
|
|
|
|
</Button>
|
|
|
|
|
</TooltipTrigger>
|
|
|
|
|
<TooltipContent>Upvote Response</TooltipContent>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
|
|
|
|
<Tooltip>
|
|
|
|
|
<TooltipTrigger asChild>
|
|
|
|
|
<Button
|
2025-03-11 14:39:36 -07:00
|
|
|
data-testid="message-downvote"
|
2024-11-05 17:15:51 +03:00
|
|
|
className="py-1 px-2 h-fit text-muted-foreground !pointer-events-auto"
|
|
|
|
|
variant="outline"
|
|
|
|
|
disabled={vote && !vote.isUpvoted}
|
|
|
|
|
onClick={async () => {
|
|
|
|
|
const downvote = fetch('/api/vote', {
|
|
|
|
|
method: 'PATCH',
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
chatId,
|
2025-01-28 18:51:04 +05:30
|
|
|
messageId: message.id,
|
2024-11-05 17:15:51 +03:00
|
|
|
type: 'down',
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
toast.promise(downvote, {
|
|
|
|
|
loading: 'Downvoting Response...',
|
|
|
|
|
success: () => {
|
|
|
|
|
mutate<Array<Vote>>(
|
|
|
|
|
`/api/vote?chatId=${chatId}`,
|
|
|
|
|
(currentVotes) => {
|
|
|
|
|
if (!currentVotes) return [];
|
|
|
|
|
|
|
|
|
|
const votesWithoutCurrent = currentVotes.filter(
|
2024-11-15 13:00:15 -05:00
|
|
|
(vote) => vote.messageId !== message.id,
|
2024-11-05 17:15:51 +03:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
...votesWithoutCurrent,
|
|
|
|
|
{
|
|
|
|
|
chatId,
|
|
|
|
|
messageId: message.id,
|
|
|
|
|
isUpvoted: false,
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
},
|
2024-11-15 13:00:15 -05:00
|
|
|
{ revalidate: false },
|
2024-11-05 17:15:51 +03:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return 'Downvoted Response!';
|
|
|
|
|
},
|
|
|
|
|
error: 'Failed to downvote response.',
|
|
|
|
|
});
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<ThumbDownIcon />
|
|
|
|
|
</Button>
|
|
|
|
|
</TooltipTrigger>
|
|
|
|
|
<TooltipContent>Downvote Response</TooltipContent>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
</div>
|
|
|
|
|
</TooltipProvider>
|
|
|
|
|
);
|
|
|
|
|
}
|
2024-12-10 17:54:10 +05:30
|
|
|
|
|
|
|
|
export const MessageActions = memo(
|
|
|
|
|
PureMessageActions,
|
|
|
|
|
(prevProps, nextProps) => {
|
|
|
|
|
if (!equal(prevProps.vote, nextProps.vote)) return false;
|
|
|
|
|
if (prevProps.isLoading !== nextProps.isLoading) return false;
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
},
|
|
|
|
|
);
|