mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-18 17:30:16 +01:00
🗂️ fix: Optimize Conversation Grouping and Sorting (#4173)
* chore: remove double import of TrashIcon * fix(convos): eslint warnings * ci(convos): add test for month sorting * fix(convos): grouping by month in chronological order instead of alphabetical, optimize sort * ci: additional tests for conversation sorting * chore: fix eslint disable rule * chore: imports, use constant enum for 'new' value * fix: test dependent on current date
This commit is contained in:
parent
44458d3832
commit
c1c13a69dc
3 changed files with 243 additions and 39 deletions
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { TVectorStore } from '~/common';
|
||||
import { DotsIcon, TrashIcon, TrashIcon } from '~/components/svg';
|
||||
import { DotsIcon, TrashIcon } from '~/components/svg';
|
||||
import { Button } from '~/components/ui';
|
||||
|
||||
type VectorStoreListItemProps = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { convoData } from './convos.fakeData';
|
||||
import { Constants } from 'librechat-data-provider';
|
||||
import type { TConversation, ConversationData } from 'librechat-data-provider';
|
||||
import {
|
||||
dateKeys,
|
||||
addConversation,
|
||||
|
|
@ -8,7 +9,7 @@ import {
|
|||
findPageForConversation,
|
||||
groupConversationsByDate,
|
||||
} from './convos';
|
||||
import type { TConversation, ConversationData } from 'librechat-data-provider';
|
||||
import { convoData } from './convos.fakeData';
|
||||
import { normalizeData } from './collection';
|
||||
|
||||
describe('Conversation Utilities', () => {
|
||||
|
|
@ -60,18 +61,182 @@ describe('Conversation Utilities', () => {
|
|||
const uniqueIds = [...new Set(allGroupedIds)];
|
||||
expect(allGroupedIds.length).toBe(uniqueIds.length);
|
||||
});
|
||||
|
||||
it('sorts conversations by month correctly', () => {
|
||||
const conversations = [
|
||||
{ conversationId: '1', updatedAt: '2023-01-01T12:00:00Z' }, // January 2023
|
||||
{ conversationId: '2', updatedAt: '2023-12-01T12:00:00Z' }, // December 2023
|
||||
{ conversationId: '3', updatedAt: '2023-02-01T12:00:00Z' }, // February 2023
|
||||
{ conversationId: '4', updatedAt: '2023-11-01T12:00:00Z' }, // November 2023
|
||||
{ conversationId: '5', updatedAt: '2022-12-01T12:00:00Z' }, // December 2022
|
||||
];
|
||||
|
||||
const grouped = groupConversationsByDate(conversations as TConversation[]);
|
||||
|
||||
// Check if the years are in the correct order (most recent first)
|
||||
expect(grouped.map(([key]) => key)).toEqual([' 2023', ' 2022']);
|
||||
|
||||
// Check if conversations within 2023 are sorted correctly by month
|
||||
const conversationsIn2023 = grouped[0][1];
|
||||
const monthsIn2023 = conversationsIn2023.map((c) => new Date(c.updatedAt).getMonth());
|
||||
expect(monthsIn2023).toEqual([11, 10, 1, 0]); // December (11), November (10), February (1), January (0)
|
||||
|
||||
// Check if the conversation from 2022 is in its own group
|
||||
expect(grouped[1][1].length).toBe(1);
|
||||
expect(new Date(grouped[1][1][0].updatedAt).getFullYear()).toBe(2022);
|
||||
});
|
||||
|
||||
it('handles conversations from multiple years correctly', () => {
|
||||
const conversations = [
|
||||
{ conversationId: '1', updatedAt: '2023-01-01T12:00:00Z' }, // January 2023
|
||||
{ conversationId: '2', updatedAt: '2022-12-01T12:00:00Z' }, // December 2022
|
||||
{ conversationId: '3', updatedAt: '2021-06-01T12:00:00Z' }, // June 2021
|
||||
{ conversationId: '4', updatedAt: '2023-06-01T12:00:00Z' }, // June 2023
|
||||
{ conversationId: '5', updatedAt: '2021-12-01T12:00:00Z' }, // December 2021
|
||||
];
|
||||
|
||||
const grouped = groupConversationsByDate(conversations as TConversation[]);
|
||||
|
||||
expect(grouped.map(([key]) => key)).toEqual([' 2023', ' 2022', ' 2021']);
|
||||
expect(grouped[0][1].map((c) => new Date(c.updatedAt).getMonth())).toEqual([5, 0]); // June, January
|
||||
expect(grouped[1][1].map((c) => new Date(c.updatedAt).getMonth())).toEqual([11]); // December
|
||||
expect(grouped[2][1].map((c) => new Date(c.updatedAt).getMonth())).toEqual([11, 5]); // December, June
|
||||
});
|
||||
|
||||
it('handles conversations from the same month correctly', () => {
|
||||
const conversations = [
|
||||
{ conversationId: '1', updatedAt: '2023-06-01T12:00:00Z' },
|
||||
{ conversationId: '2', updatedAt: '2023-06-15T12:00:00Z' },
|
||||
{ conversationId: '3', updatedAt: '2023-06-30T12:00:00Z' },
|
||||
];
|
||||
|
||||
const grouped = groupConversationsByDate(conversations as TConversation[]);
|
||||
|
||||
expect(grouped.length).toBe(1);
|
||||
expect(grouped[0][0]).toBe(' 2023');
|
||||
expect(grouped[0][1].map((c) => c.conversationId)).toEqual(['3', '2', '1']);
|
||||
});
|
||||
|
||||
it('handles conversations from today, yesterday, and previous days correctly', () => {
|
||||
const today = new Date();
|
||||
const yesterday = new Date(today);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
const twoDaysAgo = new Date(today);
|
||||
twoDaysAgo.setDate(twoDaysAgo.getDate() - 2);
|
||||
|
||||
const conversations = [
|
||||
{ conversationId: '1', updatedAt: today.toISOString() },
|
||||
{ conversationId: '2', updatedAt: yesterday.toISOString() },
|
||||
{ conversationId: '3', updatedAt: twoDaysAgo.toISOString() },
|
||||
];
|
||||
|
||||
const grouped = groupConversationsByDate(conversations as TConversation[]);
|
||||
|
||||
expect(grouped.map(([key]) => key)).toEqual([
|
||||
dateKeys.today,
|
||||
dateKeys.yesterday,
|
||||
dateKeys.previous7Days,
|
||||
]);
|
||||
});
|
||||
|
||||
it('handles conversations with null or undefined updatedAt correctly', () => {
|
||||
const conversations = [
|
||||
{ conversationId: '1', updatedAt: '2023-06-01T12:00:00Z' },
|
||||
{ conversationId: '2', updatedAt: null },
|
||||
{ conversationId: '3', updatedAt: undefined },
|
||||
];
|
||||
|
||||
const grouped = groupConversationsByDate(conversations as TConversation[]);
|
||||
|
||||
expect(grouped.length).toBe(2); // One group for 2023 and one for today (null/undefined dates)
|
||||
expect(grouped[0][0]).toBe(dateKeys.today);
|
||||
expect(grouped[0][1].length).toBe(2); // Two conversations with null/undefined dates
|
||||
expect(grouped[1][0]).toBe(' 2023');
|
||||
expect(grouped[1][1].length).toBe(1); // One conversation from 2023
|
||||
});
|
||||
|
||||
it('handles an empty array of conversations', () => {
|
||||
const grouped = groupConversationsByDate([]);
|
||||
|
||||
expect(grouped).toEqual([]);
|
||||
});
|
||||
|
||||
it('correctly groups and sorts conversations for every month of the year', () => {
|
||||
const months = [
|
||||
'january',
|
||||
'february',
|
||||
'march',
|
||||
'april',
|
||||
'may',
|
||||
'june',
|
||||
'july',
|
||||
'august',
|
||||
'september',
|
||||
'october',
|
||||
'november',
|
||||
'december',
|
||||
];
|
||||
|
||||
// Create conversations for each month in both 2023 and 2022
|
||||
const conversations = months.flatMap((month, index) => [
|
||||
{
|
||||
conversationId: `2023-${month}`,
|
||||
updatedAt: `2023-${String(index + 1).padStart(2, '0')}-15T12:00:00Z`,
|
||||
},
|
||||
{
|
||||
conversationId: `2022-${month}`,
|
||||
updatedAt: `2022-${String(index + 1).padStart(2, '0')}-15T12:00:00Z`,
|
||||
},
|
||||
]);
|
||||
|
||||
const grouped = groupConversationsByDate(conversations as TConversation[]);
|
||||
|
||||
// Check that we have two year groups
|
||||
expect(grouped.length).toBe(2);
|
||||
|
||||
// Check 2023 months
|
||||
const group2023 = grouped.find(([key]) => key === ' 2023') ?? [];
|
||||
expect(group2023).toBeDefined();
|
||||
const grouped2023 = group2023[1];
|
||||
expect(grouped2023?.length).toBe(12);
|
||||
expect(grouped2023?.map((c) => new Date(c.updatedAt).getMonth())).toEqual([
|
||||
11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0,
|
||||
]);
|
||||
|
||||
// Check 2022 months
|
||||
const group2022 = grouped.find(([key]) => key === ' 2022') ?? [];
|
||||
expect(group2022).toBeDefined();
|
||||
const grouped2022 = group2022[1];
|
||||
expect(grouped2022?.length).toBe(12);
|
||||
expect(grouped2022?.map((c) => new Date(c.updatedAt).getMonth())).toEqual([
|
||||
11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0,
|
||||
]);
|
||||
|
||||
// Check that all conversations are accounted for
|
||||
const totalGroupedConversations =
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
grouped.reduce((total, [_, convos]) => total + convos.length, 0);
|
||||
expect(totalGroupedConversations).toBe(conversations.length);
|
||||
|
||||
// Check that the years are in the correct order
|
||||
const yearOrder = grouped.map(([key]) => key);
|
||||
expect(yearOrder).toEqual([' 2023', ' 2022']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addConversation', () => {
|
||||
it('adds a new conversation to the top of the list', () => {
|
||||
const data = { pages: [{ conversations: [] }] };
|
||||
const newConversation = { conversationId: 'new', updatedAt: '2023-04-02T12:00:00Z' };
|
||||
const newConversation = {
|
||||
conversationId: Constants.NEW_CONVO,
|
||||
updatedAt: '2023-04-02T12:00:00Z',
|
||||
};
|
||||
const newData = addConversation(
|
||||
data as unknown as ConversationData,
|
||||
newConversation as TConversation,
|
||||
);
|
||||
expect(newData.pages[0].conversations).toHaveLength(1);
|
||||
expect(newData.pages[0].conversations[0].conversationId).toBe('new');
|
||||
expect(newData.pages[0].conversations[0].conversationId).toBe(Constants.NEW_CONVO);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -171,13 +336,13 @@ describe('Conversation Utilities with Fake Data', () => {
|
|||
describe('addConversation', () => {
|
||||
it('adds a new conversation to the existing fake data', () => {
|
||||
const newConversation = {
|
||||
conversationId: 'new',
|
||||
conversationId: Constants.NEW_CONVO,
|
||||
updatedAt: new Date().toISOString(),
|
||||
} as TConversation;
|
||||
const initialLength = convoData.pages[0].conversations.length;
|
||||
const newData = addConversation(convoData, newConversation);
|
||||
expect(newData.pages[0].conversations.length).toBe(initialLength + 1);
|
||||
expect(newData.pages[0].conversations[0].conversationId).toBe('new');
|
||||
expect(newData.pages[0].conversations[0].conversationId).toBe(Constants.NEW_CONVO);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -60,6 +60,32 @@ const getGroupName = (date: Date) => {
|
|||
return ' ' + getYear(date).toString();
|
||||
};
|
||||
|
||||
const monthOrderMap = new Map([
|
||||
['december', 11],
|
||||
['november', 10],
|
||||
['october', 9],
|
||||
['september', 8],
|
||||
['august', 7],
|
||||
['july', 6],
|
||||
['june', 5],
|
||||
['may', 4],
|
||||
['april', 3],
|
||||
['march', 2],
|
||||
['february', 1],
|
||||
['january', 0],
|
||||
]);
|
||||
|
||||
const dateKeysReverse = Object.fromEntries(
|
||||
Object.entries(dateKeys).map(([key, value]) => [value, key]),
|
||||
);
|
||||
|
||||
const dateGroupsSet = new Set([
|
||||
dateKeys.today,
|
||||
dateKeys.yesterday,
|
||||
dateKeys.previous7Days,
|
||||
dateKeys.previous30Days,
|
||||
]);
|
||||
|
||||
export const groupConversationsByDate = (
|
||||
conversations: Array<TConversation | null>,
|
||||
): GroupedConversations => {
|
||||
|
|
@ -68,47 +94,60 @@ export const groupConversationsByDate = (
|
|||
}
|
||||
|
||||
const seenConversationIds = new Set();
|
||||
const groups = conversations.reduce((acc, conversation) => {
|
||||
if (!conversation) {
|
||||
return acc;
|
||||
}
|
||||
const groups = new Map();
|
||||
const today = startOfToday();
|
||||
|
||||
if (seenConversationIds.has(conversation.conversationId)) {
|
||||
return acc;
|
||||
conversations.forEach((conversation) => {
|
||||
if (!conversation || seenConversationIds.has(conversation.conversationId)) {
|
||||
return;
|
||||
}
|
||||
seenConversationIds.add(conversation.conversationId);
|
||||
|
||||
const date = conversation.updatedAt ? parseISO(conversation.updatedAt) : startOfToday();
|
||||
const date = conversation.updatedAt ? parseISO(conversation.updatedAt) : today;
|
||||
const groupName = getGroupName(date);
|
||||
if (!acc[groupName]) {
|
||||
acc[groupName] = [];
|
||||
if (!groups.has(groupName)) {
|
||||
groups.set(groupName, []);
|
||||
}
|
||||
acc[groupName].push(conversation);
|
||||
return acc;
|
||||
}, {});
|
||||
groups.get(groupName).push(conversation);
|
||||
});
|
||||
|
||||
const sortedGroups = {};
|
||||
const dateGroups = [
|
||||
dateKeys.today,
|
||||
dateKeys.yesterday,
|
||||
dateKeys.previous7Days,
|
||||
dateKeys.previous30Days,
|
||||
];
|
||||
dateGroups.forEach((group) => {
|
||||
if (groups[group]) {
|
||||
sortedGroups[group] = groups[group];
|
||||
const sortedGroups = new Map();
|
||||
|
||||
// Add date groups first
|
||||
dateGroupsSet.forEach((group) => {
|
||||
if (groups.has(group)) {
|
||||
sortedGroups.set(group, groups.get(group));
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(groups)
|
||||
.filter((group) => !dateGroups.includes(group))
|
||||
.sort()
|
||||
.reverse()
|
||||
.forEach((year) => {
|
||||
sortedGroups[year] = groups[year];
|
||||
// Sort and add year/month groups
|
||||
const yearMonthGroups = Array.from(groups.keys())
|
||||
.filter((group) => !dateGroupsSet.has(group))
|
||||
.sort((a, b) => {
|
||||
const [yearA, yearB] = [parseInt(a.trim()), parseInt(b.trim())];
|
||||
if (yearA !== yearB) {
|
||||
return yearB - yearA;
|
||||
}
|
||||
|
||||
const [monthA, monthB] = [dateKeysReverse[a], dateKeysReverse[b]];
|
||||
const bOrder = monthOrderMap.get(monthB) ?? -1;
|
||||
const aOrder = monthOrderMap.get(monthA) ?? -1;
|
||||
return bOrder - aOrder;
|
||||
});
|
||||
|
||||
return Object.entries(sortedGroups);
|
||||
yearMonthGroups.forEach((group) => {
|
||||
sortedGroups.set(group, groups.get(group));
|
||||
});
|
||||
|
||||
// Sort conversations within each group
|
||||
sortedGroups.forEach((conversations) => {
|
||||
conversations.sort(
|
||||
(a: TConversation, b: TConversation) =>
|
||||
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(),
|
||||
);
|
||||
});
|
||||
|
||||
return Array.from(sortedGroups, ([key, value]) => [key, value]);
|
||||
};
|
||||
|
||||
export const addConversation = (
|
||||
|
|
@ -195,7 +234,7 @@ export const getConversationById = (
|
|||
data: ConversationData | undefined,
|
||||
conversationId: string | null,
|
||||
): TConversation | undefined => {
|
||||
if (!data || !conversationId) {
|
||||
if (!data || !(conversationId ?? '')) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
|
@ -224,11 +263,11 @@ export function storeEndpointSettings(conversation: TConversation | null) {
|
|||
return;
|
||||
}
|
||||
|
||||
const lastModel = JSON.parse(localStorage.getItem(LocalStorageKeys.LAST_MODEL) || '{}');
|
||||
const lastModel = JSON.parse(localStorage.getItem(LocalStorageKeys.LAST_MODEL) ?? '{}');
|
||||
lastModel[endpoint] = model;
|
||||
|
||||
if (endpoint === EModelEndpoint.gptPlugins) {
|
||||
lastModel.secondaryModel = agentOptions?.model || model || '';
|
||||
lastModel.secondaryModel = agentOptions?.model ?? model ?? '';
|
||||
}
|
||||
|
||||
localStorage.setItem(LocalStorageKeys.LAST_MODEL, JSON.stringify(lastModel));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue