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.go-page
parent
40fc406424
commit
0b825ca383
|
|
@ -246,6 +246,8 @@ export const dict = {
|
|||
"zen.privacy.exceptionsLink": "following exceptions",
|
||||
|
||||
"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.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.",
|
||||
|
|
|
|||
|
|
@ -21,6 +21,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
@keyframes go-graph-bar {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scaleX(1);
|
||||
}
|
||||
}
|
||||
|
||||
[data-page="go"] {
|
||||
--color-background: hsl(0, 20%, 99%);
|
||||
--color-background-weak: hsl(0, 8%, 97%);
|
||||
|
|
@ -424,13 +431,78 @@ body {
|
|||
|
||||
[data-component="limit-graph"] {
|
||||
margin: 0 auto;
|
||||
max-width: calc(100% - (var(--padding) * 2));
|
||||
width: calc(100% - 120px);
|
||||
max-width: calc(100% - 120px);
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 18px 18px 56px;
|
||||
padding: 58px var(--padding) 56px;
|
||||
|
||||
@media (max-width: 48rem) {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
[data-slot="plot"] {
|
||||
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"] {
|
||||
|
|
@ -451,8 +523,7 @@ body {
|
|||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
aspect-ratio: 720 / 220;
|
||||
height: 220px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
|
@ -479,13 +550,44 @@ body {
|
|||
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] {
|
||||
stroke: var(--color-border);
|
||||
stroke-width: 2;
|
||||
stroke-width: 1;
|
||||
stroke-linecap: round;
|
||||
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] {
|
||||
stroke: var(--color-text-strong);
|
||||
stroke-width: 2;
|
||||
|
|
@ -542,6 +644,17 @@ body {
|
|||
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) {
|
||||
[data-animate="line"] {
|
||||
stroke-dashoffset: 0;
|
||||
|
|
@ -552,34 +665,49 @@ body {
|
|||
transform: 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 {
|
||||
margin-top: 34px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
[data-slot="caption-row"] {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
[data-slot="caption-left"] {
|
||||
display: grid;
|
||||
display: flex;
|
||||
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;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
[data-slot="caption-meta"] {
|
||||
display: contents;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16px;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
[data-slot="caption-label"] {
|
||||
|
|
@ -587,8 +715,6 @@ body {
|
|||
font-weight: 650;
|
||||
white-space: nowrap;
|
||||
line-height: 1;
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
[data-slot="caption-link"] {
|
||||
|
|
@ -596,73 +722,6 @@ body {
|
|||
text-decoration-thickness: 1px;
|
||||
width: fit-content;
|
||||
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"] {
|
||||
|
|
@ -671,35 +730,8 @@ body {
|
|||
}
|
||||
|
||||
@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"] {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { Faq } from "~/component/faq"
|
|||
import { Legal } from "~/component/legal"
|
||||
import { Footer } from "~/component/footer"
|
||||
import { Header } from "~/component/header"
|
||||
import { config } from "~/config"
|
||||
import { getLastSeenWorkspaceID } from "../workspace/common"
|
||||
import { IconMiniMax, IconZai } from "~/component/icon"
|
||||
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: "minimax", name: "MiniMax M2.5", req: 20000, d: "360ms" },
|
||||
]
|
||||
const ratio = (n: number) => n / free
|
||||
|
||||
const w = 720
|
||||
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 ratio = (n: number) => n / free
|
||||
const rmax = Math.max(1, ...models.map((m) => ratio(m.req)))
|
||||
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 yFree = 74
|
||||
const yGo = 134
|
||||
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 (
|
||||
<figure
|
||||
|
|
@ -77,52 +104,81 @@ function LimitsGraph(props: { href: string }) {
|
|||
style={{ "--start": `${start}%` } as any}
|
||||
>
|
||||
<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">
|
||||
<For each={ticks}>
|
||||
{(t) => (
|
||||
<g>
|
||||
<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>
|
||||
)}
|
||||
</For>
|
||||
</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}>
|
||||
{(m) => (
|
||||
{(m, i) => (
|
||||
<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>
|
||||
)}
|
||||
</For>
|
||||
</g>
|
||||
</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")}
|
||||
</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")}
|
||||
</span>
|
||||
</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>
|
||||
|
||||
<figcaption>
|
||||
|
|
@ -134,22 +190,6 @@ function LimitsGraph(props: { href: string }) {
|
|||
{i18n.t("go.graph.usageLimits")}
|
||||
</a>
|
||||
</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>
|
||||
</figcaption>
|
||||
|
|
@ -165,9 +205,17 @@ export default function Home() {
|
|||
<main data-page="go">
|
||||
{/*<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />*/}
|
||||
<Title>{i18n.t("go.title")}</Title>
|
||||
<Meta name="description" content={i18n.t("go.meta.description")} />
|
||||
<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"} />
|
||||
|
||||
<div data-component="container">
|
||||
|
|
|
|||
Loading…
Reference in New Issue