From 4fdb212cef0815b917f8efedc25cb6c55b4dba74 Mon Sep 17 00:00:00 2001 From: Mathias Date: Thu, 1 Oct 2026 16:58:25 +0200 Subject: [PATCH] feat(web): chat with an uploaded transcript, reusing the ADR-027 path (ADR-033) GET/POST /uploads/{id}/chat, mounted only when both an Uploader and a Chatter are wired. The row comes from the user-scoped private store, so another user's upload is a 404 before any model is involved. The owner's transcript goes to the SAME Chatter with App.UploadChatMaxChars instead of the 18,000-char caption default. The chat panel is shared, not forked: chatView gains PostURL/Subject (empty = a YouTube video, so existing callers are unchanged). The done card links to chat only when chat is wired. Mutation-checked: dropping the upload budget, ignoring the done-status guard, or scoping the lookup to the wrong user each turn tests red. Co-Authored-By: Claude Sonnet 5.5 --- internal/web/chat.go | 24 ++ internal/web/handlers.go | 10 + internal/web/uploads.go | 83 ++++- internal/web/uploads.templ | 25 +- internal/web/uploads_templ.go | 178 ++++++++-- internal/web/views.templ | 8 +- internal/web/views_templ.go | 622 ++++++++++++++++++---------------- 7 files changed, 609 insertions(+), 341 deletions(-) diff --git a/internal/web/chat.go b/internal/web/chat.go index b8e2d80..71aaa31 100644 --- a/internal/web/chat.go +++ b/internal/web/chat.go @@ -6,6 +6,8 @@ import ( "net/http" "strings" + "github.com/a-h/templ" + "git.d-ma.be/mathias/tapir/internal/adapters/chat" "git.d-ma.be/mathias/tapir/internal/adapters/store" ) @@ -43,6 +45,28 @@ type chatView struct { History []chat.Turn Truncated bool Error string + // PostURL and Subject let the same chat panel serve an UPLOADED recording + // (ADR-033): where the form posts, and what the transcript is called. Empty + // means a YouTube video (/v/{VideoID}/chat, "video"), so existing callers are + // unchanged. + PostURL templ.SafeURL + Subject string +} + +// postURL is where the chat form posts. +func (v chatView) postURL() templ.SafeURL { + if v.PostURL != "" { + return v.PostURL + } + return chatURL(v.VideoID) +} + +// subject is what the transcript is called in the panel's wording. +func (v chatView) subject() string { + if v.Subject != "" { + return v.Subject + } + return "video" } // handleChat renders the chat page for a summarized video (GET). Entry is scoped diff --git a/internal/web/handlers.go b/internal/web/handlers.go index b297dce..b9b1ed2 100644 --- a/internal/web/handlers.go +++ b/internal/web/handlers.go @@ -113,6 +113,10 @@ type App struct { // front rather than discovered by failing. Uploads Uploader UploadLimits uploads.Limits + // UploadChatMaxChars is the transcript budget for chat over an uploaded + // recording. The caption default (18,000 chars, ~22 minutes of speech) would + // keep only the first third of an hour-long meeting. + UploadChatMaxChars int // Processing tracks in-flight immediate summarizations so the status endpoint // shows the animation until the summary lands. The zero value is ready to use. Processing ProcessingSet @@ -186,6 +190,12 @@ func (a *App) Router() http.Handler { app.HandleFunc("POST /uploads", a.handleUploadPost) app.HandleFunc("GET /uploads/{id}", a.handleUploadDetail) app.HandleFunc("GET /uploads/{id}/status", a.handleUploadStatus) + // Chat over the uploaded transcript reuses the ADR-027 Chatter; mounted only + // when one is wired. + if a.Chat != nil { + app.HandleFunc("GET /uploads/{id}/chat", a.handleUploadChat) + app.HandleFunc("POST /uploads/{id}/chat", a.handleUploadChatMessage) + } } app.HandleFunc("GET /register", a.handleRegisterForm) app.HandleFunc("POST /register", a.handleRegister) diff --git a/internal/web/uploads.go b/internal/web/uploads.go index abb8711..295ced5 100644 --- a/internal/web/uploads.go +++ b/internal/web/uploads.go @@ -9,6 +9,9 @@ import ( "strings" "unicode/utf8" + "github.com/a-h/templ" + + "git.d-ma.be/mathias/tapir/internal/adapters/chat" "git.d-ma.be/mathias/tapir/internal/adapters/store" "git.d-ma.be/mathias/tapir/internal/domain" "git.d-ma.be/mathias/tapir/internal/uploads" @@ -129,7 +132,7 @@ func (a *App) handleUploadDetail(w http.ResponseWriter, r *http.Request) { if !ok { return } - a.render(w, r, UploadPage(*u)) + a.render(w, r, UploadPage(*u, a.Chat != nil)) } // handleUploadStatus renders the status fragment HTMX polls. Unfinished states @@ -140,7 +143,7 @@ func (a *App) handleUploadStatus(w http.ResponseWriter, r *http.Request) { if !ok { return } - a.render(w, r, uploadStatus(*u)) + a.render(w, r, uploadStatus(*u, a.Chat != nil)) } func (a *App) loadOwnedUpload(w http.ResponseWriter, r *http.Request) (*domain.UploadedTranscript, bool) { @@ -189,3 +192,79 @@ func uploadFailureMessage(k domain.UploadErrorKind) string { return "This upload failed. Please upload it again." } } + +// handleUploadChat renders the chat page for a FINISHED upload (GET). The row is +// read through the user-scoped store, so another user's id is a 404 before any +// model is involved. An unfinished upload has no transcript to ask about yet, so +// it goes back to its status page. +func (a *App) handleUploadChat(w http.ResponseWriter, r *http.Request) { + u, ok := a.loadOwnedUpload(w, r) + if !ok { + return + } + if u.Status != domain.UploadDone { + http.Redirect(w, r, "/uploads/"+u.ID, http.StatusSeeOther) + return + } + a.render(w, r, UploadChatPage(*u, a.uploadChatView(*u, a.Chat.DefaultModel(""), nil, false, ""))) +} + +// handleUploadChatMessage answers one question against the owner's uploaded +// transcript (POST), reusing the ADR-027 Chatter. Nothing is persisted: the prior +// turns ride in the form. The transcript goes to the model with the UPLOAD budget, +// not the caption default. A model failure is inline, not a 500. +func (a *App) handleUploadChatMessage(w http.ResponseWriter, r *http.Request) { + u, ok := a.loadOwnedUpload(w, r) + if !ok { + return + } + if u.Status != domain.UploadDone { + http.Redirect(w, r, "/uploads/"+u.ID, http.StatusSeeOther) + return + } + if err := r.ParseForm(); err != nil { + http.Error(w, "bad form", http.StatusBadRequest) + return + } + model := a.resolveModel(r.FormValue("model"), "") + history := parseHistory(r.Form["hq"], r.Form["ha"]) + question := strings.TrimSpace(r.FormValue("question")) + + view := a.uploadChatView(*u, model, history, false, "") + if question != "" { + reply, err := a.Chat.Answer(r.Context(), chat.Request{ + Model: model, + Transcript: u.Content, + History: history, + Question: question, + MaxChars: a.UploadChatMaxChars, + }) + if err != nil { + a.logger().Error("upload chat answer", "upload", u.ID, "model", model, "err", err) + view.Error = "That model couldn't answer just now. Try again, or switch models." + } else { + view.History = appendTurn(history, chat.Turn{Question: question, Answer: reply.Answer}) + view.Truncated = reply.Truncated + } + } + if isHTMX(r) { + a.render(w, r, chatPanel(view)) + return + } + a.render(w, r, UploadChatPage(*u, view)) +} + +func (a *App) uploadChatView(u domain.UploadedTranscript, model string, history []chat.Turn, truncated bool, errMsg string) chatView { + return chatView{ + VideoID: u.ID, + Title: u.Title, + Available: true, + Models: a.Chat.Models(), + Selected: model, + History: history, + Truncated: truncated, + Error: errMsg, + PostURL: templ.SafeURL("/uploads/" + u.ID + "/chat"), + Subject: "recording", + } +} diff --git a/internal/web/uploads.templ b/internal/web/uploads.templ index ffcb759..b084c10 100644 --- a/internal/web/uploads.templ +++ b/internal/web/uploads.templ @@ -44,11 +44,11 @@ templ UploadsPage(v uploadsView) { } // UploadPage is one upload's page: its title and the polling status region. -templ UploadPage(u domain.UploadedTranscript) { +templ UploadPage(u domain.UploadedTranscript, chatEnabled bool) { @Layout("Tapir — " + u.Title) { } @@ -56,7 +56,7 @@ templ UploadPage(u domain.UploadedTranscript) { // 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) { +templ uploadStatus(u domain.UploadedTranscript, chatEnabled bool) { switch u.Status { case domain.UploadDone:
@@ -64,6 +64,9 @@ templ uploadStatus(u domain.UploadedTranscript) { Transcribed { uploads.FormatDuration(time.Duration(u.AudioSeconds * float64(time.Second))) } of audio by { u.Endpoint }.

+ if chatEnabled { +

Ask about this recording

+ }
Transcript
{ u.Content }
@@ -89,3 +92,19 @@ templ uploadStatus(u domain.UploadedTranscript) {
} } + +// 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) { + + } +} diff --git a/internal/web/uploads_templ.go b/internal/web/uploads_templ.go index a874853..c848d97 100644 --- a/internal/web/uploads_templ.go +++ b/internal/web/uploads_templ.go @@ -5,8 +5,10 @@ package web //lint:file-ignore SA4006 This context is only used if a nested component is present. -import "github.com/a-h/templ" -import templruntime "github.com/a-h/templ/runtime" +import ( + "github.com/a-h/templ" + templruntime "github.com/a-h/templ/runtime" +) import ( "fmt" @@ -173,7 +175,7 @@ func UploadsPage(v uploadsView) templ.Component { } // UploadPage is one upload's page: its title and the polling status region. -func UploadPage(u domain.UploadedTranscript) templ.Component { +func UploadPage(u domain.UploadedTranscript, chatEnabled bool) 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 if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -223,7 +225,7 @@ func UploadPage(u domain.UploadedTranscript) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = uploadStatus(u).Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = uploadStatus(u, chatEnabled).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -243,7 +245,7 @@ func UploadPage(u domain.UploadedTranscript) templ.Component { // 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. -func uploadStatus(u domain.UploadedTranscript) templ.Component { +func uploadStatus(u domain.UploadedTranscript, chatEnabled bool) 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 if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -292,84 +294,107 @@ func uploadStatus(u domain.UploadedTranscript) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, ".

Transcript
")
+			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, ".

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var15 string - templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(u.Content) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/uploads.templ`, Line: 69, Col: 40} + if chatEnabled { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "

Ask about this recording

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - case domain.UploadFailed: - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "

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

Transcript
")
 			if templ_7745c5c3_Err != nil {
 				return templ_7745c5c3_Err
 			}
 			var templ_7745c5c3_Var16 string
-			templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(uploadFailureMessage(u.ErrorKind))
+			templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(u.Content)
 			if templ_7745c5c3_Err != nil {
-				return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/uploads.templ`, Line: 74, Col: 42}
+				return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/uploads.templ`, Line: 72, Col: 40}
 			}
 			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
 			if templ_7745c5c3_Err != nil {
 				return templ_7745c5c3_Err
 			}
-			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - default: - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var17 string - templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue("/uploads/" + u.ID + "/status") + templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(uploadFailureMessage(u.ErrorKind)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/uploads.templ`, Line: 80, Col: 43} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/uploads.templ`, Line: 77, Col: 42} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" hx-trigger=\"") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + default: + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\" hx-trigger=\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var19 string + templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue("every " + uploadPollEvery) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/uploads.templ`, Line: 84, Col: 43} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\" hx-swap=\"outerHTML\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if u.Status == domain.UploadTranscribing { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "

Transcribing… a long recording takes a few minutes.

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

Transcribing… a long recording takes a few minutes.

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

Queued — waiting for its turn to be transcribed.

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

Queued — waiting for its turn to be transcribed.

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -378,4 +403,87 @@ func uploadStatus(u domain.UploadedTranscript) templ.Component { }) } +// 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. +func UploadChatPage(u domain.UploadedTranscript, v chatView) 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 + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var20 := templ.GetChildren(ctx) + if templ_7745c5c3_Var20 == nil { + templ_7745c5c3_Var20 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Var21 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var22 string + templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(u.Title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/uploads.templ`, Line: 101, Col: 16} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "

Ask about this recording

Answers come only from this recording's transcript, which is private to you.

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = chatPanel(v).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "

Back to the transcript

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) + templ_7745c5c3_Err = Layout("Tapir — Ask about "+u.Title).Render(templ.WithChildren(ctx, templ_7745c5c3_Var21), templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + var _ = templruntime.GeneratedTemplate diff --git a/internal/web/views.templ b/internal/web/views.templ index 602dafd..55d9c9b 100644 --- a/internal/web/views.templ +++ b/internal/web/views.templ @@ -583,7 +583,7 @@ templ chatPanel(v chatView) { } if v.Truncated { -

Working from a bounded portion of a long transcript — answers about the end of the video may be incomplete.

+

Working from a bounded portion of a long transcript — answers about the end of the { v.subject() } may be incomplete.

} if v.Error != "" { @@ -591,8 +591,8 @@ templ chatPanel(v chatView) {
@@ -609,7 +609,7 @@ templ chatPanel(v chatView) { Switch models to compare answers on the same transcript. - + thinking…
diff --git a/internal/web/views_templ.go b/internal/web/views_templ.go index 1a0562b..8f2732f 100644 --- a/internal/web/views_templ.go +++ b/internal/web/views_templ.go @@ -5,8 +5,10 @@ package web //lint:file-ignore SA4006 This context is only used if a nested component is present. -import "github.com/a-h/templ" -import templruntime "github.com/a-h/templ/runtime" +import ( + "github.com/a-h/templ" + templruntime "github.com/a-h/templ/runtime" +) import ( "fmt" @@ -292,7 +294,7 @@ func flashBanner(code string) templ.Component { } ctx = templ.ClearChildren(ctx) if f, ok := flashFor(code); ok { - var templ_7745c5c3_Var9 = []any{"flash", "flash-" + f.Kind} + templ_7745c5c3_Var9 := []any{"flash", "flash-" + f.Kind} templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var9...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err @@ -824,7 +826,7 @@ func VideoCard(r store.SummaryRow) templ.Component { templ_7745c5c3_Var25 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - var templ_7745c5c3_Var26 = []any{"card", templ.KV("card-pending", !r.Summarized)} + templ_7745c5c3_Var26 := []any{"card", templ.KV("card-pending", !r.Summarized)} templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var26...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err @@ -2152,148 +2154,174 @@ func chatPanel(v chatView) templ.Component { return templ_7745c5c3_Err } if v.Truncated { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 173, "

Working from a bounded portion of a long transcript — answers about the end of the video may be incomplete.

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 174, " ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if v.Error != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 175, "

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

Working from a bounded portion of a long transcript — answers about the end of the ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var88 string - templ_7745c5c3_Var88, templ_7745c5c3_Err = templ.JoinStringErrs(v.Error) + templ_7745c5c3_Var88, templ_7745c5c3_Err = templ.JoinStringErrs(v.subject()) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 589, Col: 48} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 586, Col: 129} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var88)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 176, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 174, " may be incomplete.

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 177, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var89 string + templ_7745c5c3_Var89, templ_7745c5c3_Err = templ.JoinStringErrs(v.Error) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 589, Col: 48} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var89)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 177, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var89)) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 178, " ") + var templ_7745c5c3_Var91 string + templ_7745c5c3_Var91, templ_7745c5c3_Err = templ.ResolveAttributeValue(string(v.postURL())) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 595, Col: 33} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var91) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 180, "\" hx-target=\"#chat-panel\" hx-swap=\"outerHTML\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for _, t := range v.History { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 180, " ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 183, "
thinking…") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 190, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 192, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -2320,12 +2348,12 @@ func RegisterPage(email, errMsg string) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var95 := templ.GetChildren(ctx) - if templ_7745c5c3_Var95 == nil { - templ_7745c5c3_Var95 = templ.NopComponent + templ_7745c5c3_Var97 := templ.GetChildren(ctx) + if templ_7745c5c3_Var97 == nil { + templ_7745c5c3_Var97 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Var96 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_Var98 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) if !templ_7745c5c3_IsBuffer { @@ -2337,59 +2365,59 @@ func RegisterPage(email, errMsg string) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 191, "

Complete your registration

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

Complete your registration

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

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

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

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

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

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

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

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, 195, "

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

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

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

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 197, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 199, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } return nil }) - templ_7745c5c3_Err = Layout("Tapir — Register").Render(templ.WithChildren(ctx, templ_7745c5c3_Var96), templ_7745c5c3_Buffer) + templ_7745c5c3_Err = Layout("Tapir — Register").Render(templ.WithChildren(ctx, templ_7745c5c3_Var98), templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -2417,12 +2445,12 @@ func AccountPage(displayName, email string, conns []store.Connection, autoSummar }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var99 := templ.GetChildren(ctx) - if templ_7745c5c3_Var99 == nil { - templ_7745c5c3_Var99 = templ.NopComponent + templ_7745c5c3_Var101 := templ.GetChildren(ctx) + if templ_7745c5c3_Var101 == nil { + templ_7745c5c3_Var101 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Var100 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_Var102 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) if !templ_7745c5c3_IsBuffer { @@ -2438,43 +2466,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, 198, "

Account

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

Account

Display name
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var101 string - templ_7745c5c3_Var101, templ_7745c5c3_Err = templ.JoinStringErrs(displayNameOr(displayName)) + var templ_7745c5c3_Var103 string + templ_7745c5c3_Var103, 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: 656, Col: 36} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var101)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var103)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 199, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 201, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if email != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 200, "
Signed in as
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 202, "
Signed in as
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var102 string - templ_7745c5c3_Var102, templ_7745c5c3_Err = templ.JoinStringErrs(email) + var templ_7745c5c3_Var104 string + templ_7745c5c3_Var104, templ_7745c5c3_Err = templ.JoinStringErrs(email) if templ_7745c5c3_Err != nil { return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 659, Col: 16} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var102)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var104)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 201, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 203, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 202, "

Summarization

Automatic summarizes new videos from about the last week as they are discovered. Older videos stay browsable — summarize them on demand. Manual lets you pick which videos to summarize — every new video appears in your list with a Summarize button.

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

Summarization

Automatic summarizes new videos from about the last week as they are discovered. Older videos stay browsable — summarize them on demand. Manual lets you pick which videos to summarize — every new video appears in your list with a Summarize button.

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -2482,178 +2510,178 @@ 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, 203, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 205, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if len(channelErrors) > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 204, "

Unavailable channels

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

Unavailable channels

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var103 string - templ_7745c5c3_Var103, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d channel(s) returned errors on the last discovery pass.", len(channelErrors))) + var templ_7745c5c3_Var105 string + templ_7745c5c3_Var105, 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: 676, Col: 100} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var103)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var105)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 205, " These may have been deleted or made private on YouTube.

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 207, " 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, 206, "
    • ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var104 string - templ_7745c5c3_Var104, 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: 682, Col: 57} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var104)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 207, " unavailable since ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var105 string - templ_7745c5c3_Var105, 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: 684, Col: 89} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var105)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 208, "
    • ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 209, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 210, "

Connected accounts

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

No connected video accounts yet.

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 212, "
    ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - for _, c := range conns { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 213, "
  • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 208, "
  • ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var106 string - templ_7745c5c3_Var106, templ_7745c5c3_Err = templ.JoinStringErrs(providerLabel(c.Provider)) + templ_7745c5c3_Var106, 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: 699, Col: 64} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 682, Col: 57} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var106)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 214, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 209, " unavailable since ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - if c.ProviderAccount != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 215, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var107 string - templ_7745c5c3_Var107, 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: 701, Col: 49} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var107)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 216, " ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } + var templ_7745c5c3_Var107 string + templ_7745c5c3_Var107, 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: 684, Col: 89} } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 217, "") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var107)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 210, "
  • ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 211, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 212, "

Connected accounts

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

No connected video accounts yet.

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 214, "
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, c := range conns { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 215, "
  • ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var108 string - templ_7745c5c3_Var108, templ_7745c5c3_Err = templ.JoinStringErrs(c.Status) + templ_7745c5c3_Var108, 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: 703, Col: 38} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 699, Col: 64} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var108)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 218, "
    connected ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 216, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var109 string - templ_7745c5c3_Var109, 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: 705, Col: 83} + if c.ProviderAccount != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 217, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var109 string + templ_7745c5c3_Var109, 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: 701, Col: 49} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var109)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 218, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var109)) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 219, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 219, "
  • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 220, "
    connected ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var111 string + templ_7745c5c3_Var111, 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: 705, Col: 83} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var111)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 221, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 221, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 223, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if !hasYouTube(conns) { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 222, "

Connect YouTube

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

Connect YouTube

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

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, 225, "

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 } return nil }) - templ_7745c5c3_Err = Layout("Tapir — Account").Render(templ.WithChildren(ctx, templ_7745c5c3_Var100), templ_7745c5c3_Buffer) + templ_7745c5c3_Err = Layout("Tapir — Account").Render(templ.WithChildren(ctx, templ_7745c5c3_Var102), templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -2681,51 +2709,51 @@ func summarizeModeControl(auto bool) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var111 := templ.GetChildren(ctx) - if templ_7745c5c3_Var111 == nil { - templ_7745c5c3_Var111 = templ.NopComponent + templ_7745c5c3_Var113 := templ.GetChildren(ctx) + if templ_7745c5c3_Var113 == nil { + templ_7745c5c3_Var113 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 224, "

Current mode: ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var112 string - templ_7745c5c3_Var112, 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: 743, Col: 53} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var112)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 225, "

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

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -2753,38 +2781,38 @@ func ActionButtons(videoID string, active map[string]bool) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var115 := templ.GetChildren(ctx) - if templ_7745c5c3_Var115 == nil { - templ_7745c5c3_Var115 = templ.NopComponent + templ_7745c5c3_Var117 := templ.GetChildren(ctx) + if templ_7745c5c3_Var117 == nil { + templ_7745c5c3_Var117 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 228, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 232, "\" hx-target=\"#action-buttons\" hx-swap=\"outerHTML\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -2796,7 +2824,7 @@ 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, 231, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 233, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -2804,7 +2832,7 @@ 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, 232, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 234, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -2830,81 +2858,81 @@ func actionButton(verb string, isActive bool) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var118 := templ.GetChildren(ctx) - if templ_7745c5c3_Var118 == nil { - templ_7745c5c3_Var118 = templ.NopComponent + templ_7745c5c3_Var120 := templ.GetChildren(ctx) + if templ_7745c5c3_Var120 == nil { + templ_7745c5c3_Var120 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - var templ_7745c5c3_Var119 = []any{"action", templ.KV("active", isActive)} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var119...) + templ_7745c5c3_Var121 := []any{"action", templ.KV("active", isActive)} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var121...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 233, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 239, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err }