perf: add useMemo to chatHistory to avoid unecessary renders on Playground (#5376)

* fix uncessary renders on chat

* 📝 (chat-view.tsx): optimize rendering performance by memoizing ChatMessage component and using memoized version for chat history items

* ♻️ (chat-view.tsx): refactor memoized ChatMessage component to include additional props for improved performance and accuracy
This commit is contained in:
Cristhian Zanforlin Lousa 2024-12-23 22:16:36 -03:00 committed by GitHub
commit 4fec41fcce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
47 changed files with 118 additions and 2363 deletions

View file

@ -1,8 +1,6 @@
import { usePostLikeComponent } from "@/controllers/API/queries/store";
import { useState } from "react";
import { getComponent } from "../../../controllers/API";
import IOModalOld from "../../../modals/IOModal";
import IOModalNew from "../../../modals/IOModal/newModal";
import useAlertStore from "../../../stores/alertStore";
import useFlowsManagerStore from "../../../stores/flowsManagerStore";
import { useStoreStore } from "../../../stores/storeStore";

View file

@ -1,6 +1,6 @@
import ForwardedIconComponent from "@/components/common/genericIconComponent";
import ShadTooltip from "@/components/common/shadTooltipComponent";
import IOModal from "@/modals/IOModal/newModal";
import IOModal from "@/modals/IOModal/new-modal";
const PlaygroundButton = ({ hasIO, open, setOpen, canvasOpen }) => {
const PlayIcon = () => (

View file

@ -5,7 +5,7 @@ import {
SelectItem,
SelectTrigger,
SelectValue,
} from "../../../../../../components/ui/select";
} from "../../../../../components/ui/select";
export default function CsvSelect({ node, handleChangeSelect }): JSX.Element {
return (

View file

@ -1,17 +1,17 @@
import { Button } from "../../../../../../components/ui/button";
import { Button } from "../../../../../components/ui/button";
import { usePostUploadFile } from "@/controllers/API/queries/files/use-post-upload-file";
import { createFileUpload } from "@/helpers/create-file-upload";
import useFileSizeValidator from "@/shared/hooks/use-file-size-validator";
import useAlertStore from "@/stores/alertStore";
import { useEffect, useState } from "react";
import IconComponent from "../../../../../../components/common/genericIconComponent";
import IconComponent from "../../../../../components/common/genericIconComponent";
import {
ALLOWED_IMAGE_INPUT_EXTENSIONS,
BASE_URL_API,
} from "../../../../../../constants/constants";
import useFlowsManagerStore from "../../../../../../stores/flowsManagerStore";
import { IOFileInputProps } from "../../../../../../types/components";
} from "../../../../../constants/constants";
import useFlowsManagerStore from "../../../../../stores/flowsManagerStore";
import { IOFileInputProps } from "../../../../../types/components";
export default function IOFileInput({ field, updateValue }: IOFileInputProps) {
//component to handle file upload from chatIO

View file

@ -1,7 +1,7 @@
import { IOJSONInputComponentType } from "@/types/components";
import { useEffect, useRef } from "react";
import JsonView from "react18-json-view";
import { useDarkStore } from "../../../../../../stores/darkStore";
import { useDarkStore } from "../../../../../stores/darkStore";
export default function IoJsonInput({
value = [],

View file

@ -1,8 +1,8 @@
import _ from "lodash";
import { useRef } from "react";
import IconComponent from "../../../../../../components/common/genericIconComponent";
import { Input } from "../../../../../../components/ui/input";
import { classNames } from "../../../../../../utils/utils";
import IconComponent from "../../../../../components/common/genericIconComponent";
import { Input } from "../../../../../components/ui/input";
import { classNames } from "../../../../../utils/utils";
export type IOKeyPairInputProps = {
value: any;

View file

@ -1,214 +0,0 @@
import IconComponent from "@/components/common/genericIconComponent";
import ShadTooltip from "@/components/common/shadTooltipComponent";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
} from "@/components/ui/select-custom";
import { useUpdateSessionName } from "@/controllers/API/queries/messages/use-rename-session";
import useFlowStore from "@/stores/flowStore";
import { cn } from "@/utils/utils";
import React, { useEffect, useRef, useState } from "react";
export default function SessionSelector({
deleteSession,
session,
toggleVisibility,
isVisible,
inspectSession,
updateVisibleSession,
selectedView,
setSelectedView,
}: {
deleteSession: (session: string) => void;
session: string;
toggleVisibility: () => void;
isVisible: boolean;
inspectSession: (session: string) => void;
updateVisibleSession: (session: string) => void;
selectedView?: { type: string; id: string };
setSelectedView: (view: { type: string; id: string } | undefined) => void;
}) {
const currentFlowId = useFlowStore((state) => state.currentFlow?.id);
const [isEditing, setIsEditing] = useState(false);
const [editedSession, setEditedSession] = useState(session);
const { mutate: updateSessionName } = useUpdateSessionName();
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
setEditedSession(session);
}, [session]);
const handleEditClick = (e?: React.MouseEvent<HTMLDivElement>) => {
e?.stopPropagation();
setIsEditing(true);
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEditedSession(e.target.value);
};
const handleConfirm = () => {
setIsEditing(false);
if (editedSession.trim() !== session) {
updateSessionName(
{ old_session_id: session, new_session_id: editedSession.trim() },
{
onSuccess: () => {
if (isVisible) {
updateVisibleSession(editedSession);
}
if (
selectedView?.type === "Session" &&
selectedView?.id === session
) {
setSelectedView({ type: "Session", id: editedSession });
}
},
},
);
}
};
const handleCancel = () => {
setIsEditing(false);
setEditedSession(session);
};
const handleSelectChange = (value: string) => {
switch (value) {
case "rename":
handleEditClick();
break;
case "messageLogs":
inspectSession(session);
break;
case "delete":
deleteSession(session);
break;
}
};
return (
<div
data-testid="session-selector"
onClick={(e) => {
if (isEditing) e.stopPropagation();
else toggleVisibility();
}}
className={cn(
"file-component-accordion-div group cursor-pointer rounded-md hover:bg-muted-foreground/30",
isVisible ? "bg-muted-foreground/15" : "",
)}
>
<div className="flex w-full items-center justify-between gap-2 overflow-hidden border-b px-2 py-3 align-middle">
<div className="flex min-w-0 items-center gap-2">
{isEditing ? (
<div className="flex items-center">
<Input
ref={inputRef}
value={editedSession}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
e.stopPropagation();
handleConfirm();
}
}}
onChange={handleInputChange}
onBlur={(e) => {
console.log(e.relatedTarget);
if (
!e.relatedTarget ||
e.relatedTarget.getAttribute("data-confirm") !== "true"
) {
handleCancel();
}
}}
autoFocus
className="h-6 flex-grow px-1 py-0"
/>
<button
onClick={handleCancel}
className="hover:text-status-red-hover ml-2 text-status-red"
>
<IconComponent name="X" className="h-4 w-4" />
</button>
<button
onClick={handleConfirm}
data-confirm="true"
className="ml-2 text-green-500 hover:text-green-600"
>
<IconComponent name="Check" className="h-4 w-4" />
</button>
</div>
) : (
<ShadTooltip styleClasses="z-50" content={session}>
<div>
<Badge
variant="gray"
size="md"
className="block cursor-pointer truncate"
>
{session === currentFlowId ? "Default Session" : session}
</Badge>
</div>
</ShadTooltip>
)}
</div>
<Select value={""} onValueChange={handleSelectChange}>
<SelectTrigger
onClick={(e) => {
e.stopPropagation();
}}
onFocusCapture={() => {
inputRef.current?.focus();
}}
data-confirm="true"
className="h-8 w-8 border-none bg-transparent p-0 focus:ring-0"
>
<IconComponent name="MoreHorizontal" className="h-4 w-4" />
</SelectTrigger>
<SelectContent side="right" align="start" className="w-40 p-0">
<SelectItem
value="rename"
className="cursor-pointer px-3 py-2 focus:bg-muted"
>
<div className="flex items-center">
<IconComponent name="Pencil" className="mr-2 h-4 w-4" />
Rename
</div>
</SelectItem>
<SelectItem
value="messageLogs"
className="cursor-pointer px-3 py-2 focus:bg-muted"
>
<div className="flex w-full items-center justify-between">
<div className="flex items-center">
<IconComponent name="ScrollText" className="mr-2 h-4 w-4" />
Message logs
</div>
<IconComponent
name="ExternalLink"
className="absolute right-2 h-4 w-4"
/>
</div>
</SelectItem>
<SelectItem
value="delete"
className="cursor-pointer px-3 py-2 focus:bg-muted"
>
<div className="flex items-center text-status-red hover:text-status-red">
<IconComponent name="Trash2" className="mr-2 h-4 w-4" />
Delete
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
);
}

View file

@ -21,10 +21,10 @@ import {
convertValuesToNumbers,
hasDuplicateKeys,
} from "../../../../utils/reactflowUtils";
import IOFileInput from "./components/FileInput";
import IoJsonInput from "./components/JSONInput";
import CsvSelect from "./components/csvSelect";
import IOKeyPairInput from "./components/keyPairInput";
import CsvSelect from "./components/csv-selected";
import IOFileInput from "./components/file-input";
import IoJsonInput from "./components/json-input";
import IOKeyPairInput from "./components/key-pair-input";
export default function IOFieldView({
type,

View file

@ -2,9 +2,9 @@ import ShadTooltip from "@/components/common/shadTooltipComponent";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { cn } from "@/utils/utils";
import IconComponent from "../../../../components/common/genericIconComponent";
import { ChatViewWrapperProps } from "../../types/chat-view-wrapper";
import ChatView from "../chatView/newChatView";
import IconComponent from "../../../components/common/genericIconComponent";
import { ChatViewWrapperProps } from "../types/chat-view-wrapper";
import ChatView from "./chatView/chat-view";
export const ChatViewWrapper = ({
selectedViewField,
@ -19,8 +19,6 @@ export const ChatViewWrapper = ({
messagesFetched,
sessionId,
sendMessage,
chatValue,
setChatValue,
lockChat,
setLockChat,
canvasOpen,
@ -95,8 +93,6 @@ export const ChatViewWrapper = ({
<ChatView
focusChat={sessionId}
sendMessage={sendMessage}
chatValue={chatValue}
setChatValue={setChatValue}
lockChat={lockChat}
setLockChat={setLockChat}
visibleSession={visibleSession}

View file

@ -4,22 +4,31 @@ import { TextEffectPerChar } from "@/components/ui/textAnimation";
import { ENABLE_NEW_LOGO } from "@/customization/feature-flags";
import { track } from "@/customization/utils/analytics";
import { useMessagesStore } from "@/stores/messagesStore";
import { useEffect, useMemo, useRef, useState } from "react";
import { useUtilityStore } from "@/stores/utilityStore";
import { memo, useEffect, useMemo, useRef, useState } from "react";
import useTabVisibility from "../../../../shared/hooks/use-tab-visibility";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import useFlowStore from "../../../../stores/flowStore";
import { ChatMessageType } from "../../../../types/chat";
import { chatViewProps } from "../../../../types/components";
import FlowRunningSqueleton from "../flowRunningSqueleton";
import FlowRunningSqueleton from "../flow-running-squeleton";
import ChatInput from "./chatInput/chat-input";
import useDragAndDrop from "./chatInput/hooks/use-drag-and-drop";
import { useFileHandler } from "./chatInput/hooks/use-file-handler";
import ChatInput from "./chatInput/newChatInput";
import ChatMessage from "./chatMessage/newChatMessage";
import ChatMessage from "./chatMessage/chat-message";
const MemoizedChatMessage = memo(ChatMessage, (prevProps, nextProps) => {
return (
prevProps.chat.message === nextProps.chat.message &&
prevProps.chat.id === nextProps.chat.id &&
prevProps.chat.session === nextProps.chat.session &&
prevProps.chat.content_blocks === nextProps.chat.content_blocks &&
prevProps.chat.properties === nextProps.chat.properties
);
});
export default function ChatView({
sendMessage,
chatValue,
setChatValue,
lockChat,
setLockChat,
visibleSession,
@ -43,7 +52,7 @@ export default function ChatView({
const inputTypes = inputs.map((obj) => obj.type);
const updateFlowPool = useFlowStore((state) => state.updateFlowPool);
const setChatValueStore = useUtilityStore((state) => state.setChatValueStore);
const isTabHidden = useTabVisibility();
//build chat history
@ -90,9 +99,11 @@ export default function ChatView({
});
if (messages.length === 0 && !lockChat && chatInputNode) {
setChatValue(chatInputNode.data.node.template["input_value"].value ?? "");
setChatValueStore(
chatInputNode.data.node.template["input_value"].value ?? "",
);
} else {
isTabHidden ? setChatValue("") : null;
isTabHidden ? setChatValueStore("") : null;
}
setChatHistory(finalChatHistory);
@ -153,17 +164,19 @@ export default function ChatView({
<div ref={messagesRef} className="chat-message-div">
{chatHistory &&
(lockChat || chatHistory?.length > 0 ? (
chatHistory.map((chat, index) => (
<ChatMessage
setLockChat={setLockChat}
lockChat={lockChat}
chat={chat}
lastMessage={chatHistory.length - 1 === index ? true : false}
key={`${chat.id}-${index}`}
updateChat={updateChat}
closeChat={closeChat}
/>
))
<>
{chatHistory?.map((chat, index) => (
<MemoizedChatMessage
setLockChat={setLockChat}
lockChat={lockChat}
chat={chat}
lastMessage={chatHistory.length - 1 === index}
key={`${chat.id}-${index}`}
updateChat={updateChat}
closeChat={closeChat}
/>
))}
</>
) : (
<div className="flex h-full w-full flex-col items-center justify-center">
<div className="flex flex-col items-center justify-center gap-4 p-8">
@ -209,16 +222,12 @@ export default function ChatView({
</div>
<div className="m-auto w-full max-w-[768px] md:w-5/6">
<ChatInput
chatValue={chatValue}
noInput={!inputTypes.includes("ChatInput")}
lockChat={lockChat}
sendMessage={({ repeat, files }) => {
sendMessage({ repeat, files });
track("Playground Message Sent");
}}
setChatValue={(value) => {
setChatValue(value);
}}
inputRef={ref}
files={files}
setFiles={setFiles}

View file

@ -4,6 +4,7 @@ import { usePostUploadFile } from "@/controllers/API/queries/files/use-post-uplo
import useFileSizeValidator from "@/shared/hooks/use-file-size-validator";
import useAlertStore from "@/stores/alertStore";
import useFlowStore from "@/stores/flowStore";
import { useUtilityStore } from "@/stores/utilityStore";
import { useEffect, useRef, useState } from "react";
import ShortUniqueId from "short-unique-id";
import {
@ -18,17 +19,15 @@ import {
ChatInputType,
FilePreviewType,
} from "../../../../../types/components";
import FilePreview from "../filePreviewChat/newFilePreview";
import ButtonSendWrapper from "./components/buttonSendWrapper/newButtonSendWrapper";
import TextAreaWrapper from "./components/textAreaWrapper/newTextAreaWrapper";
import UploadFileButton from "./components/uploadFileButton/newUploadFileButton";
import FilePreview from "../fileComponent/components/file-preview";
import ButtonSendWrapper from "./components/button-send-wrapper";
import TextAreaWrapper from "./components/text-area-wrapper";
import UploadFileButton from "./components/upload-file-button";
import useAutoResizeTextArea from "./hooks/use-auto-resize-text-area";
import useFocusOnUnlock from "./hooks/use-focus-unlock";
export default function ChatInput({
lockChat,
chatValue,
sendMessage,
setChatValue,
inputRef,
noInput,
files,
@ -36,12 +35,13 @@ export default function ChatInput({
isDragging,
}: ChatInputType): JSX.Element {
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
const [inputFocus, setInputFocus] = useState<boolean>(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const setErrorData = useAlertStore((state) => state.setErrorData);
const { validateFileSize } = useFileSizeValidator(setErrorData);
const stopBuilding = useFlowStore((state) => state.stopBuilding);
const chatValue = useUtilityStore((state) => state.chatValueStore);
useFocusOnUnlock(lockChat, inputRef);
useAutoResizeTextArea(chatValue, inputRef);
@ -219,11 +219,9 @@ export default function ChatInput({
lockChat={lockChat}
noInput={noInput}
chatValue={chatValue}
setChatValue={setChatValue}
CHAT_INPUT_PLACEHOLDER={CHAT_INPUT_PLACEHOLDER}
CHAT_INPUT_PLACEHOLDER_SEND={CHAT_INPUT_PLACEHOLDER_SEND}
inputRef={inputRef}
setInputFocus={setInputFocus}
files={files}
isDragging={isDragging}
/>

View file

@ -1,9 +1,9 @@
import Loading from "@/components/ui/loading";
import useFlowStore from "@/stores/flowStore";
import { Button } from "../../../../../../../components/ui/button";
import { Case } from "../../../../../../../shared/components/caseComponent";
import { FilePreviewType } from "../../../../../../../types/components";
import { classNames } from "../../../../../../../utils/utils";
import { Button } from "../../../../../../components/ui/button";
import { Case } from "../../../../../../shared/components/caseComponent";
import { FilePreviewType } from "../../../../../../types/components";
import { classNames } from "../../../../../../utils/utils";
const BUTTON_STATES = {
NO_INPUT: "bg-high-indigo text-background",

View file

@ -1,90 +0,0 @@
import useFlowStore from "@/stores/flowStore";
import IconComponent from "../../../../../../../components/common/genericIconComponent";
import { Button } from "../../../../../../../components/ui/button";
import { Case } from "../../../../../../../shared/components/caseComponent";
import { FilePreviewType } from "../../../../../../../types/components";
import { classNames } from "../../../../../../../utils/utils";
const BUTTON_STATES = {
NO_INPUT: "bg-high-indigo text-background",
HAS_CHAT_VALUE: "text-primary",
SHOW_STOP: "bg-error text-background cursor-pointer",
DEFAULT: "bg-chat-send text-background",
};
type ButtonSendWrapperProps = {
send: () => void;
lockChat: boolean;
noInput: boolean;
chatValue: string;
files: FilePreviewType[];
};
const ButtonSendWrapper = ({
send,
lockChat,
noInput,
chatValue,
files,
}: ButtonSendWrapperProps) => {
const stopBuilding = useFlowStore((state) => state.stopBuilding);
const isBuilding = useFlowStore((state) => state.isBuilding);
const showStopButton = lockChat || files.some((file) => file.loading);
const showPlayButton = !lockChat && noInput;
const showSendButton =
!(lockChat || files.some((file) => file.loading)) && !noInput;
const getButtonState = () => {
if (showStopButton) return BUTTON_STATES.SHOW_STOP;
if (noInput) return BUTTON_STATES.NO_INPUT;
if (chatValue) return BUTTON_STATES.HAS_CHAT_VALUE;
return BUTTON_STATES.DEFAULT;
};
const buttonClasses = classNames("form-modal-send-button", getButtonState());
const handleClick = () => {
if (showStopButton && isBuilding) {
stopBuilding();
} else if (!showStopButton) {
send();
}
};
return (
<Button
className={buttonClasses}
disabled={lockChat && !isBuilding}
onClick={handleClick}
unstyled
>
<Case condition={showStopButton}>
<IconComponent
name="Square"
className="form-modal-lock-icon"
aria-hidden="true"
/>
</Case>
<Case condition={showPlayButton}>
<IconComponent
name="Zap"
className="form-modal-play-icon"
aria-hidden="true"
/>
</Case>
<Case condition={showSendButton}>
<IconComponent
name="LucideSend"
className="form-modal-send-icon"
aria-hidden="true"
/>
</Case>
</Button>
);
};
export default ButtonSendWrapper;

View file

@ -1,6 +1,7 @@
import { useUtilityStore } from "@/stores/utilityStore";
import { useEffect } from "react";
import { Textarea } from "../../../../../../../components/ui/textarea";
import { classNames } from "../../../../../../../utils/utils";
import { Textarea } from "../../../../../../components/ui/textarea";
import { classNames } from "../../../../../../utils/utils";
const TextAreaWrapper = ({
checkSendingOk,
@ -8,11 +9,9 @@ const TextAreaWrapper = ({
lockChat,
noInput,
chatValue,
setChatValue,
CHAT_INPUT_PLACEHOLDER,
CHAT_INPUT_PLACEHOLDER_SEND,
inputRef,
setInputFocus,
files,
isDragging,
}) => {
@ -31,6 +30,8 @@ const TextAreaWrapper = ({
const fileClass = files.length > 0 ? "!rounded-t-none border-t-0" : "";
const setChatValueStore = useUtilityStore((state) => state.setChatValueStore);
const additionalClassNames =
"form-input block w-full border-0 custom-scroll focus:border-ring rounded-none shadow-none focus:ring-0 p-0 sm:text-sm !bg-transparent";
@ -43,10 +44,6 @@ const TextAreaWrapper = ({
return (
<Textarea
data-testid="input-chat-playground"
onFocus={(e) => {
setInputFocus(true);
}}
onBlur={() => setInputFocus(false)}
onKeyDown={(event) => {
if (checkSendingOk(event)) {
send();
@ -67,7 +64,7 @@ const TextAreaWrapper = ({
}}
value={chatValue}
onChange={(event): void => {
setChatValue(event.target.value);
setChatValueStore(event.target.value);
}}
className={classNames(fileClass, additionalClassNames)}
placeholder={getPlaceholderText(isDragging, noInput)}

View file

@ -1,83 +0,0 @@
import { useEffect } from "react";
import { Textarea } from "../../../../../../../components/ui/textarea";
import { classNames } from "../../../../../../../utils/utils";
const TextAreaWrapper = ({
checkSendingOk,
send,
lockChat,
noInput,
chatValue,
setChatValue,
CHAT_INPUT_PLACEHOLDER,
CHAT_INPUT_PLACEHOLDER_SEND,
inputRef,
setInputFocus,
files,
isDragging,
}) => {
const getPlaceholderText = (
isDragging: boolean,
noInput: boolean,
): string => {
if (isDragging) {
return "Drop here";
} else if (noInput) {
return CHAT_INPUT_PLACEHOLDER;
} else {
return CHAT_INPUT_PLACEHOLDER_SEND;
}
};
const lockClass = lockChat
? "form-modal-lock-true bg-input"
: noInput
? "form-modal-no-input bg-input"
: "form-modal-lock-false bg-background";
const fileClass = files.length > 0 ? "!rounded-t-none border-t-0" : "";
const additionalClassNames = "form-modal-lockchat pl-14";
useEffect(() => {
if (!lockChat && !noInput) {
inputRef.current?.focus();
}
}, [lockChat, noInput]);
return (
<Textarea
data-testid="input-chat-playground"
onFocus={(e) => {
setInputFocus(true);
}}
onBlur={() => setInputFocus(false)}
onKeyDown={(event) => {
if (checkSendingOk(event)) {
send();
}
}}
rows={1}
ref={inputRef}
disabled={lockChat || noInput}
style={{
resize: "none",
bottom: `${inputRef?.current?.scrollHeight}px`,
maxHeight: "150px",
overflow: `${
inputRef.current && inputRef.current.scrollHeight > 150
? "auto"
: "hidden"
}`,
}}
value={lockChat ? "Thinking..." : chatValue}
onChange={(event): void => {
setChatValue(event.target.value);
}}
className={classNames(lockClass, fileClass, additionalClassNames)}
placeholder={getPlaceholderText(isDragging, noInput)}
/>
);
};
export default TextAreaWrapper;

View file

@ -1,6 +1,6 @@
import ShadTooltip from "@/components/common/shadTooltipComponent";
import ForwardedIconComponent from "../../../../../../../components/common/genericIconComponent";
import { Button } from "../../../../../../../components/ui/button";
import ForwardedIconComponent from "../../../../../../components/common/genericIconComponent";
import { Button } from "../../../../../../components/ui/button";
const UploadFileButton = ({
fileInputRef,

View file

@ -1,33 +0,0 @@
import ForwardedIconComponent from "../../../../../../../components/common/genericIconComponent";
import { Button } from "../../../../../../../components/ui/button";
const UploadFileButton = ({
fileInputRef,
handleFileChange,
handleButtonClick,
lockChat,
}) => {
return (
<div>
<input
disabled={lockChat}
type="file"
ref={fileInputRef}
style={{ display: "none" }}
onChange={handleFileChange}
/>
<Button
disabled={lockChat}
className={`font-bold transition-all dark:text-white ${
lockChat ? "cursor-not-allowed" : "hover:text-muted-foreground"
}`}
onClick={handleButtonClick}
unstyled
>
<ForwardedIconComponent name="PaperclipIcon" />
</Button>
</div>
);
};
export default UploadFileButton;

View file

@ -1,219 +0,0 @@
import { usePostUploadFile } from "@/controllers/API/queries/files/use-post-upload-file";
import useFileSizeValidator from "@/shared/hooks/use-file-size-validator";
import useAlertStore from "@/stores/alertStore";
import { useEffect, useRef, useState } from "react";
import ShortUniqueId from "short-unique-id";
import {
ALLOWED_IMAGE_INPUT_EXTENSIONS,
CHAT_INPUT_PLACEHOLDER,
CHAT_INPUT_PLACEHOLDER_SEND,
FS_ERROR_TEXT,
SN_ERROR_TEXT,
} from "../../../../../constants/constants";
import useFlowsManagerStore from "../../../../../stores/flowsManagerStore";
import {
ChatInputType,
FilePreviewType,
} from "../../../../../types/components";
import FilePreview from "../filePreviewChat";
import ButtonSendWrapper from "./components/buttonSendWrapper";
import TextAreaWrapper from "./components/textAreaWrapper";
import UploadFileButton from "./components/uploadFileButton";
import { getClassNamesFilePreview } from "./helpers/get-class-file-preview";
import useAutoResizeTextArea from "./hooks/use-auto-resize-text-area";
import useFocusOnUnlock from "./hooks/use-focus-unlock";
export default function ChatInput({
lockChat,
chatValue,
sendMessage,
setChatValue,
inputRef,
noInput,
files,
setFiles,
isDragging,
}: ChatInputType): JSX.Element {
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
const [inputFocus, setInputFocus] = useState<boolean>(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const setErrorData = useAlertStore((state) => state.setErrorData);
const { validateFileSize } = useFileSizeValidator(setErrorData);
useFocusOnUnlock(lockChat, inputRef);
useAutoResizeTextArea(chatValue, inputRef);
const { mutate } = usePostUploadFile();
const handleFileChange = async (
event: React.ChangeEvent<HTMLInputElement> | ClipboardEvent,
) => {
let file: File | null = null;
if ("clipboardData" in event) {
const items = event.clipboardData?.items;
if (items) {
for (let i = 0; i < items.length; i++) {
const blob = items[i].getAsFile();
if (blob) {
file = blob;
break;
}
}
}
} else {
const fileInput = event.target as HTMLInputElement;
file = fileInput.files?.[0] ?? null;
}
if (file) {
const fileExtension = file.name.split(".").pop()?.toLowerCase();
if (!validateFileSize(file)) {
return;
}
if (
!fileExtension ||
!ALLOWED_IMAGE_INPUT_EXTENSIONS.includes(fileExtension)
) {
setErrorData({
title: "Error uploading file",
list: [FS_ERROR_TEXT, SN_ERROR_TEXT],
});
return;
}
const uid = new ShortUniqueId();
const id = uid.randomUUID(10);
const type = file.type.split("/")[0];
setFiles((prevFiles) => [
...prevFiles,
{ file, loading: true, error: false, id, type },
]);
mutate(
{ file, id: currentFlowId },
{
onSuccess: (data) => {
setFiles((prev) => {
const newFiles = [...prev];
const updatedIndex = newFiles.findIndex((file) => file.id === id);
newFiles[updatedIndex].loading = false;
newFiles[updatedIndex].path = data.file_path;
return newFiles;
});
},
onError: (error) => {
setFiles((prev) => {
const newFiles = [...prev];
const updatedIndex = newFiles.findIndex((file) => file.id === id);
newFiles[updatedIndex].loading = false;
newFiles[updatedIndex].error = true;
return newFiles;
});
setErrorData({
title: "Error uploading file",
list: [error.response?.data?.detail],
});
},
},
);
}
if ("target" in event && event.target instanceof HTMLInputElement) {
event.target.value = "";
}
};
useEffect(() => {
document.addEventListener("paste", handleFileChange);
return () => {
document.removeEventListener("paste", handleFileChange);
};
}, [handleFileChange, currentFlowId, lockChat]);
const send = () => {
sendMessage({
repeat: 1,
files: files.map((file) => file.path ?? "").filter((file) => file !== ""),
});
setFiles([]);
};
const checkSendingOk = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
return (
event.key === "Enter" &&
!lockChat &&
!event.shiftKey &&
!event.nativeEvent.isComposing
);
};
const classNameFilePreview = getClassNamesFilePreview(inputFocus);
const handleButtonClick = () => {
fileInputRef.current!.click();
};
return (
<div className="flex w-full flex-col-reverse">
<div className="w-full">
<TextAreaWrapper
checkSendingOk={checkSendingOk}
send={send}
lockChat={lockChat}
noInput={noInput}
chatValue={chatValue}
setChatValue={setChatValue}
CHAT_INPUT_PLACEHOLDER={CHAT_INPUT_PLACEHOLDER}
CHAT_INPUT_PLACEHOLDER_SEND={CHAT_INPUT_PLACEHOLDER_SEND}
inputRef={inputRef}
setInputFocus={setInputFocus}
files={files}
isDragging={isDragging}
/>
<div className="form-modal-send-icon-position">
<ButtonSendWrapper
send={send}
lockChat={lockChat}
noInput={noInput}
chatValue={chatValue}
files={files}
/>
</div>
<div
className={`absolute bottom-2 left-4 ${
lockChat ? "cursor-not-allowed" : ""
}`}
>
<UploadFileButton
lockChat={lockChat}
fileInputRef={fileInputRef}
handleFileChange={handleFileChange}
handleButtonClick={handleButtonClick}
/>
</div>
</div>
{files.length > 0 && (
<div className={classNameFilePreview}>
{files.map((file) => (
<FilePreview
error={file.error}
file={file.file}
loading={file.loading}
key={file.id}
onDelete={() => {
setFiles((prev: FilePreviewType[]) =>
prev.filter((f) => f.id !== file.id),
);
// TODO: delete file on backend
}}
/>
))}
</div>
)}
</div>
);
}

View file

@ -13,20 +13,16 @@ import IconComponent, {
ForwardedIconComponent,
} from "../../../../../components/common/genericIconComponent";
import SanitizedHTMLWrapper from "../../../../../components/common/sanitizedHTMLWrapper";
import CodeTabsComponent from "../../../../../components/core/codeTabsComponent/ChatCodeTabComponent";
import {
EMPTY_INPUT_SEND_MESSAGE,
EMPTY_OUTPUT_SEND_MESSAGE,
} from "../../../../../constants/constants";
import { EMPTY_INPUT_SEND_MESSAGE } from "../../../../../constants/constants";
import useTabVisibility from "../../../../../shared/hooks/use-tab-visibility";
import useAlertStore from "../../../../../stores/alertStore";
import { chatMessagePropsType } from "../../../../../types/components";
import { cn } from "../../../../../utils/utils";
import { ErrorView } from "./components/contentView";
import { MarkdownField } from "./components/editMessage";
import { EditMessageButton } from "./components/editMessageButton/newMessageOptions";
import EditMessageField from "./components/editMessageField/newEditMessageField";
import FileCardWrapper from "./components/fileCardWrapper";
import { ErrorView } from "./components/content-view";
import { MarkdownField } from "./components/edit-message";
import EditMessageField from "./components/edit-message-field";
import FileCardWrapper from "./components/file-card-wrapper";
import { EditMessageButton } from "./components/message-options";
import { convertFiles } from "./helpers/convert-files";
export default function ChatMessage({
@ -39,8 +35,6 @@ export default function ChatMessage({
}: chatMessagePropsType): JSX.Element {
const convert = new Convert({ newline: true });
const [hidden, setHidden] = useState(true);
const template = chat.template;
const [promptOpen, setPromptOpen] = useState(false);
const [streamUrl, setStreamUrl] = useState(chat.stream_url);
const flow_id = useFlowsManagerStore((state) => state.currentFlowId);
const fitViewNode = useFlowStore((state) => state.fitViewNode);
@ -160,7 +154,7 @@ export default function ChatMessage({
try {
decodedMessage = decodeURIComponent(chatMessage);
} catch (e) {
console.error(e);
// console.error(e);
}
const isEmpty = decodedMessage?.trim() === "";
const { mutate: updateMessageMutation } = useUpdateMessage();

View file

@ -4,8 +4,8 @@ import { cn } from "@/utils/utils";
import { AnimatePresence, motion } from "framer-motion";
import Markdown from "react-markdown";
import remarkGfm from "remark-gfm";
import CodeTabsComponent from "../../../../../../../components/core/codeTabsComponent/ChatCodeTabComponent";
import LogoIcon from "../chatLogoIcon";
import CodeTabsComponent from "../../../../../../components/core/codeTabsComponent/ChatCodeTabComponent";
import LogoIcon from "./chat-logo-icon";
export const ErrorView = ({
closeChat,

View file

@ -4,8 +4,7 @@ import { EMPTY_OUTPUT_SEND_MESSAGE } from "@/constants/constants";
import Markdown from "react-markdown";
import rehypeMathjax from "rehype-mathjax";
import remarkGfm from "remark-gfm";
import CodeTabsComponent from "../../../../../../../components/core/codeTabsComponent/ChatCodeTabComponent";
import EditMessageField from "../editMessageField";
import CodeTabsComponent from "../../../../../../components/core/codeTabsComponent/ChatCodeTabComponent";
type MarkdownFieldProps = {
chat: any;

View file

@ -1,13 +0,0 @@
import IconComponent from "@/components/common/genericIconComponent";
import { Button } from "@/components/ui/button";
import { ButtonHTMLAttributes } from "react";
export function EditMessageButton(
props: ButtonHTMLAttributes<HTMLButtonElement>,
) {
return (
<Button variant="ghost" size="icon" {...props}>
<IconComponent name="pencil" className="h-4 w-4" />
</Button>
);
}

View file

@ -1,75 +0,0 @@
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { useEffect, useRef, useState } from "react";
export default function EditMessageField({
message: initialMessage,
onEdit,
onCancel,
}: {
message: string;
onEdit: (message: string) => void;
onCancel: () => void;
}) {
const [message, setMessage] = useState(initialMessage);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const [isButtonClicked, setIsButtonClicked] = useState(false);
const adjustTextareaHeight = () => {
if (textareaRef.current) {
textareaRef.current.style.height = "auto";
textareaRef.current.style.height = `${textareaRef.current.scrollHeight + 3}px`;
}
};
useEffect(() => {
adjustTextareaHeight();
}, []);
return (
<div className="flex h-fit w-full flex-col">
<Textarea
ref={textareaRef}
className="h-mx-full"
onBlur={() => {
if (!isButtonClicked) {
onCancel();
}
}}
value={message}
autoFocus={true}
onChange={(e) => setMessage(e.target.value)}
/>
<div className="flex w-full flex-row-reverse justify-between">
<div className="flex flex-row-reverse gap-2">
<Button
data-testid="save-button"
onMouseDown={() => setIsButtonClicked(true)}
onClick={() => {
onEdit(message);
setIsButtonClicked(false);
}}
className="btn btn-primary mt-2"
>
Save
</Button>
<Button
data-testid="cancel-button"
onMouseDown={() => setIsButtonClicked(true)}
onClick={() => {
onCancel();
setIsButtonClicked(false);
}}
className="btn btn-secondary mt-2"
>
Cancel
</Button>
</div>
<div>
<span className="mr-4 text-sm text-muted-foreground">
Editing messages will update the memory but won't restart the
conversation.
</span>
</div>
</div>
</div>
);
}

View file

@ -1,7 +1,7 @@
import { useState } from "react";
import ForwardedIconComponent from "../../../../../../../components/common/genericIconComponent";
import FileCard from "../../../fileComponent";
import formatFileName from "../../../filePreviewChat/utils/format-file-name";
import ForwardedIconComponent from "../../../../../../components/common/genericIconComponent";
import FileCard from "../../fileComponent/components/file-card";
import formatFileName from "../../fileComponent/utils/format-file-name";
export default function FileCardWrapper({
index,

View file

@ -7,11 +7,9 @@ import { ButtonHTMLAttributes, useState } from "react";
export function EditMessageButton({
onEdit,
onCopy,
onDelete,
onEvaluate,
isBotMessage,
evaluation,
...props
}: ButtonHTMLAttributes<HTMLButtonElement> & {
onEdit: () => void;
onCopy: () => void;

View file

@ -1,495 +0,0 @@
import ShadTooltip from "@/components/common/shadTooltipComponent";
import { useUpdateMessage } from "@/controllers/API/queries/messages";
import useFlowsManagerStore from "@/stores/flowsManagerStore";
import { useUtilityStore } from "@/stores/utilityStore";
import Convert from "ansi-to-html";
import { useEffect, useRef, useState } from "react";
import Markdown from "react-markdown";
import rehypeMathjax from "rehype-mathjax";
import remarkGfm from "remark-gfm";
import MaleTechnology from "../../../../../assets/male-technologist.png";
import Robot from "../../../../../assets/robot.png";
import IconComponent from "../../../../../components/common/genericIconComponent";
import SanitizedHTMLWrapper from "../../../../../components/common/sanitizedHTMLWrapper";
import CodeTabsComponent from "../../../../../components/core/codeTabsComponent";
import {
EMPTY_INPUT_SEND_MESSAGE,
EMPTY_OUTPUT_SEND_MESSAGE,
} from "../../../../../constants/constants";
import useAlertStore from "../../../../../stores/alertStore";
import { chatMessagePropsType } from "../../../../../types/components";
import { cn } from "../../../../../utils/utils";
import { EditMessageButton } from "./components/editMessageButton";
import EditMessageField from "./components/editMessageField";
import FileCardWrapper from "./components/fileCardWrapper";
export default function ChatMessage({
chat,
lockChat,
lastMessage,
updateChat,
setLockChat,
}: chatMessagePropsType): JSX.Element {
const convert = new Convert({ newline: true });
const [hidden, setHidden] = useState(true);
const template = chat.template;
const [promptOpen, setPromptOpen] = useState(false);
const [streamUrl, setStreamUrl] = useState(chat.stream_url);
const flow_id = useFlowsManagerStore((state) => state.currentFlowId);
// We need to check if message is not undefined because
// we need to run .toString() on it
const [chatMessage, setChatMessage] = useState(
chat.message ? chat.message.toString() : "",
);
const [isStreaming, setIsStreaming] = useState(false);
const eventSource = useRef<EventSource | undefined>(undefined);
const setErrorData = useAlertStore((state) => state.setErrorData);
const chatMessageRef = useRef(chatMessage);
const [editMessage, setEditMessage] = useState(false);
useEffect(() => {
const chatMessageString = chat.message ? chat.message.toString() : "";
setChatMessage(chatMessageString);
}, [chat]);
const playgroundScrollBehaves = useUtilityStore(
(state) => state.playgroundScrollBehaves,
);
const setPlaygroundScrollBehaves = useUtilityStore(
(state) => state.setPlaygroundScrollBehaves,
);
// Sync ref with state
useEffect(() => {
chatMessageRef.current = chatMessage;
}, [chatMessage]);
// The idea now is that chat.stream_url MAY be a URL if we should stream the output of the chat
// probably the message is empty when we have a stream_url
// what we need is to update the chat_message with the SSE data
const streamChunks = (url: string) => {
setIsStreaming(true); // Streaming starts
return new Promise<boolean>((resolve, reject) => {
eventSource.current = new EventSource(url);
eventSource.current.onmessage = (event) => {
let parsedData = JSON.parse(event.data);
if (parsedData.chunk) {
setChatMessage((prev) => prev + parsedData.chunk);
}
};
eventSource.current.onerror = (event: any) => {
setIsStreaming(false);
eventSource.current?.close();
setStreamUrl(undefined);
if (JSON.parse(event.data)?.error) {
setErrorData({
title: "Error on Streaming",
list: [JSON.parse(event.data)?.error],
});
}
updateChat(chat, chatMessageRef.current);
reject(new Error("Streaming failed"));
};
eventSource.current.addEventListener("close", (event) => {
setStreamUrl(undefined); // Update state to reflect the stream is closed
eventSource.current?.close();
setIsStreaming(false);
resolve(true);
});
});
};
useEffect(() => {
if (streamUrl && !isStreaming) {
setLockChat(true);
streamChunks(streamUrl)
.then(() => {
setLockChat(false);
if (updateChat) {
updateChat(chat, chatMessageRef.current);
}
})
.catch((error) => {
console.error(error);
setLockChat(false);
});
}
}, [streamUrl, chatMessage]);
useEffect(() => {
return () => {
eventSource.current?.close();
};
}, []);
useEffect(() => {
const element = document.getElementById("last-chat-message");
if (element) {
if (playgroundScrollBehaves === "instant") {
element.scrollIntoView({ behavior: playgroundScrollBehaves });
setPlaygroundScrollBehaves("smooth");
} else {
setTimeout(() => {
element.scrollIntoView({ behavior: playgroundScrollBehaves });
}, 200);
}
}
}, [lastMessage, chat]);
let decodedMessage = chatMessage ?? "";
try {
decodedMessage = decodeURIComponent(chatMessage);
} catch (e) {
console.error(e);
}
const isEmpty = decodedMessage?.trim() === "";
const { mutate: updateMessageMutation } = useUpdateMessage();
const convertFiles = (
files:
| (
| string
| {
path: string;
type: string;
name: string;
}
)[]
| undefined,
) => {
if (!files) return [];
return files.map((file) => {
if (typeof file === "string") {
return file;
}
return file.path;
});
};
const handleEditMessage = (message: string) => {
updateMessageMutation(
{
message: {
...chat,
files: convertFiles(chat.files),
sender_name: chat.sender_name ?? "AI",
text: message,
sender: chat.isSend ? "User" : "Machine",
flow_id,
session_id: chat.session ?? "",
},
refetch: true,
},
{
onSuccess: () => {
updateChat(chat, message);
setEditMessage(false);
},
onError: () => {
setErrorData({
title: "Error updating messages.",
});
},
},
);
};
const editedFlag = chat.edit ? (
<span className="text-sm text-chat-trigger-disabled">(Edited)</span>
) : null;
return (
<>
<div
className={cn(
"form-modal-chat-position group hover:bg-background",
chat.isSend ? "" : " ",
)}
style={{
backgroundColor: chat.background_color || "#FF00FF", // Loud magenta as default
color: chat.text_color || "#00FFFF", // Loud cyan as default
}}
>
<div
className={
"mr-3 mt-1 flex w-24 flex-col items-center gap-1 overflow-hidden px-3 pb-3"
}
>
<div className="flex flex-col items-center gap-1">
<div
className={cn(
"relative flex h-8 w-8 items-center justify-center overflow-hidden rounded-md p-5 text-2xl",
!chat.isSend ? "bg-chat-bot-icon" : "bg-chat-user-icon",
)}
>
{chat.icon ? (
<img
src={chat.icon}
className="absolute scale-[60%]"
alt="icon"
/>
) : (
<img
src={!chat.isSend ? Robot : MaleTechnology}
className="absolute scale-[60%]"
alt={!chat.isSend ? "robot_image" : "male_technology"}
/>
)}
</div>
<span
className="max-w-24 truncate text-xs"
data-testid={
"sender_name_" + chat.sender_name?.toLocaleLowerCase()
}
>
{chat.sender_name}
</span>
</div>
</div>
{!chat.isSend ? (
<div className="form-modal-chat-text-position min-w-96 flex-grow">
<div className="form-modal-chat-text">
{hidden && chat.thought && chat.thought !== "" && (
<div
onClick={(): void => setHidden((prev) => !prev)}
className="form-modal-chat-icon-div"
>
<IconComponent
name="MessageSquare"
className="form-modal-chat-icon"
/>
</div>
)}
{chat.thought && chat.thought !== "" && !hidden && (
<SanitizedHTMLWrapper
className="form-modal-chat-thought"
content={convert.toHtml(chat.thought)}
onClick={() => setHidden((prev) => !prev)}
/>
)}
{chat.thought && chat.thought !== "" && !hidden && <br></br>}
<div className="flex w-full flex-col">
<div
className="flex w-full flex-col dark:text-white"
data-testid="div-chat-message"
>
<div
data-testid={
"chat-message-" + chat.sender_name + "-" + chatMessage
}
className="flex w-full flex-col"
>
{chatMessage === "" && lockChat ? (
<IconComponent
name="MoreHorizontal"
className="h-8 w-8 animate-pulse"
/>
) : (
<div onDoubleClick={() => setEditMessage(true)}>
{editMessage ? (
<EditMessageField
key={`edit-message-${chat.id}`}
message={decodedMessage}
onEdit={(message) => {
handleEditMessage(message);
}}
onCancel={() => setEditMessage(false)}
/>
) : (
<>
<div className="flex gap-2">
<Markdown
remarkPlugins={[remarkGfm]}
linkTarget="_blank"
rehypePlugins={[rehypeMathjax]}
className={cn(
"markdown prose flex flex-col word-break-break-word dark:prose-invert",
isEmpty
? "text-chat-trigger-disabled"
: "text-primary",
)}
components={{
pre({ node, ...props }) {
return <>{props.children}</>;
},
code: ({
node,
inline,
className,
children,
...props
}) => {
let content = children as string;
if (
Array.isArray(children) &&
children.length === 1 &&
typeof children[0] === "string"
) {
content = children[0] as string;
}
if (typeof content === "string") {
if (content.length) {
if (content[0] === "▍") {
return (
<span className="form-modal-markdown-span">
</span>
);
}
}
const match = /language-(\w+)/.exec(
className || "",
);
return !inline ? (
<CodeTabsComponent
isMessage
tabs={[
{
name: (match && match[1]) || "",
mode: (match && match[1]) || "",
image:
"https://curl.se/logo/curl-symbol-transparent.png",
language:
(match && match[1]) || "",
code: String(content).replace(
/\n$/,
"",
),
},
]}
activeTab={"0"}
setActiveTab={() => {}}
/>
) : (
<code className={className} {...props}>
{content}
</code>
);
}
},
}}
>
{isEmpty && !chat.stream_url
? EMPTY_OUTPUT_SEND_MESSAGE
: chatMessage}
</Markdown>
</div>
{editedFlag}
</>
)}
</div>
)}
</div>
</div>
</div>
</div>
</div>
) : (
<div className="form-modal-chat-text-position min-w-96 flex-grow">
{template ? (
<>
<button
className="form-modal-initial-prompt-btn"
onClick={() => {
setPromptOpen((old) => !old);
}}
>
Display Prompt
<IconComponent
name="ChevronDown"
className={`h-3 w-3 transition-all ${promptOpen ? "rotate-180" : ""}`}
/>
</button>
<span
className={cn(
"prose word-break-break-word dark:prose-invert",
!isEmpty ? "text-primary" : "text-chat-trigger-disabled",
)}
>
{promptOpen
? template?.split("\n")?.map((line, index) => {
const regex = /{([^}]+)}/g;
let match;
let parts: Array<JSX.Element | string> = [];
let lastIndex = 0;
while ((match = regex.exec(line)) !== null) {
// Push text up to the match
if (match.index !== lastIndex) {
parts.push(line.substring(lastIndex, match.index));
}
// Push div with matched text
if (chat.message[match[1]]) {
parts.push(
<span className="chat-message-highlight">
{chat.message[match[1]]}
</span>,
);
}
// Update last index
lastIndex = regex.lastIndex;
}
// Push text after the last match
if (lastIndex !== line.length) {
parts.push(line.substring(lastIndex));
}
return <p>{parts}</p>;
})
: isEmpty
? EMPTY_INPUT_SEND_MESSAGE
: chatMessage}
</span>
</>
) : (
<div className="flex flex-col">
{editMessage ? (
<EditMessageField
key={`edit-message-${chat.id}`}
message={decodedMessage}
onEdit={(message) => {
handleEditMessage(message);
}}
onCancel={() => setEditMessage(false)}
/>
) : (
<>
<div
onDoubleClick={() => {
setEditMessage(true);
}}
className={`flex gap-2 whitespace-pre-wrap break-words ${
isEmpty ? "text-chat-trigger-disabled" : "text-primary"
}`}
data-testid={`chat-message-${chat.sender_name}-${chatMessage}`}
>
{isEmpty ? EMPTY_INPUT_SEND_MESSAGE : decodedMessage}
</div>
{editedFlag}
</>
)}
{chat.files && (
<div className="my-2 flex flex-col gap-5">
{chat.files.map((file, index) => {
return <FileCardWrapper index={index} path={file} />;
})}
</div>
)}
</div>
)}
</div>
)}
{!editMessage && (
<ShadTooltip content="Edit Message" styleClasses="z-50">
<div>
<EditMessageButton
className="invisible h-fit group-hover:visible"
onClick={() => setEditMessage(true)}
/>
</div>
</ShadTooltip>
)}
</div>
<div id={lastMessage ? "last-chat-message" : undefined} />
</>
);
}

View file

@ -1,5 +1,5 @@
import ForwardedIconComponent from "../../../../../../../components/common/genericIconComponent";
import { Button } from "../../../../../../../components/ui/button";
import ForwardedIconComponent from "../../../../../../components/common/genericIconComponent";
import { Button } from "../../../../../../components/ui/button";
export default function DownloadButton({
isHovered,

View file

@ -1,11 +1,11 @@
import { useGetDownloadFileMutation } from "@/controllers/API/queries/files";
import { useState } from "react";
import { ForwardedIconComponent } from "../../../../../components/common/genericIconComponent";
import { BASE_URL_API } from "../../../../../constants/constants";
import { fileCardPropsType } from "../../../../../types/components";
import formatFileName from "../filePreviewChat/utils/format-file-name";
import DownloadButton from "./components/downloadButton/downloadButton";
import getClasses from "./utils/get-classes";
import { ForwardedIconComponent } from "../../../../../../components/common/genericIconComponent";
import { BASE_URL_API } from "../../../../../../constants/constants";
import { fileCardPropsType } from "../../../../../../types/components";
import formatFileName from "../utils/format-file-name";
import getClasses from "../utils/get-classes";
import DownloadButton from "./download-button";
const imgTypes = new Set(["png", "jpg", "jpeg", "gif", "webp", "image"]);

View file

@ -1,9 +1,9 @@
import { useState } from "react";
import IconComponent, {
ForwardedIconComponent,
} from "../../../../../components/common/genericIconComponent";
import { Skeleton } from "../../../../../components/ui/skeleton";
import formatFileName from "./utils/format-file-name";
} from "../../../../../../components/common/genericIconComponent";
import { Skeleton } from "../../../../../../components/ui/skeleton";
import formatFileName from "../utils/format-file-name";
const supImgFiles = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "image"];

View file

@ -1,110 +0,0 @@
import { useState } from "react";
import IconComponent, {
ForwardedIconComponent,
} from "../../../../../components/common/genericIconComponent";
import { Skeleton } from "../../../../../components/ui/skeleton";
import formatFileName from "./utils/format-file-name";
const supImgFiles = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "image"];
export default function FilePreview({
error,
file,
loading,
onDelete,
}: {
loading: boolean;
file: File;
error: boolean;
onDelete: () => void;
}) {
const fileType = file.type.toLowerCase();
const isImage = supImgFiles.some((type) => fileType.includes(type));
const [isHovered, setIsHovered] = useState(false);
return (
<div className="relative inline-block">
{loading ? (
isImage ? (
<div className="flex h-20 w-20 items-center justify-center rounded-md border border-ring bg-background">
<svg
aria-hidden="true"
className={`h-10 w-10 animate-spin fill-black text-muted`}
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
</div>
) : (
<div
className={`relative ${
isImage ? "h-20 w-20" : "h-20 w-80"
} cursor-wait rounded-lg border border-ring bg-background transition duration-300 ${
isHovered ? "shadow-md" : ""
}`}
>
<div className="ml-3 flex h-full w-full items-center gap-2 text-sm">
<Skeleton className="h-10 w-10 rounded-lg" />
<div className="flex flex-col gap-1">
<Skeleton className="h-3 w-48" />
<Skeleton className="h-3 w-10" />
</div>
</div>
</div>
)
) : error ? (
<div>Error...</div>
) : (
<div
className={`relative mt-2 ${
isImage ? "h-20 w-20" : "h-20 w-80"
} cursor-pointer rounded-lg border border-ring bg-background transition duration-300 ${
isHovered ? "shadow-md" : ""
}`}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{isImage ? (
<img
src={URL.createObjectURL(file)}
alt="file"
className="block h-full w-full rounded-md border border-border"
/>
) : (
<div className="ml-3 flex h-full w-full items-center gap-2 text-sm">
<ForwardedIconComponent name="File" className="h-8 w-8" />
<div className="flex flex-col">
<span className="font-bold">{formatFileName(file.name)}</span>
<span>File</span>
</div>
</div>
)}
{isHovered && (
<div
className={`absolute ${
isImage ? "bottom-16 left-16" : "bottom-16 left-[19em]"
} flex h-5 w-5 items-center justify-center`}
>
<div
className="flex h-7 w-7 cursor-pointer items-center justify-center rounded-full bg-gray-200 p-2 transition-all"
onClick={onDelete}
>
<IconComponent name="X" className="stroke-slate-950 stroke-2" />
</div>
</div>
)}
</div>
)}
</div>
);
}

View file

@ -1,341 +0,0 @@
import { INVALID_FILE_SIZE_ALERT } from "@/constants/alerts_constants";
import { useDeleteBuilds } from "@/controllers/API/queries/_builds";
import { usePostUploadFile } from "@/controllers/API/queries/files/use-post-upload-file";
import { track } from "@/customization/utils/analytics";
import { useMessagesStore } from "@/stores/messagesStore";
import { useUtilityStore } from "@/stores/utilityStore";
import { useEffect, useRef, useState } from "react";
import ShortUniqueId from "short-unique-id";
import IconComponent from "../../../../components/common/genericIconComponent";
import {
ALLOWED_IMAGE_INPUT_EXTENSIONS,
CHAT_FIRST_INITIAL_TEXT,
CHAT_SECOND_INITIAL_TEXT,
FS_ERROR_TEXT,
SN_ERROR_TEXT,
} from "../../../../constants/constants";
import useAlertStore from "../../../../stores/alertStore";
import useFlowStore from "../../../../stores/flowStore";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { ChatMessageType, PlaygroundEvent } from "../../../../types/chat";
import { FilePreviewType, chatViewProps } from "../../../../types/components";
import ChatInput from "./chatInput";
import useDragAndDrop from "./chatInput/hooks/use-drag-and-drop";
import ChatMessage from "./chatMessage";
export default function ChatView({
sendMessage,
chatValue,
setChatValue,
lockChat,
setLockChat,
visibleSession,
focusChat,
}: chatViewProps): JSX.Element {
const { flowPool, outputs, inputs, CleanFlowPool } = useFlowStore();
const { setErrorData } = useAlertStore();
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
const messagesRef = useRef<HTMLDivElement | null>(null);
const [chatHistory, setChatHistory] = useState<ChatMessageType[]>([]);
const messages = useMessagesStore((state) => state.messages);
const inputTypes = inputs.map((obj) => obj.type);
const updateFlowPool = useFlowStore((state) => state.updateFlowPool);
const [id, setId] = useState<string>("");
const { mutate: mutateDeleteFlowPool } = useDeleteBuilds();
const maxFileSizeUpload = useUtilityStore((state) => state.maxFileSizeUpload);
//build chat history
useEffect(() => {
const messagesFromMessagesStore: ChatMessageType[] = messages
.filter(
(message) =>
message.flow_id === currentFlowId &&
(visibleSession === message.session_id || visibleSession === null),
)
.map((message) => {
let files = message.files;
// Handle the "[]" case, empty string, or already parsed array
if (Array.isArray(files)) {
// files is already an array, no need to parse
} else if (files === "[]" || files === "") {
files = [];
} else if (typeof files === "string") {
try {
files = JSON.parse(files);
} catch (error) {
console.error("Error parsing files:", error);
files = [];
}
}
return {
isSend: message.sender === "User",
message: message.text,
sender_name: message.sender_name,
files: files,
id: message.id,
timestamp: message.timestamp,
session: message.session_id,
edit: message.edit,
background_color: message.background_color || "",
text_color: message.text_color || "",
};
});
const finalChatHistory = [...messagesFromMessagesStore].sort((a, b) => {
return new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime();
});
setChatHistory(finalChatHistory);
}, [flowPool, messages, visibleSession]);
useEffect(() => {
if (messagesRef.current) {
messagesRef.current.scrollTop = messagesRef.current.scrollHeight;
}
}, []);
const ref = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (ref.current) {
ref.current.focus();
}
// trigger focus on chat when new session is set
}, [focusChat]);
function clearChat(): void {
setChatHistory([]);
mutateDeleteFlowPool(
{ flowId: currentFlowId },
{
onSuccess: () => {
CleanFlowPool();
},
},
);
//TODO tell backend to clear chat session
if (lockChat) setLockChat(false);
}
function handleSelectChange(event: string): void {
switch (event) {
case "builds":
clearChat();
break;
case "buildsNSession":
console.log("delete build and session");
break;
}
}
function updateChat(
chat: ChatMessageType,
message: string,
stream_url?: string,
) {
chat.message = message;
if (chat.componentId)
updateFlowPool(chat.componentId, {
message,
sender_name: chat.sender_name ?? "Bot",
sender: chat.isSend ? "User" : "Machine",
});
}
const [files, setFiles] = useState<FilePreviewType[]>([]);
const [isDragging, setIsDragging] = useState(false);
const { dragOver, dragEnter, dragLeave } = useDragAndDrop(setIsDragging);
const onDrop = (e) => {
e.preventDefault();
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
handleFiles(e.dataTransfer.files, setFiles, currentFlowId, setErrorData);
e.dataTransfer.clearData();
}
setIsDragging(false);
};
const { mutate } = usePostUploadFile();
const handleFiles = (files, setFiles, currentFlowId, setErrorData) => {
if (files) {
const file = files?.[0];
const fileExtension = file.name.split(".").pop()?.toLowerCase();
if (file.size > maxFileSizeUpload) {
setErrorData({
title: INVALID_FILE_SIZE_ALERT(maxFileSizeUpload / 1024 / 1024),
});
return;
}
if (
!fileExtension ||
!ALLOWED_IMAGE_INPUT_EXTENSIONS.includes(fileExtension)
) {
console.log("Error uploading file");
setErrorData({
title: "Error uploading file",
list: [FS_ERROR_TEXT, SN_ERROR_TEXT],
});
return;
}
const uid = new ShortUniqueId();
const id = uid.randomUUID(3);
setId(id);
const type = files[0].type.split("/")[0];
const blob = files[0];
setFiles((prevFiles) => [
...prevFiles,
{ file: blob, loading: true, error: false, id, type },
]);
mutate(
{ file: blob, id: currentFlowId },
{
onSuccess: (data) => {
setFiles((prev) => {
const newFiles = [...prev];
const updatedIndex = newFiles.findIndex((file) => file.id === id);
newFiles[updatedIndex].loading = false;
newFiles[updatedIndex].path = data.file_path;
return newFiles;
});
},
onError: (error) => {
setFiles((prev) => {
const newFiles = [...prev];
const updatedIndex = newFiles.findIndex((file) => file.id === id);
newFiles[updatedIndex].loading = false;
newFiles[updatedIndex].error = true;
return newFiles;
});
setErrorData({
title: "Error uploading file",
list: [error.response?.data?.detail],
});
},
},
);
}
};
const handlePlaygroundEvent = (event: PlaygroundEvent) => {
switch (event.event_type) {
case "message":
setChatHistory((prev) => [
...prev,
{
isSend: event.sender_name === "User",
message: event.text || "",
sender_name: event.sender_name,
files: event.files,
id: event.id || "",
timestamp: event.timestamp || "",
content_blocks: event.content_blocks || undefined,
background_color: event.background_color || "",
text_color: event.text_color || "",
},
]);
break;
case "error":
// Handle error event (e.g., display error message)
setErrorData({
title: "Error",
list: event.text ? [event.text] : [],
});
break;
case "warning":
// Handle warning event
break;
case "info":
// Handle info event
break;
case "token":
// Update the last message with the new token
setChatHistory((prev) => {
const newHistory = [...prev];
const lastMessage = newHistory[newHistory.length - 1];
if (lastMessage && event.token) {
lastMessage.message += event.token;
}
return newHistory;
});
break;
}
};
// Use this function in your streaming logic
const handleStreamedEvent = (event: any) => {
const playgroundEvent = event.data as PlaygroundEvent;
handlePlaygroundEvent(playgroundEvent);
};
return (
<div
className="eraser-column-arrangement"
onDragOver={dragOver}
onDragEnter={dragEnter}
onDragLeave={dragLeave}
onDrop={onDrop}
>
<div className="eraser-size">
<div ref={messagesRef} className="chat-message-div">
{chatHistory?.length > 0 ? (
chatHistory.map((chat, index) => (
<ChatMessage
setLockChat={setLockChat}
lockChat={lockChat}
chat={chat}
lastMessage={chatHistory.length - 1 === index ? true : false}
key={`${chat.id}-${index}`}
updateChat={updateChat}
/>
))
) : (
<div className="chat-alert-box">
<span>
👋 <span className="langflow-chat-span">Langflow Chat</span>
</span>
<br />
<div className="langflow-chat-desc">
<span className="langflow-chat-desc-span">
{CHAT_FIRST_INITIAL_TEXT}{" "}
<span>
<IconComponent
name="MessageSquareMore"
className="mx-1 inline h-5 w-5 animate-bounce"
/>
</span>{" "}
{CHAT_SECOND_INITIAL_TEXT}
</span>
</div>
</div>
)}
<div ref={ref}></div>
</div>
<div className="langflow-chat-input-div">
<div className="langflow-chat-input">
<ChatInput
chatValue={chatValue}
noInput={!inputTypes.includes("ChatInput")}
lockChat={lockChat}
sendMessage={({ repeat, files }) => {
sendMessage({ repeat, files });
track("Playground Message Sent");
}}
setChatValue={(value) => {
setChatValue(value);
}}
inputRef={ref}
files={files}
setFiles={setFiles}
isDragging={isDragging}
/>
</div>
</div>
</div>
</div>
);
}

View file

@ -1,5 +1,5 @@
import { TextShimmer } from "@/components/ui/TextShimmer";
import LogoIcon from "../chatView/chatMessage/components/chatLogoIcon";
import LogoIcon from "./chatView/chatMessage/components/chat-logo-icon";
export default function FlowRunningSqueleton() {
return (

View file

@ -1,9 +1,9 @@
import { InputOutput } from "@/constants/enums";
import { cn } from "@/utils/utils";
import IconComponent from "../../../../components/common/genericIconComponent";
import { SelectedViewFieldProps } from "../../types/selected-view-field";
import IOFieldView from "../IOFieldView";
import SessionView from "../SessionView";
import IconComponent from "../../../components/common/genericIconComponent";
import { SelectedViewFieldProps } from "../types/selected-view-field";
import IOFieldView from "./IOFieldView/io-field-view";
import SessionView from "./session-view";
export const SelectedViewField = ({
selectedViewField,

View file

@ -7,13 +7,10 @@ import { useIsFetching } from "@tanstack/react-query";
import { NewValueParams, SelectionChangedEvent } from "ag-grid-community";
import cloneDeep from "lodash/cloneDeep";
import { useMemo, useState } from "react";
import TableComponent from "../../../../components/core/parameterRenderComponent/components/tableComponent";
import useAlertStore from "../../../../stores/alertStore";
import { useMessagesStore } from "../../../../stores/messagesStore";
import {
extractColumnsFromRows,
messagesSorter,
} from "../../../../utils/utils";
import TableComponent from "../../../components/core/parameterRenderComponent/components/tableComponent";
import useAlertStore from "../../../stores/alertStore";
import { useMessagesStore } from "../../../stores/messagesStore";
import { extractColumnsFromRows, messagesSorter } from "../../../utils/utils";
export default function SessionView({
session,

View file

@ -1,8 +1,8 @@
import ShadTooltip from "@/components/common/shadTooltipComponent";
import { Button } from "@/components/ui/button";
import IconComponent from "../../../../components/common/genericIconComponent";
import { SidebarOpenViewProps } from "../../types/sidebar-open-view";
import SessionSelector from "../IOFieldView/components/sessionSelector/newSessionSelector";
import IconComponent from "../../../components/common/genericIconComponent";
import { SidebarOpenViewProps } from "../types/sidebar-open-view";
import SessionSelector from "./IOFieldView/components/session-selector";
export const SidebarOpenView = ({
sessions,

View file

@ -1,556 +0,0 @@
import AccordionComponent from "@/components/common/accordionComponent";
import {
useDeleteMessages,
useGetMessagesQuery,
} from "@/controllers/API/queries/messages";
import { useUtilityStore } from "@/stores/utilityStore";
import { useEffect, useState } from "react";
import IconComponent from "../../components/common/genericIconComponent";
import ShadTooltip from "../../components/common/shadTooltipComponent";
import { Badge } from "../../components/ui/badge";
import { Button } from "../../components/ui/button";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "../../components/ui/tabs";
import { CHAT_FORM_DIALOG_SUBTITLE } from "../../constants/constants";
import { InputOutput } from "../../constants/enums";
import useAlertStore from "../../stores/alertStore";
import useFlowStore from "../../stores/flowStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { useMessagesStore } from "../../stores/messagesStore";
import { IOModalPropsType } from "../../types/components";
import { AllNodeType } from "../../types/flow";
import { cn } from "../../utils/utils";
import BaseModal from "../baseModal";
import IOFieldView from "./components/IOFieldView";
import SessionSelector from "./components/IOFieldView/components/sessionSelector";
import SessionView from "./components/SessionView";
import ChatView from "./components/chatView";
export default function IOModal({
children,
open,
setOpen,
disable,
isPlayground,
}: IOModalPropsType): JSX.Element {
const allNodes = useFlowStore((state) => state.nodes);
const inputs = useFlowStore((state) => state.inputs).filter(
(input) => input.type !== "ChatInput",
);
const chatInput = useFlowStore((state) => state.inputs).find(
(input) => input.type === "ChatInput",
);
const outputs = useFlowStore((state) => state.outputs).filter(
(output) => output.type !== "ChatOutput",
);
const chatOutput = useFlowStore((state) => state.outputs).find(
(output) => output.type === "ChatOutput",
);
const nodes = useFlowStore((state) => state.nodes).filter(
(node) =>
inputs.some((input) => input.id === node.id) ||
outputs.some((output) => output.id === node.id),
);
const haveChat = chatInput || chatOutput;
const [selectedTab, setSelectedTab] = useState(
inputs.length > 0 ? 1 : outputs.length > 0 ? 2 : 0,
);
const setErrorData = useAlertStore((state) => state.setErrorData);
const setNoticeData = useAlertStore((state) => state.setNoticeData);
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const deleteSession = useMessagesStore((state) => state.deleteSession);
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
const { mutate: deleteSessionFunction } = useDeleteMessages();
const [visibleSession, setvisibleSession] = useState<string | undefined>(
currentFlowId,
);
function handleDeleteSession(session_id: string) {
deleteSessionFunction(
{
ids: messages
.filter((msg) => msg.session_id === session_id)
.map((msg) => msg.id),
},
{
onSuccess: () => {
setSuccessData({
title: "Session deleted successfully.",
});
deleteSession(session_id);
if (visibleSession === session_id) {
setvisibleSession(undefined);
}
},
onError: () => {
setErrorData({
title: "Error deleting Session.",
});
},
},
);
}
function startView() {
if (!chatInput && !chatOutput) {
if (inputs.length > 0) {
return inputs[0];
} else {
return outputs[0];
}
} else {
return undefined;
}
}
const [selectedViewField, setSelectedViewField] = useState<
{ type: string; id: string } | undefined
>(startView());
const buildFlow = useFlowStore((state) => state.buildFlow);
const setIsBuilding = useFlowStore((state) => state.setIsBuilding);
const lockChat = useFlowStore((state) => state.lockChat);
const setLockChat = useFlowStore((state) => state.setLockChat);
const [chatValue, setChatValue] = useState("");
const isBuilding = useFlowStore((state) => state.isBuilding);
const setNode = useFlowStore((state) => state.setNode);
const messages = useMessagesStore((state) => state.messages);
const [sessions, setSessions] = useState<string[]>(
Array.from(
new Set(
messages
.filter((message) => message.flow_id === currentFlowId)
.map((message) => message.session_id),
),
),
);
const flowPool = useFlowStore((state) => state.flowPool);
const [sessionId, setSessionId] = useState<string>(currentFlowId);
useGetMessagesQuery(
{
mode: "union",
id: currentFlowId,
},
{ enabled: open },
);
async function sendMessage({
repeat = 1,
files,
}: {
repeat: number;
files?: string[];
}): Promise<void> {
if (isBuilding) return;
setIsBuilding(true);
setLockChat(true);
setChatValue("");
for (let i = 0; i < repeat; i++) {
await buildFlow({
input_value: chatValue,
startNodeId: chatInput?.id,
files: files,
silent: true,
session: sessionId,
setLockChat,
}).catch((err) => {
console.error(err);
setLockChat(false);
});
}
// refetch();
setLockChat(false);
if (chatInput) {
setNode(chatInput.id, (node: AllNodeType) => {
const newNode = { ...node };
newNode.data.node!.template["input_value"].value = chatValue;
return newNode;
});
}
}
useEffect(() => {
setSelectedTab(inputs.length > 0 ? 1 : outputs.length > 0 ? 2 : 0);
}, [allNodes.length]);
useEffect(() => {
const sessions = new Set<string>();
messages
.filter((message) => message.flow_id === currentFlowId)
.forEach((row) => {
sessions.add(row.session_id);
});
setSessions((prev) => {
if (prev.length < Array.from(sessions).length) {
// set the new session as visible
setvisibleSession(
Array.from(sessions)[Array.from(sessions).length - 1],
);
}
return Array.from(sessions);
});
}, [messages]);
useEffect(() => {
if (!visibleSession) {
setSessionId(
`Session ${new Date().toLocaleString("en-US", { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit", hour12: false, second: "2-digit", timeZone: "UTC" })}`,
);
} else if (visibleSession) {
setSessionId(visibleSession);
if (selectedViewField?.type === "Session") {
setSelectedViewField({
id: visibleSession,
type: "Session",
});
}
}
}, [visibleSession]);
const setPlaygroundScrollBehaves = useUtilityStore(
(state) => state.setPlaygroundScrollBehaves,
);
useEffect(() => {
if (open) {
setPlaygroundScrollBehaves("instant");
}
}, [open]);
return (
<BaseModal
open={open}
setOpen={setOpen}
disable={disable}
type={isPlayground ? "modal" : undefined}
onSubmit={() => sendMessage({ repeat: 1 })}
size="x-large"
>
<BaseModal.Trigger>{children}</BaseModal.Trigger>
{/* TODO ADAPT TO ALL TYPES OF INPUTS AND OUTPUTS */}
<BaseModal.Header description={CHAT_FORM_DIALOG_SUBTITLE}>
<div className="flex items-center">
<span className="pr-2">Playground</span>
<IconComponent
name="BotMessageSquareIcon"
className="h-6 w-6 pl-1 text-foreground"
aria-hidden="true"
/>
</div>
</BaseModal.Header>
<BaseModal.Content overflowHidden>
<div className="flex h-full flex-col">
<div className="flex-max-width h-full">
<div
className={cn(
"mr-6 flex h-full w-2/6 flex-shrink-0 flex-col justify-start transition-all duration-300",
)}
>
<Tabs
value={selectedTab.toString()}
className={
"flex h-full flex-col overflow-y-auto rounded-md border bg-muted text-center custom-scroll"
}
onValueChange={(value) => {
setSelectedTab(Number(value));
}}
>
<div className="api-modal-tablist-div">
<TabsList>
{inputs.length > 0 && (
<TabsTrigger value={"1"}>Inputs</TabsTrigger>
)}
{outputs.length > 0 && (
<TabsTrigger value={"2"}>Outputs</TabsTrigger>
)}
{haveChat && <TabsTrigger value={"0"}>Chat</TabsTrigger>}
</TabsList>
</div>
<TabsContent value={"1"} className="api-modal-tabs-content">
{nodes
.filter((node) =>
inputs.some((input) => input.id === node.id),
)
.map((node, index) => {
const input = inputs.find(
(input) => input.id === node.id,
)!;
return (
<div
className="file-component-accordion-div"
key={index}
>
<AccordionComponent
trigger={
<div className="file-component-badge-div">
<ShadTooltip
content={input.id}
styleClasses="z-50"
>
<div>
<Badge variant="gray" size="md">
{node.data.node.display_name}
</Badge>
</div>
</ShadTooltip>
<div
className="-mb-1 pr-4"
onClick={(event) => {
event.stopPropagation();
setSelectedViewField(input);
}}
>
<IconComponent
className="h-4 w-4"
name="ExternalLink"
></IconComponent>
</div>
</div>
}
key={index}
keyValue={input.id}
>
<div className="file-component-tab-column">
<div className="">
{input && (
<IOFieldView
type={InputOutput.INPUT}
left={true}
fieldType={input.type}
fieldId={input.id}
/>
)}
</div>
</div>
</AccordionComponent>
</div>
);
})}
</TabsContent>
<TabsContent value={"2"} className="api-modal-tabs-content">
{nodes
.filter((node) =>
outputs.some((output) => output.id === node.id),
)
.map((node, index) => {
const output = outputs.find(
(output) => output.id === node.id,
)!;
const textOutputValue =
(flowPool[node!.id] ?? [])[
(flowPool[node!.id]?.length ?? 1) - 1
]?.data?.artifacts ?? "";
const disabled =
textOutputValue === "" ||
JSON.stringify(textOutputValue) === "{}";
return (
<div
className="file-component-accordion-div"
key={index}
>
<AccordionComponent
disabled={disabled}
trigger={
<div className="file-component-badge-div">
<ShadTooltip
content={output.id}
styleClasses="z-50"
>
<div>
<Badge variant="gray" size="md">
{node.data.node.display_name}
</Badge>
</div>
</ShadTooltip>
<div
className="-mb-1 pr-4"
onClick={(event) => {
event.stopPropagation();
setSelectedViewField(output);
}}
>
<IconComponent
className="h-4 w-4"
name="ExternalLink"
></IconComponent>
</div>
</div>
}
key={index}
keyValue={output.id}
>
<div className="file-component-tab-column">
<div className="">
{output && (
<IOFieldView
type={InputOutput.OUTPUT}
left={true}
fieldType={output.type}
fieldId={output.id}
/>
)}
</div>
</div>
</AccordionComponent>
</div>
);
})}
</TabsContent>
<TabsContent value={"0"} className="api-modal-tabs-content">
{sessions.map((session, index) => (
<SessionSelector
setSelectedView={setSelectedViewField}
selectedView={selectedViewField}
key={index}
session={session}
deleteSession={(session) => {
handleDeleteSession(session);
if (selectedViewField?.id === session) {
setSelectedViewField(undefined);
}
}}
updateVisibleSession={(session) => {
setvisibleSession(session);
}}
toggleVisibility={() => {
setvisibleSession(session);
}}
isVisible={visibleSession === session}
inspectSession={(session) => {
setSelectedViewField({
id: session,
type: "Session",
});
}}
/>
))}
{!sessions.length && (
<span className="text-sm text-muted-foreground">
No memories available.
</span>
)}
{sessions.length > 0 && (
<div className="pt-6">
<Button
onClick={(_) => {
setvisibleSession(undefined);
setSelectedViewField(undefined);
}}
>
New Chat
</Button>
</div>
)}
</TabsContent>
</Tabs>
</div>
<div className="flex h-full min-w-96 flex-grow">
{selectedViewField && (
<div
className={cn(
"flex h-full w-full flex-col items-start gap-4 pt-4",
!selectedViewField ? "hidden" : "",
)}
>
<div className="font-xl flex items-center justify-center gap-3 font-semibold">
{haveChat && (
<button onClick={() => setSelectedViewField(undefined)}>
<IconComponent
name={"ArrowLeft"}
className="h-6 w-6"
></IconComponent>
</button>
)}
{
nodes.find((node) => node.id === selectedViewField.id)
?.data.node.display_name
}
</div>
<div className="h-full w-full">
{inputs.some(
(input) => input.id === selectedViewField.id,
) && (
<IOFieldView
type={InputOutput.INPUT}
left={false}
fieldType={selectedViewField.type!}
fieldId={selectedViewField.id!}
/>
)}
{outputs.some(
(output) => output.id === selectedViewField.id,
) && (
<IOFieldView
type={InputOutput.OUTPUT}
left={false}
fieldType={selectedViewField.type!}
fieldId={selectedViewField.id!}
/>
)}
{sessions.some(
(session) => session === selectedViewField.id,
) && (
<SessionView
session={selectedViewField.id}
id={currentFlowId}
/>
)}
</div>
</div>
)}
<div
className={cn(
"flex h-full w-full",
selectedViewField ? "hidden" : "",
)}
>
{haveChat ? (
<ChatView
focusChat={sessionId}
sendMessage={sendMessage}
chatValue={chatValue}
setChatValue={setChatValue}
lockChat={lockChat}
setLockChat={setLockChat}
visibleSession={visibleSession}
/>
) : (
<span className="flex h-full w-full items-center justify-center font-thin text-muted-foreground">
Select an IO component to view
</span>
)}
</div>
</div>
</div>
</div>
</BaseModal.Content>
{!haveChat ? (
<BaseModal.Footer
submit={{
label: "Run Flow",
icon: (
<IconComponent
name={isBuilding ? "Loader2" : "Zap"}
className={cn(
"h-4 w-4",
isBuilding
? "animate-spin"
: "fill-current text-medium-indigo",
)}
/>
),
}}
/>
) : (
<></>
)}
</BaseModal>
);
}

View file

@ -15,10 +15,10 @@ import { useMessagesStore } from "../../stores/messagesStore";
import { IOModalPropsType } from "../../types/components";
import { cn } from "../../utils/utils";
import BaseModal from "../baseModal";
import ChatView from "./components/chatView/newChatView";
import { ChatViewWrapper } from "./components/chatViewWrapper";
import { SelectedViewField } from "./components/selectedViewField";
import { SidebarOpenView } from "./components/sidebarOpenView";
import { ChatViewWrapper } from "./components/chat-view-wrapper";
import ChatView from "./components/chatView/chat-view";
import { SelectedViewField } from "./components/selected-view-field";
import { SidebarOpenView } from "./components/sidebar-open-view";
export default function IOModal({
children,
@ -115,7 +115,6 @@ export default function IOModal({
const setIsBuilding = useFlowStore((state) => state.setIsBuilding);
const lockChat = useFlowStore((state) => state.lockChat);
const setLockChat = useFlowStore((state) => state.setLockChat);
const [chatValue, setChatValue] = useState("");
const isBuilding = useFlowStore((state) => state.isBuilding);
const messages = useMessagesStore((state) => state.messages);
const [sessions, setSessions] = useState<string[]>(
@ -136,6 +135,9 @@ export default function IOModal({
{ enabled: open },
);
const chatValue = useUtilityStore((state) => state.chatValueStore);
const setChatValue = useUtilityStore((state) => state.setChatValueStore);
const sendMessage = useCallback(
async ({
repeat = 1,
@ -329,8 +331,6 @@ export default function IOModal({
messagesFetched={messagesFetched}
sessionId={sessionId}
sendMessage={sendMessage}
chatValue={chatValue}
setChatValue={setChatValue}
lockChat={lockChat}
setLockChat={setLockChat}
canvasOpen={canvasOpen}

View file

@ -14,8 +14,6 @@ export type ChatViewWrapperProps = {
messagesFetched: boolean;
sessionId: string;
sendMessage: (options: { repeat: number; files?: string[] }) => Promise<void>;
chatValue: string;
setChatValue: (value: string) => void;
lockChat: boolean;
setLockChat: (locked: boolean) => void;
canvasOpen: boolean | undefined;

View file

@ -1,5 +1,5 @@
import { useGetMessagesQuery } from "@/controllers/API/queries/messages";
import SessionView from "@/modals/IOModal/components/SessionView";
import SessionView from "@/modals/IOModal/components/session-view";
import HeaderMessagesComponent from "./components/headerMessages";
export default function MessagesPage() {

View file

@ -3,6 +3,8 @@ import { UtilityStoreType } from "@/types/zustand/utility";
import { create } from "zustand";
export const useUtilityStore = create<UtilityStoreType>((set, get) => ({
chatValueStore: "",
setChatValueStore: (value: string) => set({ chatValueStore: value }),
selectedItems: [],
setSelectedItems: (itemId) => {
if (get().selectedItems.includes(itemId)) {

View file

@ -499,7 +499,6 @@ export type ChatInputType = {
setFiles: (
files: FilePreviewType[] | ((prev: FilePreviewType[]) => FilePreviewType[]),
) => void;
chatValue: string;
inputRef: {
current: any;
};
@ -512,7 +511,6 @@ export type ChatInputType = {
repeat: number;
files?: string[];
}) => void;
setChatValue: (value: string) => void;
};
export type editNodeToggleType = {
@ -769,8 +767,6 @@ export type chatViewProps = {
repeat: number;
files?: string[];
}) => void;
chatValue: string;
setChatValue: (value: string) => void;
lockChat: boolean;
setLockChat: (lock: boolean) => void;
visibleSession?: string;

View file

@ -15,4 +15,6 @@ export type UtilityStoreType = {
setTags: (tags: Tag[]) => void;
featureFlags: Record<string, any>;
setFeatureFlags: (featureFlags: Record<string, any>) => void;
chatValueStore: string;
setChatValueStore: (value: string) => void;
};