Installation
Complete project setup with react/base, then install these components:
pnpm exec frontfriend download message marker button bubble -f react --primitive base Restore keeps the checkpoint message. Continue adds a new turn after it. The marker remains visible, and the status text announces the result. In an application, restore the matching application state as well as the messages.
Usage
Complete example
import { Message, MessageContent } from "@/components/ui/message";
import { Bubble, BubbleContent } from "@/components/ui/bubble";
import { Marker, MarkerContent } from "@/components/ui/marker";
import { Button } from "@/components/ui/button";
import * as React from "react";
export default function AiCheckpointExample() {
const [messages, setMessages] = React.useState([
{ id: "user-1", role: "user", text: "Build the page layout." },
{ id: "assistant-1", role: "assistant", text: "The page layout is ready." },
{
id: "user-2",
role: "user",
text: "Add a footer with the project links.",
},
{
id: "assistant-2",
role: "assistant",
text: "I added the footer and its links.",
},
]);
const [status, setStatus] = React.useState("");
const continueRef = React.useRef<HTMLButtonElement>(null);
return (
<div
style={{
width: "100%",
maxWidth: 560,
minWidth: 0,
marginInline: "auto",
display: "flex",
flexDirection: "column",
gap: 20,
paddingBlock: 16,
fontSize: "var(--text-sm)",
lineHeight: 1.5,
}}
>
{messages.map((message) => (
<React.Fragment key={message.id}>
<Message align={message.role === "user" ? "end" : "start"}>
<MessageContent>
{message.role === "user" ? (
<Bubble align="end" variant="muted">
<BubbleContent>{message.text}</BubbleContent>
</Bubble>
) : (
<p>{message.text}</p>
)}
</MessageContent>
</Message>
{message.id === "assistant-1" && (
<Marker variant="separator">
<MarkerContent>
<span
style={{
display: "inline-flex",
gap: 8,
alignItems: "center",
flexWrap: "wrap",
}}
>
{"Layout saved"}
{messages.length > 2 && (
<Button
size="xs"
variant="outline"
aria-label={`Restore to ${"Layout saved"}`}
onClick={() => {
setMessages((previous) => previous.slice(0, 2));
setStatus("Restored. Later turns removed.");
continueRef.current?.focus();
}}
>
Restore
</Button>
)}
</span>
</MarkerContent>
</Marker>
)}
</React.Fragment>
))}
<div>
<Button
ref={continueRef}
variant="outline"
size="sm"
onClick={() => {
setMessages((previous) => [
...previous,
{
id: crypto.randomUUID(),
role: "user",
text: "Continue from this layout.",
},
{
id: crypto.randomUUID(),
role: "assistant",
text: "I will keep the saved layout and add the next change.",
},
]);
setStatus("Added a new exchange.");
}}
>
Continue
</Button>
</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 |
|---|---|
message | Base UI API |
marker | Base UI API |
button | Base UI API |
bubble | Base UI API |