Jelajahi Sumber

feat(stats): add model momentum section

Adam 1 bulan lalu
induk
melakukan
7d84cb05ef

+ 1 - 1
packages/stats/app/src/i18n.ts

@@ -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",

+ 237 - 185
packages/stats/app/src/routes/[lab]/[model].tsx

@@ -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,205 +422,145 @@ 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")}
-      />
-      <Show
-        when={props.data}
-        fallback={
-          <ModelEmptyState title={i18n.t("model.noSummaryTitle")} description={i18n.t("model.noSummaryDescription")} />
-        }
-      >
-        {(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"}
-            />
+    <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>
         )}
-      </Show>
-    </section>
-  )
-}
-
-function ModelUsageSection(props: { data: ModelUsagePoint[] }) {
-  const i18n = useI18n()
-  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>
+      </For>
     </section>
   )
 }
 
-function ModelUsersSection(props: { data: ModelUsagePoint[] }) {
+function ModelMomentumSection(props: { data: StatsModelData | null }) {
   const i18n = useI18n()
+  const language = useLanguage()
   return (
-    <section id="users" data-section="model-panel">
-      <SectionTitle href="#users" title={i18n.t("model.uniqueUsers")} description={i18n.t("model.usersDescription")} />
+    <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.some((item) => item.users > 0)}
+        when={props.data}
         fallback={
-          <ModelEmptyState title={i18n.t("model.noUsersTitle")} description={i18n.t("model.noUsersDescription")} />
+          <ModelEmptyState title={i18n.t("model.noSummaryTitle")} description={i18n.t("model.noSummaryDescription")} />
         }
       >
-        <ModelColumnChart data={props.data} metric="users" ariaLabel={i18n.t("model.dailyUserChart")} />
+        {(data) => (
+          <>
+            <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 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]
-  })
-
+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 (
-    <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 emptyUsagePoint(date: string): ModelUsagePoint {
+  return { date, tokens: 0, users: 0, sessions: 0, cost: 0 }
 }
 
-function isModelUsageDense(count: number) {
-  return count > 20
+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 isModelUsageLabelHidden(index: number, count: number) {
-  if (count <= 16) return false
-  const interval = Math.ceil(count / 8)
-  return index !== count - 1 && index % interval !== 0
+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)
 }

+ 443 - 0
packages/stats/app/src/routes/index.css

@@ -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));