✨ (chatInput): add custom hooks for auto-resize, focus unlock, and upload
♻️ (chatInput): refactor chat input component to use new custom hooks 💡 (chatInput): replace conditional rendering with Case component for clarity
This commit is contained in:
parent
3194ba7aee
commit
69f5d93bde
4 changed files with 107 additions and 94 deletions
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useState } from "react";
|
||||||
import ShortUniqueId from "short-unique-id";
|
import ShortUniqueId from "short-unique-id";
|
||||||
import IconComponent from "../../../../../components/genericIconComponent";
|
import IconComponent from "../../../../../components/genericIconComponent";
|
||||||
import { Textarea } from "../../../../../components/ui/textarea";
|
import { Textarea } from "../../../../../components/ui/textarea";
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
CHAT_INPUT_PLACEHOLDER_SEND,
|
CHAT_INPUT_PLACEHOLDER_SEND,
|
||||||
} from "../../../../../constants/constants";
|
} from "../../../../../constants/constants";
|
||||||
import { uploadFile } from "../../../../../controllers/API";
|
import { uploadFile } from "../../../../../controllers/API";
|
||||||
|
import { Case } from "../../../../../shared/components/caseComponent";
|
||||||
import useFlowsManagerStore from "../../../../../stores/flowsManagerStore";
|
import useFlowsManagerStore from "../../../../../stores/flowsManagerStore";
|
||||||
import {
|
import {
|
||||||
FilePreviewType,
|
FilePreviewType,
|
||||||
|
|
@ -14,6 +15,9 @@ import {
|
||||||
} from "../../../../../types/components";
|
} from "../../../../../types/components";
|
||||||
import { classNames } from "../../../../../utils/utils";
|
import { classNames } from "../../../../../utils/utils";
|
||||||
import FilePreview from "../filePreviewChat";
|
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({
|
export default function ChatInput({
|
||||||
lockChat,
|
lockChat,
|
||||||
chatValue,
|
chatValue,
|
||||||
|
|
@ -28,74 +32,17 @@ export default function ChatInput({
|
||||||
const uid = new ShortUniqueId({ length: 3 });
|
const uid = new ShortUniqueId({ length: 3 });
|
||||||
const [files, setFiles] = useState<FilePreviewType[]>([]);
|
const [files, setFiles] = useState<FilePreviewType[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useFocusOnUnlock(lockChat, inputRef);
|
||||||
if (!lockChat && inputRef.current) {
|
useAutoResizeTextArea(chatValue, inputRef);
|
||||||
inputRef.current.focus();
|
useUpload(uploadFile, currentFlowId, setFiles, uid);
|
||||||
}
|
|
||||||
}, [lockChat, inputRef]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const send = () => {
|
||||||
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() {
|
|
||||||
sendMessage({
|
sendMessage({
|
||||||
repeat,
|
repeat,
|
||||||
files: files.map((file) => file.path ?? "").filter((file) => file !== ""),
|
files: files.map((file) => file.path ?? "").filter((file) => file !== ""),
|
||||||
});
|
});
|
||||||
setFiles([]);
|
setFiles([]);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col-reverse">
|
<div className="flex w-full flex-col-reverse">
|
||||||
|
|
@ -135,10 +82,10 @@ export default function ChatInput({
|
||||||
lockChat || saveLoading
|
lockChat || saveLoading
|
||||||
? " form-modal-lock-true bg-input"
|
? " form-modal-lock-true bg-input"
|
||||||
: noInput
|
: noInput
|
||||||
? "form-modal-no-input bg-input"
|
? "form-modal-no-input bg-input"
|
||||||
: " form-modal-lock-false bg-background",
|
: " form-modal-lock-false bg-background",
|
||||||
|
|
||||||
"form-modal-lockchat"
|
"form-modal-lockchat",
|
||||||
)}
|
)}
|
||||||
placeholder={
|
placeholder={
|
||||||
noInput ? CHAT_INPUT_PLACEHOLDER : CHAT_INPUT_PLACEHOLDER_SEND
|
noInput ? CHAT_INPUT_PLACEHOLDER : CHAT_INPUT_PLACEHOLDER_SEND
|
||||||
|
|
@ -151,31 +98,35 @@ export default function ChatInput({
|
||||||
noInput
|
noInput
|
||||||
? "bg-high-indigo text-background"
|
? "bg-high-indigo text-background"
|
||||||
: chatValue === ""
|
: chatValue === ""
|
||||||
? "text-primary"
|
? "text-primary"
|
||||||
: "bg-chat-send text-background"
|
: "bg-chat-send text-background",
|
||||||
)}
|
)}
|
||||||
disabled={lockChat || saveLoading}
|
disabled={lockChat || saveLoading}
|
||||||
onClick={(): void => send()}
|
onClick={(): void => send()}
|
||||||
>
|
>
|
||||||
{lockChat || saveLoading ? (
|
<Case condition={lockChat || saveLoading}>
|
||||||
<IconComponent
|
<IconComponent
|
||||||
name="Lock"
|
name="Lock"
|
||||||
className="form-modal-lock-icon"
|
className="form-modal-lock-icon"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
) : noInput ? (
|
</Case>
|
||||||
|
|
||||||
|
<Case condition={noInput}>
|
||||||
<IconComponent
|
<IconComponent
|
||||||
name="Zap"
|
name="Zap"
|
||||||
className="form-modal-play-icon"
|
className="form-modal-play-icon"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
) : (
|
</Case>
|
||||||
|
|
||||||
|
<Case condition={!(lockChat || saveLoading) && !noInput}>
|
||||||
<IconComponent
|
<IconComponent
|
||||||
name="LucideSend"
|
name="LucideSend"
|
||||||
className="form-modal-send-icon "
|
className="form-modal-send-icon "
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
)}
|
</Case>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -188,32 +139,10 @@ export default function ChatInput({
|
||||||
key={file.id}
|
key={file.id}
|
||||||
onDelete={() => {
|
onDelete={() => {
|
||||||
setFiles((prev) => prev.filter((f) => f.id !== file.id));
|
setFiles((prev) => prev.filter((f) => f.id !== file.id));
|
||||||
// TODO: delete file on backend
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{/*
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button variant="primary" className="h-13 px-4">
|
|
||||||
<IconComponent name="Repeat" className="" aria-hidden="true" />
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="w-fit">
|
|
||||||
<div className="flex flex-col items-center justify-center gap-2">
|
|
||||||
<span className="text-sm">Repetitions: </span>
|
|
||||||
<Input
|
|
||||||
onChange={(e) => {
|
|
||||||
handleChange(parseInt(e.target.value));
|
|
||||||
}}
|
|
||||||
className="w-16"
|
|
||||||
type="number"
|
|
||||||
min={0}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover> */}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue