127 lines
3.3 KiB
Svelte
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>
|