diff --git a/web/static/app.js b/web/static/app.js index 5f8bebec..dd154c55 100644 --- a/web/static/app.js +++ b/web/static/app.js @@ -30,9 +30,10 @@ const els = { // auto-split phrase pager (#2b) capSplit: $('capSplit'), splitView: $('splitView'), splitPrev: $('splitPrev'), splitNext: $('splitNext'), - splitPos: $('splitPos'), splitInfo: $('splitInfo'), + splitPos: $('splitPos'), splitInfo: $('splitInfo'), splitTags: $('splitTags'), splitPlay: $('splitPlay'), splitStop: $('splitStop'), - splitDownload: $('splitDownload'), splitDownloadAll: $('splitDownloadAll'), + splitDownload: $('splitDownload'), splitDownloadEach: $('splitDownloadEach'), + splitDownloadAll: $('splitDownloadAll'), // verbose on-page debug log debugLog: $('debugLog'), dbgCopy: $('dbgCopy'), dbgClear: $('dbgClear'), }; @@ -349,11 +350,24 @@ function renderPhrase() { draw(); els.splitPos.textContent = `phrase ${splitIdx + 1} / ${splitChunks.length}`; els.splitInfo.textContent = - `${ch.id} · bars ${ch.bar_lo}–${ch.bar_hi} · ${(ch.notes || []).length} notes · ${tags.length} tag(s)`; + `${ch.id} · bars ${ch.bar_lo}–${ch.bar_hi} · ${(ch.notes || []).length} notes`; + renderPhraseTags(tags); els.splitPrev.disabled = splitIdx === 0; els.splitNext.disabled = splitIdx === splitChunks.length - 1; } +// Shows the phrase's resolved tags (chosen + notation-derived techniques) as +// ticked, read-only boxes, so detected techniques like slide/hammer_on are +// visible per phrase. Read-only on purpose: these reflect the phrase and do not +// feed the next split (that's the capture form's Tags above). +function renderPhraseTags(tags) { + if (!els.splitTags) return; + els.splitTags.innerHTML = tags.length + ? tags.map((t) => + ``).join('') + : 'no tags detected'; +} + function stepPhrase(d) { if (splitChunks.length === 0) return; splitIdx = Math.min(splitChunks.length - 1, Math.max(0, splitIdx + d)); @@ -367,12 +381,27 @@ function downloadPhrase() { capMsg(`saved ${ch.id}.chunk.json · bars ${ch.bar_lo}–${ch.bar_hi}`, false); } -function downloadAllPhrases() { +// Saves one manifest-ready _p.chunk.json per phrase — what `griff manifest` +// ingests (it collects individual .chunk.json files, one ChunkMeta each). +// Staggered so the browser does not coalesce the rapid downloads. +function downloadEachPhrase() { if (splitChunks.length === 0) { capMsg('split into phrases first', true); return; } - // Stagger the saves so the browser does not coalesce the rapid clicks. splitChunks.forEach((ch, i) => setTimeout(() => saveBlob(`${ch.id}.chunk.json`, ch.chunk), i * 120)); - capMsg(`saving ${splitChunks.length} phrase chunk(s)…`, false); + capMsg(`saving ${splitChunks.length} .chunk.json file(s) — for griff manifest`, false); +} + +// Bundles every phrase chunk into a single JSON array — one file for sharing or +// review instead of N. NOTE: `griff manifest` reads individual .chunk.json files, +// not this array, so use "each phrase" for the corpus pipeline (#77). +function downloadBundle() { + if (splitChunks.length === 0) { capMsg('split into phrases first', true); return; } + const base = els.capId.value.trim() || (splitChunks[0].id || 'phrases').replace(/_p\d+$/, ''); + const chunks = splitChunks.map((ch) => { + try { return JSON.parse(ch.chunk); } catch (_) { return { id: ch.id, error: 'unparseable chunk' }; } + }); + saveBlob(`${base}.chunks.json`, JSON.stringify(chunks, null, 2)); + capMsg(`saved ${base}.chunks.json · ${chunks.length} phrase(s) in one file — for sharing/review`, false); } // ---- drawing ---- @@ -520,7 +549,8 @@ function bind() { els.splitPlay.addEventListener('click', () => { renderPhrase(); play(); }); els.splitStop.addEventListener('click', stop); els.splitDownload.addEventListener('click', downloadPhrase); - els.splitDownloadAll.addEventListener('click', downloadAllPhrases); + els.splitDownloadEach.addEventListener('click', downloadEachPhrase); + els.splitDownloadAll.addEventListener('click', downloadBundle); els.dbgCopy.addEventListener('click', copyDebug); els.dbgClear.addEventListener('click', clearDebug); window.addEventListener('resize', () => draw()); diff --git a/web/static/index.html b/web/static/index.html index 9ce97f47..ba2ecf27 100644 --- a/web/static/index.html +++ b/web/static/index.html @@ -169,14 +169,21 @@

Capture · chunk.json

+
- + +
+

+ each phrase → one .chunk.json per phrase for + griff manifest; all (one JSON) → a single + .chunks.json array for sharing/review. +

diff --git a/web/static/style.css b/web/static/style.css index c42a5716..2228cd53 100644 --- a/web/static/style.css +++ b/web/static/style.css @@ -150,6 +150,11 @@ h2 { font-size: 1.05rem; margin: 0 0 0.1rem; } height: 20px; accent-color: var(--accent); } +/* Read-only per-phrase tags (detected, not split input): ticked, dimmed chips + tinted with part-A blue to set them apart from the green capture inputs. */ +.tagsread { margin: 4px 0 2px; } +.tagsread label { color: var(--muted); } +.tagsread input[type="checkbox"] { accent-color: var(--a); } label.row { flex-flow: row; display: flex; align-items: center; gap: 10px; color: var(--ink); } label.row input[type="checkbox"] { width: 22px; height: 22px; accent-color: var(--accent); } #capBounds { display: block; margin: 2px; min-height: 1em; }