init
This commit is contained in:
217
src/pages/chat/MessageItem/index.tsx
Normal file
217
src/pages/chat/MessageItem/index.tsx
Normal 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';
|
||||
Reference in New Issue
Block a user