diff --git a/backend/api/routes/chatsRoute.py b/backend/api/routes/chatsRoute.py index 4638f1b..5b56200 100644 --- a/backend/api/routes/chatsRoute.py +++ b/backend/api/routes/chatsRoute.py @@ -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") diff --git a/backend/api/routes/presetsRoute.py b/backend/api/routes/presetsRoute.py index 8200464..a5770f6 100644 --- a/backend/api/routes/presetsRoute.py +++ b/backend/api/routes/presetsRoute.py @@ -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") diff --git a/backend/core/models/PromptComponent.py b/backend/core/models/PromptComponent.py index 1ca3b04..04208ee 100644 --- a/backend/core/models/PromptComponent.py +++ b/backend/core/models/PromptComponent.py @@ -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) diff --git a/backend/core/models/PromptList.py b/backend/core/models/PromptList.py index d005928..e832179 100644 --- a/backend/core/models/PromptList.py +++ b/backend/core/models/PromptList.py @@ -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: diff --git a/backend/core/models/chat_history.py b/backend/core/models/chat_history.py index ba5b48d..dfa0b5b 100644 --- a/backend/core/models/chat_history.py +++ b/backend/core/models/chat_history.py @@ -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" diff --git a/frontend-react/src/Store/Slices/ChatBoxSlice.jsx b/frontend-react/src/Store/Slices/ChatBoxSlice.jsx index ca1337a..c194623 100644 --- a/frontend-react/src/Store/Slices/ChatBoxSlice.jsx +++ b/frontend-react/src/Store/Slices/ChatBoxSlice.jsx @@ -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; diff --git a/frontend-react/src/Store/Slices/LeftTabsSlices/PresetSlice.jsx b/frontend-react/src/Store/Slices/LeftTabsSlices/PresetSlice.jsx index cf4c1d5..826842a 100644 --- a/frontend-react/src/Store/Slices/LeftTabsSlices/PresetSlice.jsx +++ b/frontend-react/src/Store/Slices/LeftTabsSlices/PresetSlice.jsx @@ -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) => ({ diff --git a/frontend-react/src/Store/Slices/RoleSelectorSlice.jsx b/frontend-react/src/Store/Slices/RoleSelectorSlice.jsx index dedc148..6b287b4 100644 --- a/frontend-react/src/Store/Slices/RoleSelectorSlice.jsx +++ b/frontend-react/src/Store/Slices/RoleSelectorSlice.jsx @@ -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, diff --git a/frontend-react/src/components/SideBarLeft/tab/Presets.jsx b/frontend-react/src/components/SideBarLeft/tab/Presets.jsx index 512d792..92bb720 100644 --- a/frontend-react/src/components/SideBarLeft/tab/Presets.jsx +++ b/frontend-react/src/components/SideBarLeft/tab/Presets.jsx @@ -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} />
- 字符数: {editComponentContent ? editComponentContent.length : 0} + {editComponentContent ? editComponentContent.length : 0} {isEditing && (
@@ -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 = () => { {component.name} {component.marker && ( - 固定 + 🔒 )}
@@ -739,10 +750,10 @@ const PresetPanel = () => { onMouseEnter={(e) => showTooltip(e, "编辑/查看组件")} onMouseLeave={hideTooltip} > - 编辑 + ✏️ - {component.content ? component.content.length : 0}字 + {component.content ? component.content.length : 0} {!component.marker && ( )}
diff --git a/frontend-react/src/components/SideBarLeft/tab/WorldBook.jsx b/frontend-react/src/components/SideBarLeft/tab/WorldBook.jsx index ecdd4ab..cec241d 100644 --- a/frontend-react/src/components/SideBarLeft/tab/WorldBook.jsx +++ b/frontend-react/src/components/SideBarLeft/tab/WorldBook.jsx @@ -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 (
- {/* 全局世界书槽位 */} -
-
- 全局世界书 -
- - {showAddWorldBookDropdown && ( -
-
- 新建世界书 -
- {worldBooks - .filter(book => !globalWorldBooks.find(gb => gb.uid === book.uid)) - .map(book => ( -
{ - addGlobalWorldBook(book.uid); - toggleAddWorldBookDropdown(false); - }} - > - {book.name} + {/* 全局世界书区域 */} +
+
+
+ 全局世界书 + {globalWorldBooks.length > 0 && ( +
+ {globalWorldBooks.map(book => ( + selectWorldBook(book.uid)} + > + {book.name} + { + e.stopPropagation(); + removeGlobalWorldBook(book.uid); + }} + > + ✕ + + + ))}
- ))} + )} +
+
+ + {/* 操作按钮组 */} +
+ + + +
- )} -
-
-
- {globalWorldBooks.map(book => ( -
selectWorldBook(book.uid)} - > - {book.name} - {selectedWorldBook?.uid === book.uid && ( - { - e.stopPropagation(); - removeGlobalWorldBook(book.uid); - }} - > - ✕ - - )}
- ))} -
-
+ {/* 世界书选择区域 */}
diff --git a/frontend-react/src/components/SideBarLeft/tabcss/Presets.css b/frontend-react/src/components/SideBarLeft/tabcss/Presets.css index 4254725..6dc3595 100644 --- a/frontend-react/src/components/SideBarLeft/tabcss/Presets.css +++ b/frontend-react/src/components/SideBarLeft/tabcss/Presets.css @@ -1,366 +1,735 @@ -.worldbook-content { +/* 主容器 */ +.preset-panel { display: flex; flex-direction: column; height: 100%; padding: 12px; gap: 12px; - overflow: hidden; -} - -/* 全局世界书槽位 */ -.global-worldbooks-slot { - display: flex; - flex-direction: column; - gap: 8px; - padding: 10px; - background: rgba(255, 255, 255, 0.05); - border-radius: 6px; - border: 1px solid rgba(255, 255, 255, 0.1); -} - -.global-books-header { - display: flex; - justify-content: space-between; - align-items: center; - font-size: 13px; - color: #333; - margin-bottom: 4px; - font-weight: 600; -} - -.global-books-list { - display: flex; - flex-wrap: wrap; - gap: 6px; - min-height: 28px; -} - -.global-book-tag { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 4px 10px; - background: rgba(74, 108, 247, 0.15); - border: 1px solid rgba(74, 108, 247, 0.3); - border-radius: 4px; - font-size: 12px; - color: #333; - cursor: pointer; - transition: all 0.15s ease; - font-weight: 500; -} - -.global-book-tag:hover { - background: rgba(74, 108, 247, 0.25); - border-color: rgba(74, 108, 247, 0.5); -} - -.global-book-tag.active { - background: rgba(74, 108, 247, 0.3); - border-color: rgba(74, 108, 247, 0.6); - box-shadow: 0 0 8px rgba(74, 108, 247, 0.2); -} - -.global-book-tag .remove-btn { - opacity: 0.8; - cursor: pointer; - transition: opacity 0.15s ease; - font-weight: bold; -} - -.global-book-tag .remove-btn:hover { - opacity: 1; - color: #e74c3c; -} - -.add-global-book-btn { - padding: 4px 8px; - background: rgba(74, 108, 247, 0.2); - border: 1px solid rgba(74, 108, 247, 0.3); - border-radius: 4px; - color: #333; - cursor: pointer; - font-size: 12px; - transition: all 0.15s ease; - font-weight: 500; -} - -.add-global-book-btn:hover { - background: rgba(74, 108, 247, 0.3); - border-color: rgba(74, 108, 247, 0.5); -} - -/* 下拉菜单 */ -.dropdown { - position: relative; -} - -.dropdown-btn { - width: 100%; - padding: 8px 10px; - background: rgba(255, 255, 255, 0.05); - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 4px; - color: #333; - cursor: pointer; - text-align: left; - display: flex; - justify-content: space-between; - align-items: center; - font-size: 13px; - transition: all 0.15s ease; - font-weight: 500; -} - -.dropdown-btn:hover { - background: rgba(255, 255, 255, 0.08); - border-color: rgba(255, 255, 255, 0.15); -} - -.dropdown-menu { - position: absolute; - top: 100%; - left: 0; - right: 0; - background: white; - border: 1px solid #e0e0e0; - border-radius: 4px; - margin-top: 4px; - max-height: 200px; + background: #f8f9fa; overflow-y: auto; - z-index: 1000; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } -.dropdown-item { - padding: 8px 10px; - cursor: pointer; - transition: background 0.15s ease; - font-size: 13px; - color: #333; - font-weight: 500; -} - -.dropdown-item:hover { - background: #f5f5f5; -} - -.dropdown-item.active { - background: rgba(74, 108, 247, 0.15); - color: #4a6cf7; -} - -/* 世界书选择区域 */ -.worldbook-selector { - display: flex; - gap: 8px; - align-items: center; -} - -/* 条目列表区域 */ -.entries-container { - flex: 1; - overflow-y: auto; - display: flex; - flex-direction: column; - gap: 6px; -} - -.entry-item { - padding: 10px; - background: rgba(255, 255, 255, 0.03); - border: 1px solid rgba(255, 255, 255, 0.05); - border-radius: 4px; - cursor: pointer; - transition: all 0.15s ease; -} - -.entry-item:hover { - background: rgba(255, 255, 255, 0.05); - border-color: rgba(255, 255, 255, 0.1); -} - -.entry-item.active { - background: rgba(74, 108, 247, 0.1); - border-color: rgba(74, 108, 247, 0.3); -} - -.entry-name { - font-weight: 600; - font-size: 14px; - color: #333; -} - -.entry-status { - font-size: 11px; - color: #777; - padding: 2px 6px; - border-radius: 3px; - background: rgba(255, 255, 255, 0.05); - font-weight: 500; -} - -.entry-status.enabled { - color: #4a90e2; - background: rgba(74, 144, 226, 0.1); -} - -.entry-meta { - display: flex; - gap: 12px; - font-size: 11px; - color: #777; - font-weight: 500; -} - -/* 编辑面板 */ -.edit-panel { +/* 工具提示 */ +.tooltip { position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 300px; - background: white; - z-index: 1000; - padding: 16px; - overflow-y: auto; - transform: translateX(100%); - transition: transform 0.2s ease-out; - border-left: 1px solid #e0e0e0; - box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1); -} - -.edit-panel.open { - transform: translateX(0); -} - -.edit-panel-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 16px; - padding-bottom: 12px; - border-bottom: 1px solid #e0e0e0; -} - -.edit-panel-header h2 { - font-size: 16px; - color: #333; - margin: 0; - font-weight: 600; -} - -.close-btn { - background: none; - border: none; - color: #999; - font-size: 20px; - cursor: pointer; - padding: 4px; - transition: color 0.15s ease; -} - -.close-btn:hover { - color: #333; -} - -.form-group { - margin-bottom: 12px; -} - -.form-label { - display: block; - margin-bottom: 6px; - font-size: 13px; - color: #555; - font-weight: 500; -} - -.form-input, -.form-textarea, -.form-select { - width: 100%; - padding: 8px 10px; - background: white; - border: 1px solid #e0e0e0; + padding: 6px 10px; + background: rgba(0, 0, 0, 0.85); + color: white; border-radius: 4px; - color: #333; - font-size: 13px; - transition: all 0.15s ease; + font-size: 11px; + font-weight: 500; + z-index: 1000; + pointer-events: none; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); + transform: translate(-50%, -100%); + margin-top: -6px; } -.form-input:focus, -.form-textarea:focus, -.form-select:focus { +/* 预设头部 */ +.preset-header { + display: flex; + align-items: center; + gap: 10px; + padding: 10px; + background: white; + border-radius: 6px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); +} + +.preset-select-container { + flex: 1; + display: flex; + align-items: center; + gap: 6px; +} + +.preset-label { + font-size: 12px; + font-weight: 600; + color: #2c3e50; + white-space: nowrap; +} + +.preset-select { + flex: 1; + padding: 6px 10px; + background: #f8f9fa; + border: 1px solid #e9ecef; + border-radius: 4px; + font-size: 12px; + color: #495057; + cursor: pointer; + transition: all 0.2s ease; +} + +.preset-select:hover { + border-color: #ced4da; + background: white; +} + +.preset-select:focus { outline: none; border-color: #4a6cf7; box-shadow: 0 0 0 2px rgba(74, 108, 247, 0.1); } -.form-textarea { - min-height: 100px; - resize: vertical; +.preset-select:disabled { + background: #f1f3f5; + cursor: not-allowed; + color: #adb5bd; } -.form-checkbox { +/* 操作按钮 */ +.preset-actions { + display: flex; + gap: 6px; +} + +.preset-action-btn { + width: 30px; + height: 30px; display: flex; align-items: center; - gap: 8px; - cursor: pointer; - font-size: 13px; - color: #555; - font-weight: 500; -} - -.form-checkbox input { - cursor: pointer; -} - -/* 按钮样式 */ -.btn { - padding: 8px 12px; - border: none; + justify-content: center; + background: white; + border: 1px solid #e9ecef; border-radius: 4px; cursor: pointer; - font-size: 13px; - transition: all 0.15s ease; - font-weight: 500; + transition: all 0.2s ease; + font-size: 14px; } -.btn-primary { +.preset-action-btn:hover { + background: #f8f9fa; + border-color: #ced4da; + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08); +} + +.preset-action-btn:active { + transform: translateY(0); +} + +/* 对话框 */ +.preset-save-dialog, +.preset-edit-dialog, +.preset-import-dialog { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: white; + padding: 16px; + border-radius: 6px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + z-index: 1000; + min-width: 280px; +} + +.preset-save-dialog input, +.preset-edit-dialog input { + width: 100%; + padding: 8px 10px; + margin-bottom: 12px; + background: #f8f9fa; + border: 1px solid #e9ecef; + border-radius: 4px; + font-size: 13px; + transition: all 0.2s ease; +} + +.preset-save-dialog input:focus, +.preset-edit-dialog input:focus { + outline: none; + border-color: #4a6cf7; + box-shadow: 0 0 0 2px rgba(74, 108, 247, 0.1); +} + +.preset-import-dialog textarea { + width: 100%; + padding: 8px 10px; + margin-bottom: 12px; + background: #f8f9fa; + border: 1px solid #e9ecef; + border-radius: 4px; + font-size: 12px; + font-family: inherit; + resize: vertical; + min-height: 100px; +} + +.preset-import-dialog textarea:focus { + outline: none; + border-color: #4a6cf7; + box-shadow: 0 0 0 2px rgba(74, 108, 247, 0.1); +} + +.dialog-buttons { + display: flex; + gap: 6px; + justify-content: flex-end; +} + +.dialog-buttons button { + padding: 6px 12px; + border: none; + border-radius: 4px; + font-size: 12px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.dialog-buttons button:first-child { background: #4a6cf7; color: white; } -.btn-primary:hover { +.dialog-buttons button:first-child:hover { background: #3a5ce5; } -.btn-danger { - background: #e74c3c; - color: white; +.dialog-buttons button:last-child { + background: #f1f3f5; + color: #495057; } -.btn-danger:hover { - background: #c0392b; +.dialog-buttons button:last-child:hover { + background: #e9ecef; } -/* 加载和错误状态 */ -.loading, -.error { - text-align: center; - padding: 20px; - font-size: 13px; +/* 组件编辑对话框 */ +.component-edit-dialog { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: white; + border-radius: 6px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + z-index: 1000; + min-width: 450px; + max-width: 80vw; + max-height: 80vh; + display: flex; + flex-direction: column; } -.loading { - color: #777; - font-weight: 500; +.dialog-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + border-bottom: 1px solid #e9ecef; } -.error { - color: #e74c3c; - background: rgba(231, 76, 60, 0.1); +.dialog-header h3 { + margin: 0; + font-size: 14px; + font-weight: 600; + color: #2c3e50; +} + +.dialog-header .close-btn { + background: none; + border: none; + font-size: 20px; + color: #adb5bd; + cursor: pointer; + padding: 0; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + transition: color 0.2s ease; +} + +.dialog-header .close-btn:hover { + color: #495057; +} + +.dialog-content { + padding: 16px; + flex: 1; + overflow-y: auto; +} + +.component-textarea { + width: 100%; + padding: 10px; + background: #f8f9fa; + border: 1px solid #e9ecef; border-radius: 4px; + font-size: 13px; + font-family: inherit; + line-height: 1.5; + resize: none; + transition: all 0.2s ease; +} + +.component-textarea:focus { + outline: none; + border-color: #4a6cf7; + box-shadow: 0 0 0 2px rgba(74, 108, 247, 0.1); +} + +.component-textarea[readonly] { + background: #f1f3f5; + cursor: default; +} + +.dialog-footer { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + border-top: 1px solid #e9ecef; +} + +.token-count { + font-size: 11px; + color: #6c757d; font-weight: 500; } + +/* 参数设置区域 */ +.preset-parameters-container { + background: white; + border-radius: 6px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); + overflow: hidden; +} + +.parameters-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 12px; + background: #f8f9fa; + cursor: pointer; + transition: background 0.2s ease; +} + +.parameters-header:hover { + background: #f1f3f5; +} + +.parameters-header span:first-child { + font-size: 13px; + font-weight: 600; + color: #2c3e50; +} + +.expand-icon { + font-size: 10px; + color: #6c757d; + transition: transform 0.3s ease; +} + +.expand-icon.expanded { + transform: rotate(180deg); +} + +.preset-parameters { + padding: 12px; +} + +.parameter-row { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 12px; +} + +.parameter-row:last-child { + margin-bottom: 0; +} + +.parameter-label { + width: 100px; + font-size: 12px; + font-weight: 500; + color: #495057; +} + +.parameter-slider { + flex: 1; + -webkit-appearance: none; + height: 4px; + background: #e9ecef; + border-radius: 2px; + outline: none; + cursor: pointer; +} + +.parameter-slider::-webkit-slider-thumb { + -webkit-appearance: none; + width: 14px; + height: 14px; + background: #4a6cf7; + border-radius: 50%; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); +} + +.parameter-slider::-webkit-slider-thumb:hover { + background: #3a5ce5; + transform: scale(1.1); +} + +.parameter-slider::-moz-range-thumb { + width: 14px; + height: 14px; + background: #4a6cf7; + border: none; + border-radius: 50%; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); +} + +.parameter-slider::-moz-range-thumb:hover { + background: #3a5ce5; + transform: scale(1.1); +} + +.parameter-number { + width: 60px; + padding: 4px 6px; + background: #f8f9fa; + border: 1px solid #e9ecef; + border-radius: 4px; + font-size: 12px; + text-align: center; + transition: all 0.2s ease; +} + +.parameter-number:focus { + outline: none; + border-color: #4a6cf7; + box-shadow: 0 0 0 2px rgba(74, 108, 247, 0.1); +} + +.parameter-input { + flex: 1; + padding: 6px 10px; + background: #f8f9fa; + border: 1px solid #e9ecef; + border-radius: 4px; + font-size: 12px; + transition: all 0.2s ease; +} + +.parameter-input:focus { + outline: none; + border-color: #4a6cf7; + box-shadow: 0 0 0 2px rgba(74, 108, 247, 0.1); +} + +.parameter-toggles { + display: flex; + flex-direction: column; + gap: 10px; + margin-top: 12px; + padding-top: 12px; + border-top: 1px solid #e9ecef; +} + +.toggle-row { + display: flex; + justify-content: space-between; + align-items: center; +} + +.toggle-label { + font-size: 12px; + font-weight: 500; + color: #495057; +} + +.toggle-checkbox { + width: 38px; + height: 20px; + -webkit-appearance: none; + background: #e9ecef; + border-radius: 10px; + position: relative; + cursor: pointer; + transition: all 0.3s ease; +} + +.toggle-checkbox::after { + content: ''; + position: absolute; + width: 16px; + height: 16px; + background: white; + border-radius: 50%; + top: 2px; + left: 2px; + transition: all 0.3s ease; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); +} + +.toggle-checkbox:checked { + background: #4a6cf7; +} + +.toggle-checkbox:checked::after { + left: 20px; +} + +/* 预设组件区域 */ +.preset-components-section { + flex: 1; + display: flex; + flex-direction: column; + background: white; + border-radius: 6px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); + overflow: hidden; +} + +.components-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 12px; + background: #f8f9fa; + border-bottom: 1px solid #e9ecef; +} + +.components-header h3 { + margin: 0; + font-size: 13px; + font-weight: 600; + color: #2c3e50; +} + +.add-component-btn { + padding: 4px 10px; + background: #4a6cf7; + color: white; + border: none; + border-radius: 4px; + font-size: 12px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.add-component-btn:hover { + background: #3a5ce5; + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(74, 108, 247, 0.2); +} + +.components-list { + flex: 1; + overflow-y: auto; + padding: 6px; +} + +.drag-indicator { + height: 3px; + background: #4a6cf7; + border-radius: 2px; + opacity: 0; + transition: opacity 0.2s ease; + margin: 3px 0; +} + +.drag-indicator.visible { + opacity: 0.5; +} + +.prompt-component-item { + display: flex; + align-items: center; + padding: 8px 10px; + margin-bottom: 6px; + background: #f8f9fa; + border: 1px solid #e9ecef; + border-radius: 4px; + transition: all 0.2s ease; + cursor: grab; +} + +.prompt-component-item:hover { + background: white; + border-color: #ced4da; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08); +} + +.prompt-component-item.disabled { + opacity: 0.6; +} + +.prompt-component-item.marker { + background: rgba(74, 108, 247, 0.05); + border-color: rgba(74, 108, 247, 0.2); +} + +.prompt-component-item.dragging { + opacity: 0.5; + cursor: grabbing; +} + +.component-header { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; +} + +.component-controls { + display: flex; + align-items: center; + gap: 6px; + flex: 1; +} + +.drag-handle { + color: #adb5bd; + cursor: grab; + font-size: 12px; + user-select: none; + transition: color 0.2s ease; +} + +.drag-handle:hover { + color: #6c757d; +} + +.toggle-btn { + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + background: white; + border: 1px solid #e9ecef; + border-radius: 3px; + cursor: pointer; + font-size: 10px; + transition: all 0.2s ease; +} + +.toggle-btn:hover { + border-color: #ced4da; + background: #f8f9fa; +} + +.toggle-btn.enabled { + background: #4a6cf7; + color: white; + border-color: #4a6cf7; +} + +.toggle-btn.enabled:hover { + background: #3a5ce5; +} + +.component-name { + font-size: 12px; + font-weight: 500; + color: #2c3e50; + flex: 1; +} + +.component-marker-badge { + padding: 1px 6px; + background: rgba(74, 108, 247, 0.1); + color: #4a6cf7; + border-radius: 3px; + font-size: 10px; + font-weight: 600; +} + +.component-actions { + display: flex; + align-items: center; + gap: 6px; +} + +.edit-btn { + padding: 3px 6px; + background: white; + border: 1px solid #e9ecef; + border-radius: 3px; + font-size: 10px; + font-weight: 500; + color: #495057; + cursor: pointer; + transition: all 0.2s ease; +} + +.edit-btn:hover { + background: #f8f9fa; + border-color: #ced4da; +} + +.delete-btn { + padding: 3px 6px; + background: white; + border: 1px solid #e9ecef; + border-radius: 3px; + font-size: 10px; + font-weight: 500; + color: #dc3545; + cursor: pointer; + transition: all 0.2s ease; +} + +.delete-btn:hover { + background: #fff5f5; + border-color: #ff6b6b; +} + +/* 滚动条样式 */ +::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +::-webkit-scrollbar-track { + background: #f1f3f5; + border-radius: 3px; +} + +::-webkit-scrollbar-thumb { + background: #ced4da; + border-radius: 3px; +} + +::-webkit-scrollbar-thumb:hover { + background: #adb5bd; +} + +/* 响应式设计 */ +@media (max-width: 768px) { + .preset-panel { + padding: 10px; + gap: 10px; + } + + .preset-header { + flex-direction: column; + align-items: stretch; + } + + .preset-select-container { + flex-direction: column; + align-items: stretch; + } + + .preset-actions { + justify-content: center; + } + + .component-edit-dialog { + min-width: 85vw; + } +} diff --git a/frontend-react/src/components/SideBarLeft/tabcss/WorldBook.css b/frontend-react/src/components/SideBarLeft/tabcss/WorldBook.css index 6dfcb29..bbc3362 100644 --- a/frontend-react/src/components/SideBarLeft/tabcss/WorldBook.css +++ b/frontend-react/src/components/SideBarLeft/tabcss/WorldBook.css @@ -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; -}