From 12a753d6b6e77fe3508cc23164dbb54985672862 Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Wed, 4 Dec 2024 17:33:22 -0300 Subject: [PATCH] fix: update drag and drop test (#5048) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: Improve wait time for modal in drag and drop test * ✨ (dragAndDrop.spec.ts): update file path to read collection.json from the correct location 🐛 (dragAndDrop.spec.ts): increase timeout for waiting for "uploaded successfully" text to prevent test failures * ✨ (dragAndDrop.spec.ts): Add tests for drag and drop functionality to ensure users can drag and drop collections and flows without crashing the application 📝 (simulate-drag-and-drop.ts): Add utility function to simulate drag and drop events in tests for improved test coverage and functionality * add debouncing time on file upload * [autofix.ci] apply automated fixes --------- Co-authored-by: cristhianzl Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .../src/hooks/flows/use-upload-flow.ts | 1 - .../pages/MainPage/hooks/use-on-file-drop.tsx | 62 +- .../tests/assets/flow_test_drag_and_drop.json | 1094 +++++++++++++++++ .../extended/features/dragAndDrop.spec.ts | 107 +- .../tests/utils/simulate-drag-and-drop.ts | 31 + 5 files changed, 1246 insertions(+), 49 deletions(-) create mode 100644 src/frontend/tests/assets/flow_test_drag_and_drop.json create mode 100644 src/frontend/tests/utils/simulate-drag-and-drop.ts diff --git a/src/frontend/src/hooks/flows/use-upload-flow.ts b/src/frontend/src/hooks/flows/use-upload-flow.ts index b31f39f39..9d7e47a88 100644 --- a/src/frontend/src/hooks/flows/use-upload-flow.ts +++ b/src/frontend/src/hooks/flows/use-upload-flow.ts @@ -9,7 +9,6 @@ import useAddFlow from "./use-add-flow"; const useUploadFlow = () => { const addFlow = useAddFlow(); const paste = useFlowStore((state) => state.paste); - const { mutate: refreshFlows } = useGetRefreshFlows(); const getFlowsFromFiles = async ({ files, diff --git a/src/frontend/src/pages/MainPage/hooks/use-on-file-drop.tsx b/src/frontend/src/pages/MainPage/hooks/use-on-file-drop.tsx index 6d583c500..2b19761ec 100644 --- a/src/frontend/src/pages/MainPage/hooks/use-on-file-drop.tsx +++ b/src/frontend/src/pages/MainPage/hooks/use-on-file-drop.tsx @@ -1,4 +1,5 @@ import useUploadFlow from "@/hooks/flows/use-upload-flow"; +import { useCallback, useRef } from "react"; import { CONSOLE_ERROR_MSG } from "../../../constants/alerts_constants"; import useAlertStore from "../../../stores/alertStore"; @@ -7,29 +8,44 @@ const useFileDrop = (type?: string) => { const setErrorData = useAlertStore((state) => state.setErrorData); const uploadFlow = useUploadFlow(); - const handleFileDrop = (e) => { - e.preventDefault(); - if (e.dataTransfer.types.every((type) => type === "Files")) { - const files: File[] = Array.from(e.dataTransfer.files); - uploadFlow({ - files, - isComponent: - type === "component" ? true : type === "flow" ? false : undefined, - }) - .then(() => { - setSuccessData({ - title: `All files uploaded successfully`, - }); - }) - .catch((error) => { - console.log(error); - setErrorData({ - title: CONSOLE_ERROR_MSG, - list: [(error as Error).message], - }); - }); - } - }; + const lastUploadTime = useRef(0); + const DEBOUNCE_INTERVAL = 1000; + + const handleFileDrop = useCallback( + (e) => { + e.preventDefault(); + + if (e.dataTransfer.types.every((type) => type === "Files")) { + const currentTime = Date.now(); + + if (currentTime - lastUploadTime.current >= DEBOUNCE_INTERVAL) { + lastUploadTime.current = currentTime; + + const files: File[] = Array.from(e.dataTransfer.files); + + uploadFlow({ + files, + isComponent: + type === "component" ? true : type === "flow" ? false : undefined, + }) + .then(() => { + setSuccessData({ + title: `All files uploaded successfully`, + }); + }) + .catch((error) => { + console.log(error); + setErrorData({ + title: CONSOLE_ERROR_MSG, + list: [(error as Error).message], + }); + }); + } + } + }, + [type, uploadFlow, setSuccessData, setErrorData], + ); + return handleFileDrop; }; diff --git a/src/frontend/tests/assets/flow_test_drag_and_drop.json b/src/frontend/tests/assets/flow_test_drag_and_drop.json new file mode 100644 index 000000000..785c02b99 --- /dev/null +++ b/src/frontend/tests/assets/flow_test_drag_and_drop.json @@ -0,0 +1,1094 @@ +{ + "id": "15f86014-8d1f-43fe-8452-133cfb543792", + "data": { + "edges": [ + { + "animated": false, + "className": "", + "data": { + "sourceHandle": { + "dataType": "Prompt", + "id": "Prompt-Smoiz", + "name": "prompt", + "output_types": ["Message"] + }, + "targetHandle": { + "fieldName": "system_message", + "id": "OpenAIModel-h60Oi", + "inputTypes": ["Message"], + "type": "str" + } + }, + "id": "reactflow__edge-Prompt-Smoiz{œdataTypeœ:œPromptœ,œidœ:œPrompt-Smoizœ,œnameœ:œpromptœ,œoutput_typesœ:[œMessageœ]}-OpenAIModel-h60Oi{œfieldNameœ:œsystem_messageœ,œidœ:œOpenAIModel-h60Oiœ,œinputTypesœ:[œMessageœ],œtypeœ:œstrœ}", + "source": "Prompt-Smoiz", + "sourceHandle": "{œdataTypeœ:œPromptœ,œidœ:œPrompt-Smoizœ,œnameœ:œpromptœ,œoutput_typesœ:[œMessageœ]}", + "target": "OpenAIModel-h60Oi", + "targetHandle": "{œfieldNameœ:œsystem_messageœ,œidœ:œOpenAIModel-h60Oiœ,œinputTypesœ:[œMessageœ],œtypeœ:œstrœ}", + "selected": false + }, + { + "animated": false, + "className": "", + "data": { + "sourceHandle": { + "dataType": "ChatInput", + "id": "ChatInput-Jd1q1", + "name": "message", + "output_types": ["Message"] + }, + "targetHandle": { + "fieldName": "input_value", + "id": "OpenAIModel-h60Oi", + "inputTypes": ["Message"], + "type": "str" + } + }, + "id": "reactflow__edge-ChatInput-Jd1q1{œdataTypeœ:œChatInputœ,œidœ:œChatInput-Jd1q1œ,œnameœ:œmessageœ,œoutput_typesœ:[œMessageœ]}-OpenAIModel-h60Oi{œfieldNameœ:œinput_valueœ,œidœ:œOpenAIModel-h60Oiœ,œinputTypesœ:[œMessageœ],œtypeœ:œstrœ}", + "source": "ChatInput-Jd1q1", + "sourceHandle": "{œdataTypeœ:œChatInputœ,œidœ:œChatInput-Jd1q1œ,œnameœ:œmessageœ,œoutput_typesœ:[œMessageœ]}", + "target": "OpenAIModel-h60Oi", + "targetHandle": "{œfieldNameœ:œinput_valueœ,œidœ:œOpenAIModel-h60Oiœ,œinputTypesœ:[œMessageœ],œtypeœ:œstrœ}", + "selected": false + }, + { + "className": "", + "data": { + "sourceHandle": { + "dataType": "OpenAIModel", + "id": "OpenAIModel-h60Oi", + "name": "text_output", + "output_types": ["Message"] + }, + "targetHandle": { + "fieldName": "input_value", + "id": "ChatOutput-tjMXx", + "inputTypes": ["Message"], + "type": "str" + } + }, + "id": "reactflow__edge-OpenAIModel-h60Oi{œdataTypeœ:œOpenAIModelœ,œidœ:œOpenAIModel-h60Oiœ,œnameœ:œtext_outputœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-tjMXx{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-tjMXxœ,œinputTypesœ:[œMessageœ],œtypeœ:œstrœ}", + "source": "OpenAIModel-h60Oi", + "sourceHandle": "{œdataTypeœ:œOpenAIModelœ,œidœ:œOpenAIModel-h60Oiœ,œnameœ:œtext_outputœ,œoutput_typesœ:[œMessageœ]}", + "target": "ChatOutput-tjMXx", + "targetHandle": "{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-tjMXxœ,œinputTypesœ:[œMessageœ],œtypeœ:œstrœ}", + "selected": false + } + ], + "nodes": [ + { + "data": { + "description": "Get chat inputs from the Playground.", + "display_name": "Chat Input", + "id": "ChatInput-Jd1q1", + "node": { + "base_classes": ["Message"], + "beta": false, + "conditional_paths": [], + "custom_fields": {}, + "description": "Get chat inputs from the Playground.", + "display_name": "Chat Input", + "documentation": "", + "edited": false, + "field_order": [ + "input_value", + "store_message", + "sender", + "sender_name", + "session_id", + "files" + ], + "frozen": false, + "icon": "MessagesSquare", + "legacy": false, + "lf_version": "1.0.19.post2", + "metadata": {}, + "output_types": [], + "outputs": [ + { + "types": ["Message"], + "selected": "Message", + "name": "message", + "display_name": "Message", + "method": "message_response", + "value": "__UNDEFINED__", + "cache": true + } + ], + "pinned": false, + "template": { + "_type": "Component", + "background_color": { + "_input_type": "MessageTextInput", + "advanced": true, + "display_name": "Background Color", + "dynamic": false, + "info": "The background color of the icon.", + "input_types": ["Message"], + "list": false, + "load_from_db": false, + "name": "background_color", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "trace_as_input": true, + "trace_as_metadata": true, + "type": "str", + "value": "" + }, + "chat_icon": { + "_input_type": "MessageTextInput", + "advanced": true, + "display_name": "Icon", + "dynamic": false, + "info": "The icon of the message.", + "input_types": ["Message"], + "list": false, + "load_from_db": false, + "name": "chat_icon", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "trace_as_input": true, + "trace_as_metadata": true, + "type": "str", + "value": "" + }, + "code": { + "type": "code", + "required": true, + "placeholder": "", + "list": false, + "show": true, + "multiline": true, + "value": "from langflow.base.data.utils import IMG_FILE_TYPES, TEXT_FILE_TYPES\nfrom langflow.base.io.chat import ChatComponent\nfrom langflow.inputs import BoolInput\nfrom langflow.io import DropdownInput, FileInput, MessageTextInput, MultilineInput, Output\nfrom langflow.schema.message import Message\nfrom langflow.utils.constants import MESSAGE_SENDER_AI, MESSAGE_SENDER_NAME_USER, MESSAGE_SENDER_USER\n\n\nclass ChatInput(ChatComponent):\n display_name = \"Chat Input\"\n description = \"Get chat inputs from the Playground.\"\n icon = \"MessagesSquare\"\n name = \"ChatInput\"\n\n inputs = [\n MultilineInput(\n name=\"input_value\",\n display_name=\"Text\",\n value=\"\",\n info=\"Message to be passed as input.\",\n ),\n BoolInput(\n name=\"should_store_message\",\n display_name=\"Store Messages\",\n info=\"Store the message in the history.\",\n value=True,\n advanced=True,\n ),\n DropdownInput(\n name=\"sender\",\n display_name=\"Sender Type\",\n options=[MESSAGE_SENDER_AI, MESSAGE_SENDER_USER],\n value=MESSAGE_SENDER_USER,\n info=\"Type of sender.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"sender_name\",\n display_name=\"Sender Name\",\n info=\"Name of the sender.\",\n value=MESSAGE_SENDER_NAME_USER,\n advanced=True,\n ),\n MessageTextInput(\n name=\"session_id\",\n display_name=\"Session ID\",\n info=\"The session ID of the chat. If empty, the current session ID parameter will be used.\",\n advanced=True,\n ),\n FileInput(\n name=\"files\",\n display_name=\"Files\",\n file_types=TEXT_FILE_TYPES + IMG_FILE_TYPES,\n info=\"Files to be sent with the message.\",\n advanced=True,\n is_list=True,\n ),\n MessageTextInput(\n name=\"background_color\",\n display_name=\"Background Color\",\n info=\"The background color of the icon.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"chat_icon\",\n display_name=\"Icon\",\n info=\"The icon of the message.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"text_color\",\n display_name=\"Text Color\",\n info=\"The text color of the name\",\n advanced=True,\n ),\n ]\n outputs = [\n Output(display_name=\"Message\", name=\"message\", method=\"message_response\"),\n ]\n\n def message_response(self) -> Message:\n _background_color = self.background_color\n _text_color = self.text_color\n _icon = self.chat_icon\n message = Message(\n text=self.input_value,\n sender=self.sender,\n sender_name=self.sender_name,\n session_id=self.session_id,\n files=self.files,\n properties={\"background_color\": _background_color, \"text_color\": _text_color, \"icon\": _icon},\n )\n if self.session_id and isinstance(message, Message) and self.should_store_message:\n stored_message = self.send_message(\n message,\n )\n self.message.value = stored_message\n message = stored_message\n\n self.status = message\n return message\n", + "fileTypes": [], + "file_path": "", + "password": false, + "name": "code", + "advanced": true, + "dynamic": true, + "info": "", + "load_from_db": false, + "title_case": false + }, + "files": { + "advanced": true, + "display_name": "Files", + "dynamic": false, + "fileTypes": [ + "txt", + "md", + "mdx", + "csv", + "json", + "yaml", + "yml", + "xml", + "html", + "htm", + "pdf", + "docx", + "py", + "sh", + "sql", + "js", + "ts", + "tsx", + "jpg", + "jpeg", + "png", + "bmp", + "image" + ], + "file_path": "", + "info": "Files to be sent with the message.", + "list": true, + "name": "files", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "trace_as_metadata": true, + "type": "file", + "value": "" + }, + "input_value": { + "advanced": false, + "display_name": "Text", + "dynamic": false, + "info": "Message to be passed as input.", + "input_types": ["Message"], + "list": false, + "load_from_db": false, + "multiline": true, + "name": "input_value", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "trace_as_input": true, + "trace_as_metadata": true, + "type": "str", + "value": "Hello" + }, + "sender": { + "advanced": true, + "display_name": "Sender Type", + "dynamic": false, + "info": "Type of sender.", + "name": "sender", + "options": ["Machine", "User"], + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "trace_as_metadata": true, + "type": "str", + "value": "User" + }, + "sender_name": { + "advanced": true, + "display_name": "Sender Name", + "dynamic": false, + "info": "Name of the sender.", + "input_types": ["Message"], + "list": false, + "load_from_db": false, + "name": "sender_name", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "trace_as_input": true, + "trace_as_metadata": true, + "type": "str", + "value": "User" + }, + "session_id": { + "advanced": true, + "display_name": "Session ID", + "dynamic": false, + "info": "The session ID of the chat. If empty, the current session ID parameter will be used.", + "input_types": ["Message"], + "list": false, + "load_from_db": false, + "name": "session_id", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "trace_as_input": true, + "trace_as_metadata": true, + "type": "str", + "value": "" + }, + "should_store_message": { + "_input_type": "BoolInput", + "advanced": true, + "display_name": "Store Messages", + "dynamic": false, + "info": "Store the message in the history.", + "list": false, + "name": "should_store_message", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "trace_as_metadata": true, + "type": "bool", + "value": true + }, + "text_color": { + "_input_type": "MessageTextInput", + "advanced": true, + "display_name": "Text Color", + "dynamic": false, + "info": "The text color of the name", + "input_types": ["Message"], + "list": false, + "load_from_db": false, + "name": "text_color", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "trace_as_input": true, + "trace_as_metadata": true, + "type": "str", + "value": "" + } + } + }, + "type": "ChatInput" + }, + "dragging": false, + "height": 234, + "id": "ChatInput-Jd1q1", + "position": { + "x": 689.5720422421635, + "y": 765.155834131403 + }, + "positionAbsolute": { + "x": 689.5720422421635, + "y": 765.155834131403 + }, + "selected": false, + "type": "genericNode", + "width": 320 + }, + { + "data": { + "description": "Create a prompt template with dynamic variables.", + "display_name": "Prompt", + "id": "Prompt-Smoiz", + "node": { + "base_classes": ["Message"], + "beta": false, + "conditional_paths": [], + "custom_fields": { + "template": [] + }, + "description": "Create a prompt template with dynamic variables.", + "display_name": "Prompt", + "documentation": "", + "edited": false, + "field_order": ["template"], + "frozen": false, + "icon": "prompts", + "legacy": false, + "metadata": {}, + "output_types": [], + "outputs": [ + { + "types": ["Message"], + "selected": "Message", + "name": "prompt", + "display_name": "Prompt Message", + "method": "build_prompt", + "value": "__UNDEFINED__", + "cache": true + } + ], + "pinned": false, + "template": { + "_type": "Component", + "code": { + "type": "code", + "required": true, + "placeholder": "", + "list": false, + "show": true, + "multiline": true, + "value": "from langflow.base.prompts.api_utils import process_prompt_template\nfrom langflow.custom import Component\nfrom langflow.inputs.inputs import DefaultPromptField\nfrom langflow.io import Output, PromptInput\nfrom langflow.schema.message import Message\nfrom langflow.template.utils import update_template_values\n\n\nclass PromptComponent(Component):\n display_name: str = \"Prompt\"\n description: str = \"Create a prompt template with dynamic variables.\"\n icon = \"prompts\"\n trace_type = \"prompt\"\n name = \"Prompt\"\n\n inputs = [\n PromptInput(name=\"template\", display_name=\"Template\"),\n ]\n\n outputs = [\n Output(display_name=\"Prompt Message\", name=\"prompt\", method=\"build_prompt\"),\n ]\n\n async def build_prompt(self) -> Message:\n prompt = Message.from_template(**self._attributes)\n self.status = prompt.text\n return prompt\n\n def _update_template(self, frontend_node: dict):\n prompt_template = frontend_node[\"template\"][\"template\"][\"value\"]\n custom_fields = frontend_node[\"custom_fields\"]\n frontend_node_template = frontend_node[\"template\"]\n _ = process_prompt_template(\n template=prompt_template,\n name=\"template\",\n custom_fields=custom_fields,\n frontend_node_template=frontend_node_template,\n )\n return frontend_node\n\n def post_code_processing(self, new_frontend_node: dict, current_frontend_node: dict):\n \"\"\"This function is called after the code validation is done.\"\"\"\n frontend_node = super().post_code_processing(new_frontend_node, current_frontend_node)\n template = frontend_node[\"template\"][\"template\"][\"value\"]\n # Kept it duplicated for backwards compatibility\n _ = process_prompt_template(\n template=template,\n name=\"template\",\n custom_fields=frontend_node[\"custom_fields\"],\n frontend_node_template=frontend_node[\"template\"],\n )\n # Now that template is updated, we need to grab any values that were set in the current_frontend_node\n # and update the frontend_node with those values\n update_template_values(new_template=frontend_node, previous_template=current_frontend_node[\"template\"])\n return frontend_node\n\n def _get_fallback_input(self, **kwargs):\n return DefaultPromptField(**kwargs)\n", + "fileTypes": [], + "file_path": "", + "password": false, + "name": "code", + "advanced": true, + "dynamic": true, + "info": "", + "load_from_db": false, + "title_case": false + }, + "template": { + "_input_type": "PromptInput", + "advanced": false, + "display_name": "Template", + "dynamic": false, + "info": "", + "list": false, + "load_from_db": false, + "name": "template", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "type": "prompt", + "value": "Answer the user as if you were a GenAI expert, enthusiastic about helping them get started building something fresh." + } + }, + "tool_mode": false + }, + "type": "Prompt" + }, + "dragging": false, + "height": 260, + "id": "Prompt-Smoiz", + "position": { + "x": 690.2015147036818, + "y": 1018.5443911764344 + }, + "positionAbsolute": { + "x": 690.2015147036818, + "y": 1018.5443911764344 + }, + "selected": false, + "type": "genericNode", + "width": 320 + }, + { + "data": { + "id": "undefined-MqgKn", + "node": { + "description": "## 📖 README\n\nPerform basic prompting with an OpenAI model.\n\n#### Quick Start\n- Add your **OpenAI API key** to the **OpenAI Model**\n- Open the **Playground** to chat with your bot.\n\n#### Next steps:\n Experiment by changing the prompt and the OpenAI model temperature to see how the bot's responses change.", + "display_name": "Read Me", + "documentation": "", + "template": { + "backgroundColor": "neutral" + } + } + }, + "dragging": false, + "height": 250, + "id": "undefined-MqgKn", + "position": { + "x": 66.38770028934243, + "y": 749.744424427066 + }, + "positionAbsolute": { + "x": 66.38770028934243, + "y": 749.744424427066 + }, + "resizing": false, + "selected": false, + "style": { + "height": 250, + "width": 600 + }, + "type": "noteNode", + "width": 600 + }, + { + "data": { + "id": "note-IpHtX", + "node": { + "description": "### 💡 Add your OpenAI API key here 👇", + "display_name": "", + "documentation": "", + "template": { + "backgroundColor": "transparent" + } + }, + "type": "note" + }, + "dragging": false, + "height": 324, + "id": "note-IpHtX", + "position": { + "x": 1075.829573520873, + "y": 657.2057655038416 + }, + "positionAbsolute": { + "x": 1075.829573520873, + "y": 657.2057655038416 + }, + "resizing": false, + "selected": false, + "style": { + "height": 324, + "width": 324 + }, + "type": "noteNode", + "width": 324 + }, + { + "data": { + "description": "Generates text using OpenAI LLMs.", + "display_name": "OpenAI", + "id": "OpenAIModel-h60Oi", + "node": { + "base_classes": ["LanguageModel", "Message"], + "beta": false, + "conditional_paths": [], + "custom_fields": {}, + "description": "Generates text using OpenAI LLMs.", + "display_name": "OpenAI", + "documentation": "", + "edited": false, + "field_order": [ + "input_value", + "system_message", + "stream", + "max_tokens", + "model_kwargs", + "json_mode", + "output_schema", + "model_name", + "openai_api_base", + "api_key", + "temperature", + "seed", + "output_parser" + ], + "frozen": false, + "icon": "OpenAI", + "legacy": false, + "metadata": {}, + "output_types": [], + "outputs": [ + { + "types": ["Message"], + "selected": "Message", + "name": "text_output", + "display_name": "Text", + "method": "text_response", + "value": "__UNDEFINED__", + "cache": true, + "required_inputs": [] + }, + { + "types": ["LanguageModel"], + "selected": "LanguageModel", + "name": "model_output", + "display_name": "Language Model", + "method": "build_model", + "value": "__UNDEFINED__", + "cache": true, + "required_inputs": [] + } + ], + "pinned": false, + "template": { + "_type": "Component", + "api_key": { + "_input_type": "SecretStrInput", + "advanced": false, + "display_name": "OpenAI API Key", + "dynamic": false, + "info": "The OpenAI API Key to use for the OpenAI model.", + "input_types": ["Message"], + "load_from_db": true, + "name": "api_key", + "password": true, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "type": "str", + "value": "" + }, + "code": { + "type": "code", + "required": true, + "placeholder": "", + "list": false, + "show": true, + "multiline": true, + "value": "import operator\nfrom functools import reduce\n\nfrom langchain_openai import ChatOpenAI\nfrom pydantic.v1 import SecretStr\n\nfrom langflow.base.models.model import LCModelComponent\nfrom langflow.base.models.openai_constants import OPENAI_MODEL_NAMES\nfrom langflow.field_typing import LanguageModel\nfrom langflow.field_typing.range_spec import RangeSpec\nfrom langflow.inputs import BoolInput, DictInput, DropdownInput, IntInput, SecretStrInput, SliderInput, StrInput\nfrom langflow.inputs.inputs import HandleInput\n\n\nclass OpenAIModelComponent(LCModelComponent):\n display_name = \"OpenAI\"\n description = \"Generates text using OpenAI LLMs.\"\n icon = \"OpenAI\"\n name = \"OpenAIModel\"\n\n inputs = [\n *LCModelComponent._base_inputs,\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n advanced=True,\n info=\"The maximum number of tokens to generate. Set to 0 for unlimited tokens.\",\n range_spec=RangeSpec(min=0, max=128000),\n ),\n DictInput(\n name=\"model_kwargs\",\n display_name=\"Model Kwargs\",\n advanced=True,\n info=\"Additional keyword arguments to pass to the model.\",\n ),\n BoolInput(\n name=\"json_mode\",\n display_name=\"JSON Mode\",\n advanced=True,\n info=\"If True, it will output JSON regardless of passing a schema.\",\n ),\n DictInput(\n name=\"output_schema\",\n is_list=True,\n display_name=\"Schema\",\n advanced=True,\n info=\"The schema for the Output of the model. \"\n \"You must pass the word JSON in the prompt. \"\n \"If left blank, JSON mode will be disabled. [DEPRECATED]\",\n ),\n DropdownInput(\n name=\"model_name\",\n display_name=\"Model Name\",\n advanced=False,\n options=OPENAI_MODEL_NAMES,\n value=OPENAI_MODEL_NAMES[0],\n ),\n StrInput(\n name=\"openai_api_base\",\n display_name=\"OpenAI API Base\",\n advanced=True,\n info=\"The base URL of the OpenAI API. \"\n \"Defaults to https://api.openai.com/v1. \"\n \"You can change this to use other APIs like JinaChat, LocalAI and Prem.\",\n ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"OpenAI API Key\",\n info=\"The OpenAI API Key to use for the OpenAI model.\",\n advanced=False,\n value=\"OPENAI_API_KEY\",\n ),\n SliderInput(name=\"temperature\", display_name=\"Temperature\", value=0.1, range_spec=RangeSpec(min=0, max=1)),\n IntInput(\n name=\"seed\",\n display_name=\"Seed\",\n info=\"The seed controls the reproducibility of the job.\",\n advanced=True,\n value=1,\n ),\n HandleInput(\n name=\"output_parser\",\n display_name=\"Output Parser\",\n info=\"The parser to use to parse the output of the model\",\n advanced=True,\n input_types=[\"OutputParser\"],\n ),\n ]\n\n def build_model(self) -> LanguageModel: # type: ignore[type-var]\n # self.output_schema is a list of dictionaries\n # let's convert it to a dictionary\n output_schema_dict: dict[str, str] = reduce(operator.ior, self.output_schema or {}, {})\n openai_api_key = self.api_key\n temperature = self.temperature\n model_name: str = self.model_name\n max_tokens = self.max_tokens\n model_kwargs = self.model_kwargs or {}\n openai_api_base = self.openai_api_base or \"https://api.openai.com/v1\"\n json_mode = bool(output_schema_dict) or self.json_mode\n seed = self.seed\n\n api_key = SecretStr(openai_api_key).get_secret_value() if openai_api_key else None\n output = ChatOpenAI(\n max_tokens=max_tokens or None,\n model_kwargs=model_kwargs,\n model=model_name,\n base_url=openai_api_base,\n api_key=api_key,\n temperature=temperature if temperature is not None else 0.1,\n seed=seed,\n )\n if json_mode:\n if output_schema_dict:\n output = output.with_structured_output(schema=output_schema_dict, method=\"json_mode\")\n else:\n output = output.bind(response_format={\"type\": \"json_object\"})\n\n return output\n\n def _get_exception_message(self, e: Exception):\n \"\"\"Get a message from an OpenAI exception.\n\n Args:\n e (Exception): The exception to get the message from.\n\n Returns:\n str: The message from the exception.\n \"\"\"\n try:\n from openai import BadRequestError\n except ImportError:\n return None\n if isinstance(e, BadRequestError):\n message = e.body.get(\"message\")\n if message:\n return message\n return None\n", + "fileTypes": [], + "file_path": "", + "password": false, + "name": "code", + "advanced": true, + "dynamic": true, + "info": "", + "load_from_db": false, + "title_case": false + }, + "input_value": { + "_input_type": "MessageInput", + "advanced": false, + "display_name": "Input", + "dynamic": false, + "info": "", + "input_types": ["Message"], + "list": false, + "load_from_db": false, + "name": "input_value", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "trace_as_input": true, + "trace_as_metadata": true, + "type": "str", + "value": "" + }, + "json_mode": { + "_input_type": "BoolInput", + "advanced": true, + "display_name": "JSON Mode", + "dynamic": false, + "info": "If True, it will output JSON regardless of passing a schema.", + "list": false, + "name": "json_mode", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "trace_as_metadata": true, + "type": "bool", + "value": false + }, + "max_tokens": { + "_input_type": "IntInput", + "advanced": true, + "display_name": "Max Tokens", + "dynamic": false, + "info": "The maximum number of tokens to generate. Set to 0 for unlimited tokens.", + "list": false, + "name": "max_tokens", + "placeholder": "", + "range_spec": { + "max": 128000, + "min": 0, + "step": 0.1, + "step_type": "float" + }, + "required": false, + "show": true, + "title_case": false, + "trace_as_metadata": true, + "type": "int", + "value": "" + }, + "model_kwargs": { + "_input_type": "DictInput", + "advanced": true, + "display_name": "Model Kwargs", + "dynamic": false, + "info": "Additional keyword arguments to pass to the model.", + "list": false, + "name": "model_kwargs", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "trace_as_input": true, + "type": "dict", + "value": {} + }, + "model_name": { + "_input_type": "DropdownInput", + "advanced": false, + "combobox": false, + "display_name": "Model Name", + "dynamic": false, + "info": "", + "name": "model_name", + "options": [ + "gpt-4o-mini", + "gpt-4o", + "gpt-4-turbo", + "gpt-4-turbo-preview", + "gpt-4", + "gpt-3.5-turbo", + "gpt-3.5-turbo-0125" + ], + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "type": "str", + "value": "gpt-4o-mini" + }, + "openai_api_base": { + "_input_type": "StrInput", + "advanced": true, + "display_name": "OpenAI API Base", + "dynamic": false, + "info": "The base URL of the OpenAI API. Defaults to https://api.openai.com/v1. You can change this to use other APIs like JinaChat, LocalAI and Prem.", + "list": false, + "load_from_db": false, + "name": "openai_api_base", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "trace_as_metadata": true, + "type": "str", + "value": "" + }, + "output_parser": { + "_input_type": "HandleInput", + "advanced": true, + "display_name": "Output Parser", + "dynamic": false, + "info": "The parser to use to parse the output of the model", + "input_types": ["OutputParser"], + "list": false, + "name": "output_parser", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "trace_as_metadata": true, + "type": "other", + "value": "" + }, + "output_schema": { + "_input_type": "DictInput", + "advanced": true, + "display_name": "Schema", + "dynamic": false, + "info": "The schema for the Output of the model. You must pass the word JSON in the prompt. If left blank, JSON mode will be disabled. [DEPRECATED]", + "list": true, + "name": "output_schema", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "trace_as_input": true, + "type": "dict", + "value": {} + }, + "seed": { + "_input_type": "IntInput", + "advanced": true, + "display_name": "Seed", + "dynamic": false, + "info": "The seed controls the reproducibility of the job.", + "list": false, + "name": "seed", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "trace_as_metadata": true, + "type": "int", + "value": 1 + }, + "stream": { + "_input_type": "BoolInput", + "advanced": false, + "display_name": "Stream", + "dynamic": false, + "info": "Stream the response from the model. Streaming works only in Chat.", + "list": false, + "name": "stream", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "trace_as_metadata": true, + "type": "bool", + "value": false + }, + "system_message": { + "_input_type": "MessageTextInput", + "advanced": false, + "display_name": "System Message", + "dynamic": false, + "info": "System message to pass to the model.", + "input_types": ["Message"], + "list": false, + "load_from_db": false, + "name": "system_message", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "type": "str", + "value": "" + }, + "temperature": { + "_input_type": "FloatInput", + "advanced": false, + "display_name": "Temperature", + "dynamic": false, + "info": "", + "list": false, + "name": "temperature", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "trace_as_metadata": true, + "type": "float", + "value": 0.1 + } + }, + "tool_mode": false + }, + "type": "OpenAIModel" + }, + "dragging": false, + "height": 630, + "id": "OpenAIModel-h60Oi", + "position": { + "x": 1081.0157946607428, + "y": 707.3740542546418 + }, + "positionAbsolute": { + "x": 1081.0157946607428, + "y": 707.3740542546418 + }, + "selected": false, + "type": "genericNode", + "width": 320 + }, + { + "data": { + "id": "ChatOutput-tjMXx", + "node": { + "base_classes": ["Message"], + "beta": false, + "conditional_paths": [], + "custom_fields": {}, + "description": "Display a chat message in the Playground.", + "display_name": "Chat Output", + "documentation": "", + "edited": false, + "field_order": [ + "input_value", + "should_store_message", + "sender", + "sender_name", + "session_id", + "data_template", + "background_color", + "chat_icon", + "text_color" + ], + "frozen": false, + "icon": "MessagesSquare", + "legacy": false, + "metadata": {}, + "output_types": [], + "outputs": [ + { + "types": ["Message"], + "selected": "Message", + "name": "message", + "display_name": "Message", + "method": "message_response", + "value": "__UNDEFINED__", + "cache": true + } + ], + "pinned": false, + "template": { + "_type": "Component", + "background_color": { + "_input_type": "MessageTextInput", + "advanced": true, + "display_name": "Background Color", + "dynamic": false, + "info": "The background color of the icon.", + "input_types": ["Message"], + "list": false, + "load_from_db": false, + "name": "background_color", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "type": "str", + "value": "" + }, + "chat_icon": { + "_input_type": "MessageTextInput", + "advanced": true, + "display_name": "Icon", + "dynamic": false, + "info": "The icon of the message.", + "input_types": ["Message"], + "list": false, + "load_from_db": false, + "name": "chat_icon", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "type": "str", + "value": "" + }, + "code": { + "type": "code", + "required": true, + "placeholder": "", + "list": false, + "show": true, + "multiline": true, + "value": "from langflow.base.io.chat import ChatComponent\nfrom langflow.inputs import BoolInput\nfrom langflow.io import DropdownInput, MessageInput, MessageTextInput, Output\nfrom langflow.schema.message import Message\nfrom langflow.schema.properties import Source\nfrom langflow.utils.constants import MESSAGE_SENDER_AI, MESSAGE_SENDER_NAME_AI, MESSAGE_SENDER_USER\n\n\nclass ChatOutput(ChatComponent):\n display_name = \"Chat Output\"\n description = \"Display a chat message in the Playground.\"\n icon = \"MessagesSquare\"\n name = \"ChatOutput\"\n\n inputs = [\n MessageInput(\n name=\"input_value\",\n display_name=\"Text\",\n info=\"Message to be passed as output.\",\n ),\n BoolInput(\n name=\"should_store_message\",\n display_name=\"Store Messages\",\n info=\"Store the message in the history.\",\n value=True,\n advanced=True,\n ),\n DropdownInput(\n name=\"sender\",\n display_name=\"Sender Type\",\n options=[MESSAGE_SENDER_AI, MESSAGE_SENDER_USER],\n value=MESSAGE_SENDER_AI,\n advanced=True,\n info=\"Type of sender.\",\n ),\n MessageTextInput(\n name=\"sender_name\",\n display_name=\"Sender Name\",\n info=\"Name of the sender.\",\n value=MESSAGE_SENDER_NAME_AI,\n advanced=True,\n ),\n MessageTextInput(\n name=\"session_id\",\n display_name=\"Session ID\",\n info=\"The session ID of the chat. If empty, the current session ID parameter will be used.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"data_template\",\n display_name=\"Data Template\",\n value=\"{text}\",\n advanced=True,\n info=\"Template to convert Data to Text. If left empty, it will be dynamically set to the Data's text key.\",\n ),\n MessageTextInput(\n name=\"background_color\",\n display_name=\"Background Color\",\n info=\"The background color of the icon.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"chat_icon\",\n display_name=\"Icon\",\n info=\"The icon of the message.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"text_color\",\n display_name=\"Text Color\",\n info=\"The text color of the name\",\n advanced=True,\n ),\n ]\n outputs = [\n Output(\n display_name=\"Message\",\n name=\"message\",\n method=\"message_response\",\n ),\n ]\n\n def _build_source(self, _id: str | None, display_name: str | None, source: str | None) -> Source:\n source_dict = {}\n if _id:\n source_dict[\"id\"] = _id\n if display_name:\n source_dict[\"display_name\"] = display_name\n if source:\n source_dict[\"source\"] = source\n return Source(**source_dict)\n\n def message_response(self) -> Message:\n _source, _icon, _display_name, _source_id = self.get_properties_from_source_component()\n _background_color = self.background_color\n _text_color = self.text_color\n if self.chat_icon:\n _icon = self.chat_icon\n message = self.input_value if isinstance(self.input_value, Message) else Message(text=self.input_value)\n message.sender = self.sender\n message.sender_name = self.sender_name\n message.session_id = self.session_id\n message.flow_id = self.graph.flow_id if hasattr(self, \"graph\") else None\n message.properties.source = self._build_source(_source_id, _display_name, _source)\n message.properties.icon = _icon\n message.properties.background_color = _background_color\n message.properties.text_color = _text_color\n if self.session_id and isinstance(message, Message) and self.should_store_message:\n stored_message = self.send_message(\n message,\n )\n self.message.value = stored_message\n message = stored_message\n\n self.status = message\n return message\n", + "fileTypes": [], + "file_path": "", + "password": false, + "name": "code", + "advanced": true, + "dynamic": true, + "info": "", + "load_from_db": false, + "title_case": false + }, + "data_template": { + "_input_type": "MessageTextInput", + "advanced": true, + "display_name": "Data Template", + "dynamic": false, + "info": "Template to convert Data to Text. If left empty, it will be dynamically set to the Data's text key.", + "input_types": ["Message"], + "list": false, + "load_from_db": false, + "name": "data_template", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "type": "str", + "value": "{text}" + }, + "input_value": { + "_input_type": "MessageInput", + "advanced": false, + "display_name": "Text", + "dynamic": false, + "info": "Message to be passed as output.", + "input_types": ["Message"], + "list": false, + "load_from_db": false, + "name": "input_value", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "trace_as_input": true, + "trace_as_metadata": true, + "type": "str", + "value": "" + }, + "sender": { + "_input_type": "DropdownInput", + "advanced": true, + "combobox": false, + "display_name": "Sender Type", + "dynamic": false, + "info": "Type of sender.", + "name": "sender", + "options": ["Machine", "User"], + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "type": "str", + "value": "Machine" + }, + "sender_name": { + "_input_type": "MessageTextInput", + "advanced": true, + "display_name": "Sender Name", + "dynamic": false, + "info": "Name of the sender.", + "input_types": ["Message"], + "list": false, + "load_from_db": false, + "name": "sender_name", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "type": "str", + "value": "AI" + }, + "session_id": { + "_input_type": "MessageTextInput", + "advanced": true, + "display_name": "Session ID", + "dynamic": false, + "info": "The session ID of the chat. If empty, the current session ID parameter will be used.", + "input_types": ["Message"], + "list": false, + "load_from_db": false, + "name": "session_id", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "type": "str", + "value": "" + }, + "should_store_message": { + "_input_type": "BoolInput", + "advanced": true, + "display_name": "Store Messages", + "dynamic": false, + "info": "Store the message in the history.", + "list": false, + "name": "should_store_message", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "trace_as_metadata": true, + "type": "bool", + "value": true + }, + "text_color": { + "_input_type": "MessageTextInput", + "advanced": true, + "display_name": "Text Color", + "dynamic": false, + "info": "The text color of the name", + "input_types": ["Message"], + "list": false, + "load_from_db": false, + "name": "text_color", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "type": "str", + "value": "" + } + }, + "tool_mode": false + }, + "type": "ChatOutput" + }, + "dragging": false, + "height": 234, + "id": "ChatOutput-tjMXx", + "position": { + "x": 1444.936881624563, + "y": 872.7273956769025 + }, + "positionAbsolute": { + "x": 1444.936881624563, + "y": 872.7273956769025 + }, + "selected": false, + "type": "genericNode", + "width": 320 + } + ], + "viewport": { + "x": -21.631817700819965, + "y": -334.5576887147924, + "zoom": 0.7749929474098888 + } + }, + "description": "Perform basic prompting with an OpenAI model.", + "name": "LANGFLOW TEST", + "last_tested_version": "1.1.1", + "endpoint_name": null, + "is_component": false +} diff --git a/src/frontend/tests/extended/features/dragAndDrop.spec.ts b/src/frontend/tests/extended/features/dragAndDrop.spec.ts index 63a149491..9c8b5ed46 100644 --- a/src/frontend/tests/extended/features/dragAndDrop.spec.ts +++ b/src/frontend/tests/extended/features/dragAndDrop.spec.ts @@ -1,9 +1,10 @@ import { expect, test } from "@playwright/test"; import { readFileSync } from "fs"; - -test.describe("drag and drop test", () => { - /// - test("drop collection", { tag: ["@release"] }, async ({ page }) => { +import { simulateDragAndDrop } from "../../utils/simulate-drag-and-drop"; +test( + "user should be able to drag and drop an old collection without crashing the application", + { tag: ["@release"] }, + async ({ page }) => { await page.goto("/"); let modalCount = 0; @@ -18,33 +19,22 @@ test.describe("drag and drop test", () => { while (modalCount === 0) { await page.getByText("New Flow", { exact: true }).click(); - await page.waitForTimeout(3000); + await page.waitForSelector('[data-testid="modal-title"]', { + timeout: 3000, + }); modalCount = await page.getByTestId("modal-title")?.count(); } + await page.locator("span").filter({ hasText: "Close" }).first().click(); - await page.locator("span").filter({ hasText: "My Collection" }).isVisible(); - // Read your file into a buffer. - const jsonContent = readFileSync("tests/assets/collection.json", "utf-8"); + await page.locator("span").filter({ hasText: "My Projects" }).isVisible(); - // Create the DataTransfer and File - const dataTransfer = await page.evaluateHandle((data) => { - const dt = new DataTransfer(); - // Convert the buffer to a hex array - const file = new File([data], "flowtest.json", { - type: "application/json", - }); - dt.items.add(file); - return dt; - }, jsonContent); + await simulateDragAndDrop(page, "assets/collection.json", "cards-wrapper"); - // Now dispatch - await page.getByTestId("cards-wrapper").dispatchEvent("drop", { - dataTransfer, + await page.waitForSelector("text=uploaded successfully", { + timeout: 60000 * 2, }); - await page.waitForTimeout(3000); - const genericNode = page.getByTestId("div-generic-node"); const elementCount = await genericNode?.count(); if (elementCount > 0) { @@ -58,5 +48,72 @@ test.describe("drag and drop test", () => { expect( await page.locator("text=Getting Started:").last().isVisible(), ).toBeTruthy(); - }); -}); + }, +); + +test( + "user should be able to drag and drop a flow on main page", + { tag: ["@release"] }, + async ({ page }) => { + await page.goto("/"); + + let modalCount = 0; + try { + const modalTitleElement = await page?.getByTestId("modal-title"); + if (modalTitleElement) { + modalCount = await modalTitleElement.count(); + } + } catch (error) { + modalCount = 0; + } + + while (modalCount === 0) { + await page.getByText("New Flow", { exact: true }).click(); + await page.waitForSelector('[data-testid="modal-title"]', { + timeout: 3000, + }); + modalCount = await page.getByTestId("modal-title")?.count(); + } + + await page.locator("span").filter({ hasText: "Close" }).first().click(); + await page.locator("span").filter({ hasText: "My Projects" }).isVisible(); + + // Read your file into a buffer. + const jsonContent = readFileSync( + "assets/flow_test_drag_and_drop.json", + "utf-8", + ); + + const randomName = Math.random().toString(36).substring(2, 15); + const jsonContentWithNewName = jsonContent.replace( + "LANGFLOW TEST", + randomName, + ); + + await simulateDragAndDrop( + page, + "assets/flow_test_drag_and_drop.json", + "cards-wrapper", + jsonContentWithNewName, + ); + + await page.waitForSelector("text=uploaded successfully", { + timeout: 60000 * 2, + }); + + const genericNode = page.getByTestId("div-generic-node"); + + const elementCount = await genericNode?.count(); + if (elementCount > 0) { + expect(true).toBeTruthy(); + } + + await page.waitForSelector(`text=${randomName}`, { + timeout: 100000, + }); + + expect( + await page.locator(`text=${randomName}`).last().isVisible(), + ).toBeTruthy(); + }, +); diff --git a/src/frontend/tests/utils/simulate-drag-and-drop.ts b/src/frontend/tests/utils/simulate-drag-and-drop.ts new file mode 100644 index 000000000..fd3819ced --- /dev/null +++ b/src/frontend/tests/utils/simulate-drag-and-drop.ts @@ -0,0 +1,31 @@ +import { Page } from "@playwright/test"; +import { readFileSync } from "fs"; + +export async function simulateDragAndDrop( + page: Page, + filePath: string, + dropSelector: string, + jsonContent?: string, +) { + // Read file content + let fileContent = await readFileSync(filePath, "utf-8"); + + if (jsonContent) { + fileContent = jsonContent; + } + + // Create DataTransfer object with file + const dataTransfer = await page.evaluateHandle(async (content) => { + const dt = new DataTransfer(); + const file = new File([content], "file.json", { type: "application/json" }); + dt.items.add(file); + return dt; + }, fileContent); + + const dropzone = page.getByTestId(dropSelector); + await dropzone.dispatchEvent("mousedown", { button: 0 }); + await dropzone.dispatchEvent("mousemove", { buttons: 1 }); + await dropzone.dispatchEvent("drop", { dataTransfer }); + + await dataTransfer.dispose(); +}