重构路由架构,修复导致的前端出错

This commit is contained in:
2026-04-06 00:52:04 +08:00
parent 7a62139683
commit e8dedb5ec4
12 changed files with 2055 additions and 1102 deletions

View File

@@ -1,7 +1,4 @@
from fastapi import APIRouter, HTTPException, status from fastapi import APIRouter, HTTPException, status
from pathlib import Path
import json
from typing import List, Dict
from backend.core.models.chat_history import ChatHistory, Message from backend.core.models.chat_history import ChatHistory, Message
router = APIRouter(prefix="/chats", tags=["chats"]) router = APIRouter(prefix="/chats", tags=["chats"])
@@ -9,84 +6,35 @@ router = APIRouter(prefix="/chats", tags=["chats"])
# ========== 聊天历史基础路由 ========== # ========== 聊天历史基础路由 ==========
@router.get("", response_model=Dict[str, List[Dict]]) @router.get("", response_model=dict)
async def list_all_chats(): async def list_all_chats():
"""获取所有角色的所有聊天列表""" """获取所有角色的所有聊天列表"""
data_dir = Path("data") return await ChatHistory.list_all_chats()
if not data_dir.exists():
return {"chats": []}
chats = []
for role_dir in data_dir.iterdir():
if role_dir.is_dir():
for chat_file in role_dir.glob("*.jsonl"):
try:
with open(chat_file, 'r', encoding='utf-8') as f:
# 读取第一行获取元数据
first_line = f.readline()
metadata = json.loads(first_line)
chats.append({
"role_name": role_dir.name,
"chat_name": chat_file.stem,
"user_name": metadata.get("user_name", "User"),
"character_name": metadata.get("character_name", "Assistant"),
"last_modified": metadata.get("last_modified", ""),
"message_count": sum(1 for _ in f) # 统计剩余行数(消息数)
})
except Exception as e:
continue # 跳过损坏的聊天文件
return {"chats": chats}
@router.get("/{role_name}/{chat_name}") @router.get("/{role_name}/{chat_name}")
async def get_chat(role_name: str, chat_name: str): async def get_chat(role_name: str, chat_name: str):
"""获取指定聊天的完整内容""" """获取指定聊天的完整内容"""
try: try:
chat_history = ChatHistory.load_from_file(role_name, chat_name) return await ChatHistory.get_chat(role_name, chat_name)
return { except FileNotFoundError as e:
"metadata": chat_history.chat_metadata.dict(),
"messages": chat_history.to_chatbox_format()
}
except FileNotFoundError:
raise HTTPException(status_code=404, detail="Chat not found") raise HTTPException(status_code=404, detail="Chat not found")
@router.post("/{role_name}", status_code=status.HTTP_201_CREATED) @router.post("/{role_name}", status_code=status.HTTP_201_CREATED)
async def create_chat(role_name: str, chat_name: str, metadata: Dict = None): async def create_chat(role_name: str, chat_name: str, metadata: dict = None):
"""创建新聊天""" """创建新聊天"""
role_dir = Path("data") / role_name try:
role_dir.mkdir(parents=True, exist_ok=True) return await ChatHistory.create_chat(role_name, chat_name, metadata)
chat_path = role_dir / f"{chat_name}.jsonl" except FileExistsError:
if chat_path.exists():
raise HTTPException(status_code=400, detail="Chat already exists") raise HTTPException(status_code=400, detail="Chat already exists")
# 创建聊天历史对象
chat_history = ChatHistory(
chat_metadata=metadata or {},
messages=[]
)
# 保存到文件
chat_history.save_to_file(role_name, chat_name)
return {"message": "Chat created successfully"}
@router.put("/{role_name}/{chat_name}") @router.put("/{role_name}/{chat_name}")
async def update_chat(role_name: str, chat_name: str, update_data: Dict): async def update_chat(role_name: str, chat_name: str, update_data: dict):
"""更新聊天元数据""" """更新聊天元数据"""
try: try:
chat_history = ChatHistory.load_from_file(role_name, chat_name) return await ChatHistory.update_chat(role_name, chat_name, update_data)
# 更新元数据
if "metadata" in update_data:
for key, value in update_data["metadata"].items():
if hasattr(chat_history.chat_metadata, key):
setattr(chat_history.chat_metadata, key, value)
# 保存更改
chat_history.save_to_file(role_name, chat_name)
return {"message": "Chat metadata updated successfully"}
except FileNotFoundError: except FileNotFoundError:
raise HTTPException(status_code=404, detail="Chat not found") raise HTTPException(status_code=404, detail="Chat not found")
@@ -94,11 +42,10 @@ async def update_chat(role_name: str, chat_name: str, update_data: Dict):
@router.delete("/{role_name}/{chat_name}") @router.delete("/{role_name}/{chat_name}")
async def delete_chat(role_name: str, chat_name: str): async def delete_chat(role_name: str, chat_name: str):
"""删除指定聊天""" """删除指定聊天"""
chat_path = Path("data") / role_name / f"{chat_name}.jsonl" try:
if not chat_path.exists(): return await ChatHistory.delete_chat(role_name, chat_name)
except FileNotFoundError:
raise HTTPException(status_code=404, detail="Chat not found") raise HTTPException(status_code=404, detail="Chat not found")
chat_path.unlink()
return {"message": "Chat deleted successfully"}
# ========== 聊天消息路由 ========== # ========== 聊天消息路由 ==========
@@ -107,8 +54,7 @@ async def delete_chat(role_name: str, chat_name: str):
async def list_messages(role_name: str, chat_name: str): async def list_messages(role_name: str, chat_name: str):
"""获取聊天的所有消息""" """获取聊天的所有消息"""
try: try:
chat_history = ChatHistory.load_from_file(role_name, chat_name) return await ChatHistory.list_messages(role_name, chat_name)
return {"messages": chat_history.to_chatbox_format()}
except FileNotFoundError: except FileNotFoundError:
raise HTTPException(status_code=404, detail="Chat not found") raise HTTPException(status_code=404, detail="Chat not found")
@@ -117,58 +63,27 @@ async def list_messages(role_name: str, chat_name: str):
async def get_message(role_name: str, chat_name: str, floor: int): async def get_message(role_name: str, chat_name: str, floor: int):
"""获取指定楼层的消息""" """获取指定楼层的消息"""
try: try:
chat_history = ChatHistory.load_from_file(role_name, chat_name) return await ChatHistory.get_message(role_name, chat_name, floor)
message = next((msg for msg in chat_history.messages if msg.floor == floor), None)
if not message:
raise HTTPException(status_code=404, detail="Message not found")
return message.dict()
except FileNotFoundError: except FileNotFoundError:
raise HTTPException(status_code=404, detail="Chat not found") raise HTTPException(status_code=404, detail="Chat not found")
@router.post("/{role_name}/{chat_name}/messages", status_code=status.HTTP_201_CREATED) @router.post("/{role_name}/{chat_name}/messages", status_code=status.HTTP_201_CREATED)
async def add_message(role_name: str, chat_name: str, message_data: Dict): async def add_message(role_name: str, chat_name: str, message_data: dict):
"""向聊天添加新消息""" """向聊天添加新消息"""
try: try:
chat_history = ChatHistory.load_from_file(role_name, chat_name) return await ChatHistory.add_message(role_name, chat_name, message_data)
# 创建消息对象
message = Message(**message_data)
# 检查楼层是否已存在
if any(msg.floor == message.floor for msg in chat_history.messages):
raise HTTPException(status_code=400, detail="Message floor already exists")
# 添加消息
chat_history.messages.append(message)
# 保存更改
chat_history.save_to_file(role_name, chat_name)
return {"message": "Message added successfully", "floor": message.floor}
except FileNotFoundError: except FileNotFoundError:
raise HTTPException(status_code=404, detail="Chat not found") raise HTTPException(status_code=404, detail="Chat not found")
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.put("/{role_name}/{chat_name}/messages/{floor}") @router.put("/{role_name}/{chat_name}/messages/{floor}")
async def update_message(role_name: str, chat_name: str, floor: int, update_data: Dict): async def update_message(role_name: str, chat_name: str, floor: int, update_data: dict):
"""更新指定楼层的消息""" """更新指定楼层的消息"""
try: try:
chat_history = ChatHistory.load_from_file(role_name, chat_name) return await ChatHistory.update_message(role_name, chat_name, floor, update_data)
message = next((msg for msg in chat_history.messages if msg.floor == floor), None)
if not message:
raise HTTPException(status_code=404, detail="Message not found")
# 更新消息字段
for key, value in update_data.items():
if hasattr(message, key):
setattr(message, key, value)
# 保存更改
chat_history.save_to_file(role_name, chat_name)
return {"message": "Message updated successfully"}
except FileNotFoundError: except FileNotFoundError:
raise HTTPException(status_code=404, detail="Chat not found") raise HTTPException(status_code=404, detail="Chat not found")
@@ -177,17 +92,6 @@ async def update_message(role_name: str, chat_name: str, floor: int, update_data
async def delete_message(role_name: str, chat_name: str, floor: int): async def delete_message(role_name: str, chat_name: str, floor: int):
"""删除指定楼层的消息""" """删除指定楼层的消息"""
try: try:
chat_history = ChatHistory.load_from_file(role_name, chat_name) return await ChatHistory.delete_message(role_name, chat_name, floor)
# 查找并删除消息
original_length = len(chat_history.messages)
chat_history.messages = [msg for msg in chat_history.messages if msg.floor != floor]
if len(chat_history.messages) == original_length:
raise HTTPException(status_code=404, detail="Message not found")
# 保存更改
chat_history.save_to_file(role_name, chat_name)
return {"message": "Message deleted successfully"}
except FileNotFoundError: except FileNotFoundError:
raise HTTPException(status_code=404, detail="Chat not found") raise HTTPException(status_code=404, detail="Chat not found")

View File

@@ -1,7 +1,4 @@
from fastapi import APIRouter, HTTPException, status from fastapi import APIRouter, HTTPException, status
from pathlib import Path
import json
from typing import List, Dict
from backend.core.models.PromptList import AIDesignSpec from backend.core.models.PromptList import AIDesignSpec
from backend.core.models.PromptComponent import PromptComponent from backend.core.models.PromptComponent import PromptComponent
@@ -10,110 +7,46 @@ router = APIRouter(prefix="/presets", tags=["presets"])
# ========== 预设基础路由 ========== # ========== 预设基础路由 ==========
@router.get("", response_model=Dict[str, List[Dict]]) @router.get("", response_model=dict)
async def list_presets(): async def list_presets():
"""获取所有预设列表及其基本信息""" """获取所有预设列表及其基本信息"""
preset_dir = Path("data/preset") return await AIDesignSpec.list_all_presets()
if not preset_dir.exists():
return {"presets": []}
presets = []
for preset_file in preset_dir.glob("*.json"):
try:
with open(preset_file, 'r', encoding='utf-8') as f:
preset_data = json.load(f)
presets.append({
"name": preset_file.stem,
"description": preset_data.get("description", ""),
"component_count": len(preset_data.get("prompts", [])),
"temperature": preset_data.get("temperature", 1.0)
})
except Exception as e:
continue # 跳过损坏的预设文件
return {"presets": presets}
@router.get("/{preset_name}") @router.get("/{preset_name}")
async def get_preset(preset_name: str): async def get_preset(preset_name: str):
"""获取指定预设的完整内容""" """获取指定预设的完整内容"""
preset_path = Path("data/preset") / f"{preset_name}.json"
if not preset_path.exists():
raise HTTPException(status_code=404, detail="Preset not found")
try: try:
with open(preset_path, 'r', encoding='utf-8') as f: return await AIDesignSpec.get_preset(preset_name)
preset_data = json.load(f) except FileNotFoundError:
raise HTTPException(status_code=404, detail="Preset not found")
# 转换为AIDesignSpec对象进行验证
ai_design_spec = AIDesignSpec(**preset_data)
return ai_design_spec.dict()
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to load preset: {str(e)}")
@router.post("", status_code=status.HTTP_201_CREATED) @router.post("", status_code=status.HTTP_201_CREATED)
async def create_preset(preset_name: str, preset_data: Dict): async def create_preset(preset_name: str, preset_data: dict):
"""创建新预设""" """创建新预设"""
preset_dir = Path("data/preset")
preset_dir.mkdir(parents=True, exist_ok=True)
preset_path = preset_dir / f"{preset_name}.json"
if preset_path.exists():
raise HTTPException(status_code=400, detail="Preset already exists")
try: try:
# 验证并转换为AIDesignSpec对象 return await AIDesignSpec.create_preset(preset_name, preset_data)
ai_design_spec = AIDesignSpec(**preset_data) except FileExistsError:
raise HTTPException(status_code=400, detail="Preset already exists")
# 保存到文件
with open(preset_path, 'w', encoding='utf-8') as f:
json.dump(ai_design_spec.dict(), f, ensure_ascii=False, indent=2)
return {"message": "Preset created successfully", "name": preset_name}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to create preset: {str(e)}")
@router.put("/{preset_name}") @router.put("/{preset_name}")
async def update_preset(preset_name: str, update_data: Dict): async def update_preset(preset_name: str, update_data: dict):
"""更新预设配置""" """更新预设配置"""
preset_path = Path("data/preset") / f"{preset_name}.json"
if not preset_path.exists():
raise HTTPException(status_code=404, detail="Preset not found")
try: try:
# 加载现有预设 return await AIDesignSpec.update_preset(preset_name, update_data)
with open(preset_path, 'r', encoding='utf-8') as f: except FileNotFoundError:
preset_data = json.load(f) raise HTTPException(status_code=404, detail="Preset not found")
# 更新字段
for key, value in update_data.items():
preset_data[key] = value
# 验证并转换为AIDesignSpec对象
ai_design_spec = AIDesignSpec(**preset_data)
# 保存更新
with open(preset_path, 'w', encoding='utf-8') as f:
json.dump(ai_design_spec.dict(), f, ensure_ascii=False, indent=2)
return {"message": "Preset updated successfully"}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to update preset: {str(e)}")
@router.delete("/{preset_name}") @router.delete("/{preset_name}")
async def delete_preset(preset_name: str): async def delete_preset(preset_name: str):
"""删除指定预设""" """删除指定预设"""
preset_path = Path("data/preset") / f"{preset_name}.json"
if not preset_path.exists():
raise HTTPException(status_code=404, detail="Preset not found")
try: try:
preset_path.unlink() return await AIDesignSpec.delete_preset(preset_name)
return {"message": "Preset deleted successfully"} except FileNotFoundError:
except Exception as e: raise HTTPException(status_code=404, detail="Preset not found")
raise HTTPException(status_code=500, detail=f"Failed to delete preset: {str(e)}")
# ========== 预设组件路由 ========== # ========== 预设组件路由 ==========
@@ -121,144 +54,45 @@ async def delete_preset(preset_name: str):
@router.get("/{preset_name}/components") @router.get("/{preset_name}/components")
async def list_preset_components(preset_name: str): async def list_preset_components(preset_name: str):
"""获取预设中的所有组件""" """获取预设中的所有组件"""
preset_path = Path("data/preset") / f"{preset_name}.json"
if not preset_path.exists():
raise HTTPException(status_code=404, detail="Preset not found")
try: try:
with open(preset_path, 'r', encoding='utf-8') as f: return await AIDesignSpec.list_components(preset_name)
preset_data = json.load(f) except FileNotFoundError:
raise HTTPException(status_code=404, detail="Preset not found")
# 获取组件列表
components = preset_data.get("prompts", [])
return {"components": components}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to load components: {str(e)}")
@router.get("/{preset_name}/components/{component_id}") @router.get("/{preset_name}/components/{component_id}")
async def get_preset_component(preset_name: str, component_id: str): async def get_preset_component(preset_name: str, component_id: str):
"""获取指定组件的详情""" """获取指定组件的详情"""
preset_path = Path("data/preset") / f"{preset_name}.json"
if not preset_path.exists():
raise HTTPException(status_code=404, detail="Preset not found")
try: try:
with open(preset_path, 'r', encoding='utf-8') as f: return await AIDesignSpec.get_component(preset_name, component_id)
preset_data = json.load(f) except FileNotFoundError:
raise HTTPException(status_code=404, detail="Preset not found")
# 查找组件
components = preset_data.get("prompts", [])
component = next((c for c in components if c.get("identifier") == component_id), None)
if not component:
raise HTTPException(status_code=404, detail="Component not found")
return component
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to load component: {str(e)}")
@router.post("/{preset_name}/components", status_code=status.HTTP_201_CREATED) @router.post("/{preset_name}/components", status_code=status.HTTP_201_CREATED)
async def add_preset_component(preset_name: str, component_data: Dict): async def add_preset_component(preset_name: str, component_data: dict):
"""向预设添加新组件""" """向预设添加新组件"""
preset_path = Path("data/preset") / f"{preset_name}.json"
if not preset_path.exists():
raise HTTPException(status_code=404, detail="Preset not found")
try: try:
# 加载预设数据 return await AIDesignSpec.add_component_to_preset(preset_name, component_data)
with open(preset_path, 'r', encoding='utf-8') as f: except FileNotFoundError:
preset_data = json.load(f) raise HTTPException(status_code=404, detail="Preset not found")
except ValueError as e:
# 验证组件数据 raise HTTPException(status_code=400, detail=str(e))
component = PromptComponent(**component_data)
# 检查组件ID是否已存在
components = preset_data.get("prompts", [])
if any(c.get("identifier") == component.identifier for c in components):
raise HTTPException(status_code=400, detail="Component identifier already exists")
# 添加组件
components.append(component.dict())
preset_data["prompts"] = components
# 保存更新
with open(preset_path, 'w', encoding='utf-8') as f:
json.dump(preset_data, f, ensure_ascii=False, indent=2)
return {"message": "Component added successfully", "identifier": component.identifier}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to add component: {str(e)}")
@router.put("/{preset_name}/components/{component_id}") @router.put("/{preset_name}/components/{component_id}")
async def update_preset_component(preset_name: str, component_id: str, update_data: Dict): async def update_preset_component(preset_name: str, component_id: str, update_data: dict):
"""更新指定组件""" """更新指定组件"""
preset_path = Path("data/preset") / f"{preset_name}.json"
if not preset_path.exists():
raise HTTPException(status_code=404, detail="Preset not found")
try: try:
# 加载预设数据 return await AIDesignSpec.update_component_in_preset(preset_name, component_id, update_data)
with open(preset_path, 'r', encoding='utf-8') as f: except FileNotFoundError:
preset_data = json.load(f) raise HTTPException(status_code=404, detail="Preset not found")
# 查找并更新组件
components = preset_data.get("prompts", [])
component_index = next((i for i, c in enumerate(components) if c.get("identifier") == component_id), None)
if component_index is None:
raise HTTPException(status_code=404, detail="Component not found")
# 更新组件字段
for key, value in update_data.items():
components[component_index][key] = value
# 保存更新
with open(preset_path, 'w', encoding='utf-8') as f:
json.dump(preset_data, f, ensure_ascii=False, indent=2)
return {"message": "Component updated successfully"}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to update component: {str(e)}")
@router.delete("/{preset_name}/components/{component_id}") @router.delete("/{preset_name}/components/{component_id}")
async def delete_preset_component(preset_name: str, component_id: str): async def delete_preset_component(preset_name: str, component_id: str):
"""从预设中删除指定组件""" """从预设中删除指定组件"""
preset_path = Path("data/preset") / f"{preset_name}.json"
if not preset_path.exists():
raise HTTPException(status_code=404, detail="Preset not found")
try: try:
# 加载预设数据 return await AIDesignSpec.delete_component_from_preset(preset_name, component_id)
with open(preset_path, 'r', encoding='utf-8') as f: except FileNotFoundError:
preset_data = json.load(f) raise HTTPException(status_code=404, detail="Preset not found")
# 查找并删除组件
components = preset_data.get("prompts", [])
original_length = len(components)
components = [c for c in components if c.get("identifier") != component_id]
if len(components) == original_length:
raise HTTPException(status_code=404, detail="Component not found")
# 更新预设数据
preset_data["prompts"] = components
# 保存更新
with open(preset_path, 'w', encoding='utf-8') as f:
json.dump(preset_data, f, ensure_ascii=False, indent=2)
return {"message": "Component deleted successfully"}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to delete component: {str(e)}")

View File

@@ -16,7 +16,7 @@ class PromptComponent(BaseModel):
@validator('role') @validator('role')
def validate_role(cls, v): def validate_role(cls, v):
"""验证角色值是否在有效范围内""" """验证角色值是否在有效范围内"""
if v not in [0, 1, 2]: if not isinstance(v, int) or v not in [0, 1, 2]:
raise ValueError("角色值必须是0(System)、1(User)或2(Assistant)") raise ValueError("角色值必须是0(System)、1(User)或2(Assistant)")
return v return v
@@ -49,7 +49,7 @@ class PromptComponent(BaseModel):
@classmethod @classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'PromptComponent': def from_dict(cls, data: Dict[str, Any]) -> 'PromptComponent':
""" """
从字典创建组件实例 从字典创建组件实例自动处理role字段的类型转换
参数: 参数:
data: 包含组件数据的字典 data: 包含组件数据的字典
@@ -57,4 +57,9 @@ class PromptComponent(BaseModel):
返回: 返回:
PromptComponent: 组件实例 PromptComponent: 组件实例
""" """
# 处理role字段将字符串转换为整数
if 'role' in data and isinstance(data['role'], str):
role_map = {'system': 0, 'user': 1, 'assistant': 2}
data['role'] = role_map.get(data['role'].lower(), 0)
return cls(**data) return cls(**data)

View File

@@ -1,5 +1,7 @@
from pydantic import BaseModel, Field, validator from pydantic import BaseModel, Field, validator
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
from pathlib import Path
import json
from .PromptComponent import PromptComponent from .PromptComponent import PromptComponent
@@ -69,6 +71,379 @@ class AIDesignSpec(BaseModel):
raise ValueError(f"prompt_order中包含不存在的组件ID: {invalid_ids}") raise ValueError(f"prompt_order中包含不存在的组件ID: {invalid_ids}")
return v return v
@classmethod
def get_preset_dir(cls) -> Path:
"""获取预设目录路径"""
try:
from backend.core.config import settings
preset_dir = settings.DATA_PATH / "preset"
# 如果路径不存在,尝试使用相对路径
if not preset_dir.exists():
# 尝试从当前工作目录构建路径
cwd_preset_dir = Path.cwd() / "data" / "preset"
if cwd_preset_dir.exists():
return cwd_preset_dir
# 尝试从脚本所在目录构建路径
script_dir = Path(__file__).resolve().parent.parent.parent
script_preset_dir = script_dir / "data" / "preset"
if script_preset_dir.exists():
return script_preset_dir
# 如果都不存在,返回默认路径
return Path("data/preset")
return preset_dir
except ImportError:
# 如果无法导入settings尝试使用相对路径
cwd_preset_dir = Path.cwd() / "data" / "preset"
if cwd_preset_dir.exists():
return cwd_preset_dir
# 尝试从脚本所在目录构建路径
script_dir = Path(__file__).resolve().parent.parent.parent
script_preset_dir = script_dir / "data" / "preset"
if script_preset_dir.exists():
return script_preset_dir
# 如果都不存在,返回默认路径
return Path("data/preset")
@classmethod
async def list_all_presets(cls) -> Dict[str, List[Dict]]:
"""获取所有预设列表及其基本信息"""
preset_dir = cls.get_preset_dir()
if not preset_dir.exists():
return {"presets": []}
presets = []
for preset_file in preset_dir.glob("*.json"):
try:
with open(preset_file, 'r', encoding='utf-8') as f:
preset_data = json.load(f)
presets.append({
"name": preset_file.stem,
"description": preset_data.get("description", ""),
"component_count": len(preset_data.get("prompts", [])),
"temperature": preset_data.get("temperature", 1.0)
})
except Exception:
continue # 跳过损坏的预设文件
return {"presets": presets}
@classmethod
async def get_preset(cls, preset_name: str) -> Dict[str, Any]:
"""获取指定预设的完整内容"""
preset_path = cls.get_preset_dir() / f"{preset_name}.json"
if not preset_path.exists():
raise FileNotFoundError(f"Preset not found: {preset_name}")
try:
with open(preset_path, 'r', encoding='utf-8') as f:
preset_data = json.load(f)
# 处理prompt_order简化为单角色配置
if 'prompt_order' in preset_data and isinstance(preset_data['prompt_order'], list) and len(
preset_data['prompt_order']) > 0:
# 检查第一个元素是否为字典(多角色配置)
first_item = preset_data['prompt_order'][0]
if isinstance(first_item, dict) and 'order' in first_item:
# 提取第一个角色的order配置
first_role_order = first_item
if isinstance(first_role_order['order'], list):
# 简化为只包含enabled为True的identifier列表
simplified_order = [
item.get('identifier')
for item in first_role_order['order']
if item.get('enabled', True)
]
preset_data['prompt_order'] = simplified_order
# 转换为AIDesignSpec对象进行验证
ai_design_spec = cls.from_dict(preset_data)
# 构建返回数据,确保格式与前端期望的一致
result = {
# 基础参数
"temperature": ai_design_spec.temperature,
"frequency_penalty": ai_design_spec.frequency_penalty,
"presence_penalty": ai_design_spec.presence_penalty,
"top_p": ai_design_spec.top_p,
"top_k": ai_design_spec.top_k,
"max_context": ai_design_spec.max_context,
"max_tokens": ai_design_spec.max_tokens,
"max_context_unlocked": ai_design_spec.max_context_unlocked,
"stream_openai": ai_design_spec.stream,
"seed": ai_design_spec.seed,
"n": ai_design_spec.n,
# 兼容旧格式
"openai_max_context": ai_design_spec.max_context,
"openai_max_tokens": ai_design_spec.max_tokens,
# 其他参数
"top_a": ai_design_spec.top_a,
"min_p": ai_design_spec.min_p,
"repetition_penalty": ai_design_spec.repetition_penalty,
"names_behavior": ai_design_spec.names_behavior,
"send_if_empty": ai_design_spec.send_if_empty,
"impersonation_prompt": ai_design_spec.impersonation_prompt,
"new_chat_prompt": ai_design_spec.new_chat_prompt,
"new_group_chat_prompt": ai_design_spec.new_group_chat_prompt,
"new_example_chat_prompt": ai_design_spec.new_example_chat_prompt,
"continue_nudge_prompt": ai_design_spec.continue_nudge_prompt,
"bias_preset_selected": ai_design_spec.bias_preset_selected,
"wi_format": ai_design_spec.wi_format,
"scenario_format": ai_design_spec.scenario_format,
"personality_format": ai_design_spec.personality_format,
"group_nudge_prompt": ai_design_spec.group_nudge_prompt,
"assistant_prefill": ai_design_spec.assistant_prefill,
"assistant_impersonation": ai_design_spec.assistant_impersonation,
"use_sysprompt": ai_design_spec.use_sysprompt,
"squash_system_messages": ai_design_spec.squash_system_messages,
"media_inlining": ai_design_spec.media_inlining,
"continue_prefill": ai_design_spec.continue_prefill,
"continue_postfix": ai_design_spec.continue_postfix,
# 处理组件
"prompts": []
}
# 处理组件列表
if ai_design_spec.prompts:
# 获取当前角色的prompt_order简化后的字符串列表
current_order = ai_design_spec.prompt_order if ai_design_spec.prompt_order else []
# 构建组件列表
for prompt in ai_design_spec.prompts:
# 检查组件是否在order中
is_in_order = prompt.identifier in current_order
# 构建组件对象
component = {
"identifier": prompt.identifier,
"name": prompt.name,
"content": prompt.content if hasattr(prompt, 'content') else "",
"role": prompt.role if hasattr(prompt, 'role') else (0 if prompt.system_prompt else 1),
"system_prompt": prompt.system_prompt,
"marker": prompt.marker,
"enabled": is_in_order if current_order else True
}
result["prompts"].append(component)
# 按照order排序组件
if current_order:
result["prompts"].sort(
key=lambda x: current_order.index(x["identifier"]) if x[
"identifier"] in current_order else len(
current_order))
# 添加prompt_order
result["prompt_order"] = ai_design_spec.prompt_order if ai_design_spec.prompt_order else []
return result
except Exception as e:
raise Exception(f"Failed to load preset: {str(e)}")
@classmethod
async def create_preset(cls, preset_name: str, preset_data: Dict) -> Dict[str, str]:
"""创建新预设"""
preset_dir = cls.get_preset_dir()
preset_dir.mkdir(parents=True, exist_ok=True)
preset_path = preset_dir / f"{preset_name}.json"
if preset_path.exists():
raise FileExistsError(f"Preset already exists: {preset_name}")
try:
# 验证并转换为AIDesignSpec对象
ai_design_spec = cls.from_dict(preset_data)
# 保存到文件
with open(preset_path, 'w', encoding='utf-8') as f:
json.dump(ai_design_spec.dict(), f, ensure_ascii=False, indent=2)
return {"message": "Preset created successfully", "name": preset_name}
except Exception as e:
raise Exception(f"Failed to create preset: {str(e)}")
@classmethod
async def update_preset(cls, preset_name: str, update_data: Dict) -> Dict[str, str]:
"""更新预设配置"""
preset_path = cls.get_preset_dir() / f"{preset_name}.json"
if not preset_path.exists():
raise FileNotFoundError(f"Preset not found: {preset_name}")
try:
# 加载现有预设
with open(preset_path, 'r', encoding='utf-8') as f:
preset_data = json.load(f)
# 更新字段
for key, value in update_data.items():
preset_data[key] = value
# 验证并转换为AIDesignSpec对象
ai_design_spec = cls.from_dict(preset_data)
# 保存更新
with open(preset_path, 'w', encoding='utf-8') as f:
json.dump(ai_design_spec.dict(), f, ensure_ascii=False, indent=2)
return {"message": "Preset updated successfully"}
except Exception as e:
raise Exception(f"Failed to update preset: {str(e)}")
@classmethod
async def delete_preset(cls, preset_name: str) -> Dict[str, str]:
"""删除指定预设"""
preset_path = cls.get_preset_dir() / f"{preset_name}.json"
if not preset_path.exists():
raise FileNotFoundError(f"Preset not found: {preset_name}")
try:
preset_path.unlink()
return {"message": "Preset deleted successfully"}
except Exception as e:
raise Exception(f"Failed to delete preset: {str(e)}")
@classmethod
async def list_components(cls, preset_name: str) -> Dict[str, List[Dict]]:
"""获取预设中的所有组件"""
preset_path = cls.get_preset_dir() / f"{preset_name}.json"
if not preset_path.exists():
raise FileNotFoundError(f"Preset not found: {preset_name}")
try:
with open(preset_path, 'r', encoding='utf-8') as f:
preset_data = json.load(f)
# 获取组件列表
components = preset_data.get("prompts", [])
return {"components": components}
except Exception as e:
raise Exception(f"Failed to load components: {str(e)}")
@classmethod
async def get_component(cls, preset_name: str, component_id: str) -> Dict[str, Any]:
"""获取指定组件的详情"""
preset_path = cls.get_preset_dir() / f"{preset_name}.json"
if not preset_path.exists():
raise FileNotFoundError(f"Preset not found: {preset_name}")
try:
with open(preset_path, 'r', encoding='utf-8') as f:
preset_data = json.load(f)
# 查找组件
components = preset_data.get("prompts", [])
component = next((c for c in components if c.get("identifier") == component_id), None)
if not component:
raise FileNotFoundError(f"Component not found: {component_id}")
return component
except FileNotFoundError:
raise
except Exception as e:
raise Exception(f"Failed to load component: {str(e)}")
@classmethod
async def add_component_to_preset(cls, preset_name: str, component_data: Dict) -> Dict[str, str]:
"""向预设添加新组件"""
preset_path = cls.get_preset_dir() / f"{preset_name}.json"
if not preset_path.exists():
raise FileNotFoundError(f"Preset not found: {preset_name}")
try:
# 加载预设数据
with open(preset_path, 'r', encoding='utf-8') as f:
preset_data = json.load(f)
# 验证组件数据
component = PromptComponent(**component_data)
# 检查组件ID是否已存在
components = preset_data.get("prompts", [])
if any(c.get("identifier") == component.identifier for c in components):
raise ValueError(f"Component identifier already exists: {component.identifier}")
# 添加组件
components.append(component.dict())
preset_data["prompts"] = components
# 保存更新
with open(preset_path, 'w', encoding='utf-8') as f:
json.dump(preset_data, f, ensure_ascii=False, indent=2)
return {"message": "Component added successfully", "identifier": component.identifier}
except (FileNotFoundError, ValueError):
raise
except Exception as e:
raise Exception(f"Failed to add component: {str(e)}")
@classmethod
async def update_component_in_preset(cls, preset_name: str, component_id: str, update_data: Dict) -> Dict[str, str]:
"""更新指定组件"""
preset_path = cls.get_preset_dir() / f"{preset_name}.json"
if not preset_path.exists():
raise FileNotFoundError(f"Preset not found: {preset_name}")
try:
# 加载预设数据
with open(preset_path, 'r', encoding='utf-8') as f:
preset_data = json.load(f)
# 查找并更新组件
components = preset_data.get("prompts", [])
component_index = next((i for i, c in enumerate(components) if c.get("identifier") == component_id), None)
if component_index is None:
raise FileNotFoundError(f"Component not found: {component_id}")
# 更新组件字段
for key, value in update_data.items():
components[component_index][key] = value
# 保存更新
with open(preset_path, 'w', encoding='utf-8') as f:
json.dump(preset_data, f, ensure_ascii=False, indent=2)
return {"message": "Component updated successfully"}
except FileNotFoundError:
raise
except Exception as e:
raise Exception(f"Failed to update component: {str(e)}")
@classmethod
async def delete_component_from_preset(cls, preset_name: str, component_id: str) -> Dict[str, str]:
"""从预设中删除指定组件"""
preset_path = cls.get_preset_dir() / f"{preset_name}.json"
if not preset_path.exists():
raise FileNotFoundError(f"Preset not found: {preset_name}")
try:
# 加载预设数据
with open(preset_path, 'r', encoding='utf-8') as f:
preset_data = json.load(f)
# 查找并删除组件
components = preset_data.get("prompts", [])
original_length = len(components)
components = [c for c in components if c.get("identifier") != component_id]
if len(components) == original_length:
raise FileNotFoundError(f"Component not found: {component_id}")
# 更新预设数据
preset_data["prompts"] = components
# 保存更新
with open(preset_path, 'w', encoding='utf-8') as f:
json.dump(preset_data, f, ensure_ascii=False, indent=2)
return {"message": "Component deleted successfully"}
except FileNotFoundError:
raise
except Exception as e:
raise Exception(f"Failed to delete component: {str(e)}")
# ========== 组件管理方法 ========== # ========== 组件管理方法 ==========
def add_component(self, component: PromptComponent) -> None: def add_component(self, component: PromptComponent) -> None:

View File

@@ -88,7 +88,175 @@ class ChatHistory(BaseModel):
class Config: class Config:
arbitrary_types_allowed = True arbitrary_types_allowed = True
@classmethod # 类方法装饰器,表示这是一个类方法,可以通过类名直接调用 @classmethod
def get_data_path(cls) -> Path:
"""获取数据目录路径"""
try:
from backend.core.config import settings
return settings.DATA_PATH / "chat"
except ImportError:
return Path("data")
@classmethod
async def list_all_chats(cls) -> Dict[str, List[Dict]]:
"""获取所有角色的所有聊天列表"""
data_dir = cls.get_data_path()
if not data_dir.exists():
return {"chats": []}
chats = []
for role_dir in data_dir.iterdir():
if role_dir.is_dir():
for chat_file in role_dir.glob("*.jsonl"):
try:
with open(chat_file, 'r', encoding='utf-8') as f:
# 读取第一行获取元数据
first_line = f.readline()
metadata = json.loads(first_line)
chats.append({
"role_name": role_dir.name,
"chat_name": chat_file.stem,
"user_name": metadata.get("user_name", "User"),
"character_name": metadata.get("character_name", "Assistant"),
"last_modified": metadata.get("last_modified", ""),
"message_count": sum(1 for _ in f) # 统计剩余行数(消息数)
})
except Exception:
continue # 跳过损坏的聊天文件
return {"chats": chats}
@classmethod
async def get_chat(cls, role_name: str, chat_name: str) -> Dict[str, Any]:
"""获取指定聊天的完整内容"""
chat_history = cls.load_from_file(role_name, chat_name)
return {
"metadata": chat_history.chat_metadata.dict(),
"messages": chat_history.to_chatbox_format()
}
@classmethod
async def create_chat(cls, role_name: str, chat_name: str, metadata: Optional[Dict] = None) -> Dict[str, str]:
"""创建新聊天"""
base_path = cls.get_data_path()
role_dir = base_path / role_name
role_dir.mkdir(parents=True, exist_ok=True)
chat_path = role_dir / f"{chat_name}.jsonl"
if chat_path.exists():
raise FileExistsError(f"Chat already exists: {chat_path}")
# 创建聊天历史对象
chat_history = cls(
chat_metadata=ChatMetadata(**(metadata or {})),
messages=[]
)
# 保存到文件
chat_history.save_to_file(role_name, chat_name, base_path)
return {"message": "Chat created successfully"}
@classmethod
async def update_chat(cls, role_name: str, chat_name: str, update_data: Dict) -> Dict[str, str]:
"""更新聊天元数据"""
chat_history = cls.load_from_file(role_name, chat_name)
# 更新元数据
if "metadata" in update_data:
for key, value in update_data["metadata"].items():
if hasattr(chat_history.chat_metadata, key):
setattr(chat_history.chat_metadata, key, value)
# 保存更改
base_path = cls.get_data_path()
chat_history.save_to_file(role_name, chat_name, base_path)
return {"message": "Chat metadata updated successfully"}
@classmethod
async def delete_chat(cls, role_name: str, chat_name: str) -> Dict[str, str]:
"""删除指定聊天"""
base_path = cls.get_data_path()
chat_path = base_path / role_name / f"{chat_name}.jsonl"
if not chat_path.exists():
raise FileNotFoundError(f"Chat not found: {chat_path}")
chat_path.unlink()
return {"message": "Chat deleted successfully"}
@classmethod
async def list_messages(cls, role_name: str, chat_name: str) -> Dict[str, List[Dict]]:
"""获取聊天的所有消息"""
chat_history = cls.load_from_file(role_name, chat_name)
return {"messages": chat_history.to_chatbox_format()}
@classmethod
async def get_message(cls, role_name: str, chat_name: str, floor: int) -> Dict[str, Any]:
"""获取指定楼层的消息"""
chat_history = cls.load_from_file(role_name, chat_name)
message = next((msg for msg in chat_history.messages if msg.floor == floor), None)
if not message:
raise FileNotFoundError(f"Message not found: floor {floor}")
return message.dict()
@classmethod
async def add_message(cls, role_name: str, chat_name: str, message_data: Dict) -> Dict[str, Any]:
"""向聊天添加新消息"""
chat_history = cls.load_from_file(role_name, chat_name)
# 创建消息对象
message = Message(**message_data)
# 检查楼层是否已存在
if any(msg.floor == message.floor for msg in chat_history.messages):
raise ValueError(f"Message floor already exists: {message.floor}")
# 添加消息
chat_history.messages.append(message)
# 保存更改
base_path = cls.get_data_path()
chat_history.save_to_file(role_name, chat_name, base_path)
return {"message": "Message added successfully", "floor": message.floor}
@classmethod
async def update_message(cls, role_name: str, chat_name: str, floor: int, update_data: Dict) -> Dict[str, str]:
"""更新指定楼层的消息"""
chat_history = cls.load_from_file(role_name, chat_name)
message = next((msg for msg in chat_history.messages if msg.floor == floor), None)
if not message:
raise FileNotFoundError(f"Message not found: floor {floor}")
# 更新消息字段
for key, value in update_data.items():
if hasattr(message, key):
setattr(message, key, value)
# 保存更改
base_path = cls.get_data_path()
chat_history.save_to_file(role_name, chat_name, base_path)
return {"message": "Message updated successfully"}
@classmethod
async def delete_message(cls, role_name: str, chat_name: str, floor: int) -> Dict[str, str]:
"""删除指定楼层的消息"""
chat_history = cls.load_from_file(role_name, chat_name)
# 查找并删除消息
original_length = len(chat_history.messages)
chat_history.messages = [msg for msg in chat_history.messages if msg.floor != floor]
if len(chat_history.messages) == original_length:
raise FileNotFoundError(f"Message not found: floor {floor}")
# 保存更改
base_path = cls.get_data_path()
chat_history.save_to_file(role_name, chat_name, base_path)
return {"message": "Message deleted successfully"}
@classmethod
def load_from_file(cls, role_name: str, chat_name: str, base_path: Path = None) -> 'ChatHistory': def load_from_file(cls, role_name: str, chat_name: str, base_path: Path = None) -> 'ChatHistory':
""" """
从JSONL文件加载聊天历史 从JSONL文件加载聊天历史
@@ -105,10 +273,9 @@ class ChatHistory(BaseModel):
FileNotFoundError: 当文件不存在时抛出 FileNotFoundError: 当文件不存在时抛出
json.JSONDecodeError: 当JSON解析失败时抛出 json.JSONDecodeError: 当JSON解析失败时抛出
""" """
# 设置默认基础路径 - 如果未提供base_path则从配置中获取默认路径 # 设置默认基础路径
if base_path is None: if base_path is None:
from backend.core.config import settings # 延迟导入配置模块 base_path = cls.get_data_path()
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"
@@ -258,7 +425,7 @@ class ChatHistory(BaseModel):
""" """
# 设置默认基础路径 # 设置默认基础路径
if base_path is None: if base_path is None:
base_path = Path("data") base_path = self.get_data_path()
# 构建文件路径 # 构建文件路径
file_path = base_path / role_name / f"{chat_name}.jsonl" file_path = base_path / role_name / f"{chat_name}.jsonl"

View File

@@ -3,46 +3,46 @@ import { subscribeWithSelector } from 'zustand/middleware';
const useChatBoxStore = create( const useChatBoxStore = create(
subscribeWithSelector((set, get) => ({ subscribeWithSelector((set, get) => ({
// 聊天历史消息列表 // 聊天历史消息列表
messages: [], messages: [],
// 用户名称 // 用户名称
userName: '', userName: '',
// 角色名称 // 角色名称
characterName: '', characterName: '',
// 当前选中的角色 // 当前选中的角色
currentRole: null, currentRole: null,
// 当前选中的聊天 // 当前选中的聊天
currentChat: null, currentChat: null,
// 是否正在加载 // 是否正在加载
isLoading: false, isLoading: false,
// 是否正在生成 // 是否正在生成
isGenerating: false, isGenerating: false,
// 错误信息 // 错误信息
error: null, error: null,
// 设置消息列表 // 设置消息列表
setMessages: (messages) => set({ messages }), setMessages: (messages) => set({ messages }),
// 设置用户名称 // 设置用户名称
setUserName: (userName) => set({ userName }), setUserName: (userName) => set({ userName }),
// 设置角色名称 // 设置角色名称
setCharacterName: (characterName) => set({ characterName }), setCharacterName: (characterName) => set({ characterName }),
// 设置当前角色 // 设置当前角色
setCurrentRole: (role) => set({ currentRole: role }), setCurrentRole: (role) => set({ currentRole: role }),
// 设置当前聊天 // 设置当前聊天
setCurrentChat: (chat) => set({ currentChat: chat }), setCurrentChat: (chat) => set({ currentChat: chat }),
// 同时设置角色和聊天 // 同时设置角色和聊天
setChatBoxRoleAndChat: (role, chat) => { setChatBoxRoleAndChat: (role, chat) => {
console.log('[ChatBoxStore] setChatBoxRoleAndChat called with:', { role, chat }); console.log('[ChatBoxStore] setChatBoxRoleAndChat called with:', { role, chat });
set({ set({
@@ -51,99 +51,207 @@ const useChatBoxStore = create(
}); });
}, },
// 设置生成状态
setIsGenerating: (status) => set({ isGenerating: status }),
// 设置生成状态 // 发送消息
setIsGenerating: (status) => set({ isGenerating: status }), sendMessage: async (content) => {
const { messages, userName, characterName, currentRole, currentChat } = get();
sendMessage: async (content) => { set({
const { messages, userName, characterName, currentRole, currentChat } = get(); isGenerating: true,
messages: [...messages, {
id: Date.now(),
floor: messages.length + 1,
mes: content,
is_user: true
}]
});
set({ try {
isGenerating: true, const response = await fetch(`/api/chats/${encodeURIComponent(currentRole)}/${encodeURIComponent(currentChat)}/messages`, {
messages: [...messages, { method: 'POST',
id: Date.now(), headers: {
floor: messages.length + 1, 'Content-Type': 'application/json',
mes: content, },
is_user: true body: JSON.stringify({
}] floor: messages.length + 1,
}); mes: content,
is_user: true
})
});
try { if (!response.ok) {
const response = await fetch('/api/chat_box/send_message', { throw new Error('Failed to send message');
method: 'POST', }
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
role_name: currentRole,
chat_name: currentChat,
message: content
})
});
if (!response.ok) { const data = await response.json();
throw new Error('Failed to send message'); set((state) => ({
} messages: [...state.messages, {
id: Date.now(),
floor: state.messages.length + 1,
mes: data.response,
is_user: false
}],
isGenerating: false
}));
} catch (error) {
set({
error: error.message,
isGenerating: false
});
}
},
const data = await response.json(); // 终止生成
set((state) => ({ stopGeneration: () => set({ isGenerating: false }),
messages: [...state.messages, {
id: Date.now(),
floor: state.messages.length + 1,
mes: data.response,
is_user: false
}],
isGenerating: false
}));
} catch (error) {
set({
error: error.message,
isGenerating: false
});
}
},
// 终止生成 // 加载聊天历史
stopGeneration: () => set({ isGenerating: false }), fetchChatHistory: async (roleName, chatName) => {
// 加载聊天历史 set({ isLoading: true, error: null });
fetchChatHistory: async (roleName, chatName) => { try {
set({ isLoading: true, error: null }); const response = await fetch(`/api/chats/${encodeURIComponent(roleName)}/${encodeURIComponent(chatName)}`);
try { if (!response.ok) {
const response = await fetch(`/api/chat_box/get_chat_history?role_name=${encodeURIComponent(roleName)}&chat_name=${encodeURIComponent(chatName)}`); throw new Error('Failed to fetch chat history');
if (!response.ok) { }
throw new Error('Failed to fetch chat history'); const data = await response.json();
}
const data = await response.json();
// 修改数据处理逻辑适配API返回的数据结构
set({
messages: data || [], // 直接使用返回的数组
userName: 'User', // 固定用户名
characterName: roleName || 'Assistant', // 使用角色名作为角色名称
isLoading: false
});
} catch (error) {
set({
error: error.message,
isLoading: false
});
}
},
set({
messages: data.messages || [],
userName: data.metadata?.user_name || 'User',
characterName: data.metadata?.character_name || roleName || 'Assistant',
isLoading: false
});
} catch (error) {
set({
error: error.message,
isLoading: false
});
}
},
// 清空聊天历史 // 清空聊天历史
clearChatHistory: () => set({ clearChatHistory: () => set({
messages: [], messages: [],
userName: '', userName: '',
characterName: '', characterName: '',
error: null error: null
}), }),
// 更新特定消息的内容 // 更新特定消息的内容
updateMessage: (id, content) => set((state) => ({ updateMessage: async (floor, content) => {
messages: state.messages.map((msg) => const { currentRole, currentChat } = get();
msg.id === id ? { ...msg, content } : msg try {
) const response = await fetch(`/api/chats/${encodeURIComponent(currentRole)}/${encodeURIComponent(currentChat)}/messages/${floor}`, {
})), method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ mes: content })
});
if (!response.ok) {
throw new Error('Failed to update message');
}
set((state) => ({
messages: state.messages.map((msg) =>
msg.floor === floor ? { ...msg, mes: content } : msg
)
}));
} catch (error) {
set({ error: error.message });
}
},
// 删除特定消息
deleteMessage: async (floor) => {
const { currentRole, currentChat } = get();
try {
const response = await fetch(`/api/chats/${encodeURIComponent(currentRole)}/${encodeURIComponent(currentChat)}/messages/${floor}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error('Failed to delete message');
}
set((state) => ({
messages: state.messages.filter((msg) => msg.floor !== floor)
}));
} catch (error) {
set({ error: error.message });
}
},
// 创建新聊天
createChat: async (roleName, chatName, metadata = {}) => {
try {
const response = await fetch(`/api/chats/${encodeURIComponent(roleName)}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
chat_name: chatName,
metadata: {
user_name: 'User',
character_name: roleName,
...metadata
}
})
});
if (!response.ok) {
throw new Error('Failed to create chat');
}
return await response.json();
} catch (error) {
set({ error: error.message });
throw error;
}
},
// 更新聊天元数据
updateChatMetadata: async (roleName, chatName, metadata) => {
try {
const response = await fetch(`/api/chats/${encodeURIComponent(roleName)}/${encodeURIComponent(chatName)}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ metadata })
});
if (!response.ok) {
throw new Error('Failed to update chat metadata');
}
return await response.json();
} catch (error) {
set({ error: error.message });
throw error;
}
},
// 删除聊天
deleteChat: async (roleName, chatName) => {
try {
const response = await fetch(`/api/chats/${encodeURIComponent(roleName)}/${encodeURIComponent(chatName)}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error('Failed to delete chat');
}
return await response.json();
} catch (error) {
set({ error: error.message });
throw error;
}
}
})) }))
); );
@@ -151,20 +259,17 @@ const useChatBoxStore = create(
useChatBoxStore.subscribe( useChatBoxStore.subscribe(
(state) => ({ role: state.currentRole, chat: state.currentChat }), (state) => ({ role: state.currentRole, chat: state.currentChat }),
({ role, chat }, prev) => { ({ role, chat }, prev) => {
// 只有当角色或聊天发生变化时才处理 // 只有当角色或聊天发生变化时才处理
if (role !== prev.role || chat !== prev.chat) { if (role !== prev.role || chat !== prev.chat) {
// 确保角色和聊天都存在且不为null // 确保角色和聊天都存在且不为null
if (role && chat) { if (role && chat) {
useChatBoxStore.getState().fetchChatHistory(role, chat); useChatBoxStore.getState().fetchChatHistory(role, chat);
} else { } else {
useChatBoxStore.getState().clearChatHistory(); useChatBoxStore.getState().clearChatHistory();
} }
} }
}, },
{ equalityFn: (a, b) => a.role === b.role && a.chat === b.chat } { equalityFn: (a, b) => a.role === b.role && a.chat === b.chat }
); );
export default useChatBoxStore; export default useChatBoxStore;

View File

@@ -100,14 +100,16 @@ const usePresetStore = create((set, get) => ({
fetchPresets: async () => { fetchPresets: async () => {
set({ isLoadingPresets: true }); set({ isLoadingPresets: true });
try { try {
const response = await fetch('/api/presets/list'); const response = await fetch('/api/presets');
const data = await response.json(); const data = await response.json();
// 转换为预设对象数组 // 转换为预设对象数组
const presetList = data.presets.map(name => ({ const presetList = data.presets.map(preset => ({
id: name, id: preset.name,
name, name: preset.name,
parameters: {} // 参数将在选择预设时加载 description: preset.description,
component_count: preset.component_count,
temperature: preset.temperature
})); }));
set({ presets: presetList, isLoadingPresets: false }); set({ presets: presetList, isLoadingPresets: false });
@@ -150,14 +152,19 @@ const usePresetStore = create((set, get) => ({
// 处理预设组件 // 处理预设组件
let components = []; let components = [];
if (presetData.prompts && Array.isArray(presetData.prompts)) { if (presetData.prompts && Array.isArray(presetData.prompts)) {
// 获取当前角色的prompt_order // 获取当前角色的prompt_order,添加更严格的检查
const currentOrder = presetData.prompt_order && presetData.prompt_order.length > 0 const currentOrder = (presetData.prompt_order &&
Array.isArray(presetData.prompt_order) &&
presetData.prompt_order.length > 0 &&
presetData.prompt_order[0] &&
presetData.prompt_order[0].order &&
Array.isArray(presetData.prompt_order[0].order))
? presetData.prompt_order[0].order ? presetData.prompt_order[0].order
: []; : [];
// 根据prompt_order排序组件 // 根据prompt_order排序组件
components = presetData.prompts.map(prompt => { components = presetData.prompts.map(prompt => {
const orderItem = currentOrder.find(item => item.identifier === prompt.identifier); const orderItem = currentOrder.find(item => item && item.identifier === prompt.identifier);
return { return {
...prompt, ...prompt,
enabled: orderItem ? orderItem.enabled : true, enabled: orderItem ? orderItem.enabled : true,
@@ -168,13 +175,14 @@ const usePresetStore = create((set, get) => ({
// 如果有prompt_order按照它排序 // 如果有prompt_order按照它排序
if (currentOrder.length > 0) { if (currentOrder.length > 0) {
components.sort((a, b) => { components.sort((a, b) => {
const indexA = currentOrder.findIndex(item => item.identifier === a.identifier); const indexA = currentOrder.findIndex(item => item && item.identifier === a.identifier);
const indexB = currentOrder.findIndex(item => item.identifier === b.identifier); const indexB = currentOrder.findIndex(item => item && item.identifier === b.identifier);
return indexA - indexB; return indexA - indexB;
}); });
} }
} }
// 更新状态,确保参数容器展开 // 更新状态,确保参数容器展开
set({ set({
selectedPreset: presetId, selectedPreset: presetId,
@@ -187,8 +195,6 @@ const usePresetStore = create((set, get) => ({
} }
}, },
// 更新参数 // 更新参数
updateParameter: ({ name, value }) => set((state) => ({ updateParameter: ({ name, value }) => set((state) => ({
parameters: { ...state.parameters, [name]: value } parameters: { ...state.parameters, [name]: value }
@@ -200,25 +206,97 @@ const usePresetStore = create((set, get) => ({
})), })),
// 保存当前设置为预设 // 保存当前设置为预设
saveCurrentAsPreset: ({ name }) => set((state) => { saveCurrentAsPreset: async ({ name }) => {
const newPreset = { const state = get();
id: `preset_${Date.now()}`, try {
name, // 构建预设数据
parameters: { ...state.parameters }, const presetData = {
promptComponents: [...state.promptComponents] ...state.parameters,
}; prompts: state.promptComponents.map(component => ({
return { identifier: component.identifier,
presets: [...state.presets, newPreset], name: component.name,
selectedPreset: newPreset.id content: component.content || '',
}; role: component.role,
}), system_prompt: component.system_prompt,
marker: component.marker
})),
prompt_order: [{
order: state.promptComponents.map(component => ({
identifier: component.identifier,
enabled: component.enabled !== false
}))
}]
};
// 发送到后端
const response = await fetch('/api/presets', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
preset_name: name,
...presetData
})
});
if (!response.ok) {
throw new Error('Failed to save preset');
}
const result = await response.json();
// 添加到本地预设列表
const newPreset = {
id: name,
name,
description: '',
component_count: state.promptComponents.length,
temperature: state.parameters.temperature
};
set((state) => ({
presets: [...state.presets, newPreset],
selectedPreset: name
}));
return result;
} catch (error) {
console.error('Failed to save preset:', error);
throw error;
}
},
// 编辑预设名称 // 编辑预设名称
editPresetName: (presetId, newName) => set((state) => ({ editPresetName: async (presetId, newName) => {
presets: state.presets.map(preset => try {
preset.id === presetId ? { ...preset, name: newName } : preset const response = await fetch(`/api/presets/${presetId}`, {
) method: 'PUT',
})), headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: newName
})
});
if (!response.ok) {
throw new Error('Failed to update preset name');
}
// 更新本地状态
set((state) => ({
presets: state.presets.map(preset =>
preset.id === presetId ? { ...preset, name: newName } : preset
)
}));
return await response.json();
} catch (error) {
console.error('Failed to update preset name:', error);
throw error;
}
},
// 切换参数设置折叠状态 // 切换参数设置折叠状态
toggleParametersExpanded: () => set((state) => ({ toggleParametersExpanded: () => set((state) => ({

View File

@@ -3,7 +3,7 @@ import { create } from 'zustand';
// 异步获取角色数据 // 异步获取角色数据
const fetchRoleData = async () => { const fetchRoleData = async () => {
try { try {
const response = await fetch('/api/tool_bar/get_all_role_and_chat', { const response = await fetch('/api/chats', {
method: 'GET', method: 'GET',
headers: { headers: {
'Cache-Control': 'no-cache', 'Cache-Control': 'no-cache',
@@ -12,7 +12,19 @@ const fetchRoleData = async () => {
} }
}); });
const data = await response.json(); const data = await response.json();
return data;
// 转换数据格式以适应前端需求
const roleData = {};
if (data.chats && Array.isArray(data.chats)) {
data.chats.forEach(chat => {
if (!roleData[chat.role_name]) {
roleData[chat.role_name] = [];
}
roleData[chat.role_name].push(chat.chat_name);
});
}
return roleData;
} catch (error) { } catch (error) {
console.error('获取角色数据失败:', error); console.error('获取角色数据失败:', error);
throw error; throw error;
@@ -66,52 +78,92 @@ const useRoleSelectorStore = create((set, get) => ({
// 同时更新角色和聊天 // 同时更新角色和聊天
setSelectedRoleAndChat: (role, chat) => set({ selectedRole: role, selectedChat: chat }), setSelectedRoleAndChat: (role, chat) => set({ selectedRole: role, selectedChat: chat }),
handleRenameRole: (oldName, newName) => { // 处理角色重命名
handleRenameRole: async (oldName, newName) => {
const { roleData, selectedRole } = get(); const { roleData, selectedRole } = get();
if (newName && newName !== oldName) { if (newName && newName !== oldName) {
const newRoleData = { ...roleData }; try {
const chats = newRoleData[oldName]; // 获取该角色下的所有聊天
delete newRoleData[oldName]; const chats = roleData[oldName] || [];
newRoleData[newName] = chats;
if (selectedRole === oldName) { // 为每个聊天更新元数据中的角色名称
set({ for (const chatName of chats) {
roleData: newRoleData, await fetch(`/api/chats/${encodeURIComponent(oldName)}/${encodeURIComponent(chatName)}`, {
selectedRole: newName, method: 'PUT',
editingRole: null headers: {
}); 'Content-Type': 'application/json',
} else { },
set({ body: JSON.stringify({
roleData: newRoleData, metadata: { character_name: newName }
editingRole: null })
}); });
}
// 更新本地状态
const newRoleData = { ...roleData };
newRoleData[newName] = chats;
delete newRoleData[oldName];
if (selectedRole === oldName) {
set({
roleData: newRoleData,
selectedRole: newName,
editingRole: null
});
} else {
set({
roleData: newRoleData,
editingRole: null
});
}
} catch (error) {
console.error('重命名角色失败:', error);
set({ editingRole: null });
} }
} else { } else {
set({ editingRole: null }); set({ editingRole: null });
} }
}, },
handleRenameChat: (oldName, newName) => { // 处理聊天重命名
handleRenameChat: async (oldName, newName) => {
const { roleData, selectedRole, selectedChat } = get(); const { roleData, selectedRole, selectedChat } = get();
if (newName && newName !== oldName) { if (newName && newName !== oldName) {
const newRoleData = { ...roleData }; try {
const chatIndex = newRoleData[selectedRole].indexOf(oldName); // 更新后端聊天名称
if (chatIndex !== -1) { await fetch(`/api/chats/${encodeURIComponent(selectedRole)}/${encodeURIComponent(oldName)}`, {
newRoleData[selectedRole][chatIndex] = newName; method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
metadata: { chat_name: newName }
})
});
if (selectedChat === oldName) { // 更新本地状态
set({ const newRoleData = { ...roleData };
roleData: newRoleData, const chatIndex = newRoleData[selectedRole].indexOf(oldName);
selectedChat: newName, if (chatIndex !== -1) {
editingChat: null newRoleData[selectedRole][chatIndex] = newName;
});
if (selectedChat === oldName) {
set({
roleData: newRoleData,
selectedChat: newName,
editingChat: null
});
} else {
set({
roleData: newRoleData,
editingChat: null
});
}
} else { } else {
set({ set({ editingChat: null });
roleData: newRoleData,
editingChat: null
});
} }
} else { } catch (error) {
console.error('重命名聊天失败:', error);
set({ editingChat: null }); set({ editingChat: null });
} }
} else { } else {
@@ -119,34 +171,27 @@ const useRoleSelectorStore = create((set, get) => ({
} }
}, },
confirmDelete: () => { // 确认删除
confirmDelete: async () => {
const { roleData, selectedRole, selectedChat, showDeleteConfirm, deleteType } = get(); const { roleData, selectedRole, selectedChat, showDeleteConfirm, deleteType } = get();
const newRoleData = { ...roleData };
if (deleteType === 'role') { try {
delete newRoleData[showDeleteConfirm]; if (deleteType === 'role') {
if (selectedRole === showDeleteConfirm) { // 删除角色下的所有聊天
set({ const chats = roleData[showDeleteConfirm] || [];
roleData: newRoleData, for (const chatName of chats) {
selectedRole: null, await fetch(`/api/chats/${encodeURIComponent(showDeleteConfirm)}/${encodeURIComponent(chatName)}`, {
selectedChat: null, method: 'DELETE'
showDeleteConfirm: null, });
deleteType: null }
});
} else { const newRoleData = { ...roleData };
set({ delete newRoleData[showDeleteConfirm];
roleData: newRoleData,
showDeleteConfirm: null, if (selectedRole === showDeleteConfirm) {
deleteType: null
});
}
} else if (deleteType === 'chat') {
const chatIndex = newRoleData[selectedRole].indexOf(showDeleteConfirm);
if (chatIndex !== -1) {
newRoleData[selectedRole].splice(chatIndex, 1);
if (selectedChat === showDeleteConfirm) {
set({ set({
roleData: newRoleData, roleData: newRoleData,
selectedRole: null,
selectedChat: null, selectedChat: null,
showDeleteConfirm: null, showDeleteConfirm: null,
deleteType: null deleteType: null
@@ -158,41 +203,116 @@ const useRoleSelectorStore = create((set, get) => ({
deleteType: null deleteType: null
}); });
} }
} else { } else if (deleteType === 'chat') {
set({ // 删除单个聊天
showDeleteConfirm: null, await fetch(`/api/chats/${encodeURIComponent(selectedRole)}/${encodeURIComponent(showDeleteConfirm)}`, {
deleteType: null method: 'DELETE'
}); });
const newRoleData = { ...roleData };
const chatIndex = newRoleData[selectedRole].indexOf(showDeleteConfirm);
if (chatIndex !== -1) {
newRoleData[selectedRole].splice(chatIndex, 1);
if (selectedChat === showDeleteConfirm) {
set({
roleData: newRoleData,
selectedChat: null,
showDeleteConfirm: null,
deleteType: null
});
} else {
set({
roleData: newRoleData,
showDeleteConfirm: null,
deleteType: null
});
}
} else {
set({
showDeleteConfirm: null,
deleteType: null
});
}
} }
} catch (error) {
console.error('删除失败:', error);
set({
showDeleteConfirm: null,
deleteType: null
});
} }
}, },
// 取消删除
cancelDelete: () => set({ showDeleteConfirm: null, deleteType: null }), cancelDelete: () => set({ showDeleteConfirm: null, deleteType: null }),
handleAddRole: () => { // 添加新角色
handleAddRole: async () => {
const { roleData } = get(); const { roleData } = get();
const newRole = '新角色'; const newRole = '新角色';
const newRoleData = { ...roleData };
newRoleData[newRole] = []; try {
set({ // 创建新角色(通过创建一个默认聊天)
roleData: newRoleData, await fetch(`/api/chats/${encodeURIComponent(newRole)}`, {
editingRole: newRole method: 'POST',
}); headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
chat_name: '默认聊天',
metadata: {
user_name: 'User',
character_name: newRole
}
})
});
const newRoleData = { ...roleData };
newRoleData[newRole] = ['默认聊天'];
set({
roleData: newRoleData,
editingRole: newRole
});
} catch (error) {
console.error('添加角色失败:', error);
}
}, },
handleAddChat: () => { // 添加新聊天
handleAddChat: async () => {
const { roleData, selectedRole } = get(); const { roleData, selectedRole } = get();
if (!selectedRole) return; if (!selectedRole) return;
const newChat = '新聊天'; const newChat = '新聊天';
const newRoleData = { ...roleData };
newRoleData[selectedRole].push(newChat); try {
set({ await fetch(`/api/chats/${encodeURIComponent(selectedRole)}`, {
roleData: newRoleData, method: 'POST',
editingChat: newChat headers: {
}); 'Content-Type': 'application/json',
},
body: JSON.stringify({
chat_name: newChat,
metadata: {
user_name: 'User',
character_name: selectedRole
}
})
});
const newRoleData = { ...roleData };
newRoleData[selectedRole].push(newChat);
set({
roleData: newRoleData,
editingChat: newChat
});
} catch (error) {
console.error('添加聊天失败:', error);
}
}, },
// 重置面板状态
resetPanel: () => set({ resetPanel: () => set({
hoveredRole: null, hoveredRole: null,
clickedRole: null, clickedRole: null,

View File

@@ -37,7 +37,7 @@ const PresetPanel = () => {
// 组件编辑状态 // 组件编辑状态
const [editingComponentIndex, setEditingComponentIndex] = useState(-1); const [editingComponentIndex, setEditingComponentIndex] = useState(-1);
const [editComponentContent, setEditComponentContent] = useState(''); const [editComponentContent, setEditComponentContent] = useState('');
const [isEditing, setIsEditing] = useState(false); // 添加编辑状态标志 const [isEditing, setIsEditing] = useState(false);
// 拖拽状态 // 拖拽状态
const [draggedItem, setDraggedItem] = useState(null); const [draggedItem, setDraggedItem] = useState(null);
@@ -75,7 +75,6 @@ const PresetPanel = () => {
// 处理参数更新 // 处理参数更新
const handleParameterChange = (name, value) => { const handleParameterChange = (name, value) => {
// 根据参数类型转换值
let convertedValue = value; let convertedValue = value;
if (name === 'temperature' || name === 'frequency_penalty' || name === 'presence_penalty' || name === 'top_p') { if (name === 'temperature' || name === 'frequency_penalty' || name === 'presence_penalty' || name === 'top_p') {
convertedValue = parseFloat(value); convertedValue = parseFloat(value);
@@ -93,30 +92,44 @@ const PresetPanel = () => {
}, [fetchPresets]); }, [fetchPresets]);
// 保存当前设置为预设 // 保存当前设置为预设
const handleSavePreset = () => { const handleSavePreset = async () => {
if (newPresetName.trim()) { if (newPresetName.trim()) {
saveCurrentAsPreset({ name: newPresetName }); try {
setNewPresetName(''); await saveCurrentAsPreset({ name: newPresetName });
setShowSaveDialog(false); setNewPresetName('');
setShowSaveDialog(false);
// 重新加载预设列表
fetchPresets();
} catch (error) {
console.error('保存预设失败:', error);
alert('保存预设失败: ' + error.message);
}
} }
}; };
// 编辑预设名称 // 编辑预设名称
const handleEditPreset = () => { const handleEditPreset = async () => {
if (editPresetId && editPresetName.trim()) { if (editPresetId && editPresetName.trim()) {
updatePresetName(editPresetId, editPresetName); try {
setEditPresetId(''); await updatePresetName(editPresetId, editPresetName);
setEditPresetName(''); setEditPresetId('');
setShowEditDialog(false); setEditPresetName('');
setShowEditDialog(false);
// 重新加载预设列表
fetchPresets();
} catch (error) {
console.error('编辑预设名称失败:', error);
alert('编辑预设名称失败: ' + error.message);
}
} }
}; };
// 导入预设 // 导入预设
const handleImportPreset = () => { const handleImportPreset = async () => {
try { try {
const importedPreset = JSON.parse(importPresetData); const importedPreset = JSON.parse(importPresetData);
if (importedPreset.name && importedPreset.parameters) { if (importedPreset.name && importedPreset.parameters) {
saveCurrentAsPreset({ name: importedPreset.name }); await saveCurrentAsPreset({ name: importedPreset.name });
// 更新参数 // 更新参数
Object.keys(importedPreset.parameters).forEach(key => { Object.keys(importedPreset.parameters).forEach(key => {
updateParameter({ name: key, value: importedPreset.parameters[key] }); updateParameter({ name: key, value: importedPreset.parameters[key] });
@@ -129,9 +142,12 @@ const PresetPanel = () => {
setImportPresetData(''); setImportPresetData('');
setShowImportDialog(false); setShowImportDialog(false);
// 重新加载预设列表
fetchPresets();
} }
} catch (error) { } catch (error) {
console.error('导入预设失败:', error); console.error('导入预设失败:', error);
alert('导入预设失败: ' + error.message);
} }
}; };
@@ -175,7 +191,7 @@ const PresetPanel = () => {
const handleStartEditComponent = (index) => { const handleStartEditComponent = (index) => {
setEditingComponentIndex(index); setEditingComponentIndex(index);
setEditComponentContent(promptComponents[index].content); setEditComponentContent(promptComponents[index].content);
setIsEditing(true); // 设置为编辑模式 setIsEditing(true);
setShowComponentEditDialog(true); setShowComponentEditDialog(true);
}; };
@@ -183,13 +199,13 @@ const PresetPanel = () => {
const handleViewComponent = (index) => { const handleViewComponent = (index) => {
setEditingComponentIndex(index); setEditingComponentIndex(index);
setEditComponentContent(promptComponents[index].content); setEditComponentContent(promptComponents[index].content);
setIsEditing(false); // 设置为查看模式 setIsEditing(false);
setShowComponentEditDialog(true); setShowComponentEditDialog(true);
}; };
// 保存组件编辑 // 保存组件编辑
const handleSaveComponentEdit = () => { const handleSaveComponentEdit = () => {
if (editingComponentIndex >= 0 && isEditing) { // 只在编辑模式下保存 if (editingComponentIndex >= 0 && isEditing) {
updateComponent(editingComponentIndex, { content: editComponentContent }); updateComponent(editingComponentIndex, { content: editComponentContent });
} }
setEditingComponentIndex(-1); setEditingComponentIndex(-1);
@@ -228,7 +244,6 @@ const PresetPanel = () => {
setDraggedItem(index); setDraggedItem(index);
e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', index.toString()); e.dataTransfer.setData('text/plain', index.toString());
// 添加拖拽时的样式
setTimeout(() => { setTimeout(() => {
e.target.classList.add('dragging'); e.target.classList.add('dragging');
}, 0); }, 0);
@@ -254,10 +269,8 @@ const PresetPanel = () => {
e.preventDefault(); e.preventDefault();
if (draggedItem === null || draggedItem === index) return; if (draggedItem === null || draggedItem === index) return;
// 移动组件
moveComponent(draggedItem, index); moveComponent(draggedItem, index);
// 重置拖拽状态
setDraggedItem(null); setDraggedItem(null);
setDragOverItem(null); setDragOverItem(null);
}; };
@@ -404,13 +417,13 @@ const PresetPanel = () => {
value={editComponentContent} value={editComponentContent}
onChange={(e) => setEditComponentContent(e.target.value)} onChange={(e) => setEditComponentContent(e.target.value)}
className="component-textarea" className="component-textarea"
readOnly={!isEditing} // 根据模式设置是否只读 readOnly={!isEditing}
rows={20} rows={20}
/> />
</div> </div>
<div className="dialog-footer"> <div className="dialog-footer">
<span className="token-count"> <span className="token-count">
字符数: {editComponentContent ? editComponentContent.length : 0} {editComponentContent ? editComponentContent.length : 0}
</span> </span>
{isEditing && ( {isEditing && (
<div className="dialog-buttons"> <div className="dialog-buttons">
@@ -581,7 +594,6 @@ const PresetPanel = () => {
type="number" type="number"
min="1" min="1"
max="10000000" max="10000000"
defaultValue="1000000"
value={parameters.max_context} value={parameters.max_context}
onChange={(e) => handleParameterChange('max_context', e.target.value)} onChange={(e) => handleParameterChange('max_context', e.target.value)}
className="parameter-input" className="parameter-input"
@@ -601,7 +613,6 @@ const PresetPanel = () => {
type="number" type="number"
min="1" min="1"
max="100000" max="100000"
defaultValue="30000"
value={parameters.max_tokens} value={parameters.max_tokens}
onChange={(e) => handleParameterChange('max_tokens', e.target.value)} onChange={(e) => handleParameterChange('max_tokens', e.target.value)}
className="parameter-input" className="parameter-input"
@@ -729,7 +740,7 @@ const PresetPanel = () => {
</button> </button>
<span className="component-name">{component.name}</span> <span className="component-name">{component.name}</span>
{component.marker && ( {component.marker && (
<span className="component-marker-badge">固定</span> <span className="component-marker-badge">🔒</span>
)} )}
</div> </div>
<div className="component-actions"> <div className="component-actions">
@@ -739,10 +750,10 @@ const PresetPanel = () => {
onMouseEnter={(e) => showTooltip(e, "编辑/查看组件")} onMouseEnter={(e) => showTooltip(e, "编辑/查看组件")}
onMouseLeave={hideTooltip} onMouseLeave={hideTooltip}
> >
编辑
</button> </button>
<span className="token-count"> <span className="token-count">
{component.content ? component.content.length : 0} {component.content ? component.content.length : 0}
</span> </span>
{!component.marker && ( {!component.marker && (
<button <button
@@ -751,7 +762,7 @@ const PresetPanel = () => {
onMouseEnter={(e) => showTooltip(e, "删除组件")} onMouseEnter={(e) => showTooltip(e, "删除组件")}
onMouseLeave={hideTooltip} onMouseLeave={hideTooltip}
> >
删除 🗑
</button> </button>
)} )}
</div> </div>

View File

@@ -12,11 +12,9 @@ const WorldBook = () => {
isSelecting, isSelecting,
error, error,
showWorldBookDropdown, showWorldBookDropdown,
showAddWorldBookDropdown,
globalWorldBooks, globalWorldBooks,
fetchWorldBooks, fetchWorldBooks,
toggleWorldBookDropdown, toggleWorldBookDropdown,
toggleAddWorldBookDropdown,
addGlobalWorldBook, addGlobalWorldBook,
removeGlobalWorldBook, removeGlobalWorldBook,
createWorldBook, createWorldBook,
@@ -28,6 +26,7 @@ const WorldBook = () => {
toggleEditPanel, toggleEditPanel,
} = useWorldBookStore(); } = useWorldBookStore();
const [newEntry, setNewEntry] = useState({ const [newEntry, setNewEntry] = useState({
name: '', name: '',
content: '', content: '',
@@ -37,9 +36,11 @@ const WorldBook = () => {
order: 0, order: 0,
}); });
useEffect(() => { useEffect(() => {
fetchWorldBooks(); fetchWorldBooks();
}, [fetchWorldBooks]); }, [fetchWorldBooks]);
const handleCreateWorldBook = async () => { const handleCreateWorldBook = async () => {
const name = prompt('请输入世界书名称:'); const name = prompt('请输入世界书名称:');
@@ -71,62 +72,75 @@ const WorldBook = () => {
await updateEntry(editingEntry.uid, updatedEntry); await updateEntry(editingEntry.uid, updatedEntry);
}; };
const isGlobalBook = (bookUid) => {
return globalWorldBooks.some(gb => gb.uid === bookUid);
};
const handleToggleGlobalBook = (bookUid) => {
if (isGlobalBook(bookUid)) {
removeGlobalWorldBook(bookUid);
} else {
addGlobalWorldBook(bookUid);
}
};
const sortedWorldBooks = [...worldBooks].sort((a, b) => {
const aIsGlobal = isGlobalBook(a.uid);
const bIsGlobal = isGlobalBook(b.uid);
if (aIsGlobal && !bIsGlobal) return -1;
if (!aIsGlobal && bIsGlobal) return 1;
return 0;
});
return ( return (
<div className="worldbook-content"> <div className="worldbook-content">
{/* 全局世界书槽位 */} {/* 全局世界书区域 */}
<div className="global-worldbooks-slot"> <div className="global-worldbooks-section">
<div className="global-books-header"> <div className="global-worldbooks-slot">
<span>全局世界书</span> <div className="global-books-header">
<div className="dropdown"> <span className="title-text">全局世界书</span>
<button className="add-global-book-btn" onClick={toggleAddWorldBookDropdown}> {globalWorldBooks.length > 0 && (
+ 添加 <div className="active-books-list">
</button> {globalWorldBooks.map(book => (
{showAddWorldBookDropdown && ( <span
<div className="dropdown-menu"> key={book.uid}
<div className="dropdown-item" onClick={handleCreateWorldBook}> className="active-book-item"
新建世界书 onClick={() => selectWorldBook(book.uid)}
</div> >
{worldBooks {book.name}
.filter(book => !globalWorldBooks.find(gb => gb.uid === book.uid)) <span
.map(book => ( className="remove-btn"
<div onClick={(e) => {
key={book.uid} e.stopPropagation();
className="dropdown-item" removeGlobalWorldBook(book.uid);
onClick={() => { }}
addGlobalWorldBook(book.uid); >
toggleAddWorldBookDropdown(false);
}} </span>
> </span>
{book.name} ))}
</div> </div>
))} )}
</div>
</div>
{/* 操作按钮组 */}
<div className="worldbook-actions">
<button className="action-btn" onClick={handleCreateWorldBook}>
+ 新建
</button>
<button className="action-btn">
📋 复制
</button>
<button className="action-btn">
📥 导入
</button>
<button className="action-btn">
📤 导出
</button>
</div> </div>
)}
</div>
</div>
<div className="global-books-list">
{globalWorldBooks.map(book => (
<div
key={book.uid}
className={`global-book-tag ${selectedWorldBook?.uid === book.uid ? 'active' : ''}`}
onClick={() => selectWorldBook(book.uid)}
>
{book.name}
{selectedWorldBook?.uid === book.uid && (
<span
className="remove-btn"
onClick={(e) => {
e.stopPropagation();
removeGlobalWorldBook(book.uid);
}}
>
</span>
)}
</div> </div>
))}
</div>
</div>
{/* 世界书选择区域 */} {/* 世界书选择区域 */}
<div className="worldbook-selector"> <div className="worldbook-selector">

File diff suppressed because it is too large Load Diff

View File

@@ -5,93 +5,104 @@
padding: 12px; padding: 12px;
gap: 12px; gap: 12px;
overflow: hidden; overflow: hidden;
background: rgba(0, 0, 0, 0.3);
} }
/* 全局世界书槽位 */ /* 全局世界书区域 */
.global-worldbooks-slot { .global-worldbooks-section {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 8px;
padding: 10px; }
background: rgba(0, 0, 0, 0.4);
.global-worldbooks-slot {
background: white;
border-radius: 6px; border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid #e0e0e0;
overflow: hidden;
} }
.global-books-header { .global-books-header {
display: flex; display: flex;
justify-content: space-between; flex-direction: column;
align-items: center; gap: 4px;
font-size: 13px; padding: 8px 12px;
color: rgba(255, 255, 255, 0.95); }
margin-bottom: 4px;
.title-text {
font-size: 11px;
color: #777;
font-weight: 500; font-weight: 500;
} }
.global-books-list { .active-books-list {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 6px; gap: 4px;
min-height: 28px;
} }
.global-book-tag { .active-book-item {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 6px; gap: 4px;
padding: 4px 10px;
background: rgba(100, 149, 237, 0.25);
border: 1px solid rgba(100, 149, 237, 0.5);
border-radius: 4px;
font-size: 12px; font-size: 12px;
color: rgba(255, 255, 255, 1); color: #4a6cf7;
font-weight: 500;
padding: 2px 6px;
background: rgba(74, 108, 247, 0.1);
border-radius: 3px;
cursor: pointer; cursor: pointer;
transition: all 0.15s ease; transition: all 0.15s ease;
font-weight: 500;
} }
.global-book-tag:hover { .active-book-item:hover {
background: rgba(100, 149, 237, 0.4); background: rgba(74, 108, 247, 0.2);
border-color: rgba(100, 149, 237, 0.7);
} }
.global-book-tag.active { .active-book-item .remove-btn {
background: rgba(100, 149, 237, 0.5); opacity: 0.7;
border-color: rgba(100, 149, 237, 0.8);
box-shadow: 0 0 8px rgba(100, 149, 237, 0.3);
}
.global-book-tag .remove-btn {
opacity: 0.9;
cursor: pointer; cursor: pointer;
transition: opacity 0.15s ease; transition: opacity 0.15s ease;
font-weight: bold;
} }
.global-book-tag .remove-btn:hover { .active-book-item .remove-btn:hover {
opacity: 1; opacity: 1;
color: rgba(255, 107, 107, 1); color: #e74c3c;
} }
.add-global-book-btn { /* 操作按钮组 */
padding: 4px 8px; .worldbook-actions {
background: rgba(100, 149, 237, 0.3); display: flex;
border: 1px solid rgba(100, 149, 237, 0.5); gap: 6px;
flex-wrap: wrap;
}
.action-btn {
padding: 5px 10px;
background: white;
border: 1px solid #e0e0e0;
border-radius: 4px; border-radius: 4px;
color: rgba(255, 255, 255, 1); color: #555;
cursor: pointer; cursor: pointer;
font-size: 12px; font-size: 12px;
transition: all 0.15s ease; transition: all 0.15s ease;
font-weight: 500; font-weight: 500;
flex: 1;
min-width: 60px;
} }
.add-global-book-btn:hover { .action-btn:hover {
background: rgba(100, 149, 237, 0.5); background: #f5f5f5;
border-color: rgba(100, 149, 237, 0.8); border-color: #4a6cf7;
color: #4a6cf7;
}
/* 世界书选择区域 */
.worldbook-selector {
display: flex;
gap: 8px;
align-items: center;
} }
/* 下拉菜单 */
.dropdown { .dropdown {
position: relative; position: relative;
} }
@@ -99,10 +110,10 @@
.dropdown-btn { .dropdown-btn {
width: 100%; width: 100%;
padding: 8px 10px; padding: 8px 10px;
background: rgba(0, 0, 0, 0.3); background: white;
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid #e0e0e0;
border-radius: 4px; border-radius: 4px;
color: rgba(255, 255, 255, 0.95); color: #333;
cursor: pointer; cursor: pointer;
text-align: left; text-align: left;
display: flex; display: flex;
@@ -114,8 +125,8 @@
} }
.dropdown-btn:hover { .dropdown-btn:hover {
background: rgba(0, 0, 0, 0.4); background: #f5f5f5;
border-color: rgba(255, 255, 255, 0.3); border-color: #4a6cf7;
} }
.dropdown-menu { .dropdown-menu {
@@ -123,14 +134,14 @@
top: 100%; top: 100%;
left: 0; left: 0;
right: 0; right: 0;
background: rgba(10, 10, 10, 0.98); background: white;
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid #e0e0e0;
border-radius: 4px; border-radius: 4px;
margin-top: 4px; margin-top: 4px;
max-height: 200px; max-height: 200px;
overflow-y: auto; overflow-y: auto;
z-index: 1000; z-index: 1000;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
} }
.dropdown-item { .dropdown-item {
@@ -138,24 +149,17 @@
cursor: pointer; cursor: pointer;
transition: background 0.15s ease; transition: background 0.15s ease;
font-size: 13px; font-size: 13px;
color: rgba(255, 255, 255, 0.95); color: #333;
font-weight: 500; font-weight: 500;
} }
.dropdown-item:hover { .dropdown-item:hover {
background: rgba(100, 149, 237, 0.3); background: #f5f5f5;
} }
.dropdown-item.active { .dropdown-item.active {
background: rgba(100, 149, 237, 0.4); background: rgba(74, 108, 247, 0.1);
color: rgba(255, 255, 255, 1); color: #4a6cf7;
}
/* 世界书选择区域 */
.worldbook-selector {
display: flex;
gap: 8px;
align-items: center;
} }
/* 条目列表区域 */ /* 条目列表区域 */
@@ -169,48 +173,55 @@
.entry-item { .entry-item {
padding: 10px; padding: 10px;
background: rgba(0, 0, 0, 0.2); background: white;
border: 1px solid rgba(255, 255, 255, 0.15); border: 1px solid #e0e0e0;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
transition: all 0.15s ease; transition: all 0.15s ease;
} }
.entry-item:hover { .entry-item:hover {
background: rgba(0, 0, 0, 0.3); background: #f9f9f9;
border-color: rgba(255, 255, 255, 0.25); border-color: #4a6cf7;
} }
.entry-item.active { .entry-item.active {
background: rgba(100, 149, 237, 0.2); background: rgba(74, 108, 247, 0.1);
border-color: rgba(100, 149, 237, 0.5); border-color: #4a6cf7;
}
.entry-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
} }
.entry-name { .entry-name {
font-weight: 600; font-weight: 600;
font-size: 14px; font-size: 14px;
color: rgba(255, 255, 255, 0.95); color: #333;
} }
.entry-status { .entry-status {
font-size: 11px; font-size: 11px;
color: rgba(255, 255, 255, 0.7); color: #777;
padding: 2px 6px; padding: 2px 6px;
border-radius: 3px; border-radius: 3px;
background: rgba(0, 0, 0, 0.3); background: #f5f5f5;
font-weight: 500; font-weight: 500;
} }
.entry-status.enabled { .entry-status.enabled {
color: rgba(100, 255, 149, 1); color: #4a90e2;
background: rgba(100, 255, 149, 0.2); background: rgba(74, 144, 226, 0.1);
} }
.entry-meta { .entry-meta {
display: flex; display: flex;
gap: 12px; gap: 12px;
font-size: 11px; font-size: 11px;
color: rgba(255, 255, 255, 0.7); color: #777;
font-weight: 500; font-weight: 500;
} }
@@ -221,13 +232,14 @@
right: 0; right: 0;
bottom: 0; bottom: 0;
left: 300px; left: 300px;
background: rgba(20, 20, 20, 0.98); background: white;
z-index: 1000; z-index: 1000;
padding: 16px; padding: 16px;
overflow-y: auto; overflow-y: auto;
transform: translateX(100%); transform: translateX(100%);
transition: transform 0.2s ease-out; transition: transform 0.2s ease-out;
border-left: 1px solid rgba(255, 255, 255, 0.1); border-left: 1px solid #e0e0e0;
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
} }
.edit-panel.open { .edit-panel.open {
@@ -240,19 +252,20 @@
align-items: center; align-items: center;
margin-bottom: 16px; margin-bottom: 16px;
padding-bottom: 12px; padding-bottom: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1); border-bottom: 1px solid #e0e0e0;
} }
.edit-panel-header h2 { .edit-panel-header h2 {
font-size: 16px; font-size: 16px;
color: rgba(255, 255, 255, 0.9); color: #333;
margin: 0; margin: 0;
font-weight: 600;
} }
.close-btn { .close-btn {
background: none; background: none;
border: none; border: none;
color: rgba(255, 255, 255, 0.7); color: #999;
font-size: 20px; font-size: 20px;
cursor: pointer; cursor: pointer;
padding: 4px; padding: 4px;
@@ -260,7 +273,7 @@
} }
.close-btn:hover { .close-btn:hover {
color: rgba(255, 255, 255, 0.9); color: #333;
} }
.form-group { .form-group {
@@ -271,7 +284,8 @@
display: block; display: block;
margin-bottom: 6px; margin-bottom: 6px;
font-size: 13px; font-size: 13px;
color: rgba(255, 255, 255, 0.7); color: #555;
font-weight: 500;
} }
.form-input, .form-input,
@@ -279,10 +293,10 @@
.form-select { .form-select {
width: 100%; width: 100%;
padding: 8px 10px; padding: 8px 10px;
background: rgba(255, 255, 255, 0.05); background: white;
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid #e0e0e0;
border-radius: 4px; border-radius: 4px;
color: rgba(255, 255, 255, 0.9); color: #333;
font-size: 13px; font-size: 13px;
transition: all 0.15s ease; transition: all 0.15s ease;
} }
@@ -291,8 +305,8 @@
.form-textarea:focus, .form-textarea:focus,
.form-select:focus { .form-select:focus {
outline: none; outline: none;
background: rgba(255, 255, 255, 0.08); border-color: #4a6cf7;
border-color: rgba(100, 149, 237, 0.5); box-shadow: 0 0 0 2px rgba(74, 108, 247, 0.1);
} }
.form-textarea { .form-textarea {
@@ -306,7 +320,8 @@
gap: 8px; gap: 8px;
cursor: pointer; cursor: pointer;
font-size: 13px; font-size: 13px;
color: rgba(255, 255, 255, 0.9); color: #555;
font-weight: 500;
} }
.form-checkbox input { .form-checkbox input {
@@ -321,30 +336,25 @@
cursor: pointer; cursor: pointer;
font-size: 13px; font-size: 13px;
transition: all 0.15s ease; transition: all 0.15s ease;
font-weight: 500;
} }
.btn-primary { .btn-primary {
background: rgba(100, 149, 237, 0.3); background: #4a6cf7;
border: 1px solid rgba(100, 149, 237, 0.5); color: white;
color: rgba(255, 255, 255, 1);
font-weight: 500;
} }
.btn-primary:hover { .btn-primary:hover {
background: rgba(100, 149, 237, 0.5); background: #3a5ce5;
border-color: rgba(100, 149, 237, 0.8);
} }
.btn-danger { .btn-danger {
background: rgba(255, 107, 107, 0.3); background: #e74c3c;
border: 1px solid rgba(255, 107, 107, 0.5); color: white;
color: rgba(255, 255, 255, 1);
font-weight: 500;
} }
.btn-danger:hover { .btn-danger:hover {
background: rgba(255, 107, 107, 0.5); background: #c0392b;
border-color: rgba(255, 107, 107, 0.8);
} }
/* 加载和错误状态 */ /* 加载和错误状态 */
@@ -356,52 +366,13 @@
} }
.loading { .loading {
color: rgba(255, 255, 255, 0.8); color: #777;
font-weight: 500; font-weight: 500;
} }
.error { .error {
color: rgba(255, 107, 107, 1); color: #e74c3c;
background: rgba(255, 107, 107, 0.2); background: rgba(231, 76, 60, 0.1);
border-radius: 4px; border-radius: 4px;
font-weight: 500; font-weight: 500;
} }
.form-label {
display: block;
margin-bottom: 6px;
font-size: 13px;
color: rgba(255, 255, 255, 0.95);
font-weight: 500;
}
.form-input,
.form-textarea,
.form-select {
width: 100%;
padding: 8px 10px;
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
color: rgba(255, 255, 255, 0.95);
font-size: 13px;
transition: all 0.15s ease;
}
.form-input:focus,
.form-textarea:focus,
.form-select:focus {
outline: none;
background: rgba(0, 0, 0, 0.4);
border-color: rgba(100, 149, 237, 0.6);
}
.form-checkbox {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-size: 13px;
color: rgba(255, 255, 255, 0.95);
font-weight: 500;
}