"use client" import * as React from "react" import { useChat } from "@ai-sdk/react" import { createChat, getMessageText } from "@/lib/ai" import { Example, ExampleWrapper, } from "@/registry/bases/base/components/example" import { Bubble, BubbleContent } from "@/registry/bases/base/ui/bubble" import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "@/registry/bases/base/ui/card" import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupTextarea, } from "@/registry/bases/base/ui/input-group" import { Marker, MarkerContent, MarkerIcon, } from "@/registry/bases/base/ui/marker" import { Message, MessageContent } from "@/registry/bases/base/ui/message" import { MessageScroller, MessageScrollerButton, MessageScrollerContent, MessageScrollerItem, MessageScrollerProvider, MessageScrollerViewport, } from "@/registry/bases/base/ui/message-scroller" import { Spinner } from "@/registry/bases/base/ui/spinner" import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder" export default function MessageScrollerExample() { return ( ) } const chat = createChat() .user("Hello there!") .sleep(2000) .assistant("Hey, how's it going?") .user( `I'm prototyping an AI chat surface for our product docs. Can you sketch a sensible component breakdown and call out anything I'd regret baking into v1?` ) .sleep(2000) .assistant( `## Recommended layout Treat the chat as three layers so scrolling and the composer never fight each other: | Layer | Responsibility | | --- | --- | | **Shell** | Card or page frame, title, status indicator | | **Transcript** | \`overflow-y-auto\` message list | | **Composer** | Input group, send, stop | ### Message rendering - **User messages**: right-aligned, muted background, \`max-w-[80%]\`. - **Assistant messages**: full width within the column; use \`whitespace-pre-wrap\` for text. ### Suggested component split \`\`\`tsx \`\`\` ### v1 pitfalls to avoid 1. **Inlining transport logic in the UI** — keep scripted/demo transport beside your example, not inside shared components. 2. **Fixed transcript height without \`overflow-hidden\`** — parent must clip, child must scroll, or the composer jumps. 3. **Not giving the scroller a ref or auto-scroll flag** — long replies should stay pinned to the bottom unless the user scrolls up. ### Practical next step Start with plain \`whitespace-pre-wrap\` text to validate spacing and scroll behavior. Once the transcript feels right, you can swap assistant text rendering to a richer renderer if you need it. Want me to walk through auto-scroll vs. manual scroll-to-bottom next?` ) .user( "What about message spacing when one reply is short and the next is really long?" ) .sleep(2000) .assistant( `Good question. The scroll container should own vertical rhythm, not individual bubbles. Use a consistent \`gap\` on the message list (something like \`gap-4\` or \`gap-6\`) so short and long messages sit on the same grid. For assistant replies that span many paragraphs, keep everything in one bubble rather than splitting on every line break — otherwise the transcript feels choppy when you're skimming. When a long reply streams in, pin the viewport to the bottom with \`MessageScroller autoScroll\` until the user scrolls up. If they've scrolled away, show a scroll-to-bottom affordance instead of yanking them back mid-read. That's usually enough for v1. Fancy diff animations or per-paragraph fade-ins can wait until you've validated that the base scroll + composer layout holds up on real devices.` ) .user("Thanks, that helps.") .sleep(1000) .assistant( "Happy to help — send another message when you're ready to keep stepping through the demo." ) const initialMessages = chat.get(0) const transport = chat.transport({ delayMs: 10, }) function MessageScrollerDemo() { const { messages, sendMessage, status, stop } = useChat({ messages: initialMessages, transport, }) const nextMessage = chat.next(messages) const isBusy = status === "submitted" || status === "streaming" function handleFormSubmit(e: React.FormEvent) { e.preventDefault() if (!nextMessage || isBusy) { return } void sendMessage(nextMessage) } return ( How can I help you today? Status: {status} {messages.map((message) => ( {message.parts.map((part, index) => { if (part.type !== "text") { return null } return ( {part.text} ) })} ))} {status === "submitted" ? ( Thinking... ) : null}
Send stop()} > Stop
) }