433 lines
16 KiB
TypeScript
433 lines
16 KiB
TypeScript
import "../index.css"
|
|
import { Link, Meta, Title } from "@solidjs/meta"
|
|
import {
|
|
getStatsLabData,
|
|
type LabUsageModelEntry,
|
|
type ModelUsagePoint,
|
|
type StatsLabData,
|
|
} from "@opencode-ai/stats-core/domain/home"
|
|
import { runtime } from "@opencode-ai/stats-core/runtime"
|
|
import { createAsync, query, useParams } from "@solidjs/router"
|
|
import { createMemo, createSignal, For, onMount, Show, type JSX } from "solid-js"
|
|
import { getRequestEvent } from "solid-js/web"
|
|
import {
|
|
findModelCatalogLab,
|
|
formatCatalogLabName,
|
|
getModelCatalog,
|
|
type ModelCatalogEntry,
|
|
type ModelCatalogLab,
|
|
} from "../model-catalog"
|
|
import {
|
|
applyThemePreference,
|
|
Footer,
|
|
getGitHubStars,
|
|
Header,
|
|
isThemePreference,
|
|
themeStorageKey,
|
|
type HeaderLink,
|
|
type ThemePreference,
|
|
} from "../stats-shell"
|
|
|
|
const statsCanonicalBaseUrl = "https://opencode.ai/data/"
|
|
const statsUnfurlPath = "banner.png"
|
|
const statsUnfurlAlt = "OpenCode Data wordmark on a dark patterned background"
|
|
const statsUnfurlUrl = new URL(statsUnfurlPath, statsCanonicalBaseUrl).toString()
|
|
const labHeaderLinks: readonly HeaderLink[] = [
|
|
{ href: "#overview", label: "Overview" },
|
|
{ href: "#usage", label: "Usage" },
|
|
{ href: "#models", label: "Models" },
|
|
]
|
|
const labFooterLinks: readonly HeaderLink[] = [
|
|
{ href: import.meta.env.BASE_URL, label: "Data Home" },
|
|
{ href: `${import.meta.env.BASE_URL}#top-models`, label: "Top Models" },
|
|
{ href: `${import.meta.env.BASE_URL}#market-share`, label: "Market Share" },
|
|
{ href: `${import.meta.env.BASE_URL}#geo-breakdown`, label: "Geo Breakdown" },
|
|
]
|
|
|
|
const getLabData = query(async (lab: string) => {
|
|
"use server"
|
|
return runtime.runPromise(getStatsLabData(lab))
|
|
}, "getStatsLabData")
|
|
|
|
export default function StatsLab() {
|
|
const event = getRequestEvent()
|
|
event?.response.headers.set("Cache-Control", "public, max-age=60, s-maxage=300, stale-while-revalidate=86400")
|
|
const params = useParams()
|
|
const labParam = createMemo(() => params.lab ?? "")
|
|
const catalog = createAsync(() => getModelCatalog())
|
|
const lab = createMemo(() => {
|
|
const data = catalog()
|
|
if (!data) return undefined
|
|
return findModelCatalogLab(data, labParam()) ?? null
|
|
})
|
|
const stats = createAsync(() => {
|
|
const entry = lab()
|
|
if (catalog() === undefined || entry === undefined) return Promise.resolve(undefined)
|
|
if (!entry) return Promise.resolve(null)
|
|
return getLabData(entry.id)
|
|
})
|
|
const githubStars = createAsync(() => getGitHubStars())
|
|
const [themePreference, setThemePreference] = createSignal<ThemePreference>("system")
|
|
const labName = createMemo(() => lab()?.name ?? formatCatalogLabName(labParam()))
|
|
const labTitle = createMemo(() => `${labName()} Models`)
|
|
const labDescription = createMemo(
|
|
() =>
|
|
`Explore ${labName()} models used in OpenCode, with recent token usage, context windows, release dates, and model-specific data.`,
|
|
)
|
|
const labUrl = createMemo(() => new URL(lab()?.id ?? labParam(), statsCanonicalBaseUrl).toString())
|
|
const updateThemePreference = (preference: ThemePreference) => {
|
|
applyThemePreference(preference)
|
|
setThemePreference(preference)
|
|
if (typeof window === "undefined") return
|
|
window.localStorage.setItem(themeStorageKey, preference)
|
|
}
|
|
|
|
onMount(() => {
|
|
if (typeof window === "undefined") return
|
|
const preference = window.localStorage.getItem(themeStorageKey)
|
|
const nextPreference = isThemePreference(preference) ? preference : "system"
|
|
applyThemePreference(nextPreference)
|
|
setThemePreference(nextPreference)
|
|
})
|
|
|
|
return (
|
|
<main data-page="stats" data-theme={themePreference()}>
|
|
<Title>{labTitle()}</Title>
|
|
<Meta name="description" content={labDescription()} />
|
|
<Link rel="canonical" href={labUrl()} />
|
|
<Meta property="og:type" content="website" />
|
|
<Meta property="og:site_name" content="OpenCode" />
|
|
<Meta property="og:title" content={labTitle()} />
|
|
<Meta property="og:description" content={labDescription()} />
|
|
<Meta property="og:url" content={labUrl()} />
|
|
<Meta property="og:image" content={statsUnfurlUrl} />
|
|
<Meta property="og:image:type" content="image/png" />
|
|
<Meta property="og:image:width" content="1200" />
|
|
<Meta property="og:image:height" content="630" />
|
|
<Meta property="og:image:alt" content={statsUnfurlAlt} />
|
|
<Meta name="twitter:card" content="summary_large_image" />
|
|
<Meta name="twitter:title" content={labTitle()} />
|
|
<Meta name="twitter:description" content={labDescription()} />
|
|
<Meta name="twitter:image" content={statsUnfurlUrl} />
|
|
<Meta name="twitter:image:alt" content={statsUnfurlAlt} />
|
|
<Header githubStars={githubStars() ?? "150K"} links={labHeaderLinks} brandHref={import.meta.env.BASE_URL} />
|
|
<div data-component="container">
|
|
<div data-component="content">
|
|
<Show when={catalog() !== undefined} fallback={<LabLoading />}>
|
|
<Show when={lab()} fallback={<LabNotFound lab={labParam()} />}>
|
|
{(data) => (
|
|
<>
|
|
<LabHero lab={data()} stats={stats() ?? null} />
|
|
<LabUsageSection lab={data()} data={stats() ?? null} />
|
|
<LabModelsSection lab={data()} usage={stats()?.models ?? []} />
|
|
</>
|
|
)}
|
|
</Show>
|
|
</Show>
|
|
</div>
|
|
<Footer
|
|
themePreference={themePreference()}
|
|
onThemePreferenceChange={updateThemePreference}
|
|
links={labFooterLinks}
|
|
/>
|
|
</div>
|
|
</main>
|
|
)
|
|
}
|
|
|
|
function LabLoading() {
|
|
return (
|
|
<section id="overview" data-section="lab-hero">
|
|
<div data-slot="model-hero-grid">
|
|
<div data-slot="model-hero-copy">
|
|
<a data-slot="model-back-link" href={import.meta.env.BASE_URL}>
|
|
Data
|
|
</a>
|
|
<h1>Model Lab</h1>
|
|
<p>Reading model availability and recent OpenCode usage.</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
function LabNotFound(props: { lab: string }) {
|
|
return (
|
|
<section id="overview" data-section="lab-hero">
|
|
<div data-slot="model-hero-grid">
|
|
<div data-slot="model-hero-copy">
|
|
<a data-slot="model-back-link" href={import.meta.env.BASE_URL}>
|
|
Data
|
|
</a>
|
|
<h1>{formatCatalogLabName(props.lab)}</h1>
|
|
<p>No models matched this lab.</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
function LabHero(props: { lab: ModelCatalogLab; stats: StatsLabData | null }) {
|
|
const latest = createMemo(
|
|
() =>
|
|
props.lab.models
|
|
.map((model) => model.releaseDate)
|
|
.filter((value): value is string => value !== undefined)
|
|
.toSorted((a, b) => new Date(b).getTime() - new Date(a).getTime())[0],
|
|
)
|
|
const featuredModels = createMemo(() => props.lab.models.slice(0, 3).map((model) => model.name))
|
|
|
|
return (
|
|
<section id="overview" data-section="lab-hero">
|
|
<a data-slot="model-back-link" href={import.meta.env.BASE_URL}>
|
|
Data
|
|
</a>
|
|
<div data-slot="model-hero-grid">
|
|
<div data-slot="model-hero-copy">
|
|
<h1>{props.lab.name}</h1>
|
|
<div data-slot="model-hero-pattern" aria-hidden="true" />
|
|
<p>
|
|
Explore {props.lab.models.length} {props.lab.name} models used in OpenCode
|
|
<Show when={featuredModels().length > 0}> including {formatList(featuredModels())}</Show>. Compare recent
|
|
token usage, context windows, release dates, and model-specific data.
|
|
</p>
|
|
</div>
|
|
<div data-component="model-rank-panel">
|
|
<span>Tokens Processed</span>
|
|
<strong>{props.stats ? formatTokens(props.stats.totals.tokens) : "Pending"}</strong>
|
|
<p>
|
|
{props.stats
|
|
? `${formatPercent(props.stats.tokenShare)} of recent OpenCode usage`
|
|
: latest()
|
|
? `Latest release ${formatCatalogDate(latest())}`
|
|
: "Usage appears after model activity lands"}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
function LabUsageSection(props: { lab: ModelCatalogLab; data: StatsLabData | null }) {
|
|
const [activeIndex, setActiveIndex] = createSignal<number>()
|
|
const usage = createMemo(() => props.data?.usage ?? [])
|
|
const max = createMemo(() => Math.max(0, ...usage().map((item) => item.tokens)) || 1)
|
|
const activePoint = createMemo(() => {
|
|
const index = activeIndex()
|
|
if (index === undefined) return undefined
|
|
return usage()[index]
|
|
})
|
|
|
|
return (
|
|
<section id="usage" data-section="model-panel">
|
|
<p data-slot="section-title">
|
|
<strong>{props.lab.name} token usage.</strong>{" "}
|
|
<span>Daily OpenCode token volume over the last two months.</span>
|
|
</p>
|
|
<Show
|
|
when={usage().some((item) => item.tokens > 0)}
|
|
fallback={
|
|
<LabEmptyState
|
|
title="No usage yet"
|
|
description="Recent token usage appears here once this lab has activity."
|
|
/>
|
|
}
|
|
>
|
|
<div
|
|
data-component="model-usage-chart"
|
|
data-dense-labels={isLabUsageDense(usage().length) ? "true" : undefined}
|
|
role="img"
|
|
aria-label={`${props.lab.name} daily token usage chart`}
|
|
style={{ "--model-usage-count": usage().length } as JSX.CSSProperties}
|
|
onPointerLeave={(event) => {
|
|
if (event.pointerType === "touch") return
|
|
setActiveIndex(undefined)
|
|
}}
|
|
>
|
|
<div data-slot="model-usage-axis" aria-hidden="true">
|
|
<For each={usage()}>
|
|
{(point, index) => (
|
|
<div
|
|
data-active={activeIndex() === index() ? "true" : undefined}
|
|
data-label-hidden={isLabUsageLabelHidden(index(), usage().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={usage()}>
|
|
{(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": `${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>
|
|
<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>
|
|
</Show>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
function LabModelsSection(props: { lab: ModelCatalogLab; usage: LabUsageModelEntry[] }) {
|
|
const usageBySlug = createMemo(() => new Map(props.usage.map((item) => [item.slug, item])))
|
|
return (
|
|
<section id="models" data-section="model-panel">
|
|
<p data-slot="section-title">
|
|
<strong>{props.lab.name} models.</strong> <span>Recent usage and limits.</span>
|
|
</p>
|
|
<div data-component="lab-model-grid">
|
|
<For each={props.lab.models}>
|
|
{(model) => <LabModelCard model={model} usage={usageBySlug().get(model.slug)} />}
|
|
</For>
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
function LabModelCard(props: { model: ModelCatalogEntry; usage: LabUsageModelEntry | undefined }) {
|
|
return (
|
|
<a data-component="lab-model-card" href={`${import.meta.env.BASE_URL}${props.model.id}`}>
|
|
<strong>{props.model.name}</strong>
|
|
<div data-slot="lab-model-card-meta">
|
|
<p>
|
|
<b>Usage</b>
|
|
<em>{props.usage ? formatTokens(props.usage.tokens) : "—"}</em>
|
|
</p>
|
|
<p>
|
|
<b>Share</b>
|
|
<em>{props.usage ? formatPercent(props.usage.share) : "—"}</em>
|
|
</p>
|
|
<p>
|
|
<b>Context</b>
|
|
<em>{formatCatalogLimit(props.model.limit?.context)}</em>
|
|
</p>
|
|
<p>
|
|
<b>Output</b>
|
|
<em>{formatCatalogLimit(props.model.limit?.output)}</em>
|
|
</p>
|
|
<p>
|
|
<b>Release</b>
|
|
<em>{formatCatalogDate(props.model.releaseDate)}</em>
|
|
</p>
|
|
</div>
|
|
</a>
|
|
)
|
|
}
|
|
|
|
function LabEmptyState(props: { title: string; description: string }) {
|
|
return (
|
|
<div data-component="empty-state" data-compact="true">
|
|
<strong>{props.title}</strong>
|
|
<p>{props.description}</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function formatCatalogLimit(value: number | undefined) {
|
|
return value === undefined ? "Unknown" : formatTokens(value)
|
|
}
|
|
|
|
function formatCatalogDate(value: string | undefined) {
|
|
if (!value) return "Unknown"
|
|
const match = /^(\d{4})(?:-(\d{2}))?(?:-(\d{2}))?$/.exec(value)
|
|
if (!match) return value
|
|
const year = Number(match[1])
|
|
const month = match[2] ? Number(match[2]) - 1 : 0
|
|
const day = match[3] ? Number(match[3]) : 1
|
|
return new Intl.DateTimeFormat("en", {
|
|
month: match[2] ? "short" : undefined,
|
|
day: match[3] ? "numeric" : undefined,
|
|
year: "numeric",
|
|
timeZone: "UTC",
|
|
}).format(new Date(Date.UTC(year, month, day)))
|
|
}
|
|
|
|
function formatList(values: string[]) {
|
|
if (values.length <= 1) return values[0] ?? ""
|
|
if (values.length === 2) return `${values[0]} and ${values[1]}`
|
|
return `${values.slice(0, -1).join(", ")}, and ${values[values.length - 1]}`
|
|
}
|
|
|
|
function formatPercent(value: number) {
|
|
return `${trimNumber(value, value >= 10 ? 1 : 2)}%`
|
|
}
|
|
|
|
function formatTokens(value: number) {
|
|
if (value >= 1_000_000_000_000)
|
|
return `${trimNumber(value / 1_000_000_000_000, value >= 10_000_000_000_000 ? 0 : 1)}T`
|
|
if (value >= 1_000_000_000) return `${trimNumber(value / 1_000_000_000, value >= 10_000_000_000 ? 0 : 1)}B`
|
|
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 String(Math.round(value))
|
|
}
|
|
|
|
function trimNumber(value: number, digits: number) {
|
|
return Number(value.toFixed(digits)).toLocaleString("en")
|
|
}
|
|
|
|
function usageHeight(value: number, max: number) {
|
|
if (value <= 0 || max <= 0) return 0
|
|
return Math.max(4, (value / max) * 100)
|
|
}
|
|
|
|
function isLabUsageDense(count: number) {
|
|
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
|
|
}
|