| @@ -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<ProviderAzureToken>(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 <ValidatedErrorIcon /> | |||
| } | |||
| if (validatedStatus === ValidatedStatus.Success) { | |||
| return <ValidatedSuccessIcon /> | |||
| } | |||
| } | |||
| const getValidatedTip = () => { | |||
| if (validating) { | |||
| return <ValidatingTip /> | |||
| } | |||
| if (validatedStatus === ValidatedStatus.Error) { | |||
| return <ValidatedErrorOnAzureOpenaiTip /> | |||
| } | |||
| } | |||
| useEffect(() => { | |||
| if (typeof onValidatedStatus === 'function') { | |||
| onValidatedStatus(validatedStatus) | |||
| } | |||
| }, [validatedStatus]) | |||
| return ( | |||
| <div className='px-4 py-3'> | |||
| @@ -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()} | |||
| /> | |||
| <ProviderValidateTokenInput | |||
| <ProviderInput | |||
| className='mb-4' | |||
| name={t('common.provider.azure.apiKey')} | |||
| placeholder={t('common.provider.azure.apiKeyPlaceholder')} | |||
| value={token.openai_api_key} | |||
| onChange={v => 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()} | |||
| /> | |||
| <Link className="flex items-center text-xs cursor-pointer text-primary-600" href="https://platform.openai.com/account/api-keys" target={'_blank'}> | |||
| {t('common.provider.azure.helpTip')} | |||
| @@ -67,7 +67,7 @@ const ProviderPage = () => { | |||
| const providerHosted = data?.filter(provider => provider.provider_name === 'openai' && provider.provider_type === 'system')?.[0] | |||
| return ( | |||
| <div> | |||
| <div className='pb-7'> | |||
| { | |||
| providerHosted && !IS_CE_EDITION && ( | |||
| <> | |||
| @@ -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<HTMLInputElement>) => { | |||
| const inputValue = e.target.value | |||
| onChange(inputValue) | |||
| if (!inputValue) { | |||
| onVerified('normal') | |||
| } | |||
| validateKey.current(inputValue) | |||
| } | |||
| return ( | |||
| <div className={classNames('flex items-center h-9 px-3 bg-white border border-gray-300 rounded-lg', s.input)}> | |||
| <input | |||
| value={value} | |||
| placeholder={t('common.provider.enterYourKey') || ''} | |||
| className='w-full h-9 mr-2 appearance-none outline-none bg-transparent text-xs' | |||
| onChange={handleChange} | |||
| /> | |||
| { | |||
| verifiedStatus === 'error' && <ExclamationCircleIcon className='w-4 h-4 text-[#D92D20]' /> | |||
| } | |||
| { | |||
| verifiedStatus === 'verified' && <CheckCircleIcon className='w-4 h-4 text-[#039855]' /> | |||
| } | |||
| </div> | |||
| ) | |||
| 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<string>('') | |||
| const [validating, setValidating] = useState(false) | |||
| const [editStatus, setEditStatus] = useState<IStatusType>('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 [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 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 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 <ValidatedErrorIcon /> | |||
| } | |||
| if (validatedStatus === ValidatedStatus.Success) { | |||
| return <ValidatedSuccessIcon /> | |||
| } | |||
| } | |||
| const getValidatedTip = () => { | |||
| if (validating) { | |||
| return ( | |||
| <div className={`mt-2 text-primary-600 text-xs font-normal`}> | |||
| {t('common.provider.validating')} | |||
| </div> | |||
| ) | |||
| return <ValidatingTip /> | |||
| } | |||
| if (editStatus === 'error-api-key-exceed-bill') { | |||
| return ( | |||
| <div className={`mt-2 text-[#D92D20] text-xs font-normal`}> | |||
| {t('common.provider.apiKeyExceedBill')} | |||
| <Link | |||
| className='underline' | |||
| href="https://platform.openai.com/account/api-keys" | |||
| target={'_blank'}> | |||
| {locale === 'en' ? 'this link' : '这篇文档'} | |||
| </Link> | |||
| </div> | |||
| ) | |||
| if (validatedStatus === ValidatedStatus.Exceed) { | |||
| return <ValidatedExceedOnOpenaiTip /> | |||
| } | |||
| if (showInvalidStatus || editStatus === 'error') { | |||
| return ( | |||
| <div className={`mt-2 text-[#D92D20] text-xs font-normal`}> | |||
| {t('common.provider.invalidKey')} | |||
| </div> | |||
| ) | |||
| if (validatedStatus === ValidatedStatus.Error) { | |||
| return <ValidatedErrorOnOpenaiTip /> | |||
| } | |||
| return null | |||
| } | |||
| return ( | |||
| <div className='px-4 pt-3 pb-4'> | |||
| <div className='flex items-center mb-2 h-6'> | |||
| <div className='grow text-[13px] text-gray-800 font-medium'> | |||
| {t('common.provider.apiKey')} | |||
| </div> | |||
| { | |||
| provider && !editing && ( | |||
| <div | |||
| className=' | |||
| flex items-center h-6 px-2 rounded-md border border-gray-200 | |||
| text-xs font-medium text-gray-700 cursor-pointer | |||
| ' | |||
| onClick={() => setEditing(true)} | |||
| > | |||
| <PencilIcon className='mr-1 w-3 h-3 text-gray-500' /> | |||
| {t('common.operation.edit')} | |||
| </div> | |||
| ) | |||
| } | |||
| { | |||
| (inputValue || editing) && ( | |||
| <> | |||
| <Button | |||
| className={classNames('mr-1', s.button)} | |||
| loading={loading} | |||
| onClick={handleReset} | |||
| > | |||
| {t('common.operation.cancel')} | |||
| </Button> | |||
| <Button | |||
| type='primary' | |||
| className={classNames(s.button)} | |||
| loading={loading} | |||
| onClick={handleSave}> | |||
| {t('common.operation.save')} | |||
| </Button> | |||
| </> | |||
| ) | |||
| } | |||
| </div> | |||
| { | |||
| (!provider || (provider && editing)) && ( | |||
| <InputWithStatus | |||
| value={inputValue} | |||
| onChange={v => setInputValue(v)} | |||
| verifiedStatus={editStatus} | |||
| onVerified={v => setEditStatus(v)} | |||
| onValidating={v => setValidating(v)} | |||
| /> | |||
| ) | |||
| } | |||
| { | |||
| (provider && !editing) && ( | |||
| <div className={classNames('flex justify-between items-center bg-white px-3 h-9 rounded-lg text-gray-800 text-xs font-medium', s.input)}> | |||
| sk-0C...skuA | |||
| <Indicator color={(provider.token_is_set && provider.token_is_valid) ? 'green' : 'orange'} /> | |||
| </div> | |||
| ) | |||
| } | |||
| {renderErrorMessage()} | |||
| <Link className="inline-flex items-center mt-3 text-xs font-normal cursor-pointer text-primary-600 w-fit" href="https://platform.openai.com/account/api-keys" target={'_blank'}> | |||
| {t('appOverview.welcome.getKeyTip')} | |||
| <ArrowTopRightOnSquareIcon className='w-3 h-3 ml-1 text-primary-600' aria-hidden="true" /> | |||
| </Link> | |||
| </div> | |||
| <ProviderInput | |||
| value={token} | |||
| name={t('common.provider.apiKey')} | |||
| placeholder={t('common.provider.enterYourKey')} | |||
| onChange={handleChange} | |||
| onFocus={handleFocus} | |||
| validatedIcon={getValidatedIcon()} | |||
| validatedTip={getValidatedTip()} | |||
| /> | |||
| <Link className="inline-flex items-center mt-3 text-xs font-normal cursor-pointer text-primary-600 w-fit" href="https://platform.openai.com/account/api-keys" target={'_blank'}> | |||
| {t('appOverview.welcome.getKeyTip')} | |||
| <ArrowTopRightOnSquareIcon className='w-3 h-3 ml-1 text-primary-600' aria-hidden="true" /> | |||
| </Link> | |||
| </div> | |||
| ) | |||
| } | |||
| @@ -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 ( | |||
| <div className='px-4 pt-3 pb-4'> | |||
| <ProviderValidateTokenInput | |||
| value={token} | |||
| name={t('common.provider.apiKey')} | |||
| placeholder={t('common.provider.enterYourKey')} | |||
| onChange={handleChange} | |||
| onFocus={handleFocus} | |||
| onValidatedStatus={onValidatedStatus} | |||
| providerName={provider.provider_name} | |||
| /> | |||
| <Link className="inline-flex items-center mt-3 text-xs font-normal cursor-pointer text-primary-600 w-fit" href="https://platform.openai.com/account/api-keys" target={'_blank'}> | |||
| {t('appOverview.welcome.getKeyTip')} | |||
| <ArrowTopRightOnSquareIcon className='w-3 h-3 ml-1 text-primary-600' aria-hidden="true" /> | |||
| </Link> | |||
| </div> | |||
| ) | |||
| } | |||
| export default OpenaiProvider | |||
| @@ -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 <ExclamationCircleIcon className='w-4 h-4 text-[#D92D20]' /> | |||
| } | |||
| export const ValidatedSuccessIcon = () => { | |||
| return <CheckCircleIcon className='w-4 h-4 text-[#039855]' /> | |||
| } | |||
| export const ValidatingTip = () => { | |||
| const { t } = useTranslation() | |||
| return ( | |||
| <div className={`mt-2 text-primary-600 text-xs font-normal`}> | |||
| {t('common.provider.validating')} | |||
| </div> | |||
| ) | |||
| } | |||
| export const ValidatedExceedOnOpenaiTip = () => { | |||
| const { t } = useTranslation() | |||
| const { locale } = useContext(I18n) | |||
| return ( | |||
| <div className={`mt-2 text-[#D92D20] text-xs font-normal`}> | |||
| {t('common.provider.apiKeyExceedBill')} | |||
| <Link | |||
| className='underline' | |||
| href="https://platform.openai.com/account/api-keys" | |||
| target={'_blank'}> | |||
| {locale === 'en' ? 'this link' : '这篇文档'} | |||
| </Link> | |||
| </div> | |||
| ) | |||
| } | |||
| export const ValidatedErrorOnOpenaiTip = () => { | |||
| const { t } = useTranslation() | |||
| return ( | |||
| <div className={`mt-2 text-[#D92D20] text-xs font-normal`}> | |||
| {t('common.provider.invalidKey')} | |||
| </div> | |||
| ) | |||
| } | |||
| export const ValidatedErrorOnAzureOpenaiTip = () => { | |||
| const { t } = useTranslation() | |||
| return ( | |||
| <div className={`mt-2 text-[#D92D20] text-xs font-normal`}> | |||
| {t('common.provider.invalidApiKey')} | |||
| </div> | |||
| ) | |||
| } | |||
| @@ -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<HTMLInputElement>) => { | |||
| @@ -47,95 +46,9 @@ const ProviderInput = ({ | |||
| onChange={handleChange} | |||
| onFocus={onFocus} | |||
| /> | |||
| {validatedIcon} | |||
| </div> | |||
| </div> | |||
| ) | |||
| } | |||
| 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<HTMLInputElement>) => { | |||
| const inputValue = e.target.value | |||
| onChange(inputValue) | |||
| validate(inputValue) | |||
| } | |||
| return ( | |||
| <div className={className}> | |||
| <div className="mb-2 text-[13px] font-medium text-gray-800">{name}</div> | |||
| <div className=' | |||
| flex items-center px-3 bg-white rounded-lg | |||
| shadow-[0_1px_2px_rgba(16,24,40,0.05)] | |||
| '> | |||
| <input | |||
| className=' | |||
| w-full py-[9px] | |||
| text-xs font-medium text-gray-700 leading-[18px] | |||
| appearance-none outline-none bg-transparent | |||
| ' | |||
| value={value} | |||
| placeholder={placeholder} | |||
| onChange={handleChange} | |||
| onFocus={onFocus} | |||
| /> | |||
| { | |||
| validatedStatus === ValidatedStatus.Error && <ExclamationCircleIcon className='w-4 h-4 text-[#D92D20]' /> | |||
| } | |||
| { | |||
| validatedStatus === ValidatedStatus.Success && <CheckCircleIcon className='w-4 h-4 text-[#039855]' /> | |||
| } | |||
| </div> | |||
| { | |||
| validating && ( | |||
| <div className={`mt-2 text-primary-600 text-xs font-normal`}> | |||
| {t('common.provider.validating')} | |||
| </div> | |||
| ) | |||
| } | |||
| { | |||
| validatedStatus === ValidatedStatus.Exceed && !validating && ( | |||
| <div className={`mt-2 text-[#D92D20] text-xs font-normal`}> | |||
| {t('common.provider.apiKeyExceedBill')} | |||
| <Link | |||
| className='underline' | |||
| href="https://platform.openai.com/account/api-keys" | |||
| target={'_blank'}> | |||
| {locale === 'en' ? 'this link' : '这篇文档'} | |||
| </Link> | |||
| </div> | |||
| ) | |||
| } | |||
| { | |||
| validatedStatus === ValidatedStatus.Error && !validating && ( | |||
| <div className={`mt-2 text-[#D92D20] text-xs font-normal`}> | |||
| {t('common.provider.invalidKey')} | |||
| </div> | |||
| ) | |||
| } | |||
| {validatedTip} | |||
| </div> | |||
| ) | |||
| } | |||
| @@ -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<SetStateAction<ValidatedStatus | undefined>> | |||
| 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<void>>] => { | |||
| const useValidateToken = (providerName: string): ValidateTokenReturn => { | |||
| const [validating, setValidating] = useState(false) | |||
| const [validatedStatus, setValidatedStatus] = useState<ValidatedStatus | undefined>() | |||
| 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 | |||
| ] | |||
| } | |||
| @@ -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 = ({ | |||
| <div className={cn(s[`icon-${icon}`], 'mr-3 w-6 h-6 rounded-md')} /> | |||
| <div className='grow text-sm font-medium text-gray-800'>{name}</div> | |||
| { | |||
| providerKey && !comingSoon && !isOpen && ( | |||
| providerTokenHasSetted() && !comingSoon && !isOpen && ( | |||
| <div className='flex items-center mr-4'> | |||
| {!isValid && <div className='text-xs text-[#D92D20]'>{t('common.provider.invalidApiKey')}</div>} | |||
| <Indicator color={!isValid ? 'red' : 'green'} className='ml-2' /> | |||
| @@ -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')} | |||
| </div> | |||
| ) | |||
| } | |||
| @@ -114,7 +125,7 @@ const ProviderItem = ({ | |||
| } | |||
| </div> | |||
| { | |||
| provider.provider_name === 'openai' && isOpen && ( | |||
| provider.provider_name === ProviderName.OPENAI && isOpen && ( | |||
| <OpenaiProvider | |||
| provider={provider} | |||
| onValidatedStatus={v => setValidatedStatus(v)} | |||
| @@ -123,7 +134,7 @@ const ProviderItem = ({ | |||
| ) | |||
| } | |||
| { | |||
| provider.provider_name === 'azure_openai' && isOpen && ( | |||
| provider.provider_name === ProviderName.AZURE_OPENAI && isOpen && ( | |||
| <AzureProvider | |||
| provider={provider} | |||
| onValidatedStatus={v => setValidatedStatus(v)} | |||
| @@ -54,18 +54,29 @@ export type Member = Pick<UserProfileResponse, 'id' | 'name' | 'email' | 'last_l | |||
| role: 'owner' | 'admin' | 'normal' | |||
| } | |||
| export enum ProviderName { | |||
| OPENAI = 'openai', | |||
| AZURE_OPENAI = 'azure_openai' | |||
| } | |||
| export type ProviderAzureToken = { | |||
| openai_api_base: string | |||
| openai_api_key: string | |||
| openai_api_base?: string | |||
| openai_api_key?: string | |||
| } | |||
| export type Provider = { | |||
| provider_name: string | |||
| provider_type: string | |||
| is_valid: boolean | |||
| is_enabled: boolean | |||
| last_used: string | |||
| token?: string | ProviderAzureToken | |||
| export type ProviderTokenType = { | |||
| [ProviderName.OPENAI]: string | |||
| [ProviderName.AZURE_OPENAI]: ProviderAzureToken | |||
| } | |||
| export type Provider = { | |||
| [Name in ProviderName]: { | |||
| provider_name: Name | |||
| } & { | |||
| provider_type: 'custom' | 'system' | |||
| is_valid: boolean | |||
| is_enabled: boolean | |||
| last_used: string | |||
| token?: ProviderTokenType[Name] | |||
| } | |||
| }[ProviderName] | |||
| export type ProviderHosted = Provider & { | |||
| quota_type: string | |||