✨ (chatInput): add ButtonSendWrapper component for better modularity

✨ (chatInput): add TextAreaWrapper component for better modularity
♻️ (chatInput): refactor chat input logic to use new wrapper components
♻️ (chatView): clean up imports and improve code readability
This commit is contained in:
cristhianzl 2024-05-29 10:47:05 -03:00
commit 7473eea263
3 changed files with 105 additions and 98 deletions

View file

@ -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 (
<button
className={classNames(
"form-modal-send-button",
noInput
? "bg-high-indigo text-background"
: chatValue === ""
? "text-primary"
: "bg-chat-send text-background",
)}
disabled={lockChat || saveLoading}
onClick={(): void => send()}
>
<Case condition={lockChat || saveLoading}>
<IconComponent
name="Lock"
className="form-modal-lock-icon"
aria-hidden="true"
/>
</Case>
<Case condition={noInput}>
<IconComponent
name="Zap"
className="form-modal-play-icon"
aria-hidden="true"
/>
</Case>
<Case condition={!(lockChat || saveLoading) && !noInput}>
<IconComponent
name="LucideSend"
className="form-modal-send-icon "
aria-hidden="true"
/>
</Case>
</button>
);
};
export default ButtonSendWrapper;

View file

@ -1,21 +1,19 @@
import { useState } from "react"; import { useState } from "react";
import ShortUniqueId from "short-unique-id";
import IconComponent from "../../../../../components/genericIconComponent";
import { Textarea } from "../../../../../components/ui/textarea";
import { import {
CHAT_INPUT_PLACEHOLDER, CHAT_INPUT_PLACEHOLDER,
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,
chatInputType, chatInputType,
} from "../../../../../types/components"; } from "../../../../../types/components";
import { classNames } from "../../../../../utils/utils";
import FilePreview from "../filePreviewChat"; import FilePreview from "../filePreviewChat";
import ButtonSendWrapper from "./components/buttonSendWrapper";
import TextAreaWrapper from "./components/textAreaWrapper";
import useAutoResizeTextArea from "./hooks/use-auto-resize-text-area"; 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 useFocusOnUnlock from "./hooks/use-focus-unlock";
import useUpload from "./hooks/use-upload"; import useUpload from "./hooks/use-upload";
export default function ChatInput({ export default function ChatInput({
@ -29,12 +27,18 @@ export default function ChatInput({
const [repeat, setRepeat] = useState(1); const [repeat, setRepeat] = useState(1);
const saveLoading = useFlowsManagerStore((state) => state.saveLoading); const saveLoading = useFlowsManagerStore((state) => state.saveLoading);
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId); const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
const uid = new ShortUniqueId({ length: 3 });
const [files, setFiles] = useState<FilePreviewType[]>([]); const [files, setFiles] = useState<FilePreviewType[]>([]);
const [isDragging, setIsDragging] = useState(false);
useFocusOnUnlock(lockChat, inputRef); useFocusOnUnlock(lockChat, inputRef);
useAutoResizeTextArea(chatValue, inputRef); useAutoResizeTextArea(chatValue, inputRef);
useUpload(uploadFile, currentFlowId, setFiles, uid); useUpload(uploadFile, currentFlowId, setFiles);
const { dragOver, dragEnter, dragLeave, onDrop } = useDragAndDrop(
setIsDragging,
setFiles,
currentFlowId,
);
const send = () => { const send = () => {
sendMessage({ sendMessage({
@ -44,90 +48,44 @@ export default function ChatInput({
setFiles([]); setFiles([]);
}; };
const checkSendingOk = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
return (
event.key === "Enter" &&
!lockChat &&
!saveLoading &&
!event.shiftKey &&
!event.nativeEvent.isComposing
);
};
return ( return (
<div className="flex w-full flex-col-reverse"> <div className="flex w-full flex-col-reverse">
<div className="relative w-full"> <div className="relative w-full">
<Textarea <TextAreaWrapper
onKeyDown={(event) => { dragOver={dragOver}
if ( dragEnter={dragEnter}
event.key === "Enter" && dragLeave={dragLeave}
!lockChat && onDrop={onDrop}
!saveLoading && checkSendingOk={checkSendingOk}
!event.shiftKey && send={send}
!event.nativeEvent.isComposing lockChat={lockChat}
) { noInput={noInput}
send(); saveLoading={saveLoading}
} chatValue={chatValue}
}} setChatValue={setChatValue}
rows={1} CHAT_INPUT_PLACEHOLDER={CHAT_INPUT_PLACEHOLDER}
ref={inputRef} CHAT_INPUT_PLACEHOLDER_SEND={CHAT_INPUT_PLACEHOLDER_SEND}
disabled={lockChat || noInput || saveLoading} inputRef={inputRef}
style={{
resize: "none",
bottom: `${inputRef?.current?.scrollHeight}px`,
maxHeight: "150px",
overflow: `${
inputRef.current && inputRef.current.scrollHeight > 150
? "auto"
: "hidden"
}`,
}}
value={
lockChat ? "Thinking..." : saveLoading ? "Saving..." : chatValue
}
onChange={(event): void => {
setChatValue(event.target.value);
}}
className={classNames(
lockChat || saveLoading
? " form-modal-lock-true bg-input"
: noInput
? "form-modal-no-input bg-input"
: " form-modal-lock-false bg-background",
"form-modal-lockchat",
)}
placeholder={
noInput ? CHAT_INPUT_PLACEHOLDER : CHAT_INPUT_PLACEHOLDER_SEND
}
/> />
<div className="form-modal-send-icon-position"> <div className="form-modal-send-icon-position">
<button <ButtonSendWrapper
className={classNames( send={send}
"form-modal-send-button", lockChat={lockChat}
noInput noInput={noInput}
? "bg-high-indigo text-background" saveLoading={saveLoading}
: chatValue === "" chatValue={chatValue}
? "text-primary" />
: "bg-chat-send text-background",
)}
disabled={lockChat || saveLoading}
onClick={(): void => send()}
>
<Case condition={lockChat || saveLoading}>
<IconComponent
name="Lock"
className="form-modal-lock-icon"
aria-hidden="true"
/>
</Case>
<Case condition={noInput}>
<IconComponent
name="Zap"
className="form-modal-play-icon"
aria-hidden="true"
/>
</Case>
<Case condition={!(lockChat || saveLoading) && !noInput}>
<IconComponent
name="LucideSend"
className="form-modal-send-icon "
aria-hidden="true"
/>
</Case>
</button>
</div> </div>
</div> </div>
<div className="flex w-full gap-2 pb-2"> <div className="flex w-full gap-2 pb-2">

View file

@ -9,11 +9,7 @@ import useAlertStore from "../../../../stores/alertStore";
import useFlowStore from "../../../../stores/flowStore"; import useFlowStore from "../../../../stores/flowStore";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore"; import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { VertexBuildTypeAPI, sendAllProps } from "../../../../types/api"; import { VertexBuildTypeAPI, sendAllProps } from "../../../../types/api";
import { import { ChatMessageType, ChatOutputType } from "../../../../types/chat";
ChatMessageType,
ChatOutputType,
FlowPoolObjectType,
} from "../../../../types/chat";
import { chatViewProps } from "../../../../types/components"; import { chatViewProps } from "../../../../types/components";
import { classNames } from "../../../../utils/utils"; import { classNames } from "../../../../utils/utils";
import ChatInput from "./chatInput"; import ChatInput from "./chatInput";
@ -64,11 +60,13 @@ export default function ChatView({
const chatMessages: ChatMessageType[] = chatOutputResponses const chatMessages: ChatMessageType[] = chatOutputResponses
.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp)) .sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp))
// //
.filter((output) => output.data.messages && output.data.messages.length > 0) .filter(
(output) => output.data.messages && output.data.messages.length > 0,
)
.map((output, index) => { .map((output, index) => {
try { try {
const { sender, message, sender_name, stream_url,files } = output.data const { sender, message, sender_name, stream_url, files } = output
.messages[0] as ChatOutputType; .data.messages[0] as ChatOutputType;
const is_ai = sender === "Machine" || sender === null; const is_ai = sender === "Machine" || sender === null;
return { return {
@ -77,8 +75,7 @@ export default function ChatView({
sender_name, sender_name,
componentId: output.id, componentId: output.id,
stream_url: stream_url, stream_url: stream_url,
files files,
}; };
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@ -123,7 +120,7 @@ export default function ChatView({
function updateChat( function updateChat(
chat: ChatMessageType, chat: ChatMessageType,
message: string, message: string,
stream_url?: string stream_url?: string,
) { ) {
// if (message === "") return; // if (message === "") return;
chat.message = message; chat.message = message;
@ -158,7 +155,7 @@ export default function ChatView({
"h-5 w-5", "h-5 w-5",
lockChat lockChat
? "animate-pulse text-primary" ? "animate-pulse text-primary"
: "text-primary hover:text-gray-600" : "text-primary hover:text-gray-600",
)} )}
aria-hidden="true" aria-hidden="true"
/> />