From 1c5f63de7e38685bb6820a06975d70262c914003 Mon Sep 17 00:00:00 2001 From: zxhlyh Date: Tue, 23 May 2023 14:15:33 +0800 Subject: [PATCH] fix: azure-openai key validate (#164) --- .../provider-page/azure-provider/index.tsx | 69 ++++- .../account-setting/provider-page/index.tsx | 2 +- .../provider-page/openai-provider/index.tsx | 272 +++++------------- .../openai-provider/provider.tsx | 52 ---- .../provider-page/provider-input/Validate.tsx | 59 ++++ .../provider-page/provider-input/index.tsx | 103 +------ .../provider-input/useValidateToken.ts | 29 +- .../provider-page/provider-item/index.tsx | 25 +- web/models/common.ts | 29 +- 9 files changed, 254 insertions(+), 386 deletions(-) delete mode 100644 web/app/components/header/account-setting/provider-page/openai-provider/provider.tsx create mode 100644 web/app/components/header/account-setting/provider-page/provider-input/Validate.tsx diff --git a/web/app/components/header/account-setting/provider-page/azure-provider/index.tsx b/web/app/components/header/account-setting/provider-page/azure-provider/index.tsx index 5681fd2204..1cbe7d0674 100644 --- a/web/app/components/header/account-setting/provider-page/azure-provider/index.tsx +++ b/web/app/components/header/account-setting/provider-page/azure-provider/index.tsx @@ -1,10 +1,17 @@ import type { Provider, ProviderAzureToken } from '@/models/common' +import { ProviderName } from '@/models/common' import { useTranslation } from 'react-i18next' import Link from 'next/link' import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline' -import ProviderInput, { ProviderValidateTokenInput} from '../provider-input' -import { useState } from 'react' -import { ValidatedStatus } from '../provider-input/useValidateToken' +import { useState, useEffect } from 'react' +import ProviderInput from '../provider-input' +import useValidateToken, { ValidatedStatus } from '../provider-input/useValidateToken' +import { + ValidatedErrorIcon, + ValidatedSuccessIcon, + ValidatingTip, + ValidatedErrorOnAzureOpenaiTip +} from '../provider-input/Validate' interface IAzureProviderProps { provider: Provider @@ -17,19 +24,51 @@ const AzureProvider = ({ onValidatedStatus }: IAzureProviderProps) => { const { t } = useTranslation() - const [token, setToken] = useState(provider.token as ProviderAzureToken || {}) - const handleFocus = () => { - if (token === provider.token) { - token.openai_api_key = '' + const [token, setToken] = useState(provider.provider_name === ProviderName.AZURE_OPENAI ? {...provider.token}: {}) + const [ validating, validatedStatus, setValidatedStatus, validate ] = useValidateToken(provider.provider_name) + const handleFocus = (type: keyof ProviderAzureToken) => { + if (token[type] === (provider?.token as ProviderAzureToken)[type]) { + token[type] = '' setToken({...token}) onTokenChange({...token}) + setValidatedStatus(undefined) } } - const handleChange = (type: keyof ProviderAzureToken, v: string) => { + const handleChange = (type: keyof ProviderAzureToken, v: string, validate: any) => { token[type] = v setToken({...token}) onTokenChange({...token}) + validate({...token}, { + beforeValidating: () => { + if (!token.openai_api_base || !token.openai_api_key) { + setValidatedStatus(undefined) + return false + } + return true + } + }) } + const getValidatedIcon = () => { + if (validatedStatus === ValidatedStatus.Error || validatedStatus === ValidatedStatus.Exceed) { + return + } + if (validatedStatus === ValidatedStatus.Success) { + return + } + } + const getValidatedTip = () => { + if (validating) { + return + } + if (validatedStatus === ValidatedStatus.Error) { + return + } + } + useEffect(() => { + if (typeof onValidatedStatus === 'function') { + onValidatedStatus(validatedStatus) + } + }, [validatedStatus]) return (
@@ -38,17 +77,19 @@ const AzureProvider = ({ name={t('common.provider.azure.apiBase')} placeholder={t('common.provider.azure.apiBasePlaceholder')} value={token.openai_api_base} - onChange={(v) => handleChange('openai_api_base', v)} + onChange={(v) => handleChange('openai_api_base', v, validate)} + onFocus={() => handleFocus('openai_api_base')} + validatedIcon={getValidatedIcon()} /> - handleChange('openai_api_key', v)} - onFocus={handleFocus} - onValidatedStatus={onValidatedStatus} - providerName={provider.provider_name} + onChange={(v) => handleChange('openai_api_key', v, validate)} + onFocus={() => handleFocus('openai_api_key')} + validatedIcon={getValidatedIcon()} + validatedTip={getValidatedTip()} /> {t('common.provider.azure.helpTip')} diff --git a/web/app/components/header/account-setting/provider-page/index.tsx b/web/app/components/header/account-setting/provider-page/index.tsx index c1bce2c54c..67112b8142 100644 --- a/web/app/components/header/account-setting/provider-page/index.tsx +++ b/web/app/components/header/account-setting/provider-page/index.tsx @@ -67,7 +67,7 @@ const ProviderPage = () => { const providerHosted = data?.filter(provider => provider.provider_name === 'openai' && provider.provider_type === 'system')?.[0] return ( -
+
{ providerHosted && !IS_CE_EDITION && ( <> diff --git a/web/app/components/header/account-setting/provider-page/openai-provider/index.tsx b/web/app/components/header/account-setting/provider-page/openai-provider/index.tsx index adff6bdf30..f49b229812 100644 --- a/web/app/components/header/account-setting/provider-page/openai-provider/index.tsx +++ b/web/app/components/header/account-setting/provider-page/openai-provider/index.tsx @@ -1,222 +1,94 @@ -import { ChangeEvent, useEffect, useRef, useState } from 'react' -import { useContext } from 'use-context-selector' +import type { Provider } from '@/models/common' +import { useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' -import { debounce } from 'lodash-es' +import ProviderInput from '../provider-input' import Link from 'next/link' -import useSWR from 'swr' -import { ArrowTopRightOnSquareIcon, PencilIcon } from '@heroicons/react/24/outline' -import { CheckCircleIcon, ExclamationCircleIcon } from '@heroicons/react/24/solid' -import Button from '@/app/components/base/button' -import s from './index.module.css' -import classNames from 'classnames' -import { fetchTenantInfo, validateProviderKey, updateProviderAIKey } from '@/service/common' -import { ToastContext } from '@/app/components/base/toast' -import Indicator from '../../../indicator' -import I18n from '@/context/i18n' +import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline' +import useValidateToken, { ValidatedStatus } from '../provider-input/useValidateToken' +import { + ValidatedErrorIcon, + ValidatedSuccessIcon, + ValidatingTip, + ValidatedExceedOnOpenaiTip, + ValidatedErrorOnOpenaiTip +} from '../provider-input/Validate' -type IStatusType = 'normal' | 'verified' | 'error' | 'error-api-key-exceed-bill' - -type TInputWithStatusProps = { - value: string - onChange: (v: string) => void - onValidating: (validating: boolean) => void - verifiedStatus: IStatusType - onVerified: (verified: IStatusType) => void -} -const InputWithStatus = ({ - value, - onChange, - onValidating, - verifiedStatus, - onVerified -}: TInputWithStatusProps) => { - const { t } = useTranslation() - const validateKey = useRef(debounce(async (token: string) => { - if (!token) return - onValidating(true) - try { - const res = await validateProviderKey({ url: '/workspaces/current/providers/openai/token-validate', body: { token } }) - onVerified(res.result === 'success' ? 'verified' : 'error') - } catch (e: any) { - if (e.status === 400) { - e.json().then(({ code }: any) => { - if (code === 'provider_request_failed') { - onVerified('error-api-key-exceed-bill') - } - }) - } else { - onVerified('error') - } - } finally { - onValidating(false) - } - }, 500)) - - const handleChange = (e: ChangeEvent) => { - const inputValue = e.target.value - onChange(inputValue) - if (!inputValue) { - onVerified('normal') - } - validateKey.current(inputValue) - } - return ( -
- - { - verifiedStatus === 'error' && - } - { - verifiedStatus === 'verified' && - } -
- ) +interface IOpenaiProviderProps { + provider: Provider + onValidatedStatus: (status?: ValidatedStatus) => void + onTokenChange: (token: string) => void } -const OpenaiProvider = () => { +const OpenaiProvider = ({ + provider, + onValidatedStatus, + onTokenChange +}: IOpenaiProviderProps) => { const { t } = useTranslation() - const { locale } = useContext(I18n) - const { data: userInfo, mutate } = useSWR({ url: '/info' }, fetchTenantInfo) - const [inputValue, setInputValue] = useState('') - const [validating, setValidating] = useState(false) - const [editStatus, setEditStatus] = useState('normal') - const [loading, setLoading] = useState(false) - const [editing, setEditing] = useState(false) - const [invalidStatus, setInvalidStatus] = useState(false) - const { notify } = useContext(ToastContext) - const provider = userInfo?.providers?.find(({ provider }) => provider === 'openai') - - const handleReset = () => { - setInputValue('') - setValidating(false) - setEditStatus('normal') - setLoading(false) - setEditing(false) - } - const handleSave = async () => { - if (editStatus === 'verified') { - try { - setLoading(true) - await updateProviderAIKey({ url: '/workspaces/current/providers/openai/token', body: { token: inputValue ?? '' } }) - notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) - } catch (e) { - notify({ type: 'error', message: t('common.provider.saveFailed') }) - } finally { - setLoading(false) - handleReset() - mutate() - } + const [token, setToken] = useState(provider.token as string || '') + const [ validating, validatedStatus, setValidatedStatus, validate ] = useValidateToken(provider.provider_name) + const handleFocus = () => { + if (token === provider.token) { + setToken('') + onTokenChange('') + setValidatedStatus(undefined) } } + const handleChange = (v: string) => { + setToken(v) + onTokenChange(v) + validate(v, { + beforeValidating: () => { + if (!v) { + setValidatedStatus(undefined) + return false + } + return true + } + }) + } useEffect(() => { - if (provider && !provider.token_is_valid && provider.token_is_set) { - setInvalidStatus(true) + if (typeof onValidatedStatus === 'function') { + onValidatedStatus(validatedStatus) } - }, [userInfo]) + }, [validatedStatus]) - const showInvalidStatus = invalidStatus && !editing - const renderErrorMessage = () => { + const getValidatedIcon = () => { + if (validatedStatus === ValidatedStatus.Error || validatedStatus === ValidatedStatus.Exceed) { + return + } + if (validatedStatus === ValidatedStatus.Success) { + return + } + } + const getValidatedTip = () => { if (validating) { - return ( -
- {t('common.provider.validating')} -
- ) + return } - if (editStatus === 'error-api-key-exceed-bill') { - return ( -
- {t('common.provider.apiKeyExceedBill')}  - - {locale === 'en' ? 'this link' : '这篇文档'} - -
- ) + if (validatedStatus === ValidatedStatus.Exceed) { + return } - if (showInvalidStatus || editStatus === 'error') { - return ( -
- {t('common.provider.invalidKey')} -
- ) + if (validatedStatus === ValidatedStatus.Error) { + return } - return null } return (
-
-
- {t('common.provider.apiKey')} -
- { - provider && !editing && ( -
setEditing(true)} - > - - {t('common.operation.edit')} -
- ) - } - { - (inputValue || editing) && ( - <> - - - - ) - } -
- { - (!provider || (provider && editing)) && ( - setInputValue(v)} - verifiedStatus={editStatus} - onVerified={v => setEditStatus(v)} - onValidating={v => setValidating(v)} - /> - ) - } - { - (provider && !editing) && ( -
- sk-0C...skuA - -
- ) - } - {renderErrorMessage()} - - {t('appOverview.welcome.getKeyTip')} -
+ + + {t('appOverview.welcome.getKeyTip')} +
) } diff --git a/web/app/components/header/account-setting/provider-page/openai-provider/provider.tsx b/web/app/components/header/account-setting/provider-page/openai-provider/provider.tsx deleted file mode 100644 index 45747cb3a8..0000000000 --- a/web/app/components/header/account-setting/provider-page/openai-provider/provider.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import type { Provider } from '@/models/common' -import { useState } from 'react' -import { useTranslation } from 'react-i18next' -import { ProviderValidateTokenInput } from '../provider-input' -import Link from 'next/link' -import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline' -import { ValidatedStatus } from '../provider-input/useValidateToken' - -interface IOpenaiProviderProps { - provider: Provider - onValidatedStatus: (status?: ValidatedStatus) => void - onTokenChange: (token: string) => void -} - -const OpenaiProvider = ({ - provider, - onValidatedStatus, - onTokenChange -}: IOpenaiProviderProps) => { - const { t } = useTranslation() - const [token, setToken] = useState(provider.token as string || '') - const handleFocus = () => { - if (token === provider.token) { - setToken('') - onTokenChange('') - } - } - const handleChange = (v: string) => { - setToken(v) - onTokenChange(v) - } - - return ( -
- - - {t('appOverview.welcome.getKeyTip')} -
- ) -} - -export default OpenaiProvider \ No newline at end of file diff --git a/web/app/components/header/account-setting/provider-page/provider-input/Validate.tsx b/web/app/components/header/account-setting/provider-page/provider-input/Validate.tsx new file mode 100644 index 0000000000..740a149a93 --- /dev/null +++ b/web/app/components/header/account-setting/provider-page/provider-input/Validate.tsx @@ -0,0 +1,59 @@ +import Link from 'next/link' +import { CheckCircleIcon, ExclamationCircleIcon } from '@heroicons/react/24/solid' +import { useTranslation } from 'react-i18next' +import { useContext } from 'use-context-selector' +import I18n from '@/context/i18n' + +export const ValidatedErrorIcon = () => { + return +} + +export const ValidatedSuccessIcon = () => { + return +} + +export const ValidatingTip = () => { + const { t } = useTranslation() + return ( +
+ {t('common.provider.validating')} +
+ ) +} + +export const ValidatedExceedOnOpenaiTip = () => { + const { t } = useTranslation() + const { locale } = useContext(I18n) + + return ( +
+ {t('common.provider.apiKeyExceedBill')}  + + {locale === 'en' ? 'this link' : '这篇文档'} + +
+ ) +} + +export const ValidatedErrorOnOpenaiTip = () => { + const { t } = useTranslation() + + return ( +
+ {t('common.provider.invalidKey')} +
+ ) +} + +export const ValidatedErrorOnAzureOpenaiTip = () => { + const { t } = useTranslation() + + return ( +
+ {t('common.provider.invalidApiKey')} +
+ ) +} \ No newline at end of file diff --git a/web/app/components/header/account-setting/provider-page/provider-input/index.tsx b/web/app/components/header/account-setting/provider-page/provider-input/index.tsx index 5a489d17f2..84ab9901c1 100644 --- a/web/app/components/header/account-setting/provider-page/provider-input/index.tsx +++ b/web/app/components/header/account-setting/provider-page/provider-input/index.tsx @@ -1,10 +1,5 @@ -import { ChangeEvent, useEffect } from 'react' -import Link from 'next/link' -import { CheckCircleIcon, ExclamationCircleIcon } from '@heroicons/react/24/solid' -import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' -import I18n from '@/context/i18n' -import useValidateToken, { ValidatedStatus } from './useValidateToken' +import { ChangeEvent } from 'react' +import { ReactElement } from 'react-markdown/lib/react-markdown' interface IProviderInputProps { value?: string @@ -13,6 +8,8 @@ interface IProviderInputProps { className?: string onChange: (v: string) => void onFocus?: () => void + validatedIcon?: ReactElement + validatedTip?: ReactElement } const ProviderInput = ({ @@ -22,6 +19,8 @@ const ProviderInput = ({ className, onChange, onFocus, + validatedIcon, + validatedTip }: IProviderInputProps) => { const handleChange = (e: ChangeEvent) => { @@ -47,95 +46,9 @@ const ProviderInput = ({ onChange={handleChange} onFocus={onFocus} /> + {validatedIcon}
-
- ) -} - -type TproviderInputProps = IProviderInputProps - & { - onValidatedStatus?: (status?: ValidatedStatus) => void - providerName: string - } -export const ProviderValidateTokenInput = ({ - value, - name, - placeholder, - className, - onChange, - onFocus, - onValidatedStatus, - providerName -}: TproviderInputProps) => { - const { t } = useTranslation() - const { locale } = useContext(I18n) - const [ validating, validatedStatus, validate ] = useValidateToken(providerName) - - useEffect(() => { - if (typeof onValidatedStatus === 'function') { - onValidatedStatus(validatedStatus) - } - }, [validatedStatus]) - - const handleChange = (e: ChangeEvent) => { - const inputValue = e.target.value - onChange(inputValue) - - validate(inputValue) - } - - return ( -
-
{name}
-
- - { - validatedStatus === ValidatedStatus.Error && - } - { - validatedStatus === ValidatedStatus.Success && - } -
- { - validating && ( -
- {t('common.provider.validating')} -
- ) - } - { - validatedStatus === ValidatedStatus.Exceed && !validating && ( -
- {t('common.provider.apiKeyExceedBill')}  - - {locale === 'en' ? 'this link' : '这篇文档'} - -
- ) - } - { - validatedStatus === ValidatedStatus.Error && !validating && ( -
- {t('common.provider.invalidKey')} -
- ) - } + {validatedTip}
) } diff --git a/web/app/components/header/account-setting/provider-page/provider-input/useValidateToken.ts b/web/app/components/header/account-setting/provider-page/provider-input/useValidateToken.ts index 5064910671..69b7529449 100644 --- a/web/app/components/header/account-setting/provider-page/provider-input/useValidateToken.ts +++ b/web/app/components/header/account-setting/provider-page/provider-input/useValidateToken.ts @@ -1,4 +1,4 @@ -import { useState, useCallback } from 'react' +import { useState, useCallback, SetStateAction, Dispatch } from 'react' import debounce from 'lodash-es/debounce' import { DebouncedFunc } from 'lodash-es' import { validateProviderKey } from '@/service/common' @@ -8,14 +8,24 @@ export enum ValidatedStatus { Error = 'error', Exceed = 'exceed' } +export type SetValidatedStatus = Dispatch> +export type ValidateFn = DebouncedFunc<(token: any, config: ValidateFnConfig) => void> +type ValidateTokenReturn = [ + boolean, + ValidatedStatus | undefined, + SetValidatedStatus, + ValidateFn +] +export type ValidateFnConfig = { + beforeValidating: (token: any) => boolean +} -const useValidateToken = (providerName: string): [boolean, ValidatedStatus | undefined, DebouncedFunc<(token: string) => Promise>] => { +const useValidateToken = (providerName: string): ValidateTokenReturn => { const [validating, setValidating] = useState(false) const [validatedStatus, setValidatedStatus] = useState() - const validate = useCallback(debounce(async (token: string) => { - if (!token) { - setValidatedStatus(undefined) - return + const validate = useCallback(debounce(async (token: string, config: ValidateFnConfig) => { + if (!config.beforeValidating(token)) { + return false } setValidating(true) try { @@ -24,8 +34,10 @@ const useValidateToken = (providerName: string): [boolean, ValidatedStatus | und } catch (e: any) { if (e.status === 400) { e.json().then(({ code }: any) => { - if (code === 'provider_request_failed') { + if (code === 'provider_request_failed' && providerName === 'openai') { setValidatedStatus(ValidatedStatus.Exceed) + } else { + setValidatedStatus(ValidatedStatus.Error) } }) } else { @@ -39,7 +51,8 @@ const useValidateToken = (providerName: string): [boolean, ValidatedStatus | und return [ validating, validatedStatus, - validate, + setValidatedStatus, + validate ] } diff --git a/web/app/components/header/account-setting/provider-page/provider-item/index.tsx b/web/app/components/header/account-setting/provider-page/provider-item/index.tsx index 6a3cf85846..14f8c3f5c3 100644 --- a/web/app/components/header/account-setting/provider-page/provider-item/index.tsx +++ b/web/app/components/header/account-setting/provider-page/provider-item/index.tsx @@ -5,7 +5,8 @@ import { useContext } from 'use-context-selector' import Indicator from '../../../indicator' import { useTranslation } from 'react-i18next' import type { Provider, ProviderAzureToken } from '@/models/common' -import OpenaiProvider from '../openai-provider/provider' +import { ProviderName } from '@/models/common' +import OpenaiProvider from '../openai-provider' import AzureProvider from '../azure-provider' import { ValidatedStatus } from '../provider-input/useValidateToken' import { updateProviderAIKey } from '@/service/common' @@ -38,13 +39,23 @@ const ProviderItem = ({ ) const id = `${provider.provider_name}-${provider.provider_type}` const isOpen = id === activeId - const providerKey = provider.provider_name === 'azure_openai' ? (provider.token as ProviderAzureToken)?.openai_api_key : provider.token const comingSoon = false const isValid = provider.is_valid + const providerTokenHasSetted = () => { + if (provider.provider_name === ProviderName.AZURE_OPENAI) { + return provider.token && provider.token.openai_api_base && provider.token.openai_api_key ? { + openai_api_base: provider.token.openai_api_base, + openai_api_key: provider.token.openai_api_key + }: undefined + } + if (provider.provider_name === ProviderName.OPENAI) { + return provider.token + } + } const handleUpdateToken = async () => { if (loading) return - if (validatedStatus === ValidatedStatus.Success || !token) { + if (validatedStatus === ValidatedStatus.Success) { try { setLoading(true) await updateProviderAIKey({ url: `/workspaces/current/providers/${provider.provider_name}/token`, body: { token } }) @@ -65,7 +76,7 @@ const ProviderItem = ({
{name}
{ - providerKey && !comingSoon && !isOpen && ( + providerTokenHasSetted() && !comingSoon && !isOpen && (
{!isValid &&
{t('common.provider.invalidApiKey')}
} @@ -78,7 +89,7 @@ const ProviderItem = ({ px-3 h-[28px] bg-white border border-gray-200 rounded-md cursor-pointer text-xs font-medium text-gray-700 flex items-center ' onClick={() => onActive(id)}> - {providerKey ? t('common.provider.editKey') : t('common.provider.addKey')} + {providerTokenHasSetted() ? t('common.provider.editKey') : t('common.provider.addKey')}
) } @@ -114,7 +125,7 @@ const ProviderItem = ({ }
{ - provider.provider_name === 'openai' && isOpen && ( + provider.provider_name === ProviderName.OPENAI && isOpen && ( setValidatedStatus(v)} @@ -123,7 +134,7 @@ const ProviderItem = ({ ) } { - provider.provider_name === 'azure_openai' && isOpen && ( + provider.provider_name === ProviderName.AZURE_OPENAI && isOpen && ( setValidatedStatus(v)} diff --git a/web/models/common.ts b/web/models/common.ts index adce856fd1..df19701a8d 100644 --- a/web/models/common.ts +++ b/web/models/common.ts @@ -54,18 +54,29 @@ export type Member = Pick