Bugfix: saving all the time, scroll on chat IO / Feat: Changing building status on navigation bar (#1460)

* 🐛 fix(menuBar/index.tsx): add isBuilding state to handle display of build status
✨ feat(menuBar/index.tsx): add printByBuildStatus function to display appropriate build status based on isBuilding and saveLoading states
🐛 fix(api.tsx): set isBuilding state to false when encountering a 500 error response
🐛 fix(buildUtils.ts): set isBuilding state to true before building vertices and set it to false after build completion

* 🐛 fix(chatMessage): scroll to last chat message when rendering a new message to improve user experience
🔥 refactor(chatMessage): remove unused state and variables to improve code readability and maintainability
🔥 refactor(flowsManagerStore): remove redundant code and improve code readability

* Add logger and session imports, and update build_vertex function

* Fix conditional rendering bug in IOView component

* Remove unused imports from chat.py

* add last message to end

* 🔧 fix(chatMessage): add useEffect hook to scroll to last chat message after rendering
🔧 fix(chatMessage): add dependency array to useEffect hook to prevent unnecessary re-renders

---------

Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@logspace.ai>
This commit is contained in:
Cristhian Zanforlin Lousa 2024-02-22 14:44:35 -03:00 • committed by GitHub
commit e2a79a57e1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 228 additions and 195 deletions

View file

@ -33,6 +33,7 @@ export const MenuBar = ({
const n = useFlowStore((state) => state.nodes); const n = useFlowStore((state) => state.nodes);
const navigate = useNavigate(); const navigate = useNavigate();
const isBuilding = useFlowStore((state) => state.isBuilding);
function handleAddFlow() { function handleAddFlow() {
try { try {
@ -45,6 +46,15 @@ export const MenuBar = ({
} }
} }
function printByBuildStatus() {
if (isBuilding) {
return "Building...";
} else if (saveLoading) {
return "Saving...";
}
return "Saved";
}
return currentFlow ? ( return currentFlow ? (
<div className="round-button-div"> <div className="round-button-div">
<button <button
@ -127,13 +137,13 @@ export const MenuBar = ({
> >
<div className="flex items-center gap-1.5 text-sm text-muted-foreground"> <div className="flex items-center gap-1.5 text-sm text-muted-foreground">
<IconComponent <IconComponent
name={saveLoading ? "Loader2" : "CheckCircle2"} name={isBuilding || saveLoading ? "Loader2" : "CheckCircle2"}
className={cn( className={cn(
"h-4 w-4", "h-4 w-4",
saveLoading ? "animate-spin" : "animate-wiggle" isBuilding || saveLoading ? "animate-spin" : "animate-wiggle"
)} )}
/> />
{saveLoading ? "Saving..." : "Saved"} {printByBuildStatus()}
</div> </div>
</Tooltip> </Tooltip>
</div> </div>

View file

@ -1,5 +1,5 @@
import Convert from "ansi-to-html"; import Convert from "ansi-to-html";
import { useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import Markdown from "react-markdown"; import Markdown from "react-markdown";
import rehypeMathjax from "rehype-mathjax"; import rehypeMathjax from "rehype-mathjax";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
@ -23,208 +23,228 @@ export default function ChatMessage({
const template = chat.template; const template = chat.template;
const [promptOpen, setPromptOpen] = useState(false); const [promptOpen, setPromptOpen] = useState(false);
const chat_message = chat.message.toString(); const chat_message = chat.message.toString();
useEffect(() => {
const element = document.getElementById("last-chat-message");
if (element) {
setTimeout(() => {
element.scrollIntoView({ behavior: "smooth" });
}, 200);
}
}, [lastMessage]);
return ( return (
<div <>
className={classNames("form-modal-chat-position", chat.isSend ? "" : " ")} <div
> className={classNames(
<div className={classNames("form-modal-chatbot-icon ")}> "form-modal-chat-position",
{!chat.isSend ? ( chat.isSend ? "" : " "
<div className="form-modal-chat-image">
<div className="form-modal-chat-bot-icon ">
<img
src={Robot}
className="form-modal-chat-icon-img"
alt="robot_image"
/>
</div>
<span className="truncate text-xs">{chat.sender_name}</span>
</div>
) : (
<div className="form-modal-chat-image">
<div className="form-modal-chat-user-icon ">
<img
src={MaleTechnology}
className="form-modal-chat-icon-img"
alt="male_technology"
/>
</div>
<span className="truncate text-xs">{chat.sender_name}</span>
</div>
)} )}
</div> >
{!chat.isSend ? ( <div className={classNames("form-modal-chatbot-icon ")}>
<div className="form-modal-chat-text-position"> {!chat.isSend ? (
<div className="form-modal-chat-text"> <div className="form-modal-chat-image">
{hidden && chat.thought && chat.thought !== "" && ( <div className="form-modal-chat-bot-icon ">
<div <img
onClick={(): void => setHidden((prev) => !prev)} src={Robot}
className="form-modal-chat-icon-div" className="form-modal-chat-icon-img"
> alt="robot_image"
<IconComponent
name="MessageSquare"
className="form-modal-chat-icon"
/> />
</div> </div>
)} <span className="truncate text-xs">{chat.sender_name}</span>
{chat.thought && chat.thought !== "" && !hidden && ( </div>
<SanitizedHTMLWrapper ) : (
className=" form-modal-chat-thought" <div className="form-modal-chat-image">
content={convert.toHtml(chat.thought)} <div className="form-modal-chat-user-icon ">
onClick={() => setHidden((prev) => !prev)} <img
/> src={MaleTechnology}
)} className="form-modal-chat-icon-img"
{chat.thought && chat.thought !== "" && !hidden && <br></br>} alt="male_technology"
<div className="w-full"> />
<div className="w-full dark:text-white"> </div>
<div className="w-full"> <span className="truncate text-xs">{chat.sender_name}</span>
{useMemo( </div>
() => )}
chat.message.toString() === "" && lockChat ? ( </div>
<IconComponent {!chat.isSend ? (
name="MoreHorizontal" <div className="form-modal-chat-text-position">
className="h-8 w-8 animate-pulse" <div className="form-modal-chat-text">
/> {hidden && chat.thought && chat.thought !== "" && (
) : ( <div
<Markdown onClick={(): void => setHidden((prev) => !prev)}
remarkPlugins={[remarkGfm, remarkMath]} className="form-modal-chat-icon-div"
rehypePlugins={[rehypeMathjax]} >
className="markdown prose min-w-full text-primary word-break-break-word <IconComponent
name="MessageSquare"
className="form-modal-chat-icon"
/>
</div>
)}
{chat.thought && chat.thought !== "" && !hidden && (
<SanitizedHTMLWrapper
className=" form-modal-chat-thought"
content={convert.toHtml(chat.thought)}
onClick={() => setHidden((prev) => !prev)}
/>
)}
{chat.thought && chat.thought !== "" && !hidden && <br></br>}
<div className="w-full">
<div className="w-full dark:text-white">
<div className="w-full">
{useMemo(
() =>
chat.message.toString() === "" && lockChat ? (
<IconComponent
name="MoreHorizontal"
className="h-8 w-8 animate-pulse"
/>
) : (
<Markdown
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeMathjax]}
className="markdown prose min-w-full text-primary word-break-break-word
dark:prose-invert" dark:prose-invert"
components={{ components={{
pre({ node, ...props }) { pre({ node, ...props }) {
return <>{props.children}</>; return <>{props.children}</>;
}, },
code: ({ code: ({
node, node,
inline, inline,
className, className,
children, children,
...props ...props
}) => { }) => {
if (children.length) { if (children.length) {
if (children[0] === "▍") { if (children[0] === "▍") {
return ( return (
<span className="form-modal-markdown-span"> <span className="form-modal-markdown-span">
▍ ▍
</span> </span>
);
}
children[0] = (children[0] as string).replace(
"`▍`",
"▍"
); );
} }
children[0] = (children[0] as string).replace( const match = /language-(\w+)/.exec(
"`▍`", className || ""
"▍"
); );
}
const match = /language-(\w+)/.exec( return !inline ? (
className || "" <CodeTabsComponent
); isMessage
tabs={[
return !inline ? ( {
<CodeTabsComponent name: (match && match[1]) || "",
isMessage mode: (match && match[1]) || "",
tabs={[ image:
{ "https://curl.se/logo/curl-symbol-transparent.png",
name: (match && match[1]) || "", language: (match && match[1]) || "",
mode: (match && match[1]) || "", code: String(children).replace(
image: /\n$/,
"https://curl.se/logo/curl-symbol-transparent.png", ""
language: (match && match[1]) || "", ),
code: String(children).replace(/\n$/, ""), },
}, ]}
]} activeTab={"0"}
activeTab={"0"} setActiveTab={() => {}}
setActiveTab={() => {}} />
/> ) : (
) : ( <code className={className} {...props}>
<code className={className} {...props}> {children}
{children} </code>
</code> );
); },
}, }}
}} >
> {chat_message}
{chat_message} </Markdown>
</Markdown> ),
), [chat.message, chat_message]
[chat.message, chat_message] )}
</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>
{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> </div>
</div> ) : (
) : ( <div>
<div> {template ? (
{template ? ( <>
<> <button
<button className="form-modal-initial-prompt-btn"
className="form-modal-initial-prompt-btn" onClick={() => {
onClick={() => { setPromptOpen((old) => !old);
setPromptOpen((old) => !old); }}
}} >
> Display Prompt
Display Prompt <IconComponent
<IconComponent name="ChevronDown"
name="ChevronDown" className={
className={ "h-3 w-3 transition-all " +
"h-3 w-3 transition-all " + (promptOpen ? "rotate-180" : "") (promptOpen ? "rotate-180" : "")
} }
/> />
</button> </button>
<span className="prose text-primary word-break-break-word dark:prose-invert"> <span className="prose text-primary word-break-break-word dark:prose-invert">
{promptOpen {promptOpen
? template?.split("\n")?.map((line, index) => { ? template?.split("\n")?.map((line, index) => {
const regex = /{([^}]+)}/g; const regex = /{([^}]+)}/g;
let match; let match;
let parts: Array<JSX.Element | string> = []; let parts: Array<JSX.Element | string> = [];
let lastIndex = 0; let lastIndex = 0;
while ((match = regex.exec(line)) !== null) { while ((match = regex.exec(line)) !== null) {
// Push text up to the match // Push text up to the match
if (match.index !== lastIndex) { if (match.index !== lastIndex) {
parts.push(line.substring(lastIndex, match.index)); parts.push(line.substring(lastIndex, match.index));
} }
// Push div with matched text // Push div with matched text
if (chat.message[match[1]]) { if (chat.message[match[1]]) {
parts.push( parts.push(
<span className="chat-message-highlight"> <span className="chat-message-highlight">
{chat.message[match[1]]} {chat.message[match[1]]}
</span> </span>
); );
} }
// Update last index // Update last index
lastIndex = regex.lastIndex; lastIndex = regex.lastIndex;
} }
// Push text after the last match // Push text after the last match
if (lastIndex !== line.length) { if (lastIndex !== line.length) {
parts.push(line.substring(lastIndex)); parts.push(line.substring(lastIndex));
} }
return <p>{parts}</p>; return <p>{parts}</p>;
}) })
: chat.message.toString()} : chat.message.toString()}
</span> </span>
</> </>
) : ( ) : (
<span>{chat.message.toString()}</span> <span>{chat.message.toString()}</span>
)} )}
</div> </div>
)} )}
</div> </div>
<div id={lastMessage ? "last-chat-message" : ""}></div>
</>
); );
} }

View file

@ -124,6 +124,7 @@ function ApiInterceptor() {
if (error?.response?.status === 500) { if (error?.response?.status === 500) {
const vertices = useFlowStore.getState().verticesBuild; const vertices = useFlowStore.getState().verticesBuild;
useFlowStore.getState().updateBuildStatus(vertices, BuildStatus.BUILT); useFlowStore.getState().updateBuildStatus(vertices, BuildStatus.BUILT);
useFlowStore.getState().setIsBuilding(false);
} }
} }

View file

@ -83,7 +83,6 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
if (saveTimeoutId) { if (saveTimeoutId) {
clearTimeout(saveTimeoutId); clearTimeout(saveTimeoutId);
} }
set({ saveLoading: true });
// Set up a new timeout. // Set up a new timeout.
saveTimeoutId = setTimeout(() => { saveTimeoutId = setTimeout(() => {
if (get().currentFlow) { if (get().currentFlow) {
@ -92,10 +91,11 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
true true
); );
} }
}, 300); // Delay of 300ms. set({ saveLoading: true });
}, 1000); // Delay of 1000ms.
}, },
saveFlow: (flow: FlowType, silent?: boolean) => { saveFlow: (flow: FlowType, silent?: boolean) => {
set({ saveLoading: true }) set({ saveLoading: true });
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
updateFlowInDatabase(flow) updateFlowInDatabase(flow)
.then((updatedFlow) => { .then((updatedFlow) => {
@ -117,7 +117,7 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
//update tabs state //update tabs state
resolve(); resolve();
set({ saveLoading: false }) set({ saveLoading: false });
} }
}) })
.catch((err) => { .catch((err) => {

View file

@ -49,6 +49,7 @@ export async function buildVertices({
const verticesIds = vertices_layers.flat(); const verticesIds = vertices_layers.flat();
useFlowStore.getState().updateBuildStatus(verticesIds, BuildStatus.TO_BUILD); useFlowStore.getState().updateBuildStatus(verticesIds, BuildStatus.TO_BUILD);
useFlowStore.getState().updateVerticesBuild(verticesIds); useFlowStore.getState().updateVerticesBuild(verticesIds);
useFlowStore.getState().setIsBuilding(true);
// Set each vertex state to building // Set each vertex state to building
const buildResults: Array<boolean> = []; const buildResults: Array<boolean> = [];
@ -76,6 +77,7 @@ export async function buildVertices({
if (onBuildComplete) { if (onBuildComplete) {
const allNodesValid = buildResults.every((result) => result); const allNodesValid = buildResults.every((result) => result);
onBuildComplete(allNodesValid); onBuildComplete(allNodesValid);
useFlowStore.getState().setIsBuilding(false);
} }
} }