feat: add new input type - SliderComponent + tests (#4144)
This commit is contained in:
parent
ab77ca752b
commit
51b3909d60
13 changed files with 589 additions and 0 deletions
|
|
@ -19,6 +19,7 @@ from .inputs import (
|
|||
NestedDictInput,
|
||||
PromptInput,
|
||||
SecretStrInput,
|
||||
SliderInput,
|
||||
StrInput,
|
||||
TableInput,
|
||||
)
|
||||
|
|
@ -46,4 +47,8 @@ __all__ = [
|
|||
"SecretStrInput",
|
||||
"StrInput",
|
||||
"TableInput",
|
||||
"Input",
|
||||
"DefaultPromptField",
|
||||
"LinkInput",
|
||||
"SliderInput",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ class FieldTypes(str, Enum):
|
|||
OTHER = "other"
|
||||
TABLE = "table"
|
||||
LINK = "link"
|
||||
SLIDER = "slider"
|
||||
|
||||
|
||||
SerializableFieldTypes = Annotated[FieldTypes, PlainSerializer(lambda v: v.value, return_type=str)]
|
||||
|
|
@ -167,6 +168,16 @@ class LinkMixin(BaseModel):
|
|||
"""Text to be displayed in the link."""
|
||||
|
||||
|
||||
class SliderMixin(BaseModel):
|
||||
min_label: str = Field(default="")
|
||||
max_label: str = Field(default="")
|
||||
min_label_icon: str = Field(default="")
|
||||
max_label_icon: str = Field(default="")
|
||||
slider_buttons: bool = Field(default=False)
|
||||
slider_buttons_options: list[str] = Field(default=[])
|
||||
slider_input: bool = Field(default=False)
|
||||
|
||||
|
||||
class TableMixin(BaseModel):
|
||||
table_schema: TableSchema | list[Column] | None = None
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ from .input_mixin import (
|
|||
MultilineMixin,
|
||||
RangeMixin,
|
||||
SerializableFieldTypes,
|
||||
SliderMixin,
|
||||
TableMixin,
|
||||
)
|
||||
|
||||
|
|
@ -470,6 +471,10 @@ class LinkInput(BaseInputMixin, LinkMixin):
|
|||
field_type: SerializableFieldTypes = FieldTypes.LINK
|
||||
|
||||
|
||||
class SliderInput(BaseInputMixin, RangeMixin, SliderMixin):
|
||||
field_type: SerializableFieldTypes = FieldTypes.SLIDER
|
||||
|
||||
|
||||
DEFAULT_PROMPT_INTUT_TYPES = ["Message", "Text"]
|
||||
|
||||
|
||||
|
|
@ -506,6 +511,7 @@ InputTypes = (
|
|||
| MessageInput
|
||||
| TableInput
|
||||
| LinkInput
|
||||
| SliderInput
|
||||
)
|
||||
|
||||
InputTypesMap: dict[str, type[InputTypes]] = {t.__name__: t for t in get_args(InputTypes)}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ from langflow.inputs import (
|
|||
NestedDictInput,
|
||||
PromptInput,
|
||||
SecretStrInput,
|
||||
SliderInput,
|
||||
StrInput,
|
||||
TableInput,
|
||||
)
|
||||
|
|
@ -46,4 +47,7 @@ __all__ = [
|
|||
"SecretStrInput",
|
||||
"StrInput",
|
||||
"TableInput",
|
||||
"DefaultPromptField",
|
||||
"LinkInput",
|
||||
"SliderInput",
|
||||
]
|
||||
|
|
|
|||
49
src/frontend/package-lock.json
generated
49
src/frontend/package-lock.json
generated
|
|
@ -24,6 +24,7 @@
|
|||
"@radix-ui/react-progress": "^1.0.3",
|
||||
"@radix-ui/react-select": "^2.0.0",
|
||||
"@radix-ui/react-separator": "^1.0.3",
|
||||
"@radix-ui/react-slider": "^1.2.1",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
|
|
@ -3921,6 +3922,54 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slider": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.2.1.tgz",
|
||||
"integrity": "sha512-bEzQoDW0XP+h/oGbutF5VMWJPAl/UU8IJjr7h02SOHDIIIxq+cep8nItVNoBV+OMmahCdqdF38FTpmXoqQUGvw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/number": "1.1.0",
|
||||
"@radix-ui/primitive": "1.1.0",
|
||||
"@radix-ui/react-collection": "1.1.0",
|
||||
"@radix-ui/react-compose-refs": "1.1.0",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-direction": "1.1.0",
|
||||
"@radix-ui/react-primitive": "2.0.0",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0",
|
||||
"@radix-ui/react-use-previous": "1.1.0",
|
||||
"@radix-ui/react-use-size": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-context": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz",
|
||||
"integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
"@radix-ui/react-progress": "^1.0.3",
|
||||
"@radix-ui/react-select": "^2.0.0",
|
||||
"@radix-ui/react-separator": "^1.0.3",
|
||||
"@radix-ui/react-slider": "^1.2.1",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { handleOnNewValueType } from "@/CustomNodes/hooks/use-handle-new-value";
|
||||
import { TEXT_FIELD_TYPES } from "@/constants/constants";
|
||||
import { APIClassType, InputFieldType } from "@/types/api";
|
||||
import { Slider } from "@radix-ui/react-slider";
|
||||
import { useMemo } from "react";
|
||||
import SliderComponent from "../sliderComponent";
|
||||
import TableNodeComponent from "./components/TableNodeComponent";
|
||||
import CodeAreaComponent from "./components/codeAreaComponent";
|
||||
import DictComponent from "./components/dictComponent";
|
||||
|
|
@ -159,6 +161,22 @@ export function ParameterRenderComponent({
|
|||
tableTitle={templateData?.display_name ?? "Table"}
|
||||
/>
|
||||
);
|
||||
case "slider":
|
||||
return (
|
||||
<SliderComponent
|
||||
{...baseInputProps}
|
||||
value={templateValue}
|
||||
rangeSpec={templateData.range_spec}
|
||||
minLabel={templateData?.min_label}
|
||||
maxLabel={templateData?.max_label}
|
||||
minLabelIcon={templateData?.min_label_icon}
|
||||
maxLabelIcon={templateData?.max_label_icon}
|
||||
sliderButtons={templateData?.slider_buttons}
|
||||
sliderButtonsOptions={templateData?.slider_buttons_options}
|
||||
sliderInput={templateData?.slider_input}
|
||||
id={`slider_${id}`}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return <EmptyParameterComponent {...baseInputProps} />;
|
||||
}
|
||||
|
|
|
|||
254
src/frontend/src/components/sliderComponent/index.tsx
Normal file
254
src/frontend/src/components/sliderComponent/index.tsx
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
import { Case } from "@/shared/components/caseComponent";
|
||||
import { useDarkStore } from "@/stores/darkStore";
|
||||
import * as SliderPrimitive from "@radix-ui/react-slider";
|
||||
import clsx from "clsx";
|
||||
import { useEffect } from "react";
|
||||
import { SliderComponentType } from "../../types/components";
|
||||
import IconComponent from "../genericIconComponent";
|
||||
import { InputProps } from "../parameterRenderComponent/types";
|
||||
import { getMinOrMaxValue } from "./utils/get-min-max-value";
|
||||
|
||||
const THRESHOLDS = [0.25, 0.5, 0.75, 1];
|
||||
const BACKGROUND_COLORS = ["#4f46e5", "#7c3aed", "#a21caf", "#c026d3"];
|
||||
const TEXT_COLORS = ["#fff", "#fff", "#fff", "#fff"];
|
||||
const PERCENTAGES = [0.125, 0.375, 0.625, 0.875];
|
||||
|
||||
const DARK_COLOR_BACKGROUND = "#09090b";
|
||||
const DARK_COLOR_TEXT = "#52525b";
|
||||
const LIGHT_COLOR_BACKGROUND = "#e4e4e7";
|
||||
const LIGHT_COLOR_TEXT = "#000";
|
||||
|
||||
const DEFAULT_SLIDER_BUTTONS_OPTIONS = [
|
||||
{ id: 0, label: "Precise" },
|
||||
{ id: 1, label: "Balanced" },
|
||||
{ id: 2, label: "Creative" },
|
||||
{ id: 3, label: "Wild" },
|
||||
];
|
||||
|
||||
const MIN_LABEL = "Precise";
|
||||
const MAX_LABEL = "Wild";
|
||||
const MIN_LABEL_ICON = "pencil-ruler";
|
||||
const MAX_LABEL_ICON = "palette";
|
||||
|
||||
type ColorType = "background" | "text";
|
||||
|
||||
export default function SliderComponent({
|
||||
value,
|
||||
disabled,
|
||||
rangeSpec,
|
||||
editNode = false,
|
||||
minLabel = MIN_LABEL,
|
||||
maxLabel = MAX_LABEL,
|
||||
minLabelIcon = MIN_LABEL_ICON,
|
||||
maxLabelIcon = MAX_LABEL_ICON,
|
||||
sliderButtons = false,
|
||||
sliderButtonsOptions = DEFAULT_SLIDER_BUTTONS_OPTIONS,
|
||||
sliderInput = false,
|
||||
handleOnNewValue,
|
||||
}: InputProps<string[] | number[], SliderComponentType>): JSX.Element {
|
||||
const min = rangeSpec?.min ?? -2;
|
||||
const max = rangeSpec?.max ?? 2;
|
||||
|
||||
sliderButtonsOptions =
|
||||
sliderButtons && sliderButtonsOptions && sliderButtonsOptions.length > 0
|
||||
? sliderButtonsOptions
|
||||
: DEFAULT_SLIDER_BUTTONS_OPTIONS;
|
||||
|
||||
minLabelIcon = minLabelIcon || MIN_LABEL_ICON;
|
||||
maxLabelIcon = maxLabelIcon || MAX_LABEL_ICON;
|
||||
minLabel = minLabel || MIN_LABEL;
|
||||
maxLabel = maxLabel || MAX_LABEL;
|
||||
|
||||
const valueAsNumber = getMinOrMaxValue(Number(value), min, max);
|
||||
const step = rangeSpec?.step ?? 0.1;
|
||||
|
||||
useEffect(() => {
|
||||
if (disabled && value !== "") {
|
||||
handleOnNewValue({ value: "" }, { skipSnapshot: true });
|
||||
}
|
||||
}, [disabled]);
|
||||
|
||||
const handleChange = (newValue: number[]) => {
|
||||
handleOnNewValue({ value: newValue[0] });
|
||||
};
|
||||
|
||||
const handleOptionClick = (option: number) => {
|
||||
const selectedPercentage = PERCENTAGES[option];
|
||||
|
||||
if (selectedPercentage !== undefined) {
|
||||
const calculatedValue = min + (max - min) * selectedPercentage;
|
||||
handleOnNewValue({ value: calculatedValue });
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const isDark = useDarkStore((state) => state.dark);
|
||||
|
||||
const getNormalizedValue = (
|
||||
value: number,
|
||||
min: number,
|
||||
max: number,
|
||||
): number => {
|
||||
return (value - min) / (max - min);
|
||||
};
|
||||
|
||||
const getColor = (
|
||||
optionValue: number,
|
||||
normalizedValue: number,
|
||||
colorType: ColorType,
|
||||
): string => {
|
||||
const colors = colorType === "background" ? BACKGROUND_COLORS : TEXT_COLORS;
|
||||
const defaultColor = isDark
|
||||
? colorType === "background"
|
||||
? DARK_COLOR_BACKGROUND
|
||||
: DARK_COLOR_TEXT
|
||||
: colorType === "background"
|
||||
? LIGHT_COLOR_BACKGROUND
|
||||
: LIGHT_COLOR_TEXT;
|
||||
|
||||
if (normalizedValue <= THRESHOLDS[0] && optionValue === 0) {
|
||||
return colors[0];
|
||||
}
|
||||
|
||||
for (let i = 1; i < THRESHOLDS.length; i++) {
|
||||
if (
|
||||
normalizedValue > THRESHOLDS[i - 1] &&
|
||||
normalizedValue <= THRESHOLDS[i] &&
|
||||
optionValue === i
|
||||
) {
|
||||
return colors[i];
|
||||
}
|
||||
}
|
||||
|
||||
return defaultColor;
|
||||
};
|
||||
|
||||
const getButtonBackground = (optionValue: number = 0): string => {
|
||||
const normalizedValue = getNormalizedValue(valueAsNumber, min, max);
|
||||
return getColor(optionValue, normalizedValue, "background");
|
||||
};
|
||||
|
||||
const getButtonTextColor = (optionValue: number = 0): string => {
|
||||
const normalizedValue = getNormalizedValue(valueAsNumber, min, max);
|
||||
return getColor(optionValue, normalizedValue, "text");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full rounded-lg pb-2">
|
||||
<Case condition={!sliderButtons && !sliderInput}>
|
||||
<div className="relative bottom-2 flex items-center justify-end">
|
||||
<span
|
||||
data-testid={`default_slider_display_value${editNode ? "_advanced" : ""}`}
|
||||
className="font-mono text-sm"
|
||||
>
|
||||
{valueAsNumber.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
</Case>
|
||||
<Case condition={sliderButtons && !sliderInput}>
|
||||
<div className="relative bottom-1 flex items-center pb-2">
|
||||
<span
|
||||
data-testid={`button_slider_display_value${editNode ? "_advanced" : ""}`}
|
||||
className="font-mono text-2xl"
|
||||
>
|
||||
{valueAsNumber.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
</Case>
|
||||
|
||||
<div className="flex cursor-default items-center justify-center">
|
||||
<SliderPrimitive.Root
|
||||
className="relative flex h-5 w-full touch-none select-none items-center"
|
||||
value={[valueAsNumber]}
|
||||
onValueChange={handleChange}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SliderPrimitive.Track
|
||||
data-testid={`slider_track${editNode ? "_advanced" : ""}`}
|
||||
className={clsx(
|
||||
"relative h-1 w-full grow rounded-full",
|
||||
isDark ? "bg-zinc-800" : "bg-zinc-200",
|
||||
)}
|
||||
>
|
||||
<SliderPrimitive.Range className="absolute h-full rounded-full bg-gradient-to-r from-indigo-600 to-pink-500" />
|
||||
</SliderPrimitive.Track>
|
||||
<SliderPrimitive.Thumb
|
||||
data-testid={`slider_thumb${editNode ? "_advanced" : ""}`}
|
||||
className={clsx(
|
||||
"block h-6 w-6 rounded-full border-2 border-muted bg-pink-500 shadow-lg",
|
||||
)}
|
||||
/>
|
||||
</SliderPrimitive.Root>
|
||||
{sliderInput && (
|
||||
<input
|
||||
data-testid={`slider_input_value${editNode ? "_advanced" : ""}`}
|
||||
type="number"
|
||||
value={valueAsNumber.toFixed(2)}
|
||||
onChange={(e) => handleChange([parseFloat(e.target.value)])}
|
||||
className={clsx(
|
||||
"ml-2 h-10 w-16 rounded-md border px-2 py-1 text-sm arrow-hide",
|
||||
isDark
|
||||
? "border-zinc-700 bg-zinc-800 text-white"
|
||||
: "border-zinc-300 bg-white text-black",
|
||||
)}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{sliderButtons && (
|
||||
<div className="my-3">
|
||||
<div
|
||||
className={clsx(
|
||||
"flex rounded-md",
|
||||
isDark ? "bg-zinc-950" : "bg-zinc-200",
|
||||
)}
|
||||
>
|
||||
{sliderButtonsOptions?.map((option) => (
|
||||
<button
|
||||
key={option.id}
|
||||
onClick={() => handleOptionClick(option.id)}
|
||||
style={{
|
||||
background: getButtonBackground(option.id),
|
||||
color: getButtonTextColor(option.id),
|
||||
}}
|
||||
className={clsx(
|
||||
"h-9 flex-1 rounded-md px-3 py-1.5 text-xs font-medium transition-colors duration-200",
|
||||
)}
|
||||
disabled={disabled}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-2 grid grid-cols-2 gap-x-2 text-sm text-gray-500">
|
||||
<div className="flex items-center">
|
||||
<IconComponent
|
||||
className="mr-1 h-4 w-4"
|
||||
name={minLabelIcon}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span data-testid="min_label">{minLabel}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-end">
|
||||
<span data-testid="max_label">{maxLabel}</span>
|
||||
<IconComponent
|
||||
className="ml-1 h-4 w-4"
|
||||
name={maxLabelIcon}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
export const getMinOrMaxValue = (
|
||||
valueAsNumber: number,
|
||||
min: number,
|
||||
max: number,
|
||||
) => {
|
||||
if (valueAsNumber < min) {
|
||||
return min;
|
||||
}
|
||||
if (valueAsNumber > max) {
|
||||
return max;
|
||||
}
|
||||
if (valueAsNumber >= min && valueAsNumber <= max) {
|
||||
return valueAsNumber;
|
||||
}
|
||||
return min;
|
||||
};
|
||||
27
src/frontend/src/components/ui/slider.tsx
Normal file
27
src/frontend/src/components/ui/slider.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
"use client";
|
||||
|
||||
import { cn } from "@/utils/utils";
|
||||
import * as SliderPrimitive from "@radix-ui/react-slider";
|
||||
import * as React from "react";
|
||||
|
||||
const Slider = React.forwardRef<
|
||||
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SliderPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full touch-none select-none items-center",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
|
||||
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
||||
</SliderPrimitive.Track>
|
||||
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
|
||||
</SliderPrimitive.Root>
|
||||
));
|
||||
Slider.displayName = SliderPrimitive.Root.displayName;
|
||||
|
||||
export { Slider };
|
||||
|
|
@ -651,6 +651,7 @@ export const LANGFLOW_SUPPORTED_TYPES = new Set([
|
|||
"NestedDict",
|
||||
"table",
|
||||
"link",
|
||||
"slider",
|
||||
]);
|
||||
|
||||
export const priorityFields = new Set(["code", "template"]);
|
||||
|
|
|
|||
|
|
@ -153,6 +153,37 @@ export type IntComponentType = {
|
|||
id?: string;
|
||||
};
|
||||
|
||||
export type FloatComponentType = {
|
||||
value: string;
|
||||
disabled?: boolean;
|
||||
onChange: (
|
||||
value: string | number,
|
||||
dbValue?: boolean,
|
||||
skipSnapshot?: boolean,
|
||||
) => void;
|
||||
rangeSpec: RangeSpecType;
|
||||
editNode?: boolean;
|
||||
id?: string;
|
||||
};
|
||||
|
||||
export type SliderComponentType = {
|
||||
value: string;
|
||||
disabled?: boolean;
|
||||
rangeSpec: RangeSpecType;
|
||||
editNode?: boolean;
|
||||
id?: string;
|
||||
minLabel?: string;
|
||||
maxLabel?: string;
|
||||
minLabelIcon?: string;
|
||||
maxLabelIcon?: string;
|
||||
sliderButtons?: boolean;
|
||||
sliderButtonsOptions?: {
|
||||
label: string;
|
||||
id: number;
|
||||
}[];
|
||||
sliderInput?: boolean;
|
||||
};
|
||||
|
||||
export type FilePreviewType = {
|
||||
loading: boolean;
|
||||
file: File;
|
||||
|
|
|
|||
166
src/frontend/tests/core/unit/sliderComponent.spec.ts
Normal file
166
src/frontend/tests/core/unit/sliderComponent.spec.ts
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
import { expect, Page, test } from "@playwright/test";
|
||||
import uaParser from "ua-parser-js";
|
||||
|
||||
test("user should be able to use slider input", 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();
|
||||
}
|
||||
|
||||
const getUA = await page.evaluate(() => navigator.userAgent);
|
||||
const userAgentInfo = uaParser(getUA);
|
||||
let control = "Control";
|
||||
|
||||
if (userAgentInfo.os.name.includes("Mac")) {
|
||||
control = "Meta";
|
||||
}
|
||||
|
||||
await page.waitForSelector('[data-testid="blank-flow"]', {
|
||||
timeout: 30000,
|
||||
});
|
||||
await page.getByTestId("blank-flow").click();
|
||||
await page.waitForSelector('[data-testid="extended-disclosure"]', {
|
||||
timeout: 30000,
|
||||
});
|
||||
await page.getByTestId("extended-disclosure").click();
|
||||
await page.getByPlaceholder("Search").click();
|
||||
await page.getByPlaceholder("Search").fill("ollama");
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page
|
||||
.getByTestId("modelsOllama")
|
||||
.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.getByTestId("title-Ollama").click();
|
||||
await page.getByTestId("code-button-modal").click();
|
||||
|
||||
let cleanCode = await extractAndCleanCode(page);
|
||||
|
||||
// Replace the import statement
|
||||
cleanCode = cleanCode.replace("FloatInput(", "SliderInput(");
|
||||
cleanCode = cleanCode.replace(
|
||||
"from langflow.io import BoolInput, DictInput, DropdownInput, FloatInput, IntInput, StrInput",
|
||||
"from langflow.io import BoolInput, DictInput, DropdownInput, FloatInput, IntInput, StrInput, SliderInput",
|
||||
);
|
||||
|
||||
cleanCode = cleanCode.replace(
|
||||
"value=0.2,",
|
||||
"value=0.2, range_spec=RangeSpec(min=3, max=30, step=1), min_label='test', max_label='test2', min_label_icon='pencil-ruler', max_label_icon='palette', slider_buttons=False, slider_buttons_options=[], slider_input=False,",
|
||||
);
|
||||
|
||||
await page.locator("textarea").last().press(`${control}+a`);
|
||||
await page.keyboard.press("Backspace");
|
||||
await page.locator("textarea").last().fill(cleanCode);
|
||||
await page.locator('//*[@id="checkAndSaveBtn"]').click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.getByTitle("fit view").click();
|
||||
|
||||
await mutualValidation(page);
|
||||
|
||||
await moveSlider(page, "right", false);
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.getByTitle("zoom out").click();
|
||||
|
||||
await page.getByTestId("more-options-modal").click();
|
||||
await page.getByText("Advanced", { exact: true }).click();
|
||||
await expect(
|
||||
page.getByTestId("default_slider_display_value_advanced"),
|
||||
).toHaveText("19.00");
|
||||
|
||||
await moveSlider(page, "left", true);
|
||||
// Wait for any potential updates
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await expect(
|
||||
page.getByTestId("default_slider_display_value_advanced"),
|
||||
).toHaveText("14.00");
|
||||
|
||||
await page.getByText("Close").last().click();
|
||||
|
||||
await expect(page.getByTestId("default_slider_display_value")).toHaveText(
|
||||
"14.00",
|
||||
);
|
||||
});
|
||||
|
||||
async function extractAndCleanCode(page: Page): Promise<string> {
|
||||
const outerHTML = await page
|
||||
.locator('//*[@id="codeValue"]')
|
||||
.evaluate((el) => el.outerHTML);
|
||||
|
||||
const valueMatch = outerHTML.match(/value="([\s\S]*?)"/);
|
||||
if (!valueMatch) {
|
||||
throw new Error("Could not find value attribute in the HTML");
|
||||
}
|
||||
|
||||
let codeContent = valueMatch[1]
|
||||
.replace(/"/g, '"')
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/'/g, "'")
|
||||
.replace(///g, "/");
|
||||
|
||||
return codeContent;
|
||||
}
|
||||
|
||||
async function mutualValidation(page: Page) {
|
||||
await expect(page.getByTestId("default_slider_display_value")).toHaveText(
|
||||
"3.00",
|
||||
);
|
||||
await expect(page.getByTestId("min_label")).toHaveText("test");
|
||||
await expect(page.getByTestId("max_label")).toHaveText("test2");
|
||||
await expect(page.getByTestId("icon-pencil-ruler")).toBeVisible();
|
||||
await expect(page.getByTestId("icon-palette")).toBeVisible();
|
||||
}
|
||||
async function moveSlider(
|
||||
page: Page,
|
||||
side: "left" | "right",
|
||||
advanced: boolean = false,
|
||||
) {
|
||||
const thumbSelector = `slider_thumb${advanced ? "_advanced" : ""}`;
|
||||
const trackSelector = `slider_track${advanced ? "_advanced" : ""}`;
|
||||
|
||||
await page.getByTestId(thumbSelector).click();
|
||||
|
||||
const trackBoundingBox = await page.getByTestId(trackSelector).boundingBox();
|
||||
|
||||
if (trackBoundingBox) {
|
||||
const moveDistance =
|
||||
trackBoundingBox.width * 0.1 * (side === "left" ? -1 : 1);
|
||||
const centerX = trackBoundingBox.x + trackBoundingBox.width / 2;
|
||||
const centerY = trackBoundingBox.y + trackBoundingBox.height / 2;
|
||||
|
||||
await page.mouse.move(centerX + moveDistance, centerY);
|
||||
await page.mouse.down();
|
||||
await page.mouse.up();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue