Installation
Complete project setup with react/radix, then install these components:
pnpm exec frontfriend download button message bubble input-group icon -f react --primitive radix The row scrolls horizontally when its buttons do not fit. Buttons stay in the tab order, and focus brings each one into view. Connect the selected prompt to your send action.
Usage
Complete example
import { Message, MessageContent } from "@/components/ui/message";
import { Bubble, BubbleContent } from "@/components/ui/bubble";
import { Button } from "@/components/ui/button";
import {
InputGroup,
InputGroupTextarea,
InputGroupAddon,
InputGroupButton,
} from "@/components/ui/input-group";
import { Icon } from "@/components/ui/icon";
import * as React from "react";
export default function AiSuggestionExample() {
const [prompt, setPrompt] = React.useState("");
const [text, setText] = React.useState("");
const [status, setStatus] = React.useState("");
const fileInput = React.useRef<HTMLInputElement>(null);
const suggestions = [
"Add a dark mode toggle",
"Check the form",
"Explain this change",
"Write a test",
];
function send(value: string) {
if (false || !value.trim()) return;
setPrompt(value.trim());
setText("");
setStatus("Reply added.");
}
return (
<div
style={{
width: "100%",
maxWidth: 560,
minWidth: 0,
marginInline: "auto",
display: "flex",
flexDirection: "column",
gap: 24,
paddingBlock: 16,
fontSize: "var(--text-sm)",
lineHeight: 1.5,
}}
>
{prompt && (
<div style={{ display: "grid", gap: 16 }}>
<Message align="end">
<MessageContent>
<Bubble align="end" variant="muted">
<BubbleContent>{prompt}</BubbleContent>
</Bubble>
</MessageContent>
</Message>
<Message>
<MessageContent>
<p>
I will help you with “{prompt}”. Start with a small change, then
check the result.
</p>
</MessageContent>
</Message>
</div>
)}
<div style={{ display: "grid", gap: 8 }}>
<div
role="group"
aria-label="Suggested prompts"
style={{
display: "flex",
gap: 8,
overflowX: "auto",
paddingBlock: 4,
}}
>
{suggestions.map((suggestion) => (
<span key={suggestion} style={{ flexShrink: 0 }}>
<Button
variant="outline"
size="sm"
disabled={false}
onClick={() => send(suggestion)}
>
{suggestion}
</Button>
</span>
))}
</div>
<form
onSubmit={(event) => {
event.preventDefault();
send(text);
}}
>
<input
ref={fileInput}
type="file"
multiple
hidden
aria-label="Attach files"
onChange={(event) => {
const count = event.target.files?.length ?? 0;
setStatus(`${count} ${count === 1 ? "file" : "files"} attached.`);
event.target.value = "";
}}
/>
<InputGroup>
<InputGroupTextarea
aria-label="Prompt"
placeholder="Ask the agent to change your code..."
value={text}
disabled={false}
onChange={(event) => setText(event.target.value)}
onKeyDown={(event) => {
if (
event.key === "Enter" &&
!event.shiftKey &&
!event.nativeEvent.isComposing
) {
event.preventDefault();
send(text);
}
}}
/>
<InputGroupAddon align="block-end">
<InputGroupButton
type="button"
size="icon-sm"
variant="outline"
aria-label="Attach files"
disabled={false}
onClick={() => fileInput.current?.click()}
>
<Icon name="Plus" />
</InputGroupButton>
<span style={{ flex: 1 }} />
<InputGroupButton
type="submit"
size="icon-sm"
variant={
text.trim() && !false
? "default"
: "secondary"
}
aria-label="Send"
aria-disabled={false || !text.trim()}
>
<Icon name="ArrowUp" />
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</form>
</div>
<p
role="status"
style={{
position: "absolute",
width: 1,
height: 1,
padding: 0,
margin: -1,
overflow: "hidden",
clipPath: "inset(50%)",
whiteSpace: "nowrap",
border: 0,
}}
>
{status}
</p>
</div>
);
} Components and API
This composition has no separate component API. Use the APIs for its installed components.
| Component | Target reference |
|---|---|
button | Radix UI API |
message | Radix UI API |
bubble | Radix UI API |
input-group | Radix UI API |
icon | Radix UI API |