Checkpoints (#73)
Browse filesCo-authored-by: Victor Muštar (aider) <[email protected]>
- package.json +1 -1
- pnpm-lock.yaml +16 -9
- src/lib/components/inference-playground/checkpoints-menu.svelte +152 -0
- src/lib/components/inference-playground/hf-token-modal.svelte +1 -1
- src/lib/components/inference-playground/project-select.svelte +16 -1
- src/lib/components/toaster.svelte +0 -1
- src/lib/state/checkpoints.svelte.ts +80 -0
- src/lib/state/session.svelte.ts +4 -0
- src/lib/utils/string.ts +6 -0
package.json
CHANGED
@@ -37,7 +37,7 @@
|
|
37 |
"globals": "^16.0.0",
|
38 |
"highlight.js": "^11.10.0",
|
39 |
"jiti": "^2.4.2",
|
40 |
-
"melt": "^0.
|
41 |
"openai": "^4.90.0",
|
42 |
"postcss": "^8.4.38",
|
43 |
"prettier": "^3.1.1",
|
|
|
37 |
"globals": "^16.0.0",
|
38 |
"highlight.js": "^11.10.0",
|
39 |
"jiti": "^2.4.2",
|
40 |
+
"melt": "^0.29.2",
|
41 |
"openai": "^4.90.0",
|
42 |
"postcss": "^8.4.38",
|
43 |
"prettier": "^3.1.1",
|
pnpm-lock.yaml
CHANGED
@@ -85,8 +85,8 @@ importers:
|
|
85 |
specifier: ^2.4.2
|
86 |
version: 2.4.2
|
87 |
melt:
|
88 |
-
specifier: ^0.
|
89 |
-
version: 0.
|
90 |
openai:
|
91 |
specifier: ^4.90.0
|
92 |
version: 4.90.0
|
@@ -1098,6 +1098,10 @@ packages:
|
|
1098 |
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
1099 |
engines: {node: '>=0.4.0'}
|
1100 |
|
|
|
|
|
|
|
|
|
1101 | |
1102 |
resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==}
|
1103 |
engines: {node: '>=0.10'}
|
@@ -1642,8 +1646,8 @@ packages:
|
|
1642 |
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
1643 |
engines: {node: '>= 0.4'}
|
1644 |
|
1645 |
-
melt@0.
|
1646 |
-
resolution: {integrity: sha512-
|
1647 |
peerDependencies:
|
1648 |
'@floating-ui/dom': ^1.6.0
|
1649 |
svelte: ^5.0.0
|
@@ -1700,8 +1704,8 @@ packages:
|
|
1700 |
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
1701 |
hasBin: true
|
1702 |
|
1703 |
-
[email protected].
|
1704 |
-
resolution: {integrity: sha512-
|
1705 |
engines: {node: ^18 || >=20}
|
1706 |
hasBin: true
|
1707 |
|
@@ -3153,6 +3157,8 @@ snapshots:
|
|
3153 |
|
3154 | |
3155 |
|
|
|
|
|
3156 | |
3157 |
|
3158 | |
@@ -3716,11 +3722,12 @@ snapshots:
|
|
3716 |
|
3717 | |
3718 |
|
3719 |
-
melt@0.
|
3720 |
dependencies:
|
3721 |
'@floating-ui/dom': 1.6.13
|
|
|
3722 |
jest-axe: 9.0.0
|
3723 |
-
nanoid: 5.1.
|
3724 |
runed: 0.23.4([email protected])
|
3725 |
svelte: 5.23.0
|
3726 |
|
@@ -3766,7 +3773,7 @@ snapshots:
|
|
3766 |
|
3767 | |
3768 |
|
3769 |
-
[email protected].
|
3770 |
|
3771 | |
3772 |
|
|
|
85 |
specifier: ^2.4.2
|
86 |
version: 2.4.2
|
87 |
melt:
|
88 |
+
specifier: ^0.29.2
|
89 |
+
version: 0.29.2(@floating-ui/[email protected])([email protected])
|
90 |
openai:
|
91 |
specifier: ^4.90.0
|
92 |
version: 4.90.0
|
|
|
1098 |
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
1099 |
engines: {node: '>=0.4.0'}
|
1100 |
|
1101 | |
1102 |
+
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
1103 |
+
engines: {node: '>=6'}
|
1104 |
+
|
1105 | |
1106 |
resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==}
|
1107 |
engines: {node: '>=0.10'}
|
|
|
1646 |
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
1647 |
engines: {node: '>= 0.4'}
|
1648 |
|
1649 |
+
melt@0.29.2:
|
1650 |
+
resolution: {integrity: sha512-x0qR8yE8+x2Bu6s1DRJNAxPBN295ANfTVJ/8UcWsNm/hb7M14ws9G64OFpRExZcI45kdh2KZb1LwHFmNsLwUbQ==}
|
1651 |
peerDependencies:
|
1652 |
'@floating-ui/dom': ^1.6.0
|
1653 |
svelte: ^5.0.0
|
|
|
1704 |
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
1705 |
hasBin: true
|
1706 |
|
1707 |
+
[email protected].5:
|
1708 |
+
resolution: {integrity: sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==}
|
1709 |
engines: {node: ^18 || >=20}
|
1710 |
hasBin: true
|
1711 |
|
|
|
3157 |
|
3158 | |
3159 |
|
3160 |
+
[email protected]: {}
|
3161 |
+
|
3162 | |
3163 |
|
3164 | |
|
|
3722 |
|
3723 | |
3724 |
|
3725 |
+
melt@0.29.2(@floating-ui/[email protected])([email protected]):
|
3726 |
dependencies:
|
3727 |
'@floating-ui/dom': 1.6.13
|
3728 |
+
dequal: 2.0.3
|
3729 |
jest-axe: 9.0.0
|
3730 |
+
nanoid: 5.1.5
|
3731 |
runed: 0.23.4([email protected])
|
3732 |
svelte: 5.23.0
|
3733 |
|
|
|
3773 |
|
3774 | |
3775 |
|
3776 |
+
[email protected].5: {}
|
3777 |
|
3778 | |
3779 |
|
src/lib/components/inference-playground/checkpoints-menu.svelte
ADDED
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import { checkpoints } from "$lib/state/checkpoints.svelte";
|
3 |
+
import { session } from "$lib/state/session.svelte.js";
|
4 |
+
import { Popover } from "melt/builders";
|
5 |
+
import { Tooltip } from "melt/components";
|
6 |
+
import { fly } from "svelte/transition";
|
7 |
+
import IconHistory from "~icons/carbon/recently-viewed";
|
8 |
+
import IconDelete from "~icons/carbon/trash-can";
|
9 |
+
import IconStar from "~icons/carbon/star";
|
10 |
+
import IconStarFilled from "~icons/carbon/star-filled";
|
11 |
+
import IconCompare from "~icons/carbon/compare";
|
12 |
+
|
13 |
+
const popover = new Popover({
|
14 |
+
floatingConfig: {
|
15 |
+
offset: { crossAxis: -12 },
|
16 |
+
},
|
17 |
+
});
|
18 |
+
|
19 |
+
const projCheckpoints = $derived(checkpoints.for(session.project.id));
|
20 |
+
</script>
|
21 |
+
|
22 |
+
<button class="btn relative size-[32px] p-0" {...popover.trigger}>
|
23 |
+
<IconHistory />
|
24 |
+
{#if projCheckpoints.length > 0}
|
25 |
+
<div class="absolute -top-1 -right-1 size-2.5 rounded-full bg-amber-500" aria-label="Project has checkpoints"></div>
|
26 |
+
{/if}
|
27 |
+
</button>
|
28 |
+
|
29 |
+
<div
|
30 |
+
class="mb-2 overflow-hidden rounded-xl border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-800"
|
31 |
+
{...popover.content}
|
32 |
+
>
|
33 |
+
<div class="max-h-120 w-80 overflow-x-clip overflow-y-auto p-3 pb-1">
|
34 |
+
<div class="mb-2 flex items-center justify-between px-1">
|
35 |
+
<h3 class="text-sm font-medium dark:text-white">Checkpoints</h3>
|
36 |
+
<button
|
37 |
+
class="rounded-lg bg-blue-600 px-2 py-1 text-xs font-medium text-white transition-colors hover:bg-blue-700"
|
38 |
+
onclick={() => checkpoints.commit(session.project.id)}
|
39 |
+
>
|
40 |
+
Create new
|
41 |
+
</button>
|
42 |
+
</div>
|
43 |
+
|
44 |
+
{#each projCheckpoints as checkpoint (checkpoint.id)}
|
45 |
+
{@const state = checkpoint.projectState}
|
46 |
+
{@const multiple = state.conversations.length > 1}
|
47 |
+
<Tooltip
|
48 |
+
openDelay={0}
|
49 |
+
floatingConfig={{
|
50 |
+
computePosition: {
|
51 |
+
placement: "right",
|
52 |
+
},
|
53 |
+
offset: {
|
54 |
+
mainAxis: 16,
|
55 |
+
},
|
56 |
+
}}
|
57 |
+
forceVisible
|
58 |
+
>
|
59 |
+
{#snippet children(tooltip)}
|
60 |
+
<div
|
61 |
+
class="mb-2 flex w-full items-center rounded-md px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700"
|
62 |
+
{...tooltip.trigger}
|
63 |
+
>
|
64 |
+
<button
|
65 |
+
class="flex flex-1 flex-col text-left text-sm transition-colors"
|
66 |
+
onclick={() => checkpoints.restore(session.project.id, checkpoint)}
|
67 |
+
>
|
68 |
+
<span class="font-medium text-gray-400">{checkpoint.timestamp}</span>
|
69 |
+
|
70 |
+
<p class="mt-0.5 flex items-center gap-2 text-sm">
|
71 |
+
{#if multiple}
|
72 |
+
<IconCompare class="text-xs text-gray-400" />
|
73 |
+
{/if}
|
74 |
+
{#each state.conversations as { messages }, i}
|
75 |
+
<span class={["text-gray-200"]}>
|
76 |
+
{messages.length} messages
|
77 |
+
</span>
|
78 |
+
{#if multiple && i === 0}
|
79 |
+
<span class="text-gray-500">|</span>
|
80 |
+
{/if}
|
81 |
+
{/each}
|
82 |
+
</p>
|
83 |
+
</button>
|
84 |
+
|
85 |
+
<button
|
86 |
+
class="mr-0.5 grid place-items-center rounded-md p-1 text-xs hover:bg-gray-300 dark:hover:bg-gray-600"
|
87 |
+
onclick={e => {
|
88 |
+
e.stopPropagation();
|
89 |
+
checkpoints.toggleFavorite(session.project.id, checkpoint);
|
90 |
+
}}
|
91 |
+
>
|
92 |
+
{#if checkpoint.favorite}
|
93 |
+
<IconStarFilled class="text-yellow-500" />
|
94 |
+
{:else}
|
95 |
+
<IconStar />
|
96 |
+
{/if}
|
97 |
+
</button>
|
98 |
+
<button
|
99 |
+
class="grid place-items-center rounded-md p-1 text-xs hover:bg-gray-300 dark:hover:bg-gray-600"
|
100 |
+
onclick={e => {
|
101 |
+
e.stopPropagation();
|
102 |
+
checkpoints.delete(session.project.id, checkpoint);
|
103 |
+
}}
|
104 |
+
>
|
105 |
+
<IconDelete />
|
106 |
+
</button>
|
107 |
+
</div>
|
108 |
+
|
109 |
+
{#if tooltip.open}
|
110 |
+
<div
|
111 |
+
class={["flex rounded-xl border border-gray-700 bg-gray-800 p-2 shadow"]}
|
112 |
+
{...tooltip.content}
|
113 |
+
transition:fly={{ x: -2 }}
|
114 |
+
>
|
115 |
+
<div class="size-4 rounded-tl border-t border-l border-gray-700" {...tooltip.arrow}></div>
|
116 |
+
{#each state.conversations as conversation, i}
|
117 |
+
{@const msgs = conversation.messages.filter(m => m.content?.trim())}
|
118 |
+
{@const sliced = msgs.slice(0, 4)}
|
119 |
+
<div
|
120 |
+
class={[
|
121 |
+
"p-2",
|
122 |
+
multiple ? "w-52" : "w-72",
|
123 |
+
i === 0 && multiple && "border-r border-gray-200 dark:border-gray-700",
|
124 |
+
]}
|
125 |
+
>
|
126 |
+
<p class="text-2xs pl-1.5 font-mono font-medium text-gray-500 uppercase">
|
127 |
+
temp: {conversation.config.temperature}
|
128 |
+
| max tokens: {conversation.config.max_tokens}
|
129 |
+
</p>
|
130 |
+
{#each sliced as msg, i}
|
131 |
+
{@const isLast = i === sliced.length - 1}
|
132 |
+
<div class="flex flex-col gap-1 p-2">
|
133 |
+
<p class="font-mono text-xs font-medium text-gray-400 uppercase">{msg.role}</p>
|
134 |
+
<p class="line-clamp-2 text-sm">{msg.content}</p>
|
135 |
+
</div>
|
136 |
+
{#if !isLast}
|
137 |
+
<div class="my-2 h-px w-full bg-gray-200 dark:bg-gray-700"></div>
|
138 |
+
{/if}
|
139 |
+
{/each}
|
140 |
+
</div>
|
141 |
+
{/each}
|
142 |
+
</div>
|
143 |
+
{/if}
|
144 |
+
{/snippet}
|
145 |
+
</Tooltip>
|
146 |
+
{:else}
|
147 |
+
<div class="flex flex-col items-center gap-2 py-3">
|
148 |
+
<span class="text-gray-500 text-sm">No checkpoints available</span>
|
149 |
+
</div>
|
150 |
+
{/each}
|
151 |
+
</div>
|
152 |
+
</div>
|
src/lib/components/inference-playground/hf-token-modal.svelte
CHANGED
@@ -103,7 +103,7 @@
|
|
103 |
<!-- Modal footer -->
|
104 |
<div class="flex items-center justify-between rounded-b border-t border-gray-200 p-4 md:p-5 dark:border-gray-800">
|
105 |
<a
|
106 |
-
href="https://huggingface.co/settings/tokens/new?
|
107 |
tabindex="-1"
|
108 |
target="_blank"
|
109 |
class="rounded-lg border border-gray-200 bg-white px-5 py-2.5 text-sm font-medium text-gray-900 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 focus:outline-hidden dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700"
|
|
|
103 |
<!-- Modal footer -->
|
104 |
<div class="flex items-center justify-between rounded-b border-t border-gray-200 p-4 md:p-5 dark:border-gray-800">
|
105 |
<a
|
106 |
+
href="https://huggingface.co/settings/tokens/new?ownUserPermissions=inference.serverless.write&tokenType=fineGrained"
|
107 |
tabindex="-1"
|
108 |
target="_blank"
|
109 |
class="rounded-lg border border-gray-200 bg-white px-5 py-2.5 text-sm font-medium text-gray-900 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 focus:outline-hidden dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700"
|
src/lib/components/inference-playground/project-select.svelte
CHANGED
@@ -2,6 +2,7 @@
|
|
2 |
import { session } from "$lib/state/session.svelte.js";
|
3 |
import { cn } from "$lib/utils/cn.js";
|
4 |
import { Select } from "melt/builders";
|
|
|
5 |
import IconCaret from "~icons/carbon/chevron-down";
|
6 |
import IconCross from "~icons/carbon/close";
|
7 |
import IconEdit from "~icons/carbon/edit";
|
@@ -9,6 +10,8 @@
|
|
9 |
import IconDelete from "~icons/carbon/trash-can";
|
10 |
import { prompt } from "../prompts.svelte";
|
11 |
import Tooltip from "../tooltip.svelte";
|
|
|
|
|
12 |
|
13 |
interface Props {
|
14 |
class?: string;
|
@@ -50,6 +53,7 @@
|
|
50 |
</button>
|
51 |
|
52 |
<div class="flex items-center gap-2">
|
|
|
53 |
{#if isDefault}
|
54 |
<Tooltip>
|
55 |
{#snippet trigger(tooltip)}
|
@@ -79,11 +83,22 @@
|
|
79 |
<div {...select.content} class="rounded-lg border bg-gray-100 dark:border-gray-700 dark:bg-gray-800">
|
80 |
{#each session.$.projects as { name, id } (id)}
|
81 |
{@const option = select.getOption(id)}
|
|
|
82 |
<div {...option} class="group block w-full p-1 text-sm dark:text-white">
|
83 |
<div
|
84 |
class="flex items-center gap-2 rounded-md py-1.5 pr-1 pl-2 group-data-[highlighted]:bg-gray-200 dark:group-data-[highlighted]:bg-gray-700"
|
85 |
>
|
86 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
87 |
{#if id !== "default"}
|
88 |
<div class="ml-auto flex items-center gap-1">
|
89 |
<button
|
|
|
2 |
import { session } from "$lib/state/session.svelte.js";
|
3 |
import { cn } from "$lib/utils/cn.js";
|
4 |
import { Select } from "melt/builders";
|
5 |
+
import IconHistory from "~icons/carbon/recently-viewed";
|
6 |
import IconCaret from "~icons/carbon/chevron-down";
|
7 |
import IconCross from "~icons/carbon/close";
|
8 |
import IconEdit from "~icons/carbon/edit";
|
|
|
10 |
import IconDelete from "~icons/carbon/trash-can";
|
11 |
import { prompt } from "../prompts.svelte";
|
12 |
import Tooltip from "../tooltip.svelte";
|
13 |
+
import CheckpointsMenu from "./checkpoints-menu.svelte";
|
14 |
+
import { checkpoints } from "$lib/state/checkpoints.svelte";
|
15 |
|
16 |
interface Props {
|
17 |
class?: string;
|
|
|
53 |
</button>
|
54 |
|
55 |
<div class="flex items-center gap-2">
|
56 |
+
<CheckpointsMenu />
|
57 |
{#if isDefault}
|
58 |
<Tooltip>
|
59 |
{#snippet trigger(tooltip)}
|
|
|
83 |
<div {...select.content} class="rounded-lg border bg-gray-100 dark:border-gray-700 dark:bg-gray-800">
|
84 |
{#each session.$.projects as { name, id } (id)}
|
85 |
{@const option = select.getOption(id)}
|
86 |
+
{@const hasCheckpoints = checkpoints.for(id).length > 0}
|
87 |
<div {...option} class="group block w-full p-1 text-sm dark:text-white">
|
88 |
<div
|
89 |
class="flex items-center gap-2 rounded-md py-1.5 pr-1 pl-2 group-data-[highlighted]:bg-gray-200 dark:group-data-[highlighted]:bg-gray-700"
|
90 |
>
|
91 |
+
<div class="flex items-center gap-2">
|
92 |
+
{name}
|
93 |
+
{#if hasCheckpoints}
|
94 |
+
<div
|
95 |
+
class="text-3xs grid aspect-square place-items-center rounded bg-yellow-400/25 p-0.5 text-yellow-400"
|
96 |
+
aria-label="Project has checkpoints"
|
97 |
+
>
|
98 |
+
<IconHistory />
|
99 |
+
</div>
|
100 |
+
{/if}
|
101 |
+
</div>
|
102 |
{#if id !== "default"}
|
103 |
<div class="ml-auto flex items-center gap-1">
|
104 |
<button
|
src/lib/components/toaster.svelte
CHANGED
@@ -14,7 +14,6 @@
|
|
14 |
|
15 |
const toastEls = Array.from(rootEl.querySelectorAll("[data-melt-toaster-toast-content]"));
|
16 |
toastHeights = toastEls.map(el => el.clientHeight);
|
17 |
-
// console.log(toastHeights);
|
18 |
});
|
19 |
|
20 |
const isComparing = $derived(session.project.conversations.length > 1);
|
|
|
14 |
|
15 |
const toastEls = Array.from(rootEl.querySelectorAll("[data-melt-toaster-toast-content]"));
|
16 |
toastHeights = toastEls.map(el => el.clientHeight);
|
|
|
17 |
});
|
18 |
|
19 |
const isComparing = $derived(session.project.conversations.length > 1);
|
src/lib/state/checkpoints.svelte.ts
ADDED
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import type { Project } from "$lib/types.js";
|
2 |
+
import { PersistedState } from "runed";
|
3 |
+
import { session } from "./session.svelte";
|
4 |
+
|
5 |
+
const ls_key = "checkpoints";
|
6 |
+
|
7 |
+
type Checkpoint = {
|
8 |
+
id: string;
|
9 |
+
timestamp: string;
|
10 |
+
projectState: Project;
|
11 |
+
favorite?: boolean;
|
12 |
+
};
|
13 |
+
|
14 |
+
class Checkpoints {
|
15 |
+
#checkpoints = new PersistedState<Record<Project["id"], Checkpoint[]>>(
|
16 |
+
ls_key,
|
17 |
+
{},
|
18 |
+
{
|
19 |
+
serializer: {
|
20 |
+
serialize: JSON.stringify,
|
21 |
+
deserialize: v => {
|
22 |
+
return JSON.parse(v);
|
23 |
+
},
|
24 |
+
},
|
25 |
+
}
|
26 |
+
);
|
27 |
+
|
28 |
+
for(projectId: Project["id"]) {
|
29 |
+
return (
|
30 |
+
this.#checkpoints.current[projectId]?.toSorted((a, b) => {
|
31 |
+
return b.timestamp.localeCompare(a.timestamp);
|
32 |
+
}) ?? []
|
33 |
+
);
|
34 |
+
}
|
35 |
+
|
36 |
+
commit(projectId: Project["id"]) {
|
37 |
+
const project = session.$.projects.find(p => p.id == projectId);
|
38 |
+
if (!project) return;
|
39 |
+
const prev: Checkpoint[] = this.#checkpoints.current[projectId] ?? [];
|
40 |
+
this.#checkpoints.current[projectId] = [
|
41 |
+
...prev,
|
42 |
+
{ projectState: project, timestamp: new Date().toLocaleString(), id: crypto.randomUUID() },
|
43 |
+
];
|
44 |
+
}
|
45 |
+
|
46 |
+
restore(projectId: Project["id"], checkpoint: Checkpoint) {
|
47 |
+
const project = session.$.projects.find(p => p.id == projectId);
|
48 |
+
if (!project) return;
|
49 |
+
|
50 |
+
session.$.activeProjectId = projectId;
|
51 |
+
session.project = checkpoint.projectState;
|
52 |
+
}
|
53 |
+
|
54 |
+
toggleFavorite(projectId: Project["id"], checkpoint: Checkpoint) {
|
55 |
+
const prev: Checkpoint[] = this.#checkpoints.current[projectId] ?? [];
|
56 |
+
this.#checkpoints.current[projectId] = prev.map(c => {
|
57 |
+
if (c.id == checkpoint.id) {
|
58 |
+
return { ...c, favorite: !c.favorite };
|
59 |
+
}
|
60 |
+
return c;
|
61 |
+
});
|
62 |
+
}
|
63 |
+
|
64 |
+
delete(projectId: Project["id"], checkpoint: Checkpoint) {
|
65 |
+
const prev: Checkpoint[] = this.#checkpoints.current[projectId] ?? [];
|
66 |
+
this.#checkpoints.current[projectId] = prev.filter(c => c.id != checkpoint.id);
|
67 |
+
}
|
68 |
+
|
69 |
+
clear(projectId: Project["id"]) {
|
70 |
+
this.#checkpoints.current[projectId] = [];
|
71 |
+
}
|
72 |
+
|
73 |
+
migrate(from: Project["id"], to: Project["id"]) {
|
74 |
+
const fromArr = this.#checkpoints.current[from] ?? [];
|
75 |
+
this.#checkpoints.current[to] = [...fromArr];
|
76 |
+
this.#checkpoints.current[from] = [];
|
77 |
+
}
|
78 |
+
}
|
79 |
+
|
80 |
+
export const checkpoints = new Checkpoints();
|
src/lib/state/session.svelte.ts
CHANGED
@@ -12,6 +12,7 @@ import {
|
|
12 |
import { safeParse } from "$lib/utils/json.js";
|
13 |
import typia from "typia";
|
14 |
import { models } from "./models.svelte";
|
|
|
15 |
|
16 |
const LOCAL_STORAGE_KEY = "hf_inference_playground_session";
|
17 |
|
@@ -148,6 +149,8 @@ class SessionState {
|
|
148 |
id: crypto.randomUUID(),
|
149 |
};
|
150 |
|
|
|
|
|
151 |
defaultProject.conversations = [getDefaults().defaultConversation];
|
152 |
|
153 |
this.addProject(project);
|
@@ -169,6 +172,7 @@ class SessionState {
|
|
169 |
|
170 |
const currProject = projects.find(p => p.id === this.$.activeProjectId);
|
171 |
this.#setAnySession({ ...this.$, projects, activeProjectId: currProject?.id ?? projects[0]?.id });
|
|
|
172 |
};
|
173 |
|
174 |
updateProject = (id: string, data: Partial<Project>) => {
|
|
|
12 |
import { safeParse } from "$lib/utils/json.js";
|
13 |
import typia from "typia";
|
14 |
import { models } from "./models.svelte";
|
15 |
+
import { checkpoints } from "./checkpoints.svelte";
|
16 |
|
17 |
const LOCAL_STORAGE_KEY = "hf_inference_playground_session";
|
18 |
|
|
|
149 |
id: crypto.randomUUID(),
|
150 |
};
|
151 |
|
152 |
+
checkpoints.migrate(defaultProject.id, project.id);
|
153 |
+
|
154 |
defaultProject.conversations = [getDefaults().defaultConversation];
|
155 |
|
156 |
this.addProject(project);
|
|
|
172 |
|
173 |
const currProject = projects.find(p => p.id === this.$.activeProjectId);
|
174 |
this.#setAnySession({ ...this.$, projects, activeProjectId: currProject?.id ?? projects[0]?.id });
|
175 |
+
checkpoints.clear(id);
|
176 |
};
|
177 |
|
178 |
updateProject = (id: string, data: Partial<Project>) => {
|
src/lib/utils/string.ts
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export function pluralize(word: string, count: number): string {
|
2 |
+
if (count === 1) {
|
3 |
+
return word;
|
4 |
+
}
|
5 |
+
return word + "s";
|
6 |
+
}
|