Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
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());
|