This commit is contained in:
4
frontend/components/__init__.py
Normal file
4
frontend/components/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
"""
|
||||
UI 组件模块
|
||||
包含所有可复用的 Streamlit 组件
|
||||
"""
|
||||
148
frontend/components/chat_area.py
Normal file
148
frontend/components/chat_area.py
Normal file
@@ -0,0 +1,148 @@
|
||||
"""
|
||||
中间聊天区组件
|
||||
包含模型选择、消息显示和输入框
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
|
||||
# 使用绝对导入
|
||||
from frontend.state import AppState
|
||||
from frontend.api_client import api_client
|
||||
from frontend.config import config
|
||||
|
||||
|
||||
def render_chat_area():
|
||||
"""渲染中间聊天区域"""
|
||||
# 模型选择器
|
||||
_render_model_selector()
|
||||
|
||||
st.divider()
|
||||
|
||||
# 聊天容器
|
||||
_render_chat_container()
|
||||
|
||||
# 输入框
|
||||
_render_input_box()
|
||||
|
||||
|
||||
def _render_model_selector():
|
||||
"""渲染模型选择器"""
|
||||
col_model, col_empty = st.columns([2, 3])
|
||||
|
||||
with col_model:
|
||||
selected_model = st.selectbox(
|
||||
"🧠 选择模型",
|
||||
options=list(config.model_options.keys()),
|
||||
format_func=lambda x: config.model_options[x],
|
||||
index=_get_model_index()
|
||||
)
|
||||
AppState.set_selected_model(selected_model)
|
||||
|
||||
|
||||
def _get_model_index() -> int:
|
||||
"""
|
||||
获取当前选中模型的索引
|
||||
|
||||
Returns:
|
||||
模型索引
|
||||
"""
|
||||
current_model = AppState.get_selected_model()
|
||||
model_keys = list(config.model_options.keys())
|
||||
return model_keys.index(current_model) if current_model in model_keys else 0
|
||||
|
||||
|
||||
def _render_chat_container():
|
||||
"""渲染聊天消息容器"""
|
||||
chat_container = st.container(height=500)
|
||||
|
||||
with chat_container:
|
||||
messages = AppState.get_messages()
|
||||
for msg in messages:
|
||||
with st.chat_message(msg["role"]):
|
||||
st.markdown(msg["content"])
|
||||
|
||||
|
||||
def _render_input_box():
|
||||
"""渲染输入框和流式响应处理"""
|
||||
if prompt := st.chat_input("请输入您的问题...", key="chat_input"):
|
||||
_handle_user_message(prompt)
|
||||
|
||||
|
||||
def _handle_user_message(prompt: str):
|
||||
"""
|
||||
处理用户消息
|
||||
|
||||
Args:
|
||||
prompt: 用户输入的消息
|
||||
"""
|
||||
# 显示用户消息
|
||||
with st.chat_message("user"):
|
||||
st.markdown(prompt)
|
||||
AppState.add_message("user", prompt)
|
||||
|
||||
# 流式调用 AI 回复
|
||||
_handle_ai_response()
|
||||
|
||||
|
||||
def _handle_ai_response():
|
||||
"""处理 AI 流式响应"""
|
||||
with st.chat_message("assistant"):
|
||||
message_placeholder = st.empty()
|
||||
tool_status_placeholder = st.empty()
|
||||
full_response = ""
|
||||
|
||||
# 调用流式 API
|
||||
stream = api_client.chat_stream(
|
||||
message=AppState.get_messages()[-1]["content"],
|
||||
thread_id=AppState.get_current_thread_id(),
|
||||
model=AppState.get_selected_model(),
|
||||
user_id=AppState.get_user_id()
|
||||
)
|
||||
|
||||
# 消费流式响应
|
||||
for event in stream:
|
||||
event_type = event.get("type")
|
||||
|
||||
if event_type == "token":
|
||||
# 逐字输出
|
||||
full_response += event.get("content", "")
|
||||
message_placeholder.markdown(full_response + "▌")
|
||||
|
||||
elif event_type == "tool_start":
|
||||
# 工具调用开始
|
||||
tool_name = event.get("tool", "")
|
||||
tool_status_placeholder.info(f"🔧 调用工具: {tool_name}...")
|
||||
|
||||
elif event_type == "tool_end":
|
||||
# 工具调用完成
|
||||
tool_name = event.get("tool", "")
|
||||
tool_status_placeholder.success(f"✅ 工具 {tool_name} 完成")
|
||||
tool_status_placeholder.empty()
|
||||
|
||||
elif event_type == "done":
|
||||
# 对话完成
|
||||
_show_completion_stats(event)
|
||||
|
||||
elif event_type == "error":
|
||||
# 错误处理
|
||||
st.error(f"❌ 错误: {event.get('message', '未知错误')}")
|
||||
|
||||
# 显示完整响应
|
||||
message_placeholder.markdown(full_response)
|
||||
AppState.add_message("assistant", full_response)
|
||||
tool_status_placeholder.empty()
|
||||
|
||||
|
||||
def _show_completion_stats(event: dict):
|
||||
"""
|
||||
显示对话完成统计信息
|
||||
|
||||
Args:
|
||||
event: 完成事件数据
|
||||
"""
|
||||
token_usage = event.get("token_usage", {})
|
||||
elapsed = event.get("elapsed_time", 0)
|
||||
|
||||
if token_usage:
|
||||
total_tokens = token_usage.get("total_tokens", 0)
|
||||
st.caption(f"📊 消耗 {total_tokens} tokens | ⏱️ {elapsed:.2f}s")
|
||||
59
frontend/components/info_panel.py
Normal file
59
frontend/components/info_panel.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""
|
||||
右侧信息面板组件
|
||||
显示会话信息和统计数据
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
|
||||
# 使用绝对导入
|
||||
from frontend.state import AppState
|
||||
|
||||
|
||||
def render_info_panel():
|
||||
"""渲染右侧信息面板"""
|
||||
st.header("📊 会话信息")
|
||||
|
||||
# 当前线程信息
|
||||
_render_thread_info()
|
||||
|
||||
st.divider()
|
||||
|
||||
# 消息统计
|
||||
_render_message_stats()
|
||||
|
||||
st.divider()
|
||||
|
||||
# 使用提示
|
||||
_render_tips()
|
||||
|
||||
|
||||
def _render_thread_info():
|
||||
"""渲染当前线程信息"""
|
||||
st.subheader("当前对话")
|
||||
thread_id = AppState.get_current_thread_id()
|
||||
st.code(thread_id[:8] + "...", language=None)
|
||||
|
||||
|
||||
def _render_message_stats():
|
||||
"""渲染消息统计"""
|
||||
st.subheader("消息统计")
|
||||
|
||||
stats = AppState.get_message_stats()
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
st.metric("用户消息", stats["user"])
|
||||
with col2:
|
||||
st.metric("AI 回复", stats["assistant"])
|
||||
|
||||
|
||||
def _render_tips():
|
||||
"""渲染使用提示"""
|
||||
st.subheader("💡 使用提示")
|
||||
st.markdown("""
|
||||
- 左侧可切换历史对话
|
||||
- 点击"新对话"开始新话题
|
||||
- 登录后对话历史隔离
|
||||
- 支持流式实时响应
|
||||
- 模型可随时切换
|
||||
""")
|
||||
169
frontend/components/sidebar.py
Normal file
169
frontend/components/sidebar.py
Normal file
@@ -0,0 +1,169 @@
|
||||
"""
|
||||
左侧栏组件
|
||||
包含用户登录和历史对话列表
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
from datetime import datetime
|
||||
|
||||
# 使用绝对导入
|
||||
from frontend.state import AppState
|
||||
from frontend.api_client import api_client
|
||||
from frontend.config import config
|
||||
|
||||
|
||||
def render_sidebar():
|
||||
"""渲染左侧栏"""
|
||||
_render_user_section()
|
||||
st.divider()
|
||||
_render_history_section()
|
||||
|
||||
|
||||
def _render_user_section():
|
||||
"""渲染用户登录区域"""
|
||||
st.header("👤 用户")
|
||||
|
||||
if not AppState.is_logged_in():
|
||||
_render_login_form()
|
||||
else:
|
||||
_render_user_info()
|
||||
|
||||
|
||||
def _render_login_form():
|
||||
"""渲染登录表单"""
|
||||
username = st.text_input(
|
||||
"输入用户名(可选)",
|
||||
key="login_input",
|
||||
placeholder="留空使用默认用户",
|
||||
help="未登录将使用 default_user,可能导致对话污染"
|
||||
)
|
||||
|
||||
if st.button("✅ 进入", type="primary", use_container_width=True):
|
||||
AppState.login(username)
|
||||
_refresh_threads()
|
||||
st.rerun()
|
||||
|
||||
st.info("💡 建议登录以隔离对话历史")
|
||||
|
||||
|
||||
def _render_user_info():
|
||||
"""渲染用户信息"""
|
||||
st.success(f"✅ 当前用户: `{AppState.get_user_id()}`")
|
||||
|
||||
if st.button("🔄 切换用户", use_container_width=True):
|
||||
AppState.logout()
|
||||
st.rerun()
|
||||
|
||||
|
||||
def _render_history_section():
|
||||
"""渲染历史对话列表"""
|
||||
st.header("📚 对话历史")
|
||||
|
||||
# 操作按钮
|
||||
_render_history_actions()
|
||||
|
||||
st.divider()
|
||||
|
||||
# 历史列表
|
||||
_render_thread_list()
|
||||
|
||||
|
||||
def _render_history_actions():
|
||||
"""渲染历史操作按钮"""
|
||||
if st.button("🔄 刷新列表", use_container_width=True):
|
||||
_refresh_threads()
|
||||
|
||||
if st.button("➕ 新对话", type="primary", use_container_width=True):
|
||||
AppState.start_new_thread()
|
||||
st.rerun()
|
||||
|
||||
|
||||
def _render_thread_list():
|
||||
"""渲染线程列表"""
|
||||
threads = AppState.get_threads()
|
||||
|
||||
if not threads:
|
||||
st.info("暂无对话历史")
|
||||
return
|
||||
|
||||
for thread in threads:
|
||||
_render_thread_item(thread)
|
||||
|
||||
|
||||
def _render_thread_item(thread: dict):
|
||||
"""
|
||||
渲染单个线程项
|
||||
|
||||
Args:
|
||||
thread: 线程信息字典
|
||||
"""
|
||||
thread_id = thread["thread_id"]
|
||||
summary = thread.get("summary", "空对话")
|
||||
message_count = thread.get("message_count", 0)
|
||||
last_updated = thread.get("last_updated", "")
|
||||
|
||||
# 格式化时间
|
||||
time_str = _format_time(last_updated)
|
||||
|
||||
# 判断是否为当前线程
|
||||
is_current = thread_id == AppState.get_current_thread_id()
|
||||
button_type = "primary" if is_current else "secondary"
|
||||
|
||||
# 截断摘要
|
||||
summary_display = summary[:config.summary_max_length]
|
||||
if len(summary) > config.summary_max_length:
|
||||
summary_display += "..."
|
||||
|
||||
# 渲染按钮
|
||||
if st.button(
|
||||
f"💬 {summary_display}\n\n🕐 {time_str} | {message_count}条",
|
||||
key=f"thread_{thread_id}",
|
||||
use_container_width=True,
|
||||
type=button_type
|
||||
):
|
||||
_load_thread(thread_id)
|
||||
|
||||
|
||||
def _format_time(time_str: str) -> str:
|
||||
"""
|
||||
格式化时间字符串
|
||||
|
||||
Args:
|
||||
time_str: ISO 格式时间字符串
|
||||
|
||||
Returns:
|
||||
格式化后的时间字符串
|
||||
"""
|
||||
if not time_str:
|
||||
return "未知"
|
||||
|
||||
try:
|
||||
dt = datetime.fromisoformat(time_str.replace("Z", "+00:00"))
|
||||
return dt.strftime("%m-%d %H:%M")
|
||||
except Exception:
|
||||
return time_str[:10]
|
||||
|
||||
|
||||
def _refresh_threads():
|
||||
"""刷新历史线程列表"""
|
||||
threads = api_client.get_user_threads(AppState.get_user_id())
|
||||
AppState.set_threads(threads)
|
||||
|
||||
|
||||
def _load_thread(thread_id: str):
|
||||
"""
|
||||
加载指定线程的消息历史
|
||||
|
||||
Args:
|
||||
thread_id: 线程 ID
|
||||
"""
|
||||
messages = api_client.get_thread_messages(thread_id, AppState.get_user_id())
|
||||
|
||||
if messages:
|
||||
AppState.set_current_thread_id(thread_id)
|
||||
AppState.clear_messages()
|
||||
for msg in messages:
|
||||
AppState.add_message(msg["role"], msg["content"])
|
||||
st.rerun()
|
||||
else:
|
||||
st.error("加载对话失败")
|
||||
Reference in New Issue
Block a user