WiseAI Realtor — Conversation Inbox Expected Output Spec
⛔ STATUS: DRAFT — NOT APPROVED. CLAUDE.md Rule #17 (HARD GATE) is NOT satisfied.
Stage-1 agent research, pre-populated from the realtor design docs + the
inbox.htmlmock. Do not build the customer-facing screen until the founder approves and confirms the open items in a Stage-2 interview.
Sourced from (read-only, 2026-06-29):
design-direction.md(Conversation inbox = "3-pane split: filters/saved views | conversation list | thread + context rail (linked record, AI summary, take over from AI)"; transcript/ conversation-view + autopilot-per-conversation components),information-architecture.md(group A "Conversations / Inbox"),data-model.md(re_interactions,re_sms_messages, consent on STOP/HELP), the approved siblingacceptance/ai-front-desk.md(the capture/escalation behaviour this UI surfaces), andrealtor-mockups/inbox.html.
0. Scope — what this spec covers, and what it does NOT
This spec covers the Conversation Inbox — the unified workspace where an
agent reads AI-handled conversations, sees what the AI escalated, and takes
over from the AI mid-conversation across chat + voice + SMS in every
language. It is the sibling of realtor-dashboard-mvp.md ("Needs a human" tile
deep-links here), realtor-leads-mvp.md, and realtor-contact-record-mvp.md
(the context rail links to the full record).
Does NOT cover: the capture / escalation logic itself (when/why the AI
escalates, the universal crisis/safety layer, lead writes) — that is the approved
ai-front-desk.md (this inbox is the human-side surface of that behaviour); the
voice/chat runtime internals (voice-agent-livekit/, api/chatbot/stream);
the AI agent configuration screen (a sibling AI-config spec — guardrails,
knowledge sources, voices); the full contact record (realtor-contact-record-mvp.md).
Build posture: the local-business platform already has a unified leads inbox
(InboxTab.tsx); chat is already multilingual (route.ts) and the RE voice
vertical exists. The RE Conversation Inbox extends these into a 3-pane,
take-over-capable surface over the re_interactions/re_sms_messages spine — do
not rebuild the runtime (do-not-reinvent.md).
Voice-agent guardrail: any change that touches the LiveKit voice agent, SIP, or
moderation.pyis LIFE-SAFETY tier — owned by thevoice-agent-engineeragent, requires independent QA voice review + founder go before any deploy. This inbox is a read/take-over surface; it must not weaken the universal crisis/safety layer (ai-front-desk.md§V7).
1. The mock screen this spec governs + what it reads
Governs: realtor-mockups/inbox.html — a 3-pane layout inside the
Workflow-Rail shell:
- Pane 1 — Views/filters (~196px): saved Views (Needs a human · 4, Unassigned · 7, AI-handled · 31, Assigned to me · 9, All · 47), Channel filters (💬 Chat / 📞 Voice / ✉️ SMS), Language filter (EN · FR · ਪੰ · 中文).
- Pane 2 — Conversation list (~312px): rows with avatar, name, last-message snippet, timestamp, channel chip + language chip, and an "↗ AI escalated" badge; an AI-handled row shown muted with a "🤖 AI" badge for contrast.
- Pane 3 — Thread + context rail: the conversation thread (alternating AI vs contact bubbles — "🤖 Aria" labelled — with an inline "Aria escalated: commission is outside its lane (per your AI guardrails). Waiting for you." system row), a thread header with an "AI paused — you're in control" state + Open record + Take over actions, a composer with suggested replies, and a right context rail (Aria's summary, Contact facts: Stage/Score/Source/ Agent, Quick actions incl. "↩︎ Resume AI (Aria)").
Reads (target-state CRM-spine APIs — WS1a; verify exact route names; do not fabricate):
| Surface | Underlying read |
|---|---|
| Conversation list | re_interactions grouped by contact/conversation (chat_session / call / text), with escalation + assignment state |
| Channel/language chips | interaction channel + detected language |
| Thread (chat/voice/SMS) | re_interactions (chat/transcript) + re_sms_messages (SMS thread) for the conversation |
| Escalation / "needs a human" | escalation flag set by the AI (the ai-front-desk.md escalation path) |
| Context rail — Aria summary | re_contacts.ai_summary |
| Context rail — contact facts + "Open record" | re_contacts → realtor-contact-record-mvp.md |
| Take over / Resume AI | per-conversation autopilot state (+ SMS outbound consent gate via re_sms_messages.consent_checked / re_consent_events) |
Demo tenant: Terry & Sheri Real Estate (…0c01). Demo conversations
(David Tran / Janet Lowe / Mei Chen / Amrit Brar / Harjit Singh) are demo content;
NEVER a real customer (feedback_never_modify_customer_data).
2. The AI-Bridge / honesty anchor (this screen IS the bridge made visible)
Per knowledge/architecture/ai-bridge-principle.md — the inbox is where the
"bridge to humans" is operationalized:
- The AI escalates rather than pretends. Every escalation row states why the AI stopped ("commission is outside its lane, per your AI guardrails") and waits for a human — never bluffs an answer. The RE guardrails (no value/ legal/mortgage/tax advice, no steering — audit P0 #5) are what produce these escalations; the inbox surfaces them honestly.
- AI vs human authorship is always labelled. Thread bubbles distinguish "🤖 Aria" from the human agent and from the contact; a taken-over thread shows "AI paused — you're in control".
- Take-over is clean and reversible. Taking over pauses the AI for that conversation; Resume AI (Aria) hands it back. The contact is never double- answered by AI + human at once.
- Universal crisis/safety is untouched — a personal-crisis disclosure on any
RE line still triggers the universal 988/911/DV layer (
ai-front-desk.md§V7); the inbox never gates or hides it.
3. Role-based visibility
| Bucket | Roles | Inbox scope |
|---|---|---|
| Brokerage management | brokerage_owner, broker_admin (brokerage scope) | All conversations across the brokerage; "Unassigned" pond; can assign + take over any |
| Team management | team_admin (team scope) | All conversations within the team; assign + take over within team |
| Agent / ISA | agent, isa (own scope) | Own assigned + unassigned pond; "Assigned to me" view; cannot read a peer's assigned private threads (per routing) |
| Support / external | transaction_coordinator, marketing_assistant, external_partner (bounded) | Read-only or scoped; external_partner (read-only) cannot send/take over |
The mock's views ("Assigned to me · 9", "Unassigned · 7") are the agent-scope
slices; "All · 47" is a management/whole-tenant slice. Language-based routing
(audit P1; Beckett's two agents) may auto-assign by preferred_language.
4. Expected outputs — per pane
4.1 — Pane 1: Views / saved filters / channel / language
Should see:
- Saved Views with live counts: Needs a human (escalated, not taken over), Unassigned, AI-handled, Assigned to me, All.
- Channel filter (Chat / Voice / SMS) and a Language filter — the language
options are per-tenant/per-agent enabled languages, not a hardcoded set
(consistent with the Smart-List language rule in
realtor-leads-mvp.md§4.3). - Views are dynamic (counts update as conversations are handled/escalated).
Should NOT see:
- A hardcoded language list unrelated to the agent's enabled languages.
- A static "AI-handled" view that doesn't update when the AI handles a new one.
Success: the agent can jump to exactly the slice they need ("Needs a human" first thing in the morning), filtered by channel/language.
4.2 — Pane 2: Conversation list (chat + voice + SMS unified)
Should see:
- One list spanning chat + voice + SMS, each row: avatar, name, last-message snippet (voice rows summarize, e.g. "Seller — wants a callback re timing & staging"), timestamp, channel chip, language chip, and an "↗ AI escalated" badge where applicable. An AI-handled conversation appears muted with a "🤖 AI" badge so the agent can tell what still needs them vs what Aria closed.
- Selecting a row opens it in Pane 3; the selected row is visually marked.
Should NOT see:
- Separate inboxes per channel (the value is one unified inbox).
- An escalated conversation that isn't visually distinguished from an AI-handled one.
Reads: re_interactions (+ re_sms_messages) grouped per conversation with
escalation/assignment/AI-handled state.
Success: in one list the agent sees, across all channels and languages, which conversations need them and which the AI already handled.
4.3 — Pane 3: Thread + take-over + composer
Should see:
- A thread with alternating bubbles clearly labelled contact vs "🤖 Aria" vs human agent, monospace/relative timestamps, and inline system rows for AI events — notably the escalation row ("Aria escalated: commission is outside its lane, per your AI guardrails. Waiting for you."). For a voice conversation the thread shows the call summary + a transcript (with the original-language → English where translated) and a recording link.
- A thread header showing the conversation's autopilot state — when a human
is handling it: "AI paused — you're in control" — plus Open record
(→
realtor-contact-record-mvp.md) and a Take over button. - A composer with suggested replies (e.g. "Happy to walk you through our commission in a quick call —", "Book a call"), free-text reply, and Send. Nothing auto-sends — the human presses Send (and for SMS, the outbound is consent-gated, §4.5).
Should NOT see:
- AI and human both replying to the same live conversation simultaneously (taking over pauses the AI for that thread).
- A suggested reply that asserts a home value / legal / mortgage position (the guardrails apply to suggestions too).
- A "Send" that fires without the human (no unattended sends —
feedback_db_writes_ok_sends_need_blessing).
Success: the agent reads the full thread (any channel/language), sees exactly why the AI escalated, takes over, and replies — with the AI cleanly paused for that conversation.
4.4 — Per-conversation autopilot / take-over / resume (the handoff)
Should see (verify behaviour, not DOM):
- Take over → the AI is paused for that conversation only; the header
flips to "AI paused — you're in control"; the AI will not send further messages
on that thread until resumed. (Sample state at ≥2 timepoints —
feedback_verify_behavior_not_dom_presence.) - ↩︎ Resume AI (Aria) → control returns to the AI for that conversation.
- The global Autopilot/Co-pilot toggle (dashboard/top bar) sets the default; the inbox provides the per-conversation override (design-direction "autopilot per conversation").
- A handed-off conversation updates the relevant Views counts ("Needs a human" decrements once taken over).
Should NOT see:
- A "Take over" that doesn't actually stop the AI (must verify the AI is silent on that thread afterward).
- A take-over that pauses the AI globally for all conversations (it is per-conversation).
Reads/writes: per-conversation autopilot state; a status_change/handoff
re_interaction records the take-over (actor=human) and resume.
Success: take-over and resume durably change which side (AI vs human) answers the thread — confirmed by observing the AI's behaviour after each toggle.
4.5 — SMS outbound consent gate (CASL)
Should see / NOT see:
- An SMS reply/composer is consent-gated: an outbound SMS to a contact whose
SMS consent is
withdrawn/expired or who isdo_not_contactis blocked with the reason; the gate is enforced before send (re_sms_messages.consent_checked,re_consent_events, audit P0 #3/#6). An inbound STOP/HELP writes a consent withdrawal and is honored immediately (data-model.md§1.9). - The inbox never originates an unattended/cron SMS blast
(
feedback_never_cron_send_campaigns); a human presses Send per message.
Success: STOP suppresses the contact and the inbox refuses further outbound to them; a consented contact can be replied to normally.
4.6 — Context rail (linked record + Aria summary + quick actions)
Should see:
- A right context rail: Aria's summary (teal accent, the relationship recap for this contact), Contact facts (Stage / Score / Source / Agent), and Quick actions (Book a showing, Send comparables, Resume AI), plus a path to the full record.
- The rail reflects the same contact as the open thread (linked record), so the agent has full context without leaving the inbox.
Should NOT see:
- A context rail showing a different contact than the open thread.
- Commission/financials in the rail for an agent-scope viewer (RBAC, §3).
Success: the agent can answer with full context — summary + stage + history — in the same view as the conversation.
5. Empty / loading / error states
| State | Expected output |
|---|---|
| No conversations | Demo-seeded preview + onboarding nudge ("Your AI front office's chats and calls will appear here once your voice line / chat embed is live") — never a blank 3-pane (anti-pattern #6). |
| Empty view (0 in a filter) | Honest "Nothing in this view right now" with the filter shown + a clear-filter — not blank. |
| Loading | Skeleton list rows + skeleton thread, not a spinner (anti-pattern #7). |
| Thread load error | Inline "Couldn't load this conversation — retry"; panes 1–2 stay usable. |
| Voice line / chat not provisioned | The inbox renders with an honest "AI front office not yet connected — Calls/Chats not measured" (mirrors ai-front-desk.md §9), not fabricated conversations. |
| Suppressed contact / external_partner (read-only) role | Composer disabled with the reason (consent or role). |
6. Accessibility (AODA → WCAG 2.1 AA)
- Landmarks/regions: the three panes are navigable regions; the conversation
list is a list with selectable rows (
aria-selected); the thread is a log (role="log"/list) with author + time exposed to assistive tech. - Take-over state is announced (not color-only): "AI paused — you're in control" is text; the escalation system row reads as text + icon.
- Channel/language chips convey meaning by text, not color alone.
- Keyboard: move between panes, select a conversation, focus the composer, Take over / Resume, and Send — all keyboard-operable with visible focus; suggested-reply chips are buttons.
- Contrast: ≥4.5:1; teal action/links meet AA; gold accent only; the amber escalation row text meets AA over its tint.
- Motion: typing/skeleton animation respects
prefers-reduced-motion.
7. Acceptance checklist (QA runs on the deployed URL)
Behavioural verification on wiseaiagency.com (real host) against the demo
tenant (…0c01); never "build passes". Sample at ≥2 timepoints; assert change.
Any voice-path change additionally requires voice-agent-engineer review.
// 3-pane + unified list
test.fixme('three panes render: views/filters | conversation list | thread+context rail', () => {});
test.fixme('one list spans chat+voice+SMS with channel+language chips; AI-handled rows muted, escalated rows badged', () => {});
test.fixme('language filter options are the agent/tenant enabled languages, not a hardcoded set', () => {});
// Thread + escalation honesty
test.fixme('thread labels contact vs 🤖 Aria vs human; the escalation system row states WHY the AI stopped', () => {});
test.fixme('a voice conversation shows call summary + transcript (orig→EN) + recording link', () => {});
test.fixme('a suggested reply never asserts a value/legal/mortgage position (guardrails apply to suggestions)', () => {});
// Take over / resume (behaviour, not DOM)
test.fixme('Take over pauses the AI for THAT conversation only (AI silent on the thread after); header shows AI paused', () => {});
test.fixme('Resume AI hands control back; "Needs a human" count decrements on take-over (verify at 2 timepoints)', () => {});
test.fixme('global Autopilot default is overridable per conversation', () => {});
// Consent / sends
test.fixme('outbound SMS to a withdrawn/expired/do_not_contact contact is BLOCKED with a reason; STOP suppresses', () => {});
test.fixme('nothing auto-sends — a human presses Send; no cron/unattended SMS', () => {});
// Context rail + RBAC + safety
test.fixme('context rail shows the SAME contact as the thread (Aria summary + facts + Open record)', () => {});
test.fixme('agent-scope viewer sees no commission/financials in the inbox rail; external_partner (read-only) cannot send/take over', () => {});
test.fixme('a personal-crisis disclosure still triggers the universal 988/911/DV layer (not gated by the inbox)', () => {});
// States + a11y
test.fixme('empty/0-filter/loading/error/not-provisioned states render per §5 (never blank, never a spinner)', () => {});
test.fixme('panes are keyboard-navigable; take-over state announced as text; reduced-motion honored', () => {});
8. Guardrails for agents building against this spec
- Rule #17 not satisfied — do not build until founder approval.
- The inbox is the bridge made visible — escalations must state why, AI vs
human authorship must be labelled, take-over must cleanly pause the AI per
conversation, and crisis/safety must never be gated (
ai-front-desk.md§V7). - Voice = LIFE-SAFETY — any change touching the LiveKit agent / SIP /
moderation.pygoes throughvoice-agent-engineer+ founder go; this inbox is a read/take-over surface and must not weaken the universal safety layer. - Take-over is per-conversation, not global — verify the AI actually goes silent on the taken-over thread (behaviour, not DOM presence).
- SMS outbound is consent-gated; no unattended sends — a human presses Send; STOP/HELP withdraws consent immediately.
- Extend the existing inbox/runtime (
InboxTab.tsx, chat/voice) — do not rebuild (do-not-reinvent.md). - Migrate-before-use —
re_*/re_sms_messagesare proposals; verify columns viainformation_schema(Rule #18); founder-gated DDL. - Verify on the real host; behaviour not DOM presence; evidence-or-nothing.
- If code diverges, update the spec first (founder approval), then the code.
End of spec. STATUS: DRAFT — NOT APPROVED. Open items for Stage-2: (1) does the RE MVP include outbound SMS at all (the approved AI Front Desk MVP is email-first / inbound-only — confirm the SMS lane is in scope before building the consent-gated composer); (2) take-over notification (does taking over notify the contact "you're now with a person"?); (3) per-conversation autopilot persistence across sessions.