* 🎨 feat(web/default): add shadcn-style theme presets, radius prefs, and fix selection badges Integrate the qn-platform–style OKLCH color system into the default frontend while keeping the existing blue-tinted dark tokens for the default theme. Add [data-theme-preset] palettes for seven named presets plus the default zinc-like scale, define [data-theme-radius] overrides so user radius beats preset --radius, and align the Tailwind @custom-variant dark helper with .dark usage. Introduce ThemeCustomizationProvider to own preset and radius state, persist choices in cookies (theme-preset, theme-radius), and sync data-theme-preset / data-theme-radius on <html>. Wrap the tree in main.tsx. Extend ConfigDrawer with theme preset swatches (scoped data-theme-preset) and radius previews wired to context; refactor swatch/card markup so selected CircleCheck badges sit outside clipped rows (remove outer overflow-hidden that hid the centered checkmark). Add i18n keys for preset names, radius, and accessibility labels across en, zh, fr, ja, ru, vi. * 🎨 fix(web): align segmented controls with theme radius tokens - Replace hard-coded inner pill radii (rounded-[5px]) on dashboard chart toolbars with radius-md so the active state follows --radius when users change Radius in Theme Settings. - Use nested radii consistent with TabsList/TabsTrigger: outer rounded-lg (var(--radius)) and inner rounded-md (calc(var(--radius) - 2px)) so the track and active thumb stay concentric at small scales (e.g. 0.3rem) instead of a squared “focus” block inside a rounded shell. - Apply the same pattern to pricing SegmentedControl and the segmented groups in consumption-distribution-chart, model-charts, and user-charts. Verified: bun run typecheck (web/default) * ✨ feat(pricing): enrich model details with uptime sparkline and API documentation Add a compact 30-day uptime sparkline (OpenRouter-style bars + aggregate %) with per-day tooltips, surface it in a status row under quick stats and in the per-group performance table, and extend mock data so uptime series are stable and optionally scoped by group. Introduce an API tab with Shiki-highlighted code samples (cURL, Python, TypeScript, JavaScript), endpoint-type switching, authentication guidance, a supported-parameters table, and mock per-group RPM/TPM/RPD limits. Infer vendor, tokenizer, license, and data-retention hints for a provider & data privacy card on the Overview tab (capabilities/modalities stay with model identity; rate limits stay with the API tab). Update i18n for all new user-facing strings across en, zh, fr, ja, ru, and vi. * 🏆 feat(rankings): add comprehensive rankings dashboard Add a mock-data powered rankings experience with period tabs, model, app, and vendor leaderboards, market share and history charts, movers, new releases, and per-category sections while backend analytics are pending. Link ranked models to pricing details and ranked vendors to filtered pricing results, and include localized copy for all supported frontend locales. * fix(theme): correct theme preset selection state - update Base UI Radio selectors to use data-checked/data-unchecked states. - fix unchecked theme options still showing selected indicators. - isolate the default theme preview tokens to prevent preset changes from leaking into it. * fix(setup): correct usage mode radio state - use Base UI data-checked/data-unchecked states for RadioGroup styling. - hide radio indicators when options are unchecked to avoid setup page display issues. - drive usage mode card and icon selection styles from Base UI state. * fix(auth): submit sign-in and sign-up forms * 🎨 refactor: Align default theme with shadcn Base Nova and prune legacy customization Migrate shadcn UI to Base UI primitives via CLI (`base-nova` / `components.json`) and reinstall full component registry with `--overwrite`, including Hugeicons-backed widgets and newly added registry components. - Remove custom multi-preset/theme-radius system (`ThemeCustomizationProvider`, cookies, preset UI from config drawer); rely on official semantic CSS tokens + light/dark only. - Replace `theme.css` with shadcn’s documented neutral `:root`/`.dark` palette and `@theme inline` mappings (plus skeleton token vars for existing shimmer usage). - Update global styles for Base UI: collapsible animation uses `--collapsible-panel-height`; clarify scroll-lock override comment. Application compatibility: - Keep minimal shims where app code diverged from official APIs (popover collision props, combobox legacy `options` callers, Spinner prop typing). - Switch interactive styling from Radix-era `data-state` / `--radix-*` selectors to Base UI semantics (`data-open`, `data-popup-open`, `data-panel-open`, `--anchor-width`, etc.) Tooling / docs / build: - Rename Rsbuild vendor chunk grouping to `@base-ui` + transitive `@radix-ui`. - Refresh AGENTS.md / CLAUDE.md / classic→default sync skill for Base UI stack. - Bump `package.json` / lockfile for shadcn-postinstall deps (Hugeicons, chart stack, themes, etc.) Verified: `bun run typecheck` passes. Note: `bun run lint` still reports pre-existing hooks rule violations elsewhere; not addressed in this change. * 🎨 chore(web/default): unify table toolbar, relocate usage stats, refine filters - Refactor DataTableToolbar to a single wrapping flex row with a right-aligned action cluster (Reset / Search / View / Expand) for a cleaner Ant Design Pro–style filter bar; remove the dedicated stats row and the toolbar `stats` prop. - Move Common Logs summary badges (Usage / RPM / TPM) and the sensitive- data visibility toggle into the page header via CommonLogsHeaderActions and SectionPageLayout.Actions so the toolbar stays focused on filters. - Slim CommonLogsFilterBar props (no stats / preActions eye control). - Improve CompactDateTimeRangePicker: show minute-precision labels on the trigger (seconds omitted; aligns with datetime-local inputs); widen the trigger on sm+ breakpoints so the full range is visible without truncation; apply the same width in task logs filters. - Simplify DataTableViewOptions: text-only “View” trigger, no sliders icon. - Earlier layout tweak: extra top padding on SectionPageLayout scroll content so control focus rings are not clipped by overflow. * feat(web/default): Base UI migration and component foundation Migrate from Radix UI to Base UI, rewrite core UI primitives, update dependencies (recharts, date-fns, next-themes), add shadcn agent skill documentation, and refresh AI element components. This is the foundational work from the v2/localmain lineage that was not covered by the individual feature commits above. --------- Co-authored-by: t0ng7u <dev@aiass.cc> Co-authored-by: QuentinHsu <xuquentinyang@gmail.com>
285 lines
8.7 KiB
TypeScript
Vendored
285 lines
8.7 KiB
TypeScript
Vendored
import { useMemo } from 'react'
|
|
import { VChart } from '@visactor/react-vchart'
|
|
import { PieChart } from 'lucide-react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { useChartTheme } from '@/lib/use-chart-theme'
|
|
import { VCHART_OPTION } from '@/lib/vchart'
|
|
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 area 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 area 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 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: 'area' as const,
|
|
data: [{ id: 'vendor-share', values: orderedPoints }],
|
|
xField: 'label',
|
|
yField: 'share',
|
|
seriesField: 'vendor',
|
|
stack: true,
|
|
legends: { visible: false },
|
|
area: {
|
|
style: { fillOpacity: 0.85, curveType: 'monotone' },
|
|
},
|
|
line: { style: { lineWidth: 0, curveType: 'monotone' } },
|
|
point: { visible: false },
|
|
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 },
|
|
}
|
|
}, [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>
|
|
)
|
|
}
|