Spaces:
Running
Running
Download source/src/App.jsx from mindchain/model-flow: direct link, hf CLI and curl.
- Browser
- Download file 24.4 kB
-
https://huggingface.co/spaces/mindchain/model-flow/resolve/main/source/src/App.jsx
- Command line
-
hf download hf://spaces/mindchain/model-flow/source/src/App.jsx
-
curl -L -o App.jsx https://huggingface.co/spaces/mindchain/model-flow/resolve/main/source/src/App.jsx
24.4 kB
| import React, { useEffect, useMemo, useRef, useState } from 'react' | |
| const nf = new Intl.NumberFormat('en-US') | |
| const fmt = (n) => (n == null ? '—' : nf.format(n)) | |
| const FAMILIES = [ | |
| { key: 'imajev', label: 'Imajev', sub: 'vision decision models', match: /imajev/, chip: 'bg-[var(--accent)] text-black' }, | |
| { key: 'jevk5', label: 'JevK5', sub: 'decision models', match: /jevk5/, chip: 'bg-emerald-400/90 text-black' }, | |
| { key: 'decider', label: 'Decider', sub: 'typed decisions', match: /decider/, chip: 'bg-sky-400/90 text-black' }, | |
| { key: 'train', label: 'Training & Experiments', sub: '', match: /qwen|grpo|t5|gemma|sdxl|kaggle|sft|dpo|colab|needle|arithmetic|sailing|functiongemma|reward|falcon|offload|outputs|selftest/i, fold: true }, | |
| { key: 'legacy', label: 'Legacy', sub: '', match: /./, fold: true }, | |
| ] | |
| function dailyDeltas(series) { | |
| const out = [] | |
| for (let i = 1; i < series.length; i++) out.push([series[i][0], Math.max(0, series[i][1] - series[i - 1][1])]) | |
| return out | |
| } | |
| function sumLast(deltas, n) { | |
| const t = deltas.slice(-n) | |
| return t.reduce((a, x) => a + x[1], 0) | |
| } | |
| function useCountUp(target, dur = 900) { | |
| const [v, setV] = useState(0) | |
| useEffect(() => { | |
| let raf | |
| const t0 = performance.now() | |
| const tick = (t) => { | |
| const p = Math.min(1, (t - t0) / dur) | |
| setV(Math.round(target * (1 - Math.pow(1 - p, 3)))) | |
| if (p < 1) raf = requestAnimationFrame(tick) | |
| } | |
| raf = requestAnimationFrame(tick) | |
| return () => cancelAnimationFrame(raf) | |
| }, [target, dur]) | |
| return v | |
| } | |
| function Sparkline({ data, w = 96, h = 28, color = 'var(--accent)' }) { | |
| if (!data.length) return null | |
| const max = Math.max(1, ...data.map((d) => d[1])) | |
| const pts = data.map((d, i) => [ | |
| (i / Math.max(1, data.length - 1)) * (w - 4) + 2, | |
| h - 3 - (d[1] / max) * (h - 8), | |
| ]) | |
| const line = pts.map((p, i) => (i ? 'L' : 'M') + p[0].toFixed(1) + ' ' + p[1].toFixed(1)).join(' ') | |
| const area = line + ' L' + pts[pts.length - 1][0].toFixed(1) + ' ' + (h - 1) + ' L' + pts[0][0].toFixed(1) + ' ' + (h - 1) + ' Z' | |
| return ( | |
| <svg width={w} height={h} viewBox={'0 0 ' + w + ' ' + h} className="overflow-visible" aria-hidden="true"> | |
| <path d={area} fill={color} opacity="0.13" /> | |
| <path d={line} fill="none" stroke={color} strokeWidth="1.7" strokeLinejoin="round" strokeLinecap="round" /> | |
| <circle cx={pts[pts.length - 1][0]} cy={pts[pts.length - 1][1]} r="2.2" fill={color} /> | |
| </svg> | |
| ) | |
| } | |
| function BigChart({ model }) { | |
| const wrapRef = useRef(null) | |
| const [hover, setHover] = useState(null) | |
| const daily = useMemo(() => dailyDeltas(model.series), [model]) | |
| const cum = useMemo(() => daily.map((d, i) => [d[0], model.series[i + 1][1]]), [model]) | |
| const W = 1000, H = 300, pad = { l: 52, r: 16, t: 18, b: 28 } | |
| if (daily.length < 2) { | |
| return ( | |
| <p className="py-10 text-center text-sm text-[var(--dim)]"> | |
| Not enough history yet — first snapshot {model.series[0] ? model.series[0][0] : '?'} · come back tomorrow. | |
| </p> | |
| ) | |
| } | |
| const dmax = Math.max(1, ...daily.map((d) => d[1])) | |
| const cmax = Math.max(1, ...cum.map((d) => d[1])) | |
| const iw = W - pad.l - pad.r, ih = H - pad.t - pad.b | |
| const x = (i) => pad.l + (i / (daily.length - 1)) * iw | |
| const yd = (v) => pad.t + ih - (v / dmax) * ih | |
| const yc = (v) => pad.t + ih - (v / cmax) * ih | |
| const cpts = cum.map((d, i) => x(i).toFixed(1) + ',' + yc(d[1]).toFixed(1)).join(' ') | |
| const bars = daily.map((d, i) => { | |
| const bw = Math.max(2, iw / daily.length - 2) | |
| return ( | |
| <rect key={d[0]} x={x(i) - bw / 2} y={yd(d[1])} width={bw} height={pad.t + ih - yd(d[1])} rx="2" | |
| fill={hover === i ? 'var(--accent)' : 'var(--bar)'} opacity={hover == null || hover === i ? 1 : 0.55}> | |
| <title>{d[0] + ': ' + fmt(d[1]) + ' downloads'}</title> | |
| </rect> | |
| ) | |
| }) | |
| const grid = [0, 1, 2, 3, 4].map((g) => { | |
| const gy = pad.t + ih - (g / 4) * ih | |
| return ( | |
| <g key={g}> | |
| <line x1={pad.l} y1={gy} x2={W - pad.r} y2={gy} stroke="var(--line2)" /> | |
| <text x={pad.l - 8} y={gy + 4} fontSize="10.5" fill="var(--faint)" textAnchor="end" className="tnum">{fmt(Math.round((dmax * g) / 4))}</text> | |
| </g> | |
| ) | |
| }) | |
| const onMove = (e) => { | |
| const rect = wrapRef.current.getBoundingClientRect() | |
| const rel = (e.clientX - rect.left) / rect.width | |
| const i = Math.max(0, Math.min(daily.length - 1, Math.round(rel * (daily.length - 1)))) | |
| setHover(i) | |
| } | |
| const hd = hover != null ? daily[hover] : null | |
| return ( | |
| <div ref={wrapRef} className="relative mt-2" onMouseMove={onMove} onMouseLeave={() => setHover(null)}> | |
| <svg viewBox={'0 0 ' + W + ' ' + H} className="w-full" role="img" aria-label={'download history for ' + model.id}> | |
| <defs> | |
| <linearGradient id="cumfade" x1="0" y1="0" x2="0" y2="1"> | |
| <stop offset="0%" stopColor="var(--accent)" stopOpacity="0.25" /> | |
| <stop offset="100%" stopColor="var(--accent)" stopOpacity="0" /> | |
| </linearGradient> | |
| </defs> | |
| {grid} | |
| {bars} | |
| <polygon points={x(0) + ',' + (pad.t + ih) + ' ' + cpts + ' ' + x(daily.length - 1) + ',' + (pad.t + ih)} fill="url(#cumfade)" /> | |
| <polyline points={cpts} fill="none" stroke="var(--accent)" strokeWidth="2.6" strokeLinejoin="round" /> | |
| {hover != null && ( | |
| <g> | |
| <line x1={x(hover)} y1={pad.t} x2={x(hover)} y2={pad.t + ih} stroke="var(--faint)" strokeDasharray="3 3" /> | |
| <circle cx={x(hover)} cy={yc(cum[hover][1])} r="4" fill="var(--accent)" stroke="var(--bg)" strokeWidth="2" /> | |
| </g> | |
| )} | |
| <text x={W - pad.r} y={yc(cum[cum.length - 1][1]) - 8} fontSize="11.5" fontWeight="700" fill="var(--accent)" textAnchor="end" className="tnum"> | |
| {fmt(cum[cum.length - 1][1])} | |
| </text> | |
| {[daily[0][0], daily[Math.floor(daily.length / 2)][0], daily[daily.length - 1][0]].map((t, i) => ( | |
| <text key={t} x={i === 0 ? pad.l : i === 1 ? W / 2 : W - pad.r} y={H - 8} fontSize="10.5" fill="var(--faint)" textAnchor={i === 0 ? 'start' : i === 1 ? 'middle' : 'end'}>{t}</text> | |
| ))} | |
| </svg> | |
| {hd && ( | |
| <div | |
| className="pointer-events-none absolute top-1 z-10 rounded-lg border border-[var(--line)] bg-[var(--panel)] px-3 py-2 text-xs shadow-xl" | |
| style={{ left: 'min(max(' + ((hover / Math.max(1, daily.length - 1)) * 100) + '% - 60px, 0px), calc(100% - 150px))' }}> | |
| <div className="font-semibold text-[var(--ink)]">{hd[0]}</div> | |
| <div className="tnum mt-0.5 text-[var(--dim)]">{fmt(hd[1])} downloads/day</div> | |
| <div className="tnum text-[var(--accent)]">{fmt(cum[hover][1])} all time</div> | |
| </div> | |
| )} | |
| </div> | |
| ) | |
| } | |
| function Stat({ k, v, d, dCls = '' }) { | |
| const n = useCountUp(v) | |
| return ( | |
| <div className="min-w-[130px] flex-1 px-4 py-3.5 sm:px-5"> | |
| <div className="text-[10.5px] font-semibold uppercase tracking-[0.08em] text-[var(--faint)]">{k}</div> | |
| <div className="tnum mt-0.5 text-2xl font-bold tracking-tight sm:text-[26px]">{fmt(n)}</div> | |
| {d && <div className={'mt-0.5 font-mono text-[11.5px] ' + dCls}>{d}</div>} | |
| </div> | |
| ) | |
| } | |
| function ModelDetail({ model, live, onClose }) { | |
| const daily = useMemo(() => dailyDeltas(model.series), [model]) | |
| const best = daily.reduce((a, d) => (d[1] > (a ? a[1] : 0) ? d : a), null) | |
| const all = live?.downloads ?? model.downloadsAllTime | |
| return ( | |
| <div className="rise mt-2 rounded-xl border border-[var(--line)] bg-[var(--panel)] p-4 sm:p-6"> | |
| <div className="flex flex-wrap items-baseline justify-between gap-2"> | |
| <div> | |
| <h3 className="font-mono text-base font-bold sm:text-lg">{model.id}</h3> | |
| <div className="mt-1 flex flex-wrap gap-x-5 gap-y-1 text-xs text-[var(--dim)]"> | |
| <span className="tnum"><b className="text-[var(--ink)]">{fmt(all)}</b> all time</span> | |
| <span className="tnum">best day <b className="text-[var(--ink)]">{best ? fmt(best[1]) : '—'}</b> {best ? best[0] : ''}</span> | |
| <span className="tnum">♥ {fmt(live?.likes ?? model.likes)}</span> | |
| {model.created && <span>since {model.created}</span>} | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <a href={'https://huggingface.co/' + model.id} target="_blank" rel="noopener" | |
| className="rounded-lg border border-[var(--line)] px-3 py-1.5 text-xs font-semibold text-[var(--dim)] transition-colors hover:border-[var(--accent)] hover:text-[var(--ink)]"> | |
| Open on the Hub ↗ | |
| </a> | |
| <button onClick={onClose} className="rounded-lg border border-[var(--line)] px-3 py-1.5 text-xs font-semibold text-[var(--dim)] transition-colors hover:text-[var(--ink)]"> | |
| Close | |
| </button> | |
| </div> | |
| </div> | |
| <div className="mt-1 flex gap-4 text-[11px] text-[var(--dim)]"> | |
| <span><i className="mr-1.5 inline-block h-2 w-2 rounded-[2px] bg-[var(--ink)]" />downloads / day</span> | |
| <span><i className="mr-1.5 inline-block h-2 w-2 rounded-[2px] bg-[var(--accent)]" />cumulative</span> | |
| </div> | |
| <BigChart model={model} /> | |
| </div> | |
| ) | |
| } | |
| function ModelRow({ m, live, open, onToggle }) { | |
| const deltas = useMemo(() => dailyDeltas(m.series), [m]) | |
| const d7 = sumLast(deltas, 7) | |
| const all = live?.downloads ?? m.downloadsAllTime | |
| const short = m.id.replace('mindchain/', '') | |
| return ( | |
| <> | |
| <tr onClick={onToggle} role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === "Enter") onToggle() }} className="group cursor-pointer border-b border-[var(--line2)] transition-colors hover:bg-[var(--accent-soft)]/40"> | |
| <td className="py-3 pl-1 pr-3 align-middle"> | |
| <div className="break-all font-mono text-[13px] font-semibold"> | |
| <span className="text-[var(--faint)]">mindchain/</span>{short} | |
| </div> | |
| {m.pipeline && <div className="mt-0.5 truncate text-[11px] text-[var(--faint)]">{m.pipeline}</div>} | |
| </td> | |
| <td className="hidden px-3 py-3 align-middle md:table-cell"><Sparkline data={deltas.slice(-10)} /></td> | |
| <td className="tnum px-3 py-3 text-right align-middle font-semibold">{fmt(all)}</td> | |
| <td className={'tnum px-3 py-3 text-right align-middle font-mono text-[12.5px] ' + (d7 > 0 ? 'text-[var(--up)]' : 'text-[var(--faint)]')}> | |
| {d7 > 0 ? '+' + fmt(d7) : '0'} | |
| </td> | |
| <td className="tnum hidden px-3 py-3 pr-1 text-right align-middle text-[var(--dim)] md:table-cell">♥ {fmt(live?.likes ?? m.likes)}</td> | |
| </tr> | |
| {open && ( | |
| <tr> | |
| <td colSpan={5} className="border-b border-[var(--line2)] p-0"> | |
| <ModelDetail model={m} live={live} onClose={onToggle} /> | |
| </td> | |
| </tr> | |
| )} | |
| </> | |
| ) | |
| } | |
| function SkeletonRow() { | |
| return ( | |
| <tr className="border-b border-[var(--line2)]"> | |
| <td className="py-4 pl-1"><div className="h-4 w-2/3 animate-pulse rounded bg-[var(--line2)]" /></td> | |
| <td className="hidden md:table-cell"><div className="h-5 w-24 animate-pulse rounded bg-[var(--line2)]" /></td> | |
| <td><div className="ml-auto h-4 w-14 animate-pulse rounded bg-[var(--line2)]" /></td> | |
| <td><div className="ml-auto h-4 w-12 animate-pulse rounded bg-[var(--line2)]" /></td> | |
| <td className="hidden md:table-cell"><div className="ml-auto h-4 w-8 animate-pulse rounded bg-[var(--line2)]" /></td> | |
| </tr> | |
| ) | |
| } | |
| export default function App() { | |
| const [data, setData] = useState(null) | |
| const [error, setError] = useState(false) | |
| const [live, setLive] = useState({}) | |
| const [liveOk, setLiveOk] = useState(false) | |
| const [openId, setOpenId] = useState(null) | |
| const [query, setQuery] = useState('') | |
| const [sortBy, setSortBy] = useState('downloads') | |
| const [theme, setTheme] = useState(() => (localStorage.getItem('mf-theme') || 'dark')) | |
| const searchRef = useRef(null) | |
| useEffect(() => { | |
| document.documentElement.classList.toggle('light', theme === 'light') | |
| localStorage.setItem('mf-theme', theme) | |
| }, [theme]) | |
| useEffect(() => { | |
| fetch('data.json').then((r) => { if (!r.ok) throw new Error(r.status); return r.json() }) | |
| .then((d) => setData(d)).catch(() => setError(true)) | |
| }, []) | |
| useEffect(() => { | |
| fetch('https://huggingface.co/api/models?author=mindchain&limit=100&full=true') | |
| .then((r) => { if (!r.ok) throw new Error(r.status); return r.json() }) | |
| .then((arr) => { const map = {}; arr.forEach((m) => (map[m.id] = m)); setLive(map); setLiveOk(true) }) | |
| .catch(() => setLiveOk(false)) | |
| }, []) | |
| useEffect(() => { | |
| const apply = () => { | |
| const match = location.hash.startsWith('#/m/') ? location.hash.slice(4) : null | |
| setOpenId(match ? decodeURIComponent(match) : null) | |
| } | |
| apply() | |
| window.addEventListener('hashchange', apply) | |
| return () => window.removeEventListener('hashchange', apply) | |
| }, []) | |
| useEffect(() => { | |
| const onKey = (e) => { | |
| if (e.key === '/' && document.activeElement !== searchRef.current) { e.preventDefault(); searchRef.current?.focus() } | |
| if (e.key === 'Escape') { setOpenId(null); history.replaceState(null, '', location.pathname); searchRef.current?.blur() } | |
| } | |
| window.addEventListener('keydown', onKey) | |
| return () => window.removeEventListener('keydown', onKey) | |
| }, []) | |
| const toggle = (id) => { | |
| if (openId === id) { setOpenId(null); history.replaceState(null, '', location.pathname) } | |
| else { setOpenId(id); location.hash = '#/m/' + encodeURIComponent(id) } | |
| } | |
| const models = data?.models ?? [] | |
| const get = (m, k) => (k === 'downloads' ? (live[m.id]?.downloads ?? m.downloadsAllTime) : k === 'delta7' ? sumLast(dailyDeltas(m.series), 7) : m.id) | |
| const groups = useMemo(() => { | |
| const q = query.trim().toLowerCase() | |
| const out = [] | |
| for (const f of FAMILIES) { | |
| let ms = models.filter((m) => f.match.test(m.id)) | |
| if (q) ms = ms.filter((m) => m.id.toLowerCase().includes(q)) | |
| ms = ms.slice().sort((a, b) => (sortBy === 'name' ? a.id.localeCompare(b.id) : get(b, sortBy) - get(a, sortBy))) | |
| if (ms.length) out.push({ ...f, models: ms }) | |
| } | |
| return out | |
| }, [models, query, sortBy, live]) | |
| const port = models.filter((m) => /imajev|jevk5|decider/.test(m.id)) | |
| const tot = (arr) => arr.reduce((a, m) => a + (live[m.id]?.downloads ?? m.downloadsAllTime), 0) | |
| const d7all = models.reduce((a, m) => a + sumLast(dailyDeltas(m.series), 7), 0) | |
| const d7prev = models.reduce((a, m) => { const t = dailyDeltas(m.series).slice(-14, -7); return a + t.reduce((x, y) => x + y[1], 0) }, 0) | |
| let best = { v: 0, day: null } | |
| models.forEach((m) => dailyDeltas(m.series).forEach((d) => { if (d[1] > best.v) best = { v: d[1], day: d[0] } })) | |
| const lastDay = models[0]?.series[models[0].series.length - 1]?.[0] | |
| 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" onClick={(e) => { e.preventDefault(); setOpenId(null); setQuery(''); history.replaceState(null, '', location.pathname); window.scrollTo({ top: 0, behavior: 'smooth' }) }}> | |
| model<span className="text-[var(--accent)]">·</span><span className="text-[var(--dim)]">flow</span> | |
| </a> | |
| <span className={'inline-flex items-center gap-1.5 rounded-full border border-[var(--line)] px-2.5 py-0.5 font-mono text-[11px] text-[var(--dim)]'}> | |
| <span className={'h-[7px] w-[7px] rounded-full ' + (liveOk ? 'live-dot bg-[var(--up)]' : 'bg-[var(--faint)]')} /> | |
| {liveOk ? 'live from hub' : 'snapshot ' + (data?.generated ?? '…')} | |
| </span> | |
| <div className="ml-auto flex items-center gap-2"> | |
| <input ref={searchRef} value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search models /" | |
| className="w-40 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-52" /> | |
| <select value={sortBy} onChange={(e) => setSortBy(e.target.value)} | |
| className="rounded-lg border border-[var(--line)] bg-[var(--panel)] px-2 py-1.5 text-xs text-[var(--dim)] outline-none"> | |
| <option value="downloads">All time</option> | |
| <option value="delta7">7 days</option> | |
| <option value="name">A–Z</option> | |
| </select> | |
| <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> | |
| </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"> | |
| <div className="pt-10 sm:pt-14"> | |
| <h1 className="max-w-2xl text-3xl font-extrabold leading-[1.1] tracking-tight sm:text-[40px]"> | |
| Every download of the <span className="text-[var(--accent)]">mindchain</span> portfolio, day by day. | |
| </h1> | |
| <p className="mt-3 max-w-xl text-sm leading-relaxed text-[var(--dim)] sm:text-[15px]"> | |
| Imajev, JevK5 and Decider — decision models that run on an 8 GB card. | |
| Numbers overlay live from the {' '} | |
| <a className="underline decoration-[var(--line)] underline-offset-2 hover:decoration-[var(--dim)]" href="https://huggingface.co/api/models" target="_blank" rel="noopener">Hub API</a>, | |
| history comes from daily snapshots. | |
| </p> | |
| </div> | |
| {error && ( | |
| <div className="mt-10 rounded-xl border border-[var(--danger)]/40 bg-[var(--danger)]/5 p-8 text-center"> | |
| <p className="text-sm text-[var(--ink)]">Could not load the download history.</p> | |
| <button onClick={() => location.reload()} className="mt-3 rounded-lg border border-[var(--line)] px-4 py-2 text-sm font-semibold hover:border-[var(--accent)]">Retry</button> | |
| </div> | |
| )} | |
| {!data && !error && ( | |
| <div className="mt-10 rounded-xl border border-[var(--line)]"> | |
| <div className="flex flex-wrap gap-0 divide-x divide-[var(--line)]"> | |
| {[0, 1, 2, 3, 4].map((i) => (<div key={i} className="min-w-[130px] flex-1 px-5 py-4"><div className="h-3 w-20 animate-pulse rounded bg-[var(--line2)]" /><div className="mt-2 h-6 w-16 animate-pulse rounded bg-[var(--line2)]" /></div>))} | |
| </div> | |
| <table className="w-full"><tbody>{[0, 1, 2, 3, 4, 5].map((i) => (<SkeletonRow key={i} />))}</tbody></table> | |
| </div> | |
| )} | |
| {data && ( | |
| <> | |
| <div className="rise mt-8 grid grid-cols-2 divide-[var(--line)] overflow-hidden rounded-2xl border border-[var(--line)] bg-[var(--panel)] sm:grid-cols-3 lg:grid-cols-5 lg:divide-x"> | |
| <Stat k="Portfolio downloads" v={tot(port)} d={fmt(tot(models)) + ' incl. legacy'} /> | |
| <Stat k="Last 7 days" v={d7all} d={'vs ' + fmt(d7prev) + ' before'} dCls="text-[var(--up)]" /> | |
| <Stat k="Best day" v={best.v} d={best.day ?? ''} /> | |
| <Stat k="Models tracked" v={models.length} d={'snapshot ' + (lastDay ?? '')} /> | |
| <Stat k="Likes" v={models.reduce((a, m) => a + (live[m.id]?.likes ?? m.likes), 0)} d="on the hub" /> | |
| </div> | |
| <div className="mt-6 flex flex-wrap gap-2"> | |
| {groups.map((g) => ( | |
| <a key={g.key} href={'#f-' + g.key} | |
| className="rounded-full border border-[var(--line)] px-3.5 py-1.5 text-xs font-semibold text-[var(--dim)] transition-colors hover:border-[var(--accent)] hover:text-[var(--ink)]"> | |
| {g.label} <span className="tnum text-[var(--faint)]">{g.models.length}</span> | |
| </a> | |
| ))} | |
| </div> | |
| {groups.length === 0 && ( | |
| <div className="mt-16 text-center"> | |
| <p className="text-sm text-[var(--dim)]">No models match <span className="font-mono text-[var(--ink)]">{query}</span></p> | |
| <button onClick={() => setQuery('')} className="mt-3 rounded-lg border border-[var(--line)] px-4 py-2 text-xs font-semibold hover:border-[var(--accent)]">Clear search</button> | |
| </div> | |
| )} | |
| {groups.map((g) => { | |
| const gtot = tot(g.models) | |
| return ( | |
| <section key={g.key} id={'f-' + g.key} className="mt-10 scroll-mt-20"> | |
| <div className="flex items-baseline justify-between gap-3 border-b border-[var(--ink)] pb-2"> | |
| <h2 className="flex items-center gap-2.5 text-[13px] font-bold uppercase tracking-[0.08em]"> | |
| <span className={'inline-block h-2 w-2 rounded-full ' + g.chip} /> | |
| {g.label} | |
| {g.sub && <span className="font-medium normal-case tracking-normal text-[var(--faint)]">· {g.sub}</span>} | |
| </h2> | |
| <span className="tnum font-mono text-[11.5px] text-[var(--faint)]">{g.models.length} models · {fmt(gtot)} downloads</span> | |
| </div> | |
| <table className="mt-1 w-full"> | |
| <thead> | |
| <tr className="text-[10.5px] uppercase tracking-[0.06em] text-[var(--faint)]"> | |
| <th className="py-2 pl-1 text-left font-semibold">Model</th> | |
| <th className="hidden px-3 py-2 text-left font-semibold md:table-cell">Last 10 days</th> | |
| <th className="px-3 py-2 text-right font-semibold">All time</th> | |
| <th className="px-3 py-2 text-right font-semibold">Δ 7d</th> | |
| <th className="hidden px-3 py-2 pr-1 text-right font-semibold md:table-cell">♥</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {g.models.map((m) => ( | |
| <ModelRow key={m.id} m={m} live={live[m.id]} open={openId === m.id} onToggle={() => toggle(m.id)} /> | |
| ))} | |
| </tbody> | |
| </table> | |
| </section> | |
| ) | |
| })} | |
| <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:</b> daily snapshots from{' '} | |
| <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>{' '} | |
| (builds 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>), | |
| current values live from the public Hub API. Companion dataset:{' '} | |
| <a className="underline decoration-[var(--line)] underline-offset-2 hover:decoration-[var(--dim)]" href="https://huggingface.co/datasets/mindchain/model-flow-data" target="_blank" rel="noopener">mindchain/model-flow-data</a>. | |
| </p> | |
| <p className="mt-1.5"> | |
| <b className="text-[var(--dim)]">model-flow</b> is an independent project, not affiliated with Hugging Face. Inspired by{' '} | |
| <a className="underline decoration-[var(--line)] underline-offset-2 hover:decoration-[var(--dim)]" href="https://huggingface.co/spaces/tardellirs/model-pulse" target="_blank" rel="noopener">model-pulse</a>. | |
| Built with React & Tailwind. Press <kbd className="rounded border border-[var(--line)] px-1 font-mono">/</kbd> to search. | |
| </p> | |
| </footer> | |
| </> | |
| )} | |
| </main> | |
| </div> | |
| ) | |
| } | |