feat(ui,api,admin): sveltekit admin spa, usage by day/agent/conversation/model, rate limits, memory tree, turn snapshot

This commit is contained in:
hh committed 2026-08-28 22:14:15 +02:00
1 parent 61562e947d
commit 98796a82c6
158 files changed
+8459 -2531

No files matched your search

+96
View File
@@ -0,0 +1,96 @@
<script lang="ts">
import MessageSquareIcon from "@lucide/svelte/icons/message-square";
import StatusPill from "$lib/components/status-pill.svelte";
import {
elapsedMs,
fmtDuration,
fmtMoney,
fmtTime,
fmtTokens,
} from "$lib/format";
import { cn } from "$lib/utils";
import type { Turn } from "./activity.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 originLabel = $derived(
turn.itemOrigin && turn.itemOrigin !== turn.origin
? `${turn.origin} · ${turn.itemOrigin}`
: turn.origin
);
const toolCount = $derived(Object.keys(turn.nodes).length);
</script>
<article
class={cn(
"flex animate-rise flex-col gap-2 border-b py-3",
turn.status === "running" && "bg-signal/[0.03]"
)}
>
<header class="flex flex-wrap items-center gap-x-3 gap-y-1 px-1 text-xs">
<StatusPill status={turn.status} />
<span
class={cn(
"font-medium",
turn.origin === "inject" ? "text-note" : "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"}
<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}
<p
class="mx-1 rounded-md bg-muted/50 px-2.5 py-1.5 text-sm whitespace-pre-wrap"
>
{turn.userText}
</p>
{/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)}
<p class="mx-1 flex gap-2 rounded-md bg-primary/8 px-2.5 py-1.5 text-sm">
<MessageSquareIcon class="mt-0.5 size-3.5 shrink-0 text-link" />
<span class="whitespace-pre-wrap">{text}</span>
</p>
{/each}
{#if turn.text}
<p class="mx-1 px-1 text-sm whitespace-pre-wrap">{turn.text}</p>
{:else if turn.status === "running" && 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>