Gap-to-Draft Pipeline (ietf pipeline): - Context builder assembles ideas, RFC foundations, similar drafts, ecosystem vision - Generator produces outlines + sections using rich context with Claude - Quality gates: novelty (embedding similarity), references, format, self-rating - Family coordinator generates 5-draft ecosystem (AEM/ATD/HITL/AEPB/APAE) - I-D formatter with proper headers, references, 72-char wrapping Living Standards Observatory (ietf observatory): - Source abstraction with IETF + W3C fetchers - 7-step update pipeline: snapshot, fetch, analyze, embed, ideas, gaps, record - Static GitHub Pages dashboard (explorer, gap tracker, timeline) - Weekly CI/CD automation via GitHub Actions Also includes: - 361 drafts (expanded from 260 with 6 new keywords), 403 authors, 1,262 ideas, 12 gaps - Blog series (8 posts planned), reports, arXiv paper figures - Agent team infrastructure (CLAUDE.md, scripts, dev journal) - 5 new DB tables, schema migration, ~15 new query methods Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
239 lines
9.4 KiB
HTML
239 lines
9.4 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Explorer - Living Standards Observatory</title>
|
|
<link rel="stylesheet" href="../assets/style.css">
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<div class="container">
|
|
<h1>Living Standards Observatory</h1>
|
|
<nav>
|
|
<a href="../index.html">Dashboard</a>
|
|
<a href="explorer.html" class="active">Explorer</a>
|
|
<a href="gaps.html">Gaps</a>
|
|
<a href="timeline.html">Timeline</a>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
<div class="container">
|
|
|
|
<div class="controls">
|
|
<div class="controls-row">
|
|
<input type="text" class="search-box" id="searchBox" placeholder="Search by name, title, summary, or keyword...">
|
|
<select id="sourceFilter" style="padding:8px;border:1px solid var(--border);border-radius:6px;font-size:0.85rem">
|
|
<option value="">All sources</option>
|
|
</select>
|
|
<div class="slider-group">Min score: <input type="range" id="minScore" min="1" max="5" step="0.1" value="1"><span class="slider-val" id="minScoreVal">1.0</span></div>
|
|
<div class="slider-group">Min novelty: <input type="range" id="minNovelty" min="1" max="5" step="1" value="1"><span class="slider-val" id="minNoveltyVal">1</span></div>
|
|
<div class="slider-group">Max overlap: <input type="range" id="maxOverlap" min="1" max="5" step="1" value="5"><span class="slider-val" id="maxOverlapVal">5</span></div>
|
|
<button class="reset-btn" onclick="resetFilters()">Reset</button>
|
|
</div>
|
|
<div class="controls-row">
|
|
<div class="chip-row" id="catChips"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="result-count" id="resultCount"></div>
|
|
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th onclick="sortBy('score')" width="60">Score <span class="sort-arrow" id="sort-score"></span></th>
|
|
<th onclick="sortBy('name')">Draft <span class="sort-arrow" id="sort-name"></span></th>
|
|
<th onclick="sortBy('source')" width="60">Src <span class="sort-arrow" id="sort-source"></span></th>
|
|
<th onclick="sortBy('date')" width="90">Date <span class="sort-arrow" id="sort-date"></span></th>
|
|
<th onclick="sortBy('novelty')" width="30">N</th>
|
|
<th onclick="sortBy('maturity')" width="30">M</th>
|
|
<th onclick="sortBy('overlap')" width="30">O</th>
|
|
<th onclick="sortBy('momentum')" width="30">Mom</th>
|
|
<th onclick="sortBy('relevance')" width="30">R</th>
|
|
<th>Categories</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="tableBody"></tbody>
|
|
</table>
|
|
|
|
</div>
|
|
|
|
<script>
|
|
let DRAFTS = [];
|
|
let ALL_CATS = [];
|
|
let activeCats = new Set();
|
|
let sortField = 'score';
|
|
let sortAsc = false;
|
|
let expandedRow = null;
|
|
|
|
function escHtml(s) { const d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML; }
|
|
function scoreBadge(s) {
|
|
const cls = s >= 4.0 ? 'score-high' : s >= 3.0 ? 'score-mid' : 'score-low';
|
|
return '<span class="score-badge ' + cls + '">' + s.toFixed(1) + '</span>';
|
|
}
|
|
function dimBar(v) { return '<span class="bar" style="width:' + (v * 12) + 'px"></span> ' + v; }
|
|
|
|
const searchBox = document.getElementById('searchBox');
|
|
const sourceFilter = document.getElementById('sourceFilter');
|
|
const minScore = document.getElementById('minScore');
|
|
const minNovelty = document.getElementById('minNovelty');
|
|
const maxOverlap = document.getElementById('maxOverlap');
|
|
|
|
searchBox.oninput = render;
|
|
sourceFilter.onchange = render;
|
|
minScore.oninput = () => { document.getElementById('minScoreVal').textContent = parseFloat(minScore.value).toFixed(1); render(); };
|
|
minNovelty.oninput = () => { document.getElementById('minNoveltyVal').textContent = minNovelty.value; render(); };
|
|
maxOverlap.oninput = () => { document.getElementById('maxOverlapVal').textContent = maxOverlap.value; render(); };
|
|
|
|
function resetFilters() {
|
|
searchBox.value = '';
|
|
sourceFilter.value = '';
|
|
minScore.value = 1; document.getElementById('minScoreVal').textContent = '1.0';
|
|
minNovelty.value = 1; document.getElementById('minNoveltyVal').textContent = '1';
|
|
maxOverlap.value = 5; document.getElementById('maxOverlapVal').textContent = '5';
|
|
activeCats.clear();
|
|
document.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
|
|
sortField = 'score'; sortAsc = false;
|
|
render();
|
|
}
|
|
|
|
function sortBy(field) {
|
|
if (sortField === field) sortAsc = !sortAsc;
|
|
else { sortField = field; sortAsc = field === 'name' || field === 'date'; }
|
|
render();
|
|
}
|
|
|
|
function cmp(a, b) {
|
|
let va = a[sortField], vb = b[sortField];
|
|
if (typeof va === 'string') return sortAsc ? va.localeCompare(vb) : vb.localeCompare(va);
|
|
return sortAsc ? va - vb : vb - va;
|
|
}
|
|
|
|
function render() {
|
|
const q = searchBox.value.toLowerCase().trim();
|
|
const src = sourceFilter.value;
|
|
const ms = parseFloat(minScore.value);
|
|
const mn = parseInt(minNovelty.value);
|
|
const mo = parseInt(maxOverlap.value);
|
|
|
|
let filtered = DRAFTS.filter(d => {
|
|
if (d.score < ms) return false;
|
|
if (d.novelty < mn) return false;
|
|
if (d.overlap > mo) return false;
|
|
if (src && (d.source || 'ietf') !== src) return false;
|
|
if (activeCats.size > 0 && !d.categories.some(c => activeCats.has(c))) return false;
|
|
if (q) {
|
|
const hay = (d.name + ' ' + d.title + ' ' + d.summary + ' ' + d.categories.join(' ')).toLowerCase();
|
|
const words = q.split(/\s+/);
|
|
if (!words.every(w => hay.includes(w))) return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
filtered.sort(cmp);
|
|
|
|
document.querySelectorAll('.sort-arrow').forEach(el => el.textContent = '');
|
|
const arrow = document.getElementById('sort-' + sortField);
|
|
if (arrow) arrow.textContent = sortAsc ? '\u25B2' : '\u25BC';
|
|
|
|
const tbody = document.getElementById('tableBody');
|
|
tbody.innerHTML = '';
|
|
expandedRow = null;
|
|
|
|
filtered.forEach(d => {
|
|
const tr = document.createElement('tr');
|
|
tr.className = 'clickable';
|
|
const srcClass = 'source-' + (d.source || 'ietf');
|
|
tr.innerHTML =
|
|
'<td>' + scoreBadge(d.score) + '</td>' +
|
|
'<td style="max-width:300px"><a href="' + escHtml(d.url) + '" target="_blank" onclick="event.stopPropagation()" style="color:var(--accent);font-weight:500">' + escHtml(d.name) + '</a>' +
|
|
'<br><span class="dim">' + escHtml(d.title.substring(0, 80)) + '</span></td>' +
|
|
'<td><span class="source-badge ' + srcClass + '">' + (d.source || 'ietf').toUpperCase() + '</span></td>' +
|
|
'<td class="dim">' + d.date + '</td>' +
|
|
'<td>' + dimBar(d.novelty) + '</td>' +
|
|
'<td>' + dimBar(d.maturity) + '</td>' +
|
|
'<td>' + dimBar(d.overlap) + '</td>' +
|
|
'<td>' + dimBar(d.momentum) + '</td>' +
|
|
'<td>' + dimBar(d.relevance) + '</td>' +
|
|
'<td>' + d.categories.map(c => '<span class="cat-badge">' + escHtml(c) + '</span>').join('') + '</td>';
|
|
|
|
tr.onclick = () => toggleDetail(tr, d);
|
|
tbody.appendChild(tr);
|
|
});
|
|
|
|
document.getElementById('resultCount').textContent =
|
|
'Showing ' + filtered.length + ' of ' + DRAFTS.length + ' drafts';
|
|
}
|
|
|
|
function toggleDetail(tr, d) {
|
|
if (expandedRow) {
|
|
expandedRow.previousElementSibling?.classList.remove('expanded');
|
|
expandedRow.remove();
|
|
if (expandedRow._draftName === d.name) { expandedRow = null; return; }
|
|
}
|
|
tr.classList.add('expanded');
|
|
const detail = document.createElement('tr');
|
|
detail.className = 'detail-row';
|
|
detail._draftName = d.name;
|
|
function detailItem(label, score, note) {
|
|
return '<div class="detail-item"><strong>' + label + ':</strong> ' + score + '/5 ' +
|
|
'<span class="bar" style="width:' + (score * 16) + 'px"></span>' +
|
|
(note ? '<div class="note">' + escHtml(note) + '</div>' : '') + '</div>';
|
|
}
|
|
detail.innerHTML = '<td colspan="10">' +
|
|
'<div class="summary-text"><strong>Summary:</strong> ' + escHtml(d.summary) + '</div>' +
|
|
'<div class="detail-grid" style="margin-top:10px">' +
|
|
detailItem('Novelty', d.novelty, d.novelty_note) +
|
|
detailItem('Maturity', d.maturity, d.maturity_note) +
|
|
detailItem('Overlap', d.overlap, d.overlap_note) +
|
|
detailItem('Momentum', d.momentum, d.momentum_note) +
|
|
detailItem('Relevance', d.relevance, d.relevance_note) +
|
|
'<div class="detail-item"><strong>Source:</strong> ' + (d.source || 'ietf').toUpperCase() + ' · <strong>Pages:</strong> ' + d.pages + '</div>' +
|
|
'</div>' +
|
|
'<div style="margin-top:8px"><a href="' + escHtml(d.url) + '" target="_blank" style="color:var(--accent)">Open document \u2192</a></div>' +
|
|
'</td>';
|
|
tr.after(detail);
|
|
expandedRow = detail;
|
|
}
|
|
|
|
async function init() {
|
|
DRAFTS = await fetch('../data/drafts.json').then(r => r.json());
|
|
|
|
// Build categories
|
|
const catSet = new Set();
|
|
const sources = new Set();
|
|
DRAFTS.forEach(d => {
|
|
d.categories.forEach(c => catSet.add(c));
|
|
sources.add(d.source || 'ietf');
|
|
});
|
|
ALL_CATS = [...catSet].sort();
|
|
|
|
// Source filter options
|
|
sources.forEach(s => {
|
|
const opt = document.createElement('option');
|
|
opt.value = s;
|
|
opt.textContent = s.toUpperCase();
|
|
sourceFilter.appendChild(opt);
|
|
});
|
|
|
|
// Category chips
|
|
const chipBox = document.getElementById('catChips');
|
|
ALL_CATS.forEach(cat => {
|
|
const el = document.createElement('span');
|
|
el.className = 'chip';
|
|
const count = DRAFTS.filter(d => d.categories.includes(cat)).length;
|
|
el.innerHTML = escHtml(cat) + '<span style="font-size:0.65rem;opacity:0.7;margin-left:2px">(' + count + ')</span>';
|
|
el.onclick = () => {
|
|
if (activeCats.has(cat)) { activeCats.delete(cat); el.classList.remove('active'); }
|
|
else { activeCats.add(cat); el.classList.add('active'); }
|
|
render();
|
|
};
|
|
chipBox.appendChild(el);
|
|
});
|
|
|
|
render();
|
|
}
|
|
init();
|
|
</script>
|
|
</body>
|
|
</html> |