import { useEffect, useRef,useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useDarkTheme } from '../hooks'; import { useSelector, useDispatch } from 'react-redux'; import userService from '../api/services/userService'; import endpoints from '../api/endpoints'; import { getOS, isTouchDevice } from '../utils/browserUtils'; import PaperPlane from '../assets/paper_plane.svg'; import SourceIcon from '../assets/source.svg'; import ToolIcon from '../assets/tool.svg'; import SpinnerDark from '../assets/spinner-dark.svg'; import Spinner from '../assets/spinner.svg'; import ExitIcon from '../assets/exit.svg'; import AlertIcon from '../assets/alert.svg'; import SourcesPopup from './SourcesPopup'; import ToolsPopup from './ToolsPopup'; import { selectSelectedDocs, selectToken } from '../preferences/preferenceSlice'; import { ActiveState } from '../models/misc'; import Upload from '../upload/Upload'; import ClipIcon from '../assets/clip.svg'; import { addAttachment, updateAttachment, removeAttachment, selectAttachments } from '../conversation/conversationSlice'; interface MessageInputProps { value: string; onChange: (e: React.ChangeEvent) => void; onSubmit: () => void; loading: boolean; } interface UploadState { taskId: string; fileName: string; progress: number; attachment_id?: string; token_count?: number; status: 'uploading' | 'processing' | 'completed' | 'failed'; } export default function MessageInput({ value, onChange, onSubmit, loading, }: MessageInputProps) { const { t } = useTranslation(); const [isDarkTheme] = useDarkTheme(); 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(() => { inputRef.current?.focus(); 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 = () => { onSubmit(); }; return (
{attachments.map((attachment, index) => (
{attachment.fileName} {attachment.status === 'completed' && ( )} {attachment.status === 'failed' && ( Upload failed )} {(attachment.status === 'uploading' || attachment.status === 'processing') && (
{/* Background circle */}
)}
))}