mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 00:40:14 +01:00
🔖 feat: Enhance Bookmarks UX, add RBAC, toggle via librechat.yaml (#3747)
* chore: update package version to 0.7.416 * chore: Update Role.js imports order * refactor: move updateTagsInConvo to tags route, add RBAC for tags * refactor: add updateTagsInConvoOptions * fix: loading state for bookmark form * refactor: update primaryText class in TitleButton component * refactor: remove duplicate bookmarks and theming * refactor: update EditIcon component to use React.forwardRef * refactor: add _id field to tConversationTagSchema * refactor: remove promises * refactor: move mutation logic from BookmarkForm -> BookmarkEditDialog * refactor: update button class in BookmarkForm component * fix: conversation mutations and add better logging to useConversationTagMutation * refactor: update logger message in BookmarkEditDialog component * refactor: improve UI consistency in BookmarkNav and NewChat components * refactor: update logger message in BookmarkEditDialog component * refactor: Add tags prop to BookmarkForm component * refactor: Update BookmarkForm to avoid tag mutation if the tag already exists; also close dialog on submission programmatically * refactor: general role helper function to support updating access permissions for different permission types * refactor: Update getLatestText function to handle undefined values in message.content * refactor: Update useHasAccess hook to handle null role values for authenticated users * feat: toggle bookmarks access * refactor: Update PromptsCommand to handle access permissions for prompts * feat: updateConversationSelector * refactor: rename `vars` to `tagToDelete` for clarity * fix: prevent recreation of deleted tags in BookmarkMenu on Item Click * ci: mock updateBookmarksAccess function * ci: mock updateBookmarksAccess function
This commit is contained in:
parent
366e4c5adb
commit
f86e9dd04c
39 changed files with 530 additions and 298 deletions
|
|
@ -1,10 +1,11 @@
|
||||||
const {
|
const {
|
||||||
SystemRoles,
|
|
||||||
CacheKeys,
|
CacheKeys,
|
||||||
|
SystemRoles,
|
||||||
roleDefaults,
|
roleDefaults,
|
||||||
PermissionTypes,
|
PermissionTypes,
|
||||||
Permissions,
|
removeNullishValues,
|
||||||
promptPermissionsSchema,
|
promptPermissionsSchema,
|
||||||
|
bookmarkPermissionsSchema,
|
||||||
} = require('librechat-data-provider');
|
} = require('librechat-data-provider');
|
||||||
const getLogStores = require('~/cache/getLogStores');
|
const getLogStores = require('~/cache/getLogStores');
|
||||||
const Role = require('~/models/schema/roleSchema');
|
const Role = require('~/models/schema/roleSchema');
|
||||||
|
|
@ -69,37 +70,52 @@ const updateRoleByName = async function (roleName, updates) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const permissionSchemas = {
|
||||||
|
[PermissionTypes.PROMPTS]: promptPermissionsSchema,
|
||||||
|
[PermissionTypes.BOOKMARKS]: bookmarkPermissionsSchema,
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the Prompt access for a specific role.
|
* Updates access permissions for a specific role and permission type.
|
||||||
* @param {SystemRoles} roleName - The role to update the prompt access for.
|
* @param {SystemRoles} roleName - The role to update.
|
||||||
* @param {boolean | undefined} [value] - The new value for the prompt access.
|
* @param {PermissionTypes} permissionType - The type of permission to update.
|
||||||
|
* @param {Object.<Permissions, boolean>} permissions - Permissions to update and their values.
|
||||||
*/
|
*/
|
||||||
async function updatePromptsAccess(roleName, value) {
|
async function updateAccessPermissions(roleName, permissionType, _permissions) {
|
||||||
if (typeof value === 'undefined') {
|
const permissions = removeNullishValues(_permissions);
|
||||||
|
if (Object.keys(permissions).length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsedUpdates = promptPermissionsSchema.partial().parse({ [Permissions.USE]: value });
|
|
||||||
const role = await getRoleByName(roleName);
|
const role = await getRoleByName(roleName);
|
||||||
if (!role) {
|
if (!role || !permissionSchemas[permissionType]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mergedUpdates = {
|
await updateRoleByName(roleName, {
|
||||||
[PermissionTypes.PROMPTS]: {
|
[permissionType]: {
|
||||||
...role[PermissionTypes.PROMPTS],
|
...role[permissionType],
|
||||||
...parsedUpdates,
|
...permissionSchemas[permissionType].partial().parse(permissions),
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
await updateRoleByName(roleName, mergedUpdates);
|
Object.entries(permissions).forEach(([permission, value]) =>
|
||||||
logger.info(`Updated '${roleName}' role prompts 'USE' permission to: ${value}`);
|
logger.info(
|
||||||
|
`Updated '${roleName}' role ${permissionType} '${permission}' permission to: ${value}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to update USER role prompts USE permission:', error);
|
logger.error(`Failed to update ${roleName} role ${permissionType} permissions:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updatePromptsAccess = (roleName, permissions) =>
|
||||||
|
updateAccessPermissions(roleName, PermissionTypes.PROMPTS, permissions);
|
||||||
|
|
||||||
|
const updateBookmarksAccess = (roleName, permissions) =>
|
||||||
|
updateAccessPermissions(roleName, PermissionTypes.BOOKMARKS, permissions);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize default roles in the system.
|
* Initialize default roles in the system.
|
||||||
* Creates the default roles (ADMIN, USER) if they don't exist in the database.
|
* Creates the default roles (ADMIN, USER) if they don't exist in the database.
|
||||||
|
|
@ -123,4 +139,5 @@ module.exports = {
|
||||||
initializeRoles,
|
initializeRoles,
|
||||||
updateRoleByName,
|
updateRoleByName,
|
||||||
updatePromptsAccess,
|
updatePromptsAccess,
|
||||||
|
updateBookmarksAccess,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,12 @@ const roleSchema = new mongoose.Schema({
|
||||||
unique: true,
|
unique: true,
|
||||||
index: true,
|
index: true,
|
||||||
},
|
},
|
||||||
|
[PermissionTypes.BOOKMARKS]: {
|
||||||
|
[Permissions.USE]: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
[PermissionTypes.PROMPTS]: {
|
[PermissionTypes.PROMPTS]: {
|
||||||
[Permissions.SHARED_GLOBAL]: {
|
[Permissions.SHARED_GLOBAL]: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ const requireJwtAuth = require('~/server/middleware/requireJwtAuth');
|
||||||
const { forkConversation } = require('~/server/utils/import/fork');
|
const { forkConversation } = require('~/server/utils/import/fork');
|
||||||
const { importConversations } = require('~/server/utils/import');
|
const { importConversations } = require('~/server/utils/import');
|
||||||
const { createImportLimiters } = require('~/server/middleware');
|
const { createImportLimiters } = require('~/server/middleware');
|
||||||
const { updateTagsForConversation } = require('~/models/ConversationTag');
|
|
||||||
const getLogStores = require('~/cache/getLogStores');
|
const getLogStores = require('~/cache/getLogStores');
|
||||||
const { sleep } = require('~/server/utils');
|
const { sleep } = require('~/server/utils');
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('~/config');
|
||||||
|
|
@ -174,18 +173,4 @@ router.post('/fork', async (req, res) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.put('/tags/:conversationId', async (req, res) => {
|
|
||||||
try {
|
|
||||||
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,13 +1,21 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
const { PermissionTypes, Permissions } = require('librechat-data-provider');
|
||||||
const {
|
const {
|
||||||
getConversationTags,
|
getConversationTags,
|
||||||
updateConversationTag,
|
updateConversationTag,
|
||||||
createConversationTag,
|
createConversationTag,
|
||||||
deleteConversationTag,
|
deleteConversationTag,
|
||||||
|
updateTagsForConversation,
|
||||||
} = require('~/models/ConversationTag');
|
} = require('~/models/ConversationTag');
|
||||||
const requireJwtAuth = require('~/server/middleware/requireJwtAuth');
|
const { requireJwtAuth, generateCheckAccess } = require('~/server/middleware');
|
||||||
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
const checkBookmarkAccess = generateCheckAccess(PermissionTypes.BOOKMARKS, [Permissions.USE]);
|
||||||
|
|
||||||
router.use(requireJwtAuth);
|
router.use(requireJwtAuth);
|
||||||
|
router.use(checkBookmarkAccess);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /
|
* GET /
|
||||||
|
|
@ -24,7 +32,7 @@ router.get('/', async (req, res) => {
|
||||||
res.status(404).end();
|
res.status(404).end();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting conversation tags:', error);
|
logger.error('Error getting conversation tags:', error);
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -40,7 +48,7 @@ router.post('/', async (req, res) => {
|
||||||
const tag = await createConversationTag(req.user.id, req.body);
|
const tag = await createConversationTag(req.user.id, req.body);
|
||||||
res.status(200).json(tag);
|
res.status(200).json(tag);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating conversation tag:', error);
|
logger.error('Error creating conversation tag:', error);
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -60,7 +68,7 @@ router.put('/:tag', async (req, res) => {
|
||||||
res.status(404).json({ error: 'Tag not found' });
|
res.status(404).json({ error: 'Tag not found' });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating conversation tag:', error);
|
logger.error('Error updating conversation tag:', error);
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -80,9 +88,29 @@ router.delete('/:tag', async (req, res) => {
|
||||||
res.status(404).json({ error: 'Tag not found' });
|
res.status(404).json({ error: 'Tag not found' });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting conversation tag:', error);
|
logger.error('Error deleting conversation tag:', error);
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT /convo/:conversationId
|
||||||
|
* Updates the tags for a conversation.
|
||||||
|
* @param {Object} req - Express request object
|
||||||
|
* @param {Object} res - Express response object
|
||||||
|
*/
|
||||||
|
router.put('/convo/:conversationId', async (req, res) => {
|
||||||
|
try {
|
||||||
|
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;
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ jest.mock('./Files/Firebase/initialize', () => ({
|
||||||
jest.mock('~/models/Role', () => ({
|
jest.mock('~/models/Role', () => ({
|
||||||
initializeRoles: jest.fn(),
|
initializeRoles: jest.fn(),
|
||||||
updatePromptsAccess: jest.fn(),
|
updatePromptsAccess: jest.fn(),
|
||||||
|
updateBookmarksAccess: jest.fn(),
|
||||||
}));
|
}));
|
||||||
jest.mock('./ToolService', () => ({
|
jest.mock('./ToolService', () => ({
|
||||||
loadAndFormatTools: jest.fn().mockReturnValue({
|
loadAndFormatTools: jest.fn().mockReturnValue({
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
const { SystemRoles, removeNullishValues } = require('librechat-data-provider');
|
const { SystemRoles, Permissions, removeNullishValues } = require('librechat-data-provider');
|
||||||
const { updatePromptsAccess } = require('~/models/Role');
|
const { updatePromptsAccess, updateBookmarksAccess } = require('~/models/Role');
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -24,10 +24,12 @@ async function loadDefaultInterface(config, configDefaults, roleName = SystemRol
|
||||||
sidePanel: interfaceConfig?.sidePanel ?? defaults.sidePanel,
|
sidePanel: interfaceConfig?.sidePanel ?? defaults.sidePanel,
|
||||||
privacyPolicy: interfaceConfig?.privacyPolicy ?? defaults.privacyPolicy,
|
privacyPolicy: interfaceConfig?.privacyPolicy ?? defaults.privacyPolicy,
|
||||||
termsOfService: interfaceConfig?.termsOfService ?? defaults.termsOfService,
|
termsOfService: interfaceConfig?.termsOfService ?? defaults.termsOfService,
|
||||||
|
bookmarks: interfaceConfig?.bookmarks ?? defaults.bookmarks,
|
||||||
prompts: interfaceConfig?.prompts ?? defaults.prompts,
|
prompts: interfaceConfig?.prompts ?? defaults.prompts,
|
||||||
});
|
});
|
||||||
|
|
||||||
await updatePromptsAccess(roleName, loadedInterface.prompts);
|
await updatePromptsAccess(roleName, { [Permissions.USE]: loadedInterface.prompts });
|
||||||
|
await updateBookmarksAccess(roleName, { [Permissions.USE]: loadedInterface.bookmarks });
|
||||||
|
|
||||||
let i = 0;
|
let i = 0;
|
||||||
const logSettings = () => {
|
const logSettings = () => {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
const { SystemRoles } = require('librechat-data-provider');
|
const { SystemRoles, Permissions } = require('librechat-data-provider');
|
||||||
const { updatePromptsAccess } = require('~/models/Role');
|
const { updatePromptsAccess } = require('~/models/Role');
|
||||||
const { loadDefaultInterface } = require('./interface');
|
const { loadDefaultInterface } = require('./interface');
|
||||||
|
|
||||||
jest.mock('~/models/Role', () => ({
|
jest.mock('~/models/Role', () => ({
|
||||||
updatePromptsAccess: jest.fn(),
|
updatePromptsAccess: jest.fn(),
|
||||||
|
updateBookmarksAccess: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('loadDefaultInterface', () => {
|
describe('loadDefaultInterface', () => {
|
||||||
|
|
@ -13,7 +14,7 @@ describe('loadDefaultInterface', () => {
|
||||||
|
|
||||||
await loadDefaultInterface(config, configDefaults);
|
await loadDefaultInterface(config, configDefaults);
|
||||||
|
|
||||||
expect(updatePromptsAccess).toHaveBeenCalledWith(SystemRoles.USER, true);
|
expect(updatePromptsAccess).toHaveBeenCalledWith(SystemRoles.USER, { [Permissions.USE]: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call updatePromptsAccess with false when prompts is false', async () => {
|
it('should call updatePromptsAccess with false when prompts is false', async () => {
|
||||||
|
|
@ -22,7 +23,9 @@ describe('loadDefaultInterface', () => {
|
||||||
|
|
||||||
await loadDefaultInterface(config, configDefaults);
|
await loadDefaultInterface(config, configDefaults);
|
||||||
|
|
||||||
expect(updatePromptsAccess).toHaveBeenCalledWith(SystemRoles.USER, false);
|
expect(updatePromptsAccess).toHaveBeenCalledWith(SystemRoles.USER, {
|
||||||
|
[Permissions.USE]: false,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call updatePromptsAccess with undefined when prompts is not specified in config', async () => {
|
it('should call updatePromptsAccess with undefined when prompts is not specified in config', async () => {
|
||||||
|
|
@ -31,7 +34,9 @@ describe('loadDefaultInterface', () => {
|
||||||
|
|
||||||
await loadDefaultInterface(config, configDefaults);
|
await loadDefaultInterface(config, configDefaults);
|
||||||
|
|
||||||
expect(updatePromptsAccess).toHaveBeenCalledWith(SystemRoles.USER, undefined);
|
expect(updatePromptsAccess).toHaveBeenCalledWith(SystemRoles.USER, {
|
||||||
|
[Permissions.USE]: undefined,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call updatePromptsAccess with undefined when prompts is explicitly undefined', async () => {
|
it('should call updatePromptsAccess with undefined when prompts is explicitly undefined', async () => {
|
||||||
|
|
@ -40,6 +45,8 @@ describe('loadDefaultInterface', () => {
|
||||||
|
|
||||||
await loadDefaultInterface(config, configDefaults);
|
await loadDefaultInterface(config, configDefaults);
|
||||||
|
|
||||||
expect(updatePromptsAccess).toHaveBeenCalledWith(SystemRoles.USER, undefined);
|
expect(updatePromptsAccess).toHaveBeenCalledWith(SystemRoles.USER, {
|
||||||
|
[Permissions.USE]: undefined,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,17 @@
|
||||||
import React, { useRef, useState, Dispatch, SetStateAction } from 'react';
|
import React, { useRef, Dispatch, SetStateAction } from 'react';
|
||||||
import { TConversationTag, TConversation } from 'librechat-data-provider';
|
import { TConversationTag, TConversation } from 'librechat-data-provider';
|
||||||
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
||||||
import { OGDialog, OGDialogClose } from '~/components/ui/';
|
import { useConversationTagMutation } from '~/data-provider';
|
||||||
|
import { NotificationSeverity } from '~/common';
|
||||||
|
import { useToastContext } from '~/Providers';
|
||||||
|
import { OGDialog } from '~/components/ui';
|
||||||
|
import { Spinner } from '~/components/svg';
|
||||||
import BookmarkForm from './BookmarkForm';
|
import BookmarkForm from './BookmarkForm';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
import { Spinner } from '../svg';
|
import { logger } from '~/utils';
|
||||||
|
|
||||||
type BookmarkEditDialogProps = {
|
type BookmarkEditDialogProps = {
|
||||||
|
context: string;
|
||||||
bookmark?: TConversationTag;
|
bookmark?: TConversationTag;
|
||||||
conversation?: TConversation;
|
conversation?: TConversation;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
|
|
@ -16,6 +21,7 @@ type BookmarkEditDialogProps = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const BookmarkEditDialog = ({
|
const BookmarkEditDialog = ({
|
||||||
|
context,
|
||||||
bookmark,
|
bookmark,
|
||||||
conversation,
|
conversation,
|
||||||
tags,
|
tags,
|
||||||
|
|
@ -24,9 +30,40 @@ const BookmarkEditDialog = ({
|
||||||
setOpen,
|
setOpen,
|
||||||
}: BookmarkEditDialogProps) => {
|
}: BookmarkEditDialogProps) => {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const formRef = useRef<HTMLFormElement>(null);
|
const formRef = useRef<HTMLFormElement>(null);
|
||||||
|
|
||||||
|
const { showToast } = useToastContext();
|
||||||
|
const mutation = useConversationTagMutation({
|
||||||
|
context,
|
||||||
|
tag: bookmark?.tag,
|
||||||
|
options: {
|
||||||
|
onSuccess: (_data, vars) => {
|
||||||
|
showToast({
|
||||||
|
message: bookmark
|
||||||
|
? localize('com_ui_bookmarks_update_success')
|
||||||
|
: localize('com_ui_bookmarks_create_success'),
|
||||||
|
});
|
||||||
|
setOpen(false);
|
||||||
|
logger.log('tag_mutation', 'tags before setting', tags);
|
||||||
|
if (setTags && vars.addToConversation === true) {
|
||||||
|
const newTags = [...(tags || []), vars.tag].filter(
|
||||||
|
(tag) => tag !== undefined,
|
||||||
|
) as string[];
|
||||||
|
setTags(newTags);
|
||||||
|
logger.log('tag_mutation', 'tags after', newTags);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
showToast({
|
||||||
|
message: bookmark
|
||||||
|
? localize('com_ui_bookmarks_update_error')
|
||||||
|
: localize('com_ui_bookmarks_create_error'),
|
||||||
|
severity: NotificationSeverity.ERROR,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const handleSubmitForm = () => {
|
const handleSubmitForm = () => {
|
||||||
if (formRef.current) {
|
if (formRef.current) {
|
||||||
formRef.current.dispatchEvent(new Event('submit', { cancelable: true, bubbles: true }));
|
formRef.current.dispatchEvent(new Event('submit', { cancelable: true, bubbles: true }));
|
||||||
|
|
@ -40,26 +77,23 @@ const BookmarkEditDialog = ({
|
||||||
showCloseButton={false}
|
showCloseButton={false}
|
||||||
main={
|
main={
|
||||||
<BookmarkForm
|
<BookmarkForm
|
||||||
|
tags={tags}
|
||||||
|
setOpen={setOpen}
|
||||||
|
mutation={mutation}
|
||||||
conversation={conversation}
|
conversation={conversation}
|
||||||
onOpenChange={setOpen}
|
|
||||||
setIsLoading={setIsLoading}
|
|
||||||
bookmark={bookmark}
|
bookmark={bookmark}
|
||||||
formRef={formRef}
|
formRef={formRef}
|
||||||
setTags={setTags}
|
|
||||||
tags={tags}
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
buttons={
|
buttons={
|
||||||
<OGDialogClose asChild>
|
<button
|
||||||
<button
|
type="submit"
|
||||||
type="submit"
|
disabled={mutation.isLoading}
|
||||||
disabled={isLoading}
|
onClick={handleSubmitForm}
|
||||||
onClick={handleSubmitForm}
|
className="btn rounded bg-green-500 font-bold text-white transition-all hover:bg-green-600"
|
||||||
className="btn rounded bg-green-500 font-bold text-white transition-all hover:bg-green-600"
|
>
|
||||||
>
|
{mutation.isLoading ? <Spinner /> : localize('com_ui_save')}
|
||||||
{isLoading ? <Spinner /> : localize('com_ui_save')}
|
</button>
|
||||||
</button>
|
|
||||||
</OGDialogClose>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</OGDialog>
|
</OGDialog>
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,39 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
import { QueryKeys } from 'librechat-data-provider';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import type {
|
import type {
|
||||||
TConversationTag,
|
|
||||||
TConversation,
|
TConversation,
|
||||||
|
TConversationTag,
|
||||||
TConversationTagRequest,
|
TConversationTagRequest,
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
import { cn, removeFocusOutlines, defaultTextProps } from '~/utils/';
|
import { cn, removeFocusOutlines, defaultTextProps, logger } from '~/utils';
|
||||||
|
import { Checkbox, Label, TextareaAutosize } from '~/components/ui';
|
||||||
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 { useLocalize, useBookmarkSuccess } from '~/hooks';
|
|
||||||
import { NotificationSeverity } from '~/common';
|
|
||||||
import { useToastContext } from '~/Providers';
|
import { useToastContext } from '~/Providers';
|
||||||
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
||||||
type TBookmarkFormProps = {
|
type TBookmarkFormProps = {
|
||||||
|
tags?: string[];
|
||||||
bookmark?: TConversationTag;
|
bookmark?: TConversationTag;
|
||||||
conversation?: TConversation;
|
conversation?: TConversation;
|
||||||
onOpenChange: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
formRef: React.RefObject<HTMLFormElement>;
|
formRef: React.RefObject<HTMLFormElement>;
|
||||||
setIsLoading: React.Dispatch<React.SetStateAction<boolean>>;
|
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
tags?: string[];
|
mutation: ReturnType<typeof useConversationTagMutation>;
|
||||||
setTags?: (tags: string[]) => void;
|
|
||||||
};
|
};
|
||||||
const BookmarkForm = ({
|
const BookmarkForm = ({
|
||||||
bookmark,
|
|
||||||
conversation,
|
|
||||||
onOpenChange,
|
|
||||||
formRef,
|
|
||||||
setIsLoading,
|
|
||||||
tags,
|
tags,
|
||||||
setTags,
|
bookmark,
|
||||||
|
mutation,
|
||||||
|
conversation,
|
||||||
|
setOpen,
|
||||||
|
formRef,
|
||||||
}: TBookmarkFormProps) => {
|
}: TBookmarkFormProps) => {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const { showToast } = useToastContext();
|
const { showToast } = useToastContext();
|
||||||
const { bookmarks } = useBookmarkContext();
|
const { bookmarks } = useBookmarkContext();
|
||||||
const mutation = useConversationTagMutation(bookmark?.tag);
|
|
||||||
const onSuccess = useBookmarkSuccess(conversation?.conversationId || '');
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
|
|
@ -46,56 +44,47 @@ const BookmarkForm = ({
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm<TConversationTagRequest>({
|
} = useForm<TConversationTagRequest>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
tag: bookmark?.tag || '',
|
tag: bookmark?.tag ?? '',
|
||||||
description: bookmark?.description || '',
|
description: bookmark?.description ?? '',
|
||||||
conversationId: conversation?.conversationId || '',
|
conversationId: conversation?.conversationId ?? '',
|
||||||
addToConversation: conversation ? true : false,
|
addToConversation: conversation ? true : false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (bookmark) {
|
if (bookmark && bookmark.tag) {
|
||||||
setValue('tag', bookmark.tag || '');
|
setValue('tag', bookmark.tag);
|
||||||
setValue('description', bookmark.description || '');
|
setValue('description', bookmark.description ?? '');
|
||||||
}
|
}
|
||||||
}, [bookmark, setValue]);
|
}, [bookmark, setValue]);
|
||||||
|
|
||||||
const onSubmit = (data: TConversationTagRequest) => {
|
const onSubmit = (data: TConversationTagRequest) => {
|
||||||
|
logger.log('tag_mutation', 'BookmarkForm - onSubmit: data', data);
|
||||||
if (mutation.isLoading) {
|
if (mutation.isLoading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (data.tag === bookmark?.tag && data.description === bookmark?.description) {
|
if (data.tag === bookmark?.tag && data.description === bookmark?.description) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (data.tag != null && (tags ?? []).includes(data.tag)) {
|
||||||
|
showToast({
|
||||||
|
message: localize('com_ui_bookmarks_create_exists'),
|
||||||
|
status: 'warning',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const allTags =
|
||||||
|
queryClient.getQueryData<TConversationTag[]>([QueryKeys.conversationTags]) ?? [];
|
||||||
|
if (allTags.some((tag) => tag.tag === data.tag)) {
|
||||||
|
showToast({
|
||||||
|
message: localize('com_ui_bookmarks_create_exists'),
|
||||||
|
status: 'warning',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setIsLoading(true);
|
mutation.mutate(data);
|
||||||
mutation.mutate(data, {
|
setOpen(false);
|
||||||
onSuccess: () => {
|
|
||||||
showToast({
|
|
||||||
message: bookmark
|
|
||||||
? localize('com_ui_bookmarks_update_success')
|
|
||||||
: localize('com_ui_bookmarks_create_success'),
|
|
||||||
});
|
|
||||||
setIsLoading(false);
|
|
||||||
onOpenChange(false);
|
|
||||||
if (setTags && data.addToConversation) {
|
|
||||||
const newTags = [...(tags || []), data.tag].filter(
|
|
||||||
(tag) => tag !== undefined,
|
|
||||||
) as string[];
|
|
||||||
setTags(newTags);
|
|
||||||
onSuccess(newTags);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError: () => {
|
|
||||||
showToast({
|
|
||||||
message: bookmark
|
|
||||||
? localize('com_ui_bookmarks_update_error')
|
|
||||||
: localize('com_ui_bookmarks_create_error'),
|
|
||||||
severity: NotificationSeverity.ERROR,
|
|
||||||
});
|
|
||||||
setIsLoading(false);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -175,10 +164,11 @@ const BookmarkForm = ({
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
aria-label={localize('com_ui_bookmarks_add_to_conversation')}
|
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 w-full cursor-pointer text-text-primary"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setValue('addToConversation', !getValues('addToConversation'), {
|
setValue('addToConversation', !(getValues('addToConversation') ?? false), {
|
||||||
shouldDirty: true,
|
shouldDirty: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ type MenuItemProps = {
|
||||||
tag: string | React.ReactNode;
|
tag: string | React.ReactNode;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
count?: number;
|
count?: number;
|
||||||
handleSubmit: (tag?: string) => Promise<void>;
|
handleSubmit: (tag?: string) => void;
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -17,12 +17,12 @@ const BookmarkItem: FC<MenuItemProps> = ({ tag, selected, handleSubmit, icon, ..
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const clickHandler = async () => {
|
const clickHandler = async () => {
|
||||||
if (tag === 'New Bookmark') {
|
if (tag === 'New Bookmark') {
|
||||||
await handleSubmit();
|
handleSubmit();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
await handleSubmit(tag as string);
|
handleSubmit(tag as string);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -32,7 +32,7 @@ const BookmarkItem: FC<MenuItemProps> = ({ tag, selected, handleSubmit, icon, ..
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderIcon = () => {
|
const renderIcon = () => {
|
||||||
if (icon) {
|
if (icon != null) {
|
||||||
return icon;
|
return icon;
|
||||||
}
|
}
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { useBookmarkContext } from '~/Providers/BookmarkContext';
|
||||||
import BookmarkItem from './BookmarkItem';
|
import BookmarkItem from './BookmarkItem';
|
||||||
interface BookmarkItemsProps {
|
interface BookmarkItemsProps {
|
||||||
tags: string[];
|
tags: string[];
|
||||||
handleSubmit: (tag?: string) => Promise<void>;
|
handleSubmit: (tag?: string) => void;
|
||||||
header: React.ReactNode;
|
header: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -14,9 +14,9 @@ const BookmarkItems: FC<BookmarkItemsProps> = ({ tags, handleSubmit, header }) =
|
||||||
<>
|
<>
|
||||||
{header}
|
{header}
|
||||||
{bookmarks.length > 0 && <div className="my-1.5 h-px" role="none" />}
|
{bookmarks.length > 0 && <div className="my-1.5 h-px" role="none" />}
|
||||||
{bookmarks.map((bookmark) => (
|
{bookmarks.map((bookmark, i) => (
|
||||||
<BookmarkItem
|
<BookmarkItem
|
||||||
key={bookmark.tag}
|
key={`${bookmark._id ?? bookmark.tag}-${i}`}
|
||||||
tag={bookmark.tag}
|
tag={bookmark.tag}
|
||||||
selected={tags.includes(bookmark.tag)}
|
selected={tags.includes(bookmark.tag)}
|
||||||
handleSubmit={handleSubmit}
|
handleSubmit={handleSubmit}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,12 @@ const EditBookmarkButton: FC<{
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<BookmarkEditDialog bookmark={bookmark} open={open} setOpen={setOpen} />
|
<BookmarkEditDialog
|
||||||
|
context="EditBookmarkButton"
|
||||||
|
bookmark={bookmark}
|
||||||
|
open={open}
|
||||||
|
setOpen={setOpen}
|
||||||
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="transition-color flex size-7 items-center justify-center rounded-lg duration-200 hover:bg-surface-hover"
|
className="transition-color flex size-7 items-center justify-center rounded-lg duration-200 hover:bg-surface-hover"
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useOutletContext } from 'react-router-dom';
|
import { useOutletContext } from 'react-router-dom';
|
||||||
import { getConfigDefaults } from 'librechat-data-provider';
|
import { getConfigDefaults, PermissionTypes, Permissions } from 'librechat-data-provider';
|
||||||
import { useGetStartupConfig } from 'librechat-data-provider/react-query';
|
import { useGetStartupConfig } from 'librechat-data-provider/react-query';
|
||||||
import type { ContextType } from '~/common';
|
import type { ContextType } from '~/common';
|
||||||
import { EndpointsMenu, ModelSpecsMenu, PresetsMenu, HeaderNewChat } from './Menus';
|
import { EndpointsMenu, ModelSpecsMenu, PresetsMenu, HeaderNewChat } from './Menus';
|
||||||
import ExportAndShareMenu from './ExportAndShareMenu';
|
import ExportAndShareMenu from './ExportAndShareMenu';
|
||||||
|
import { useMediaQuery, useHasAccess } from '~/hooks';
|
||||||
import HeaderOptions from './Input/HeaderOptions';
|
import HeaderOptions from './Input/HeaderOptions';
|
||||||
import BookmarkMenu from './Menus/BookmarkMenu';
|
import BookmarkMenu from './Menus/BookmarkMenu';
|
||||||
import AddMultiConvo from './AddMultiConvo';
|
import AddMultiConvo from './AddMultiConvo';
|
||||||
import { useMediaQuery } from '~/hooks';
|
|
||||||
|
|
||||||
const defaultInterface = getConfigDefaults().interface;
|
const defaultInterface = getConfigDefaults().interface;
|
||||||
|
|
||||||
|
|
@ -21,6 +21,11 @@ export default function Header() {
|
||||||
[startupConfig],
|
[startupConfig],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const hasAccessToBookmarks = useHasAccess({
|
||||||
|
permissionType: PermissionTypes.BOOKMARKS,
|
||||||
|
permission: Permissions.USE,
|
||||||
|
});
|
||||||
|
|
||||||
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -28,11 +33,11 @@ export default function Header() {
|
||||||
<div className="hide-scrollbar flex w-full items-center justify-between gap-2 overflow-x-auto">
|
<div className="hide-scrollbar flex w-full items-center justify-between gap-2 overflow-x-auto">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{!navVisible && <HeaderNewChat />}
|
{!navVisible && <HeaderNewChat />}
|
||||||
{interfaceConfig.endpointsMenu && <EndpointsMenu />}
|
{interfaceConfig.endpointsMenu === true && <EndpointsMenu />}
|
||||||
{modelSpecs.length > 0 && <ModelSpecsMenu modelSpecs={modelSpecs} />}
|
{modelSpecs.length > 0 && <ModelSpecsMenu modelSpecs={modelSpecs} />}
|
||||||
{<HeaderOptions interfaceConfig={interfaceConfig} />}
|
{<HeaderOptions interfaceConfig={interfaceConfig} />}
|
||||||
{interfaceConfig.presets && <PresetsMenu />}
|
{interfaceConfig.presets === true && <PresetsMenu />}
|
||||||
<BookmarkMenu />
|
{hasAccessToBookmarks === true && <BookmarkMenu />}
|
||||||
<AddMultiConvo />
|
<AddMultiConvo />
|
||||||
{isSmallScreen && (
|
{isSmallScreen && (
|
||||||
<ExportAndShareMenu
|
<ExportAndShareMenu
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import { useSetRecoilState, useRecoilValue } from 'recoil';
|
|
||||||
import { useState, useRef, useEffect, useMemo, memo, useCallback } from 'react';
|
import { useState, useRef, useEffect, useMemo, memo, useCallback } from 'react';
|
||||||
|
import { useSetRecoilState, useRecoilValue } from 'recoil';
|
||||||
|
import { PermissionTypes, Permissions } from 'librechat-data-provider';
|
||||||
import type { TPromptGroup } from 'librechat-data-provider';
|
import type { TPromptGroup } from 'librechat-data-provider';
|
||||||
import type { PromptOption } from '~/common';
|
import type { PromptOption } from '~/common';
|
||||||
import { removeCharIfLast, mapPromptGroups, detectVariables } from '~/utils';
|
import { removeCharIfLast, mapPromptGroups, detectVariables } from '~/utils';
|
||||||
import VariableDialog from '~/components/Prompts/Groups/VariableDialog';
|
import VariableDialog from '~/components/Prompts/Groups/VariableDialog';
|
||||||
import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon';
|
import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon';
|
||||||
|
import { useLocalize, useCombobox, useHasAccess } from '~/hooks';
|
||||||
import { useGetAllPromptGroups } from '~/data-provider';
|
import { useGetAllPromptGroups } from '~/data-provider';
|
||||||
import { useLocalize, useCombobox } from '~/hooks';
|
|
||||||
import { Spinner } from '~/components/svg';
|
import { Spinner } from '~/components/svg';
|
||||||
import MentionItem from './MentionItem';
|
import MentionItem from './MentionItem';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
@ -51,8 +52,13 @@ function PromptsCommand({
|
||||||
submitPrompt: (textPrompt: string) => void;
|
submitPrompt: (textPrompt: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
const hasAccess = useHasAccess({
|
||||||
|
permissionType: PermissionTypes.PROMPTS,
|
||||||
|
permission: Permissions.USE,
|
||||||
|
});
|
||||||
|
|
||||||
const { data, isLoading } = useGetAllPromptGroups(undefined, {
|
const { data, isLoading } = useGetAllPromptGroups(undefined, {
|
||||||
|
enabled: hasAccess,
|
||||||
select: (data) => {
|
select: (data) => {
|
||||||
const mappedArray = data.map((group) => ({
|
const mappedArray = data.map((group) => ({
|
||||||
id: group._id,
|
id: group._id,
|
||||||
|
|
@ -144,6 +150,10 @@ function PromptsCommand({
|
||||||
currentActiveItem?.scrollIntoView({ behavior: 'instant', block: 'nearest' });
|
currentActiveItem?.scrollIntoView({ behavior: 'instant', block: 'nearest' });
|
||||||
}, [activeIndex]);
|
}, [activeIndex]);
|
||||||
|
|
||||||
|
if (!hasAccess) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PopoverContainer
|
<PopoverContainer
|
||||||
index={index}
|
index={index}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import { useState, type FC, useCallback } from 'react';
|
import { useState, type FC, useCallback } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { Constants } from 'librechat-data-provider';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { Constants, QueryKeys } from 'librechat-data-provider';
|
||||||
import { Menu, MenuButton, MenuItems } from '@headlessui/react';
|
import { Menu, MenuButton, MenuItems } from '@headlessui/react';
|
||||||
import { BookmarkFilledIcon, BookmarkIcon } from '@radix-ui/react-icons';
|
import { BookmarkFilledIcon, BookmarkIcon } from '@radix-ui/react-icons';
|
||||||
|
import type { TConversationTag } from 'librechat-data-provider';
|
||||||
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';
|
||||||
|
|
@ -11,19 +13,32 @@ import { NotificationSeverity } from '~/common';
|
||||||
import { useToastContext } from '~/Providers';
|
import { useToastContext } from '~/Providers';
|
||||||
import { useBookmarkSuccess } from '~/hooks';
|
import { useBookmarkSuccess } from '~/hooks';
|
||||||
import { Spinner } from '~/components';
|
import { Spinner } from '~/components';
|
||||||
import { cn } from '~/utils';
|
import { cn, logger } from '~/utils';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
const BookmarkMenu: FC = () => {
|
const BookmarkMenu: FC = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const { showToast } = useToastContext();
|
const { showToast } = useToastContext();
|
||||||
|
|
||||||
const conversation = useRecoilValue(store.conversationByIndex(0)) || undefined;
|
const conversation = useRecoilValue(store.conversationByIndex(0)) || undefined;
|
||||||
const conversationId = conversation?.conversationId ?? '';
|
const conversationId = conversation?.conversationId ?? '';
|
||||||
const onSuccess = useBookmarkSuccess(conversationId);
|
const updateConvoTags = useBookmarkSuccess(conversationId);
|
||||||
const [tags, setTags] = useState<string[]>(conversation?.tags || []);
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
const { mutateAsync, isLoading } = useTagConversationMutation(conversationId);
|
const [open, setOpen] = useState(false);
|
||||||
|
const [tags, setTags] = useState<string[]>(conversation?.tags || []);
|
||||||
|
|
||||||
|
const mutation = useTagConversationMutation(conversationId, {
|
||||||
|
onSuccess: (newTags: string[]) => {
|
||||||
|
setTags(newTags);
|
||||||
|
updateConvoTags(newTags);
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
showToast({
|
||||||
|
message: 'Error adding bookmark',
|
||||||
|
severity: NotificationSeverity.ERROR,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const { data } = useConversationTagsQuery();
|
const { data } = useConversationTagsQuery();
|
||||||
|
|
||||||
|
|
@ -35,7 +50,7 @@ const BookmarkMenu: FC = () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
async (tag?: string): Promise<void> => {
|
(tag?: string) => {
|
||||||
if (tag === undefined || tag === '' || !conversationId) {
|
if (tag === undefined || tag === '' || !conversationId) {
|
||||||
showToast({
|
showToast({
|
||||||
message: 'Invalid tag or conversationId',
|
message: 'Invalid tag or conversationId',
|
||||||
|
|
@ -44,34 +59,29 @@ const BookmarkMenu: FC = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newTags = tags.includes(tag) ? tags.filter((t) => t !== tag) : [...tags, tag];
|
logger.log('tag_mutation', 'BookmarkMenu - handleSubmit: tags before setting', tags);
|
||||||
await mutateAsync(
|
const allTags =
|
||||||
{
|
queryClient.getQueryData<TConversationTag[]>([QueryKeys.conversationTags]) ?? [];
|
||||||
tags: newTags,
|
const existingTags = allTags.map((t) => t.tag);
|
||||||
},
|
const filteredTags = tags.filter((t) => existingTags.includes(t));
|
||||||
{
|
logger.log('tag_mutation', 'BookmarkMenu - handleSubmit: tags after filtering', filteredTags);
|
||||||
onSuccess: (newTags: string[]) => {
|
const newTags = filteredTags.includes(tag)
|
||||||
setTags(newTags);
|
? filteredTags.filter((t) => t !== tag)
|
||||||
onSuccess(newTags);
|
: [...filteredTags, tag];
|
||||||
},
|
logger.log('tag_mutation', 'BookmarkMenu - handleSubmit: tags after', newTags);
|
||||||
onError: () => {
|
mutation.mutate({
|
||||||
showToast({
|
tags: newTags,
|
||||||
message: 'Error adding bookmark',
|
});
|
||||||
severity: NotificationSeverity.ERROR,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
[tags, conversationId, mutateAsync, setTags, onSuccess, showToast],
|
[tags, conversationId, mutation, queryClient, showToast],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isActiveConvo) {
|
if (!isActiveConvo) {
|
||||||
return <></>;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderButtonContent = () => {
|
const renderButtonContent = () => {
|
||||||
if (isLoading) {
|
if (mutation.isLoading) {
|
||||||
return <Spinner aria-label="Spinner" />;
|
return <Spinner aria-label="Spinner" />;
|
||||||
}
|
}
|
||||||
if (tags.length > 0) {
|
if (tags.length > 0) {
|
||||||
|
|
@ -80,10 +90,7 @@ const BookmarkMenu: FC = () => {
|
||||||
return <BookmarkIcon className="icon-sm" aria-label="Bookmark" />;
|
return <BookmarkIcon className="icon-sm" aria-label="Bookmark" />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToggleOpen = () => {
|
const handleToggleOpen = () => setOpen(!open);
|
||||||
setOpen(!open);
|
|
||||||
return Promise.resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -116,6 +123,7 @@ const BookmarkMenu: FC = () => {
|
||||||
)}
|
)}
|
||||||
</Menu>
|
</Menu>
|
||||||
<BookmarkEditDialog
|
<BookmarkEditDialog
|
||||||
|
context="BookmarkMenu - BookmarkEditDialog"
|
||||||
conversation={conversation}
|
conversation={conversation}
|
||||||
tags={tags}
|
tags={tags}
|
||||||
setTags={setTags}
|
setTags={setTags}
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ import { useLocalize } from '~/hooks';
|
||||||
|
|
||||||
export const BookmarkMenuItems: FC<{
|
export const BookmarkMenuItems: FC<{
|
||||||
tags: string[];
|
tags: string[];
|
||||||
handleToggleOpen?: () => Promise<void>;
|
handleToggleOpen?: () => void;
|
||||||
handleSubmit: (tag?: string) => Promise<void>;
|
handleSubmit: (tag?: string) => void;
|
||||||
}> = ({
|
}> = ({
|
||||||
tags,
|
tags,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ export default function TitleButton({ primaryText = '', secondaryText = '' }) {
|
||||||
onClick={() => setIsExpanded(!isExpanded)}
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-token-text-secondary"> {primaryText} </span>
|
<span className="text-text-primary"> {primaryText} </span>
|
||||||
{!!secondaryText && <span className="text-token-text-secondary">{secondaryText}</span>}
|
{!!secondaryText && <span className="text-token-text-secondary">{secondaryText}</span>}
|
||||||
</div>
|
</div>
|
||||||
<ChevronDown className="text-token-text-secondary size-4" />
|
<ChevronDown className="text-token-text-secondary size-4" />
|
||||||
|
|
|
||||||
|
|
@ -43,14 +43,16 @@ const BookmarkNav: FC<BookmarkNavProps> = ({ tags, setTags }: BookmarkNavProps)
|
||||||
)}
|
)}
|
||||||
data-testid="bookmark-menu"
|
data-testid="bookmark-menu"
|
||||||
>
|
>
|
||||||
<div className="relative flex h-8 w-8 items-center justify-center rounded-full p-1 text-text-primary">
|
<div className="h-7 w-7 flex-shrink-0">
|
||||||
{tags.length > 0 ? (
|
<div className="relative flex h-full items-center justify-center rounded-full border border-border-medium bg-surface-primary-alt text-text-primary">
|
||||||
<BookmarkFilledIcon className="h-5 w-5" />
|
{tags.length > 0 ? (
|
||||||
) : (
|
<BookmarkFilledIcon className="h-4 w-4" />
|
||||||
<BookmarkIcon className="h-5 w-5" />
|
) : (
|
||||||
)}
|
<BookmarkIcon className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grow overflow-hidden whitespace-nowrap text-left text-sm text-text-primary">
|
<div className="grow overflow-hidden whitespace-nowrap text-left text-sm font-medium text-text-primary">
|
||||||
{tags.length > 0 ? tags.join(', ') : localize('com_ui_bookmarks')}
|
{tags.length > 0 ? tags.join(', ') : localize('com_ui_bookmarks')}
|
||||||
</div>
|
</div>
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ const BookmarkNavItems: FC<{
|
||||||
conversation: TConversation;
|
conversation: TConversation;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
setTags: (tags: string[]) => void;
|
setTags: (tags: string[]) => void;
|
||||||
}> = ({ conversation, tags, setTags }) => {
|
}> = ({ conversation, tags = [], setTags }) => {
|
||||||
const [currentConversation, setCurrentConversation] = useState<TConversation>();
|
const [currentConversation, setCurrentConversation] = useState<TConversation>();
|
||||||
const { bookmarks } = useBookmarkContext();
|
const { bookmarks } = useBookmarkContext();
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
|
@ -24,19 +24,22 @@ const BookmarkNavItems: FC<{
|
||||||
if (tags.some((selectedTag) => selectedTag === tag)) {
|
if (tags.some((selectedTag) => selectedTag === tag)) {
|
||||||
return tags.filter((selectedTag) => selectedTag !== tag);
|
return tags.filter((selectedTag) => selectedTag !== tag);
|
||||||
} else {
|
} else {
|
||||||
return [...(tags || []), tag];
|
return [...tags, tag];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = (tag: string) => {
|
const handleSubmit = (tag?: string) => {
|
||||||
|
if (tag === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const updatedSelected = getUpdatedSelected(tag);
|
const updatedSelected = getUpdatedSelected(tag);
|
||||||
setTags(updatedSelected);
|
setTags(updatedSelected);
|
||||||
return Promise.resolve();
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
const clear = () => {
|
const clear = () => {
|
||||||
setTags([]);
|
setTags([]);
|
||||||
return Promise.resolve();
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (bookmarks.length === 0) {
|
if (bookmarks.length === 0) {
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
import { useCallback, useEffect, useState, useMemo, memo } from 'react';
|
import { useCallback, useEffect, useState, useMemo, memo } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { PermissionTypes, Permissions } from 'librechat-data-provider';
|
||||||
import type { ConversationListResponse } from 'librechat-data-provider';
|
import type { ConversationListResponse } from 'librechat-data-provider';
|
||||||
import {
|
import {
|
||||||
|
useLocalize,
|
||||||
|
useHasAccess,
|
||||||
useMediaQuery,
|
useMediaQuery,
|
||||||
useAuthContext,
|
useAuthContext,
|
||||||
useConversation,
|
useConversation,
|
||||||
useLocalStorage,
|
useLocalStorage,
|
||||||
useNavScrolling,
|
useNavScrolling,
|
||||||
useConversations,
|
useConversations,
|
||||||
useLocalize,
|
|
||||||
} from '~/hooks';
|
} from '~/hooks';
|
||||||
import { useConversationsInfiniteQuery } from '~/data-provider';
|
import { useConversationsInfiniteQuery } from '~/data-provider';
|
||||||
import { TooltipProvider, Tooltip } from '~/components/ui';
|
import { TooltipProvider, Tooltip } from '~/components/ui';
|
||||||
|
|
@ -41,6 +43,11 @@ const Nav = ({
|
||||||
const [newUser, setNewUser] = useLocalStorage('newUser', true);
|
const [newUser, setNewUser] = useLocalStorage('newUser', true);
|
||||||
const [isToggleHovering, setIsToggleHovering] = useState(false);
|
const [isToggleHovering, setIsToggleHovering] = useState(false);
|
||||||
|
|
||||||
|
const hasAccessToBookmarks = useHasAccess({
|
||||||
|
permissionType: PermissionTypes.BOOKMARKS,
|
||||||
|
permission: Permissions.USE,
|
||||||
|
});
|
||||||
|
|
||||||
const handleMouseEnter = useCallback(() => {
|
const handleMouseEnter = useCallback(() => {
|
||||||
setIsHovering(true);
|
setIsHovering(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -128,7 +135,7 @@ const Nav = ({
|
||||||
<div
|
<div
|
||||||
data-testid="nav"
|
data-testid="nav"
|
||||||
className={
|
className={
|
||||||
'nav active max-w-[320px] flex-shrink-0 overflow-x-hidden bg-gray-50 dark:bg-gray-850 md:max-w-[260px]'
|
'nav active max-w-[320px] flex-shrink-0 overflow-x-hidden bg-surface-primary-alt md:max-w-[260px]'
|
||||||
}
|
}
|
||||||
style={{
|
style={{
|
||||||
width: navVisible ? navWidth : '0px',
|
width: navVisible ? navWidth : '0px',
|
||||||
|
|
@ -168,7 +175,9 @@ const Nav = ({
|
||||||
{isSearchEnabled === true && (
|
{isSearchEnabled === true && (
|
||||||
<SearchBar clearSearch={clearSearch} isSmallScreen={isSmallScreen} />
|
<SearchBar clearSearch={clearSearch} isSmallScreen={isSmallScreen} />
|
||||||
)}
|
)}
|
||||||
<BookmarkNav tags={tags} setTags={setTags} />
|
{hasAccessToBookmarks === true && (
|
||||||
|
<BookmarkNav tags={tags} setTags={setTags} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<NewChat
|
<NewChat
|
||||||
|
|
@ -181,7 +190,9 @@ const Nav = ({
|
||||||
isSmallScreen={isSmallScreen}
|
isSmallScreen={isSmallScreen}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<BookmarkNav tags={tags} setTags={setTags} />
|
{hasAccessToBookmarks === true && (
|
||||||
|
<BookmarkNav tags={tags} setTags={setTags} />
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,7 @@ export default function NewChat({
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{subHeaders ? subHeaders : null}
|
{subHeaders != null ? subHeaders : null}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ const BookmarkPanel = () => {
|
||||||
<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">
|
||||||
<BookmarkEditDialog open={open} setOpen={setOpen} />
|
<BookmarkEditDialog context="BookmarkPanel" open={open} setOpen={setOpen} />
|
||||||
<Button variant="outline" className="w-full text-sm" onClick={() => setOpen(!open)}>
|
<Button variant="outline" className="w-full text-sm" onClick={() => setOpen(!open)}>
|
||||||
<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>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,15 @@ import { BookmarkContext, useBookmarkContext } from '~/Providers/BookmarkContext
|
||||||
import BookmarkTableRow from './BookmarkTableRow';
|
import BookmarkTableRow from './BookmarkTableRow';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
||||||
|
const removeDuplicates = (bookmarks: TConversationTag[]) => {
|
||||||
|
const seen = new Set();
|
||||||
|
return bookmarks.filter((bookmark) => {
|
||||||
|
const duplicate = seen.has(bookmark._id);
|
||||||
|
seen.add(bookmark._id);
|
||||||
|
return !duplicate;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const BookmarkTable = () => {
|
const BookmarkTable = () => {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const [rows, setRows] = useState<ConversationTagsResponse>([]);
|
const [rows, setRows] = useState<ConversationTagsResponse>([]);
|
||||||
|
|
@ -12,13 +21,10 @@ const BookmarkTable = () => {
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const pageSize = 10;
|
const pageSize = 10;
|
||||||
|
|
||||||
const { bookmarks } = useBookmarkContext();
|
const { bookmarks = [] } = useBookmarkContext();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRows(
|
const _bookmarks = removeDuplicates(bookmarks).sort((a, b) => a.position - b.position);
|
||||||
bookmarks
|
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) => {
|
||||||
|
|
@ -32,17 +38,16 @@ const BookmarkTable = () => {
|
||||||
|
|
||||||
const renderRow = useCallback(
|
const renderRow = useCallback(
|
||||||
(row: TConversationTag) => {
|
(row: TConversationTag) => {
|
||||||
return <BookmarkTableRow key={row.tag} moveRow={moveRow} row={row} position={row.position} />;
|
return <BookmarkTableRow key={row._id} moveRow={moveRow} row={row} position={row.position} />;
|
||||||
},
|
},
|
||||||
[moveRow],
|
[moveRow],
|
||||||
);
|
);
|
||||||
|
|
||||||
const filteredRows = rows.filter((row) =>
|
const filteredRows = rows.filter(
|
||||||
row.tag.toLowerCase().includes(searchQuery.toLowerCase()),
|
(row) => row.tag && row.tag.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||||
);
|
);
|
||||||
|
|
||||||
const currentRows = filteredRows.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize);
|
const currentRows = filteredRows.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BookmarkContext.Provider value={{ bookmarks }}>
|
<BookmarkContext.Provider value={{ bookmarks }}>
|
||||||
<div className="flex items-center gap-4 py-4">
|
<div className="flex items-center gap-4 py-4">
|
||||||
|
|
@ -53,14 +58,14 @@ const BookmarkTable = () => {
|
||||||
className="w-full border-border-light placeholder:text-text-secondary"
|
className="w-full border-border-light placeholder:text-text-secondary"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-y-auto rounded-md border border-black/10 dark:border-white/10">
|
<div className="overflow-y-auto rounded-md border border-border-light">
|
||||||
<Table className="table-fixed border-separate border-spacing-0">
|
<Table className="table-fixed border-separate border-spacing-0">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell className="w-full px-3 py-3.5 pl-6 dark:bg-gray-700">
|
<TableCell className="w-full bg-header-primary px-3 py-3.5 pl-6">
|
||||||
<div>{localize('com_ui_bookmarks_title')}</div>
|
<div>{localize('com_ui_bookmarks_title')}</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="w-full px-3 py-3.5 dark:bg-gray-700 sm:pl-6">
|
<TableCell className="w-full bg-header-primary px-3 py-3.5 sm:pl-6">
|
||||||
<div>{localize('com_ui_bookmarks_count')}</div>
|
<div>{localize('com_ui_bookmarks_count')}</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|
@ -69,7 +74,7 @@ const BookmarkTable = () => {
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between py-4">
|
<div className="flex items-center justify-between py-4">
|
||||||
<div className="pl-1 text-gray-400">
|
<div className="pl-1 text-text-secondary">
|
||||||
{localize('com_ui_showing')} {pageIndex * pageSize + 1} -{' '}
|
{localize('com_ui_showing')} {pageIndex * pageSize + 1} -{' '}
|
||||||
{Math.min((pageIndex + 1) * pageSize, filteredRows.length)} {localize('com_ui_of')}{' '}
|
{Math.min((pageIndex + 1) * pageSize, filteredRows.length)} {localize('com_ui_of')}{' '}
|
||||||
{filteredRows.length}
|
{filteredRows.length}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ const BookmarkTableRow: React.FC<BookmarkTableRowProps> = ({ row, moveRow, posit
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
const ref = useRef<HTMLTableRowElement>(null);
|
const ref = useRef<HTMLTableRowElement>(null);
|
||||||
|
|
||||||
const mutation = useConversationTagMutation(row.tag);
|
const mutation = useConversationTagMutation({ context: 'BookmarkTableRow', tag: row.tag });
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const { showToast } = useToastContext();
|
const { showToast } = useToastContext();
|
||||||
|
|
||||||
|
|
@ -73,7 +73,7 @@ const BookmarkTableRow: React.FC<BookmarkTableRowProps> = ({ row, moveRow, posit
|
||||||
return (
|
return (
|
||||||
<TableRow
|
<TableRow
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className="cursor-move hover:bg-surface-secondary"
|
className="cursor-move hover:bg-surface-tertiary"
|
||||||
style={{ opacity: isDragging ? 0.5 : 1 }}
|
style={{ opacity: isDragging ? 0.5 : 1 }}
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,37 @@
|
||||||
|
import React from 'react';
|
||||||
import { cn } from '~/utils';
|
import { cn } from '~/utils';
|
||||||
|
|
||||||
export default function EditIcon({ className = 'icon-md', size = '1.2em' }) {
|
const EditIcon = React.forwardRef<SVGSVGElement>(
|
||||||
return (
|
(
|
||||||
<svg
|
props: {
|
||||||
fill="none"
|
className?: string;
|
||||||
strokeWidth="2"
|
size?: string;
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
},
|
||||||
viewBox="0 0 24 24"
|
ref,
|
||||||
strokeLinecap="round"
|
) => {
|
||||||
strokeLinejoin="round"
|
const { className = 'icon-md', size = '1.2em' } = props;
|
||||||
height={size}
|
return (
|
||||||
width={size}
|
<svg
|
||||||
className={cn(className)}
|
ref={ref}
|
||||||
>
|
fill="none"
|
||||||
<path
|
strokeWidth="2"
|
||||||
fillRule="evenodd"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
clipRule="evenodd"
|
viewBox="0 0 24 24"
|
||||||
d="M13.2929 4.29291C15.0641 2.52167 17.9359 2.52167 19.7071 4.2929C21.4783 6.06414 21.4783 8.93588 19.7071 10.7071L18.7073 11.7069L11.1603 19.2539C10.7182 19.696 10.1489 19.989 9.53219 20.0918L4.1644 20.9864C3.84584 21.0395 3.52125 20.9355 3.29289 20.7071C3.06453 20.4788 2.96051 20.1542 3.0136 19.8356L3.90824 14.4678C4.01103 13.8511 4.30396 13.2818 4.7461 12.8397L13.2929 4.29291ZM13 7.41422L6.16031 14.2539C6.01293 14.4013 5.91529 14.591 5.88102 14.7966L5.21655 18.7835L9.20339 18.119C9.40898 18.0847 9.59872 17.9871 9.7461 17.8397L16.5858 11L13 7.41422ZM18 9.5858L14.4142 6.00001L14.7071 5.70712C15.6973 4.71693 17.3027 4.71693 18.2929 5.70712C19.2831 6.69731 19.2831 8.30272 18.2929 9.29291L18 9.5858Z"
|
strokeLinecap="round"
|
||||||
fill="currentColor"
|
strokeLinejoin="round"
|
||||||
></path>
|
height={size}
|
||||||
</svg>
|
width={size}
|
||||||
);
|
className={cn(className)}
|
||||||
}
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M13.2929 4.29291C15.0641 2.52167 17.9359 2.52167 19.7071 4.2929C21.4783 6.06414 21.4783 8.93588 19.7071 10.7071L18.7073 11.7069L11.1603 19.2539C10.7182 19.696 10.1489 19.989 9.53219 20.0918L4.1644 20.9864C3.84584 21.0395 3.52125 20.9355 3.29289 20.7071C3.06453 20.4788 2.96051 20.1542 3.0136 19.8356L3.90824 14.4678C4.01103 13.8511 4.30396 13.2818 4.7461 12.8397L13.2929 4.29291ZM13 7.41422L6.16031 14.2539C6.01293 14.4013 5.91529 14.591 5.88102 14.7966L5.21655 18.7835L9.20339 18.119C9.40898 18.0847 9.59872 17.9871 9.7461 17.8397L16.5858 11L13 7.41422ZM18 9.5858L14.4142 6.00001L14.7071 5.70712C15.6973 4.71693 17.3027 4.71693 18.2929 5.70712C19.2831 6.69731 19.2831 8.30272 18.2929 9.29291L18 9.5858Z"
|
||||||
|
fill="currentColor"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default EditIcon;
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import {
|
||||||
useSharedLinksInfiniteQuery,
|
useSharedLinksInfiniteQuery,
|
||||||
} from './queries';
|
} from './queries';
|
||||||
import {
|
import {
|
||||||
|
logger,
|
||||||
/* Shared Links */
|
/* Shared Links */
|
||||||
addSharedLink,
|
addSharedLink,
|
||||||
deleteSharedLink,
|
deleteSharedLink,
|
||||||
|
|
@ -96,6 +97,7 @@ export const useUpdateConversationMutation = (
|
||||||
*/
|
*/
|
||||||
export const useTagConversationMutation = (
|
export const useTagConversationMutation = (
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
|
options?: t.updateTagsInConvoOptions,
|
||||||
): UseMutationResult<t.TTagConversationResponse, unknown, t.TTagConversationRequest, unknown> => {
|
): UseMutationResult<t.TTagConversationResponse, unknown, t.TTagConversationRequest, unknown> => {
|
||||||
const query = useConversationTagsQuery();
|
const query = useConversationTagsQuery();
|
||||||
const { updateTagsInConversation } = useUpdateTagsInConvo();
|
const { updateTagsInConversation } = useUpdateTagsInConvo();
|
||||||
|
|
@ -103,13 +105,17 @@ export const useTagConversationMutation = (
|
||||||
(payload: t.TTagConversationRequest) =>
|
(payload: t.TTagConversationRequest) =>
|
||||||
dataService.addTagToConversation(conversationId, payload),
|
dataService.addTagToConversation(conversationId, payload),
|
||||||
{
|
{
|
||||||
onSuccess: (updatedTags) => {
|
onSuccess: (updatedTags, ...rest) => {
|
||||||
// Because the logic for calculating the bookmark count is complex,
|
// Because the logic for calculating the bookmark count is complex,
|
||||||
// the client does not perform the calculation,
|
// the client does not perform the calculation,
|
||||||
// but instead refetch the data from the API.
|
// but instead refetch the data from the API.
|
||||||
query.refetch();
|
query.refetch();
|
||||||
updateTagsInConversation(conversationId, updatedTags);
|
updateTagsInConversation(conversationId, updatedTags);
|
||||||
|
|
||||||
|
options?.onSuccess?.(updatedTags, ...rest);
|
||||||
},
|
},
|
||||||
|
onError: options?.onError,
|
||||||
|
onMutate: options?.onMutate,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -296,62 +302,99 @@ export const useDeleteSharedLinkMutation = (
|
||||||
});
|
});
|
||||||
const current = queryClient.getQueryData<t.ConversationData>([QueryKeys.sharedLinks]);
|
const current = queryClient.getQueryData<t.ConversationData>([QueryKeys.sharedLinks]);
|
||||||
refetch({
|
refetch({
|
||||||
refetchPage: (page, index) => index === (current?.pages.length || 1) - 1,
|
refetchPage: (page, index) => index === (current?.pages.length ?? 1) - 1,
|
||||||
});
|
});
|
||||||
onSuccess?.(_data, vars, context);
|
onSuccess?.(_data, vars, context);
|
||||||
},
|
},
|
||||||
...(_options || {}),
|
..._options,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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,
|
context,
|
||||||
options?: t.UpdateConversationTagOptions,
|
tag,
|
||||||
): UseMutationResult<t.TConversationTagResponse, unknown, t.TConversationTagRequest, unknown> => {
|
options,
|
||||||
|
}: {
|
||||||
|
context: string;
|
||||||
|
tag?: string;
|
||||||
|
options?: t.UpdateConversationTagOptions;
|
||||||
|
}): UseMutationResult<t.TConversationTagResponse, unknown, t.TConversationTagRequest, unknown> => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { ..._options } = options || {};
|
const { onSuccess, ..._options } = options || {};
|
||||||
|
const onMutationSuccess: typeof onSuccess = (_data, vars) => {
|
||||||
|
queryClient.setQueryData<t.TConversationTag[]>([QueryKeys.conversationTags], (queryData) => {
|
||||||
|
if (!queryData) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
count: 1,
|
||||||
|
position: 0,
|
||||||
|
tag: Constants.SAVED_TAG,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
] as t.TConversationTag[];
|
||||||
|
}
|
||||||
|
if (tag === undefined || !tag.length) {
|
||||||
|
// Check if the tag already exists
|
||||||
|
const existingTagIndex = queryData.findIndex((item) => item.tag === _data.tag);
|
||||||
|
if (existingTagIndex !== -1) {
|
||||||
|
logger.log(
|
||||||
|
'tag_mutation',
|
||||||
|
`"Created" tag exists, updating from ${context}`,
|
||||||
|
queryData,
|
||||||
|
_data,
|
||||||
|
);
|
||||||
|
// If the tag exists, update it
|
||||||
|
const updatedData = [...queryData];
|
||||||
|
updatedData[existingTagIndex] = { ...updatedData[existingTagIndex], ..._data };
|
||||||
|
return updatedData.sort((a, b) => a.position - b.position);
|
||||||
|
} else {
|
||||||
|
// If the tag doesn't exist, add it
|
||||||
|
logger.log(
|
||||||
|
'tag_mutation',
|
||||||
|
`"Created" tag is new, adding from ${context}`,
|
||||||
|
queryData,
|
||||||
|
_data,
|
||||||
|
);
|
||||||
|
return [...queryData, _data].sort((a, b) => a.position - b.position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.log('tag_mutation', `Updating tag from ${context}`, queryData, _data);
|
||||||
|
return updateConversationTag(queryData, vars, _data, tag);
|
||||||
|
});
|
||||||
|
if (vars.addToConversation === true && vars.conversationId != null && _data.tag) {
|
||||||
|
const currentConvo = queryClient.getQueryData<t.TConversation>([
|
||||||
|
QueryKeys.conversation,
|
||||||
|
vars.conversationId,
|
||||||
|
]);
|
||||||
|
if (!currentConvo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.log(
|
||||||
|
'tag_mutation',
|
||||||
|
`\`updateTagsInConversation\` Update from ${context}`,
|
||||||
|
currentConvo,
|
||||||
|
);
|
||||||
|
updateTagsInConversation(vars.conversationId, [...(currentConvo.tags || []), _data.tag]);
|
||||||
|
}
|
||||||
|
// Change the tag title to the new title
|
||||||
|
if (tag != null) {
|
||||||
|
replaceTagsInAllConversations(tag, _data.tag);
|
||||||
|
}
|
||||||
|
};
|
||||||
const { updateTagsInConversation, replaceTagsInAllConversations } = useUpdateTagsInConvo();
|
const { updateTagsInConversation, replaceTagsInAllConversations } = useUpdateTagsInConvo();
|
||||||
return useMutation(
|
return useMutation(
|
||||||
(payload: t.TConversationTagRequest) =>
|
(payload: t.TConversationTagRequest) =>
|
||||||
tag
|
tag != null
|
||||||
? dataService.updateConversationTag(tag, payload)
|
? dataService.updateConversationTag(tag, payload)
|
||||||
: dataService.createConversationTag(payload),
|
: dataService.createConversationTag(payload),
|
||||||
{
|
{
|
||||||
onSuccess: (_data, vars) => {
|
onSuccess: (...args) => {
|
||||||
queryClient.setQueryData<t.TConversationTag[]>([QueryKeys.conversationTags], (data) => {
|
onMutationSuccess(...args);
|
||||||
if (!data) {
|
onSuccess?.(...args);
|
||||||
return [
|
|
||||||
{
|
|
||||||
count: 1,
|
|
||||||
position: 0,
|
|
||||||
tag: Constants.SAVED_TAG,
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
] as t.TConversationTag[];
|
|
||||||
}
|
|
||||||
if (!tag) {
|
|
||||||
return [...data, _data].sort((a, b) => a.position - b.position);
|
|
||||||
}
|
|
||||||
return updateConversationTag(data, vars, _data, tag);
|
|
||||||
});
|
|
||||||
if (vars.addToConversation && vars.conversationId && _data.tag) {
|
|
||||||
const currentConvo = queryClient.getQueryData<t.TConversation>([
|
|
||||||
QueryKeys.conversation,
|
|
||||||
vars.conversationId,
|
|
||||||
]);
|
|
||||||
if (!currentConvo) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
updateTagsInConversation(vars.conversationId, [...(currentConvo.tags || []), _data.tag]);
|
|
||||||
}
|
|
||||||
// Change the tag title to the new title
|
|
||||||
if (tag) {
|
|
||||||
replaceTagsInAllConversations(tag, _data.tag);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
...(_options || {}),
|
..._options,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -407,20 +450,22 @@ export const useDeleteConversationTagMutation = (
|
||||||
): UseMutationResult<t.TConversationTagResponse, unknown, string, void> => {
|
): UseMutationResult<t.TConversationTagResponse, unknown, string, void> => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const deleteTagInAllConversations = useDeleteTagInConversations();
|
const deleteTagInAllConversations = useDeleteTagInConversations();
|
||||||
|
|
||||||
const { onSuccess, ..._options } = options || {};
|
const { onSuccess, ..._options } = options || {};
|
||||||
|
|
||||||
return useMutation((tag: string) => dataService.deleteConversationTag(tag), {
|
return useMutation((tag: string) => dataService.deleteConversationTag(tag), {
|
||||||
onSuccess: (_data, vars, context) => {
|
onSuccess: (_data, tagToDelete, context) => {
|
||||||
queryClient.setQueryData<t.TConversationTag[]>([QueryKeys.conversationTags], (data) => {
|
queryClient.setQueryData<t.TConversationTag[]>([QueryKeys.conversationTags], (data) => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
return data.filter((t) => t.tag !== vars);
|
return data.filter((t) => t.tag !== tagToDelete);
|
||||||
});
|
});
|
||||||
|
|
||||||
deleteTagInAllConversations(vars);
|
deleteTagInAllConversations(tagToDelete);
|
||||||
onSuccess?.(_data, vars, context);
|
onSuccess?.(_data, tagToDelete, context);
|
||||||
},
|
},
|
||||||
...(_options || {}),
|
..._options,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -817,7 +862,8 @@ export const useUpdateAssistantMutation = (
|
||||||
({ assistant_id, data }: { assistant_id: string; data: t.AssistantUpdateParams }) => {
|
({ assistant_id, data }: { assistant_id: string; data: t.AssistantUpdateParams }) => {
|
||||||
const { endpoint } = data;
|
const { endpoint } = data;
|
||||||
const endpointsConfig = queryClient.getQueryData<t.TEndpointsConfig>([QueryKeys.endpoints]);
|
const endpointsConfig = queryClient.getQueryData<t.TEndpointsConfig>([QueryKeys.endpoints]);
|
||||||
const version = endpointsConfig?.[endpoint].version ?? defaultAssistantsVersion[endpoint];
|
const endpointConfig = endpointsConfig?.[endpoint];
|
||||||
|
const version = endpointConfig?.version ?? defaultAssistantsVersion[endpoint];
|
||||||
return dataService.updateAssistant({
|
return dataService.updateAssistant({
|
||||||
data,
|
data,
|
||||||
version,
|
version,
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import useUpdateTagsInConvo from './useUpdateTagsInConvo';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
const useBookmarkSuccess = (conversationId: string) => {
|
const useBookmarkSuccess = (conversationId: string) => {
|
||||||
const setConversation = useSetRecoilState(store.conversationByIndex(0));
|
const updateConversation = useSetRecoilState(store.updateConversationSelector(conversationId));
|
||||||
const { updateTagsInConversation } = useUpdateTagsInConvo();
|
const { updateTagsInConversation } = useUpdateTagsInConvo();
|
||||||
|
|
||||||
return (newTags: string[]) => {
|
return (newTags: string[]) => {
|
||||||
|
|
@ -11,16 +11,7 @@ const useBookmarkSuccess = (conversationId: string) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
updateTagsInConversation(conversationId, newTags);
|
updateTagsInConversation(conversationId, newTags);
|
||||||
setConversation((prev) => {
|
updateConversation({ tags: newTags });
|
||||||
if (prev) {
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
tags: newTags,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
console.error('Conversation not found for bookmark/tags update');
|
|
||||||
return prev;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,10 @@ export default function useSideNavLinks({
|
||||||
permissionType: PermissionTypes.PROMPTS,
|
permissionType: PermissionTypes.PROMPTS,
|
||||||
permission: Permissions.USE,
|
permission: Permissions.USE,
|
||||||
});
|
});
|
||||||
|
const hasAccessToBookmarks = useHasAccess({
|
||||||
|
permissionType: PermissionTypes.BOOKMARKS,
|
||||||
|
permission: Permissions.USE,
|
||||||
|
});
|
||||||
|
|
||||||
const Links = useMemo(() => {
|
const Links = useMemo(() => {
|
||||||
const links: NavLink[] = [];
|
const links: NavLink[] = [];
|
||||||
|
|
@ -75,13 +79,15 @@ export default function useSideNavLinks({
|
||||||
Component: FilesPanel,
|
Component: FilesPanel,
|
||||||
});
|
});
|
||||||
|
|
||||||
links.push({
|
if (hasAccessToBookmarks) {
|
||||||
title: 'com_sidepanel_conversation_tags',
|
links.push({
|
||||||
label: '',
|
title: 'com_sidepanel_conversation_tags',
|
||||||
icon: Bookmark,
|
label: '',
|
||||||
id: 'bookmarks',
|
icon: Bookmark,
|
||||||
Component: BookmarkPanel,
|
id: 'bookmarks',
|
||||||
});
|
Component: BookmarkPanel,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
links.push({
|
links.push({
|
||||||
title: 'com_sidepanel_hide_panel',
|
title: 'com_sidepanel_hide_panel',
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ const useHasAccess = ({
|
||||||
({ user, permissionType, permission }) => {
|
({ user, permissionType, permission }) => {
|
||||||
if (isAuthenticated && user?.role === SystemRoles.ADMIN) {
|
if (isAuthenticated && user?.role === SystemRoles.ADMIN) {
|
||||||
return true;
|
return true;
|
||||||
} else if (isAuthenticated && user?.role && roles && roles[user.role]) {
|
} else if (isAuthenticated && user?.role != null && roles && roles[user.role]) {
|
||||||
return roles[user.role]?.[permissionType]?.[permission] === true;
|
return roles[user.role]?.[permissionType]?.[permission] === true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -311,6 +311,7 @@ export default {
|
||||||
com_ui_bookmarks_create_success: 'Bookmark created successfully',
|
com_ui_bookmarks_create_success: 'Bookmark created successfully',
|
||||||
com_ui_bookmarks_update_success: 'Bookmark updated successfully',
|
com_ui_bookmarks_update_success: 'Bookmark updated successfully',
|
||||||
com_ui_bookmarks_delete_success: 'Bookmark deleted successfully',
|
com_ui_bookmarks_delete_success: 'Bookmark deleted successfully',
|
||||||
|
com_ui_bookmarks_create_exists: 'This bookmark already exists',
|
||||||
com_ui_bookmarks_create_error: 'There was an error creating the bookmark',
|
com_ui_bookmarks_create_error: 'There was an error creating the bookmark',
|
||||||
com_ui_bookmarks_update_error: 'There was an error updating the bookmark',
|
com_ui_bookmarks_update_error: 'There was an error updating the bookmark',
|
||||||
com_ui_bookmarks_delete_error: 'There was an error deleting the bookmark',
|
com_ui_bookmarks_delete_error: 'There was an error deleting the bookmark',
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import {
|
||||||
atom,
|
atom,
|
||||||
selector,
|
selector,
|
||||||
atomFamily,
|
atomFamily,
|
||||||
|
DefaultValue,
|
||||||
selectorFamily,
|
selectorFamily,
|
||||||
useRecoilState,
|
useRecoilState,
|
||||||
useRecoilValue,
|
useRecoilValue,
|
||||||
|
|
@ -325,6 +326,31 @@ function useClearLatestMessages(context?: string) {
|
||||||
return clearAllLatestMessages;
|
return clearAllLatestMessages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateConversationSelector = selectorFamily({
|
||||||
|
key: 'updateConversationSelector',
|
||||||
|
get: () => () => null as Partial<TConversation> | null,
|
||||||
|
set:
|
||||||
|
(conversationId: string) =>
|
||||||
|
({ set, get }, newPartialConversation) => {
|
||||||
|
if (newPartialConversation instanceof DefaultValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keys = get(conversationKeysAtom);
|
||||||
|
keys.forEach((key) => {
|
||||||
|
set(conversationByIndex(key), (prevConversation) => {
|
||||||
|
if (prevConversation && prevConversation.conversationId === conversationId) {
|
||||||
|
return {
|
||||||
|
...prevConversation,
|
||||||
|
...newPartialConversation,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return prevConversation;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
conversationByIndex,
|
conversationByIndex,
|
||||||
filesByIndex,
|
filesByIndex,
|
||||||
|
|
@ -354,4 +380,5 @@ export default {
|
||||||
useClearSubmissionState,
|
useClearSubmissionState,
|
||||||
useClearLatestMessages,
|
useClearLatestMessages,
|
||||||
showPromptsPopoverFamily,
|
showPromptsPopoverFamily,
|
||||||
|
updateConversationSelector,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { ContentTypes, Constants } from 'librechat-data-provider';
|
||||||
import type { TMessage } from 'librechat-data-provider';
|
import type { TMessage } from 'librechat-data-provider';
|
||||||
|
|
||||||
export const getLengthAndLastTenChars = (str?: string): string => {
|
export const getLengthAndLastTenChars = (str?: string): string => {
|
||||||
if (!str) {
|
if (typeof str !== 'string' || str.length === 0) {
|
||||||
return '0';
|
return '0';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -18,12 +18,15 @@ export const getLatestText = (message?: TMessage | null, includeIndex?: boolean)
|
||||||
if (message.text) {
|
if (message.text) {
|
||||||
return message.text;
|
return message.text;
|
||||||
}
|
}
|
||||||
if (message.content?.length) {
|
if (message.content && message.content.length > 0) {
|
||||||
for (let i = message.content.length - 1; i >= 0; i--) {
|
for (let i = message.content.length - 1; i >= 0; i--) {
|
||||||
const part = message.content[i];
|
const part = message.content[i];
|
||||||
if (part.type === ContentTypes.TEXT && part[ContentTypes.TEXT].value.length > 0) {
|
if (
|
||||||
|
part.type === ContentTypes.TEXT &&
|
||||||
|
((part[ContentTypes.TEXT].value as string | undefined)?.length ?? 0) > 0
|
||||||
|
) {
|
||||||
const text = part[ContentTypes.TEXT].value;
|
const text = part[ContentTypes.TEXT].value;
|
||||||
if (includeIndex) {
|
if (includeIndex === true) {
|
||||||
return `${text}-${i}`;
|
return `${text}-${i}`;
|
||||||
} else {
|
} else {
|
||||||
return text;
|
return text;
|
||||||
|
|
@ -39,9 +42,11 @@ export const getTextKey = (message?: TMessage | null, convoId?: string | null) =
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
const text = getLatestText(message, true);
|
const text = getLatestText(message, true);
|
||||||
return `${message.messageId ?? ''}${Constants.COMMON_DIVIDER}${getLengthAndLastTenChars(text)}${
|
return `${(message.messageId as string | null) ?? ''}${
|
||||||
Constants.COMMON_DIVIDER
|
Constants.COMMON_DIVIDER
|
||||||
}${message.conversationId ?? convoId}`;
|
}${getLengthAndLastTenChars(text)}${Constants.COMMON_DIVIDER}${
|
||||||
|
message.conversationId ?? convoId
|
||||||
|
}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const scrollToEnd = (callback?: () => void) => {
|
export const scrollToEnd = (callback?: () => void) => {
|
||||||
|
|
|
||||||
2
package-lock.json
generated
2
package-lock.json
generated
|
|
@ -31493,7 +31493,7 @@
|
||||||
},
|
},
|
||||||
"packages/data-provider": {
|
"packages/data-provider": {
|
||||||
"name": "librechat-data-provider",
|
"name": "librechat-data-provider",
|
||||||
"version": "0.7.415",
|
"version": "0.7.416",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "librechat-data-provider",
|
"name": "librechat-data-provider",
|
||||||
"version": "0.7.415",
|
"version": "0.7.416",
|
||||||
"description": "data services for librechat apps",
|
"description": "data services for librechat apps",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/index.es.js",
|
"module": "dist/index.es.js",
|
||||||
|
|
|
||||||
|
|
@ -201,4 +201,4 @@ export const conversationTagsList = (pageNumber: string, sort?: string, order?:
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
export const addTagToConversation = (conversationId: string) =>
|
export const addTagToConversation = (conversationId: string) =>
|
||||||
`${conversationsRoot}/tags/${conversationId}`;
|
`${conversationTags()}/convo/${conversationId}`;
|
||||||
|
|
|
||||||
|
|
@ -413,6 +413,7 @@ export const configSchema = z.object({
|
||||||
modelSelect: z.boolean().optional(),
|
modelSelect: z.boolean().optional(),
|
||||||
parameters: z.boolean().optional(),
|
parameters: z.boolean().optional(),
|
||||||
sidePanel: z.boolean().optional(),
|
sidePanel: z.boolean().optional(),
|
||||||
|
bookmarks: z.boolean().optional(),
|
||||||
presets: z.boolean().optional(),
|
presets: z.boolean().optional(),
|
||||||
prompts: z.boolean().optional(),
|
prompts: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,10 @@ export enum PermissionTypes {
|
||||||
* Type for Prompt Permissions
|
* Type for Prompt Permissions
|
||||||
*/
|
*/
|
||||||
PROMPTS = 'PROMPTS',
|
PROMPTS = 'PROMPTS',
|
||||||
|
/**
|
||||||
|
* Type for Bookmarks Permissions
|
||||||
|
*/
|
||||||
|
BOOKMARKS = 'BOOKMARKS',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -41,13 +45,19 @@ export const promptPermissionsSchema = z.object({
|
||||||
[Permissions.SHARE]: z.boolean().default(false),
|
[Permissions.SHARE]: z.boolean().default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const bookmarkPermissionsSchema = z.object({
|
||||||
|
[Permissions.USE]: z.boolean().default(true),
|
||||||
|
});
|
||||||
|
|
||||||
export const roleSchema = z.object({
|
export const roleSchema = z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
[PermissionTypes.PROMPTS]: promptPermissionsSchema,
|
[PermissionTypes.PROMPTS]: promptPermissionsSchema,
|
||||||
|
[PermissionTypes.BOOKMARKS]: bookmarkPermissionsSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TRole = z.infer<typeof roleSchema>;
|
export type TRole = z.infer<typeof roleSchema>;
|
||||||
export type TPromptPermissions = z.infer<typeof promptPermissionsSchema>;
|
export type TPromptPermissions = z.infer<typeof promptPermissionsSchema>;
|
||||||
|
export type TBookmarkPermissions = z.infer<typeof bookmarkPermissionsSchema>;
|
||||||
|
|
||||||
const defaultRolesSchema = z.object({
|
const defaultRolesSchema = z.object({
|
||||||
[SystemRoles.ADMIN]: roleSchema.extend({
|
[SystemRoles.ADMIN]: roleSchema.extend({
|
||||||
|
|
@ -58,10 +68,14 @@ const defaultRolesSchema = z.object({
|
||||||
[Permissions.CREATE]: z.boolean().default(true),
|
[Permissions.CREATE]: z.boolean().default(true),
|
||||||
[Permissions.SHARE]: z.boolean().default(true),
|
[Permissions.SHARE]: z.boolean().default(true),
|
||||||
}),
|
}),
|
||||||
|
[PermissionTypes.BOOKMARKS]: bookmarkPermissionsSchema.extend({
|
||||||
|
[Permissions.USE]: z.boolean().default(true),
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
[SystemRoles.USER]: roleSchema.extend({
|
[SystemRoles.USER]: roleSchema.extend({
|
||||||
name: z.literal(SystemRoles.USER),
|
name: z.literal(SystemRoles.USER),
|
||||||
[PermissionTypes.PROMPTS]: promptPermissionsSchema,
|
[PermissionTypes.PROMPTS]: promptPermissionsSchema,
|
||||||
|
[PermissionTypes.BOOKMARKS]: bookmarkPermissionsSchema,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -69,9 +83,11 @@ export const roleDefaults = defaultRolesSchema.parse({
|
||||||
[SystemRoles.ADMIN]: {
|
[SystemRoles.ADMIN]: {
|
||||||
name: SystemRoles.ADMIN,
|
name: SystemRoles.ADMIN,
|
||||||
[PermissionTypes.PROMPTS]: {},
|
[PermissionTypes.PROMPTS]: {},
|
||||||
|
[PermissionTypes.BOOKMARKS]: {},
|
||||||
},
|
},
|
||||||
[SystemRoles.USER]: {
|
[SystemRoles.USER]: {
|
||||||
name: SystemRoles.USER,
|
name: SystemRoles.USER,
|
||||||
[PermissionTypes.PROMPTS]: {},
|
[PermissionTypes.PROMPTS]: {},
|
||||||
|
[PermissionTypes.BOOKMARKS]: {},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -481,6 +481,7 @@ export const tSharedLinkSchema = z.object({
|
||||||
export type TSharedLink = z.infer<typeof tSharedLinkSchema>;
|
export type TSharedLink = z.infer<typeof tSharedLinkSchema>;
|
||||||
|
|
||||||
export const tConversationTagSchema = z.object({
|
export const tConversationTagSchema = z.object({
|
||||||
|
_id: z.string(),
|
||||||
user: z.string(),
|
user: z.string(),
|
||||||
tag: z.string(),
|
tag: z.string(),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,12 @@ export type CreateSharedLinkOptions = MutationOptions<
|
||||||
types.TSharedLink,
|
types.TSharedLink,
|
||||||
Partial<types.TSharedLink>
|
Partial<types.TSharedLink>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export type updateTagsInConvoOptions = MutationOptions<
|
||||||
|
types.TTagConversationResponse,
|
||||||
|
types.TTagConversationRequest
|
||||||
|
>;
|
||||||
|
|
||||||
export type UpdateSharedLinkOptions = MutationOptions<
|
export type UpdateSharedLinkOptions = MutationOptions<
|
||||||
types.TSharedLink,
|
types.TSharedLink,
|
||||||
Partial<types.TSharedLink>
|
Partial<types.TSharedLink>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue