diff --git a/.gitignore b/.gitignore index 980be5b8eb..ff2ae59633 100644 --- a/.gitignore +++ b/.gitignore @@ -171,5 +171,7 @@ claude-flow.config.json *.sqlite-journal *.sqlite-wal claude-flow +.playwright-mcp/* # Removed Windows wrapper files per user request hive-mind-prompt-*.txt +CLAUDE.md diff --git a/api/server/middleware/limiters/index.js b/api/server/middleware/limiters/index.js index ab110443dc..a38188d2a6 100644 --- a/api/server/middleware/limiters/index.js +++ b/api/server/middleware/limiters/index.js @@ -8,6 +8,7 @@ const forkLimiters = require('./forkLimiters'); const registerLimiter = require('./registerLimiter'); const toolCallLimiter = require('./toolCallLimiter'); const messageLimiters = require('./messageLimiters'); +const promptUsageLimiter = require('./promptUsageLimiter'); const verifyEmailLimiter = require('./verifyEmailLimiter'); const resetPasswordLimiter = require('./resetPasswordLimiter'); @@ -16,6 +17,7 @@ module.exports = { ...importLimiters, ...messageLimiters, ...forkLimiters, + ...promptUsageLimiter, loginLimiter, registerLimiter, toolCallLimiter, diff --git a/api/server/middleware/limiters/promptUsageLimiter.js b/api/server/middleware/limiters/promptUsageLimiter.js new file mode 100644 index 0000000000..38bdeed636 --- /dev/null +++ b/api/server/middleware/limiters/promptUsageLimiter.js @@ -0,0 +1,17 @@ +const rateLimit = require('express-rate-limit'); +const { limiterCache } = require('@librechat/api'); + +const PROMPT_USAGE_WINDOW_MS = 60 * 1000; // 1 minute +const PROMPT_USAGE_MAX = 30; // 30 usage increments per user per minute + +const promptUsageLimiter = rateLimit({ + windowMs: PROMPT_USAGE_WINDOW_MS, + max: PROMPT_USAGE_MAX, + handler: (_req, res) => { + res.status(429).json({ message: 'Too many prompt usage requests. Try again later' }); + }, + keyGenerator: (req) => req.user?.id, + store: limiterCache('prompt_usage_limiter'), +}); + +module.exports = { promptUsageLimiter }; diff --git a/api/server/routes/prompts.js b/api/server/routes/prompts.js index 60165d367b..5fcf51ba73 100644 --- a/api/server/routes/prompts.js +++ b/api/server/routes/prompts.js @@ -1,5 +1,6 @@ const express = require('express'); -const { logger } = require('@librechat/data-schemas'); +const { ObjectId } = require('mongodb'); +const { logger, isValidObjectIdString } = require('@librechat/data-schemas'); const { generateCheckAccess, markPublicPromptGroups, @@ -20,6 +21,8 @@ const { const { SystemCapabilities } = require('@librechat/data-schemas'); const { getListPromptGroupsByAccess, + getOwnedPromptGroupIds, + incrementPromptGroupUsage, makePromptProduction, updatePromptGroup, deletePromptGroup, @@ -34,6 +37,7 @@ const { const { canAccessPromptGroupResource, canAccessPromptViaGroup, + promptUsageLimiter, requireJwtAuth, } = require('~/server/middleware'); const { @@ -60,6 +64,12 @@ const checkPromptCreate = generateCheckAccess({ router.use(requireJwtAuth); router.use(checkPromptAccess); +const checkGlobalPromptShare = generateCheckAccess({ + permissionType: PermissionTypes.PROMPTS, + permissions: [Permissions.USE, Permissions.CREATE], + getRoleByName, +}); + /** * Route to get single prompt group by its ID * GET /groups/:groupId @@ -94,11 +104,10 @@ router.get( router.get('/all', async (req, res) => { try { const userId = req.user.id; - const { name, category, ...otherFilters } = req.query; + const { name, category } = req.query; const { filter, searchShared, searchSharedOnly } = buildPromptGroupFilter({ name, category, - ...otherFilters, }); let accessibleIds = await findAccessibleResources({ @@ -108,16 +117,20 @@ router.get('/all', async (req, res) => { requiredPermissions: PermissionBits.VIEW, }); - const publiclyAccessibleIds = await findPubliclyAccessibleResources({ - resourceType: ResourceType.PROMPTGROUP, - requiredPermissions: PermissionBits.VIEW, - }); + const [publiclyAccessibleIds, ownedPromptGroupIds] = await Promise.all([ + findPubliclyAccessibleResources({ + resourceType: ResourceType.PROMPTGROUP, + requiredPermissions: PermissionBits.VIEW, + }), + getOwnedPromptGroupIds(userId), + ]); const filteredAccessibleIds = await filterAccessibleIdsBySharedLogic({ accessibleIds, searchShared, searchSharedOnly, publicPromptGroupIds: publiclyAccessibleIds, + ownedPromptGroupIds, }); const result = await getListPromptGroupsByAccess({ @@ -149,12 +162,11 @@ router.get('/all', async (req, res) => { router.get('/groups', async (req, res) => { try { const userId = req.user.id; - const { pageSize, limit, cursor, name, category, ...otherFilters } = req.query; + const { pageSize, limit, cursor, name, category } = req.query; const { filter, searchShared, searchSharedOnly } = buildPromptGroupFilter({ name, category, - ...otherFilters, }); let actualLimit = limit; @@ -178,16 +190,20 @@ router.get('/groups', async (req, res) => { requiredPermissions: PermissionBits.VIEW, }); - const publiclyAccessibleIds = await findPubliclyAccessibleResources({ - resourceType: ResourceType.PROMPTGROUP, - requiredPermissions: PermissionBits.VIEW, - }); + const [publiclyAccessibleIds, ownedPromptGroupIds] = await Promise.all([ + findPubliclyAccessibleResources({ + resourceType: ResourceType.PROMPTGROUP, + requiredPermissions: PermissionBits.VIEW, + }), + getOwnedPromptGroupIds(userId), + ]); const filteredAccessibleIds = await filterAccessibleIdsBySharedLogic({ accessibleIds, searchShared, searchSharedOnly, publicPromptGroupIds: publiclyAccessibleIds, + ownedPromptGroupIds, }); // Cursor-based pagination only @@ -291,6 +307,16 @@ const addPromptToGroup = async (req, res) => { return res.status(400).send({ error: 'Prompt is required' }); } + if (typeof prompt.prompt !== 'string' || !prompt.prompt.trim()) { + return res + .status(400) + .send({ error: 'Prompt text is required and must be a non-empty string' }); + } + + if (prompt.type !== 'text' && prompt.type !== 'chat') { + return res.status(400).send({ error: 'Prompt type must be "text" or "chat"' }); + } + // Ensure the prompt is associated with the correct group prompt.groupId = groupId; @@ -321,6 +347,37 @@ router.post( addPromptToGroup, ); +/** + * Records a prompt group usage (increments numberOfGenerations) + * POST /groups/:groupId/use + */ +router.post( + '/groups/:groupId/use', + promptUsageLimiter, + canAccessPromptGroupResource({ + requiredPermission: PermissionBits.VIEW, + }), + async (req, res) => { + try { + const { groupId } = req.params; + if (!isValidObjectIdString(groupId)) { + return res.status(400).send({ error: 'Invalid groupId' }); + } + const result = await incrementPromptGroupUsage(groupId); + res.status(200).send(result); + } catch (error) { + logger.error('[recordPromptUsage]', error); + if (error.message === 'Invalid groupId') { + return res.status(400).send({ error: 'Invalid groupId' }); + } + if (error.message === 'Prompt group not found') { + return res.status(404).send({ error: 'Prompt group not found' }); + } + res.status(500).send({ error: 'Error recording prompt usage' }); + } + }, +); + /** * Updates a prompt group * @param {object} req @@ -332,18 +389,8 @@ router.post( const patchPromptGroup = async (req, res) => { try { const { groupId } = req.params; - const author = req.user.id; - const filter = { _id: groupId, author }; - let canManagePrompts = false; - try { - canManagePrompts = await hasCapability(req.user, SystemCapabilities.MANAGE_PROMPTS); - } catch (err) { - logger.warn(`[patchPromptGroup] capability check failed, denying bypass: ${err.message}`); - } - if (canManagePrompts) { - logger.debug(`[patchPromptGroup] MANAGE_PROMPTS bypass for user ${req.user.id}`); - delete filter.author; - } + // Don't pass author - permissions are now checked by middleware + const filter = { _id: groupId }; const validationResult = safeValidatePromptGroupUpdate(req.body); if (!validationResult.success) { @@ -363,7 +410,7 @@ const patchPromptGroup = async (req, res) => { router.patch( '/groups/:groupId', - checkPromptCreate, + checkGlobalPromptShare, canAccessPromptGroupResource({ requiredPermission: PermissionBits.EDIT, }), @@ -409,6 +456,10 @@ router.get('/', async (req, res) => { // If requesting prompts for a specific group, check permissions if (groupId) { + if (!isValidObjectIdString(groupId)) { + return res.status(400).send({ error: 'Invalid groupId' }); + } + const permissions = await getEffectivePermissions({ userId: req.user.id, role: req.user.role, @@ -423,7 +474,7 @@ router.get('/', async (req, res) => { } // If user has access, fetch all prompts in the group (not just their own) - const prompts = await getPrompts({ groupId }); + const prompts = await getPrompts({ groupId: new ObjectId(groupId) }); return res.status(200).send(prompts); } @@ -460,6 +511,9 @@ const deletePromptController = async (req, res) => { try { const { promptId } = req.params; const { groupId } = req.query; + if (!groupId || !isValidObjectIdString(groupId)) { + return res.status(400).send({ error: 'Invalid or missing groupId' }); + } const query = { promptId, groupId }; const result = await deletePrompt(query); res.status(200).send(result); diff --git a/api/server/routes/prompts.test.js b/api/server/routes/prompts.test.js index a3b868f022..c979023ffc 100644 --- a/api/server/routes/prompts.test.js +++ b/api/server/routes/prompts.test.js @@ -36,6 +36,7 @@ jest.mock('~/models', () => { jest.mock('~/server/middleware', () => ({ requireJwtAuth: (req, res, next) => next(), + promptUsageLimiter: (req, res, next) => next(), canAccessPromptViaGroup: jest.requireActual('~/server/middleware').canAccessPromptViaGroup, canAccessPromptGroupResource: jest.requireActual('~/server/middleware').canAccessPromptGroupResource, diff --git a/client/src/Providers/PromptGroupsContext.tsx b/client/src/Providers/PromptGroupsContext.tsx index 7c9dbe8258..3df373b165 100644 --- a/client/src/Providers/PromptGroupsContext.tsx +++ b/client/src/Providers/PromptGroupsContext.tsx @@ -2,9 +2,9 @@ import React, { createContext, useContext, ReactNode, useMemo } from 'react'; import { PermissionTypes, Permissions } from 'librechat-data-provider'; import type { TPromptGroup } from 'librechat-data-provider'; import type { PromptOption } from '~/common'; -import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon'; import { usePromptGroupsNav, useHasAccess } from '~/hooks'; import { useGetAllPromptGroups } from '~/data-provider'; +import { CategoryIcon } from '~/components/Prompts'; import { mapPromptGroups } from '~/utils'; type AllPromptGroupsData = diff --git a/client/src/components/Chat/Input/PromptsCommand.tsx b/client/src/components/Chat/Input/PromptsCommand.tsx index 1740ed43a2..f05a46f6ac 100644 --- a/client/src/components/Chat/Input/PromptsCommand.tsx +++ b/client/src/components/Chat/Input/PromptsCommand.tsx @@ -4,8 +4,9 @@ import { Spinner, useCombobox } from '@librechat/client'; import { useSetRecoilState, useRecoilValue } from 'recoil'; import type { TPromptGroup } from 'librechat-data-provider'; import type { PromptOption } from '~/common'; -import VariableDialog from '~/components/Prompts/Groups/VariableDialog'; import { removeCharIfLast, detectVariables } from '~/utils'; +import { useRecordPromptUsage } from '~/data-provider'; +import { VariableDialog } from '~/components/Prompts'; import { usePromptGroupsContext } from '~/Providers'; import MentionItem from './MentionItem'; import { useLocalize } from '~/hooks'; @@ -60,6 +61,7 @@ function PromptsCommand({ submitPrompt: (textPrompt: string) => void; }) { const localize = useLocalize(); + const { mutate: recordUsage } = useRecordPromptUsage(); const { allPromptGroups, hasAccess } = usePromptGroupsContext(); const { data, isLoading } = allPromptGroups; @@ -107,9 +109,20 @@ function PromptsCommand({ return; } else { submitPrompt(group.productionPrompt?.prompt ?? ''); + if (group._id) { + recordUsage(group._id); + } } }, - [setSearchValue, setOpen, setShowPromptsPopover, textAreaRef, promptsMap, submitPrompt], + [ + setSearchValue, + setOpen, + setShowPromptsPopover, + textAreaRef, + promptsMap, + submitPrompt, + recordUsage, + ], ); useEffect(() => { diff --git a/client/src/components/Nav/SettingsTabs/Chat/Chat.tsx b/client/src/components/Nav/SettingsTabs/Chat/Chat.tsx index 5cbbd73619..e68d008f3d 100644 --- a/client/src/components/Nav/SettingsTabs/Chat/Chat.tsx +++ b/client/src/components/Nav/SettingsTabs/Chat/Chat.tsx @@ -7,6 +7,20 @@ import ToggleSwitch from '../ToggleSwitch'; import store from '~/store'; const toggleSwitchConfigs = [ + { + stateAtom: store.alwaysMakeProd, + localizationKey: 'com_nav_always_make_prod' as const, + switchId: 'alwaysMakeProd', + hoverCardText: undefined, + key: 'alwaysMakeProd', + }, + { + stateAtom: store.autoSendPrompts, + localizationKey: 'com_nav_auto_send_prompts' as const, + switchId: 'autoSendPrompts', + hoverCardText: 'com_nav_auto_send_prompts_desc' as const, + key: 'autoSendPrompts', + }, { stateAtom: store.enterToSend, localizationKey: 'com_nav_enter_to_send' as const, diff --git a/client/src/components/Prompts/AdvancedSwitch.tsx b/client/src/components/Prompts/AdvancedSwitch.tsx deleted file mode 100644 index b050f6b343..0000000000 --- a/client/src/components/Prompts/AdvancedSwitch.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { useRecoilState, useSetRecoilState } from 'recoil'; -import { PromptsEditorMode } from '~/common'; -import { useLocalize } from '~/hooks'; -import store from '~/store'; - -const { promptsEditorMode, alwaysMakeProd } = store; - -const AdvancedSwitch = () => { - const localize = useLocalize(); - const [mode, setMode] = useRecoilState(promptsEditorMode); - const setAlwaysMakeProd = useSetRecoilState(alwaysMakeProd); - - return ( -
-
-
- - {/* Simple Mode Button */} - - - {/* Advanced Mode Button */} - -
-
- ); -}; - -export default AdvancedSwitch; diff --git a/client/src/components/Prompts/BackToChat.tsx b/client/src/components/Prompts/BackToChat.tsx deleted file mode 100644 index c8e28ba2fe..0000000000 --- a/client/src/components/Prompts/BackToChat.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { ArrowLeft } from 'lucide-react'; -import { useNavigate } from 'react-router-dom'; -import { buttonVariants } from '@librechat/client'; -import { useLocalize } from '~/hooks'; -import { cn } from '~/utils'; - -export default function BackToChat({ className }: { className?: string }) { - const navigate = useNavigate(); - const localize = useLocalize(); - const clickHandler = (event: React.MouseEvent) => { - if (event.button === 0 && !(event.ctrlKey || event.metaKey)) { - event.preventDefault(); - navigate('/c/new'); - } - }; - return ( - - - ); -} diff --git a/client/src/components/Prompts/Groups/ChatGroupItem.tsx b/client/src/components/Prompts/Groups/ChatGroupItem.tsx deleted file mode 100644 index 9c4f149e57..0000000000 --- a/client/src/components/Prompts/Groups/ChatGroupItem.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { useState, useMemo, memo, useRef } from 'react'; -import { Link } from 'react-router-dom'; -import { PermissionBits, ResourceType } from 'librechat-data-provider'; -import { Menu as MenuIcon, Edit as EditIcon, EarthIcon, TextSearch } from 'lucide-react'; -import { - DropdownMenu, - DropdownMenuItem, - DropdownMenuGroup, - DropdownMenuContent, - DropdownMenuTrigger, -} from '@librechat/client'; -import type { TPromptGroup } from 'librechat-data-provider'; -import { useLocalize, useSubmitMessage, useResourcePermissions } from '~/hooks'; -import VariableDialog from '~/components/Prompts/Groups/VariableDialog'; -import PreviewPrompt from '~/components/Prompts/PreviewPrompt'; -import ListCard from '~/components/Prompts/Groups/ListCard'; -import { detectVariables } from '~/utils'; - -function ChatGroupItem({ group }: { group: TPromptGroup }) { - const localize = useLocalize(); - const { submitPrompt } = useSubmitMessage(); - const [isPreviewDialogOpen, setPreviewDialogOpen] = useState(false); - const [isVariableDialogOpen, setVariableDialogOpen] = useState(false); - - const groupIsGlobal = useMemo(() => group.isPublic === true, [group.isPublic]); - - // Check permissions for the promptGroup - const { hasPermission } = useResourcePermissions(ResourceType.PROMPTGROUP, group._id || ''); - const canEdit = hasPermission(PermissionBits.EDIT); - - const triggerButtonRef = useRef(null); - - const onCardClick: React.MouseEventHandler = () => { - const text = group.productionPrompt?.prompt; - if (!text?.trim()) { - return; - } - - if (detectVariables(text)) { - setVariableDialogOpen(true); - return; - } - - submitPrompt(text); - }; - - return ( - <> -
- 0 - ? group.oneliner - : (group.productionPrompt?.prompt ?? '') - } - > - {groupIsGlobal === true && ( -
- -
- )} -
- - - - - - { - e.stopPropagation(); - setPreviewDialogOpen(true); - }} - onKeyDown={(e) => { - e.stopPropagation(); - }} - className="w-full cursor-pointer rounded-lg text-text-primary hover:bg-surface-hover focus:bg-surface-hover disabled:cursor-not-allowed" - > - - {canEdit && ( - - - - - - )} - - -
-
- { - requestAnimationFrame(() => { - triggerButtonRef.current?.focus({ preventScroll: true }); - }); - }} - /> - setVariableDialogOpen(false)} - group={group} - /> - - ); -} - -export default memo(ChatGroupItem); diff --git a/client/src/components/Prompts/Groups/DashGroupItem.tsx b/client/src/components/Prompts/Groups/DashGroupItem.tsx deleted file mode 100644 index ee8c9acf38..0000000000 --- a/client/src/components/Prompts/Groups/DashGroupItem.tsx +++ /dev/null @@ -1,216 +0,0 @@ -import { memo, useState, useRef, useMemo, useCallback, KeyboardEvent } from 'react'; -import { Trans } from 'react-i18next'; -import { EarthIcon, Pen } from 'lucide-react'; -import { useNavigate, useParams } from 'react-router-dom'; -import { PermissionBits, ResourceType, type TPromptGroup } from 'librechat-data-provider'; -import { - Input, - Label, - OGDialog, - OGDialogTrigger, - OGDialogTemplate, - TrashIcon, -} from '@librechat/client'; -import { useDeletePromptGroup, useUpdatePromptGroup } from '~/data-provider'; -import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon'; -import { useLocalize, useResourcePermissions } from '~/hooks'; -import { useLiveAnnouncer } from '~/Providers'; -import { cn } from '~/utils'; - -interface DashGroupItemProps { - group: TPromptGroup; -} - -function DashGroupItemComponent({ group }: DashGroupItemProps) { - const params = useParams(); - const navigate = useNavigate(); - const localize = useLocalize(); - const { announcePolite } = useLiveAnnouncer(); - - const blurTimeoutRef = useRef(null); - const [nameInputValue, setNameInputValue] = useState(group.name); - - const { hasPermission } = useResourcePermissions(ResourceType.PROMPTGROUP, group._id || ''); - const canEdit = hasPermission(PermissionBits.EDIT); - const canDelete = hasPermission(PermissionBits.DELETE); - - const isPublicGroup = useMemo(() => group.isPublic === true, [group.isPublic]); - - const updateGroup = useUpdatePromptGroup({ - onMutate: () => { - if (blurTimeoutRef.current) { - clearTimeout(blurTimeoutRef.current); - } - }, - }); - - const deleteGroup = useDeletePromptGroup({ - onSuccess: (_response, variables) => { - if (variables.id === group._id) { - const announcement = localize('com_ui_prompt_deleted', { 0: group.name }); - announcePolite({ message: announcement, isStatus: true }); - navigate('/d/prompts'); - } - }, - }); - - const { isLoading } = updateGroup; - - const handleSaveRename = useCallback(() => { - console.log(group._id ?? '', { name: nameInputValue }); - updateGroup.mutate({ id: group._id ?? '', payload: { name: nameInputValue } }); - }, [group._id, nameInputValue, updateGroup]); - - const handleKeyDown = useCallback( - (e: KeyboardEvent) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - navigate(`/d/prompts/${group._id}`, { replace: true }); - } - }, - [group._id, navigate], - ); - - const triggerDelete = useCallback(() => { - deleteGroup.mutate({ id: group._id ?? '' }); - }, [group._id, deleteGroup]); - - const handleContainerClick = useCallback(() => { - navigate(`/d/prompts/${group._id}`, { replace: true }); - }, [group._id, navigate]); - - return ( -
- - -
- {canEdit && ( - - - - - -
- setNameInputValue(e.target.value)} - className="w-full" - aria-label={localize('com_ui_rename_prompt_name', { name: group.name })} - /> -
-
- } - selection={{ - selectHandler: handleSaveRename, - selectClasses: - 'bg-surface-submit hover:bg-surface-submit-hover text-white disabled:hover:bg-surface-submit', - selectText: localize('com_ui_save'), - isLoading, - }} - /> - - )} - - {canDelete && ( - - - - - -
- -
-
- } - selection={{ - selectHandler: triggerDelete, - selectClasses: - 'bg-red-600 dark:bg-red-600 hover:bg-red-700 dark:hover:bg-red-800 text-white', - selectText: localize('com_ui_delete'), - }} - /> - - )} -
- - ); -} - -export default memo(DashGroupItemComponent); diff --git a/client/src/components/Prompts/Groups/List.tsx b/client/src/components/Prompts/Groups/List.tsx deleted file mode 100644 index ec19a07068..0000000000 --- a/client/src/components/Prompts/Groups/List.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { FileText, Plus } from 'lucide-react'; -import { Link } from 'react-router-dom'; -import { Button, Skeleton } from '@librechat/client'; -import { PermissionTypes, Permissions } from 'librechat-data-provider'; -import type { TPromptGroup } from 'librechat-data-provider'; -import DashGroupItem from '~/components/Prompts/Groups/DashGroupItem'; -import ChatGroupItem from '~/components/Prompts/Groups/ChatGroupItem'; -import { useLocalize, useHasAccess } from '~/hooks'; -import { cn } from '~/utils'; - -export default function List({ - groups = [], - isChatRoute, - isLoading, -}: { - groups?: TPromptGroup[]; - isChatRoute: boolean; - isLoading: boolean; -}) { - const localize = useLocalize(); - const hasCreateAccess = useHasAccess({ - permissionType: PermissionTypes.PROMPTS, - permission: Permissions.CREATE, - }); - - return ( -
- {hasCreateAccess && ( -
- -
- )} -
-
- {isLoading && isChatRoute && ( - - )} - {isLoading && - !isChatRoute && - Array.from({ length: 10 }).map((_, index: number) => ( - - ))} - {!isLoading && groups.length === 0 && ( -
-
-
-

- {localize('com_ui_no_prompts_title')} -

-

- {localize('com_ui_add_first_prompt')} -

-
- )} - {groups.map((group) => { - if (isChatRoute) { - return ; - } - return ; - })} -
-
-
- ); -} diff --git a/client/src/components/Prompts/Groups/ListCard.tsx b/client/src/components/Prompts/Groups/ListCard.tsx deleted file mode 100644 index 0503a69aa6..0000000000 --- a/client/src/components/Prompts/Groups/ListCard.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react'; -import { Label } from '@librechat/client'; -import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon'; -import { useLocalize } from '~/hooks'; - -export default function ListCard({ - category, - name, - snippet, - onClick, - children, -}: { - category: string; - name: string; - snippet: string; - onClick?: React.MouseEventHandler; - children?: React.ReactNode; -}) { - const localize = useLocalize(); - const handleKeyDown = (event: React.KeyboardEvent) => { - if (event.key === 'Enter' || event.key === ' ') { - event.preventDefault(); - onClick?.(event as unknown as React.MouseEvent); - } - }; - - return ( -
-
-
-
-
{children}
-
-
- {snippet} -
-
- ); -} diff --git a/client/src/components/Prompts/Markdown.tsx b/client/src/components/Prompts/Markdown.tsx deleted file mode 100644 index 23353ed7ea..0000000000 --- a/client/src/components/Prompts/Markdown.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; -import { handleDoubleClick } from '~/utils'; - -export const CodeVariableGfm: React.ElementType = ({ children }: { children: React.ReactNode }) => { - return ( - - {children} - - ); -}; - -const regex = /{{(.*?)}}/g; -export const PromptVariableGfm = ({ - children, -}: { - children: React.ReactNode & React.ReactNode[]; -}) => { - const renderContent = (child: React.ReactNode) => { - if (typeof child === 'object' && child !== null) { - return child; - } - if (typeof child !== 'string') { - return child; - } - - const parts = child.split(regex); - return parts.map((part, index) => - index % 2 === 1 ? ( - - {`{{${part}}}`} - - ) : ( - part - ), - ); - }; - - return

{React.Children.map(children, (child) => renderContent(child))}

; -}; diff --git a/client/src/components/Prompts/PromptDetails.tsx b/client/src/components/Prompts/PromptDetails.tsx deleted file mode 100644 index f759079d18..0000000000 --- a/client/src/components/Prompts/PromptDetails.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import React, { useMemo } from 'react'; -import ReactMarkdown from 'react-markdown'; -import remarkGfm from 'remark-gfm'; -import rehypeKatex from 'rehype-katex'; -import remarkMath from 'remark-math'; -import supersub from 'remark-supersub'; -import { Label } from '@librechat/client'; -import rehypeHighlight from 'rehype-highlight'; -import { replaceSpecialVars } from 'librechat-data-provider'; -import type { TPromptGroup } from 'librechat-data-provider'; -import { codeNoExecution } from '~/components/Chat/Messages/Content/MarkdownComponents'; -import { useLocalize, useAuthContext } from '~/hooks'; -import CategoryIcon from './Groups/CategoryIcon'; -import PromptVariables from './PromptVariables'; -import { PromptVariableGfm } from './Markdown'; -import Description from './Description'; -import Command from './Command'; - -const PromptDetails = ({ group }: { group?: TPromptGroup }) => { - const localize = useLocalize(); - const { user } = useAuthContext(); - - const mainText = useMemo(() => { - const initialText = group?.productionPrompt?.prompt ?? ''; - return replaceSpecialVars({ text: initialText, user }); - }, [group?.productionPrompt?.prompt, user]); - - if (!group) { - return null; - } - - return ( -
-
-
-
-
- {(group.category?.length ?? 0) > 0 ? ( - - ) : null} -
- -
-
-
-
-
-
-

- {localize('com_ui_prompt_text')} -

-
- - {mainText} - -
-
- - - -
-
-
- ); -}; - -export default PromptDetails; diff --git a/client/src/components/Prompts/PromptEditor.tsx b/client/src/components/Prompts/PromptEditor.tsx deleted file mode 100644 index ea0d1ef15c..0000000000 --- a/client/src/components/Prompts/PromptEditor.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import { useMemo, memo } from 'react'; -import remarkGfm from 'remark-gfm'; -import remarkMath from 'remark-math'; -import rehypeKatex from 'rehype-katex'; -import supersub from 'remark-supersub'; -import { useRecoilValue } from 'recoil'; -import { EditIcon } from 'lucide-react'; -import ReactMarkdown from 'react-markdown'; -import rehypeHighlight from 'rehype-highlight'; -import { SaveIcon, CrossIcon, TextareaAutosize } from '@librechat/client'; -import { Controller, useFormContext, useFormState } from 'react-hook-form'; -import type { PluggableList } from 'unified'; -import { codeNoExecution } from '~/components/Chat/Messages/Content/MarkdownComponents'; -import AlwaysMakeProd from '~/components/Prompts/Groups/AlwaysMakeProd'; -import VariablesDropdown from './VariablesDropdown'; -import { PromptVariableGfm } from './Markdown'; -import { PromptsEditorMode } from '~/common'; -import { cn, langSubset } from '~/utils'; -import { useLocalize } from '~/hooks'; -import store from '~/store'; - -const { promptsEditorMode } = store; - -type Props = { - name: string; - isEditing: boolean; - setIsEditing: React.Dispatch>; -}; - -const PromptEditor: React.FC = ({ name, isEditing, setIsEditing }) => { - const localize = useLocalize(); - const { control } = useFormContext(); - const editorMode = useRecoilValue(promptsEditorMode); - const { dirtyFields } = useFormState({ control: control }); - const { prompt } = dirtyFields as { prompt?: string }; - - const EditorIcon = useMemo(() => { - if (isEditing && prompt?.length == null) { - return CrossIcon; - } - return isEditing ? SaveIcon : EditIcon; - }, [isEditing, prompt]); - - const rehypePlugins: PluggableList = [ - [rehypeKatex], - [ - rehypeHighlight, - { - detect: true, - ignoreMissing: true, - subset: langSubset, - }, - ], - ]; - - return ( -
-

{localize('com_ui_control_bar')}

-
- - {localize('com_ui_prompt_text')} - -
- {editorMode === PromptsEditorMode.ADVANCED && ( - - )} - - -
-
-
!isEditing && setIsEditing(true)} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - !isEditing && setIsEditing(true); - } - }} - tabIndex={0} - > - {!isEditing && ( - - )} - - isEditing ? ( - setIsEditing(false)} - onKeyDown={(e) => { - if (e.key === 'Escape') { - e.preventDefault(); - setIsEditing(false); - } - }} - aria-label={localize('com_ui_prompt_input')} - /> - ) : ( -
- - {field.value} - -
- ) - } - /> -
-
- ); -}; - -export default memo(PromptEditor); diff --git a/client/src/components/Prompts/PromptForm.tsx b/client/src/components/Prompts/PromptForm.tsx deleted file mode 100644 index 6f575f8577..0000000000 --- a/client/src/components/Prompts/PromptForm.tsx +++ /dev/null @@ -1,530 +0,0 @@ -import { useEffect, useState, useMemo, useCallback, useRef } from 'react'; -import React from 'react'; -import debounce from 'lodash/debounce'; -import { useRecoilValue } from 'recoil'; -import { Menu, Rocket } from 'lucide-react'; -import { useParams } from 'react-router-dom'; -import { useForm, FormProvider } from 'react-hook-form'; -import { Button, Skeleton, useToastContext } from '@librechat/client'; -import { - Permissions, - ResourceType, - PermissionBits, - PermissionTypes, -} from 'librechat-data-provider'; -import type { TCreatePrompt, TPrompt, TPromptGroup } from 'librechat-data-provider'; -import { - useGetPrompts, - useGetPromptGroup, - useAddPromptToGroup, - useUpdatePromptGroup, - useMakePromptProduction, -} from '~/data-provider'; -import { useResourcePermissions, useHasAccess, useLocalize } from '~/hooks'; -import CategorySelector from './Groups/CategorySelector'; -import { usePromptGroupsContext } from '~/Providers'; -import NoPromptGroup from './Groups/NoPromptGroup'; -import PromptVariables from './PromptVariables'; -import { cn, findPromptGroup } from '~/utils'; -import PromptVersions from './PromptVersions'; -import { PromptsEditorMode } from '~/common'; -import DeleteVersion from './DeleteVersion'; -import PromptDetails from './PromptDetails'; -import PromptEditor from './PromptEditor'; -import SkeletonForm from './SkeletonForm'; -import Description from './Description'; -import SharePrompt from './SharePrompt'; -import PromptName from './PromptName'; -import Command from './Command'; -import store from '~/store'; - -interface RightPanelProps { - group: TPromptGroup; - prompts: TPrompt[]; - selectedPrompt: any; - selectionIndex: number; - selectedPromptId?: string; - isLoadingPrompts: boolean; - canEdit: boolean; - setSelectionIndex: React.Dispatch>; -} - -const RightPanel = React.memo( - ({ - group, - prompts, - selectedPrompt, - selectedPromptId, - isLoadingPrompts, - canEdit, - selectionIndex, - setSelectionIndex, - }: RightPanelProps) => { - const localize = useLocalize(); - const { showToast } = useToastContext(); - const editorMode = useRecoilValue(store.promptsEditorMode); - const hasShareAccess = useHasAccess({ - permissionType: PermissionTypes.PROMPTS, - permission: Permissions.SHARE, - }); - - const updateGroupMutation = useUpdatePromptGroup({ - onError: () => { - showToast({ - status: 'error', - message: localize('com_ui_prompt_update_error'), - }); - }, - }); - - const makeProductionMutation = useMakePromptProduction(); - - const groupId = group?._id || ''; - const groupName = group?.name || ''; - const groupCategory = group?.category || ''; - const isLoadingGroup = !group; - - return ( -
-
- - updateGroupMutation.mutate({ - id: groupId, - payload: { name: groupName, category: value }, - }) - : undefined - } - /> -
- {hasShareAccess && } - {editorMode === PromptsEditorMode.ADVANCED && canEdit && ( - - )} - -
-
- {editorMode === PromptsEditorMode.ADVANCED && - (isLoadingPrompts - ? Array.from({ length: 6 }).map((_, index: number) => ( -
- -
- )) - : prompts.length > 0 && ( - - ))} -
- ); - }, -); - -RightPanel.displayName = 'RightPanel'; - -const PromptForm = () => { - const params = useParams(); - const localize = useLocalize(); - const { showToast } = useToastContext(); - const { hasAccess } = usePromptGroupsContext(); - const alwaysMakeProd = useRecoilValue(store.alwaysMakeProd); - const promptId = params.promptId || ''; - - const editorMode = useRecoilValue(store.promptsEditorMode); - const [selectionIndex, setSelectionIndex] = useState(0); - - const prevIsEditingRef = useRef(false); - const [isEditing, setIsEditing] = useState(false); - const [initialLoad, setInitialLoad] = useState(true); - const [showSidePanel, setShowSidePanel] = useState(false); - const sidePanelWidth = '320px'; - - const { data: group, isLoading: isLoadingGroup } = useGetPromptGroup(promptId, { - enabled: hasAccess && !!promptId, - }); - const { data: prompts = [], isLoading: isLoadingPrompts } = useGetPrompts( - { groupId: promptId }, - { enabled: hasAccess && !!promptId }, - ); - - const { hasPermission, isLoading: permissionsLoading } = useResourcePermissions( - ResourceType.PROMPTGROUP, - group?._id || '', - ); - - const canEdit = hasPermission(PermissionBits.EDIT); - const canView = hasPermission(PermissionBits.VIEW); - - const methods = useForm({ - defaultValues: { - prompt: '', - promptName: group ? group.name : '', - category: group ? group.category : '', - }, - }); - const { handleSubmit, setValue, reset, watch } = methods; - const promptText = watch('prompt'); - - const selectedPrompt = useMemo( - () => (prompts.length > 0 ? prompts[selectionIndex] : undefined), - [prompts, selectionIndex], - ); - - const selectedPromptId = useMemo(() => selectedPrompt?._id, [selectedPrompt?._id]); - - const { groupsQuery } = usePromptGroupsContext(); - - const updateGroupMutation = useUpdatePromptGroup({ - onError: () => { - showToast({ - status: 'error', - message: localize('com_ui_prompt_update_error'), - }); - }, - }); - - const makeProductionMutation = useMakePromptProduction(); - const addPromptToGroupMutation = useAddPromptToGroup({ - onMutate: (variables) => { - reset( - { - prompt: variables.prompt.prompt, - category: group?.category || '', - }, - { keepDirtyValues: true }, - ); - }, - onSuccess(data) { - if (alwaysMakeProd && data.prompt._id != null && data.prompt._id && data.prompt.groupId) { - makeProductionMutation.mutate({ - id: data.prompt._id, - groupId: data.prompt.groupId, - productionPrompt: { prompt: data.prompt.prompt }, - }); - } - - reset({ - prompt: data.prompt.prompt, - promptName: group?.name || '', - category: group?.category || '', - }); - }, - }); - - const onSave = useCallback( - (value: string) => { - if (!canEdit) { - return; - } - if (!value) { - // TODO: show toast, cannot be empty. - return; - } - if (!selectedPrompt) { - return; - } - - const groupId = selectedPrompt.groupId || group?._id; - if (!groupId) { - console.error('No groupId available'); - return; - } - - const tempPrompt: TCreatePrompt = { - prompt: { - type: selectedPrompt.type ?? 'text', - groupId: groupId, - prompt: value, - }, - }; - - if (value === selectedPrompt.prompt) { - return; - } - - // We're adding to an existing group, so use the addPromptToGroup mutation - addPromptToGroupMutation.mutate({ ...tempPrompt, groupId }); - }, - [selectedPrompt, group, addPromptToGroupMutation, canEdit], - ); - - const handleLoadingComplete = useCallback(() => { - if (isLoadingGroup || isLoadingPrompts) { - return; - } - setInitialLoad(false); - }, [isLoadingGroup, isLoadingPrompts]); - - useEffect(() => { - if (prevIsEditingRef.current && !isEditing && canEdit) { - handleSubmit((data) => onSave(data.prompt))(); - } - prevIsEditingRef.current = isEditing; - }, [isEditing, onSave, handleSubmit, canEdit]); - - useEffect(() => { - handleLoadingComplete(); - }, [params.promptId, editorMode, group?.productionId, prompts, handleLoadingComplete]); - - useEffect(() => { - setValue('prompt', selectedPrompt ? selectedPrompt.prompt : '', { shouldDirty: false }); - setValue('category', group ? group.category : '', { shouldDirty: false }); - }, [selectedPrompt, group, setValue]); - - useEffect(() => { - const handleResize = () => { - if (window.matchMedia('(min-width: 1022px)').matches) { - setShowSidePanel(false); - } - }; - - window.addEventListener('resize', handleResize); - return () => window.removeEventListener('resize', handleResize); - }, []); - - const debouncedUpdateOneliner = useMemo( - () => - debounce((groupId: string, oneliner: string, mutate: any) => { - mutate({ id: groupId, payload: { oneliner } }); - }, 950), - [], - ); - - const debouncedUpdateCommand = useMemo( - () => - debounce((groupId: string, command: string, mutate: any) => { - mutate({ id: groupId, payload: { command } }); - }, 950), - [], - ); - - const handleUpdateOneliner = useCallback( - (oneliner: string) => { - if (!group || !group._id) { - return console.warn('Group not found'); - } - debouncedUpdateOneliner(group._id, oneliner, updateGroupMutation.mutate); - }, - [group, updateGroupMutation.mutate, debouncedUpdateOneliner], - ); - - const handleUpdateCommand = useCallback( - (command: string) => { - if (!group || !group._id) { - return console.warn('Group not found'); - } - debouncedUpdateCommand(group._id, command, updateGroupMutation.mutate); - }, - [group, updateGroupMutation.mutate, debouncedUpdateCommand], - ); - - if (initialLoad) { - return ; - } - - // Show read-only view if user doesn't have edit permission - if (!canEdit && !permissionsLoading && groupsQuery.data) { - const fetchedPrompt = findPromptGroup( - groupsQuery.data, - (group) => group._id === params.promptId, - ); - if (!fetchedPrompt && !canView) { - return ; - } - - if (fetchedPrompt || group) { - return ; - } - } - - if (!group || group._id == null) { - return null; - } - - const groupName = group.name; - - return ( - -
onSave(data.prompt))}> -

{localize('com_ui_edit_prompt_page')}

-
-
-
-
-
- {isLoadingGroup ? ( - - ) : ( - <> - { - if (!canEdit || !group._id) { - return; - } - updateGroupMutation.mutate({ - id: group._id, - payload: { name: value }, - }); - }} - /> -
- -
- {editorMode === PromptsEditorMode.SIMPLE && ( - - )} -
- - )} -
- {isLoadingPrompts ? ( - - ) : ( -
- canEdit && setIsEditing(value)} - /> - - - -
- )} -
- - {editorMode === PromptsEditorMode.ADVANCED && ( -
- -
- )} -
-
- -
- - - ); -}; - -export default PromptForm; diff --git a/client/src/components/Prompts/PromptName.tsx b/client/src/components/Prompts/PromptName.tsx deleted file mode 100644 index cfb18ec973..0000000000 --- a/client/src/components/Prompts/PromptName.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import React, { useEffect, useState, useRef } from 'react'; -import { Button, Label, Input, EditIcon, SaveIcon } from '@librechat/client'; - -type Props = { - name?: string; - onSave: (newName: string) => void; -}; - -const PromptName: React.FC = ({ name, onSave }) => { - const inputRef = useRef(null); - const blurTimeoutRef = useRef(); - const [isEditing, setIsEditing] = useState(false); - const [newName, setNewName] = useState(name); - - const handleEditClick = () => { - setIsEditing(true); - }; - - const handleInputChange = (e: React.ChangeEvent) => { - setNewName(e.target.value); - }; - - const saveName = () => { - const savedName = newName?.trim(); - onSave(savedName || ''); - setIsEditing(false); - }; - - const handleSaveClick: React.MouseEventHandler = () => { - saveName(); - clearTimeout(blurTimeoutRef.current); - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Escape') { - setIsEditing(false); - setNewName(name); - } - if (e.key === 'Enter') { - saveName(); - } - }; - - useEffect(() => { - if (isEditing) { - inputRef.current?.focus(); - } - }, [isEditing]); - - useEffect(() => { - setNewName(name); - }, [name]); - - return ( -
-
- {isEditing ? ( - <> - - - - - ) : ( - <> - - - - )} -
-
- ); -}; - -export default PromptName; diff --git a/client/src/components/Prompts/PromptVariables.tsx b/client/src/components/Prompts/PromptVariables.tsx deleted file mode 100644 index fbb956e844..0000000000 --- a/client/src/components/Prompts/PromptVariables.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import React, { useMemo } from 'react'; -import { Variable } from 'lucide-react'; -import ReactMarkdown from 'react-markdown'; -import { Separator } from '@librechat/client'; -import { specialVariables } from 'librechat-data-provider'; -import { cn, extractUniqueVariables } from '~/utils'; -import { CodeVariableGfm } from './Markdown'; -import { useLocalize } from '~/hooks'; - -const specialVariableClasses = - 'bg-amber-100 text-yellow-800 border-yellow-600 dark:border-yellow-500/50 dark:bg-transparent dark:text-yellow-500/90'; - -const components: { - [nodeType: string]: React.ElementType; -} = { code: CodeVariableGfm }; - -const PromptVariables = ({ - promptText, - showInfo = true, -}: { - promptText: string; - showInfo?: boolean; -}) => { - const localize = useLocalize(); - - const variables = useMemo(() => { - return extractUniqueVariables(promptText || ''); - }, [promptText]); - - return ( -
-

-

-
- {variables.length ? ( -
- {variables.map((variable, index) => ( - - {specialVariables[variable.toLowerCase()] != null - ? variable.toLowerCase() - : variable} - - ))} -
- ) : ( -
- - {localize('com_ui_variables_info')} - -
- )} - - {showInfo && ( -
-
- - {localize('com_ui_special_variables')} - - - - {localize('com_ui_special_variables_more_info')} - - -
-
- - {localize('com_ui_dropdown_variables')} - - - - {localize('com_ui_dropdown_variables_info')} - - -
-
- )} -
-
- ); -}; - -export default PromptVariables; diff --git a/client/src/components/Prompts/PromptVersions.tsx b/client/src/components/Prompts/PromptVersions.tsx deleted file mode 100644 index d21562edcd..0000000000 --- a/client/src/components/Prompts/PromptVersions.tsx +++ /dev/null @@ -1,192 +0,0 @@ -import React from 'react'; -import { format } from 'date-fns'; -import { Layers3, Crown, Zap } from 'lucide-react'; -import { Tag, TooltipAnchor, Label } from '@librechat/client'; -import type { TPrompt, TPromptGroup } from 'librechat-data-provider'; -import { useLocalize } from '~/hooks'; -import { cn } from '~/utils'; - -const CombinedStatusIcon = ({ description }: { description: string }) => ( - - -
- } - > -); - -const VersionTags = ({ tags }: { tags: string[] }) => { - const localize = useLocalize(); - const isLatestAndProduction = tags.includes('latest') && tags.includes('production'); - - if (isLatestAndProduction) { - return ( - - - - ); - } - - return ( - - {tags.map((tag, i) => ( - { - if (tag === 'production') { - return ( -
- -
- ); - } - if (tag === 'latest') { - return ( -
- -
- ); - } - return null; - })()} - /> - } - >
- ))} -
- ); -}; - -const VersionCard = ({ - prompt, - index, - isSelected, - totalVersions, - onClick, - authorName, - tags, -}: { - prompt: TPrompt; - index: number; - isSelected: boolean; - totalVersions: number; - onClick: () => void; - authorName?: string; - tags: string[]; -}) => { - const localize = useLocalize(); - - return ( - - ); -}; - -const PromptVersions = ({ - prompts, - group, - selectionIndex, - setSelectionIndex, -}: { - prompts: TPrompt[]; - group?: TPromptGroup; - selectionIndex: number; - setSelectionIndex: React.Dispatch>; -}) => { - const localize = useLocalize(); - - return ( -
-
-

- - {localize('com_ui_versions')} -

-
- -
- {prompts.map((prompt: TPrompt, index: number) => { - const tags: string[] = []; - - if (index === 0) { - tags.push('latest'); - } - - if (prompt._id === group?.productionId) { - tags.push('production'); - } - - return ( - setSelectionIndex(index)} - authorName={group?.authorName} - tags={tags} - /> - ); - })} -
-
- ); -}; - -export default PromptVersions; diff --git a/client/src/components/Prompts/PromptsAccordion.tsx b/client/src/components/Prompts/PromptsAccordion.tsx deleted file mode 100644 index 67ece18cc2..0000000000 --- a/client/src/components/Prompts/PromptsAccordion.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import PromptSidePanel from '~/components/Prompts/Groups/GroupSidePanel'; -import AutoSendPrompt from '~/components/Prompts/Groups/AutoSendPrompt'; -import FilterPrompts from '~/components/Prompts/Groups/FilterPrompts'; -import { usePromptGroupsContext } from '~/Providers'; - -export default function PromptsAccordion() { - const groupsNav = usePromptGroupsContext(); - return ( -
- - -
- -
-
-
- ); -} diff --git a/client/src/components/Prompts/VariablesDropdown.tsx b/client/src/components/Prompts/VariablesDropdown.tsx deleted file mode 100644 index bfde6525c9..0000000000 --- a/client/src/components/Prompts/VariablesDropdown.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { useState, useId } from 'react'; -import { PlusCircle } from 'lucide-react'; -import * as Menu from '@ariakit/react/menu'; -import { useFormContext } from 'react-hook-form'; -import { DropdownPopup } from '@librechat/client'; -import { specialVariables } from 'librechat-data-provider'; -import type { TSpecialVarLabel } from 'librechat-data-provider'; -import { useLiveAnnouncer } from '~/Providers'; -import { useLocalize } from '~/hooks'; - -interface VariableOption { - label: TSpecialVarLabel; - value: string; -} - -const variableOptions: VariableOption[] = Object.keys(specialVariables).map((key) => ({ - label: `com_ui_special_var_${key}` as TSpecialVarLabel, - value: `{{${key}}}`, -})); - -interface VariablesDropdownProps { - fieldName?: string; - className?: string; -} - -export default function VariablesDropdown({ - fieldName = 'prompt', - className = '', -}: VariablesDropdownProps) { - const menuId = useId(); - const localize = useLocalize(); - const methods = useFormContext(); - const { setValue, getValues } = methods; - const { announcePolite } = useLiveAnnouncer(); - - const [isMenuOpen, setIsMenuOpen] = useState(false); - - const handleAddVariable = (label: TSpecialVarLabel, value: string) => { - const currentText = getValues(fieldName) || ''; - const spacer = currentText.length > 0 ? '\n\n' : ''; - const prefix = localize(label); - setValue(fieldName, currentText + spacer + prefix + ': ' + value); - setIsMenuOpen(false); - const announcement = localize('com_ui_special_variable_added', { 0: prefix }); - announcePolite({ message: announcement, isStatus: true }); - }; - - return ( -
- - - {localize('com_ui_special_variables')} - - } - items={variableOptions.map((option) => ({ - label: localize(option.label) || option.label, - onClick: () => handleAddVariable(option.label, option.value), - }))} - menuId={menuId} - className="z-30" - /> -
- ); -} diff --git a/client/src/components/Prompts/AdminSettings.tsx b/client/src/components/Prompts/buttons/AdminSettings.tsx similarity index 93% rename from client/src/components/Prompts/AdminSettings.tsx rename to client/src/components/Prompts/buttons/AdminSettings.tsx index 6d382fbb91..25e6df05a8 100644 --- a/client/src/components/Prompts/AdminSettings.tsx +++ b/client/src/components/Prompts/buttons/AdminSettings.tsx @@ -2,10 +2,10 @@ import { useState } from 'react'; import { ShieldEllipsis } from 'lucide-react'; import { Permissions, PermissionTypes } from 'librechat-data-provider'; import { OGDialog, OGDialogTemplate, Button, useToastContext } from '@librechat/client'; -import { AdminSettingsDialog } from '~/components/ui'; -import { useUpdatePromptPermissionsMutation } from '~/data-provider'; -import { useLocalize } from '~/hooks'; import type { PermissionConfig } from '~/components/ui'; +import { useUpdatePromptPermissionsMutation } from '~/data-provider'; +import { AdminSettingsDialog } from '~/components/ui'; +import { useLocalize } from '~/hooks'; const permissions: PermissionConfig[] = [ { permission: Permissions.USE, labelKey: 'com_ui_prompts_allow_use' }, @@ -43,7 +43,8 @@ const AdminSettings = () => { ); } diff --git a/client/src/components/Prompts/buttons/BackToChat.tsx b/client/src/components/Prompts/buttons/BackToChat.tsx new file mode 100644 index 0000000000..0840e87f46 --- /dev/null +++ b/client/src/components/Prompts/buttons/BackToChat.tsx @@ -0,0 +1,33 @@ +import { useMemo } from 'react'; +import { ArrowLeft } from 'lucide-react'; +import { buttonVariants } from '@librechat/client'; +import { useDashboardContext } from '~/Providers'; +import { useLocalize, useCustomLink } from '~/hooks'; +import { cn } from '~/utils'; + +export default function BackToChat({ className }: { className?: string }) { + const localize = useLocalize(); + const { prevLocationPath } = useDashboardContext(); + + const conversationId = useMemo(() => { + if (!prevLocationPath || prevLocationPath.includes('/d/')) { + return 'new'; + } + const parts = prevLocationPath.split('/'); + return parts[parts.length - 1]; + }, [prevLocationPath]); + + const href = `/c/${conversationId}`; + const clickHandler = useCustomLink(href); + + return ( + + + ); +} diff --git a/client/src/components/Prompts/buttons/CreatePromptButton.tsx b/client/src/components/Prompts/buttons/CreatePromptButton.tsx new file mode 100644 index 0000000000..016721d509 --- /dev/null +++ b/client/src/components/Prompts/buttons/CreatePromptButton.tsx @@ -0,0 +1,37 @@ +import { Plus } from 'lucide-react'; +import { Link } from 'react-router-dom'; +import { Button, TooltipAnchor } from '@librechat/client'; +import { PermissionTypes, Permissions } from 'librechat-data-provider'; +import { useHasAccess, useLocalize } from '~/hooks'; + +export default function CreatePromptButton() { + const localize = useLocalize(); + const hasCreateAccess = useHasAccess({ + permissionType: PermissionTypes.PROMPTS, + permission: Permissions.CREATE, + }); + + if (!hasCreateAccess) { + return null; + } + + return ( + + +