diff --git a/src/main/ipc.ts b/src/main/ipc.ts index a4e0fe5c53..08bfbac6f8 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -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) diff --git a/src/main/services/SearchService.ts b/src/main/services/SearchService.ts index 8a4e42099a..6c69f80889 100644 --- a/src/main/services/SearchService.ts +++ b/src/main/services/SearchService.ts @@ -14,38 +14,36 @@ export class SearchService { return SearchService.instance } - constructor() { - // Initialize the service - } - - private async createNewSearchWindow(uid: string): Promise { + private async createNewSearchWindow(uid: string, show: boolean = false): Promise { 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 { - await this.createNewSearchWindow(uid) + public async openSearchWindow(uid: string, show: boolean = false): Promise { + const existingWindow = this.searchWindows[uid] + + if (existingWindow) { + show && existingWindow.show() + return + } + + await this.createNewSearchWindow(uid, show) } public async closeSearchWindow(uid: string): Promise { diff --git a/src/preload/index.ts b/src/preload/index.ts index d393d4a6e2..424253f8e3 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -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) }, diff --git a/src/renderer/src/assets/images/search/baidu.svg b/src/renderer/src/assets/images/search/baidu.svg new file mode 100644 index 0000000000..ead7f89822 --- /dev/null +++ b/src/renderer/src/assets/images/search/baidu.svg @@ -0,0 +1 @@ +Baidu \ No newline at end of file diff --git a/src/renderer/src/assets/images/search/bing.svg b/src/renderer/src/assets/images/search/bing.svg new file mode 100644 index 0000000000..b411a4f068 --- /dev/null +++ b/src/renderer/src/assets/images/search/bing.svg @@ -0,0 +1 @@ +Bing \ No newline at end of file diff --git a/src/renderer/src/assets/images/search/google.svg b/src/renderer/src/assets/images/search/google.svg new file mode 100644 index 0000000000..e8e0f867bd --- /dev/null +++ b/src/renderer/src/assets/images/search/google.svg @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 63d77e03bf..9528b4cd6b 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -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", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index b3dbc9e365..524f32c338 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -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": "订阅源添加失败", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index a2c26fa399..fe30018ac5 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -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": "訂閱來源新增失敗", diff --git a/src/renderer/src/i18n/translate/de-de.json b/src/renderer/src/i18n/translate/de-de.json index c13e174b06..e77b9dede1 100644 --- a/src/renderer/src/i18n/translate/de-de.json +++ b/src/renderer/src/i18n/translate/de-de.json @@ -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", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 8746eed716..1593099707 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -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": "Η προσθήκη της ροής συνδρομής απέτυχε", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index df7743694e..56f06b1b53 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -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", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 990c94a3c1..4e8f2ac8e6 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -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", diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index d36fddc63c..58ee184061 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -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": "購読ソースの追加に失敗しました", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 65783166cb..553795f6b3 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -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", diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index 2e245f9ff5..489e8b4695 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -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": "Не удалось добавить источник подписки", diff --git a/src/renderer/src/pages/settings/SettingsPage.tsx b/src/renderer/src/pages/settings/SettingsPage.tsx index cb5d8df32a..0f7659ddac 100644 --- a/src/renderer/src/pages/settings/SettingsPage.tsx +++ b/src/renderer/src/pages/settings/SettingsPage.tsx @@ -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 = () => { - + {t('settings.mcp.title')} - - - - {t('notes.settings.title')} - - - + {t('settings.tool.websearch.title')} @@ -122,6 +116,12 @@ const SettingsPage: FC = () => { {t('settings.tool.preprocess.title')} + + + + {t('notes.settings.title')} + + @@ -159,7 +159,7 @@ const SettingsPage: FC = () => { } /> } /> - } /> + } /> } /> } /> } /> diff --git a/src/renderer/src/pages/settings/WebSearchSettings/BasicSettings.tsx b/src/renderer/src/pages/settings/WebSearchSettings/BasicSettings.tsx index 95c0749126..e4db2caf22 100644 --- a/src/renderer/src/pages/settings/WebSearchSettings/BasicSettings.tsx +++ b/src/renderer/src/pages/settings/WebSearchSettings/BasicSettings.tsx @@ -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 ( +
+ {logo ? ( + {provider.name} + ) : ( +
+ )} + + {provider.name} + {needsApiKey && ` (${t('settings.tool.websearch.apikey')})`} + +
+ ) + } + return ( <> + + {t('settings.tool.websearch.search_provider')} + + + {t('settings.tool.websearch.default_provider')} + updateSelectedWebSearchProvider(value)} + placeholder={t('settings.tool.websearch.search_provider_placeholder')} + options={sortedProviders.map((p) => ({ + value: p.id, + label: renderProviderLabel(p) + }))} + /> + + {t('settings.general.title')} @@ -48,4 +164,5 @@ const BasicSettings: FC = () => { ) } + export default BasicSettings diff --git a/src/renderer/src/pages/settings/WebSearchSettings/WebSearchGeneralSettings.tsx b/src/renderer/src/pages/settings/WebSearchSettings/WebSearchGeneralSettings.tsx new file mode 100644 index 0000000000..0af3fb4332 --- /dev/null +++ b/src/renderer/src/pages/settings/WebSearchSettings/WebSearchGeneralSettings.tsx @@ -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 ( + + + + + + ) +} + +export default WebSearchGeneralSettings diff --git a/src/renderer/src/pages/settings/WebSearchSettings/WebSearchProviderSetting.tsx b/src/renderer/src/pages/settings/WebSearchSettings/WebSearchProviderSetting.tsx index a92b8646c1..823f6fac81 100644 --- a/src/renderer/src/pages/settings/WebSearchSettings/WebSearchProviderSetting.tsx +++ b/src/renderer/src/pages/settings/WebSearchSettings/WebSearchProviderSetting.tsx @@ -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 = ({ 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 = ({ 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 ( <> - - - {provider.name} - {officialWebsite && webSearchProviderConfig?.websites && ( - - - - )} + + + {providerLogo ? ( + {provider.name} + ) : ( +
+ )} + {provider.name} + {officialWebsite && webSearchProviderConfig?.websites && ( + + + + )} + + - {hasObjectKey(provider, 'apiKey') && ( + {isLocalProvider && ( + <> + + {t('settings.tool.websearch.local_provider.settings')} + + + + {t('settings.tool.websearch.local_provider.hint')} + + + )} + {!isLocalProvider && hasObjectKey(provider, 'apiKey') && ( <> = ({ providerId }) => { )} - {hasObjectKey(provider, 'apiHost') && ( + {!isLocalProvider && hasObjectKey(provider, 'apiHost') && ( <> {t('settings.provider.api_host')} @@ -234,10 +291,11 @@ const WebSearchProviderSetting: FC = ({ providerId }) => { )} - {hasObjectKey(provider, 'basicAuthUsername') && ( + {!isLocalProvider && hasObjectKey(provider, 'basicAuthUsername') && ( <> - + {t('settings.provider.basic_auth.label')} @@ -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 diff --git a/src/renderer/src/pages/settings/WebSearchSettings/WebSearchProviderSettings.tsx b/src/renderer/src/pages/settings/WebSearchSettings/WebSearchProviderSettings.tsx new file mode 100644 index 0000000000..884c43e6b4 --- /dev/null +++ b/src/renderer/src/pages/settings/WebSearchSettings/WebSearchProviderSettings.tsx @@ -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 ( + + + + + + ) +} + +export default WebSearchProviderSettings diff --git a/src/renderer/src/pages/settings/WebSearchSettings/index.tsx b/src/renderer/src/pages/settings/WebSearchSettings/index.tsx index 7867cb57e0..a21de63764 100644 --- a/src/renderer/src/pages/settings/WebSearchSettings/index.tsx +++ b/src/renderer/src/pages/settings/WebSearchSettings/index.tsx @@ -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(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 ( - - - {t('settings.tool.websearch.title')} - - - {t('settings.tool.websearch.search_provider')} -
- 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')})` - }))} - /> -
-
-
- {!isLocalProvider && ( - - {selectedProvider && } - - )} - - - -
+ + + + navigate('/settings/websearch/general')} + icon={} + titleStyle={{ fontWeight: 500 }} + /> + + {apiProviders.map((provider) => { + const logo = getProviderLogo(provider.id) + const isDefault = defaultProvider?.id === provider.id + return ( + navigate(`/settings/websearch/provider/${provider.id}`)} + icon={ + logo ? ( + {provider.name} + ) : ( +
+ ) + } + titleStyle={{ fontWeight: 500 }} + rightContent={ + isDefault ? ( + + {t('common.default')} + + ) : undefined + } + /> + ) + })} + {localProviders.length > 0 && ( + <> + + {localProviders.map((provider) => { + const logo = getProviderLogo(provider.id) + const isDefault = defaultProvider?.id === provider.id + return ( + navigate(`/settings/websearch/provider/${provider.id}`)} + icon={ + logo ? ( + {provider.name} + ) : ( +
+ ) + } + titleStyle={{ fontWeight: 500 }} + rightContent={ + isDefault ? ( + + {t('common.default')} + + ) : undefined + } + /> + ) + })} + + )} + + + + } /> + } /> + } /> + + + + ) } + +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