Files
tapir/internal/web/uploads.templ
T
mathiasandClaude Sonnet 5.5 cca0a7461b feat(web): tell the user when a video track was ignored, in every state
A Teams meeting with the camera off is still a video file. It is accepted, only
its audio is transcribed, and the page says so in queued, transcribing, done and
failed -- not just at the end.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 18:43:14 +02:00

126 lines
4.2 KiB
Templ

package web
import (
"fmt"
"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") {
<article class="account">
<h1>Upload a recording</h1>
<p class="muted">
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.
</p>
if v.Error != "" {
<div class="flash flash-error" role="alert">{ v.Error }</div>
}
<form method="post" action="/uploads" enctype="multipart/form-data">
<p><label>Title (optional) <input type="text" name="title" maxlength="200"/></label></p>
<p><input type="file" name="file" accept="audio/*,.m4a,.mp3,.wav,.opus,.ogg,.mp4" required/></p>
<p><button type="submit">Upload and transcribe</button></p>
</form>
if len(v.Rows) > 0 {
<section>
<h2>Your recordings</h2>
<ul>
for _, u := range v.Rows {
<li>
<a href={ templ.SafeURL("/uploads/" + u.ID) }>{ u.Title }</a>
<span class="chip">{ string(u.Status) }</span>
</li>
}
</ul>
</section>
}
</article>
}
}
// UploadPage is one upload's page: its title and the polling status region.
templ UploadPage(u domain.UploadedTranscript, chatEnabled bool) {
@Layout("Tapir — " + u.Title) {
<article class="account">
<h1>{ u.Title }</h1>
@uploadStatus(u, chatEnabled)
<p><a href="/uploads">All recordings</a></p>
</article>
}
}
// 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:
<div class="upload-status upload-done" role="status">
@videoNotice(u.HadVideo)
<p>
Transcribed { uploads.FormatDuration(time.Duration(u.AudioSeconds * float64(time.Second))) }
of audio by <span class="chip">{ u.Endpoint }</span>.
</p>
if chatEnabled {
<p><a class="btn" href={ templ.SafeURL("/uploads/" + u.ID + "/chat") }>Ask about this recording</a></p>
}
<details open>
<summary>Transcript</summary>
<pre class="transcript">{ u.Content }</pre>
</details>
</div>
case domain.UploadFailed:
<div class="upload-status upload-failed flash flash-error" role="alert">
@videoNotice(u.HadVideo)
<p>{ uploadFailureMessage(u.ErrorKind) }</p>
</div>
default:
<div
class="upload-status upload-working"
role="status"
hx-get={ "/uploads/" + u.ID + "/status" }
hx-trigger={ "every " + uploadPollEvery }
hx-swap="outerHTML"
>
@videoNotice(u.HadVideo)
if u.Status == domain.UploadTranscribing {
<p>Transcribing… a long recording takes a few minutes.</p>
} else {
<p>Queued — waiting for its turn to be transcribed.</p>
}
</div>
}
}
// 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) {
<article class="account">
<h1>{ u.Title }</h1>
<section id="chat-section" class="chat-dock chat-dock-open">
<h2 class="chat-heading">Ask about this recording</h2>
<p class="chat-scope muted">Answers come only from this recording's transcript, which is private to you.</p>
@chatPanel(v)
</section>
<p><a href={ templ.SafeURL("/uploads/" + u.ID) }>Back to the transcript</a></p>
</article>
}
}
// 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 {
<p class="muted video-notice">
This file contained a video track. Only its audio was used; the video was discarded and nothing from it was kept.
</p>
}
}