feat(web): collapse older + caption-less videos in the list

Stop the un-summarized back-catalogue from burying the readable summaries
(UX review B3/B4). One feed, with a noise-collapse — not sections:

- Summarized + recent un-summarized videos lead inline as cards.
- Un-summarized videos older than the recency window collapse into a single
  "Show N older videos — summarize on demand" disclosure (they will not
  auto-fill; they are manual-only). Window comes from App.RecencyWindow
  (= cfg.AutoSummarizeWindow); 0 disables the collapse (all inline).
- Caption-less videos collapse into one honest line ("N videos have no
  captions and can't be summarized") instead of N dead terminal cards.

bucketRows is a pure classifier (cutoff-driven; undated rows never age out);
App gains RecencyWindow + an injectable clock for the cutoff.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 13:55:07 +02:00
co-authored by Claude Opus 4.8
parent 3df0459fed
commit 40b703e02a
6 changed files with 748 additions and 546 deletions
+24 -8
View File
@@ -104,7 +104,7 @@ templ flashBanner(code string) {
// #summary-list region; a non-HTMX request renders the whole page. flash carries
// a one-shot notification (e.g. "connected", "registered") surfaced on arrival
// after a POST→redirect.
templ ListPage(rows []store.SummaryRow, f Filter, stats PipelineStats, flash string, hasConnected bool) {
templ ListPage(b listBuckets, f Filter, stats PipelineStats, flash string, hasConnected bool) {
@Layout("Tapir — Summaries") {
@flashBanner(flash)
@filterForm(f)
@@ -118,7 +118,7 @@ templ ListPage(rows []store.SummaryRow, f Filter, stats PipelineStats, flash str
</p>
}
<div id="summary-list">
@summaryList(rows, hasConnected)
@summaryList(b, hasConnected)
</div>
}
}
@@ -163,11 +163,14 @@ templ filterForm(f Filter) {
</form>
}
// summaryList is the swappable list fragment: one card per video (summarized or
// not). Cards reflow to a single column on mobile; an empty list shows a friendly
// first-run state instead of a blank table.
templ summaryList(rows []store.SummaryRow, hasConnected bool) {
if len(rows) == 0 {
// summaryList is the swappable list fragment. It leads with readable summaries +
// recent un-summarized cards (b.Main), then collapses the noise so it does not
// bury the payload (UX review B3/B4): a one-line count of caption-less videos,
// and a single disclosure holding the older un-summarized back-catalogue. Cards
// reflow to a single column on mobile; an empty list shows a friendly first-run
// state instead of a blank table.
templ summaryList(b listBuckets, hasConnected bool) {
if b.empty() {
if hasConnected {
<div class="empty empty-connected">
<strong>Your account is connected</strong>
@@ -182,10 +185,23 @@ templ summaryList(rows []store.SummaryRow, hasConnected bool) {
}
} else {
<ul class="cards">
for _, r := range rows {
for _, r := range b.Main {
@VideoCard(r)
}
</ul>
if b.NoCaption > 0 {
<p class="list-note muted">{ fmt.Sprintf("%d video(s) have no captions and can't be summarized.", b.NoCaption) }</p>
}
if len(b.Older) > 0 {
<details class="older-videos">
<summary>{ fmt.Sprintf("Show %d older videos — summarize on demand", len(b.Older)) }</summary>
<ul class="cards">
for _, r := range b.Older {
@VideoCard(r)
}
</ul>
</details>
}
}
}