diff --git a/client/src/components/Files/VectorStore/VectorStoreListItem.tsx b/client/src/components/Files/VectorStore/VectorStoreListItem.tsx index 4b132e92a3..720ebd0afe 100644 --- a/client/src/components/Files/VectorStore/VectorStoreListItem.tsx +++ b/client/src/components/Files/VectorStore/VectorStoreListItem.tsx @@ -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 = { diff --git a/client/src/utils/convos.spec.ts b/client/src/utils/convos.spec.ts index 8669dadd46..d66f286333 100644 --- a/client/src/utils/convos.spec.ts +++ b/client/src/utils/convos.spec.ts @@ -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); }); }); diff --git a/client/src/utils/convos.ts b/client/src/utils/convos.ts index 4b6b05cdd6..5c61b905af 100644 --- a/client/src/utils/convos.ts +++ b/client/src/utils/convos.ts @@ -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, ): 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));