Spaces:
Runtime error
Runtime error
Download static/index.html from RohitPoreddy/OpenModelsTimeline: direct link, hf CLI and curl.
- Browser
- Download file 27.4 kB
-
https://huggingface.co/spaces/RohitPoreddy/OpenModelsTimeline/resolve/main/static/index.html
- Command line
-
hf download hf://spaces/RohitPoreddy/OpenModelsTimeline/static/index.html
-
curl -L -o index.html https://huggingface.co/spaces/RohitPoreddy/OpenModelsTimeline/resolve/main/static/index.html
27.4 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <title>AI Model & API Release Timeline</title> | |
| <style> | |
| :root{ | |
| --bg: #f5f5f7; | |
| --panel: rgba(255,255,255,.72); | |
| --panel-solid: #ffffff; | |
| --text: #1d1d1f; | |
| --muted: #6e6e73; | |
| --border: rgba(0,0,0,.08); | |
| --shadow: 0 8px 30px rgba(0,0,0,.08); | |
| --accent: #0071e3; /* Apple blue */ | |
| --good: #34c759; /* Apple green */ | |
| --bad: #ff3b30; /* Apple red */ | |
| --warn: #ffcc00; /* Apple yellow */ | |
| --radius-xl: 22px; | |
| --radius-lg: 16px; | |
| --radius-md: 12px; | |
| --radius-sm: 10px; | |
| } | |
| *{ box-sizing: border-box; } | |
| html, body { height: 100%; } | |
| body{ | |
| margin:0; | |
| font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", "Segoe UI", Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji"; | |
| color: var(--text); | |
| background: radial-gradient(1200px 800px at 20% 0%, rgba(0,113,227,.15), transparent 55%), | |
| radial-gradient(900px 700px at 90% 20%, rgba(52,199,89,.10), transparent 55%), | |
| var(--bg); | |
| } | |
| a{ color: var(--accent); text-decoration: none; } | |
| a:hover{ text-decoration: underline; } | |
| /* Top bar */ | |
| .topbar{ | |
| position: sticky; | |
| top: 0; | |
| z-index: 50; | |
| backdrop-filter: blur(18px); | |
| -webkit-backdrop-filter: blur(18px); | |
| background: rgba(245,245,247,.75); | |
| border-bottom: 1px solid var(--border); | |
| } | |
| .topbar-inner{ | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| padding: 14px 16px; | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .brand{ | |
| display:flex; align-items:center; gap:10px; | |
| min-width: 230px; | |
| } | |
| .logo{ | |
| width: 34px; height: 34px; border-radius: 10px; | |
| background: linear-gradient(135deg, rgba(0,113,227,.95), rgba(88,86,214,.92)); | |
| box-shadow: 0 12px 24px rgba(0,113,227,.22); | |
| } | |
| .brand h1{ | |
| font-size: 14px; | |
| margin: 0; | |
| letter-spacing: .2px; | |
| font-weight: 650; | |
| } | |
| .brand p{ | |
| margin: 0; | |
| font-size: 12px; | |
| color: var(--muted); | |
| } | |
| .search{ | |
| flex: 1; | |
| display:flex; | |
| align-items:center; | |
| gap: 10px; | |
| padding: 10px 12px; | |
| border-radius: 999px; | |
| border: 1px solid var(--border); | |
| background: rgba(255,255,255,.65); | |
| box-shadow: 0 1px 0 rgba(255,255,255,.6) inset; | |
| } | |
| .search input{ | |
| border:none; | |
| outline:none; | |
| background: transparent; | |
| font-size: 14px; | |
| width: 100%; | |
| color: var(--text); | |
| } | |
| .pillrow{ | |
| display:flex; gap:8px; align-items:center; | |
| flex-wrap: wrap; | |
| justify-content: flex-end; | |
| } | |
| .pill{ | |
| padding: 8px 12px; | |
| border-radius: 999px; | |
| border: 1px solid var(--border); | |
| background: rgba(255,255,255,.55); | |
| color: var(--text); | |
| font-size: 13px; | |
| cursor: pointer; | |
| user-select: none; | |
| transition: transform .12s ease, background .15s ease, border-color .15s ease; | |
| } | |
| .pill:hover{ transform: translateY(-1px); } | |
| .pill.active{ | |
| background: rgba(0,113,227,.12); | |
| border-color: rgba(0,113,227,.35); | |
| color: var(--accent); | |
| font-weight: 650; | |
| } | |
| .select{ | |
| padding: 8px 12px; | |
| border-radius: 999px; | |
| border: 1px solid var(--border); | |
| background: rgba(255,255,255,.55); | |
| font-size: 13px; | |
| color: var(--text); | |
| outline: none; | |
| cursor: pointer; | |
| } | |
| /* Summary row */ | |
| .summary{ | |
| max-width: 1200px; | |
| margin: 14px auto 0; | |
| padding: 0 16px; | |
| display: grid; | |
| grid-template-columns: 1.3fr .7fr; | |
| gap: 12px; | |
| } | |
| .card{ | |
| background: var(--panel); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-xl); | |
| box-shadow: var(--shadow); | |
| backdrop-filter: blur(18px); | |
| -webkit-backdrop-filter: blur(18px); | |
| } | |
| .card-inner{ padding: 14px 16px; } | |
| .kpis{ | |
| display:flex; gap: 14px; flex-wrap: wrap; align-items: center; | |
| } | |
| .kpi{ | |
| display:flex; gap:10px; align-items:center; | |
| padding: 10px 12px; | |
| border-radius: 16px; | |
| background: rgba(255,255,255,.55); | |
| border: 1px solid rgba(0,0,0,.06); | |
| } | |
| .dot{ | |
| width: 10px; height: 10px; border-radius: 999px; | |
| } | |
| .kpi .label{ font-size: 12px; color: var(--muted); margin:0; } | |
| .kpi .value{ font-size: 16px; font-weight: 750; margin:0; } | |
| .hint{ | |
| color: var(--muted); | |
| font-size: 12px; | |
| line-height: 1.35; | |
| } | |
| /* Timeline layout */ | |
| .wrap{ | |
| max-width: 1200px; | |
| margin: 14px auto 30px; | |
| padding: 0 16px; | |
| } | |
| /* .timeline{ | |
| display:flex; | |
| gap: 14px; | |
| overflow-x: auto; | |
| padding: 14px; | |
| scroll-snap-type: x mandatory; | |
| border-radius: var(--radius-xl); | |
| border: 1px solid var(--border); | |
| background: rgba(255,255,255,.35); | |
| backdrop-filter: blur(18px); | |
| -webkit-backdrop-filter: blur(18px); | |
| box-shadow: var(--shadow); | |
| } */ | |
| .timeline { | |
| display: flex; | |
| gap: 14px; | |
| /* FULL WIDTH FIX */ | |
| width: 100vw; | |
| margin-left: calc(50% - 50vw); | |
| margin-right: calc(50% - 50vw); | |
| overflow-x: auto; | |
| padding: 20px 28px; | |
| scroll-snap-type: x mandatory; | |
| border-radius: 0; /* full-bleed = no rounded corners */ | |
| border-top: 1px solid var(--border); | |
| border-bottom: 1px solid var(--border); | |
| background: rgba(255,255,255,0.55); | |
| backdrop-filter: blur(20px); | |
| -webkit-backdrop-filter: blur(20px); | |
| box-shadow: inset 0 1px 0 rgba(255,255,255,.4); | |
| } | |
| .timeline::-webkit-scrollbar{ height: 10px; } | |
| .timeline::-webkit-scrollbar-thumb{ background: rgba(0,0,0,.18); border-radius: 999px; } | |
| .timeline::-webkit-scrollbar-track{ background: rgba(0,0,0,.06); border-radius: 999px; } | |
| .timeline::-webkit-scrollbar { | |
| height: 6px; | |
| } | |
| .timeline::-webkit-scrollbar-thumb { | |
| background: rgba(0,0,0,0.25); | |
| border-radius: 999px; | |
| } | |
| .timeline::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| /* Scroll buttons */ | |
| .scroll-controls{ position: relative; } | |
| .scroll-btn{ | |
| position: absolute; | |
| top: 50%; transform: translateY(-50%); | |
| width: 44px; height: 44px; border-radius: 999px; | |
| border: 1px solid var(--border); | |
| background: rgba(255,255,255,.9); | |
| box-shadow: 0 6px 18px rgba(0,0,0,.06); | |
| display:flex; align-items:center; justify-content:center; | |
| cursor:pointer; font-size:18px; color:var(--text); | |
| z-index: 40; | |
| } | |
| .scroll-btn.left{ left: 8px; } | |
| .scroll-btn.right{ right: 8px; } | |
| .scroll-btn:active{ transform: translateY(-50%) scale(.98); } | |
| .scroll-btn.fixed{ position: fixed; left: 12px; top: 48%; transform: translateY(-50%); } | |
| .scroll-btn.fixed.right{ left: auto; right: 12px; } | |
| .scroll-btn.fixed{ box-shadow: 0 10px 30px rgba(0,0,0,.12); background: rgba(255,255,255,.95); } | |
| /* translucent vertical bands behind fixed scroll buttons to make them pop */ | |
| .scroll-band{ | |
| position: fixed; | |
| top: 0; | |
| bottom: 0; | |
| width: 60px; | |
| pointer-events: none; | |
| z-index: 30; | |
| backdrop-filter: blur(6px); | |
| } | |
| .scroll-band.left{ | |
| left: 0; | |
| background: linear-gradient(to right, rgba(81, 127, 255, 0.1), rgba(0,0,0,0)); | |
| } | |
| .scroll-band.right{ | |
| right: 0; | |
| background: linear-gradient(to left, rgba(252, 188, 188, 0.1), rgba(0,0,0,0)); | |
| } | |
| .month{ | |
| scroll-snap-align: start; | |
| min-width: 320px; | |
| max-width: 320px; | |
| display:flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| } | |
| .month-header{ | |
| position: sticky; /* stays visible within the month column while scrolling its content */ | |
| top: 74px; /* below topbar */ | |
| z-index: 20; | |
| display:flex; | |
| align-items:center; | |
| justify-content: space-between; | |
| padding: 12px 14px; | |
| border-radius: 16px; | |
| background: linear-gradient(135deg, rgba(0,113,227,0.95), rgba(88,86,214,0.92)); | |
| border: 1px solid rgba(255,255,255,0.14); | |
| box-shadow: 0 6px 18px rgba(0,0,0,0.08); | |
| color: #fff; | |
| backdrop-filter: blur(10px); | |
| -webkit-backdrop-filter: blur(10px); | |
| } | |
| .month-header h2{ | |
| margin:0; | |
| font-size: 14px; | |
| letter-spacing: .2px; | |
| font-weight: 750; | |
| } | |
| .month-header .count{ | |
| font-size: 12px; | |
| color: rgba(255,255,255,0.88); | |
| } | |
| .events{ | |
| display:flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| /* reserve vertical space so sticky month header does not visually overlap the first cards */ | |
| padding-top: 64px; | |
| } | |
| .event{ | |
| background: rgba(255,255,255,.78); | |
| border: 1px solid rgba(0,0,0,.06); | |
| border-radius: 18px; | |
| padding: 12px 12px 11px; | |
| box-shadow: 0 10px 25px rgba(0,0,0,.06); | |
| transition: transform .12s ease, box-shadow .2s ease; | |
| cursor: pointer; | |
| } | |
| .event:hover{ | |
| transform: translateY(-1px); | |
| box-shadow: 0 14px 32px rgba(0,0,0,.09); | |
| } | |
| .event-title{ | |
| margin:0; | |
| font-size: 13px; | |
| font-weight: 720; | |
| letter-spacing: .1px; | |
| } | |
| .event-desc{ | |
| margin: 6px 0 0; | |
| font-size: 12.5px; | |
| color: var(--muted); | |
| line-height: 1.35; | |
| } | |
| .meta{ | |
| margin-top: 10px; | |
| display:flex; | |
| align-items:center; | |
| justify-content: space-between; | |
| gap: 10px; | |
| } | |
| .tag{ | |
| font-size: 11px; | |
| padding: 5px 8px; | |
| border-radius: 999px; | |
| border: 1px solid rgba(0,0,0,.08); | |
| background: rgba(255,255,255,.55); | |
| color: var(--text); | |
| display:inline-flex; | |
| align-items:center; | |
| gap:6px; | |
| } | |
| .tag .dot{ width: 7px; height: 7px; } | |
| .tag.open-weights{ border-color: rgba(52,199,89,.25); background: rgba(52,199,89,.09); } | |
| .tag.open-weights .dot{ background: var(--good); } | |
| .tag.api{ border-color: rgba(255,59,48,.22); background: rgba(255,59,48,.08); } | |
| .tag.api .dot{ background: var(--bad); } | |
| .small{ | |
| font-size: 11px; | |
| color: var(--muted); | |
| white-space: nowrap; | |
| } | |
| /* Modal */ | |
| .overlay{ | |
| position: fixed; | |
| inset: 0; | |
| background: rgba(0,0,0,.45); | |
| display:none; | |
| align-items:center; | |
| justify-content:center; | |
| padding: 18px; | |
| z-index: 80; | |
| } | |
| .overlay.open{ display:flex; } | |
| .modal{ | |
| width: min(720px, 100%); | |
| border-radius: 26px; | |
| background: rgba(255,255,255,.92); | |
| border: 1px solid rgba(255,255,255,.65); | |
| box-shadow: 0 30px 80px rgba(0,0,0,.35); | |
| overflow: hidden; | |
| backdrop-filter: blur(18px); | |
| -webkit-backdrop-filter: blur(18px); | |
| } | |
| .modal-head{ | |
| padding: 14px 16px; | |
| display:flex; | |
| justify-content: space-between; | |
| align-items:center; | |
| border-bottom: 1px solid rgba(0,0,0,.06); | |
| } | |
| .modal-head h3{ margin:0; font-size: 15px; font-weight: 760; } | |
| .xbtn{ | |
| border: 1px solid rgba(0,0,0,.08); | |
| background: rgba(255,255,255,.7); | |
| border-radius: 999px; | |
| padding: 8px 10px; | |
| cursor: pointer; | |
| font-size: 12px; | |
| } | |
| .modal-body{ padding: 16px; } | |
| .modal-body p{ margin: 8px 0 0; color: var(--muted); line-height: 1.45; } | |
| .modal-grid{ | |
| display:grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 10px; | |
| margin-top: 14px; | |
| } | |
| .mini{ | |
| border: 1px solid rgba(0,0,0,.06); | |
| background: rgba(245,245,247,.85); | |
| border-radius: 16px; | |
| padding: 10px 12px; | |
| } | |
| .mini .k{ font-size: 11px; color: var(--muted); } | |
| .mini .v{ font-size: 12.5px; font-weight: 650; margin-top: 3px; } | |
| footer{ | |
| max-width: 1200px; | |
| margin: 0 auto 30px; | |
| padding: 0 16px; | |
| color: var(--muted); | |
| font-size: 12px; | |
| display:flex; | |
| justify-content: space-between; | |
| gap: 14px; | |
| flex-wrap: wrap; | |
| } | |
| @media (max-width: 860px){ | |
| .topbar-inner{ flex-wrap: wrap; } | |
| .brand{ min-width: unset; width: 100%; } | |
| .pillrow{ width: 100%; justify-content: flex-start; } | |
| .summary{ grid-template-columns: 1fr; } | |
| .month{ min-width: 290px; max-width: 290px; } | |
| .modal-grid{ grid-template-columns: 1fr; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="topbar"> | |
| <div class="topbar-inner"> | |
| <div class="brand"> | |
| <div class="logo" aria-hidden="true"></div> | |
| <div> | |
| <h1>AI Release Timeline</h1> | |
| <p id="subtitle">Open weights + API releases • 2022 → present</p> | |
| </div> | |
| </div> | |
| <div class="search" role="search"> | |
| <span style="color:var(--muted);font-size:13px;">⌘K</span> | |
| <input id="searchInput" placeholder="Search: Llama, Mistral, diffusion, Claude…" autocomplete="off" /> | |
| </div> | |
| <div class="pillrow"> | |
| <span class="pill active" data-filter="all">All</span> | |
| <span class="pill" data-filter="open-weights">Open weights</span> | |
| <span class="pill" data-filter="api">API only</span> | |
| <select id="monthJump" class="select" title="Jump to month"></select> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="summary"> | |
| <div class="card"> | |
| <div class="card-inner kpis"> | |
| <div class="kpi"> | |
| <span class="dot" style="background:var(--good)"></span> | |
| <div> | |
| <p class="label">Open weights</p> | |
| <p class="value" id="kpiOpen">—</p> | |
| </div> | |
| </div> | |
| <div class="kpi"> | |
| <span class="dot" style="background:var(--bad)"></span> | |
| <div> | |
| <p class="label">API only</p> | |
| <p class="value" id="kpiApi">—</p> | |
| </div> | |
| </div> | |
| <div class="kpi"> | |
| <span class="dot" style="background:var(--accent)"></span> | |
| <div> | |
| <p class="label">Total</p> | |
| <p class="value" id="kpiTotal">—</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <div class="card-inner"> | |
| <div class="hint"> | |
| <div><b>Tips</b></div> | |
| <div>• Press <b>⌘K</b> (or Ctrl+K) to focus search.</div> | |
| <div>• Use ← / → to move month-by-month.</div> | |
| <div>• Click any card to open details.</div> | |
| <div style="margin-top:8px;"> | |
| Data refresh: <span id="asOf">—</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="wrap"> | |
| <div class="scroll-controls"> | |
| <div id="timeline" class="timeline" aria-label="Timeline (horizontal scroll)"> | |
| <!-- populated by JS --> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Fixed overlay buttons for easy access --> | |
| <button id="scrollLeftFixed" class="scroll-btn left fixed" aria-label="Scroll left">◀</button> | |
| <button id="scrollRightFixed" class="scroll-btn right fixed" aria-label="Scroll right">▶</button> | |
| <div class="scroll-band left" aria-hidden="true"></div> | |
| <div class="scroll-band right" aria-hidden="true"></div> | |
| <div id="overlay" class="overlay" role="dialog" aria-modal="true" aria-label="Event details"> | |
| <div class="modal"> | |
| <div class="modal-head"> | |
| <h3 id="mTitle">—</h3> | |
| <button class="xbtn" id="closeBtn">Close</button> | |
| </div> | |
| <div class="modal-body"> | |
| <div id="mTag"></div> | |
| <p id="mDesc">—</p> | |
| <div style="margin-top:10px;"> | |
| <a id="mLink" href="#" target="_blank" rel="noreferrer">Open link →</a> | |
| </div> | |
| <div class="modal-grid"> | |
| <div class="mini"> | |
| <div class="k">Month</div> | |
| <div class="v" id="mMonth">—</div> | |
| </div> | |
| <div class="mini"> | |
| <div class="k">Date</div> | |
| <div class="v" id="mDate">—</div> | |
| </div> | |
| <div class="mini"> | |
| <div class="k">Source</div> | |
| <div class="v" id="mSource">—</div> | |
| </div> | |
| <div class="mini"> | |
| <div class="k">Org / Author</div> | |
| <div class="v" id="mOrg">—</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <footer> | |
| <div> © Made for fast scanning & navigation by Rohit Poreddy.</div> | |
| <div><span id="metaNote">—</span></div> | |
| </footer> | |
| <script> | |
| const el = (id) => document.getElementById(id); | |
| const timeline = el("timeline"); | |
| let STATE = { | |
| events: [], | |
| filter: "all", | |
| q: "", | |
| months: [], | |
| monthIndexByName: new Map(), | |
| }; | |
| function groupByMonth(events){ | |
| const groups = new Map(); | |
| for (const e of events){ | |
| const month = e.month || "Unknown"; | |
| if (!groups.has(month)) groups.set(month, []); | |
| groups.get(month).push(e); | |
| } | |
| // Sort months chronologically by first event date we have (date is YYYY-MM-DD) | |
| const monthList = Array.from(groups.entries()).map(([month, evs]) => { | |
| evs.sort((a,b)=> (b.date||"").localeCompare(a.date||"")); | |
| const key = evs.length ? (evs[0].date || "") : ""; | |
| return { month, key, events: evs }; | |
| }); | |
| monthList.sort((a,b)=> (a.key||"").localeCompare(b.key||"")); // oldest -> newest | |
| return monthList; | |
| } | |
| function tagChip(category){ | |
| const cls = category === "open-weights" ? "open-weights" : "api"; | |
| const label = category === "open-weights" ? "Open weights" : "API only"; | |
| return `<span class="tag ${cls}"><span class="dot"></span>${label}</span>`; | |
| } | |
| function render(){ | |
| timeline.innerHTML = ""; | |
| const months = groupByMonth(STATE.events); | |
| STATE.months = months; | |
| STATE.monthIndexByName = new Map(months.map((m,i)=>[m.month,i])); | |
| // Month jump options | |
| const jump = el("monthJump"); | |
| jump.innerHTML = ""; | |
| for (let i=0;i<months.length;i++){ | |
| const opt = document.createElement("option"); | |
| opt.value = months[i].month; | |
| opt.textContent = months[i].month; | |
| jump.appendChild(opt); | |
| } | |
| // default to latest month | |
| if (months.length){ | |
| jump.value = months[months.length-1].month; | |
| } | |
| // Render month columns | |
| months.forEach((m, idx) => { | |
| const col = document.createElement("section"); | |
| col.className = "month"; | |
| col.dataset.month = m.month; | |
| const head = document.createElement("div"); | |
| head.className = "month-header"; | |
| head.innerHTML = `<h2>${m.month}</h2><div class="count">${m.events.length} items</div>`; | |
| col.appendChild(head); | |
| const list = document.createElement("div"); | |
| list.className = "events"; | |
| for (const e of m.events){ | |
| const card = document.createElement("article"); | |
| card.className = "event"; | |
| card.tabIndex = 0; | |
| card.innerHTML = ` | |
| <div style="display:flex;justify-content:space-between;gap:10px;align-items:flex-start;"> | |
| <div> | |
| <p class="event-title">${escapeHtml(e.title || "Untitled")}</p> | |
| <p class="event-desc">${escapeHtml(e.description || "")}</p> | |
| </div> | |
| </div> | |
| <div class="meta"> | |
| ${tagChip(e.category)} | |
| <span class="small">${escapeHtml(e.date || "")}</span> | |
| </div> | |
| `; | |
| card.addEventListener("click", () => openModal(e)); | |
| card.addEventListener("keydown", (ev) => { if (ev.key === "Enter") openModal(e); }); | |
| list.appendChild(card); | |
| } | |
| col.appendChild(list); | |
| timeline.appendChild(col); | |
| }); | |
| // KPIs | |
| const openCount = STATE.events.filter(e => e.category === "open-weights").length; | |
| const apiCount = STATE.events.filter(e => e.category === "api").length; | |
| el("kpiOpen").textContent = openCount; | |
| el("kpiApi").textContent = apiCount; | |
| el("kpiTotal").textContent = STATE.events.length; | |
| // Meta footer | |
| el("metaNote").textContent = `${STATE.events.length} events loaded • filter: ${STATE.filter}${STATE.q ? " • search: "+STATE.q : ""}`; | |
| } | |
| function escapeHtml(s){ | |
| return String(s).replace(/[&<>"']/g, (m)=>({ "&":"&","<":"<",">":">",'"':""","'":"'" }[m])); | |
| } | |
| async function load(){ | |
| const params = new URLSearchParams(); | |
| if (STATE.filter !== "all") params.set("category", STATE.filter); | |
| if (STATE.q) params.set("q", STATE.q); | |
| const res = await fetch(`/api/events?${params.toString()}`, { cache: "no-store" }); | |
| const data = await res.json(); | |
| el("asOf").textContent = new Date(data.as_of).toLocaleString(); | |
| el("subtitle").textContent = `Open weights + API releases • 2022 → present • ${data.count} showing`; | |
| STATE.events = data.events || []; | |
| // update month jump once we render | |
| render(); | |
| // snap to latest month | |
| requestAnimationFrame(() => scrollToMonth(el("monthJump").value, "instant")); | |
| } | |
| function scrollToMonth(month, behavior="smooth"){ | |
| const col = timeline.querySelector(`.month[data-month="${cssEscape(month)}"]`); | |
| if (!col) return; | |
| // center the month column in the timeline viewport | |
| const containerWidth = timeline.clientWidth; | |
| const colWidth = col.offsetWidth; | |
| const maxScroll = Math.max(0, timeline.scrollWidth - containerWidth); | |
| // Compute desired scrollLeft so the column is centered | |
| const target = col.offsetLeft - Math.round((containerWidth - colWidth) / 2); | |
| const bounded = Math.min(maxScroll, Math.max(0, target)); | |
| if (behavior === 'instant'){ | |
| timeline.scrollLeft = bounded; | |
| return; | |
| } | |
| // perform smooth scroll to center the column | |
| timeline.scrollTo({ left: bounded, behavior }); | |
| } | |
| function cssEscape(s){ | |
| // minimal escape for attribute selector | |
| return String(s).replace(/"/g, '\\"'); | |
| } | |
| // Filters | |
| document.querySelectorAll(".pill").forEach(p => { | |
| p.addEventListener("click", () => { | |
| document.querySelectorAll(".pill").forEach(x=>x.classList.remove("active")); | |
| p.classList.add("active"); | |
| STATE.filter = p.dataset.filter; | |
| load(); | |
| }); | |
| }); | |
| // Month jump | |
| el("monthJump").addEventListener("change", (e) => scrollToMonth(e.target.value)); | |
| // Search (debounced) | |
| let t = null; | |
| el("searchInput").addEventListener("input", (e) => { | |
| clearTimeout(t); | |
| t = setTimeout(() => { STATE.q = e.target.value.trim(); load(); }, 220); | |
| }); | |
| // Keyboard shortcuts | |
| window.addEventListener("keydown", (e) => { | |
| const isMac = navigator.platform.toLowerCase().includes("mac"); | |
| const cmdk = (isMac && e.metaKey && e.key.toLowerCase()==="k") || (!isMac && e.ctrlKey && e.key.toLowerCase()==="k"); | |
| if (cmdk){ | |
| e.preventDefault(); | |
| el("searchInput").focus(); | |
| el("searchInput").select(); | |
| return; | |
| } | |
| if (e.key === "Escape") closeModal(); | |
| if (e.key === "ArrowRight" || e.key === "ArrowLeft"){ | |
| const months = STATE.months || []; | |
| if (!months.length) return; | |
| // Find closest visible month column (based on scrollLeft) | |
| const cols = Array.from(timeline.querySelectorAll(".month")); | |
| const left = timeline.scrollLeft; | |
| let bestIdx = 0; | |
| let bestDist = Infinity; | |
| cols.forEach((c, idx) => { | |
| const dist = Math.abs(c.offsetLeft - left); | |
| if (dist < bestDist){ bestDist = dist; bestIdx = idx; } | |
| }); | |
| const nextIdx = e.key === "ArrowRight" ? Math.min(cols.length-1, bestIdx+1) : Math.max(0, bestIdx-1); | |
| cols[nextIdx].scrollIntoView({ behavior: "smooth", inline: "start", block: "nearest" }); | |
| } | |
| }); | |
| // Modal | |
| const overlay = el("overlay"); | |
| const closeBtn = el("closeBtn"); | |
| closeBtn.addEventListener("click", closeModal); | |
| overlay.addEventListener("click", (e) => { if (e.target === overlay) closeModal(); }); | |
| function openModal(e){ | |
| el("mTitle").textContent = e.title || "Untitled"; | |
| el("mDesc").textContent = e.description || ""; | |
| el("mMonth").textContent = e.month || "—"; | |
| el("mDate").textContent = e.date || "—"; | |
| el("mSource").textContent = e.source || "—"; | |
| el("mOrg").textContent = e.org || "—"; | |
| el("mTag").innerHTML = tagChip(e.category); | |
| const link = el("mLink"); | |
| if (e.link){ | |
| link.style.display = "inline"; | |
| link.href = e.link; | |
| } else { | |
| link.style.display = "none"; | |
| } | |
| overlay.classList.add("open"); | |
| } | |
| function closeModal(){ | |
| overlay.classList.remove("open"); | |
| } | |
| // Replace cursor auto-scroll with left/right arrow buttons (4-month jump) | |
| (function(){ | |
| const leftBtn = el('scrollLeft'); | |
| const rightBtn = el('scrollRight'); | |
| const leftFixed = el('scrollLeftFixed'); | |
| const rightFixed = el('scrollRightFixed'); | |
| function scrollMonths(delta){ | |
| const cols = Array.from(timeline.querySelectorAll('.month')); | |
| if (!cols.length) return; | |
| const left = timeline.scrollLeft; | |
| let bestIdx = 0; | |
| let bestDist = Infinity; | |
| cols.forEach((c, idx) => { | |
| const dist = Math.abs(c.offsetLeft - left); | |
| if (dist < bestDist){ bestDist = dist; bestIdx = idx; } | |
| }); | |
| const targetIdx = Math.min(cols.length - 1, Math.max(0, bestIdx + delta)); | |
| cols[targetIdx].scrollIntoView({ behavior: 'smooth', inline: 'start', block: 'nearest' }); | |
| } | |
| [leftBtn, rightBtn, leftFixed, rightFixed].forEach(btn => { | |
| if (!btn) return; | |
| btn.addEventListener('click', (e) => { | |
| if (btn === leftBtn || btn === leftFixed) scrollMonths(-4); | |
| else scrollMonths(4); | |
| }); | |
| }); | |
| // Horizontal scrolling with mouse wheel when over timeline | |
| // Allow normal vertical page scroll unless horizontal intent is clear or Shift is held. | |
| timeline.addEventListener('wheel', (ev) => { | |
| const absX = Math.abs(ev.deltaX); | |
| const absY = Math.abs(ev.deltaY); | |
| const wantHorizontal = ev.shiftKey || absX > absY; | |
| if (wantHorizontal){ | |
| // use deltaX when available, otherwise fall back to deltaY | |
| const delta = ev.deltaX || ev.deltaY; | |
| timeline.scrollLeft += delta; | |
| ev.preventDefault(); | |
| } | |
| // otherwise, let the wheel event scroll the page vertically | |
| }, { passive: false }); | |
| // Click-and-drag to pan timeline | |
| (function(){ | |
| let isDown = false; | |
| let startX = 0; | |
| let startScroll = 0; | |
| timeline.addEventListener('mousedown', (e) => { | |
| isDown = true; startX = e.clientX; startScroll = timeline.scrollLeft; | |
| timeline.classList.add('dragging'); | |
| e.preventDefault(); | |
| }); | |
| window.addEventListener('mousemove', (e) => { | |
| if (!isDown) return; | |
| const dx = e.clientX - startX; | |
| timeline.scrollLeft = startScroll - dx; | |
| }); | |
| window.addEventListener('mouseup', () => { isDown = false; timeline.classList.remove('dragging'); }); | |
| })(); | |
| })(); | |
| // Initial load | |
| load(); | |
| </script> | |
| </body> | |
| </html> | |