mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-10 04:08:30 +00:00
feat(stats): add model comparison home
This commit is contained in:
parent
5a533e366f
commit
ccd8676bee
2 changed files with 1411 additions and 79 deletions
|
|
@ -1,11 +1,16 @@
|
|||
import "../index.css"
|
||||
import { Link, Meta, Title } from "@solidjs/meta"
|
||||
import { Meta, Title } from "@solidjs/meta"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import { createAsync } from "@solidjs/router"
|
||||
import { createMemo, createSignal, onMount, Show } from "solid-js"
|
||||
import { createEffect, createMemo, createSignal, For, 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 { LocaleLinks } from "../../component/locale-links"
|
||||
import { useI18n } from "../../context/i18n"
|
||||
import { useLanguage } from "../../context/language"
|
||||
import { localizedUrl } from "../../lib/language"
|
||||
import { comparisonHref, modelRefFromCatalog, type ComparisonModelRef } from "../compare-cards"
|
||||
import { formatCatalogLabName, getModelCatalog, type ModelCatalogEntry } from "../model-catalog"
|
||||
import { setStatsPageCacheHeaders } from "../stats-cache"
|
||||
import {
|
||||
applyThemePreference,
|
||||
Footer,
|
||||
|
|
@ -17,35 +22,75 @@ import {
|
|||
type ThemePreference,
|
||||
} from "../stats-shell"
|
||||
|
||||
const compareFallbackUrl = "https://stats.opencode.ai"
|
||||
const comparePath = "/data/compare"
|
||||
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" },
|
||||
]
|
||||
"Compare AI models on key metrics including benchmarks, price, context length, and other model features."
|
||||
const statsUnfurlPath = "banner.png"
|
||||
const uptimeBars = Array.from({ length: 10 }, (_, index) => index)
|
||||
const heroLabs = [
|
||||
{ lab: "deepseek", label: "DeepSeek" },
|
||||
{ lab: "openai", label: "OpenAI" },
|
||||
{ lab: "anthropic", label: "Anthropic" },
|
||||
] as const
|
||||
const categoryTemplates = [
|
||||
{
|
||||
title: "Flagship models",
|
||||
description: "The latest top-tier model from major labs.",
|
||||
kind: "flagship",
|
||||
},
|
||||
{
|
||||
title: "Most affordable",
|
||||
description: "Compact, lower-cost picks for high-throughput use.",
|
||||
kind: "affordable",
|
||||
},
|
||||
{
|
||||
title: "Best for code",
|
||||
description: "Frequently chosen for programming tasks.",
|
||||
kind: "code",
|
||||
},
|
||||
{
|
||||
title: "Image generation",
|
||||
description: "Compare top image models on cost and quality.",
|
||||
kind: "image",
|
||||
},
|
||||
] as const
|
||||
|
||||
type CompareCategory = {
|
||||
title: string
|
||||
description: string
|
||||
first: ComparisonModelRef
|
||||
second: ComparisonModelRef
|
||||
avatars: ComparisonModelRef[]
|
||||
}
|
||||
type CompareSlot = "first" | "second"
|
||||
|
||||
export default function ModelCompareIndex() {
|
||||
const i18n = useI18n()
|
||||
const language = useLanguage()
|
||||
const event = getRequestEvent()
|
||||
event?.response.headers.set("Cache-Control", "public, max-age=60, s-maxage=300, stale-while-revalidate=86400")
|
||||
setStatsPageCacheHeaders(event?.response.headers)
|
||||
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 compareUrl = createMemo(() => localizedUrl(language.locale(), comparePath))
|
||||
const statsUnfurlUrl = new URL(statsUnfurlPath, localizedUrl("en", "/data/")).toString()
|
||||
const featuredModels = createMemo(() => (catalog()?.models ?? []).slice(0, 120))
|
||||
const pairs = createMemo(() => buildPopularPairs(featuredModels()))
|
||||
const categories = createMemo(() => buildComparisonCategories(featuredModels()))
|
||||
const compareHeaderLinks = createMemo<readonly HeaderLink[]>(() => [
|
||||
{ href: `${import.meta.env.BASE_URL}#top-models`, label: i18n.t("nav.topModels") },
|
||||
{ href: `${import.meta.env.BASE_URL}#leaderboard`, label: i18n.t("nav.leaderboard") },
|
||||
{ href: `${import.meta.env.BASE_URL}#market-share`, label: i18n.t("nav.marketShare") },
|
||||
{ href: `${import.meta.env.BASE_URL}#token-cost`, label: i18n.t("nav.tokenCost") },
|
||||
{ href: `${import.meta.env.BASE_URL}#session-cost`, label: i18n.t("nav.sessionCost") },
|
||||
])
|
||||
const compareFooterLinks = createMemo<readonly HeaderLink[]>(() => [
|
||||
{ href: `${import.meta.env.BASE_URL}#top-models`, label: i18n.t("nav.topModels") },
|
||||
{ href: `${import.meta.env.BASE_URL}#leaderboard`, label: i18n.t("nav.leaderboard") },
|
||||
{ href: `${import.meta.env.BASE_URL}#market-share`, label: i18n.t("nav.marketShare") },
|
||||
{ href: `${import.meta.env.BASE_URL}#token-cost`, label: i18n.t("nav.tokenCost") },
|
||||
{ href: `${import.meta.env.BASE_URL}#session-cost`, label: i18n.t("nav.sessionCost") },
|
||||
])
|
||||
const updateThemePreference = (preference: ThemePreference) => {
|
||||
applyThemePreference(preference)
|
||||
setThemePreference(preference)
|
||||
|
|
@ -65,84 +110,527 @@ export default function ModelCompareIndex() {
|
|||
<main data-page="stats" data-theme={themePreference()}>
|
||||
<Title>{compareTitle}</Title>
|
||||
<Meta name="description" content={compareDescription} />
|
||||
<Link rel="canonical" href={compareUrl()} />
|
||||
<LocaleLinks path={comparePath} />
|
||||
<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 property="og:image" content={statsUnfurlUrl} />
|
||||
<Meta property="og:image:type" content="image/png" />
|
||||
<Meta property="og:image:width" content="1200" />
|
||||
<Meta property="og:image:height" content="630" />
|
||||
<Meta property="og:image:alt" content={i18n.t("app.unfurlAlt")} />
|
||||
<Meta name="twitter:card" content="summary_large_image" />
|
||||
<Meta name="twitter:title" content={compareTitle} />
|
||||
<Meta name="twitter:description" content={compareDescription} />
|
||||
<Header githubStars={githubStars() ?? "150K"} links={compareHeaderLinks} brandHref={import.meta.env.BASE_URL} />
|
||||
<Meta name="twitter:image" content={statsUnfurlUrl} />
|
||||
<Meta name="twitter:image:alt" content={i18n.t("app.unfurlAlt")} />
|
||||
<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>
|
||||
<section id="compare-tool" data-section="compare-home-hero">
|
||||
<nav data-component="compare-home-breadcrumb" aria-label="Breadcrumb">
|
||||
<a data-slot="compare-home-crumb" href={language.route(import.meta.env.BASE_URL)}>
|
||||
Data
|
||||
</a>
|
||||
<span data-slot="compare-home-separator">/</span>
|
||||
<span data-slot="compare-home-crumb" data-current="true">
|
||||
Compare
|
||||
</span>
|
||||
</nav>
|
||||
<div data-slot="compare-home-hero-grid">
|
||||
<h1 aria-label="Compare AI models">
|
||||
<span>Compare</span>
|
||||
<HeroModelStack />
|
||||
<span>AI models</span>
|
||||
</h1>
|
||||
<p>{compareDescription}</p>
|
||||
</div>
|
||||
<div data-slot="model-hero-pattern" aria-hidden="true" />
|
||||
<div data-slot="compare-home-pattern" aria-hidden="true" />
|
||||
</section>
|
||||
<section data-section="compare-home-selector" aria-label="Choose models to compare">
|
||||
<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>
|
||||
}
|
||||
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()} />
|
||||
<CompareHomeSelector models={featuredModels()} />
|
||||
</Show>
|
||||
</section>
|
||||
<ComparisonCardsSection
|
||||
pairs={pairs()}
|
||||
title="Popular Model Comparisons"
|
||||
description="Common model pairs to start with."
|
||||
/>
|
||||
<Show when={categories().length > 0}>
|
||||
<section id="model-comparison" data-section="compare-home-related">
|
||||
<p data-slot="section-title">
|
||||
<strong>Related comparisons.</strong> <span>Other model pairs to check.</span>
|
||||
</p>
|
||||
<div data-component="compare-home-card-grid">
|
||||
<For each={categories()}>{(category) => <CompareHomeCard category={category} />}</For>
|
||||
</div>
|
||||
</section>
|
||||
</Show>
|
||||
</div>
|
||||
<Footer
|
||||
themePreference={themePreference()}
|
||||
onThemePreferenceChange={updateThemePreference}
|
||||
links={compareFooterLinks}
|
||||
bridge={{ href: "#compare-tool", label: "MODEL COMPARE" }}
|
||||
links={compareFooterLinks()}
|
||||
bridge={null}
|
||||
/>
|
||||
</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 CompareHomeSelector(props: { models: ModelCatalogEntry[] }) {
|
||||
const [firstId, setFirstId] = createSignal("")
|
||||
const [secondId, setSecondId] = createSignal("")
|
||||
const [activeSlot, setActiveSlot] = createSignal<CompareSlot>()
|
||||
const modelById = createMemo(() => new Map(props.models.map((model) => [model.id, model])))
|
||||
const first = createMemo(() => modelById().get(firstId()))
|
||||
const second = createMemo(() => modelById().get(secondId()))
|
||||
const activeSelected = createMemo(() => (activeSlot() === "first" ? first() : second()))
|
||||
const activeBlockedId = createMemo(() => (activeSlot() === "first" ? secondId() : firstId()))
|
||||
const href = createMemo(() => {
|
||||
const firstModel = first()
|
||||
const secondModel = second()
|
||||
if (!firstModel || !secondModel || firstModel.id === secondModel.id) return undefined
|
||||
return comparisonHref(modelRefFromCatalog(firstModel), modelRefFromCatalog(secondModel))
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!secondId() || secondId() !== firstId()) return
|
||||
setSecondId("")
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const url = href()
|
||||
if (!url || typeof window === "undefined") return
|
||||
window.location.href = url
|
||||
})
|
||||
|
||||
return (
|
||||
<form
|
||||
data-component="compare-home-selector"
|
||||
aria-label="Model comparison selector"
|
||||
onSubmit={(event) => event.preventDefault()}
|
||||
>
|
||||
<CompareHomeSelect
|
||||
selected={first()}
|
||||
label="First model"
|
||||
expanded={activeSlot() === "first"}
|
||||
onOpen={() => setActiveSlot("first")}
|
||||
/>
|
||||
<CompareHomeSelect
|
||||
selected={second()}
|
||||
label="Second model"
|
||||
expanded={activeSlot() === "second"}
|
||||
onOpen={() => setActiveSlot("second")}
|
||||
/>
|
||||
<Show when={activeSlot()}>
|
||||
{(slot) => (
|
||||
<CompareModelSelectModal
|
||||
models={props.models}
|
||||
selected={activeSelected()}
|
||||
blockedId={activeBlockedId()}
|
||||
label={slot() === "first" ? "Choose first model" : "Choose second model"}
|
||||
onClose={() => setActiveSlot(undefined)}
|
||||
onSelect={(model) => {
|
||||
if (slot() === "first") setFirstId(model.id)
|
||||
if (slot() === "second") setSecondId(model.id)
|
||||
setActiveSlot(undefined)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
function formatPairCount(count: number) {
|
||||
if (count < 2) return "0"
|
||||
return new Intl.NumberFormat("en", { notation: "compact", maximumFractionDigits: 1 }).format((count * (count - 1)) / 2)
|
||||
function CompareHomeSelect(props: {
|
||||
selected: ModelCatalogEntry | undefined
|
||||
label: string
|
||||
expanded: boolean
|
||||
onOpen: () => void
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
data-slot="compare-home-select-panel"
|
||||
data-selected={props.selected ? "true" : undefined}
|
||||
aria-label={props.label}
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded={props.expanded}
|
||||
onClick={props.onOpen}
|
||||
>
|
||||
<span data-slot="compare-home-select-copy">
|
||||
<span data-slot="compare-home-plus" aria-hidden="true">
|
||||
+
|
||||
</span>
|
||||
<Show when={props.selected}>{(model) => <ModelAvatar model={model()} size="tiny" />}</Show>
|
||||
<span data-slot="compare-home-select-name">{props.selected?.name ?? "Select a model"}</span>
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function CompareModelSelectModal(props: {
|
||||
models: ModelCatalogEntry[]
|
||||
selected: ModelCatalogEntry | undefined
|
||||
blockedId: string
|
||||
label: string
|
||||
onClose: () => void
|
||||
onSelect: (model: ModelCatalogEntry) => void
|
||||
}) {
|
||||
let searchInput: HTMLInputElement | undefined
|
||||
const [search, setSearch] = createSignal("")
|
||||
const [previewId, setPreviewId] = createSignal(props.selected?.id ?? "")
|
||||
const availableModels = createMemo(() => props.models.filter((model) => model.id !== props.blockedId))
|
||||
const filteredModels = createMemo(() => {
|
||||
const terms = search().trim().toLowerCase().split(/\s+/).filter(Boolean)
|
||||
if (terms.length === 0) return availableModels()
|
||||
return availableModels().filter((model) => terms.every((term) => modelSearchText(model).includes(term)))
|
||||
})
|
||||
const preview = createMemo(
|
||||
() =>
|
||||
filteredModels().find((model) => model.id === previewId()) ??
|
||||
filteredModels()[0] ??
|
||||
availableModels()[0],
|
||||
)
|
||||
|
||||
createEffect(() => {
|
||||
const selected = props.selected
|
||||
const fallback = selected && selected.id !== props.blockedId ? selected.id : filteredModels()[0]?.id
|
||||
if (!fallback) return
|
||||
if (availableModels().some((model) => model.id === previewId())) return
|
||||
setPreviewId(fallback)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const models = filteredModels()
|
||||
if (models.some((model) => model.id === previewId())) return
|
||||
setPreviewId(models[0]?.id ?? "")
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (typeof window === "undefined") return
|
||||
window.requestAnimationFrame(() => searchInput?.focus())
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
data-component="compare-model-modal-scrim"
|
||||
role="presentation"
|
||||
onClick={props.onClose}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== "Escape") return
|
||||
props.onClose()
|
||||
}}
|
||||
>
|
||||
<div
|
||||
data-component="compare-model-modal"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={props.label}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div data-slot="compare-model-modal-list">
|
||||
<label data-slot="compare-model-modal-search">
|
||||
<span data-slot="compare-model-modal-search-icon" aria-hidden="true" />
|
||||
<input
|
||||
ref={searchInput}
|
||||
value={search()}
|
||||
placeholder="Search models"
|
||||
aria-label="Search models"
|
||||
onInput={(event) => setSearch(event.currentTarget.value)}
|
||||
/>
|
||||
</label>
|
||||
<div data-slot="compare-model-modal-results">
|
||||
<Show
|
||||
when={filteredModels().length > 0}
|
||||
fallback={
|
||||
<div data-slot="compare-model-modal-empty">
|
||||
<strong>No models found</strong>
|
||||
<span>Try another search.</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<For each={filteredModels()}>
|
||||
{(model) => (
|
||||
<button
|
||||
type="button"
|
||||
data-slot="compare-model-modal-row"
|
||||
data-active={preview()?.id === model.id ? "true" : undefined}
|
||||
onPointerEnter={() => setPreviewId(model.id)}
|
||||
onFocus={() => setPreviewId(model.id)}
|
||||
onClick={() => props.onSelect(model)}
|
||||
>
|
||||
<span data-slot="compare-model-modal-row-main">
|
||||
<ModelAvatar model={model} size="tiny" />
|
||||
<span>{model.name}</span>
|
||||
</span>
|
||||
<Show when={isFreeModel(model)}>
|
||||
<span data-slot="compare-model-modal-badge">Free</span>
|
||||
</Show>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<div data-slot="compare-model-modal-divider" aria-hidden="true" />
|
||||
<Show when={preview()}>{(model) => <CompareModelDetail model={model()} />}</Show>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CompareModelDetail(props: { model: ModelCatalogEntry }) {
|
||||
return (
|
||||
<aside data-slot="compare-model-modal-detail">
|
||||
<header data-slot="compare-model-modal-detail-header">
|
||||
<ModelAvatar model={props.model} size="small" />
|
||||
<strong>{props.model.name}</strong>
|
||||
</header>
|
||||
<div data-slot="compare-model-modal-description">
|
||||
<p>{props.model.description ?? `${props.model.name} is an AI model from ${formatCatalogLabName(props.model.lab)}.`}</p>
|
||||
<span aria-hidden="true" />
|
||||
</div>
|
||||
<dl data-slot="compare-model-modal-facts">
|
||||
<CompareModelFact label="Release" value={formatCatalogDate(props.model.releaseDate)} />
|
||||
<CompareModelFact label="Context" value={formatCatalogLimit(props.model.limit?.context)} />
|
||||
<CompareModelFact label="Input" value={formatCatalogUnitPrice(props.model.cost?.input)} />
|
||||
<CompareModelFact label="Output" value={formatCatalogUnitPrice(props.model.cost?.output)} />
|
||||
<div data-slot="compare-model-modal-fact">
|
||||
<dt>Uptime</dt>
|
||||
<dd data-slot="compare-model-modal-uptime">
|
||||
<For each={uptimeBars}>{(bar) => <span data-active={bar < 7 ? "true" : undefined} />}</For>
|
||||
</dd>
|
||||
</div>
|
||||
<div data-slot="compare-model-modal-fact">
|
||||
<dt>URL</dt>
|
||||
<dd>
|
||||
<a href={modelHref(props.model)}>{formatModelUrl(props.model)}</a>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
function CompareModelFact(props: { label: string; value: string }) {
|
||||
return (
|
||||
<div data-slot="compare-model-modal-fact">
|
||||
<dt>{props.label}</dt>
|
||||
<dd>{props.value}</dd>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function HeroModelStack() {
|
||||
return (
|
||||
<span data-slot="compare-home-avatar-stack" aria-hidden="true">
|
||||
<For each={heroLabs}>
|
||||
{(lab) => (
|
||||
<span data-slot="compare-home-avatar-frame">
|
||||
<LabLogo lab={lab.lab} label={lab.label} size="large" />
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function CompareHomeCard(props: { category: CompareCategory }) {
|
||||
return (
|
||||
<a
|
||||
data-component="compare-home-card"
|
||||
href={comparisonHref(props.category.first, props.category.second)}
|
||||
aria-label={`${props.category.title}: ${props.category.first.name} vs ${props.category.second.name}`}
|
||||
>
|
||||
<span data-slot="compare-home-card-head">
|
||||
<span>
|
||||
<strong>{props.category.title}</strong>
|
||||
<em>{props.category.description}</em>
|
||||
</span>
|
||||
<b aria-hidden="true" />
|
||||
</span>
|
||||
<span data-slot="compare-home-card-divider" aria-hidden="true" />
|
||||
<span data-slot="compare-home-card-models">
|
||||
<span>{props.category.first.name}</span>
|
||||
<i aria-hidden="true">·</i>
|
||||
<span>{props.category.second.name}</span>
|
||||
</span>
|
||||
<span data-slot="compare-home-card-avatars" aria-hidden="true">
|
||||
<For each={props.category.avatars}>{(model) => <LabLogo lab={model.lab} label={model.name} size="small" />}</For>
|
||||
</span>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
function ModelAvatar(props: { model: ModelCatalogEntry; size: "large" | "small" | "tiny" }) {
|
||||
return <LabLogo lab={props.model.lab} label={props.model.name} size={props.size} />
|
||||
}
|
||||
|
||||
function LabLogo(props: { lab: string; label: string; size: "large" | "small" | "tiny" }) {
|
||||
const iconId = () => getProviderIconId(props.lab)
|
||||
|
||||
return (
|
||||
<span data-slot="compare-home-avatar" data-lab={iconId()} data-size={props.size} aria-label={props.label}>
|
||||
<ProviderIcon aria-hidden="true" id={iconId()} />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function buildComparisonCategories(models: ModelCatalogEntry[]): CompareCategory[] {
|
||||
return categoryTemplates.reduce<{ keys: Set<string>; categories: CompareCategory[] }>((result, template, index) => {
|
||||
const candidates = categoryCandidates(template.kind, models)
|
||||
const pair = categoryPair(candidates, models, index, result.keys)
|
||||
if (!pair) return result
|
||||
result.keys.add(comparisonKey(pair.first, pair.second))
|
||||
const first = modelRefFromCatalog(pair.first)
|
||||
const second = modelRefFromCatalog(pair.second)
|
||||
result.categories.push({
|
||||
title: template.title,
|
||||
description: template.description,
|
||||
first,
|
||||
second,
|
||||
avatars: [first, second],
|
||||
})
|
||||
return result
|
||||
}, { keys: new Set(), categories: [] }).categories
|
||||
}
|
||||
|
||||
function categoryPair(
|
||||
candidates: ModelCatalogEntry[],
|
||||
fallback: ModelCatalogEntry[],
|
||||
offset: number,
|
||||
usedKeys: Set<string>,
|
||||
) {
|
||||
const pool = uniqueModels([...candidates, ...fallback.slice(offset), ...fallback])
|
||||
return pool
|
||||
.flatMap((first, firstIndex) =>
|
||||
pool.slice(firstIndex + 1).map((second) => ({
|
||||
first,
|
||||
second,
|
||||
key: comparisonKey(first, second),
|
||||
})),
|
||||
)
|
||||
.find((pair) => !usedKeys.has(pair.key))
|
||||
}
|
||||
|
||||
function categoryCandidates(kind: (typeof categoryTemplates)[number]["kind"], models: ModelCatalogEntry[]) {
|
||||
if (kind === "affordable")
|
||||
return models
|
||||
.filter((model) => model.cost)
|
||||
.toSorted((a, b) => modelCost(a) - modelCost(b) || displayDateTime(b.releaseDate) - displayDateTime(a.releaseDate))
|
||||
if (kind === "code")
|
||||
return models.filter((model) => model.toolCall || model.reasoning).toSorted(recentModelSort)
|
||||
if (kind === "image")
|
||||
return models
|
||||
.filter((model) => model.modalities.output.some((modality) => modality.toLowerCase().includes("image")))
|
||||
.toSorted(recentModelSort)
|
||||
return models.toSorted(recentModelSort)
|
||||
}
|
||||
|
||||
function uniqueModels(models: (ModelCatalogEntry | undefined)[]) {
|
||||
return models.reduce<{ keys: Set<string>; models: ModelCatalogEntry[] }>(
|
||||
(result, model) => {
|
||||
if (!model || result.keys.has(model.id)) return result
|
||||
result.keys.add(model.id)
|
||||
result.models.push(model)
|
||||
return result
|
||||
},
|
||||
{ keys: new Set(), models: [] },
|
||||
).models
|
||||
}
|
||||
|
||||
function recentModelSort(a: ModelCatalogEntry, b: ModelCatalogEntry) {
|
||||
return displayDateTime(b.releaseDate) - displayDateTime(a.releaseDate) || a.name.localeCompare(b.name)
|
||||
}
|
||||
|
||||
function comparisonKey(first: ModelCatalogEntry, second: ModelCatalogEntry) {
|
||||
return [first.id, second.id].toSorted().join("|")
|
||||
}
|
||||
|
||||
function modelCost(model: ModelCatalogEntry) {
|
||||
return (model.cost?.input ?? Number.POSITIVE_INFINITY) + (model.cost?.output ?? Number.POSITIVE_INFINITY)
|
||||
}
|
||||
|
||||
function modelSearchText(model: ModelCatalogEntry) {
|
||||
return `${model.name} ${formatCatalogLabName(model.lab)} ${model.id}`.toLowerCase()
|
||||
}
|
||||
|
||||
function isFreeModel(model: ModelCatalogEntry) {
|
||||
return model.cost?.input === 0 && model.cost.output === 0
|
||||
}
|
||||
|
||||
function modelHref(model: ModelCatalogEntry) {
|
||||
return `${import.meta.env.BASE_URL}${model.lab}/${model.slug}`
|
||||
}
|
||||
|
||||
function formatModelUrl(model: ModelCatalogEntry) {
|
||||
return `.../${model.slug}`
|
||||
}
|
||||
|
||||
function formatCatalogLimit(value: number | undefined) {
|
||||
return value === undefined ? "Unknown" : formatTokens(value)
|
||||
}
|
||||
|
||||
function formatCatalogUnitPrice(value: number | undefined) {
|
||||
if (value === undefined) return "Unknown"
|
||||
return `${formatModelPrice(value)} / 1M`
|
||||
}
|
||||
|
||||
function formatModelPrice(value: number) {
|
||||
if (value > 0 && value < 0.01) return `$${value.toFixed(4)}`
|
||||
return formatMoney(value)
|
||||
}
|
||||
|
||||
function formatMoney(value: number) {
|
||||
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 `$${value.toFixed(value >= 10 ? 0 : 2)}`
|
||||
}
|
||||
|
||||
function formatCatalogDate(value: string | undefined) {
|
||||
if (!value) return "Unknown"
|
||||
const match = /^(\d{4})(?:-(\d{2}))?(?:-(\d{2}))?$/.exec(value)
|
||||
if (!match) return value
|
||||
return new Intl.DateTimeFormat("en", {
|
||||
month: match[2] ? "short" : undefined,
|
||||
day: match[3] ? "numeric" : undefined,
|
||||
year: "numeric",
|
||||
timeZone: "UTC",
|
||||
}).format(new Date(Date.UTC(Number(match[1]), match[2] ? Number(match[2]) - 1 : 0, match[3] ? Number(match[3]) : 1)))
|
||||
}
|
||||
|
||||
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 trimNumber(value: number, digits: number) {
|
||||
return Number(value.toFixed(digits)).toLocaleString("en")
|
||||
}
|
||||
|
||||
function displayDateTime(value: string | undefined) {
|
||||
if (!value) return 0
|
||||
const date = new Date(value)
|
||||
if (!Number.isNaN(date.getTime())) return date.getTime()
|
||||
const year = Number(value.match(/\d{4}/)?.[0] ?? 0)
|
||||
return Number.isFinite(year) ? year : 0
|
||||
}
|
||||
|
||||
function getProviderIconId(provider: string) {
|
||||
const id = provider.toLowerCase().replace(/[^a-z0-9]+/g, "")
|
||||
if (id === "moonshot") return "moonshotai"
|
||||
if (id === "zhipu") return "zhipuai"
|
||||
return id
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5489,6 +5489,742 @@
|
|||
background: var(--stats-accent);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-section="compare-home-hero"],
|
||||
[data-page="stats"] [data-section="compare-home-selector"],
|
||||
[data-page="stats"] [data-section="compare-home-related"] {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
color: var(--stats-text);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-section="compare-home-hero"] {
|
||||
display: grid;
|
||||
align-content: end;
|
||||
gap: 24px;
|
||||
min-height: 316px;
|
||||
padding: 128px 40px 40px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="compare-home-breadcrumb"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 24px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-home-crumb"] {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
box-sizing: border-box;
|
||||
padding: 0 8px;
|
||||
color: var(--stats-hero-muted);
|
||||
background: var(--stats-layer-2);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 1.1;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-page="stats"] a[data-slot="compare-home-crumb"]:hover,
|
||||
[data-page="stats"] a[data-slot="compare-home-crumb"]:focus-visible {
|
||||
color: var(--stats-text);
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-home-separator"] {
|
||||
color: var(--stats-faint);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-home-hero-grid"] {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(320px, 519px);
|
||||
gap: 24px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-section="compare-home-hero"] h1 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
color: var(--stats-text);
|
||||
font-size: 40px;
|
||||
font-weight: 500;
|
||||
line-height: 60px;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-home-avatar-stack"] {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 46px;
|
||||
padding-inline: 2px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-home-avatar-frame"] {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
padding: 3px;
|
||||
background: var(--stats-bg);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-home-avatar-frame"] + [data-slot="compare-home-avatar-frame"] {
|
||||
margin-left: -16px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-home-avatar"] {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex: 0 0 auto;
|
||||
color: #ffffff;
|
||||
background: var(--stats-accent-text);
|
||||
box-shadow: inset 0 0 0 1px #0000001a;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-home-avatar"][data-lab="deepseek"] {
|
||||
background: #5d6ff4;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-home-avatar"][data-lab="openai"] {
|
||||
background: #252525;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-home-avatar"][data-lab="anthropic"] {
|
||||
background: #c99b75;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-home-avatar"][data-size="large"] {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-home-avatar"][data-size="large"] svg {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-home-avatar"][data-size="small"] {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-home-avatar"][data-size="small"] svg {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-home-avatar"][data-size="tiny"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-home-avatar"][data-size="tiny"] svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-home-hero-grid"] > p {
|
||||
max-width: 520px;
|
||||
margin: 0;
|
||||
color: var(--stats-muted);
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-home-pattern"] {
|
||||
width: calc(100% + 80px);
|
||||
height: 16px;
|
||||
margin-inline: -40px;
|
||||
overflow: hidden;
|
||||
background: color-mix(in srgb, var(--stats-text) 10%, transparent);
|
||||
mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0H2V2H0V0Z' fill='black'/%3E%3C/svg%3E");
|
||||
mask-position: center top;
|
||||
mask-repeat: repeat;
|
||||
mask-size: 6px 6px;
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0H2V2H0V0Z' fill='black'/%3E%3C/svg%3E");
|
||||
-webkit-mask-position: center top;
|
||||
-webkit-mask-repeat: repeat;
|
||||
-webkit-mask-size: 6px 6px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-section="compare-home-selector"] {
|
||||
min-height: 98px;
|
||||
border-top: 1px solid var(--stats-line);
|
||||
border-bottom: 1px solid var(--stats-line);
|
||||
box-shadow:
|
||||
inset 1px 0 var(--stats-line),
|
||||
inset -1px 0 var(--stats-line);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="compare-home-selector"] {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
min-height: 96px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="compare-home-selector"] button[data-slot="compare-home-select-panel"] {
|
||||
position: relative;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-width: 0;
|
||||
min-height: 96px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
appearance: none;
|
||||
border-radius: 0;
|
||||
color: var(--stats-text);
|
||||
background: var(--stats-bg);
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
text-align: inherit;
|
||||
}
|
||||
|
||||
[data-page="stats"]
|
||||
[data-component="compare-home-selector"]
|
||||
button[data-slot="compare-home-select-panel"]:first-child::before,
|
||||
[data-page="stats"]
|
||||
[data-component="compare-home-selector"]
|
||||
button[data-slot="compare-home-select-panel"]:last-child::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
content: "";
|
||||
background: var(--stats-line);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
[data-page="stats"]
|
||||
[data-component="compare-home-selector"]
|
||||
button[data-slot="compare-home-select-panel"]:first-child::before {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
[data-page="stats"]
|
||||
[data-component="compare-home-selector"]
|
||||
button[data-slot="compare-home-select-panel"]:last-child::after {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
[data-page="stats"]
|
||||
[data-component="compare-home-selector"]
|
||||
button[data-slot="compare-home-select-panel"]
|
||||
+ button[data-slot="compare-home-select-panel"] {
|
||||
box-shadow: inset 1px 0 var(--stats-line);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="compare-home-selector"] button[data-slot="compare-home-select-panel"]:hover,
|
||||
[data-page="stats"] [data-component="compare-home-selector"] button[data-slot="compare-home-select-panel"]:focus-within {
|
||||
background: var(--stats-layer);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="compare-home-selector"] button[data-slot="compare-home-select-panel"]:focus-visible {
|
||||
z-index: 1;
|
||||
outline: 1px solid var(--stats-line-strong);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-home-select-copy"] {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
max-width: calc(100% - 48px);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-home-plus"],
|
||||
[data-page="stats"] [data-slot="compare-home-select-name"] {
|
||||
min-width: 0;
|
||||
color: inherit;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-home-select-name"] {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-home-select-panel"][data-selected="true"] [data-slot="compare-home-plus"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-section="compare-home-related"] {
|
||||
min-height: 425px;
|
||||
margin-top: 40px;
|
||||
padding: 80px 40px;
|
||||
box-shadow:
|
||||
inset 0 1px var(--stats-line),
|
||||
inset 0 -1px var(--stats-line),
|
||||
inset 1px 0 var(--stats-line),
|
||||
inset -1px 0 var(--stats-line);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-section="compare-home-related"] [data-slot="section-title"] {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="compare-home-card-grid"] {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="compare-home-card"] {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-rows: 62px 1px 20px 20px;
|
||||
gap: 16px;
|
||||
min-width: 0;
|
||||
height: 184px;
|
||||
box-sizing: border-box;
|
||||
padding: 16px;
|
||||
overflow: hidden;
|
||||
color: var(--stats-text);
|
||||
background: var(--stats-layer);
|
||||
border: 1px solid var(--stats-line);
|
||||
box-shadow: 0 1px 2px #00000008;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="compare-home-card"]:hover,
|
||||
[data-page="stats"] [data-component="compare-home-card"]:focus-visible {
|
||||
border-color: var(--stats-line-strong);
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-home-card-head"] {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 32px;
|
||||
gap: 16px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-home-card-head"] > span {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-home-card-head"] strong,
|
||||
[data-page="stats"] [data-slot="compare-home-card-head"] em,
|
||||
[data-page="stats"] [data-slot="compare-home-card-models"] {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-home-card-head"] strong {
|
||||
color: var(--stats-text);
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-home-card-head"] em {
|
||||
display: -webkit-box;
|
||||
color: var(--stats-muted);
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-home-card-head"] b {
|
||||
position: relative;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--stats-line);
|
||||
background: var(--stats-bg);
|
||||
box-shadow: 0 1px 1.5px #0000000f;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-home-card-head"] b::before {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
content: "";
|
||||
background: currentColor;
|
||||
box-shadow:
|
||||
7px 0 currentColor,
|
||||
0 7px currentColor;
|
||||
color: var(--stats-muted);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-home-card-head"] b::after {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
bottom: 5px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
box-sizing: border-box;
|
||||
content: "";
|
||||
background:
|
||||
radial-gradient(circle at 4px 4px, transparent 2.5px, var(--stats-muted) 2.75px 4px, transparent 4.25px),
|
||||
linear-gradient(var(--stats-muted) 0 0) 7px 8px / 5px 1.5px no-repeat;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-home-card-divider"] {
|
||||
height: 1px;
|
||||
min-width: 0;
|
||||
background: var(--stats-line);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-home-card-models"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--stats-muted);
|
||||
font-weight: 400;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-home-card-models"] span {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-home-card-models"] i {
|
||||
flex: 0 0 auto;
|
||||
color: var(--stats-faint);
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-home-card-avatars"] {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="compare-model-modal-scrim"] {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
box-sizing: border-box;
|
||||
padding: 24px;
|
||||
background: rgb(0 0 0 / 40%);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="compare-model-modal"] {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 440px) 1px minmax(0, 279px);
|
||||
width: min(720px, calc(100vw - 48px));
|
||||
height: min(480px, calc(100vh - 48px));
|
||||
overflow: hidden;
|
||||
color: var(--stats-text);
|
||||
background: var(--stats-bg);
|
||||
box-shadow:
|
||||
0 0 0 0.5px rgb(0 0 0 / 12%),
|
||||
0 8px 16px rgb(0 0 0 / 8%),
|
||||
0 16px 32px rgb(0 0 0 / 4%);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-model-modal-list"] {
|
||||
display: grid;
|
||||
grid-template-rows: 52px minmax(0, 1fr);
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
background: var(--stats-bg);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-model-modal-search"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
box-sizing: border-box;
|
||||
height: 52px;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--stats-line);
|
||||
color: var(--stats-muted);
|
||||
background: var(--stats-bg);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-model-modal-search-icon"] {
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--stats-faint);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-model-modal-search-icon"]::before {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border: 1.5px solid currentColor;
|
||||
border-radius: 999px;
|
||||
content: "";
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-model-modal-search-icon"]::after {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 9px;
|
||||
width: 5px;
|
||||
height: 1.5px;
|
||||
background: currentColor;
|
||||
content: "";
|
||||
transform: rotate(45deg);
|
||||
transform-origin: left center;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-model-modal-search"] input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
color: var(--stats-text);
|
||||
background: transparent;
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-model-modal-search"] input::placeholder {
|
||||
color: var(--stats-faint);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-model-modal-results"] {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
background: var(--stats-layer);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="compare-model-modal"] button[data-slot="compare-model-modal-row"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
height: 49px;
|
||||
box-sizing: border-box;
|
||||
padding: 0 16px;
|
||||
border: 0;
|
||||
border-bottom: 1px solid var(--stats-line);
|
||||
appearance: none;
|
||||
border-radius: 0;
|
||||
color: var(--stats-text);
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="compare-model-modal"] button[data-slot="compare-model-modal-row"]:hover,
|
||||
[data-page="stats"] [data-component="compare-model-modal"] button[data-slot="compare-model-modal-row"]:focus-visible,
|
||||
[data-page="stats"] [data-component="compare-model-modal"] button[data-slot="compare-model-modal-row"][data-active="true"] {
|
||||
color: var(--stats-text);
|
||||
background: var(--stats-layer-2);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-model-modal-row-main"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1 1 auto;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-model-modal-row-main"] > span:last-child {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: var(--stats-text);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 1.1;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-model-modal-badge"] {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
height: 16px;
|
||||
padding: 0 6px;
|
||||
color: var(--stats-muted);
|
||||
background: var(--stats-layer-2);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-model-modal-empty"] {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 24px 16px;
|
||||
color: var(--stats-muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-model-modal-empty"] strong {
|
||||
color: var(--stats-text);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-model-modal-divider"] {
|
||||
width: 1px;
|
||||
min-height: 0;
|
||||
background: var(--stats-line);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-model-modal-detail"] {
|
||||
display: grid;
|
||||
grid-template-rows: 52px minmax(0, 1fr) auto;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
background: var(--stats-bg);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-model-modal-detail-header"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
box-sizing: border-box;
|
||||
height: 52px;
|
||||
padding: 16px;
|
||||
background: var(--stats-bg);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-model-modal-detail-header"] strong {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: var(--stats-text);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 1.1;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-model-modal-description"] {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
padding: 0 16px 16px;
|
||||
overflow: hidden;
|
||||
background: var(--stats-bg);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-model-modal-description"] p {
|
||||
margin: 0;
|
||||
color: var(--stats-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-model-modal-description"] > span {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 32px;
|
||||
background: linear-gradient(to bottom, transparent, var(--stats-bg));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-model-modal-facts"] {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
overflow: hidden;
|
||||
background: var(--stats-bg);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-model-modal-fact"] {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 16px;
|
||||
min-width: 0;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--stats-line);
|
||||
color: var(--stats-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-model-modal-fact"]:last-child {
|
||||
padding-bottom: 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-model-modal-fact"] dt,
|
||||
[data-page="stats"] [data-slot="compare-model-modal-fact"] dd {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-model-modal-fact"] dd {
|
||||
color: var(--stats-text);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-model-modal-fact"] a {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: color-mix(in srgb, var(--stats-text) 20%, transparent);
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-model-modal-uptime"] {
|
||||
display: inline-flex;
|
||||
justify-content: flex-end;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-model-modal-uptime"] span {
|
||||
width: 3px;
|
||||
height: 12px;
|
||||
background: var(--stats-layer-2);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-model-modal-uptime"] span[data-active="true"] {
|
||||
background: #198b43;
|
||||
}
|
||||
|
||||
[data-page="stats"][data-theme="dark"],
|
||||
:root[data-stats-theme="dark"] [data-page="stats"]:not([data-theme="light"]) {
|
||||
color-scheme: dark;
|
||||
|
|
@ -5886,10 +6622,22 @@
|
|||
[data-page="stats"] [data-section="model-hero"],
|
||||
[data-page="stats"] [data-section="lab-hero"],
|
||||
[data-page="stats"] [data-section="model-momentum"],
|
||||
[data-page="stats"] [data-section="model-panel"] {
|
||||
[data-page="stats"] [data-section="model-panel"],
|
||||
[data-page="stats"] [data-section="compare-home-hero"],
|
||||
[data-page="stats"] [data-section="compare-home-related"] {
|
||||
padding: 64px 32px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-section="compare-home-hero"] {
|
||||
min-height: 280px;
|
||||
padding: 104px 32px 40px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-home-pattern"] {
|
||||
width: calc(100% + 64px);
|
||||
margin-inline: -32px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-section="lab-hero"] {
|
||||
min-height: 280px;
|
||||
padding: 104px 32px 40px;
|
||||
|
|
@ -5943,6 +6691,15 @@
|
|||
align-items: start;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-home-hero-grid"] {
|
||||
grid-template-columns: 1fr;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="compare-home-card-grid"] {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-section="lab-overview"] {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
|
@ -6165,10 +6922,87 @@
|
|||
[data-page="stats"] [data-section="model-hero"],
|
||||
[data-page="stats"] [data-section="lab-hero"],
|
||||
[data-page="stats"] [data-section="model-momentum"],
|
||||
[data-page="stats"] [data-section="model-panel"] {
|
||||
[data-page="stats"] [data-section="model-panel"],
|
||||
[data-page="stats"] [data-section="compare-home-hero"],
|
||||
[data-page="stats"] [data-section="compare-home-related"] {
|
||||
padding: 48px 24px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-section="compare-home-hero"] {
|
||||
gap: 20px;
|
||||
min-height: 316px;
|
||||
padding: 72px 24px 40px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-home-pattern"] {
|
||||
width: calc(100% + 48px);
|
||||
margin-inline: -24px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-section="compare-home-hero"] h1 {
|
||||
gap: 12px;
|
||||
font-size: 32px;
|
||||
line-height: 42px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="compare-home-breadcrumb"] {
|
||||
flex-wrap: wrap;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="compare-home-selector"] {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="compare-home-selector"] button[data-slot="compare-home-select-panel"]::before,
|
||||
[data-page="stats"] [data-component="compare-home-selector"] button[data-slot="compare-home-select-panel"]::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
content: "";
|
||||
background: var(--stats-line);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="compare-home-selector"] button[data-slot="compare-home-select-panel"]::before {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="compare-home-selector"] button[data-slot="compare-home-select-panel"]::after {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="compare-model-modal-scrim"] {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="compare-model-modal"] {
|
||||
grid-template-columns: 1fr;
|
||||
width: calc(100vw - 32px);
|
||||
height: min(520px, calc(100vh - 32px));
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-model-modal-detail"],
|
||||
[data-page="stats"] [data-slot="compare-model-modal-divider"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-page="stats"]
|
||||
[data-component="compare-home-selector"]
|
||||
button[data-slot="compare-home-select-panel"]
|
||||
+ button[data-slot="compare-home-select-panel"] {
|
||||
box-shadow: inset 0 1px var(--stats-line);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-section="compare-home-related"] {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="compare-home-card-grid"] {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="top-models-title"],
|
||||
[data-page="stats"] [data-slot="section-title"] {
|
||||
margin-bottom: 32px;
|
||||
|
|
@ -6860,6 +7694,16 @@
|
|||
}
|
||||
|
||||
@media (max-width: 40rem) {
|
||||
[data-page="stats"] [data-section="compare-home-hero"] h1 {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="compare-home-hero-grid"] > p {
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="footer-grid"] {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue