refactor(chat): split screen orchestration
Docker Image / Build and Push Docker Image (push) Successful in 14m9s

This commit is contained in:
2026-07-08 15:33:24 +08:00
parent 18aa69bdd9
commit 21b6d346bd
15 changed files with 522 additions and 179 deletions
+47 -12
View File
@@ -12,7 +12,7 @@
* - 新消息到达时自动滚到底部
* - 用户向上滚动时不强制拉回(保持阅读上下文)
*/
import { useEffect, useLayoutEffect, useRef } from "react";
import { useEffect, useLayoutEffect, useMemo, useRef } from "react";
import type { UiMessage } from "@/data/dto/chat";
@@ -31,11 +31,14 @@ import styles from "./chat-area.module.css";
const CHAT_AUTO_SCROLL_BOTTOM_THRESHOLD = 96;
type RestoreState = "idle" | "checking" | "restoring" | "settlingBottom" | "done";
type ChatMessageKeyResolver = (message: UiMessage) => string;
type ChatRenderItem =
| { type: "date"; date: string; key: string }
| { type: "msg"; message: UiMessage; key: string };
export interface ChatAreaProps {
messages: readonly UiMessage[];
isReplyingAI: boolean;
isGuest: boolean;
isUnlockingMessage?: boolean;
unlockingMessageId?: string | null;
onUnlockPrivateMessage?: (messageId: string) => void;
@@ -55,6 +58,7 @@ export function ChatArea({
const restoredScrollRef = useRef(false);
const restoreStateRef = useRef<RestoreState>("idle");
const wasNearBottomRef = useRef(true);
const getMessageKey = useMemo(() => createChatMessageKeyResolver(), []);
const saveCurrentScrollSnapshotNow = (anchorMessageId: string) => {
const scrollNode = scrollRef.current;
@@ -160,6 +164,7 @@ export function ChatArea({
{renderMessagesWithDateHeaders(
messages,
getMessageKey,
isUnlockingMessage,
unlockingMessageId,
onUnlockPrivateMessage,
@@ -221,21 +226,13 @@ function startStableScrollTask({
/** 渲染消息列表(按日期分组插入分隔条) */
function renderMessagesWithDateHeaders(
messages: readonly UiMessage[],
getMessageKey: (message: UiMessage) => string,
isUnlockingMessage?: boolean,
unlockingMessageId?: string | null,
onUnlockPrivateMessage?: (messageId: string) => void,
onUnlockVoiceMessage?: (messageId: string) => void,
) {
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) =>
return buildChatRenderItems(messages, getMessageKey).map((item) =>
item.type === "date" ? (
<DateHeader key={item.key} date={item.date} />
) : (
@@ -261,3 +258,41 @@ function renderMessagesWithDateHeaders(
),
);
}
export function createChatMessageKeyResolver(): ChatMessageKeyResolver {
const localMessageKeys = new WeakMap<UiMessage, string>();
let nextLocalMessageKey = 0;
return (message) => {
if (message.id && message.id.length > 0) return `msg-${message.id}`;
const existing = localMessageKeys.get(message);
if (existing) return existing;
nextLocalMessageKey += 1;
const next = `local-msg-${nextLocalMessageKey}`;
localMessageKeys.set(message, next);
return next;
};
}
export function buildChatRenderItems(
messages: readonly UiMessage[],
getMessageKey: ChatMessageKeyResolver,
): ChatRenderItem[] {
const items: ChatRenderItem[] = [];
messages.forEach((message, index) => {
const messageKey = getMessageKey(message);
if (index === 0 || message.date !== messages[index - 1].date) {
items.push({
type: "date",
date: message.date,
key: `date-${message.date}-${messageKey}`,
});
}
items.push({ type: "msg", message, key: messageKey });
});
return items;
}