mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-10 00:28:29 +00:00
feat(stats): add model comparison pages
This commit is contained in:
parent
77429f5982
commit
5a533e366f
10 changed files with 1373 additions and 5 deletions
|
|
@ -33,6 +33,12 @@ import {
|
|||
import { SectionHeading } from "../section-heading"
|
||||
import { runStatsEffect } from "../../stats-runtime"
|
||||
import { setStatsPageCacheHeaders } from "../stats-cache"
|
||||
import {
|
||||
ComparisonCardsSection,
|
||||
modelRefFromCatalog,
|
||||
uniqueComparisonPairs,
|
||||
type ComparisonModelRef,
|
||||
} from "../compare-cards"
|
||||
import {
|
||||
applyThemePreference,
|
||||
Footer,
|
||||
|
|
@ -189,6 +195,11 @@ export default function StatsModel() {
|
|||
<ModelEfficiencySection data={stats() ?? null} catalog={catalogEntry() ?? null} />
|
||||
<ModelGeoBreakdownSection data={stats()?.country ?? emptyCountryRecord()} />
|
||||
<ModelPeersSection data={stats() ?? null} />
|
||||
<ComparisonCardsSection
|
||||
pairs={modelComparisonPairs(catalog(), catalogEntry() ?? null, stats() ?? null)}
|
||||
title="Compare This Model"
|
||||
description="Other models to compare with this one."
|
||||
/>
|
||||
</>
|
||||
</Show>
|
||||
</Show>
|
||||
|
|
@ -197,6 +208,7 @@ export default function StatsModel() {
|
|||
themePreference={themePreference()}
|
||||
onThemePreferenceChange={updateThemePreference}
|
||||
links={modelFooterLinks()}
|
||||
bridge={{ href: "#model-comparison", label: "MODEL COMPARISONS" }}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
|
|
@ -1172,6 +1184,53 @@ function ModelEmptyState(props: { title: string; description: string; compact?:
|
|||
)
|
||||
}
|
||||
|
||||
function modelComparisonPairs(
|
||||
catalog: ModelCatalog | undefined,
|
||||
catalogEntry: ModelCatalogEntry | null,
|
||||
data: StatsModelData | null,
|
||||
) {
|
||||
const current = modelComparisonRef(catalogEntry, data)
|
||||
if (!current) return []
|
||||
const peerPairs = (data?.peers ?? [])
|
||||
.filter((peer) => peer.model !== data?.model)
|
||||
.slice(0, 3)
|
||||
.map((peer) => ({
|
||||
first: current,
|
||||
second: {
|
||||
name: peer.model,
|
||||
lab: peer.provider,
|
||||
slug: peer.slug,
|
||||
labName: peer.author,
|
||||
metric: `#${peer.rank} / ${formatTokens(peer.tokens)}`,
|
||||
},
|
||||
detail: "Usage peer",
|
||||
}))
|
||||
const catalogPairs = (catalogEntry && catalog ? catalog.labs.find((lab) => lab.id === catalogEntry.lab)?.models ?? [] : [])
|
||||
.filter((model) => model.id !== catalogEntry?.id)
|
||||
.slice(0, 3)
|
||||
.map((model) => ({
|
||||
first: current,
|
||||
second: modelRefFromCatalog(model),
|
||||
detail: "Same lab pair",
|
||||
}))
|
||||
return uniqueComparisonPairs([...peerPairs, ...catalogPairs])
|
||||
}
|
||||
|
||||
function modelComparisonRef(
|
||||
catalogEntry: ModelCatalogEntry | null,
|
||||
data: StatsModelData | null,
|
||||
): ComparisonModelRef | undefined {
|
||||
if (catalogEntry) return modelRefFromCatalog(catalogEntry)
|
||||
if (!data) return undefined
|
||||
return {
|
||||
name: data.model,
|
||||
lab: data.provider,
|
||||
slug: data.slug,
|
||||
labName: data.author,
|
||||
metric: `#${data.rank}`,
|
||||
}
|
||||
}
|
||||
|
||||
function getProviderIconId(author: string) {
|
||||
if (author === "MiniMax") return "minimax"
|
||||
if (author === "Moonshot") return "moonshotai"
|
||||
|
|
|
|||
|
|
@ -28,6 +28,11 @@ import {
|
|||
import { SectionHeading } from "../section-heading"
|
||||
import { runStatsEffect } from "../../stats-runtime"
|
||||
import { setStatsPageCacheHeaders } from "../stats-cache"
|
||||
import {
|
||||
ComparisonCardsSection,
|
||||
modelRefFromCatalog,
|
||||
uniqueComparisonPairs,
|
||||
} from "../compare-cards"
|
||||
import {
|
||||
applyThemePreference,
|
||||
Footer,
|
||||
|
|
@ -149,6 +154,11 @@ export default function StatsLab() {
|
|||
labs={catalog()?.labs ?? []}
|
||||
market={homeStats()?.market["2M"] ?? []}
|
||||
/>
|
||||
<ComparisonCardsSection
|
||||
pairs={labComparisonPairs(data(), stats()?.models ?? [])}
|
||||
title={`${data().name} Model Comparisons`}
|
||||
description="Model pairs from this lab."
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Show>
|
||||
|
|
@ -158,6 +168,7 @@ export default function StatsLab() {
|
|||
themePreference={themePreference()}
|
||||
onThemePreferenceChange={updateThemePreference}
|
||||
links={labFooterLinks()}
|
||||
bridge={{ href: "#model-comparison", label: "MODEL COMPARISONS" }}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
|
|
@ -732,6 +743,31 @@ function LabEmptyState(props: { title: string; description: string }) {
|
|||
)
|
||||
}
|
||||
|
||||
function labComparisonPairs(lab: ModelCatalogLab, usage: LabUsageModelEntry[]) {
|
||||
const usageRefs = usage.slice(0, 4).map((model) => ({
|
||||
name: model.model,
|
||||
lab: model.provider,
|
||||
slug: model.slug,
|
||||
labName: model.author,
|
||||
metric: formatTokens(model.tokens),
|
||||
}))
|
||||
const refs = usageRefs.length > 1 ? usageRefs : lab.models.slice(0, 4).map(modelRefFromCatalog)
|
||||
return uniqueComparisonPairs(
|
||||
(
|
||||
[
|
||||
[0, 1, "Most-used lab pair"],
|
||||
[0, 2, "Lab alternative"],
|
||||
[1, 2, "Adjacent lab pair"],
|
||||
[2, 3, "Same lab pair"],
|
||||
] as const
|
||||
).flatMap(([firstIndex, secondIndex, detail]) => {
|
||||
const first = refs[firstIndex]
|
||||
const second = refs[secondIndex]
|
||||
return first && second ? [{ first, second, detail }] : []
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
type RelatedLabEntry = { lab: ModelCatalogLab; share: number; tokens: number }
|
||||
|
||||
function relatedLabs(current: ModelCatalogLab, labs: ModelCatalogLab[], market: MarketDay[]): RelatedLabEntry[] {
|
||||
|
|
|
|||
88
packages/stats/app/src/routes/compare-cards.tsx
Normal file
88
packages/stats/app/src/routes/compare-cards.tsx
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import { For, Show } from "solid-js"
|
||||
import { catalogSlug, formatCatalogLabName, type ModelCatalogEntry } from "./model-catalog"
|
||||
|
||||
export type ComparisonModelRef = {
|
||||
name: string
|
||||
lab: string
|
||||
slug: string
|
||||
labName?: string
|
||||
metric?: string
|
||||
}
|
||||
|
||||
export type ComparisonPair = {
|
||||
first: ComparisonModelRef
|
||||
second: ComparisonModelRef
|
||||
detail: string
|
||||
}
|
||||
|
||||
export function modelRefFromCatalog(entry: ModelCatalogEntry): ComparisonModelRef {
|
||||
return {
|
||||
name: entry.name,
|
||||
lab: entry.lab,
|
||||
slug: entry.slug,
|
||||
labName: formatCatalogLabName(entry.lab),
|
||||
}
|
||||
}
|
||||
|
||||
export function comparisonHref(first: ComparisonModelRef, second: ComparisonModelRef) {
|
||||
return `${import.meta.env.BASE_URL}compare/${catalogSlug(first.lab)}/${catalogSlug(first.slug)}/${catalogSlug(
|
||||
second.lab,
|
||||
)}/${catalogSlug(second.slug)}`
|
||||
}
|
||||
|
||||
export function uniqueComparisonPairs(pairs: ComparisonPair[]) {
|
||||
return pairs.reduce<{ keys: Set<string>; pairs: ComparisonPair[] }>(
|
||||
(result, pair) => {
|
||||
const key = [modelKey(pair.first), modelKey(pair.second)].toSorted().join("|")
|
||||
if (result.keys.has(key) || modelKey(pair.first) === modelKey(pair.second)) return result
|
||||
result.keys.add(key)
|
||||
result.pairs.push(pair)
|
||||
return result
|
||||
},
|
||||
{ keys: new Set(), pairs: [] },
|
||||
).pairs
|
||||
}
|
||||
|
||||
export function ComparisonCardsSection(props: {
|
||||
pairs: ComparisonPair[]
|
||||
title?: string
|
||||
description?: string
|
||||
compact?: boolean
|
||||
}) {
|
||||
return (
|
||||
<Show when={props.pairs.length > 0}>
|
||||
<section id="model-comparison" data-section="model-panel" data-variant={props.compact ? "compact" : undefined}>
|
||||
<p data-slot="section-title">
|
||||
<strong>{props.title ?? "Model Comparisons"}.</strong>{" "}
|
||||
<span>{props.description ?? "Compare usage, cost, limits, and features."}</span>
|
||||
</p>
|
||||
<div data-component="comparison-card-grid">
|
||||
<For each={props.pairs}>
|
||||
{(pair) => (
|
||||
<a data-component="comparison-card" href={comparisonHref(pair.first, pair.second)}>
|
||||
<span>{pair.detail}</span>
|
||||
<strong>
|
||||
{pair.first.name} <em>vs</em> {pair.second.name}
|
||||
</strong>
|
||||
<p>
|
||||
<b>{pair.first.labName ?? formatCatalogLabName(pair.first.lab)}</b>
|
||||
<i />
|
||||
<b>{pair.second.labName ?? formatCatalogLabName(pair.second.lab)}</b>
|
||||
</p>
|
||||
<Show when={pair.first.metric || pair.second.metric}>
|
||||
<small>
|
||||
{pair.first.metric ?? "Listed"} / {pair.second.metric ?? "Listed"}
|
||||
</small>
|
||||
</Show>
|
||||
</a>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</section>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
function modelKey(model: ComparisonModelRef) {
|
||||
return `${catalogSlug(model.lab)}/${catalogSlug(model.slug)}`
|
||||
}
|
||||
83
packages/stats/app/src/routes/compare-selector.tsx
Normal file
83
packages/stats/app/src/routes/compare-selector.tsx
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import { createEffect, createMemo, createSignal, For } from "solid-js"
|
||||
import { comparisonHref, modelRefFromCatalog } from "./compare-cards"
|
||||
import type { ModelCatalogEntry } from "./model-catalog"
|
||||
|
||||
export function ComparisonSelector(props: {
|
||||
models: ModelCatalogEntry[]
|
||||
firstId?: string
|
||||
secondId?: string
|
||||
label?: string
|
||||
}) {
|
||||
const [firstId, setFirstId] = createSignal(props.firstId ?? props.models[0]?.id ?? "")
|
||||
const [secondId, setSecondId] = createSignal(differentModelId(props.models, firstId(), props.secondId))
|
||||
const modelById = createMemo(() => new Map(props.models.map((model) => [model.id, model])))
|
||||
const href = createMemo(() => {
|
||||
const first = modelById().get(firstId())
|
||||
const second = modelById().get(secondId())
|
||||
if (!first || !second || first.id === second.id) return undefined
|
||||
return comparisonHref(modelRefFromCatalog(first), modelRefFromCatalog(second))
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (firstId() && modelById().has(firstId())) return
|
||||
const next = props.firstId && modelById().has(props.firstId) ? props.firstId : props.models[0]?.id
|
||||
if (next) setFirstId(next)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (secondId() && secondId() !== firstId() && modelById().has(secondId())) return
|
||||
setSecondId(differentModelId(props.models, firstId(), props.secondId))
|
||||
})
|
||||
|
||||
return (
|
||||
<form
|
||||
data-component="comparison-selector"
|
||||
aria-label={props.label ?? "Model comparison selector"}
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault()
|
||||
const url = href()
|
||||
if (!url || typeof window === "undefined") return
|
||||
window.location.href = url
|
||||
}}
|
||||
>
|
||||
<label>
|
||||
<span>First model</span>
|
||||
<select value={firstId()} onInput={(event) => setFirstId(event.currentTarget.value)} required>
|
||||
<option value="" disabled>
|
||||
Select model
|
||||
</option>
|
||||
<For each={props.models}>
|
||||
{(model) => (
|
||||
<option value={model.id}>
|
||||
{model.name} ({model.lab})
|
||||
</option>
|
||||
)}
|
||||
</For>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Second model</span>
|
||||
<select value={secondId()} onInput={(event) => setSecondId(event.currentTarget.value)} required>
|
||||
<option value="" disabled>
|
||||
Select model
|
||||
</option>
|
||||
<For each={props.models}>
|
||||
{(model) => (
|
||||
<option value={model.id} disabled={model.id === firstId()}>
|
||||
{model.name} ({model.lab})
|
||||
</option>
|
||||
)}
|
||||
</For>
|
||||
</select>
|
||||
</label>
|
||||
<button type="submit" disabled={!href()}>
|
||||
Compare models
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
function differentModelId(models: ModelCatalogEntry[], firstId: string, preferredId: string | undefined) {
|
||||
if (preferredId && preferredId !== firstId && models.some((model) => model.id === preferredId)) return preferredId
|
||||
return models.find((model) => model.id !== firstId)?.id ?? ""
|
||||
}
|
||||
|
|
@ -0,0 +1,608 @@
|
|||
import "../../../../index.css"
|
||||
import { Link, Meta, Title } from "@solidjs/meta"
|
||||
import {
|
||||
getStatsModelComparisonData,
|
||||
type StatsModelComparisonEntry,
|
||||
} from "@opencode-ai/stats-core/domain/home"
|
||||
import { runtime } from "@opencode-ai/stats-core/runtime"
|
||||
import { createAsync, query, useParams } from "@solidjs/router"
|
||||
import { createMemo, createSignal, For, onMount, Show } from "solid-js"
|
||||
import { getRequestEvent } from "solid-js/web"
|
||||
import {
|
||||
ComparisonCardsSection,
|
||||
modelRefFromCatalog,
|
||||
uniqueComparisonPairs,
|
||||
type ComparisonModelRef,
|
||||
type ComparisonPair,
|
||||
} from "../../../../compare-cards"
|
||||
import { ComparisonSelector } from "../../../../compare-selector"
|
||||
import {
|
||||
catalogSlug,
|
||||
findModelCatalogEntry,
|
||||
formatCatalogLabName,
|
||||
getModelCatalog,
|
||||
type ModelCatalog,
|
||||
type ModelCatalogEntry,
|
||||
} from "../../../../model-catalog"
|
||||
import {
|
||||
applyThemePreference,
|
||||
Footer,
|
||||
getGitHubStars,
|
||||
Header,
|
||||
isThemePreference,
|
||||
themeStorageKey,
|
||||
type HeaderLink,
|
||||
type ThemePreference,
|
||||
} from "../../../../stats-shell"
|
||||
|
||||
const compareFallbackUrl = "https://stats.opencode.ai"
|
||||
const compareHeaderLinks: readonly HeaderLink[] = [
|
||||
{ href: "#overview", label: "Overview" },
|
||||
{ href: "#comparison", label: "Comparison" },
|
||||
{ href: "#compare-tool", label: "Compare" },
|
||||
{ href: "#model-comparison", label: "Related" },
|
||||
]
|
||||
const compareFooterLinks: readonly HeaderLink[] = [
|
||||
{ href: import.meta.env.BASE_URL, label: "Data Home" },
|
||||
{ href: `${import.meta.env.BASE_URL}compare`, label: "Model Compare" },
|
||||
{ href: `${import.meta.env.BASE_URL}#top-models`, label: "Top Models" },
|
||||
{ href: `${import.meta.env.BASE_URL}#token-cost`, label: "Token Cost" },
|
||||
]
|
||||
|
||||
type ComparisonModel = {
|
||||
name: string
|
||||
lab: string
|
||||
labName: string
|
||||
slug: string
|
||||
catalog: ModelCatalogEntry | null
|
||||
stats: StatsModelComparisonEntry | null
|
||||
}
|
||||
type ComparisonDirection = "higher" | "lower"
|
||||
type ComparisonCell = { value: string; detail?: string; score?: number }
|
||||
type ComparisonRow = {
|
||||
label: string
|
||||
description: string
|
||||
direction: ComparisonDirection
|
||||
cells: [ComparisonCell, ComparisonCell]
|
||||
}
|
||||
|
||||
const getComparisonData = query(
|
||||
async (firstLab: string, firstModel: string, secondLab: string, secondModel: string) => {
|
||||
"use server"
|
||||
return runtime.runPromise(getStatsModelComparisonData(firstLab, firstModel, secondLab, secondModel))
|
||||
},
|
||||
"getStatsModelComparisonData",
|
||||
)
|
||||
|
||||
export default function ModelComparePair() {
|
||||
const event = getRequestEvent()
|
||||
event?.response.headers.set("Cache-Control", "public, max-age=60, s-maxage=300, stale-while-revalidate=86400")
|
||||
const params = useParams()
|
||||
const firstLabParam = createMemo(() => params.firstLab ?? "")
|
||||
const firstModelParam = createMemo(() => params.firstModel ?? "")
|
||||
const secondLabParam = createMemo(() => params.secondLab ?? "")
|
||||
const secondModelParam = createMemo(() => params.secondModel ?? "")
|
||||
const catalog = createAsync(() => getModelCatalog())
|
||||
const firstCatalog = createMemo(() => resolvedCatalogEntry(catalog(), firstLabParam(), firstModelParam()))
|
||||
const secondCatalog = createMemo(() => resolvedCatalogEntry(catalog(), secondLabParam(), secondModelParam()))
|
||||
const stats = createAsync(() => {
|
||||
if (catalog() === undefined || firstCatalog() === undefined || secondCatalog() === undefined)
|
||||
return Promise.resolve(undefined)
|
||||
return getComparisonData(
|
||||
firstCatalog()?.lab ?? firstLabParam(),
|
||||
firstCatalog()?.slug ?? firstModelParam(),
|
||||
secondCatalog()?.lab ?? secondLabParam(),
|
||||
secondCatalog()?.slug ?? secondModelParam(),
|
||||
)
|
||||
})
|
||||
const githubStars = createAsync(() => getGitHubStars())
|
||||
const [themePreference, setThemePreference] = createSignal<ThemePreference>("system")
|
||||
const models = createMemo(
|
||||
() =>
|
||||
[
|
||||
buildComparisonModel(firstLabParam(), firstModelParam(), firstCatalog() ?? null, stats()?.models[0] ?? null),
|
||||
buildComparisonModel(secondLabParam(), secondModelParam(), secondCatalog() ?? null, stats()?.models[1] ?? null),
|
||||
] as const,
|
||||
)
|
||||
const title = createMemo(() => `${models()[0].name} vs ${models()[1].name} - Model Comparison`)
|
||||
const description = createMemo(
|
||||
() =>
|
||||
`Compare ${models()[0].name} and ${models()[1].name} by usage, rank, context window, output limit, cache ratio, and cost across OpenCode data.`,
|
||||
)
|
||||
const canonicalPath = createMemo(
|
||||
() =>
|
||||
`${import.meta.env.BASE_URL}compare/${catalogSlug(models()[0].lab)}/${catalogSlug(models()[0].slug)}/${catalogSlug(
|
||||
models()[1].lab,
|
||||
)}/${catalogSlug(models()[1].slug)}`,
|
||||
)
|
||||
const canonicalUrl = createMemo(() =>
|
||||
new URL(
|
||||
canonicalPath(),
|
||||
event?.request.url ?? (typeof window === "undefined" ? compareFallbackUrl : window.location.href),
|
||||
).toString(),
|
||||
)
|
||||
const rows = createMemo(() => buildComparisonRows(models()[0], models()[1]))
|
||||
const relatedPairs = createMemo(() => buildRelatedPairs(catalog(), models()[0], models()[1]))
|
||||
const selectorModels = createMemo(() =>
|
||||
uniqueCatalogModels([comparisonCatalogEntry(models()[0]), comparisonCatalogEntry(models()[1]), ...(catalog()?.models ?? [])]),
|
||||
)
|
||||
const structuredData = createMemo(() =>
|
||||
JSON.stringify({
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebPage",
|
||||
name: title(),
|
||||
description: description(),
|
||||
url: canonicalUrl(),
|
||||
about: models().map((model) => ({
|
||||
"@type": "SoftwareApplication",
|
||||
name: model.name,
|
||||
applicationCategory: "AI model",
|
||||
provider: model.labName,
|
||||
})),
|
||||
}),
|
||||
)
|
||||
const updateThemePreference = (preference: ThemePreference) => {
|
||||
applyThemePreference(preference)
|
||||
setThemePreference(preference)
|
||||
if (typeof window === "undefined") return
|
||||
window.localStorage.setItem(themeStorageKey, preference)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (typeof window === "undefined") return
|
||||
const preference = window.localStorage.getItem(themeStorageKey)
|
||||
const nextPreference = isThemePreference(preference) ? preference : "system"
|
||||
applyThemePreference(nextPreference)
|
||||
setThemePreference(nextPreference)
|
||||
})
|
||||
|
||||
return (
|
||||
<main data-page="stats" data-theme={themePreference()}>
|
||||
<Title>{title()}</Title>
|
||||
<Meta name="description" content={description()} />
|
||||
<Link rel="canonical" href={canonicalUrl()} />
|
||||
<Meta property="og:type" content="website" />
|
||||
<Meta property="og:site_name" content="OpenCode" />
|
||||
<Meta property="og:title" content={title()} />
|
||||
<Meta property="og:description" content={description()} />
|
||||
<Meta property="og:url" content={canonicalUrl()} />
|
||||
<Meta name="twitter:card" content="summary" />
|
||||
<Meta name="twitter:title" content={title()} />
|
||||
<Meta name="twitter:description" content={description()} />
|
||||
<script type="application/ld+json">{structuredData()}</script>
|
||||
<Header githubStars={githubStars() ?? "150K"} links={compareHeaderLinks} brandHref={import.meta.env.BASE_URL} />
|
||||
<div data-component="container">
|
||||
<div data-component="content">
|
||||
<ComparisonHero models={models()} />
|
||||
<section id="comparison" data-section="model-panel">
|
||||
<p data-slot="section-title">
|
||||
<strong>Comparison Table.</strong>{" "}
|
||||
<span>Compare usage, cost, limits, and features.</span>
|
||||
</p>
|
||||
<Show
|
||||
when={stats() !== undefined}
|
||||
fallback={
|
||||
<div data-component="empty-state" data-compact="true">
|
||||
<strong>Loading comparison</strong>
|
||||
<p>Loading stats for both models.</p>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<ComparisonTable models={models()} rows={rows()} />
|
||||
</Show>
|
||||
</section>
|
||||
<section id="compare-tool" data-section="model-panel" data-variant="compact">
|
||||
<p data-slot="section-title">
|
||||
<strong>Compare Another Pair.</strong> <span>Choose two models to compare.</span>
|
||||
</p>
|
||||
<Show
|
||||
when={selectorModels().length > 1}
|
||||
fallback={
|
||||
<div data-component="empty-state" data-compact="true">
|
||||
<strong>No models found</strong>
|
||||
<p>The model list could not be loaded.</p>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<ComparisonSelector
|
||||
models={selectorModels()}
|
||||
firstId={comparisonCatalogEntry(models()[0]).id}
|
||||
secondId={comparisonCatalogEntry(models()[1]).id}
|
||||
/>
|
||||
</Show>
|
||||
</section>
|
||||
<ComparisonCardsSection
|
||||
pairs={relatedPairs()}
|
||||
title="Related Model Comparisons"
|
||||
description="Other model pairs to check."
|
||||
/>
|
||||
</div>
|
||||
<Footer
|
||||
themePreference={themePreference()}
|
||||
onThemePreferenceChange={updateThemePreference}
|
||||
links={compareFooterLinks}
|
||||
bridge={{ href: "#comparison", label: "COMPARE TABLE" }}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
function ComparisonHero(props: { models: readonly [ComparisonModel, ComparisonModel] }) {
|
||||
return (
|
||||
<section id="overview" data-section="model-hero">
|
||||
<a data-slot="model-back-link" href={`${import.meta.env.BASE_URL}compare`}>
|
||||
Compare
|
||||
</a>
|
||||
<div data-slot="model-hero-copy">
|
||||
<h1>
|
||||
{props.models[0].name} vs {props.models[1].name}
|
||||
</h1>
|
||||
<p>Compare usage, cost, limits, and features for these two models.</p>
|
||||
</div>
|
||||
<div data-slot="model-hero-pattern" aria-hidden="true" />
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function ComparisonTable(props: { models: readonly [ComparisonModel, ComparisonModel]; rows: ComparisonRow[] }) {
|
||||
return (
|
||||
<div data-component="comparison-table-wrap">
|
||||
<table data-component="comparison-table">
|
||||
<caption>
|
||||
{props.models[0].name} compared with {props.models[1].name}
|
||||
</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Metric</th>
|
||||
<For each={props.models}>{(model) => <th scope="col">{model.name}</th>}</For>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<For each={props.rows}>
|
||||
{(row) => {
|
||||
const best = () => bestCellIndex(row)
|
||||
return (
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<strong>{row.label}</strong>
|
||||
<span>{row.description}</span>
|
||||
</th>
|
||||
<For each={row.cells}>
|
||||
{(cell, index) => (
|
||||
<td data-best={best() === index() ? "true" : undefined}>
|
||||
<strong>{cell.value}</strong>
|
||||
<Show when={cell.detail}>{(detail) => <span>{detail()}</span>}</Show>
|
||||
</td>
|
||||
)}
|
||||
</For>
|
||||
</tr>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function resolvedCatalogEntry(catalog: ModelCatalog | undefined, lab: string, model: string) {
|
||||
if (!catalog) return undefined
|
||||
return findModelCatalogEntry(catalog, model, lab) ?? null
|
||||
}
|
||||
|
||||
function buildComparisonModel(
|
||||
labParam: string,
|
||||
modelParam: string,
|
||||
catalog: ModelCatalogEntry | null,
|
||||
stats: StatsModelComparisonEntry | null,
|
||||
): ComparisonModel {
|
||||
return {
|
||||
name: catalog?.name ?? stats?.model ?? formatParamName(modelParam),
|
||||
lab: catalog?.lab ?? stats?.provider ?? catalogSlug(labParam),
|
||||
labName: formatCatalogLabName(catalog?.lab ?? stats?.provider ?? labParam),
|
||||
slug: catalog?.slug ?? stats?.slug ?? catalogSlug(modelParam),
|
||||
catalog,
|
||||
stats,
|
||||
}
|
||||
}
|
||||
|
||||
function comparisonCatalogEntry(model: ComparisonModel): ModelCatalogEntry {
|
||||
if (model.catalog) return model.catalog
|
||||
return {
|
||||
id: `${catalogSlug(model.lab)}/${catalogSlug(model.slug)}`,
|
||||
lab: catalogSlug(model.lab),
|
||||
slug: catalogSlug(model.slug),
|
||||
name: model.name,
|
||||
modalities: { input: [], output: [] },
|
||||
openWeights: false,
|
||||
reasoning: false,
|
||||
toolCall: false,
|
||||
attachment: false,
|
||||
temperature: false,
|
||||
weights: [],
|
||||
benchmarks: [],
|
||||
}
|
||||
}
|
||||
|
||||
function uniqueCatalogModels(models: ModelCatalogEntry[]) {
|
||||
return Object.values(
|
||||
models.reduce<Record<string, ModelCatalogEntry>>((result, model) => {
|
||||
result[model.id] = result[model.id] ?? model
|
||||
return result
|
||||
}, {}),
|
||||
)
|
||||
}
|
||||
|
||||
function buildComparisonRows(first: ComparisonModel, second: ComparisonModel): ComparisonRow[] {
|
||||
return [
|
||||
comparisonRow(
|
||||
"Recent Rank",
|
||||
"Lower is better.",
|
||||
{
|
||||
value: first.stats?.rank == null ? "No usage" : `#${first.stats.rank}`,
|
||||
score: first.stats?.rank ?? undefined,
|
||||
},
|
||||
{
|
||||
value: second.stats?.rank == null ? "No usage" : `#${second.stats.rank}`,
|
||||
score: second.stats?.rank ?? undefined,
|
||||
},
|
||||
"lower",
|
||||
),
|
||||
comparisonRow(
|
||||
"Token Share",
|
||||
"Share of recent OpenCode usage.",
|
||||
{ value: first.stats ? formatPercent(first.stats.tokenShare) : "No usage", score: first.stats?.tokenShare },
|
||||
{ value: second.stats ? formatPercent(second.stats.tokenShare) : "No usage", score: second.stats?.tokenShare },
|
||||
"higher",
|
||||
),
|
||||
comparisonRow(
|
||||
"Tokens",
|
||||
"Recent token volume.",
|
||||
{ value: first.stats ? formatTokens(first.stats.totals.tokens) : "No usage", score: first.stats?.totals.tokens },
|
||||
{
|
||||
value: second.stats ? formatTokens(second.stats.totals.tokens) : "No usage",
|
||||
score: second.stats?.totals.tokens,
|
||||
},
|
||||
"higher",
|
||||
),
|
||||
comparisonRow(
|
||||
"Sessions",
|
||||
"Recent session count.",
|
||||
{
|
||||
value: first.stats ? formatInteger(first.stats.totals.sessions) : "No usage",
|
||||
score: first.stats?.totals.sessions,
|
||||
},
|
||||
{
|
||||
value: second.stats ? formatInteger(second.stats.totals.sessions) : "No usage",
|
||||
score: second.stats?.totals.sessions,
|
||||
},
|
||||
"higher",
|
||||
),
|
||||
comparisonRow(
|
||||
"Cost / 1M Tokens",
|
||||
"Lower is better.",
|
||||
{
|
||||
value: first.stats ? formatMoney(first.stats.totals.costPerMillion) : "No usage",
|
||||
score: positiveScore(first.stats?.totals.costPerMillion),
|
||||
},
|
||||
{
|
||||
value: second.stats ? formatMoney(second.stats.totals.costPerMillion) : "No usage",
|
||||
score: positiveScore(second.stats?.totals.costPerMillion),
|
||||
},
|
||||
"lower",
|
||||
),
|
||||
comparisonRow(
|
||||
"Cost / Session",
|
||||
"Lower is better.",
|
||||
{
|
||||
value: first.stats ? formatSessionCost(first.stats.totals.costPerSession) : "No usage",
|
||||
score: positiveScore(first.stats?.totals.costPerSession),
|
||||
},
|
||||
{
|
||||
value: second.stats ? formatSessionCost(second.stats.totals.costPerSession) : "No usage",
|
||||
score: positiveScore(second.stats?.totals.costPerSession),
|
||||
},
|
||||
"lower",
|
||||
),
|
||||
comparisonRow(
|
||||
"Cache Ratio",
|
||||
"Higher is better.",
|
||||
{ value: first.stats ? formatPercent(first.stats.totals.cacheRatio) : "No usage", score: first.stats?.totals.cacheRatio },
|
||||
{
|
||||
value: second.stats ? formatPercent(second.stats.totals.cacheRatio) : "No usage",
|
||||
score: second.stats?.totals.cacheRatio,
|
||||
},
|
||||
"higher",
|
||||
),
|
||||
comparisonRow(
|
||||
"Context Window",
|
||||
"Higher limit is better.",
|
||||
{
|
||||
value: formatCatalogLimit(first.catalog?.limit?.context),
|
||||
score: first.catalog?.limit?.context,
|
||||
},
|
||||
{
|
||||
value: formatCatalogLimit(second.catalog?.limit?.context),
|
||||
score: second.catalog?.limit?.context,
|
||||
},
|
||||
"higher",
|
||||
),
|
||||
comparisonRow(
|
||||
"Output Limit",
|
||||
"Higher limit is better.",
|
||||
{
|
||||
value: formatCatalogLimit(first.catalog?.limit?.output),
|
||||
score: first.catalog?.limit?.output,
|
||||
},
|
||||
{
|
||||
value: formatCatalogLimit(second.catalog?.limit?.output),
|
||||
score: second.catalog?.limit?.output,
|
||||
},
|
||||
"higher",
|
||||
),
|
||||
comparisonRow(
|
||||
"Release Date",
|
||||
"Newer release is highlighted.",
|
||||
{
|
||||
value: formatCatalogDate(first.catalog?.releaseDate),
|
||||
score: catalogDateScore(first.catalog?.releaseDate),
|
||||
},
|
||||
{
|
||||
value: formatCatalogDate(second.catalog?.releaseDate),
|
||||
score: catalogDateScore(second.catalog?.releaseDate),
|
||||
},
|
||||
"higher",
|
||||
),
|
||||
comparisonRow(
|
||||
"Reasoning",
|
||||
"Supports reasoning.",
|
||||
booleanCell(first.catalog?.reasoning),
|
||||
booleanCell(second.catalog?.reasoning),
|
||||
"higher",
|
||||
),
|
||||
comparisonRow(
|
||||
"Tool Calling",
|
||||
"Supports tool calls.",
|
||||
booleanCell(first.catalog?.toolCall),
|
||||
booleanCell(second.catalog?.toolCall),
|
||||
"higher",
|
||||
),
|
||||
comparisonRow(
|
||||
"Attachments",
|
||||
"Supports attachments.",
|
||||
booleanCell(first.catalog?.attachment),
|
||||
booleanCell(second.catalog?.attachment),
|
||||
"higher",
|
||||
),
|
||||
comparisonRow(
|
||||
"Open Weights",
|
||||
"Open weights available.",
|
||||
booleanCell(first.catalog?.openWeights),
|
||||
booleanCell(second.catalog?.openWeights),
|
||||
"higher",
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
function comparisonRow(
|
||||
label: string,
|
||||
description: string,
|
||||
first: ComparisonCell,
|
||||
second: ComparisonCell,
|
||||
direction: ComparisonDirection,
|
||||
): ComparisonRow {
|
||||
return { label, description, direction, cells: [first, second] }
|
||||
}
|
||||
|
||||
function bestCellIndex(row: ComparisonRow) {
|
||||
const [first, second] = row.cells.map((cell) => cell.score)
|
||||
if (first === undefined || second === undefined || first === second) return undefined
|
||||
if (row.direction === "higher") return first > second ? 0 : 1
|
||||
return first < second ? 0 : 1
|
||||
}
|
||||
|
||||
function buildRelatedPairs(
|
||||
catalog: ModelCatalog | undefined,
|
||||
first: ComparisonModel,
|
||||
second: ComparisonModel,
|
||||
): ComparisonPair[] {
|
||||
const current = [comparisonRef(first), comparisonRef(second)] as const
|
||||
const alternatives = (catalog?.models ?? [])
|
||||
.filter((model) => model.id !== first.catalog?.id && model.id !== second.catalog?.id)
|
||||
.slice(0, 4)
|
||||
.map(modelRefFromCatalog)
|
||||
|
||||
return uniqueComparisonPairs([
|
||||
...alternatives.slice(0, 3).flatMap((model, index) => [
|
||||
{ first: current[0], second: model, detail: index === 0 ? "Nearby alternative" : "Related comparison" },
|
||||
{ first: current[1], second: model, detail: index === 0 ? "Nearby alternative" : "Related comparison" },
|
||||
]),
|
||||
]).slice(0, 6)
|
||||
}
|
||||
|
||||
function comparisonRef(model: ComparisonModel): ComparisonModelRef {
|
||||
return {
|
||||
name: model.name,
|
||||
lab: model.lab,
|
||||
slug: model.slug,
|
||||
labName: model.labName,
|
||||
metric: model.stats ? `#${model.stats.rank}` : "Catalog",
|
||||
}
|
||||
}
|
||||
|
||||
function positiveScore(value: number | undefined) {
|
||||
return value && value > 0 ? value : undefined
|
||||
}
|
||||
|
||||
function booleanCell(value: boolean | undefined): ComparisonCell {
|
||||
if (value === undefined) return { value: "Unknown" }
|
||||
return { value: value ? "Yes" : "No", score: value ? 1 : 0 }
|
||||
}
|
||||
|
||||
function catalogDateScore(value: string | undefined) {
|
||||
if (!value) return undefined
|
||||
const match = /^(\d{4})(?:-(\d{2}))?(?:-(\d{2}))?$/.exec(value)
|
||||
if (!match) return undefined
|
||||
return Date.UTC(Number(match[1]), match[2] ? Number(match[2]) - 1 : 0, match[3] ? Number(match[3]) : 1)
|
||||
}
|
||||
|
||||
function formatParamName(value: string) {
|
||||
return value
|
||||
.replace(/[-_]/g, " ")
|
||||
.replace(/\b\w/g, (letter) => letter.toUpperCase())
|
||||
.trim()
|
||||
}
|
||||
|
||||
function formatCatalogLimit(value: number | undefined) {
|
||||
return value === undefined ? "Unknown" : formatTokens(value)
|
||||
}
|
||||
|
||||
function formatCatalogDate(value: string | undefined) {
|
||||
if (!value) return "Unknown"
|
||||
const match = /^(\d{4})(?:-(\d{2}))?(?:-(\d{2}))?$/.exec(value)
|
||||
if (!match) return value
|
||||
const year = Number(match[1])
|
||||
const month = match[2] ? Number(match[2]) - 1 : 0
|
||||
const day = match[3] ? Number(match[3]) : 1
|
||||
return new Intl.DateTimeFormat("en", {
|
||||
month: match[2] ? "short" : undefined,
|
||||
day: match[3] ? "numeric" : undefined,
|
||||
year: "numeric",
|
||||
timeZone: "UTC",
|
||||
}).format(new Date(Date.UTC(year, month, day)))
|
||||
}
|
||||
|
||||
function formatTokens(value: number) {
|
||||
if (value >= 1_000_000_000_000)
|
||||
return `${trimNumber(value / 1_000_000_000_000, value >= 10_000_000_000_000 ? 0 : 1)}T`
|
||||
if (value >= 1_000_000_000) return `${trimNumber(value / 1_000_000_000, value >= 10_000_000_000 ? 0 : 1)}B`
|
||||
if (value >= 1_000_000) return `${trimNumber(value / 1_000_000, value >= 10_000_000 ? 0 : 1)}M`
|
||||
if (value >= 1_000) return `${trimNumber(value / 1_000, value >= 10_000 ? 0 : 1)}K`
|
||||
return String(Math.round(value))
|
||||
}
|
||||
|
||||
function formatInteger(value: number) {
|
||||
return new Intl.NumberFormat("en").format(value)
|
||||
}
|
||||
|
||||
function formatPercent(value: number) {
|
||||
return `${trimNumber(value, value >= 10 ? 1 : 2)}%`
|
||||
}
|
||||
|
||||
function formatMoney(value: number) {
|
||||
if (value >= 1) return `$${trimNumber(value, 2)}`
|
||||
if (value > 0) return `$${value.toFixed(4)}`
|
||||
return "$0"
|
||||
}
|
||||
|
||||
function formatSessionCost(value: number) {
|
||||
if (value >= 1) return `$${trimNumber(value, 2)}`
|
||||
if (value >= 0.01) return `$${value.toFixed(2)}`
|
||||
if (value > 0) return `$${value.toFixed(4)}`
|
||||
return "$0"
|
||||
}
|
||||
|
||||
function trimNumber(value: number, digits: number) {
|
||||
return Number(value.toFixed(digits)).toLocaleString("en")
|
||||
}
|
||||
148
packages/stats/app/src/routes/compare/index.tsx
Normal file
148
packages/stats/app/src/routes/compare/index.tsx
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
import "../index.css"
|
||||
import { Link, Meta, Title } from "@solidjs/meta"
|
||||
import { createAsync } from "@solidjs/router"
|
||||
import { createMemo, createSignal, onMount, Show } from "solid-js"
|
||||
import { getRequestEvent } from "solid-js/web"
|
||||
import { ComparisonCardsSection, modelRefFromCatalog, uniqueComparisonPairs } from "../compare-cards"
|
||||
import { ComparisonSelector } from "../compare-selector"
|
||||
import { getModelCatalog, type ModelCatalogEntry } from "../model-catalog"
|
||||
import {
|
||||
applyThemePreference,
|
||||
Footer,
|
||||
getGitHubStars,
|
||||
Header,
|
||||
isThemePreference,
|
||||
themeStorageKey,
|
||||
type HeaderLink,
|
||||
type ThemePreference,
|
||||
} from "../stats-shell"
|
||||
|
||||
const compareFallbackUrl = "https://stats.opencode.ai"
|
||||
const compareTitle = "AI Model Comparison"
|
||||
const compareDescription =
|
||||
"Compare AI models used in OpenCode by context, output, release date, usage, rank, token share, and cost."
|
||||
const compareHeaderLinks: readonly HeaderLink[] = [
|
||||
{ href: "#compare-tool", label: "Compare" },
|
||||
{ href: "#model-comparison", label: "Popular" },
|
||||
]
|
||||
const compareFooterLinks: readonly HeaderLink[] = [
|
||||
{ href: import.meta.env.BASE_URL, label: "Data Home" },
|
||||
{ href: `${import.meta.env.BASE_URL}#top-models`, label: "Top Models" },
|
||||
{ href: `${import.meta.env.BASE_URL}#token-cost`, label: "Token Cost" },
|
||||
{ href: `${import.meta.env.BASE_URL}compare`, label: "Model Compare" },
|
||||
]
|
||||
|
||||
export default function ModelCompareIndex() {
|
||||
const event = getRequestEvent()
|
||||
event?.response.headers.set("Cache-Control", "public, max-age=60, s-maxage=300, stale-while-revalidate=86400")
|
||||
const catalog = createAsync(() => getModelCatalog())
|
||||
const githubStars = createAsync(() => getGitHubStars())
|
||||
const [themePreference, setThemePreference] = createSignal<ThemePreference>("system")
|
||||
const compareUrl = createMemo(() =>
|
||||
new URL(
|
||||
`${import.meta.env.BASE_URL}compare`,
|
||||
event?.request.url ?? (typeof window === "undefined" ? compareFallbackUrl : window.location.href),
|
||||
).toString(),
|
||||
)
|
||||
const featuredModels = createMemo(() => (catalog()?.models ?? []).slice(0, 120))
|
||||
const pairs = createMemo(() => buildPopularPairs(featuredModels()))
|
||||
const updateThemePreference = (preference: ThemePreference) => {
|
||||
applyThemePreference(preference)
|
||||
setThemePreference(preference)
|
||||
if (typeof window === "undefined") return
|
||||
window.localStorage.setItem(themeStorageKey, preference)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (typeof window === "undefined") return
|
||||
const preference = window.localStorage.getItem(themeStorageKey)
|
||||
const nextPreference = isThemePreference(preference) ? preference : "system"
|
||||
applyThemePreference(nextPreference)
|
||||
setThemePreference(nextPreference)
|
||||
})
|
||||
|
||||
return (
|
||||
<main data-page="stats" data-theme={themePreference()}>
|
||||
<Title>{compareTitle}</Title>
|
||||
<Meta name="description" content={compareDescription} />
|
||||
<Link rel="canonical" href={compareUrl()} />
|
||||
<Meta property="og:type" content="website" />
|
||||
<Meta property="og:site_name" content="OpenCode" />
|
||||
<Meta property="og:title" content={compareTitle} />
|
||||
<Meta property="og:description" content={compareDescription} />
|
||||
<Meta property="og:url" content={compareUrl()} />
|
||||
<Meta name="twitter:card" content="summary" />
|
||||
<Meta name="twitter:title" content={compareTitle} />
|
||||
<Meta name="twitter:description" content={compareDescription} />
|
||||
<Header githubStars={githubStars() ?? "150K"} links={compareHeaderLinks} brandHref={import.meta.env.BASE_URL} />
|
||||
<div data-component="container">
|
||||
<div data-component="content">
|
||||
<section id="compare-tool" data-section="model-hero">
|
||||
<a data-slot="model-back-link" href={import.meta.env.BASE_URL}>
|
||||
Data
|
||||
</a>
|
||||
<div data-slot="model-hero-grid">
|
||||
<div data-slot="model-hero-copy">
|
||||
<span data-slot="model-id-tag">/compare</span>
|
||||
<h1>Model Comparison</h1>
|
||||
<p>Choose two models and compare usage, cost, limits, and features.</p>
|
||||
</div>
|
||||
<div data-component="model-rank-panel">
|
||||
<span>Model Pairs</span>
|
||||
<strong>{formatPairCount(featuredModels().length)}</strong>
|
||||
<p>Available pairings from the model list.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div data-slot="model-hero-pattern" aria-hidden="true" />
|
||||
<Show
|
||||
when={featuredModels().length > 1}
|
||||
fallback={
|
||||
<div data-component="empty-state" data-compact="true">
|
||||
<strong>No models found</strong>
|
||||
<p>The model list could not be loaded.</p>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<ComparisonSelector models={featuredModels()} />
|
||||
</Show>
|
||||
</section>
|
||||
<ComparisonCardsSection
|
||||
pairs={pairs()}
|
||||
title="Popular Model Comparisons"
|
||||
description="Common model pairs to start with."
|
||||
/>
|
||||
</div>
|
||||
<Footer
|
||||
themePreference={themePreference()}
|
||||
onThemePreferenceChange={updateThemePreference}
|
||||
links={compareFooterLinks}
|
||||
bridge={{ href: "#compare-tool", label: "MODEL COMPARE" }}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
function buildPopularPairs(models: ModelCatalogEntry[]) {
|
||||
return uniqueComparisonPairs(
|
||||
(
|
||||
[
|
||||
[0, 1, "Popular pair"],
|
||||
[0, 2, "Top alternative"],
|
||||
[1, 2, "Nearby pair"],
|
||||
[2, 3, "Recent model pair"],
|
||||
[3, 4, "Model pair"],
|
||||
[4, 5, "Model pair"],
|
||||
] as const
|
||||
).flatMap(([firstIndex, secondIndex, detail]) => {
|
||||
const first = models[firstIndex]
|
||||
const second = models[secondIndex]
|
||||
return first && second ? [{ first: modelRefFromCatalog(first), second: modelRefFromCatalog(second), detail }] : []
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function formatPairCount(count: number) {
|
||||
if (count < 2) return "0"
|
||||
return new Intl.NumberFormat("en", { notation: "compact", maximumFractionDigits: 1 }).format((count * (count - 1)) / 2)
|
||||
}
|
||||
|
|
@ -5252,6 +5252,243 @@
|
|||
color: var(--stats-text);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-section="model-panel"][data-variant="compact"] {
|
||||
padding-top: 56px;
|
||||
padding-bottom: 56px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="comparison-card-grid"] {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="comparison-card"] {
|
||||
display: grid;
|
||||
align-content: space-between;
|
||||
gap: 18px;
|
||||
min-height: 168px;
|
||||
min-width: 0;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--stats-line);
|
||||
background: var(--stats-layer);
|
||||
color: var(--stats-text);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="comparison-card"]:hover,
|
||||
[data-page="stats"] [data-component="comparison-card"]:focus-visible {
|
||||
border-color: var(--stats-text);
|
||||
outline: none;
|
||||
background: var(--stats-bg);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="comparison-card"] span,
|
||||
[data-page="stats"] [data-component="comparison-card"] p,
|
||||
[data-page="stats"] [data-component="comparison-card"] small {
|
||||
color: var(--stats-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="comparison-card"] strong {
|
||||
min-width: 0;
|
||||
color: var(--stats-text);
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
line-height: 1.08;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="comparison-card"] em {
|
||||
color: var(--stats-faint);
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="comparison-card"] p {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="comparison-card"] b {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
font: inherit;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="comparison-card"] i {
|
||||
flex: 0 0 auto;
|
||||
width: 16px;
|
||||
height: 1px;
|
||||
background: var(--stats-line-strong);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="comparison-selector"] {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto;
|
||||
gap: 8px;
|
||||
align-items: end;
|
||||
max-width: 920px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="comparison-selector"] label {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="comparison-selector"] label span {
|
||||
color: var(--stats-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="comparison-selector"] select,
|
||||
[data-page="stats"] [data-component="comparison-selector"] button {
|
||||
box-sizing: border-box;
|
||||
height: 40px;
|
||||
min-width: 0;
|
||||
border: 1px solid var(--stats-line);
|
||||
border-radius: 0;
|
||||
appearance: none;
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="comparison-selector"] select {
|
||||
width: 100%;
|
||||
padding: 0 36px 0 12px;
|
||||
background:
|
||||
linear-gradient(45deg, transparent 50%, var(--stats-muted) 50%) calc(100% - 18px) 17px / 5px 5px no-repeat,
|
||||
linear-gradient(135deg, var(--stats-muted) 50%, transparent 50%) calc(100% - 13px) 17px / 5px 5px no-repeat,
|
||||
var(--stats-bg);
|
||||
color: var(--stats-text);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="comparison-selector"] button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 14px;
|
||||
background: var(--stats-text);
|
||||
color: var(--stats-bg);
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="comparison-selector"] button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="comparison-selector"] select:focus-visible,
|
||||
[data-page="stats"] [data-component="comparison-selector"] button:focus-visible {
|
||||
outline: 2px solid var(--stats-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="comparison-table-wrap"] {
|
||||
overflow-x: auto;
|
||||
border: 1px solid var(--stats-line);
|
||||
background: var(--stats-layer);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="comparison-table"] {
|
||||
width: 100%;
|
||||
min-width: 720px;
|
||||
border-collapse: collapse;
|
||||
color: var(--stats-text);
|
||||
font-size: 13px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="comparison-table"] caption {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="comparison-table"] th,
|
||||
[data-page="stats"] [data-component="comparison-table"] td {
|
||||
min-width: 0;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--stats-line);
|
||||
border-left: 1px solid var(--stats-line);
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="comparison-table"] th:first-child {
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="comparison-table"] thead th {
|
||||
color: var(--stats-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="comparison-table"] tbody tr:last-child th,
|
||||
[data-page="stats"] [data-component="comparison-table"] tbody tr:last-child td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="comparison-table"] tbody th {
|
||||
width: 34%;
|
||||
background: var(--stats-bg);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="comparison-table"] tbody th strong,
|
||||
[data-page="stats"] [data-component="comparison-table"] td strong {
|
||||
display: block;
|
||||
color: var(--stats-text);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="comparison-table"] tbody th span,
|
||||
[data-page="stats"] [data-component="comparison-table"] td span {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
color: var(--stats-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="comparison-table"] td[data-best="true"] {
|
||||
position: relative;
|
||||
background: var(--stats-bg);
|
||||
box-shadow: inset 0 0 0 1px var(--stats-accent);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="comparison-table"] td[data-best="true"]::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 3px;
|
||||
content: "";
|
||||
background: var(--stats-accent);
|
||||
}
|
||||
|
||||
[data-page="stats"][data-theme="dark"],
|
||||
:root[data-stats-theme="dark"] [data-page="stats"]:not([data-theme="light"]) {
|
||||
color-scheme: dark;
|
||||
|
|
@ -5784,10 +6021,19 @@
|
|||
[data-page="stats"] [data-component="model-metric-grid"],
|
||||
[data-page="stats"] [data-component="model-metric-grid"][data-variant="dense"],
|
||||
[data-page="stats"] [data-component="model-efficiency-grid"],
|
||||
[data-page="stats"] [data-component="lab-model-grid"] {
|
||||
[data-page="stats"] [data-component="lab-model-grid"],
|
||||
[data-page="stats"] [data-component="comparison-card-grid"] {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="comparison-selector"] {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="comparison-selector"] button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-peer-list"] a {
|
||||
grid-template-columns: 32px 32px minmax(0, 1fr) minmax(64px, auto);
|
||||
gap: 12px;
|
||||
|
|
@ -6212,10 +6458,15 @@
|
|||
[data-page="stats"] [data-component="model-metric-grid"],
|
||||
[data-page="stats"] [data-component="model-metric-grid"][data-variant="dense"],
|
||||
[data-page="stats"] [data-component="model-efficiency-grid"],
|
||||
[data-page="stats"] [data-component="lab-model-grid"] {
|
||||
[data-page="stats"] [data-component="lab-model-grid"],
|
||||
[data-page="stats"] [data-component="comparison-card-grid"] {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="comparison-table"] {
|
||||
min-width: 640px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="model-metric"] {
|
||||
min-height: 112px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import { localizedUrl } from "../lib/language"
|
|||
import { findModelCatalogEntry, getModelCatalog, type ModelCatalog } from "./model-catalog"
|
||||
import { SectionHeading } from "./section-heading"
|
||||
import { setStatsPageCacheHeaders } from "./stats-cache"
|
||||
import { ComparisonCardsSection, uniqueComparisonPairs, type ComparisonModelRef } from "./compare-cards"
|
||||
import {
|
||||
applyThemePreference,
|
||||
Footer,
|
||||
|
|
@ -48,6 +49,12 @@ import {
|
|||
const products = ["All Users", "Zen", "Go"] as const
|
||||
const tokenProducts = ["Zen", "Go"] as const
|
||||
const ranges = ["1D", "1W", "2W", "1M", "2M"] as const
|
||||
const comparisonPairIndexes = [
|
||||
[0, 1, "Top two by recent usage"],
|
||||
[0, 2, "Leader vs challenger"],
|
||||
[1, 2, "Adjacent leaderboard pair"],
|
||||
[2, 3, "Top model alternative"],
|
||||
] as const
|
||||
const statsUnfurlPath = "banner.jpg"
|
||||
const usageColors = [
|
||||
"#ed6aff",
|
||||
|
|
@ -183,11 +190,20 @@ export default function StatsHome() {
|
|||
<CacheRatioSection data={stats().cacheRatio} />
|
||||
<MarketShareSection data={stats().market} />
|
||||
<GeoBreakdownSection data={stats().country} />
|
||||
<ComparisonCardsSection
|
||||
pairs={homeComparisonPairs(stats().leaderboard["All Users"]["2M"])}
|
||||
title="Model Comparisons"
|
||||
description="Popular model pairs from the leaderboard."
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
<Footer themePreference={themePreference()} onThemePreferenceChange={updateThemePreference} />
|
||||
<Footer
|
||||
themePreference={themePreference()}
|
||||
onThemePreferenceChange={updateThemePreference}
|
||||
bridge={{ href: "#model-comparison", label: "MODEL COMPARISONS" }}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
|
|
@ -1969,6 +1985,26 @@ function formatSessionCost(value: number) {
|
|||
return `$${value.toFixed(4)}`
|
||||
}
|
||||
|
||||
function homeComparisonPairs(leaderboard: LeaderboardEntry[]) {
|
||||
return uniqueComparisonPairs(
|
||||
comparisonPairIndexes.flatMap(([firstIndex, secondIndex, detail]) => {
|
||||
const first = leaderboard[firstIndex]
|
||||
const second = leaderboard[secondIndex]
|
||||
return first && second ? [{ first: leaderboardRef(first), second: leaderboardRef(second), detail }] : []
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function leaderboardRef(entry: LeaderboardEntry): ComparisonModelRef {
|
||||
return {
|
||||
name: entry.model,
|
||||
lab: entry.provider,
|
||||
slug: modelSlug(entry.model),
|
||||
labName: entry.author,
|
||||
metric: `#${entry.rank} / ${formatBillions(entry.tokens)}`,
|
||||
}
|
||||
}
|
||||
|
||||
function modelSlug(value: string) {
|
||||
return value
|
||||
.trim()
|
||||
|
|
|
|||
|
|
@ -216,6 +216,7 @@ export function Footer(props: {
|
|||
themePreference: ThemePreference
|
||||
onThemePreferenceChange: (preference: ThemePreference) => void
|
||||
links?: readonly HeaderLink[]
|
||||
bridge?: HeaderLink | null
|
||||
}) {
|
||||
const i18n = useI18n()
|
||||
const language = useLanguage()
|
||||
|
|
@ -241,10 +242,12 @@ export function Footer(props: {
|
|||
{ href: githubLink.href, label: i18n.t("header.github") },
|
||||
{ href: "https://www.youtube.com/@anomaly-co", label: i18n.t("footer.youtube") },
|
||||
]
|
||||
const bridge = () =>
|
||||
props.bridge === undefined ? { href: "#geo-breakdown", label: i18n.t("nav.geoBreakdown").toUpperCase() } : props.bridge
|
||||
|
||||
return (
|
||||
<footer data-component="footer">
|
||||
<SectionBridge label={i18n.t("nav.geoBreakdown").toUpperCase()} href="#geo-breakdown" />
|
||||
<Show when={bridge()}>{(link) => <SectionBridge label={link().label} href={link().href} />}</Show>
|
||||
<div data-slot="footer-grid">
|
||||
<a data-slot="footer-mark" href="https://opencode.ai" aria-label={i18n.t("footer.homeAria")}>
|
||||
<OpenCodeMark />
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { Client } from "@planetscale/database"
|
||||
import { Effect } from "effect"
|
||||
import { Resource } from "sst/resource"
|
||||
import { DatabaseError } from "../database"
|
||||
import type { GeoStatMetric } from "./geo"
|
||||
import type { ModelStatMetric } from "./model"
|
||||
import { ModelStatRepo, type ModelStatMetric } from "./model"
|
||||
import type { ProviderStatMetric } from "./provider"
|
||||
|
||||
export type UsageProduct = "All Users" | "Zen" | "Go" | "Enterprise"
|
||||
|
|
@ -81,6 +82,23 @@ export type StatsLabData = {
|
|||
usage: ModelUsagePoint[]
|
||||
models: LabUsageModelEntry[]
|
||||
}
|
||||
export type StatsModelComparisonEntry = {
|
||||
updatedAt: string | null
|
||||
model: string
|
||||
slug: string
|
||||
provider: string
|
||||
author: string
|
||||
rank: number | null
|
||||
previousRank: number | null
|
||||
totalModels: number
|
||||
tokenShare: number
|
||||
tokenChange: number
|
||||
totals: StatsModelData["totals"]
|
||||
}
|
||||
export type StatsModelComparisonData = {
|
||||
updatedAt: string | null
|
||||
models: [StatsModelComparisonEntry | null, StatsModelComparisonEntry | null]
|
||||
}
|
||||
export type StatsHomeData = {
|
||||
updatedAt: string | null
|
||||
usage: Record<UsageProduct, Record<UsageRange, UsagePoint[]>>
|
||||
|
|
@ -271,6 +289,27 @@ function dateValue(value: unknown) {
|
|||
return value instanceof Date ? value : new Date(stringValue(value))
|
||||
}
|
||||
|
||||
export const getStatsModelComparisonData: (
|
||||
firstProvider: string,
|
||||
firstModel: string,
|
||||
secondProvider: string,
|
||||
secondModel: string,
|
||||
) => Effect.Effect<StatsModelComparisonData, DatabaseError, ModelStatRepo> = Effect.fn("StatsModelComparison.getData")(
|
||||
function* (firstProvider, firstModel, secondProvider, secondModel) {
|
||||
const modelStats = yield* ModelStatRepo
|
||||
const rows = yield* modelStats.listDaily()
|
||||
const first = toComparisonEntry(buildStatsModelData(firstModel, rows, [], firstProvider))
|
||||
const second = toComparisonEntry(buildStatsModelData(secondModel, rows, [], secondProvider))
|
||||
const latest = [first?.updatedAt, second?.updatedAt]
|
||||
.flatMap((value) => (value ? [dateTime(value)] : []))
|
||||
.toSorted((a, b) => b - a)[0]
|
||||
return {
|
||||
updatedAt: latest === undefined ? null : new Date(latest).toISOString(),
|
||||
models: [first, second],
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
function buildStatsHomeData(
|
||||
modelRows: ModelStatMetric[],
|
||||
providerRows: ProviderStatMetric[],
|
||||
|
|
@ -448,6 +487,23 @@ function buildStatsLabData(providerParam: string, modelRows: ModelStatMetric[]):
|
|||
}
|
||||
}
|
||||
|
||||
function toComparisonEntry(data: StatsModelData | null): StatsModelComparisonEntry | null {
|
||||
if (!data) return null
|
||||
return {
|
||||
updatedAt: data.updatedAt,
|
||||
model: data.model,
|
||||
slug: data.slug,
|
||||
provider: data.provider,
|
||||
author: data.author,
|
||||
rank: data.rank,
|
||||
previousRank: data.previousRank,
|
||||
totalModels: data.totalModels,
|
||||
tokenShare: data.tokenShare,
|
||||
tokenChange: data.tokenChange,
|
||||
totals: data.totals,
|
||||
}
|
||||
}
|
||||
|
||||
function emptyStatsHomeData(): StatsHomeData {
|
||||
return {
|
||||
updatedAt: null,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue