diff --git a/backend/__pycache__/__init__.cpython-311.pyc b/backend/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..c9a6da1 Binary files /dev/null and b/backend/__pycache__/__init__.cpython-311.pyc differ diff --git a/backend/__pycache__/main.cpython-311.pyc b/backend/__pycache__/main.cpython-311.pyc new file mode 100644 index 0000000..4432136 Binary files /dev/null and b/backend/__pycache__/main.cpython-311.pyc differ diff --git a/backend/api/__pycache__/__init__.cpython-311.pyc b/backend/api/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..f71c610 Binary files /dev/null and b/backend/api/__pycache__/__init__.cpython-311.pyc differ diff --git a/backend/api/__pycache__/route.cpython-311.pyc b/backend/api/__pycache__/route.cpython-311.pyc new file mode 100644 index 0000000..3de0bff Binary files /dev/null and b/backend/api/__pycache__/route.cpython-311.pyc differ diff --git a/backend/core/__pycache__/__init__.cpython-311.pyc b/backend/core/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..0d3b742 Binary files /dev/null and b/backend/core/__pycache__/__init__.cpython-311.pyc differ diff --git a/backend/core/__pycache__/config.cpython-311.pyc b/backend/core/__pycache__/config.cpython-311.pyc new file mode 100644 index 0000000..356e5ff Binary files /dev/null and b/backend/core/__pycache__/config.cpython-311.pyc differ diff --git a/backend/core/__pycache__/items.cpython-311.pyc b/backend/core/__pycache__/items.cpython-311.pyc new file mode 100644 index 0000000..fd1b335 Binary files /dev/null and b/backend/core/__pycache__/items.cpython-311.pyc differ diff --git a/backend/core/models/chat_history.py b/backend/core/models/chat_history.py new file mode 100644 index 0000000..dddb861 --- /dev/null +++ b/backend/core/models/chat_history.py @@ -0,0 +1,158 @@ +from typing import List, Dict, Any, Optional +from datetime import datetime +from pathlib import Path +from pydantic import BaseModel, Field +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="变量值数组") + 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毫秒数)") + + +class ChatMetadata(BaseModel): + """聊天元数据模型,代表JSONL文件的第一行内容""" + integrity: str = Field("", description="完整性校验哈希值(UUID格式)") + 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") + + +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="聊天元数据") + messages: List[Message] = Field(default_factory=list, description="消息列表") + + class Config: + arbitrary_types_allowed = True + + +def load_chat_file_data(chat_name: str, role_name: str, base_path: Path = None) -> Dict[str, Any]: + """ + 从文件系统加载聊天原始数据 + + 参数: + chat_name: 聊天名称 + role_name: 角色名称 + base_path: 基础路径,默认为项目数据目录 + + 返回: + dict: 包含元数据和消息列表的原始数据字典 + """ + # 设置默认基础路径 + 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" + + # 检查文件是否存在 + 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": [] + } + + with open(file_path, 'r', encoding='utf-8') as f: + for line in f: + try: + # 解析JSON行 + line_data = json.loads(line.strip()) + + # 添加到消息列表 + result["messages"].append(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) + ) + 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 + ) + + +def load_chat_file(chat_name: str, role_name: str, base_path: Path = None) -> ChatFile: + """ + 从文件系统加载聊天数据并创建ChatFile对象 + + 参数: + chat_name: 聊天名称 + role_name: 角色名称 + base_path: 基础路径,默认为项目数据目录 + + 返回: + ChatFile: 加载的聊天文件对象 + """ + # 加载原始数据 + data = load_chat_file_data(chat_name, role_name, base_path) + + # 创建ChatFile对象 + return create_chat_file_from_data(data) diff --git a/backend/tools/__pycache__/__init__.cpython-311.pyc b/backend/tools/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..221097a Binary files /dev/null and b/backend/tools/__pycache__/__init__.cpython-311.pyc differ diff --git a/backend/tools/__pycache__/get_all_role_and_chat.cpython-311.pyc b/backend/tools/__pycache__/get_all_role_and_chat.cpython-311.pyc new file mode 100644 index 0000000..3e701c2 Binary files /dev/null and b/backend/tools/__pycache__/get_all_role_and_chat.cpython-311.pyc differ diff --git a/backend/tools/__pycache__/save_input_to_json.cpython-311.pyc b/backend/tools/__pycache__/save_input_to_json.cpython-311.pyc new file mode 100644 index 0000000..e7aba5e Binary files /dev/null and b/backend/tools/__pycache__/save_input_to_json.cpython-311.pyc differ diff --git a/backend/tools/get_all_role_and_chat.py b/backend/tools/get_all_role_and_chat.py index 48aec80..5fc805d 100644 --- a/backend/tools/get_all_role_and_chat.py +++ b/backend/tools/get_all_role_and_chat.py @@ -9,7 +9,7 @@ def get_all_role_and_chat() -> Dict[str, List[str]]: 读取配置目录下的所有子文件夹,并收集每个子文件夹中的 JSONL 文件 返回: - dict: 字典结构,键是文件夹名称,值是该文件夹中的 JSONL 文件列表 + dict: 字典结构,键是文件夹名称,值是该文件夹中的 JSONL 文件列表(仅文件名,无路径和后缀) """ result = {} @@ -33,7 +33,8 @@ def get_all_role_and_chat() -> Dict[str, List[str]]: # 遍历子文件夹中的所有文件 for file in entry.iterdir(): if file.is_file() and file.suffix == '.jsonl': - jsonl_files.append(str(file)) + # 使用 file.stem 获取不带后缀的文件名 + jsonl_files.append(file.stem) print(f" 找到文件: {file.name}") # 调试信息 # 如果该文件夹中有 JSONL 文件,则添加到结果中 diff --git a/data/chat/test/12312.jsonl b/data/chat/test/12331212.jsonl similarity index 100% rename from data/chat/test/12312.jsonl rename to data/chat/test/12331212.jsonl diff --git a/frontend-react/src/components/RoleSelector/RoleSelector.css b/frontend-react/src/components/RoleSelector/RoleSelector.css index e39d7bc..520e3ed 100644 --- a/frontend-react/src/components/RoleSelector/RoleSelector.css +++ b/frontend-react/src/components/RoleSelector/RoleSelector.css @@ -1,95 +1,419 @@ -/* ==================== 下拉框样式 ==================== */ +/* ==================== 角色选择器容器 ==================== */ -/* 角色选择器容器 */ .role-selector { width: 100%; + height: 100%; } -/* 下拉框容器 */ -.dropdown-container { - position: relative; +/* ==================== 角色面板样式 ==================== */ + +.role-panel { width: 100%; - max-width: 300px; + height: 100%; } -/* 下拉框触发器 */ -.dropdown-trigger { +.panel-controls { display: flex; justify-content: space-between; align-items: center; - padding: 8px 12px; - background-color: #f5f5f5; - border: 1px solid #ddd; - border-radius: 4px; + margin-bottom: 20px; +} + +.search-box { + flex: 1; + margin-right: 16px; +} + +.search-box input { + width: 100%; + padding: 10px 16px; + border: 1px solid #e0e0e0; + border-radius: 6px; + font-size: 14px; + transition: border-color 0.2s ease; +} + +.search-box input:focus { + outline: none; + border-color: #1890ff; + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1); +} + +.add-button { + display: flex; + align-items: center; + gap: 6px; + padding: 10px 20px; + background-color: #1890ff; + color: white; + border: none; + border-radius: 6px; cursor: pointer; font-size: 14px; + transition: all 0.2s ease; } -.dropdown-arrow { - margin-left: 8px; - font-size: 12px; +.add-button:hover { + background-color: #40a9ff; + transform: translateY(-1px); + box-shadow: 0 2px 6px rgba(24, 144, 255, 0.2); } -/* 下拉菜单 */ -.dropdown-menu { - position: absolute; - top: 100%; - left: 0; - width: 100%; - background-color: white; - border: 1px solid #ddd; - border-radius: 4px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); - z-index: 10; - max-height: 200px; - overflow-y: auto; - margin-top: 5px; +.add-button .icon { + font-size: 16px; + font-weight: bold; } -/* 下拉菜单项 */ -.dropdown-item { - padding: 8px 12px; +.panel-list-container { + display: flex; + flex-direction: column; + height: calc(100% - 80px); +} + +.loading-container { + display: flex; + align-items: center; + justify-content: center; + padding: 40px; + gap: 12px; + color: #666; +} + +.loading-spinner { + display: inline-block; + width: 20px; + height: 20px; + border: 2px solid rgba(24, 144, 255, 0.2); + border-radius: 50%; + border-top-color: #1890ff; + animation: spin 1s ease-in-out infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.empty-state { + display: flex; + align-items: center; + justify-content: center; + padding: 40px; + color: #999; +} + +/* ==================== 角色卡片网格布局 ==================== */ + +.role-cards-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 20px; +} + +/* ==================== 角色卡片样式 ==================== */ + +.role-card { + border-radius: 8px; cursor: pointer; position: relative; - font-size: 14px; + transition: all 0.2s ease; + background-color: #fff; + border: 1px solid #e0e0e0; + display: flex; + flex-direction: column; + overflow: hidden; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); } -.dropdown-item:hover { +.role-card:hover { background-color: #f5f5f5; + border-color: #1890ff; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + transform: translateY(-2px); } -.dropdown-item.selected { +.role-card.selected { + background-color: #e6f7ff; + border-color: #1890ff; + box-shadow: 0 4px 12px rgba(24, 144, 255, 0.2); +} + +.role-card-header { + padding: 16px; + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid #f0f0f0; +} + +.role-name { + font-size: 15px; + color: #333; + font-weight: 500; +} + +.role-card.selected .role-name { + color: #1890ff; +} + +.role-chat-count { + font-size: 12px; + color: #999; + background-color: #f0f0f0; + padding: 4px 10px; + border-radius: 12px; +} + +.role-card-actions { + display: flex; + justify-content: flex-end; + padding: 8px 12px; + opacity: 0; + transition: opacity 0.2s ease; +} + +.role-card:hover .role-card-actions, +.role-card.clicked .role-card-actions { + opacity: 1; +} + +.action-button { + width: 28px; + height: 28px; + border-radius: 4px; + border: none; + background-color: #f5f5f5; + color: #666; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + transition: all 0.2s ease; +} + +.action-button:hover { background-color: #e6f7ff; color: #1890ff; } -/* 嵌套下拉框 */ -.nested-dropdown { - position: absolute; - left: 100%; - top: 0; - width: 100%; - background-color: white; - border: 1px solid #ddd; +.edit-button:hover { + background-color: #e6f7ff; + color: #1890ff; +} + +.delete-button:hover { + background-color: #fff1f0; + color: #ff4d4f; +} + +.role-card-header input { + padding: 6px 10px; + border: 1px solid #d9d9d9; border-radius: 4px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); - z-index: 11; - max-height: 200px; + font-size: 14px; + width: 150px; +} + +.role-card-header input:focus { + outline: none; + border-color: #1890ff; + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1); +} + +/* ==================== 聊天列表样式 ==================== */ + +.chat-list { + margin-top: 0; + border-top: 1px solid #e0e0e0; + padding: 12px; + background-color: #fafafa; + animation: slideDown 0.2s ease; + max-height: 300px; overflow-y: auto; } -.nested-dropdown .dropdown-item { - padding-left: 20px; +.chat-list-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 0; + border-bottom: 1px solid #e0e0e0; + margin-bottom: 8px; } -/* 工具栏下拉框容器 */ -.toolbar-dropdown { - margin-bottom: 15px; -} - -.toolbar-dropdown label { - display: block; - margin-bottom: 5px; - font-size: 14px; +.chat-list-header span { + font-size: 13px; + font-weight: 500; color: #333; } + +.add-chat-button { + display: flex; + align-items: center; + gap: 4px; + padding: 4px 10px; + background-color: #1890ff; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + transition: all 0.2s ease; +} + +.add-chat-button:hover { + background-color: #40a9ff; +} + +.add-chat-button .icon { + font-size: 14px; + font-weight: bold; +} + +.chat-item { + padding: 10px 12px; + cursor: pointer; + transition: background-color 0.2s ease; + display: flex; + justify-content: space-between; + align-items: center; + border-radius: 4px; + margin-bottom: 4px; +} + +.chat-item:hover { + background-color: #f0f0f0; +} + +.chat-item.selected { + background-color: #e6f7ff; + color: #1890ff; +} + +.chat-name { + font-size: 13px; + color: #333; +} + +.chat-item.selected .chat-name { + color: #1890ff; + font-weight: 500; +} + +.chat-actions { + display: flex; + gap: 6px; + opacity: 0; + transition: opacity 0.2s ease; +} + +.chat-item:hover .chat-actions { + opacity: 1; +} + +.chat-content input { + padding: 6px 10px; + border: 1px solid #d9d9d9; + border-radius: 4px; + font-size: 13px; + width: 180px; +} + +.chat-content input:focus { + outline: none; + border-color: #1890ff; + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1); +} + +/* ==================== 模态框样式 ==================== */ + +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + z-index: 2000; + display: flex; + align-items: center; + justify-content: center; + animation: fadeIn 0.2s ease; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +.modal-content { + background-color: #fff; + border-radius: 8px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); + width: 90%; + max-width: 400px; + padding: 24px; + animation: slideDown 0.3s ease; +} + +@keyframes slideDown { + from { + transform: translateY(-20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +.modal-content h3 { + margin: 0 0 16px 0; + font-size: 16px; + font-weight: 500; + color: #333; +} + +.modal-content p { + margin: 0 0 24px 0; + font-size: 14px; + color: #666; +} + +.modal-actions { + display: flex; + justify-content: flex-end; + gap: 12px; +} + +.modal-button { + padding: 8px 16px; + border-radius: 4px; + font-size: 14px; + cursor: pointer; + transition: all 0.2s ease; + border: none; +} + +.cancel-button { + background-color: #f5f5f5; + color: #666; +} + +.cancel-button:hover { + background-color: #e6e6e6; + color: #333; +} + +.confirm-button { + background-color: #ff4d4f; + color: white; +} + +.confirm-button:hover { + background-color: #ff7875; +} diff --git a/frontend-react/src/components/RoleSelector/RoleSelector.jsx b/frontend-react/src/components/RoleSelector/RoleSelector.jsx index a520d6b..9ad3ceb 100644 --- a/frontend-react/src/components/RoleSelector/RoleSelector.jsx +++ b/frontend-react/src/components/RoleSelector/RoleSelector.jsx @@ -1,94 +1,409 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import './RoleSelector.css'; const RoleSelector = ({ onRoleChange, onChatChange }) => { - const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [roleData, setRoleData] = useState({}); const [selectedRole, setSelectedRole] = useState(null); const [selectedChat, setSelectedChat] = useState(null); + const [hoveredRole, setHoveredRole] = useState(null); + const [clickedRole, setClickedRole] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [searchTerm, setSearchTerm] = useState(''); + const [editingRole, setEditingRole] = useState(null); + const [editingChat, setEditingChat] = useState(null); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(null); + const [deleteType, setDeleteType] = useState(null); + const panelRef = useRef(null); // 获取角色和聊天数据 - useEffect(() => { - const fetchRoleData = async () => { - try { - console.log('开始获取角色数据...'); - const response = await fetch('/api/get_all_role_and_chat'); - console.log('响应状态:', response.status); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); + const fetchRoleData = async () => { + setIsLoading(true); + try { + const response = await fetch('/api/tool_bar/get_all_role_and_chat', { + method: 'GET', + headers: { + 'Cache-Control': 'no-cache', + 'Pragma': 'no-cache', + 'Expires': '0' } + }); + const data = await response.json(); + setRoleData(data); + } catch (error) { + console.error('获取角色数据失败:', error); + } finally { + setIsLoading(false); + } + }; - const data = await response.json(); - console.log('获取到的数据:', data); - setRoleData(data); - } catch (error) { - console.error('获取角色数据失败:', error); + // 组件挂载时获取数据 + useEffect(() => { + fetchRoleData(); + }, []); + + // 点击外部关闭面板 + useEffect(() => { + const handleClickOutside = (event) => { + if (panelRef.current && !panelRef.current.contains(event.target)) { + setHoveredRole(null); + setClickedRole(null); + setEditingRole(null); + setEditingChat(null); + setShowDeleteConfirm(null); } }; - fetchRoleData(); + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; }, []); // 处理角色选择 const handleRoleSelect = (role) => { setSelectedRole(role); - setSelectedChat(null); if (onRoleChange) { onRoleChange(role); } + + // 如果该角色有聊天记录,默认选择第一个 + if (roleData[role] && roleData[role].length > 0) { + const firstChat = roleData[role][0]; + setSelectedChat(firstChat); + if (onChatChange) { + onChatChange(role, firstChat); + } + } else { + setSelectedChat(null); + } }; // 处理聊天选择 const handleChatSelect = (chat) => { setSelectedChat(chat); - setIsDropdownOpen(false); + setHoveredRole(null); + setClickedRole(null); if (onChatChange) { onChatChange(selectedRole, chat); } }; + // 处理角色卡片点击 + const handleRoleCardClick = (role) => { + if (clickedRole === role) { + setClickedRole(null); + } else { + setClickedRole(role); + handleRoleSelect(role); + } + }; + + // 处理搜索 + const handleSearchChange = (e) => { + setSearchTerm(e.target.value); + }; + + // 处理角色编辑 + const handleEditRole = (e, role) => { + e.stopPropagation(); + setEditingRole(role); + }; + + // 处理角色重命名 + const handleRenameRole = (e, oldName) => { + e.stopPropagation(); + const newName = e.target.value; + if (newName && newName !== oldName) { + const newRoleData = { ...roleData }; + const chats = newRoleData[oldName]; + delete newRoleData[oldName]; + newRoleData[newName] = chats; + setRoleData(newRoleData); + + if (selectedRole === oldName) { + setSelectedRole(newName); + if (onRoleChange) { + onRoleChange(newName); + } + } + } + setEditingRole(null); + }; + + // 处理聊天编辑 + const handleEditChat = (e, chat) => { + e.stopPropagation(); + setEditingChat(chat); + }; + + // 处理聊天重命名 + const handleRenameChat = (e, oldName) => { + e.stopPropagation(); + const newName = e.target.value; + if (newName && newName !== oldName) { + const newRoleData = { ...roleData }; + const chatIndex = newRoleData[selectedRole].indexOf(oldName); + if (chatIndex !== -1) { + newRoleData[selectedRole][chatIndex] = newName; + setRoleData(newRoleData); + + if (selectedChat === oldName) { + setSelectedChat(newName); + if (onChatChange) { + onChatChange(selectedRole, newName); + } + } + } + } + setEditingChat(null); + }; + + // 处理删除确认 + const handleDeleteClick = (e, type, name) => { + e.stopPropagation(); + setDeleteType(type); + setShowDeleteConfirm(name); + }; + + // 确认删除 + const confirmDelete = () => { + if (deleteType === 'role') { + const newRoleData = { ...roleData }; + delete newRoleData[showDeleteConfirm]; + setRoleData(newRoleData); + + if (selectedRole === showDeleteConfirm) { + setSelectedRole(null); + setSelectedChat(null); + if (onRoleChange) { + onRoleChange(null); + } + } + } else if (deleteType === 'chat') { + const newRoleData = { ...roleData }; + const chatIndex = newRoleData[selectedRole].indexOf(showDeleteConfirm); + if (chatIndex !== -1) { + newRoleData[selectedRole].splice(chatIndex, 1); + setRoleData(newRoleData); + + if (selectedChat === showDeleteConfirm) { + setSelectedChat(null); + if (onChatChange) { + onChatChange(selectedRole, null); + } + } + } + } + setShowDeleteConfirm(null); + setDeleteType(null); + }; + + // 取消删除 + const cancelDelete = () => { + setShowDeleteConfirm(null); + setDeleteType(null); + }; + + // 处理新增角色 + const handleAddRole = () => { + const newRole = '新角色'; + const newRoleData = { ...roleData }; + newRoleData[newRole] = []; + setRoleData(newRoleData); + setEditingRole(newRole); + }; + + // 处理新增聊天 + const handleAddChat = () => { + if (!selectedRole) return; + + const newChat = '新聊天'; + const newRoleData = { ...roleData }; + newRoleData[selectedRole].push(newChat); + setRoleData(newRoleData); + setEditingChat(newChat); + }; + + // 过滤角色 + const filteredRoles = Object.keys(roleData).filter(role => + role.toLowerCase().includes(searchTerm.toLowerCase()) + ); + return ( -
-
-
setIsDropdownOpen(!isDropdownOpen)} - > - {selectedRole || '选择角色'} - +
+
+
+
+ +
+
- {isDropdownOpen && ( -
- {Object.keys(roleData).map(role => ( -
handleRoleSelect(role)} - > - {role} - {selectedRole === role && ( -
- {roleData[role].map(chat => ( -
{ - e.stopPropagation(); - handleChatSelect(chat); +
+ {isLoading ? ( +
+
+ 加载中... +
+ ) : filteredRoles.length === 0 ? ( +
+

没有找到匹配的角色

+
+ ) : ( +
+ {filteredRoles.map(role => ( +
handleRoleCardClick(role)} + onMouseEnter={() => setHoveredRole(role)} + onMouseLeave={() => setHoveredRole(null)} + > +
+ {editingRole === role ? ( + handleRenameRole(e, role)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + handleRenameRole(e, role); + } }} - > - {chat} -
- ))} + onClick={(e) => e.stopPropagation()} + /> + ) : ( +
{role}
+ )} + +
+ {roleData[role] ? roleData[role].length : 0} 个聊天 +
- )} -
- ))} -
- )} + +
+ {editingRole !== role && ( + <> + + + + )} +
+ + {/* 聊天列表 */} + {(hoveredRole === role || clickedRole === role) && roleData[role] && roleData[role].length > 0 && ( +
+
+ 聊天记录 + +
+ + {roleData[role].map(chat => ( +
{ + e.stopPropagation(); + handleChatSelect(chat); + }} + > +
+ {editingChat === chat ? ( + handleRenameChat(e, chat)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + handleRenameChat(e, chat); + } + }} + onClick={(e) => e.stopPropagation()} + /> + ) : ( +
{chat}
+ )} +
+ +
+ {editingChat !== chat && ( + <> + + + + )} +
+
+ ))} +
+ )} +
+ ))} +
+ )} +
+ + {/* 删除确认对话框 */} + {showDeleteConfirm && ( +
+
+

确认删除

+

确定要删除{deleteType === 'role' ? '角色' : '聊天'} "{showDeleteConfirm}" 吗?

+
+ + +
+
+
+ )}
); }; diff --git a/frontend-react/src/components/ToolBar/ToolBar.css b/frontend-react/src/components/ToolBar/ToolBar.css index a24c855..43920cf 100644 --- a/frontend-react/src/components/ToolBar/ToolBar.css +++ b/frontend-react/src/components/ToolBar/ToolBar.css @@ -59,7 +59,22 @@ color: #fff; } -/* ==================== 弹出面板样式 ==================== */ +/* ==================== 弹出面板通用样式 ==================== */ + +.close-panel-button { + background: none; + border: none; + font-size: 20px; + cursor: pointer; + color: #666; + padding: 5px 10px; + margin-left: auto; + transition: color 0.2s; +} + +.close-panel-button:hover { + color: #333; +} .panel-overlay { position: fixed; @@ -124,411 +139,12 @@ color: #333; } -.current-info { - display: flex; - gap: 20px; - font-size: 13px; - color: #666; -} - .panel-body { padding: 20px; overflow-y: auto; flex: 1; } -/* ==================== 角色面板样式 ==================== */ - -.role-panel { - max-width: 1200px; -} - -.panel-controls { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 20px; -} - -.search-box { - flex: 1; - margin-right: 16px; -} - -.search-box input { - width: 100%; - padding: 10px 16px; - border: 1px solid #e0e0e0; - border-radius: 6px; - font-size: 14px; - transition: border-color 0.2s ease; -} - -.search-box input:focus { - outline: none; - border-color: #1890ff; - box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1); -} - -.add-button { - display: flex; - align-items: center; - gap: 6px; - padding: 10px 20px; - background-color: #1890ff; - color: white; - border: none; - border-radius: 6px; - cursor: pointer; - font-size: 14px; - transition: all 0.2s ease; -} - -.add-button:hover { - background-color: #40a9ff; - transform: translateY(-1px); - box-shadow: 0 2px 6px rgba(24, 144, 255, 0.2); -} - -.add-button .icon { - font-size: 16px; - font-weight: bold; -} - -.panel-list-container { - display: flex; - flex-direction: column; - height: calc(100% - 80px); -} - -.loading-container { - display: flex; - align-items: center; - justify-content: center; - padding: 40px; - gap: 12px; - color: #666; -} - -.loading-spinner { - display: inline-block; - width: 20px; - height: 20px; - border: 2px solid rgba(24, 144, 255, 0.2); - border-radius: 50%; - border-top-color: #1890ff; - animation: spin 1s ease-in-out infinite; -} - -@keyframes spin { - to { transform: rotate(360deg); } -} - -.empty-state { - display: flex; - align-items: center; - justify-content: center; - padding: 40px; - color: #999; -} - -/* ==================== 角色卡片网格布局 ==================== */ - -.role-cards-grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 20px; -} - -/* ==================== 角色卡片样式 ==================== */ - -.role-card { - border-radius: 8px; - cursor: pointer; - position: relative; - transition: all 0.2s ease; - background-color: #fff; - border: 1px solid #e0e0e0; - display: flex; - flex-direction: column; - overflow: hidden; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); -} - -.role-card:hover { - background-color: #f5f5f5; - border-color: #1890ff; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); - transform: translateY(-2px); -} - -.role-card.selected { - background-color: #e6f7ff; - border-color: #1890ff; - box-shadow: 0 4px 12px rgba(24, 144, 255, 0.2); -} - -.role-card-header { - padding: 16px; - display: flex; - justify-content: space-between; - align-items: center; - border-bottom: 1px solid #f0f0f0; -} - -.role-name { - font-size: 15px; - color: #333; - font-weight: 500; -} - -.role-card.selected .role-name { - color: #1890ff; -} - -.role-chat-count { - font-size: 12px; - color: #999; - background-color: #f0f0f0; - padding: 4px 10px; - border-radius: 12px; -} - -.role-card-actions { - display: flex; - justify-content: flex-end; - padding: 8px 12px; - opacity: 0; - transition: opacity 0.2s ease; -} - -.role-card:hover .role-card-actions, -.role-card.clicked .role-card-actions { - opacity: 1; -} - -.action-button { - width: 28px; - height: 28px; - border-radius: 4px; - border: none; - background-color: #f5f5f5; - color: #666; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - font-size: 14px; - transition: all 0.2s ease; -} - -.action-button:hover { - background-color: #e6f7ff; - color: #1890ff; -} - -.edit-button:hover { - background-color: #e6f7ff; - color: #1890ff; -} - -.delete-button:hover { - background-color: #fff1f0; - color: #ff4d4f; -} - -.role-card-header input { - padding: 6px 10px; - border: 1px solid #d9d9d9; - border-radius: 4px; - font-size: 14px; - width: 150px; -} - -.role-card-header input:focus { - outline: none; - border-color: #1890ff; - box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1); -} - -/* ==================== 聊天列表样式 ==================== */ - -.chat-list { - margin-top: 0; - border-top: 1px solid #e0e0e0; - padding: 12px; - background-color: #fafafa; - animation: slideDown 0.2s ease; - max-height: 300px; - overflow-y: auto; -} - -.chat-list-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px 0; - border-bottom: 1px solid #e0e0e0; - margin-bottom: 8px; -} - -.chat-list-header span { - font-size: 13px; - font-weight: 500; - color: #333; -} - -.add-chat-button { - display: flex; - align-items: center; - gap: 4px; - padding: 4px 10px; - background-color: #1890ff; - color: white; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 12px; - transition: all 0.2s ease; -} - -.add-chat-button:hover { - background-color: #40a9ff; -} - -.add-chat-button .icon { - font-size: 14px; - font-weight: bold; -} - -.chat-item { - padding: 10px 12px; - cursor: pointer; - transition: background-color 0.2s ease; - display: flex; - justify-content: space-between; - align-items: center; - border-radius: 4px; - margin-bottom: 4px; -} - -.chat-item:hover { - background-color: #f0f0f0; -} - -.chat-item.selected { - background-color: #e6f7ff; - color: #1890ff; -} - -.chat-name { - font-size: 13px; - color: #333; -} - -.chat-item.selected .chat-name { - color: #1890ff; - font-weight: 500; -} - -.chat-actions { - display: flex; - gap: 6px; - opacity: 0; - transition: opacity 0.2s ease; -} - -.chat-item:hover .chat-actions { - opacity: 1; -} - -.chat-content input { - padding: 6px 10px; - border: 1px solid #d9d9d9; - border-radius: 4px; - font-size: 13px; - width: 180px; -} - -.chat-content input:focus { - outline: none; - border-color: #1890ff; - box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1); -} - -/* ==================== 模态框样式 ==================== */ - -.modal-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(0, 0, 0, 0.5); - z-index: 2000; - display: flex; - align-items: center; - justify-content: center; - animation: fadeIn 0.2s ease; -} - -.modal-content { - background-color: #fff; - border-radius: 8px; - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); - width: 90%; - max-width: 400px; - padding: 24px; - animation: slideDown 0.3s ease; -} - -.modal-content h3 { - margin: 0 0 16px 0; - font-size: 16px; - font-weight: 500; - color: #333; -} - -.modal-content p { - margin: 0 0 24px 0; - font-size: 14px; - color: #666; -} - -.modal-actions { - display: flex; - justify-content: flex-end; - gap: 12px; -} - -.modal-button { - padding: 8px 16px; - border-radius: 4px; - font-size: 14px; - cursor: pointer; - transition: all 0.2s ease; - border: none; -} - -.cancel-button { - background-color: #f5f5f5; - color: #666; -} - -.cancel-button:hover { - background-color: #e6e6e6; - color: #333; -} - -.confirm-button { - background-color: #ff4d4f; - color: white; -} - -.confirm-button:hover { - background-color: #ff7875; -} - /* 主内容区域 */ .main-container { margin-top: 50px; diff --git a/frontend-react/src/components/ToolBar/ToolBar.jsx b/frontend-react/src/components/ToolBar/ToolBar.jsx index 1d1eb8f..28188b9 100644 --- a/frontend-react/src/components/ToolBar/ToolBar.jsx +++ b/frontend-react/src/components/ToolBar/ToolBar.jsx @@ -1,58 +1,16 @@ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useRef } from 'react'; +import RoleSelector from '../RoleSelector/RoleSelector'; import './ToolBar.css'; -const Toolbar = ({ - onRoleChange, - onChatChange -}) => { - const [selectedRole, setSelectedRole] = useState(null); - const [selectedChat, setSelectedChat] = useState(null); +const Toolbar = () => { const [activePanel, setActivePanel] = useState(null); - const [roleData, setRoleData] = useState({}); - const [hoveredRole, setHoveredRole] = useState(null); - const [clickedRole, setClickedRole] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [searchTerm, setSearchTerm] = useState(''); - const [editingRole, setEditingRole] = useState(null); - const [editingChat, setEditingChat] = useState(null); - const [showDeleteConfirm, setShowDeleteConfirm] = useState(null); - const [deleteType, setDeleteType] = useState(null); const panelRef = useRef(null); - // 获取角色和聊天数据 - const fetchRoleData = async () => { - setIsLoading(true); - console.log('开始获取角色数据...'); - try { - const response = await fetch('/api/tool_bar/get_all_role_and_chat', { - method: 'GET', - headers: { - 'Cache-Control': 'no-cache', - 'Pragma': 'no-cache', - 'Expires': '0' - } - }); - console.log('响应状态:', response.status); - const data = await response.json(); - console.log('获取到的数据:', data); - setRoleData(data); - } catch (error) { - console.error('获取角色数据失败:', error); - } finally { - setIsLoading(false); - } - }; - // 点击外部关闭面板 - useEffect(() => { + React.useEffect(() => { const handleClickOutside = (event) => { if (panelRef.current && !panelRef.current.contains(event.target)) { setActivePanel(null); - setHoveredRole(null); - setClickedRole(null); - setEditingRole(null); - setEditingChat(null); - setShowDeleteConfirm(null); } }; @@ -67,216 +25,15 @@ const Toolbar = ({ if (activePanel === panelName) { setActivePanel(null); } else { - if (panelName === 'role') { - fetchRoleData(); - } setActivePanel(panelName); } }; - // 处理角色选择 - const handleRoleSelect = (role) => { - setSelectedRole(role); - if (onRoleChange) { - onRoleChange(role); - } - - // 如果该角色有聊天记录,默认选择第一个 - if (roleData[role] && roleData[role].length > 0) { - const firstChat = roleData[role][0]; - setSelectedChat(firstChat); - if (onChatChange) { - onChatChange(role, firstChat); - } - } else { - setSelectedChat(null); - } - }; - - // 处理聊天选择 - const handleChatSelect = (chat) => { - setSelectedChat(chat); + // 关闭面板 + const handleClosePanel = () => { setActivePanel(null); - setHoveredRole(null); - setClickedRole(null); - if (onChatChange) { - onChatChange(selectedRole, chat); - } }; - // 处理角色卡片点击 - const handleRoleCardClick = (role) => { - if (clickedRole === role) { - setClickedRole(null); - } else { - setClickedRole(role); - handleRoleSelect(role); - } - }; - - // 处理搜索 - const handleSearchChange = (e) => { - setSearchTerm(e.target.value); - }; - - // 处理角色编辑 - const handleEditRole = (e, role) => { - e.stopPropagation(); - setEditingRole(role); - }; - - // 处理角色重命名 - const handleRenameRole = (e, oldName) => { - e.stopPropagation(); - const newName = e.target.value; - if (newName && newName !== oldName) { - // 这里应该调用API更新角色名 - console.log('重命名角色:', oldName, '->', newName); - - // 更新本地状态 - const newRoleData = { ...roleData }; - const chats = newRoleData[oldName]; - delete newRoleData[oldName]; - newRoleData[newName] = chats; - setRoleData(newRoleData); - - // 如果当前选中的是被重命名的角色,更新选中状态 - if (selectedRole === oldName) { - setSelectedRole(newName); - if (onRoleChange) { - onRoleChange(newName); - } - } - } - setEditingRole(null); - }; - - // 处理聊天编辑 - const handleEditChat = (e, chat) => { - e.stopPropagation(); - setEditingChat(chat); - }; - - // 处理聊天重命名 - const handleRenameChat = (e, oldName) => { - e.stopPropagation(); - const newName = e.target.value; - if (newName && newName !== oldName) { - // 这里应该调用API更新聊天名 - console.log('重命名聊天:', oldName, '->', newName); - - // 更新本地状态 - const newRoleData = { ...roleData }; - const chatIndex = newRoleData[selectedRole].indexOf(oldName); - if (chatIndex !== -1) { - newRoleData[selectedRole][chatIndex] = newName; - setRoleData(newRoleData); - - // 如果当前选中的是被重命名的聊天,更新选中状态 - if (selectedChat === oldName) { - setSelectedChat(newName); - if (onChatChange) { - onChatChange(selectedRole, newName); - } - } - } - } - setEditingChat(null); - }; - - // 处理删除确认 - const handleDeleteClick = (e, type, name) => { - e.stopPropagation(); - setDeleteType(type); - setShowDeleteConfirm(name); - }; - - // 确认删除 - const confirmDelete = () => { - if (deleteType === 'role') { - // 这里应该调用API删除角色 - console.log('删除角色:', showDeleteConfirm); - - // 更新本地状态 - const newRoleData = { ...roleData }; - delete newRoleData[showDeleteConfirm]; - setRoleData(newRoleData); - - // 如果删除的是当前选中的角色,清空选中状态 - if (selectedRole === showDeleteConfirm) { - setSelectedRole(null); - setSelectedChat(null); - if (onRoleChange) { - onRoleChange(null); - } - } - } else if (deleteType === 'chat') { - // 这里应该调用API删除聊天 - console.log('删除聊天:', showDeleteConfirm); - - // 更新本地状态 - const newRoleData = { ...roleData }; - const chatIndex = newRoleData[selectedRole].indexOf(showDeleteConfirm); - if (chatIndex !== -1) { - newRoleData[selectedRole].splice(chatIndex, 1); - setRoleData(newRoleData); - - // 如果删除的是当前选中的聊天,清空选中状态 - if (selectedChat === showDeleteConfirm) { - setSelectedChat(null); - if (onChatChange) { - onChatChange(selectedRole, null); - } - } - } - } - setShowDeleteConfirm(null); - setDeleteType(null); - }; - - // 取消删除 - const cancelDelete = () => { - setShowDeleteConfirm(null); - setDeleteType(null); - }; - - // 处理新增角色 - const handleAddRole = () => { - // 这里应该调用API创建新角色 - const newRole = '新角色'; - console.log('创建新角色:', newRole); - - // 更新本地状态 - const newRoleData = { ...roleData }; - newRoleData[newRole] = []; - setRoleData(newRoleData); - - // 自动进入编辑模式 - setEditingRole(newRole); - }; - - // 处理新增聊天 - const handleAddChat = () => { - if (!selectedRole) return; - - // 这里应该调用API创建新聊天 - const newChat = '新聊天'; - console.log('创建新聊天:', newChat); - - // 更新本地状态 - const newRoleData = { ...roleData }; - newRoleData[selectedRole].push(newChat); - setRoleData(newRoleData); - - // 自动进入编辑模式 - setEditingChat(newChat); - }; - - // 过滤角色 - const filteredRoles = Object.keys(roleData).filter(role => - role.toLowerCase().includes(searchTerm.toLowerCase()) - ); - return ( <>
@@ -332,187 +89,15 @@ const Toolbar = ({ {/* 角色管理面板 */} {activePanel === 'role' && (
-
+

角色管理

-
- 当前角色: {selectedRole || '未选择'} - 当前聊天: {selectedChat || '未选择'} -
+
-
-
-
- -
- -
- -
- {isLoading ? ( -
-
- 加载中... -
- ) : filteredRoles.length === 0 ? ( -
-

没有找到匹配的角色

-
- ) : ( -
- {filteredRoles.map(role => ( -
handleRoleCardClick(role)} - onMouseEnter={() => setHoveredRole(role)} - onMouseLeave={() => setHoveredRole(null)} - > -
- {editingRole === role ? ( - handleRenameRole(e, role)} - onKeyDown={(e) => { - if (e.key === 'Enter') { - handleRenameRole(e, role); - } - }} - onClick={(e) => e.stopPropagation()} - /> - ) : ( -
{role}
- )} - -
- {roleData[role] ? roleData[role].length : 0} 个聊天 -
-
- -
- {editingRole !== role && ( - <> - - - - )} -
- - {/* 聊天列表 */} - {(hoveredRole === role || clickedRole === role) && roleData[role] && roleData[role].length > 0 && ( -
-
- 聊天记录 - -
- - {roleData[role].map(chat => ( -
{ - e.stopPropagation(); - handleChatSelect(chat); - }} - > -
- {editingChat === chat ? ( - handleRenameChat(e, chat)} - onKeyDown={(e) => { - if (e.key === 'Enter') { - handleRenameChat(e, chat); - } - }} - onClick={(e) => e.stopPropagation()} - /> - ) : ( -
{chat}
- )} -
- -
- {editingChat !== chat && ( - <> - - - - )} -
-
- ))} -
- )} -
- ))} -
- )} -
-
-
-
- )} - - {/* 删除确认对话框 */} - {showDeleteConfirm && ( -
-
-

确认删除

-

确定要删除{deleteType === 'role' ? '角色' : '聊天'} "{showDeleteConfirm}" 吗?

-
- - +
@@ -523,6 +108,9 @@ const Toolbar = ({

设置

+

设置内容...

@@ -536,6 +124,9 @@ const Toolbar = ({

帮助

+

帮助内容...

diff --git a/frontend/__pycache__/app.cpython-39.pyc b/frontend/__pycache__/app.cpython-39.pyc new file mode 100644 index 0000000..8481713 Binary files /dev/null and b/frontend/__pycache__/app.cpython-39.pyc differ