Skip to content

Commit d0878dd

Browse files
authored
Merge pull request #23 from peoray/feature/checkpoint
feat: add checkpoint component
2 parents cc86aaa + 18871dd commit d0878dd

File tree

9 files changed

+493
-0
lines changed

9 files changed

+493
-0
lines changed
Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
---
2+
title: Checkpoint
3+
description: A simple component for marking conversation history points and restoring the chat to a previous state.
4+
icon: lucide:flag
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+
:::ComponentLoader{label="Preview" componentName="Checkpoint"}
10+
:::
11+
12+
## Install using CLI
13+
14+
::tabs{variant="card"}
15+
::div{label="ai-elements-vue"}
16+
```sh
17+
npx ai-elements-vue@latest add checkpoint
18+
```
19+
::
20+
::div{label="shadcn-vue"}
21+
22+
```sh
23+
npx shadcn-vue@latest add https://registry.ai-elements-vue.com/checkpoint.json
24+
```
25+
::
26+
::
27+
28+
## Install Manually
29+
30+
Copy and paste the following code in the same folder.
31+
32+
:::code-group
33+
```vue [Checkpoint.vue]
34+
<script lang="ts" setup>
35+
import type { HTMLAttributes } from 'vue'
36+
import { Separator } from '@repo/shadcn-vue/components/ui/separator'
37+
import { cn } from '@repo/shadcn-vue/lib/utils'
38+
39+
const props = defineProps<{
40+
class?: HTMLAttributes['class']
41+
}>()
42+
</script>
43+
44+
<template>
45+
<div
46+
:class="cn('flex items-center gap-0.5 text-muted-foreground', props.class)"
47+
v-bind="$attrs"
48+
>
49+
<slot />
50+
<Separator />
51+
</div>
52+
</template>
53+
```
54+
55+
```vue [CheckpointIcon.vue]
56+
<script lang="ts" setup>
57+
import type { HTMLAttributes } from 'vue'
58+
import { cn } from '@repo/shadcn-vue/lib/utils'
59+
import { BookmarkIcon } from 'lucide-vue-next'
60+
import { useSlots } from 'vue'
61+
62+
const props = defineProps<{
63+
class?: HTMLAttributes['class']
64+
}>()
65+
66+
const slots = useSlots()
67+
</script>
68+
69+
<template>
70+
<slot v-if="slots.default" />
71+
72+
<BookmarkIcon
73+
v-else
74+
:class="cn('size-4 shrink-0', props.class)"
75+
v-bind="$attrs"
76+
/>
77+
</template>
78+
```
79+
80+
```vue [CheckpointTrigger.vue]
81+
<script lang="ts" setup>
82+
import type { ButtonVariants } from '@repo/shadcn-vue/components/ui/button'
83+
import { Button } from '@repo/shadcn-vue/components/ui/button'
84+
import {
85+
Tooltip,
86+
TooltipContent,
87+
TooltipTrigger,
88+
} from '@repo/shadcn-vue/components/ui/tooltip'
89+
90+
interface Props {
91+
tooltip?: string
92+
variant?: ButtonVariants['variant']
93+
size?: ButtonVariants['size']
94+
}
95+
96+
const props = withDefaults(defineProps<Props>(), {
97+
variant: 'ghost',
98+
size: 'sm',
99+
})
100+
101+
const buttonProps = {
102+
variant: props.variant,
103+
size: props.size,
104+
type: 'button' as const,
105+
}
106+
</script>
107+
108+
<template>
109+
<Tooltip v-if="props.tooltip">
110+
<TooltipTrigger as-child>
111+
<Button v-bind="{ ...buttonProps, ...$attrs }">
112+
<slot />
113+
</Button>
114+
</TooltipTrigger>
115+
<TooltipContent align="start" side="bottom">
116+
<p>{{ props.tooltip }}</p>
117+
</TooltipContent>
118+
</Tooltip>
119+
120+
<Button v-else v-bind="{ ...buttonProps, ...$attrs }">
121+
<slot />
122+
</Button>
123+
</template>
124+
```
125+
126+
```ts [index.ts]
127+
export { default as Checkpoint } from './Checkpoint.vue'
128+
export { default as CheckpointIcon } from './CheckpointIcon.vue'
129+
export { default as CheckpointTrigger } from './CheckpointTrigger.vue'
130+
```
131+
:::
132+
133+
## Features
134+
135+
- Simple flex layout with icon, trigger, and separator
136+
- Visual separator line for clear conversation breaks
137+
- Clickable restore button for reverting to checkpoint
138+
- Customizable icon (defaults to BookmarkIcon)
139+
- Keyboard accessible with proper ARIA labels
140+
- Responsive design that adapts to different screen sizes
141+
- Seamless light/dark theme integration
142+
143+
## Usage with AI SDK
144+
145+
Build a chat interface with conversation checkpoints that allow users to restore to previous states.
146+
147+
Add the following component to your frontend:
148+
149+
```vue [pages/index.vue]
150+
<script setup lang="ts">
151+
import { useChat } from '@ai-sdk/vue'
152+
import { nanoid } from 'nanoid'
153+
import { computed, ref } from 'vue'
154+
import {
155+
Checkpoint,
156+
CheckpointIcon,
157+
CheckpointTrigger,
158+
} from '@/components/ai-elements/checkpoint'
159+
import {
160+
Conversation,
161+
ConversationContent,
162+
} from '@/components/ai-elements/conversation'
163+
import {
164+
Message,
165+
MessageContent,
166+
MessageResponse,
167+
} from '@/components/ai-elements/message'
168+
169+
interface CheckpointType {
170+
id: string
171+
messageIndex: number
172+
timestamp: Date
173+
messageCount: number
174+
}
175+
176+
const { messages, setMessages } = useChat()
177+
const checkpoints = ref<CheckpointType[]>([])
178+
179+
const messagesWithCheckpoints = computed(() => {
180+
return messages.value.map((message, index) => {
181+
const checkpoint = checkpoints.value.find(
182+
cp => cp.messageIndex === index
183+
)
184+
return { message, index, checkpoint }
185+
})
186+
})
187+
188+
function createCheckpoint(messageIndex: number) {
189+
const checkpoint: CheckpointType = {
190+
id: nanoid(),
191+
messageIndex,
192+
timestamp: new Date(),
193+
messageCount: messageIndex + 1,
194+
}
195+
checkpoints.value.push(checkpoint)
196+
}
197+
198+
function restoreToCheckpoint(messageIndex: number) {
199+
// Restore messages to checkpoint state (assuming setMessages API is the same)
200+
setMessages(messages.value.slice(0, messageIndex + 1))
201+
// Remove checkpoints after this point
202+
checkpoints.value = checkpoints.value.filter(
203+
cp => cp.messageIndex <= messageIndex
204+
)
205+
}
206+
</script>
207+
208+
<template>
209+
<div
210+
class="max-w-4xl mx-auto p-6 relative size-full rounded-lg border h-[600px]"
211+
>
212+
<Conversation>
213+
<ConversationContent>
214+
<template
215+
v-for="{ message, checkpoint } in messagesWithCheckpoints"
216+
:key="message.id"
217+
>
218+
<Message :from="message.role">
219+
<MessageContent>
220+
<MessageResponse>{{ message.content }}</MessageResponse>
221+
</MessageContent>
222+
</Message>
223+
224+
<Checkpoint v-if="checkpoint">
225+
<CheckpointIcon />
226+
<CheckpointTrigger
227+
@click="restoreToCheckpoint(checkpoint.messageIndex)"
228+
>
229+
Restore checkpoint
230+
</CheckpointTrigger>
231+
</Checkpoint>
232+
</template>
233+
</ConversationContent>
234+
</Conversation>
235+
</div>
236+
</template>
237+
```
238+
239+
## Use Cases
240+
241+
### Manual Checkpoints
242+
243+
Allow users to manually create checkpoints at important conversation points:
244+
245+
```vue
246+
<Button @click="createCheckpoint(messages.length - 1)">
247+
Create Checkpoint
248+
</Button>
249+
```
250+
251+
### Automatic Checkpoints
252+
253+
Create checkpoints automatically after significant conversation milestones:
254+
255+
```tsx
256+
watch(
257+
() => messages.value.length,
258+
(length) => {
259+
// Create checkpoint every 5 messages
260+
if (length > 0 && length % 5 === 0) {
261+
createCheckpoint(length - 1)
262+
}
263+
}
264+
)
265+
```
266+
267+
### Branching Conversations
268+
269+
Use checkpoints to enable conversation branching where users can explore different conversation paths:
270+
271+
```tsx
272+
function restoreAndBranch(messageIndex: number) {
273+
// Save current branch
274+
const currentBranch = messages.value.slice(messageIndex + 1)
275+
saveBranch(currentBranch)
276+
277+
// Restore to checkpoint
278+
restoreToCheckpoint(messageIndex)
279+
}
280+
```
281+
282+
## Props
283+
284+
### `<Checkpoint />`
285+
286+
:::field-group
287+
::field{name="class" type="string" defaultValue="''"}
288+
The class name to apply to the component.
289+
::
290+
:::
291+
292+
### `<CheckpointIcon />`
293+
294+
:::field-group
295+
::field{name="class" type="string" defaultValue="''"}
296+
The class name to apply to the component.
297+
::
298+
:::
299+
300+
### `<CheckpointTrigger />`
301+
302+
:::field-group
303+
::field{name="tooltip" type="string" defaultValue="''"}
304+
The tooltip text to display when the trigger is hovered.
305+
::
306+
::field{name="variant" type="string" defaultValue="'ghost'"}
307+
The variant of the button (e.g., 'ghost', 'outline', 'solid').
308+
::
309+
::field{name="size" type="string" defaultValue="'sm'"}
310+
The size of the button (e.g., 'sm', 'md', 'lg').
311+
::
312+
:::

apps/www/plugins/ai-elements.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
ActionsHover,
44
Branch,
55
ChainOfThought,
6+
Checkpoint,
67
CodeBlock,
78
CodeBlockDark,
89
Conversation,
@@ -68,4 +69,5 @@ export default defineNuxtPlugin((nuxtApp) => {
6869
vueApp.component('InlineCitation', InlineCitation)
6970
vueApp.component('CodeBlock', CodeBlock)
7071
vueApp.component('CodeBlockDark', CodeBlockDark)
72+
vueApp.component('Checkpoint', Checkpoint)
7173
})
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<script lang="ts" setup>
2+
import type { HTMLAttributes } from 'vue'
3+
import { Separator } from '@repo/shadcn-vue/components/ui/separator'
4+
import { cn } from '@repo/shadcn-vue/lib/utils'
5+
6+
const props = defineProps<{
7+
class?: HTMLAttributes['class']
8+
}>()
9+
</script>
10+
11+
<template>
12+
<div
13+
:class="cn('flex items-center gap-0.5 text-muted-foreground overflow-hidden', props.class)"
14+
v-bind="$attrs"
15+
>
16+
<slot />
17+
<Separator />
18+
</div>
19+
</template>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<script lang="ts" setup>
2+
import type { HTMLAttributes } from 'vue'
3+
import { cn } from '@repo/shadcn-vue/lib/utils'
4+
import { BookmarkIcon } from 'lucide-vue-next'
5+
import { useSlots } from 'vue'
6+
7+
const props = defineProps<{
8+
class?: HTMLAttributes['class']
9+
}>()
10+
11+
const slots = useSlots()
12+
</script>
13+
14+
<template>
15+
<slot v-if="slots.default" />
16+
17+
<BookmarkIcon
18+
v-else
19+
:class="cn('size-4 shrink-0', props.class)"
20+
v-bind="$attrs"
21+
/>
22+
</template>

0 commit comments

Comments
 (0)