feat(web): the upload page shows the job's phases, a bar only where the count is real, and says when the service is lost (ADR-035)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -45,11 +45,11 @@ templ UploadsPage(v uploadsView) {
|
||||
}
|
||||
|
||||
// UploadPage is one upload's page: its title and the polling status region.
|
||||
templ UploadPage(u domain.UploadedTranscript, chatEnabled bool) {
|
||||
templ UploadPage(u domain.UploadedTranscript, chatEnabled bool, now time.Time) {
|
||||
@Layout("Tapir — " + u.Title) {
|
||||
<article class="account">
|
||||
<h1>{ u.Title }</h1>
|
||||
@uploadStatus(u, chatEnabled)
|
||||
@uploadStatus(u, chatEnabled, now)
|
||||
<p><a href="/uploads">All recordings</a></p>
|
||||
</article>
|
||||
}
|
||||
@@ -57,7 +57,7 @@ templ UploadPage(u domain.UploadedTranscript, chatEnabled bool) {
|
||||
|
||||
// uploadStatus is the region HTMX polls. Unfinished states carry hx-trigger so the
|
||||
// page re-asks; done and failed render without it, so nothing polls forever.
|
||||
templ uploadStatus(u domain.UploadedTranscript, chatEnabled bool) {
|
||||
templ uploadStatus(u domain.UploadedTranscript, chatEnabled bool, now time.Time) {
|
||||
switch u.Status {
|
||||
case domain.UploadDone:
|
||||
<div class="upload-status upload-done" role="status">
|
||||
@@ -67,6 +67,12 @@ templ uploadStatus(u domain.UploadedTranscript, chatEnabled bool) {
|
||||
of audio by <span class="chip">{ u.Endpoint }</span>.
|
||||
</p>
|
||||
@reconciliationNote(u.Reconciliation)
|
||||
if len(u.Progress) > 0 {
|
||||
<details class="what-was-done">
|
||||
<summary>What was done</summary>
|
||||
@progressSteps(u.Progress, now, false)
|
||||
</details>
|
||||
}
|
||||
if chatEnabled {
|
||||
<p><a class="btn" href={ templ.SafeURL("/uploads/" + u.ID + "/chat") }>Ask about this recording</a></p>
|
||||
}
|
||||
@@ -79,6 +85,12 @@ templ uploadStatus(u domain.UploadedTranscript, chatEnabled bool) {
|
||||
<div class="upload-status upload-failed flash flash-error" role="alert">
|
||||
@videoNotice(u.HadVideo)
|
||||
<p>{ uploadFailureMessage(u.ErrorKind) }</p>
|
||||
if len(u.Progress) > 0 {
|
||||
<details class="what-was-done" open>
|
||||
<summary>Where it stopped</summary>
|
||||
@progressSteps(u.Progress, now, false)
|
||||
</details>
|
||||
}
|
||||
</div>
|
||||
default:
|
||||
<div
|
||||
@@ -90,7 +102,13 @@ templ uploadStatus(u domain.UploadedTranscript, chatEnabled bool) {
|
||||
>
|
||||
@videoNotice(u.HadVideo)
|
||||
if u.Status == domain.UploadTranscribing {
|
||||
<p>Transcribing… a long recording takes a few minutes.</p>
|
||||
{{ health, silent := uploadHealth(u, now) }}
|
||||
if len(u.Progress) > 0 {
|
||||
@progressSteps(u.Progress, now, health != healthLost)
|
||||
@healthNote(health, silent)
|
||||
} else {
|
||||
<p>Transcribing… a long recording takes a few minutes.</p>
|
||||
}
|
||||
} else {
|
||||
<p>Queued — waiting for its turn to be transcribed.</p>
|
||||
}
|
||||
@@ -170,3 +188,54 @@ templ reconciliationNote(r *domain.Reconciliation) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// progressSteps is the job's phases as a log that fills in as it runs: finished ones
|
||||
// collapse to a line with how long they took, the running one has a spinner and its
|
||||
// elapsed time, and what has not started is dimmed. live is false when the spinner would
|
||||
// be a lie (the service is lost, or the job is over), so the running phase shows a
|
||||
// warning mark instead. Only a counted phase gets a bar (ADR-035).
|
||||
templ progressSteps(steps []domain.ProgressStep, now time.Time, live bool) {
|
||||
<ol class="steps" aria-label="Progress">
|
||||
for _, s := range steps {
|
||||
<li
|
||||
class={ "step step-" + string(s.State) }
|
||||
if s.State == domain.StepRunning {
|
||||
aria-current="step"
|
||||
}
|
||||
>
|
||||
if s.State == domain.StepRunning && live {
|
||||
<span class="step-mark spinner" aria-hidden="true"></span>
|
||||
} else if s.State == domain.StepRunning {
|
||||
<span class="step-mark" aria-hidden="true">!</span>
|
||||
} else {
|
||||
<span class="step-mark" aria-hidden="true">{ stepMark(s) }</span>
|
||||
}
|
||||
<span class="step-label">{ s.Label }</span>
|
||||
if t := stepTime(s, now); t != "" {
|
||||
<span class="step-time">{ t }</span>
|
||||
}
|
||||
if s.State == domain.StepRunning && s.Total > 0 {
|
||||
<progress class="step-bar" value={ strconv.Itoa(s.Done) } max={ strconv.Itoa(s.Total) }></progress>
|
||||
<span class="step-count">{ strconv.Itoa(s.Done) } of { strconv.Itoa(s.Total) }</span>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
</ol>
|
||||
}
|
||||
|
||||
// healthNote says so when the job is not simply running: the service stopped reporting,
|
||||
// or a counted phase has gone quiet. Running says nothing: the spinner does.
|
||||
templ healthNote(h jobHealth, silent time.Duration) {
|
||||
switch h {
|
||||
case healthLost:
|
||||
<p class="step-health step-health-lost" role="alert">
|
||||
The service stopped reporting { minutesAgo(silent) } ago. It may have restarted or crashed.
|
||||
This page keeps checking; if nothing changes, upload the recording again.
|
||||
</p>
|
||||
case healthQuiet:
|
||||
<p class="step-health step-health-quiet">
|
||||
Nothing new for { minutesAgo(silent) }: the model may be slow. The service is still running,
|
||||
and the job gives up on its own if it takes too long.
|
||||
</p>
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user