diff --git a/src/renderer/src/pages/settings/components/ProviderSetting.tsx b/src/renderer/src/pages/settings/components/ProviderSetting.tsx index ca3920b6..3ede6490 100644 --- a/src/renderer/src/pages/settings/components/ProviderSetting.tsx +++ b/src/renderer/src/pages/settings/components/ProviderSetting.tsx @@ -1,15 +1,16 @@ import { Provider } from '@renderer/types' import { FC, useEffect, useState } from 'react' import styled from 'styled-components' -import { Avatar, Button, Card, Divider, Flex, Input, Switch } from 'antd' +import { Avatar, Button, Card, Divider, Flex, Input, Space, Switch } from 'antd' import { useProvider } from '@renderer/hooks/useProvider' import { groupBy } from 'lodash' import { SettingContainer, SettingSubtitle, SettingTitle } from '.' import { getModelLogo } from '@renderer/services/provider' -import { EditOutlined, ExportOutlined, PlusOutlined } from '@ant-design/icons' +import { CheckOutlined, EditOutlined, ExportOutlined, LoadingOutlined, PlusOutlined } from '@ant-design/icons' import AddModelPopup from './AddModelPopup' import EditModelsPopup from './EditModelsPopup' import Link from 'antd/es/typography/Link' +import { checkApi } from '@renderer/services/api' interface Props { provider: Provider @@ -92,6 +93,8 @@ const PROVIDER_CONFIG = { const ProviderSetting: FC = ({ provider }) => { const [apiKey, setApiKey] = useState(provider.apiKey) const [apiHost, setApiHost] = useState(provider.apiHost) + const [apiValid, setApiValid] = useState(false) + const [apiChecking, setApiChecking] = useState(false) const { updateProvider, models } = useProvider(provider.id) const modelGroups = groupBy(models, 'group') @@ -106,12 +109,22 @@ const ProviderSetting: FC = ({ provider }) => { const onManageModel = () => EditModelsPopup.show({ provider }) const onAddModel = () => AddModelPopup.show({ title: 'Add Model', provider }) + const onCheckApi = async () => { + setApiChecking(true) + const valid = await checkApi({ ...provider, apiKey, apiHost }) + setApiValid(valid) + setApiChecking(false) + setTimeout(() => setApiValid(false), 3000) + } + const providerConfig = PROVIDER_CONFIG[provider.id] const officialWebsite = providerConfig?.websites?.official const apiKeyWebsite = providerConfig?.websites?.apiKey const docsWebsite = providerConfig?.websites?.docs const modelsWebsite = providerConfig?.websites?.models + const apiKeyDisabled = provider.id === 'ollama' + return ( @@ -131,15 +144,22 @@ const ProviderSetting: FC = ({ provider }) => { API Key - setApiKey(e.target.value)} - onBlur={onUpdateApiKey} - spellCheck={false} - disabled={provider.id === 'ollama'} - autoFocus={provider.enabled && apiKey === ''} - /> + + setApiKey(e.target.value)} + onBlur={onUpdateApiKey} + spellCheck={false} + disabled={apiKeyDisabled} + autoFocus={provider.enabled && apiKey === ''} + /> + {!apiKeyDisabled && ( + + )} + {apiKeyWebsite && ( Get API key from: diff --git a/src/renderer/src/pages/settings/components/index.tsx b/src/renderer/src/pages/settings/components/index.tsx index fff538e5..986ed39d 100644 --- a/src/renderer/src/pages/settings/components/index.tsx +++ b/src/renderer/src/pages/settings/components/index.tsx @@ -20,12 +20,14 @@ export const SettingTitle = styled.div` justify-content: space-between; align-items: center; font-weight: 900; + user-select: none; ` export const SettingSubtitle = styled.div` font-size: 12px; color: var(--color-text-2); margin: 15px 0 10px 0; + user-select: none; ` export const SettingDivider = styled(Divider)` diff --git a/src/renderer/src/services/api.ts b/src/renderer/src/services/api.ts index 465d50f4..3af2c34d 100644 --- a/src/renderer/src/services/api.ts +++ b/src/renderer/src/services/api.ts @@ -104,6 +104,53 @@ export async function fetchMessagesSummary({ messages, assistant }: FetchMessage return response.choices[0].message?.content } +export async function checkApi(provider: Provider) { + const openaiProvider = getOpenAiProvider(provider) + const model = provider.models[0] + const key = 'api-check' + const style = { marginTop: '3vh' } + + if (!provider.apiKey) { + window.message.error({ content: 'Please enter your API key first', key, style }) + return false + } + + if (!provider.apiHost) { + window.message.error({ content: 'Please enter your API host first', key, style }) + return false + } + + if (!model) { + window.message.error({ content: 'Please select a model first', key, style }) + return false + } + + let valid = false + let errorMessage = '' + + try { + const response = await openaiProvider.chat.completions.create({ + model: model.id, + messages: [{ role: 'user', content: 'hello' }], + stream: false + }) + + valid = Boolean(response?.choices[0].message) + } catch (error) { + errorMessage = (error as Error).message + valid = false + } + + window.message[valid ? 'success' : 'error']({ + key: 'api-check', + style: { marginTop: '3vh' }, + duration: valid ? 2 : 8, + content: valid ? 'API connection successful' : 'API connection failed ' + errorMessage + }) + + return valid +} + export async function fetchModels(provider: Provider) { try { const openaiProvider = getOpenAiProvider(provider)