inference-playground / src /lib /state /checkpoints.svelte.ts
Thomas G. Lopes
fix reset; add some tests (#90)
893b0be unverified
import { idb } from "$lib/remult.js";
import { snapshot } from "$lib/utils/object.svelte";
import { dequal } from "dequal";
import { Entity, Fields, repo } from "remult";
import { conversations, type ConversationEntityMembers } from "./conversations.svelte";
import { ProjectEntity, projects } from "./projects.svelte";
@Entity("checkpoint")
export class Checkpoint {
@Fields.cuid()
id!: string;
@Fields.boolean()
favorite: boolean = false;
@Fields.createdAt()
timestamp!: Date;
@Fields.json()
conversations: ConversationEntityMembers[] = [];
@Fields.string()
projectId!: string;
}
const checkpointsRepo = repo(Checkpoint, idb);
class Checkpoints {
#checkpoints: Record<ProjectEntity["id"], Checkpoint[]> = $state({});
for(projectId: ProjectEntity["id"]) {
// Async load from db
checkpointsRepo
.find({
where: {
projectId,
},
})
.then(c => {
// Dequal to avoid infinite loops
if (dequal(c, this.#checkpoints[projectId])) return;
this.#checkpoints[projectId] = c;
});
return (
this.#checkpoints[projectId]?.toSorted((a, b) => {
const aTime = a.timestamp?.getTime() ?? new Date().getTime();
const bTime = b.timestamp?.getTime() ?? new Date().getTime();
return bTime - aTime;
}) ?? []
);
}
async commit(projectId: ProjectEntity["id"]) {
const project = projects.all.find(p => p.id == projectId);
if (!project) return;
const newCheckpoint = await checkpointsRepo.save(
snapshot({
conversations: conversations.for(project.id).map(c => c.data),
timestamp: new Date(),
projectId: project.id,
})
);
// Hack because dates are formatted to string by save
newCheckpoint.conversations.forEach((c, i) => {
newCheckpoint.conversations[i] = { ...c, createdAt: new Date(c.createdAt) };
});
const prev: Checkpoint[] = this.#checkpoints[projectId] ?? [];
this.#checkpoints[projectId] = [...prev, newCheckpoint];
}
restore(checkpoint: Checkpoint) {
const cloned = snapshot(checkpoint);
const modified = {
...cloned,
conversations: cloned.conversations.map(c => ({
...c,
projectId: cloned.projectId,
})),
};
const project = projects.all.find(p => p.id == modified.projectId);
if (!project) return;
projects.activeId = modified.projectId;
// conversations.deleteAllFrom(cloned.projectId);
const prev = conversations.for(modified.projectId);
modified.conversations.forEach((c, i) => {
const prevC = prev[i];
if (prevC) return prevC.update({ ...c });
conversations.create({
...c,
projectId: modified.projectId,
});
});
if (modified.conversations.length < prev.length) {
prev.forEach((p, i) => {
if (i < modified.conversations.length) return;
conversations.delete(p.data);
});
}
}
async toggleFavorite({ id, projectId }: Checkpoint) {
if (!id) return;
const p = await checkpointsRepo.findFirst({ id });
if (!p) return;
await checkpointsRepo.update(id, { favorite: !p.favorite });
const prev: Checkpoint[] = snapshot(this.#checkpoints[projectId] ?? []);
this.#checkpoints[projectId] = prev.map(c => {
if (c.id !== id) return c;
return { ...c, favorite: !c.favorite };
});
}
async delete({ id, projectId }: Checkpoint) {
if (!id) return;
await checkpointsRepo.delete(id);
const prev: Checkpoint[] = this.#checkpoints[projectId] ?? [];
this.#checkpoints[projectId] = prev.filter(c => c.id != id);
}
async clear(projectId: ProjectEntity["id"]) {
await checkpointsRepo.deleteMany({ where: { projectId } });
this.#checkpoints[projectId] = [];
}
async migrate(from: ProjectEntity["id"], to: ProjectEntity["id"]) {
await checkpointsRepo.updateMany({ where: { projectId: from }, set: { projectId: to } });
const fromArr = snapshot(this.#checkpoints[from] ?? []);
this.#checkpoints[to] = [
...fromArr.map(c => ({
...c,
projectId: to,
conversations: c.conversations.map(cn => ({ ...cn, projectId: to })),
})),
];
this.#checkpoints[from] = [];
}
}
export const checkpoints = new Checkpoints();