diff --git a/backend/api/route.py b/backend/api/route.py index 331b6c0..e68013e 100644 --- a/backend/api/route.py +++ b/backend/api/route.py @@ -1,18 +1,20 @@ from fastapi import APIRouter from ..core.items import ChatRequest from ..tools.get_all_role_and_chat import get_all_role_and_chat -from ..tools.save_input_to_json import save_input_to_json +from ..core.models import chat_history router = APIRouter() -# 1. 将输入内容持久化存储到本地jsonl方便前端读 -@router.post("/generate_reply") -async def save_chat_to_json(chat_request: ChatRequest): - # 调用实际的保存函数 - return await save_input_to_json(chat_request) - -# 2. 从本地jsonl中读取历史对话 +# 1. 从本地读取所有的data内容 @router.get("/tool_bar/get_all_role_and_chat") def get_all_role_and_chat_endpoint(): # 正确调用函数并返回结果 return get_all_role_and_chat() + +# 2. 根据rolename和chatname读取特定聊天记录 +@router.post("/chat_box/get_chat_history") +async def get_chat_history_endpoint(role_name: str, chat_name: str): + # 实例化工具类 + reader = chat_history.load_from_file(role_name, chat_name) + + return reader.to_chatbox_format() diff --git a/backend/core/models/chat_history.py b/backend/core/models/chat_history.py index dddb861..e9dc7a4 100644 --- a/backend/core/models/chat_history.py +++ b/backend/core/models/chat_history.py @@ -7,152 +7,224 @@ import json class Message(BaseModel): """消息类,代表JSONL文件中的一行消息内容""" - name: str = Field(..., description="发言者名称") - is_user: bool = Field(..., description="是否为用户消息(true=用户,false=AI/角色)") - is_system: bool = Field(False, description="是否为系统消息(系统消息在文本导出时会被排除)") - send_date: str = Field(default_factory=lambda: str(int(datetime.now().timestamp() * 1000)), - description="发送时间戳(Unix毫秒数)") - mes: str = Field(..., description="消息正文内容") - extra: Dict[str, Any] = Field(default_factory=dict, description="额外信息,包含推理内容、API、模型等") - swipes: List[str] = Field(default_factory=list, description="备选回复列表") - swipe_id: int = Field(0, description="当前选中的备选索引(0=第一条)") - swipe_info: List[Dict[str, Any]] = Field(default_factory=list, description="每个备选回复的生成信息") - title: str = Field("", description="消息标题,用于消息摘要或分支标记") - force_avatar: Optional[str] = Field(None, description="强制头像路径") - variables: List[Any] = Field(default_factory=list, description="变量值数组") + name: str = Field(..., description="发送者名称") + is_user: bool = Field(..., description="是否为用户消息") + is_system: bool = Field(False, description="是否为系统消息") + send_date: str = Field( + default_factory=lambda: str(int(datetime.now().timestamp() * 1000)), + description="消息发送时间戳" + ) + floor: int = Field(0, description="对话楼层数") + mes: str = Field(..., description="消息内容文本") + extra: Dict[str, Any] = Field( + default_factory=dict, + description="额外信息,包含推理内容、API、模型等" + ) + force_avatar: Optional[str] = Field(None, description="强制头像URL") + variables: List[Any] = Field(default_factory=list, description="消息变量列表") variables_initialized: List[bool] = Field(default_factory=list, description="变量初始化状态数组") - is_ejs_processed: List[bool] = Field(default_factory=list, description="EJS模板处理状态数组") - gen_started: Optional[str] = Field(None, description="生成开始时间戳(Unix毫秒数)") - gen_finished: Optional[str] = Field(None, description="生成结束时间戳(Unix毫秒数)") + is_ejs_processed: List[bool] = Field(default_factory=list, description="EJS处理状态数组") + + # 以下属性仅在is_user为False时有值 + api: Optional[str] = Field(None, description="使用的API提供商") + model: Optional[str] = Field(None, description="使用的AI模型") + reasoning: Optional[str] = Field(None, description="推理内容") + reasoning_duration: Optional[float] = Field(None, description="推理耗时") + reasoning_signature: Optional[str] = Field(None, description="推理签名") + time_to_first_token: Optional[float] = Field(None, description="首Token响应时间") + bias: Optional[float] = Field(None, description="偏差值") class ChatMetadata(BaseModel): - """聊天元数据模型,代表JSONL文件的第一行内容""" - integrity: str = Field("", description="完整性校验哈希值(UUID格式)") + """聊天元数据类,包含整个聊天的共享属性""" + user_name: str = Field("User", description="用户名称") + character_name: str = Field("Assistant", description="角色名称") + + # 完整性校验相关 + integrity: str = Field("", description="完整性校验值") chat_id_hash: str = Field("", description="聊天ID哈希值") - note_prompt: str = Field("", description="笔记提示词") - note_interval: int = Field(0, description="笔记间隔(整数)") - note_position: int = Field(0, description="笔记位置(整数)") - note_depth: int = Field(0, description="笔记深度(整数)") - note_role: int = Field(0, description="笔记角色(整数,0=用户,1=助手)") - extensions: Dict[str, Any] = Field(default_factory=dict, description="扩展信息,如LittleWhiteBox等") - timedWorldInfo: Dict[str, Any] = Field(default_factory=dict, description="定时世界信息") - variables: Dict[str, Any] = Field(default_factory=dict, description="变量字典") - tainted: bool = Field(False, description="是否被污染") - lastInContextMessageId: int = Field(-1, description="上下文中最后一条消息的ID") + + # 笔记相关 + note_prompt: str = Field("", description="作者笔记提示词") + note_interval: int = Field(0, description="笔记插入间隔数") + note_position: int = Field(0, description="笔记插入位置") + note_depth: int = Field(0, description="笔记插入深度") + note_role: str = Field("", description="笔记使用角色类型") + + # 扩展信息 + extensions: Dict[str, Any] = Field( + default_factory=dict, + description="扩展信息,如LittleWhiteBox等" + ) + # 世界信息 + timedWorldInfo: Dict[str, Any] = Field( + default_factory=dict, + description="定时世界信息" + ) + # 变量 + variables: Dict[str, Any] = Field( + default_factory=dict, + description="变量字典" + ) + # 状态标记 + tainted: bool = Field(False, description="是否被修改标记") + lastInContextMessageId: int = Field(-1, description="最后上下文消息ID") -class ChatFile(BaseModel): - """聊天文件类,包含元数据和消息列表""" - user_name: str = Field("User", description="用户名") - character_name: str = Field("Assistant", description="角色名") - create_date: str = Field(default_factory=lambda: datetime.now().isoformat(), description="创建日期(ISO 8601格式)") - chat_metadata: ChatMetadata = Field(default_factory=ChatMetadata, description="聊天元数据") +class ChatHistory(BaseModel): + """聊天文件类,包含完整的聊天记录""" + chat_metadata: ChatMetadata = Field(..., description="聊天元数据,包含基本信息和配置") messages: List[Message] = Field(default_factory=list, description="消息列表") class Config: arbitrary_types_allowed = True + @classmethod # 类方法装饰器,表示这是一个类方法,可以通过类名直接调用 + def load_from_file(cls, role_name: str, chat_name: str, base_path: Path = None) -> 'ChatHistory': + """ + 从JSONL文件加载聊天历史 -def load_chat_file_data(chat_name: str, role_name: str, base_path: Path = None) -> Dict[str, Any]: - """ - 从文件系统加载聊天原始数据 + 参数: + role_name: 角色名称(文件夹名) + chat_name: 聊天名称(文件名,不含扩展名) + base_path: 基础路径,默认为配置中的DATA_PATH/chat - 参数: - chat_name: 聊天名称 - role_name: 角色名称 - base_path: 基础路径,默认为项目数据目录 + 返回: + ChatHistory: 加载的聊天历史对象 - 返回: - dict: 包含元数据和消息列表的原始数据字典 - """ - # 设置默认基础路径 - if base_path is None: - from backend.core.config import settings - base_path = settings.DATA_PATH / "chat" + 异常: + FileNotFoundError: 当文件不存在时抛出 + json.JSONDecodeError: 当JSON解析失败时抛出 + """ + # 设置默认基础路径 - 如果未提供base_path,则从配置中获取默认路径 + if base_path is None: + from backend.core.config import settings # 延迟导入配置模块 + base_path = settings.DATA_PATH / "chat" # 构建默认路径 - # 构建文件路径 - file_path = base_path / role_name / f"{chat_name}.jsonl" + # 构建文件路径 + file_path = base_path / role_name / f"{chat_name}.jsonl" - # 检查文件是否存在 - if not file_path.exists(): - raise FileNotFoundError(f"聊天文件不存在: {file_path}") + # 检查文件是否存在 + if not file_path.exists(): + raise FileNotFoundError(f"聊天文件不存在: {file_path}") - # 读取文件内容 - result = { - "user_name": "User", - "character_name": role_name, - "create_date": datetime.now().isoformat(), - "chat_metadata": {}, - "messages": [] - } + # 初始化结果数据 + messages = [] + metadata = None - with open(file_path, 'r', encoding='utf-8') as f: - for line in f: - try: - # 解析JSON行 - line_data = json.loads(line.strip()) + # 读取文件内容 + with open(file_path, 'r', encoding='utf-8') as f: + for line_num, line in enumerate(f): + try: + line_data = json.loads(line.strip()) - # 添加到消息列表 - result["messages"].append(line_data) - except json.JSONDecodeError: - continue + # 第一行是元数据 + if line_num == 0: + metadata = ChatMetadata(**line_data) + else: + # 后续行是消息 + messages.append(Message(**line_data)) + except json.JSONDecodeError: + continue - return result - - -def create_chat_file_from_data(data: Dict[str, Any]) -> ChatFile: - """ - 从原始数据创建ChatFile对象 - - 参数: - data: 包含元数据和消息列表的原始数据字典 - - 返回: - ChatFile: 创建的聊天文件对象 - """ - # 提取元数据 - metadata = data.get("chat_metadata", {}) - chat_metadata = ChatMetadata(**metadata) - - # 处理消息列表 - messages = [] - for msg_data in data.get("messages", []): - # 转换为Message对象 - message = Message( - name=msg_data.get('name', ''), - is_user=msg_data.get('is_user', False), - send_date=msg_data.get('send_date', ''), - mes=msg_data.get('content', ''), - swipes=msg_data.get('swipes', []), - swipe_id=msg_data.get('swipes_id', 0) + # 创建并返回ChatHistory对象 + return cls( + chat_metadata=metadata or ChatMetadata(), + messages=messages ) - messages.append(message) - # 创建并返回ChatFile对象 - return ChatFile( - user_name=data.get("user_name", "User"), - character_name=data.get("character_name", "Assistant"), - create_date=data.get("create_date", datetime.now().isoformat()), - chat_metadata=chat_metadata, - messages=messages - ) + @classmethod + def load_from_jsonl(cls, file_path: Path) -> 'ChatHistory': + """ + 从JSONL文件加载聊天历史 + 参数: + file_path: JSONL文件路径 -def load_chat_file(chat_name: str, role_name: str, base_path: Path = None) -> ChatFile: - """ - 从文件系统加载聊天数据并创建ChatFile对象 + 返回: + ChatHistory: 加载的聊天历史对象 - 参数: - chat_name: 聊天名称 - role_name: 角色名称 - base_path: 基础路径,默认为项目数据目录 + 异常: + FileNotFoundError: 当文件不存在时抛出 + json.JSONDecodeError: 当JSON解析失败时抛出 + """ + # 检查文件是否存在 + if not file_path.exists(): + raise FileNotFoundError(f"聊天文件不存在: {file_path}") - 返回: - ChatFile: 加载的聊天文件对象 - """ - # 加载原始数据 - data = load_chat_file_data(chat_name, role_name, base_path) + # 初始化结果数据 + messages = [] + metadata = None - # 创建ChatFile对象 - return create_chat_file_from_data(data) + # 读取文件内容 + with open(file_path, 'r', encoding='utf-8') as f: + for line_num, line in enumerate(f): + try: + line_data = json.loads(line.strip()) + + # 第一行是元数据 + if line_num == 0: + # 处理元数据中的嵌套结构 + if 'chat_metadata' in line_data: + metadata_dict = line_data['chat_metadata'] + # 合并顶层字段和chat_metadata中的字段 + metadata_dict.update(line_data) + metadata = ChatMetadata(**metadata_dict) + else: + metadata = ChatMetadata(**line_data) + else: + # 后续行是消息 + # 处理extra字段中的内容 + extra_data = line_data.get('extra', {}) + + # 如果是AI消息(is_user=False),将extra中的某些字段提升到顶层 + if not line_data.get('is_user', True): + ai_fields = ['api', 'model', 'reasoning', 'reasoning_duration', + 'reasoning_signature', 'time_to_first_token', 'bias'] + for field in ai_fields: + if field in extra_data: + line_data[field] = extra_data.pop(field) + + # 创建Message实例 + message = Message(**line_data) + # 将剩余的extra数据保存回extra字段 + message.extra = extra_data + messages.append(message) + + except json.JSONDecodeError: + continue + + # 创建并返回ChatHistory对象 + return cls( + chat_metadata=metadata or ChatMetadata(), + messages=messages + ) + + def to_chatbox_format(self) -> List[Dict[str, Any]]: + """ + 将聊天历史转换为适合前端chatbox显示的格式 + + 返回: + List[Dict[str, Any]]: 按floor排序的消息字典列表,每个字典包含: + { + "name": str, + "is_user": bool, + "floor": int, + "mes": str + } + """ + # 创建消息字典列表 + messages_list = [] + for msg in self.messages: + msg_dict = { + "name": msg.name, + "is_user": msg.is_user, + "floor": msg.floor, + "mes": msg.mes + } + messages_list.append(msg_dict) + + # 按floor排序 + messages_list.sort(key=lambda x: x["floor"]) + + return messages_list diff --git a/backend/tools/save_input_to_json.py b/backend/tools/save_input_to_json.py index f1a43ae..b79cdfe 100644 --- a/backend/tools/save_input_to_json.py +++ b/backend/tools/save_input_to_json.py @@ -140,7 +140,7 @@ if __name__ == '__main__': async def test(): req = MockChatRequest( mes="这是重roll后的新回复2", - role_name="test", + role_name="testRole1", chat_name="111", name="AI", is_user=False, diff --git a/data/chat/test/111.json b/data/chat/test/111.json deleted file mode 100644 index e69de29..0000000 diff --git a/data/chat/test/12331212.jsonl b/data/chat/test/12331212.jsonl deleted file mode 100644 index e69de29..0000000 diff --git a/data/chat/test/33211.jsonl b/data/chat/test/33211.jsonl deleted file mode 100644 index c3ebaf5..0000000 --- a/data/chat/test/33211.jsonl +++ /dev/null @@ -1,2 +0,0 @@ -{"role": "test", "chat": "111", "content": "你好", "name": "用户", "is_user": true, "send_date": "2026-03-12 18:26:50", "floor_number": 1, "swipes": [], "swipes_id": 0} -{"role": "test", "chat": "111", "content": "这是重roll后的新回复2", "name": "AI", "is_user": false, "send_date": "2026-03-12 18:26:50", "floor_number": 2, "swipes": ["这是重roll后的新回复", "这是重roll后的新回复2"], "swipes_id": 1} diff --git a/data/chat/test1/222.jsonl b/data/chat/test1/222.jsonl deleted file mode 100644 index e69de29..0000000 diff --git a/data/chat/testRole1/12331212.jsonl b/data/chat/testRole1/12331212.jsonl new file mode 100644 index 0000000..30f9980 --- /dev/null +++ b/data/chat/testRole1/12331212.jsonl @@ -0,0 +1,4 @@ +{"integrity": "test-uuid-2", "chat_id_hash": "hash2", "note_prompt": "探索神秘森林", "note_interval": 5, "note_position": 0, "note_depth": 2, "note_role": 1, "extensions": {}, "timedWorldInfo": {}, "variables": {"location": "forest"}, "tainted": false, "lastInContextMessageId": -1} +{"name": "System", "is_user": false, "is_system": true, "send_date": "1700000002000", "mes": "你进入了一片神秘的森林,周围充满了未知的危险。", "extra": {}, "swipes": [], "swipe_id": 0, "swipe_info": [], "title": "", "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "gen_started": null, "gen_finished": null} +{"name": "User", "is_user": true, "is_system": false, "send_date": "1700000003000", "mes": "我该往哪个方向走?", "extra": {}, "swipes": [], "swipe_id": 0, "swipe_info": [], "title": "", "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "gen_started": null, "gen_finished": null} +{"name": "Ranger", "is_user": false, "is_system": false, "send_date": "1700000004000", "mes": "东边有一条小路,但西边似乎有奇怪的声音。", "extra": {}, "swipes": ["东边有一条小路,但西边似乎有奇怪的声音。", "建议往东走,那边比较安全。", "小心西边,可能有野兽。"], "swipe_id": 0, "swipe_info": [], "title": "", "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "gen_started": null, "gen_finished": null} diff --git a/data/chat/testRole1/33211.jsonl b/data/chat/testRole1/33211.jsonl new file mode 100644 index 0000000..5700a36 --- /dev/null +++ b/data/chat/testRole1/33211.jsonl @@ -0,0 +1,3 @@ +{"integrity": "test-uuid-1", "chat_id_hash": "hash1", "note_prompt": "", "note_interval": 0, "note_position": 0, "note_depth": 0, "note_role": 0, "extensions": {}, "timedWorldInfo": {}, "variables": {}, "tainted": false, "lastInContextMessageId": -1} +{"name": "User", "is_user": true, "is_system": false, "send_date": "1700000000000", "mes": "你好,我是新来的冒险者。", "extra": {}, "swipes": [], "swipe_id": 0, "swipe_info": [], "title": "", "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "gen_started": null, "gen_finished": null} +{"name": "Guide", "is_user": false, "is_system": false, "send_date": "1700000001000", "mes": "欢迎来到我们的世界!你需要什么帮助?", "extra": {}, "swipes": [], "swipe_id": 0, "swipe_info": [], "title": "", "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "gen_started": null, "gen_finished": null} diff --git a/data/chat/testRole2/222.jsonl b/data/chat/testRole2/222.jsonl new file mode 100644 index 0000000..5ea8768 --- /dev/null +++ b/data/chat/testRole2/222.jsonl @@ -0,0 +1,4 @@ +{"integrity": "test-uuid-3", "chat_id_hash": "hash3", "note_prompt": "酒馆闲聊", "note_interval": 0, "note_position": 0, "note_depth": 0, "note_role": 0, "extensions": {}, "timedWorldInfo": {}, "variables": {}, "tainted": false, "lastInContextMessageId": -1} +{"name": "User", "is_user": true, "is_system": false, "send_date": "1700000005000", "mes": "有人知道关于龙的消息吗?", "extra": {}, "swipes": [], "swipe_id": 0, "swipe_info": [], "title": "", "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "gen_started": null, "gen_finished": null} +{"name": "Bard", "is_user": false, "is_system": false, "send_date": "1700000006000", "mes": "听说北边的山脉里有一条红龙,它守护着巨大的宝藏。", "extra": {}, "swipes": [], "swipe_id": 0, "swipe_info": [], "title": "", "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "gen_started": null, "gen_finished": null} +{"name": "Warrior", "is_user": false, "is_system": false, "send_date": "1700000007000", "mes": "别信那些谣言,我上周去过那里,什么都没发现。", "extra": {}, "swipes": [], "swipe_id": 0, "swipe_info": [], "title": "", "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejsprocessed": [], "gen_started": null, "gen_finished": null} diff --git a/frontend-react/src/App.jsx b/frontend-react/src/App.jsx index e7aec7f..535a64a 100644 --- a/frontend-react/src/App.jsx +++ b/frontend-react/src/App.jsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React from 'react'; import Toolbar from './components/ToolBar/ToolBar'; import ChatBox from './components/ChatBox/ChatBox'; import DicePanel from './components/DicePanel/DicePanel'; @@ -7,25 +7,9 @@ import PresetPanel from './components/PresetPanel/PresetPanel'; import './index.css'; function App() { - const [selectedRole, setSelectedRole] = useState(null); - const [selectedChat, setSelectedChat] = useState(null); - - const handleRoleChange = (role) => { - setSelectedRole(role); - console.log('角色已更改:', role); - }; - - const handleChatChange = (role, chat) => { - setSelectedChat(chat); - console.log('聊天已更改:', role, chat); - }; - return (
- + {/* 主内容容器 */}
@@ -36,10 +20,7 @@ function App() { {/* 中间栏:聊天框 */}
- +
{/* 右侧栏 */} diff --git a/frontend-react/src/Store/Slices/ChatBoxSlice.jsx b/frontend-react/src/Store/Slices/ChatBoxSlice.jsx new file mode 100644 index 0000000..1c20fb9 --- /dev/null +++ b/frontend-react/src/Store/Slices/ChatBoxSlice.jsx @@ -0,0 +1,95 @@ +import { create } from 'zustand'; +import { subscribeWithSelector } from 'zustand/middleware'; + +const useChatBoxStore = create( + subscribeWithSelector((set, get) => ({ + // 聊天历史消息列表 + messages: [], + + // 用户名称 + userName: '', + + // 角色名称 + characterName: '', + + // 当前选中的角色 + currentRole: null, + + // 当前选中的聊天 + currentChat: null, + + // 是否正在加载 + isLoading: false, + + // 错误信息 + error: null, + + // 设置消息列表 + setMessages: (messages) => set({ messages }), + + // 设置用户名称 + setUserName: (userName) => set({ userName }), + + // 设置角色名称 + setCharacterName: (characterName) => set({ characterName }), + + // 设置当前角色 + setCurrentRole: (role) => set({ currentRole: role }), + + // 设置当前聊天 + setCurrentChat: (chat) => set({ currentChat: chat }), + + // 加载聊天历史 + fetchChatHistory: async (roleName, chatName) => { + set({ isLoading: true, error: null }); + try { + const response = await fetch(`/api/chat_box/get_chat_history?role_name=${encodeURIComponent(roleName)}&chat_name=${encodeURIComponent(chatName)}`); + if (!response.ok) { + throw new Error('Failed to fetch chat history'); + } + const data = await response.json(); + set({ + messages: data.messages || [], + userName: data.userName || 'User', + characterName: data.characterName || 'Assistant', + isLoading: false + }); + } catch (error) { + set({ + error: error.message, + isLoading: false + }); + } + }, + + // 清空聊天历史 + clearChatHistory: () => set({ + messages: [], + userName: '', + characterName: '', + error: null + }), + + // 更新特定消息的内容 + updateMessage: (id, content) => set((state) => ({ + messages: state.messages.map((msg) => + msg.id === id ? { ...msg, content } : msg + ) + })), + })) +); + +// 监听角色和聊天变化,自动加载聊天历史 +useChatBoxStore.subscribe( + (state) => ({ role: state.currentRole, chat: state.currentChat }), + ({ role, chat }) => { + if (role && chat) { + useChatBoxStore.getState().fetchChatHistory(role, chat); + } else { + useChatBoxStore.getState().clearChatHistory(); + } + }, + { equalityFn: (a, b) => a.role === b.role && a.chat === b.chat } +); + +export default useChatBoxStore; diff --git a/frontend-react/src/components/ChatBox/ChatBox.css b/frontend-react/src/components/ChatBox/ChatBox.css index 2f7d178..c901b29 100644 --- a/frontend-react/src/components/ChatBox/ChatBox.css +++ b/frontend-react/src/components/ChatBox/ChatBox.css @@ -403,3 +403,27 @@ .send-button:hover { background-color: #40a9ff; } + +/* 加载状态样式 */ +.loading { + display: flex; + justify-content: center; + align-items: center; + padding: 20px; + color: #888; + font-size: 14px; +} + +/* 错误信息样式 */ +.error { + display: flex; + justify-content: center; + align-items: center; + padding: 20px; + color: #f5222d; + font-size: 14px; + background-color: rgba(245, 34, 45, 0.05); + border-radius: 4px; + margin: 10px 0; +} + diff --git a/frontend-react/src/components/ChatBox/ChatBox.jsx b/frontend-react/src/components/ChatBox/ChatBox.jsx index 4279067..8fe227b 100644 --- a/frontend-react/src/components/ChatBox/ChatBox.jsx +++ b/frontend-react/src/components/ChatBox/ChatBox.jsx @@ -1,7 +1,8 @@ import React, { useState, useRef } from 'react'; +import useChatBoxStore from '../../Store/Slices/ChatBoxSlice'; import './ChatBox.css'; -const ChatBox = ({ selectedRole, selectedChat }) => { +const ChatBox = () => { const [isHtmlRender, setIsHtmlRender] = useState(false); const [isImageGen, setIsImageGen] = useState(false); const [isDynamicTable, setIsDynamicTable] = useState(false); @@ -10,6 +11,9 @@ const ChatBox = ({ selectedRole, selectedChat }) => { const textareaRef = useRef(null); + // 从 store 获取状态 + const { messages, userName, characterName, isLoading, error } = useChatBoxStore(); + // 自动调整 Textarea 高度 const adjustHeight = () => { const textarea = textareaRef.current; @@ -60,36 +64,21 @@ const ChatBox = ({ selectedRole, selectedChat }) => { setEditContent(''); }; - // 生成示例数据 - const generateMessages = () => { - const messages = []; - for (let i = 1; i <= 150; i++) { - const isUser = i % 2 !== 0; - messages.push({ - id: i, - role: isUser ? 'user' : 'ai', - name: isUser ? '我' : 'AI助手', - content: isUser - ? `这是第 ${i} 条用户消息。这是一段比较长的文本,用来测试气泡的换行效果以及滚动条的表现。` - : `这是第 ${i} 条 AI 回复。包含 HTML 标签的内容。如果渲染开关开启,这里应该显示粗体字。如果不开启,应该显示原始标签。` - }); - } - return messages; - }; - - const messages = generateMessages(); - return (
{/* 上方:消息列表区域 */}
+ {/* 加载状态和错误信息 */} + {isLoading &&
加载中...
} + {error &&
{error}
} + {/* 消息列表 */} {messages.map((msg) => (
{/* 消息名称和工具栏在同一行 */}
-
{msg.name}
+
{msg.role === 'user' ? userName : characterName}
{/* 消息工具栏 */}
diff --git a/frontend-react/src/components/RoleSelector/RoleSelector.jsx b/frontend-react/src/components/RoleSelector/RoleSelector.jsx index fe110bf..eb740c9 100644 --- a/frontend-react/src/components/RoleSelector/RoleSelector.jsx +++ b/frontend-react/src/components/RoleSelector/RoleSelector.jsx @@ -1,8 +1,9 @@ import React, { useEffect, useRef } from 'react'; import useRoleSelectorStore from '../../store/Slices/RoleSelectorSlice'; +import useChatBoxStore from '../../Store/Slices/ChatBoxSlice'; import './RoleSelector.css'; -const RoleSelector = ({ onRoleChange, onChatChange }) => { +const RoleSelector = () => { const panelRef = useRef(null); // 从 Zustand store 中获取状态和操作 @@ -37,6 +38,9 @@ const RoleSelector = ({ onRoleChange, onChatChange }) => { resetPanel } = useRoleSelectorStore(); + // 从 ChatBoxStore 获取状态更新方法 + const { setCurrentRole, setCurrentChat } = useChatBoxStore(); + // 组件挂载时获取数据 useEffect(() => { fetchRoleData(); @@ -59,41 +63,56 @@ const RoleSelector = ({ onRoleChange, onChatChange }) => { // 处理角色选择 const handleRoleSelect = (role) => { setSelectedRole(role); - if (onRoleChange) { - onRoleChange(role); - } + // 更新 ChatBoxStore 中的当前角色 + setCurrentRole(role); // 如果该角色有聊天记录,默认选择第一个 if (roleData[role] && roleData[role].length > 0) { const firstChat = roleData[role][0]; setSelectedChat(firstChat); - if (onChatChange) { - onChatChange(role, firstChat); - } + // 更新 ChatBoxStore 中的当前聊天 + setCurrentChat(firstChat); } else { setSelectedChat(null); + // 清除 ChatBoxStore 中的当前聊天 + setCurrentChat(null); } }; // 处理聊天选择 const handleChatSelect = (chat) => { - setSelectedChat(chat); - setHoveredRole(null); - setClickedRole(null); - if (onChatChange) { - onChatChange(selectedRole, chat); - } + // 获取当前展开的角色(聊天所属的角色) + const currentRole = hoveredRole || clickedRole; + + setSelectedChat(chat); + setSelectedRole(currentRole); // 使用当前展开的角色 + setHoveredRole(null); + setClickedRole(null); + // 更新 ChatBoxStore 中的当前聊天和角色 + setCurrentChat(chat); + setCurrentRole(currentRole); }; // 处理角色卡片点击 - const handleRoleCardClick = (role) => { - if (clickedRole === role) { - setClickedRole(null); - } else { - setClickedRole(role); - handleRoleSelect(role); - } - }; + const handleRoleCardClick = (role) => { + if (clickedRole === role) { + setClickedRole(null); + // 取消选择角色时,更新 selectedRole 和 selectedChat + setSelectedRole(null); + setSelectedChat(null); + // 同步更新 ChatBoxStore 中的状态 + setCurrentRole(null); + setCurrentChat(null); + } else { + setClickedRole(role); + handleRoleSelect(role); + setSelectedRole(role); + setSelectedChat(chat); + // 同步更新 ChatBoxStore 中的状态 + setSele + } + }; + // 处理搜索 const handleSearchChange = (e) => { @@ -112,9 +131,8 @@ const RoleSelector = ({ onRoleChange, onChatChange }) => { const newName = e.target.value; handleRenameRole(oldName, newName); if (selectedRole === oldName && newName && newName !== oldName) { - if (onRoleChange) { - onRoleChange(newName); - } + // 更新 ChatBoxStore 中的当前角色 + setCurrentRole(newName); } }; @@ -130,9 +148,8 @@ const RoleSelector = ({ onRoleChange, onChatChange }) => { const newName = e.target.value; handleRenameChat(oldName, newName); if (selectedChat === oldName && newName && newName !== oldName) { - if (onChatChange) { - onChatChange(selectedRole, newName); - } + // 更新 ChatBoxStore 中的当前聊天 + setCurrentChat(selectedRole, newName); } }; @@ -147,13 +164,12 @@ const RoleSelector = ({ onRoleChange, onChatChange }) => { const confirmDeleteWrapper = () => { confirmDelete(); if (deleteType === 'role' && selectedRole === showDeleteConfirm) { - if (onRoleChange) { - onRoleChange(null); - } + // 清除 ChatBoxStore 中的当前角色和聊天 + setCurrentRole(null); + setCurrentChat(null); } else if (deleteType === 'chat' && selectedChat === showDeleteConfirm) { - if (onChatChange) { - onChatChange(selectedRole, null); - } + // 清除 ChatBoxStore 中的当前聊天 + setCurrentChat(null); } };