Skip to content

feat(spec-detail): πŸ‘ top-left, πŸ‘Ž bottom-left on the plot, one vote per image - #11827

Merged
MarkusNeusinger merged 2 commits into
mainfrom
claude/thumbs-rating-image-placement-x4tws7
Sep 10, 2026
Merged

feat(spec-detail): πŸ‘ top-left, πŸ‘Ž bottom-left on the plot, one vote per image#11827
MarkusNeusinger merged 2 commits into
mainfrom
claude/thumbs-rating-image-placement-x4tws7

Conversation

@MarkusNeusinger

@MarkusNeusinger MarkusNeusinger commented Sep 10, 2026

Copy link
Copy Markdown
Owner

Summary

  • πŸ‘ / πŸ‘Ž directly on the plot. The spec detail view now renders a thumbs-up button top-left and a thumbs-down button bottom-left over the image, on both the static preview and the interactive surface, in place of the .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 in the centre like .copied, and posts a reaction-only entry to /feedback naming the implementation.
  • Votes are countable per image. New Alembic revision a7c3e9d1f5b8 adds feedback.library_id + feedback.language and 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 by created_at, then id, so ties resolve the same way every run). Nothing is displayed yet β€” this is the seam for a later "show counts" step.
  • One vote per image per session. The choice is kept in localStorage (anyplot_plot_votes) so the thumb stays inked across reloads and carousel switches, the other thumb is natively disabled (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.
  • Reaction-only rate limit. Free text stays at 5/min per IP, now counted over message-bearing rows only; a πŸ‘/πŸ‘Ž tap is capped at 30/min, so rating several plots in a row neither trips the free-text limit nor blocks a message written afterwards.
  • Shared feedback helpers (FEEDBACK_SESSION_KEY, newFeedbackSessionId, specIdFromPath) move out of FeedbackWidget into utils/feedback.ts and routes/paths.ts; the widget keeps its behaviour (page-level reactions carry no library_id, so they never count as plot votes).

Supersedes #8126 (both thumbs top-left, no per-library field, three months behind main with 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 core clean. New tests: repository has_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 persisted library_id/language and one-vote-per-session.
  • /verify-migrations: throwaway Postgres 16 (pgserver) β€” alembic heads single head a7c3e9d1f5b8, upgrade head, alembic check "No new upgrade operations detected.", downgrade -1 + upgrade head roundtrip clean.
  • /verify-frontend: yarn type-check, yarn lint (0 warnings), Prettier clean, yarn vitest run β€” 71 files / 640 tests passed. Drove /scatter-basic/python/matplotlib in 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/main passes.
  • Copilot review round 1: three findings (native disabled, stable ranking tiebreaker, tz-naive test cutoff) applied in the second commit; threads resolved.

Checklist

  • changelog.d/thumbs-on-plot.md added (Added / Changed / Removed), no new bullet in CHANGELOG.md
  • Related documentation updated: docs/reference/plausible.md (feedback_submitted mode plot_overlay + library prop, report_issue source), docs/contributing.md (where "report issue" now lives)

Deploy note: the migration only adds nullable columns and an index; sync-postgres.yml runs alembic upgrade head after merge. The frontend sends library_id/language regardless; if the API deploys first, the extra fields are still accepted (they are optional in FeedbackRequest).

πŸ€– Generated with Claude Code

https://claude.ai/code/session_01Eckdg6kWrjasZMwNn8A1v8

…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

codecov Bot commented Sep 10, 2026

Copy link
Copy Markdown

Codecov Report

βœ… All modified and coverable lines are covered by tests.

πŸ“’ Thoughts on this report? Let us know!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟑 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 with library_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.

Comment thread app/src/sections/spec-detail/SpecDetailView.tsx
Comment thread core/database/repositories.py Outdated
Comment thread tests/integration/test_repositories.py Outdated
…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
@MarkusNeusinger
MarkusNeusinger merged commit dfb409b into main Sep 10, 2026
17 checks passed
@MarkusNeusinger
MarkusNeusinger deleted the claude/thumbs-rating-image-placement-x4tws7 branch September 10, 2026 20:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants