Skip to content

Commit 62a2f0e

Browse files
authored
feat(console): animate Go usage allowances and bonuses (anomalyco#46055)
1 parent df35e84 commit 62a2f0e

7 files changed

Lines changed: 528 additions & 215 deletions

File tree

bun.lock

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/console/app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"@upstash/redis": "1.38.0",
3030
"chart.js": "4.5.1",
3131
"nitro": "3.0.1-alpha.1",
32+
"number-flow": "0.6.2",
3233
"solid-js": "catalog:",
3334
"solid-list": "0.3.0",
3435
"solid-stripe": "0.8.1",
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
import { For, createSignal, onCleanup, onMount } from "solid-js"
2+
import { useI18n } from "~/context/i18n"
3+
import { RollingNumber } from "./rolling-number"
4+
5+
export function LimitsGraph(props: { href: string }) {
6+
let root!: HTMLElement
7+
const [visible, setVisible] = createSignal(false)
8+
const [boosted, setBoosted] = createSignal(false)
9+
const [promoted, setPromoted] = createSignal<string[]>([])
10+
let timer: ReturnType<typeof setTimeout> | undefined
11+
12+
const i18n = useI18n()
13+
14+
onMount(() => {
15+
const motion = window.matchMedia("(prefers-reduced-motion: reduce)")
16+
const finish = () => {
17+
if (!motion.matches) return
18+
clearTimeout(timer)
19+
setVisible(true)
20+
setBoosted(true)
21+
setPromoted(bonuses.map((model) => model.id))
22+
}
23+
motion.addEventListener("change", finish)
24+
onCleanup(() => {
25+
clearTimeout(timer)
26+
motion.removeEventListener("change", finish)
27+
})
28+
if (motion.matches) return finish()
29+
if (typeof IntersectionObserver === "undefined") return setVisible(true)
30+
const observer = new IntersectionObserver(
31+
(entries) => {
32+
const entry = entries[0]
33+
if (!entry?.isIntersecting || entry.intersectionRatio < 0.35) return
34+
setVisible(true)
35+
observer.disconnect()
36+
},
37+
{ threshold: 0.35 },
38+
)
39+
observer.observe(root)
40+
onCleanup(() => observer.disconnect())
41+
})
42+
43+
const baseline = 100
44+
const graph = [
45+
{ id: "kimi-k3", name: "Kimi K3", req: 110 },
46+
{ id: "grok-4.6", name: "Grok 4.6", req: 169 },
47+
{ id: "hy4-preview", name: "Hy4 preview", req: 1350 },
48+
{ id: "gpt-5.6-luna", name: "GPT 5.6 Luna", req: 2050 },
49+
{ id: "glm-5.3-flash", name: "GLM-5.3-Flash", req: 3160, baseReq: 1580, bonus: "2x usage" },
50+
{ id: "minimax-m3", name: "MiniMax M3", req: 3200 },
51+
{ id: "qwen3.7-plus", name: "Qwen3.7 Plus", req: 4300 },
52+
{ id: "qwen3.8-flash", name: "Qwen3.8 Flash", req: 5400 },
53+
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", req: 7600 },
54+
{ id: "longcat-2.0", name: "LongCat-2.0", req: 11400 },
55+
{ id: "mimo-v2.5", name: "MiMo-V2.5", req: 30100 },
56+
{ id: "hy3", name: "Hy3", req: 34400, baseReq: 4300, bonus: "8x usage" },
57+
{ id: "muse-spark-1.2-contributor", name: "Muse Spark 1.2 Contributor", req: 45300, edge: true },
58+
].map((model, index) => ({ ...model, d: `${50 + index * 25}ms` }))
59+
const bonuses = graph.filter((model) => model.baseReq)
60+
61+
const w = 1040
62+
const chartW = 720
63+
const left = 40
64+
const right = 60
65+
const top = 18
66+
const bottom = 44
67+
const plot = chartW - left - right
68+
const infiniteX = w - 180
69+
70+
const ratio = (n: number) => n / baseline
71+
const rmax = Math.max(1, ...graph.filter((m) => !("infinite" in m)).map((m) => ratio(m.req)))
72+
const log = (n: number) => Math.log10(Math.max(n, 1))
73+
const base = 24
74+
const p = 2.2
75+
const x = (r: number) => left + base + Math.pow(log(r) / log(rmax), p) * (plot - base)
76+
const ticks = [1, 5, 10, 25, 50, 100, 250].filter((t) => t <= rmax)
77+
const labels = (() => {
78+
const set = new Set<number>()
79+
let last = -Infinity
80+
for (const t of ticks) {
81+
if (t === 1) {
82+
set.add(t)
83+
last = x(t)
84+
continue
85+
}
86+
const pos = x(t)
87+
if (pos - last < 44) continue
88+
set.add(t)
89+
last = pos
90+
}
91+
return set
92+
})()
93+
const shown = ticks.filter((t) => labels.has(t))
94+
const bh = 8
95+
const gap = 20
96+
const step = bh + gap
97+
const gy = (i: number) => top + 22 + step * i
98+
const h = gy(graph.length - 1) + bottom
99+
const my = graph.length < 2 ? gy(0) : (gy(0) + gy(graph.length - 1)) / 2
100+
const px = (n: number) => `${(n / w) * 100}%`
101+
const py = (n: number) => `${(n / h) * 100}%`
102+
const lx = px(left - 16)
103+
const ty = py(h - 18)
104+
const timing = () => {
105+
const style = getComputedStyle(root)
106+
return {
107+
duration: Number.parseFloat(style.getPropertyValue("--bonus-duration")),
108+
easing: style.getPropertyValue("--spring-easing").trim(),
109+
spinEasing: style.getPropertyValue("--digit-easing").trim(),
110+
}
111+
}
112+
113+
return (
114+
<figure
115+
data-component="limit-graph"
116+
aria-label={i18n.t("go.graph.label")}
117+
data-visible={visible() ? "" : undefined}
118+
data-boosted={boosted() ? "" : undefined}
119+
ref={root}
120+
onAnimationStart={(event) => {
121+
if (!(event.target instanceof SVGElement) || event.animationName !== "go-graph-reveal") return
122+
if (event.target.hasAttribute("data-stage-end") && !boosted()) {
123+
const duration = Number.parseFloat(getComputedStyle(root).getPropertyValue("--reveal-duration"))
124+
timer = setTimeout(() => setBoosted(true), duration * 0.6)
125+
return
126+
}
127+
if (event.target.dataset.animate !== "bonus") return
128+
const model = event.target.dataset.model
129+
if (!model) return
130+
setPromoted((current) => [...current, model])
131+
}}
132+
>
133+
<div data-slot="plot">
134+
<svg
135+
viewBox={`0 0 ${w} ${h}`}
136+
preserveAspectRatio="none"
137+
role="img"
138+
aria-hidden="true"
139+
style={{ height: `${h}px` }}
140+
>
141+
<g data-slot="grid">
142+
<For each={ticks}>
143+
{(t, i) => (
144+
<line x1={x(t)} y1={h - bottom} x2={x(t)} y2={top} data-grid style={{ "--d": `${i() * 100}ms` }} />
145+
)}
146+
</For>
147+
</g>
148+
149+
<line x1={left} y1={h - bottom} x2={left} y2={top} data-stub />
150+
151+
<g data-slot="bars">
152+
<For each={graph}>
153+
{(m, i) => (
154+
<>
155+
<rect
156+
data-animate="bar"
157+
data-model={m.id}
158+
data-stage-end={i() === graph.length - 1 ? "" : undefined}
159+
style={{ "--d": m.d }}
160+
x={left}
161+
y={gy(i()) - bh / 2}
162+
width={Math.max(0, ("infinite" in m ? infiniteX : x(ratio(m.baseReq ?? m.req))) - left)}
163+
height={bh}
164+
data-bar
165+
data-kind={"infinite" in m ? "infinite" : "go"}
166+
/>
167+
{m.baseReq && (
168+
<rect
169+
data-animate="bonus"
170+
data-model={m.id}
171+
style={{ "--bonus-delay": `${bonuses.indexOf(m) * 60}ms` }}
172+
x={x(ratio(m.baseReq)) + 2}
173+
y={gy(i()) - bh / 2}
174+
width={Math.max(0, x(ratio(m.req)) - x(ratio(m.baseReq)) - 2)}
175+
height={bh}
176+
data-bar
177+
data-kind="promo"
178+
/>
179+
)}
180+
</>
181+
)}
182+
</For>
183+
</g>
184+
</svg>
185+
186+
<div data-slot="ylabels" aria-hidden="true">
187+
<span data-ylabel style={{ "--x": lx, "--y": py(my) } as any}>
188+
{i18n.t("go.graph.go")}
189+
</span>
190+
</div>
191+
192+
<div data-slot="xlabels" aria-hidden="true">
193+
<For each={shown}>
194+
{(t) => (
195+
<span
196+
data-xlabel
197+
data-tick={t}
198+
style={{ "--x": px(x(t)), "--y": ty, "--d": `${ticks.indexOf(t) * 100}ms` }}
199+
>
200+
{i18n.t("go.graph.tick", { n: t })}
201+
</span>
202+
)}
203+
</For>
204+
</div>
205+
206+
<div data-slot="pills">
207+
<For each={graph}>
208+
{(m, i) => (
209+
<span
210+
data-item
211+
data-kind="go"
212+
data-model={m.id}
213+
data-edge={"edge" in m ? "" : undefined}
214+
data-infinite={"infinite" in m ? "" : undefined}
215+
data-promo={m.baseReq ? "" : undefined}
216+
style={{
217+
"--x": px("infinite" in m ? infiniteX : x(ratio(m.baseReq ?? m.req))),
218+
"--y": py(gy(i())),
219+
"--d": m.d,
220+
"--bonus-delay": `${Math.max(0, bonuses.indexOf(m)) * 60}ms`,
221+
"--travel": `${"infinite" in m ? 0 : ((x(ratio(m.req)) - x(ratio(m.baseReq ?? m.req))) / w) * 100}cqw`,
222+
}}
223+
>
224+
<span data-label>
225+
{!("infinite" in m) && m.baseReq ? (
226+
<RollingNumber
227+
value={promoted().includes(m.id) ? m.req : m.baseReq}
228+
target={m.req}
229+
timing={timing}
230+
/>
231+
) : (
232+
<span data-value>{"infinite" in m ? "\u221e" : m.req.toLocaleString()}</span>
233+
)}
234+
<span data-name>{m.name}</span>
235+
{m.id === "muse-spark-1.2-contributor" && (
236+
<span data-regions>
237+
(
238+
<a href="https://ai.developer.meta.com/legal/geographic-use-policy">
239+
{i18n.t("go.graph.limitedRegions")}
240+
</a>
241+
)
242+
</span>
243+
)}
244+
{"infinite" in m && <span data-limited>({i18n.t("go.graph.limitedTime")})</span>}
245+
</span>
246+
{"bonus" in m && <span data-bonus>{m.bonus}</span>}
247+
</span>
248+
)}
249+
</For>
250+
</div>
251+
</div>
252+
253+
<figcaption>
254+
<div data-slot="caption-row">
255+
<div data-slot="caption-left">
256+
<div data-slot="caption-meta">
257+
<span data-slot="caption-label">{i18n.t("go.graph.label")}</span>
258+
<a data-slot="caption-link" href={props.href}>
259+
{i18n.t("go.graph.usageLimits")}
260+
</a>
261+
</div>
262+
</div>
263+
</div>
264+
</figcaption>
265+
</figure>
266+
)
267+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import NumberFlow from "number-flow"
2+
import { continuous } from "number-flow/plugins"
3+
import { createEffect, onCleanup, onMount } from "solid-js"
4+
5+
export function RollingNumber(props: {
6+
value: number
7+
target: number
8+
timing: () => { duration: number; easing: string; spinEasing: string }
9+
}) {
10+
let root!: HTMLSpanElement
11+
// Keep the server-rendered text stable while the custom element owns its contents.
12+
const initial = props.value.toLocaleString()
13+
const growing = props.target.toLocaleString().length > initial.length
14+
15+
onMount(() => {
16+
if (new Intl.NumberFormat().resolvedOptions().numberingSystem !== "latn") {
17+
createEffect(() => {
18+
root.textContent = props.value.toLocaleString()
19+
})
20+
return
21+
}
22+
23+
const flow = new NumberFlow()
24+
flow.format = { useGrouping: true, maximumFractionDigits: 0 }
25+
flow.trend = 1
26+
if (growing) flow.plugins = [continuous]
27+
flow.opacityTiming = { duration: 180, easing: "ease-out" }
28+
const motion = window.matchMedia("(prefers-reduced-motion: reduce)")
29+
const updateMotion = () => {
30+
flow.animated = !motion.matches
31+
}
32+
updateMotion()
33+
motion.addEventListener("change", updateMotion)
34+
onCleanup(() => motion.removeEventListener("change", updateMotion))
35+
root.replaceChildren(flow)
36+
createEffect(() => {
37+
const value = props.value
38+
if (flow.value === value) return
39+
const timing = props.timing()
40+
flow.transformTiming = { duration: timing.duration, easing: timing.easing }
41+
flow.spinTiming = {
42+
duration: timing.duration,
43+
easing: growing ? "cubic-bezier(0.45, 0, 0.55, 1)" : timing.spinEasing,
44+
}
45+
flow.update(value)
46+
})
47+
})
48+
49+
return (
50+
<span
51+
data-value
52+
ref={root}
53+
style={{ "min-width": `${props.target.toLocaleString().length}ch`, "text-align": "right" }}
54+
>
55+
{initial}
56+
</span>
57+
)
58+
}

packages/console/app/src/i18n/en.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ export const dict = {
267267
"go.graph.free": "Free",
268268
"go.graph.freePill": "Big Pickle and free models",
269269
"go.graph.go": "Go",
270-
"go.graph.label": "Requests per 5 hour",
270+
"go.graph.label": "Requests / 5 hours",
271271
"go.graph.limitedRegions": "limited regions",
272272
"go.graph.limitedTime": "limited time",
273273
"go.graph.tick": "{{n}}x",

0 commit comments

Comments
 (0)