mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-07-08 14:35:09 +00:00
DropdownMenuLabel renders Base UI's Menu.GroupLabel, which throws "MenuGroupContext is missing" (Base UI error #31) unless rendered inside a Menu.Group. Wrap the label and items in DropdownMenuGroup so the "Jump to..." menu opens without crashing. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
169 lines
6.4 KiB
TypeScript
169 lines
6.4 KiB
TypeScript
"use client"
|
|
|
|
import * as React from "react"
|
|
|
|
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,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/styles/base-rhea/ui/card"
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuGroup,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuTrigger,
|
|
} from "@/styles/base-rhea/ui/dropdown-menu"
|
|
import { Message, MessageContent } from "@/styles/base-rhea/ui/message"
|
|
import {
|
|
MessageScroller,
|
|
MessageScrollerButton,
|
|
MessageScrollerContent,
|
|
MessageScrollerItem,
|
|
MessageScrollerProvider,
|
|
MessageScrollerViewport,
|
|
useMessageScroller,
|
|
} from "@/styles/base-rhea/ui/message-scroller"
|
|
|
|
const chat = createChat()
|
|
.user(
|
|
"We're seeing activation dip after workspace creation. Can you help me find the likely step?",
|
|
{ id: "command-activation" }
|
|
)
|
|
.assistant(
|
|
"The sharpest drop is between creating the workspace and inviting the first teammate.\n\nWorkspace creation is still healthy, but the invite step is where users pause. That suggests the product is asking for collaboration before the user has enough confidence in the workspace."
|
|
)
|
|
.user("What should I compare before we change the onboarding flow?", {
|
|
id: "command-compare",
|
|
})
|
|
.assistant(
|
|
"Compare three cohorts:\n\n1. Users who choose a template before inviting teammates.\n2. Users who start from a blank workspace.\n3. Users who skip invites and return within 24 hours.\n\nIf template users invite faster, the fix is probably better first-run guidance rather than a louder invite prompt."
|
|
)
|
|
.user("Can you turn that into an experiment?", {
|
|
id: "command-experiment",
|
|
})
|
|
.assistant(
|
|
"Yes. Create a variant that shows a short checklist after workspace creation:\n\n- Pick a template.\n- Add one project detail.\n- Invite a teammate when the workspace has context.\n\nMeasure first invite completion, 24-hour return rate, and whether teams create a second project."
|
|
)
|
|
.user("What's the risk if we delay the invite prompt?", {
|
|
id: "command-risk",
|
|
})
|
|
.assistant(
|
|
"The main risk is reducing team creation for accounts that already know who they want to invite.\n\nTo protect that path, keep the invite action visible in the header and only change the primary empty-state guidance. That gives confident teams a direct route without forcing uncertain users through the invite step too early."
|
|
)
|
|
|
|
const messages = chat.get()
|
|
const userMessages = messages.filter((message) => message.role === "user")
|
|
|
|
export function MessageScrollerCommands() {
|
|
return (
|
|
<MessageScrollerProvider defaultScrollPosition="end">
|
|
<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>Commands</CardTitle>
|
|
<CardDescription>
|
|
Drive the transcript from outside.
|
|
</CardDescription>
|
|
<CardAction>
|
|
<CommandMenu />
|
|
</CardAction>
|
|
</CardHeader>
|
|
<CardContent className="flex-1 overflow-hidden p-0">
|
|
<MessageScroller>
|
|
<MessageScrollerViewport>
|
|
<MessageScrollerContent className="p-(--card-spacing)">
|
|
{messages.map((message) => {
|
|
const isUserMessage = message.role === "user"
|
|
const text = getMessageText(message)
|
|
|
|
return (
|
|
<MessageScrollerItem
|
|
key={message.id}
|
|
messageId={message.id}
|
|
scrollAnchor={isUserMessage}
|
|
>
|
|
<Message align={isUserMessage ? "end" : "start"}>
|
|
<MessageContent>
|
|
<Bubble variant={isUserMessage ? "muted" : "ghost"}>
|
|
<BubbleContent className="space-y-2">
|
|
{text
|
|
.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>
|
|
)
|
|
})}
|
|
</MessageScrollerContent>
|
|
</MessageScrollerViewport>
|
|
<MessageScrollerButton />
|
|
</MessageScroller>
|
|
</CardContent>
|
|
</Card>
|
|
<div className="mx-auto max-w-sm px-0.5 text-center text-xs text-balance text-muted-foreground">
|
|
Use the controls to jump to any message in the conversation.
|
|
</div>
|
|
</div>
|
|
</MessageScrollerProvider>
|
|
)
|
|
}
|
|
|
|
function CommandMenu() {
|
|
const { scrollToMessage } = useMessageScroller()
|
|
|
|
return (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger
|
|
render={<Button type="button" variant="secondary" />}
|
|
>
|
|
Jump to...
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" side="bottom" className="w-64">
|
|
<DropdownMenuGroup>
|
|
<DropdownMenuLabel>Conversations</DropdownMenuLabel>
|
|
{userMessages.map((message) => (
|
|
<DropdownMenuItem
|
|
key={message.id}
|
|
onSelect={() =>
|
|
scrollToMessage(message.id, {
|
|
align: "start",
|
|
behavior: "smooth",
|
|
})
|
|
}
|
|
>
|
|
<span className="line-clamp-1 min-w-0">
|
|
{getTrimmedMessageText(message)}
|
|
</span>
|
|
</DropdownMenuItem>
|
|
))}
|
|
</DropdownMenuGroup>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
)
|
|
}
|
|
|
|
function getTrimmedMessageText(message: (typeof userMessages)[number]) {
|
|
const text = getMessageText(message)
|
|
|
|
return text.length > 42 ? `${text.slice(0, 39)}...` : text
|
|
}
|