diff --git a/.env b/.env index e69de29..3e50054 100644 --- a/.env +++ b/.env @@ -0,0 +1,11 @@ +# ---------- 路径配置 ---------- +VECTORSTORE_PATH=/data/vectorstore +STATE_FILE=/data/state.json +SCHEMA_FILE=/data/schema.json +PRESETS_FILE=/data/presets.json +REGEX_FILE=/data/regex_rules.json + +# ---------- 服务地址 ---------- +COMFYUI_API_URL=http://comfyui:8188 +BACKEND_PORT=8000 +FRONTEND_PORT=8501 \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore index 35410ca..e3a9b74 100644 --- a/.idea/.gitignore +++ b/.idea/.gitignore @@ -6,3 +6,4 @@ # Datasource local storage ignored files /dataSources/ /dataSources.local.xml +.env diff --git a/.idea/llm-workflow-engine.iml b/.idea/llm-workflow-engine.iml index 155a4cb..c1e4429 100644 --- a/.idea/llm-workflow-engine.iml +++ b/.idea/llm-workflow-engine.iml @@ -1,7 +1,9 @@ - + + + diff --git a/backend/Dockerfile b/backend/Dockerfile index 1142fda..36e41fa 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -4,15 +4,21 @@ FROM python:3.11-slim # 设置工作目录 WORKDIR /app -# 复制依赖文件并安装 +# 复制依赖文件 +# 注意:这里的 requirements.txt 在 backend/ 目录下 COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt + +# 安装依赖 +RUN pip install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt # 复制所有代码 -COPY app/ ./app/ +# 关键修改:把 backend/ 目录下的内容复制到 /app/backend/ 下 +# 这样镜像内的结构就是 /app/backend/app/... +COPY . ./backend/ # 暴露端口 EXPOSE 8000 # 启动命令 -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file +# 关键修改:路径改为 backend.app.api.route +CMD ["uvicorn", "backend.app.api.route:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/backend/app/nodes/__init__.py b/backend/__init__.py similarity index 100% rename from backend/app/nodes/__init__.py rename to backend/__init__.py diff --git a/backend/app/tool/load_chat_history.py b/backend/api/__init__.py similarity index 100% rename from backend/app/tool/load_chat_history.py rename to backend/api/__init__.py diff --git a/backend/api/route.py b/backend/api/route.py new file mode 100644 index 0000000..b625977 --- /dev/null +++ b/backend/api/route.py @@ -0,0 +1,18 @@ +from fastapi import FastAPI +# 假设这些函数已经在其他地方定义 +from backend.core.items import ChatRequest +from backend.tools.get_all_role_and_chat import get_all_role_and_chat as get_chat_file + +app = FastAPI() + +# 1. 将输入内容持久化存储到本地jsonl方便前端读 +@app.post("/generate_reply") +async def save_input_to_json(chat_request: ChatRequest): + return 0 + +# 2. 从本地jsonl中读取历史对话 +@app.get("/get_all_role_and_chat") +def get_all_role_and_chat(): + # 直接调用导入的函数 + result = get_chat_file() + return result \ No newline at end of file diff --git a/backend/app/core/Start.py b/backend/app/core/Start.py deleted file mode 100644 index 345a65c..0000000 --- a/backend/app/core/Start.py +++ /dev/null @@ -1,46 +0,0 @@ -import base64 -from typing import Any, Dict -from IPython.core.magic_arguments import defaults -from .. import nodes - -class StartNode(): - name = "开始节点" - inputs = { - "user_input": "string", # 用户输入文本 - "stream": "boolean", # 是否流式输出 - "img_switch": "boolean", # 是否处理图片 - "table_switch": "boolean", # 是否处理表格 - "role_name": "string", # 角色名称 - "chat_name": "string" # 会话名称 - } - - async def run(self, text: str = None, image: bytes = None, **kwargs) -> Dict[str, Any]: - # 查空:文本不能为空字符串 - if not text or text.strip() == "": - raise ValueError("文本输入不能为空") - - # 查空:图片数据不能为空 - if image is None or len(image) == 0: - raise ValueError("图片输入不能为空") - - # 将图片字节转换为 Base64 字符串,便于在节点间传递 - image_base64 = base64.b64encode(image).decode('utf-8') - - return { - "text": text, - "image": image_base64 - } - -async def run(is_user,floor_number,mes: str = None, stream: bool = False, img_switch: bool = False,name = "default", - table_switch: bool = False, role_name: str = None, chat_name: str = None,preset: str = None): - # 将输入内容持久化存储到本地json方便前端读 - nodes.save_input_to_json(mes=mes, role_name=role_name, chat_name=chat_name, name=name, is_user=is_user, floor_number=floor_number) - # 对上一条输入内容(已确定不变的内容)调用向量化,根据role和chat嵌入到对应本地数据库 - embed_input(user_input, role_name, chat_name) - # 根据role和chat去读取绑定的世界书 - # 读取预设,进行拼接 - # 调用模型,返回结果 - # 将结果持久化存储到本地json方便前端读(用JSONL) - # 如果img_switch是开的,那么异步调用生图,并存储到目标文件夹里 - # 如果table_switch是开的,那么异步调用表格生成,并存储到目标文件夹里 - # 将结果返回给前端 \ No newline at end of file diff --git a/backend/core/__init__.py b/backend/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config.py b/backend/core/config.py similarity index 87% rename from config.py rename to backend/core/config.py index db25d60..53a4e1e 100644 --- a/config.py +++ b/backend/core/config.py @@ -7,7 +7,7 @@ from dotenv import load_dotenv # __file__ 指向本文件的绝对路径 # .parent 指向 backend/ 目录 # .parent.parent 指向项目根目录 (即包含 backend/ 和 frontend/ 的目录) -PROJECT_ROOT = Path(__file__).resolve().parent +PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent.parent # 修改这里,添加一个 .parent # 2. 加载 .env 文件 # 假设 .env 文件位于项目根目录下 @@ -38,8 +38,10 @@ class Settings: REGEX_FILE = DATA_PATH / "regex_rules.json" VECTORSTORE_PATH = DATA_PATH / "vectorstore" - # ... 其他配置 ... - - -# 实例化配置对象 settings = Settings() + +if __name__ == '__main__': + # 实例化配置对象 + settings = Settings() + print(settings.BASE_PATH) + diff --git a/backend/core/items.py b/backend/core/items.py new file mode 100644 index 0000000..fbdb3b0 --- /dev/null +++ b/backend/core/items.py @@ -0,0 +1,24 @@ +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 diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..61a8314 --- /dev/null +++ b/backend/main.py @@ -0,0 +1,12 @@ +# backend/app/main.py +from fastapi import FastAPI +from .api.routes import router + +app = FastAPI(title="LLM Workflow Engine") + +# 注册路由 +app.include_router(router, prefix="/api") + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/backend/nodes/__init__.py b/backend/nodes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/nodes/text_splitter_node.py b/backend/nodes/text_splitter_node.py similarity index 100% rename from backend/app/nodes/text_splitter_node.py rename to backend/nodes/text_splitter_node.py diff --git a/backend/tools/__init__.py b/backend/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tools/get_all_role_and_chat.py b/backend/tools/get_all_role_and_chat.py new file mode 100644 index 0000000..722046d --- /dev/null +++ b/backend/tools/get_all_role_and_chat.py @@ -0,0 +1,46 @@ +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': + jsonl_files.append(str(file)) + 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 diff --git a/backend/app/nodes/save_input_to_json.py b/backend/tools/save_input_to_json.py similarity index 60% rename from backend/app/nodes/save_input_to_json.py rename to backend/tools/save_input_to_json.py index b4fa9b2..8f01dac 100644 --- a/backend/app/nodes/save_input_to_json.py +++ b/backend/tools/save_input_to_json.py @@ -1,33 +1,37 @@ import json -from typing import Dict, Any from datetime import datetime -import config as cfg +from backend.core import config as cfg from pathlib import Path -def save_input_to_json( - mes: str, - role_name: str, - chat_name: str, - name: str, - is_user: bool, - floor_number: int = 0 -) -> Dict[str, Any]: +# 假设 ChatRequest 定义在这里或者从其他地方导入 +# from backend.app.core.items import ChatRequest + +async def save_input_to_json(chat_request: ChatRequest): """ 保存消息到JSONL文件或处理重roll请求 参数: - mes: 消息内容 - role_name: 角色名称 - chat_name: 对话名称 - name: 发送者名称 - is_user: 是否为用户消息 - floor_number: 楼层号(对话中的第几次回复),用于判断是否为重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" # 确保目录存在 @@ -115,31 +119,34 @@ def save_input_to_json( if __name__ == '__main__': - # 测试普通消息保存 - # save_input_to_json( - # mes="你好", - # role_name="test", - # chat_name="111", - # name="用户", - # is_user=True, - # floor_number=0 - # ) - # - # save_input_to_json( - # mes="你好,我是AI助手", - # role_name="test", - # chat_name="111", - # name="AI", - # is_user=False, - # floor_number=1 - # ) + # 注意:为了在本地运行测试,你需要手动构造一个 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消息 - save_input_to_json( - mes="这是重roll后的新回复2", - role_name="test", - chat_name="111", - name="AI", - is_user=False, - floor_number=2 # 与当前楼层号相同,表示重roll - ) + 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()) diff --git a/backend/workflows/__init__.py b/backend/workflows/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/workflows/main_llm_workflow.py b/backend/workflows/main_llm_workflow.py new file mode 100644 index 0000000..238852c --- /dev/null +++ b/backend/workflows/main_llm_workflow.py @@ -0,0 +1,201 @@ +# 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 diff --git a/data/chat/test/111.jsonl b/data/chat/test/3111.jsonl similarity index 100% rename from data/chat/test/111.jsonl rename to data/chat/test/3111.jsonl diff --git a/data/chat/test1/222.jsonl b/data/chat/test1/222.jsonl new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.yml b/docker-compose.yml index b9d1e84..a032029 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,26 +1,29 @@ version: '3.8' services: - # 后端服务 backend: - build: ./backend # 使用 backend 目录下的 Dockerfile 构建 + build: ./backend + command: uvicorn backend.api.route:app --host 0.0.0.0 --port 8000 --reload ports: - - "8000:8000" # 映射端口:主机8000 -> 容器8000 + - "3001:8000" volumes: - - ./data:/data # 挂载数据目录(持久化) - - ./outputs:/outputs # 挂载输出目录 + - .:/app + - ./data:/data + - ./outputs:/outputs environment: - - DATA_PATH=/data - - OUTPUT_PATH=/outputs - restart: always + - PYTHONUNBUFFERED=1 + restart: unless-stopped - # 前端服务 frontend: - build: ./frontend # 使用 frontend 目录下的 Dockerfile 构建 + build: ./frontend + command: streamlit run app.py --server.port=8501 --server.address=0.0.0.0 --server.fileWatcherType=poll ports: - - "8501:8501" # 映射端口:主机8501 -> 容器8501 + - "3000:8501" environment: - - BACKEND_URL=http://backend:8000 # 后端内部地址 + - BACKEND_URL=http://backend:8000 + - PYTHONUNBUFFERED=1 + volumes: + - ./frontend:/app # 确保宿主机路径与容器内路径一致 depends_on: - - backend # 等后端启动后再启动前端 - restart: always \ No newline at end of file + - backend + restart: unless-stopped diff --git a/frontend/Dockerfile b/frontend/Dockerfile index ecfc272..77f9504 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -6,7 +6,7 @@ WORKDIR /app # 复制依赖文件并安装 COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt +RUN pip install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt # 复制所有代码 COPY . . @@ -14,5 +14,5 @@ COPY . . # 暴露端口 EXPOSE 8501 -# 启动命令 +# 启动命令(使用 8501 端口,与 docker-compose 映射保持一致) CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"] \ No newline at end of file diff --git a/frontend/app.py b/frontend/app.py index d809223..bf4f2ed 100644 --- a/frontend/app.py +++ b/frontend/app.py @@ -1,3 +1,4 @@ +import requests import streamlit as st import os from pathlib import Path @@ -11,6 +12,8 @@ st.set_page_config( 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(""" @@ -114,7 +117,142 @@ st.markdown(""" 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; + } + + """, unsafe_allow_html=True) # --- 状态初始化 --- @@ -137,19 +275,44 @@ if "splice_blocks" not in st.session_state: {"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 # --- 顶部工具栏 --- -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("

AI WorkFlow Engine

", unsafe_allow_html=True) -with c_top3: - if st.button("⚙️ 设置", key="btn_settings"): - st.toast("全局设置预留") +# 工具栏切换按钮 +st.markdown(""" +
+ +
+ +""", 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("

AI WorkFlow Engine

", unsafe_allow_html=True) + with c_top3: + if st.button("⚙️ 设置", key="btn_settings"): + st.toast("全局设置预留") st.divider() @@ -160,6 +323,9 @@ col_left, col_mid, col_right = st.columns([1, 3, 1], gap="small") # 1. 左侧:预设与拼接管理 (蓝白风格适配) # ======================= with col_left: + # 使用自定义容器类 + st.markdown('
', unsafe_allow_html=True) + st.markdown("#### 📜 全局预设") c_pre1, c_pre2 = st.columns([4, 1]) with c_pre1: @@ -216,44 +382,134 @@ with col_left: if st.button("+ 添加拼接块", use_container_width=True): st.toast("添加新功能预留") + st.markdown('
', unsafe_allow_html=True) + # ======================= # 2. 中间:流式对话区 (动态读取历史) # ======================= with col_mid: - # 顶部控制条 - c_ctrl1, c_ctrl2 = st.columns([4, 1]) + # 使用自定义容器类 + st.markdown('
', unsafe_allow_html=True) + + # --- 控制区域 --- + c_ctrl1, c_ctrl2, c_ctrl3 = st.columns([3, 2, 1]) + with c_ctrl1: - st.caption("当前会话:Active_Session_01") + # --- 数据集选择下拉框 --- + 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() - # --- 核心:动态渲染历史记录 --- - # 使用 st.container 包裹,确保每次 rerun 都能完整重绘 - chat_container = st.container() + # 显示当前会话信息 + 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") - with chat_container: - # 遍历 session_state 中的消息历史 + # --- 核心:动态渲染历史记录 --- + # 使用自定义容器类包裹聊天区域 + st.markdown('
', 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"] - # 根据开关决定是否解析 HTML if st.session_state.render_html and msg["role"] == "assistant": - # 允许 HTML 标签 st.markdown(content, unsafe_allow_html=True) else: - # 纯文本/Markdown 模式 st.markdown(content) - # 可选:在每条消息下添加操作按钮 (编辑/复制/重试) - 预留 - # with st.expander("...", expanded=False): ... + st.markdown('
', unsafe_allow_html=True) # --- 输入区域 --- - st.markdown("---") + # 使用自定义容器类包裹输入区域 + st.markdown('
', unsafe_allow_html=True) # 聊天输入框 user_input = st.chat_input("输入消息... (支持 /命令)") @@ -262,25 +518,19 @@ with col_mid: # 1. 将用户输入加入历史 st.session_state.messages.append({"role": "user", "content": user_input}) - # 2. 触发重新渲染,此时上方循环会立即显示用户消息 - # 注意:Streamlit 是同步执行的,要模拟“流式”通常需要后端配合 yield - # 这里为了演示前端动态读取,我们先显示用户消息,然后模拟一个后台任务 - - # 占位符用于显示正在生成的状态或流式内容 + # 2. 触发重新渲染 with st.chat_message("assistant"): message_placeholder = st.empty() message_placeholder.markdown("*思考中...*") - # === 模拟后端流式响应 (实际项目中此处替换为 requests.post(stream=True)) === + # === 模拟后端流式响应 === full_response = "" - # 构造一个包含 HTML 的回复用于测试 simulated_text = f"收到您的指令:**{user_input}**。\n\n这是一个测试回复,如果您开启了 **HTML 渲染**,下方将显示彩色文本和表格:
蓝色高亮文本
属性
状态正常
" - # 简单的逐字模拟 (实际应来自后端 chunk) chunks = simulated_text.split(" ") for chunk in chunks: full_response += chunk + " " - time.sleep(0.1) # 模拟网络延迟 + time.sleep(0.1) if st.session_state.render_html: message_placeholder.markdown(full_response, unsafe_allow_html=True) @@ -293,10 +543,17 @@ with col_mid: # 强制刷新以确保持久化显示 st.rerun() + st.markdown('
', unsafe_allow_html=True) + + st.markdown('
', unsafe_allow_html=True) + # ======================= # 3. 右侧:图片与骰子 (蓝白风格) # ======================= with col_right: + # 使用自定义容器类 + st.markdown('
', unsafe_allow_html=True) + st.markdown("#### 🖼️ 本地图库") img_path = Path(st.session_state.image_folder) img_path.mkdir(parents=True, exist_ok=True) @@ -346,4 +603,6 @@ with col_right: f"
{res}
", unsafe_allow_html=True) with c_r2: - st.caption(f"目标:{selected_diff.split('(')[1].strip(')')}") \ No newline at end of file + st.caption(f"目标:{selected_diff.split('(')[1].strip(')')}") + + st.markdown('
', unsafe_allow_html=True) diff --git a/frontend/components/chat_window.py b/frontend/components/chat_window.py index d0609f9..7e1fbbc 100644 --- a/frontend/components/chat_window.py +++ b/frontend/components/chat_window.py @@ -29,7 +29,7 @@ def render_chat_window(backend_url): full_response = "" # 模拟流式接收 (实际需使用 requests stream 或 websocket) - # POST /api/chat/stream + # POST /api/role/stream try: # 伪代码示例: # with requests.post(f"{backend_url}/api/chat/stream", json={"message": prompt}, stream=True) as r: