| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223 | import { ChangeEvent, useEffect, useRef, useState } from 'react'import { useContext } from 'use-context-selector'import { useTranslation } from 'react-i18next'import { debounce } from 'lodash-es'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'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>  )}const OpenaiProvider = () => {  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 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()      }    }  }  useEffect(() => {    if (provider && !provider.token_is_valid && provider.token_is_set) {      setInvalidStatus(true)    }  }, [userInfo])  const showInvalidStatus = invalidStatus && !editing  const renderErrorMessage = () => {    if (validating) {      return (        <div className={`mt-2 text-primary-600 text-xs font-normal`}>          {t('common.provider.validating')}        </div>      )    }    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 (showInvalidStatus || editStatus === 'error') {      return (        <div className={`mt-2 text-[#D92D20] text-xs font-normal`}>          {t('common.provider.invalidKey')}        </div>      )    }    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>  )}export default OpenaiProvider
 |