From ca64efec1b05da06c545bf69844dfefc2afff5af Mon Sep 17 00:00:00 2001 From: Danny Avila <110412045+danny-avila@users.noreply.github.com> Date: Wed, 6 Dec 2023 14:00:15 -0500 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20Implement=20Default=20Preset=20Sele?= =?UTF-8?q?ction=20for=20Conversations=20=F0=9F=93=8C=20(#1275)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: type issues with icons * refactor: use react query for presets, show toasts on preset crud, refactor mutations, remove presetsQuery from Root (breaking change) * refactor: change preset titling * refactor: update preset schemas and methods for necessary new properties `order` and `defaultPreset` * feat: add `defaultPreset` Recoil value * refactor(getPresetTitle): make logic cleaner and more concise * feat: complete UI portion of defaultPreset feature, with animations added to preset items * chore: remove console.logs() * feat: complete default preset handling * refactor: remove user sensitive values on logout * fix: allow endpoint selection without default preset overwriting --- api/models/Preset.js | 43 +++- api/models/schema/presetSchema.js | 6 + api/server/routes/presets.js | 19 +- client/package.json | 1 + .../Chat/Menus/Presets/EditPresetDialog.tsx | 53 ++--- .../Chat/Menus/Presets/PresetItems.tsx | 130 ++++++---- .../src/components/Chat/Menus/PresetsMenu.tsx | 102 ++------ .../src/components/Chat/Menus/UI/MenuItem.tsx | 4 +- .../Endpoints/SaveAsPresetDialog.tsx | 26 +- client/src/components/Nav/Logout.tsx | 4 + client/src/components/svg/AnthropicIcon.tsx | 8 +- .../src/components/svg/AzureMinimalIcon.tsx | 8 +- client/src/components/svg/GPTIcon.tsx | 8 +- client/src/components/svg/MinimalPlugin.tsx | 8 +- client/src/components/svg/PinIcon.tsx | 53 +++++ client/src/components/svg/index.ts | 1 + client/src/data-provider/mutations.ts | 32 +++ client/src/hooks/Conversations/index.ts | 1 + client/src/hooks/Conversations/usePresets.ts | 223 ++++++++++++++++++ client/src/hooks/index.ts | 1 + client/src/hooks/useNewConvo.ts | 29 ++- client/src/localization/languages/Eng.tsx | 13 + client/src/routes/Root.tsx | 18 +- client/src/store/preset.ts | 12 + client/src/utils/presets.ts | 65 +++-- package-lock.json | 35 +++ packages/data-provider/src/data-service.ts | 7 +- packages/data-provider/src/index.ts | 1 + packages/data-provider/src/keys.ts | 2 + .../data-provider/src/react-query-service.ts | 14 +- packages/data-provider/src/schemas.ts | 2 + packages/data-provider/src/types/presets.ts | 22 ++ 32 files changed, 681 insertions(+), 270 deletions(-) create mode 100644 client/src/components/svg/PinIcon.tsx create mode 100644 client/src/hooks/Conversations/index.ts create mode 100644 client/src/hooks/Conversations/usePresets.ts create mode 100644 packages/data-provider/src/types/presets.ts diff --git a/api/models/Preset.js b/api/models/Preset.js index 68cfaa7a334..553e2e7fcec 100644 --- a/api/models/Preset.js +++ b/api/models/Preset.js @@ -14,24 +14,53 @@ module.exports = { getPreset, getPresets: async (user, filter) => { try { - return await Preset.find({ ...filter, user }).lean(); + const presets = await Preset.find({ ...filter, user }).lean(); + const defaultValue = 10000; + + presets.sort((a, b) => { + let orderA = a.order !== undefined ? a.order : defaultValue; + let orderB = b.order !== undefined ? b.order : defaultValue; + + if (orderA !== orderB) { + return orderA - orderB; + } + + return b.updatedAt - a.updatedAt; + }); + + return presets; } catch (error) { console.log(error); return { message: 'Error retrieving presets' }; } }, - savePreset: async (user, { presetId, newPresetId, ...preset }) => { + savePreset: async (user, { presetId, newPresetId, defaultPreset, ...preset }) => { try { + const setter = { $set: {} }; const update = { presetId, ...preset }; if (newPresetId) { update.presetId = newPresetId; } - return await Preset.findOneAndUpdate( - { presetId, user }, - { $set: update }, - { new: true, upsert: true }, - ); + if (defaultPreset) { + update.defaultPreset = defaultPreset; + update.order = 0; + + const currentDefault = await Preset.findOne({ defaultPreset: true, user }); + + if (currentDefault && currentDefault.presetId !== presetId) { + await Preset.findByIdAndUpdate(currentDefault._id, { + $unset: { defaultPreset: '', order: '' }, + }); + } + } else if (defaultPreset === false) { + update.defaultPreset = undefined; + update.order = undefined; + setter['$unset'] = { defaultPreset: '', order: '' }; + } + + setter.$set = update; + return await Preset.findOneAndUpdate({ presetId, user }, setter, { new: true, upsert: true }); } catch (error) { console.log(error); return { message: 'Error saving preset' }; diff --git a/api/models/schema/presetSchema.js b/api/models/schema/presetSchema.js index 908811a0e7a..e1c92ab9c07 100644 --- a/api/models/schema/presetSchema.js +++ b/api/models/schema/presetSchema.js @@ -17,6 +17,12 @@ const presetSchema = mongoose.Schema( type: String, default: null, }, + defaultPreset: { + type: Boolean, + }, + order: { + type: Number, + }, // google only examples: [{ type: mongoose.Schema.Types.Mixed }], ...conversationPreset, diff --git a/api/server/routes/presets.js b/api/server/routes/presets.js index 127a8e5b6be..e21d2df9d30 100644 --- a/api/server/routes/presets.js +++ b/api/server/routes/presets.js @@ -5,9 +5,7 @@ const crypto = require('crypto'); const requireJwtAuth = require('../middleware/requireJwtAuth'); router.get('/', requireJwtAuth, async (req, res) => { - const presets = (await getPresets(req.user.id)).map((preset) => { - return preset; - }); + const presets = (await getPresets(req.user.id)).map((preset) => preset); res.status(200).send(presets); }); @@ -17,12 +15,8 @@ router.post('/', requireJwtAuth, async (req, res) => { update.presetId = update?.presetId || crypto.randomUUID(); try { - await savePreset(req.user.id, update); - - const presets = (await getPresets(req.user.id)).map((preset) => { - return preset; - }); - res.status(201).send(presets); + const preset = await savePreset(req.user.id, update); + res.status(201).send(preset); } catch (error) { console.error(error); res.status(500).send(error); @@ -31,7 +25,7 @@ router.post('/', requireJwtAuth, async (req, res) => { router.post('/delete', requireJwtAuth, async (req, res) => { let filter = {}; - const { presetId } = req.body.arg || {}; + const { presetId } = req.body || {}; if (presetId) { filter = { presetId }; @@ -40,9 +34,8 @@ router.post('/delete', requireJwtAuth, async (req, res) => { console.log('delete preset filter', filter); try { - await deletePresets(req.user.id, filter); - const presets = await getPresets(req.user.id); - res.status(201).send(presets); + const deleteCount = await deletePresets(req.user.id, filter); + res.status(201).send(deleteCount); } catch (error) { console.error(error); res.status(500).send(error); diff --git a/client/package.json b/client/package.json index 58ea265ba4b..ff8c73b40a6 100644 --- a/client/package.json +++ b/client/package.json @@ -61,6 +61,7 @@ "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.2.0", + "react-flip-toolkit": "^7.1.0", "react-hook-form": "^7.43.9", "react-lazy-load-image-component": "^1.6.0", "react-markdown": "^8.0.6", diff --git a/client/src/components/Chat/Menus/Presets/EditPresetDialog.tsx b/client/src/components/Chat/Menus/Presets/EditPresetDialog.tsx index 0ff550f5392..9b07416f4f4 100644 --- a/client/src/components/Chat/Menus/Presets/EditPresetDialog.tsx +++ b/client/src/components/Chat/Menus/Presets/EditPresetDialog.tsx @@ -1,10 +1,6 @@ -import axios from 'axios'; -import filenamify from 'filenamify'; -import { useSetRecoilState } from 'recoil'; -import exportFromJSON from 'export-from-json'; +import { useRecoilState } from 'recoil'; import { useGetEndpointsQuery } from 'librechat-data-provider'; -import type { TEditPresetProps } from '~/common'; -import { cn, defaultTextProps, removeFocusOutlines, cleanupPreset, mapEndpoints } from '~/utils'; +import { cn, defaultTextProps, removeFocusOutlines, mapEndpoints } from '~/utils'; import { Input, Label, Dropdown, Dialog, DialogClose, DialogButton } from '~/components/'; import PopoverButtons from '~/components/Endpoints/PopoverButtons'; import DialogTemplate from '~/components/ui/DialogTemplate'; @@ -13,42 +9,21 @@ import { EndpointSettings } from '~/components/Endpoints'; import { useChatContext } from '~/Providers'; import store from '~/store'; -const EditPresetDialog = ({ open, onOpenChange, title }: Omit) => { +const EditPresetDialog = ({ + exportPreset, + submitPreset, +}: { + exportPreset: () => void; + submitPreset: () => void; +}) => { + const localize = useLocalize(); const { preset } = useChatContext(); + const { setOption } = useSetIndexOptions(preset); + const [presetModalVisible, setPresetModalVisible] = useRecoilState(store.presetModalVisible); - // TODO: use React Query for presets data - const setPresets = useSetRecoilState(store.presets); const { data: availableEndpoints = [] } = useGetEndpointsQuery({ select: mapEndpoints, }); - const { setOption } = useSetIndexOptions(preset); - const localize = useLocalize(); - - const submitPreset = () => { - if (!preset) { - return; - } - axios({ - method: 'post', - url: '/api/presets', - data: cleanupPreset({ preset }), - withCredentials: true, - }).then((res) => { - setPresets(res?.data); - }); - }; - - const exportPreset = () => { - if (!preset) { - return; - } - const fileName = filenamify(preset?.title || 'preset'); - exportFromJSON({ - data: cleanupPreset({ preset }), - fileName, - exportType: exportFromJSON.types.json, - }); - }; const { endpoint } = preset || {}; if (!endpoint) { @@ -56,9 +31,9 @@ const EditPresetDialog = ({ open, onOpenChange, title }: Omit + void; onSelectPreset: (preset: TPreset) => void; onChangePreset: (preset: TPreset) => void; onDeletePreset: (preset: TPreset) => void; @@ -20,12 +24,14 @@ const PresetItems: FC<{ onFileSelected: (jsonData: Record) => void; }> = ({ presets, + onSetDefaultPreset, onSelectPreset, onChangePreset, onDeletePreset, clearAllPresets, onFileSelected, }) => { + const defaultPreset = useRecoilValue(store.defaultPreset); const localize = useLocalize(); return ( <> @@ -35,11 +41,19 @@ const PresetItems: FC<{ tabIndex={-1} >
+
)} - {presets && - presets.length > 0 && - presets.map((preset, i) => { - if (!preset) { - return null; - } + presetId).join('.')}> + {presets && + presets.length > 0 && + presets.map((preset, i) => { + if (!preset || !preset.presetId) { + return null; + } - return ( - -
- onSelectPreset(preset)} - icon={icons[preset.endpoint ?? 'unknown']({ className: 'icon-md mr-1 ' })} - // value={preset.presetId} - selected={false} - data-testid={`preset-item-${preset}`} - // description="With DALL·E, browsing and analysis" - > -
- - -
-
- {i !== presets.length - 1 && } -
-
- ); - })} +
+ + + +
+ + + {i !== presets.length - 1 && } + + + ); + })} +
); }; diff --git a/client/src/components/Chat/Menus/PresetsMenu.tsx b/client/src/components/Chat/Menus/PresetsMenu.tsx index 183b9560c98..bdd47895e92 100644 --- a/client/src/components/Chat/Menus/PresetsMenu.tsx +++ b/client/src/components/Chat/Menus/PresetsMenu.tsx @@ -1,96 +1,25 @@ import type { FC } from 'react'; -import { useState } from 'react'; -import { useRecoilState } from 'recoil'; import { BookCopy } from 'lucide-react'; -import { - modularEndpoints, - useDeletePresetMutation, - useCreatePresetMutation, -} from 'librechat-data-provider'; -import type { TPreset } from 'librechat-data-provider'; import { Content, Portal, Root, Trigger } from '@radix-ui/react-popover'; -import { useLocalize, useDefaultConvo, useNavigateToConvo } from '~/hooks'; -import { useChatContext, useToastContext } from '~/Providers'; import { EditPresetDialog, PresetItems } from './Presets'; -import { cleanupPreset, cn } from '~/utils'; -import store from '~/store'; +import { useLocalize, usePresets } from '~/hooks'; +import { cn } from '~/utils'; const PresetsMenu: FC = () => { const localize = useLocalize(); - const { showToast } = useToastContext(); - const { conversation, newConversation, setPreset } = useChatContext(); - const { navigateToConvo } = useNavigateToConvo(); - const getDefaultConversation = useDefaultConvo(); - - const [presetModalVisible, setPresetModalVisible] = useState(false); - // TODO: rely on react query for presets data - const [presets, setPresets] = useRecoilState(store.presets); - - const deletePresetsMutation = useDeletePresetMutation(); - const createPresetMutation = useCreatePresetMutation(); - - const { endpoint } = conversation ?? {}; - - const importPreset = (jsonPreset: TPreset) => { - createPresetMutation.mutate( - { ...jsonPreset }, - { - onSuccess: (data) => { - setPresets(data); - }, - onError: (error) => { - console.error('Error uploading the preset:', error); - }, - }, - ); - }; - const onFileSelected = (jsonData: Record) => { - const jsonPreset = { ...cleanupPreset({ preset: jsonData }), presetId: null }; - importPreset(jsonPreset); - }; - const onSelectPreset = (newPreset: TPreset) => { - if (!newPreset) { - return; - } - - showToast({ - message: localize('com_endpoint_preset_selected'), - showIcon: false, - duration: 750, - }); - - if ( - modularEndpoints.has(endpoint ?? '') && - modularEndpoints.has(newPreset?.endpoint ?? '') && - endpoint === newPreset?.endpoint - ) { - const currentConvo = getDefaultConversation({ - conversation: conversation ?? {}, - preset: newPreset, - }); - - /* We don't reset the latest message, only when changing settings mid-converstion */ - navigateToConvo(currentConvo, false); - return; - } - - console.log('preset', newPreset, endpoint); - newConversation({ preset: newPreset }); - }; - - const onChangePreset = (preset: TPreset) => { - setPreset(preset); - setPresetModalVisible(true); - }; - - const clearAllPresets = () => { - deletePresetsMutation.mutate({ arg: {} }); - }; - - const onDeletePreset = (preset: TPreset) => { - deletePresetsMutation.mutate({ arg: preset }); - }; + const { + presetsQuery, + onSetDefaultPreset, + onFileSelected, + onSelectPreset, + onChangePreset, + clearAllPresets, + onDeletePreset, + submitPreset, + exportPreset, + } = usePresets(); + const presets = presetsQuery.data || []; return ( @@ -125,6 +54,7 @@ const PresetsMenu: FC = () => { > { - + ); }; diff --git a/client/src/components/Chat/Menus/UI/MenuItem.tsx b/client/src/components/Chat/Menus/UI/MenuItem.tsx index 30806573e97..fe02a87a7e2 100644 --- a/client/src/components/Chat/Menus/UI/MenuItem.tsx +++ b/client/src/components/Chat/Menus/UI/MenuItem.tsx @@ -15,7 +15,7 @@ type MenuItemProps = { textClassName?: string; disableHover?: boolean; // hoverContent?: string; -}; +} & Record; const MenuItem: FC = ({ title, @@ -30,6 +30,7 @@ const MenuItem: FC = ({ disableHover = false, children, onClick, + ...rest }) => { return (
= ({ )} tabIndex={-1} onClick={onClick} + {...rest} >
diff --git a/client/src/components/Endpoints/SaveAsPresetDialog.tsx b/client/src/components/Endpoints/SaveAsPresetDialog.tsx index 98bc8bed0bc..904d962b9be 100644 --- a/client/src/components/Endpoints/SaveAsPresetDialog.tsx +++ b/client/src/components/Endpoints/SaveAsPresetDialog.tsx @@ -1,14 +1,17 @@ import React, { useEffect, useState } from 'react'; import { useCreatePresetMutation } from 'librechat-data-provider'; import type { TEditPresetProps } from '~/common'; -import { Dialog, Input, Label } from '~/components/ui/'; -import DialogTemplate from '~/components/ui/DialogTemplate'; import { cn, defaultTextPropsLabel, removeFocusOutlines, cleanupPreset } from '~/utils/'; +import DialogTemplate from '~/components/ui/DialogTemplate'; +import { Dialog, Input, Label } from '~/components/ui/'; +import { NotificationSeverity } from '~/common'; +import { useToastContext } from '~/Providers'; 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 submitPreset = () => { @@ -18,7 +21,24 @@ const SaveAsPresetDialog = ({ open, onOpenChange, preset }: TEditPresetProps) => title, }, }); - createPresetMutation.mutate(_preset); + + const toastTitle = _preset.title + ? `\`${_preset.title}\`` + : localize('com_endpoint_preset_title'); + + createPresetMutation.mutate(_preset, { + onSuccess: () => { + showToast({ + message: `${toastTitle} ${localize('com_endpoint_preset_saved')}`, + }); + }, + onError: () => { + showToast({ + message: localize('com_endpoint_preset_save_error'), + severity: NotificationSeverity.ERROR, + }); + }, + }); }; useEffect(() => { diff --git a/client/src/components/Nav/Logout.tsx b/client/src/components/Nav/Logout.tsx index 455a4ba1471..4a7eca5e448 100644 --- a/client/src/components/Nav/Logout.tsx +++ b/client/src/components/Nav/Logout.tsx @@ -8,6 +8,10 @@ const Logout = forwardRef(() => { const localize = useLocalize(); const handleLogout = () => { + localStorage.removeItem('lastConversationSetup'); + localStorage.removeItem('lastSelectedTools'); + localStorage.removeItem('lastAssistant'); + localStorage.removeItem('autoScroll'); logout(); }; diff --git a/client/src/components/svg/AnthropicIcon.tsx b/client/src/components/svg/AnthropicIcon.tsx index ffb703c1e75..ee5e57edaa3 100644 --- a/client/src/components/svg/AnthropicIcon.tsx +++ b/client/src/components/svg/AnthropicIcon.tsx @@ -1,5 +1,11 @@ import { cn } from '~/utils'; -export default function AnthropicIcon({ size = 25, className = '' }) { +export default function AnthropicIcon({ + size = 25, + className = '', +}: { + size?: number; + className?: string; +}) { return ( + + + + + ); + } + return ( + + + + ); +} diff --git a/client/src/components/svg/index.ts b/client/src/components/svg/index.ts index 1c3ec66d839..8a2964b718a 100644 --- a/client/src/components/svg/index.ts +++ b/client/src/components/svg/index.ts @@ -25,6 +25,7 @@ export { default as SendIcon } from './SendIcon'; export { default as LinkIcon } from './LinkIcon'; export { default as DotsIcon } from './DotsIcon'; export { default as GearIcon } from './GearIcon'; +export { default as PinIcon } from './PinIcon'; export { default as TrashIcon } from './TrashIcon'; export { default as MinimalPlugin } from './MinimalPlugin'; export { default as AzureMinimalIcon } from './AzureMinimalIcon'; diff --git a/client/src/data-provider/mutations.ts b/client/src/data-provider/mutations.ts index b8069bccdbf..26c51855007 100644 --- a/client/src/data-provider/mutations.ts +++ b/client/src/data-provider/mutations.ts @@ -7,6 +7,10 @@ import type { DeleteFilesResponse, DeleteFilesBody, DeleteMutationOptions, + UpdatePresetOptions, + DeletePresetOptions, + PresetDeleteResponse, + TPreset, } from 'librechat-data-provider'; import { dataService, MutationKeys } from 'librechat-data-provider'; @@ -37,3 +41,31 @@ export const useDeleteFilesMutation = ( ...(options || {}), }); }; + +export const useUpdatePresetMutation = ( + options?: UpdatePresetOptions, +): UseMutationResult< + TPreset, // response data + unknown, + TPreset, + unknown +> => { + return useMutation([MutationKeys.updatePreset], { + mutationFn: (preset: TPreset) => dataService.updatePreset(preset), + ...(options || {}), + }); +}; + +export const useDeletePresetMutation = ( + options?: DeletePresetOptions, +): UseMutationResult< + PresetDeleteResponse, // response data + unknown, + TPreset | undefined, + unknown +> => { + return useMutation([MutationKeys.deletePreset], { + mutationFn: (preset: TPreset | undefined) => dataService.deletePreset(preset), + ...(options || {}), + }); +}; diff --git a/client/src/hooks/Conversations/index.ts b/client/src/hooks/Conversations/index.ts new file mode 100644 index 00000000000..666341ddd64 --- /dev/null +++ b/client/src/hooks/Conversations/index.ts @@ -0,0 +1 @@ +export { default as usePresets } from './usePresets'; diff --git a/client/src/hooks/Conversations/usePresets.ts b/client/src/hooks/Conversations/usePresets.ts new file mode 100644 index 00000000000..7af73657b41 --- /dev/null +++ b/client/src/hooks/Conversations/usePresets.ts @@ -0,0 +1,223 @@ +import { + QueryKeys, + modularEndpoints, + useGetPresetsQuery, + useCreatePresetMutation, +} from 'librechat-data-provider'; +import filenamify from 'filenamify'; +import { useCallback, useEffect, useRef } from 'react'; +import { useRecoilState, useSetRecoilState } from 'recoil'; +import exportFromJSON from 'export-from-json'; +import { useQueryClient } from '@tanstack/react-query'; +import type { TPreset } from 'librechat-data-provider'; +import { useUpdatePresetMutation, useDeletePresetMutation } from '~/data-provider'; +import { useChatContext, useToastContext } from '~/Providers'; +import useNavigateToConvo from '~/hooks/useNavigateToConvo'; +import useDefaultConvo from '~/hooks/useDefaultConvo'; +import { useAuthContext } from '~/hooks/AuthContext'; +import { NotificationSeverity } from '~/common'; +import useLocalize from '~/hooks/useLocalize'; +import { cleanupPreset } from '~/utils'; +import store from '~/store'; + +export default function usePresets() { + const localize = useLocalize(); + const { user } = useAuthContext(); + const queryClient = useQueryClient(); + const { showToast } = useToastContext(); + const hasLoaded = useRef(false); + + const [_defaultPreset, setDefaultPreset] = useRecoilState(store.defaultPreset); + const setPresetModalVisible = useSetRecoilState(store.presetModalVisible); + const { preset, conversation, newConversation, setPreset } = useChatContext(); + const presetsQuery = useGetPresetsQuery({ enabled: !!user }); + + useEffect(() => { + if (_defaultPreset || !presetsQuery.data || hasLoaded.current) { + return; + } + + const defaultPreset = presetsQuery.data.find((p) => p.defaultPreset); + if (!defaultPreset) { + hasLoaded.current = true; + return; + } + setDefaultPreset(defaultPreset); + if (!conversation?.conversationId || conversation.conversationId === 'new') { + newConversation({ preset: defaultPreset }); + } + hasLoaded.current = true; + // dependencies are stable and only needed once + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [presetsQuery.data]); + + const setPresets = useCallback( + (presets: TPreset[]) => { + queryClient.setQueryData([QueryKeys.presets], presets); + }, + [queryClient], + ); + + const deletePresetsMutation = useDeletePresetMutation({ + onMutate: (preset) => { + if (!preset) { + setPresets([]); + return; + } + const previousPresets = presetsQuery.data ?? []; + if (previousPresets) { + setPresets(previousPresets.filter((p) => p.presetId !== preset?.presetId)); + } + }, + onSuccess: () => { + queryClient.invalidateQueries([QueryKeys.presets]); + }, + onError: (error) => { + queryClient.invalidateQueries([QueryKeys.presets]); + console.error('Error deleting the preset:', error); + showToast({ + message: localize('com_endpoint_preset_delete_error'), + severity: NotificationSeverity.ERROR, + }); + }, + }); + const createPresetMutation = useCreatePresetMutation(); + const updatePreset = useUpdatePresetMutation({ + onSuccess: (data, preset) => { + const toastTitle = data.title ? `"${data.title}"` : localize('com_endpoint_preset_title'); + let message = `${toastTitle} ${localize('com_endpoint_preset_saved')}`; + if (data.defaultPreset && data.presetId !== _defaultPreset?.presetId) { + message = `${toastTitle} ${localize('com_endpoint_preset_default')}`; + setDefaultPreset(data); + newConversation({ preset: data }); + } else if (preset?.defaultPreset === false) { + setDefaultPreset(null); + message = `${toastTitle} ${localize('com_endpoint_preset_default_removed')}`; + } + showToast({ + message, + }); + queryClient.invalidateQueries([QueryKeys.presets]); + }, + onError: (error) => { + console.error('Error updating the preset:', error); + showToast({ + message: localize('com_endpoint_preset_save_error'), + severity: NotificationSeverity.ERROR, + }); + }, + }); + const { navigateToConvo } = useNavigateToConvo(); + const getDefaultConversation = useDefaultConvo(); + + const { endpoint } = conversation ?? {}; + + const importPreset = (jsonPreset: TPreset) => { + createPresetMutation.mutate( + { ...jsonPreset }, + { + onSuccess: () => { + showToast({ + message: localize('com_endpoint_preset_import'), + }); + queryClient.invalidateQueries([QueryKeys.presets]); + }, + onError: (error) => { + console.error('Error uploading the preset:', error); + showToast({ + message: localize('com_endpoint_preset_import_error'), + severity: NotificationSeverity.ERROR, + }); + }, + }, + ); + }; + + const onFileSelected = (jsonData: Record) => { + const jsonPreset = { ...cleanupPreset({ preset: jsonData }), presetId: null }; + importPreset(jsonPreset); + }; + + const onSelectPreset = (newPreset: TPreset) => { + if (!newPreset) { + return; + } + + const toastTitle = newPreset.title + ? `"${newPreset.title}"` + : localize('com_endpoint_preset_title'); + + showToast({ + message: `${toastTitle} ${localize('com_endpoint_preset_selected_title')}`, + showIcon: false, + duration: 750, + }); + + if ( + modularEndpoints.has(endpoint ?? '') && + modularEndpoints.has(newPreset?.endpoint ?? '') && + endpoint === newPreset?.endpoint + ) { + const currentConvo = getDefaultConversation({ + conversation: conversation ?? {}, + preset: newPreset, + }); + + /* We don't reset the latest message, only when changing settings mid-converstion */ + navigateToConvo(currentConvo, false); + return; + } + + newConversation({ preset: newPreset }); + }; + + const onChangePreset = (preset: TPreset) => { + setPreset(preset); + setPresetModalVisible(true); + }; + + const clearAllPresets = () => deletePresetsMutation.mutate(undefined); + + const onDeletePreset = (preset: TPreset) => { + if (!confirm(localize('com_endpoint_preset_delete_confirm'))) { + return; + } + deletePresetsMutation.mutate(preset); + }; + + const submitPreset = () => { + if (!preset) { + return; + } + + updatePreset.mutate(cleanupPreset({ preset })); + }; + + const onSetDefaultPreset = (preset: TPreset, remove = false) => { + updatePreset.mutate({ ...preset, defaultPreset: !remove }); + }; + + const exportPreset = () => { + if (!preset) { + return; + } + const fileName = filenamify(preset?.title || 'preset'); + exportFromJSON({ + data: cleanupPreset({ preset }), + fileName, + exportType: exportFromJSON.types.json, + }); + }; + + return { + presetsQuery, + onSetDefaultPreset, + onFileSelected, + onSelectPreset, + onChangePreset, + clearAllPresets, + onDeletePreset, + submitPreset, + exportPreset, + }; +} diff --git a/client/src/hooks/index.ts b/client/src/hooks/index.ts index ee710e8caf0..dabf59055e0 100644 --- a/client/src/hooks/index.ts +++ b/client/src/hooks/index.ts @@ -1,5 +1,6 @@ export * from './Messages'; export * from './Input'; +export * from './Conversations'; export * from './AuthContext'; export * from './ThemeContext'; diff --git a/client/src/hooks/useNewConvo.ts b/client/src/hooks/useNewConvo.ts index c06768a783c..00b3a94a743 100644 --- a/client/src/hooks/useNewConvo.ts +++ b/client/src/hooks/useNewConvo.ts @@ -1,6 +1,12 @@ import { useCallback } from 'react'; import { useGetEndpointsQuery } from 'librechat-data-provider'; -import { useSetRecoilState, useResetRecoilState, useRecoilCallback, useRecoilState } from 'recoil'; +import { + useSetRecoilState, + useResetRecoilState, + useRecoilCallback, + useRecoilState, + useRecoilValue, +} from 'recoil'; import type { TConversation, TSubmission, TPreset, TModelsConfig } from 'librechat-data-provider'; import { buildDefaultConvo, getDefaultEndpoint } from '~/utils'; import { useDeleteFilesMutation } from '~/data-provider'; @@ -11,6 +17,7 @@ import store from '~/store'; const useNewConvo = (index = 0) => { const setStorage = useSetStorage(); const navigate = useOriginNavigate(); + const defaultPreset = useRecoilValue(store.defaultPreset); const { setConversation } = store.useCreateConversationAtom(index); const [files, setFiles] = useRecoilState(store.filesByIndex(index)); const setSubmission = useSetRecoilState(store.submissionByIndex(index)); @@ -36,17 +43,29 @@ const useNewConvo = (index = 0) => { ) => { const modelsConfig = modelsData ?? snapshot.getLoadable(store.modelsConfig).contents; const { endpoint = null } = conversation; + const buildDefaultConversation = endpoint === null || buildDefault; + const activePreset = + // use default preset only when it's defined, + // preset is not provided, + // endpoint matches or is null (to allow endpoint change), + // and buildDefaultConversation is true + defaultPreset && + !preset && + (defaultPreset.endpoint === endpoint || !endpoint) && + buildDefaultConversation + ? defaultPreset + : preset; - if (endpoint === null || buildDefault) { + if (buildDefaultConversation) { const defaultEndpoint = getDefaultEndpoint({ - convoSetup: preset ?? conversation, + convoSetup: activePreset ?? conversation, endpointsConfig, }); const models = modelsConfig?.[defaultEndpoint] ?? []; conversation = buildDefaultConvo({ conversation, - lastConversationSetup: preset as TConversation, + lastConversationSetup: activePreset as TConversation, endpoint: defaultEndpoint, models, }); @@ -61,7 +80,7 @@ const useNewConvo = (index = 0) => { navigate('new'); } }, - [endpointsConfig], + [endpointsConfig, defaultPreset], ); const newConversation = useCallback( diff --git a/client/src/localization/languages/Eng.tsx b/client/src/localization/languages/Eng.tsx index 492b1d7acb8..8aed3624ced 100644 --- a/client/src/localization/languages/Eng.tsx +++ b/client/src/localization/languages/Eng.tsx @@ -178,9 +178,22 @@ export default { 'Set custom instructions to include in System Message. Default: none', com_endpoint_import: 'Import', com_endpoint_set_custom_name: 'Set a custom name, in case you can find this preset', + com_endpoint_preset_delete_confirm: 'Are you sure you want to delete this preset?', + com_endpoint_preset_clear_all_confirm: 'Are you sure you want to delete all of your presets?', + com_endpoint_preset_import: 'Preset Imported!', + com_endpoint_preset_import_error: 'There was an error importing your preset. Please try again.', + com_endpoint_preset_save_error: 'There was an error saving your preset. Please try again.', + com_endpoint_preset_delete_error: 'There was an error deleting your preset. Please try again.', + com_endpoint_preset_default_removed: 'is no longer the default preset.', + com_endpoint_preset_default_item: 'Default:', + com_endpoint_preset_default_none: 'No default preset active.', + com_endpoint_preset_title: 'Preset', + com_endpoint_preset_saved: 'Saved!', + com_endpoint_preset_default: 'is now the default preset.', com_endpoint_preset: 'preset', com_endpoint_presets: 'presets', com_endpoint_preset_selected: 'Preset Active!', + com_endpoint_preset_selected_title: 'Active!', com_endpoint_preset_name: 'Preset Name', com_endpoint_new_topic: 'New Topic', com_endpoint: 'Endpoint', diff --git a/client/src/routes/Root.tsx b/client/src/routes/Root.tsx index d6619cdd525..7fe5fc40096 100644 --- a/client/src/routes/Root.tsx +++ b/client/src/routes/Root.tsx @@ -2,11 +2,7 @@ import { useEffect, useState } from 'react'; import { useRecoilValue, useSetRecoilState } from 'recoil'; import { Outlet, useLocation } from 'react-router-dom'; -import { - useGetModelsQuery, - useGetPresetsQuery, - useGetSearchEnabledQuery, -} from 'librechat-data-provider'; +import { useGetModelsQuery, useGetSearchEnabledQuery } from 'librechat-data-provider'; import type { ContextType } from '~/common'; import { Nav, MobileNav } from '~/components/Nav'; import { useAuthContext, useServerStream, useConversation } from '~/hooks'; @@ -15,7 +11,7 @@ import store from '~/store'; export default function Root() { const location = useLocation(); const { newConversation } = useConversation(); - const { user, isAuthenticated } = useAuthContext(); + const { isAuthenticated } = useAuthContext(); const [navVisible, setNavVisible] = useState(() => { const savedNavVisible = localStorage.getItem('navVisible'); return savedNavVisible !== null ? JSON.parse(savedNavVisible) : false; @@ -24,13 +20,11 @@ export default function Root() { const submission = useRecoilValue(store.submission); useServerStream(submission ?? null); - const setPresets = useSetRecoilState(store.presets); const setIsSearchEnabled = useSetRecoilState(store.isSearchEnabled); const setModelsConfig = useSetRecoilState(store.modelsConfig); const searchEnabledQuery = useGetSearchEnabledQuery({ enabled: isAuthenticated }); const modelsQuery = useGetModelsQuery({ enabled: isAuthenticated }); - const presetsQuery = useGetPresetsQuery({ enabled: !!user }); useEffect(() => { localStorage.setItem('navVisible', JSON.stringify(navVisible)); @@ -48,14 +42,6 @@ export default function Root() { } }, [modelsQuery.data, modelsQuery.isError]); - useEffect(() => { - if (presetsQuery.data) { - setPresets(presetsQuery.data); - } else if (presetsQuery.isError) { - console.error('Failed to get presets', presetsQuery.error); - } - }, [presetsQuery.data, presetsQuery.isError]); - useEffect(() => { if (searchEnabledQuery.data) { setIsSearchEnabled(searchEnabledQuery.data); diff --git a/client/src/store/preset.ts b/client/src/store/preset.ts index d5953c8474a..f91c80faf29 100644 --- a/client/src/store/preset.ts +++ b/client/src/store/preset.ts @@ -16,7 +16,19 @@ const preset = atom({ default: null, }); +const defaultPreset = atom({ + key: 'defaultPreset', + default: null, +}); + +const presetModalVisible = atom({ + key: 'presetModalVisible', + default: false, +}); + export default { preset, presets, + defaultPreset, + presetModalVisible, }; diff --git a/client/src/utils/presets.ts b/client/src/utils/presets.ts index 2e6147eb995..512572526d6 100644 --- a/client/src/utils/presets.ts +++ b/client/src/utils/presets.ts @@ -1,5 +1,5 @@ import type { TPreset } from 'librechat-data-provider'; -import { EModelEndpoint, alternateName } from 'librechat-data-provider'; +import { EModelEndpoint } from 'librechat-data-provider'; export const getPresetIcon = (preset: TPreset, Icon) => { return Icon({ @@ -13,43 +13,34 @@ export const getPresetIcon = (preset: TPreset, Icon) => { }; export const getPresetTitle = (preset: TPreset) => { - const { endpoint } = preset; - let _title = `${alternateName[endpoint ?? '']}`; - const { chatGptLabel, modelLabel, model, jailbreak, toneStyle } = preset; + const { + endpoint, + title: presetTitle, + model, + chatGptLabel, + modelLabel, + jailbreak, + toneStyle, + } = preset; + let title = ''; + let modelInfo = model || ''; + let label = ''; - if (endpoint === EModelEndpoint.azureOpenAI || endpoint === EModelEndpoint.openAI) { - if (chatGptLabel) { - _title = chatGptLabel; - } - if (model) { - _title += `: ${model}`; - } - } else if (endpoint === EModelEndpoint.google || endpoint === EModelEndpoint.anthropic) { - if (modelLabel) { - _title = modelLabel; - } - if (model) { - _title += `: ${model}`; - } + if (endpoint && [EModelEndpoint.azureOpenAI, EModelEndpoint.openAI].includes(endpoint)) { + label = chatGptLabel || ''; + } else if (endpoint && [EModelEndpoint.google, EModelEndpoint.anthropic].includes(endpoint)) { + label = modelLabel || ''; } else if (endpoint === EModelEndpoint.bingAI) { - if (jailbreak) { - _title = 'Sydney'; - } - if (toneStyle) { - _title += `: ${toneStyle}`; - } - } else if (endpoint === EModelEndpoint.chatGPTBrowser) { - if (model) { - _title += `: ${model}`; - } - } else if (endpoint === EModelEndpoint.gptPlugins) { - if (model) { - _title += `: ${model}`; - } - } else if (endpoint === null) { - null; - } else { - null; + modelInfo = jailbreak ? 'Sydney' : modelInfo; + label = toneStyle ? `: ${toneStyle}` : ''; } - return _title; + + if (label && presetTitle && label.toLowerCase().includes(presetTitle.toLowerCase())) { + title = label + ': '; + label = ''; + } else if (presetTitle && presetTitle.trim() !== 'New Chat') { + title = presetTitle + ': '; + } + + return `${title}${modelInfo}${label ? ` (${label})` : ''}`.trim(); }; diff --git a/package-lock.json b/package-lock.json index 3616f36b20d..1a52e18af1b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -681,6 +681,7 @@ "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.2.0", + "react-flip-toolkit": "^7.1.0", "react-hook-form": "^7.43.9", "react-lazy-load-image-component": "^1.6.0", "react-markdown": "^8.0.6", @@ -12470,6 +12471,18 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "node_modules/flip-toolkit": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/flip-toolkit/-/flip-toolkit-7.1.0.tgz", + "integrity": "sha512-tvids+uibr8gVFUp1xHMkNSIFvM4++Xr4jAJouUVsId2hv3YvhvC4Ht2FJzdxBZHhI4AeULPFAF6z9fhc20XWQ==", + "dependencies": { + "rematrix": "0.2.2" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, "node_modules/fn.name": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", @@ -20605,6 +20618,23 @@ "react": "^18.2.0" } }, + "node_modules/react-flip-toolkit": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/react-flip-toolkit/-/react-flip-toolkit-7.1.0.tgz", + "integrity": "sha512-KJ2IecKpYOJWgtXY9myyJzzC96FJaE9/8pFSAKgIoG54tiUAZ64ksDpmB+QmMofqFTa06RK7xWb9Rfavf8qz4Q==", + "dependencies": { + "flip-toolkit": "7.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + }, + "peerDependencies": { + "react": ">= 16.x", + "react-dom": ">= 16.x" + } + }, "node_modules/react-hook-form": { "version": "7.46.1", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.46.1.tgz", @@ -21153,6 +21183,11 @@ "unist-util-visit": "^4.0.0" } }, + "node_modules/rematrix": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/rematrix/-/rematrix-0.2.2.tgz", + "integrity": "sha512-agFFS3RzrLXJl5LY5xg/xYyXvUuVAnkhgKO7RaO9J1Ssth6yvbO+PIiV67V59MB5NCdAK2flvGvNT4mdKVniFA==" + }, "node_modules/remove-accents": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz", diff --git a/packages/data-provider/src/data-service.ts b/packages/data-provider/src/data-service.ts index a9cb4191ee8..d49ab599beb 100644 --- a/packages/data-provider/src/data-service.ts +++ b/packages/data-provider/src/data-service.ts @@ -1,4 +1,5 @@ import * as f from './types/files'; +import * as p from './types/presets'; import * as a from './types/assistants'; import * as t from './types'; import * as s from './schemas'; @@ -73,15 +74,15 @@ export function getPresets(): Promise { return request.get(endpoints.presets()); } -export function createPreset(payload: s.TPreset): Promise { +export function createPreset(payload: s.TPreset): Promise { return request.post(endpoints.presets(), payload); } -export function updatePreset(payload: s.TPreset): Promise { +export function updatePreset(payload: s.TPreset): Promise { return request.post(endpoints.presets(), payload); } -export function deletePreset(arg: s.TPreset | object): Promise { +export function deletePreset(arg: s.TPreset | undefined): Promise { return request.post(endpoints.deletePreset(), arg); } diff --git a/packages/data-provider/src/index.ts b/packages/data-provider/src/index.ts index 2cf6c8dbeb1..2cb3aa2e24d 100644 --- a/packages/data-provider/src/index.ts +++ b/packages/data-provider/src/index.ts @@ -2,6 +2,7 @@ export * from './types'; export * from './types/assistants'; export * from './types/files'; +export * from './types/presets'; /* * react query * TODO: move to client, or move schemas/types to their own package diff --git a/packages/data-provider/src/keys.ts b/packages/data-provider/src/keys.ts index 7354e80e0d9..f4c9f3ce588 100644 --- a/packages/data-provider/src/keys.ts +++ b/packages/data-provider/src/keys.ts @@ -20,4 +20,6 @@ export enum QueryKeys { export enum MutationKeys { imageUpload = 'imageUpload', fileDelete = 'fileDelete', + updatePreset = 'updatePreset', + deletePreset = 'deletePreset', } diff --git a/packages/data-provider/src/react-query-service.ts b/packages/data-provider/src/react-query-service.ts index 8f015acf1ef..3f2dd778d03 100644 --- a/packages/data-provider/src/react-query-service.ts +++ b/packages/data-provider/src/react-query-service.ts @@ -8,6 +8,7 @@ import { } from '@tanstack/react-query'; import * as t from './types'; import * as s from './schemas'; +import * as p from './types/presets'; import * as dataService from './data-service'; import request from './request'; import { QueryKeys } from './keys'; @@ -276,7 +277,7 @@ export const useGetModelsQuery = ( }; export const useCreatePresetMutation = (): UseMutationResult< - s.TPreset[], + s.TPreset, unknown, s.TPreset, unknown @@ -290,7 +291,7 @@ export const useCreatePresetMutation = (): UseMutationResult< }; export const useUpdatePresetMutation = (): UseMutationResult< - s.TPreset[], + s.TPreset, unknown, s.TPreset, unknown @@ -315,13 +316,13 @@ export const useGetPresetsQuery = ( }; export const useDeletePresetMutation = (): UseMutationResult< - s.TPreset[], + p.PresetDeleteResponse, unknown, - s.TPreset | object, + s.TPreset | undefined, unknown > => { const queryClient = useQueryClient(); - return useMutation((payload: s.TPreset | object) => dataService.deletePreset(payload), { + return useMutation((payload: s.TPreset | undefined) => dataService.deletePreset(payload), { onSuccess: () => { queryClient.invalidateQueries([QueryKeys.presets]); }, @@ -369,6 +370,9 @@ export const useLoginUserMutation = (): UseMutationResult< return useMutation((payload: t.TLoginUser) => dataService.login(payload), { onSuccess: () => { queryClient.invalidateQueries([QueryKeys.user]); + queryClient.invalidateQueries([QueryKeys.presets]); + queryClient.invalidateQueries([QueryKeys.conversation]); + queryClient.invalidateQueries([QueryKeys.allConversations]); }, onMutate: () => { queryClient.invalidateQueries([QueryKeys.models]); diff --git a/packages/data-provider/src/schemas.ts b/packages/data-provider/src/schemas.ts index 41ba554b329..f8904b8ea35 100644 --- a/packages/data-provider/src/schemas.ts +++ b/packages/data-provider/src/schemas.ts @@ -225,6 +225,8 @@ export const tPresetSchema = tConversationSchema conversationId: z.string().optional(), presetId: z.string().nullable().optional(), title: z.string().nullable().optional(), + defaultPreset: z.boolean().optional(), + order: z.number().optional(), }), ); diff --git a/packages/data-provider/src/types/presets.ts b/packages/data-provider/src/types/presets.ts new file mode 100644 index 00000000000..2beaf307a60 --- /dev/null +++ b/packages/data-provider/src/types/presets.ts @@ -0,0 +1,22 @@ +import { TPreset } from '../types'; + +export type PresetDeleteResponse = { + acknowledged: boolean; + deletedCount: number; +}; + +export type UpdatePresetOptions = { + onSuccess?: (data: TPreset, variables: TPreset, context?: unknown) => void; + onMutate?: (variables: TPreset) => void | Promise; + onError?: (error: unknown, variables: TPreset, context?: unknown) => void; +}; + +export type DeletePresetOptions = { + onSuccess?: ( + data: PresetDeleteResponse, + variables: TPreset | undefined, + context?: unknown, + ) => void; + onMutate?: (variables: TPreset | undefined) => void | Promise; + onError?: (error: unknown, variables: TPreset | undefined, context?: unknown) => void; +}; From 2e390596eac3fff2aacabe8244d66ff06cf89da0 Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+Berry-13@users.noreply.github.com> Date: Wed, 6 Dec 2023 20:08:15 +0100 Subject: [PATCH 2/5] return 404 instead of 200 (#1294) --- api/server/index.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/api/server/index.js b/api/server/index.js index 52500cd9675..1120dfe6dc9 100644 --- a/api/server/index.js +++ b/api/server/index.js @@ -71,9 +71,8 @@ const startServer = async () => { app.use('/api/assistants', routes.assistants); app.use('/api/files', routes.files); - // Static files - app.get('/*', function (req, res) { - res.sendFile(path.join(projectPath, 'dist', 'index.html')); + app.use((req, res) => { + res.status(404).sendFile(path.join(projectPath, 'dist', 'index.html')); }); app.listen(port, host, () => { From 9b2359fc2724b807cba18c3a33f69f91ebaf521e Mon Sep 17 00:00:00 2001 From: Danny Avila <110412045+danny-avila@users.noreply.github.com> Date: Wed, 6 Dec 2023 14:10:06 -0500 Subject: [PATCH 3/5] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20refactor:=20Improve?= =?UTF-8?q?=20Input=20Placeholder=20Handling=20and=20Error=20Management=20?= =?UTF-8?q?=F0=9F=94=84=20(#1296)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: identify new chat buttons with testid * fix: avoid parsing error in useSSE, which causes errorHandler to fail * fix: ensure last message isn't setting latestMessage when conversationId is `new` and text is the same due to possible re-renders * refactor: set placeholder through inputRef and useEffect * Update useSSE.ts * Update useSSE.ts --- client/src/components/Chat/Input/Textarea.tsx | 2 - client/src/components/Chat/Menus/NewChat.tsx | 3 +- client/src/components/Nav/MobileNav.tsx | 7 ++- client/src/components/Nav/NewChat.tsx | 2 +- client/src/hooks/Input/useTextarea.ts | 56 ++++++++++++++----- .../src/hooks/Messages/useMessageHelpers.ts | 12 +++- client/src/hooks/useSSE.ts | 5 ++ 7 files changed, 64 insertions(+), 23 deletions(-) diff --git a/client/src/components/Chat/Input/Textarea.tsx b/client/src/components/Chat/Input/Textarea.tsx index 7050fef3492..70cd0dee41c 100644 --- a/client/src/components/Chat/Input/Textarea.tsx +++ b/client/src/components/Chat/Input/Textarea.tsx @@ -11,7 +11,6 @@ export default function Textarea({ value, disabled, onChange, setText, submitMes handleKeyDown, handleCompositionStart, handleCompositionEnd, - placeholder, } = useTextarea({ setText, submitMessage, disabled }); return ( @@ -31,7 +30,6 @@ export default function Textarea({ value, disabled, onChange, setText, submitMes data-testid="text-input" style={{ height: 44, overflowY: 'auto' }} rows={1} - placeholder={placeholder} className={cn( supportsFiles[endpoint] ? ' pl-10 md:pl-[55px]' : 'pl-3 md:pl-4', 'm-0 w-full resize-none border-0 bg-transparent py-[10px] pr-10 placeholder-black/50 focus:ring-0 focus-visible:ring-0 dark:bg-transparent dark:placeholder-white/50 md:py-3.5 md:pr-12 ', diff --git a/client/src/components/Chat/Menus/NewChat.tsx b/client/src/components/Chat/Menus/NewChat.tsx index 28af271c64e..4035714f9cd 100644 --- a/client/src/components/Chat/Menus/NewChat.tsx +++ b/client/src/components/Chat/Menus/NewChat.tsx @@ -1,7 +1,7 @@ import { useChatContext } from '~/Providers'; import { useMediaQuery } from '~/hooks'; -export default function Header() { +export default function NewChat() { const { newConversation } = useChatContext(); const isSmallScreen = useMediaQuery('(max-width: 768px)'); if (isSmallScreen) { @@ -9,6 +9,7 @@ export default function Header() { } return (