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