# Checkpoint

Marks a point in the conversation you can return to.

## When to use

* Mark a point in the conversation the user can go back to, such as before a large edit.
* Offer a restore action next to a past state of the work.

## How it works

Checkpoint is built with the [Marker](/components/marker) and [Button](/components/button) components. The demo streams a predefined conversation through `useChat` from `@ai-sdk/react`, with `createChat` from [`@shadcn/helpers/ai-sdk`](/helpers/ai-sdk), so it needs no model or API key. There is no package to install: copy the code into your project.

## Example

### Base UI

```tsx
'use client'

import * as React from "react"
import { useChat } from "@ai-sdk/react"
import type { UIMessage } from "ai"

import { createChat, getMessageText } from "@/lib/ai"
import { Bubble, BubbleContent } from "@/shadcn/base/ui/bubble"
import { Button } from "@/shadcn/base/ui/button"
import { Marker, MarkerContent } from "@/shadcn/base/ui/marker"
import { Message, MessageContent } from "@/shadcn/base/ui/message"

type ChatMessage = UIMessage<
  unknown,
  {
    checkpoint: {
      label: string
    }
  }
>

type CheckpointPart = Extract<
  ChatMessage["parts"][number],
  { type: "data-checkpoint" }
>

const chat = createChat<ChatMessage>()
  .user("Rename the button component to action.")
  .assistant(({ writer }) => {
    writer.text("Renamed Button to Action in components/ui/button.tsx.")
    writer.data({
      type: "data-checkpoint",
      id: "rename",
      data: { label: "Button renamed" },
    })
  })
  .user("Update the docs to match.")
  .assistant(({ writer }) => {
    writer.text("Updated the Button page to say Action.")
  })

const transcript = chat.get()
const transport = chat.transport({ delayMs: 20 })

export default function CheckpointDemo() {
  const { messages, sendMessage, setMessages, status } = useChat({
    messages: transcript,
    transport,
  })
  const nextMessage = chat.next(messages)
  const busy = status === "submitted" || status === "streaming"

  function restore(messageId: string) {
    const index = messages.findIndex((message) => message.id === messageId)

    if (index === -1) {
      return
    }

    setMessages(messages.slice(0, index + 1))
  }

  return (
    <div className="flex min-h-72 w-full max-w-sm flex-col gap-4 py-6 text-sm">
      {messages.map((message) => {
        const checkpoint = message.parts.find(
          (part): part is CheckpointPart => part.type === "data-checkpoint"
        )
        const index = messages.findIndex((item) => item.id === message.id)
        const canRestore = index < messages.length - 1

        return (
          <React.Fragment key={message.id}>
            {message.role === "user" ? (
              <Message align="end">
                <MessageContent>
                  <Bubble align="end" variant="muted">
                    <BubbleContent>{getMessageText(message)}</BubbleContent>
                  </Bubble>
                </MessageContent>
              </Message>
            ) : (
              <Message>
                <MessageContent>
                  <p>{getMessageText(message)}</p>
                </MessageContent>
              </Message>
            )}
            {checkpoint && (
              <Marker variant="separator">
                <MarkerContent className="inline-flex items-center gap-2">
                  {checkpoint.data.label}
                  {canRestore && (
                    <Button
                      size="xs"
                      variant="outline"
                      aria-label={`Restore to ${checkpoint.data.label}`}
                      onClick={() => restore(message.id)}
                    >
                      Restore
                    </Button>
                  )}
                </MarkerContent>
              </Marker>
            )}
          </React.Fragment>
        )
      })}
      {!busy && nextMessage && (
        <Button
          variant="outline"
          size="sm"
          className="mt-auto self-start"
          onClick={() => sendMessage(nextMessage)}
        >
          Continue
        </Button>
      )}
    </div>
  )
}
```

### Radix

```tsx
'use client'

import * as React from "react"
import { useChat } from "@ai-sdk/react"
import type { UIMessage } from "ai"

import { createChat, getMessageText } from "@/lib/ai"
import { Bubble, BubbleContent } from "@/shadcn/radix/ui/bubble"
import { Button } from "@/shadcn/radix/ui/button"
import { Marker, MarkerContent } from "@/shadcn/radix/ui/marker"
import { Message, MessageContent } from "@/shadcn/radix/ui/message"

type ChatMessage = UIMessage<
  unknown,
  {
    checkpoint: {
      label: string
    }
  }
>

type CheckpointPart = Extract<
  ChatMessage["parts"][number],
  { type: "data-checkpoint" }
>

const chat = createChat<ChatMessage>()
  .user("Rename the button component to action.")
  .assistant(({ writer }) => {
    writer.text("Renamed Button to Action in components/ui/button.tsx.")
    writer.data({
      type: "data-checkpoint",
      id: "rename",
      data: { label: "Button renamed" },
    })
  })
  .user("Update the docs to match.")
  .assistant(({ writer }) => {
    writer.text("Updated the Button page to say Action.")
  })

const transcript = chat.get()
const transport = chat.transport({ delayMs: 20 })

export default function CheckpointDemo() {
  const { messages, sendMessage, setMessages, status } = useChat({
    messages: transcript,
    transport,
  })
  const nextMessage = chat.next(messages)
  const busy = status === "submitted" || status === "streaming"

  function restore(messageId: string) {
    const index = messages.findIndex((message) => message.id === messageId)

    if (index === -1) {
      return
    }

    setMessages(messages.slice(0, index + 1))
  }

  return (
    <div className="flex min-h-72 w-full max-w-sm flex-col gap-4 py-6 text-sm">
      {messages.map((message) => {
        const checkpoint = message.parts.find(
          (part): part is CheckpointPart => part.type === "data-checkpoint"
        )
        const index = messages.findIndex((item) => item.id === message.id)
        const canRestore = index < messages.length - 1

        return (
          <React.Fragment key={message.id}>
            {message.role === "user" ? (
              <Message align="end">
                <MessageContent>
                  <Bubble align="end" variant="muted">
                    <BubbleContent>{getMessageText(message)}</BubbleContent>
                  </Bubble>
                </MessageContent>
              </Message>
            ) : (
              <Message>
                <MessageContent>
                  <p>{getMessageText(message)}</p>
                </MessageContent>
              </Message>
            )}
            {checkpoint && (
              <Marker variant="separator">
                <MarkerContent className="inline-flex items-center gap-2">
                  {checkpoint.data.label}
                  {canRestore && (
                    <Button
                      size="xs"
                      variant="outline"
                      aria-label={`Restore to ${checkpoint.data.label}`}
                      onClick={() => restore(message.id)}
                    >
                      Restore
                    </Button>
                  )}
                </MarkerContent>
              </Marker>
            )}
          </React.Fragment>
        )
      })}
      {!busy && nextMessage && (
        <Button
          variant="outline"
          size="sm"
          className="mt-auto self-start"
          onClick={() => sendMessage(nextMessage)}
        >
          Continue
        </Button>
      )}
    </div>
  )
}
```

## Installation

```bash
npx shadcn@latest add marker button
```

## Reference

Official page: https://ui.shadcn.com/docs/ai-elements/base/checkpoint
