feat(ui,api,telegram): shared conversation panel for obsidian and /admin/panel, bind materializes a window, inbox survives link previews
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user