feat: add check all keys popup
This commit is contained in:
parent
1ff8fe0c2e
commit
e9ca1d54a0
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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'
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -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]
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user