feat: add check all keys popup

This commit is contained in:
kangfenmao 2024-11-11 10:05:58 +08:00
parent 1ff8fe0c2e
commit e9ca1d54a0
7 changed files with 229 additions and 40 deletions

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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<Props> = ({ title, provider, apiKeys, resolve }) => {
const [open, setOpen] = useState(true)
const [keyStatuses, setKeyStatuses] = useState<KeyStatus[]>(() => {
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 (
<Modal
title={title}
open={open}
onOk={onOk}
onCancel={onCancel}
afterClose={onClose}
centered
maskClosable={false}
maskProps={{
style: {
backgroundColor: theme === 'dark' ? 'rgba(0, 0, 0, 0.9)' : 'rgba(255, 255, 255, 0.9)'
}
}}
footer={
<Space style={{ display: 'flex', justifyContent: 'space-between' }}>
<Space>
<Button key="remove" danger onClick={removeInvalidKeys}>
{t('settings.provider.remove_invalid_keys')}
</Button>
</Space>
<Space>
<Button key="check" type="primary" ghost onClick={checkAllKeys}>
{t('settings.provider.check_all_keys')}
</Button>
<Button key="save" type="primary" onClick={onOk}>
{t('common.save')}
</Button>
</Space>
</Space>
}>
<List
dataSource={keyStatuses}
renderItem={(status) => (
<List.Item>
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
<Typography.Text copyable={{ text: status.key }}>
{status.key.slice(0, 8)}...{status.key.slice(-8)}
</Typography.Text>
<Space>
{status.checking && <span>{t('settings.provider.check')}</span>}
{status.isValid === true && <CheckCircleFilled style={{ color: '#52c41a' }} />}
{status.isValid === false && <CloseCircleFilled style={{ color: '#ff4d4f' }} />}
{status.isValid === undefined && !status.checking && <span>{t('settings.provider.not_checked')}</span>}
</Space>
</Space>
</List.Item>
)}
/>
</Modal>
)
}
export default class ApiCheckPopup {
static topviewId = 0
static hide() {
TopView.hide('ApiCheckPopup')
}
static show(props: ShowParams) {
return new Promise<any>((resolve) => {
TopView.show(
<PopupContainer
{...props}
resolve={(v) => {
resolve(v)
this.hide()
}}
/>,
'ApiCheckPopup'
)
})
}
}

View File

@ -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<Props> = ({ 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]

View File

@ -39,7 +39,7 @@ const SettingsPage: FC = () => {
<MenuItemLink to="/settings/provider">
<MenuItem className={isRoute('/settings/provider')}>
<CloudOutlined />
{t('settings.provider')}
{t('settings.provider.title')}
</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/model">

View File

@ -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
}