feat(web): 前端接会话持久化——拉历史热分页 + 侧栏会话列表
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- 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:
Aska committed 2026-09-07 03:52:50 +08:00
1 parent 48421923f6
commit bcc53d68f3
9 files changed
+371 -32

No files matched your search

+7 -13
View File
@@ -3,7 +3,7 @@ import { useAppStore } from "../store/useAppStore";
import { useModeStore } from "../store/useModeStore";
import { useSendChat } from "../lib/useChat";
import { sendUp } from "../lib/ws";
import { SplitChatBubble } from "../components/chat";
import { HistoryList } from "../components/messages/HistoryList";
import { ChatInput } from "../components/ChatInput";
import { EmptyState } from "../components/primitives";
import { Icon } from "../components/Icon";
@@ -49,9 +49,13 @@ export function ChatShell(): React.ReactNode {
return undefined;
}, [messages]);
// Scroll to the newest message only when a *new* message reaches the tail
// (append/session-switch), not when the user hot-pages older history into the
// list — otherwise "load earlier" would yank the viewport to the bottom.
const lastMessageId = messages[messages.length - 1]?.id;
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: "smooth", block: "end" });
}, [messages]);
}, [lastMessageId]);
return (
<div className="flex h-full flex-col">
@@ -77,17 +81,7 @@ export function ChatShell(): React.ReactNode {
hint="模式只改变消息框的呈现方式,会话上下文与记忆保持不变"
/>
) : (
messages.map((m) => (
<SplitChatBubble
key={m.id}
message={m}
active={
m.id === lastAssistantId &&
m.role === "assistant" &&
(m.streaming ?? false)
}
/>
))
<HistoryList lastAssistantId={lastAssistantId} />
)}
<div ref={bottomRef} />
</div>