"use client" import * as React from "react" import { useChat } from "@ai-sdk/react" import type { UIMessage } from "ai" import { createChat, getMessageText } from "@/lib/ai" import { PartText } from "@/registry/bases/radix/blocks/preview-03/components/part-text" import { PromptInput } from "@/registry/bases/radix/blocks/preview-03/components/prompt-input" import { Example } from "@/registry/bases/radix/components/example" import { Button } from "@/registry/bases/radix/ui/button" import { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "@/registry/bases/radix/ui/card" import { Marker, MarkerContent, MarkerIcon, } from "@/registry/bases/radix/ui/marker" import { Message, MessageContent, MessageHeader, } from "@/registry/bases/radix/ui/message" import { MessageScroller, MessageScrollerButton, MessageScrollerContent, MessageScrollerItem, MessageScrollerProvider, MessageScrollerViewport, } from "@/registry/bases/radix/ui/message-scroller" import { Spinner } from "@/registry/bases/radix/ui/spinner" import { IconPlaceholder } from "@/app/(app)/create/components/icon-placeholder" type Metadata = { name?: string } type ChatEventUserLeft = { type: "user-left" name: string } type ChatEventUserJoined = { type: "user-joined" name: string } type ChatEventUserTyping = { type: "user-typing" name: string } type ChatEvent = ChatEventUserLeft | ChatEventUserJoined | ChatEventUserTyping type ScriptedChatEvent = ChatEvent & { order: number delayMs: number } type ChatMessage = UIMessage const scriptedEvents = [ { type: "user-joined", name: "Stella", order: 0.5, delayMs: 2000 }, { type: "user-typing", name: "Stella", order: 0.5, delayMs: 4000 }, { type: "user-joined", name: "Marcus", order: 1.5, delayMs: 2000 }, { type: "user-typing", name: "Marcus", order: 1.5, delayMs: 4000 }, { type: "user-joined", name: "Aleena", order: 2.5, delayMs: 0 }, { type: "user-typing", name: "Aleena", order: 3.5, delayMs: 0 }, { type: "user-left", name: "Aleena", order: 5.5, delayMs: 2000 }, ] satisfies ScriptedChatEvent[] const chat = createChat() .user( "Hey everyone — dinner this Saturday? I was thinking something casual at my place." ) .user( "I'm in! Should we do potluck or does someone want to cook the whole thing?", { metadata: { name: "Stella", }, } ) .user("Potluck sounds good. I can handle appetizers and drinks.", { metadata: { name: "Marcus", }, }) .user("Alright! Aleena?") .user("Can't make it. I'm out of town. 😭", { metadata: { name: "Aleena", }, }) .user("Sorry guys, let's catch up another time. Bye.", { metadata: { name: "Aleena", }, }) .user( "@chat, can you suggest a menu and split up what each of us should bring?" ) .sleep(1000) .assistant( `Here's a simple **Saturday potluck** for the three of you: **Who brings what** - **You:** main — salmon or mushroom risotto - **Stella:** salad + bread - **Marcus:** appetizers + drinks - **Chat:** dessert ideas below **Menu** - **Starter:** caprese skewers or whipped ricotta crostini - **Main:** lemon-herb salmon with roasted asparagus *(or risotto for vegetarians)* - **Side:** arugula salad with shaved parmesan - **Dessert:** berry crisp or pistachio gelato **Timing:** aim for **6:30 PM** so Marcus can set up apps while you finish the main. Want me to scale this for **6 guests** or swap the main for something vegetarian-only?` ) const INITIAL_MESSAGE_COUNT = 0 const initialMessages = chat.get(INITIAL_MESSAGE_COUNT) const transport = chat.transport() export function GroupChat() { const eventRunRef = React.useRef(0) const [events, setEvents] = React.useState(() => scriptedEvents.filter((event) => event.order < INITIAL_MESSAGE_COUNT) ) const { status, messages, sendMessage, setMessages } = useChat({ messages: initialMessages, transport, }) const nextMessage = chat.next(messages) const isBusy = status === "submitted" || status === "streaming" const senderName = nextMessage?.metadata?.name ?? "You" const onSubmit = (input: string) => { if (!input.trim()) { return } if (nextMessage && input === getMessageText(nextMessage)) { const message = nextMessage const messageIndex = messages.length const eventRun = eventRunRef.current + 1 const nextEvents = scriptedEvents .filter((event) => event.order > messageIndex) .filter((event) => event.order < messageIndex + 1) .filter( (event) => !( event.type === "user-typing" && event.name === message.metadata?.name ) ) eventRunRef.current = eventRun setEvents((events) => events.filter( (event) => !( event.type === "user-typing" && event.name === message.metadata?.name ) ) ) if (getMessageText(message).trim().toLowerCase().startsWith("@chat,")) { void sendMessage(nextMessage) return } setMessages((messages) => [...messages, message]) void (async () => { let previousDelayMs = 0 for (const event of nextEvents) { const delayMs = Math.max(0, event.delayMs - previousDelayMs) previousDelayMs = event.delayMs if (delayMs > 0) { await new Promise((resolve) => setTimeout(resolve, delayMs)) } if (eventRunRef.current !== eventRun) { return } setEvents((events) => [ ...events.filter( (currentEvent) => !( currentEvent.type === event.type && currentEvent.name === event.name ) ), event, ].sort((a, b) => a.order - b.order) ) } })() return } void sendMessage({ text: input, }) } return ( Saturday dinner plans A group chat with multiple users. Includes tinted bubbles for user messages. {messages.map((message, index) => { const isOwnMessage = message.role === "user" && message.metadata?.name === undefined const startsModelTurn = getMessageText(message) .trim() .toLowerCase() .startsWith("@chat,") return ( {!isOwnMessage && ( {message.role === "assistant" ? "Chat" : message.metadata?.name || "Unknown"} )} {message.parts.map((part, index) => ( {part.type === "text" ? ( ) : null} ))} {events .filter((event) => event.order > index) .filter((event) => event.order < index + 1) .map((event) => ( {event.type === "user-joined" ? `${event.name} joined the chat` : event.type === "user-left" ? `${event.name} left the chat` : `${event.name} is typing...`} ))} ) })} {status === "submitted" ? ( Thinking... ) : null}

Posting as {senderName}.

) }