config-firecrawl-modal.tsx 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  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/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. }
  50. if (errorMsg) {
  51. Toast.notify({
  52. type: 'error',
  53. message: errorMsg,
  54. })
  55. return
  56. }
  57. const postData = {
  58. category: 'website',
  59. provider: 'firecrawl',
  60. credentials: {
  61. auth_type: 'bearer',
  62. config: {
  63. api_key: config.api_key,
  64. base_url: config.base_url || DEFAULT_BASE_URL,
  65. },
  66. },
  67. }
  68. try {
  69. setIsSaving(true)
  70. await createDataSourceApiKeyBinding(postData)
  71. Toast.notify({
  72. type: 'success',
  73. message: t('common.api.success'),
  74. })
  75. }
  76. finally {
  77. setIsSaving(false)
  78. }
  79. onSaved()
  80. }, [config.api_key, config.base_url, onSaved, t, isSaving])
  81. return (
  82. <PortalToFollowElem open>
  83. <PortalToFollowElemContent className='w-full h-full z-[60]'>
  84. <div className='fixed inset-0 flex items-center justify-center bg-black/[.25]'>
  85. <div className='mx-2 w-[640px] max-h-[calc(100vh-120px)] bg-white shadow-xl rounded-2xl overflow-y-auto'>
  86. <div className='px-8 pt-8'>
  87. <div className='flex justify-between items-center mb-4'>
  88. <div className='text-xl font-semibold text-gray-900'>{t(`${I18N_PREFIX}.configFirecrawl`)}</div>
  89. </div>
  90. <div className='space-y-4'>
  91. <Field
  92. label='API Key'
  93. labelClassName='!text-sm'
  94. isRequired
  95. value={config.api_key}
  96. onChange={handleConfigChange('api_key')}
  97. placeholder={t(`${I18N_PREFIX}.apiKeyPlaceholder`)!}
  98. />
  99. <Field
  100. label='Base URL'
  101. labelClassName='!text-sm'
  102. value={config.base_url}
  103. onChange={handleConfigChange('base_url')}
  104. placeholder={DEFAULT_BASE_URL}
  105. />
  106. </div>
  107. <div className='my-8 flex justify-between items-center h-8'>
  108. <a className='flex items-center space-x-1 leading-[18px] text-xs font-normal text-[#155EEF]' target='_blank' href='https://www.firecrawl.dev/account'>
  109. <span>{t(`${I18N_PREFIX}.getApiKeyLinkText`)}</span>
  110. <LinkExternal02 className='w-3 h-3' />
  111. </a>
  112. <div className='flex'>
  113. <Button
  114. size='large'
  115. className='mr-2'
  116. onClick={onCancel}
  117. >
  118. {t('common.operation.cancel')}
  119. </Button>
  120. <Button
  121. variant='primary'
  122. size='large'
  123. onClick={handleSave}
  124. loading={isSaving}
  125. >
  126. {t('common.operation.save')}
  127. </Button>
  128. </div>
  129. </div>
  130. </div>
  131. <div className='border-t-[0.5px] border-t-black/5'>
  132. <div className='flex justify-center items-center py-3 bg-gray-50 text-xs text-gray-500'>
  133. <Lock01 className='mr-1 w-3 h-3 text-gray-500' />
  134. {t('common.modelProvider.encrypted.front')}
  135. <a
  136. className='text-primary-600 mx-1'
  137. target='_blank' rel='noopener noreferrer'
  138. href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html'
  139. >
  140. PKCS1_OAEP
  141. </a>
  142. {t('common.modelProvider.encrypted.back')}
  143. </div>
  144. </div>
  145. </div>
  146. </div>
  147. </PortalToFollowElemContent>
  148. </PortalToFollowElem>
  149. )
  150. }
  151. export default React.memo(ConfigFirecrawlModal)