feat(web): 前端接会话持久化——拉历史热分页 + 侧栏会话列表
- store: 消息真源移到后端(sessions元数据map + activeSessionId,partialize 移除 messages 改存会话元数据,version:2 migrate 清旧数据) - api: fetchSessions/createSubSession/fetchMessages(热分页before游标)/deleteSession + MessageDto→toChatMessage - Nav: 侧栏「会话」区——主会话置顶 + 次级列表 + +新建会话(Miuix) - 新增 HistoryList: 顶部「加载更早」向上分页 + 保守虚拟化(>200只渲染最近200) - useChat: send 用 activeSessionId;App: token 后 fetchSessions→激活主会话→拉近50条 - Docs/api-client: v0.10 补 WS sessionId + history 回放事件契约
This commit is contained in:
1 parent
48421923f6
commit
bcc53d68f3
9 files changed
+371
-32
No files matched your search
@@ -0,0 +1,83 @@
|
||||
import { useState } from "react";
|
||||
import { fetchMessages } from "../../lib/api";
|
||||
import { useAppStore } from "../../store/useAppStore";
|
||||
import { SplitChatBubble } from "../chat";
|
||||
|
||||
/** Page size for the history cursor (design/16 §5.1); the wire returns rows
|
||||
* newest-first, `fetchMessages` reverses them into display order for us. */
|
||||
const PAGE = 50;
|
||||
/** Conservative DOM cap: beyond this we stop mounting every bubble so an
|
||||
* extremely long session never explodes the DOM (design/16 §5.2). Older messages
|
||||
* already loaded into the store are still fetched on "load earlier"; we just
|
||||
* stop rendering the furthest-back ones. */
|
||||
const MAX_RENDER = 200;
|
||||
|
||||
/**
|
||||
* The scrollable message history for the focused session. Owns the "load earlier"
|
||||
* hot-pagination button at the top and a conservative render cap. Streaming
|
||||
* bubbles (the active turn) render exactly as before via SplitChatBubble.
|
||||
*/
|
||||
export function HistoryList({
|
||||
lastAssistantId,
|
||||
}: {
|
||||
lastAssistantId?: string;
|
||||
}): React.ReactNode {
|
||||
const messages = useAppStore((s) => s.messages);
|
||||
const activeSessionId = useAppStore((s) => s.activeSessionId);
|
||||
const prependMessages = useAppStore((s) => s.prependMessages);
|
||||
const [exhausted, setExhausted] = useState(false);
|
||||
const [loadingEarlier, setLoadingEarlier] = useState(false);
|
||||
|
||||
// The oldest loaded message's createdAt is the `before` cursor. We show the
|
||||
// button only after a full page has loaded (so there might be more) and until
|
||||
// an upward fetch returns a short page (signalling the beginning reached).
|
||||
const before = messages[0]?.createdAt;
|
||||
const showLoadEarlier = !exhausted && messages.length >= PAGE;
|
||||
|
||||
const loadEarlier = async () => {
|
||||
if (!activeSessionId || before === undefined || loadingEarlier) return;
|
||||
setLoadingEarlier(true);
|
||||
try {
|
||||
const older = await fetchMessages(activeSessionId, before, PAGE);
|
||||
prependMessages(older);
|
||||
if (older.length < PAGE) setExhausted(true);
|
||||
} catch {
|
||||
// Transient backend not wired / offline — user can retry; keep the button.
|
||||
} finally {
|
||||
setLoadingEarlier(false);
|
||||
}
|
||||
};
|
||||
|
||||
const collapsed = messages.length > MAX_RENDER;
|
||||
const visible = collapsed ? messages.slice(-MAX_RENDER) : messages;
|
||||
|
||||
return (
|
||||
<>
|
||||
{showLoadEarlier ? (
|
||||
<button
|
||||
onClick={() => void loadEarlier()}
|
||||
disabled={loadingEarlier}
|
||||
className="mx-auto flex items-center gap-2 rounded-[var(--mu-radius)] border border-[var(--mu-glass-border)] bg-[var(--mu-surface-3)] px-4 py-2 text-sm text-[var(--mu-text-mid)] transition-all duration-200 ease-[var(--mu-spring)] hover:bg-[var(--mu-surface-4)] active:scale-[0.98] disabled:opacity-60"
|
||||
>
|
||||
{loadingEarlier ? "加载中…" : "↥ 加载更早的消息"}
|
||||
</button>
|
||||
) : null}
|
||||
{collapsed ? (
|
||||
<div className="px-2 text-center text-[11px] text-[var(--mu-text-lo)]">
|
||||
已收起 {messages.length - MAX_RENDER} 条较早消息
|
||||
</div>
|
||||
) : null}
|
||||
{visible.map((m) => (
|
||||
<SplitChatBubble
|
||||
key={m.id}
|
||||
message={m}
|
||||
active={
|
||||
m.id === lastAssistantId &&
|
||||
m.role === "assistant" &&
|
||||
(m.streaming ?? false)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user