feat(stats): add model momentum section
This commit is contained in:
@@ -198,7 +198,7 @@ const en = {
|
|||||||
"model.knowledge": "Knowledge",
|
"model.knowledge": "Knowledge",
|
||||||
"model.release": "Release",
|
"model.release": "Release",
|
||||||
"model.inputs": "Inputs",
|
"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.noSummaryTitle": "No usage summary",
|
||||||
"model.noSummaryDescription": "This model has no OpenCode Go usage rows yet.",
|
"model.noSummaryDescription": "This model has no OpenCode Go usage rows yet.",
|
||||||
"model.tokens": "Tokens",
|
"model.tokens": "Tokens",
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import {
|
|||||||
const statsUnfurlPath = "banner.png"
|
const statsUnfurlPath = "banner.png"
|
||||||
const geoMapWidth = 960
|
const geoMapWidth = 960
|
||||||
const geoMapHeight = 430
|
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 IsoCountryCode = readonly [string, string, string]
|
||||||
type WorldCountryProperties = GeoJsonProperties & { name?: string }
|
type WorldCountryProperties = GeoJsonProperties & { name?: string }
|
||||||
@@ -119,8 +120,7 @@ export default function StatsModel() {
|
|||||||
const statsUnfurlUrl = new URL(statsUnfurlPath, localizedUrl("en", "/data/")).toString()
|
const statsUnfurlUrl = new URL(statsUnfurlPath, localizedUrl("en", "/data/")).toString()
|
||||||
const modelHeaderLinks = createMemo<readonly HeaderLink[]>(() => [
|
const modelHeaderLinks = createMemo<readonly HeaderLink[]>(() => [
|
||||||
{ href: "#overview", label: i18n.t("nav.overview") },
|
{ href: "#overview", label: i18n.t("nav.overview") },
|
||||||
{ href: "#usage", label: i18n.t("nav.usage") },
|
{ href: "#momentum", label: i18n.t("model.momentum") },
|
||||||
{ href: "#users", label: i18n.t("nav.users") },
|
|
||||||
{ href: "#efficiency", label: i18n.t("nav.efficiency") },
|
{ href: "#efficiency", label: i18n.t("nav.efficiency") },
|
||||||
{ href: "#geo-breakdown", label: i18n.t("nav.geoBreakdown") },
|
{ href: "#geo-breakdown", label: i18n.t("nav.geoBreakdown") },
|
||||||
{ href: "#peers", label: i18n.t("nav.peers") },
|
{ href: "#peers", label: i18n.t("nav.peers") },
|
||||||
@@ -181,9 +181,8 @@ export default function StatsModel() {
|
|||||||
catalogData={catalog() ?? null}
|
catalogData={catalog() ?? null}
|
||||||
labName={labName()}
|
labName={labName()}
|
||||||
/>
|
/>
|
||||||
<ModelOverview data={stats() ?? null} />
|
<ModelOverview catalog={catalogEntry() ?? null} />
|
||||||
<ModelUsageSection data={stats()?.usage ?? []} />
|
<ModelMomentumSection data={stats() ?? null} />
|
||||||
<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} />
|
||||||
@@ -423,15 +422,59 @@ function ChevronDownIcon() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ModelOverview(props: { data: StatsModelData | null }) {
|
function ModelOverview(props: { catalog: ModelCatalogEntry | null }) {
|
||||||
const i18n = useI18n()
|
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 (
|
return (
|
||||||
<section id="model-overview" data-section="model-panel">
|
<section id="model-overview" data-section="model-specs" aria-label={i18n.t("nav.overview")}>
|
||||||
<SectionTitle
|
<For each={specs()}>
|
||||||
href="#model-overview"
|
{(spec) => (
|
||||||
title={i18n.t("nav.overview")}
|
<div data-component="model-spec">
|
||||||
description={i18n.t("model.overviewDescription")}
|
<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
|
<Show
|
||||||
when={props.data}
|
when={props.data}
|
||||||
fallback={
|
fallback={
|
||||||
@@ -439,189 +482,85 @@ function ModelOverview(props: { data: StatsModelData | null }) {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{(data) => (
|
{(data) => (
|
||||||
<div data-component="model-metric-grid">
|
<>
|
||||||
<MetricCard
|
<MomentumChart data={data()} locale={language.tag(language.locale())} />
|
||||||
label={i18n.t("model.tokens")}
|
<div data-slot="model-momentum-metrics">
|
||||||
value={formatTokens(data().totals.tokens)}
|
<MomentumMetric label={i18n.t("model.uniqueUsers")} value={formatUsers(data().totals.uniqueUsers)} />
|
||||||
detail={i18n.t("model.lastTwoMonths")}
|
<MomentumMetric
|
||||||
/>
|
label={capitalizeLabel(i18n.t("model.completedSessions"))}
|
||||||
<MetricCard
|
value={formatInteger(data().totals.sessions)}
|
||||||
label={i18n.t("model.uniqueUsers")}
|
/>
|
||||||
value={formatUsers(data().totals.uniqueUsers)}
|
<MomentumMetric label={i18n.t("model.tokenShare")} value={formatPercent(data().tokenShare)} />
|
||||||
detail={i18n.t("model.lastTwoMonths")}
|
<MomentumMetric
|
||||||
/>
|
label="Rank"
|
||||||
<MetricCard
|
value={formatRankLabel(data().rank)}
|
||||||
label={i18n.t("model.sessions")}
|
watermark={formatRankLabel(data().rank)}
|
||||||
value={formatInteger(data().totals.sessions)}
|
/>
|
||||||
detail={i18n.t("model.completedSessions")}
|
</div>
|
||||||
/>
|
</>
|
||||||
<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>
|
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ModelUsageSection(props: { data: ModelUsagePoint[] }) {
|
function MomentumChart(props: { data: StatsModelData; locale: string }) {
|
||||||
const i18n = useI18n()
|
const chart = createMemo(() => momentumChart(props.data.usage, props.data.updatedAt))
|
||||||
|
const changeState = createMemo(() => (props.data.tokenChange < 0 ? "negative" : "positive"))
|
||||||
return (
|
return (
|
||||||
<section id="usage" data-section="model-panel">
|
<div data-component="model-momentum-chart" role="img" aria-label="Recent model token momentum">
|
||||||
<SectionTitle href="#usage" title={i18n.t("nav.usage")} description={i18n.t("model.usageDescription")} />
|
<div data-slot="model-momentum-summary">
|
||||||
<Show
|
<div data-slot="model-momentum-total">
|
||||||
when={props.data.some((item) => item.tokens > 0)}
|
<strong>{formatTokens(props.data.totals.tokens)} tokens</strong>
|
||||||
fallback={
|
<span data-state={changeState()}>{formatChange(props.data.tokenChange)}</span>
|
||||||
<ModelEmptyState title={i18n.t("model.noUsageTitle")} description={i18n.t("model.noUsageDescription")} />
|
</div>
|
||||||
}
|
<p>
|
||||||
>
|
<span>{formatMomentumDate(chart().startDate, props.locale, props.data.updatedAt)}</span>
|
||||||
<ModelColumnChart data={props.data} metric="tokens" ariaLabel={i18n.t("model.dailyTokenChart")} />
|
<span aria-hidden="true">→</span>
|
||||||
</Show>
|
<span>{formatMomentumDate(chart().endDate, props.locale, props.data.updatedAt)}</span>
|
||||||
</section>
|
</p>
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
</div>
|
||||||
<div data-slot="model-usage-bars">
|
<div data-slot="model-momentum-plot">
|
||||||
<For each={props.data}>
|
<svg viewBox="0 0 1200 370" preserveAspectRatio="none" aria-hidden="true">
|
||||||
{(point, index) => (
|
<path data-slot="model-momentum-line-muted" d={chart().previousPath} />
|
||||||
<div
|
<path data-slot="model-momentum-line-active" d={chart().currentPath} />
|
||||||
data-slot="model-usage-column"
|
<For each={chart().markers}>
|
||||||
role="button"
|
{(marker) => (
|
||||||
tabIndex={0}
|
<rect
|
||||||
aria-label={`${point.date} ${formatModelUsageValue(point, props.metric)} ${modelUsageLabel(props.metric, i18n)}`}
|
data-slot="model-momentum-marker"
|
||||||
data-active={activeIndex() === index() ? "true" : undefined}
|
data-active={marker.active ? "true" : undefined}
|
||||||
data-muted={activeIndex() !== undefined && activeIndex() !== index() ? "true" : undefined}
|
x={marker.x - 3}
|
||||||
onPointerDown={(event) => {
|
y={marker.y - 3}
|
||||||
if (event.pointerType !== "touch") return
|
width="6"
|
||||||
setActiveIndex(index())
|
height="6"
|
||||||
}}
|
|
||||||
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) => (
|
</For>
|
||||||
<div
|
</svg>
|
||||||
data-component="chart-tooltip"
|
<span data-slot="model-momentum-end" data-state={changeState()} style={chart().endStyle}>
|
||||||
data-placement={index() > props.data.length * 0.62 ? "left" : "right"}
|
<i />
|
||||||
>
|
{formatChange(props.data.tokenChange)}
|
||||||
<strong>{active().date}</strong>
|
</span>
|
||||||
<span>
|
</div>
|
||||||
{formatModelUsageValue(active(), props.metric)} {modelUsageLabel(props.metric, i18n)}
|
<div data-slot="model-momentum-months" aria-hidden="true">
|
||||||
</span>
|
<For each={momentumMonthLabels(chart().startDate, props.locale, props.data.updatedAt)}>
|
||||||
<div data-slot="tooltip-divider" />
|
{(month) => <span style={{ left: `${month.x}%` }}>{month.label}</span>}
|
||||||
<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>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function modelUsageMetricValue(point: ModelUsagePoint, metric: "tokens" | "users") {
|
function MomentumMetric(props: { label: string; value: string; watermark?: string }) {
|
||||||
if (metric === "users") return point.users
|
return (
|
||||||
return point.tokens
|
<div data-component="model-momentum-metric">
|
||||||
}
|
<Show when={props.watermark}>
|
||||||
|
{(watermark) => <em aria-hidden="true">{watermark()}</em>}
|
||||||
function formatModelUsageValue(point: ModelUsagePoint, metric: "tokens" | "users") {
|
</Show>
|
||||||
if (metric === "users") return formatUsers(point.users)
|
<span>{props.label}</span>
|
||||||
return formatTokens(point.tokens)
|
<strong>{props.value}</strong>
|
||||||
}
|
</div>
|
||||||
|
)
|
||||||
function modelUsageLabel(metric: "tokens" | "users", i18n: ReturnType<typeof useI18n>) {
|
|
||||||
if (metric === "users") return i18n.t("format.users")
|
|
||||||
return i18n.t("format.tokens")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ModelEfficiencySection(props: { data: StatsModelData | null; catalog: ModelCatalogEntry | null }) {
|
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)}%`
|
return `${value.toFixed(value > 0 && value < 1 ? 1 : 0)}%`
|
||||||
}
|
}
|
||||||
|
|
||||||
function modelUsageHeight(tokens: number, max: number) {
|
function momentumChart(data: ModelUsagePoint[], updatedAt: string | null) {
|
||||||
if (tokens <= 0) return 0
|
const fallbackDate = updatedAt ? formatMomentumDateLabel(updatedAt) : "JAN 1"
|
||||||
return Math.max(2, Math.min(100, (tokens / max) * 100))
|
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) {
|
function emptyUsagePoint(date: string): ModelUsagePoint {
|
||||||
return count > 20
|
return { date, tokens: 0, users: 0, sessions: 0, cost: 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
function isModelUsageLabelHidden(index: number, count: number) {
|
function smoothLinePath(points: { x: number; y: number }[]) {
|
||||||
if (count <= 16) return false
|
if (points.length === 0) return ""
|
||||||
const interval = Math.ceil(count / 8)
|
if (points.length === 1) return `M${formatSparklinePoint(points[0].x)} ${formatSparklinePoint(points[0].y)}`
|
||||||
return index !== count - 1 && index % interval !== 0
|
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) {
|
function formatRankMove(change: number) {
|
||||||
@@ -1050,6 +1080,28 @@ function formatTokens(value: number) {
|
|||||||
return String(Math.round(value))
|
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) {
|
function formatInteger(value: number) {
|
||||||
return new Intl.NumberFormat("en").format(value)
|
return new Intl.NumberFormat("en").format(value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3335,6 +3335,321 @@
|
|||||||
height: 24px;
|
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"] {
|
[data-page="stats"] [data-section="lab-overview"] {
|
||||||
position: relative;
|
position: relative;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -4966,6 +5281,7 @@
|
|||||||
[data-page="stats"] [data-section="session-cost"],
|
[data-page="stats"] [data-section="session-cost"],
|
||||||
[data-page="stats"] [data-section="model-hero"],
|
[data-page="stats"] [data-section="model-hero"],
|
||||||
[data-page="stats"] [data-section="lab-hero"],
|
[data-page="stats"] [data-section="lab-hero"],
|
||||||
|
[data-page="stats"] [data-section="model-momentum"],
|
||||||
[data-page="stats"] [data-section="model-panel"] {
|
[data-page="stats"] [data-section="model-panel"] {
|
||||||
padding: 64px 32px;
|
padding: 64px 32px;
|
||||||
}
|
}
|
||||||
@@ -5056,6 +5372,48 @@
|
|||||||
min-height: 0;
|
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-page="stats"] [data-component="model-metric-grid"][data-variant="dense"],
|
[data-page="stats"] [data-component="model-metric-grid"][data-variant="dense"],
|
||||||
[data-page="stats"] [data-component="lab-model-grid"] {
|
[data-page="stats"] [data-component="lab-model-grid"] {
|
||||||
@@ -5184,6 +5542,7 @@
|
|||||||
[data-page="stats"] [data-section="session-cost"],
|
[data-page="stats"] [data-section="session-cost"],
|
||||||
[data-page="stats"] [data-section="model-hero"],
|
[data-page="stats"] [data-section="model-hero"],
|
||||||
[data-page="stats"] [data-section="lab-hero"],
|
[data-page="stats"] [data-section="lab-hero"],
|
||||||
|
[data-page="stats"] [data-section="model-momentum"],
|
||||||
[data-page="stats"] [data-section="model-panel"] {
|
[data-page="stats"] [data-section="model-panel"] {
|
||||||
padding: 48px 24px;
|
padding: 48px 24px;
|
||||||
}
|
}
|
||||||
@@ -5351,6 +5710,90 @@
|
|||||||
line-height: 22px;
|
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"] {
|
[data-page="stats"] [data-slot="model-catalog-grid"] {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
|||||||
Reference in New Issue
Block a user