feat: implement useToolToggle hook for managing tool toggle state, refactor CodeInterpreter and WebSearch components to utilize new hook

This commit is contained in:
Danny Avila 2025-06-21 22:41:46 -04:00
parent ef7e517c06
commit 1d532a864d
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
4 changed files with 118 additions and 136 deletions

View file

@ -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<HTMLInputElement>(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<boolean>(
`${LocalStorageKeys.LAST_CODE_TOGGLE_}${key}`,
isCodeToggleEnabled,
setValue,
storageCondition,
);
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>, 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;

View file

@ -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<HTMLInputElement>(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<boolean>(
`${LocalStorageKeys.LAST_WEB_SEARCH_TOGGLE_}${key}`,
isWebSearchToggleEnabled,
setValue,
storageCondition,
);
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>, 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;

View file

@ -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';

View file

@ -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<boolean>(
`${localStorageKey}${key}`,
isToolEnabled,
setValue,
storageCondition,
);
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>, 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,
};
}