前端 + AI 进阶 Day 9: 多轮对话状态管理

前端 + AI 进阶学习路线|Week 9-10:对话式界面设计

Day 9:多轮对话状态管理

学习时间:2026年1月2日(星期五)
关键词:多会话管理、上下文记忆、本地持久化、会话切换、localStorage


📁 项目文件结构

day09-multi-session-chat/
├── src/
│   ├── components/
│   │   ├── SessionSidebar.jsx       # 会话列表侧边栏
│   │   ├── ChatWindow.jsx           # 聊天主窗口(复用 Day 4 流式组件)
│   │   └── NewSessionButton.jsx     # 新建会话按钮
│   ├── hooks/
│   │   └── useSessionManager.js     # 会话状态管理 Hook
│   ├── lib/
│   │   └── aiClient.js              # 复用 Day 4 的 AI 客户端
│   └── App.jsx                      # 主应用集成
└── public/

✅ 本日核心:支持多个独立 AI 对话会话,且关闭页面后不丢失


🎯 今日学习目标

  1. 实现 多会话创建与切换(每个会话独立上下文)
  2. 本地持久化 所有会话历史(使用 localStorage
  3. 支持 会话重命名 / 删除
  4. 构建专业级 AI 聊天工作台基础

💡 为什么需要多会话管理?

在真实使用中,用户常需:

  • 同时处理 多个任务(如“写代码” + “查资料”)
  • 隔离上下文(避免不同话题混淆)
  • 保留历史(关闭浏览器后仍可继续对话)

✅ 单一会话体验简陋,多会话 + 持久化 是专业 AI 应用的标配


📚 核心设计思路

功能 实现方式
会话数据结构 { id, title, messages: [{role, content}], createdAt }
状态管理 React Context + useState + useEffect
本地持久化 localStorage + JSON 序列化(带错误处理)
默认会话 首次访问自动创建“新会话”

⚠️ 注意:localStorage 有 5-10MB 限制,需定期清理或分页


🔧 动手实践:构建多会话聊天工作台

步骤 1:创建项目并安装依赖

npx create-react-app day09-multi-session-chat
cd day09-multi-session-chat
npm install react-virtual react-markdown remark-gfm rehype-highlight rehype-katex katex @microsoft/fetch-event-source

💡 复用 Day 4 的 aiClient.js 和流式组件

步骤 2:创建会话管理 Hook

// src/hooks/useSessionManager.js
import { useState, useEffect } from 'react';

const SESSIONS_KEY = 'ai_chat_sessions';
const CURRENT_SESSION_KEY = 'ai_current_session_id';

// 生成会话 ID
const generateSessionId = () => Date.now().toString(36) + Math.random().toString(36).substr(2, 5);

// 默认欢迎消息
const createNewSession = (title = '新会话') => ({
  id: generateSessionId(),
  title,
  messages: [
    { id: 'welcome', role: 'assistant', content: '你好!我是你的 AI 助手,有什么可以帮你的吗?' }
  ],
  createdAt: Date.now(),
});

export const useSessionManager = () => {
  const [sessions, setSessions] = useState(() => {
    // 从 localStorage 读取
    const saved = localStorage.getItem(SESSIONS_KEY);
    if (saved) {
      try {
        return JSON.parse(saved);
      } catch (e) {
        console.error('Failed to parse sessions from localStorage');
      }
    }
    // 首次访问,创建默认会话
    const defaultSession = createNewSession();
    return [defaultSession];
  });

  const [currentSessionId, setCurrentSessionId] = useState(() => {
    const savedId = localStorage.getItem(CURRENT_SESSION_KEY);
    if (savedId && sessions.find(s => s.id === savedId)) {
      return savedId;
    }
    return sessions[0]?.id || null;
  });

  // 持久化 sessions
  useEffect(() => {
    try {
      localStorage.setItem(SESSIONS_KEY, JSON.stringify(sessions));
    } catch (e) {
      console.error('Failed to save sessions to localStorage', e);
    }
  }, [sessions]);

  // 持久化 currentSessionId
  useEffect(() => {
    if (currentSessionId) {
      localStorage.setItem(CURRENT_SESSION_KEY, currentSessionId);
    }
  }, [currentSessionId]);

  // 创建新会话
  const createSession = (title) => {
    const newSession = createNewSession(title);
    setSessions(prev => [newSession, ...prev]);
    setCurrentSessionId(newSession.id);
    return newSession.id;
  };

  // 切换会话
  const switchSession = (id) => {
    if (sessions.find(s => s.id === id)) {
      setCurrentSessionId(id);
    }
  };

  // 更新会话消息
  const updateSessionMessages = (sessionId, newMessages) => {
    setSessions(prev => 
      prev.map(session => 
        session.id === sessionId 
          ? { ...session, messages: newMessages }
          : session
      )
    );
  };

  // 更新会话标题
  const updateSessionTitle = (sessionId, title) => {
    setSessions(prev => 
      prev.map(session => 
        session.id === sessionId 
          ? { ...session, title }
          : session
      )
    );
  };

  // 删除会话
  const deleteSession = (sessionId) => {
    setSessions(prev => {
      const filtered = prev.filter(s => s.id !== sessionId);
      // 如果删除的是当前会话,切换到第一个
      if (sessionId === currentSessionId && filtered.length > 0) {
        setCurrentSessionId(filtered[0].id);
      }
      return filtered;
    });
  };

  const currentSession = sessions.find(s => s.id === currentSessionId) || null;

  return {
    sessions,
    currentSession,
    createSession,
    switchSession,
    updateSessionMessages,
    updateSessionTitle,
    deleteSession,
  };
};

步骤 3:创建会话侧边栏组件

// src/components/SessionSidebar.jsx
import { useState } from 'react';

const SessionSidebar = ({ 
  sessions, 
  currentSessionId, 
  onSwitchSession, 
  onCreateSession, 
  onUpdateTitle, 
  onDeleteSession 
}) => {
  return (
    <div style={{ width: '240px', borderRight: '1px solid #e8e8e8', display: 'flex', flexDirection: 'column' }}>
      <div style={{ padding: '16px', paddingBottom: '8px' }}>
        <button
          onClick={() => onCreateSession(prompt('会话标题:', '新会话') || '新会话')}
          style={{
            width: '100%',
            padding: '8px',
            backgroundColor: '#1890ff',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            fontWeight: '500',
          }}
        >
          ➕ 新建会话
        </button>
      </div>

      <div style={{ flex: 1, overflowY: 'auto' }}>
        {sessions.map((session) => (
          <SessionItem
            key={session.id}
            session={session}
            isActive={session.id === currentSessionId}
            onSwitch={() => onSwitchSession(session.id)}
            onUpdateTitle={(title) => onUpdateTitle(session.id, title)}
            onDelete={() => {
              if (window.confirm(`确定删除会话 "${session.title}"?`)) {
                onDeleteSession(session.id);
              }
            }}
          />
        ))}
      </div>
    </div>
  );
};

// 单个会话项
const SessionItem = ({ session, isActive, onSwitch, onUpdateTitle, onDelete }) => {
  const [isEditing, setIsEditing] = useState(false);
  const [editTitle, setEditTitle] = useState(session.title);

  const handleTitleBlur = () => {
    if (editTitle.trim() && editTitle !== session.title) {
      onUpdateTitle(editTitle.trim());
    }
    setIsEditing(false);
  };

  return (
    <div
      style={{
        padding: '10px 16px',
        cursor: 'pointer',
        backgroundColor: isActive ? '#e6f7ff' : 'transparent',
        borderLeft: isActive ? '3px solid #1890ff' : 'none',
        display: 'flex',
        justifyContent: 'space-between',
        alignItems: 'center',
      }}
      onClick={onSwitch}
    >
      {isEditing ? (
        <input
          value={editTitle}
          onChange={(e) => setEditTitle(e.target.value)}
          onBlur={handleTitleBlur}
          onKeyDown={(e) => e.key === 'Enter' && handleTitleBlur()}
          autoFocus
          style={{ 
            flex: 1, 
            border: '1px solid #d9d9d9', 
            borderRadius: '3px',
            padding: '2px 4px',
            fontSize: '14px'
          }}
        />
      ) : (
        <div 
          style={{ 
            flex: 1, 
            whiteSpace: 'nowrap', 
            overflow: 'hidden', 
            textOverflow: 'ellipsis',
            fontSize: '14px'
          }}
          onDoubleClick={() => setIsEditing(true)}
          title={session.title}
        >
          {session.title}
        </div>
      )}
      
      <button
        onClick={(e) => {
          e.stopPropagation();
          onDelete();
        }}
        style={{
          background: 'none',
          border: 'none',
          color: '#ff4d4f',
          cursor: 'pointer',
          fontSize: '16px',
          marginLeft: '8px',
        }}
      >
        &times;
      </button>
    </div>
  );
};

export default SessionSidebar;

步骤 4:集成主应用(App.jsx)

// src/App.jsx
import 'katex/dist/katex.min.css';
import { useSessionManager } from './hooks/useSessionManager';
import SessionSidebar from './components/SessionSidebar';
import ChatWindow from './components/ChatWindow'; // 复用 Day 4 的流式聊天组件

function App() {
  const {
    sessions,
    currentSession,
    createSession,
    switchSession,
    updateSessionMessages,
    updateSessionTitle,
    deleteSession,
  } = useSessionManager();

  if (!currentSession) {
    return <div style={{ padding: '20px' }}>加载中...</div>;
  }

  return (
    <div style={{ display: 'flex', height: '100vh', fontFamily: 'Inter, -apple-system, sans-serif' }}>
      <SessionSidebar
        sessions={sessions}
        currentSessionId={currentSession.id}
        onSwitchSession={switchSession}
        onCreateSession={createSession}
        onUpdateTitle={updateSessionTitle}
        onDeleteSession={deleteSession}
      />
      
      <div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
        <div style={{ 
          padding: '16px 24px', 
          borderBottom: '1px solid #e8e8e8',
          backgroundColor: '#fff',
          fontSize: '16px',
          fontWeight: '600'
        }}>
          {currentSession.title}
        </div>
        
        <ChatWindow
          session={currentSession}
          onUpdateMessages={(newMessages) => 
            updateSessionMessages(currentSession.id, newMessages)
          }
        />
      </div>
    </div>
  );
}

export default App;

💡 ChatWindow.jsx 可直接复用 Day 4 的 StreamingChat.jsx,仅需调整 messagesonSend 逻辑以适配会话管理。


✅ 效果验证

  • ✅ 首次打开 → 自动创建“新会话”
  • ✅ 点击“新建会话” → 创建新会话并切换
  • ✅ 双击会话标题 → 可编辑重命名
  • ✅ 点击 × → 删除会话(确认弹窗)
  • ✅ 刷新页面 → 所有会话和当前状态保留
  • ✅ 不同会话消息完全隔离

🤔 思考与延伸

  1. 存储优化localStorage 有大小限制,如何处理大量会话?
    → 可按时间归档,或切换到 IndexedDB

  2. 同步支持:如何实现多设备同步?
    → 集成 Firebase 或自建后端 API

  3. 会话搜索:如何快速查找历史会话?
    → 在侧边栏增加搜索框,按标题/内容过滤

💡 为 Week 2 做准备:此会话管理逻辑可抽象为 useAISession Hook,纳入 ai-frontend-kit


📅 明日预告

Day 10:富交互消息

  • 在消息流中嵌入 按钮 / 卡片 / 表单
  • 支持用户点击按钮触发 AI 动作
  • 构建 AI 原生交互范式

✍️ 小结

今天,我们让 AI 聊天从“单线对话”升级为“多任务工作台”!通过多会话管理与本地持久化,用户可同时处理不同任务,且历史永不丢失。会话隔离与持久化,是专业 AI 应用的基石。

💬 实践提示:如果遇到 localStorage 满的错误,可在 useEffect 中添加清理逻辑(如保留最近 50 个会话)。欢迎分享你的多会话管理设计!

posted @ 2025-12-30 09:29  XiaoZhengTou  阅读(34)  评论(0)    收藏  举报