初始化仅前端版本

This commit is contained in:
2026-03-16 21:47:06 +08:00
parent 43adf1a7c4
commit 5a78b7b392
25 changed files with 699 additions and 153 deletions

11
.env
View File

@@ -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

1
.idea/.gitignore generated vendored
View File

@@ -6,3 +6,4 @@
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
.env

View File

@@ -1,7 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
</content>
<orderEntry type="jdk" jdkName="Python 3.9 (pythonProject1)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>

View File

@@ -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"]
# 关键修改:路径改为 backend.app.api.route
CMD ["uvicorn", "backend.app.api.route:app", "--host", "0.0.0.0", "--port", "8000"]

18
backend/api/route.py Normal file
View File

@@ -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

View File

@@ -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是开的那么异步调用表格生成并存储到目标文件夹里
# 将结果返回给前端

0
backend/core/__init__.py Normal file
View File

View File

@@ -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)

24
backend/core/items.py Normal file
View File

@@ -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

12
backend/main.py Normal file
View File

@@ -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)

View File

View File

View File

@@ -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

View File

@@ -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())

View File

View File

@@ -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

View File

View File

@@ -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
- backend
restart: unless-stopped

View File

@@ -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"]

View File

@@ -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;
}
</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)
# --- 状态初始化 ---
@@ -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("<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.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()
@@ -160,6 +323,9 @@ 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:
@@ -216,44 +382,134 @@ with col_left:
if st.button("+ 添加拼接块", use_container_width=True):
st.toast("添加新功能预留")
st.markdown('</div>', unsafe_allow_html=True)
# =======================
# 2. 中间:流式对话区 (动态读取历史)
# =======================
with col_mid:
# 顶部控制条
c_ctrl1, c_ctrl2 = st.columns([4, 1])
# 使用自定义容器类
st.markdown('<div class="middle-column">', 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('<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"]
# 根据开关决定是否解析 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('</div>', unsafe_allow_html=True)
# --- 输入区域 ---
st.markdown("---")
# 使用自定义容器类包裹输入区域
st.markdown('<div class="input-area">', 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 渲染**,下方将显示彩色文本和表格:<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>"
# 简单的逐字模拟 (实际应来自后端 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('</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)
@@ -346,4 +603,6 @@ with col_right:
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.caption(f"目标:{selected_diff.split('(')[1].strip(')')}")
st.markdown('</div>', unsafe_allow_html=True)

View File

@@ -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: