Skip to content

Commit 4b7752e

Browse files
feat: LiveKit OSS Shadcn initial registry (#1251)
1 parent e5329db commit 4b7752e

File tree

12 files changed

+301
-176
lines changed

12 files changed

+301
-176
lines changed

docs/storybook/stories/agents-ui/AudioVisualizerBar.stories.tsx renamed to docs/storybook/stories/agents-ui/AgentAudioVisualizerBar.stories.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ import {
44
AgentSessionProvider,
55
useMicrophone,
66
} from '../../.storybook/lk-decorators/AgentSessionProvider';
7-
import { AudioVisualizerBar, AudioVisualizerBarProps } from '@agents-ui';
7+
import { AgentAudioVisualizerBar, AgentAudioVisualizerBarProps } from '@agents-ui';
88

99
export default {
10-
component: AudioVisualizerBar,
10+
component: AgentAudioVisualizerBar,
1111
decorators: [AgentSessionProvider],
12-
render: (args: AudioVisualizerBarProps) => {
12+
render: (args: AgentAudioVisualizerBarProps) => {
1313
const audioTrack = useMicrophone();
1414

15-
return <AudioVisualizerBar {...args} audioTrack={audioTrack} />;
15+
return <AgentAudioVisualizerBar {...args} audioTrack={audioTrack} />;
1616
},
1717
args: {
1818
size: 'xl',
@@ -51,6 +51,6 @@ export default {
5151
},
5252
};
5353

54-
export const Default: StoryObj<AudioVisualizerBarProps> = {
54+
export const Default: StoryObj<AgentAudioVisualizerBarProps> = {
5555
args: {},
5656
};

packages/shadcn/components/agents-ui/audio-visualizer-bar/audio-visualizer-bar.tsx renamed to packages/shadcn/components/agents-ui/agent-audio-visualizer-bar.tsx

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,48 @@
1-
import React, { type ReactNode, useMemo } from 'react';
1+
'use client';
2+
3+
import React, {
4+
type ReactNode,
5+
type CSSProperties,
6+
useMemo,
7+
Children,
8+
cloneElement,
9+
isValidElement,
10+
} from 'react';
211
import { type VariantProps, cva } from 'class-variance-authority';
312
import { type LocalAudioTrack, type RemoteAudioTrack } from 'livekit-client';
413
import {
514
type AgentState,
615
type TrackReferenceOrPlaceholder,
716
useMultibandTrackVolume,
817
} from '@livekit/components-react';
18+
import { useAgentAudioVisualizerBarAnimator } from '@/hooks/agents-ui/use-agent-audio-visualizer-bar';
919
import { cn } from '@/lib/utils';
10-
import { cloneSingleChild } from '@/lib/clone-single-child';
11-
import { useBarAnimator } from './hooks/useBarAnimator';
1220

13-
export const AudioVisualizerBarVariants = cva(
21+
export function cloneSingleChild(
22+
children: ReactNode | ReactNode[],
23+
props?: Record<string, unknown>,
24+
key?: unknown,
25+
) {
26+
return Children.map(children, (child) => {
27+
// Checking isValidElement is the safe way and avoids a typescript error too.
28+
if (isValidElement(child) && Children.only(children)) {
29+
const childProps = child.props as Record<string, unknown>;
30+
if (childProps.className) {
31+
// make sure we retain classnames of both passed props and child
32+
props ??= {};
33+
props.className = cn(childProps.className as string, props.className as string);
34+
props.style = {
35+
...(childProps.style as CSSProperties),
36+
...(props.style as CSSProperties),
37+
};
38+
}
39+
return cloneElement(child, { ...props, key: key ? String(key) : undefined });
40+
}
41+
return child;
42+
});
43+
}
44+
45+
export const AgentAudioVisualizerBarVariants = cva(
1446
[
1547
'relative flex items-center justify-center',
1648
'[&_>_*]:rounded-full [&_>_*]:transition-colors [&_>_*]:duration-250 [&_>_*]:ease-linear',
@@ -32,22 +64,22 @@ export const AudioVisualizerBarVariants = cva(
3264
},
3365
);
3466

35-
export interface AudioVisualizerBarProps {
67+
export interface AgentAudioVisualizerBarProps {
3668
state?: AgentState;
3769
barCount?: number;
3870
audioTrack?: LocalAudioTrack | RemoteAudioTrack | TrackReferenceOrPlaceholder;
3971
className?: string;
4072
children?: ReactNode | ReactNode[];
4173
}
4274

43-
export function AudioVisualizerBar({
75+
export function AgentAudioVisualizerBar({
4476
size,
4577
state,
4678
barCount,
4779
audioTrack,
4880
className,
4981
children,
50-
}: AudioVisualizerBarProps & VariantProps<typeof AudioVisualizerBarVariants>) {
82+
}: AgentAudioVisualizerBarProps & VariantProps<typeof AgentAudioVisualizerBarVariants>) {
5183
const _barCount = useMemo(() => {
5284
if (barCount) {
5385
return barCount;
@@ -82,14 +114,19 @@ export function AudioVisualizerBar({
82114
}
83115
}, [state, _barCount]);
84116

85-
const highlightedIndices = useBarAnimator(state, _barCount, sequencerInterval);
117+
const highlightedIndices = useAgentAudioVisualizerBarAnimator(
118+
state,
119+
_barCount,
120+
sequencerInterval,
121+
);
122+
86123
const bands = useMemo(
87124
() => (state === 'speaking' ? volumeBands : new Array(_barCount).fill(0)),
88125
[state, volumeBands, _barCount],
89126
);
90127

91128
return (
92-
<div className={cn(AudioVisualizerBarVariants({ size }), className)}>
129+
<div className={cn(AgentAudioVisualizerBarVariants({ size }), className)}>
93130
{bands.map((band: number, idx: number) =>
94131
children ? (
95132
<React.Fragment key={idx}>

packages/shadcn/components/agents-ui/agent-control-bar/agent-control-bar.tsx renamed to packages/shadcn/components/agents-ui/agent-control-bar.tsx

Lines changed: 75 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,24 @@
11
'use client';
22

3-
import { type HTMLAttributes, useState } from 'react';
3+
import { useEffect, useRef, type HTMLAttributes, useState } from 'react';
44
import { Track } from 'livekit-client';
55
import { motion } from 'motion/react';
66
import { useChat } from '@livekit/components-react';
7-
import { MessageSquareTextIcon } from 'lucide-react';
7+
import { Loader, MessageSquareTextIcon, SendHorizontal } from 'lucide-react';
8+
import { Toggle } from '@/components/ui/toggle';
9+
import { Button } from '@/components/ui/button';
810
import {
911
AgentTrackToggle,
1012
agentTrackToggleVariants,
1113
} from '@/components/agents-ui/agent-track-toggle';
1214
import { AgentTrackControl } from '@/components/agents-ui/agent-track-control';
13-
import { Toggle } from '@/components/ui/toggle';
15+
import { AgentDisconnectButton } from '@/components/agents-ui/agent-disconnect-button';
16+
import {
17+
useInputControls,
18+
usePublishPermissions,
19+
type UseInputControlsProps,
20+
} from '@/hooks/agents-ui/use-agent-control-bar';
1421
import { cn } from '@/lib/utils';
15-
import { AgentChatInput } from './agent-chat-input';
16-
import { UseInputControlsProps, useInputControls } from './hooks/use-input-controls';
17-
import { usePublishPermissions } from './hooks/use-publish-permissions';
18-
import { AgentDisconnectButton } from '../agent-disconnect-button';
1922

2023
const TOGGLE_VARIANT_1 = [
2124
'[&_[data-state=off]]:bg-accent [&_[data-state=off]]:hover:bg-foreground/10',
@@ -58,6 +61,71 @@ const MOTION_PROPS = {
5861
},
5962
};
6063

64+
interface AgentChatInputProps {
65+
chatOpen: boolean;
66+
onSend?: (message: string) => void;
67+
className?: string;
68+
}
69+
70+
export function AgentChatInput({
71+
chatOpen,
72+
onSend = async () => {},
73+
className,
74+
}: AgentChatInputProps) {
75+
const inputRef = useRef<HTMLTextAreaElement>(null);
76+
const [isSending, setIsSending] = useState(false);
77+
const [message, setMessage] = useState<string>('');
78+
79+
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
80+
e.preventDefault();
81+
82+
try {
83+
setIsSending(true);
84+
await onSend(message);
85+
setMessage('');
86+
} catch (error) {
87+
console.error(error);
88+
} finally {
89+
setIsSending(false);
90+
}
91+
};
92+
93+
const isDisabled = isSending || message.trim().length === 0;
94+
95+
useEffect(() => {
96+
if (chatOpen) return;
97+
// when not disabled refocus on input
98+
inputRef.current?.focus();
99+
}, [chatOpen]);
100+
101+
return (
102+
<form
103+
onSubmit={handleSubmit}
104+
className={cn('mb-3 flex grow items-end gap-2 rounded-md pl-1 text-sm', className)}
105+
>
106+
<textarea
107+
autoFocus
108+
ref={inputRef}
109+
value={message}
110+
disabled={!chatOpen}
111+
placeholder="Type something..."
112+
onChange={(e) => setMessage(e.target.value)}
113+
className="field-sizing-content max-h-16 min-h-8 flex-1 py-2 [scrollbar-width:thin] focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
114+
/>
115+
<Button
116+
size="icon"
117+
type="submit"
118+
disabled={isDisabled}
119+
variant={isDisabled ? 'secondary' : 'default'}
120+
title={isSending ? 'Sending...' : 'Send'}
121+
className="self-end disabled:cursor-not-allowed"
122+
>
123+
{isSending ? <Loader className="animate-spin" /> : <SendHorizontal />}
124+
</Button>
125+
</form>
126+
);
127+
}
128+
61129
export interface ControlBarControls {
62130
leave?: boolean;
63131
camera?: boolean;

packages/shadcn/components/agents-ui/agent-control-bar/agent-chat-input.tsx

Lines changed: 0 additions & 69 deletions
This file was deleted.

packages/shadcn/components/agents-ui/agent-control-bar/hooks/use-publish-permissions.ts

Lines changed: 0 additions & 42 deletions
This file was deleted.

packages/shadcn/components/agents-ui/agent-track-control.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
useMediaDeviceSelect,
1111
} from '@livekit/components-react';
1212

13-
import { AudioVisualizerBar } from '@/components/agents-ui/audio-visualizer-bar/audio-visualizer-bar';
13+
import { AgentAudioVisualizerBar } from '@/components/agents-ui/agent-audio-visualizer-bar';
1414
import { AgentTrackToggle } from '@/components/agents-ui/agent-track-toggle';
1515
import {
1616
Select,
@@ -192,7 +192,7 @@ export function AgentTrackControl({
192192
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"
193193
>
194194
{audioTrack && (
195-
<AudioVisualizerBar
195+
<AgentAudioVisualizerBar
196196
size="icon"
197197
barCount={3}
198198
audioTrack={audioTrack}
@@ -205,7 +205,7 @@ export function AgentTrackControl({
205205
'data-lk-muted:bg-muted',
206206
])}
207207
/>
208-
</AudioVisualizerBar>
208+
</AgentAudioVisualizerBar>
209209
)}
210210
</AgentTrackToggle>
211211
{kind && (

packages/shadcn/components/agents-ui/agent-track-toggle.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
'use client';
2-
31
import * as React from 'react';
42
import { cva } from 'class-variance-authority';
53
import { Track } from 'livekit-client';

packages/shadcn/components/agents-ui/audio-visualizer-bar/hooks/useBarAnimator.ts renamed to packages/shadcn/hooks/agents-ui/use-agent-audio-visualizer-bar.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ function generateListeningSequenceBar(columns: number): number[][] {
1818
return [[center], [noIndex]];
1919
}
2020

21-
export const useBarAnimator = (
21+
export function useAgentAudioVisualizerBarAnimator(
2222
state: AgentState | undefined,
2323
columns: number,
2424
interval: number,
25-
): number[] => {
25+
): number[] {
2626
const [index, setIndex] = useState(0);
2727
const [sequence, setSequence] = useState<number[][]>([[]]);
2828

@@ -67,4 +67,4 @@ export const useBarAnimator = (
6767
}, [interval, columns, state, sequence.length]);
6868

6969
return sequence[index % sequence.length];
70-
};
70+
}

0 commit comments

Comments
 (0)