summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDavid Hill <[email protected]>2026-03-04 15:21:07 +0000
committerDavid Hill <[email protected]>2026-03-04 15:21:07 +0000
commit7c215c0d02796803cd20602bb4b04a7d3c43760a (patch)
treeb41257bf490b2ea6131fb762ed0bda5e23751038
parentad563381087084de2cd7b88ad25965b52ed162da (diff)
downloadopencode-7c215c0d02796803cd20602bb4b04a7d3c43760a.tar.gz
opencode-7c215c0d02796803cd20602bb4b04a7d3c43760a.zip
docs: replace Go landing page video with interactive limits graph
Users can now see a clear visual comparison of request limits between Free tier and Go tier across all available models, making it easier to understand the value of a Go subscription at a glance.
-rw-r--r--packages/console/app/src/i18n/en.ts5
-rw-r--r--packages/console/app/src/routes/go/index.css313
-rw-r--r--packages/console/app/src/routes/go/index.tsx141
3 files changed, 446 insertions, 13 deletions
diff --git a/packages/console/app/src/i18n/en.ts b/packages/console/app/src/i18n/en.ts
index 41c3a76fc..5052529c8 100644
--- a/packages/console/app/src/i18n/en.ts
+++ b/packages/console/app/src/i18n/en.ts
@@ -251,11 +251,12 @@ export const dict = {
"go.cta.start": "Subscribe to Go - $10/mo",
"go.pricing.body": "Use with any agent. Top up credit if needed. Cancel any time.",
+ "go.graph.free": "Free",
+ "go.graph.go": "Go",
"go.problem.title": "What problem is Go solving?",
"go.problem.body":
"We're focused on bringing the OpenCode experience to as many people as possible. OpenCode Go is a low cost ($10/month) subscription designed to bring agentic coding to programmers around the world. It provides generous limits and reliable access to the most capable open source models.",
- "go.problem.subtitle":
- " ",
+ "go.problem.subtitle": " ",
"go.problem.item1": "Low cost subscription pricing",
"go.problem.item2": "Generous limits and reliable access",
"go.problem.item3": "Built for as many programmers as possible",
diff --git a/packages/console/app/src/routes/go/index.css b/packages/console/app/src/routes/go/index.css
index 2167731a9..e0a1ead0c 100644
--- a/packages/console/app/src/routes/go/index.css
+++ b/packages/console/app/src/routes/go/index.css
@@ -8,6 +8,19 @@
}
}
+@keyframes go-graph-line {
+ to {
+ stroke-dashoffset: 0;
+ }
+}
+
+@keyframes go-graph-point {
+ to {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
+
[data-page="go"] {
--color-background: hsl(0, 20%, 99%);
--color-background-weak: hsl(0, 8%, 97%);
@@ -16,6 +29,10 @@
--color-background-interactive: hsl(62, 84%, 88%);
--color-background-interactive-weaker: hsl(64, 74%, 95%);
+ --color-go-1: hsl(61.9, 82.6%, 77.5%);
+ --color-go-2: hsl(62.4, 78.6%, 56.1%);
+ --color-go-3: hsl(62.1, 100%, 39.8%);
+
--color-text: hsl(0, 1%, 39%);
--color-text-weak: hsl(0, 1%, 74%);
--color-text-weaker: hsl(30, 2%, 81%);
@@ -37,6 +54,10 @@
--color-background-interactive: hsl(62, 100%, 90%);
--color-background-interactive-weaker: hsl(60, 20%, 8%);
+ --color-go-1: hsl(62.1, 64.9%, 25.7%);
+ --color-go-2: hsl(61.7, 46.4%, 53.9%);
+ --color-go-3: hsl(61.9, 100%, 50%);
+
--color-text: hsl(0, 4%, 71%);
--color-text-weak: hsl(0, 2%, 49%);
--color-text-weaker: hsl(0, 3%, 28%);
@@ -381,12 +402,292 @@ body {
[data-component="comparison"] {
border-top: 1px solid var(--color-border-weak);
- video {
- width: 100%;
- height: auto;
- max-width: none;
- max-height: none;
- display: block;
+ padding: 0;
+ background:
+ radial-gradient(1200px 400px at 15% 0%, rgba(0, 0, 0, 0.035), transparent 55%),
+ radial-gradient(900px 320px at 85% 15%, rgba(0, 0, 0, 0.02), transparent 60%), var(--color-background-weak);
+
+ @media (prefers-color-scheme: dark) {
+ background:
+ radial-gradient(1200px 400px at 15% 0%, rgba(255, 255, 255, 0.03), transparent 55%),
+ radial-gradient(900px 320px at 85% 15%, rgba(255, 255, 255, 0.02), transparent 60%),
+ var(--color-background-weak);
+ }
+
+ [data-component="limit-graph"] {
+ margin: 0 auto;
+ max-width: calc(100% - (var(--padding) * 2));
+ border: none;
+ background: transparent;
+ padding: 18px 18px 56px;
+
+ [data-slot="plot"] {
+ position: relative;
+ }
+
+ [data-slot="plot-labels"] {
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ }
+
+ [data-row-label] {
+ position: absolute;
+ left: 0;
+ top: var(--y);
+ transform: translateY(-50%);
+ color: var(--color-text-strong);
+ font-size: 16px;
+ font-weight: 700;
+ }
+
+ svg {
+ width: 100%;
+ height: auto;
+ aspect-ratio: 720 / 220;
+ display: block;
+ }
+
+ [data-grid] {
+ stroke: var(--color-border);
+ stroke-width: 1;
+ opacity: 0.6;
+ }
+
+ [data-tick] {
+ fill: var(--color-text-weak);
+ font-size: 12px;
+ }
+
+ [data-row] {
+ fill: var(--color-text-strong);
+ font-size: 13px;
+ font-weight: 600;
+ }
+
+ [data-stub] {
+ stroke: var(--color-border);
+ stroke-width: 2;
+ stroke-linecap: round;
+ opacity: 0.55;
+ }
+
+ [data-range] {
+ stroke: var(--color-text-strong);
+ stroke-width: 2;
+ stroke-linecap: round;
+ opacity: 0.65;
+ }
+
+ [data-point] {
+ vector-effect: non-scaling-stroke;
+ stroke-width: 1;
+ transform-box: fill-box;
+ transform-origin: center;
+ }
+
+ [data-point][data-kind="free"] {
+ fill: var(--color-background);
+ stroke: var(--color-text-strong);
+ }
+
+ [data-point][data-kind="go"] {
+ fill: var(--color-background-interactive);
+ stroke: var(--color-text-strong);
+ }
+
+ [data-point][data-kind="go"][data-model="glm"] {
+ fill: var(--color-go-1);
+ }
+
+ [data-point][data-kind="go"][data-model="kimi"] {
+ fill: var(--color-go-2);
+ }
+
+ [data-point][data-kind="go"][data-model="minimax"] {
+ fill: var(--color-go-3);
+ }
+
+ [data-animate="line"] {
+ stroke-dasharray: 900;
+ stroke-dashoffset: 900;
+ }
+
+ &[data-visible] [data-animate="line"] {
+ animation: go-graph-line 1000ms cubic-bezier(0.2, 0.7, 0.2, 1) forwards;
+ animation-delay: 80ms;
+ }
+
+ [data-point] {
+ opacity: 0;
+ transform: scale(0.85);
+ }
+
+ &[data-visible] [data-point] {
+ animation: go-graph-point 520ms cubic-bezier(0.2, 0.7, 0.2, 1) forwards;
+ animation-delay: var(--d, 0ms);
+ }
+
+ @media (prefers-reduced-motion: reduce) {
+ [data-animate="line"] {
+ stroke-dashoffset: 0;
+ animation: none;
+ }
+ [data-point] {
+ opacity: 1;
+ transform: none;
+ animation: none;
+ }
+ }
+
+ figcaption {
+ margin-top: 34px;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ font-size: 13px;
+ }
+
+ [data-slot="caption-row"] {
+ display: flex;
+ width: 100%;
+ }
+
+ [data-slot="caption-left"] {
+ display: grid;
+ 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;
+ }
+
+ [data-slot="caption-meta"] {
+ display: contents;
+ }
+
+ [data-slot="caption-label"] {
+ color: var(--color-text-strong);
+ font-weight: 650;
+ white-space: nowrap;
+ line-height: 1;
+ grid-column: 1;
+ grid-row: 1;
+ }
+
+ [data-slot="caption-link"] {
+ color: var(--color-text-strong);
+ 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"] {
+ color: var(--color-text-weak);
+ font-size: 12px;
+ }
+
+ @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;
+ }
+ }
}
}
diff --git a/packages/console/app/src/routes/go/index.tsx b/packages/console/app/src/routes/go/index.tsx
index 74da24832..af77917a3 100644
--- a/packages/console/app/src/routes/go/index.tsx
+++ b/packages/console/app/src/routes/go/index.tsx
@@ -1,11 +1,10 @@
import "./index.css"
import { createAsync, query, redirect } from "@solidjs/router"
import { Title, Meta } from "@solidjs/meta"
+import { For, createSignal, onCleanup, onMount } from "solid-js"
//import { HttpHeader } from "@solidjs/start"
import goLogoLight from "../../asset/go-ornate-light.svg"
import goLogoDark from "../../asset/go-ornate-dark.svg"
-import compareVideo from "../../asset/lander/opencode-comparison-min.mp4"
-import compareVideoPoster from "../../asset/lander/opencode-comparison-poster.png"
import avatarDax from "../../asset/lander/avatar-dax.png"
import avatarJay from "../../asset/lander/avatar-jay.png"
import avatarFrank from "../../asset/lander/avatar-frank.png"
@@ -28,6 +27,137 @@ const checkLoggedIn = query(async () => {
if (workspaceID) throw redirect(`/workspace/${workspaceID}`)
}, "checkLoggedIn.get")
+function LimitsGraph(props: { href: string; labels: { free: string; go: string } }) {
+ let root!: HTMLElement
+ const [visible, setVisible] = createSignal(false)
+
+ onMount(() => {
+ if (typeof IntersectionObserver === "undefined") return setVisible(true)
+ const observer = new IntersectionObserver(
+ (entries) => {
+ const entry = entries[0]
+ if (!entry?.isIntersecting) return
+ setVisible(true)
+ observer.disconnect()
+ },
+ { threshold: 0.35 },
+ )
+ observer.observe(root)
+ onCleanup(() => observer.disconnect())
+ })
+
+ const free = 200 * 30
+ const models = [
+ { id: "glm", name: "GLM-5", month: 5750, d: "120ms" },
+ { id: "kimi", name: "Kimi K2.5", month: 9250, d: "240ms" },
+ { id: "minimax", name: "MiniMax M2.5", month: 100000, d: "360ms" },
+ ]
+ const scale = 18
+ 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 plot = w - left - right
+ const x = (r: number) => left + (Math.min(r, scale) / scale) * plot
+ const start = (x(1) / w) * 100
+
+ const yFree = 74
+ const yGo = 134
+ const ticks = [1, 2, 5, 10, 15]
+ const y = (n: number) => `${(n / h) * 100}%`
+
+ return (
+ <figure
+ data-component="limit-graph"
+ aria-label="Requests per month: Free vs Go"
+ data-visible={visible() ? "" : undefined}
+ ref={root}
+ style={{ "--start": `${start}%` } as any}
+ >
+ <div data-slot="plot">
+ <svg viewBox={`0 0 ${w} ${h}`} role="img" aria-hidden="true">
+ <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>
+ {t}x
+ </text>
+ </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>
+
+ <g data-slot="go">
+ <line
+ x1={x(ratio(models[0]!.month))}
+ y1={yGo}
+ x2={x(ratio(models[2]!.month))}
+ y2={yGo}
+ data-range
+ data-animate="line"
+ />
+ <For each={models}>
+ {(m) => (
+ <g style={{ "--d": m.d } as any}>
+ <circle cx={x(ratio(m.month))} cy={yGo} r={5.5} data-point 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}>
+ {props.labels.free}
+ </span>
+ <span data-row-label style={{ "--y": y(yGo) } as any}>
+ {props.labels.go}
+ </span>
+ </div>
+ </div>
+
+ <figcaption>
+ <div data-slot="caption-row">
+ <div data-slot="caption-left">
+ <div data-slot="caption-meta">
+ <span data-slot="caption-label">Requests/month</span>
+ <a data-slot="caption-link" href={props.href}>
+ Usage limits
+ </a>
+ </div>
+ <div data-slot="legend">
+ <span data-item>
+ <i data-dot data-kind="free" />
+ <span data-name>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.month.toLocaleString()}</span>
+ </span>
+ )}
+ </For>
+ </div>
+ </div>
+ </div>
+ </figcaption>
+ </figure>
+ )
+}
+
export default function Home() {
const loggedin = createAsync(() => checkLoggedIn())
const i18n = useI18n()
@@ -144,9 +274,10 @@ export default function Home() {
</section>
<section data-component="comparison">
- <video src={compareVideo} autoplay playsinline loop muted preload="auto" poster={compareVideoPoster}>
- {i18n.t("common.videoUnsupported")}
- </video>
+ <LimitsGraph
+ href={language.route("/docs/go/#usage-limits")}
+ labels={{ free: i18n.t("go.graph.free"), go: i18n.t("go.graph.go") }}
+ />
</section>
<section data-component="problem">