mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-21 10:50:14 +01:00
refactor: Settings/Presets UI Restructure, convert many files to TS (#740)
* progress on settings refactor * fix(helpers.js): replace fs.rmdirSync with fs.rm to delete node_modules directory recursively fix(packages.js): delete package-lock.json if it exists before running the script * feat(CrossIcon.tsx): add CrossIcon component * wip: refactor Options for modularity into higher order components, OptionsBar > ModelSelect/Settings * refactor: import more from utils/index, including cardStyle used by model select/settings * refactor(AnthropicOptions): refactor to new format, OpenAI: reduce format to name of endpoint * refactor(AnthropicSettings): refactor to new format, match defaults to API docs * fix: google and anthropic defaults * refactor(conversation/submission atoms): add typing, remove unused code * chore(types.ts): add missing type definitions for TMessages, TMessagesAtom, TConversationAtom, and ModelSelectProps feat(types.ts): make endpoint property nullable in TSubmission, TEndpointOption, TConversation, and TPreset types * refactor(ChatGPT): refactor to new format, add omit settings logic * refactor(EndpointSettings/BingAI): new dir structure and format BingAI options/settings to new * fix: update useUpdateTokenCountMutation to accept an object with a 'text' property instead of a string * fix(endpoints): ensure expected behaviors for preset dialogs * chore(index.ts): add defaultTextProps to utils/index.ts for use in settings components * chore(index.ts): add optionText to utils/index.ts for use in settings components * wip: refactor google settings * wip: progress with Google refactor, needs AdditionalButtons handling and global state setters * refactor(OptionsBar.tsx): The setOption function has been refactored to use the useSetOptions custom hook for setting conversation options. * chore(Anthropic.tsx, BingAI.tsx, Google.tsx, OpenAI.tsx): adjust height of container div in Settings component; chore(Examples.tsx): adjust height in Examples component * refactor(Google): complete google refactor feat(client): add new component PopoverButtons for displaying popover buttons in EndpointPopover feat(data-provider): add types for PopoverButton and EndpointOptionsPopoverProps * fix(OptionsBar.tsx): add useEffect hook to handle opacity class based on messagesTree and advancedMode fix(style.css): rename class from 'openAIOptions-simple-container' to 'options-bar' and update references * refactor(Plugins/OptionsBar): complete refactor of Plugins Select options, consolidate logic from TextChat to OptionsBar * fix(Plugins.tsx): filter lastSelectedTools to remove any tools that are not in the current tools list fix(useSetOptions.ts): remove unnecessary empty line * feat(useSetOptions.ts): add setAgentOption function to update agentOptions in conversation state feat(types.ts): add setAgentOption function to UseSetOptions type * refactor(Settings/Plugins): refactor to new format, refactor(OptionHover): use same component for all endpoints * refactor(OptionHover.tsx): refactor types object to use nested objects for openAI and gptPlugins feat(OptionHover.tsx): add openAI object with specific properties for openAI configuration * refactor(AgentSettings): new format, feat(types.ts): add TAgentOptions type for defining agent options in a conversation * feat(PopoverButtons.tsx): add support for GPT plugin settings button feat(Plugins.tsx): create PluginsView component for displaying plugin settings feat(optionSettings.ts): add showAgentSettings atom for controlling agent settings visibility * feat(client): add support for PluginsSettings in Input/Settings component fix(client): change import path for PluginsSettings in Input/Settings component * refactor(Settings/Plugins): complete refactor, store: refactor to TS, refactor: import defaultTextPropsLabel from utils * feat(EndpointSettings, AgentSettings, Anthropic, Google, types.ts): Add support for Recoil state management and useRecoilValue hook; Pass models from endpointsConfig to various components; Add TModels type and update ModelSelectProps type. fix(AgentSettings, Anthropic, Google, GoogleView, Plugins, OpenAI, Settings.tsx): Change import statements for ModelSelectProps from librechat-data-provider; Add models as a parameter to various components; Add models prop to PluginsView, Settings, and other components. * refactor(EditPresetDialog.jsx): update import statements for Examples and AgentSettings components feat(Settings/index.ts): add export statements for Examples and AgentSettings components * chore(package.json): update eslint-plugin-import to version 2.28.0 * fix(eslint): dependency cycle rule is now working * fix: dependency cycle errors and type errors * refactor(EditPresetDialog.jsx): update import path for DialogTemplate component refactor(NewConversationMenu/index.jsx): update import path for DialogTemplate component refactor(ExportModel.jsx): update import path for DialogTemplate component * refactor: rename NewConversationMenu to EndpointMenu * style: mobile and desktop optimizations * chore: eslint changes * chore(eslintrc.js): update eslint configuration to use 'prettier' plugin chore(postcss.config.cjs): update postcss configuration to use single quotes for require statements fix(helpers.js): fix fs.rmSync function call to delete node_modules directory recursively feat(update.js): add support for skipping git commands with '-g' flag * chore(ModelSelect.tsx): add support for azureOpenAI option component chore(Settings.tsx): add support for azureOpenAI option component chore(package.json): add rebuild:package-lock and update:branch scripts * fix(OptionHover.tsx): fix accessing nested properties in types object feat(OptionHover.tsx): add check for existence of text before rendering HoverCardContent * chore(style.css): update transition duration for options-bar from 0.3s to 0.25s * fix(ScrollToBottom.jsx): fix z-index value for scroll button * style: improve dialogs * fix(Nav.jsx): adjust width and max-width of nav component * chore(Nav.jsx): update max-width class for nav component in different screen sizes chore(Dialog.tsx): update class for DialogFooter component to use flex-row layout * fix(client): fix node_module resolution with path mapping * fix(AdjustToneButton.jsx): add z-index to adjust tone button for proper layering fix(TextChat.jsx): change onClick function to use arrow function to avoid immediate execution fix(mobile.css): update z-index for nav and nav-mask for proper layering chore(package.json): rename update:branch script to reinstall for clarity and consistency * fix(OptionsBar/Settings): add null checks for conversation in BingAI.tsx, ChatGPT.tsx, Plugins.tsx, Settings.tsx * style(TextChat/OptionsBar): match official site styles, setup regen/continue/stop buttons div * chore: Import and apply removeFocusOutlines utility across various components, and rename removeButtonOutline to removeFocusOutlines chore(Settings): Remove unused import and conditionally return null if conversation is falsy * feat(hooks): add useLocalize hook The useLocalize hook is added to the hooks/index.ts file. This hook allows for localization of phrases using the localize function from the ~/localization/Translation module. The hook uses the lang value from the store to determine the current language and returns a function that takes a phraseKey and optional values array as arguments and returns the localized phrase. * refactor(OptionHover.tsx): Update text keys for OptionHover component, use new hook: useLocalize * refactor(useDocumentTitle.ts): refactor to TS * fix(typescript): type issues and update typescript linting deps * refactor: Update ThemeContext and useOnClickOutside to TypeScript chore(useDidMountEffect.js): Remove useDidMountEffect hook * feat: GenerationButtons for stop/continue/regen, remove AdjustToneButton in favor of alternate advanced mode/Settings in OptionsBar * fix(EndpointOptionsPopover.tsx): change switchToSimpleMode function name to closePopover fix(GenerationButtons.tsx): change advancedMode prop name to showPopover fix(OptionsBar.tsx): change advancedMode state name to showPopover feat(OptionsBar.tsx): add logic to show/hide popover based on showPopover state fix(types.ts): change switchToSimpleMode function name to closePopover * chore: remove template button * chore(GenerationButtons.tsx): adjust positioning of the div element chore(Plugins.tsx): adjust width of the MultiSelectDropDown component chore(OptionsBar.tsx): adjust padding of the button element * refactor(EditPresetDialog): use new modular higher order components * chore(newoptionsbar.html): delete unused file newoptionsbar.html * refactor(EditPresetDialog): convert to TS * chore(babel.config.cjs): update babel configuration, linting * chore(EditPresetDialog.tsx): update className for DialogTemplate to include pb-0 chore(EndpointOptionsDialog.jsx): update className for DialogTemplate to include pb-0 chore(PopoverButtons.tsx): add buttonClass prop to PopoverButtons component chore(DialogTemplate.tsx): update className for the footer div to include h-auto chore(Dropdown.jsx): remove id prop from Dropdown component chore(mobile.css): update transition duration for .nav class from 0.2s to 0.15s * refactor(EditPresetDialog.tsx): simplify localization usage with hook * chore(EditPresetDialog.tsx): update containerClassName to include z-index value * fix(endpoints.ts): change type of endpointsConfig atom to TEndpointsConfig refactor(cleanupPreset.ts): convert to TS fix(index.ts): export cleanupPreset utility function fix(types.ts): add missing properties to TPreset type * refactor(EndpointOptionsDialog): convert to TS * fix(EditPresetDialog.tsx): - import cleanupPreset from index - add null check before submitting preset - add null check before exporting preset refactor(SaveAsPresetDialog.tsx): convert to TS fix(usePresetOptions.ts): import cleanupPreset from index fix(types.ts): - make title prop optional in EditPresetProps - change preset prop in CleanupPreset to be partial * chore: reorganize imports in App, EndpointMenu, Messages, and ExportModel components feat(ScreenshotContext.jsx): add ScreenshotContext to hooks/index chore(index.ts): export ThemeContext, ScreenshotContext, ApiErrorBoundaryContext hooks, cleanupPreset, and getIcon functions from utils * wip: add headerClassName for dialog template * chore(EndpointOptionsDialog.tsx): remove unused headerClassName prop chore(EndpointOptionsDialog.tsx): adjust height of main container in mobile and desktop view * fix(react-query-service.ts): change return type of useGetEndpointsQuery to QueryObserverResult<t.TEndpointsConfig> * refactor: imports from index and refactor to TS * refactor: refactor all svg components to TS * refactor: refactor all UI components to TS, remove unused component * fix(SelectDropDown.tsx): remove file extension from import statement for CheckMark component * fix: SaveAsPresetDialog typing issue * fix(OptionsBar): close popover when an endpoint with no settings is selected * chore(ChatGPT.tsx): update width of model select dropdown to 60px refactor(types.ts): decouple ModelSelectProps from SettingsProps * fix(popover Settings): space taken from the options menu for each endpoint * fix:'Set token first' element alignment, add padding to endpointmenu icon in mobile * style: match official site header * refactor(EndpointOptionsDialog): make functionality explicitly saving current convos as presets * fix(useLocalize.ts): change values parameter from an array to rest parameters * refactor(EndpointSettings): Utilize useLocalize hook for all endpoint settings * fix(Popover): correct spacing/center and remove focus outlines for close button * chore: employ use of cn (clsx) in Popover styles * chore(EditPresetDialog.tsx): update className to add padding bottom chore(EndpointOptionsDialog.tsx): update className to add padding bottom * style(EndpointMenu, TextChat): add better styling at diff. breakpoints * refactor(EndpointSettings): consolidate container style to higher order component * refactor(EditPresetDialog.tsx): pass custom style to Settings from here * style: setting dialogs improved in all views * style(EndpointMenu): improve UX for mobile * style(PresetDialog): increase height so scrollbar isn't triggered * chore(EditPresetDialog.tsx): update className to include xl height for DialogTemplate chore(InputNumber.tsx): update className to include max height for InputNumber component * fix: light mode styling * fix(OptionsBar/ScrollToBottom/Popover): quick fix to rework in future: hide scrollToBottom when Popover is open * style: remove bg-gradient around textarea in mobile view * chore(ThemeContext.tsx): refactor ThemeContext to use default context value, also fixes type issue * chore(EditPresetDialog.tsx): adjust grid layout in EditPresetDialog component * style(TextChat): make gradient more opaque/smoother * fix(TextChat.jsx): fix background gradient color based on theme and system preference * test(layout-test-utils.tsx): add mock implementation for window.matchMedia in test setup feat(layout-test-utils.tsx): add authConfig prop to AuthContextProvider in renderWithProvidersWrapper function chore(tsconfig.json): include test directory in tsconfig include section * chore(jest.config.cjs): update test file paths in jest configuration chore(Login.spec.tsx): update test file path in import statement chore(LoginForm.spec.tsx): update test file path in import statement chore(Registration.spec.tsx): update test file path in import statement chore(PluginAuthForm.spec.tsx): update test file path in import statement chore(PluginStoreDialog.spec.tsx): update test file path in import statement chore(layout-test-utils.tsx): move matchMedia mock to separate file chore(tsconfig.json): add path mapping for test files in client directory * test: add import for 'test/matchMedia.mock' in test files The changes in this commit add an import statement for 'test/matchMedia.mock' in multiple test files. This import is necessary for mocking the behavior of the matchMedia function during testing. * style(ClearConvosDialog): remove borders from button and modal, uniform button size * fix(AgentSettings.tsx): overlapping issue * fix(PresetDialogs): improve spacing of top row and dialog content * style(Settings): 2nd column will now dynamically adjust better across all screen sizes * style(ModelSelect): improve styling for mobile/desktop, add hover shadow feat(ModelSelect/Plugins): hide ModelSelect when screen is small * refactor(RowButton, buildTree): convert to TS * style(ModelSelect): add transition effect to shadows on hover
This commit is contained in:
parent
fb99e5a7da
commit
956aa6c674
203 changed files with 5062 additions and 4327 deletions
|
|
@ -1,22 +1,22 @@
|
|||
import React, { useState } from 'react';
|
||||
import { TError } from 'librechat-data-provider';
|
||||
|
||||
export type ApiError = {
|
||||
error: any;
|
||||
setError: (error: any) => void;
|
||||
type ProviderValue = {
|
||||
error: TError;
|
||||
setError: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
const ApiErrorBoundaryContext = React.createContext<ApiError | undefined>(undefined);
|
||||
const ApiErrorBoundaryContext = React.createContext<ProviderValue | undefined>(undefined);
|
||||
|
||||
export const ApiErrorBoundaryProvider = ({
|
||||
value,
|
||||
children,
|
||||
}: {
|
||||
value?: ApiError;
|
||||
value: ProviderValue;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const [error, setError] = useState(false);
|
||||
return (
|
||||
<ApiErrorBoundaryContext.Provider value={value ? value : { error, setError }}>
|
||||
<ApiErrorBoundaryContext.Provider value={value ?? { error, setError }}>
|
||||
{children}
|
||||
</ApiErrorBoundaryContext.Provider>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ const AuthContextProvider = ({
|
|||
);
|
||||
|
||||
const getCookieValue = (key: string) => {
|
||||
let keyValue = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)');
|
||||
const keyValue = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)');
|
||||
return keyValue ? keyValue[2] : null;
|
||||
};
|
||||
|
||||
|
|
|
|||
44
client/src/hooks/ScreenshotContext.jsx
Normal file
44
client/src/hooks/ScreenshotContext.jsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { createContext, useRef, useContext } from 'react';
|
||||
import html2canvas from 'html2canvas';
|
||||
|
||||
const ScreenshotContext = createContext({});
|
||||
|
||||
export const useScreenshot = () => {
|
||||
const { ref } = useContext(ScreenshotContext);
|
||||
|
||||
const takeScreenShot = (node) => {
|
||||
if (!node) {
|
||||
throw new Error('You should provide correct html node.');
|
||||
}
|
||||
return html2canvas(node).then((canvas) => {
|
||||
const croppedCanvas = document.createElement('canvas');
|
||||
const croppedCanvasContext = croppedCanvas.getContext('2d');
|
||||
// init data
|
||||
const cropPositionTop = 0;
|
||||
const cropPositionLeft = 0;
|
||||
const cropWidth = canvas.width;
|
||||
const cropHeight = canvas.height;
|
||||
|
||||
croppedCanvas.width = cropWidth;
|
||||
croppedCanvas.height = cropHeight;
|
||||
|
||||
croppedCanvasContext.drawImage(canvas, cropPositionLeft, cropPositionTop);
|
||||
|
||||
const base64Image = croppedCanvas.toDataURL('image/png', 1);
|
||||
|
||||
return base64Image;
|
||||
});
|
||||
};
|
||||
|
||||
const captureScreenshot = () => {
|
||||
return takeScreenShot(ref.current);
|
||||
};
|
||||
|
||||
return { screenshotTargetRef: ref, captureScreenshot };
|
||||
};
|
||||
|
||||
export const ScreenshotProvider = ({ children }) => {
|
||||
const ref = useRef(null);
|
||||
|
||||
return <ScreenshotContext.Provider value={{ ref }}>{children}</ScreenshotContext.Provider>;
|
||||
};
|
||||
|
|
@ -19,12 +19,23 @@ const getInitialTheme = () => {
|
|||
return 'light'; // light theme as the default;
|
||||
};
|
||||
|
||||
export const ThemeContext = createContext();
|
||||
type ProviderValue = {
|
||||
theme: string;
|
||||
setTheme: React.Dispatch<React.SetStateAction<string>>;
|
||||
};
|
||||
|
||||
const defaultContextValue: ProviderValue = {
|
||||
theme: getInitialTheme(),
|
||||
setTheme: () => {
|
||||
return;
|
||||
},
|
||||
};
|
||||
export const ThemeContext = createContext<ProviderValue>(defaultContextValue);
|
||||
|
||||
export const ThemeProvider = ({ initialTheme, children }) => {
|
||||
const [theme, setTheme] = useState(getInitialTheme);
|
||||
|
||||
const rawSetTheme = (rawTheme) => {
|
||||
const rawSetTheme = (rawTheme: string) => {
|
||||
const root = window.document.documentElement;
|
||||
let isDark = rawTheme === 'dark';
|
||||
|
||||
|
|
@ -1,2 +1,9 @@
|
|||
export * from './AuthContext';
|
||||
export * from './ThemeContext';
|
||||
export * from './ScreenshotContext';
|
||||
export * from './ApiErrorBoundaryContext';
|
||||
export { default as useDebounce } from './useDebounce';
|
||||
export { default as useLocalize } from './useLocalize';
|
||||
export { default as useMediaQuery } from './useMediaQuery';
|
||||
export { default as useSetOptions } from './useSetOptions';
|
||||
export { default as useMessageHandler } from './useMessageHandler';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
|
||||
function useDebounce(value: number, delay: number) {
|
||||
function useDebounce(value: string, delay: number) {
|
||||
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
import { useEffect, useRef } from 'react';
|
||||
|
||||
const useDidMountEffect = (func, deps) => {
|
||||
const didMount = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (didMount.current) {
|
||||
func();
|
||||
} else {
|
||||
didMount.current = true;
|
||||
}
|
||||
|
||||
return func;
|
||||
}, deps);
|
||||
};
|
||||
|
||||
export default useDidMountEffect;
|
||||
|
|
@ -3,7 +3,7 @@ import { useEffect } from 'react';
|
|||
|
||||
// function useDocumentTitle(title, prevailOnUnmount = false) {
|
||||
// const defaultTitle = useRef(document.title);
|
||||
function useDocumentTitle(title) {
|
||||
function useDocumentTitle(title: string) {
|
||||
useEffect(() => {
|
||||
document.title = title;
|
||||
}, [title]);
|
||||
8
client/src/hooks/useLocalize.ts
Normal file
8
client/src/hooks/useLocalize.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { useRecoilValue } from 'recoil';
|
||||
import { localize } from '~/localization/Translation';
|
||||
import store from '~/store';
|
||||
|
||||
export default function useLocalize() {
|
||||
const lang = useRecoilValue(store.lang);
|
||||
return (phraseKey: string, ...values: string[]) => localize(lang, phraseKey, ...(values ?? []));
|
||||
}
|
||||
17
client/src/hooks/useMediaQuery.tsx
Normal file
17
client/src/hooks/useMediaQuery.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
|
||||
export default function useMediaQuery(query: string) {
|
||||
const [matches, setMatches] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const media = window.matchMedia(query);
|
||||
if (media.matches !== matches) {
|
||||
setMatches(media.matches);
|
||||
}
|
||||
const listener = () => setMatches(media.matches);
|
||||
media.addEventListener('change', listener);
|
||||
return () => media.removeEventListener('change', listener);
|
||||
}, [matches, query]);
|
||||
|
||||
return matches;
|
||||
}
|
||||
219
client/src/hooks/useMessageHandler.js
Normal file
219
client/src/hooks/useMessageHandler.js
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
import { v4 } from 'uuid';
|
||||
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import store from '~/store';
|
||||
|
||||
const useMessageHandler = () => {
|
||||
const currentConversation = useRecoilValue(store.conversation) || {};
|
||||
const setSubmission = useSetRecoilState(store.submission);
|
||||
const isSubmitting = useRecoilValue(store.isSubmitting);
|
||||
const endpointsConfig = useRecoilValue(store.endpointsConfig);
|
||||
|
||||
const { getToken } = store.useToken(currentConversation?.endpoint);
|
||||
|
||||
const latestMessage = useRecoilValue(store.latestMessage);
|
||||
|
||||
const [messages, setMessages] = useRecoilState(store.messages);
|
||||
|
||||
const ask = (
|
||||
{ text, parentMessageId = null, conversationId = null, messageId = null },
|
||||
{ isRegenerate = false } = {},
|
||||
) => {
|
||||
if (!!isSubmitting || text === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
// determine the model to be used
|
||||
const { endpoint } = currentConversation;
|
||||
let endpointOption = {};
|
||||
let responseSender = '';
|
||||
if (endpoint === 'azureOpenAI' || endpoint === 'openAI') {
|
||||
endpointOption = {
|
||||
endpoint,
|
||||
model:
|
||||
currentConversation?.model ??
|
||||
endpointsConfig[endpoint]?.availableModels?.[0] ??
|
||||
'gpt-3.5-turbo',
|
||||
chatGptLabel: currentConversation?.chatGptLabel ?? null,
|
||||
promptPrefix: currentConversation?.promptPrefix ?? null,
|
||||
temperature: currentConversation?.temperature ?? 1,
|
||||
top_p: currentConversation?.top_p ?? 1,
|
||||
presence_penalty: currentConversation?.presence_penalty ?? 0,
|
||||
frequency_penalty: currentConversation?.frequency_penalty ?? 0,
|
||||
token: endpointsConfig[endpoint]?.userProvide ? getToken() : null,
|
||||
};
|
||||
responseSender = endpointOption.chatGptLabel ?? 'ChatGPT';
|
||||
} else if (endpoint === 'google') {
|
||||
endpointOption = {
|
||||
endpoint,
|
||||
model:
|
||||
currentConversation?.model ??
|
||||
endpointsConfig[endpoint]?.availableModels?.[0] ??
|
||||
'chat-bison',
|
||||
modelLabel: currentConversation?.modelLabel ?? null,
|
||||
promptPrefix: currentConversation?.promptPrefix ?? null,
|
||||
examples: currentConversation?.examples ?? [
|
||||
{ input: { content: '' }, output: { content: '' } },
|
||||
],
|
||||
temperature: currentConversation?.temperature ?? 0.2,
|
||||
maxOutputTokens: currentConversation?.maxOutputTokens ?? 1024,
|
||||
topP: currentConversation?.topP ?? 0.95,
|
||||
topK: currentConversation?.topK ?? 40,
|
||||
token: endpointsConfig[endpoint]?.userProvide ? getToken() : null,
|
||||
};
|
||||
responseSender = endpointOption.chatGptLabel ?? 'ChatGPT';
|
||||
} else if (endpoint === 'bingAI') {
|
||||
endpointOption = {
|
||||
endpoint,
|
||||
jailbreak: currentConversation?.jailbreak ?? false,
|
||||
systemMessage: currentConversation?.systemMessage ?? null,
|
||||
context: currentConversation?.context ?? null,
|
||||
toneStyle: currentConversation?.toneStyle ?? 'creative',
|
||||
jailbreakConversationId: currentConversation?.jailbreakConversationId ?? null,
|
||||
conversationSignature: currentConversation?.conversationSignature ?? null,
|
||||
clientId: currentConversation?.clientId ?? null,
|
||||
invocationId: currentConversation?.invocationId ?? 1,
|
||||
token: endpointsConfig[endpoint]?.userProvide ? getToken() : null,
|
||||
};
|
||||
responseSender = endpointOption.jailbreak ? 'Sydney' : 'BingAI';
|
||||
} else if (endpoint === 'anthropic') {
|
||||
endpointOption = {
|
||||
endpoint,
|
||||
model:
|
||||
currentConversation?.model ??
|
||||
endpointsConfig[endpoint]?.availableModels?.[0] ??
|
||||
'claude-1',
|
||||
modelLabel: currentConversation?.modelLabel ?? null,
|
||||
promptPrefix: currentConversation?.promptPrefix ?? null,
|
||||
temperature: currentConversation?.temperature ?? 1,
|
||||
maxOutputTokens: currentConversation?.maxOutputTokens ?? 1024,
|
||||
topP: currentConversation?.topP ?? 0.7,
|
||||
topK: currentConversation?.topK ?? 5,
|
||||
token: endpointsConfig[endpoint]?.userProvide ? getToken() : null,
|
||||
};
|
||||
responseSender = 'Anthropic';
|
||||
} else if (endpoint === 'chatGPTBrowser') {
|
||||
endpointOption = {
|
||||
endpoint,
|
||||
model:
|
||||
currentConversation?.model ??
|
||||
endpointsConfig[endpoint]?.availableModels?.[0] ??
|
||||
'text-davinci-002-render-sha',
|
||||
token: endpointsConfig[endpoint]?.userProvide ? getToken() : null,
|
||||
};
|
||||
responseSender = 'ChatGPT';
|
||||
} else if (endpoint === 'gptPlugins') {
|
||||
const agentOptions = currentConversation?.agentOptions ?? {
|
||||
agent: 'functions',
|
||||
skipCompletion: true,
|
||||
model: 'gpt-3.5-turbo',
|
||||
temperature: 0,
|
||||
};
|
||||
endpointOption = {
|
||||
endpoint,
|
||||
tools: currentConversation?.tools ?? [],
|
||||
model:
|
||||
currentConversation?.model ??
|
||||
endpointsConfig[endpoint]?.availableModels?.[0] ??
|
||||
'gpt-3.5-turbo',
|
||||
chatGptLabel: currentConversation?.chatGptLabel ?? null,
|
||||
promptPrefix: currentConversation?.promptPrefix ?? null,
|
||||
temperature: currentConversation?.temperature ?? 0.8,
|
||||
top_p: currentConversation?.top_p ?? 1,
|
||||
presence_penalty: currentConversation?.presence_penalty ?? 0,
|
||||
frequency_penalty: currentConversation?.frequency_penalty ?? 0,
|
||||
token: endpointsConfig[endpoint]?.userProvide ? getToken() : null,
|
||||
agentOptions,
|
||||
};
|
||||
responseSender = 'ChatGPT';
|
||||
} else if (endpoint === null) {
|
||||
console.error('No endpoint available');
|
||||
return;
|
||||
} else {
|
||||
console.error(`Unknown endpoint ${endpoint}`);
|
||||
return;
|
||||
}
|
||||
|
||||
let currentMessages = messages;
|
||||
|
||||
// construct the query message
|
||||
// this is not a real messageId, it is used as placeholder before real messageId returned
|
||||
text = text.trim();
|
||||
const fakeMessageId = v4();
|
||||
parentMessageId =
|
||||
parentMessageId || latestMessage?.messageId || '00000000-0000-0000-0000-000000000000';
|
||||
conversationId = conversationId || currentConversation?.conversationId;
|
||||
if (conversationId == 'search') {
|
||||
console.error('cannot send any message under search view!');
|
||||
return;
|
||||
}
|
||||
if (conversationId == 'new') {
|
||||
parentMessageId = '00000000-0000-0000-0000-000000000000';
|
||||
currentMessages = [];
|
||||
conversationId = null;
|
||||
}
|
||||
const currentMsg = {
|
||||
sender: 'User',
|
||||
text,
|
||||
current: true,
|
||||
isCreatedByUser: true,
|
||||
parentMessageId,
|
||||
conversationId,
|
||||
messageId: fakeMessageId,
|
||||
};
|
||||
|
||||
// construct the placeholder response message
|
||||
const initialResponse = {
|
||||
sender: responseSender,
|
||||
text: '<span className="result-streaming">█</span>',
|
||||
parentMessageId: isRegenerate ? messageId : fakeMessageId,
|
||||
messageId: (isRegenerate ? messageId : fakeMessageId) + '_',
|
||||
conversationId,
|
||||
unfinished: false,
|
||||
submitting: true,
|
||||
};
|
||||
|
||||
const submission = {
|
||||
conversation: {
|
||||
...currentConversation,
|
||||
conversationId,
|
||||
},
|
||||
endpointOption,
|
||||
message: {
|
||||
...currentMsg,
|
||||
overrideParentMessageId: isRegenerate ? messageId : null,
|
||||
},
|
||||
messages: currentMessages,
|
||||
isRegenerate,
|
||||
initialResponse,
|
||||
};
|
||||
|
||||
console.log('User Input:', text, submission);
|
||||
|
||||
if (isRegenerate) {
|
||||
setMessages([...currentMessages, initialResponse]);
|
||||
} else {
|
||||
setMessages([...currentMessages, currentMsg, initialResponse]);
|
||||
}
|
||||
setSubmission(submission);
|
||||
};
|
||||
|
||||
const regenerate = ({ parentMessageId }) => {
|
||||
const parentMessage = messages?.find((element) => element.messageId == parentMessageId);
|
||||
|
||||
if (parentMessage && parentMessage.isCreatedByUser) {
|
||||
ask({ ...parentMessage }, { isRegenerate: true });
|
||||
} else {
|
||||
console.error(
|
||||
'Failed to regenerate the message: parentMessage not found or not created by user.',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const stopGenerating = () => {
|
||||
setSubmission(null);
|
||||
};
|
||||
|
||||
return { ask, regenerate, stopGenerating };
|
||||
};
|
||||
|
||||
export default useMessageHandler;
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
import { useEffect } from 'react';
|
||||
|
||||
export default function useOnClickOutside(ref, handler, excludeIds) {
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (excludeIds.includes(event.target.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ref.current && !ref.current.contains(event.target)) {
|
||||
handler();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ref, handler]);
|
||||
}
|
||||
27
client/src/hooks/useOnClickOutside.ts
Normal file
27
client/src/hooks/useOnClickOutside.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { useEffect, RefObject } from 'react';
|
||||
type Handler = () => void;
|
||||
|
||||
export default function useOnClickOutside(
|
||||
ref: RefObject<HTMLElement>,
|
||||
handler: Handler,
|
||||
excludeIds: string[],
|
||||
): void {
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
const target = event.target as Node | null;
|
||||
if (target && 'id' in target && excludeIds.includes((target as HTMLElement).id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ref.current && !ref.current.contains(target)) {
|
||||
handler();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ref, handler]);
|
||||
}
|
||||
128
client/src/hooks/usePresetOptions.ts
Normal file
128
client/src/hooks/usePresetOptions.ts
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
import { UsePresetOptions, TPreset, SetOption, SetExample } from 'librechat-data-provider';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { cleanupPreset } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
const usePresetOptions: UsePresetOptions = (_preset) => {
|
||||
const [preset, setPreset] = useRecoilState(store.preset);
|
||||
const endpointsConfig = useRecoilValue(store.endpointsConfig);
|
||||
if (!_preset) {
|
||||
return false;
|
||||
}
|
||||
const getConversation: () => TPreset | null = () => preset;
|
||||
const setOption: SetOption = (param) => (newValue) => {
|
||||
const update = {};
|
||||
update[param] = newValue;
|
||||
setPreset(
|
||||
(prevState) =>
|
||||
cleanupPreset({
|
||||
preset: {
|
||||
...prevState,
|
||||
...update,
|
||||
},
|
||||
endpointsConfig,
|
||||
}) as TPreset,
|
||||
);
|
||||
};
|
||||
|
||||
const setExample: SetExample = (i, type, newValue = null) => {
|
||||
const update = {};
|
||||
const current = preset?.examples?.slice() || [];
|
||||
const currentExample = { ...current[i] } || {};
|
||||
currentExample[type] = { content: newValue };
|
||||
current[i] = currentExample;
|
||||
update['examples'] = current;
|
||||
setPreset(
|
||||
(prevState) =>
|
||||
cleanupPreset({
|
||||
preset: {
|
||||
...prevState,
|
||||
...update,
|
||||
},
|
||||
endpointsConfig,
|
||||
}) as TPreset,
|
||||
);
|
||||
};
|
||||
|
||||
const addExample: () => void = () => {
|
||||
const update = {};
|
||||
const current = preset?.examples?.slice() || [];
|
||||
current.push({ input: { content: '' }, output: { content: '' } });
|
||||
update['examples'] = current;
|
||||
setPreset(
|
||||
(prevState) =>
|
||||
cleanupPreset({
|
||||
preset: {
|
||||
...prevState,
|
||||
...update,
|
||||
},
|
||||
endpointsConfig,
|
||||
}) as TPreset,
|
||||
);
|
||||
};
|
||||
|
||||
const removeExample: () => void = () => {
|
||||
const update = {};
|
||||
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) =>
|
||||
cleanupPreset({
|
||||
preset: {
|
||||
...prevState,
|
||||
...update,
|
||||
},
|
||||
endpointsConfig,
|
||||
}) as TPreset,
|
||||
);
|
||||
};
|
||||
|
||||
const setAgentOption: SetOption = (param) => (newValue) => {
|
||||
const editablePreset = JSON.parse(JSON.stringify(_preset));
|
||||
const { agentOptions } = editablePreset;
|
||||
agentOptions[param] = newValue;
|
||||
setPreset(
|
||||
(prevState) =>
|
||||
cleanupPreset({
|
||||
preset: {
|
||||
...prevState,
|
||||
agentOptions,
|
||||
},
|
||||
endpointsConfig,
|
||||
}) as TPreset,
|
||||
);
|
||||
};
|
||||
|
||||
const checkPluginSelection: (value: string) => boolean = () => false;
|
||||
const setTools: (newValue: string) => void = () => {
|
||||
return;
|
||||
};
|
||||
|
||||
return {
|
||||
setOption,
|
||||
setExample,
|
||||
addExample,
|
||||
removeExample,
|
||||
getConversation,
|
||||
checkPluginSelection,
|
||||
setAgentOption,
|
||||
setTools,
|
||||
};
|
||||
};
|
||||
|
||||
export default usePresetOptions;
|
||||
152
client/src/hooks/useSetOptions.ts
Normal file
152
client/src/hooks/useSetOptions.ts
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
import {
|
||||
UseSetOptions,
|
||||
TConversation,
|
||||
SetOption,
|
||||
SetExample,
|
||||
TPlugin,
|
||||
} from 'librechat-data-provider';
|
||||
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import usePresetOptions from './usePresetOptions';
|
||||
import store from '~/store';
|
||||
|
||||
const useSetOptions: UseSetOptions = (preset = false) => {
|
||||
const setShowPluginStoreDialog = useSetRecoilState(store.showPluginStoreDialog);
|
||||
const [conversation, setConversation] = useRecoilState(store.conversation);
|
||||
const availableTools = useRecoilValue(store.availableTools);
|
||||
|
||||
const result = usePresetOptions(preset);
|
||||
|
||||
if (result && typeof result !== 'boolean') {
|
||||
return result;
|
||||
}
|
||||
|
||||
const setOption: SetOption = (param) => (newValue) => {
|
||||
const update = {};
|
||||
update[param] = newValue;
|
||||
setConversation(
|
||||
(prevState) =>
|
||||
({
|
||||
...prevState,
|
||||
...update,
|
||||
} as TConversation),
|
||||
);
|
||||
};
|
||||
|
||||
const setExample: SetExample = (i, type, newValue = null) => {
|
||||
const update = {};
|
||||
const current = conversation?.examples?.slice() || [];
|
||||
const currentExample = { ...current[i] } || {};
|
||||
currentExample[type] = { content: newValue };
|
||||
current[i] = currentExample;
|
||||
update['examples'] = current;
|
||||
setConversation(
|
||||
(prevState) =>
|
||||
({
|
||||
...prevState,
|
||||
...update,
|
||||
} as TConversation),
|
||||
);
|
||||
};
|
||||
|
||||
const addExample: () => void = () => {
|
||||
const update = {};
|
||||
const current = conversation?.examples?.slice() || [];
|
||||
current.push({ input: { content: '' }, output: { content: '' } });
|
||||
update['examples'] = current;
|
||||
setConversation(
|
||||
(prevState) =>
|
||||
({
|
||||
...prevState,
|
||||
...update,
|
||||
} as TConversation),
|
||||
);
|
||||
};
|
||||
|
||||
const removeExample: () => void = () => {
|
||||
const update = {};
|
||||
const current = conversation?.examples?.slice() || [];
|
||||
if (current.length <= 1) {
|
||||
update['examples'] = [{ input: { content: '' }, output: { content: '' } }];
|
||||
setConversation(
|
||||
(prevState) =>
|
||||
({
|
||||
...prevState,
|
||||
...update,
|
||||
} as TConversation),
|
||||
);
|
||||
return;
|
||||
}
|
||||
current.pop();
|
||||
update['examples'] = current;
|
||||
setConversation(
|
||||
(prevState) =>
|
||||
({
|
||||
...prevState,
|
||||
...update,
|
||||
} as TConversation),
|
||||
);
|
||||
};
|
||||
|
||||
const getConversation: () => TConversation | null = () => conversation;
|
||||
|
||||
function checkPluginSelection(value: string) {
|
||||
if (!conversation?.tools) {
|
||||
return false;
|
||||
}
|
||||
return conversation.tools.find((el) => el.pluginKey === value) ? true : false;
|
||||
}
|
||||
|
||||
const setAgentOption: SetOption = (param) => (newValue) => {
|
||||
const editableConvo = JSON.stringify(conversation);
|
||||
const convo = JSON.parse(editableConvo);
|
||||
const { agentOptions } = convo;
|
||||
agentOptions[param] = newValue;
|
||||
setConversation(
|
||||
(prevState) =>
|
||||
({
|
||||
...prevState,
|
||||
agentOptions,
|
||||
} as TConversation),
|
||||
);
|
||||
};
|
||||
|
||||
const setTools: (newValue: string) => void = (newValue) => {
|
||||
if (newValue === 'pluginStore') {
|
||||
setShowPluginStoreDialog(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const update = {};
|
||||
const current = conversation?.tools || [];
|
||||
const isSelected = checkPluginSelection(newValue);
|
||||
const tool =
|
||||
availableTools[availableTools.findIndex((el: TPlugin) => el.pluginKey === newValue)];
|
||||
if (isSelected) {
|
||||
update['tools'] = current.filter((el) => el.pluginKey !== newValue);
|
||||
} else {
|
||||
update['tools'] = [...current, tool];
|
||||
}
|
||||
|
||||
localStorage.setItem('lastSelectedTools', JSON.stringify(update['tools']));
|
||||
setConversation(
|
||||
(prevState) =>
|
||||
({
|
||||
...prevState,
|
||||
...update,
|
||||
} as TConversation),
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
setOption,
|
||||
setExample,
|
||||
addExample,
|
||||
removeExample,
|
||||
setAgentOption,
|
||||
getConversation,
|
||||
checkPluginSelection,
|
||||
setTools,
|
||||
};
|
||||
};
|
||||
|
||||
export default useSetOptions;
|
||||
Loading…
Add table
Add a link
Reference in a new issue