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

129 lines
3.3 KiB
Svelte

<script lang="ts">
import SendHorizontalIcon from "@lucide/svelte/icons/send-horizontal";
import { toast } from "svelte-sonner";
import type { ApiClient } from "$lib/api/client";
import { Button } from "$lib/components/ui/button";
import * as Select from "$lib/components/ui/select";
let {
client,
conversationId,
disabled = false,
}: {
client: ApiClient;
conversationId: string;
disabled?: boolean;
} = $props();
type Mode = "message" | "inject" | "urgent";
const MODES: { value: Mode; label: string; hint: string }[] = [
{
hint: "as the user, mirrored to Telegram",
label: "Message",
value: "message",
},
{
hint: "system note, rides with the next turn",
label: "Inject",
value: "inject",
},
{
hint: "interrupts the running turn",
label: "Urgent inject",
value: "urgent",
},
];
let mode = $state<Mode>("message");
let text = $state("");
let busy = $state(false);
const current = $derived(MODES.find((m) => m.value === mode) ?? MODES[0]);
async function send() {
const body = text.trim();
if (!body || busy) {
return;
}
busy = true;
try {
if (mode === "message") {
await client.postMessage(conversationId, body);
} else {
await client.inject(
conversationId,
body,
mode === "urgent" ? "urgent" : "normal"
);
}
text = "";
} catch (error) {
toast.error(error instanceof Error ? error.message : String(error));
} finally {
busy = false;
}
}
function onKeydown(event: KeyboardEvent) {
if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
send();
}
}
</script>
<form
class="flex flex-col gap-2 border-t bg-background px-3 py-2"
onsubmit={(event) => {
event.preventDefault();
send();
}}
>
<textarea
aria-label="Message"
class="min-h-16 w-full resize-y 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"
{disabled}
onkeydown={onKeydown}
placeholder={mode === "message"
? "Say something to the agent…"
: "Text of the inject…"}
rows="2"
bind:value={text}
></textarea>
<div class="flex items-center gap-2">
<Select.Root
onValueChange={(value) => {
mode = value as Mode;
}}
type="single"
value={mode}
>
<Select.Trigger class="h-8 text-xs" size="sm"
>{current.label}</Select.Trigger
>
<Select.Content>
{#each MODES 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>
<span class="hidden text-muted-foreground text-xs @md:inline"
>{current.hint}</span
>
<Button
class="ml-auto"
disabled={disabled || busy || !text.trim()}
size="sm"
type="submit"
>
<SendHorizontalIcon class="size-4" />
Send
<kbd class="hidden text-[10px] opacity-70 @md:inline">⌘↩</kbd>
</Button>
</div>
</form>