Skip to content

Commit

Permalink
feat: enhance slider component with labels and improved input handling (
Browse files Browse the repository at this point in the history
#5065)

* 📝 (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
  • Loading branch information
Cristhianzl authored Dec 6, 2024
1 parent 78081be commit b2691ee
Show file tree
Hide file tree
Showing 9 changed files with 215 additions and 54 deletions.
4 changes: 3 additions & 1 deletion src/backend/base/langflow/components/models/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 6 additions & 0 deletions src/frontend/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
@@ -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 (
<>
<div className="text mt-2 grid grid-cols-2 gap-x-2 text-sm">
<div className="flex items-center">
<IconComponent
className="mr-1 h-4 w-4 text-placeholder-foreground"
name={minLabelIcon}
aria-hidden="true"
/>
<span
data-testid="min_label"
className="text-xs text-placeholder-foreground"
>
{minLabel}
</span>
</div>
<div className="flex items-center justify-end">
<span
data-testid="max_label"
className="text-xs text-placeholder-foreground"
>
{maxLabel}
</span>
<IconComponent
className="ml-1 h-4 w-4 text-placeholder-foreground"
name={maxLabelIcon}
aria-hidden="true"
/>
</div>
</div>
</>
);
};
Original file line number Diff line number Diff line change
@@ -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}%)`;
};
Original file line number Diff line number Diff line change
@@ -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"];
Expand All @@ -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({
Expand All @@ -43,7 +47,6 @@ export default function SliderComponent({
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;
Expand All @@ -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 !== "") {
Expand Down Expand Up @@ -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<HTMLInputElement>) => {
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<HTMLInputElement>) => {
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 (
<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"
<Case condition={!sliderButtons}>
<div className="noflow nowheel nopan nodelete nodrag flex items-center justify-end">
<div
className={clsx(
"input-slider-text",
(isGrabbing || isEditing) && ringClassInputClass,
)}
>
{valueAsNumber.toFixed(2)}
</span>
{isEditing ? (
<input
type="number"
value={inputValue}
onChange={handleInputChange}
onBlur={handleInputBlur}
onKeyDown={handleKeyDown}
className="relative bottom-[1px] w-full cursor-text rounded-sm bg-transparent text-center font-mono text-[0.88rem] arrow-hide"
autoFocus
/>
) : (
<span
onClick={() => {
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)}
</span>
)}
</div>
</div>
</Case>
<Case condition={sliderButtons && !sliderInput}>
<Case condition={sliderButtons}>
<div className="relative bottom-1 flex items-center pb-2">
<span
data-testid={`button_slider_display_value${editNode ? "_advanced" : ""}`}
Expand Down Expand Up @@ -174,30 +260,28 @@ export default function SliderComponent({
isDark ? "bg-muted" : "bg-border",
)}
>
<SliderPrimitive.Range className="absolute h-full rounded-full bg-gradient-to-r from-indigo-600 to-pink-500" />
<SliderPrimitive.Range
className="absolute h-full rounded-full bg-gradient-to-r from-accent-indigo-foreground to-accent-pink-foreground"
style={{
width: `${percentage}%`,
background: `linear-gradient(to right, rgb(79, 70, 229) 0%, ${getThumbColor(percentage)} ${percentage}%)`,
}}
/>
</SliderPrimitive.Track>
<SliderPrimitive.Thumb
data-testid={`slider_thumb${editNode ? "_advanced" : ""}`}
className={clsx(
"block h-6 w-6 cursor-pointer rounded-full border-2 border-background bg-pink-500 shadow-lg",
"block h-6 w-6 rounded-full border-2 border-background shadow-lg",
isGrabbing ? "cursor-grabbing" : "cursor-grab",
valueAsNumber === max && "relative left-1",
)}
onPointerDown={() => setIsGrabbing(true)}
onPointerUp={() => setIsGrabbing(false)}
style={{
backgroundColor: getThumbColor(percentage),
}}
/>
</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(
"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}
/>
)}
</div>

{sliderButtons && (
Expand All @@ -223,28 +307,12 @@ export default function SliderComponent({
</div>
)}

<div className="text mt-2 grid grid-cols-2 gap-x-2 text-sm">
<div className="flex items-center">
<IconComponent
className="mr-1 h-4 w-4 text-placeholder-foreground"
name={minLabelIcon}
aria-hidden="true"
/>
<span data-testid="min_label" className="text-muted-foreground">
{minLabel}
</span>
</div>
<div className="flex items-center justify-end">
<span data-testid="max_label" className="text-muted-foreground">
{maxLabel}
</span>
<IconComponent
className="ml-1 h-4 w-4 text-placeholder-foreground"
name={maxLabelIcon}
aria-hidden="true"
/>
</div>
</div>
<SliderLabels
minLabel={minLabel}
maxLabel={maxLabel}
minLabelIcon={minLabelIcon}
maxLabelIcon={maxLabelIcon}
/>
</div>
);
}
4 changes: 4 additions & 0 deletions src/frontend/src/style/applies.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
4 changes: 4 additions & 0 deletions src/frontend/src/style/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@

--tool-mode-gradient-1: #f480ff;
--tool-mode-gradient-2: #ff3276;

--slider-input-border: #d4d4d8;
}

.dark {
Expand Down Expand Up @@ -313,5 +315,7 @@
--datatype-orange-foreground: 30.7 97.2% 72.4%;

--node-ring: 240 6% 90%;

--slider-input-border: #d4d4d8;
}
}
1 change: 1 addition & 0 deletions src/frontend/tailwind.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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)`,
Expand Down

0 comments on commit b2691ee

Please sign in to comment.