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

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 pathlib import Path
import json
from typing import List, Dict
from backend.core.models.chat_history import ChatHistory, Message
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():
"""获取所有角色的所有聊天列表"""
data_dir = Path("data")
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}
return await ChatHistory.list_all_chats()
@router.get("/{role_name}/{chat_name}")
async def get_chat(role_name: str, chat_name: str):
"""获取指定聊天的完整内容"""
try:
chat_history = ChatHistory.load_from_file(role_name, chat_name)
return {
"metadata": chat_history.chat_metadata.dict(),
"messages": chat_history.to_chatbox_format()
}
except FileNotFoundError:
return await ChatHistory.get_chat(role_name, chat_name)
except FileNotFoundError as e:
raise HTTPException(status_code=404, detail="Chat not found")
@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
role_dir.mkdir(parents=True, exist_ok=True)
chat_path = role_dir / f"{chat_name}.jsonl"
if chat_path.exists():
try:
return await ChatHistory.create_chat(role_name, chat_name, metadata)
except FileExistsError:
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}")
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:
chat_history = ChatHistory.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)
# 保存更改
chat_history.save_to_file(role_name, chat_name)
return {"message": "Chat metadata updated successfully"}
return await ChatHistory.update_chat(role_name, chat_name, update_data)
except FileNotFoundError:
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}")
async def delete_chat(role_name: str, chat_name: str):
"""删除指定聊天"""
chat_path = Path("data") / role_name / f"{chat_name}.jsonl"
if not chat_path.exists():
try:
return await ChatHistory.delete_chat(role_name, chat_name)
except FileNotFoundError:
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):
"""获取聊天的所有消息"""
try:
chat_history = ChatHistory.load_from_file(role_name, chat_name)
return {"messages": chat_history.to_chatbox_format()}
return await ChatHistory.list_messages(role_name, chat_name)
except FileNotFoundError:
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):
"""获取指定楼层的消息"""
try:
chat_history = ChatHistory.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 HTTPException(status_code=404, detail="Message not found")
return message.dict()
return await ChatHistory.get_message(role_name, chat_name, floor)
except FileNotFoundError:
raise HTTPException(status_code=404, detail="Chat not found")
@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:
chat_history = ChatHistory.load_from_file(role_name, chat_name)
# 创建消息对象
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}
return await ChatHistory.add_message(role_name, chat_name, message_data)
except FileNotFoundError:
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}")
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:
chat_history = ChatHistory.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 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"}
return await ChatHistory.update_message(role_name, chat_name, floor, update_data)
except FileNotFoundError:
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):
"""删除指定楼层的消息"""
try:
chat_history = ChatHistory.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 HTTPException(status_code=404, detail="Message not found")
# 保存更改
chat_history.save_to_file(role_name, chat_name)
return {"message": "Message deleted successfully"}
return await ChatHistory.delete_message(role_name, chat_name, floor)
except FileNotFoundError:
raise HTTPException(status_code=404, detail="Chat not found")

View File

@@ -1,7 +1,4 @@
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.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():
"""获取所有预设列表及其基本信息"""
preset_dir = Path("data/preset")
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}
return await AIDesignSpec.list_all_presets()
@router.get("/{preset_name}")
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:
with open(preset_path, 'r', encoding='utf-8') as f:
preset_data = json.load(f)
# 转换为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)}")
return await AIDesignSpec.get_preset(preset_name)
except FileNotFoundError:
raise HTTPException(status_code=404, detail="Preset not found")
@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:
# 验证并转换为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 created successfully", "name": preset_name}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to create preset: {str(e)}")
return await AIDesignSpec.create_preset(preset_name, preset_data)
except FileExistsError:
raise HTTPException(status_code=400, detail="Preset already exists")
@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:
# 加载现有预设
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 = 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)}")
return await AIDesignSpec.update_preset(preset_name, update_data)
except FileNotFoundError:
raise HTTPException(status_code=404, detail="Preset not found")
@router.delete("/{preset_name}")
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:
preset_path.unlink()
return {"message": "Preset deleted successfully"}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to delete preset: {str(e)}")
return await AIDesignSpec.delete_preset(preset_name)
except FileNotFoundError:
raise HTTPException(status_code=404, detail="Preset not found")
# ========== 预设组件路由 ==========
@@ -121,144 +54,45 @@ async def delete_preset(preset_name: str):
@router.get("/{preset_name}/components")
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:
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 HTTPException(status_code=500, detail=f"Failed to load components: {str(e)}")
return await AIDesignSpec.list_components(preset_name)
except FileNotFoundError:
raise HTTPException(status_code=404, detail="Preset not found")
@router.get("/{preset_name}/components/{component_id}")
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:
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 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)}")
return await AIDesignSpec.get_component(preset_name, component_id)
except FileNotFoundError:
raise HTTPException(status_code=404, detail="Preset not found")
@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:
# 加载预设数据
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 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)}")
return await AIDesignSpec.add_component_to_preset(preset_name, component_data)
except FileNotFoundError:
raise HTTPException(status_code=404, detail="Preset not found")
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@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:
# 加载预设数据
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 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)}")
return await AIDesignSpec.update_component_in_preset(preset_name, component_id, update_data)
except FileNotFoundError:
raise HTTPException(status_code=404, detail="Preset not found")
@router.delete("/{preset_name}/components/{component_id}")
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:
# 加载预设数据
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 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)}")
return await AIDesignSpec.delete_component_from_preset(preset_name, component_id)
except FileNotFoundError:
raise HTTPException(status_code=404, detail="Preset not found")

View File

@@ -16,7 +16,7 @@ class PromptComponent(BaseModel):
@validator('role')
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)")
return v
@@ -49,7 +49,7 @@ class PromptComponent(BaseModel):
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'PromptComponent':
"""
从字典创建组件实例
从字典创建组件实例自动处理role字段的类型转换
参数:
data: 包含组件数据的字典
@@ -57,4 +57,9 @@ class PromptComponent(BaseModel):
返回:
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)

View File

@@ -1,5 +1,7 @@
from pydantic import BaseModel, Field, validator
from typing import List, Dict, Any, Optional
from pathlib import Path
import json
from .PromptComponent import PromptComponent
@@ -69,6 +71,379 @@ class AIDesignSpec(BaseModel):
raise ValueError(f"prompt_order中包含不存在的组件ID: {invalid_ids}")
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:

View File

@@ -88,7 +88,175 @@ class ChatHistory(BaseModel):
class Config:
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':
"""
从JSONL文件加载聊天历史
@@ -105,10 +273,9 @@ class ChatHistory(BaseModel):
FileNotFoundError: 当文件不存在时抛出
json.JSONDecodeError: 当JSON解析失败时抛出
"""
# 设置默认基础路径 - 如果未提供base_path则从配置中获取默认路径
# 设置默认基础路径
if base_path is None:
from backend.core.config import settings # 延迟导入配置模块
base_path = settings.DATA_PATH / "chat" # 构建默认路径
base_path = cls.get_data_path()
# 构建文件路径
file_path = base_path / role_name / f"{chat_name}.jsonl"
@@ -258,7 +425,7 @@ class ChatHistory(BaseModel):
"""
# 设置默认基础路径
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"

View File

@@ -3,46 +3,46 @@ import { subscribeWithSelector } from 'zustand/middleware';
const useChatBoxStore = create(
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) => {
console.log('[ChatBoxStore] setChatBoxRoleAndChat called with:', { role, chat });
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) => {
const { messages, userName, characterName, currentRole, currentChat } = get();
set({
isGenerating: true,
messages: [...messages, {
id: Date.now(),
floor: messages.length + 1,
mes: content,
is_user: true
}]
});
set({
isGenerating: true,
messages: [...messages, {
id: Date.now(),
floor: messages.length + 1,
mes: content,
is_user: true
}]
});
try {
const response = await fetch(`/api/chats/${encodeURIComponent(currentRole)}/${encodeURIComponent(currentChat)}/messages`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
floor: messages.length + 1,
mes: content,
is_user: true
})
});
try {
const response = await fetch('/api/chat_box/send_message', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
role_name: currentRole,
chat_name: currentChat,
message: content
})
});
if (!response.ok) {
throw new Error('Failed to send message');
}
if (!response.ok) {
throw new Error('Failed to send message');
}
const data = await response.json();
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) => ({
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 }),
// 终止生成
stopGeneration: () => set({ isGenerating: false }),
// 加载聊天历史
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();
// 修改数据处理逻辑适配API返回的数据结构
set({
messages: data || [], // 直接使用返回的数组
userName: 'User', // 固定用户名
characterName: roleName || 'Assistant', // 使用角色名作为角色名称
isLoading: false
});
} catch (error) {
set({
error: error.message,
isLoading: false
});
}
},
// 加载聊天历史
fetchChatHistory: async (roleName, chatName) => {
set({ isLoading: true, error: null });
try {
const response = await fetch(`/api/chats/${encodeURIComponent(roleName)}/${encodeURIComponent(chatName)}`);
if (!response.ok) {
throw new Error('Failed to fetch chat history');
}
const data = await response.json();
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({
messages: [],
userName: '',
characterName: '',
error: null
}),
// 清空聊天历史
clearChatHistory: () => set({
messages: [],
userName: '',
characterName: '',
error: null
}),
// 更新特定消息的内容
updateMessage: (id, content) => set((state) => ({
messages: state.messages.map((msg) =>
msg.id === id ? { ...msg, content } : msg
)
})),
// 更新特定消息的内容
updateMessage: async (floor, content) => {
const { currentRole, currentChat } = get();
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(
(state) => ({ role: state.currentRole, chat: state.currentChat }),
({ role, chat }, prev) => {
// 只有当角色或聊天发生变化时才处理
if (role !== prev.role || chat !== prev.chat) {
// 确保角色和聊天都存在且不为null
if (role && chat) {
useChatBoxStore.getState().fetchChatHistory(role, chat);
} else {
useChatBoxStore.getState().clearChatHistory();
}
}
// 只有当角色或聊天发生变化时才处理
if (role !== prev.role || chat !== prev.chat) {
// 确保角色和聊天都存在且不为null
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;

View File

@@ -100,14 +100,16 @@ const usePresetStore = create((set, get) => ({
fetchPresets: async () => {
set({ isLoadingPresets: true });
try {
const response = await fetch('/api/presets/list');
const response = await fetch('/api/presets');
const data = await response.json();
// 转换为预设对象数组
const presetList = data.presets.map(name => ({
id: name,
name,
parameters: {} // 参数将在选择预设时加载
const presetList = data.presets.map(preset => ({
id: preset.name,
name: preset.name,
description: preset.description,
component_count: preset.component_count,
temperature: preset.temperature
}));
set({ presets: presetList, isLoadingPresets: false });
@@ -150,14 +152,19 @@ const usePresetStore = create((set, get) => ({
// 处理预设组件
let components = [];
if (presetData.prompts && Array.isArray(presetData.prompts)) {
// 获取当前角色的prompt_order
const currentOrder = presetData.prompt_order && presetData.prompt_order.length > 0
// 获取当前角色的prompt_order,添加更严格的检查
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
: [];
// 根据prompt_order排序组件
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 {
...prompt,
enabled: orderItem ? orderItem.enabled : true,
@@ -168,13 +175,14 @@ const usePresetStore = create((set, get) => ({
// 如果有prompt_order按照它排序
if (currentOrder.length > 0) {
components.sort((a, b) => {
const indexA = currentOrder.findIndex(item => item.identifier === a.identifier);
const indexB = currentOrder.findIndex(item => item.identifier === b.identifier);
const indexA = currentOrder.findIndex(item => item && item.identifier === a.identifier);
const indexB = currentOrder.findIndex(item => item && item.identifier === b.identifier);
return indexA - indexB;
});
}
}
// 更新状态,确保参数容器展开
set({
selectedPreset: presetId,
@@ -187,8 +195,6 @@ const usePresetStore = create((set, get) => ({
}
},
// 更新参数
updateParameter: ({ name, value }) => set((state) => ({
parameters: { ...state.parameters, [name]: value }
@@ -200,25 +206,97 @@ const usePresetStore = create((set, get) => ({
})),
// 保存当前设置为预设
saveCurrentAsPreset: ({ name }) => set((state) => {
const newPreset = {
id: `preset_${Date.now()}`,
name,
parameters: { ...state.parameters },
promptComponents: [...state.promptComponents]
};
return {
presets: [...state.presets, newPreset],
selectedPreset: newPreset.id
};
}),
saveCurrentAsPreset: async ({ name }) => {
const state = get();
try {
// 构建预设数据
const presetData = {
...state.parameters,
prompts: state.promptComponents.map(component => ({
identifier: component.identifier,
name: component.name,
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) => ({
presets: state.presets.map(preset =>
preset.id === presetId ? { ...preset, name: newName } : preset
)
})),
editPresetName: async (presetId, newName) => {
try {
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) => ({

View File

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

View File

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

View File

@@ -12,11 +12,9 @@ const WorldBook = () => {
isSelecting,
error,
showWorldBookDropdown,
showAddWorldBookDropdown,
globalWorldBooks,
fetchWorldBooks,
toggleWorldBookDropdown,
toggleAddWorldBookDropdown,
addGlobalWorldBook,
removeGlobalWorldBook,
createWorldBook,
@@ -28,6 +26,7 @@ const WorldBook = () => {
toggleEditPanel,
} = useWorldBookStore();
const [newEntry, setNewEntry] = useState({
name: '',
content: '',
@@ -37,9 +36,11 @@ const WorldBook = () => {
order: 0,
});
useEffect(() => {
fetchWorldBooks();
}, [fetchWorldBooks]);
useEffect(() => {
fetchWorldBooks();
}, [fetchWorldBooks]);
const handleCreateWorldBook = async () => {
const name = prompt('请输入世界书名称:');
@@ -71,62 +72,75 @@ const WorldBook = () => {
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 (
<div className="worldbook-content">
{/* 全局世界书槽位 */}
<div className="global-worldbooks-slot">
<div className="global-books-header">
<span>全局世界书</span>
<div className="dropdown">
<button className="add-global-book-btn" onClick={toggleAddWorldBookDropdown}>
+ 添加
</button>
{showAddWorldBookDropdown && (
<div className="dropdown-menu">
<div className="dropdown-item" onClick={handleCreateWorldBook}>
新建世界书
</div>
{worldBooks
.filter(book => !globalWorldBooks.find(gb => gb.uid === book.uid))
.map(book => (
<div
key={book.uid}
className="dropdown-item"
onClick={() => {
addGlobalWorldBook(book.uid);
toggleAddWorldBookDropdown(false);
}}
>
{book.name}
{/* 全局世界书区域 */}
<div className="global-worldbooks-section">
<div className="global-worldbooks-slot">
<div className="global-books-header">
<span className="title-text">全局世界书</span>
{globalWorldBooks.length > 0 && (
<div className="active-books-list">
{globalWorldBooks.map(book => (
<span
key={book.uid}
className="active-book-item"
onClick={() => selectWorldBook(book.uid)}
>
{book.name}
<span
className="remove-btn"
onClick={(e) => {
e.stopPropagation();
removeGlobalWorldBook(book.uid);
}}
>
</span>
</span>
))}
</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 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 className="worldbook-selector">

File diff suppressed because it is too large Load Diff

View File

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