diff --git a/client/src/components/Chat/Input/BadgeRow.tsx b/client/src/components/Chat/Input/BadgeRow.tsx index 6fea6b0d58..9db3ce6ffa 100644 --- a/client/src/components/Chat/Input/BadgeRow.tsx +++ b/client/src/components/Chat/Input/BadgeRow.tsx @@ -21,6 +21,7 @@ import FileSearch from './FileSearch'; import Artifacts from './Artifacts'; import MCPSelect from './MCPSelect'; import WebSearch from './WebSearch'; +import NativeWebSearch from './NativeWebSearch'; import store from '~/store'; interface BadgeRowProps { @@ -371,6 +372,7 @@ function BadgeRow({ {showEphemeralBadges === true && ( <> + diff --git a/client/src/components/Chat/Input/NativeWebSearch.tsx b/client/src/components/Chat/Input/NativeWebSearch.tsx new file mode 100644 index 0000000000..ac5ef3258c --- /dev/null +++ b/client/src/components/Chat/Input/NativeWebSearch.tsx @@ -0,0 +1,57 @@ +import React, { memo } from 'react'; +import { Globe } from 'lucide-react'; +import { CheckboxButton } from '@librechat/client'; +import { LocalStorageKeys, Permissions, PermissionTypes } from 'librechat-data-provider'; +import { useLocalize, useSetIndexOptions, useHasAccess } from '~/hooks'; +import useLocalStorage from '~/hooks/useLocalStorageAlt'; +import { useChatContext } from '~/Providers/ChatContext'; + +function NativeWebSearch() { + const localize = useLocalize(); + const { setOption } = useSetIndexOptions(); + const { conversation } = useChatContext(); + const [isPinned] = useLocalStorage( + `${LocalStorageKeys.LAST_NATIVE_WEB_SEARCH_TOGGLE_}pinned`, + false, + ); + + // Only show native web search if user doesn't have permission for authenticated web search + const canUseWebSearch = useHasAccess({ + permissionType: PermissionTypes.WEB_SEARCH, + permission: Permissions.USE, + }); + + // Don't render if user has access to authenticated web search + if (canUseWebSearch) { + return null; + } + + // Use conversation.web_search as the single source of truth + const webSearchEnabled = conversation?.web_search ?? false; + + // Don't render if not enabled and not pinned + if (!webSearchEnabled && !isPinned) { + return null; + } + + const handleChange = (values: { + e?: React.ChangeEvent; + value: string | boolean; + }) => { + const checked = typeof values.value === 'boolean' ? values.value : values.value === 'true'; + setOption('web_search')(checked); + }; + + return ( + } + /> + ); +} + +export default memo(NativeWebSearch); diff --git a/client/src/components/Chat/Input/ToolsDropdown.tsx b/client/src/components/Chat/Input/ToolsDropdown.tsx index 98d35914c5..b633ab73a4 100644 --- a/client/src/components/Chat/Input/ToolsDropdown.tsx +++ b/client/src/components/Chat/Input/ToolsDropdown.tsx @@ -8,13 +8,16 @@ import { Permissions, ArtifactModes, PermissionTypes, + LocalStorageKeys, defaultAgentCapabilities, } from 'librechat-data-provider'; -import { useLocalize, useHasAccess, useAgentCapabilities } from '~/hooks'; +import { useLocalize, useHasAccess, useAgentCapabilities, useSetIndexOptions } from '~/hooks'; import ArtifactsSubMenu from '~/components/Chat/Input/ArtifactsSubMenu'; import MCPSubMenu from '~/components/Chat/Input/MCPSubMenu'; import { useGetStartupConfig } from '~/data-provider'; import { useBadgeRowContext } from '~/Providers'; +import { useChatContext } from '~/Providers/ChatContext'; +import useLocalStorage from '~/hooks/useLocalStorageAlt'; import { cn } from '~/utils'; interface ToolsDropdownProps { @@ -36,6 +39,8 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => { searchApiKeyForm, } = useBadgeRowContext(); const { data: startupConfig } = useGetStartupConfig(); + const { conversation } = useChatContext(); + const { setOption } = useSetIndexOptions(); const { codeEnabled, webSearchEnabled, artifactsEnabled, fileSearchEnabled } = useAgentCapabilities(agentsConfig?.capabilities ?? defaultAgentCapabilities); @@ -49,6 +54,10 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => { setIsPinned: setIsSearchPinned, authData: webSearchAuthData, } = webSearch; + const [isNativeWebSearchPinned, setIsNativeWebSearchPinned] = useLocalStorage( + `${LocalStorageKeys.LAST_NATIVE_WEB_SEARCH_TOGGLE_}pinned`, + false, + ); const { isPinned: isCodePinned, setIsPinned: setIsCodePinned, @@ -77,6 +86,8 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => { permission: Permissions.USE, }); + const shouldShowNativeWebSearch = !canUseWebSearch; + const showWebSearchSettings = useMemo(() => { const authTypes = webSearchAuthData?.authTypes ?? []; if (authTypes.length === 0) return true; @@ -93,6 +104,11 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => { webSearch.debouncedChange({ value: newValue }); }, [webSearch]); + const handleNativeWebSearchToggle = useCallback(() => { + const currentValue = conversation?.web_search ?? false; + setOption('web_search')(!currentValue); + }, [conversation?.web_search, setOption]); + const handleCodeInterpreterToggle = useCallback(() => { const newValue = !codeInterpreter.toggleState; codeInterpreter.debouncedChange({ value: newValue }); @@ -220,6 +236,38 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => { }); } + if (shouldShowNativeWebSearch) { + dropdownItems.push({ + onClick: handleNativeWebSearchToggle, + hideOnClick: false, + render: (props) => ( +
+
+ + {localize('com_ui_web_search')} +
+ +
+ ), + }); + } + if (canRunCode && codeEnabled) { dropdownItems.push({ onClick: handleCodeInterpreterToggle, diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index ca40ec2c8c..4c91246af8 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -1931,6 +1931,8 @@ export enum LocalStorageKeys { LAST_CODE_TOGGLE_ = 'LAST_CODE_TOGGLE_', /** Last checked toggle for Web Search per conversation ID */ LAST_WEB_SEARCH_TOGGLE_ = 'LAST_WEB_SEARCH_TOGGLE_', + /** Last checked toggle for Native Web Search per conversation ID */ + LAST_NATIVE_WEB_SEARCH_TOGGLE_ = 'LAST_NATIVE_WEB_SEARCH_TOGGLE_', /** Last checked toggle for File Search per conversation ID */ LAST_FILE_SEARCH_TOGGLE_ = 'LAST_FILE_SEARCH_TOGGLE_', /** Last checked toggle for Artifacts per conversation ID */