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:
parent
ca8f3cad62
commit
4fec41fcce
47 changed files with 118 additions and 2363 deletions
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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 = () => (
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
@ -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
|
||||
|
|
@ -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 = [],
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
@ -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}
|
||||
|
|
@ -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}
|
||||
|
|
@ -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}
|
||||
/>
|
||||
|
|
@ -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",
|
||||
|
|
@ -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;
|
||||
|
|
@ -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)}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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,
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
@ -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,
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
@ -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;
|
||||
|
|
@ -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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
@ -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"]);
|
||||
|
||||
|
|
@ -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"];
|
||||
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 (
|
||||
|
|
@ -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,
|
||||
|
|
@ -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,
|
||||
|
|
@ -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,
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue