'use client'; import { useEffect, useMemo, useState } from 'react'; import { type VariantProps, cva } from 'class-variance-authority'; import { LocalAudioTrack, LocalVideoTrack } from 'livekit-client'; import { type TrackReferenceOrPlaceholder, useMaybeRoomContext, useMediaDeviceSelect, } from '@livekit/components-react'; import { AgentAudioVisualizerBar } from '@/components/agents-ui/agent-audio-visualizer-bar'; import { AgentTrackToggle } from '@/components/agents-ui/agent-track-toggle'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { toggleVariants } from '@/components/ui/toggle'; import { cn } from '@/lib/shadcn/utils'; const selectVariants = cva( [ 'rounded-l-none shadow-none pl-2 ', 'text-foreground hover:text-muted-foreground', 'peer-data-[state=on]/track:bg-muted peer-data-[state=on]/track:hover:bg-foreground/10', 'peer-data-[state=off]/track:text-destructive', 'peer-data-[state=off]/track:focus-visible:border-destructive peer-data-[state=off]/track:focus-visible:ring-destructive/30', '[&_svg]:opacity-100', ], { variants: { variant: { default: [ 'border-none', 'peer-data-[state=off]/track:bg-destructive/10', 'peer-data-[state=off]/track:hover:bg-destructive/15', 'peer-data-[state=off]/track:[&_svg]:!text-destructive', 'dark:peer-data-[state=on]/track:bg-accent', 'dark:peer-data-[state=on]/track:hover:bg-foreground/10', 'dark:peer-data-[state=off]/track:bg-destructive/10', 'dark:peer-data-[state=off]/track:hover:bg-destructive/15', ], outline: [ 'border border-l-0', 'peer-data-[state=off]/track:border-destructive/20', 'peer-data-[state=off]/track:bg-destructive/10', 'peer-data-[state=off]/track:hover:bg-destructive/15', 'peer-data-[state=off]/track:[&_svg]:!text-destructive', 'peer-data-[state=on]/track:hover:border-foreground/12', 'dark:peer-data-[state=off]/track:bg-destructive/10', 'dark:peer-data-[state=off]/track:hover:bg-destructive/15', 'dark:peer-data-[state=on]/track:bg-accent', 'dark:peer-data-[state=on]/track:hover:bg-foreground/10', ], }, size: { default: 'w-[180px]', sm: 'w-auto', }, }, defaultVariants: { variant: 'default', size: 'default', }, } ); /** * Props for the TrackDeviceSelect component. */ type TrackDeviceSelectProps = React.ComponentProps & VariantProps & { /** * The size of the select. * @defaultValue 'default' */ size?: 'default' | 'sm'; /** * The variant of the select. * @defaultValue 'default' */ variant?: 'default' | 'outline' | null; /** * The type of media device (audioinput or videoinput). */ kind: MediaDeviceKind; /** * The track source to control (Microphone, Camera, or ScreenShare). */ track?: LocalAudioTrack | LocalVideoTrack | undefined; /** * Whether to request permissions for the media device. */ requestPermissions?: boolean; /** * Callback when a media device error occurs. */ onMediaDeviceError?: (error: Error) => void; /** * Callback when the device list changes. */ onDeviceListChange?: (devices: MediaDeviceInfo[]) => void; /** * Callback when the active device changes. */ onActiveDeviceChange?: (deviceId: string) => void; }; /** * A select component for selecting a media device. * * @extends ComponentProps<'button'> * * @example * ```tsx * * ``` */ function TrackDeviceSelect({ kind, track, size = 'default', variant = 'default', className, requestPermissions = false, onMediaDeviceError, onDeviceListChange, onActiveDeviceChange, ...props }: TrackDeviceSelectProps) { const room = useMaybeRoomContext(); const [open, setOpen] = useState(false); const [requestPermissionsState, setRequestPermissionsState] = useState(requestPermissions); const { devices, activeDeviceId, setActiveMediaDevice } = useMediaDeviceSelect({ room, kind, track, requestPermissions: requestPermissionsState, onError: onMediaDeviceError, }); useEffect(() => { onDeviceListChange?.(devices); }, [devices, onDeviceListChange]); const handleOpenChange = (open: boolean) => { setOpen(open); if (open) { setRequestPermissionsState(true); } }; const handleActiveDeviceChange = (deviceId: string) => { setActiveMediaDevice(deviceId); onActiveDeviceChange?.(deviceId); }; const filteredDevices = useMemo(() => devices.filter((d) => d.deviceId !== ''), [devices]); if (filteredDevices.length < 2) { return null; } return ( ); } /** * Props for the AgentTrackControl component. */ export type AgentTrackControlProps = VariantProps & { /** * The type of media device (audioinput or videoinput). */ kind: MediaDeviceKind; /** * The track source to control (Microphone, Camera, or ScreenShare). */ source: 'camera' | 'microphone' | 'screen_share'; /** * Whether the track is currently enabled/published. */ pressed?: boolean; /** * Whether the control is in a pending/loading state. */ pending?: boolean; /** * Whether the control is disabled. */ disabled?: boolean; /** * Additional CSS class names to apply to the container. */ className?: string; /** * The audio track reference for visualization (only for microphone). */ audioTrack?: TrackReferenceOrPlaceholder; /** * Callback when the pressed state changes. */ onPressedChange?: (pressed: boolean) => void; /** * Callback when a media device error occurs. */ onMediaDeviceError?: (error: Error) => void; /** * Callback when the active device changes. */ onActiveDeviceChange?: (deviceId: string) => void; }; /** * A combined track toggle and device selector control. * Includes a toggle button and a dropdown to select the active device. * For microphone tracks, displays an audio visualizer. * * @example * ```tsx * setMicEnabled(pressed)} * onActiveDeviceChange={(deviceId) => setMicDevice(deviceId)} * /> * ``` */ export function AgentTrackControl({ kind, variant = 'default', source, pressed, pending, disabled, className, audioTrack, onPressedChange, onMediaDeviceError, onActiveDeviceChange, }: AgentTrackControlProps) { return (
{audioTrack && ( )} {kind && ( )}
); }