package web
import (
"fmt"
"strconv"
"time"
"git.d-ma.be/mathias/tapir/internal/domain"
"git.d-ma.be/mathias/tapir/internal/uploads"
)
// UploadsPage is the upload form plus the caller's own uploads (ADR-033).
templ UploadsPage(v uploadsView) {
@Layout("Tapir — Upload a recording") {
Upload a recording
Audio only. Up to { fmt.Sprintf("%d MB", v.Limits.MaxBytes>>20) } and
{ uploads.FormatDuration(v.Limits.MaxDuration) }. The transcript is private to you;
the audio is deleted once it has been transcribed.
if v.Error != "" {
{ v.Error }
}
if len(v.Rows) > 0 {
Your recordings
for _, u := range v.Rows {
-
{ u.Title }
{ string(u.Status) }
}
}
}
}
// UploadPage is one upload's page: its title and the polling status region.
templ UploadPage(u domain.UploadedTranscript, chatEnabled bool) {
@Layout("Tapir — " + u.Title) {
{ u.Title }
@uploadStatus(u, chatEnabled)
All recordings
}
}
// 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) {
switch u.Status {
case domain.UploadDone:
@videoNotice(u.HadVideo)
Transcribed { uploads.FormatDuration(time.Duration(u.AudioSeconds * float64(time.Second))) }
of audio by { u.Endpoint }.
@reconciliationNote(u.Reconciliation)
if chatEnabled {
Ask about this recording
}
Transcript
{ u.Content }
case domain.UploadFailed:
@videoNotice(u.HadVideo)
{ uploadFailureMessage(u.ErrorKind) }
default:
@videoNotice(u.HadVideo)
if u.Status == domain.UploadTranscribing {
Transcribing… a long recording takes a few minutes.
} else {
Queued — waiting for its turn to be transcribed.
}
}
}
// UploadChatPage is the no-JS full page for chatting with an uploaded transcript:
// the same chat panel as a video's (ADR-027), scoped to this recording.
templ UploadChatPage(u domain.UploadedTranscript, v chatView) {
@Layout("Tapir — Ask about " + u.Title) {
{ u.Title }
Ask about this recording
Answers come only from this recording's transcript, which is private to you.
@chatPanel(v)
Back to the transcript
}
}
// videoNotice tells the user a video track was present and what became of it. A
// Teams meeting with the camera off is still a video file; it is accepted, only the
// audio is transcribed, and the picture is thrown away — so say so, in every state,
// instead of leaving the user to wonder (ADR-033).
templ videoNotice(hadVideo bool) {
if hadVideo {
This file contained a video track. Only its audio was used; the video was discarded and nothing from it was kept.
}
}
// reconciliationNote says what was done when two engines transcribed the audio
// (ADR-034), and never claims a second opinion that was not there: nil means a single
// engine and renders nothing. Spots the model changed or was unsure about are listed
// with a minute mark and both readings so the user can check them against the audio.
templ reconciliationNote(r *domain.Reconciliation) {
if r != nil {
switch r.Status {
case domain.ReconcileDone:
Reconciled from two transcripts ({ r.Base } and { r.Alt }):
{ strconv.Itoa(r.Disputes) } differences,
{ strconv.Itoa(r.AutoResolved) } settled automatically,
{ strconv.Itoa(r.Adjudicated) } decided by the model ({ r.Reconciler }),
{ strconv.Itoa(r.Unsure) } uncertain.
if len(r.Contested) > 0 {
Spots to check against the audio
for _, c := range shownSpots(r) {
-
{ minuteMark(c.Seconds) }: { c.Base } / { c.Alt } —
if c.Unsure {
uncertain kept { c.Used }
} else {
changed used { c.Used }
}
}
if moreSpots(r) > 0 {
and { strconv.Itoa(moreSpots(r)) } more not listed
}
}
case domain.ReconcileSingle:
Only one engine produced this transcript ({ r.Reason }), so there was no second opinion to reconcile it with.
case domain.ReconcileSkipped:
Two transcripts were made but could not be reconciled ({ r.Reason }); the text shown is { r.Base }'s, unreconciled.
}
}
}