Add a Bun script to apply and normalize AGPL copyright headers across the default frontend source files. The script keeps headers idempotent, upgrades existing headers to the 2023-2026 QuantumNous range, and is exposed through `bun run copyright` for future maintenance.
309 lines
9.7 KiB
TypeScript
Vendored
309 lines
9.7 KiB
TypeScript
Vendored
/*
|
|
Copyright (C) 2023-2026 QuantumNous
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as
|
|
published by the Free Software Foundation, either version 3 of the
|
|
License, or (at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
For commercial licensing, please contact support@quantumnous.com
|
|
*/
|
|
import { useMemo } from 'react'
|
|
import { VChart } from '@visactor/react-vchart'
|
|
import { PieChart } from 'lucide-react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { useThemeRadiusPx } from '@/lib/theme-radius'
|
|
import { useChartTheme } from '@/lib/use-chart-theme'
|
|
import { VCHART_OPTION } from '@/lib/vchart'
|
|
import { useThemeCustomization } from '@/context/theme-customization-provider'
|
|
import { formatShare, formatTokens } from '../lib/format'
|
|
import type { RankingPeriod, VendorRanking, VendorShareSeries } from '../types'
|
|
import { VendorLink } from './entity-links'
|
|
|
|
const PERIOD_DESCRIPTIONS: Record<RankingPeriod, string> = {
|
|
today: 'Token share by model author across the last 24 hours',
|
|
week: 'Token share by model author across the past few weeks',
|
|
month: 'Token share by model author across the past month',
|
|
year: 'Token share by model author across the past year',
|
|
all: 'Token share by model author since launch',
|
|
}
|
|
|
|
/** Stable colour palette for vendors, used in both the share chart and the
|
|
* legend dots. Falls back to a neutral palette for unknown vendors so that
|
|
* future additions still render. */
|
|
const VENDOR_COLOURS: Record<string, string> = {
|
|
OpenAI: '#10a37f',
|
|
Anthropic: '#d97757',
|
|
Google: '#4285f4',
|
|
DeepSeek: '#7c5cff',
|
|
Alibaba: '#ff9900',
|
|
xAI: '#1f2937',
|
|
Meta: '#1877f2',
|
|
Moonshot: '#ec4899',
|
|
Zhipu: '#06b6d4',
|
|
Mistral: '#ff7000',
|
|
ByteDance: '#3b82f6',
|
|
Tencent: '#22c55e',
|
|
MiniMax: '#a855f7',
|
|
Cohere: '#fb923c',
|
|
Baidu: '#ef4444',
|
|
Others: '#94a3b8',
|
|
}
|
|
|
|
const FALLBACK_PALETTE = [
|
|
'#0ea5e9',
|
|
'#22c55e',
|
|
'#a855f7',
|
|
'#f97316',
|
|
'#14b8a6',
|
|
'#eab308',
|
|
'#ec4899',
|
|
'#84cc16',
|
|
'#6366f1',
|
|
'#10b981',
|
|
'#f43f5e',
|
|
'#0891b2',
|
|
'#94a3b8',
|
|
]
|
|
|
|
function buildVendorColourMap(names: string[]): Record<string, string> {
|
|
const result: Record<string, string> = {}
|
|
let fallbackIdx = 0
|
|
for (const name of names) {
|
|
if (VENDOR_COLOURS[name]) {
|
|
result[name] = VENDOR_COLOURS[name]
|
|
} else {
|
|
result[name] = FALLBACK_PALETTE[fallbackIdx % FALLBACK_PALETTE.length]
|
|
fallbackIdx += 1
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
const MAX_VENDORS_IN_LIST = 12
|
|
|
|
type MarketShareSectionProps = {
|
|
history: VendorShareSeries
|
|
rows: VendorRanking[]
|
|
period: RankingPeriod
|
|
}
|
|
|
|
/**
|
|
* Combined "Market Share" card: a 100%-stacked bar chart showing each
|
|
* vendor's slice of total token volume, paired below with a two-column
|
|
* vendor list.
|
|
*/
|
|
export function MarketShareSection(props: MarketShareSectionProps) {
|
|
const { t } = useTranslation()
|
|
const { resolvedTheme, themeReady } = useChartTheme()
|
|
const { customization } = useThemeCustomization()
|
|
const barRadius = useThemeRadiusPx(
|
|
'--radius-sm',
|
|
`${customization.preset}:${customization.radius}`
|
|
)
|
|
|
|
const colourMap = useMemo(
|
|
() => buildVendorColourMap(props.history.vendors.map((v) => v.name)),
|
|
[props.history]
|
|
)
|
|
|
|
const orderedPoints = useMemo(() => {
|
|
const order = new Map(
|
|
props.history.vendors.map((v, idx) => [v.name, idx] as const)
|
|
)
|
|
return [...props.history.points].sort((a, b) => {
|
|
const tsCmp = a.ts.localeCompare(b.ts)
|
|
if (tsCmp !== 0) return tsCmp
|
|
return (order.get(a.vendor) ?? 999) - (order.get(b.vendor) ?? 999)
|
|
})
|
|
}, [props.history])
|
|
|
|
const spec = useMemo(() => {
|
|
if (orderedPoints.length === 0) return null
|
|
return {
|
|
type: 'bar' as const,
|
|
data: [{ id: 'vendor-share', values: orderedPoints }],
|
|
xField: 'label',
|
|
yField: 'share',
|
|
seriesField: 'vendor',
|
|
stack: true,
|
|
paddingInner: 0.12,
|
|
legends: { visible: false },
|
|
bar: {
|
|
style: barRadius == null ? {} : { cornerRadius: barRadius },
|
|
},
|
|
color: { specified: colourMap },
|
|
axes: [
|
|
{
|
|
orient: 'bottom',
|
|
label: {
|
|
style: { fill: 'currentColor', fontSize: 10 },
|
|
autoHide: true,
|
|
autoLimit: true,
|
|
},
|
|
tick: { visible: false },
|
|
},
|
|
{
|
|
orient: 'left',
|
|
min: 0,
|
|
max: 1,
|
|
label: {
|
|
formatMethod: (val: number | string) =>
|
|
`${Math.round(Number(val) * 100)}%`,
|
|
style: { fill: 'currentColor', fontSize: 10 },
|
|
},
|
|
grid: { visible: true, style: { lineDash: [3, 3] } },
|
|
},
|
|
],
|
|
tooltip: {
|
|
mark: {
|
|
content: [
|
|
{
|
|
key: (datum: Record<string, unknown>) =>
|
|
String(datum?.vendor ?? ''),
|
|
value: (datum: Record<string, unknown>) =>
|
|
`${(Number(datum?.share) * 100).toFixed(1)}% · ${formatTokens(Number(datum?.tokens) || 0)}`,
|
|
},
|
|
],
|
|
},
|
|
dimension: {
|
|
title: {
|
|
value: (datum: Record<string, unknown>) =>
|
|
String(datum?.label ?? ''),
|
|
},
|
|
content: [
|
|
{
|
|
key: (datum: Record<string, unknown>) =>
|
|
String(datum?.vendor ?? ''),
|
|
value: (datum: Record<string, unknown>) =>
|
|
Number(datum?.share) || 0,
|
|
},
|
|
],
|
|
updateContent: (
|
|
array: Array<{ key: string; value: string | number }>
|
|
) => {
|
|
return array
|
|
.filter((item) => Number(item.value) > 0.001)
|
|
.sort((a, b) => Number(b.value) - Number(a.value))
|
|
.map((item) => ({
|
|
key: item.key,
|
|
value: `${(Number(item.value) * 100).toFixed(1)}%`,
|
|
}))
|
|
},
|
|
},
|
|
},
|
|
animationAppear: { duration: 500 },
|
|
}
|
|
}, [barRadius, colourMap, orderedPoints])
|
|
|
|
const visible = props.rows.slice(0, MAX_VENDORS_IN_LIST)
|
|
const half = Math.ceil(visible.length / 2)
|
|
const left = visible.slice(0, half)
|
|
const right = visible.slice(half)
|
|
|
|
return (
|
|
<section className='bg-card overflow-hidden rounded-lg border'>
|
|
{/* Chart block ----------------------------------------------------- */}
|
|
<header className='px-5 py-4'>
|
|
<h2 className='text-foreground inline-flex items-center gap-2 text-base font-semibold'>
|
|
<PieChart className='text-primary size-4' />
|
|
{t('Market Share')}
|
|
</h2>
|
|
<p className='text-muted-foreground mt-1 text-sm'>
|
|
{t(PERIOD_DESCRIPTIONS[props.period])}
|
|
</p>
|
|
</header>
|
|
|
|
<div className='px-5 pb-5'>
|
|
<div className='h-60 sm:h-72'>
|
|
{themeReady && spec ? (
|
|
<VChart
|
|
key={`vendor-share-${resolvedTheme}-${props.period}`}
|
|
spec={{
|
|
...spec,
|
|
theme: resolvedTheme === 'dark' ? 'dark' : 'light',
|
|
background: 'transparent',
|
|
}}
|
|
option={VCHART_OPTION}
|
|
/>
|
|
) : (
|
|
<div className='text-muted-foreground/80 flex h-full items-center justify-center text-xs'>
|
|
{t('No history data available')}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Vendor list block ----------------------------------------------- */}
|
|
<div className='border-t'>
|
|
<header className='px-5 pt-4 pb-2'>
|
|
<h3 className='text-foreground text-sm font-semibold'>
|
|
{t('By model author')}
|
|
</h3>
|
|
<p className='text-muted-foreground/80 mt-0.5 text-xs'>
|
|
{t('Vendors ranked by aggregated token volume')}
|
|
</p>
|
|
</header>
|
|
{visible.length === 0 ? (
|
|
<div className='text-muted-foreground/80 px-5 py-8 text-center text-sm'>
|
|
{t('No vendor data available')}
|
|
</div>
|
|
) : (
|
|
<div className='grid grid-cols-1 gap-x-8 px-5 pt-1 pb-4 md:grid-cols-2'>
|
|
<VendorList rows={left} colourMap={colourMap} />
|
|
{right.length > 0 && (
|
|
<VendorList rows={right} colourMap={colourMap} />
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
function VendorList(props: {
|
|
rows: VendorRanking[]
|
|
colourMap: Record<string, string>
|
|
}) {
|
|
return (
|
|
<ul>
|
|
{props.rows.map((vendor) => (
|
|
<li key={vendor.vendor} className='flex items-center gap-3 py-2.5'>
|
|
<span className='text-muted-foreground/80 w-6 shrink-0 text-right font-mono text-xs tabular-nums'>
|
|
{vendor.rank}.
|
|
</span>
|
|
<span
|
|
aria-hidden
|
|
className='size-2.5 shrink-0 rounded-full'
|
|
style={{
|
|
backgroundColor: props.colourMap[vendor.vendor] ?? '#94a3b8',
|
|
}}
|
|
/>
|
|
<VendorLink
|
|
vendor={vendor.vendor}
|
|
className='text-foreground min-w-0 flex-1 truncate text-sm font-medium'
|
|
>
|
|
{vendor.vendor}
|
|
</VendorLink>
|
|
<div className='shrink-0 text-right'>
|
|
<div className='text-foreground font-mono text-sm font-semibold tabular-nums'>
|
|
{formatTokens(vendor.total_tokens)}
|
|
</div>
|
|
<div className='text-muted-foreground/80 font-mono text-[11px] tabular-nums'>
|
|
{formatShare(vendor.share)}
|
|
</div>
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)
|
|
}
|