From 69f5d93bde6e0c7a723838e3b24e8fcfdac95c21 Mon Sep 17 00:00:00 2001 From: cristhianzl Date: Tue, 28 May 2024 20:31:56 -0300 Subject: [PATCH 01/76] =?UTF-8?q?=E2=9C=A8=20(chatInput):=20add=20custom?= =?UTF-8?q?=20hooks=20for=20auto-resize,=20focus=20unlock,=20and=20upload?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ♻️ (chatInput): refactor chat input component to use new custom hooks 💡 (chatInput): replace conditional rendering with Case component for clarity --- .../hooks/use-auto-resize-text-area.tsx | 14 +++ .../chatInput/hooks/use-focus-unlock.tsx | 13 ++ .../chatView/chatInput/hooks/use-upload.tsx | 57 +++++++++ .../components/chatView/chatInput/index.tsx | 117 ++++-------------- 4 files changed, 107 insertions(+), 94 deletions(-) create mode 100644 src/frontend/src/modals/IOModal/components/chatView/chatInput/hooks/use-auto-resize-text-area.tsx create mode 100644 src/frontend/src/modals/IOModal/components/chatView/chatInput/hooks/use-focus-unlock.tsx create mode 100644 src/frontend/src/modals/IOModal/components/chatView/chatInput/hooks/use-upload.tsx diff --git a/src/frontend/src/modals/IOModal/components/chatView/chatInput/hooks/use-auto-resize-text-area.tsx b/src/frontend/src/modals/IOModal/components/chatView/chatInput/hooks/use-auto-resize-text-area.tsx new file mode 100644 index 000000000..d4102fe9d --- /dev/null +++ b/src/frontend/src/modals/IOModal/components/chatView/chatInput/hooks/use-auto-resize-text-area.tsx @@ -0,0 +1,14 @@ +import { useEffect } from "react"; + +const useAutoResizeTextArea = (value, inputRef) => { + useEffect(() => { + if (inputRef.current && inputRef.current.scrollHeight! !== 0) { + inputRef.current.style!.height = "inherit"; // Reset the height + inputRef.current.style!.height = `${inputRef.current.scrollHeight!}px`; // Set it to the scrollHeight + } + }, [value]); + + return inputRef; +}; + +export default useAutoResizeTextArea; diff --git a/src/frontend/src/modals/IOModal/components/chatView/chatInput/hooks/use-focus-unlock.tsx b/src/frontend/src/modals/IOModal/components/chatView/chatInput/hooks/use-focus-unlock.tsx new file mode 100644 index 000000000..15dfe70ae --- /dev/null +++ b/src/frontend/src/modals/IOModal/components/chatView/chatInput/hooks/use-focus-unlock.tsx @@ -0,0 +1,13 @@ +import { useEffect } from "react"; + +const useFocusOnUnlock = (lockChat, inputRef) => { + useEffect(() => { + if (!lockChat && inputRef.current) { + inputRef.current.focus(); + } + }, [lockChat, inputRef]); + + return inputRef; +}; + +export default useFocusOnUnlock; diff --git a/src/frontend/src/modals/IOModal/components/chatView/chatInput/hooks/use-upload.tsx b/src/frontend/src/modals/IOModal/components/chatView/chatInput/hooks/use-upload.tsx new file mode 100644 index 000000000..30e6559e6 --- /dev/null +++ b/src/frontend/src/modals/IOModal/components/chatView/chatInput/hooks/use-upload.tsx @@ -0,0 +1,57 @@ +import { useEffect } from "react"; +import { uploadFile } from "../../../../../../controllers/API"; + +const useUpload = (uploadFile, currentFlowId, setFiles, uid) => { + useEffect(() => { + const handlePaste = (event: ClipboardEvent): void => { + const items = event.clipboardData?.items; + if (items) { + for (let i = 0; i < items.length; i++) { + const type = items[0].type.split("/")[0]; + + const blob = items[i].getAsFile(); + if (blob) { + const id = uid(); + setFiles((prevFiles) => [ + ...prevFiles, + { file: blob, loading: true, error: false, id, type }, + ]); + + uploadFiles(blob, currentFlowId, setFiles, id); + } + } + } + }; + + document.addEventListener("paste", handlePaste); + return () => { + document.removeEventListener("paste", handlePaste); + }; + }, [uploadFile, currentFlowId]); + + return null; +}; + +const uploadFiles = (blob, currentFlowId, setFiles, id) => { + uploadFile(blob, currentFlowId) + .then((res) => { + setFiles((prev) => { + const newFiles = [...prev]; + const updatedIndex = newFiles.findIndex((file) => file.id === id); + newFiles[updatedIndex].loading = false; + newFiles[updatedIndex].path = res.data.file_path; + return newFiles; + }); + }) + .catch(() => { + setFiles((prev) => { + const newFiles = [...prev]; + const updatedIndex = newFiles.findIndex((file) => file.id === id); + newFiles[updatedIndex].loading = false; + newFiles[updatedIndex].error = true; + return newFiles; + }); + }); +}; + +export default useUpload; diff --git a/src/frontend/src/modals/IOModal/components/chatView/chatInput/index.tsx b/src/frontend/src/modals/IOModal/components/chatView/chatInput/index.tsx index 1ef238f79..8a57fc768 100644 --- a/src/frontend/src/modals/IOModal/components/chatView/chatInput/index.tsx +++ b/src/frontend/src/modals/IOModal/components/chatView/chatInput/index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useState } from "react"; import ShortUniqueId from "short-unique-id"; import IconComponent from "../../../../../components/genericIconComponent"; import { Textarea } from "../../../../../components/ui/textarea"; @@ -7,6 +7,7 @@ import { CHAT_INPUT_PLACEHOLDER_SEND, } from "../../../../../constants/constants"; import { uploadFile } from "../../../../../controllers/API"; +import { Case } from "../../../../../shared/components/caseComponent"; import useFlowsManagerStore from "../../../../../stores/flowsManagerStore"; import { FilePreviewType, @@ -14,6 +15,9 @@ import { } from "../../../../../types/components"; import { classNames } from "../../../../../utils/utils"; import FilePreview from "../filePreviewChat"; +import useAutoResizeTextArea from "./hooks/use-auto-resize-text-area"; +import useFocusOnUnlock from "./hooks/use-focus-unlock"; +import useUpload from "./hooks/use-upload"; export default function ChatInput({ lockChat, chatValue, @@ -28,74 +32,17 @@ export default function ChatInput({ const uid = new ShortUniqueId({ length: 3 }); const [files, setFiles] = useState([]); - useEffect(() => { - if (!lockChat && inputRef.current) { - inputRef.current.focus(); - } - }, [lockChat, inputRef]); + useFocusOnUnlock(lockChat, inputRef); + useAutoResizeTextArea(chatValue, inputRef); + useUpload(uploadFile, currentFlowId, setFiles, uid); - useEffect(() => { - if (inputRef.current && inputRef.current.scrollHeight !== 0) { - inputRef.current.style.height = "inherit"; // Reset the height - inputRef.current.style.height = `${inputRef.current.scrollHeight}px`; // Set it to the scrollHeight - } - }, [chatValue]); - - //listen to ctrl-v to paste images - useEffect(() => { - const handlePaste = (event: ClipboardEvent): void => { - const items = event.clipboardData?.items; - if (items) { - for (let i = 0; i < items.length; i++) { - if (items[i].type.indexOf("image") !== -1) { - const blob = items[i].getAsFile(); - if (blob) { - const id = uid(); - setFiles([ - ...files, - { file: blob, loading: true, error: false, id }, - ]); - uploadFile(blob, currentFlowId) - .then((res) => { - setFiles((prev) => { - const newFiles = [...prev]; - const updatedIndex = newFiles.findIndex( - (file) => file.id === id - ); - newFiles[updatedIndex].loading = false; - newFiles[updatedIndex].path = res.data.file_path; - return newFiles; - }); - }) - .catch((err) => { - setFiles((prev) => { - const newFiles = [...prev]; - const updatedIndex = newFiles.findIndex( - (file) => file.id === id - ); - newFiles[updatedIndex].loading = false; - newFiles[updatedIndex].error = true; - return newFiles; - }); - }); - } - } - } - } - }; - document.addEventListener("paste", handlePaste); - return () => { - document.removeEventListener("paste", handlePaste); - }; - }, []); - - function send() { + const send = () => { sendMessage({ repeat, files: files.map((file) => file.path ?? "").filter((file) => file !== ""), }); setFiles([]); - } + }; return (
@@ -135,10 +82,10 @@ export default function ChatInput({ lockChat || saveLoading ? " form-modal-lock-true bg-input" : noInput - ? "form-modal-no-input bg-input" - : " form-modal-lock-false bg-background", + ? "form-modal-no-input bg-input" + : " form-modal-lock-false bg-background", - "form-modal-lockchat" + "form-modal-lockchat", )} placeholder={ noInput ? CHAT_INPUT_PLACEHOLDER : CHAT_INPUT_PLACEHOLDER_SEND @@ -151,31 +98,35 @@ export default function ChatInput({ noInput ? "bg-high-indigo text-background" : chatValue === "" - ? "text-primary" - : "bg-chat-send text-background" + ? "text-primary" + : "bg-chat-send text-background", )} disabled={lockChat || saveLoading} onClick={(): void => send()} > - {lockChat || saveLoading ? ( + + + + +
@@ -188,32 +139,10 @@ export default function ChatInput({ key={file.id} onDelete={() => { setFiles((prev) => prev.filter((f) => f.id !== file.id)); - // TODO: delete file on backend }} /> ))} - {/* - - - - - -
- Repetitions: - { - handleChange(parseInt(e.target.value)); - }} - className="w-16" - type="number" - min={0} - /> -
-
-
*/} ); } From 5638e2909c507390115f2b7b4d779197cc97ac3b Mon Sep 17 00:00:00 2001 From: cristhianzl Date: Wed, 29 May 2024 10:46:51 -0300 Subject: [PATCH 02/76] =?UTF-8?q?=E2=9C=A8=20(chatInput):=20add=20drag-and?= =?UTF-8?q?-drop=20file=20upload=20functionality=20=E2=99=BB=EF=B8=8F=20(c?= =?UTF-8?q?hatInput):=20refactor=20file=20upload=20logic=20into=20a=20reus?= =?UTF-8?q?able=20hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chatInput/hooks/use-drag-and-drop.tsx | 55 +++++++++++++++++++ .../chatInput/hooks/use-file-upload.tsx | 27 +++++++++ .../chatView/chatInput/hooks/use-upload.tsx | 32 ++--------- 3 files changed, 87 insertions(+), 27 deletions(-) create mode 100644 src/frontend/src/modals/IOModal/components/chatView/chatInput/hooks/use-drag-and-drop.tsx create mode 100644 src/frontend/src/modals/IOModal/components/chatView/chatInput/hooks/use-file-upload.tsx diff --git a/src/frontend/src/modals/IOModal/components/chatView/chatInput/hooks/use-drag-and-drop.tsx b/src/frontend/src/modals/IOModal/components/chatView/chatInput/hooks/use-drag-and-drop.tsx new file mode 100644 index 000000000..c374cf550 --- /dev/null +++ b/src/frontend/src/modals/IOModal/components/chatView/chatInput/hooks/use-drag-and-drop.tsx @@ -0,0 +1,55 @@ +import ShortUniqueId from "short-unique-id"; +import useFileUpload from "./use-file-upload"; + +const useDragAndDrop = (setIsDragging, setFiles, currentFlowId) => { + const dragOver = (e) => { + e.preventDefault(); + if (e.dataTransfer.types.some((type) => type === "Files")) { + setIsDragging(true); + } + }; + + const dragEnter = (e) => { + if (e.dataTransfer.types.some((type) => type === "Files")) { + setIsDragging(true); + } + e.preventDefault(); + }; + + const dragLeave = (e) => { + e.preventDefault(); + setIsDragging(false); + }; + + const onDrop = (e) => { + e.preventDefault(); + if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { + handleFiles(e.dataTransfer.files, setFiles, currentFlowId); + e.dataTransfer.clearData(); + } + setIsDragging(false); + }; + return { + dragOver, + dragEnter, + dragLeave, + onDrop, + }; +}; + +const handleFiles = (files, setFiles, currentFlowId) => { + if (files) { + const uid = new ShortUniqueId({ length: 3 }); + const id = uid(); + const type = files[0].type.split("/")[0]; + const blob = files[0]; + + setFiles((prevFiles) => [ + ...prevFiles, + { file: blob, loading: true, error: false, id, type }, + ]); + + useFileUpload(blob, currentFlowId, setFiles, id); + } +}; +export default useDragAndDrop; diff --git a/src/frontend/src/modals/IOModal/components/chatView/chatInput/hooks/use-file-upload.tsx b/src/frontend/src/modals/IOModal/components/chatView/chatInput/hooks/use-file-upload.tsx new file mode 100644 index 000000000..4acc2ed1b --- /dev/null +++ b/src/frontend/src/modals/IOModal/components/chatView/chatInput/hooks/use-file-upload.tsx @@ -0,0 +1,27 @@ +import { uploadFile } from "../../../../../../controllers/API"; + +const useFileUpload = (blob, currentFlowId, setFiles, id) => { + uploadFile(blob, currentFlowId) + .then((res) => { + setFiles((prev) => { + const newFiles = [...prev]; + const updatedIndex = newFiles.findIndex((file) => file.id === id); + newFiles[updatedIndex].loading = false; + newFiles[updatedIndex].path = res.data.file_path; + return newFiles; + }); + }) + .catch(() => { + setFiles((prev) => { + const newFiles = [...prev]; + const updatedIndex = newFiles.findIndex((file) => file.id === id); + newFiles[updatedIndex].loading = false; + newFiles[updatedIndex].error = true; + return newFiles; + }); + }); + + return null; +}; + +export default useFileUpload; diff --git a/src/frontend/src/modals/IOModal/components/chatView/chatInput/hooks/use-upload.tsx b/src/frontend/src/modals/IOModal/components/chatView/chatInput/hooks/use-upload.tsx index 30e6559e6..487bd7db8 100644 --- a/src/frontend/src/modals/IOModal/components/chatView/chatInput/hooks/use-upload.tsx +++ b/src/frontend/src/modals/IOModal/components/chatView/chatInput/hooks/use-upload.tsx @@ -1,14 +1,15 @@ import { useEffect } from "react"; -import { uploadFile } from "../../../../../../controllers/API"; +import ShortUniqueId from "short-unique-id"; +import useFileUpload from "./use-file-upload"; -const useUpload = (uploadFile, currentFlowId, setFiles, uid) => { +const useUpload = (uploadFile, currentFlowId, setFiles) => { useEffect(() => { const handlePaste = (event: ClipboardEvent): void => { const items = event.clipboardData?.items; if (items) { for (let i = 0; i < items.length; i++) { const type = items[0].type.split("/")[0]; - + const uid = new ShortUniqueId({ length: 3 }); const blob = items[i].getAsFile(); if (blob) { const id = uid(); @@ -16,8 +17,7 @@ const useUpload = (uploadFile, currentFlowId, setFiles, uid) => { ...prevFiles, { file: blob, loading: true, error: false, id, type }, ]); - - uploadFiles(blob, currentFlowId, setFiles, id); + useFileUpload(blob, currentFlowId, setFiles, id); } } } @@ -32,26 +32,4 @@ const useUpload = (uploadFile, currentFlowId, setFiles, uid) => { return null; }; -const uploadFiles = (blob, currentFlowId, setFiles, id) => { - uploadFile(blob, currentFlowId) - .then((res) => { - setFiles((prev) => { - const newFiles = [...prev]; - const updatedIndex = newFiles.findIndex((file) => file.id === id); - newFiles[updatedIndex].loading = false; - newFiles[updatedIndex].path = res.data.file_path; - return newFiles; - }); - }) - .catch(() => { - setFiles((prev) => { - const newFiles = [...prev]; - const updatedIndex = newFiles.findIndex((file) => file.id === id); - newFiles[updatedIndex].loading = false; - newFiles[updatedIndex].error = true; - return newFiles; - }); - }); -}; - export default useUpload; From 7473eea263956207881badb9d9a229fe6c1c989b Mon Sep 17 00:00:00 2001 From: cristhianzl Date: Wed, 29 May 2024 10:47:05 -0300 Subject: [PATCH 03/76] =?UTF-8?q?=E2=9C=A8=20(chatInput):=20add=20ButtonSe?= =?UTF-8?q?ndWrapper=20component=20for=20better=20modularity=20=E2=9C=A8?= =?UTF-8?q?=20(chatInput):=20add=20TextAreaWrapper=20component=20for=20bet?= =?UTF-8?q?ter=20modularity=20=E2=99=BB=EF=B8=8F=20(chatInput):=20refactor?= =?UTF-8?q?=20chat=20input=20logic=20to=20use=20new=20wrapper=20components?= =?UTF-8?q?=20=E2=99=BB=EF=B8=8F=20(chatView):=20clean=20up=20imports=20an?= =?UTF-8?q?d=20improve=20code=20readability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/buttonSendWrapper/index.tsx | 52 +++++++ .../components/chatView/chatInput/index.tsx | 130 ++++++------------ .../IOModal/components/chatView/index.tsx | 21 ++- 3 files changed, 105 insertions(+), 98 deletions(-) create mode 100644 src/frontend/src/modals/IOModal/components/chatView/chatInput/components/buttonSendWrapper/index.tsx diff --git a/src/frontend/src/modals/IOModal/components/chatView/chatInput/components/buttonSendWrapper/index.tsx b/src/frontend/src/modals/IOModal/components/chatView/chatInput/components/buttonSendWrapper/index.tsx new file mode 100644 index 000000000..ff93a9f7b --- /dev/null +++ b/src/frontend/src/modals/IOModal/components/chatView/chatInput/components/buttonSendWrapper/index.tsx @@ -0,0 +1,52 @@ +import IconComponent from "../../../../../../../components/genericIconComponent"; +import { Case } from "../../../../../../../shared/components/caseComponent"; +import { classNames } from "../../../../../../../utils/utils"; + +const ButtonSendWrapper = ({ + send, + lockChat, + noInput, + saveLoading, + chatValue, +}) => { + return ( + + ); +}; + +export default ButtonSendWrapper; diff --git a/src/frontend/src/modals/IOModal/components/chatView/chatInput/index.tsx b/src/frontend/src/modals/IOModal/components/chatView/chatInput/index.tsx index 8a57fc768..8fe8ba09b 100644 --- a/src/frontend/src/modals/IOModal/components/chatView/chatInput/index.tsx +++ b/src/frontend/src/modals/IOModal/components/chatView/chatInput/index.tsx @@ -1,21 +1,19 @@ import { useState } from "react"; -import ShortUniqueId from "short-unique-id"; -import IconComponent from "../../../../../components/genericIconComponent"; -import { Textarea } from "../../../../../components/ui/textarea"; import { CHAT_INPUT_PLACEHOLDER, CHAT_INPUT_PLACEHOLDER_SEND, } from "../../../../../constants/constants"; import { uploadFile } from "../../../../../controllers/API"; -import { Case } from "../../../../../shared/components/caseComponent"; import useFlowsManagerStore from "../../../../../stores/flowsManagerStore"; import { FilePreviewType, chatInputType, } from "../../../../../types/components"; -import { classNames } from "../../../../../utils/utils"; import FilePreview from "../filePreviewChat"; +import ButtonSendWrapper from "./components/buttonSendWrapper"; +import TextAreaWrapper from "./components/textAreaWrapper"; import useAutoResizeTextArea from "./hooks/use-auto-resize-text-area"; +import useDragAndDrop from "./hooks/use-drag-and-drop"; import useFocusOnUnlock from "./hooks/use-focus-unlock"; import useUpload from "./hooks/use-upload"; export default function ChatInput({ @@ -29,12 +27,18 @@ export default function ChatInput({ const [repeat, setRepeat] = useState(1); const saveLoading = useFlowsManagerStore((state) => state.saveLoading); const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId); - const uid = new ShortUniqueId({ length: 3 }); const [files, setFiles] = useState([]); + const [isDragging, setIsDragging] = useState(false); useFocusOnUnlock(lockChat, inputRef); useAutoResizeTextArea(chatValue, inputRef); - useUpload(uploadFile, currentFlowId, setFiles, uid); + useUpload(uploadFile, currentFlowId, setFiles); + + const { dragOver, dragEnter, dragLeave, onDrop } = useDragAndDrop( + setIsDragging, + setFiles, + currentFlowId, + ); const send = () => { sendMessage({ @@ -44,90 +48,44 @@ export default function ChatInput({ setFiles([]); }; + const checkSendingOk = (event: React.KeyboardEvent) => { + return ( + event.key === "Enter" && + !lockChat && + !saveLoading && + !event.shiftKey && + !event.nativeEvent.isComposing + ); + }; + return (
-