|
|
@@ -1,10 +1,7 @@
|
|
|
import { Link, Meta, Title } from "@solidjs/meta"
|
|
|
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
|
|
-import { geoEquirectangular, geoPath } from "d3-geo"
|
|
|
import { scaleSqrt } from "d3-scale"
|
|
|
import countryCodesSource from "i18n-iso-countries/codes.json?raw"
|
|
|
-import { feature, mesh } from "topojson-client"
|
|
|
-import countriesTopologySource from "world-atlas/countries-50m.json?raw"
|
|
|
import ibmPlexMonoRegularLatin1 from "@ibm/plex/IBM-Plex-Mono/fonts/split/woff2/IBMPlexMono-Regular-Latin1.woff2?url"
|
|
|
import ibmPlexMonoMediumLatin1 from "@ibm/plex/IBM-Plex-Mono/fonts/split/woff2/IBMPlexMono-Medium-Latin1.woff2?url"
|
|
|
import ibmPlexMonoSemiBoldLatin1 from "@ibm/plex/IBM-Plex-Mono/fonts/split/woff2/IBMPlexMono-SemiBold-Latin1.woff2?url"
|
|
|
@@ -15,7 +12,6 @@ import {
|
|
|
type CountryEntry,
|
|
|
type LeaderboardEntry,
|
|
|
type MarketDay,
|
|
|
- type StatsHomeData,
|
|
|
type SessionCostEntry,
|
|
|
type TokenCostEntry,
|
|
|
type UsagePoint,
|
|
|
@@ -23,14 +19,13 @@ import {
|
|
|
import { createAsync, query } from "@solidjs/router"
|
|
|
import { createEffect, createMemo, createSignal, For, onCleanup, onMount, Show, type JSX } from "solid-js"
|
|
|
import { getRequestEvent } from "solid-js/web"
|
|
|
-import type { FeatureCollection, GeometryObject, GeoJsonProperties } from "geojson"
|
|
|
-import type { GeometryCollection, Topology } from "topojson-specification"
|
|
|
import { runStatsEffect } from "../stats-runtime"
|
|
|
import { LocaleLinks } from "../component/locale-links"
|
|
|
import { useI18n } from "../context/i18n"
|
|
|
import { useLanguage } from "../context/language"
|
|
|
import { localizedUrl } from "../lib/language"
|
|
|
-import { findModelCatalogEntry, getModelCatalog, type ModelCatalog } from "./model-catalog"
|
|
|
+import { findModelCatalogEntry, loadModelCatalog, type ModelCatalog } from "./model-catalog"
|
|
|
+import { geoMapHeight, geoMapWidth, worldBorderPath, worldCountryMarkers, worldCountryPaths } from "./geo-map"
|
|
|
import { SectionHeading } from "./section-heading"
|
|
|
import { setStatsPageCacheHeaders } from "./stats-cache"
|
|
|
import { ComparisonCardsSection, uniqueComparisonPairs, type ComparisonModelRef } from "./compare-cards"
|
|
|
@@ -45,9 +40,6 @@ import {
|
|
|
type ThemePreference,
|
|
|
} from "./stats-shell"
|
|
|
|
|
|
-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"],
|
|
|
@@ -69,60 +61,40 @@ const usageColors = [
|
|
|
"#ff6467",
|
|
|
]
|
|
|
const marketColors = ["#ed6aff", "#a684ff", "#7c86ff", "#51a2ff", "#00d3f2", "#00d5be", "#00bc7d", "#9ae600", "#ffb900"]
|
|
|
-const geoMapWidth = 960
|
|
|
-const geoMapHeight = 430
|
|
|
|
|
|
-type UsageProduct = (typeof products)[number]
|
|
|
-type TokenProduct = (typeof tokenProducts)[number]
|
|
|
-type UsageRange = (typeof ranges)[number]
|
|
|
+type UsageRange = "1D" | "1W" | "2W" | "1M" | "2M"
|
|
|
type IsoCountryCode = readonly [string, string, string]
|
|
|
-type WorldCountryProperties = GeoJsonProperties & { name?: string }
|
|
|
-type WorldTopology = Topology<{ countries: GeometryCollection<WorldCountryProperties> }>
|
|
|
|
|
|
-function productLabel(product: UsageProduct | TokenProduct, i18n: ReturnType<typeof useI18n>) {
|
|
|
- if (product === "All Users") return i18n.t("product.allUsers")
|
|
|
- if (product === "Zen") return i18n.t("product.zen")
|
|
|
- return i18n.t("product.go")
|
|
|
-}
|
|
|
-
|
|
|
-function rangeLabel(range: UsageRange, i18n: ReturnType<typeof useI18n>) {
|
|
|
- if (range === "1D") return i18n.t("range.1D")
|
|
|
- if (range === "1W") return i18n.t("range.1W")
|
|
|
- if (range === "2W") return i18n.t("range.2W")
|
|
|
- if (range === "1M") return i18n.t("range.1M")
|
|
|
- return i18n.t("range.2M")
|
|
|
+type StatsHomePageData = {
|
|
|
+ updatedAt: string | null
|
|
|
+ usage: UsagePoint[]
|
|
|
+ users: UsagePoint[]
|
|
|
+ leaderboard: LeaderboardEntry[]
|
|
|
+ market: MarketDay[]
|
|
|
+ tokenCost: TokenCostEntry[]
|
|
|
+ cacheRatio: CacheRatioEntry[]
|
|
|
+ sessionCost: SessionCostEntry[]
|
|
|
+ country: CountryEntry[]
|
|
|
}
|
|
|
|
|
|
const countryNumericIds = new Map(
|
|
|
(JSON.parse(countryCodesSource) as IsoCountryCode[]).map((country) => [country[0], country[2]] as const),
|
|
|
)
|
|
|
-const worldTopology = JSON.parse(countriesTopologySource) as WorldTopology
|
|
|
-const worldCountryGeometries: GeometryCollection<WorldCountryProperties> = {
|
|
|
- ...worldTopology.objects.countries,
|
|
|
- geometries: worldTopology.objects.countries.geometries.filter((country) => String(country.id ?? "") !== "010"),
|
|
|
-}
|
|
|
-const worldCountries = feature<WorldCountryProperties>(worldTopology, worldCountryGeometries) as FeatureCollection<
|
|
|
- GeometryObject,
|
|
|
- WorldCountryProperties
|
|
|
->
|
|
|
-const worldProjection = geoEquirectangular().fitExtent(
|
|
|
- [
|
|
|
- [10, 12],
|
|
|
- [geoMapWidth - 10, geoMapHeight - 12],
|
|
|
- ],
|
|
|
- worldCountries,
|
|
|
-)
|
|
|
-const worldPath = geoPath(worldProjection)
|
|
|
-const worldCountryPaths = worldCountries.features.map((country) => ({
|
|
|
- id: String(country.id ?? "").padStart(3, "0"),
|
|
|
- path: worldPath(country) ?? "",
|
|
|
- marker: geoCountryMarker(country),
|
|
|
-}))
|
|
|
-const worldBorderPath = worldPath(mesh(worldTopology, worldCountryGeometries, (a, b) => a !== b)) ?? ""
|
|
|
|
|
|
const getData = query(async () => {
|
|
|
"use server"
|
|
|
- return runStatsEffect(getStatsHomeData())
|
|
|
+ const [stats, catalog] = await Promise.all([runStatsEffect(getStatsHomeData()), loadModelCatalog()])
|
|
|
+ return {
|
|
|
+ updatedAt: stats.updatedAt,
|
|
|
+ usage: stats.usage.Go["2M"],
|
|
|
+ users: stats.users.Go["2M"],
|
|
|
+ leaderboard: stats.leaderboard.Go["2M"],
|
|
|
+ market: stats.market["2M"],
|
|
|
+ tokenCost: priceTokenCostFromCatalog(stats.tokenCost.Go, catalog),
|
|
|
+ cacheRatio: stats.cacheRatio.Go,
|
|
|
+ sessionCost: stats.sessionCost.Go,
|
|
|
+ country: stats.country["2M"],
|
|
|
+ } satisfies StatsHomePageData
|
|
|
}, "getStatsHomeData")
|
|
|
|
|
|
export default function StatsHome() {
|
|
|
@@ -133,7 +105,6 @@ export default function StatsHome() {
|
|
|
const statsHomeUrl = localizedUrl(language.locale(), "/data/")
|
|
|
const statsUnfurlUrl = new URL(statsUnfurlPath, localizedUrl("en", "/data/")).toString()
|
|
|
const data = createAsync(() => getData())
|
|
|
- const catalog = createAsync(() => getModelCatalog())
|
|
|
const githubStars = createAsync(() => getGitHubStars())
|
|
|
const [themePreference, setThemePreference] = createSignal<ThemePreference>("system")
|
|
|
const updateThemePreference = (preference: ThemePreference) => {
|
|
|
@@ -185,12 +156,12 @@ export default function StatsHome() {
|
|
|
<TopModelsSection data={stats().usage} leaderboard={stats().leaderboard} />
|
|
|
<UniqueUsersSection data={stats().users} />
|
|
|
<SessionCostSection data={stats().sessionCost} />
|
|
|
- <TokenCostSection data={stats().tokenCost} catalog={catalog() ?? null} />
|
|
|
+ <TokenCostSection data={stats().tokenCost} />
|
|
|
<CacheRatioSection data={stats().cacheRatio} />
|
|
|
<MarketShareSection data={stats().market} />
|
|
|
<GeoBreakdownSection data={stats().country} />
|
|
|
<ComparisonCardsSection
|
|
|
- pairs={homeComparisonPairs(stats().leaderboard["All Users"]["2M"])}
|
|
|
+ pairs={homeComparisonPairs(stats().leaderboard)}
|
|
|
title="Model Comparisons"
|
|
|
description="Popular model pairs from the leaderboard."
|
|
|
variant="featured"
|
|
|
@@ -401,32 +372,9 @@ function formatUpdatedAtLabel(value: { date: string; time: string }) {
|
|
|
return `${value.date}, ${value.time}`
|
|
|
}
|
|
|
|
|
|
-function TopModelsSection(props: { data: StatsHomeData["usage"]; leaderboard: StatsHomeData["leaderboard"] }) {
|
|
|
+function TopModelsSection(props: { data: UsagePoint[]; leaderboard: LeaderboardEntry[] }) {
|
|
|
const i18n = useI18n()
|
|
|
- const [product, setProduct] = createSignal<UsageProduct>("Go")
|
|
|
- const [range, setRange] = createSignal<UsageRange>("2M")
|
|
|
- const [sheet, setSheet] = createSignal<"product" | "range">()
|
|
|
const [activeModel, setActiveModel] = createSignal<string>()
|
|
|
- const data = createMemo(() => props.data[product()][range()])
|
|
|
- const leaderboard = createMemo(() => props.leaderboard[product()][range()])
|
|
|
-
|
|
|
- createEffect(() => {
|
|
|
- if (!sheet()) return
|
|
|
- if (typeof document === "undefined") return
|
|
|
- const htmlOverflow = document.documentElement.style.overflow
|
|
|
- const bodyOverflow = document.body.style.overflow
|
|
|
- document.documentElement.style.overflow = "hidden"
|
|
|
- document.body.style.overflow = "hidden"
|
|
|
- const onKeyDown = (event: KeyboardEvent) => {
|
|
|
- if (event.key === "Escape") setSheet(undefined)
|
|
|
- }
|
|
|
- document.addEventListener("keydown", onKeyDown)
|
|
|
- onCleanup(() => {
|
|
|
- document.documentElement.style.overflow = htmlOverflow
|
|
|
- document.body.style.overflow = bodyOverflow
|
|
|
- document.removeEventListener("keydown", onKeyDown)
|
|
|
- })
|
|
|
- })
|
|
|
|
|
|
return (
|
|
|
<section id="top-models" data-section="top-models">
|
|
|
@@ -438,201 +386,28 @@ function TopModelsSection(props: { data: StatsHomeData["usage"]; leaderboard: St
|
|
|
description={i18n.t("home.topModelsDescription")}
|
|
|
/>
|
|
|
<Show
|
|
|
- when={data().some((item) => usageTotal(item) > 0)}
|
|
|
+ when={props.data.some((item) => usageTotal(item) > 0)}
|
|
|
fallback={<EmptyState title={i18n.t("home.noUsageTitle")} description={i18n.t("home.noUsageDescription")} />}
|
|
|
>
|
|
|
<TopModelsChart
|
|
|
- data={data()}
|
|
|
- range={range()}
|
|
|
+ data={props.data}
|
|
|
+ range="2M"
|
|
|
activeModel={activeModel()}
|
|
|
onActiveModelChange={setActiveModel}
|
|
|
/>
|
|
|
</Show>
|
|
|
<Show
|
|
|
- when={leaderboard().length > 0}
|
|
|
+ when={props.leaderboard.length > 0}
|
|
|
fallback={
|
|
|
<EmptyState title={i18n.t("home.noLeaderboardTitle")} description={i18n.t("home.noLeaderboardDescription")} />
|
|
|
}
|
|
|
>
|
|
|
- <Leaderboard data={leaderboard()} activeModel={activeModel()} onActiveModelChange={setActiveModel} />
|
|
|
- </Show>
|
|
|
- <div data-slot="chart-footer" hidden>
|
|
|
- <StatsFilters product={product()} range={range()} onProductSelect={setProduct} onRangeSelect={setRange} />
|
|
|
- <div data-slot="top-models-mobile-controls">
|
|
|
- <MobileFilterButton
|
|
|
- label={i18n.t("home.productFilter")}
|
|
|
- value={productLabel(product(), i18n)}
|
|
|
- expanded={sheet() === "product"}
|
|
|
- onClick={() => setSheet(sheet() === "product" ? undefined : "product")}
|
|
|
- />
|
|
|
- <MobileFilterButton
|
|
|
- label={i18n.t("home.dateRange")}
|
|
|
- value={rangeLabel(range(), i18n)}
|
|
|
- expanded={sheet() === "range"}
|
|
|
- onClick={() => setSheet(sheet() === "range" ? undefined : "range")}
|
|
|
- />
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <Show when={sheet()}>
|
|
|
- {(kind) => (
|
|
|
- <MobileFilterSheet
|
|
|
- kind={kind()}
|
|
|
- product={product()}
|
|
|
- range={range()}
|
|
|
- onProductSelect={(value) => {
|
|
|
- setProduct(value)
|
|
|
- setSheet(undefined)
|
|
|
- }}
|
|
|
- onRangeSelect={(value) => {
|
|
|
- setRange(value)
|
|
|
- setSheet(undefined)
|
|
|
- }}
|
|
|
- onClose={() => setSheet(undefined)}
|
|
|
- />
|
|
|
- )}
|
|
|
+ <Leaderboard data={props.leaderboard} activeModel={activeModel()} onActiveModelChange={setActiveModel} />
|
|
|
</Show>
|
|
|
</section>
|
|
|
)
|
|
|
}
|
|
|
|
|
|
-function MobileFilterButton(props: { label: string; value: string; expanded: boolean; onClick: () => void }) {
|
|
|
- return (
|
|
|
- <button
|
|
|
- data-slot="mobile-filter-button"
|
|
|
- type="button"
|
|
|
- aria-label={props.label}
|
|
|
- aria-expanded={props.expanded ? "true" : "false"}
|
|
|
- onClick={props.onClick}
|
|
|
- >
|
|
|
- <span>{props.value}</span>
|
|
|
- <ChevronDown />
|
|
|
- </button>
|
|
|
- )
|
|
|
-}
|
|
|
-
|
|
|
-function MobileFilterSheet(props: {
|
|
|
- kind: "product" | "range"
|
|
|
- product: UsageProduct
|
|
|
- range: UsageRange
|
|
|
- onProductSelect: (product: UsageProduct) => void
|
|
|
- onRangeSelect: (range: UsageRange) => void
|
|
|
- onClose: () => void
|
|
|
-}) {
|
|
|
- const i18n = useI18n()
|
|
|
- return (
|
|
|
- <div data-component="mobile-filter-sheet" role="presentation" onClick={props.onClose}>
|
|
|
- <div
|
|
|
- data-slot="filter-sheet-panel"
|
|
|
- role="radiogroup"
|
|
|
- aria-label={props.kind === "product" ? i18n.t("home.productFilter") : i18n.t("home.dateRange")}
|
|
|
- >
|
|
|
- <Show
|
|
|
- when={props.kind === "product"}
|
|
|
- fallback={
|
|
|
- <For each={ranges}>
|
|
|
- {(item) => (
|
|
|
- <button
|
|
|
- type="button"
|
|
|
- role="radio"
|
|
|
- aria-checked={props.range === item}
|
|
|
- data-active={props.range === item ? "true" : undefined}
|
|
|
- onClick={(event) => {
|
|
|
- event.stopPropagation()
|
|
|
- props.onRangeSelect(item)
|
|
|
- }}
|
|
|
- >
|
|
|
- {rangeLabel(item, i18n)}
|
|
|
- </button>
|
|
|
- )}
|
|
|
- </For>
|
|
|
- }
|
|
|
- >
|
|
|
- <For each={products}>
|
|
|
- {(item) => (
|
|
|
- <button
|
|
|
- type="button"
|
|
|
- role="radio"
|
|
|
- aria-checked={props.product === item}
|
|
|
- data-active={props.product === item ? "true" : undefined}
|
|
|
- onClick={(event) => {
|
|
|
- event.stopPropagation()
|
|
|
- props.onProductSelect(item)
|
|
|
- }}
|
|
|
- >
|
|
|
- {productLabel(item, i18n)}
|
|
|
- </button>
|
|
|
- )}
|
|
|
- </For>
|
|
|
- </Show>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- )
|
|
|
-}
|
|
|
-
|
|
|
-function ChevronDown() {
|
|
|
- return (
|
|
|
- <svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true" fill="none">
|
|
|
- <path d="M5 7L8 10L11 7" stroke="currentColor" />
|
|
|
- </svg>
|
|
|
- )
|
|
|
-}
|
|
|
-
|
|
|
-function StatsFilters(props: {
|
|
|
- product: UsageProduct
|
|
|
- range: UsageRange
|
|
|
- onProductSelect: (product: UsageProduct) => void
|
|
|
- onRangeSelect: (range: UsageRange) => void
|
|
|
-}) {
|
|
|
- const i18n = useI18n()
|
|
|
- return (
|
|
|
- <>
|
|
|
- <FilterPills
|
|
|
- items={products}
|
|
|
- selected={props.product}
|
|
|
- label={i18n.t("home.productFilter")}
|
|
|
- variant="product"
|
|
|
- formatLabel={(item) => productLabel(item, i18n)}
|
|
|
- onSelect={props.onProductSelect}
|
|
|
- />
|
|
|
- <FilterPills
|
|
|
- items={ranges}
|
|
|
- selected={props.range}
|
|
|
- label={i18n.t("home.dateRange")}
|
|
|
- variant="range"
|
|
|
- formatLabel={(item) => rangeLabel(item, i18n)}
|
|
|
- onSelect={props.onRangeSelect}
|
|
|
- />
|
|
|
- </>
|
|
|
- )
|
|
|
-}
|
|
|
-
|
|
|
-function FilterPills<T extends string>(props: {
|
|
|
- items: readonly T[]
|
|
|
- selected: T
|
|
|
- label: string
|
|
|
- variant: "product" | "range"
|
|
|
- formatLabel?: (item: T) => string
|
|
|
- onSelect: (item: T) => void
|
|
|
-}) {
|
|
|
- return (
|
|
|
- <div data-component="usage-filter" data-variant={props.variant} role="radiogroup" aria-label={props.label}>
|
|
|
- <For each={props.items}>
|
|
|
- {(item) => (
|
|
|
- <button
|
|
|
- type="button"
|
|
|
- role="radio"
|
|
|
- aria-checked={props.selected === item}
|
|
|
- data-active={props.selected === item ? "true" : undefined}
|
|
|
- onClick={() => props.onSelect(item)}
|
|
|
- >
|
|
|
- {props.formatLabel ? props.formatLabel(item) : item}
|
|
|
- </button>
|
|
|
- )}
|
|
|
- </For>
|
|
|
- </div>
|
|
|
- )
|
|
|
-}
|
|
|
-
|
|
|
function TopModelsChart(props: {
|
|
|
data: UsagePoint[]
|
|
|
range: UsageRange
|
|
|
@@ -823,10 +598,9 @@ function TopModelsChart(props: {
|
|
|
)
|
|
|
}
|
|
|
|
|
|
-function UniqueUsersSection(props: { data: StatsHomeData["users"] }) {
|
|
|
+function UniqueUsersSection(props: { data: UsagePoint[] }) {
|
|
|
const i18n = useI18n()
|
|
|
const [activeModel, setActiveModel] = createSignal<string>()
|
|
|
- const data = createMemo(() => props.data.Go["2M"])
|
|
|
|
|
|
return (
|
|
|
<section id="unique-users" data-section="unique-users">
|
|
|
@@ -837,13 +611,13 @@ function UniqueUsersSection(props: { data: StatsHomeData["users"] }) {
|
|
|
description={i18n.t("home.uniqueUsersDescription")}
|
|
|
/>
|
|
|
<Show
|
|
|
- when={data().some((item) => usageTotal(item) > 0)}
|
|
|
+ when={props.data.some((item) => usageTotal(item) > 0)}
|
|
|
fallback={
|
|
|
<EmptyState title={i18n.t("home.noUserDataTitle")} description={i18n.t("home.noUserDataDescription")} />
|
|
|
}
|
|
|
>
|
|
|
<TopModelsChart
|
|
|
- data={data()}
|
|
|
+ data={props.data}
|
|
|
range="2M"
|
|
|
metric="users"
|
|
|
ariaLabel={i18n.t("home.uniqueUsersChart")}
|
|
|
@@ -1084,16 +858,14 @@ function formatChange(value: number | null, i18n: ReturnType<typeof useI18n>) {
|
|
|
return `${value}%`
|
|
|
}
|
|
|
|
|
|
-function MarketShareSection(props: { data: StatsHomeData["market"] }) {
|
|
|
+function MarketShareSection(props: { data: MarketDay[] }) {
|
|
|
const i18n = useI18n()
|
|
|
- const [range, setRange] = createSignal<UsageRange>("2M")
|
|
|
const [activeIndex, setActiveIndex] = createSignal(2)
|
|
|
const [activeAuthor, setActiveAuthor] = createSignal<string>()
|
|
|
const [inspecting, setInspecting] = createSignal(false)
|
|
|
- const data = createMemo(() => props.data[range()])
|
|
|
- const authorOrder = createMemo(() => getMarketAuthorOrder(data()))
|
|
|
- const selectedIndex = createMemo(() => Math.min(activeIndex(), Math.max(data().length - 1, 0)))
|
|
|
- const activeDay = createMemo(() => data()[selectedIndex()])
|
|
|
+ const authorOrder = createMemo(() => getMarketAuthorOrder(props.data))
|
|
|
+ const selectedIndex = createMemo(() => Math.min(activeIndex(), Math.max(props.data.length - 1, 0)))
|
|
|
+ const activeDay = createMemo(() => props.data[selectedIndex()])
|
|
|
|
|
|
return (
|
|
|
<section
|
|
|
@@ -1118,8 +890,8 @@ function MarketShareSection(props: { data: StatsHomeData["market"] }) {
|
|
|
{(day) => (
|
|
|
<>
|
|
|
<MarketShare
|
|
|
- data={data()}
|
|
|
- range={range()}
|
|
|
+ data={props.data}
|
|
|
+ range="2M"
|
|
|
authorOrder={authorOrder()}
|
|
|
activeIndex={selectedIndex()}
|
|
|
activeAuthor={activeAuthor()}
|
|
|
@@ -1151,22 +923,9 @@ function MarketShareSection(props: { data: StatsHomeData["market"] }) {
|
|
|
<strong>
|
|
|
{inspecting()
|
|
|
? formatMarketDate(activeDay(), i18n.t("home.noData"))
|
|
|
- : formatMarketRange(data(), i18n.t("home.noData"))}
|
|
|
+ : formatMarketRange(props.data, i18n.t("home.noData"))}
|
|
|
</strong>
|
|
|
</p>
|
|
|
- <div hidden>
|
|
|
- <FilterPills
|
|
|
- items={ranges}
|
|
|
- selected={range()}
|
|
|
- label={i18n.t("home.dateRange")}
|
|
|
- variant="range"
|
|
|
- onSelect={(item) => {
|
|
|
- setRange(item)
|
|
|
- setActiveAuthor(undefined)
|
|
|
- setInspecting(false)
|
|
|
- }}
|
|
|
- />
|
|
|
- </div>
|
|
|
</div>
|
|
|
</section>
|
|
|
)
|
|
|
@@ -1333,23 +1092,22 @@ function MarketShareList(props: {
|
|
|
)
|
|
|
}
|
|
|
|
|
|
-function GeoBreakdownSection(props: { data: StatsHomeData["country"] }) {
|
|
|
+function GeoBreakdownSection(props: { data: CountryEntry[] }) {
|
|
|
const i18n = useI18n()
|
|
|
const language = useLanguage()
|
|
|
const [activeCountry, setActiveCountry] = createSignal<string>()
|
|
|
- const data = createMemo(() => props.data["2M"])
|
|
|
const countryById = createMemo(
|
|
|
() =>
|
|
|
new Map(
|
|
|
- data().flatMap((country) => {
|
|
|
+ props.data.flatMap((country) => {
|
|
|
const id = countryNumericId(country.country)
|
|
|
return id ? [[id, country] as const] : []
|
|
|
}),
|
|
|
),
|
|
|
)
|
|
|
- const maxTokens = createMemo(() => Math.max(0, ...data().map((country) => country.tokens)) || 1)
|
|
|
- const topCountries = createMemo(() => data().slice(0, 15))
|
|
|
- const active = createMemo(() => data().find((country) => country.country === activeCountry()) ?? data()[0])
|
|
|
+ const maxTokens = createMemo(() => Math.max(0, ...props.data.map((country) => country.tokens)) || 1)
|
|
|
+ const topCountries = createMemo(() => props.data.slice(0, 15))
|
|
|
+ const active = createMemo(() => props.data.find((country) => country.country === activeCountry()) ?? props.data[0])
|
|
|
|
|
|
return (
|
|
|
<section
|
|
|
@@ -1363,7 +1121,7 @@ function GeoBreakdownSection(props: { data: StatsHomeData["country"] }) {
|
|
|
<SectionBridge label={i18n.t("nav.marketShare").toUpperCase()} href="#market-share" />
|
|
|
<SectionTitle id="geo-breakdown" title={i18n.t("home.geoTitle")} description={i18n.t("home.geoDescription")} />
|
|
|
<Show
|
|
|
- when={data().length > 0}
|
|
|
+ when={props.data.length > 0}
|
|
|
fallback={<EmptyState title={i18n.t("home.noGeoTitle")} description={i18n.t("home.noGeoDescription")} />}
|
|
|
>
|
|
|
<div data-component="geo-breakdown">
|
|
|
@@ -1453,31 +1211,30 @@ function GeoWorldMap(props: {
|
|
|
</For>
|
|
|
</g>
|
|
|
<g data-slot="geo-country-markers">
|
|
|
- <For each={worldCountryPaths}>
|
|
|
+ <For each={worldCountryMarkers}>
|
|
|
{(country) => {
|
|
|
const entry = () => props.countryById.get(country.id)
|
|
|
return (
|
|
|
- <Show when={country.marker && entry() ? country.marker : undefined}>
|
|
|
- {(marker) => (
|
|
|
- <circle
|
|
|
- cx={marker().x}
|
|
|
- cy={marker().y}
|
|
|
- r={entry()?.country === props.activeCountry ? 3.4 : 2.4}
|
|
|
- data-active={entry()?.country === props.activeCountry ? "true" : undefined}
|
|
|
- style={{ "--geo-country-opacity": String(countryOpacity(entry())) } as JSX.CSSProperties}
|
|
|
- aria-hidden="true"
|
|
|
- onPointerEnter={() => {
|
|
|
- const item = entry()
|
|
|
- if (!item) return
|
|
|
- props.onActiveCountryChange(item.country)
|
|
|
- }}
|
|
|
- onClick={() => {
|
|
|
- const item = entry()
|
|
|
- if (!item) return
|
|
|
- props.onActiveCountryChange(item.country)
|
|
|
- }}
|
|
|
- />
|
|
|
- )}
|
|
|
+ <Show when={entry()}>
|
|
|
+ <circle
|
|
|
+ cx={country.marker.x}
|
|
|
+ cy={country.marker.y}
|
|
|
+ data-country-id={country.id}
|
|
|
+ r={entry()?.country === props.activeCountry ? 3.4 : 2.4}
|
|
|
+ data-active={entry()?.country === props.activeCountry ? "true" : undefined}
|
|
|
+ style={{ "--geo-country-opacity": String(countryOpacity(entry())) } as JSX.CSSProperties}
|
|
|
+ aria-hidden="true"
|
|
|
+ onPointerEnter={() => {
|
|
|
+ const item = entry()
|
|
|
+ if (!item) return
|
|
|
+ props.onActiveCountryChange(item.country)
|
|
|
+ }}
|
|
|
+ onClick={() => {
|
|
|
+ const item = entry()
|
|
|
+ if (!item) return
|
|
|
+ props.onActiveCountryChange(item.country)
|
|
|
+ }}
|
|
|
+ />
|
|
|
</Show>
|
|
|
)
|
|
|
}}
|
|
|
@@ -1531,14 +1288,6 @@ function countryNumericId(country: string) {
|
|
|
return countryNumericIds.get(country.toUpperCase())?.padStart(3, "0")
|
|
|
}
|
|
|
|
|
|
-function geoCountryMarker(country: (typeof worldCountries.features)[number]) {
|
|
|
- const bounds = worldPath.bounds(country)
|
|
|
- const [x, y] = worldPath.centroid(country)
|
|
|
- if (!Number.isFinite(x) || !Number.isFinite(y)) return undefined
|
|
|
- if (bounds[1][0] - bounds[0][0] >= 3 && bounds[1][1] - bounds[0][1] >= 3) return undefined
|
|
|
- return { x, y }
|
|
|
-}
|
|
|
-
|
|
|
function formatCountryName(country: string, locale: string, unknown: string) {
|
|
|
const code = country.toUpperCase()
|
|
|
if (code === "ZZ") return unknown
|
|
|
@@ -1636,12 +1385,10 @@ function marketDateParts(label: string) {
|
|
|
return { start: start ?? label, end: end ?? start ?? label }
|
|
|
}
|
|
|
|
|
|
-function TokenCostSection(props: { data: StatsHomeData["tokenCost"]; catalog: ModelCatalog | null }) {
|
|
|
+function TokenCostSection(props: { data: TokenCostEntry[] }) {
|
|
|
const i18n = useI18n()
|
|
|
- const [product, setProduct] = createSignal<TokenProduct>("Go")
|
|
|
const [activeIndex, setActiveIndex] = createSignal(2)
|
|
|
- const data = createMemo(() => priceTokenCostFromCatalog(props.data[product()], props.catalog))
|
|
|
- const visible = createMemo(() => data().slice(0, 13))
|
|
|
+ const visible = createMemo(() => props.data.slice(0, 13))
|
|
|
const selectedIndex = createMemo(() => Math.min(activeIndex(), Math.max(visible().length - 1, 0)))
|
|
|
|
|
|
return (
|
|
|
@@ -1660,17 +1407,6 @@ function TokenCostSection(props: { data: StatsHomeData["tokenCost"]; catalog: Mo
|
|
|
>
|
|
|
<TokenCostChart data={visible()} activeIndex={selectedIndex()} onActiveIndexChange={setActiveIndex} />
|
|
|
</Show>
|
|
|
- <div data-slot="token-footer" hidden>
|
|
|
- <FilterPills
|
|
|
- items={tokenProducts}
|
|
|
- selected={product()}
|
|
|
- label={i18n.t("home.productFilter")}
|
|
|
- variant="product"
|
|
|
- formatLabel={(item) => productLabel(item, i18n)}
|
|
|
- onSelect={setProduct}
|
|
|
- />
|
|
|
- <LiveIndicator />
|
|
|
- </div>
|
|
|
</section>
|
|
|
)
|
|
|
}
|
|
|
@@ -1723,12 +1459,10 @@ function TokenCostChart(props: {
|
|
|
)
|
|
|
}
|
|
|
|
|
|
-function CacheRatioSection(props: { data: StatsHomeData["cacheRatio"] }) {
|
|
|
+function CacheRatioSection(props: { data: CacheRatioEntry[] }) {
|
|
|
const i18n = useI18n()
|
|
|
- const [product, setProduct] = createSignal<TokenProduct>("Go")
|
|
|
const [activeIndex, setActiveIndex] = createSignal(2)
|
|
|
- const data = createMemo(() => props.data[product()])
|
|
|
- const visible = createMemo(() => data().slice(0, 16))
|
|
|
+ const visible = createMemo(() => props.data.slice(0, 16))
|
|
|
const selectedIndex = createMemo(() => Math.min(activeIndex(), Math.max(visible().length - 1, 0)))
|
|
|
|
|
|
return (
|
|
|
@@ -1745,17 +1479,6 @@ function CacheRatioSection(props: { data: StatsHomeData["cacheRatio"] }) {
|
|
|
>
|
|
|
<CacheRatioChart data={visible()} activeIndex={selectedIndex()} onActiveIndexChange={setActiveIndex} />
|
|
|
</Show>
|
|
|
- <div data-slot="token-footer" hidden>
|
|
|
- <FilterPills
|
|
|
- items={tokenProducts}
|
|
|
- selected={product()}
|
|
|
- label={i18n.t("home.productFilter")}
|
|
|
- variant="product"
|
|
|
- formatLabel={(item) => productLabel(item, i18n)}
|
|
|
- onSelect={setProduct}
|
|
|
- />
|
|
|
- <LiveIndicator />
|
|
|
- </div>
|
|
|
</section>
|
|
|
)
|
|
|
}
|
|
|
@@ -1853,12 +1576,10 @@ function MetricBar(props: { value: number; max: number; active: boolean }) {
|
|
|
)
|
|
|
}
|
|
|
|
|
|
-function SessionCostSection(props: { data: StatsHomeData["sessionCost"] }) {
|
|
|
+function SessionCostSection(props: { data: SessionCostEntry[] }) {
|
|
|
const i18n = useI18n()
|
|
|
- const [product, setProduct] = createSignal<TokenProduct>("Go")
|
|
|
const [activeIndex, setActiveIndex] = createSignal(2)
|
|
|
- const data = createMemo(() => props.data[product()])
|
|
|
- const visible = createMemo(() => data().slice(0, 16))
|
|
|
+ const visible = createMemo(() => props.data.slice(0, 16))
|
|
|
const selectedIndex = createMemo(() => Math.min(activeIndex(), Math.max(visible().length - 1, 0)))
|
|
|
|
|
|
return (
|
|
|
@@ -1877,17 +1598,6 @@ function SessionCostSection(props: { data: StatsHomeData["sessionCost"] }) {
|
|
|
>
|
|
|
<SessionCostChart data={visible()} activeIndex={selectedIndex()} onActiveIndexChange={setActiveIndex} />
|
|
|
</Show>
|
|
|
- <div data-slot="token-footer" hidden>
|
|
|
- <FilterPills
|
|
|
- items={tokenProducts}
|
|
|
- selected={product()}
|
|
|
- label={i18n.t("home.productFilter")}
|
|
|
- variant="product"
|
|
|
- formatLabel={(item) => productLabel(item, i18n)}
|
|
|
- onSelect={setProduct}
|
|
|
- />
|
|
|
- <LiveIndicator />
|
|
|
- </div>
|
|
|
</section>
|
|
|
)
|
|
|
}
|
|
|
@@ -1949,11 +1659,6 @@ function SessionCostChart(props: {
|
|
|
)
|
|
|
}
|
|
|
|
|
|
-function LiveIndicator() {
|
|
|
- const i18n = useI18n()
|
|
|
- return <span data-component="live-filter">{i18n.t("chart.live")}</span>
|
|
|
-}
|
|
|
-
|
|
|
function formatTokenCount(value: number) {
|
|
|
if (value >= 1_000_000) return `${Number((value / 1_000_000).toFixed(1))}M`
|
|
|
return `${Math.round(value / 1_000)}K`
|