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("""
""", 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": "好的,这是一个示例角色卡:
姓名: 艾莉娅
年龄: 24
背景: 一位来自北方边境的流浪法师。
(这是 HTML 渲染测试)"}
]
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("""
▼
""", 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()
# --- 三栏布局 ---
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:
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"
{block['name']}
",
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("
", unsafe_allow_html=True)
if st.button("+ 添加拼接块", use_container_width=True):
st.toast("添加新功能预留")
st.markdown('
', unsafe_allow_html=True)
# =======================
# 2. 中间:流式对话区 (动态读取历史)
# =======================
with col_mid:
# 使用自定义容器类
st.markdown('', 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('
', 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('
', unsafe_allow_html=True)
# --- 输入区域 ---
# 使用自定义容器类包裹输入区域
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)
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"
",
unsafe_allow_html=True)
st.image(str(img_path / img_name), use_container_width=True)
st.caption(img_name)
st.markdown("
", 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"
{res}
",
unsafe_allow_html=True)
with c_r2:
st.caption(f"目标:{selected_diff.split('(')[1].strip(')')}")
st.markdown('
', unsafe_allow_html=True)