From b2691eebce291670c1425f4e316dde51cbe087e1 Mon Sep 17 00:00:00 2001 From: Cristhian Zanforlin Lousa Date: Fri, 6 Dec 2024 10:02:59 -0300 Subject: [PATCH] feat: enhance slider component with labels and improved input handling (#5065) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 📝 (openai.py): Update temperature slider range from 0-1 to 0-2 with step 0.01 for more precise control 📝 (sliderComponent/index.tsx): Update step value for slider component from 0.1 to 0.01 for smoother and more accurate adjustments 📝 (sliderComponent/index.tsx): Update MAX_LABEL from "Wild" to "Creative" for better semantic representation 📝 (sliderComponent/index.tsx): Add cursor-grabbing style when slider thumb is being grabbed to improve user experience * 📝 (sliderComponent/index.tsx): improve styling and structure of the SliderComponent by refactoring the display value element to use a div container with appropriate classes and styles. * 📝 (App.css): Add styles to hide spin buttons in input[type=number] elements for better UX 🔧 (sliderComponent/index.tsx): Add input element to allow users to directly edit the slider value for improved user experience * ✨ (slider-labels.tsx): Add SliderLabels component to display min and max labels with icons in SliderComponent for better user experience 📝 (index.tsx): Remove sliderInput prop and refactor SliderComponent to improve code readability and maintainability 🔧 (applies.css): Add styling for input-slider-text class to improve consistency in SliderComponent styling * 📝 (applies.css): update hover:ring value to use variable hover:ring-slider-input-border for consistency and maintainability 📝 (index.css): add variable --slider-input-border to define the color value for slider input border 🔧 (tailwind.config.mjs): add slider-input-border custom property to map to the defined color value in the CSS variables * ✨ (build-color-by-name.ts): add function to dynamically build color based on input values to customize UI ✨ (get-min-max-value.ts): add function to get minimum or maximum value based on input constraints 🔧 (index.tsx): update import path for getMinOrMaxValue function 🔧 (index.tsx): add buildColorByName function to dynamically set thumb color based on percentage 🔧 (index.tsx): add logic to dynamically set background color gradient based on thumb color and percentage 🔧 (index.tsx): add logic to dynamically set thumb background color based on percentage and color calculation --- .../base/langflow/components/models/openai.py | 4 +- src/frontend/src/App.css | 6 + .../components/slider-labels.tsx | 46 +++++ .../helpers/build-color-by-name.ts | 30 +++ .../{utils => helpers}/get-min-max-value.ts | 0 .../components/sliderComponent/index.tsx | 174 ++++++++++++------ src/frontend/src/style/applies.css | 4 + src/frontend/src/style/index.css | 4 + src/frontend/tailwind.config.mjs | 1 + 9 files changed, 215 insertions(+), 54 deletions(-) create mode 100644 src/frontend/src/components/core/parameterRenderComponent/components/sliderComponent/components/slider-labels.tsx create mode 100644 src/frontend/src/components/core/parameterRenderComponent/components/sliderComponent/helpers/build-color-by-name.ts rename src/frontend/src/components/core/parameterRenderComponent/components/sliderComponent/{utils => helpers}/get-min-max-value.ts (100%) diff --git a/src/backend/base/langflow/components/models/openai.py b/src/backend/base/langflow/components/models/openai.py index 92d216250f22..60f960a73109 100644 --- a/src/backend/base/langflow/components/models/openai.py +++ b/src/backend/base/langflow/components/models/openai.py @@ -70,7 +70,9 @@ class OpenAIModelComponent(LCModelComponent): advanced=False, value="OPENAI_API_KEY", ), - SliderInput(name="temperature", display_name="Temperature", value=0.1, range_spec=RangeSpec(min=0, max=1)), + SliderInput( + name="temperature", display_name="Temperature", value=0.1, range_spec=RangeSpec(min=0, max=2, step=0.01) + ), IntInput( name="seed", display_name="Seed", diff --git a/src/frontend/src/App.css b/src/frontend/src/App.css index 2cd6f1d451e9..f099a3aba0fa 100644 --- a/src/frontend/src/App.css +++ b/src/frontend/src/App.css @@ -191,3 +191,9 @@ code { .ag-cell-wrapper > *:not(.ag-cell-value):not(.ag-group-value) { --ag-internal-calculated-line-height: none !important; } + +.arrow-hide::-webkit-inner-spin-button, +.arrow-hide::-webkit-outer-spin-button { + appearance: none; + margin: 0; +} diff --git a/src/frontend/src/components/core/parameterRenderComponent/components/sliderComponent/components/slider-labels.tsx b/src/frontend/src/components/core/parameterRenderComponent/components/sliderComponent/components/slider-labels.tsx new file mode 100644 index 000000000000..e85aa45b3efd --- /dev/null +++ b/src/frontend/src/components/core/parameterRenderComponent/components/sliderComponent/components/slider-labels.tsx @@ -0,0 +1,46 @@ +import IconComponent from "@/components/common/genericIconComponent"; + +export const SliderLabels = ({ + minLabel, + maxLabel, + minLabelIcon, + maxLabelIcon, +}: { + minLabel: string; + maxLabel: string; + minLabelIcon: string; + maxLabelIcon: string; +}) => { + return ( + <> +
+
+
+
+ + {maxLabel} + +
+
+ + ); +}; diff --git a/src/frontend/src/components/core/parameterRenderComponent/components/sliderComponent/helpers/build-color-by-name.ts b/src/frontend/src/components/core/parameterRenderComponent/components/sliderComponent/helpers/build-color-by-name.ts new file mode 100644 index 000000000000..962c6c386f87 --- /dev/null +++ b/src/frontend/src/components/core/parameterRenderComponent/components/sliderComponent/helpers/build-color-by-name.ts @@ -0,0 +1,30 @@ +export const buildColorByName = ( + accentIndigoForeground: string, + accentPinkForeground: string, + percentage: number, +) => { + const startHue = parseInt(accentIndigoForeground.split(" ")[0]); + const endHue = parseInt(accentPinkForeground.split(" ")[0]); + + const startSaturation = parseInt( + accentIndigoForeground.split(" ")[1].replace("%", ""), + ); + const endSaturation = parseInt( + accentPinkForeground.split(" ")[1].replace("%", ""), + ); + + const startLightness = parseInt( + accentIndigoForeground.split(" ")[2].replace("%", ""), + ); + const endLightness = parseInt( + accentPinkForeground.split(" ")[2].replace("%", ""), + ); + + const hue = startHue + (endHue - startHue) * (percentage / 100); + const saturation = + startSaturation + (endSaturation - startSaturation) * (percentage / 100); + const lightness = + startLightness + (endLightness - startLightness) * (percentage / 100); + + return `hsl(${hue}, ${saturation}%, ${lightness}%)`; +}; diff --git a/src/frontend/src/components/core/parameterRenderComponent/components/sliderComponent/utils/get-min-max-value.ts b/src/frontend/src/components/core/parameterRenderComponent/components/sliderComponent/helpers/get-min-max-value.ts similarity index 100% rename from src/frontend/src/components/core/parameterRenderComponent/components/sliderComponent/utils/get-min-max-value.ts rename to src/frontend/src/components/core/parameterRenderComponent/components/sliderComponent/helpers/get-min-max-value.ts diff --git a/src/frontend/src/components/core/parameterRenderComponent/components/sliderComponent/index.tsx b/src/frontend/src/components/core/parameterRenderComponent/components/sliderComponent/index.tsx index 522ef87b59ed..3bf71dcc882a 100644 --- a/src/frontend/src/components/core/parameterRenderComponent/components/sliderComponent/index.tsx +++ b/src/frontend/src/components/core/parameterRenderComponent/components/sliderComponent/index.tsx @@ -1,12 +1,13 @@ -import IconComponent from "@/components/common/genericIconComponent"; -import { getMinOrMaxValue } from "@/components/core/parameterRenderComponent/components/sliderComponent/utils/get-min-max-value"; +import { getMinOrMaxValue } from "@/components/core/parameterRenderComponent/components/sliderComponent/helpers/get-min-max-value"; import { InputProps } from "@/components/core/parameterRenderComponent/types"; import { Case } from "@/shared/components/caseComponent"; import { useDarkStore } from "@/stores/darkStore"; import { SliderComponentType } from "@/types/components"; import * as SliderPrimitive from "@radix-ui/react-slider"; import clsx from "clsx"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; +import { SliderLabels } from "./components/slider-labels"; +import { buildColorByName } from "./helpers/build-color-by-name"; const THRESHOLDS = [0.25, 0.5, 0.75, 1]; const BACKGROUND_COLORS = ["#4f46e5", "#7c3aed", "#a21caf", "#c026d3"]; @@ -26,10 +27,13 @@ const DEFAULT_SLIDER_BUTTONS_OPTIONS = [ ]; const MIN_LABEL = "Precise"; -const MAX_LABEL = "Wild"; +const MAX_LABEL = "Creative"; const MIN_LABEL_ICON = "pencil-ruler"; const MAX_LABEL_ICON = "palette"; +const DEFAULT_ACCENT_PINK_FOREGROUND_COLOR = "333 71% 51%"; +const DEFAULT_ACCENT_INDIGO_FOREGROUND_COLOR = "243 75% 59%"; + type ColorType = "background" | "text"; export default function SliderComponent({ @@ -43,7 +47,6 @@ export default function SliderComponent({ maxLabelIcon = MAX_LABEL_ICON, sliderButtons = false, sliderButtonsOptions = DEFAULT_SLIDER_BUTTONS_OPTIONS, - sliderInput = false, handleOnNewValue, }: InputProps): JSX.Element { const min = rangeSpec?.min ?? -2; @@ -60,7 +63,7 @@ export default function SliderComponent({ maxLabel = maxLabel || MAX_LABEL; const valueAsNumber = getMinOrMaxValue(Number(value), min, max); - const step = rangeSpec?.step ?? 0.1; + const step = rangeSpec?.step ?? 0.01; useEffect(() => { if (disabled && value !== "") { @@ -134,19 +137,102 @@ export default function SliderComponent({ return getColor(optionValue, normalizedValue, "text"); }; + const [isGrabbing, setIsGrabbing] = useState(false); + const [isEditing, setIsEditing] = useState(false); + const [inputValue, setInputValue] = useState(valueAsNumber.toFixed(2)); + + const handleInputChange = (e: React.ChangeEvent) => { + setInputValue(e.target.value); + }; + + const handleInputBlur = () => { + const newValue = parseFloat(inputValue); + if (!isNaN(newValue)) { + const clampedValue = Math.min(Math.max(newValue, min), max); + handleOnNewValue({ value: clampedValue }); + } + setIsEditing(false); + setInputValue(valueAsNumber.toFixed(2)); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + handleInputBlur(); + } else if (e.key === "Escape") { + setIsEditing(false); + setInputValue(valueAsNumber.toFixed(2)); + } + }; + + const percentage = ((valueAsNumber - min) / (max - min)) * 100; + + if (isDark) { + document.documentElement.classList.add("dark"); + } else { + document.documentElement.classList.remove("dark"); + } + + const accentIndigoForeground = getComputedStyle( + document.documentElement, + ).getPropertyValue("--accent-indigo-foreground"); + + const accentPinkForeground = getComputedStyle( + document.documentElement, + ).getPropertyValue("--accent-pink-foreground"); + + const getThumbColor = (percentage) => { + if (accentIndigoForeground && accentPinkForeground) { + return buildColorByName( + accentIndigoForeground, + accentPinkForeground, + percentage, + ); + } + return buildColorByName( + DEFAULT_ACCENT_INDIGO_FOREGROUND_COLOR, + DEFAULT_ACCENT_PINK_FOREGROUND_COLOR, + percentage, + ); + }; + + const ringClassInputClass = "ring-[1px] ring-slider-input-border"; + return (
- -
- +
+
- {valueAsNumber.toFixed(2)} - + {isEditing ? ( + + ) : ( + { + setIsEditing(true); + setInputValue(valueAsNumber.toFixed(2)); + }} + data-testid={`default_slider_display_value${editNode ? "_advanced" : ""}`} + className="relative bottom-[1px] font-mono text-sm hover:cursor-text" + > + {valueAsNumber.toFixed(2)} + + )} +
- +
- + setIsGrabbing(true)} + onPointerUp={() => setIsGrabbing(false)} + style={{ + backgroundColor: getThumbColor(percentage), + }} /> - {sliderInput && ( - handleChange([parseFloat(e.target.value)])} - className={clsx( - "primary-input ml-2 h-10 w-16 rounded-md border px-2 py-1 text-sm arrow-hide", - )} - min={min} - max={max} - step={step} - disabled={disabled} - /> - )}
{sliderButtons && ( @@ -223,28 +307,12 @@ export default function SliderComponent({
)} -
-
-
-
- - {maxLabel} - -
-
+
); } diff --git a/src/frontend/src/style/applies.css b/src/frontend/src/style/applies.css index 661a49f484f6..72f388a1741a 100644 --- a/src/frontend/src/style/applies.css +++ b/src/frontend/src/style/applies.css @@ -1272,6 +1272,10 @@ .playground-btn-flow-toolbar { @apply relative inline-flex h-8 w-full items-center justify-center gap-1.5 rounded px-3 py-1.5 text-sm font-semibold transition-all duration-500 ease-in-out; } + + .input-slider-text { + @apply absolute bottom-[4.2rem] right-3 w-14 cursor-text rounded-sm px-2 py-[1px] text-center hover:ring-[1px] hover:ring-slider-input-border; + } } /* Gradient background */ diff --git a/src/frontend/src/style/index.css b/src/frontend/src/style/index.css index 58c46731cf0e..156a39add25a 100644 --- a/src/frontend/src/style/index.css +++ b/src/frontend/src/style/index.css @@ -162,6 +162,8 @@ --tool-mode-gradient-1: #f480ff; --tool-mode-gradient-2: #ff3276; + + --slider-input-border: #d4d4d8; } .dark { @@ -313,5 +315,7 @@ --datatype-orange-foreground: 30.7 97.2% 72.4%; --node-ring: 240 6% 90%; + + --slider-input-border: #d4d4d8; } } diff --git a/src/frontend/tailwind.config.mjs b/src/frontend/tailwind.config.mjs index 3455040f0768..b3b8671035e7 100644 --- a/src/frontend/tailwind.config.mjs +++ b/src/frontend/tailwind.config.mjs @@ -256,6 +256,7 @@ const config = { "holo-frost": "hsl(var(--holo-frost))", "terminal-green": "hsl(var(--terminal-green))", "cosmic-void": "hsl(var(--cosmic-void))", + "slider-input-border": "var(--slider-input-border)", }, borderRadius: { lg: `var(--radius)`,