Fixed tooltip not being at the top of all nodes
This commit is contained in:
parent
6b3fcd5608
commit
e020e23124
3 changed files with 11 additions and 588 deletions
|
|
@ -14,15 +14,17 @@ const TooltipContent = React.forwardRef<
|
|||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</TooltipPrimitive.Portal>
|
||||
));
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,156 +0,0 @@
|
|||
import Convert from "ansi-to-html";
|
||||
import { MessageCircle, User2 } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import rehypeMathjax from "rehype-mathjax";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import remarkMath from "remark-math";
|
||||
import AiIcon from "../../../assets/Gooey Ring-5s-271px.svg";
|
||||
import AiIconStill from "../../../assets/froze-flow.png";
|
||||
import SanitizedHTMLWrapper from "../../../components/SanitizedHTMLWrapper";
|
||||
import { ChatMessageType } from "../../../types/chat";
|
||||
import { classNames } from "../../../utils";
|
||||
import { CodeBlock } from "../../formModal/chatMessage/codeBlock";
|
||||
import FileCard from "../../formModal/fileComponent";
|
||||
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(
|
||||
"chat-message-modal",
|
||||
chat.isSend ? "bg-background " : "bg-input"
|
||||
)}
|
||||
>
|
||||
<div className={classNames("chat-message-modal-div")}>
|
||||
{!chat.isSend && (
|
||||
<div className="relative h-8 w-8">
|
||||
<img
|
||||
className={
|
||||
"chat-message-modal-img " +
|
||||
(lockChat ? "opacity-100" : "opacity-0")
|
||||
}
|
||||
src={lastMessage ? AiIcon : AiIconStill}
|
||||
/>
|
||||
<img
|
||||
className={
|
||||
"chat-message-modal-img " +
|
||||
(lockChat ? "opacity-0" : "opacity-100")
|
||||
}
|
||||
src={AiIconStill}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{chat.isSend && <User2 className="-mb-1 h-6 w-6 text-primary " />}
|
||||
</div>
|
||||
{!chat.isSend ? (
|
||||
<div className="chat-message-modal-display">
|
||||
<div className="chat-message-modal-text">
|
||||
{hidden && chat.thought && chat.thought !== "" && (
|
||||
<div
|
||||
onClick={() => setHidden((prev) => !prev)}
|
||||
className="chat-message-modal-icon-div"
|
||||
>
|
||||
<MessageCircle className="h-5 w-5 animate-bounce " />
|
||||
</div>
|
||||
)}
|
||||
{chat.thought && chat.thought !== "" && !hidden && (
|
||||
<SanitizedHTMLWrapper
|
||||
className=" chat-message-modal-thought"
|
||||
content={convert.toHtml(chat.thought)}
|
||||
suppressWarning={false}
|
||||
onClick={() => setHidden((prev) => !prev)}
|
||||
/>
|
||||
)}
|
||||
{chat.thought && chat.thought !== "" && !hidden && <br></br>}
|
||||
<div className="chat-message-modal-markdown">
|
||||
<div className="w-full">
|
||||
<div className="w-full">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm, remarkMath]}
|
||||
rehypePlugins={[rehypeMathjax]}
|
||||
className="markdown prose text-muted-foreground "
|
||||
components={{
|
||||
code({ node, inline, className, children, ...props }) {
|
||||
if (children.length) {
|
||||
if (children[0] === "▍") {
|
||||
return (
|
||||
<span className="chat-message-modal-markdown-span">
|
||||
▍
|
||||
</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="flex w-full items-center">
|
||||
<div className="chat-message-modal-alert ">
|
||||
{message.split("\n").map((line, index) => (
|
||||
<span key={index} className="text-muted-foreground ">
|
||||
{line}
|
||||
<br />
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,423 +0,0 @@
|
|||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { Eraser, MessagesSquare, X } from "lucide-react";
|
||||
import { Fragment, useContext, useEffect, useRef, useState } from "react";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { typesContext } from "../../contexts/typesContext";
|
||||
import { sendAllProps } from "../../types/api";
|
||||
import { ChatMessageType } from "../../types/chat";
|
||||
import { FlowType } from "../../types/flow";
|
||||
import { validateNodes } from "../../utils";
|
||||
import ChatInput from "./chatInput";
|
||||
import ChatMessage from "./chatMessage";
|
||||
|
||||
import _ from "lodash";
|
||||
import { getHealth } from "../../controllers/API";
|
||||
|
||||
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 } = 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 += str;
|
||||
}
|
||||
}
|
||||
if (thought) {
|
||||
if (end) {
|
||||
newChat[newChat.length - 1].thought = thought;
|
||||
} else {
|
||||
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,
|
||||
thought: data.intermediate_steps,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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) => {
|
||||
getHealth()
|
||||
.then((res) => {
|
||||
if (res.status === 200) {
|
||||
connectWS();
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
setErrorData({
|
||||
// message when the backend failed
|
||||
title: "The backend is not responding. Please try again later.",
|
||||
// possible solution list
|
||||
list: [
|
||||
"Check your internet connection.",
|
||||
"Check if the backend is running.",
|
||||
],
|
||||
});
|
||||
});
|
||||
};
|
||||
ws.current = newWs;
|
||||
} catch (error) {
|
||||
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="send-message-modal-transition" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="chat-modal-box">
|
||||
<div className="chat-modal-box-div">
|
||||
<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=" chat-modal-dialog-panel">
|
||||
<div className="chat-modal-dialog-panel-div">
|
||||
<button
|
||||
onClick={() => clearChat()}
|
||||
className="chat-modal-dialog-trash-panel"
|
||||
>
|
||||
<Eraser className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setModalOpen(false)}
|
||||
className="chat-modal-dialog-x-panel"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div ref={messagesRef} className="chat-modal-dialog-history">
|
||||
{chatHistory.length > 0 ? (
|
||||
chatHistory.map((c, i) => (
|
||||
<ChatMessage
|
||||
lockChat={lockChat}
|
||||
chat={c}
|
||||
lastMessage={
|
||||
chatHistory.length - 1 === i ? true : false
|
||||
}
|
||||
key={i}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="chat-modal-dialog-span-box">
|
||||
<span>
|
||||
👋{" "}
|
||||
<span className="text-lg text-muted-foreground">
|
||||
Langflow Chat
|
||||
</span>
|
||||
</span>
|
||||
<br />
|
||||
<div className="chat-modal-dialog-desc">
|
||||
<span className="text-base text-ring">
|
||||
Start a conversation and click the agent’s thoughts{" "}
|
||||
<span>
|
||||
<MessagesSquare className="mx-1 inline h-5 w-5 animate-bounce " />
|
||||
</span>{" "}
|
||||
to inspect the chaining process.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div ref={ref}></div>
|
||||
</div>
|
||||
<div className="chat-modal-input-div">
|
||||
<div className="chat-modal-input">
|
||||
<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