Skip to content

feat: chat + claim verification AsyncStatus integration (MON-216) - #303

Merged
Walid-peach merged 2 commits into
masterfrom
feature/mon-216-chat-claim-verification-asyncstatus-integration-mon-207
Aug 3, 2026
Merged

feat: chat + claim verification AsyncStatus integration (MON-216)#303
Walid-peach merged 2 commits into
masterfrom
feature/mon-216-chat-claim-verification-asyncstatus-integration-mon-207

Conversation

@Walid-peach

@Walid-peach Walid-peach commented Aug 3, 2026

Copy link
Copy Markdown
Owner

What

Wires MON-214's AsyncStatus primitive into /chat's search and claim-verification flows, replacing the bouncing-dots typing indicator with contextual status text, a cancel affordance for in-flight requests, and a retry affordance on failure.

Why

Chat search showed three bouncing dots with no context and no way to cancel or retry. Claim verification already showed static contextual text, but neither mode exposed aria-busy, a cancel path, or a retry path - a failed request just left a plain red error string with no way to resubmit without retyping.

Re-reading the actual code surfaced a scope correction: the issue named components/chat/Bubbles.tsx's TypingIndicator as the integration target. That component (along with UserBubble, AssistantBubble, ErrorBubble in the same file) turned out to be entirely dead code - chat/page.tsx has its own inline bubble implementations and never imported Bubbles.tsx. Removed the whole file rather than leave misleading dead code that references the exact pattern this issue was meant to replace.

Changes

  • frontend/src/app/chat/page.tsx:
    • The "typing" message now renders AsyncStatus with contextual text for both modes (SEARCH_LOADING_TEXT, the existing VERIFY_LOADING_TEXT), wired to a new cancelActive callback.
    • The "error" message now renders AsyncStatus in its error state, with a retry affordance that resubmits the exact question/claim that failed (stored on the error message itself as retry: { kind, value }, so historical errors in a restored conversation retry correctly even if mode has since changed - omitted entirely for errors restored from pre-MON-216 stored conversations, so old data doesn't render a broken retry button).
    • send/submitClaim now create an AbortController per request, threaded through api.search/api.verify; both catch blocks special-case AbortError (silently removes the typing message, no error message) - mirrors the existing AbortError handling already used by shareAnswer's navigator.share() call in this same file.
    • Removed the now-dead local Dot/KeyframeStyle bouncing-dots implementation.
  • frontend/src/lib/api.ts: apiPost and api.search/api.verify accept an optional AbortSignal (backward compatible - every other caller is unaffected).
  • frontend/src/components/chat/Bubbles.tsx - deleted (dead code, see above); the now-empty components/chat/ directory is removed with it.

Testing

  • New frontend/__tests__/components/ChatPage.test.tsx (no prior test coverage existed for this page) - 5 tests: loading state shows AsyncStatus with aria-busy and the right contextual text per mode, cancel aborts the request and clears the loading state with no error, a failed request shows retry and resubmits the same text, and a successful verification clears the loading state.
  • npm test - full suite, 229/229 passing.
  • npm run lint / npm run build - clean.
  • Manual verification against the live production API (Playwright): both /chat question and claim-verification flows show the contextual status + cancel affordance while in flight (aria-busy="true"), then settle into a real answer/verdict. Screenshot shared with the requester.

Review caught two UX gaps in the new retry/cancel flows, both fixed:

  • Retry duplicated the failed user message (both the original failed bubble and the resubmitted one stayed in the transcript) - fixed by stripping the error message's own (user, error) pair before resubmitting.
  • Cancel only removed the typing indicator, leaving the user's message dangling with no reply and the typed text lost (it was cleared before the network call) - fixed by removing both the trailing user and typing messages and restoring the text into the input.

Risks / Notes

  • No DB/schema/deploy-config changes. apiPost's signature change is additive/optional.
  • Token-level streaming is out of scope per the issue - rag/chain/rag_chain.py's ask() doesn't stream today, so this delivers contextual progress only, not streamed output.
  • Reduced-motion handling for the activity dot comes from AsyncStatus itself (already tested in MON-214); not re-tested here.

Breaking Changes

None.

Replaces chat/page.tsx's bouncing-dots typing indicator with MON-214's
AsyncStatus for both search and verify modes: contextual status text
("Recherche dans les votes et profils des deputes...", the existing
verify-mode text), a cancel affordance for in-flight requests, and a
retry affordance on failure that resubmits the original question/claim.

Adds AbortSignal support to apiPost/api.search/api.verify (optional,
backward compatible) so cancel can actually abort the fetch rather than
just hiding the UI. Follows the existing AbortError handling convention
already used by shareAnswer's navigator.share() call in this same file.

Scope correction: the issue described components/chat/Bubbles.tsx's
TypingIndicator as the target - that file turned out to be entirely
dead code (chat/page.tsx has its own inline bubble implementations,
Bubbles.tsx was never imported anywhere). Removed it along with the
now-unused local Dot/KeyframeStyle bouncing-dots implementation.

Token-level streaming remains out of scope per the issue - the RAG
backend doesn't stream today.
@vercel

vercel Bot commented Aug 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
mon-elu Ready Ready Preview Aug 3, 2026 10:12pm

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

🧪 dbt data-health check — ✅ passed

22:11:13  52 of 62 PASS source_not_null_raw_votes_vote_id ................................ [PASS in 1.49s]
22:11:13  56 of 62 START test source_unique_raw_deputies_deputy_id ....................... [RUN]
22:11:13  53 of 62 PASS source_not_null_raw_votes_voted_at ............................... [PASS in 1.45s]
22:11:13  57 of 62 START test source_unique_raw_votes_vote_id ............................ [RUN]
22:11:14  56 of 62 PASS source_unique_raw_deputies_deputy_id ............................. [PASS in 1.45s]
22:11:14  58 of 62 START test unique_mart_deputy_scorecard_deputy_id ..................... [RUN]
22:11:14  54 of 62 PASS source_relationships_raw_vote_positions_deputy_id__deputy_id__source_raw_deputies_  [PASS in 1.77s]
22:11:14  59 of 62 START test unique_mart_party_alignment_deputy_id ...................... [RUN]
22:11:14  55 of 62 PASS source_relationships_raw_vote_positions_vote_id__vote_id__source_raw_votes_  [PASS in 2.06s]
22:11:14  60 of 62 START test unique_mart_vote_summary_vote_id ........................... [RUN]
22:11:15  57 of 62 PASS source_unique_raw_votes_vote_id .................................. [PASS in 1.44s]
22:11:15  61 of 62 START test unique_stg_deputies_deputy_id .............................. [RUN]
22:11:15  58 of 62 PASS unique_mart_deputy_scorecard_deputy_id ........................... [PASS in 1.46s]
22:11:15  62 of 62 START test unique_stg_votes_vote_id ................................... [RUN]
22:11:16  59 of 62 PASS unique_mart_party_alignment_deputy_id ............................ [PASS in 1.46s]
22:11:16  60 of 62 PASS unique_mart_vote_summary_vote_id ................................. [PASS in 1.45s]
22:11:16  61 of 62 PASS unique_stg_deputies_deputy_id .................................... [PASS in 1.43s]
22:11:17  62 of 62 PASS unique_stg_votes_vote_id ......................................... [PASS in 1.44s]
22:11:18  
22:11:18  Finished running 62 data tests in 0 hours 0 minutes and 31.56 seconds (31.56s).
22:11:18  
22:11:18  Completed successfully
22:11:18  
22:11:18  Done. PASS=62 WARN=0 ERROR=0 SKIP=0 TOTAL=62

Validates prod data health. A failure means current prod data is stale/broken, not necessarily that this PR is wrong.

@Walid-peach Walid-peach left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Attention Score

50/100 - NEEDS YOUR ATTENTION

  • Base 100
  • -10: 1 Should Fix finding (retry duplicates the failed user message)
  • -10: 1 Should Fix finding (cancel leaves a dangling, unexplained user message)
  • -10: Testing / Validation Gaps section is non-empty
  • -15: could not fully verify the retry/cancel UX end-to-end via automated tests (the gaps below weren't caught by the new test suite)
    Reason to take a closer look: both findings are real, user-visible UX defects in the two new interactive affordances this PR adds (retry and cancel), not just polish - worth confirming intent before merge even though nothing crashes or fails CI.

Summary

This PR replaces /chat's bouncing-dots typing indicator with MON-214's AsyncStatus for both search and claim-verification modes, adds real AbortController-backed cancellation, and adds a retry affordance on failure. The scope correction (components/chat/Bubbles.tsx was dead code, deleted along with the file) is well-reasoned and clearly documented. Test coverage is a genuine improvement - this page had zero tests before. Two UX gaps in the new retry/cancel flows are worth fixing before merge.

Must Fix

None.

Should Fix

  • Retry duplicates the failed user message. frontend/src/app/chat/page.tsx's onRetry callback (in the msg.role === 'error' branch, ~line 707) calls send(msg.retry.value) / submitClaim(msg.retry.value) directly. Both functions unconditionally do setMessages(prev => [...prev, { role: 'user', text: q }, ...]) - so after a failed attempt + retry, the transcript shows the same question/claim as two separate user bubbles (the original failed one, still in the array since only the typing role gets filtered on error - and a second one from the retry). A reviewer clicking retry twice on the same conversation would see three copies of the same message. Since every error message is always immediately preceded by its own user message in messages (append-only, error/typing only ever follow their own user message), the retry callback can safely strip that pair first: setMessages(prev => prev.filter((_, idx) => idx !== i && idx !== i - 1)) before calling send/submitClaim (using the i already in scope from the .map closure).
  • Cancel leaves a dangling, unexplained user message. In both send and submitClaim's AbortError catch branch, only the typing message is filtered out (setMessages(prev => prev.filter(m => m.role !== 'typing'))) - the user message that triggered the request stays in the transcript with no reply and no visible indication it was cancelled. Combined with inputVal already having been cleared at the top of send/submitClaim before the network call, the user's typed text is gone entirely with no way to recover it. Suggest removing both the trailing user and typing messages on cancel (setMessages(prev => prev.slice(0, -2)) is safe here specifically because nothing else can be appended between them while loading blocks further submissions) and restoring the text into the input (setInputVal(q) / setInputVal(claim)) so cancelling reads as "return to editing," not "silently swallow what I typed."

Nice to Have

  • frontend/src/app/chat/page.tsx's error-branch AsyncStatus passes status={msg.text} even though status is never rendered once error is set - harmless (satisfies the required prop) but a reader might wonder why the same string is passed twice; a one-line comment (already present just above, in a different context) or accepting status as optional on AsyncStatus when error is given would remove the redundancy. Not worth reopening MON-214's already-merged API for.
  • The apiPost/api.search/api.verify signature change (adding an optional trailing signal param) is backward compatible and low-risk, but worth a quick grep confirmation that no other caller passes a truthy third positional argument that could now be misinterpreted as a signal - a quick check shows none do, just flagging the class of risk for future callers of apiPost who might add a third argument without checking this PR's addition.

Testing / Validation Gaps

The two Should Fix findings above weren't caught by the new ChatPage.test.tsx suite: the retry test (shows a retry affordance on failure and resubmits the same question) only asserts mockSearch was called twice and the answer text appears - it doesn't assert on the number of user-bubble elements in the DOM, which is exactly where the duplication would show up. Similarly the cancel test only asserts the status role and an absent retry button, not that the dangling user message and cleared input are handled. Once the two Should Fix items are addressed, tests asserting the DOM message count (not just mock call counts) would close this gap and prevent regression.

Documentation / Reviewer Notes

The manual verification against the live production API (Playwright, screenshot of both modes' loading state) is a strong signal for a page with this much interactive/timing-sensitive behavior and no prior test coverage - same pattern used well in MON-215, good to see it continue here.

Verdict

Needs changes before merge

@Walid-peach Walid-peach added the needs-attention PR review score below threshold - maintainer should look closely label Aug 3, 2026
Retry resubmitted the failed question/claim without removing the
original failed attempt's user bubble, so the transcript showed the
same message twice. Now strips the error message's own (user, error)
pair before resubmitting - safe because error/typing messages are
always appended immediately after their own user message.

Cancel only removed the typing indicator, leaving the user's message
dangling with no reply and no restored input text. Now removes both
the user and typing messages (the trailing pair - nothing else can be
appended between them while `loading` blocks new submits) and restores
the text into the input, so cancelling reads as "back to editing."

Both fixes covered by strengthened assertions in ChatPage.test.tsx.
@Walid-peach Walid-peach added safe-to-skim PR review score above threshold - agent review likely sufficient and removed needs-attention PR review score below threshold - maintainer should look closely labels Aug 3, 2026
@Walid-peach

Copy link
Copy Markdown
Owner Author

Applied both Should Fix findings from the review:

  1. Retry duplicated the failed user message - onRetry now strips the error message's own (user, error) pair before resubmitting (safe since error/typing messages are always appended immediately after their own user message, append-only).
  2. Cancel left a dangling user message and lost the typed text - the AbortError branches in send/submitClaim now remove both the trailing user and typing messages (safe - nothing else can be appended between them while loading blocks new submits) and restore the text into the input, so cancel reads as "back to editing."

Verified live against the production API: cancelling now correctly returns to the empty state with the question restored in the input box and no orphaned sidebar conversation. Strengthened the two relevant tests in ChatPage.test.tsx to assert on DOM message counts (not just mock call counts), per the review's Testing Gap note.

CI green again after the fix.

@Walid-peach
Walid-peach merged commit 1c8cd52 into master Aug 3, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

safe-to-skim PR review score above threshold - agent review likely sufficient

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant