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,
|
||||
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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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/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<string[] | number[], SliderComponentType>): 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<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" : ""}`}
|
||||
|
|
@ -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 && (
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)`,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue