From 681fd1d0e601647b3adb00a02d0c7ecda5f15459 Mon Sep 17 00:00:00 2001 From: brownrw8 Date: Wed, 22 Jan 2025 07:28:43 -1000 Subject: [PATCH] feat: drag-n-drop on shift (single file solution) --- .../src/components/chat/ChatTextArea.tsx | 91 ++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index fcf613e9f5..b9fbcbd2aa 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -746,6 +746,93 @@ const ChatTextArea = forwardRef( } }, [showModelSelector]) + /** + * Handles the drag over event to allow dropping. + * Prevents the default behavior to enable drop. + * + * @param {React.DragEvent} e - The drag event. + */ + const onDragOver = (e: React.DragEvent) => { + e.preventDefault() + } + + /** + * Handles the drop event for files and text. + * Processes dropped images and text, updating the state accordingly. + * + * @param {React.DragEvent} e - The drop event. + */ + const onDrop = async (e: React.DragEvent) => { + e.preventDefault() + + const files = Array.from(e.dataTransfer.files) + const text = e.dataTransfer.getData("text") + + if (text) { + handleTextDrop(text) + return + } + + const acceptedTypes = ["png", "jpeg", "webp"] + const imageFiles = files.filter((file) => { + const [type, subtype] = file.type.split("/") + return type === "image" && acceptedTypes.includes(subtype) + }) + + if (shouldDisableImages || imageFiles.length === 0) return + + const imageDataArray = await readImageFiles(imageFiles) + const dataUrls = imageDataArray.filter((dataUrl): dataUrl is string => dataUrl !== null) + + if (dataUrls.length > 0) { + setSelectedImages((prevImages) => [...prevImages, ...dataUrls].slice(0, MAX_IMAGES_PER_MESSAGE)) + } else { + console.warn("No valid images were processed") + } + } + + /** + * Handles the drop event for text. + * Inserts the dropped text at the current cursor position. + * + * @param {string} text - The dropped text. + */ + const handleTextDrop = (text: string) => { + const newValue = inputValue.slice(0, cursorPosition) + text + inputValue.slice(cursorPosition) + setInputValue(newValue) + const newCursorPosition = cursorPosition + text.length + setCursorPosition(newCursorPosition) + setIntendedCursorPosition(newCursorPosition) + } + + /** + * Reads image files and returns their data URLs. + * Uses FileReader to read the files as data URLs. + * + * @param {File[]} imageFiles - The image files to read. + * @returns {Promise<(string | null)[]>} - A promise that resolves to an array of data URLs or null values. + */ + const readImageFiles = (imageFiles: File[]): Promise<(string | null)[]> => { + return Promise.all( + imageFiles.map( + (file) => + new Promise((resolve) => { + const reader = new FileReader() + reader.onloadend = () => { + if (reader.error) { + console.error("Error reading file:", reader.error) + resolve(null) + } else { + const result = reader.result + resolve(typeof result === "string" ? result : null) + } + } + reader.readAsDataURL(file) + }), + ), + ) + } + return (
( opacity: textAreaDisabled ? 0.5 : 1, position: "relative", display: "flex", - }}> + }} + onDrop={onDrop} + onDragOver={onDragOver}> {showContextMenu && (