import React, { useState, useCallback, useRef, useEffect } from 'react'; import { useLocalize } from '~/hooks'; import { Minus, Plus } from 'lucide-react'; interface ModelParametersProps { label?: string; ariaLabel?: string; min?: number; max?: number; step?: number; stepClick?: number; initialValue?: number; showButtons?: boolean; onChange?: (value: number) => void; disabled?: boolean; } const ModelParameters: React.FC = ({ label = 'Value', ariaLabel = 'Value', min = 0, max = 100, step = 1, stepClick = 1, initialValue = 0, showButtons = true, onChange, disabled = false, }) => { const localize = useLocalize(); const [value, setValue] = useState(initialValue); const [isHovering, setIsHovering] = useState(false); const rangeRef = useRef(null); const id = `model-parameter-${ariaLabel.toLowerCase().replace(/\s+/g, '-')}`; const displayLabel = label.startsWith('com_') ? localize(label) : label; const getDecimalPlaces = (num: number) => { const match = ('' + num).match(/(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/); if (!match) { return 0; } return Math.max(0, (match[1] ? match[1].length : 0) - (match[2] ? +match[2] : 0)); }; const decimalPlaces = getDecimalPlaces(step); const handleChange = useCallback( (newValue: number) => { const clampedValue = Math.min(Math.max(newValue, min), max); const finalValue = Object.is(clampedValue, -0) ? 0 : clampedValue; setValue(finalValue); onChange?.(finalValue); }, [min, max, onChange], ); const handleInputChange = useCallback( (e: React.ChangeEvent) => { handleChange(parseFloat(e.target.value)); }, [handleChange], ); const handleIncrement = useCallback(() => { handleChange(value + stepClick); }, [value, stepClick, handleChange]); const handleDecrement = useCallback(() => { handleChange(value - stepClick); }, [value, stepClick, handleChange]); const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === 'ArrowRight' || e.key === 'ArrowUp') { e.preventDefault(); handleIncrement(); } else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') { e.preventDefault(); handleDecrement(); } }, [handleIncrement, handleDecrement], ); useEffect(() => { const rangeElement = rangeRef.current; if (rangeElement) { const percentage = ((value - min) / (max - min)) * 100; rangeElement.style.backgroundSize = `${percentage}% 100%`; } }, [value, min, max]); return (
{value.toFixed(decimalPlaces).replace('-0.00', '0.00')} {showButtons && (
)}
setIsHovering(true)} onMouseLeave={() => setIsHovering(false)} className={`slider-thumb h-2 w-full appearance-none rounded-lg bg-gradient-to-r from-gray-500 to-gray-500 bg-no-repeat focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 ${ disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer' }`} tabIndex={0} style={{ backgroundSize: '50% 100%', backgroundPosition: 'left', }} aria-valuemin={min} aria-valuemax={max} aria-valuenow={value} aria-valuetext={`${value.toFixed(decimalPlaces).replace('-0.00', '0.00')}`} disabled={disabled} /> {isHovering ? (
{min} {max}
) : (
)}
); }; export default React.memo(ModelParameters);