Spaces:
Runtime error
Runtime error
File size: 5,681 Bytes
7450ebd f36471e 7450ebd f36471e 7450ebd f36471e 7450ebd f36471e 7450ebd f36471e 7450ebd f36471e 7450ebd f36471e 7450ebd f36471e 7450ebd 28faefd 7450ebd 28faefd 7450ebd f36471e 7450ebd 28faefd 7450ebd f36471e 7450ebd 28faefd 7450ebd f36471e |
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 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 |
<script lang="ts">
import { clickOutside } from "$lib/actions/click-outside.js";
import { checkpoints } from "$lib/state/checkpoints.svelte";
import { session } from "$lib/state/session.svelte.js";
import { iterate } from "$lib/utils/array.js";
import { Popover } from "melt/builders";
import { Tooltip } from "melt/components";
import { fly } from "svelte/transition";
import IconCompare from "~icons/carbon/compare";
import IconHistory from "~icons/carbon/recently-viewed";
import IconStar from "~icons/carbon/star";
import IconStarFilled from "~icons/carbon/star-filled";
import IconDelete from "~icons/carbon/trash-can";
const popover = new Popover({
floatingConfig: {
offset: { crossAxis: -12 },
},
onOpenChange: open => {
if (open) dialog?.showModal();
else dialog?.close();
},
});
let dialog = $state<HTMLDialogElement>();
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>
<dialog
bind:this={dialog}
class="mb-2 !overflow-visible rounded-xl border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-800"
use:clickOutside={() => (popover.open = false)}
{...popover.content}
>
<div
class="size-4 translate-x-3 rounded-tl border-t border-l border-gray-200 dark:border-gray-700"
{...popover.arrow}
></div>
<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-200 dark: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 iterate(sliced) as [msg, isLast]}
<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>
</dialog>
|