diff --git a/src/App.jsx b/src/App.jsx index 3631fb1..db93809 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -8,6 +8,10 @@ import LibraryManager from './LibraryManager' import WebsearchSettings from './WebsearchSettings' import { markdownToHTML } from './markdown'; import { applyColorScheme } from './colorSchemes' +import { + loadStoredWebsearchEngines, + normalizeWebsearchEngines, +} from './websearchEngines' // Extract or block (first occurrence) and return { think, answer } function splitThinkBlocks(text) { if (!text) return { think: null, answer: '' }; @@ -180,20 +184,19 @@ export default function App() { const [startupTaskMessage, setStartupTaskMessage] = useState(''); const [startupTaskBusy, setStartupTaskBusy] = useState(false); const [searxUrl, setSearxUrl] = useState(() => migrateLegacySearxUrl(localStorage.getItem(WEBSEARCH_URL_KEY))); - const [searxEngines, setSearxEngines] = useState(() => { - try { - const raw = localStorage.getItem(WEBSEARCH_ENGINES_KEY); - if (raw) return JSON.parse(raw); - } catch {} - return ["duckduckgo","bing","wikipedia","github","stack_overflow"]; - }); + const [searxEngines, setSearxEngines] = useState(() => + loadStoredWebsearchEngines(localStorage.getItem(WEBSEARCH_ENGINES_KEY)) + ); useEffect(() => { localStorage.setItem(WEBSEARCH_URL_KEY, searxUrl || ''); }, [searxUrl]); useEffect(() => { try { - localStorage.setItem(WEBSEARCH_ENGINES_KEY, JSON.stringify(searxEngines || [])); + localStorage.setItem( + WEBSEARCH_ENGINES_KEY, + JSON.stringify(normalizeWebsearchEngines(searxEngines)) + ); } catch {} }, [searxEngines]); const [webSearchEnabled, setWebSearchEnabled] = useState(false); @@ -2148,7 +2151,7 @@ async function createNewChat() { searxUrl={searxUrl} setSearxUrl={setSearxUrl} engines={searxEngines} - setEngines={setSearxEngines} + setEngines={(next) => setSearxEngines(normalizeWebsearchEngines(next))} /> )} diff --git a/src/WebsearchSettings.jsx b/src/WebsearchSettings.jsx index 1907e38..3918424 100644 --- a/src/WebsearchSettings.jsx +++ b/src/WebsearchSettings.jsx @@ -1,5 +1,6 @@ // src/WebsearchSettings.jsx -import React, { useEffect, useMemo, useState } from 'react'; +import React from 'react'; +import { WEBSEARCH_ENGINE_OPTIONS, normalizeWebsearchEngines } from './websearchEngines' export default function WebsearchSettings({ searxUrl, @@ -7,27 +8,11 @@ export default function WebsearchSettings({ engines, setEngines, }) { - const KNOWN_ENGINES = useMemo( - () => ["google","bing","yahoo","duckduckgo","brave","github","stackoverflow","reddit","arxiv"], - [] - ); - - const [custom, setCustom] = useState(""); - const toggleEngine = (name) => { const set = new Set(engines || []); if (set.has(name)) set.delete(name); else set.add(name); - setEngines(Array.from(set)); - }; - - const addCustom = () => { - const name = custom.trim(); - if (!name) return; - const set = new Set(engines || []); - set.add(name); - setEngines(Array.from(set)); - setCustom(""); - }; + setEngines(normalizeWebsearchEngines(Array.from(set))); + } return (
@@ -45,14 +30,14 @@ return (

Search Engines

- {KNOWN_ENGINES.map(name => ( -
diff --git a/src/websearchEngines.js b/src/websearchEngines.js new file mode 100644 index 0000000..e4ec1b4 --- /dev/null +++ b/src/websearchEngines.js @@ -0,0 +1,67 @@ +export const WEBSEARCH_ENGINE_OPTIONS = [ + { value: 'google', label: 'Google' }, + { value: 'bing', label: 'Bing' }, + { value: 'yahoo', label: 'Yahoo' }, + { value: 'duckduckgo', label: 'DuckDuckGo' }, + { value: 'brave', label: 'Brave' }, + { value: 'startpage', label: 'Startpage' }, + { value: 'wikipedia', label: 'Wikipedia' }, + { value: 'github', label: 'GitHub' }, + { value: 'stack_overflow', label: 'Stack Overflow' }, + { value: 'reddit', label: 'Reddit' }, + { value: 'arxiv', label: 'arXiv' }, +] + +export const DEFAULT_WEBSEARCH_ENGINES = [ + 'google', + 'bing', + 'duckduckgo', + 'brave', + 'startpage', +] + +const WEBSEARCH_ENGINE_ALIASES = { + stackoverflow: 'stack_overflow', +} + +const ENGINE_ORDER = new Map( + WEBSEARCH_ENGINE_OPTIONS.map((option, index) => [option.value, index]) +) + +export function normalizeWebsearchEngineId(value) { + if (typeof value !== 'string') return null + + const trimmed = value.trim().toLowerCase() + if (!trimmed) return null + + const canonical = WEBSEARCH_ENGINE_ALIASES[trimmed] ?? trimmed + return ENGINE_ORDER.has(canonical) ? canonical : null +} + +export function normalizeWebsearchEngines(value) { + if (!Array.isArray(value)) return [] + + const seen = new Set() + const normalized = [] + + for (const entry of value) { + const canonical = normalizeWebsearchEngineId(entry) + if (!canonical || seen.has(canonical)) continue + seen.add(canonical) + normalized.push(canonical) + } + + normalized.sort((left, right) => ENGINE_ORDER.get(left) - ENGINE_ORDER.get(right)) + return normalized +} + +export function loadStoredWebsearchEngines(rawValue) { + if (typeof rawValue !== 'string') return [...DEFAULT_WEBSEARCH_ENGINES] + + try { + const parsed = JSON.parse(rawValue) + return normalizeWebsearchEngines(parsed) + } catch { + return [...DEFAULT_WEBSEARCH_ENGINES] + } +}