|
@@ -1,6 +1,6 @@
|
|
|
import { Billing } from "@opencode-ai/console-core/billing.js"
|
|
import { Billing } from "@opencode-ai/console-core/billing.js"
|
|
|
import { createAsync, query, useParams } from "@solidjs/router"
|
|
import { createAsync, query, useParams } from "@solidjs/router"
|
|
|
-import { createMemo, For, Show, createEffect } from "solid-js"
|
|
|
|
|
|
|
+import { createMemo, For, Show, createEffect, createSignal } from "solid-js"
|
|
|
import { formatDateUTC, formatDateForTable } from "../common"
|
|
import { formatDateUTC, formatDateForTable } from "../common"
|
|
|
import { withActor } from "~/context/auth.withActor"
|
|
import { withActor } from "~/context/auth.withActor"
|
|
|
import { IconChevronLeft, IconChevronRight } from "~/component/icon"
|
|
import { IconChevronLeft, IconChevronRight } from "~/component/icon"
|
|
@@ -22,15 +22,34 @@ export function UsageSection() {
|
|
|
const params = useParams()
|
|
const params = useParams()
|
|
|
const usage = createAsync(() => queryUsageInfo(params.id!, 0))
|
|
const usage = createAsync(() => queryUsageInfo(params.id!, 0))
|
|
|
const [store, setStore] = createStore({ page: 0, usage: [] as Awaited<ReturnType<typeof getUsageInfo>> })
|
|
const [store, setStore] = createStore({ page: 0, usage: [] as Awaited<ReturnType<typeof getUsageInfo>> })
|
|
|
|
|
+ const [openBreakdownId, setOpenBreakdownId] = createSignal<string | null>(null)
|
|
|
|
|
|
|
|
createEffect(() => {
|
|
createEffect(() => {
|
|
|
setStore({ usage: usage() })
|
|
setStore({ usage: usage() })
|
|
|
}, [usage])
|
|
}, [usage])
|
|
|
|
|
|
|
|
|
|
+ createEffect(() => {
|
|
|
|
|
+ if (!openBreakdownId()) return
|
|
|
|
|
+
|
|
|
|
|
+ const handleClickOutside = (e: MouseEvent) => {
|
|
|
|
|
+ const target = e.target as HTMLElement
|
|
|
|
|
+ if (!target.closest('[data-slot="tokens-with-breakdown"]')) {
|
|
|
|
|
+ setOpenBreakdownId(null)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ document.addEventListener("click", handleClickOutside)
|
|
|
|
|
+ return () => document.removeEventListener("click", handleClickOutside)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
const hasResults = createMemo(() => store.usage && store.usage.length > 0)
|
|
const hasResults = createMemo(() => store.usage && store.usage.length > 0)
|
|
|
const canGoPrev = createMemo(() => store.page > 0)
|
|
const canGoPrev = createMemo(() => store.page > 0)
|
|
|
const canGoNext = createMemo(() => store.usage && store.usage.length === PAGE_SIZE)
|
|
const canGoNext = createMemo(() => store.usage && store.usage.length === PAGE_SIZE)
|
|
|
|
|
|
|
|
|
|
+ const calculateTotalInputTokens = (u: Awaited<ReturnType<typeof getUsageInfo>>[0]) => {
|
|
|
|
|
+ return u.inputTokens + (u.cacheReadTokens ?? 0) + (u.cacheWrite5mTokens ?? 0) + (u.cacheWrite1hTokens ?? 0)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
const goPrev = async () => {
|
|
const goPrev = async () => {
|
|
|
const usage = await getUsageInfo(params.id!, store.page - 1)
|
|
const usage = await getUsageInfo(params.id!, store.page - 1)
|
|
|
setStore({
|
|
setStore({
|
|
@@ -73,15 +92,53 @@ export function UsageSection() {
|
|
|
</thead>
|
|
</thead>
|
|
|
<tbody>
|
|
<tbody>
|
|
|
<For each={store.usage}>
|
|
<For each={store.usage}>
|
|
|
- {(usage) => {
|
|
|
|
|
|
|
+ {(usage, index) => {
|
|
|
const date = createMemo(() => new Date(usage.timeCreated))
|
|
const date = createMemo(() => new Date(usage.timeCreated))
|
|
|
|
|
+ const totalInputTokens = createMemo(() => calculateTotalInputTokens(usage))
|
|
|
|
|
+ const breakdownId = `breakdown-${index()}`
|
|
|
|
|
+ const isOpen = createMemo(() => openBreakdownId() === breakdownId)
|
|
|
|
|
+ const isClaude = usage.model.toLowerCase().includes("claude")
|
|
|
return (
|
|
return (
|
|
|
<tr>
|
|
<tr>
|
|
|
<td data-slot="usage-date" title={formatDateUTC(date())}>
|
|
<td data-slot="usage-date" title={formatDateUTC(date())}>
|
|
|
{formatDateForTable(date())}
|
|
{formatDateForTable(date())}
|
|
|
</td>
|
|
</td>
|
|
|
<td data-slot="usage-model">{usage.model}</td>
|
|
<td data-slot="usage-model">{usage.model}</td>
|
|
|
- <td data-slot="usage-tokens">{usage.inputTokens}</td>
|
|
|
|
|
|
|
+ <td data-slot="usage-tokens">
|
|
|
|
|
+ <div data-slot="tokens-with-breakdown" onClick={(e) => e.stopPropagation()}>
|
|
|
|
|
+ <button
|
|
|
|
|
+ data-slot="breakdown-button"
|
|
|
|
|
+ onClick={(e) => {
|
|
|
|
|
+ e.stopPropagation()
|
|
|
|
|
+ setOpenBreakdownId(isOpen() ? null : breakdownId)
|
|
|
|
|
+ }}
|
|
|
|
|
+ >
|
|
|
|
|
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
|
|
|
+ <circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.5" />
|
|
|
|
|
+ <path d="M8 4V8L11 11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
|
|
|
|
+ </svg>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <span onClick={() => setOpenBreakdownId(null)}>{totalInputTokens()}</span>
|
|
|
|
|
+ <Show when={isOpen()}>
|
|
|
|
|
+ <div data-slot="breakdown-popup" onClick={(e) => e.stopPropagation()}>
|
|
|
|
|
+ <div data-slot="breakdown-row">
|
|
|
|
|
+ <span data-slot="breakdown-label">Input</span>
|
|
|
|
|
+ <span data-slot="breakdown-value">{usage.inputTokens}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div data-slot="breakdown-row">
|
|
|
|
|
+ <span data-slot="breakdown-label">Cache Read</span>
|
|
|
|
|
+ <span data-slot="breakdown-value">{usage.cacheReadTokens ?? 0}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <Show when={isClaude}>
|
|
|
|
|
+ <div data-slot="breakdown-row">
|
|
|
|
|
+ <span data-slot="breakdown-label">Cache Write</span>
|
|
|
|
|
+ <span data-slot="breakdown-value">{usage.cacheWrite5mTokens ?? 0}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </Show>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </Show>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </td>
|
|
|
<td data-slot="usage-tokens">{usage.outputTokens}</td>
|
|
<td data-slot="usage-tokens">{usage.outputTokens}</td>
|
|
|
<td data-slot="usage-cost">${((usage.cost ?? 0) / 100000000).toFixed(4)}</td>
|
|
<td data-slot="usage-cost">${((usage.cost ?? 0) / 100000000).toFixed(4)}</td>
|
|
|
</tr>
|
|
</tr>
|