diff --git a/web-app/src/containers/ChatInput.tsx b/web-app/src/containers/ChatInput.tsx index a8ceaed04..97990281f 100644 --- a/web-app/src/containers/ChatInput.tsx +++ b/web-app/src/containers/ChatInput.tsx @@ -1,7 +1,7 @@ 'use client' import TextareaAutosize from 'react-textarea-autosize' -import { cn } from '@/lib/utils' +import { cn, toGigabytes } from '@/lib/utils' import { usePrompt } from '@/hooks/usePrompt' import { useThreads } from '@/hooks/useThreads' import { useCallback, useEffect, useRef, useState } from 'react' @@ -58,6 +58,15 @@ const ChatInput = ({ const { selectedModel } = useModelProvider() const { sendMessage } = useChat() const [message, setMessage] = useState('') + const [uploadedFiles, setUploadedFiles] = useState< + Array<{ + name: string + type: string + size: number + base64: string + dataUrl: string + }> + >([]) const handleSendMesage = (prompt: string) => { if (!selectedModel) { @@ -137,6 +146,122 @@ const ChatInput = ({ [abortControllers] ) + const fileInputRef = useRef(null) + + const handleAttachmentClick = () => { + fileInputRef.current?.click() + } + + const handleRemoveFile = (indexToRemove: number) => { + setUploadedFiles((prev) => + prev.filter((_, index) => index !== indexToRemove) + ) + } + + const getFileTypeFromExtension = (fileName: string): string => { + const extension = fileName.toLowerCase().split('.').pop() + switch (extension) { + case 'jpg': + case 'jpeg': + return 'image/jpeg' + case 'png': + return 'image/png' + case 'pdf': + return 'application/pdf' + default: + return '' + } + } + + const handleFileChange = (e: React.ChangeEvent) => { + const files = e.target.files + + if (files && files.length > 0) { + const maxSize = 10 * 1024 * 1024 // 10MB in bytes + const newFiles: Array<{ + name: string + type: string + size: number + base64: string + dataUrl: string + }> = [] + + Array.from(files).forEach((file) => { + // Check file size + if (file.size > maxSize) { + setMessage(`File is too large. Maximum size is 10MB.`) + // Reset file input to allow re-uploading + if (fileInputRef.current) { + fileInputRef.current.value = '' + } + return + } + + // Get file type - use extension as fallback if MIME type is incorrect + const detectedType = file.type || getFileTypeFromExtension(file.name) + const actualType = getFileTypeFromExtension(file.name) || detectedType + + // Check file type + const allowedTypes = [ + 'image/jpg', + 'image/jpeg', + 'image/png', + 'application/pdf', + ] + + if (!allowedTypes.includes(actualType)) { + setMessage( + `File is not supported. Only JPEG, JPG, PNG, and PDF files are allowed.` + ) + // Reset file input to allow re-uploading + if (fileInputRef.current) { + fileInputRef.current.value = '' + } + return + } + + const reader = new FileReader() + reader.onload = () => { + const result = reader.result + if (typeof result === 'string') { + const base64String = result.split(',')[1] + const fileData = { + name: file.name, + size: file.size, + type: actualType, + base64: base64String, + dataUrl: result, + } + newFiles.push(fileData) + // Update state + if ( + newFiles.length === + Array.from(files).filter((f) => { + const fType = getFileTypeFromExtension(f.name) || f.type + return f.size <= maxSize && allowedTypes.includes(fType) + }).length + ) { + setUploadedFiles((prev) => { + const updated = [...prev, ...newFiles] + return updated + }) + // Reset the file input value to allow re-uploading the same file + if (fileInputRef.current) { + fileInputRef.current.value = '' + setMessage('') + } + } + } + } + reader.readAsDataURL(file) + }) + } + + if (textareaRef.current) { + textareaRef.current.focus() + } + } + return (
@@ -157,12 +282,61 @@ const ChatInput = ({
)} +
+ {uploadedFiles.length > 0 && ( +
+ {uploadedFiles.map((file, index) => { + return ( +
+ {file.type.startsWith('image/') && ( + {`${file.name} + )} + {file.type === 'application/pdf' && ( +
+
+
+ + {file.name.split('.').pop()} + +
+
+
+ {file.name} +
+

+ {toGigabytes(file.size)} +

+
+
+
+ )} +
handleRemoveFile(index)} + > + +
+
+ ) + })} +
+ )} +
+
{/* Microphone - always available - Temp Hide */} @@ -272,7 +455,9 @@ const ChatInput = ({ @@ -293,7 +478,7 @@ const ChatInput = ({
- {message && !selectedModel && ( + {message && (
{message} @@ -301,6 +486,10 @@ const ChatInput = ({ className="size-3 text-main-view-fg/30 cursor-pointer" onClick={() => { setMessage('') + // Reset file input to allow re-uploading the same file + if (fileInputRef.current) { + fileInputRef.current.value = '' + } }} />
diff --git a/web-app/src/containers/ThreadContent.tsx b/web-app/src/containers/ThreadContent.tsx index 711543868..199d8484f 100644 --- a/web-app/src/containers/ThreadContent.tsx +++ b/web-app/src/containers/ThreadContent.tsx @@ -182,9 +182,9 @@ export const ThreadContent = memo( - +

Edit

@@ -331,9 +331,9 @@ export const ThreadContent = memo( - +

Metadata