import { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; import endpoints from '../api/endpoints'; import userService from '../api/services/userService'; import AlertIcon from '../assets/alert.svg'; import ClipIcon from '../assets/clip.svg'; import ExitIcon from '../assets/exit.svg'; import PaperPlane from '../assets/paper_plane.svg'; import SourceIcon from '../assets/source.svg'; import DocumentationDark from '../assets/documentation-dark.svg'; import SpinnerDark from '../assets/spinner-dark.svg'; import Spinner from '../assets/spinner.svg'; import ToolIcon from '../assets/tool.svg'; import { addAttachment, removeAttachment, selectAttachments, updateAttachment, } from '../upload/uploadSlice'; import { useDarkTheme } from '../hooks'; import { ActiveState } from '../models/misc'; import { selectSelectedDocs, selectToken, } from '../preferences/preferenceSlice'; import Upload from '../upload/Upload'; import { getOS, isTouchDevice } from '../utils/browserUtils'; import SourcesPopup from './SourcesPopup'; import ToolsPopup from './ToolsPopup'; type MessageInputProps = { onSubmit: (text: string) => void; loading: boolean; showSourceButton?: boolean; showToolButton?: boolean; autoFocus?: boolean; }; export default function MessageInput({ onSubmit, loading, showSourceButton = true, showToolButton = true, autoFocus = true, }: MessageInputProps) { const { t } = useTranslation(); const [isDarkTheme] = useDarkTheme(); const [value, setValue] = useState(''); const inputRef = useRef(null); const sourceButtonRef = useRef(null); const toolButtonRef = useRef(null); const [isSourcesPopupOpen, setIsSourcesPopupOpen] = useState(false); const [isToolsPopupOpen, setIsToolsPopupOpen] = useState(false); const [uploadModalState, setUploadModalState] = useState('INACTIVE'); const selectedDocs = useSelector(selectSelectedDocs); const token = useSelector(selectToken); const attachments = useSelector(selectAttachments); const dispatch = useDispatch(); const browserOS = getOS(); const isTouch = isTouchDevice(); useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if ( ((browserOS === 'win' || browserOS === 'linux') && event.ctrlKey && event.key === 'k') || (browserOS === 'mac' && event.metaKey && event.key === 'k') ) { event.preventDefault(); setIsSourcesPopupOpen(!isSourcesPopupOpen); } }; document.addEventListener('keydown', handleKeyDown); return () => { document.removeEventListener('keydown', handleKeyDown); }; }, [browserOS]); const handleFileAttachment = (e: React.ChangeEvent) => { if (!e.target.files || e.target.files.length === 0) return; const file = e.target.files[0]; const formData = new FormData(); formData.append('file', file); const apiHost = import.meta.env.VITE_API_HOST; const xhr = new XMLHttpRequest(); const newAttachment = { fileName: file.name, progress: 0, status: 'uploading' as const, taskId: '', }; dispatch(addAttachment(newAttachment)); xhr.upload.addEventListener('progress', (event) => { if (event.lengthComputable) { const progress = Math.round((event.loaded / event.total) * 100); dispatch( updateAttachment({ taskId: newAttachment.taskId, updates: { progress }, }), ); } }); xhr.onload = () => { if (xhr.status === 200) { const response = JSON.parse(xhr.responseText); if (response.task_id) { dispatch( updateAttachment({ taskId: newAttachment.taskId, updates: { taskId: response.task_id, status: 'processing', progress: 10, }, }), ); } } else { dispatch( updateAttachment({ taskId: newAttachment.taskId, updates: { status: 'failed' }, }), ); } }; xhr.onerror = () => { dispatch( updateAttachment({ taskId: newAttachment.taskId, updates: { status: 'failed' }, }), ); }; xhr.open('POST', `${apiHost}${endpoints.USER.STORE_ATTACHMENT}`); xhr.setRequestHeader('Authorization', `Bearer ${token}`); xhr.send(formData); e.target.value = ''; }; useEffect(() => { const checkTaskStatus = () => { const processingAttachments = attachments.filter( (att) => att.status === 'processing' && att.taskId, ); processingAttachments.forEach((attachment) => { userService .getTaskStatus(attachment.taskId!, null) .then((data) => data.json()) .then((data) => { if (data.status === 'SUCCESS') { dispatch( updateAttachment({ taskId: attachment.taskId!, updates: { status: 'completed', progress: 100, id: data.result?.attachment_id, token_count: data.result?.token_count, }, }), ); } else if (data.status === 'FAILURE') { dispatch( updateAttachment({ taskId: attachment.taskId!, updates: { status: 'failed' }, }), ); } else if (data.status === 'PROGRESS' && data.result?.current) { dispatch( updateAttachment({ taskId: attachment.taskId!, updates: { progress: data.result.current }, }), ); } }) .catch(() => { dispatch( updateAttachment({ taskId: attachment.taskId!, updates: { status: 'failed' }, }), ); }); }); }; const interval = setInterval(() => { if (attachments.some((att) => att.status === 'processing')) { checkTaskStatus(); } }, 2000); return () => clearInterval(interval); }, [attachments, dispatch]); const handleInput = () => { if (inputRef.current) { if (window.innerWidth < 350) inputRef.current.style.height = 'auto'; else inputRef.current.style.height = '64px'; inputRef.current.style.height = `${Math.min( inputRef.current.scrollHeight, 96, )}px`; } }; useEffect(() => { if (autoFocus) inputRef.current?.focus(); handleInput(); }, []); const handleChange = (e: React.ChangeEvent) => { setValue(e.target.value); handleInput(); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSubmit(); if (inputRef.current) { inputRef.current.value = ''; handleInput(); } } }; const handlePostDocumentSelect = (doc: any) => { console.log('Selected document:', doc); }; const handleSubmit = () => { if (value.trim() && !loading) { onSubmit(value); setValue(''); } }; return (
{attachments.map((attachment, index) => (
{attachment.status === 'completed' && ( Attachment )} {attachment.status === 'failed' && ( Failed )} {(attachment.status === 'uploading' || attachment.status === 'processing') && (
)}
{attachment.fileName}
))}