Skip to content

Commit 3c34582

Browse files
committed
Resolves #182
1 parent 37ff591 commit 3c34582

File tree

4 files changed

+364
-0
lines changed

4 files changed

+364
-0
lines changed

.changeset/clean-windows-drive.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"ai-elements": minor
3+
---
4+
5+
Create new Checkpoint component
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
---
2+
title: Checkpoint
3+
description: A simple component for marking conversation history points and restoring the chat to a previous state.
4+
path: elements/components/checkpoint
5+
---
6+
7+
The `Checkpoint` component provides a way to mark specific points in a conversation history and restore the chat to that state. Inspired by VSCode's Copilot checkpoint feature, it allows users to revert to an earlier conversation state while maintaining a clear visual separation between different conversation segments.
8+
9+
<Preview path="checkpoint" />
10+
11+
## Installation
12+
13+
<ElementsInstaller path="checkpoint" />
14+
15+
## Features
16+
17+
- Simple flex layout with icon, trigger, and separator
18+
- Visual separator line for clear conversation breaks
19+
- Clickable restore button for reverting to checkpoint
20+
- Customizable icon (defaults to BookmarkIcon)
21+
- Keyboard accessible with proper ARIA labels
22+
- Responsive design that adapts to different screen sizes
23+
- Seamless light/dark theme integration
24+
25+
## Usage with AI SDK
26+
27+
Build a chat interface with conversation checkpoints that allow users to restore to previous states.
28+
29+
Add the following component to your frontend:
30+
31+
```tsx title="app/page.tsx"
32+
'use client';
33+
34+
import { useState, Fragment } from 'react';
35+
import { useChat } from '@ai-sdk/react';
36+
import {
37+
Checkpoint,
38+
CheckpointIcon,
39+
CheckpointTrigger,
40+
} from '@/components/ai-elements/checkpoint';
41+
import { Message, MessageContent, MessageResponse } from '@/components/ai-elements/message';
42+
import { Conversation, ConversationContent } from '@/components/ai-elements/conversation';
43+
44+
type CheckpointType = {
45+
id: string;
46+
messageIndex: number;
47+
timestamp: Date;
48+
messageCount: number;
49+
};
50+
51+
const CheckpointDemo = () => {
52+
const { messages, setMessages } = useChat();
53+
const [checkpoints, setCheckpoints] = useState<CheckpointType[]>([]);
54+
55+
const createCheckpoint = (messageIndex: number) => {
56+
const checkpoint: CheckpointType = {
57+
id: nanoid(),
58+
messageIndex,
59+
timestamp: new Date(),
60+
messageCount: messageIndex + 1,
61+
};
62+
setCheckpoints([...checkpoints, checkpoint]);
63+
};
64+
65+
const restoreToCheckpoint = (messageIndex: number) => {
66+
// Restore messages to checkpoint state
67+
setMessages(messages.slice(0, messageIndex + 1));
68+
// Remove checkpoints after this point
69+
setCheckpoints(checkpoints.filter(cp => cp.messageIndex <= messageIndex));
70+
};
71+
72+
return (
73+
<div className="max-w-4xl mx-auto p-6 relative size-full rounded-lg border h-[600px]">
74+
<Conversation>
75+
<ConversationContent>
76+
{messages.map((message, index) => {
77+
const checkpoint = checkpoints.find(cp => cp.messageIndex === index);
78+
79+
return (
80+
<Fragment key={message.id}>
81+
<Message from={message.role}>
82+
<MessageContent>
83+
<MessageResponse>{message.content}</MessageResponse>
84+
</MessageContent>
85+
</Message>
86+
{checkpoint && (
87+
<Checkpoint>
88+
<CheckpointIcon />
89+
<CheckpointTrigger
90+
onClick={() => restoreToCheckpoint(checkpoint.messageIndex)}
91+
>
92+
Restore checkpoint
93+
</CheckpointTrigger>
94+
</Checkpoint>
95+
)}
96+
</Fragment>
97+
);
98+
})}
99+
</ConversationContent>
100+
</Conversation>
101+
</div>
102+
);
103+
};
104+
105+
export default CheckpointDemo;
106+
```
107+
108+
## Use Cases
109+
110+
### Manual Checkpoints
111+
112+
Allow users to manually create checkpoints at important conversation points:
113+
114+
```tsx
115+
<Button onClick={() => createCheckpoint(messages.length - 1)}>
116+
Create Checkpoint
117+
</Button>
118+
```
119+
120+
### Automatic Checkpoints
121+
122+
Create checkpoints automatically after significant conversation milestones:
123+
124+
```tsx
125+
useEffect(() => {
126+
// Create checkpoint every 5 messages
127+
if (messages.length > 0 && messages.length % 5 === 0) {
128+
createCheckpoint(messages.length - 1);
129+
}
130+
}, [messages.length]);
131+
```
132+
133+
### Branching Conversations
134+
135+
Use checkpoints to enable conversation branching where users can explore different conversation paths:
136+
137+
```tsx
138+
const restoreAndBranch = (messageIndex: number) => {
139+
// Save current branch
140+
const currentBranch = messages.slice(messageIndex + 1);
141+
saveBranch(currentBranch);
142+
143+
// Restore to checkpoint
144+
restoreToCheckpoint(messageIndex);
145+
};
146+
```
147+
148+
## Props
149+
150+
### `<Checkpoint />`
151+
152+
<TypeTable
153+
type={{
154+
children: {
155+
description: 'The checkpoint icon and trigger components. Automatically includes a Separator at the end.',
156+
type: 'React.ReactNode',
157+
},
158+
'...props': {
159+
description: 'Any other props are spread to the root div.',
160+
type: 'React.HTMLAttributes<HTMLDivElement>',
161+
},
162+
}}
163+
/>
164+
165+
### `<CheckpointIcon />`
166+
167+
<TypeTable
168+
type={{
169+
children: {
170+
description: 'Custom icon content. If not provided, defaults to a BookmarkIcon from lucide-react.',
171+
type: 'React.ReactNode',
172+
},
173+
'...props': {
174+
description: 'Any other props are spread to the BookmarkIcon component.',
175+
type: 'LucideProps',
176+
},
177+
}}
178+
/>
179+
180+
### `<CheckpointTrigger />`
181+
182+
<TypeTable
183+
type={{
184+
children: {
185+
description: 'The text or content to display in the trigger button.',
186+
type: 'React.ReactNode',
187+
},
188+
variant: {
189+
description: 'The button variant style.',
190+
type: 'string',
191+
default: '"ghost"',
192+
},
193+
size: {
194+
description: 'The button size.',
195+
type: 'string',
196+
default: '"sm"',
197+
},
198+
'...props': {
199+
description: 'Any other props are spread to the underlying shadcn/ui Button component.',
200+
type: 'React.ComponentProps<typeof Button>',
201+
},
202+
}}
203+
/>
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"use client";
2+
3+
import { Button } from "@repo/shadcn-ui/components/ui/button";
4+
import { Separator } from "@repo/shadcn-ui/components/ui/separator";
5+
import {
6+
Tooltip,
7+
TooltipContent,
8+
TooltipTrigger,
9+
} from "@repo/shadcn-ui/components/ui/tooltip";
10+
import { cn } from "@repo/shadcn-ui/lib/utils";
11+
import { BookmarkIcon, type LucideProps } from "lucide-react";
12+
import type { ComponentProps, HTMLAttributes } from "react";
13+
14+
export type CheckpointProps = HTMLAttributes<HTMLDivElement>;
15+
16+
export const Checkpoint = ({
17+
className,
18+
children,
19+
...props
20+
}: CheckpointProps) => (
21+
<div
22+
className={cn("flex items-center gap-0.5 text-muted-foreground", className)}
23+
{...props}
24+
>
25+
{children}
26+
<Separator />
27+
</div>
28+
);
29+
30+
export type CheckpointIconProps = LucideProps;
31+
32+
export const CheckpointIcon = ({
33+
className,
34+
children,
35+
...props
36+
}: CheckpointIconProps) =>
37+
children ?? (
38+
<BookmarkIcon className={cn("size-4 shrink-0", className)} {...props} />
39+
);
40+
41+
export type CheckpointTriggerProps = ComponentProps<typeof Button> & {
42+
tooltip?: string;
43+
};
44+
45+
export const CheckpointTrigger = ({
46+
children,
47+
className,
48+
variant = "ghost",
49+
size = "sm",
50+
tooltip,
51+
...props
52+
}: CheckpointTriggerProps) =>
53+
tooltip ? (
54+
<Tooltip>
55+
<TooltipTrigger asChild>
56+
<Button size={size} type="button" variant={variant} {...props}>
57+
{children}
58+
</Button>
59+
</TooltipTrigger>
60+
<TooltipContent align="start" side="bottom">
61+
{tooltip}
62+
</TooltipContent>
63+
</Tooltip>
64+
) : (
65+
<Button size={size} type="button" variant={variant} {...props}>
66+
{children}
67+
</Button>
68+
);
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
"use client";
2+
3+
import {
4+
Checkpoint,
5+
CheckpointIcon,
6+
CheckpointTrigger,
7+
} from "@repo/elements/checkpoint";
8+
import { Conversation, ConversationContent } from "@repo/elements/conversation";
9+
import {
10+
Message,
11+
MessageContent,
12+
MessageResponse,
13+
} from "@repo/elements/message";
14+
import { nanoid } from "nanoid";
15+
import { Fragment, useState } from "react";
16+
17+
type MessageType = {
18+
id: string;
19+
role: "user" | "assistant";
20+
content: string;
21+
};
22+
23+
const initialMessages: MessageType[] = [
24+
{
25+
id: nanoid(),
26+
role: "user",
27+
content: "What is React?",
28+
},
29+
{
30+
id: nanoid(),
31+
role: "assistant",
32+
content:
33+
"React is a JavaScript library for building user interfaces. It was developed by Facebook and is now maintained by Meta and a community of developers.",
34+
},
35+
{
36+
id: nanoid(),
37+
role: "user",
38+
content: "How does component state work?",
39+
},
40+
];
41+
42+
const Example = () => {
43+
const [messages, setMessages] = useState<MessageType[]>(initialMessages);
44+
const [checkpoints] = useState([
45+
{ messageCount: 2, timestamp: new Date(Date.now() - 3_600_000) },
46+
]);
47+
48+
const handleRestore = (messageCount: number) => {
49+
setMessages(initialMessages.slice(0, messageCount));
50+
};
51+
52+
return (
53+
<div className="flex size-full flex-col rounded-lg border p-6">
54+
<Conversation>
55+
<ConversationContent>
56+
{messages.map((message, index) => {
57+
const checkpoint = checkpoints.find(
58+
(cp) => cp.messageCount === index + 1
59+
);
60+
61+
return (
62+
<Fragment key={message.id}>
63+
<Message from={message.role}>
64+
<MessageContent>
65+
<MessageResponse>{message.content}</MessageResponse>
66+
</MessageContent>
67+
</Message>
68+
{checkpoint && (
69+
<Checkpoint>
70+
<CheckpointIcon />
71+
<CheckpointTrigger
72+
onClick={() => handleRestore(checkpoint.messageCount)}
73+
tooltip="Restores workspace and chat to this point"
74+
>
75+
Restore checkpoint
76+
</CheckpointTrigger>
77+
</Checkpoint>
78+
)}
79+
</Fragment>
80+
);
81+
})}
82+
</ConversationContent>
83+
</Conversation>
84+
</div>
85+
);
86+
};
87+
88+
export default Example;

0 commit comments

Comments
 (0)