File size: 15,958 Bytes
174bac0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
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>
  )
}