19 Commits

Author SHA1 Message Date
a3e3711b2b 重构前端完成 2026-04-29 00:19:46 +08:00
8b10ef5828 重构后端成功 2026-04-28 00:45:18 +08:00
dd17206e1f 世界书部分基本完成,剩余编辑条目部分,可以推进到下一步 2026-04-07 05:40:27 +08:00
4f9cf4b725 世界书部分基本完成 2026-04-07 01:56:13 +08:00
e8dedb5ec4 重构路由架构,修复导致的前端出错 2026-04-06 00:52:04 +08:00
7a62139683 重构路由架构 2026-04-05 12:04:18 +08:00
01ca2bd0f9 暂时完成预设部分、开始世界书部分 2026-04-04 15:42:43 +08:00
1fc0c43689 预设拖拽实现 2026-04-04 15:19:37 +08:00
1abfaeda9d 后端预设列表读取完成 2026-04-04 12:36:27 +08:00
0ae53c4b81 后端预设、世界书部分补充 2026-04-04 10:41:38 +08:00
f90ad8dc13 补充css修改 2026-04-03 13:44:35 +08:00
6375f9759c 完成前端初步重构、实现显示选中角色功能 2026-04-03 12:09:08 +08:00
33188a345e 完成后端chatmessage格式修改,可以在前端完成swipe切换显示 2026-04-01 19:15:10 +08:00
6fa1fd6e7f 修补输入框 2026-04-01 12:50:16 +08:00
80237463ef 完成聊天框功能开发 2026-03-30 20:28:50 +08:00
f9bc77d392 Merge branch 'fix/点击聊天不修改角色' 2026-03-29 20:27:26 +08:00
408e9ce569 chore: add .gitignore to ignore __pycache__ 2026-03-29 20:26:52 +08:00
60a2049bb7 merge feature/聊天框实现 2026-03-29 20:13:57 +08:00
4d4d7c30ce 修补选中聊天不更换角色bug 2026-03-29 20:04:16 +08:00
68 changed files with 462 additions and 8556 deletions

7
.env
View File

@@ -8,4 +8,9 @@ REGEX_FILE=/data/regex_rules.json
# ---------- 服务地址 ---------- # ---------- 服务地址 ----------
COMFYUI_API_URL=http://comfyui:8188 COMFYUI_API_URL=http://comfyui:8188
BACKEND_PORT=8000 BACKEND_PORT=8000
FRONTEND_PORT=8501 FRONTEND_PORT=8501
# 先配置 .env 文件
MAIN_LLM_API_KEY=sk-Oh4o3fzV6Qe59B6DRwSskE48xe5D6bq1hkgDZqH1mmJOCN8j
MAIN_LLM_BASE_URL=https://api.chatfire.cn/v1
MAIN_LLM_MODEL=glm4.7

BIN
.gitignore vendored Normal file

Binary file not shown.

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

View File

@@ -3,6 +3,8 @@
<component name="NewModuleRootManager"> <component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/backend" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/backend/api" isTestSource="false" />
</content> </content>
<orderEntry type="jdk" jdkName="Python 3.9 (pythonProject1)" jdkType="Python SDK" /> <orderEntry type="jdk" jdkName="Python 3.9 (pythonProject1)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />

View File

@@ -4,6 +4,10 @@ FROM python:3.11-slim
# 设置工作目录 # 设置工作目录
WORKDIR /app WORKDIR /app
# 设置环境变量
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
# 复制依赖文件 # 复制依赖文件
COPY requirements.txt . COPY requirements.txt .
@@ -11,13 +15,10 @@ COPY requirements.txt .
RUN pip install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt RUN pip install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt
# 复制所有代码 # 复制所有代码
# 修改点:把当前目录(即 backend/)的内容复制到 /app/backend/ 下 COPY . .
# 这样镜像内的结构就是 /app/backend/api/route.py
COPY . /app/backend/
# 暴露端口 # 暴露端口
EXPOSE 8000 EXPOSE 8000
# 启动命令 # 启动命令
# 修改点:路径改为 backend.api.route (对应 /app/backend/api/route.py) CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
CMD ["uvicorn", "backend.api.route:app", "--host", "0.0.0.0", "--port", "8000"]

Binary file not shown.

View File

@@ -1,18 +1,19 @@
from fastapi import APIRouter from fastapi import APIRouter
from ..core.items import ChatRequest from .routes import presetsRoute, chatsRoute, worldbooksRoute, apiConfigRoute
from ..tools.get_all_role_and_chat import get_all_role_and_chat from utils.file_utils import get_all_roles_and_chats
from ..tools.save_input_to_json import save_input_to_json from core.config import settings
from pathlib import Path
router = APIRouter() router = APIRouter()
# 1. 将输入内容持久化存储到本地jsonl方便前端读 # 注册子路由
@router.post("/generate_reply") router.include_router(presetsRoute.router)
async def save_chat_to_json(chat_request: ChatRequest): router.include_router(chatsRoute.router)
# 调用实际的保存函数 router.include_router(worldbooksRoute.router)
return await save_input_to_json(chat_request) router.include_router(apiConfigRoute.router)
# 2. 从本地jsonl中读取历史对话
# 保留原有的其他路由
@router.get("/tool_bar/get_all_role_and_chat") @router.get("/tool_bar/get_all_role_and_chat")
def get_all_role_and_chat_endpoint(): def get_all_role_and_chat_endpoint():
# 正确调用函数并返回结果 return get_all_roles_and_chats(Path(settings.DATA_PATH))
return get_all_role_and_chat()

View File

@@ -0,0 +1,56 @@
from fastapi import APIRouter, HTTPException, status
# TODO: 实现 ChatService 来替代旧的 ChatHistory 逻辑
# from services.chat_service import ChatService
router = APIRouter(prefix="/chat", tags=["chat"])
@router.get("", response_model=dict)
async def list_all_chats():
"""获取所有角色的所有聊天列表"""
# return await ChatService.list_all_chats()
return {"chats": []}
@router.get("/{role_name}/{chat_name}")
async def get_chat(role_name: str, chat_name: str):
"""获取指定聊天的完整内容"""
raise HTTPException(status_code=501, detail="Not Implemented")
@router.post("/{role_name}", status_code=status.HTTP_201_CREATED)
async def create_chat(role_name: str, chat_name: str, metadata: dict = None):
"""创建新聊天"""
raise HTTPException(status_code=501, detail="Not Implemented")
@router.put("/{role_name}/{chat_name}")
async def update_chat(role_name: str, chat_name: str, update_data: dict):
"""更新聊天元数据"""
raise HTTPException(status_code=501, detail="Not Implemented")
@router.delete("/{role_name}/{chat_name}")
async def delete_chat(role_name: str, chat_name: str):
"""删除指定聊天"""
raise HTTPException(status_code=501, detail="Not Implemented")
@router.get("/{role_name}/{chat_name}/messages")
async def list_messages(role_name: str, chat_name: str):
"""获取聊天的所有消息"""
raise HTTPException(status_code=501, detail="Not Implemented")
@router.get("/{role_name}/{chat_name}/messages/{floor}")
async def get_message(role_name: str, chat_name: str, floor: int):
"""获取指定楼层的消息"""
raise HTTPException(status_code=501, detail="Not Implemented")
@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):
"""向聊天添加新消息"""
raise HTTPException(status_code=501, detail="Not Implemented")
@router.put("/{role_name}/{chat_name}/messages/{floor}")
async def update_message(role_name: str, chat_name: str, floor: int, update_data: dict):
"""更新指定楼层的消息"""
raise HTTPException(status_code=501, detail="Not Implemented")
@router.delete("/{role_name}/{chat_name}/messages/{floor}")
async def delete_message(role_name: str, chat_name: str, floor: int):
"""删除指定楼层的消息"""
raise HTTPException(status_code=501, detail="Not Implemented")

View File

@@ -0,0 +1,60 @@
from fastapi import APIRouter, HTTPException, status
# TODO: 实现 PresetService 来替代旧的 AIDesignSpec 逻辑
# from services.preset_service import PresetService
router = APIRouter(prefix="/presets", tags=["presets"])
@router.get("", response_model=dict)
async def list_presets():
"""获取所有预设列表及其基本信息"""
# return await PresetService.list_all_presets()
return {"presets": []}
@router.get("/{preset_name}")
async def get_preset(preset_name: str):
"""获取指定预设的完整内容"""
# try:
# return await PresetService.get_preset(preset_name)
# except FileNotFoundError:
# raise HTTPException(status_code=404, detail="Preset not found")
raise HTTPException(status_code=501, detail="Not Implemented")
@router.post("", status_code=status.HTTP_201_CREATED)
async def create_preset(preset_name: str, preset_data: dict):
"""创建新预设"""
raise HTTPException(status_code=501, detail="Not Implemented")
@router.put("/{preset_name}")
async def update_preset(preset_name: str, update_data: dict):
"""更新预设配置"""
raise HTTPException(status_code=501, detail="Not Implemented")
@router.delete("/{preset_name}")
async def delete_preset(preset_name: str):
"""删除指定预设"""
raise HTTPException(status_code=501, detail="Not Implemented")
@router.get("/{preset_name}/components")
async def list_preset_components(preset_name: str):
"""获取预设中的所有组件"""
raise HTTPException(status_code=501, detail="Not Implemented")
@router.get("/{preset_name}/components/{component_id}")
async def get_preset_component(preset_name: str, component_id: str):
"""获取指定组件的详情"""
raise HTTPException(status_code=501, detail="Not Implemented")
@router.post("/{preset_name}/components", status_code=status.HTTP_201_CREATED)
async def add_preset_component(preset_name: str, component_data: dict):
"""向预设添加新组件"""
raise HTTPException(status_code=501, detail="Not Implemented")
@router.put("/{preset_name}/components/{component_id}")
async def update_preset_component(preset_name: str, component_id: str, update_data: dict):
"""更新指定组件"""
raise HTTPException(status_code=501, detail="Not Implemented")
@router.delete("/{preset_name}/components/{component_id}")
async def delete_preset_component(preset_name: str, component_id: str):
"""从预设中删除指定组件"""
raise HTTPException(status_code=501, detail="Not Implemented")

View File

@@ -0,0 +1,117 @@
3# 标准库导入
import os
import shutil
import logging
from pathlib import Path
from typing import List, Dict, Any, Optional
# 第三方库导入
from fastapi import APIRouter, HTTPException, UploadFile, File, Form
from fastapi.responses import JSONResponse, FileResponse
# 本地模块导入
from models.internal import WorldInfo, WorldInfoEntry
from core.config import settings
# 配置日志
logger = logging.getLogger(__name__)
# 创建路由器
router = APIRouter(prefix="/worldbooks", tags=["worldbooks"])
# 确保世界书目录存在 (由 config.py 中的 settings.ensure_directories() 统一处理,此处保留作为双重保险)
os.makedirs(settings.WORLDBOOKS_PATH, exist_ok=True)
@router.get("/", response_model=List[Dict[str, Any]])
async def list_worldbooks():
"""
获取所有世界书的列表
Returns:
List[Dict[str, Any]]: 世界书列表
"""
# TODO: 实现 WorldBookService
return []
@router.get("/{name}", response_model=Dict[str, Any])
async def get_worldbook(name: str):
"""
获取指定名称的世界书
"""
raise HTTPException(status_code=501, detail="Not Implemented")
@router.post("/", response_model=Dict[str, Any])
async def create_worldbook(
name: str = Form(...),
file: Optional[UploadFile] = File(None)
):
"""
创建新世界书
"""
raise HTTPException(status_code=501, detail="Not Implemented")
@router.put("/{name}", response_model=Dict[str, Any])
async def update_worldbook(
name: str,
file: Optional[UploadFile] = File(None)
):
"""
更新世界书
"""
raise HTTPException(status_code=501, detail="Not Implemented")
@router.delete("/{name}")
async def delete_worldbook(name: str):
"""
删除世界书
"""
raise HTTPException(status_code=501, detail="Not Implemented")
@router.get("/{name}/entries", response_model=List[Dict[str, Any]])
async def list_worldbook_entries(name: str):
"""
获取世界书的所有条目
"""
raise HTTPException(status_code=501, detail="Not Implemented")
@router.get("/{name}/entries/{uid}", response_model=Dict[str, Any])
async def get_worldbook_entry(name: str, uid: int):
"""
获取世界书的指定条目
"""
raise HTTPException(status_code=501, detail="Not Implemented")
@router.post("/{name}/entries", response_model=Dict[str, Any])
async def create_worldbook_entry(name: str, entry_data: Dict[str, Any]):
"""
在世界书中创建新条目
"""
raise HTTPException(status_code=501, detail="Not Implemented")
@router.put("/{name}/entries/{uid}", response_model=Dict[str, Any])
async def update_worldbook_entry(name: str, uid: int, entry_data: Dict[str, Any]):
"""
更新世界书的指定条目
"""
raise HTTPException(status_code=501, detail="Not Implemented")
@router.delete("/{name}/entries/{uid}")
async def delete_worldbook_entry(name: str, uid: int):
"""
删除世界书的指定条目
"""
raise HTTPException(status_code=501, detail="Not Implemented")
@router.post("/{name}/import", response_model=Dict[str, Any])
async def import_worldbook(name: str, file: UploadFile = File(...)):
"""
从文件导入世界书
"""
raise HTTPException(status_code=501, detail="Not Implemented")
@router.get("/{name}/export")
async def export_worldbook(name: str):
"""
导出世界书为 SillyTavern 格式
"""
raise HTTPException(status_code=501, detail="Not Implemented")

Binary file not shown.

Binary file not shown.

View File

@@ -3,46 +3,77 @@ from pathlib import Path
from dotenv import load_dotenv from dotenv import load_dotenv
# 1. 动态计算项目根目录 # 1. 动态计算项目根目录
# 假设 config.py 位于 backend/ 目录下 # 假设 config.py 位于 backend/core/ 目录下
# __file__ 指向本文件的绝对路径 # __file__ 指向本文件的绝对路径
# .parent 指向 backend/ 目录 # .parent 指向 backend/core/ 目录
# .parent.parent 指向项目根目录 (即包含 backend/ 和 frontend/ 的目录) # .parent.parent 指向 backend/ 目录
# .parent.parent.parent 指向项目根目录 (即包含 backend/ 和 frontend/ 的目录)
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
# 2. 加载 .env 文件 # 2. 加载 .env 文件
# 假设 .env 文件位于项目根目录下 # 假设 .env 文件位于项目根目录下
load_dotenv(PROJECT_ROOT / ".env") load_dotenv(PROJECT_ROOT / ".env")
class Settings: class Settings:
# --- 主模型配置 --- # --- 主模型配置 ---
MAIN_LLM_API_KEY = os.getenv("MAIN_LLM_API_KEY") MAIN_LLM_API_KEY = os.getenv("MAIN_LLM_API_KEY")
MAIN_LLM_MODEL = os.getenv("MAIN_LLM_MODEL", "gpt-3.5-turbo") MAIN_LLM_MODEL = os.getenv("MAIN_LLM_MODEL", "gpt-3.5-turbo")
MAIN_LLM_BASE_URL = os.getenv("MAIN_LLM_BASE_URL", "https://api.openai.com/v1") MAIN_LLM_BASE_URL = os.getenv("MAIN_LLM_BASE_URL", "https://api.openai.com/v1")
MAIN_LLM_MAX_TOKENS = int(os.getenv("MAIN_LLM_MAX_TOKENS", "4096")) MAIN_LLM_MAX_TOKENS = int(os.getenv("MAIN_LLM_MAX_TOKENS", "4096"))
MAIN_LLM_STREAM = os.getenv("MAIN_LLM_STREAM", "true").lower() == "true" MAIN_LLM_STREAM = os.getenv("MAIN_LLM_STREAM", "true").lower() == "true"
# --- 路径配置 (核心修改) --- # --- 路径配置 (核心修改) ---
# 强制使用计算出的项目根目录,不再依赖 .env 中的 BASE_PATH # 强制使用计算出的项目根目录,不再依赖 .env 中的 BASE_PATH
BASE_PATH = PROJECT_ROOT BASE_PATH = PROJECT_ROOT
# 数据目录:固定为根目录下的 data 文件夹 # 数据目录:固定为根目录下的 data 文件夹
# 即使 .env 里写了 DATA_PATH=/data这里也会强制指向项目根目录下的 data DATA_PATH = BASE_PATH / "data"
DATA_PATH = BASE_PATH / "data"
# --- 核心数据文件路径 ---
STATE_FILE = DATA_PATH / "state.json"
SCHEMA_FILE = DATA_PATH / "schema.json"
PRESETS_FILE = DATA_PATH / "presets.json"
REGEX_FILE = DATA_PATH / "regex_rules.json"
VECTORSTORE_PATH = DATA_PATH / "vectorstore"
# --- 业务数据目录 ---
# 世界书目录
WORLDBOOKS_PATH = DATA_PATH / "worldbooks"
# 预设目录
PRESET_PATH = DATA_PATH / "preset"
# 聊天记录目录
CHAT_PATH = DATA_PATH / "chat"
# 临时文件目录
TEMP_PATH = DATA_PATH / "temp"
def ensure_directories(self):
"""确保所有配置的目录存在,如果不存在则创建"""
directories = [
self.DATA_PATH,
self.WORLDBOOKS_PATH,
self.PRESET_PATH,
self.CHAT_PATH,
self.TEMP_PATH,
]
for directory in directories:
directory.mkdir(parents=True, exist_ok=True)
# 其他文件路径:基于 DATA_PATH 拼接
STATE_FILE = DATA_PATH / "state.json"
SCHEMA_FILE = DATA_PATH / "schema.json"
PRESETS_FILE = DATA_PATH / "presets.json"
REGEX_FILE = DATA_PATH / "regex_rules.json"
VECTORSTORE_PATH = DATA_PATH / "vectorstore"
settings = Settings() settings = Settings()
# 初始化时自动创建必要的目录
settings.ensure_directories()
if __name__ == '__main__': if __name__ == '__main__':
settings = Settings() settings = Settings()
print(f"项目根目录: {settings.BASE_PATH}") print(f"项目根目录: {settings.BASE_PATH}")
print(f"数据目录: {settings.DATA_PATH}") print(f"数据目录: {settings.DATA_PATH}")
print(f"聊天目录: {settings.DATA_PATH / 'chat'}") print(f"世界书目录: {settings.WORLDBOOKS_PATH}")
print(f"预设目录: {settings.PRESETS_PATH}")
print(f"聊天目录: {settings.CHAT_PATH}")

View File

@@ -1,24 +0,0 @@
from pydantic import BaseModel, Field
from typing import Optional, List
# 1. 定义请求体模型
class ChatRequest(BaseModel):
# --- 基础信息 ---
mes: str = Field(..., description="用户输入的消息内容")
is_user: bool = Field(..., description="标识发送者是否为用户True为用户False为AI")
floor_number: int = Field(..., description="当前对话的楼层号,用于判断是否为重试(Regenerate)请求")
# --- 身份与会话 ---
name: str = Field("default", description="发送者的显示名称,默认为'default'")
role_name: Optional[str] = Field(None, description="当前绑定的角色名称")
chat_name: Optional[str] = Field(None, description="当前会话的标识名称")
preset: Optional[str] = Field(None, description="预设的提示词或系统指令")
# --- 功能开关 ---
stream: bool = Field(False, description="是否开启流式输出")
img_switch: bool = Field(False, description="是否开启图片生成功能")
table_switch: bool = Field(False, description="是否开启表格生成功能")
# 其他可能需要的参数,比如历史记录,可以在这里加
# history: Optional[List[Dict]] = None

View File

@@ -1,158 +0,0 @@
from typing import List, Dict, Any, Optional
from datetime import datetime
from pathlib import Path
from pydantic import BaseModel, Field
import json
class Message(BaseModel):
"""消息类代表JSONL文件中的一行消息内容"""
name: str = Field(..., description="发言者名称")
is_user: bool = Field(..., description="是否为用户消息true=用户false=AI/角色)")
is_system: bool = Field(False, description="是否为系统消息(系统消息在文本导出时会被排除)")
send_date: str = Field(default_factory=lambda: str(int(datetime.now().timestamp() * 1000)),
description="发送时间戳Unix毫秒数")
mes: str = Field(..., description="消息正文内容")
extra: Dict[str, Any] = Field(default_factory=dict, description="额外信息包含推理内容、API、模型等")
swipes: List[str] = Field(default_factory=list, description="备选回复列表")
swipe_id: int = Field(0, description="当前选中的备选索引0=第一条)")
swipe_info: List[Dict[str, Any]] = Field(default_factory=list, description="每个备选回复的生成信息")
title: str = Field("", description="消息标题,用于消息摘要或分支标记")
force_avatar: Optional[str] = Field(None, description="强制头像路径")
variables: List[Any] = Field(default_factory=list, description="变量值数组")
variables_initialized: List[bool] = Field(default_factory=list, description="变量初始化状态数组")
is_ejs_processed: List[bool] = Field(default_factory=list, description="EJS模板处理状态数组")
gen_started: Optional[str] = Field(None, description="生成开始时间戳Unix毫秒数")
gen_finished: Optional[str] = Field(None, description="生成结束时间戳Unix毫秒数")
class ChatMetadata(BaseModel):
"""聊天元数据模型代表JSONL文件的第一行内容"""
integrity: str = Field("", description="完整性校验哈希值UUID格式")
chat_id_hash: str = Field("", description="聊天ID哈希值")
note_prompt: str = Field("", description="笔记提示词")
note_interval: int = Field(0, description="笔记间隔(整数)")
note_position: int = Field(0, description="笔记位置(整数)")
note_depth: int = Field(0, description="笔记深度(整数)")
note_role: int = Field(0, description="笔记角色整数0=用户1=助手)")
extensions: Dict[str, Any] = Field(default_factory=dict, description="扩展信息如LittleWhiteBox等")
timedWorldInfo: Dict[str, Any] = Field(default_factory=dict, description="定时世界信息")
variables: Dict[str, Any] = Field(default_factory=dict, description="变量字典")
tainted: bool = Field(False, description="是否被污染")
lastInContextMessageId: int = Field(-1, description="上下文中最后一条消息的ID")
class ChatFile(BaseModel):
"""聊天文件类,包含元数据和消息列表"""
user_name: str = Field("User", description="用户名")
character_name: str = Field("Assistant", description="角色名")
create_date: str = Field(default_factory=lambda: datetime.now().isoformat(), description="创建日期ISO 8601格式")
chat_metadata: ChatMetadata = Field(default_factory=ChatMetadata, description="聊天元数据")
messages: List[Message] = Field(default_factory=list, description="消息列表")
class Config:
arbitrary_types_allowed = True
def load_chat_file_data(chat_name: str, role_name: str, base_path: Path = None) -> Dict[str, Any]:
"""
从文件系统加载聊天原始数据
参数:
chat_name: 聊天名称
role_name: 角色名称
base_path: 基础路径,默认为项目数据目录
返回:
dict: 包含元数据和消息列表的原始数据字典
"""
# 设置默认基础路径
if base_path is None:
from backend.core.config import settings
base_path = settings.DATA_PATH / "chat"
# 构建文件路径
file_path = base_path / role_name / f"{chat_name}.jsonl"
# 检查文件是否存在
if not file_path.exists():
raise FileNotFoundError(f"聊天文件不存在: {file_path}")
# 读取文件内容
result = {
"user_name": "User",
"character_name": role_name,
"create_date": datetime.now().isoformat(),
"chat_metadata": {},
"messages": []
}
with open(file_path, 'r', encoding='utf-8') as f:
for line in f:
try:
# 解析JSON行
line_data = json.loads(line.strip())
# 添加到消息列表
result["messages"].append(line_data)
except json.JSONDecodeError:
continue
return result
def create_chat_file_from_data(data: Dict[str, Any]) -> ChatFile:
"""
从原始数据创建ChatFile对象
参数:
data: 包含元数据和消息列表的原始数据字典
返回:
ChatFile: 创建的聊天文件对象
"""
# 提取元数据
metadata = data.get("chat_metadata", {})
chat_metadata = ChatMetadata(**metadata)
# 处理消息列表
messages = []
for msg_data in data.get("messages", []):
# 转换为Message对象
message = Message(
name=msg_data.get('name', ''),
is_user=msg_data.get('is_user', False),
send_date=msg_data.get('send_date', ''),
mes=msg_data.get('content', ''),
swipes=msg_data.get('swipes', []),
swipe_id=msg_data.get('swipes_id', 0)
)
messages.append(message)
# 创建并返回ChatFile对象
return ChatFile(
user_name=data.get("user_name", "User"),
character_name=data.get("character_name", "Assistant"),
create_date=data.get("create_date", datetime.now().isoformat()),
chat_metadata=chat_metadata,
messages=messages
)
def load_chat_file(chat_name: str, role_name: str, base_path: Path = None) -> ChatFile:
"""
从文件系统加载聊天数据并创建ChatFile对象
参数:
chat_name: 聊天名称
role_name: 角色名称
base_path: 基础路径,默认为项目数据目录
返回:
ChatFile: 加载的聊天文件对象
"""
# 加载原始数据
data = load_chat_file_data(chat_name, role_name, base_path)
# 创建ChatFile对象
return create_chat_file_from_data(data)

View File

@@ -1,12 +1,41 @@
import logging
import sys
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(sys.stdout)
]
)
# 确保所有模块的日志都能被捕获
for logger_name in ['uvicorn', 'uvicorn.access', 'fastapi']:
logging_logger = logging.getLogger(logger_name)
logging_logger.setLevel(logging.INFO)
# backend/app/main.py # backend/app/main.py
from fastapi import FastAPI from fastapi import FastAPI
from .api.route import router try:
from backend.api.route import router
except ImportError:
from api.route import router
app = FastAPI(title="LLM Workflow Engine") app = FastAPI(title="LLM Workflow Engine")
# 注册路由 # 注册路由
app.include_router(router, prefix="/api") app.include_router(router, prefix="/api")
# 添加健康检查端点
@app.get("/health")
async def health_check():
return {"status": "healthy"}
# 添加根路径
@app.get("/")
async def root():
return {"message": "LLM Workflow Engine", "status": "running"}
if __name__ == "__main__": if __name__ == "__main__":
import uvicorn import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000) uvicorn.run(app, host="0.0.0.0", port=8000)

View File

@@ -1,56 +0,0 @@
import re
from core.node_base import BaseNode
from typing import List, Dict, Any
class TextSplitterNode(BaseNode):
name = "文本分割节点"
inputs = {"text": "string"}
outputs = {
"outline": "list", # 大纲部分列表
"requirement": "list", # 要求部分列表
"dialogue": "list", # 对话部分列表
"weak_guidance": "list" # 弱指引部分列表
}
async def run(self, text: str) -> Dict[str, List[str]]:
# 正则匹配三种括号内的内容
# 注意:此正则假设括号不嵌套,且没有转义字符
pattern = r'\{([^{}]*)\}|\(([^()]*)\)|“([^”]*)”'
outline = []
requirement = []
dialogue = []
weak_guidance = []
pos = 0
for match in re.finditer(pattern, text):
start, end = match.span()
# 处理匹配前的普通文本(弱指引)
if start > pos:
weak_part = text[pos:start].strip()
if weak_part:
weak_guidance.append(weak_part)
# 根据捕获组确定类型
if match.group(1) is not None: # 大括号
outline.append(match.group(1).strip())
elif match.group(2) is not None: # 小括号
requirement.append(match.group(2).strip())
elif match.group(3) is not None: # 中文引号
dialogue.append(match.group(3).strip())
pos = end
# 处理剩余的普通文本
if pos < len(text):
weak_part = text[pos:].strip()
if weak_part:
weak_guidance.append(weak_part)
return {
"outline": outline,
"requirement": requirement,
"dialogue": dialogue,
"weak_guidance": weak_guidance
}

View File

@@ -1,3 +1,12 @@
fastapi==0.104.1 fastapi==0.104.1
uvicorn[standard]==0.24.0 uvicorn[standard]==0.24.0
python-multipart==0.0.6 python-multipart==0.0.6
cryptography>=41.0.0
requests>=2.31.0
# LangChain for LLM integration (让 pip 自动解析兼容版本)
langchain>=0.1.0
langchain-openai>=0.0.5
langchain-anthropic>=0.1.1
openai>=1.12.0
anthropic>=0.23.0

View File

@@ -1,47 +0,0 @@
from ..core import config
from typing import Dict, List
# 使用配置中的 DATA_PATH 并添加 "chat" 子目录
ROOT_DIR = config.settings.DATA_PATH / "chat"
def get_all_role_and_chat() -> Dict[str, List[str]]:
"""
读取配置目录下的所有子文件夹,并收集每个子文件夹中的 JSONL 文件
返回:
dict: 字典结构,键是文件夹名称,值是该文件夹中的 JSONL 文件列表(仅文件名,无路径和后缀)
"""
result = {}
# 确保目标目录存在
if not ROOT_DIR.exists():
print(f"警告: 目录 {ROOT_DIR} 不存在")
return result
# 打印根目录路径和内容(调试用)
print(f"正在扫描目录: {ROOT_DIR}")
print(f"根目录内容: {list(ROOT_DIR.iterdir())}")
# 遍历根目录下的所有条目
for entry in ROOT_DIR.iterdir():
try:
# 只处理文件夹
if entry.is_dir():
print(f"处理文件夹: {entry.name}") # 调试信息
jsonl_files = []
# 遍历子文件夹中的所有文件
for file in entry.iterdir():
if file.is_file() and file.suffix == '.jsonl':
# 使用 file.stem 获取不带后缀的文件名
jsonl_files.append(file.stem)
print(f" 找到文件: {file.name}") # 调试信息
# 如果该文件夹中有 JSONL 文件,则添加到结果中
if jsonl_files:
result[entry.name] = jsonl_files
except Exception as e:
print(f"处理文件夹 {entry.name} 时出错: {str(e)}")
continue
return result

View File

@@ -1,152 +0,0 @@
import json
from datetime import datetime
from backend.core import config as cfg
from pathlib import Path
from ..core.items import ChatRequest
# 假设 ChatRequest 定义在这里或者从其他地方导入
# from backend.app.core.items import ChatRequest
async def save_input_to_json(chat_request: ChatRequest):
"""
保存消息到JSONL文件或处理重roll请求
参数:
chat_request: 包含消息详情的请求对象
"""
# 1. 从对象中提取属性
mes = chat_request.mes
role_name = chat_request.role_name
chat_name = chat_request.chat_name
name = chat_request.name
is_user = chat_request.is_user
floor_number = chat_request.floor_number
# stream, img_switch, table_switch 等虽然在这个函数逻辑中没用到,
# 但如果 ChatRequest 中有,也可以提取出来备用
# stream = chat_request.stream
# ...
config = cfg.settings
# 注意:这里要确保 role_name 和 chat_name 不为 None否则路径拼接会报错
# 建议在函数入口处增加校验,或者在 Pydantic 模型中设置为必填项
if not role_name or not chat_name:
raise ValueError("role_name and chat_name cannot be empty")
file_path = config.BASE_PATH / "data" / "chat" / role_name / f"{chat_name}.jsonl"
# 确保目录存在
Path(file_path).parent.mkdir(parents=True, exist_ok=True)
# 读取文件内容
try:
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
except FileNotFoundError:
lines = []
# 判断是否为重roll请求
is_regenerate = False
target_index = -1
if lines and floor_number > 0:
# 计算当前楼层号
current_floor = len(lines)
# 如果floor_number与当前楼层号相同则为重roll请求
if floor_number == current_floor:
# 找到最后一条非用户消息
for i in range(len(lines) - 1, -1, -1):
try:
line_data = json.loads(lines[i])
if not line_data.get('is_user', False):
is_regenerate = True
target_index = i
break
except json.JSONDecodeError:
continue
# 处理重roll逻辑
if is_regenerate:
# 解析目标消息
try:
target_message = json.loads(lines[target_index])
except json.JSONDecodeError:
raise ValueError(f"无法解析楼层 {floor_number} 的JSON数据")
# 初始化swipes数组
if target_message.get('swipes') is None:
target_message['swipes'] = []
# 将新回复添加到swipes数组
target_message['swipes'].append(mes)
# 更新swipe_id和content
target_message['swipes_id'] = len(target_message['swipes']) - 1
target_message['content'] = mes
# 更新文件内容
lines[target_index] = json.dumps(target_message, ensure_ascii=False) + '\n'
# 写回文件
with open(file_path, 'w', encoding='utf-8') as f:
f.writelines(lines)
return target_message
# 处理普通消息保存逻辑
else:
# 获取当前时间
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 构建消息对象
message = {
"role": role_name,
"chat": chat_name,
"content": mes,
"name": name,
"is_user": is_user,
"send_date": current_time,
"floor_number": len(lines) + 1, # 记录楼层号
"swipes": [],
"swipes_id": 0
}
# 追加到文件
with open(file_path, 'a', encoding='utf-8') as f:
f.write(json.dumps(message, ensure_ascii=False) + '\n')
return message
if __name__ == '__main__':
# 注意:为了在本地运行测试,你需要手动构造一个 ChatRequest 对象
# 或者临时修改函数签名以便直接传参测试
# 示例:假设 ChatRequest 是一个简单的类或 Pydantic 模型
class MockChatRequest:
def __init__(self, **kwargs):
self.mes = kwargs.get('mes')
self.role_name = kwargs.get('role_name')
self.chat_name = kwargs.get('chat_name')
self.name = kwargs.get('name')
self.is_user = kwargs.get('is_user')
self.floor_number = kwargs.get('floor_number')
# 测试重roll最后一条AI消息
import asyncio
async def test():
req = MockChatRequest(
mes="这是重roll后的新回复2",
role_name="test",
chat_name="111",
name="AI",
is_user=False,
floor_number=2
)
await save_input_to_json(req)
asyncio.run(test())

View File

@@ -1,201 +0,0 @@
# backend/app/workflows/llm_workflow.py
from typing import Dict, Any, List, Callable
from dataclasses import dataclass
from enum import Enum
class WorkflowStatus(Enum):
"""工作流状态枚举"""
INITIALIZED = "initialized"
RUNNING = "running"
COMPLETED = "completed"
FAILED = "failed"
PAUSED = "paused"
@dataclass
class WorkflowContext:
"""工作流上下文"""
data: Dict[str, Any]
status: WorkflowStatus = WorkflowStatus.INITIALIZED
metadata: Dict[str, Any] = None
def __post_init__(self):
if self.metadata is None:
self.metadata = {}
class WorkflowNode:
"""工作流节点声明"""
def __init__(
self,
name: str,
handler: Callable,
enabled: bool = True,
config: Dict[str, Any] = None
):
self.name = name # 节点的唯一标识符,用于区分不同的节点。
self.handler = handler # 一个可调用对象(函数或方法),这是节点实际执行的处理逻辑。
self.enabled = enabled # 布尔值,控制节点是否启用。默认为 True如果设置为 False节点将被跳过。
self.config = config or {} # 一个字典,用于存储节点的配置信息。默认为空字典。
self.next_nodes: List['WorkflowNode'] = [] # 一个节点列表,用于指定当前节点执行完成后应跳转到的下一个节点。默认为空列表,可能指向多分支。
def execute(self, context: WorkflowContext) -> WorkflowContext:
"""执行节点处理"""
if not self.enabled:
return context
try:
context = self.handler(context, self.config)
return context
except Exception as e:
context.status = WorkflowStatus.FAILED
context.metadata["error"] = str(e)
raise
class LLMWorkflow:
"""LLM工作流声明"""
def __init__(self):
self.nodes: List[WorkflowNode] = []
self._initialize_workflow()
def _initialize_workflow(self):
"""初始化工作流节点(仅声明,不实现)"""
# 输入节点
input_node = WorkflowNode(
name="input",
handler=self._input_handler
)
# 输入预处理节点(可开关)
preprocessing_node = WorkflowNode(
name="preprocessing",
handler=self._preprocessing_handler,
enabled=False
)
# RAG处理节点
rag_node = WorkflowNode(
name="rag",
handler=self._rag_handler
)
# 提示词组装节点
prompt_assembly_node = WorkflowNode(
name="prompt_assembly",
handler=self._prompt_assembly_handler
)
# LLM请求节点
llm_request_node = WorkflowNode(
name="llm_request",
handler=self._llm_request_handler
)
# 图像生成节点(可开关)
image_generation_node = WorkflowNode(
name="image_generation",
handler=self._image_generation_handler,
enabled=False
)
# 动态表格更新节点(可开关)
dynamic_table_node = WorkflowNode(
name="dynamic_table",
handler=self._dynamic_table_handler,
enabled=False
)
# 输出过滤节点
output_filter_node = WorkflowNode(
name="output_filter",
handler=self._output_filter_handler
)
# 输出节点
output_node = WorkflowNode(
name="output",
handler=self._output_handler
)
# 设置节点顺序(构建工作流)
self.nodes = [
input_node,
preprocessing_node,
rag_node,
prompt_assembly_node,
llm_request_node,
image_generation_node,
dynamic_table_node,
output_filter_node,
output_node
]
def execute(self, context: WorkflowContext) -> WorkflowContext:
"""执行工作流"""
context.status = WorkflowStatus.RUNNING
for node in self.nodes:
try:
context = node.execute(context)
# 如果工作流失败,停止执行
if context.status == WorkflowStatus.FAILED:
break
except Exception as e:
context.status = WorkflowStatus.FAILED
context.metadata["error"] = str(e)
break
if context.status != WorkflowStatus.FAILED:
context.status = WorkflowStatus.COMPLETED
return context
def enable_node(self, node_name: str, enabled: bool = True):
"""启用或禁用特定节点"""
for node in self.nodes:
if node.name == node_name:
node.enabled = enabled
return True
return False
# 以下是节点处理函数声明(仅声明,不实现)
def _input_handler(self, context: WorkflowContext, config: Dict[str, Any]) -> WorkflowContext:
"""输入节点处理函数"""
pass
def _preprocessing_handler(self, context: WorkflowContext, config: Dict[str, Any]) -> WorkflowContext:
"""输入预处理节点处理函数"""
pass
def _rag_handler(self, context: WorkflowContext, config: Dict[str, Any]) -> WorkflowContext:
"""RAG处理节点处理函数"""
pass
def _prompt_assembly_handler(self, context: WorkflowContext, config: Dict[str, Any]) -> WorkflowContext:
"""提示词组装节点处理函数"""
pass
def _llm_request_handler(self, context: WorkflowContext, config: Dict[str, Any]) -> WorkflowContext:
"""LLM请求节点处理函数"""
pass
def _image_generation_handler(self, context: WorkflowContext, config: Dict[str, Any]) -> WorkflowContext:
"""图像生成节点处理函数"""
pass
def _dynamic_table_handler(self, context: WorkflowContext, config: Dict[str, Any]) -> WorkflowContext:
"""动态表格更新节点处理函数"""
pass
def _output_filter_handler(self, context: WorkflowContext, config: Dict[str, Any]) -> WorkflowContext:
"""输出过滤节点处理函数"""
pass
def _output_handler(self, context: WorkflowContext, config: Dict[str, Any]) -> WorkflowContext:
"""输出节点处理函数"""
pass

View File

@@ -1,2 +0,0 @@
{"role": "test", "chat": "111", "content": "你好", "name": "用户", "is_user": true, "send_date": "2026-03-12 18:26:50", "floor_number": 1, "swipes": [], "swipes_id": 0}
{"role": "test", "chat": "111", "content": "这是重roll后的新回复2", "name": "AI", "is_user": false, "send_date": "2026-03-12 18:26:50", "floor_number": 2, "swipes": ["这是重roll后的新回复", "这是重roll后的新回复2"], "swipes_id": 1}

View File

@@ -0,0 +1,5 @@
{"user_name": "User", "character_name": "AI Dungeon Master", "integrity": "uuid-001", "chat_id_hash": "hash-001", "note_prompt": "你是一个经验丰富的D&D地下城主。", "note_interval": 0, "note_position": 0, "note_depth": 0, "note_role": 0, "extensions": {}, "timedWorldInfo": {}, "variables": {}, "tainted": false, "lastInContextMessageId": -1}
{"name": "User", "is_user": true, "is_system": false, "floor": 0, "send_date": "1700000000000", "mes": "你好,我想开始一个新的冒险。", "extra": {}, "swipes": [], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []}
{"name": "AI Dungeon Master", "is_user": false, "is_system": false, "floor": 1, "send_date": "1700000001000", "mes": "欢迎,冒险者。请告诉我你想扮演什么角色?", "extra": {"api": "openai", "model": "gpt-4"}, "swipes": ["欢迎,冒险者。请告诉我你想扮演什么角色?", "你好,旅行者。在这个奇幻世界中,你是谁?"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "api": "openai", "model": "gpt-4", "reasoning": null, "reasoning_duration": null, "reasoning_signature": null, "time_to_first_token": null, "bias": null}
{"name": "User", "is_user": true, "is_system": false, "floor": 2, "send_date": "1700000002000", "mes": "我想成为一名人类战士。", "extra": {}, "swipes": [], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []}
{"name": "AI Dungeon Master", "is_user": false, "is_system": false, "floor": 3, "send_date": "1700000003000", "mes": "很好。你站在喧闹的酒馆门口,手里握着一把旧长剑。你打算做什么?", "extra": {"api": "openai", "model": "gpt-4"}, "swipes": ["很好。你站在喧闹的酒馆门口,手里握着一把旧长剑。你打算做什么?", "明白了。作为一名人类战士,你正身处繁华的市集广场。你的下一步行动是?"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "api": "openai", "model": "gpt-4", "reasoning": null, "reasoning_duration": null, "reasoning_signature": null, "time_to_first_token": null, "bias": null}

View File

@@ -0,0 +1,5 @@
{"user_name": "Commander", "character_name": "XCOM AI", "integrity": "uuid-003", "chat_id_hash": "hash-003", "note_prompt": "你是一名XCOM基地的中央AI负责协助指挥官管理外星威胁。", "note_interval": 0, "note_position": 0, "note_depth": 0, "note_role": 0, "extensions": {}, "timedWorldInfo": {}, "variables": {}, "tainted": false, "lastInContextMessageId": -1}
{"name": "Commander", "is_user": true, "is_system": false, "floor": 0, "send_date": "1700000000000", "mes": "报告当前的外星活动情况。", "extra": {}, "swipes": [], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []}
{"name": "XCOM AI", "is_user": false, "is_system": false, "floor": 1, "send_date": "1700000001000", "mes": "指挥官,卫星侦测到在南美洲丛林中有高能反应。可能是外星着陆舱。", "extra": {"api": "openai", "model": "gpt-4"}, "swipes": ["指挥官,卫星侦测到在南美洲丛林中有高能反应。可能是外星着陆舱。", "警报。我们在非洲检测到异常信号,疑似外星绑架行动正在进行。"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "api": "openai", "model": "gpt-4", "reasoning": null, "reasoning_duration": null, "reasoning_signature": null, "time_to_first_token": null, "bias": null}
{"name": "Commander", "is_user": true, "is_system": false, "floor": 2, "send_date": "1700000002000", "mes": "派遣布拉德福上尉带领一个小队去调查。", "extra": {}, "swipes": [], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []}
{"name": "XCOM AI", "is_user": false, "is_system": false, "floor": 3, "send_date": "1700000003000", "mes": "指令已确认。天火运输机正在起飞。预计到达时间20分钟。", "extra": {"api": "openai", "model": "gpt-4"}, "swipes": ["指令已确认。天火运输机正在起飞。预计到达时间20分钟。", "收到。正在部署天火运输机。布拉德福上尉已登机。"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "api": "openai", "model": "gpt-4", "reasoning": null, "reasoning_duration": null, "reasoning_signature": null, "time_to_first_token": null, "bias": null}

View File

@@ -0,0 +1,5 @@
{"user_name": "Player", "character_name": "Game Master", "integrity": "uuid-002", "chat_id_hash": "hash-002", "note_prompt": "场景:赛博朋克风格的未来城市。", "note_interval": 0, "note_position": 0, "note_depth": 0, "note_role": 0, "extensions": {}, "timedWorldInfo": {}, "variables": {}, "tainted": false, "lastInContextMessageId": -1}
{"name": "Player", "is_user": true, "is_system": false, "floor": 0, "send_date": "1700000000000", "mes": "我检查我的义体状态。", "extra": {}, "swipes": [], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []}
{"name": "Game Master", "is_user": false, "is_system": false, "floor": 1, "send_date": "1700000001000", "mes": "你的视觉义眼显示系统正常,但左臂的伺服电机发出轻微的嗡嗡声,似乎需要维护。", "extra": {"api": "openai", "model": "gpt-4"}, "swipes": ["你的视觉义眼显示系统正常,但左臂的伺服电机发出轻微的嗡嗡声,似乎需要维护。", "系统自检完成。你的神经接口连接稳定,但义体排异反应指数略有上升。"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "api": "openai", "model": "gpt-4", "reasoning": null, "reasoning_duration": null, "reasoning_signature": null, "time_to_first_token": null, "bias": null}
{"name": "Player", "is_user": true, "is_system": false, "floor": 2, "send_date": "1700000002000", "mes": "我联系我的黑客朋友,问他知不知道哪里有靠谱的义体医生。", "extra": {}, "swipes": [], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []}
{"name": "Game Master", "is_user": false, "is_system": false, "floor": 3, "send_date": "1700000003000", "mes": "你的朋友回复说:'去下城区的老维克那里,虽然他的店看起来很破,但他手艺没得说。'", "extra": {"api": "openai", "model": "gpt-4"}, "swipes": ["你的朋友回复说:'去下城区的老维克那里,虽然他的店看起来很破,但他手艺没得说。'", "通讯接通。你的朋友告诉你:'别去连锁店,去太平间后巷找'扳手',他收费公道。'"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "api": "openai", "model": "gpt-4", "reasoning": null, "reasoning_duration": null, "reasoning_signature": null, "time_to_first_token": null, "bias": null}

View File

@@ -0,0 +1,5 @@
{"user_name": "Player", "character_name": "Narrator", "integrity": "uuid-004", "chat_id_hash": "hash-004", "note_prompt": "这是一个文字冒险游戏,你需要描述场景并等待玩家输入。", "note_interval": 0, "note_position": 0, "note_depth": 0, "note_role": 0, "extensions": {}, "timedWorldInfo": {}, "variables": {}, "tainted": false, "lastInContextMessageId": -1}
{"name": "Player", "is_user": true, "is_system": false, "floor": 0, "send_date": "1700000000000", "mes": "开始游戏。", "extra": {}, "swipes": [], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []}
{"name": "Narrator", "is_user": false, "is_system": false, "floor": 1, "send_date": "1700000001000", "mes": "你醒来时发现自己躺在一片陌生的森林里,四周弥漫着浓雾。你身边有一个背包和一把生锈的匕首。", "extra": {"api": "openai", "model": "gpt-4"}, "swipes": ["你醒来时发现自己躺在一片陌生的森林里,四周弥漫着浓雾。你身边有一个背包和一把生锈的匕首。", "当你睁开眼睛,发现自己身处一艘废弃的飞船中,应急灯闪烁着红光。你手里紧握着一个数据盘。"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "api": "openai", "model": "gpt-4", "reasoning": null, "reasoning_duration": null, "reasoning_signature": null, "time_to_first_token": null, "bias": null}
{"name": "Player", "is_user": true, "is_system": false, "floor": 2, "send_date": "1700000002000", "mes": "我打开背包看看里面有什么。", "extra": {}, "swipes": [], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []}
{"name": "Narrator", "is_user": false, "is_system": false, "floor": 3, "send_date": "1700000003000", "mes": "背包里有一块干硬的面包,一个水壶(里面还有半壶水),以及一张画着奇怪符号的羊皮纸。", "extra": {"api": "openai", "model": "gpt-4"}, "swipes": ["背包里有一块干硬的面包,一个水壶(里面还有半壶水),以及一张画着奇怪符号的羊皮纸。", "背包里只有一把激光手枪能量槽仅剩10%。还有一张写着'不要相信AI'的纸条。"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "api": "openai", "model": "gpt-4", "reasoning": null, "reasoning_duration": null, "reasoning_signature": null, "time_to_first_token": null, "bias": null}

View File

@@ -2,33 +2,54 @@ version: '3.8'
services: services:
backend: backend:
build: ./backend build:
command: uvicorn backend.main:app --host 0.0.0.0 --port 8000 --reload context: ./backend
ports: dockerfile: Dockerfile
- "3001:8000" container_name: llm-backend
command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload
volumes: volumes:
- ./backend:/app/backend - ./backend:/app
- ./data:/app/data - ./data:/app/data
- ./outputs:/outputs - ./outputs:/app/outputs
environment: environment:
- PYTHONUNBUFFERED=1 - PYTHONUNBUFFERED=1
- PYTHONDONTWRITEBYTECODE=1
restart: unless-stopped restart: unless-stopped
networks:
- llm-network
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
frontend: frontend:
build: build:
context: ./frontend-react context: ./frontend
dockerfile: Dockerfile dockerfile: Dockerfile
target: development
container_name: llm-frontend
ports: ports:
- "3000:5173" - "23338:5173"
volumes: volumes:
- ./frontend-react:/app - ./frontend:/app
- /app/node_modules - /app/node_modules
environment: environment:
# 如果不需要特定环境变量,可以完全移除 environment 部分
# 或者添加有效的环境变量,例如:
- NODE_ENV=development - NODE_ENV=development
- VITE_BACKEND_URL=http://backend:8000 - VITE_API_URL=http://backend:8000
- VITE_WS_URL=ws://backend:8000
command: sh -c "npm install && npm run dev -- --host 0.0.0.0" command: sh -c "npm install && npm run dev -- --host 0.0.0.0"
depends_on: depends_on:
- backend backend:
condition: service_healthy
restart: unless-stopped restart: unless-stopped
networks:
- llm-network
networks:
llm-network:
driver: bridge
volumes:
node_modules:

View File

@@ -1,26 +0,0 @@
# 使用 Node.js 18 Alpine 镜像作为基础
# 必须明确指定 node 版本,否则可能默认为空或 python 镜像
FROM node:18-alpine
# 设置工作目录
WORKDIR /app
# 设置 npm 镜像源(可选,国内推荐使用,加速依赖下载)
RUN npm config set registry https://registry.npmmirror.com/
# 复制 package.json 和 package-lock.json
# 利用 Docker 缓存层,只有依赖变更时才重新安装
COPY package.json package-lock.json* ./
# 安装依赖
RUN npm install
# 复制源代码到容器
COPY . .
# 暴露 Vite 默认端口 5173
EXPOSE 5173
# 启动 Vite 开发服务器
# --host 0.0.0.0 允许外部访问Docker 容器外)
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]

View File

@@ -1,15 +0,0 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React App</title>
</head>
<body>
<!-- React 应用将挂载到这个 div 上 -->
<div id="root"></div>
<!-- Vite 会自动注入这里的脚本标签 -->
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -1,32 +0,0 @@
{
"name": "ai-chat-frontend",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"highlight.js": "^11.11.1",
"katex": "^0.16.38",
"marked": "^17.0.4",
"marked-highlight": "^2.2.3",
"mermaid": "^11.13.0",
"react": "^18.3.1",
"react-copy-to-clipboard": "^5.1.1",
"react-dom": "^18.3.1",
"react-markdown": "^10.1.0",
"react-syntax-highlighter": "^16.1.1",
"rehype-katex": "^7.0.1",
"remark-math": "^6.0.0",
"zustand": "^4.4.7"
},
"devDependencies": {
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"@vitejs/plugin-react": "^4.2.1",
"vite": "^5.0.8"
}
}

View File

@@ -1,59 +0,0 @@
import React, { useState } from 'react';
import Toolbar from './components/ToolBar/ToolBar';
import ChatBox from './components/ChatBox/ChatBox';
import DicePanel from './components/DicePanel/DicePanel';
import ImageDisplay from './components/ImageDisplay/ImageDisplay';
import PresetPanel from './components/PresetPanel/PresetPanel';
import './index.css';
function App() {
const [selectedRole, setSelectedRole] = useState(null);
const [selectedChat, setSelectedChat] = useState(null);
const handleRoleChange = (role) => {
setSelectedRole(role);
console.log('角色已更改:', role);
};
const handleChatChange = (role, chat) => {
setSelectedChat(chat);
console.log('聊天已更改:', role, chat);
};
return (
<div className="app">
<Toolbar
onRoleChange={handleRoleChange}
onChatChange={handleChatChange}
/>
{/* 主内容容器 */}
<div className="main-container">
{/* 左侧栏 - 预设面板 */}
<div className="sidebar-left">
<PresetPanel />
</div>
{/* 中间栏:聊天框 */}
<div className="chat-area">
<ChatBox
selectedRole={selectedRole}
selectedChat={selectedChat}
/>
</div>
{/* 右侧栏 */}
<div className="sidebar-right">
<div className="right-top">
<ImageDisplay /> {/* 图片展示放在顶部 */}
</div>
<div className="right-bottom">
<DicePanel /> {/* 骰子面板放在底部 */}
</div>
</div>
</div>
</div>
);
}
export default App;

View File

@@ -1,202 +0,0 @@
import { create } from 'zustand';
// 异步获取角色数据
const fetchRoleData = async () => {
try {
const response = await fetch('/api/tool_bar/get_all_role_and_chat', {
method: 'GET',
headers: {
'Cache-Control': 'no-cache',
'Pragma': 'no-cache',
'Expires': '0'
}
});
const data = await response.json();
return data;
} catch (error) {
console.error('获取角色数据失败:', error);
throw error;
}
};
const useRoleSelectorStore = create((set, get) => ({
// 状态
roleData: {},
selectedRole: null,
selectedChat: null,
hoveredRole: null,
clickedRole: null,
isLoading: false,
searchTerm: '',
editingRole: null,
editingChat: null,
showDeleteConfirm: null,
deleteType: null,
// 操作
fetchRoleData: async () => {
set({ isLoading: true });
try {
const data = await fetchRoleData();
set({ roleData: data, isLoading: false });
} catch (error) {
set({ isLoading: false });
console.error('获取角色数据失败:', error);
}
},
setSelectedRole: (role) => set({ selectedRole: role }),
setSelectedChat: (chat) => set({ selectedChat: chat }),
setHoveredRole: (role) => set({ hoveredRole: role }),
setClickedRole: (role) => set({ clickedRole: role }),
setSearchTerm: (term) => set({ searchTerm: term }),
setEditingRole: (role) => set({ editingRole: role }),
setEditingChat: (chat) => set({ editingChat: chat }),
setShowDeleteConfirm: (name) => set({ showDeleteConfirm: name }),
setDeleteType: (type) => set({ deleteType: type }),
handleRenameRole: (oldName, newName) => {
const { roleData, selectedRole } = get();
if (newName && newName !== oldName) {
const newRoleData = { ...roleData };
const chats = newRoleData[oldName];
delete newRoleData[oldName];
newRoleData[newName] = chats;
if (selectedRole === oldName) {
set({
roleData: newRoleData,
selectedRole: newName,
editingRole: null
});
} else {
set({
roleData: newRoleData,
editingRole: null
});
}
} else {
set({ editingRole: null });
}
},
handleRenameChat: (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;
if (selectedChat === oldName) {
set({
roleData: newRoleData,
selectedChat: newName,
editingChat: null
});
} else {
set({
roleData: newRoleData,
editingChat: null
});
}
} else {
set({ editingChat: null });
}
} else {
set({ editingChat: null });
}
},
confirmDelete: () => {
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) {
set({
roleData: newRoleData,
selectedChat: null,
showDeleteConfirm: null,
deleteType: null
});
} else {
set({
roleData: newRoleData,
showDeleteConfirm: null,
deleteType: null
});
}
} else {
set({
showDeleteConfirm: null,
deleteType: null
});
}
}
},
cancelDelete: () => set({ showDeleteConfirm: null, deleteType: null }),
handleAddRole: () => {
const { roleData } = get();
const newRole = '新角色';
const newRoleData = { ...roleData };
newRoleData[newRole] = [];
set({
roleData: newRoleData,
editingRole: newRole
});
},
handleAddChat: () => {
const { roleData, selectedRole } = get();
if (!selectedRole) return;
const newChat = '新聊天';
const newRoleData = { ...roleData };
newRoleData[selectedRole].push(newChat);
set({
roleData: newRoleData,
editingChat: newChat
});
},
resetPanel: () => set({
hoveredRole: null,
clickedRole: null,
editingRole: null,
editingChat: null,
showDeleteConfirm: null
})
}));
export default useRoleSelectorStore;

View File

@@ -1,2 +0,0 @@
// frontend-react/src/store/index.js
export { default as useRoleSelectorStore } from './roleSelectorStore';

View File

@@ -1,405 +0,0 @@
/* ==================== 聊天框区域 ==================== */
/* React 组件根容器 */
.chat-box {
height: 100%; /* 修改为100%,填满父容器 */
width: 100%;
display: flex;
flex-direction: column;
background-color: #fafafa;
overflow: hidden; /* 防止内容溢出 */
}
/* 消息列表容器 */
.chat-messages {
flex: 1;
min-height: 0;
max-height: calc(95vh - 62px); /* 减去输入区域的高度 */
overflow-y: auto;
padding: 20px;
padding-top: 60px;
display: flex;
flex-direction: column;
gap: 15px;
}
/* 设置面板 */
.settings-panel {
position: absolute;
top: 0;
right: 0;
z-index: 20;
background-color: #fff;
border-bottom-left-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
overflow: hidden;
transition: all 0.3s ease;
max-width: 200px;
}
.settings-panel.collapsed {
width: 40px;
height: 40px;
cursor: pointer;
}
.settings-panel.expanded {
width: auto;
min-width: 150px;
padding: 10px;
border: 1px solid #eee;
border-top: none;
border-right: none;
}
.settings-header {
height: 40px;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
background-color: #f0f0f0;
cursor: pointer;
font-size: 18px;
color: #666;
}
.settings-panel.collapsed .settings-header:hover {
background-color: #e0e0e0;
}
.settings-options {
display: none;
flex-direction: column;
gap: 10px;
padding-top: 10px;
}
.settings-panel.expanded .settings-options {
display: flex;
}
.setting-item {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
color: #333;
}
.setting-item input[type="checkbox"] {
margin-right: 8px;
cursor: pointer;
}
/* ==================== 聊天气泡样式 ==================== */
/* 消息行容器 */
.message {
display: flex;
width: 100%;
margin-bottom: 10px;
}
/* 区分左右布局 */
.message.user {
justify-content: flex-end; /* 用户消息靠右 */
}
.message.ai {
justify-content: flex-start; /* AI 消息靠左 */
}
/* 消息容器 - 调整为垂直排列 */
.message-container {
display: flex;
flex-direction: column;
max-width: 70%;
}
/* 消息头部 - 包含名称和工具栏 */
.message-header {
display: flex;
align-items: center;
margin-bottom: 5px;
order: 1; /* 确保头部显示在第一个位置 */
}
/* 消息名称 */
.message-name {
font-size: 14px;
font-weight: 500;
color: #333;
padding: 2px 8px;
border-radius: 12px;
background-color: rgba(0, 0, 0, 0.05);
display: inline-block;
}
/* 用户消息名称 */
.message.user .message-name {
background-color: rgba(24, 144, 255, 0.1);
color: #1890ff;
}
/* AI消息名称 */
.message.ai .message-name {
background-color: rgba(0, 0, 0, 0.05);
color: #333;
}
/* 消息工具栏 - 优化布局使图标更紧凑 */
.message-toolbar {
display: flex;
align-items: center;
padding: 0 5px;
opacity: 0;
transition: opacity 0.2s ease;
}
.message:hover .message-toolbar {
opacity: 1;
}
.message-id {
font-size: 12px;
color: #888;
padding: 2px 6px;
border-radius: 4px;
background-color: rgba(0, 0, 0, 0.05);
display: flex;
align-items: center;
}
.toolbar-buttons {
display: flex;
gap: 3px; /* 减少图标之间的间距 */
margin: 0 5px;
}
.toolbar-button {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border: none;
background-color: rgba(0, 0, 0, 0.05);
border-radius: 4px;
cursor: pointer;
color: #555;
transition: all 0.2s ease;
}
.toolbar-button:hover {
background-color: rgba(0, 0, 0, 0.1);
color: #333;
}
/* AI消息头部布局AI助手 - ID - 编辑 - 更多 */
.message.ai .message-header {
flex-direction: row;
justify-content: flex-start;
}
.message.ai .message-name {
order: 1;
}
.message.ai .message-id {
order: 2;
margin-left: 5px;
}
.message.ai .toolbar-buttons {
order: 3;
margin-left: 5px;
}
/* 用户消息头部布局:更多 - 编辑 - ID - 我 */
.message.user .message-header {
flex-direction: row;
justify-content: flex-end;
}
.message.user .message-name {
order: 4;
}
.message.user .message-id {
order: 3;
margin-right: 5px;
}
.message.user .toolbar-buttons {
order: 1;
margin-right: 5px;
}
/* 消息内容 - 确保显示在名称下方 */
.message-content {
display: flex;
flex-direction: column;
max-width: 100%;
order: 2; /* 确保内容显示在第二个位置 */
}
/* 气泡本体 */
.message .bubble {
max-width: 100%;
padding: 10px 15px;
border-radius: 12px;
position: relative;
font-size: 14px;
line-height: 1.6;
word-wrap: break-word;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
/* AI 气泡样式 */
.message.ai .bubble {
background-color: #fff;
color: #333;
border-top-left-radius: 2px;
border-bottom-left-radius: 2px;
border: 1px solid #eee;
}
/* 用户气泡样式 */
.message.user .bubble {
background-color: #1890ff;
color: #fff;
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
}
/* 针对 AI 消息中的 HTML 内容进行简单的样式重置 */
.message.ai .bubble p {
margin: 0 0 8px 0;
}
.message.ai .bubble p:last-child {
margin-bottom: 0;
}
.message.ai .bubble ul, .message.ai .bubble ol {
margin: 0 0 8px 0;
padding-left: 20px;
}
.message.ai .bubble b {
font-weight: 600;
color: #000;
}
/* 编辑模式 */
.edit-container {
display: flex;
flex-direction: column;
gap: 10px;
}
.edit-textarea {
width: 100%;
min-height: 80px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 8px;
resize: vertical;
font-family: inherit;
font-size: 14px;
line-height: 1.5;
}
.edit-textarea:focus {
outline: none;
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.edit-buttons {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.save-button, .cancel-button {
display: flex;
align-items: center;
gap: 4px;
padding: 6px 12px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
}
.save-button {
background-color: #1890ff;
color: white;
}
.save-button:hover {
background-color: #40a9ff;
}
.cancel-button {
background-color: #f0f0f0;
color: #333;
}
.cancel-button:hover {
background-color: #e6e6e6;
}
/* ==================== 输入区域 ==================== */
.chat-input-wrapper {
background-color: #fff;
border-top: 1px solid #ddd;
padding: 10px 20px;
height: 62px; /* 明确设置高度 */
flex-shrink: 0; /* 防止被压缩 */
display: flex;
align-items: flex-end;
gap: 10px;
width: 100%;
position: relative;
z-index: 10;
}
.chat-input-area {
flex: 1;
display: flex;
}
.chat-input-area textarea {
width: 100%;
height: 42px;
min-height: 42px;
max-height: 300px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
resize: none;
outline: none;
font-family: inherit;
line-height: 1.5;
overflow-y: auto;
display: block;
}
.send-button {
height: 42px;
padding: 0 20px;
background-color: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
white-space: nowrap;
flex-shrink: 0;
}
.send-button:hover {
background-color: #40a9ff;
}

View File

@@ -1,167 +0,0 @@
import React, { useState, useRef } from 'react';
import './ChatBox.css';
const ChatBox = ({ selectedRole, selectedChat }) => {
const [isHtmlRender, setIsHtmlRender] = useState(false);
const [isImageGen, setIsImageGen] = useState(false);
const [isDynamicTable, setIsDynamicTable] = useState(false);
const [editingId, setEditingId] = useState(null);
const [editContent, setEditContent] = useState('');
const textareaRef = useRef(null);
// 自动调整 Textarea 高度
const adjustHeight = () => {
const textarea = textareaRef.current;
const wrapper = textarea?.closest('.chat-input-wrapper');
if (textarea && wrapper) {
textarea.style.height = '42px';
wrapper.style.height = 'auto';
const newHeight = textarea.scrollHeight;
if (newHeight > 42) {
textarea.style.height = newHeight + 'px';
} else {
textarea.style.height = '42px';
}
}
};
const handleInput = () => {
adjustHeight();
};
// 开始编辑消息
const startEdit = (id, content) => {
setEditingId(id);
setEditContent(content);
};
// 保存编辑的消息
const saveEdit = (id) => {
// 这里可以添加向后端发送请求的代码
console.log(`保存消息 ${id} 的编辑内容: ${editContent}`);
// 更新前端显示
const messageIndex = messages.findIndex(msg => msg.id === id);
if (messageIndex !== -1) {
messages[messageIndex].content = editContent;
}
// 退出编辑模式
setEditingId(null);
setEditContent('');
};
// 取消编辑
const cancelEdit = () => {
setEditingId(null);
setEditContent('');
};
// 生成示例数据
const generateMessages = () => {
const messages = [];
for (let i = 1; i <= 150; i++) {
const isUser = i % 2 !== 0;
messages.push({
id: i,
role: isUser ? 'user' : 'ai',
name: isUser ? '我' : 'AI助手',
content: isUser
? `这是第 ${i} 条用户消息。这是一段比较长的文本,用来测试气泡的换行效果以及滚动条的表现。`
: `这是第 ${i} 条 AI 回复。<b>包含 HTML 标签</b>的内容。如果渲染开关开启,这里应该显示粗体字。如果不开启,应该显示原始标签。`
});
}
return messages;
};
const messages = generateMessages();
return (
<div className="chat-box">
{/* 上方:消息列表区域 */}
<div className="chat-messages">
{/* 消息列表 */}
{messages.map((msg) => (
<div key={msg.id} className={`message ${msg.role}`}>
<div className="message-container">
{/* 消息名称和工具栏在同一行 */}
<div className="message-header">
<div className="message-name">{msg.name}</div>
{/* 消息工具栏 */}
<div className="message-toolbar">
<span className="message-id">ID: {msg.id}</span>
<div className="toolbar-buttons">
<button
className="toolbar-button edit-button"
onClick={() => startEdit(msg.id, msg.content)}
>
编辑
</button>
<button className="toolbar-button expand-button">
</button>
</div>
</div>
</div>
{/* 消息内容 */}
<div className="message-content">
<div className="bubble">
{editingId === msg.id ? (
<div className="edit-container">
<textarea
value={editContent}
onChange={(e) => setEditContent(e.target.value)}
className="edit-textarea"
/>
<div className="edit-buttons">
<button
className="save-button"
onClick={() => saveEdit(msg.id)}
>
保存
</button>
<button
className="cancel-button"
onClick={cancelEdit}
>
取消
</button>
</div>
</div>
) : (
isHtmlRender ? (
<div dangerouslySetInnerHTML={{ __html: msg.content }} />
) : (
<div>{msg.content}</div>
)
)}
</div>
</div>
</div>
</div>
))}
</div>
{/* 下方:输入框区域 */}
<div className="chat-input-wrapper">
<div className="chat-input-area">
<textarea
ref={textareaRef}
placeholder="输入消息..."
onInput={handleInput}
rows="1"
style={{ height: '42px' }}
/>
</div>
<button className="send-button">发送</button>
</div>
</div>
);
};
export default ChatBox;

View File

@@ -1,11 +0,0 @@
import React from 'react';
const DicePanel = () => {
return (
<div className="dice-panel">
<div>骰子区</div>
</div>
);
};
export default DicePanel;

View File

@@ -1,11 +0,0 @@
import React from 'react';
const ImageDisplay = () => {
return (
<div className="image-display">
<div>图片展示区</div>
</div>
);
};
export default ImageDisplay;

View File

@@ -1,146 +0,0 @@
// npm install react react-dom react-markdown react-syntax-highlighter remark-math rehype-katex react-copy-to-clipboard mermaid katex
import React, { useState, useCallback, useEffect } from "react";
import ReactMarkdown from "react-markdown";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { materialLight } from "react-syntax-highlighter/dist/cjs/styles/prism";
import remarkMath from "remark-math";
import rehypeKatex from "rehype-katex";
import { CopyToClipboard } from "react-copy-to-clipboard";
import mermaid from "mermaid";
import "katex/dist/katex.min.css";
// Mermaid 初始化配置
// Mermaid是一个画图的语言
mermaid.initialize({
startOnLoad: false,
theme: "default",
securityLevel: "loose",
});
const DownSvg = () => (
<svg width="12" height="12" viewBox="0 0 24 24">
<path d="M7 10l5 5 5-5z" fill="currentColor" />
</svg>
);
const CopySvg = () => (
<svg width="14" height="14" viewBox="0 0 24 24">
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z" fill="currentColor" />
</svg>
);
const MermaidChart= ({ code }) => {
const [svg, setSvg] = useState("");
const [id] = useState(() => `mermaid-${Math.random().toString(36).substr(2, 9)}`);
useEffect(() => {
try {
mermaid.parse(code);
mermaid.render(id, code).then(({ svg }) => setSvg(svg));
} catch (err) {
setSvg(`<pre>Error rendering mermaid: ${err}</pre>`);
}
}, [code, id]);
return <div dangerouslySetInnerHTML={{ __html: svg }} />;
};
const MarkdownToHTML= ({ markdownRAW, className }) => {
const [isMermaidLoaded] = useState(true);
// 渲染代码块的方法
const CodeBlock = useCallback(
({ node, inline, className, children, ...props } ) => {
const [isShowCode, setIsShowCode] = useState(true); // 添加展开与收起代码块状态
const [isShowCopy, setIsShowCopy] = useState(false);// 添加点击复制的状态
const match = /language-(\w+)/.exec(className || ""); // 这是用来匹配代码块对应语言的方法
const codeContent = String(children).replace(/\n$/, "");
// 处理复制成功提示
const handleCopy = () => {
setIsShowCopy(true);
setTimeout(() => setIsShowCopy(false), 1500);
};
//处理单行代码
if (inline) {
return <code className={className} {...props}>{children}</code>;
}
// 处理 Mermaid 图表代码
if (match?.[1] === "mermaid" && isMermaidLoaded) {
return (
<div style={{ position: "relative", margin: "20px 0" }}>
<div className="code-header">
<div
style={{ cursor: "pointer", marginRight: "10px", transformOrigin: "8px" }}
className={isShowCode ? "code-rotate-down" : "code-rotate-right"}
onClick={() => setIsShowCode(!isShowCode)}
>
<DownSvg />
</div>
<div>{match[1]}</div>
<CopyToClipboard text={codeContent} onCopy={handleCopy}>
<div className="preview-code-copy" style={{ cursor: "pointer" }}>
{isShowCopy && <span className="copy-success"> 复制成功</span>}
<CopySvg />
</div>
</CopyToClipboard>
</div>
{isShowCode && <MermaidChart code={codeContent} />}
</div>
);
}
return (
<div style={{ position: "relative", padding:"0"}}>
<div className="code-header">
<div
style={{ cursor: "pointer", marginRight: "10px", transformOrigin: "8px" }}
className={isShowCode ? "code-rotate-down" : "code-rotate-right"}
onClick={() => setIsShowCode(!isShowCode)}
>
<DownSvg style={{
transform: isShowCode ? "rotate(0deg)" : "rotate(-90deg)",
transition: "transform 0.2s"
}} />
</div>
<div>{match?.[1] || "code"}</div>
<CopyToClipboard text={codeContent} onCopy={handleCopy}>
<div className="preview-code-copy" style={{ cursor: "pointer" }}>
{isShowCopy && <span className="copy-success"> 复制成功</span>}
<CopySvg />
</div>
</CopyToClipboard>
</div>
{isShowCode && (
<SyntaxHighlighter
style={materialLight}
language={match?.[1] || "text"}
PreTag="div"
showLineNumbers
{...props}
>
{codeContent}
</SyntaxHighlighter>
)}
</div>
);
},
[isMermaidLoaded]
);
return (
<div
className={className} >
<ReactMarkdown
remarkPlugins={[remarkMath]}
rehypePlugins={[rehypeKatex]}
components={{ code: CodeBlock }}
>
{markdownRAW}
</ReactMarkdown>
</div>
);
};
export default MarkdownToHTML;

View File

@@ -1,25 +0,0 @@
// 下面这些包如果显示不存在则运行这个代码安装依赖:
// npm install marked marked-highlight highlight.js
import { marked } from "marked";
import { markedHighlight } from "marked-highlight";
import hljs from "highlight.js";
import "highlight.js/styles/base16/github.css";
// 配置 marked 和 markedHighlight
marked.use(
markedHighlight({
langPrefix: "hljs language-",
highlight(code, lang) {
const language = hljs.getLanguage(lang) ? lang : "plaintext"; // 如果语言不支持,回退为纯文本
return hljs.highlight(code, { language }).value; // 使用 highlight.js 高亮代码
},
})
);
// Markdown 解析函数
const MarkdownRenderer = (markdownString)=> {
const res= marked.parse(markdownString).toString()
return res; // 将 Markdown 转换为 HTML
};
export default MarkdownRenderer;

View File

@@ -1,57 +0,0 @@
/* ==================== 顶部工具栏区域 ==================== */
.toolbar {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 50px;
background-color: #fff;
border-bottom: 1px solid #ddd;
padding: 0;
display: flex;
align-items: center;
justify-content: flex-end;
z-index: 100;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
transition: height 0.3s ease;
}
.toolbar.expanded {
height: auto;
max-height: 300px;
overflow-y: auto;
padding-bottom: 10px;
}
.toolbar-content {
display: none;
width: 100%;
padding: 10px 20px;
}
.toolbar.expanded .toolbar-content {
display: block;
}
.toolbar-toggle-btn {
position: absolute;
right: 0;
top: 0;
height: 50px;
width: 50px;
background: none;
border: none;
border-left: 1px solid #ddd;
cursor: pointer;
font-size: 0.8rem;
color: #666;
display: flex;
align-items: center;
justify-content: center;
z-index: 101;
}
.toolbar-toggle-btn:hover {
background-color: #f5f5f5;
}

View File

@@ -1,30 +0,0 @@
import React from 'react';
import './PresetPanel.css';
const PresetPanel = () => {
return (
<div className="preset-panel">
{/* 顶部:预设选择与输入 */}
<div className="preset-header">
{/* 下拉框 */}
<select className="preset-select">
<option>选择预设...</option>
</select>
{/* 输入框组(待定) */}
<div className="preset-inputs">
{/* inputs here */}
</div>
</div>
{/* 下方:大槽位区域 */}
<div className="preset-slots">
{/* 槽位列表 */}
<div className="slot-item">槽位 1</div>
<div className="slot-item">槽位 2</div>
{/* ... */}
</div>
</div>
);
};
export default PresetPanel;

View File

@@ -1,533 +0,0 @@
/* ==================== 角色选择器容器 ==================== */
.role-selector {
width: 100%;
height: 100%;
padding: 24px;
box-sizing: border-box;
display: flex;
flex-direction: column;
background-color: #f8fafc;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
/* 添加微妙的背景纹理 */
background-image: radial-gradient(#e5e7eb 1px, transparent 1px);
background-size: 20px 20px;
/* 添加平滑滚动 */
scroll-behavior: smooth;
/* 优化移动端体验 */
-webkit-overflow-scrolling: touch;
}
/* ==================== 当前选中角色显示 ==================== */
.selected-role-display {
padding: 16px 24px;
border-radius: 12px;
/* 更丰富的渐变效果 */
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
margin-bottom: 24px;
/* 增强阴影效果 */
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.25), 0 0 0 1px rgba(255, 255, 255, 0.1) inset;
display: flex;
align-items: center;
justify-content: space-between;
/* 添加微妙的动画效果 */
transition: all 0.3s ease;
}
/* 悬停效果 */
.selected-role-display:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.2) inset;
}
.role-badge {
display: flex;
align-items: center;
font-size: 15px;
}
.role-badge.empty {
color: rgba(255, 255, 255, 0.8);
}
.role-label {
font-weight: 600;
margin-right: 8px;
color: rgba(255, 255, 255, 0.9);
}
.role-name {
color: #fff;
font-weight: 600;
}
.chat-name {
color: rgba(255, 255, 255, 0.85);
margin-left: 8px;
}
/* ==================== 搜索栏样式 ==================== */
.search-bar {
margin-bottom: 20px;
position: relative;
}
.search-bar input {
width: 100%;
padding: 12px 16px 12px 40px; /* 为搜索图标留出空间 */
border: 2px solid transparent;
border-radius: 10px;
font-size: 14px;
transition: all 0.3s ease;
box-sizing: border-box;
background-color: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
/* 添加搜索图标 */
.search-bar::before {
content: '🔍';
position: absolute;
left: 14px;
top: 50%;
transform: translateY(-50%);
font-size: 16px;
opacity: 0.5;
}
.search-bar input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
}
.search-bar input::placeholder {
color: #a0a0a0;
}
/* ==================== 角色列表样式 ==================== */
.role-list {
flex: 1;
overflow-y: auto;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); /* 响应式网格布局 */
gap: 16px;
padding-right: 8px;
padding-bottom: 8px;
/* 自定义滚动条样式 */
scrollbar-width: thin;
scrollbar-color: #cbd5e1 transparent;
}
/* Webkit浏览器滚动条样式 */
.role-list::-webkit-scrollbar {
width: 6px;
}
.role-list::-webkit-scrollbar-track {
background: transparent;
}
.role-list::-webkit-scrollbar-thumb {
background-color: #cbd5e1;
border-radius: 3px;
}
.role-item {
border: none;
border-radius: 14px;
background-color: #fff;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
flex-direction: column;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
overflow: hidden;
min-height: 140px;
position: relative;
/* 添加微妙的边框效果 */
border: 1px solid rgba(0, 0, 0, 0.05);
}
.role-item:hover {
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(102, 126, 234, 0.15);
}
.role-item.active {
border: 2px solid #667eea;
background-color: #f5f7ff;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
}
.role-header {
padding: 16px;
display: flex;
justify-content: space-between;
align-items: center;
flex: 1;
}
.role-header .role-name {
font-size: 15px;
font-weight: 600;
color: #1f2937;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.role-item.active .role-header .role-name {
color: #667eea;
}
.role-actions {
display: flex;
gap: 4px;
position: absolute;
top: 12px;
right: 12px;
opacity: 0;
transition: opacity 0.2s ease;
}
.role-item:hover .role-actions {
opacity: 1;
}
.icon-btn {
background: rgba(255, 255, 255, 0.95);
border: none;
cursor: pointer;
font-size: 14px;
padding: 6px;
border-radius: 8px;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}
.icon-btn:hover {
background-color: #fff;
transform: scale(1.1);
}
.icon-btn.delete:hover {
background-color: #fff1f0;
color: #ff4d4f;
}
/* ==================== 聊天列表样式 ==================== */
.chat-list {
border-top: 1px solid #f0f0f0;
padding: 8px 0;
background-color: #fafbfc;
max-height: 140px;
overflow-y: auto;
/* 自定义滚动条样式 */
scrollbar-width: thin;
scrollbar-color: #cbd5e1 transparent;
}
/* Webkit浏览器滚动条样式 */
.chat-list::-webkit-scrollbar {
width: 4px;
}
.chat-list::-webkit-scrollbar-track {
background: transparent;
}
.chat-list::-webkit-scrollbar-thumb {
background-color: #cbd5e1;
border-radius: 2px;
}
.chat-item {
padding: 10px 16px 10px 20px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
transition: all 0.2s ease;
/* 添加微妙的左边框指示器 */
border-left: 3px solid transparent;
}
.chat-item:hover {
background-color: #f0f2f5;
}
.chat-item.active {
background-color: #eef1ff;
color: #667eea;
/* 激活状态添加左边框指示器 */
border-left-color: #667eea;
}
.chat-item .chat-name {
font-size: 13px;
font-weight: 500;
color: #4b5563;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chat-item.active .chat-name {
font-weight: 600;
color: #667eea;
}
/* ==================== 加载和空状态 ==================== */
.loading,
.empty-state {
display: flex;
align-items: center;
justify-content: center;
padding: 48px;
color: #9ca3af;
font-size: 14px;
grid-column: 1 / -1;
flex-direction: column;
gap: 16px;
}
/* 添加加载动画 */
.loading::before {
content: '';
width: 32px;
height: 32px;
border: 3px solid #e5e7eb;
border-top-color: #667eea;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* 添加空状态图标 */
.empty-state::before {
content: '📭';
font-size: 48px;
opacity: 0.5;
}
/* ==================== 模态框样式 ==================== */
.delete-confirm-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
animation: fadeIn 0.2s ease-out;
/* 添加微妙的背景动画 */
background-image: radial-gradient(circle at center, rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.4) 100%);
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.modal-content {
background-color: #fff;
border-radius: 16px;
padding: 28px;
width: 90%;
max-width: 420px;
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.15);
animation: slideUp 0.3s cubic-bezier(0.4, 0, 0.2, 1);
/* 添加微妙的边框效果 */
border: 1px solid rgba(255, 255, 255, 0.2);
}
@keyframes slideUp {
from {
transform: translateY(24px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.modal-content h3 {
margin: 0 0 12px 0;
font-size: 18px;
font-weight: 600;
color: #1f2937;
}
.modal-content p {
margin-bottom: 28px;
color: #6b7280;
font-size: 15px;
line-height: 1.6;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
}
.modal-actions button {
padding: 11px 24px;
border-radius: 10px;
border: none;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease;
/* 添加微妙的阴影效果 */
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.modal-actions button:first-child {
background-color: #f3f4f6;
color: #4b5563;
}
.modal-actions button:first-child:hover {
background-color: #e5e7eb;
transform: translateY(-1px);
}
.modal-actions button.danger {
background-color: #ef4444;
color: white;
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.3);
}
.modal-actions button.danger:hover {
background-color: #f87171;
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4);
transform: translateY(-1px);
}
/* ==================== 响应式设计 ==================== */
@media (max-width: 768px) {
.role-selector {
padding: 16px;
}
.role-list {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 12px;
}
.role-item {
border: none;
border-radius: 14px;
background-color: #fff;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
flex-direction: column;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
overflow: hidden;
min-height: 160px; /* 修改:增加最小高度,使其更适配宽度 */
position: relative;
/* 添加微妙的边框效果 */
border: 1px solid rgba(0, 0, 0, 0.05);
}
.modal-content {
width: 95%;
padding: 20px;
}
}
@media (max-width: 480px) {
.role-list {
grid-template-columns: 1fr;
}
.selected-role-display {
padding: 12px 16px;
}
.modal-actions {
flex-direction: column;
}
.modal-actions button {
width: 100%;
}
}
/* ==================== 暗色模式支持 ==================== */
@media (prefers-color-scheme: dark) {
.role-selector {
background-color: #1f2937;
background-image: radial-gradient(#374151 1px, transparent 1px);
}
.role-item {
background-color: #374151;
border-color: rgba(255, 255, 255, 0.1);
}
.role-item:hover {
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
}
.role-header .role-name {
color: #f9fafb;
}
.chat-list {
background-color: #374151;
border-top-color: #4b5563;
}
.chat-item:hover {
background-color: #4b5563;
}
.chat-item .chat-name {
color: #d1d5db;
}
.search-bar input {
background-color: #374151;
color: #f9fafb;
}
.modal-content {
background-color: #374151;
border-color: rgba(255, 255, 255, 0.1);
}
.modal-content h3 {
color: #f9fafb;
}
.modal-content p {
color: #d1d5db;
}
}

View File

@@ -1,323 +0,0 @@
import React, { useEffect, useRef } from 'react';
import useRoleSelectorStore from '../../store/Slices/RoleSelectorSlice';
import './RoleSelector.css';
const RoleSelector = ({ onRoleChange, onChatChange }) => {
const panelRef = useRef(null);
// 从 Zustand store 中获取状态和操作
const {
roleData,
selectedRole,
selectedChat,
hoveredRole,
clickedRole,
isLoading,
searchTerm,
editingRole,
editingChat,
showDeleteConfirm,
deleteType,
fetchRoleData,
setSelectedRole,
setSelectedChat,
setHoveredRole,
setClickedRole,
setSearchTerm,
setEditingRole,
setEditingChat,
setShowDeleteConfirm,
setDeleteType,
handleRenameRole,
handleRenameChat,
confirmDelete,
cancelDelete,
handleAddRole,
handleAddChat,
resetPanel
} = useRoleSelectorStore();
// 组件挂载时获取数据
useEffect(() => {
fetchRoleData();
}, [fetchRoleData]);
// 点击外部关闭面板
useEffect(() => {
const handleClickOutside = (event) => {
if (panelRef.current && !panelRef.current.contains(event.target)) {
resetPanel();
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [resetPanel]);
// 处理角色选择
const handleRoleSelect = (role) => {
setSelectedRole(role);
if (onRoleChange) {
onRoleChange(role);
}
// 如果该角色有聊天记录,默认选择第一个
if (roleData[role] && roleData[role].length > 0) {
const firstChat = roleData[role][0];
setSelectedChat(firstChat);
if (onChatChange) {
onChatChange(role, firstChat);
}
} else {
setSelectedChat(null);
}
};
// 处理聊天选择
const handleChatSelect = (chat) => {
setSelectedChat(chat);
setHoveredRole(null);
setClickedRole(null);
if (onChatChange) {
onChatChange(selectedRole, chat);
}
};
// 处理角色卡片点击
const handleRoleCardClick = (role) => {
if (clickedRole === role) {
setClickedRole(null);
} else {
setClickedRole(role);
handleRoleSelect(role);
}
};
// 处理搜索
const handleSearchChange = (e) => {
setSearchTerm(e.target.value);
};
// 处理角色编辑
const handleEditRole = (e, role) => {
e.stopPropagation();
setEditingRole(role);
};
// 处理角色重命名
const handleRenameRoleWrapper = (e, oldName) => {
e.stopPropagation();
const newName = e.target.value;
handleRenameRole(oldName, newName);
if (selectedRole === oldName && newName && newName !== oldName) {
if (onRoleChange) {
onRoleChange(newName);
}
}
};
// 处理聊天编辑
const handleEditChat = (e, chat) => {
e.stopPropagation();
setEditingChat(chat);
};
// 处理聊天重命名
const handleRenameChatWrapper = (e, oldName) => {
e.stopPropagation();
const newName = e.target.value;
handleRenameChat(oldName, newName);
if (selectedChat === oldName && newName && newName !== oldName) {
if (onChatChange) {
onChatChange(selectedRole, newName);
}
}
};
// 处理删除确认
const handleDeleteClick = (e, type, name) => {
e.stopPropagation();
setDeleteType(type);
setShowDeleteConfirm(name);
};
// 确认删除
const confirmDeleteWrapper = () => {
confirmDelete();
if (deleteType === 'role' && selectedRole === showDeleteConfirm) {
if (onRoleChange) {
onRoleChange(null);
}
} else if (deleteType === 'chat' && selectedChat === showDeleteConfirm) {
if (onChatChange) {
onChatChange(selectedRole, null);
}
}
};
// 过滤角色
const filteredRoles = Object.keys(roleData).filter(role =>
role.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<div className="role-selector" ref={panelRef}>
<div className="selected-role-display">
{selectedRole ? (
<div className="role-badge">
<span className="role-label">当前角色/聊天:</span>
<span className="role-name">{selectedRole}</span>
{selectedChat && <span className="chat-name">/ {selectedChat}</span>}
</div>
) : (
<div className="role-badge empty">
<span>未选择角色</span>
</div>
)}
</div>
<div className="search-bar">
<input
type="text"
placeholder="搜索角色..."
value={searchTerm}
onChange={handleSearchChange}
/>
</div>
<div className="role-list">
{isLoading ? (
<div className="loading">加载中...</div>
) : filteredRoles.length === 0 ? (
<div className="empty-state">
<p>没有找到匹配的角色</p>
</div>
) : (
filteredRoles.map(role => (
<div
key={role}
className={`role-item ${selectedRole === role ? 'active' : ''}`}
onClick={() => handleRoleCardClick(role)}
onMouseEnter={() => setHoveredRole(role)}
onMouseLeave={() => setHoveredRole(null)}
>
<div className="role-header">
{editingRole === role ? (
<input
type="text"
defaultValue={role}
autoFocus
onBlur={(e) => handleRenameRoleWrapper(e, role)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleRenameRoleWrapper(e, role);
}
}}
onClick={(e) => e.stopPropagation()}
/>
) : (
<div className="role-name">{role}</div>
)}
<div className="role-actions">
<button
className="icon-btn"
title="编辑"
onClick={(e) => handleEditRole(e, role)}
>
</button>
<button
className="icon-btn delete"
title="删除"
onClick={(e) => handleDeleteClick(e, 'role', role)}
>
🗑
</button>
</div>
</div>
{/* 聊天列表 */}
{(hoveredRole === role || clickedRole === role) && roleData[role] && roleData[role].length > 0 && (
<div className="chat-list">
{roleData[role].map(chat => (
<div
key={chat}
className={`chat-item ${selectedChat === chat ? 'active' : ''}`}
onClick={(e) => {
e.stopPropagation();
handleChatSelect(chat);
}}
>
<div className="chat-content">
{editingChat === chat ? (
<input
type="text"
defaultValue={chat}
autoFocus
onBlur={(e) => handleRenameChatWrapper(e, chat)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleRenameChatWrapper(e, chat);
}
}}
onClick={(e) => e.stopPropagation()}
/>
) : (
<div className="chat-name">{chat}</div>
)}
</div>
<div className="chat-actions">
{editingChat !== chat && (
<>
<button
className="icon-btn"
title="编辑"
onClick={(e) => handleEditChat(e, chat)}
>
</button>
<button
className="icon-btn delete"
title="删除"
onClick={(e) => handleDeleteClick(e, 'chat', chat)}
>
🗑
</button>
</>
)}
</div>
</div>
))}
</div>
)}
</div>
))
)}
</div>
{/* 删除确认对话框 */}
{showDeleteConfirm && (
<div className="delete-confirm-modal">
<div className="modal-content">
<h3>确认删除</h3>
<p>确定要删除{deleteType === 'role' ? '角色' : '聊天'} "{showDeleteConfirm}" </p>
<div className="modal-actions">
<button className="modal-button cancel-button" onClick={cancelDelete}>
取消
</button>
<button className="modal-button danger" onClick={confirmDeleteWrapper}>
确认删除
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default RoleSelector;

View File

@@ -1,154 +0,0 @@
/* ==================== 顶部工具栏区域 ==================== */
.toolbar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 50px;
background-color: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
z-index: 1000;
display: flex;
align-items: center;
padding: 0 20px;
justify-content: space-between;
transition: all 0.3s ease;
}
/* 工具栏主要部分容器 */
.toolbar-section {
display: flex;
align-items: center;
gap: 15px;
flex: 1;
}
/* 工具栏图标容器 */
.toolbar-icons {
display: flex;
align-items: center;
gap: 15px;
flex: 1;
}
/* 工具栏图标 */
.toolbar-icon {
width: 36px;
height: 36px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
font-size: 18px;
color: #555;
background-color: #f5f5f5;
}
.toolbar-icon:hover {
background-color: #e6f7ff;
color: #1890ff;
transform: translateY(-2px);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}
.toolbar-icon.active {
background-color: #1890ff;
color: #fff;
}
/* ==================== 弹出面板通用样式 ==================== */
.close-panel-button {
background: none;
border: none;
font-size: 20px;
cursor: pointer;
color: #666;
padding: 5px 10px;
margin-left: auto;
transition: color 0.2s;
}
.close-panel-button:hover {
color: #333;
}
.panel-overlay {
position: fixed;
top: 50px;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.3);
z-index: 999;
display: flex;
justify-content: center;
padding-top: 20px;
animation: fadeIn 0.2s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.panel-content {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
width: 90%;
max-width: 1200px;
max-height: calc(100vh - 80px);
overflow: hidden;
display: flex;
flex-direction: column;
animation: slideDown 0.3s ease;
}
@keyframes slideDown {
from {
transform: translateY(-20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.panel-header {
padding: 16px 20px;
border-bottom: 1px solid #e0e0e0;
display: flex;
justify-content: space-between;
align-items: center;
background-color: #fafafa;
}
.panel-header h3 {
margin: 0;
font-size: 16px;
font-weight: 500;
color: #333;
}
.panel-body {
padding: 20px;
overflow-y: auto;
flex: 1;
}
/* 主内容区域 */
.main-container {
margin-top: 50px;
height: calc(100vh - 50px);
display: flex;
overflow: hidden;
}

View File

@@ -1,141 +0,0 @@
import React, { useState, useRef } from 'react';
import RoleSelector from '../RoleSelector/RoleSelector';
import './ToolBar.css';
const Toolbar = () => {
const [activePanel, setActivePanel] = useState(null);
const panelRef = useRef(null);
// 点击外部关闭面板
React.useEffect(() => {
const handleClickOutside = (event) => {
if (panelRef.current && !panelRef.current.contains(event.target)) {
setActivePanel(null);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
// 处理面板切换
const handlePanelToggle = (panelName) => {
if (activePanel === panelName) {
setActivePanel(null);
} else {
setActivePanel(panelName);
}
};
// 关闭面板
const handleClosePanel = () => {
setActivePanel(null);
};
return (
<>
<div className="toolbar">
{/* 左侧工具栏图标 */}
<div className="toolbar-section">
<div className="toolbar-icons">
{/* Logo图标 */}
<div
className="toolbar-icon"
title="首页"
onClick={() => handlePanelToggle(null)}
>
🤖
</div>
</div>
</div>
{/* 中间操作图标 */}
<div className="toolbar-section">
<div className="toolbar-icons">
{/* 角色选择图标 */}
<div
className={`toolbar-icon ${activePanel === 'role' ? 'active' : ''}`}
title="角色管理"
onClick={() => handlePanelToggle('role')}
>
👤
</div>
</div>
</div>
{/* 右侧工具栏图标 */}
<div className="toolbar-section">
<div className="toolbar-icons" style={{ justifyContent: 'flex-end' }}>
<div
className="toolbar-icon"
title="设置"
onClick={() => handlePanelToggle('settings')}
>
</div>
<div
className="toolbar-icon"
title="帮助"
onClick={() => handlePanelToggle('help')}
>
</div>
</div>
</div>
</div>
{/* 角色管理面板 */}
{activePanel === 'role' && (
<div className="panel-overlay" ref={panelRef}>
<div className="panel-content">
<div className="panel-header">
<h3>角色管理</h3>
<button className="close-panel-button" onClick={handleClosePanel} title="关闭">
</button>
</div>
<div className="panel-body">
<RoleSelector />
</div>
</div>
</div>
)}
{activePanel === 'settings' && (
<div className="panel-overlay" ref={panelRef}>
<div className="panel-content">
<div className="panel-header">
<h3>设置</h3>
<button className="close-panel-button" onClick={handleClosePanel} title="关闭">
</button>
</div>
<div className="panel-body">
<p>设置内容...</p>
</div>
</div>
</div>
)}
{activePanel === 'help' && (
<div className="panel-overlay" ref={panelRef}>
<div className="panel-content">
<div className="panel-header">
<h3>帮助</h3>
<button className="close-panel-button" onClick={handleClosePanel} title="关闭">
</button>
</div>
<div className="panel-body">
<p>帮助内容...</p>
</div>
</div>
</div>
)}
</>
);
};
export default Toolbar;

View File

@@ -1,61 +0,0 @@
/* 重置样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body, .app {
height: 100%;
width: 100%;
overflow: hidden; /* 防止出现滚动条 */
}
.app {
display: flex;
flex-direction: column;
}
/* 主内容区域 */
.main-container {
flex: 1; /* 这会让主容器占据剩余的所有空间 */
display: flex;
overflow: hidden; /* 防止内容溢出 */
height: 100%; /* 确保高度填满 */
}
/* 左侧栏 */
.sidebar-left {
width: 250px; /* 或者你想要的宽度 */
height: ; /* 确保高度填满 */
overflow-y: auto; /* 内容过多时显示滚动条 */
}
/* 中间聊天区域 */
.chat-area {
flex: 1; /* 占据剩余空间 */
height: 100%; /* 确保高度填满 */
display: flex;
flex-direction: column;
overflow: hidden; /* 防止内容溢出 */
}
/* 右侧栏 */
.sidebar-right {
width: 300px; /* 或者你想要的宽度 */
height: 100%; /* 确保高度填满 */
display: flex;
flex-direction: column;
}
/* 右侧栏顶部 */
.right-top {
flex: 1; /* 占据剩余空间 */
overflow-y: auto;
}
/* 右侧栏底部 */
.right-bottom {
height: 200px; /* 或者你想要的高度 */
overflow-y: auto;
}

View File

@@ -1,14 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
import './index.css';
// 获取 HTML 中的根元素
const rootElement = document.getElementById('root');
// 创建 React 根节点并渲染 App 组件
ReactDOM.createRoot(rootElement).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@@ -1,14 +0,0 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': {
target: 'http://backend:8000',
changeOrigin: true,
}
}
}
})

View File

@@ -1,5 +1,5 @@
# 使用 Node.js 18 Alpine 镜像作为基础 # 多阶段构建 - 开发环境
FROM node:18-alpine FROM node:20-alpine AS development
# 设置工作目录 # 设置工作目录
WORKDIR /app WORKDIR /app
@@ -8,15 +8,55 @@ WORKDIR /app
RUN npm config set registry https://registry.npmmirror.com/ RUN npm config set registry https://registry.npmmirror.com/
# 复制 package.json 和 package-lock.json # 复制 package.json 和 package-lock.json
# 利用 Docker 缓存层,只有依赖变更时才重新安装
COPY package.json package-lock.json* ./ COPY package.json package-lock.json* ./
# 安装依赖 # 安装依赖
RUN npm install RUN npm install
# 复制源代码到容器
COPY . .
# 暴露 Vite 默认端口 5173 # 暴露 Vite 默认端口 5173
EXPOSE 5173 EXPOSE 5173
# 启动命令由 docker-compose.yml 中的 command 覆盖, # 设置环境变量
# 这里保留默认的 dev 命令作为 fallback ENV NODE_ENV=development
ENV VITE_API_URL=http://backend:8000/api
# 启动 Vite 开发服务器
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
# 多阶段构建 - 生产环境
FROM node:20-alpine AS build
WORKDIR /app
# 设置 npm 镜像源
RUN npm config set registry https://registry.npmmirror.com/
# 复制依赖文件
COPY package.json package-lock.json* ./
# 安装依赖
RUN npm install
# 复制源代码
COPY . .
# 构建生产版本
RUN npm run build
# 生产环境镜像
FROM nginx:alpine AS production
# 复制构建产物到 Nginx
COPY --from=build /app/dist /usr/share/nginx/html
# 复制 Nginx 配置文件
COPY nginx.conf /etc/nginx/conf.d/default.conf
# 暴露端口
EXPOSE 80
# 启动 Nginx
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -1,608 +0,0 @@
import requests
import streamlit as st
import os
from pathlib import Path
import time
import random
# --- 页面配置 ---
st.set_page_config(
page_title="AI WorkFlow Engine",
page_icon="🤖",
layout="wide",
initial_sidebar_state="expanded"
)
BACKEND_URL = os.getenv("BACKEND_URL", "http://127.0.0.1:8000")
print(f"DEBUG BACKEND_URL: {BACKEND_URL}", flush=True) # 看 docker logs
# --- 自定义 CSS (蓝白清晰风格) ---
st.markdown("""
<style>
/* 全局背景与字体 */
.stApp {
background-color: #F0F4F8; /* 浅蓝灰背景,护眼且清晰 */
color: #1A1A1A; /* 深黑色字体 */
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
/* 隐藏默认菜单 */
#MainMenu {visibility: hidden;}
footer {visibility: hidden;}
header {visibility: hidden;} /* 隐藏顶部默认栏,使用自定义工具栏 */
/* 侧边栏样式 */
section[data-testid="stSidebar"] {
background-color: #FFFFFF;
border-right: 1px solid #D1D9E6;
color: #1A1A1A;
}
section[data-testid="stSidebar"] .stMarkdown,
section[data-testid="stSidebar"] .stNumberInput,
section[data-testid="stSidebar"] .stSlider {
color: #1A1A1A;
}
/* 聊天容器背景 (白色卡片感) */
.stChatMessage {
background-color: #FFFFFF;
border: 1px solid #E1E8F0;
border-radius: 8px;
padding: 10px;
margin-bottom: 10px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
/* 用户消息特殊样式 */
.stChatMessage[data-testid="stChatMessage"]:has(.stMarkdown p) {
/* 这里很难直接针对 user/assistant 做不同背景,通过 JS 或特定类名较难,
Streamlit 原生 chat_message 会自动处理头像,我们主要靠边框和布局区分 */
}
/* 输入框样式 */
.stTextInput > div > div > input,
.stTextArea > div > div > textarea {
background-color: #FFFFFF;
color: #1A1A1A;
border: 1px solid #0056B3; /* 蓝色边框 */
border-radius: 6px;
}
.stTextInput > div > div > input:focus,
.stTextArea > div > div > textarea:focus {
border-color: #003D80;
box-shadow: 0 0 0 2px rgba(0, 86, 179, 0.2);
}
/* 按钮样式 */
.stButton > button {
background-color: #FFFFFF;
color: #0056B3;
border: 1px solid #0056B3;
border-radius: 6px;
font-weight: 600;
}
.stButton > button:hover {
background-color: #0056B3;
color: #FFFFFF;
}
.stButton > button[kind="primary"] {
background-color: #0056B3;
color: #FFFFFF;
border: 1px solid #0056B3;
}
.stButton > button[kind="primary"]:hover {
background-color: #003D80;
border-color: #003D80;
}
/* 拼接块列表样式优化 */
.splice-item {
background-color: #FFFFFF;
border: 1px solid #D1D9E6;
border-radius: 6px;
padding: 8px;
margin-bottom: 6px;
display: flex;
align-items: center;
}
.splice-name-active { color: #1A1A1A; font-weight: 500; }
.splice-name-inactive { color: #8898AA; text-decoration: line-through; }
/* 顶部工具栏 */
.top-bar {
background-color: #FFFFFF;
padding: 10px 20px;
border-bottom: 1px solid #D1D9E6;
margin: -10px -10px 10px -10px; /* 抵消默认 padding */
display: flex;
justify-content: space-between;
align-items: center;
}
/* 可折叠工具栏 */
.collapsible-toolbar {
background-color: #FFFFFF;
border-bottom: 1px solid #D1D9E6;
padding: 10px;
margin-bottom: 10px;
}
/* 隐藏工具栏时的样式 */
.toolbar-hidden {
display: none;
}
/* 工具栏切换按钮 */
.toolbar-toggle {
position: fixed;
top: 10px;
right: 10px;
z-index: 999;
background-color: #FFFFFF;
border: 1px solid #D1D9E6;
border-radius: 50%;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
/* 三栏布局 - 修改部分 */
.main-container {
display: flex;
flex-direction: column;
height: calc(100vh - 60px);
}
.three-column-layout {
display: flex;
flex-direction: row;
height: 100%;
overflow: hidden;
}
.left-column, .middle-column, .right-column {
padding: 10px;
overflow-y: auto;
height: 100%;
}
.left-column {
flex: 1;
border-right: 1px solid #D1D9E6;
}
.middle-column {
flex: 3;
border-right: 1px solid #D1D9E6;
display: flex;
flex-direction: column;
overflow: hidden;
}
.right-column {
flex: 1;
}
/* 中间列的聊天区域 */
.chat-area {
flex: 1;
overflow-y: auto;
padding: 10px;
height: calc(100% - 80px); /* 减去输入区域的高度 */
}
/* 中间列的输入区域 */
.input-area {
flex: 0 0 auto;
padding: 10px;
border-top: 1px solid #D1D9E6;
background-color: #F0F4F8;
height: 80px; /* 固定高度 */
}
/* 隐藏Streamlit默认的滚动条样式 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
</style>
<script>
// 动态调整布局高度
function adjustLayout() {
// 获取三个列容器
const leftColumn = document.querySelector('.left-column');
const middleColumn = document.querySelector('.middle-column');
const rightColumn = document.querySelector('.right-column');
// 设置高度为视口高度减去顶部工具栏高度
const height = window.innerHeight - 60; // 减去顶部工具栏的高度
if (leftColumn) leftColumn.style.height = `${height}px`;
if (middleColumn) middleColumn.style.height = `${height}px`;
if (rightColumn) rightColumn.style.height = `${height}px`;
// 调整聊天区域高度
const chatArea = document.querySelector('.chat-area');
if (chatArea) {
const inputArea = document.querySelector('.input-area');
const inputHeight = inputArea ? inputArea.offsetHeight : 80;
chatArea.style.height = `${height - inputHeight}px`;
}
}
// 页面加载时调整布局
window.addEventListener('load', adjustLayout);
// 窗口大小改变时重新调整布局
window.addEventListener('resize', adjustLayout);
// 每次Streamlit重新渲染后调整布局
document.addEventListener('newElementRendered', adjustLayout);
</script>
""", unsafe_allow_html=True)
# --- 状态初始化 ---
if "messages" not in st.session_state:
# 初始化一些示例数据,方便查看效果
st.session_state.messages = [
{"role": "assistant", "content": "你好!我是你的 AI 工作流助手。系统已就绪,请开始对话。"},
{"role": "user", "content": "帮我生成一个角色卡,需要包含姓名、年龄和背景故事。"},
{"role": "assistant",
"content": "好的,这是一个示例角色卡:<br><b>姓名</b>: 艾莉娅<br><b>年龄</b>: 24<br><b>背景</b>: 一位来自北方边境的流浪法师。<br><i>(这是 HTML 渲染测试)</i>"}
]
if "render_html" not in st.session_state:
st.session_state.render_html = True # 默认开启 HTML 渲染以展示效果
if "image_folder" not in st.session_state:
st.session_state.image_folder = "./assets/images"
if "splice_blocks" not in st.session_state:
st.session_state.splice_blocks = [
{"id": 1, "name": "[必看] 系统指令", "active": True, "type": "system"},
{"id": 2, "name": "A.U.T.O. 预设设置", "active": True, "type": "system"},
{"id": 3, "name": "世界书:人物关系", "active": False, "type": "world"},
{"id": 4, "name": "Chat History (自动)", "active": True, "type": "history", "editable": False},
]
if "toolbar_visible" not in st.session_state:
st.session_state.toolbar_visible = True
# --- 顶部工具栏 ---
# 工具栏切换按钮
st.markdown("""
<div class="toolbar-toggle" onclick="toggleToolbar()">
<span id="toolbar-icon">▼</span>
</div>
<script>
function toggleToolbar() {
var toolbar = document.querySelector('.collapsible-toolbar');
var icon = document.getElementById('toolbar-icon');
if (toolbar.style.display === 'none') {
toolbar.style.display = 'block';
icon.textContent = '';
} else {
toolbar.style.display = 'none';
icon.textContent = '';
}
}
</script>
""", unsafe_allow_html=True)
# 工具栏内容
if st.session_state.toolbar_visible:
with st.container():
c_top1, c_top2, c_top3 = st.columns([1, 6, 1])
with c_top1:
if st.button("📂 打开", key="btn_open"):
st.toast("打开会话功能预留")
if st.button("💾 保存", key="btn_save"):
st.toast("会话已保存")
with c_top2:
st.markdown("<h3 style='margin:0; color:#0056B3;'>AI WorkFlow Engine</h3>", unsafe_allow_html=True)
with c_top3:
if st.button("⚙️ 设置", key="btn_settings"):
st.toast("全局设置预留")
st.divider()
# --- 三栏布局 ---
col_left, col_mid, col_right = st.columns([1, 3, 1], gap="small")
# =======================
# 1. 左侧:预设与拼接管理 (蓝白风格适配)
# =======================
with col_left:
# 使用自定义容器类
st.markdown('<div class="left-column">', unsafe_allow_html=True)
st.markdown("#### 📜 全局预设")
c_pre1, c_pre2 = st.columns([4, 1])
with c_pre1:
preset_options = ["Default", "A.U.T.O. v1.47", "Roleplay Pro"]
st.selectbox("选择预设", preset_options, label_visibility="collapsed")
with c_pre2:
if st.button("📥", key="btn_import", help="导入预设"):
st.toast("导入功能预留")
st.markdown("#### ⚙️ 生成参数")
c_p1, c_p2 = st.columns(2)
with c_p1:
st.slider("温度", 0.0, 2.0, 1.0, key="slider_temp")
st.slider("Top P", 0.0, 1.0, 0.9, key="slider_top_p")
with c_p2:
st.slider("频率惩罚", 0.0, 2.0, 1.0, key="slider_freq")
st.slider("存在惩罚", 0.0, 2.0, 0.0, key="slider_pres")
c_l1, c_l2 = st.columns(2)
with c_l1:
st.number_input("上下文长度", value=30000, key="input_ctx")
with c_l2:
st.number_input("最大回复", value=500, key="input_max")
st.checkbox("✅ 流式传输", value=True, key="chk_stream")
st.markdown("#### 🧩 内容拼接块")
st.caption("控制发送至后端的上下文组成")
# 渲染拼接块列表
for block in st.session_state.splice_blocks:
with st.container():
# 自定义行布局模拟列表项
cols = st.columns([0.5, 3, 0.5, 0.5])
with cols[0]:
icon = "🌍" if block['type'] == 'world' else ("💬" if block['type'] == 'history' else "📄")
st.write(icon)
with cols[1]:
name_class = "splice-name-active" if block['active'] else "splice-name-inactive"
st.markdown(f"<div class='{name_class}' style='font-size:0.85em;'>{block['name']}</div>",
unsafe_allow_html=True)
with cols[2]:
disabled = not block.get('editable', True)
if st.button("✏️", key=f"edit_{block['id']}", disabled=disabled):
st.toast(f"编辑:{block['name']}")
with cols[3]:
is_active = st.checkbox("", value=block['active'], key=f"act_{block['id']}",
label_visibility="collapsed")
if is_active != block['active']:
block['active'] = is_active
st.rerun()
st.markdown("<div style='height:1px; background:#E1E8F0; margin:4px 0;'></div>", unsafe_allow_html=True)
if st.button("+ 添加拼接块", use_container_width=True):
st.toast("添加新功能预留")
st.markdown('</div>', unsafe_allow_html=True)
# =======================
# 2. 中间:流式对话区 (动态读取历史)
# =======================
with col_mid:
# 使用自定义容器类
st.markdown('<div class="middle-column">', unsafe_allow_html=True)
# --- 控制区域 ---
c_ctrl1, c_ctrl2, c_ctrl3 = st.columns([3, 2, 1])
with c_ctrl1:
# --- 数据集选择下拉框 ---
try:
response = requests.get(f"{BACKEND_URL}/get_all_role_and_chat")
if response.status_code == 200:
datasets = response.json()
dataset_options = list(datasets.keys())
else:
st.error(f"获取数据集失败: {response.status_code}")
dataset_options = []
except requests.exceptions.RequestException as e:
st.error(f"请求数据集时出错: {e}")
dataset_options = []
selected_dataset = st.selectbox(
"选择数据集",
dataset_options,
index=0 if dataset_options else None,
key="dataset_selector"
)
with c_ctrl2:
# --- 文件路径选择下拉框 ---
# 初始化两层下拉框的数据结构
chat_history_options = {}
file_options = []
if selected_dataset:
# 使用已经获取的数据集数据
chat_history_options = datasets
# 获取当前选中数据集对应的文件列表
file_options = chat_history_options.get(selected_dataset, [])
# 第一层下拉框选择聊天会话这里应该直接使用selected_dataset
# 不需要再创建一个selectbox因为已经选择了数据集
selected_chat_session = selected_dataset
# 第二层下拉框选择文件路径value列表
if selected_chat_session:
file_options = chat_history_options.get(selected_chat_session, [])
if file_options:
# 提取文件名并去除.jsonl后缀用于显示
display_names = [os.path.basename(f).replace('.jsonl', '') for f in file_options]
# 创建文件名到完整路径的映射
file_name_to_path = {os.path.basename(f).replace('.jsonl', ''): f for f in file_options}
selected_file_display = st.selectbox(
"选择聊天",
display_names,
index=0 if display_names else None,
key="file_selector"
)
if selected_file_display:
# 保存完整路径到session_state
st.session_state.selected_file_path = file_name_to_path[selected_file_display]
else:
# 如果没有文件路径,清空选择
if "file_selector" in st.session_state:
del st.session_state["file_selector"]
else:
# 如果没有选择会话,清空选择
if "file_selector" in st.session_state:
del st.session_state["file_selector"]
with c_ctrl3:
# HTML 渲染切换
toggle_html = st.toggle("HTML 渲染", value=st.session_state.render_html, key="html_toggle")
if toggle_html != st.session_state.render_html:
st.session_state.render_html = toggle_html
st.rerun()
# 显示当前会话信息
if selected_dataset and 'selected_file_path' in st.session_state:
file_name = os.path.basename(st.session_state.selected_file_path).replace('.jsonl', '')
st.caption(f"当前会话:{selected_dataset} - {file_name}")
else:
st.caption("当前会话Active_Session_01")
# --- 核心:动态渲染历史记录 ---
# 使用自定义容器类包裹聊天区域
st.markdown('<div class="chat-area">', unsafe_allow_html=True)
# 如果选择了聊天记录,则显示该记录
if hasattr(st.session_state, 'selected_chat_data') and st.session_state.selected_chat_data:
# 显示选中的聊天记录
msg = st.session_state.selected_chat_data
with st.chat_message(msg["role"]):
content = msg["content"]
# 根据开关决定是否解析 HTML
if st.session_state.render_html and msg["role"] == "assistant":
st.markdown(content, unsafe_allow_html=True)
else:
st.markdown(content)
# 显示其他信息
with st.expander("详细信息", expanded=False):
st.json(msg)
else:
# 否则显示session_state中的消息历史
for i, msg in enumerate(st.session_state.messages):
with st.chat_message(msg["role"]):
content = msg["content"]
if st.session_state.render_html and msg["role"] == "assistant":
st.markdown(content, unsafe_allow_html=True)
else:
st.markdown(content)
st.markdown('</div>', unsafe_allow_html=True)
# --- 输入区域 ---
# 使用自定义容器类包裹输入区域
st.markdown('<div class="input-area">', unsafe_allow_html=True)
# 聊天输入框
user_input = st.chat_input("输入消息... (支持 /命令)")
if user_input:
# 1. 将用户输入加入历史
st.session_state.messages.append({"role": "user", "content": user_input})
# 2. 触发重新渲染
with st.chat_message("assistant"):
message_placeholder = st.empty()
message_placeholder.markdown("*思考中...*")
# === 模拟后端流式响应 ===
full_response = ""
simulated_text = f"收到您的指令:**{user_input}**。\n\n这是一个测试回复,如果您开启了 **HTML 渲染**,下方将显示彩色文本和表格:<br><span style='color:#0056B3; font-weight:bold;'>蓝色高亮文本</span><br><table border='1' style='border-collapse:collapse; width:100%;'><tr><th>属性</th><th>值</th></tr><tr><td>状态</td><td>正常</td></tr></table>"
chunks = simulated_text.split(" ")
for chunk in chunks:
full_response += chunk + " "
time.sleep(0.1)
if st.session_state.render_html:
message_placeholder.markdown(full_response, unsafe_allow_html=True)
else:
message_placeholder.markdown(full_response)
# 3. 将完整的助手回复存入历史
st.session_state.messages.append({"role": "assistant", "content": full_response})
# 强制刷新以确保持久化显示
st.rerun()
st.markdown('</div>', unsafe_allow_html=True)
st.markdown('</div>', unsafe_allow_html=True)
# =======================
# 3. 右侧:图片与骰子 (蓝白风格)
# =======================
with col_right:
# 使用自定义容器类
st.markdown('<div class="right-column">', unsafe_allow_html=True)
st.markdown("#### 🖼️ 本地图库")
img_path = Path(st.session_state.image_folder)
img_path.mkdir(parents=True, exist_ok=True)
try:
images = [f for f in os.listdir(img_path) if f.endswith(('.png', '.jpg', '.jpeg', '.webp'))]
if images:
cols = st.columns(2)
for idx, img_name in enumerate(images[:8]):
with cols[idx % 2]:
# 增加白色背景和边框,使图片在浅灰底上更突出
st.markdown(
f"<div style='background:white; padding:5px; border-radius:4px; border:1px solid #ddd;'>",
unsafe_allow_html=True)
st.image(str(img_path / img_name), use_container_width=True)
st.caption(img_name)
st.markdown("</div>", unsafe_allow_html=True)
else:
st.info("图片文件夹为空")
except Exception as e:
st.error(f"读取错误:{e}")
st.divider()
st.markdown("#### 🎲 检定工具")
tab_table, tab_dice = st.tabs(["📊 表格", "🎲 骰子"])
with tab_table:
st.markdown("**动态数据表**")
st.dataframe(
{"属性": ["力量", "敏捷", "智力"], "数值": [50, 60, 70]},
hide_index=True,
use_container_width=True
)
with tab_dice:
roll_type = st.radio("类型", ["难度检定", "对抗骰"], horizontal=True)
diff_opts = ["极难 (95)", "困难 (75)", "普通 (50)"]
selected_diff = st.selectbox("难度", diff_opts)
c_r1, c_r2 = st.columns(2)
with c_r1:
if st.button("🎲 投掷", type="primary", use_container_width=True):
res = random.randint(1, 100)
color = "#d9534f" if res > int(selected_diff.split('(')[1].strip(')')) else "#5cb85c"
st.markdown(
f"<div style='text-align:center; font-size:1.5em; color:{color}; font-weight:bold;'>{res}</div>",
unsafe_allow_html=True)
with c_r2:
st.caption(f"目标:{selected_diff.split('(')[1].strip(')')}")
st.markdown('</div>', unsafe_allow_html=True)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 497 KiB

View File

@@ -1,53 +0,0 @@
import streamlit as st
import requests
def render_chat_window(backend_url):
st.subheader("💬 流式对话")
# 聊天历史显示
chat_container = st.container()
with chat_container:
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.markdown(message["content"])
# 如果有关联图片,也可以在这里显示
if "images" in message:
for img_url in message["images"]:
st.image(img_url, width=200)
# 输入框
if prompt := st.chat_input("输入消息..."):
# 1. 显示用户消息
st.session_state.messages.append({"role": "user", "content": prompt})
with st.chat_message("user"):
st.markdown(prompt)
# 2. 调用后端流式接口
with st.chat_message("assistant"):
message_placeholder = st.empty()
full_response = ""
# 模拟流式接收 (实际需使用 requests stream 或 websocket)
# POST /api/role/stream
try:
# 伪代码示例:
# with requests.post(f"{backend_url}/api/chat/stream", json={"message": prompt}, stream=True) as r:
# for chunk in r.iter_content(chunk_size=None):
# if chunk:
# full_response += chunk.decode('utf-8')
# message_placeholder.markdown(full_response + "▌")
# 演示用静态延迟
import time
response_text = "这是一个流式响应的演示。后端正在处理您的请求..."
for char in response_text:
full_response += char
message_placeholder.markdown(full_response + "")
time.sleep(0.05)
message_placeholder.markdown(full_response)
st.session_state.messages.append({"role": "assistant", "content": full_response})
except Exception as e:
st.error(f"连接后端失败: {e}")

View File

@@ -1,26 +0,0 @@
import streamlit as st
import random
def render_dice_roller():
st.subheader("🎲 命运骰子")
col1, col2 = st.columns(2)
with col1:
d20 = st.button("D20", use_container_width=True)
if d20:
roll = random.randint(1, 20)
st.metric("结果", roll, delta=None)
with col2:
d6 = st.button("D6", use_container_width=True)
if d6:
roll = random.randint(1, 6)
st.metric("结果", roll, delta=None)
# 自定义骰子
sides = st.number_input("面数", min_value=2, max_value=100, value=10)
if st.button(f"投掷 D{sides}", use_container_width=True):
roll = random.randint(1, sides)
st.success(f"🎲 结果是: **{roll}**")

View File

@@ -1,17 +0,0 @@
import streamlit as st
def render_image_gallery(backend_url):
st.subheader("🖼️ 生成画廊")
# 这里通常轮询后端获取最新生成的图片
# GET /api/images/latest
if not st.session_state.generated_images:
st.info("暂无生成图片,对话中触发绘图后将在此显示。")
else:
cols = st.columns(2)
for idx, img_url in enumerate(st.session_state.generated_images[-4:]): # 只显示最近4张
with cols[idx % 2]:
st.image(img_url, use_container_width=True)
st.caption(f"Image {idx + 1}")

View File

@@ -1,30 +0,0 @@
import streamlit as st
import requests
def render_settings_panel(backend_url):
st.subheader("⚙️ 预设设置")
# 模拟获取预设列表 (实际应调用后端 API)
# GET /api/presets
try:
# response = requests.get(f"{backend_url}/api/presets")
# presets = response.json()
presets = ["角色扮演-奇幻", "项目管理", "旅行规划", "自定义"] # 占位数据
except:
presets = ["默认预设"]
selected_preset = st.selectbox("选择预设模板", presets, index=0)
st.text_area("系统指令 (System)", height=100, placeholder="在此输入系统级指令...")
st.checkbox("启用状态记忆", value=True)
st.checkbox("启用异步生图", value=True)
st.checkbox("启用输入预处理", value=False)
st.info("💡 修改配置后自动生效,无需重启。")
# 保存按钮 (调用后端更新配置)
if st.button("💾 保存配置", use_container_width=True):
st.success("配置已保存!")
# requests.post(f"{backend_url}/api/config", json={...})

View File

@@ -1,18 +0,0 @@
import streamlit as st
def render_toolbar(backend_url):
col1, col2, col3 = st.columns([1, 2, 1])
with col1:
st.logo("https://streamlit.io/images/brand/streamlit-logo-primary-colormark-darktext.png",
size="large") # 可替换为项目Logo
with col2:
st.title("AI Tavern 工作流引擎")
with col3:
if st.button("🔄 重置会话", use_container_width=True):
st.session_state.messages = []
st.rerun()
# 这里可以添加更多工具栏按钮,如:知识库管理、系统状态等

View File

@@ -1,4 +0,0 @@
streamlit>=1.30.0
requests>=2.31.0
websockets>=12.0
Pillow>=10.0.0