最新代码

This commit is contained in:
2026-03-20 18:13:52 +08:00
parent 73cdf5ac23
commit 6c74bef8da
18 changed files with 944 additions and 939 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -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)

Binary file not shown.

View File

@@ -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 文件,则添加到结果中

View File

@@ -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;
}

View File

@@ -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 (
<div className="role-selector">
<div className="dropdown-container">
<div
className="dropdown-trigger"
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
>
{selectedRole || '选择角色'}
<span className="dropdown-arrow"></span>
<div className="role-selector" ref={panelRef}>
<div className="role-panel">
<div className="panel-controls">
<div className="search-box">
<input
type="text"
placeholder="搜索角色..."
value={searchTerm}
onChange={handleSearchChange}
/>
</div>
<button className="add-button" onClick={handleAddRole}>
<span className="icon">+</span>
<span>新增角色</span>
</button>
</div>
{isDropdownOpen && (
<div className="dropdown-menu">
{Object.keys(roleData).map(role => (
<div
key={role}
className={`dropdown-item ${selectedRole === role ? 'selected' : ''}`}
onClick={() => handleRoleSelect(role)}
>
{role}
{selectedRole === role && (
<div className="nested-dropdown">
{roleData[role].map(chat => (
<div
key={chat}
className={`dropdown-item ${selectedChat === chat ? 'selected' : ''}`}
onClick={(e) => {
e.stopPropagation();
handleChatSelect(chat);
<div className="panel-list-container">
{isLoading ? (
<div className="loading-container">
<div className="loading-spinner"></div>
<span>加载中...</span>
</div>
) : filteredRoles.length === 0 ? (
<div className="empty-state">
<p>没有找到匹配的角色</p>
</div>
) : (
<div className="role-cards-grid">
{filteredRoles.map(role => (
<div
key={role}
className={`role-card ${selectedRole === role ? 'selected' : ''} ${hoveredRole === role ? 'hovered' : ''} ${clickedRole === role ? 'clicked' : ''}`}
onClick={() => handleRoleCardClick(role)}
onMouseEnter={() => setHoveredRole(role)}
onMouseLeave={() => setHoveredRole(null)}
>
<div className="role-card-header">
{editingRole === role ? (
<input
type="text"
defaultValue={role}
autoFocus
onBlur={(e) => handleRenameRole(e, role)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleRenameRole(e, role);
}
}}
>
{chat}
</div>
))}
onClick={(e) => e.stopPropagation()}
/>
) : (
<div className="role-name">{role}</div>
)}
<div className="role-chat-count">
{roleData[role] ? roleData[role].length : 0} 个聊天
</div>
</div>
)}
</div>
))}
</div>
)}
<div className="role-card-actions">
{editingRole !== role && (
<>
<button
className="action-button edit-button"
title="编辑"
onClick={(e) => handleEditRole(e, role)}
>
</button>
<button
className="action-button delete-button"
title="删除"
onClick={(e) => handleDeleteClick(e, 'role', role)}
>
🗑
</button>
</>
)}
</div>
{/* 聊天列表 */}
{(hoveredRole === role || clickedRole === role) && roleData[role] && roleData[role].length > 0 && (
<div className="chat-list">
<div className="chat-list-header">
<span>聊天记录</span>
<button
className="add-chat-button"
onClick={(e) => {
e.stopPropagation();
handleAddChat();
}}
>
<span className="icon">+</span>
<span>新建聊天</span>
</button>
</div>
{roleData[role].map(chat => (
<div
key={chat}
className={`chat-item ${selectedChat === chat ? 'selected' : ''}`}
onClick={(e) => {
e.stopPropagation();
handleChatSelect(chat);
}}
>
<div className="chat-content">
{editingChat === chat ? (
<input
type="text"
defaultValue={chat}
autoFocus
onBlur={(e) => handleRenameChat(e, chat)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleRenameChat(e, chat);
}
}}
onClick={(e) => e.stopPropagation()}
/>
) : (
<div className="chat-name">{chat}</div>
)}
</div>
<div className="chat-actions">
{editingChat !== chat && (
<>
<button
className="action-button edit-button"
title="编辑"
onClick={(e) => handleEditChat(e, chat)}
>
</button>
<button
className="action-button delete-button"
title="删除"
onClick={(e) => handleDeleteClick(e, 'chat', chat)}
>
🗑
</button>
</>
)}
</div>
</div>
))}
</div>
)}
</div>
))}
</div>
)}
</div>
</div>
{/* 删除确认对话框 */}
{showDeleteConfirm && (
<div className="modal-overlay">
<div className="modal-content">
<h3>确认删除</h3>
<p>确定要删除{deleteType === 'role' ? '角色' : '聊天'} "{showDeleteConfirm}" </p>
<div className="modal-actions">
<button className="modal-button cancel-button" onClick={cancelDelete}>
取消
</button>
<button className="modal-button confirm-button" onClick={confirmDelete}>
确认删除
</button>
</div>
</div>
</div>
)}
</div>
);
};

View File

@@ -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;

View File

@@ -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 (
<>
<div className="toolbar">
@@ -332,187 +89,15 @@ const Toolbar = ({
{/* 角色管理面板 */}
{activePanel === 'role' && (
<div className="panel-overlay" ref={panelRef}>
<div className="panel-content role-panel">
<div className="panel-content">
<div className="panel-header">
<h3>角色管理</h3>
<div className="current-info">
<span>当前角色: {selectedRole || '未选择'}</span>
<span>当前聊天: {selectedChat || '未选择'}</span>
</div>
<button className="close-panel-button" onClick={handleClosePanel} title="关闭">
</button>
</div>
<div className="panel-body">
<div className="panel-controls">
<div className="search-box">
<input
type="text"
placeholder="搜索角色..."
value={searchTerm}
onChange={handleSearchChange}
/>
</div>
<button className="add-button" onClick={handleAddRole}>
<span className="icon">+</span>
<span>新增角色</span>
</button>
</div>
<div className="panel-list-container">
{isLoading ? (
<div className="loading-container">
<div className="loading-spinner"></div>
<span>加载中...</span>
</div>
) : filteredRoles.length === 0 ? (
<div className="empty-state">
<p>没有找到匹配的角色</p>
</div>
) : (
<div className="role-cards-grid">
{filteredRoles.map(role => (
<div
key={role}
className={`role-card ${selectedRole === role ? 'selected' : ''} ${hoveredRole === role ? 'hovered' : ''} ${clickedRole === role ? 'clicked' : ''}`}
onClick={() => handleRoleCardClick(role)}
onMouseEnter={() => setHoveredRole(role)}
onMouseLeave={() => setHoveredRole(null)}
>
<div className="role-card-header">
{editingRole === role ? (
<input
type="text"
defaultValue={role}
autoFocus
onBlur={(e) => handleRenameRole(e, role)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleRenameRole(e, role);
}
}}
onClick={(e) => e.stopPropagation()}
/>
) : (
<div className="role-name">{role}</div>
)}
<div className="role-chat-count">
{roleData[role] ? roleData[role].length : 0} 个聊天
</div>
</div>
<div className="role-card-actions">
{editingRole !== role && (
<>
<button
className="action-button edit-button"
title="编辑"
onClick={(e) => handleEditRole(e, role)}
>
</button>
<button
className="action-button delete-button"
title="删除"
onClick={(e) => handleDeleteClick(e, 'role', role)}
>
🗑
</button>
</>
)}
</div>
{/* 聊天列表 */}
{(hoveredRole === role || clickedRole === role) && roleData[role] && roleData[role].length > 0 && (
<div className="chat-list">
<div className="chat-list-header">
<span>聊天记录</span>
<button
className="add-chat-button"
onClick={(e) => {
e.stopPropagation();
handleAddChat();
}}
>
<span className="icon">+</span>
<span>新建聊天</span>
</button>
</div>
{roleData[role].map(chat => (
<div
key={chat}
className={`chat-item ${selectedChat === chat ? 'selected' : ''}`}
onClick={(e) => {
e.stopPropagation();
handleChatSelect(chat);
}}
>
<div className="chat-content">
{editingChat === chat ? (
<input
type="text"
defaultValue={chat}
autoFocus
onBlur={(e) => handleRenameChat(e, chat)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleRenameChat(e, chat);
}
}}
onClick={(e) => e.stopPropagation()}
/>
) : (
<div className="chat-name">{chat}</div>
)}
</div>
<div className="chat-actions">
{editingChat !== chat && (
<>
<button
className="action-button edit-button"
title="编辑"
onClick={(e) => handleEditChat(e, chat)}
>
</button>
<button
className="action-button delete-button"
title="删除"
onClick={(e) => handleDeleteClick(e, 'chat', chat)}
>
🗑
</button>
</>
)}
</div>
</div>
))}
</div>
)}
</div>
))}
</div>
)}
</div>
</div>
</div>
</div>
)}
{/* 删除确认对话框 */}
{showDeleteConfirm && (
<div className="modal-overlay">
<div className="modal-content">
<h3>确认删除</h3>
<p>确定要删除{deleteType === 'role' ? '角色' : '聊天'} "{showDeleteConfirm}" </p>
<div className="modal-actions">
<button className="modal-button cancel-button" onClick={cancelDelete}>
取消
</button>
<button className="modal-button confirm-button" onClick={confirmDelete}>
确认删除
</button>
<RoleSelector />
</div>
</div>
</div>
@@ -523,6 +108,9 @@ const Toolbar = ({
<div className="panel-content">
<div className="panel-header">
<h3>设置</h3>
<button className="close-panel-button" onClick={handleClosePanel} title="关闭">
</button>
</div>
<div className="panel-body">
<p>设置内容...</p>
@@ -536,6 +124,9 @@ const Toolbar = ({
<div className="panel-content">
<div className="panel-header">
<h3>帮助</h3>
<button className="close-panel-button" onClick={handleClosePanel} title="关闭">
</button>
</div>
<div className="panel-body">
<p>帮助内容...</p>

Binary file not shown.