Просмотр исходного кода

feat(stats): update model comparison page

Adam 1 месяц назад
Родитель
Сommit
60c88336b9

Разница между файлами не показана из-за своего большого размера
+ 787 - 122
packages/stats/app/src/routes/compare/[firstLab]/[firstModel]/[secondLab]/[secondModel].tsx


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

@@ -87,6 +87,11 @@
   display: none !important;
 }
 
+[data-page="stats"][data-layout="compare-detail"] {
+  /* The table contains its own wide rows; keep the page itself out of the horizontal scroll chain. */
+  overflow-x: visible;
+}
+
 [data-page="stats"] section[id],
 [data-page="stats"] [data-component="leaderboard"][id] {
   scroll-margin-top: 88px;
@@ -5923,6 +5928,585 @@
   min-width: 0;
 }
 
+[data-page="stats"] [data-section="compare-detail-hero"] {
+  position: relative;
+  display: grid;
+  align-content: end;
+  gap: 24px;
+  min-height: 316px;
+  box-sizing: border-box;
+  padding: 128px 40px 40px;
+  color: var(--stats-text);
+}
+
+[data-page="stats"] [data-slot="compare-detail-hero-grid"] {
+  display: grid;
+  grid-template-columns: minmax(0, 1fr) auto;
+  gap: 24px;
+  align-items: center;
+}
+
+[data-page="stats"] [data-section="compare-detail-hero"] h1 {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  gap: 16px;
+  min-width: 0;
+  margin: 0;
+  color: var(--stats-text);
+  font-size: 40px;
+  font-weight: 500;
+  line-height: 60px;
+  letter-spacing: 0;
+}
+
+[data-page="stats"] [data-slot="compare-detail-actions"] {
+  display: inline-flex;
+  align-items: center;
+  justify-content: flex-end;
+  gap: 8px;
+  min-width: 0;
+}
+
+[data-page="stats"] button[data-slot="compare-detail-action"],
+[data-page="stats"] a[data-slot="compare-detail-action"] {
+  position: relative;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+  height: 32px;
+  box-sizing: border-box;
+  padding: 0 12px 0 8px;
+  overflow: hidden;
+  border: 0;
+  border-radius: 0;
+  color: var(--stats-text);
+  background: var(--stats-bg);
+  box-shadow:
+    0 0 0 0.5px color-mix(in srgb, var(--stats-text) 14%, transparent),
+    0 1px 1.5px color-mix(in srgb, #000000 10%, transparent);
+  cursor: pointer;
+  font: inherit;
+  font-size: 13px;
+  font-weight: 500;
+  line-height: 1.1;
+  text-decoration: none;
+  white-space: nowrap;
+}
+
+[data-page="stats"] button[data-slot="compare-detail-action"]::before,
+[data-page="stats"] a[data-slot="compare-detail-action"]::before {
+  position: absolute;
+  inset: 0 0 auto;
+  height: 16px;
+  background: linear-gradient(to bottom, rgb(255 255 255 / 7%), transparent);
+  content: "";
+  pointer-events: none;
+}
+
+[data-page="stats"] [data-slot="compare-detail-action"] > * {
+  position: relative;
+  z-index: 1;
+}
+
+[data-page="stats"] button[data-slot="compare-detail-action"][aria-pressed] {
+  gap: 12px;
+}
+
+[data-page="stats"] button[data-slot="compare-detail-action"]:hover,
+[data-page="stats"] button[data-slot="compare-detail-action"]:focus-visible,
+[data-page="stats"] a[data-slot="compare-detail-action"]:hover,
+[data-page="stats"] a[data-slot="compare-detail-action"]:focus-visible {
+  background: var(--stats-layer);
+  outline: none;
+  text-decoration: none;
+}
+
+[data-page="stats"] button[data-slot="compare-detail-action"]:disabled {
+  color: var(--stats-muted);
+  background: var(--stats-bg);
+  cursor: not-allowed;
+}
+
+[data-page="stats"] button[data-slot="compare-detail-action"][data-active="true"] {
+  color: var(--stats-text);
+  background: var(--stats-bg);
+}
+
+[data-page="stats"] [data-slot="compare-detail-highlight-icon"] {
+  display: flex;
+  align-items: center;
+  gap: 1px;
+  width: 31px;
+  height: 16px;
+  box-sizing: border-box;
+  padding: 1px;
+  overflow: hidden;
+  background: var(--stats-line-strong);
+}
+
+[data-page="stats"] [data-slot="compare-detail-highlight-icon"] i {
+  flex: 0 0 14px;
+  width: 14px;
+  height: 14px;
+  background: transparent;
+}
+
+[data-page="stats"]
+  [data-slot="compare-detail-action"][aria-pressed="true"]
+  [data-slot="compare-detail-highlight-icon"] {
+  background: var(--stats-accent);
+}
+
+[data-page="stats"]
+  [data-slot="compare-detail-action"][aria-pressed="false"]
+  [data-slot="compare-detail-highlight-icon"]
+  i:first-child,
+[data-page="stats"]
+  [data-slot="compare-detail-action"][aria-pressed="true"]
+  [data-slot="compare-detail-highlight-icon"]
+  i:last-child {
+  background: #fafafa;
+  box-shadow:
+    0 0 0 0.5px rgb(0 0 0 / 12%),
+    0 1px 2px -1px rgb(0 0 0 / 8%),
+    0 2px 4px rgb(0 0 0 / 4%);
+}
+
+[data-page="stats"] [data-slot="compare-detail-action"] [data-slot="compare-home-plus"] {
+  display: grid;
+  place-items: center;
+  width: 16px;
+  height: 16px;
+  font-size: 16px;
+  line-height: 16px;
+}
+
+[data-page="stats"] [data-component="compare-detail-table"] {
+  --compare-detail-label-column: 292px;
+  --compare-detail-model-column-min: 360px;
+  position: relative;
+}
+
+[data-page="stats"] [data-component="compare-detail-heading-scroll"] {
+  position: sticky;
+  top: 72px;
+  z-index: 9;
+  overflow-x: auto;
+  background: var(--stats-bg);
+  scrollbar-width: none;
+}
+
+[data-page="stats"] [data-component="compare-detail-heading-scroll"]::-webkit-scrollbar {
+  display: none;
+}
+
+[data-page="stats"] [data-component="compare-detail-body-scroll"] {
+  overflow-x: auto;
+}
+
+[data-page="stats"] [data-section="compare-detail-selector"],
+[data-page="stats"] [data-section="compare-detail-matrix"] {
+  min-width: calc(
+    var(--compare-detail-label-column) + var(--compare-detail-model-column-min) + var(--compare-detail-model-column-min)
+  );
+}
+
+[data-page="stats"] [data-section="compare-detail-selector"] {
+  position: relative;
+  height: 98px;
+  min-height: 98px;
+  box-sizing: border-box;
+  border: 0;
+}
+
+[data-page="stats"] [data-section="compare-detail-selector"]::after {
+  position: absolute;
+  z-index: 5;
+  inset: 0;
+  border-top: 1px solid var(--stats-line);
+  border-bottom: 1px solid var(--stats-line);
+  content: "";
+  pointer-events: none;
+}
+
+[data-page="stats"] [data-component="compare-detail-selector-grid"] {
+  display: grid;
+  grid-template-columns: var(--compare-detail-grid);
+  height: 100%;
+}
+
+[data-page="stats"]
+  [data-component="compare-detail-table"][data-model-count="3"]
+  [data-section="compare-detail-selector"],
+[data-page="stats"]
+  [data-component="compare-detail-table"][data-model-count="3"]
+  [data-section="compare-detail-matrix"] {
+  min-width: calc(
+    var(--compare-detail-label-column) + var(--compare-detail-model-column-min) +
+      var(--compare-detail-model-column-min) + var(--compare-detail-model-column-min)
+  );
+}
+
+[data-page="stats"]
+  [data-component="compare-detail-table"][data-model-count="4"]
+  [data-section="compare-detail-selector"],
+[data-page="stats"]
+  [data-component="compare-detail-table"][data-model-count="4"]
+  [data-section="compare-detail-matrix"] {
+  min-width: calc(
+    var(--compare-detail-label-column) + var(--compare-detail-model-column-min) +
+      var(--compare-detail-model-column-min) + var(--compare-detail-model-column-min) +
+      var(--compare-detail-model-column-min)
+  );
+}
+
+[data-page="stats"]
+  [data-component="compare-detail-table"][data-model-count="5"]
+  [data-section="compare-detail-selector"],
+[data-page="stats"]
+  [data-component="compare-detail-table"][data-model-count="5"]
+  [data-section="compare-detail-matrix"] {
+  min-width: calc(
+    var(--compare-detail-label-column) + var(--compare-detail-model-column-min) +
+      var(--compare-detail-model-column-min) + var(--compare-detail-model-column-min) +
+      var(--compare-detail-model-column-min) + var(--compare-detail-model-column-min)
+  );
+}
+
+[data-page="stats"]
+  [data-component="compare-detail-table"][data-model-count="6"]
+  [data-section="compare-detail-selector"],
+[data-page="stats"]
+  [data-component="compare-detail-table"][data-model-count="6"]
+  [data-section="compare-detail-matrix"] {
+  min-width: calc(
+    var(--compare-detail-label-column) + var(--compare-detail-model-column-min) +
+      var(--compare-detail-model-column-min) + var(--compare-detail-model-column-min) +
+      var(--compare-detail-model-column-min) + var(--compare-detail-model-column-min) +
+      var(--compare-detail-model-column-min)
+  );
+}
+
+[data-page="stats"] [data-slot="compare-detail-selector-spacer"] {
+  position: sticky;
+  left: 0;
+  z-index: 3;
+  min-width: 0;
+  box-sizing: border-box;
+  border-right: 1px solid var(--stats-line);
+  border-left: 1px solid var(--stats-line);
+  background: var(--stats-bg);
+}
+
+[data-page="stats"] button[data-slot="compare-detail-select-model"] {
+  position: relative;
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  min-width: 0;
+  height: 100%;
+  box-sizing: border-box;
+  padding: 0 40px;
+  border: 0;
+  appearance: none;
+  border-radius: 0;
+  color: var(--stats-text);
+  background: var(--stats-bg);
+  cursor: pointer;
+  font: inherit;
+  text-align: left;
+}
+
+[data-page="stats"] button[data-slot="compare-detail-select-model"][data-column]:not([data-column="0"]) {
+  border-left: 1px solid var(--stats-line);
+}
+
+[data-page="stats"] button[data-slot="compare-detail-select-model"][data-last="true"] {
+  border-right: 1px solid var(--stats-line);
+}
+
+[data-page="stats"] button[data-slot="compare-detail-select-model"]:hover,
+[data-page="stats"] button[data-slot="compare-detail-select-model"]:focus-visible {
+  background: var(--stats-layer);
+  outline: none;
+}
+
+[data-page="stats"] [data-slot="compare-detail-select-name"] {
+  min-width: 0;
+  overflow: hidden;
+  color: var(--stats-text);
+  font-size: 13px;
+  font-weight: 500;
+  line-height: 18px;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+[data-page="stats"] button[data-slot="compare-detail-select-model"] svg {
+  flex: 0 0 auto;
+  color: var(--stats-muted);
+}
+
+[data-page="stats"] [data-section="compare-detail-matrix"] {
+  position: relative;
+  color: var(--stats-text);
+  border-bottom: 1px solid var(--stats-line);
+}
+
+[data-page="stats"] [data-component="compare-detail-matrix"] {
+  position: relative;
+}
+
+[data-page="stats"] [data-slot="compare-detail-group"] {
+  display: grid;
+  grid-template-columns: var(--compare-detail-grid);
+}
+
+[data-page="stats"] [data-slot="compare-detail-group"] + [data-slot="compare-detail-group"] {
+  border-top: 1px solid var(--stats-line);
+}
+
+[data-page="stats"] [data-slot="compare-detail-label"],
+[data-page="stats"] [data-slot="compare-detail-value"] {
+  display: flex;
+  align-items: center;
+  min-width: 0;
+  min-height: 56px;
+  box-sizing: border-box;
+  padding: 0 40px;
+  font-size: 14px;
+  line-height: 24px;
+}
+
+[data-page="stats"] [data-slot="compare-detail-label"] {
+  position: sticky;
+  left: 0;
+  z-index: 3;
+  border-right: 1px solid var(--stats-line);
+  border-left: 1px solid var(--stats-line);
+  color: var(--stats-muted);
+  background: var(--stats-bg);
+  font-weight: 400;
+}
+
+[data-page="stats"] [data-slot="compare-detail-value"][data-column]:not([data-column="0"]) {
+  border-left: 1px solid var(--stats-line);
+}
+
+[data-page="stats"] [data-slot="compare-detail-value"][data-last="true"] {
+  border-right: 1px solid var(--stats-line);
+}
+
+[data-page="stats"] [data-slot="compare-detail-label"][data-spacer="true"],
+[data-page="stats"] [data-slot="compare-detail-value"][data-spacer="true"] {
+  min-height: 40px;
+  padding: 0;
+}
+
+[data-page="stats"] [data-slot="compare-detail-label"][data-heading="true"] {
+  gap: 12px;
+  color: var(--stats-text);
+  font-weight: 500;
+}
+
+[data-page="stats"] [data-slot="compare-detail-label"][data-heading="true"] strong {
+  font-weight: 600;
+}
+
+[data-page="stats"] [data-slot="compare-detail-label"][data-heading="true"] span {
+  display: inline-flex;
+  align-items: center;
+  height: 24px;
+  padding: 0 8px;
+  color: var(--stats-muted);
+  background: var(--stats-layer-2);
+  font-size: 12px;
+  font-weight: 500;
+  line-height: 1;
+}
+
+[data-page="stats"] [data-slot="compare-detail-value"] {
+  justify-content: flex-end;
+  color: var(--stats-text);
+  font-weight: 400;
+  text-align: right;
+}
+
+[data-page="stats"] [data-slot="compare-detail-value"][data-best="true"] {
+  background: color-mix(in srgb, #198b43 8%, transparent);
+}
+
+[data-page="stats"] [data-slot="compare-detail-value-main"],
+[data-page="stats"] [data-slot="compare-detail-value-link"] {
+  display: inline-flex;
+  align-items: center;
+  justify-content: flex-end;
+  gap: 12px;
+  min-width: 0;
+}
+
+[data-page="stats"] [data-slot="compare-detail-value-link"] {
+  color: inherit;
+  text-decoration: underline;
+  text-decoration-color: color-mix(in srgb, var(--stats-text) 30%, transparent);
+  text-underline-offset: 2px;
+}
+
+[data-page="stats"] [data-slot="compare-detail-unit"] {
+  color: var(--stats-muted);
+}
+
+[data-page="stats"] [data-slot="compare-detail-trend"] {
+  display: inline-flex;
+  align-items: center;
+  height: 16px;
+  padding: 0 5px;
+  color: var(--stats-muted);
+  background: var(--stats-layer-2);
+  font-size: 11px;
+  font-weight: 600;
+  line-height: 1;
+}
+
+[data-page="stats"] [data-slot="compare-detail-trend"][data-trend="up"] {
+  color: #198b43;
+  background: #e2f8e9;
+}
+
+[data-page="stats"] [data-slot="compare-detail-trend"][data-trend="down"] {
+  color: #c93737;
+  background: #fae8e8;
+}
+
+[data-page="stats"] [data-slot="compare-detail-boolean"] {
+  display: inline-flex;
+  align-items: center;
+  height: 24px;
+  padding: 0 8px;
+  font-size: 12px;
+  font-weight: 700;
+  line-height: 1;
+}
+
+[data-page="stats"] [data-slot="compare-detail-boolean"][data-value="true"] {
+  color: #198b43;
+  background: #e2f8e9;
+}
+
+[data-page="stats"] [data-slot="compare-detail-boolean"][data-value="false"] {
+  color: #c93737;
+  background: #fae8e8;
+}
+
+[data-page="stats"] [data-slot="compare-detail-value"][data-chart="true"] {
+  display: grid;
+  align-content: center;
+  gap: 12px;
+  min-height: 102px;
+}
+
+[data-page="stats"] [data-slot="compare-detail-bars"] {
+  display: flex;
+  align-items: flex-end;
+  gap: 2px;
+  width: 100%;
+  height: 40px;
+}
+
+[data-page="stats"] [data-slot="compare-detail-bars"] i {
+  flex: 1 1 0;
+  min-width: 2px;
+  max-width: 6px;
+  background: color-mix(in srgb, var(--stats-text) 18%, transparent);
+}
+
+[data-page="stats"] [data-slot="compare-detail-bar-dates"] {
+  display: flex;
+  justify-content: space-between;
+  gap: 16px;
+  color: var(--stats-muted);
+  font-size: 11px;
+  font-weight: 500;
+  line-height: 14px;
+}
+
+[data-page="stats"] [data-slot="compare-detail-no-chart"] {
+  justify-self: end;
+  color: var(--stats-muted);
+  font-size: 12px;
+}
+
+@media (max-width: 80rem) {
+  [data-page="stats"] [data-section="compare-detail-hero"] {
+    min-height: 280px;
+    padding: 104px 32px 40px;
+  }
+
+  [data-page="stats"] [data-slot="compare-detail-hero-grid"] {
+    grid-template-columns: 1fr;
+    align-items: start;
+  }
+
+  [data-page="stats"] [data-slot="compare-detail-actions"] {
+    justify-content: flex-start;
+  }
+
+  [data-page="stats"] [data-component="compare-detail-table"] {
+    --compare-detail-label-column: 220px;
+    --compare-detail-model-column-min: 320px;
+  }
+
+  [data-page="stats"] button[data-slot="compare-detail-select-model"],
+  [data-page="stats"] [data-slot="compare-detail-label"],
+  [data-page="stats"] [data-slot="compare-detail-value"] {
+    padding-right: 32px;
+    padding-left: 32px;
+  }
+}
+
+@media (max-width: 60rem) {
+  [data-page="stats"] [data-section="compare-detail-hero"] {
+    gap: 20px;
+    min-height: 316px;
+    padding: 72px 24px 40px;
+  }
+
+  [data-page="stats"] [data-section="compare-detail-hero"] h1 {
+    gap: 12px;
+    font-size: 32px;
+    line-height: 42px;
+  }
+
+  [data-page="stats"] [data-slot="compare-detail-actions"] {
+    flex-wrap: wrap;
+  }
+}
+
+@media (max-width: 40rem) {
+  [data-page="stats"] [data-section="compare-detail-hero"] h1 {
+    align-items: flex-start;
+    flex-direction: column;
+  }
+
+  [data-page="stats"] [data-slot="compare-detail-action"] {
+    width: 100%;
+  }
+
+  [data-page="stats"] [data-component="compare-detail-table"] {
+    --compare-detail-label-column: 188px;
+    --compare-detail-model-column-min: 246px;
+  }
+
+  [data-page="stats"] button[data-slot="compare-detail-select-model"],
+  [data-page="stats"] [data-slot="compare-detail-label"],
+  [data-page="stats"] [data-slot="compare-detail-value"] {
+    padding-right: 24px;
+    padding-left: 24px;
+  }
+}
+
 [data-page="stats"] [data-component="compare-model-modal-scrim"] {
   position: fixed;
   inset: 0;

+ 26 - 12
packages/stats/core/src/domain/home.ts

@@ -94,10 +94,15 @@ export type StatsModelComparisonEntry = {
   tokenShare: number
   tokenChange: number
   totals: StatsModelData["totals"]
+  usage: ModelUsagePoint[]
+}
+export type StatsModelComparisonInput = {
+  provider: string
+  model: string
 }
 export type StatsModelComparisonData = {
   updatedAt: string | null
-  models: [StatsModelComparisonEntry | null, StatsModelComparisonEntry | null]
+  models: (StatsModelComparisonEntry | null)[]
 }
 export type StatsHomeData = {
   updatedAt: string | null
@@ -289,27 +294,35 @@ function dateValue(value: unknown) {
   return value instanceof Date ? value : new Date(stringValue(value))
 }
 
-export const getStatsModelComparisonData: (
-  firstProvider: string,
-  firstModel: string,
-  secondProvider: string,
-  secondModel: string,
-) => Effect.Effect<StatsModelComparisonData, DatabaseError, ModelStatRepo> = Effect.fn("StatsModelComparison.getData")(
-  function* (firstProvider, firstModel, secondProvider, secondModel) {
+export const getStatsModelsComparisonData: (
+  models: readonly StatsModelComparisonInput[],
+) => Effect.Effect<StatsModelComparisonData, DatabaseError, ModelStatRepo> = Effect.fn("StatsModelsComparison.getData")(
+  function* (models) {
     const modelStats = yield* ModelStatRepo
     const rows = yield* modelStats.listDaily()
-    const first = toComparisonEntry(buildStatsModelData(firstModel, rows, [], firstProvider))
-    const second = toComparisonEntry(buildStatsModelData(secondModel, rows, [], secondProvider))
-    const latest = [first?.updatedAt, second?.updatedAt]
+    const entries = models.map((model) => toComparisonEntry(buildStatsModelData(model.model, rows, [], model.provider)))
+    const latest = entries
+      .map((model) => model?.updatedAt)
       .flatMap((value) => (value ? [dateTime(value)] : []))
       .toSorted((a, b) => b - a)[0]
     return {
       updatedAt: latest === undefined ? null : new Date(latest).toISOString(),
-      models: [first, second],
+      models: entries,
     }
   },
 )
 
+export const getStatsModelComparisonData = (
+  firstProvider: string,
+  firstModel: string,
+  secondProvider: string,
+  secondModel: string,
+) =>
+  getStatsModelsComparisonData([
+    { provider: firstProvider, model: firstModel },
+    { provider: secondProvider, model: secondModel },
+  ])
+
 function buildStatsHomeData(
   modelRows: ModelStatMetric[],
   providerRows: ProviderStatMetric[],
@@ -501,6 +514,7 @@ function toComparisonEntry(data: StatsModelData | null): StatsModelComparisonEnt
     tokenShare: data.tokenShare,
     tokenChange: data.tokenChange,
     totals: data.totals,
+    usage: data.usage,
   }
 }
 

Некоторые файлы не были показаны из-за большого количества измененных файлов