74 lines
4.9 KiB
Markdown
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.
|