2026-02-27 09:55:36 -06:00
|
|
|
import type { WidgetProps } from "@rjsf/utils";
|
|
|
|
|
import { useMemo } from "react";
|
|
|
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
|
import { Switch } from "@/components/ui/switch";
|
|
|
|
|
|
2026-08-16 13:59:03 -05:00
|
|
|
const INPUT_ROLES = ["detect", "record", "record_sub", "audio"] as const;
|
|
|
|
|
|
|
|
|
|
// Recording the sub stream from the same input as record would just
|
|
|
|
|
// re-record the main stream, so the two roles are mutually exclusive.
|
|
|
|
|
const CONFLICTING_ROLES: Partial<Record<string, string>> = {
|
|
|
|
|
record: "record_sub",
|
|
|
|
|
record_sub: "record",
|
|
|
|
|
};
|
2026-02-27 09:55:36 -06:00
|
|
|
|
|
|
|
|
function normalizeValue(value: unknown): string[] {
|
|
|
|
|
if (Array.isArray(value)) {
|
|
|
|
|
return value.filter((item): item is string => typeof item === "string");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (typeof value === "string" && value.trim()) {
|
|
|
|
|
return [value.trim()];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function InputRolesWidget(props: WidgetProps) {
|
2026-08-16 13:59:03 -05:00
|
|
|
const { id, value, disabled, readonly, onChange, options } = props;
|
2026-02-27 09:55:36 -06:00
|
|
|
const { t } = useTranslation(["views/settings"]);
|
|
|
|
|
|
|
|
|
|
const selectedRoles = useMemo(() => normalizeValue(value), [value]);
|
|
|
|
|
|
2026-08-16 13:59:03 -05:00
|
|
|
// Each role may only be assigned to a single input, so roles already
|
|
|
|
|
// used by sibling inputs are locked.
|
|
|
|
|
const rolesUsedByOtherInputs = useMemo(
|
|
|
|
|
() => normalizeValue(options?.rolesUsedByOtherInputs),
|
|
|
|
|
[options],
|
|
|
|
|
);
|
|
|
|
|
|
2026-02-27 09:55:36 -06:00
|
|
|
const toggleRole = (role: string, enabled: boolean) => {
|
|
|
|
|
if (enabled) {
|
|
|
|
|
if (!selectedRoles.includes(role)) {
|
|
|
|
|
onChange([...selectedRoles, role]);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onChange(selectedRoles.filter((item) => item !== role));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="rounded-lg border border-secondary-highlight bg-background_alt p-2 pr-0 md:max-w-md">
|
|
|
|
|
<div className="grid gap-2">
|
|
|
|
|
{INPUT_ROLES.map((role) => {
|
|
|
|
|
const checked = selectedRoles.includes(role);
|
2026-08-16 13:59:03 -05:00
|
|
|
const usedByOtherInput =
|
|
|
|
|
!checked && rolesUsedByOtherInputs.includes(role);
|
|
|
|
|
const conflictingRole = CONFLICTING_ROLES[role];
|
|
|
|
|
const hasConflict =
|
|
|
|
|
!checked &&
|
|
|
|
|
conflictingRole !== undefined &&
|
|
|
|
|
selectedRoles.includes(conflictingRole);
|
|
|
|
|
const hint = usedByOtherInput
|
|
|
|
|
? t("configForm.inputRoles.roleInUse", { ns: "views/settings" })
|
|
|
|
|
: hasConflict
|
|
|
|
|
? t("configForm.inputRoles.recordSubConflict", {
|
|
|
|
|
ns: "views/settings",
|
|
|
|
|
})
|
|
|
|
|
: undefined;
|
2026-02-27 09:55:36 -06:00
|
|
|
const label = t(`configForm.inputRoles.options.${role}`, {
|
|
|
|
|
ns: "views/settings",
|
|
|
|
|
defaultValue: role,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={role}
|
|
|
|
|
className="flex items-center justify-between rounded-md px-3 py-0"
|
|
|
|
|
>
|
2026-08-16 13:59:03 -05:00
|
|
|
<div className="flex flex-col">
|
|
|
|
|
<label htmlFor={`${id}-${role}`} className="text-sm">
|
|
|
|
|
{label}
|
|
|
|
|
</label>
|
|
|
|
|
{hint ? (
|
|
|
|
|
<span className="text-xs text-muted-foreground">{hint}</span>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
2026-02-27 09:55:36 -06:00
|
|
|
<Switch
|
|
|
|
|
id={`${id}-${role}`}
|
|
|
|
|
checked={checked}
|
2026-08-16 13:59:03 -05:00
|
|
|
disabled={
|
|
|
|
|
disabled || readonly || usedByOtherInput || hasConflict
|
|
|
|
|
}
|
2026-02-27 09:55:36 -06:00
|
|
|
onCheckedChange={(enabled) => toggleRole(role, !!enabled)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|