Преглед изворни кода

feat(stats): add clickable section headings (#34095)

Adam пре 1 месец
родитељ
комит
82f47cb312

+ 37 - 16
packages/stats/app/src/routes/[lab]/[model].tsx

@@ -30,6 +30,7 @@ import {
   type ModelCatalogCost,
   type ModelCatalogEntry,
 } from "../model-catalog"
+import { SectionHeading } from "../section-heading"
 import { runStatsEffect } from "../../stats-runtime"
 import { setStatsPageCacheHeaders } from "../stats-cache"
 import {
@@ -205,7 +206,11 @@ function ModelLoading() {
             <a data-slot="model-back-link" href={language.route(import.meta.env.BASE_URL)}>
               {i18n.t("footer.modelData")}
             </a>
-            <h1>{i18n.t("model.loadingTitle")}</h1>
+            <h1>
+              <a data-slot="heading-link" href="#overview">
+                {i18n.t("model.loadingTitle")}
+              </a>
+            </h1>
             <p>{i18n.t("model.loadingDescription")}</p>
           </div>
         </div>
@@ -228,7 +233,11 @@ function ModelNotFound(props: { lab: string; model: string }) {
             <a data-slot="model-back-link" href={language.route(import.meta.env.BASE_URL)}>
               {i18n.t("footer.modelData")}
             </a>
-            <h1>{props.model || i18n.t("model.fallback")}</h1>
+            <h1>
+              <a data-slot="heading-link" href="#overview">
+                {props.model || i18n.t("model.fallback")}
+              </a>
+            </h1>
             <p>{i18n.t("model.noMatched", { id: props.lab ? `${props.lab}/${props.model}` : props.model })}</p>
           </div>
         </div>
@@ -260,7 +269,11 @@ function ModelHero(props: { data: StatsModelData | null; catalog: ModelCatalogEn
             </a>
             <span data-slot="model-id-tag">{modelId()}</span>
           </div>
-          <h1>{props.catalog?.name ?? props.data?.model ?? i18n.t("model.fallback")}</h1>
+          <h1>
+            <a data-slot="heading-link" href="#overview">
+              {props.catalog?.name ?? props.data?.model ?? i18n.t("model.fallback")}
+            </a>
+          </h1>
           <Show when={props.data} fallback={<p>{i18n.t("model.catalogFallback")}</p>}>
             {(data) => (
               <p>
@@ -352,8 +365,12 @@ function CatalogDatum(props: { label: string; value: string }) {
 function ModelOverview(props: { data: StatsModelData | null }) {
   const i18n = useI18n()
   return (
-    <section data-section="model-panel">
-      <SectionTitle title={i18n.t("nav.overview")} description={i18n.t("model.overviewDescription")} />
+    <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={
@@ -399,7 +416,7 @@ function ModelUsageSection(props: { data: ModelUsagePoint[] }) {
   const i18n = useI18n()
   return (
     <section id="usage" data-section="model-panel">
-      <SectionTitle title={i18n.t("nav.usage")} description={i18n.t("model.usageDescription")} />
+      <SectionTitle href="#usage" title={i18n.t("nav.usage")} description={i18n.t("model.usageDescription")} />
       <Show
         when={props.data.some((item) => item.tokens > 0)}
         fallback={
@@ -416,7 +433,7 @@ function ModelUsersSection(props: { data: ModelUsagePoint[] }) {
   const i18n = useI18n()
   return (
     <section id="users" data-section="model-panel">
-      <SectionTitle title={i18n.t("model.uniqueUsers")} description={i18n.t("model.usersDescription")} />
+      <SectionTitle href="#users" title={i18n.t("model.uniqueUsers")} description={i18n.t("model.usersDescription")} />
       <Show
         when={props.data.some((item) => item.users > 0)}
         fallback={
@@ -550,7 +567,11 @@ function ModelEfficiencySection(props: { data: StatsModelData | null; catalog: M
   const i18n = useI18n()
   return (
     <section id="efficiency" data-section="model-panel">
-      <SectionTitle title={i18n.t("nav.efficiency")} description={i18n.t("model.efficiencyDescription")} />
+      <SectionTitle
+        href="#efficiency"
+        title={i18n.t("nav.efficiency")}
+        description={i18n.t("model.efficiencyDescription")}
+      />
       <Show
         when={props.data}
         fallback={
@@ -623,7 +644,11 @@ function ModelGeoBreakdownSection(props: { data: Record<UsageRange, CountryEntry
         setActiveCountry(undefined)
       }}
     >
-      <SectionTitle title={i18n.t("nav.geoBreakdown")} description={i18n.t("model.geoDescription")} />
+      <SectionTitle
+        href="#geo-breakdown"
+        title={i18n.t("nav.geoBreakdown")}
+        description={i18n.t("model.geoDescription")}
+      />
       <Show
         when={data().length > 0}
         fallback={<ModelEmptyState title={i18n.t("model.noGeoTitle")} description={i18n.t("model.noGeoDescription")} />}
@@ -788,7 +813,7 @@ function ModelPeersSection(props: { data: StatsModelData | null }) {
   const i18n = useI18n()
   return (
     <section id="peers" data-section="model-panel">
-      <SectionTitle title={i18n.t("nav.peers")} description={i18n.t("model.peersDescription")} />
+      <SectionTitle href="#peers" title={i18n.t("nav.peers")} description={i18n.t("model.peersDescription")} />
       <Show
         when={props.data?.peers.length}
         fallback={
@@ -833,12 +858,8 @@ function PeerRow(props: { peer: ModelPeerEntry; active: boolean }) {
   )
 }
 
-function SectionTitle(props: { title: string; description: string }) {
-  return (
-    <p data-slot="section-title">
-      <strong>{props.title}.</strong> <span>{props.description}</span>
-    </p>
-  )
+function SectionTitle(props: { href: string; title: string; description: string }) {
+  return <SectionHeading href={props.href} title={props.title} description={props.description} />
 }
 
 function ModelEmptyState(props: { title: string; description: string; compact?: boolean }) {

+ 26 - 11
packages/stats/app/src/routes/[lab]/index.tsx

@@ -20,6 +20,7 @@ import {
   type ModelCatalogEntry,
   type ModelCatalogLab,
 } from "../model-catalog"
+import { SectionHeading } from "../section-heading"
 import { runStatsEffect } from "../../stats-runtime"
 import { setStatsPageCacheHeaders } from "../stats-cache"
 import {
@@ -148,7 +149,11 @@ function LabLoading() {
           <a data-slot="model-back-link" href={language.route(import.meta.env.BASE_URL)}>
             {i18n.t("footer.modelData")}
           </a>
-          <h1>{i18n.t("lab.loadingTitle")}</h1>
+          <h1>
+            <a data-slot="heading-link" href="#overview">
+              {i18n.t("lab.loadingTitle")}
+            </a>
+          </h1>
           <p>{i18n.t("lab.loadingDescription")}</p>
         </div>
       </div>
@@ -166,7 +171,11 @@ function LabNotFound(props: { lab: string }) {
           <a data-slot="model-back-link" href={language.route(import.meta.env.BASE_URL)}>
             {i18n.t("footer.modelData")}
           </a>
-          <h1>{formatCatalogLabName(props.lab)}</h1>
+          <h1>
+            <a data-slot="heading-link" href="#overview">
+              {formatCatalogLabName(props.lab)}
+            </a>
+          </h1>
           <p>{i18n.t("lab.notFound")}</p>
         </div>
       </div>
@@ -193,7 +202,11 @@ function LabHero(props: { lab: ModelCatalogLab; stats: StatsLabData | null }) {
       </a>
       <div data-slot="model-hero-grid">
         <div data-slot="model-hero-copy">
-          <h1>{props.lab.name}</h1>
+          <h1>
+            <a data-slot="heading-link" href="#overview">
+              {props.lab.name}
+            </a>
+          </h1>
           <div data-slot="model-hero-pattern" aria-hidden="true" />
           <p>
             {i18n.t("lab.heroPrefix", { count: props.lab.models.length, lab: props.lab.name })}
@@ -235,10 +248,11 @@ function LabUsageSection(props: { lab: ModelCatalogLab; data: StatsLabData | nul
 
   return (
     <section id="usage" data-section="model-panel">
-      <p data-slot="section-title">
-        <strong>{i18n.t("lab.usageTitle", { lab: props.lab.name })}.</strong>{" "}
-        <span>{i18n.t("lab.usageDescription")}</span>
-      </p>
+      <SectionHeading
+        href="#usage"
+        title={i18n.t("lab.usageTitle", { lab: props.lab.name })}
+        description={i18n.t("lab.usageDescription")}
+      />
       <Show
         when={usage().some((item) => item.tokens > 0)}
         fallback={<LabEmptyState title={i18n.t("lab.noUsageTitle")} description={i18n.t("lab.noUsageDescription")} />}
@@ -336,10 +350,11 @@ function LabModelsSection(props: { lab: ModelCatalogLab; usage: LabUsageModelEnt
   const usageBySlug = createMemo(() => new Map(props.usage.map((item) => [item.slug, item])))
   return (
     <section id="models" data-section="model-panel">
-      <p data-slot="section-title">
-        <strong>{i18n.t("lab.modelsTitle", { lab: props.lab.name })}.</strong>{" "}
-        <span>{i18n.t("lab.recentUsageAndLimits")}</span>
-      </p>
+      <SectionHeading
+        href="#models"
+        title={i18n.t("lab.modelsTitle", { lab: props.lab.name })}
+        description={i18n.t("lab.recentUsageAndLimits")}
+      />
       <div data-component="lab-model-grid">
         <For each={props.lab.models}>
           {(model) => <LabModelCard model={model} usage={usageBySlug().get(model.slug)} />}

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

@@ -87,6 +87,11 @@
   display: none !important;
 }
 
+[data-page="stats"] section[id],
+[data-page="stats"] [data-component="leaderboard"][id] {
+  scroll-margin-top: 88px;
+}
+
 [data-page="stats"] [data-component="content"] {
   color: var(--stats-text);
   font-family:
@@ -1781,6 +1786,35 @@
   font-weight: 400;
 }
 
+[data-page="stats"] [data-slot="heading-link"] {
+  position: relative;
+  color: inherit;
+  text-decoration: none;
+}
+
+[data-page="stats"] [data-slot="heading-link"]:hover {
+  text-decoration: none;
+}
+
+[data-page="stats"] [data-slot="heading-link"]:focus-visible {
+  outline: 1px solid var(--stats-accent);
+  outline-offset: 4px;
+}
+
+[data-page="stats"] [data-slot="heading-anchor"] {
+  position: absolute;
+  top: -0.08em;
+  right: 100%;
+  margin-right: 0.48em;
+  color: var(--stats-accent);
+  opacity: 0;
+}
+
+[data-page="stats"] [data-slot="heading-link"]:hover [data-slot="heading-anchor"],
+[data-page="stats"] [data-slot="heading-link"]:focus-visible [data-slot="heading-anchor"] {
+  opacity: 1;
+}
+
 [data-page="stats"] [data-component="leaderboard"],
 [data-page="stats"] [data-slot="leaderboard-featured"],
 [data-page="stats"] [data-slot="leaderboard-compact"],

+ 51 - 18
packages/stats/app/src/routes/index.tsx

@@ -32,6 +32,7 @@ import { useI18n } from "../context/i18n"
 import { useLanguage } from "../context/language"
 import { localizedUrl } from "../lib/language"
 import { findModelCatalogEntry, getModelCatalog, type ModelCatalog } from "./model-catalog"
+import { SectionHeading } from "./section-heading"
 import { setStatsPageCacheHeaders } from "./stats-cache"
 import {
   applyThemePreference,
@@ -273,7 +274,11 @@ function Hero(props: { updatedAt: string | null }) {
       </p>
       <div data-slot="hero-canvas">
         <div data-slot="hero-pattern" aria-hidden="true" />
-        <h1>{i18n.t("footer.modelData")}</h1>
+        <h1>
+          <a data-slot="heading-link" href="#overview">
+            {i18n.t("footer.modelData")}
+          </a>
+        </h1>
         <p data-slot="hero-copy">{i18n.t("home.heroCopy")}</p>
       </div>
     </section>
@@ -296,7 +301,7 @@ function StatsLoading() {
   return (
     <>
       <Hero updatedAt={null} />
-      <ChartSection title={i18n.t("home.usageTitle")}>
+      <ChartSection id="top-models" title={i18n.t("home.usageTitle")}>
         <EmptyState title={i18n.t("home.loadingTitle")} description={i18n.t("home.loadingDescription")} />
       </ChartSection>
     </>
@@ -314,7 +319,15 @@ function ChartSection(props: {
     <section id={props.id} data-section="chart">
       <div data-slot="section-header">
         <div>
-          <h2>{props.title}</h2>
+          <h2>
+            <Show when={props.id} fallback={props.title}>
+              {(id) => (
+                <a data-slot="heading-link" href={`#${id()}`}>
+                  {props.title}
+                </a>
+              )}
+            </Show>
+          </h2>
           {props.description && <p>{props.description}</p>}
         </div>
         {props.controls}
@@ -324,12 +337,8 @@ function ChartSection(props: {
   )
 }
 
-function SectionTitle(props: { title: string; description: string }) {
-  return (
-    <p data-slot="section-title">
-      <strong>{props.title}.</strong> <span>{props.description}</span>
-    </p>
-  )
+function SectionTitle(props: { id: string; title: string; description: string }) {
+  return <SectionHeading href={`#${props.id}`} title={props.title} description={props.description} />
 }
 
 function SectionBridge(props: { label: string; href: string }) {
@@ -405,9 +414,13 @@ function TopModelsSection(props: { data: StatsHomeData["usage"]; leaderboard: St
 
   return (
     <section id="top-models" data-section="top-models">
-      <h2 data-slot="top-models-title">
-        <strong>{i18n.t("nav.topModels")}.</strong> <span>{i18n.t("home.topModelsDescription")}</span>
-      </h2>
+      <SectionHeading
+        as="h2"
+        slot="top-models-title"
+        href="#top-models"
+        title={i18n.t("nav.topModels")}
+        description={i18n.t("home.topModelsDescription")}
+      />
       <Show
         when={data().some((item) => usageTotal(item) > 0)}
         fallback={<EmptyState title={i18n.t("home.noUsageTitle")} description={i18n.t("home.noUsageDescription")} />}
@@ -802,7 +815,11 @@ function UniqueUsersSection(props: { data: StatsHomeData["users"] }) {
   return (
     <section id="unique-users" data-section="unique-users">
       <SectionBridge label={i18n.t("nav.topModels").toUpperCase()} href="#top-models" />
-      <SectionTitle title={i18n.t("home.uniqueUsersTitle")} description={i18n.t("home.uniqueUsersDescription")} />
+      <SectionTitle
+        id="unique-users"
+        title={i18n.t("home.uniqueUsersTitle")}
+        description={i18n.t("home.uniqueUsersDescription")}
+      />
       <Show
         when={data().some((item) => usageTotal(item) > 0)}
         fallback={
@@ -1073,7 +1090,11 @@ function MarketShareSection(props: { data: StatsHomeData["market"] }) {
       }}
     >
       <SectionBridge label={i18n.t("nav.cacheRatio").toUpperCase()} href="#cache-ratio" />
-      <SectionTitle title={i18n.t("home.marketShareTitle")} description={i18n.t("home.marketShareDescription")} />
+      <SectionTitle
+        id="market-share"
+        title={i18n.t("home.marketShareTitle")}
+        description={i18n.t("home.marketShareDescription")}
+      />
       <Show
         when={activeDay()}
         fallback={<EmptyState title={i18n.t("home.noMarketTitle")} description={i18n.t("home.noMarketDescription")} />}
@@ -1298,7 +1319,7 @@ function GeoBreakdownSection(props: { data: StatsHomeData["country"] }) {
       }}
     >
       <SectionBridge label={i18n.t("nav.marketShare").toUpperCase()} href="#market-share" />
-      <SectionTitle title={i18n.t("home.geoTitle")} description={i18n.t("home.geoDescription")} />
+      <SectionTitle id="geo-breakdown" title={i18n.t("home.geoTitle")} description={i18n.t("home.geoDescription")} />
       <Show
         when={data().length > 0}
         fallback={<EmptyState title={i18n.t("home.noGeoTitle")} description={i18n.t("home.noGeoDescription")} />}
@@ -1583,7 +1604,11 @@ function TokenCostSection(props: { data: StatsHomeData["tokenCost"]; catalog: Mo
   return (
     <section id="token-cost" data-section="token-cost">
       <SectionBridge label={i18n.t("nav.sessionCost").toUpperCase()} href="#session-cost" />
-      <SectionTitle title={i18n.t("home.tokenCostTitle")} description={i18n.t("home.tokenCostDescription")} />
+      <SectionTitle
+        id="token-cost"
+        title={i18n.t("home.tokenCostTitle")}
+        description={i18n.t("home.tokenCostDescription")}
+      />
       <Show
         when={visible().length > 0}
         fallback={
@@ -1666,7 +1691,11 @@ function CacheRatioSection(props: { data: StatsHomeData["cacheRatio"] }) {
   return (
     <section id="cache-ratio" data-section="cache-ratio">
       <SectionBridge label={i18n.t("nav.tokenCost").toUpperCase()} href="#token-cost" />
-      <SectionTitle title={i18n.t("home.cacheRatioTitle")} description={i18n.t("home.cacheRatioDescription")} />
+      <SectionTitle
+        id="cache-ratio"
+        title={i18n.t("home.cacheRatioTitle")}
+        description={i18n.t("home.cacheRatioDescription")}
+      />
       <Show
         when={visible().length > 0}
         fallback={<EmptyState title={i18n.t("home.noCacheTitle")} description={i18n.t("home.noCacheDescription")} />}
@@ -1792,7 +1821,11 @@ function SessionCostSection(props: { data: StatsHomeData["sessionCost"] }) {
   return (
     <section id="session-cost" data-section="session-cost">
       <SectionBridge label={i18n.t("nav.topModels").toUpperCase()} href="#top-models" />
-      <SectionTitle title={i18n.t("home.sessionCostTitle")} description={i18n.t("home.sessionCostDescription")} />
+      <SectionTitle
+        id="session-cost"
+        title={i18n.t("home.sessionCostTitle")}
+        description={i18n.t("home.sessionCostDescription")}
+      />
       <Show
         when={visible().length > 0}
         fallback={

+ 21 - 0
packages/stats/app/src/routes/section-heading.tsx

@@ -0,0 +1,21 @@
+export function SectionHeading(props: {
+  href: string
+  title: string
+  description: string
+  as?: "h2" | "p"
+  slot?: string
+}) {
+  const content = (
+    <>
+      <strong>
+        <a data-slot="heading-link" href={props.href}>
+          <span data-slot="heading-anchor" aria-hidden="true">#</span>{props.title}.
+        </a>
+      </strong>{" "}
+      <span>{props.description}</span>
+    </>
+  )
+
+  if (props.as === "h2") return <h2 data-slot={props.slot ?? "section-title"}>{content}</h2>
+  return <p data-slot={props.slot ?? "section-title"}>{content}</p>
+}