From fb2ad66c91fa89fa03c601bc88bd587407021648 Mon Sep 17 00:00:00 2001 From: Nikhil Date: Fri, 6 Mar 2026 12:08:39 -0800 Subject: [PATCH] feat: add search provider settings page (#429) * feat: add search provider settings page with 5 engine options Allow users to select their preferred search engine (Google, DuckDuckGo, Bing, Brave Search, Yahoo) from a new settings page. The selected provider drives search suggestions, search URL navigation, placeholder text, and analytics tracking. Replaces all hardcoded Google references with the stored preference. Adds Brave Search support, replacing Yandex. Co-Authored-By: Claude Opus 4.6 * fix: add error handling for search provider storage writes Write to storage before updating React state so UI never diverges from persisted value on failure. Add try/catch in the settings page to show an error toast if the write fails. Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- .../components/sidebar/SettingsSidebar.tsx | 2 + apps/agent/entrypoints/app/App.tsx | 3 ++ .../search-provider/SearchProviderCard.tsx | 54 +++++++++++++++++++ .../search-provider/SearchProviderGrid.tsx | 31 +++++++++++ .../search-provider/SearchProviderHeader.tsx | 34 ++++++++++++ .../search-provider/SearchProviderPage.tsx | 39 ++++++++++++++ .../agent/entrypoints/newtab/index/NewTab.tsx | 10 ++-- .../lib/searchSuggestions/SearchProviders.ts | 2 +- .../searchSuggestions/getSearchSuggestions.ts | 8 +-- .../index/lib/suggestions/useSuggestions.ts | 14 +++-- apps/agent/lib/constants/analyticsEvents.ts | 3 ++ apps/agent/lib/search-provider/providers.ts | 45 ++++++++++++++++ .../search-provider-storage.ts | 32 +++++++++++ apps/agent/wxt.config.ts | 2 +- 14 files changed, 265 insertions(+), 14 deletions(-) create mode 100644 apps/agent/entrypoints/app/search-provider/SearchProviderCard.tsx create mode 100644 apps/agent/entrypoints/app/search-provider/SearchProviderGrid.tsx create mode 100644 apps/agent/entrypoints/app/search-provider/SearchProviderHeader.tsx create mode 100644 apps/agent/entrypoints/app/search-provider/SearchProviderPage.tsx create mode 100644 apps/agent/lib/search-provider/providers.ts create mode 100644 apps/agent/lib/search-provider/search-provider-storage.ts diff --git a/apps/agent/components/sidebar/SettingsSidebar.tsx b/apps/agent/components/sidebar/SettingsSidebar.tsx index ada5d22d..6d138c2f 100644 --- a/apps/agent/components/sidebar/SettingsSidebar.tsx +++ b/apps/agent/components/sidebar/SettingsSidebar.tsx @@ -6,6 +6,7 @@ import { MessageSquare, Palette, RotateCcw, + Search, Server, Sparkles, } from 'lucide-react' @@ -33,6 +34,7 @@ const settingsNavItems: NavItem[] = [ icon: Palette, feature: Feature.CUSTOMIZATION_SUPPORT, }, + { name: 'Search Provider', to: '/settings/search', icon: Search }, { name: 'Agent Soul', to: '/settings/soul', diff --git a/apps/agent/entrypoints/app/App.tsx b/apps/agent/entrypoints/app/App.tsx index f522ec9a..86798a7e 100644 --- a/apps/agent/entrypoints/app/App.tsx +++ b/apps/agent/entrypoints/app/App.tsx @@ -23,6 +23,7 @@ import { MagicLinkCallback } from './login/MagicLinkCallback' import { MCPSettingsPage } from './mcp-settings/MCPSettingsPage' import { ProfilePage } from './profile/ProfilePage' import { ScheduledTasksPage } from './scheduled-tasks/ScheduledTasksPage' +import { SearchProviderPage } from './search-provider/SearchProviderPage' import { SoulPage } from './soul/SoulPage' import { WorkflowsPageWrapper } from './workflows/WorkflowsPageWrapper' @@ -44,6 +45,7 @@ const OptionsRedirect: FC = () => { 'connect-mcp': '/connect-apps', mcp: '/settings/mcp', customization: '/settings/customization', + search: '/settings/search', soul: '/settings/soul', 'jtbd-agent': '/settings/survey', workflows: '/workflows', @@ -91,6 +93,7 @@ export const App: FC = () => { } /> } /> } /> + } /> } /> } /> diff --git a/apps/agent/entrypoints/app/search-provider/SearchProviderCard.tsx b/apps/agent/entrypoints/app/search-provider/SearchProviderCard.tsx new file mode 100644 index 00000000..97b9150c --- /dev/null +++ b/apps/agent/entrypoints/app/search-provider/SearchProviderCard.tsx @@ -0,0 +1,54 @@ +import { Check } from 'lucide-react' +import type { FC } from 'react' +import type { SearchProviderConfig } from '@/lib/search-provider/providers' +import { cn } from '@/lib/utils' + +interface SearchProviderCardProps { + provider: SearchProviderConfig + isSelected: boolean + onSelect: (provider: SearchProviderConfig) => void +} + +export const SearchProviderCard: FC = ({ + provider, + isSelected, + onSelect, +}) => { + return ( + + ) +} diff --git a/apps/agent/entrypoints/app/search-provider/SearchProviderGrid.tsx b/apps/agent/entrypoints/app/search-provider/SearchProviderGrid.tsx new file mode 100644 index 00000000..d180fbcb --- /dev/null +++ b/apps/agent/entrypoints/app/search-provider/SearchProviderGrid.tsx @@ -0,0 +1,31 @@ +import type { FC } from 'react' +import type { SearchProviders } from '@/entrypoints/newtab/index/lib/searchSuggestions/SearchProviders' +import type { SearchProviderConfig } from '@/lib/search-provider/providers' +import { SEARCH_PROVIDERS } from '@/lib/search-provider/providers' +import { SearchProviderCard } from './SearchProviderCard' + +interface SearchProviderGridProps { + selectedProvider: SearchProviders + onSelectProvider: (provider: SearchProviderConfig) => void +} + +export const SearchProviderGrid: FC = ({ + selectedProvider, + onSelectProvider, +}) => { + return ( +
+

Available Search Engines

+
+ {SEARCH_PROVIDERS.map((provider) => ( + + ))} +
+
+ ) +} diff --git a/apps/agent/entrypoints/app/search-provider/SearchProviderHeader.tsx b/apps/agent/entrypoints/app/search-provider/SearchProviderHeader.tsx new file mode 100644 index 00000000..cced0e20 --- /dev/null +++ b/apps/agent/entrypoints/app/search-provider/SearchProviderHeader.tsx @@ -0,0 +1,34 @@ +import { Search } from 'lucide-react' +import type { FC } from 'react' +import type { SearchProviderConfig } from '@/lib/search-provider/providers' + +interface SearchProviderHeaderProps { + activeProvider: SearchProviderConfig +} + +export const SearchProviderHeader: FC = ({ + activeProvider, +}) => { + return ( +
+
+
+ +
+
+

Search Provider

+

+ Choose the default search engine for your browser's address bar and + new tab page +

+
+ + Currently using: + + {activeProvider.name} +
+
+
+
+ ) +} diff --git a/apps/agent/entrypoints/app/search-provider/SearchProviderPage.tsx b/apps/agent/entrypoints/app/search-provider/SearchProviderPage.tsx new file mode 100644 index 00000000..464bfe5d --- /dev/null +++ b/apps/agent/entrypoints/app/search-provider/SearchProviderPage.tsx @@ -0,0 +1,39 @@ +import type { FC } from 'react' +import { toast } from 'sonner' +import { SEARCH_PROVIDER_CHANGED_EVENT } from '@/lib/constants/analyticsEvents' +import { track } from '@/lib/metrics/track' +import type { SearchProviderConfig } from '@/lib/search-provider/providers' +import { getProviderConfig } from '@/lib/search-provider/providers' +import { useSearchProvider } from '@/lib/search-provider/search-provider-storage' +import { SearchProviderGrid } from './SearchProviderGrid' +import { SearchProviderHeader } from './SearchProviderHeader' + +export const SearchProviderPage: FC = () => { + const { provider, setProvider } = useSearchProvider() + const activeConfig = getProviderConfig(provider) + + const handleSelectProvider = async (selected: SearchProviderConfig) => { + if (selected.id === provider) return + + try { + await setProvider(selected.id) + track(SEARCH_PROVIDER_CHANGED_EVENT, { + provider: selected.id, + previous_provider: provider, + }) + toast.success(`Search provider changed to ${selected.name}`) + } catch { + toast.error('Failed to save search provider') + } + } + + return ( +
+ + +
+ ) +} diff --git a/apps/agent/entrypoints/newtab/index/NewTab.tsx b/apps/agent/entrypoints/newtab/index/NewTab.tsx index 76971d1f..644a8aea 100644 --- a/apps/agent/entrypoints/newtab/index/NewTab.tsx +++ b/apps/agent/entrypoints/newtab/index/NewTab.tsx @@ -122,7 +122,7 @@ export const NewTab = () => { setSelectedTabs((prev) => prev.filter((t) => t.id !== tabId)) } - const { sections, flatItems } = useSuggestions({ + const { sections, flatItems, providerConfig } = useSuggestions({ query: inputValue, selectedTabs, }) @@ -290,9 +290,11 @@ export const NewTab = () => { switch (item.type) { case 'search': - track(NEWTAB_SEARCH_EXECUTED_EVENT, { search_engine: 'google' }) + track(NEWTAB_SEARCH_EXECUTED_EVENT, { + search_engine: providerConfig.id, + }) window.open( - `https://www.google.com/search?q=${encodeURIComponent(item.query)}`, + `${providerConfig.searchUrl}${encodeURIComponent(item.query)}`, '_self', ) break @@ -422,7 +424,7 @@ export const NewTab = () => { /> => { return data[1] || [] } -const getYandexSuggestions = async (query: string): Promise => { +const getBraveSuggestions = async (query: string): Promise => { const response = await fetch( - `https://suggest.yandex.com/suggest-ff.cgi?part=${encodeURIComponent(query)}&uil=en&v=3`, + `https://search.brave.com/api/suggest?q=${encodeURIComponent(query)}`, ) const data = await response.json() return data[1] || [] @@ -60,8 +60,8 @@ export const getSearchSuggestions = async ([searchEngine, query]: [ return getYahooIndiaSuggestions(query) case 'duckduckgo': return getDuckDuckGoSuggestions(query) - case 'yandex': - return getYandexSuggestions(query) + case 'brave': + return getBraveSuggestions(query) default: return [] } diff --git a/apps/agent/entrypoints/newtab/index/lib/suggestions/useSuggestions.ts b/apps/agent/entrypoints/newtab/index/lib/suggestions/useSuggestions.ts index cc483fff..72255674 100644 --- a/apps/agent/entrypoints/newtab/index/lib/suggestions/useSuggestions.ts +++ b/apps/agent/entrypoints/newtab/index/lib/suggestions/useSuggestions.ts @@ -1,4 +1,6 @@ import { useMemo } from 'react' +import { getProviderConfig } from '@/lib/search-provider/providers' +import { useSearchProvider } from '@/lib/search-provider/search-provider-storage' import { useAITabSuggestions } from '../aiTabSuggestions/useAITabSuggestions' import { useBrowserOSSuggestions } from '../browserOSSuggestions/useBrowserOSSuggestions' import { useSearchSuggestions } from '../searchSuggestions/useSearchSuggestions' @@ -19,9 +21,12 @@ interface UseSuggestionsArgs { * @public */ export const useSuggestions = ({ query, selectedTabs }: UseSuggestionsArgs) => { + const { provider } = useSearchProvider() + const providerConfig = getProviderConfig(provider) + const { data: searchResultsFromAPI } = useSearchSuggestions({ query, - searchEngine: 'google', + searchEngine: provider, }) const searchResults: string[] = useMemo(() => { @@ -81,8 +86,8 @@ export const useSuggestions = ({ query, selectedTabs }: UseSuggestionsArgs) => { }), ) result.push({ - id: 'google-search', - title: 'Google Search', + id: 'search', + title: `${providerConfig.name} Search`, items: searchItems, }) } @@ -94,6 +99,7 @@ export const useSuggestions = ({ query, selectedTabs }: UseSuggestionsArgs) => { selectedTabs.length, aiTabResults, searchResults, + providerConfig.name, ]) const flatItems = useMemo( @@ -101,7 +107,7 @@ export const useSuggestions = ({ query, selectedTabs }: UseSuggestionsArgs) => { [sections], ) - return { sections, flatItems } + return { sections, flatItems, providerConfig } } /** diff --git a/apps/agent/lib/constants/analyticsEvents.ts b/apps/agent/lib/constants/analyticsEvents.ts index 3b803087..3cbe4d9a 100644 --- a/apps/agent/lib/constants/analyticsEvents.ts +++ b/apps/agent/lib/constants/analyticsEvents.ts @@ -177,6 +177,9 @@ export const SCHEDULED_TASK_RETRIED_EVENT = 'settings.scheduled_task.retried' /** @public */ export const JTBD_POPUP_DISMISSED_EVENT = 'ui.jtbd_popup.dismissed' +/** @public */ +export const SEARCH_PROVIDER_CHANGED_EVENT = 'settings.search_provider.changed' + /** @public */ export const ONBOARDING_STARTED_EVENT = 'onboarding.started' diff --git a/apps/agent/lib/search-provider/providers.ts b/apps/agent/lib/search-provider/providers.ts new file mode 100644 index 00000000..44585d8d --- /dev/null +++ b/apps/agent/lib/search-provider/providers.ts @@ -0,0 +1,45 @@ +import type { SearchProviders } from '@/entrypoints/newtab/index/lib/searchSuggestions/SearchProviders' + +export interface SearchProviderConfig { + id: SearchProviders + name: string + searchUrl: string + description: string +} + +export const SEARCH_PROVIDERS: SearchProviderConfig[] = [ + { + id: 'google', + name: 'Google', + searchUrl: 'https://www.google.com/search?q=', + description: 'The most popular search engine worldwide', + }, + { + id: 'duckduckgo', + name: 'DuckDuckGo', + searchUrl: 'https://duckduckgo.com/?q=', + description: 'Privacy-focused search with no tracking', + }, + { + id: 'bing', + name: 'Bing', + searchUrl: 'https://www.bing.com/search?q=', + description: 'Microsoft search with AI-powered answers', + }, + { + id: 'brave', + name: 'Brave Search', + searchUrl: 'https://search.brave.com/search?q=', + description: 'Independent search with its own index', + }, + { + id: 'yahoo', + name: 'Yahoo', + searchUrl: 'https://search.yahoo.com/search?p=', + description: 'Classic search with news and web results', + }, +] + +export function getProviderConfig(id: SearchProviders): SearchProviderConfig { + return SEARCH_PROVIDERS.find((p) => p.id === id) ?? SEARCH_PROVIDERS[0] +} diff --git a/apps/agent/lib/search-provider/search-provider-storage.ts b/apps/agent/lib/search-provider/search-provider-storage.ts new file mode 100644 index 00000000..591bb586 --- /dev/null +++ b/apps/agent/lib/search-provider/search-provider-storage.ts @@ -0,0 +1,32 @@ +import { storage } from '@wxt-dev/storage' +import { useCallback, useEffect, useState } from 'react' +import type { SearchProviders } from '@/entrypoints/newtab/index/lib/searchSuggestions/SearchProviders' + +const DEFAULT_PROVIDER: SearchProviders = 'google' + +export const searchProviderStorage = storage.defineItem( + 'local:search-provider', + { fallback: DEFAULT_PROVIDER }, +) + +export function useSearchProvider() { + const [provider, setProviderState] = + useState(DEFAULT_PROVIDER) + + useEffect(() => { + searchProviderStorage.getValue().then((value) => { + setProviderState(value ?? DEFAULT_PROVIDER) + }) + const unwatch = searchProviderStorage.watch((newValue) => { + setProviderState(newValue ?? DEFAULT_PROVIDER) + }) + return unwatch + }, []) + + const setProvider = useCallback(async (value: SearchProviders) => { + await searchProviderStorage.setValue(value) + setProviderState(value) + }, []) + + return { provider, setProvider } +} diff --git a/apps/agent/wxt.config.ts b/apps/agent/wxt.config.ts index 15440d46..0c4b849c 100644 --- a/apps/agent/wxt.config.ts +++ b/apps/agent/wxt.config.ts @@ -66,7 +66,7 @@ export default defineConfig({ 'https://api.bing.com/*', 'https://in.search.yahoo.com/*', 'https://duckduckgo.com/*', - 'https://suggest.yandex.com/*', + 'https://search.brave.com/*', ], }, vite: () => ({