diff --git a/internal/web/account.go b/internal/web/account.go index 1a6c792..e099523 100644 --- a/internal/web/account.go +++ b/internal/web/account.go @@ -28,7 +28,12 @@ func (a *App) handleAccount(w http.ResponseWriter, r *http.Request) { if u, ok := a.Auth.CurrentUser(r); ok { email = u.Email } - a.render(w, r, AccountPage(name, email, conns, takeFlash(w, r))) + auto, err := a.Store.GetAutoSummarize(r.Context(), userID) + if err != nil { + a.serverError(w, r, "summarize mode", err) + return + } + a.render(w, r, AccountPage(name, email, conns, auto, takeFlash(w, r))) } // handleDisconnect removes a provider connection: it deletes the OAuth token from diff --git a/internal/web/handlers.go b/internal/web/handlers.go index 450330c..41e8908 100644 --- a/internal/web/handlers.go +++ b/internal/web/handlers.go @@ -16,12 +16,19 @@ import ( // the concrete *store.Store). *store.Store satisfies it; tests can substitute a // fake without a database. type Store interface { - ListSummaries(ctx context.Context, userID string, limit int) ([]store.SummaryRow, error) + ListVideos(ctx context.Context, userID string, limit int) ([]store.SummaryRow, error) GetSummaryByVideo(ctx context.Context, userID, videoID string) (*store.SummaryRow, error) + GetVideoRow(ctx context.Context, userID, videoID string) (*store.SummaryRow, error) ActionsFor(ctx context.Context, userID string, videoIDs []string) (map[string][]string, error) SetAction(ctx context.Context, userID, videoID, action string) error ClearAction(ctx context.Context, userID, videoID, action string) error + // Summarization mode: the per-user auto/manual toggle and the per-video + // manual queue (the "Summarize" button). The runner consumes the queue. + GetAutoSummarize(ctx context.Context, userID string) (bool, error) + SetAutoSummarize(ctx context.Context, userID string, enabled bool) error + RequestSummarize(ctx context.Context, userID, videoID string) error + // Account management (the /account page, disconnect, delete-account). ConnectionsForUser(ctx context.Context, userID string) ([]store.Connection, error) DeleteConnection(ctx context.Context, userID, provider string) error @@ -76,6 +83,7 @@ func (a *App) Router() http.Handler { app.HandleFunc("GET /{$}", a.handleList) app.HandleFunc("GET /v/{videoId}", a.handleDetail) app.HandleFunc("POST /v/{videoId}/action", a.handleAction) + app.HandleFunc("POST /v/{videoId}/summarize", a.handleRequestSummarize) app.HandleFunc("GET /register", a.handleRegisterForm) app.HandleFunc("POST /register", a.handleRegister) @@ -84,6 +92,7 @@ func (a *App) Router() http.Handler { app.HandleFunc("GET /account", a.handleAccount) app.HandleFunc("POST /account/disconnect/{provider}", a.handleDisconnect) app.HandleFunc("POST /account/delete", a.handleDeleteAccount) + app.HandleFunc("POST /account/summarize-mode", a.handleSummarizeMode) // Web-initiated YouTube connect (ADR-006). Gated like every app route, so // CurrentUserID is set and the connection binds to the authenticated user. @@ -121,9 +130,9 @@ func (a *App) handleList(w http.ResponseWriter, r *http.Request) { To: q.Get("to"), } - rows, err := a.Store.ListSummaries(r.Context(), userID, 0) + rows, err := a.Store.ListVideos(r.Context(), userID, 0) if err != nil { - a.serverError(w, r, "list summaries", err) + a.serverError(w, r, "list videos", err) return } rows = f.apply(rows) @@ -199,6 +208,59 @@ func (a *App) handleAction(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/v/"+videoID, http.StatusSeeOther) } +// handleRequestSummarize queues a video for manual summarization. It does NOT run +// the engine inline — it only flips summarize_requested; the next `tapir run` +// picks it up (the single summarization driver). For HTMX it returns the refreshed +// card (now showing "Queued"); without JS it redirects back to the list. +func (a *App) handleRequestSummarize(w http.ResponseWriter, r *http.Request) { + userID, ok := a.currentUserID(w, r) + if !ok { + return + } + videoID := r.PathValue("videoId") + + err := a.Store.RequestSummarize(r.Context(), userID, videoID) + if errors.Is(err, store.ErrNotFound) { + http.NotFound(w, r) + return + } + if err != nil { + a.serverError(w, r, "request summarize", err) + return + } + + if !isHTMX(r) { + http.Redirect(w, r, "/", http.StatusSeeOther) + return + } + row, err := a.Store.GetVideoRow(r.Context(), userID, videoID) + if err != nil { + a.serverError(w, r, "get video", err) + return + } + a.render(w, r, VideoCard(*row)) +} + +// handleSummarizeMode toggles the user's auto/manual summarization mode. The form +// submits the desired new value (enabled=true|false). For HTMX it returns the +// refreshed mode control; without JS it redirects back to the account page. +func (a *App) handleSummarizeMode(w http.ResponseWriter, r *http.Request) { + userID, ok := a.currentUserID(w, r) + if !ok { + return + } + enabled := r.FormValue("enabled") == "true" + if err := a.Store.SetAutoSummarize(r.Context(), userID, enabled); err != nil { + a.serverError(w, r, "set summarize mode", err) + return + } + if !isHTMX(r) { + http.Redirect(w, r, "/account", http.StatusSeeOther) + return + } + a.render(w, r, summarizeModeControl(enabled)) +} + // currentUserID returns the tapir user_id the registration gate resolved for this // request. Behind the gate it is always present; a miss means a handler was // reached without scoping (a wiring bug), so it answers 500 and reports false. diff --git a/internal/web/handlers_test.go b/internal/web/handlers_test.go index 211e3dc..710b1b6 100644 --- a/internal/web/handlers_test.go +++ b/internal/web/handlers_test.go @@ -185,8 +185,10 @@ func TestListRendersRowsAndActionState(t *testing.T) { func TestListHTMXReturnsFragment(t *testing.T) { ctx := context.Background() app := newApp(t) - resetDB(t, rawPool(t)) + p := rawPool(t) + resetDB(t, p) require.NoError(t, deliver(ctx, app, videoX, "body x")) + seedVideo(t, p, videoX, "X Title", "https://x", time.Time{}) req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set("HX-Request", "true") @@ -290,6 +292,103 @@ func TestActionRejectsUnknownVerb(t *testing.T) { require.Equal(t, http.StatusBadRequest, rec.Code) } +func TestListShowsSummarizeButtonForUnsummarized(t *testing.T) { + app := newApp(t) + p := rawPool(t) + resetDB(t, p) + // A discovered-but-unsummarized video (no summary delivered). + seedVideo(t, p, videoX, "Pending Title", "https://x", time.Time{}) + + rec := do(t, app, httptest.NewRequest(http.MethodGet, "/", nil)) + require.Equal(t, http.StatusOK, rec.Code) + html := body(t, rec) + + require.Contains(t, html, "Pending Title", "unsummarized videos are listed too") + require.Contains(t, html, "Summarize", "a Summarize button is offered") + require.Contains(t, html, "/v/"+videoX+"/summarize", "button posts to the queue endpoint") + require.Contains(t, html, "card-pending", "muted pending treatment") + require.NotContains(t, html, "Queued", "not queued yet") +} + +func TestRequestSummarizeQueuesAndRendersCard(t *testing.T) { + ctx := context.Background() + app := newApp(t) + p := rawPool(t) + resetDB(t, p) + seedVideo(t, p, videoX, "Pending Title", "https://x", time.Time{}) + + rec := postSummarize(t, app, videoX, true) + require.Equal(t, http.StatusOK, rec.Code) + html := body(t, rec) + require.Contains(t, html, "Queued", "card now shows the queued state") + require.NotContains(t, html, ">Summarize<", "the Summarize button is gone once queued") + + // The flag is persisted, so the next run picks it up. + row, err := app.Store.GetVideoRow(ctx, userID, videoX) + require.NoError(t, err) + require.True(t, row.SummarizeRequested) +} + +func TestRequestSummarizeNonHTMXRedirects(t *testing.T) { + ctx := context.Background() + app := newApp(t) + p := rawPool(t) + resetDB(t, p) + seedVideo(t, p, videoX, "Pending Title", "https://x", time.Time{}) + + rec := postSummarize(t, app, videoX, false) + require.Equal(t, http.StatusSeeOther, rec.Code) + require.Equal(t, "/", rec.Header().Get("Location")) + + row, err := app.Store.GetVideoRow(ctx, userID, videoX) + require.NoError(t, err) + require.True(t, row.SummarizeRequested, "queued on the no-JS path too") +} + +func TestRequestSummarizeNotFound(t *testing.T) { + app := newApp(t) + resetDB(t, rawPool(t)) + rec := postSummarize(t, app, videoX, true) + require.Equal(t, http.StatusNotFound, rec.Code, "queuing an unknown video is a 404") +} + +func TestSummarizeModeToggle(t *testing.T) { + ctx := context.Background() + app := newApp(t) + resetDB(t, rawPool(t)) + + // Account page defaults to manual. + rec := do(t, app, httptest.NewRequest(http.MethodGet, "/account", nil)) + require.Equal(t, http.StatusOK, rec.Code) + html := body(t, rec) + require.Contains(t, html, "Manual", "default mode shown") + require.Contains(t, html, "Switch to automatic") + + // Toggle to automatic via HTMX returns the refreshed control. + req := httptest.NewRequest(http.MethodPost, "/account/summarize-mode", + strings.NewReader("enabled=true")) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("HX-Request", "true") + rec = do(t, app, req) + require.Equal(t, http.StatusOK, rec.Code) + html = body(t, rec) + require.Contains(t, html, "Automatic") + require.Contains(t, html, "Switch to manual") + + got, err := app.Store.GetAutoSummarize(ctx, userID) + require.NoError(t, err) + require.True(t, got, "mode persisted") +} + +func postSummarize(t *testing.T, app *web.App, videoID string, htmx bool) *httptest.ResponseRecorder { + t.Helper() + req := httptest.NewRequest(http.MethodPost, "/v/"+videoID+"/summarize", nil) + if htmx { + req.Header.Set("HX-Request", "true") + } + return do(t, app, req) +} + // deliver stores a summary through the App's store under test. func deliver(ctx context.Context, app *web.App, videoID, text string) error { return app.Store.(*store.Store).Deliver(ctx, summary(videoID, text)) diff --git a/internal/web/view.go b/internal/web/view.go index 9e703e1..1314561 100644 --- a/internal/web/view.go +++ b/internal/web/view.go @@ -171,6 +171,36 @@ func actionURL(videoID string) templ.SafeURL { return templ.SafeURL("/v/" + videoID + "/action") } +// summarizeURL builds the manual-queue POST path for a video id. +func summarizeURL(videoID string) templ.SafeURL { + return templ.SafeURL("/v/" + videoID + "/summarize") +} + +// summarizeModeLabel names the current mode for display. +func summarizeModeLabel(auto bool) string { + if auto { + return "Automatic" + } + return "Manual" +} + +// summarizeModeToggleLabel is the caption on the toggle button — it names the mode +// the click switches TO (the opposite of the current one). +func summarizeModeToggleLabel(auto bool) string { + if auto { + return "Switch to manual" + } + return "Switch to automatic" +} + +// boolStr renders a bool as the "enabled" form value the toggle submits. +func boolStr(b bool) string { + if b { + return "true" + } + return "false" +} + // externalURL passes a stored source URL through templ's URL sanitiser. func externalURL(u string) templ.SafeURL { return templ.URL(u) @@ -347,6 +377,15 @@ main { max-width: 60rem; margin: 0 auto; padding: var(--s4) var(--s3); } .card-state { color: var(--muted); font-size: .8rem; } .badge { display: inline-block; padding: .15rem .55rem; border-radius: 999px; background: var(--badge-bg); color: var(--badge-fg); font-size: .72rem; font-weight: 600; } +/* pending (discovered-but-unsummarized) card: muted until summarized */ +.card-pending { border-style: dashed; } +.card-pending .card-title { color: var(--muted); font-weight: 600; } + +/* 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; } +.summarize-mode form { margin: 0; } + /* empty state */ .empty { text-align: center; color: var(--muted); padding: var(--s5) var(--s4); border: 1px dashed var(--line); border-radius: var(--radius); background: var(--card); } .empty strong { display: block; color: var(--fg); font-size: 1.05rem; margin-bottom: var(--s2); } diff --git a/internal/web/views.templ b/internal/web/views.templ index 5198730..493e6e5 100644 --- a/internal/web/views.templ +++ b/internal/web/views.templ @@ -73,44 +73,73 @@ templ filterForm(f Filter) { } -// summaryList is the swappable list fragment: one card per summary (title link, -// channel · date meta, provider chip, fallback badge, action state). Cards -// reflow to a single column on mobile; an empty list shows a friendly first-run -// state instead of a blank table. +// summaryList is the swappable list fragment: one card per video (summarized or +// not). Cards reflow to a single column on mobile; an empty list shows a friendly +// first-run state instead of a blank table. templ summaryList(rows []store.SummaryRow) { if len(rows) == 0 {
- No summaries yet - Summaries appear here as your subscriptions are processed — run tapir run to fetch and summarize new videos. + No videos yet + Videos appear here as your subscriptions are processed — run tapir run to fetch them. In manual mode, use the Summarize button to queue one.
} else { } } +// VideoCard is one list card, also returned standalone by POST /v/{id}/summarize +// (HTMX swaps it in place via outerHTML). A summarized video links to its detail +// page and shows its provider chip / fallback badge / action state. An +// unsummarized video gets a muted "pending" treatment and either a "Summarize" +// button (to queue it) or a "Queued" chip when already requested. +templ VideoCard(r store.SummaryRow) { +
  • + if r.Summarized { +
    { displayTitle(r) }
    + } else { +
    { displayTitle(r) }
    + } + if cardMeta(r) != "" { +
    { cardMeta(r) }
    + } + if r.Summarized { + if p := previewText(r.Summary, 160); p != "" { +
    { p }
    + } + } +
    + if r.Summarized { + if r.AIProvider != "" { + { r.AIProvider } + } + if r.FallbackUsed { + fallback + } + if len(r.Actions) > 0 { + { strings.Join(r.Actions, ", ") } + } + } else if r.SummarizeRequested { + Queued + waiting for the next run + } else { +
    + +
    + } +
    +
  • +} + // DetailPage is the full summary view: text, highlights, takeaways, metadata, // and the action button group. templ DetailPage(r store.SummaryRow) { @@ -202,7 +231,7 @@ templ RegisterPage(email, errMsg string) { // signed-in email, the user's connected video accounts (each with a Disconnect // control), a Connect-YouTube link when none is connected, and the delete-account // danger zone. flash surfaces a one-shot notification (disconnect/connect). -templ AccountPage(displayName, email string, conns []store.Connection, flash string) { +templ AccountPage(displayName, email string, conns []store.Connection, autoSummarize bool, flash string) { @Layout("Tapir — Account") { @flashBanner(flash)
    @@ -215,6 +244,15 @@ templ AccountPage(displayName, email string, conns []store.Connection, flash str
    { email }
    } +
    +

    Summarization

    +

    + Automatic summarizes every new video as it is discovered. Manual lets you + pick which videos to summarize — new videos appear in your list with a + Summarize button. +

    + @summarizeModeControl(autoSummarize) +

    Connected accounts

    if len(conns) == 0 { @@ -262,6 +300,26 @@ templ AccountPage(displayName, email string, conns []store.Connection, flash str } } +// summarizeModeControl is the auto/manual toggle, also returned standalone by +// POST /account/summarize-mode (HTMX swaps it via outerHTML). The hidden field +// submits the desired NEW value, so a single submit flips the mode; without JS the +// form posts and the handler redirects back to /account. +templ summarizeModeControl(auto bool) { +
    +

    Current mode: { summarizeModeLabel(auto) }

    +
    + + +
    +
    +} + // ActionButtons is the toggle group fragment returned by POST /v/{id}/action. // Each button submits its verb; HTMX swaps this element in place (outerHTML), // and without JS the form POSTs and the handler redirects back to the detail diff --git a/internal/web/views_templ.go b/internal/web/views_templ.go index 8ea23b9..fe4481c 100644 --- a/internal/web/views_templ.go +++ b/internal/web/views_templ.go @@ -280,10 +280,9 @@ func filterForm(f Filter) templ.Component { }) } -// summaryList is the swappable list fragment: one card per summary (title link, -// channel · date meta, provider chip, fallback badge, action state). Cards -// reflow to a single column on mobile; an empty list shows a friendly first-run -// state instead of a blank table. +// summaryList is the swappable list fragment: one card per video (summarized or +// not). Cards reflow to a single column on mobile; an empty list shows a friendly +// first-run state instead of a blank table. func summaryList(rows []store.SummaryRow) 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 @@ -306,7 +305,7 @@ func summaryList(rows []store.SummaryRow) templ.Component { } ctx = templ.ClearChildren(ctx) if len(rows) == 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
    No summaries yet Summaries appear here as your subscriptions are processed — run tapir run to fetch and summarize new videos.
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
    No videos yet Videos appear here as your subscriptions are processed — run tapir run to fetch them. In manual mode, use the Summarize button to queue one.
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -316,128 +315,12 @@ func summaryList(rows []store.SummaryRow) templ.Component { return templ_7745c5c3_Err } for _, r := range rows { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
  • ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if cardMeta(r) != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
    ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var16 string - templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(cardMeta(r)) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 92, Col: 42} - } - _, 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, 21, "
    ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - if p := previewText(r.Summary, 160); p != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "
    ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var17 string - templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(p) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 95, Col: 35} - } - _, 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, 23, "
    ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "
    ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if r.AIProvider != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var18 string - templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(r.AIProvider) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 99, Col: 40} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, " ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - if r.FallbackUsed { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "fallback ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - if len(r.Actions) > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var19 string - templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(r.Actions, ", ")) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 105, Col: 63} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "
  • ") + templ_7745c5c3_Err = VideoCard(r).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -446,6 +329,272 @@ func summaryList(rows []store.SummaryRow) templ.Component { }) } +// VideoCard is one list card, also returned standalone by POST /v/{id}/summarize +// (HTMX swaps it in place via outerHTML). A summarized video links to its detail +// page and shows its provider chip / fallback badge / action state. An +// unsummarized video gets a muted "pending" treatment and either a "Summarize" +// button (to queue it) or a "Queued" chip when already requested. +func VideoCard(r store.SummaryRow) 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_Var14 := templ.GetChildren(ctx) + if templ_7745c5c3_Var14 == nil { + templ_7745c5c3_Var14 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + var templ_7745c5c3_Var15 = []any{"card", templ.KV("card-pending", !r.Summarized)} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var15...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
  • ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if r.Summarized { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var20 string + templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(displayTitle(r)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 104, Col: 44} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if cardMeta(r) != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var21 string + templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(cardMeta(r)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 107, Col: 39} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if r.Summarized { + if p := previewText(r.Summary, 160); p != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var22 string + templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(p) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 111, Col: 33} + } + _, 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, 29, "
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if r.Summarized { + if r.AIProvider != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var23 string + templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(r.AIProvider) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 117, Col: 38} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if r.FallbackUsed { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "fallback") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if len(r.Actions) > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var24 string + templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(r.Actions, ", ")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 123, Col: 61} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + } else if r.SummarizeRequested { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "Queued waiting for the next run") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "
  • ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + // DetailPage is the full summary view: text, highlights, takeaways, metadata, // and the action button group. func DetailPage(r store.SummaryRow) templ.Component { @@ -464,12 +613,12 @@ func DetailPage(r store.SummaryRow) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var20 := templ.GetChildren(ctx) - if templ_7745c5c3_Var20 == nil { - templ_7745c5c3_Var20 = templ.NopComponent + templ_7745c5c3_Var28 := templ.GetChildren(ctx) + if templ_7745c5c3_Var28 == nil { + templ_7745c5c3_Var28 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Var21 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_Var29 := 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 { @@ -481,99 +630,99 @@ func DetailPage(r store.SummaryRow) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "

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

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var22 string - templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(displayTitle(r)) + var templ_7745c5c3_Var30 string + templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(displayTitle(r)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 119, Col: 24} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 148, Col: 24} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "

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

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if detailMeta(r) != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var23 string - templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(detailMeta(r)) + var templ_7745c5c3_Var31 string + templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(detailMeta(r)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 122, Col: 26} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 151, Col: 26} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if r.FallbackUsed { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "fallback") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "fallback") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "

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

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if url, ok := embedURL(r.ProviderVideoID); ok { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "\" loading=\"lazy\" referrerpolicy=\"strict-origin-when-cross-origin\" allow=\"accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" allowfullscreen>") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if r.URL != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "

    watch on source ↗

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "\" rel=\"noopener noreferrer\">watch on source ↗

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -582,88 +731,88 @@ func DetailPage(r store.SummaryRow) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "

    Summary

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

    Summary

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var27 string - templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(r.Summary) + var templ_7745c5c3_Var35 string + templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(r.Summary) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 146, Col: 31} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 175, Col: 31} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "

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

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if len(r.Highlights) > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "

    Highlights

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

      Highlights

        ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for _, h := range r.Highlights { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "
      • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "
      • ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var28 string - templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(h) + var templ_7745c5c3_Var36 string + templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(h) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 153, Col: 14} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 182, Col: 14} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "
      • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "
      ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if len(r.Takeaways) > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "

    Takeaways

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

      Takeaways

        ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for _, t := range r.Takeaways { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "
      • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "
      • ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var29 string - templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(t) + var templ_7745c5c3_Var37 string + templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(t) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 163, Col: 14} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 192, Col: 14} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "
      • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "
      ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } return nil }) - templ_7745c5c3_Err = Layout("Tapir — "+displayTitle(r)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var21), templ_7745c5c3_Buffer) + templ_7745c5c3_Err = Layout("Tapir — "+displayTitle(r)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var29), templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -690,12 +839,12 @@ func RegisterPage(email, errMsg string) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var30 := templ.GetChildren(ctx) - if templ_7745c5c3_Var30 == nil { - templ_7745c5c3_Var30 = templ.NopComponent + templ_7745c5c3_Var38 := templ.GetChildren(ctx) + if templ_7745c5c3_Var38 == nil { + templ_7745c5c3_Var38 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Var31 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_Var39 := 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 { @@ -707,59 +856,59 @@ func RegisterPage(email, errMsg string) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "

    Complete your registration

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

    Complete your registration

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

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

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

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

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

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

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

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

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

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

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

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } return nil }) - templ_7745c5c3_Err = Layout("Tapir — Register").Render(templ.WithChildren(ctx, templ_7745c5c3_Var31), templ_7745c5c3_Buffer) + templ_7745c5c3_Err = Layout("Tapir — Register").Render(templ.WithChildren(ctx, templ_7745c5c3_Var39), templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -771,7 +920,7 @@ func RegisterPage(email, errMsg string) templ.Component { // signed-in email, the user's connected video accounts (each with a Disconnect // control), a Connect-YouTube link when none is connected, and the delete-account // danger zone. flash surfaces a one-shot notification (disconnect/connect). -func AccountPage(displayName, email string, conns []store.Connection, flash string) templ.Component { +func AccountPage(displayName, email string, conns []store.Connection, autoSummarize bool, flash string) 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 { @@ -787,12 +936,12 @@ func AccountPage(displayName, email string, conns []store.Connection, flash stri }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var34 := templ.GetChildren(ctx) - if templ_7745c5c3_Var34 == nil { - templ_7745c5c3_Var34 = templ.NopComponent + templ_7745c5c3_Var42 := templ.GetChildren(ctx) + if templ_7745c5c3_Var42 == nil { + templ_7745c5c3_Var42 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Var35 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_Var43 := 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 { @@ -808,155 +957,235 @@ func AccountPage(displayName, email string, conns []store.Connection, flash stri if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "

    Account

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

    Account

    Display name
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var36 string - templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(displayNameOr(displayName)) + var templ_7745c5c3_Var44 string + templ_7745c5c3_Var44, 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: 212, Col: 36} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 241, Col: 36} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if email != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "
    Signed in as
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "
    Signed in as
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var37 string - templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(email) + var templ_7745c5c3_Var45 string + templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(email) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 215, Col: 16} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 244, Col: 16} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "

    Connected accounts

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

    Summarization

    Automatic summarizes every new video as it is discovered. Manual lets you pick which videos to summarize — new videos appear in your list with a Summarize button.

    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = summarizeModeControl(autoSummarize).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "

    Connected accounts

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

    No connected video accounts yet.

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

    No connected video accounts yet.

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "
      ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "
        ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for _, c := range conns { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "
      • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "
      • ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var38 string - templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(providerLabel(c.Provider)) + var templ_7745c5c3_Var46 string + templ_7745c5c3_Var46, 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: 227, Col: 64} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 265, Col: 64} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if c.ProviderAccount != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var39 string - templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(c.ProviderAccount) + var templ_7745c5c3_Var47 string + templ_7745c5c3_Var47, 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: 229, Col: 49} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 267, Col: 49} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var40 string - templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(c.Status) + var templ_7745c5c3_Var48 string + templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(c.Status) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 231, Col: 38} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 269, Col: 38} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "
        connected ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "
        connected ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var41 string - templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(c.ConnectedAt.Format("2006-01-02")) + var templ_7745c5c3_Var49 string + templ_7745c5c3_Var49, 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: 233, Col: 83} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 271, Col: 83} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "
      • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "
      ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if !hasYouTube(conns) { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "

    Connect YouTube

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

    Connect YouTube

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

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

    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_Var35), templ_7745c5c3_Buffer) + templ_7745c5c3_Err = Layout("Tapir — Account").Render(templ.WithChildren(ctx, templ_7745c5c3_Var43), templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// summarizeModeControl is the auto/manual toggle, also returned standalone by +// POST /account/summarize-mode (HTMX swaps it via outerHTML). The hidden field +// submits the desired NEW value, so a single submit flips the mode; without JS the +// form posts and the handler redirects back to /account. +func summarizeModeControl(auto 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 { + 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_Var51 := templ.GetChildren(ctx) + if templ_7745c5c3_Var51 == nil { + templ_7745c5c3_Var51 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "

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

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -984,117 +1213,117 @@ func ActionButtons(videoID string, active map[string]bool) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var43 := templ.GetChildren(ctx) - if templ_7745c5c3_Var43 == nil { - templ_7745c5c3_Var43 = templ.NopComponent + templ_7745c5c3_Var55 := templ.GetChildren(ctx) + if templ_7745c5c3_Var55 == nil { + templ_7745c5c3_Var55 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "\" hx-target=\"#action-buttons\" hx-swap=\"outerHTML\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for _, v := range actionVerbs { - var templ_7745c5c3_Var46 = []any{"action", templ.KV("active", active[v])} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var46...) + var templ_7745c5c3_Var58 = []any{"action", templ.KV("active", active[v])} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var58...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err }