Skip to content

feat(web): "Why this?" panel for content recommendations#587

Draft
arberx wants to merge 1 commit into
mainfrom
feat/recommendation-explain-ui
Draft

feat(web): "Why this?" panel for content recommendations#587
arberx wants to merge 1 commit into
mainfrom
feat/recommendation-explain-ui

Conversation

@arberx
Copy link
Copy Markdown
Member

@arberx arberx commented May 18, 2026

Summary

Frontend for the LLM-explanation feature shipped in PR #586. Adds a "Why this?" button next to "Mark addressed" on every content-recommendation card in the report's action plan. Clicking expands an inline panel that:

  • Hydrates from cached analysis on first open (GET /analysis) — instant render for previously-analyzed cards.
  • Auto-fires POST /analyze if no cache exists — explanation appears as soon as the LLM responds.
  • Provider dropdown lets the operator switch between Claude / OpenAI / Gemini / Zai — changing it auto-regenerates with forceRefresh: true.
  • Regenerate button for manual refresh; Hide button to collapse the panel.
  • Cost footer shows the model id + USD cost in cents (e.g. claude-sonnet-4-6 · ~0.0420¢).

Loading + error states throughout; "Try again" affordance when the API call fails.

Architecture

ReportPage.tsx (ActionPlanSection)
  ├── State: expandedExplanations: Set<targetRef>
  ├── Per-card: "Why this?" button → toggleExplanation(targetRef)
  └── When expanded: <WhyThisPanel projectName targetRef onClose />
         ├── useEffect on mount → fetchRecommendationAnalysis() (GET)
         │     ├── 200 → setExplanation(cached)
         │     └── 404 → analyzeMutation.mutate({}) (POST)
         ├── Regenerate → analyzeMutation.mutate({ forceRefresh: true })
         └── Provider change → analyzeMutation.mutate({ provider, forceRefresh: true })

useAnalyzeRecommendation() (mutation) intentionally does NOT invalidate project-scoped queries — the explanation is per-card and doesn't affect the recommendation list, health scores, or the report DTO. Mirrors how the dismiss mutation invalidates everything; the explanation mutation invalidates nothing.

Test plan

  • pnpm -r typecheck — clean
  • pnpm run lint — 0 errors
  • npx vitest run260 files, 3123 passed (+9 new)
    • Hydrates from cached analysis when GET returns 200
    • Falls through to POST analyze when GET 404s
    • Renders error state when analyze fails
    • Regenerate button forces a fresh POST with forceRefresh: true
    • Provider dropdown change triggers immediate regenerate with that provider
    • Hide button calls onClose
    • ExplanationBody: bullets, paragraphs, mixed content

What's not in scope

  • HTML report parity: the downloadable HTML report (canonry report) doesn't render the explanation panel. The explanation is interactive/dashboard-only for now. A follow-up PR could wire the cached explanation into the ProjectReportDto and render it as a static block in report-renderer.ts to maintain full parity, but that's separable.
  • /content/targets standalone view doesn't exist yet as a dashboard page; this PR only wires the button into the report's action plan. If a dedicated page lands later, it can reuse WhyThisPanel verbatim.

Builds on #585 (capability tiers) and #586 (explain backend).

🤖 Generated with Claude Code

Adds an expandable LLM-explanation panel on each content-recommendation
card in the report's action plan. Clicking "Why this?" opens a small panel
that hydrates from the cached explanation if one exists, otherwise auto-
fires POST /analyze to generate one. Backed by the endpoints shipped in
PR #586.

Wiring:
- `apps/web/src/api.ts` — `fetchRecommendationAnalysis()` (returns null
  on 404 for clean branching) + `analyzeRecommendation(body)`.
- `apps/web/src/queries/mutations.ts` — `useAnalyzeRecommendation()`.
  Deliberately does NOT invalidate any project-scoped queries: the
  explanation is per-card and doesn't change the recommendation list.
- `apps/web/src/pages/ReportPage.tsx`:
  - New `WhyThisPanel` + `ExplanationBody` components (inline — only
    used in one place per AGENTS.md component rule).
  - "Why this?" button next to the existing "Mark addressed" button.
  - Expand state lives on `ActionPlanSection` as a `Set<targetRef>`,
    matching the optimistic-dismiss state pattern.

Panel UX:
- Auto-fetches cached analysis on first open → falls through to POST
  if no cache.
- Provider dropdown switches the explainer on the fly (forceRefresh =
  true with `provider` override; auto-fires on change for snappier feel).
- Manual "Regenerate" button forces a fresh call.
- Footer renders model id + cost in cents (millicents / 1000).
- Loading + error states; "Try again" affordance on error.

Tests: 9 new — covers cache hit, cache miss → auto-POST, error path,
regenerate, provider switch, hide button. All 3123 workspace tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@arberx arberx marked this pull request as draft May 18, 2026 02:47
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.

1 participant