4.9 KiB
4.9 KiB
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:
- 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_replyfallback could return that non-protocol-looking-but-still-JSON text verbatim as the bubble. - 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. - 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
tryin addition tobuy/walk/pending. _special_instrforwrong_text: opener is a normal greeting; ONLY the first reply to the seller's opening may cool off (wrong chat / never mind).
- Hardened
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_messagenow auto-closes onbuy/walk/try. Ontry(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, thefinishingstate, and thefinish()function; chat now auto-closes/summarizes viachatSend→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-levelchat/sendwith a stubbed simulator assertingtry/buy/walkall auto-close (finished=true,status=finished, debrief present) and thattryinserts 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/finishendpoint and thefinish/finishing/finishConfirmi18n 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
activeand 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.