diff --git a/client/src/components/Chat/Input/HeaderOptions.tsx b/client/src/components/Chat/Input/HeaderOptions.tsx deleted file mode 100644 index 7b0e01a17e..0000000000 --- a/client/src/components/Chat/Input/HeaderOptions.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { useState } from 'react'; -import { Settings2 } from 'lucide-react'; -import { TooltipAnchor } from '@librechat/client'; -import { Root, Anchor } from '@radix-ui/react-popover'; -import { isParamEndpoint, getEndpointField, tConvoUpdateSchema } from 'librechat-data-provider'; -import type { TPreset, TInterfaceConfig } from 'librechat-data-provider'; -import { EndpointSettings, SaveAsPresetDialog, AlternativeSettings } from '~/components/Endpoints'; -import { useSetIndexOptions, useLocalize } from '~/hooks'; -import { useGetEndpointsQuery } from '~/data-provider'; -import OptionsPopover from './OptionsPopover'; -import PopoverButtons from './PopoverButtons'; -import { useChatContext } from '~/Providers'; - -export default function HeaderOptions({ - interfaceConfig, -}: { - interfaceConfig?: Partial; -}) { - const { data: endpointsConfig } = useGetEndpointsQuery(); - - const [saveAsDialogShow, setSaveAsDialogShow] = useState(false); - const localize = useLocalize(); - - const { showPopover, conversation, setShowPopover } = useChatContext(); - const { setOption } = useSetIndexOptions(); - const { endpoint } = conversation ?? {}; - - const saveAsPreset = () => { - setSaveAsDialogShow(true); - }; - - if (!endpoint) { - return null; - } - - const triggerAdvancedMode = () => setShowPopover((prev) => !prev); - - const endpointType = getEndpointField(endpointsConfig, endpoint, 'type'); - const paramEndpoint = isParamEndpoint(endpoint, endpointType); - - return ( - - -
- -
- {interfaceConfig?.parameters === true && paramEndpoint === false && ( - - - )} -
- {interfaceConfig?.parameters === true && paramEndpoint === false && ( - } - closePopover={() => setShowPopover(false)} - > -
- - -
-
- )} - {interfaceConfig?.presets === true && ( - - )} -
-
-
-
- ); -} diff --git a/client/src/components/Chat/Input/OptionsPopover.tsx b/client/src/components/Chat/Input/OptionsPopover.tsx deleted file mode 100644 index f3102bd27c..0000000000 --- a/client/src/components/Chat/Input/OptionsPopover.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { useRef } from 'react'; -import { Save } from 'lucide-react'; -import { Portal, Content } from '@radix-ui/react-popover'; -import { Button, CrossIcon, useOnClickOutside } from '@librechat/client'; -import type { ReactNode } from 'react'; -import { cn, removeFocusOutlines } from '~/utils'; -import { useLocalize } from '~/hooks'; - -type TOptionsPopoverProps = { - children: ReactNode; - visible: boolean; - saveAsPreset: () => void; - closePopover: () => void; - PopoverButtons: ReactNode; - presetsDisabled: boolean; -}; - -export default function OptionsPopover({ - children, - // endpoint, - visible, - saveAsPreset, - closePopover, - PopoverButtons, - presetsDisabled, -}: TOptionsPopoverProps) { - const popoverRef = useRef(null); - useOnClickOutside( - popoverRef, - () => closePopover(), - ['dialog-template-content', 'shadcn-button', 'advanced-settings'], - (_target) => { - const target = _target as Element; - if ( - target.id === 'presets-button' || - (target.parentNode instanceof Element && target.parentNode.id === 'presets-button') - ) { - return false; - } - const tagName = target.tagName; - return tagName === 'path' || tagName === 'svg' || tagName === 'circle'; - }, - ); - - const localize = useLocalize(); - const cardStyle = - 'shadow-xl rounded-md min-w-[75px] font-normal bg-white border-black/10 border dark:bg-gray-700 text-black dark:text-white'; - - if (!visible) { - return null; - } - - return ( - - -
-
-
- {presetsDisabled ? null : ( - - )} - {PopoverButtons} - -
-
{children}
-
-
-
-
- ); -} diff --git a/client/src/components/Endpoints/AlternativeSettings.tsx b/client/src/components/Endpoints/AlternativeSettings.tsx deleted file mode 100644 index 2ee51da492..0000000000 --- a/client/src/components/Endpoints/AlternativeSettings.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { useRecoilValue } from 'recoil'; -import { SettingsViews } from 'librechat-data-provider'; -import type { TSettingsProps } from '~/common'; -import { Advanced } from './Settings'; -import { cn } from '~/utils'; -import store from '~/store'; - -export default function AlternativeSettings({ - conversation, - setOption, - isPreset = false, - className = '', -}: TSettingsProps) { - const currentSettingsView = useRecoilValue(store.currentSettingsView); - if (!conversation?.endpoint || currentSettingsView === SettingsViews.default) { - return null; - } - - return ( -
- -
- ); -} diff --git a/client/src/components/Endpoints/index.ts b/client/src/components/Endpoints/index.ts index f171b4074a..b0d5d78dbe 100644 --- a/client/src/components/Endpoints/index.ts +++ b/client/src/components/Endpoints/index.ts @@ -1,8 +1,6 @@ export { default as Icon } from './Icon'; -export { default as MinimalIcon } from './MinimalIcon'; export { default as ConvoIcon } from './ConvoIcon'; +export { default as MinimalIcon } from './MinimalIcon'; export { default as EndpointIcon } from './EndpointIcon'; export { default as ConvoIconURL } from './ConvoIconURL'; export { default as EndpointSettings } from './EndpointSettings'; -export { default as SaveAsPresetDialog } from './SaveAsPresetDialog'; -export { default as AlternativeSettings } from './AlternativeSettings'; diff --git a/client/src/components/SidePanel/Agents/ModelPanel.tsx b/client/src/components/SidePanel/Agents/ModelPanel.tsx index bfcac5bdea..c796e5efa3 100644 --- a/client/src/components/SidePanel/Agents/ModelPanel.tsx +++ b/client/src/components/SidePanel/Agents/ModelPanel.tsx @@ -1,9 +1,8 @@ import React, { useMemo, useEffect } from 'react'; import keyBy from 'lodash/keyBy'; -import { ControlCombobox } from '@librechat/client'; import { ChevronLeft, RotateCcw } from 'lucide-react'; +import { ControlCombobox, Button } from '@librechat/client'; import { useFormContext, useWatch, Controller } from 'react-hook-form'; -import { componentMapping } from '~/components/SidePanel/Parameters/components'; import { alternateName, getSettingsKeys, @@ -13,6 +12,7 @@ import { agentParamSettings, } from 'librechat-data-provider'; import type * as t from 'librechat-data-provider'; +import { componentMapping } from '~/components/SidePanel/Parameters/components'; import type { AgentForm, AgentModelPanelProps, StringOption } from '~/common'; import { useGetEndpointsQuery } from '~/data-provider'; import { useLocalize } from '~/hooks'; @@ -245,14 +245,10 @@ export default function ModelPanel({ )} {/* Reset Parameters Button */} - + ); } diff --git a/client/src/components/SidePanel/Parameters/Panel.tsx b/client/src/components/SidePanel/Parameters/Panel.tsx index 7541f4f0e8..be78f24137 100644 --- a/client/src/components/SidePanel/Parameters/Panel.tsx +++ b/client/src/components/SidePanel/Parameters/Panel.tsx @@ -1,6 +1,7 @@ -import React, { useMemo, useState, useEffect, useCallback } from 'react'; +import React, { useMemo, useEffect, useCallback, useState } from 'react'; import keyBy from 'lodash/keyBy'; -import { RotateCcw } from 'lucide-react'; +import { Button } from '@librechat/client'; +import { RotateCcw, BookPlus } from 'lucide-react'; import { excludedKeys, paramSettings, @@ -10,9 +11,9 @@ import { tConvoUpdateSchema, } from 'librechat-data-provider'; import type { TPreset } from 'librechat-data-provider'; -import { SaveAsPresetDialog } from '~/components/Endpoints'; import { useSetIndexOptions, useLocalize } from '~/hooks'; import { useGetEndpointsQuery } from '~/data-provider'; +import SaveAsPresetDialog from './SaveAsPresetDialog'; import { componentMapping } from './components'; import { useChatContext } from '~/Providers'; import { logger } from '~/utils'; @@ -22,13 +23,14 @@ export default function Parameters() { const { conversation, setConversation } = useChatContext(); const { setOption } = useSetIndexOptions(); - const [isDialogOpen, setIsDialogOpen] = useState(false); - const [preset, setPreset] = useState(null); - const { data: endpointsConfig = {} } = useGetEndpointsQuery(); const provider = conversation?.endpoint ?? ''; const model = conversation?.model ?? ''; + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [preset, setPreset] = useState(null); + const [isResetting, setIsResetting] = useState(false); + const bedrockRegions = useMemo(() => { return endpointsConfig?.[conversation?.endpoint ?? '']?.availableRegions ?? []; }, [endpointsConfig, conversation?.endpoint]); @@ -105,6 +107,13 @@ export default function Parameters() { }, [parameters, setConversation]); const resetParameters = useCallback(() => { + if (isResetting) { + return; + } + + setIsResetting(true); + setTimeout(() => setIsResetting(false), 500); + setConversation((prev) => { if (!prev) { return prev; @@ -127,9 +136,9 @@ export default function Parameters() { logger.log('parameters', 'parameters reset, affected keys:', resetKeys); return updatedConversation; }); - }, [setConversation]); + }, [isResetting, setConversation]); - const openDialog = useCallback(() => { + const saveAsPreset = useCallback(() => { const newPreset = tConvoUpdateSchema.parse({ ...conversation, }) as TPreset; @@ -171,23 +180,24 @@ export default function Parameters() { })}
- +
- +
{preset && ( diff --git a/client/src/components/Endpoints/SaveAsPresetDialog.tsx b/client/src/components/SidePanel/Parameters/SaveAsPresetDialog.tsx similarity index 58% rename from client/src/components/Endpoints/SaveAsPresetDialog.tsx rename to client/src/components/SidePanel/Parameters/SaveAsPresetDialog.tsx index 6467a4a408..ad6b0f608a 100644 --- a/client/src/components/Endpoints/SaveAsPresetDialog.tsx +++ b/client/src/components/SidePanel/Parameters/SaveAsPresetDialog.tsx @@ -1,16 +1,26 @@ import React, { useEffect, useState } from 'react'; import { useCreatePresetMutation } from 'librechat-data-provider/react-query'; -import { OGDialogTemplate, OGDialog, Input, Label, useToastContext } from '@librechat/client'; +import { + Input, + Label, + Button, + Spinner, + useToastContext, + OGDialog, + OGDialogTemplate, +} from '@librechat/client'; import type { TEditPresetProps } from '~/common'; -import { cn, removeFocusOutlines, cleanupPreset, defaultTextProps } from '~/utils'; import { NotificationSeverity } from '~/common'; +import { cleanupPreset, logger } from '~/utils'; import { useLocalize } from '~/hooks'; const SaveAsPresetDialog = ({ open, onOpenChange, preset }: TEditPresetProps) => { - const [title, setTitle] = useState(preset.title ?? 'My Preset'); - const createPresetMutation = useCreatePresetMutation(); - const { showToast } = useToastContext(); const localize = useLocalize(); + const { showToast } = useToastContext(); + const createPresetMutation = useCreatePresetMutation(); + const isLoading = createPresetMutation.isLoading; + + const [title, setTitle] = useState(preset.title ?? 'My Preset'); const submitPreset = () => { const _preset = cleanupPreset({ @@ -30,7 +40,8 @@ const SaveAsPresetDialog = ({ open, onOpenChange, preset }: TEditPresetProps) => }); onOpenChange(false); // Close the dialog on success }, - onError: () => { + onError: (error) => { + logger.error('Error saving preset:', error); showToast({ message: localize('com_endpoint_preset_save_error'), severity: NotificationSeverity.ERROR, @@ -44,7 +55,6 @@ const SaveAsPresetDialog = ({ open, onOpenChange, preset }: TEditPresetProps) => // eslint-disable-next-line react-hooks/exhaustive-deps }, [open]); - // Handle Enter key press const handleKeyDown = (event: React.KeyboardEvent) => { if (event.key === 'Enter') { event.preventDefault(); @@ -56,36 +66,28 @@ const SaveAsPresetDialog = ({ open, onOpenChange, preset }: TEditPresetProps) => -
- - setTitle(e.target.value || '')} - onKeyDown={handleKeyDown} - placeholder={localize('com_endpoint_preset_custom_name_placeholder')} - aria-label={localize('com_endpoint_preset_name')} - className={cn( - defaultTextProps, - 'flex h-10 max-h-10 w-full resize-none border-border-medium px-3 py-2', - removeFocusOutlines, - )} - /> -
+ + setTitle(e.target.value || '')} + onKeyDown={handleKeyDown} + placeholder={localize('com_endpoint_enter_name_placeholder')} + aria-label={localize('com_endpoint_preset_name')} + /> } - selection={{ - selectHandler: submitPreset, - selectClasses: 'bg-green-500 hover:bg-green-600 dark:hover:bg-green-600 text-white', - selectText: localize('com_ui_save'), - }} + selection={ + + } />
); diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 7651b5a51d..0c05ee4e86 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -308,7 +308,7 @@ "com_endpoint_plug_resend_files": "Resend Files", "com_endpoint_presence_penalty": "Presence Penalty", "com_endpoint_preset": "preset", - "com_endpoint_preset_custom_name_placeholder": "something needs to go here. was empty", + "com_endpoint_enter_name_placeholder": "Enter a name", "com_endpoint_preset_default": "is now the default preset.", "com_endpoint_preset_default_item": "Default:", "com_endpoint_preset_default_none": "No default preset active.", @@ -884,7 +884,7 @@ "com_ui_delete_confirm_strong": "This will delete {{title}}", "com_ui_delete_conversation": "Delete chat?", "com_ui_delete_conversation_tooltip": "Delete conversation", - "com_ui_delete_memory": "Delete Memory", + "com_ui_delete_memory": "Delete Memory?", "com_ui_delete_not_allowed": "Delete operation is not allowed", "com_ui_delete_preset": "Delete Preset?", "com_ui_delete_prompt": "Delete Prompt?", diff --git a/client/tailwind.config.cjs b/client/tailwind.config.cjs index c30d2ca703..45763cffde 100644 --- a/client/tailwind.config.cjs +++ b/client/tailwind.config.cjs @@ -47,6 +47,10 @@ module.exports = { '0%': { transform: 'translateX(0)' }, '100%': { transform: 'translateX(100%)' }, }, + 'spin-reset': { + '0%': { transform: 'rotate(0deg)' }, + '100%': { transform: 'rotate(-360deg)' }, + }, }, animation: { 'fade-in': 'fadeIn 0.5s ease-out forwards', @@ -56,6 +60,7 @@ module.exports = { 'slide-in-left': 'slide-in-left 300ms cubic-bezier(0.25, 0.1, 0.25, 1)', 'slide-out-left': 'slide-out-left 300ms cubic-bezier(0.25, 0.1, 0.25, 1)', 'slide-out-right': 'slide-out-right 300ms cubic-bezier(0.25, 0.1, 0.25, 1)', + 'spin-reset': 'spin-reset 500ms ease-in-out', }, colors: { gray: {