// Copyright (c) Meta Platforms, Inc. and affiliates. 'use client'; /** * @file ChatDictationButton.tsx * @input Uses React, StyleX, Button, Icon, useChatDictation return * @output Exports ChatDictationButton for voice input in chat composer * @position UI component — renders in sendActions slot of ChatComposer * * A toggle button that connects to useChatDictation. Shows a microphone * icon when idle. When listening, replaces the icon with volume-reactive * frequency bars (equalizer style) that respond to real mic input. * Bars use the accent color and hue-shift when volume clips past 10%. * * SYNC: When modified, update: * - /packages/core/src/Chat/index.ts (exports) * - /packages/cli/assets/templates/blocks/components/ChatDictationButton/ (block examples) */ import React from 'react'; import type { UseSpeechRecognitionReturn } from "./useSpeechRecognition.js"; import * as stylex from "../style-runtime.js"; import "../theme/tokens.stylex.js"; import { colorVars, radiusVars } from "../theme/tokens.stylex.js"; import { Button } from "../Button/index.js"; import { Icon } from "../Icon/index.js"; import { mergeProps } from "../utils/index.js"; import type { BaseProps } from "../BaseProps.js"; import { themeProps } from "../utils/themeProps.js"; import { useTranslator } from "../i18n/index.js"; // ============================================================================= // Types // ============================================================================= export interface ChatDictationButtonProps extends BaseProps { ref?: React.Ref; /** The return value from useChatDictation or useSpeechRecognition. */ dictation: UseSpeechRecognitionReturn; /** Button size. @default "md" */ size?: 'sm' | 'md'; /** Hide the button when SpeechRecognition is not supported. @default true */ isHiddenWhenUnsupported?: boolean; /** Accessible label override. */ label?: string; } // ============================================================================= // Styles // ============================================================================= const styles = { wrapper: { kVAEAm: "x1n2onr6", k1xSpc: "x3nfvp2", kGNEyG: "x6s0dn4", kjj79g: "xl56j7k", $$css: true as const } }; // ============================================================================= // Constants // ============================================================================= const BAR_COUNT = 5; const BAR_MIN_SCALE = 0.08; const SIZE_CONFIG = { sm: { barWidth: 2, barGap: 1.5, barMaxHeight: 14 }, md: { barWidth: 2.5, barGap: 2, barMaxHeight: 18 } }; // ============================================================================= // Component // ============================================================================= /** * Microphone button for voice input in a chat composer. * Requires the return value of useChatDictation. * * @example * ``` * * ``` */ export function ChatDictationButton({ ref, dictation, size = 'md', isHiddenWhenUnsupported = true, label, xstyle, className, style, ...rest }: ChatDictationButtonProps) { const t = useTranslator(); if (isHiddenWhenUnsupported && !dictation.isSupported) { return null; } const { isListening, bands, volume: rawVolume } = dictation; const accessibleLabel = label ?? (isListening ? t('@astryx.chatDictationButton.stopDictation') : t('@astryx.chatDictationButton.startDictation')); // Boost each band for visibility — quiet speech (0-10%) maps to full visual range const boostedBands = bands.map(b => Math.min(Math.pow(b / 0.2, 0.5), 1)); // Hue shift from accent color when volume clips past 10% const isClipping = rawVolume >= 0.2; const hueShift = isClipping ? Math.min((rawVolume - 0.2) / 0.1, 1) * 60 : 0; const barColor = isClipping ? `hsl(calc(var(--accent-hue, 210) + ${hueShift}), 80%, 50%)` : `var(--color-accent, ${colorVars['--color-accent']})`; const { barWidth, barGap, barMaxHeight } = SIZE_CONFIG[size]; return {isListening && {boostedBands.slice(0, BAR_COUNT).map((level, i) => { const scale = BAR_MIN_SCALE + level * (1 - BAR_MIN_SCALE); return ; })} }