diff --git a/docs/storybook/stories/agents-ui/AgentControlBar.stories.tsx b/docs/storybook/stories/agents-ui/AgentControlBar.stories.tsx new file mode 100644 index 000000000..71dd683c5 --- /dev/null +++ b/docs/storybook/stories/agents-ui/AgentControlBar.stories.tsx @@ -0,0 +1,79 @@ +import * as React from 'react'; +import { StoryObj } from '@storybook/react-vite'; +import { + AgentSessionProvider, + useMicrophone, +} from '../../.storybook/lk-decorators/AgentSessionProvider'; +import { AgentControlBar, AgentControlBarProps } from '@agents-ui'; + +export default { + component: AgentControlBar, + decorators: [AgentSessionProvider], + render: (args: AgentControlBarProps) => { + useMicrophone(); + return ; + }, + args: { + isConnected: true, + controls: { + microphone: true, + camera: true, + screenShare: true, + chat: true, + leave: true, + }, + className: 'w-full', + }, + argTypes: { + controls: { control: { type: 'object' } }, + isConnected: { control: { type: 'boolean' } }, + isChatOpen: { control: { type: 'boolean' } }, + }, + parameters: { + layout: 'centered', + actions: { + handles: [], + }, + }, +}; + +export const Default: StoryObj = { + args: {}, +}; + +export const Outline: StoryObj = { + args: { + variant: 'outline', + }, +}; + +export const Livekit: StoryObj = { + args: { + variant: 'livekit', + }, +}; + +export const NoControls: StoryObj = { + args: { + controls: { + microphone: false, + camera: false, + screenShare: false, + chat: false, + leave: false, + }, + }, + render: (args: AgentControlBarProps) => { + return ( + <> +

+ This control bar does not render +
+ because visibleControls contains + only false values. +

+ + + ); + }, +}; diff --git a/docs/storybook/stories/agents-ui/AgentDisconnectButton.stories.tsx b/docs/storybook/stories/agents-ui/AgentDisconnectButton.stories.tsx new file mode 100644 index 000000000..5357bea31 --- /dev/null +++ b/docs/storybook/stories/agents-ui/AgentDisconnectButton.stories.tsx @@ -0,0 +1,36 @@ +import * as React from 'react'; +import { StoryObj } from '@storybook/react-vite'; +import { AgentSessionProvider } from '../../.storybook/lk-decorators/AgentSessionProvider'; +import { AgentDisconnectButton, type AgentDisconnectButtonProps } from '@agents-ui'; + +export default { + component: AgentDisconnectButton, + decorators: [AgentSessionProvider], + render: (args: AgentDisconnectButtonProps) => ( + + ), + argTypes: { + size: { + options: ['default', 'sm', 'lg', 'icon', 'icon-sm', 'icon-lg'], + control: { type: 'select' }, + }, + onClick: { action: 'onClick' }, + className: { control: { type: 'text' } }, + }, + parameters: { + layout: 'centered', + actions: { + handles: [], + }, + }, +}; + +export const Default: StoryObj = { + args: {}, +}; + +export const Icon: StoryObj = { + args: { + size: 'icon', + }, +}; diff --git a/docs/storybook/stories/agents-ui/AgentTrackControl.stories.tsx b/docs/storybook/stories/agents-ui/AgentTrackControl.stories.tsx new file mode 100644 index 000000000..f1970a22d --- /dev/null +++ b/docs/storybook/stories/agents-ui/AgentTrackControl.stories.tsx @@ -0,0 +1,119 @@ +import * as React from 'react'; +import { StoryObj } from '@storybook/react-vite'; +import { + AgentSessionProvider, + useMicrophone, +} from '../../.storybook/lk-decorators/AgentSessionProvider'; +import { AgentTrackControl, type AgentTrackControlProps } from '@agents-ui'; +import { Track } from 'livekit-client'; + +export default { + component: AgentTrackControl, + decorators: [AgentSessionProvider], + render: (args: AgentTrackControlProps) => { + const [isPressed, setIsPressed] = React.useState( + args.source === Track.Source.Microphone ? true : false, + ); + + const microphoneTrack = useMicrophone(); + + return ( + setIsPressed(pressed)} + audioTrack={args.source === Track.Source.Microphone ? microphoneTrack : undefined} + /> + ); + }, + argTypes: { + size: { + options: ['default', 'sm', 'lg'], + control: { type: 'radio' }, + }, + pending: { control: { type: 'boolean' } }, + className: { control: { type: 'text' } }, + }, + parameters: { + layout: 'centered', + actions: { + handles: [], + }, + }, +}; + +export const Default: StoryObj = { + render: (args: AgentTrackControlProps) => { + const [isCameraPressed, setIsCameraPressed] = React.useState(true); + const [isMicrophonePressed, setIsMicrophonePressed] = React.useState(false); + const [isScreenSharePressed, setIsScreenSharePressed] = React.useState(true); + + const microphoneTrack = useMicrophone(); + + return ( +
+ setIsMicrophonePressed(pressed)} + /> + setIsCameraPressed(pressed)} + /> + setIsScreenSharePressed(pressed)} + /> +
+ ); + }, + args: {}, +}; + +export const Outlined: StoryObj = { + args: { + variant: 'outline', + }, + render: (args: AgentTrackControlProps) => { + const [isCameraPressed, setIsCameraPressed] = React.useState(true); + const [isMicrophonePressed, setIsMicrophonePressed] = React.useState(false); + const [isScreenSharePressed, setIsScreenSharePressed] = React.useState(true); + + const microphoneTrack = useMicrophone(); + + return ( +
+ setIsMicrophonePressed(pressed)} + /> + setIsCameraPressed(pressed)} + /> + setIsScreenSharePressed(pressed)} + /> +
+ ); + }, +}; diff --git a/docs/storybook/stories/agents-ui/AgentTrackToggle.stories.tsx b/docs/storybook/stories/agents-ui/AgentTrackToggle.stories.tsx new file mode 100644 index 000000000..d96af0a9a --- /dev/null +++ b/docs/storybook/stories/agents-ui/AgentTrackToggle.stories.tsx @@ -0,0 +1,106 @@ +import * as React from 'react'; +import { StoryObj } from '@storybook/react-vite'; +import { AgentSessionProvider } from '../../.storybook/lk-decorators/AgentSessionProvider'; +import { AgentTrackToggle, AgentTrackToggleProps } from '@agents-ui'; +import { Track } from 'livekit-client'; + +export default { + component: AgentTrackToggle, + decorators: [AgentSessionProvider], + render: (args: AgentTrackToggleProps) => { + const [isPressed, setIsPressed] = React.useState( + args.source === Track.Source.Microphone ? true : false, + ); + + return ( + setIsPressed(pressed)} + /> + ); + }, + args: { + size: 'default', + }, + argTypes: { + size: { + options: ['default', 'sm', 'lg'], + control: { type: 'radio' }, + }, + pending: { control: { type: 'boolean' } }, + className: { control: { type: 'text' } }, + }, + parameters: { + layout: 'centered', + actions: { + handles: [], + }, + }, +}; + +export const Default: StoryObj = { + render: (args: AgentTrackToggleProps) => { + const [isCameraPressed, setIsCameraPressed] = React.useState(true); + const [isMicrophonePressed, setIsMicrophonePressed] = React.useState(false); + const [isScreenSharePressed, setIsScreenSharePressed] = React.useState(true); + + return ( +
+ setIsMicrophonePressed(pressed)} + /> + setIsCameraPressed(pressed)} + /> + setIsScreenSharePressed(pressed)} + /> +
+ ); + }, + args: {}, +}; + +export const Outlined: StoryObj = { + args: { + variant: 'outline', + }, + render: (args: AgentTrackToggleProps) => { + const [isCameraPressed, setIsCameraPressed] = React.useState(true); + const [isMicrophonePressed, setIsMicrophonePressed] = React.useState(false); + const [isScreenSharePressed, setIsScreenSharePressed] = React.useState(true); + + return ( +
+ setIsMicrophonePressed(pressed)} + /> + setIsCameraPressed(pressed)} + /> + setIsScreenSharePressed(pressed)} + /> +
+ ); + }, +}; diff --git a/docs/storybook/stories/agents-ui/AudioVisualizerBar.stories.tsx b/docs/storybook/stories/agents-ui/AudioVisualizerBar.stories.tsx new file mode 100644 index 000000000..5e01c6f75 --- /dev/null +++ b/docs/storybook/stories/agents-ui/AudioVisualizerBar.stories.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { StoryObj } from '@storybook/react-vite'; +import { + AgentSessionProvider, + useMicrophone, +} from '../../.storybook/lk-decorators/AgentSessionProvider'; +import { AudioVisualizerBar, AudioVisualizerBarProps } from '@agents-ui'; + +export default { + component: AudioVisualizerBar, + decorators: [AgentSessionProvider], + render: (args: AudioVisualizerBarProps) => { + const audioTrack = useMicrophone(); + + return ; + }, + args: { + size: 'xl', + barCount: 5, + state: 'connecting', + }, + argTypes: { + size: { + options: ['icon', 'sm', 'md', 'lg', 'xl'], + control: { type: 'radio' }, + }, + state: { + options: [ + 'idle', + 'disconnected', + 'pre-connect-buffering', + 'connecting', + 'initializing', + 'listening', + 'thinking', + 'speaking', + 'failed', + ], + control: { type: 'radio' }, + }, + barCount: { + control: { type: 'range', min: 1, max: 24, step: 1 }, + }, + className: { control: { type: 'text' } }, + }, + parameters: { + layout: 'centered', + actions: { + handles: [], + }, + }, +}; + +export const Default: StoryObj = { + args: {}, +}; diff --git a/packages/react/src/components/participant/BarVisualizer.tsx b/packages/react/src/components/participant/BarVisualizer.tsx index bef470101..69255d99a 100644 --- a/packages/react/src/components/participant/BarVisualizer.tsx +++ b/packages/react/src/components/participant/BarVisualizer.tsx @@ -112,7 +112,7 @@ export const BarVisualizer = /* @__PURE__ */ React.forwardRef 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 ( +
+