feat(web): immediate summarize + status poll + ASCII tapir spinner
Clicking "Summarize" now runs the summary in the background (when a Processor is
wired) instead of only queuing it. The handler flips the DB flag, kicks off the
work on a detached context, and returns an animated "processing" card that polls
GET /v/{id}/status every 2s via HTMX. Status returns the summary card once it
lands (no poll → polling stops), the animation while in-flight, or the Queued
card otherwise. Queue-only behaviour is unchanged when no Processor is set.
The spinner is a CSS-only cross-fade of three ASCII tapir frames — no JS, honours
prefers-reduced-motion.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -176,6 +176,30 @@ func summarizeURL(videoID string) templ.SafeURL {
|
||||
return templ.SafeURL("/v/" + videoID + "/summarize")
|
||||
}
|
||||
|
||||
// statusURL builds the processing-status poll path (GET) for a video id — the
|
||||
// HTMX poll target while an immediate summarization is in flight.
|
||||
func statusURL(videoID string) templ.SafeURL {
|
||||
return templ.SafeURL("/v/" + videoID + "/status")
|
||||
}
|
||||
|
||||
// tapirFrame1..3 are the ASCII tapir frames cycled by the TapirSpinner CSS
|
||||
// animation. Three near-identical frames (snout and feet shift) read as a stocky,
|
||||
// big-snouted tapir ambling in place while a summary is generated.
|
||||
const (
|
||||
tapirFrame1 = ` /\_____/\
|
||||
( · ω · )
|
||||
) ∩∩ (
|
||||
(__( )__)`
|
||||
tapirFrame2 = ` /\_____/\
|
||||
( · ω · )
|
||||
) ∩∩ (
|
||||
(__( ^ )__)`
|
||||
tapirFrame3 = ` /\_____/\
|
||||
( · ω · )
|
||||
) ∪∪ (
|
||||
(__( )__)`
|
||||
)
|
||||
|
||||
// summarizeModeLabel names the current mode for display.
|
||||
func summarizeModeLabel(auto bool) string {
|
||||
if auto {
|
||||
@@ -381,6 +405,22 @@ main { max-width: 60rem; margin: 0 auto; padding: var(--s4) var(--s3); }
|
||||
.card-pending { border-style: dashed; }
|
||||
.card-pending .card-title { color: var(--muted); font-weight: 600; }
|
||||
|
||||
/* summarizing animation — a little ASCII tapir ambling in place. Three frames
|
||||
are stacked absolutely and cross-faded by a stepped keyframe with staggered
|
||||
delays, so exactly one shows at a time (no JS). */
|
||||
.card-processing { border-style: dashed; }
|
||||
.tapir-spin { position: relative; height: 5.2em; margin: var(--s2) 0; }
|
||||
.tapir-spin pre { position: absolute; inset: 0; margin: 0; opacity: 0; font: .9rem/1.15 ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--accent); animation: tapir-cycle 1.2s steps(1, end) infinite; }
|
||||
.tapir-spin pre:nth-child(1) { animation-delay: 0s; }
|
||||
.tapir-spin pre:nth-child(2) { animation-delay: .4s; }
|
||||
.tapir-spin pre:nth-child(3) { animation-delay: .8s; }
|
||||
@keyframes tapir-cycle { 0%, 33.32% { opacity: 1; } 33.33%, 100% { opacity: 0; } }
|
||||
.tapir-label { color: var(--muted); font-size: .9rem; margin: 0; }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.tapir-spin pre { animation: none; }
|
||||
.tapir-spin pre:nth-child(1) { opacity: 1; }
|
||||
}
|
||||
|
||||
/* summarization mode toggle on the account page */
|
||||
.summarize-mode { display: flex; gap: var(--s3); align-items: center; flex-wrap: wrap; }
|
||||
.summarize-mode p { margin: 0; }
|
||||
|
||||
Reference in New Issue
Block a user