snapshot of https://github.com/shadcn-ui/ui.git (main, 2026-09-15; blobs > 131072B omitted)
All checks were successful
Release / Create Version PR or Publish Stable Release (push) Has been skipped
Release / Publish Prerelease to NPM (push) Has been skipped
Monitor Registries / Monitor registry health (push) Has been skipped

This commit is contained in:
upstream-mirror
2026-09-15 22:44:43 +05:00
commit 1416db152c
5727 changed files with 630238 additions and 0 deletions

View File

@@ -0,0 +1,194 @@
"use client"
import * as React from "react"
import { useChat } from "@ai-sdk/react"
import { createChat, getMessageText } from "@/lib/ai"
import { PartFile } from "@/registry/bases/radix/blocks/preview-03/components/part-file"
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 } 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"
const chat = createChat()
.user("Review these files and tell me what stands out.", {
files: [
{
filename: "workspace.png",
mediaType: "image/png",
url: "https://images.unsplash.com/photo-1497366754035-f200968a6e72?w=900&auto=format&fit=crop&q=80",
},
{
filename: "research-brief.pdf",
mediaType: "application/pdf",
url: "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf",
},
],
})
.sleep(1000)
.assistant(({ writer }) => {
writer.text(
"The image reads like a focused workspace, while the document should be treated as the source of record. I would summarize the PDF first, then use the image only as supporting context."
)
writer.file({
filename: "annotated-workspace.png",
mediaType: "image/png",
url: "https://images.unsplash.com/photo-1497366811353-6870744d04b2?w=900&auto=format&fit=crop&q=80",
})
writer.file({
filename: "file-review-notes.pdf",
mediaType: "application/pdf",
url: "https://example.com/file-review-notes.pdf",
})
})
const initialMessages = chat.get(0)
const transport = chat.transport()
export function FilesChat() {
const { status, messages, sendMessage, setMessages } = useChat({
messages: initialMessages,
transport,
})
const nextMessage = chat.next(messages)
const isBusy = status === "submitted" || status === "streaming"
const onSubmit = (input: string) => {
if (!input.trim()) {
return
}
if (nextMessage && input === getMessageText(nextMessage)) {
void sendMessage(nextMessage)
return
}
void sendMessage({ text: input })
}
return (
<Example
title="Files"
className="bg-transparent p-0"
containerClassName="flex-1"
>
<Card className="max-h-140 min-h-0 flex-1">
<CardHeader>
<CardTitle>File review</CardTitle>
<CardDescription>Image and document file parts.</CardDescription>
<CardAction>
<Button
variant="outline"
size="icon"
aria-label="Start over"
onClick={() => setMessages(initialMessages)}
>
<IconPlaceholder
lucide="PlusIcon"
tabler="IconPlus"
hugeicons="PlusSignIcon"
phosphor="PlusIcon"
remixicon="RiAddLine"
/>
</Button>
</CardAction>
</CardHeader>
<CardContent className="flex-1 overflow-hidden p-0">
<MessageScrollerProvider>
<MessageScroller>
<MessageScrollerViewport>
<MessageScrollerContent className="p-(--card-spacing)">
{messages.map((message) => (
<MessageScrollerItem
key={message.id}
messageId={message.id}
scrollAnchor={message.role === "user"}
>
<Message
align={message.role === "user" ? "end" : "start"}
data-role={message.role}
>
<MessageContent
className={
message.role === "user"
? "flex-col items-end gap-2"
: "flex-col items-start gap-2"
}
>
{message.parts.map((part, index) =>
part.type === "file" ? (
<PartFile
key={`${message.id}-${index}`}
part={part}
/>
) : null
)}
{message.parts.map((part, index) =>
part.type === "text" ? (
<PartText
key={`${message.id}-${index}`}
part={part}
variant={
message.role === "user" ? "bubble" : "default"
}
/>
) : null
)}
</MessageContent>
</Message>
</MessageScrollerItem>
))}
{status === "submitted" ? (
<MessageScrollerItem scrollAnchor={false}>
<Marker role="status">
<MarkerIcon>
<Spinner />
</MarkerIcon>
<MarkerContent>Thinking...</MarkerContent>
</Marker>
</MessageScrollerItem>
) : null}
</MessageScrollerContent>
</MessageScrollerViewport>
<MessageScrollerButton />
</MessageScroller>
</MessageScrollerProvider>
</CardContent>
<CardFooter>
<PromptInput
onSubmit={onSubmit}
status={status}
defaultValue={
isBusy ? "" : nextMessage ? getMessageText(nextMessage) : ""
}
/>
</CardFooter>
</Card>
</Example>
)
}

View File

@@ -0,0 +1,377 @@
"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<Metadata>
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<ChatMessage>()
.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<ScriptedChatEvent[]>(() =>
scriptedEvents.filter((event) => event.order < INITIAL_MESSAGE_COUNT)
)
const { status, messages, sendMessage, setMessages } = useChat<ChatMessage>({
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 (
<Example
title="Group Chat"
className="bg-transparent p-0"
containerClassName="flex-1"
>
<Card className="max-h-140 min-h-0 flex-1">
<CardHeader>
<CardTitle>Saturday dinner plans</CardTitle>
<CardDescription className="leading-relaxed">
A group chat with multiple users. Includes tinted bubbles for user
messages.
</CardDescription>
<CardAction>
<Button
variant="outline"
size="icon"
aria-label="Start over"
onClick={() => {
eventRunRef.current += 1
setMessages(initialMessages)
setEvents(
scriptedEvents.filter(
(event) => event.order < INITIAL_MESSAGE_COUNT
)
)
}}
>
<IconPlaceholder
lucide="PlusIcon"
tabler="IconPlus"
hugeicons="PlusSignIcon"
phosphor="PlusIcon"
remixicon="RiAddLine"
/>
</Button>
</CardAction>
</CardHeader>
<CardContent className="flex-1 overflow-hidden p-0">
<MessageScrollerProvider>
<MessageScroller>
<MessageScrollerViewport>
<MessageScrollerContent className="p-(--card-spacing)">
{messages.map((message, index) => {
const isOwnMessage =
message.role === "user" &&
message.metadata?.name === undefined
const startsModelTurn = getMessageText(message)
.trim()
.toLowerCase()
.startsWith("@chat,")
return (
<React.Fragment key={message.id}>
<MessageScrollerItem
messageId={message.id}
scrollAnchor={startsModelTurn}
>
<Message
align={isOwnMessage ? "end" : "start"}
data-role={message.role}
>
{!isOwnMessage && (
<MessageHeader>
{message.role === "assistant"
? "Chat"
: message.metadata?.name || "Unknown"}
</MessageHeader>
)}
<MessageContent>
{message.parts.map((part, index) => (
<React.Fragment key={`${message.id}-${index}`}>
{part.type === "text" ? (
<PartText
part={part}
variant={
message.role === "user"
? "bubble"
: "default"
}
tinted={isOwnMessage}
/>
) : null}
</React.Fragment>
))}
</MessageContent>
</Message>
</MessageScrollerItem>
{events
.filter((event) => event.order > index)
.filter((event) => event.order < index + 1)
.map((event) => (
<MessageScrollerItem
key={`${event.type}-${event.name}-${event.order}`}
scrollAnchor={false}
>
<Marker
variant={
event.type === "user-typing"
? "default"
: "separator"
}
role={
event.type === "user-typing"
? "status"
: undefined
}
>
<MarkerContent>
{event.type === "user-joined"
? `${event.name} joined the chat`
: event.type === "user-left"
? `${event.name} left the chat`
: `${event.name} is typing...`}
</MarkerContent>
</Marker>
</MessageScrollerItem>
))}
</React.Fragment>
)
})}
{status === "submitted" ? (
<MessageScrollerItem scrollAnchor={false}>
<Marker role="status">
<MarkerIcon>
<Spinner />
</MarkerIcon>
<MarkerContent>Thinking...</MarkerContent>
</Marker>
</MessageScrollerItem>
) : null}
</MessageScrollerContent>
</MessageScrollerViewport>
<MessageScrollerButton />
</MessageScroller>
</MessageScrollerProvider>
</CardContent>
<CardFooter className="flex-col items-stretch gap-2">
<p className="px-1 text-xs text-muted-foreground">
Posting as {senderName}.
</p>
<PromptInput
onSubmit={onSubmit}
status={status}
defaultValue={
isBusy ? "" : nextMessage ? getMessageText(nextMessage) : ""
}
/>
</CardFooter>
</Card>
</Example>
)
}

View File

@@ -0,0 +1,240 @@
"use client"
import * as React from "react"
import { useChat } from "@ai-sdk/react"
import { createChat, getMessageText } from "@/lib/ai"
import {
getReasoningLevelLabel,
PartReasoning,
} from "@/registry/bases/radix/blocks/preview-03/components/part-reasoning"
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 } 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"
const chat = createChat()
.user("Explain why the dashboard feels slow even though the API is fast.")
.sleep(1000)
.assistant(({ writer }) => {
writer.reasoning(
`The user is separating perceived performance from backend latency. They likely measured API response times and found them acceptable, but users still complain about slowness.
I should avoid jumping straight to server-side optimizations and instead frame the answer around what people actually experience.`
)
writer.sleep(600)
writer.reasoning(
`Perceived slowness usually lives in the gap between user action and visible feedback:
- input latency before the request fires
- loading state placement and timing
- layout shifts when data arrives
- whether optimistic UI gives immediate acknowledgment
For a dashboard, I should also consider chart re-renders, independent card fetches, and skeleton dimensions that do not match the final layout.`
)
writer.sleep(600)
writer.reasoning(
`Ranking likely bottlenecks for this scenario:
1. No immediate feedback on filter or refresh actions
2. Skeleton cards that collapse or expand when data lands
3. Waterfall requests where each widget fetches on its own
4. Empty-state flashes because stale data is not shown while revalidating
The answer should stay practical and ordered by impact, not by how technical each fix sounds.`
)
writer.text(
`The API can be fast while the interface still **feels** slow.
Start by checking:
- whether the button gives immediate feedback
- whether skeletons match the final layout
- whether any reflow happens after data arrives
- whether the loading state appears near the content it replaces`
)
})
.user("What should I fix first?")
.sleep(1000)
.assistant(({ writer }) => {
writer.reasoning(
`The user wants prioritization, not another diagnostic list.
Given the dashboard context, immediate feedback and layout stability usually beat micro-optimizations. I should recommend one fix with a clear way to measure success.`
)
writer.text(
`Fix **immediate feedback** first.
When someone clicks "Refresh", the button should disable and show a spinner within one frame — before the network round trip. That single change often cuts perceived latency in half.
Measure it with a screen recording and count frames between click and visual response.`
)
})
const initialMessages = chat.get(0)
const transport = chat.transport()
export function ReasoningChat() {
const { status, messages, sendMessage, setMessages } = useChat({
messages: initialMessages,
transport,
})
const nextMessage = chat.next(messages)
const isBusy = status === "submitted" || status === "streaming"
const onSubmit = (input: string) => {
if (!input.trim()) {
return
}
if (nextMessage && input === getMessageText(nextMessage)) {
void sendMessage(nextMessage)
return
}
void sendMessage({ text: input })
}
return (
<Example
title="Reasoning"
className="bg-transparent p-0"
containerClassName="flex-1"
>
<Card className="max-h-140 min-h-0 flex-1">
<CardHeader>
<CardTitle>Reasoning trace</CardTitle>
<CardDescription>
Layered reasoning traces across multiple assistant turns.
</CardDescription>
<CardAction>
<Button
variant="outline"
size="icon"
aria-label="Start over"
onClick={() => setMessages(initialMessages)}
>
<IconPlaceholder
lucide="PlusIcon"
tabler="IconPlus"
hugeicons="PlusSignIcon"
phosphor="PlusIcon"
remixicon="RiAddLine"
/>
</Button>
</CardAction>
</CardHeader>
<CardContent className="flex-1 overflow-hidden p-0">
<MessageScrollerProvider>
<MessageScroller>
<MessageScrollerViewport>
<MessageScrollerContent className="p-(--card-spacing)">
{messages.map((message) => (
<MessageScrollerItem
key={message.id}
messageId={message.id}
scrollAnchor={message.role === "user"}
>
<Message
align={message.role === "user" ? "end" : "start"}
data-role={message.role}
>
<MessageContent
className={
message.role === "user"
? "flex-col items-end gap-2"
: "flex-col items-start gap-2"
}
>
{message.parts.map((part, index) => {
const reasoningIndex =
part.type === "reasoning"
? message.parts
.slice(0, index)
.filter(
(currentPart) =>
currentPart.type === "reasoning"
).length
: 0
return (
<React.Fragment key={`${message.id}-${index}`}>
{part.type === "text" ? (
<PartText
part={part}
variant={
message.role === "user"
? "bubble"
: "default"
}
/>
) : part.type === "reasoning" ? (
<PartReasoning
part={part}
label={getReasoningLevelLabel(
reasoningIndex
)}
/>
) : null}
</React.Fragment>
)
})}
</MessageContent>
</Message>
</MessageScrollerItem>
))}
{status === "submitted" ? (
<MessageScrollerItem scrollAnchor={false}>
<Marker role="status">
<MarkerIcon>
<Spinner />
</MarkerIcon>
<MarkerContent>Thinking...</MarkerContent>
</Marker>
</MessageScrollerItem>
) : null}
</MessageScrollerContent>
</MessageScrollerViewport>
<MessageScrollerButton />
</MessageScroller>
</MessageScrollerProvider>
</CardContent>
<CardFooter>
<PromptInput
onSubmit={onSubmit}
status={status}
defaultValue={
isBusy ? "" : nextMessage ? getMessageText(nextMessage) : ""
}
/>
</CardFooter>
</Card>
</Example>
)
}

View File

@@ -0,0 +1,166 @@
"use client"
import * as React from "react"
import { useChat } from "@ai-sdk/react"
import { createChat, getMessageText } from "@/lib/ai"
import { Example } from "@/registry/bases/base/components/example"
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 { 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 } 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"
const chat = createChat()
.user("Write me a haiku about centering a div and how it's done.")
.sleep(3000)
.assistant(
"One box in the void / Twenty wrappers, still offset / `margin: 0 auto`"
)
.user(
`Is this conversation actually happening? I've been sitting here wondering if you're **really** reading this.
Every reply feels a little too convenient — like someone already wrote \`chat.next(messages)\` and I'm just clicking send on my own lines.`
)
.sleep(3000)
.assistant(
"You're in a preview block. I'm four hardcoded replies and a transport. Real enough for the demo — philosophically, we're both props."
)
.user(
"Okay, how do I make my loading spinner feel faster? Tell me in detail about the technique."
)
.sleep(3000)
.assistant(
"Add text that says “Almost there…” and never update it. Users assume progress. Ethically dubious. Statistically effective."
)
.user("Can you at least pretend to be surprised?")
.sleep(3000)
.assistant("😮 WOW! I had no idea you would ask that!")
const initialMessages = chat.get(0)
const transport = chat.transport()
export function SimpleChat() {
const { status, messages, sendMessage, setMessages } = useChat({
messages: initialMessages,
transport,
})
const nextMessage = chat.next(messages)
const isBusy = status === "submitted" || status === "streaming"
const onSubmit = (input: string) => {
const message = input ? { text: input } : (nextMessage ?? undefined)
if (!message) {
return
}
void sendMessage(message)
}
return (
<Example
title="Simple Chat"
className="bg-transparent p-0"
containerClassName="flex-1"
>
<Card className="max-h-140 min-h-0 flex-1">
<CardHeader>
<CardTitle>What can I help you with today?</CardTitle>
<CardDescription>A simple chat example.</CardDescription>
<CardAction>
<Button
variant="outline"
size="icon"
onClick={() => setMessages([])}
>
<IconPlaceholder
lucide="PlusIcon"
tabler="IconPlus"
hugeicons="PlusSignIcon"
phosphor="PlusIcon"
remixicon="RiAddLine"
/>
</Button>
</CardAction>
</CardHeader>
<CardContent className="flex-1 overflow-hidden p-0">
<MessageScrollerProvider>
<MessageScroller>
<MessageScrollerViewport>
<MessageScrollerContent className="p-(--card-spacing)">
{messages.map((message) => {
return (
<MessageScrollerItem
key={message.id}
messageId={message.id}
scrollAnchor={message.role === "user"}
>
<Message
align={message.role === "user" ? "end" : "start"}
data-role={message.role}
>
<MessageContent>
{message.parts.map((part, index) => (
<React.Fragment key={`${message.id}-${index}`}>
{part.type === "text" ? (
<PartText part={part} role={message.role} />
) : null}
</React.Fragment>
))}
</MessageContent>
</Message>
</MessageScrollerItem>
)
})}
{status === "submitted" ? (
<MessageScrollerItem scrollAnchor={false}>
<Marker role="status">
<MarkerIcon>
<Spinner />
</MarkerIcon>
<MarkerContent>Thinking...</MarkerContent>
</Marker>
</MessageScrollerItem>
) : null}
</MessageScrollerContent>
</MessageScrollerViewport>
<MessageScrollerButton />
</MessageScroller>
</MessageScrollerProvider>
</CardContent>
<CardFooter>
<PromptInput
onSubmit={onSubmit}
status={status}
defaultValue={
isBusy ? "" : nextMessage ? getMessageText(nextMessage) : ""
}
/>
</CardFooter>
</Card>
</Example>
)
}

View File

@@ -0,0 +1,187 @@
"use client"
import * as React from "react"
import { useChat } from "@ai-sdk/react"
import { createChat, getMessageText } from "@/lib/ai"
import { PartSource } from "@/registry/bases/radix/blocks/preview-03/components/part-source"
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 } 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"
const chat = createChat()
.user("What should I read before migrating message rendering?")
.sleep(1000)
.assistant(({ writer }) => {
writer.sourceUrl({
sourceId: "source-ai-sdk-ui-message",
title: "AI SDK UIMessage",
url: "https://ai-sdk.dev/docs/reference/ai-sdk-core/ui-message",
})
writer.sourceUrl({
sourceId: "source-ai-sdk-transport",
title: "AI SDK transport",
url: "https://ai-sdk.dev/docs/ai-sdk-ui/transport",
})
writer.sourceDocument({
sourceId: "source-migration-notes",
title: "Internal migration notes",
filename: "message-rendering-plan.md",
mediaType: "text/markdown",
})
writer.text(
"Start with the **UIMessage** shape, then read the transport docs so your renderer understands streamed parts. The migration notes should stay close to the component work because they capture the local composition decisions."
)
})
const initialMessages = chat.get(0)
const transport = chat.transport()
export function SourcesChat() {
const { status, messages, sendMessage, setMessages } = useChat({
messages: initialMessages,
transport,
})
const nextMessage = chat.next(messages)
const isBusy = status === "submitted" || status === "streaming"
const onSubmit = (input: string) => {
if (!input.trim()) {
return
}
if (nextMessage && input === getMessageText(nextMessage)) {
void sendMessage(nextMessage)
return
}
void sendMessage({ text: input })
}
return (
<Example
title="Sources"
className="bg-transparent p-0"
containerClassName="flex-1"
>
<Card className="max-h-140 min-h-0 flex-1">
<CardHeader>
<CardTitle>Reading list</CardTitle>
<CardDescription>
Source URL and source document parts.
</CardDescription>
<CardAction>
<Button
variant="outline"
size="icon"
aria-label="Start over"
onClick={() => setMessages(initialMessages)}
>
<IconPlaceholder
lucide="PlusIcon"
tabler="IconPlus"
hugeicons="PlusSignIcon"
phosphor="PlusIcon"
remixicon="RiAddLine"
/>
</Button>
</CardAction>
</CardHeader>
<CardContent className="flex-1 overflow-hidden">
<MessageScrollerProvider>
<MessageScroller>
<MessageScrollerViewport>
<MessageScrollerContent>
{messages.map((message) => (
<MessageScrollerItem
key={message.id}
messageId={message.id}
scrollAnchor={message.role === "user"}
>
<Message
align={message.role === "user" ? "end" : "start"}
data-role={message.role}
>
<MessageContent
className={
message.role === "user"
? "flex-col items-end gap-2"
: "flex-col items-start gap-2"
}
>
{message.parts.map((part, index) => (
<React.Fragment key={`${message.id}-${index}`}>
{part.type === "text" ? (
<PartText
part={part}
variant={
message.role === "user"
? "bubble"
: "default"
}
/>
) : part.type === "source-url" ||
part.type === "source-document" ? (
<PartSource part={part} />
) : null}
</React.Fragment>
))}
</MessageContent>
</Message>
</MessageScrollerItem>
))}
{status === "submitted" ? (
<MessageScrollerItem scrollAnchor={false}>
<Marker role="status">
<MarkerIcon>
<Spinner />
</MarkerIcon>
<MarkerContent>Thinking...</MarkerContent>
</Marker>
</MessageScrollerItem>
) : null}
</MessageScrollerContent>
</MessageScrollerViewport>
<MessageScrollerButton />
</MessageScroller>
</MessageScrollerProvider>
</CardContent>
<CardFooter>
<PromptInput
onSubmit={onSubmit}
status={status}
defaultValue={
isBusy ? "" : nextMessage ? getMessageText(nextMessage) : ""
}
/>
</CardFooter>
</Card>
</Example>
)
}

View File

@@ -0,0 +1,215 @@
"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 { PartReasoning } from "@/registry/bases/radix/blocks/preview-03/components/part-reasoning"
import { PartText } from "@/registry/bases/radix/blocks/preview-03/components/part-text"
import { PartTool } from "@/registry/bases/radix/blocks/preview-03/components/part-tool"
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 } 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 Tools = {
getDeploymentHealth: {
input: {
project: string
region: string
}
output: {
status: "healthy" | "degraded" | "down"
p95: number
errors: number
recommendation: string
}
}
}
type ChatMessage = UIMessage<unknown, Record<string, never>, Tools>
const chat = createChat<ChatMessage>()
.user("Check the deployment health for the checkout flow.")
.sleep(1000)
.assistant(({ writer }) => {
writer.reasoning(
"The user needs current operational state, so I should call a deployment-health tool before making a recommendation."
)
const health = writer.tool("getDeploymentHealth", {
title: "Checking deployment health",
input: {
project: "checkout",
region: "iad1",
},
})
health.sleep(1200).output({
status: "healthy",
p95: 186,
errors: 2,
recommendation: "No rollback needed. Watch error rate for 15 minutes.",
})
writer.text(
"The checkout deployment looks healthy. P95 latency is **186ms** with **2 recent errors**, so I would keep the release live and watch it for another 15 minutes."
)
})
const initialMessages = chat.get(0)
const transport = chat.transport()
function isToolPart(part: {
type: string
}): part is React.ComponentProps<typeof PartTool>["part"] {
return part.type === "dynamic-tool" || part.type.startsWith("tool-")
}
export function ToolChat() {
const { status, messages, sendMessage, setMessages } = useChat({
messages: initialMessages,
transport,
})
const nextMessage = chat.next(messages)
const isBusy = status === "submitted" || status === "streaming"
const onSubmit = (input: string) => {
if (!input.trim()) {
return
}
if (nextMessage && input === getMessageText(nextMessage)) {
void sendMessage(nextMessage)
return
}
void sendMessage({ text: input })
}
return (
<Example
title="Reasoning + Tool"
className="bg-transparent p-0"
containerClassName="flex-1"
>
<Card className="max-h-140 min-h-0 flex-1">
<CardHeader>
<CardTitle>Deployment check</CardTitle>
<CardDescription>
Reasoning followed by a tool call and result.
</CardDescription>
<CardAction>
<Button
variant="outline"
size="icon"
aria-label="Start over"
onClick={() => setMessages(initialMessages)}
>
<IconPlaceholder
lucide="PlusIcon"
tabler="IconPlus"
hugeicons="PlusSignIcon"
phosphor="PlusIcon"
remixicon="RiAddLine"
/>
</Button>
</CardAction>
</CardHeader>
<CardContent className="flex-1 overflow-hidden">
<MessageScrollerProvider>
<MessageScroller>
<MessageScrollerViewport>
<MessageScrollerContent>
{messages.map((message) => (
<MessageScrollerItem
key={message.id}
messageId={message.id}
scrollAnchor={message.role === "user"}
>
<Message
align={message.role === "user" ? "end" : "start"}
data-role={message.role}
>
<MessageContent
className={
message.role === "user"
? "flex-col items-end gap-2"
: "flex-col items-start gap-2"
}
>
{message.parts.map((part, index) => (
<React.Fragment key={`${message.id}-${index}`}>
{part.type === "text" ? (
<PartText
part={part}
variant={
message.role === "user"
? "bubble"
: "default"
}
/>
) : part.type === "reasoning" ? (
<PartReasoning part={part} />
) : isToolPart(part) ? (
<PartTool part={part} />
) : null}
</React.Fragment>
))}
</MessageContent>
</Message>
</MessageScrollerItem>
))}
{status === "submitted" ? (
<MessageScrollerItem scrollAnchor={false}>
<Marker role="status">
<MarkerIcon>
<Spinner />
</MarkerIcon>
<MarkerContent>Thinking...</MarkerContent>
</Marker>
</MessageScrollerItem>
) : null}
</MessageScrollerContent>
</MessageScrollerViewport>
<MessageScrollerButton />
</MessageScroller>
</MessageScrollerProvider>
</CardContent>
<CardFooter>
<PromptInput
onSubmit={onSubmit}
status={status}
defaultValue={
isBusy ? "" : nextMessage ? getMessageText(nextMessage) : ""
}
/>
</CardFooter>
</Card>
</Example>
)
}