mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-24 04:10:15 +01:00
🏷️ fix: Address Statefulness Issues for Bookmarks (#3590)
* refactor: optimize tag methods, remove rebuild * refactor(tags): add lean db operations, fix updateTagsForConversation, remove rebuild button, only send convoId once * refactor: Update BookmarkMenu to use Constants.NEW_CONVO constant for comparison * style: Update BookmarkMenu styles and constants, use theming * refactor: move tags query from package to client workspace * refactor: optimize ConversationTag document creation and update logic * style: Update BookmarkMenuItems to use theming * refactor: JSDocs + try/catch for conversation tags API routes * refactor: Update BookmarkNav theming classes and new data provider location * fix: statefulness of conversation bookmarks - move non-mutation hook to hooks/Conversation - remove use of deprecated global convo - update convo infinite data as well as current convo state upon successful tag add * refactor: Update BookmarkMenu styles and constants, use theming * refactor: Add lean option to ConversationTag deletion query * fix(BookmarkTable): position order rendering esp. when new tag is created * refactor: Update useBookmarkSucess to useBookmarkSuccess for consistency * refactor: Update ConversationTag creation logic to increment count only if addToConversation is true * style: theming
This commit is contained in:
parent
6ea2628b56
commit
016ed866a3
28 changed files with 622 additions and 536 deletions
|
|
@ -1,277 +1,257 @@
|
||||||
//const crypto = require('crypto');
|
|
||||||
|
|
||||||
const logger = require('~/config/winston');
|
|
||||||
const Conversation = require('./schema/convoSchema');
|
|
||||||
const ConversationTag = require('./schema/conversationTagSchema');
|
const ConversationTag = require('./schema/conversationTagSchema');
|
||||||
|
const Conversation = require('./schema/convoSchema');
|
||||||
|
const logger = require('~/config/winston');
|
||||||
|
|
||||||
const SAVED_TAG = 'Saved';
|
const SAVED_TAG = 'Saved';
|
||||||
|
|
||||||
const updateTagsForConversation = async (user, conversationId, tags) => {
|
/**
|
||||||
|
* Retrieves all conversation tags for a user.
|
||||||
|
* @param {string} user - The user ID.
|
||||||
|
* @returns {Promise<Array>} An array of conversation tags.
|
||||||
|
*/
|
||||||
|
const getConversationTags = async (user) => {
|
||||||
try {
|
try {
|
||||||
const conversation = await Conversation.findOne({ user, conversationId });
|
const cTags = await ConversationTag.find({ user }).sort({ position: 1 }).lean();
|
||||||
if (!conversation) {
|
|
||||||
return { message: 'Conversation not found' };
|
|
||||||
}
|
|
||||||
|
|
||||||
const addedTags = tags.tags.filter((tag) => !conversation.tags.includes(tag));
|
cTags.sort((a, b) => {
|
||||||
const removedTags = conversation.tags.filter((tag) => !tags.tags.includes(tag));
|
if (a.tag === SAVED_TAG) {
|
||||||
for (const tag of addedTags) {
|
return -1;
|
||||||
await ConversationTag.updateOne({ tag, user }, { $inc: { count: 1 } }, { upsert: true });
|
}
|
||||||
}
|
if (b.tag === SAVED_TAG) {
|
||||||
for (const tag of removedTags) {
|
return 1;
|
||||||
await ConversationTag.updateOne({ tag, user }, { $inc: { count: -1 } });
|
}
|
||||||
}
|
return 0;
|
||||||
conversation.tags = tags.tags;
|
});
|
||||||
await conversation.save({ timestamps: { updatedAt: false } });
|
|
||||||
return conversation.tags;
|
return cTags;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[updateTagsToConversation] Error updating tags', error);
|
logger.error('[getConversationTags] Error getting conversation tags', error);
|
||||||
return { message: 'Error updating tags' };
|
throw new Error('Error getting conversation tags');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new conversation tag.
|
||||||
|
* @param {string} user - The user ID.
|
||||||
|
* @param {Object} data - The tag data.
|
||||||
|
* @param {string} data.tag - The tag name.
|
||||||
|
* @param {string} [data.description] - The tag description.
|
||||||
|
* @param {boolean} [data.addToConversation] - Whether to add the tag to a conversation.
|
||||||
|
* @param {string} [data.conversationId] - The conversation ID to add the tag to.
|
||||||
|
* @returns {Promise<Object>} The created tag.
|
||||||
|
*/
|
||||||
const createConversationTag = async (user, data) => {
|
const createConversationTag = async (user, data) => {
|
||||||
try {
|
try {
|
||||||
const cTag = await ConversationTag.findOne({ user, tag: data.tag });
|
const { tag, description, addToConversation, conversationId } = data;
|
||||||
if (cTag) {
|
|
||||||
return cTag;
|
const existingTag = await ConversationTag.findOne({ user, tag }).lean();
|
||||||
|
if (existingTag) {
|
||||||
|
return existingTag;
|
||||||
}
|
}
|
||||||
|
|
||||||
const addToConversation = data.addToConversation && data.conversationId;
|
const maxPosition = await ConversationTag.findOne({ user }).sort('-position').lean();
|
||||||
const newTag = await ConversationTag.create({
|
const position = (maxPosition?.position || 0) + 1;
|
||||||
user,
|
|
||||||
tag: data.tag,
|
|
||||||
count: 0,
|
|
||||||
description: data.description,
|
|
||||||
position: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
await ConversationTag.updateMany(
|
const newTag = await ConversationTag.findOneAndUpdate(
|
||||||
{ user, position: { $gte: 1 }, _id: { $ne: newTag._id } },
|
{ tag, user },
|
||||||
{ $inc: { position: 1 } },
|
{
|
||||||
|
tag,
|
||||||
|
user,
|
||||||
|
count: addToConversation ? 1 : 0,
|
||||||
|
position,
|
||||||
|
description,
|
||||||
|
$setOnInsert: { createdAt: new Date() },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
new: true,
|
||||||
|
upsert: true,
|
||||||
|
lean: true,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (addToConversation) {
|
if (addToConversation && conversationId) {
|
||||||
const conversation = await Conversation.findOne({
|
await Conversation.findOneAndUpdate(
|
||||||
user,
|
{ user, conversationId },
|
||||||
conversationId: data.conversationId,
|
{ $addToSet: { tags: tag } },
|
||||||
});
|
{ new: true },
|
||||||
if (conversation) {
|
);
|
||||||
const tags = [...(conversation.tags || []), data.tag];
|
|
||||||
await updateTagsForConversation(user, data.conversationId, { tags });
|
|
||||||
} else {
|
|
||||||
logger.warn('[updateTagsForConversation] Conversation not found', data.conversationId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return await ConversationTag.findOne({ user, tag: data.tag });
|
return newTag;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[createConversationTag] Error updating conversation tag', error);
|
logger.error('[createConversationTag] Error creating conversation tag', error);
|
||||||
return { message: 'Error updating conversation tag' };
|
throw new Error('Error creating conversation tag');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const replaceOrRemoveTagInConversations = async (user, oldtag, newtag) => {
|
/**
|
||||||
|
* Updates an existing conversation tag.
|
||||||
|
* @param {string} user - The user ID.
|
||||||
|
* @param {string} oldTag - The current tag name.
|
||||||
|
* @param {Object} data - The updated tag data.
|
||||||
|
* @param {string} [data.tag] - The new tag name.
|
||||||
|
* @param {string} [data.description] - The updated description.
|
||||||
|
* @param {number} [data.position] - The new position.
|
||||||
|
* @returns {Promise<Object>} The updated tag.
|
||||||
|
*/
|
||||||
|
const updateConversationTag = async (user, oldTag, data) => {
|
||||||
try {
|
try {
|
||||||
const conversations = await Conversation.find({ user, tags: { $in: [oldtag] } });
|
const { tag: newTag, description, position } = data;
|
||||||
for (const conversation of conversations) {
|
|
||||||
if (newtag && newtag !== '') {
|
const existingTag = await ConversationTag.findOne({ user, tag: oldTag }).lean();
|
||||||
conversation.tags = conversation.tags.map((tag) => (tag === oldtag ? newtag : tag));
|
if (!existingTag) {
|
||||||
} else {
|
return null;
|
||||||
conversation.tags = conversation.tags.filter((tag) => tag !== oldtag);
|
}
|
||||||
|
|
||||||
|
if (newTag && newTag !== oldTag) {
|
||||||
|
const tagAlreadyExists = await ConversationTag.findOne({ user, tag: newTag }).lean();
|
||||||
|
if (tagAlreadyExists) {
|
||||||
|
throw new Error('Tag already exists');
|
||||||
}
|
}
|
||||||
await conversation.save({ timestamps: { updatedAt: false } });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('[replaceOrRemoveTagInConversations] Error updating conversation tags', error);
|
|
||||||
return { message: 'Error updating conversation tags' };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateTagPosition = async (user, tag, newPosition) => {
|
await Conversation.updateMany({ user, tags: oldTag }, { $set: { 'tags.$': newTag } });
|
||||||
try {
|
|
||||||
const cTag = await ConversationTag.findOne({ user, tag });
|
|
||||||
if (!cTag) {
|
|
||||||
return { message: 'Tag not found' };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const oldPosition = cTag.position;
|
const updateData = {};
|
||||||
|
if (newTag) {
|
||||||
if (newPosition === oldPosition) {
|
updateData.tag = newTag;
|
||||||
return cTag;
|
}
|
||||||
|
if (description !== undefined) {
|
||||||
|
updateData.description = description;
|
||||||
|
}
|
||||||
|
if (position !== undefined) {
|
||||||
|
await adjustPositions(user, existingTag.position, position);
|
||||||
|
updateData.position = position;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateOperations = [];
|
return await ConversationTag.findOneAndUpdate({ user, tag: oldTag }, updateData, {
|
||||||
|
new: true,
|
||||||
if (newPosition > oldPosition) {
|
lean: true,
|
||||||
// Move other tags up
|
|
||||||
updateOperations.push({
|
|
||||||
updateMany: {
|
|
||||||
filter: {
|
|
||||||
user,
|
|
||||||
position: { $gt: oldPosition, $lte: newPosition },
|
|
||||||
tag: { $ne: SAVED_TAG },
|
|
||||||
},
|
|
||||||
update: { $inc: { position: -1 } },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Move other tags down
|
|
||||||
updateOperations.push({
|
|
||||||
updateMany: {
|
|
||||||
filter: {
|
|
||||||
user,
|
|
||||||
position: { $gte: newPosition, $lt: oldPosition },
|
|
||||||
tag: { $ne: SAVED_TAG },
|
|
||||||
},
|
|
||||||
update: { $inc: { position: 1 } },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the target tag's position
|
|
||||||
updateOperations.push({
|
|
||||||
updateOne: {
|
|
||||||
filter: { _id: cTag._id },
|
|
||||||
update: { $set: { position: newPosition } },
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await ConversationTag.bulkWrite(updateOperations);
|
|
||||||
|
|
||||||
return await ConversationTag.findById(cTag._id);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[updateTagPosition] Error updating tag position', error);
|
logger.error('[updateConversationTag] Error updating conversation tag', error);
|
||||||
return { message: 'Error updating tag position' };
|
throw new Error('Error updating conversation tag');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adjusts positions of tags when a tag's position is changed.
|
||||||
|
* @param {string} user - The user ID.
|
||||||
|
* @param {number} oldPosition - The old position of the tag.
|
||||||
|
* @param {number} newPosition - The new position of the tag.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
const adjustPositions = async (user, oldPosition, newPosition) => {
|
||||||
|
if (oldPosition === newPosition) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const update = oldPosition < newPosition ? { $inc: { position: -1 } } : { $inc: { position: 1 } };
|
||||||
|
|
||||||
|
await ConversationTag.updateMany(
|
||||||
|
{
|
||||||
|
user,
|
||||||
|
position: {
|
||||||
|
$gt: Math.min(oldPosition, newPosition),
|
||||||
|
$lte: Math.max(oldPosition, newPosition),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a conversation tag.
|
||||||
|
* @param {string} user - The user ID.
|
||||||
|
* @param {string} tag - The tag to delete.
|
||||||
|
* @returns {Promise<Object>} The deleted tag.
|
||||||
|
*/
|
||||||
|
const deleteConversationTag = async (user, tag) => {
|
||||||
|
try {
|
||||||
|
const deletedTag = await ConversationTag.findOneAndDelete({ user, tag }).lean();
|
||||||
|
if (!deletedTag) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Conversation.updateMany({ user, tags: tag }, { $pull: { tags: tag } });
|
||||||
|
|
||||||
|
await ConversationTag.updateMany(
|
||||||
|
{ user, position: { $gt: deletedTag.position } },
|
||||||
|
{ $inc: { position: -1 } },
|
||||||
|
);
|
||||||
|
|
||||||
|
return deletedTag;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[deleteConversationTag] Error deleting conversation tag', error);
|
||||||
|
throw new Error('Error deleting conversation tag');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates tags for a specific conversation.
|
||||||
|
* @param {string} user - The user ID.
|
||||||
|
* @param {string} conversationId - The conversation ID.
|
||||||
|
* @param {string[]} tags - The new set of tags for the conversation.
|
||||||
|
* @returns {Promise<string[]>} The updated list of tags for the conversation.
|
||||||
|
*/
|
||||||
|
const updateTagsForConversation = async (user, conversationId, tags) => {
|
||||||
|
try {
|
||||||
|
const conversation = await Conversation.findOne({ user, conversationId }).lean();
|
||||||
|
if (!conversation) {
|
||||||
|
throw new Error('Conversation not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldTags = new Set(conversation.tags);
|
||||||
|
const newTags = new Set(tags);
|
||||||
|
|
||||||
|
const addedTags = [...newTags].filter((tag) => !oldTags.has(tag));
|
||||||
|
const removedTags = [...oldTags].filter((tag) => !newTags.has(tag));
|
||||||
|
|
||||||
|
const bulkOps = [];
|
||||||
|
|
||||||
|
for (const tag of addedTags) {
|
||||||
|
bulkOps.push({
|
||||||
|
updateOne: {
|
||||||
|
filter: { user, tag },
|
||||||
|
update: { $inc: { count: 1 } },
|
||||||
|
upsert: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const tag of removedTags) {
|
||||||
|
bulkOps.push({
|
||||||
|
updateOne: {
|
||||||
|
filter: { user, tag },
|
||||||
|
update: { $inc: { count: -1 } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bulkOps.length > 0) {
|
||||||
|
await ConversationTag.bulkWrite(bulkOps);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedConversation = (
|
||||||
|
await Conversation.findOneAndUpdate(
|
||||||
|
{ user, conversationId },
|
||||||
|
{ $set: { tags: [...newTags] } },
|
||||||
|
{ new: true },
|
||||||
|
)
|
||||||
|
).toObject();
|
||||||
|
|
||||||
|
return updatedConversation.tags;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[updateTagsForConversation] Error updating tags', error);
|
||||||
|
throw new Error('Error updating tags for conversation');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
SAVED_TAG,
|
SAVED_TAG,
|
||||||
ConversationTag,
|
getConversationTags,
|
||||||
getConversationTags: async (user) => {
|
|
||||||
try {
|
|
||||||
const cTags = await ConversationTag.find({ user }).sort({ position: 1 }).lean();
|
|
||||||
|
|
||||||
cTags.sort((a, b) => {
|
|
||||||
if (a.tag === SAVED_TAG) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (b.tag === SAVED_TAG) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
return cTags;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('[getShare] Error getting share link', error);
|
|
||||||
return { message: 'Error getting share link' };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
createConversationTag,
|
createConversationTag,
|
||||||
updateConversationTag: async (user, tag, data) => {
|
updateConversationTag,
|
||||||
try {
|
deleteConversationTag,
|
||||||
const cTag = await ConversationTag.findOne({ user, tag });
|
|
||||||
if (!cTag) {
|
|
||||||
return createConversationTag(user, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cTag.tag !== data.tag || cTag.description !== data.description) {
|
|
||||||
cTag.tag = data.tag;
|
|
||||||
cTag.description = data.description === undefined ? cTag.description : data.description;
|
|
||||||
await cTag.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.position !== undefined && cTag.position !== data.position) {
|
|
||||||
await updateTagPosition(user, tag, data.position);
|
|
||||||
}
|
|
||||||
|
|
||||||
// update conversation tags properties
|
|
||||||
replaceOrRemoveTagInConversations(user, tag, data.tag);
|
|
||||||
return await ConversationTag.findOne({ user, tag: data.tag });
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('[updateConversationTag] Error updating conversation tag', error);
|
|
||||||
return { message: 'Error updating conversation tag' };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
deleteConversationTag: async (user, tag) => {
|
|
||||||
try {
|
|
||||||
const currentTag = await ConversationTag.findOne({ user, tag });
|
|
||||||
if (!currentTag) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await currentTag.deleteOne({ user, tag });
|
|
||||||
|
|
||||||
await replaceOrRemoveTagInConversations(user, tag, null);
|
|
||||||
return currentTag;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('[deleteConversationTag] Error deleting conversation tag', error);
|
|
||||||
return { message: 'Error deleting conversation tag' };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
updateTagsForConversation,
|
updateTagsForConversation,
|
||||||
rebuildConversationTags: async (user) => {
|
|
||||||
try {
|
|
||||||
const conversations = await Conversation.find({ user }).select('tags');
|
|
||||||
const tagCountMap = {};
|
|
||||||
|
|
||||||
// Count the occurrences of each tag
|
|
||||||
conversations.forEach((conversation) => {
|
|
||||||
conversation.tags.forEach((tag) => {
|
|
||||||
if (tagCountMap[tag]) {
|
|
||||||
tagCountMap[tag]++;
|
|
||||||
} else {
|
|
||||||
tagCountMap[tag] = 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const tags = await ConversationTag.find({ user }).sort({ position: -1 });
|
|
||||||
|
|
||||||
// Update existing tags and add new tags
|
|
||||||
for (const [tag, count] of Object.entries(tagCountMap)) {
|
|
||||||
const existingTag = tags.find((t) => t.tag === tag);
|
|
||||||
if (existingTag) {
|
|
||||||
existingTag.count = count;
|
|
||||||
await existingTag.save();
|
|
||||||
} else {
|
|
||||||
const newTag = new ConversationTag({ user, tag, count });
|
|
||||||
tags.push(newTag);
|
|
||||||
await newTag.save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set count to 0 for tags that are not in the grouped tags
|
|
||||||
for (const tag of tags) {
|
|
||||||
if (!tagCountMap[tag.tag]) {
|
|
||||||
tag.count = 0;
|
|
||||||
await tag.save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort tags by position in descending order
|
|
||||||
tags.sort((a, b) => a.position - b.position);
|
|
||||||
|
|
||||||
// Move the tag with name "saved" to the first position
|
|
||||||
const savedTagIndex = tags.findIndex((tag) => tag.tag === SAVED_TAG);
|
|
||||||
if (savedTagIndex !== -1) {
|
|
||||||
const [savedTag] = tags.splice(savedTagIndex, 1);
|
|
||||||
tags.unshift(savedTag);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reassign positions starting from 0
|
|
||||||
tags.forEach((tag, index) => {
|
|
||||||
tag.position = index;
|
|
||||||
tag.save();
|
|
||||||
});
|
|
||||||
return tags;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('[rearrangeTags] Error rearranging tags', error);
|
|
||||||
return { message: 'Error rearranging tags' };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -175,8 +175,17 @@ router.post('/fork', async (req, res) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
router.put('/tags/:conversationId', async (req, res) => {
|
router.put('/tags/:conversationId', async (req, res) => {
|
||||||
const tag = await updateTagsForConversation(req.user.id, req.params.conversationId, req.body);
|
try {
|
||||||
res.status(200).json(tag);
|
const conversationTags = await updateTagsForConversation(
|
||||||
|
req.user.id,
|
||||||
|
req.params.conversationId,
|
||||||
|
req.body.tags,
|
||||||
|
);
|
||||||
|
res.status(200).json(conversationTags);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error updating conversation tags', error);
|
||||||
|
res.status(500).send('Error updating conversation tags');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
|
||||||
|
|
@ -1,44 +1,88 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
getConversationTags,
|
getConversationTags,
|
||||||
updateConversationTag,
|
updateConversationTag,
|
||||||
createConversationTag,
|
createConversationTag,
|
||||||
deleteConversationTag,
|
deleteConversationTag,
|
||||||
rebuildConversationTags,
|
|
||||||
} = require('~/models/ConversationTag');
|
} = require('~/models/ConversationTag');
|
||||||
const requireJwtAuth = require('~/server/middleware/requireJwtAuth');
|
const requireJwtAuth = require('~/server/middleware/requireJwtAuth');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
router.use(requireJwtAuth);
|
router.use(requireJwtAuth);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /
|
||||||
|
* Retrieves all conversation tags for the authenticated user.
|
||||||
|
* @param {Object} req - Express request object
|
||||||
|
* @param {Object} res - Express response object
|
||||||
|
*/
|
||||||
router.get('/', async (req, res) => {
|
router.get('/', async (req, res) => {
|
||||||
const tags = await getConversationTags(req.user.id);
|
try {
|
||||||
|
const tags = await getConversationTags(req.user.id);
|
||||||
if (tags) {
|
if (tags) {
|
||||||
res.status(200).json(tags);
|
res.status(200).json(tags);
|
||||||
} else {
|
} else {
|
||||||
res.status(404).end();
|
res.status(404).end();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting conversation tags:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /
|
||||||
|
* Creates a new conversation tag for the authenticated user.
|
||||||
|
* @param {Object} req - Express request object
|
||||||
|
* @param {Object} res - Express response object
|
||||||
|
*/
|
||||||
router.post('/', async (req, res) => {
|
router.post('/', async (req, res) => {
|
||||||
const tag = await createConversationTag(req.user.id, req.body);
|
try {
|
||||||
res.status(200).json(tag);
|
const tag = await createConversationTag(req.user.id, req.body);
|
||||||
});
|
res.status(200).json(tag);
|
||||||
|
} catch (error) {
|
||||||
router.post('/rebuild', async (req, res) => {
|
console.error('Error creating conversation tag:', error);
|
||||||
const tag = await rebuildConversationTags(req.user.id);
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
res.status(200).json(tag);
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT /:tag
|
||||||
|
* Updates an existing conversation tag for the authenticated user.
|
||||||
|
* @param {Object} req - Express request object
|
||||||
|
* @param {Object} res - Express response object
|
||||||
|
*/
|
||||||
router.put('/:tag', async (req, res) => {
|
router.put('/:tag', async (req, res) => {
|
||||||
const tag = await updateConversationTag(req.user.id, req.params.tag, req.body);
|
try {
|
||||||
res.status(200).json(tag);
|
const tag = await updateConversationTag(req.user.id, req.params.tag, req.body);
|
||||||
|
if (tag) {
|
||||||
|
res.status(200).json(tag);
|
||||||
|
} else {
|
||||||
|
res.status(404).json({ error: 'Tag not found' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating conversation tag:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /:tag
|
||||||
|
* Deletes a conversation tag for the authenticated user.
|
||||||
|
* @param {Object} req - Express request object
|
||||||
|
* @param {Object} res - Express response object
|
||||||
|
*/
|
||||||
router.delete('/:tag', async (req, res) => {
|
router.delete('/:tag', async (req, res) => {
|
||||||
const tag = await deleteConversationTag(req.user.id, req.params.tag);
|
try {
|
||||||
res.status(200).json(tag);
|
const tag = await deleteConversationTag(req.user.id, req.params.tag);
|
||||||
|
if (tag) {
|
||||||
|
res.status(200).json(tag);
|
||||||
|
} else {
|
||||||
|
res.status(404).json({ error: 'Tag not found' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting conversation tag:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,9 @@ import { cn, removeFocusOutlines, defaultTextProps } from '~/utils/';
|
||||||
import { useBookmarkContext } from '~/Providers/BookmarkContext';
|
import { useBookmarkContext } from '~/Providers/BookmarkContext';
|
||||||
import { useConversationTagMutation } from '~/data-provider';
|
import { useConversationTagMutation } from '~/data-provider';
|
||||||
import { Checkbox, Label, TextareaAutosize } from '~/components/ui/';
|
import { Checkbox, Label, TextareaAutosize } from '~/components/ui/';
|
||||||
|
import { useLocalize, useBookmarkSuccess } from '~/hooks';
|
||||||
import { NotificationSeverity } from '~/common';
|
import { NotificationSeverity } from '~/common';
|
||||||
import { useToastContext } from '~/Providers';
|
import { useToastContext } from '~/Providers';
|
||||||
import { useLocalize } from '~/hooks';
|
|
||||||
|
|
||||||
type TBookmarkFormProps = {
|
type TBookmarkFormProps = {
|
||||||
bookmark?: TConversationTag;
|
bookmark?: TConversationTag;
|
||||||
|
|
@ -31,10 +31,11 @@ const BookmarkForm = ({
|
||||||
tags,
|
tags,
|
||||||
setTags,
|
setTags,
|
||||||
}: TBookmarkFormProps) => {
|
}: TBookmarkFormProps) => {
|
||||||
const { showToast } = useToastContext();
|
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const mutation = useConversationTagMutation(bookmark?.tag);
|
const { showToast } = useToastContext();
|
||||||
const { bookmarks } = useBookmarkContext();
|
const { bookmarks } = useBookmarkContext();
|
||||||
|
const mutation = useConversationTagMutation(bookmark?.tag);
|
||||||
|
const onSuccess = useBookmarkSuccess(conversation?.conversationId || '');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
|
|
@ -82,6 +83,7 @@ const BookmarkForm = ({
|
||||||
(tag) => tag !== undefined,
|
(tag) => tag !== undefined,
|
||||||
) as string[];
|
) as string[];
|
||||||
setTags(newTags);
|
setTags(newTags);
|
||||||
|
onSuccess(newTags);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
|
|
@ -172,9 +174,9 @@ const BookmarkForm = ({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<label
|
<button
|
||||||
|
aria-label={localize('com_ui_bookmarks_add_to_conversation')}
|
||||||
className="form-check-label text-token-text-primary w-full cursor-pointer"
|
className="form-check-label text-token-text-primary w-full cursor-pointer"
|
||||||
htmlFor="addToConversation"
|
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setValue('addToConversation', !getValues('addToConversation'), {
|
setValue('addToConversation', !getValues('addToConversation'), {
|
||||||
shouldDirty: true,
|
shouldDirty: true,
|
||||||
|
|
@ -184,7 +186,7 @@ const BookmarkForm = ({
|
||||||
<div className="flex select-none items-center">
|
<div className="flex select-none items-center">
|
||||||
{localize('com_ui_bookmarks_add_to_conversation')}
|
{localize('com_ui_bookmarks_add_to_conversation')}
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { cn } from '~/utils';
|
||||||
type MenuItemProps = {
|
type MenuItemProps = {
|
||||||
tag: string | React.ReactNode;
|
tag: string | React.ReactNode;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
|
ctx: 'header' | 'nav';
|
||||||
count?: number;
|
count?: number;
|
||||||
handleSubmit: (tag: string) => Promise<void>;
|
handleSubmit: (tag: string) => Promise<void>;
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
|
|
@ -15,6 +16,7 @@ type MenuItemProps = {
|
||||||
|
|
||||||
const BookmarkItem: FC<MenuItemProps> = ({
|
const BookmarkItem: FC<MenuItemProps> = ({
|
||||||
tag,
|
tag,
|
||||||
|
ctx,
|
||||||
selected,
|
selected,
|
||||||
count,
|
count,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
|
@ -34,13 +36,30 @@ const BookmarkItem: FC<MenuItemProps> = ({
|
||||||
overflowWrap: 'anywhere',
|
overflowWrap: 'anywhere',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderIcon = () => {
|
||||||
|
if (icon) {
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
|
if (isLoading) {
|
||||||
|
return <Spinner className="size-4" />;
|
||||||
|
}
|
||||||
|
if (selected) {
|
||||||
|
return <BookmarkFilledIcon className="size-4" />;
|
||||||
|
}
|
||||||
|
return <BookmarkIcon className="size-4" />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ariaLabel =
|
||||||
|
ctx === 'header' ? `${selected ? 'Remove' : 'Add'} bookmark for ${tag}` : (tag as string);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<button
|
||||||
|
aria-label={ariaLabel}
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
className={cn(
|
className={cn(
|
||||||
'group m-1.5 flex w-[225px] cursor-pointer gap-2 rounded px-2 py-2.5 !pr-3 text-sm !opacity-100 focus:ring-0 radix-disabled:pointer-events-none radix-disabled:opacity-50',
|
'group m-1.5 flex w-[225px] cursor-pointer gap-2 rounded bg-transparent px-2 py-2.5 !pr-3 text-sm !opacity-100 focus:ring-0 radix-disabled:pointer-events-none radix-disabled:opacity-50',
|
||||||
'hover:bg-black/5 dark:hover:bg-white/5',
|
highlightSelected && selected ? 'bg-surface-secondary' : '',
|
||||||
highlightSelected && selected && 'bg-black/5 dark:bg-white/5',
|
ctx === 'header' ? 'hover:bg-header-hover' : 'hover:bg-surface-hover',
|
||||||
)}
|
)}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
{...rest}
|
{...rest}
|
||||||
|
|
@ -48,25 +67,14 @@ const BookmarkItem: FC<MenuItemProps> = ({
|
||||||
>
|
>
|
||||||
<div className="flex grow items-center justify-between gap-2">
|
<div className="flex grow items-center justify-between gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{icon ? (
|
{renderIcon()}
|
||||||
icon
|
|
||||||
) : isLoading ? (
|
|
||||||
<Spinner className="size-4" />
|
|
||||||
) : selected ? (
|
|
||||||
<BookmarkFilledIcon className="size-4" />
|
|
||||||
) : (
|
|
||||||
<BookmarkIcon className="size-4" />
|
|
||||||
)}
|
|
||||||
<div style={breakWordStyle}>{tag}</div>
|
<div style={breakWordStyle}>{tag}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{count !== undefined && (
|
{count !== undefined && (
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end">
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className="ml-auto w-7 min-w-max whitespace-nowrap rounded-md bg-surface-secondary px-2.5 py-0.5 text-center text-xs font-medium leading-5 text-text-secondary"
|
||||||
'ml-auto w-7 min-w-max whitespace-nowrap rounded-md bg-white px-2.5 py-0.5 text-center text-xs font-medium leading-5 text-gray-600',
|
|
||||||
'dark:bg-gray-800 dark:text-white',
|
|
||||||
)}
|
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
{count}
|
{count}
|
||||||
|
|
@ -74,7 +82,8 @@ const BookmarkItem: FC<MenuItemProps> = ({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default BookmarkItem;
|
export default BookmarkItem;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import type { FC } from 'react';
|
||||||
import { useBookmarkContext } from '~/Providers/BookmarkContext';
|
import { useBookmarkContext } from '~/Providers/BookmarkContext';
|
||||||
import BookmarkItem from './BookmarkItem';
|
import BookmarkItem from './BookmarkItem';
|
||||||
interface BookmarkItemsProps {
|
interface BookmarkItemsProps {
|
||||||
|
ctx: 'header' | 'nav';
|
||||||
tags: string[];
|
tags: string[];
|
||||||
handleSubmit: (tag: string) => Promise<void>;
|
handleSubmit: (tag: string) => Promise<void>;
|
||||||
header: React.ReactNode;
|
header: React.ReactNode;
|
||||||
|
|
@ -9,6 +10,7 @@ interface BookmarkItemsProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const BookmarkItems: FC<BookmarkItemsProps> = ({
|
const BookmarkItems: FC<BookmarkItemsProps> = ({
|
||||||
|
ctx,
|
||||||
tags,
|
tags,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
header,
|
header,
|
||||||
|
|
@ -19,9 +21,10 @@ const BookmarkItems: FC<BookmarkItemsProps> = ({
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{header}
|
{header}
|
||||||
<div className="my-1.5 h-px bg-black/10 dark:bg-white/10" role="none" />
|
<div className="my-1.5 h-px" role="none" />
|
||||||
{bookmarks.map((bookmark) => (
|
{bookmarks.map((bookmark) => (
|
||||||
<BookmarkItem
|
<BookmarkItem
|
||||||
|
ctx={ctx}
|
||||||
key={bookmark.tag}
|
key={bookmark.tag}
|
||||||
tag={bookmark.tag}
|
tag={bookmark.tag}
|
||||||
selected={tags.includes(bookmark.tag)}
|
selected={tags.includes(bookmark.tag)}
|
||||||
|
|
|
||||||
|
|
@ -14,14 +14,14 @@ const FileContainer = ({
|
||||||
const fileType = getFileType(file.type);
|
const fileType = getFileType(file.type);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group relative inline-block text-sm text-black/70 dark:text-white/90">
|
<div className="group relative inline-block text-sm text-text-primary">
|
||||||
<div className="relative overflow-hidden rounded-xl border border-gray-200 dark:border-gray-600">
|
<div className="relative overflow-hidden rounded-xl border border-border-medium">
|
||||||
<div className="w-60 p-2 dark:bg-gray-600">
|
<div className="w-60 bg-surface-active p-2">
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-row items-center gap-2">
|
||||||
<FilePreview file={file} fileType={fileType} className="relative" />
|
<FilePreview file={file} fileType={fileType} className="relative" />
|
||||||
<div className="overflow-hidden">
|
<div className="overflow-hidden">
|
||||||
<div className="truncate font-medium">{file.filename}</div>
|
<div className="truncate font-medium">{file.filename}</div>
|
||||||
<div className="truncate text-gray-300">{fileType.title}</div>
|
<div className="truncate text-text-secondary">{fileType.title}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ export default function OptionsPopover({
|
||||||
{presetsDisabled ? null : (
|
{presetsDisabled ? null : (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
className="h-auto w-[150px] justify-start rounded-md border border-gray-300/50 bg-transparent px-2 py-1 text-xs font-medium font-normal text-black hover:bg-gray-100 hover:text-black focus:ring-1 focus:ring-green-500/90 dark:border-gray-500/50 dark:bg-transparent dark:text-white dark:hover:bg-gray-600 dark:focus:ring-white"
|
className="h-auto w-[150px] justify-start rounded-md border border-gray-300/50 bg-transparent px-2 py-1 text-xs font-normal text-black hover:bg-gray-100 hover:text-black focus:ring-1 focus:ring-ring-primary dark:border-gray-500/50 dark:bg-transparent dark:text-white dark:hover:bg-gray-600 dark:focus:ring-white"
|
||||||
onClick={saveAsPreset}
|
onClick={saveAsPreset}
|
||||||
>
|
>
|
||||||
<Save className="mr-1 w-[14px]" />
|
<Save className="mr-1 w-[14px]" />
|
||||||
|
|
@ -77,7 +77,7 @@ export default function OptionsPopover({
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
className={cn(
|
className={cn(
|
||||||
'ml-auto h-auto bg-transparent px-3 py-2 text-xs font-medium font-normal text-black hover:bg-gray-100 hover:text-black dark:bg-transparent dark:text-white dark:hover:bg-gray-700 dark:hover:text-white',
|
'ml-auto h-auto bg-transparent px-3 py-2 text-xs font-normal text-black hover:bg-gray-100 hover:text-black dark:bg-transparent dark:text-white dark:hover:bg-gray-700 dark:hover:text-white',
|
||||||
removeFocusOutlines,
|
removeFocusOutlines,
|
||||||
)}
|
)}
|
||||||
onClick={closePopover}
|
onClick={closePopover}
|
||||||
|
|
|
||||||
|
|
@ -1,62 +1,36 @@
|
||||||
import { useEffect, useState, type FC } from 'react';
|
import { useState, type FC } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { Constants } from 'librechat-data-provider';
|
||||||
import { TConversation } from 'librechat-data-provider';
|
|
||||||
import { Content, Portal, Root, Trigger } from '@radix-ui/react-popover';
|
import { Content, Portal, Root, Trigger } from '@radix-ui/react-popover';
|
||||||
import { BookmarkFilledIcon, BookmarkIcon } from '@radix-ui/react-icons';
|
import { BookmarkFilledIcon, BookmarkIcon } from '@radix-ui/react-icons';
|
||||||
import { useConversationTagsQuery, useTagConversationMutation } from '~/data-provider';
|
import { useConversationTagsQuery, useTagConversationMutation } from '~/data-provider';
|
||||||
import { BookmarkMenuItems } from './Bookmarks/BookmarkMenuItems';
|
import { BookmarkMenuItems } from './Bookmarks/BookmarkMenuItems';
|
||||||
import { BookmarkContext } from '~/Providers/BookmarkContext';
|
import { BookmarkContext } from '~/Providers/BookmarkContext';
|
||||||
|
import { useLocalize, useBookmarkSuccess } from '~/hooks';
|
||||||
import { Spinner } from '~/components';
|
import { Spinner } from '~/components';
|
||||||
import { useLocalize } from '~/hooks';
|
|
||||||
import { cn } from '~/utils';
|
import { cn } from '~/utils';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
const SAVED_TAG = 'Saved';
|
const SAVED_TAG = 'Saved';
|
||||||
const BookmarkMenu: FC = () => {
|
const BookmarkMenu: FC = () => {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
const activeConvo = useRecoilValue(store.conversationByIndex(0));
|
const conversation = useRecoilValue(store.conversationByIndex(0));
|
||||||
|
const conversationId = conversation?.conversationId ?? '';
|
||||||
const globalConvo = useRecoilValue(store.conversation) ?? ({} as TConversation);
|
const onSuccess = useBookmarkSuccess(conversationId);
|
||||||
const [tags, setTags] = useState<string[]>();
|
const [tags, setTags] = useState<string[]>(conversation?.tags || []);
|
||||||
|
|
||||||
const [open, setIsOpen] = useState(false);
|
const [open, setIsOpen] = useState(false);
|
||||||
const [conversation, setConversation] = useState<TConversation>();
|
|
||||||
|
|
||||||
let thisConversation: TConversation | null | undefined;
|
const { mutateAsync, isLoading } = useTagConversationMutation(conversationId);
|
||||||
if (location.state?.from?.pathname.includes('/chat')) {
|
|
||||||
thisConversation = globalConvo;
|
|
||||||
} else {
|
|
||||||
thisConversation = activeConvo;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { mutateAsync, isLoading } = useTagConversationMutation(
|
|
||||||
thisConversation?.conversationId ?? '',
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data } = useConversationTagsQuery();
|
const { data } = useConversationTagsQuery();
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
(!conversation && thisConversation) ||
|
|
||||||
(conversation &&
|
|
||||||
thisConversation &&
|
|
||||||
conversation.conversationId !== thisConversation.conversationId)
|
|
||||||
) {
|
|
||||||
setConversation(thisConversation);
|
|
||||||
setTags(thisConversation.tags ?? []);
|
|
||||||
}
|
|
||||||
if (tags === undefined && conversation) {
|
|
||||||
setTags(conversation.tags ?? []);
|
|
||||||
}
|
|
||||||
}, [thisConversation, conversation, tags]);
|
|
||||||
|
|
||||||
const isActiveConvo =
|
const isActiveConvo =
|
||||||
thisConversation &&
|
conversation &&
|
||||||
thisConversation.conversationId &&
|
conversationId &&
|
||||||
thisConversation.conversationId !== 'new' &&
|
conversationId !== Constants.NEW_CONVO &&
|
||||||
thisConversation.conversationId !== 'search';
|
conversationId !== 'search';
|
||||||
|
|
||||||
if (!isActiveConvo) {
|
if (!isActiveConvo) {
|
||||||
return <></>;
|
return <></>;
|
||||||
|
|
@ -70,59 +44,59 @@ const BookmarkMenu: FC = () => {
|
||||||
if (open && tags && tags.length > 0) {
|
if (open && tags && tags.length > 0) {
|
||||||
setIsOpen(open);
|
setIsOpen(open);
|
||||||
} else {
|
} else {
|
||||||
if (thisConversation && thisConversation.conversationId) {
|
if (conversation && conversationId) {
|
||||||
await mutateAsync({
|
await mutateAsync(
|
||||||
conversationId: thisConversation.conversationId,
|
{
|
||||||
tags: [SAVED_TAG],
|
tags: [SAVED_TAG],
|
||||||
});
|
},
|
||||||
setTags([SAVED_TAG]);
|
{
|
||||||
setConversation({ ...thisConversation, tags: [SAVED_TAG] });
|
onSuccess: (newTags: string[]) => {
|
||||||
|
setTags(newTags);
|
||||||
|
onSuccess(newTags);
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
console.error('Error adding bookmark');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderButtonContent = () => {
|
||||||
|
if (isLoading) {
|
||||||
|
return <Spinner />;
|
||||||
|
}
|
||||||
|
if (tags && tags.length > 0) {
|
||||||
|
return <BookmarkFilledIcon className="icon-sm" />;
|
||||||
|
}
|
||||||
|
return <BookmarkIcon className="icon-sm" />;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Root open={open} onOpenChange={onOpenChange}>
|
<Root open={open} onOpenChange={onOpenChange}>
|
||||||
<Trigger asChild>
|
<Trigger asChild>
|
||||||
<button
|
<button
|
||||||
|
id="header-bookmarks-menu"
|
||||||
className={cn(
|
className={cn(
|
||||||
'pointer-cursor relative flex flex-col rounded-md border border-gray-100 bg-white text-left focus:outline-none focus:ring-0 dark:border-gray-700 dark:bg-gray-800 sm:text-sm',
|
'pointer-cursor relative flex flex-col rounded-md border border-border-light bg-transparent text-left focus:outline-none focus:ring-0 sm:text-sm',
|
||||||
'hover:bg-gray-50 radix-state-open:bg-gray-50 dark:hover:bg-gray-700 dark:radix-state-open:bg-gray-700',
|
'hover:bg-header-button-hover radix-state-open:bg-header-button-hover',
|
||||||
'z-50 flex h-[40px] min-w-4 flex-none items-center justify-center px-3 focus:outline-offset-2 focus:ring-0 focus-visible:ring-2 focus-visible:ring-gray-500',
|
'z-50 flex h-[40px] min-w-4 flex-none items-center justify-center px-3 focus:outline-offset-2 focus:ring-0 focus-visible:ring-2 focus-visible:ring-ring-primary ',
|
||||||
)}
|
)}
|
||||||
title={localize('com_ui_bookmarks')}
|
title={localize('com_ui_bookmarks')}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{renderButtonContent()}
|
||||||
<Spinner />
|
|
||||||
) : tags && tags.length > 0 ? (
|
|
||||||
<BookmarkFilledIcon className="icon-sm" />
|
|
||||||
) : (
|
|
||||||
<BookmarkIcon className="icon-sm" />
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
</Trigger>
|
</Trigger>
|
||||||
<Portal>
|
<Portal>
|
||||||
<Content
|
<Content
|
||||||
className={cn(
|
className="mt-2 grid max-h-[500px] w-full min-w-[240px] overflow-y-auto rounded-lg border border-border-medium bg-header-primary text-text-primary shadow-lg"
|
||||||
'grid w-full',
|
|
||||||
'mt-2 min-w-[240px] overflow-y-auto rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-700 dark:text-white',
|
|
||||||
'max-h-[500px]',
|
|
||||||
)}
|
|
||||||
side="bottom"
|
side="bottom"
|
||||||
align="start"
|
align="start"
|
||||||
>
|
>
|
||||||
{data && conversation && (
|
{data && conversation && (
|
||||||
// Display all bookmarks registered by the user and highlight the tags of the currently selected conversation
|
|
||||||
<BookmarkContext.Provider value={{ bookmarks: data }}>
|
<BookmarkContext.Provider value={{ bookmarks: data }}>
|
||||||
<BookmarkMenuItems
|
<BookmarkMenuItems conversation={conversation} tags={tags ?? []} setTags={setTags} />
|
||||||
// Currently selected conversation
|
|
||||||
conversation={conversation}
|
|
||||||
setConversation={setConversation}
|
|
||||||
// Tags in the conversation
|
|
||||||
tags={tags ?? []}
|
|
||||||
// Update tags in the conversation
|
|
||||||
setTags={setTags}
|
|
||||||
/>
|
|
||||||
</BookmarkContext.Provider>
|
</BookmarkContext.Provider>
|
||||||
)}
|
)}
|
||||||
</Content>
|
</Content>
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,37 @@
|
||||||
import { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { BookmarkPlusIcon } from 'lucide-react';
|
import { BookmarkPlusIcon } from 'lucide-react';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import type { TConversation } from 'librechat-data-provider';
|
import type { TConversation } from 'librechat-data-provider';
|
||||||
import { BookmarkItems, BookmarkEditDialog } from '~/components/Bookmarks';
|
import { BookmarkItems, BookmarkEditDialog } from '~/components/Bookmarks';
|
||||||
import { useTagConversationMutation } from '~/data-provider';
|
import { useTagConversationMutation } from '~/data-provider';
|
||||||
|
import { useLocalize, useBookmarkSuccess } from '~/hooks';
|
||||||
import { NotificationSeverity } from '~/common';
|
import { NotificationSeverity } from '~/common';
|
||||||
import { useToastContext } from '~/Providers';
|
import { useToastContext } from '~/Providers';
|
||||||
import { useLocalize } from '~/hooks';
|
|
||||||
|
|
||||||
export const BookmarkMenuItems: FC<{
|
export const BookmarkMenuItems: FC<{
|
||||||
conversation: TConversation;
|
conversation: TConversation;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
setTags: (tags: string[]) => void;
|
setTags: React.Dispatch<React.SetStateAction<string[]>>;
|
||||||
setConversation: (conversation: TConversation) => void;
|
}> = ({ conversation, tags, setTags }) => {
|
||||||
}> = ({ conversation, tags, setTags, setConversation }) => {
|
|
||||||
const { showToast } = useToastContext();
|
const { showToast } = useToastContext();
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
|
||||||
const { mutateAsync } = useTagConversationMutation(conversation?.conversationId ?? '');
|
const conversationId = conversation?.conversationId ?? '';
|
||||||
|
const onSuccess = useBookmarkSuccess(conversationId);
|
||||||
|
|
||||||
|
const { mutateAsync } = useTagConversationMutation(conversationId);
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
async (tag: string): Promise<void> => {
|
async (tag: string): Promise<void> => {
|
||||||
if (tags !== undefined && conversation?.conversationId) {
|
if (tags !== undefined && conversationId) {
|
||||||
const newTags = tags.includes(tag) ? tags.filter((t) => t !== tag) : [...tags, tag];
|
const newTags = tags.includes(tag) ? tags.filter((t) => t !== tag) : [...tags, tag];
|
||||||
await mutateAsync(
|
await mutateAsync(
|
||||||
{
|
{
|
||||||
conversationId: conversation.conversationId,
|
|
||||||
tags: newTags,
|
tags: newTags,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: (newTags: string[]) => {
|
onSuccess: (newTags: string[]) => {
|
||||||
setTags(newTags);
|
setTags(newTags);
|
||||||
setConversation({ ...conversation, tags: newTags });
|
onSuccess(newTags);
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
showToast({
|
showToast({
|
||||||
|
|
@ -42,11 +43,12 @@ export const BookmarkMenuItems: FC<{
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[tags, conversation],
|
[tags, conversationId, mutateAsync, setTags, onSuccess, showToast],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BookmarkItems
|
<BookmarkItems
|
||||||
|
ctx="header"
|
||||||
tags={tags}
|
tags={tags}
|
||||||
handleSubmit={handleSubmit}
|
handleSubmit={handleSubmit}
|
||||||
header={
|
header={
|
||||||
|
|
@ -58,7 +60,7 @@ export const BookmarkMenuItems: FC<{
|
||||||
trigger={
|
trigger={
|
||||||
<div
|
<div
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
className="group m-1.5 flex cursor-pointer gap-2 rounded px-2 !pr-3.5 pb-2.5 pt-3 text-sm !opacity-100 hover:bg-black/5 focus:ring-0 radix-disabled:pointer-events-none radix-disabled:opacity-50 dark:hover:bg-white/5"
|
className="group m-1.5 flex cursor-pointer gap-2 rounded px-2 !pr-3.5 pb-2.5 pt-3 text-sm !opacity-100 hover:bg-header-hover focus:ring-0 radix-disabled:pointer-events-none radix-disabled:opacity-50"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
<div className="flex grow items-center justify-between gap-2">
|
<div className="flex grow items-center justify-between gap-2">
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ import { useLocation } from 'react-router-dom';
|
||||||
import { TConversation } from 'librechat-data-provider';
|
import { TConversation } from 'librechat-data-provider';
|
||||||
import { Content, Portal, Root, Trigger } from '@radix-ui/react-popover';
|
import { Content, Portal, Root, Trigger } from '@radix-ui/react-popover';
|
||||||
import { BookmarkFilledIcon, BookmarkIcon } from '@radix-ui/react-icons';
|
import { BookmarkFilledIcon, BookmarkIcon } from '@radix-ui/react-icons';
|
||||||
import { useGetConversationTags } from 'librechat-data-provider/react-query';
|
|
||||||
import { BookmarkContext } from '~/Providers/BookmarkContext';
|
import { BookmarkContext } from '~/Providers/BookmarkContext';
|
||||||
|
import { useGetConversationTags } from '~/data-provider';
|
||||||
import BookmarkNavItems from './BookmarkNavItems';
|
import BookmarkNavItems from './BookmarkNavItems';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
import { cn } from '~/utils';
|
import { cn } from '~/utils';
|
||||||
|
|
@ -39,22 +39,22 @@ const BookmarkNav: FC<BookmarkNavProps> = ({ tags, setTags }: BookmarkNavProps)
|
||||||
<Trigger asChild>
|
<Trigger asChild>
|
||||||
<button
|
<button
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative mt-1 flex h-10 w-full cursor-pointer items-center gap-1 rounded-lg border-white bg-gray-50 px-1 py-2 text-black transition-colors duration-200 focus-within:bg-gray-200 hover:bg-gray-200 dark:bg-gray-850 dark:text-white dark:focus-within:bg-gray-800 dark:hover:bg-gray-800',
|
'relative mt-1 flex h-10 w-full cursor-pointer items-center gap-1 rounded-lg border-border-light bg-transparent px-1 py-2 text-text-primary transition-colors duration-200 focus-within:bg-surface-hover hover:bg-surface-hover',
|
||||||
open ? 'bg-gray-200 dark:bg-gray-800' : '',
|
open ? 'bg-surface-hover' : '',
|
||||||
)}
|
)}
|
||||||
id="presets-button"
|
id="show-bookmarks"
|
||||||
data-testid="presets-button"
|
data-testid="show-bookmarks"
|
||||||
title={localize('com_endpoint_examples')}
|
title={localize('com_ui_bookmarks')}
|
||||||
>
|
>
|
||||||
<div className="relative flex h-8 w-8 items-center justify-center rounded-full p-1 dark:text-white">
|
<div className="relative flex h-8 w-8 items-center justify-center rounded-full p-1 text-text-primary">
|
||||||
{tags.length > 0 ? (
|
{tags.length > 0 ? (
|
||||||
<BookmarkFilledIcon className="h-5 w-5" />
|
<BookmarkFilledIcon className="h-5 w-5" />
|
||||||
) : (
|
) : (
|
||||||
<BookmarkIcon className="h-5 w-5" />
|
<BookmarkIcon className="h-5 w-5" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="grow overflow-hidden whitespace-nowrap text-left text-sm text-black dark:text-gray-100">
|
<div className="grow overflow-hidden whitespace-nowrap text-left text-sm text-text-primary">
|
||||||
{tags.length > 0 ? tags.join(',') : localize('com_ui_bookmarks')}
|
{tags.length > 0 ? tags.join(', ') : localize('com_ui_bookmarks')}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</Trigger>
|
</Trigger>
|
||||||
|
|
@ -63,7 +63,7 @@ const BookmarkNav: FC<BookmarkNavProps> = ({ tags, setTags }: BookmarkNavProps)
|
||||||
<Content
|
<Content
|
||||||
side="bottom"
|
side="bottom"
|
||||||
align="start"
|
align="start"
|
||||||
className="mt-2 max-h-96 min-w-[240px] overflow-y-auto rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-700 dark:text-white lg:max-h-96"
|
className="mt-2 max-h-96 min-w-[240px] overflow-y-auto rounded-lg border border-border-medium bg-surface-primary-alt text-text-primary shadow-lg lg:max-h-96"
|
||||||
>
|
>
|
||||||
{data && conversation && (
|
{data && conversation && (
|
||||||
// Display bookmarks and highlight the selected tag
|
// Display bookmarks and highlight the selected tag
|
||||||
|
|
|
||||||
|
|
@ -39,12 +39,11 @@ const BookmarkNavItems: FC<{
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('bookmarks', bookmarks);
|
|
||||||
|
|
||||||
if (bookmarks.length === 0) {
|
if (bookmarks.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<BookmarkItem
|
<BookmarkItem
|
||||||
|
ctx="nav"
|
||||||
tag={localize('com_ui_no_bookmarks')}
|
tag={localize('com_ui_no_bookmarks')}
|
||||||
data-testid="bookmark-item-clear"
|
data-testid="bookmark-item-clear"
|
||||||
handleSubmit={() => Promise.resolve()}
|
handleSubmit={() => Promise.resolve()}
|
||||||
|
|
@ -58,11 +57,13 @@ const BookmarkNavItems: FC<{
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<BookmarkItems
|
<BookmarkItems
|
||||||
|
ctx="nav"
|
||||||
tags={tags}
|
tags={tags}
|
||||||
handleSubmit={handleSubmit}
|
handleSubmit={handleSubmit}
|
||||||
highlightSelected={true}
|
highlightSelected={true}
|
||||||
header={
|
header={
|
||||||
<BookmarkItem
|
<BookmarkItem
|
||||||
|
ctx="nav"
|
||||||
tag="Clear all"
|
tag="Clear all"
|
||||||
data-testid="bookmark-item-clear"
|
data-testid="bookmark-item-clear"
|
||||||
handleSubmit={clear}
|
handleSubmit={clear}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
|
||||||
/* Reason: SearchContext is not specifying potential undefined type */
|
|
||||||
import { useCallback, useEffect, useState, useMemo, memo } from 'react';
|
import { useCallback, useEffect, useState, useMemo, memo } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
import type { ConversationListResponse } from 'librechat-data-provider';
|
import type { ConversationListResponse } from 'librechat-data-provider';
|
||||||
import {
|
import {
|
||||||
useMediaQuery,
|
useMediaQuery,
|
||||||
|
|
@ -159,7 +157,7 @@ const Nav = ({ navVisible, setNavVisible }) => {
|
||||||
toggleNav={itemToggleNav}
|
toggleNav={itemToggleNav}
|
||||||
subHeaders={
|
subHeaders={
|
||||||
<>
|
<>
|
||||||
{isSearchEnabled && <SearchBar clearSearch={clearSearch} />}
|
{isSearchEnabled && <SearchBar clearSearch={clearSearch} />}
|
||||||
<BookmarkNav tags={tags} setTags={setTags} />
|
<BookmarkNav tags={tags} setTags={setTags} />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,12 +58,12 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className="relative mt-1 flex h-10 cursor-pointer items-center gap-3 rounded-lg border-white bg-gray-50 px-2 px-3 py-2 text-black transition-colors duration-200 focus-within:bg-gray-200 hover:bg-gray-200 dark:bg-gray-850 dark:text-white dark:focus-within:bg-gray-800 dark:hover:bg-gray-800"
|
className="relative mt-1 flex h-10 cursor-pointer items-center gap-3 rounded-lg border-border-medium px-3 py-2 text-text-primary transition-colors duration-200 focus-within:bg-surface-hover hover:bg-surface-hover dark:focus-within:bg-surface-hover"
|
||||||
>
|
>
|
||||||
{<Search className="absolute left-3 h-4 w-4" />}
|
{<Search className="absolute left-3 h-4 w-4" />}
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="m-0 mr-0 w-full border-none bg-transparent p-0 pl-7 text-sm leading-tight placeholder-gray-500 placeholder-opacity-100 outline-none dark:placeholder-white dark:placeholder-opacity-100"
|
className="m-0 mr-0 w-full border-none bg-transparent p-0 pl-7 text-sm leading-tight placeholder-text-secondary placeholder-opacity-100 outline-none dark:placeholder-opacity-100"
|
||||||
value={text}
|
value={text}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,23 @@
|
||||||
import { BookmarkPlusIcon } from 'lucide-react';
|
import { BookmarkPlusIcon } from 'lucide-react';
|
||||||
import { useConversationTagsQuery, useRebuildConversationTagsMutation } from '~/data-provider';
|
import { useConversationTagsQuery } from '~/data-provider';
|
||||||
import { Button } from '~/components/ui';
|
import { Button } from '~/components/ui';
|
||||||
import { BookmarkContext } from '~/Providers/BookmarkContext';
|
import { BookmarkContext } from '~/Providers/BookmarkContext';
|
||||||
import { BookmarkEditDialog } from '~/components/Bookmarks';
|
import { BookmarkEditDialog } from '~/components/Bookmarks';
|
||||||
import BookmarkTable from './BookmarkTable';
|
import BookmarkTable from './BookmarkTable';
|
||||||
import { Spinner } from '~/components/svg';
|
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
import HoverCardSettings from '~/components/Nav/SettingsTabs/HoverCardSettings';
|
|
||||||
|
|
||||||
const BookmarkPanel = () => {
|
const BookmarkPanel = () => {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const { mutate, isLoading } = useRebuildConversationTagsMutation();
|
|
||||||
const { data } = useConversationTagsQuery();
|
const { data } = useConversationTagsQuery();
|
||||||
const rebuildTags = () => {
|
|
||||||
mutate({});
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<div className="h-auto max-w-full overflow-x-hidden">
|
<div className="h-auto max-w-full overflow-x-hidden">
|
||||||
<BookmarkContext.Provider value={{ bookmarks: data || [] }}>
|
<BookmarkContext.Provider value={{ bookmarks: data || [] }}>
|
||||||
<BookmarkTable />
|
<BookmarkTable />
|
||||||
<div className="flex justify-between gap-2">
|
<div className="flex justify-between gap-2">
|
||||||
<Button variant="outline" onClick={rebuildTags} className="w-50 text-sm">
|
|
||||||
{isLoading ? (
|
|
||||||
<Spinner />
|
|
||||||
) : (
|
|
||||||
<div className="flex gap-2">
|
|
||||||
{localize('com_ui_bookmarks_rebuild')}
|
|
||||||
<HoverCardSettings side="top" text="com_nav_info_bookmarks_rebuild" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
<BookmarkEditDialog
|
<BookmarkEditDialog
|
||||||
trigger={
|
trigger={
|
||||||
<Button variant="outline" onClick={rebuildTags} className="w-full text-sm">
|
<Button variant="outline" className="w-full text-sm">
|
||||||
<BookmarkPlusIcon className="mr-1 size-4" />
|
<BookmarkPlusIcon className="mr-1 size-4" />
|
||||||
<div className="break-all">{localize('com_ui_bookmarks_new')}</div>
|
<div className="break-all">{localize('com_ui_bookmarks_new')}</div>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,11 @@ const BookmarkTable = () => {
|
||||||
|
|
||||||
const { bookmarks } = useBookmarkContext();
|
const { bookmarks } = useBookmarkContext();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRows(bookmarks?.map((item) => ({ id: item.tag, ...item })) || []);
|
setRows(
|
||||||
|
bookmarks
|
||||||
|
?.map((item) => ({ id: item.tag, ...item }))
|
||||||
|
.sort((a, b) => a.position - b.position) || [],
|
||||||
|
);
|
||||||
}, [bookmarks]);
|
}, [bookmarks]);
|
||||||
|
|
||||||
const moveRow = useCallback((dragIndex: number, hoverIndex: number) => {
|
const moveRow = useCallback((dragIndex: number, hoverIndex: number) => {
|
||||||
|
|
@ -22,13 +26,16 @@ const BookmarkTable = () => {
|
||||||
const updatedRows = [...prevTags];
|
const updatedRows = [...prevTags];
|
||||||
const [movedRow] = updatedRows.splice(dragIndex, 1);
|
const [movedRow] = updatedRows.splice(dragIndex, 1);
|
||||||
updatedRows.splice(hoverIndex, 0, movedRow);
|
updatedRows.splice(hoverIndex, 0, movedRow);
|
||||||
return updatedRows;
|
return updatedRows.map((row, index) => ({ ...row, position: index }));
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const renderRow = useCallback((row: TConversationTag, position: number) => {
|
const renderRow = useCallback(
|
||||||
return <BookmarkTableRow key={row.tag} moveRow={moveRow} row={row} position={position} />;
|
(row: TConversationTag) => {
|
||||||
}, []);
|
return <BookmarkTableRow key={row.tag} moveRow={moveRow} row={row} position={row.position} />;
|
||||||
|
},
|
||||||
|
[moveRow],
|
||||||
|
);
|
||||||
|
|
||||||
const filteredRows = rows.filter((row) =>
|
const filteredRows = rows.filter((row) =>
|
||||||
row.tag.toLowerCase().includes(searchQuery.toLowerCase()),
|
row.tag.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||||
|
|
@ -58,7 +65,7 @@ const BookmarkTable = () => {
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>{currentRows.map((row, i) => renderRow(row, i))}</TableBody>
|
<TableBody>{currentRows.map((row) => renderRow(row))}</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between py-4">
|
<div className="flex items-center justify-between py-4">
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
import { useDrag, useDrop } from 'react-dnd';
|
import { useDrag, useDrop } from 'react-dnd';
|
||||||
import type { TConversationTag } from 'librechat-data-provider';
|
import type { TConversationTag } from 'librechat-data-provider';
|
||||||
import { DeleteBookmarkButton, EditBookmarkButton } from '~/components/Bookmarks';
|
import { DeleteBookmarkButton, EditBookmarkButton } from '~/components/Bookmarks';
|
||||||
|
import { useConversationTagMutation } from '~/data-provider';
|
||||||
import { TableRow, TableCell } from '~/components/ui';
|
import { TableRow, TableCell } from '~/components/ui';
|
||||||
|
import { NotificationSeverity } from '~/common';
|
||||||
|
import { useToastContext } from '~/Providers';
|
||||||
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
||||||
interface BookmarkTableRowProps {
|
interface BookmarkTableRowProps {
|
||||||
row: TConversationTag;
|
row: TConversationTag;
|
||||||
|
|
@ -10,13 +14,39 @@ interface BookmarkTableRowProps {
|
||||||
position: number;
|
position: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DragItem {
|
||||||
|
index: number;
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
const BookmarkTableRow: React.FC<BookmarkTableRowProps> = ({ row, moveRow, position }) => {
|
const BookmarkTableRow: React.FC<BookmarkTableRowProps> = ({ row, moveRow, position }) => {
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
const ref = React.useRef<HTMLTableRowElement>(null);
|
const ref = useRef<HTMLTableRowElement>(null);
|
||||||
|
|
||||||
|
const mutation = useConversationTagMutation(row.tag);
|
||||||
|
const localize = useLocalize();
|
||||||
|
const { showToast } = useToastContext();
|
||||||
|
|
||||||
|
const handleDrop = (item: DragItem) => {
|
||||||
|
const data = {
|
||||||
|
...row,
|
||||||
|
position: item.index,
|
||||||
|
};
|
||||||
|
mutation.mutate(data, {
|
||||||
|
onError: () => {
|
||||||
|
showToast({
|
||||||
|
message: localize('com_ui_bookmarks_update_error'),
|
||||||
|
severity: NotificationSeverity.ERROR,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const [, drop] = useDrop({
|
const [, drop] = useDrop({
|
||||||
accept: 'bookmark',
|
accept: 'bookmark',
|
||||||
hover(item: { index: number }) {
|
drop: (item: DragItem) => handleDrop(item),
|
||||||
|
hover(item: DragItem) {
|
||||||
if (!ref.current) {
|
if (!ref.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -43,7 +73,7 @@ const BookmarkTableRow: React.FC<BookmarkTableRowProps> = ({ row, moveRow, posit
|
||||||
return (
|
return (
|
||||||
<TableRow
|
<TableRow
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className="cursor-move hover:bg-gray-100 dark:hover:bg-gray-800"
|
className="cursor-move hover:bg-surface-secondary"
|
||||||
style={{ opacity: isDragging ? 0.5 : 1 }}
|
style={{ opacity: isDragging ? 0.5 : 1 }}
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
|
|
|
||||||
|
|
@ -3,3 +3,4 @@ export * from './mutations';
|
||||||
export * from './prompts';
|
export * from './prompts';
|
||||||
export * from './queries';
|
export * from './queries';
|
||||||
export * from './roles';
|
export * from './roles';
|
||||||
|
export * from './tags';
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { dataService, MutationKeys, QueryKeys, defaultOrderQuery } from 'librechat-data-provider';
|
import { dataService, MutationKeys, QueryKeys, defaultOrderQuery } from 'librechat-data-provider';
|
||||||
import type t from 'librechat-data-provider';
|
import type t from 'librechat-data-provider';
|
||||||
import type { InfiniteData, UseMutationResult } from '@tanstack/react-query';
|
import type { InfiniteData, UseMutationResult } from '@tanstack/react-query';
|
||||||
|
import useUpdateTagsInConvo from '~/hooks/Conversations/useUpdateTagsInConvo';
|
||||||
import { updateConversationTag } from '~/utils/conversationTags';
|
import { updateConversationTag } from '~/utils/conversationTags';
|
||||||
import { normalizeData } from '~/utils/collection';
|
import { normalizeData } from '~/utils/collection';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
@ -89,89 +90,6 @@ export const useUpdateConversationMutation = (
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const useUpdateTagsInConversation = () => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
// Update the queryClient cache with the new tag when a new tag is added/removed to a conversation
|
|
||||||
const updateTagsInConversation = (conversationId: string, tags: string[]) => {
|
|
||||||
// Update the tags for the current conversation
|
|
||||||
const currentConvo = queryClient.getQueryData<t.TConversation>([
|
|
||||||
QueryKeys.conversation,
|
|
||||||
conversationId,
|
|
||||||
]);
|
|
||||||
if (!currentConvo) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedConvo = {
|
|
||||||
...currentConvo,
|
|
||||||
tags,
|
|
||||||
} as t.TConversation;
|
|
||||||
queryClient.setQueryData([QueryKeys.conversation, conversationId], updatedConvo);
|
|
||||||
queryClient.setQueryData<t.ConversationData>([QueryKeys.allConversations], (convoData) => {
|
|
||||||
if (!convoData) {
|
|
||||||
return convoData;
|
|
||||||
}
|
|
||||||
return updateConvoFields(
|
|
||||||
convoData,
|
|
||||||
{
|
|
||||||
conversationId: currentConvo.conversationId,
|
|
||||||
tags: updatedConvo.tags,
|
|
||||||
} as t.TConversation,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// update the tag to newTag in all conversations when a tag is updated to a newTag
|
|
||||||
// The difference with updateTagsInConversation is that it adds or removes tags for a specific conversation,
|
|
||||||
// whereas this function is for changing the title of a specific tag.
|
|
||||||
const replaceTagsInAllConversations = (tag: string, newTag: string) => {
|
|
||||||
const data = queryClient.getQueryData<InfiniteData<ConversationListResponse>>([
|
|
||||||
QueryKeys.allConversations,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const conversationIdsWithTag = [] as string[];
|
|
||||||
|
|
||||||
// update tag to newTag in all conversations
|
|
||||||
const newData = JSON.parse(JSON.stringify(data)) as InfiniteData<ConversationListResponse>;
|
|
||||||
for (let pageIndex = 0; pageIndex < newData.pages.length; pageIndex++) {
|
|
||||||
const page = newData.pages[pageIndex];
|
|
||||||
page.conversations = page.conversations.map((conversation) => {
|
|
||||||
if (conversation.conversationId && conversation.tags?.includes(tag)) {
|
|
||||||
conversationIdsWithTag.push(conversation.conversationId);
|
|
||||||
conversation.tags = conversation.tags.map((t) => (t === tag ? newTag : t));
|
|
||||||
}
|
|
||||||
return conversation;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
queryClient.setQueryData<InfiniteData<ConversationListResponse>>(
|
|
||||||
[QueryKeys.allConversations],
|
|
||||||
newData,
|
|
||||||
);
|
|
||||||
|
|
||||||
// update the tag to newTag from the cache of each conversation
|
|
||||||
for (let i = 0; i < conversationIdsWithTag.length; i++) {
|
|
||||||
const conversationId = conversationIdsWithTag[i];
|
|
||||||
const conversation = queryClient.getQueryData<t.TConversation>([
|
|
||||||
QueryKeys.conversation,
|
|
||||||
conversationId,
|
|
||||||
]);
|
|
||||||
if (conversation && conversation.tags) {
|
|
||||||
const updatedConvo = {
|
|
||||||
...conversation,
|
|
||||||
tags: conversation.tags.map((t) => (t === tag ? newTag : t)),
|
|
||||||
} as t.TConversation;
|
|
||||||
queryClient.setQueryData<t.TConversation>(
|
|
||||||
[QueryKeys.conversation, conversationId],
|
|
||||||
updatedConvo,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return { updateTagsInConversation, replaceTagsInAllConversations };
|
|
||||||
};
|
|
||||||
/**
|
/**
|
||||||
* Add or remove tags for a conversation
|
* Add or remove tags for a conversation
|
||||||
*/
|
*/
|
||||||
|
|
@ -179,7 +97,7 @@ export const useTagConversationMutation = (
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
): UseMutationResult<t.TTagConversationResponse, unknown, t.TTagConversationRequest, unknown> => {
|
): UseMutationResult<t.TTagConversationResponse, unknown, t.TTagConversationRequest, unknown> => {
|
||||||
const query = useConversationTagsQuery();
|
const query = useConversationTagsQuery();
|
||||||
const { updateTagsInConversation } = useUpdateTagsInConversation();
|
const { updateTagsInConversation } = useUpdateTagsInConvo();
|
||||||
return useMutation(
|
return useMutation(
|
||||||
(payload: t.TTagConversationRequest) =>
|
(payload: t.TTagConversationRequest) =>
|
||||||
dataService.addTagToConversation(conversationId, payload),
|
dataService.addTagToConversation(conversationId, payload),
|
||||||
|
|
@ -385,21 +303,6 @@ export const useDeleteSharedLinkMutation = (
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// If the number of conversations tagged is incorrect, recalculate the tag information.
|
|
||||||
export const useRebuildConversationTagsMutation = (): UseMutationResult<
|
|
||||||
t.TConversationTagsResponse,
|
|
||||||
unknown,
|
|
||||||
unknown,
|
|
||||||
unknown
|
|
||||||
> => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
return useMutation(() => dataService.rebuildConversationTags(), {
|
|
||||||
onSuccess: (_data) => {
|
|
||||||
queryClient.setQueryData<t.TConversationTag[]>([QueryKeys.conversationTags], _data);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add a tag or update tag information (tag, description, position, etc.)
|
// Add a tag or update tag information (tag, description, position, etc.)
|
||||||
export const useConversationTagMutation = (
|
export const useConversationTagMutation = (
|
||||||
tag?: string,
|
tag?: string,
|
||||||
|
|
@ -407,7 +310,7 @@ export const useConversationTagMutation = (
|
||||||
): UseMutationResult<t.TConversationTagResponse, unknown, t.TConversationTagRequest, unknown> => {
|
): UseMutationResult<t.TConversationTagResponse, unknown, t.TConversationTagRequest, unknown> => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { ..._options } = options || {};
|
const { ..._options } = options || {};
|
||||||
const { updateTagsInConversation, replaceTagsInAllConversations } = useUpdateTagsInConversation();
|
const { updateTagsInConversation, replaceTagsInAllConversations } = useUpdateTagsInConvo();
|
||||||
return useMutation(
|
return useMutation(
|
||||||
(payload: t.TConversationTagRequest) =>
|
(payload: t.TConversationTagRequest) =>
|
||||||
tag
|
tag
|
||||||
|
|
@ -427,6 +330,9 @@ export const useConversationTagMutation = (
|
||||||
},
|
},
|
||||||
] as t.TConversationTag[];
|
] as t.TConversationTag[];
|
||||||
}
|
}
|
||||||
|
if (!tag) {
|
||||||
|
return [...data, _data].sort((a, b) => a.position - b.position);
|
||||||
|
}
|
||||||
return updateConversationTag(data, vars, _data, tag);
|
return updateConversationTag(data, vars, _data, tag);
|
||||||
});
|
});
|
||||||
if (vars.addToConversation && vars.conversationId && _data.tag) {
|
if (vars.addToConversation && vars.conversationId && _data.tag) {
|
||||||
|
|
|
||||||
19
client/src/data-provider/tags.ts
Normal file
19
client/src/data-provider/tags.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import type { UseQueryOptions, QueryObserverResult } from '@tanstack/react-query';
|
||||||
|
import type { TConversationTagsResponse } from 'librechat-data-provider';
|
||||||
|
import { QueryKeys, dataService } from 'librechat-data-provider';
|
||||||
|
|
||||||
|
export const useGetConversationTags = (
|
||||||
|
config?: UseQueryOptions<TConversationTagsResponse>,
|
||||||
|
): QueryObserverResult<TConversationTagsResponse> => {
|
||||||
|
return useQuery<TConversationTagsResponse>(
|
||||||
|
[QueryKeys.conversationTags],
|
||||||
|
() => dataService.getConversationTags(),
|
||||||
|
{
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
refetchOnReconnect: false,
|
||||||
|
refetchOnMount: false,
|
||||||
|
...config,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -6,7 +6,9 @@ export { default as useConversation } from './useConversation';
|
||||||
export { default as useGenerateConvo } from './useGenerateConvo';
|
export { default as useGenerateConvo } from './useGenerateConvo';
|
||||||
export { default as useConversations } from './useConversations';
|
export { default as useConversations } from './useConversations';
|
||||||
export { default as useDebouncedInput } from './useDebouncedInput';
|
export { default as useDebouncedInput } from './useDebouncedInput';
|
||||||
|
export { default as useBookmarkSuccess } from './useBookmarkSuccess';
|
||||||
export { default as useNavigateToConvo } from './useNavigateToConvo';
|
export { default as useNavigateToConvo } from './useNavigateToConvo';
|
||||||
export { default as useSetIndexOptions } from './useSetIndexOptions';
|
export { default as useSetIndexOptions } from './useSetIndexOptions';
|
||||||
export { default as useParameterEffects } from './useParameterEffects';
|
export { default as useParameterEffects } from './useParameterEffects';
|
||||||
|
export { default as useUpdateTagsInConvo } from './useUpdateTagsInConvo';
|
||||||
export { default as useExportConversation } from './useExportConversation';
|
export { default as useExportConversation } from './useExportConversation';
|
||||||
|
|
|
||||||
27
client/src/hooks/Conversations/useBookmarkSuccess.ts
Normal file
27
client/src/hooks/Conversations/useBookmarkSuccess.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { useSetRecoilState } from 'recoil';
|
||||||
|
import useUpdateTagsInConvo from './useUpdateTagsInConvo';
|
||||||
|
import store from '~/store';
|
||||||
|
|
||||||
|
const useBookmarkSuccess = (conversationId: string) => {
|
||||||
|
const setConversation = useSetRecoilState(store.conversationByIndex(0));
|
||||||
|
const { updateTagsInConversation } = useUpdateTagsInConvo();
|
||||||
|
|
||||||
|
return (newTags: string[]) => {
|
||||||
|
if (!conversationId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateTagsInConversation(conversationId, newTags);
|
||||||
|
setConversation((prev) => {
|
||||||
|
if (prev) {
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
tags: newTags,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
console.error('Conversation not found for bookmark/tags update');
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useBookmarkSuccess;
|
||||||
92
client/src/hooks/Conversations/useUpdateTagsInConvo.ts
Normal file
92
client/src/hooks/Conversations/useUpdateTagsInConvo.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { QueryKeys } from 'librechat-data-provider';
|
||||||
|
import type { ConversationListResponse } from 'librechat-data-provider';
|
||||||
|
import type { InfiniteData } from '@tanstack/react-query';
|
||||||
|
import type t from 'librechat-data-provider';
|
||||||
|
import { updateConvoFields } from '~/utils/convos';
|
||||||
|
|
||||||
|
const useUpdateTagsInConvo = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
// Update the queryClient cache with the new tag when a new tag is added/removed to a conversation
|
||||||
|
const updateTagsInConversation = (conversationId: string, tags: string[]) => {
|
||||||
|
// Update the tags for the current conversation
|
||||||
|
const currentConvo = queryClient.getQueryData<t.TConversation>([
|
||||||
|
QueryKeys.conversation,
|
||||||
|
conversationId,
|
||||||
|
]);
|
||||||
|
if (!currentConvo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedConvo = {
|
||||||
|
...currentConvo,
|
||||||
|
tags,
|
||||||
|
} as t.TConversation;
|
||||||
|
queryClient.setQueryData([QueryKeys.conversation, conversationId], updatedConvo);
|
||||||
|
queryClient.setQueryData<t.ConversationData>([QueryKeys.allConversations], (convoData) => {
|
||||||
|
if (!convoData) {
|
||||||
|
return convoData;
|
||||||
|
}
|
||||||
|
return updateConvoFields(
|
||||||
|
convoData,
|
||||||
|
{
|
||||||
|
conversationId: currentConvo.conversationId,
|
||||||
|
tags: updatedConvo.tags,
|
||||||
|
} as t.TConversation,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// update the tag to newTag in all conversations when a tag is updated to a newTag
|
||||||
|
// The difference with updateTagsInConversation is that it adds or removes tags for a specific conversation,
|
||||||
|
// whereas this function is for changing the title of a specific tag.
|
||||||
|
const replaceTagsInAllConversations = (tag: string, newTag: string) => {
|
||||||
|
const data = queryClient.getQueryData<InfiniteData<ConversationListResponse>>([
|
||||||
|
QueryKeys.allConversations,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const conversationIdsWithTag = [] as string[];
|
||||||
|
|
||||||
|
// update tag to newTag in all conversations
|
||||||
|
const newData = JSON.parse(JSON.stringify(data)) as InfiniteData<ConversationListResponse>;
|
||||||
|
for (let pageIndex = 0; pageIndex < newData.pages.length; pageIndex++) {
|
||||||
|
const page = newData.pages[pageIndex];
|
||||||
|
page.conversations = page.conversations.map((conversation) => {
|
||||||
|
if (conversation.conversationId && conversation.tags?.includes(tag)) {
|
||||||
|
conversationIdsWithTag.push(conversation.conversationId);
|
||||||
|
conversation.tags = conversation.tags.map((t) => (t === tag ? newTag : t));
|
||||||
|
}
|
||||||
|
return conversation;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
queryClient.setQueryData<InfiniteData<ConversationListResponse>>(
|
||||||
|
[QueryKeys.allConversations],
|
||||||
|
newData,
|
||||||
|
);
|
||||||
|
|
||||||
|
// update the tag to newTag from the cache of each conversation
|
||||||
|
for (let i = 0; i < conversationIdsWithTag.length; i++) {
|
||||||
|
const conversationId = conversationIdsWithTag[i];
|
||||||
|
const conversation = queryClient.getQueryData<t.TConversation>([
|
||||||
|
QueryKeys.conversation,
|
||||||
|
conversationId,
|
||||||
|
]);
|
||||||
|
if (conversation && conversation.tags) {
|
||||||
|
const updatedConvo = {
|
||||||
|
...conversation,
|
||||||
|
tags: conversation.tags.map((t) => (t === tag ? newTag : t)),
|
||||||
|
} as t.TConversation;
|
||||||
|
queryClient.setQueryData<t.TConversation>(
|
||||||
|
[QueryKeys.conversation, conversationId],
|
||||||
|
updatedConvo,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { updateTagsInConversation, replaceTagsInAllConversations };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useUpdateTagsInConvo;
|
||||||
|
|
@ -294,7 +294,6 @@ export default {
|
||||||
com_ui_min_tags: 'Cannot remove more values, a minimum of {0} are required.',
|
com_ui_min_tags: 'Cannot remove more values, a minimum of {0} are required.',
|
||||||
com_ui_max_tags: 'Maximum number allowed is {0}, using latest values.',
|
com_ui_max_tags: 'Maximum number allowed is {0}, using latest values.',
|
||||||
com_ui_bookmarks: 'Bookmarks',
|
com_ui_bookmarks: 'Bookmarks',
|
||||||
com_ui_bookmarks_rebuild: 'Rebuild',
|
|
||||||
com_ui_bookmarks_new: 'New Bookmark',
|
com_ui_bookmarks_new: 'New Bookmark',
|
||||||
com_ui_bookmark_delete_confirm: 'Are you sure you want to delete this bookmark?',
|
com_ui_bookmark_delete_confirm: 'Are you sure you want to delete this bookmark?',
|
||||||
com_ui_bookmarks_title: 'Title',
|
com_ui_bookmarks_title: 'Title',
|
||||||
|
|
@ -696,8 +695,6 @@ export default {
|
||||||
'This action will revoke and remove all the API keys that you have provided. You will need to re-enter these credentials to continue using those endpoints.',
|
'This action will revoke and remove all the API keys that you have provided. You will need to re-enter these credentials to continue using those endpoints.',
|
||||||
com_nav_info_delete_cache_storage:
|
com_nav_info_delete_cache_storage:
|
||||||
'This action will delete all cached TTS (Text-to-Speech) audio files stored on your device. Cached audio files are used to speed up playback of previously generated TTS audio, but they can consume storage space on your device.',
|
'This action will delete all cached TTS (Text-to-Speech) audio files stored on your device. Cached audio files are used to speed up playback of previously generated TTS audio, but they can consume storage space on your device.',
|
||||||
com_nav_info_bookmarks_rebuild:
|
|
||||||
'If the bookmark count is incorrect, please rebuild the bookmark information. The bookmark count will be recalculated and the data will be restored to its correct state.',
|
|
||||||
// Command Settings Tab
|
// Command Settings Tab
|
||||||
com_nav_commands: 'Commands',
|
com_nav_commands: 'Commands',
|
||||||
com_nav_commands_tab: 'Command Settings',
|
com_nav_commands_tab: 'Command Settings',
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,12 @@ html {
|
||||||
--text-secondary:var(--gray-600);
|
--text-secondary:var(--gray-600);
|
||||||
--text-secondary-alt:var(--gray-500);
|
--text-secondary-alt:var(--gray-500);
|
||||||
--text-tertiary:var(--gray-500);
|
--text-tertiary:var(--gray-500);
|
||||||
|
--ring-primary:var(--gray-500);
|
||||||
|
--header-primary:var(--white);
|
||||||
|
--header-hover:var(--gray-50);
|
||||||
|
--header-button-hover:var(--gray-50);
|
||||||
--surface-active:var(--gray-100);
|
--surface-active:var(--gray-100);
|
||||||
|
--surface-hover:var(--gray-200);
|
||||||
--surface-primary:var(--white);
|
--surface-primary:var(--white);
|
||||||
--surface-primary-alt:var(--white);
|
--surface-primary-alt:var(--white);
|
||||||
--surface-primary-contrast:var(--gray-100);
|
--surface-primary-contrast:var(--gray-100);
|
||||||
|
|
@ -50,7 +55,11 @@ html {
|
||||||
--text-secondary:var(--gray-300);
|
--text-secondary:var(--gray-300);
|
||||||
--text-secondary-alt:var(--gray-400);
|
--text-secondary-alt:var(--gray-400);
|
||||||
--text-tertiary:var(--gray-500);
|
--text-tertiary:var(--gray-500);
|
||||||
|
--header-primary:var(--gray-700);
|
||||||
|
--header-hover:var(--gray-600);
|
||||||
|
--header-button-hover:var(--gray-700);
|
||||||
--surface-active:var(--gray-600);
|
--surface-active:var(--gray-600);
|
||||||
|
--surface-hover:var(--gray-700);
|
||||||
--surface-primary:var(--gray-900);
|
--surface-primary:var(--gray-900);
|
||||||
--surface-primary-alt:var(--gray-850);
|
--surface-primary-alt:var(--gray-850);
|
||||||
--surface-primary-contrast:var(--gray-850);
|
--surface-primary-contrast:var(--gray-850);
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,12 @@ module.exports = {
|
||||||
'text-secondary': 'var(--text-secondary)',
|
'text-secondary': 'var(--text-secondary)',
|
||||||
'text-secondary-alt': 'var(--text-secondary-alt)',
|
'text-secondary-alt': 'var(--text-secondary-alt)',
|
||||||
'text-tertiary': 'var(--text-tertiary)',
|
'text-tertiary': 'var(--text-tertiary)',
|
||||||
|
'ring-primary': 'var(--ring-primary)',
|
||||||
|
'header-primary': 'var(--header-primary)',
|
||||||
|
'header-hover': 'var(--header-hover)',
|
||||||
|
'header-button-hover': 'var(--header-button-hover)',
|
||||||
'surface-active': 'var(--surface-active)',
|
'surface-active': 'var(--surface-active)',
|
||||||
|
'surface-hover': 'var(--surface-hover)',
|
||||||
'surface-primary': 'var(--surface-primary)',
|
'surface-primary': 'var(--surface-primary)',
|
||||||
'surface-primary-alt': 'var(--surface-primary-alt)',
|
'surface-primary-alt': 'var(--surface-primary-alt)',
|
||||||
'surface-primary-contrast': 'var(--surface-primary-contrast)',
|
'surface-primary-contrast': 'var(--surface-primary-contrast)',
|
||||||
|
|
|
||||||
|
|
@ -74,21 +74,6 @@ 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 = (
|
export const useGetUserBalance = (
|
||||||
config?: UseQueryOptions<string>,
|
config?: UseQueryOptions<string>,
|
||||||
): QueryObserverResult<string> => {
|
): QueryObserverResult<string> => {
|
||||||
|
|
|
||||||
|
|
@ -185,7 +185,6 @@ export type TConversationTagResponse = TConversationTag;
|
||||||
|
|
||||||
// type for tagging conversation
|
// type for tagging conversation
|
||||||
export type TTagConversationRequest = {
|
export type TTagConversationRequest = {
|
||||||
conversationId: string;
|
|
||||||
tags: string[];
|
tags: string[];
|
||||||
};
|
};
|
||||||
export type TTagConversationResponse = string[];
|
export type TTagConversationResponse = string[];
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue