This commit is contained in:
Cledwyn Lew
2025-11-25 13:32:33 +08:00
parent fd92202cba
commit 54f9dc947d
29 changed files with 8660 additions and 93 deletions

View File

@@ -0,0 +1,217 @@
import { cn } from '@heroui/react';
import { type Message as BaseMessage } from '@mujian/js-sdk/react';
import React, { type MouseEventHandler } from 'react';
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 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(), 50);
}
};
// 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={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('Message ID:', id);
}
}}
>
<MessageBubble
message={message}
isUser={isUser}
isEditing={isEditing}
editedMessage={editedMessage}
onEditChange={setEditedMessage}
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('nearest', 'smooth'), 10);
}}
/>
<MessageActions
isUser={isUser}
isLastMsg={Boolean(isLastMsg)}
isFirstMsg={Boolean(isFirstMsg)}
isStreaming={isStreaming}
isEditing={isEditing}
swipes={swipes}
activeSwipeId={activeSwipeId}
onRegenerate={onRegenerate}
onContinue={onContinue}
onEditButtonClick={handleEditButtonClick}
onSwipe={handleSwipe}
onMessageTextClick={handleMessageTextClick}
onDelete={handleDeleteMessage}
/>
</div>
);
}, arePropsEqual);
MessageItem.displayName = 'Message';