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

127 lines
3.3 KiB
Svelte

<script lang="ts">
import { onMount, untrack } from "svelte";
import type { ApiClient } from "$lib/api/client";
import ErrorNote from "$lib/components/error-note.svelte";
import ActivityFeed from "./activity-feed.svelte";
import ChatView from "./chat-view.svelte";
import Composer from "./composer.svelte";
import ContextView from "./context-view.svelte";
import { ConversationFeed } from "./conversation.svelte";
import ConversationHeader from "./conversation-header.svelte";
import { usePanelHost } from "./host";
import RawEntries from "./raw-entries.svelte";
let {
client,
id,
href = null,
onOpen,
showTitle = true,
initialView = "chat",
onViewChange,
}: {
client: ApiClient;
id: string;
href?: ((id: string) => string) | null;
onOpen: (id: string) => void;
// Off when a switcher above the thread already names it.
showTitle?: boolean;
// "activity" beside a deep chat's own note: the file is the thread.
initialView?: string;
// The page keeps the open tab in the URL so it survives a reload.
onViewChange?: (view: string) => void;
} = $props();
const host = usePanelHost();
const feed = new ConversationFeed(
() => client,
untrack(() => id),
host.cache ?? null
);
let view = $state(untrack(() => initialView));
let historyKey = $state(0);
onMount(() => {
feed.start();
return () => feed.stop();
});
const info = $derived(feed.model.conversation);
function refresh() {
feed.refresh().catch(() => undefined);
historyKey += 1;
}
$effect(() => {
onViewChange?.(view);
});
$effect(() => {
const { running } = feed.model;
if (!running) {
untrack(() => {
historyKey += 1;
});
}
});
</script>
<div class="@container flex h-full min-h-0 flex-col">
{#if feed.error && !info}
<div class="p-4"><ErrorNote message={feed.error} retry={refresh} /></div>
{:else if !info}
<div class="flex flex-1 items-center justify-center p-4">
<span
class="size-2 animate-pulse-dot rounded-full bg-muted-foreground/40"
></span>
</div>
{:else}
<ConversationHeader
{client}
{href}
{info}
liveDetail={feed.live.detail}
liveState={feed.live.state}
onChanged={refresh}
{onOpen}
{showTitle}
bind:view
/>
<div class="flex min-h-0 flex-1 flex-col">
{#if view === "chat"}
<ChatView
{client}
conversationId={id}
model={feed.model}
refreshKey={historyKey}
/>
{:else if view === "activity"}
<div class="min-h-0 flex-1 overflow-y-auto px-3 py-3 @md:px-6">
<ActivityFeed
{client}
connected={feed.live.state === "open"}
conversationId={id}
model={feed.model}
/>
</div>
{:else if view === "context"}
<div class="min-h-0 flex-1 overflow-y-auto px-3 py-4 @md:px-6">
{#key historyKey}
<ContextView {client} conversationId={id} />
{/key}
</div>
{:else}
<div class="min-h-0 flex-1 overflow-y-auto px-3 py-3 @md:px-6">
<RawEntries {client} conversationId={id} />
</div>
{/if}
<Composer
{client}
conversationId={id}
disabled={info.status !== "open"}
/>
</div>
{/if}
</div>