Files
cozsweet-frontend-nextjs/src/app/chat/components/message-content.tsx
T
Codex 02f6964484
Docker Image / Build and Push Docker Image (push) Successful in 1m56s
feat(chat): render commercial actions and persist greetings
2026-07-23 14:54:23 +08:00

147 lines
4.5 KiB
TypeScript

"use client";
import type { CommercialAction } from "@/data/schemas/chat";
import { CommercialActionCard } from "./commercial-action-card";
import { ImageBubble } from "./image-bubble";
import { LockedImageMessageCard } from "./locked-image-message-card";
import { PrivateMessageCard } from "./private-message-card";
import { TextBubble } from "./text-bubble";
import { VoiceBubble } from "./voice-bubble";
import styles from "./chat-area.module.css";
export interface MessageContentProps {
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;
commercialAction?: CommercialAction | null;
isUnlockingMessage?: boolean;
onUnlockPrivateMessage?: ChatMessageAction;
onUnlockVoiceMessage?: ChatMessageAction;
onUnlockImageMessage?: ChatMessageAction;
onOpenImage?: (displayMessageId: string) => void;
onCommercialAction?: (action: CommercialAction) => void;
onCommercialActionDismiss?: (action: CommercialAction) => void;
}
type ChatMessageAction = (
displayMessageId: string,
remoteMessageId?: string,
) => void;
const IMAGE_PLACEHOLDER = "[图片]";
export function MessageContent({
characterId,
displayMessageId,
remoteMessageId,
content,
imageUrl,
imagePaywalled,
audioUrl,
isFromAI,
locked,
lockReason,
lockedPrivate,
privateMessageHint,
commercialAction,
isUnlockingMessage,
onUnlockPrivateMessage,
onUnlockVoiceMessage,
onUnlockImageMessage,
onOpenImage,
onCommercialAction,
onCommercialActionDismiss,
}: MessageContentProps) {
const hasImage = imageUrl != null && imageUrl.length > 0;
const hasAudio = audioUrl != null && audioUrl.length > 0;
const hasText = content.length > 0 && content !== IMAGE_PLACEHOLDER;
const isLockedPrivateMessage =
lockedPrivate === true ||
(locked === true && lockReason === "private_message");
const isLockedVoiceMessage = locked === true && lockReason === "voice_message";
const isLockedImageMessage =
locked === true &&
(lockReason === "image_paywall" || lockReason === "image");
const shouldRenderVoiceMessage = hasAudio || isLockedVoiceMessage;
const handleUnlockPrivateMessage =
onUnlockPrivateMessage
? () =>
onUnlockPrivateMessage(displayMessageId, remoteMessageId)
: undefined;
const handleUnlockVoiceMessage =
isLockedVoiceMessage && onUnlockVoiceMessage
? () => onUnlockVoiceMessage(displayMessageId, remoteMessageId)
: undefined;
const handleUnlockImageMessage =
isLockedImageMessage && onUnlockImageMessage
? () => onUnlockImageMessage(displayMessageId, remoteMessageId)
: undefined;
return (
<div
className={[
styles.messageContent,
isFromAI ? styles.messageContentAi : styles.messageContentUser,
].join(" ")}
>
{isLockedPrivateMessage ? (
<PrivateMessageCard
hint={privateMessageHint}
isUnlocking={isUnlockingMessage}
onUnlock={handleUnlockPrivateMessage}
/>
) : isLockedImageMessage && !hasImage ? (
<LockedImageMessageCard
hint={privateMessageHint}
isUnlocking={isUnlockingMessage}
onUnlock={handleUnlockImageMessage}
/>
) : (
<>
{hasImage && imageUrl && (
<ImageBubble
characterId={characterId}
displayMessageId={displayMessageId}
remoteMessageId={remoteMessageId}
imageUrl={imageUrl}
imagePaywalled={imagePaywalled}
onOpenImage={onOpenImage}
/>
)}
{shouldRenderVoiceMessage ? (
<VoiceBubble
characterId={characterId}
remoteMessageId={remoteMessageId}
audioUrl={audioUrl}
isFromAI={isFromAI}
locked={isLockedVoiceMessage}
hint={privateMessageHint}
isUnlocking={isUnlockingMessage}
onUnlock={handleUnlockVoiceMessage}
/>
) : null}
{hasText && !shouldRenderVoiceMessage ? (
<TextBubble content={content} isFromAI={isFromAI} />
) : null}
</>
)}
{isFromAI && commercialAction ? (
<CommercialActionCard
action={commercialAction}
onActivate={onCommercialAction}
onDismiss={onCommercialActionDismiss}
/>
) : null}
</div>
);
}