mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-24 18:50:56 +08:00
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:
parent
6815ab65d1
commit
5f0006dced
@ -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)
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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)
|
||||
},
|
||||
|
||||
1
src/renderer/src/assets/images/search/baidu.svg
Normal file
1
src/renderer/src/assets/images/search/baidu.svg
Normal 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 |
1
src/renderer/src/assets/images/search/bing.svg
Normal file
1
src/renderer/src/assets/images/search/bing.svg
Normal 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 |
1
src/renderer/src/assets/images/search/google.svg
Normal file
1
src/renderer/src/assets/images/search/google.svg
Normal 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 |
@ -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",
|
||||
|
||||
@ -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": "订阅源添加失败",
|
||||
|
||||
@ -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": "訂閱來源新增失敗",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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": "Η προσθήκη της ροής συνδρομής απέτυχε",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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": "購読ソースの追加に失敗しました",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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": "Не удалось добавить источник подписки",
|
||||
|
||||
@ -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 />} />
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user