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

feat(data): another redesigned lab section

Adam 1 месяц назад
Родитель
Сommit
85e8ac3b36
2 измененных файлов с 493 добавлено и 58 удалено
  1. 149 25
      packages/stats/app/src/routes/[lab]/index.tsx
  2. 344 33
      packages/stats/app/src/routes/index.css

+ 149 - 25
packages/stats/app/src/routes/[lab]/index.tsx

@@ -42,6 +42,14 @@ const getLabData = query(async (lab: string) => {
   return runStatsEffect(getStatsLabData(lab))
 }, "getStatsLabData")
 
+type LabModelTooltipState = {
+  model: ModelCatalogEntry
+  placement: "left" | "right"
+  usage: LabUsageModelEntry | undefined
+  x: number
+  y: number
+}
+
 export default function StatsLab() {
   const i18n = useI18n()
   const language = useLanguage()
@@ -478,52 +486,168 @@ function LabUsageSection(props: { lab: ModelCatalogLab; data: StatsLabData | nul
 
 function LabModelsSection(props: { lab: ModelCatalogLab; usage: LabUsageModelEntry[] }) {
   const i18n = useI18n()
+  const [activeTooltip, setActiveTooltip] = createSignal<LabModelTooltipState>()
   const usageBySlug = createMemo(() => new Map(props.usage.map((item) => [item.slug, item])))
   return (
-    <section id="models" data-section="model-panel">
-      <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)} />}
-        </For>
+    <section id="models" data-section="model-panel" data-variant="lab-models">
+      <div data-slot="lab-model-heading">
+        <SectionHeading href="#models" title={i18n.t("nav.models")} description={i18n.t("lab.recentUsageAndLimits")} />
+        <button data-slot="lab-model-compare" type="button" aria-label="Compare models" hidden>
+          <span data-slot="lab-model-compare-icon" aria-hidden="true" />
+          <span>Compare</span>
+        </button>
       </div>
+      <div data-slot="lab-model-pattern" aria-hidden="true" />
+      <div data-component="lab-model-table" role="table" aria-label={i18n.t("lab.modelsTitle", { lab: props.lab.name })}>
+        <div data-slot="lab-model-table-track">
+          <div data-slot="lab-model-table-head" role="row">
+            <span data-column="model" role="columnheader">
+              {i18n.t("nav.models")}
+            </span>
+            <span data-column="usage" role="columnheader">
+              {i18n.t("lab.usage")}
+            </span>
+            <span data-column="share" role="columnheader">
+              {i18n.t("lab.share")}
+            </span>
+            <span data-column="context" role="columnheader">
+              {i18n.t("model.context")}
+            </span>
+            <span data-column="output" role="columnheader">
+              {i18n.t("model.output")}
+            </span>
+            <span data-column="release" role="columnheader">
+              {i18n.t("model.release")}
+            </span>
+          </div>
+          <For each={props.lab.models}>
+            {(model) => (
+              <LabModelRow
+                model={model}
+                usage={usageBySlug().get(model.slug)}
+                onTooltipChange={setActiveTooltip}
+              />
+            )}
+          </For>
+        </div>
+      </div>
+      <Show when={activeTooltip()} keyed>
+        {(state) => <LabModelTooltip state={state} />}
+      </Show>
     </section>
   )
 }
 
-function LabModelCard(props: { model: ModelCatalogEntry; usage: LabUsageModelEntry | undefined }) {
+function LabModelRow(props: {
+  model: ModelCatalogEntry
+  onTooltipChange: (state: LabModelTooltipState | undefined) => void
+  usage: LabUsageModelEntry | undefined
+}) {
   const i18n = useI18n()
   const language = useLanguage()
+  const showTooltip = (x: number, y: number) => {
+    const viewportWidth = typeof window === "undefined" ? 0 : window.innerWidth
+    const viewportHeight = typeof window === "undefined" ? 0 : window.innerHeight
+    props.onTooltipChange({
+      model: props.model,
+      placement: viewportWidth > 0 && x > viewportWidth - 280 ? "left" : "right",
+      usage: props.usage,
+      x,
+      y: viewportHeight > 0 ? Math.min(Math.max(y, 96), viewportHeight - 128) : y,
+    })
+  }
+  const showPointerTooltip: JSX.EventHandler<HTMLAnchorElement, PointerEvent> = (event) => {
+    if (event.pointerType === "touch") return
+    showTooltip(event.clientX, event.clientY)
+  }
+  const showFocusTooltip: JSX.EventHandler<HTMLAnchorElement, FocusEvent> = (event) => {
+    const rect = event.currentTarget.getBoundingClientRect()
+    showTooltip(rect.left + rect.width * 0.58, rect.top + rect.height / 2)
+  }
   return (
-    <a data-component="lab-model-card" href={language.route(`${import.meta.env.BASE_URL}${props.model.id}`)}>
-      <strong>{props.model.name}</strong>
-      <div data-slot="lab-model-card-meta">
+    <a
+      data-component="lab-model-row"
+      href={language.route(`${import.meta.env.BASE_URL}${props.model.id}`)}
+      role="row"
+      aria-label={props.model.name}
+      onBlur={() => props.onTooltipChange(undefined)}
+      onFocus={showFocusTooltip}
+      onPointerEnter={showPointerTooltip}
+      onPointerLeave={(event) => {
+        if (event.pointerType === "touch") return
+        props.onTooltipChange(undefined)
+      }}
+      onPointerMove={showPointerTooltip}
+    >
+      <span data-slot="lab-model-cell" data-column="model" role="cell">
+        <span data-slot="lab-model-avatar" aria-hidden="true">
+          <ProviderIcon id={getProviderIconId(props.model.lab)} />
+        </span>
+        <strong>{props.model.name}</strong>
+      </span>
+      <span data-slot="lab-model-cell" data-column="usage" role="cell">
+        {props.usage ? formatTokens(props.usage.tokens) : "-"}
+      </span>
+      <span data-slot="lab-model-cell" data-column="share" role="cell">
+        {props.usage ? formatPercent(props.usage.share) : "-"}
+      </span>
+      <span data-slot="lab-model-cell" data-column="context" role="cell">
+        {formatCatalogLimit(props.model.limit?.context, i18n.t("home.unknown"))}
+      </span>
+      <span data-slot="lab-model-cell" data-column="output" role="cell">
+        {formatCatalogLimit(props.model.limit?.output, i18n.t("home.unknown"))}
+      </span>
+      <span data-slot="lab-model-cell" data-column="release" role="cell">
+        {formatCatalogDate(props.model.releaseDate, language.tag(language.locale()), i18n.t("home.unknown"))}
+      </span>
+    </a>
+  )
+}
+
+function LabModelTooltip(props: { state: LabModelTooltipState }) {
+  const i18n = useI18n()
+  return (
+    <div
+      data-component="lab-model-tooltip"
+      data-placement={props.state.placement}
+      style={
+        {
+          "--lab-model-tooltip-x": `${props.state.x}px`,
+          "--lab-model-tooltip-y": `${props.state.y}px`,
+        } as JSX.CSSProperties
+      }
+    >
+      <div data-slot="lab-model-tooltip-summary">
+        <div data-slot="lab-model-tooltip-head">
+          <span data-slot="lab-model-tooltip-avatar" aria-hidden="true">
+            <ProviderIcon id={getProviderIconId(props.state.model.lab)} />
+          </span>
+          <strong>{props.state.model.name}</strong>
+        </div>
         <p>
-          <b>{i18n.t("lab.usage")}</b>
-          <em>{props.usage ? formatTokens(props.usage.tokens) : "—"}</em>
+          Recent OpenCode Go usage, share, context, and output limits.
         </p>
+      </div>
+      <div data-slot="tooltip-divider" />
+      <div data-slot="lab-model-tooltip-metrics">
         <p>
-          <b>{i18n.t("lab.share")}</b>
-          <em>{props.usage ? formatPercent(props.usage.share) : "—"}</em>
+          <span>{i18n.t("lab.usage")}</span>
+          <b>{props.state.usage ? formatTokens(props.state.usage.tokens) : "-"}</b>
         </p>
         <p>
-          <b>{i18n.t("model.context")}</b>
-          <em>{formatCatalogLimit(props.model.limit?.context, i18n.t("home.unknown"))}</em>
+          <span>{i18n.t("lab.share")}</span>
+          <b>{props.state.usage ? formatPercent(props.state.usage.share) : "-"}</b>
         </p>
         <p>
-          <b>{i18n.t("model.output")}</b>
-          <em>{formatCatalogLimit(props.model.limit?.output, i18n.t("home.unknown"))}</em>
+          <span>{i18n.t("model.context")}</span>
+          <b>{formatCatalogLimit(props.state.model.limit?.context, i18n.t("home.unknown"))}</b>
         </p>
         <p>
-          <b>{i18n.t("model.release")}</b>
-          <em>{formatCatalogDate(props.model.releaseDate, language.tag(language.locale()), i18n.t("home.unknown"))}</em>
+          <span>{i18n.t("model.output")}</span>
+          <b>{formatCatalogLimit(props.state.model.limit?.output, i18n.t("home.unknown"))}</b>
         </p>
       </div>
-    </a>
+    </div>
   )
 }
 

+ 344 - 33
packages/stats/app/src/routes/index.css

@@ -3299,68 +3299,360 @@
   color: #b82d35;
 }
 
-[data-page="stats"] [data-component="lab-model-grid"] {
-  display: grid;
-  grid-template-columns: repeat(3, minmax(0, 1fr));
+[data-page="stats"] [data-section="model-panel"][data-variant="lab-models"] {
+  overflow: hidden;
+}
+
+[data-page="stats"] [data-slot="lab-model-heading"] {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 40px;
+  min-height: 42px;
+  margin-bottom: 40px;
+}
+
+[data-page="stats"] [data-slot="lab-model-heading"] [data-slot="section-title"] {
+  min-width: 0;
+  margin: 0;
+}
+
+[data-page="stats"] [data-slot="lab-model-compare"] {
+  position: relative;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  flex: 0 0 auto;
   gap: 8px;
+  height: 32px;
+  padding: 0 12px 0 8px;
+  border: 0;
+  border-radius: 0;
+  color: var(--stats-text);
+  background: var(--stats-bg);
+  box-shadow:
+    0 0 0 0.5px var(--stats-line-strong),
+    0 1px 1.5px #0000001a;
+  font: inherit;
+  font-size: 13px;
+  font-weight: 500;
+  line-height: 14px;
+  white-space: nowrap;
+  cursor: pointer;
+}
+
+[data-page="stats"] [data-slot="lab-model-compare"][hidden] {
+  display: none;
 }
 
-[data-page="stats"] [data-component="lab-model-card"] {
+[data-page="stats"] [data-slot="lab-model-compare"]::before {
+  position: absolute;
+  top: 0;
+  right: 0;
+  left: 0;
+  height: 16px;
+  pointer-events: none;
+  content: "";
+  background: linear-gradient(180deg, #ffffff00 0%, #ffffff16 100%);
+}
+
+[data-page="stats"] [data-slot="lab-model-compare"] span {
   position: relative;
-  display: grid;
-  align-content: space-between;
-  gap: 28px;
-  min-height: 220px;
-  padding: 16px;
+  z-index: 1;
+}
+
+[data-page="stats"] [data-slot="lab-model-compare-icon"] {
+  width: 16px;
+  height: 16px;
+  color: var(--stats-muted);
+  background:
+    linear-gradient(currentColor 0 0) 3px 3px / 3px 3px no-repeat,
+    linear-gradient(currentColor 0 0) 10px 3px / 3px 3px no-repeat,
+    linear-gradient(currentColor 0 0) 3px 10px / 3px 3px no-repeat,
+    linear-gradient(currentColor 0 0) 10px 10px / 3px 3px no-repeat;
+}
+
+[data-page="stats"] [data-slot="lab-model-compare"]:hover,
+[data-page="stats"] [data-slot="lab-model-compare"]:focus-visible {
+  color: var(--stats-text);
+  outline: none;
+  box-shadow:
+    0 0 0 0.5px var(--stats-text),
+    0 1px 1.5px #0000001a;
+}
+
+[data-page="stats"] [data-slot="lab-model-pattern"] {
+  height: 16px;
+  margin-bottom: 40px;
   overflow: hidden;
-  border: 1px solid var(--stats-line);
+  background: color-mix(in srgb, var(--stats-text) 10%, transparent);
+  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="lab-model-table"] {
+  --lab-model-row-bleed: 16px;
+  box-sizing: border-box;
+  width: calc(100% + 32px);
+  margin-inline: -16px;
+  padding-inline: 16px;
+  overflow-x: auto;
+  overflow-y: hidden;
+  scrollbar-width: none;
+}
+
+[data-page="stats"] [data-component="lab-model-table"]::-webkit-scrollbar {
+  display: none;
+}
+
+[data-page="stats"] [data-slot="lab-model-table-track"] {
+  --lab-model-column-template: 240px repeat(4, 200px) minmax(160px, 1fr);
+  --lab-model-table-width: 1200px;
+  position: relative;
+  display: grid;
+  width: 100%;
+  min-width: var(--lab-model-table-width);
+}
+
+[data-page="stats"] [data-slot="lab-model-table-track"]::before {
+  position: absolute;
+  inset: 0;
+  z-index: 0;
+  pointer-events: none;
+  content: "";
+  background:
+    linear-gradient(var(--stats-line), var(--stats-line)) 240px 0 / 1px 100% no-repeat,
+    linear-gradient(var(--stats-line), var(--stats-line)) 440px 0 / 1px 100% no-repeat,
+    linear-gradient(var(--stats-line), var(--stats-line)) 640px 0 / 1px 100% no-repeat,
+    linear-gradient(var(--stats-line), var(--stats-line)) 840px 0 / 1px 100% no-repeat,
+    linear-gradient(var(--stats-line), var(--stats-line)) 1040px 0 / 1px 100% no-repeat;
+}
+
+[data-page="stats"] [data-slot="lab-model-table-head"],
+[data-page="stats"] [data-component="lab-model-row"] {
+  position: relative;
+  z-index: 1;
+  display: grid;
+  grid-template-columns: var(--lab-model-column-template);
+}
+
+[data-page="stats"] [data-slot="lab-model-table-head"] {
+  align-items: start;
+  height: 20px;
+  margin-bottom: 20px;
+  color: var(--stats-muted);
+  font-size: 13px;
+  font-weight: 500;
+  line-height: 20px;
+  text-transform: uppercase;
+}
+
+[data-page="stats"] [data-slot="lab-model-table-head"] span,
+[data-page="stats"] [data-slot="lab-model-cell"] {
+  min-width: 0;
+}
+
+[data-page="stats"] [data-slot="lab-model-table-head"] span:not([data-column="model"]) {
+  padding-left: 40px;
+}
+
+[data-page="stats"] [data-component="lab-model-row"] {
+  align-items: center;
+  height: 64px;
+  color: var(--stats-text);
+  text-decoration: none;
+  isolation: isolate;
+}
+
+[data-page="stats"] [data-component="lab-model-row"]::before {
+  position: absolute;
+  top: 8px;
+  right: calc(var(--lab-model-row-bleed) * -1);
+  bottom: 8px;
+  left: calc(var(--lab-model-row-bleed) * -1);
+  z-index: 0;
+  content: "";
+  background: transparent;
+  pointer-events: none;
+}
+
+[data-page="stats"] [data-component="lab-model-row"]:hover,
+[data-page="stats"] [data-component="lab-model-row"]:focus-visible {
   color: var(--stats-text);
+  outline: none;
+  text-decoration: none;
+}
+
+[data-page="stats"] [data-component="lab-model-row"]:hover::before,
+[data-page="stats"] [data-component="lab-model-row"]:focus-visible::before {
   background: var(--stats-layer);
 }
 
-[data-page="stats"] [data-component="lab-model-card"]:hover,
-[data-page="stats"] [data-component="lab-model-card"]:focus-visible {
+[data-page="stats"] [data-slot="lab-model-cell"] {
+  position: relative;
   z-index: 1;
-  border-color: var(--stats-text);
-  outline: none;
+  display: flex;
+  align-items: center;
+  height: 24px;
+  color: var(--stats-text);
+  font-size: 16px;
+  font-weight: 400;
+  line-height: 24px;
+  font-variant-numeric: tabular-nums;
+  white-space: nowrap;
+}
+
+[data-page="stats"] [data-slot="lab-model-cell"][data-column="model"] {
+  gap: 16px;
+}
+
+[data-page="stats"] [data-slot="lab-model-cell"]:not([data-column="model"]):not([data-column="release"]) {
+  justify-content: flex-end;
+  padding-right: 40px;
+  text-align: right;
+}
+
+[data-page="stats"] [data-slot="lab-model-cell"][data-column="release"] {
+  padding-left: 40px;
+}
+
+[data-page="stats"] [data-slot="lab-model-avatar"] {
+  display: grid;
+  place-items: center;
+  flex: 0 0 auto;
+  width: 20px;
+  height: 20px;
+  color: #ffffff;
+  background: var(--stats-accent-text);
+  box-shadow: inset 0 0 0 1px #0000001a;
 }
 
-[data-page="stats"] [data-component="lab-model-card"] > strong {
-  align-self: start;
+[data-page="stats"] [data-slot="lab-model-avatar"] svg {
+  width: 15px;
+  height: 15px;
+}
+
+[data-page="stats"] [data-slot="lab-model-cell"] strong {
   min-width: 0;
+  overflow: hidden;
   color: var(--stats-text);
-  font-size: 24px;
+  font-size: inherit;
   font-weight: 500;
-  line-height: 1.05;
-  letter-spacing: 0;
-  overflow-wrap: anywhere;
+  line-height: inherit;
+  text-overflow: ellipsis;
+}
+
+[data-page="stats"] [data-component="lab-model-tooltip"] {
+  position: fixed;
+  top: var(--lab-model-tooltip-y);
+  left: var(--lab-model-tooltip-x);
+  z-index: 40;
+  width: 240px;
+  color: var(--stats-text);
+  background: var(--stats-bg);
+  border: 1px solid var(--stats-line);
+  box-shadow: 0 12px 24px #0000001f;
+  pointer-events: none;
+  transform: translate(12px, -50%);
+}
+
+[data-page="stats"] [data-component="lab-model-tooltip"][data-placement="left"] {
+  transform: translate(calc(-100% - 12px), -50%);
 }
 
-[data-page="stats"] [data-slot="lab-model-card-meta"] {
+[data-page="stats"] [data-slot="lab-model-tooltip-summary"] {
   display: grid;
-  gap: 8px;
+  gap: 6px;
+  min-width: 0;
+  padding: 12px;
 }
 
-[data-page="stats"] [data-slot="lab-model-card-meta"] p {
+[data-page="stats"] [data-slot="lab-model-tooltip-head"] {
   display: flex;
-  justify-content: space-between;
-  gap: 12px;
+  align-items: center;
+  gap: 8px;
+  min-width: 0;
+}
+
+[data-page="stats"] [data-slot="lab-model-tooltip-avatar"] {
+  display: grid;
+  place-items: center;
+  flex: 0 0 auto;
+  width: 16px;
+  height: 16px;
+  color: #ffffff;
+  background: var(--stats-accent-text);
+  box-shadow: inset 0 0 0 1px #0000001a;
+}
+
+[data-page="stats"] [data-slot="lab-model-tooltip-avatar"] svg {
+  width: 12px;
+  height: 12px;
+}
+
+[data-page="stats"] [data-slot="lab-model-tooltip-head"] strong {
+  min-width: 0;
+  overflow: hidden;
+  color: var(--stats-text);
+  font-size: 12px;
+  font-weight: 500;
+  line-height: 17px;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+[data-page="stats"] [data-slot="lab-model-tooltip-summary"] > p {
   margin: 0;
-  padding-top: 8px;
-  border-top: 1px solid var(--stats-line);
   color: var(--stats-muted);
-  font-size: 11px;
-  font-weight: 500;
-  line-height: 1.3;
+  font-size: 12px;
+  font-weight: 400;
+  line-height: 17px;
+}
+
+[data-page="stats"] [data-component="lab-model-tooltip"] [data-slot="tooltip-divider"] {
+  height: 0.5px;
+  background: var(--stats-line);
+}
+
+[data-page="stats"] [data-slot="lab-model-tooltip-metrics"] {
+  display: grid;
+  padding: 12px;
 }
 
-[data-page="stats"] [data-slot="lab-model-card-meta"] b,
-[data-page="stats"] [data-slot="lab-model-card-meta"] em {
+[data-page="stats"] [data-slot="lab-model-tooltip-metrics"] p {
+  display: grid;
+  grid-template-columns: minmax(0, 100px) minmax(0, 1fr);
+  align-items: center;
+  column-gap: 16px;
+  min-width: 0;
+  height: 17px;
+  margin: 0;
+  padding: 0;
+  color: var(--stats-muted);
+  font-size: 12px;
+  font-weight: 400;
+  line-height: 17px;
+}
+
+[data-page="stats"] [data-slot="lab-model-tooltip-metrics"] span,
+[data-page="stats"] [data-slot="lab-model-tooltip-metrics"] b {
+  min-width: 0;
+  overflow: hidden;
   font: inherit;
+  text-overflow: ellipsis;
+  white-space: nowrap;
 }
 
-[data-page="stats"] [data-slot="lab-model-card-meta"] em {
+[data-page="stats"] [data-slot="lab-model-tooltip-metrics"] b {
   color: var(--stats-text);
+  font-weight: 500;
+  font-variant-numeric: tabular-nums;
   text-align: right;
 }
 
@@ -4527,6 +4819,25 @@
     font-size: 13px;
   }
 
+  [data-page="stats"] [data-slot="lab-model-heading"] {
+    align-items: flex-start;
+    flex-direction: column;
+    gap: 16px;
+    margin-bottom: 32px;
+  }
+
+  [data-page="stats"] [data-slot="lab-model-pattern"] {
+    margin-bottom: 32px;
+  }
+
+  [data-page="stats"] [data-component="lab-model-table"] {
+    --lab-model-row-bleed: 24px;
+    box-sizing: border-box;
+    width: calc(100% + 48px);
+    margin-inline: -24px;
+    padding-inline: 24px;
+  }
+
   [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"] {