summaryrefslogtreecommitdiffhomepage
path: root/src/features/chat/ui
diff options
context:
space:
mode:
Diffstat (limited to 'src/features/chat/ui')
-rw-r--r--src/features/chat/ui/ChatView.svelte54
1 files changed, 46 insertions, 8 deletions
diff --git a/src/features/chat/ui/ChatView.svelte b/src/features/chat/ui/ChatView.svelte
index 3a078fb..ba6e961 100644
--- a/src/features/chat/ui/ChatView.svelte
+++ b/src/features/chat/ui/ChatView.svelte
@@ -1,17 +1,33 @@
<script lang="ts">
import { groupRenderedChunks, type RenderedChunk } from "../index";
+ import { interleaveTurnMetrics, viewStepMetrics, viewTurnMetrics, type TurnMetricsEntry } from "../../../core/metrics";
- let { chunks }: { chunks: readonly RenderedChunk[] } = $props();
+ let {
+ chunks,
+ turnMetrics = [],
+ }: {
+ chunks: readonly RenderedChunk[];
+ turnMetrics?: readonly TurnMetricsEntry[];
+ } = $props();
const groups = $derived(groupRenderedChunks(chunks));
+ const rows = $derived(interleaveTurnMetrics(groups, turnMetrics));
+
// Stable per-row keys. Thinking blocks get an ordinal key (`think<n>`) that
// survives the provisional→committed (seq null → seq N) transition, so the
// collapse's open/close state is NOT lost when a turn seals. (App isolates
// these keys per conversation via {#key}.)
- const rows = $derived.by(() => {
+ const keyedRows = $derived.by(() => {
let thinking = 0;
- return groups.map((group, i) => {
+ return rows.map((row, i) => {
+ if (row.kind === "step-metrics") {
+ return { row, key: `s${row.step.stepId}` };
+ }
+ if (row.kind === "turn-metrics") {
+ return { row, key: `m${row.turn.turnId}` };
+ }
+ const group = row.group;
let key: string;
if (group.kind === "tool-batch") {
key = `b${group.stepId}`;
@@ -22,7 +38,7 @@
} else {
key = `p${i}`;
}
- return { group, key };
+ return { row, key };
});
});
</script>
@@ -102,9 +118,31 @@
{/snippet}
<div class="flex flex-col gap-2 p-4 pl-6" role="log" aria-live="polite">
- {#each rows as { group, key } (key)}
- {#if group.kind === "single"}
- {@render chunkRow(group.chunk)}
+ {#each keyedRows as { row, key } (key)}
+ {#if row.kind === "step-metrics"}
+ {@const sv = viewStepMetrics(row.step, row.index)}
+ <div class="chat chat-start">
+ <div class="chat-bubble w-full max-w-5xl bg-transparent p-0">
+ <div class="text-xs opacity-70">
+ {sv.label} · {sv.tokensLabel}
+ {#if sv.tps} · {sv.tps}{/if}
+ {#if sv.genTotal} · {sv.genTotal}{/if}
+ </div>
+ </div>
+ </div>
+ {:else if row.kind === "turn-metrics"}
+ {@const turnView = viewTurnMetrics(row.turn)}
+ <div class="chat chat-start">
+ <div class="chat-bubble w-full max-w-5xl bg-transparent p-0">
+ <div class="text-xs opacity-70">
+ turn · {turnView.tokensLabel} ({turnView.breakdown})
+ {#if turnView.tps} · {turnView.tps}{/if}
+ {#if turnView.duration} · {turnView.duration}{/if}
+ </div>
+ </div>
+ </div>
+ {:else if row.group.kind === "single"}
+ {@render chunkRow(row.group.chunk)}
{:else}
<!-- Batched tool calls (one step): a single bubble holding a DaisyUI list,
one row per call paired with its result. Same chat-start grid shim as
@@ -112,7 +150,7 @@
<div class="chat chat-start [&>.chat-bubble]:max-w-full [&>.chat-bubble]:p-0">
<div class="chat-bubble bg-transparent">
<ul class="list w-fit max-w-full rounded-box bg-base-200 text-sm">
- {#each group.entries as entry (entry.call.toolCallId)}
+ {#each row.group.entries as entry (entry.call.toolCallId)}
<li class="list-row">
<div>
<strong>{entry.call.toolName}</strong>