Thomas G. Lopes
commited on
fix reset; add some tests (#90)
Browse files- e2e/home.test.ts +48 -0
- playwright.config.ts +3 -2
- src/lib/components/inference-playground/checkpoints-menu.svelte +4 -1
- src/lib/components/inference-playground/message.svelte +2 -0
- src/lib/components/inference-playground/playground.svelte +8 -1
- src/lib/components/inference-playground/structured-output-modal.svelte +2 -6
- src/lib/constants.ts +9 -0
- src/lib/state/checkpoints.svelte.ts +7 -2
- src/lib/state/conversations.svelte.ts +32 -30
e2e/home.test.ts
CHANGED
@@ -1,4 +1,5 @@
|
|
1 |
import { expect, test } from "@playwright/test";
|
|
|
2 |
|
3 |
const HF_TOKEN = "hf_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
4 |
const HF_TOKEN_STORAGE_KEY = "hf_token";
|
@@ -59,5 +60,52 @@ test.describe.serial("Token Handling and Subsequent Tests", () => {
|
|
59 |
await expect(userInputAfterReload).toBeVisible();
|
60 |
expect(await userInputAfterReload.inputValue()).toBe("Hello Hugging Face!");
|
61 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
62 |
});
|
63 |
});
|
|
|
1 |
import { expect, test } from "@playwright/test";
|
2 |
+
import { TEST_IDS } from "../src/lib/constants.js";
|
3 |
|
4 |
const HF_TOKEN = "hf_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
5 |
const HF_TOKEN_STORAGE_KEY = "hf_token";
|
|
|
60 |
await expect(userInputAfterReload).toBeVisible();
|
61 |
expect(await userInputAfterReload.inputValue()).toBe("Hello Hugging Face!");
|
62 |
});
|
63 |
+
|
64 |
+
test("checkpoints, resetting, and restoring", async ({ page }) => {
|
65 |
+
await page.goto("/");
|
66 |
+
const userMsg = "user message: hi";
|
67 |
+
const assistantMsg = "assistant message: hey";
|
68 |
+
|
69 |
+
// Fill user message
|
70 |
+
await page.getByRole("textbox", { name: "Enter user message" }).click();
|
71 |
+
await page.getByRole("textbox", { name: "Enter user message" }).fill(userMsg);
|
72 |
+
// Blur
|
73 |
+
await page.locator(".relative > div:nth-child(2) > div").first().click();
|
74 |
+
|
75 |
+
// Fill assistant message
|
76 |
+
await page.getByRole("button", { name: "Add message" }).click();
|
77 |
+
await page.getByRole("textbox", { name: "Enter assistant message" }).fill(assistantMsg);
|
78 |
+
// Blur
|
79 |
+
await page.locator(".relative > div:nth-child(2) > div").first().click();
|
80 |
+
|
81 |
+
// Create Checkpoint
|
82 |
+
await page.locator(`[data-test-id="${TEST_IDS.checkpoints_trigger}"]`).click();
|
83 |
+
await page.getByRole("button", { name: "Create new" }).click();
|
84 |
+
|
85 |
+
// Check that there are checkpoints
|
86 |
+
await expect(page.locator(`[data-test-id="${TEST_IDS.checkpoint}"] `)).toBeVisible();
|
87 |
+
|
88 |
+
// Get out of menu
|
89 |
+
await page.locator(`[data-test-id="${TEST_IDS.checkpoints_menu}"] `).press("Escape");
|
90 |
+
await page.locator(`[data-test-id="${TEST_IDS.checkpoints_menu}"] `).press("Escape");
|
91 |
+
|
92 |
+
// Reset
|
93 |
+
await page.locator(`[data-test-id="${TEST_IDS.reset}"]`).click();
|
94 |
+
|
95 |
+
// Check that messages are gone now
|
96 |
+
await expect(page.getByRole("textbox", { name: "Enter user message" })).toHaveValue("");
|
97 |
+
|
98 |
+
// Call in a checkpoint
|
99 |
+
await page.locator(`[data-test-id="${TEST_IDS.checkpoints_trigger}"]`).click();
|
100 |
+
await page.locator(`[data-test-id="${TEST_IDS.checkpoint}"] `).click();
|
101 |
+
|
102 |
+
// Get out of menu
|
103 |
+
await page.locator(`[data-test-id="${TEST_IDS.checkpoints_menu}"] `).press("Escape");
|
104 |
+
await page.locator(`[data-test-id="${TEST_IDS.checkpoints_menu}"] `).press("Escape");
|
105 |
+
|
106 |
+
// Check that the messages are back
|
107 |
+
await expect(page.getByRole("textbox", { name: "Enter user message" })).toHaveValue(userMsg);
|
108 |
+
await expect(page.getByRole("textbox", { name: "Enter assistant message" })).toHaveValue(assistantMsg);
|
109 |
+
});
|
110 |
});
|
111 |
});
|
playwright.config.ts
CHANGED
@@ -2,8 +2,9 @@ import { defineConfig } from "@playwright/test";
|
|
2 |
|
3 |
export default defineConfig({
|
4 |
webServer: {
|
5 |
-
command: "npm run build && npm run preview",
|
6 |
-
port: 4173,
|
|
|
7 |
timeout: 1000 * 60 * 10,
|
8 |
},
|
9 |
testDir: "e2e",
|
|
|
2 |
|
3 |
export default defineConfig({
|
4 |
webServer: {
|
5 |
+
command: process.env.CI ? "npm run build && npm run preview" : "",
|
6 |
+
port: process.env.CI ? 4173 : 5173,
|
7 |
+
reuseExistingServer: !process.env.CI,
|
8 |
timeout: 1000 * 60 * 10,
|
9 |
},
|
10 |
testDir: "e2e",
|
src/lib/components/inference-playground/checkpoints-menu.svelte
CHANGED
@@ -12,6 +12,7 @@
|
|
12 |
import IconStar from "~icons/carbon/star";
|
13 |
import IconStarFilled from "~icons/carbon/star-filled";
|
14 |
import IconDelete from "~icons/carbon/trash-can";
|
|
|
15 |
|
16 |
const popover = new Popover({
|
17 |
floatingConfig: {
|
@@ -27,7 +28,7 @@
|
|
27 |
const projCheckpoints = $derived(checkpoints.for(projects.activeId));
|
28 |
</script>
|
29 |
|
30 |
-
<button class="btn relative size-[32px] p-0" {...popover.trigger}>
|
31 |
<IconHistory />
|
32 |
{#if projCheckpoints.length > 0}
|
33 |
<div class="absolute -top-1 -right-1 size-2.5 rounded-full bg-amber-500" aria-label="Project has checkpoints"></div>
|
@@ -39,6 +40,7 @@
|
|
39 |
class="mb-2 !overflow-visible rounded-xl border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-800"
|
40 |
{@attach clickOutside(() => (popover.open = false))}
|
41 |
{...popover.content}
|
|
|
42 |
>
|
43 |
<div
|
44 |
class="size-4 translate-x-3 rounded-tl border-t border-l border-gray-200 dark:border-gray-700"
|
@@ -74,6 +76,7 @@
|
|
74 |
<div
|
75 |
class="mb-2 flex w-full items-center rounded-md px-3 hover:bg-gray-100 dark:hover:bg-gray-700"
|
76 |
{...tooltip.trigger}
|
|
|
77 |
>
|
78 |
<button
|
79 |
class="flex flex-1 flex-col py-2 text-left text-sm transition-colors"
|
|
|
12 |
import IconStar from "~icons/carbon/star";
|
13 |
import IconStarFilled from "~icons/carbon/star-filled";
|
14 |
import IconDelete from "~icons/carbon/trash-can";
|
15 |
+
import { TEST_IDS } from "$lib/constants.js";
|
16 |
|
17 |
const popover = new Popover({
|
18 |
floatingConfig: {
|
|
|
28 |
const projCheckpoints = $derived(checkpoints.for(projects.activeId));
|
29 |
</script>
|
30 |
|
31 |
+
<button class="btn relative size-[32px] p-0" {...popover.trigger} data-test-id={TEST_IDS.checkpoints_trigger}>
|
32 |
<IconHistory />
|
33 |
{#if projCheckpoints.length > 0}
|
34 |
<div class="absolute -top-1 -right-1 size-2.5 rounded-full bg-amber-500" aria-label="Project has checkpoints"></div>
|
|
|
40 |
class="mb-2 !overflow-visible rounded-xl border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-800"
|
41 |
{@attach clickOutside(() => (popover.open = false))}
|
42 |
{...popover.content}
|
43 |
+
data-test-id={TEST_IDS.checkpoints_menu}
|
44 |
>
|
45 |
<div
|
46 |
class="size-4 translate-x-3 rounded-tl border-t border-l border-gray-200 dark:border-gray-700"
|
|
|
76 |
<div
|
77 |
class="mb-2 flex w-full items-center rounded-md px-3 hover:bg-gray-100 dark:hover:bg-gray-700"
|
78 |
{...tooltip.trigger}
|
79 |
+
data-test-id={TEST_IDS.checkpoint}
|
80 |
>
|
81 |
<button
|
82 |
class="flex flex-1 flex-col py-2 text-left text-sm transition-colors"
|
src/lib/components/inference-playground/message.svelte
CHANGED
@@ -15,6 +15,7 @@
|
|
15 |
import IconCustom from "../icon-custom.svelte";
|
16 |
import LocalToasts from "../local-toasts.svelte";
|
17 |
import ImgPreview from "./img-preview.svelte";
|
|
|
18 |
|
19 |
type Props = {
|
20 |
conversation: ConversationClass;
|
@@ -106,6 +107,7 @@
|
|
106 |
class="grow resize-none overflow-hidden rounded-lg bg-transparent px-2 py-2.5 ring-gray-100 outline-none group-hover/message:ring-3 hover:bg-white focus:bg-white focus:ring-3 @2xl:px-3 dark:ring-gray-600 dark:hover:bg-gray-900 dark:focus:bg-gray-900"
|
107 |
rows="1"
|
108 |
data-message
|
|
|
109 |
{@attach autofocusAction(autofocus)}
|
110 |
{@attach autosized.attachment}
|
111 |
></textarea>
|
|
|
15 |
import IconCustom from "../icon-custom.svelte";
|
16 |
import LocalToasts from "../local-toasts.svelte";
|
17 |
import ImgPreview from "./img-preview.svelte";
|
18 |
+
import { TEST_IDS } from "$lib/constants.js";
|
19 |
|
20 |
type Props = {
|
21 |
conversation: ConversationClass;
|
|
|
107 |
class="grow resize-none overflow-hidden rounded-lg bg-transparent px-2 py-2.5 ring-gray-100 outline-none group-hover/message:ring-3 hover:bg-white focus:bg-white focus:ring-3 @2xl:px-3 dark:ring-gray-600 dark:hover:bg-gray-900 dark:focus:bg-gray-900"
|
108 |
rows="1"
|
109 |
data-message
|
110 |
+
data-test-id={TEST_IDS.message}
|
111 |
{@attach autofocusAction(autofocus)}
|
112 |
{@attach autosized.attachment}
|
113 |
></textarea>
|
src/lib/components/inference-playground/playground.svelte
CHANGED
@@ -29,6 +29,7 @@
|
|
29 |
import ModelSelectorModal from "./model-selector-modal.svelte";
|
30 |
import ModelSelector from "./model-selector.svelte";
|
31 |
import ProjectSelect from "./project-select.svelte";
|
|
|
32 |
|
33 |
const multiple = $derived(conversations.active.length > 1);
|
34 |
|
@@ -153,7 +154,13 @@
|
|
153 |
{/if}
|
154 |
<Tooltip>
|
155 |
{#snippet trigger(tooltip)}
|
156 |
-
<button
|
|
|
|
|
|
|
|
|
|
|
|
|
157 |
<IconDelete />
|
158 |
</button>
|
159 |
{/snippet}
|
|
|
29 |
import ModelSelectorModal from "./model-selector-modal.svelte";
|
30 |
import ModelSelector from "./model-selector.svelte";
|
31 |
import ProjectSelect from "./project-select.svelte";
|
32 |
+
import { TEST_IDS } from "$lib/constants.js";
|
33 |
|
34 |
const multiple = $derived(conversations.active.length > 1);
|
35 |
|
|
|
154 |
{/if}
|
155 |
<Tooltip>
|
156 |
{#snippet trigger(tooltip)}
|
157 |
+
<button
|
158 |
+
type="button"
|
159 |
+
onclick={conversations.reset}
|
160 |
+
class="btn size-[39px]"
|
161 |
+
{...tooltip.trigger}
|
162 |
+
data-test-id={TEST_IDS.reset}
|
163 |
+
>
|
164 |
<IconDelete />
|
165 |
</button>
|
166 |
{/snippet}
|
src/lib/components/inference-playground/structured-output-modal.svelte
CHANGED
@@ -99,11 +99,7 @@
|
|
99 |
});
|
100 |
}
|
101 |
|
102 |
-
|
103 |
-
new TextareaAutosize({
|
104 |
-
element: () => textarea,
|
105 |
-
input: () => tempSchema,
|
106 |
-
});
|
107 |
</script>
|
108 |
|
109 |
<Dialog class="!w-2xl max-w-[90vw]" title="Edit Structured Output" {open} onClose={() => (open = false)}>
|
@@ -262,7 +258,6 @@
|
|
262 |
{/await}
|
263 |
</div>
|
264 |
<textarea
|
265 |
-
bind:this={textarea}
|
266 |
autofocus
|
267 |
value={conversation.data.structuredOutput?.schema ?? ""}
|
268 |
{...onchange(v => {
|
@@ -270,6 +265,7 @@
|
|
270 |
})}
|
271 |
{...oninput(v => (tempSchema = v))}
|
272 |
class="relative z-10 h-120 w-full resize-none overflow-hidden rounded-lg bg-transparent whitespace-pre-wrap text-transparent caret-black outline-none @2xl:px-3 dark:caret-white"
|
|
|
273 |
></textarea>
|
274 |
</div>
|
275 |
{/if}
|
|
|
99 |
});
|
100 |
}
|
101 |
|
102 |
+
const autosized = new TextareaAutosize();
|
|
|
|
|
|
|
|
|
103 |
</script>
|
104 |
|
105 |
<Dialog class="!w-2xl max-w-[90vw]" title="Edit Structured Output" {open} onClose={() => (open = false)}>
|
|
|
258 |
{/await}
|
259 |
</div>
|
260 |
<textarea
|
|
|
261 |
autofocus
|
262 |
value={conversation.data.structuredOutput?.schema ?? ""}
|
263 |
{...onchange(v => {
|
|
|
265 |
})}
|
266 |
{...oninput(v => (tempSchema = v))}
|
267 |
class="relative z-10 h-120 w-full resize-none overflow-hidden rounded-lg bg-transparent whitespace-pre-wrap text-transparent caret-black outline-none @2xl:px-3 dark:caret-white"
|
268 |
+
{@attach autosized.attachment}
|
269 |
></textarea>
|
270 |
</div>
|
271 |
{/if}
|
src/lib/constants.ts
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export enum TEST_IDS {
|
2 |
+
checkpoints_trigger,
|
3 |
+
checkpoints_menu,
|
4 |
+
checkpoint,
|
5 |
+
|
6 |
+
reset,
|
7 |
+
|
8 |
+
message,
|
9 |
+
}
|
src/lib/state/checkpoints.svelte.ts
CHANGED
@@ -63,6 +63,11 @@ class Checkpoints {
|
|
63 |
})
|
64 |
);
|
65 |
|
|
|
|
|
|
|
|
|
|
|
66 |
const prev: Checkpoint[] = this.#checkpoints[projectId] ?? [];
|
67 |
this.#checkpoints[projectId] = [...prev, newCheckpoint];
|
68 |
}
|
@@ -85,8 +90,8 @@ class Checkpoints {
|
|
85 |
// conversations.deleteAllFrom(cloned.projectId);
|
86 |
const prev = conversations.for(modified.projectId);
|
87 |
modified.conversations.forEach((c, i) => {
|
88 |
-
const
|
89 |
-
if (
|
90 |
conversations.create({
|
91 |
...c,
|
92 |
projectId: modified.projectId,
|
|
|
63 |
})
|
64 |
);
|
65 |
|
66 |
+
// Hack because dates are formatted to string by save
|
67 |
+
newCheckpoint.conversations.forEach((c, i) => {
|
68 |
+
newCheckpoint.conversations[i] = { ...c, createdAt: new Date(c.createdAt) };
|
69 |
+
});
|
70 |
+
|
71 |
const prev: Checkpoint[] = this.#checkpoints[projectId] ?? [];
|
72 |
this.#checkpoints[projectId] = [...prev, newCheckpoint];
|
73 |
}
|
|
|
90 |
// conversations.deleteAllFrom(cloned.projectId);
|
91 |
const prev = conversations.for(modified.projectId);
|
92 |
modified.conversations.forEach((c, i) => {
|
93 |
+
const prevC = prev[i];
|
94 |
+
if (prevC) return prevC.update({ ...c });
|
95 |
conversations.create({
|
96 |
...c,
|
97 |
projectId: modified.projectId,
|
src/lib/state/conversations.svelte.ts
CHANGED
@@ -110,7 +110,7 @@ export class ConversationClass {
|
|
110 |
return this.isStructuredOutputAllowed && this.data.structuredOutput?.enabled;
|
111 |
}
|
112 |
|
113 |
-
async
|
114 |
if (this.data.id === -1) return;
|
115 |
// if (this.data.id === undefined) return;
|
116 |
const cloned = snapshot({ ...this.data, ...data });
|
@@ -122,16 +122,16 @@ export class ConversationClass {
|
|
122 |
await conversationsRepo.update(this.data.id, cloned);
|
123 |
this.#data = cloned;
|
124 |
}
|
125 |
-
}
|
126 |
|
127 |
-
async
|
128 |
this.update({
|
129 |
...this.data,
|
130 |
messages: [...this.data.messages, snapshot(message)],
|
131 |
});
|
132 |
-
}
|
133 |
|
134 |
-
async
|
135 |
const prev = await poll(() => this.data.messages[args.index], { interval: 10, maxAttempts: 200 });
|
136 |
|
137 |
if (!prev) return;
|
@@ -144,9 +144,9 @@ export class ConversationClass {
|
|
144 |
...this.data.messages.slice(args.index + 1),
|
145 |
],
|
146 |
});
|
147 |
-
}
|
148 |
|
149 |
-
async
|
150 |
const imgKeys = this.data.messages.flatMap(m => m.images).filter(isString);
|
151 |
await Promise.all([
|
152 |
...imgKeys.map(k => images.delete(k)),
|
@@ -155,9 +155,9 @@ export class ConversationClass {
|
|
155 |
messages: this.data.messages.slice(0, idx),
|
156 |
}),
|
157 |
]);
|
158 |
-
}
|
159 |
|
160 |
-
async
|
161 |
const sliced = this.data.messages.slice(0, from);
|
162 |
const notSliced = this.data.messages.slice(from);
|
163 |
|
@@ -169,9 +169,9 @@ export class ConversationClass {
|
|
169 |
messages: sliced,
|
170 |
}),
|
171 |
]);
|
172 |
-
}
|
173 |
|
174 |
-
async
|
175 |
this.generating = true;
|
176 |
const startTime = performance.now();
|
177 |
|
@@ -223,7 +223,7 @@ export class ConversationClass {
|
|
223 |
const endTime = performance.now();
|
224 |
this.generationStats.latency = Math.round(endTime - startTime);
|
225 |
this.generating = false;
|
226 |
-
}
|
227 |
|
228 |
stopGenerating = () => {
|
229 |
this.abortManager.abortAll();
|
@@ -236,7 +236,7 @@ class Conversations {
|
|
236 |
generationStats = $derived(this.active.map(c => c.generationStats));
|
237 |
loaded = $state(false);
|
238 |
|
239 |
-
#active = $derived(this.for(projects.activeId));
|
240 |
|
241 |
init = createInit(() => {
|
242 |
const searchParams = new URLSearchParams(window.location.search);
|
@@ -271,7 +271,9 @@ class Conversations {
|
|
271 |
return this.#active;
|
272 |
}
|
273 |
|
274 |
-
|
|
|
|
|
275 |
const conv = snapshot({
|
276 |
...getDefaultConversation(args.projectId),
|
277 |
...args,
|
@@ -286,9 +288,9 @@ class Conversations {
|
|
286 |
};
|
287 |
|
288 |
return id;
|
289 |
-
}
|
290 |
|
291 |
-
for(projectId: ProjectEntity["id"]): ConversationClass[] {
|
292 |
// Async load from db
|
293 |
if (!this.#conversations[projectId]?.length) {
|
294 |
conversationsRepo.find({ where: { projectId } }).then(c => {
|
@@ -310,27 +312,27 @@ class Conversations {
|
|
310 |
return res.slice(0, 2).toSorted((a, b) => {
|
311 |
return a.data.createdAt.getTime() - b.data.createdAt.getTime();
|
312 |
});
|
313 |
-
}
|
314 |
|
315 |
-
async
|
316 |
if (!id) return;
|
317 |
|
318 |
await conversationsRepo.delete(id);
|
319 |
|
320 |
const prev = this.#conversations[projectId] ?? [];
|
321 |
this.#conversations = { ...this.#conversations, [projectId]: prev.filter(c => c.data.id != id) };
|
322 |
-
}
|
323 |
|
324 |
-
async
|
325 |
this.for(projectId).forEach(c => this.delete(c.data));
|
326 |
-
}
|
327 |
|
328 |
-
async
|
329 |
-
this.active.
|
330 |
this.create(getDefaultConversation(projects.activeId));
|
331 |
-
}
|
332 |
|
333 |
-
async
|
334 |
const fromArr = this.#conversations[from] ?? [];
|
335 |
await Promise.allSettled(fromArr.map(c => c.update({ projectId: to })));
|
336 |
this.#conversations = {
|
@@ -338,18 +340,18 @@ class Conversations {
|
|
338 |
[to]: [...fromArr],
|
339 |
[from]: [],
|
340 |
};
|
341 |
-
}
|
342 |
|
343 |
-
async
|
344 |
const fromArr = this.#conversations[from] ?? [];
|
345 |
await Promise.allSettled(
|
346 |
fromArr.map(async c => {
|
347 |
conversations.create({ ...c.data, projectId: to });
|
348 |
})
|
349 |
);
|
350 |
-
}
|
351 |
|
352 |
-
async
|
353 |
if (!token.value) {
|
354 |
token.showModal = true;
|
355 |
return;
|
@@ -402,7 +404,7 @@ class Conversations {
|
|
402 |
addToast({ title: "Error", description: "An unknown error occurred", variant: "error" });
|
403 |
}
|
404 |
}
|
405 |
-
}
|
406 |
|
407 |
stopGenerating = () => {
|
408 |
this.active.forEach(c => c.abortManager.abortAll());
|
|
|
110 |
return this.isStructuredOutputAllowed && this.data.structuredOutput?.enabled;
|
111 |
}
|
112 |
|
113 |
+
update = async (data: Partial<ConversationEntityMembers>) => {
|
114 |
if (this.data.id === -1) return;
|
115 |
// if (this.data.id === undefined) return;
|
116 |
const cloned = snapshot({ ...this.data, ...data });
|
|
|
122 |
await conversationsRepo.update(this.data.id, cloned);
|
123 |
this.#data = cloned;
|
124 |
}
|
125 |
+
};
|
126 |
|
127 |
+
addMessage = async (message: ConversationMessage) => {
|
128 |
this.update({
|
129 |
...this.data,
|
130 |
messages: [...this.data.messages, snapshot(message)],
|
131 |
});
|
132 |
+
};
|
133 |
|
134 |
+
updateMessage = async (args: { index: number; message: Partial<ConversationMessage> }) => {
|
135 |
const prev = await poll(() => this.data.messages[args.index], { interval: 10, maxAttempts: 200 });
|
136 |
|
137 |
if (!prev) return;
|
|
|
144 |
...this.data.messages.slice(args.index + 1),
|
145 |
],
|
146 |
});
|
147 |
+
};
|
148 |
|
149 |
+
deleteMessage = async (idx: number) => {
|
150 |
const imgKeys = this.data.messages.flatMap(m => m.images).filter(isString);
|
151 |
await Promise.all([
|
152 |
...imgKeys.map(k => images.delete(k)),
|
|
|
155 |
messages: this.data.messages.slice(0, idx),
|
156 |
}),
|
157 |
]);
|
158 |
+
};
|
159 |
|
160 |
+
deleteMessages = async (from: number) => {
|
161 |
const sliced = this.data.messages.slice(0, from);
|
162 |
const notSliced = this.data.messages.slice(from);
|
163 |
|
|
|
169 |
messages: sliced,
|
170 |
}),
|
171 |
]);
|
172 |
+
};
|
173 |
|
174 |
+
genNextMessage = async () => {
|
175 |
this.generating = true;
|
176 |
const startTime = performance.now();
|
177 |
|
|
|
223 |
const endTime = performance.now();
|
224 |
this.generationStats.latency = Math.round(endTime - startTime);
|
225 |
this.generating = false;
|
226 |
+
};
|
227 |
|
228 |
stopGenerating = () => {
|
229 |
this.abortManager.abortAll();
|
|
|
236 |
generationStats = $derived(this.active.map(c => c.generationStats));
|
237 |
loaded = $state(false);
|
238 |
|
239 |
+
#active = $derived.by(() => this.for(projects.activeId));
|
240 |
|
241 |
init = createInit(() => {
|
242 |
const searchParams = new URLSearchParams(window.location.search);
|
|
|
271 |
return this.#active;
|
272 |
}
|
273 |
|
274 |
+
create = async (
|
275 |
+
args: { projectId: ProjectEntity["id"]; modelId?: Model["id"] } & Partial<ConversationEntityMembers>
|
276 |
+
) => {
|
277 |
const conv = snapshot({
|
278 |
...getDefaultConversation(args.projectId),
|
279 |
...args,
|
|
|
288 |
};
|
289 |
|
290 |
return id;
|
291 |
+
};
|
292 |
|
293 |
+
for = (projectId: ProjectEntity["id"]): ConversationClass[] => {
|
294 |
// Async load from db
|
295 |
if (!this.#conversations[projectId]?.length) {
|
296 |
conversationsRepo.find({ where: { projectId } }).then(c => {
|
|
|
312 |
return res.slice(0, 2).toSorted((a, b) => {
|
313 |
return a.data.createdAt.getTime() - b.data.createdAt.getTime();
|
314 |
});
|
315 |
+
};
|
316 |
|
317 |
+
delete = async ({ id, projectId }: ConversationEntityMembers) => {
|
318 |
if (!id) return;
|
319 |
|
320 |
await conversationsRepo.delete(id);
|
321 |
|
322 |
const prev = this.#conversations[projectId] ?? [];
|
323 |
this.#conversations = { ...this.#conversations, [projectId]: prev.filter(c => c.data.id != id) };
|
324 |
+
};
|
325 |
|
326 |
+
deleteAllFrom = async (projectId: string) => {
|
327 |
this.for(projectId).forEach(c => this.delete(c.data));
|
328 |
+
};
|
329 |
|
330 |
+
reset = async () => {
|
331 |
+
await Promise.allSettled(this.active.map(c => this.delete(c.data)));
|
332 |
this.create(getDefaultConversation(projects.activeId));
|
333 |
+
};
|
334 |
|
335 |
+
migrate = async (from: ProjectEntity["id"], to: ProjectEntity["id"]) => {
|
336 |
const fromArr = this.#conversations[from] ?? [];
|
337 |
await Promise.allSettled(fromArr.map(c => c.update({ projectId: to })));
|
338 |
this.#conversations = {
|
|
|
340 |
[to]: [...fromArr],
|
341 |
[from]: [],
|
342 |
};
|
343 |
+
};
|
344 |
|
345 |
+
duplicate = async (from: ProjectEntity["id"], to: ProjectEntity["id"]) => {
|
346 |
const fromArr = this.#conversations[from] ?? [];
|
347 |
await Promise.allSettled(
|
348 |
fromArr.map(async c => {
|
349 |
conversations.create({ ...c.data, projectId: to });
|
350 |
})
|
351 |
);
|
352 |
+
};
|
353 |
|
354 |
+
genNextMessages = async (conv: "left" | "right" | "both" | ConversationClass = "both") => {
|
355 |
if (!token.value) {
|
356 |
token.showModal = true;
|
357 |
return;
|
|
|
404 |
addToast({ title: "Error", description: "An unknown error occurred", variant: "error" });
|
405 |
}
|
406 |
}
|
407 |
+
};
|
408 |
|
409 |
stopGenerating = () => {
|
410 |
this.active.forEach(c => c.abortManager.abortAll());
|