mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 06:00:56 +02:00

* chore: enforcing language keys to adhere to the new standard. * chore: enforcing i18n forbids to write plain text in JSX markup * chore: enforcing i18n forbids to write plain text in JSX markup * fix: ci with checkbox for unused keys :) * refactor: removed all the unused `i18n` keys
185 lines
5.8 KiB
TypeScript
185 lines
5.8 KiB
TypeScript
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
|
import { TranslationKeys, 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<ModelParametersProps> = ({
|
|
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<HTMLInputElement>(null);
|
|
|
|
const id = `model-parameter-${ariaLabel.toLowerCase().replace(/\s+/g, '-')}`;
|
|
const displayLabel = label.startsWith('com_') ? localize(label as TranslationKeys) : 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<HTMLInputElement>) => {
|
|
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<HTMLInputElement>) => {
|
|
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 (
|
|
<div className="w-full">
|
|
<div className="mb-2 flex items-center justify-between">
|
|
<label
|
|
htmlFor={id}
|
|
className={`text-sm font-medium ${disabled ? 'text-gray-400 dark:text-gray-400' : ''}`}
|
|
>
|
|
{displayLabel}
|
|
</label>
|
|
<div className="flex items-center gap-2">
|
|
<output
|
|
htmlFor={id}
|
|
className={`select-none text-sm font-medium ${
|
|
disabled ? 'text-gray-400 dark:text-gray-400' : ''
|
|
}`}
|
|
aria-live="polite"
|
|
>
|
|
{value.toFixed(decimalPlaces).replace('-0.00', '0.00')}
|
|
</output>
|
|
{showButtons && (
|
|
<div className="flex items-center gap-1">
|
|
<button
|
|
type="button"
|
|
onClick={handleDecrement}
|
|
className={`rounded-md p-1 transition-colors ${
|
|
disabled
|
|
? 'cursor-not-allowed text-gray-400 dark:text-gray-400'
|
|
: 'hover:bg-gray-200 dark:hover:bg-gray-700'
|
|
}`}
|
|
aria-label={`Decrease ${label}`}
|
|
disabled={disabled}
|
|
>
|
|
<Minus size={16} />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={handleIncrement}
|
|
className={`rounded-md p-1 transition-colors ${
|
|
disabled
|
|
? 'cursor-not-allowed text-gray-400 dark:text-gray-400'
|
|
: 'hover:bg-gray-200 dark:hover:bg-gray-700'
|
|
}`}
|
|
aria-label={`Increase ${label}`}
|
|
disabled={disabled}
|
|
>
|
|
<Plus size={16} />
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="relative w-full">
|
|
<input
|
|
ref={rangeRef}
|
|
type="range"
|
|
id={id}
|
|
min={min}
|
|
max={max}
|
|
step={step}
|
|
value={value}
|
|
onChange={handleInputChange}
|
|
onKeyDown={handleKeyDown}
|
|
onMouseEnter={() => 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 ? (
|
|
<div className="trab mt-1 flex justify-between">
|
|
<span className="text-xs text-gray-500">{min}</span>
|
|
<span className="text-xs text-gray-500">{max}</span>
|
|
</div>
|
|
) : (
|
|
<div className="mt-1" style={{ height: '1rem' }}></div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default React.memo(ModelParameters);
|