170 lines
4.0 KiB
Python
170 lines
4.0 KiB
Python
"""
|
||
左侧栏组件
|
||
包含用户登录和历史对话列表
|
||
"""
|
||
|
||
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("加载对话失败")
|