| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>SGLang Release Lookup</title> |
| <style> |
| :root { |
| --primary: #3b82f6; |
| --primary-hover: #2563eb; |
| --bg: #f8fafc; |
| --card-bg: #ffffff; |
| --text-main: #1e293b; |
| --text-secondary: #64748b; |
| --border: #e2e8f0; |
| --success-bg: #f0fdf4; |
| --success-border: #bbf7d0; |
| --success-text: #166534; |
| --error-bg: #fef2f2; |
| --error-border: #fecaca; |
| --error-text: #991b1b; |
| } |
| |
| body { |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; |
| background-color: var(--bg); |
| color: var(--text-main); |
| display: flex; |
| justify-content: center; |
| align-items: center; |
| min-height: 100vh; |
| margin: 0; |
| padding: 20px; |
| } |
| |
| .container { |
| background-color: var(--card-bg); |
| padding: 2.5rem; |
| border-radius: 16px; |
| box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); |
| width: 100%; |
| max-width: 550px; |
| text-align: center; |
| transition: transform 0.2s; |
| } |
| |
| h1 { |
| margin-top: 0; |
| margin-bottom: 0.5rem; |
| color: var(--text-main); |
| font-size: 1.8rem; |
| font-weight: 700; |
| } |
| |
| p.subtitle { |
| margin-bottom: 2rem; |
| color: var(--text-secondary); |
| font-size: 0.95rem; |
| } |
| |
| .input-group { |
| display: flex; |
| gap: 12px; |
| margin-bottom: 1.5rem; |
| position: relative; |
| } |
| |
| input { |
| flex: 1; |
| padding: 12px 16px; |
| border: 2px solid var(--border); |
| border-radius: 8px; |
| font-size: 1rem; |
| outline: none; |
| transition: all 0.2s ease; |
| color: var(--text-main); |
| } |
| |
| input:focus { |
| border-color: var(--primary); |
| box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); |
| } |
| |
| input::placeholder { |
| color: #94a3b8; |
| } |
| |
| button { |
| padding: 12px 24px; |
| background-color: var(--primary); |
| color: white; |
| border: none; |
| border-radius: 8px; |
| font-size: 1rem; |
| font-weight: 600; |
| cursor: pointer; |
| transition: background-color 0.2s, transform 0.1s; |
| } |
| |
| button:hover { |
| background-color: var(--primary-hover); |
| } |
| |
| button:active { |
| transform: translateY(1px); |
| } |
| |
| button:disabled { |
| background-color: #cbd5e1; |
| cursor: not-allowed; |
| transform: none; |
| } |
| |
| #result { |
| margin-top: 1.5rem; |
| text-align: left; |
| border-radius: 8px; |
| display: none; |
| opacity: 0; |
| transition: opacity 0.3s ease; |
| } |
| |
| #result.visible { |
| opacity: 1; |
| } |
| |
| .result-content { |
| padding: 1.25rem; |
| border-radius: 8px; |
| } |
| |
| .result-success { |
| background-color: var(--success-bg); |
| border: 1px solid var(--success-border); |
| color: var(--success-text); |
| } |
| |
| .result-error { |
| background-color: var(--error-bg); |
| border: 1px solid var(--error-border); |
| color: var(--error-text); |
| } |
| |
| .result-row { |
| display: flex; |
| justify-content: space-between; |
| margin-bottom: 0.5rem; |
| align-items: baseline; |
| } |
| |
| .result-row:last-child { |
| margin-bottom: 0; |
| } |
| |
| .result-label { |
| font-weight: 600; |
| margin-right: 1rem; |
| min-width: 80px; |
| } |
| |
| .tag-link { |
| color: var(--primary); |
| text-decoration: none; |
| font-weight: bold; |
| font-size: 1.1rem; |
| } |
| |
| .tag-link:hover { |
| text-decoration: underline; |
| } |
| |
| .loader { |
| display: inline-block; |
| width: 18px; |
| height: 18px; |
| border: 3px solid rgba(59, 130, 246, 0.2); |
| border-radius: 50%; |
| border-top-color: var(--primary); |
| animation: spin 1s linear infinite; |
| margin-right: 8px; |
| vertical-align: text-bottom; |
| } |
| |
| @keyframes spin { |
| to { transform: rotate(360deg); } |
| } |
| |
| .status-msg { |
| margin-top: 1rem; |
| font-size: 0.85rem; |
| color: var(--text-secondary); |
| min-height: 20px; |
| } |
| |
| .badge { |
| display: inline-block; |
| padding: 2px 8px; |
| border-radius: 12px; |
| font-size: 0.75rem; |
| font-weight: 600; |
| text-transform: uppercase; |
| } |
| |
| .badge-main { |
| background-color: #dbeafe; |
| color: #1e40af; |
| } |
| |
| .badge-gateway { |
| background-color: #f3e8ff; |
| color: #6b21a8; |
| } |
| </style> |
| </head> |
| <body> |
|
|
| <div class="container"> |
| <h1>Release Lookup</h1> |
| <p class="subtitle">Find which SGLang release first included your PR or commit.</p> |
|
|
| <div class="input-group"> |
| <input type="text" id="queryInput" placeholder="PR # (e.g. 1425), URL, or Commit Hash" autocomplete="off" /> |
| <button id="searchBtn" disabled>Search</button> |
| </div> |
|
|
| <div id="loading" style="display: none; margin-bottom: 1rem; color: var(--text-secondary);"> |
| <span class="loader"></span> Loading index... |
| </div> |
|
|
| <div id="result"></div> |
|
|
| <div id="indexStatus" class="status-msg">Initializing...</div> |
| </div> |
|
|
| <script> |
| let tagIndex = null; |
| let tagsArray = null; |
| let sortedCommitKeys = null; |
| const INDEX_FILE = 'release_index.json'; |
| const SHORT_HASH_LEN = 8; |
| |
| const input = document.getElementById('queryInput'); |
| const btn = document.getElementById('searchBtn'); |
| const resultDiv = document.getElementById('result'); |
| const loadingDiv = document.getElementById('loading'); |
| const statusDiv = document.getElementById('indexStatus'); |
| |
| |
| function formatDate(isoString) { |
| if (!isoString) return 'Unknown'; |
| try { |
| return new Date(isoString).toLocaleDateString('en-US', { |
| year: 'numeric', month: 'long', day: 'numeric' |
| }); |
| } catch(e) { return isoString; } |
| } |
| |
| |
| function isCompactFormat(data) { |
| return Array.isArray(data.t); |
| } |
| |
| |
| function getTagInfo(tagRef) { |
| if (tagsArray) { |
| |
| const tag = tagsArray[tagRef]; |
| return { |
| name: tag[0], |
| date: tag[1], |
| type: tag[2] === 1 ? 'gateway' : 'main' |
| }; |
| } else { |
| |
| const info = tagIndex.tags[tagRef]; |
| return { name: tagRef, ...info }; |
| } |
| } |
| |
| |
| function parseTagRef(ref) { |
| if (typeof ref === 'string' && /^[mg]\d+$/.test(ref)) { |
| return { |
| type: ref[0], |
| idx: parseInt(ref.slice(1)) |
| }; |
| } |
| return null; |
| } |
| |
| async function loadIndex() { |
| loadingDiv.style.display = 'block'; |
| statusDiv.innerText = 'Downloading index...'; |
| |
| try { |
| const response = await fetch(INDEX_FILE); |
| if (!response.ok) { |
| throw new Error("No index file found. Please run generate_index.py."); |
| } |
| const data = await response.json(); |
| |
| |
| if (isCompactFormat(data)) { |
| tagsArray = data.t; |
| tagIndex = { |
| prs: data.p, |
| commits: data.c |
| }; |
| } else { |
| tagIndex = data; |
| tagsArray = null; |
| } |
| |
| sortedCommitKeys = Object.keys(tagIndex.commits).sort(); |
| |
| const tagCount = tagsArray ? tagsArray.length : Object.keys(tagIndex.tags).length; |
| const prCount = Object.keys(tagIndex.prs).length; |
| |
| statusDiv.innerText = `Ready. Indexed ${tagCount} releases and ${prCount} PRs.`; |
| btn.disabled = false; |
| } catch (e) { |
| statusDiv.textContent = ''; |
| const errorSpan = document.createElement('span'); |
| errorSpan.style.color = 'var(--error-text)'; |
| errorSpan.textContent = `Error: ${e.message}`; |
| statusDiv.appendChild(errorSpan); |
| btn.disabled = true; |
| } finally { |
| loadingDiv.style.display = 'none'; |
| } |
| } |
| |
| |
| function prefixSearchCommit(prefix) { |
| if (!sortedCommitKeys) return null; |
| let lo = 0, hi = sortedCommitKeys.length; |
| while (lo < hi) { |
| const mid = (lo + hi) >>> 1; |
| if (sortedCommitKeys[mid] < prefix) lo = mid + 1; |
| else hi = mid; |
| } |
| if (lo < sortedCommitKeys.length && sortedCommitKeys[lo].startsWith(prefix)) { |
| return sortedCommitKeys[lo]; |
| } |
| return null; |
| } |
| |
| |
| loadIndex(); |
| |
| |
| btn.addEventListener('click', performSearch); |
| input.addEventListener('keypress', (e) => { |
| if (e.key === 'Enter') performSearch(); |
| }); |
| |
| |
| input.focus(); |
| |
| function performSearch() { |
| if (!tagIndex) return; |
| |
| const rawQuery = input.value.trim(); |
| if (!rawQuery) return; |
| |
| |
| resultDiv.style.display = 'none'; |
| resultDiv.classList.remove('visible'); |
| |
| let queryType = 'unknown'; |
| let key = rawQuery; |
| |
| |
| |
| const urlMatch = rawQuery.match(/\/pull\/(\d+)/); |
| if (urlMatch) { |
| key = urlMatch[1]; |
| queryType = 'pr'; |
| } |
| |
| else if (rawQuery.match(/^#?\d+$/)) { |
| key = rawQuery.replace('#', ''); |
| queryType = 'pr'; |
| } |
| |
| else if (rawQuery.match(/^[0-9a-fA-F]{7,40}$/)) { |
| key = rawQuery.toLowerCase(); |
| queryType = 'commit'; |
| } |
| |
| let tagData = null; |
| |
| if (queryType === 'pr') { |
| tagData = tagIndex.prs[key]; |
| } else if (queryType === 'commit') { |
| |
| const shortKey = key.slice(0, SHORT_HASH_LEN); |
| tagData = tagIndex.commits[shortKey]; |
| |
| |
| if (!tagData) { |
| const matchKey = prefixSearchCommit(shortKey); |
| if (matchKey) { |
| tagData = tagIndex.commits[matchKey]; |
| } |
| } |
| } |
| |
| renderResult(tagData, queryType, key); |
| } |
| |
| function renderResult(tagData, queryType, key) { |
| resultDiv.innerHTML = ''; |
| resultDiv.style.display = 'block'; |
| |
| |
| void resultDiv.offsetWidth; |
| resultDiv.classList.add('visible'); |
| |
| |
| let tagRefs = []; |
| |
| if (!tagData) { |
| |
| } else if (typeof tagData === 'string') { |
| |
| const parsed = parseTagRef(tagData); |
| if (parsed) { |
| tagRefs.push(parsed.idx); |
| } else { |
| |
| tagRefs.push(tagData); |
| } |
| } else if (typeof tagData === 'object') { |
| |
| if ('m' in tagData) tagRefs.push(tagData.m); |
| if ('g' in tagData) tagRefs.push(tagData.g); |
| if ('main' in tagData) tagRefs.push(tagData.main); |
| if ('gateway' in tagData) tagRefs.push(tagData.gateway); |
| } |
| |
| if (tagRefs.length === 0) { |
| const label = queryType === 'pr' ? `PR #${key}` : `Commit ${key.substring(0, 7)}`; |
| |
| const container = document.createElement('div'); |
| container.className = 'result-content result-error'; |
| |
| const statusRow = document.createElement('div'); |
| statusRow.className = 'result-row'; |
| const statusLabel = document.createElement('span'); |
| statusLabel.className = 'result-label'; |
| statusLabel.textContent = 'Status'; |
| const statusValue = document.createElement('span'); |
| statusValue.textContent = 'Not Found'; |
| statusRow.appendChild(statusLabel); |
| statusRow.appendChild(statusValue); |
| |
| const msgDiv = document.createElement('div'); |
| msgDiv.style.marginTop = '8px'; |
| const strongEl = document.createElement('strong'); |
| strongEl.textContent = label; |
| msgDiv.append( |
| `The ${queryType} `, |
| strongEl, |
| ' has not been included in any release yet, or is not in the index.' |
| ); |
| |
| container.appendChild(statusRow); |
| container.appendChild(msgDiv); |
| resultDiv.appendChild(container); |
| return; |
| } |
| |
| const repoUrl = "https://github.com/sgl-project/sglang"; |
| resultDiv.innerHTML = ''; |
| |
| for (const tagRef of tagRefs) { |
| const tagInfo = getTagInfo(tagRef); |
| const dateStr = formatDate(tagInfo.date); |
| const tagUrl = `${repoUrl}/releases/tag/${encodeURIComponent(tagInfo.name)}`; |
| const badgeClass = tagInfo.type === 'gateway' ? 'badge-gateway' : 'badge-main'; |
| |
| const container = document.createElement('div'); |
| container.className = 'result-content result-success'; |
| container.style.marginBottom = '0.75rem'; |
| |
| container.innerHTML = ` |
| <div class="result-row"> |
| <span class="result-label">Release</span> |
| <a target="_blank" class="tag-link"></a> |
| </div> |
| <div class="result-row"> |
| <span class="result-label">Date</span> |
| <span class="date-value"></span> |
| </div> |
| <div class="result-row"> |
| <span class="result-label">Module</span> |
| <span class="badge ${badgeClass} module-value"></span> |
| </div> |
| `; |
| |
| |
| const link = container.querySelector('.tag-link'); |
| link.href = tagUrl; |
| link.textContent = tagInfo.name; |
| container.querySelector('.date-value').textContent = dateStr; |
| container.querySelector('.module-value').textContent = tagInfo.type; |
| |
| resultDiv.appendChild(container); |
| } |
| } |
| </script> |
|
|
| </body> |
| </html> |
|
|