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
27 changes: 27 additions & 0 deletions docs/storybook/stories/agents-ui/AgentChatIndicator.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';
import { StoryObj } from '@storybook/react-vite';
import { AgentSessionProvider } from '../../.storybook/lk-decorators/AgentSessionProvider';
import { AgentChatIndicator, AgentChatIndicatorProps } from '@agents-ui';

export default {
component: AgentChatIndicator,
decorators: [AgentSessionProvider],
render: (args: AgentChatIndicatorProps) => <AgentChatIndicator {...args} />,
args: {
agentState: 'thinking',
},
argTypes: {
size: {
options: ['sm', 'md', 'lg'],
control: { type: 'radio' },
},
},
parameters: {
layout: 'centered',
actions: { handles: [] },
},
};

export const Default: StoryObj<AgentChatIndicatorProps> = {
args: {},
};
74 changes: 74 additions & 0 deletions docs/storybook/stories/agents-ui/AgentChatTranscript.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import React from 'react';
import { StoryObj } from '@storybook/react-vite';
import { AgentSessionProvider } from '../../.storybook/lk-decorators/AgentSessionProvider';
import { AgentChatTranscript, AgentChatTranscriptProps } from '@agents-ui';

export default {
component: AgentChatTranscript,
decorators: [AgentSessionProvider],
render: (args: AgentChatTranscriptProps) => {
return (
<div className="w-96 h-dvh overflow-hidden mx-auto flex flex-col">
<AgentChatTranscript {...args} />
</div>
);
},
args: {
agentState: 'thinking',
messages: [
{
id: '1',
timestamp: new Date().toISOString(),
from: { isLocal: false },
message: 'Hi, how are you?',
},
{
id: '2',
timestamp: new Date().toISOString(),
from: { isLocal: true },
message: "I'm good, thank you!",
},
{
id: '3',
timestamp: new Date().toISOString(),
from: { isLocal: false },
message: 'This is a longer message that should wrap to the next line.',
},
{
id: '4',
timestamp: new Date().toISOString(),
from: { isLocal: true },
message: "Great I'm responding with an even longer message to see how it wraps.",
},
...Array.from({ length: 20 }).map((_, index) => ({
id: `${5 + index}`,
timestamp: new Date().toISOString(),
from: { isLocal: index % 2 === 1 },
message:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
})),
],
},
argTypes: {
agentState: {
control: 'radio',
options: [
'thinking',
'speaking',
'listening',
'idle',
'connecting',
'disconnected',
'failed',
],
},
},
parameters: {
layout: 'fullscreen',
actions: { handles: [] },
},
};

export const Default: StoryObj<AgentChatTranscriptProps> = {
args: {},
};
45 changes: 45 additions & 0 deletions docs/storybook/stories/agents-ui/Introduction.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Meta } from '@storybook/addon-docs/blocks';

<Meta title="Agents UI/Introduction" />

# Agents UI

Agents UI is a set of components for building voice-first agents using LiveKit.

## Setup

Configure our registry in your project by adding the following to your projects `components.json` file:

```json | components.json
{
"registries": {
"@agents-ui": "https://ui.livekit.io/r/{name}.json"
}
}
```

## Installation

```bash
pnpm dlx shadcn@latest add @agents-ui/{componentName}
```

## Implementation Example

```tsx
import { useSession } from '@livekit/components-react';
import { TokenSource } from 'livekit-client';
import { AgentSessionProvider } from '@agents-ui/agent-session-provider';
import { AgentControlBar } from '@agents-ui/agent-control-bar';

export default function Example() {
// using a sandbox token server for prototyping
const session = useSession(TokenSource.sandboxTokenServer('token-XXXXXX'));

return (
<AgentSessionProvider session={session}>
<AgentControlBar />
</AgentSessionProvider>
);
}
```
35 changes: 35 additions & 0 deletions docs/storybook/stories/agents-ui/StartAudioButton.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import * as React from 'react';
import { StoryObj } from '@storybook/react-vite';
import { AgentSessionProvider } from '../../.storybook/lk-decorators/AgentSessionProvider';
import { StartAudioButton, type StartAudioButtonProps } from '@agents-ui';

export default {
component: StartAudioButton,
decorators: [AgentSessionProvider],
render: (args: StartAudioButtonProps) => {
return (
<>
<p>A button will be rendered below if audio playback is blocked.</p>
<StartAudioButton {...args} />
</>
);
},
args: {
label: 'Click to allow audio playback',
},
argTypes: {
label: { control: { type: 'text' } },
onClick: { action: 'onClick' },
className: { control: { type: 'text' } },
},
parameters: {
layout: 'centered',
actions: {
handles: [],
},
},
};

export const Default: StoryObj<StartAudioButtonProps> = {
args: {},
};
1 change: 1 addition & 0 deletions packages/shadcn/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DEST_PATH=/path/to/dest/folder
2 changes: 2 additions & 0 deletions packages/shadcn/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ node_modules
.cache
dist
temp

!.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ export interface AgentAudioVisualizerBarProps {
}

export function AgentAudioVisualizerBar({
size,
state,
size = 'md',
state = 'connecting',
barCount,
audioTrack,
className,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,15 +159,15 @@ const GridCell = memo(function GridCell({
});

export type AgentAudioVisualizerGridProps = GridOptions & {
state: AgentState;
state?: AgentState;
audioTrack?: LocalAudioTrack | RemoteAudioTrack | TrackReferenceOrPlaceholder;
className?: string;
children?: ReactNode;
} & VariantProps<typeof AgentAudioVisualizerGridVariants>;

export function AgentAudioVisualizerGrid({
size = 'md',
state,
state = 'connecting',
radius,
rowCount: _rowCount = 5,
columnCount: _columnCount = 5,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ export interface AgentAudioVisualizerRadialProps {
}

export function AgentAudioVisualizerRadial({
size,
state,
size = 'md',
state = 'connecting',
radius,
barCount,
audioTrack,
Expand Down
57 changes: 57 additions & 0 deletions packages/shadcn/components/agents-ui/agent-chat-indicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { motion } from 'motion/react';
import { cva, VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';

const motionAnimationProps = {
variants: {
hidden: {
opacity: 0,
scale: 0.1,
transition: {
duration: 0.1,
ease: 'linear',
},
},
visible: {
opacity: [0.5, 1],
scale: [1, 1.2],
transition: {
type: 'spring',
bounce: 0,
duration: 0.5,
repeat: Infinity,
repeatType: 'mirror' as const,
},
},
},
initial: 'hidden',
animate: 'visible',
exit: 'hidden',
};

const agentChatIndicatorVariants = cva('bg-muted-foreground inline-block size-2.5 rounded-full', {
variants: {
size: {
sm: 'size-2.5',
md: 'size-4',
lg: 'size-6',
},
},
defaultVariants: {
size: 'md',
},
});

export interface AgentChatIndicatorProps extends VariantProps<typeof agentChatIndicatorVariants> {
className?: string;
}

export function AgentChatIndicator({ size, className }: AgentChatIndicatorProps) {
return (
<motion.span
{...motionAnimationProps}
transition={{ duration: 0.1, ease: 'linear' }}
className={cn(agentChatIndicatorVariants({ size }), className)}
/>
);
}
49 changes: 49 additions & 0 deletions packages/shadcn/components/agents-ui/agent-chat-transcript.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
'use client';

import { type AgentState, type ReceivedMessage } from '@livekit/components-react';
import {
Conversation,
ConversationContent,
ConversationScrollButton,
} from '@/components/ai-elements/conversation';
import { Message, MessageContent, MessageResponse } from '@/components/ai-elements/message';
import { AgentChatIndicator } from '@/components/agents-ui/agent-chat-indicator';
import { AnimatePresence } from 'motion/react';

export interface AgentChatTranscriptProps {
agentState?: AgentState;
messages?: ReceivedMessage[];
className?: string;
}

export function AgentChatTranscript({
agentState,
messages = [],
className,
}: AgentChatTranscriptProps) {
return (
<Conversation className={className}>
<ConversationContent>
{messages.map((receivedMessage) => {
const { id, timestamp, from, message } = receivedMessage;
const locale = navigator?.language ?? 'en-US';
const messageOrigin = from?.isLocal ? 'user' : 'assistant';
const time = new Date(timestamp);
const title = time.toLocaleTimeString(locale, { timeStyle: 'full' });

return (
<Message key={id} title={title} from={messageOrigin}>
<MessageContent>
<MessageResponse>{message}</MessageResponse>
</MessageContent>
</Message>
);
})}
<AnimatePresence>
{agentState === 'thinking' && <AgentChatIndicator size="sm" />}
</AnimatePresence>
</ConversationContent>
<ConversationScrollButton />
</Conversation>
);
}
21 changes: 21 additions & 0 deletions packages/shadcn/components/agents-ui/agent-session-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {
SessionProvider,
RoomAudioRenderer,
type SessionProviderProps,
type RoomAudioRendererProps,
} from '@livekit/components-react';

export type AgentSessionProviderProps = SessionProviderProps & RoomAudioRendererProps;

export function AgentSessionProvider({
session,
children,
...roomAudioRendererProps
}: AgentSessionProviderProps) {
return (
<SessionProvider session={session}>
{children}
<RoomAudioRenderer {...roomAudioRendererProps} />
</SessionProvider>
);
}
15 changes: 15 additions & 0 deletions packages/shadcn/components/agents-ui/start-audio-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useEnsureRoom, useStartAudio } from '@livekit/components-react';
import { Button } from '@/components/ui/button';
import { Room } from 'livekit-client';

export interface StartAudioButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
room?: Room;
label: string;
}

export function StartAudioButton({ label, room, ...props }: StartAudioButtonProps) {
const roomEnsured = useEnsureRoom(room);
const { mergedProps } = useStartAudio({ room: roomEnsured, props });

return <Button {...mergedProps}>{label}</Button>;
}
15 changes: 15 additions & 0 deletions packages/shadcn/components/session-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use client';
import type { ReactNode } from 'react';
import { TokenSource } from 'livekit-client';
import { useSession, SessionProvider as LiveKitSessionProvider } from '@livekit/components-react';

type SessionProviderProps = {
children: ReactNode;
};

export function SessionProvider({ children }: SessionProviderProps) {
const tokenSource = TokenSource.endpoint(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT);
const session = useSession(tokenSource);

return <LiveKitSessionProvider session={session}>{children}</LiveKitSessionProvider>;
}
Loading