|
@@ -45,6 +45,7 @@ const statsUnfurlUrl = new URL(statsUnfurlPath, statsCanonicalBaseUrl).toString(
|
|
|
const modelHeaderLinks: readonly HeaderLink[] = [
|
|
const modelHeaderLinks: readonly HeaderLink[] = [
|
|
|
{ href: "#overview", label: "Overview" },
|
|
{ href: "#overview", label: "Overview" },
|
|
|
{ href: "#usage", label: "Usage" },
|
|
{ href: "#usage", label: "Usage" },
|
|
|
|
|
+ { href: "#users", label: "Users" },
|
|
|
{ href: "#efficiency", label: "Efficiency" },
|
|
{ href: "#efficiency", label: "Efficiency" },
|
|
|
{ href: "#geo-breakdown", label: "Geo Breakdown" },
|
|
{ href: "#geo-breakdown", label: "Geo Breakdown" },
|
|
|
{ href: "#peers", label: "Peers" },
|
|
{ href: "#peers", label: "Peers" },
|
|
@@ -175,6 +176,7 @@ export default function StatsModel() {
|
|
|
<ModelHero data={stats() ?? null} catalog={catalogEntry() ?? null} labName={labName()} />
|
|
<ModelHero data={stats() ?? null} catalog={catalogEntry() ?? null} labName={labName()} />
|
|
|
<ModelOverview data={stats() ?? null} />
|
|
<ModelOverview data={stats() ?? null} />
|
|
|
<ModelUsageSection data={stats()?.usage ?? []} />
|
|
<ModelUsageSection data={stats()?.usage ?? []} />
|
|
|
|
|
+ <ModelUsersSection data={stats()?.usage ?? []} />
|
|
|
<ModelEfficiencySection data={stats() ?? null} catalog={catalogEntry() ?? null} />
|
|
<ModelEfficiencySection data={stats() ?? null} catalog={catalogEntry() ?? null} />
|
|
|
<ModelGeoBreakdownSection data={stats()?.country ?? emptyCountryRecord()} />
|
|
<ModelGeoBreakdownSection data={stats()?.country ?? emptyCountryRecord()} />
|
|
|
<ModelPeersSection data={stats() ?? null} />
|
|
<ModelPeersSection data={stats() ?? null} />
|
|
@@ -358,14 +360,6 @@ function ModelOverview(props: { data: StatsModelData | null }) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function ModelUsageSection(props: { data: ModelUsagePoint[] }) {
|
|
function ModelUsageSection(props: { data: ModelUsagePoint[] }) {
|
|
|
- const [activeIndex, setActiveIndex] = createSignal<number>()
|
|
|
|
|
- const max = createMemo(() => Math.max(0, ...props.data.map((item) => item.tokens)) || 1)
|
|
|
|
|
- const activePoint = createMemo(() => {
|
|
|
|
|
- const index = activeIndex()
|
|
|
|
|
- if (index === undefined) return undefined
|
|
|
|
|
- return props.data[index]
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
return (
|
|
return (
|
|
|
<section id="usage" data-section="model-panel">
|
|
<section id="usage" data-section="model-panel">
|
|
|
<SectionTitle title="Usage" description="Daily OpenCode Go token volume over the recent two-month window." />
|
|
<SectionTitle title="Usage" description="Daily OpenCode Go token volume over the recent two-month window." />
|
|
@@ -373,92 +367,144 @@ function ModelUsageSection(props: { data: ModelUsagePoint[] }) {
|
|
|
when={props.data.some((item) => item.tokens > 0)}
|
|
when={props.data.some((item) => item.tokens > 0)}
|
|
|
fallback={<ModelEmptyState title="No usage" description="No usage landed in the current window." />}
|
|
fallback={<ModelEmptyState title="No usage" description="No usage landed in the current window." />}
|
|
|
>
|
|
>
|
|
|
- <div
|
|
|
|
|
- data-component="model-usage-chart"
|
|
|
|
|
- data-dense-labels={isModelUsageDense(props.data.length) ? "true" : undefined}
|
|
|
|
|
- role="img"
|
|
|
|
|
- aria-label="Daily token usage chart"
|
|
|
|
|
- style={{ "--model-usage-count": props.data.length } as JSX.CSSProperties}
|
|
|
|
|
- onPointerLeave={(event) => {
|
|
|
|
|
- if (event.pointerType === "touch") return
|
|
|
|
|
- setActiveIndex(undefined)
|
|
|
|
|
- }}
|
|
|
|
|
- >
|
|
|
|
|
- <div data-slot="model-usage-axis" aria-hidden="true">
|
|
|
|
|
- <For each={props.data}>
|
|
|
|
|
- {(point, index) => (
|
|
|
|
|
- <div
|
|
|
|
|
- data-active={activeIndex() === index() ? "true" : undefined}
|
|
|
|
|
- data-label-hidden={isModelUsageLabelHidden(index(), props.data.length) ? "true" : undefined}
|
|
|
|
|
- >
|
|
|
|
|
- <span data-slot="model-usage-label">
|
|
|
|
|
- <span data-slot="model-usage-total">{formatTokens(point.tokens)}</span>
|
|
|
|
|
- <span data-slot="model-usage-date">{point.date}</span>
|
|
|
|
|
- </span>
|
|
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
- </For>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div data-slot="model-usage-bars">
|
|
|
|
|
- <For each={props.data}>
|
|
|
|
|
- {(point, index) => (
|
|
|
|
|
- <div
|
|
|
|
|
- data-slot="model-usage-column"
|
|
|
|
|
- role="button"
|
|
|
|
|
- tabIndex={0}
|
|
|
|
|
- aria-label={`${point.date} ${formatTokens(point.tokens)} tokens`}
|
|
|
|
|
- data-active={activeIndex() === index() ? "true" : undefined}
|
|
|
|
|
- data-muted={activeIndex() !== undefined && activeIndex() !== index() ? "true" : undefined}
|
|
|
|
|
- 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="model-usage-bar"
|
|
|
|
|
- style={{ "--model-usage-fill": `${modelUsageHeight(point.tokens, max())}%` } as JSX.CSSProperties}
|
|
|
|
|
- />
|
|
|
|
|
- <Show when={activeIndex() === index() && activePoint()}>
|
|
|
|
|
- {(active) => (
|
|
|
|
|
- <div
|
|
|
|
|
- data-component="chart-tooltip"
|
|
|
|
|
- data-placement={index() > props.data.length * 0.62 ? "left" : "right"}
|
|
|
|
|
- >
|
|
|
|
|
- <strong>{active().date}</strong>
|
|
|
|
|
- <span>{formatTokens(active().tokens)} tokens</span>
|
|
|
|
|
- <div data-slot="tooltip-divider" />
|
|
|
|
|
- <p>
|
|
|
|
|
- <span data-slot="tooltip-label">
|
|
|
|
|
- <i /> Daily tokens
|
|
|
|
|
- </span>
|
|
|
|
|
- <b>{formatTokens(active().tokens)}</b>
|
|
|
|
|
- </p>
|
|
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
- </Show>
|
|
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
- </For>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <ModelColumnChart data={props.data} metric="tokens" ariaLabel="Daily token usage chart" />
|
|
|
</Show>
|
|
</Show>
|
|
|
</section>
|
|
</section>
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function ModelUsersSection(props: { data: ModelUsagePoint[] }) {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <section id="users" data-section="model-panel">
|
|
|
|
|
+ <SectionTitle title="Unique Users" description="Daily unique OpenCode Go users over the recent two-month window." />
|
|
|
|
|
+ <Show
|
|
|
|
|
+ when={props.data.some((item) => item.users > 0)}
|
|
|
|
|
+ fallback={<ModelEmptyState title="No user data" description="No user-bearing rows landed in the current window." />}
|
|
|
|
|
+ >
|
|
|
|
|
+ <ModelColumnChart data={props.data} metric="users" ariaLabel="Daily unique user chart" />
|
|
|
|
|
+ </Show>
|
|
|
|
|
+ </section>
|
|
|
|
|
+ )
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function ModelColumnChart(props: {
|
|
|
|
|
+ data: ModelUsagePoint[]
|
|
|
|
|
+ metric: "tokens" | "users"
|
|
|
|
|
+ ariaLabel: string
|
|
|
|
|
+}) {
|
|
|
|
|
+ const [activeIndex, setActiveIndex] = createSignal<number>()
|
|
|
|
|
+ const max = createMemo(() => Math.max(0, ...props.data.map((item) => modelUsageMetricValue(item, props.metric))) || 1)
|
|
|
|
|
+ const activePoint = createMemo(() => {
|
|
|
|
|
+ const index = activeIndex()
|
|
|
|
|
+ if (index === undefined) return undefined
|
|
|
|
|
+ return props.data[index]
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div
|
|
|
|
|
+ data-component="model-usage-chart"
|
|
|
|
|
+ data-metric={props.metric}
|
|
|
|
|
+ data-dense-labels={isModelUsageDense(props.data.length) ? "true" : undefined}
|
|
|
|
|
+ role="img"
|
|
|
|
|
+ aria-label={props.ariaLabel}
|
|
|
|
|
+ style={{ "--model-usage-count": props.data.length } as JSX.CSSProperties}
|
|
|
|
|
+ onPointerLeave={(event) => {
|
|
|
|
|
+ if (event.pointerType === "touch") return
|
|
|
|
|
+ setActiveIndex(undefined)
|
|
|
|
|
+ }}
|
|
|
|
|
+ >
|
|
|
|
|
+ <div data-slot="model-usage-axis" aria-hidden="true">
|
|
|
|
|
+ <For each={props.data}>
|
|
|
|
|
+ {(point, index) => (
|
|
|
|
|
+ <div
|
|
|
|
|
+ data-active={activeIndex() === index() ? "true" : undefined}
|
|
|
|
|
+ data-label-hidden={isModelUsageLabelHidden(index(), props.data.length) ? "true" : undefined}
|
|
|
|
|
+ >
|
|
|
|
|
+ <span data-slot="model-usage-label">
|
|
|
|
|
+ <span data-slot="model-usage-total">{formatModelUsageValue(point, props.metric)}</span>
|
|
|
|
|
+ <span data-slot="model-usage-date">{point.date}</span>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </For>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div data-slot="model-usage-bars">
|
|
|
|
|
+ <For each={props.data}>
|
|
|
|
|
+ {(point, index) => (
|
|
|
|
|
+ <div
|
|
|
|
|
+ data-slot="model-usage-column"
|
|
|
|
|
+ role="button"
|
|
|
|
|
+ tabIndex={0}
|
|
|
|
|
+ aria-label={`${point.date} ${formatModelUsageValue(point, props.metric)} ${modelUsageLabel(props.metric)}`}
|
|
|
|
|
+ data-active={activeIndex() === index() ? "true" : undefined}
|
|
|
|
|
+ data-muted={activeIndex() !== undefined && activeIndex() !== index() ? "true" : undefined}
|
|
|
|
|
+ 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="model-usage-bar"
|
|
|
|
|
+ style={{
|
|
|
|
|
+ "--model-usage-fill": `${modelUsageHeight(modelUsageMetricValue(point, props.metric), max())}%`,
|
|
|
|
|
+ } as JSX.CSSProperties}
|
|
|
|
|
+ />
|
|
|
|
|
+ <Show when={activeIndex() === index() && activePoint()}>
|
|
|
|
|
+ {(active) => (
|
|
|
|
|
+ <div
|
|
|
|
|
+ data-component="chart-tooltip"
|
|
|
|
|
+ data-placement={index() > props.data.length * 0.62 ? "left" : "right"}
|
|
|
|
|
+ >
|
|
|
|
|
+ <strong>{active().date}</strong>
|
|
|
|
|
+ <span>
|
|
|
|
|
+ {formatModelUsageValue(active(), props.metric)} {modelUsageLabel(props.metric)}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <div data-slot="tooltip-divider" />
|
|
|
|
|
+ <p>
|
|
|
|
|
+ <span data-slot="tooltip-label">
|
|
|
|
|
+ <i /> Daily {modelUsageLabel(props.metric)}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <b>{formatModelUsageValue(active(), props.metric)}</b>
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </Show>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </For>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function modelUsageMetricValue(point: ModelUsagePoint, metric: "tokens" | "users") {
|
|
|
|
|
+ if (metric === "users") return point.users
|
|
|
|
|
+ return point.tokens
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function formatModelUsageValue(point: ModelUsagePoint, metric: "tokens" | "users") {
|
|
|
|
|
+ if (metric === "users") return formatUsers(point.users)
|
|
|
|
|
+ return formatTokens(point.tokens)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function modelUsageLabel(metric: "tokens" | "users") {
|
|
|
|
|
+ if (metric === "users") return "users"
|
|
|
|
|
+ return "tokens"
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function ModelEfficiencySection(props: { data: StatsModelData | null; catalog: ModelCatalogEntry | null }) {
|
|
function ModelEfficiencySection(props: { data: StatsModelData | null; catalog: ModelCatalogEntry | null }) {
|
|
|
return (
|
|
return (
|
|
|
<section id="efficiency" data-section="model-panel">
|
|
<section id="efficiency" data-section="model-panel">
|
|
@@ -834,6 +880,12 @@ function formatInteger(value: number) {
|
|
|
return new Intl.NumberFormat("en").format(value)
|
|
return new Intl.NumberFormat("en").format(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 formatInteger(Math.round(value))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function formatPercent(value: number) {
|
|
function formatPercent(value: number) {
|
|
|
return `${value.toFixed(value > 0 && value < 10 ? 1 : 0)}%`
|
|
return `${value.toFixed(value > 0 && value < 10 ? 1 : 0)}%`
|
|
|
}
|
|
}
|