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: () => ({