fix(chat): add history loading skeletons
This commit is contained in:
@@ -8,6 +8,56 @@ const renderWithTheme = (ui: React.ReactElement) =>
|
|||||||
render(<ThemeProvider theme={createTheme()}>{ui}</ThemeProvider>);
|
render(<ThemeProvider theme={createTheme()}>{ui}</ThemeProvider>);
|
||||||
|
|
||||||
describe("AgentHistoryPanel", () => {
|
describe("AgentHistoryPanel", () => {
|
||||||
|
it("shows skeleton rows while history sessions are loading", () => {
|
||||||
|
renderWithTheme(
|
||||||
|
<AgentHistoryPanel
|
||||||
|
sessions={[]}
|
||||||
|
isLoadingSessions
|
||||||
|
onNewSession={jest.fn()}
|
||||||
|
onRenameSession={jest.fn()}
|
||||||
|
onSelectSession={jest.fn()}
|
||||||
|
onDeleteSession={jest.fn()}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByLabelText("正在加载历史会话")).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("暂无历史会话")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("disables the loading history session item", () => {
|
||||||
|
const onSelectSession = jest.fn();
|
||||||
|
const onRenameSession = jest.fn();
|
||||||
|
const onDeleteSession = jest.fn();
|
||||||
|
|
||||||
|
renderWithTheme(
|
||||||
|
<AgentHistoryPanel
|
||||||
|
sessions={[
|
||||||
|
{
|
||||||
|
id: "session-loading",
|
||||||
|
title: "正在加载的会话",
|
||||||
|
createdAt: Date.now(),
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
loadingSessionId="session-loading"
|
||||||
|
onNewSession={jest.fn()}
|
||||||
|
onRenameSession={onRenameSession}
|
||||||
|
onSelectSession={onSelectSession}
|
||||||
|
onDeleteSession={onDeleteSession}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByText("正在加载的会话")).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole("button", { name: "修改会话标题" })).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole("button", { name: "删除会话" })).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByLabelText("正在加载会话 正在加载的会话"));
|
||||||
|
|
||||||
|
expect(onSelectSession).not.toHaveBeenCalled();
|
||||||
|
expect(onRenameSession).not.toHaveBeenCalled();
|
||||||
|
expect(onDeleteSession).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("renames a history session from the list", () => {
|
it("renames a history session from the list", () => {
|
||||||
const onRenameSession = jest.fn();
|
const onRenameSession = jest.fn();
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
Divider,
|
Divider,
|
||||||
IconButton,
|
IconButton,
|
||||||
Paper,
|
Paper,
|
||||||
|
Skeleton,
|
||||||
Stack,
|
Stack,
|
||||||
TextField,
|
TextField,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@@ -34,9 +35,11 @@ type AgentHistoryPanelProps = {
|
|||||||
sessions: ChatSessionSummary[];
|
sessions: ChatSessionSummary[];
|
||||||
activeSessionId?: string;
|
activeSessionId?: string;
|
||||||
isHydrating?: boolean;
|
isHydrating?: boolean;
|
||||||
|
isLoadingSessions?: boolean;
|
||||||
|
loadingSessionId?: string;
|
||||||
onNewSession: () => void;
|
onNewSession: () => void;
|
||||||
onRenameSession: (sessionId: string, title: string) => void;
|
onRenameSession: (sessionId: string, title: string) => void;
|
||||||
onSelectSession: (sessionId: string) => void;
|
onSelectSession: (sessionId: string, title: string) => void;
|
||||||
onDeleteSession: (sessionId: string) => void;
|
onDeleteSession: (sessionId: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -76,6 +79,8 @@ export const AgentHistoryPanel = ({
|
|||||||
sessions,
|
sessions,
|
||||||
activeSessionId,
|
activeSessionId,
|
||||||
isHydrating = false,
|
isHydrating = false,
|
||||||
|
isLoadingSessions = false,
|
||||||
|
loadingSessionId,
|
||||||
onNewSession,
|
onNewSession,
|
||||||
onRenameSession,
|
onRenameSession,
|
||||||
onSelectSession,
|
onSelectSession,
|
||||||
@@ -127,6 +132,30 @@ export const AgentHistoryPanel = ({
|
|||||||
(session) => session.id === pendingDeleteSessionId,
|
(session) => session.id === pendingDeleteSessionId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const renderSessionListSkeleton = () => (
|
||||||
|
<Stack spacing={1} aria-label="正在加载历史会话">
|
||||||
|
{Array.from({ length: 6 }, (_, index) => (
|
||||||
|
<Paper
|
||||||
|
key={index}
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
px: 1.25,
|
||||||
|
py: 1,
|
||||||
|
borderRadius: 3,
|
||||||
|
bgcolor: alpha("#fff", 0.48),
|
||||||
|
border: `1px solid ${alpha("#fff", 0.68)}`,
|
||||||
|
boxShadow: `0 4px 12px ${alpha("#000", 0.025)}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack spacing={0.75} sx={{ minHeight: 46, justifyContent: "center" }}>
|
||||||
|
<Skeleton variant="text" width={`${72 - (index % 3) * 12}%`} height={18} />
|
||||||
|
<Skeleton variant="text" width="32%" height={14} />
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
|
||||||
const handleStartRename = (sessionId: string, title: string) => {
|
const handleStartRename = (sessionId: string, title: string) => {
|
||||||
setEditingSessionId(sessionId);
|
setEditingSessionId(sessionId);
|
||||||
setDraftTitle(title);
|
setDraftTitle(title);
|
||||||
@@ -215,7 +244,9 @@ export const AgentHistoryPanel = ({
|
|||||||
<Divider sx={{ borderColor: alpha("#fff", 0.6) }} />
|
<Divider sx={{ borderColor: alpha("#fff", 0.6) }} />
|
||||||
|
|
||||||
<Box sx={{ flex: 1, overflowY: "auto", px: 1.25, py: 1.25 }}>
|
<Box sx={{ flex: 1, overflowY: "auto", px: 1.25, py: 1.25 }}>
|
||||||
{sessions.length === 0 ? (
|
{isLoadingSessions ? (
|
||||||
|
renderSessionListSkeleton()
|
||||||
|
) : sessions.length === 0 ? (
|
||||||
<Stack
|
<Stack
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
@@ -271,27 +302,42 @@ export const AgentHistoryPanel = ({
|
|||||||
<Stack spacing={1}>
|
<Stack spacing={1}>
|
||||||
{groupSessions.map((session) => {
|
{groupSessions.map((session) => {
|
||||||
const isActive = session.id === activeSessionId;
|
const isActive = session.id === activeSessionId;
|
||||||
|
const isLoading = session.id === loadingSessionId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper
|
<Paper
|
||||||
key={session.id}
|
key={session.id}
|
||||||
elevation={0}
|
elevation={0}
|
||||||
|
aria-label={isLoading ? `正在加载会话 ${session.title}` : undefined}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (editingSessionId === session.id) return;
|
if (editingSessionId === session.id || isLoading) return;
|
||||||
onSelectSession(session.id);
|
onSelectSession(session.id, session.title);
|
||||||
}}
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
px: 1.25,
|
px: 1.25,
|
||||||
py: 1,
|
py: 1,
|
||||||
borderRadius: 3,
|
borderRadius: 3,
|
||||||
cursor: isHydrating ? "default" : "pointer",
|
cursor: isHydrating || isLoading ? "default" : "pointer",
|
||||||
bgcolor: isActive ? alpha("#00acc1", 0.12) : alpha("#fff", 0.56),
|
bgcolor:
|
||||||
border: `1px solid ${isActive ? alpha("#00acc1", 0.25) : alpha("#fff", 0.72)}`,
|
isActive || isLoading
|
||||||
boxShadow: isActive ? `0 8px 20px ${alpha("#00acc1", 0.12)}` : `0 4px 12px ${alpha("#000", 0.03)}`,
|
? alpha("#00acc1", 0.12)
|
||||||
|
: alpha("#fff", 0.56),
|
||||||
|
border: `1px solid ${
|
||||||
|
isActive || isLoading
|
||||||
|
? alpha("#00acc1", 0.25)
|
||||||
|
: alpha("#fff", 0.72)
|
||||||
|
}`,
|
||||||
|
boxShadow:
|
||||||
|
isActive || isLoading
|
||||||
|
? `0 8px 20px ${alpha("#00acc1", 0.12)}`
|
||||||
|
: `0 4px 12px ${alpha("#000", 0.03)}`,
|
||||||
transition: "all 0.2s ease",
|
transition: "all 0.2s ease",
|
||||||
pointerEvents: isHydrating ? "none" : "auto",
|
pointerEvents: isHydrating || isLoading ? "none" : "auto",
|
||||||
"&:hover": {
|
"&:hover": {
|
||||||
bgcolor: isActive ? alpha("#00acc1", 0.14) : alpha("#fff", 0.86),
|
bgcolor:
|
||||||
|
isActive || isLoading
|
||||||
|
? alpha("#00acc1", 0.14)
|
||||||
|
: alpha("#fff", 0.86),
|
||||||
borderColor: alpha("#00acc1", 0.2),
|
borderColor: alpha("#00acc1", 0.2),
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
@@ -382,6 +428,11 @@ export const AgentHistoryPanel = ({
|
|||||||
<CloseRounded sx={{ fontSize: 16 }} />
|
<CloseRounded sx={{ fontSize: 16 }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
) : isLoading ? (
|
||||||
|
<Box sx={{ minHeight: 46, display: "flex", flexDirection: "column", justifyContent: "center" }}>
|
||||||
|
<Skeleton variant="text" width="74%" height={18} />
|
||||||
|
<Skeleton variant="text" width="34%" height={14} sx={{ mt: 0.5 }} />
|
||||||
|
</Box>
|
||||||
) : pendingDeleteSessionId === session.id ? (
|
) : pendingDeleteSessionId === session.id ? (
|
||||||
<Stack direction="row" spacing={0.75} alignItems="center" sx={{ minHeight: 46 }}>
|
<Stack direction="row" spacing={0.75} alignItems="center" sx={{ minHeight: 46 }}>
|
||||||
<Box
|
<Box
|
||||||
@@ -437,7 +488,7 @@ export const AgentHistoryPanel = ({
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{!(editingSessionId === session.id || pendingDeleteSessionId === session.id) && (
|
{!(editingSessionId === session.id || pendingDeleteSessionId === session.id || isLoading) && (
|
||||||
<Stack direction="row" spacing={0.25}>
|
<Stack direction="row" spacing={0.25}>
|
||||||
<Tooltip title="修改会话标题">
|
<Tooltip title="修改会话标题">
|
||||||
<span>
|
<span>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import "@testing-library/jest-dom";
|
import "@testing-library/jest-dom";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { render } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
|
|
||||||
import { AgentWorkspace } from "./AgentWorkspace";
|
import { AgentWorkspace } from "./AgentWorkspace";
|
||||||
import type { Message } from "./GlobalChatbox.types";
|
import type { Message } from "./GlobalChatbox.types";
|
||||||
@@ -51,6 +51,20 @@ describe("AgentWorkspace", () => {
|
|||||||
renderCounts.clear();
|
renderCounts.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows a loading skeleton instead of the empty state while switching history sessions", () => {
|
||||||
|
render(
|
||||||
|
<AgentWorkspace
|
||||||
|
{...defaultProps}
|
||||||
|
isStreaming={false}
|
||||||
|
isLoadingSession
|
||||||
|
messages={[]}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByLabelText("正在加载历史记录")).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("我已就绪,请描述任务")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps stable history turns from re-rendering while the last assistant message streams", () => {
|
it("keeps stable history turns from re-rendering while the last assistant message streams", () => {
|
||||||
const userMessage: Message = {
|
const userMessage: Message = {
|
||||||
id: "user-1",
|
id: "user-1",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
import { Box, Paper, Stack, Typography, alpha, useTheme, Grid } from "@mui/material";
|
import { Box, Paper, Skeleton, Stack, Typography, alpha, useTheme, Grid } from "@mui/material";
|
||||||
import WaterDropRounded from "@mui/icons-material/WaterDropRounded";
|
import WaterDropRounded from "@mui/icons-material/WaterDropRounded";
|
||||||
import SensorsRounded from "@mui/icons-material/SensorsRounded";
|
import SensorsRounded from "@mui/icons-material/SensorsRounded";
|
||||||
import TroubleshootRounded from "@mui/icons-material/TroubleshootRounded";
|
import TroubleshootRounded from "@mui/icons-material/TroubleshootRounded";
|
||||||
@@ -20,6 +20,7 @@ import type {
|
|||||||
type AgentWorkspaceProps = {
|
type AgentWorkspaceProps = {
|
||||||
messages: Message[];
|
messages: Message[];
|
||||||
isStreaming: boolean;
|
isStreaming: boolean;
|
||||||
|
isLoadingSession?: boolean;
|
||||||
bottomRef: React.RefObject<HTMLDivElement | null>;
|
bottomRef: React.RefObject<HTMLDivElement | null>;
|
||||||
speakingMessageId: string | null;
|
speakingMessageId: string | null;
|
||||||
speechState: SpeechState;
|
speechState: SpeechState;
|
||||||
@@ -226,9 +227,72 @@ const EmptyState = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const SessionLoadingSkeleton = () => (
|
||||||
|
<Stack
|
||||||
|
spacing={2.25}
|
||||||
|
aria-label="正在加载历史记录"
|
||||||
|
sx={{ width: "100%", maxWidth: 760, alignSelf: "stretch" }}
|
||||||
|
>
|
||||||
|
{Array.from({ length: 2 }, (_, turnIndex) => (
|
||||||
|
<Stack key={turnIndex} spacing={1.25}>
|
||||||
|
<Stack direction="row" justifyContent="flex-end">
|
||||||
|
<Paper
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
width: turnIndex === 0 ? "72%" : "64%",
|
||||||
|
maxWidth: "86%",
|
||||||
|
p: 1.75,
|
||||||
|
borderRadius: 5,
|
||||||
|
borderBottomRightRadius: 2,
|
||||||
|
bgcolor: alpha("#00acc1", 0.16),
|
||||||
|
border: `1px solid ${alpha("#00acc1", 0.12)}`,
|
||||||
|
boxShadow: `0 8px 24px -12px ${alpha("#00acc1", 0.35)}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack spacing={0.85}>
|
||||||
|
<Skeleton variant="text" width="76%" height={18} />
|
||||||
|
<Skeleton variant="text" width="48%" height={15} />
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack direction="row" spacing={1.5} alignItems="flex-start">
|
||||||
|
<Skeleton
|
||||||
|
variant="circular"
|
||||||
|
width={34}
|
||||||
|
height={34}
|
||||||
|
sx={{ bgcolor: alpha("#00acc1", 0.12), flexShrink: 0, mt: 0.25 }}
|
||||||
|
/>
|
||||||
|
<Paper
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 0,
|
||||||
|
p: 2,
|
||||||
|
borderRadius: 5,
|
||||||
|
bgcolor: alpha("#ffffff", 0.52),
|
||||||
|
border: `1px solid ${alpha("#fff", 0.72)}`,
|
||||||
|
boxShadow: `0 10px 30px -10px ${alpha("#000", 0.06)}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack spacing={1}>
|
||||||
|
<Skeleton variant="text" width="38%" height={16} />
|
||||||
|
<Skeleton variant="text" width="94%" height={16} />
|
||||||
|
<Skeleton variant="text" width={turnIndex === 0 ? "88%" : "82%"} height={16} />
|
||||||
|
<Skeleton variant="text" width={turnIndex === 0 ? "78%" : "70%"} height={16} />
|
||||||
|
<Skeleton variant="rounded" width="100%" height={turnIndex === 0 ? 104 : 76} sx={{ borderRadius: 2 }} />
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
|
||||||
export const AgentWorkspace = ({
|
export const AgentWorkspace = ({
|
||||||
messages,
|
messages,
|
||||||
isStreaming,
|
isStreaming,
|
||||||
|
isLoadingSession = false,
|
||||||
bottomRef,
|
bottomRef,
|
||||||
speakingMessageId,
|
speakingMessageId,
|
||||||
speechState,
|
speechState,
|
||||||
@@ -270,6 +334,10 @@ export const AgentWorkspace = ({
|
|||||||
zIndex: 5,
|
zIndex: 5,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{isLoadingSession ? (
|
||||||
|
<SessionLoadingSkeleton />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<AnimatePresence initial={false}>
|
<AnimatePresence initial={false}>
|
||||||
{messages.length === 0 ? <EmptyState /> : null}
|
{messages.length === 0 ? <EmptyState /> : null}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
@@ -311,8 +379,10 @@ export const AgentWorkspace = ({
|
|||||||
) : null}
|
) : null}
|
||||||
</Box>
|
</Box>
|
||||||
) : null}
|
) : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{showTypingIndicator ? (
|
{!isLoadingSession && showTypingIndicator ? (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 10, scale: 0.94 }}
|
initial={{ opacity: 0, y: 10, scale: 0.94 }}
|
||||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
|
|||||||
chatSessions,
|
chatSessions,
|
||||||
activeSessionId,
|
activeSessionId,
|
||||||
isHydrating,
|
isHydrating,
|
||||||
|
loadingSessionId,
|
||||||
isStreaming,
|
isStreaming,
|
||||||
sessionTitle,
|
sessionTitle,
|
||||||
sendPrompt,
|
sendPrompt,
|
||||||
@@ -159,9 +160,9 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSelectSession = useCallback(
|
const handleSelectSession = useCallback(
|
||||||
(sessionId: string) => {
|
(sessionId: string, title: string) => {
|
||||||
composerRef.current?.clear();
|
composerRef.current?.clear();
|
||||||
void switchSession(sessionId);
|
void switchSession(sessionId, title);
|
||||||
},
|
},
|
||||||
[switchSession],
|
[switchSession],
|
||||||
);
|
);
|
||||||
@@ -326,12 +327,14 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
|
|||||||
sessions={chatSessions}
|
sessions={chatSessions}
|
||||||
activeSessionId={activeSessionId}
|
activeSessionId={activeSessionId}
|
||||||
isHydrating={isHydrating}
|
isHydrating={isHydrating}
|
||||||
|
isLoadingSessions={isHydrating && chatSessions.length === 0}
|
||||||
|
loadingSessionId={loadingSessionId}
|
||||||
onNewSession={() => {
|
onNewSession={() => {
|
||||||
handleNewConversation();
|
handleNewConversation();
|
||||||
setIsHistoryOpen(false);
|
setIsHistoryOpen(false);
|
||||||
}}
|
}}
|
||||||
onSelectSession={(id) => {
|
onSelectSession={(id, title) => {
|
||||||
handleSelectSession(id);
|
handleSelectSession(id, title);
|
||||||
setIsHistoryOpen(false);
|
setIsHistoryOpen(false);
|
||||||
}}
|
}}
|
||||||
onRenameSession={handleRenameSession}
|
onRenameSession={handleRenameSession}
|
||||||
@@ -343,6 +346,7 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
|
|||||||
<AgentWorkspace
|
<AgentWorkspace
|
||||||
messages={messages}
|
messages={messages}
|
||||||
isStreaming={isStreaming}
|
isStreaming={isStreaming}
|
||||||
|
isLoadingSession={Boolean(loadingSessionId)}
|
||||||
bottomRef={bottomRef}
|
bottomRef={bottomRef}
|
||||||
speakingMessageId={speakingMessageId}
|
speakingMessageId={speakingMessageId}
|
||||||
speechState={speechState}
|
speechState={speechState}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export const useAgentChatSession = ({
|
|||||||
const [chatSessions, setChatSessions] = useState<ChatSessionSummary[]>([]);
|
const [chatSessions, setChatSessions] = useState<ChatSessionSummary[]>([]);
|
||||||
const [isStreaming, setIsStreaming] = useState(false);
|
const [isStreaming, setIsStreaming] = useState(false);
|
||||||
const [isHydrating, setIsHydrating] = useState(true);
|
const [isHydrating, setIsHydrating] = useState(true);
|
||||||
|
const [loadingSessionId, setLoadingSessionId] = useState<string | undefined>(undefined);
|
||||||
const abortRef = useRef<AbortController | null>(null);
|
const abortRef = useRef<AbortController | null>(null);
|
||||||
const sessionIdRef = useRef<string | undefined>(undefined);
|
const sessionIdRef = useRef<string | undefined>(undefined);
|
||||||
const messagesRef = useRef<Message[]>([]);
|
const messagesRef = useRef<Message[]>([]);
|
||||||
@@ -756,12 +757,17 @@ export const useAgentChatSession = ({
|
|||||||
}, [isHydrating, isStreaming]);
|
}, [isHydrating, isStreaming]);
|
||||||
|
|
||||||
const switchSession = useCallback(
|
const switchSession = useCallback(
|
||||||
async (nextSessionId: string) => {
|
async (nextSessionId: string, optimisticTitle?: string) => {
|
||||||
if (isHydrating || isStreaming || sessionIdRef.current === nextSessionId) {
|
if (isHydrating || isStreaming || sessionIdRef.current === nextSessionId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsHydrating(true);
|
setIsHydrating(true);
|
||||||
|
setLoadingSessionId(nextSessionId);
|
||||||
|
const nextTitle = optimisticTitle?.trim();
|
||||||
|
if (nextTitle) {
|
||||||
|
setSessionTitle(nextTitle);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const [nextState, sessions] = await Promise.all([
|
const [nextState, sessions] = await Promise.all([
|
||||||
loadChatSessionById(nextSessionId),
|
loadChatSessionById(nextSessionId),
|
||||||
@@ -785,6 +791,7 @@ export const useAgentChatSession = ({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[GlobalChatbox] Failed to switch chat session:", error);
|
console.error("[GlobalChatbox] Failed to switch chat session:", error);
|
||||||
} finally {
|
} finally {
|
||||||
|
setLoadingSessionId(undefined);
|
||||||
setIsHydrating(false);
|
setIsHydrating(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -932,6 +939,7 @@ export const useAgentChatSession = ({
|
|||||||
chatSessions,
|
chatSessions,
|
||||||
activeSessionId: sessionIdRef.current,
|
activeSessionId: sessionIdRef.current,
|
||||||
isHydrating,
|
isHydrating,
|
||||||
|
loadingSessionId,
|
||||||
isStreaming,
|
isStreaming,
|
||||||
sessionTitle,
|
sessionTitle,
|
||||||
sessionId,
|
sessionId,
|
||||||
|
|||||||
Reference in New Issue
Block a user