The live mic-level halo around the stop button was frozen at a constant scale (1.15) whenever the OS "Reduce motion" setting was on, so it never reacted to the voice while dictating. Make haloScale unconditional so it always follows audioLevel (amplitude 0.9), and drop the now-unused useReducedMotion import and reduceMotion local. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
88 lines
2.5 KiB
TypeScript
88 lines
2.5 KiB
TypeScript
import { FC } from "react";
|
|
import { ActionIcon, Loader, Tooltip } from "@mantine/core";
|
|
import { IconMicrophone, IconPlayerStopFilled } from "@tabler/icons-react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { useDictation } from "@/features/dictation/hooks/use-dictation";
|
|
import classes from "./mic-button.module.css";
|
|
|
|
interface MicButtonProps {
|
|
onText: (text: string) => void;
|
|
onStart?: () => void;
|
|
disabled?: boolean;
|
|
// Mantine ActionIcon size token; "lg" matches the chat composer, "md" the
|
|
// editor toolbar.
|
|
size?: "md" | "lg";
|
|
}
|
|
|
|
/**
|
|
* Self-contained dictation toggle. Owns its own capture state machine: a click
|
|
* starts recording (mic icon), a second click stops it (stop icon), and while
|
|
* the audio is being transcribed it shows a spinner and is disabled to prevent
|
|
* overlapping requests.
|
|
*/
|
|
export const MicButton: FC<MicButtonProps> = ({
|
|
onText,
|
|
onStart,
|
|
disabled,
|
|
size = "lg",
|
|
}) => {
|
|
const { t } = useTranslation();
|
|
const { status, start, stop, audioLevel } = useDictation({ onText, onStart });
|
|
const iconSize = size === "lg" ? 18 : 16;
|
|
|
|
if (status === "recording") {
|
|
// Live volume-driven halo: the scale follows the current mic level.
|
|
const haloScale = 1 + Math.min(1, audioLevel) * 0.9;
|
|
return (
|
|
<Tooltip label={t("Stop recording")} withArrow>
|
|
<span className={classes.recordingWrap}>
|
|
<span
|
|
className={classes.pulse}
|
|
style={{ transform: `scale(${haloScale})` }}
|
|
aria-hidden="true"
|
|
/>
|
|
<ActionIcon
|
|
size={size}
|
|
color="red"
|
|
variant="light"
|
|
onClick={stop}
|
|
aria-label={t("Stop recording")}
|
|
style={{ position: "relative", zIndex: 1 }}
|
|
>
|
|
<IconPlayerStopFilled size={iconSize} />
|
|
</ActionIcon>
|
|
</span>
|
|
</Tooltip>
|
|
);
|
|
}
|
|
|
|
if (status === "transcribing" || status === "error") {
|
|
return (
|
|
<Tooltip label={t("Transcribing…")} withArrow>
|
|
<ActionIcon
|
|
size={size}
|
|
variant="subtle"
|
|
disabled
|
|
aria-label={t("Transcribing…")}
|
|
>
|
|
<Loader size="xs" />
|
|
</ActionIcon>
|
|
</Tooltip>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Tooltip label={t("Start dictation")} withArrow>
|
|
<ActionIcon
|
|
size={size}
|
|
variant="subtle"
|
|
onClick={() => void start()}
|
|
disabled={disabled}
|
|
aria-label={t("Start dictation")}
|
|
>
|
|
<IconMicrophone size={iconSize} />
|
|
</ActionIcon>
|
|
</Tooltip>
|
|
);
|
|
};
|