From 2fedc45443f42e943e63d214fe666c69a2a08557 Mon Sep 17 00:00:00 2001 From: Mathias Date: Sat, 6 Jun 2026 22:31:38 +0200 Subject: [PATCH] =?UTF-8?q?feat(web):=20unify=20card=20states=20=E2=80=94?= =?UTF-8?q?=20one=20'Summarize=20now'=20verb,=20honest=20no-captions=20sta?= =?UTF-8?q?te?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five explicit footer states, status-primary: 1. Summarized — chip + actions, no button (unchanged) 2. No captions (TranscriptStatus=="none") — NEW: 'No transcript available' muted text, no button, no POST URL. Removes the dead-end 'Summarize' button that tried and failed when there were no captions to fetch. 3. Queued (SummarizeRequested) — chip + muted text, no button (unchanged) 4. Rate-limited — 'Fetching soon…' + quiet 'Summarize now' → /retry-now 5. Pending — 'Not summarized' + quiet 'Summarize now' → /summarize One verb ('Summarize now'), one quiet style (.btn-quiet, renamed from .btn-retry which was state-specific). User doesn't see the internal pipeline distinction; both buttons post to their existing handlers unchanged. Form class renamed card-nudge-form. Dropped engineer-facing tooltip; user-facing hint added. 'Try now' wording removed entirely. --- internal/web/videocard_internal_test.go | 126 ++++++++- internal/web/view.go | 6 +- internal/web/views.templ | 36 ++- internal/web/views_templ.go | 360 ++++++++++++------------ 4 files changed, 327 insertions(+), 201 deletions(-) diff --git a/internal/web/videocard_internal_test.go b/internal/web/videocard_internal_test.go index d4f68a0..4bf2968 100644 --- a/internal/web/videocard_internal_test.go +++ b/internal/web/videocard_internal_test.go @@ -17,9 +17,75 @@ func renderVideoCard(t *testing.T, r store.SummaryRow) string { return sb.String() } -// A rate-limited, unsummarized video shows an active "Try now" button so the user -// can manually trigger an immediate fetch through the shared rate gate. -func TestVideoCard_RateLimitedShowsRetryingBadge(t *testing.T) { +// noActionButton asserts the rendered HTML contains no form POST (no action URL +// of the form /v/.../summarize or /v/.../retry-now) and no btn-quiet button. +func noActionButton(t *testing.T, html, state string) { + t.Helper() + if strings.Contains(html, "/summarize") { + t.Errorf("state %q: expected no summarize URL, got:\n%s", state, html) + } + if strings.Contains(html, "/retry-now") { + t.Errorf("state %q: expected no retry-now URL, got:\n%s", state, html) + } + if strings.Contains(html, "btn-quiet") { + t.Errorf("state %q: expected no action button, got:\n%s", state, html) + } +} + +// TestVideoCard_State1_Summarized — chip + no nudge button. +func TestVideoCard_State1_Summarized(t *testing.T) { + html := renderVideoCard(t, store.SummaryRow{ + VideoID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + Title: "Done Video", + Summarized: true, + Summary: "A great talk about Go.", + AIProvider: "local", + }) + + if !strings.Contains(html, "local") { + t.Errorf("state 1: expected AI provider chip, got:\n%s", html) + } + noActionButton(t, html, "summarized") + if strings.Contains(html, "Summarize now") { + t.Errorf("state 1: no nudge button on a summarized card, got:\n%s", html) + } +} + +// TestVideoCard_State2_NoTranscript — terminal; muted status, NO button, NO POST URL. +func TestVideoCard_State2_NoTranscript(t *testing.T) { + html := renderVideoCard(t, store.SummaryRow{ + VideoID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + Title: "Silent Video", + Summarized: false, + TranscriptStatus: "none", + }) + + if !strings.Contains(html, "No transcript available") { + t.Errorf("state 2: expected 'No transcript available' text, got:\n%s", html) + } + noActionButton(t, html, "none-transcript") + if strings.Contains(html, "Summarize now") { + t.Errorf("state 2: no nudge button when there are no captions, got:\n%s", html) + } +} + +// TestVideoCard_State3_Queued — "Queued" chip, no button. +func TestVideoCard_State3_Queued(t *testing.T) { + html := renderVideoCard(t, store.SummaryRow{ + VideoID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + Title: "Queued Video", + Summarized: false, + SummarizeRequested: true, + }) + + if !strings.Contains(html, "Queued") { + t.Errorf("state 3: expected 'Queued' chip, got:\n%s", html) + } + noActionButton(t, html, "queued") +} + +// TestVideoCard_State4_RateLimited — quiet status + "Summarize now" → retry-now URL. +func TestVideoCard_State4_RateLimited(t *testing.T) { html := renderVideoCard(t, store.SummaryRow{ VideoID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", Title: "Throttled Video", @@ -27,29 +93,61 @@ func TestVideoCard_RateLimitedShowsRetryingBadge(t *testing.T) { TranscriptStatus: "rate_limited", }) - if !strings.Contains(html, "Try now") { - t.Errorf("expected a 'Try now' button, got:\n%s", html) + if !strings.Contains(html, "Fetching soon") { + t.Errorf("state 4: expected 'Fetching soon' status text, got:\n%s", html) + } + if !strings.Contains(html, "Summarize now") { + t.Errorf("state 4: expected 'Summarize now' button, got:\n%s", html) } if !strings.Contains(html, "retry-now") { - t.Errorf("expected the retry-now route in the form action, got:\n%s", html) + t.Errorf("state 4: expected retry-now URL in form action, got:\n%s", html) } - if strings.Contains(html, ">Summarize<") { - t.Errorf("the Summarize button must be hidden for a rate-limited video, got:\n%s", html) + if strings.Contains(html, "/summarize\"") { + t.Errorf("state 4: rate-limited card must not POST to /summarize, got:\n%s", html) + } + if strings.Contains(html, "Try now") { + t.Errorf("state 4: 'Try now' verb must not appear, got:\n%s", html) } } -// An ordinary unsummarized video still offers the Summarize button. -func TestVideoCard_UnsummarizedShowsSummarize(t *testing.T) { +// TestVideoCard_State5_Pending — quiet status + "Summarize now" → summarize URL. +func TestVideoCard_State5_Pending(t *testing.T) { html := renderVideoCard(t, store.SummaryRow{ VideoID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", Title: "Fresh Video", Summarized: false, }) - if !strings.Contains(html, ">Summarize<") { - t.Errorf("expected a Summarize button, got:\n%s", html) + if !strings.Contains(html, "Not summarized") { + t.Errorf("state 5: expected 'Not summarized' status text, got:\n%s", html) } - if strings.Contains(html, "Retrying later") { - t.Errorf("no retry badge for a non-rate-limited video, got:\n%s", html) + if !strings.Contains(html, "Summarize now") { + t.Errorf("state 5: expected 'Summarize now' button, got:\n%s", html) + } + if !strings.Contains(html, "/summarize") { + t.Errorf("state 5: expected summarize URL in form action, got:\n%s", html) + } + if strings.Contains(html, "retry-now") { + t.Errorf("state 5: pending card must not POST to /retry-now, got:\n%s", html) + } + if strings.Contains(html, "Try now") { + t.Errorf("state 5: 'Try now' verb must not appear, got:\n%s", html) + } +} + +// TestVideoCard_TryNowAbsent — "Try now" must not appear in any rendered card. +func TestVideoCard_TryNowAbsent(t *testing.T) { + cases := []store.SummaryRow{ + {VideoID: "a", Summarized: true, Summary: "s", AIProvider: "local"}, + {VideoID: "b", TranscriptStatus: "none"}, + {VideoID: "c", SummarizeRequested: true}, + {VideoID: "d", TranscriptStatus: "rate_limited"}, + {VideoID: "e"}, + } + for _, r := range cases { + html := renderVideoCard(t, r) + if strings.Contains(html, "Try now") { + t.Errorf("'Try now' must not appear in any card state; video %q got:\n%s", r.VideoID, html) + } } } diff --git a/internal/web/view.go b/internal/web/view.go index 660a801..10823df 100644 --- a/internal/web/view.go +++ b/internal/web/view.go @@ -544,9 +544,9 @@ a.btn, a.btn:visited { color: var(--accent-fg); } .pipeline-bar { display: flex; gap: var(--s3); align-items: center; flex-wrap: wrap; margin-bottom: var(--s3); font-size: .8rem; color: var(--muted); } .pipeline-bar span { display: flex; align-items: center; gap: var(--s1); } .pipeline-bar span + span::before { content: "·"; margin-right: var(--s1); } -.retry-form { display: inline; } -.btn-retry { font: inherit; font-size: .72rem; font-weight: 600; padding: .15rem .55rem; border-radius: 999px; border: 1px solid var(--accent); background: transparent; color: var(--accent); cursor: pointer; } -.btn-retry:hover { background: var(--accent-weak); } +.card-nudge-form { display: inline; } +.btn-quiet { font: inherit; font-size: .72rem; font-weight: 600; padding: .15rem .55rem; border-radius: 999px; border: 1px solid var(--accent); background: transparent; color: var(--accent); cursor: pointer; } +.btn-quiet:hover { background: var(--accent-weak); } .chip-warn { background: rgba(255, 110, 156, .15); color: #FF6E9C; } .card-state { color: var(--muted); font-size: .8rem; } .badge { display: inline-block; padding: .15rem .55rem; border-radius: 999px; background: var(--badge-bg); color: var(--badge-fg); font-size: .72rem; font-weight: 600; } diff --git a/internal/web/views.templ b/internal/web/views.templ index 8d8096c..957c69b 100644 --- a/internal/web/views.templ +++ b/internal/web/views.templ @@ -182,11 +182,15 @@ templ summaryList(rows []store.SummaryRow, hasConnected bool) { } } -// VideoCard is one list card, also returned standalone by POST /v/{id}/summarize -// (HTMX swaps it in place via outerHTML). A summarized video links to its detail -// page and shows its provider chip / fallback badge / action state. An -// unsummarized video gets a muted "pending" treatment and either a "Summarize" -// button (to queue it) or a "Queued" chip when already requested. +// VideoCard is one list card, returned standalone by POST /v/{id}/summarize and +// /v/{id}/retry-now (HTMX swaps outerHTML). Five footer states, status-primary: +// 1. Summarized — preview + chip + actions; no button. +// 2. No captions (TranscriptStatus=="none") — terminal; "No transcript available"; no button. +// 3. Queued (SummarizeRequested) — "Queued · waiting for the next run"; no button. +// 4. Rate-limited — "Fetching soon" + quiet "Summarize now" → /retry-now. +// 5. Pending (else) — "Not summarized" + quiet "Summarize now" → /summarize. +// States 4 and 5 use one verb ("Summarize now") and one style (.btn-quiet); the +// backend side-effect difference (clear-backoff vs. set-flag) is invisible to users. templ VideoCard(r store.SummaryRow) {
  • if r.Summarized { @@ -204,6 +208,7 @@ templ VideoCard(r store.SummaryRow) { }
    if r.Summarized { + // State 1: summarized — provider chip, fallback badge, action state. if r.AIProvider != "" { { r.AIProvider } } @@ -213,29 +218,38 @@ templ VideoCard(r store.SummaryRow) { if len(r.Actions) > 0 { { strings.Join(r.Actions, ", ") } } + } else if r.TranscriptStatus == "none" { + // State 2: no captions — terminal dead-end; nothing the user can do. + No transcript available + } else if r.SummarizeRequested { + // State 3: queued for the next scheduled pass. + Queued + waiting for the next run } else if r.TranscriptStatus == "rate_limited" { + // State 4: rate-limited — quiet status + quiet nudge button → retry-now handler. + Fetching soon…
    - +
    - } else if r.SummarizeRequested { - Queued - waiting for the next run } else { + // State 5: pending — discovered, not yet attempted; nudge button → summarize handler. + Not summarized
    - +
    }
    diff --git a/internal/web/views_templ.go b/internal/web/views_templ.go index 3a69655..46286fb 100644 --- a/internal/web/views_templ.go +++ b/internal/web/views_templ.go @@ -624,11 +624,16 @@ func summaryList(rows []store.SummaryRow, hasConnected bool) templ.Component { }) } -// VideoCard is one list card, also returned standalone by POST /v/{id}/summarize -// (HTMX swaps it in place via outerHTML). A summarized video links to its detail -// page and shows its provider chip / fallback badge / action state. An -// unsummarized video gets a muted "pending" treatment and either a "Summarize" -// button (to queue it) or a "Queued" chip when already requested. +// VideoCard is one list card, returned standalone by POST /v/{id}/summarize and +// /v/{id}/retry-now (HTMX swaps outerHTML). Five footer states, status-primary: +// 1. Summarized — preview + chip + actions; no button. +// 2. No captions (TranscriptStatus=="none") — terminal; "No transcript available"; no button. +// 3. Queued (SummarizeRequested) — "Queued · waiting for the next run"; no button. +// 4. Rate-limited — "Fetching soon" + quiet "Summarize now" → /retry-now. +// 5. Pending (else) — "Not summarized" + quiet "Summarize now" → /summarize. +// +// States 4 and 5 use one verb ("Summarize now") and one style (.btn-quiet); the +// backend side-effect difference (clear-backoff vs. set-flag) is invisible to users. func VideoCard(r store.SummaryRow) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context @@ -675,7 +680,7 @@ func VideoCard(r store.SummaryRow) templ.Component { var templ_7745c5c3_Var27 string templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.ResolveAttributeValue("video-" + r.VideoID) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 191, Col: 88} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 195, Col: 88} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var27) if templ_7745c5c3_Err != nil { @@ -693,7 +698,7 @@ func VideoCard(r store.SummaryRow) templ.Component { var templ_7745c5c3_Var28 templ.SafeURL templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinURLErrs(videoURL(r.VideoID)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 193, Col: 56} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 197, Col: 56} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) if templ_7745c5c3_Err != nil { @@ -706,7 +711,7 @@ func VideoCard(r store.SummaryRow) templ.Component { var templ_7745c5c3_Var29 string templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(displayTitle(r)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 193, Col: 76} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 197, Col: 76} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) if templ_7745c5c3_Err != nil { @@ -724,7 +729,7 @@ func VideoCard(r store.SummaryRow) templ.Component { var templ_7745c5c3_Var30 string templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(displayTitle(r)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 195, Col: 44} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 199, Col: 44} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) if templ_7745c5c3_Err != nil { @@ -743,7 +748,7 @@ func VideoCard(r store.SummaryRow) templ.Component { var templ_7745c5c3_Var31 string templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(cardMeta(r)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 198, Col: 39} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 202, Col: 39} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) if templ_7745c5c3_Err != nil { @@ -763,7 +768,7 @@ func VideoCard(r store.SummaryRow) templ.Component { var templ_7745c5c3_Var32 string templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(p) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 202, Col: 33} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 206, Col: 33} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) if templ_7745c5c3_Err != nil { @@ -780,153 +785,162 @@ func VideoCard(r store.SummaryRow) templ.Component { return templ_7745c5c3_Err } if r.Summarized { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } if r.AIProvider != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var33 string templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(r.AIProvider) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 208, Col: 38} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 213, Col: 38} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if r.FallbackUsed { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "fallback") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "fallback") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if len(r.Actions) > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var34 string templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(r.Actions, ", ")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 214, Col: 61} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 219, Col: 61} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } + } else if r.TranscriptStatus == "none" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, " No transcript available") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else if r.SummarizeRequested { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, " Queued waiting for the next run") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } } else if r.TranscriptStatus == "rate_limited" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "
    Fetching soon…
    ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } else if r.SummarizeRequested { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "Queued waiting for the next run") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "\" hx-swap=\"outerHTML\" class=\"card-nudge-form\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "
    Not summarized
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "\" hx-swap=\"outerHTML\" class=\"card-nudge-form\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "
  • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -960,7 +974,7 @@ func TapirSpinner() templ.Component { templ_7745c5c3_Var41 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "
    ")
    +		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "
    ")
     		if templ_7745c5c3_Err != nil {
     			return templ_7745c5c3_Err
     		}
    @@ -968,7 +982,7 @@ func TapirSpinner() templ.Component {
     		if templ_7745c5c3_Err != nil {
     			return templ_7745c5c3_Err
     		}
    -		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "
    ")
    +		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "
    ")
     		if templ_7745c5c3_Err != nil {
     			return templ_7745c5c3_Err
     		}
    @@ -976,7 +990,7 @@ func TapirSpinner() templ.Component {
     		if templ_7745c5c3_Err != nil {
     			return templ_7745c5c3_Err
     		}
    -		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "
    ")
    +		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "
    ")
     		if templ_7745c5c3_Err != nil {
     			return templ_7745c5c3_Err
     		}
    @@ -984,33 +998,33 @@ func TapirSpinner() templ.Component {
     		if templ_7745c5c3_Err != nil {
     			return templ_7745c5c3_Err
     		}
    -		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var43 string templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(tapirBarFill) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 255, Col: 99} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 269, Col: 99} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "

    Summarizing…

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "

    Summarizing…

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1044,64 +1058,64 @@ func processingCard(r store.SummaryRow) templ.Component { templ_7745c5c3_Var44 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "
  • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "\" hx-trigger=\"every 2s\" hx-swap=\"outerHTML\">
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var47 string templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(displayTitle(r)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 273, Col: 43} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 287, Col: 43} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if cardMeta(r) != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var48 string templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(cardMeta(r)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 275, Col: 39} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 289, Col: 39} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1110,7 +1124,7 @@ func processingCard(r store.SummaryRow) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "
  • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1153,99 +1167,99 @@ func DetailPage(r store.SummaryRow) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var51 string templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinStringErrs(displayTitle(r)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 286, Col: 24} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 300, Col: 24} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if detailMeta(r) != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var52 string templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.JoinStringErrs(detailMeta(r)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 289, Col: 26} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 303, Col: 26} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var52)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if r.FallbackUsed { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "fallback") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, "fallback") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if url, ok := embedURL(r.ProviderVideoID); ok { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "\" loading=\"lazy\" referrerpolicy=\"strict-origin-when-cross-origin\" allow=\"accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" allowfullscreen>") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if r.URL != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "

    watch on source ↗

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "\" rel=\"noopener noreferrer\">watch on source ↗

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1254,82 +1268,82 @@ func DetailPage(r store.SummaryRow) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "

    Summary

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "

    Summary

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var56 string templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.JoinStringErrs(r.Summary) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 313, Col: 31} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 327, Col: 31} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var56)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if len(r.Highlights) > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "

    Highlights

      ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "

      Highlights

        ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for _, h := range r.Highlights { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "
      • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, "
      • ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var57 string templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.JoinStringErrs(h) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 320, Col: 14} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 334, Col: 14} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var57)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "
      • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, "
      ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if len(r.Takeaways) > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "

    Takeaways

      ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, "

      Takeaways

        ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for _, t := range r.Takeaways { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, "
      • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, "
      • ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var58 string templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.JoinStringErrs(t) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 330, Col: 14} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 344, Col: 14} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var58)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, "
      • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, "
      ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1379,53 +1393,53 @@ func RegisterPage(email, errMsg string) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "

    Complete your registration

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "

    Complete your registration

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if email != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, "

    Signed in as ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, "

    Signed in as ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var61 string templ_7745c5c3_Var61, templ_7745c5c3_Err = templ.JoinStringErrs(email) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 347, Col: 40} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 361, Col: 40} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var61)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, ".

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, ".

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, "

    Choose a display name to finish setting up your Tapir account.

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, "

    Choose a display name to finish setting up your Tapir account.

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if errMsg != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var62 string templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.JoinStringErrs(errMsg) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 351, Col: 42} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 365, Col: 42} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var62)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1478,69 +1492,69 @@ func InvitePage(email, token, errMsg string) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, "

    Set up your Tapir account

    Invitation for ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, "

    Set up your Tapir account

    Invitation for ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var65 string templ_7745c5c3_Var65, templ_7745c5c3_Err = templ.JoinStringErrs(email) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 378, Col: 41} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 392, Col: 41} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var65)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, ".

    Choose a password to finish creating your account. You'll then log in with this email and password.

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, ".

    Choose a password to finish creating your account. You'll then log in with this email and password.

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if errMsg != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var66 string templ_7745c5c3_Var66, templ_7745c5c3_Err = templ.JoinStringErrs(errMsg) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 381, Col: 42} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 395, Col: 42} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var66)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 124, "\" readonly>
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1589,7 +1603,7 @@ func InviteInvalidPage() templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 123, "

    This invite link is no longer valid

    This invitation has expired or has already been used. Ask for a fresh invite link, or log in if you already have an account.

    Log in

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 125, "

    This invite link is no longer valid

    This invitation has expired or has already been used. Ask for a fresh invite link, or log in if you already have an account.

    Log in

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1640,30 +1654,30 @@ func InviteNoticePage(message string, showLogin bool) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 124, "

    Invitation

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "

    Invitation

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var73 string templ_7745c5c3_Var73, templ_7745c5c3_Err = templ.JoinStringErrs(message) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 422, Col: 15} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 436, Col: 15} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var73)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 125, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 127, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if showLogin { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "

    Log in

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 128, "

    Log in

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 127, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 129, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1718,43 +1732,43 @@ func AccountPage(displayName, email string, conns []store.Connection, autoSummar if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 128, "

    Account

    Display name
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 130, "

    Account

    Display name
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var76 string templ_7745c5c3_Var76, templ_7745c5c3_Err = templ.JoinStringErrs(displayNameOr(displayName)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 441, Col: 36} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 455, Col: 36} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var76)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 129, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 131, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if email != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 130, "
    Signed in as
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 132, "
    Signed in as
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var77 string templ_7745c5c3_Var77, templ_7745c5c3_Err = templ.JoinStringErrs(email) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 444, Col: 16} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 458, Col: 16} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var77)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 131, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 133, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 132, "

    Summarization

    Automatic summarizes every new video as it is discovered. Manual lets you pick which videos to summarize — new videos appear in your list with a Summarize button.

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 134, "

    Summarization

    Automatic summarizes every new video as it is discovered. Manual lets you pick which videos to summarize — new videos appear in your list with a Summarize button.

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1762,172 +1776,172 @@ func AccountPage(displayName, email string, conns []store.Connection, autoSummar if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 133, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 135, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if len(channelErrors) > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 134, "

    Unavailable channels

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 136, "

    Unavailable channels

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var78 string templ_7745c5c3_Var78, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d channel(s) returned errors on the last discovery pass.", len(channelErrors))) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 460, Col: 100} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 474, Col: 100} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var78)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 135, " These may have been deleted or made private on YouTube.

      ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 137, " These may have been deleted or made private on YouTube.

        ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for _, ce := range channelErrors { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 136, "
      • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 138, "
      • ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var79 string templ_7745c5c3_Var79, templ_7745c5c3_Err = templ.JoinStringErrs(ce.ChannelName) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 466, Col: 57} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 480, Col: 57} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var79)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 137, " unavailable since ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 139, " unavailable since ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var80 string templ_7745c5c3_Var80, templ_7745c5c3_Err = templ.JoinStringErrs(ce.FirstSeen.Format("2006-01-02")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 468, Col: 89} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 482, Col: 89} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var80)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 138, "
      • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 140, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 139, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 141, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 140, "

    Connected accounts

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 142, "

    Connected accounts

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if len(conns) == 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 141, "

    No connected video accounts yet.

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 143, "

    No connected video accounts yet.

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 142, "
      ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 144, "
        ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for _, c := range conns { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 143, "
      • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 145, "
      • ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var81 string templ_7745c5c3_Var81, templ_7745c5c3_Err = templ.JoinStringErrs(providerLabel(c.Provider)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 483, Col: 64} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 497, Col: 64} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var81)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 144, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 146, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if c.ProviderAccount != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 145, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 147, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var82 string templ_7745c5c3_Var82, templ_7745c5c3_Err = templ.JoinStringErrs(c.ProviderAccount) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 485, Col: 49} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 499, Col: 49} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var82)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 146, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 148, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 147, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 149, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var83 string templ_7745c5c3_Var83, templ_7745c5c3_Err = templ.JoinStringErrs(c.Status) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 487, Col: 38} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 501, Col: 38} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var83)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 148, "
        connected ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 150, "
        connected ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var84 string templ_7745c5c3_Var84, templ_7745c5c3_Err = templ.JoinStringErrs(c.ConnectedAt.Format("2006-01-02")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 489, Col: 83} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 503, Col: 83} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var84)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 149, "
      • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 152, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 151, "
      ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 153, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if !hasYouTube(conns) { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 152, "

    Connect YouTube

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 154, "

    Connect YouTube

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 153, "

    Delete account

    Permanently remove your Tapir account and all of its data — summaries, watch/skip/save actions, and connected accounts. This cannot be undone.

    Delete account…

    This permanently deletes your account and all data. Are you sure?

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 155, "

    Delete account

    Permanently remove your Tapir account and all of its data — summaries, watch/skip/save actions, and connected accounts. This cannot be undone.

    Delete account…

    This permanently deletes your account and all data. Are you sure?

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1966,46 +1980,46 @@ func summarizeModeControl(auto bool) templ.Component { templ_7745c5c3_Var86 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 154, "

    Current mode: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 156, "

    Current mode: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var87 string templ_7745c5c3_Var87, templ_7745c5c3_Err = templ.JoinStringErrs(summarizeModeLabel(auto)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 527, Col: 53} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 541, Col: 53} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var87)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 155, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 159, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -2038,33 +2052,33 @@ func ActionButtons(videoID string, active map[string]bool) templ.Component { templ_7745c5c3_Var90 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 158, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 162, "\" hx-target=\"#action-buttons\" hx-swap=\"outerHTML\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -2074,20 +2088,20 @@ func ActionButtons(videoID string, active map[string]bool) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 161, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 167, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 166, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 168, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err }