Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
Thomas G. Lopes
commited on
branching wip (#98)
Browse files
src/lib/components/inference-playground/message.svelte
CHANGED
@@ -22,6 +22,9 @@
|
|
22 |
import { parseThinkingTokens } from "$lib/utils/thinking.js";
|
23 |
import IconChevronDown from "~icons/carbon/chevron-down";
|
24 |
import IconChevronRight from "~icons/carbon/chevron-right";
|
|
|
|
|
|
|
25 |
|
26 |
type Props = {
|
27 |
conversation: ConversationClass;
|
@@ -341,6 +344,34 @@
|
|
341 |
</div>
|
342 |
</Tooltip>
|
343 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
344 |
<Tooltip>
|
345 |
{#snippet trigger(tooltip)}
|
346 |
<button
|
@@ -399,3 +430,23 @@
|
|
399 |
{/each}
|
400 |
</div>
|
401 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
22 |
import { parseThinkingTokens } from "$lib/utils/thinking.js";
|
23 |
import IconChevronDown from "~icons/carbon/chevron-down";
|
24 |
import IconChevronRight from "~icons/carbon/chevron-right";
|
25 |
+
import ArrowSplitRounded from "~icons/material-symbols/arrow-split-rounded";
|
26 |
+
import { addToast } from "$lib/components/toaster.svelte.js";
|
27 |
+
import { projects } from "$lib/state/projects.svelte";
|
28 |
|
29 |
type Props = {
|
30 |
conversation: ConversationClass;
|
|
|
344 |
</div>
|
345 |
</Tooltip>
|
346 |
|
347 |
+
<Tooltip>
|
348 |
+
{#snippet trigger(tooltip)}
|
349 |
+
<button
|
350 |
+
tabindex="0"
|
351 |
+
onclick={async () => {
|
352 |
+
try {
|
353 |
+
await projects.branch(projects.activeId, index);
|
354 |
+
} catch (error) {
|
355 |
+
addToast({
|
356 |
+
title: "Error",
|
357 |
+
description: error instanceof Error ? error.message : "Failed to create branch",
|
358 |
+
variant: "error",
|
359 |
+
});
|
360 |
+
}
|
361 |
+
}}
|
362 |
+
type="button"
|
363 |
+
class="grid size-7 place-items-center border border-gray-200 bg-white text-xs font-medium text-gray-900 hover:bg-gray-100
|
364 |
+
hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100
|
365 |
+
focus:outline-hidden dark:border-gray-600 dark:bg-gray-800
|
366 |
+
dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700"
|
367 |
+
{...tooltip.trigger}
|
368 |
+
>
|
369 |
+
<ArrowSplitRounded />
|
370 |
+
</button>
|
371 |
+
{/snippet}
|
372 |
+
Branch from here
|
373 |
+
</Tooltip>
|
374 |
+
|
375 |
<Tooltip>
|
376 |
{#snippet trigger(tooltip)}
|
377 |
<button
|
|
|
430 |
{/each}
|
431 |
</div>
|
432 |
</div>
|
433 |
+
|
434 |
+
{#if projects.current?.branchedFromId && projects.current?.branchedFromMessageIndex === index}
|
435 |
+
<div class="mt-4 flex items-center justify-center">
|
436 |
+
<div
|
437 |
+
class="flex items-center gap-1 rounded-full bg-gray-100 px-3 py-1.5 text-sm text-gray-600 dark:bg-gray-800 dark:text-gray-400"
|
438 |
+
>
|
439 |
+
<ArrowSplitRounded class="mr-1 size-4" />
|
440 |
+
<span>Branched from</span>
|
441 |
+
<button
|
442 |
+
onclick={() => {
|
443 |
+
if (!projects.current?.branchedFromId) return;
|
444 |
+
projects.activeId = projects.current.branchedFromId;
|
445 |
+
}}
|
446 |
+
class="font-medium text-blue-600 underline hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
|
447 |
+
>
|
448 |
+
{projects.getBranchedFromProject(projects.current.id)?.name || "original project"}
|
449 |
+
</button>
|
450 |
+
</div>
|
451 |
+
</div>
|
452 |
+
{/if}
|
src/lib/components/inference-playground/project-select.svelte
CHANGED
@@ -10,6 +10,7 @@
|
|
10 |
import IconHistory from "~icons/carbon/recently-viewed";
|
11 |
import IconSave from "~icons/carbon/save";
|
12 |
import IconDelete from "~icons/carbon/trash-can";
|
|
|
13 |
import Dialog from "../dialog.svelte";
|
14 |
import { prompt } from "../prompts.svelte";
|
15 |
import Tooltip from "../tooltip.svelte";
|
@@ -114,7 +115,21 @@
|
|
114 |
>
|
115 |
<div class="flex items-center gap-2">
|
116 |
{name}
|
117 |
-
{#if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
118 |
<div
|
119 |
class="text-3xs grid aspect-square place-items-center rounded bg-yellow-300 p-0.5 text-yellow-700 dark:bg-yellow-400/25 dark:text-yellow-400"
|
120 |
aria-label="Project has checkpoints"
|
|
|
10 |
import IconHistory from "~icons/carbon/recently-viewed";
|
11 |
import IconSave from "~icons/carbon/save";
|
12 |
import IconDelete from "~icons/carbon/trash-can";
|
13 |
+
import ArrowSplitRounded from "~icons/material-symbols/arrow-split-rounded";
|
14 |
import Dialog from "../dialog.svelte";
|
15 |
import { prompt } from "../prompts.svelte";
|
16 |
import Tooltip from "../tooltip.svelte";
|
|
|
115 |
>
|
116 |
<div class="flex items-center gap-2">
|
117 |
{name}
|
118 |
+
{#if projects.all.find(p => p.id === id)?.branchedFromId}
|
119 |
+
{@const originalProject = projects.getBranchedFromProject(id)}
|
120 |
+
<Tooltip>
|
121 |
+
{#snippet trigger(tooltip)}
|
122 |
+
<div
|
123 |
+
class="text-3xs grid aspect-square place-items-center rounded bg-blue-300 p-0.5 text-blue-700 dark:bg-blue-400/25 dark:text-blue-400"
|
124 |
+
aria-label="Branched project"
|
125 |
+
{...tooltip.trigger}
|
126 |
+
>
|
127 |
+
<ArrowSplitRounded />
|
128 |
+
</div>
|
129 |
+
{/snippet}
|
130 |
+
Branched from {originalProject?.name || "unknown project"}
|
131 |
+
</Tooltip>
|
132 |
+
{:else if hasCheckpoints}
|
133 |
<div
|
134 |
class="text-3xs grid aspect-square place-items-center rounded bg-yellow-300 p-0.5 text-yellow-700 dark:bg-yellow-400/25 dark:text-yellow-400"
|
135 |
aria-label="Project has checkpoints"
|
src/lib/state/conversations.svelte.ts
CHANGED
@@ -157,9 +157,24 @@ export class ConversationClass {
|
|
157 |
});
|
158 |
};
|
159 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
160 |
deleteMessage = async (idx: number) => {
|
161 |
if (!this.data.messages) return;
|
162 |
const imgKeys = this.data.messages.flatMap(m => m.images).filter(isString);
|
|
|
|
|
|
|
|
|
163 |
await Promise.all([
|
164 |
...imgKeys.map(k => images.delete(k)),
|
165 |
this.update({
|
@@ -174,6 +189,9 @@ export class ConversationClass {
|
|
174 |
const sliced = this.data.messages.slice(0, from);
|
175 |
const notSliced = this.data.messages.slice(from);
|
176 |
|
|
|
|
|
|
|
177 |
const imgKeys = notSliced.flatMap(m => m.images).filter(isString);
|
178 |
await Promise.all([
|
179 |
...imgKeys.map(k => images.delete(k)),
|
@@ -380,6 +398,42 @@ class Conversations {
|
|
380 |
);
|
381 |
};
|
382 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
383 |
genNextMessages = async (conv: "left" | "right" | "both" | ConversationClass = "both") => {
|
384 |
if (!token.value) {
|
385 |
token.showModal = true;
|
|
|
157 |
});
|
158 |
};
|
159 |
|
160 |
+
checkAndClearBranchStatus = async (deletionIndex: number) => {
|
161 |
+
const currentProject = projects.current;
|
162 |
+
|
163 |
+
if (!currentProject?.branchedFromId || typeof currentProject?.branchedFromMessageIndex !== "number") return;
|
164 |
+
|
165 |
+
// If we're deleting messages at or before the branch point, clear branch status
|
166 |
+
if (deletionIndex <= currentProject.branchedFromMessageIndex) {
|
167 |
+
await projects.clearBranchStatus(currentProject.id);
|
168 |
+
}
|
169 |
+
};
|
170 |
+
|
171 |
deleteMessage = async (idx: number) => {
|
172 |
if (!this.data.messages) return;
|
173 |
const imgKeys = this.data.messages.flatMap(m => m.images).filter(isString);
|
174 |
+
|
175 |
+
// Check if we need to clear branch status
|
176 |
+
await this.checkAndClearBranchStatus(idx);
|
177 |
+
|
178 |
await Promise.all([
|
179 |
...imgKeys.map(k => images.delete(k)),
|
180 |
this.update({
|
|
|
189 |
const sliced = this.data.messages.slice(0, from);
|
190 |
const notSliced = this.data.messages.slice(from);
|
191 |
|
192 |
+
// Check if we need to clear branch status
|
193 |
+
await this.checkAndClearBranchStatus(from);
|
194 |
+
|
195 |
const imgKeys = notSliced.flatMap(m => m.images).filter(isString);
|
196 |
await Promise.all([
|
197 |
...imgKeys.map(k => images.delete(k)),
|
|
|
398 |
);
|
399 |
};
|
400 |
|
401 |
+
duplicateUpToMessage = async (from: ProjectEntity["id"], to: ProjectEntity["id"], messageIndex: number) => {
|
402 |
+
const fromArr = this.#conversations[from] ?? [];
|
403 |
+
|
404 |
+
// Clear any existing conversations for the target project first
|
405 |
+
this.#conversations[to] = [];
|
406 |
+
|
407 |
+
// Delete any existing conversations in the database for this project
|
408 |
+
const existingConversations = await conversationsRepo.find({ where: { projectId: to } });
|
409 |
+
await Promise.all(existingConversations.map(c => conversationsRepo.delete(c.id)));
|
410 |
+
|
411 |
+
const newConversations: ConversationClass[] = [];
|
412 |
+
|
413 |
+
for (const c of fromArr) {
|
414 |
+
// Copy only messages up to the specified index with deep clone
|
415 |
+
const truncatedMessages =
|
416 |
+
c.data.messages?.slice(0, messageIndex + 1).map(msg => ({
|
417 |
+
...msg,
|
418 |
+
images: msg.images ? [...msg.images] : undefined,
|
419 |
+
})) || [];
|
420 |
+
|
421 |
+
const conversationData = {
|
422 |
+
...snapshot(c.data),
|
423 |
+
projectId: to,
|
424 |
+
messages: truncatedMessages,
|
425 |
+
id: undefined, // Let the database generate a new ID
|
426 |
+
};
|
427 |
+
|
428 |
+
// Use conversationsRepo directly to avoid default conversation merging
|
429 |
+
const saved = await conversationsRepo.save(conversationData);
|
430 |
+
newConversations.push(new ConversationClass(saved));
|
431 |
+
}
|
432 |
+
|
433 |
+
// Update the in-memory cache
|
434 |
+
this.#conversations[to] = newConversations;
|
435 |
+
};
|
436 |
+
|
437 |
genNextMessages = async (conv: "left" | "right" | "both" | ConversationClass = "both") => {
|
438 |
if (!token.value) {
|
439 |
token.showModal = true;
|
src/lib/state/projects.svelte.ts
CHANGED
@@ -15,6 +15,12 @@ export class ProjectEntity {
|
|
15 |
|
16 |
@Fields.string()
|
17 |
systemMessage?: string;
|
|
|
|
|
|
|
|
|
|
|
|
|
18 |
}
|
19 |
|
20 |
export type ProjectEntityMembers = MembersOnly<ProjectEntity>;
|
@@ -72,14 +78,6 @@ class Projects {
|
|
72 |
return id;
|
73 |
};
|
74 |
|
75 |
-
setCurrent = async (id: string) => {
|
76 |
-
await checkpoints.migrate(id, this.activeId);
|
77 |
-
conversations.migrate(this.activeId, id).then(() => {
|
78 |
-
this.#activeId.current = id;
|
79 |
-
});
|
80 |
-
this.activeId = id;
|
81 |
-
};
|
82 |
-
|
83 |
get current() {
|
84 |
return this.#projects[this.activeId];
|
85 |
}
|
@@ -105,6 +103,46 @@ class Projects {
|
|
105 |
this.activeId = DEFAULT_PROJECT_ID;
|
106 |
}
|
107 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
108 |
}
|
109 |
|
110 |
export const projects = new Projects();
|
|
|
15 |
|
16 |
@Fields.string()
|
17 |
systemMessage?: string;
|
18 |
+
|
19 |
+
@Fields.string()
|
20 |
+
branchedFromId?: string | null;
|
21 |
+
|
22 |
+
@Fields.number()
|
23 |
+
branchedFromMessageIndex?: number | null;
|
24 |
}
|
25 |
|
26 |
export type ProjectEntityMembers = MembersOnly<ProjectEntity>;
|
|
|
78 |
return id;
|
79 |
};
|
80 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
81 |
get current() {
|
82 |
return this.#projects[this.activeId];
|
83 |
}
|
|
|
103 |
this.activeId = DEFAULT_PROJECT_ID;
|
104 |
}
|
105 |
}
|
106 |
+
|
107 |
+
branch = async (fromProjectId: string, messageIndex: number): Promise<string> => {
|
108 |
+
const fromProject = this.#projects[fromProjectId];
|
109 |
+
if (!fromProject) throw new Error("Source project not found");
|
110 |
+
|
111 |
+
// Create new project with branching info
|
112 |
+
const newProjectId = await this.create({
|
113 |
+
name: `${fromProject.name} (branch)`,
|
114 |
+
systemMessage: fromProject.systemMessage,
|
115 |
+
branchedFromId: fromProjectId,
|
116 |
+
branchedFromMessageIndex: messageIndex,
|
117 |
+
});
|
118 |
+
|
119 |
+
// Copy conversations up to the specified message index
|
120 |
+
await conversations.duplicateUpToMessage(fromProjectId, newProjectId, messageIndex);
|
121 |
+
|
122 |
+
// Switch to the new project
|
123 |
+
this.activeId = newProjectId;
|
124 |
+
|
125 |
+
return newProjectId;
|
126 |
+
};
|
127 |
+
|
128 |
+
getBranchedFromProject = (projectId: string) => {
|
129 |
+
const project = this.#projects[projectId];
|
130 |
+
if (!project?.branchedFromId) return null;
|
131 |
+
|
132 |
+
const originalProject = this.#projects[project.branchedFromId];
|
133 |
+
return originalProject;
|
134 |
+
};
|
135 |
+
|
136 |
+
clearBranchStatus = async (projectId: string) => {
|
137 |
+
const project = this.#projects[projectId];
|
138 |
+
if (!project?.branchedFromId) return;
|
139 |
+
|
140 |
+
await this.update({
|
141 |
+
...project,
|
142 |
+
branchedFromId: null,
|
143 |
+
branchedFromMessageIndex: null,
|
144 |
+
});
|
145 |
+
};
|
146 |
}
|
147 |
|
148 |
export const projects = new Projects();
|