feat(spec-detail): π top-left, π bottom-left on the plot, one vote per image - #11827
Conversation
β¦r image Rating an implementation used to be two clicks away behind the floating feedback FAB and landed on the page, not on the library. The spec detail view now carries a thumbs-up button top-left and a thumbs-down button bottom-left over the image (static preview and interactive surface), in place of the little-used `.report()` flag β reporting stays on the hub page link and in the footer. - One tap inks the thumb (brand green / matte red), flashes `>>> .liked` / `>>> .disliked` like `.copied`, and posts a reaction-only entry to /feedback naming the implementation. - `feedback.library_id` + `feedback.language` (Alembic a7c3e9d1f5b8, with a (spec_id, library_id) index) make votes countable per image; `FeedbackRepository.reaction_counts` does the counting (newest row per session) β nothing is displayed yet. - One vote per image per session: the choice is kept in localStorage and the other thumb fades out; the server drops a repeat vote from the same session for the same implementation silently. - Reaction-only entries get their own rate limit (30/min) so flipping through the carousel neither trips the free-text limit (5/min, now counted over message rows only) nor blocks a message written after. - Shared feedback helpers (session id, spec id from path) move out of FeedbackWidget into utils/feedback.ts and routes/paths.ts. Supersedes the stale #8126 (both thumbs top-left, no per-library field). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Eckdg6kWrjasZMwNn8A1v8
Codecov Reportβ All modified and coverable lines are covered by tests. π’ Thoughts on this report? Let us know! |
There was a problem hiding this comment.
π‘ Changes recommended
There are correctness/accessibility issues to address (nondeterministic vote counting ordering and disabled-button semantics, plus a test contract mismatch around tz-naive timestamps).
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR updates the spec detail experience to support one-tap π/π voting directly on the plot (static and interactive views), persists βone vote per image per sessionβ in localStorage + backend, and extends the feedback pipeline (API, DB, tests, and docs) to store/count implementation-scoped reactions and apply separate rate limits for message vs reaction-only submissions.
Changes:
- Add plot-overlay π/π UI in
SpecDetailView, persist votes per implementation, and submit reaction-only feedback withlibrary_id+language. - Extend backend feedback schema/router + repository with plot-vote guard, reaction-only rate limit, and per-implementation reaction counting.
- Add migration + model fields for
feedback.library_id/feedback.language, plus tests and documentation/changelog updates.
File summaries
| File | Description |
|---|---|
| tests/unit/api/test_feedback_router.py | Adds unit coverage for plot vote guard, persisted impl context, and reaction-only rate limiting behavior. |
| tests/integration/test_repositories.py | Adds integration coverage for message-only rate limiting, plot-vote detection, and per-implementation reaction counts. |
| tests/integration/api/test_api_endpoints.py | Extends integration tests to assert persisted library_id/language and one-vote-per-session semantics. |
| docs/reference/plausible.md | Documents updated analytics semantics for feedback_submitted and report_issue. |
| docs/contributing.md | Updates contributor guidance for where βreport issueβ now lives in the UI. |
| core/database/repositories.py | Implements messages_only rate-limit counting, plot-vote guard lookup, and per-implementation reaction counting. |
| core/database/models.py | Adds nullable library_id/language fields and the (spec_id, library_id) index definition. |
| changelog.d/thumbs-on-plot.md | Adds changelog fragment describing the new plot-overlay voting and backend seams. |
| app/src/utils/feedback.ts | Extracts shared feedback/session helpers and vote keying for per-implementation persistence. |
| app/src/utils/feedback.test.ts | Adds unit tests for the new feedback utilities. |
| app/src/sections/spec-detail/SpecDetailView.tsx | Renders thumbs overlay buttons (π top-left, π bottom-left), persists vote state, and posts reactions via hook. |
| app/src/sections/spec-detail/SpecDetailView.test.tsx | Adds tests for overlay button rendering, one-vote lock, optimistic UI, rollback on failure, and persistence across remounts. |
| app/src/routes/paths.ts | Adds specIdFromPath() for shared route parsing. |
| app/src/routes/paths.test.ts | Adds tests for specIdFromPath(). |
| app/src/pages/SpecPage.tsx | Wires specId into SpecDetailView and removes the in-plot report action wiring. |
| app/src/hooks/useQuickReaction.ts | New hook for submitting plot-overlay reaction-only feedback with impl context and analytics. |
| app/src/components/FeedbackWidget.tsx | Switches to shared specIdFromPath and shared session-id helpers. |
| api/schemas.py | Extends FeedbackRequest with optional library_id/language. |
| api/routers/feedback.py | Implements message-vs-reaction rate limits and one-vote-per-image-per-session drop logic; persists impl context. |
| alembic/versions/a7c3e9d1f5b8_feedback_library_and_language.py | Adds nullable DB columns and (spec_id, library_id) index via Alembic migration. |
Review details
- Files reviewed: 20/20 changed files
- Comments generated: 3
- Review effort level: Lite
π‘ Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
β¦ote ranking - The locked thumb is now natively `disabled` (keyboard and assistive tech get the right semantics) with the tooltip on a wrapping span, as MUI documents for disabled buttons; the overlay surface is kept under the faded button. - `reaction_counts` ranks by `(created_at DESC, id DESC)` so two rows sharing a created_at resolve the same way on every run. - The `messages_only` repository test passes a tz-naive UTC cutoff, as the repository contract asks. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Eckdg6kWrjasZMwNn8A1v8
Summary
.report()flag (reporting stays on the hub page link and in the footer). One tap inks the thumb (brand green / matte red), flashes>>> .liked/>>> .dislikedin the centre like.copied, and posts a reaction-only entry to/feedbacknaming the implementation.a7c3e9d1f5b8addsfeedback.library_id+feedback.languageand a(spec_id, library_id)index;FeedbackRepository.reaction_counts(spec_id, library_id, language?)returns{thumbs_up, thumbs_down}counting the newest row per session (ordered bycreated_at, thenid, so ties resolve the same way every run). Nothing is displayed yet β this is the seam for a later "show counts" step.anyplot_plot_votes) so the thumb stays inked across reloads and carousel switches, the other thumb is nativelydisabled(faded, tooltip.rated()on a wrapping span), and the server silently drops a repeat π/π from the same session for the same implementation (FeedbackRepository.has_plot_vote). A vote is final; nobody can flip up and down at will.FEEDBACK_SESSION_KEY,newFeedbackSessionId,specIdFromPath) move out ofFeedbackWidgetintoutils/feedback.tsandroutes/paths.ts; the widget keeps its behaviour (page-level reactions carry nolibrary_id, so they never count as plot votes).Supersedes #8126 (both thumbs top-left, no per-library field, three months behind
mainwith the files since moved). Placement per owner decision: π top-left, π bottom-left.Test plan
/verify-core:uv run pytest tests/unit tests/integrationβ 2004 passed, 1 skipped (pre-existing MonoLisa italic skip);ruff check+ruff format --check+mypy api coreclean. New tests: repositoryhas_plot_vote,reaction_counts(newest-per-session, language filter, session-less rows),count_recent_by_ip(messages_only=); router unit tests for the vote guard, the looser reaction limit and the page-level bypass; integration tests for persistedlibrary_id/languageand one-vote-per-session./verify-migrations: throwaway Postgres 16 (pgserver) βalembic headssingle heada7c3e9d1f5b8,upgrade head,alembic check"No new upgrade operations detected.",downgrade -1+upgrade headroundtrip clean./verify-frontend:yarn type-check,yarn lint(0 warnings), Prettier clean,yarn vitest runβ 71 files / 640 tests passed. Drove/scatter-basic/python/matplotlibin headless Chromium at 1280px and 390px, light and dark: both thumbs render, tap π β inked + toast + exactly one POST (spec_id,library_id,language,session_id), π locked, vote survives reload. The POST was stubbed locally; no row was written to the shared production DB.uv run python -m tools.changelog check --base origin/mainpasses.disabled, stable ranking tiebreaker, tz-naive test cutoff) applied in the second commit; threads resolved.Checklist
changelog.d/thumbs-on-plot.mdadded (Added / Changed / Removed), no new bullet inCHANGELOG.mddocs/reference/plausible.md(feedback_submittedmodeplot_overlay+libraryprop,report_issuesource),docs/contributing.md(where "report issue" now lives)Deploy note: the migration only adds nullable columns and an index;
sync-postgres.ymlrunsalembic upgrade headafter merge. The frontend sendslibrary_id/languageregardless; if the API deploys first, the extra fields are still accepted (they are optional inFeedbackRequest).π€ Generated with Claude Code
https://claude.ai/code/session_01Eckdg6kWrjasZMwNn8A1v8