import equal from "fast-deep-equal"; import { memo } from "react"; import { toast } from "sonner"; import { useSWRConfig } from "swr"; import { useCopyToClipboard } from "usehooks-ts"; import type { Vote } from "@/lib/db/schema"; import type { ChatMessage } from "@/lib/types"; import { extractImageUrlsFromText } from "@/lib/image-url"; import { Action, Actions } from "./elements/actions"; import { CopyIcon, PencilEditIcon, ThumbDownIcon, ThumbUpIcon } from "./icons"; export function PureMessageActions({ chatId, message, vote, isLoading, setMode, onImageEdit, }: { chatId: string; message: ChatMessage; vote: Vote | undefined; isLoading: boolean; setMode?: (mode: "view" | "edit") => void; onImageEdit?: (imageUrl: string) => 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("当前没有可复制的文本"); return; } await copyToClipboard(textFromParts); toast.success("已复制到剪贴板"); }; // 检查消息中是否包含图片 const extractImageUrl = () => { if (!textFromParts) return null; const imageUrls = extractImageUrlsFromText(textFromParts); return imageUrls[0] || null; }; const imageUrl = extractImageUrl(); // User messages get edit (on hover) and copy actions if (message.role === "user") { return (
{setMode && ( setMode("edit")} tooltip="编辑内容" > )}
); } return ( {imageUrl && onImageEdit && ( onImageEdit(imageUrl)} tooltip="编辑图片" > )} { const upvote = fetch("/api/vote", { method: "PATCH", body: JSON.stringify({ chatId, messageId: message.id, type: "up", }), }); toast.promise(upvote, { loading: "正在点赞该回复...", success: () => { mutate( `/api/vote?chatId=${chatId}`, (currentVotes) => { if (!currentVotes) { return []; } const votesWithoutCurrent = currentVotes.filter( (currentVote) => currentVote.messageId !== message.id ); return [ ...votesWithoutCurrent, { chatId, messageId: message.id, isUpvoted: true, }, ]; }, { revalidate: false } ); return "已点赞该回复"; }, error: "点赞失败,请稍后重试", }); }} tooltip="点赞回复" > { const downvote = fetch("/api/vote", { method: "PATCH", body: JSON.stringify({ chatId, messageId: message.id, type: "down", }), }); toast.promise(downvote, { loading: "正在点踩该回复...", success: () => { mutate( `/api/vote?chatId=${chatId}`, (currentVotes) => { if (!currentVotes) { return []; } const votesWithoutCurrent = currentVotes.filter( (currentVote) => currentVote.messageId !== message.id ); return [ ...votesWithoutCurrent, { chatId, messageId: message.id, isUpvoted: false, }, ]; }, { revalidate: false } ); return "已点踩该回复"; }, error: "点踩失败,请稍后重试", }); }} tooltip="点踩回复" > ); } export const MessageActions = memo( PureMessageActions, (prevProps, nextProps) => { if (!equal(prevProps.vote, nextProps.vote)) { return false; } if (prevProps.isLoading !== nextProps.isLoading) { return false; } return true; } );