Skip to content

Fix UX feedback across Review, Intake, Dashboard, Document, Consultation, Response (FEEDBACK-01..10)#3

Merged
dinosaurchi merged 34 commits into
mainfrom
chi/fix-more-issues
Apr 18, 2026
Merged

Fix UX feedback across Review, Intake, Dashboard, Document, Consultation, Response (FEEDBACK-01..10)#3
dinosaurchi merged 34 commits into
mainfrom
chi/fix-more-issues

Conversation

@dinosaurchi

@dinosaurchi dinosaurchi commented Apr 17, 2026

Copy link
Copy Markdown
Owner

Summary

Two rounds of UX fixes addressing ten feedback items across the web UI.
Each is shipped as an independent commit with deploy + Playwright
coverage.

Round 1 (FEEDBACK-01..07)

  • FEEDBACK-01 — Review queue rows clickable: whole table row
    navigates to the document detail page (no more hidden "View" button);
    keyboard-accessible with role="link" + Enter/Space.
  • FEEDBACK-02 — Intake processing steps + CTA: replaced the spinner
    with a live stepper (upload → validate → extract → analyze) and added
    a prominent "Open review case" link once processing completes.
  • FEEDBACK-03 — Human-readable file size: document detail now
    formats bytes as B / KB / MB / GB with the raw byte count in
    parentheses; reusable formatBytes util + unit tests.
  • FEEDBACK-04 — Consultation chat polish: messages sort
    chronologically, own messages render on the right, others on the
    left, roles that can't reply see a read-only composer. Also fixed an
    AttributeError in workflow.add_consultation caused by an import
    shadowing the document module.
  • FEEDBACK-05/06 — Workflow actions guide: rebuilt the document
    detail sidebar with a visual pipeline, role/status context panel,
    "next steps for you" group, and a collapsible "actions not available
    right now" list that explains why each action is disabled. Also
    fixed a silent bug in the shared Card components that were
    dropping data-testid and other HTML attributes.
  • FEEDBACK-07 — Sidebar + chat bubble polish: new sidebar layout on
    both /consultation and /response with search, status groups,
    status pill + icon, relative timestamps, last-message snippet,
    urgency badge. Chat bubbles wrap long text, composer is an
    auto-sizing textarea with "Enter to send · Shift+Enter for newline",
    and the thread auto-scrolls to the bottom.
  • Misc: added a favicon for the browser tab.

Round 2 (FEEDBACK-08..10)

  • FEEDBACK-08 — Lazy-loading pagination for /review: backend
    GET /documents/ now supports offset + limit and returns
    X-Total-Count; the review page renders 25 rows at a time, debounces
    search to the server, and loads more via an IntersectionObserver
    sentinel (with a "Load more" button fallback and end-of-queue state).
  • FEEDBACK-09 — In-place scrolling + lazy sidebars on Consultation
    and Response
    : both pages are now fixed to the viewport
    (h-[calc(100vh-8rem)]) with two internally-scrolling columns, so
    the browser never gets an outer scrollbar. Sidebars mount 15 threads
    initially and grow by 15 as the user scrolls the sidebar itself.
  • FEEDBACK-10 — Dashboard + Intake alignment: stat cards pin their
    labels and values to the bottom, trend indicators become
    emerald/red pills; the two dashboard charts sit in an
    equal-height grid and use tabular-nums for right-aligned numeric
    columns; intake dropzone and guidelines cards stretch to equal
    height.

Commits (most recent last)

  • 8310558 Make review queue rows clickable (FEEDBACK-01)
  • 411f107 Show processing steps and review-case CTA on intake (FEEDBACK-02)
  • e4a578a Format file metadata size as B/KB/MB/GB (FEEDBACK-04)
  • 8b89ca5 Two-sided consultation chat, chronological, consultant replies (FEEDBACK-05)
  • a2592fb Workflow actions guide with pipeline + role context (FEEDBACK-06)
  • cb1305a Add favicon for browser tab logo
  • 20b8d3f Polish Consultation + Response sidebars and chat composer (FEEDBACK-07)
  • c0a077e Add lazy-loading pagination to review queue (FEEDBACK-08)
  • 18d1ecf Fit Consultation + Response pages to viewport and lazy-render sidebars (FEEDBACK-09)
  • 2ba0c28 Polish Dashboard and Intake alignment (FEEDBACK-10)
  • 1d0186e Extend e2e suite for pagination + in-place scrolling

Test plan

  • make ci — 51 web unit tests, 227 backend tests
  • npx playwright test — 17 E2E including Steps 3a/3b/3c (intake
    stepper, review-row clickability, human-readable file size),
    9b/9c/9d (two-sided chat, workflow guide, sidebar polish), and
    new 9e (pagination + in-place scrolling on Consultation/Response)
  • Manual QA with screenshots as each role (Intake Clerk, Reviewer,
    Consultant, Supervisor) after every deploy

Each document row in /review now navigates to the detail page on click
(mouse or keyboard Enter/Space), instead of requiring a small 'View'
button in the last column. The last column shows a chevron indicator
on hover. Existing E2E helper updated to click the row; new E2E test
added to verify clicking the title cell navigates correctly.

Made-with: Cursor
Replaced the bare spinner on /intake with a 4-step progress stepper
(Upload -> Validate -> Extract -> AI analyze) that advances in real
time while the upload request is in flight and finalises based on the
concrete artifact + analyses contained in the server response.

On success the page shows a prominent call-to-action linking directly
to the created document's review case (/documents/<id>), plus a
secondary link to the review queue and an 'Upload another' action.

Typed uploadDocument<T>() in web/src/lib/api.ts to preserve the
response shape consumed by the intake stepper.

Made-with: Cursor
Introduced web/src/lib/format.ts with a formatBytes() helper (binary
units, 1 KB = 1024 B, up to PB). The DocumentDetailPage file metadata
block now renders the size in a human-readable unit (e.g. '5.1 MB'),
while still exposing the exact byte count inline for audit purposes.

Covered with Vitest unit tests and a new Playwright E2E that asserts
the UI shows both a unit label and the raw byte count.

Made-with: Cursor
…FEEDBACK-05)

Rewrote the /consultation page to behave like a proper chat app:

- Messages are now sorted by created_at ascending so new notes always
  appear at the bottom of the thread regardless of role or send order
  (fixes the 'added at different positions' bug).
- The active user's messages render on the right in a blue bubble;
  other roles render on the left in a white bubble with a role-coloured
  badge. Each bubble carries data-author-role / data-own attributes for
  tests and styling.
- Reviewers and supervisors continue to use the workflow-aware
  /documents/{id}/request-consultation endpoint when the doc is not
  yet in consultation; consultants (and everyone once the thread is
  open) use the simpler /consultation/{id}/notes endpoint so all three
  participating roles can reply from the same composer.
- The composer now displays the active role, inline API errors are
  rendered in the thread, and roles that cannot participate
  (Intake Clerk) see a clear lock message instead of a broken input.

Also fixed a pre-existing crash in workflow.add_consultation: the
import 'from app.repositories import document as doc_repo' was being
shadowed by the CRUD instance re-exported from the package __init__,
so doc_repo.document.get(...) raised AttributeError. Switched to
explicit 'from app.repositories.document import document as
doc_repo_document, ...' which also fixes the 500 on POST
/consultation/{doc_id}/notes.

Added a Playwright test that verifies bubble sides, chronological
order across two roles, consultant reply, and the read-only state
for Intake Clerk.

Made-with: Cursor
- Add a visual WorkflowPipeline on the document detail page so any role can
  see where the case sits in the canonical lifecycle (received → extracted
  → analyzed → routed → under review → in consultation → approved → closed).
- Add WorkflowContext that prints the acting role, current status, a concise
  "what happens next" hint, and a "waiting on another role" banner when the
  current role cannot act but someone else can.
- Split workflow actions into "Next steps for you" (currently available) and
  a collapsible "Actions not available right now" disclosure that explains
  why each disabled action is blocked.
- Extend workflow-actions helpers with PIPELINE_STAGES, getPipelineIndex,
  ROLE_LABEL, getResponsibleRoles, and getNextStepHint.
- Fix ui-card so Card/CardHeader/CardTitle/CardContent forward extra HTML
  attributes (data-testid, etc.) — previously they were dropped, which
  broke the new panels for automated tests.
- Raise intake upload E2E timeout to 60s since the upload fans out to four
  synchronous LLM calls whose latency varies.
- Add a dedicated Playwright test that verifies the pipeline, role context,
  waiting-on banner (Intake Clerk), and disabled-actions disclosure
  (Supervisor), and tighten Step 9b to pick an in-consultation thread.

Made-with: Cursor
…ACK-07)

- Sidebar: search box, Active/Resolved groups with counts, richer cards
  (status pill with icon, relative timestamp, last-message snippet with
  author, urgency badge), sticky and independently scrollable column.
- Chat bubbles: max-w cap 75%, break-words + whitespace-pre-wrap so long
  messages wrap inside the bubble instead of clipping.
- Composer: textarea with Enter-to-send / Shift+Enter newline hint, role
  chip stays aligned, auto-scroll-to-bottom when notes update so the
  composer no longer covers new replies.
- Response page: mirror the same sidebar polish (search, Pending /
  Dispatched groups, relative timestamps, urgency badge).
- Add Playwright 'Step 9d' covering sidebar structure, search filter,
  textarea composer, bubble wrap + auto-scroll.
- Fix BUG-004 test to use [data-testid=consultation-thread] (header no
  longer contains the literal 'Thread:' prefix).
- Broaden FEEDBACK-06 test's doc filter to canonical pipeline statuses
  only (the demo seed includes error states like 'analysis_failed').

Made-with: Cursor
Adds server-side offset/limit pagination to `GET /documents/` with a
`X-Total-Count` response header, wires up a paged `apiGetPaged` helper in
the web client, and rewrites the review page around an IntersectionObserver
sentinel that fetches the next page when the user scrolls near the bottom.
Search is now debounced and sent to the backend via the `q=` query param.

- Backend: `DocumentRepository` gains `count()` + `offset=` on `list()`; the
  endpoint exposes `X-Total-Count` (also whitelisted on CORS).
- Web: new `apiGetPaged<T>` helper returns items + total header value.
- Review page: replaces eager load with 25-row pages, footer shows
  "Showing N of M", sentinel auto-loads more, "Load more" button as a
  fallback, and the search input talks to the backend.

Made-with: Cursor
…debars (FEEDBACK-09)

Before this change the outer page scrolled because the sidebar grew taller
than the thread pane when there were many threads. Now each page is a fixed
`h-[calc(100vh-8rem)]` flex container with two internally-scrolling columns,
so the browser never gets an outer scrollbar.

The sidebars also render incrementally: only 15 cards are mounted up front,
and an IntersectionObserver sentinel grows the window by 15 at a time as the
user scrolls — keeping the DOM light on large demos.

Made-with: Cursor
Dashboard
- Stat cards: trend is now a colored pill (emerald/red) rather than
  bare text, the label and value sit at the card bottom so the four
  cards have matching internal rhythm.
- Chart cards: the two cards are grid-stretched to equal height; the
  inner surrounded block fills that height instead of double-padding
  via `m-6 p-6`, which previously made the donut card taller.
- Status bars + donut legend: numeric values use `tabular-nums` and
  a fixed-width right-aligned column so bars/labels line up
  regardless of digit count. Long labels truncate gracefully.

Intake
- Dropzone and guidelines cards now share the same height via
  `items-stretch` + `flex flex-col`. The dropzone fills its Card
  with `flex-1 min-h-[260px]` instead of hard-coded padding, so the
  two cards always match visually when the page is idle.

Made-with: Cursor
- New Step 9e verifies /review lazy-loads more rows when the sentinel
  enters the viewport, and that /consultation and /response do NOT
  produce an outer page scrollbar while their sidebars remain
  internally scrollable. The test seeds a handful of lightweight .txt
  uploads via API when the DB doesn't already have enough rows to
  exercise pagination.
- Bumps per-test timeouts on AI-heavy steps (3, 3a, 5, 6, 7) to 120s
  so variability in synchronous LLM calls no longer flakes the suite.

Made-with: Cursor
@dinosaurchi dinosaurchi changed the title Fix UX feedback across Review, Intake, Document, Consultation, Response (FEEDBACK-01..07) Fix UX feedback across Review, Intake, Dashboard, Document, Consultation, Response (FEEDBACK-01..10) Apr 17, 2026
@dinosaurchi dinosaurchi self-assigned this Apr 18, 2026
… flash (FEEDBACK-11)

Clicking "Reroute document" and picking a department silently worked
on the backend but felt like a no-op in the UI: after reroute the
reviewer role's GET /documents/{id} implicitly flips status from
`routed` back to `under_review` (by design), so the status pill
never changes, and the page did not show the assigned department
anywhere in the header or metadata. Users saw "nothing happened".

Changes:

- DocumentDetailPage: new "Assigned to: <dept>" line under the title
  (with a building icon and an italic "Not yet assigned" fallback),
  fed by `doc.assigned_department_id` + `departmentNames` — so any
  reroute is immediately visible after the silent refetch.
- Add a dismissible success/error flash banner (5s auto-hide) shown
  after every workflow action. For reroute specifically it reads
  "Document rerouted to <dept name>." using the picked department.
  Replaces the previous bare `alert()` on failure too.
- New e2e spec (`reroute-visibility.spec.ts`) drives the whole flow
  against a real backend: picks a reroutable doc + target dept via
  API, opens the detail page as Department Reviewer, triggers the
  reroute, and asserts the assigned-to line updates and the success
  flash mentions the new department.

Made-with: Cursor
After a reroute, the page landed the user back on `under_review` with
the action buttons repainted — but with no signal that *progressing*
the workflow now belongs to a Supervisor (reviewers cannot close).
Users asked "after reroute, how do I go to the next step?". Added two
things to make that obvious:

1. **Latest routing callout** — sits at the top of the Workflow
   actions card. Shows decision (Rerouted / Accepted), the final
   department, the rationale, and who decided it and when. Makes the
   causal chain visible instead of forcing users to expand the
   collapsed routing history block.
2. **Next owner(s) for this document** — groups the actions that
   would be available to *other* roles on this same status, with a
   "Switch role" button per group. Clicking it rewrites
   `govdoc_role` in the RoleProvider/localStorage, so the whole page
   re-renders as that role and the correct set of actions (e.g.
   "Close document" for Supervisor) is immediately revealed.

Supporting changes:

- `workflow-actions.ts`: new `getActionsAvailableForOtherRoles()`
  helper and `toFrontendRoleLabel()` inverse of `toRoleId()`.
- `DocumentDetailInner`: pulls `setRole` from the role hook and wires
  the switch button through it.
- `reroute-visibility.spec.ts`: extends the Playwright spec to
  assert the callout shows the final dept + decided-by role, the
  "next owner" block lists Supervisor with `Close document`, and
  clicking "Switch role" moves the acting-as chip to Supervisor and
  reveals the `Close document` action.

Made-with: Cursor
…il (FEEDBACK-13)

The previous "Next steps for you" list rendered every available
action with equal visual weight, grouped by their internal taxonomy
(Review / Consultation / Closeout). After a reroute the Supervisor
landed on the doc seeing five buttons (Reroute, Escalate, Request
consultation, Mark out of scope, Close) with no signal which one
actually advances the pipeline — the user asked "how do I move
forward?". The "What happens next" hint also said "A Reviewer
must..." even when the active role was already Supervisor.

Changes:

- `workflow-actions.ts`:
  - New `FORWARD_ACTION_BY_STATUS_ROLE` map defining the single most
    progressive action per (status, role): approve-routing, close,
    resolve-consultation, etc. Exposed via `getForwardActionId` and
    `partitionByForwardness` helpers.
  - `getNextStepHint` now takes an optional `role` and returns
    role-aware copy for under_review/analyzed/in_consultation/approved
    statuses. Default fallback unchanged for backwards compat.
  - `getActionsAvailableForOtherRoles` now only reports a role if
    *that* role has a forward action for the current status (e.g.
    reviewer sees "Supervisor can Close document" but supervisor no
    longer sees "Reviewer can Reroute" — that's a loop, not a handoff).

- `DocumentDetailPage.tsx`:
  - "Next steps for you" replaced by a two-tier layout: a green
    "RECOMMENDED NEXT STEP" card with an explanation sentence and
    the single forward button, followed by a muted "Or, alternatives"
    list for reroute / request-consultation / mark-out-of-scope.
    When no forward action exists for the current role the block
    degrades to "Available actions" and the other-roles handoff CTA
    takes over.
  - `escalate` is hidden when the active role is already Supervisor
    (self-escalation was a no-op).
  - "Next owner(s)" CTA now only renders when `getForwardActionId`
    returns null for the active role — i.e. the user genuinely needs
    to hand off to make progress. Reduces noise for supervisors.
  - Each other-role card now says "Can <Forward action> to move the
    workflow forward" instead of listing every branch action.

- Tests:
  - `reroute-visibility.spec.ts` extended: after switching to
    Supervisor it asserts the forward-action card promotes Close,
    that the other-roles CTA disappears, that Escalate is no longer
    visible, and that the hint no longer reads "A Reviewer must...".
  - `govdoc.spec.ts` Step 9c updated to accept either "Recommended
    next step" or "Available actions" in the available-actions block.

Made-with: Cursor
…K-13)

Resolving a single consultation note used to unconditionally flip the
document to under_review, even if sibling notes were still open. That
stranded documents with multiple parallel consultations in an
inconsistent state (status=under_review while open notes remained),
hiding the "Resolve consultation" action from the UI.

Now the transition only happens once the last open note is resolved.
Covers the broken state seen on ac153273-... in QA.

Made-with: Cursor
…EDBACK-13)

Documents that drifted into a non-in_consultation status while still
holding open notes (legacy data from the pre-fix resolve-consultation
bug) now show an amber warning banner at the top of the document detail
page. The banner explains the situation and links to
/consultation?doc=<id>, which now honors the doc query param to
auto-select the right thread.

Made-with: Cursor
Uploads a fresh document, drives it to in_consultation via two parallel
notes, then asserts:
  - resolving the first note keeps status=in_consultation
  - resolving the second note flips it to under_review

Locks in the backend fix for ac153273-... style drift.

Made-with: Cursor
The consultation page used to be chat-only, forcing consultants to jump
back to /documents/<id> just to mark a note resolved. That round-trip
caused drift: many documents accumulated open notes because no one
bothered to navigate away.

Now each open note shows a "Mark as resolved" button, visible only to
the role named in the note's target_role (and not to the author). The
button hits the existing resolve-consultation endpoint, so once the
last open note is resolved the document flips to under_review (the
backend fix from 3c11c99 keeps sibling notes sticky).

A new footer hint under the composer explains the current state:
  - N notes waiting for YOUR reply
  - waiting on N open notes from other roles
  - thread closed → open the document page for next-step actions

Approve / reroute / close actions deliberately stay on the document
detail page since they are document-level decisions.

Made-with: Cursor
Root cause for documents stuck on under_review with no owner: the
approve-routing endpoint updated status -> routed but never copied the
AI's suggested_department_id onto the document, and never finalized the
routing_decision row created at analysis time. Supervisors then saw
"Close document" as the recommended next step on orphaned docs.

Fix approve-routing to:
- Look up the AI-produced routing_decision (accepted, final=None)
- Set final_department_id = suggested_department_id
- Set decided_by_role = current role
- Copy the suggestion onto document.assigned_department_id
- Refuse with NO_AI_ROUTING if there is no AI suggestion to accept

Adds integration tests for both the happy path and the guard, plus an
e2e regression that uploads -> waits for analysis -> approves routing
and verifies the full populated state.

Made-with: Cursor
When a supervisor opens a doc that is under_review but has no owning
department, "Close document" is the wrong forward action — closing a
docless doc just hides an un-owned case. Instead:

- getForwardActionId + getNextStepHint accept a DocContext so they can
  see assigned_department_id and recommend "Reroute document" with a
  tailored hint for supervisors/reviewers in that state.
- DocumentDetailPage gains a NoAssignedDepartmentBanner that surfaces
  the missing-owner situation right under the flash area on any
  non-terminal routed/under_review/in_consultation doc without a dept.
- forwardExplanation gets a "reroute" case.
- ConsultationPage header pill now correctly says "1 note" (singular)
  instead of "1 notes".

Unit tests cover the new (status, role, dept) combinations and the
string-arg legacy overloads. The corresponding e2e regression lives in
e2e/approve-routing-assigns-dept.spec.ts.

Made-with: Cursor
Add POST /documents/{id}/approve for reviewers and supervisors, moving the
document to `approved` from `under_review` or `in_consultation`. Reject with
OPEN_CONSULTATION_NOTES when any note is still unresolved.

POST /close now only performs `approved` → `closed` (supervisor-only).

Grant documents.approve to reviewer and supervisor in roles.yaml.

Add integration tests, RBAC coverage, and contract assertion that clerks
cannot approve.

Made-with: Cursor
Add workflow action `approve` (reviewer + supervisor) with forward-action
priority over close on under_review. Close is only available in `approved`
status for supervisors. Pass assigned_department_id into
getActionsAvailableForOtherRoles so supervisor handoff hints stay correct.

Response queue "Approve & Close" chains POST /approve then POST /close.

Update unit tests, govdoc Step 8 e2e, and reroute-visibility (consultant view
for next-owner CTA now that reviewers have a forward action).

Made-with: Cursor
`closed` is terminal for actions but was blanking the whole pipeline because
`terminal` suppressed all done/active states. Add getPipelineStepState() so
closed shows every stage as completed.

When status is approved without any consultation activity, keep the In
Consultation row pending so we do not imply a branch that never happened.

Pass hadConsultationActivity from the document detail page (notes exist or
currently in_consultation).

Made-with: Cursor
…ehind nginx

- Parse `{ error: { message } }` from custom FastAPI JSONResponse handlers;
  the client only looked at `detail`, so most failures showed as the useless
  "API request failed" string.
- On non-JSON error bodies (e.g. 413 HTML), include HTTP status and a body
  snippet in the thrown message.
- Raise nginx client_max_body_size to 100m and extend proxy timeouts so
  uploads and AI intake are not cut off at the default 1m / 60s limits.

Made-with: Cursor
Success flash now includes a button-style link to /consultation?doc=<id>,
clearer copy, and a short note that the thread also appears on the document
page. Extends flash timeout when a follow-up link is shown.

Made-with: Cursor
…tion card

The success flash alone was easy to miss (viewport scroll). Add persistent
actions on the Consultation thread card: deep-link to /consultation?doc=,
link to Review queue, and short copy. Scroll the flash banner into view
when a consultation follow-up link is shown.

Made-with: Cursor
Users confused closing from Review with seeing an outcome on Response.
Add an on-page explainer and clearer empty state: Pending vs Dispatched,
that closed items move to Dispatched, and which statuses appear in the queue.

Made-with: Cursor
@dinosaurchi
dinosaurchi merged commit 9191bd0 into main Apr 18, 2026
1 check passed
@dinosaurchi
dinosaurchi deleted the chi/fix-more-issues branch April 18, 2026 06:10
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