mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-10 03:18:31 +00:00
feat(stats): add model momentum section
This commit is contained in:
parent
eba0bd0397
commit
7d84cb05ef
3 changed files with 685 additions and 190 deletions
|
|
@ -198,7 +198,7 @@ const en = {
|
|||
"model.knowledge": "Knowledge",
|
||||
"model.release": "Release",
|
||||
"model.inputs": "Inputs",
|
||||
"model.overviewDescription": "Recent OpenCode Go tokens, unique users, and market position.",
|
||||
"model.overviewDescription": "Recent tokens, unique users, and market position.",
|
||||
"model.noSummaryTitle": "No usage summary",
|
||||
"model.noSummaryDescription": "This model has no OpenCode Go usage rows yet.",
|
||||
"model.tokens": "Tokens",
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ import {
|
|||
const statsUnfurlPath = "banner.png"
|
||||
const geoMapWidth = 960
|
||||
const geoMapHeight = 430
|
||||
const shortMonths = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"] as const
|
||||
|
||||
type IsoCountryCode = readonly [string, string, string]
|
||||
type WorldCountryProperties = GeoJsonProperties & { name?: string }
|
||||
|
|
@ -119,8 +120,7 @@ export default function StatsModel() {
|
|||
const statsUnfurlUrl = new URL(statsUnfurlPath, localizedUrl("en", "/data/")).toString()
|
||||
const modelHeaderLinks = createMemo<readonly HeaderLink[]>(() => [
|
||||
{ href: "#overview", label: i18n.t("nav.overview") },
|
||||
{ href: "#usage", label: i18n.t("nav.usage") },
|
||||
{ href: "#users", label: i18n.t("nav.users") },
|
||||
{ href: "#momentum", label: i18n.t("model.momentum") },
|
||||
{ href: "#efficiency", label: i18n.t("nav.efficiency") },
|
||||
{ href: "#geo-breakdown", label: i18n.t("nav.geoBreakdown") },
|
||||
{ href: "#peers", label: i18n.t("nav.peers") },
|
||||
|
|
@ -181,9 +181,8 @@ export default function StatsModel() {
|
|||
catalogData={catalog() ?? null}
|
||||
labName={labName()}
|
||||
/>
|
||||
<ModelOverview data={stats() ?? null} />
|
||||
<ModelUsageSection data={stats()?.usage ?? []} />
|
||||
<ModelUsersSection data={stats()?.usage ?? []} />
|
||||
<ModelOverview catalog={catalogEntry() ?? null} />
|
||||
<ModelMomentumSection data={stats() ?? null} />
|
||||
<ModelEfficiencySection data={stats() ?? null} catalog={catalogEntry() ?? null} />
|
||||
<ModelGeoBreakdownSection data={stats()?.country ?? emptyCountryRecord()} />
|
||||
<ModelPeersSection data={stats() ?? null} />
|
||||
|
|
@ -423,15 +422,59 @@ function ChevronDownIcon() {
|
|||
)
|
||||
}
|
||||
|
||||
function ModelOverview(props: { data: StatsModelData | null }) {
|
||||
function ModelOverview(props: { catalog: ModelCatalogEntry | null }) {
|
||||
const i18n = useI18n()
|
||||
const language = useLanguage()
|
||||
const specs = createMemo(() => [
|
||||
{
|
||||
label: i18n.t("model.context"),
|
||||
value: formatCatalogLimit(props.catalog?.limit?.context, i18n.t("home.unknown")),
|
||||
},
|
||||
{
|
||||
label: i18n.t("model.output"),
|
||||
value: formatCatalogLimit(props.catalog?.limit?.output, i18n.t("home.unknown")),
|
||||
},
|
||||
{
|
||||
label: i18n.t("model.knowledge"),
|
||||
value: formatCatalogMonth(props.catalog?.knowledge, language.tag(language.locale()), i18n.t("home.unknown")),
|
||||
},
|
||||
{
|
||||
label: i18n.t("model.release"),
|
||||
value: formatCatalogMonth(props.catalog?.releaseDate, language.tag(language.locale()), i18n.t("home.unknown")),
|
||||
},
|
||||
{
|
||||
label: i18n.t("model.inputs"),
|
||||
value: formatCatalogModalities(
|
||||
props.catalog?.modalities.input ?? [],
|
||||
language.tag(language.locale()),
|
||||
i18n.t("home.unknown"),
|
||||
),
|
||||
},
|
||||
])
|
||||
return (
|
||||
<section id="model-overview" data-section="model-panel">
|
||||
<SectionTitle
|
||||
href="#model-overview"
|
||||
title={i18n.t("nav.overview")}
|
||||
description={i18n.t("model.overviewDescription")}
|
||||
/>
|
||||
<section id="model-overview" data-section="model-specs" aria-label={i18n.t("nav.overview")}>
|
||||
<For each={specs()}>
|
||||
{(spec) => (
|
||||
<div data-component="model-spec">
|
||||
<span>{spec.label}</span>
|
||||
<strong>{spec.value}</strong>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function ModelMomentumSection(props: { data: StatsModelData | null }) {
|
||||
const i18n = useI18n()
|
||||
const language = useLanguage()
|
||||
return (
|
||||
<section id="momentum" data-section="model-momentum">
|
||||
<h2 data-slot="model-momentum-title">
|
||||
<a href="#momentum">{i18n.t("model.momentum")}.</a>
|
||||
<span>{i18n.t("model.overviewDescription")}</span>
|
||||
</h2>
|
||||
<div data-slot="model-momentum-pattern" aria-hidden="true" />
|
||||
<Show
|
||||
when={props.data}
|
||||
fallback={
|
||||
|
|
@ -439,189 +482,85 @@ function ModelOverview(props: { data: StatsModelData | null }) {
|
|||
}
|
||||
>
|
||||
{(data) => (
|
||||
<div data-component="model-metric-grid">
|
||||
<MetricCard
|
||||
label={i18n.t("model.tokens")}
|
||||
value={formatTokens(data().totals.tokens)}
|
||||
detail={i18n.t("model.lastTwoMonths")}
|
||||
/>
|
||||
<MetricCard
|
||||
label={i18n.t("model.uniqueUsers")}
|
||||
value={formatUsers(data().totals.uniqueUsers)}
|
||||
detail={i18n.t("model.lastTwoMonths")}
|
||||
/>
|
||||
<MetricCard
|
||||
label={i18n.t("model.sessions")}
|
||||
value={formatInteger(data().totals.sessions)}
|
||||
detail={i18n.t("model.completedSessions")}
|
||||
/>
|
||||
<MetricCard
|
||||
label={i18n.t("model.tokenShare")}
|
||||
value={formatPercent(data().tokenShare)}
|
||||
detail={i18n.t("model.totalModels", { count: data().totalModels })}
|
||||
/>
|
||||
<MetricCard
|
||||
label={i18n.t("model.momentum")}
|
||||
value={formatChange(data().tokenChange)}
|
||||
detail={i18n.t("model.vsPreviousWindow")}
|
||||
state={data().tokenChange < 0 ? "negative" : "positive"}
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
<MomentumChart data={data()} locale={language.tag(language.locale())} />
|
||||
<div data-slot="model-momentum-metrics">
|
||||
<MomentumMetric label={i18n.t("model.uniqueUsers")} value={formatUsers(data().totals.uniqueUsers)} />
|
||||
<MomentumMetric
|
||||
label={capitalizeLabel(i18n.t("model.completedSessions"))}
|
||||
value={formatInteger(data().totals.sessions)}
|
||||
/>
|
||||
<MomentumMetric label={i18n.t("model.tokenShare")} value={formatPercent(data().tokenShare)} />
|
||||
<MomentumMetric
|
||||
label="Rank"
|
||||
value={formatRankLabel(data().rank)}
|
||||
watermark={formatRankLabel(data().rank)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Show>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function ModelUsageSection(props: { data: ModelUsagePoint[] }) {
|
||||
const i18n = useI18n()
|
||||
function MomentumChart(props: { data: StatsModelData; locale: string }) {
|
||||
const chart = createMemo(() => momentumChart(props.data.usage, props.data.updatedAt))
|
||||
const changeState = createMemo(() => (props.data.tokenChange < 0 ? "negative" : "positive"))
|
||||
return (
|
||||
<section id="usage" data-section="model-panel">
|
||||
<SectionTitle href="#usage" title={i18n.t("nav.usage")} description={i18n.t("model.usageDescription")} />
|
||||
<Show
|
||||
when={props.data.some((item) => item.tokens > 0)}
|
||||
fallback={
|
||||
<ModelEmptyState title={i18n.t("model.noUsageTitle")} description={i18n.t("model.noUsageDescription")} />
|
||||
}
|
||||
>
|
||||
<ModelColumnChart data={props.data} metric="tokens" ariaLabel={i18n.t("model.dailyTokenChart")} />
|
||||
</Show>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function ModelUsersSection(props: { data: ModelUsagePoint[] }) {
|
||||
const i18n = useI18n()
|
||||
return (
|
||||
<section id="users" data-section="model-panel">
|
||||
<SectionTitle href="#users" title={i18n.t("model.uniqueUsers")} description={i18n.t("model.usersDescription")} />
|
||||
<Show
|
||||
when={props.data.some((item) => item.users > 0)}
|
||||
fallback={
|
||||
<ModelEmptyState title={i18n.t("model.noUsersTitle")} description={i18n.t("model.noUsersDescription")} />
|
||||
}
|
||||
>
|
||||
<ModelColumnChart data={props.data} metric="users" ariaLabel={i18n.t("model.dailyUserChart")} />
|
||||
</Show>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function ModelColumnChart(props: { data: ModelUsagePoint[]; metric: "tokens" | "users"; ariaLabel: string }) {
|
||||
const i18n = useI18n()
|
||||
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 data-component="model-momentum-chart" role="img" aria-label="Recent model token momentum">
|
||||
<div data-slot="model-momentum-summary">
|
||||
<div data-slot="model-momentum-total">
|
||||
<strong>{formatTokens(props.data.totals.tokens)} tokens</strong>
|
||||
<span data-state={changeState()}>{formatChange(props.data.tokenChange)}</span>
|
||||
</div>
|
||||
<p>
|
||||
<span>{formatMomentumDate(chart().startDate, props.locale, props.data.updatedAt)}</span>
|
||||
<span aria-hidden="true">→</span>
|
||||
<span>{formatMomentumDate(chart().endDate, props.locale, props.data.updatedAt)}</span>
|
||||
</p>
|
||||
</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, i18n)}`}
|
||||
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
|
||||
}
|
||||
<div data-slot="model-momentum-plot">
|
||||
<svg viewBox="0 0 1200 370" preserveAspectRatio="none" aria-hidden="true">
|
||||
<path data-slot="model-momentum-line-muted" d={chart().previousPath} />
|
||||
<path data-slot="model-momentum-line-active" d={chart().currentPath} />
|
||||
<For each={chart().markers}>
|
||||
{(marker) => (
|
||||
<rect
|
||||
data-slot="model-momentum-marker"
|
||||
data-active={marker.active ? "true" : undefined}
|
||||
x={marker.x - 3}
|
||||
y={marker.y - 3}
|
||||
width="6"
|
||||
height="6"
|
||||
/>
|
||||
<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, i18n)}
|
||||
</span>
|
||||
<div data-slot="tooltip-divider" />
|
||||
<p>
|
||||
<span data-slot="tooltip-label">
|
||||
<i /> {i18n.t("chart.daily")} {modelUsageLabel(props.metric, i18n)}
|
||||
</span>
|
||||
<b>{formatModelUsageValue(active(), props.metric)}</b>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</For>
|
||||
</svg>
|
||||
<span data-slot="model-momentum-end" data-state={changeState()} style={chart().endStyle}>
|
||||
<i />
|
||||
{formatChange(props.data.tokenChange)}
|
||||
</span>
|
||||
</div>
|
||||
<div data-slot="model-momentum-months" aria-hidden="true">
|
||||
<For each={momentumMonthLabels(chart().startDate, props.locale, props.data.updatedAt)}>
|
||||
{(month) => <span style={{ left: `${month.x}%` }}>{month.label}</span>}
|
||||
</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", i18n: ReturnType<typeof useI18n>) {
|
||||
if (metric === "users") return i18n.t("format.users")
|
||||
return i18n.t("format.tokens")
|
||||
function MomentumMetric(props: { label: string; value: string; watermark?: string }) {
|
||||
return (
|
||||
<div data-component="model-momentum-metric">
|
||||
<Show when={props.watermark}>
|
||||
{(watermark) => <em aria-hidden="true">{watermark()}</em>}
|
||||
</Show>
|
||||
<span>{props.label}</span>
|
||||
<strong>{props.value}</strong>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ModelEfficiencySection(props: { data: StatsModelData | null; catalog: ModelCatalogEntry | null }) {
|
||||
|
|
@ -979,19 +918,110 @@ function formatGeoShare(value: number) {
|
|||
return `${value.toFixed(value > 0 && value < 1 ? 1 : 0)}%`
|
||||
}
|
||||
|
||||
function modelUsageHeight(tokens: number, max: number) {
|
||||
if (tokens <= 0) return 0
|
||||
return Math.max(2, Math.min(100, (tokens / max) * 100))
|
||||
function momentumChart(data: ModelUsagePoint[], updatedAt: string | null) {
|
||||
const fallbackDate = updatedAt ? formatMomentumDateLabel(updatedAt) : "JAN 1"
|
||||
const points = data.length > 1 ? data : [data[0] ?? emptyUsagePoint(fallbackDate), data[0] ?? emptyUsagePoint(fallbackDate)]
|
||||
const max = Math.max(1, ...points.map((point) => point.tokens))
|
||||
const split = Math.max(1, Math.floor((points.length - 1) / 2))
|
||||
const coordinates = points.map((point, index) => ({
|
||||
date: point.date,
|
||||
tokens: point.tokens,
|
||||
x: (index / Math.max(1, points.length - 1)) * 1200,
|
||||
y: 364 - (point.tokens / max) * 364,
|
||||
}))
|
||||
const end = coordinates[coordinates.length - 1]
|
||||
return {
|
||||
startDate: coordinates[0].date,
|
||||
endDate: end.date,
|
||||
previousPath: smoothLinePath(coordinates.slice(0, split + 1)),
|
||||
currentPath: smoothLinePath(coordinates.slice(split)),
|
||||
markers: [
|
||||
{ ...coordinates[0], active: false },
|
||||
{ ...coordinates[split], active: true },
|
||||
{ ...end, active: true },
|
||||
],
|
||||
endStyle: {
|
||||
"--momentum-end-x": `${Math.min(94, Math.max(0, ((end.x + 8) / 1200) * 100))}%`,
|
||||
"--momentum-end-y": `${Math.min(96, Math.max(0, ((end.y - 7) / 370) * 100))}%`,
|
||||
} as JSX.CSSProperties,
|
||||
}
|
||||
}
|
||||
|
||||
function isModelUsageDense(count: number) {
|
||||
return count > 20
|
||||
function emptyUsagePoint(date: string): ModelUsagePoint {
|
||||
return { date, tokens: 0, users: 0, sessions: 0, cost: 0 }
|
||||
}
|
||||
|
||||
function isModelUsageLabelHidden(index: number, count: number) {
|
||||
if (count <= 16) return false
|
||||
const interval = Math.ceil(count / 8)
|
||||
return index !== count - 1 && index % interval !== 0
|
||||
function smoothLinePath(points: { x: number; y: number }[]) {
|
||||
if (points.length === 0) return ""
|
||||
if (points.length === 1) return `M${formatSparklinePoint(points[0].x)} ${formatSparklinePoint(points[0].y)}`
|
||||
return points
|
||||
.map((point, index) => {
|
||||
if (index === 0) return `M${formatSparklinePoint(point.x)} ${formatSparklinePoint(point.y)}`
|
||||
const previous = points[index - 1]
|
||||
const next = points[index + 1] ?? point
|
||||
const beforePrevious = points[index - 2] ?? previous
|
||||
const controlStart = {
|
||||
x: previous.x + (point.x - beforePrevious.x) / 6,
|
||||
y: previous.y + (point.y - beforePrevious.y) / 6,
|
||||
}
|
||||
const controlEnd = {
|
||||
x: point.x - (next.x - previous.x) / 6,
|
||||
y: point.y - (next.y - previous.y) / 6,
|
||||
}
|
||||
return `C${formatSparklinePoint(controlStart.x)} ${formatSparklinePoint(controlStart.y)} ${formatSparklinePoint(controlEnd.x)} ${formatSparklinePoint(controlEnd.y)} ${formatSparklinePoint(point.x)} ${formatSparklinePoint(point.y)}`
|
||||
})
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
function momentumMonthLabels(startDate: string, locale: string, updatedAt: string | null) {
|
||||
const start = parseMomentumDate(startDate, updatedAt)
|
||||
const first = new Date(Date.UTC(start.getUTCFullYear(), start.getUTCMonth() - 1, 1))
|
||||
return Array.from({ length: 5 }, (_, index) => {
|
||||
const date = new Date(Date.UTC(first.getUTCFullYear(), first.getUTCMonth() + index, 1))
|
||||
return {
|
||||
label: new Intl.DateTimeFormat(locale, { month: "short", timeZone: "UTC" }).format(date).toUpperCase(),
|
||||
x: index === 4 ? 98 : index * 24.5,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function formatMomentumDate(date: string, locale: string, updatedAt: string | null) {
|
||||
return new Intl.DateTimeFormat(locale, {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
timeZone: "UTC",
|
||||
})
|
||||
.format(parseMomentumDate(date, updatedAt))
|
||||
.toUpperCase()
|
||||
}
|
||||
|
||||
function parseMomentumDate(date: string, updatedAt: string | null): Date {
|
||||
const iso = /^(\d{4})-(\d{2})-(\d{2})/.exec(date)
|
||||
if (iso) return new Date(Date.UTC(Number(iso[1]), Number(iso[2]) - 1, Number(iso[3])))
|
||||
|
||||
const label = /^([A-Za-z]{3})\s+(\d{1,2})$/.exec(date.trim())
|
||||
const month = label ? shortMonths.findIndex((item) => item.toLowerCase() === label[1].toLowerCase()) : -1
|
||||
if (!label || month < 0) return new Date(Date.UTC(1970, 0, 1))
|
||||
|
||||
const anchor = updatedAt ? parseMomentumDate(updatedAt, null) : new Date(Date.UTC(1970, 0, 1))
|
||||
const year = month > anchor.getUTCMonth() + 1 ? anchor.getUTCFullYear() - 1 : anchor.getUTCFullYear()
|
||||
return new Date(Date.UTC(year, month, Number(label[2])))
|
||||
}
|
||||
|
||||
function formatMomentumDateLabel(date: string) {
|
||||
const parsed = parseMomentumDate(date, null)
|
||||
if (parsed.getUTCFullYear() === 1970) return "JAN 1"
|
||||
return `${shortMonths[parsed.getUTCMonth()]} ${parsed.getUTCDate()}`
|
||||
}
|
||||
|
||||
function formatRankLabel(rank: number | null) {
|
||||
if (rank === null) return "--"
|
||||
return `#${String(rank).padStart(2, "0")}`
|
||||
}
|
||||
|
||||
function capitalizeLabel(value: string) {
|
||||
return value.charAt(0).toUpperCase() + value.slice(1)
|
||||
}
|
||||
|
||||
function formatRankMove(change: number) {
|
||||
|
|
@ -1050,6 +1080,28 @@ function formatTokens(value: number) {
|
|||
return String(Math.round(value))
|
||||
}
|
||||
|
||||
function formatCatalogLimit(value: number | undefined, unknown: string) {
|
||||
return value === undefined ? unknown : formatTokens(value)
|
||||
}
|
||||
|
||||
function formatCatalogMonth(value: string | undefined, locale: string, unknown: string) {
|
||||
if (!value) return unknown
|
||||
const match = /^(\d{4})(?:-(\d{2}))?(?:-(\d{2}))?$/.exec(value)
|
||||
if (!match) return value
|
||||
return new Intl.DateTimeFormat(locale, {
|
||||
month: match[2] ? "short" : undefined,
|
||||
year: "numeric",
|
||||
timeZone: "UTC",
|
||||
}).format(new Date(Date.UTC(Number(match[1]), match[2] ? Number(match[2]) - 1 : 0, 1)))
|
||||
}
|
||||
|
||||
function formatCatalogModalities(values: string[], locale: string, unknown: string) {
|
||||
if (values.length === 0) return unknown
|
||||
const labels = values.map((value) => value.replace(/[-_]/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()))
|
||||
if (labels.length === 1) return labels[0] ?? unknown
|
||||
return new Intl.ListFormat(locale, { style: "long", type: "conjunction" }).format(labels)
|
||||
}
|
||||
|
||||
function formatInteger(value: number) {
|
||||
return new Intl.NumberFormat("en").format(value)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3335,6 +3335,321 @@
|
|||
height: 24px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-section="model-specs"] {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
min-height: 144px;
|
||||
color: var(--stats-text);
|
||||
box-shadow:
|
||||
inset 0 1px var(--stats-line),
|
||||
inset 0 -1px var(--stats-line),
|
||||
inset 1px 0 var(--stats-line),
|
||||
inset -1px 0 var(--stats-line);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-spec"] {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
min-height: 144px;
|
||||
box-sizing: border-box;
|
||||
padding: 40px;
|
||||
border-left: 1px solid var(--stats-line);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-spec"]:first-child {
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-spec"] span {
|
||||
color: var(--stats-muted);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-spec"] strong {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: var(--stats-text);
|
||||
font-size: 28px;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
letter-spacing: 0;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-section="model-momentum"] {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
display: grid;
|
||||
gap: 40px;
|
||||
min-height: 938px;
|
||||
padding: 80px 40px;
|
||||
color: var(--stats-text);
|
||||
box-shadow:
|
||||
inset 1px 0 var(--stats-line),
|
||||
inset -1px 0 var(--stats-line);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-momentum-title"] {
|
||||
min-width: 0;
|
||||
height: 42px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
color: var(--stats-muted);
|
||||
font-size: 28px;
|
||||
font-weight: 400;
|
||||
line-height: 42px;
|
||||
letter-spacing: 0;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-momentum-title"] a {
|
||||
color: var(--stats-text);
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-momentum-title"] a:hover,
|
||||
[data-page="stats"] [data-slot="model-momentum-title"] a:focus-visible {
|
||||
color: var(--stats-text);
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-momentum-title"] span {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-momentum-pattern"] {
|
||||
width: 100%;
|
||||
height: 16px;
|
||||
overflow: hidden;
|
||||
background: var(--stats-line);
|
||||
mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0H2V2H0V0Z' fill='black'/%3E%3C/svg%3E");
|
||||
mask-position: center top;
|
||||
mask-repeat: repeat;
|
||||
mask-size: 6px 6px;
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0H2V2H0V0Z' fill='black'/%3E%3C/svg%3E");
|
||||
-webkit-mask-position: center top;
|
||||
-webkit-mask-repeat: repeat;
|
||||
-webkit-mask-size: 6px 6px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-momentum-chart"] {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-rows: 64px 370px 14px;
|
||||
gap: 16px;
|
||||
height: 480px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-momentum-summary"] {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 8px;
|
||||
width: fit-content;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-momentum-total"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
min-width: 0;
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-momentum-total"] strong {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: var(--stats-text);
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
line-height: 42px;
|
||||
letter-spacing: 0;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-momentum-total"] span,
|
||||
[data-page="stats"] [data-slot="model-momentum-end"] {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 24px;
|
||||
box-sizing: border-box;
|
||||
padding: 0 8px;
|
||||
background: #198b4324;
|
||||
color: #198b43;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
line-height: 14px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-momentum-total"] span[data-state="negative"],
|
||||
[data-page="stats"] [data-slot="model-momentum-end"][data-state="negative"] {
|
||||
background: #b82d3524;
|
||||
color: #b82d35;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-momentum-summary"] p {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
color: var(--stats-muted);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 14px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-momentum-plot"] {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
height: 370px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-momentum-plot"] svg {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-momentum-line-muted"],
|
||||
[data-page="stats"] [data-slot="model-momentum-line-active"] {
|
||||
fill: none;
|
||||
stroke-width: 1.5;
|
||||
vector-effect: non-scaling-stroke;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-momentum-line-muted"] {
|
||||
stroke: var(--stats-bar-idle);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-momentum-line-active"] {
|
||||
stroke: #198b43;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-momentum-marker"] {
|
||||
fill: var(--stats-bar-idle);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-momentum-marker"][data-active="true"] {
|
||||
fill: #198b43;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-momentum-end"] {
|
||||
position: absolute;
|
||||
top: var(--momentum-end-y);
|
||||
left: var(--momentum-end-x);
|
||||
gap: 8px;
|
||||
height: 14px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-momentum-end"] i {
|
||||
display: block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-momentum-months"] {
|
||||
position: relative;
|
||||
height: 14px;
|
||||
color: var(--stats-muted);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-momentum-months"] span {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-momentum-months"] span:first-child {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-momentum-months"] span:last-child {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-momentum-metrics"] {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-momentum-metric"] {
|
||||
position: relative;
|
||||
display: grid;
|
||||
align-content: space-between;
|
||||
min-width: 0;
|
||||
min-height: 120px;
|
||||
box-sizing: border-box;
|
||||
padding: 16px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--stats-line);
|
||||
background:
|
||||
linear-gradient(180deg, color-mix(in srgb, var(--stats-bg) 76%, transparent), transparent),
|
||||
var(--stats-layer);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-momentum-metric"] span {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: var(--stats-muted);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-momentum-metric"] strong {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: var(--stats-text);
|
||||
font-size: 28px;
|
||||
font-weight: 500;
|
||||
line-height: 42px;
|
||||
letter-spacing: 0;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-momentum-metric"] em {
|
||||
position: absolute;
|
||||
top: -90px;
|
||||
left: -69px;
|
||||
color: var(--stats-text);
|
||||
font-size: 200px;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
line-height: 300px;
|
||||
opacity: 0.04;
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-section="lab-overview"] {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
|
|
@ -4966,6 +5281,7 @@
|
|||
[data-page="stats"] [data-section="session-cost"],
|
||||
[data-page="stats"] [data-section="model-hero"],
|
||||
[data-page="stats"] [data-section="lab-hero"],
|
||||
[data-page="stats"] [data-section="model-momentum"],
|
||||
[data-page="stats"] [data-section="model-panel"] {
|
||||
padding: 64px 32px;
|
||||
}
|
||||
|
|
@ -5056,6 +5372,48 @@
|
|||
min-height: 0;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-section="model-specs"] {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-spec"] {
|
||||
min-height: 132px;
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-spec"]:nth-child(3n + 1) {
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-spec"]:nth-child(n + 4) {
|
||||
border-top: 1px solid var(--stats-line);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-section="model-momentum"] {
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-momentum-title"] {
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
line-height: 1.5;
|
||||
text-overflow: clip;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-momentum-chart"] {
|
||||
grid-template-rows: auto 320px 14px;
|
||||
height: 434px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-momentum-plot"] {
|
||||
height: 320px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-momentum-metrics"] {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-metric-grid"],
|
||||
[data-page="stats"] [data-component="model-metric-grid"][data-variant="dense"],
|
||||
[data-page="stats"] [data-component="lab-model-grid"] {
|
||||
|
|
@ -5184,6 +5542,7 @@
|
|||
[data-page="stats"] [data-section="session-cost"],
|
||||
[data-page="stats"] [data-section="model-hero"],
|
||||
[data-page="stats"] [data-section="lab-hero"],
|
||||
[data-page="stats"] [data-section="model-momentum"],
|
||||
[data-page="stats"] [data-section="model-panel"] {
|
||||
padding: 48px 24px;
|
||||
}
|
||||
|
|
@ -5351,6 +5710,90 @@
|
|||
line-height: 22px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-section="model-specs"] {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-spec"] {
|
||||
min-height: 116px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-spec"]:nth-child(n) {
|
||||
border-left: 1px solid var(--stats-line);
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-spec"]:nth-child(2n + 1) {
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-spec"]:nth-child(n + 3) {
|
||||
border-top: 1px solid var(--stats-line);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-spec"]:last-child:nth-child(odd) {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-spec"] strong {
|
||||
font-size: 24px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-section="model-momentum"] {
|
||||
gap: 28px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-momentum-title"] {
|
||||
font-size: 20px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-momentum-title"] span {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-momentum-chart"] {
|
||||
grid-template-rows: auto 260px 14px;
|
||||
gap: 14px;
|
||||
height: 380px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-momentum-total"] {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-momentum-total"] strong {
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-momentum-summary"] p {
|
||||
flex-wrap: wrap;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-momentum-plot"] {
|
||||
height: 260px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-momentum-end"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-momentum-metric"] {
|
||||
min-height: 112px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-momentum-metric"] strong {
|
||||
font-size: clamp(20px, 5.5vw, 24px);
|
||||
line-height: 34px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="model-catalog-grid"] {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue