feat: 新增会话历史侧边栏,支持新建/切换/删除/重命名会话
类似 ChatGPT 的侧边栏布局,消息历史持久化到 localStorage。 - 新增 SessionSummary 类型和 localStorage 读写函数 - 新增 useSessionList Hook 管理会话列表 CRUD - 新增 SessionSidebar 组件(展开/折叠、行内重命名) - App.tsx 协调 useSessionList 和 useVisionSession - 消息变化时自动持久化,切换/新建会话时保存并加载 - 新增 i18n 翻译 key(中/英/日)
This commit is contained in:
@@ -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} />
|
||||
|
||||
Reference in New Issue
Block a user