ui: mobile chatbot improvements (#1155)
This commit is contained in:
parent
fd8ed4d863
commit
31de38ab18
16 changed files with 547 additions and 449 deletions
|
|
@ -3,7 +3,7 @@ import { useCopyToClipboard } from 'usehooks-ts';
|
|||
|
||||
import type { Vote } from '@/lib/db/schema';
|
||||
|
||||
import { CopyIcon, ThumbDownIcon, ThumbUpIcon } from './icons';
|
||||
import { CopyIcon, ThumbDownIcon, ThumbUpIcon, PencilEditIcon } from './icons';
|
||||
import { Actions, Action } from './elements/actions';
|
||||
import { memo } from 'react';
|
||||
import equal from 'fast-deep-equal';
|
||||
|
|
@ -15,134 +15,156 @@ export function PureMessageActions({
|
|||
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;
|
||||
if (message.role === 'user') 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 (
|
||||
<Actions className="justify-end -mr-0.5">
|
||||
<div className="relative">
|
||||
{setMode && (
|
||||
<Action
|
||||
tooltip="Edit"
|
||||
onClick={() => setMode('edit')}
|
||||
className="absolute top-0 -left-10 opacity-0 transition-opacity group-hover/message:opacity-100"
|
||||
>
|
||||
<PencilEditIcon />
|
||||
</Action>
|
||||
)}
|
||||
<Action tooltip="Copy" onClick={handleCopy}>
|
||||
<CopyIcon />
|
||||
</Action>
|
||||
</div>
|
||||
</Actions>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Actions>
|
||||
<Action
|
||||
tooltip="Copy"
|
||||
onClick={async () => {
|
||||
const textFromParts = message.parts
|
||||
?.filter((part) => part.type === 'text')
|
||||
.map((part) => part.text)
|
||||
.join('\n')
|
||||
.trim();
|
||||
<Actions className="-ml-0.5">
|
||||
<Action tooltip="Copy" onClick={handleCopy}>
|
||||
<CopyIcon />
|
||||
</Action>
|
||||
|
||||
if (!textFromParts) {
|
||||
toast.error("There's no text to copy!");
|
||||
return;
|
||||
}
|
||||
<Action
|
||||
tooltip="Upvote Response"
|
||||
data-testid="message-upvote"
|
||||
disabled={vote?.isUpvoted}
|
||||
onClick={async () => {
|
||||
const upvote = fetch('/api/vote', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({
|
||||
chatId,
|
||||
messageId: message.id,
|
||||
type: 'up',
|
||||
}),
|
||||
});
|
||||
|
||||
await copyToClipboard(textFromParts);
|
||||
toast.success('Copied to clipboard!');
|
||||
}}
|
||||
>
|
||||
<CopyIcon />
|
||||
</Action>
|
||||
toast.promise(upvote, {
|
||||
loading: 'Upvoting Response...',
|
||||
success: () => {
|
||||
mutate<Array<Vote>>(
|
||||
`/api/vote?chatId=${chatId}`,
|
||||
(currentVotes) => {
|
||||
if (!currentVotes) return [];
|
||||
|
||||
<Action
|
||||
tooltip="Upvote Response"
|
||||
data-testid="message-upvote"
|
||||
disabled={vote?.isUpvoted}
|
||||
onClick={async () => {
|
||||
const upvote = fetch('/api/vote', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({
|
||||
chatId,
|
||||
messageId: message.id,
|
||||
type: 'up',
|
||||
}),
|
||||
});
|
||||
const votesWithoutCurrent = currentVotes.filter(
|
||||
(vote) => vote.messageId !== message.id,
|
||||
);
|
||||
|
||||
toast.promise(upvote, {
|
||||
loading: 'Upvoting Response...',
|
||||
success: () => {
|
||||
mutate<Array<Vote>>(
|
||||
`/api/vote?chatId=${chatId}`,
|
||||
(currentVotes) => {
|
||||
if (!currentVotes) return [];
|
||||
return [
|
||||
...votesWithoutCurrent,
|
||||
{
|
||||
chatId,
|
||||
messageId: message.id,
|
||||
isUpvoted: true,
|
||||
},
|
||||
];
|
||||
},
|
||||
{ revalidate: false },
|
||||
);
|
||||
|
||||
const votesWithoutCurrent = currentVotes.filter(
|
||||
(vote) => vote.messageId !== message.id,
|
||||
);
|
||||
return 'Upvoted Response!';
|
||||
},
|
||||
error: 'Failed to upvote response.',
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ThumbUpIcon />
|
||||
</Action>
|
||||
|
||||
return [
|
||||
...votesWithoutCurrent,
|
||||
{
|
||||
chatId,
|
||||
messageId: message.id,
|
||||
isUpvoted: true,
|
||||
},
|
||||
];
|
||||
},
|
||||
{ revalidate: false },
|
||||
);
|
||||
<Action
|
||||
tooltip="Downvote Response"
|
||||
data-testid="message-downvote"
|
||||
disabled={vote && !vote.isUpvoted}
|
||||
onClick={async () => {
|
||||
const downvote = fetch('/api/vote', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({
|
||||
chatId,
|
||||
messageId: message.id,
|
||||
type: 'down',
|
||||
}),
|
||||
});
|
||||
|
||||
return 'Upvoted Response!';
|
||||
},
|
||||
error: 'Failed to upvote response.',
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ThumbUpIcon />
|
||||
</Action>
|
||||
toast.promise(downvote, {
|
||||
loading: 'Downvoting Response...',
|
||||
success: () => {
|
||||
mutate<Array<Vote>>(
|
||||
`/api/vote?chatId=${chatId}`,
|
||||
(currentVotes) => {
|
||||
if (!currentVotes) return [];
|
||||
|
||||
<Action
|
||||
tooltip="Downvote Response"
|
||||
data-testid="message-downvote"
|
||||
disabled={vote && !vote.isUpvoted}
|
||||
onClick={async () => {
|
||||
const downvote = fetch('/api/vote', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({
|
||||
chatId,
|
||||
messageId: message.id,
|
||||
type: 'down',
|
||||
}),
|
||||
});
|
||||
const votesWithoutCurrent = currentVotes.filter(
|
||||
(vote) => vote.messageId !== message.id,
|
||||
);
|
||||
|
||||
toast.promise(downvote, {
|
||||
loading: 'Downvoting Response...',
|
||||
success: () => {
|
||||
mutate<Array<Vote>>(
|
||||
`/api/vote?chatId=${chatId}`,
|
||||
(currentVotes) => {
|
||||
if (!currentVotes) return [];
|
||||
return [
|
||||
...votesWithoutCurrent,
|
||||
{
|
||||
chatId,
|
||||
messageId: message.id,
|
||||
isUpvoted: false,
|
||||
},
|
||||
];
|
||||
},
|
||||
{ revalidate: false },
|
||||
);
|
||||
|
||||
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.',
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ThumbDownIcon />
|
||||
</Action>
|
||||
return 'Downvoted Response!';
|
||||
},
|
||||
error: 'Failed to downvote response.',
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ThumbDownIcon />
|
||||
</Action>
|
||||
</Actions>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue