重构会话标题编辑和删除确认逻辑;重构历史会话时间记录
This commit is contained in:
@@ -33,7 +33,7 @@ describe("AgentHeader", () => {
|
||||
fireEvent.change(screen.getByPlaceholderText("请输入对话标题"), {
|
||||
target: { value: "更新后的标题" },
|
||||
});
|
||||
fireEvent.click(screen.getByLabelText("确认修改对话标题"));
|
||||
fireEvent.click(screen.getByLabelText("确认"));
|
||||
|
||||
expect(onRenameSessionTitle).toHaveBeenCalledWith("更新后的标题");
|
||||
});
|
||||
|
||||
@@ -129,10 +129,9 @@ export const AgentHeader = ({
|
||||
/>
|
||||
</Box>
|
||||
</motion.div>
|
||||
<Box sx={{ minWidth: 0, minHeight: 52, display: "flex", flexDirection: "column", justifyContent: "center" }}>
|
||||
<Box sx={{ minWidth: 0 }}>
|
||||
{isEditingTitle ? (
|
||||
<Box>
|
||||
<Stack direction="row" spacing={0.75} alignItems="center" sx={{ width: { xs: "calc(100vw - 256px)", sm: 280 }, transform: "translateY(2px)" }}>
|
||||
<Stack direction="row" spacing={0.75} alignItems="center" sx={{ width: { xs: "calc(100vw - 256px)", sm: 280 } }}>
|
||||
<TextField
|
||||
value={draftTitle}
|
||||
onChange={(event) => setDraftTitle(event.target.value)}
|
||||
@@ -152,27 +151,35 @@ export const AgentHeader = ({
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
"& .MuiOutlinedInput-root": {
|
||||
height: 34,
|
||||
bgcolor: alpha("#fff", 0.7),
|
||||
padding: "6px 8px",
|
||||
bgcolor: "transparent",
|
||||
borderRadius: 1.5,
|
||||
transition: "all 0.2s ease-in-out",
|
||||
"&.Mui-focused": {
|
||||
bgcolor: alpha("#fff", 0.6),
|
||||
boxShadow: `0 2px 10px ${alpha("#000", 0.05)}`,
|
||||
},
|
||||
"& fieldset": {
|
||||
borderColor: alpha("#000", 0.08),
|
||||
borderColor: "transparent",
|
||||
},
|
||||
"&:hover fieldset": {
|
||||
borderColor: alpha(theme.palette.primary.main, 0.4),
|
||||
borderColor: alpha(theme.palette.primary.main, 0.2),
|
||||
},
|
||||
"&.Mui-focused fieldset": {
|
||||
borderColor: theme.palette.primary.main,
|
||||
borderWidth: "1.5px",
|
||||
boxShadow: `0 0 0 3px ${alpha(theme.palette.primary.main, 0.1)}`,
|
||||
borderColor: alpha(theme.palette.primary.main, 0.5),
|
||||
borderWidth: "1px",
|
||||
},
|
||||
},
|
||||
"& .MuiInputBase-input": {
|
||||
padding: "4px 12px",
|
||||
fontSize: "1.05rem",
|
||||
fontWeight: 700,
|
||||
color: theme.palette.text.primary,
|
||||
padding: 0,
|
||||
height: "auto",
|
||||
fontSize: "1.25rem",
|
||||
fontWeight: 800,
|
||||
letterSpacing: -0.3,
|
||||
lineHeight: "1.2",
|
||||
background: `linear-gradient(90deg, #01579b, #00838f)`,
|
||||
WebkitBackgroundClip: "text",
|
||||
WebkitTextFillColor: "transparent",
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -206,22 +213,22 @@ export const AgentHeader = ({
|
||||
<CloseRounded sx={{ fontSize: 18 }} />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
<Stack direction="row" spacing={0.75} alignItems="center" sx={{ minWidth: 0 }}>
|
||||
<Typography
|
||||
<Stack direction="row" spacing={0.75} alignItems="center" sx={{ minWidth: 0 }}>
|
||||
<Typography
|
||||
variant="h6"
|
||||
fontWeight={800}
|
||||
sx={{
|
||||
background: `linear-gradient(90deg, #01579b, #00838f)`,
|
||||
backgroundClip: "text",
|
||||
color: "transparent",
|
||||
WebkitBackgroundClip: "text",
|
||||
WebkitTextFillColor: "transparent",
|
||||
letterSpacing: -0.3,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
maxWidth: { xs: "calc(100vw - 256px)", sm: 284 },
|
||||
px: "8px",
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
{displayTitle}
|
||||
@@ -235,8 +242,8 @@ export const AgentHeader = ({
|
||||
onClick={handleStartEditing}
|
||||
disabled={isHydrating || isStreaming}
|
||||
sx={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
width: 30,
|
||||
height: 30,
|
||||
color: "text.secondary",
|
||||
bgcolor: alpha("#fff", 0.45),
|
||||
"&:hover": {
|
||||
@@ -245,20 +252,12 @@ export const AgentHeader = ({
|
||||
},
|
||||
}}
|
||||
>
|
||||
<EditRounded sx={{ fontSize: 16 }} />
|
||||
<EditRounded sx={{ fontSize: 18 }} />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</Stack>
|
||||
<Typography variant="caption" color="text.secondary" fontWeight={500}>
|
||||
{isStreaming
|
||||
? "正在思考分析任务..."
|
||||
: displayTitle === "TJWater Agent"
|
||||
? "基于大模型的水力分析引擎"
|
||||
: "当前会话标题"}
|
||||
</Typography>
|
||||
</>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
@@ -85,7 +85,6 @@ export const AgentHistoryPanel = ({
|
||||
const [keyword, setKeyword] = React.useState("");
|
||||
const [editingSessionId, setEditingSessionId] = React.useState<string | null>(null);
|
||||
const [draftTitle, setDraftTitle] = React.useState("");
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = React.useState(false);
|
||||
const [pendingDeleteSessionId, setPendingDeleteSessionId] = React.useState<string | null>(null);
|
||||
|
||||
const filteredSessions = React.useMemo(() => {
|
||||
@@ -300,7 +299,7 @@ export const AgentHistoryPanel = ({
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" spacing={1} alignItems="flex-start">
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||
{editingSessionId === session.id ? (
|
||||
<Stack direction="row" spacing={0.5} alignItems="center" sx={{ minHeight: 46 }}>
|
||||
@@ -386,6 +385,38 @@ export const AgentHistoryPanel = ({
|
||||
<CloseRounded sx={{ fontSize: 16 }} />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
) : pendingDeleteSessionId === session.id ? (
|
||||
<Stack direction="row" spacing={0.75} alignItems="center" sx={{ minHeight: 46 }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: "50%",
|
||||
bgcolor: alpha("#ef5350", 0.15),
|
||||
color: "#ef5350",
|
||||
flexShrink: 0
|
||||
}}
|
||||
>
|
||||
<WarningRounded sx={{ fontSize: 13 }} />
|
||||
</Box>
|
||||
<Typography
|
||||
variant="body2"
|
||||
fontWeight={800}
|
||||
color="error.main"
|
||||
sx={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
确认删除此会话?
|
||||
</Typography>
|
||||
</Stack>
|
||||
) : (
|
||||
<Box sx={{ minHeight: 46, display: "flex", flexDirection: "column", justifyContent: "center" }}>
|
||||
<Typography
|
||||
@@ -409,8 +440,9 @@ export const AgentHistoryPanel = ({
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Stack direction="row" spacing={0.25} sx={{ display: editingSessionId === session.id ? 'none' : 'flex' }}>
|
||||
<Tooltip title="修改会话标题">
|
||||
{!(editingSessionId === session.id || pendingDeleteSessionId === session.id) && (
|
||||
<Stack direction="row" spacing={0.25}>
|
||||
<Tooltip title="修改会话标题">
|
||||
<span>
|
||||
<IconButton
|
||||
size="small"
|
||||
@@ -421,8 +453,8 @@ export const AgentHistoryPanel = ({
|
||||
}}
|
||||
disabled={isHydrating || editingSessionId === session.id}
|
||||
sx={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
width: 28,
|
||||
height: 28,
|
||||
color: "text.secondary",
|
||||
"&:hover": {
|
||||
color: "primary.main",
|
||||
@@ -442,11 +474,11 @@ export const AgentHistoryPanel = ({
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setPendingDeleteSessionId(session.id);
|
||||
setIsDeleteDialogOpen(true);
|
||||
}}
|
||||
disabled={isHydrating}
|
||||
sx={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
width: 28,
|
||||
height: 28,
|
||||
color: "text.secondary",
|
||||
"&:hover": {
|
||||
color: "error.main",
|
||||
@@ -459,6 +491,48 @@ export const AgentHistoryPanel = ({
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{pendingDeleteSessionId === session.id && (
|
||||
<Stack direction="row" spacing={0.5} alignItems="center">
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label="确认删除"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onDeleteSession(session.id);
|
||||
setPendingDeleteSessionId(null);
|
||||
}}
|
||||
sx={{
|
||||
width: 28,
|
||||
height: 28,
|
||||
color: "error.main",
|
||||
bgcolor: alpha("#ef5350", 0.1),
|
||||
"&:hover": { bgcolor: alpha("#ef5350", 0.2) },
|
||||
}}
|
||||
>
|
||||
<CheckRounded sx={{ fontSize: 16 }} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label="取消删除"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setPendingDeleteSessionId(null);
|
||||
}}
|
||||
sx={{
|
||||
width: 28,
|
||||
height: 28,
|
||||
color: "text.secondary",
|
||||
bgcolor: alpha("#000", 0.05),
|
||||
"&:hover": { bgcolor: alpha("#000", 0.1) },
|
||||
}}
|
||||
>
|
||||
<CloseRounded sx={{ fontSize: 16 }} />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
</Stack>
|
||||
</Paper>
|
||||
);
|
||||
@@ -470,97 +544,6 @@ export const AgentHistoryPanel = ({
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
<Dialog
|
||||
open={isDeleteDialogOpen}
|
||||
onClose={() => setIsDeleteDialogOpen(false)}
|
||||
sx={{ zIndex: (theme) => theme.zIndex.modal + 200 }}
|
||||
TransitionProps={{
|
||||
onExited: () => setPendingDeleteSessionId(null)
|
||||
}}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
borderRadius: 4,
|
||||
bgcolor: alpha("#fff", 0.85),
|
||||
backdropFilter: "blur(24px)",
|
||||
boxShadow: `0 16px 40px ${alpha("#000", 0.12)}`,
|
||||
border: `1px solid ${alpha("#fff", 0.6)}`,
|
||||
minWidth: 320,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DialogTitle sx={{ display: "flex", alignItems: "center", gap: 1.5, pb: 1, pt: 3, px: 3 }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: "50%",
|
||||
bgcolor: alpha("#ef5350", 0.12),
|
||||
color: "#ef5350",
|
||||
}}
|
||||
>
|
||||
<WarningRounded sx={{ fontSize: 22 }} />
|
||||
</Box>
|
||||
<Typography component="span" variant="h6" fontWeight={800} color="text.primary">
|
||||
删除确认
|
||||
</Typography>
|
||||
</DialogTitle>
|
||||
<DialogContent sx={{ px: 3, pb: 2 }}>
|
||||
<DialogContentText color="text.secondary" sx={{ fontSize: "0.95rem" }}>
|
||||
确定要删除
|
||||
{pendingDeleteSession ? (
|
||||
<Typography component="span" fontWeight={700} color="text.primary">
|
||||
“{pendingDeleteSession.title}”
|
||||
</Typography>
|
||||
) : (
|
||||
"该会话"
|
||||
)}
|
||||
吗?
|
||||
<br />
|
||||
此操作不可撤销,删除后聊天记录将永久丢失。
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 3, pt: 1 }}>
|
||||
<Button
|
||||
onClick={() => setIsDeleteDialogOpen(false)}
|
||||
sx={{
|
||||
color: "text.secondary",
|
||||
fontWeight: 600,
|
||||
borderRadius: 2.5,
|
||||
px: 2.5,
|
||||
"&:hover": { bgcolor: alpha("#000", 0.04) },
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
if (pendingDeleteSessionId) {
|
||||
onDeleteSession(pendingDeleteSessionId);
|
||||
}
|
||||
setIsDeleteDialogOpen(false);
|
||||
}}
|
||||
sx={{
|
||||
bgcolor: "#ef5350",
|
||||
color: "#fff",
|
||||
fontWeight: 700,
|
||||
borderRadius: 2.5,
|
||||
px: 3,
|
||||
boxShadow: `0 4px 12px ${alpha("#ef5350", 0.3)}`,
|
||||
"&:hover": {
|
||||
bgcolor: "#e53935",
|
||||
boxShadow: `0 6px 16px ${alpha("#ef5350", 0.4)}`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
确认删除
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import type { ChatSessionRecord } from "./GlobalChatbox.types";
|
||||
import {
|
||||
createEmptyChatSession,
|
||||
loadChatSessionById,
|
||||
saveActiveChatState,
|
||||
updateChatSessionTitle,
|
||||
} from "./chatStorage";
|
||||
|
||||
type StoreName = "sessions" | "meta";
|
||||
|
||||
const stores: Record<StoreName, Map<string, any>> = {
|
||||
sessions: new Map(),
|
||||
meta: new Map(),
|
||||
};
|
||||
|
||||
const mockDb = {
|
||||
get: jest.fn(async (storeName: StoreName, key: string) => stores[storeName].get(key)),
|
||||
getAll: jest.fn(async (storeName: StoreName) => Array.from(stores[storeName].values())),
|
||||
put: jest.fn(async (storeName: StoreName, value: { id?: string; key?: string }) => {
|
||||
const key = storeName === "sessions" ? value.id : value.key;
|
||||
if (!key) {
|
||||
throw new Error(`Missing key for store ${storeName}`);
|
||||
}
|
||||
stores[storeName].set(key, value);
|
||||
return key;
|
||||
}),
|
||||
delete: jest.fn(async (storeName: StoreName, key: string) => {
|
||||
stores[storeName].delete(key);
|
||||
}),
|
||||
};
|
||||
|
||||
jest.mock("idb", () => ({
|
||||
openDB: jest.fn(async () => mockDb),
|
||||
}));
|
||||
|
||||
describe("chatStorage timestamp semantics", () => {
|
||||
let now = new Date("2026-05-19T09:00:00+08:00").getTime();
|
||||
let dateNowSpy: jest.SpyInstance<number, []>;
|
||||
|
||||
beforeEach(() => {
|
||||
stores.sessions.clear();
|
||||
stores.meta.clear();
|
||||
mockDb.get.mockClear();
|
||||
mockDb.getAll.mockClear();
|
||||
mockDb.put.mockClear();
|
||||
mockDb.delete.mockClear();
|
||||
window.localStorage.clear();
|
||||
now = new Date("2026-05-19T09:00:00+08:00").getTime();
|
||||
dateNowSpy = jest.spyOn(Date, "now").mockImplementation(() => now);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
dateNowSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("keeps anchor and content timestamps when reopening an old session", async () => {
|
||||
const record: ChatSessionRecord = {
|
||||
id: "old-session",
|
||||
title: "很久之前的会话",
|
||||
isTitleManuallyEdited: false,
|
||||
createdAt: new Date("2026-04-01T10:00:00+08:00").getTime(),
|
||||
updatedAt: new Date("2026-04-01T10:30:00+08:00").getTime(),
|
||||
sessionId: "remote-1",
|
||||
messages: [
|
||||
{
|
||||
id: "message-1",
|
||||
role: "user",
|
||||
content: "老问题",
|
||||
branchRootId: "message-1",
|
||||
},
|
||||
],
|
||||
branchGroups: [],
|
||||
};
|
||||
stores.sessions.set(record.id, record);
|
||||
|
||||
const loadedState = await loadChatSessionById(record.id);
|
||||
now = new Date("2026-05-19T09:30:00+08:00").getTime();
|
||||
await saveActiveChatState(loadedState);
|
||||
|
||||
expect(stores.sessions.get(record.id)).toMatchObject({
|
||||
createdAt: record.createdAt,
|
||||
updatedAt: record.updatedAt,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not change timestamps when renaming a session", async () => {
|
||||
const record: ChatSessionRecord = {
|
||||
id: "rename-session",
|
||||
title: "旧标题",
|
||||
isTitleManuallyEdited: false,
|
||||
createdAt: new Date("2026-04-10T08:00:00+08:00").getTime(),
|
||||
updatedAt: new Date("2026-04-10T08:05:00+08:00").getTime(),
|
||||
sessionId: "remote-2",
|
||||
messages: [
|
||||
{
|
||||
id: "message-2",
|
||||
role: "user",
|
||||
content: "保留时间",
|
||||
branchRootId: "message-2",
|
||||
},
|
||||
],
|
||||
branchGroups: [],
|
||||
};
|
||||
stores.sessions.set(record.id, record);
|
||||
|
||||
now = new Date("2026-05-19T11:00:00+08:00").getTime();
|
||||
await updateChatSessionTitle(record.id, "新标题", {
|
||||
isTitleManuallyEdited: true,
|
||||
});
|
||||
|
||||
expect(stores.sessions.get(record.id)).toMatchObject({
|
||||
title: "新标题",
|
||||
isTitleManuallyEdited: true,
|
||||
createdAt: record.createdAt,
|
||||
updatedAt: record.updatedAt,
|
||||
});
|
||||
});
|
||||
|
||||
it("anchors createdAt to the first real message time for a new empty session", async () => {
|
||||
const emptyState = await createEmptyChatSession();
|
||||
const storageSessionId = emptyState.storageSessionId;
|
||||
|
||||
now = new Date("2026-05-19T09:05:00+08:00").getTime();
|
||||
await saveActiveChatState({
|
||||
...emptyState,
|
||||
messages: [
|
||||
{
|
||||
id: "message-3",
|
||||
role: "user",
|
||||
content: "第一条消息",
|
||||
branchRootId: "message-3",
|
||||
},
|
||||
],
|
||||
sessionId: "remote-3",
|
||||
});
|
||||
|
||||
expect(stores.sessions.get(storageSessionId!)).toMatchObject({
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -51,6 +51,17 @@ const sanitizeMessages = (messages: Message[] | undefined) =>
|
||||
const sanitizeBranchGroups = (branchGroups: BranchGroup[] | undefined) =>
|
||||
Array.isArray(branchGroups) ? cloneBranchGroups(branchGroups) : [];
|
||||
|
||||
const serializeConversationState = (state: {
|
||||
messages: Message[];
|
||||
branchGroups: BranchGroup[];
|
||||
sessionId?: string;
|
||||
}) =>
|
||||
JSON.stringify({
|
||||
messages: sanitizeMessages(state.messages),
|
||||
branchGroups: sanitizeBranchGroups(state.branchGroups),
|
||||
sessionId: state.sessionId ?? null,
|
||||
});
|
||||
|
||||
const hasChatContent = (state: {
|
||||
messages: Message[];
|
||||
branchGroups: BranchGroup[];
|
||||
@@ -260,6 +271,9 @@ export const saveActiveChatState = async (
|
||||
const storageSessionId = state.storageSessionId ?? createId();
|
||||
const preferredTitle = state.title?.trim();
|
||||
const finalTitle = preferredTitle || existingSession?.title || "新对话";
|
||||
const hasContentChanged =
|
||||
!existingSession ||
|
||||
serializeConversationState(existingSession) !== serializeConversationState(state);
|
||||
const shouldAnchorCreatedAtToFirstMessage =
|
||||
Boolean(existingSession) && !hasChatContent(existingSession) && hasContent;
|
||||
const nextRecord: ChatSessionRecord = {
|
||||
@@ -269,7 +283,7 @@ export const saveActiveChatState = async (
|
||||
createdAt: shouldAnchorCreatedAtToFirstMessage
|
||||
? now
|
||||
: existingSession?.createdAt ?? now,
|
||||
updatedAt: now,
|
||||
updatedAt: hasContentChanged ? now : existingSession?.updatedAt ?? now,
|
||||
sessionId: state.sessionId,
|
||||
messages: sanitizeMessages(state.messages),
|
||||
branchGroups: sanitizeBranchGroups(state.branchGroups),
|
||||
@@ -315,7 +329,6 @@ export const updateChatSessionTitle = async (
|
||||
title: normalizedTitle,
|
||||
isTitleManuallyEdited:
|
||||
options?.isTitleManuallyEdited ?? session.isTitleManuallyEdited ?? false,
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -48,6 +48,16 @@ type PromptRunOptions = {
|
||||
assistantMessage?: Message;
|
||||
};
|
||||
|
||||
const createPersistedStateKey = (state: LoadedChatState) =>
|
||||
JSON.stringify({
|
||||
storageSessionId: state.storageSessionId ?? null,
|
||||
title: state.title ?? null,
|
||||
isTitleManuallyEdited: state.isTitleManuallyEdited ?? false,
|
||||
sessionId: state.sessionId ?? null,
|
||||
messages: state.messages,
|
||||
branchGroups: state.branchGroups,
|
||||
});
|
||||
|
||||
const upsertProgress = (
|
||||
progress: ChatProgress[] | undefined,
|
||||
event: StreamEvent & { type: "progress" },
|
||||
@@ -158,6 +168,16 @@ export const useAgentChatSession = ({
|
||||
const isSessionTitleManuallyEditedRef = useRef(false);
|
||||
const cancelPromiseRef = useRef<Promise<void> | null>(null);
|
||||
const titleUpdateNonceRef = useRef(0);
|
||||
const lastPersistedStateKeyRef = useRef(
|
||||
createPersistedStateKey({
|
||||
storageSessionId: undefined,
|
||||
title: undefined,
|
||||
isTitleManuallyEdited: false,
|
||||
messages: [],
|
||||
sessionId: undefined,
|
||||
branchGroups: [],
|
||||
}),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
sessionIdRef.current = sessionId;
|
||||
@@ -180,6 +200,7 @@ export const useAgentChatSession = ({
|
||||
|
||||
storageSessionIdRef.current = loadedState.storageSessionId;
|
||||
sessionIdRef.current = loadedState.sessionId;
|
||||
lastPersistedStateKeyRef.current = createPersistedStateKey(loadedState);
|
||||
hydrationCompletedRef.current = true;
|
||||
hydrationNonceRef.current += 1;
|
||||
titleUpdateNonceRef.current += 1;
|
||||
@@ -219,11 +240,19 @@ export const useAgentChatSession = ({
|
||||
sessionId,
|
||||
branchGroups,
|
||||
};
|
||||
const currentStateKey = createPersistedStateKey(state);
|
||||
if (currentStateKey === lastPersistedStateKeyRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
void saveActiveChatState(state)
|
||||
.then((storageSessionId) => {
|
||||
if (hydrationNonceRef.current !== currentHydrationNonce) return;
|
||||
storageSessionIdRef.current = storageSessionId;
|
||||
lastPersistedStateKeyRef.current = createPersistedStateKey({
|
||||
...state,
|
||||
storageSessionId,
|
||||
});
|
||||
return listChatSessions();
|
||||
})
|
||||
.then((sessions) => {
|
||||
@@ -503,6 +532,14 @@ export const useAgentChatSession = ({
|
||||
setSessionId(undefined);
|
||||
sessionIdRef.current = undefined;
|
||||
storageSessionIdRef.current = undefined;
|
||||
lastPersistedStateKeyRef.current = createPersistedStateKey({
|
||||
storageSessionId: undefined,
|
||||
title: undefined,
|
||||
isTitleManuallyEdited: false,
|
||||
messages: [],
|
||||
sessionId: undefined,
|
||||
branchGroups: [],
|
||||
});
|
||||
titleUpdateNonceRef.current += 1;
|
||||
setIsStreaming(false);
|
||||
}, []);
|
||||
@@ -521,6 +558,7 @@ export const useAgentChatSession = ({
|
||||
titleUpdateNonceRef.current += 1;
|
||||
storageSessionIdRef.current = newState.storageSessionId;
|
||||
sessionIdRef.current = newState.sessionId;
|
||||
lastPersistedStateKeyRef.current = createPersistedStateKey(newState);
|
||||
setMessages(newState.messages);
|
||||
setSessionTitle(newState.title);
|
||||
setIsSessionTitleManuallyEdited(newState.isTitleManuallyEdited ?? false);
|
||||
@@ -547,6 +585,7 @@ export const useAgentChatSession = ({
|
||||
titleUpdateNonceRef.current += 1;
|
||||
storageSessionIdRef.current = nextState.storageSessionId;
|
||||
sessionIdRef.current = nextState.sessionId;
|
||||
lastPersistedStateKeyRef.current = createPersistedStateKey(nextState);
|
||||
setBranchTransition(null);
|
||||
setMessages(nextState.messages);
|
||||
setSessionTitle(nextState.title);
|
||||
@@ -581,6 +620,14 @@ export const useAgentChatSession = ({
|
||||
titleUpdateNonceRef.current += 1;
|
||||
storageSessionIdRef.current = undefined;
|
||||
sessionIdRef.current = undefined;
|
||||
lastPersistedStateKeyRef.current = createPersistedStateKey({
|
||||
storageSessionId: undefined,
|
||||
title: undefined,
|
||||
isTitleManuallyEdited: false,
|
||||
messages: [],
|
||||
sessionId: undefined,
|
||||
branchGroups: [],
|
||||
});
|
||||
setBranchTransition(null);
|
||||
setMessages([]);
|
||||
setSessionTitle(undefined);
|
||||
@@ -599,6 +646,7 @@ export const useAgentChatSession = ({
|
||||
titleUpdateNonceRef.current += 1;
|
||||
storageSessionIdRef.current = nextState.storageSessionId;
|
||||
sessionIdRef.current = nextState.sessionId;
|
||||
lastPersistedStateKeyRef.current = createPersistedStateKey(nextState);
|
||||
setBranchTransition(null);
|
||||
setMessages(nextState.messages);
|
||||
setSessionTitle(nextState.title);
|
||||
@@ -637,12 +685,20 @@ export const useAgentChatSession = ({
|
||||
if (storageSessionIdRef.current === targetStorageSessionId) {
|
||||
setSessionTitle(normalizedTitle);
|
||||
setIsSessionTitleManuallyEdited(true);
|
||||
lastPersistedStateKeyRef.current = createPersistedStateKey({
|
||||
storageSessionId: targetStorageSessionId,
|
||||
title: normalizedTitle,
|
||||
isTitleManuallyEdited: true,
|
||||
messages,
|
||||
sessionId: sessionIdRef.current,
|
||||
branchGroups,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[GlobalChatbox] Failed to rename chat session:", error);
|
||||
}
|
||||
},
|
||||
[isHydrating],
|
||||
[branchGroups, isHydrating, messages],
|
||||
);
|
||||
|
||||
const regenerate = useCallback(async () => {
|
||||
|
||||
Reference in New Issue
Block a user