| import type { Provider, ProviderAzureToken } from '@/models/common' | import type { Provider, ProviderAzureToken } from '@/models/common' | ||||
| import { ProviderName } from '@/models/common' | |||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import Link from 'next/link' | import Link from 'next/link' | ||||
| import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline' | 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 { | interface IAzureProviderProps { | ||||
| provider: Provider | provider: Provider | ||||
| onValidatedStatus | onValidatedStatus | ||||
| }: IAzureProviderProps) => { | }: IAzureProviderProps) => { | ||||
| const { t } = useTranslation() | 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}) | setToken({...token}) | ||||
| onTokenChange({...token}) | onTokenChange({...token}) | ||||
| setValidatedStatus(undefined) | |||||
| } | } | ||||
| } | } | ||||
| const handleChange = (type: keyof ProviderAzureToken, v: string) => { | |||||
| const handleChange = (type: keyof ProviderAzureToken, v: string, validate: any) => { | |||||
| token[type] = v | token[type] = v | ||||
| setToken({...token}) | setToken({...token}) | ||||
| onTokenChange({...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 ( | return ( | ||||
| <div className='px-4 py-3'> | <div className='px-4 py-3'> | ||||
| name={t('common.provider.azure.apiBase')} | name={t('common.provider.azure.apiBase')} | ||||
| placeholder={t('common.provider.azure.apiBasePlaceholder')} | placeholder={t('common.provider.azure.apiBasePlaceholder')} | ||||
| value={token.openai_api_base} | 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' | className='mb-4' | ||||
| name={t('common.provider.azure.apiKey')} | name={t('common.provider.azure.apiKey')} | ||||
| placeholder={t('common.provider.azure.apiKeyPlaceholder')} | placeholder={t('common.provider.azure.apiKeyPlaceholder')} | ||||
| value={token.openai_api_key} | 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'}> | <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')} | {t('common.provider.azure.helpTip')} | 
| const providerHosted = data?.filter(provider => provider.provider_name === 'openai' && provider.provider_type === 'system')?.[0] | const providerHosted = data?.filter(provider => provider.provider_name === 'openai' && provider.provider_type === 'system')?.[0] | ||||
| return ( | return ( | ||||
| <div> | |||||
| <div className='pb-7'> | |||||
| { | { | ||||
| providerHosted && !IS_CE_EDITION && ( | providerHosted && !IS_CE_EDITION && ( | ||||
| <> | <> | 
| 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 { useTranslation } from 'react-i18next' | ||||
| import { debounce } from 'lodash-es' | |||||
| import ProviderInput from '../provider-input' | |||||
| import Link from 'next/link' | 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 { 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(() => { | 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) { | 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 ( | return ( | ||||
| <div className='px-4 pt-3 pb-4'> | <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> | |||||
| ) | ) | ||||
| } | } | ||||
| 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 | 
| 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> | |||||
| ) | |||||
| } | 
| 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 { | interface IProviderInputProps { | ||||
| value?: string | value?: string | ||||
| className?: string | className?: string | ||||
| onChange: (v: string) => void | onChange: (v: string) => void | ||||
| onFocus?: () => void | onFocus?: () => void | ||||
| validatedIcon?: ReactElement | |||||
| validatedTip?: ReactElement | |||||
| } | } | ||||
| const ProviderInput = ({ | const ProviderInput = ({ | ||||
| className, | className, | ||||
| onChange, | onChange, | ||||
| onFocus, | onFocus, | ||||
| validatedIcon, | |||||
| validatedTip | |||||
| }: IProviderInputProps) => { | }: IProviderInputProps) => { | ||||
| const handleChange = (e: ChangeEvent<HTMLInputElement>) => { | const handleChange = (e: ChangeEvent<HTMLInputElement>) => { | ||||
| onChange={handleChange} | onChange={handleChange} | ||||
| onFocus={onFocus} | onFocus={onFocus} | ||||
| /> | /> | ||||
| {validatedIcon} | |||||
| </div> | </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> | </div> | ||||
| ) | ) | ||||
| } | } | 
| import { useState, useCallback } from 'react' | |||||
| import { useState, useCallback, SetStateAction, Dispatch } from 'react' | |||||
| import debounce from 'lodash-es/debounce' | import debounce from 'lodash-es/debounce' | ||||
| import { DebouncedFunc } from 'lodash-es' | import { DebouncedFunc } from 'lodash-es' | ||||
| import { validateProviderKey } from '@/service/common' | import { validateProviderKey } from '@/service/common' | ||||
| Error = 'error', | Error = 'error', | ||||
| Exceed = 'exceed' | 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 [validating, setValidating] = useState(false) | ||||
| const [validatedStatus, setValidatedStatus] = useState<ValidatedStatus | undefined>() | 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) | setValidating(true) | ||||
| try { | try { | ||||
| } catch (e: any) { | } catch (e: any) { | ||||
| if (e.status === 400) { | if (e.status === 400) { | ||||
| e.json().then(({ code }: any) => { | e.json().then(({ code }: any) => { | ||||
| if (code === 'provider_request_failed') { | |||||
| if (code === 'provider_request_failed' && providerName === 'openai') { | |||||
| setValidatedStatus(ValidatedStatus.Exceed) | setValidatedStatus(ValidatedStatus.Exceed) | ||||
| } else { | |||||
| setValidatedStatus(ValidatedStatus.Error) | |||||
| } | } | ||||
| }) | }) | ||||
| } else { | } else { | ||||
| return [ | return [ | ||||
| validating, | validating, | ||||
| validatedStatus, | validatedStatus, | ||||
| validate, | |||||
| setValidatedStatus, | |||||
| validate | |||||
| ] | ] | ||||
| } | } | ||||
| import Indicator from '../../../indicator' | import Indicator from '../../../indicator' | ||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import type { Provider, ProviderAzureToken } from '@/models/common' | 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 AzureProvider from '../azure-provider' | ||||
| import { ValidatedStatus } from '../provider-input/useValidateToken' | import { ValidatedStatus } from '../provider-input/useValidateToken' | ||||
| import { updateProviderAIKey } from '@/service/common' | import { updateProviderAIKey } from '@/service/common' | ||||
| ) | ) | ||||
| const id = `${provider.provider_name}-${provider.provider_type}` | const id = `${provider.provider_name}-${provider.provider_type}` | ||||
| const isOpen = id === activeId | const isOpen = id === activeId | ||||
| const providerKey = provider.provider_name === 'azure_openai' ? (provider.token as ProviderAzureToken)?.openai_api_key : provider.token | |||||
| const comingSoon = false | const comingSoon = false | ||||
| const isValid = provider.is_valid | 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 () => { | const handleUpdateToken = async () => { | ||||
| if (loading) return | if (loading) return | ||||
| if (validatedStatus === ValidatedStatus.Success || !token) { | |||||
| if (validatedStatus === ValidatedStatus.Success) { | |||||
| try { | try { | ||||
| setLoading(true) | setLoading(true) | ||||
| await updateProviderAIKey({ url: `/workspaces/current/providers/${provider.provider_name}/token`, body: { token } }) | await updateProviderAIKey({ url: `/workspaces/current/providers/${provider.provider_name}/token`, body: { token } }) | ||||
| <div className={cn(s[`icon-${icon}`], 'mr-3 w-6 h-6 rounded-md')} /> | <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> | <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'> | <div className='flex items-center mr-4'> | ||||
| {!isValid && <div className='text-xs text-[#D92D20]'>{t('common.provider.invalidApiKey')}</div>} | {!isValid && <div className='text-xs text-[#D92D20]'>{t('common.provider.invalidApiKey')}</div>} | ||||
| <Indicator color={!isValid ? 'red' : 'green'} className='ml-2' /> | <Indicator color={!isValid ? 'red' : 'green'} className='ml-2' /> | ||||
| px-3 h-[28px] bg-white border border-gray-200 rounded-md cursor-pointer | px-3 h-[28px] bg-white border border-gray-200 rounded-md cursor-pointer | ||||
| text-xs font-medium text-gray-700 flex items-center | text-xs font-medium text-gray-700 flex items-center | ||||
| ' onClick={() => onActive(id)}> | ' onClick={() => onActive(id)}> | ||||
| {providerKey ? t('common.provider.editKey') : t('common.provider.addKey')} | |||||
| {providerTokenHasSetted() ? t('common.provider.editKey') : t('common.provider.addKey')} | |||||
| </div> | </div> | ||||
| ) | ) | ||||
| } | } | ||||
| } | } | ||||
| </div> | </div> | ||||
| { | { | ||||
| provider.provider_name === 'openai' && isOpen && ( | |||||
| provider.provider_name === ProviderName.OPENAI && isOpen && ( | |||||
| <OpenaiProvider | <OpenaiProvider | ||||
| provider={provider} | provider={provider} | ||||
| onValidatedStatus={v => setValidatedStatus(v)} | onValidatedStatus={v => setValidatedStatus(v)} | ||||
| ) | ) | ||||
| } | } | ||||
| { | { | ||||
| provider.provider_name === 'azure_openai' && isOpen && ( | |||||
| provider.provider_name === ProviderName.AZURE_OPENAI && isOpen && ( | |||||
| <AzureProvider | <AzureProvider | ||||
| provider={provider} | provider={provider} | ||||
| onValidatedStatus={v => setValidatedStatus(v)} | onValidatedStatus={v => setValidatedStatus(v)} | 
| role: 'owner' | 'admin' | 'normal' | role: 'owner' | 'admin' | 'normal' | ||||
| } | } | ||||
| export enum ProviderName { | |||||
| OPENAI = 'openai', | |||||
| AZURE_OPENAI = 'azure_openai' | |||||
| } | |||||
| export type ProviderAzureToken = { | 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 & { | export type ProviderHosted = Provider & { | ||||
| quota_type: string | quota_type: string |