mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-29 21:52:08 +00:00
feat(console): animate Go usage allowances and bonuses (#46055)
This commit is contained in:
parent
df35e842f5
commit
62a2f0e617
7 changed files with 528 additions and 215 deletions
5
bun.lock
5
bun.lock
|
|
@ -178,6 +178,7 @@
|
|||
"@upstash/redis": "1.38.0",
|
||||
"chart.js": "4.5.1",
|
||||
"nitro": "3.0.1-alpha.1",
|
||||
"number-flow": "0.6.2",
|
||||
"solid-js": "catalog:",
|
||||
"solid-list": "0.3.0",
|
||||
"solid-stripe": "0.8.1",
|
||||
|
|
@ -3651,6 +3652,8 @@
|
|||
|
||||
"escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
||||
|
||||
"esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
|
||||
|
||||
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
|
||||
|
||||
"estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="],
|
||||
|
|
@ -4565,6 +4568,8 @@
|
|||
|
||||
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
|
||||
|
||||
"number-flow": ["number-flow@0.6.2", "", { "dependencies": { "esm-env": "^1.1.4" } }, "sha512-MCnImG4Q5vPwhSXnov56nOuyyKn6LC+Qd7II1UiKc+ACRtug5iAtn0+CwXNxM38AC5lSowEY+oYEtZX2qMnUyw=="],
|
||||
|
||||
"nypm": ["nypm@0.6.6", "", { "dependencies": { "citty": "^0.2.2", "pathe": "^2.0.3", "tinyexec": "^1.1.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-vRyr0r4cbBapw07Xw8xrj9Teq3o7MUD35rSaTcanDbW+aK2XHDgJFiU6ZTj2GBw7Q12ysdsyFss+Vdz4hQ0Y6Q=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
"@upstash/redis": "1.38.0",
|
||||
"chart.js": "4.5.1",
|
||||
"nitro": "3.0.1-alpha.1",
|
||||
"number-flow": "0.6.2",
|
||||
"solid-js": "catalog:",
|
||||
"solid-list": "0.3.0",
|
||||
"solid-stripe": "0.8.1",
|
||||
|
|
|
|||
267
packages/console/app/src/component/limits-graph.tsx
Normal file
267
packages/console/app/src/component/limits-graph.tsx
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
import { For, createSignal, onCleanup, onMount } from "solid-js"
|
||||
import { useI18n } from "~/context/i18n"
|
||||
import { RollingNumber } from "./rolling-number"
|
||||
|
||||
export function LimitsGraph(props: { href: string }) {
|
||||
let root!: HTMLElement
|
||||
const [visible, setVisible] = createSignal(false)
|
||||
const [boosted, setBoosted] = createSignal(false)
|
||||
const [promoted, setPromoted] = createSignal<string[]>([])
|
||||
let timer: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
const i18n = useI18n()
|
||||
|
||||
onMount(() => {
|
||||
const motion = window.matchMedia("(prefers-reduced-motion: reduce)")
|
||||
const finish = () => {
|
||||
if (!motion.matches) return
|
||||
clearTimeout(timer)
|
||||
setVisible(true)
|
||||
setBoosted(true)
|
||||
setPromoted(bonuses.map((model) => model.id))
|
||||
}
|
||||
motion.addEventListener("change", finish)
|
||||
onCleanup(() => {
|
||||
clearTimeout(timer)
|
||||
motion.removeEventListener("change", finish)
|
||||
})
|
||||
if (motion.matches) return finish()
|
||||
if (typeof IntersectionObserver === "undefined") return setVisible(true)
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const entry = entries[0]
|
||||
if (!entry?.isIntersecting || entry.intersectionRatio < 0.35) return
|
||||
setVisible(true)
|
||||
observer.disconnect()
|
||||
},
|
||||
{ threshold: 0.35 },
|
||||
)
|
||||
observer.observe(root)
|
||||
onCleanup(() => observer.disconnect())
|
||||
})
|
||||
|
||||
const baseline = 100
|
||||
const graph = [
|
||||
{ id: "kimi-k3", name: "Kimi K3", req: 110 },
|
||||
{ id: "grok-4.6", name: "Grok 4.6", req: 169 },
|
||||
{ id: "hy4-preview", name: "Hy4 preview", req: 1350 },
|
||||
{ id: "gpt-5.6-luna", name: "GPT 5.6 Luna", req: 2050 },
|
||||
{ id: "glm-5.3-flash", name: "GLM-5.3-Flash", req: 3160, baseReq: 1580, bonus: "2x usage" },
|
||||
{ id: "minimax-m3", name: "MiniMax M3", req: 3200 },
|
||||
{ id: "qwen3.7-plus", name: "Qwen3.7 Plus", req: 4300 },
|
||||
{ id: "qwen3.8-flash", name: "Qwen3.8 Flash", req: 5400 },
|
||||
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", req: 7600 },
|
||||
{ id: "longcat-2.0", name: "LongCat-2.0", req: 11400 },
|
||||
{ id: "mimo-v2.5", name: "MiMo-V2.5", req: 30100 },
|
||||
{ id: "hy3", name: "Hy3", req: 34400, baseReq: 4300, bonus: "8x usage" },
|
||||
{ id: "muse-spark-1.2-contributor", name: "Muse Spark 1.2 Contributor", req: 45300, edge: true },
|
||||
].map((model, index) => ({ ...model, d: `${50 + index * 25}ms` }))
|
||||
const bonuses = graph.filter((model) => model.baseReq)
|
||||
|
||||
const w = 1040
|
||||
const chartW = 720
|
||||
const left = 40
|
||||
const right = 60
|
||||
const top = 18
|
||||
const bottom = 44
|
||||
const plot = chartW - left - right
|
||||
const infiniteX = w - 180
|
||||
|
||||
const ratio = (n: number) => n / baseline
|
||||
const rmax = Math.max(1, ...graph.filter((m) => !("infinite" in m)).map((m) => ratio(m.req)))
|
||||
const log = (n: number) => Math.log10(Math.max(n, 1))
|
||||
const base = 24
|
||||
const p = 2.2
|
||||
const x = (r: number) => left + base + Math.pow(log(r) / log(rmax), p) * (plot - base)
|
||||
const ticks = [1, 5, 10, 25, 50, 100, 250].filter((t) => t <= rmax)
|
||||
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 shown = ticks.filter((t) => labels.has(t))
|
||||
const bh = 8
|
||||
const gap = 20
|
||||
const step = bh + gap
|
||||
const gy = (i: number) => top + 22 + step * i
|
||||
const h = gy(graph.length - 1) + bottom
|
||||
const my = graph.length < 2 ? gy(0) : (gy(0) + gy(graph.length - 1)) / 2
|
||||
const px = (n: number) => `${(n / w) * 100}%`
|
||||
const py = (n: number) => `${(n / h) * 100}%`
|
||||
const lx = px(left - 16)
|
||||
const ty = py(h - 18)
|
||||
const timing = () => {
|
||||
const style = getComputedStyle(root)
|
||||
return {
|
||||
duration: Number.parseFloat(style.getPropertyValue("--bonus-duration")),
|
||||
easing: style.getPropertyValue("--spring-easing").trim(),
|
||||
spinEasing: style.getPropertyValue("--digit-easing").trim(),
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<figure
|
||||
data-component="limit-graph"
|
||||
aria-label={i18n.t("go.graph.label")}
|
||||
data-visible={visible() ? "" : undefined}
|
||||
data-boosted={boosted() ? "" : undefined}
|
||||
ref={root}
|
||||
onAnimationStart={(event) => {
|
||||
if (!(event.target instanceof SVGElement) || event.animationName !== "go-graph-reveal") return
|
||||
if (event.target.hasAttribute("data-stage-end") && !boosted()) {
|
||||
const duration = Number.parseFloat(getComputedStyle(root).getPropertyValue("--reveal-duration"))
|
||||
timer = setTimeout(() => setBoosted(true), duration * 0.6)
|
||||
return
|
||||
}
|
||||
if (event.target.dataset.animate !== "bonus") return
|
||||
const model = event.target.dataset.model
|
||||
if (!model) return
|
||||
setPromoted((current) => [...current, model])
|
||||
}}
|
||||
>
|
||||
<div data-slot="plot">
|
||||
<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, i) => (
|
||||
<line x1={x(t)} y1={h - bottom} x2={x(t)} y2={top} data-grid style={{ "--d": `${i() * 100}ms` }} />
|
||||
)}
|
||||
</For>
|
||||
</g>
|
||||
|
||||
<line x1={left} y1={h - bottom} x2={left} y2={top} data-stub />
|
||||
|
||||
<g data-slot="bars">
|
||||
<For each={graph}>
|
||||
{(m, i) => (
|
||||
<>
|
||||
<rect
|
||||
data-animate="bar"
|
||||
data-model={m.id}
|
||||
data-stage-end={i() === graph.length - 1 ? "" : undefined}
|
||||
style={{ "--d": m.d }}
|
||||
x={left}
|
||||
y={gy(i()) - bh / 2}
|
||||
width={Math.max(0, ("infinite" in m ? infiniteX : x(ratio(m.baseReq ?? m.req))) - left)}
|
||||
height={bh}
|
||||
data-bar
|
||||
data-kind={"infinite" in m ? "infinite" : "go"}
|
||||
/>
|
||||
{m.baseReq && (
|
||||
<rect
|
||||
data-animate="bonus"
|
||||
data-model={m.id}
|
||||
style={{ "--bonus-delay": `${bonuses.indexOf(m) * 60}ms` }}
|
||||
x={x(ratio(m.baseReq)) + 2}
|
||||
y={gy(i()) - bh / 2}
|
||||
width={Math.max(0, x(ratio(m.req)) - x(ratio(m.baseReq)) - 2)}
|
||||
height={bh}
|
||||
data-bar
|
||||
data-kind="promo"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</For>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<div data-slot="ylabels" aria-hidden="true">
|
||||
<span data-ylabel style={{ "--x": lx, "--y": py(my) } as any}>
|
||||
{i18n.t("go.graph.go")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div data-slot="xlabels" aria-hidden="true">
|
||||
<For each={shown}>
|
||||
{(t) => (
|
||||
<span
|
||||
data-xlabel
|
||||
data-tick={t}
|
||||
style={{ "--x": px(x(t)), "--y": ty, "--d": `${ticks.indexOf(t) * 100}ms` }}
|
||||
>
|
||||
{i18n.t("go.graph.tick", { n: t })}
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
<div data-slot="pills">
|
||||
<For each={graph}>
|
||||
{(m, i) => (
|
||||
<span
|
||||
data-item
|
||||
data-kind="go"
|
||||
data-model={m.id}
|
||||
data-edge={"edge" in m ? "" : undefined}
|
||||
data-infinite={"infinite" in m ? "" : undefined}
|
||||
data-promo={m.baseReq ? "" : undefined}
|
||||
style={{
|
||||
"--x": px("infinite" in m ? infiniteX : x(ratio(m.baseReq ?? m.req))),
|
||||
"--y": py(gy(i())),
|
||||
"--d": m.d,
|
||||
"--bonus-delay": `${Math.max(0, bonuses.indexOf(m)) * 60}ms`,
|
||||
"--travel": `${"infinite" in m ? 0 : ((x(ratio(m.req)) - x(ratio(m.baseReq ?? m.req))) / w) * 100}cqw`,
|
||||
}}
|
||||
>
|
||||
<span data-label>
|
||||
{!("infinite" in m) && m.baseReq ? (
|
||||
<RollingNumber
|
||||
value={promoted().includes(m.id) ? m.req : m.baseReq}
|
||||
target={m.req}
|
||||
timing={timing}
|
||||
/>
|
||||
) : (
|
||||
<span data-value>{"infinite" in m ? "\u221e" : m.req.toLocaleString()}</span>
|
||||
)}
|
||||
<span data-name>{m.name}</span>
|
||||
{m.id === "muse-spark-1.2-contributor" && (
|
||||
<span data-regions>
|
||||
(
|
||||
<a href="https://ai.developer.meta.com/legal/geographic-use-policy">
|
||||
{i18n.t("go.graph.limitedRegions")}
|
||||
</a>
|
||||
)
|
||||
</span>
|
||||
)}
|
||||
{"infinite" in m && <span data-limited>({i18n.t("go.graph.limitedTime")})</span>}
|
||||
</span>
|
||||
{"bonus" in m && <span data-bonus>{m.bonus}</span>}
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<figcaption>
|
||||
<div data-slot="caption-row">
|
||||
<div data-slot="caption-left">
|
||||
<div data-slot="caption-meta">
|
||||
<span data-slot="caption-label">{i18n.t("go.graph.label")}</span>
|
||||
<a data-slot="caption-link" href={props.href}>
|
||||
{i18n.t("go.graph.usageLimits")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</figcaption>
|
||||
</figure>
|
||||
)
|
||||
}
|
||||
58
packages/console/app/src/component/rolling-number.tsx
Normal file
58
packages/console/app/src/component/rolling-number.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import NumberFlow from "number-flow"
|
||||
import { continuous } from "number-flow/plugins"
|
||||
import { createEffect, onCleanup, onMount } from "solid-js"
|
||||
|
||||
export function RollingNumber(props: {
|
||||
value: number
|
||||
target: number
|
||||
timing: () => { duration: number; easing: string; spinEasing: string }
|
||||
}) {
|
||||
let root!: HTMLSpanElement
|
||||
// Keep the server-rendered text stable while the custom element owns its contents.
|
||||
const initial = props.value.toLocaleString()
|
||||
const growing = props.target.toLocaleString().length > initial.length
|
||||
|
||||
onMount(() => {
|
||||
if (new Intl.NumberFormat().resolvedOptions().numberingSystem !== "latn") {
|
||||
createEffect(() => {
|
||||
root.textContent = props.value.toLocaleString()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const flow = new NumberFlow()
|
||||
flow.format = { useGrouping: true, maximumFractionDigits: 0 }
|
||||
flow.trend = 1
|
||||
if (growing) flow.plugins = [continuous]
|
||||
flow.opacityTiming = { duration: 180, easing: "ease-out" }
|
||||
const motion = window.matchMedia("(prefers-reduced-motion: reduce)")
|
||||
const updateMotion = () => {
|
||||
flow.animated = !motion.matches
|
||||
}
|
||||
updateMotion()
|
||||
motion.addEventListener("change", updateMotion)
|
||||
onCleanup(() => motion.removeEventListener("change", updateMotion))
|
||||
root.replaceChildren(flow)
|
||||
createEffect(() => {
|
||||
const value = props.value
|
||||
if (flow.value === value) return
|
||||
const timing = props.timing()
|
||||
flow.transformTiming = { duration: timing.duration, easing: timing.easing }
|
||||
flow.spinTiming = {
|
||||
duration: timing.duration,
|
||||
easing: growing ? "cubic-bezier(0.45, 0, 0.55, 1)" : timing.spinEasing,
|
||||
}
|
||||
flow.update(value)
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<span
|
||||
data-value
|
||||
ref={root}
|
||||
style={{ "min-width": `${props.target.toLocaleString().length}ch`, "text-align": "right" }}
|
||||
>
|
||||
{initial}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
|
@ -267,7 +267,7 @@ export const dict = {
|
|||
"go.graph.free": "Free",
|
||||
"go.graph.freePill": "Big Pickle and free models",
|
||||
"go.graph.go": "Go",
|
||||
"go.graph.label": "Requests per 5 hour",
|
||||
"go.graph.label": "Requests / 5 hours",
|
||||
"go.graph.limitedRegions": "limited regions",
|
||||
"go.graph.limitedTime": "limited time",
|
||||
"go.graph.tick": "{{n}}x",
|
||||
|
|
|
|||
|
|
@ -21,10 +21,40 @@
|
|||
}
|
||||
}
|
||||
|
||||
@keyframes go-graph-bar {
|
||||
@keyframes go-graph-reveal {
|
||||
to {
|
||||
clip-path: inset(0 0 0 0) fill-box;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes go-graph-label {
|
||||
to {
|
||||
mask-position: 0 0;
|
||||
opacity: 1;
|
||||
transform: scaleX(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes go-graph-grid {
|
||||
to {
|
||||
mask-position: 0 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes go-graph-heat {
|
||||
from {
|
||||
filter: brightness(var(--arrival-brightness));
|
||||
}
|
||||
to {
|
||||
filter: brightness(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes go-graph-bonus-heat {
|
||||
from {
|
||||
color: var(--color-go-3);
|
||||
}
|
||||
to {
|
||||
color: var(--color-text-weak);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -477,6 +507,48 @@ body {
|
|||
}
|
||||
|
||||
[data-component="limit-graph"] {
|
||||
--reveal-duration: 950ms;
|
||||
--bonus-duration: 1500ms;
|
||||
--digit-easing: cubic-bezier(0.22, 1, 0.36, 1);
|
||||
--grid-duration: 2000ms;
|
||||
--grid-easing: cubic-bezier(0.22, 1, 0.36, 1);
|
||||
--arrival-brightness: 1.15;
|
||||
--heat-duration: 1600ms;
|
||||
--bar-delay: 240ms;
|
||||
/* Critically damped response, shared by the bars, labels, and rolling digits. */
|
||||
--spring-easing: linear(
|
||||
0,
|
||||
0.02991,
|
||||
0.10078,
|
||||
0.19179,
|
||||
0.28962,
|
||||
0.38611,
|
||||
0.47651,
|
||||
0.55839,
|
||||
0.63079,
|
||||
0.69365,
|
||||
0.74748,
|
||||
0.79306,
|
||||
0.83131,
|
||||
0.86315,
|
||||
0.8895,
|
||||
0.91117,
|
||||
0.92892,
|
||||
0.94339,
|
||||
0.95515,
|
||||
0.96467,
|
||||
0.97236,
|
||||
0.97855,
|
||||
0.98352,
|
||||
0.98751,
|
||||
0.9907,
|
||||
0.99324,
|
||||
0.99527,
|
||||
0.99689,
|
||||
0.99817,
|
||||
0.99919,
|
||||
1
|
||||
);
|
||||
margin: 0 auto;
|
||||
width: calc(100% - 120px);
|
||||
max-width: calc(100% - 120px);
|
||||
|
|
@ -491,6 +563,7 @@ body {
|
|||
}
|
||||
|
||||
[data-slot="plot"] {
|
||||
container-type: inline-size;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
width: 100%;
|
||||
|
|
@ -515,6 +588,7 @@ body {
|
|||
}
|
||||
|
||||
[data-slot="xlabels"] [data-xlabel] {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
left: var(--x);
|
||||
top: var(--y);
|
||||
|
|
@ -551,11 +625,26 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
[data-slot="xlabels"] [data-xlabel],
|
||||
[data-slot="pills"] [data-label],
|
||||
[data-bonus] {
|
||||
mask-image: linear-gradient(to right, #000 40%, transparent 60%);
|
||||
mask-size: 250% 100%;
|
||||
mask-position: 100% 0;
|
||||
mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
[data-slot="pills"] {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
|
||||
[data-label] {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
[data-item] {
|
||||
position: absolute;
|
||||
left: var(--x);
|
||||
|
|
@ -573,7 +662,6 @@ body {
|
|||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
box-sizing: border-box;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 60rem) {
|
||||
|
|
@ -605,10 +693,16 @@ body {
|
|||
[data-value] {
|
||||
color: var(--color-text-strong);
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
|
||||
number-flow {
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
[data-bonus] {
|
||||
opacity: 0;
|
||||
color: var(--color-text-weak);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
|
|
@ -685,11 +779,22 @@ body {
|
|||
opacity: 0.55;
|
||||
}
|
||||
|
||||
[data-grid],
|
||||
[data-stub] {
|
||||
mask-image: linear-gradient(to top, #000 48%, transparent 52%);
|
||||
mask-size: 100% 250%;
|
||||
mask-position: 0 0;
|
||||
mask-repeat: no-repeat;
|
||||
mask-origin: stroke-box;
|
||||
mask-clip: stroke-box;
|
||||
}
|
||||
|
||||
[data-animate="bar"],
|
||||
[data-animate="bonus"] {
|
||||
clip-path: inset(0 100% 0 0) fill-box;
|
||||
}
|
||||
|
||||
[data-bar] {
|
||||
transform-box: fill-box;
|
||||
transform-origin: left center;
|
||||
opacity: 0;
|
||||
transform: scaleX(0.02);
|
||||
fill: var(--bar-go);
|
||||
stroke: none;
|
||||
}
|
||||
|
|
@ -760,18 +865,74 @@ 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;
|
||||
&[data-visible] [data-grid],
|
||||
&[data-visible] [data-stub] {
|
||||
animation:
|
||||
go-graph-grid var(--grid-duration) var(--grid-easing) forwards,
|
||||
go-graph-heat var(--heat-duration) linear backwards;
|
||||
animation-delay: var(--d, 0ms), calc(var(--d, 0ms) + var(--grid-duration));
|
||||
}
|
||||
|
||||
&[data-visible] [data-slot="xlabels"] [data-xlabel] {
|
||||
animation: go-graph-label 300ms ease-out forwards;
|
||||
animation-delay: var(--d, 0ms);
|
||||
}
|
||||
|
||||
&[data-visible] [data-slot="pills"] [data-item] {
|
||||
opacity: 1;
|
||||
transition: opacity 240ms ease;
|
||||
transition-delay: var(--d, 0ms);
|
||||
&[data-visible] [data-animate="bar"] {
|
||||
animation:
|
||||
go-graph-reveal var(--reveal-duration) var(--spring-easing) forwards,
|
||||
go-graph-heat var(--heat-duration) linear backwards;
|
||||
animation-delay:
|
||||
calc(var(--bar-delay) + var(--d, 0ms)), calc(var(--bar-delay) + var(--d, 0ms) + var(--reveal-duration));
|
||||
}
|
||||
|
||||
&[data-visible] [data-slot="pills"] [data-label] {
|
||||
animation: go-graph-label 400ms ease-out forwards;
|
||||
animation-delay: calc(var(--bar-delay) + var(--d, 0ms) + var(--reveal-duration) * 0.4);
|
||||
}
|
||||
|
||||
&[data-boosted] [data-animate="bonus"] {
|
||||
animation:
|
||||
go-graph-reveal var(--bonus-duration) var(--spring-easing) forwards,
|
||||
go-graph-heat var(--heat-duration) linear backwards;
|
||||
animation-delay: var(--bonus-delay), calc(var(--bonus-delay) + var(--bonus-duration));
|
||||
}
|
||||
|
||||
&[data-boosted] [data-slot="pills"] [data-item][data-promo] {
|
||||
translate: var(--travel) 0;
|
||||
transition: translate var(--bonus-duration) var(--spring-easing) var(--bonus-delay);
|
||||
|
||||
@media (max-width: 60rem) {
|
||||
&[data-edge] {
|
||||
translate: none;
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-boosted] [data-bonus] {
|
||||
animation:
|
||||
go-graph-label 400ms ease-out forwards,
|
||||
go-graph-bonus-heat 3000ms linear backwards;
|
||||
animation-delay: var(--bonus-delay);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
[data-grid],
|
||||
[data-stub],
|
||||
&[data-visible] [data-grid],
|
||||
&[data-visible] [data-stub] {
|
||||
mask-image: none;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
[data-slot="xlabels"] [data-xlabel],
|
||||
&[data-visible] [data-slot="xlabels"] [data-xlabel] {
|
||||
opacity: 1;
|
||||
mask-image: none;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
[data-animate="line"] {
|
||||
stroke-dashoffset: 0;
|
||||
animation: none;
|
||||
|
|
@ -781,9 +942,12 @@ body {
|
|||
transform: none;
|
||||
animation: none;
|
||||
}
|
||||
[data-bar] {
|
||||
[data-animate="bar"],
|
||||
&[data-visible] [data-animate="bar"],
|
||||
[data-animate="bonus"],
|
||||
&[data-boosted] [data-animate="bonus"] {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
clip-path: none;
|
||||
animation: none;
|
||||
}
|
||||
[data-row],
|
||||
|
|
@ -792,8 +956,16 @@ body {
|
|||
transition: none;
|
||||
}
|
||||
|
||||
[data-slot="pills"] [data-item] {
|
||||
[data-slot="pills"] [data-label],
|
||||
&[data-visible] [data-slot="pills"] [data-label],
|
||||
[data-bonus],
|
||||
&[data-boosted] [data-bonus] {
|
||||
opacity: 1;
|
||||
mask-image: none;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
&[data-boosted] [data-slot="pills"] [data-item][data-promo] {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
|
@ -886,6 +1058,12 @@ body {
|
|||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
|
||||
[data-label] {
|
||||
max-width: 100%;
|
||||
flex-wrap: wrap;
|
||||
gap: 3px 8px;
|
||||
}
|
||||
|
||||
&[data-model="muse-spark-1.2-contributor"] {
|
||||
transform: translateY(11px);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import "./index.css"
|
||||
import { createAsync, query } from "@solidjs/router"
|
||||
import { Title, Meta } from "@solidjs/meta"
|
||||
import { For, createMemo, createSignal, onCleanup, onMount } from "solid-js"
|
||||
import { For, createMemo } from "solid-js"
|
||||
//import { HttpHeader } from "@solidjs/start"
|
||||
import goLogoLight from "../../asset/go-ornate-light.svg"
|
||||
import goLogoDark from "../../asset/go-ornate-dark.svg"
|
||||
|
|
@ -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 { LimitsGraph } from "~/component/limits-graph"
|
||||
import { config } from "~/config"
|
||||
import { getLastSeenWorkspaceID } from "../workspace/common"
|
||||
import { IconMiniMax, IconMiMo, IconZai, IconAlibaba, IconDeepSeek } from "~/component/icon"
|
||||
|
|
@ -49,203 +50,6 @@ const models = [
|
|||
{ name: "Hy3", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" },
|
||||
] as const
|
||||
|
||||
function LimitsGraph(props: { href: string }) {
|
||||
let root!: HTMLElement
|
||||
const [visible, setVisible] = createSignal(false)
|
||||
|
||||
const i18n = useI18n()
|
||||
|
||||
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 baseline = 100
|
||||
const graph = [
|
||||
{ id: "kimi-k3", name: "Kimi K3", req: 110, d: "50ms" },
|
||||
{ id: "grok-4.6", name: "Grok 4.6", req: 169, d: "75ms" },
|
||||
{ id: "hy4-preview", name: "Hy4 preview", req: 1350, d: "90ms" },
|
||||
{ id: "gpt-5.6-luna", name: "GPT 5.6 Luna", req: 2050, d: "290ms" },
|
||||
{ id: "glm-5.3-flash", name: "GLM-5.3-Flash", req: 3160, baseReq: 1580, bonus: "2x usage", d: "100ms" },
|
||||
{ id: "minimax-m3", name: "MiniMax M3", req: 3200, d: "210ms" },
|
||||
{ id: "qwen3.7-plus", name: "Qwen3.7 Plus", req: 4300, d: "300ms" },
|
||||
{ id: "qwen3.8-flash", name: "Qwen3.8 Flash", req: 5400, d: "315ms" },
|
||||
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", req: 7600, d: "330ms" },
|
||||
{ id: "longcat-2.0", name: "LongCat-2.0", req: 11400, d: "335ms" },
|
||||
{ id: "mimo-v2.5", name: "MiMo-V2.5", req: 30100, d: "340ms" },
|
||||
{ id: "hy3", name: "Hy3", req: 34400, baseReq: 4300, bonus: "8x usage", d: "320ms" },
|
||||
{ id: "muse-spark-1.2-contributor", name: "Muse Spark 1.2 Contributor", req: 45300, edge: true, d: "360ms" },
|
||||
]
|
||||
|
||||
const w = 1040
|
||||
const chartW = 720
|
||||
const left = 40
|
||||
const right = 60
|
||||
const top = 18
|
||||
const bottom = 44
|
||||
const plot = chartW - left - right
|
||||
const infiniteX = w - 180
|
||||
|
||||
const ratio = (n: number) => n / baseline
|
||||
const rmax = Math.max(1, ...graph.filter((m) => !("infinite" in m)).map((m) => ratio(m.req)))
|
||||
const log = (n: number) => Math.log10(Math.max(n, 1))
|
||||
const base = 24
|
||||
const p = 2.2
|
||||
const x = (r: number) => left + base + Math.pow(log(r) / log(rmax), p) * (plot - base)
|
||||
const ticks = [1, 5, 10, 25, 50, 100, 250].filter((t) => t <= rmax)
|
||||
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 shown = ticks.filter((t) => labels.has(t))
|
||||
const bh = 8
|
||||
const gap = 20
|
||||
const step = bh + gap
|
||||
const gy = (i: number) => top + 22 + step * i
|
||||
const h = gy(graph.length - 1) + bottom
|
||||
const my = graph.length < 2 ? gy(0) : (gy(0) + gy(graph.length - 1)) / 2
|
||||
const px = (n: number) => `${(n / w) * 100}%`
|
||||
const py = (n: number) => `${(n / h) * 100}%`
|
||||
const lx = px(left - 16)
|
||||
const ty = py(h - 18)
|
||||
|
||||
return (
|
||||
<figure
|
||||
data-component="limit-graph"
|
||||
aria-label={i18n.t("go.graph.label")}
|
||||
data-visible={visible() ? "" : undefined}
|
||||
ref={root}
|
||||
>
|
||||
<div data-slot="plot">
|
||||
<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) => <line x1={x(t)} y1={top} x2={x(t)} y2={h - bottom} data-grid />}</For>
|
||||
</g>
|
||||
|
||||
<line x1={left} y1={top} x2={left} y2={h - bottom} data-stub />
|
||||
|
||||
<g data-slot="bars">
|
||||
<For each={graph}>
|
||||
{(m, i) => (
|
||||
<g data-model={m.id} style={{ "--d": m.d } as any}>
|
||||
<rect
|
||||
x={left}
|
||||
y={gy(i()) - bh / 2}
|
||||
width={Math.max(0, ("infinite" in m ? infiniteX : x(ratio(m.baseReq ?? m.req))) - left)}
|
||||
height={bh}
|
||||
data-bar
|
||||
data-kind={"infinite" in m ? "infinite" : "go"}
|
||||
data-model={m.id}
|
||||
/>
|
||||
{m.baseReq && (
|
||||
<rect
|
||||
x={x(ratio(m.baseReq)) + 2}
|
||||
y={gy(i()) - bh / 2}
|
||||
width={Math.max(0, x(ratio(m.req)) - x(ratio(m.baseReq)) - 2)}
|
||||
height={bh}
|
||||
data-bar
|
||||
data-kind="promo"
|
||||
data-model={m.id}
|
||||
/>
|
||||
)}
|
||||
</g>
|
||||
)}
|
||||
</For>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<div data-slot="ylabels" aria-hidden="true">
|
||||
<span data-ylabel style={{ "--x": lx, "--y": py(my) } as any}>
|
||||
{i18n.t("go.graph.go")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div data-slot="xlabels" aria-hidden="true">
|
||||
<For each={shown}>
|
||||
{(t) => (
|
||||
<span data-xlabel data-tick={t} style={{ "--x": px(x(t)), "--y": ty } as any}>
|
||||
{i18n.t("go.graph.tick", { n: t })}
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
<div data-slot="pills">
|
||||
<For each={graph}>
|
||||
{(m, i) => (
|
||||
<span
|
||||
data-item
|
||||
data-kind="go"
|
||||
data-model={m.id}
|
||||
data-edge={"edge" in m ? "" : undefined}
|
||||
data-infinite={"infinite" in m ? "" : undefined}
|
||||
style={
|
||||
{ "--x": px("infinite" in m ? infiniteX : x(ratio(m.req))), "--y": py(gy(i())), "--d": m.d } as any
|
||||
}
|
||||
>
|
||||
<span data-value>{"infinite" in m ? "∞" : m.req.toLocaleString()}</span>
|
||||
<span data-name>{m.name}</span>
|
||||
{m.id === "muse-spark-1.2-contributor" && (
|
||||
<span data-regions>
|
||||
(
|
||||
<a href="https://ai.developer.meta.com/legal/geographic-use-policy">
|
||||
{i18n.t("go.graph.limitedRegions")}
|
||||
</a>
|
||||
)
|
||||
</span>
|
||||
)}
|
||||
{"infinite" in m && <span data-limited>({i18n.t("go.graph.limitedTime")})</span>}
|
||||
{"bonus" in m && <span data-bonus>{m.bonus}</span>}
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<figcaption>
|
||||
<div data-slot="caption-row">
|
||||
<div data-slot="caption-left">
|
||||
<div data-slot="caption-meta">
|
||||
<span data-slot="caption-label">{i18n.t("go.graph.label")}</span>
|
||||
<a data-slot="caption-link" href={props.href}>
|
||||
{i18n.t("go.graph.usageLimits")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</figcaption>
|
||||
</figure>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const workspaceID = createAsync(() => checkLoggedIn())
|
||||
const subscribeUrl = createMemo(() => (workspaceID() ? `/workspace/${workspaceID()}/go` : "/auth"))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue