From ab5fff0bd97b90e78e9baf68bdd7d68b657a4e98 Mon Sep 17 00:00:00 2001 From: Macky Date: Wed, 19 Aug 2026 07:26:21 +0700 Subject: [PATCH] [verified] IP-protect pain (hide from all roles) + persona summary button + topbar user dropdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Backend: strip pain/painProgress/revealed_persona.pains from debrief serializer and remove pain/initialPainFit from admin report so the coaching formula never leaks to any user-facing role (persona keeps pains internally to drive the judge/training) - Fix [object Object] array-of-objects rendering in Chat/SessionDetail - Personas: trained persona shows 'สรุปผล/Summary' button -> chat page with past result (chat already loads finished session) - Top bar: username dropdown containing Settings + Logout (was separate logout button); variant-from-base already preserves tier/difficulty - Updated debrief allowlist tests to reflect new redaction --- backend/app/api/chat_routes.py | 17 +----- backend/app/services/report.py | 12 +--- backend/tests/test_final_judge.py | 6 +- backend/tests/test_sprint1_review_findings.py | 8 ++- frontend/src/App.vue | 57 +++++++++++++++---- frontend/src/i18n/index.js | 2 + frontend/src/views/Chat.vue | 17 +++++- frontend/src/views/Personas.vue | 4 ++ frontend/src/views/SessionDetail.vue | 5 +- 9 files changed, 82 insertions(+), 46 deletions(-) diff --git a/backend/app/api/chat_routes.py b/backend/app/api/chat_routes.py index 7871655..a75d6c3 100644 --- a/backend/app/api/chat_routes.py +++ b/backend/app/api/chat_routes.py @@ -108,30 +108,19 @@ def _safe_judge_debrief(verdict: object, outcome: str, persona: dict) -> dict: score = max(0, min(100, int(raw.get("score", 0)))) except (TypeError, ValueError): score = 0 - progress = {} - raw_progress = raw.get("painProgress") - if isinstance(raw_progress, dict): - for key, value in list(raw_progress.items())[:20]: - if not isinstance(key, str): - continue - try: - progress[key[:100]] = max(0, min(100, int(value))) - except (TypeError, ValueError): - continue return { "outcome": outcome, "score": score, - "pain": _bounded_text(raw.get("pain")), + # IP protection: do NOT surface the customer's pain (prose or raw list) to any + # user-facing role. Pain drives the judge/training internally but is the core + # "formula" of the coaching product, so it must not leak through the debrief. "why": _bounded_text(raw.get("why")), "failurePoints": _string_list(raw.get("failurePoints")), "coaching": _string_list(raw.get("coaching")), - "painProgress": progress, "revealed_persona": { - "pains": persona.get("pains", []), "income": persona.get("income", ""), "personality": persona.get("personality", ""), "budget": persona.get("budget", ""), - "negotiation_levers": persona.get("negotiation_levers", []), "opener": persona.get("opener", ""), "background": persona.get("background", ""), }, diff --git a/backend/app/services/report.py b/backend/app/services/report.py index 5ad2f01..d017815 100644 --- a/backend/app/services/report.py +++ b/backend/app/services/report.py @@ -51,10 +51,7 @@ def _render_sales_kit(kit: dict[str, Any], thai: bool) -> str: ta = kit.get("targetAudience") or {} if ta.get("segment"): lines.append(f"**{'กลุ่มเป้าหมาย' if thai else 'Target segment'}:** {ta['segment']}") - if kit.get("initialPainFit"): - lines.append(f"**{'Pain ที่สินค้าแก้ได้เบื้องต้น' if thai else 'Initial pain-fit'}:**") - for p in kit["initialPainFit"]: - lines.append(f"- ({p.get('fit', '?')}) {p.get('pain', '')}") + # IP protection: hide pain-fit details from the admin report (the "formula"). return "\n".join(lines) @@ -79,13 +76,6 @@ def _render_persona(p: dict[str, Any], thai: bool) -> str: lines.append(f"- {'รายได้' if thai else 'Income'}: {p.get('income', '-')} | " f"{'ไลฟ์สไตล์' if thai else 'Lifestyle'}: {p.get('lifestyle', '-')}") lines.append(f"- {'นิสัย' if thai else 'Personality'}: {p.get('personality', '-')}") - if p.get("pains"): - lines.append(f"- {'Pain points (latent)' if thai else 'Pains (latent)'}:") - for pain in p.get("pains", []): - conds = "; ".join(pain.get("resolutionConditions", [])) if isinstance(pain, dict) else "" - lines.append(f" - [{pain.get('fit', '?') if isinstance(pain, dict) else '?'}] " - f"{pain.get('name', pain) if isinstance(pain, dict) else pain}" - f"{' — resolve: ' + conds if conds else ''}") if p.get("negotiation_levers"): levers = p.get("negotiation_levers") or [] lines.append(f"- {'ต่อรอง' if thai else 'Negotiation levers'}: " + ", ".join(str(x) for x in levers)) diff --git a/backend/tests/test_final_judge.py b/backend/tests/test_final_judge.py index cacb872..ea57913 100644 --- a/backend/tests/test_final_judge.py +++ b/backend/tests/test_final_judge.py @@ -81,9 +81,11 @@ def test_final_judge_supplies_score_and_debrief_for_automatic_buy(monkeypatch): assert updated["status"] == "finished" assert updated["outcome"] == "won" assert debrief["score"] == 88 - assert debrief["pain"] == "qualified pain" assert debrief["coaching"] == ["Keep the close concise"] - assert debrief["revealed_persona"]["pains"] + # IP protection: pain is hidden from the debrief (prose + raw list). + assert "pain" not in debrief + assert "painProgress" not in debrief + assert "pains" not in debrief["revealed_persona"] assert sessions.updates[0]["messages"] diff --git a/backend/tests/test_sprint1_review_findings.py b/backend/tests/test_sprint1_review_findings.py index fdf825f..f239829 100644 --- a/backend/tests/test_sprint1_review_findings.py +++ b/backend/tests/test_sprint1_review_findings.py @@ -148,12 +148,14 @@ def test_judge_debrief_uses_closed_allowlist(): ) assert set(debrief) == { - "outcome", "score", "pain", "why", "failurePoints", "coaching", - "painProgress", "revealed_persona", + "outcome", "score", "why", "failurePoints", "coaching", "revealed_persona", } assert debrief["score"] == 100 assert debrief["failurePoints"] == ["missed discovery"] - assert debrief["painProgress"] == {"main": 100} + # IP protection: pain (prose + list) and painProgress are stripped from the debrief. + assert "pain" not in debrief + assert "painProgress" not in debrief + assert "pains" not in debrief["revealed_persona"] assert "password_hash" not in str(debrief) assert "provider_path" not in str(debrief) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index ced4cc5..96707db 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -15,10 +15,19 @@
@@ -29,24 +38,32 @@