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) # 聊天输入框 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 渲染**,下方将显示彩色文本和表格:
蓝色高亮文本
属性
状态正常
" 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('
', 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)