- Accordion
- Agent Thinking
- Alert
- Alert Dialog
- Announcement
- Aspect Ratio
- Attachment
- Avatar
- Badge
- Breadcrumb
- Bubble
- Button
- Button Group
- Calendar
- Card
- Carousel
- Checkbox
- Checkbox Card
- Code Block
- Collapsible
- Color Picker
- Combobox
- Command
- Composer
- Composer Loader
- Context Menu
- Data Grid
- Data Table
- Date Picker
- Dialog
- Direction
- Drawer
- Dropdown Menu
- Empty
- Field
- File Upload
- Hover Card
- Input
- Input Group
- Input Otp
- Item
- Kbd
- Label
- Marker
- Menubar
- Message
- Message Scroller
- Meter
- Native Select
- Navigation Menu
- Number Field
- Pagination
- Period Switcher
- Popover
- Progress
- Questionnaire
- Radio Card
- Radio Group
- Resizable
- Reveal
- Scroll Area
- Search Field
- Select
- Separator
- Sheet
- Sidebar
- Skeleton
- Slider
- Social Button
- Sonner
- Spinner
- Status Dot
- Switch
- Switch Card
- Table
- Tabs
- Tag Group
- Textarea
- Theme Toggle
- Time Field
- Toggle
- Toggle Group
- Tooltip
- Trace
- Tree View
- Agent Creator 01
- Agent Limits Card 01
- Agent Progress 01
- AI Chat 01
- AI Image Generation 01
- AI Profile 01
- Alerts Card 01
- Auth Card 01
- Calendar 01
- Composer 01
- Composer Attachments 01
- Composer Panel 01
- Finance Dashboard 01
- Home Dashboard 01
- Marketing Dashboard 01
- Medical Profile 01
- Meeting Scheduler 01
- Multi Agent Chat 01
- Notification Center 01
- Patient Info Card 01
- Project Board 01
- Questionnaire 01
- Settings Modal 01
- Sidebar 01
- Stat Cards 01
- Task List 01
- Web Search 01
- Activity Rings Card 01
- Area Chart Card 01
- Bar List Card 01
- Combo Chart Card 01
- Contributions Card 01
- Earnings Chart Card 01
- Funnel Chart Card 01
- Heatmap Chart Card 01
- Line Chart Card 01
- Most Active Days Card 01
- Orders Chart Card 01
- Radar Chart Card 01
- Radial Chart Card 01
- Revenue Chart Card 01
- Sankey Chart Card 01
- Scatter Chart Card 01
- Sleep Score Card 01
- Stage Bars Card 01
- Steps Card 01
Plan the launch page.
I would start with one clear headline, a strong visual hierarchy and a single primary action. Then check the layout at mobile and desktop sizes. Start with the smallest useful version: pick one audience and one outcome, make the main action easy to find, and remove anything that competes with it. A focused first pass makes the next decision much easier.
Once the core experience works, we can add supporting details where they answer a real question. That keeps the first impression simple without losing the depth people need to feel confident.
I would clarify the main takeaway, shorten the opening and replace vague claims with a concrete example. Keep the voice consistent throughout. I would explore two directions before settling on one.
Installation
Set up the @cobalt registry as described in Installation, then add the block:
$ pnpm dlx shadcn@latest add @cobalt/multi-agent-chat-01
The block is made of the existing agent-creator-01, settings-modal-01 and composer pieces. It uses local sample agents and scripted replies. Agents, conversations and edits are saved in localStorage.
Source
"use client";
import {
RiAddLine,
RiArrowDownSLine,
RiCustomerServiceLine,
RiCheckLine,
RiDeleteBinLine,
RiEraserLine,
RiLayoutRightLine,
RiChat3Line,
RiMoonLine,
RiMoreLine,
RiSunLine,
RiSearchLine,
RiSettings3Line,
RiUploadLine,
RiShieldCheckLine,
RiStoreLine,
RiGroupLine,
RiRobot2Line,
} from "@remixicon/react";
import { cn } from "cn";
import Link from "next/link";
import * as React from "react";
import { AgentCreator } from "@/components/blocks/agent-creator-01/components/agent-creator";
import { ComposerModelPicker } from "@/components/blocks/composer-01/components/composer-model-picker";
import { VoiceButton } from "@/components/blocks/composer-01/components/voice-button";
import {
AgentAvatar,
GroupAvatar,
} from "@/components/blocks/multi-agent-chat-01/components/multi-agent-chat-avatar";
import type {
ChatAgent,
ChatConversation,
} from "@/components/blocks/multi-agent-chat-01/components/multi-agent-chat-data";
import { MultiAgentChatGroupDialog } from "@/components/blocks/multi-agent-chat-01/components/multi-agent-chat-group-dialog";
import { MultiAgentChatMarketplace } from "@/components/blocks/multi-agent-chat-01/components/multi-agent-chat-marketplace";
import { useMultiAgentChat } from "@/components/blocks/multi-agent-chat-01/components/multi-agent-chat-store";
import { SettingsModal } from "@/components/blocks/settings-modal-01/components/settings-modal";
import type { Page as SettingsPage } from "@/components/blocks/settings-modal-01/components/settings-modal";
import type { DashboardNavItem } from "@/components/blocks/sidebar-01/components/dashboard-sidebar";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Bubble, BubbleContent } from "@/components/ui/bubble";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import {
Composer,
ComposerInput,
ComposerSend,
ComposerShell,
} from "@/components/ui/composer";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Empty,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty";
import {
InputGroupAddon,
InputGroupButton,
} from "@/components/ui/input-group";
import { Message, MessageContent } from "@/components/ui/message";
import {
MessageScroller,
MessageScrollerButton,
MessageScrollerContent,
MessageScrollerItem,
MessageScrollerProvider,
MessageScrollerViewport,
} from "@/components/ui/message-scroller";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { SearchField } from "@/components/ui/search-field";
import { Sidebar, SidebarProvider } from "@/components/ui/sidebar";
const enter = "multi-agent-chat-01-enter";
const models = ["Fable 5", "Fable 4", "Swift"];
const starters = [
"Summarize what we decided",
"Suggest the next three steps",
"Review this with fresh eyes",
];
function titleOf(conversation: ChatConversation, agents: ChatAgent[]) {
if (conversation.title) {
return conversation.title;
}
const members = conversation.agentIds.flatMap((id) => {
const agent = agents.find((item) => item.id === id);
return agent ? [agent] : [];
});
return conversation.group
? members.map((agent) => agent.label || agent.name).join(" and ")
: (members[0]?.name ?? "Conversation");
}
/**
* A three-column agent workspace: conversations, a chat where several agents
* can answer in turn, and a live avatar editor. Everything is sample data and
* scripted replies; state is kept in localStorage.
*/
export function MultiAgentChat({
className,
popupClassName,
pages,
theme,
onThemeChange,
}: {
className?: string;
popupClassName?: string;
/** Other pages to list in the account menu, as a way back out of the app. */
pages?: DashboardNavItem[];
/** With `onThemeChange`, adds an appearance switch to the account menu and the settings. */
theme?: "light" | "dark";
onThemeChange?: (theme: "light" | "dark" | "system") => void;
}) {
const chat = useMultiAgentChat();
const rootRef = React.useRef<HTMLDivElement>(null);
const [wide, setWide] = React.useState(false);
const [inspector, setInspector] = React.useState<"auto" | "open" | "closed">(
"auto"
);
const [inspectedId, setInspectedId] = React.useState<string | null>(null);
const [drawer, setDrawer] = React.useState(false);
const [searching, setSearching] = React.useState(false);
const [query, setQuery] = React.useState("");
const [draft, setDraft] = React.useState("");
const [model, setModel] = React.useState(models[0]);
const [bypass, setBypass] = React.useState(false);
const [listening, setListening] = React.useState(false);
const [plusOpen, setPlusOpen] = React.useState(false);
const [groupOpen, setGroupOpen] = React.useState(false);
const [settings, setSettings] = React.useState<{
open: boolean;
page: SettingsPage;
}>({ open: false, page: "general" });
const [copied, setCopied] = React.useState(false);
React.useEffect(() => {
const element = rootRef.current;
if (!element) {
return;
}
const observer = new ResizeObserver(([entry]) =>
setWide(entry.contentRect.width >= 1024)
);
observer.observe(element);
return () => observer.disconnect();
}, []);
React.useEffect(() => {
if (!copied) {
return;
}
const timer = setTimeout(() => setCopied(false), 1500);
return () => clearTimeout(timer);
}, [copied]);
const { agents, conversations } = chat;
const active =
conversations.find((item) => item.id === chat.activeId) ?? conversations[0];
const members = active.agentIds.flatMap((id) => {
const agent = agents.find((item) => item.id === id);
return agent ? [agent] : [];
});
const inspected =
members.find((agent) => agent.id === inspectedId) ?? members[0];
const inspectorVisible =
inspector === "open" || (inspector === "auto" && wide);
const thinkingAgent =
chat.thinking?.conversationId === active.id
? agents.find((agent) => agent.id === chat.thinking?.agentId)
: undefined;
const needle = query.trim().toLowerCase();
const preview = (conversation: ChatConversation) => {
if (chat.thinking?.conversationId === conversation.id) {
return "Thinking...";
}
const last = conversation.messages.at(-1);
if (last) {
return last.text;
}
const first = agents.find((agent) => agent.id === conversation.agentIds[0]);
return conversation.group
? "A new group chat"
: (first?.description ?? "Start a conversation");
};
const visible = conversations.filter(
(conversation) =>
!needle ||
`${titleOf(conversation, agents)} ${preview(conversation)}`
.toLowerCase()
.includes(needle)
);
const openSettings = (page: "general" | "marketplace") =>
setSettings({ open: true, page });
const select = (id: string) => {
chat.setActive(id);
setInspectedId(null);
setDrawer(false);
};
const share = () => {
const text = active.messages
.map((message) => {
const who =
message.role === "user"
? "You"
: (agents.find((agent) => agent.id === message.agentId)?.name ??
"Agent");
return `${who}: ${message.text}`;
})
.join("\n");
void navigator.clipboard?.writeText(text).catch(() => setCopied(false));
setCopied(true);
};
const toggleInspector = () =>
setInspector(inspectorVisible ? "closed" : "open");
return (
<div
ref={rootRef}
className={cn(
"@container relative flex h-[620px] min-h-0 w-full gap-4 overflow-clip text-foreground",
className
)}
>
{drawer && (
<div className="absolute inset-0 z-10 rounded-[16px] bg-foreground/20 @3xl:hidden">
<Button
variant="unstyled"
radius={16}
aria-label="Close conversations"
className="size-full"
onClick={() => setDrawer(false)}
/>
</div>
)}
<div
className={cn(
"absolute inset-y-0 left-0 z-20 shrink-0 transition-transform duration-200 ease-out motion-reduce:transition-none @3xl:static @3xl:translate-x-0",
drawer ? "translate-x-0" : "-translate-x-[110%]"
)}
>
<SidebarProvider
persistState={false}
keyboardShortcut={false}
style={{ "--sidebar-width": "260px" } as React.CSSProperties}
className="h-full min-h-0 w-auto"
>
<Sidebar collapsible="none" variant="floating" className="shrink-0">
<div className="flex min-h-0 flex-1 flex-col p-2">
<div className="flex h-9 items-center gap-2">
{searching ? (
<SearchField
aria-label="Search conversations"
placeholder="Search conversations"
size="sm"
autoFocus
fieldClassName="w-full"
className="w-full"
value={query}
onChange={setQuery}
onClear={() => {
setQuery("");
setSearching(false);
}}
onKeyDown={(event) => {
if (event.key === "Escape") {
setQuery("");
setSearching(false);
}
}}
/>
) : (
<>
<Button
variant="muted"
shape="pill"
className="flex-1"
tooltip="Search conversations"
aria-label="Search conversations"
onClick={() => setSearching(true)}
>
<RiSearchLine />
</Button>
<Button
variant="muted"
shape="pill"
className="flex-1"
tooltip="Marketplace"
aria-label="Marketplace"
onClick={() => openSettings("marketplace")}
>
<RiStoreLine />
</Button>
<Popover open={plusOpen} onOpenChange={setPlusOpen}>
<PopoverTrigger
render={
<Button
shape="pill"
className="flex-1"
tooltip="New chat"
aria-label="New chat"
/>
}
>
<RiAddLine />
</PopoverTrigger>
<PopoverContent
align="start"
size="xs"
className={cn("w-72", popupClassName)}
>
<Button
variant="ghost"
className="h-auto justify-start text-left whitespace-normal"
onClick={() => {
setPlusOpen(false);
chat.createBot();
setInspector("open");
setDrawer(false);
}}
>
<span className="flex items-center gap-3 py-1.5">
{agents[5] ? (
<AgentAvatar
agent={agents[5]}
className="size-9"
/>
) : (
<RiRobot2Line className="size-5" aria-hidden />
)}
<span className="min-w-0">
<span className="block text-sm font-medium">
Create a new bot
</span>
<span className="block text-xs font-normal text-muted-foreground">
Design its look and voice
</span>
</span>
</span>
</Button>
<Button
variant="ghost"
className="h-auto justify-start text-left whitespace-normal"
onClick={() => {
setPlusOpen(false);
setGroupOpen(true);
}}
>
<span className="flex items-center gap-3 py-1.5">
{agents.length > 1 ? (
<GroupAvatar
agents={[
agents[3] ?? agents[0],
agents[4] ?? agents[1],
]}
className="size-9"
/>
) : (
<RiGroupLine className="size-5" aria-hidden />
)}
<span className="min-w-0">
<span className="block text-sm font-medium">
Chat with your agents
</span>
<span className="block text-xs font-normal text-muted-foreground">
Start a group conversation
</span>
</span>
</span>
</Button>
</PopoverContent>
</Popover>
</>
)}
</div>
<nav
aria-label="Conversations"
className="mt-2 flex min-h-0 flex-1 flex-col gap-0.5 overflow-y-auto"
>
{visible.map((conversation, index) => {
const people = conversation.agentIds.flatMap((id) => {
const agent = agents.find((item) => item.id === id);
return agent ? [agent] : [];
});
const current = conversation.id === active.id;
return (
<div
key={conversation.id}
style={
{
"--delay": `${Math.min(index, 8) * 40}ms`,
} as React.CSSProperties
}
className={cn("shrink-0", enter)}
>
<Button
variant={current ? "secondary" : "ghost"}
radius={12}
aria-current={current ? "true" : undefined}
onClick={() => select(conversation.id)}
className="h-auto w-full justify-start text-left"
>
<span className="flex min-w-0 flex-1 items-center gap-3 py-2">
{conversation.group ? (
<GroupAvatar agents={people} className="size-10" />
) : (
<AgentAvatar
agent={people[0]}
className="size-10"
/>
)}
<span className="min-w-0 flex-1">
<span className="block truncate text-sm font-medium">
{titleOf(conversation, agents)}
</span>
<span
className={cn(
"block truncate text-sm font-normal text-muted-foreground/70",
chat.thinking?.conversationId ===
conversation.id &&
"shimmer motion-reduce:shimmer-none"
)}
>
{preview(conversation)}
</span>
</span>
</span>
</Button>
</div>
);
})}
{visible.length === 0 && (
<Empty variant="ghost">
<EmptyHeader>
<EmptyDescription>
No conversations found.
</EmptyDescription>
</EmptyHeader>
</Empty>
)}
</nav>
<div className="mt-2 flex shrink-0 flex-col gap-1">
<Button
variant="text"
className="justify-start gap-2"
nativeButton={false}
render={
// Base UI renders the Button children into this anchor.
// oxlint-disable-next-line jsx-a11y/anchor-has-content
<a href="mailto:support@example.com" />
}
>
<RiCustomerServiceLine /> Support
</Button>
<Button
variant="text"
className="justify-start gap-2"
onClick={() => openSettings("general")}
>
<RiSettings3Line /> Settings
</Button>
<DropdownMenu>
<DropdownMenuTrigger
aria-label="Workspace menu"
render={
<Button
variant="muted"
radius={12}
className="group mt-1 h-auto w-full justify-start text-left"
/>
}
>
<span className="flex min-w-0 flex-1 items-center gap-2 py-1.5">
<Avatar color="primary">
<AvatarFallback>N</AvatarFallback>
</Avatar>
<span className="min-w-0 flex-1">
<span className="block truncate text-sm font-medium text-foreground">
Northstar team
</span>
<span className="block truncate text-sm font-normal text-muted-foreground">
hello@example.com
</span>
</span>
<span className="flex size-6 shrink-0 items-center justify-center rounded-[6px] bg-surface-strong text-muted-foreground">
<RiArrowDownSLine className="size-4 transition-transform duration-150 ease-out group-aria-expanded:rotate-180 motion-reduce:transition-none" />
</span>
</span>
</DropdownMenuTrigger>
<DropdownMenuContent
side="top"
align="start"
className={cn("w-56", popupClassName)}
>
{pages && pages.length > 0 && (
<>
<DropdownMenuGroup>
<DropdownMenuLabel>Pages</DropdownMenuLabel>
{pages.map(({ key, label, href, icon: Icon }) => (
<DropdownMenuItem
key={key}
render={<Link href={href} />}
>
<Icon /> {label}
</DropdownMenuItem>
))}
</DropdownMenuGroup>
<DropdownMenuSeparator />
</>
)}
<DropdownMenuGroup>
<DropdownMenuItem onClick={() => openSettings("general")}>
<RiSettings3Line /> Account settings
</DropdownMenuItem>
{onThemeChange && (
<DropdownMenuItem
onClick={() =>
onThemeChange(theme === "dark" ? "light" : "dark")
}
>
{theme === "dark" ? <RiSunLine /> : <RiMoonLine />}
{theme === "dark" ? "Light theme" : "Dark theme"}
</DropdownMenuItem>
)}
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</Sidebar>
</SidebarProvider>
</div>
<Card
radius={24}
size="flush"
variant="raised"
className="min-w-0 flex-1"
>
<header className="flex h-14 shrink-0 items-center gap-2 px-3">
<Button
variant="ghost"
size="icon-sm"
className="@3xl:hidden"
tooltip="Conversations"
onClick={() => setDrawer(true)}
>
<RiChat3Line />
</Button>
<div className="flex h-8 min-w-0">
<Button
variant="muted"
size="sm"
shape="pill"
aria-label={`Edit ${inspected?.name ?? "agent"}`}
onClick={() => setInspector("open")}
className="min-w-0 gap-1.5"
>
{active.group ? (
<GroupAvatar agents={members} className="size-6" />
) : (
<AgentAvatar agent={members[0]} className="size-6" />
)}
<span className="truncate">{titleOf(active, agents)}</span>
</Button>
</div>
<div className="flex-1" />
<Button
variant="ghost-muted"
size="icon-sm"
tooltip={copied ? "Copied" : "Copy conversation"}
onClick={share}
>
{copied ? <RiCheckLine /> : <RiUploadLine />}
</Button>
<Button
variant="ghost"
size="icon-sm"
className="@5xl:hidden"
tooltip={inspectorVisible ? "Hide editor" : "Edit agent"}
aria-pressed={inspectorVisible}
onClick={toggleInspector}
>
<RiLayoutRightLine />
</Button>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
variant="ghost-muted"
size="icon-sm"
tooltip="More"
aria-label="More"
/>
}
>
<RiMoreLine />
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className={cn("w-52", popupClassName)}
>
<DropdownMenuGroup>
<DropdownMenuItem onClick={() => openSettings("general")}>
<RiSettings3Line />
Settings
</DropdownMenuItem>
<DropdownMenuItem
disabled={active.messages.length === 0}
onClick={() => chat.clearConversation(active.id)}
>
<RiEraserLine />
Clear messages
</DropdownMenuItem>
<DropdownMenuItem
disabled={conversations.length < 2}
onClick={() => chat.deleteConversation(active.id)}
>
<RiDeleteBinLine />
Delete conversation
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</header>
<div className="min-h-0 flex-1">
{active.messages.length === 0 && !thinkingAgent ? (
<Empty variant="ghost" className="h-full">
<EmptyHeader>
<EmptyMedia className="mb-2 h-20" aria-hidden>
{members.length > 1 ? (
<span className="flex items-center -space-x-4">
{members.slice(0, 3).map((agent) => (
<AgentAvatar
key={agent.id}
agent={agent}
className="size-16"
/>
))}
</span>
) : (
<AgentAvatar agent={members[0]} className="size-20" />
)}
</EmptyMedia>
<EmptyTitle>
{members.length > 1
? "Your agents are ready"
: `Say hi to ${members[0]?.name}`}
</EmptyTitle>
<EmptyDescription>
{members.length > 1
? "Send a message and each agent answers in turn."
: (members[0]?.description ?? "Ask anything to begin.")}
</EmptyDescription>
</EmptyHeader>
</Empty>
) : (
<MessageScrollerProvider
key={active.id}
autoScroll
defaultScrollPosition="end"
>
<MessageScroller>
<MessageScrollerViewport>
<MessageScrollerContent className="gap-4 px-4 pt-4 pb-2">
<div className="flex justify-center">
<span className="rounded-full bg-muted px-3 py-1 text-xs text-muted-foreground">
Today
</span>
</div>
{active.messages.map((message, index) => {
const author =
message.role === "agent"
? agents.find((agent) => agent.id === message.agentId)
: undefined;
return (
<MessageScrollerItem
key={message.id}
messageId={message.id}
style={
{
"--delay": `${Math.min(index, 8) * 40}ms`,
} as React.CSSProperties
}
>
<div className={enter}>
<Message
align={message.role === "user" ? "end" : "start"}
>
<MessageContent className="gap-1.5">
<Bubble
align={
message.role === "user" ? "end" : "start"
}
variant={
message.role === "user"
? "default"
: "secondary"
}
className="max-w-[min(520px,85%)]"
>
<BubbleContent className="flex flex-col gap-2 whitespace-normal">
{message.text.split("\n\n").map((part) => (
<p key={part}>{part}</p>
))}
</BubbleContent>
</Bubble>
{author && (
<Button
variant="text"
size="xs"
aria-label={`Edit ${author.name}`}
onClick={() => {
setInspectedId(author.id);
setInspector("open");
}}
className="w-fit gap-1.5"
>
<AgentAvatar
agent={author}
className="size-5"
/>
{author.name}
</Button>
)}
</MessageContent>
</Message>
</div>
</MessageScrollerItem>
);
})}
{thinkingAgent && (
<MessageScrollerItem key="thinking" messageId="thinking">
<span
role="status"
className={cn(
"flex items-center gap-1.5 px-1 text-xs text-muted-foreground",
enter
)}
>
<AgentAvatar
agent={thinkingAgent}
className="size-4"
/>
<span className="shimmer motion-reduce:shimmer-none">
{thinkingAgent.name} is thinking...
</span>
</span>
</MessageScrollerItem>
)}
</MessageScrollerContent>
</MessageScrollerViewport>
<MessageScrollerButton />
</MessageScroller>
</MessageScrollerProvider>
)}
</div>
<div className="p-1">
<Composer
value={draft}
onValueChange={setDraft}
working={chat.replying}
onSend={(text) => chat.send(active.id, text)}
>
<ComposerShell shape="default">
<ComposerInput
aria-label="Message"
placeholder="Hi, what do you need today?"
className="min-h-13"
/>
<InputGroupAddon align="block-end" className="relative z-1 px-2">
<DropdownMenu>
<DropdownMenuTrigger
render={
<InputGroupButton
shape="pill"
size="icon"
aria-label="Add"
tooltip="Add"
variant="tonal"
/>
}
>
<RiAddLine className="size-5" />
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
side="top"
className={cn("w-60", popupClassName)}
>
<DropdownMenuGroup>
<DropdownMenuLabel>Start with</DropdownMenuLabel>
{starters.map((starter) => (
<DropdownMenuItem
key={starter}
onClick={() => setDraft(starter)}
>
{starter}
</DropdownMenuItem>
))}
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
<Button
variant="ghost-muted"
size="sm"
aria-pressed={bypass}
onClick={() => setBypass(!bypass)}
className="aria-pressed:text-primary"
>
<RiShieldCheckLine data-icon="inline-start" />
{bypass ? "Bypass on" : "Bypass all"}
</Button>
<div className="flex-1" />
<ComposerModelPicker
model={model}
models={models}
onModelChange={setModel}
popupClassName={popupClassName}
/>
<VoiceButton
listening={listening}
onClick={() => setListening(!listening)}
/>
<ComposerSend />
</InputGroupAddon>
</ComposerShell>
</Composer>
</div>
</Card>
{inspected && (
<div
className={cn(
"absolute inset-y-0 right-0 z-20 w-[336px] max-w-full shrink-0 rounded-[24px] bg-background shadow-md @5xl:static @5xl:z-auto @5xl:bg-transparent @5xl:shadow-none",
inspector === "closed"
? "hidden"
: inspector === "open"
? "block"
: "hidden @5xl:block"
)}
>
<AgentCreator
agent={inspected}
onChange={(next) => chat.updateAgent({ ...inspected, ...next })}
popupClassName={popupClassName}
onClose={() => setInspector("closed")}
bare
className="h-full w-full"
/>
</div>
)}
<MultiAgentChatGroupDialog
open={groupOpen}
onOpenChange={setGroupOpen}
agents={agents}
onStart={(ids) => {
chat.createGroup(ids);
setDrawer(false);
}}
popupClassName={popupClassName}
/>
<SettingsModal
open={settings.open}
onOpenChange={(open) =>
setSettings((current) => ({ ...current, open }))
}
defaultPage={settings.page}
onThemeChange={onThemeChange}
className={popupClassName}
marketplace={
<MultiAgentChatMarketplace
items={chat.marketItems}
installedIds={chat.installed}
onInstall={(item) => {
chat.install(item);
if (item.kind === "bot") {
setInspectedId(null);
}
}}
onRemove={chat.uninstallPlugin}
/>
}
/>
</div>
);
}