From e9ca1d54a0a5d30f16db0973927794452bb3c989 Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Mon, 11 Nov 2024 10:05:58 +0800 Subject: [PATCH] feat: add check all keys popup --- src/renderer/src/i18n/locales/en-us.json | 21 ++- src/renderer/src/i18n/locales/zh-cn.json | 33 +++-- src/renderer/src/i18n/locales/zh-tw.json | 33 +++-- .../ProviderSettings/ApiCheckPopup.tsx | 138 ++++++++++++++++++ .../ProviderSettings/ProviderSetting.tsx | 35 ++++- .../src/pages/settings/SettingsPage.tsx | 2 +- src/renderer/src/services/ApiService.ts | 7 - 7 files changed, 229 insertions(+), 40 deletions(-) create mode 100644 src/renderer/src/pages/settings/ProviderSettings/ApiCheckPopup.tsx diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index ae04ed88..a7155c0f 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -253,7 +253,6 @@ "title": "Settings", "general": "General Settings", "data": "Data Settings", - "provider": "Model Provider", "model": "Default Model", "assistant": "Default Assistant", "about": "About & Feedback", @@ -357,6 +356,26 @@ "zoom_in": "Zoom In", "zoom_out": "Zoom Out", "zoom_reset": "Reset Zoom" + }, + "provider": { + "title": "Model Provider", + "api_key": "API Key", + "api_key.tip": "Multiple keys separated by commas", + "check": "Check", + "get_api_key": "Get API Key", + "api_host": "API Host", + "api_version": "API Version", + "docs_check": "Check", + "docs_more_details": "for more details", + "search_placeholder": "Search model id or name", + "api.url.reset": "Reset", + "api.url.preview": "Preview: {{url}}", + "api.url.tip": "Ending with / ignores v1, ending with # forces use of input address", + "check_multiple_keys": "Check Multiple API Keys", + "check_all_keys": "Check All Keys", + "remove_invalid_keys": "Remove Invalid Keys", + "remove_duplicate_keys": "Remove Duplicate Keys", + "not_checked": "Not Checked" } }, "translate": { diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 18ca45e4..6cb98e67 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -253,7 +253,6 @@ "title": "设置", "general": "常规设置", "data": "数据设置", - "provider": "模型服务", "model": "默认模型", "assistant": "默认助手", "about": "关于我们", @@ -288,18 +287,6 @@ "data.webdav.restore.button": "从 WebDAV 恢复", "advanced.title": "高级设置", "advanced.click_assistant_switch_to_topics": "点击助手切换到话题", - "provider.api_key": "API 密钥", - "provider.api_key.tip": "多个密钥使用逗号分隔", - "provider.check": "检查", - "provider.get_api_key": "点击这里获取密钥", - "provider.api_host": "API 地址", - "provider.api_version": "API 版本", - "provider.docs_check": "查看", - "provider.docs_more_details": "获取更多详情", - "provider.search_placeholder": "搜索模型 ID 或名称", - "provider.api.url.reset": "重置", - "provider.api.url.preview": "预览: {{url}}", - "provider.api.url.tip": "/结尾忽略v1版本,#结尾制使用输入地址", "models.default_assistant_model": "默认助手模型", "models.topic_naming_model": "话题命名模型", "models.translate_model": "翻译模型", @@ -357,6 +344,26 @@ "zoom_in": "放大界面", "zoom_out": "缩小界面", "zoom_reset": "重置缩放" + }, + "provider": { + "title": "模型服务", + "api_key": "API 密钥", + "api_key.tip": "多个密钥使用逗号分隔", + "check": "检查", + "get_api_key": "点击这里获取密钥", + "api_host": "API 地址", + "api_version": "API 版本", + "docs_check": "查看", + "docs_more_details": "获取更多详情", + "search_placeholder": "搜索模型 ID 或名称", + "api.url.reset": "重置", + "api.url.preview": "预览: {{url}}", + "api.url.tip": "/结尾忽略v1版本,#结尾制使用输入地址", + "check_multiple_keys": "检查多个 API 密钥", + "check_all_keys": "检查所有密钥", + "remove_invalid_keys": "删除无效密钥", + "remove_duplicate_keys": "移除重复密钥", + "not_checked": "未检查" } }, "translate": { diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index d57fee85..73920a91 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -253,7 +253,6 @@ "title": "設定", "general": "一般設定", "data": "數據設定", - "provider": "模型提供者", "model": "預設模型", "assistant": "預設助手", "about": "關於與回饋", @@ -288,18 +287,6 @@ "data.webdav.restore.button": "從 WebDAV 恢復", "advanced.title": "進階設定", "advanced.click_assistant_switch_to_topics": "點擊助手切換到話題", - "provider.api_key": "API 密鑰", - "provider.api_key.tip": "多個密鑰使用逗號分隔", - "provider.check": "檢查", - "provider.get_api_key": "獲取 API 密鑰", - "provider.api_host": "API 主機地址", - "provider.api_version": "API 版本", - "provider.docs_check": "檢查", - "provider.docs_more_details": "查看更多細節", - "provider.search_placeholder": "搜尋模型 ID 或名稱", - "provider.api.url.reset": "重置", - "provider.api.url.preview": "預覽: {{url}}", - "provider.api.url.tip": "/結尾忽略v1版本,#結尾強制使用輸入位址", "models.default_assistant_model": "預設助手模型", "models.topic_naming_model": "話題命名模型", "models.translate_model": "翻譯模型", @@ -357,6 +344,26 @@ "zoom_in": "放大界面", "zoom_out": "縮小界面", "zoom_reset": "重置縮放" + }, + "provider": { + "title": "模型提供者", + "api_key": "API 密鑰", + "api_key.tip": "多個密鑰使用逗號分隔", + "check": "檢查", + "get_api_key": "獲取 API 密鑰", + "api_host": "API 主機地址", + "api_version": "API 版本", + "docs_check": "檢查", + "docs_more_details": "查看更多細節", + "search_placeholder": "搜尋模型 ID 或名稱", + "api.url.reset": "重置", + "api.url.preview": "預覽: {{url}}", + "api.url.tip": "/結尾忽略v1版本,#結尾強制使用輸入位址", + "check_multiple_keys": "檢查多個 API 密鑰", + "check_all_keys": "檢查所有密鑰", + "remove_invalid_keys": "刪除無效密鑰", + "remove_duplicate_keys": "移除重複密鑰", + "not_checked": "未檢查" } }, "translate": { diff --git a/src/renderer/src/pages/settings/ProviderSettings/ApiCheckPopup.tsx b/src/renderer/src/pages/settings/ProviderSettings/ApiCheckPopup.tsx new file mode 100644 index 00000000..f7ab2a72 --- /dev/null +++ b/src/renderer/src/pages/settings/ProviderSettings/ApiCheckPopup.tsx @@ -0,0 +1,138 @@ +import { CheckCircleFilled, CloseCircleFilled } from '@ant-design/icons' +import { TopView } from '@renderer/components/TopView' +import { useTheme } from '@renderer/context/ThemeProvider' +import { checkApi } from '@renderer/services/ApiService' +import { Button, List, Modal, Space, Typography } from 'antd' +import { useState } from 'react' +import { useTranslation } from 'react-i18next' + +interface ShowParams { + title: string + provider: any + apiKeys: string[] +} + +interface Props extends ShowParams { + resolve: (data: any) => void +} + +interface KeyStatus { + key: string + isValid?: boolean + checking?: boolean +} + +const PopupContainer: React.FC = ({ title, provider, apiKeys, resolve }) => { + const [open, setOpen] = useState(true) + const [keyStatuses, setKeyStatuses] = useState(() => { + const uniqueKeys = new Set(apiKeys) + return Array.from(uniqueKeys).map((key) => ({ key })) + }) + const { t } = useTranslation() + const { theme } = useTheme() + + const checkAllKeys = async () => { + const newStatuses = [...keyStatuses] + + for (let i = 0; i < newStatuses.length; i++) { + setKeyStatuses((prev) => prev.map((status, idx) => (idx === i ? { ...status, checking: true } : status))) + + const valid = await checkApi({ ...provider, apiKey: newStatuses[i].key }) + + setKeyStatuses((prev) => + prev.map((status, idx) => (idx === i ? { ...status, checking: false, isValid: valid } : status)) + ) + } + } + + const removeInvalidKeys = () => { + setKeyStatuses((prev) => prev.filter((status) => status.isValid !== false)) + } + + const onOk = () => { + const allKeys = keyStatuses.map((status) => status.key) + resolve({ validKeys: allKeys }) + setOpen(false) + } + + const onCancel = () => { + setOpen(false) + } + + const onClose = () => { + resolve({}) + } + + return ( + + + + + + + + + + }> + ( + + + + {status.key.slice(0, 8)}...{status.key.slice(-8)} + + + {status.checking && {t('settings.provider.check')}} + {status.isValid === true && } + {status.isValid === false && } + {status.isValid === undefined && !status.checking && {t('settings.provider.not_checked')}} + + + + )} + /> + + ) +} + +export default class ApiCheckPopup { + static topviewId = 0 + static hide() { + TopView.hide('ApiCheckPopup') + } + static show(props: ShowParams) { + return new Promise((resolve) => { + TopView.show( + { + resolve(v) + this.hide() + }} + />, + 'ApiCheckPopup' + ) + }) + } +} diff --git a/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx b/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx index 18df2c87..6fc5294a 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx @@ -11,6 +11,7 @@ import { getModelLogo, isVisionModel } from '@renderer/config/models' import { PROVIDER_CONFIG } from '@renderer/config/providers' import { useTheme } from '@renderer/context/ThemeProvider' import { useProvider } from '@renderer/hooks/useProvider' +import i18n from '@renderer/i18n' import { isOpenAIProvider } from '@renderer/providers/ProviderFactory' import { checkApi } from '@renderer/services/ApiService' import { Provider } from '@renderer/types' @@ -30,6 +31,7 @@ import { SettingTitle } from '..' import AddModelPopup from './AddModelPopup' +import ApiCheckPopup from './ApiCheckPopup' import EditModelsPopup from './EditModelsPopup' import GraphRAGSettings from './GraphRAGSettings' import OllamSettings from './OllamaSettings' @@ -63,11 +65,34 @@ const ProviderSetting: FC = ({ provider: _provider }) => { const onAddModel = () => AddModelPopup.show({ title: t('settings.models.add.add_model'), provider }) const onCheckApi = async () => { - setApiChecking(true) - const valid = await checkApi({ ...provider, apiKey, apiHost }) - setApiValid(valid) - setApiChecking(false) - setTimeout(() => setApiValid(false), 3000) + if (apiKey.includes(',')) { + const keys = apiKey + .split(',') + .map((k) => k.trim()) + .filter((k) => k) + const result = await ApiCheckPopup.show({ + title: t('settings.provider.check_multiple_keys'), + provider: { ...provider, apiHost }, + apiKeys: keys + }) + + if (result?.validKeys) { + setApiKey(result.validKeys.join(',')) + updateProvider({ ...provider, apiKey: result.validKeys.join(',') }) + } + } else { + setApiChecking(true) + const valid = await checkApi({ ...provider, apiKey, apiHost }) + window.message[valid ? 'success' : 'error']({ + key: 'api-check', + style: { marginTop: '3vh' }, + duration: valid ? 2 : 8, + content: valid ? i18n.t('message.api.connection.success') : i18n.t('message.api.connection.failed') + }) + setApiValid(valid) + setApiChecking(false) + setTimeout(() => setApiValid(false), 3000) + } } const providerConfig = PROVIDER_CONFIG[provider.id] diff --git a/src/renderer/src/pages/settings/SettingsPage.tsx b/src/renderer/src/pages/settings/SettingsPage.tsx index e1bd61f9..73273cdd 100644 --- a/src/renderer/src/pages/settings/SettingsPage.tsx +++ b/src/renderer/src/pages/settings/SettingsPage.tsx @@ -39,7 +39,7 @@ const SettingsPage: FC = () => { - {t('settings.provider')} + {t('settings.provider.title')} diff --git a/src/renderer/src/services/ApiService.ts b/src/renderer/src/services/ApiService.ts index 9a76a4e3..6517aa1d 100644 --- a/src/renderer/src/services/ApiService.ts +++ b/src/renderer/src/services/ApiService.ts @@ -213,13 +213,6 @@ export async function checkApi(provider: Provider) { const { valid } = await AI.check() - window.message[valid ? 'success' : 'error']({ - key: 'api-check', - style: { marginTop: '3vh' }, - duration: valid ? 2 : 8, - content: valid ? i18n.t('message.api.connection.success') : i18n.t('message.api.connection.failed') - }) - return valid }