refactor: Enhance error handling, message editing and prompt display (#5310)

* 📝 (newChatMessage.tsx): add event listener to handle tab visibility change and update state accordingly
📝 (newChatMessage.tsx): remove event listener when component unmounts to prevent memory leaks

*  (use-tab-visibility.tsx): introduce custom hook useTabVisibility to track tab visibility changes in the browser
📝 (newChatMessage.tsx, newChatView.tsx): import and use useTabVisibility hook to handle tab visibility changes and update chat behavior accordingly

* 📝 (newChatMessage.tsx): remove duplicate import of useTabVisibility and update import path
📝 (newChatView.tsx): remove duplicate import of useTabVisibility and update import path
 (use-tab-visibility.tsx): create a new custom hook to track tab visibility changes in the browser

* reducing to smaller components

* 📝 (frontend): remove unused imports and clean up code in various files to improve code readability and maintainability

* 📝 (editMessage/index.tsx): Rename EditMessage component to MarkdownField for better clarity and consistency
📝 (newChatMessage.tsx): Replace EditMessage component with MarkdownField component for rendering chat messages with markdown support
📝 (newChatView.tsx): Add conditional rendering to display a message when no chat messages are fetched

 (chatViewWrapper/index.tsx): Add messageFetched prop to ChatView component to handle messages fetching status and improve component functionality.

* prompt viedw

* Refactor chat view component and remove unused prop

* [autofix.ci] apply automated fixes

---------

Co-authored-by: anovazzi1 <otavio2204@gmail.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Cristhian Zanforlin Lousa 2024-12-19 16:50:05 -03:00 committed by GitHub
commit 01c1d47ff5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 703 additions and 590 deletions

View file

@ -1,6 +1,5 @@
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,

View file

@ -0,0 +1,197 @@
import { ForwardedIconComponent } from "@/components/common/genericIconComponent";
import { TextShimmer } from "@/components/ui/TextShimmer";
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";
export const ErrorView = ({
closeChat,
fitViewNode,
chat,
showError,
lastMessage,
blocks,
}: {
blocks: any;
showError: boolean;
lastMessage: boolean;
closeChat?: () => void;
fitViewNode: (id: string) => void;
chat: any;
}) => {
return (
<>
<div className="w-5/6 max-w-[768px] py-4 word-break-break-word">
<AnimatePresence mode="wait">
{!showError && lastMessage ? (
<motion.div
key="loading"
initial={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="flex w-full gap-4 rounded-md p-2"
>
<LogoIcon />
<div className="flex items-center">
<TextShimmer className="" duration={1}>
Flow running...
</TextShimmer>
</div>
</motion.div>
) : (
<motion.div
key="error"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
className="flex w-full gap-4 rounded-md p-2"
>
<LogoIcon />
{blocks.map((block, blockIndex) => (
<div
key={blockIndex}
className="w-full rounded-xl border border-error-red-border bg-error-red p-4 text-[14px] text-foreground"
>
{block.contents.map((content, contentIndex) => {
if (content.type === "error") {
return (
<div className="" key={contentIndex}>
<div className="mb-2 flex items-center">
<ForwardedIconComponent
className="mr-2 h-[18px] w-[18px] text-destructive"
name="OctagonAlert"
/>
{content.component && (
<>
<span>
An error occured in the{" "}
<span
className={cn(
closeChat ?? "cursor-pointer underline",
)}
onClick={() => {
fitViewNode(
chat.properties?.source?.id ?? "",
);
closeChat?.();
}}
>
<strong>{content.component}</strong>
</span>{" "}
Component, stopping your flow. See below for
more details.
</span>
</>
)}
</div>
<div>
<h3 className="pb-3 font-semibold">
Error details:
</h3>
{content.field && (
<p className="pb-1">Field: {content.field}</p>
)}
{content.reason && (
<span className="">
<Markdown
linkTarget="_blank"
remarkPlugins={[remarkGfm]}
components={{
a: ({ node, ...props }) => (
<a
href={props.href}
target="_blank"
className="underline"
rel="noopener noreferrer"
>
{props.children}
</a>
),
p({ node, ...props }) {
return (
<span className="inline-block w-fit max-w-full">
{props.children}
</span>
);
},
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
language={(match && match[1]) || ""}
code={String(content).replace(
/\n$/,
"",
)}
/>
) : (
<code
className={className}
{...props}
>
{content}
</code>
);
}
},
}}
>
{content.reason}
</Markdown>
</span>
)}
{content.solution && (
<div className="mt-4">
<h3 className="pb-3 font-semibold">
Steps to fix:
</h3>
<ol className="list-decimal pl-5">
<li>Check the component settings</li>
<li>Ensure all required fields are filled</li>
<li>Re-run your flow</li>
</ol>
</div>
)}
</div>
</div>
);
}
return null;
})}
</div>
))}
</motion.div>
)}
</AnimatePresence>
</div>
</>
);
};

View file

@ -0,0 +1,83 @@
import { cn } from "@/utils/utils";
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";
type MarkdownFieldProps = {
chat: any;
isEmpty: boolean;
chatMessage: string;
editedFlag: React.ReactNode;
};
export const MarkdownField = ({
chat,
isEmpty,
chatMessage,
editedFlag,
}: MarkdownFieldProps) => {
return (
<div className="w-full items-baseline gap-2">
<Markdown
remarkPlugins={[remarkGfm]}
linkTarget="_blank"
rehypePlugins={[rehypeMathjax]}
className={cn(
"markdown prose flex w-fit max-w-full flex-col items-baseline text-[14px] font-normal word-break-break-word dark:prose-invert",
isEmpty ? "text-muted-foreground" : "text-primary",
)}
components={{
p({ node, ...props }) {
return <span className="w-fit max-w-full">{props.children}</span>;
},
ol({ node, ...props }) {
return <ol className="max-w-full">{props.children}</ol>;
},
ul({ node, ...props }) {
return <ul className="max-w-full">{props.children}</ul>;
},
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
language={(match && match[1]) || ""}
code={String(content).replace(/\n$/, "")}
/>
) : (
<code className={className} {...props}>
{content}
</code>
);
}
},
}}
>
{isEmpty && !chat.stream_url ? EMPTY_OUTPUT_SEND_MESSAGE : chatMessage}
</Markdown>
{editedFlag}
</div>
);
};

View file

@ -0,0 +1,20 @@
export 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;
});
};

View file

@ -1,6 +1,5 @@
import { ProfileIcon } from "@/components/core/appHeaderComponent/components/ProfileIcon";
import { ContentBlockDisplay } from "@/components/core/chatComponents/ContentBlockDisplay";
import { TextShimmer } from "@/components/ui/TextShimmer";
import { useUpdateMessage } from "@/controllers/API/queries/messages";
import { CustomProfileIcon } from "@/customization/components/custom-profile-icon";
import { ENABLE_DATASTAX_LANGFLOW } from "@/customization/feature-flags";
@ -8,11 +7,7 @@ import useFlowsManagerStore from "@/stores/flowsManagerStore";
import useFlowStore from "@/stores/flowStore";
import { useUtilityStore } from "@/stores/utilityStore";
import Convert from "ansi-to-html";
import { AnimatePresence, motion } from "framer-motion";
import { useEffect, useRef, useState } from "react";
import Markdown from "react-markdown";
import rehypeMathjax from "rehype-mathjax";
import remarkGfm from "remark-gfm";
import Robot from "../../../../../assets/robot.png";
import IconComponent, {
ForwardedIconComponent,
@ -27,10 +22,12 @@ import useTabVisibility from "../../../../../shared/hooks/use-tab-visibility";
import useAlertStore from "../../../../../stores/alertStore";
import { chatMessagePropsType } from "../../../../../types/components";
import { cn } from "../../../../../utils/utils";
import LogoIcon from "./components/chatLogoIcon";
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 { convertFiles } from "./helpers/convert-files";
export default function ChatMessage({
chat,
@ -63,6 +60,7 @@ export default function ChatMessage({
const chatMessageString = chat.message ? chat.message.toString() : "";
setChatMessage(chatMessageString);
}, [chat]);
const playgroundScrollBehaves = useUtilityStore(
(state) => state.playgroundScrollBehaves,
);
@ -167,27 +165,6 @@ export default function ChatMessage({
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(
{
@ -252,176 +229,14 @@ export default function ChatMessage({
const blocks = chat.content_blocks ?? [];
return (
<div className="w-5/6 max-w-[768px] py-4 word-break-break-word">
<AnimatePresence mode="wait">
{!showError && lastMessage ? (
<motion.div
key="loading"
initial={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="flex w-full gap-4 rounded-md p-2"
>
<LogoIcon />
<div className="flex items-center">
<TextShimmer className="" duration={1}>
Flow running...
</TextShimmer>
</div>
</motion.div>
) : (
<motion.div
key="error"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
className="flex w-full gap-4 rounded-md p-2"
>
<LogoIcon />
{blocks.map((block, blockIndex) => (
<div
key={blockIndex}
className="w-full rounded-xl border border-error-red-border bg-error-red p-4 text-[14px] text-foreground"
>
{block.contents.map((content, contentIndex) => {
if (content.type === "error") {
return (
<div className="" key={contentIndex}>
<div className="mb-2 flex items-center">
<ForwardedIconComponent
className="mr-2 h-[18px] w-[18px] text-destructive"
name="OctagonAlert"
/>
{content.component && (
<>
<span>
An error occured in the{" "}
<span
className={cn(
closeChat
? "cursor-pointer underline"
: "",
)}
onClick={() => {
fitViewNode(
chat.properties?.source?.id ?? "",
);
closeChat?.();
}}
>
<strong>{content.component}</strong>
</span>{" "}
Component, stopping your flow. See below for
more details.
</span>
</>
)}
</div>
<div>
<h3 className="pb-3 font-semibold">
Error details:
</h3>
{content.field && (
<p className="pb-1">Field: {content.field}</p>
)}
{content.reason && (
<span className="">
<Markdown
linkTarget="_blank"
remarkPlugins={[remarkGfm]}
components={{
a: ({ node, ...props }) => (
<a
href={props.href}
target="_blank"
className="underline"
rel="noopener noreferrer"
>
{props.children}
</a>
),
p({ node, ...props }) {
return (
<span className="inline-block w-fit max-w-full">
{props.children}
</span>
);
},
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
language={(match && match[1]) || ""}
code={String(content).replace(
/\n$/,
"",
)}
/>
) : (
<code
className={className}
{...props}
>
{content}
</code>
);
}
},
}}
>
{content.reason}
</Markdown>
</span>
)}
{content.solution && (
<div className="mt-4">
<h3 className="pb-3 font-semibold">
Steps to fix:
</h3>
<ol className="list-decimal pl-5">
<li>Check the component settings</li>
<li>Ensure all required fields are filled</li>
<li>Re-run your flow</li>
</ol>
</div>
)}
</div>
</div>
);
}
return null;
})}
</div>
))}
</motion.div>
)}
</AnimatePresence>
</div>
<ErrorView
blocks={blocks}
showError={showError}
lastMessage={lastMessage}
closeChat={closeChat}
fitViewNode={fitViewNode}
chat={chat}
/>
);
}
@ -568,100 +383,12 @@ export default function ChatMessage({
onCancel={() => setEditMessage(false)}
/>
) : (
<>
<div className="w-full items-baseline gap-2">
<Markdown
remarkPlugins={[remarkGfm]}
linkTarget="_blank"
rehypePlugins={[rehypeMathjax]}
className={cn(
"markdown prose flex w-fit max-w-full flex-col items-baseline text-[14px] font-normal word-break-break-word dark:prose-invert",
isEmpty
? "text-muted-foreground"
: "text-primary",
)}
components={{
p({ node, ...props }) {
return (
<span className="w-fit max-w-full">
{props.children}
</span>
);
},
ol({ node, ...props }) {
return (
<ol className="max-w-full">
{props.children}
</ol>
);
},
ul({ node, ...props }) {
return (
<ul className="max-w-full">
{props.children}
</ul>
);
},
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
language={
(match && match[1]) || ""
}
code={String(content).replace(
/\n$/,
"",
)}
/>
) : (
<code
className={className}
{...props}
>
{content}
</code>
);
}
},
}}
>
{isEmpty && !chat.stream_url
? EMPTY_OUTPUT_SEND_MESSAGE
: chatMessage}
</Markdown>
{editedFlag}
</div>
</>
<MarkdownField
chat={chat}
isEmpty={isEmpty}
chatMessage={chatMessage}
editedFlag={editedFlag}
/>
)}
</div>
)}
@ -672,95 +399,37 @@ export default function ChatMessage({
</div>
) : (
<div className="form-modal-chat-text-position flex-grow">
{template ? (
<>
<button
className="form-modal-initial-prompt-btn"
onClick={() => {
setPromptOpen((old) => !old);
<div className="flex w-full flex-col">
{editMessage ? (
<EditMessageField
key={`edit-message-${chat.id}`}
message={decodedMessage}
onEdit={(message) => {
handleEditMessage(message);
}}
>
Display Prompt
<IconComponent
name="ChevronDown"
className={`h-3 w-3 transition-all ${promptOpen ? "rotate-180" : ""}`}
/>
</button>
<span
className={cn(
"prose text-[14px] font-normal word-break-break-word dark:prose-invert",
!isEmpty ? "text-primary" : "text-muted-foreground",
)}
>
{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 w-full flex-col">
{editMessage ? (
<EditMessageField
key={`edit-message-${chat.id}`}
message={decodedMessage}
onEdit={(message) => {
handleEditMessage(message);
}}
onCancel={() => setEditMessage(false)}
/>
) : (
<>
<div
className={`w-full items-baseline whitespace-pre-wrap break-words text-[14px] font-normal ${
isEmpty ? "text-muted-foreground" : "text-primary"
}`}
data-testid={`chat-message-${chat.sender_name}-${chatMessage}`}
>
{isEmpty ? EMPTY_INPUT_SEND_MESSAGE : decodedMessage}
{editedFlag}
</div>
</>
)}
{chat.files && (
<div className="my-2 flex flex-col gap-5">
{chat.files?.map((file, index) => {
return <FileCardWrapper index={index} path={file} />;
})}
onCancel={() => setEditMessage(false)}
/>
) : (
<>
<div
className={`w-full items-baseline whitespace-pre-wrap break-words text-[14px] font-normal ${
isEmpty ? "text-muted-foreground" : "text-primary"
}`}
data-testid={`chat-message-${chat.sender_name}-${chatMessage}`}
>
{isEmpty ? EMPTY_INPUT_SEND_MESSAGE : decodedMessage}
{editedFlag}
</div>
)}
</div>
)}
</>
)}
{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>
)}
</div>

View file

@ -91,8 +91,8 @@ export default function ChatView({
if (messages.length === 0 && !lockChat && chatInputNode) {
setChatValue(chatInputNode.data.node.template["input_value"].value ?? "");
} else if (isTabHidden) {
setChatValue("");
} else {
isTabHidden ? setChatValue("") : null;
}
setChatHistory(finalChatHistory);

View file

@ -0,0 +1,115 @@
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";
export const ChatViewWrapper = ({
selectedViewField,
visibleSession,
sessions,
sidebarOpen,
currentFlowId,
setSidebarOpen,
isPlayground,
setvisibleSession,
setSelectedViewField,
messagesFetched,
sessionId,
sendMessage,
chatValue,
setChatValue,
lockChat,
setLockChat,
canvasOpen,
setOpen,
}: ChatViewWrapperProps) => {
return (
<div
className={cn(
"flex h-full w-full flex-col justify-between p-4",
selectedViewField ? "hidden" : "",
)}
>
<div className="mb-4 h-[5%] text-[16px] font-semibold">
{visibleSession && sessions.length > 0 && sidebarOpen && (
<div className="hidden lg:block">
{visibleSession === currentFlowId
? "Default Session"
: `${visibleSession}`}
</div>
)}
<div className={cn(sidebarOpen ? "lg:hidden" : "")}>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="icon"
onClick={() => setSidebarOpen(true)}
className="h-8 w-8"
>
<IconComponent
name="PanelLeftOpen"
className="h-[18px] w-[18px] text-ring"
/>
</Button>
<div className="font-semibold">Playground</div>
</div>
</div>
<div
className={cn(
sidebarOpen ? "pointer-events-none opacity-0" : "",
"absolute flex h-8 items-center justify-center rounded-sm ring-offset-background transition-opacity focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
isPlayground ? "right-2 top-4" : "right-12 top-2",
)}
>
<ShadTooltip side="bottom" styleClasses="z-50" content="New Chat">
<Button
className="mr-2 h-[32px] w-[32px] hover:bg-secondary-hover"
variant="ghost"
size="icon"
onClick={() => {
setvisibleSession(undefined);
setSelectedViewField(undefined);
}}
>
<IconComponent
name="Plus"
className="!h-[18px] !w-[18px] text-ring"
/>
</Button>
</ShadTooltip>
{!isPlayground && <Separator orientation="vertical" />}
</div>
</div>
<div
className={cn(
visibleSession ? "h-[95%]" : "h-full",
sidebarOpen
? "pointer-events-none blur-sm lg:pointer-events-auto lg:blur-0"
: "",
)}
>
{messagesFetched && (
<ChatView
focusChat={sessionId}
sendMessage={sendMessage}
chatValue={chatValue}
setChatValue={setChatValue}
lockChat={lockChat}
setLockChat={setLockChat}
visibleSession={visibleSession}
closeChat={
!canvasOpen
? undefined
: () => {
setOpen(false);
}
}
/>
)}
</div>
</div>
);
};

View file

@ -0,0 +1,64 @@
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";
export const SelectedViewField = ({
selectedViewField,
setSelectedViewField,
haveChat,
inputs,
outputs,
sessions,
currentFlowId,
nodes,
}: SelectedViewFieldProps) => {
return (
<>
<div
className={cn(
"flex h-full w-full flex-col items-start gap-4 p-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>
</>
);
};

View file

@ -0,0 +1,79 @@
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";
export const SidebarOpenView = ({
sessions,
setSelectedViewField,
setvisibleSession,
handleDeleteSession,
visibleSession,
selectedViewField,
}: SidebarOpenViewProps) => {
return (
<>
<div className="flex flex-col pl-3">
<div className="flex flex-col gap-2 pb-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<IconComponent
name="MessagesSquare"
className="h-[18px] w-[18px] text-ring"
/>
<div className="text-[13px] font-normal">Chat</div>
</div>
<ShadTooltip styleClasses="z-50" content="New Chat">
<div>
<Button
data-testid="new-chat"
variant="ghost"
className="flex h-8 w-8 items-center justify-center !p-0 hover:bg-secondary-hover"
onClick={(_) => {
setvisibleSession(undefined);
setSelectedViewField(undefined);
}}
>
<IconComponent
name="Plus"
className="h-[18px] w-[18px] text-ring"
/>
</Button>
</div>
</ShadTooltip>
</div>
</div>
<div className="flex flex-col">
{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",
});
}}
/>
))}
</div>
</div>
</>
);
};

View file

@ -8,7 +8,6 @@ import { useCallback, useEffect, useState } from "react";
import IconComponent from "../../components/common/genericIconComponent";
import ShadTooltip from "../../components/common/shadTooltipComponent";
import { Button } from "../../components/ui/button";
import { InputOutput } from "../../constants/enums";
import useAlertStore from "../../stores/alertStore";
import useFlowStore from "../../stores/flowStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
@ -16,10 +15,10 @@ import { useMessagesStore } from "../../stores/messagesStore";
import { IOModalPropsType } from "../../types/components";
import { cn } from "../../utils/utils";
import BaseModal from "../baseModal";
import IOFieldView from "./components/IOFieldView";
import SessionSelector from "./components/IOFieldView/components/sessionSelector/newSessionSelector";
import SessionView from "./components/SessionView";
import ChatView from "./components/chatView/newChatView";
import { ChatViewWrapper } from "./components/chatViewWrapper";
import { SelectedViewField } from "./components/selectedViewField";
import { SidebarOpenView } from "./components/sidebarOpenView";
export default function IOModal({
children,
@ -292,217 +291,51 @@ export default function IOModal({
)}
</div>
{sidebarOpen && (
<div className="flex flex-col pl-3">
<div className="flex flex-col gap-2 pb-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<IconComponent
name="MessagesSquare"
className="h-[18px] w-[18px] text-ring"
/>
<div className="text-[13px] font-normal">Chat</div>
</div>
<ShadTooltip styleClasses="z-50" content="New Chat">
<div>
<Button
data-testid="new-chat"
variant="ghost"
className="flex h-8 w-8 items-center justify-center !p-0 hover:bg-secondary-hover"
onClick={(_) => {
setvisibleSession(undefined);
setSelectedViewField(undefined);
}}
>
<IconComponent
name="Plus"
className="h-[18px] w-[18px] text-ring"
/>
</Button>
</div>
</ShadTooltip>
</div>
</div>
<div className="flex flex-col">
{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",
});
}}
/>
))}
</div>
</div>
<SidebarOpenView
sessions={sessions}
setSelectedViewField={setSelectedViewField}
setvisibleSession={setvisibleSession}
handleDeleteSession={handleDeleteSession}
visibleSession={visibleSession}
selectedViewField={selectedViewField}
/>
)}
</div>
</div>
<div className="flex h-full min-w-96 flex-grow bg-background">
{selectedViewField && (
<div
className={cn(
"flex h-full w-full flex-col items-start gap-4 p-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>
<SelectedViewField
selectedViewField={selectedViewField}
setSelectedViewField={setSelectedViewField}
haveChat={haveChat}
inputs={inputs}
outputs={outputs}
sessions={sessions}
currentFlowId={currentFlowId}
nodes={nodes}
/>
)}
<div
className={cn(
"flex h-full w-full flex-col justify-between p-4",
selectedViewField ? "hidden" : "",
)}
>
<div className="mb-4 h-[5%] text-[16px] font-semibold">
{visibleSession && sessions.length > 0 && sidebarOpen && (
<div className="hidden lg:block">
{visibleSession === currentFlowId
? "Default Session"
: `${visibleSession}`}
</div>
)}
<div className={cn(sidebarOpen ? "lg:hidden" : "")}>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="icon"
onClick={() => setSidebarOpen(true)}
className="h-8 w-8"
>
<IconComponent
name={"PanelLeftOpen"}
className="h-[18px] w-[18px] text-ring"
/>
</Button>
<div className="font-semibold">Playground</div>
</div>
</div>
<div
className={cn(
sidebarOpen ? "pointer-events-none opacity-0" : "",
"absolute flex h-8 items-center justify-center rounded-sm ring-offset-background transition-opacity focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
isPlayground ? "right-2 top-4" : "right-12 top-2",
)}
>
<ShadTooltip
side="bottom"
styleClasses="z-50"
content="New Chat"
>
<Button
className="mr-2 h-[32px] w-[32px] hover:bg-secondary-hover"
variant="ghost"
size="icon"
onClick={(_) => {
setvisibleSession(undefined);
setSelectedViewField(undefined);
}}
>
<IconComponent
name="Plus"
className="!h-[18px] !w-[18px] text-ring"
/>
</Button>
</ShadTooltip>
{!isPlayground && <Separator orientation="vertical" />}
</div>
</div>
{haveChat ? (
<div
className={cn(
visibleSession ? "h-[95%]" : "h-full",
sidebarOpen
? "pointer-events-none blur-sm lg:pointer-events-auto lg:blur-0"
: "",
)}
>
{messagesFetched && (
<ChatView
focusChat={sessionId}
sendMessage={sendMessage}
chatValue={chatValue}
setChatValue={setChatValue}
lockChat={lockChat}
setLockChat={setLockChat}
visibleSession={visibleSession}
closeChat={
!canvasOpen
? undefined
: () => {
setOpen(false);
}
}
/>
)}
</div>
) : (
<span className="flex h-full w-full items-center justify-center font-thin text-muted-foreground">
Select an IO component to view
</span>
)}
</div>
<ChatViewWrapper
selectedViewField={selectedViewField}
visibleSession={visibleSession}
sessions={sessions}
sidebarOpen={sidebarOpen}
currentFlowId={currentFlowId}
setSidebarOpen={setSidebarOpen}
isPlayground={isPlayground}
setvisibleSession={setvisibleSession}
setSelectedViewField={setSelectedViewField}
haveChat={haveChat}
messagesFetched={messagesFetched}
sessionId={sessionId}
sendMessage={sendMessage}
chatValue={chatValue}
setChatValue={setChatValue}
lockChat={lockChat}
setLockChat={setLockChat}
canvasOpen={canvasOpen}
setOpen={setOpen}
/>
</div>
</div>
)}

View file

@ -0,0 +1,23 @@
export type ChatViewWrapperProps = {
selectedViewField: { type: string; id: string } | undefined;
visibleSession: string | undefined;
sessions: string[];
sidebarOpen: boolean;
currentFlowId: string;
setSidebarOpen: (open: boolean) => void;
isPlayground: boolean | undefined;
setvisibleSession: (session: string | undefined) => void;
setSelectedViewField: (
field: { type: string; id: string } | undefined,
) => void;
haveChat: { type: string; id: string; displayName: string } | undefined;
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;
setOpen: (open: boolean) => void;
};

View file

@ -0,0 +1,21 @@
import { Node } from "reactflow";
export type SelectedViewFieldProps = {
selectedViewField: { type: string; id: string } | undefined;
setSelectedViewField: (
field: { type: string; id: string } | undefined,
) => void;
haveChat: { type: string; id: string; displayName: string } | undefined;
inputs: Array<{
type: string;
id: string;
displayName: string;
}>;
outputs: Array<{
type: string;
id: string;
displayName: string;
}>;
sessions: string[];
currentFlowId: string;
nodes: Node[];
};

View file

@ -0,0 +1,10 @@
export type SidebarOpenViewProps = {
sessions: string[];
setSelectedViewField: (
field: { type: string; id: string } | undefined,
) => void;
setvisibleSession: (session: string | undefined) => void;
handleDeleteSession: (session: string) => void;
visibleSession: string | undefined;
selectedViewField: { type: string; id: string } | undefined;
};