初始化仅前端版本
This commit is contained in:
@@ -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"]
|
||||
329
frontend/app.py
329
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;
|
||||
}
|
||||
</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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user