Refactor: Make IOView component use zustand store
This commit is contained in:
parent
35bdfafa4f
commit
30940cb853
17 changed files with 994 additions and 17 deletions
93
src/frontend/src/components/IOview/index.tsx
Normal file
93
src/frontend/src/components/IOview/index.tsx
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import { ReactNode, useContext, useState } from "react";
|
||||
import NewChatView from "../newChatView";
|
||||
import { extractTypeFromLongId, removeCountFromString } from "../../utils/utils";
|
||||
import AccordionComponent from "../AccordionComponent";
|
||||
import { Badge } from "../ui/badge";
|
||||
import ShadTooltip from "../ShadTooltipComponent";
|
||||
import IconComponent from "../genericIconComponent";
|
||||
import { Textarea } from "../ui/textarea";
|
||||
import { NodeDataType } from "../../types/flow";
|
||||
import useFlowStore from "../../stores/flowStore";
|
||||
import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
||||
import useFlowIOStore from "../../stores/flowsIOStore";
|
||||
|
||||
|
||||
export default function IOView(): JSX.Element {
|
||||
const {inputIds, outputIds, updateNodeFlowData } =
|
||||
useFlowIOStore();
|
||||
const { reactFlowInstance } = useFlowStore();
|
||||
const options = inputIds.concat(outputIds);
|
||||
const [selectedView, setSelectedView] = useState<ReactNode>(handleSelectChange(options[0]));
|
||||
// if (outputTypes.includes("ChatOutput")) {
|
||||
// return <NewChatView />;
|
||||
// }
|
||||
function handleSelectChange(selected: string) {
|
||||
const type = extractTypeFromLongId(selected);
|
||||
return <NewChatView />
|
||||
switch (type) {
|
||||
case "ChatOutput":
|
||||
return <NewChatView />;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="form-modal-iv-box">
|
||||
<div className="mr-6 flex h-full w-2/6 flex-col justify-start overflow-auto scrollbar-hide">
|
||||
<div className="file-component-arrangement">
|
||||
<IconComponent
|
||||
name="Variable"
|
||||
className=" file-component-variable"
|
||||
/>
|
||||
<span className="file-component-variables-span text-md">
|
||||
Inputs
|
||||
</span>
|
||||
</div>
|
||||
{
|
||||
inputIds.filter(input=>extractTypeFromLongId(input)!=="ChatInput").map((inputId,index) => {
|
||||
const nodeData:NodeDataType = reactFlowInstance?.getNodes().find(node=>node.id===inputId)?.data;
|
||||
return (
|
||||
<div className="file-component-accordion-div" key={index}>
|
||||
<AccordionComponent
|
||||
trigger={
|
||||
<div className="file-component-badge-div">
|
||||
<Badge variant="gray" size="md">
|
||||
{inputId}
|
||||
</Badge>
|
||||
<div
|
||||
className="-mb-1"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
key={index}
|
||||
keyValue={inputId}
|
||||
>
|
||||
<div className="file-component-tab-column">
|
||||
<Textarea
|
||||
value={reactFlowInstance?.getNodes().find(node=>node.id===inputId)?.data?.node?.template?.value.value}
|
||||
className="custom-scroll"
|
||||
onChange={(e) => {
|
||||
e.target.value;
|
||||
if(nodeData){
|
||||
nodeData.node!.template["value"].value = e.target.value;
|
||||
updateNodeFlowData(inputId,nodeData);
|
||||
}
|
||||
}}
|
||||
placeholder="Enter text..."
|
||||
></Textarea>
|
||||
</div>
|
||||
</AccordionComponent>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
{selectedView}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
127
src/frontend/src/components/newChatView/chatInput/index.tsx
Normal file
127
src/frontend/src/components/newChatView/chatInput/index.tsx
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import IconComponent from "../../../components/genericIconComponent";
|
||||
import { Textarea } from "../../../components/ui/textarea";
|
||||
import { chatInputType } from "../../../types/components";
|
||||
import { classNames } from "../../../utils/utils";
|
||||
|
||||
export default function ChatInput({
|
||||
lockChat,
|
||||
chatValue,
|
||||
sendMessage,
|
||||
setChatValue,
|
||||
inputRef,
|
||||
noInput,
|
||||
}: chatInputType): JSX.Element {
|
||||
const [repeate, setRepeate] = useState(1);
|
||||
useEffect(() => {
|
||||
if (!lockChat && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [lockChat, inputRef]);
|
||||
|
||||
|
||||
function handleChange(value:number){
|
||||
console.log(value)
|
||||
if(value>0){
|
||||
setRepeate(value);
|
||||
}
|
||||
else{
|
||||
setRepeate(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs">repeate</span>
|
||||
<input onChange={(e)=>{
|
||||
handleChange(parseInt(e.target.value));
|
||||
}} className="bg-background" type="number" min={0}/>
|
||||
|
||||
|
||||
</div>
|
||||
<Textarea
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" && !lockChat && !event.shiftKey) {
|
||||
sendMessage(repeate);
|
||||
}
|
||||
}}
|
||||
rows={1}
|
||||
ref={inputRef}
|
||||
disabled={lockChat || noInput}
|
||||
style={{
|
||||
resize: "none",
|
||||
bottom: `${inputRef?.current?.scrollHeight}px`,
|
||||
maxHeight: "150px",
|
||||
overflow: `${
|
||||
inputRef.current && inputRef.current.scrollHeight > 150
|
||||
? "auto"
|
||||
: "hidden"
|
||||
}`,
|
||||
}}
|
||||
value={lockChat ? "Thinking..." : chatValue}
|
||||
onChange={(event): void => {
|
||||
setChatValue(event.target.value);
|
||||
}}
|
||||
className={classNames(
|
||||
lockChat
|
||||
? " form-modal-lock-true bg-input"
|
||||
: noInput
|
||||
? "form-modal-no-input bg-input"
|
||||
: " form-modal-lock-false bg-background",
|
||||
|
||||
|
||||
"form-modal-lockchat"
|
||||
)}
|
||||
placeholder={
|
||||
noInput
|
||||
? "No chat input variables found. Click to run your flow."
|
||||
: "Send a message..."
|
||||
}
|
||||
/>
|
||||
<div className="form-modal-send-icon-position">
|
||||
<button
|
||||
className={classNames(
|
||||
"form-modal-send-button",
|
||||
noInput
|
||||
? "bg-high-indigo text-background"
|
||||
: chatValue === ""
|
||||
? "text-primary"
|
||||
: "bg-chat-send text-background"
|
||||
)}
|
||||
disabled={lockChat}
|
||||
onClick={(): void => sendMessage()}
|
||||
>
|
||||
{lockChat ? (
|
||||
<IconComponent
|
||||
name="Lock"
|
||||
className="form-modal-lock-icon"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : noInput ? (
|
||||
<IconComponent
|
||||
name="Sparkles"
|
||||
className="form-modal-play-icon"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<IconComponent
|
||||
name="LucideSend"
|
||||
className="form-modal-send-icon "
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
import { IconCheck, IconClipboard, IconDownload } from "@tabler/icons-react";
|
||||
import { useState } from "react";
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { oneDark } from "react-syntax-highlighter/dist/cjs/styles/prism";
|
||||
import { programmingLanguages } from "../../../../constants/constants";
|
||||
import { Props } from "../../../../types/components";
|
||||
|
||||
export function CodeBlock({ language, value }: Props): JSX.Element {
|
||||
const [isCopied, setIsCopied] = useState<Boolean>(false);
|
||||
|
||||
|
||||
const copyToClipboard = (): void => {
|
||||
if (!navigator.clipboard || !navigator.clipboard.writeText) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
navigator.clipboard.writeText(value).then(() => {
|
||||
setIsCopied(true);
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
setIsCopied(false);
|
||||
}, 2000);
|
||||
});
|
||||
};
|
||||
const downloadAsFile = (): void => {
|
||||
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="code-block-modal">
|
||||
<span className="code-block-modal-span">{language}</span>
|
||||
|
||||
|
||||
<div className="flex items-center">
|
||||
<button className="code-block-modal-button" onClick={copyToClipboard}>
|
||||
{isCopied ? <IconCheck size={18} /> : <IconClipboard size={18} />}
|
||||
{isCopied ? "Copied!" : "Copy code"}
|
||||
</button>
|
||||
<button className="code-block-modal-button" onClick={downloadAsFile}>
|
||||
<IconDownload size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<SyntaxHighlighter
|
||||
className="overflow-auto"
|
||||
language={language}
|
||||
style={oneDark}
|
||||
customStyle={{ margin: 0 }}
|
||||
>
|
||||
{value}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
CodeBlock.displayName = "CodeBlock";
|
||||
234
src/frontend/src/components/newChatView/chatMessage/index.tsx
Normal file
234
src/frontend/src/components/newChatView/chatMessage/index.tsx
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
import Convert from "ansi-to-html";
|
||||
import { useMemo, useState } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import rehypeMathjax from "rehype-mathjax";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import remarkMath from "remark-math";
|
||||
import MaleTechnology from "../../../assets/male-technologist.png";
|
||||
import Robot from "../../../assets/robot.png";
|
||||
import SanitizedHTMLWrapper from "../../../components/SanitizedHTMLWrapper";
|
||||
import CodeTabsComponent from "../../../components/codeTabsComponent";
|
||||
import IconComponent from "../../../components/genericIconComponent";
|
||||
import { chatMessagePropsType } from "../../../types/components";
|
||||
import { classNames } from "../../../utils/utils";
|
||||
import FileCard from "../fileComponent";
|
||||
|
||||
|
||||
export default function ChatMessage({
|
||||
chat,
|
||||
lockChat,
|
||||
lastMessage,
|
||||
}: chatMessagePropsType): JSX.Element {
|
||||
const convert = new Convert({ newline: true });
|
||||
const [hidden, setHidden] = useState(true);
|
||||
const template = chat.template;
|
||||
const [promptOpen, setPromptOpen] = useState(false);
|
||||
return (
|
||||
<div
|
||||
className={classNames("form-modal-chat-position", chat.isSend ? "" : " ")}
|
||||
>
|
||||
<div className={classNames("form-modal-chatbot-icon ")}>
|
||||
{!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="text-xs truncate">{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="text-xs truncate">{chat.sender_name}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!chat.isSend ? (
|
||||
<div className="form-modal-chat-text-position">
|
||||
<div className="form-modal-chat-text">
|
||||
{hidden && chat.thought && chat.thought !== "" && (
|
||||
<div
|
||||
onClick={(): void => setHidden((prev) => !prev)}
|
||||
className="form-modal-chat-icon-div"
|
||||
>
|
||||
<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"
|
||||
/>
|
||||
) : (
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm, remarkMath]}
|
||||
rehypePlugins={[rehypeMathjax]}
|
||||
className="markdown prose min-w-full text-primary word-break-break-word
|
||||
dark:prose-invert"
|
||||
components={{
|
||||
pre({ node, ...props }) {
|
||||
return <>{props.children}</>;
|
||||
},
|
||||
code: ({
|
||||
node,
|
||||
inline,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
if (children.length) {
|
||||
if (children[0] === "▍") {
|
||||
return (
|
||||
<span className="form-modal-markdown-span">
|
||||
▍
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
children[0] = (children[0] as string).replace(
|
||||
"`▍`",
|
||||
"▍"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const match = /language-(\w+)/.exec(
|
||||
className || ""
|
||||
);
|
||||
|
||||
|
||||
return !inline ? (
|
||||
<CodeTabsComponent
|
||||
isMessage
|
||||
tabs={[
|
||||
{
|
||||
name: (match && match[1]) || "",
|
||||
mode: (match && match[1]) || "",
|
||||
image:
|
||||
"https://curl.se/logo/curl-symbol-transparent.png",
|
||||
language: (match && match[1]) || "",
|
||||
code: String(children).replace(/\n$/, ""),
|
||||
},
|
||||
]}
|
||||
activeTab={"0"}
|
||||
setActiveTab={() => {}}
|
||||
/>
|
||||
) : (
|
||||
<code className={className} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
}}
|
||||
>
|
||||
{chat.message.toString()}
|
||||
</ReactMarkdown>
|
||||
),
|
||||
[chat.message, chat.message.toString()]
|
||||
)}
|
||||
</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>
|
||||
{template ? (
|
||||
<>
|
||||
<button
|
||||
className="form-modal-initial-prompt-btn"
|
||||
onClick={() => {
|
||||
setPromptOpen((old) => !old);
|
||||
}}
|
||||
>
|
||||
Display Prompt
|
||||
<IconComponent
|
||||
name="ChevronDown"
|
||||
className={
|
||||
"h-3 w-3 transition-all " + (promptOpen ? "rotate-180" : "")
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
<span className="prose text-primary word-break-break-word dark:prose-invert">
|
||||
{promptOpen
|
||||
? template?.split("\n")?.map((line, index) => {
|
||||
const regex = /{([^}]+)}/g;
|
||||
let match;
|
||||
let parts: Array<JSX.Element | string> = [];
|
||||
let lastIndex = 0;
|
||||
while ((match = regex.exec(line)) !== null) {
|
||||
// Push text up to the match
|
||||
if (match.index !== lastIndex) {
|
||||
parts.push(line.substring(lastIndex, match.index));
|
||||
}
|
||||
// Push div with matched text
|
||||
if (chat.message[match[1]]) {
|
||||
parts.push(
|
||||
<span className="chat-message-highlight">
|
||||
{chat.message[match[1]]}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Update last index
|
||||
lastIndex = regex.lastIndex;
|
||||
}
|
||||
// Push text after the last match
|
||||
if (lastIndex !== line.length) {
|
||||
parts.push(line.substring(lastIndex));
|
||||
}
|
||||
return <p>{parts}</p>;
|
||||
})
|
||||
: chat.message.toString()}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<span>{chat.message.toString()}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
import * as base64js from "base64-js";
|
||||
import { useState } from "react";
|
||||
import IconComponent from "../../../components/genericIconComponent";
|
||||
import { fileCardPropsType } from "../../../types/components";
|
||||
|
||||
export default function FileCard({
|
||||
fileName,
|
||||
content,
|
||||
fileType,
|
||||
}: fileCardPropsType): JSX.Element {
|
||||
const handleDownload = (): void => {
|
||||
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(): void {
|
||||
setIsHovered(true);
|
||||
}
|
||||
function handleMouseLeave(): void {
|
||||
setIsHovered(false);
|
||||
}
|
||||
|
||||
|
||||
if (fileType === "image") {
|
||||
return (
|
||||
<div
|
||||
className="relative h-1/4 w-1/4"
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<img
|
||||
src={`data:image/png;base64,${content}`}
|
||||
alt="generated image"
|
||||
className="h-full w-full rounded-lg"
|
||||
/>
|
||||
{isHovered && (
|
||||
<div className={`file-card-modal-image-div `}>
|
||||
<button
|
||||
className="file-card-modal-image-button "
|
||||
onClick={handleDownload}
|
||||
>
|
||||
<IconComponent
|
||||
name="DownloadCloud"
|
||||
className="h-5 w-5 text-current hover:scale-110"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<button onClick={handleDownload} className="file-card-modal-button">
|
||||
<div className="file-card-modal-div">
|
||||
ooooooooooooooo{" "}
|
||||
{fileType === "image" ? (
|
||||
<img
|
||||
src={`data:image/png;base64,${content}`}
|
||||
alt=""
|
||||
className="h-8 w-8"
|
||||
/>
|
||||
) : (
|
||||
<IconComponent name="File" className="h-8 w-8" />
|
||||
)}
|
||||
<div className="file-card-modal-footer">
|
||||
{" "}
|
||||
<div className="file-card-modal-name">{fileName}</div>
|
||||
<div className="file-card-modal-type">{fileType}</div>
|
||||
</div>
|
||||
<IconComponent
|
||||
name="DownloadCloud"
|
||||
className="ml-auto h-6 w-6 text-current"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
193
src/frontend/src/components/newChatView/index.tsx
Normal file
193
src/frontend/src/components/newChatView/index.tsx
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
import { useContext, useEffect, useRef, useState } from "react";
|
||||
import { sendAllProps } from "../../types/api";
|
||||
import { ChatMessageType } from "../../types/chat";
|
||||
import { NodeType } from "../../types/flow";
|
||||
import { classNames } from "../../utils/utils";
|
||||
import ChatInput from "./chatInput";
|
||||
import ChatMessage from "./chatMessage";
|
||||
import { cloneDeep } from "lodash";
|
||||
import IconComponent from "../../components/genericIconComponent";
|
||||
import {
|
||||
ChatOutputType,
|
||||
FlowPoolObjectType,
|
||||
} from "../../types/chat";
|
||||
import { validateNodes } from "../../utils/reactflowUtils";
|
||||
import useAlertStore from "../../stores/alertStore";
|
||||
import useFlowIOStore from "../../stores/flowsIOStore";
|
||||
import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
||||
import useFlowStore from "../../stores/flowStore";
|
||||
|
||||
|
||||
export default function newChatView(): JSX.Element {
|
||||
const [chatValue, setChatValue] = useState("");
|
||||
const [chatHistory, setChatHistory] = useState<ChatMessageType[]>([]);
|
||||
const { reactFlowInstance } = useFlowStore()
|
||||
const {
|
||||
flowPool,
|
||||
outputIds,
|
||||
inputIds, inputTypes,
|
||||
updateNodeFlowData,
|
||||
buildFlow,
|
||||
CleanFlowPool
|
||||
} = useFlowIOStore();
|
||||
const { setErrorData } = useAlertStore();
|
||||
const [lockChat, setLockChat] = useState(false);
|
||||
const messagesRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
|
||||
//build chat history
|
||||
useEffect(() => {
|
||||
const chatOutputResponses: FlowPoolObjectType[] = [];
|
||||
outputIds.forEach((outputId) => {
|
||||
if (outputId.includes("ChatOutput")) {
|
||||
if (flowPool[outputId] && flowPool[outputId].length > 0) {
|
||||
chatOutputResponses.push(...flowPool[outputId]);
|
||||
}
|
||||
}
|
||||
});
|
||||
const chatMessages: ChatMessageType[] = chatOutputResponses
|
||||
.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp)).filter((output) => !!output.data.artifacts.message)
|
||||
.map((output) => {
|
||||
const { sender, message, sender_name } = output.data.artifacts as ChatOutputType;
|
||||
console.log(output.data.artifacts);
|
||||
const is_ai = sender === "Machine";
|
||||
return { isSend: !is_ai, message, sender_name };
|
||||
});
|
||||
setChatHistory(chatMessages);
|
||||
}, [flowPool, outputIds]);
|
||||
useEffect(() => {
|
||||
if (messagesRef.current) {
|
||||
messagesRef.current.scrollTop = messagesRef.current.scrollHeight;
|
||||
}
|
||||
}, [chatHistory]);
|
||||
|
||||
|
||||
async function sendAll(data: sendAllProps): Promise<void> { }
|
||||
useEffect(() => {
|
||||
if (ref.current) ref.current.scrollIntoView({ behavior: "smooth" });
|
||||
}, [chatHistory]);
|
||||
|
||||
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
ref.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
async function sendMessage(count = 1): Promise<void> {
|
||||
let nodeValidationErrors = validateNodes(
|
||||
reactFlowInstance!.getNodes(),
|
||||
reactFlowInstance!.getEdges()
|
||||
);
|
||||
if (nodeValidationErrors.length === 0) {
|
||||
setLockChat(true);
|
||||
setChatValue("");
|
||||
const chatInputId = inputIds.find((inputId) =>
|
||||
inputId.includes("ChatInput")
|
||||
);
|
||||
const chatInput: NodeType = reactFlowInstance?.getNode(
|
||||
chatInputId!
|
||||
) as NodeType;
|
||||
if (chatInput) {
|
||||
let newData = cloneDeep(chatInput.data);
|
||||
newData.node!.template["message"].value = chatValue;
|
||||
chatInput.data = { ...newData };
|
||||
updateNodeFlowData(chatInputId!, newData);
|
||||
}
|
||||
for (let i = 0; i < count; i++) {
|
||||
await buildFlow().catch((err) => {
|
||||
console.error(err);
|
||||
setLockChat(false);
|
||||
});
|
||||
}
|
||||
setLockChat(false);
|
||||
|
||||
|
||||
//set chat message in the flow and run build
|
||||
//@ts-ignore
|
||||
} else {
|
||||
setErrorData({
|
||||
title: "Oops! Looks like you missed some required information:",
|
||||
list: nodeValidationErrors,
|
||||
});
|
||||
}
|
||||
}
|
||||
function clearChat(): void {
|
||||
setChatHistory([]);
|
||||
CleanFlowPool();
|
||||
//TODO tell backend to clear chat session
|
||||
if (lockChat) setLockChat(false);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="eraser-column-arrangement">
|
||||
<div className="eraser-size">
|
||||
<div className="eraser-position">
|
||||
<button disabled={lockChat} onClick={() => clearChat()}>
|
||||
<IconComponent
|
||||
name="Eraser"
|
||||
className={classNames(
|
||||
"h-5 w-5",
|
||||
lockChat
|
||||
? "animate-pulse text-primary"
|
||||
: "text-primary hover:text-gray-600"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div ref={messagesRef} className="chat-message-div">
|
||||
{chatHistory?.length > 0 ? (
|
||||
chatHistory.map((chat, index) => (
|
||||
<ChatMessage
|
||||
lockChat={lockChat}
|
||||
chat={chat}
|
||||
lastMessage={chatHistory.length - 1 === index ? true : false}
|
||||
key={index}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="chat-alert-box">
|
||||
<span>
|
||||
👋 <span className="langflow-chat-span">Langflow Chat</span>
|
||||
</span>
|
||||
<br />
|
||||
<div className="langflow-chat-desc">
|
||||
<span className="langflow-chat-desc-span">
|
||||
Start a conversation and click the agent's thoughts{" "}
|
||||
<span>
|
||||
<IconComponent
|
||||
name="MessageSquare"
|
||||
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="langflow-chat-input-div">
|
||||
<div className="langflow-chat-input">
|
||||
<ChatInput
|
||||
chatValue={chatValue}
|
||||
noInput={!inputTypes.includes("ChatInput")}
|
||||
lockChat={lockChat}
|
||||
sendMessage={(count) => sendMessage(count)}
|
||||
setChatValue={(value) => {
|
||||
setChatValue(value);
|
||||
}}
|
||||
inputRef={ref}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
15
src/frontend/src/components/textInputComponent/index.tsx
Normal file
15
src/frontend/src/components/textInputComponent/index.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
export default function TextInputComponent({
|
||||
text,
|
||||
emissor,
|
||||
}: {
|
||||
text: string;
|
||||
emissor: string;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<strong> {emissor}</strong>
|
||||
<br></br>
|
||||
<span>{text}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
15
src/frontend/src/components/textOutputComponent/index.tsx
Normal file
15
src/frontend/src/components/textOutputComponent/index.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
export default function TextOutputComponent({
|
||||
text,
|
||||
emissor,
|
||||
}: {
|
||||
text: string;
|
||||
emissor: string;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<strong>{emissor}</strong>
|
||||
<br></br>
|
||||
<div className="w-80 break-all">{text}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -7,6 +7,8 @@ import {
|
|||
Component,
|
||||
LoginType,
|
||||
Users,
|
||||
VertexBuildTypeAPI,
|
||||
VerticesOrderTypeAPI,
|
||||
changeUser,
|
||||
resetPasswordType,
|
||||
sendAllProps,
|
||||
|
|
@ -850,3 +852,20 @@ export async function requestLogout() {
|
|||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getVerticesOrder(
|
||||
flowId: string
|
||||
): Promise<AxiosResponse<VerticesOrderTypeAPI>> {
|
||||
console.log;
|
||||
return await api.get(`${BASE_URL_API}build/${flowId}/vertices`);
|
||||
}
|
||||
|
||||
export async function postBuildVertex(
|
||||
flow: FlowType,
|
||||
vertexId: string
|
||||
): Promise<AxiosResponse<VertexBuildTypeAPI>> {
|
||||
return await api.post(
|
||||
`${BASE_URL_API}build/${flow.id}/vertices/${vertexId}`,
|
||||
flow
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { isInputNode, isOutputNode } from "../utils/reactflowUtils";
|
|||
import useFlowsManagerStore from "./flowsManagerStore";
|
||||
import useAlertStore from "./alertStore";
|
||||
import { flowIOStoreType } from "../types/zustand/flowIOStore";
|
||||
import { buildVertices } from "../utils/buildUtils";
|
||||
/* const { getNodeId, saveFlow } = useContext(FlowsContext);
|
||||
const { setErrorData, setNoticeData } = useContext(alertContext); */
|
||||
|
||||
|
|
@ -22,6 +23,7 @@ const useFlowIOStore = create<flowIOStoreType>((set, get) => ({
|
|||
inputIds: [],
|
||||
outputIds: [],
|
||||
isBuilding: true,
|
||||
actualFlow: null,
|
||||
|
||||
setFilterEdge: (filterEdge) => { set({getFilterEdge: filterEdge}) },
|
||||
setFlowPool: (flowPool) => { set({flowPool}) },
|
||||
|
|
@ -214,29 +216,29 @@ const useFlowIOStore = create<flowIOStoreType>((set, get) => ({
|
|||
// simulate a click on the link element to trigger the download
|
||||
link.click();
|
||||
},
|
||||
/* buildFlow: async (nodeId?: string) => {
|
||||
buildFlow: async (nodeId?: string) => {
|
||||
function handleBuildUpdate(data: any) {
|
||||
get().addDataToFlowPool(data.data[data.id], data.id);
|
||||
}
|
||||
console.log(
|
||||
"building flow before save",
|
||||
JSON.parse(JSON.stringify(actualFlow))
|
||||
JSON.parse(JSON.stringify(get().actualFlow))
|
||||
);
|
||||
console.log(saveFlow);
|
||||
await saveFlow(
|
||||
{ ...actualFlow!, data: reactFlowInstance!.toObject()! },
|
||||
{ ...get().actualFlow!, data: reactFlowInstance!.toObject()! },
|
||||
true
|
||||
);
|
||||
console.log(
|
||||
"building flow AFTER save",
|
||||
JSON.parse(JSON.stringify(actualFlow))
|
||||
JSON.parse(JSON.stringify(get().actualFlow))
|
||||
);
|
||||
return buildVertices({
|
||||
flow: {
|
||||
data: reactFlowInstance?.toObject()!,
|
||||
description: actualFlow!.description,
|
||||
id: actualFlow!.id,
|
||||
name: actualFlow!.name,
|
||||
description: get().actualFlow!.description,
|
||||
id: get().actualFlow!.id,
|
||||
name: get().actualFlow!.name,
|
||||
},
|
||||
nodeId,
|
||||
onBuildComplete: () => {
|
||||
|
|
@ -249,7 +251,7 @@ const useFlowIOStore = create<flowIOStoreType>((set, get) => ({
|
|||
setErrorData({ list, title });
|
||||
},
|
||||
});
|
||||
}, */
|
||||
},
|
||||
}));
|
||||
|
||||
export default useFlowIOStore;
|
||||
|
|
|
|||
|
|
@ -128,3 +128,15 @@ export type Component = {
|
|||
data: Object;
|
||||
tags: [string];
|
||||
};
|
||||
|
||||
export type VerticesOrderTypeAPI = {
|
||||
ids: Array<string>;
|
||||
};
|
||||
|
||||
export type VertexBuildTypeAPI = {
|
||||
id: string;
|
||||
valid: boolean;
|
||||
params: string;
|
||||
results: { [key: string]: { [key: string]: string } };
|
||||
artifacts: { [key: string]: string };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,5 +8,24 @@ export type ChatMessageType = {
|
|||
thought?: string;
|
||||
files?: Array<{ data: string; type: string; data_type: string }>;
|
||||
prompt?: string;
|
||||
chatKey: string;
|
||||
chatKey?: string;
|
||||
sender_name?: string;
|
||||
};
|
||||
|
||||
export type ChatOutputType = {
|
||||
message: string;
|
||||
sender: string;
|
||||
sender_name: string;
|
||||
};
|
||||
|
||||
export type chatInputType = {
|
||||
result: string;
|
||||
};
|
||||
|
||||
export type FlowPoolObjectType = {
|
||||
timestamp: string;
|
||||
valid: boolean;
|
||||
params: any;
|
||||
data: { artifacts: any; results: any | ChatOutputType | chatInputType };
|
||||
id: string;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -444,7 +444,7 @@ export type chatInputType = {
|
|||
};
|
||||
lockChat: boolean;
|
||||
noInput: boolean;
|
||||
sendMessage: () => void;
|
||||
sendMessage: (count?:number) => void;
|
||||
setChatValue: (value: string) => void;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -7,12 +7,8 @@ export type FlowType = {
|
|||
data: ReactFlowJsonObject | null;
|
||||
description: string;
|
||||
style?: FlowStyleType;
|
||||
is_component: boolean;
|
||||
parent?: string;
|
||||
date_created?: string;
|
||||
updated_at?: string;
|
||||
last_tested_version?: string;
|
||||
};
|
||||
|
||||
export type NodeType = {
|
||||
id: string;
|
||||
type?: string;
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ export type FlowPoolType = {
|
|||
};
|
||||
|
||||
export type flowIOStoreType = {
|
||||
actualFlow: null | FlowType;
|
||||
flowPool: FlowPoolType;
|
||||
getFilterEdge: any[];
|
||||
isBuilt: boolean;
|
||||
|
|
@ -56,6 +57,5 @@ export type flowIOStoreType = {
|
|||
flowName: string,
|
||||
flowDescription?: string
|
||||
) => void;
|
||||
|
||||
/* buildFlow: (nodeId?:string) => Promise<void>; */
|
||||
buildFlow: (nodeId?:string) => Promise<void>;
|
||||
};
|
||||
81
src/frontend/src/utils/buildUtils.ts
Normal file
81
src/frontend/src/utils/buildUtils.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { AxiosError } from "axios";
|
||||
import { FlowType } from "../types/flow";
|
||||
import { getVerticesOrder, postBuildVertex } from "../controllers/API";
|
||||
|
||||
type BuildVerticesParams = {
|
||||
flow: FlowType; // Assuming FlowType is the type for your flow
|
||||
nodeId?: string | null; // Assuming nodeId is of type string, and it's optional
|
||||
onProgressUpdate?: (progress: number) => void; // Replace number with the actual type if it's not a number
|
||||
onBuildUpdate?: (data: any) => void; // Replace any with the actual type of data
|
||||
onBuildComplete?: (allNodesValid: boolean) => void;
|
||||
onBuildError?: (title, list) => void;
|
||||
};
|
||||
|
||||
export async function buildVertices({
|
||||
flow,
|
||||
nodeId = null,
|
||||
onProgressUpdate,
|
||||
onBuildUpdate,
|
||||
onBuildComplete,
|
||||
onBuildError,
|
||||
}: BuildVerticesParams) {
|
||||
try {
|
||||
// Step 1: Get vertices order
|
||||
console.log(JSON.parse(JSON.stringify(flow)));
|
||||
let orderResponse = await getVerticesOrder(flow.id);
|
||||
let verticesOrder = orderResponse.data.ids.flatMap((id) => id);
|
||||
// Determine the range of vertices to build
|
||||
let vertexIndex: number | null = null;
|
||||
if (nodeId) {
|
||||
vertexIndex = verticesOrder.indexOf(nodeId);
|
||||
}
|
||||
let buildRange =
|
||||
vertexIndex !== null
|
||||
? verticesOrder.slice(0, vertexIndex + 1)
|
||||
: verticesOrder;
|
||||
|
||||
const buildResults: boolean[] = [];
|
||||
for (let vertexId of buildRange) {
|
||||
try {
|
||||
const buildResponse = await postBuildVertex(flow, vertexId);
|
||||
const buildData = buildResponse.data;
|
||||
if (onProgressUpdate) {
|
||||
const progress =
|
||||
verticesOrder.indexOf(vertexId) / verticesOrder.length;
|
||||
onProgressUpdate(progress);
|
||||
}
|
||||
if (onBuildUpdate) {
|
||||
let data = {};
|
||||
if (!buildData.valid) {
|
||||
if (onBuildError) {
|
||||
onBuildError("Error Building Component", [buildData.params]);
|
||||
}
|
||||
}
|
||||
data[buildData.id] = buildData;
|
||||
|
||||
onBuildUpdate({ data, id: buildData.id });
|
||||
}
|
||||
buildResults.push(buildData.valid);
|
||||
} catch (error : any) {
|
||||
|
||||
if (onBuildError) {
|
||||
console.log(error)
|
||||
onBuildError("Error Building Component", [(error as AxiosError<any>).response?.data?.detail??"Unknown Error"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Callback for when all vertices have been built
|
||||
if (onBuildComplete) {
|
||||
const allNodesValid = buildResults.every((result) => result);
|
||||
onBuildComplete(allNodesValid);
|
||||
}
|
||||
} catch (error:any) {
|
||||
// Callback for handling errors
|
||||
if (onBuildError) {
|
||||
if (onBuildError) {
|
||||
onBuildError("Error Building Component", [(error as AxiosError<any>).response?.data?.detail??"Unknown Error"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -592,6 +592,11 @@ export function removeCountFromString(input: string): string {
|
|||
return result.trim(); // Trim any leading/trailing spaces
|
||||
}
|
||||
|
||||
export function extractTypeFromLongId(id: string): string {
|
||||
let [newId,_] = id.split("-");
|
||||
return newId;
|
||||
}
|
||||
|
||||
export function createRandomKey(key: string, uid: string): string {
|
||||
return removeCountFromString(key) + ` (${uid})`;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue