Files
beaver-gateway/ui/src/lib/panel/branch-dialog.svelte
T

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>