File size: 5,218 Bytes
7450ebd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28faefd
 
7450ebd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28faefd
 
 
7450ebd
 
 
 
 
28faefd
7450ebd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28faefd
 
 
 
 
7450ebd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
<script lang="ts">
	import { checkpoints } from "$lib/state/checkpoints.svelte";
	import { session } from "$lib/state/session.svelte.js";
	import { Popover } from "melt/builders";
	import { Tooltip } from "melt/components";
	import { fly } from "svelte/transition";
	import IconHistory from "~icons/carbon/recently-viewed";
	import IconDelete from "~icons/carbon/trash-can";
	import IconStar from "~icons/carbon/star";
	import IconStarFilled from "~icons/carbon/star-filled";
	import IconCompare from "~icons/carbon/compare";

	const popover = new Popover({
		floatingConfig: {
			offset: { crossAxis: -12 },
		},
	});

	const projCheckpoints = $derived(checkpoints.for(session.project.id));
</script>

<button class="btn relative size-[32px] p-0" {...popover.trigger}>
	<IconHistory />
	{#if projCheckpoints.length > 0}
		<div class="absolute -top-1 -right-1 size-2.5 rounded-full bg-amber-500" aria-label="Project has checkpoints"></div>
	{/if}
</button>

<div
	class="mb-2 overflow-hidden rounded-xl border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-800"
	{...popover.content}
>
	<div class="max-h-120 w-80 overflow-x-clip overflow-y-auto p-3 pb-1">
		<div class="mb-2 flex items-center justify-between px-1">
			<h3 class="text-sm font-medium dark:text-white">Checkpoints</h3>
			<button
				class="rounded-lg bg-blue-600 px-2 py-1 text-xs font-medium text-white transition-colors hover:bg-blue-700"
				onclick={() => checkpoints.commit(session.project.id)}
			>
				Create new
			</button>
		</div>

		{#each projCheckpoints as checkpoint (checkpoint.id)}
			{@const state = checkpoint.projectState}
			{@const multiple = state.conversations.length > 1}
			<Tooltip
				openDelay={0}
				floatingConfig={{
					computePosition: {
						placement: "right",
					},
					offset: {
						mainAxis: 16,
					},
				}}
				forceVisible
			>
				{#snippet children(tooltip)}
					<div
						class="mb-2 flex w-full items-center rounded-md px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700"
						{...tooltip.trigger}
					>
						<button
							class="flex flex-1 flex-col text-left text-sm transition-colors"
							onclick={() => checkpoints.restore(session.project.id, checkpoint)}
						>
							<span class="font-medium text-gray-400">{checkpoint.timestamp}</span>

							<p class="mt-0.5 flex items-center gap-2 text-sm">
								{#if multiple}
									<IconCompare class="text-xs text-gray-400" />
								{/if}
								{#each state.conversations as { messages }, i}
									<span class={["text-gray-800 dark:text-gray-200"]}>
										{messages.length} message{messages.length === 1 ? "" : "s"}
									</span>
									{#if multiple && i === 0}
										<span class="text-gray-500">|</span>
									{/if}
								{/each}
							</p>
						</button>

						<button
							class="mr-0.5 grid place-items-center rounded-md p-1 text-xs hover:bg-gray-300 dark:hover:bg-gray-600"
							onclick={e => {
								e.stopPropagation();
								checkpoints.toggleFavorite(session.project.id, checkpoint);
							}}
						>
							{#if checkpoint.favorite}
								<IconStarFilled class="text-yellow-500" />
							{:else}
								<IconStar />
							{/if}
						</button>
						<button
							class="grid place-items-center rounded-md p-1 text-xs hover:bg-gray-300 dark:hover:bg-gray-600"
							onclick={e => {
								e.stopPropagation();
								checkpoints.delete(session.project.id, checkpoint);
							}}
						>
							<IconDelete />
						</button>
					</div>

					{#if tooltip.open}
						<div
							class={[
								"flex rounded-xl border border-gray-100 bg-gray-50 p-2 shadow dark:border-gray-700 dark:bg-gray-800",
							]}
							{...tooltip.content}
							transition:fly={{ x: -2 }}
						>
							<div class="size-4 rounded-tl border-t border-l border-gray-700" {...tooltip.arrow}></div>
							{#each state.conversations as conversation, i}
								{@const msgs = conversation.messages}
								{@const sliced = msgs.slice(0, 4)}
								<div
									class={[
										"p-2",
										multiple ? "w-52" : "w-72",
										i === 0 && multiple && "border-r border-gray-200 dark:border-gray-700",
									]}
								>
									<p class="text-2xs pl-1.5 font-mono font-medium text-gray-500 uppercase">
										temp: {conversation.config.temperature}
										| max tokens: {conversation.config.max_tokens}
									</p>
									{#each sliced as msg, i}
										{@const isLast = i === sliced.length - 1}
										<div class="flex flex-col gap-1 p-2">
											<p class="font-mono text-xs font-medium text-gray-400 uppercase">{msg.role}</p>
											{#if msg.content?.trim()}
												<p class="line-clamp-2 text-sm">{msg.content.trim()}</p>
											{:else}
												<p class="text-sm text-gray-500 italic">No content</p>
											{/if}
										</div>
										{#if !isLast}
											<div class="my-2 h-px w-full bg-gray-200 dark:bg-gray-700"></div>
										{/if}
									{/each}
								</div>
							{/each}
						</div>
					{/if}
				{/snippet}
			</Tooltip>
		{:else}
			<div class="flex flex-col items-center gap-2 py-3">
				<span class="text-gray-500 text-sm">No checkpoints available</span>
			</div>
		{/each}
	</div>
</div>