diff --git a/api/models/Categories.js b/api/models/Categories.js index fc2cbdd98b..0f7f29703f 100644 --- a/api/models/Categories.js +++ b/api/models/Categories.js @@ -1,10 +1,6 @@ const { logger } = require('~/config'); // const { Categories } = require('./schema/categories'); const options = [ - { - label: '', - value: '', - }, { label: 'idea', value: 'idea', diff --git a/client/src/components/Bookmarks/DeleteBookmarkButton.tsx b/client/src/components/Bookmarks/DeleteBookmarkButton.tsx index d592c3cd72..e9dcf0e4d1 100644 --- a/client/src/components/Bookmarks/DeleteBookmarkButton.tsx +++ b/client/src/components/Bookmarks/DeleteBookmarkButton.tsx @@ -65,7 +65,7 @@ const DeleteBookmarkButton: FC<{ {localize('com_ui_bookmark_delete_confirm')} {bookmark} diff --git a/client/src/components/Prompts/AdminSettings.tsx b/client/src/components/Prompts/AdminSettings.tsx index b3925ca6d4..11ee14ff58 100644 --- a/client/src/components/Prompts/AdminSettings.tsx +++ b/client/src/components/Prompts/AdminSettings.tsx @@ -1,13 +1,22 @@ import * as Ariakit from '@ariakit/react'; +import { ExternalLink } from 'lucide-react'; import { useMemo, useEffect, useState } from 'react'; import { ShieldEllipsis } from 'lucide-react'; import { useForm, Controller } from 'react-hook-form'; import { Permissions, SystemRoles, roleDefaults, PermissionTypes } from 'librechat-data-provider'; import type { Control, UseFormSetValue, UseFormGetValues } from 'react-hook-form'; -import { OGDialog, OGDialogTitle, OGDialogContent, OGDialogTrigger } from '~/components/ui'; +import { + OGDialog, + OGDialogTitle, + OGDialogContent, + OGDialogTrigger, + Button, + Switch, + DropdownPopup, +} from '~/components/ui'; import { useUpdatePromptPermissionsMutation } from '~/data-provider'; +import OGDialogTemplate from '~/components/ui/OGDialogTemplate'; import { useLocalize, useAuthContext } from '~/hooks'; -import { Button, Switch, DropdownPopup } from '~/components/ui'; import { useToastContext } from '~/Providers'; type FormValues = Record; @@ -18,28 +27,17 @@ type LabelControllerProps = { control: Control; setValue: UseFormSetValue; getValues: UseFormGetValues; + confirmChange?: (newValue: boolean, onChange: (value: boolean) => void) => void; }; const LabelController: React.FC = ({ control, promptPerm, label, - getValues, - setValue, + confirmChange, }) => (
- + {label} = ({ { + if (val === false && confirmChange) { + confirmChange(val, field.onChange); + } else { + field.onChange(val); + } + }} value={field.value.toString()} /> )} @@ -59,6 +63,10 @@ const AdminSettings = () => { const localize = useLocalize(); const { user, roles } = useAuthContext(); const { showToast } = useToastContext(); + const [confirmAdminUseChange, setConfirmAdminUseChange] = useState<{ + newValue: boolean; + callback: (value: boolean) => void; + } | null>(null); const { mutate, isLoading } = useUpdatePromptPermissionsMutation({ onSuccess: () => { showToast({ status: 'success', message: localize('com_ui_saved') }); @@ -137,82 +145,117 @@ const AdminSettings = () => { ]; return ( - - - - - - - {`${localize('com_ui_admin_settings')} - ${localize('com_ui_prompts')}`} - -
- {/* Role selection dropdown */} -
- {localize('com_ui_role_select')}: - - {selectedRole} - - } - items={roleDropdownItems} - itemClassName="items-center justify-center" - sameWidth={true} - /> + <> + + + + + + + {`${localize('com_ui_admin_settings')} - ${localize('com_ui_prompts')}`} + +
+ {/* Role selection dropdown */} +
+ {localize('com_ui_role_select')}: + + {selectedRole} + + } + items={roleDropdownItems} + itemClassName="items-center justify-center" + sameWidth={true} + /> +
+
+
+ {labelControllerData.map(({ promptPerm, label }) => ( +
+ void, + ) => setConfirmAdminUseChange({ newValue, callback: onChange }), + } + : {})} + /> + {selectedRole === SystemRoles.ADMIN && promptPerm === Permissions.USE && ( + <> +
+ {localize('com_ui_admin_access_warning')} + {'\n'} + + {localize('com_ui_more_info')} + + +
+ + )} +
+ ))} +
+
+ +
+
-
-
- {labelControllerData.map(({ promptPerm, label }) => ( -
- - {selectedRole === SystemRoles.ADMIN && promptPerm === Permissions.USE && ( - <> -
- {localize('com_ui_admin_access_warning')} - {'\n'} - - {localize('com_ui_more_info')} - -
- - )} -
- ))} -
-
- -
-
-
- - + + + + { + if (!open) { + setConfirmAdminUseChange(null); + } + }} + > + {localize('com_ui_confirm_admin_use_change')}

} + selection={{ + selectHandler: () => { + if (confirmAdminUseChange) { + confirmAdminUseChange.callback(confirmAdminUseChange.newValue); + } + setConfirmAdminUseChange(null); + }, + selectClasses: + 'bg-surface-destructive hover:bg-surface-destructive-hover text-white transition-colors duration-200', + selectText: localize('com_ui_confirm_action'), + isLoading: false, + }} + /> +
+ ); }; diff --git a/client/src/components/Prompts/AdvancedSwitch.tsx b/client/src/components/Prompts/AdvancedSwitch.tsx index c0ee9d273c..d8d6c219fc 100644 --- a/client/src/components/Prompts/AdvancedSwitch.tsx +++ b/client/src/components/Prompts/AdvancedSwitch.tsx @@ -11,32 +11,45 @@ const AdvancedSwitch = () => { const setAlwaysMakeProd = useSetRecoilState(alwaysMakeProd); return ( -
-
+
+
+
+ + {/* Simple Mode Button */} + + {/* Advanced Mode Button */}
diff --git a/client/src/components/Prompts/Command.tsx b/client/src/components/Prompts/Command.tsx index a80dc1d78c..905a57cc87 100644 --- a/client/src/components/Prompts/Command.tsx +++ b/client/src/components/Prompts/Command.tsx @@ -1,6 +1,7 @@ import { SquareSlash } from 'lucide-react'; import { Constants } from 'librechat-data-provider'; import { useState, useEffect } from 'react'; +import { Input } from '~/components/ui'; import { useLocalize } from '~/hooks'; const Command = ({ @@ -43,20 +44,20 @@ const Command = ({ } return ( -
-

+
+

- {disabled !== true && ( - {`${charCount}/${Constants.COMMANDS_MAX_LENGTH}`} + {`${charCount}/${Constants.COMMANDS_MAX_LENGTH}`} )}

diff --git a/client/src/components/Prompts/DeleteVersion.tsx b/client/src/components/Prompts/DeleteVersion.tsx index 9a2800933a..ba54e49f53 100644 --- a/client/src/components/Prompts/DeleteVersion.tsx +++ b/client/src/components/Prompts/DeleteVersion.tsx @@ -18,15 +18,16 @@ const DeleteVersion = ({ diff --git a/client/src/components/Prompts/Description.tsx b/client/src/components/Prompts/Description.tsx index 452c624ce8..6ea3388af4 100644 --- a/client/src/components/Prompts/Description.tsx +++ b/client/src/components/Prompts/Description.tsx @@ -1,8 +1,9 @@ import { useState, useEffect } from 'react'; -import { Info } from 'lucide-react'; +import { Input } from '~/components/ui'; import { useLocalize } from '~/hooks'; +import { Info } from 'lucide-react'; -const MAX_LENGTH = 56; +const MAX_LENGTH = 120; const Description = ({ initialValue, @@ -40,20 +41,20 @@ const Description = ({ } return ( -
-

+
+

- {!disabled && ( - {`${charCount}/${MAX_LENGTH}`} + {`${charCount}/${MAX_LENGTH}`} )}

diff --git a/client/src/components/Prompts/Groups/AlwaysMakeProd.tsx b/client/src/components/Prompts/Groups/AlwaysMakeProd.tsx index 7ecdf739b6..6a3132775e 100644 --- a/client/src/components/Prompts/Groups/AlwaysMakeProd.tsx +++ b/client/src/components/Prompts/Groups/AlwaysMakeProd.tsx @@ -28,6 +28,7 @@ export default function AlwaysMakeProd({ checked={alwaysMakeProd} onCheckedChange={handleCheckedChange} data-testid="alwaysMakeProd" + aria-label="Always make prompt production" />
{localize('com_nav_always_make_prod')}

diff --git a/client/src/components/Prompts/Groups/CategorySelector.tsx b/client/src/components/Prompts/Groups/CategorySelector.tsx index 61c30c6c76..6e2245e8c3 100644 --- a/client/src/components/Prompts/Groups/CategorySelector.tsx +++ b/client/src/components/Prompts/Groups/CategorySelector.tsx @@ -1,59 +1,80 @@ import React, { useMemo } from 'react'; import { useFormContext, Controller } from 'react-hook-form'; import { LocalStorageKeys } from 'librechat-data-provider'; -import { useLocalize, useCategories } from '~/hooks'; -import { cn, createDropdownSetter } from '~/utils'; -import { SelectDropDown } from '~/components/ui'; +import { Dropdown } from '~/components/ui'; +import { useCategories } from '~/hooks'; -const CategorySelector = ({ - currentCategory, - onValueChange, - className = '', - tabIndex, -}: { +interface CategorySelectorProps { currentCategory?: string; onValueChange?: (value: string) => void; className?: string; - tabIndex?: number; +} + +const CategorySelector: React.FC = ({ + currentCategory, + onValueChange, + className = '', }) => { - const localize = useLocalize(); - const { control, watch, setValue } = useFormContext(); + const formContext = useFormContext(); const { categories, emptyCategory } = useCategories(); - const watchedCategory = watch('category'); + const control = formContext.control; + const watch = formContext.watch; + const setValue = formContext.setValue; + + const watchedCategory = watch ? watch('category') : currentCategory; + const categoryOption = useMemo( () => - categories.find((category) => category.value === (watchedCategory ?? currentCategory)) ?? - emptyCategory, + (categories ?? []).find( + (category) => category.value === (watchedCategory ?? currentCategory), + ) ?? emptyCategory, [watchedCategory, categories, currentCategory, emptyCategory], ); - return ( + return formContext ? ( ( - { + { setValue('category', value, { shouldDirty: false }); localStorage.setItem(LocalStorageKeys.LAST_PROMPT_CATEGORY, value); onValueChange?.(value); - })} - availableValues={categories} - showAbove={false} - showLabel={false} - emptyTitle={true} - showOptionIcon={true} - searchPlaceholder={localize('com_ui_search_categories')} - className={cn('h-10 w-56 cursor-pointer', className)} - currentValueClass="text-md gap-2" - optionsListClass="text-sm max-h-72" + }} + aria-labelledby="category-selector-label" + ariaLabel="Prompt's category selector" + className={className} + options={categories || []} + renderValue={(option) => ( +
+ {option.icon != null && {option.icon as React.ReactNode}} + {option.label} +
+ )} /> )} /> + ) : ( + { + localStorage.setItem(LocalStorageKeys.LAST_PROMPT_CATEGORY, value); + onValueChange?.(value); + }} + aria-labelledby="category-selector-label" + ariaLabel="Prompt's category selector" + className={className} + options={categories || []} + renderValue={(option) => ( +
+ {option.icon != null && {option.icon as React.ReactNode}} + {option.label} +
+ )} + /> ); }; diff --git a/client/src/components/Prompts/Groups/ChatGroupItem.tsx b/client/src/components/Prompts/Groups/ChatGroupItem.tsx index b372f8a59b..f9b2850af6 100644 --- a/client/src/components/Prompts/Groups/ChatGroupItem.tsx +++ b/client/src/components/Prompts/Groups/ChatGroupItem.tsx @@ -80,7 +80,7 @@ function ChatGroupItem({ e.stopPropagation(); } }} - className="z-50 inline-flex h-7 w-7 items-center justify-center rounded-md border border-border-medium bg-transparent p-0 text-sm font-medium transition-all duration-300 ease-in-out hover:border-border-heavy hover:bg-surface-secondary focus:border-border-heavy focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" + className="z-50 inline-flex h-8 w-8 items-center justify-center rounded-lg border border-border-medium bg-transparent p-0 text-sm font-medium transition-all duration-300 ease-in-out hover:border-border-heavy hover:bg-surface-hover focus:border-border-heavy focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" >

)} /> - +
@@ -166,7 +166,12 @@ const CreatePromptForm = ({ /> methods.setValue('command', value)} tabIndex={0} />
-
diff --git a/client/src/components/Prompts/Groups/DashGroupItem.tsx b/client/src/components/Prompts/Groups/DashGroupItem.tsx index 533a33d8c9..7fb46df047 100644 --- a/client/src/components/Prompts/Groups/DashGroupItem.tsx +++ b/client/src/components/Prompts/Groups/DashGroupItem.tsx @@ -1,237 +1,175 @@ -import { useState, useRef, useMemo } from 'react'; -import { MenuIcon, EarthIcon } from 'lucide-react'; +import { memo, useState, useRef, useMemo, useCallback, KeyboardEvent } from 'react'; +import { EarthIcon, Pen } from 'lucide-react'; import { useNavigate, useParams } from 'react-router-dom'; import { SystemRoles, type TPromptGroup } from 'librechat-data-provider'; import { useDeletePromptGroup, useUpdatePromptGroup } from '~/data-provider'; -import { - Input, - Label, - Button, - OGDialog, - DropdownMenu, - OGDialogTrigger, - DropdownMenuGroup, - DropdownMenuContent, - DropdownMenuTrigger, - DropdownMenuItem, -} from '~/components/ui'; +import { Input, Label, Button, OGDialog, OGDialogTrigger } from '~/components/ui'; import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon'; import OGDialogTemplate from '~/components/ui/OGDialogTemplate'; import { useLocalize, useAuthContext } from '~/hooks'; import { TrashIcon } from '~/components/svg'; import { cn } from '~/utils/'; -export default function DashGroupItem({ - group, - instanceProjectId, -}: { +interface DashGroupItemProps { group: TPromptGroup; instanceProjectId?: string; -}) { +} + +function DashGroupItemComponent({ group, instanceProjectId }: DashGroupItemProps) { const params = useParams(); const navigate = useNavigate(); const localize = useLocalize(); - const { user } = useAuthContext(); - const blurTimeoutRef = useRef(); - const [nameEditFlag, setNameEditFlag] = useState(false); - const [nameInputField, setNameInputField] = useState(group.name); - const isOwner = useMemo(() => user?.id === group.author, [user, group]); - const groupIsGlobal = useMemo( - () => instanceProjectId != null && group.projectIds?.includes(instanceProjectId), - [group, instanceProjectId], + + const blurTimeoutRef = useRef(null); + const [nameInputValue, setNameInputValue] = useState(group.name); + + const isOwner = useMemo(() => user?.id === group.author, [user?.id, group.author]); + const isGlobalGroup = useMemo( + () => instanceProjectId && group.projectIds?.includes(instanceProjectId), + [group.projectIds, instanceProjectId], ); const updateGroup = useUpdatePromptGroup({ onMutate: () => { - clearTimeout(blurTimeoutRef.current); - setNameEditFlag(false); + if (blurTimeoutRef.current) { + clearTimeout(blurTimeoutRef.current); + } }, }); - const deletePromptGroupMutation = useDeletePromptGroup({ - onSuccess: (response, variables) => { + + const deleteGroup = useDeletePromptGroup({ + onSuccess: (_response, variables) => { if (variables.id === group._id) { navigate('/d/prompts'); } }, }); - const cancelRename = () => { - setNameEditFlag(false); - }; + const { isLoading } = updateGroup; - const saveRename = () => { - updateGroup.mutate({ payload: { name: nameInputField }, id: group._id ?? '' }); - }; + const handleSaveRename = useCallback(() => { + console.log(group._id ?? '', { name: nameInputValue }); + updateGroup.mutate({ id: group._id ?? '', payload: { name: nameInputValue } }); + }, [group._id, nameInputValue, updateGroup]); - const handleBlur = () => { - blurTimeoutRef.current = setTimeout(() => { - cancelRename(); - }, 100); - }; + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); + navigate(`/d/prompts/${group._id}`, { replace: true }); + } + }, + [group._id, navigate], + ); - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - e.preventDefault(); - navigate(`/d/prompts/${group._id}`, { replace: true }); - } - }; + const triggerDelete = useCallback(() => { + deleteGroup.mutate({ id: group._id ?? '' }); + }, [group._id, deleteGroup]); - const handleRename = (e: Event) => { - e.stopPropagation(); - setNameEditFlag(true); - }; - - const handleDelete = () => { - deletePromptGroupMutation.mutate({ id: group._id ?? '' }); - }; + const handleContainerClick = useCallback(() => { + navigate(`/d/prompts/${group._id}`, { replace: true }); + }, [group._id, navigate]); return (
{ - if (!nameEditFlag) { - navigate(`/d/prompts/${group._id}`, { replace: true }); - } - }} + onClick={handleContainerClick} onKeyDown={handleKeyDown} role="button" tabIndex={0} aria-label={`${group.name} prompt group`} > -
-
- {nameEditFlag ? ( +
+
+
+ +
+ {isGlobalGroup && ( + + )} + {(isOwner || user?.role === SystemRoles.ADMIN) && ( <> -
- - e.stopPropagation()} - onChange={(e) => setNameInputField(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Escape') { - cancelRename(); - } else if (e.key === 'Enter') { - saveRename(); - } - }} - onBlur={handleBlur} - aria-label={localize('com_ui_rename_group')} - /> - -
-
- {localize('com_ui_renaming_var', group.name)} -
- - ) : ( - <> -
-
-
-
- {groupIsGlobal === true && ( - - )} - {(isOwner || user?.role === SystemRoles.ADMIN) && ( - <> - - - - - - - - {localize('com_ui_rename')} - - - - - - - - - -
-
- -
-
- - } - selection={{ - selectHandler: handleDelete, - selectClasses: - 'bg-red-600 dark:bg-red-600 hover:bg-red-700 dark:hover:bg-red-800 text-white', - selectText: localize('com_ui_delete'), - }} + + + + + +
+ setNameInputValue(e.target.value)} + className="w-full" + aria-label={localize('com_ui_rename_prompt') + ' ' + group.name} /> - - - )} -
-
-
- {group.oneliner ?? '' ? group.oneliner : group.productionPrompt?.prompt ?? ''} -
+
+
+ } + 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, + }} + /> + + + + + + + +
+ +
+
+ } + 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'), + }} + /> + )}
@@ -239,3 +177,5 @@ export default function DashGroupItem({
); } + +export default memo(DashGroupItemComponent); diff --git a/client/src/components/Prompts/Groups/FilterPrompts.tsx b/client/src/components/Prompts/Groups/FilterPrompts.tsx index 88893868f2..0d0ab9a2fc 100644 --- a/client/src/components/Prompts/Groups/FilterPrompts.tsx +++ b/client/src/components/Prompts/Groups/FilterPrompts.tsx @@ -1,109 +1,13 @@ -import { ListFilter, User, Share2, Dot } from 'lucide-react'; -import React, { useState, useCallback, useMemo } from 'react'; +import { ListFilter, User, Share2 } from 'lucide-react'; +import React, { useState, useCallback, useMemo, useEffect } from 'react'; import { useRecoilValue, useSetRecoilState } from 'recoil'; import { SystemCategories } from 'librechat-data-provider'; -import type { OptionWithIcon } from '~/common'; import { usePromptGroupsNav, useLocalize, useCategories } from '~/hooks'; -import { - Input, - Button, - DropdownMenu, - DropdownMenuItem, - DropdownMenuGroup, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuSeparator, -} from '~/components/ui'; +import { Dropdown, AnimatedSearchInput } from '~/components/ui'; +import type { Option } from '~/common'; import { cn } from '~/utils'; import store from '~/store'; -export function FilterItem({ - label, - icon, - onClick, - isActive, -}: { - label: string; - icon: React.ReactNode; - onClick?: () => void; - isActive?: boolean; -}) { - return ( - - {icon} - {label} - {isActive === true && ( - - - - )} - - ); -} - -export function FilterMenu({ - onSelect, -}: { - onSelect: (category: string, icon?: React.ReactNode | null) => void; -}) { - const localize = useLocalize(); - const { categories } = useCategories('h-4 w-4'); - const memoizedCategories = useMemo(() => { - const noCategory = { - label: localize('com_ui_no_category'), - value: SystemCategories.NO_CATEGORY, - }; - if (!categories) { - return [noCategory]; - } - - return [noCategory, ...categories]; - }, [categories, localize]); - - const categoryFilter = useRecoilValue(store.promptsCategory); - return ( - - - } - onClick={() => onSelect(SystemCategories.ALL, )} - isActive={categoryFilter === ''} - /> - } - onClick={() => onSelect(SystemCategories.MY_PROMPTS, )} - isActive={categoryFilter === SystemCategories.MY_PROMPTS} - /> - } - onClick={() => onSelect(SystemCategories.SHARED_PROMPTS, )} - isActive={categoryFilter === SystemCategories.SHARED_PROMPTS} - /> - - - - {memoizedCategories - .filter((category) => category.value) - .map((category, i) => ( - onSelect(category.value, (category as OptionWithIcon).icon)} - isActive={category.value === categoryFilter} - /> - ))} - - - ); -} - export default function FilterPrompts({ setName, className = '', @@ -113,46 +17,81 @@ export default function FilterPrompts({ const localize = useLocalize(); const [displayName, setDisplayName] = useState(''); const setCategory = useSetRecoilState(store.promptsCategory); - const [selectedIcon, setSelectedIcon] = useState(); + const categoryFilter = useRecoilValue(store.promptsCategory); + const { categories } = useCategories('h-4 w-4'); + const [isSearching, setIsSearching] = useState(false); + + const filterOptions = useMemo(() => { + const baseOptions: Option[] = [ + { + value: SystemCategories.ALL, + label: localize('com_ui_all_proper'), + icon: , + }, + { + value: SystemCategories.MY_PROMPTS, + label: localize('com_ui_my_prompts'), + icon: , + }, + { + value: SystemCategories.SHARED_PROMPTS, + label: localize('com_ui_shared_prompts'), + icon: , + }, + { divider: true, value: null }, + ]; + + const categoryOptions = categories + ? [...categories] + : [ + { + value: SystemCategories.NO_CATEGORY, + label: localize('com_ui_no_category'), + }, + ]; + + return [...baseOptions, ...categoryOptions]; + }, [categories, localize]); const onSelect = useCallback( - (category: string, icon?: React.ReactNode | null) => { - if (category === SystemCategories.ALL) { - setSelectedIcon(); - return setCategory(''); - } - setCategory(category); - if (icon != null && React.isValidElement(icon)) { - setSelectedIcon(icon); + (value: string) => { + if (value === SystemCategories.ALL) { + setCategory(''); + } else { + setCategory(value); } }, [setCategory], ); + useEffect(() => { + setIsSearching(true); + const timeout = setTimeout(() => { + setIsSearching(false); + }, 500); + return () => clearTimeout(timeout); + }, [displayName]); + return ( -
- - - - - - - + } + label="Filter: " + ariaLabel={localize('com_ui_filter_prompts')} + iconOnly + /> + { setDisplayName(e.target.value); setName(e.target.value); }} - className="w-full border-border-light placeholder:text-text-secondary" + isSearching={isSearching} + placeholder={localize('com_ui_filter_prompts_name')} />
); diff --git a/client/src/components/Prompts/Groups/GroupSidePanel.tsx b/client/src/components/Prompts/Groups/GroupSidePanel.tsx index 23170da54f..74e3de1745 100644 --- a/client/src/components/Prompts/Groups/GroupSidePanel.tsx +++ b/client/src/components/Prompts/Groups/GroupSidePanel.tsx @@ -29,7 +29,7 @@ export default function GroupSidePanel({ return (
)} @@ -43,16 +45,18 @@ export default function List({ {isLoading && isChatRoute && ( )} - {isLoading && !isChatRoute && ( - - )} + {isLoading && + !isChatRoute && + Array.from({ length: 10 }).map((_, index: number) => ( + + ))} {!isLoading && groups.length === 0 && isChatRoute && (
{localize('com_ui_nothing_found')}
)} {!isLoading && groups.length === 0 && !isChatRoute && ( -
+
{localize('com_ui_nothing_found')}
)} diff --git a/client/src/components/Prompts/Groups/ListCard.tsx b/client/src/components/Prompts/Groups/ListCard.tsx index c17faed8bf..4de783caaa 100644 --- a/client/src/components/Prompts/Groups/ListCard.tsx +++ b/client/src/components/Prompts/Groups/ListCard.tsx @@ -1,5 +1,6 @@ import React from 'react'; import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon'; +import { Label } from '~/components/ui'; export default function ListCard({ category, @@ -25,25 +26,31 @@ export default function ListCard({
-
+
{children}
-
+
{snippet}
diff --git a/client/src/components/Prompts/Groups/PanelNavigation.tsx b/client/src/components/Prompts/Groups/PanelNavigation.tsx index 39df43f160..76395e842d 100644 --- a/client/src/components/Prompts/Groups/PanelNavigation.tsx +++ b/client/src/components/Prompts/Groups/PanelNavigation.tsx @@ -19,7 +19,7 @@ function PanelNavigation({ }) { const localize = useLocalize(); return ( -
+
{!isChatRoute && }
diff --git a/client/src/components/Prompts/Groups/VariableForm.tsx b/client/src/components/Prompts/Groups/VariableForm.tsx index eb8b6683ac..096c7416a9 100644 --- a/client/src/components/Prompts/Groups/VariableForm.tsx +++ b/client/src/components/Prompts/Groups/VariableForm.tsx @@ -15,8 +15,8 @@ import { extractVariableInfo, } from '~/utils'; import { codeNoExecution } from '~/components/Chat/Messages/Content/Markdown'; +import { TextareaAutosize, InputCombobox, Button } from '~/components/ui'; import { useAuthContext, useLocalize, useSubmitMessage } from '~/hooks'; -import { TextareaAutosize, InputCombobox } from '~/components/ui'; type FieldType = 'text' | 'select'; @@ -202,12 +202,9 @@ export default function VariableForm({ ))}
- +
diff --git a/client/src/components/Prompts/ManagePrompts.tsx b/client/src/components/Prompts/ManagePrompts.tsx index 6c9ce8661b..a05e54cbcb 100644 --- a/client/src/components/Prompts/ManagePrompts.tsx +++ b/client/src/components/Prompts/ManagePrompts.tsx @@ -14,10 +14,19 @@ export default function ManagePrompts({ className }: { className?: string }) { setPromptsCategory(''); }, [setPromptsName, setPromptsCategory]); - const clickHandler = useCustomLink('/d/prompts', clickCallback); + const customLink = useCustomLink('/d/prompts', clickCallback); + const clickHandler = (e: React.MouseEvent) => { + customLink(e as unknown as React.MouseEvent); + }; return ( - ); diff --git a/client/src/components/Prompts/PreviewPrompt.tsx b/client/src/components/Prompts/PreviewPrompt.tsx index 368840d698..1b1efba70c 100644 --- a/client/src/components/Prompts/PreviewPrompt.tsx +++ b/client/src/components/Prompts/PreviewPrompt.tsx @@ -13,7 +13,7 @@ const PreviewPrompt = ({ }) => { return ( - +
diff --git a/client/src/components/Prompts/PromptDetails.tsx b/client/src/components/Prompts/PromptDetails.tsx index 4dec2dd4fe..53aeff3182 100644 --- a/client/src/components/Prompts/PromptDetails.tsx +++ b/client/src/components/Prompts/PromptDetails.tsx @@ -12,6 +12,7 @@ import CategoryIcon from './Groups/CategoryIcon'; import PromptVariables from './PromptVariables'; import { PromptVariableGfm } from './Markdown'; import { replaceSpecialVars } from '~/utils'; +import { Label } from '~/components/ui'; import Description from './Description'; import Command from './Command'; @@ -30,25 +31,25 @@ const PromptDetails = ({ group }: { group?: TPromptGroup }) => { return (
-
+
-
+
{(group.category?.length ?? 0) > 0 ? ( ) : null}
- {group.name} +
-
-
+
+
-

+

{localize('com_ui_prompt_text')}

-
+
= ({ name, isEditing, setIsEditing }) => { ]; return ( -
-

- {localize('com_ui_prompt_text')} -
+
+

+ + {localize('com_ui_prompt_text')} + +
{editorMode === PromptsEditorMode.ADVANCED && ( )} - + )} + { + if (!selectedPrompt || !selectedPrompt._id) { + console.warn('No prompt is selected or prompt _id is missing'); + return; + } + deletePromptMutation.mutate({ + _id: selectedPrompt._id, + groupId, + }); + }} + /> +
+

+ {editorMode === PromptsEditorMode.ADVANCED && + (isLoadingPrompts + ? Array.from({ length: 6 }).map((_, index: number) => ( +
+ +
+ )) + : prompts.length > 0 && ( + + ))} +
+ ); + return ( -
onSave(data.prompt))}> -
-
- {isLoadingGroup ? ( - - ) : ( - { - if (!group) { - return console.warn('Group not found'); - } - updateGroupMutation.mutate({ id: group._id || '', payload: { name: value } }); - }} - /> - )} -
- - updateGroupMutation.mutate({ - id: group._id || '', - payload: { name: group.name || '', category: value }, - }) - } - /> - {hasShareAccess && } + onSave(data.prompt))}> +
+
+
+
+
+ {isLoadingGroup ? ( + + ) : ( + <> + { + if (!group._id) { + return; + } + updateGroupMutation.mutate({ id: group._id, payload: { name: value } }); + }} + /> +
+ +
+ {editorMode === PromptsEditorMode.SIMPLE && } +
+ + )} +
+ {isLoadingPrompts ? ( + + ) : ( +
+ + + + +
+ )} +
+ {editorMode === PromptsEditorMode.ADVANCED && ( - - )} - { - deletePromptMutation.mutate({ - _id: selectedPrompt?._id || '', - groupId: group._id || '', - }); - }} - /> -
-
- {editorMode === PromptsEditorMode.ADVANCED && ( -
- -
- )} -
- {/* Left Section */} -
- {isLoadingPrompts ? ( - - ) : ( -
- - - - +
+
)}
- {/* Right Section */} - {editorMode === PromptsEditorMode.ADVANCED && ( -
- {isLoadingPrompts ? ( - - ) : ( - !!prompts.length && ( - - ) - )} -
+
+ +
diff --git a/client/src/components/Prompts/PromptName.tsx b/client/src/components/Prompts/PromptName.tsx index 77edbe726c..3066d629e6 100644 --- a/client/src/components/Prompts/PromptName.tsx +++ b/client/src/components/Prompts/PromptName.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState, useRef } from 'react'; -import { EditIcon, SaveIcon } from '~/components/svg'; +import { Button, Label, Input, EditIcon, SaveIcon } from '~/components'; type Props = { name?: string; @@ -31,21 +31,12 @@ const PromptName: React.FC = ({ name, onSave }) => { clearTimeout(blurTimeoutRef.current); }; - const handleBlur = () => { - blurTimeoutRef.current = setTimeout(() => { - if (document.activeElement !== inputRef.current) { - setIsEditing(false); - setNewName(name); - } - }, 200); - }; - const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Escape') { setIsEditing(false); setNewName(name); } - if (e.key === 'Enter' || e.key === 'Tab') { + if (e.key === 'Enter') { saveName(); } }; @@ -61,39 +52,64 @@ const PromptName: React.FC = ({ name, onSave }) => { }, [name]); return ( -
- {isEditing ? ( -
- - -
- ) : ( -
- {newName} - -
- )} +
+
+ {isEditing ? ( + <> + + + + + ) : ( + <> + + + + )} +
); }; diff --git a/client/src/components/Prompts/PromptVariables.tsx b/client/src/components/Prompts/PromptVariables.tsx index 8fd25d3095..8b8140e066 100644 --- a/client/src/components/Prompts/PromptVariables.tsx +++ b/client/src/components/Prompts/PromptVariables.tsx @@ -28,18 +28,18 @@ const PromptVariables = ({ }, [promptText]); return ( -
-

+
+

{localize('com_ui_variables')}

-
+
{variables.length ? ( -
+
{variables.map((variable, index) => ( -
) : ( -
- - {/** @ts-ignore */} - - {localize('com_ui_variables_info')} - - +
+ + {localize('com_ui_variables_info')} +
)} - + {showInfo && ( -
+
- + {localize('com_ui_special_variables')} - {'\u00A0'} - - {/** @ts-ignore */} + {localize('com_ui_special_variables_info')}
- + {localize('com_ui_dropdown_variables')} - {'\u00A0'} - - {/** @ts-ignore */} + {localize('com_ui_dropdown_variables_info')} diff --git a/client/src/components/Prompts/PromptVersions.tsx b/client/src/components/Prompts/PromptVersions.tsx index 86415e3c80..2f34a8d2e2 100644 --- a/client/src/components/Prompts/PromptVersions.tsx +++ b/client/src/components/Prompts/PromptVersions.tsx @@ -1,11 +1,140 @@ import React from 'react'; import { format } from 'date-fns'; -import { Layers3 } from 'lucide-react'; +import { Layers3, Crown, Zap } from 'lucide-react'; import type { TPrompt, TPromptGroup } from 'librechat-data-provider'; +import { Tag, TooltipAnchor, Label } from '~/components/ui'; import { useLocalize } from '~/hooks'; -import { Tag } from '~/components/ui'; 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, @@ -14,19 +143,24 @@ const PromptVersions = ({ }: { prompts: TPrompt[]; group?: TPromptGroup; - selectionIndex: React.SetStateAction; + selectionIndex: number; setSelectionIndex: React.Dispatch>; }) => { const localize = useLocalize(); + return ( - <> -

- - {localize('com_ui_versions')} -

-
    +
    +
    +

    + + {localize('com_ui_versions')} +

    +
    + +
    {prompts.map((prompt: TPrompt, index: number) => { const tags: string[] = []; + if (index === 0) { tags.push('latest'); } @@ -36,53 +170,20 @@ const PromptVersions = ({ } return ( -
  • setSelectionIndex(index)} - > -

    - {localize('com_ui_version_var', `${prompts.length - index}`)} -

    -

    - {format(new Date(prompt.createdAt), 'yyyy-MM-dd HH:mm')} -

    - {tags.length > 0 && ( - - {tags.map((tag, i) => { - return ( - - -
  • - ) : null - } - /> - ); - })} - - )} - {group?.authorName && ( -

    by {group.authorName}

    - )} - + authorName={group?.authorName} + tags={tags} + /> ); })} -
- +
+ ); }; diff --git a/client/src/components/Prompts/PromptsAccordion.tsx b/client/src/components/Prompts/PromptsAccordion.tsx index ef455a80a7..444646c5ee 100644 --- a/client/src/components/Prompts/PromptsAccordion.tsx +++ b/client/src/components/Prompts/PromptsAccordion.tsx @@ -9,11 +9,11 @@ export default function PromptsAccordion() { return (
-
+
- +
); diff --git a/client/src/components/Prompts/PromptsView.tsx b/client/src/components/Prompts/PromptsView.tsx index fcc23bfb7c..9a5b3f4eaa 100644 --- a/client/src/components/Prompts/PromptsView.tsx +++ b/client/src/components/Prompts/PromptsView.tsx @@ -1,7 +1,6 @@ import { useMemo, useEffect } from 'react'; import { Outlet, useParams, useNavigate } from 'react-router-dom'; import { PermissionTypes, Permissions } from 'librechat-data-provider'; -import AutoSendPrompt from '~/components/Prompts/Groups/AutoSendPrompt'; import FilterPrompts from '~/components/Prompts/Groups/FilterPrompts'; import DashBreadcrumb from '~/routes/Layouts/DashBreadcrumb'; import { usePromptGroupsNav, useHasAccess } from '~/hooks'; @@ -35,13 +34,12 @@ export default function PromptsView() { } return ( -
+
-
!!(group?.projectIds ?? []).includes(instanceProjectId), + () => ((group?.projectIds ?? []) as string[]).includes(instanceProjectId as string), [group, instanceProjectId], ); const { control, setValue, - getValues, handleSubmit, formState: { isSubmitting }, } = useForm({ @@ -51,19 +51,26 @@ const SharePrompt = ({ group, disabled }: { group?: TPromptGroup; disabled: bool setValue(Permissions.SHARED_GLOBAL, groupIsGlobal); }, [groupIsGlobal, setValue]); - if (!group || !instanceProjectId) { + if (group == null || !instanceProjectId) { return null; } const onSubmit = (data: FormValues) => { const groupId = group._id ?? ''; - if (!groupId || !instanceProjectId) { + if (groupId === '' || !instanceProjectId) { + return; + } + + if (data[Permissions.SHARED_GLOBAL] === true && groupIsGlobal) { + showToast({ + message: localize('com_ui_prompt_already_shared_to_all'), + status: 'info', + }); return; } const payload = {} as TUpdatePromptGroupPayload; - - if (data[Permissions.SHARED_GLOBAL]) { + if (data[Permissions.SHARED_GLOBAL] === true) { payload.projectIds = [startupConfig.instanceProjectId]; } else { payload.removeProjectIds = [startupConfig.instanceProjectId]; @@ -81,81 +88,50 @@ const SharePrompt = ({ group, disabled }: { group?: TPromptGroup; disabled: bool - - + + {localize('com_ui_share_var', `"${group.name}"`)} -
+ +
+ {localize('com_ui_share_form_description')} +
-
- - +
+ {localize('com_ui_share_to_all_users')}
{ - const isValid = !(value && groupIsGlobal); - if (!isValid) { - showToast({ - message: localize('com_ui_prompt_already_shared_to_all'), - status: 'warning', - }); - } - return isValid; - }, - }} + disabled={isFetching === true || updateGroup.isLoading || !instanceProjectId} render={({ field }) => ( )} />
- +
diff --git a/client/src/components/ui/AnimatedSearchInput.tsx b/client/src/components/ui/AnimatedSearchInput.tsx index 422d7f943b..dddfef4231 100644 --- a/client/src/components/ui/AnimatedSearchInput.tsx +++ b/client/src/components/ui/AnimatedSearchInput.tsx @@ -1,59 +1,66 @@ -import React, { useState } from 'react'; +import React from 'react'; import { Search } from 'lucide-react'; +import { cn } from '~/utils'; -const AnimatedSearchInput = ({ value, onChange, isSearching: searching, placeholder }) => { - const [isFocused, setIsFocused] = useState(false); +const AnimatedSearchInput = ({ + value, + onChange, + isSearching: searching, + placeholder, +}: { + value?: string; + onChange: (e: React.ChangeEvent) => void; + isSearching?: boolean; + placeholder: string; +}) => { const isSearching = searching === true; + const hasValue = value != null && value.length > 0; return (
- {/* Background gradient effect */} -
-
-
+ {/* Icon on the left */} +
- {/* Input field with background transitions */} + {/* Input field */} setIsFocused(true)} - onBlur={() => setIsFocused(false)} placeholder={placeholder} className={` - w-full rounded-lg px-10 py-2 - transition-all duration-500 ease-in-out - placeholder:text-gray-400 + peer relative z-20 w-full rounded-lg bg-surface-secondary px-10 + py-2 outline-none ring-0 backdrop-blur-sm transition-all + duration-500 ease-in-out placeholder:text-gray-400 focus:outline-none focus:ring-0 - ${isFocused ? 'bg-white/10' : 'bg-white/5'} - ${isSearching ? 'bg-white/15' : ''} - backdrop-blur-sm + `} + /> + + {/* Gradient overlay */} +
{/* Animated loading indicator */}
@@ -69,7 +76,7 @@ const AnimatedSearchInput = ({ value, onChange, isSearching: searching, placehol className={` absolute -inset-8 -z-10 transition-all duration-700 ease-in-out - ${isSearching ? 'scale-105 opacity-100' : 'scale-100 opacity-0'} + ${isSearching && hasValue ? 'scale-105 opacity-100' : 'scale-100 opacity-0'} `} >
@@ -77,26 +84,24 @@ const AnimatedSearchInput = ({ value, onChange, isSearching: searching, placehol className={` bg-gradient-radial absolute inset-0 from-blue-500/10 to-transparent transition-opacity duration-700 ease-in-out - ${isSearching ? 'animate-pulse-slow opacity-100' : 'opacity-0'} + ${isSearching && hasValue ? 'animate-pulse-slow opacity-100' : 'opacity-0'} `} />
- - {/* Focus state background glow */}
diff --git a/client/src/components/ui/Button.tsx b/client/src/components/ui/Button.tsx index 4687db0fbc..35b2d103b7 100644 --- a/client/src/components/ui/Button.tsx +++ b/client/src/components/ui/Button.tsx @@ -9,7 +9,8 @@ const buttonVariants = cva( variants: { variant: { default: 'bg-primary text-primary-foreground hover:bg-primary/90', - destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/80', + destructive: + 'bg-surface-destructive text-destructive-foreground hover:bg-surface-destructive-hover', outline: 'text-text-primary border border-border-light bg-background hover:bg-accent hover:text-accent-foreground', secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', diff --git a/client/src/components/ui/Dropdown.tsx b/client/src/components/ui/Dropdown.tsx index f89ae7dc78..1f3ab1e534 100644 --- a/client/src/components/ui/Dropdown.tsx +++ b/client/src/components/ui/Dropdown.tsx @@ -7,12 +7,22 @@ interface DropdownProps { value?: string; label?: string; onChange: (value: string) => void; - options: string[] | Option[]; + options: (string | Option | { divider: true })[]; className?: string; sizeClasses?: string; testId?: string; + icon?: React.ReactNode; + iconOnly?: boolean; + renderValue?: (option: Option) => React.ReactNode; + ariaLabel?: string; } +const isDivider = (item: string | Option | { divider: true }): item is { divider: true } => + typeof item === 'object' && 'divider' in item; + +const isOption = (item: string | Option | { divider: true }): item is Option => + typeof item === 'object' && 'value' in item && 'label' in item; + const Dropdown: React.FC = ({ value: selectedValue, label = '', @@ -21,6 +31,10 @@ const Dropdown: React.FC = ({ className = '', sizeClasses, testId = 'dropdown-menu', + icon, + iconOnly = false, + renderValue, + ariaLabel, }) => { const handleChange = (value: string) => { onChange(value); @@ -31,63 +45,101 @@ const Dropdown: React.FC = ({ setValue: handleChange, }); + const getOptionObject = (val: string | undefined): Option | undefined => { + if (val == null || val === '') { + return undefined; + } + return options + .filter((o) => !isDivider(o)) + .map((o) => (typeof o === 'string' ? { value: o, label: o } : o)) + .find((o) => isOption(o) && o.value === val) as Option | undefined; + }; + + const getOptionLabel = (currentValue: string | undefined) => { + if (currentValue == null || currentValue === '') { + return ''; + } + const option = getOptionObject(currentValue); + return option ? option.label : currentValue; + }; + return (
-
- - {label} - {options - .map((o) => (typeof o === 'string' ? { value: o, label: o } : o)) - .find((o) => o.value === selectedValue)?.label ?? selectedValue} - - +
+ {icon} + {!iconOnly && ( + + {label} + {(() => { + const matchedOption = getOptionObject(selectedValue); + if (matchedOption && renderValue) { + return renderValue(matchedOption); + } + return getOptionLabel(selectedValue); + })()} + + )}
+ {!iconOnly && } - {options.map((item, index) => ( - -
- - {typeof item === 'string' ? item : (item as Option).label} - - {selectedValue === (typeof item === 'string' ? item : item.value) && ( - - - - - - )} -
-
- ))} + {options.map((item, index) => { + if (isDivider(item)) { + return
; + } + + const option = typeof item === 'string' ? { value: item, label: item } : item; + if (!isOption(option)) { + return null; + } + + return ( + +
+ {option.icon != null && {option.icon as React.ReactNode}} + {option.label} + {selectedValue === option.value && ( + + + + + + )} +
+
+ ); + })}
); diff --git a/client/src/components/ui/Input.tsx b/client/src/components/ui/Input.tsx index adc3b422c1..855567a5b4 100644 --- a/client/src/components/ui/Input.tsx +++ b/client/src/components/ui/Input.tsx @@ -16,6 +16,7 @@ const Input = React.forwardRef(({ className, ...pr /> ); }); + Input.displayName = 'Input'; export { Input }; diff --git a/client/src/components/ui/Label.tsx b/client/src/components/ui/Label.tsx index 9c6697c2e3..2ff6e600da 100644 --- a/client/src/components/ui/Label.tsx +++ b/client/src/components/ui/Label.tsx @@ -10,7 +10,7 @@ const Label = React.forwardRef< void; selectClasses?: string; selectText?: string | ReactNode; + isLoading?: boolean; }; type DialogTemplateProps = { @@ -30,6 +33,7 @@ type DialogTemplateProps = { footerClassName?: string; showCloseButton?: boolean; showCancelButton?: boolean; + onClose?: () => void; }; const OGDialogTemplate = forwardRef((props: DialogTemplateProps, ref: Ref) => { @@ -49,7 +53,7 @@ const OGDialogTemplate = forwardRef((props: DialogTemplateProps, ref: Ref - {selectText} + {isLoading === true ? : selectText} ) : null}
diff --git a/client/src/data-provider/prompts.ts b/client/src/data-provider/prompts.ts index 1fcb8ea64b..3f6e4bb89a 100644 --- a/client/src/data-provider/prompts.ts +++ b/client/src/data-provider/prompts.ts @@ -32,29 +32,31 @@ export const useUpdatePromptGroup = ( mutationFn: (variables: t.TUpdatePromptGroupVariables) => dataService.updatePromptGroup(variables), onMutate: (variables: t.TUpdatePromptGroupVariables) => { - const group = JSON.parse( - JSON.stringify( - queryClient.getQueryData([QueryKeys.promptGroup, variables.id]), - ), - ) as t.TPromptGroup; - const groupData = queryClient.getQueryData([ + const groupData = queryClient.getQueryData([ + QueryKeys.promptGroup, + variables.id, + ]); + const group = groupData ? structuredClone(groupData) : undefined; + + const groupListData = queryClient.getQueryData([ QueryKeys.promptGroups, name, category, pageSize, ]); - const previousListData = JSON.parse(JSON.stringify(groupData)) as t.PromptGroupListData; + const previousListData = groupListData ? structuredClone(groupListData) : undefined; + let update = variables.payload; - if (update.removeProjectIds && group.projectIds) { - update = JSON.parse(JSON.stringify(update)); + if (update.removeProjectIds && group?.projectIds) { + update = structuredClone(update); update.projectIds = group.projectIds.filter((id) => !update.removeProjectIds?.includes(id)); delete update.removeProjectIds; } - if (groupData) { + if (groupListData) { const newData = updateGroupFields( /* Paginated Data */ - groupData, + groupListData, /* Update */ { _id: variables.id, ...update }, /* Callback */ @@ -74,12 +76,12 @@ export const useUpdatePromptGroup = ( }, onError: (err, variables, context) => { if (context?.group) { - queryClient.setQueryData([QueryKeys.promptGroups, variables.id], context?.group); + queryClient.setQueryData([QueryKeys.promptGroups, variables.id], context.group); } if (context?.previousListData) { queryClient.setQueryData( [QueryKeys.promptGroups, name, category, pageSize], - context?.previousListData, + context.previousListData, ); } if (onError) { @@ -110,7 +112,7 @@ export const useCreatePrompt = ( onSuccess: (response, variables, context) => { const { prompt, group } = response; queryClient.setQueryData( - [QueryKeys.prompts, variables?.prompt?.groupId], + [QueryKeys.prompts, variables.prompt.groupId], (oldData: t.TPrompt[] | undefined) => { return [prompt, ...(oldData ?? [])]; }, @@ -301,12 +303,12 @@ export const useMakePromptProduction = (options?: t.MakePromptProductionOptions) }, onError: (err, variables, context) => { if (context?.group) { - queryClient.setQueryData([QueryKeys.promptGroups, variables.groupId], context?.group); + queryClient.setQueryData([QueryKeys.promptGroups, variables.groupId], context.group); } if (context?.previousListData) { queryClient.setQueryData( [QueryKeys.promptGroups, name, category, pageSize], - context?.previousListData, + context.previousListData, ); } if (onError) { diff --git a/client/src/hooks/Prompts/useCategories.tsx b/client/src/hooks/Prompts/useCategories.tsx index 7a74862950..6bdaa5272f 100644 --- a/client/src/hooks/Prompts/useCategories.tsx +++ b/client/src/hooks/Prompts/useCategories.tsx @@ -19,9 +19,7 @@ const useCategories = (className = '') => { const { data: categories = loadingCategories } = useGetCategories({ select: (data) => data.map((category) => ({ - label: category.label - ? localize(`com_ui_${category.label}`) || category.label - : localize('com_ui_select_a_category'), + label: localize(`com_ui_${category.label}`) || category.label, value: category.value, icon: category.value ? ( diff --git a/client/src/localization/languages/Eng.ts b/client/src/localization/languages/Eng.ts index 5da1f46d6c..338609101a 100644 --- a/client/src/localization/languages/Eng.ts +++ b/client/src/localization/languages/Eng.ts @@ -214,6 +214,7 @@ export default { com_ui_prompt: 'Prompt', com_ui_prompts: 'Prompts', com_ui_prompt_name: 'Prompt Name', + com_ui_rename_prompt: 'Rename Prompt', com_ui_delete_prompt: 'Delete Prompt?', com_ui_admin: 'Admin', com_ui_simple: 'Simple', @@ -229,9 +230,12 @@ export default { com_ui_prompt_name_required: 'Prompt Name is required', com_ui_prompt_text_required: 'Text is required', com_ui_prompt_text: 'Text', + com_ui_currently_production: 'Currently in production', + com_ui_latest_version: 'Latest version', com_ui_back_to_chat: 'Back to Chat', com_ui_back_to_prompts: 'Back to Prompts', com_ui_categories: 'Categories', + com_ui_filter_prompts: 'Filter Prompts', com_ui_filter_prompts_name: 'Filter prompts by name', com_ui_search_categories: 'Search Categories', com_ui_manage: 'Manage', @@ -373,9 +377,13 @@ export default { com_ui_agent_duplicate_error: 'There was an error duplicating the agent', com_ui_prompt_already_shared_to_all: 'This prompt is already shared to all users', com_ui_description_placeholder: 'Optional: Enter a description to display for the prompt', - com_ui_command_placeholder: 'Optional: Enter a command for the prompt or name will be used.', + com_ui_command_placeholder: 'Optional: Enter a command for the prompt or name will be used', com_ui_command_usage_placeholder: 'Select a Prompt by command or name', com_ui_no_prompt_description: 'No description found.', + com_ui_latest_production_version: 'Latest production version', + com_ui_confirm_change: 'Confirm Change', + com_ui_confirm_admin_use_change: + 'Changing this setting will block access for admins, including yourself. Are you sure you want to proceed?', com_ui_share_link_to_chat: 'Share link to chat', com_ui_share_error: 'There was an error sharing the chat link', com_ui_share_retrieve_error: 'There was an error retrieving the shared links', diff --git a/client/src/routes/Layouts/DashBreadcrumb.tsx b/client/src/routes/Layouts/DashBreadcrumb.tsx index f031689e94..60a6eb9a82 100644 --- a/client/src/routes/Layouts/DashBreadcrumb.tsx +++ b/client/src/routes/Layouts/DashBreadcrumb.tsx @@ -1,7 +1,7 @@ -import { useSetRecoilState } from 'recoil'; import { useMemo, useCallback } from 'react'; import { useLocation } from 'react-router-dom'; import { SystemRoles } from 'librechat-data-provider'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; import { ArrowLeft, MessageSquareQuote } from 'lucide-react'; import { Breadcrumb, @@ -17,8 +17,10 @@ import { } from '~/components/ui'; import { useLocalize, useCustomLink, useAuthContext } from '~/hooks'; import AdvancedSwitch from '~/components/Prompts/AdvancedSwitch'; +import { RightPanel } from '../../components/Prompts/RightPanel'; import AdminSettings from '~/components/Prompts/AdminSettings'; import { useDashboardContext } from '~/Providers'; +import { PromptsEditorMode } from '~/common'; import store from '~/store'; const promptsPathPattern = /prompts\/(?!new(?:\/|$)).*$/; @@ -40,6 +42,7 @@ export default function DashBreadcrumb() { const setPromptsName = useSetRecoilState(store.promptsName); const setPromptsCategory = useSetRecoilState(store.promptsCategory); + const editorMode = useRecoilValue(store.promptsEditorMode); const clickCallback = useCallback(() => { setPromptsName(''); @@ -55,7 +58,7 @@ export default function DashBreadcrumb() { ); return ( -
+
diff --git a/client/src/style.css b/client/src/style.css index f632f8eb03..d1b7790030 100644 --- a/client/src/style.css +++ b/client/src/style.css @@ -29,6 +29,17 @@ --green-800: #065f46; --green-900: #064e3b; --green-950: #022c22; + --red-50: #fef2f2; + --red-100: #fee2e2; + --red-200: #fecaca; + --red-300: #fca5a5; + --red-400: #f87171; + --red-500: #ef4444; + --red-600: #dc2626; + --red-700: #b91c1c; + --red-800: #991b1b; + --red-900: #7f1d1d; + --red-950: #450a0a; --gizmo-gray-500: #999; --gizmo-gray-600: #666; --gizmo-gray-950: #0f0f0f; @@ -62,6 +73,8 @@ html { --surface-dialog: var(--white); --surface-submit: var(--green-700); --surface-submit-hover: var(--green-800); + --surface-destructive: var(--red-700); + --surface-destructive-hover: var(--red-800); --border-light: var(--gray-200); --border-medium-alt: var(--gray-300); --border-medium: var(--gray-300); @@ -117,6 +130,8 @@ html { --surface-dialog: var(--gray-850); --surface-submit: var(--green-700); --surface-submit-hover: var(--green-800); + --surface-destructive: var(--red-800); + --surface-destructive-hover: var(--red-900); --border-light: var(--gray-700); --border-medium-alt: var(--gray-600); --border-medium: var(--gray-600); @@ -2347,7 +2362,7 @@ button.scroll-convo { .popover-ui { z-index: 1000; display: flex; - max-height: min(var(--popover-available-height, 300px), 300px); + max-height: min(var(--popover-available-height, 1700px), 1700px); flex-direction: column; overflow: auto; overscroll-behavior: contain; @@ -2418,9 +2433,15 @@ button.scroll-convo { /** AnimatedSearchInput style */ @keyframes gradient-x { - 0% { background-position: 0% 50%; } - 50% { background-position: 100% 50%; } - 100% { background-position: 0% 50%; } + 0% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } + 100% { + background-position: 0% 50%; + } } .animate-gradient-x { @@ -2452,4 +2473,4 @@ button.scroll-convo { .scale-98 { transform: scale(0.98); -} \ No newline at end of file +} diff --git a/client/tailwind.config.cjs b/client/tailwind.config.cjs index ac867e8568..45f6533fb5 100644 --- a/client/tailwind.config.cjs +++ b/client/tailwind.config.cjs @@ -85,6 +85,8 @@ module.exports = { 'surface-dialog': 'var(--surface-dialog)', 'surface-submit': 'var(--surface-submit)', 'surface-submit-hover': 'var(--surface-submit-hover)', + 'surface-destructive': 'var(--surface-destructive)', + 'surface-destructive-hover': 'var(--surface-destructive-hover)', 'border-light': 'var(--border-light)', 'border-medium': 'var(--border-medium)', 'border-medium-alt': 'var(--border-medium-alt)',