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:
parent
4c498a98aa
commit
e2a79a57e1
5 changed files with 228 additions and 195 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue