Acceptance Criteria β Client Portal Chat
Status: PARTIAL (revised 2026-05-25 after dev walkthrough) Original date: 2026-05-01 Author: Tyler AI review / E alignment Linked Stories: C-W1, C-W2, C-W3, C-W4, C-W8 Linked Runbook:
docs/testing/MVP_P0_RUNBOOK.md(see Client-portal AC-PC mapping section)
Revision note (2026-05-25)β
This spec was authored 2026-05-01 against an earlier portal UI. A dev walkthrough on 2026-05-25 against dev.h852.work (seed: Acme Financial / Eleanora Ogilvie) found that the portal UI evolved past several of the original ACs. ACs are now classified:
- ACTIVE β still represents the desired behaviour, testable today.
- OBSOLETE β REPLACED β the underlying need is met by a different implementation. Keep as historical context; do not test against the old criteria.
- OPEN BUG β desired behaviour confirmed; implementation gap remains.
Status per AC is recorded at the head of each section below. Walkthrough evidence is captured in the runbook AC-PC mapping section.
Context: What a Client Actually Seesβ
When a client logs in to [slug].h.work, they land on /client/chat. This is their entire experience of the product. They are talking to "Amy" β their named AI + expert specialist. They never see queue items, risk scores, or expert names.
The client's mental model: "I have a dedicated support specialist named Amy at h.work. I message her when I need something done."
Current Issues (as of 2026-05-01)β
| # | Issue | Severity |
|---|---|---|
| 1 | No org slug shown in the top-left title β hardcoded "H" / "h.work" | High |
| 2 | "Message" button on the Workforce pane does not start a thread β nothing happens | High |
| 3 | Chat interface not reachable without knowing the URL β no clear entry point | High |
| 4 | No Specialist assigned or shown β client doesn't know who Amy is | High |
| 5 | Section label says "Direct Messages" β should be "Your Threads" | Medium |
| 6 | Section label says "Your Experts" β should be "Your Specialists" | Medium |
Acceptance Criteriaβ
AC-PC-01 β Org slug shown in sidebar headerβ
Status (2026-05-25): OPEN BUG β fixed in PR #719 (
fix(client-portal): derive sidebar avatar initial from orgName). The sidebar avatar rendered a hardcodedHletter regardless of org context. The adjacent text already used{orgName}; only the avatar div was hardcoded. One-line patch infrontend/src/components/client/PortalChat.tsx:288.
Given a client is logged into their portal,
When the chat UI loads,
Then the top-left sidebar header shows the org's slug (e.g. acmecorp) as the workspace name, not a hardcoded string.
Implementation: fetch slug from JWT payload β
orgMemberships[0]βorgSlug, or from a/client/contextendpoint. Fall back to "h.work" if unavailable.
Acceptance check: Log in as any seeded client user. Top-left shows their org's slug, not "H / h.work".
AC-PC-02 β Specialist name and avatar shown in sidebar and chat headerβ
Status (2026-05-26): ACTIVE β right-panel pattern. Persona name and avatar surface in the
/client/chatdashboard right panel, which now renders the full Specialist profile fromGET /client/specialists. Multiple assigned Specialists use a carousel; a single Specialist renders as a plain card with no carousel controls.
Given an AM has assigned a Specialist to the client's org, When the client opens the chat, Then the Specialist's first name (e.g. "Amy") and avatar (initials fallback) appear:
- In the sidebar thread list (as the "contact" name)
- In the chat header when a thread is selected
- In the Workforce pane card
Given no Specialist is assigned, Then the name falls back to "Amy" (the generic persona) and the avatar shows "A".
Implementation: call
GET /client/contextor embed specialist info in the JWT; the API side should return{ specialistName, specialistAvatarUrl, orgSlug, agentName }.
AC-PC-03 β "Message" button on the Workforce pane starts a new threadβ
Status (2026-05-26): OBSOLETE β REPLACED. The "Message" button on the Specialist card no longer exists (zero
>Message<matches infrontend/src/components/client/). Specialist discovery now happens inside the/client/chatdashboard right panel, and thread creation happens via the sidebar+ New threadbutton (covered by AC-PC-06). Do not re-implement the "Message" button or the standalone/client/specialistdiscovery page.
Given the client is on the chat page,
When the client clicks the sidebar + New thread button,
Then:
- A new conversation is created via
POST /conversations - The sidebar updates to show the new thread
- The chat panel opens on the new thread, ready to type
Acceptance check: Click sidebar + New thread β thread appears in sidebar β user can type and send immediately.
AC-PC-04 β Client can enter chat and send a messageβ
Status (2026-05-25): ACTIVE β PASS-IMMEDIATE. Send flow verified on dev (message appears in DOM post-send). "Amy's reply within 3s" depends on Expert release per the default
autoRespondThreshold=101config β runbook C-W1 already documents the immediate-vs-end-to-end split. No bug here; the AC's 3-second wording should be read as "if the system is configured to auto-reply" β which is not the dev default.
Given the client is in a thread (selected in sidebar or newly created),
When the client types in the message input and presses Enter or the send button,
Then the message is sent via POST /conversations/:id/messages and appears immediately in the message list (optimistic update).
And then Amy's reply appears via Socket.io push when available; a reconnect banner is shown on transient disconnect (#843, closes #804 β 8s polling fallback removed).
Acceptance check: Full sendβreceive loop works end-to-end on dev.
AC-PC-05 β Sidebar section labels use correct terminologyβ
Status (2026-05-26): OBSOLETE β REPLACED. Zero matches in
frontend/src/for any of the four label strings ("Direct Messages","Your Threads","Your Experts","Your Specialists"). The sidebar redesign collapsed the two-section layout (DMs + Experts list) into a flat thread list under the+ New threadbutton, with Specialist discovery embedded in the/client/chatdashboard right panel and approvals at/client/approvals. The terminology problem the AC was solving (wrong words) no longer exists in this UI.
Given the client is viewing the chat sidebar, Then:
- The thread list section is labelled "Your Threads" (not "Direct Messages")
- The specialist section (if present) is labelled "Your Specialists" (not "Your Experts")
AC-PC-06 β Sidebar "New Thread" button creates a conversationβ
Status (2026-05-25): ACTIVE β PASS. Verified on dev: clicking
+ New threadincreased thread count and rendered a composer textarea. This is now the canonical thread-creation path (replaces the obsolete AC-PC-03).
Given the client already has one or more threads, When the client clicks "New Thread" (or "+ New Task"), Then a new conversation is created and selected, matching AC-PC-03 behavior.
AC-PC-07 β Thread list shows correct thread metadataβ
Status (2026-05-25, reprobe): ACTIVE β PASS. Walked 14 rows on dev (
aside button[aria-label^='Open thread:']). Each row exposes: avatar initial, thread name, relative timestamp, status badge. Example:E | AC-PC-04 smoke: hello from agent walk | 1h | PENDING. The spec's "green active dot" is replaced by an explicit status-badge text (PENDING,NEEDS YOUR INPUT, etc.) β strictly clearer than a colored dot. No code change needed.
Given the client has existing threads, When the sidebar renders, Then each thread entry shows:
- Specialist name (or "Amy" fallback)
- A snippet of the last message (truncated at ~40 chars)
- Relative timestamp of last activity
- Active indicator (green dot) if the thread status is
activeoropen
AC-PC-08 β Chat header shows correct specialist and thread infoβ
Status (2026-05-25, reprobe): ACTIVE β PASS-PARTIAL with spec drift. Chat header located via
button[aria-label='Back to briefing']parent div. Dev renders: thread subject + status badge (e.g.Hi+NEEDS INPUT). The AC's"Online Β· Support Specialist"line was removed in the redesign β specialist identity is shown per-message rather than in the header. Not a regression; ambient specialist context lives in the sidebar avatar (AC-PC-01, AC-PC-02). Update the AC wording to match the current pattern, or revisit if Tyler/E want the named-specialist header back.
Given a thread is selected, When the chat panel header renders, Then it shows:
- Specialist name and avatar
- Online/active status ("Online Β· Support Specialist" or "Conversation resolved")
- Active/Resolved status badge
AC-PC-09 β Empty state leads to first thread naturallyβ
Status (2026-05-25): ACTIVE β PASS-VIA-DIFFERENT-CTA. The dead-end risk is mitigated; the sidebar
+ New threadbutton is reachable without any existing thread (verified on dev). The AC's specific "Message" button CTA is obsolete (see AC-PC-03), but the "no dead-end state" PASS criterion is met by the redesigned pattern.
Given the client has no existing threads, When the chat page loads, Then the Workforce pane shows the Specialist card with a "Message" button. When the client clicks "Message", Then a thread is created and the client lands directly in the chat input, ready to type.
There is no dead-end state β the client always has a clear call to action.
AC-PC-10 β Mobile-responsive layoutβ
Status (2026-05-25): ACTIVE β PASS. Verified on dev at 480px viewport: with a thread selected, sidebar collapses (
width=0,height=0). The back-button / swipe-gesture return path was not exercised by the walker and remains untested; tracked in runbook AC-PC mapping for the next UAT pass.
Given the client accesses the portal on a screen < 640px wide, When no thread is selected, the sidebar is shown full-width. When a thread is selected, the chat panel fills the screen and the sidebar is hidden. A "back" button or swipe/tap gesture returns to the sidebar.
AC-PC-11 β Last-viewed thread is remembered per user (localStorage)β
Status (2026-05-25): OBSOLETE β REPLACED. Zero matches in
frontend/src/forhw_last_thread_*,lastThreadId, orlast_thread. The portal uses Next.js dynamic routing (/client/chat/[id]) instead β the URL IS the state. URL-as-state is strictly better than localStorage: cross-device, shareable, bookmark-able, survives browser data clear. The original AC's goal (return-to-context) is met by URL persistence. Do not re-implement the localStorage key.
Given the client previously had a thread selected,
When the client returns to /client/chat,
Then the last-viewed thread is restored automatically (keyed by userId in localStorage).
Key format:
hw_last_thread_{userId}`` β{ conversationId, timestamp }.
AC-PC-12 β Thread title editable inline (optional, P2)β
Status (2026-05-25, reprobe): ACTIVE (P2) β PASS. Walked dev: 14
.hw-thread-rename-btnbuttons exist (one per row,aria-label="Rename thread: <name>",opacity: 0until row:hoverfor touch-friendly hide). Force-click reveals the rename input field (aria-label="Rename thread"); save commits vialocalStorage[hw_threadNames]+ UI update. Implementation matches the AC; no API call observed (renames are local βPATCH /conversations/:idis not invoked, which means renames don't persist across browsers/devices). If cross-device persistence is wanted, wire the existing PATCH endpoint intocommitRename; otherwise mark this AC as accepted-with-local-only behaviour.
Given a thread is selected,
When the client clicks/double-clicks the thread title in the sidebar or header,
Then an inline edit field appears.
When the client types a new name and presses Enter,
Then the thread title is saved (locally or via PATCH /conversations/:id).
Out of Scope (for this spec)β
- Expert-facing queue or escalation UI (separate spec)
- WhatsApp channel integration (deferred β see WHATSAPP.md)
- Client approval flow for high-stakes actions (UI scaffolded as
ApprovalBannerbut API not wired β demoted to P2 on 2026-05-23, tracked in USER_STORIES.md C-W8. MVP handles approval manually via the conversation channel.) - Conversation search (C-W6, P1 β future sprint)
Test Credentials (Dev)β
| Role | Password | |
|---|---|---|
| Client user | See README.md seeded credentials | See README |
Login flow: /login β invite OTP β /client/chat
Implementation Notesβ
API surface neededβ
The chat portal needs the following data that is currently not returned to the frontend:
-
GET /client/context(new or augment existing/client/trial-info) β returns:{
"orgSlug": "acmecorp",
"orgName": "Acme Corp",
"specialistName": "Amy",
"specialistFirstName": "Amy",
"specialistAvatarUrl": null,
"agentName": "Amy"
} -
POST /conversationsmust work forrole: "customer"β currently implemented, verify on dev. -
POST /conversations/:id/messagesmust return the sent message β currently implemented.
Frontend changes neededβ
| Component | Change |
|---|---|
PortalChat.tsx / Sidebar | Replace hardcoded "H" / "h.work" with org slug from context |
PortalChat.tsx / Sidebar | Replace "Direct Messages" with "Your Threads" |
PortalChat.tsx / WorkforceEmptyState | "Message" button wired to handleNew() (check if broken) |
PortalChat.tsx / Sidebar | Show specialist name/avatar instead of hardcoded "Amy"/"A" |
PortalChat.tsx | Load portal context on mount, use for all specialist references |
PortalChat.tsx | Implement last-viewed thread via localStorage |
| Mobile | Add responsive breakpoint β hide sidebar on narrow screens when thread selected |
Sign-off Requiredβ
- E β product alignment
- Tyler β implementation assigned
- QA β test pass on dev before merge to staging