config-firecrawl-modal.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. 'use client'
  2. import type { FC } from 'react'
  3. import React, { useCallback, useState } from 'react'
  4. import { useTranslation } from 'react-i18next'
  5. import {
  6. PortalToFollowElem,
  7. PortalToFollowElemContent,
  8. } from '@/app/components/base/portal-to-follow-elem'
  9. import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security'
  10. import Button from '@/app/components/base/button'
  11. import type { FirecrawlConfig } from '@/models/common'
  12. import Field from '@/app/components/datasets/create/website/firecrawl/base/field'
  13. import Toast from '@/app/components/base/toast'
  14. import { createDataSourceApiKeyBinding } from '@/service/datasets'
  15. import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general'
  16. type Props = {
  17. onCancel: () => void
  18. onSaved: () => void
  19. }
  20. const I18N_PREFIX = 'datasetCreation.firecrawl'
  21. const DEFAULT_BASE_URL = 'https://api.firecrawl.dev'
  22. const ConfigFirecrawlModal: FC<Props> = ({
  23. onCancel,
  24. onSaved,
  25. }) => {
  26. const { t } = useTranslation()
  27. const [isSaving, setIsSaving] = useState(false)
  28. const [config, setConfig] = useState<FirecrawlConfig>({
  29. api_key: '',
  30. base_url: '',
  31. })
  32. const handleConfigChange = useCallback((key: string) => {
  33. return (value: string | number) => {
  34. setConfig(prev => ({ ...prev, [key]: value as string }))
  35. }
  36. }, [])
  37. const handleSave = useCallback(async () => {
  38. if (isSaving)
  39. return
  40. let errorMsg = ''
  41. if (config.base_url && !((config.base_url.startsWith('http://') || config.base_url.startsWith('https://'))))
  42. errorMsg = t('common.errorMsg.urlError')
  43. if (!errorMsg) {
  44. if (!config.api_key) {
  45. errorMsg = t('common.errorMsg.fieldRequired', {
  46. field: 'API Key',
  47. })
  48. }
  49. else if (!config.api_key.startsWith('fc-')) {
  50. errorMsg = t(`${I18N_PREFIX}.apiKeyFormatError`)
  51. }
  52. }
  53. if (errorMsg) {
  54. Toast.notify({
  55. type: 'error',
  56. message: errorMsg,
  57. })
  58. return
  59. }
  60. const postData = {
  61. category: 'website',
  62. provider: 'firecrawl',
  63. credentials: {
  64. auth_type: 'bearer',
  65. config: {
  66. api_key: config.api_key,
  67. base_url: config.base_url || DEFAULT_BASE_URL,
  68. },
  69. },
  70. }
  71. try {
  72. setIsSaving(true)
  73. await createDataSourceApiKeyBinding(postData)
  74. Toast.notify({
  75. type: 'success',
  76. message: t('common.api.success'),
  77. })
  78. }
  79. finally {
  80. setIsSaving(false)
  81. }
  82. onSaved()
  83. }, [config.api_key, config.base_url, onSaved, t, isSaving])
  84. return (
  85. <PortalToFollowElem open>
  86. <PortalToFollowElemContent className='w-full h-full z-[60]'>
  87. <div className='fixed inset-0 flex items-center justify-center bg-black/[.25]'>
  88. <div className='mx-2 w-[640px] max-h-[calc(100vh-120px)] bg-white shadow-xl rounded-2xl overflow-y-auto'>
  89. <div className='px-8 pt-8'>
  90. <div className='flex justify-between items-center mb-4'>
  91. <div className='text-xl font-semibold text-gray-900'>{t(`${I18N_PREFIX}.configFirecrawl`)}</div>
  92. </div>
  93. <div className='space-y-4'>
  94. <Field
  95. label='API Key'
  96. labelClassName='!text-sm'
  97. isRequired
  98. value={config.api_key}
  99. onChange={handleConfigChange('api_key')}
  100. placeholder={t(`${I18N_PREFIX}.apiKeyPlaceholder`)!}
  101. />
  102. <Field
  103. label='Base URL'
  104. labelClassName='!text-sm'
  105. value={config.base_url}
  106. onChange={handleConfigChange('base_url')}
  107. placeholder={DEFAULT_BASE_URL}
  108. />
  109. </div>
  110. <div className='my-8 flex justify-between items-center h-8'>
  111. <a className='flex items-center space-x-1 leading-[18px] text-xs font-normal text-[#155EEF]' target='_blank' href='https://www.firecrawl.dev/account'>
  112. <span>{t(`${I18N_PREFIX}.getApiKeyLinkText`)}</span>
  113. <LinkExternal02 className='w-3 h-3' />
  114. </a>
  115. <div className='flex'>
  116. <Button
  117. className='mr-2 h-9 text-sm font-medium text-gray-700'
  118. onClick={onCancel}
  119. >
  120. {t('common.operation.cancel')}
  121. </Button>
  122. <Button
  123. className='h-9 text-sm font-medium'
  124. variant='primary'
  125. onClick={handleSave}
  126. loading={isSaving}
  127. >
  128. {t('common.operation.save')}
  129. </Button>
  130. </div>
  131. </div>
  132. </div>
  133. <div className='border-t-[0.5px] border-t-black/5'>
  134. <div className='flex justify-center items-center py-3 bg-gray-50 text-xs text-gray-500'>
  135. <Lock01 className='mr-1 w-3 h-3 text-gray-500' />
  136. {t('common.modelProvider.encrypted.front')}
  137. <a
  138. className='text-primary-600 mx-1'
  139. target='_blank' rel='noopener noreferrer'
  140. href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html'
  141. >
  142. PKCS1_OAEP
  143. </a>
  144. {t('common.modelProvider.encrypted.back')}
  145. </div>
  146. </div>
  147. </div>
  148. </div>
  149. </PortalToFollowElemContent>
  150. </PortalToFollowElem>
  151. )
  152. }
  153. export default React.memo(ConfigFirecrawlModal)