| | import { |
| | format, |
| | isToday, |
| | subDays, |
| | getYear, |
| | parseISO, |
| | startOfDay, |
| | startOfYear, |
| | isWithinInterval, |
| | } from 'date-fns'; |
| | import { QueryClient } from '@tanstack/react-query'; |
| | import { EModelEndpoint, LocalStorageKeys, QueryKeys } from 'librechat-data-provider'; |
| | import type { TConversation, GroupedConversations } from 'librechat-data-provider'; |
| | import type { InfiniteData } from '@tanstack/react-query'; |
| |
|
| | |
| | export const dateKeys = { |
| | today: 'com_ui_date_today', |
| | yesterday: 'com_ui_date_yesterday', |
| | previous7Days: 'com_ui_date_previous_7_days', |
| | previous30Days: 'com_ui_date_previous_30_days', |
| | january: 'com_ui_date_january', |
| | february: 'com_ui_date_february', |
| | march: 'com_ui_date_march', |
| | april: 'com_ui_date_april', |
| | may: 'com_ui_date_may', |
| | june: 'com_ui_date_june', |
| | july: 'com_ui_date_july', |
| | august: 'com_ui_date_august', |
| | september: 'com_ui_date_september', |
| | october: 'com_ui_date_october', |
| | november: 'com_ui_date_november', |
| | december: 'com_ui_date_december', |
| | }; |
| |
|
| | const getGroupName = (date: Date) => { |
| | const now = new Date(Date.now()); |
| | if (isToday(date)) { |
| | return dateKeys.today; |
| | } |
| | if (isWithinInterval(date, { start: startOfDay(subDays(now, 1)), end: now })) { |
| | return dateKeys.yesterday; |
| | } |
| | if (isWithinInterval(date, { start: subDays(now, 7), end: now })) { |
| | return dateKeys.previous7Days; |
| | } |
| | if (isWithinInterval(date, { start: subDays(now, 30), end: now })) { |
| | return dateKeys.previous30Days; |
| | } |
| | if (isWithinInterval(date, { start: startOfYear(now), end: now })) { |
| | const month = format(date, 'MMMM').toLowerCase(); |
| | return dateKeys[month]; |
| | } |
| | 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(([k, v]) => [v, k])); |
| | const dateGroupsSet = new Set([ |
| | dateKeys.today, |
| | dateKeys.yesterday, |
| | dateKeys.previous7Days, |
| | dateKeys.previous30Days, |
| | ]); |
| |
|
| | export const groupConversationsByDate = ( |
| | conversations: Array<TConversation | null>, |
| | ): GroupedConversations => { |
| | if (!Array.isArray(conversations)) { |
| | return []; |
| | } |
| | const seenConversationIds = new Set(); |
| | const groups = new Map(); |
| | const now = new Date(Date.now()); |
| |
|
| | conversations.forEach((conversation) => { |
| | if (!conversation || seenConversationIds.has(conversation.conversationId)) { |
| | return; |
| | } |
| | seenConversationIds.add(conversation.conversationId); |
| |
|
| | let date: Date; |
| | if (conversation.updatedAt) { |
| | date = parseISO(conversation.updatedAt); |
| | } else { |
| | date = now; |
| | } |
| | const groupName = getGroupName(date); |
| | if (!groups.has(groupName)) { |
| | groups.set(groupName, []); |
| | } |
| | groups.get(groupName).push(conversation); |
| | }); |
| |
|
| | const sortedGroups = new Map(); |
| | dateGroupsSet.forEach((group) => { |
| | if (groups.has(group)) { |
| | sortedGroups.set(group, groups.get(group)); |
| | } |
| | }); |
| |
|
| | 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, |
| | aOrder = monthOrderMap.get(monthA) ?? -1; |
| | return bOrder - aOrder; |
| | }); |
| | yearMonthGroups.forEach((group) => { |
| | sortedGroups.set(group, groups.get(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 type ConversationCursorData = { |
| | conversations: TConversation[]; |
| | nextCursor?: string | null; |
| | }; |
| |
|
| | |
| |
|
| | export function findConversationInInfinite( |
| | data: InfiniteData<ConversationCursorData> | undefined, |
| | conversationId: string, |
| | ): TConversation | undefined { |
| | if (!data) { |
| | return undefined; |
| | } |
| | for (const page of data.pages) { |
| | const found = page.conversations.find((c) => c.conversationId === conversationId); |
| | if (found) { |
| | return found; |
| | } |
| | } |
| | return undefined; |
| | } |
| |
|
| | export function updateInfiniteConvoPage( |
| | data: InfiniteData<ConversationCursorData> | undefined, |
| | conversationId: string, |
| | updater: (c: TConversation) => TConversation, |
| | ): InfiniteData<ConversationCursorData> | undefined { |
| | if (!data) { |
| | return data; |
| | } |
| | return { |
| | ...data, |
| | pages: data.pages.map((page) => ({ |
| | ...page, |
| | conversations: page.conversations.map((c) => |
| | c.conversationId === conversationId ? updater(c) : c, |
| | ), |
| | })), |
| | }; |
| | } |
| |
|
| | export function addConversationToInfinitePages( |
| | data: InfiniteData<ConversationCursorData> | undefined, |
| | newConversation: TConversation, |
| | ): InfiniteData<ConversationCursorData> { |
| | if (!data) { |
| | return { |
| | pageParams: [undefined], |
| | pages: [{ conversations: [newConversation], nextCursor: null }], |
| | }; |
| | } |
| | return { |
| | ...data, |
| | pages: [ |
| | { ...data.pages[0], conversations: [newConversation, ...data.pages[0].conversations] }, |
| | ...data.pages.slice(1), |
| | ], |
| | }; |
| | } |
| |
|
| | export function addConversationToAllConversationsQueries( |
| | queryClient: QueryClient, |
| | newConversation: TConversation, |
| | ) { |
| | |
| | const queries = queryClient |
| | .getQueryCache() |
| | .findAll([QueryKeys.allConversations], { exact: false }); |
| |
|
| | for (const query of queries) { |
| | queryClient.setQueryData<InfiniteData<ConversationCursorData>>(query.queryKey, (old) => { |
| | if ( |
| | !old || |
| | old.pages[0].conversations.some((c) => c.conversationId === newConversation.conversationId) |
| | ) { |
| | return old; |
| | } |
| | return { |
| | ...old, |
| | pages: [ |
| | { |
| | ...old.pages[0], |
| | conversations: [newConversation, ...old.pages[0].conversations], |
| | }, |
| | ...old.pages.slice(1), |
| | ], |
| | }; |
| | }); |
| | } |
| | } |
| |
|
| | export function removeConvoFromInfinitePages( |
| | data: InfiniteData<ConversationCursorData> | undefined, |
| | conversationId: string, |
| | ): InfiniteData<ConversationCursorData> | undefined { |
| | if (!data) { |
| | return data; |
| | } |
| | return { |
| | ...data, |
| | pages: data.pages |
| | .map((page) => ({ |
| | ...page, |
| | conversations: page.conversations.filter((c) => c.conversationId !== conversationId), |
| | })) |
| | .filter((page) => page.conversations.length > 0), |
| | }; |
| | } |
| |
|
| | |
| | export function updateConvoFieldsInfinite( |
| | data: InfiniteData<ConversationCursorData> | undefined, |
| | updatedConversation: Partial<TConversation> & { conversationId: string }, |
| | keepPosition = false, |
| | ): InfiniteData<ConversationCursorData> | undefined { |
| | if (!data) { |
| | return data; |
| | } |
| | let found: TConversation | undefined; |
| | let pageIdx = -1, |
| | convoIdx = -1; |
| | for (let i = 0; i < data.pages.length; ++i) { |
| | const idx = data.pages[i].conversations.findIndex( |
| | (c) => c.conversationId === updatedConversation.conversationId, |
| | ); |
| | if (idx !== -1) { |
| | pageIdx = i; |
| | convoIdx = idx; |
| | found = data.pages[i].conversations[idx]; |
| | break; |
| | } |
| | } |
| | if (!found) { |
| | return data; |
| | } |
| |
|
| | if (keepPosition) { |
| | return { |
| | ...data, |
| | pages: data.pages.map((page, pi) => |
| | pi === pageIdx |
| | ? { |
| | ...page, |
| | conversations: page.conversations.map((c, ci) => |
| | ci === convoIdx ? { ...c, ...updatedConversation } : c, |
| | ), |
| | } |
| | : page, |
| | ), |
| | }; |
| | } else { |
| | const patched = { ...found, ...updatedConversation, updatedAt: new Date().toISOString() }; |
| | const pages = data.pages.map((page) => ({ |
| | ...page, |
| | conversations: page.conversations.filter((c) => c.conversationId !== patched.conversationId), |
| | })); |
| |
|
| | pages[0].conversations = [patched, ...pages[0].conversations]; |
| |
|
| | const finalPages = pages.filter((page) => page.conversations.length > 0); |
| | return { ...data, pages: finalPages }; |
| | } |
| | } |
| |
|
| | export function storeEndpointSettings(conversation: TConversation | null) { |
| | if (!conversation) { |
| | return; |
| | } |
| | const { endpoint, model, agentOptions } = conversation; |
| | if (!endpoint) { |
| | return; |
| | } |
| | const lastModel = JSON.parse(localStorage.getItem(LocalStorageKeys.LAST_MODEL) ?? '{}'); |
| | lastModel[endpoint] = model; |
| | if (endpoint === EModelEndpoint.gptPlugins) { |
| | lastModel.secondaryModel = agentOptions?.model ?? model ?? ''; |
| | } |
| | localStorage.setItem(LocalStorageKeys.LAST_MODEL, JSON.stringify(lastModel)); |
| | } |
| |
|
| | |
| | export function addConvoToAllQueries(queryClient: QueryClient, newConvo: TConversation) { |
| | const queries = queryClient |
| | .getQueryCache() |
| | .findAll([QueryKeys.allConversations], { exact: false }); |
| |
|
| | for (const query of queries) { |
| | queryClient.setQueryData<InfiniteData<ConversationCursorData>>(query.queryKey, (oldData) => { |
| | if (!oldData) { |
| | return oldData; |
| | } |
| | if ( |
| | oldData.pages.some((p) => |
| | p.conversations.some((c) => c.conversationId === newConvo.conversationId), |
| | ) |
| | ) { |
| | return oldData; |
| | } |
| | return { |
| | ...oldData, |
| | pages: [ |
| | { |
| | ...oldData.pages[0], |
| | conversations: [newConvo, ...oldData.pages[0].conversations], |
| | }, |
| | ...oldData.pages.slice(1), |
| | ], |
| | }; |
| | }); |
| | } |
| | } |
| |
|
| | |
| | export function updateConvoInAllQueries( |
| | queryClient: QueryClient, |
| | conversationId: string, |
| | updater: (c: TConversation) => TConversation, |
| | ) { |
| | const queries = queryClient |
| | .getQueryCache() |
| | .findAll([QueryKeys.allConversations], { exact: false }); |
| |
|
| | for (const query of queries) { |
| | queryClient.setQueryData<InfiniteData<ConversationCursorData>>(query.queryKey, (oldData) => { |
| | if (!oldData) { |
| | return oldData; |
| | } |
| | return { |
| | ...oldData, |
| | pages: oldData.pages.map((page) => ({ |
| | ...page, |
| | conversations: page.conversations.map((c) => |
| | c.conversationId === conversationId ? updater(c) : c, |
| | ), |
| | })), |
| | }; |
| | }); |
| | } |
| | } |
| |
|
| | |
| | export function removeConvoFromAllQueries(queryClient: QueryClient, conversationId: string) { |
| | const queries = queryClient |
| | .getQueryCache() |
| | .findAll([QueryKeys.allConversations], { exact: false }); |
| |
|
| | for (const query of queries) { |
| | queryClient.setQueryData<InfiniteData<ConversationCursorData>>(query.queryKey, (oldData) => { |
| | if (!oldData) { |
| | return oldData; |
| | } |
| | return { |
| | ...oldData, |
| | pages: oldData.pages |
| | .map((page) => ({ |
| | ...page, |
| | conversations: page.conversations.filter((c) => c.conversationId !== conversationId), |
| | })) |
| | .filter((page) => page.conversations.length > 0), |
| | }; |
| | }); |
| | } |
| | } |
| |
|