feat(web): summarization-mode UI — all-videos list, Summarize queue, mode toggle
The list now shows ALL videos (ListVideos), not just summaries. Summarized cards
are unchanged; discovered-but-unsummarized videos render with a muted "pending"
treatment and either a "Summarize" button or a "Queued" chip.
- POST /v/{videoId}/summarize queues a video (RequestSummarize) and returns the
refreshed card — it does NOT run the engine inline; `tapir run` is the single
summarization driver, which picks up the flag on its next pass.
- Account page gains an Automatic/Manual toggle (POST /account/summarize-mode →
SetAutoSummarize), shown as the current mode with a one-click switch.
- Both new POSTs degrade without JS (redirect back); HTMX swaps the fragment.
VideoCard and summarizeModeControl are extracted templ fragments reused as the
HTMX swap targets. views_templ.go regenerated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user