修补选中聊天不更换角色bug
This commit is contained in:
@@ -1,18 +1,20 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from ..core.items import ChatRequest
|
from ..core.items import ChatRequest
|
||||||
from ..tools.get_all_role_and_chat import get_all_role_and_chat
|
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()
|
router = APIRouter()
|
||||||
|
|
||||||
# 1. 将输入内容持久化存储到本地jsonl方便前端读
|
# 1. 从本地读取所有的data内容
|
||||||
@router.post("/generate_reply")
|
|
||||||
async def save_chat_to_json(chat_request: ChatRequest):
|
|
||||||
# 调用实际的保存函数
|
|
||||||
return await save_input_to_json(chat_request)
|
|
||||||
|
|
||||||
# 2. 从本地jsonl中读取历史对话
|
|
||||||
@router.get("/tool_bar/get_all_role_and_chat")
|
@router.get("/tool_bar/get_all_role_and_chat")
|
||||||
def get_all_role_and_chat_endpoint():
|
def get_all_role_and_chat_endpoint():
|
||||||
# 正确调用函数并返回结果
|
# 正确调用函数并返回结果
|
||||||
return get_all_role_and_chat()
|
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()
|
||||||
|
|||||||
@@ -7,152 +7,224 @@ import json
|
|||||||
|
|
||||||
class Message(BaseModel):
|
class Message(BaseModel):
|
||||||
"""消息类,代表JSONL文件中的一行消息内容"""
|
"""消息类,代表JSONL文件中的一行消息内容"""
|
||||||
name: str = Field(..., description="发言者名称")
|
name: str = Field(..., description="发送者名称")
|
||||||
is_user: bool = Field(..., description="是否为用户消息(true=用户,false=AI/角色)")
|
is_user: bool = Field(..., description="是否为用户消息")
|
||||||
is_system: bool = Field(False, description="是否为系统消息(系统消息在文本导出时会被排除)")
|
is_system: bool = Field(False, description="是否为系统消息")
|
||||||
send_date: str = Field(default_factory=lambda: str(int(datetime.now().timestamp() * 1000)),
|
send_date: str = Field(
|
||||||
description="发送时间戳(Unix毫秒数)")
|
default_factory=lambda: str(int(datetime.now().timestamp() * 1000)),
|
||||||
mes: str = Field(..., description="消息正文内容")
|
description="消息发送时间戳"
|
||||||
extra: Dict[str, Any] = Field(default_factory=dict, description="额外信息,包含推理内容、API、模型等")
|
)
|
||||||
swipes: List[str] = Field(default_factory=list, description="备选回复列表")
|
floor: int = Field(0, description="对话楼层数")
|
||||||
swipe_id: int = Field(0, description="当前选中的备选索引(0=第一条)")
|
mes: str = Field(..., description="消息内容文本")
|
||||||
swipe_info: List[Dict[str, Any]] = Field(default_factory=list, description="每个备选回复的生成信息")
|
extra: Dict[str, Any] = Field(
|
||||||
title: str = Field("", description="消息标题,用于消息摘要或分支标记")
|
default_factory=dict,
|
||||||
force_avatar: Optional[str] = Field(None, description="强制头像路径")
|
description="额外信息,包含推理内容、API、模型等"
|
||||||
variables: List[Any] = Field(default_factory=list, description="变量值数组")
|
)
|
||||||
|
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="变量初始化状态数组")
|
variables_initialized: List[bool] = Field(default_factory=list, description="变量初始化状态数组")
|
||||||
is_ejs_processed: List[bool] = Field(default_factory=list, description="EJS模板处理状态数组")
|
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_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):
|
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哈希值")
|
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_prompt: str = Field("", description="作者笔记提示词")
|
||||||
note_depth: int = Field(0, description="笔记深度(整数)")
|
note_interval: int = Field(0, description="笔记插入间隔数")
|
||||||
note_role: int = Field(0, description="笔记角色(整数,0=用户,1=助手)")
|
note_position: int = Field(0, description="笔记插入位置")
|
||||||
extensions: Dict[str, Any] = Field(default_factory=dict, description="扩展信息,如LittleWhiteBox等")
|
note_depth: int = Field(0, description="笔记插入深度")
|
||||||
timedWorldInfo: Dict[str, Any] = Field(default_factory=dict, description="定时世界信息")
|
note_role: str = Field("", description="笔记使用角色类型")
|
||||||
variables: Dict[str, Any] = Field(default_factory=dict, description="变量字典")
|
|
||||||
tainted: bool = Field(False, description="是否被污染")
|
# 扩展信息
|
||||||
lastInContextMessageId: int = Field(-1, description="上下文中最后一条消息的ID")
|
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):
|
class ChatHistory(BaseModel):
|
||||||
"""聊天文件类,包含元数据和消息列表"""
|
"""聊天文件类,包含完整的聊天记录"""
|
||||||
user_name: str = Field("User", description="用户名")
|
chat_metadata: ChatMetadata = Field(..., 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="消息列表")
|
messages: List[Message] = Field(default_factory=list, description="消息列表")
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
arbitrary_types_allowed = True
|
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: 聊天名称
|
ChatHistory: 加载的聊天历史对象
|
||||||
role_name: 角色名称
|
|
||||||
base_path: 基础路径,默认为项目数据目录
|
|
||||||
|
|
||||||
返回:
|
异常:
|
||||||
dict: 包含元数据和消息列表的原始数据字典
|
FileNotFoundError: 当文件不存在时抛出
|
||||||
"""
|
json.JSONDecodeError: 当JSON解析失败时抛出
|
||||||
# 设置默认基础路径
|
"""
|
||||||
if base_path is None:
|
# 设置默认基础路径 - 如果未提供base_path,则从配置中获取默认路径
|
||||||
from backend.core.config import settings
|
if base_path is None:
|
||||||
base_path = settings.DATA_PATH / "chat"
|
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():
|
if not file_path.exists():
|
||||||
raise FileNotFoundError(f"聊天文件不存在: {file_path}")
|
raise FileNotFoundError(f"聊天文件不存在: {file_path}")
|
||||||
|
|
||||||
# 读取文件内容
|
# 初始化结果数据
|
||||||
result = {
|
messages = []
|
||||||
"user_name": "User",
|
metadata = None
|
||||||
"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:
|
with open(file_path, 'r', encoding='utf-8') as f:
|
||||||
try:
|
for line_num, line in enumerate(f):
|
||||||
# 解析JSON行
|
try:
|
||||||
line_data = json.loads(line.strip())
|
line_data = json.loads(line.strip())
|
||||||
|
|
||||||
# 添加到消息列表
|
# 第一行是元数据
|
||||||
result["messages"].append(line_data)
|
if line_num == 0:
|
||||||
except json.JSONDecodeError:
|
metadata = ChatMetadata(**line_data)
|
||||||
continue
|
else:
|
||||||
|
# 后续行是消息
|
||||||
|
messages.append(Message(**line_data))
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
continue
|
||||||
|
|
||||||
return result
|
# 创建并返回ChatHistory对象
|
||||||
|
return cls(
|
||||||
|
chat_metadata=metadata or ChatMetadata(),
|
||||||
def create_chat_file_from_data(data: Dict[str, Any]) -> ChatFile:
|
messages=messages
|
||||||
"""
|
|
||||||
从原始数据创建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对象
|
@classmethod
|
||||||
return ChatFile(
|
def load_from_jsonl(cls, file_path: Path) -> 'ChatHistory':
|
||||||
user_name=data.get("user_name", "User"),
|
"""
|
||||||
character_name=data.get("character_name", "Assistant"),
|
从JSONL文件加载聊天历史
|
||||||
create_date=data.get("create_date", datetime.now().isoformat()),
|
|
||||||
chat_metadata=chat_metadata,
|
|
||||||
messages=messages
|
|
||||||
)
|
|
||||||
|
|
||||||
|
参数:
|
||||||
|
file_path: JSONL文件路径
|
||||||
|
|
||||||
def load_chat_file(chat_name: str, role_name: str, base_path: Path = None) -> ChatFile:
|
返回:
|
||||||
"""
|
ChatHistory: 加载的聊天历史对象
|
||||||
从文件系统加载聊天数据并创建ChatFile对象
|
|
||||||
|
|
||||||
参数:
|
异常:
|
||||||
chat_name: 聊天名称
|
FileNotFoundError: 当文件不存在时抛出
|
||||||
role_name: 角色名称
|
json.JSONDecodeError: 当JSON解析失败时抛出
|
||||||
base_path: 基础路径,默认为项目数据目录
|
"""
|
||||||
|
# 检查文件是否存在
|
||||||
|
if not file_path.exists():
|
||||||
|
raise FileNotFoundError(f"聊天文件不存在: {file_path}")
|
||||||
|
|
||||||
返回:
|
# 初始化结果数据
|
||||||
ChatFile: 加载的聊天文件对象
|
messages = []
|
||||||
"""
|
metadata = None
|
||||||
# 加载原始数据
|
|
||||||
data = load_chat_file_data(chat_name, role_name, base_path)
|
|
||||||
|
|
||||||
# 创建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
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ if __name__ == '__main__':
|
|||||||
async def test():
|
async def test():
|
||||||
req = MockChatRequest(
|
req = MockChatRequest(
|
||||||
mes="这是重roll后的新回复2",
|
mes="这是重roll后的新回复2",
|
||||||
role_name="test",
|
role_name="testRole1",
|
||||||
chat_name="111",
|
chat_name="111",
|
||||||
name="AI",
|
name="AI",
|
||||||
is_user=False,
|
is_user=False,
|
||||||
|
|||||||
@@ -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}
|
|
||||||
4
data/chat/testRole1/12331212.jsonl
Normal file
4
data/chat/testRole1/12331212.jsonl
Normal file
@@ -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}
|
||||||
3
data/chat/testRole1/33211.jsonl
Normal file
3
data/chat/testRole1/33211.jsonl
Normal file
@@ -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}
|
||||||
4
data/chat/testRole2/222.jsonl
Normal file
4
data/chat/testRole2/222.jsonl
Normal file
@@ -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}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import Toolbar from './components/ToolBar/ToolBar';
|
import Toolbar from './components/ToolBar/ToolBar';
|
||||||
import ChatBox from './components/ChatBox/ChatBox';
|
import ChatBox from './components/ChatBox/ChatBox';
|
||||||
import DicePanel from './components/DicePanel/DicePanel';
|
import DicePanel from './components/DicePanel/DicePanel';
|
||||||
@@ -7,25 +7,9 @@ import PresetPanel from './components/PresetPanel/PresetPanel';
|
|||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
function App() {
|
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 (
|
return (
|
||||||
<div className="app">
|
<div className="app">
|
||||||
<Toolbar
|
<Toolbar />
|
||||||
onRoleChange={handleRoleChange}
|
|
||||||
onChatChange={handleChatChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 主内容容器 */}
|
{/* 主内容容器 */}
|
||||||
<div className="main-container">
|
<div className="main-container">
|
||||||
@@ -36,10 +20,7 @@ function App() {
|
|||||||
|
|
||||||
{/* 中间栏:聊天框 */}
|
{/* 中间栏:聊天框 */}
|
||||||
<div className="chat-area">
|
<div className="chat-area">
|
||||||
<ChatBox
|
<ChatBox />
|
||||||
selectedRole={selectedRole}
|
|
||||||
selectedChat={selectedChat}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 右侧栏 */}
|
{/* 右侧栏 */}
|
||||||
|
|||||||
95
frontend-react/src/Store/Slices/ChatBoxSlice.jsx
Normal file
95
frontend-react/src/Store/Slices/ChatBoxSlice.jsx
Normal file
@@ -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;
|
||||||
@@ -403,3 +403,27 @@
|
|||||||
.send-button:hover {
|
.send-button:hover {
|
||||||
background-color: #40a9ff;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React, { useState, useRef } from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
|
import useChatBoxStore from '../../Store/Slices/ChatBoxSlice';
|
||||||
import './ChatBox.css';
|
import './ChatBox.css';
|
||||||
|
|
||||||
const ChatBox = ({ selectedRole, selectedChat }) => {
|
const ChatBox = () => {
|
||||||
const [isHtmlRender, setIsHtmlRender] = useState(false);
|
const [isHtmlRender, setIsHtmlRender] = useState(false);
|
||||||
const [isImageGen, setIsImageGen] = useState(false);
|
const [isImageGen, setIsImageGen] = useState(false);
|
||||||
const [isDynamicTable, setIsDynamicTable] = useState(false);
|
const [isDynamicTable, setIsDynamicTable] = useState(false);
|
||||||
@@ -10,6 +11,9 @@ const ChatBox = ({ selectedRole, selectedChat }) => {
|
|||||||
|
|
||||||
const textareaRef = useRef(null);
|
const textareaRef = useRef(null);
|
||||||
|
|
||||||
|
// 从 store 获取状态
|
||||||
|
const { messages, userName, characterName, isLoading, error } = useChatBoxStore();
|
||||||
|
|
||||||
// 自动调整 Textarea 高度
|
// 自动调整 Textarea 高度
|
||||||
const adjustHeight = () => {
|
const adjustHeight = () => {
|
||||||
const textarea = textareaRef.current;
|
const textarea = textareaRef.current;
|
||||||
@@ -60,36 +64,21 @@ const ChatBox = ({ selectedRole, selectedChat }) => {
|
|||||||
setEditContent('');
|
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 回复。<b>包含 HTML 标签</b>的内容。如果渲染开关开启,这里应该显示粗体字。如果不开启,应该显示原始标签。`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return messages;
|
|
||||||
};
|
|
||||||
|
|
||||||
const messages = generateMessages();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="chat-box">
|
<div className="chat-box">
|
||||||
{/* 上方:消息列表区域 */}
|
{/* 上方:消息列表区域 */}
|
||||||
<div className="chat-messages">
|
<div className="chat-messages">
|
||||||
|
{/* 加载状态和错误信息 */}
|
||||||
|
{isLoading && <div className="loading">加载中...</div>}
|
||||||
|
{error && <div className="error">{error}</div>}
|
||||||
|
|
||||||
{/* 消息列表 */}
|
{/* 消息列表 */}
|
||||||
{messages.map((msg) => (
|
{messages.map((msg) => (
|
||||||
<div key={msg.id} className={`message ${msg.role}`}>
|
<div key={msg.id} className={`message ${msg.role}`}>
|
||||||
<div className="message-container">
|
<div className="message-container">
|
||||||
{/* 消息名称和工具栏在同一行 */}
|
{/* 消息名称和工具栏在同一行 */}
|
||||||
<div className="message-header">
|
<div className="message-header">
|
||||||
<div className="message-name">{msg.name}</div>
|
<div className="message-name">{msg.role === 'user' ? userName : characterName}</div>
|
||||||
|
|
||||||
{/* 消息工具栏 */}
|
{/* 消息工具栏 */}
|
||||||
<div className="message-toolbar">
|
<div className="message-toolbar">
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import useRoleSelectorStore from '../../store/Slices/RoleSelectorSlice';
|
import useRoleSelectorStore from '../../store/Slices/RoleSelectorSlice';
|
||||||
|
import useChatBoxStore from '../../Store/Slices/ChatBoxSlice';
|
||||||
import './RoleSelector.css';
|
import './RoleSelector.css';
|
||||||
|
|
||||||
const RoleSelector = ({ onRoleChange, onChatChange }) => {
|
const RoleSelector = () => {
|
||||||
const panelRef = useRef(null);
|
const panelRef = useRef(null);
|
||||||
|
|
||||||
// 从 Zustand store 中获取状态和操作
|
// 从 Zustand store 中获取状态和操作
|
||||||
@@ -37,6 +38,9 @@ const RoleSelector = ({ onRoleChange, onChatChange }) => {
|
|||||||
resetPanel
|
resetPanel
|
||||||
} = useRoleSelectorStore();
|
} = useRoleSelectorStore();
|
||||||
|
|
||||||
|
// 从 ChatBoxStore 获取状态更新方法
|
||||||
|
const { setCurrentRole, setCurrentChat } = useChatBoxStore();
|
||||||
|
|
||||||
// 组件挂载时获取数据
|
// 组件挂载时获取数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchRoleData();
|
fetchRoleData();
|
||||||
@@ -59,41 +63,56 @@ const RoleSelector = ({ onRoleChange, onChatChange }) => {
|
|||||||
// 处理角色选择
|
// 处理角色选择
|
||||||
const handleRoleSelect = (role) => {
|
const handleRoleSelect = (role) => {
|
||||||
setSelectedRole(role);
|
setSelectedRole(role);
|
||||||
if (onRoleChange) {
|
// 更新 ChatBoxStore 中的当前角色
|
||||||
onRoleChange(role);
|
setCurrentRole(role);
|
||||||
}
|
|
||||||
|
|
||||||
// 如果该角色有聊天记录,默认选择第一个
|
// 如果该角色有聊天记录,默认选择第一个
|
||||||
if (roleData[role] && roleData[role].length > 0) {
|
if (roleData[role] && roleData[role].length > 0) {
|
||||||
const firstChat = roleData[role][0];
|
const firstChat = roleData[role][0];
|
||||||
setSelectedChat(firstChat);
|
setSelectedChat(firstChat);
|
||||||
if (onChatChange) {
|
// 更新 ChatBoxStore 中的当前聊天
|
||||||
onChatChange(role, firstChat);
|
setCurrentChat(firstChat);
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
setSelectedChat(null);
|
setSelectedChat(null);
|
||||||
|
// 清除 ChatBoxStore 中的当前聊天
|
||||||
|
setCurrentChat(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理聊天选择
|
// 处理聊天选择
|
||||||
const handleChatSelect = (chat) => {
|
const handleChatSelect = (chat) => {
|
||||||
setSelectedChat(chat);
|
// 获取当前展开的角色(聊天所属的角色)
|
||||||
setHoveredRole(null);
|
const currentRole = hoveredRole || clickedRole;
|
||||||
setClickedRole(null);
|
|
||||||
if (onChatChange) {
|
setSelectedChat(chat);
|
||||||
onChatChange(selectedRole, chat);
|
setSelectedRole(currentRole); // 使用当前展开的角色
|
||||||
}
|
setHoveredRole(null);
|
||||||
|
setClickedRole(null);
|
||||||
|
// 更新 ChatBoxStore 中的当前聊天和角色
|
||||||
|
setCurrentChat(chat);
|
||||||
|
setCurrentRole(currentRole);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理角色卡片点击
|
// 处理角色卡片点击
|
||||||
const handleRoleCardClick = (role) => {
|
const handleRoleCardClick = (role) => {
|
||||||
if (clickedRole === role) {
|
if (clickedRole === role) {
|
||||||
setClickedRole(null);
|
setClickedRole(null);
|
||||||
} else {
|
// 取消选择角色时,更新 selectedRole 和 selectedChat
|
||||||
setClickedRole(role);
|
setSelectedRole(null);
|
||||||
handleRoleSelect(role);
|
setSelectedChat(null);
|
||||||
}
|
// 同步更新 ChatBoxStore 中的状态
|
||||||
};
|
setCurrentRole(null);
|
||||||
|
setCurrentChat(null);
|
||||||
|
} else {
|
||||||
|
setClickedRole(role);
|
||||||
|
handleRoleSelect(role);
|
||||||
|
setSelectedRole(role);
|
||||||
|
setSelectedChat(chat);
|
||||||
|
// 同步更新 ChatBoxStore 中的状态
|
||||||
|
setSele
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// 处理搜索
|
// 处理搜索
|
||||||
const handleSearchChange = (e) => {
|
const handleSearchChange = (e) => {
|
||||||
@@ -112,9 +131,8 @@ const RoleSelector = ({ onRoleChange, onChatChange }) => {
|
|||||||
const newName = e.target.value;
|
const newName = e.target.value;
|
||||||
handleRenameRole(oldName, newName);
|
handleRenameRole(oldName, newName);
|
||||||
if (selectedRole === oldName && newName && newName !== oldName) {
|
if (selectedRole === oldName && newName && newName !== oldName) {
|
||||||
if (onRoleChange) {
|
// 更新 ChatBoxStore 中的当前角色
|
||||||
onRoleChange(newName);
|
setCurrentRole(newName);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -130,9 +148,8 @@ const RoleSelector = ({ onRoleChange, onChatChange }) => {
|
|||||||
const newName = e.target.value;
|
const newName = e.target.value;
|
||||||
handleRenameChat(oldName, newName);
|
handleRenameChat(oldName, newName);
|
||||||
if (selectedChat === oldName && newName && newName !== oldName) {
|
if (selectedChat === oldName && newName && newName !== oldName) {
|
||||||
if (onChatChange) {
|
// 更新 ChatBoxStore 中的当前聊天
|
||||||
onChatChange(selectedRole, newName);
|
setCurrentChat(selectedRole, newName);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -147,13 +164,12 @@ const RoleSelector = ({ onRoleChange, onChatChange }) => {
|
|||||||
const confirmDeleteWrapper = () => {
|
const confirmDeleteWrapper = () => {
|
||||||
confirmDelete();
|
confirmDelete();
|
||||||
if (deleteType === 'role' && selectedRole === showDeleteConfirm) {
|
if (deleteType === 'role' && selectedRole === showDeleteConfirm) {
|
||||||
if (onRoleChange) {
|
// 清除 ChatBoxStore 中的当前角色和聊天
|
||||||
onRoleChange(null);
|
setCurrentRole(null);
|
||||||
}
|
setCurrentChat(null);
|
||||||
} else if (deleteType === 'chat' && selectedChat === showDeleteConfirm) {
|
} else if (deleteType === 'chat' && selectedChat === showDeleteConfirm) {
|
||||||
if (onChatChange) {
|
// 清除 ChatBoxStore 中的当前聊天
|
||||||
onChatChange(selectedRole, null);
|
setCurrentChat(null);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user