2025-05-30 22:18:13 -04:00
|
|
|
const { logger } = require('@librechat/data-schemas');
|
2025-06-25 17:16:26 -04:00
|
|
|
const { createTempChatExpirationDate } = require('@librechat/api');
|
2023-02-07 16:22:35 -05:00
|
|
|
const { getMessages, deleteMessages } = require('./Message');
|
2025-05-30 22:18:13 -04:00
|
|
|
const { Conversation } = require('~/db/models');
|
2023-02-06 14:05:02 -05:00
|
|
|
|
2024-08-09 15:17:13 -04:00
|
|
|
/**
|
|
|
|
* Searches for a conversation by conversationId and returns a lean document with only conversationId and user.
|
|
|
|
* @param {string} conversationId - The conversation's ID.
|
|
|
|
* @returns {Promise<{conversationId: string, user: string} | null>} The conversation object with selected fields or null if not found.
|
|
|
|
*/
|
|
|
|
const searchConversation = async (conversationId) => {
|
|
|
|
try {
|
|
|
|
return await Conversation.findOne({ conversationId }, 'conversationId user').lean();
|
|
|
|
} catch (error) {
|
|
|
|
logger.error('[searchConversation] Error searching conversation', error);
|
|
|
|
throw new Error('Error searching conversation');
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
🌿 feat: Fork Messages/Conversations (#2617)
* typedef for ImportBatchBuilder
* feat: first pass, fork conversations
* feat: fork - getMessagesUpToTargetLevel
* fix: additional tests and fix getAllMessagesUpToParent
* chore: arrow function return
* refactor: fork 3 options
* chore: remove unused genbuttons
* chore: remove unused hover buttons code
* feat: fork first pass
* wip: fork remember setting
* style: user icon
* chore: move clear chats to data tab
* WIP: fork UI options
* feat: data-provider fork types/services/vars and use generic MutationOptions
* refactor: use single param for fork option, use enum, fix mongo errors, use Date.now(), add records flag for testing, use endpoint from original convo and messages, pass originalConvo to finishConversation
* feat: add fork mutation hook and consolidate type imports
* refactor: use enum
* feat: first pass, fork mutation
* chore: add enum for target level fork option
* chore: add enum for target level fork option
* show toast when checking remember selection
* feat: splitAtTarget
* feat: split at target option
* feat: navigate to new fork, show toasts, set result query data
* feat: hover info for all fork options
* refactor: add Messages settings tab
* fix(Fork): remember text info
* ci: test for single message and is target edge case
* feat: additional tests for getAllMessagesUpToParent
* ci: additional tests and cycle detection for getMessagesUpToTargetLevel
* feat: circular dependency checks for getAllMessagesUpToParent
* fix: getMessagesUpToTargetLevel circular dep. check
* ci: more tests for getMessagesForConversation
* style: hover text for checkbox fork items
* refactor: add statefulness to conversation import
2024-05-05 11:48:20 -04:00
|
|
|
/**
|
|
|
|
* Retrieves a single conversation for a given user and conversation ID.
|
|
|
|
* @param {string} user - The user's ID.
|
|
|
|
* @param {string} conversationId - The conversation's ID.
|
|
|
|
* @returns {Promise<TConversation>} The conversation object.
|
|
|
|
*/
|
2023-03-14 01:24:43 +08:00
|
|
|
const getConvo = async (user, conversationId) => {
|
2023-03-08 22:30:29 -05:00
|
|
|
try {
|
2023-07-25 19:27:55 -04:00
|
|
|
return await Conversation.findOne({ user, conversationId }).lean();
|
2023-03-08 22:30:29 -05:00
|
|
|
} catch (error) {
|
2023-12-14 07:49:27 -05:00
|
|
|
logger.error('[getConvo] Error getting single conversation', error);
|
2025-09-15 22:51:34 +02:00
|
|
|
throw new Error('Error getting single conversation');
|
2023-03-08 22:30:29 -05:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2024-09-22 17:21:50 -04:00
|
|
|
const deleteNullOrEmptyConversations = async () => {
|
|
|
|
try {
|
|
|
|
const filter = {
|
|
|
|
$or: [
|
|
|
|
{ conversationId: null },
|
|
|
|
{ conversationId: '' },
|
|
|
|
{ conversationId: { $exists: false } },
|
|
|
|
],
|
|
|
|
};
|
|
|
|
|
|
|
|
const result = await Conversation.deleteMany(filter);
|
|
|
|
|
|
|
|
// Delete associated messages
|
|
|
|
const messageDeleteResult = await deleteMessages(filter);
|
|
|
|
|
|
|
|
logger.info(
|
|
|
|
`[deleteNullOrEmptyConversations] Deleted ${result.deletedCount} conversations and ${messageDeleteResult.deletedCount} messages`,
|
|
|
|
);
|
|
|
|
|
|
|
|
return {
|
|
|
|
conversations: result,
|
|
|
|
messages: messageDeleteResult,
|
|
|
|
};
|
|
|
|
} catch (error) {
|
|
|
|
logger.error('[deleteNullOrEmptyConversations] Error deleting conversations', error);
|
|
|
|
throw new Error('Error deleting conversations with null or empty conversationId');
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2025-03-10 17:23:46 -04:00
|
|
|
/**
|
2025-03-19 03:27:20 -04:00
|
|
|
* Searches for a conversation by conversationId and returns associated file ids.
|
|
|
|
* @param {string} conversationId - The conversation's ID.
|
|
|
|
* @returns {Promise<string[] | null>}
|
2025-03-10 17:23:46 -04:00
|
|
|
*/
|
2025-03-19 03:27:20 -04:00
|
|
|
const getConvoFiles = async (conversationId) => {
|
2025-03-10 17:23:46 -04:00
|
|
|
try {
|
2025-03-19 03:27:20 -04:00
|
|
|
return (await Conversation.findOne({ conversationId }, 'files').lean())?.files ?? [];
|
2025-03-10 17:23:46 -04:00
|
|
|
} catch (error) {
|
2025-03-19 03:27:20 -04:00
|
|
|
logger.error('[getConvoFiles] Error getting conversation files', error);
|
|
|
|
throw new Error('Error getting conversation files');
|
2025-03-10 17:23:46 -04:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-02-06 14:05:02 -05:00
|
|
|
module.exports = {
|
2025-03-19 03:27:20 -04:00
|
|
|
getConvoFiles,
|
2024-08-09 15:17:13 -04:00
|
|
|
searchConversation,
|
2024-09-22 17:21:50 -04:00
|
|
|
deleteNullOrEmptyConversations,
|
2024-07-20 01:51:59 -04:00
|
|
|
/**
|
|
|
|
* Saves a conversation to the database.
|
|
|
|
* @param {Object} req - The request object.
|
|
|
|
* @param {string} conversationId - The conversation's ID.
|
|
|
|
* @param {Object} metadata - Additional metadata to log for operation.
|
|
|
|
* @returns {Promise<TConversation>} The conversation object.
|
|
|
|
*/
|
|
|
|
saveConvo: async (req, { conversationId, newConversationId, ...convo }, metadata) => {
|
2023-02-08 00:02:29 -05:00
|
|
|
try {
|
2025-04-15 10:04:00 +02:00
|
|
|
if (metadata?.context) {
|
2024-07-21 13:54:47 -04:00
|
|
|
logger.debug(`[saveConvo] ${metadata.context}`);
|
2024-07-20 01:51:59 -04:00
|
|
|
}
|
2025-04-15 10:04:00 +02:00
|
|
|
|
2024-05-29 09:15:05 -04:00
|
|
|
const messages = await getMessages({ conversationId }, '_id');
|
2024-07-20 01:51:59 -04:00
|
|
|
const update = { ...convo, messages, user: req.user.id };
|
2025-04-15 10:04:00 +02:00
|
|
|
|
2023-03-15 00:54:50 +08:00
|
|
|
if (newConversationId) {
|
|
|
|
update.conversationId = newConversationId;
|
|
|
|
}
|
2023-02-06 14:05:02 -05:00
|
|
|
|
2025-06-25 17:16:26 -04:00
|
|
|
if (req?.body?.isTemporary) {
|
|
|
|
try {
|
2025-08-26 12:10:18 -04:00
|
|
|
const appConfig = req.config;
|
|
|
|
update.expiredAt = createTempChatExpirationDate(appConfig?.interfaceConfig);
|
2025-06-25 17:16:26 -04:00
|
|
|
} catch (err) {
|
|
|
|
logger.error('Error creating temporary chat expiration date:', err);
|
|
|
|
logger.info(`---\`saveConvo\` context: ${metadata?.context}`);
|
|
|
|
update.expiredAt = null;
|
|
|
|
}
|
2025-02-06 08:11:47 -08:00
|
|
|
} else {
|
|
|
|
update.expiredAt = null;
|
|
|
|
}
|
|
|
|
|
2025-02-26 15:02:03 -05:00
|
|
|
/** @type {{ $set: Partial<TConversation>; $unset?: Record<keyof TConversation, number> }} */
|
|
|
|
const updateOperation = { $set: update };
|
|
|
|
if (metadata && metadata.unsetFields && Object.keys(metadata.unsetFields).length > 0) {
|
|
|
|
updateOperation.$unset = metadata.unsetFields;
|
|
|
|
}
|
|
|
|
|
🎉 feat: Code Interpreter API and Agents Release (#4860)
* feat: Code Interpreter API & File Search Agent Uploads
chore: add back code files
wip: first pass, abstract key dialog
refactor: influence checkbox on key changes
refactor: update localization keys for 'execute code' to 'run code'
wip: run code button
refactor: add throwError parameter to loadAuthValues and getUserPluginAuthValue functions
feat: first pass, API tool calling
fix: handle missing toolId in callTool function and return 404 for non-existent tools
feat: show code outputs
fix: improve error handling in callTool function and log errors
fix: handle potential null value for filepath in attachment destructuring
fix: normalize language before rendering and prevent null return
fix: add loading indicator in RunCode component while executing code
feat: add support for conditional code execution in Markdown components
feat: attachments
refactor: remove bash
fix: pass abort signal to graph/run
refactor: debounce and rate limit tool call
refactor: increase debounce delay for execute function
feat: set code output attachments
feat: image attachments
refactor: apply message context
refactor: pass `partIndex`
feat: toolCall schema/model/methods
feat: block indexing
feat: get tool calls
chore: imports
chore: typing
chore: condense type imports
feat: get tool calls
fix: block indexing
chore: typing
refactor: update tool calls mapping to support multiple results
fix: add unique key to nav link for rendering
wip: first pass, tool call results
refactor: update query cache from successful tool call mutation
style: improve result switcher styling
chore: note on using \`.toObject()\`
feat: add agent_id field to conversation schema
chore: typing
refactor: rename agentMap to agentsMap for consistency
feat: Agent Name as chat input placeholder
chore: bump agents
📦 chore: update @langchain dependencies to latest versions to match agents package
📦 chore: update @librechat/agents dependency to version 1.8.0
fix: Aborting agent stream removes sender; fix(bedrock): completion removes preset name label
refactor: remove direct file parameter to use req.file, add `processAgentFileUpload` for image uploads
feat: upload menu
feat: prime message_file resources
feat: implement conversation access validation in chat route
refactor: remove file parameter from processFileUpload and use req.file instead
feat: add savedMessageIds set to track saved message IDs in BaseClient, to prevent unnecessary double-write to db
feat: prevent duplicate message saves by checking savedMessageIds in AgentController
refactor: skip legacy RAG API handling for agents
feat: add files field to convoSchema
refactor: update request type annotations from Express.Request to ServerRequest in file processing functions
feat: track conversation files
fix: resendFiles, addPreviousAttachments handling
feat: add ID validation for session_id and file_id in download route
feat: entity_id for code file uploads/downloads
fix: code file edge cases
feat: delete related tool calls
feat: add stream rate handling for LLM configuration
feat: enhance system content with attached file information
fix: improve error logging in resource priming function
* WIP: PoC, sequential agents
WIP: PoC Sequential Agents, first pass content data + bump agents package
fix: package-lock
WIP: PoC, o1 support, refactor bufferString
feat: convertJsonSchemaToZod
fix: form issues and schema defining erroneous model
fix: max length issue on agent form instructions, limit conversation messages to sequential agents
feat: add abort signal support to createRun function and AgentClient
feat: PoC, hide prior sequential agent steps
fix: update parameter naming from config to metadata in event handlers for clarity, add model to usage data
refactor: use only last contentData, track model for usage data
chore: bump agents package
fix: content parts issue
refactor: filter contentParts to include tool calls and relevant indices
feat: show function calls
refactor: filter context messages to exclude tool calls when no tools are available to the agent
fix: ensure tool call content is not undefined in formatMessages
feat: add agent_id field to conversationPreset schema
feat: hide sequential agents
feat: increase upload toast duration to 10 seconds
* refactor: tool context handling & update Code API Key Dialog
feat: toolContextMap
chore: skipSpecs -> useSpecs
ci: fix handleTools tests
feat: API Key Dialog
* feat: Agent Permissions Admin Controls
feat: replace label with button for prompt permission toggle
feat: update agent permissions
feat: enable experimental agents and streamline capability configuration
feat: implement access control for agents and enhance endpoint menu items
feat: add welcome message for agent selection in localization
feat: add agents permission to access control and update version to 0.7.57
* fix: update types in useAssistantListMap and useMentions hooks for better null handling
* feat: mention agents
* fix: agent tool resource race conditions when deleting agent tool resource files
* feat: add error handling for code execution with user feedback
* refactor: rename AdminControls to AdminSettings for clarity
* style: add gap to button in AdminSettings for improved layout
* refactor: separate agent query hooks and check access to enable fetching
* fix: remove unused provider from agent initialization options, creates issue with custom endpoints
* refactor: remove redundant/deprecated modelOptions from AgentClient processes
* chore: update @librechat/agents to version 1.8.5 in package.json and package-lock.json
* fix: minor styling issues + agent panel uniformity
* fix: agent edge cases when set endpoint is no longer defined
* refactor: remove unused cleanup function call from AppService
* fix: update link in ApiKeyDialog to point to pricing page
* fix: improve type handling and layout calculations in SidePanel component
* fix: add missing localization string for agent selection in SidePanel
* chore: form styling and localizations for upload filesearch/code interpreter
* fix: model selection placeholder logic in AgentConfig component
* style: agent capabilities
* fix: add localization for provider selection and improve dropdown styling in ModelPanel
* refactor: use gpt-4o-mini > gpt-3.5-turbo
* fix: agents configuration for loadDefaultInterface and update related tests
* feat: DALLE Agents support
2024-12-04 15:48:13 -05:00
|
|
|
/** Note: the resulting Model object is necessary for Meilisearch operations */
|
2024-07-20 01:51:59 -04:00
|
|
|
const conversation = await Conversation.findOneAndUpdate(
|
|
|
|
{ conversationId, user: req.user.id },
|
2025-02-26 15:02:03 -05:00
|
|
|
updateOperation,
|
2024-07-20 01:51:59 -04:00
|
|
|
{
|
|
|
|
new: true,
|
|
|
|
upsert: true,
|
|
|
|
},
|
|
|
|
);
|
2024-06-28 21:57:53 -04:00
|
|
|
|
|
|
|
return conversation.toObject();
|
2023-02-11 10:22:15 -05:00
|
|
|
} catch (error) {
|
2023-12-14 07:49:27 -05:00
|
|
|
logger.error('[saveConvo] Error saving conversation', error);
|
2024-07-21 13:54:47 -04:00
|
|
|
if (metadata && metadata?.context) {
|
|
|
|
logger.info(`[saveConvo] ${metadata.context}`);
|
|
|
|
}
|
2023-04-11 03:26:38 +08:00
|
|
|
return { message: 'Error saving conversation' };
|
2023-02-11 10:22:15 -05:00
|
|
|
}
|
|
|
|
},
|
2024-05-02 08:48:26 +02:00
|
|
|
bulkSaveConvos: async (conversations) => {
|
|
|
|
try {
|
|
|
|
const bulkOps = conversations.map((convo) => ({
|
|
|
|
updateOne: {
|
|
|
|
filter: { conversationId: convo.conversationId, user: convo.user },
|
|
|
|
update: convo,
|
|
|
|
upsert: true,
|
|
|
|
timestamps: false,
|
|
|
|
},
|
|
|
|
}));
|
|
|
|
|
|
|
|
const result = await Conversation.bulkWrite(bulkOps);
|
|
|
|
return result;
|
|
|
|
} catch (error) {
|
2025-09-15 22:51:34 +02:00
|
|
|
logger.error('[bulkSaveConvos] Error saving conversations in bulk', error);
|
2024-05-02 08:48:26 +02:00
|
|
|
throw new Error('Failed to save conversations in bulk.');
|
|
|
|
}
|
|
|
|
},
|
2025-04-15 10:04:00 +02:00
|
|
|
getConvosByCursor: async (
|
|
|
|
user,
|
2025-09-15 22:51:34 +02:00
|
|
|
{
|
|
|
|
cursor,
|
|
|
|
limit = 25,
|
|
|
|
isArchived = false,
|
|
|
|
tags,
|
|
|
|
search,
|
|
|
|
sortBy = 'createdAt',
|
|
|
|
sortDirection = 'desc',
|
|
|
|
} = {},
|
2025-04-15 10:04:00 +02:00
|
|
|
) => {
|
|
|
|
const filters = [{ user }];
|
🚀feat: Archive conversations (#2590)
* 🔧chore: add internationalization labels for archive feature
* ✨ feat: Add function to useArchiveConversationMutation()
This commit adds a new mutation function `useArchiveConversationMutation()` for archiving conversations. This function takes the ID string of the conversation to be archived and returns a mutation result object. Upon successful archiving, it removes and refreshes the conversation from the query data cache.
While ChatGPT PATCHes the archived status by sending `{is_archived: true}` to the URL `/backend-api/conversation/$conversation_id`, this implementation uses the `dataService.updateConversation(payload)` with a POST method, aligning with the existing code conventions.
* ✨ feat(api): add is_archived field to Conversation schema and update getConvosByPage method
This commit adds a new field `is_archived` with a default value of false to the Conversation schema. It also modifies the `getConvosByPage` method within the Conversation API to adjust the query to only target conversations where `is_archived` is set to false or where the `is_archived` field does not exist. The function `getConvosQueried`, which returns conversations for a specified Conversation ID, was determined not to require consideration of whether `is_archived` is true or false, and thus was not modified.
* ♻️ refactor: add className prop to DotsIcon component
To enhance the versatility of the DotsIcon component, this commit introduces the ability to specify a className prop, allowing for greater customization.
* ✨ feat(ui): add Edit Button to group Title change and Conversation delete buttons
Added a new Edit Button to the conversations, similar to the ChatGPT UI, which groups options for editing the conversation title and deleting conversations. This grouping is accessible through a dialogue that appears when the three-dot icon is clicked.
* ♻️ refactor(ui): enhance Delete Button to accept className and label options
Enhanced the Delete Button component to accept a `className` for customization and an optional `appendLabel`. The DeleteButton component is used by both `Convo.tsx` and `Conversation.tsx`, but currently only `Convo.tsx` is active and `Conversation.tsx `is apparently not used; removing `Conversation.tsx` may eliminate the need for the `appendLabel` property in the future.
* ♻️ refactor(ui): enhance RenameButton to accept label options
Added the ability to optionally display labels; the Rename Button component is used by both `Convo.tsx` and `Conversation.tsx`, but currently only `Convo.tsx` is active and `Conversation.tsx `is apparently not used; removing `Conversation.tsx` may eliminate the need for the `appendLabel` property in the future.
* 🔧 chors: additional localization labels
* ♻️ refactor: change is_archived property of conversation to camelCase
* Refactor the is_archived property of conversation to camelCase (isArchived) to adhere to the existing code conventions
* Modify the function that retrieves conversations to accept the isArchived parameter
* ♻️ refactor: add archiveConversation mutation
I thought I could divert dataService.updateConversation, but added a new archiveConversation because the request types are different. It might be better to make them common, but to avoid side effects, I added a new function this time.
Added process to deleteConversationMutation to delete archived conversations
* ✨ feat: Add the function to hide a cancel button in DialogTemplate component
The Cancel button is not needed when displaying the archive list, so I made the Cancel button optional.
* ♻️ refactor: Add support for filtering archived conversations in Nav component
This commit modifies the Nav component to add the ability to filter out archived conversations when fetching data. This is done by adding `isArchived: false` to the query parameters for both the `useConversationsInfiniteQuery()` and `useSearchInfiniteQuery()` hooks, effectively excluding any archived conversations from the results returned.
* ♻️ refactor: add Tooltip to DeleteButton
* Add Tooltip to DeleteButton component
* Display Tooltip when DeleteButton only shows an Icon without text
* ✨ feat(ui): add ArchiveButton component for archiving conversations
To be compatible with the ChatGPT UI, no confirmation dialog is displayed when ArchiveButton is clicked. The basic behavior conforms to DeleteButton and RenameButton.
* ✨ feat(ui): add Archive button to list of conversations
Modify the Nav of the conversation list to include a dropdown that contains the Rename and Delete options, similar to the ChatGPT UI. Additionally, an Archive button has been added adjacent to the dropdown menu.
* ✨ feat: Add ArchivedChatsTable component
Adds the `ArchivedChatsTable` component, which displays a table of archived chats. It has been implemented to be as compatible with the ChatGPT UI as possible.
* 🚑 fix(tooltip): increase z-index to ensure visibility over Dialog
Resolve an issue where tooltips were not visible when displayed over a Dialog. The z-index of `DialogPrimitive.Portal` in `Dialog.tsx` is set to 999. Since the rationale for this value is unclear, the z-index of the tooltip has been increased to 1000 to guarantee its visibility above the Dialog component.
* 🔧 chors: add internationalization labels
2024-05-06 20:07:00 -07:00
|
|
|
if (isArchived) {
|
2025-04-15 10:04:00 +02:00
|
|
|
filters.push({ isArchived: true });
|
🚀feat: Archive conversations (#2590)
* 🔧chore: add internationalization labels for archive feature
* ✨ feat: Add function to useArchiveConversationMutation()
This commit adds a new mutation function `useArchiveConversationMutation()` for archiving conversations. This function takes the ID string of the conversation to be archived and returns a mutation result object. Upon successful archiving, it removes and refreshes the conversation from the query data cache.
While ChatGPT PATCHes the archived status by sending `{is_archived: true}` to the URL `/backend-api/conversation/$conversation_id`, this implementation uses the `dataService.updateConversation(payload)` with a POST method, aligning with the existing code conventions.
* ✨ feat(api): add is_archived field to Conversation schema and update getConvosByPage method
This commit adds a new field `is_archived` with a default value of false to the Conversation schema. It also modifies the `getConvosByPage` method within the Conversation API to adjust the query to only target conversations where `is_archived` is set to false or where the `is_archived` field does not exist. The function `getConvosQueried`, which returns conversations for a specified Conversation ID, was determined not to require consideration of whether `is_archived` is true or false, and thus was not modified.
* ♻️ refactor: add className prop to DotsIcon component
To enhance the versatility of the DotsIcon component, this commit introduces the ability to specify a className prop, allowing for greater customization.
* ✨ feat(ui): add Edit Button to group Title change and Conversation delete buttons
Added a new Edit Button to the conversations, similar to the ChatGPT UI, which groups options for editing the conversation title and deleting conversations. This grouping is accessible through a dialogue that appears when the three-dot icon is clicked.
* ♻️ refactor(ui): enhance Delete Button to accept className and label options
Enhanced the Delete Button component to accept a `className` for customization and an optional `appendLabel`. The DeleteButton component is used by both `Convo.tsx` and `Conversation.tsx`, but currently only `Convo.tsx` is active and `Conversation.tsx `is apparently not used; removing `Conversation.tsx` may eliminate the need for the `appendLabel` property in the future.
* ♻️ refactor(ui): enhance RenameButton to accept label options
Added the ability to optionally display labels; the Rename Button component is used by both `Convo.tsx` and `Conversation.tsx`, but currently only `Convo.tsx` is active and `Conversation.tsx `is apparently not used; removing `Conversation.tsx` may eliminate the need for the `appendLabel` property in the future.
* 🔧 chors: additional localization labels
* ♻️ refactor: change is_archived property of conversation to camelCase
* Refactor the is_archived property of conversation to camelCase (isArchived) to adhere to the existing code conventions
* Modify the function that retrieves conversations to accept the isArchived parameter
* ♻️ refactor: add archiveConversation mutation
I thought I could divert dataService.updateConversation, but added a new archiveConversation because the request types are different. It might be better to make them common, but to avoid side effects, I added a new function this time.
Added process to deleteConversationMutation to delete archived conversations
* ✨ feat: Add the function to hide a cancel button in DialogTemplate component
The Cancel button is not needed when displaying the archive list, so I made the Cancel button optional.
* ♻️ refactor: Add support for filtering archived conversations in Nav component
This commit modifies the Nav component to add the ability to filter out archived conversations when fetching data. This is done by adding `isArchived: false` to the query parameters for both the `useConversationsInfiniteQuery()` and `useSearchInfiniteQuery()` hooks, effectively excluding any archived conversations from the results returned.
* ♻️ refactor: add Tooltip to DeleteButton
* Add Tooltip to DeleteButton component
* Display Tooltip when DeleteButton only shows an Icon without text
* ✨ feat(ui): add ArchiveButton component for archiving conversations
To be compatible with the ChatGPT UI, no confirmation dialog is displayed when ArchiveButton is clicked. The basic behavior conforms to DeleteButton and RenameButton.
* ✨ feat(ui): add Archive button to list of conversations
Modify the Nav of the conversation list to include a dropdown that contains the Rename and Delete options, similar to the ChatGPT UI. Additionally, an Archive button has been added adjacent to the dropdown menu.
* ✨ feat: Add ArchivedChatsTable component
Adds the `ArchivedChatsTable` component, which displays a table of archived chats. It has been implemented to be as compatible with the ChatGPT UI as possible.
* 🚑 fix(tooltip): increase z-index to ensure visibility over Dialog
Resolve an issue where tooltips were not visible when displayed over a Dialog. The z-index of `DialogPrimitive.Portal` in `Dialog.tsx` is set to 999. Since the rationale for this value is unclear, the z-index of the tooltip has been increased to 1000 to guarantee its visibility above the Dialog component.
* 🔧 chors: add internationalization labels
2024-05-06 20:07:00 -07:00
|
|
|
} else {
|
2025-04-15 10:04:00 +02:00
|
|
|
filters.push({ $or: [{ isArchived: false }, { isArchived: { $exists: false } }] });
|
🚀feat: Archive conversations (#2590)
* 🔧chore: add internationalization labels for archive feature
* ✨ feat: Add function to useArchiveConversationMutation()
This commit adds a new mutation function `useArchiveConversationMutation()` for archiving conversations. This function takes the ID string of the conversation to be archived and returns a mutation result object. Upon successful archiving, it removes and refreshes the conversation from the query data cache.
While ChatGPT PATCHes the archived status by sending `{is_archived: true}` to the URL `/backend-api/conversation/$conversation_id`, this implementation uses the `dataService.updateConversation(payload)` with a POST method, aligning with the existing code conventions.
* ✨ feat(api): add is_archived field to Conversation schema and update getConvosByPage method
This commit adds a new field `is_archived` with a default value of false to the Conversation schema. It also modifies the `getConvosByPage` method within the Conversation API to adjust the query to only target conversations where `is_archived` is set to false or where the `is_archived` field does not exist. The function `getConvosQueried`, which returns conversations for a specified Conversation ID, was determined not to require consideration of whether `is_archived` is true or false, and thus was not modified.
* ♻️ refactor: add className prop to DotsIcon component
To enhance the versatility of the DotsIcon component, this commit introduces the ability to specify a className prop, allowing for greater customization.
* ✨ feat(ui): add Edit Button to group Title change and Conversation delete buttons
Added a new Edit Button to the conversations, similar to the ChatGPT UI, which groups options for editing the conversation title and deleting conversations. This grouping is accessible through a dialogue that appears when the three-dot icon is clicked.
* ♻️ refactor(ui): enhance Delete Button to accept className and label options
Enhanced the Delete Button component to accept a `className` for customization and an optional `appendLabel`. The DeleteButton component is used by both `Convo.tsx` and `Conversation.tsx`, but currently only `Convo.tsx` is active and `Conversation.tsx `is apparently not used; removing `Conversation.tsx` may eliminate the need for the `appendLabel` property in the future.
* ♻️ refactor(ui): enhance RenameButton to accept label options
Added the ability to optionally display labels; the Rename Button component is used by both `Convo.tsx` and `Conversation.tsx`, but currently only `Convo.tsx` is active and `Conversation.tsx `is apparently not used; removing `Conversation.tsx` may eliminate the need for the `appendLabel` property in the future.
* 🔧 chors: additional localization labels
* ♻️ refactor: change is_archived property of conversation to camelCase
* Refactor the is_archived property of conversation to camelCase (isArchived) to adhere to the existing code conventions
* Modify the function that retrieves conversations to accept the isArchived parameter
* ♻️ refactor: add archiveConversation mutation
I thought I could divert dataService.updateConversation, but added a new archiveConversation because the request types are different. It might be better to make them common, but to avoid side effects, I added a new function this time.
Added process to deleteConversationMutation to delete archived conversations
* ✨ feat: Add the function to hide a cancel button in DialogTemplate component
The Cancel button is not needed when displaying the archive list, so I made the Cancel button optional.
* ♻️ refactor: Add support for filtering archived conversations in Nav component
This commit modifies the Nav component to add the ability to filter out archived conversations when fetching data. This is done by adding `isArchived: false` to the query parameters for both the `useConversationsInfiniteQuery()` and `useSearchInfiniteQuery()` hooks, effectively excluding any archived conversations from the results returned.
* ♻️ refactor: add Tooltip to DeleteButton
* Add Tooltip to DeleteButton component
* Display Tooltip when DeleteButton only shows an Icon without text
* ✨ feat(ui): add ArchiveButton component for archiving conversations
To be compatible with the ChatGPT UI, no confirmation dialog is displayed when ArchiveButton is clicked. The basic behavior conforms to DeleteButton and RenameButton.
* ✨ feat(ui): add Archive button to list of conversations
Modify the Nav of the conversation list to include a dropdown that contains the Rename and Delete options, similar to the ChatGPT UI. Additionally, an Archive button has been added adjacent to the dropdown menu.
* ✨ feat: Add ArchivedChatsTable component
Adds the `ArchivedChatsTable` component, which displays a table of archived chats. It has been implemented to be as compatible with the ChatGPT UI as possible.
* 🚑 fix(tooltip): increase z-index to ensure visibility over Dialog
Resolve an issue where tooltips were not visible when displayed over a Dialog. The z-index of `DialogPrimitive.Portal` in `Dialog.tsx` is set to 999. Since the rationale for this value is unclear, the z-index of the tooltip has been increased to 1000 to guarantee its visibility above the Dialog component.
* 🔧 chors: add internationalization labels
2024-05-06 20:07:00 -07:00
|
|
|
}
|
2025-04-15 10:04:00 +02:00
|
|
|
|
2024-07-29 07:45:59 -07:00
|
|
|
if (Array.isArray(tags) && tags.length > 0) {
|
2025-04-15 10:04:00 +02:00
|
|
|
filters.push({ tags: { $in: tags } });
|
2024-07-29 07:45:59 -07:00
|
|
|
}
|
2025-02-06 08:11:47 -08:00
|
|
|
|
2025-04-15 10:04:00 +02:00
|
|
|
filters.push({ $or: [{ expiredAt: null }, { expiredAt: { $exists: false } }] });
|
|
|
|
|
|
|
|
if (search) {
|
|
|
|
try {
|
|
|
|
const meiliResults = await Conversation.meiliSearch(search);
|
|
|
|
const matchingIds = Array.isArray(meiliResults.hits)
|
|
|
|
? meiliResults.hits.map((result) => result.conversationId)
|
|
|
|
: [];
|
|
|
|
if (!matchingIds.length) {
|
|
|
|
return { conversations: [], nextCursor: null };
|
|
|
|
}
|
|
|
|
filters.push({ conversationId: { $in: matchingIds } });
|
|
|
|
} catch (error) {
|
|
|
|
logger.error('[getConvosByCursor] Error during meiliSearch', error);
|
2025-09-15 22:51:34 +02:00
|
|
|
throw new Error('Error during meiliSearch');
|
2025-04-15 10:04:00 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-09-15 22:51:34 +02:00
|
|
|
const validSortFields = ['title', 'createdAt', 'updatedAt'];
|
|
|
|
if (!validSortFields.includes(sortBy)) {
|
|
|
|
throw new Error(
|
|
|
|
`Invalid sortBy field: ${sortBy}. Must be one of ${validSortFields.join(', ')}`,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
const finalSortBy = sortBy;
|
|
|
|
const finalSortDirection = sortDirection === 'asc' ? 'asc' : 'desc';
|
|
|
|
|
|
|
|
let cursorFilter = null;
|
2025-04-15 10:04:00 +02:00
|
|
|
if (cursor) {
|
2025-09-15 22:51:34 +02:00
|
|
|
try {
|
|
|
|
const decoded = JSON.parse(Buffer.from(cursor, 'base64').toString());
|
|
|
|
const { primary, secondary } = decoded;
|
|
|
|
const primaryValue = finalSortBy === 'title' ? primary : new Date(primary);
|
|
|
|
const secondaryValue = new Date(secondary);
|
|
|
|
const op = finalSortDirection === 'asc' ? '$gt' : '$lt';
|
|
|
|
|
|
|
|
cursorFilter = {
|
|
|
|
$or: [
|
|
|
|
{ [finalSortBy]: { [op]: primaryValue } },
|
|
|
|
{
|
|
|
|
[finalSortBy]: primaryValue,
|
|
|
|
updatedAt: { [op]: secondaryValue },
|
|
|
|
},
|
|
|
|
],
|
|
|
|
};
|
|
|
|
} catch (err) {
|
|
|
|
logger.warn('[getConvosByCursor] Invalid cursor format, starting from beginning');
|
|
|
|
}
|
|
|
|
if (cursorFilter) {
|
|
|
|
filters.push(cursorFilter);
|
|
|
|
}
|
2025-04-15 10:04:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
const query = filters.length === 1 ? filters[0] : { $and: filters };
|
2025-02-06 08:11:47 -08:00
|
|
|
|
2023-03-07 14:22:33 -05:00
|
|
|
try {
|
2025-09-15 22:51:34 +02:00
|
|
|
const sortOrder = finalSortDirection === 'asc' ? 1 : -1;
|
|
|
|
const sortObj = { [finalSortBy]: sortOrder };
|
|
|
|
|
|
|
|
if (finalSortBy !== 'updatedAt') {
|
|
|
|
sortObj.updatedAt = sortOrder;
|
|
|
|
}
|
|
|
|
|
🚀feat: Archive conversations (#2590)
* 🔧chore: add internationalization labels for archive feature
* ✨ feat: Add function to useArchiveConversationMutation()
This commit adds a new mutation function `useArchiveConversationMutation()` for archiving conversations. This function takes the ID string of the conversation to be archived and returns a mutation result object. Upon successful archiving, it removes and refreshes the conversation from the query data cache.
While ChatGPT PATCHes the archived status by sending `{is_archived: true}` to the URL `/backend-api/conversation/$conversation_id`, this implementation uses the `dataService.updateConversation(payload)` with a POST method, aligning with the existing code conventions.
* ✨ feat(api): add is_archived field to Conversation schema and update getConvosByPage method
This commit adds a new field `is_archived` with a default value of false to the Conversation schema. It also modifies the `getConvosByPage` method within the Conversation API to adjust the query to only target conversations where `is_archived` is set to false or where the `is_archived` field does not exist. The function `getConvosQueried`, which returns conversations for a specified Conversation ID, was determined not to require consideration of whether `is_archived` is true or false, and thus was not modified.
* ♻️ refactor: add className prop to DotsIcon component
To enhance the versatility of the DotsIcon component, this commit introduces the ability to specify a className prop, allowing for greater customization.
* ✨ feat(ui): add Edit Button to group Title change and Conversation delete buttons
Added a new Edit Button to the conversations, similar to the ChatGPT UI, which groups options for editing the conversation title and deleting conversations. This grouping is accessible through a dialogue that appears when the three-dot icon is clicked.
* ♻️ refactor(ui): enhance Delete Button to accept className and label options
Enhanced the Delete Button component to accept a `className` for customization and an optional `appendLabel`. The DeleteButton component is used by both `Convo.tsx` and `Conversation.tsx`, but currently only `Convo.tsx` is active and `Conversation.tsx `is apparently not used; removing `Conversation.tsx` may eliminate the need for the `appendLabel` property in the future.
* ♻️ refactor(ui): enhance RenameButton to accept label options
Added the ability to optionally display labels; the Rename Button component is used by both `Convo.tsx` and `Conversation.tsx`, but currently only `Convo.tsx` is active and `Conversation.tsx `is apparently not used; removing `Conversation.tsx` may eliminate the need for the `appendLabel` property in the future.
* 🔧 chors: additional localization labels
* ♻️ refactor: change is_archived property of conversation to camelCase
* Refactor the is_archived property of conversation to camelCase (isArchived) to adhere to the existing code conventions
* Modify the function that retrieves conversations to accept the isArchived parameter
* ♻️ refactor: add archiveConversation mutation
I thought I could divert dataService.updateConversation, but added a new archiveConversation because the request types are different. It might be better to make them common, but to avoid side effects, I added a new function this time.
Added process to deleteConversationMutation to delete archived conversations
* ✨ feat: Add the function to hide a cancel button in DialogTemplate component
The Cancel button is not needed when displaying the archive list, so I made the Cancel button optional.
* ♻️ refactor: Add support for filtering archived conversations in Nav component
This commit modifies the Nav component to add the ability to filter out archived conversations when fetching data. This is done by adding `isArchived: false` to the query parameters for both the `useConversationsInfiniteQuery()` and `useSearchInfiniteQuery()` hooks, effectively excluding any archived conversations from the results returned.
* ♻️ refactor: add Tooltip to DeleteButton
* Add Tooltip to DeleteButton component
* Display Tooltip when DeleteButton only shows an Icon without text
* ✨ feat(ui): add ArchiveButton component for archiving conversations
To be compatible with the ChatGPT UI, no confirmation dialog is displayed when ArchiveButton is clicked. The basic behavior conforms to DeleteButton and RenameButton.
* ✨ feat(ui): add Archive button to list of conversations
Modify the Nav of the conversation list to include a dropdown that contains the Rename and Delete options, similar to the ChatGPT UI. Additionally, an Archive button has been added adjacent to the dropdown menu.
* ✨ feat: Add ArchivedChatsTable component
Adds the `ArchivedChatsTable` component, which displays a table of archived chats. It has been implemented to be as compatible with the ChatGPT UI as possible.
* 🚑 fix(tooltip): increase z-index to ensure visibility over Dialog
Resolve an issue where tooltips were not visible when displayed over a Dialog. The z-index of `DialogPrimitive.Portal` in `Dialog.tsx` is set to 999. Since the rationale for this value is unclear, the z-index of the tooltip has been increased to 1000 to guarantee its visibility above the Dialog component.
* 🔧 chors: add internationalization labels
2024-05-06 20:07:00 -07:00
|
|
|
const convos = await Conversation.find(query)
|
2025-04-16 03:00:06 +02:00
|
|
|
.select(
|
2025-04-17 03:07:43 +02:00
|
|
|
'conversationId endpoint title createdAt updatedAt user model agent_id assistant_id spec iconURL',
|
2025-04-16 03:00:06 +02:00
|
|
|
)
|
2025-09-15 22:51:34 +02:00
|
|
|
.sort(sortObj)
|
2025-04-15 10:04:00 +02:00
|
|
|
.limit(limit + 1)
|
2023-07-25 19:27:55 -04:00
|
|
|
.lean();
|
2025-04-15 10:04:00 +02:00
|
|
|
|
|
|
|
let nextCursor = null;
|
|
|
|
if (convos.length > limit) {
|
|
|
|
const lastConvo = convos.pop();
|
2025-09-15 22:51:34 +02:00
|
|
|
const primaryValue = lastConvo[finalSortBy];
|
|
|
|
const primaryStr = finalSortBy === 'title' ? primaryValue : primaryValue.toISOString();
|
|
|
|
const secondaryStr = lastConvo.updatedAt.toISOString();
|
|
|
|
const composite = { primary: primaryStr, secondary: secondaryStr };
|
|
|
|
nextCursor = Buffer.from(JSON.stringify(composite)).toString('base64');
|
2025-04-15 10:04:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return { conversations: convos, nextCursor };
|
2023-03-07 14:22:33 -05:00
|
|
|
} catch (error) {
|
2025-04-15 10:04:00 +02:00
|
|
|
logger.error('[getConvosByCursor] Error getting conversations', error);
|
2025-09-15 22:51:34 +02:00
|
|
|
throw new Error('Error getting conversations');
|
2023-03-07 14:22:33 -05:00
|
|
|
}
|
|
|
|
},
|
2025-04-15 10:04:00 +02:00
|
|
|
getConvosQueried: async (user, convoIds, cursor = null, limit = 25) => {
|
2023-03-18 01:40:49 -04:00
|
|
|
try {
|
2025-04-15 10:04:00 +02:00
|
|
|
if (!convoIds?.length) {
|
|
|
|
return { conversations: [], nextCursor: null, convoMap: {} };
|
2023-03-18 01:40:49 -04:00
|
|
|
}
|
|
|
|
|
2025-04-15 10:04:00 +02:00
|
|
|
const conversationIds = convoIds.map((convo) => convo.conversationId);
|
2023-03-18 17:49:24 -04:00
|
|
|
|
2025-04-15 10:04:00 +02:00
|
|
|
const results = await Conversation.find({
|
|
|
|
user,
|
|
|
|
conversationId: { $in: conversationIds },
|
|
|
|
$or: [{ expiredAt: { $exists: false } }, { expiredAt: null }],
|
|
|
|
}).lean();
|
2023-07-28 13:16:41 -04:00
|
|
|
|
2025-04-15 10:04:00 +02:00
|
|
|
results.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
|
|
|
|
|
|
|
|
let filtered = results;
|
|
|
|
if (cursor && cursor !== 'start') {
|
|
|
|
const cursorDate = new Date(cursor);
|
|
|
|
filtered = results.filter((convo) => new Date(convo.updatedAt) < cursorDate);
|
|
|
|
}
|
|
|
|
|
|
|
|
const limited = filtered.slice(0, limit + 1);
|
|
|
|
let nextCursor = null;
|
|
|
|
if (limited.length > limit) {
|
|
|
|
const lastConvo = limited.pop();
|
|
|
|
nextCursor = lastConvo.updatedAt.toISOString();
|
|
|
|
}
|
|
|
|
|
|
|
|
const convoMap = {};
|
|
|
|
limited.forEach((convo) => {
|
2023-07-28 13:16:41 -04:00
|
|
|
convoMap[convo.conversationId] = convo;
|
2023-03-18 01:40:49 -04:00
|
|
|
});
|
2023-03-18 23:18:36 -04:00
|
|
|
|
2025-04-15 10:04:00 +02:00
|
|
|
return { conversations: limited, nextCursor, convoMap };
|
2023-03-18 01:40:49 -04:00
|
|
|
} catch (error) {
|
2023-12-14 07:49:27 -05:00
|
|
|
logger.error('[getConvosQueried] Error getting conversations', error);
|
2025-09-15 22:51:34 +02:00
|
|
|
throw new Error('Error fetching conversations');
|
2023-03-18 01:40:49 -04:00
|
|
|
}
|
|
|
|
},
|
2023-03-08 22:30:29 -05:00
|
|
|
getConvo,
|
2023-03-20 01:35:02 -04:00
|
|
|
/* chore: this method is not properly error handled */
|
2023-03-14 01:24:43 +08:00
|
|
|
getConvoTitle: async (user, conversationId) => {
|
2023-03-07 14:22:33 -05:00
|
|
|
try {
|
2023-03-14 01:24:43 +08:00
|
|
|
const convo = await getConvo(user, conversationId);
|
2023-03-20 01:35:02 -04:00
|
|
|
/* ChatGPT Browser was triggering error here due to convo being saved later */
|
|
|
|
if (convo && !convo.title) {
|
|
|
|
return null;
|
|
|
|
} else {
|
2023-03-20 02:30:08 -04:00
|
|
|
// TypeError: Cannot read properties of null (reading 'title')
|
|
|
|
return convo?.title || 'New Chat';
|
2023-03-20 01:35:02 -04:00
|
|
|
}
|
2023-03-07 14:22:33 -05:00
|
|
|
} catch (error) {
|
2023-12-14 07:49:27 -05:00
|
|
|
logger.error('[getConvoTitle] Error getting conversation title', error);
|
2025-09-15 22:51:34 +02:00
|
|
|
throw new Error('Error getting conversation title');
|
2023-03-07 14:22:33 -05:00
|
|
|
}
|
2023-03-05 14:41:50 -05:00
|
|
|
},
|
2023-09-18 15:19:50 -04:00
|
|
|
/**
|
|
|
|
* Asynchronously deletes conversations and associated messages for a given user and filter.
|
|
|
|
*
|
|
|
|
* @async
|
|
|
|
* @function
|
|
|
|
* @param {string|ObjectId} user - The user's ID.
|
|
|
|
* @param {Object} filter - Additional filter criteria for the conversations to be deleted.
|
|
|
|
* @returns {Promise<{ n: number, ok: number, deletedCount: number, messages: { n: number, ok: number, deletedCount: number } }>}
|
|
|
|
* An object containing the count of deleted conversations and associated messages.
|
|
|
|
* @throws {Error} Throws an error if there's an issue with the database operations.
|
|
|
|
*
|
|
|
|
* @example
|
|
|
|
* const user = 'someUserId';
|
|
|
|
* const filter = { someField: 'someValue' };
|
|
|
|
* const result = await deleteConvos(user, filter);
|
2023-12-14 07:49:27 -05:00
|
|
|
* logger.error(result); // { n: 5, ok: 1, deletedCount: 5, messages: { n: 10, ok: 1, deletedCount: 10 } }
|
2023-09-18 15:19:50 -04:00
|
|
|
*/
|
2023-03-14 01:24:43 +08:00
|
|
|
deleteConvos: async (user, filter) => {
|
2025-04-15 10:04:00 +02:00
|
|
|
try {
|
|
|
|
const userFilter = { ...filter, user };
|
|
|
|
const conversations = await Conversation.find(userFilter).select('conversationId');
|
|
|
|
const conversationIds = conversations.map((c) => c.conversationId);
|
|
|
|
|
|
|
|
if (!conversationIds.length) {
|
|
|
|
throw new Error('Conversation not found or already deleted.');
|
|
|
|
}
|
|
|
|
|
|
|
|
const deleteConvoResult = await Conversation.deleteMany(userFilter);
|
|
|
|
|
|
|
|
const deleteMessagesResult = await deleteMessages({
|
|
|
|
conversationId: { $in: conversationIds },
|
|
|
|
});
|
|
|
|
|
|
|
|
return { ...deleteConvoResult, messages: deleteMessagesResult };
|
|
|
|
} catch (error) {
|
|
|
|
logger.error('[deleteConvos] Error deleting conversations and messages', error);
|
|
|
|
throw error;
|
|
|
|
}
|
2023-07-14 09:36:49 -04:00
|
|
|
},
|
2023-02-06 14:05:02 -05:00
|
|
|
};
|