{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 (
+
+ );
+}
+
export interface ControlBarControls {
leave?: boolean;
camera?: boolean;
diff --git a/packages/shadcn/components/agents-ui/agent-control-bar/agent-chat-input.tsx b/packages/shadcn/components/agents-ui/agent-control-bar/agent-chat-input.tsx
deleted file mode 100644
index d1f8bb2a8..000000000
--- a/packages/shadcn/components/agents-ui/agent-control-bar/agent-chat-input.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import { useEffect, useRef, useState } from 'react';
-import { SendHorizontal, Loader } from 'lucide-react';
-import { Button } from '@/components/ui/button';
-import { cn } from '@/lib/utils';
-
-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 (
-
- );
-}
diff --git a/packages/shadcn/components/agents-ui/agent-control-bar/hooks/use-publish-permissions.ts b/packages/shadcn/components/agents-ui/agent-control-bar/hooks/use-publish-permissions.ts
deleted file mode 100644
index 835ecd3e0..000000000
--- a/packages/shadcn/components/agents-ui/agent-control-bar/hooks/use-publish-permissions.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { Track } from 'livekit-client';
-import { useLocalParticipantPermissions } from '@livekit/components-react';
-
-const trackSourceToProtocol = (source: Track.Source) => {
- // NOTE: this mapping avoids importing the protocol package as that leads to a significant bundle size increase
- switch (source) {
- case Track.Source.Camera:
- return 1;
- case Track.Source.Microphone:
- return 2;
- case Track.Source.ScreenShare:
- return 3;
- default:
- return 0;
- }
-};
-
-export interface PublishPermissions {
- camera: boolean;
- microphone: boolean;
- screenShare: boolean;
- data: boolean;
-}
-
-export function usePublishPermissions(): PublishPermissions {
- const localPermissions = useLocalParticipantPermissions();
-
- const canPublishSource = (source: Track.Source) => {
- return (
- !!localPermissions?.canPublish &&
- (localPermissions.canPublishSources.length === 0 ||
- localPermissions.canPublishSources.includes(trackSourceToProtocol(source)))
- );
- };
-
- return {
- camera: canPublishSource(Track.Source.Camera),
- microphone: canPublishSource(Track.Source.Microphone),
- screenShare: canPublishSource(Track.Source.ScreenShare),
- data: localPermissions?.canPublishData ?? false,
- };
-}
diff --git a/packages/shadcn/components/agents-ui/agent-track-control.tsx b/packages/shadcn/components/agents-ui/agent-track-control.tsx
index 80e1087e9..84621bc6a 100644
--- a/packages/shadcn/components/agents-ui/agent-track-control.tsx
+++ b/packages/shadcn/components/agents-ui/agent-track-control.tsx
@@ -10,7 +10,7 @@ import {
useMediaDeviceSelect,
} from '@livekit/components-react';
-import { AudioVisualizerBar } from '@/components/agents-ui/audio-visualizer-bar/audio-visualizer-bar';
+import { AgentAudioVisualizerBar } from '@/components/agents-ui/agent-audio-visualizer-bar';
import { AgentTrackToggle } from '@/components/agents-ui/agent-track-toggle';
import {
Select,
@@ -192,7 +192,7 @@ export function AgentTrackControl({
className="peer/track group/track has-[.audiovisualizer]:w-auto has-[.audiovisualizer]:px-3 has-[~_button]:rounded-r-none has-[~_button]:pr-2 has-[~_button]:pl-3 has-[~_button]:border-r-0 focus:z-10"
>
{audioTrack && (
-
-
+
)}
{kind && (
diff --git a/packages/shadcn/components/agents-ui/agent-track-toggle.tsx b/packages/shadcn/components/agents-ui/agent-track-toggle.tsx
index c4e1be82f..3e08faf3a 100644
--- a/packages/shadcn/components/agents-ui/agent-track-toggle.tsx
+++ b/packages/shadcn/components/agents-ui/agent-track-toggle.tsx
@@ -1,5 +1,3 @@
-'use client';
-
import * as React from 'react';
import { cva } from 'class-variance-authority';
import { Track } from 'livekit-client';
diff --git a/packages/shadcn/components/agents-ui/audio-visualizer-bar/hooks/useBarAnimator.ts b/packages/shadcn/hooks/agents-ui/use-agent-audio-visualizer-bar.ts
similarity index 96%
rename from packages/shadcn/components/agents-ui/audio-visualizer-bar/hooks/useBarAnimator.ts
rename to packages/shadcn/hooks/agents-ui/use-agent-audio-visualizer-bar.ts
index fc397ca13..101e388e3 100644
--- a/packages/shadcn/components/agents-ui/audio-visualizer-bar/hooks/useBarAnimator.ts
+++ b/packages/shadcn/hooks/agents-ui/use-agent-audio-visualizer-bar.ts
@@ -18,11 +18,11 @@ function generateListeningSequenceBar(columns: number): number[][] {
return [[center], [noIndex]];
}
-export const useBarAnimator = (
+export function useAgentAudioVisualizerBarAnimator(
state: AgentState | undefined,
columns: number,
interval: number,
-): number[] => {
+): number[] {
const [index, setIndex] = useState(0);
const [sequence, setSequence] = useState([[]]);
@@ -67,4 +67,4 @@ export const useBarAnimator = (
}, [interval, columns, state, sequence.length]);
return sequence[index % sequence.length];
-};
+}
diff --git a/packages/shadcn/components/agents-ui/agent-control-bar/hooks/use-input-controls.ts b/packages/shadcn/hooks/agents-ui/use-agent-control-bar.ts
similarity index 73%
rename from packages/shadcn/components/agents-ui/agent-control-bar/hooks/use-input-controls.ts
rename to packages/shadcn/hooks/agents-ui/use-agent-control-bar.ts
index e89f173be..eb29b8a97 100644
--- a/packages/shadcn/components/agents-ui/agent-control-bar/hooks/use-input-controls.ts
+++ b/packages/shadcn/hooks/agents-ui/use-agent-control-bar.ts
@@ -2,11 +2,52 @@ import { useCallback, useMemo } from 'react';
import { Track } from 'livekit-client';
import {
type TrackReferenceOrPlaceholder,
+ useTrackToggle,
useLocalParticipant,
usePersistentUserChoices,
- useTrackToggle,
+ useLocalParticipantPermissions,
} from '@livekit/components-react';
+const trackSourceToProtocol = (source: Track.Source) => {
+ // NOTE: this mapping avoids importing the protocol package as that leads to a significant bundle size increase
+ switch (source) {
+ case Track.Source.Camera:
+ return 1;
+ case Track.Source.Microphone:
+ return 2;
+ case Track.Source.ScreenShare:
+ return 3;
+ default:
+ return 0;
+ }
+};
+
+export interface PublishPermissions {
+ camera: boolean;
+ microphone: boolean;
+ screenShare: boolean;
+ data: boolean;
+}
+
+export function usePublishPermissions(): PublishPermissions {
+ const localPermissions = useLocalParticipantPermissions();
+
+ const canPublishSource = (source: Track.Source) => {
+ return (
+ !!localPermissions?.canPublish &&
+ (localPermissions.canPublishSources.length === 0 ||
+ localPermissions.canPublishSources.includes(trackSourceToProtocol(source)))
+ );
+ };
+
+ return {
+ camera: canPublishSource(Track.Source.Camera),
+ microphone: canPublishSource(Track.Source.Microphone),
+ screenShare: canPublishSource(Track.Source.ScreenShare),
+ data: localPermissions?.canPublishData ?? false,
+ };
+}
+
export interface UseInputControlsProps {
saveUserChoices?: boolean;
onDisconnect?: () => void;
@@ -64,14 +105,14 @@ export function useInputControls({
(deviceId: string) => {
saveAudioInputDeviceId(deviceId ?? 'default');
},
- [saveAudioInputDeviceId]
+ [saveAudioInputDeviceId],
);
const handleVideoDeviceChange = useCallback(
(deviceId: string) => {
saveVideoInputDeviceId(deviceId ?? 'default');
},
- [saveVideoInputDeviceId]
+ [saveVideoInputDeviceId],
);
const handleToggleCamera = useCallback(
@@ -83,7 +124,7 @@ export function useInputControls({
// persist video input enabled preference
saveVideoInputEnabled(!cameraToggle.enabled);
},
- [cameraToggle, screenShareToggle, saveVideoInputEnabled]
+ [cameraToggle, screenShareToggle, saveVideoInputEnabled],
);
const handleToggleMicrophone = useCallback(
@@ -92,7 +133,7 @@ export function useInputControls({
// persist audio input enabled preference
saveAudioInputEnabled(!microphoneToggle.enabled);
},
- [microphoneToggle, saveAudioInputEnabled]
+ [microphoneToggle, saveAudioInputEnabled],
);
const handleToggleScreenShare = useCallback(
@@ -102,16 +143,16 @@ export function useInputControls({
}
await screenShareToggle.toggle(enabled);
},
- [cameraToggle, screenShareToggle]
+ [cameraToggle, screenShareToggle],
);
const handleMicrophoneDeviceSelectError = useCallback(
(error: Error) => onDeviceError?.({ source: Track.Source.Microphone, error }),
- [onDeviceError]
+ [onDeviceError],
);
const handleCameraDeviceSelectError = useCallback(
(error: Error) => onDeviceError?.({ source: Track.Source.Camera, error }),
- [onDeviceError]
+ [onDeviceError],
);
return {
diff --git a/packages/shadcn/index.ts b/packages/shadcn/index.ts
index a9b3a0427..b84395d14 100644
--- a/packages/shadcn/index.ts
+++ b/packages/shadcn/index.ts
@@ -1,5 +1,5 @@
-export * from './components/agents-ui/audio-visualizer-bar/audio-visualizer-bar';
+export * from './components/agents-ui/agent-audio-visualizer-bar';
export * from './components/agents-ui/agent-track-toggle';
export * from './components/agents-ui/agent-track-control';
export * from './components/agents-ui/agent-disconnect-button';
-export * from './components/agents-ui/agent-control-bar/agent-control-bar';
+export * from './components/agents-ui/agent-control-bar';
diff --git a/packages/shadcn/lib/clone-single-child.ts b/packages/shadcn/lib/clone-single-child.ts
deleted file mode 100644
index 19c71783a..000000000
--- a/packages/shadcn/lib/clone-single-child.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { ReactNode, Children, isValidElement, cloneElement, CSSProperties } from 'react';
-import { cn } from '@/lib/utils';
-
-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;
- });
-}
diff --git a/packages/shadcn/registry.json b/packages/shadcn/registry.json
new file mode 100644
index 000000000..ae187cc79
--- /dev/null
+++ b/packages/shadcn/registry.json
@@ -0,0 +1,118 @@
+{
+ "$schema": "https://ui.shadcn.com/schema/registry.json",
+ "name": "@agents-ui",
+ "homepage": "https://livekit.io/ui",
+ "items": [
+ {
+ "name": "agent-disconnect-button",
+ "type": "registry:component",
+ "title": "Agent Session Disconnect",
+ "description": "A button for disconnecting the agent session.",
+ "files": [
+ {
+ "path": "components/agents-ui/agent-disconnect-button.tsx",
+ "type": "registry:component"
+ }
+ ],
+
+ "dependencies": [
+ "@livekit/components-react@^2.0.0",
+ "class-variance-authority",
+ "lucide-react"
+ ],
+ "registryDependencies": ["button"]
+ },
+ {
+ "name": "agent-track-toggle",
+ "type": "registry:component",
+ "title": "Agent Track Toggle",
+ "description": "A toggle button for controlling media tracks (microphone, camera, screen share) with appropriate icons and loading states.",
+ "files": [
+ {
+ "path": "components/agents-ui/agent-track-toggle.tsx",
+ "type": "registry:component"
+ }
+ ],
+ "dependencies": [
+ "livekit-client@^2.0.0",
+ "@livekit/components-react@^2.0.0",
+ "class-variance-authority",
+ "lucide-react"
+ ],
+ "registryDependencies": ["toggle", "utils"]
+ },
+ {
+ "name": "agent-track-control",
+ "type": "registry:component",
+ "title": "Agent Track Control",
+ "description": "A control for managing media tracks (microphone, camera, screen share) with appropriate icons and loading states.",
+ "files": [
+ {
+ "path": "components/agents-ui/agent-track-control.tsx",
+ "type": "registry:component"
+ }
+ ],
+ "dependencies": [
+ "livekit-client@^2.0.0",
+ "@livekit/components-react@^2.0.0",
+ "class-variance-authority",
+ "lucide-react"
+ ],
+ "registryDependencies": ["select", "@agents-ui/agent-track-toggle", "utils"]
+ },
+ {
+ "name": "agent-control-bar",
+ "type": "registry:component",
+ "title": "Agent Control Bar",
+ "description": "A control bar for managing media tracks (microphone, camera, screen share) with appropriate icons and loading states.",
+ "files": [
+ {
+ "path": "components/agents-ui/agent-control-bar.tsx",
+ "type": "registry:component"
+ },
+ {
+ "path": "hooks/agents-ui/use-agent-control-bar.ts",
+ "type": "registry:hook"
+ }
+ ],
+ "dependencies": [
+ "livekit-client@^2.0.0",
+ "@livekit/components-react@^2.0.0",
+ "class-variance-authority",
+ "motion",
+ "lucide-react"
+ ],
+ "registryDependencies": [
+ "button",
+ "toggle",
+ "@agents-ui/agent-track-control",
+ "@agents-ui/agent-track-toggle",
+ "@agents-ui/agent-audio-visualizer-bar",
+ "utils"
+ ]
+ },
+ {
+ "name": "agent-audio-visualizer-bar",
+ "type": "registry:component",
+ "title": "Audio Bar Visualizer",
+ "description": "A linear bar visualizer for audio tracks.",
+ "files": [
+ {
+ "path": "components/agents-ui/agent-audio-visualizer-bar.tsx",
+ "type": "registry:component"
+ },
+ {
+ "path": "hooks/agents-ui/use-agent-audio-visualizer-bar.ts",
+ "type": "registry:hook"
+ }
+ ],
+ "dependencies": [
+ "livekit-client@^2.0.0",
+ "@livekit/components-react@^2.0.0",
+ "class-variance-authority",
+ "lucide-react"
+ ],
+ "registryDependencies": ["utils"]
+ }
+ ]
+}