🔖 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
This commit is contained in:
Yuichi Oneda 2024-07-29 07:45:59 -07:00 committed by GitHub
parent d4d56281e3
commit e565e0faab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
65 changed files with 3751 additions and 36 deletions

View file

@ -32,8 +32,10 @@ export const abortRequest = (endpoint: string) => `/api/ask/${endpoint}/abort`;
export const conversationsRoot = '/api/convos';
export const conversations = (pageNumber: string, isArchived?: boolean) =>
`${conversationsRoot}?pageNumber=${pageNumber}${isArchived ? '&isArchived=true' : ''}`;
export const conversations = (pageNumber: string, isArchived?: boolean, tags?: string[]) =>
`${conversationsRoot}?pageNumber=${pageNumber}${isArchived ? '&isArchived=true' : ''}${tags
?.map((tag) => `&tags=${tag}`)
.join('')}`;
export const conversationById = (id: string) => `${conversationsRoot}/${id}`;
@ -188,3 +190,14 @@ export const roles = () => '/api/roles';
export const getRole = (roleName: string) => `${roles()}/${roleName.toLowerCase()}`;
export const updatePromptPermissions = (roleName: string) =>
`${roles()}/${roleName.toLowerCase()}/prompts`;
/* Conversation Tags */
export const conversationTags = (tag?: string) => `/api/tags${tag ? `/${tag}` : ''}`;
export const conversationTagsList = (pageNumber: string, sort?: string, order?: string) =>
`${conversationTags()}/list?pageNumber=${pageNumber}${sort ? `&sort=${sort}` : ''}${
order ? `&order=${order}` : ''
}`;
export const addTagToConversation = (conversationId: string) =>
`${conversationsRoot}/tags/${conversationId}`;

View file

@ -424,7 +424,8 @@ export const listConversations = (
// Assuming params has a pageNumber property
const pageNumber = params?.pageNumber || '1'; // Default to page 1 if not provided
const isArchived = params?.isArchived || false; // Default to false if not provided
return request.get(endpoints.conversations(pageNumber, isArchived));
const tags = params?.tags || []; // Default to an empty array if not provided
return request.get(endpoints.conversations(pageNumber, isArchived, tags));
};
export const listConversationsByQuery = (
@ -541,3 +542,34 @@ export function updatePromptPermissions(
): Promise<m.UpdatePromptPermResponse> {
return request.put(endpoints.updatePromptPermissions(variables.roleName), variables.updates);
}
/* Tags */
export function getConversationTags(): Promise<t.TConversationTagsResponse> {
return request.get(endpoints.conversationTags());
}
export function createConversationTag(
payload: t.TConversationTagRequest,
): Promise<t.TConversationTagResponse> {
return request.post(endpoints.conversationTags(), payload);
}
export function updateConversationTag(
tag: string,
payload: t.TConversationTagRequest,
): Promise<t.TConversationTagResponse> {
return request.put(endpoints.conversationTags(tag), payload);
}
export function deleteConversationTag(tag: string): Promise<t.TConversationTagResponse> {
return request.delete(endpoints.conversationTags(tag));
}
export function addTagToConversation(
conversationId: string,
payload: t.TTagConversationRequest,
): Promise<t.TTagConversationResponse> {
return request.put(endpoints.addTagToConversation(conversationId), payload);
}
export function rebuildConversationTags(): Promise<t.TConversationTagsResponse> {
return request.post(endpoints.conversationTags('rebuild'));
}

View file

@ -36,6 +36,7 @@ export enum QueryKeys {
categories = 'categories',
randomPrompts = 'randomPrompts',
roles = 'roles',
conversationTags = 'conversationTags',
}
export enum MutationKeys {

View file

@ -74,6 +74,21 @@ export const useGetSharedMessages = (
);
};
export const useGetConversationTags = (
config?: UseQueryOptions<t.TConversationTagsResponse>,
): QueryObserverResult<t.TConversationTagsResponse> => {
return useQuery<t.TConversationTagsResponse>(
[QueryKeys.conversationTags],
() => dataService.getConversationTags(),
{
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
...config,
},
);
};
export const useGetUserBalance = (
config?: UseQueryOptions<string>,
): QueryObserverResult<string> => {

View file

@ -371,6 +371,7 @@ export const tConversationSchema = z.object({
updatedAt: z.string(),
modelLabel: z.string().nullable().optional(),
examples: z.array(tExampleSchema).optional(),
tags: z.array(z.string()).optional(),
/* Prefer modelLabel over chatGptLabel */
chatGptLabel: z.string().nullable().optional(),
userLabel: z.string().optional(),
@ -476,6 +477,17 @@ export const tSharedLinkSchema = z.object({
});
export type TSharedLink = z.infer<typeof tSharedLinkSchema>;
export const tConversationTagSchema = z.object({
user: z.string(),
tag: z.string(),
description: z.string().optional(),
createdAt: z.string(),
updatedAt: z.string(),
count: z.number(),
position: z.number(),
});
export type TConversationTag = z.infer<typeof tConversationTagSchema>;
export const openAISchema = tConversationSchema
.pick({
model: true,

View file

@ -7,6 +7,7 @@ import type {
TSharedLink,
TConversation,
EModelEndpoint,
TConversationTag,
} from './schemas';
import type { TSpecsConfig } from './models';
export type TOpenAIMessage = OpenAI.Chat.ChatCompletionMessageParam;
@ -170,6 +171,25 @@ export type TSharedLinkResponse = TSharedLink;
export type TSharedLinksResponse = TSharedLink[];
export type TDeleteSharedLinkResponse = TSharedLink;
// type for getting conversation tags
export type TConversationTagsResponse = TConversationTag[];
// type for creating conversation tag
export type TConversationTagRequest = Partial<
Omit<TConversationTag, 'createdAt' | 'updatedAt' | 'count' | 'user'>
> & {
conversationId?: string;
addToConversation?: boolean;
};
export type TConversationTagResponse = TConversationTag;
// type for tagging conversation
export type TTagConversationRequest = {
conversationId: string;
tags: string[];
};
export type TTagConversationResponse = string[];
export type TForkConvoRequest = {
messageId: string;
conversationId: string;

View file

@ -173,3 +173,9 @@ export type UpdatePromptPermOptions = MutationOptions<
unknown,
types.TError
>;
export type UpdateConversationTagOptions = MutationOptions<
types.TConversationTag,
types.TConversationTagRequest
>;
export type DeleteConversationTagOptions = MutationOptions<types.TConversationTag, string>;

View file

@ -1,6 +1,7 @@
import type { InfiniteData } from '@tanstack/react-query';
import type { TMessage, TConversation, TSharedLink } from '../schemas';
import type * as t from '../types';
import type { TMessage, TConversation, TSharedLink, TConversationTag } from '../schemas';
export type Conversation = {
id: string;
createdAt: number;
@ -18,6 +19,7 @@ export type ConversationListParams = {
pageNumber: string; // Add this line
conversationId?: string;
isArchived?: boolean;
tags?: string[];
};
// Type for the response from the conversation list API
@ -68,3 +70,5 @@ export type AllPromptGroupsFilterRequest = {
};
export type AllPromptGroupsResponse = t.TPromptGroup[];
export type ConversationTagsResponse = TConversationTag[];