فهرست منبع

docs: redesign Go pricing graph with horizontal bars and inline request labels

Improve visual clarity of request limits on the Go pricing page by replacing
dot-based chart with animated horizontal bars that directly show model names
and exact request counts. Add proper OpenGraph and Twitter Card meta tags for
better social sharing discovery.
David Hill 6 ماه پیش
والد
کامیت
0b825ca383
3فایلهای تغییر یافته به همراه242 افزوده شده و 160 حذف شده
  1. 2 0
      packages/console/app/src/i18n/en.ts
  2. 143 111
      packages/console/app/src/routes/go/index.css
  3. 97 49
      packages/console/app/src/routes/go/index.tsx

+ 2 - 0
packages/console/app/src/i18n/en.ts

@@ -246,6 +246,8 @@ export const dict = {
   "zen.privacy.exceptionsLink": "following exceptions",
   "zen.privacy.exceptionsLink": "following exceptions",
 
 
   "go.title": "OpenCode Go | Low cost coding models for everyone",
   "go.title": "OpenCode Go | Low cost coding models for everyone",
+  "go.meta.description":
+    "Go is a $10/month subscription with generous 5-hour request limits for GLM-5, Kimi K2.5, and MiniMax M2.5.",
   "go.hero.title": "Low cost coding models for everyone",
   "go.hero.title": "Low cost coding models for everyone",
   "go.hero.body":
   "go.hero.body":
     "Go brings agentic coding to programmers around the world. Offering generous limits and reliable access to the most capable open-source models, so you can build with powerful agents without worrying about cost or availability.",
     "Go brings agentic coding to programmers around the world. Offering generous limits and reliable access to the most capable open-source models, so you can build with powerful agents without worrying about cost or availability.",

+ 143 - 111
packages/console/app/src/routes/go/index.css

@@ -21,6 +21,13 @@
   }
   }
 }
 }
 
 
+@keyframes go-graph-bar {
+  to {
+    opacity: 1;
+    transform: scaleX(1);
+  }
+}
+
 [data-page="go"] {
 [data-page="go"] {
   --color-background: hsl(0, 20%, 99%);
   --color-background: hsl(0, 20%, 99%);
   --color-background-weak: hsl(0, 8%, 97%);
   --color-background-weak: hsl(0, 8%, 97%);
@@ -424,13 +431,78 @@ body {
 
 
     [data-component="limit-graph"] {
     [data-component="limit-graph"] {
       margin: 0 auto;
       margin: 0 auto;
-      max-width: calc(100% - (var(--padding) * 2));
+      width: calc(100% - 120px);
+      max-width: calc(100% - 120px);
       border: none;
       border: none;
       background: transparent;
       background: transparent;
-      padding: 18px 18px 56px;
+      padding: 58px var(--padding) 56px;
+
+      @media (max-width: 48rem) {
+        width: 100%;
+        max-width: 100%;
+      }
 
 
       [data-slot="plot"] {
       [data-slot="plot"] {
         position: relative;
         position: relative;
+        overflow: visible;
+        width: 100%;
+        margin: 0 auto;
+        margin-left: -40px;
+      }
+
+      [data-slot="ylabels"] {
+        position: absolute;
+        inset: 0;
+        pointer-events: none;
+      }
+
+      [data-slot="ylabels"] [data-ylabel] {
+        position: absolute;
+        left: var(--x);
+        top: var(--y);
+        transform: translate(-100%, -50%);
+        color: var(--color-text-strong);
+        font-size: 16px;
+        font-weight: 700;
+        line-height: 1;
+        white-space: nowrap;
+      }
+
+      [data-slot="pills"] {
+        position: absolute;
+        inset: 0;
+        pointer-events: none;
+
+        [data-item] {
+          position: absolute;
+          left: var(--x);
+          top: var(--y);
+          transform: translate(12px, -50%);
+          display: inline-flex;
+          align-items: center;
+          gap: 8px;
+          border: none;
+          background: transparent;
+          height: 20px;
+          padding: 0 8px;
+          border-radius: 2px;
+          max-width: calc(100% - 12px);
+          font-size: 13px;
+          line-height: 20px;
+          box-sizing: border-box;
+          opacity: 0;
+        }
+
+        [data-name] {
+          color: var(--color-text);
+          white-space: nowrap;
+        }
+
+        [data-value] {
+          color: var(--color-text-strong);
+          font-weight: 600;
+          white-space: nowrap;
+        }
       }
       }
 
 
       [data-slot="plot-labels"] {
       [data-slot="plot-labels"] {
@@ -451,8 +523,7 @@ body {
 
 
       svg {
       svg {
         width: 100%;
         width: 100%;
-        height: auto;
-        aspect-ratio: 720 / 220;
+        height: 220px;
         display: block;
         display: block;
       }
       }
 
 
@@ -479,13 +550,44 @@ body {
         font-weight: 600;
         font-weight: 600;
       }
       }
 
 
+      [data-row],
+      [data-val] {
+        opacity: 0;
+      }
+
+      &[data-visible] [data-row],
+      &[data-visible] [data-val] {
+        opacity: 1;
+        transition: opacity 240ms ease;
+        transition-delay: var(--d, 0ms);
+      }
+
       [data-stub] {
       [data-stub] {
         stroke: var(--color-border);
         stroke: var(--color-border);
-        stroke-width: 2;
+        stroke-width: 1;
         stroke-linecap: round;
         stroke-linecap: round;
         opacity: 0.55;
         opacity: 0.55;
       }
       }
 
 
+      [data-bar] {
+        transform-box: fill-box;
+        transform-origin: left center;
+        opacity: 0;
+        transform: scaleX(0.02);
+        fill: var(--color-go-2);
+        stroke: none;
+      }
+
+      [data-bar][data-kind="free"] {
+        fill: var(--color-text-strong);
+      }
+
+      [data-val] {
+        fill: var(--color-text-strong);
+        font-size: 13px;
+        font-weight: 650;
+      }
+
       [data-range] {
       [data-range] {
         stroke: var(--color-text-strong);
         stroke: var(--color-text-strong);
         stroke-width: 2;
         stroke-width: 2;
@@ -542,6 +644,17 @@ body {
         animation-delay: var(--d, 0ms);
         animation-delay: var(--d, 0ms);
       }
       }
 
 
+      &[data-visible] [data-bar] {
+        animation: go-graph-bar 560ms cubic-bezier(0.2, 0.7, 0.2, 1) forwards;
+        animation-delay: var(--d, 0ms);
+      }
+
+      &[data-visible] [data-slot="pills"] [data-item] {
+        opacity: 1;
+        transition: opacity 240ms ease;
+        transition-delay: var(--d, 0ms);
+      }
+
       @media (prefers-reduced-motion: reduce) {
       @media (prefers-reduced-motion: reduce) {
         [data-animate="line"] {
         [data-animate="line"] {
           stroke-dashoffset: 0;
           stroke-dashoffset: 0;
@@ -552,34 +665,49 @@ body {
           transform: none;
           transform: none;
           animation: none;
           animation: none;
         }
         }
+        [data-bar] {
+          opacity: 1;
+          transform: none;
+          animation: none;
+        }
+        [data-row],
+        [data-val] {
+          opacity: 1;
+          transition: none;
+        }
+
+        [data-slot="pills"] [data-item] {
+          opacity: 1;
+          transition: none;
+        }
       }
       }
 
 
       figcaption {
       figcaption {
         margin-top: 34px;
         margin-top: 34px;
         display: flex;
         display: flex;
-        flex-direction: column;
-        gap: 10px;
+        justify-content: center;
         font-size: 13px;
         font-size: 13px;
+        text-align: center;
       }
       }
 
 
       [data-slot="caption-row"] {
       [data-slot="caption-row"] {
         display: flex;
         display: flex;
         width: 100%;
         width: 100%;
+        justify-content: center;
       }
       }
 
 
       [data-slot="caption-left"] {
       [data-slot="caption-left"] {
-        display: grid;
+        display: flex;
         width: 100%;
         width: 100%;
-        grid-template-columns: var(--start, 16.9%) minmax(0, 1fr);
-        grid-template-rows: auto auto;
-        align-items: center;
-        column-gap: 0;
-        row-gap: 0;
         min-width: 0;
         min-width: 0;
+        justify-content: center;
       }
       }
 
 
       [data-slot="caption-meta"] {
       [data-slot="caption-meta"] {
-        display: contents;
+        display: flex;
+        flex-direction: row;
+        gap: 16px;
+        align-items: baseline;
       }
       }
 
 
       [data-slot="caption-label"] {
       [data-slot="caption-label"] {
@@ -587,8 +715,6 @@ body {
         font-weight: 650;
         font-weight: 650;
         white-space: nowrap;
         white-space: nowrap;
         line-height: 1;
         line-height: 1;
-        grid-column: 1;
-        grid-row: 1;
       }
       }
 
 
       [data-slot="caption-link"] {
       [data-slot="caption-link"] {
@@ -596,73 +722,6 @@ body {
         text-decoration-thickness: 1px;
         text-decoration-thickness: 1px;
         width: fit-content;
         width: fit-content;
         line-height: 1;
         line-height: 1;
-        grid-column: 1;
-        grid-row: 2;
-        align-self: start;
-      }
-
-      [data-slot="legend"] {
-        display: flex;
-        width: 100%;
-        flex-wrap: nowrap;
-        gap: 10px;
-        min-width: 0;
-        overflow-x: auto;
-        -webkit-overflow-scrolling: touch;
-        padding-bottom: 8px;
-        margin-left: -12px;
-        grid-column: 2;
-        grid-row: 1;
-        align-self: center;
-
-        [data-item] {
-          display: inline-flex;
-          flex: 0 0 auto;
-          align-items: center;
-          gap: 8px;
-          border: 1px solid var(--color-border-weak);
-          background: var(--color-background);
-          padding: 6px 10px;
-          border-radius: 999px;
-          max-width: 100%;
-        }
-
-        [data-dot] {
-          width: 10px;
-          height: 10px;
-          border-radius: 999px;
-          display: inline-block;
-          border: 1px solid var(--color-text-strong);
-          background: var(--color-background);
-          flex: 0 0 auto;
-        }
-
-        [data-dot][data-kind="go"] {
-          background: var(--color-background-interactive);
-        }
-
-        [data-dot][data-kind="go"][data-model="glm"] {
-          background: var(--color-go-1);
-        }
-
-        [data-dot][data-kind="go"][data-model="kimi"] {
-          background: var(--color-go-2);
-        }
-
-        [data-dot][data-kind="go"][data-model="minimax"] {
-          background: var(--color-go-3);
-        }
-
-        [data-name] {
-          color: var(--color-text);
-          white-space: nowrap;
-        }
-
-        [data-value] {
-          color: var(--color-text-strong);
-          font-weight: 600;
-          white-space: nowrap;
-        }
       }
       }
 
 
       [data-slot="caption-note"] {
       [data-slot="caption-note"] {
@@ -671,35 +730,8 @@ body {
       }
       }
 
 
       @media (max-width: 56.25rem) {
       @media (max-width: 56.25rem) {
-        [data-slot="caption-left"] {
-          grid-template-columns: var(--start, 16.9%) minmax(0, 1fr);
-          grid-template-rows: auto auto;
-          align-items: start;
-        }
-
-        [data-slot="legend"] {
-          grid-column: 2;
-          grid-row: 1;
-        }
-
         [data-slot="caption-meta"] {
         [data-slot="caption-meta"] {
-          display: flex;
-          gap: 24px;
-          align-items: baseline;
-          grid-column: 2;
-          grid-row: 2;
-          margin-top: 12px;
-        }
-
-        [data-slot="caption-label"] {
-          grid-column: auto;
-          grid-row: auto;
-        }
-
-        [data-slot="caption-link"] {
-          grid-column: auto;
-          grid-row: auto;
-          align-self: baseline;
+          gap: 14px;
         }
         }
       }
       }
     }
     }

+ 97 - 49
packages/console/app/src/routes/go/index.tsx

@@ -10,6 +10,7 @@ import { Faq } from "~/component/faq"
 import { Legal } from "~/component/legal"
 import { Legal } from "~/component/legal"
 import { Footer } from "~/component/footer"
 import { Footer } from "~/component/footer"
 import { Header } from "~/component/header"
 import { Header } from "~/component/header"
+import { config } from "~/config"
 import { getLastSeenWorkspaceID } from "../workspace/common"
 import { getLastSeenWorkspaceID } from "../workspace/common"
 import { IconMiniMax, IconZai } from "~/component/icon"
 import { IconMiniMax, IconZai } from "~/component/icon"
 import { useI18n } from "~/context/i18n"
 import { useI18n } from "~/context/i18n"
@@ -49,24 +50,50 @@ function LimitsGraph(props: { href: string }) {
     { id: "kimi", name: "Kimi K2.5", req: 1850, d: "240ms" },
     { id: "kimi", name: "Kimi K2.5", req: 1850, d: "240ms" },
     { id: "minimax", name: "MiniMax M2.5", req: 20000, d: "360ms" },
     { id: "minimax", name: "MiniMax M2.5", req: 20000, d: "360ms" },
   ]
   ]
-  const ratio = (n: number) => n / free
 
 
   const w = 720
   const w = 720
   const h = 220
   const h = 220
-  const left = 88
-  const right = 24
-  const top = 22
-  const bottom = 46
+  const left = 40
+  const right = 60
+  const top = 18
+  const bottom = 44
   const plot = w - left - right
   const plot = w - left - right
+
+  const ratio = (n: number) => n / free
   const rmax = Math.max(1, ...models.map((m) => ratio(m.req)))
   const rmax = Math.max(1, ...models.map((m) => ratio(m.req)))
   const log = (n: number) => Math.log10(Math.max(n, 1))
   const log = (n: number) => Math.log10(Math.max(n, 1))
-  const x = (r: number) => left + (log(r) / log(rmax)) * plot
+  const base = 24
+  const p = 2.2
+  const x = (r: number) => left + base + Math.pow(log(r) / log(rmax), p) * (plot - base)
   const start = (x(1) / w) * 100
   const start = (x(1) / w) * 100
 
 
-  const yFree = 74
-  const yGo = 134
   const ticks = [1, 2, 5, 10, 25, 50, 100].filter((t) => t <= rmax)
   const ticks = [1, 2, 5, 10, 25, 50, 100].filter((t) => t <= rmax)
-  const y = (n: number) => `${(n / h) * 100}%`
+  const labels = (() => {
+    const set = new Set<number>()
+    let last = -Infinity
+    for (const t of ticks) {
+      if (t === 1) {
+        set.add(t)
+        last = x(t)
+        continue
+      }
+      const pos = x(t)
+      if (pos - last < 44) continue
+      set.add(t)
+      last = pos
+    }
+    return set
+  })()
+  const bh = 8
+  const gap = 16
+  const step = bh + gap
+  const sep = bh + 40
+  const fy = top + 22
+  const gy = (i: number) => fy + sep + step * i
+  const my = models.length < 2 ? gy(0) : (gy(0) + gy(models.length - 1)) / 2
+  const px = (n: number) => `${(n / w) * 100}%`
+  const py = (n: number) => `${(n / h) * 100}%`
+  const lx = px(left - 16)
 
 
   return (
   return (
     <figure
     <figure
@@ -77,52 +104,81 @@ function LimitsGraph(props: { href: string }) {
       style={{ "--start": `${start}%` } as any}
       style={{ "--start": `${start}%` } as any}
     >
     >
       <div data-slot="plot">
       <div data-slot="plot">
-        <svg viewBox={`0 0 ${w} ${h}`} role="img" aria-hidden="true">
+        <svg
+          viewBox={`0 0 ${w} ${h}`}
+          preserveAspectRatio="none"
+          role="img"
+          aria-hidden="true"
+          style={{ height: `${h}px` }}
+        >
           <g data-slot="grid">
           <g data-slot="grid">
             <For each={ticks}>
             <For each={ticks}>
               {(t) => (
               {(t) => (
                 <g>
                 <g>
                   <line x1={x(t)} y1={top} x2={x(t)} y2={h - bottom} data-grid />
                   <line x1={x(t)} y1={top} x2={x(t)} y2={h - bottom} data-grid />
-                  <text x={x(t)} y={h - 18} text-anchor="middle" data-tick>
-                    {i18n.t("go.graph.tick", { n: t })}
-                  </text>
+                  {labels.has(t) ? (
+                    <text x={x(t)} y={h - 18} text-anchor="middle" data-tick>
+                      {i18n.t("go.graph.tick", { n: t })}
+                    </text>
+                  ) : null}
                 </g>
                 </g>
               )}
               )}
             </For>
             </For>
           </g>
           </g>
 
 
-          <g data-slot="free" style={{ "--d": "0ms" } as any}>
-            <circle cx={x(1)} cy={yFree} r={5.5} data-point data-kind="free" />
-          </g>
+          <line x1={left} y1={top} x2={left} y2={h - bottom} data-stub />
+
+          <g data-slot="bars">
+            <g style={{ "--d": "0ms" } as any}>
+              <rect x={left} y={fy - bh / 2} width={Math.max(0, x(1) - left)} height={bh} data-bar data-kind="free" />
+            </g>
 
 
-          <g data-slot="go">
-            <line x1={x(1)} y1={yGo} x2={x(ratio(models[0]!.req))} y2={yGo} data-range data-animate="line" />
-            <line
-              x1={x(ratio(models[0]!.req))}
-              y1={yGo}
-              x2={x(ratio(models[2]!.req))}
-              y2={yGo}
-              data-range
-              data-animate="line"
-            />
             <For each={models}>
             <For each={models}>
-              {(m) => (
+              {(m, i) => (
                 <g style={{ "--d": m.d } as any}>
                 <g style={{ "--d": m.d } as any}>
-                  <circle cx={x(ratio(m.req))} cy={yGo} r={5.5} data-point data-kind="go" data-model={m.id} />
+                  <rect
+                    x={left}
+                    y={gy(i()) - bh / 2}
+                    width={Math.max(0, x(ratio(m.req)) - left)}
+                    height={bh}
+                    data-bar
+                    data-kind="go"
+                    data-model={m.id}
+                  />
                 </g>
                 </g>
               )}
               )}
             </For>
             </For>
           </g>
           </g>
         </svg>
         </svg>
 
 
-        <div data-slot="plot-labels">
-          <span data-row-label style={{ "--y": y(yFree) } as any}>
+        <div data-slot="ylabels" aria-hidden="true">
+          <span data-ylabel style={{ "--x": lx, "--y": py(fy) } as any}>
             {i18n.t("go.graph.free")}
             {i18n.t("go.graph.free")}
           </span>
           </span>
-          <span data-row-label style={{ "--y": y(yGo) } as any}>
+          <span data-ylabel style={{ "--x": lx, "--y": py(my) } as any}>
             {i18n.t("go.graph.go")}
             {i18n.t("go.graph.go")}
           </span>
           </span>
         </div>
         </div>
+
+        <div data-slot="pills" aria-hidden="true">
+          <span data-item data-kind="free" style={{ "--x": px(x(1)), "--y": py(fy), "--d": "0ms" } as any}>
+            <span data-name>{i18n.t("go.graph.free")}</span>
+            <span data-value>{free.toLocaleString()}</span>
+          </span>
+          <For each={models}>
+            {(m, i) => (
+              <span
+                data-item
+                data-kind="go"
+                data-model={m.id}
+                style={{ "--x": px(x(ratio(m.req))), "--y": py(gy(i())), "--d": m.d } as any}
+              >
+                <span data-name>{m.name}</span>
+                <span data-value>{m.req.toLocaleString()}</span>
+              </span>
+            )}
+          </For>
+        </div>
       </div>
       </div>
 
 
       <figcaption>
       <figcaption>
@@ -134,22 +190,6 @@ function LimitsGraph(props: { href: string }) {
                 {i18n.t("go.graph.usageLimits")}
                 {i18n.t("go.graph.usageLimits")}
               </a>
               </a>
             </div>
             </div>
-            <div data-slot="legend">
-              <span data-item>
-                <i data-dot data-kind="free" />
-                <span data-name>{i18n.t("go.graph.free")}</span>
-                <span data-value>{free.toLocaleString()}</span>
-              </span>
-              <For each={models}>
-                {(m) => (
-                  <span data-item>
-                    <i data-dot data-kind="go" data-model={m.id} />
-                    <span data-name>{m.name}</span>
-                    <span data-value>{m.req.toLocaleString()}</span>
-                  </span>
-                )}
-              </For>
-            </div>
           </div>
           </div>
         </div>
         </div>
       </figcaption>
       </figcaption>
@@ -165,9 +205,17 @@ export default function Home() {
     <main data-page="go">
     <main data-page="go">
       {/*<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />*/}
       {/*<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />*/}
       <Title>{i18n.t("go.title")}</Title>
       <Title>{i18n.t("go.title")}</Title>
+      <Meta name="description" content={i18n.t("go.meta.description")} />
       <LocaleLinks path="/go" />
       <LocaleLinks path="/go" />
-      <Meta property="og:image" content="/social-share-zen.png" />
-      <Meta name="twitter:image" content="/social-share-zen.png" />
+      <Meta property="og:type" content="website" />
+      <Meta property="og:url" content={`${config.baseUrl}${language.route("/go")}`} />
+      <Meta property="og:title" content={i18n.t("go.title")} />
+      <Meta property="og:description" content={i18n.t("go.meta.description")} />
+      <Meta property="og:image" content="/social-share-black.png" />
+      <Meta name="twitter:card" content="summary_large_image" />
+      <Meta name="twitter:title" content={i18n.t("go.title")} />
+      <Meta name="twitter:description" content={i18n.t("go.meta.description")} />
+      <Meta name="twitter:image" content="/social-share-black.png" />
       <Meta name="opencode:auth" content={loggedin() ? "true" : "false"} />
       <Meta name="opencode:auth" content={loggedin() ? "true" : "false"} />
 
 
       <div data-component="container">
       <div data-component="container">