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

@@ -1,12 +1,14 @@
// ============================================================
// CamTalk — 主应用组件Web 端栏布局)
// CamTalk — 主应用组件Web 端栏布局:侧边栏 + 视频 + 聊天
// ============================================================
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useVisionSession } from "./hooks/useVisionSession";
import { useSessionList } from "./hooks/useSessionList";
import { VideoPreview } from "./components/VideoPreview";
import { ChatPanel } from "./components/ChatPanel";
import { ConfigPanel } from "./components/ConfigPanel";
import { SessionSidebar } from "./components/SessionSidebar";
import { ToastContainer } from "./components/Toast";
import { loadConfig, loadTheme, saveTheme } from "./lib/storage";
import { I18nContext, parseLocale, t } from "./lib/i18n";
@@ -14,11 +16,12 @@ import type { Locale } from "./lib/i18n";
import type { Theme } from "./types";
import "./App.css";
/** 内部组件,确保在 I18nContext.Provider 内部使用 useVisionSession */
/** 内部组件,确保在 I18nContext.Provider 内部使用 hooks */
function AppContent() {
const [showConfig, setShowConfig] = useState(false);
const [theme, setTheme] = useState<Theme>(loadTheme);
const [elapsed, setElapsed] = useState(0);
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
// 切换主题时更新 <html> 的 data-theme 属性
@@ -37,8 +40,21 @@ function AppContent() {
return `${m.toString().padStart(2, "0")}:${sec.toString().padStart(2, "0")}`;
};
// ---- 会话列表 ----
const {
sessions,
activeSessionId,
createSession,
deleteSession,
renameSession,
persistSession,
selectSession,
} = useSessionList();
// ---- 视觉会话 ----
const {
messages,
setMessages,
currentReply,
isProcessing,
isAudioPlaying,
@@ -93,6 +109,58 @@ function AppContent() {
const { t: tr } = useMemo(() => ({ t: (key: string) => t(key, parseLocale(config.language)) }), [config.language]);
// ---- 初始化:如果没有会话,创建一个 ----
const initializedRef = useRef(false);
useEffect(() => {
if (!initializedRef.current) {
initializedRef.current = true;
if (sessions.length === 0) {
createSession();
}
}
}, [sessions.length, createSession]);
// ---- 自动保存messages 变化时持久化到当前会话 ----
const messagesRef = useRef(messages);
useEffect(() => { messagesRef.current = messages; }, [messages]);
useEffect(() => {
if (activeSessionId && messages.length > 0) {
persistSession(activeSessionId, messages);
}
}, [messages, activeSessionId, persistSession]);
// ---- 侧边栏操作 ----
const handleNewSession = useCallback(() => {
createSession();
setMessages([]);
// 如果已连接,断开
if (connectionStatus === "connected") {
stopSession();
}
}, [createSession, setMessages, connectionStatus, stopSession]);
const handleSelectSession = useCallback((id: string) => {
// 保存当前会话
if (activeSessionId && messagesRef.current.length > 0) {
persistSession(activeSessionId, messagesRef.current);
}
// 如果已连接,断开
if (connectionStatus === "connected") {
stopSession();
}
// 加载目标会话
const loaded = selectSession(id);
setMessages(loaded);
}, [activeSessionId, persistSession, connectionStatus, stopSession, selectSession, setMessages]);
const handleDeleteSession = useCallback((id: string) => {
deleteSession(id);
if (id === activeSessionId) {
setMessages([]);
}
}, [deleteSession, activeSessionId, setMessages]);
return (
<div className="app">
{/* ---- 顶部导航栏 ---- */}
@@ -134,9 +202,21 @@ function AppContent() {
/>
)}
{/* ---- 主体:侧视频 + 右侧聊天 ---- */}
{/* ---- 主体:侧边栏 + 视频 + 聊天 ---- */}
<div className="workspace">
{/* 左侧:视频预览 + 控制栏 */}
{/* 左侧:会话历史侧边栏 */}
<SessionSidebar
sessions={sessions}
activeSessionId={activeSessionId}
collapsed={sidebarCollapsed}
onToggleCollapse={() => setSidebarCollapsed((v) => !v)}
onNewSession={handleNewSession}
onSelectSession={handleSelectSession}
onDeleteSession={handleDeleteSession}
onRenameSession={renameSession}
/>
{/* 中间:视频预览 + 控制栏 */}
<div className="video-panel">
<div className="video-container">
<VideoPreview ref={videoRef} isStreaming={!!stream} />