hub-flow / source /src /App.jsx
mindchain's picture
Upload source/src/App.jsx with huggingface_hub
174bac0 verified
Raw History Blame Contribute Delete
16 kB
import React, { useEffect, useMemo, useRef, useState } from 'react'
import { api, hubTop } from './api.js'
import { Sparkline, Stat, AreaChart, fmt, pct } from './components.jsx'
import { ModelPage, AuthorPage, Loading, ErrBox } from './pages.jsx'
function parseHash() {
const h = location.hash
if (h.startsWith('#/m/')) return { v: 'model', id: decodeURIComponent(h.slice(4)) }
if (h.startsWith('#/a/')) return { v: 'author', author: decodeURIComponent(h.slice(4)) }
return { v: 'home' }
}
const BOARDS = [
{ key: 'gainers_7d', label: 'Gainers 7d', sub: 'biggest absolute 7-day gains' },
{ key: 'growth_7d', label: 'Fastest growing', sub: 'relative growth vs week before' },
{ key: 'breakouts', label: 'Breakouts', sub: 'new entries with traction' },
{ key: 'likes_7d', label: 'Most liked 7d', sub: 'likes gained this week' },
{ key: 'families', label: 'Top families', sub: 'ecosystems by family downloads' },
{ key: 'authors_dl30', label: 'Authors 30d', sub: 'most downloaded authors' },
{ key: 'authors_7d', label: 'Authors 7d', sub: 'this week\'s top authors' },
]
function BoardTable({ rows, go, isAuthors }) {
if (!rows || !rows.length) return <p className="py-6 text-sm text-[var(--dim)]">Empty board this week.</p>
const maxdl = Math.max(1, ...rows.map((r) => r.dl30 || 0))
return (
<table className="w-full">
<thead>
<tr className="text-[10.5px] uppercase tracking-[0.06em] text-[var(--faint)]">
<th className="w-8 py-2 pl-1 text-left font-semibold">#</th>
<th className="py-2 text-left font-semibold">{isAuthors ? 'Author' : 'Model'}</th>
<th className="hidden px-3 py-2 text-right font-semibold md:table-cell">30d</th>
<th className="px-3 py-2 text-right font-semibold">7d</th>
{!isAuthors && <th className="hidden px-3 py-2 text-right font-semibold sm:table-cell">Growth</th>}
<th className="px-3 py-2 pr-1 text-right font-semibold">♥</th>
</tr>
</thead>
<tbody>
{rows.map((r, i) => {
const name = isAuthors ? r.author : r.id
const href = isAuthors ? '#/a/' + r.author : '#/m/' + r.id
return (
<tr key={name} onClick={() => go(href)} className="cursor-pointer border-b border-[var(--line2)] transition-colors hover:bg-[var(--accent-soft)]/40">
<td className="tnum py-2.5 pl-1 text-[var(--faint)]">{i + 1}</td>
<td className="break-all py-2.5 pr-3 font-mono text-[12.5px] font-semibold">
{name}{!isAuthors && r.pipeline_tag && <span className="ml-2 hidden font-sans text-[11px] font-normal text-[var(--faint)] lg:inline">{r.pipeline_tag}</span>}
{isAuthors && <span className="ml-2 font-sans text-[11px] font-normal text-[var(--faint)]">{fmt(r.models)} models</span>}
</td>
<td className="tnum hidden px-3 py-2.5 text-right md:table-cell">{fmt(r.dl30)}</td>
<td className="tnum px-3 py-2.5 text-right font-semibold">{fmt(r.dl_7d)}</td>
{!isAuthors && <td className={'tnum hidden px-3 py-2.5 text-right font-mono text-[12px] sm:table-cell ' + ((r.growth_7d || 0) >= 0 ? 'text-[var(--up)]' : 'text-[var(--danger)]')}>{r.growth_7d != null ? pct(r.growth_7d) : '—'}</td>}
<td className="tnum px-3 py-2.5 pr-1 text-right text-[var(--dim)]">♥ {fmt(r.likes)}</td>
</tr>
)
})}
</tbody>
</table>
)
}
function Home({ go }) {
const [hub, setHub] = useState(null)
const [hubErr, setHubErr] = useState(null)
const [boards, setBoards] = useState(null)
const [board, setBoard] = useState('gainers_7d')
const [top, setTop] = useState(null)
const [trend, setTrend] = useState(null)
useEffect(() => {
api.hub().then(setHub).catch(setHubErr)
api.leaderboards().then(setBoards).catch(() => {})
hubTop('downloads', 12).then(setTop).catch(() => {})
hubTop('trendingScore', 10).then(setTrend).catch(() => {})
}, [])
const pulse = useMemo(() => {
if (!hub || !hub.day) return null
const byDay = new Map()
const low = new Set(hub.low_days || [])
hub.day.forEach((day, i) => { if (low.has(day)) return; byDay.set(day, (byDay.get(day) || 0) + (hub.dl[i] || 0)) })
const days = Array.from(byDay.keys()).sort()
return { days, vals: days.map((d) => byDay.get(d)) }
}, [hub])
const yestTotal = pulse ? pulse.vals[pulse.vals.length - 1] : null
const prevTotal = pulse ? pulse.vals[pulse.vals.length - 8] : null
const week = pulse ? pulse.vals.slice(-7).reduce((a, b) => a + b, 0) : null
return (
<div className="rise">
<h1 className="max-w-2xl pt-10 text-3xl font-extrabold leading-[1.08] tracking-tight sm:pt-14 sm:text-[42px]">
The pulse of the <span className="text-[var(--accent)]">whole Hub</span>, day by day.
</h1>
<p className="mt-3 max-w-xl text-sm leading-relaxed text-[var(--dim)] sm:text-[15px]">
Download history for every model on Hugging Face — search any repo, follow its curve,
climb through the leaderboards. ~1M+ models, updated daily.
</p>
{hubErr && <ErrBox e={hubErr} retry={() => location.reload()} />}
{!pulse && !hubErr && (
<div className="mt-10 rounded-2xl border border-[var(--line)] bg-[var(--panel)] p-6">
<div className="h-40 animate-pulse rounded-lg bg-[var(--line2)]" />
</div>
)}
{pulse && (
<section className="mt-8 rounded-2xl border border-[var(--line)] bg-[var(--panel)] p-4 sm:p-6">
<div className="flex flex-wrap items-baseline justify-between gap-2">
<h2 className="text-[13px] font-bold uppercase tracking-[0.08em]">Hub downloads per day</h2>
<span className="tnum font-mono text-[11.5px] text-[var(--faint)]">{fmt(yestTotal)} yesterday · {fmt(week)} last 7d · vs {fmt(prevTotal)} week before</span>
</div>
<div className="mt-2"><AreaChart days={pulse.days} vals={pulse.vals} /></div>
<p className="mt-2 text-[11px] text-[var(--faint)]">Aggregate across the 8 largest task families · gaps in the source data are smoothed out</p>
</section>
)}
{trend && trend.length > 0 && (
<section className="mt-6">
<h2 className="text-[12px] font-semibold uppercase tracking-[0.06em] text-[var(--dim)]">Trending right now <span className="font-normal normal-case text-[var(--faint)]">· live from the Hub API</span></h2>
<div className="mt-2 flex flex-wrap gap-2">
{trend.map((m) => (
<a key={m.id} href={'#/m/' + m.id} className="rounded-full border border-[var(--line)] px-3 py-1.5 font-mono text-[11.5px] transition-colors hover:border-[var(--accent)]">
{m.id}
</a>
))}
</div>
</section>
)}
<section className="mt-10">
<div className="flex flex-wrap items-baseline justify-between gap-3">
<h2 className="text-[13px] font-bold uppercase tracking-[0.08em]">Leaderboards <span className="font-medium normal-case tracking-normal text-[var(--faint)]">· week of {boards ? boards.updated : '…'}</span></h2>
</div>
<div className="mt-3 flex flex-wrap gap-2">
{BOARDS.map((b) => (
<button key={b.key} onClick={() => setBoard(b.key)}
className={'rounded-full border px-3.5 py-1.5 text-xs font-semibold transition-colors ' + (board === b.key ? 'border-[var(--accent)] bg-[var(--accent-soft)] text-[var(--ink)]' : 'border-[var(--line)] text-[var(--dim)] hover:border-[var(--accent)] hover:text-[var(--ink)]')}>
{b.label}
</button>
))}
</div>
<p className="mt-2 text-[11.5px] text-[var(--faint)]">{(BOARDS.find((b) => b.key === board) || {}).sub}</p>
<div className="mt-2">
{boards ? <BoardTable rows={boards[board]} go={go} isAuthors={board.startsWith('authors')} /> : <Loading label="Loading leaderboards…" />}
</div>
</section>
{top && (
<section className="mt-10">
<h2 className="border-b border-[var(--ink)] pb-2 text-[13px] font-bold uppercase tracking-[0.08em]">Most downloaded right now <span className="font-medium normal-case tracking-normal text-[var(--faint)]">· live from the Hub API</span></h2>
<table className="mt-1 w-full">
<tbody>
{top.map((m, i) => (
<tr key={m.id} onClick={() => go('#/m/' + m.id)} className="cursor-pointer border-b border-[var(--line2)] transition-colors hover:bg-[var(--accent-soft)]/40">
<td className="tnum w-8 py-2.5 pl-1 text-[var(--faint)]">{i + 1}</td>
<td className="break-all py-2.5 pr-3 font-mono text-[12.5px] font-semibold">{m.id}<span className="ml-2 hidden font-sans text-[11px] font-normal text-[var(--faint)] lg:inline">{m.pipeline_tag || ''}</span></td>
<td className="tnum px-3 py-2.5 text-right font-semibold">{fmt(m.downloads)}</td>
<td className="tnum hidden px-3 py-2.5 text-right text-[var(--dim)] sm:table-cell">{fmt(m.downloadsAllTime)} all time</td>
<td className="tnum hidden px-3 py-2.5 pr-1 text-right text-[var(--dim)] md:table-cell">♥ {fmt(m.likes)}</td>
</tr>
))}
</tbody>
</table>
</section>
)}
</div>
)
}
export default function App() {
const [route, setRoute] = useState(parseHash())
const [theme, setTheme] = useState(() => (localStorage.getItem('hf-theme') || 'dark'))
const [query, setQuery] = useState('')
const [results, setResults] = useState(null)
const [searching, setSearching] = useState(false)
const searchRef = useRef(null)
const boxRef = useRef(null)
useEffect(() => {
document.documentElement.classList.toggle('light', theme === 'light')
localStorage.setItem('hf-theme', theme)
}, [theme])
useEffect(() => {
const f = () => { setRoute(parseHash()); setResults(null); setQuery('') }
window.addEventListener('hashchange', f)
return () => window.removeEventListener('hashchange', f)
}, [])
useEffect(() => {
const onKey = (e) => {
if (e.key === '/' && document.activeElement !== searchRef.current) { e.preventDefault(); searchRef.current?.focus() }
if (e.key === 'Escape') { setResults(null); searchRef.current?.blur() }
}
const onClick = (e) => { if (boxRef.current && !boxRef.current.contains(e.target)) setResults(null) }
window.addEventListener('keydown', onKey)
window.addEventListener('mousedown', onClick)
return () => { window.removeEventListener('keydown', onKey); window.removeEventListener('mousedown', onClick) }
}, [])
useEffect(() => {
const q = query.trim()
if (q.length < 2) { setResults(null); setSearching(false); return }
setSearching(true)
const t = setTimeout(() => {
api.search(q).then((r) => { setResults(r.slice(0, 8)); setSearching(false) }).catch(() => { setResults([]); setSearching(false) })
}, 250)
return () => clearTimeout(t)
}, [query])
const go = (href) => { location.hash = href }
const open = (href) => { setResults(null); setQuery(''); go(href) }
return (
<div className="min-h-screen">
<header className="sticky top-0 z-40 border-b border-[var(--line)] bg-[var(--bg)]/95">
<div className="mx-auto flex max-w-6xl flex-wrap items-center gap-x-4 gap-y-2 px-4 py-3 sm:px-6">
<a href="#/" className="flex items-baseline gap-1 text-xl font-extrabold tracking-tight">
hub<span className="text-[var(--accent)]">·</span><span className="text-[var(--dim)]">flow</span>
</a>
<span className="hidden rounded-full border border-[var(--line)] px-2.5 py-0.5 font-mono text-[11px] text-[var(--dim)] sm:inline">
{route.v === 'home' ? 'every model on the hub' : route.v === 'model' ? 'model' : 'author'}
</span>
<div className="relative ml-auto" ref={boxRef}>
<input ref={searchRef} value={query} onChange={(e) => setQuery(e.target.value)}
placeholder={'Search models /'}
className="w-44 rounded-lg border border-[var(--line)] bg-[var(--panel)] px-3 py-1.5 font-mono text-xs text-[var(--ink)] placeholder-[var(--faint)] outline-none transition-colors focus:border-[var(--accent)] sm:w-64" />
{results && results.length > 0 && (
<div className="absolute right-0 top-10 z-50 w-[320px] overflow-hidden rounded-xl border border-[var(--line)] bg-[var(--panel)] shadow-2xl">
{results.map((r) => (
<button key={r.id} onClick={() => open('#/m/' + r.id)} className="flex w-full items-baseline justify-between gap-3 px-3.5 py-2 text-left transition-colors hover:bg-[var(--accent-soft)]">
<span className="truncate font-mono text-[12px] font-semibold text-[var(--ink)]">{r.id}</span>
<span className="tnum shrink-0 font-mono text-[11px] text-[var(--faint)]">{fmt(r.dl30)}/30d</span>
</button>
))}
</div>
)}
{results && !results.length && !searching && (
<div className="absolute right-0 top-10 z-50 w-[320px] rounded-xl border border-[var(--line)] bg-[var(--panel)] px-3.5 py-3 text-xs text-[var(--dim)] shadow-2xl">Nothing found for “{query}”</div>
)}
</div>
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')} aria-label="toggle theme"
className="rounded-lg border border-[var(--line)] px-2.5 py-1.5 text-xs text-[var(--dim)] transition-colors hover:text-[var(--ink)]">
{theme === 'dark' ? '☾' : '☀'}
</button>
</div>
</header>
<div className="grid-tex pointer-events-none absolute inset-x-0 top-0 h-56 opacity-40" aria-hidden="true" />
<main className="relative mx-auto max-w-6xl px-4 pb-16 sm:px-6">
{route.v === 'home' && <Home go={open} />}
{route.v === 'model' && <ModelPage key={route.id} id={route.id} go={open} />}
{route.v === 'author' && <AuthorPage key={route.author} author={route.author} go={open} />}
<footer className="mt-16 border-t border-[var(--line)] pt-6 text-xs leading-relaxed text-[var(--faint)]">
<p>
<b className="text-[var(--dim)]">Data & history API:</b>{' '}
<a className="underline decoration-[var(--line)] underline-offset-2 hover:decoration-[var(--dim)]" href="https://modelpulse.ifsp.dev" target="_blank" rel="noopener">model-pulse</a>{' '}
over the open dataset{' '}
<a className="underline decoration-[var(--line)] underline-offset-2 hover:decoration-[var(--dim)]" href="https://huggingface.co/datasets/modelpulse/model-pulse-data" target="_blank" rel="noopener">modelpulse/model-pulse-data</a>{' '}
(built on <a className="underline decoration-[var(--line)] underline-offset-2 hover:decoration-[var(--dim)]" href="https://huggingface.co/datasets/cfahlgren1/hub-stats" target="_blank" rel="noopener">cfahlgren1/hub-stats</a>).
“Right now” lists come live from the public Hugging Face Hub API. Big thanks to the model-pulse project for open-sourcing both data and API.
</p>
<p className="mt-1.5">
<b className="text-[var(--dim)]">hub-flow</b> is an independent project, not affiliated with Hugging Face or model-pulse.{' '}
Sibling space: <a className="underline decoration-[var(--line)] underline-offset-2 hover:decoration-[var(--dim)]" href="https://mindchain-model-flow.static.hf.space/" target="_blank" rel="noopener">mindchain/model-flow</a>.{' '}
Press <kbd className="rounded border border-[var(--line)] px-1 font-mono">/</kbd> to search any of the ~1M models.
</p>
</footer>
</main>
</div>
)
}