♾️ 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>
2024-02-03 20:25:35 -05:00
|
|
|
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);
|
|
|
|
|
|
2024-02-11 08:46:14 -05:00
|
|
|
expect(grouped).toHaveLength(1);
|
♾️ 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>
2024-02-03 20:25:35 -05:00
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|