Added form button to bottom right and copied chat modal
This commit is contained in:
parent
466d583103
commit
90cc4023e5
8 changed files with 877 additions and 1 deletions
|
|
@ -29,7 +29,7 @@ export default function ChatTrigger({ open, setOpen, isBuilt }) {
|
||||||
leaveFrom="translate-y-0"
|
leaveFrom="translate-y-0"
|
||||||
leaveTo="translate-y-96"
|
leaveTo="translate-y-96"
|
||||||
>
|
>
|
||||||
<div className="absolute bottom-4 right-3">
|
<div className="absolute bottom-4 right-4">
|
||||||
<div
|
<div
|
||||||
className="flex justify-center align-center py-1 px-3 w-12 h-12 rounded-full shadow-md shadow-[#0000002a] hover:shadow-[#00000032]
|
className="flex justify-center align-center py-1 px-3 w-12 h-12 rounded-full shadow-md shadow-[#0000002a] hover:shadow-[#00000032]
|
||||||
bg-[#E2E7EE] dark:border-gray-600 cursor-pointer"
|
bg-[#E2E7EE] dark:border-gray-600 cursor-pointer"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { Transition } from "@headlessui/react";
|
||||||
|
import { FormInput, MessagesSquare } from "lucide-react";
|
||||||
|
|
||||||
|
import { alertContext } from "../../../contexts/alertContext";
|
||||||
|
import { useContext } from "react";
|
||||||
|
|
||||||
|
export default function FormTrigger({ open, setOpen, isBuilt }) {
|
||||||
|
const { setErrorData } = useContext(alertContext);
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
if (isBuilt) {
|
||||||
|
setOpen(true);
|
||||||
|
} else {
|
||||||
|
setErrorData({
|
||||||
|
title: "Flow not built",
|
||||||
|
list: ["Please build the flow before chatting"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition
|
||||||
|
show={!open}
|
||||||
|
appear={true}
|
||||||
|
enter="transition ease-out duration-300"
|
||||||
|
enterFrom="translate-y-96"
|
||||||
|
enterTo="translate-y-0"
|
||||||
|
leave="transition ease-in duration-300"
|
||||||
|
leaveFrom="translate-y-0"
|
||||||
|
leaveTo="translate-y-96"
|
||||||
|
>
|
||||||
|
<div className="absolute bottom-36 right-4">
|
||||||
|
<div
|
||||||
|
className="flex justify-center align-center py-1 px-3 w-12 h-12 rounded-full shadow-md shadow-[#0000002a] hover:shadow-[#00000032]
|
||||||
|
bg-[#E2E7EE] dark:border-gray-600 cursor-pointer"
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<button>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<FormInput
|
||||||
|
className="pth-6 w-6 stroke-2 stroke-[#5c8be1]"
|
||||||
|
style={{ color: "white" }}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -7,9 +7,12 @@ import ChatModal from "../../modals/chatModal";
|
||||||
|
|
||||||
import { getBuildStatus } from "../../controllers/API";
|
import { getBuildStatus } from "../../controllers/API";
|
||||||
import { NodeType } from "../../types/flow";
|
import { NodeType } from "../../types/flow";
|
||||||
|
import FormTrigger from "./formTrigger";
|
||||||
|
import FormModal from "../../modals/formModal";
|
||||||
|
|
||||||
export default function Chat({ flow }: ChatType) {
|
export default function Chat({ flow }: ChatType) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
const [openForm, setOpenForm] = useState(false);
|
||||||
const [isBuilt, setIsBuilt] = useState(false);
|
const [isBuilt, setIsBuilt] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -69,7 +72,9 @@ export default function Chat({ flow }: ChatType) {
|
||||||
isBuilt={isBuilt}
|
isBuilt={isBuilt}
|
||||||
/>
|
/>
|
||||||
<ChatModal key={flow.id} flow={flow} open={open} setOpen={setOpen} />
|
<ChatModal key={flow.id} flow={flow} open={open} setOpen={setOpen} />
|
||||||
|
<FormModal key={flow.id} flow={flow} open={openForm} setOpen={setOpenForm} />
|
||||||
<ChatTrigger open={open} setOpen={setOpen} isBuilt={isBuilt} />
|
<ChatTrigger open={open} setOpen={setOpen} isBuilt={isBuilt} />
|
||||||
|
<FormTrigger open={openForm} setOpen={setOpenForm} isBuilt={isBuilt} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<BuildTrigger
|
<BuildTrigger
|
||||||
|
|
|
||||||
78
src/frontend/src/modals/formModal/chatInput/index.tsx
Normal file
78
src/frontend/src/modals/formModal/chatInput/index.tsx
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
import { classNames } from "../../../utils";
|
||||||
|
import { useContext, useEffect, useRef, useState } from "react";
|
||||||
|
import { TabsContext } from "../../../contexts/tabsContext";
|
||||||
|
import { INPUT_STYLE } from "../../../constants";
|
||||||
|
import { Lock, Send } from "lucide-react";
|
||||||
|
|
||||||
|
export default function ChatInput({
|
||||||
|
lockChat,
|
||||||
|
chatValue,
|
||||||
|
sendMessage,
|
||||||
|
setChatValue,
|
||||||
|
inputRef,
|
||||||
|
}) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!lockChat && inputRef.current) {
|
||||||
|
inputRef.current.focus();
|
||||||
|
}
|
||||||
|
}, [lockChat, inputRef]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (inputRef.current) {
|
||||||
|
inputRef.current.style.height = "inherit"; // Reset the height
|
||||||
|
inputRef.current.style.height = `${inputRef.current.scrollHeight}px`; // Set it to the scrollHeight
|
||||||
|
}
|
||||||
|
}, [chatValue]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<textarea
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === "Enter" && !lockChat && !event.shiftKey) {
|
||||||
|
sendMessage();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
rows={1}
|
||||||
|
ref={inputRef}
|
||||||
|
disabled={lockChat}
|
||||||
|
style={{
|
||||||
|
resize: "none",
|
||||||
|
bottom: `${inputRef?.current?.scrollHeight}px`,
|
||||||
|
maxHeight: "150px",
|
||||||
|
overflow: `${
|
||||||
|
inputRef.current && inputRef.current.scrollHeight > 150
|
||||||
|
? "auto"
|
||||||
|
: "hidden"
|
||||||
|
}`,
|
||||||
|
}}
|
||||||
|
value={lockChat ? "Thinking..." : chatValue}
|
||||||
|
onChange={(e) => {
|
||||||
|
setChatValue(e.target.value);
|
||||||
|
}}
|
||||||
|
className={classNames(
|
||||||
|
lockChat
|
||||||
|
? " bg-input text-black dark:bg-gray-700 dark:text-gray-300"
|
||||||
|
: " bg-white-200 text-black dark:bg-gray-900 dark:text-gray-300",
|
||||||
|
"form-input block w-full custom-scroll rounded-md border-gray-300 dark:border-gray-600 pr-10 sm:text-sm" +
|
||||||
|
INPUT_STYLE
|
||||||
|
)}
|
||||||
|
placeholder={"Send a message..."}
|
||||||
|
/>
|
||||||
|
<div className="absolute bottom-0.5 right-3">
|
||||||
|
<button disabled={lockChat} onClick={() => sendMessage()}>
|
||||||
|
{lockChat ? (
|
||||||
|
<Lock
|
||||||
|
className="h-5 w-5 text-gray-500 dark:hover:text-gray-300 animate-pulse"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Send
|
||||||
|
className="h-5 w-5 text-gray-500 hover:text-gray-600 dark:hover:text-gray-300"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { IconCheck, IconClipboard, IconDownload } from "@tabler/icons-react";
|
||||||
|
import { FC, memo, useState } from "react";
|
||||||
|
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||||
|
import { oneDark } from "react-syntax-highlighter/dist/cjs/styles/prism";
|
||||||
|
import { programmingLanguages } from "../../../../utils";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
language: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CodeBlock: FC<Props> = memo(({ language, value }) => {
|
||||||
|
const [isCopied, setIsCopied] = useState<Boolean>(false);
|
||||||
|
|
||||||
|
const copyToClipboard = () => {
|
||||||
|
if (!navigator.clipboard || !navigator.clipboard.writeText) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(value).then(() => {
|
||||||
|
setIsCopied(true);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsCopied(false);
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const downloadAsFile = () => {
|
||||||
|
const fileExtension = programmingLanguages[language] || ".file";
|
||||||
|
const suggestedFileName = `${"generated-code"}${fileExtension}`;
|
||||||
|
const fileName = window.prompt("enter file name", suggestedFileName);
|
||||||
|
|
||||||
|
if (!fileName) {
|
||||||
|
// user pressed cancel on prompt
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob([value], { type: "text/plain" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.download = fileName;
|
||||||
|
link.href = url;
|
||||||
|
link.style.display = "none";
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="codeblock font-sans text-[16px]">
|
||||||
|
<div className="flex items-center justify-between py-1.5 px-4">
|
||||||
|
<span className="text-xs lowercase text-white">{language}</span>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<button
|
||||||
|
className="flex gap-1.5 items-center rounded bg-none p-1 text-xs text-white"
|
||||||
|
onClick={copyToClipboard}
|
||||||
|
>
|
||||||
|
{isCopied ? <IconCheck size={18} /> : <IconClipboard size={18} />}
|
||||||
|
{isCopied ? "Copied!" : "Copy code"}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="flex items-center rounded bg-none p-1 text-xs text-white"
|
||||||
|
onClick={downloadAsFile}
|
||||||
|
>
|
||||||
|
<IconDownload size={18} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SyntaxHighlighter
|
||||||
|
className=" w-[570px]"
|
||||||
|
language={language}
|
||||||
|
style={oneDark}
|
||||||
|
customStyle={{ margin: 0 }}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</SyntaxHighlighter>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
CodeBlock.displayName = "CodeBlock";
|
||||||
166
src/frontend/src/modals/formModal/chatMessage/index.tsx
Normal file
166
src/frontend/src/modals/formModal/chatMessage/index.tsx
Normal file
|
|
@ -0,0 +1,166 @@
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { ChatMessageType } from "../../../types/chat";
|
||||||
|
import { classNames } from "../../../utils";
|
||||||
|
import AiIcon from "../../../assets/Gooey Ring-5s-271px.svg";
|
||||||
|
import AiIconStill from "../../../assets/froze-flow.png";
|
||||||
|
import FileCard from "../fileComponent";
|
||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
|
import rehypeMathjax from "rehype-mathjax";
|
||||||
|
import remarkGfm from "remark-gfm";
|
||||||
|
import remarkMath from "remark-math";
|
||||||
|
import { CodeBlock } from "./codeBlock";
|
||||||
|
import Convert from "ansi-to-html";
|
||||||
|
import { User2, MessageCircle } from "lucide-react";
|
||||||
|
|
||||||
|
export default function ChatMessage({
|
||||||
|
chat,
|
||||||
|
lockChat,
|
||||||
|
lastMessage,
|
||||||
|
}: {
|
||||||
|
chat: ChatMessageType;
|
||||||
|
lockChat: boolean;
|
||||||
|
lastMessage: boolean;
|
||||||
|
}) {
|
||||||
|
const convert = new Convert({ newline: true });
|
||||||
|
const [message, setMessage] = useState("");
|
||||||
|
const imgRef = useRef(null);
|
||||||
|
useEffect(() => {
|
||||||
|
setMessage(chat.message);
|
||||||
|
}, [chat.message]);
|
||||||
|
const [hidden, setHidden] = useState(true);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"w-full py-2 pl-2 flex",
|
||||||
|
chat.isSend
|
||||||
|
? "bg-background dark:bg-gray-900 "
|
||||||
|
: "bg-input dark:bg-gray-800"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"rounded-full overflow-hidden w-8 h-8 flex items-center my-3 justify-center"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{!chat.isSend && (
|
||||||
|
<div className="relative w-8 h-8">
|
||||||
|
<img
|
||||||
|
className={
|
||||||
|
"absolute transition-opacity duration-500 scale-150 " +
|
||||||
|
(lockChat ? "opacity-100" : "opacity-0")
|
||||||
|
}
|
||||||
|
src={lastMessage ? AiIcon : AiIconStill}
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
className={
|
||||||
|
"absolute transition-opacity duration-500 scale-150 " +
|
||||||
|
(lockChat ? "opacity-0" : "opacity-100")
|
||||||
|
}
|
||||||
|
src={AiIconStill}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{chat.isSend && (
|
||||||
|
<User2 className="w-6 h-6 -mb-1 text-gray-800 dark:text-gray-200" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!chat.isSend ? (
|
||||||
|
<div className="w-full text-start flex items-center">
|
||||||
|
<div className="w-full relative text-start inline-block text-gray-600 dark:text-gray-300 text-sm font-normal">
|
||||||
|
{hidden && chat.thought && chat.thought !== "" && (
|
||||||
|
<div
|
||||||
|
onClick={() => setHidden((prev) => !prev)}
|
||||||
|
className="absolute -top-1 -left-2 cursor-pointer"
|
||||||
|
>
|
||||||
|
<MessageCircle className="w-5 h-5 animate-bounce dark:text-white" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{chat.thought && chat.thought !== "" && !hidden && (
|
||||||
|
<div
|
||||||
|
onClick={() => setHidden((prev) => !prev)}
|
||||||
|
className=" text-start inline-block rounded-md text-gray-600 dark:text-gray-200 h-full border border-gray-300 dark:border-gray-500
|
||||||
|
bg-muted dark:bg-gray-800 w-[95%] pb-3 pt-3 px-2 ml-3 cursor-pointer scrollbar-hide overflow-scroll"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: convert.toHtml(chat.thought),
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
)}
|
||||||
|
{chat.thought && chat.thought !== "" && !hidden && <br></br>}
|
||||||
|
<div className="w-full px-4 pb-3 pt-3 pr-8">
|
||||||
|
<div className="dark:text-white w-full">
|
||||||
|
<div className="w-full">
|
||||||
|
<ReactMarkdown
|
||||||
|
remarkPlugins={[remarkGfm, remarkMath]}
|
||||||
|
rehypePlugins={[rehypeMathjax]}
|
||||||
|
className="markdown prose dark:prose-invert text-gray-600 dark:text-gray-200"
|
||||||
|
components={{
|
||||||
|
code({ node, inline, className, children, ...props }) {
|
||||||
|
if (children.length) {
|
||||||
|
if (children[0] == "▍") {
|
||||||
|
return (
|
||||||
|
<span className="animate-pulse cursor-default mt-1">
|
||||||
|
▍
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
children[0] = (children[0] as string).replace(
|
||||||
|
"`▍`",
|
||||||
|
"▍"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = /language-(\w+)/.exec(className || "");
|
||||||
|
|
||||||
|
return !inline ? (
|
||||||
|
<CodeBlock
|
||||||
|
key={Math.random()}
|
||||||
|
language={(match && match[1]) || ""}
|
||||||
|
value={String(children).replace(/\n$/, "")}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<code className={className} {...props}>
|
||||||
|
{children}
|
||||||
|
</code>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{message}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
{chat.files && (
|
||||||
|
<div className="my-2 w-full">
|
||||||
|
{chat.files.map((file, index) => {
|
||||||
|
return (
|
||||||
|
<div key={index} className="my-2 w-full">
|
||||||
|
<FileCard
|
||||||
|
fileName={"Generated File"}
|
||||||
|
fileType={file.data_type}
|
||||||
|
content={file.data}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="w-full flex items-center">
|
||||||
|
<div className="text-start inline-block px-3 text-gray-600 dark:text-white">
|
||||||
|
<span
|
||||||
|
className="text-gray-600 dark:text-gray-200"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: message.replace(/\n/g, "<br>"),
|
||||||
|
}}
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
79
src/frontend/src/modals/formModal/fileComponent/index.tsx
Normal file
79
src/frontend/src/modals/formModal/fileComponent/index.tsx
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
import * as base64js from "base64-js";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { DownloadCloud, File } from "lucide-react";
|
||||||
|
|
||||||
|
export default function FileCard({ fileName, content, fileType }) {
|
||||||
|
const handleDownload = () => {
|
||||||
|
const byteArray = new Uint8Array(base64js.toByteArray(content));
|
||||||
|
const blob = new Blob([byteArray], { type: "application/octet-stream" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = url;
|
||||||
|
link.download = fileName + ".png";
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
function handleMouseEnter() {
|
||||||
|
setIsHovered(true);
|
||||||
|
}
|
||||||
|
function handleMouseLeave() {
|
||||||
|
setIsHovered(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileType === "image") {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="relative w-1/4 h-1/4"
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={`data:image/png;base64,${content}`}
|
||||||
|
alt="generated image"
|
||||||
|
className="rounded-lg w-full h-full"
|
||||||
|
/>
|
||||||
|
{isHovered && (
|
||||||
|
<div
|
||||||
|
className={`absolute top-0 right-0 bg-muted text-gray-700 rounded-bl-lg px-1 text-sm font-bold dark:bg-gray-700 dark:text-gray-300`}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="text-gray-500 py-1 px-2 dark:bg-gray-700 dark:text-gray-300"
|
||||||
|
onClick={handleDownload}
|
||||||
|
>
|
||||||
|
<DownloadCloud className="hover:scale-110 w-5 h-5 text-current" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={handleDownload}
|
||||||
|
className="bg-muted shadow rounded w-1/2 text-gray-700 hover:drop-shadow-lg px-2 py-2 flex justify-between items-center border border-gray-300"
|
||||||
|
>
|
||||||
|
<div className="flex gap-2 text-current items-center w-full mr-2">
|
||||||
|
{" "}
|
||||||
|
{fileType === "image" ? (
|
||||||
|
<img
|
||||||
|
src={`data:image/png;base64,${content}`}
|
||||||
|
alt=""
|
||||||
|
className="w-8 h-8"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<File className="w-8 h-8" />
|
||||||
|
)}
|
||||||
|
<div className="flex flex-col items-start">
|
||||||
|
{" "}
|
||||||
|
<div className="truncate text-sm text-current">{fileName}</div>
|
||||||
|
<div className="truncate text-xs text-gray-500">{fileType}</div>
|
||||||
|
</div>
|
||||||
|
<DownloadCloud className="w-6 h-6 text-current ml-auto" />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
415
src/frontend/src/modals/formModal/index.tsx
Normal file
415
src/frontend/src/modals/formModal/index.tsx
Normal file
|
|
@ -0,0 +1,415 @@
|
||||||
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
import { Fragment, useContext, useEffect, useRef, useState } from "react";
|
||||||
|
import { FlowType } from "../../types/flow";
|
||||||
|
import { alertContext } from "../../contexts/alertContext";
|
||||||
|
import { validateNodes } from "../../utils";
|
||||||
|
import { typesContext } from "../../contexts/typesContext";
|
||||||
|
import ChatMessage from "./chatMessage";
|
||||||
|
import { X, MessagesSquare, Eraser } from "lucide-react";
|
||||||
|
import { sendAllProps } from "../../types/api";
|
||||||
|
import { ChatMessageType } from "../../types/chat";
|
||||||
|
import ChatInput from "./chatInput";
|
||||||
|
|
||||||
|
import _ from "lodash";
|
||||||
|
|
||||||
|
export default function FormModal({
|
||||||
|
flow,
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
}: {
|
||||||
|
open: boolean;
|
||||||
|
setOpen: Function;
|
||||||
|
flow: FlowType;
|
||||||
|
}) {
|
||||||
|
const [chatValue, setChatValue] = useState("");
|
||||||
|
const [chatHistory, setChatHistory] = useState<ChatMessageType[]>([]);
|
||||||
|
const { reactFlowInstance } = useContext(typesContext);
|
||||||
|
const { setErrorData, setNoticeData } = useContext(alertContext);
|
||||||
|
const ws = useRef<WebSocket | null>(null);
|
||||||
|
const [lockChat, setLockChat] = useState(false);
|
||||||
|
const isOpen = useRef(open);
|
||||||
|
const messagesRef = useRef(null);
|
||||||
|
const id = useRef(flow.id);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (messagesRef.current) {
|
||||||
|
messagesRef.current.scrollTop = messagesRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
}, [chatHistory]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
isOpen.current = open;
|
||||||
|
}, [open]);
|
||||||
|
useEffect(() => {
|
||||||
|
id.current = flow.id;
|
||||||
|
}, [flow.id]);
|
||||||
|
|
||||||
|
var isStream = false;
|
||||||
|
|
||||||
|
const addChatHistory = (
|
||||||
|
message: string,
|
||||||
|
isSend: boolean,
|
||||||
|
thought?: string,
|
||||||
|
files?: Array<any>
|
||||||
|
) => {
|
||||||
|
setChatHistory((old) => {
|
||||||
|
let newChat = _.cloneDeep(old);
|
||||||
|
if (files) {
|
||||||
|
newChat.push({ message, isSend, files, thought });
|
||||||
|
} else if (thought) {
|
||||||
|
newChat.push({ message, isSend, thought });
|
||||||
|
} else {
|
||||||
|
newChat.push({ message, isSend });
|
||||||
|
}
|
||||||
|
return newChat;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
//add proper type signature for function
|
||||||
|
|
||||||
|
function updateLastMessage({
|
||||||
|
str,
|
||||||
|
thought,
|
||||||
|
end = false,
|
||||||
|
files,
|
||||||
|
}: {
|
||||||
|
str?: string;
|
||||||
|
thought?: string;
|
||||||
|
// end param default is false
|
||||||
|
end?: boolean;
|
||||||
|
files?: Array<any>;
|
||||||
|
}) {
|
||||||
|
setChatHistory((old) => {
|
||||||
|
let newChat = [...old];
|
||||||
|
if (str) {
|
||||||
|
if (end) {
|
||||||
|
newChat[newChat.length - 1].message = str;
|
||||||
|
} else {
|
||||||
|
newChat[newChat.length - 1].message =
|
||||||
|
newChat[newChat.length - 1].message + str;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (thought) {
|
||||||
|
newChat[newChat.length - 1].thought = thought;
|
||||||
|
}
|
||||||
|
if (files) {
|
||||||
|
newChat[newChat.length - 1].files = files;
|
||||||
|
}
|
||||||
|
return newChat;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOnClose(event: CloseEvent) {
|
||||||
|
if (isOpen.current) {
|
||||||
|
setErrorData({ title: event.reason });
|
||||||
|
setTimeout(() => {
|
||||||
|
connectWS();
|
||||||
|
setLockChat(false);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWebSocketUrl(chatId, isDevelopment = false) {
|
||||||
|
const isSecureProtocol = window.location.protocol === "https:";
|
||||||
|
const webSocketProtocol = isSecureProtocol ? "wss" : "ws";
|
||||||
|
const host = isDevelopment ? "localhost:7860" : window.location.host;
|
||||||
|
const chatEndpoint = `/api/v1/chat/${chatId}`;
|
||||||
|
|
||||||
|
return `${
|
||||||
|
isDevelopment ? "ws" : webSocketProtocol
|
||||||
|
}://${host}${chatEndpoint}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleWsMessage(data: any) {
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
//set chat history
|
||||||
|
setChatHistory((_) => {
|
||||||
|
let newChatHistory: ChatMessageType[] = [];
|
||||||
|
data.forEach(
|
||||||
|
(chatItem: {
|
||||||
|
intermediate_steps?: "string";
|
||||||
|
is_bot: boolean;
|
||||||
|
message: string;
|
||||||
|
type: string;
|
||||||
|
files?: Array<any>;
|
||||||
|
}) => {
|
||||||
|
if (chatItem.message) {
|
||||||
|
newChatHistory.push(
|
||||||
|
chatItem.files
|
||||||
|
? {
|
||||||
|
isSend: !chatItem.is_bot,
|
||||||
|
message: chatItem.message,
|
||||||
|
thought: chatItem.intermediate_steps,
|
||||||
|
files: chatItem.files,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
isSend: !chatItem.is_bot,
|
||||||
|
message: chatItem.message,
|
||||||
|
thought: chatItem.intermediate_steps,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return newChatHistory;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (data.type === "start") {
|
||||||
|
addChatHistory("", false);
|
||||||
|
isStream = true;
|
||||||
|
}
|
||||||
|
if (data.type === "end") {
|
||||||
|
if (data.message) {
|
||||||
|
updateLastMessage({ str: data.message, end: true });
|
||||||
|
}
|
||||||
|
if (data.intermediate_steps) {
|
||||||
|
updateLastMessage({
|
||||||
|
str: data.message,
|
||||||
|
thought: data.intermediate_steps,
|
||||||
|
end: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (data.files) {
|
||||||
|
updateLastMessage({
|
||||||
|
end: true,
|
||||||
|
files: data.files,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setLockChat(false);
|
||||||
|
isStream = false;
|
||||||
|
}
|
||||||
|
if (data.type === "stream" && isStream) {
|
||||||
|
updateLastMessage({ str: data.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectWS() {
|
||||||
|
try {
|
||||||
|
const urlWs = getWebSocketUrl(
|
||||||
|
id.current,
|
||||||
|
process.env.NODE_ENV === "development"
|
||||||
|
);
|
||||||
|
const newWs = new WebSocket(urlWs);
|
||||||
|
newWs.onopen = () => {
|
||||||
|
console.log("WebSocket connection established!");
|
||||||
|
};
|
||||||
|
newWs.onmessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
console.log("Received data:", data);
|
||||||
|
handleWsMessage(data);
|
||||||
|
//get chat history
|
||||||
|
};
|
||||||
|
newWs.onclose = (event) => {
|
||||||
|
handleOnClose(event);
|
||||||
|
};
|
||||||
|
newWs.onerror = (ev) => {
|
||||||
|
console.log(ev, "error");
|
||||||
|
if (flow.id === "") {
|
||||||
|
connectWS();
|
||||||
|
} else {
|
||||||
|
setErrorData({
|
||||||
|
title: "There was an error on web connection, please: ",
|
||||||
|
list: [
|
||||||
|
"Refresh the page",
|
||||||
|
"Use a new flow tab",
|
||||||
|
"Check if the backend is up",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ws.current = newWs;
|
||||||
|
} catch (error) {
|
||||||
|
if (flow.id === "") {
|
||||||
|
connectWS();
|
||||||
|
}
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
connectWS();
|
||||||
|
return () => {
|
||||||
|
console.log("unmount");
|
||||||
|
console.log(ws);
|
||||||
|
if (ws.current) {
|
||||||
|
ws.current.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
ws.current &&
|
||||||
|
(ws.current.readyState === ws.current.CLOSED ||
|
||||||
|
ws.current.readyState === ws.current.CLOSING)
|
||||||
|
) {
|
||||||
|
connectWS();
|
||||||
|
setLockChat(false);
|
||||||
|
}
|
||||||
|
}, [lockChat]);
|
||||||
|
|
||||||
|
async function sendAll(data: sendAllProps) {
|
||||||
|
try {
|
||||||
|
if (ws) {
|
||||||
|
ws.current.send(JSON.stringify(data));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setErrorData({
|
||||||
|
title: "There was an error sending the message",
|
||||||
|
list: [error.message],
|
||||||
|
});
|
||||||
|
setChatValue(data.message);
|
||||||
|
connectWS();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (ref.current) ref.current.scrollIntoView({ behavior: "smooth" });
|
||||||
|
}, [chatHistory]);
|
||||||
|
|
||||||
|
const ref = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open && ref.current) {
|
||||||
|
ref.current.focus();
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
function sendMessage() {
|
||||||
|
if (chatValue !== "") {
|
||||||
|
let nodeValidationErrors = validateNodes(reactFlowInstance);
|
||||||
|
if (nodeValidationErrors.length === 0) {
|
||||||
|
setLockChat(true);
|
||||||
|
let message = chatValue;
|
||||||
|
setChatValue("");
|
||||||
|
addChatHistory(message, true);
|
||||||
|
sendAll({
|
||||||
|
...reactFlowInstance.toObject(),
|
||||||
|
message,
|
||||||
|
chatHistory,
|
||||||
|
name: flow.name,
|
||||||
|
description: flow.description,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setErrorData({
|
||||||
|
title: "Oops! Looks like you missed some required information:",
|
||||||
|
list: nodeValidationErrors,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setErrorData({
|
||||||
|
title: "Error sending message",
|
||||||
|
list: ["The message cannot be empty."],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function clearChat() {
|
||||||
|
setChatHistory([]);
|
||||||
|
ws.current.send(JSON.stringify({ clear_history: true }));
|
||||||
|
if (lockChat) setLockChat(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setModalOpen(x: boolean) {
|
||||||
|
setOpen(x);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Transition.Root show={open} appear={open} as={Fragment}>
|
||||||
|
<Dialog
|
||||||
|
as="div"
|
||||||
|
className="relative z-10"
|
||||||
|
onClose={setModalOpen}
|
||||||
|
initialFocus={ref}
|
||||||
|
>
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 bg-black backdrop-blur-sm dark:bg-gray-600 dark:bg-opacity-80 bg-opacity-80 transition-opacity" />
|
||||||
|
</Transition.Child>
|
||||||
|
|
||||||
|
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||||
|
<div className="flex h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
>
|
||||||
|
<Dialog.Panel className=" drop-shadow-2xl relative flex flex-col justify-between transform h-[95%] overflow-hidden rounded-lg bg-white dark:bg-gray-800 text-left shadow-xl transition-all w-[690px]">
|
||||||
|
<div className="relative w-full p-4">
|
||||||
|
<button
|
||||||
|
onClick={() => clearChat()}
|
||||||
|
className="absolute top-2 right-10 hover:text-red-500 text-gray-600 dark:text-gray-300 dark:hover:text-red-500 z-30"
|
||||||
|
>
|
||||||
|
<Eraser className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setModalOpen(false)}
|
||||||
|
className="absolute top-1.5 right-2 hover:text-red-500 text-gray-600 dark:text-gray-300 dark:hover:text-red-500 z-30"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ref={messagesRef}
|
||||||
|
className="w-full h-full bg-white dark:bg-gray-800 border-t dark:border-t-gray-600 flex-col flex items-center overflow-scroll scrollbar-hide"
|
||||||
|
>
|
||||||
|
{chatHistory.length > 0 ? (
|
||||||
|
chatHistory.map((c, i) => (
|
||||||
|
<ChatMessage
|
||||||
|
lockChat={lockChat}
|
||||||
|
chat={c}
|
||||||
|
lastMessage={chatHistory.length - 1 == i ? true : false}
|
||||||
|
key={i}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col h-full text-center justify-center w-full items-center align-middle">
|
||||||
|
<span>
|
||||||
|
👋{" "}
|
||||||
|
<span className="text-gray-600 dark:text-gray-300 text-lg">
|
||||||
|
LangFlow Chat
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
|
<div className="bg-muted dark:bg-gray-900 rounded-md w-2/4 px-6 py-8 border border-gray-200 dark:border-gray-700">
|
||||||
|
<span className="text-base text-gray-500">
|
||||||
|
Start a conversation and click the agent’s thoughts{" "}
|
||||||
|
<span>
|
||||||
|
<MessagesSquare className="w-5 h-5 inline animate-bounce mx-1 " />
|
||||||
|
</span>{" "}
|
||||||
|
to inspect the chaining process.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div ref={ref}></div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-white dark:bg-gray-800 border-t dark:border-t-gray-600 flex-col flex items-center justify-between p-3">
|
||||||
|
<div className="relative w-full mt-1 rounded-md shadow-sm">
|
||||||
|
<ChatInput
|
||||||
|
chatValue={chatValue}
|
||||||
|
lockChat={lockChat}
|
||||||
|
sendMessage={sendMessage}
|
||||||
|
setChatValue={setChatValue}
|
||||||
|
inputRef={ref}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue