diff --git a/src/backend/langflow/api/v1/chat.py b/src/backend/langflow/api/v1/chat.py index f6f0eceb1..937eb2cf6 100644 --- a/src/backend/langflow/api/v1/chat.py +++ b/src/backend/langflow/api/v1/chat.py @@ -126,7 +126,7 @@ async def stream_build(flow_id: str): params = vertex._built_object_repr() valid = True logger.debug( - f"Building node {params[:50]}{'...' if len(params) > 50 else ''}" + f"Building node {str(params)[:50]}{'...' if len(str(params)) > 50 else ''}" ) if vertex.artifacts: # The artifacts will be prompt variables diff --git a/src/backend/langflow/graph/vertex/types.py b/src/backend/langflow/graph/vertex/types.py index 17481b8a0..effd00071 100644 --- a/src/backend/langflow/graph/vertex/types.py +++ b/src/backend/langflow/graph/vertex/types.py @@ -213,23 +213,27 @@ class PromptVertex(Vertex): def _built_object_repr(self): if ( - self.artifacts - and self._built_object is not None - and hasattr(self._built_object, "format") + not self.artifacts + or self._built_object is None + or not hasattr(self._built_object, "format") ): - # We'll build the prompt with the artifacts - # to show the user what the prompt looks like - # with the variables filled in - artifacts = self.artifacts.copy() - # Remove the handle_keys from the artifacts - # so the prompt format doesn't break - artifacts.pop("handle_keys", None) - try: - return self._built_object.format(**artifacts) - except KeyError: - return super()._built_object_repr() - else: return super()._built_object_repr() + # We'll build the prompt with the artifacts + # to show the user what the prompt looks like + # with the variables filled in + artifacts = self.artifacts.copy() + # Remove the handle_keys from the artifacts + # so the prompt format doesn't break + artifacts.pop("handle_keys", None) + try: + template = self._built_object.format(**artifacts) + return ( + template + if isinstance(template, str) + else f"{self.vertex_type}({template})" + ) + except KeyError: + return str(self._built_object) class OutputParserVertex(Vertex): diff --git a/src/backend/langflow/interface/initialize/loading.py b/src/backend/langflow/interface/initialize/loading.py index 5418f6906..ba617fe34 100644 --- a/src/backend/langflow/interface/initialize/loading.py +++ b/src/backend/langflow/interface/initialize/loading.py @@ -1,3 +1,4 @@ +import contextlib import json from typing import Any, Callable, Dict, List, Sequence, Type @@ -67,7 +68,11 @@ def instantiate_based_on_type(class_object, base_type, node_type, params): elif base_type == "prompts": return instantiate_prompt(node_type, class_object, params) elif base_type == "tools": - return instantiate_tool(node_type, class_object, params) + tool = instantiate_tool(node_type, class_object, params) + if hasattr(tool, "name") and isinstance(tool, BaseTool): + # tool name shouldn't contain spaces + tool.name = tool.name.replace(" ", "_") + return tool elif base_type == "toolkits": return instantiate_toolkit(node_type, class_object, params) elif base_type == "embeddings": @@ -132,6 +137,9 @@ def instantiate_llm(node_type, class_object, params: Dict): def instantiate_memory(node_type, class_object, params): # process input_key and output_key to remove them if # they are empty strings + if node_type == "ConversationEntityMemory": + params.pop("memory_key", None) + for key in ["input_key", "output_key"]: if key in params and (params[key] == "" or not params[key]): params.pop(key) @@ -196,7 +204,7 @@ def instantiate_prompt(node_type, class_object, params: Dict): if "tools" not in params: params["tools"] = [] return ZeroShotAgent.create_prompt(**params) - if "MessagePromptTemplate" in node_type: + elif "MessagePromptTemplate" in node_type: # Then we only need the template from_template_params = { "template": params.pop("prompt", params.pop("template", "")) @@ -204,12 +212,12 @@ def instantiate_prompt(node_type, class_object, params: Dict): if not from_template_params.get("template"): raise ValueError("Prompt template is required") - return class_object.from_template(**from_template_params) + prompt = class_object.from_template(**from_template_params) - if node_type == "ChatPromptTemplate": - return class_object.from_messages(**params) - - prompt = class_object(**params) + elif node_type == "ChatPromptTemplate": + prompt = class_object.from_messages(**params) + else: + prompt = class_object(**params) format_kwargs: Dict[str, Any] = {} for input_variable in prompt.input_variables: @@ -221,18 +229,23 @@ def instantiate_prompt(node_type, class_object, params: Dict): variable, "get_format_instructions" ): format_kwargs[input_variable] = variable.get_format_instructions() - # check if is a list of Document elif isinstance(variable, List) and all( isinstance(item, Document) for item in variable ): # Format document to contain page_content and metadata # as one string separated by a newline - format_kwargs[input_variable] = "\n".join( - [ - f"Document:{item.page_content}\nMetadata:{item.metadata}" - for item in variable - ] - ) + if len(variable) > 1: + content = "\n".join( + [item.page_content for item in variable if item.page_content] + ) + else: + content = variable[0].page_content + # content could be a json list of strings + with contextlib.suppress(json.JSONDecodeError): + content = json.loads(content) + if isinstance(content, list): + content = ",".join([str(item) for item in content]) + format_kwargs[input_variable] = content # handle_keys will be a list but it does not exist yet # so we need to create it diff --git a/src/backend/langflow/processing/base.py b/src/backend/langflow/processing/base.py index e8880c34e..f8690bbdf 100644 --- a/src/backend/langflow/processing/base.py +++ b/src/backend/langflow/processing/base.py @@ -19,8 +19,11 @@ async def get_result_and_steps(langchain_object, inputs: Union[dict, str], **kwa # Deactivating until we have a frontend solution # to display intermediate steps langchain_object.return_intermediate_steps = True + try: + fix_memory_inputs(langchain_object) + except Exception as exc: + logger.error(exc) - fix_memory_inputs(langchain_object) try: async_callbacks = [AsyncStreamingLLMCallbackHandler(**kwargs)] output = await langchain_object.acall(inputs, callbacks=async_callbacks) diff --git a/src/backend/langflow/processing/process.py b/src/backend/langflow/processing/process.py index d4d7b8a26..228a2836d 100644 --- a/src/backend/langflow/processing/process.py +++ b/src/backend/langflow/processing/process.py @@ -22,7 +22,10 @@ def fix_memory_inputs(langchain_object): if not hasattr(langchain_object, "memory") or langchain_object.memory is None: return try: - if langchain_object.memory.memory_key in langchain_object.input_variables: + if ( + hasattr(langchain_object.memory, "memory_key") + and langchain_object.memory.memory_key in langchain_object.input_variables + ): return except AttributeError: input_variables = ( diff --git a/src/backend/langflow/template/frontend_node/chains.py b/src/backend/langflow/template/frontend_node/chains.py index ce8c1c62c..b678dec3b 100644 --- a/src/backend/langflow/template/frontend_node/chains.py +++ b/src/backend/langflow/template/frontend_node/chains.py @@ -81,7 +81,7 @@ class ChainFrontendNode(FrontendNode): field.advanced = False if field.name == "verbose": field.required = False - field.show = True + field.show = False field.advanced = True if field.name == "llm": field.required = True diff --git a/src/backend/langflow/template/frontend_node/memories.py b/src/backend/langflow/template/frontend_node/memories.py index f84c18751..374d36ff0 100644 --- a/src/backend/langflow/template/frontend_node/memories.py +++ b/src/backend/langflow/template/frontend_node/memories.py @@ -90,6 +90,9 @@ class MemoryFrontendNode(FrontendNode): field.show = True if field.name == "entity_store": field.show = False + if name == "ConversationEntityMemory" and field.name == "memory_key": + field.show = False + field.required = False class PostgresChatMessageHistoryFrontendNode(MemoryFrontendNode): diff --git a/src/backend/langflow/template/frontend_node/tools.py b/src/backend/langflow/template/frontend_node/tools.py index 6b6903fdc..ece765ed7 100644 --- a/src/backend/langflow/template/frontend_node/tools.py +++ b/src/backend/langflow/template/frontend_node/tools.py @@ -109,7 +109,7 @@ class PythonFunctionToolNode(FrontendNode): ], ) description: str = "Python function to be executed." - base_classes: list[str] = ["Tool"] + base_classes: list[str] = ["BaseTool", "Tool"] def to_dict(self): return super().to_dict() diff --git a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx index ed6cd1f7a..a8d99ae0b 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx @@ -16,6 +16,7 @@ import { PopUpContext } from "../../../../contexts/popUpContext"; import { TabsContext } from "../../../../contexts/tabsContext"; import { typesContext } from "../../../../contexts/typesContext"; import { ParameterComponentType } from "../../../../types/components"; +import { cleanEdges } from "../../../../util/reactflowUtils"; import { classNames, getRandomKeyByssmm, @@ -25,6 +26,7 @@ import { nodeIconsLucide, nodeNames, } from "../../../../utils"; +import { MAX_LENGTH_TO_SCROLL_TOOLTIP } from "../../../../constants"; export default function ParameterComponent({ left, @@ -96,8 +98,9 @@ export default function ParameterComponent({ useEffect(() => { const groupedObj = groupByFamily(myData, tooltipTitle, left, data.type); - refNumberComponents.current = groupedObj[0]?.component?.length; - + refNumberComponents.current = groupedObj[0]?.type?.length; + console.log(refNumberComponents); + refHtml.current = groupedObj.map((item, i) => { const Icon: any = nodeIconsLucide[item.family]; @@ -180,7 +183,7 @@ export default function ParameterComponent({ <> ) : ( 10 ? "tooltip-fixed-width custom-scroll overflow-y-scroll nowheel" : "tooltip-fixed-width"} + styleClasses={refNumberComponents.current > MAX_LENGTH_TO_SCROLL_TOOLTIP ? "tooltip-fixed-width custom-scroll overflow-y-scroll nowheel" : "tooltip-fixed-width"} delayDuration={0} content={refHtml.current} side={left ? "left" : "right"} @@ -306,6 +309,13 @@ export default function ParameterComponent({ field_name={name} setNodeClass={(nodeClass) => { data.node = nodeClass; + cleanEdges({ + flow: { + edges: reactFlowInstance.getEdges(), + nodes: reactFlowInstance.getNodes(), + }, + updateEdge: (edge) => reactFlowInstance.setEdges(edge), + }); }} nodeClass={data.node} disabled={disabled} diff --git a/src/frontend/src/components/ShadTooltipComponent/index.tsx b/src/frontend/src/components/ShadTooltipComponent/index.tsx index 58aec630b..89f301e60 100644 --- a/src/frontend/src/components/ShadTooltipComponent/index.tsx +++ b/src/frontend/src/components/ShadTooltipComponent/index.tsx @@ -6,7 +6,7 @@ export default function ShadTooltip({ side, asChild = true, children, - style, + styleClasses, delayDuration = 500, }: ShadToolTipType) { return ( @@ -14,7 +14,7 @@ export default function ShadTooltip({ {children} diff --git a/src/frontend/src/components/codeAreaComponent/index.tsx b/src/frontend/src/components/codeAreaComponent/index.tsx index 2e0893321..966d8f26d 100644 --- a/src/frontend/src/components/codeAreaComponent/index.tsx +++ b/src/frontend/src/components/codeAreaComponent/index.tsx @@ -48,8 +48,8 @@ export default function CodeAreaComponent({ className={ editNode ? "input-edit-node input-dialog" - : (disabled ? " input-disable " : "") + - " input-dialog input-primary" + : (disabled ? " input-disable input-ring " : "") + + " input-primary text-muted-foreground " } > {myValue !== "" ? myValue : "Type something..."} diff --git a/src/frontend/src/components/promptComponent/index.tsx b/src/frontend/src/components/promptComponent/index.tsx index 4b4f9c1e1..4f9a56ad0 100644 --- a/src/frontend/src/components/promptComponent/index.tsx +++ b/src/frontend/src/components/promptComponent/index.tsx @@ -44,7 +44,7 @@ export default function PromptAreaComponent({ }) .catch((error) => {}); } - }, [reactFlowInstance, field_name, myValue, nodeClass, setNodeClass]); + }, [reactFlowInstance, field_name, myValue, nodeClass, setNodeClass, value]); return (
@@ -69,8 +69,8 @@ export default function PromptAreaComponent({ className={ editNode ? "input-edit-node input-dialog" - : (disabled ? " input-disable " : "") + - " input-dialog input-primary" + : (disabled ? " input-disable text-ring " : "") + + " input-primary text-muted-foreground " } > {myValue !== "" ? myValue : "Type your prompt here"} diff --git a/src/frontend/src/components/ui/tooltip.tsx b/src/frontend/src/components/ui/tooltip.tsx index dc847052e..6bb983652 100644 --- a/src/frontend/src/components/ui/tooltip.tsx +++ b/src/frontend/src/components/ui/tooltip.tsx @@ -14,15 +14,17 @@ const TooltipContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, sideOffset = 4, ...props }, ref) => ( - + + + )); TooltipContent.displayName = TooltipPrimitive.Content.displayName; diff --git a/src/frontend/src/constants.tsx b/src/frontend/src/constants.tsx index e6c1dcd1f..25c0dc7d5 100644 --- a/src/frontend/src/constants.tsx +++ b/src/frontend/src/constants.tsx @@ -5,6 +5,12 @@ import { FlowType } from "./types/flow"; import { TabsState } from "./types/tabs"; import { buildInputs, buildTweaks } from "./utils"; +/** + * Number maximum of components to scroll on tooltips + * @constant + */ +export const MAX_LENGTH_TO_SCROLL_TOOLTIP = 200; + /** * The base text for subtitle of Export Dialog (Toolbar) * @constant diff --git a/src/frontend/src/index.css b/src/frontend/src/index.css index de0839761..5af8ff70a 100644 --- a/src/frontend/src/index.css +++ b/src/frontend/src/index.css @@ -6,8 +6,8 @@ /* TODO: Confirm that all colors here are found in tailwind config */ @layer base { - -:root { + + :root { --background: 0 0% 100%; /* hsl(0 0% 100%) */ --foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */ --muted: 210 40% 98%; /* hsl(210 40% 98%) */ @@ -28,21 +28,24 @@ --destructive-foreground: 210 40% 98%; /* hsl(210 40% 98%) */ --radius: 0.5rem; --ring: 215 20.2% 65.1%; /* hsl(215 20% 65%) */ - + --round-btn-shadow: #00000063; - + --error-background: #fef2f2; --error-foreground: #991b1b; - + --success-background: #f0fdf4; --success-foreground: #14532d; --info-background: #f0f4fd; --info-foreground: #141653; - + --high-indigo: #4338ca; --medium-indigo: #6366f1; + --chat-bot-icon: #afe6ef; + --chat-user-icon: #aface9; + /* Colors that are shared in dark and light mode */ --blur-shared: #151923de; --build-trigger: #dc735b; @@ -213,7 +216,7 @@ The cursor: default; property value restores the browser's default cursor style @apply input-primary pr-7 mx-2 } .input-disable{ - @apply bg-input text-muted-foreground + @apply bg-border placeholder:text-ring border-transparent } .input-dialog{ @apply text-ring cursor-pointer bg-transparent @@ -915,13 +918,16 @@ The cursor: default; property value restores the browser's default cursor style } .form-modal-lock-true { - @apply bg-input text-black dark:bg-gray-700 dark:text-gray-300 + @apply bg-input text-black + } + .form-modal-no-input { + @apply bg-input text-center text-black dark:bg-gray-700 dark:text-gray-300 } .form-modal-lock-false { - @apply bg-white text-black dark:bg-gray-900 dark:text-gray-300 + @apply bg-white text-black } .form-modal-lockchat { - @apply form-input block w-full rounded-md border-gray-300 p-4 pr-16 custom-scroll dark:border-gray-600 sm:text-sm + @apply form-input focus:ring-ring focus:border-ring block w-full rounded-md border-border p-4 pr-16 custom-scroll sm:text-sm } .form-modal-send-icon-position { @apply absolute bottom-2 right-4 @@ -935,6 +941,9 @@ The cursor: default; property value restores the browser's default cursor style .form-modal-send-icon { @apply mr-2 h-5 w-5 rotate-[44deg] } + .form-modal-play-icon { + @apply h-5 w-5 mx-1 + } .form-modal-chat-position { @apply flex-max-width px-2 py-6 pl-4 pr-9 } @@ -948,10 +957,10 @@ The cursor: default; property value restores the browser's default cursor style @apply relative flex h-8 w-8 items-center justify-center overflow-hidden rounded-md p-5 text-2xl } .form-modal-chat-bot-icon { - @apply form-modal-chat-img-box bg-[#afe6ef] + @apply form-modal-chat-img-box bg-chat-bot-icon } .form-modal-chat-user-icon { - @apply form-modal-chat-img-box bg-[#aface9] + @apply form-modal-chat-img-box bg-chat-user-icon } .form-modal-chat-icon-img { @apply absolute scale-[60%] @@ -966,7 +975,7 @@ The cursor: default; property value restores the browser's default cursor style @apply absolute -left-6 -top-3 cursor-pointer } .form-modal-chat-icon { - @apply h-4 w-4 animate-bounce dark:text-white + @apply h-4 w-4 animate-bounce } .form-modal-chat-thought-border { @apply rounded-md border border-ring/60 @@ -1035,13 +1044,13 @@ The cursor: default; property value restores the browser's default cursor style @apply flex-max-width h-full flex-col items-center justify-center text-center align-middle } .langflow-chat-span { - @apply text-lg text-gray-600 dark:text-gray-300 + @apply text-lg text-foreground } .langflow-chat-desc { - @apply w-2/4 rounded-md border border-gray-200 bg-muted px-6 py-8 dark:border-gray-700 dark:bg-gray-900 + @apply w-2/4 rounded-md border border-border bg-muted px-6 py-8 } .langflow-chat-desc-span { - @apply text-base text-gray-500 + @apply text-base text-muted-foreground } .langflow-chat-input-div { @apply flex-max-width flex-col items-center justify-between px-8 pb-6 @@ -1054,4 +1063,4 @@ The cursor: default; property value restores the browser's default cursor style @apply max-w-[30vw] max-h-[20vh] overflow-auto } -} \ No newline at end of file +} diff --git a/src/frontend/src/modals/chatModal/chatMessage/index.tsx b/src/frontend/src/modals/chatModal/chatMessage/index.tsx deleted file mode 100644 index 2411f2103..000000000 --- a/src/frontend/src/modals/chatModal/chatMessage/index.tsx +++ /dev/null @@ -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 ( -
-
- {!chat.isSend && ( -
- - -
- )} - {chat.isSend && } -
- {!chat.isSend ? ( -
-
- {hidden && chat.thought && chat.thought !== "" && ( -
setHidden((prev) => !prev)} - className="chat-message-modal-icon-div" - > - -
- )} - {chat.thought && chat.thought !== "" && !hidden && ( - setHidden((prev) => !prev)} - /> - )} - {chat.thought && chat.thought !== "" && !hidden &&

} -
-
-
- - ▍ - - ); - } - - children[0] = (children[0] as string).replace( - "`▍`", - "▍" - ); - } - - const match = /language-(\w+)/.exec(className || ""); - - return !inline ? ( - - ) : ( - - {children} - - ); - }, - }} - > - {message} - -
- {chat.files && ( -
- {chat.files.map((file, index) => { - return ( -
- -
- ); - })} -
- )} -
-
-
-
- ) : ( -
-
- {message.split("\n").map((line, index) => ( - - {line} -
-
- ))} -
-
- )} -
- ); -} diff --git a/src/frontend/src/modals/chatModal/index.tsx b/src/frontend/src/modals/chatModal/index.tsx deleted file mode 100644 index 90baca4df..000000000 --- a/src/frontend/src/modals/chatModal/index.tsx +++ /dev/null @@ -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([]); - const { reactFlowInstance } = useContext(typesContext); - const { setErrorData } = useContext(alertContext); - const ws = useRef(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 - ) => { - 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; - }) { - 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; - }) => { - 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 ( - - - -
- - -
-
- - -
- - -
-
- {chatHistory.length > 0 ? ( - chatHistory.map((c, i) => ( - - )) - ) : ( -
- - πŸ‘‹{" "} - - Langflow Chat - - -
-
- - Start a conversation and click the agent’s thoughts{" "} - - - {" "} - to inspect the chaining process. - -
-
- )} -
-
-
-
- -
-
-
-
-
-
-
-
- ); -} diff --git a/src/frontend/src/modals/codeAreaModal/index.tsx b/src/frontend/src/modals/codeAreaModal/index.tsx index f79fb5d4f..b38bef5f2 100644 --- a/src/frontend/src/modals/codeAreaModal/index.tsx +++ b/src/frontend/src/modals/codeAreaModal/index.tsx @@ -5,7 +5,7 @@ import "ace-builds/src-noconflict/mode-python"; import "ace-builds/src-noconflict/theme-github"; import "ace-builds/src-noconflict/theme-twilight"; import { TerminalSquare } from "lucide-react"; -import { useContext, useEffect, useRef, useState } from "react"; +import { useContext, useState } from "react"; import AceEditor from "react-ace"; import { Button } from "../../components/ui/button"; import { CODE_PROMPT_DIALOG_SUBTITLE } from "../../constants"; @@ -27,19 +27,10 @@ export default function CodeAreaModal({ nodeClass: APIClassType; setNodeClass: (Class: APIClassType) => void; }) { - const [open, setOpen] = useState(true); const [code, setCode] = useState(value); - const [loading, setLoading] = useState(false); const { dark } = useContext(darkContext); const { closePopUp, setCloseEdit } = useContext(PopUpContext); const { setErrorData, setSuccessData } = useContext(alertContext); - const [error, setError] = useState<{ - detail: { error: string; traceback: string }; - }>(null); - const ref = useRef(); - useEffect(() => { - setValue(code); - }, [code, setValue]); function setModalOpen(x: boolean) { if (x === false) { @@ -49,10 +40,8 @@ export default function CodeAreaModal({ } function handleClick() { - setLoading(true); postValidateCode(code) .then((apiReturn) => { - setLoading(false); if (apiReturn.data) { let importsErrors = apiReturn.data.imports.errors; let funcErrors = apiReturn.data.function.errors; @@ -61,7 +50,6 @@ export default function CodeAreaModal({ title: "Code is ready to run", }); setValue(code); - setOpen((old) => !old); setModalOpen(false); } else { if (funcErrors.length !== 0) { @@ -84,7 +72,6 @@ export default function CodeAreaModal({ } }) .catch((_) => { - setLoading(false); setErrorData({ title: "There is something wrong with this code, please review it", }); @@ -92,7 +79,7 @@ export default function CodeAreaModal({ } return ( - + Edit Code @@ -122,23 +109,6 @@ export default function CodeAreaModal({ className="h-full w-full rounded-lg border-[1px] border-gray-300 custom-scroll dark:border-gray-600" />
-
-
-

- {error?.detail?.error} -

-
-
-                  {error?.detail?.traceback}
-                
-
-
-
= memo(({ language, value }) => { ); -}); +} CodeBlock.displayName = "CodeBlock"; diff --git a/src/frontend/src/modals/formModal/chatMessage/index.tsx b/src/frontend/src/modals/formModal/chatMessage/index.tsx index 6e7457549..8b5453607 100644 --- a/src/frontend/src/modals/formModal/chatMessage/index.tsx +++ b/src/frontend/src/modals/formModal/chatMessage/index.tsx @@ -1,6 +1,6 @@ import Convert from "ansi-to-html"; import { ChevronDown } from "lucide-react"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import ReactMarkdown from "react-markdown"; import rehypeMathjax from "rehype-mathjax"; import remarkGfm from "remark-gfm"; @@ -24,7 +24,7 @@ export default function ChatMessage({ }) { const convert = new Convert({ newline: true }); const [hidden, setHidden] = useState(true); - const [template, setTemplate] = useState(chat.template); + const template = chat.template; const [promptOpen, setPromptOpen] = useState(false); return (
- - ▍ - + components={{ + code: ({ + node, + inline, + className, + children, + ...props + }) => { + if (children.length) { + if (children[0] === "▍") { + return ( + + ▍ + + ); + } + + children[0] = (children[0] as string).replace( + "`▍`", + "▍" + ); + } + + const match = /language-(\w+)/.exec( + className || "" ); - } - children[0] = (children[0] as string).replace( - "`▍`", - "▍" - ); - } - - const match = /language-(\w+)/.exec(className || ""); - - return !inline ? ( - - ) : ( - - {children} - - ); - }, - }} - > - {chat.message.toString()} - + return !inline ? ( + + ) : ( + + {children} + + ); + }, + }} + > + {chat.message.toString()} + + ), + [chat.message, chat.message.toString()] + )}
{chat.files && (
@@ -138,51 +151,56 @@ export default function ChatMessage({
) : (
- - - {promptOpen - ? template?.split("\n")?.map((line, index) => { - const regex = /{([^}]+)}/g; - let match; - let parts = []; - 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( - - {chat.message[match[1]]} - - ); - } + {template && ( + <> + + + {promptOpen + ? template?.split("\n")?.map((line, index) => { + const regex = /{([^}]+)}/g; + let match; + let parts = []; + 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( + + {chat.message[match[1]]} + + ); + } - // Update last index - lastIndex = regex.lastIndex; - } - // Push text after the last match - if (lastIndex !== line.length) { - parts.push(line.substring(lastIndex)); - } - return

{parts}

; - }) - : chat.message[chat.chatKey]} -
+ // Update last index + lastIndex = regex.lastIndex; + } + // Push text after the last match + if (lastIndex !== line.length) { + parts.push(line.substring(lastIndex)); + } + return

{parts}

; + }) + : chat.message[chat.chatKey]} +
+ + )} + {chat.message[chat.chatKey]}
)}
diff --git a/src/frontend/src/modals/formModal/index.tsx b/src/frontend/src/modals/formModal/index.tsx index 7a5dae1a6..d0a73f941 100644 --- a/src/frontend/src/modals/formModal/index.tsx +++ b/src/frontend/src/modals/formModal/index.tsx @@ -292,7 +292,7 @@ export default function FormModal({ ws.current.close(); } }; - }, []); + }, [connectWS]); useEffect(() => { if ( @@ -303,7 +303,7 @@ export default function FormModal({ connectWS(); setLockChat(false); } - }, [lockChat]); + }, [lockChat, connectWS]); async function sendAll(data: sendAllProps) { try { @@ -333,41 +333,35 @@ export default function FormModal({ }, [open]); function sendMessage() { - if (chatValue !== "") { - let nodeValidationErrors = validateNodes(reactFlowInstance); - if (nodeValidationErrors.length === 0) { - setLockChat(true); - let inputs = tabsState[id.current].formKeysData.input_keys; - setChatValue(""); - const message = inputs; - addChatHistory( - message, - true, - chatKey, - tabsState[flow.id].formKeysData.template - ); - sendAll({ - ...reactFlowInstance.toObject(), - inputs: inputs, - chatHistory, - name: flow.name, - description: flow.description, - }); - setTabsState((old) => { - let newTabsState = _.cloneDeep(old); - newTabsState[id.current].formKeysData.input_keys[chatKey] = ""; - return newTabsState; - }); - } else { - setErrorData({ - title: "Oops! Looks like you missed some required information:", - list: nodeValidationErrors, - }); - } + let nodeValidationErrors = validateNodes(reactFlowInstance); + if (nodeValidationErrors.length === 0) { + setLockChat(true); + let inputs = tabsState[id.current].formKeysData.input_keys; + setChatValue(""); + const message = inputs; + addChatHistory( + message, + true, + chatKey, + tabsState[flow.id].formKeysData.template + ); + sendAll({ + ...reactFlowInstance.toObject(), + inputs: inputs, + chatHistory, + name: flow.name, + description: flow.description, + }); + setTabsState((old) => { + if (!chatKey) return old; + let newTabsState = _.cloneDeep(old); + newTabsState[id.current].formKeysData.input_keys[chatKey] = ""; + return newTabsState; + }); } else { setErrorData({ - title: "Error sending message", - list: ["The message cannot be empty."], + title: "Oops! Looks like you missed some required information:", + list: nodeValidationErrors, }); } } @@ -549,13 +543,12 @@ export default function FormModal({
{ setChatValue(value); setTabsState((old) => { - // chatKey is undefined when there are no chat input keys - if (!chatKey) return old; let newTabsState = _.cloneDeep(old); newTabsState[id.current].formKeysData.input_keys[ chatKey diff --git a/src/frontend/src/modals/genericModal/index.tsx b/src/frontend/src/modals/genericModal/index.tsx index 179bef869..923549e28 100644 --- a/src/frontend/src/modals/genericModal/index.tsx +++ b/src/frontend/src/modals/genericModal/index.tsx @@ -57,7 +57,6 @@ export default function GenericModal({ const [myModalType] = useState(type); const [inputValue, setInputValue] = useState(value); const [isEdit, setIsEdit] = useState(true); - const [wordsHighlightInvalid, setWordsHighlightInvalid] = useState([]); const [wordsHighlight, setWordsHighlight] = useState([]); const { dark } = useContext(darkContext); const { setErrorData, setSuccessData, setNoticeData } = @@ -102,15 +101,14 @@ export default function GenericModal({ (word) => !invalid_chars.includes(word) ); - setWordsHighlightInvalid(invalid_chars); setWordsHighlight(filteredWordsHighlight); } useEffect(() => { - if (type == TypeModal.PROMPT && inputValue && inputValue != "") { + if (type === TypeModal.PROMPT && inputValue && inputValue != "") { checkVariables(inputValue); } - }, []); + }, [inputValue, type]); const coloredContent = (inputValue || "") .replace(/ { if (apiReturn.data) { - setNodeClass(apiReturn.data.frontend_node); - - let inputVariables = apiReturn.data.input_variables; - if (inputVariables.length === 0) { + setNodeClass(apiReturn.data?.frontend_node); + let inputVariables = apiReturn.data.input_variables ?? []; + if (inputVariables && inputVariables.length === 0) { setIsEdit(true); setNoticeData({ title: "Your template does not have any variables.", @@ -159,6 +156,7 @@ export default function GenericModal({ } }) .catch((error) => { + console.log(error); setIsEdit(true); return setErrorData({ title: "There is something wrong with this prompt, please review it", diff --git a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx index 65ae72178..23033d174 100644 --- a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx @@ -288,7 +288,7 @@ export default function Page({ flow }: { flow: FlowType }) { } }, // Specify dependencies for useCallback - [getNodeId, reactFlowInstance, setErrorData, setNodes, takeSnapshot] + [getNodeId, reactFlowInstance, setNodes, takeSnapshot] ); useEffect(() => { @@ -322,7 +322,7 @@ export default function Page({ flow }: { flow: FlowType }) { setEdges((els) => updateEdge(oldEdge, newConnection, els)); } }, - [] + [reactFlowInstance, setEdges] ); const onEdgeUpdateEnd = useCallback((_, edge) => { diff --git a/src/frontend/src/types/components/index.ts b/src/frontend/src/types/components/index.ts index 56269627e..4ce9e526f 100644 --- a/src/frontend/src/types/components/index.ts +++ b/src/frontend/src/types/components/index.ts @@ -147,7 +147,7 @@ export type ShadToolTipType = { asChild?: boolean; children?: ReactElement; delayDuration?: number; - style?: string; + styleClasses?: string; }; export type TextHighlightType = { diff --git a/src/frontend/src/types/utils/reactflowUtils.ts b/src/frontend/src/types/utils/reactflowUtils.ts new file mode 100644 index 000000000..ecbbda4e4 --- /dev/null +++ b/src/frontend/src/types/utils/reactflowUtils.ts @@ -0,0 +1,10 @@ +import { Edge } from "reactflow"; +import { NodeType } from "../flow"; + +export type cleanEdgesType = { + flow: { + edges: Edge[]; + nodes: NodeType[]; + }; + updateEdge: (edge: Edge[]) => void; +}; diff --git a/src/frontend/src/util/reactflowUtils.ts b/src/frontend/src/util/reactflowUtils.ts new file mode 100644 index 000000000..da04cc75c --- /dev/null +++ b/src/frontend/src/util/reactflowUtils.ts @@ -0,0 +1,46 @@ +import _ from "lodash"; +import { cleanEdgesType } from "./../types/utils/reactflowUtils"; + +export function cleanEdges({ + flow: { edges, nodes }, + updateEdge, +}: cleanEdgesType) { + let newEdges = _.cloneDeep(edges); + edges.forEach((edge) => { + // check if the source and target node still exists + const sourceNode = nodes.find((node) => node.id === edge.source); + const targetNode = nodes.find((node) => node.id === edge.target); + if (!sourceNode || !targetNode) { + newEdges = newEdges.filter((e) => e.id !== edge.id); + } + // check if the source and target handle still exists + if (sourceNode && targetNode) { + const sourceHandle = edge.sourceHandle; //right + const targetHandle = edge.targetHandle; //left + if (targetHandle) { + const field = targetHandle.split("|")[1]; + const id = + (targetNode.data.node.template[field]?.input_types?.join(";") ?? + targetNode.data.node.template[field]?.type) + + "|" + + field + + "|" + + targetNode.data.id; + if (id !== targetHandle) { + newEdges = newEdges.filter((e) => e.id !== edge.id); + } + } + if (sourceHandle) { + const id = [ + sourceNode.data.type, + sourceNode.data.id, + ...sourceNode.data.node.base_classes, + ].join("|"); + if (id !== sourceHandle) { + newEdges = newEdges.filter((e) => e.id !== edge.id); + } + } + } + }); + updateEdge(newEdges); +} diff --git a/src/frontend/tailwind.config.js b/src/frontend/tailwind.config.js index b3d296560..739308b92 100644 --- a/src/frontend/tailwind.config.js +++ b/src/frontend/tailwind.config.js @@ -82,6 +82,8 @@ module.exports = { "status-yellow": "var(--status-yellow)", "success-background": "var(--success-background)", "success-foreground": "var(--success-foreground)", + "chat-bot-icon": "var(--chat-bot-icon)", + "chat-user-icon": "var(--chat-user-icon)", white: "var(--white)", border: "hsl(var(--border))",