import { useSpeech } from '@/web/common/hooks/useSpeech'; import { useSystemStore } from '@/web/common/system/useSystemStore'; import { Box, Flex, Image, Spinner, Textarea } from '@chakra-ui/react'; import React, { useRef, useEffect, useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; import MyTooltip from '../MyTooltip'; import MyIcon from '../Icon'; import styles from './index.module.scss'; import { useRouter } from 'next/router'; import { useSelectFile } from '@/web/common/file/hooks/useSelectFile'; import { compressImgAndUpload } from '@/web/common/file/controller'; import { useToast } from '@/web/common/hooks/useToast'; import { customAlphabet } from 'nanoid'; import { IMG_BLOCK_KEY } from '@fastgpt/global/core/chat/constants'; import { addDays } from 'date-fns'; const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6); enum FileTypeEnum { image = 'image', file = 'file' } type FileItemType = { id: string; rawFile: File; type: `${FileTypeEnum}`; name: string; icon: string; // img is base64 src?: string; }; const MessageInput = ({ onChange, onSendMessage, onStop, isChatting, TextareaDom, showFileSelector = false, resetInputVal }: { onChange: (e: string) => void; onSendMessage: (e: string) => void; onStop: () => void; isChatting: boolean; showFileSelector?: boolean; TextareaDom: React.MutableRefObject; resetInputVal: (val: string) => void; }) => { const { shareId } = useRouter().query as { shareId?: string }; const { toast } = useToast(); const { isSpeaking, isTransCription, stopSpeak, startSpeak, speakingTimeString, renderAudioGraph, stream } = useSpeech({ shareId }); const { isPc } = useSystemStore(); const canvasRef = useRef(null); const { t } = useTranslation(); const textareaMinH = '22px'; const [fileList, setFileList] = useState([]); const havInput = !!TextareaDom.current?.value || fileList.length > 0; const { File, onOpen: onOpenSelectFile } = useSelectFile({ fileType: 'image/*', multiple: true, maxCount: 10 }); const uploadFile = async (file: FileItemType) => { if (file.type === FileTypeEnum.image) { try { const src = await compressImgAndUpload({ file: file.rawFile, maxW: 1000, maxH: 1000, maxSize: 1024 * 1024 * 5, // 30 day expired. expiredTime: addDays(new Date(), 30) }); setFileList((state) => state.map((item) => item.id === file.id ? { ...item, src: `${location.origin}${src}` } : item ) ); } catch (error) { setFileList((state) => state.filter((item) => item.id !== file.id)); toast({ status: 'error', title: t('common.Upload File Failed') }); } } }; const onSelectFile = useCallback(async (files: File[]) => { if (!files || files.length === 0) { return; } const loadFiles = await Promise.all( files.map( (file) => new Promise((resolve, reject) => { if (file.type.includes('image')) { const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = () => { const item = { id: nanoid(), rawFile: file, type: FileTypeEnum.image, name: file.name, icon: reader.result as string }; uploadFile(item); resolve(item); }; reader.onerror = () => { reject(reader.error); }; } else { resolve({ id: nanoid(), rawFile: file, type: FileTypeEnum.file, name: file.name, icon: 'pdf' }); } }) ) ); setFileList((state) => [...state, ...loadFiles]); }, []); const handleSend = useCallback(async () => { const textareaValue = TextareaDom.current?.value || ''; const images = fileList.filter((item) => item.type === FileTypeEnum.image); const imagesText = images.length === 0 ? '' : `\`\`\`${IMG_BLOCK_KEY} ${images.map((img) => JSON.stringify({ src: img.src })).join('\n')} \`\`\` `; const inputMessage = `${imagesText}${textareaValue}`; onSendMessage(inputMessage); setFileList([]); }, [TextareaDom, fileList, onSendMessage]); useEffect(() => { if (!stream) { return; } const audioContext = new AudioContext(); const analyser = audioContext.createAnalyser(); analyser.fftSize = 4096; analyser.smoothingTimeConstant = 1; const source = audioContext.createMediaStreamSource(stream); source.connect(analyser); const renderCurve = () => { if (!canvasRef.current) return; renderAudioGraph(analyser, canvasRef.current); window.requestAnimationFrame(renderCurve); }; renderCurve(); }, [renderAudioGraph, stream]); return ( 0 ? '10px' : ['14px', '18px']} pb={['14px', '18px']} position={'relative'} boxShadow={isSpeaking ? `0 0 10px rgba(54,111,255,0.4)` : `0 0 10px rgba(0,0,0,0.2)`} borderRadius={['none', 'md']} bg={'white'} {...(isPc ? { border: '1px solid', borderColor: 'rgba(0,0,0,0.12)' } : { borderTop: '1px solid', borderTopColor: 'rgba(0,0,0,0.15)' })} > {/* translate loading */} {t('chat.Converting to text')} {/* file preview */} {fileList.map((item) => ( {/* uploading */} {!item.src && ( )} { setFileList((state) => state.filter((file) => file.id !== item.id)); }} className="close-icon" display={['', 'none']} /> {item.type === FileTypeEnum.image && ( {'img'} )} ))} 0 ? 1 : 0} pl={[2, 4]}> {/* file selector */} {showFileSelector && ( { if (isSpeaking) return; onOpenSelectFile; }} > )} {/* input area */}