Deleted chat modal and changed disposition of clearChat button

This commit is contained in:
Lucas Oliveira 2023-06-28 00:44:48 -03:00
commit a6e5dcaadc
8 changed files with 5 additions and 836 deletions

View file

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

View file

@ -1,82 +0,0 @@
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";

View file

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

View file

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

View file

@ -1,415 +0,0 @@
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 ChatModal({
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 agents 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>
);
}

View file

@ -8,7 +8,6 @@ export default function ChatInput({
lockChat,
chatValue,
sendMessage,
clearChat,
setChatValue,
inputRef,
}) {
@ -59,19 +58,11 @@ export default function ChatInput({
)}
placeholder={"Send a message..."}
/>
{/* <div className="absolute bottom-2.5 right-16">
<button disabled={lockChat} onClick={() => clearChat()}>
<Eraser
className={classNames("h-5 w-5", lockChat ? "text-gray-500 animate-pulse" : "text-gray-500 hover:text-gray-600")}
aria-hidden="true"
/>
</button>
</div> */}
<div className="absolute bottom-2 right-4">
<button className={classNames("p-2 pl-1 pr-3 transition-all duration-300 rounded-md",chatValue == "" ? "text-gray-500 hover:text-gray-600" : " bg-indigo-600 text-background")} disabled={lockChat} onClick={() => sendMessage()}>
<button className={classNames("p-2 pl-1 pr-3 transition-all duration-300 rounded-md",chatValue == "" ? "text-primary" : " bg-indigo-600 text-background")} disabled={lockChat} onClick={() => sendMessage()}>
{lockChat ? (
<Lock
className="h-5 w-5 text-gray-500 animate-pulse"
className="h-5 w-5 text-primary animate-pulse"
aria-hidden="true"
/>
) : (

View file

@ -31,7 +31,7 @@ export default function ChatMessage({
return (
<div
className={classNames(
"w-full py-2 px-2 pl-4 flex",
"w-full py-2 px-2 pl-4 pr-9 flex",
chat.isSend
? " bg-border"
: " "

View file

@ -435,11 +435,10 @@ export default function FormModal({
</div>
<div className="w-full">
<div className="flex flex-col rounded-md border bg-muted w-full h-full relative">
<div className="absolute right-3 top-3">
<div className="absolute right-3 top-3 z-50">
<button disabled={lockChat} onClick={() => clearChat()}>
<Eraser
className={classNames("h-5 w-5", lockChat ? "text-gray-500 animate-pulse" : "text-gray-500 hover:text-gray-600")}
className={classNames("h-5 w-5", lockChat ? "text-primary animate-pulse" : "text-primary hover:text-gray-600")}
aria-hidden="true"
/>
</button>
@ -484,7 +483,6 @@ export default function FormModal({
<ChatInput
chatValue={chatValue}
lockChat={lockChat}
clearChat={clearChat}
sendMessage={sendMessage}
setChatValue={setChatValue}
inputRef={ref}