Files
gitmost/apps/client/src/features/dictation/components/mic-button.tsx
claude_code 0deded342d fix(dictation): drive the recording halo from mic level under reduced-motion
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>
2026-06-21 23:34:07 +03:00

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>
);
};