index.tsx 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. import {
  2. memo,
  3. useState,
  4. } from 'react'
  5. import { useTranslation } from 'react-i18next'
  6. import { RiUploadCloud2Line } from '@remixicon/react'
  7. import FileInput from '../file-input'
  8. import { useFile } from '../hooks'
  9. import { useStore } from '../store'
  10. import { FILE_URL_REGEX } from '../constants'
  11. import {
  12. PortalToFollowElem,
  13. PortalToFollowElemContent,
  14. PortalToFollowElemTrigger,
  15. } from '@/app/components/base/portal-to-follow-elem'
  16. import Button from '@/app/components/base/button'
  17. import type { FileUpload } from '@/app/components/base/features/types'
  18. import cn from '@/utils/classnames'
  19. type FileFromLinkOrLocalProps = {
  20. showFromLink?: boolean
  21. showFromLocal?: boolean
  22. trigger: (open: boolean) => React.ReactNode
  23. fileConfig: FileUpload
  24. }
  25. const FileFromLinkOrLocal = ({
  26. showFromLink = true,
  27. showFromLocal = true,
  28. trigger,
  29. fileConfig,
  30. }: FileFromLinkOrLocalProps) => {
  31. const { t } = useTranslation()
  32. const files = useStore(s => s.files)
  33. const [open, setOpen] = useState(false)
  34. const [url, setUrl] = useState('')
  35. const [showError, setShowError] = useState(false)
  36. const { handleLoadFileFromLink } = useFile(fileConfig)
  37. const disabled = !!fileConfig.number_limits && files.length >= fileConfig.number_limits
  38. const handleSaveUrl = () => {
  39. if (!url)
  40. return
  41. if (!FILE_URL_REGEX.test(url)) {
  42. setShowError(true)
  43. return
  44. }
  45. handleLoadFileFromLink(url)
  46. setUrl('')
  47. }
  48. return (
  49. <PortalToFollowElem
  50. placement='top'
  51. offset={4}
  52. open={open}
  53. onOpenChange={setOpen}
  54. >
  55. <PortalToFollowElemTrigger onClick={() => setOpen(v => !v)} asChild>
  56. {trigger(open)}
  57. </PortalToFollowElemTrigger>
  58. <PortalToFollowElemContent className='z-10'>
  59. <div className='p-3 w-[280px] bg-components-panel-bg-blur border-[0.5px] border-components-panel-border rounded-xl shadow-lg'>
  60. {
  61. showFromLink && (
  62. <>
  63. <div className={cn(
  64. 'flex items-center p-1 h-8 bg-components-input-bg-active border border-components-input-border-active rounded-lg shadow-xs',
  65. showError && 'border-components-input-border-destructive',
  66. )}>
  67. <input
  68. className='grow block mr-0.5 px-1 bg-transparent system-sm-regular outline-none appearance-none'
  69. placeholder={t('common.fileUploader.pasteFileLinkInputPlaceholder') || ''}
  70. value={url}
  71. onChange={(e) => {
  72. setShowError(false)
  73. setUrl(e.target.value)
  74. }}
  75. disabled={disabled}
  76. />
  77. <Button
  78. className='shrink-0'
  79. size='small'
  80. variant='primary'
  81. disabled={!url || disabled}
  82. onClick={handleSaveUrl}
  83. >
  84. {t('common.operation.ok')}
  85. </Button>
  86. </div>
  87. {
  88. showError && (
  89. <div className='mt-0.5 body-xs-regular text-text-destructive'>
  90. {t('common.fileUploader.pasteFileLinkInvalid')}
  91. </div>
  92. )
  93. }
  94. </>
  95. )
  96. }
  97. {
  98. showFromLink && showFromLocal && (
  99. <div className='flex items-center p-2 h-7 system-2xs-medium-uppercase text-text-quaternary'>
  100. <div className='mr-2 w-[93px] h-[1px] bg-gradient-to-l from-[rgba(16,24,40,0.08)]' />
  101. OR
  102. <div className='ml-2 w-[93px] h-[1px] bg-gradient-to-r from-[rgba(16,24,40,0.08)]' />
  103. </div>
  104. )
  105. }
  106. {
  107. showFromLocal && (
  108. <Button
  109. className='relative w-full'
  110. variant='secondary-accent'
  111. disabled={disabled}
  112. >
  113. <RiUploadCloud2Line className='mr-1 w-4 h-4' />
  114. {t('common.fileUploader.uploadFromComputer')}
  115. <FileInput fileConfig={fileConfig} />
  116. </Button>
  117. )
  118. }
  119. </div>
  120. </PortalToFollowElemContent>
  121. </PortalToFollowElem>
  122. )
  123. }
  124. export default memo(FileFromLinkOrLocal)