feat: add stop button on Playground Chat (#3704)
* 📝 (buttonSendWrapper/index.tsx): refactor button styling logic to use constants for better readability and maintainability 🐛 (buttonSendWrapper/index.tsx): fix logic to disable button when chat is locked and not building 🐛 (buttonSendWrapper/index.tsx): fix conditional rendering of icons based on button state * ✨ (stop-button-playground.spec.ts): Add end-to-end test for stopping building from inside Playground. * 🔧 (buttonSendWrapper/index.tsx): refactor conditional logic to correctly prioritize showStopButton condition and improve code readability * ✅ (stop-button-playground.spec.ts): add assertion to check if "build stopped" text is visible after clicking on the stop button
This commit is contained in:
parent
f72e009406
commit
9b83f44611
2 changed files with 200 additions and 18 deletions
|
|
@ -1,9 +1,17 @@
|
||||||
|
import useFlowStore from "@/stores/flowStore";
|
||||||
import IconComponent from "../../../../../../../components/genericIconComponent";
|
import IconComponent from "../../../../../../../components/genericIconComponent";
|
||||||
import { Button } from "../../../../../../../components/ui/button";
|
import { Button } from "../../../../../../../components/ui/button";
|
||||||
import { Case } from "../../../../../../../shared/components/caseComponent";
|
import { Case } from "../../../../../../../shared/components/caseComponent";
|
||||||
import { FilePreviewType } from "../../../../../../../types/components";
|
import { FilePreviewType } from "../../../../../../../types/components";
|
||||||
import { classNames } from "../../../../../../../utils/utils";
|
import { classNames } from "../../../../../../../utils/utils";
|
||||||
|
|
||||||
|
const BUTTON_STATES = {
|
||||||
|
NO_INPUT: "bg-high-indigo text-background",
|
||||||
|
HAS_CHAT_VALUE: "text-primary",
|
||||||
|
SHOW_STOP: "bg-error text-background cursor-pointer",
|
||||||
|
DEFAULT: "bg-chat-send text-background",
|
||||||
|
};
|
||||||
|
|
||||||
type ButtonSendWrapperProps = {
|
type ButtonSendWrapperProps = {
|
||||||
send: () => void;
|
send: () => void;
|
||||||
lockChat: boolean;
|
lockChat: boolean;
|
||||||
|
|
@ -19,29 +27,48 @@ const ButtonSendWrapper = ({
|
||||||
chatValue,
|
chatValue,
|
||||||
files,
|
files,
|
||||||
}: ButtonSendWrapperProps) => {
|
}: ButtonSendWrapperProps) => {
|
||||||
|
const stopBuilding = useFlowStore((state) => state.stopBuilding);
|
||||||
|
|
||||||
|
const isBuilding = useFlowStore((state) => state.isBuilding);
|
||||||
|
const showStopButton = lockChat || files.some((file) => file.loading);
|
||||||
|
const showPlayButton = !lockChat && noInput;
|
||||||
|
const showSendButton =
|
||||||
|
!(lockChat || files.some((file) => file.loading)) && !noInput;
|
||||||
|
|
||||||
|
const getButtonState = () => {
|
||||||
|
if (showStopButton) return BUTTON_STATES.SHOW_STOP;
|
||||||
|
if (noInput) return BUTTON_STATES.NO_INPUT;
|
||||||
|
if (chatValue) return BUTTON_STATES.HAS_CHAT_VALUE;
|
||||||
|
|
||||||
|
return BUTTON_STATES.DEFAULT;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buttonClasses = classNames("form-modal-send-button", getButtonState());
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
if (showStopButton && isBuilding) {
|
||||||
|
stopBuilding();
|
||||||
|
} else if (!showStopButton) {
|
||||||
|
send();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
className={classNames(
|
className={buttonClasses}
|
||||||
"form-modal-send-button",
|
disabled={lockChat && !isBuilding}
|
||||||
noInput
|
onClick={handleClick}
|
||||||
? "bg-high-indigo text-background"
|
|
||||||
: chatValue
|
|
||||||
? "text-primary"
|
|
||||||
: "bg-chat-send text-background",
|
|
||||||
)}
|
|
||||||
disabled={lockChat}
|
|
||||||
onClick={(): void => send()}
|
|
||||||
unstyled
|
unstyled
|
||||||
>
|
>
|
||||||
<Case condition={lockChat || files.some((file) => file.loading)}>
|
<Case condition={showStopButton}>
|
||||||
<IconComponent
|
<IconComponent
|
||||||
name="Lock"
|
name="Square"
|
||||||
className="form-modal-lock-icon"
|
className="form-modal-lock-icon"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</Case>
|
</Case>
|
||||||
|
|
||||||
<Case condition={noInput && !lockChat}>
|
<Case condition={showPlayButton}>
|
||||||
<IconComponent
|
<IconComponent
|
||||||
name="Zap"
|
name="Zap"
|
||||||
className="form-modal-play-icon"
|
className="form-modal-play-icon"
|
||||||
|
|
@ -49,11 +76,7 @@ const ButtonSendWrapper = ({
|
||||||
/>
|
/>
|
||||||
</Case>
|
</Case>
|
||||||
|
|
||||||
<Case
|
<Case condition={showSendButton}>
|
||||||
condition={
|
|
||||||
!(lockChat || files.some((file) => file.loading)) && !noInput
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<IconComponent
|
<IconComponent
|
||||||
name="LucideSend"
|
name="LucideSend"
|
||||||
className="form-modal-send-icon"
|
className="form-modal-send-icon"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,159 @@
|
||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
import uaParser from "ua-parser-js";
|
||||||
|
|
||||||
|
test("User must be able to stop building from inside Playground", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.goto("/");
|
||||||
|
await page.waitForSelector('[data-testid="mainpage_title"]', {
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.waitForSelector('[id="new-project-btn"]', {
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
|
|
||||||
|
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 Project", { exact: true }).click();
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
modalCount = await page.getByTestId("modal-title")?.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.getByTestId("blank-flow").click();
|
||||||
|
await page.waitForSelector('[data-testid="extended-disclosure"]', {
|
||||||
|
timeout: 1000,
|
||||||
|
});
|
||||||
|
await page.getByTestId("extended-disclosure").click();
|
||||||
|
await page.getByPlaceholder("Search").click();
|
||||||
|
await page.getByPlaceholder("Search").fill("custom");
|
||||||
|
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
await page
|
||||||
|
.locator('//*[@id="helpersCustom Component"]')
|
||||||
|
.dragTo(page.locator('//*[@id="react-flow-id"]'));
|
||||||
|
await page.mouse.up();
|
||||||
|
await page.mouse.down();
|
||||||
|
await page.getByTitle("fit view").click();
|
||||||
|
await page.getByTitle("zoom out").click();
|
||||||
|
await page.getByTitle("zoom out").click();
|
||||||
|
await page.getByTitle("zoom out").click();
|
||||||
|
|
||||||
|
await page.getByPlaceholder("Search").click();
|
||||||
|
await page.getByPlaceholder("Search").fill("chat output");
|
||||||
|
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByTestId("outputsChat Output")
|
||||||
|
.dragTo(page.locator('//*[@id="react-flow-id"]'));
|
||||||
|
|
||||||
|
await page.getByTitle("fit view").click();
|
||||||
|
await page.getByTitle("zoom out").click();
|
||||||
|
await page.getByTitle("zoom out").click();
|
||||||
|
await page.getByTitle("zoom out").click();
|
||||||
|
|
||||||
|
await page.getByTestId("div-generic-node").nth(0).click();
|
||||||
|
|
||||||
|
await page.getByTestId("code-button-modal").nth(0).click();
|
||||||
|
|
||||||
|
const waitTimeoutCode = `
|
||||||
|
# from langflow.field_typing import Data
|
||||||
|
from langflow.custom import Component
|
||||||
|
from langflow.io import MessageTextInput, Output
|
||||||
|
from langflow.schema import Data
|
||||||
|
from time import sleep
|
||||||
|
from langflow.schema.message import Message
|
||||||
|
|
||||||
|
class CustomComponent(Component):
|
||||||
|
display_name = "Custom Component"
|
||||||
|
description = "Use as a template to create your own component."
|
||||||
|
documentation: str = "http://docs.langflow.org/components/custom"
|
||||||
|
icon = "custom_components"
|
||||||
|
name = "CustomComponent"
|
||||||
|
|
||||||
|
inputs = [
|
||||||
|
MessageTextInput(name="input_value", display_name="Input Value", value="Hello, World!"),
|
||||||
|
]
|
||||||
|
|
||||||
|
outputs = [
|
||||||
|
Output(display_name="Output", name="output", method="build_output"),
|
||||||
|
]
|
||||||
|
|
||||||
|
def build_output(self) -> Message:
|
||||||
|
data = Data(value=self.input_value)
|
||||||
|
self.status = data
|
||||||
|
sleep(60)
|
||||||
|
return data`;
|
||||||
|
|
||||||
|
const getUA = await page.evaluate(() => navigator.userAgent);
|
||||||
|
const userAgentInfo = uaParser(getUA);
|
||||||
|
let control = "Control";
|
||||||
|
|
||||||
|
if (userAgentInfo.os.name.includes("Mac")) {
|
||||||
|
control = "Meta";
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.locator(".ace_content").click();
|
||||||
|
await page.keyboard.press(`${control}+A`);
|
||||||
|
await page.locator("textarea").fill(waitTimeoutCode);
|
||||||
|
|
||||||
|
await page.getByText("Check & Save").last().click();
|
||||||
|
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
await page.getByTitle("fit view").click();
|
||||||
|
await page.getByTitle("zoom out").click();
|
||||||
|
await page.getByTitle("zoom out").click();
|
||||||
|
|
||||||
|
//connection 1
|
||||||
|
const elementCustomComponentOutput = await page
|
||||||
|
.getByTestId("handle-customcomponent-shownode-output-right")
|
||||||
|
.first();
|
||||||
|
|
||||||
|
await elementCustomComponentOutput.hover();
|
||||||
|
await page.mouse.down();
|
||||||
|
const elementChatOutput = await page
|
||||||
|
.getByTestId("handle-chatoutput-shownode-text-left")
|
||||||
|
.first();
|
||||||
|
await elementChatOutput.hover();
|
||||||
|
await page.mouse.up();
|
||||||
|
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
await page.getByTestId("button_run_chat output").click();
|
||||||
|
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
await page.getByText("Playground", { exact: true }).click();
|
||||||
|
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
await page.waitForSelector('[data-testid="icon-Square"]', {
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const elements = await page.$$('[data-testid="icon-Square"]');
|
||||||
|
|
||||||
|
if (elements.length > 0) {
|
||||||
|
const lastElement = elements[elements.length - 1];
|
||||||
|
await lastElement.waitForElementState("visible");
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(await page.getByTestId("icon-Square").last()).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByTestId("icon-Square").last().click();
|
||||||
|
|
||||||
|
await page.waitForSelector("text=build stopped", { timeout: 30000 });
|
||||||
|
expect(await page.getByText("build stopped").isVisible()).toBeTruthy();
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue