127 lines
3.6 KiB
TypeScript
127 lines
3.6 KiB
TypeScript
"use client";
|
||
/**
|
||
* MessageBubble 消息气泡(容器)
|
||
*
|
||
*
|
||
*
|
||
* 组成(从左到右):
|
||
* - AI:[Avatar] [Content] [占位 spacer]
|
||
* - 用户:[占位 spacer] [Content] [Avatar]
|
||
*/
|
||
import { useUserSelector } from "@/stores/user/user-context";
|
||
|
||
import { MessageAvatar } from "./message-avatar";
|
||
import { MessageContent } from "./message-content";
|
||
import styles from "./chat-area.module.css";
|
||
|
||
export interface MessageBubbleProps {
|
||
characterId: string;
|
||
displayMessageId: string;
|
||
remoteMessageId?: string;
|
||
content: string;
|
||
imageUrl?: string | null;
|
||
imagePaywalled?: boolean;
|
||
audioUrl?: string | null;
|
||
isFromAI: boolean;
|
||
locked?: boolean | null;
|
||
lockReason?: string | null;
|
||
lockedPrivate?: boolean | null;
|
||
privateMessageHint?: string | null;
|
||
isUnlockingMessage?: boolean;
|
||
onUnlockPrivateMessage?: ChatMessageAction;
|
||
onUnlockVoiceMessage?: ChatMessageAction;
|
||
onUnlockImageMessage?: ChatMessageAction;
|
||
onOpenImage?: (displayMessageId: string) => void;
|
||
}
|
||
|
||
type ChatMessageAction = (
|
||
displayMessageId: string,
|
||
remoteMessageId?: string,
|
||
) => void;
|
||
|
||
export function MessageBubble({
|
||
characterId,
|
||
displayMessageId,
|
||
remoteMessageId,
|
||
content,
|
||
imageUrl,
|
||
imagePaywalled,
|
||
audioUrl,
|
||
isFromAI,
|
||
locked,
|
||
lockReason,
|
||
lockedPrivate,
|
||
privateMessageHint,
|
||
isUnlockingMessage,
|
||
onUnlockPrivateMessage,
|
||
onUnlockVoiceMessage,
|
||
onUnlockImageMessage,
|
||
onOpenImage,
|
||
}: MessageBubbleProps) {
|
||
const avatarUrl = useUserSelector((state) => state.context.avatarUrl);
|
||
|
||
if (isFromAI) {
|
||
return (
|
||
<div
|
||
className={styles.bubbleRowAi}
|
||
data-chat-message-id={displayMessageId}
|
||
aria-label="AI message"
|
||
>
|
||
<MessageAvatar isFromAI={true} />
|
||
<div className={styles.bubbleInlineSpacer} aria-hidden="true" />
|
||
<MessageContent
|
||
characterId={characterId}
|
||
displayMessageId={displayMessageId}
|
||
remoteMessageId={remoteMessageId}
|
||
content={content}
|
||
imageUrl={imageUrl}
|
||
imagePaywalled={imagePaywalled}
|
||
audioUrl={audioUrl}
|
||
isFromAI={true}
|
||
locked={locked}
|
||
lockReason={lockReason}
|
||
lockedPrivate={lockedPrivate}
|
||
privateMessageHint={privateMessageHint}
|
||
isUnlockingMessage={isUnlockingMessage}
|
||
onUnlockPrivateMessage={onUnlockPrivateMessage}
|
||
onUnlockVoiceMessage={onUnlockVoiceMessage}
|
||
onUnlockImageMessage={onUnlockImageMessage}
|
||
onOpenImage={onOpenImage}
|
||
/>
|
||
<div className={styles.bubbleAvatarSpacer} aria-hidden="true" />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div
|
||
className={styles.bubbleRowUser}
|
||
data-chat-message-id={displayMessageId}
|
||
aria-label="User message"
|
||
>
|
||
<div className={styles.bubbleAvatarSpacer} aria-hidden="true" />
|
||
<MessageContent
|
||
characterId={characterId}
|
||
displayMessageId={displayMessageId}
|
||
remoteMessageId={remoteMessageId}
|
||
content={content}
|
||
imageUrl={imageUrl}
|
||
imagePaywalled={imagePaywalled}
|
||
audioUrl={audioUrl}
|
||
isFromAI={false}
|
||
locked={locked}
|
||
lockReason={lockReason}
|
||
lockedPrivate={lockedPrivate}
|
||
privateMessageHint={privateMessageHint}
|
||
isUnlockingMessage={isUnlockingMessage}
|
||
onUnlockPrivateMessage={onUnlockPrivateMessage}
|
||
onUnlockVoiceMessage={onUnlockVoiceMessage}
|
||
onUnlockImageMessage={onUnlockImageMessage}
|
||
onOpenImage={onOpenImage}
|
||
/>
|
||
<div className={styles.bubbleInlineSpacer} aria-hidden="true" />
|
||
<MessageAvatar isFromAI={false} userAvatarUrl={avatarUrl} />
|
||
</div>
|
||
);
|
||
}
|