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 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 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 { setAttachments } 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 [uploads, setUploads] = useState([]); const selectedDocs = useSelector(selectSelectedDocs); const token = useSelector(selectToken); const dispatch = useDispatch(); 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 uploadState: UploadState = { taskId: '', fileName: file.name, progress: 0, status: 'uploading' }; setUploads(prev => [...prev, uploadState]); const uploadIndex = uploads.length; xhr.upload.addEventListener('progress', (event) => { if (event.lengthComputable) { const progress = Math.round((event.loaded / event.total) * 100); setUploads(prev => prev.map((upload, index) => index === uploadIndex ? { ...upload, progress } : upload )); } }); xhr.onload = () => { if (xhr.status === 200) { const response = JSON.parse(xhr.responseText); console.log('File uploaded successfully:', response); if (response.task_id) { setUploads(prev => prev.map((upload, index) => index === uploadIndex ? { ...upload, taskId: response.task_id, status: 'processing' } : upload )); } } else { setUploads(prev => prev.map((upload, index) => index === uploadIndex ? { ...upload, status: 'failed' } : upload )); console.error('Error uploading file:', xhr.responseText); } }; xhr.onerror = () => { setUploads(prev => prev.map((upload, index) => index === uploadIndex ? { ...upload, status: 'failed' } : upload )); console.error('Network error during file upload'); }; xhr.open('POST', `${apiHost}${endpoints.USER.STORE_ATTACHMENT}`); xhr.setRequestHeader('Authorization', `Bearer ${token}`); xhr.send(formData); e.target.value = ''; }; useEffect(() => { let timeoutIds: number[] = []; const checkTaskStatus = () => { const processingUploads = uploads.filter(upload => upload.status === 'processing' && upload.taskId ); processingUploads.forEach(upload => { userService .getTaskStatus(upload.taskId, null) .then((data) => data.json()) .then((data) => { console.log('Task status:', data); setUploads(prev => prev.map(u => { if (u.taskId !== upload.taskId) return u; if (data.status === 'SUCCESS') { return { ...u, status: 'completed', progress: 100, attachment_id: data.result?.attachment_id, token_count: data.result?.token_count }; } else if (data.status === 'FAILURE') { return { ...u, status: 'failed' }; } else if (data.status === 'PROGRESS' && data.result?.current) { return { ...u, progress: data.result.current }; } return u; })); if (data.status !== 'SUCCESS' && data.status !== 'FAILURE') { const timeoutId = window.setTimeout(() => checkTaskStatus(), 2000); timeoutIds.push(timeoutId); } }) .catch((error) => { console.error('Error checking task status:', error); setUploads(prev => prev.map(u => u.taskId === upload.taskId ? { ...u, status: 'failed' } : u )); }); }); }; if (uploads.some(upload => upload.status === 'processing')) { const timeoutId = window.setTimeout(checkTaskStatus, 2000); timeoutIds.push(timeoutId); } return () => { timeoutIds.forEach(id => clearTimeout(id)); }; }, [uploads]); 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 = () => { const completedAttachments = uploads .filter(upload => upload.status === 'completed' && upload.attachment_id) .map(upload => ({ fileName: upload.fileName, id: upload.attachment_id as string })); dispatch(setAttachments(completedAttachments)); onSubmit(); }; return (
{uploads.map((upload, index) => (
{upload.fileName} {upload.status === 'completed' && ( )} {upload.status === 'failed' && ( )} {(upload.status === 'uploading' || upload.status === 'processing') && (
)}
))}