import type { FC } from 'react' import { memo, useRef, useState, } from 'react' import { useContext } from 'use-context-selector' import Recorder from 'js-audio-recorder' import { useTranslation } from 'react-i18next' import Textarea from 'rc-textarea' import type { EnableType, OnSend, VisionConfig, } from '../types' import { TransferMethod } from '../types' import { useChatWithHistoryContext } from '../chat-with-history/context' import type { Theme } from '../embedded-chatbot/theme/theme-context' import { CssTransform } from '../embedded-chatbot/theme/utils' import TooltipPlus from '@/app/components/base/tooltip-plus' import { ToastContext } from '@/app/components/base/toast' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import VoiceInput from '@/app/components/base/voice-input' import { Microphone01 } from '@/app/components/base/icons/src/vender/line/mediaAndDevices' import { Microphone01 as Microphone01Solid } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices' import { XCircle } from '@/app/components/base/icons/src/vender/solid/general' import { Send03 } from '@/app/components/base/icons/src/vender/solid/communication' import ChatImageUploader from '@/app/components/base/image-uploader/chat-image-uploader' import ImageList from '@/app/components/base/image-uploader/image-list' import { useClipboardUploader, useDraggableUploader, useImageFiles, } from '@/app/components/base/image-uploader/hooks' type ChatInputProps = { visionConfig?: VisionConfig speechToTextConfig?: EnableType onSend?: OnSend theme?: Theme | null } const ChatInput: FC = ({ visionConfig, speechToTextConfig, onSend, theme, }) => { const { appData } = useChatWithHistoryContext() const { t } = useTranslation() const { notify } = useContext(ToastContext) const [voiceInputShow, setVoiceInputShow] = useState(false) const textAreaRef = useRef(null) const { files, onUpload, onRemove, onReUpload, onImageLinkLoadError, onImageLinkLoadSuccess, onClear, } = useImageFiles() const { onPaste } = useClipboardUploader({ onUpload, visionConfig, files }) const { onDragEnter, onDragLeave, onDragOver, onDrop, isDragActive } = useDraggableUploader({ onUpload, files, visionConfig }) const isUseInputMethod = useRef(false) const [query, setQuery] = useState('') const handleContentChange = (e: React.ChangeEvent) => { const value = e.target.value setQuery(value) } const handleSend = () => { if (onSend) { if (files.find(item => item.type === TransferMethod.local_file && !item.fileId)) { notify({ type: 'info', message: t('appDebug.errorMessage.waitForImgUpload') }) return } if (!query || !query.trim()) { notify({ type: 'info', message: t('appAnnotation.errorMessage.queryRequired') }) return } onSend(query, files.filter(file => file.progress !== -1).map(fileItem => ({ type: 'image', transfer_method: fileItem.type, url: fileItem.url, upload_file_id: fileItem.fileId, }))) setQuery('') onClear() } } const handleKeyUp = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { e.preventDefault() // prevent send message when using input method enter if (!e.shiftKey && !isUseInputMethod.current) handleSend() } } const handleKeyDown = (e: React.KeyboardEvent) => { isUseInputMethod.current = e.nativeEvent.isComposing if (e.key === 'Enter' && !e.shiftKey) { setQuery(query.replace(/\n$/, '')) e.preventDefault() } } const logError = (message: string) => { notify({ type: 'error', message }) } const handleVoiceInputShow = () => { (Recorder as any).getPermission().then(() => { setVoiceInputShow(true) }, () => { logError(t('common.voiceInput.notAllow')) }) } const [isActiveIconFocused, setActiveIconFocused] = useState(false) const media = useBreakpoints() const isMobile = media === MediaType.mobile const sendIconThemeStyle = theme ? { color: (isActiveIconFocused || query || (query.trim() !== '')) ? theme.primaryColor : '#d1d5db', } : {} const sendBtn = (
setActiveIconFocused(true)} onMouseLeave={() => setActiveIconFocused(false)} onClick={handleSend} style={isActiveIconFocused ? CssTransform(theme?.chatBubbleColorStyle ?? '') : {}} >
) return ( <>
{ visionConfig?.enabled && ( <>
= visionConfig.number_limits} />
) }