From 90df4f885ee6a840dc197d9e71c68cf3580a0a6f Mon Sep 17 00:00:00 2001 From: Thomas Yuill Date: Fri, 10 Jul 2026 11:05:24 -0400 Subject: [PATCH 1/5] refactor(shadcn): replace ai-elements chat components with shadcn base primitives Drops the external @ai-elements registry dependency in agents-ui, rebuilding the chat transcript and pre-connect shimmer on shadcn's native MessageScroller, Message, and Bubble components plus the shimmer CSS utility. Also bumps Tailwind to 4.3.2 so the scroll button's logical-inset centering utility (inset-s-*, added in 4.2.0) actually resolves. --- docs/storybook/.storybook/tailwind.css | 1 + docs/storybook/package.json | 7 +- packages/shadcn/README.md | 3 +- packages/shadcn/components.json | 5 +- .../agents-ui/agent-chat-indicator.tsx | 4 +- .../agents-ui/agent-chat-transcript.tsx | 69 ++-- .../components/agent-session-block.tsx | 10 +- .../components/ai-elements/conversation.tsx | 87 ----- .../shadcn/components/ai-elements/message.tsx | 367 ------------------ .../shadcn/components/ai-elements/shimmer.tsx | 53 --- packages/shadcn/components/ui/bubble.tsx | 125 ++++++ .../shadcn/components/ui/message-scroller.tsx | 130 +++++++ packages/shadcn/components/ui/message.tsx | 92 +++++ packages/shadcn/package.json | 8 +- packages/shadcn/registry.json | 8 +- .../tests/agent-chat-transcript.test.tsx | 27 +- .../shadcn/tests/agent-session-block.test.tsx | 12 +- pnpm-lock.yaml | 210 ++++------ 18 files changed, 510 insertions(+), 708 deletions(-) delete mode 100644 packages/shadcn/components/ai-elements/conversation.tsx delete mode 100644 packages/shadcn/components/ai-elements/message.tsx delete mode 100644 packages/shadcn/components/ai-elements/shimmer.tsx create mode 100644 packages/shadcn/components/ui/bubble.tsx create mode 100644 packages/shadcn/components/ui/message-scroller.tsx create mode 100644 packages/shadcn/components/ui/message.tsx diff --git a/docs/storybook/.storybook/tailwind.css b/docs/storybook/.storybook/tailwind.css index 768e35784..8ec02c689 100644 --- a/docs/storybook/.storybook/tailwind.css +++ b/docs/storybook/.storybook/tailwind.css @@ -1,4 +1,5 @@ @import 'tailwindcss'; +@import 'shadcn/tailwind.css'; /* where to source the shadcn styles from */ @source './**/*.{js,ts,jsx,tsx}'; diff --git a/docs/storybook/package.json b/docs/storybook/package.json index 2d1889f23..a7fe2727d 100644 --- a/docs/storybook/package.json +++ b/docs/storybook/package.json @@ -27,16 +27,17 @@ "@storybook/react": "^10.1.4", "@storybook/react-vite": "^10.1.11", "@storybook/testing-library": "^0.2.2", - "@tailwindcss/postcss": "^4", - "@tailwindcss/vite": "^4.1.17", + "@tailwindcss/postcss": "^4.3.2", + "@tailwindcss/vite": "^4.3.2", "autoprefixer": "^10.4.22", "babel-loader": "^10.0.0", "eslint-config-lk-custom": "workspace:*", "eslint-plugin-storybook": "10.1.4", "next-themes": "^0.4.6", "postcss": "^8.5.15", + "shadcn": "^4.13.0", "storybook": "^10.1.4", - "tailwindcss": "^4.1.17", + "tailwindcss": "^4.3.2", "tsx": "^4.0.0", "typescript": "5.8.2", "vite": "^8.0.0", diff --git a/packages/shadcn/README.md b/packages/shadcn/README.md index 3c76b4839..a9b7d772a 100644 --- a/packages/shadcn/README.md +++ b/packages/shadcn/README.md @@ -2,7 +2,7 @@ Agents UI is the easiest way to build agentic voice applications faster on top of LiveKit primitives. -Agents UI is a component library built on top of [shadcn/ui](https://ui.shadcn.com/) and [AI Elements](https://ai-sdk.dev/elements) to accelerate building agentic applications on top of LiveKit's real-time platform. It provides pre-built components like controling IO, managing sessions, rendering transcripts, visualing audio streams, and more. +Agents UI is a component library built on top of [shadcn/ui](https://ui.shadcn.com/) to accelerate building agentic applications on top of LiveKit's real-time platform. It provides pre-built components like controling IO, managing sessions, rendering transcripts, visualing audio streams, and more. ## Components @@ -103,7 +103,6 @@ After installation, no additional setup is needed. The component's styles (Tailw packages/shadcn/ ├── components/ │ ├── agents-ui/ # LiveKit agent-specific components -│ ├── ai-elements/ # Reusable AI conversation components │ ├── ui/ # Base UI primitives (shadcn/ui style) │ └── session-provider.tsx ├── hooks/ diff --git a/packages/shadcn/components.json b/packages/shadcn/components.json index 9094cdde7..d1eb749aa 100644 --- a/packages/shadcn/components.json +++ b/packages/shadcn/components.json @@ -17,8 +17,5 @@ "lib": "@/lib", "hooks": "@/hooks" }, - "iconLibrary": "lucide", - "registries": { - "@ai-elements": "https://registry.ai-sdk.dev/{name}.json" - } + "iconLibrary": "lucide" } diff --git a/packages/shadcn/components/agents-ui/agent-chat-indicator.tsx b/packages/shadcn/components/agents-ui/agent-chat-indicator.tsx index d4f845c96..984b43385 100644 --- a/packages/shadcn/components/agents-ui/agent-chat-indicator.tsx +++ b/packages/shadcn/components/agents-ui/agent-chat-indicator.tsx @@ -16,7 +16,7 @@ const motionAnimationProps = { }, visible: { opacity: [0.5, 1], - scale: [1, 1.2], + scale: [0.9, 1], transition: { type: 'spring' as const, bounce: 0, @@ -34,7 +34,7 @@ const motionAnimationProps = { const agentChatIndicatorVariants = cva('bg-muted-foreground inline-block size-2.5 rounded-full', { variants: { size: { - sm: 'size-2.5', + sm: 'size-3', md: 'size-4', lg: 'size-6', }, diff --git a/packages/shadcn/components/agents-ui/agent-chat-transcript.tsx b/packages/shadcn/components/agents-ui/agent-chat-transcript.tsx index 1cd0749b9..f0c79d32d 100644 --- a/packages/shadcn/components/agents-ui/agent-chat-transcript.tsx +++ b/packages/shadcn/components/agents-ui/agent-chat-transcript.tsx @@ -2,12 +2,17 @@ import { type ComponentProps } from 'react'; import { type AgentState, type ReceivedMessage } from '@livekit/components-react'; +import { Streamdown } from 'streamdown'; +import { Bubble, BubbleContent } from '@/components/ui/bubble'; +import { Message, MessageContent } from '@/components/ui/message'; import { - Conversation, - ConversationContent, - ConversationScrollButton, -} from '@/components/ai-elements/conversation'; -import { Message, MessageContent, MessageResponse } from '@/components/ai-elements/message'; + MessageScroller, + MessageScrollerButton, + MessageScrollerContent, + MessageScrollerItem, + MessageScrollerProvider, + MessageScrollerViewport, +} from '@/components/ui/message-scroller'; import { AgentChatIndicator } from '@/components/agents-ui/agent-chat-indicator'; import { AnimatePresence } from 'motion/react'; @@ -52,28 +57,38 @@ export function AgentChatTranscript({ ...props }: AgentChatTranscriptProps) { return ( - - - {messages.map((receivedMessage) => { - const { id, timestamp, from, message } = receivedMessage; - const time = new Date(timestamp); - const messageOrigin = from?.isLocal ? 'user' : 'assistant'; - const locale = typeof navigator !== 'undefined' ? navigator.language : 'en-US'; - const title = time.toLocaleTimeString(locale, { timeStyle: 'full' }); + + + + + {messages.map((receivedMessage) => { + const { id, timestamp, from, message } = receivedMessage; + const time = new Date(timestamp); + const isUser = from?.isLocal; + const locale = typeof navigator !== 'undefined' ? navigator.language : 'en-US'; + const title = time.toLocaleTimeString(locale, { timeStyle: 'full' }); - return ( - - - {message} - - - ); - })} - - {agentState === 'thinking' && } - - - - + return ( + + + + + + {message} + + + + + + ); + })} + + {agentState === 'thinking' && } + + + + + + ); } diff --git a/packages/shadcn/components/agents-ui/blocks/agent-session-view-01/components/agent-session-block.tsx b/packages/shadcn/components/agents-ui/blocks/agent-session-view-01/components/agent-session-block.tsx index ca7e26cc3..1d23250d0 100644 --- a/packages/shadcn/components/agents-ui/blocks/agent-session-view-01/components/agent-session-block.tsx +++ b/packages/shadcn/components/agents-ui/blocks/agent-session-view-01/components/agent-session-block.tsx @@ -8,12 +8,9 @@ import { AgentControlBar, type AgentControlBarControls, } from '@/components/agents-ui/agent-control-bar'; -import { Shimmer } from '@/components/ai-elements/shimmer'; import { cn } from '@/lib/utils'; import { TileLayout } from './tile-view'; -const MotionMessage = motion.create(Shimmer); - const BOTTOM_VIEW_MOTION_PROPS: MotionProps = { variants: { visible: { @@ -252,15 +249,14 @@ export function AgentSessionView_01({ {isPreConnectBufferEnabled && ( {messages.length === 0 && ( - 0} {...SHIMMER_MOTION_PROPS} - className="pointer-events-none mx-auto block w-full max-w-2xl pb-4 text-center text-sm font-semibold" + className="shimmer shimmer-duration-2000 pointer-events-none mx-auto block w-full max-w-2xl pb-4 text-center text-sm font-semibold" > {preConnectMessage} - + )} )} diff --git a/packages/shadcn/components/ai-elements/conversation.tsx b/packages/shadcn/components/ai-elements/conversation.tsx deleted file mode 100644 index bf911361b..000000000 --- a/packages/shadcn/components/ai-elements/conversation.tsx +++ /dev/null @@ -1,87 +0,0 @@ -'use client'; - -import { Button } from '@/components/ui/button'; -import { cn } from '@/lib/utils'; -import { ArrowDownIcon } from 'lucide-react'; -import type { ComponentProps } from 'react'; -import { useCallback } from 'react'; -import { StickToBottom, useStickToBottomContext } from 'use-stick-to-bottom'; - -export type ConversationProps = ComponentProps; - -export const Conversation = ({ className, ...props }: ConversationProps) => ( - -); - -export type ConversationContentProps = ComponentProps; - -export const ConversationContent = ({ className, ...props }: ConversationContentProps) => ( - -); - -export type ConversationEmptyStateProps = ComponentProps<'div'> & { - title?: string; - description?: string; - icon?: React.ReactNode; -}; - -export const ConversationEmptyState = ({ - className, - title = 'No messages yet', - description = 'Start a conversation to see messages here', - icon, - children, - ...props -}: ConversationEmptyStateProps) => ( -
- {children ?? ( - <> - {icon &&
{icon}
} -
-

{title}

- {description &&

{description}

} -
- - )} -
-); - -export type ConversationScrollButtonProps = ComponentProps; - -export const ConversationScrollButton = ({ - className, - ...props -}: ConversationScrollButtonProps) => { - const { isAtBottom, scrollToBottom } = useStickToBottomContext(); - - const handleScrollToBottom = useCallback(() => { - scrollToBottom(); - }, [scrollToBottom]); - - return ( - !isAtBottom && ( - - ) - ); -}; diff --git a/packages/shadcn/components/ai-elements/message.tsx b/packages/shadcn/components/ai-elements/message.tsx deleted file mode 100644 index b1642b0a2..000000000 --- a/packages/shadcn/components/ai-elements/message.tsx +++ /dev/null @@ -1,367 +0,0 @@ -'use client'; - -import { Button } from '@/components/ui/button'; -import { ButtonGroup, ButtonGroupText } from '@/components/ui/button-group'; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; -import { cn } from '@/lib/utils'; -import type { FileUIPart, UIMessage } from 'ai'; -import { ChevronLeftIcon, ChevronRightIcon, PaperclipIcon, XIcon } from 'lucide-react'; -import type { ComponentProps, HTMLAttributes, ReactElement } from 'react'; -import { createContext, memo, useContext, useEffect, useState } from 'react'; -import { Streamdown } from 'streamdown'; - -export type MessageProps = HTMLAttributes & { - from: UIMessage['role']; -}; - -export const Message = ({ className, from, ...props }: MessageProps) => ( -
-); - -export type MessageContentProps = HTMLAttributes; - -export const MessageContent = ({ children, className, ...props }: MessageContentProps) => ( -
- {children} -
-); - -export type MessageActionsProps = ComponentProps<'div'>; - -export const MessageActions = ({ className, children, ...props }: MessageActionsProps) => ( -
- {children} -
-); - -export type MessageActionProps = ComponentProps & { - tooltip?: string; - label?: string; -}; - -export const MessageAction = ({ - tooltip, - children, - label, - variant = 'ghost', - size = 'icon-sm', - ...props -}: MessageActionProps) => { - const button = ( - - ); - - if (tooltip) { - return ( - - - {button} - -

{tooltip}

-
-
-
- ); - } - - return button; -}; - -type MessageBranchContextType = { - currentBranch: number; - totalBranches: number; - goToPrevious: () => void; - goToNext: () => void; - branches: ReactElement[]; - setBranches: (branches: ReactElement[]) => void; -}; - -const MessageBranchContext = createContext(null); - -const useMessageBranch = () => { - const context = useContext(MessageBranchContext); - - if (!context) { - throw new Error('MessageBranch components must be used within MessageBranch'); - } - - return context; -}; - -export type MessageBranchProps = HTMLAttributes & { - defaultBranch?: number; - onBranchChange?: (branchIndex: number) => void; -}; - -export const MessageBranch = ({ - defaultBranch = 0, - onBranchChange, - className, - ...props -}: MessageBranchProps) => { - const [currentBranch, setCurrentBranch] = useState(defaultBranch); - const [branches, setBranches] = useState([]); - - const handleBranchChange = (newBranch: number) => { - setCurrentBranch(newBranch); - onBranchChange?.(newBranch); - }; - - const goToPrevious = () => { - const newBranch = currentBranch > 0 ? currentBranch - 1 : branches.length - 1; - handleBranchChange(newBranch); - }; - - const goToNext = () => { - const newBranch = currentBranch < branches.length - 1 ? currentBranch + 1 : 0; - handleBranchChange(newBranch); - }; - - const contextValue: MessageBranchContextType = { - currentBranch, - totalBranches: branches.length, - goToPrevious, - goToNext, - branches, - setBranches, - }; - - return ( - -
div]:pb-0', className)} {...props} /> - - ); -}; - -export type MessageBranchContentProps = HTMLAttributes; - -export const MessageBranchContent = ({ children, ...props }: MessageBranchContentProps) => { - const { currentBranch, setBranches, branches } = useMessageBranch(); - const childrenArray = Array.isArray(children) ? children : [children]; - - // Use useEffect to update branches when they change - useEffect(() => { - if (branches.length !== childrenArray.length) { - setBranches(childrenArray); - } - }, [childrenArray, branches, setBranches]); - - return childrenArray.map((branch, index) => ( -
div]:pb-0', - index === currentBranch ? 'block' : 'hidden', - )} - key={branch.key} - {...props} - > - {branch} -
- )); -}; - -export type MessageBranchSelectorProps = HTMLAttributes & { - from: UIMessage['role']; -}; - -export const MessageBranchSelector = ({ - className, - from, - ...props -}: MessageBranchSelectorProps) => { - const { totalBranches } = useMessageBranch(); - - // Don't render if there's only one branch - if (totalBranches <= 1) { - return null; - } - - return ( - - ); -}; - -export type MessageBranchPreviousProps = ComponentProps; - -export const MessageBranchPrevious = ({ children, ...props }: MessageBranchPreviousProps) => { - const { goToPrevious, totalBranches } = useMessageBranch(); - - return ( - - ); -}; - -export type MessageBranchNextProps = ComponentProps; - -export const MessageBranchNext = ({ children, className, ...props }: MessageBranchNextProps) => { - const { goToNext, totalBranches } = useMessageBranch(); - - return ( - - ); -}; - -export type MessageBranchPageProps = HTMLAttributes; - -export const MessageBranchPage = ({ className, ...props }: MessageBranchPageProps) => { - const { currentBranch, totalBranches } = useMessageBranch(); - - return ( - - {currentBranch + 1} of {totalBranches} - - ); -}; - -export type MessageResponseProps = ComponentProps; - -export const MessageResponse = memo( - ({ className, ...props }: MessageResponseProps) => ( - *:first-child]:mt-0 [&>*:last-child]:mb-0', className)} - {...props} - /> - ), - (prevProps, nextProps) => prevProps.children === nextProps.children, -); - -MessageResponse.displayName = 'MessageResponse'; - -export type MessageAttachmentProps = HTMLAttributes & { - data: FileUIPart; - className?: string; - onRemove?: () => void; -}; - -export function MessageAttachment({ data, className, onRemove, ...props }: MessageAttachmentProps) { - const filename = data.filename || ''; - const mediaType = data.mediaType?.startsWith('image/') && data.url ? 'image' : 'file'; - const isImage = mediaType === 'image'; - const attachmentLabel = filename || (isImage ? 'Image' : 'Attachment'); - - return ( -
- {isImage ? ( - <> - {filename - {onRemove && ( - - )} - - ) : ( - <> - - -
- -
-
- -

{attachmentLabel}

-
-
- {onRemove && ( - - )} - - )} -
- ); -} - -export type MessageAttachmentsProps = ComponentProps<'div'>; - -export function MessageAttachments({ children, className, ...props }: MessageAttachmentsProps) { - if (!children) { - return null; - } - - return ( -
- {children} -
- ); -} - -export type MessageToolbarProps = ComponentProps<'div'>; - -export const MessageToolbar = ({ className, children, ...props }: MessageToolbarProps) => ( -
- {children} -
-); diff --git a/packages/shadcn/components/ai-elements/shimmer.tsx b/packages/shadcn/components/ai-elements/shimmer.tsx deleted file mode 100644 index 2509f5a83..000000000 --- a/packages/shadcn/components/ai-elements/shimmer.tsx +++ /dev/null @@ -1,53 +0,0 @@ -'use client'; - -import { cn } from '@/lib/utils'; -import { motion } from 'motion/react'; -import { type CSSProperties, type ElementType, type JSX, memo, useMemo } from 'react'; - -export type TextShimmerProps = { - children: string; - as?: ElementType; - className?: string; - duration?: number; - spread?: number; -}; - -const ShimmerComponent = ({ - children, - as: Component = 'p', - className, - duration = 2, - spread = 2, -}: TextShimmerProps) => { - const MotionComponent = motion.create(Component as keyof JSX.IntrinsicElements); - - const dynamicSpread = useMemo(() => (children?.length ?? 0) * spread, [children, spread]); - - return ( - - {children} - - ); -}; - -export const Shimmer = memo(ShimmerComponent); diff --git a/packages/shadcn/components/ui/bubble.tsx b/packages/shadcn/components/ui/bubble.tsx new file mode 100644 index 000000000..792146e61 --- /dev/null +++ b/packages/shadcn/components/ui/bubble.tsx @@ -0,0 +1,125 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from "radix-ui" + +import { cn } from "@/lib/utils" + +function BubbleGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +const bubbleVariants = cva( + "group/bubble relative flex w-fit max-w-[80%] min-w-0 flex-col gap-1 group-data-[align=end]/message:self-end data-[align=end]:self-end data-[variant=ghost]:max-w-full", + { + variants: { + variant: { + default: + "*:data-[slot=bubble-content]:bg-primary *:data-[slot=bubble-content]:text-primary-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-primary/80", + secondary: + "*:data-[slot=bubble-content]:bg-secondary *:data-[slot=bubble-content]:text-secondary-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)]", + muted: + "*:data-[slot=bubble-content]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[color-mix(in_oklch,var(--muted),var(--foreground)_5%)]", + tinted: + "*:data-[slot=bubble-content]:bg-[oklch(from_var(--primary)_0.93_calc(c*0.4)_h)] *:data-[slot=bubble-content]:text-foreground dark:*:data-[slot=bubble-content]:bg-[oklch(from_var(--primary)_0.3_calc(c*0.4)_h)] [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[oklch(from_var(--primary)_0.88_calc(c*0.5)_h)] dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-[oklch(from_var(--primary)_0.35_calc(c*0.5)_h)]", + outline: + "*:data-[slot=bubble-content]:border-border *:data-[slot=bubble-content]:bg-background [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:text-foreground dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-input/30", + ghost: + "border-none *:data-[slot=bubble-content]:rounded-none *:data-[slot=bubble-content]:bg-transparent *:data-[slot=bubble-content]:p-0 [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:text-foreground dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted/50", + destructive: + "*:data-[slot=bubble-content]:bg-destructive/10 *:data-[slot=bubble-content]:text-destructive dark:*:data-[slot=bubble-content]:bg-destructive/20 [&>[data-slot=bubble-content]:is(button,a):hover]:bg-destructive/20 dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-destructive/30", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Bubble({ + variant = "default", + align = "start", + className, + ...props +}: React.ComponentProps<"div"> & + VariantProps & { + align?: "start" | "end" + }) { + return ( +
+ ) +} + +function BubbleContent({ + asChild = false, + className, + ...props +}: React.ComponentProps<"div"> & { + asChild?: boolean +}) { + const Comp = asChild ? Slot.Root : "div" + + return ( + + ) +} + +const bubbleReactionsVariants = cva( + "absolute z-10 flex w-fit shrink-0 items-center justify-center gap-1 rounded-full bg-muted px-1.5 py-0.5 text-sm ring-3 ring-card has-[button]:p-0", + { + variants: { + side: { + top: "top-0 -translate-y-3/4", + bottom: "bottom-0 translate-y-3/4", + }, + align: { + start: "left-3", + end: "right-3", + }, + }, + defaultVariants: { + side: "bottom", + align: "end", + }, + } +) + +function BubbleReactions({ + side = "bottom", + align = "end", + className, + ...props +}: React.ComponentProps<"div"> & { + align?: "start" | "end" + side?: "top" | "bottom" +}) { + return ( +
+ ) +} + +export { BubbleGroup, Bubble, BubbleContent, BubbleReactions } diff --git a/packages/shadcn/components/ui/message-scroller.tsx b/packages/shadcn/components/ui/message-scroller.tsx new file mode 100644 index 000000000..0b0501125 --- /dev/null +++ b/packages/shadcn/components/ui/message-scroller.tsx @@ -0,0 +1,130 @@ +"use client" + +import * as React from "react" +import { + MessageScroller as MessageScrollerPrimitive, + useMessageScroller, + useMessageScrollerScrollable, + useMessageScrollerVisibility, +} from "@shadcn/react/message-scroller" +import { ArrowDownIcon } from "lucide-react" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" + +function MessageScrollerProvider( + props: React.ComponentProps +) { + return +} + +function MessageScroller({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function MessageScrollerViewport({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function MessageScrollerContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function MessageScrollerItem({ + className, + scrollAnchor = false, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function MessageScrollerButton({ + direction = "end", + className, + children, + render, + variant = "secondary", + size = "icon-sm", + ...props +}: React.ComponentProps & + Pick, "variant" | "size">) { + return ( + } + {...props} + > + {children ?? ( + <> + + + {direction === "end" ? "Scroll to end" : "Scroll to start"} + + + )} + + ) +} + +export { + MessageScrollerProvider, + MessageScroller, + MessageScrollerViewport, + MessageScrollerContent, + MessageScrollerItem, + MessageScrollerButton, + useMessageScroller, + useMessageScrollerScrollable, + useMessageScrollerVisibility, +} diff --git a/packages/shadcn/components/ui/message.tsx b/packages/shadcn/components/ui/message.tsx new file mode 100644 index 000000000..53437e8db --- /dev/null +++ b/packages/shadcn/components/ui/message.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function MessageGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function Message({ + className, + align = "start", + ...props +}: React.ComponentProps<"div"> & { align?: "start" | "end" }) { + return ( +
+ ) +} + +function MessageAvatar({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function MessageContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function MessageHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function MessageFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + MessageGroup, + Message, + MessageAvatar, + MessageContent, + MessageFooter, + MessageHeader, +} diff --git a/packages/shadcn/package.json b/packages/shadcn/package.json index a3ff777a1..2d5ad653a 100644 --- a/packages/shadcn/package.json +++ b/packages/shadcn/package.json @@ -10,7 +10,7 @@ "registry:serve": "python3 -m http.server 3210 -d ./dist", "registry:doc-gen": "node --experimental-strip-types --env-file=.env.local ./scripts/doc-gen.ts", "registry:update": "pnpm registry:build && node --experimental-strip-types --env-file=.env.local ./scripts/update.ts", - "shadcn:update": "pnpm dlx shadcn@latest add alert button button-group select separator sonner toggle tooltip @ai-elements/conversation @ai-elements/message @ai-elements/shimmer && pnpm format", + "shadcn:update": "pnpm dlx shadcn@latest add alert button button-group select separator sonner toggle tooltip message-scroller message bubble && pnpm format", "test": "vitest --run", "test:watch": "vitest", "test:coverage": "vitest --coverage", @@ -32,6 +32,7 @@ "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-tooltip": "^1.2.8", + "@shadcn/react": "^0.2.1", "ai": "^5.0.105", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -43,8 +44,7 @@ "radix-ui": "^1.6.2", "sonner": "^2.0.7", "streamdown": "^2.3.0", - "tailwind-merge": "^3.3.0", - "use-stick-to-bottom": "^1.1.1" + "tailwind-merge": "^3.3.0" }, "devDependencies": { "@eslint/eslintrc": "^3", @@ -68,7 +68,7 @@ "prettier": "^3.4.2", "prettier-plugin-tailwindcss": "^0.6.11", "react-docgen-typescript": "^2.4.0", - "shadcn": "^3.8.5", + "shadcn": "^4.13.0", "tailwindcss": "^4", "typescript": "^5", "vite": "^8.0.0", diff --git a/packages/shadcn/registry.json b/packages/shadcn/registry.json index dde08e928..99ab231a6 100644 --- a/packages/shadcn/registry.json +++ b/packages/shadcn/registry.json @@ -211,10 +211,11 @@ "type": "registry:component" } ], - "dependencies": ["@livekit/components-react@^2.0.0", "motion"], + "dependencies": ["@livekit/components-react@^2.0.0", "motion", "streamdown"], "registryDependencies": [ - "@ai-elements/message", - "@ai-elements/conversation", + "message", + "bubble", + "message-scroller", "@agents-ui/agent-chat-indicator" ] }, @@ -315,7 +316,6 @@ ], "registryDependencies": [ "utils", - "@ai-elements/shimmer", "@agents-ui/agent-control-bar", "@agents-ui/agent-chat-transcript", "@agents-ui/agent-audio-visualizer-aura", diff --git a/packages/shadcn/tests/agent-chat-transcript.test.tsx b/packages/shadcn/tests/agent-chat-transcript.test.tsx index c8c6bf49c..afcec95af 100644 --- a/packages/shadcn/tests/agent-chat-transcript.test.tsx +++ b/packages/shadcn/tests/agent-chat-transcript.test.tsx @@ -2,26 +2,37 @@ import { describe, it, expect, vi } from 'vitest'; import { render, screen, fireEvent } from '@testing-library/react'; import { AgentChatTranscript } from '@/components/agents-ui/agent-chat-transcript'; -vi.mock('@/components/ai-elements/conversation', () => ({ - Conversation: ({ children, ...props }: any) => ( +vi.mock('@/components/ui/message-scroller', () => ({ + MessageScrollerProvider: ({ children }: any) => <>{children}, + MessageScroller: ({ children, ...props }: any) => (
{children}
), - ConversationContent: ({ children }: any) => ( + MessageScrollerViewport: ({ children }: any) => <>{children}, + MessageScrollerContent: ({ children }: any) => (
{children}
), - ConversationScrollButton: () =>
, + MessageScrollerItem: ({ children }: any) => <>{children}, + MessageScrollerButton: () =>
, })); -vi.mock('@/components/ai-elements/message', () => ({ - Message: ({ children, from, title }: any) => ( -
+vi.mock('@/components/ui/message', () => ({ + Message: ({ children, align, title }: any) => ( +
{children}
), MessageContent: ({ children }: any) =>
{children}
, - MessageResponse: ({ children }: any) =>
{children}
, +})); + +vi.mock('@/components/ui/bubble', () => ({ + Bubble: ({ children }: any) => <>{children}, + BubbleContent: ({ children }: any) =>
{children}
, +})); + +vi.mock('streamdown', () => ({ + Streamdown: ({ children }: any) => <>{children}, })); vi.mock('@/components/agents-ui/agent-chat-indicator', () => ({ diff --git a/packages/shadcn/tests/agent-session-block.test.tsx b/packages/shadcn/tests/agent-session-block.test.tsx index 43414a92d..b9b7e8f86 100644 --- a/packages/shadcn/tests/agent-session-block.test.tsx +++ b/packages/shadcn/tests/agent-session-block.test.tsx @@ -38,20 +38,10 @@ vi.mock('@/components/agents-ui/agent-chat-transcript', () => ({ ), })); -vi.mock('@/components/ai-elements/shimmer', () => ({ - Shimmer: ({ children, ...props }: any) => ( -
- {children} -
- ), -})); - vi.mock('motion/react', () => ({ motion: { div: ({ children, ...props }: any) =>
{children}
, - create: (Component: any) => (props: any) => ( - - ), + p: ({ children, ...props }: any) =>

{children}

, }, AnimatePresence: ({ children }: any) => <>{children}, })); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cd6f4d224..667a51f79 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -117,10 +117,10 @@ importers: specifier: ^0.2.2 version: 0.2.2 '@tailwindcss/postcss': - specifier: ^4 + specifier: ^4.3.2 version: 4.3.2 '@tailwindcss/vite': - specifier: ^4.1.17 + specifier: ^4.3.2 version: 4.3.2(vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.0)(yaml@2.9.0)) autoprefixer: specifier: ^10.4.22 @@ -140,11 +140,14 @@ importers: postcss: specifier: ^8.5.15 version: 8.5.16 + shadcn: + specifier: ^4.13.0 + version: 4.13.0(supports-color@8.1.1)(typescript@5.8.2) storybook: specifier: ^10.1.4 version: 10.5.0(@types/react@19.2.17)(prettier@3.9.5)(react@19.2.7) tailwindcss: - specifier: ^4.1.17 + specifier: ^4.3.2 version: 4.3.2 tsx: specifier: ^4.0.0 @@ -386,6 +389,9 @@ importers: '@radix-ui/react-tooltip': specifier: ^1.2.8 version: 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@shadcn/react': + specifier: ^0.2.1 + version: 0.2.1(@types/react@19.2.17)(react@19.2.7) ai: specifier: ^5.0.105 version: 5.0.210(zod@4.4.3) @@ -431,9 +437,6 @@ importers: tailwind-merge: specifier: ^3.3.0 version: 3.6.0 - use-stick-to-bottom: - specifier: ^1.1.1 - version: 1.1.6(react@19.2.7) devDependencies: '@eslint/eslintrc': specifier: ^3 @@ -499,8 +502,8 @@ importers: specifier: ^2.4.0 version: 2.4.0(typescript@5.8.2) shadcn: - specifier: ^3.8.5 - version: 3.8.5(@types/node@24.13.3)(supports-color@8.1.1)(typescript@5.8.2) + specifier: ^4.13.0 + version: 4.13.0(supports-color@8.1.1)(typescript@5.8.2) tailwindcss: specifier: ^4 version: 4.3.2 @@ -695,10 +698,6 @@ packages: '@antfu/install-pkg@1.1.0': resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} - '@antfu/ni@25.0.0': - resolution: {integrity: sha512-9q/yCljni37pkMr4sPrI3G4jqdIk074+iukc5aFJl7kmDCCsiJrbZ6zKxnES1Gwg+i9RcDZwvktl23puGslmvA==} - hasBin: true - '@asamuzakjp/css-color@5.1.11': resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -3712,6 +3711,17 @@ packages: '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + '@shadcn/react@0.2.1': + resolution: {integrity: sha512-5krgi3dRMKb5jH6a+qPzVJUy/54s0kKE4Rw4LjDfLqOdVQTWKUgxWf1kW8r912I0jX/Lzxqc+pgjkjWxUIK5BQ==} + peerDependencies: + '@types/react': '>=19' + react: '>=19' + peerDependenciesMeta: + '@types/react': + optional: true + react: + optional: true + '@sinclair/typebox@0.34.50': resolution: {integrity: sha512-ydBWw0G6WFwWHzh9RK4B5c690UkreOG0llq0r+DaI7LgKgxigf8mhHzIPI3S0850g1BPkq/zpuCfrq4QFgUlTQ==} @@ -4774,10 +4784,6 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - agent-base@7.1.4: - resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} - engines: {node: '>= 14'} - ai@5.0.210: resolution: {integrity: sha512-HAYfzP0vMU3/1GzjQkpJT7/x7BqacIYimAMgm6wXZKjZfOHGv1458hLDqYcC7o9BuAr23sFijvL41MSx9KEnEQ==} engines: {node: '>=18'} @@ -4853,10 +4859,6 @@ packages: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} - ansis@4.3.1: - resolution: {integrity: sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==} - engines: {node: '>=14'} - any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} @@ -5554,10 +5556,6 @@ packages: resolution: {integrity: sha512-hpA5C/YrPjucXypHPPc0oJ1l9Hf6wWbiOL7Ik42cxnsUOhWiCB/fylKbKqqJalW9FgkNQCw16YO8uW9Hs0Iy1A==} engines: {node: '>=4'} - data-uri-to-buffer@4.0.1: - resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} - engines: {node: '>= 12'} - data-urls@7.0.0: resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -6211,10 +6209,6 @@ packages: picomatch: optional: true - fetch-blob@3.2.0: - resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} - engines: {node: ^12.20 || >= 14.13} - figures@6.1.0: resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} engines: {node: '>=18'} @@ -6261,10 +6255,6 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} - formdata-polyfill@4.0.10: - resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} - engines: {node: '>=12.20.0'} - forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -6323,9 +6313,6 @@ packages: fuzzysort@3.1.0: resolution: {integrity: sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==} - fzf@0.5.2: - resolution: {integrity: sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==} - generator-function@2.0.1: resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} engines: {node: '>= 0.4'} @@ -6539,10 +6526,6 @@ packages: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} - https-proxy-agent@7.0.6: - resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} - engines: {node: '>= 14'} - human-id@4.2.0: resolution: {integrity: sha512-K3GbkIWqyvvlpfhBPlbEvD97TtqBpAYA4kt+cn2lD2x2HuohzZCibcA2nOlnJT6exqvJLggoB5nv2dNf192nEA==} hasBin: true @@ -7762,19 +7745,10 @@ packages: node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} - node-domexception@1.0.0: - resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} - engines: {node: '>=10.5.0'} - deprecated: Use your platform's native DOMException instead - node-exports-info@1.6.2: resolution: {integrity: sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==} engines: {node: '>= 0.4'} - node-fetch@3.3.2: - resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} @@ -8645,8 +8619,9 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - shadcn@3.8.5: - resolution: {integrity: sha512-jPRx44e+eyeV7xwY3BLJXcfrks00+M0h5BGB9l6DdcBW4BpAj4x3lVmVy0TXPEs2iHEisxejr62sZAAw6B1EVA==} + shadcn@4.13.0: + resolution: {integrity: sha512-5fuJ4jI/GcPeA/iTL4cJivCZuYQGXz/N3bIzyd+Gd/FM6xUCy2MxGG+LaDQuw2cjNy9zGPSFPTEmI048UwPTZA==} + engines: {node: '>=20.18.1'} hasBin: true sharp@0.34.5: @@ -9323,11 +9298,6 @@ packages: '@types/react': optional: true - use-stick-to-bottom@1.1.6: - resolution: {integrity: sha512-z3Up8jYQGTkUCsGBnwg6/wj70KgXoW5Kz1AAc1j8MtQuYMBo6ZsdhrIXoegxa7gaMMilgQYyTohTrt3p94jHog==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - use-sync-external-store@1.6.0: resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} peerDependencies: @@ -9486,10 +9456,6 @@ packages: web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} - web-streams-polyfill@3.3.3: - resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} - engines: {node: '>= 8'} - webidl-conversions@8.0.1: resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} engines: {node: '>=20'} @@ -9684,13 +9650,6 @@ snapshots: package-manager-detector: 1.7.0 tinyexec: 1.2.4 - '@antfu/ni@25.0.0': - dependencies: - ansis: 4.3.1 - fzf: 0.5.2 - package-manager-detector: 1.7.0 - tinyexec: 1.2.4 - '@asamuzakjp/css-color@5.1.11': dependencies: '@asamuzakjp/generational-cache': 1.0.1 @@ -10776,7 +10735,8 @@ snapshots: '@img/sharp-win32-x64@0.34.5': optional: true - '@inquirer/ansi@2.0.7': {} + '@inquirer/ansi@2.0.7': + optional: true '@inquirer/confirm@6.1.1(@types/node@24.13.3)': dependencies: @@ -10784,6 +10744,7 @@ snapshots: '@inquirer/type': 4.0.7(@types/node@24.13.3) optionalDependencies: '@types/node': 24.13.3 + optional: true '@inquirer/core@11.2.1(@types/node@24.13.3)': dependencies: @@ -10796,6 +10757,7 @@ snapshots: signal-exit: 4.1.0 optionalDependencies: '@types/node': 24.13.3 + optional: true '@inquirer/external-editor@1.0.3(@types/node@24.13.3)': dependencies: @@ -10804,11 +10766,13 @@ snapshots: optionalDependencies: '@types/node': 24.13.3 - '@inquirer/figures@2.0.7': {} + '@inquirer/figures@2.0.7': + optional: true '@inquirer/type@4.0.7(@types/node@24.13.3)': optionalDependencies: '@types/node': 24.13.3 + optional: true '@isaacs/cliui@8.0.2': dependencies: @@ -11219,6 +11183,7 @@ snapshots: is-node-process: 1.2.0 outvariant: 1.4.3 strict-event-emitter: 0.5.1 + optional: true '@napi-rs/wasm-runtime@1.0.7': dependencies: @@ -11292,16 +11257,20 @@ snapshots: '@nolyfill/is-core-module@1.0.39': {} - '@open-draft/deferred-promise@2.2.0': {} + '@open-draft/deferred-promise@2.2.0': + optional: true - '@open-draft/deferred-promise@3.0.0': {} + '@open-draft/deferred-promise@3.0.0': + optional: true '@open-draft/logger@0.3.0': dependencies: is-node-process: 1.2.0 outvariant: 1.4.3 + optional: true - '@open-draft/until@2.1.0': {} + '@open-draft/until@2.1.0': + optional: true '@opentelemetry/api@1.9.0': {} @@ -12663,6 +12632,11 @@ snapshots: '@sec-ant/readable-stream@0.4.1': {} + '@shadcn/react@0.2.1(@types/react@19.2.17)(react@19.2.7)': + optionalDependencies: + '@types/react': 19.2.17 + react: 19.2.7 + '@sinclair/typebox@0.34.50': {} '@sindresorhus/merge-streams@4.0.0': {} @@ -13557,10 +13531,12 @@ snapshots: '@types/set-cookie-parser@2.4.10': dependencies: '@types/node': 24.13.3 + optional: true '@types/stack-utils@2.0.3': {} - '@types/statuses@2.0.6': {} + '@types/statuses@2.0.6': + optional: true '@types/tapable@1.0.6': {} @@ -14184,8 +14160,6 @@ snapshots: acorn@8.17.0: {} - agent-base@7.1.4: {} - ai@5.0.210(zod@4.4.3): dependencies: '@ai-sdk/gateway': 2.0.109(zod@4.4.3) @@ -14269,8 +14243,6 @@ snapshots: ansi-styles@6.2.3: {} - ansis@4.3.1: {} - any-promise@1.3.0: {} anymatch@3.1.3: @@ -14671,7 +14643,8 @@ snapshots: cli-spinners@2.9.2: {} - cli-width@4.1.0: {} + cli-width@4.1.0: + optional: true client-only@0.0.1: {} @@ -14754,7 +14727,8 @@ snapshots: cookie@0.7.2: {} - cookie@1.1.1: {} + cookie@1.1.1: + optional: true cors@2.8.6: dependencies: @@ -15023,8 +14997,6 @@ snapshots: dashify@2.0.0: {} - data-uri-to-buffer@4.0.1: {} - data-urls@7.0.0: dependencies: whatwg-mimetype: 5.0.0 @@ -16064,17 +16036,20 @@ snapshots: fast-levenshtein@2.0.6: {} - fast-string-truncated-width@3.0.3: {} + fast-string-truncated-width@3.0.3: + optional: true fast-string-width@3.0.2: dependencies: fast-string-truncated-width: 3.0.3 + optional: true fast-uri@3.1.3: {} fast-wrap-ansi@0.2.2: dependencies: fast-string-width: 3.0.2 + optional: true fastparse@1.1.2: {} @@ -16090,11 +16065,6 @@ snapshots: optionalDependencies: picomatch: 4.0.5 - fetch-blob@3.2.0: - dependencies: - node-domexception: 1.0.0 - web-streams-polyfill: 3.3.3 - figures@6.1.0: dependencies: is-unicode-supported: 2.1.0 @@ -16154,10 +16124,6 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - formdata-polyfill@4.0.10: - dependencies: - fetch-blob: 3.2.0 - forwarded@0.2.0: {} fraction.js@5.3.4: {} @@ -16214,8 +16180,6 @@ snapshots: fuzzysort@3.1.0: {} - fzf@0.5.2: {} - generator-function@2.0.1: {} generic-names@4.0.0: @@ -16337,7 +16301,8 @@ snapshots: graceful-fs@4.2.11: {} - graphql@16.14.2: {} + graphql@16.14.2: + optional: true gzip-size@6.0.0: dependencies: @@ -16457,6 +16422,7 @@ snapshots: dependencies: '@types/set-cookie-parser': 2.4.10 set-cookie-parser: 3.1.2 + optional: true hermes-estree@0.25.1: {} @@ -16486,13 +16452,6 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 - https-proxy-agent@7.0.6(supports-color@8.1.1): - dependencies: - agent-base: 7.1.4 - debug: 4.4.3(supports-color@8.1.1) - transitivePeerDependencies: - - supports-color - human-id@4.2.0: {} human-signals@2.1.0: {} @@ -16669,7 +16628,8 @@ snapshots: is-negative-zero@2.0.3: {} - is-node-process@1.2.0: {} + is-node-process@1.2.0: + optional: true is-number-object@1.1.1: dependencies: @@ -17948,6 +17908,7 @@ snapshots: typescript: 5.8.2 transitivePeerDependencies: - '@types/node' + optional: true msw@2.15.0(@types/node@24.13.3)(typescript@5.9.3): dependencies: @@ -17975,7 +17936,8 @@ snapshots: - '@types/node' optional: true - mute-stream@3.0.0: {} + mute-stream@3.0.0: + optional: true mz@2.7.0: dependencies: @@ -18037,8 +17999,6 @@ snapshots: node-addon-api@7.1.1: optional: true - node-domexception@1.0.0: {} - node-exports-info@1.6.2: dependencies: array.prototype.flatmap: 1.3.3 @@ -18046,12 +18006,6 @@ snapshots: object.entries: 1.1.9 semver: 6.3.1 - node-fetch@3.3.2: - dependencies: - data-uri-to-buffer: 4.0.1 - fetch-blob: 3.2.0 - formdata-polyfill: 4.0.10 - node-int64@0.4.0: {} node-releases@2.0.51: {} @@ -18196,7 +18150,8 @@ snapshots: outdent@0.5.0: {} - outvariant@1.4.3: {} + outvariant@1.4.3: + optional: true own-keys@1.0.1: dependencies: @@ -18369,7 +18324,8 @@ snapshots: lru-cache: 11.5.2 minipass: 7.1.3 - path-to-regexp@6.3.0: {} + path-to-regexp@6.3.0: + optional: true path-to-regexp@8.4.2: {} @@ -18876,7 +18832,8 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 - rettime@0.11.11: {} + rettime@0.11.11: + optional: true reusify@1.1.0: {} @@ -19061,7 +19018,8 @@ snapshots: transitivePeerDependencies: - supports-color - set-cookie-parser@3.1.2: {} + set-cookie-parser@3.1.2: + optional: true set-function-length@1.2.2: dependencies: @@ -19087,9 +19045,8 @@ snapshots: setprototypeof@1.2.0: {} - shadcn@3.8.5(@types/node@24.13.3)(supports-color@8.1.1)(typescript@5.8.2): + shadcn@4.13.0(supports-color@8.1.1)(typescript@5.8.2): dependencies: - '@antfu/ni': 25.0.0 '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/parser': 7.29.7 '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) @@ -19107,10 +19064,7 @@ snapshots: fast-glob: 3.3.3 fs-extra: 11.3.6 fuzzysort: 3.1.0 - https-proxy-agent: 7.0.6(supports-color@8.1.1) kleur: 4.1.5 - msw: 2.15.0(@types/node@24.13.3)(typescript@5.8.2) - node-fetch: 3.3.2 open: 11.0.0 ora: 8.2.0 postcss: 8.5.16 @@ -19121,12 +19075,12 @@ snapshots: tailwind-merge: 3.6.0 ts-morph: 26.0.0 tsconfig-paths: 4.2.0 + undici: 7.28.0 validate-npm-package-name: 7.0.2 zod: 3.25.76 zod-to-json-schema: 3.25.2(zod@3.25.76) transitivePeerDependencies: - '@cfworker/json-schema' - - '@types/node' - babel-plugin-macros - supports-color - typescript @@ -19333,7 +19287,8 @@ snapshots: transitivePeerDependencies: - supports-color - strict-event-emitter@0.5.1: {} + strict-event-emitter@0.5.1: + optional: true string-argv@0.3.2: {} @@ -19509,7 +19464,8 @@ snapshots: systeminformation@5.31.15: {} - tagged-tag@1.0.0: {} + tagged-tag@1.0.0: + optional: true tailwind-merge@3.6.0: {} @@ -19680,6 +19636,7 @@ snapshots: type-fest@5.8.0: dependencies: tagged-tag: 1.0.0 + optional: true type-is@2.1.0: dependencies: @@ -19894,7 +19851,8 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.12.2 '@unrs/resolver-binding-win32-x64-msvc': 1.12.2 - until-async@3.0.2: {} + until-async@3.0.2: + optional: true update-browserslist-db@1.2.3(browserslist@4.28.5): dependencies: @@ -19929,10 +19887,6 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 - use-stick-to-bottom@1.1.6(react@19.2.7): - dependencies: - react: 19.2.7 - use-sync-external-store@1.6.0(react@19.2.7): dependencies: react: 19.2.7 @@ -20149,8 +20103,6 @@ snapshots: web-namespaces@2.0.1: {} - web-streams-polyfill@3.3.3: {} - webidl-conversions@8.0.1: {} webpack-sources@3.5.1: {} From 666478de84f4c987f791a7b4fec3fa7975e27b45 Mon Sep 17 00:00:00 2001 From: Thomas Yuill Date: Fri, 10 Jul 2026 14:21:07 -0400 Subject: [PATCH 2/5] feat(shadcn): add conversational AgentSessionView-01 story, fix transcript hit-testing Adds a WithConversation story that populates the transcript with a scripted conversation by emitting fake chat events onto the session's Room, plus a play function to open the panel. Extends the sample conversation to 20 messages so the transcript overflows and scrolls. While verifying it, found that TileLayout's absolutely positioned wrapper (z-50, rendered after the transcript in DOM order) intercepts all pointer/wheel events over the transcript whenever the chat panel is open, even with no messages -- confirmed via AgentSessionView_01's Default story. Fixes it with pointer-events-none, since nothing in that layer is interactive today. --- .../lk-decorators/MockConversation.tsx | 56 +++++++++++++++++++ docs/storybook/package.json | 1 + .../agents-ui/AgentSessionView-01.stories.tsx | 37 +++++++++++- .../components/tile-view.tsx | 2 +- pnpm-lock.yaml | 3 + 5 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 docs/storybook/.storybook/lk-decorators/MockConversation.tsx diff --git a/docs/storybook/.storybook/lk-decorators/MockConversation.tsx b/docs/storybook/.storybook/lk-decorators/MockConversation.tsx new file mode 100644 index 000000000..63a66ac72 --- /dev/null +++ b/docs/storybook/.storybook/lk-decorators/MockConversation.tsx @@ -0,0 +1,56 @@ +import * as React from 'react'; +import { Decorator } from '@storybook/react-vite'; +import { useSessionContext } from '@livekit/components-react'; +import { DataPacket_Kind, Participant, type RemoteParticipant, RoomEvent } from 'livekit-client'; + +/** + * Mirrors `LegacyDataTopic.CHAT` from `@livekit/components-core` (not re-exported from + * `@livekit/components-react`, so it's inlined here rather than adding a new dependency). + */ +const LEGACY_CHAT_TOPIC = 'lk-chat-topic'; + +const MOCK_AGENT_PARTICIPANT = new Participant('mock-agent-sid', 'agent', 'Agent'); + +export type MockConversationMessage = { + id: string; + from: 'user' | 'agent'; + message: string; +}; + +/** + * Populates the current session's transcript with a scripted conversation, without a real + * connection or backend agent. Works by emitting fake `RoomEvent.DataReceived` events on the + * legacy chat topic -- the same public SDK event the chat pipeline listens on -- so any + * component reading messages via `useSessionMessages()` renders them as if they were received + * for real. + * + * Must be nested inside a decorator that provides `SessionContext` (e.g. `AgentSessionProvider`). + */ +export function withMockConversation(messages: MockConversationMessage[]): Decorator { + return (Story) => { + const { room } = useSessionContext(); + + React.useEffect(() => { + messages.forEach(({ id, from, message }) => { + const payload = new TextEncoder().encode( + JSON.stringify({ id, timestamp: Date.now(), message }), + ) as Uint8Array; + // `RoomEvent.DataReceived` is typed as remote-only, but `setupChat` (the only consumer of + // this event) reads `from` as a plain `Participant`, so this cast is safe -- the real + // local participant is a `LocalParticipant`, not a `RemoteParticipant` either. + const participant = (from === 'user' + ? room.localParticipant + : MOCK_AGENT_PARTICIPANT) as unknown as RemoteParticipant; + room.emit( + RoomEvent.DataReceived, + payload, + participant, + DataPacket_Kind.RELIABLE, + LEGACY_CHAT_TOPIC, + ); + }); + }, [room]); + + return <>{Story()}; + }; +} diff --git a/docs/storybook/package.json b/docs/storybook/package.json index a7fe2727d..2fa3fe0c8 100644 --- a/docs/storybook/package.json +++ b/docs/storybook/package.json @@ -38,6 +38,7 @@ "shadcn": "^4.13.0", "storybook": "^10.1.4", "tailwindcss": "^4.3.2", + "tslib": "^2.6.2", "tsx": "^4.0.0", "typescript": "5.8.2", "vite": "^8.0.0", diff --git a/docs/storybook/stories/agents-ui/AgentSessionView-01.stories.tsx b/docs/storybook/stories/agents-ui/AgentSessionView-01.stories.tsx index 8411868fd..fad05eb92 100644 --- a/docs/storybook/stories/agents-ui/AgentSessionView-01.stories.tsx +++ b/docs/storybook/stories/agents-ui/AgentSessionView-01.stories.tsx @@ -1,17 +1,38 @@ import React from 'react'; import { StoryObj } from '@storybook/react-vite'; +import { within, userEvent } from 'storybook/test'; import { useTheme } from 'next-themes'; import { AgentSessionProvider } from '../../.storybook/lk-decorators/AgentSessionProvider'; +import { + MockConversationMessage, + withMockConversation, +} from '../../.storybook/lk-decorators/MockConversation'; import { AgentSessionView_01, AgentSessionView_01Props } from '@livekit/agents-ui'; +const SAMPLE_CONVERSATION: MockConversationMessage[] = [ + { id: '1', from: 'agent', message: 'Hi, how can I help you today?' }, + { id: '2', from: 'user', message: 'Hi, how are you?' }, + { id: '3', from: 'agent', message: "I'm good, thank you!" }, + { id: '4', from: 'user', message: 'This is a longer message that should wrap to the next line.' }, + { + id: '5', + from: 'agent', + message: "Great, I'm responding with an even longer message to see how it wraps.", + }, + ...Array.from({ length: 15 }, (_, index) => ({ + id: `${6 + index}`, + from: (index % 2 === 0 ? 'user' : 'agent') as MockConversationMessage['from'], + message: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', + })), +]; + export default { component: AgentSessionView_01, decorators: [AgentSessionProvider], render: (args: AgentSessionView_01Props) => { const { resolvedTheme = 'dark' } = useTheme(); - return ( - - ); + return ; }, args: { className: 'h-screen w-screen', @@ -57,3 +78,13 @@ export default { export const Default: StoryObj = { args: {}, }; + +export const WithConversation: StoryObj = { + decorators: [withMockConversation(SAMPLE_CONVERSATION)], + args: {}, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const transcriptToggle = await canvas.findByRole('button', { name: 'Toggle transcript' }); + await userEvent.click(transcriptToggle); + }, +}; diff --git a/packages/shadcn/components/agents-ui/blocks/agent-session-view-01/components/tile-view.tsx b/packages/shadcn/components/agents-ui/blocks/agent-session-view-01/components/tile-view.tsx index 49f5434a4..f6d23d04d 100644 --- a/packages/shadcn/components/agents-ui/blocks/agent-session-view-01/components/tile-view.tsx +++ b/packages/shadcn/components/agents-ui/blocks/agent-session-view-01/components/tile-view.tsx @@ -109,7 +109,7 @@ export function TileLayout({ const videoHeight = agentVideoTrack?.publication.dimensions?.height ?? 0; return ( -
+
{/* Agent */} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 667a51f79..b9385c383 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -149,6 +149,9 @@ importers: tailwindcss: specifier: ^4.3.2 version: 4.3.2 + tslib: + specifier: ^2.6.2 + version: 2.8.1 tsx: specifier: ^4.0.0 version: 4.23.0 From 81cc99624ec0b69cb633d54f7cd02db93ae9495e Mon Sep 17 00:00:00 2001 From: Thomas Yuill Date: Fri, 10 Jul 2026 14:38:28 -0400 Subject: [PATCH 3/5] fixing chat transcript in seesion block --- .../agent-session-view-01/components/agent-session-block.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/shadcn/components/agents-ui/blocks/agent-session-view-01/components/agent-session-block.tsx b/packages/shadcn/components/agents-ui/blocks/agent-session-view-01/components/agent-session-block.tsx index 1d23250d0..64ec68be9 100644 --- a/packages/shadcn/components/agents-ui/blocks/agent-session-view-01/components/agent-session-block.tsx +++ b/packages/shadcn/components/agents-ui/blocks/agent-session-view-01/components/agent-session-block.tsx @@ -215,12 +215,12 @@ export function AgentSessionView_01({ {isChatOpen && ( )} @@ -261,7 +261,6 @@ export function AgentSessionView_01({ )}
- Date: Mon, 20 Jul 2026 09:48:14 -0400 Subject: [PATCH 4/5] fix(shadcn): drop w-screen from AgentSessionView-01 story to prevent horizontal scroll w-screen (100vw) includes the scrollbar gutter in its width calculation, so it can render wider than the visible viewport and force an unwanted horizontal scrollbar. The component already defaults to w-full; the story's arg was overriding it unnecessarily. --- .../storybook/stories/agents-ui/AgentSessionView-01.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/storybook/stories/agents-ui/AgentSessionView-01.stories.tsx b/docs/storybook/stories/agents-ui/AgentSessionView-01.stories.tsx index fad05eb92..a936a82f5 100644 --- a/docs/storybook/stories/agents-ui/AgentSessionView-01.stories.tsx +++ b/docs/storybook/stories/agents-ui/AgentSessionView-01.stories.tsx @@ -35,7 +35,7 @@ export default { return ; }, args: { - className: 'h-screen w-screen', + className: 'h-screen w-full', supportsChatInput: true, supportsVideoInput: true, supportsScreenShare: true, From 2baa88a80917ea123300ef36d7ba8eb7f731d473 Mon Sep 17 00:00:00 2001 From: Thomas Yuill Date: Mon, 20 Jul 2026 10:44:32 -0400 Subject: [PATCH 5/5] fix(shadcn): switch AgentSessionView-01 story to fullscreen layout The centered layout parameter wraps stories in a flex container sized to its content (no explicit width), so the component's own w-full created a circular sizing dependency and collapsed to width 0. Switching to fullscreen (matching the sibling AgentChatTranscript story) removes that wrapper so width resolves normally. Height still needs h-screen since fullscreen doesn't give an ancestor a real height for h-full to resolve against, but vh doesn't have the scrollbar-gutter overflow issue vw does. --- .../stories/agents-ui/AgentSessionView-01.stories.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/storybook/stories/agents-ui/AgentSessionView-01.stories.tsx b/docs/storybook/stories/agents-ui/AgentSessionView-01.stories.tsx index a936a82f5..e5a61a454 100644 --- a/docs/storybook/stories/agents-ui/AgentSessionView-01.stories.tsx +++ b/docs/storybook/stories/agents-ui/AgentSessionView-01.stories.tsx @@ -35,7 +35,7 @@ export default { return ; }, args: { - className: 'h-screen w-full', + className: 'h-screen', supportsChatInput: true, supportsVideoInput: true, supportsScreenShare: true, @@ -70,7 +70,7 @@ export default { audioVisualizerWaveLineWidth: { control: { type: 'range', min: 1, max: 10, step: 0.1 } }, }, parameters: { - layout: 'centered', + layout: 'fullscreen', actions: { handles: [] }, }, };