♾️ style: Infinite Scroll Nav and Sort Convos by Date/Usage (#1708)

* Style: Infinite Scroll and Group convos by date

* Style: Infinite Scroll and Group convos by date- Redesign NavBar

* Style: Infinite Scroll and Group convos by date- Redesign NavBar - Clean code

* Style: Infinite Scroll and Group convos by date- Redesign NavBar - Redesign NewChat Component

* Style: Infinite Scroll and Group convos by date- Redesign NavBar - Redesign NewChat Component

* Style: Infinite Scroll and Group convos by date- Redesign NavBar - Redesign NewChat Component

* Including OpenRouter and Mistral icon

* refactor(Conversations): cleanup use of utility functions and typing

* refactor(Nav/NewChat): use localStorage `lastConversationSetup` to determine the endpoint to use, as well as icons -> JSX components, remove use of `endpointSelected`

* refactor: remove use of `isFirstToday`

* refactor(Nav): remove use of `endpointSelected`, consolidate scrolling logic to its own hook `useNavScrolling`, remove use of recoil `conversation`

* refactor: Add spinner to bottom of list, throttle fetching, move query hooks to client workspace

* chore: sort by `updatedAt` field

* refactor: optimize conversation infinite query, use optimistic updates, add conversation helpers for managing pagination, remove unnecessary operations

* feat: gen_title route for generating the title for the conversation

* style(Convo): change hover bg-color

* refactor: memoize groupedConversations and return as array of tuples, correctly update convos pre/post message stream, only call genTitle if conversation is new, make `addConversation` dynamically either add/update depending if convo exists in pages already, reorganize type definitions

* style: rename Header NewChat Button -> HeaderNewChat, add NewChatIcon, closely match main Nav New Chat button to ChatGPT

* style(NewChat): add hover bg color

* style: cleanup comments, match ChatGPT nav styling, redesign search bar, make part of new chat sticky header, move Nav under same parent as outlet/mobilenav, remove legacy code, search only if searchQuery is not empty

* feat: add tests for conversation helpers and ensure no duplicate conversations are ever grouped

* style: hover bg-color

* feat: alt-click on convo item to open conversation in new tab

* chore: send error message when `gen_title` fails

---------

Co-authored-by: Walber Cardoso <walbercardoso@gmail.com>
This commit is contained in:
Danny Avila 2024-02-03 20:25:35 -05:00 committed by GitHub
parent 13b2d6e34a
commit 74459d6261
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
48 changed files with 1788 additions and 391 deletions

View file

@ -0,0 +1,637 @@
import type { ConversationData } from 'librechat-data-provider';
/* @ts-ignore */
export const convoData: ConversationData = {
pages: [
{
conversations: [
{
_id: '65bd0a2f7cb605e374e93ed1',
conversationId: 'bf71b257-3625-440c-b6a6-03f6a3fd6a4d',
user: 'my-user-id',
__v: 0,
chatGptLabel: null,
createdAt: '2024-02-02T15:28:47.123Z',
endpoint: 'openAI',
frequency_penalty: 0,
imageDetail: 'auto',
messages: [
'65bd0a2f7cb605e374e93ea3',
'65bd0a2f7cb605e374e94028',
'65bec4af7cb605e3741e84d1',
'65bec4af7cb605e3741e86aa',
],
model: 'gpt-3.5-turbo-0125',
presence_penalty: 0,
promptPrefix: null,
resendImages: false,
temperature: 1,
title: 'A Long Story',
top_p: 1,
updatedAt: '2024-02-03T22:56:46.269Z',
_meiliIndex: true,
},
{
_id: '65bec2c27cb605e374189730',
conversationId: '544f1c4f-030f-4ea2-997c-35923f5d8ee2',
user: 'my-user-id',
__v: 0,
_meiliIndex: true,
chatGptLabel: null,
createdAt: '2024-02-03T22:48:33.144Z',
endpoint: 'OpenRouter',
endpointType: 'custom',
frequency_penalty: 0,
imageDetail: 'auto',
messages: [
'65bec2c27cb605e3741896fd',
'65bec2c47cb605e374189c16',
'65bec2d97cb605e37418d7dc',
'65bec2e67cb605e374190490',
'65bec2e77cb605e3741907df',
],
model: 'meta-llama/llama-2-13b-chat',
presence_penalty: 0,
promptPrefix: null,
resendImages: false,
temperature: 1,
title: 'How Are You Doing?',
top_p: 1,
updatedAt: '2024-02-03T22:49:21.140Z',
},
{
_id: '65be8c0d7cb605e3747323ad',
conversationId: 'e3f19866-190e-43ab-869f-10260f07530f',
user: 'my-user-id',
__v: 0,
chatGptLabel: null,
createdAt: '2024-02-03T18:55:09.560Z',
endpoint: 'openAI',
frequency_penalty: 0,
imageDetail: 'auto',
messages: ['65be8c0d7cb605e37473236c', '65be8c0d7cb605e374732475'],
model: 'gpt-3.5-turbo-0301',
presence_penalty: 0,
promptPrefix: null,
resendImages: false,
temperature: 1,
title: 'A Long Story',
top_p: 1,
updatedAt: '2024-02-03T18:55:17.586Z',
_meiliIndex: true,
},
{
_id: '65be6bd17cb605e37412706b',
conversationId: '4d569723-3aff-4f52-9bbf-e127783a06ac',
user: 'my-user-id',
__v: 0,
chatGptLabel: null,
createdAt: '2024-02-03T16:37:37.600Z',
endpoint: 'openAI',
frequency_penalty: 0,
imageDetail: 'auto',
messages: [
'65be6bd17cb605e374127036',
'65be6bd17cb605e374127156',
'65be8c007cb605e37472f7a9',
'65be8c007cb605e37472f8b5',
'65be8c057cb605e374730c05',
'65be8c067cb605e374730dae',
],
model: 'gpt-3.5-turbo-0301',
presence_penalty: 0,
promptPrefix: null,
resendImages: false,
temperature: 1,
title: 'Write Einstein\'s Famous Equation in LaTeX',
top_p: 1,
updatedAt: '2024-02-03T18:55:02.407Z',
_meiliIndex: true,
},
{
_id: '65be6b7b7cb605e374117546',
conversationId: '640db89d-459f-4411-a0b0-26cb1d53bf1a',
user: 'my-user-id',
__v: 0,
chatGptLabel: null,
createdAt: '2024-02-03T16:36:11.010Z',
endpoint: 'openAI',
frequency_penalty: 0,
imageDetail: 'auto',
messages: [
'65be6b7a7cb605e374117519',
'65be6b7b7cb605e37411766c',
'65be6e1c7cb605e374195898',
'65be6e1d7cb605e374195985',
'65be6e767cb605e3741a5d94',
'65be6e767cb605e3741a5e8e',
'65be89ee7cb605e3746ccb52',
],
model: 'gpt-3.5-turbo-0301',
presence_penalty: 0,
promptPrefix: null,
resendImages: false,
temperature: 1,
title: 'Fibonacci Solver in Python',
top_p: 1,
updatedAt: '2024-02-03T18:46:06.636Z',
_meiliIndex: true,
},
{
_id: '65bde6117cb605e37481d315',
conversationId: 'a9b39a05-fdc0-47f4-bd3b-b0aca618f656',
user: 'my-user-id',
__v: 0,
chatGptLabel: null,
createdAt: '2024-02-03T07:06:55.573Z',
endpoint: 'openAI',
frequency_penalty: 0,
imageDetail: 'auto',
messages: [
'65bde6117cb605e37481d294',
'65bde6117cb605e37481d4eb',
'65be6e7b7cb605e3741a6dd4',
'65be6e7b7cb605e3741a6ebe',
'65be6fa97cb605e3741df0ed',
'65be6fa97cb605e3741df249',
'65be709a7cb605e37420ca1b',
'65be709a7cb605e37420cb24',
'65be71ba7cb605e374244131',
'65be71bb7cb605e37424423e',
'65be79017cb605e37439dddd',
'65be79027cb605e37439df49',
'65be82e57cb605e37457d6b5',
'65be84727cb605e3745c76ff',
],
model: 'gpt-3.5-turbo-0301',
presence_penalty: 0,
promptPrefix: null,
resendImages: false,
temperature: 1,
title: 'test',
top_p: 1,
updatedAt: '2024-02-03T18:22:42.524Z',
_meiliIndex: true,
},
{
_id: '65bd1b347cb605e3741e29dc',
conversationId: '3ce779d7-8535-4a43-9b70-e0d3160f299e',
user: 'my-user-id',
__v: 0,
chatGptLabel: null,
createdAt: '2024-02-02T16:41:24.324Z',
endpoint: 'openAI',
frequency_penalty: 0,
imageDetail: 'auto',
messages: [
'65bd1b347cb605e3741e299d',
'65bd1b347cb605e3741e2ba6',
'65be82ed7cb605e37457f381',
],
model: 'gpt-3.5-turbo-0125',
presence_penalty: 0,
promptPrefix: null,
resendImages: false,
temperature: 1,
title: 'Hello! How can I assist you today?',
top_p: 1,
updatedAt: '2024-02-03T18:16:13.357Z',
_meiliIndex: true,
},
{
_id: '65bdd6d77cb605e37454b694',
conversationId: 'c162f906-06fb-405a-b7e6-773a0fc5f8e9',
user: 'my-user-id',
__v: 0,
chatGptLabel: null,
createdAt: '2024-02-03T06:01:57.968Z',
endpoint: 'openAI',
frequency_penalty: 0,
imageDetail: 'auto',
messages: [
'65bdd6d77cb605e37454b66c',
'65bdd6d87cb605e37454b892',
'65bddca57cb605e37465ceea',
'65bddcab7cb605e37465de2b',
'65bddccb7cb605e374663d37',
'65bddccc7cb605e374663ea9',
'65bddce17cb605e374667f08',
'65bddce27cb605e374668096',
'65bdeb557cb605e37491787a',
'65bdeb567cb605e374917aa2',
'65be82dc7cb605e37457b70e',
],
model: 'gpt-4-0125-preview',
presence_penalty: 0,
promptPrefix: null,
resendImages: false,
temperature: 1,
title: 'test',
top_p: 1,
updatedAt: '2024-02-03T18:15:57.133Z',
_meiliIndex: true,
},
{
_id: '65be82c87cb605e374577820',
conversationId: '48bbc7d5-1815-4024-8ac6-6c9f59242426',
user: 'my-user-id',
__v: 0,
chatGptLabel: null,
createdAt: '2024-02-03T18:15:36.759Z',
endpoint: 'openAI',
frequency_penalty: 0,
imageDetail: 'auto',
messages: [
'65be82c87cb605e3745777f6',
'65be82c97cb605e374577911',
'65be82d57cb605e37457a2fc',
],
model: 'gpt-3.5-turbo-0301',
presence_penalty: 0,
promptPrefix: null,
resendImages: false,
temperature: 1,
title: 'Hello! How can I assist you today?',
top_p: 1,
updatedAt: '2024-02-03T18:15:49.536Z',
_meiliIndex: true,
},
{
_id: '65bde8567cb605e37488ac01',
conversationId: '97d6e676-b05b-43f9-8f56-1c07e8a1eb4e',
user: 'my-user-id',
__v: 0,
chatGptLabel: null,
createdAt: '2024-02-03T07:16:36.407Z',
endpoint: 'openAI',
frequency_penalty: 0,
imageDetail: 'auto',
messages: [
'65bde8557cb605e37488abe2',
'65bde8567cb605e37488ad32',
'65be6eb97cb605e3741b267b',
'65be6eba7cb605e3741b2849',
'65be703c7cb605e3741fb06d',
'65be703d7cb605e3741fb182',
'65be710b7cb605e374221776',
'65be710b7cb605e37422193a',
'65be72137cb605e37425544c',
'65be72137cb605e37425556c',
'65be7e2c7cb605e3744975ee',
'65be7e6c7cb605e3744a3d29',
'65be81147cb605e374525ccb',
'65be826b7cb605e374565dcf',
'65be827e7cb605e37456986c',
'65be82967cb605e37456db94',
'65be82c07cb605e374575ef6',
],
model: 'gpt-3.5-turbo-0301',
presence_penalty: 0,
promptPrefix: null,
resendImages: false,
temperature: 1,
title: 'test',
top_p: 1,
updatedAt: '2024-02-03T18:15:28.531Z',
_meiliIndex: true,
},
{
_id: '65bde95c7cb605e3748ba8ae',
conversationId: '293f230b-ceaa-4802-9611-c4fe7e4b1fd6',
user: 'my-user-id',
__v: 0,
chatGptLabel: null,
createdAt: '2024-02-03T07:20:58.933Z',
endpoint: 'openAI',
frequency_penalty: 0,
imageDetail: 'auto',
messages: [
'65bde95c7cb605e3748ba84d',
'65bde95c7cb605e3748baa9d',
'65be6b3a7cb605e37410ab2d',
'65be6b3a7cb605e37410ac16',
],
model: 'gpt-3.5-turbo-0301',
presence_penalty: 0,
promptPrefix: null,
resendImages: false,
temperature: 1,
title: 'Hello, How Can I Help You?',
top_p: 1,
updatedAt: '2024-02-03T16:35:07.134Z',
_meiliIndex: true,
},
{
_id: '65be6a967cb605e3740ebdc4',
conversationId: '279db3ad-2219-4229-b99a-e19a2b191dd7',
user: 'my-user-id',
__v: 0,
chatGptLabel: null,
createdAt: '2024-02-03T16:32:22.480Z',
endpoint: 'openAI',
frequency_penalty: 0,
imageDetail: 'auto',
messages: ['65be6a967cb605e3740ebd60', '65be6a967cb605e3740ebf38'],
model: 'gpt-3.5-turbo-0301',
presence_penalty: 0,
promptPrefix: null,
resendImages: false,
temperature: 1,
title: 'Hello there! How may I assist you today?',
top_p: 1,
updatedAt: '2024-02-03T16:32:25.066Z',
_meiliIndex: true,
},
{
_id: '65bdea947cb605e3748f42c0',
conversationId: '3e62a081-055c-4ee5-9e33-7ab8b3d367c9',
user: 'my-user-id',
__v: 0,
chatGptLabel: null,
createdAt: '2024-02-03T07:26:10.988Z',
endpoint: 'openAI',
frequency_penalty: 0,
imageDetail: 'auto',
messages: ['65bdea947cb605e3748f4275', '65bdea947cb605e3748f43af'],
model: 'gpt-3.5-turbo-0301',
presence_penalty: 0,
promptPrefix: null,
resendImages: false,
temperature: 1,
title: 'Hello! How may I assist you today?',
top_p: 1,
updatedAt: '2024-02-03T07:26:13.177Z',
_meiliIndex: true,
},
{
_id: '65bde8aa7cb605e37489a27b',
conversationId: 'b97836fc-8566-48e2-a28d-99f99528ca20',
user: 'my-user-id',
__v: 0,
chatGptLabel: null,
createdAt: '2024-02-03T07:18:01.245Z',
endpoint: 'openAI',
frequency_penalty: 0,
imageDetail: 'auto',
messages: ['65bde8aa7cb605e37489a256', '65bde8ab7cb605e37489a3a1'],
model: 'gpt-3.5-turbo-0301',
presence_penalty: 0,
promptPrefix: null,
resendImages: false,
temperature: 1,
title: 'Hello! How can I assist you today?',
top_p: 1,
updatedAt: '2024-02-03T07:18:04.006Z',
_meiliIndex: true,
},
{
_id: '65bde8357cb605e3748850a7',
conversationId: 'aa52b79d-ebe7-49d1-9fee-5f5b89d56069',
user: 'my-user-id',
__v: 0,
chatGptLabel: null,
createdAt: '2024-02-03T07:16:03.728Z',
endpoint: 'openAI',
frequency_penalty: 0,
imageDetail: 'auto',
messages: ['65bde8357cb605e37488508e', '65bde8357cb605e37488520e'],
model: 'gpt-3.5-turbo-0301',
presence_penalty: 0,
promptPrefix: null,
resendImages: false,
temperature: 1,
title: 'Hello! How may I assist you today?',
top_p: 1,
updatedAt: '2024-02-03T07:16:06.189Z',
_meiliIndex: true,
},
{
_id: '65bde7887cb605e374864527',
conversationId: 'fe50b20f-8465-4866-b5ef-9bc519a00eef',
user: 'my-user-id',
__v: 0,
chatGptLabel: null,
createdAt: '2024-02-03T07:13:10.682Z',
endpoint: 'openAI',
frequency_penalty: 0,
imageDetail: 'auto',
messages: ['65bde7887cb605e3748644e0', '65bde7887cb605e37486463b'],
model: 'gpt-3.5-turbo-0301',
presence_penalty: 0,
promptPrefix: null,
resendImages: false,
temperature: 1,
title: 'Hello! How can I assist you today?',
top_p: 1,
updatedAt: '2024-02-03T07:13:12.960Z',
_meiliIndex: true,
},
{
_id: '65bde6f47cb605e37484824b',
conversationId: '2fbb4a34-4d17-4e05-8c0a-949e78572aa3',
user: 'my-user-id',
__v: 0,
chatGptLabel: null,
createdAt: '2024-02-03T07:10:42.904Z',
endpoint: 'openAI',
frequency_penalty: 0,
imageDetail: 'auto',
messages: ['65bde6f47cb605e374848207', '65bde6f47cb605e3748483b5'],
model: 'gpt-3.5-turbo-0301',
presence_penalty: 0,
promptPrefix: null,
resendImages: false,
temperature: 1,
title: 'Hello! How can I assist you today?',
top_p: 1,
updatedAt: '2024-02-03T07:10:45.245Z',
_meiliIndex: true,
},
{
_id: '65bde6a77cb605e37483941b',
conversationId: 'c0d587d0-e881-42be-a2cf-5bf01198bdac',
user: 'my-user-id',
__v: 0,
chatGptLabel: null,
createdAt: '2024-02-03T07:09:25.506Z',
endpoint: 'openAI',
frequency_penalty: 0,
imageDetail: 'auto',
messages: ['65bde6a77cb605e3748393d7', '65bde6a77cb605e374839506'],
model: 'gpt-3.5-turbo-0301',
presence_penalty: 0,
promptPrefix: null,
resendImages: false,
temperature: 1,
title: 'Hello! How can I assist you today?',
top_p: 1,
updatedAt: '2024-02-03T07:09:27.717Z',
_meiliIndex: true,
},
{
_id: '65bde65c7cb605e37482b717',
conversationId: 'acd7fa14-4165-4fa1-b2a6-637041743a78',
user: 'my-user-id',
__v: 0,
chatGptLabel: null,
createdAt: '2024-02-03T07:08:10.607Z',
endpoint: 'openAI',
frequency_penalty: 0,
imageDetail: 'auto',
messages: ['65bde65c7cb605e37482b6f7', '65bde65c7cb605e37482b822'],
model: 'gpt-3.5-turbo-0301',
presence_penalty: 0,
promptPrefix: null,
resendImages: false,
temperature: 1,
title: 'Hello! How can I assist you today?',
top_p: 1,
updatedAt: '2024-02-03T07:08:12.971Z',
_meiliIndex: true,
},
{
_id: '65bde6467cb605e37482700c',
conversationId: '61ba520e-d53b-4816-b8cc-059d89f15ed4',
user: 'my-user-id',
__v: 0,
chatGptLabel: null,
createdAt: '2024-02-03T07:07:49.166Z',
endpoint: 'openAI',
frequency_penalty: 0,
imageDetail: 'auto',
messages: ['65bde6467cb605e374826fee', '65bde6477cb605e374827125'],
model: 'gpt-3.5-turbo-0301',
presence_penalty: 0,
promptPrefix: null,
resendImages: false,
temperature: 1,
title: 'Hello! How can I assist you today?',
top_p: 1,
updatedAt: '2024-02-03T07:07:51.592Z',
_meiliIndex: true,
},
{
_id: '65bde4677cb605e3747cd139',
conversationId: 'd4f599af-aeae-4a54-b34c-bd85ce8134af',
user: 'my-user-id',
__v: 0,
chatGptLabel: null,
createdAt: '2024-02-03T06:59:49.834Z',
endpoint: 'openAI',
frequency_penalty: 0,
imageDetail: 'auto',
messages: ['65bde4677cb605e3747cd0ed', '65bde4677cb605e3747cd26d'],
model: 'gpt-3.5-turbo-0301',
presence_penalty: 0,
promptPrefix: null,
resendImages: false,
temperature: 1,
title: 'Hello! How can I assist you today?',
top_p: 1,
updatedAt: '2024-02-03T06:59:52.004Z',
_meiliIndex: true,
},
{
_id: '65bddfd37cb605e3746f4328',
conversationId: 'e424c98c-8540-428a-ae43-dc314e15849d',
user: 'my-user-id',
__v: 0,
chatGptLabel: null,
createdAt: '2024-02-03T06:40:18.167Z',
endpoint: 'openAI',
frequency_penalty: 0,
imageDetail: 'auto',
messages: ['65bddfd37cb605e3746f42c5', '65bddfd47cb605e3746f4471'],
model: 'gpt-3.5-turbo-0301',
presence_penalty: 0,
promptPrefix: null,
resendImages: false,
temperature: 1,
title: 'Hello! How can I assist you today?',
top_p: 1,
updatedAt: '2024-02-03T06:40:20.382Z',
_meiliIndex: true,
},
{
_id: '65bddeb97cb605e3746bfb8c',
conversationId: 'edac9c4d-bb66-4550-acaf-98006b83db4d',
user: 'my-user-id',
__v: 0,
chatGptLabel: null,
createdAt: '2024-02-03T06:35:35.937Z',
endpoint: 'openAI',
frequency_penalty: 0,
imageDetail: 'auto',
messages: ['65bddeb97cb605e3746bfb5e', '65bddeb97cb605e3746bfc8a'],
model: 'gpt-3.5-turbo-0301',
presence_penalty: 0,
promptPrefix: null,
resendImages: false,
temperature: 1,
title: 'Hello! How can I assist you today?',
top_p: 1,
updatedAt: '2024-02-03T06:35:38.519Z',
_meiliIndex: true,
},
{
_id: '65bdd6817cb605e37453b949',
conversationId: 'dbeca051-8af8-42cb-a611-70f669c66502',
user: 'my-user-id',
__v: 0,
chatGptLabel: null,
createdAt: '2024-02-03T06:00:31.691Z',
endpoint: 'openAI',
frequency_penalty: 0,
imageDetail: 'auto',
messages: [
'65bdd6817cb605e37453b904',
'65bdd6817cb605e37453ba9b',
'65bddd7e7cb605e3746858ff',
'65bddd7f7cb605e374685ac6',
],
model: 'gpt-3.5-turbo-0301',
presence_penalty: 0,
promptPrefix: null,
resendImages: false,
temperature: 1,
title: 'test 2',
top_p: 1,
updatedAt: '2024-02-03T06:30:21.941Z',
_meiliIndex: true,
},
{
_id: '65bdd9ac7cb605e3745cf331',
conversationId: '4a69c491-5cfc-4a62-b7d3-6a54d890dfa8',
user: 'my-user-id',
__v: 0,
chatGptLabel: null,
createdAt: '2024-02-03T06:14:02.394Z',
endpoint: 'openAI',
frequency_penalty: 0,
imageDetail: 'auto',
messages: [
'65bdd9ab7cb605e3745cf30b',
'65bdd9ac7cb605e3745cf3f6',
'65bddc417cb605e37464abc7',
'65bddc427cb605e37464ad09',
'65bddc4a7cb605e37464c7cc',
'65bddc767cb605e374654895',
],
model: 'gpt-3.5-turbo-0301',
presence_penalty: 0,
promptPrefix: null,
resendImages: false,
temperature: 1,
title: 'Hi there! How can I assist you today?',
top_p: 1,
updatedAt: '2024-02-03T06:25:59.827Z',
_meiliIndex: true,
},
],
pages: 49,
pageNumber: 1,
pageSize: 25,
},
],
pageParams: [null],
};

View file

@ -0,0 +1,219 @@
import { convoData } from './convos.fakeData';
import {
groupConversationsByDate,
addConversation,
updateConversation,
updateConvoFields,
deleteConversation,
findPageForConversation,
} from './convos';
import type { TConversation, ConversationData } from 'librechat-data-provider';
describe('Conversation Utilities', () => {
describe('groupConversationsByDate', () => {
it('groups conversations by date correctly', () => {
const conversations = [
{ conversationId: '1', updatedAt: '2023-04-01T12:00:00Z' },
{ conversationId: '2', updatedAt: new Date().toISOString() },
];
const grouped = groupConversationsByDate(conversations as TConversation[]);
expect(grouped[0][0]).toBe('Today');
expect(grouped[0][1]).toHaveLength(1);
expect(grouped[1][0]).toBe(' 2023');
expect(grouped[1][1]).toHaveLength(1);
});
it('returns an empty array for no conversations', () => {
expect(groupConversationsByDate([])).toEqual([]);
});
it('skips conversations with duplicate conversationIds', () => {
const conversations = [
{ conversationId: '1', updatedAt: '2023-12-01T12:00:00Z' }, // " 2023"
{ conversationId: '2', updatedAt: '2023-11-25T12:00:00Z' }, // " 2023"
{ conversationId: '1', updatedAt: '2023-11-20T12:00:00Z' }, // Should be skipped because of duplicate ID
{ conversationId: '3', updatedAt: '2022-12-01T12:00:00Z' }, // " 2022"
];
const grouped = groupConversationsByDate(conversations as TConversation[]);
expect(grouped).toEqual(
expect.arrayContaining([
expect.arrayContaining([' 2023', expect.arrayContaining(conversations.slice(0, 2))]),
expect.arrayContaining([' 2022', expect.arrayContaining([conversations[3]])]),
]),
);
// No duplicate IDs are present
const allGroupedIds = grouped.flatMap(([, convs]) => convs.map((c) => c.conversationId));
const uniqueIds = [...new Set(allGroupedIds)];
expect(allGroupedIds.length).toBe(uniqueIds.length);
});
});
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 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');
});
});
describe('updateConversation', () => {
it('updates an existing conversation and moves it to the top', () => {
const initialData = {
pages: [
{
conversations: [
{ conversationId: '1', updatedAt: '2023-04-01T12:00:00Z' },
{ conversationId: '2', updatedAt: '2023-04-01T13:00:00Z' },
],
},
],
};
const updatedConversation = { conversationId: '1', updatedAt: '2023-04-02T12:00:00Z' };
const newData = updateConversation(
initialData as unknown as ConversationData,
updatedConversation as TConversation,
);
expect(newData.pages[0].conversations).toHaveLength(2);
expect(newData.pages[0].conversations[0].conversationId).toBe('1');
});
});
describe('updateConvoFields', () => {
it('updates specific fields of a conversation', () => {
const initialData = {
pages: [
{
conversations: [
{ conversationId: '1', title: 'Old Title', updatedAt: '2023-04-01T12:00:00Z' },
],
},
],
};
const updatedFields = { conversationId: '1', title: 'New Title' };
const newData = updateConvoFields(
initialData as ConversationData,
updatedFields as TConversation,
);
expect(newData.pages[0].conversations[0].title).toBe('New Title');
});
});
describe('deleteConversation', () => {
it('removes a conversation by id', () => {
const initialData = {
pages: [
{
conversations: [
{ conversationId: '1', updatedAt: '2023-04-01T12:00:00Z' },
{ conversationId: '2', updatedAt: '2023-04-01T13:00:00Z' },
],
},
],
};
const newData = deleteConversation(initialData as ConversationData, '1');
expect(newData.pages[0].conversations).toHaveLength(1);
expect(newData.pages[0].conversations[0].conversationId).not.toBe('1');
});
});
describe('findPageForConversation', () => {
it('finds the correct page and index for a given conversation', () => {
const data = {
pages: [
{
conversations: [
{ conversationId: '1', updatedAt: '2023-04-01T12:00:00Z' },
{ conversationId: '2', updatedAt: '2023-04-02T13:00:00Z' },
],
},
],
};
const { pageIndex, convIndex } = findPageForConversation(data as ConversationData, {
conversationId: '2',
});
expect(pageIndex).toBe(0);
expect(convIndex).toBe(1);
});
});
});
describe('Conversation Utilities with Fake Data', () => {
describe('groupConversationsByDate', () => {
it('correctly groups conversations from fake data by date', () => {
const { pages } = convoData;
const allConversations = pages.flatMap((p) => p.conversations);
const grouped = groupConversationsByDate(allConversations);
expect(grouped).toHaveLength(1);
expect(grouped[0][1]).toBeInstanceOf(Array);
});
});
describe('addConversation', () => {
it('adds a new conversation to the existing fake data', () => {
const newConversation = {
conversationId: 'new',
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');
});
});
describe('updateConversation', () => {
it('updates an existing conversation within fake data', () => {
const updatedConversation = {
...convoData.pages[0].conversations[0],
title: 'Updated Title',
};
const newData = updateConversation(convoData, updatedConversation);
expect(newData.pages[0].conversations[0].title).toBe('Updated Title');
});
});
describe('updateConvoFields', () => {
it('updates specific fields of a conversation in fake data', () => {
const updatedFields = {
conversationId: convoData.pages[0].conversations[0].conversationId,
title: 'Partially Updated Title',
};
const newData = updateConvoFields(convoData, updatedFields as TConversation);
const updatedConversation = newData.pages[0].conversations.find(
(c) => c.conversationId === updatedFields.conversationId,
);
expect(updatedConversation?.title).toBe('Partially Updated Title');
});
});
describe('deleteConversation', () => {
it('removes a conversation by id from fake data', () => {
const conversationIdToDelete = convoData.pages[0].conversations[0].conversationId as string;
const newData = deleteConversation(convoData, conversationIdToDelete);
const deletedConvoExists = newData.pages[0].conversations.some(
(c) => c.conversationId === conversationIdToDelete,
);
expect(deletedConvoExists).toBe(false);
});
});
describe('findPageForConversation', () => {
it('finds the correct page and index for a given conversation in fake data', () => {
const targetConversation = convoData.pages[0].conversations[0];
const { pageIndex, convIndex } = findPageForConversation(convoData, {
conversationId: targetConversation.conversationId as string,
});
expect(pageIndex).toBeGreaterThanOrEqual(0);
expect(convIndex).toBeGreaterThanOrEqual(0);
});
});
});

148
client/src/utils/convos.ts Normal file
View file

@ -0,0 +1,148 @@
import { parseISO, isToday, isWithinInterval, subDays, getYear } from 'date-fns';
import type {
TConversation,
ConversationData,
ConversationUpdater,
GroupedConversations,
} from 'librechat-data-provider';
const getGroupName = (date: Date) => {
const now = new Date();
if (isToday(date)) {
return 'Today';
}
if (isWithinInterval(date, { start: subDays(now, 7), end: now })) {
return 'Last 7 days';
}
if (isWithinInterval(date, { start: subDays(now, 30), end: now })) {
return 'Last 30 days';
}
return ' ' + getYear(date).toString();
};
export const groupConversationsByDate = (conversations: TConversation[]): GroupedConversations => {
if (!Array.isArray(conversations)) {
return [];
}
const seenConversationIds = new Set();
const groups = conversations.reduce((acc, conversation) => {
if (seenConversationIds.has(conversation.conversationId)) {
return acc;
}
seenConversationIds.add(conversation.conversationId);
const date = parseISO(conversation.updatedAt);
const groupName = getGroupName(date);
if (!acc[groupName]) {
acc[groupName] = [];
}
acc[groupName].push(conversation);
return acc;
}, {});
const sortedGroups = {};
const dateGroups = ['Today', 'Last 7 days', 'Last 30 days'];
dateGroups.forEach((group) => {
if (groups[group]) {
sortedGroups[group] = groups[group];
}
});
Object.keys(groups)
.filter((group) => !dateGroups.includes(group))
.sort()
.reverse()
.forEach((year) => {
sortedGroups[year] = groups[year];
});
return Object.entries(sortedGroups);
};
export const addConversation: ConversationUpdater = (data, newConversation) => {
const newData = JSON.parse(JSON.stringify(data)) as ConversationData;
const { pageIndex, convIndex } = findPageForConversation(newData, newConversation);
if (pageIndex !== -1 && convIndex !== -1) {
return updateConversation(data, newConversation);
}
newData.pages[0].conversations.unshift({
...newConversation,
updatedAt: new Date().toISOString(),
});
return newData;
};
export function findPageForConversation(
data: ConversationData,
conversation: TConversation | { conversationId: string },
) {
for (let pageIndex = 0; pageIndex < data.pages.length; pageIndex++) {
const page = data.pages[pageIndex];
const convIndex = page.conversations.findIndex(
(c) => c.conversationId === conversation.conversationId,
);
if (convIndex !== -1) {
return { pageIndex, convIndex };
}
}
return { pageIndex: -1, convIndex: -1 }; // Not found
}
export const updateConversation: ConversationUpdater = (data, updatedConversation) => {
const newData = JSON.parse(JSON.stringify(data));
const { pageIndex, convIndex } = findPageForConversation(newData, updatedConversation);
if (pageIndex !== -1 && convIndex !== -1) {
// Remove the conversation from its current position
newData.pages[pageIndex].conversations.splice(convIndex, 1);
// Add the updated conversation to the top of the first page
newData.pages[0].conversations.unshift({
...updatedConversation,
updatedAt: new Date().toISOString(),
});
}
return newData;
};
export const updateConvoFields: ConversationUpdater = (
data: ConversationData,
updatedConversation: Partial<TConversation> & Pick<TConversation, 'conversationId'>,
): ConversationData => {
const newData = JSON.parse(JSON.stringify(data));
const { pageIndex, convIndex } = findPageForConversation(
newData,
updatedConversation as { conversationId: string },
);
if (pageIndex !== -1 && convIndex !== -1) {
const deleted = newData.pages[pageIndex].conversations.splice(convIndex, 1);
const oldConversation = deleted[0] as TConversation;
newData.pages[0].conversations.unshift({
...oldConversation,
...updatedConversation,
updatedAt: new Date().toISOString(),
});
}
return newData;
};
export const deleteConversation = (
data: ConversationData,
conversationId: string,
): ConversationData => {
const newData = JSON.parse(JSON.stringify(data));
const { pageIndex, convIndex } = findPageForConversation(newData, { conversationId });
if (pageIndex !== -1 && convIndex !== -1) {
// Delete the conversation from its current page
newData.pages[pageIndex].conversations.splice(convIndex, 1);
}
return newData;
};

View file

@ -1,6 +1,7 @@
export * from './json';
export * from './files';
export * from './latex';
export * from './convos';
export * from './presets';
export * from './languages';
export * from './endpoints';