refactor(websearch): redesign settings with two-column layout (#12068)

- Refactor WebSearchSettings to use two-column layout (left sidebar + right content)
- Add local search provider settings with internal browser window support
- Add "Set as Default" button in provider settings page
- Show default indicator tag in provider list
- Prevent selection of providers without API key configured
- Add logos for local search providers (Google, Bing, Baidu)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
亢奋猫 2025-12-23 13:22:02 +08:00 committed by GitHub
parent 6815ab65d1
commit 5f0006dced
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 603 additions and 106 deletions

View File

@ -858,8 +858,8 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
)
// search window
ipcMain.handle(IpcChannel.SearchWindow_Open, async (_, uid: string) => {
await searchService.openSearchWindow(uid)
ipcMain.handle(IpcChannel.SearchWindow_Open, async (_, uid: string, show?: boolean) => {
await searchService.openSearchWindow(uid, show)
})
ipcMain.handle(IpcChannel.SearchWindow_Close, async (_, uid: string) => {
await searchService.closeSearchWindow(uid)

View File

@ -14,38 +14,36 @@ export class SearchService {
return SearchService.instance
}
constructor() {
// Initialize the service
}
private async createNewSearchWindow(uid: string): Promise<BrowserWindow> {
private async createNewSearchWindow(uid: string, show: boolean = false): Promise<BrowserWindow> {
const newWindow = new BrowserWindow({
width: 800,
height: 600,
show: false,
width: 1280,
height: 768,
show,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
devTools: is.dev
}
})
newWindow.webContents.session.webRequest.onBeforeSendHeaders({ urls: ['*://*/*'] }, (details, callback) => {
const headers = {
...details.requestHeaders,
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
callback({ requestHeaders: headers })
})
this.searchWindows[uid] = newWindow
newWindow.on('closed', () => {
delete this.searchWindows[uid]
})
newWindow.on('closed', () => delete this.searchWindows[uid])
newWindow.webContents.userAgent =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36'
return newWindow
}
public async openSearchWindow(uid: string): Promise<void> {
await this.createNewSearchWindow(uid)
public async openSearchWindow(uid: string, show: boolean = false): Promise<void> {
const existingWindow = this.searchWindows[uid]
if (existingWindow) {
show && existingWindow.show()
return
}
await this.createNewSearchWindow(uid, show)
}
public async closeSearchWindow(uid: string): Promise<void> {

View File

@ -442,7 +442,7 @@ const api = {
ipcRenderer.invoke(IpcChannel.Nutstore_GetDirectoryContents, token, path)
},
searchService: {
openSearchWindow: (uid: string) => ipcRenderer.invoke(IpcChannel.SearchWindow_Open, uid),
openSearchWindow: (uid: string, show?: boolean) => ipcRenderer.invoke(IpcChannel.SearchWindow_Open, uid, show),
closeSearchWindow: (uid: string) => ipcRenderer.invoke(IpcChannel.SearchWindow_Close, uid),
openUrlInSearchWindow: (uid: string, url: string) => ipcRenderer.invoke(IpcChannel.SearchWindow_OpenUrl, uid, url)
},

View File

@ -0,0 +1 @@
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Baidu</title><path d="M8.859 11.735c1.017-1.71 4.059-3.083 6.202.286 1.579 2.284 4.284 4.397 4.284 4.397s2.027 1.601.73 4.684c-1.24 2.956-5.64 1.607-6.005 1.49l-.024-.009s-1.746-.568-3.776-.112c-2.026.458-3.773.286-3.773.286l-.045-.001c-.328-.01-2.38-.187-3.001-2.968-.675-3.028 2.365-4.687 2.592-4.968.226-.288 1.802-1.37 2.816-3.085zm.986 1.738v2.032h-1.64s-1.64.138-2.213 2.014c-.2 1.252.177 1.99.242 2.148.067.157.596 1.073 1.927 1.342h3.078v-7.514l-1.394-.022zm3.588 2.191l-1.44.024v3.956s.064.985 1.44 1.344h3.541v-5.3h-1.528v3.979h-1.46s-.466-.068-.553-.447v-3.556zM9.82 16.715v3.06H8.58s-.863-.045-1.126-1.049c-.136-.445.02-.959.088-1.16.063-.203.353-.671.951-.85H9.82zm9.525-9.036c2.086 0 2.646 2.06 2.646 2.742 0 .688.284 3.597-2.309 3.655-2.595.057-2.704-1.77-2.704-3.08 0-1.374.277-3.317 2.367-3.317zM4.24 6.08c1.523-.135 2.645 1.55 2.762 2.513.07.625.393 3.486-1.975 4-2.364.515-3.244-2.249-2.984-3.544 0 0 .28-2.797 2.197-2.969zm8.847-1.483c.14-1.31 1.69-3.316 2.931-3.028 1.236.285 2.367 1.944 2.137 3.37-.224 1.428-1.345 3.313-3.095 3.082-1.748-.226-2.143-1.823-1.973-3.424zM9.425 1c1.307 0 2.364 1.519 2.364 3.398 0 1.879-1.057 3.4-2.364 3.4s-2.367-1.521-2.367-3.4C7.058 2.518 8.118 1 9.425 1z" fill="#2932E1" fill-rule="nonzero"></path></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1 @@
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Bing</title><path d="M11.97 7.569a.92.92 0 00-.805.863c-.013.195-.01.209.43 1.347 1 2.59 1.242 3.214 1.283 3.302.099.213.237.413.41.592.134.138.222.212.37.311.26.176.39.224 1.405.527.989.295 1.529.49 1.994.723.603.302 1.024.644 1.29 1.051.191.292.36.815.434 1.342.029.206.029.661 0 .847a2.491 2.491 0 01-.376 1.026c-.1.151-.065.126.081-.058.415-.52.838-1.408 1.054-2.213a6.728 6.728 0 00.102-3.012 6.626 6.626 0 00-3.291-4.53 104.157 104.157 0 00-1.322-.698l-.254-.133a737.941 737.941 0 01-1.575-.827c-.548-.29-.78-.406-.846-.426a1.376 1.376 0 00-.29-.045l-.093.01z" fill="url(#lobe-icons-bing-fill-0)"></path><path d="M13.164 17.24a4.385 4.385 0 00-.202.125 511.45 511.45 0 00-1.795 1.115 163.087 163.087 0 01-.989.614l-.463.288a99.198 99.198 0 01-1.502.941c-.326.2-.704.334-1.09.387-.18.024-.52.024-.7 0a2.807 2.807 0 01-1.318-.538 3.665 3.665 0 01-.543-.545 2.837 2.837 0 01-.506-1.141 2.161 2.161 0 00-.041-.182c-.008-.008.006.138.032.33.027.199.085.487.147.733.482 1.907 1.85 3.457 3.705 4.195a6.31 6.31 0 001.658.412c.22.025.844.035 1.074.017 1.054-.08 1.972-.393 2.913-.992a325.28 325.28 0 01.937-.596l.384-.244.684-.435.234-.149.009-.005.025-.017.013-.007.172-.11.597-.38c.76-.481.987-.65 1.34-.998.148-.146.37-.394.381-.425.002-.007.042-.068.088-.136a2.49 2.49 0 00.373-1.023 4.181 4.181 0 000-.847 4.336 4.336 0 00-.318-1.137c-.224-.472-.7-.9-1.383-1.245a2.972 2.972 0 00-.406-.181c-.01 0-.646.392-1.413.87a7089.171 7089.171 0 00-1.658 1.031l-.439.274z" fill="url(#lobe-icons-bing-fill-1)" fill-rule="nonzero"></path><path d="M4.003 14.946l.004 3.33.042.193c.134.604.366 1.04.77 1.445a2.701 2.701 0 001.955.814c.536 0 1-.135 1.479-.43l.703-.435.556-.346V8.003c0-2.306-.004-3.675-.012-3.782a2.734 2.734 0 00-.797-1.765c-.145-.144-.268-.24-.637-.496A1780.102 1780.102 0 015.762.362C5.406.115 5.38.098 5.271.059a.943.943 0 00-1.254.696C4.003.818 4 1.659 4 6.223v5.394H4l.003 3.329z" fill="url(#lobe-icons-bing-fill-2)" fill-rule="nonzero"></path><defs><radialGradient cx="93.717%" cy="77.818%" fx="93.717%" fy="77.818%" gradientTransform="scale(-1 -.7146) rotate(49.288 2.035 -2.198)" id="lobe-icons-bing-fill-0" r="143.691%"><stop offset="0%" stop-color="#00CACC"></stop><stop offset="100%" stop-color="#048FCE"></stop></radialGradient><radialGradient cx="13.893%" cy="71.448%" fx="13.893%" fy="71.448%" gradientTransform="scale(.6042 1) rotate(-23.34 .184 .494)" id="lobe-icons-bing-fill-1" r="149.21%"><stop offset="0%" stop-color="#00BBEC"></stop><stop offset="100%" stop-color="#2756A9"></stop></radialGradient><linearGradient id="lobe-icons-bing-fill-2" x1="50%" x2="50%" y1="0%" y2="100%"><stop offset="0%" stop-color="#00BBEC"></stop><stop offset="100%" stop-color="#2756A9"></stop></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1 @@
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Google</title><path d="M23 12.245c0-.905-.075-1.565-.236-2.25h-10.54v4.083h6.186c-.124 1.014-.797 2.542-2.294 3.569l-.021.136 3.332 2.53.23.022C21.779 18.417 23 15.593 23 12.245z" fill="#4285F4"></path><path d="M12.225 23c3.03 0 5.574-.978 7.433-2.665l-3.542-2.688c-.948.648-2.22 1.1-3.891 1.1a6.745 6.745 0 01-6.386-4.572l-.132.011-3.465 2.628-.045.124C4.043 20.531 7.835 23 12.225 23z" fill="#34A853"></path><path d="M5.84 14.175A6.65 6.65 0 015.463 12c0-.758.138-1.491.361-2.175l-.006-.147-3.508-2.67-.115.054A10.831 10.831 0 001 12c0 1.772.436 3.447 1.197 4.938l3.642-2.763z" fill="#FBBC05"></path><path d="M12.225 5.253c2.108 0 3.529.892 4.34 1.638l3.167-3.031C17.787 2.088 15.255 1 12.225 1 7.834 1 4.043 3.469 2.197 7.062l3.63 2.763a6.77 6.77 0 016.398-4.572z" fill="#EB4335"></path></svg>

After

Width:  |  Height:  |  Size: 920 B

View File

@ -4756,6 +4756,12 @@
},
"title": "Other Settings",
"websearch": {
"api_key_required": {
"content": "{{provider}} requires an API key to work. Would you like to configure it now?",
"ok": "Configure",
"title": "API Key Required"
},
"api_providers": "API Providers",
"apikey": "API key",
"blacklist": "Blacklist",
"blacklist_description": "Results from the following websites will not appear in search results",
@ -4797,7 +4803,15 @@
},
"content_limit": "Content length limit",
"content_limit_tooltip": "Limit the content length of the search results; content that exceeds the limit will be truncated.",
"default_provider": "Default Provider",
"free": "Free",
"is_default": "Default",
"local_provider": {
"hint": "Log in to the website to get better search results and personalize your search settings.",
"open_settings": "Open {{provider}} Settings",
"settings": "Local Search Settings"
},
"local_providers": "Local Providers",
"no_provider_selected": "Please select a search service provider before checking.",
"overwrite": "Override search service",
"overwrite_tooltip": "Force use search service instead of LLM",
@ -4808,6 +4822,7 @@
"search_provider": "Search service provider",
"search_provider_placeholder": "Choose a search service provider.",
"search_with_time": "Search with dates included",
"set_as_default": "Set as Default",
"subscribe": "Blacklist Subscription",
"subscribe_add": "Add Subscription",
"subscribe_add_failed": "Failed to add feed source",

View File

@ -4756,6 +4756,12 @@
},
"title": "其他设置",
"websearch": {
"api_key_required": {
"content": "{{provider}} 需要 API 密钥才能使用。是否现在去配置?",
"ok": "去配置",
"title": "需要 API 密钥"
},
"api_providers": "API 服务商",
"apikey": "API 密钥",
"blacklist": "黑名单",
"blacklist_description": "在搜索结果中不会出现以下网站的结果",
@ -4797,7 +4803,15 @@
},
"content_limit": "内容长度限制",
"content_limit_tooltip": "限制搜索结果的内容长度, 超过限制的内容将被截断",
"default_provider": "默认搜索引擎",
"free": "免费",
"is_default": "默认搜索",
"local_provider": {
"hint": "登录网站可以获得更好的搜索结果,也可以对搜索进行个性化设置。",
"open_settings": "打开 {{provider}} 设置",
"settings": "本地搜索设置"
},
"local_providers": "本地搜索",
"no_provider_selected": "请选择搜索服务商后再检测",
"overwrite": "覆盖服务商搜索",
"overwrite_tooltip": "强制使用搜索服务商而不是大语言模型进行搜索",
@ -4808,6 +4822,7 @@
"search_provider": "搜索服务商",
"search_provider_placeholder": "选择一个搜索服务商",
"search_with_time": "搜索包含日期",
"set_as_default": "设为默认",
"subscribe": "黑名单订阅",
"subscribe_add": "添加订阅",
"subscribe_add_failed": "订阅源添加失败",

View File

@ -4756,6 +4756,12 @@
},
"title": "其他設定",
"websearch": {
"api_key_required": {
"content": "{{provider}} 需要 API 金鑰才能運作。您現在要設定嗎?",
"ok": "設定",
"title": "需要 API 金鑰"
},
"api_providers": "API 服務商",
"apikey": "API 金鑰",
"blacklist": "黑名單",
"blacklist_description": "以下網站不會出現在搜尋結果中",
@ -4797,7 +4803,15 @@
},
"content_limit": "內容長度限制",
"content_limit_tooltip": "限制搜尋結果的內容長度;超過限制的內容將被截斷。",
"default_provider": "預設搜尋引擎",
"free": "免費",
"is_default": "[to be translated]:Default",
"local_provider": {
"hint": "登入網站以獲得更佳搜尋結果並個人化您的搜尋設定。",
"open_settings": "開啟 {{provider}} 設定",
"settings": "本地搜尋設定"
},
"local_providers": "本地搜尋",
"no_provider_selected": "請選擇搜尋供應商後再檢查",
"overwrite": "覆蓋搜尋服務",
"overwrite_tooltip": "強制使用搜尋服務而不是 LLM",
@ -4808,6 +4822,7 @@
"search_provider": "搜尋供應商",
"search_provider_placeholder": "選擇一個搜尋供應商",
"search_with_time": "搜尋包含日期",
"set_as_default": "[to be translated]:Set as Default",
"subscribe": "黑名單訂閱",
"subscribe_add": "新增訂閱",
"subscribe_add_failed": "訂閱來源新增失敗",

View File

@ -4756,6 +4756,12 @@
},
"title": "Weitere Einstellungen",
"websearch": {
"api_key_required": {
"content": "{{provider}} erfordert einen API-Schlüssel, um zu funktionieren. Möchten Sie ihn jetzt konfigurieren?",
"ok": "Konfigurieren",
"title": "API-Schlüssel erforderlich"
},
"api_providers": "API-Anbieter",
"apikey": "API-Schlüssel",
"blacklist": "Schwarze Liste",
"blacklist_description": "Folgende Websites werden nicht in Suchergebnissen angezeigt",
@ -4797,7 +4803,15 @@
},
"content_limit": "Inhaltslängenbegrenzung",
"content_limit_tooltip": "Begrenzen Sie die Länge der Suchergebnisse, überschreitende Inhalte werden abgeschnitten",
"default_provider": "Standardanbieter",
"free": "Kostenlos",
"is_default": "[to be translated]:Default",
"local_provider": {
"hint": "Melden Sie sich auf der Website an, um bessere Suchergebnisse zu erhalten und Ihre Sucheinstellungen zu personalisieren.",
"open_settings": "{{provider}}-Einstellungen öffnen",
"settings": "Lokale Sucheinstellungen"
},
"local_providers": "Lokale Anbieter",
"no_provider_selected": "Wählen Sie einen Suchanbieter aus, bevor Sie suchen",
"overwrite": "Suchanbieter statt LLM für Suche erzwingen",
"overwrite_tooltip": "Suchanbieter statt LLM für Suche erzwingen",
@ -4808,6 +4822,7 @@
"search_provider": "Suchanbieter",
"search_provider_placeholder": "Einen Suchanbieter auswählen",
"search_with_time": "Suche mit Datum",
"set_as_default": "[to be translated]:Set as Default",
"subscribe": "Schwarze Liste-Abonnement",
"subscribe_add": "Abonnement hinzufügen",
"subscribe_add_failed": "Abonnement-Quelle hinzufügen fehlgeschlagen",

View File

@ -4756,6 +4756,12 @@
},
"title": "Ρυθμίσεις Εργαλείων",
"websearch": {
"api_key_required": {
"content": "Ο {{provider}} απαιτεί κλειδί API για να λειτουργήσει. Θα θέλατε να το διαμορφώσετε τώρα;",
"ok": "Ρυθμίστε",
"title": "Απαιτείται κλειδί API"
},
"api_providers": "Πάροχοι API",
"apikey": "Κλειδί API",
"blacklist": "Μαύρη Λίστα",
"blacklist_description": "Τα αποτελέσματα από τους παρακάτω ιστότοπους δεν θα εμφανίζονται στα αποτελέσματα αναζήτησης",
@ -4797,7 +4803,15 @@
},
"content_limit": "Όριο μήκους περιεχομένου",
"content_limit_tooltip": "Περιορίζει το μήκος του περιεχομένου των αποτελεσμάτων αναζήτησης, το περιεχόμενο πέραν του ορίου θα περικοπεί",
"default_provider": "Προεπιλεγμένος Πάροχος",
"free": "Δωρεάν",
"is_default": "[to be translated]:Default",
"local_provider": {
"hint": "Συνδεθείτε στην ιστοσελίδα για να λάβετε καλύτερα αποτελέσματα αναζήτησης και να εξατομικεύσετε τις ρυθμίσεις αναζήτησής σας.",
"open_settings": "Άνοιγμα Ρυθμίσεων {{provider}}",
"settings": "Ρυθμίσεις τοπικής αναζήτησης"
},
"local_providers": "Τοπικοί Πάροχοι",
"no_provider_selected": "Παρακαλώ επιλέξτε πάροχο αναζήτησης πριν τον έλεγχο",
"overwrite": "Αντικατάσταση αναζήτησης παρόχου",
"overwrite_tooltip": "Εξαναγκάζει τη χρήση του παρόχου αναζήτησης αντί για μοντέλο μεγάλης γλώσσας για αναζήτηση",
@ -4808,6 +4822,7 @@
"search_provider": "Πάροχος αναζήτησης",
"search_provider_placeholder": "Επιλέξτε έναν πάροχο αναζήτησης",
"search_with_time": "Αναζήτηση με ημερομηνία",
"set_as_default": "[to be translated]:Set as Default",
"subscribe": "Εγγραφή σε μαύρη λίστα",
"subscribe_add": "Προσθήκη εγγραφής",
"subscribe_add_failed": "Η προσθήκη της ροής συνδρομής απέτυχε",

View File

@ -4756,6 +4756,12 @@
},
"title": "Configuración de Herramientas",
"websearch": {
"api_key_required": {
"content": "{{provider}} requiere una clave de API para funcionar. ¿Te gustaría configurarla ahora?",
"ok": "Configurar",
"title": "Se requiere clave de API"
},
"api_providers": "Proveedores de API",
"apikey": "Clave API",
"blacklist": "Lista negra",
"blacklist_description": "Los resultados de los siguientes sitios web no aparecerán en los resultados de búsqueda",
@ -4797,7 +4803,15 @@
},
"content_limit": "Límite de longitud del contenido",
"content_limit_tooltip": "Limita la longitud del contenido en los resultados de búsqueda; el contenido que exceda el límite será truncado",
"default_provider": "Proveedor Predeterminado",
"free": "Gratis",
"is_default": "[to be translated]:Default",
"local_provider": {
"hint": "Inicia sesión en el sitio web para obtener mejores resultados de búsqueda y personalizar tu configuración de búsqueda.",
"open_settings": "Abrir configuración de {{provider}}",
"settings": "Configuración de búsqueda local"
},
"local_providers": "Proveedores locales",
"no_provider_selected": "Seleccione un proveedor de búsqueda antes de comprobar",
"overwrite": "Sobrescribir búsqueda del proveedor",
"overwrite_tooltip": "Forzar el uso del proveedor de búsqueda en lugar del modelo de lenguaje grande",
@ -4808,6 +4822,7 @@
"search_provider": "Proveedor de búsqueda",
"search_provider_placeholder": "Seleccione un proveedor de búsqueda",
"search_with_time": "Buscar con fecha",
"set_as_default": "[to be translated]:Set as Default",
"subscribe": "Suscripción a lista negra",
"subscribe_add": "Añadir suscripción",
"subscribe_add_failed": "Error al agregar la fuente de suscripción",

View File

@ -4756,6 +4756,12 @@
},
"title": "Paramètres des outils",
"websearch": {
"api_key_required": {
"content": "{{provider}} nécessite une clé API pour fonctionner. Souhaitez-vous la configurer maintenant ?",
"ok": "Configurer",
"title": "Clé API requise"
},
"api_providers": "Fournisseurs d'API",
"apikey": "Clé API",
"blacklist": "Liste noire",
"blacklist_description": "Les résultats provenant des sites suivants n'apparaîtront pas dans les résultats de recherche",
@ -4797,7 +4803,15 @@
},
"content_limit": "Limite de longueur du contenu",
"content_limit_tooltip": "Limiter la longueur du contenu des résultats de recherche ; le contenu dépassant cette limite sera tronqué",
"default_provider": "Fournisseur par défaut",
"free": "Gratuit",
"is_default": "[to be translated]:Default",
"local_provider": {
"hint": "Connectez-vous au site Web pour obtenir de meilleurs résultats de recherche et personnaliser vos paramètres de recherche.",
"open_settings": "Ouvrir les paramètres de {{provider}}",
"settings": "Paramètres de recherche locale"
},
"local_providers": "Fournisseurs locaux",
"no_provider_selected": "Veuillez sélectionner un fournisseur de recherche avant de vérifier",
"overwrite": "Remplacer la recherche du fournisseur",
"overwrite_tooltip": "Forcer l'utilisation du fournisseur de recherche au lieu du grand modèle linguistique",
@ -4808,6 +4822,7 @@
"search_provider": "Fournisseur de recherche",
"search_provider_placeholder": "Sélectionnez un fournisseur de recherche",
"search_with_time": "Rechercher avec date",
"set_as_default": "[to be translated]:Set as Default",
"subscribe": "Abonnement à la liste noire",
"subscribe_add": "Ajouter un abonnement",
"subscribe_add_failed": "Échec de l'ajout de la source d'abonnement",

View File

@ -4756,6 +4756,12 @@
},
"title": "その他の設定",
"websearch": {
"api_key_required": {
"content": "{{provider}}はAPIキーが必要です。今すぐ設定しますか",
"ok": "設定",
"title": "APIキーが必要"
},
"api_providers": "APIプロバイダー",
"apikey": "APIキー",
"blacklist": "ブラックリスト",
"blacklist_description": "以下のウェブサイトの結果は検索結果に表示されません",
@ -4797,7 +4803,15 @@
},
"content_limit": "コンテンツ制限",
"content_limit_tooltip": "検索結果のコンテンツの長さを制限します。制限を超えるコンテンツは切り捨てられます。",
"default_provider": "デフォルトプロバイダー",
"free": "無料",
"is_default": "[to be translated]:Default",
"local_provider": {
"hint": "ウェブサイトにログインして、より良い検索結果を得て、検索設定をパーソナライズしてください。",
"open_settings": "{{provider}}設定を開く",
"settings": "ローカル検索設定"
},
"local_providers": "地元のプロバイダー",
"no_provider_selected": "検索サービスプロバイダーを選択してから再確認してください。",
"overwrite": "検索サービスを上書き",
"overwrite_tooltip": "LLMの代わりに検索サービスを強制的に使用する",
@ -4808,6 +4822,7 @@
"search_provider": "検索サービスプロバイダー",
"search_provider_placeholder": "検索サービスプロバイダーを選択する",
"search_with_time": "日付を含む検索",
"set_as_default": "[to be translated]:Set as Default",
"subscribe": "ブラックリスト購読",
"subscribe_add": "購読を追加",
"subscribe_add_failed": "購読ソースの追加に失敗しました",

View File

@ -40,7 +40,7 @@
"error": {
"description": "O Git Bash é necessário para executar agentes no Windows. O agente não pode funcionar sem ele. Por favor, instale o Git para Windows a partir de",
"recheck": "Reverificar a Instalação do Git Bash",
"required": "[to be translated]:Git Bash path is required on Windows",
"required": "O caminho do Git Bash é necessário no Windows",
"title": "Git Bash Necessário"
},
"found": {
@ -53,7 +53,7 @@
"invalidPath": "O arquivo selecionado não é um executável válido do Git Bash (bash.exe).",
"title": "Selecionar executável do Git Bash"
},
"placeholder": "[to be translated]:Select bash.exe path",
"placeholder": "Selecione o caminho do bash.exe",
"success": "Git Bash detectado com sucesso!",
"tooltip": "O Git Bash é necessário para executar agentes no Windows. Instale-o a partir de git-scm.com, caso não esteja disponível."
},
@ -2198,7 +2198,7 @@
"collapse": "[minimizar]",
"content_placeholder": "Introduza o conteúdo da nota...",
"copyContent": "copiar conteúdo",
"crossPlatformRestoreWarning": "[to be translated]:Cross-platform configuration restored, but notes directory is empty. Please copy your note files to: {{path}}",
"crossPlatformRestoreWarning": "Configuração multiplataforma restaurada, mas o diretório de notas está vazio. Por favor, copie seus arquivos de nota para: {{path}}",
"delete": "eliminar",
"delete_confirm": "Tem a certeza de que deseja eliminar este {{type}}?",
"delete_folder_confirm": "Tem a certeza de que deseja eliminar a pasta \"{{name}}\" e todos os seus conteúdos?",
@ -4756,6 +4756,12 @@
},
"title": "Configurações de Ferramentas",
"websearch": {
"api_key_required": {
"content": "{{provider}} requer uma chave de API para funcionar. Você gostaria de configurá-la agora?",
"ok": "Configurar",
"title": "Chave de API Necessária"
},
"api_providers": "Provedores de API",
"apikey": "Chave API",
"blacklist": "Lista Negra",
"blacklist_description": "Os resultados dos seguintes sites não aparecerão nos resultados de pesquisa",
@ -4797,7 +4803,15 @@
},
"content_limit": "Limite de comprimento do conteúdo",
"content_limit_tooltip": "Limita o comprimento do conteúdo dos resultados de pesquisa; o conteúdo excedente será truncado",
"default_provider": "Provedor Padrão",
"free": "Grátis",
"is_default": "[to be translated]:Default",
"local_provider": {
"hint": "Faça login no site para obter melhores resultados de pesquisa e personalizar suas configurações de busca.",
"open_settings": "Abrir Configurações do {{provider}}",
"settings": "Configurações de Pesquisa Local"
},
"local_providers": "Fornecedores Locais",
"no_provider_selected": "Por favor, selecione um provedor de pesquisa antes de verificar",
"overwrite": "Substituir busca do provedor",
"overwrite_tooltip": "Força o uso do provedor de pesquisa em vez do modelo de linguagem grande",
@ -4808,6 +4822,7 @@
"search_provider": "Provedor de pesquisa",
"search_provider_placeholder": "Selecione um provedor de pesquisa",
"search_with_time": "Pesquisar com data",
"set_as_default": "[to be translated]:Set as Default",
"subscribe": "Assinatura de lista negra",
"subscribe_add": "Adicionar assinatura",
"subscribe_add_failed": "Falha ao adicionar a fonte de subscrição",

View File

@ -4756,6 +4756,12 @@
},
"title": "Другие настройки",
"websearch": {
"api_key_required": {
"content": "{{provider}} требует API-ключ для работы. Хотите настроить его сейчас?",
"ok": "Настроить",
"title": "Требуется ключ API"
},
"api_providers": "Поставщики API",
"apikey": "API ключ",
"blacklist": "Черный список",
"blacklist_description": "Результаты из следующих веб-сайтов не будут отображаться в результатах поиска",
@ -4797,7 +4803,15 @@
},
"content_limit": "Ограничение длины контента",
"content_limit_tooltip": "Ограничить длину контента в результатах поиска; контент, превышающий лимит, будет усечен.",
"default_provider": "Поставщик по умолчанию",
"free": "Бесплатно",
"is_default": "[to be translated]:Default",
"local_provider": {
"hint": "Войдите на сайт, чтобы получать более точные результаты поиска и настроить параметры поиска под себя.",
"open_settings": "Открыть настройки {{provider}}",
"settings": "Настройки локального поиска"
},
"local_providers": "Местные поставщики",
"no_provider_selected": "Пожалуйста, выберите поставщика поисковых услуг, затем проверьте.",
"overwrite": "Переопределить поисковый сервис",
"overwrite_tooltip": "Принудительно использовать поисковый сервис вместо LLM",
@ -4808,6 +4822,7 @@
"search_provider": "поиск сервисного провайдера",
"search_provider_placeholder": "Выберите поставщика поисковых услуг",
"search_with_time": "Поиск, содержащий дату",
"set_as_default": "[to be translated]:Set as Default",
"subscribe": "Подписка на черный список",
"subscribe_add": "Добавить подписку",
"subscribe_add_failed": "Не удалось добавить источник подписки",

View File

@ -1,4 +1,3 @@
import { GlobalOutlined } from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import { McpLogo } from '@renderer/components/Icons'
import Scrollbar from '@renderer/components/Scrollbar'
@ -15,6 +14,7 @@ import {
NotebookPen,
Package,
PictureInPicture2,
Search,
Server,
Settings2,
TextCursorInput,
@ -88,19 +88,13 @@ const SettingsPage: FC = () => {
<Divider />
<MenuItemLink to="/settings/mcp">
<MenuItem className={isRoute('/settings/mcp')}>
<McpLogo width={18} height={18} />
<McpLogo width={18} height={18} style={{ opacity: 0.8 }} />
{t('settings.mcp.title')}
</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/notes">
<MenuItem className={isRoute('/settings/notes')}>
<NotebookPen size={18} />
{t('notes.settings.title')}
</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/websearch">
<MenuItem className={isRoute('/settings/websearch')}>
<GlobalOutlined style={{ fontSize: 18 }} />
<Search size={18} />
{t('settings.tool.websearch.title')}
</MenuItem>
</MenuItemLink>
@ -122,6 +116,12 @@ const SettingsPage: FC = () => {
{t('settings.tool.preprocess.title')}
</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/notes">
<MenuItem className={isRoute('/settings/notes')}>
<NotebookPen size={18} />
{t('notes.settings.title')}
</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/quickphrase">
<MenuItem className={isRoute('/settings/quickphrase')}>
<Zap size={18} />
@ -159,7 +159,7 @@ const SettingsPage: FC = () => {
<Routes>
<Route path="provider" element={<ProviderList />} />
<Route path="model" element={<ModelSettings />} />
<Route path="websearch" element={<WebSearchSettings />} />
<Route path="websearch/*" element={<WebSearchSettings />} />
<Route path="api-server" element={<ApiServerSettings />} />
<Route path="docprocess" element={<DocProcessSettings />} />
<Route path="quickphrase" element={<QuickPhraseSettings />} />

View File

@ -1,22 +1,138 @@
import BaiduLogo from '@renderer/assets/images/search/baidu.svg'
import BingLogo from '@renderer/assets/images/search/bing.svg'
import BochaLogo from '@renderer/assets/images/search/bocha.webp'
import ExaLogo from '@renderer/assets/images/search/exa.png'
import GoogleLogo from '@renderer/assets/images/search/google.svg'
import SearxngLogo from '@renderer/assets/images/search/searxng.svg'
import TavilyLogo from '@renderer/assets/images/search/tavily.png'
import ZhipuLogo from '@renderer/assets/images/search/zhipu.png'
import Selector from '@renderer/components/Selector'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useWebSearchSettings } from '@renderer/hooks/useWebSearchProviders'
import {
useDefaultWebSearchProvider,
useWebSearchProviders,
useWebSearchSettings
} from '@renderer/hooks/useWebSearchProviders'
import { useAppDispatch } from '@renderer/store'
import { setMaxResult, setSearchWithTime } from '@renderer/store/websearch'
import type { WebSearchProvider, WebSearchProviderId } from '@renderer/types'
import { hasObjectKey } from '@renderer/utils'
import { Slider, Switch, Tooltip } from 'antd'
import { t } from 'i18next'
import { Info } from 'lucide-react'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router'
import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
// Provider logos map
const getProviderLogo = (providerId: WebSearchProviderId): string | undefined => {
switch (providerId) {
case 'zhipu':
return ZhipuLogo
case 'tavily':
return TavilyLogo
case 'searxng':
return SearxngLogo
case 'exa':
case 'exa-mcp':
return ExaLogo
case 'bocha':
return BochaLogo
case 'local-google':
return GoogleLogo
case 'local-bing':
return BingLogo
case 'local-baidu':
return BaiduLogo
default:
return undefined
}
}
const BasicSettings: FC = () => {
const { theme } = useTheme()
const { t } = useTranslation()
const { providers } = useWebSearchProviders()
const { provider: defaultProvider, setDefaultProvider } = useDefaultWebSearchProvider()
const { searchWithTime, maxResults, compressionConfig } = useWebSearchSettings()
const navigate = useNavigate()
const dispatch = useAppDispatch()
const updateSelectedWebSearchProvider = (providerId: string) => {
const provider = providers.find((p) => p.id === providerId)
if (provider) {
// Check if provider needs API key but doesn't have one
const needsApiKey = hasObjectKey(provider, 'apiKey')
const hasApiKey = provider.apiKey && provider.apiKey.trim() !== ''
if (needsApiKey && !hasApiKey) {
// Don't allow selection, show modal to configure
window.modal.confirm({
title: t('settings.tool.websearch.api_key_required.title'),
content: t('settings.tool.websearch.api_key_required.content', { provider: provider.name }),
okText: t('settings.tool.websearch.api_key_required.ok'),
cancelText: t('common.cancel'),
centered: true,
onOk: () => {
navigate(`/settings/websearch/provider/${provider.id}`)
}
})
return
}
setDefaultProvider(provider as WebSearchProvider)
}
}
// Sort providers: API providers first, then local providers
const sortedProviders = [...providers].sort((a, b) => {
const aIsLocal = a.id.startsWith('local')
const bIsLocal = b.id.startsWith('local')
if (aIsLocal && !bIsLocal) return 1
if (!aIsLocal && bIsLocal) return -1
return 0
})
const renderProviderLabel = (provider: WebSearchProvider) => {
const logo = getProviderLogo(provider.id)
const needsApiKey = hasObjectKey(provider, 'apiKey')
return (
<div className="flex items-center gap-2">
{logo ? (
<img src={logo} alt={provider.name} className="h-4 w-4 rounded-sm object-contain" />
) : (
<div className="h-4 w-4 rounded-sm bg-[var(--color-background-soft)]" />
)}
<span>
{provider.name}
{needsApiKey && ` (${t('settings.tool.websearch.apikey')})`}
</span>
</div>
)
}
return (
<>
<SettingGroup theme={theme}>
<SettingTitle>{t('settings.tool.websearch.search_provider')}</SettingTitle>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.tool.websearch.default_provider')}</SettingRowTitle>
<Selector
size={14}
value={defaultProvider?.id}
onChange={(value: string) => updateSelectedWebSearchProvider(value)}
placeholder={t('settings.tool.websearch.search_provider_placeholder')}
options={sortedProviders.map((p) => ({
value: p.id,
label: renderProviderLabel(p)
}))}
/>
</SettingRow>
</SettingGroup>
<SettingGroup theme={theme} style={{ paddingBottom: 8 }}>
<SettingTitle>{t('settings.general.title')}</SettingTitle>
<SettingDivider />
@ -48,4 +164,5 @@ const BasicSettings: FC = () => {
</>
)
}
export default BasicSettings

View File

@ -0,0 +1,21 @@
import { useTheme } from '@renderer/context/ThemeProvider'
import type { FC } from 'react'
import { SettingContainer } from '..'
import BasicSettings from './BasicSettings'
import BlacklistSettings from './BlacklistSettings'
import CompressionSettings from './CompressionSettings'
const WebSearchGeneralSettings: FC = () => {
const { theme } = useTheme()
return (
<SettingContainer theme={theme}>
<BasicSettings />
<CompressionSettings />
<BlacklistSettings />
</SettingContainer>
)
}
export default WebSearchGeneralSettings

View File

@ -1,7 +1,10 @@
import { CheckOutlined, ExportOutlined, LoadingOutlined } from '@ant-design/icons'
import { loggerService } from '@logger'
import BaiduLogo from '@renderer/assets/images/search/baidu.svg'
import BingLogo from '@renderer/assets/images/search/bing.svg'
import BochaLogo from '@renderer/assets/images/search/bocha.webp'
import ExaLogo from '@renderer/assets/images/search/exa.png'
import GoogleLogo from '@renderer/assets/images/search/google.svg'
import SearxngLogo from '@renderer/assets/images/search/searxng.svg'
import TavilyLogo from '@renderer/assets/images/search/tavily.png'
import ZhipuLogo from '@renderer/assets/images/search/zhipu.png'
@ -9,7 +12,7 @@ import { HStack } from '@renderer/components/Layout'
import ApiKeyListPopup from '@renderer/components/Popups/ApiKeyListPopup/popup'
import { WEB_SEARCH_PROVIDER_CONFIG } from '@renderer/config/webSearchProviders'
import { useTimer } from '@renderer/hooks/useTimer'
import { useWebSearchProvider } from '@renderer/hooks/useWebSearchProviders'
import { useDefaultWebSearchProvider, useWebSearchProvider } from '@renderer/hooks/useWebSearchProviders'
import WebSearchService from '@renderer/services/WebSearchService'
import type { WebSearchProviderId } from '@renderer/types'
import { formatApiKeys, hasObjectKey } from '@renderer/utils'
@ -30,6 +33,7 @@ interface Props {
const WebSearchProviderSetting: FC<Props> = ({ providerId }) => {
const { provider, updateProvider } = useWebSearchProvider(providerId)
const { provider: defaultProvider, setDefaultProvider } = useDefaultWebSearchProvider()
const { t } = useTranslation()
const [apiKey, setApiKey] = useState(provider.apiKey || '')
const [apiHost, setApiHost] = useState(provider.apiHost || '')
@ -149,26 +153,79 @@ const WebSearchProviderSetting: FC<Props> = ({ providerId }) => {
return ExaLogo
case 'bocha':
return BochaLogo
case 'local-google':
return GoogleLogo
case 'local-bing':
return BingLogo
case 'local-baidu':
return BaiduLogo
default:
return undefined
}
}
const isLocalProvider = provider.id.startsWith('local')
const openLocalProviderSettings = async () => {
if (officialWebsite) {
await window.api.searchService.openSearchWindow(provider.id, true)
await window.api.searchService.openUrlInSearchWindow(provider.id, officialWebsite)
}
}
const providerLogo = getWebSearchProviderLogo(provider.id)
// Check if this provider is already the default
const isDefault = defaultProvider?.id === provider.id
// Check if provider needs API key but doesn't have one configured
const needsApiKey = hasObjectKey(provider, 'apiKey')
const hasApiKey = provider.apiKey && provider.apiKey.trim() !== ''
const canSetAsDefault = !isDefault && (!needsApiKey || hasApiKey)
const handleSetAsDefault = () => {
if (canSetAsDefault) {
setDefaultProvider(provider)
}
}
return (
<>
<SettingTitle>
<Flex align="center" gap={8}>
<ProviderLogo src={getWebSearchProviderLogo(provider.id)} />
<ProviderName> {provider.name}</ProviderName>
{officialWebsite && webSearchProviderConfig?.websites && (
<Link target="_blank" href={webSearchProviderConfig.websites.official}>
<ExportOutlined style={{ color: 'var(--color-text)', fontSize: '12px' }} />
</Link>
)}
<Flex align="center" justify="space-between" style={{ width: '100%' }}>
<Flex align="center" gap={8}>
{providerLogo ? (
<img src={providerLogo} alt={provider.name} className="h-5 w-5 object-contain" />
) : (
<div className="h-5 w-5 rounded bg-[var(--color-background-soft)]" />
)}
<ProviderName> {provider.name}</ProviderName>
{officialWebsite && webSearchProviderConfig?.websites && (
<Link target="_blank" href={webSearchProviderConfig.websites.official}>
<ExportOutlined style={{ color: 'var(--color-text)', fontSize: '12px' }} />
</Link>
)}
</Flex>
<Button type="default" disabled={!canSetAsDefault} onClick={handleSetAsDefault}>
{isDefault ? t('settings.tool.websearch.is_default') : t('settings.tool.websearch.set_as_default')}
</Button>
</Flex>
</SettingTitle>
<Divider style={{ width: '100%', margin: '10px 0' }} />
{hasObjectKey(provider, 'apiKey') && (
{isLocalProvider && (
<>
<SettingSubtitle style={{ marginTop: 5, marginBottom: 10 }}>
{t('settings.tool.websearch.local_provider.settings')}
</SettingSubtitle>
<Button type="primary" onClick={openLocalProviderSettings} icon={<ExportOutlined />}>
{t('settings.tool.websearch.local_provider.open_settings', { provider: provider.name })}
</Button>
<SettingHelpTextRow style={{ marginTop: 10 }}>
<SettingHelpText>{t('settings.tool.websearch.local_provider.hint')}</SettingHelpText>
</SettingHelpTextRow>
</>
)}
{!isLocalProvider && hasObjectKey(provider, 'apiKey') && (
<>
<SettingSubtitle
style={{
@ -219,7 +276,7 @@ const WebSearchProviderSetting: FC<Props> = ({ providerId }) => {
</SettingHelpTextRow>
</>
)}
{hasObjectKey(provider, 'apiHost') && (
{!isLocalProvider && hasObjectKey(provider, 'apiHost') && (
<>
<SettingSubtitle style={{ marginTop: 5, marginBottom: 10 }}>
{t('settings.provider.api_host')}
@ -234,10 +291,11 @@ const WebSearchProviderSetting: FC<Props> = ({ providerId }) => {
</Flex>
</>
)}
{hasObjectKey(provider, 'basicAuthUsername') && (
{!isLocalProvider && hasObjectKey(provider, 'basicAuthUsername') && (
<>
<SettingDivider style={{ marginTop: 12, marginBottom: 12 }} />
<SettingSubtitle style={{ marginTop: 5, marginBottom: 10 }}>
<SettingSubtitle
style={{ marginTop: 5, marginBottom: 10, display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
{t('settings.provider.basic_auth.label')}
<Tooltip title={t('settings.provider.basic_auth.tip')} placement="right">
<Info size={16} color="var(--color-icon)" style={{ marginLeft: 5, cursor: 'pointer' }} />
@ -291,10 +349,5 @@ const ProviderName = styled.span`
font-size: 14px;
font-weight: 500;
`
const ProviderLogo = styled.img`
width: 20px;
height: 20px;
object-fit: contain;
`
export default WebSearchProviderSetting

View File

@ -0,0 +1,26 @@
import { useTheme } from '@renderer/context/ThemeProvider'
import type { WebSearchProviderId } from '@renderer/types'
import type { FC } from 'react'
import { useParams } from 'react-router'
import { SettingContainer, SettingGroup } from '..'
import WebSearchProviderSetting from './WebSearchProviderSetting'
const WebSearchProviderSettings: FC = () => {
const { providerId } = useParams<{ providerId: string }>()
const { theme } = useTheme()
if (!providerId) {
return null
}
return (
<SettingContainer theme={theme}>
<SettingGroup theme={theme}>
<WebSearchProviderSetting providerId={providerId as WebSearchProviderId} />
</SettingGroup>
</SettingContainer>
)
}
export default WebSearchProviderSettings

View File

@ -1,66 +1,195 @@
import Selector from '@renderer/components/Selector'
import { useTheme } from '@renderer/context/ThemeProvider'
import BaiduLogo from '@renderer/assets/images/search/baidu.svg'
import BingLogo from '@renderer/assets/images/search/bing.svg'
import BochaLogo from '@renderer/assets/images/search/bocha.webp'
import ExaLogo from '@renderer/assets/images/search/exa.png'
import GoogleLogo from '@renderer/assets/images/search/google.svg'
import SearxngLogo from '@renderer/assets/images/search/searxng.svg'
import TavilyLogo from '@renderer/assets/images/search/tavily.png'
import ZhipuLogo from '@renderer/assets/images/search/zhipu.png'
import DividerWithText from '@renderer/components/DividerWithText'
import ListItem from '@renderer/components/ListItem'
import Scrollbar from '@renderer/components/Scrollbar'
import { useDefaultWebSearchProvider, useWebSearchProviders } from '@renderer/hooks/useWebSearchProviders'
import type { WebSearchProvider } from '@renderer/types'
import type { WebSearchProviderId } from '@renderer/types'
import { hasObjectKey } from '@renderer/utils'
import { Flex, Tag } from 'antd'
import { Search } from 'lucide-react'
import type { FC } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router'
import styled from 'styled-components'
import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
import BasicSettings from './BasicSettings'
import BlacklistSettings from './BlacklistSettings'
import CompressionSettings from './CompressionSettings'
import WebSearchProviderSetting from './WebSearchProviderSetting'
import WebSearchGeneralSettings from './WebSearchGeneralSettings'
import WebSearchProviderSettings from './WebSearchProviderSettings'
const WebSearchSettings: FC = () => {
const { providers } = useWebSearchProviders()
const { provider: defaultProvider, setDefaultProvider } = useDefaultWebSearchProvider()
const { t } = useTranslation()
const [selectedProvider, setSelectedProvider] = useState<WebSearchProvider | undefined>(defaultProvider)
const { theme: themeMode } = useTheme()
const { providers } = useWebSearchProviders()
const { provider: defaultProvider } = useDefaultWebSearchProvider()
const navigate = useNavigate()
const location = useLocation()
const isLocalProvider = selectedProvider?.id.startsWith('local')
// Get the currently active view
const getActiveView = () => {
const path = location.pathname
function updateSelectedWebSearchProvider(providerId: string) {
const provider = providers.find((p) => p.id === providerId)
if (!provider) {
return
if (path === '/settings/websearch/general' || path === '/settings/websearch') {
return 'general'
}
// Check if it's a provider page
for (const provider of providers) {
if (path === `/settings/websearch/provider/${provider.id}`) {
return provider.id
}
}
return 'general'
}
const activeView = getActiveView()
// Filter providers that have API settings (apiKey or apiHost)
const apiProviders = providers.filter((p) => hasObjectKey(p, 'apiKey') || hasObjectKey(p, 'apiHost'))
const localProviders = providers.filter((p) => p.id.startsWith('local'))
// Provider logos map
const getProviderLogo = (providerId: WebSearchProviderId): string | undefined => {
switch (providerId) {
case 'zhipu':
return ZhipuLogo
case 'tavily':
return TavilyLogo
case 'searxng':
return SearxngLogo
case 'exa':
case 'exa-mcp':
return ExaLogo
case 'bocha':
return BochaLogo
case 'local-google':
return GoogleLogo
case 'local-bing':
return BingLogo
case 'local-baidu':
return BaiduLogo
default:
return undefined
}
setSelectedProvider(provider)
setDefaultProvider(provider)
}
return (
<SettingContainer theme={themeMode}>
<SettingGroup theme={themeMode}>
<SettingTitle>{t('settings.tool.websearch.title')}</SettingTitle>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.tool.websearch.search_provider')}</SettingRowTitle>
<div style={{ display: 'flex', gap: '8px' }}>
<Selector
size={14}
value={selectedProvider?.id}
onChange={(value: string) => updateSelectedWebSearchProvider(value)}
placeholder={t('settings.tool.websearch.search_provider_placeholder')}
options={providers.map((p) => ({
value: p.id,
label: `${p.name} (${hasObjectKey(p, 'apiKey') ? t('settings.tool.websearch.apikey') : t('settings.tool.websearch.free')})`
}))}
/>
</div>
</SettingRow>
</SettingGroup>
{!isLocalProvider && (
<SettingGroup theme={themeMode}>
{selectedProvider && <WebSearchProviderSetting providerId={selectedProvider.id} />}
</SettingGroup>
)}
<BasicSettings />
<CompressionSettings />
<BlacklistSettings />
</SettingContainer>
<Container>
<MainContainer>
<MenuList>
<ListItem
title={t('settings.tool.websearch.title')}
active={activeView === 'general'}
onClick={() => navigate('/settings/websearch/general')}
icon={<Search size={18} />}
titleStyle={{ fontWeight: 500 }}
/>
<DividerWithText text={t('settings.tool.websearch.api_providers')} style={{ margin: '10px 0 8px 0' }} />
{apiProviders.map((provider) => {
const logo = getProviderLogo(provider.id)
const isDefault = defaultProvider?.id === provider.id
return (
<ListItem
key={provider.id}
title={provider.name}
active={activeView === provider.id}
onClick={() => navigate(`/settings/websearch/provider/${provider.id}`)}
icon={
logo ? (
<img src={logo} alt={provider.name} className="h-5 w-5 rounded object-contain" />
) : (
<div className="h-5 w-5 rounded bg-[var(--color-background-soft)]" />
)
}
titleStyle={{ fontWeight: 500 }}
rightContent={
isDefault ? (
<Tag color="green" style={{ marginLeft: 'auto', marginRight: 0, borderRadius: 16 }}>
{t('common.default')}
</Tag>
) : undefined
}
/>
)
})}
{localProviders.length > 0 && (
<>
<DividerWithText text={t('settings.tool.websearch.local_providers')} style={{ margin: '10px 0 8px 0' }} />
{localProviders.map((provider) => {
const logo = getProviderLogo(provider.id)
const isDefault = defaultProvider?.id === provider.id
return (
<ListItem
key={provider.id}
title={provider.name}
active={activeView === provider.id}
onClick={() => navigate(`/settings/websearch/provider/${provider.id}`)}
icon={
logo ? (
<img src={logo} alt={provider.name} className="h-5 w-5 rounded object-contain" />
) : (
<div className="h-5 w-5 rounded bg-[var(--color-background-soft)]" />
)
}
titleStyle={{ fontWeight: 500 }}
rightContent={
isDefault ? (
<Tag color="green" style={{ marginLeft: 'auto', marginRight: 0, borderRadius: 16 }}>
{t('common.default')}
</Tag>
) : undefined
}
/>
)
})}
</>
)}
</MenuList>
<RightContainer>
<Routes>
<Route index element={<Navigate to="general" replace />} />
<Route path="general" element={<WebSearchGeneralSettings />} />
<Route path="provider/:providerId" element={<WebSearchProviderSettings />} />
</Routes>
</RightContainer>
</MainContainer>
</Container>
)
}
const Container = styled(Flex)`
flex: 1;
`
const MainContainer = styled.div`
display: flex;
flex: 1;
flex-direction: row;
width: 100%;
height: calc(100vh - var(--navbar-height) - 6px);
overflow: hidden;
`
const MenuList = styled(Scrollbar)`
display: flex;
flex-direction: column;
gap: 5px;
width: var(--settings-width);
padding: 12px;
padding-bottom: 48px;
border-right: 0.5px solid var(--color-border);
height: calc(100vh - var(--navbar-height));
`
const RightContainer = styled.div`
flex: 1;
position: relative;
display: flex;
`
export default WebSearchSettings