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:
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user