feat(stats): redesign lab usage
This commit is contained in:
parent
262ef4fdc2
commit
91ca75c4dc
3 changed files with 515 additions and 79 deletions
|
|
@ -161,7 +161,7 @@ const en = {
|
||||||
"lab.latestRelease": "Latest release {{date}}",
|
"lab.latestRelease": "Latest release {{date}}",
|
||||||
"lab.shareOfUsage": "{{share}} of recent OpenCode usage",
|
"lab.shareOfUsage": "{{share}} of recent OpenCode usage",
|
||||||
"lab.usageTitle": "{{lab}} token usage",
|
"lab.usageTitle": "{{lab}} token usage",
|
||||||
"lab.usageDescription": "Daily OpenCode token volume over the last two months.",
|
"lab.usageDescription": "Daily token volume.",
|
||||||
"lab.noUsageTitle": "No usage yet",
|
"lab.noUsageTitle": "No usage yet",
|
||||||
"lab.noUsageDescription": "Recent token usage appears here once this lab has activity.",
|
"lab.noUsageDescription": "Recent token usage appears here once this lab has activity.",
|
||||||
"lab.dailyTokenChart": "{{lab}} daily token usage chart",
|
"lab.dailyTokenChart": "{{lab}} daily token usage chart",
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import {
|
||||||
type StatsLabData,
|
type StatsLabData,
|
||||||
} from "@opencode-ai/stats-core/domain/home"
|
} from "@opencode-ai/stats-core/domain/home"
|
||||||
import { createAsync, query, useParams } from "@solidjs/router"
|
import { createAsync, query, useParams } from "@solidjs/router"
|
||||||
import { createMemo, createSignal, For, onMount, Show, type JSX } from "solid-js"
|
import { createMemo, createSignal, createUniqueId, For, onMount, Show, type JSX } from "solid-js"
|
||||||
import { getRequestEvent } from "solid-js/web"
|
import { getRequestEvent } from "solid-js/web"
|
||||||
import { LocaleLinks } from "../../component/locale-links"
|
import { LocaleLinks } from "../../component/locale-links"
|
||||||
import { useI18n } from "../../context/i18n"
|
import { useI18n } from "../../context/i18n"
|
||||||
|
|
@ -270,20 +270,58 @@ function LabOverviewMetric(props: { label: string; value: string }) {
|
||||||
|
|
||||||
function LabUsageSection(props: { lab: ModelCatalogLab; data: StatsLabData | null }) {
|
function LabUsageSection(props: { lab: ModelCatalogLab; data: StatsLabData | null }) {
|
||||||
const i18n = useI18n()
|
const i18n = useI18n()
|
||||||
|
const activeLineClipId = createUniqueId()
|
||||||
|
const activeLineMaskId = createUniqueId()
|
||||||
const [activeIndex, setActiveIndex] = createSignal<number>()
|
const [activeIndex, setActiveIndex] = createSignal<number>()
|
||||||
const usage = createMemo(() => props.data?.usage ?? [])
|
const usage = createMemo(() => props.data?.usage ?? [])
|
||||||
const max = createMemo(() => Math.max(0, ...usage().map((item) => item.tokens)) || 1)
|
const tokenMax = createMemo(() => Math.max(0, ...usage().map((item) => item.tokens)) || 1)
|
||||||
|
const userMax = createMemo(() => Math.max(0, ...usage().map((item) => item.users)) || 1)
|
||||||
|
const linePoints = createMemo(() =>
|
||||||
|
usage().map((point, index) => ({
|
||||||
|
point,
|
||||||
|
x: usagePointX(index, usage().length),
|
||||||
|
y: usageLineY(point.users, userMax()),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
const userLinePath = createMemo(() => usageLinePath(linePoints()))
|
||||||
|
const activeLineBreak = createMemo(() => {
|
||||||
|
const index = activeIndex()
|
||||||
|
if (index === undefined) return undefined
|
||||||
|
const points = linePoints()
|
||||||
|
if (points.length < 2) return undefined
|
||||||
|
return usageColumnBounds(index, points.length)
|
||||||
|
})
|
||||||
|
const activeLineClip = createMemo(() => {
|
||||||
|
const index = activeIndex()
|
||||||
|
if (index === undefined) return undefined
|
||||||
|
const points = linePoints()
|
||||||
|
if (points.length < 2) return undefined
|
||||||
|
return usageColumnInnerBounds(index, points.length)
|
||||||
|
})
|
||||||
|
const monthTicks = createMemo(() => labUsageMonthTicks(usage()))
|
||||||
const activePoint = createMemo(() => {
|
const activePoint = createMemo(() => {
|
||||||
const index = activeIndex()
|
const index = activeIndex()
|
||||||
if (index === undefined) return undefined
|
if (index === undefined) return undefined
|
||||||
return usage()[index]
|
return usage()[index]
|
||||||
})
|
})
|
||||||
|
const activeTooltip = createMemo(() => {
|
||||||
|
const index = activeIndex()
|
||||||
|
const point = activePoint()
|
||||||
|
if (index === undefined || !point) return undefined
|
||||||
|
const bounds = usageColumnBounds(index, usage().length)
|
||||||
|
return {
|
||||||
|
bounds,
|
||||||
|
index,
|
||||||
|
point,
|
||||||
|
userY: linePoints()[index]?.y ?? 100,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="usage" data-section="model-panel">
|
<section id="usage" data-section="model-panel">
|
||||||
<SectionHeading
|
<SectionHeading
|
||||||
href="#usage"
|
href="#usage"
|
||||||
title={i18n.t("lab.usageTitle", { lab: props.lab.name })}
|
title={i18n.t("nav.usage")}
|
||||||
description={i18n.t("lab.usageDescription")}
|
description={i18n.t("lab.usageDescription")}
|
||||||
/>
|
/>
|
||||||
<Show
|
<Show
|
||||||
|
|
@ -292,83 +330,143 @@ function LabUsageSection(props: { lab: ModelCatalogLab; data: StatsLabData | nul
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
data-component="model-usage-chart"
|
data-component="model-usage-chart"
|
||||||
|
data-variant="lab-usage"
|
||||||
data-dense-labels={isLabUsageDense(usage().length) ? "true" : undefined}
|
data-dense-labels={isLabUsageDense(usage().length) ? "true" : undefined}
|
||||||
role="img"
|
role="img"
|
||||||
aria-label={i18n.t("lab.dailyTokenChart", { lab: props.lab.name })}
|
aria-label={`${props.lab.name} daily token volume and unique active user chart`}
|
||||||
style={{ "--model-usage-count": usage().length } as JSX.CSSProperties}
|
style={{ "--model-usage-count": usage().length } as JSX.CSSProperties}
|
||||||
onPointerLeave={(event) => {
|
onPointerLeave={(event) => {
|
||||||
if (event.pointerType === "touch") return
|
if (event.pointerType === "touch") return
|
||||||
setActiveIndex(undefined)
|
setActiveIndex(undefined)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div data-slot="model-usage-axis" aria-hidden="true">
|
<div data-slot="lab-usage-plot">
|
||||||
<For each={usage()}>
|
<Show when={userLinePath()}>
|
||||||
{(point, index) => (
|
{(path) => (
|
||||||
<div
|
<>
|
||||||
data-active={activeIndex() === index() ? "true" : undefined}
|
<svg
|
||||||
data-label-hidden={isLabUsageLabelHidden(index(), usage().length) ? "true" : undefined}
|
data-slot="lab-usage-line"
|
||||||
>
|
data-layer="base"
|
||||||
<span data-slot="model-usage-label">
|
viewBox="0 0 100 100"
|
||||||
<span data-slot="model-usage-total">{formatTokens(point.tokens)}</span>
|
preserveAspectRatio="none"
|
||||||
<span data-slot="model-usage-date">{point.date}</span>
|
aria-hidden="true"
|
||||||
</span>
|
>
|
||||||
</div>
|
<Show
|
||||||
)}
|
when={activeLineBreak()}
|
||||||
</For>
|
fallback={<path data-slot="lab-usage-line-base" d={path()} />}
|
||||||
</div>
|
>
|
||||||
<div data-slot="model-usage-bars">
|
{(lineBreak) => (
|
||||||
<For each={usage()}>
|
<>
|
||||||
{(point, index) => (
|
<defs>
|
||||||
<div
|
<mask id={activeLineMaskId} maskUnits="userSpaceOnUse">
|
||||||
data-slot="model-usage-column"
|
<rect x="0" y="-2" width="100" height="104" fill="white" />
|
||||||
role="button"
|
<rect x={lineBreak().x} y="-2" width={lineBreak().width} height="104" fill="black" />
|
||||||
tabIndex={0}
|
</mask>
|
||||||
aria-label={`${point.date} ${formatTokens(point.tokens)} ${i18n.t("lab.tokens")}`}
|
</defs>
|
||||||
data-active={activeIndex() === index() ? "true" : undefined}
|
<path data-slot="lab-usage-line-base" d={path()} mask={`url(#${activeLineMaskId})`} />
|
||||||
data-muted={activeIndex() !== undefined && activeIndex() !== index() ? "true" : undefined}
|
</>
|
||||||
onPointerDown={(event) => {
|
)}
|
||||||
if (event.pointerType !== "touch") return
|
</Show>
|
||||||
setActiveIndex(index())
|
</svg>
|
||||||
}}
|
<Show when={activeLineClip()}>
|
||||||
onPointerEnter={() => setActiveIndex(index())}
|
{(clip) => (
|
||||||
onPointerMove={(event) => {
|
<svg
|
||||||
if (event.pointerType === "touch") return
|
data-slot="lab-usage-line"
|
||||||
setActiveIndex(index())
|
data-layer="active"
|
||||||
}}
|
viewBox="0 0 100 100"
|
||||||
onClick={() => setActiveIndex(index())}
|
preserveAspectRatio="none"
|
||||||
onFocus={() => setActiveIndex(index())}
|
aria-hidden="true"
|
||||||
onBlur={() => setActiveIndex(undefined)}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key !== "Enter" && event.key !== " ") return
|
|
||||||
event.preventDefault()
|
|
||||||
setActiveIndex(index())
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
data-slot="model-usage-bar"
|
|
||||||
style={{ "--model-usage-fill": `${usageHeight(point.tokens, max())}%` } as JSX.CSSProperties}
|
|
||||||
/>
|
|
||||||
<Show when={activeIndex() === index() && activePoint()}>
|
|
||||||
{(active) => (
|
|
||||||
<div
|
|
||||||
data-component="chart-tooltip"
|
|
||||||
data-placement={index() > usage().length * 0.62 ? "left" : "right"}
|
|
||||||
>
|
>
|
||||||
<strong>{active().date}</strong>
|
<defs>
|
||||||
<span>
|
<clipPath id={activeLineClipId} clipPathUnits="userSpaceOnUse">
|
||||||
{formatTokens(active().tokens)} {i18n.t("lab.tokens")}
|
<rect x={clip().x} y="-2" width={clip().width} height="104" />
|
||||||
</span>
|
</clipPath>
|
||||||
<div data-slot="tooltip-divider" />
|
</defs>
|
||||||
<p>
|
<path data-slot="lab-usage-line-active" d={path()} clip-path={`url(#${activeLineClipId})`} />
|
||||||
<span data-slot="tooltip-label">
|
</svg>
|
||||||
<i /> {i18n.t("lab.dailyTokens")}
|
|
||||||
</span>
|
|
||||||
<b>{formatTokens(active().tokens)}</b>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
<div data-slot="lab-usage-bars">
|
||||||
|
<For each={usage()}>
|
||||||
|
{(point, index) => (
|
||||||
|
<div
|
||||||
|
data-slot="lab-usage-column"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label={`${point.date} ${formatTokens(point.tokens)} ${i18n.t("lab.tokens")}, ${formatUsers(point.users)} ${i18n.t("format.users")}`}
|
||||||
|
data-active={activeIndex() === index() ? "true" : undefined}
|
||||||
|
data-muted={activeIndex() !== undefined && activeIndex() !== index() ? "true" : undefined}
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--lab-usage-token-height": `${usageStripHeight(point.tokens, tokenMax())}px`,
|
||||||
|
"--lab-usage-user-y": `${linePoints()[index()]?.y ?? 100}`,
|
||||||
|
} as JSX.CSSProperties
|
||||||
|
}
|
||||||
|
onPointerDown={(event) => {
|
||||||
|
if (event.pointerType !== "touch") return
|
||||||
|
setActiveIndex(index())
|
||||||
|
}}
|
||||||
|
onPointerEnter={() => setActiveIndex(index())}
|
||||||
|
onPointerMove={(event) => {
|
||||||
|
if (event.pointerType === "touch") return
|
||||||
|
setActiveIndex(index())
|
||||||
|
}}
|
||||||
|
onClick={() => setActiveIndex(index())}
|
||||||
|
onFocus={() => setActiveIndex(index())}
|
||||||
|
onBlur={() => setActiveIndex(undefined)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key !== "Enter" && event.key !== " ") return
|
||||||
|
event.preventDefault()
|
||||||
|
setActiveIndex(index())
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div data-slot="lab-usage-token-bar" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
<Show when={activeTooltip()} keyed>
|
||||||
|
{(active) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-component="chart-tooltip"
|
||||||
|
data-placement={active.index > usage().length * 0.62 ? "left" : "right"}
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--lab-usage-tooltip-left": `${active.bounds.x}%`,
|
||||||
|
"--lab-usage-tooltip-right": `${active.bounds.x + active.bounds.width}%`,
|
||||||
|
"--lab-usage-user-y": `${active.userY}`,
|
||||||
|
} as JSX.CSSProperties
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<strong>{formatUsageTooltipDate(active.point.date)}</strong>
|
||||||
|
<span>
|
||||||
|
{formatTokens(active.point.tokens)} {i18n.t("lab.tokens")}
|
||||||
|
</span>
|
||||||
|
<div data-slot="tooltip-divider" />
|
||||||
|
<p>
|
||||||
|
<span data-slot="tooltip-label">
|
||||||
|
<i data-kind="tokens" /> {i18n.t("lab.dailyTokens")}
|
||||||
|
</span>
|
||||||
|
<b>{formatTokens(active.point.tokens)}</b>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<div data-slot="lab-usage-months" aria-hidden="true">
|
||||||
|
<For each={monthTicks()}>
|
||||||
|
{(tick) => (
|
||||||
|
<span
|
||||||
|
data-align={tick.align}
|
||||||
|
style={{ "--lab-usage-month-left": `${tick.left}%` } as JSX.CSSProperties}
|
||||||
|
>
|
||||||
|
{tick.label}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -479,25 +577,119 @@ function formatTokens(value: number) {
|
||||||
return String(Math.round(value))
|
return String(Math.round(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatUsers(value: number) {
|
||||||
|
if (value >= 1_000_000) return `${trimNumber(value / 1_000_000, value >= 10_000_000 ? 0 : 1)}M`
|
||||||
|
if (value >= 1_000) return `${trimNumber(value / 1_000, value >= 10_000 ? 0 : 1)}K`
|
||||||
|
return new Intl.NumberFormat("en").format(Math.round(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatUsageTooltipDate(value: string) {
|
||||||
|
const match = /^([A-Z]{3})\s+(\d{1,2})$/.exec(value)
|
||||||
|
if (!match) return value
|
||||||
|
return `${monthName(match[1])} ${Number(match[2])} ${new Date().getFullYear()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function monthName(value: string) {
|
||||||
|
const names: Record<string, string> = {
|
||||||
|
JAN: "Jan",
|
||||||
|
FEB: "Feb",
|
||||||
|
MAR: "Mar",
|
||||||
|
APR: "Apr",
|
||||||
|
MAY: "May",
|
||||||
|
JUN: "Jun",
|
||||||
|
JUL: "Jul",
|
||||||
|
AUG: "Aug",
|
||||||
|
SEP: "Sep",
|
||||||
|
OCT: "Oct",
|
||||||
|
NOV: "Nov",
|
||||||
|
DEC: "Dec",
|
||||||
|
}
|
||||||
|
return names[value] ?? value
|
||||||
|
}
|
||||||
|
|
||||||
function trimNumber(value: number, digits: number) {
|
function trimNumber(value: number, digits: number) {
|
||||||
return Number(value.toFixed(digits)).toLocaleString("en")
|
return Number(value.toFixed(digits)).toLocaleString("en")
|
||||||
}
|
}
|
||||||
|
|
||||||
function usageHeight(value: number, max: number) {
|
function usageStripHeight(value: number, max: number) {
|
||||||
if (value <= 0 || max <= 0) return 0
|
if (value <= 0 || max <= 0) return 0
|
||||||
return Math.max(4, (value / max) * 100)
|
return Math.max(1, (value / max) * 40)
|
||||||
|
}
|
||||||
|
|
||||||
|
function usageLineY(value: number, max: number) {
|
||||||
|
if (value <= 0 || max <= 0) return 100
|
||||||
|
return Math.max(0, 100 - (value / max) * 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
function usagePointX(index: number, count: number) {
|
||||||
|
if (count <= 1) return 50
|
||||||
|
return ((index + 0.5) / count) * 100
|
||||||
|
}
|
||||||
|
|
||||||
|
function usageColumnBounds(index: number, count: number) {
|
||||||
|
if (count <= 0) return { x: 0, width: 100 }
|
||||||
|
return { x: (index / count) * 100, width: 100 / count }
|
||||||
|
}
|
||||||
|
|
||||||
|
function usageColumnInnerBounds(index: number, count: number) {
|
||||||
|
const bounds = usageColumnBounds(index, count)
|
||||||
|
const inset = Math.min(bounds.width * 0.1, 0.24)
|
||||||
|
return { x: bounds.x + inset, width: Math.max(0.001, bounds.width - inset * 2) }
|
||||||
|
}
|
||||||
|
|
||||||
|
type UsageLinePoint = { x: number; y: number }
|
||||||
|
|
||||||
|
function usageLinePath(points: UsageLinePoint[]) {
|
||||||
|
if (points.length === 0) return ""
|
||||||
|
if (points.length === 1) return `M ${pathNumber(points[0].x)} ${pathNumber(points[0].y)}`
|
||||||
|
|
||||||
|
return points.slice(0, -1).reduce(
|
||||||
|
(path, point, index) => {
|
||||||
|
const next = points[index + 1]
|
||||||
|
const previous = points[index - 1] ?? point
|
||||||
|
const afterNext = points[index + 2] ?? next
|
||||||
|
const controlA = {
|
||||||
|
x: point.x + (next.x - previous.x) / 6,
|
||||||
|
y: clampUsagePercent(point.y + (next.y - previous.y) / 6),
|
||||||
|
}
|
||||||
|
const controlB = {
|
||||||
|
x: next.x - (afterNext.x - point.x) / 6,
|
||||||
|
y: clampUsagePercent(next.y - (afterNext.y - point.y) / 6),
|
||||||
|
}
|
||||||
|
return `${path} C ${pathNumber(controlA.x)} ${pathNumber(controlA.y)}, ${pathNumber(controlB.x)} ${pathNumber(controlB.y)}, ${pathNumber(next.x)} ${pathNumber(next.y)}`
|
||||||
|
},
|
||||||
|
`M ${pathNumber(points[0].x)} ${pathNumber(points[0].y)}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function pathNumber(value: number) {
|
||||||
|
return Number(value.toFixed(3))
|
||||||
|
}
|
||||||
|
|
||||||
|
function clampUsagePercent(value: number) {
|
||||||
|
return Math.max(0, Math.min(100, value))
|
||||||
|
}
|
||||||
|
|
||||||
|
function labUsageMonthTicks(points: ModelUsagePoint[]) {
|
||||||
|
const seen = new Set<string>()
|
||||||
|
return points.flatMap((point, index) => {
|
||||||
|
const label = point.date.split(" ")[0]
|
||||||
|
if (!label || seen.has(label)) return []
|
||||||
|
seen.add(label)
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label,
|
||||||
|
left: usagePointX(index, points.length),
|
||||||
|
align: index === 0 ? "start" : index >= points.length - 2 ? "end" : "center",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function isLabUsageDense(count: number) {
|
function isLabUsageDense(count: number) {
|
||||||
return count > 20
|
return count > 20
|
||||||
}
|
}
|
||||||
|
|
||||||
function isLabUsageLabelHidden(index: number, count: number) {
|
|
||||||
if (count <= 14) return false
|
|
||||||
const cadence = count > 45 ? 7 : count > 28 ? 4 : 2
|
|
||||||
return index % cadence !== 0 && index !== count - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
function getProviderIconId(provider: string) {
|
function getProviderIconId(provider: string) {
|
||||||
const id = provider.toLowerCase().replace(/[^a-z0-9]+/g, "")
|
const id = provider.toLowerCase().replace(/[^a-z0-9]+/g, "")
|
||||||
if (id === "moonshot") return "moonshotai"
|
if (id === "moonshot") return "moonshotai"
|
||||||
|
|
|
||||||
|
|
@ -2848,7 +2848,7 @@
|
||||||
align-content: start;
|
align-content: start;
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
min-height: 312px;
|
min-height: 312px;
|
||||||
padding: 128px 40px 40px;
|
padding: 128px 0 40px;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3583,6 +3583,189 @@
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-component="model-usage-chart"][data-variant="lab-usage"] {
|
||||||
|
--lab-usage-gap: 4px;
|
||||||
|
--lab-usage-column-gutter: calc(var(--lab-usage-gap) / 2);
|
||||||
|
--lab-usage-bar-height: 40px;
|
||||||
|
--lab-usage-line-bottom: 80px;
|
||||||
|
--lab-usage-bar: #dbdbdb;
|
||||||
|
--lab-usage-line: #808080;
|
||||||
|
--lab-usage-active: #3b5cf6;
|
||||||
|
--lab-usage-band: #f2f2f2;
|
||||||
|
grid-template-rows: minmax(0, 450px) 14px;
|
||||||
|
gap: 40px;
|
||||||
|
height: 504px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-slot="lab-usage-plot"] {
|
||||||
|
position: relative;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-slot="lab-usage-plot"]::after {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 0;
|
||||||
|
background: linear-gradient(to bottom, transparent 42%, color-mix(in srgb, var(--stats-text) 3%, transparent));
|
||||||
|
content: "";
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-slot="lab-usage-line"] {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: auto;
|
||||||
|
left: 0;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100% - var(--lab-usage-line-bottom));
|
||||||
|
overflow: visible;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-slot="lab-usage-line"][data-layer="base"] {
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-slot="lab-usage-line"][data-layer="active"] {
|
||||||
|
z-index: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-slot="lab-usage-line"] path {
|
||||||
|
fill: none;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
stroke-width: 1.5;
|
||||||
|
vector-effect: non-scaling-stroke;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-slot="lab-usage-line-base"] {
|
||||||
|
stroke: var(--lab-usage-line);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-slot="lab-usage-line-active"] {
|
||||||
|
stroke: var(--lab-usage-active);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-slot="lab-usage-bars"] {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 3;
|
||||||
|
display: flex;
|
||||||
|
gap: 0;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-slot="lab-usage-column"] {
|
||||||
|
position: relative;
|
||||||
|
box-sizing: border-box;
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-width: 0;
|
||||||
|
height: 100%;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-slot="lab-usage-column"]::before {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 1;
|
||||||
|
background: transparent;
|
||||||
|
content: "";
|
||||||
|
pointer-events: none;
|
||||||
|
transition: background 120ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-slot="lab-usage-column"][data-active="true"]::before,
|
||||||
|
[data-page="stats"] [data-slot="lab-usage-column"]:focus-visible::before {
|
||||||
|
background: var(--lab-usage-band);
|
||||||
|
box-shadow:
|
||||||
|
inset var(--lab-usage-column-gutter) 0 0 var(--stats-bg),
|
||||||
|
inset calc(var(--lab-usage-column-gutter) * -1) 0 0 var(--stats-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-slot="lab-usage-token-bar"] {
|
||||||
|
position: absolute;
|
||||||
|
right: var(--lab-usage-column-gutter);
|
||||||
|
bottom: 0;
|
||||||
|
left: var(--lab-usage-column-gutter);
|
||||||
|
z-index: 2;
|
||||||
|
height: var(--lab-usage-token-height);
|
||||||
|
background: var(--lab-usage-bar);
|
||||||
|
transition:
|
||||||
|
background 120ms ease,
|
||||||
|
opacity 120ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-slot="lab-usage-column"][data-active="true"] [data-slot="lab-usage-token-bar"] {
|
||||||
|
background: var(--lab-usage-active);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-slot="lab-usage-column"][data-muted="true"] [data-slot="lab-usage-token-bar"] {
|
||||||
|
opacity: 0.64;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-slot="lab-usage-months"] {
|
||||||
|
position: relative;
|
||||||
|
height: 14px;
|
||||||
|
color: var(--stats-muted);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-slot="lab-usage-months"] span {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: var(--lab-usage-month-left);
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-slot="lab-usage-months"] span[data-align="start"] {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-slot="lab-usage-months"] span[data-align="end"] {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-component="model-usage-chart"][data-variant="lab-usage"] [data-component="chart-tooltip"] {
|
||||||
|
z-index: 6;
|
||||||
|
top: clamp(12px, calc(var(--lab-usage-user-y) * 1% - 38px), calc(100% - 92px));
|
||||||
|
width: 192px;
|
||||||
|
min-width: 192px;
|
||||||
|
background: var(--stats-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"]
|
||||||
|
[data-component="model-usage-chart"][data-variant="lab-usage"]
|
||||||
|
[data-component="chart-tooltip"][data-placement="right"] {
|
||||||
|
right: auto;
|
||||||
|
left: calc(var(--lab-usage-tooltip-right) + 8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"]
|
||||||
|
[data-component="model-usage-chart"][data-variant="lab-usage"]
|
||||||
|
[data-component="chart-tooltip"][data-placement="left"] {
|
||||||
|
right: calc(100% - var(--lab-usage-tooltip-left) + 8px);
|
||||||
|
left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-component="model-usage-chart"][data-variant="lab-usage"] [data-component="chart-tooltip"] p {
|
||||||
|
height: 16px;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"]
|
||||||
|
[data-component="model-usage-chart"][data-variant="lab-usage"]
|
||||||
|
[data-component="chart-tooltip"]
|
||||||
|
i[data-kind="tokens"] {
|
||||||
|
background: var(--lab-usage-active);
|
||||||
|
}
|
||||||
|
|
||||||
[data-page="stats"] [data-component="model-peer-list"] {
|
[data-page="stats"] [data-component="model-peer-list"] {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
@ -3669,6 +3852,15 @@
|
||||||
--stats-logo-stroke: #211e1e;
|
--stats-logo-stroke: #211e1e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-page="stats"][data-theme="dark"] [data-component="model-usage-chart"][data-variant="lab-usage"],
|
||||||
|
:root[data-stats-theme="dark"]
|
||||||
|
[data-page="stats"]:not([data-theme="light"])
|
||||||
|
[data-component="model-usage-chart"][data-variant="lab-usage"] {
|
||||||
|
--lab-usage-bar: #3a3a3a;
|
||||||
|
--lab-usage-line: #a3a3a3;
|
||||||
|
--lab-usage-band: #242424;
|
||||||
|
}
|
||||||
|
|
||||||
[data-page="stats"][data-theme="dark"] [data-component="chart-tooltip"],
|
[data-page="stats"][data-theme="dark"] [data-component="chart-tooltip"],
|
||||||
:root[data-stats-theme="dark"] [data-page="stats"]:not([data-theme="light"]) [data-component="chart-tooltip"] {
|
:root[data-stats-theme="dark"] [data-page="stats"]:not([data-theme="light"]) [data-component="chart-tooltip"] {
|
||||||
background: #242424f2;
|
background: #242424f2;
|
||||||
|
|
@ -3820,6 +4012,14 @@
|
||||||
--stats-logo-stroke: #211e1e;
|
--stats-logo-stroke: #211e1e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root:not([data-stats-theme="light"])
|
||||||
|
[data-page="stats"]:not([data-theme="light"])
|
||||||
|
[data-component="model-usage-chart"][data-variant="lab-usage"] {
|
||||||
|
--lab-usage-bar: #3a3a3a;
|
||||||
|
--lab-usage-line: #a3a3a3;
|
||||||
|
--lab-usage-band: #242424;
|
||||||
|
}
|
||||||
|
|
||||||
:root:not([data-stats-theme="light"]) [data-page="stats"]:not([data-theme="light"]) [data-component="chart-tooltip"] {
|
:root:not([data-stats-theme="light"]) [data-page="stats"]:not([data-theme="light"]) [data-component="chart-tooltip"] {
|
||||||
background: #242424f2;
|
background: #242424f2;
|
||||||
}
|
}
|
||||||
|
|
@ -3915,6 +4115,21 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-page="stats"][data-theme="dark"]
|
||||||
|
[data-component="model-usage-chart"][data-variant="lab-usage"]
|
||||||
|
[data-component="chart-tooltip"],
|
||||||
|
:root[data-stats-theme="dark"]
|
||||||
|
[data-page="stats"]:not([data-theme="light"])
|
||||||
|
[data-component="model-usage-chart"][data-variant="lab-usage"]
|
||||||
|
[data-component="chart-tooltip"],
|
||||||
|
:root:not([data-stats-theme="light"])
|
||||||
|
[data-page="stats"]:not([data-theme="light"])
|
||||||
|
[data-component="model-usage-chart"][data-variant="lab-usage"]
|
||||||
|
[data-component="chart-tooltip"],
|
||||||
|
[data-page="stats"] [data-component="model-usage-chart"][data-variant="lab-usage"] [data-component="chart-tooltip"] {
|
||||||
|
background: var(--stats-bg);
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-width: 48rem) {
|
@media (min-width: 48rem) {
|
||||||
[data-page="stats"] [data-slot="header-button"][data-variant="neutral"] {
|
[data-page="stats"] [data-slot="header-button"][data-variant="neutral"] {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|
@ -4372,6 +4587,35 @@
|
||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-component="model-usage-chart"][data-variant="lab-usage"] {
|
||||||
|
--model-usage-mobile-bar-width: 20px;
|
||||||
|
--model-usage-mobile-edge-space: 48px;
|
||||||
|
--model-usage-mobile-track-width: calc(
|
||||||
|
var(--model-usage-count) * var(--model-usage-mobile-bar-width) + var(--model-usage-mobile-edge-space)
|
||||||
|
);
|
||||||
|
--lab-usage-line-bottom: 64px;
|
||||||
|
grid-template-rows: minmax(0, 360px) 14px;
|
||||||
|
gap: 24px;
|
||||||
|
height: 398px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-component="model-usage-chart"][data-variant="lab-usage"] [data-slot="lab-usage-plot"],
|
||||||
|
[data-page="stats"] [data-component="model-usage-chart"][data-variant="lab-usage"] [data-slot="lab-usage-months"] {
|
||||||
|
direction: ltr;
|
||||||
|
width: var(--model-usage-mobile-track-width);
|
||||||
|
min-width: var(--model-usage-mobile-track-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-component="model-usage-chart"][data-variant="lab-usage"] [data-slot="lab-usage-column"] {
|
||||||
|
flex: 0 0 var(--model-usage-mobile-bar-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-component="model-usage-chart"][data-variant="lab-usage"] [data-component="chart-tooltip"] {
|
||||||
|
top: auto;
|
||||||
|
width: auto;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
[data-page="stats"] [data-component="model-peer-list"] a {
|
[data-page="stats"] [data-component="model-peer-list"] a {
|
||||||
grid-template-columns: 30px minmax(0, 1fr) minmax(64px, auto);
|
grid-template-columns: 30px minmax(0, 1fr) minmax(64px, auto);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue