优化历史会话排序逻辑,按首条消息时间排序
This commit is contained in:
@@ -33,8 +33,37 @@ describe("AgentHistoryPanel", () => {
|
|||||||
fireEvent.change(screen.getByPlaceholderText("请输入会话标题"), {
|
fireEvent.change(screen.getByPlaceholderText("请输入会话标题"), {
|
||||||
target: { value: "新的会话标题" },
|
target: { value: "新的会话标题" },
|
||||||
});
|
});
|
||||||
fireEvent.click(screen.getByLabelText("确认修改历史会话标题"));
|
fireEvent.click(screen.getByLabelText("确认"));
|
||||||
|
|
||||||
expect(onRenameSession).toHaveBeenCalledWith("session-1", "新的会话标题");
|
expect(onRenameSession).toHaveBeenCalledWith("session-1", "新的会话标题");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("orders history by the first message time instead of the latest update time", () => {
|
||||||
|
renderWithTheme(
|
||||||
|
<AgentHistoryPanel
|
||||||
|
sessions={[
|
||||||
|
{
|
||||||
|
id: "session-newer-update",
|
||||||
|
title: "较新的更新",
|
||||||
|
createdAt: new Date("2026-05-18T09:00:00+08:00").getTime(),
|
||||||
|
updatedAt: new Date("2026-05-19T12:00:00+08:00").getTime(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "session-newer-first-message",
|
||||||
|
title: "较新的首条消息",
|
||||||
|
createdAt: new Date("2026-05-19T08:00:00+08:00").getTime(),
|
||||||
|
updatedAt: new Date("2026-05-19T08:30:00+08:00").getTime(),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onNewSession={jest.fn()}
|
||||||
|
onRenameSession={jest.fn()}
|
||||||
|
onSelectSession={jest.fn()}
|
||||||
|
onDeleteSession={jest.fn()}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const sessionTitles = screen.getAllByText(/较新的/).map((element) => element.textContent);
|
||||||
|
|
||||||
|
expect(sessionTitles).toEqual(["较新的首条消息", "较新的更新"]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -73,7 +73,6 @@ const getSessionGroupLabel = (timestamp: number) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const AgentHistoryPanel = ({
|
export const AgentHistoryPanel = ({
|
||||||
|
|
||||||
sessions,
|
sessions,
|
||||||
activeSessionId,
|
activeSessionId,
|
||||||
isHydrating = false,
|
isHydrating = false,
|
||||||
@@ -95,11 +94,25 @@ export const AgentHistoryPanel = ({
|
|||||||
return sessions.filter((session) => session.title.toLowerCase().includes(normalizedKeyword));
|
return sessions.filter((session) => session.title.toLowerCase().includes(normalizedKeyword));
|
||||||
}, [keyword, sessions]);
|
}, [keyword, sessions]);
|
||||||
|
|
||||||
|
const sortedFilteredSessions = React.useMemo(
|
||||||
|
() =>
|
||||||
|
[...filteredSessions].sort((left, right) => {
|
||||||
|
const createdAtDiff = right.createdAt - left.createdAt;
|
||||||
|
if (createdAtDiff !== 0) return createdAtDiff;
|
||||||
|
|
||||||
|
const updatedAtDiff = right.updatedAt - left.updatedAt;
|
||||||
|
if (updatedAtDiff !== 0) return updatedAtDiff;
|
||||||
|
|
||||||
|
return right.id.localeCompare(left.id);
|
||||||
|
}),
|
||||||
|
[filteredSessions],
|
||||||
|
);
|
||||||
|
|
||||||
const groupedSessions = React.useMemo(() => {
|
const groupedSessions = React.useMemo(() => {
|
||||||
const groups = new Map<string, ChatSessionSummary[]>();
|
const groups = new Map<string, ChatSessionSummary[]>();
|
||||||
|
|
||||||
filteredSessions.forEach((session) => {
|
sortedFilteredSessions.forEach((session) => {
|
||||||
const label = getSessionGroupLabel(session.updatedAt);
|
const label = getSessionGroupLabel(session.createdAt);
|
||||||
const existing = groups.get(label);
|
const existing = groups.get(label);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
existing.push(session);
|
existing.push(session);
|
||||||
@@ -109,7 +122,7 @@ export const AgentHistoryPanel = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return Array.from(groups.entries());
|
return Array.from(groups.entries());
|
||||||
}, [filteredSessions]);
|
}, [sortedFilteredSessions]);
|
||||||
|
|
||||||
const pendingDeleteSession = filteredSessions.find(
|
const pendingDeleteSession = filteredSessions.find(
|
||||||
(session) => session.id === pendingDeleteSessionId,
|
(session) => session.id === pendingDeleteSessionId,
|
||||||
@@ -390,7 +403,7 @@ export const AgentHistoryPanel = ({
|
|||||||
{session.title}
|
{session.title}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ mt: 0.5, display: "block" }}>
|
<Typography variant="caption" color="text.secondary" sx={{ mt: 0.5, display: "block" }}>
|
||||||
{formatRelativeDate(session.updatedAt)}
|
{formatRelativeDate(session.createdAt)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -51,6 +51,28 @@ const sanitizeMessages = (messages: Message[] | undefined) =>
|
|||||||
const sanitizeBranchGroups = (branchGroups: BranchGroup[] | undefined) =>
|
const sanitizeBranchGroups = (branchGroups: BranchGroup[] | undefined) =>
|
||||||
Array.isArray(branchGroups) ? cloneBranchGroups(branchGroups) : [];
|
Array.isArray(branchGroups) ? cloneBranchGroups(branchGroups) : [];
|
||||||
|
|
||||||
|
const hasChatContent = (state: {
|
||||||
|
messages: Message[];
|
||||||
|
branchGroups: BranchGroup[];
|
||||||
|
sessionId?: string;
|
||||||
|
}) =>
|
||||||
|
state.messages.length > 0 ||
|
||||||
|
state.branchGroups.length > 0 ||
|
||||||
|
Boolean(state.sessionId);
|
||||||
|
|
||||||
|
const compareSessionsByAnchorTime = (
|
||||||
|
left: Pick<ChatSessionRecord, "id" | "createdAt" | "updatedAt">,
|
||||||
|
right: Pick<ChatSessionRecord, "id" | "createdAt" | "updatedAt">,
|
||||||
|
) => {
|
||||||
|
const createdAtDiff = right.createdAt - left.createdAt;
|
||||||
|
if (createdAtDiff !== 0) return createdAtDiff;
|
||||||
|
|
||||||
|
const updatedAtDiff = right.updatedAt - left.updatedAt;
|
||||||
|
if (updatedAtDiff !== 0) return updatedAtDiff;
|
||||||
|
|
||||||
|
return right.id.localeCompare(left.id);
|
||||||
|
};
|
||||||
|
|
||||||
const toLoadedChatState = (session: ChatSessionRecord | undefined): LoadedChatState => {
|
const toLoadedChatState = (session: ChatSessionRecord | undefined): LoadedChatState => {
|
||||||
if (!session) return emptyLoadedChatState();
|
if (!session) return emptyLoadedChatState();
|
||||||
return {
|
return {
|
||||||
@@ -131,7 +153,7 @@ const getLatestSession = async () => {
|
|||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
const sessions = await db.getAll(SESSION_STORE);
|
const sessions = await db.getAll(SESSION_STORE);
|
||||||
if (sessions.length === 0) return undefined;
|
if (sessions.length === 0) return undefined;
|
||||||
return sessions.sort((left, right) => right.updatedAt - left.updatedAt)[0];
|
return sessions.sort(compareSessionsByAnchorTime)[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
const migrateLegacyLocalStorage = async () => {
|
const migrateLegacyLocalStorage = async () => {
|
||||||
@@ -215,10 +237,7 @@ export const saveActiveChatState = async (
|
|||||||
): Promise<string | undefined> => {
|
): Promise<string | undefined> => {
|
||||||
if (typeof window === "undefined") return state.storageSessionId;
|
if (typeof window === "undefined") return state.storageSessionId;
|
||||||
|
|
||||||
const hasContent =
|
const hasContent = hasChatContent(state);
|
||||||
state.messages.length > 0 ||
|
|
||||||
state.branchGroups.length > 0 ||
|
|
||||||
Boolean(state.sessionId);
|
|
||||||
|
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
const existingSession = state.storageSessionId
|
const existingSession = state.storageSessionId
|
||||||
@@ -241,11 +260,15 @@ export const saveActiveChatState = async (
|
|||||||
const storageSessionId = state.storageSessionId ?? createId();
|
const storageSessionId = state.storageSessionId ?? createId();
|
||||||
const preferredTitle = state.title?.trim();
|
const preferredTitle = state.title?.trim();
|
||||||
const finalTitle = preferredTitle || existingSession?.title || "新对话";
|
const finalTitle = preferredTitle || existingSession?.title || "新对话";
|
||||||
|
const shouldAnchorCreatedAtToFirstMessage =
|
||||||
|
Boolean(existingSession) && !hasChatContent(existingSession) && hasContent;
|
||||||
const nextRecord: ChatSessionRecord = {
|
const nextRecord: ChatSessionRecord = {
|
||||||
id: storageSessionId,
|
id: storageSessionId,
|
||||||
title: finalTitle,
|
title: finalTitle,
|
||||||
isTitleManuallyEdited: state.isTitleManuallyEdited ?? existingSession?.isTitleManuallyEdited ?? false,
|
isTitleManuallyEdited: state.isTitleManuallyEdited ?? existingSession?.isTitleManuallyEdited ?? false,
|
||||||
createdAt: existingSession?.createdAt ?? now,
|
createdAt: shouldAnchorCreatedAtToFirstMessage
|
||||||
|
? now
|
||||||
|
: existingSession?.createdAt ?? now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
sessionId: state.sessionId,
|
sessionId: state.sessionId,
|
||||||
messages: sanitizeMessages(state.messages),
|
messages: sanitizeMessages(state.messages),
|
||||||
@@ -268,9 +291,7 @@ export const listChatSessions = async (): Promise<ChatSessionSummary[]> => {
|
|||||||
|
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
const sessions = await db.getAll(SESSION_STORE);
|
const sessions = await db.getAll(SESSION_STORE);
|
||||||
return sessions
|
return sessions.sort(compareSessionsByAnchorTime).map(toSessionSummary);
|
||||||
.sort((left, right) => right.updatedAt - left.updatedAt)
|
|
||||||
.map(toSessionSummary);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateChatSessionTitle = async (
|
export const updateChatSessionTitle = async (
|
||||||
@@ -353,9 +374,7 @@ export const deleteChatSession = async (sessionId: string): Promise<string | und
|
|||||||
await db.delete(SESSION_STORE, sessionId);
|
await db.delete(SESSION_STORE, sessionId);
|
||||||
|
|
||||||
const remainingSessions = await db.getAll(SESSION_STORE);
|
const remainingSessions = await db.getAll(SESSION_STORE);
|
||||||
const nextActiveSession = remainingSessions.sort(
|
const nextActiveSession = remainingSessions.sort(compareSessionsByAnchorTime)[0];
|
||||||
(left, right) => right.updatedAt - left.updatedAt,
|
|
||||||
)[0];
|
|
||||||
const meta = await getMeta();
|
const meta = await getMeta();
|
||||||
|
|
||||||
await setMeta({
|
await setMeta({
|
||||||
|
|||||||
Reference in New Issue
Block a user