132 lines
4.0 KiB
Svelte
132 lines
4.0 KiB
Svelte
<script lang="ts">
|
|
import { toast } from "svelte-sonner";
|
|
import type { ApiClient } from "$lib/api/client";
|
|
import type { ConversationSummary } from "$lib/api/types";
|
|
import { Button } from "$lib/components/ui/button";
|
|
import * as Dialog from "$lib/components/ui/dialog";
|
|
import { Input } from "$lib/components/ui/input";
|
|
import { Label } from "$lib/components/ui/label";
|
|
import * as Select from "$lib/components/ui/select";
|
|
import { shortId } from "$lib/format";
|
|
|
|
let {
|
|
client,
|
|
parent,
|
|
open = $bindable(false),
|
|
onOpened,
|
|
}: {
|
|
client: ApiClient;
|
|
parent: Pick<ConversationSummary, "id" | "title" | "kind">;
|
|
open?: boolean;
|
|
onOpened: (id: string) => void;
|
|
} = $props();
|
|
|
|
const SEEDS = [
|
|
{ hint: "empty context", label: "clean", value: "clean" },
|
|
{ hint: "the morning handout", label: "morning", value: "morning" },
|
|
{ hint: "a copy of this history", label: "copy", value: "copy" },
|
|
{ hint: "your text as the seed", label: "brief", value: "brief" },
|
|
];
|
|
|
|
let seed = $state("clean");
|
|
let title = $state("");
|
|
let text = $state("");
|
|
let busy = $state(false);
|
|
|
|
const current = $derived(SEEDS.find((s) => s.value === seed) ?? SEEDS[0]);
|
|
const parentName = $derived(
|
|
parent.title || `${parent.kind} ${shortId(parent.id)}`
|
|
);
|
|
|
|
async function branch() {
|
|
busy = true;
|
|
try {
|
|
const child = await client.branch(parent.id, {
|
|
seed,
|
|
text: text || undefined,
|
|
title: title || undefined,
|
|
});
|
|
open = false;
|
|
title = "";
|
|
text = "";
|
|
toast.success("Branch opened");
|
|
onOpened(child.id);
|
|
} catch (error) {
|
|
toast.error(error instanceof Error ? error.message : String(error));
|
|
} finally {
|
|
busy = false;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<Dialog.Root bind:open>
|
|
<Dialog.Content>
|
|
<Dialog.Header>
|
|
<Dialog.Title>Branch off {parentName}</Dialog.Title>
|
|
<Dialog.Description>
|
|
A branch gets its own window and session and ends with a merge back into
|
|
this thread.
|
|
</Dialog.Description>
|
|
</Dialog.Header>
|
|
<div class="flex flex-col gap-3">
|
|
<div class="flex flex-col gap-1.5">
|
|
<Label>Seed</Label>
|
|
<Select.Root
|
|
onValueChange={(value) => {
|
|
seed = value;
|
|
}}
|
|
type="single"
|
|
value={seed}
|
|
>
|
|
<Select.Trigger class="w-full">
|
|
{current.label}
|
|
<span class="text-muted-foreground">- {current.hint}</span>
|
|
</Select.Trigger>
|
|
<Select.Content>
|
|
{#each SEEDS as option (option.value)}
|
|
<Select.Item label={option.label} value={option.value}>
|
|
<span class="flex flex-col">
|
|
<span>{option.label}</span>
|
|
<span class="text-muted-foreground text-xs">
|
|
{option.hint}
|
|
</span>
|
|
</span>
|
|
</Select.Item>
|
|
{/each}
|
|
</Select.Content>
|
|
</Select.Root>
|
|
</div>
|
|
<div class="flex flex-col gap-1.5">
|
|
<Label for="branch-title">Title</Label>
|
|
<Input id="branch-title" placeholder="optional" bind:value={title} />
|
|
</div>
|
|
<div class="flex flex-col gap-1.5">
|
|
<Label for="branch-text">First message</Label>
|
|
<textarea
|
|
class="min-h-20 rounded-md border bg-input/30 px-3 py-2 text-sm focus-visible:border-ring focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-ring/30"
|
|
id="branch-text"
|
|
placeholder={seed === "brief" ? "required for a brief seed" : "optional"}
|
|
bind:value={text}
|
|
></textarea>
|
|
</div>
|
|
</div>
|
|
<Dialog.Footer>
|
|
<Button
|
|
onclick={() => {
|
|
// biome-ignore lint/suspicious/noGlobalAssign: bindable prop, not window.open
|
|
open = false;
|
|
}}
|
|
variant="ghost"
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
disabled={busy || (seed === "brief" && !text.trim())}
|
|
onclick={branch}
|
|
>
|
|
Open branch
|
|
</Button>
|
|
</Dialog.Footer>
|
|
</Dialog.Content>
|
|
</Dialog.Root>
|