feat: enhance slider component with labels and improved input handling (#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
This commit is contained in:
parent
78081be782
commit
b2691eebce
9 changed files with 215 additions and 54 deletions
|
|
@ -70,7 +70,9 @@ class OpenAIModelComponent(LCModelComponent):
|
||||||
advanced=False,
|
advanced=False,
|
||||||
value="OPENAI_API_KEY",
|
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(
|
IntInput(
|
||||||
name="seed",
|
name="seed",
|
||||||
display_name="Seed",
|
display_name="Seed",
|
||||||
|
|
|
||||||
|
|
@ -191,3 +191,9 @@ code {
|
||||||
.ag-cell-wrapper > *:not(.ag-cell-value):not(.ag-group-value) {
|
.ag-cell-wrapper > *:not(.ag-cell-value):not(.ag-group-value) {
|
||||||
--ag-internal-calculated-line-height: none !important;
|
--ag-internal-calculated-line-height: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.arrow-hide::-webkit-inner-spin-button,
|
||||||
|
.arrow-hide::-webkit-outer-spin-button {
|
||||||
|
appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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}%)`;
|
||||||
|
};
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import IconComponent from "@/components/common/genericIconComponent";
|
import { getMinOrMaxValue } from "@/components/core/parameterRenderComponent/components/sliderComponent/helpers/get-min-max-value";
|
||||||
import { getMinOrMaxValue } from "@/components/core/parameterRenderComponent/components/sliderComponent/utils/get-min-max-value";
|
|
||||||
import { InputProps } from "@/components/core/parameterRenderComponent/types";
|
import { InputProps } from "@/components/core/parameterRenderComponent/types";
|
||||||
import { Case } from "@/shared/components/caseComponent";
|
import { Case } from "@/shared/components/caseComponent";
|
||||||
import { useDarkStore } from "@/stores/darkStore";
|
import { useDarkStore } from "@/stores/darkStore";
|
||||||
import { SliderComponentType } from "@/types/components";
|
import { SliderComponentType } from "@/types/components";
|
||||||
import * as SliderPrimitive from "@radix-ui/react-slider";
|
import * as SliderPrimitive from "@radix-ui/react-slider";
|
||||||
import clsx from "clsx";
|
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 THRESHOLDS = [0.25, 0.5, 0.75, 1];
|
||||||
const BACKGROUND_COLORS = ["#4f46e5", "#7c3aed", "#a21caf", "#c026d3"];
|
const BACKGROUND_COLORS = ["#4f46e5", "#7c3aed", "#a21caf", "#c026d3"];
|
||||||
|
|
@ -26,10 +27,13 @@ const DEFAULT_SLIDER_BUTTONS_OPTIONS = [
|
||||||
];
|
];
|
||||||
|
|
||||||
const MIN_LABEL = "Precise";
|
const MIN_LABEL = "Precise";
|
||||||
const MAX_LABEL = "Wild";
|
const MAX_LABEL = "Creative";
|
||||||
const MIN_LABEL_ICON = "pencil-ruler";
|
const MIN_LABEL_ICON = "pencil-ruler";
|
||||||
const MAX_LABEL_ICON = "palette";
|
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";
|
type ColorType = "background" | "text";
|
||||||
|
|
||||||
export default function SliderComponent({
|
export default function SliderComponent({
|
||||||
|
|
@ -43,7 +47,6 @@ export default function SliderComponent({
|
||||||
maxLabelIcon = MAX_LABEL_ICON,
|
maxLabelIcon = MAX_LABEL_ICON,
|
||||||
sliderButtons = false,
|
sliderButtons = false,
|
||||||
sliderButtonsOptions = DEFAULT_SLIDER_BUTTONS_OPTIONS,
|
sliderButtonsOptions = DEFAULT_SLIDER_BUTTONS_OPTIONS,
|
||||||
sliderInput = false,
|
|
||||||
handleOnNewValue,
|
handleOnNewValue,
|
||||||
}: InputProps<string[] | number[], SliderComponentType>): JSX.Element {
|
}: InputProps<string[] | number[], SliderComponentType>): JSX.Element {
|
||||||
const min = rangeSpec?.min ?? -2;
|
const min = rangeSpec?.min ?? -2;
|
||||||
|
|
@ -60,7 +63,7 @@ export default function SliderComponent({
|
||||||
maxLabel = maxLabel || MAX_LABEL;
|
maxLabel = maxLabel || MAX_LABEL;
|
||||||
|
|
||||||
const valueAsNumber = getMinOrMaxValue(Number(value), min, max);
|
const valueAsNumber = getMinOrMaxValue(Number(value), min, max);
|
||||||
const step = rangeSpec?.step ?? 0.1;
|
const step = rangeSpec?.step ?? 0.01;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (disabled && value !== "") {
|
if (disabled && value !== "") {
|
||||||
|
|
@ -134,19 +137,102 @@ export default function SliderComponent({
|
||||||
return getColor(optionValue, normalizedValue, "text");
|
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 (
|
return (
|
||||||
<div className="w-full rounded-lg pb-2">
|
<div className="w-full rounded-lg pb-2">
|
||||||
<Case condition={!sliderButtons && !sliderInput}>
|
<Case condition={!sliderButtons}>
|
||||||
<div className="relative bottom-2 flex items-center justify-end">
|
<div className="noflow nowheel nopan nodelete nodrag flex items-center justify-end">
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"input-slider-text",
|
||||||
|
(isGrabbing || isEditing) && ringClassInputClass,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{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
|
<span
|
||||||
|
onClick={() => {
|
||||||
|
setIsEditing(true);
|
||||||
|
setInputValue(valueAsNumber.toFixed(2));
|
||||||
|
}}
|
||||||
data-testid={`default_slider_display_value${editNode ? "_advanced" : ""}`}
|
data-testid={`default_slider_display_value${editNode ? "_advanced" : ""}`}
|
||||||
className="font-mono text-sm"
|
className="relative bottom-[1px] font-mono text-sm hover:cursor-text"
|
||||||
>
|
>
|
||||||
{valueAsNumber.toFixed(2)}
|
{valueAsNumber.toFixed(2)}
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Case>
|
</Case>
|
||||||
<Case condition={sliderButtons && !sliderInput}>
|
<Case condition={sliderButtons}>
|
||||||
<div className="relative bottom-1 flex items-center pb-2">
|
<div className="relative bottom-1 flex items-center pb-2">
|
||||||
<span
|
<span
|
||||||
data-testid={`button_slider_display_value${editNode ? "_advanced" : ""}`}
|
data-testid={`button_slider_display_value${editNode ? "_advanced" : ""}`}
|
||||||
|
|
@ -174,30 +260,28 @@ export default function SliderComponent({
|
||||||
isDark ? "bg-muted" : "bg-border",
|
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.Track>
|
||||||
<SliderPrimitive.Thumb
|
<SliderPrimitive.Thumb
|
||||||
data-testid={`slider_thumb${editNode ? "_advanced" : ""}`}
|
data-testid={`slider_thumb${editNode ? "_advanced" : ""}`}
|
||||||
className={clsx(
|
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>
|
</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>
|
</div>
|
||||||
|
|
||||||
{sliderButtons && (
|
{sliderButtons && (
|
||||||
|
|
@ -223,28 +307,12 @@ export default function SliderComponent({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="text mt-2 grid grid-cols-2 gap-x-2 text-sm">
|
<SliderLabels
|
||||||
<div className="flex items-center">
|
minLabel={minLabel}
|
||||||
<IconComponent
|
maxLabel={maxLabel}
|
||||||
className="mr-1 h-4 w-4 text-placeholder-foreground"
|
minLabelIcon={minLabelIcon}
|
||||||
name={minLabelIcon}
|
maxLabelIcon={maxLabelIcon}
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1272,6 +1272,10 @@
|
||||||
.playground-btn-flow-toolbar {
|
.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;
|
@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 */
|
/* Gradient background */
|
||||||
|
|
|
||||||
|
|
@ -162,6 +162,8 @@
|
||||||
|
|
||||||
--tool-mode-gradient-1: #f480ff;
|
--tool-mode-gradient-1: #f480ff;
|
||||||
--tool-mode-gradient-2: #ff3276;
|
--tool-mode-gradient-2: #ff3276;
|
||||||
|
|
||||||
|
--slider-input-border: #d4d4d8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
|
|
@ -313,5 +315,7 @@
|
||||||
--datatype-orange-foreground: 30.7 97.2% 72.4%;
|
--datatype-orange-foreground: 30.7 97.2% 72.4%;
|
||||||
|
|
||||||
--node-ring: 240 6% 90%;
|
--node-ring: 240 6% 90%;
|
||||||
|
|
||||||
|
--slider-input-border: #d4d4d8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -256,6 +256,7 @@ const config = {
|
||||||
"holo-frost": "hsl(var(--holo-frost))",
|
"holo-frost": "hsl(var(--holo-frost))",
|
||||||
"terminal-green": "hsl(var(--terminal-green))",
|
"terminal-green": "hsl(var(--terminal-green))",
|
||||||
"cosmic-void": "hsl(var(--cosmic-void))",
|
"cosmic-void": "hsl(var(--cosmic-void))",
|
||||||
|
"slider-input-border": "var(--slider-input-border)",
|
||||||
},
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
lg: `var(--radius)`,
|
lg: `var(--radius)`,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue