Files
sales-trainer/docs/engineering-log/2026-08-18-live-qa-ux-fixes.md

74 lines
4.9 KiB
Markdown

# 2026-08-18 — Live-QA UX fixes: JSON leak, complaint openers, auto-close chat
Date: 2026-08-18
Status: implemented + verification evidence; deploy is the repo's normal Gitea→EasyPanel path (operator runs it)
## Context
User tested the hardest-tier personas and reported three UX bugs from a live screenshot:
1. **Raw JSON leaked into the chat bubble.** A persona's reply rendered as
`{"reply". "สวัสดีค่ะ...อยากรู้ว่ามีโปรแกรมช่วยจัดการร้านขายเสื้อผ้ามั้ยคะ" "'decision": "none", "mood"' 0}`
instead of just the spoken sentence. Root cause: when the provider emits a
valid-looking reply **glued to trailing contract keys** (`"decision": ..., "mood": ...`)
with or without braces, the old `_parse_persona_reply` fallback could return that
non-protocol-looking-but-still-JSON text verbatim as the bubble.
2. **Hardest-tier opener was a complaint**, not a greeting ("ไม่มีเงินซื้อหรอก ของแบบนี้แพง" /
wrong_text opener `'never mind, forget it'`). The user wants openers to start with
"สวัสดี" / "สนใจ" — and the wrong_text persona should open normally, then only cool off
("wrong chat") AFTER the seller's first reply.
3. **No automatic close / summary + no visible "สรุปผล" button.** Guide mentioned pressing
"สรุปผล" but the user expected the chat to auto-close and auto-summarize when the customer
decides, with no manual summarize button.
## Changes
- `backend/app/services/simulator.py`
- Hardened `_parse_persona_reply`: added `_looks_like_protocol()` and
`_strip_trailing_protocol_noise()`. Any output carrying the contract's reserved keys
(`"reply"`/`"decision"`/`"mood"`, plain or quoted), an opening `{`/`[`/ fenced block, or a
quoted fragment + colon is now rejected → bounded retry → fail-closed (`LLMError`), never
rendered. Legitimate natural Thai/EN sentences (even with a time colon e.g. "09:00") still
pass clean; a lone quoted sentence unwraps to clean text.
- Per-turn judge decision vocabulary now includes `try` in addition to `buy`/`walk`/`pending`.
- `_special_instr` for `wrong_text`: opener is a normal greeting; ONLY the first reply to the
seller's opening may cool off (wrong chat / never mind).
- `backend/app/services/persona_prompts.py` — Rule 6 rewritten (wrong_text opens normally, cools
off after first reply) + new Rule 8 OPENER RULE: every opener, for every tier incl. wrong_text,
must be a natural polite greeting with interest/question — never a complaint/refusal.
- `backend/app/api/chat_routes.py``send_message` now auto-closes on `buy`/`walk`/`try`. On
`try` (customer decides to trial first + come back) it inserts a localized system note
("ลูกค้าตัดสินใจจะลองใช้สินค้า/บริการก่อน แล้วจะกลับมาติดต่ออีกครั้ง...") then finalizes
(close + judge summary) exactly like buy/walk.
- `frontend/src/views/Chat.vue` — removed the manual "สรุปผล"/Finish button, the `finishing`
state, and the `finish()` function; chat now auto-closes/summarizes via `chatSend``finished`.
- `frontend/src/i18n/index.js``chatGuideText` (TH + EN) updated: the chat closes automatically
when the customer decides (buy / walk / trial first), no instruction to press "สรุปผล".
## Tests
- `backend/tests/test_persona_reply_contract.py` +3: partial-protocol-no-braces never leaks
(retry then succeed), malformed-brace payload fails closed (`LLMError`), quoted-sentence
unwraps quotes.
- `backend/tests/test_auto_close_try.py` (new): route-level `chat/send` with a stubbed simulator
asserting `try`/`buy`/`walk` all auto-close (`finished=true`, `status=finished`, debrief
present) and that `try` inserts a "ลองใช้"/trial system note.
## Verification evidence
| Check | Result |
|---|---|
| Full backend pytest suite (fresh venv via `uv`) | **336 passed** (baseline 330 → +6) |
| Frontend `vite build` | clean |
| Frontend vitest unit | 4/4 pass |
| Static security scan of diff (+lines) | no secrets / shell / eval / pickle / SQL |
| Independent reviewer subagent (fail-closed, verified against actual files) | **passed** — parsing is regex/string only, test-fixture credentials recognized as non-secrets |
## Notes / assumptions
- The manual backend `chat/finish` endpoint and the `finish`/`finishing`/`finishConfirm` i18n keys
are left in place (unused by the UI) as a safe non-breaking safety net; no UI path calls them now.
- Sessions the customer has not decided on stay `active` and are resumable (per existing
resume-without-re-pick behavior); they only close when a decision is reached.
- Deploy remains the repo's normal Gitea→EasyPanel webhook path (≈3 min rebuild) — operator runs it.