"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import { useTranslation } from "react-i18next"; import { SidebarShell } from "@/components/sidebar/SidebarShell"; import { LogoutButton } from "@/components/auth/LogoutButton"; import { AdminLink } from "@/components/auth/AdminLink"; import { ProfileLink } from "@/components/auth/ProfileLink"; import { useUnifiedChat } from "@/context/UnifiedChatContext"; import { deleteSession, listSessions, updateSessionTitle, type SessionSummary, } from "@/lib/session-api"; export default function WorkspaceSidebar() { const { t } = useTranslation(); const router = useRouter(); const { newSession, cancelStreamingTurn, selectedSessionId, sessionStatuses, sidebarRefreshToken, } = useUnifiedChat(); const [sessions, setSessions] = useState([]); const [loadingSessions, setLoadingSessions] = useState(false); const hasLoadedSessionsRef = useRef(false); const refreshSessions = useCallback(async () => { if (!hasLoadedSessionsRef.current) { setLoadingSessions(true); } try { setSessions(await listSessions(50, 0, { force: true })); hasLoadedSessionsRef.current = true; } catch (error) { console.error("Failed to load sessions", error); } finally { setLoadingSessions(false); } }, []); // First mount shows the skeleton; subsequent refreshes triggered by // ``sidebarRefreshToken`` (STREAM_END, server-side session bind, // turn deletion) silently swap in the new list. Resetting the ref // each refresh briefly re-renders the loading skeleton, which the // user perceives as a flicker on every message send / Answer Now. useEffect(() => { void refreshSessions(); }, [refreshSessions, sidebarRefreshToken]); const orderedSessions = sessions .map((session, index) => { const runtime = sessionStatuses[session.session_id]; return { index, session: runtime ? { ...session, status: runtime.status, active_turn_id: runtime.activeTurnId || session.active_turn_id, } : session, }; }) .sort((a, b) => { const aPriority = a.session.status === "running" ? 0 : 1; const bPriority = b.session.status === "running" ? 0 : 1; if (aPriority !== bPriority) return aPriority - bPriority; return a.index - b.index; }) .map(({ session }) => session); // Cancel any in-flight streaming turn before starting a fresh session, so a // new chat never inherits a still-running turn (mirrors handleDeleteSession). const handleNewChat = useCallback(() => { cancelStreamingTurn(); newSession(); router.push("/home"); }, [cancelStreamingTurn, newSession, router]); const handleSelectSession = useCallback( async (sessionId: string) => { router.push(`/home/${sessionId}`); }, [router], ); const handleRenameSession = useCallback( async (sessionId: string, title: string) => { const updated = await updateSessionTitle(sessionId, title); setSessions((prev) => prev.map((session) => session.session_id === sessionId ? { ...session, title: updated.title, updated_at: updated.updated_at, } : session, ), ); }, [], ); const handleDeleteSession = useCallback( async (sessionId: string) => { if (!window.confirm(t("Delete this chat history?"))) return; await deleteSession(sessionId); setSessions((prev) => prev.filter((session) => session.session_id !== sessionId), ); if (selectedSessionId === sessionId) { cancelStreamingTurn(); newSession(); router.push("/home"); } }, [cancelStreamingTurn, newSession, router, selectedSessionId, t], ); return ( ( <> )} /> ); }