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
@@ -1,6 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { applyTheme, readTheme, type ThemeMode } from "../lib/theme";
|
||||
import { getAuthToken } from "../lib/api";
|
||||
import { createSubSession, getAuthToken } from "../lib/api";
|
||||
import { useAppStore } from "../store/useAppStore";
|
||||
import { Icon, type IconName } from "./Icon";
|
||||
import { Tag } from "./primitives";
|
||||
@@ -20,7 +20,12 @@ import { Tag } from "./primitives";
|
||||
function NavContent({ onNavigate }: { onNavigate: () => void }): React.ReactNode {
|
||||
const wsStatus = useAppStore((s) => s.wsStatus);
|
||||
const mood = useAppStore((s) => s.mood);
|
||||
const sessions = useAppStore((s) => s.sessions);
|
||||
const activeSessionId = useAppStore((s) => s.activeSessionId);
|
||||
const setSessions = useAppStore((s) => s.setSessions);
|
||||
const setActiveSession = useAppStore((s) => s.setActiveSession);
|
||||
const [theme, setTheme] = useState<ThemeMode>(readTheme);
|
||||
const [creating, setCreating] = useState(false);
|
||||
const connected = wsStatus === "connected";
|
||||
const hasToken = !!getAuthToken();
|
||||
|
||||
@@ -30,6 +35,35 @@ function NavContent({ onNavigate }: { onNavigate: () => void }): React.ReactNode
|
||||
setTheme(next);
|
||||
};
|
||||
|
||||
// Main session pinned first; secondary sessions follow by creation time.
|
||||
const sessionList = Object.values(sessions).sort((a, b) => {
|
||||
if (a.type !== b.type) return a.type === "main" ? -1 : 1;
|
||||
return a.createdAt - b.createdAt;
|
||||
});
|
||||
|
||||
const selectSession = (id: string) => {
|
||||
setActiveSession(id);
|
||||
onNavigate();
|
||||
};
|
||||
|
||||
const createSession = async () => {
|
||||
if (creating) return;
|
||||
setCreating(true);
|
||||
try {
|
||||
const meta = await createSubSession();
|
||||
// Optimistically append so the new session shows immediately; the backend
|
||||
// will return it on the next GET /sessions too.
|
||||
const next = Object.values(useAppStore.getState().sessions);
|
||||
setSessions([...next, ...(next.some((s) => s.id === meta.id) ? [] : [meta])]);
|
||||
setActiveSession(meta.id);
|
||||
onNavigate();
|
||||
} catch {
|
||||
// Backend not wired yet — leave the list unchanged.
|
||||
} finally {
|
||||
setCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center gap-2 px-2 py-2">
|
||||
@@ -55,6 +89,32 @@ function NavContent({ onNavigate }: { onNavigate: () => void }): React.ReactNode
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* conversation sessions (design/16 §2) — main fixed, secondary created */}
|
||||
{hasToken ? (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="px-2 pt-1 text-[11px] text-[var(--mu-text-lo)]">
|
||||
会话
|
||||
</div>
|
||||
{sessionList.map((s) => (
|
||||
<NavItem
|
||||
key={s.id}
|
||||
active={s.id === activeSessionId}
|
||||
icon={s.type === "main" ? "chat" : "folder"}
|
||||
label={s.title ?? (s.type === "main" ? "主会话" : "次级会话")}
|
||||
onClick={() => selectSession(s.id)}
|
||||
/>
|
||||
))}
|
||||
<button
|
||||
onClick={() => void createSession()}
|
||||
disabled={creating}
|
||||
className="flex items-center gap-2.5 rounded-[var(--mu-radius)] px-3 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"
|
||||
>
|
||||
<Icon name="chat" size={18} />
|
||||
<span>{creating ? "创建中…" : "+ 新建会话"}</span>
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="mt-auto flex flex-col gap-1">
|
||||
<NavItem
|
||||
icon="settings"
|
||||
|
||||
Reference in new issue
Block a user