diff --git a/client/src/components/Chat/Input/CodeInterpreter.tsx b/client/src/components/Chat/Input/CodeInterpreter.tsx index f4b380f5b1..03407ff068 100644 --- a/client/src/components/Chat/Input/CodeInterpreter.tsx +++ b/client/src/components/Chat/Input/CodeInterpreter.tsx @@ -1,52 +1,25 @@ -import debounce from 'lodash/debounce'; -import React, { memo, useMemo, useCallback, useEffect, useRef } from 'react'; -import { useRecoilState } from 'recoil'; +import React, { memo, useMemo, useRef } from 'react'; import { TerminalSquareIcon } from 'lucide-react'; import { Tools, AuthType, - Constants, - LocalStorageKeys, PermissionTypes, Permissions, + LocalStorageKeys, } from 'librechat-data-provider'; import ApiKeyDialog from '~/components/SidePanel/Agents/Code/ApiKeyDialog'; -import { useLocalize, useHasAccess, useCodeApiKeyForm } from '~/hooks'; +import { useLocalize, useHasAccess, useCodeApiKeyForm, useToolToggle } from '~/hooks'; import CheckboxButton from '~/components/ui/CheckboxButton'; -import useLocalStorage from '~/hooks/useLocalStorageAlt'; import { useVerifyAgentToolAuth } from '~/data-provider'; -import { ephemeralAgentByConvoId } from '~/store'; - -const storageCondition = (value: unknown, rawCurrentValue?: string | null) => { - if (rawCurrentValue) { - try { - const currentValue = rawCurrentValue?.trim() ?? ''; - if (currentValue === 'true' && value === false) { - return true; - } - } catch (e) { - console.error(e); - } - } - return value !== undefined && value !== null && value !== '' && value !== false; -}; function CodeInterpreter({ conversationId }: { conversationId?: string | null }) { const triggerRef = useRef(null); const localize = useLocalize(); - const key = conversationId ?? Constants.NEW_CONVO; const canRunCode = useHasAccess({ permissionType: PermissionTypes.RUN_CODE, permission: Permissions.USE, }); - const [ephemeralAgent, setEphemeralAgent] = useRecoilState(ephemeralAgentByConvoId(key)); - const isCodeToggleEnabled = useMemo(() => { - return ephemeralAgent?.execute_code ?? false; - }, [ephemeralAgent?.execute_code]); - - /** Track previous value to prevent infinite loops */ - const prevIsCodeToggleEnabled = useRef(isCodeToggleEnabled); const { data } = useVerifyAgentToolAuth( { toolId: Tools.execute_code }, @@ -59,46 +32,13 @@ function CodeInterpreter({ conversationId }: { conversationId?: string | null }) const { methods, onSubmit, isDialogOpen, setIsDialogOpen, handleRevokeApiKey } = useCodeApiKeyForm({}); - const setValue = useCallback( - (isChecked: boolean) => { - setEphemeralAgent((prev) => ({ - ...prev, - [Tools.execute_code]: isChecked, - })); - }, - [setEphemeralAgent], - ); - - const [runCode, setRunCode] = useLocalStorage( - `${LocalStorageKeys.LAST_CODE_TOGGLE_}${key}`, - isCodeToggleEnabled, - setValue, - storageCondition, - ); - - const handleChange = useCallback( - (e: React.ChangeEvent, isChecked: boolean) => { - if (!isAuthenticated) { - setIsDialogOpen(true); - e.preventDefault(); - return; - } - setRunCode(isChecked); - }, - [setRunCode, setIsDialogOpen, isAuthenticated], - ); - - const debouncedChange = useMemo( - () => debounce(handleChange, 50, { leading: true }), - [handleChange], - ); - - useEffect(() => { - if (prevIsCodeToggleEnabled.current !== isCodeToggleEnabled) { - setRunCode(isCodeToggleEnabled); - } - prevIsCodeToggleEnabled.current = isCodeToggleEnabled; - }, [isCodeToggleEnabled, runCode, setRunCode]); + const { toggleState: runCode, debouncedChange } = useToolToggle({ + conversationId, + isAuthenticated, + setIsDialogOpen, + toolKey: Tools.execute_code, + localStorageKey: LocalStorageKeys.LAST_CODE_TOGGLE_, + }); if (!canRunCode) { return null; diff --git a/client/src/components/Chat/Input/WebSearch.tsx b/client/src/components/Chat/Input/WebSearch.tsx index 6844ee1da0..3fe0faa8f3 100644 --- a/client/src/components/Chat/Input/WebSearch.tsx +++ b/client/src/components/Chat/Input/WebSearch.tsx @@ -1,49 +1,19 @@ -import React, { memo, useRef, useMemo, useCallback } from 'react'; +import React, { memo, useRef, useMemo } from 'react'; import { Globe } from 'lucide-react'; -import debounce from 'lodash/debounce'; -import { useRecoilState } from 'recoil'; -import { - Tools, - AuthType, - Constants, - Permissions, - PermissionTypes, - LocalStorageKeys, -} from 'librechat-data-provider'; +import { Tools, Permissions, PermissionTypes, LocalStorageKeys } from 'librechat-data-provider'; +import { useLocalize, useHasAccess, useSearchApiKeyForm, useToolToggle } from '~/hooks'; import ApiKeyDialog from '~/components/SidePanel/Agents/Search/ApiKeyDialog'; -import { useLocalize, useHasAccess, useSearchApiKeyForm } from '~/hooks'; import CheckboxButton from '~/components/ui/CheckboxButton'; -import useLocalStorage from '~/hooks/useLocalStorageAlt'; import { useVerifyAgentToolAuth } from '~/data-provider'; -import { ephemeralAgentByConvoId } from '~/store'; - -const storageCondition = (value: unknown, rawCurrentValue?: string | null) => { - if (rawCurrentValue) { - try { - const currentValue = rawCurrentValue?.trim() ?? ''; - if (currentValue === 'true' && value === false) { - return true; - } - } catch (e) { - console.error(e); - } - } - return value !== undefined && value !== null && value !== '' && value !== false; -}; function WebSearch({ conversationId }: { conversationId?: string | null }) { const triggerRef = useRef(null); const localize = useLocalize(); - const key = conversationId ?? Constants.NEW_CONVO; const canUseWebSearch = useHasAccess({ permissionType: PermissionTypes.WEB_SEARCH, permission: Permissions.USE, }); - const [ephemeralAgent, setEphemeralAgent] = useRecoilState(ephemeralAgentByConvoId(key)); - const isWebSearchToggleEnabled = useMemo(() => { - return ephemeralAgent?.web_search ?? false; - }, [ephemeralAgent?.web_search]); const { data } = useVerifyAgentToolAuth( { toolId: Tools.web_search }, @@ -56,39 +26,13 @@ function WebSearch({ conversationId }: { conversationId?: string | null }) { const { methods, onSubmit, isDialogOpen, setIsDialogOpen, handleRevokeApiKey } = useSearchApiKeyForm({}); - const setValue = useCallback( - (isChecked: boolean) => { - setEphemeralAgent((prev) => ({ - ...prev, - web_search: isChecked, - })); - }, - [setEphemeralAgent], - ); - - const [webSearch, setWebSearch] = useLocalStorage( - `${LocalStorageKeys.LAST_WEB_SEARCH_TOGGLE_}${key}`, - isWebSearchToggleEnabled, - setValue, - storageCondition, - ); - - const handleChange = useCallback( - (e: React.ChangeEvent, isChecked: boolean) => { - if (!isAuthenticated) { - setIsDialogOpen(true); - e.preventDefault(); - return; - } - setWebSearch(isChecked); - }, - [setWebSearch, setIsDialogOpen, isAuthenticated], - ); - - const debouncedChange = useMemo( - () => debounce(handleChange, 50, { leading: true }), - [handleChange], - ); + const { toggleState: webSearch, debouncedChange } = useToolToggle({ + conversationId, + toolKey: Tools.web_search, + localStorageKey: LocalStorageKeys.LAST_WEB_SEARCH_TOGGLE_, + isAuthenticated, + setIsDialogOpen, + }); if (!canUseWebSearch) { return null; diff --git a/client/src/hooks/Plugins/index.ts b/client/src/hooks/Plugins/index.ts index c2a0ffe97a..93a2fd3d5d 100644 --- a/client/src/hooks/Plugins/index.ts +++ b/client/src/hooks/Plugins/index.ts @@ -3,3 +3,4 @@ export { default as usePluginInstall } from './usePluginInstall'; export { default as useCodeApiKeyForm } from './useCodeApiKeyForm'; export { default as useSearchApiKeyForm } from './useSearchApiKeyForm'; export { default as usePluginDialogHelpers } from './usePluginDialogHelpers'; +export { useToolToggle } from './useToolToggle'; diff --git a/client/src/hooks/Plugins/useToolToggle.ts b/client/src/hooks/Plugins/useToolToggle.ts new file mode 100644 index 0000000000..ab2fd4e04f --- /dev/null +++ b/client/src/hooks/Plugins/useToolToggle.ts @@ -0,0 +1,97 @@ +import { useRef, useEffect, useCallback, useMemo } from 'react'; +import { useRecoilState } from 'recoil'; +import debounce from 'lodash/debounce'; +import { Constants, LocalStorageKeys } from 'librechat-data-provider'; +import useLocalStorage from '~/hooks/useLocalStorageAlt'; +import { ephemeralAgentByConvoId } from '~/store'; + +const storageCondition = (value: unknown, rawCurrentValue?: string | null) => { + if (rawCurrentValue) { + try { + const currentValue = rawCurrentValue?.trim() ?? ''; + if (currentValue === 'true' && value === false) { + return true; + } + } catch (e) { + console.error(e); + } + } + return value !== undefined && value !== null && value !== '' && value !== false; +}; + +interface UseToolToggleOptions { + conversationId?: string | null; + toolKey: string; + localStorageKey: LocalStorageKeys; + isAuthenticated?: boolean; + setIsDialogOpen?: (open: boolean) => void; +} + +export function useToolToggle({ + conversationId, + toolKey, + localStorageKey, + isAuthenticated, + setIsDialogOpen, +}: UseToolToggleOptions) { + const key = conversationId ?? Constants.NEW_CONVO; + const [ephemeralAgent, setEphemeralAgent] = useRecoilState(ephemeralAgentByConvoId(key)); + + const isToolEnabled = useMemo(() => { + return ephemeralAgent?.[toolKey] ?? false; + }, [ephemeralAgent, toolKey]); + + /** Track previous value to prevent infinite loops */ + const prevIsToolEnabled = useRef(isToolEnabled); + + const setValue = useCallback( + (isChecked: boolean) => { + setEphemeralAgent((prev) => ({ + ...prev, + [toolKey]: isChecked, + })); + }, + [setEphemeralAgent, toolKey], + ); + + const [toggleState, setToggleState] = useLocalStorage( + `${localStorageKey}${key}`, + isToolEnabled, + setValue, + storageCondition, + ); + + const handleChange = useCallback( + (e: React.ChangeEvent, isChecked: boolean) => { + if (isAuthenticated !== undefined && !isAuthenticated && setIsDialogOpen) { + setIsDialogOpen(true); + e.preventDefault(); + return; + } + setToggleState(isChecked); + }, + [setToggleState, setIsDialogOpen, isAuthenticated], + ); + + const debouncedChange = useMemo( + () => debounce(handleChange, 50, { leading: true }), + [handleChange], + ); + + useEffect(() => { + if (prevIsToolEnabled.current !== isToolEnabled) { + setToggleState(isToolEnabled); + } + prevIsToolEnabled.current = isToolEnabled; + }, [isToolEnabled, setToggleState]); + + return { + toggleState, + handleChange, + isToolEnabled, + setToggleState, + ephemeralAgent, + debouncedChange, + setEphemeralAgent, + }; +}