- 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
Installation
Set up the @cobalt registry first, as described in Installation, then add the block:
$ pnpm dlx shadcn@latest add @cobalt/composer-01
The menus, models, and folders are sample content, and sending simulates three seconds of work. Replace them with your own data in components/chat-composer.tsx.
Source
"use client";
import {
RiBookOpenLine,
RiArrowDownSLine,
RiFolderLine,
RiGitBranchLine,
RiGlobeLine,
RiInfinityLine,
RiLink,
RiAttachment2,
RiAddLine,
RiTerminalLine,
} from "@remixicon/react";
import { cn } from "cn";
import * as React from "react";
import { ContextMeter } from "@/components/blocks/composer-01/components/context-meter";
import { VoiceButton } from "@/components/blocks/composer-01/components/voice-button";
import { Button } from "@/components/ui/button";
import {
Composer,
ComposerFooter,
ComposerInput,
ComposerSend,
ComposerShell,
ComposerThinking,
ComposerToolbar,
} from "@/components/ui/composer";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { InputGroupButton } from "@/components/ui/input-group";
import { ComposerModelPicker } from "./composer-model-picker";
const addItems = [
{
description: "Images, PDFs, or text",
icon: RiAttachment2,
title: "Upload a file",
},
{ description: "Read a web page", icon: RiLink, title: "Paste a link" },
];
const toolItems = [
{ description: "Look up sources", icon: RiGlobeLine, title: "Web lookup" },
{
description: "Try it in a sandbox",
icon: RiTerminalLine,
title: "Run code",
},
{ description: "Search your notes", icon: RiBookOpenLine, title: "Library" },
];
const models = ["Swift", "Steady", "Thorough", "Maximal"];
const effortLevels = ["Low", "Medium", "High", "Higher", "Highest", "Max"];
const folders = [
{ name: "harbor", path: "~/work/" },
{ name: "lighthouse", path: "~/work/" },
{ name: "garden", path: "~/notes/" },
];
const chevronClass =
"size-3 transition-transform duration-150 ease-out motion-reduce:transition-none group-aria-expanded/button:rotate-180";
function MenuRows({ items }: { items: typeof addItems }) {
return items.map(({ description, icon: Icon, title }) => (
<DropdownMenuItem key={title}>
<Icon />
<span className="font-medium">{title}</span>
<span className="truncate text-muted-foreground">{description}</span>
</DropdownMenuItem>
));
}
export function ChatComposer({
className,
popupClassName,
value,
onValueChange,
onSend,
}: {
className?: string;
popupClassName?: string;
/** Controls the prompt text. Leave unset to let the composer keep it. */
value?: string;
onValueChange?: (value: string) => void;
/** Called with the trimmed message when the user sends. */
onSend?: (message: string) => void;
}) {
const [folder, setFolder] = React.useState(folders[0].name);
const [lastSent, setLastSent] = React.useState<string | null>(null);
const [listening, setListening] = React.useState(false);
const [model, setModel] = React.useState(models[1]);
const [effort, setEffort] = React.useState(1);
const [working, setWorking] = React.useState(false);
const workTimer = React.useRef<ReturnType<typeof setTimeout> | null>(null);
React.useEffect(
() => () => {
if (workTimer.current) {
clearTimeout(workTimer.current);
}
},
[]
);
return (
<div className={cn("flex w-full flex-col gap-3", className)}>
<Composer
value={value}
onValueChange={onValueChange}
onSend={(message) => {
onSend?.(message);
setLastSent(message);
setWorking(true);
workTimer.current = setTimeout(() => setWorking(false), 3000);
}}
working={working}
>
<ComposerThinking />
<ComposerShell>
<ComposerToolbar align="inline-start">
<DropdownMenu>
<DropdownMenuTrigger
render={
<InputGroupButton
aria-label="Add"
tooltip="Add"
variant="tonal"
shape="pill"
size="icon"
>
<RiAddLine className="size-5" />
</InputGroupButton>
}
/>
<DropdownMenuContent
size="sm"
align="start"
className={cn("w-80", popupClassName)}
side="top"
sideOffset={8}
>
<DropdownMenuGroup>
<DropdownMenuLabel>Add</DropdownMenuLabel>
<MenuRows items={addItems} />
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuLabel>Tools</DropdownMenuLabel>
<MenuRows items={toolItems} />
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</ComposerToolbar>
<ComposerInput
aria-label="Message"
placeholder="Write a message..."
/>
<ComposerToolbar>
<ComposerModelPicker
model={model}
models={models}
onModelChange={setModel}
popupClassName={popupClassName}
effort={{
levels: effortLevels,
onValueChange: setEffort,
value: effort,
}}
/>
<VoiceButton
listening={listening}
onClick={() => setListening((current) => !current)}
/>
<ComposerSend />
</ComposerToolbar>
</ComposerShell>
<ComposerFooter>
<div className="flex min-w-0 items-center gap-1">
<Button size="xs" variant="text">
<RiGitBranchLine />
main
</Button>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button size="xs" variant="text">
<RiFolderLine />
{folder}
<RiArrowDownSLine className={chevronClass} />
</Button>
}
/>
<DropdownMenuContent
size="sm"
align="start"
className={cn("w-64", popupClassName)}
side="top"
sideOffset={8}
>
<DropdownMenuGroup>
<DropdownMenuLabel>Folders</DropdownMenuLabel>
{folders.map(({ name, path }) => (
<DropdownMenuItem
key={name}
onClick={() => setFolder(name)}
>
<RiFolderLine />
<span>
<span className="text-muted-foreground">{path}</span>
<span className="font-medium">{name}</span>
</span>
</DropdownMenuItem>
))}
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="flex min-w-0 items-center gap-1">
<Button size="xs" variant="text">
<RiInfinityLine />
Auto
<RiArrowDownSLine className={chevronClass} />
</Button>
<ContextMeter value={42} />
</div>
</ComposerFooter>
</Composer>
<p
aria-live="polite"
className={cn(
"px-1 text-sm text-muted-foreground",
!lastSent && "sr-only"
)}
>
{lastSent ? `Sent: ${lastSent}` : null}
</p>
</div>
);
} On this page