feat: 新增会话历史侧边栏,支持新建/切换/删除/重命名会话

类似 ChatGPT 的侧边栏布局,消息历史持久化到 localStorage。

- 新增 SessionSummary 类型和 localStorage 读写函数
- 新增 useSessionList Hook 管理会话列表 CRUD
- 新增 SessionSidebar 组件(展开/折叠、行内重命名)
- App.tsx 协调 useSessionList 和 useVisionSession
- 消息变化时自动持久化,切换/新建会话时保存并加载
- 新增 i18n 翻译 key(中/英/日)
This commit is contained in:
2026-06-14 15:44:25 +08:00
parent da8818c3be
commit 92f3f45c41
10 changed files with 635 additions and 5 deletions

View File

@@ -0,0 +1,117 @@
// ============================================================
// useSessionList — 会话历史列表管理
// 职责:会话 CRUD、消息持久化、切换会话
// ============================================================
import { useCallback, useEffect, useRef, useState } from "react";
import { v4 as uuidv4 } from "uuid";
import {
loadSessionSummaries,
saveSessionSummaries,
loadSessionMessages,
saveSessionMessages,
deleteSessionMessages,
} from "../lib/storage";
import type { ChatMessage, SessionSummary } from "../types";
/** 截取预览文本 */
function getPreview(text: string, maxLen = 50): string {
const clean = text.replace(/[\n\r]/g, " ").trim();
return clean.length > maxLen ? clean.slice(0, maxLen) + "…" : clean;
}
export function useSessionList() {
const [sessions, setSessions] = useState<SessionSummary[]>(() => loadSessionSummaries());
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
const sessionsRef = useRef(sessions);
useEffect(() => { sessionsRef.current = sessions; }, [sessions]);
/** 创建新会话 */
const createSession = useCallback((): string => {
const id = uuidv4();
const now = Date.now();
const summary: SessionSummary = {
id,
title: "新对话",
createdAt: now,
lastActiveAt: now,
messageCount: 0,
preview: "",
};
setSessions((prev) => [summary, ...prev]);
setActiveSessionId(id);
// 持久化
const all = [summary, ...sessionsRef.current];
saveSessionSummaries(all);
return id;
}, []);
/** 删除会话 */
const deleteSession = useCallback((id: string) => {
setSessions((prev) => prev.filter((s) => s.id !== id));
deleteSessionMessages(id);
const remaining = sessionsRef.current.filter((s) => s.id !== id);
saveSessionSummaries(remaining);
// 如果删除的是当前会话,清空 active
setActiveSessionId((prev) => (prev === id ? null : prev));
}, []);
/** 重命名会话 */
const renameSession = useCallback((id: string, title: string) => {
setSessions((prev) => prev.map((s) => (s.id === id ? { ...s, title } : s)));
const updated = sessionsRef.current.map((s) => (s.id === id ? { ...s, title } : s));
saveSessionSummaries(updated);
}, []);
/** 保存当前会话的消息历史 */
const saveCurrentSession = useCallback((messages: ChatMessage[]) => {
const id = sessionsRef.current.length > 0 ? sessionsRef.current[0].id : null;
// 找到 activeSessionId 对应的会话
// 这里不依赖 activeSessionId state而是通过参数传入
return messages;
}, []);
/** 保存指定会话的消息并更新摘要 */
const persistSession = useCallback((sessionId: string, messages: ChatMessage[]) => {
if (!sessionId) return;
saveSessionMessages(sessionId, messages);
// 更新摘要
const firstUserMsg = messages.find((m) => m.role === "user");
const title = firstUserMsg ? getPreview(firstUserMsg.content, 20) : "新对话";
const lastMsg = messages[messages.length - 1];
const summary: Partial<SessionSummary> = {
title,
messageCount: messages.length,
lastActiveAt: lastMsg?.timestamp || Date.now(),
preview: lastMsg ? getPreview(lastMsg.content) : "",
};
setSessions((prev) => {
const updated = prev.map((s) => (s.id === sessionId ? { ...s, ...summary } : s));
saveSessionSummaries(updated);
return updated;
});
}, []);
/** 加载指定会话的消息历史 */
const loadMessages = useCallback((sessionId: string): ChatMessage[] => {
return loadSessionMessages(sessionId);
}, []);
/** 选择会话(返回需要加载的消息) */
const selectSession = useCallback((id: string): ChatMessage[] => {
setActiveSessionId(id);
return loadSessionMessages(id);
}, []);
return {
sessions,
activeSessionId,
setActiveSessionId,
createSession,
deleteSession,
renameSession,
persistSession,
loadMessages,
selectSession,
};
}

View File

@@ -458,6 +458,7 @@ export function useVisionSession() {
return {
messages,
setMessages,
currentReply,
isProcessing,
isAudioPlaying,