You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

index.tsx 6.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. import {
  2. useCallback,
  3. useState,
  4. } from 'react'
  5. import Textarea from 'rc-textarea'
  6. import { useTranslation } from 'react-i18next'
  7. import Recorder from 'js-audio-recorder'
  8. import type {
  9. EnableType,
  10. OnSend,
  11. } from '../../types'
  12. import type { Theme } from '../../embedded-chatbot/theme/theme-context'
  13. import type { InputForm } from '../type'
  14. import { useCheckInputsForms } from '../check-input-forms-hooks'
  15. import { useTextAreaHeight } from './hooks'
  16. import Operation from './operation'
  17. import cn from '@/utils/classnames'
  18. import { FileListInChatInput } from '@/app/components/base/file-uploader'
  19. import { useFile } from '@/app/components/base/file-uploader/hooks'
  20. import {
  21. FileContextProvider,
  22. useFileStore,
  23. } from '@/app/components/base/file-uploader/store'
  24. import VoiceInput from '@/app/components/base/voice-input'
  25. import { useToastContext } from '@/app/components/base/toast'
  26. import FeatureBar from '@/app/components/base/features/new-feature-panel/feature-bar'
  27. import type { FileUpload } from '@/app/components/base/features/types'
  28. import { TransferMethod } from '@/types/app'
  29. type ChatInputAreaProps = {
  30. showFeatureBar?: boolean
  31. showFileUpload?: boolean
  32. featureBarDisabled?: boolean
  33. onFeatureBarClick?: (state: boolean) => void
  34. visionConfig?: FileUpload
  35. speechToTextConfig?: EnableType
  36. onSend?: OnSend
  37. inputs?: Record<string, any>
  38. inputsForm?: InputForm[]
  39. theme?: Theme | null
  40. }
  41. const ChatInputArea = ({
  42. showFeatureBar,
  43. showFileUpload,
  44. featureBarDisabled,
  45. onFeatureBarClick,
  46. visionConfig,
  47. speechToTextConfig = { enabled: true },
  48. onSend,
  49. inputs = {},
  50. inputsForm = [],
  51. theme,
  52. }: ChatInputAreaProps) => {
  53. const { t } = useTranslation()
  54. const { notify } = useToastContext()
  55. const {
  56. wrapperRef,
  57. textareaRef,
  58. textValueRef,
  59. holdSpaceRef,
  60. handleTextareaResize,
  61. isMultipleLine,
  62. } = useTextAreaHeight()
  63. const [query, setQuery] = useState('')
  64. const [showVoiceInput, setShowVoiceInput] = useState(false)
  65. const filesStore = useFileStore()
  66. const {
  67. handleDragFileEnter,
  68. handleDragFileLeave,
  69. handleDragFileOver,
  70. handleDropFile,
  71. handleClipboardPasteFile,
  72. isDragActive,
  73. } = useFile(visionConfig!)
  74. const { checkInputsForm } = useCheckInputsForms()
  75. const handleSend = () => {
  76. if (onSend) {
  77. const { files, setFiles } = filesStore.getState()
  78. if (files.find(item => item.transferMethod === TransferMethod.local_file && !item.uploadedId)) {
  79. notify({ type: 'info', message: t('appDebug.errorMessage.waitForFileUpload') })
  80. return
  81. }
  82. if (!query || !query.trim()) {
  83. notify({ type: 'info', message: t('appAnnotation.errorMessage.queryRequired') })
  84. return
  85. }
  86. if (checkInputsForm(inputs, inputsForm)) {
  87. onSend(query, files)
  88. setQuery('')
  89. setFiles([])
  90. }
  91. }
  92. }
  93. const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
  94. if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
  95. e.preventDefault()
  96. setQuery(query.replace(/\n$/, ''))
  97. handleSend()
  98. }
  99. }
  100. const handleShowVoiceInput = useCallback(() => {
  101. (Recorder as any).getPermission().then(() => {
  102. setShowVoiceInput(true)
  103. }, () => {
  104. notify({ type: 'error', message: t('common.voiceInput.notAllow') })
  105. })
  106. }, [t, notify])
  107. const operation = (
  108. <Operation
  109. ref={holdSpaceRef}
  110. fileConfig={visionConfig}
  111. speechToTextConfig={speechToTextConfig}
  112. onShowVoiceInput={handleShowVoiceInput}
  113. onSend={handleSend}
  114. theme={theme}
  115. />
  116. )
  117. return (
  118. <>
  119. <div
  120. className={cn(
  121. 'relative pb-[9px] bg-components-panel-bg-blur border border-components-chat-input-border rounded-xl shadow-md z-10',
  122. isDragActive && 'border border-dashed border-components-option-card-option-selected-border',
  123. )}
  124. >
  125. <div className='relative px-[9px] pt-[9px] max-h-[158px] overflow-x-hidden overflow-y-auto'>
  126. <FileListInChatInput fileConfig={visionConfig!} />
  127. <div
  128. ref={wrapperRef}
  129. className='flex items-center justify-between'
  130. >
  131. <div className='flex items-center relative grow w-full'>
  132. <div
  133. ref={textValueRef}
  134. className='absolute w-auto h-auto p-1 leading-6 body-lg-regular pointer-events-none whitespace-pre invisible'
  135. >
  136. {query}
  137. </div>
  138. <Textarea
  139. ref={textareaRef}
  140. className={cn(
  141. 'p-1 w-full leading-6 body-lg-regular text-text-tertiary outline-none',
  142. )}
  143. placeholder={t('common.chat.inputPlaceholder') || ''}
  144. autoSize={{ minRows: 1 }}
  145. onResize={handleTextareaResize}
  146. value={query}
  147. onChange={(e) => {
  148. setQuery(e.target.value)
  149. handleTextareaResize()
  150. }}
  151. onKeyDown={handleKeyDown}
  152. onPaste={handleClipboardPasteFile}
  153. onDragEnter={handleDragFileEnter}
  154. onDragLeave={handleDragFileLeave}
  155. onDragOver={handleDragFileOver}
  156. onDrop={handleDropFile}
  157. />
  158. </div>
  159. {
  160. !isMultipleLine && operation
  161. }
  162. </div>
  163. {
  164. showVoiceInput && (
  165. <VoiceInput
  166. onCancel={() => setShowVoiceInput(false)}
  167. onConverted={text => setQuery(text)}
  168. />
  169. )
  170. }
  171. </div>
  172. {
  173. isMultipleLine && (
  174. <div className='px-[9px]'>{operation}</div>
  175. )
  176. }
  177. </div>
  178. {showFeatureBar && <FeatureBar showFileUpload={showFileUpload} disabled={featureBarDisabled} onFeatureBarClick={onFeatureBarClick} />}
  179. </>
  180. )
  181. }
  182. const ChatInputAreaWrapper = (props: ChatInputAreaProps) => {
  183. return (
  184. <FileContextProvider>
  185. <ChatInputArea {...props} />
  186. </FileContextProvider>
  187. )
  188. }
  189. export default ChatInputAreaWrapper