mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-07-09 06:55:07 +00:00
* feat(@shadcn/react): add message-scroller package Add the @shadcn/react headless primitives package with MessageScroller scroll anchoring, streaming follow, history prepend, and jump-to-message behavior. Includes geometry helpers, use-render utility, and unit, browser, and perf tests. * feat(registry): add chat components Add MessageScroller, Message, Bubble, Attachment, and Marker registry sources for base and radix, style variants, preview-03 chat blocks, and registry index wiring. * feat(v4): integrate chat components into docs site Wire chat components into the v4 app with docs routes, example preview pages, message part renderers, markdown support, registry build updates, and supporting lib utilities. * feat(examples): add chat component demos Add base and radix example demos for MessageScroller, Message, Bubble, Attachment, Marker, scroll-fade, and shimmer. * docs: add chat component documentation Add component and utility docs for the chat component set, update docs navigation, and add the June 2026 chat components changelog entry. * chore: regenerate registry JSON output Rebuild public registry artifacts for all style variants with the new chat components. * chore(release): add @shadcn/react publish and CI pipeline Add Changesets prerelease workflow, browser test job, RELEASING docs, and monorepo wiring for publishing @shadcn/react independently from the shadcn CLI. * docs: fix display of component preview on mobile * fix * fix * docs: add message scroller docs * style: format * fix
172 lines
7.0 KiB
TypeScript
172 lines
7.0 KiB
TypeScript
"use client"
|
|
|
|
import * as React from "react"
|
|
import { RotateCwIcon } from "lucide-react"
|
|
import { toast } from "sonner"
|
|
|
|
import { createChat, getMessageText } from "@/lib/ai"
|
|
import { Bubble, BubbleContent } from "@/styles/base-rhea/ui/bubble"
|
|
import { Button } from "@/styles/base-rhea/ui/button"
|
|
import {
|
|
Card,
|
|
CardAction,
|
|
CardContent,
|
|
CardDescription,
|
|
CardFooter,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/styles/base-rhea/ui/card"
|
|
import { Marker, MarkerContent } from "@/styles/base-rhea/ui/marker"
|
|
import { Message, MessageContent } from "@/styles/base-rhea/ui/message"
|
|
import {
|
|
MessageScroller,
|
|
MessageScrollerButton,
|
|
MessageScrollerContent,
|
|
MessageScrollerItem,
|
|
MessageScrollerProvider,
|
|
MessageScrollerViewport,
|
|
} from "@/styles/base-rhea/ui/message-scroller"
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from "@/styles/base-rhea/ui/tooltip"
|
|
|
|
const chat = createChat()
|
|
.user("Can you summarize the incident channel?")
|
|
.assistant(
|
|
"The first alert was a delayed export job. It started backing up around 09:42 UTC and triggered the warning once the retry queue crossed the threshold.\n\nNo customer-facing checkout paths were affected, but exports for larger workspaces were running about 12 minutes behind."
|
|
)
|
|
.user("Was checkout affected?")
|
|
.assistant(
|
|
"No checkout errors were reported. Payment authorization, order creation, and confirmation emails stayed inside their normal latency bands.\n\nThe only elevated metric was export queue depth, which maps to analytics downloads instead of checkout."
|
|
)
|
|
.user("What changed in the last deploy?")
|
|
.assistant(
|
|
"Only the export queue worker changed. The deploy moved large CSV jobs onto the shared retry policy, which made each failed attempt hold a worker slot longer than before.\n\nThe app deploy did not include checkout, pricing, or billing API changes."
|
|
)
|
|
.user("Do we need to roll back?")
|
|
.assistant(
|
|
"Not yet. Queue depth is recovering after we reduced retry concurrency, and the oldest pending job is now under five minutes old.\n\nKeep rollback ready if the queue starts climbing again, but the current trend points toward recovery."
|
|
)
|
|
.user("Keep watching for customer-visible issues.")
|
|
.assistant(
|
|
"I will watch the queue and support tags for another 15 minutes. I am tracking export failures, delayed download requests, and any support thread that mentions missing reports.\n\nIf those stay quiet through the next batch window, we can close this as an internal degradation."
|
|
)
|
|
|
|
const history = chat.get()
|
|
const INITIAL_VISIBLE_COUNT = 5
|
|
|
|
export function MessageScrollerLoadHistory() {
|
|
const [demoKey, setDemoKey] = React.useState(0)
|
|
const [visibleCount, setVisibleCount] = React.useState(INITIAL_VISIBLE_COUNT)
|
|
const visibleMessages = history.slice(-visibleCount)
|
|
const canLoadHistory = visibleCount < history.length
|
|
|
|
return (
|
|
<MessageScrollerProvider>
|
|
<div className="relative flex flex-col gap-4">
|
|
<Card className="mx-auto h-140 w-full max-w-sm gap-0">
|
|
<CardHeader className="gap-1 border-b">
|
|
<CardTitle>Load History</CardTitle>
|
|
<CardDescription>
|
|
Prepended messages keep your place.
|
|
</CardDescription>
|
|
<CardAction>
|
|
<Tooltip>
|
|
<TooltipTrigger
|
|
render={
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="icon"
|
|
aria-label="Reset loaded messages"
|
|
disabled={visibleCount === INITIAL_VISIBLE_COUNT}
|
|
onClick={() => {
|
|
setVisibleCount(INITIAL_VISIBLE_COUNT)
|
|
setDemoKey((key) => key + 1)
|
|
}}
|
|
/>
|
|
}
|
|
>
|
|
<RotateCwIcon />
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p>Reset</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</CardAction>
|
|
</CardHeader>
|
|
<CardContent className="flex-1 overflow-hidden p-0">
|
|
<MessageScroller key={demoKey}>
|
|
<MessageScrollerViewport>
|
|
<MessageScrollerContent className="p-(--card-spacing)">
|
|
{visibleMessages.map((message) => {
|
|
const isUserMessage = message.role === "user"
|
|
|
|
return (
|
|
<MessageScrollerItem
|
|
key={message.id}
|
|
messageId={message.id}
|
|
>
|
|
<Message align={isUserMessage ? "end" : "start"}>
|
|
<MessageContent>
|
|
<Bubble variant={isUserMessage ? "muted" : "ghost"}>
|
|
<BubbleContent className="space-y-2">
|
|
{getMessageText(message)
|
|
.split(/\n\s*\n/)
|
|
.map((paragraph) => paragraph.trim())
|
|
.filter(Boolean)
|
|
.map((paragraph, index) => (
|
|
<p
|
|
key={index}
|
|
className="whitespace-pre-wrap"
|
|
>
|
|
{paragraph}
|
|
</p>
|
|
))}
|
|
</BubbleContent>
|
|
</Bubble>
|
|
</MessageContent>
|
|
</Message>
|
|
</MessageScrollerItem>
|
|
)
|
|
})}
|
|
<MessageScrollerItem scrollAnchor={false}>
|
|
<Marker variant="separator">
|
|
<MarkerContent>End of Conversation</MarkerContent>
|
|
</Marker>
|
|
</MessageScrollerItem>
|
|
</MessageScrollerContent>
|
|
</MessageScrollerViewport>
|
|
<MessageScrollerButton />
|
|
</MessageScroller>
|
|
</CardContent>
|
|
<CardFooter className="flex flex-col items-center gap-2 border-t">
|
|
<Button
|
|
type="button"
|
|
disabled={!canLoadHistory}
|
|
onClick={() => {
|
|
setVisibleCount(history.length)
|
|
toast("History loaded", {
|
|
description: "Scroll up to see earlier messages.",
|
|
})
|
|
}}
|
|
className="w-full"
|
|
variant="secondary"
|
|
>
|
|
{canLoadHistory ? "Load History" : "History Loaded"}
|
|
</Button>
|
|
<p className="text-xs text-muted-foreground">
|
|
Restore earlier messages while keeping your place.
|
|
</p>
|
|
</CardFooter>
|
|
</Card>
|
|
<div className="mx-auto max-w-sm px-0.5 text-center text-xs text-balance text-muted-foreground">
|
|
Click Load History to load the entire conversation
|
|
</div>
|
|
</div>
|
|
</MessageScrollerProvider>
|
|
)
|
|
}
|