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

110 lines
3.4 KiB
Svelte

<script lang="ts">
import StatusPill from "$lib/components/status-pill.svelte";
import {
elapsedMs,
fmtDuration,
fmtMoney,
fmtTime,
fmtTokens,
} from "$lib/format";
import { cn } from "$lib/utils";
import { isLive, ORIGIN_LABELS, type Turn } from "./activity.svelte";
import Markdown from "./markdown.svelte";
import Reasoning from "./reasoning.svelte";
import SayBubble from "./say-bubble.svelte";
import ToolNodeView from "./tool-node.svelte";
let { turn, now }: { turn: Turn; now: number } = $props();
const duration = $derived(
turn.usage?.duration_ms ?? elapsedMs(turn.startedAt, turn.endedAt, now)
);
const origin = $derived(ORIGIN_LABELS[turn.origin] ?? turn.origin);
const originLabel = $derived(
turn.itemOrigin && turn.itemOrigin !== turn.origin
? `${origin} · ${turn.itemOrigin}`
: origin
);
const pill = $derived(
turn.status !== "running" && isLive(turn) ? "running" : turn.status
);
const toolCount = $derived(Object.keys(turn.nodes).length);
</script>
<article
class={cn(
"flex flex-col gap-2 border-b py-3",
pill === "running" && "bg-signal/[0.03]"
)}
>
<header class="flex flex-wrap items-center gap-x-3 gap-y-1 px-1 text-xs">
{#if turn.origin !== "agent"}
<StatusPill status={pill} />
{/if}
<span
class={cn(
"font-medium",
turn.origin === "inject" && "text-note",
turn.origin === "agent" || turn.origin === "task"
? "text-kind-deep"
: "text-foreground"
)}
>
{originLabel}
</span>
<span class="tabular text-muted-foreground">{fmtTime(turn.startedAt)}</span>
<span class="tabular text-muted-foreground">{fmtDuration(duration)}</span>
{#if toolCount > 0}
<span class="tabular text-muted-foreground">
{toolCount}
tool {toolCount === 1 ? "call" : "calls"}
</span>
{/if}
{#if turn.thinking > 0 && turn.status === "running" && !turn.reasoning}
<span class="text-kind-deep">thinking…</span>
{/if}
{#if turn.usage}
<span class="tabular ml-auto text-muted-foreground">
in {fmtTokens(turn.usage.input)} · out {fmtTokens(turn.usage.output)} ·
cache {fmtTokens(turn.usage.cache_read)}/{fmtTokens(
turn.usage.cache_creation
)}
{#if typeof turn.usage.cost_usd === "number"}
· {fmtMoney(turn.usage.cost_usd)}
{/if}
</span>
{/if}
</header>
{#if turn.userText && turn.origin === "task"}
<p class="mx-1 px-1 text-muted-foreground text-xs">{turn.userText}</p>
{:else if turn.userText}
<Markdown
class="mx-1 rounded-md bg-muted/50 px-2.5 py-1.5"
inert
text={turn.userText}
/>
{/if}
{#if turn.reasoning.trim()}
<Reasoning live={turn.status === "running"} text={turn.reasoning} />
{/if}
{#if turn.roots.length > 0}
<div class="flex flex-col">
{#each turn.roots as rootId (rootId)}
{#if turn.nodes[rootId]}
<ToolNodeView node={turn.nodes[rootId]} nodes={turn.nodes} {now} />
{/if}
{/each}
</div>
{/if}
{#each turn.says as text, index (index)}
<SayBubble {text} />
{/each}
{#if turn.text}
<Markdown class="mx-1 px-1" text={turn.text} />
{:else if turn.status === "running" && turn.origin !== "agent" && turn.roots.length === 0 && turn.thinking === 0}
<p class="mx-1 px-1 text-muted-foreground text-sm">
Waiting for the model…
</p>
{/if}
</article>