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

5.7 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:

  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.pysend_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 chatSendfinished.
  • frontend/src/i18n/index.jschatGuideText (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.

Deployment follow-up (same day)

After the commit+push (ffb7cdd), a live check showed the deployed site still served the old bundle: the frontend index-*.js hash was unchanged and still contained the OLD guide text ("พอใจแล้วกด "สรุปผล"") and the old finish button, with none of the new strings. The Gitea→EasyPanel deploy webhook (hook id 3, active:true, push event) is configured, but the push did not propagate within ~10+ min. The exact reported leak payload was re-run through the NEW parser and extracts a clean reply (no leak), so the code fix is correct — the leak seen was the stale deploy. Action: force a redeploy (re-push to re-trigger the webhook, or redeploy from the EasyPanel console) and re-verify the live bundle contains the new auto-close guide text.