"use client"; /** * ChatArea 消息列表区 * * 组成(自上而下): * - AI 披露横幅 * - 日期分隔条 * - 消息气泡(AI 左 / 用户右) * - AI 正在回复时的加载动画气泡 * * 滚动行为: * - 新消息到达时自动滚到底部 * - 用户向上滚动时不强制拉回(保持阅读上下文) */ import { useEffect, useLayoutEffect, useMemo, useRef } from "react"; import type { UiMessage } from "@/data/dto/chat"; import { buildChatRenderItems, createChatMessageKeyResolver, type ChatMessageKeyResolver, } from "../chat-render-items"; import { AiDisclosureBanner } from "./ai-disclosure-banner"; import { DateHeader } from "./date-header"; import { LottieMessageBubble } from "./lottie-message-bubble"; import { MessageBubble } from "./message-bubble"; import styles from "./chat-area.module.css"; const CHAT_AUTO_SCROLL_BOTTOM_THRESHOLD = 96; export interface ChatAreaProps { messages: readonly UiMessage[]; isReplyingAI: boolean; isUnlockingMessage?: boolean; unlockingMessageId?: string | null; onUnlockPrivateMessage?: (messageId: string) => void; onUnlockVoiceMessage?: (messageId: string) => void; onOpenImage?: (messageId: string) => void; } export function ChatArea({ messages, isReplyingAI, isUnlockingMessage, unlockingMessageId, onUnlockPrivateMessage, onUnlockVoiceMessage, onOpenImage, }: ChatAreaProps) { const scrollRef = useRef(null); const prevLengthRef = useRef(messages.length); const initialScrollSettledRef = useRef(false); const wasNearBottomRef = useRef(true); const getMessageKey = useMemo(() => createChatMessageKeyResolver(), []); useLayoutEffect(() => { if (initialScrollSettledRef.current || messages.length === 0) return; const scrollNode = scrollRef.current; if (!scrollNode) return; initialScrollSettledRef.current = true; prevLengthRef.current = messages.length; scrollNode.scrollTop = scrollNode.scrollHeight; wasNearBottomRef.current = true; }, [messages.length]); // 新消息 → 滚到底部 useEffect(() => { if (messages.length !== prevLengthRef.current) { const scrollNode = scrollRef.current; if (scrollNode && wasNearBottomRef.current) { scrollNode.scrollTo({ top: scrollNode.scrollHeight, behavior: "smooth", }); } prevLengthRef.current = messages.length; } }, [messages.length]); return (
{ wasNearBottomRef.current = isNearBottom(event.currentTarget); }} > {renderMessagesWithDateHeaders( messages, getMessageKey, isUnlockingMessage, unlockingMessageId, onUnlockPrivateMessage, onUnlockVoiceMessage, onOpenImage, )} {isReplyingAI && }
); } function isNearBottom(scrollNode: HTMLElement): boolean { const distanceFromBottom = scrollNode.scrollHeight - scrollNode.scrollTop - scrollNode.clientHeight; return distanceFromBottom <= CHAT_AUTO_SCROLL_BOTTOM_THRESHOLD; } /** 渲染消息列表(按日期分组插入分隔条) */ function renderMessagesWithDateHeaders( messages: readonly UiMessage[], getMessageKey: ChatMessageKeyResolver, isUnlockingMessage?: boolean, unlockingMessageId?: string | null, onUnlockPrivateMessage?: (messageId: string) => void, onUnlockVoiceMessage?: (messageId: string) => void, onOpenImage?: (messageId: string) => void, ) { return buildChatRenderItems(messages, getMessageKey).map((item) => item.type === "date" ? ( ) : ( ), ); }