Files
cozsweet-frontend-nextjs/src/app/chat/components/chat-area.tsx
T
admin d64ad0f130 refactor(chat): extract ai disclosure into dedicated banner component
- Move AiDisclosure and EmptyState logic into a new AiDisclosureBanner component
- Remove isGuest prop from ChatArea as it's no longer needed there
- Update styling with explicit color values for better visibility
2026-06-12 12:12:13 +08:00

82 lines
2.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
/**
* ChatArea 消息列表区
*
* 原始 Dart: lib/ui/chat/widgets/chat_area.dart153 行)
*
* 组成(自上而下):
* - AI 披露横幅
* - 日期分隔条
* - 消息气泡(AI 左 / 用户右)
* - AI 正在回复时的加载动画气泡
*
* 滚动行为:
* - 新消息到达时自动滚到底部
* - 用户向上滚动时不强制拉回(保持阅读上下文)
*/
import { useEffect, useRef } from "react";
import type { UiMessage } from "@/models/chat/ui-message";
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";
export interface ChatAreaProps {
messages: readonly UiMessage[];
isReplyingAI: boolean;
isGuest: boolean;
}
export function ChatArea({ messages, isReplyingAI }: ChatAreaProps) {
const scrollRef = useRef<HTMLDivElement>(null);
const prevLengthRef = useRef(messages.length);
// 新消息 → 滚到底部
useEffect(() => {
if (messages.length !== prevLengthRef.current) {
scrollRef.current?.scrollTo({
top: scrollRef.current.scrollHeight,
behavior: "smooth",
});
prevLengthRef.current = messages.length;
}
}, [messages.length]);
return (
<main ref={scrollRef} className={styles.area} aria-label="Chat messages">
<AiDisclosureBanner />
{renderMessagesWithDateHeaders(messages)}
{isReplyingAI && <LottieMessageBubble />}
</main>
);
}
/** 渲染消息列表(按日期分组插入分隔条) */
function renderMessagesWithDateHeaders(messages: readonly UiMessage[]) {
const items: Array<{ type: "date"; date: string; key: string } | { type: "msg"; message: UiMessage; key: string }> = [];
messages.forEach((m, i) => {
if (i === 0 || m.date !== messages[i - 1].date) {
items.push({ type: "date", date: m.date, key: `d-${i}-${m.date}` });
}
items.push({ type: "msg", message: m, key: `m-${i}` });
});
return items.map((item) =>
item.type === "date" ? (
<DateHeader key={item.key} date={item.date} />
) : (
<MessageBubble
key={item.key}
content={item.message.content}
imageUrl={item.message.imageUrl}
isFromAI={item.message.isFromAI}
/>
),
);
}