LibreChat/client/src/data-provider/queries.ts
Yuichi Oneda e565e0faab
🔖 feat: Conversation Bookmarks (#3344)
* feat: add tags property in Conversation model

* feat: add ConversationTag model

* feat: add the tags parameter to getConvosByPage

* feat: add API route to ConversationTag

* feat: add types of ConversationTag

* feat: add data access functions for conversation tags

* feat: add Bookmark table component

* feat: Add an action to bookmark

* feat: add Bookmark nav component

* fix: failed test

* refactor: made 'Saved' tag a constant

* feat: add new bookmark to current conversation

* chore: Add comment

* fix: delete tag from conversations when it's deleted

* fix: Update the query cache when the tag title is changed.

* chore: fix typo

* refactor: add description of rebuilding bookmarks

* chore: remove unused variables

* fix: position when adding a new bookmark

* refactor: add comment, rename a function

* refactor: add a unique constraint in ConversationTag

* chore: add localizations
2024-07-29 10:45:59 -04:00

564 lines
18 KiB
TypeScript

import {
QueryKeys,
dataService,
defaultOrderQuery,
defaultAssistantsVersion,
} from 'librechat-data-provider';
import { useQuery, useInfiniteQuery, useQueryClient } from '@tanstack/react-query';
import type {
UseInfiniteQueryOptions,
QueryObserverResult,
UseQueryOptions,
UseQueryResult,
} from '@tanstack/react-query';
import type t from 'librechat-data-provider';
import type {
Action,
TPreset,
TFile,
TPlugin,
FileConfig,
ConversationListResponse,
ConversationListParams,
Assistant,
AssistantListParams,
AssistantListResponse,
AssistantDocument,
TEndpointsConfig,
TCheckUserKeyResponse,
SharedLinkListParams,
SharedLinksResponse,
} from 'librechat-data-provider';
import { findPageForConversation, addFileToCache } from '~/utils';
export const useGetFiles = <TData = TFile[] | boolean>(
config?: UseQueryOptions<TFile[], unknown, TData>,
): QueryObserverResult<TData, unknown> => {
return useQuery<TFile[], unknown, TData>([QueryKeys.files], () => dataService.getFiles(), {
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
...config,
});
};
export const useGetFileConfig = <TData = FileConfig>(
config?: UseQueryOptions<FileConfig, unknown, TData>,
): QueryObserverResult<TData, unknown> => {
return useQuery<FileConfig, unknown, TData>(
[QueryKeys.fileConfig],
() => dataService.getFileConfig(),
{
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
...config,
},
);
};
export const useGetPresetsQuery = (
config?: UseQueryOptions<TPreset[]>,
): QueryObserverResult<TPreset[], unknown> => {
return useQuery<TPreset[]>([QueryKeys.presets], () => dataService.getPresets(), {
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
...config,
});
};
export const useGetEndpointsConfigOverride = <TData = unknown | boolean>(
config?: UseQueryOptions<unknown | boolean, unknown, TData>,
): QueryObserverResult<TData> => {
return useQuery<unknown | boolean, unknown, TData>(
[QueryKeys.endpointsConfigOverride],
() => dataService.getEndpointsConfigOverride(),
{
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
...config,
},
);
};
export const useGetConvoIdQuery = (
id: string,
config?: UseQueryOptions<t.TConversation>,
): QueryObserverResult<t.TConversation> => {
const queryClient = useQueryClient();
return useQuery<t.TConversation>(
[QueryKeys.conversation, id],
() => {
const defaultQuery = () => dataService.getConversationById(id);
const convosQuery = queryClient.getQueryData<t.ConversationData>([
QueryKeys.allConversations,
]);
if (!convosQuery) {
return defaultQuery();
}
const { pageIndex, index } = findPageForConversation(convosQuery, { conversationId: id });
if (pageIndex > -1 && index > -1) {
return convosQuery.pages[pageIndex].conversations[index];
}
return defaultQuery();
},
{
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
...config,
},
);
};
export const useSearchInfiniteQuery = (
params?: ConversationListParams & { searchQuery?: string },
config?: UseInfiniteQueryOptions<ConversationListResponse, unknown>,
) => {
return useInfiniteQuery<ConversationListResponse, unknown>(
[QueryKeys.searchConversations, params], // Include the searchQuery in the query key
({ pageParam = '1' }) =>
dataService.listConversationsByQuery({ ...params, pageNumber: pageParam }),
{
getNextPageParam: (lastPage) => {
const currentPageNumber = Number(lastPage.pageNumber);
const totalPages = Number(lastPage.pages);
return currentPageNumber < totalPages ? currentPageNumber + 1 : undefined;
},
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
...config,
},
);
};
export const useConversationsInfiniteQuery = (
params?: ConversationListParams,
config?: UseInfiniteQueryOptions<ConversationListResponse, unknown>,
) => {
return useInfiniteQuery<ConversationListResponse, unknown>(
params?.isArchived ? [QueryKeys.archivedConversations] : [QueryKeys.allConversations],
({ pageParam = '' }) =>
dataService.listConversations({
...params,
pageNumber: pageParam?.toString(),
isArchived: params?.isArchived || false,
tags: params?.tags || [],
}),
{
getNextPageParam: (lastPage) => {
const currentPageNumber = Number(lastPage.pageNumber);
const totalPages = Number(lastPage.pages); // Convert totalPages to a number
// If the current page number is less than total pages, return the next page number
return currentPageNumber < totalPages ? currentPageNumber + 1 : undefined;
},
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
...config,
},
);
};
export const useSharedLinksInfiniteQuery = (
params?: SharedLinkListParams,
config?: UseInfiniteQueryOptions<SharedLinksResponse, unknown>,
) => {
return useInfiniteQuery<SharedLinksResponse, unknown>(
[QueryKeys.sharedLinks],
({ pageParam = '' }) =>
dataService.listSharedLinks({
...params,
pageNumber: pageParam?.toString(),
isPublic: params?.isPublic || true,
}),
{
getNextPageParam: (lastPage) => {
const currentPageNumber = Number(lastPage.pageNumber);
const totalPages = Number(lastPage.pages); // Convert totalPages to a number
// If the current page number is less than total pages, return the next page number
return currentPageNumber < totalPages ? currentPageNumber + 1 : undefined;
},
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
...config,
},
);
};
export const useConversationTagsQuery = (
config?: UseQueryOptions<t.TConversationTagsResponse>,
): QueryObserverResult<t.TConversationTagsResponse> => {
return useQuery<t.TConversationTag[]>(
[QueryKeys.conversationTags],
() => dataService.getConversationTags(),
{
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
...config,
},
);
};
/**
* ASSISTANTS
*/
/**
* Hook for getting all available tools for Assistants
*/
export const useAvailableToolsQuery = (
endpoint: t.AssistantsEndpoint,
): QueryObserverResult<TPlugin[]> => {
const queryClient = useQueryClient();
const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]);
const keyExpiry = queryClient.getQueryData<TCheckUserKeyResponse>([QueryKeys.name, endpoint]);
const userProvidesKey = !!endpointsConfig?.[endpoint]?.userProvide;
const keyProvided = userProvidesKey ? !!keyExpiry?.expiresAt : true;
const enabled = !!endpointsConfig?.[endpoint] && keyProvided;
const version = endpointsConfig?.[endpoint]?.version ?? defaultAssistantsVersion[endpoint];
return useQuery<TPlugin[]>(
[QueryKeys.tools],
() => dataService.getAvailableTools(version, endpoint),
{
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
enabled,
},
);
};
/**
* Hook for listing all assistants, with optional parameters provided for pagination and sorting
*/
export const useListAssistantsQuery = <TData = AssistantListResponse>(
endpoint: t.AssistantsEndpoint,
params: Omit<AssistantListParams, 'endpoint'> = defaultOrderQuery,
config?: UseQueryOptions<AssistantListResponse, unknown, TData>,
): QueryObserverResult<TData> => {
const queryClient = useQueryClient();
const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]);
const keyExpiry = queryClient.getQueryData<TCheckUserKeyResponse>([QueryKeys.name, endpoint]);
const userProvidesKey = !!endpointsConfig?.[endpoint]?.userProvide;
const keyProvided = userProvidesKey ? !!keyExpiry?.expiresAt : true;
const enabled = !!endpointsConfig?.[endpoint] && keyProvided;
const version = endpointsConfig?.[endpoint]?.version ?? defaultAssistantsVersion[endpoint];
return useQuery<AssistantListResponse, unknown, TData>(
[QueryKeys.assistants, endpoint, params],
() => dataService.listAssistants({ ...params, endpoint }, version),
{
// Example selector to sort them by created_at
// select: (res) => {
// return res.data.sort((a, b) => a.created_at - b.created_at);
// },
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
retry: false,
...config,
enabled: config?.enabled !== undefined ? config?.enabled && enabled : enabled,
},
);
};
/*
export const useListAssistantsInfiniteQuery = (
params?: AssistantListParams,
config?: UseInfiniteQueryOptions<AssistantListResponse, Error>,
) => {
const queryClient = useQueryClient();
const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]);
const keyExpiry = queryClient.getQueryData<TCheckUserKeyResponse>([
QueryKeys.name,
EModelEndpoint.assistants,
]);
const userProvidesKey = !!endpointsConfig?.[EModelEndpoint.assistants]?.userProvide;
const keyProvided = userProvidesKey ? !!keyExpiry?.expiresAt : true;
const enabled = !!endpointsConfig?.[EModelEndpoint.assistants] && keyProvided;
return useInfiniteQuery<AssistantListResponse, Error>(
['assistantsList', params],
({ pageParam = '' }) => dataService.listAssistants({ ...params, after: pageParam }),
{
getNextPageParam: (lastPage) => {
// lastPage is of type AssistantListResponse, you can use the has_more and last_id from it directly
if (lastPage.has_more) {
return lastPage.last_id;
}
return undefined;
},
...config,
enabled: config?.enabled !== undefined ? config?.enabled && enabled : enabled,
},
);
};
*/
/**
* Hook for retrieving details about a single assistant
*/
export const useGetAssistantByIdQuery = (
endpoint: t.AssistantsEndpoint,
assistant_id: string,
config?: UseQueryOptions<Assistant>,
): QueryObserverResult<Assistant> => {
const queryClient = useQueryClient();
const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]);
const keyExpiry = queryClient.getQueryData<TCheckUserKeyResponse>([QueryKeys.name, endpoint]);
const userProvidesKey = !!endpointsConfig?.[endpoint]?.userProvide;
const keyProvided = userProvidesKey ? !!keyExpiry?.expiresAt : true;
const enabled = !!endpointsConfig?.[endpoint] && keyProvided;
const version = endpointsConfig?.[endpoint]?.version ?? defaultAssistantsVersion[endpoint];
return useQuery<Assistant>(
[QueryKeys.assistant, assistant_id],
() =>
dataService.getAssistantById({
endpoint,
assistant_id,
version,
}),
{
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
retry: false,
...config,
// Query will not execute until the assistant_id exists
enabled: config?.enabled !== undefined ? config?.enabled && enabled : enabled,
},
);
};
/**
* Hook for retrieving user's saved Assistant Actions
*/
export const useGetActionsQuery = <TData = Action[]>(
endpoint: t.AssistantsEndpoint,
config?: UseQueryOptions<Action[], unknown, TData>,
): QueryObserverResult<TData> => {
const queryClient = useQueryClient();
const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]);
const keyExpiry = queryClient.getQueryData<TCheckUserKeyResponse>([QueryKeys.name, endpoint]);
const userProvidesKey = !!endpointsConfig?.[endpoint]?.userProvide;
const keyProvided = userProvidesKey ? !!keyExpiry?.expiresAt : true;
const enabled = !!endpointsConfig?.[endpoint] && keyProvided;
const version = endpointsConfig?.[endpoint]?.version ?? defaultAssistantsVersion[endpoint];
return useQuery<Action[], unknown, TData>(
[QueryKeys.actions],
() =>
dataService.getActions({
endpoint,
version,
}),
{
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
...config,
enabled: config?.enabled !== undefined ? config?.enabled && enabled : enabled,
},
);
};
/**
* Hook for retrieving user's saved Assistant Documents (metadata saved to Database)
*/
export const useGetAssistantDocsQuery = (
endpoint: t.AssistantsEndpoint,
config?: UseQueryOptions<AssistantDocument[]>,
): QueryObserverResult<AssistantDocument[], unknown> => {
const queryClient = useQueryClient();
const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]);
const keyExpiry = queryClient.getQueryData<TCheckUserKeyResponse>([QueryKeys.name, endpoint]);
const userProvidesKey = !!endpointsConfig?.[endpoint]?.userProvide;
const keyProvided = userProvidesKey ? !!keyExpiry?.expiresAt : true;
const enabled = !!endpointsConfig?.[endpoint] && keyProvided;
const version = endpointsConfig?.[endpoint]?.version ?? defaultAssistantsVersion[endpoint];
return useQuery<AssistantDocument[]>(
[QueryKeys.assistantDocs],
() =>
dataService.getAssistantDocs({
endpoint,
version,
}),
{
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
...config,
enabled: config?.enabled !== undefined ? config?.enabled && enabled : enabled,
},
);
};
export const useFileDownload = (userId?: string, file_id?: string): QueryObserverResult<string> => {
const queryClient = useQueryClient();
return useQuery(
[QueryKeys.fileDownload, file_id],
async () => {
if (!userId || !file_id) {
console.warn('No user ID provided for file download');
return;
}
const response = await dataService.getFileDownload(userId, file_id);
const blob = response.data;
const downloadURL = window.URL.createObjectURL(blob);
try {
const metadata: TFile | undefined = JSON.parse(response.headers['x-file-metadata']);
if (!metadata) {
console.warn('No metadata found for file download', response.headers);
return downloadURL;
}
addFileToCache(queryClient, metadata);
} catch (e) {
console.error('Error parsing file metadata, skipped updating file query cache', e);
}
return downloadURL;
},
{
enabled: false,
retry: false,
},
);
};
/** STT/TTS */
/* Text to speech voices */
export const useVoicesQuery = (): UseQueryResult<t.VoiceResponse> => {
return useQuery([QueryKeys.voices], () => dataService.getVoices());
};
/* Custom config speech */
export const useCustomConfigSpeechQuery = (): UseQueryResult<t.getCustomConfigSpeechResponse> => {
return useQuery([QueryKeys.customConfigSpeech], () => dataService.getCustomConfigSpeech());
};
/** Prompt */
export const usePromptGroupsInfiniteQuery = (
params?: t.TPromptGroupsWithFilterRequest,
config?: UseInfiniteQueryOptions<t.PromptGroupListResponse, unknown>,
) => {
const { name, pageSize, category, ...rest } = params || {};
return useInfiniteQuery<t.PromptGroupListResponse, unknown>(
[QueryKeys.promptGroups, name, category, pageSize],
({ pageParam = '1' }) =>
dataService.getPromptGroups({
...rest,
name,
category: category || '',
pageNumber: pageParam?.toString(),
pageSize: (pageSize || 10).toString(),
}),
{
getNextPageParam: (lastPage) => {
const currentPageNumber = Number(lastPage.pageNumber);
const totalPages = Number(lastPage.pages);
return currentPageNumber < totalPages ? currentPageNumber + 1 : undefined;
},
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
...config,
},
);
};
export const useGetPromptGroup = (
id: string,
config?: UseQueryOptions<t.TPromptGroup>,
): QueryObserverResult<t.TPromptGroup> => {
return useQuery<t.TPromptGroup>(
[QueryKeys.promptGroup, id],
() => dataService.getPromptGroup(id),
{
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
retry: false,
...config,
enabled: config?.enabled !== undefined ? config?.enabled : true,
},
);
};
export const useGetPrompts = (
filter: t.TPromptsWithFilterRequest,
config?: UseQueryOptions<t.TPrompt[]>,
): QueryObserverResult<t.TPrompt[]> => {
return useQuery<t.TPrompt[]>(
[QueryKeys.prompts, filter.groupId ?? ''],
() => dataService.getPrompts(filter),
{
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
retry: false,
...config,
enabled: config?.enabled !== undefined ? config?.enabled : true,
},
);
};
export const useGetAllPromptGroups = <TData = t.AllPromptGroupsResponse>(
filter?: t.AllPromptGroupsFilterRequest,
config?: UseQueryOptions<t.AllPromptGroupsResponse, unknown, TData>,
): QueryObserverResult<TData> => {
return useQuery<t.AllPromptGroupsResponse, unknown, TData>(
[QueryKeys.allPromptGroups],
() => dataService.getAllPromptGroups(),
{
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
retry: false,
...config,
},
);
};
export const useGetCategories = <TData = t.TGetCategoriesResponse>(
config?: UseQueryOptions<t.TGetCategoriesResponse, unknown, TData>,
): QueryObserverResult<TData> => {
return useQuery<t.TGetCategoriesResponse, unknown, TData>(
[QueryKeys.categories],
() => dataService.getCategories(),
{
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
retry: false,
...config,
enabled: config?.enabled !== undefined ? config?.enabled : true,
},
);
};
export const useGetRandomPrompts = (
filter: t.TGetRandomPromptsRequest,
config?: UseQueryOptions<t.TGetRandomPromptsResponse>,
): QueryObserverResult<t.TGetRandomPromptsResponse> => {
return useQuery<t.TGetRandomPromptsResponse>(
[QueryKeys.randomPrompts],
() => dataService.getRandomPrompts(filter),
{
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
retry: false,
...config,
enabled: config?.enabled !== undefined ? config?.enabled : true,
},
);
};