Date: Sat, 5 Aug 2023 12:10:36 -0400
Subject: [PATCH 2/2] refactor(types): use zod for better type safety,
style(Messages): new scroll behavior, style(Buttons): match ChatGPT (#761)
* feat: add zod schemas for better type safety
* refactor(useSetOptions): remove 'as Type' in favor of zod schema
* fix: descendant console error, change
tag for content in PluginTooltip component
* style(MessagesView): instant/snappier scroll behavior matching official site
* fix(Messages): add null check for scrollableRef before accessing its properties in handleScroll and useEffect
* fix(messageSchema.js): change type of invocationId from string to number
fix(schemas.ts): make authenticated property in tPluginSchema optional
fix(schemas.ts): make isButton property in tPluginSchema optional
fix(schemas.ts): make messages property in tConversationSchema optional and change its type to array of strings
fix(schemas.ts): make systemMessage property in tConversationSchema nullable and optional
fix(schemas.ts): make modelLabel property in tConversationSchema nullable and optional
fix(schemas.ts): make chatGptLabel property in tConversationSchema nullable and optional
fix(schemas.ts): make promptPrefix property in tConversationSchema nullable and optional
fix(schemas.ts): make context property in tConversationSchema nullable and optional
fix(schemas.ts): make jailbreakConversationId property in tConversationSchema nullable and optional
fix(schemas.ts): make conversationSignature property in tConversationSchema nullable and optional
fix(schemas.ts): make clientId property
* refactor(types): replace main types with zod schemas and inferred types
* refactor(types/schemas): use schemas for better type safety of main types
* style(ModelSelect/Buttons): remove shadow and transition
* style(ModelSelect): button changes to closer match OpenAI
* style(ModelSelect): remove green rings which flicker
* style(scrollToBottom): add two separate scrolling functions
* fix(OptionsBar.tsx): handle onFocus and onBlur events to update opacityClass
fix(Messages/index.jsx): increase debounce time for scrollIntoView function
---
api/models/schema/messageSchema.js | 2 +-
.../Endpoints/EndpointOptionsDialog.tsx | 13 +-
.../Endpoints/SaveAsPresetDialog.tsx | 4 +-
.../Endpoints/Settings/Examples.tsx | 4 +-
.../Input/ModelSelect/Anthropic.tsx | 2 +-
.../components/Input/ModelSelect/BingAI.tsx | 6 +-
.../components/Input/ModelSelect/ChatGPT.tsx | 2 +-
.../components/Input/ModelSelect/Google.tsx | 2 +-
.../components/Input/ModelSelect/OpenAI.tsx | 2 +-
.../components/Input/ModelSelect/Plugins.tsx | 16 +--
client/src/components/Input/OptionsBar.tsx | 21 ++-
client/src/components/Messages/index.jsx | 21 ++-
.../Plugins/Store/PluginStoreDialog.tsx | 13 +-
.../Plugins/Store/PluginTooltip.tsx | 4 +-
client/src/components/ui/Dropdown.jsx | 2 +-
.../src/components/ui/MultiSelectDropDown.tsx | 2 +-
client/src/components/ui/SelectDropDown.tsx | 2 +-
client/src/components/ui/Tabs.tsx | 6 +-
client/src/hooks/usePresetOptions.ts | 100 ++++++-------
client/src/hooks/useSetOptions.ts | 78 +++++-----
client/src/utils/cleanupPreset.ts | 6 +-
client/src/utils/index.ts | 2 +-
e2e/setup/authenticate.ts | 4 +
package-lock.json | 6 +-
packages/data-provider/package.json | 5 +-
packages/data-provider/src/createPayload.ts | 5 +-
packages/data-provider/src/data-service.ts | 15 +-
.../data-provider/src/react-query-service.ts | 45 +++---
packages/data-provider/src/schemas.ts | 121 ++++++++++++++++
packages/data-provider/src/types.ts | 135 +-----------------
30 files changed, 329 insertions(+), 317 deletions(-)
create mode 100644 packages/data-provider/src/schemas.ts
diff --git a/api/models/schema/messageSchema.js b/api/models/schema/messageSchema.js
index 6c0c1490a8..792b2d545a 100644
--- a/api/models/schema/messageSchema.js
+++ b/api/models/schema/messageSchema.js
@@ -25,7 +25,7 @@ const messageSchema = mongoose.Schema(
type: String,
},
invocationId: {
- type: String,
+ type: Number,
},
parentMessageId: {
type: String,
diff --git a/client/src/components/Endpoints/EndpointOptionsDialog.tsx b/client/src/components/Endpoints/EndpointOptionsDialog.tsx
index 79050829e9..ff3efc51a7 100644
--- a/client/src/components/Endpoints/EndpointOptionsDialog.tsx
+++ b/client/src/components/Endpoints/EndpointOptionsDialog.tsx
@@ -1,7 +1,7 @@
import exportFromJSON from 'export-from-json';
import { useEffect, useState } from 'react';
import { useRecoilValue, useRecoilState } from 'recoil';
-import { EditPresetProps, SetOption, TPreset } from 'librechat-data-provider';
+import { EditPresetProps, SetOption, tPresetSchema } from 'librechat-data-provider';
import { Dialog, DialogButton } from '~/components/ui';
import DialogTemplate from '~/components/ui/DialogTemplate';
import SaveAsPresetDialog from './SaveAsPresetDialog';
@@ -21,12 +21,11 @@ const EndpointOptionsDialog = ({ open, onOpenChange, preset: _preset, title }: E
const setOption: SetOption = (param) => (newValue) => {
const update = {};
update[param] = newValue;
- setPreset(
- (prevState) =>
- ({
- ...prevState,
- ...update,
- } as TPreset),
+ setPreset((prevState) =>
+ tPresetSchema.parse({
+ ...prevState,
+ ...update,
+ }),
);
};
diff --git a/client/src/components/Endpoints/SaveAsPresetDialog.tsx b/client/src/components/Endpoints/SaveAsPresetDialog.tsx
index b9b81fb846..cd9f535f4e 100644
--- a/client/src/components/Endpoints/SaveAsPresetDialog.tsx
+++ b/client/src/components/Endpoints/SaveAsPresetDialog.tsx
@@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil';
-import { useCreatePresetMutation, EditPresetProps, TPreset } from 'librechat-data-provider';
+import { useCreatePresetMutation, EditPresetProps } from 'librechat-data-provider';
import { Dialog, Input, Label } from '~/components/ui/';
import DialogTemplate from '~/components/ui/DialogTemplate';
import { cn, defaultTextPropsLabel, removeFocusOutlines, cleanupPreset } from '~/utils/';
@@ -20,7 +20,7 @@ const SaveAsPresetDialog = ({ open, onOpenChange, preset }: EditPresetProps) =>
title,
},
endpointsConfig,
- }) as TPreset;
+ });
createPresetMutation.mutate(_preset);
};
diff --git a/client/src/components/Endpoints/Settings/Examples.tsx b/client/src/components/Endpoints/Settings/Examples.tsx
index fd737bfd56..ee697f75be 100644
--- a/client/src/components/Endpoints/Settings/Examples.tsx
+++ b/client/src/components/Endpoints/Settings/Examples.tsx
@@ -68,14 +68,14 @@ function Examples({ readonly, examples, setExample, addExample, removeExample }:
diff --git a/client/src/components/Input/ModelSelect/Anthropic.tsx b/client/src/components/Input/ModelSelect/Anthropic.tsx
index 45fccf59ce..c03565de60 100644
--- a/client/src/components/Input/ModelSelect/Anthropic.tsx
+++ b/client/src/components/Input/ModelSelect/Anthropic.tsx
@@ -12,7 +12,7 @@ export default function Anthropic({ conversation, setOption, models }: ModelSele
showLabel={false}
className={cn(
cardStyle,
- 'min-w-48 z-50 flex h-[40px] w-48 flex-none items-center justify-center px-4 ring-0 transition duration-700 ease-in-out hover:cursor-pointer hover:bg-slate-50 hover:shadow-md focus:ring-0 focus:ring-offset-0 data-[state=open]:bg-slate-50 dark:bg-gray-700 dark:hover:bg-gray-600 dark:data-[state=open]:bg-gray-600',
+ 'min-w-48 z-50 flex h-[40px] w-48 flex-none items-center justify-center px-4 ring-0 hover:cursor-pointer',
)}
/>
);
diff --git a/client/src/components/Input/ModelSelect/BingAI.tsx b/client/src/components/Input/ModelSelect/BingAI.tsx
index 0482241903..97707484b6 100644
--- a/client/src/components/Input/ModelSelect/BingAI.tsx
+++ b/client/src/components/Input/ModelSelect/BingAI.tsx
@@ -34,15 +34,15 @@ export default function BingAI({ conversation, setOption, models }: ModelSelectP
showLabel={false}
className={cn(
cardStyle,
- 'z-50 flex h-[40px] w-36 flex-none items-center justify-center px-4 ring-0 transition duration-700 ease-in-out hover:cursor-pointer hover:bg-slate-50 hover:shadow-md focus:ring-0 focus:ring-offset-0 data-[state=open]:bg-slate-50 dark:bg-gray-700 dark:hover:bg-gray-600 dark:data-[state=open]:bg-gray-600',
+ 'z-50 flex h-[40px] w-36 flex-none items-center justify-center px-4 ring-0 hover:cursor-pointer hover:bg-slate-50 focus:ring-0 focus:ring-offset-0 data-[state=open]:bg-slate-50 dark:bg-gray-800 dark:hover:bg-gray-700 dark:data-[state=open]:bg-gray-600',
showBingToneSetting ? 'hidden' : '',
)}
/>
setOption('toneStyle')(value.toLowerCase())}
>
diff --git a/client/src/components/Input/ModelSelect/ChatGPT.tsx b/client/src/components/Input/ModelSelect/ChatGPT.tsx
index 9dfdf0633c..cf947e96c4 100644
--- a/client/src/components/Input/ModelSelect/ChatGPT.tsx
+++ b/client/src/components/Input/ModelSelect/ChatGPT.tsx
@@ -20,7 +20,7 @@ export default function ChatGPT({ conversation, setOption, models }: ModelSelect
showLabel={false}
className={cn(
cardStyle,
- 'min-w-48 z-50 flex h-[40px] w-60 flex-none items-center justify-center px-4 ring-0 transition duration-700 ease-in-out hover:cursor-pointer hover:bg-slate-50 hover:shadow-md focus:ring-0 focus:ring-offset-0 data-[state=open]:bg-slate-50 dark:bg-gray-700 dark:hover:bg-gray-600 dark:data-[state=open]:bg-gray-600',
+ 'min-w-48 z-50 flex h-[40px] w-60 flex-none items-center justify-center px-4 ring-0 hover:cursor-pointer',
)}
/>
);
diff --git a/client/src/components/Input/ModelSelect/Google.tsx b/client/src/components/Input/ModelSelect/Google.tsx
index a754a0aa9e..c743e98014 100644
--- a/client/src/components/Input/ModelSelect/Google.tsx
+++ b/client/src/components/Input/ModelSelect/Google.tsx
@@ -12,7 +12,7 @@ export default function Google({ conversation, setOption, models }: ModelSelectP
showLabel={false}
className={cn(
cardStyle,
- 'min-w-48 z-50 flex h-[40px] w-48 flex-none items-center justify-center px-4 ring-0 transition duration-700 ease-in-out hover:cursor-pointer hover:bg-slate-50 hover:shadow-md focus:ring-0 focus:ring-offset-0 data-[state=open]:bg-slate-50 dark:bg-gray-700 dark:hover:bg-gray-600 dark:data-[state=open]:bg-gray-600',
+ 'min-w-48 z-50 flex h-[40px] w-48 flex-none items-center justify-center px-4 ring-0 hover:cursor-pointer',
)}
/>
);
diff --git a/client/src/components/Input/ModelSelect/OpenAI.tsx b/client/src/components/Input/ModelSelect/OpenAI.tsx
index 6ebe35a7d5..cca893163b 100644
--- a/client/src/components/Input/ModelSelect/OpenAI.tsx
+++ b/client/src/components/Input/ModelSelect/OpenAI.tsx
@@ -12,7 +12,7 @@ export default function OpenAI({ conversation, setOption, models }: ModelSelectP
showLabel={false}
className={cn(
cardStyle,
- 'min-w-48 z-50 flex h-[40px] w-48 flex-none items-center justify-center px-4 ring-0 transition duration-700 ease-in-out hover:cursor-pointer hover:bg-slate-50 hover:shadow-md focus:ring-0 focus:ring-offset-0 data-[state=open]:bg-slate-50 dark:bg-gray-700 dark:hover:bg-gray-600 dark:data-[state=open]:bg-gray-600',
+ 'min-w-48 z-50 flex h-[40px] w-48 flex-none items-center justify-center px-4 hover:cursor-pointer',
)}
/>
);
diff --git a/client/src/components/Input/ModelSelect/Plugins.tsx b/client/src/components/Input/ModelSelect/Plugins.tsx
index 63311a9cff..6deac98108 100644
--- a/client/src/components/Input/ModelSelect/Plugins.tsx
+++ b/client/src/components/Input/ModelSelect/Plugins.tsx
@@ -46,7 +46,7 @@ export default function Plugins({ conversation, setOption, models }: ModelSelect
}
const tools = [...user.plugins]
- .map((el) => allPlugins.find((plugin) => plugin.pluginKey === el))
+ .map((el) => allPlugins.find((plugin: TPlugin) => plugin.pluginKey === el))
.filter((el): el is TPlugin => el !== undefined);
/* Filter Last Selected Tools */
@@ -71,7 +71,7 @@ export default function Plugins({ conversation, setOption, models }: ModelSelect
type="button"
className={cn(
cardStyle,
- 'min-w-4 z-40 flex h-[40px] flex-none items-center justify-center px-3 transition duration-700 ease-in-out hover:bg-white hover:shadow-md focus:ring-0 focus:ring-offset-0 dark:hover:bg-gray-700',
+ 'min-w-4 z-40 flex h-[40px] flex-none items-center justify-center px-3 hover:bg-white focus:ring-0 focus:ring-offset-0 dark:hover:bg-gray-700',
)}
onClick={() => setVisibility((prev) => !prev)}
>
@@ -87,11 +87,7 @@ export default function Plugins({ conversation, setOption, models }: ModelSelect
setValue={setOption('model')}
availableValues={models}
showAbove={true}
- className={cn(
- cardStyle,
- 'min-w-60 z-40 flex w-64 transition duration-700 ease-in-out hover:shadow-md sm:w-48',
- visible ? '' : 'hidden',
- )}
+ className={cn(cardStyle, 'min-w-60 z-40 flex w-64 sm:w-48', visible ? '' : 'hidden')}
/>
>
);
diff --git a/client/src/components/Input/OptionsBar.tsx b/client/src/components/Input/OptionsBar.tsx
index 9d95baabde..12dfc63360 100644
--- a/client/src/components/Input/OptionsBar.tsx
+++ b/client/src/components/Input/OptionsBar.tsx
@@ -1,7 +1,7 @@
import { Settings2 } from 'lucide-react';
import { useState, useEffect, useMemo } from 'react';
import { useRecoilValue, useRecoilState, useSetRecoilState } from 'recoil';
-import { TPreset } from 'librechat-data-provider';
+import { tPresetSchema } from 'librechat-data-provider';
import { PluginStoreDialog } from '~/components';
import {
EndpointSettings,
@@ -98,6 +98,21 @@ export default function OptionsBar() {
}
setOpacityClass('show');
}}
+ onFocus={() => {
+ if (showPopover) {
+ return;
+ }
+ setOpacityClass('full-opacity');
+ }}
+ onBlur={() => {
+ if (showPopover) {
+ return;
+ }
+ if (!messagesTree || messagesTree.length === 0) {
+ return;
+ }
+ setOpacityClass('show');
+ }}
>
{!noSettings[endpoint] && (
@@ -105,7 +120,7 @@ export default function OptionsBar() {
type="button"
className={cn(
cardStyle,
- 'min-w-4 z-50 flex h-[40px] flex-none items-center justify-center px-3 transition duration-700 ease-in-out hover:bg-slate-50 hover:shadow-md focus:ring-0 focus:ring-offset-0 dark:hover:bg-gray-600',
+ 'min-w-4 z-50 flex h-[40px] flex-none items-center justify-center px-3 focus:ring-0 focus:ring-offset-0',
)}
onClick={triggerAdvancedMode}
>
@@ -126,7 +141,7 @@ export default function OptionsBar() {
diff --git a/client/src/components/Messages/index.jsx b/client/src/components/Messages/index.jsx
index e2d0612e82..667e67e81b 100644
--- a/client/src/components/Messages/index.jsx
+++ b/client/src/components/Messages/index.jsx
@@ -28,6 +28,9 @@ export default function Messages({ isSearchView = false }) {
const { screenshotTargetRef } = useScreenshot();
const handleScroll = () => {
+ if (!scrollableRef.current) {
+ return;
+ }
const { scrollTop, scrollHeight, clientHeight } = scrollableRef.current;
const diff = Math.abs(scrollHeight - scrollTop);
const percent = Math.abs(clientHeight - diff) / clientHeight;
@@ -40,6 +43,9 @@ export default function Messages({ isSearchView = false }) {
useEffect(() => {
const timeoutId = setTimeout(() => {
+ if (!scrollableRef.current) {
+ return;
+ }
const { scrollTop, scrollHeight, clientHeight } = scrollableRef.current;
const diff = Math.abs(scrollHeight - scrollTop);
const percent = Math.abs(clientHeight - diff) / clientHeight;
@@ -58,6 +64,19 @@ export default function Messages({ isSearchView = false }) {
// eslint-disable-next-line react-hooks/exhaustive-deps
const scrollToBottom = useCallback(
+ throttle(
+ () => {
+ messagesEndRef.current?.scrollIntoView({ behavior: 'instant' });
+ setShowScrollButton(false);
+ },
+ 450,
+ { leading: true },
+ ),
+ [messagesEndRef],
+ );
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ const scrollToBottomSmooth = useCallback(
throttle(
() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
@@ -77,7 +96,7 @@ export default function Messages({ isSearchView = false }) {
const scrollHandler = (e) => {
e.preventDefault();
- scrollToBottom();
+ scrollToBottomSmooth();
};
return (
diff --git a/client/src/components/Plugins/Store/PluginStoreDialog.tsx b/client/src/components/Plugins/Store/PluginStoreDialog.tsx
index f97848ba44..bf28425d11 100644
--- a/client/src/components/Plugins/Store/PluginStoreDialog.tsx
+++ b/client/src/components/Plugins/Store/PluginStoreDialog.tsx
@@ -11,7 +11,7 @@ import {
useUpdateUserPluginsMutation,
TPlugin,
TPluginAction,
- TConversation,
+ tConversationSchema,
TError,
} from 'librechat-data-provider';
import { useAuthContext } from '~/hooks/AuthContext';
@@ -69,12 +69,11 @@ function PluginStoreDialog({ isOpen, setIsOpen }: TPluginStoreDialogProps) {
return t.pluginKey !== plugin;
});
localStorage.setItem('lastSelectedTools', JSON.stringify(tools));
- setConversation(
- (prevState) =>
- ({
- ...prevState,
- tools,
- } as TConversation),
+ setConversation((prevState) =>
+ tConversationSchema.parse({
+ ...prevState,
+ tools,
+ }),
);
},
},
diff --git a/client/src/components/Plugins/Store/PluginTooltip.tsx b/client/src/components/Plugins/Store/PluginTooltip.tsx
index 5e0d38dbf2..0a7df905b2 100644
--- a/client/src/components/Plugins/Store/PluginTooltip.tsx
+++ b/client/src/components/Plugins/Store/PluginTooltip.tsx
@@ -11,9 +11,9 @@ function PluginTooltip({ content, position }: TPluginTooltipProps) {
diff --git a/client/src/components/ui/Dropdown.jsx b/client/src/components/ui/Dropdown.jsx
index ed0cb9a02a..d31a8cb39a 100644
--- a/client/src/components/ui/Dropdown.jsx
+++ b/client/src/components/ui/Dropdown.jsx
@@ -12,7 +12,7 @@ function Dropdown({ value, onChange, options, className, containerClassName }) {
diff --git a/client/src/components/ui/MultiSelectDropDown.tsx b/client/src/components/ui/MultiSelectDropDown.tsx
index 8d727451bb..c4476586c3 100644
--- a/client/src/components/ui/MultiSelectDropDown.tsx
+++ b/client/src/components/ui/MultiSelectDropDown.tsx
@@ -43,7 +43,7 @@ function MultiSelectDropDown({
<>
diff --git a/client/src/components/ui/Tabs.tsx b/client/src/components/ui/Tabs.tsx
index db13fde848..83645e9a5b 100644
--- a/client/src/components/ui/Tabs.tsx
+++ b/client/src/components/ui/Tabs.tsx
@@ -10,7 +10,7 @@ const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
+>(({ className = '', ...props }, ref) => (
,
React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
+>(({ className = '', ...props }, ref) => (
,
React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
+>(({ className = '', ...props }, ref) => (
{
const setOption: SetOption = (param) => (newValue) => {
const update = {};
update[param] = newValue;
- setPreset(
- (prevState) =>
- cleanupPreset({
- preset: {
- ...prevState,
- ...update,
- },
- endpointsConfig,
- }) as TPreset,
+ setPreset((prevState) =>
+ cleanupPreset({
+ preset: {
+ ...prevState,
+ ...update,
+ },
+ endpointsConfig,
+ }),
);
};
@@ -32,15 +31,14 @@ const usePresetOptions: UsePresetOptions = (_preset) => {
currentExample[type] = { content: newValue };
current[i] = currentExample;
update['examples'] = current;
- setPreset(
- (prevState) =>
- cleanupPreset({
- preset: {
- ...prevState,
- ...update,
- },
- endpointsConfig,
- }) as TPreset,
+ setPreset((prevState) =>
+ cleanupPreset({
+ preset: {
+ ...prevState,
+ ...update,
+ },
+ endpointsConfig,
+ }),
);
};
@@ -49,15 +47,14 @@ const usePresetOptions: UsePresetOptions = (_preset) => {
const current = preset?.examples?.slice() || [];
current.push({ input: { content: '' }, output: { content: '' } });
update['examples'] = current;
- setPreset(
- (prevState) =>
- cleanupPreset({
- preset: {
- ...prevState,
- ...update,
- },
- endpointsConfig,
- }) as TPreset,
+ setPreset((prevState) =>
+ cleanupPreset({
+ preset: {
+ ...prevState,
+ ...update,
+ },
+ endpointsConfig,
+ }),
);
};
@@ -66,29 +63,27 @@ const usePresetOptions: UsePresetOptions = (_preset) => {
const current = preset?.examples?.slice() || [];
if (current.length <= 1) {
update['examples'] = [{ input: { content: '' }, output: { content: '' } }];
- setPreset(
- (prevState) =>
- cleanupPreset({
- preset: {
- ...prevState,
- ...update,
- },
- endpointsConfig,
- }) as TPreset,
- );
- return;
- }
- current.pop();
- update['examples'] = current;
- setPreset(
- (prevState) =>
+ setPreset((prevState) =>
cleanupPreset({
preset: {
...prevState,
...update,
},
endpointsConfig,
- }) as TPreset,
+ }),
+ );
+ return;
+ }
+ current.pop();
+ update['examples'] = current;
+ setPreset((prevState) =>
+ cleanupPreset({
+ preset: {
+ ...prevState,
+ ...update,
+ },
+ endpointsConfig,
+ }),
);
};
@@ -96,15 +91,14 @@ const usePresetOptions: UsePresetOptions = (_preset) => {
const editablePreset = JSON.parse(JSON.stringify(_preset));
const { agentOptions } = editablePreset;
agentOptions[param] = newValue;
- setPreset(
- (prevState) =>
- cleanupPreset({
- preset: {
- ...prevState,
- agentOptions,
- },
- endpointsConfig,
- }) as TPreset,
+ setPreset((prevState) =>
+ cleanupPreset({
+ preset: {
+ ...prevState,
+ agentOptions,
+ },
+ endpointsConfig,
+ }),
);
};
diff --git a/client/src/hooks/useSetOptions.ts b/client/src/hooks/useSetOptions.ts
index 83f26741b6..feb357773b 100644
--- a/client/src/hooks/useSetOptions.ts
+++ b/client/src/hooks/useSetOptions.ts
@@ -4,6 +4,7 @@ import {
SetOption,
SetExample,
TPlugin,
+ tConversationSchema,
} from 'librechat-data-provider';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import usePresetOptions from './usePresetOptions';
@@ -23,12 +24,11 @@ const useSetOptions: UseSetOptions = (preset = false) => {
const setOption: SetOption = (param) => (newValue) => {
const update = {};
update[param] = newValue;
- setConversation(
- (prevState) =>
- ({
- ...prevState,
- ...update,
- } as TConversation),
+ setConversation((prevState) =>
+ tConversationSchema.parse({
+ ...prevState,
+ ...update,
+ }),
);
};
@@ -39,12 +39,11 @@ const useSetOptions: UseSetOptions = (preset = false) => {
currentExample[type] = { content: newValue };
current[i] = currentExample;
update['examples'] = current;
- setConversation(
- (prevState) =>
- ({
- ...prevState,
- ...update,
- } as TConversation),
+ setConversation((prevState) =>
+ tConversationSchema.parse({
+ ...prevState,
+ ...update,
+ }),
);
};
@@ -53,12 +52,11 @@ const useSetOptions: UseSetOptions = (preset = false) => {
const current = conversation?.examples?.slice() || [];
current.push({ input: { content: '' }, output: { content: '' } });
update['examples'] = current;
- setConversation(
- (prevState) =>
- ({
- ...prevState,
- ...update,
- } as TConversation),
+ setConversation((prevState) =>
+ tConversationSchema.parse({
+ ...prevState,
+ ...update,
+ }),
);
};
@@ -67,23 +65,21 @@ const useSetOptions: UseSetOptions = (preset = false) => {
const current = conversation?.examples?.slice() || [];
if (current.length <= 1) {
update['examples'] = [{ input: { content: '' }, output: { content: '' } }];
- setConversation(
- (prevState) =>
- ({
- ...prevState,
- ...update,
- } as TConversation),
+ setConversation((prevState) =>
+ tConversationSchema.parse({
+ ...prevState,
+ ...update,
+ }),
);
return;
}
current.pop();
update['examples'] = current;
- setConversation(
- (prevState) =>
- ({
- ...prevState,
- ...update,
- } as TConversation),
+ setConversation((prevState) =>
+ tConversationSchema.parse({
+ ...prevState,
+ ...update,
+ }),
);
};
@@ -101,12 +97,11 @@ const useSetOptions: UseSetOptions = (preset = false) => {
const convo = JSON.parse(editableConvo);
const { agentOptions } = convo;
agentOptions[param] = newValue;
- setConversation(
- (prevState) =>
- ({
- ...prevState,
- agentOptions,
- } as TConversation),
+ setConversation((prevState) =>
+ tConversationSchema.parse({
+ ...prevState,
+ agentOptions,
+ }),
);
};
@@ -128,12 +123,11 @@ const useSetOptions: UseSetOptions = (preset = false) => {
}
localStorage.setItem('lastSelectedTools', JSON.stringify(update['tools']));
- setConversation(
- (prevState) =>
- ({
- ...prevState,
- ...update,
- } as TConversation),
+ setConversation((prevState) =>
+ tConversationSchema.parse({
+ ...prevState,
+ ...update,
+ }),
);
};
diff --git a/client/src/utils/cleanupPreset.ts b/client/src/utils/cleanupPreset.ts
index ace695b591..c02d9b8258 100644
--- a/client/src/utils/cleanupPreset.ts
+++ b/client/src/utils/cleanupPreset.ts
@@ -1,9 +1,9 @@
-import { CleanupPreset } from 'librechat-data-provider';
+import { CleanupPreset, TPreset } from 'librechat-data-provider';
-const cleanupPreset = ({ preset: _preset, endpointsConfig = {} }: CleanupPreset) => {
+const cleanupPreset = ({ preset: _preset, endpointsConfig = {} }: CleanupPreset): TPreset => {
const { endpoint } = _preset;
- let preset = {};
+ let preset = {} as TPreset;
let models = [];
if (endpoint) {
models = endpointsConfig[endpoint]?.availableModels || [];
diff --git a/client/src/utils/index.ts b/client/src/utils/index.ts
index af53786eb6..53ae0553eb 100644
--- a/client/src/utils/index.ts
+++ b/client/src/utils/index.ts
@@ -55,7 +55,7 @@ export const removeFocusOutlines =
'focus:outline-none focus:ring-0 focus:ring-opacity-0 focus:ring-offset-0';
export const cardStyle =
- 'transition-colors rounded-md min-w-[75px] font-normal bg-white border-black/10 hover:border-black/10 focus:border-black/10 dark:border-black/10 dark:hover:border-black/10 dark:focus:border-black/10 border dark:bg-gray-700 text-black dark:text-white';
+ 'transition-colors rounded-md min-w-[75px] border font-normal bg-white hover:bg-slate-50 dark:border-gray-600 dark:hover:bg-gray-700 dark:bg-gray-800 text-black dark:text-gray-600 focus:outline-none data-[state=open]:bg-slate-50 dark:data-[state=open]:bg-gray-700';
export const defaultTextProps =
'rounded-md border border-gray-200 focus:border-slate-400 focus:bg-gray-50 bg-transparent text-sm shadow-[0_0_10px_rgba(0,0,0,0.05)] outline-none placeholder:text-gray-400 focus:outline-none focus:ring-gray-400 focus:ring-opacity-20 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-500 dark:bg-gray-700 focus:dark:bg-gray-600 dark:text-gray-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:focus:border-gray-400 dark:focus:outline-none dark:focus:ring-0 dark:focus:ring-gray-400 dark:focus:ring-offset-0';
diff --git a/e2e/setup/authenticate.ts b/e2e/setup/authenticate.ts
index cb796ae4d0..0b1a3ef1af 100644
--- a/e2e/setup/authenticate.ts
+++ b/e2e/setup/authenticate.ts
@@ -15,6 +15,10 @@ async function authenticate(config: FullConfig, user: User) {
const browser = await chromium.launch();
const page = await browser.newPage();
console.log('🤖: 🗝 authenticating user:', user.username);
+
+ if (!baseURL) {
+ throw new Error('🤖: baseURL is not defined');
+ }
await page.goto(baseURL);
await login(page, user);
await page.locator('h1:has-text("LibreChat")').waitFor();
diff --git a/package-lock.json b/package-lock.json
index 91716ff12d..13b8f3ab2c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -26948,7 +26948,8 @@
"license": "ISC",
"dependencies": {
"@tanstack/react-query": "^4.28.0",
- "axios": "^1.3.4"
+ "axios": "^1.3.4",
+ "zod": "^3.21.4"
},
"devDependencies": {
"@babel/preset-env": "^7.21.5",
@@ -39519,7 +39520,8 @@
"rimraf": "^5.0.1",
"rollup": "^3.26.0",
"rollup-plugin-typescript2": "^0.35.0",
- "typescript": "^5.0.4"
+ "typescript": "^5.0.4",
+ "zod": "*"
},
"dependencies": {
"brace-expansion": {
diff --git a/packages/data-provider/package.json b/packages/data-provider/package.json
index c80ac17f26..2a870c3cbb 100644
--- a/packages/data-provider/package.json
+++ b/packages/data-provider/package.json
@@ -1,6 +1,6 @@
{
"name": "librechat-data-provider",
- "version": "0.1.3",
+ "version": "0.1.4",
"description": "data services for librechat apps",
"main": "dist/index.js",
"module": "dist/index.es.js",
@@ -25,7 +25,8 @@
"homepage": "https://github.com/danny-avila/LibreChat#readme",
"dependencies": {
"@tanstack/react-query": "^4.28.0",
- "axios": "^1.3.4"
+ "axios": "^1.3.4",
+ "zod": "^3.21.4"
},
"devDependencies": {
"@babel/preset-env": "^7.21.5",
diff --git a/packages/data-provider/src/createPayload.ts b/packages/data-provider/src/createPayload.ts
index 4372416fcd..debe5c6b2e 100644
--- a/packages/data-provider/src/createPayload.ts
+++ b/packages/data-provider/src/createPayload.ts
@@ -1,8 +1,9 @@
-import type { TConversation, TSubmission, EModelEndpoint } from './types';
+import { tConversationSchema } from './schemas';
+import type { TSubmission, EModelEndpoint } from './types';
export default function createPayload(submission: TSubmission) {
const { conversation, message, endpointOption } = submission;
- const { conversationId } = conversation as TConversation;
+ const { conversationId } = tConversationSchema.parse(conversation);
const { endpoint } = endpointOption as { endpoint: EModelEndpoint };
const endpointUrlMap = {
diff --git a/packages/data-provider/src/data-service.ts b/packages/data-provider/src/data-service.ts
index 0f38578019..44d3dea41c 100644
--- a/packages/data-provider/src/data-service.ts
+++ b/packages/data-provider/src/data-service.ts
@@ -1,4 +1,5 @@
import * as t from './types';
+import * as s from './schemas';
import request from './request';
import * as endpoints from './api-endpoints';
@@ -23,11 +24,11 @@ export function clearAllConversations(): Promise {
return request.post(endpoints.deleteConversation(), { arg: {} });
}
-export function getMessagesByConvoId(id: string): Promise {
+export function getMessagesByConvoId(id: string): Promise {
return request.get(endpoints.messages(id));
}
-export function getConversationById(id: string): Promise {
+export function getConversationById(id: string): Promise {
return request.get(endpoints.conversationById(id));
}
@@ -37,19 +38,19 @@ export function updateConversation(
return request.post(endpoints.updateConversation(), { arg: payload });
}
-export function getPresets(): Promise {
+export function getPresets(): Promise {
return request.get(endpoints.presets());
}
-export function createPreset(payload: t.TPreset): Promise {
+export function createPreset(payload: s.TPreset): Promise {
return request.post(endpoints.presets(), payload);
}
-export function updatePreset(payload: t.TPreset): Promise {
+export function updatePreset(payload: s.TPreset): Promise {
return request.post(endpoints.presets(), payload);
}
-export function deletePreset(arg: t.TPreset | object): Promise {
+export function deletePreset(arg: s.TPreset | object): Promise {
return request.post(endpoints.deletePreset(), arg);
}
@@ -106,7 +107,7 @@ export const resetPassword = (payload: t.TResetPassword) => {
return request.post(endpoints.resetPassword(), payload);
};
-export const getAvailablePlugins = (): Promise => {
+export const getAvailablePlugins = (): Promise => {
return request.get(endpoints.plugins());
};
diff --git a/packages/data-provider/src/react-query-service.ts b/packages/data-provider/src/react-query-service.ts
index d6f3db1959..3bcbffc7d9 100644
--- a/packages/data-provider/src/react-query-service.ts
+++ b/packages/data-provider/src/react-query-service.ts
@@ -7,6 +7,7 @@ import {
QueryObserverResult,
} from '@tanstack/react-query';
import * as t from './types';
+import * as s from './schemas';
import * as dataService from './data-service';
export enum QueryKeys {
@@ -47,9 +48,9 @@ export const useGetUserQuery = (
export const useGetMessagesByConvoId = (
id: string,
- config?: UseQueryOptions,
-): QueryObserverResult => {
- return useQuery(
+ config?: UseQueryOptions,
+): QueryObserverResult => {
+ return useQuery(
[QueryKeys.messages, id],
() => dataService.getMessagesByConvoId(id),
{
@@ -63,9 +64,9 @@ export const useGetMessagesByConvoId = (
export const useGetConversationByIdQuery = (
id: string,
- config?: UseQueryOptions,
-): QueryObserverResult => {
- return useQuery(
+ config?: UseQueryOptions,
+): QueryObserverResult => {
+ return useQuery(
[QueryKeys.conversation, id],
() => dataService.getConversationById(id),
{
@@ -79,10 +80,10 @@ export const useGetConversationByIdQuery = (
//This isn't ideal because its just a query and we're using mutation, but it was the only way
//to make it work with how the Chat component is structured
-export const useGetConversationByIdMutation = (id: string): UseMutationResult => {
+export const useGetConversationByIdMutation = (id: string): UseMutationResult => {
const queryClient = useQueryClient();
return useMutation(() => dataService.getConversationById(id), {
- // onSuccess: (res: t.TConversation) => {
+ // onSuccess: (res: s.TConversation) => {
onSuccess: () => {
queryClient.invalidateQueries([QueryKeys.conversation, id]);
},
@@ -174,13 +175,13 @@ export const useGetEndpointsQuery = (): QueryObserverResult
};
export const useCreatePresetMutation = (): UseMutationResult<
- t.TPreset[],
+ s.TPreset[],
unknown,
- t.TPreset,
+ s.TPreset,
unknown
> => {
const queryClient = useQueryClient();
- return useMutation((payload: t.TPreset) => dataService.createPreset(payload), {
+ return useMutation((payload: s.TPreset) => dataService.createPreset(payload), {
onSuccess: () => {
queryClient.invalidateQueries([QueryKeys.presets]);
},
@@ -188,13 +189,13 @@ export const useCreatePresetMutation = (): UseMutationResult<
};
export const useUpdatePresetMutation = (): UseMutationResult<
- t.TPreset[],
+ s.TPreset[],
unknown,
- t.TPreset,
+ s.TPreset,
unknown
> => {
const queryClient = useQueryClient();
- return useMutation((payload: t.TPreset) => dataService.updatePreset(payload), {
+ return useMutation((payload: s.TPreset) => dataService.updatePreset(payload), {
onSuccess: () => {
queryClient.invalidateQueries([QueryKeys.presets]);
},
@@ -202,9 +203,9 @@ export const useUpdatePresetMutation = (): UseMutationResult<
};
export const useGetPresetsQuery = (
- config?: UseQueryOptions,
-): QueryObserverResult => {
- return useQuery([QueryKeys.presets], () => dataService.getPresets(), {
+ config?: UseQueryOptions,
+): QueryObserverResult => {
+ return useQuery([QueryKeys.presets], () => dataService.getPresets(), {
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
@@ -213,13 +214,13 @@ export const useGetPresetsQuery = (
};
export const useDeletePresetMutation = (): UseMutationResult<
- t.TPreset[],
+ s.TPreset[],
unknown,
- t.TPreset | object,
+ s.TPreset | object,
unknown
> => {
const queryClient = useQueryClient();
- return useMutation((payload: t.TPreset | object) => dataService.deletePreset(payload), {
+ return useMutation((payload: s.TPreset | object) => dataService.deletePreset(payload), {
onSuccess: () => {
queryClient.invalidateQueries([QueryKeys.presets]);
},
@@ -323,8 +324,8 @@ export const useResetPasswordMutation = (): UseMutationResult<
return useMutation((payload: t.TResetPassword) => dataService.resetPassword(payload));
};
-export const useAvailablePluginsQuery = (): QueryObserverResult => {
- return useQuery(
+export const useAvailablePluginsQuery = (): QueryObserverResult => {
+ return useQuery(
[QueryKeys.availablePlugins],
() => dataService.getAvailablePlugins(),
{
diff --git a/packages/data-provider/src/schemas.ts b/packages/data-provider/src/schemas.ts
new file mode 100644
index 0000000000..dda6ad22b7
--- /dev/null
+++ b/packages/data-provider/src/schemas.ts
@@ -0,0 +1,121 @@
+import { z } from 'zod';
+
+export enum EModelEndpoint {
+ azureOpenAI = 'azureOpenAI',
+ openAI = 'openAI',
+ bingAI = 'bingAI',
+ chatGPT = 'chatGPT',
+ chatGPTBrowser = 'chatGPTBrowser',
+ google = 'google',
+ gptPlugins = 'gptPlugins',
+ anthropic = 'anthropic',
+}
+
+export const eModelEndpointSchema = z.nativeEnum(EModelEndpoint);
+
+export const tMessageSchema = z.object({
+ messageId: z.string(),
+ conversationId: z.string(),
+ clientId: z.string(),
+ parentMessageId: z.string(),
+ sender: z.string(),
+ text: z.string(),
+ isCreatedByUser: z.boolean(),
+ error: z.boolean(),
+ createdAt: z.string(),
+ updatedAt: z.string(),
+});
+
+export type TMessage = z.infer;
+
+export const tPluginAuthConfigSchema = z.object({
+ authField: z.string(),
+ label: z.string(),
+ description: z.string(),
+});
+
+export type TPluginAuthConfig = z.infer;
+
+export const tPluginSchema = z.object({
+ name: z.string(),
+ pluginKey: z.string(),
+ description: z.string(),
+ icon: z.string(),
+ authConfig: z.array(tPluginAuthConfigSchema),
+ authenticated: z.boolean().optional(),
+ isButton: z.boolean().optional(),
+});
+
+export type TPlugin = z.infer;
+
+export const tExampleSchema = z.object({
+ input: z.object({
+ content: z.string(),
+ }),
+ output: z.object({
+ content: z.string(),
+ }),
+});
+
+export type TExample = z.infer;
+
+export const tAgentOptionsSchema = z.object({
+ agent: z.string(),
+ skipCompletion: z.boolean(),
+ model: z.string(),
+ temperature: z.number(),
+});
+
+export const tConversationSchema = z.object({
+ conversationId: z.string().nullable(),
+ title: z.string(),
+ user: z.string().optional(),
+ endpoint: eModelEndpointSchema.nullable(),
+ suggestions: z.array(z.string()).optional(),
+ messages: z.array(z.string()).optional(),
+ tools: z.array(tPluginSchema).optional(),
+ createdAt: z.string(),
+ updatedAt: z.string(),
+ systemMessage: z.string().nullable().optional(),
+ modelLabel: z.string().nullable().optional(),
+ examples: z.array(tExampleSchema).optional(),
+ chatGptLabel: z.string().nullable().optional(),
+ userLabel: z.string().optional(),
+ model: z.string().optional(),
+ promptPrefix: z.string().nullable().optional(),
+ temperature: z.number().optional(),
+ topP: z.number().optional(),
+ topK: z.number().optional(),
+ context: z.string().nullable().optional(),
+ top_p: z.number().optional(),
+ frequency_penalty: z.number().optional(),
+ presence_penalty: z.number().optional(),
+ jailbreak: z.boolean().optional(),
+ jailbreakConversationId: z.string().nullable().optional(),
+ conversationSignature: z.string().nullable().optional(),
+ parentMessageId: z.string().optional(),
+ clientId: z.string().nullable().optional(),
+ invocationId: z.number().nullable().optional(),
+ toneStyle: z.string().nullable().optional(),
+ maxOutputTokens: z.number().optional(),
+ agentOptions: tAgentOptionsSchema.nullable().optional(),
+});
+
+export type TConversation = z.infer;
+
+export const tPresetSchema = tConversationSchema
+ .omit({
+ conversationId: true,
+ createdAt: true,
+ updatedAt: true,
+ title: true,
+ })
+ .merge(
+ z.object({
+ conversationId: z.string().optional(),
+ presetId: z.string().nullable().optional(),
+ title: z.string().nullable().optional(),
+ }),
+ );
+
+export type TPreset = z.infer;
diff --git a/packages/data-provider/src/types.ts b/packages/data-provider/src/types.ts
index 23eac7f1f5..95f4496fcb 100644
--- a/packages/data-provider/src/types.ts
+++ b/packages/data-provider/src/types.ts
@@ -1,42 +1,12 @@
import * as React from 'react';
+import { TExample, TMessage, EModelEndpoint, TPlugin, TConversation, TPreset } from './schemas';
-export type TMessage = {
- messageId: string;
- conversationId: string;
- clientId: string;
- parentMessageId: string;
- sender: string;
- text: string;
- isCreatedByUser: boolean;
- error: boolean;
- createdAt: string;
- updatedAt: string;
-};
+export * from './schemas';
export type TMessages = TMessage[];
export type TMessagesAtom = TMessages | null;
-export type TExample = {
- input: {
- content: string;
- };
- output: {
- content: string;
- };
-};
-
-export enum EModelEndpoint {
- azureOpenAI = 'azureOpenAI',
- openAI = 'openAI',
- bingAI = 'bingAI',
- chatGPT = 'chatGPT',
- chatGPTBrowser = 'chatGPTBrowser',
- google = 'google',
- gptPlugins = 'gptPlugins',
- anthropic = 'anthropic',
-}
-
export type TSubmission = {
clientId?: string;
context?: string;
@@ -73,22 +43,6 @@ export type TEndpointOption = {
temperature?: number;
};
-export type TPluginAuthConfig = {
- authField: string;
- label: string;
- description: string;
-};
-
-export type TPlugin = {
- name: string;
- pluginKey: string;
- description: string;
- icon: string;
- authConfig: TPluginAuthConfig[];
- authenticated: boolean;
- isButton?: boolean;
-};
-
export type TPluginAction = {
pluginKey: string;
action: 'install' | 'uninstall';
@@ -105,91 +59,6 @@ export type TUpdateUserPlugins = {
auth?: unknown;
};
-export type TAgentOptions = {
- agent: string;
- skipCompletion: boolean;
- model: string;
- temperature: number;
-};
-
-export type TConversation = {
- conversationId: string | null;
- title: string;
- user?: string;
- endpoint: EModelEndpoint | null;
- suggestions?: string[];
- messages?: TMessage[];
- tools?: TPlugin[];
- createdAt: string;
- updatedAt: string;
- // google only
- systemMessage?: string;
- modelLabel?: string;
- examples?: TExample[];
- // for azureOpenAI, openAI only
- chatGptLabel?: string;
- userLabel?: string;
- model?: string;
- promptPrefix?: string;
- temperature?: number;
- topP?: number;
- topK?: number;
- // bing and google
- context?: string;
- top_p?: number;
- frequency_penalty?: number;
- presence_penalty?: number;
- // for bingAI only
- jailbreak?: boolean;
- jailbreakConversationId?: string;
- conversationSignature?: string;
- parentMessageId?: string;
- clientId?: string;
- invocationId?: string;
- toneStyle?: string;
- maxOutputTokens?: number;
- // plugins only
- agentOptions?: TAgentOptions;
-};
-
-export type TPreset = {
- title: string;
- conversationId?: string;
- endpoint: EModelEndpoint | null;
- conversationSignature?: string;
- createdAt?: string;
- updatedAt?: string;
- presetId?: string;
- tools?: TPlugin[];
- user?: string;
- modelLabel?: string;
- maxOutputTokens?: number;
- topP?: number;
- topK?: number;
- context?: string;
- systemMessage?: string;
- // for azureOpenAI, openAI only
- chatGptLabel?: string;
- frequence_penalty?: number;
- model?: string;
- presence_penalty?: number;
- frequency_penalty?: number;
- promptPrefix?: string;
- temperature?: number;
- top_p?: number;
- //for BingAI
- clientId?: string;
- invocationId?: number;
- jailbreak?: boolean;
- jailbreakPresetId?: string;
- presetSignature?: string;
- toneStyle?: string;
- // plugins only
- agentOptions?: TAgentOptions;
- // google only
- examples?: TExample[];
-};
-
export type TOptionSettings = {
showExamples?: boolean;
isCodeChat?: boolean;