diff --git a/ui/src/components/MessageInput.tsx b/ui/src/components/MessageInput.tsx index 416883fed48b713603edef36d5c67e6179cea02d..0853b7cc3102d48982d9012c45649fe90ac927f3 100644 --- a/ui/src/components/MessageInput.tsx +++ b/ui/src/components/MessageInput.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef, useEffect, useCallback } from "react"; +import React, { useState, useRef, useEffect, useCallback, useMemo } from "react"; // Web Speech API types interface SpeechRecognitionEvent extends Event { @@ -76,7 +76,12 @@ function MessageInput({ const [uploadsInProgress, setUploadsInProgress] = useState(0); const [dragCounter, setDragCounter] = useState(0); const [isListening, setIsListening] = useState(false); + const [isSmallScreen, setIsSmallScreen] = useState(() => { + if (typeof window === "undefined") return false; + return window.innerWidth < 480; + }); const textareaRef = useRef(null); + const fileInputRef = useRef(null); const recognitionRef = useRef(null); // Track the base text (before speech recognition started) and finalized speech text const baseTextRef = useRef(""); @@ -86,6 +91,21 @@ function MessageInput({ const speechRecognitionAvailable = typeof window !== "undefined" && (window.SpeechRecognition || window.webkitSpeechRecognition); + // Responsive placeholder text + const placeholderText = useMemo( + () => (isSmallScreen ? "Message..." : "Message, paste image, or attach file..."), + [isSmallScreen], + ); + + // Track screen size for responsive placeholder + useEffect(() => { + const handleResize = () => { + setIsSmallScreen(window.innerWidth < 480); + }; + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); + }, []); + const stopListening = useCallback(() => { if (recognitionRef.current) { recognitionRef.current.stop(); @@ -170,13 +190,10 @@ function MessageInput({ }; }, []); - const uploadFile = async (file: File, insertPosition: number) => { - const textBefore = message.substring(0, insertPosition); - const textAfter = message.substring(insertPosition); - - // Add a loading indicator + const uploadFile = async (file: File) => { + // Add a loading indicator at the end of the current message const loadingText = `[uploading ${file.name}...]`; - setMessage(`${textBefore}${loadingText}${textAfter}`); + setMessage((prev) => (prev ? prev + " " : "") + loadingText); setUploadsInProgress((prev) => prev + 1); try { @@ -218,8 +235,7 @@ function MessageInput({ const file = item.getAsFile(); if (file) { event.preventDefault(); - const cursorPos = textareaRef.current?.selectionStart ?? message.length; - await uploadFile(file, cursorPos); + await uploadFile(file); return; } } @@ -253,17 +269,28 @@ function MessageInput({ // Process all dropped files for (let i = 0; i < event.dataTransfer.files.length; i++) { const file = event.dataTransfer.files[i]; - const insertPosition = - i === 0 ? (textareaRef.current?.selectionStart ?? message.length) : message.length; - await uploadFile(file, insertPosition); - // Add a space between files - if (i < event.dataTransfer.files.length - 1) { - setMessage((prev) => prev + " "); - } + await uploadFile(file); } } }; + const handleAttachClick = () => { + fileInputRef.current?.click(); + }; + + const handleFileSelect = async (event: React.ChangeEvent) => { + const files = event.target.files; + if (!files || files.length === 0) return; + + for (let i = 0; i < files.length; i++) { + const file = files[i]; + await uploadFile(file); + } + + // Reset input so same file can be selected again + event.target.value = ""; + }; + // Auto-insert injected text (diff comments) directly into the textarea useEffect(() => { if (injectedText) { @@ -374,6 +401,15 @@ function MessageInput({ )}
+