Files
ailine/frontend/components/sidebar.py

165 lines
4.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
左侧栏组件
包含用户登录和历史对话列表
"""
import streamlit as st
from datetime import datetime
# 使用绝对导入
from frontend.state import AppState
from frontend.api_client import api_client
def render_sidebar():
"""渲染左侧栏"""
# 顶部放置新对话按钮,像 ChatGPT/DeepSeek 一样显眼
_render_history_actions()
st.divider()
# 历史列表
_render_history_section()
# 底部放用户部分
st.divider()
_render_user_section()
def _render_user_section():
"""渲染用户登录区域"""
# st.header("👤 用户") # 移除显眼的标题,改用更柔和的 caption
st.caption("👤 用户管理")
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可能导致对话污染",
label_visibility="collapsed"
)
if st.button("进入", type="secondary", use_container_width=True):
AppState.login(username)
_refresh_threads()
st.rerun()
# st.info("💡 建议登录以隔离对话历史") # 移除多余色块
def _render_user_info():
"""渲染用户信息"""
st.markdown(f"**当前用户**: `{AppState.get_user_id()}`")
if st.button("切换用户", type="secondary", use_container_width=True):
AppState.logout()
_refresh_threads()
st.rerun()
def _render_history_section():
"""渲染历史对话列表"""
col1, col2 = st.columns([3, 1])
with col1:
st.caption("📚 对话历史")
with col2:
if st.button("🔄", help="刷新列表", key="refresh_history_btn"):
_refresh_threads()
_render_thread_list()
def _render_history_actions():
"""渲染历史操作按钮"""
# 移除了 type="primary",让它变成普通的线框按钮,不再是大红块
if st.button(" 新对话", use_container_width=True):
AppState.start_new_thread()
st.rerun()
def _render_thread_list():
"""渲染线程列表"""
# 仅在初次加载时拉取,或由外部主动调用 _refresh_threads() 更新
if "threads_loaded" not in st.session_state:
_refresh_threads()
st.session_state.threads_loaded = True
threads = AppState.get_threads()
if not threads:
st.caption("暂无对话历史")
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", "新对话")
# 判断是否为当前线程
is_current = thread_id == AppState.get_current_thread_id()
# 根据是否当前线程改变按钮样式
btn_type = "primary" if is_current else "tertiary"
# 为了避免内容过长,截断摘要
display_text = summary[:15] + "..." if len(summary) > 15 else summary
if st.button(
display_text,
key=f"thread_{thread_id}",
help=f"完整摘要: {summary}",
use_container_width=True,
type=btn_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("加载对话失败")