Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 <AudioVisualizerBar {...args} audioTrack={audioTrack} />;
return <AgentAudioVisualizerBar {...args} audioTrack={audioTrack} />;
},
args: {
size: 'xl',
Expand Down Expand Up @@ -51,6 +51,6 @@ export default {
},
};

export const Default: StoryObj<AudioVisualizerBarProps> = {
export const Default: StoryObj<AgentAudioVisualizerBarProps> = {
args: {},
};
Original file line number Diff line number Diff line change
@@ -1,16 +1,48 @@
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 {
type AgentState,
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<string, unknown>,
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<string, unknown>;
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',
Expand All @@ -32,22 +64,22 @@ export const AudioVisualizerBarVariants = cva(
},
);

export interface AudioVisualizerBarProps {
export interface AgentAudioVisualizerBarProps {
state?: AgentState;
barCount?: number;
audioTrack?: LocalAudioTrack | RemoteAudioTrack | TrackReferenceOrPlaceholder;
className?: string;
children?: ReactNode | ReactNode[];
}

export function AudioVisualizerBar({
export function AgentAudioVisualizerBar({
size,
state,
barCount,
audioTrack,
className,
children,
}: AudioVisualizerBarProps & VariantProps<typeof AudioVisualizerBarVariants>) {
}: AgentAudioVisualizerBarProps & VariantProps<typeof AgentAudioVisualizerBarVariants>) {
const _barCount = useMemo(() => {
if (barCount) {
return barCount;
Expand Down Expand Up @@ -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 (
<div className={cn(AudioVisualizerBarVariants({ size }), className)}>
<div className={cn(AgentAudioVisualizerBarVariants({ size }), className)}>
{bands.map((band: number, idx: number) =>
children ? (
<React.Fragment key={idx}>
Expand Down
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -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<HTMLTextAreaElement>(null);
const [isSending, setIsSending] = useState(false);
const [message, setMessage] = useState<string>('');

const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
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 (
<form
onSubmit={handleSubmit}
className={cn('mb-3 flex grow items-end gap-2 rounded-md pl-1 text-sm', className)}
>
<textarea
autoFocus
ref={inputRef}
value={message}
disabled={!chatOpen}
placeholder="Type something..."
onChange={(e) => setMessage(e.target.value)}
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"
/>
<Button
size="icon"
type="submit"
disabled={isDisabled}
variant={isDisabled ? 'secondary' : 'default'}
title={isSending ? 'Sending...' : 'Send'}
className="self-end disabled:cursor-not-allowed"
>
{isSending ? <Loader className="animate-spin" /> : <SendHorizontal />}
</Button>
</form>
);
}

export interface ControlBarControls {
leave?: boolean;
camera?: boolean;
Expand Down

This file was deleted.

This file was deleted.

6 changes: 3 additions & 3 deletions packages/shadcn/components/agents-ui/agent-track-control.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 && (
<AudioVisualizerBar
<AgentAudioVisualizerBar
size="icon"
barCount={3}
audioTrack={audioTrack}
Expand All @@ -205,7 +205,7 @@ export function AgentTrackControl({
'data-lk-muted:bg-muted',
])}
/>
</AudioVisualizerBar>
</AgentAudioVisualizerBar>
)}
</AgentTrackToggle>
{kind && (
Expand Down
2 changes: 0 additions & 2 deletions packages/shadcn/components/agents-ui/agent-track-toggle.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
'use client';

import * as React from 'react';
import { cva } from 'class-variance-authority';
import { Track } from 'livekit-client';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<number[][]>([[]]);

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

return sequence[index % sequence.length];
};
}
Loading