Thomas G. Lopes victor HF Staff commited on
Commit
7450ebd
·
unverified ·
1 Parent(s): 97c4991

Checkpoints (#73)

Browse files

Co-authored-by: Victor Muštar (aider) <[email protected]>

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.28.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.28.0
89
- version: 0.28.0(@floating-ui/[email protected])([email protected])
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.28.0:
1646
- resolution: {integrity: sha512-kiqaTgNB/IkADmUfJZKROqQ3z+isal8LjLhckQANqjfjggIosHM8M7RO3Og7IQ12zK06nLnwanL80SuTPhblrw==}
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
1704
- resolution: {integrity: sha512-b+CiXQCNMUGe0Ri64S9SXFcP9hogjAJ2Rd6GdVxhPLRm7mhGaM7VgOvCAJ1ZshfHbqVDI3uqTI5C8/GaKuLI7g==}
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.28.0(@floating-ui/[email protected])([email protected]):
3720
  dependencies:
3721
  '@floating-ui/dom': 1.6.13
 
3722
  jest-axe: 9.0.0
3723
- nanoid: 5.1.2
3724
  runed: 0.23.4([email protected])
3725
  svelte: 5.23.0
3726
 
@@ -3766,7 +3773,7 @@ snapshots:
3766
 
3767
3768
 
3769
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
1708
+ resolution: {integrity: sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==}
1709
  engines: {node: ^18 || >=20}
1710
  hasBin: true
1711
 
 
3157
 
3158
3159
 
3160
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
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?globalPermissions=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"
 
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
- {name}
 
 
 
 
 
 
 
 
 
 
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
+ }