diff --git a/docs/storybook/stories/agents-ui/AudioVisualizerBar.stories.tsx b/docs/storybook/stories/agents-ui/AgentAudioVisualizerBar.stories.tsx similarity index 76% rename from docs/storybook/stories/agents-ui/AudioVisualizerBar.stories.tsx rename to docs/storybook/stories/agents-ui/AgentAudioVisualizerBar.stories.tsx index 5e01c6f75..943d38431 100644 --- a/docs/storybook/stories/agents-ui/AudioVisualizerBar.stories.tsx +++ b/docs/storybook/stories/agents-ui/AgentAudioVisualizerBar.stories.tsx @@ -4,15 +4,15 @@ import { AgentSessionProvider, useMicrophone, } from '../../.storybook/lk-decorators/AgentSessionProvider'; -import { AudioVisualizerBar, AudioVisualizerBarProps } from '@agents-ui'; +import { AgentAudioVisualizerBar, AgentAudioVisualizerBarProps } from '@agents-ui'; export default { - component: AudioVisualizerBar, + component: AgentAudioVisualizerBar, decorators: [AgentSessionProvider], - render: (args: AudioVisualizerBarProps) => { + render: (args: AgentAudioVisualizerBarProps) => { const audioTrack = useMicrophone(); - return ; + return ; }, args: { size: 'xl', @@ -51,6 +51,6 @@ export default { }, }; -export const Default: StoryObj = { +export const Default: StoryObj = { args: {}, }; diff --git a/packages/shadcn/components/agents-ui/audio-visualizer-bar/audio-visualizer-bar.tsx b/packages/shadcn/components/agents-ui/agent-audio-visualizer-bar.tsx similarity index 63% rename from packages/shadcn/components/agents-ui/audio-visualizer-bar/audio-visualizer-bar.tsx rename to packages/shadcn/components/agents-ui/agent-audio-visualizer-bar.tsx index 02eec005f..6cae6e905 100644 --- a/packages/shadcn/components/agents-ui/audio-visualizer-bar/audio-visualizer-bar.tsx +++ b/packages/shadcn/components/agents-ui/agent-audio-visualizer-bar.tsx @@ -1,4 +1,13 @@ -import React, { type ReactNode, useMemo } from 'react'; +'use client'; + +import React, { + type ReactNode, + type CSSProperties, + useMemo, + Children, + cloneElement, + isValidElement, +} from 'react'; import { type VariantProps, cva } from 'class-variance-authority'; import { type LocalAudioTrack, type RemoteAudioTrack } from 'livekit-client'; import { @@ -6,11 +15,34 @@ import { type TrackReferenceOrPlaceholder, useMultibandTrackVolume, } from '@livekit/components-react'; +import { useAgentAudioVisualizerBarAnimator } from '@/hooks/agents-ui/use-agent-audio-visualizer-bar'; import { cn } from '@/lib/utils'; -import { cloneSingleChild } from '@/lib/clone-single-child'; -import { useBarAnimator } from './hooks/useBarAnimator'; -export const AudioVisualizerBarVariants = cva( +export function cloneSingleChild( + children: ReactNode | ReactNode[], + props?: Record, + key?: unknown, +) { + return Children.map(children, (child) => { + // Checking isValidElement is the safe way and avoids a typescript error too. + if (isValidElement(child) && Children.only(children)) { + const childProps = child.props as Record; + if (childProps.className) { + // make sure we retain classnames of both passed props and child + props ??= {}; + props.className = cn(childProps.className as string, props.className as string); + props.style = { + ...(childProps.style as CSSProperties), + ...(props.style as CSSProperties), + }; + } + return cloneElement(child, { ...props, key: key ? String(key) : undefined }); + } + return child; + }); +} + +export const AgentAudioVisualizerBarVariants = cva( [ 'relative flex items-center justify-center', '[&_>_*]:rounded-full [&_>_*]:transition-colors [&_>_*]:duration-250 [&_>_*]:ease-linear', @@ -32,7 +64,7 @@ export const AudioVisualizerBarVariants = cva( }, ); -export interface AudioVisualizerBarProps { +export interface AgentAudioVisualizerBarProps { state?: AgentState; barCount?: number; audioTrack?: LocalAudioTrack | RemoteAudioTrack | TrackReferenceOrPlaceholder; @@ -40,14 +72,14 @@ export interface AudioVisualizerBarProps { children?: ReactNode | ReactNode[]; } -export function AudioVisualizerBar({ +export function AgentAudioVisualizerBar({ size, state, barCount, audioTrack, className, children, -}: AudioVisualizerBarProps & VariantProps) { +}: AgentAudioVisualizerBarProps & VariantProps) { const _barCount = useMemo(() => { if (barCount) { return barCount; @@ -82,14 +114,19 @@ export function AudioVisualizerBar({ } }, [state, _barCount]); - const highlightedIndices = useBarAnimator(state, _barCount, sequencerInterval); + const highlightedIndices = useAgentAudioVisualizerBarAnimator( + state, + _barCount, + sequencerInterval, + ); + const bands = useMemo( () => (state === 'speaking' ? volumeBands : new Array(_barCount).fill(0)), [state, volumeBands, _barCount], ); return ( -
+
{bands.map((band: number, idx: number) => children ? ( diff --git a/packages/shadcn/components/agents-ui/agent-control-bar/agent-control-bar.tsx b/packages/shadcn/components/agents-ui/agent-control-bar.tsx similarity index 80% rename from packages/shadcn/components/agents-ui/agent-control-bar/agent-control-bar.tsx rename to packages/shadcn/components/agents-ui/agent-control-bar.tsx index 86eb12b20..c06e391a6 100644 --- a/packages/shadcn/components/agents-ui/agent-control-bar/agent-control-bar.tsx +++ b/packages/shadcn/components/agents-ui/agent-control-bar.tsx @@ -1,21 +1,24 @@ 'use client'; -import { type HTMLAttributes, useState } from 'react'; +import { useEffect, useRef, type HTMLAttributes, useState } from 'react'; import { Track } from 'livekit-client'; import { motion } from 'motion/react'; import { useChat } from '@livekit/components-react'; -import { MessageSquareTextIcon } from 'lucide-react'; +import { Loader, MessageSquareTextIcon, SendHorizontal } from 'lucide-react'; +import { Toggle } from '@/components/ui/toggle'; +import { Button } from '@/components/ui/button'; import { AgentTrackToggle, agentTrackToggleVariants, } from '@/components/agents-ui/agent-track-toggle'; import { AgentTrackControl } from '@/components/agents-ui/agent-track-control'; -import { Toggle } from '@/components/ui/toggle'; +import { AgentDisconnectButton } from '@/components/agents-ui/agent-disconnect-button'; +import { + useInputControls, + usePublishPermissions, + type UseInputControlsProps, +} from '@/hooks/agents-ui/use-agent-control-bar'; import { cn } from '@/lib/utils'; -import { AgentChatInput } from './agent-chat-input'; -import { UseInputControlsProps, useInputControls } from './hooks/use-input-controls'; -import { usePublishPermissions } from './hooks/use-publish-permissions'; -import { AgentDisconnectButton } from '../agent-disconnect-button'; const TOGGLE_VARIANT_1 = [ '[&_[data-state=off]]:bg-accent [&_[data-state=off]]:hover:bg-foreground/10', @@ -58,6 +61,71 @@ const MOTION_PROPS = { }, }; +interface AgentChatInputProps { + chatOpen: boolean; + onSend?: (message: string) => void; + className?: string; +} + +export function AgentChatInput({ + chatOpen, + onSend = async () => {}, + className, +}: AgentChatInputProps) { + const inputRef = useRef(null); + const [isSending, setIsSending] = useState(false); + const [message, setMessage] = useState(''); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + try { + setIsSending(true); + await onSend(message); + setMessage(''); + } catch (error) { + console.error(error); + } finally { + setIsSending(false); + } + }; + + const isDisabled = isSending || message.trim().length === 0; + + useEffect(() => { + if (chatOpen) return; + // when not disabled refocus on input + inputRef.current?.focus(); + }, [chatOpen]); + + return ( +
+