e4dcfc49aa
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
146 lines
4.4 KiB
TypeScript
146 lines
4.4 KiB
TypeScript
"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<SessionSummary[]>([]);
|
|
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 (
|
|
<SidebarShell
|
|
showSessions
|
|
sessions={orderedSessions}
|
|
activeSessionId={selectedSessionId}
|
|
loadingSessions={loadingSessions}
|
|
onNewChat={handleNewChat}
|
|
onSelectSession={handleSelectSession}
|
|
onRenameSession={handleRenameSession}
|
|
onDeleteSession={handleDeleteSession}
|
|
footerSlot={(collapsed) => (
|
|
<>
|
|
<ProfileLink collapsed={collapsed} />
|
|
<AdminLink collapsed={collapsed} />
|
|
<LogoutButton collapsed={collapsed} />
|
|
</>
|
|
)}
|
|
/>
|
|
);
|
|
}
|