|
@@ -1,200 +0,0 @@
|
|
|
-'use client'
|
|
|
-import type { FC } from 'react'
|
|
|
-import React, { useState } from 'react'
|
|
|
-import { useContext } from 'use-context-selector'
|
|
|
-import { useTranslation } from 'react-i18next'
|
|
|
-import Link from 'next/link'
|
|
|
-import useSWR, { useSWRConfig } from 'swr'
|
|
|
-import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline'
|
|
|
-import { ExclamationCircleIcon } from '@heroicons/react/24/solid'
|
|
|
-import { debounce } from 'lodash-es'
|
|
|
-import Popover from '@/app/components/base/popover'
|
|
|
-import Button from '@/app/components/base/button'
|
|
|
-import Tag from '@/app/components/base/tag'
|
|
|
-import { ToastContext } from '@/app/components/base/toast'
|
|
|
-import { updateOpenAIKey, validateOpenAIKey } from '@/service/apps'
|
|
|
-import { fetchTenantInfo } from '@/service/common'
|
|
|
-import I18n from '@/context/i18n'
|
|
|
-
|
|
|
-type IStatusType = 'normal' | 'verified' | 'error' | 'error-api-key-exceed-bill'
|
|
|
-
|
|
|
-const STATUS_COLOR_MAP = {
|
|
|
- 'normal': { color: '', bgColor: 'bg-primary-50', borderColor: 'border-primary-100' },
|
|
|
- 'error': { color: 'text-red-600', bgColor: 'bg-red-50', borderColor: 'border-red-100' },
|
|
|
- 'verified': { color: '', bgColor: 'bg-green-50', borderColor: 'border-green-100' },
|
|
|
- 'error-api-key-exceed-bill': { color: 'text-red-600', bgColor: 'bg-red-50', borderColor: 'border-red-100' },
|
|
|
-}
|
|
|
-
|
|
|
-const CheckCircleIcon: FC<{ className?: string }> = ({ className }) => {
|
|
|
- return <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
|
|
|
- <rect width="20" height="20" rx="10" fill="#DEF7EC" />
|
|
|
- <path fillRule="evenodd" clipRule="evenodd" d="M14.6947 6.70495C14.8259 6.83622 14.8996 7.01424 14.8996 7.19985C14.8996 7.38547 14.8259 7.56348 14.6947 7.69475L9.0947 13.2948C8.96343 13.426 8.78541 13.4997 8.5998 13.4997C8.41418 13.4997 8.23617 13.426 8.1049 13.2948L5.3049 10.4948C5.17739 10.3627 5.10683 10.1859 5.10842 10.0024C5.11002 9.81883 5.18364 9.64326 5.31342 9.51348C5.44321 9.38369 5.61878 9.31007 5.80232 9.30848C5.98585 9.30688 6.16268 9.37744 6.2947 9.50495L8.5998 11.8101L13.7049 6.70495C13.8362 6.57372 14.0142 6.5 14.1998 6.5C14.3854 6.5 14.5634 6.57372 14.6947 6.70495Z" fill="#046C4E" />
|
|
|
- </svg>
|
|
|
-}
|
|
|
-
|
|
|
-type IEditKeyDiv = {
|
|
|
- className?: string
|
|
|
- showInPopover?: boolean
|
|
|
- onClose?: () => void
|
|
|
- getTenantInfo?: () => void
|
|
|
-}
|
|
|
-
|
|
|
-const EditKeyDiv: FC<IEditKeyDiv> = ({ className = '', showInPopover = false, onClose, getTenantInfo }) => {
|
|
|
- const [inputValue, setInputValue] = useState<string | undefined>()
|
|
|
- const [editStatus, setEditStatus] = useState<IStatusType>('normal')
|
|
|
- const [loading, setLoading] = useState(false)
|
|
|
- const [validating, setValidating] = useState(false)
|
|
|
- const { notify } = useContext(ToastContext)
|
|
|
- const { t } = useTranslation()
|
|
|
- const { locale } = useContext(I18n)
|
|
|
-
|
|
|
- // Hide the pop-up window and need to get the latest key again
|
|
|
- // If the key is valid, the edit button will be hidden later
|
|
|
- const onClosePanel = () => {
|
|
|
- getTenantInfo && getTenantInfo()
|
|
|
- onClose && onClose()
|
|
|
- }
|
|
|
-
|
|
|
- const onSaveKey = async () => {
|
|
|
- if (editStatus === 'verified') {
|
|
|
- setLoading(true)
|
|
|
- try {
|
|
|
- await updateOpenAIKey({ url: '/providers/openai/token', body: { token: inputValue ?? '' } })
|
|
|
- notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
|
|
- onClosePanel()
|
|
|
- }
|
|
|
- catch (err) {
|
|
|
- notify({ type: 'error', message: t('common.actionMsg.modificationFailed') })
|
|
|
- }
|
|
|
- finally {
|
|
|
- setLoading(false)
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- const validateKey = async (value: string) => {
|
|
|
- try {
|
|
|
- setValidating(true)
|
|
|
- const res = await validateOpenAIKey({ url: '/providers/openai/token-validate', body: { token: value ?? '' } })
|
|
|
- setEditStatus(res.result === 'success' ? 'verified' : 'error')
|
|
|
- }
|
|
|
- catch (err: any) {
|
|
|
- if (err.status === 400) {
|
|
|
- err.json().then(({ code }: any) => {
|
|
|
- if (code === 'provider_request_failed')
|
|
|
- setEditStatus('error-api-key-exceed-bill')
|
|
|
- })
|
|
|
- }
|
|
|
- else {
|
|
|
- setEditStatus('error')
|
|
|
- }
|
|
|
- }
|
|
|
- finally {
|
|
|
- setValidating(false)
|
|
|
- }
|
|
|
- }
|
|
|
- const renderErrorMessage = () => {
|
|
|
- if (validating) {
|
|
|
- return (
|
|
|
- <div className={'text-primary-600 mt-2 text-xs'}>
|
|
|
- {t('common.provider.validating')}
|
|
|
- </div>
|
|
|
- )
|
|
|
- }
|
|
|
- if (editStatus === 'error-api-key-exceed-bill') {
|
|
|
- return (
|
|
|
- <div className={'text-[#D92D20] mt-2 text-xs'}>
|
|
|
- {t('common.provider.apiKeyExceedBill')}
|
|
|
- {locale === 'en' ? ' ' : ''}
|
|
|
- <Link
|
|
|
- className='underline'
|
|
|
- href="https://platform.openai.com/account/api-keys"
|
|
|
- target={'_blank'}>
|
|
|
- {locale === 'en' ? 'this link' : '这篇文档'}
|
|
|
- </Link>
|
|
|
- </div>
|
|
|
- )
|
|
|
- }
|
|
|
- if (editStatus === 'error') {
|
|
|
- return (
|
|
|
- <div className={'text-[#D92D20] mt-2 text-xs'}>
|
|
|
- {t('common.provider.invalidKey')}
|
|
|
- </div>
|
|
|
- )
|
|
|
- }
|
|
|
- return null
|
|
|
- }
|
|
|
-
|
|
|
- return (
|
|
|
- <div className={`flex flex-col w-full rounded-lg px-8 py-6 border-solid border-[0.5px] ${className} ${Object.values(STATUS_COLOR_MAP[editStatus]).join(' ')}`}>
|
|
|
- {!showInPopover && <p className='text-xl font-medium text-gray-800'>{t('appOverview.welcome.firstStepTip')}</p>}
|
|
|
- <p className={`${showInPopover ? 'text-sm' : 'text-xl'} font-medium text-gray-800`}>{t('appOverview.welcome.enterKeyTip')} {showInPopover ? '' : '👇'}</p>
|
|
|
- <div className='relative mt-2'>
|
|
|
- <input type="text"
|
|
|
- className={`h-9 w-96 max-w-full py-2 pl-2 text-gray-900 rounded-lg bg-white sm:text-xs focus:ring-blue-500 focus:border-blue-500 shadow-sm ${editStatus === 'normal' ? 'pr-2' : 'pr-8'}`}
|
|
|
- placeholder={t('appOverview.welcome.placeholder') || ''}
|
|
|
- onChange={debounce((e) => {
|
|
|
- setInputValue(e.target.value)
|
|
|
- if (!e.target.value) {
|
|
|
- setEditStatus('normal')
|
|
|
- return
|
|
|
- }
|
|
|
- validateKey(e.target.value)
|
|
|
- }, 300)}
|
|
|
- />
|
|
|
- {editStatus === 'verified' && <div className="absolute inset-y-0 right-0 flex flex-row-reverse items-center pr-6 pointer-events-none">
|
|
|
- <CheckCircleIcon className="rounded-lg" />
|
|
|
- </div>}
|
|
|
- {(editStatus === 'error' || editStatus === 'error-api-key-exceed-bill') && <div className="absolute inset-y-0 right-0 flex flex-row-reverse items-center pr-6 pointer-events-none">
|
|
|
- <ExclamationCircleIcon className="w-5 h-5 text-red-800" />
|
|
|
- </div>}
|
|
|
- {showInPopover ? null : <Button type='primary' onClick={onSaveKey} className='!h-9 !inline-block ml-2' loading={loading} disabled={editStatus !== 'verified'}>{t('common.operation.save')}</Button>}
|
|
|
- </div>
|
|
|
- {renderErrorMessage()}
|
|
|
- <Link className="inline-flex items-center mt-2 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>
|
|
|
- {showInPopover && <div className='flex justify-end mt-6'>
|
|
|
- <Button className='flex-shrink-0 mr-2' onClick={onClosePanel}>{t('common.operation.cancel')}</Button>
|
|
|
- <Button type='primary' className='flex-shrink-0' onClick={onSaveKey} loading={loading} disabled={editStatus !== 'verified'}>{t('common.operation.save')}</Button>
|
|
|
- </div>}
|
|
|
- </div>
|
|
|
- )
|
|
|
-}
|
|
|
-
|
|
|
-const WelcomeBanner: FC = () => {
|
|
|
- const { data: userInfo } = useSWR({ url: '/info' }, fetchTenantInfo)
|
|
|
- if (!userInfo)
|
|
|
- return null
|
|
|
- return userInfo?.providers?.find(({ token_is_set }) => token_is_set) ? null : <EditKeyDiv className='mb-8' />
|
|
|
-}
|
|
|
-
|
|
|
-export const EditKeyPopover: FC = () => {
|
|
|
- const { data: userInfo } = useSWR({ url: '/info' }, fetchTenantInfo)
|
|
|
- const { mutate } = useSWRConfig()
|
|
|
- if (!userInfo)
|
|
|
- return null
|
|
|
-
|
|
|
- const getTenantInfo = () => {
|
|
|
- mutate({ url: '/info' })
|
|
|
- }
|
|
|
- // In this case, the edit button is displayed
|
|
|
- const targetProvider = userInfo?.providers?.some(({ token_is_set, is_valid }) => token_is_set && is_valid)
|
|
|
- return (
|
|
|
- !targetProvider
|
|
|
- ? <div className='flex items-center'>
|
|
|
- <Tag className='mr-2 h-fit' color='red'><ExclamationCircleIcon className='h-3.5 w-3.5 mr-2' />OpenAI API key invalid</Tag>
|
|
|
- <Popover
|
|
|
- htmlContent={<EditKeyDiv className='!border-0' showInPopover={true} getTenantInfo={getTenantInfo} />}
|
|
|
- trigger='click'
|
|
|
- position='br'
|
|
|
- btnElement='Edit'
|
|
|
- btnClassName='text-primary-600 !text-xs px-3 py-1.5'
|
|
|
- className='!p-0 !w-[464px] h-[200px]'
|
|
|
- />
|
|
|
- </div>
|
|
|
- : null)
|
|
|
-}
|
|
|
-
|
|
|
-export default WelcomeBanner
|