Procházet zdrojové kódy

Combine top models and leaderboard sections

Adam před 2 měsíci
rodič
revize
c977cb7f31

+ 115 - 28
packages/stats/app/src/routes/index.css

@@ -1033,7 +1033,7 @@
   grid-template-rows: 34px minmax(0, 1fr);
   gap: 12px;
   width: 100%;
-  height: 560px;
+  height: clamp(320px, 44vh, 400px);
 }
 
 [data-page="stats"] [data-slot="top-models-axis"],
@@ -1418,7 +1418,7 @@
 }
 
 [data-page="stats"] [data-section="top-models"] [data-slot="chart-footer"] {
-  margin-top: 40px;
+  margin-top: 32px;
 }
 
 [data-page="stats"] [data-component="usage-filter"] {
@@ -1706,27 +1706,15 @@
   font-weight: 400;
 }
 
-[data-page="stats"] [data-component="leaderboard"],
-[data-page="stats"] [data-slot="leaderboard-featured"],
-[data-page="stats"] [data-slot="leaderboard-compact"],
-[data-page="stats"] [data-slot="leaderboard-column"],
-[data-page="stats"] [data-slot="leaderboard-mobile"] {
-  display: grid;
-}
-
 [data-page="stats"] [data-component="leaderboard"] {
-  gap: 0;
-}
-
-[data-page="stats"] [data-slot="leaderboard-featured"],
-[data-page="stats"] [data-slot="leaderboard-compact"] {
-  grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
-  gap: 8px;
+  display: block;
+  width: 100%;
 }
 
 [data-page="stats"] [data-slot="leaderboard-pattern"] {
+  scroll-margin-top: 88px;
   height: 16px;
-  margin: 16px 0;
+  margin: 32px 0 16px;
   overflow: hidden;
   background: var(--stats-hero-pattern);
   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");
@@ -1739,16 +1727,28 @@
   -webkit-mask-size: 6px 6px;
 }
 
-[data-page="stats"] [data-slot="leaderboard-column"] {
+[data-page="stats"] [data-slot="leaderboard-scroll"] {
+  box-sizing: border-box;
+  display: flex;
   gap: 8px;
+  width: calc(100% + 80px);
+  margin-inline: -40px;
+  padding: 4px 40px 8px;
+  overflow-x: auto;
+  overscroll-behavior-x: contain;
+  scroll-padding-inline: 40px;
+  scroll-snap-type: x proximity;
+  scrollbar-width: none;
 }
 
-[data-page="stats"] [data-slot="leaderboard-mobile"] {
+[data-page="stats"] [data-slot="leaderboard-scroll"]::-webkit-scrollbar {
   display: none;
 }
 
-[data-page="stats"] [data-section="leaderboard"] [data-slot="chart-footer"] {
-  margin-top: 32px;
+[data-page="stats"] [data-slot="leaderboard-scroll"] [data-component="leader-card"] {
+  flex: 0 0 clamp(240px, 31vw, 360px);
+  width: clamp(240px, 31vw, 360px);
+  scroll-snap-align: start;
 }
 
 [data-page="stats"] [data-component="leader-card"] {
@@ -1889,6 +1889,26 @@
   color: #b82d35;
 }
 
+[data-page="stats"] [data-component="leader-card"][data-active="true"] {
+  border-color: var(--stats-line-strong);
+  background: #ffffff;
+  box-shadow:
+    0 0 0 0.5px #00000024,
+    0 6px 16px #0000000d,
+    0 2px 6px #0000000f;
+  transform: translateY(-1px);
+}
+
+[data-page="stats"] [data-component="leader-card"][data-active="true"] [data-slot="leader-watermark"] {
+  color: var(--stats-layer-2);
+  opacity: 0.86;
+}
+
+[data-page="stats"] [data-component="leader-card"][data-active="true"] [data-slot="leader-avatar"] {
+  border-color: var(--stats-line-strong);
+  color: var(--stats-text);
+}
+
 @media (hover: hover) {
   [data-page="stats"] [data-component="leader-card"]:hover,
   [data-page="stats"] [data-component="leader-card"]:focus-visible {
@@ -2331,8 +2351,12 @@
     0 2px 4px #0000003d;
 }
 
+[data-page="stats"][data-theme="dark"] [data-component="leader-card"][data-active="true"],
 [data-page="stats"][data-theme="dark"] [data-component="leader-card"]:hover,
 [data-page="stats"][data-theme="dark"] [data-component="leader-card"]:focus-visible,
+:root[data-stats-theme="dark"]
+  [data-page="stats"]:not([data-theme="light"])
+  [data-component="leader-card"][data-active="true"],
 :root[data-stats-theme="dark"] [data-page="stats"]:not([data-theme="light"]) [data-component="leader-card"]:hover,
 :root[data-stats-theme="dark"]
   [data-page="stats"]:not([data-theme="light"])
@@ -2344,8 +2368,13 @@
     0 2px 6px #00000052;
 }
 
+[data-page="stats"][data-theme="dark"] [data-component="leader-card"][data-active="true"] [data-slot="leader-watermark"],
 [data-page="stats"][data-theme="dark"] [data-component="leader-card"]:hover [data-slot="leader-watermark"],
 [data-page="stats"][data-theme="dark"] [data-component="leader-card"]:focus-visible [data-slot="leader-watermark"],
+:root[data-stats-theme="dark"]
+  [data-page="stats"]:not([data-theme="light"])
+  [data-component="leader-card"][data-active="true"]
+  [data-slot="leader-watermark"],
 :root[data-stats-theme="dark"]
   [data-page="stats"]:not([data-theme="light"])
   [data-component="leader-card"]:hover
@@ -2455,6 +2484,9 @@
       0 2px 4px #0000003d;
   }
 
+  :root:not([data-stats-theme="light"])
+    [data-page="stats"]:not([data-theme="light"])
+    [data-component="leader-card"][data-active="true"],
   :root:not([data-stats-theme="light"])
     [data-page="stats"]:not([data-theme="light"])
     [data-component="leader-card"]:hover,
@@ -2468,6 +2500,10 @@
       0 2px 6px #00000052;
   }
 
+  :root:not([data-stats-theme="light"])
+    [data-page="stats"]:not([data-theme="light"])
+    [data-component="leader-card"][data-active="true"]
+    [data-slot="leader-watermark"],
   :root:not([data-stats-theme="light"])
     [data-page="stats"]:not([data-theme="light"])
     [data-component="leader-card"]:hover
@@ -2560,6 +2596,14 @@
   [data-page="stats"] [data-section="session-cost"] {
     padding: 64px 32px;
   }
+
+  [data-page="stats"] [data-slot="leaderboard-scroll"] {
+    width: calc(100% + 64px);
+    margin-inline: -32px;
+    padding-right: 32px;
+    padding-left: 32px;
+    scroll-padding-inline: 32px;
+  }
 }
 
 @media (max-width: 58rem) {
@@ -2603,9 +2647,9 @@
     flex-wrap: nowrap;
   }
 
-  [data-page="stats"] [data-slot="leaderboard-featured"],
-  [data-page="stats"] [data-slot="leaderboard-compact"] {
-    grid-template-columns: 1fr;
+  [data-page="stats"] [data-slot="leaderboard-scroll"] [data-component="leader-card"] {
+    flex-basis: 280px;
+    width: 280px;
   }
 
   [data-page="stats"] [data-component="leader-card"][data-size="featured"],
@@ -2701,19 +2745,62 @@
   [data-page="stats"] [data-slot="top-models-mobile-controls"] {
     display: flex;
     gap: 8px;
-    margin-bottom: 32px;
+    width: 100%;
   }
 
   [data-page="stats"] [data-section="top-models"] [data-slot="chart-footer"] {
+    display: flex;
+    margin-top: 24px;
+  }
+
+  [data-page="stats"] [data-section="top-models"] [data-slot="chart-footer"] [data-component="usage-filter"] {
     display: none;
   }
 
   [data-page="stats"] [data-slot="leaderboard-featured"],
-  [data-page="stats"] [data-slot="leaderboard-pattern"],
   [data-page="stats"] [data-slot="leaderboard-compact"] {
     display: none;
   }
 
+  [data-page="stats"] [data-slot="leaderboard-pattern"] {
+    margin: 24px 0 12px;
+  }
+
+  [data-page="stats"] [data-slot="leaderboard-scroll"] {
+    gap: 12px;
+    width: calc(100% + 48px);
+    margin-inline: -24px;
+    padding-right: 24px;
+    padding-left: 24px;
+    scroll-padding-inline: 24px;
+  }
+
+  [data-page="stats"] [data-slot="leaderboard-scroll"] [data-component="leader-card"] {
+    flex: 0 0 240px;
+    width: 240px;
+    min-height: 156px;
+    gap: 32px;
+  }
+
+  [data-page="stats"] [data-slot="leaderboard-scroll"] [data-slot="leader-body"] {
+    flex-direction: column;
+    align-items: flex-start;
+    gap: 16px;
+  }
+
+  [data-page="stats"] [data-slot="leaderboard-scroll"] [data-slot="leader-avatar"] {
+    width: 28px;
+    height: 28px;
+    padding: 4px;
+    border-radius: 6px;
+  }
+
+  [data-page="stats"] [data-slot="leaderboard-scroll"] [data-slot="leader-watermark"] {
+    right: -68px;
+    width: 240px;
+    height: 240px;
+  }
+
   [data-page="stats"] [data-slot="leaderboard-mobile"] {
     box-sizing: border-box;
     display: flex;
@@ -2761,7 +2848,7 @@
 
   [data-page="stats"] [data-component="top-models-chart"] {
     grid-template-rows: 40px minmax(0, 1fr);
-    height: 400px;
+    height: 360px;
   }
 
   [data-page="stats"] [data-component="market-share"] {

+ 97 - 84
packages/stats/app/src/routes/index.tsx

@@ -159,8 +159,7 @@ export default function StatsHome() {
             {(stats) => (
               <>
                 <Hero updatedAt={stats().updatedAt} />
-                <TopModelsSection data={stats().usage} />
-                <LeaderboardSection data={stats().leaderboard} />
+                <TopModelsSection data={stats().usage} leaderboard={stats().leaderboard} />
                 <MarketShareSection data={stats().market} />
                 <TokenCostSection data={stats().tokenCost} />
                 <SessionCostSection data={stats().sessionCost} />
@@ -368,11 +367,13 @@ function formatUpdatedAtLabel(value: { date: string; time: string }) {
   return `${value.date}, ${value.time}`
 }
 
-function TopModelsSection(props: { data: StatsHomeData["usage"] }) {
+function TopModelsSection(props: { data: StatsHomeData["usage"]; leaderboard: StatsHomeData["leaderboard"] }) {
   const [product, setProduct] = createSignal<UsageProduct>("All Users")
   const [range, setRange] = createSignal<UsageRange>("2M")
   const [sheet, setSheet] = createSignal<"product" | "range">()
+  const [activeModel, setActiveModel] = createSignal<string>()
   const data = createMemo(() => props.data[product()][range()])
+  const leaderboard = createMemo(() => props.leaderboard[product()][range()])
 
   createEffect(() => {
     if (!sheet()) return
@@ -397,28 +398,42 @@ function TopModelsSection(props: { data: StatsHomeData["usage"] }) {
       <h2 data-slot="top-models-title">
         <strong>Top models.</strong> <span>Usage of models across OpenCode.</span>
       </h2>
-      <div data-slot="top-models-mobile-controls">
-        <MobileFilterButton
-          label="Product filter"
-          value={product()}
-          expanded={sheet() === "product"}
-          onClick={() => setSheet(sheet() === "product" ? undefined : "product")}
-        />
-        <MobileFilterButton
-          label="Date range"
-          value={range()}
-          expanded={sheet() === "range"}
-          onClick={() => setSheet(sheet() === "range" ? undefined : "range")}
-        />
-      </div>
       <Show
         when={data().some((item) => usageTotal(item) > 0)}
         fallback={<EmptyState title="No usage data" description="No model_stat rows matched this product and range." />}
       >
-        <TopModelsChart data={data()} range={range()} />
+        <TopModelsChart
+          data={data()}
+          range={range()}
+          activeModel={activeModel()}
+          onActiveModelChange={setActiveModel}
+        />
+      </Show>
+      <div id="leaderboard" data-slot="leaderboard-pattern" aria-hidden="true" />
+      <Show
+        when={leaderboard().length > 0}
+        fallback={
+          <EmptyState title="No leaderboard data" description="No model_stat rows matched this product and range." />
+        }
+      >
+        <Leaderboard data={leaderboard()} activeModel={activeModel()} onActiveModelChange={setActiveModel} />
       </Show>
       <div data-slot="chart-footer">
         <StatsFilters product={product()} range={range()} onProductSelect={setProduct} onRangeSelect={setRange} />
+        <div data-slot="top-models-mobile-controls">
+          <MobileFilterButton
+            label="Product filter"
+            value={product()}
+            expanded={sheet() === "product"}
+            onClick={() => setSheet(sheet() === "product" ? undefined : "product")}
+          />
+          <MobileFilterButton
+            label="Date range"
+            value={range()}
+            expanded={sheet() === "range"}
+            onClick={() => setSheet(sheet() === "range" ? undefined : "range")}
+          />
+        </div>
       </div>
       <Show when={sheet()}>
         {(kind) => (
@@ -575,9 +590,13 @@ function FilterPills<T extends string>(props: {
   )
 }
 
-function TopModelsChart(props: { data: UsagePoint[]; range: UsageRange }) {
+function TopModelsChart(props: {
+  data: UsagePoint[]
+  range: UsageRange
+  activeModel: string | undefined
+  onActiveModelChange: (model: string | undefined) => void
+}) {
   const [activeIndex, setActiveIndex] = createSignal<number>()
-  const [activeSegment, setActiveSegment] = createSignal<number>()
   const maxTotal = createMemo(() => getTopModelsMaxTotal(props.data))
   const activePoint = createMemo(() => props.data[activeIndex() ?? -1])
 
@@ -622,31 +641,34 @@ function TopModelsChart(props: { data: UsagePoint[]; range: UsageRange }) {
               onPointerDown={(event) => {
                 if (event.pointerType !== "touch") return
                 setActiveIndex(dayIndex())
-                setActiveSegment(undefined)
+                props.onActiveModelChange(undefined)
               }}
               onPointerEnter={() => {
                 setActiveIndex(dayIndex())
-                setActiveSegment(undefined)
+                props.onActiveModelChange(undefined)
               }}
               onPointerLeave={(event) => {
                 if (event.pointerType === "touch") return
                 setActiveIndex(undefined)
-                setActiveSegment(undefined)
+                props.onActiveModelChange(undefined)
+              }}
+              onClick={() => {
+                setActiveIndex(dayIndex())
+                props.onActiveModelChange(undefined)
               }}
-              onClick={() => setActiveIndex(dayIndex())}
               onFocus={() => {
                 setActiveIndex(dayIndex())
-                setActiveSegment(undefined)
+                props.onActiveModelChange(undefined)
               }}
               onBlur={() => {
                 setActiveIndex(undefined)
-                setActiveSegment(undefined)
+                props.onActiveModelChange(undefined)
               }}
               onKeyDown={(event) => {
                 if (event.key !== "Enter" && event.key !== " ") return
                 event.preventDefault()
                 setActiveIndex(dayIndex())
-                setActiveSegment(undefined)
+                props.onActiveModelChange(undefined)
               }}
             >
               <div data-slot="top-models-stack" style={{ "grid-template-rows": getTopModelsSegmentRows(day) }}>
@@ -654,28 +676,30 @@ function TopModelsChart(props: { data: UsagePoint[]; range: UsageRange }) {
                   {(item) => (
                     <i
                       data-series={item.index}
-                      data-active={activeSegment() === item.index ? "true" : undefined}
+                      data-model={item.segment.model}
+                      data-active={props.activeModel === item.segment.model ? "true" : undefined}
                       style={{
                         background: getTopModelsSegmentColor(
+                          item.segment.model,
                           item.index,
                           activeIndex() !== undefined && activeIndex() !== dayIndex(),
-                          activeSegment(),
+                          props.activeModel,
                         ),
                       }}
                       onPointerEnter={(event) => {
                         event.stopPropagation()
                         setActiveIndex(dayIndex())
-                        setActiveSegment(item.index)
+                        props.onActiveModelChange(item.segment.model)
                       }}
                       onPointerDown={(event) => {
                         event.stopPropagation()
                         setActiveIndex(dayIndex())
-                        setActiveSegment(item.index)
+                        props.onActiveModelChange(item.segment.model)
                       }}
                       onClick={(event) => {
                         event.stopPropagation()
                         setActiveIndex(dayIndex())
-                        setActiveSegment(item.index)
+                        props.onActiveModelChange(item.segment.model)
                       }}
                     />
                   )}
@@ -693,9 +717,11 @@ function TopModelsChart(props: { data: UsagePoint[]; range: UsageRange }) {
                     <For each={visibleTopModelsSegments(point())}>
                       {(item) => (
                         <p
-                          data-active={activeSegment() === item.index ? "true" : undefined}
+                          data-active={props.activeModel === item.segment.model ? "true" : undefined}
                           data-muted={
-                            activeSegment() !== undefined && activeSegment() !== item.index ? "true" : undefined
+                            props.activeModel !== undefined && props.activeModel !== item.segment.model
+                              ? "true"
+                              : undefined
                           }
                         >
                           <span data-slot="tooltip-label">
@@ -746,9 +772,14 @@ function stackedTopModelsSegments(point: UsagePoint) {
     .sort((a, b) => a.segment.value - b.segment.value || a.index - b.index)
 }
 
-function getTopModelsSegmentColor(index: number, muted: boolean, activeSegment: number | undefined) {
-  if (activeSegment !== undefined)
-    return activeSegment === index ? (usageColors[index] ?? "var(--stats-text)") : "var(--stats-layer-2)"
+function getTopModelsSegmentColor(
+  model: string,
+  index: number,
+  muted: boolean,
+  activeModel: string | undefined,
+) {
+  if (activeModel !== undefined)
+    return activeModel === model ? (usageColors[index] ?? "var(--stats-text)") : "var(--stats-layer-2)"
   if (muted) return "var(--stats-layer-2)"
   return usageColors[index] ?? "var(--stats-text)"
 }
@@ -781,67 +812,49 @@ function formatTokens(value: number) {
   return `${Math.round(value * 1000)}B`
 }
 
-function LeaderboardSection(props: { data: StatsHomeData["leaderboard"] }) {
-  const [product, setProduct] = createSignal<UsageProduct>("All Users")
-  const [range, setRange] = createSignal<UsageRange>("2M")
-  const data = createMemo(() => props.data[product()][range()])
-
-  return (
-    <section id="leaderboard" data-section="leaderboard">
-      <SectionTitle
-        title="Leaderboard"
-        description="Shown are the sum of prompt and completion tokens per model, including reasoning tokens."
-      />
-      <Show
-        when={data().length > 0}
-        fallback={
-          <EmptyState title="No leaderboard data" description="No model_stat rows matched this product and range." />
-        }
-      >
-        <Leaderboard data={data()} />
-      </Show>
-      <div data-slot="chart-footer">
-        <StatsFilters product={product()} range={range()} onProductSelect={setProduct} onRangeSelect={setRange} />
-      </div>
-    </section>
-  )
-}
-
-function Leaderboard(props: { data: LeaderboardEntry[] }) {
-  const featured = createMemo(() => props.data.slice(0, 3))
-  const columns = createMemo(() =>
-    [0, 1, 2].map((index) => props.data.slice(3 + index * 5, 8 + index * 5)).filter((column) => column.length > 0),
-  )
-
+function Leaderboard(props: {
+  data: LeaderboardEntry[]
+  activeModel: string | undefined
+  onActiveModelChange: (model: string | undefined) => void
+}) {
   return (
     <div data-component="leaderboard" role="list" aria-label="Model token leaderboard">
-      <div data-slot="leaderboard-featured">
-        <For each={featured()}>{(entry) => <LeaderboardCard entry={entry} size="featured" />}</For>
-      </div>
-      <div data-slot="leaderboard-pattern" aria-hidden="true" />
-      <div data-slot="leaderboard-compact">
-        <For each={columns()}>
-          {(column) => (
-            <div data-slot="leaderboard-column">
-              <For each={column}>{(entry) => <LeaderboardCard entry={entry} size="compact" />}</For>
-            </div>
+      <div data-slot="leaderboard-scroll" aria-label="Scrollable model token leaderboard">
+        <For each={props.data}>
+          {(entry) => (
+            <LeaderboardCard
+              entry={entry}
+              active={props.activeModel === entry.model}
+              onActiveModelChange={props.onActiveModelChange}
+            />
           )}
         </For>
       </div>
-      <div data-slot="leaderboard-mobile" aria-label="Scrollable model token leaderboard">
-        <For each={props.data}>{(entry) => <LeaderboardCard entry={entry} size="featured" />}</For>
-      </div>
     </div>
   )
 }
 
-function LeaderboardCard(props: { entry: LeaderboardEntry; size: "featured" | "compact" }) {
+function LeaderboardCard(props: {
+  entry: LeaderboardEntry
+  active: boolean
+  onActiveModelChange: (model: string | undefined) => void
+}) {
   return (
     <article
       data-component="leader-card"
-      data-size={props.size}
+      data-size="featured"
+      data-active={props.active ? "true" : undefined}
       role="listitem"
+      tabIndex={0}
       aria-label={`${String(props.entry.rank).padStart(2, "0")} ${props.entry.model} by ${props.entry.author}`}
+      onPointerEnter={() => props.onActiveModelChange(props.entry.model)}
+      onPointerLeave={(event) => {
+        if (event.pointerType === "touch") return
+        props.onActiveModelChange(undefined)
+      }}
+      onFocus={() => props.onActiveModelChange(props.entry.model)}
+      onBlur={() => props.onActiveModelChange(undefined)}
+      onClick={() => props.onActiveModelChange(props.entry.model)}
     >
       <span data-slot="rank">{String(props.entry.rank).padStart(2, "0")}</span>
       <ProviderIcon data-slot="leader-watermark" aria-hidden="true" id={getProviderIconId(props.entry.author)} />