import { useCallback, useMemo } from "react"; import { useTranslation } from "react-i18next"; import useSWR from "swr"; import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { DetectionHardware } from "@/types/hardware"; import { hardwareForDevices, MAX_DETECTORS, recommendedDetectorCount, } from "@/utils/detectionHardware"; type HardwarePickerProps = { // scopes the unit checkbox ids, since several models can list the same unit idPrefix: string; devices: string[]; // device strings already taken by another model, mapped to that model's scene claimedElsewhere: Record; cameraCount: number; disabled?: boolean; onChange: (devices: string[]) => void; }; export function HardwarePicker({ idPrefix, devices, claimedElsewhere, cameraCount, disabled, onChange, }: HardwarePickerProps) { const { t } = useTranslation(["views/settings", "common"]); const { data: hardware, isLoading } = useSWR("hardware/probe"); const selected = useMemo( () => hardwareForDevices(hardware ?? [], devices), [hardware, devices], ); const recommended = useMemo( () => recommendedDetectorCount(cameraCount), [cameraCount], ); // the units a model is assigned to, in the order the probe reports them. A // shareable unit repeats in `devices` once per inference process, so the // distinct entries are what is selected. const selectedUnits = useMemo(() => { if (!selected) { return []; } return selected.units .map((unit) => unit.device) .filter((device) => devices.includes(device)); }, [selected, devices]); /** Spread `count` detectors round robin over the selected units. */ const buildDevices = useCallback((units: string[], count: number) => { if (units.length === 0) { return []; } return Array.from( { length: Math.max(count, units.length) }, (_, index) => units[index % units.length], ); }, []); const handleHardwareChange = useCallback( (key: string) => { const entry = hardware?.find((candidate) => candidate.key === key); if (!entry) { return; } // start with the first unit no other model has taken const free = entry.units.find((unit) => !claimedElsewhere[unit.device]); if (!free) { onChange([]); return; } onChange( entry.unlimited ? buildDevices([free.device], recommended) : [free.device], ); }, [hardware, claimedElsewhere, recommended, buildDevices, onChange], ); const handleUnitToggle = useCallback( (device: string, checked: boolean) => { if (!selected) { return; } const units = selected.units .map((unit) => unit.device) .filter((candidate) => candidate === device ? checked : selectedUnits.includes(candidate), ); if (!selected.unlimited) { onChange(units); return; } // keep the detector count while the set of units changes onChange(buildDevices(units, devices.length)); }, [selected, selectedUnits, devices.length, buildDevices, onChange], ); const handleCountChange = useCallback( (value: string) => { onChange(buildDevices(selectedUnits, Number(value))); }, [selectedUnits, buildDevices, onChange], ); if (isLoading) { return (

{t("detectionModels.hardware.loading")}

); } // a hand-written config can name hardware this system does not report const unrecognized = devices.length > 0 && !selected; return (

{t("detectionModels.hardware.description")}

{unrecognized ? (

{t("detectionModels.hardware.unrecognized", { devices: devices.join(", "), })}

) : null} {selected && (selected.units.length > 1 || !selected.unlimited) ? (

{t("detectionModels.hardware.unitsDescription")}

{selected.units.map((unit) => { const claimedBy = claimedElsewhere[unit.device]; return (
handleUnitToggle(unit.device, checked === true) } />
); })}
) : null} {selected?.unlimited ? (

{t("detectionModels.hardware.detectorCountDescription")}

) : null}
); } export default HardwarePicker;