index.tsx 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. import { ChangeEvent, useEffect, useRef, useState } from 'react'
  2. import { useContext } from 'use-context-selector'
  3. import { useTranslation } from 'react-i18next'
  4. import { debounce } from 'lodash-es'
  5. import Link from 'next/link'
  6. import useSWR from 'swr'
  7. import { ArrowTopRightOnSquareIcon, PencilIcon } from '@heroicons/react/24/outline'
  8. import { CheckCircleIcon, ExclamationCircleIcon } from '@heroicons/react/24/solid'
  9. import Button from '@/app/components/base/button'
  10. import s from './index.module.css'
  11. import classNames from 'classnames'
  12. import { fetchTenantInfo, validateProviderKey, updateProviderAIKey } from '@/service/common'
  13. import { ToastContext } from '@/app/components/base/toast'
  14. import Indicator from '../../../indicator'
  15. import I18n from '@/context/i18n'
  16. type IStatusType = 'normal' | 'verified' | 'error' | 'error-api-key-exceed-bill'
  17. type TInputWithStatusProps = {
  18. value: string
  19. onChange: (v: string) => void
  20. onValidating: (validating: boolean) => void
  21. verifiedStatus: IStatusType
  22. onVerified: (verified: IStatusType) => void
  23. }
  24. const InputWithStatus = ({
  25. value,
  26. onChange,
  27. onValidating,
  28. verifiedStatus,
  29. onVerified
  30. }: TInputWithStatusProps) => {
  31. const { t } = useTranslation()
  32. const validateKey = useRef(debounce(async (token: string) => {
  33. if (!token) return
  34. onValidating(true)
  35. try {
  36. const res = await validateProviderKey({ url: '/workspaces/current/providers/openai/token-validate', body: { token } })
  37. onVerified(res.result === 'success' ? 'verified' : 'error')
  38. } catch (e: any) {
  39. if (e.status === 400) {
  40. e.json().then(({ code }: any) => {
  41. if (code === 'provider_request_failed') {
  42. onVerified('error-api-key-exceed-bill')
  43. }
  44. })
  45. } else {
  46. onVerified('error')
  47. }
  48. } finally {
  49. onValidating(false)
  50. }
  51. }, 500))
  52. const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
  53. const inputValue = e.target.value
  54. onChange(inputValue)
  55. if (!inputValue) {
  56. onVerified('normal')
  57. }
  58. validateKey.current(inputValue)
  59. }
  60. return (
  61. <div className={classNames('flex items-center h-9 px-3 bg-white border border-gray-300 rounded-lg', s.input)}>
  62. <input
  63. value={value}
  64. placeholder={t('common.provider.enterYourKey') || ''}
  65. className='w-full h-9 mr-2 appearance-none outline-none bg-transparent text-xs'
  66. onChange={handleChange}
  67. />
  68. {
  69. verifiedStatus === 'error' && <ExclamationCircleIcon className='w-4 h-4 text-[#D92D20]' />
  70. }
  71. {
  72. verifiedStatus === 'verified' && <CheckCircleIcon className='w-4 h-4 text-[#039855]' />
  73. }
  74. </div>
  75. )
  76. }
  77. const OpenaiProvider = () => {
  78. const { t } = useTranslation()
  79. const { locale } = useContext(I18n)
  80. const { data: userInfo, mutate } = useSWR({ url: '/info' }, fetchTenantInfo)
  81. const [inputValue, setInputValue] = useState<string>('')
  82. const [validating, setValidating] = useState(false)
  83. const [editStatus, setEditStatus] = useState<IStatusType>('normal')
  84. const [loading, setLoading] = useState(false)
  85. const [editing, setEditing] = useState(false)
  86. const [invalidStatus, setInvalidStatus] = useState(false)
  87. const { notify } = useContext(ToastContext)
  88. const provider = userInfo?.providers?.find(({ provider }) => provider === 'openai')
  89. const handleReset = () => {
  90. setInputValue('')
  91. setValidating(false)
  92. setEditStatus('normal')
  93. setLoading(false)
  94. setEditing(false)
  95. }
  96. const handleSave = async () => {
  97. if (editStatus === 'verified') {
  98. try {
  99. setLoading(true)
  100. await updateProviderAIKey({ url: '/workspaces/current/providers/openai/token', body: { token: inputValue ?? '' } })
  101. notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
  102. } catch (e) {
  103. notify({ type: 'error', message: t('common.provider.saveFailed') })
  104. } finally {
  105. setLoading(false)
  106. handleReset()
  107. mutate()
  108. }
  109. }
  110. }
  111. useEffect(() => {
  112. if (provider && !provider.token_is_valid && provider.token_is_set) {
  113. setInvalidStatus(true)
  114. }
  115. }, [userInfo])
  116. const showInvalidStatus = invalidStatus && !editing
  117. const renderErrorMessage = () => {
  118. if (validating) {
  119. return (
  120. <div className={`mt-2 text-primary-600 text-xs font-normal`}>
  121. {t('common.provider.validating')}
  122. </div>
  123. )
  124. }
  125. if (editStatus === 'error-api-key-exceed-bill') {
  126. return (
  127. <div className={`mt-2 text-[#D92D20] text-xs font-normal`}>
  128. {t('common.provider.apiKeyExceedBill')}&nbsp;
  129. <Link
  130. className='underline'
  131. href="https://platform.openai.com/account/api-keys"
  132. target={'_blank'}>
  133. {locale === 'en' ? 'this link' : '这篇文档'}
  134. </Link>
  135. </div>
  136. )
  137. }
  138. if (showInvalidStatus || editStatus === 'error') {
  139. return (
  140. <div className={`mt-2 text-[#D92D20] text-xs font-normal`}>
  141. {t('common.provider.invalidKey')}
  142. </div>
  143. )
  144. }
  145. return null
  146. }
  147. return (
  148. <div className='px-4 pt-3 pb-4'>
  149. <div className='flex items-center mb-2 h-6'>
  150. <div className='grow text-[13px] text-gray-800 font-medium'>
  151. {t('common.provider.apiKey')}
  152. </div>
  153. {
  154. provider && !editing && (
  155. <div
  156. className='
  157. flex items-center h-6 px-2 rounded-md border border-gray-200
  158. text-xs font-medium text-gray-700 cursor-pointer
  159. '
  160. onClick={() => setEditing(true)}
  161. >
  162. <PencilIcon className='mr-1 w-3 h-3 text-gray-500' />
  163. {t('common.operation.edit')}
  164. </div>
  165. )
  166. }
  167. {
  168. (inputValue || editing) && (
  169. <>
  170. <Button
  171. className={classNames('mr-1', s.button)}
  172. loading={loading}
  173. onClick={handleReset}
  174. >
  175. {t('common.operation.cancel')}
  176. </Button>
  177. <Button
  178. type='primary'
  179. className={classNames(s.button)}
  180. loading={loading}
  181. onClick={handleSave}>
  182. {t('common.operation.save')}
  183. </Button>
  184. </>
  185. )
  186. }
  187. </div>
  188. {
  189. (!provider || (provider && editing)) && (
  190. <InputWithStatus
  191. value={inputValue}
  192. onChange={v => setInputValue(v)}
  193. verifiedStatus={editStatus}
  194. onVerified={v => setEditStatus(v)}
  195. onValidating={v => setValidating(v)}
  196. />
  197. )
  198. }
  199. {
  200. (provider && !editing) && (
  201. <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)}>
  202. sk-0C...skuA
  203. <Indicator color={(provider.token_is_set && provider.token_is_valid) ? 'green' : 'orange'} />
  204. </div>
  205. )
  206. }
  207. {renderErrorMessage()}
  208. <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'}>
  209. {t('appOverview.welcome.getKeyTip')}
  210. <ArrowTopRightOnSquareIcon className='w-3 h-3 ml-1 text-primary-600' aria-hidden="true" />
  211. </Link>
  212. </div>
  213. )
  214. }
  215. export default OpenaiProvider