240 lines
6.7 KiB
TypeScript
240 lines
6.7 KiB
TypeScript
import { addToast, cn } from '@heroui/react';
|
|
import { type Message as BaseMessage, useMujian } from '@mujian/js-sdk/react';
|
|
import React, { type MouseEventHandler } from 'react';
|
|
import { mjChatCls } from '@/utils/cls';
|
|
import { EditActions } from './components/EditActions';
|
|
import { MessageActions } from './components/MessageActions';
|
|
import { MessageBubble } from './components/MessageBubble';
|
|
|
|
export type MessageItemProps = {
|
|
message: BaseMessage;
|
|
originalMessage: BaseMessage;
|
|
|
|
index: number;
|
|
avatar: string;
|
|
name: string;
|
|
isLastMsg?: boolean;
|
|
isFirstMsg?: boolean;
|
|
|
|
onRegenerate?: () => Promise<void>;
|
|
onContinue?: () => Promise<void>;
|
|
onDelete: (messageId: string) => Promise<void>;
|
|
onEdit: (messageId: string, content: string) => Promise<void>;
|
|
onSwipe: (messageId: string, swipeId: number) => Promise<void>;
|
|
sendMessage?: (message: string) => Promise<void>;
|
|
};
|
|
|
|
// Add comparison function for React.memo
|
|
const arePropsEqual = (
|
|
prevProps: MessageItemProps,
|
|
nextProps: MessageItemProps,
|
|
) => {
|
|
// 如果 swipes 长度不同,直接返回 false
|
|
if (prevProps.message.swipes?.length !== nextProps.message.swipes?.length) {
|
|
return false;
|
|
}
|
|
// 如果 swipes 内容不同,直接返回 false
|
|
if (
|
|
prevProps.message.swipes?.some(
|
|
(swipe, index) => swipe !== nextProps.message.swipes[index],
|
|
)
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
// 其他属性比较
|
|
return (
|
|
prevProps.avatar === nextProps.avatar &&
|
|
prevProps.message.content === nextProps.message.content &&
|
|
prevProps.message.role === nextProps.message.role &&
|
|
prevProps.message.isStreaming === nextProps.message.isStreaming &&
|
|
prevProps.name === nextProps.name &&
|
|
prevProps.isLastMsg === nextProps.isLastMsg &&
|
|
prevProps.message.sendAt === nextProps.message.sendAt &&
|
|
prevProps.message.activeSwipeId === nextProps.message.activeSwipeId &&
|
|
prevProps.message.id === nextProps.message.id &&
|
|
prevProps.index === nextProps.index
|
|
);
|
|
};
|
|
|
|
export const MessageItem = React.memo((props: MessageItemProps) => {
|
|
const {
|
|
message,
|
|
originalMessage,
|
|
|
|
// index,
|
|
// avatar,
|
|
// name,
|
|
isLastMsg,
|
|
isFirstMsg,
|
|
onRegenerate,
|
|
onContinue,
|
|
onDelete,
|
|
onEdit,
|
|
onSwipe,
|
|
} = props;
|
|
const {
|
|
id,
|
|
|
|
role,
|
|
isStreaming,
|
|
|
|
content,
|
|
swipes = [],
|
|
activeSwipeId,
|
|
|
|
// sendAt,
|
|
} = message;
|
|
const isUser = role === 'user';
|
|
|
|
const messageRef = React.useRef<HTMLDivElement>(null); // 使用 useRef 获取当前组件的引用
|
|
const [isEditing, setIsEditing] = React.useState(false); // State to toggle edit mode
|
|
const [editedMessage, setEditedMessage] = React.useState(content);
|
|
const mujian = useMujian();
|
|
|
|
const renderedMessage = isUser ? content : swipes[activeSwipeId];
|
|
|
|
// 滚动到消息可见位置的通用函数
|
|
const scrollMessageIntoView = (
|
|
alignToBlock: ScrollLogicalPosition = 'end',
|
|
behavior: ScrollBehavior = 'instant',
|
|
) => {
|
|
if (messageRef.current) {
|
|
messageRef.current.scrollIntoView({
|
|
behavior: behavior,
|
|
block: alignToBlock,
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleSaveEdit = async () => {
|
|
if (id && activeSwipeId !== undefined) {
|
|
await onEdit(id, editedMessage);
|
|
setIsEditing(false);
|
|
|
|
setTimeout(() => scrollMessageIntoView('end', 'smooth'), 100);
|
|
}
|
|
};
|
|
|
|
const handleCopy = async () => {
|
|
try {
|
|
await mujian.utils.clipboard.writeText(originalMessage.content);
|
|
addToast({
|
|
color: 'success',
|
|
description: '复制成功',
|
|
timeout: 2000,
|
|
});
|
|
} catch (error) {
|
|
console.error(error);
|
|
addToast({
|
|
description: '复制失败,请重试。',
|
|
});
|
|
}
|
|
};
|
|
|
|
// Separate function to handle message deletion
|
|
const handleDeleteMessage = async () => {
|
|
if (id) {
|
|
onDelete(id);
|
|
}
|
|
};
|
|
// console.log("content", index, content);
|
|
|
|
// 确保 currentSwipeId 在有效范围内
|
|
const validSwipeId = Math.min(Math.max(0, activeSwipeId), swipes.length - 1);
|
|
|
|
const handleSwipe = async (direction: 'prev' | 'next') => {
|
|
if (swipes.length <= 1) return;
|
|
|
|
// 简化计算逻辑,直接使用模运算确保能够循环浏览所有页面
|
|
const newSwipeId =
|
|
direction === 'prev'
|
|
? (activeSwipeId - 1 + swipes.length) % swipes.length
|
|
: (activeSwipeId + 1) % swipes.length;
|
|
|
|
await onSwipe(id, newSwipeId);
|
|
|
|
scrollMessageIntoView();
|
|
};
|
|
|
|
// Stop propagation for message text clicks
|
|
const handleMessageTextClick: MouseEventHandler<HTMLDivElement> = (e) => {
|
|
e.stopPropagation();
|
|
};
|
|
|
|
const handleEditButtonClick = async () => {
|
|
setIsEditing(true);
|
|
if (isUser) {
|
|
setEditedMessage(originalMessage.content);
|
|
} else {
|
|
setEditedMessage(originalMessage.swipes[validSwipeId]);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div
|
|
key={`message-${activeSwipeId}`}
|
|
ref={messageRef} // 将 ref 绑定到根容器
|
|
className={mjChatCls(
|
|
['msg-item-wrapper'],
|
|
cn('grid items-end gap-2 mb-2 w-full grid-cols-[32px_1fr_32px]'),
|
|
)}
|
|
onContextMenu={(event) => {
|
|
// 按ctrl点击时打印ID
|
|
if (event.ctrlKey) {
|
|
event.preventDefault();
|
|
console.log('Official Chat Message ID:', id);
|
|
}
|
|
}}
|
|
>
|
|
<MessageBubble
|
|
message={message}
|
|
isUser={isUser}
|
|
isEditing={isEditing}
|
|
editedMessage={editedMessage}
|
|
onEditChange={setEditedMessage}
|
|
onCopyClick={handleCopy}
|
|
isStreaming={isStreaming}
|
|
currentMessage={renderedMessage}
|
|
onEditButtonClick={handleEditButtonClick}
|
|
onDelete={handleDeleteMessage}
|
|
onEditAreaShow={() => scrollMessageIntoView()}
|
|
/>
|
|
<EditActions
|
|
isUser={isUser}
|
|
isEditing={isEditing}
|
|
onSaveEdit={handleSaveEdit}
|
|
onCancelEdit={() => {
|
|
setIsEditing(false);
|
|
setEditedMessage(
|
|
isUser
|
|
? originalMessage.content
|
|
: originalMessage.swipes[activeSwipeId],
|
|
);
|
|
// 滚动到消息位置
|
|
setTimeout(() => scrollMessageIntoView('end', 'smooth'), 100);
|
|
}}
|
|
/>
|
|
<MessageActions
|
|
message={message}
|
|
isUser={isUser}
|
|
isLastMsg={Boolean(isLastMsg)}
|
|
isFirstMsg={Boolean(isFirstMsg)}
|
|
isStreaming={isStreaming}
|
|
isEditing={isEditing}
|
|
swipes={swipes}
|
|
activeSwipeId={activeSwipeId}
|
|
onRegenerate={onRegenerate}
|
|
onContinue={onContinue}
|
|
onEditButtonClick={handleEditButtonClick}
|
|
onCopyClick={handleCopy}
|
|
onSwipe={handleSwipe}
|
|
onMessageTextClick={handleMessageTextClick}
|
|
onDelete={handleDeleteMessage}
|
|
/>
|
|
</div>
|
|
);
|
|
}, arePropsEqual);
|
|
|
|
MessageItem.displayName = 'Message';
|