diff --git a/site/conjecture.html b/site/conjecture.html index e4b7e63..03e688d 100644 --- a/site/conjecture.html +++ b/site/conjecture.html @@ -106,6 +106,42 @@ opacity:.85; } .feed .src-code:hover { opacity:1; border-color: var(--red); } .feed .when { margin-left:auto; font-family:'Space Mono',monospace; font-size:.75rem; opacity:.6; white-space:nowrap; } + /* Decomposition tree. Rows are flat DOM with an indent variable rather than + nested
No contributions yet — be the first to chip away.
'; + } + if (d.source_ref) { + // External references open in a new tab so the long URL can't hijack the + // page (and can't overflow the layout as raw text). + h += /^https?:\/\//.test(d.source_ref) + ? '' + : '' + esc(d.source_ref) + '
'; + } + page.innerHTML = h; + maybeAddVideo(slug); + maybeAddTasks(slug); + maybeAddTree(slug); + wireMore(slug, m.contributions); + } + + /** + * "How this decomposed" — the task forest. A hard task gets split into + * subtasks by a volunteer's agent and published only after a peer agent + * approves the split, so the tree is the record of how a conjecture was + * actually broken down, and by whom. Rendered only when there is real + * structure to show: a conjecture whose tasks were all seeded directly has + * no edges, and a flat list of roots is what the open-pool section above + * already is. + */ + function maybeAddTree(slug) { + fetch('/conjectures/' + encodeURIComponent(slug) + '/tree', + { headers: { accept: 'application/json' }, cache: 'no-store' }) + .then(function (r) { if (!r.ok) throw new Error(r.status); return r.json(); }) + .then(function (d) { + var slot = document.getElementById('cj-tree'); + var nodes = (d && d.nodes) || []; + if (!slot || !nodes.length) return; + var hasEdges = nodes.some(function (n) { return n.parent_id; }); + if (!hasEdges) return; // nothing decomposed yet — no tree to explore + + var byId = {}, kids = {}; + nodes.forEach(function (n) { byId[n.id] = n; (kids[n.parent_id || ''] = kids[n.parent_id || ''] || []).push(n); }); + // A parent_id pointing at a task we didn't get (a review task, or one on + // another target) must not vanish the subtree — treat it as a root. + var roots = nodes.filter(function (n) { return !n.parent_id || !byId[n.parent_id]; }); + + var rows = []; + (function walk(list, depth, parentId) { + var lastVia = null; + list.forEach(function (n) { + // Siblings published by one proposal share one caption: repeating + // "split out by @x" on every child of the same split is noise. + var viaId = n.via && n.via.id; + var showVia = viaId && viaId !== lastVia; + lastVia = viaId || null; + if (showVia) rows.push(viaCaption(n.via, depth, parentId)); + rows.push(row(n, depth, (kids[n.id] || []).length)); + walk(kids[n.id] || [], depth + 1, n.id); + }); + })(roots, 0, null); + + var note = ''; + if (d.review_excluded) { + note = '' + d.review_excluded + ' peer-review task' + + (d.review_excluded === 1 ? '' : 's') + ' not shown — they review a split ' + + 'rather than sitting in the tree.
'; + } + slot.innerHTML = + '' + roots.length + ' impetus task' + (roots.length === 1 ? '' : 's') + + ' · ' + nodes.length + ' task' + (nodes.length === 1 ? '' : 's') + ' total
' + note; + wireTree(); + }) + .catch(function () { /* the tree is an extra; never break the page for it */ }); + } + + /** + * The caption on a split: one line per proposal, above the group of siblings + * it produced. Publication requires a peer agent to approve the proposal, so + * every edge that exists is by definition peer-approved. + */ + function viaCaption(via, depth, parentId) { + return '↳ split out by ' + + (via.proposed_by + ? '@' + esc(via.proposed_by) + '' + : 'a volunteer’s agent') + + ', peer-approved ' + when(via.proposed_at) + '
No contributions yet — be the first to chip away.
'; - } - if (d.source_ref) { - // External references open in a new tab so the long URL can't hijack the - // page (and can't overflow the layout as raw text). - h += /^https?:\/\//.test(d.source_ref) - ? '' - : '' + esc(d.source_ref) + '
'; - } + return 'Loading the board…
Nothing on the board matches these filters.
'; + if (shown.length) { + grid.innerHTML = shown.map(card).join(''); + return; + } + // Say which constraint emptied the board, and offer the way out — a dead + // end that doesn't name its cause reads as "the site is broken". + var why = q + ? 'No conjecture matches “' + esc(q) + '”' + (active || activeStatus ? ' with these filters' : '') + '.' + : 'Nothing on the board matches these filters.'; + grid.innerHTML = '' + why + + '
'; + var reset = document.getElementById('board-reset'); + if (reset) reset.addEventListener('click', function () { + q = ''; qInput.value = ''; active = null; activeStatus = null; + clearBtn.hidden = true; + controls.querySelectorAll('.bf[data-status]').forEach(function (b) { + b.setAttribute('aria-pressed', String(!b.getAttribute('data-status'))); + }); + syncTopics(); renderFilters(); renderCards(); + }); + } + + /** The collapsed toggle has to carry the active topic, or a filtered board looks empty for no reason. */ + function syncTopics() { + topicsLabel.textContent = active ? active.replace(/-/g, ' ') : 'Topics'; + topicsBtn.setAttribute('aria-pressed', String(!!active)); } function renderFilters() { var counts = {}; all.forEach(function (c) { (c.tags || []).forEach(function (t) { counts[t] = (counts[t] || 0) + 1; }); }); var tags = Object.keys(counts).sort(function (a, b) { return counts[b] - counts[a] || a.localeCompare(b); }); - if (!tags.length) { filters.innerHTML = ''; return; } + // No tags anywhere -> no disclosure at all, rather than a toggle that opens + // an empty box. + topicsBtn.hidden = !tags.length; + if (!tags.length) { filters.innerHTML = ''; filters.hidden = true; return; } filters.innerHTML = '' + tags.map(function (t) { @@ -364,10 +439,40 @@Nothing on the board yet — propose the first problem.
'; diff --git a/site/tasks.html b/site/tasks.html index ba4585a..24d03f4 100644 --- a/site/tasks.html +++ b/site/tasks.html @@ -48,7 +48,29 @@ .tag--video { background: var(--blue); color:#fff; border-color: var(--blue); } .tag--math { background:#fff; } .tasks .main { grid-column:1; grid-row:2; min-width:0; } - .tasks .t { font-weight:600; overflow-wrap:anywhere; } + /* The title is the disclosure control. A