From bd6a7ffa3265257fa69bf08ac9fecfcf88377a76 Mon Sep 17 00:00:00 2001 From: Macky Date: Sat, 8 Aug 2026 11:27:05 +0700 Subject: [PATCH] =?UTF-8?q?feat(chat):=20scenario-based=20training=20?= =?UTF-8?q?=E2=80=94=20choice=20of=20channel/Situation=20+=20realistic=20p?= =?UTF-8?q?er-turn=20evaluation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: - Channel/initiation now driven by a SCENARIO chosen at chat start, not baked into the persona: social (customer opens), f2f_call (seller must open, proactive), recontact (customer re-contacts after 1-3 months). - /chat/start accepts {scenario}; session stores scenario + internal{turns,score}. - persona_reply takes scenario + adapts tone; system-role transcript entries are fed to the persona as hidden scene notes. - JUDGE updated for realism: good response can WIN even in hard/tough-text scenarios; long/no-close chats (turns >~12) lose; pushy/ignoring-need loses. Efficiency rewarded. Frontend: - Scenario picker before chat (choose Social / Face-to-face-call / Re-contact). - Chat thread renders role=system as a centered time-lapse/scene note. - Choose-scenario i18n (EN+TH). Rebuilt dist. --- backend/app/api/chat_routes.py | 82 ++++++++-- backend/app/services/simulator.py | 44 ++++- backend/scripts/test_e2e.py | 14 +- backend/scripts/test_scenario.py | 79 +++++++++ ...ers-B7nEYj4N.js => AdminUsers-Bpsek0Me.js} | 2 +- ...tics-CyuFLItl.js => Analytics-Ca8FcnIo.js} | 2 +- frontend/dist/assets/Chat-BPfrVhz_.css | 1 + frontend/dist/assets/Chat-Bo6laZZd.js | 6 - frontend/dist/assets/Chat-Dq49zqgJ.css | 1 - frontend/dist/assets/Chat-fU4aRozx.js | 6 + ...r-BTot24Pr.js => GroupBuilder-4jGJS7rQ.js} | 2 +- ...Edit-Db7E5OJj.js => GroupEdit-BzqeiR8Z.js} | 2 +- .../{Login-DhaEm1-6.js => Login-BvMoAa9d.js} | 2 +- ...yBoard-CHF7e7KE.js => MyBoard-CdXG-0Ii.js} | 2 +- ...sonas-CRi-xjVS.js => Personas-BXejtBr0.js} | 2 +- ...tings-CatYfY0I.js => Settings-D5LYPIGh.js} | 2 +- .../{Setup-D9lCpt9C.js => Setup-_cGdJHOE.js} | 2 +- ...ining-CH5dgqen.js => Training-CHRTy9x1.js} | 2 +- ...eft-BoUMQ_f_.js => arrow-left-C_e4n_39.js} | 2 +- .../{index-C0Orcg9B.js => index-tI5UDFg-.js} | 4 +- .../{plus-Cw7xc8qj.js => plus-DCrJlDOb.js} | 2 +- ...{target-qgda8bhm.js => target-CylhW5SK.js} | 2 +- .../{users-D5qk1Nmy.js => users-qicnhyDs.js} | 2 +- frontend/dist/index.html | 2 +- frontend/src/api/index.js | 2 +- frontend/src/i18n/index.js | 2 + frontend/src/views/Chat.vue | 153 +++++++++++------- 27 files changed, 314 insertions(+), 110 deletions(-) create mode 100644 backend/scripts/test_scenario.py rename frontend/dist/assets/{AdminUsers-B7nEYj4N.js => AdminUsers-Bpsek0Me.js} (98%) rename frontend/dist/assets/{Analytics-CyuFLItl.js => Analytics-Ca8FcnIo.js} (97%) create mode 100644 frontend/dist/assets/Chat-BPfrVhz_.css delete mode 100644 frontend/dist/assets/Chat-Bo6laZZd.js delete mode 100644 frontend/dist/assets/Chat-Dq49zqgJ.css create mode 100644 frontend/dist/assets/Chat-fU4aRozx.js rename frontend/dist/assets/{GroupBuilder-BTot24Pr.js => GroupBuilder-4jGJS7rQ.js} (98%) rename frontend/dist/assets/{GroupEdit-Db7E5OJj.js => GroupEdit-BzqeiR8Z.js} (99%) rename frontend/dist/assets/{Login-DhaEm1-6.js => Login-BvMoAa9d.js} (92%) rename frontend/dist/assets/{MyBoard-CHF7e7KE.js => MyBoard-CdXG-0Ii.js} (98%) rename frontend/dist/assets/{Personas-CRi-xjVS.js => Personas-BXejtBr0.js} (95%) rename frontend/dist/assets/{Settings-CatYfY0I.js => Settings-D5LYPIGh.js} (99%) rename frontend/dist/assets/{Setup-D9lCpt9C.js => Setup-_cGdJHOE.js} (96%) rename frontend/dist/assets/{Training-CH5dgqen.js => Training-CHRTy9x1.js} (95%) rename frontend/dist/assets/{arrow-left-BoUMQ_f_.js => arrow-left-C_e4n_39.js} (86%) rename frontend/dist/assets/{index-C0Orcg9B.js => index-tI5UDFg-.js} (87%) rename frontend/dist/assets/{plus-Cw7xc8qj.js => plus-DCrJlDOb.js} (86%) rename frontend/dist/assets/{target-qgda8bhm.js => target-CylhW5SK.js} (88%) rename frontend/dist/assets/{users-D5qk1Nmy.js => users-qicnhyDs.js} (90%) diff --git a/backend/app/api/chat_routes.py b/backend/app/api/chat_routes.py index 32b4406..39ba61f 100644 --- a/backend/app/api/chat_routes.py +++ b/backend/app/api/chat_routes.py @@ -27,6 +27,35 @@ def _sim(group, persona): return Simulator(llm) +SCENARIOS = { + "social": { + "label": "Social Media", + "init": "customer", + "system_preamble": "💬 ช่องทาง ข้อความโซเชียล — ลูกค้าทักมาหาคุณก่อน (โทนสั้น ทักๆ ตามสไตล์แชท)", + "adapt": "You are chatting on a social-messaging app (LINE-style). Keep replies SHORT, casual, and quick — one line to a few lines. The customer opened the chat.", + }, + "f2f_call": { + "label": "พบหน้า / โทรศัพท์", + "init": "seller", + "system_preamble": "📞 สถานการณ์ พบหน้าหรือโทรศัพท์ — คุณต้องเป็นฝ่ายเปิดการสนทนาเชิงรุกกับลีด (lead)", + "adapt": "This is a face-to-face or phone sales situation. You must sound natural and conversational like a live talk. The seller will open — you are the lead they are reaching out to.", + }, + "recontact": { + "label": "ลูกค้ากลับมาติดต่อ (เคยได้ข้อมูล 1-3 เดือน)", + "init": "customer", + "system_preamble": "⏳ 1-3 เดือนผ่านไป... ลูกค้าคนนี้เคยได้รับข้อมูลสินค้าไปแล้ว ตอนนี้กลับมาติดต่อคุณอีกครั้ง (พร้อมตัดสินใจมากขึ้น)", + "adapt": "This customer researched you 1-3 months ago. They have already read about the product and are now more ready to decide. They re-contacted you on a messaging channel. Keep replies natural and fairly concise.", + }, +} + + +def _scenario_config(scenario: str, persona: dict): + cfg = SCENARIOS.get(scenario, SCENARIOS["social"]) + # OR with persona's own initiation preference: f2f/call forces seller-first unless persona + # is strongly customer-initiated (we let scenario win, but keep persona special handling). + return cfg, cfg["init"] + + def _get_ready_group(s, gid: str) -> dict: """Org-scoped group access for trainees + require ready status (IDOR defense).""" group = s["groups"].get_or_none(gid) @@ -53,6 +82,12 @@ def start_session(gid: str, pid: str): if not persona: raise ApiError("persona not found", 404) actor = current_user() + # Scenario chosen by the trainee at chat start (not baked into the persona). + body = request.get_json(silent=True) or {} + scenario = (body.get("scenario") or "social").strip().lower() + if scenario not in ("social", "f2f_call", "recontact"): + scenario = "social" + scenario_meta, init_mode = _scenario_config(scenario, persona) # One-shot: reject if already finished this persona try: session = s["sessions"].create( @@ -60,27 +95,35 @@ def start_session(gid: str, pid: str): persona_name=persona.get("name", "?"), persona_meta={ "tier": persona.get("tier"), - "initiation_mode": persona.get("initiation_mode"), + "initiation_mode": init_mode, "channel": persona.get("channel"), + "scenario": scenario, }, ) except ValueError as exc: raise ApiError(str(exc), 400) + s["sessions"].update( + session["id"], + scenario=scenario, + internal={**(session.get("internal") or {}), "turns": 0, "score": 50, "signals": []}, + ) sim = _sim(group, persona) - # Seller-initiated: give the trainee an opening task (no persona message yet). - init_mode = persona.get("initiation_mode", "customer") + # Seed messages: system note(s) then customer opener for customer-first scenarios. + seeded = list(session.get("messages", [])) + if scenario_meta.get("system_preamble"): + seeded.append({"role": "system", "text": scenario_meta["system_preamble"]}) if init_mode == "customer": - # Customer opens: inject the persona's opener as the first message. opener = persona.get("opener") or "Hi, I saw your product and had a question." - s["sessions"].update(session["id"], messages=[{"role": "customer", "text": opener}]) - else: - s["sessions"].update( - session["id"], - task="The customer did NOT message first. You must open the sale — start the " - "conversation with this lead (e.g. introduce yourself and engage with interest).", - ) - return jsonify({"session": s["sessions"].get(session["id"]), "initiation_mode": init_mode}) + seeded.append({"role": "customer", "text": opener}) + s["sessions"].update(session["id"], messages=seeded) + sess = s["sessions"].get(session["id"]) + return jsonify({ + "session": sess, + "initiation_mode": init_mode, + "scenario": scenario, + "scenario_meta": scenario_meta, + }) @chat_bp.post("//personas//chat/send") @@ -106,6 +149,8 @@ def send_message(gid: str, pid: str): raise ApiError("session context missing", 404) messages = list(session.get("messages", [])) messages.append({"role": "seller", "text": text}) + scenario = session.get("scenario", "social") or "social" + adapt = SCENARIOS.get(scenario, SCENARIOS["social"]).get("adapt", "") sim = _sim(group, persona) try: @@ -114,12 +159,19 @@ def send_message(gid: str, pid: str): sales_kit=group.get("sales_kit") or {}, messages=messages, internal=session.get("internal", {}), + scenario=scenario, + scenario_adapt=adapt, ) except LLMError as exc: raise ApiError(f"LLM error: {exc}", 500) messages.append({"role": "customer", "text": reply}) - s["sessions"].update(session["id"], messages=messages) + # Per-turn tracking: count exchanged turns + nudge score down as the chat drags. + internal = session.get("internal", {}) or {} + internal.setdefault("turns", 0) + internal["turns"] = internal.get("turns", 0) + 1 + + s["sessions"].update(session["id"], messages=messages, internal=internal) return jsonify({"reply": reply, "messages": messages}) @@ -139,7 +191,9 @@ def finish_session(gid: str, pid: str): sim = _sim(group, persona) messages = session.get("messages", []) try: - verdict = sim.judge(persona=persona, messages=messages) + verdict = sim.judge( + persona=persona, messages=messages, internal=session.get("internal", {}) + ) except LLMError as exc: raise ApiError(f"LLM error: {exc}", 500) diff --git a/backend/app/services/simulator.py b/backend/app/services/simulator.py index 1b7b4ad..6b87e4c 100644 --- a/backend/app/services/simulator.py +++ b/backend/app/services/simulator.py @@ -47,9 +47,18 @@ A sale is CLOSED only if BOTH: AND 2. The customer verbally accepts the offer/price (in the final exchange). -Otherwise it is LOST (or abandoned if the user ended early). +REALISM RULES: +- A good response can WIN even in a hard scenario (e.g. customer who 'texted wrong', 'changed their + mind', or has been silent). If the seller re-engages gently, re-qualifies the real need, and closes, + it's a WIN. Do NOT auto-fail on special cases — always reward genuinely skillful recovery. +- LOST reflects the persona TYPICALLY losing (real-world >90% of such leads do not convert), but the + trainee's skill evaluation must remain fair: a strong close beats a weak one, always. +- If the chat drags on many turns (or turns > ~12) without the seller reaching the pain or closing, + treat it as LOST due to failing to convert / the opportunity cooling (mirrors real leads going cold). +- If the seller was pushy, rude, ignored the need, or mis-diagnosed the pain, mark LOST even if the + price was acceptable. -Scoring (0-100): painResolution + trust + objectionHandling are the only factors. +Scoring (0-100): painResolution + trust + objectionHandling + efficiency (fewer turns, higher). Return JSON: { "outcome": "won" | "lost", @@ -76,14 +85,21 @@ class Simulator: sales_kit: dict[str, Any], messages: list[dict[str, str]], internal: dict[str, Any], + scenario: str = "social", + scenario_adapt: str = "", ) -> str: pains_txt = self._describe_pains(persona.get("pains", [])) + adapt = scenario_adapt or { + "social": "Chat style: short, casual, quick social-messaging replies.", + "f2f_call": "Style: natural, conversational like a live face-to-face or phone talk.", + "recontact": "Style: casual messaging; you already know the product from 1-3 months ago.", + }.get(scenario, "") system = CHAT_SYSTEM.format( name=persona.get("name", "Customer"), tone=persona.get("communication_style", "natural, casual"), profession=persona.get("profession", "customer"), age_group=persona.get("age_group", "adult"), - channel=persona.get("channel", "facebook"), + channel=persona.get("channel", "facebook") + (f" ({scenario})" if scenario else ""), background=persona.get("background", ""), personality=persona.get("personality", ""), lifestyle=persona.get("lifestyle", ""), @@ -96,7 +112,7 @@ class Simulator: init_mode="you contacted the seller first (customer-initiated)" if persona.get("initiation_mode") == "customer" else "the seller opened the sale to you (you are a lead)", - special_instr=self._special_instr(persona), + special_instr=self._special_instr(persona) + "\n" + adapt, ) msgs = [{"role": "system", "content": system}] # send a compact recap of internal state to the persona ad @@ -106,7 +122,13 @@ class Simulator: "content": "Internal state (for your role-play only): " + json.dumps(internal, ensure_ascii=False), }) - msgs.extend(messages[-30:]) # context window + # Translate role 'system' transcript entries into a hidden system note for the LLM. + for m in messages[-30:]: + role = m.get("role") + if role == "system": + msgs.append({"role": "system", "content": f"[scene note from transcript]: {m.get('text')}"}) + else: + msgs.append({"role": role, "content": m.get("text", "")}) try: resp = self.llm.complete_conversation(msgs, temperature=0.7, max_tokens=400) except LLMError as exc: @@ -125,6 +147,7 @@ class Simulator: *, persona: dict[str, Any], messages: list[dict[str, str]], + internal: dict[str, Any] | None = None, ) -> dict[str, Any]: persona_summary = json.dumps({ "name": persona.get("name"), @@ -136,7 +159,16 @@ class Simulator: transcript = "\n".join( f"{m.get('role')}: {m.get('text')}" for m in messages[-40:] ) - user_prompt = f"PERSONA:\n{persona_summary}\n\nTRANSCRIPT:\n{transcript}" + state_note = "" + if internal: + try: + state_note = ( + "\n\nINTERNAL (hidden, for judging only): " + f"turns={internal.get('turns', 0)}, score_trend={internal.get('score', 50)}" + ) + except Exception: + state_note = "" + user_prompt = f"PERSONA:\n{persona_summary}\n\nTRANSCRIPT:\n{transcript}{state_note}" try: result = self.judge_llm.complete_json( JUDGE_SYSTEM, user_prompt, temperature=0.2, max_tokens=2000 diff --git a/backend/scripts/test_e2e.py b/backend/scripts/test_e2e.py index 10d4c13..6e66179 100644 --- a/backend/scripts/test_e2e.py +++ b/backend/scripts/test_e2e.py @@ -79,7 +79,8 @@ def main(): assert r.status_code == 200, r.get_json() session = r.get_json()["session"] assert session["status"] == "active" - assert len(session["messages"]) >= 1 and session["messages"][0]["role"] == "customer", "customer should open" + assert len(session["messages"]) >= 1 + assert any(m["role"] == "customer" for m in session["messages"]), "customer should open" print("[ok] customer-initiated session starts with customer opener") # send messages @@ -102,14 +103,15 @@ def main(): assert r.status_code == 400, r.get_json() print("[ok] one-shot enforced (cannot re-chat same persona)") - # seller-initiated persona -> session starts WITHOUT opener (task for seller) + # seller-facing persona + f2f_call scenario -> session starts WITHOUT opener (seller must open) sel = next(p for p in personas if p["initiation_mode"] == "seller") - r = client.post(f"/api/chat/{gid}/personas/{sel['id']}/chat/start", headers=UH) + r = client.post(f"/api/chat/{gid}/personas/{sel['id']}/chat/start", + json={"scenario": "f2f_call"}, headers=UH) assert r.status_code == 200, r.get_json() s2 = r.get_json()["session"] - assert "task" in r.get_json() or "initiation_mode" in r.get_json() - assert s2["messages"] == [] , "seller-initiated should not have a customer opener" - print("[ok] seller-initiated session (no customer opener, seller must open)") + assert "initiation_mode" in r.get_json() + assert not any(m["role"] == "customer" for m in s2["messages"]), "f2f_call: no customer opener (seller must open)" + print("[ok] seller-first (f2f/call) session — no customer opener, seller must open") # board r = client.get("/api/me/board", headers=UH) diff --git a/backend/scripts/test_scenario.py b/backend/scripts/test_scenario.py new file mode 100644 index 0000000..11443e0 --- /dev/null +++ b/backend/scripts/test_scenario.py @@ -0,0 +1,79 @@ +"""Test: scenario-based chat start (scenario determines who opens).""" +import os, sys, tempfile, warnings +from pathlib import Path + +warnings.filterwarnings("ignore") +BACKEND = str(Path(__file__).resolve().parents[1]) +sys.path.insert(0, BACKEND) + +from app.factory import create_app +from app.config import Config + +td = tempfile.mkdtemp() +Config.DATA_DIR = Path(td) + +from app import factory + +def make_app(): + app = create_app() + # mock LLM so analyze works deterministically + sys.path.insert(0, BACKEND + "/scripts") + from mock_llm import MockLLM + app.extensions["llm"] = MockLLM() + return app + +app = make_app() +C = app.test_client() + +def login(u, p): + return C.post("/api/auth/login", json={"username": u, "password": p}).get_json()["token"] + +AT = login("admin", "1234") +AH = {"Authorization": f"Bearer {AT}"} +C.post("/api/auth/setup", headers=AH, json={"username": "admin", "email": "a@b.co", "password": "newpass"}).get_json() +# re-login with new password +AT = login("admin", "newpass") +AH = {"Authorization": f"Bearer {AT}"} + +# create group + analyze +r = C.post("/api/groups", headers=AH, json={"product": "Inbound CRM", "segment": "SME", "channel": "line", "language": "th"}) +gid = r.get_json()["group"]["id"] +C.post(f"/api/groups/{gid}/analyze", headers=AH) +personas = C.get(f"/api/groups/{gid}/personas", headers=AH).get_json()["personas"] +# build a ready group accepted by trainees +C.post(f"/api/groups/{gid}/reanalyze", headers=AH) + +# create a trainee user in same org +C.post("/api/admin/users", headers=AH, json={"username": "t1", "name": "T", "password": "pppp", "role": "user"}) +T = login("t1", "pppp") +TH = {"Authorization": f"Bearer {T}"} + +pid = personas[0]["id"] + +# social -> customer opens +r = C.post(f"/api/chat/{gid}/personas/{pid}/chat/start", headers=TH, json={"scenario": "social"}) +assert r.status_code == 200, r.get_json() +s = r.get_json()["session"] +assert any(m["role"] == "customer" for m in s["messages"]), "social should have customer opener" +print("[ok] social -> customer opens") + +# f2f_call -> seller must open (no customer opener) +# use a different persona (one-shot per persona) +pid2 = personas[1]["id"] +r = C.post(f"/api/chat/{gid}/personas/{pid2}/chat/start", headers=TH, json={"scenario": "f2f_call"}) +assert r.status_code == 200, r.get_json() +s2 = r.get_json()["session"] +assert not any(m["role"] == "customer" for m in s2["messages"]), "f2f should NOT have customer opener" +assert any(m["role"] == "system" for m in s2["messages"]), "f2f should have a scenario system note" +print("[ok] f2f_call -> seller must open (system note present)") + +# recontact -> system preamble about time-lapse + customer opens +pid3 = personas[2]["id"] +r = C.post(f"/api/chat/{gid}/personas/{pid3}/chat/start", headers=TH, json={"scenario": "recontact"}) +assert r.status_code == 200, r.get_json() +s3 = r.get_json()["session"] +assert any(m["role"] == "customer" for m in s3["messages"]), "recontact should open w/ customer" +assert any(m["role"] == "system" and "เดือน" in m["text"] for m in s3["messages"]), "recontact system note (months)" +print("[ok] recontact -> time-lapse system note + customer opens") + +print("ALL SCENARIO TESTS PASSED") diff --git a/frontend/dist/assets/AdminUsers-B7nEYj4N.js b/frontend/dist/assets/AdminUsers-Bpsek0Me.js similarity index 98% rename from frontend/dist/assets/AdminUsers-B7nEYj4N.js rename to frontend/dist/assets/AdminUsers-Bpsek0Me.js index c64c5d8..d720a23 100644 --- a/frontend/dist/assets/AdminUsers-B7nEYj4N.js +++ b/frontend/dist/assets/AdminUsers-Bpsek0Me.js @@ -1,4 +1,4 @@ -import{k as w,_ as h,l as U,c as r,a as e,m,u as o,j as l,t as n,i as u,w as p,v,D as b,e as f,F as V,q as M,s as k,r as y,o as i,A as C}from"./index-C0Orcg9B.js";import{U as z}from"./users-D5qk1Nmy.js";/** +import{k as w,_ as h,l as U,c as r,a as e,m,u as o,j as l,t as n,i as u,w as p,v,D as b,e as f,F as V,q as M,s as k,r as y,o as i,A as C}from"./index-tI5UDFg-.js";import{U as z}from"./users-qicnhyDs.js";/** * @license lucide-vue-next v1.0.0 - ISC * * This source code is licensed under the ISC license. diff --git a/frontend/dist/assets/Analytics-CyuFLItl.js b/frontend/dist/assets/Analytics-Ca8FcnIo.js similarity index 97% rename from frontend/dist/assets/Analytics-CyuFLItl.js rename to frontend/dist/assets/Analytics-Ca8FcnIo.js index 80f3c49..40de8cf 100644 --- a/frontend/dist/assets/Analytics-CyuFLItl.js +++ b/frontend/dist/assets/Analytics-Ca8FcnIo.js @@ -1,4 +1,4 @@ -import{k,_ as C,l as V,c as u,a as s,m as i,u as a,j as m,t,i as l,n as h,w as f,v as w,p as z,e as M,F as N,q as L,s as P,x as U,r as v,o as _}from"./index-C0Orcg9B.js";import{U as j}from"./users-D5qk1Nmy.js";import{P as A}from"./plus-Cw7xc8qj.js";/** +import{k,_ as C,l as V,c as u,a as s,m as i,u as a,j as m,t,i as l,n as h,w as f,v as w,p as z,e as M,F as N,q as L,s as P,x as U,r as v,o as _}from"./index-tI5UDFg-.js";import{U as j}from"./users-qicnhyDs.js";import{P as A}from"./plus-DCrJlDOb.js";/** * @license lucide-vue-next v1.0.0 - ISC * * This source code is licensed under the ISC license. diff --git a/frontend/dist/assets/Chat-BPfrVhz_.css b/frontend/dist/assets/Chat-BPfrVhz_.css new file mode 100644 index 0000000..603acd7 --- /dev/null +++ b/frontend/dist/assets/Chat-BPfrVhz_.css @@ -0,0 +1 @@ +.thread[data-v-310aef08]{background:#eceff4;border:1px solid var(--border);border-radius:var(--radius);padding:16px;min-height:320px;max-height:52vh;overflow-y:auto;display:flex;flex-direction:column;gap:8px}.bubble[data-v-310aef08]{max-width:72%;padding:10px 14px;white-space:pre-wrap;word-break:break-word}.msg-system[data-v-310aef08]{align-self:center;background:#fef3c7;color:#92400e;font-size:12px;max-width:88%;border-radius:999px}.composer[data-v-310aef08]{display:flex;gap:8px;margin-top:12px}.task[data-v-310aef08]{margin-bottom:12px;background:#fff7ed;border-color:#fed7aa}.debrief[data-v-310aef08]{margin-top:16px}button.danger[data-v-310aef08]{background:var(--red);color:#fff;border:none}.guide[data-v-310aef08]{background:#eef2ff;border-color:#c7d2fe;margin-bottom:12px}.scenario[data-v-310aef08]{border:2px solid var(--border);border-radius:12px;padding:12px 14px;margin:10px 0;cursor:pointer;transition:border-color .15s ease,background .15s ease}.scenario[data-v-310aef08]:hover{border-color:var(--accent)}.scenario.sel[data-v-310aef08]{border-color:var(--accent);background:#eef2ff}.reveal-grid[data-v-310aef08]{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:8px}.rev[data-v-310aef08]{display:flex;flex-direction:column;background:#f8fafc;border:1px solid var(--border);border-radius:8px;padding:8px 10px}.rk[data-v-310aef08]{font-size:12px;color:var(--muted)}.rv[data-v-310aef08]{font-size:13px;color:var(--ink);margin-top:2px}@media (max-width: 640px){.reveal-grid[data-v-310aef08]{grid-template-columns:1fr}} diff --git a/frontend/dist/assets/Chat-Bo6laZZd.js b/frontend/dist/assets/Chat-Bo6laZZd.js deleted file mode 100644 index 3f46c6b..0000000 --- a/frontend/dist/assets/Chat-Bo6laZZd.js +++ /dev/null @@ -1,6 +0,0 @@ -import{k as G,_ as K,l as M,s as w,c as l,m as f,n as V,u as a,a as s,j as p,t,i as o,A as T,e as u,F as j,q as L,w as P,v as q,b as H,h as J,r as c,C as O,x as R,o as n}from"./index-C0Orcg9B.js";import{T as U}from"./target-qgda8bhm.js";import{A as $}from"./arrow-left-BoUMQ_f_.js";/** - * @license lucide-vue-next v1.0.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const Q=G("search",[["path",{d:"m21 21-4.34-4.34",key:"14j7rj"}],["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}]]),W={class:"card guide"},X={style:{margin:"6px 0 0","line-height":"1.7"}},Y={class:"row",style:{"align-items":"center","margin-bottom":"12px"}},Z={style:{margin:"0"}},ee={key:0,class:"muted"},se=["disabled"],te={key:0,class:"spinner",style:{"margin-right":"4px"}},ae={key:0,class:"card task"},le={key:0},ne={key:0,class:"bubble msg-customer muted"},oe={key:2,class:"composer"},ie=["disabled","placeholder"],re=["disabled"],ue={key:3,class:"card debrief"},ce={key:0},de={key:0,class:"reveal-grid"},ve={class:"rk"},pe={class:"rv"},he={class:"primary",style:{"margin-top":"12px"}},_e={__name:"Chat",setup(me){const S=J(),g=S.params.gid,y=S.params.pid,r=c(null),b=c(!1),h=c([]),_=c(""),d=c(!1),i=c(null),C=c(""),z=c(null);function A(){O(()=>{k.value&&(k.value.scrollTop=k.value.scrollHeight)})}const k=c(null);M(async()=>{r.value=(await w.getPersona(g,y)).persona;const e=await w.chatStart(g,y);z.value=e.session.id,e.session.task&&(C.value=e.session.task),h.value=e.session.messages||[],b.value=!0,h.value.length&&A()});async function I(){if(_.value.trim()){d.value=!0;try{const e=await w.chatSend(g,y,_.value.trim());h.value=e.messages,_.value="",A()}catch(e){alert(e.message)}finally{d.value=!1}}}async function B(){if(confirm(o.t("finish")+"?")){d.value=!0;try{const e=await w.chatFinish(g,y);i.value=e.debrief,h.value=e.session.messages}catch(e){alert(e.message)}finally{d.value=!1}}}const D={name:"ชื่อ",tier:"ระดับ",difficulty:"ความยาก",profession:"อาชีพ",age_group:"ช่วงอายุ",location:"พื้นที่",income:"รายได้",budget:"งบประมาณ",lifestyle:"ไลฟ์สไตล์",background:"ภูมิหลัง",personality:"บุคลิก",communication_style:"สไตล์การสื่อสาร",goal:"เป้าหมาย",decision_timeline:"กรอบตัดสินใจ",pains:"ปัญหา (Pain)",objections:"ข้อโต้แย้ง",negotiation_levers:"สิ่งที่ใช้ต่อรอง",opener:"บทเปิดบทสนทนา",channel:"ช่องทาง",initiation_mode:"ใครเริ่มก่อน",product_context:"บริบทสินค้า"};function F(e){return D[e]||e}function E(e){return Array.isArray(e)?e.join(", "):e&&typeof e=="object"?JSON.stringify(e):String(e??"—")}return(e,x)=>{const N=R("router-link");return n(),l("div",null,[f(N,{to:`/groups/${a(g)}/personas`,class:"btn-back"},{default:V(()=>[f(a($),{size:16,"stroke-width":2}),p(" "+t(a(o).t("personas")),1)]),_:1},8,["to"]),s("div",W,[s("strong",null,[f(a(U),{size:17,"stroke-width":1.8,style:{"vertical-align":"-3px"}}),p(" "+t(a(o).t("chatGuideTitle")),1)]),s("p",X,t(a(o).t("chatGuideText")),1)]),s("div",Y,[s("h2",Z,t(r.value?r.value.name:"..."),1),s("span",{class:T(["badge",r.value&&r.value.channel])},t(r.value?r.value.channel:""),3),r.value?(n(),l("span",ee,t(r.value.profession)+" · "+t(r.value.age_group),1)):u("",!0),s("button",{class:"danger",style:{"margin-left":"auto"},onClick:B,disabled:h.value.length===0||!!i.value},[d.value?(n(),l("span",te)):u("",!0),p(t(a(o).t("finish")),1)],8,se)]),b.value?u("",!0):(n(),l("div",ae,[s("strong",null,"📣 "+t(a(o).t("sellerInitiated")),1),C.value?(n(),l("div",le,t(C.value),1)):u("",!0)])),b.value?(n(),l("div",{key:1,class:"thread",ref_key:"thread",ref:k},[(n(!0),l(j,null,L(h.value,(v,m)=>(n(),l("div",{key:m,class:T(["bubble",v.role==="seller"?"msg-seller":"msg-customer"])},t(v.text),3))),128)),d.value?(n(),l("div",ne,"...")):u("",!0)],512)):u("",!0),b.value&&!i.value?(n(),l("div",oe,[P(s("input",{"onUpdate:modelValue":x[0]||(x[0]=v=>_.value=v),onKeyup:H(I,["enter"]),disabled:d.value,placeholder:a(o).t("send")},null,40,ie),[[q,_.value]]),s("button",{class:"primary",onClick:I,disabled:d.value||!_.value.trim()},t(a(o).t("send")),9,re)])):u("",!0),i.value?(n(),l("div",ue,[s("h3",null,t(a(o).t("debrief")),1),s("p",null,[s("span",{class:T(["badge",i.value.outcome])},t(i.value.outcome==="won"?a(o).t("won"):a(o).t("lost")),3),p(" — "+t(a(o).t("score"))+": ",1),s("strong",null,t(i.value.score),1)]),s("p",null,[s("strong",null,t(a(o).t("pain"))+":",1),p(" "+t(i.value.pain||"—"),1)]),s("p",null,[s("strong",null,t(a(o).t("why"))+":",1),p(" "+t(i.value.why),1)]),i.value.coaching&&i.value.coaching.length?(n(),l("div",ce,[x[1]||(x[1]=s("strong",null,"Coaching:",-1)),s("ul",null,[(n(!0),l(j,null,L(i.value.coaching,(v,m)=>(n(),l("li",{key:m},t(v),1))),128))])])):u("",!0),s("details",null,[s("summary",null,[f(a(Q),{size:15,"stroke-width":1.8,style:{"vertical-align":"-2px"}}),p(" "+t(a(o).t("reveal")),1)]),i.value.revealed_persona?(n(),l("div",de,[(n(!0),l(j,null,L(i.value.revealed_persona,(v,m)=>(n(),l("div",{key:m,class:"rev"},[s("span",ve,t(F(m)),1),s("span",pe,t(E(v)),1)]))),128))])):u("",!0)]),f(N,{to:"/"},{default:V(()=>[s("button",he,t(a(o).t("dashboard")),1)]),_:1})])):u("",!0)])}}},be=K(_e,[["__scopeId","data-v-b21475ac"]]);export{be as default}; diff --git a/frontend/dist/assets/Chat-Dq49zqgJ.css b/frontend/dist/assets/Chat-Dq49zqgJ.css deleted file mode 100644 index 0a31445..0000000 --- a/frontend/dist/assets/Chat-Dq49zqgJ.css +++ /dev/null @@ -1 +0,0 @@ -.thread[data-v-b21475ac]{background:#eceff4;border:1px solid var(--border);border-radius:var(--radius);padding:16px;min-height:320px;max-height:52vh;overflow-y:auto;display:flex;flex-direction:column;gap:8px}.bubble[data-v-b21475ac]{max-width:72%;padding:10px 14px;white-space:pre-wrap;word-break:break-word}.composer[data-v-b21475ac]{display:flex;gap:8px;margin-top:12px}.task[data-v-b21475ac]{margin-bottom:12px;background:#fff7ed;border-color:#fed7aa}.debrief[data-v-b21475ac]{margin-top:16px}.json[data-v-b21475ac]{background:#0f172a;color:#9ca3af;padding:10px;border-radius:8px;font-size:11px;overflow:auto;max-height:260px}button.danger[data-v-b21475ac]{background:var(--red);color:#fff;border:none}.guide[data-v-b21475ac]{background:#eef2ff;border-color:#c7d2fe;margin-bottom:12px}.reveal-grid[data-v-b21475ac]{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:8px}.rev[data-v-b21475ac]{display:flex;flex-direction:column;background:#f8fafc;border:1px solid var(--border);border-radius:8px;padding:8px 10px}.rk[data-v-b21475ac]{font-size:12px;color:var(--muted)}.rv[data-v-b21475ac]{font-size:13px;color:var(--ink);margin-top:2px}@media (max-width: 640px){.reveal-grid[data-v-b21475ac]{grid-template-columns:1fr}} diff --git a/frontend/dist/assets/Chat-fU4aRozx.js b/frontend/dist/assets/Chat-fU4aRozx.js new file mode 100644 index 0000000..57a6768 --- /dev/null +++ b/frontend/dist/assets/Chat-fU4aRozx.js @@ -0,0 +1,6 @@ +import{k as P,_ as $,l as q,s as C,c as l,m as b,n as z,u as a,a as e,t as s,i,F as j,q as S,j as p,A as T,e as m,w as H,v as J,b as O,h as R,r as u,x as U,o as n,C as Q}from"./index-tI5UDFg-.js";import{T as W}from"./target-CylhW5SK.js";import{A as X}from"./arrow-left-C_e4n_39.js";/** + * @license lucide-vue-next v1.0.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Y=P("search",[["path",{d:"m21 21-4.34-4.34",key:"14j7rj"}],["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}]]),Z={key:0,class:"card"},ee={style:{"margin-top":"0"}},se=["onClick"],te={class:"row",style:{"align-items":"center"}},ae={key:0,class:"badge line"},le={key:1,class:"badge facebook"},ne={class:"muted",style:{"margin-top":"6px"}},oe=["disabled"],ie={key:1},re={class:"card guide"},ce={style:{margin:"6px 0 0","line-height":"1.7"}},ue={class:"row",style:{"align-items":"center","margin-bottom":"12px"}},de={style:{margin:"0"}},ve={key:0,class:"muted"},pe=["disabled"],me={key:0,class:"spinner",style:{"margin-right":"4px"}},_e={key:0,class:"bubble msg-customer muted"},he={key:0,class:"composer"},ge=["disabled","placeholder"],ye=["disabled"],fe={key:1,class:"card debrief"},be={key:0},ke={key:0,class:"reveal-grid"},xe={class:"rk"},we={class:"rv"},Ce={class:"primary",style:{"margin-top":"12px"}},je={__name:"Chat",setup(Se){const L=R(),f=L.params.gid,k=L.params.pid,c=u(null),_=u("pick"),h=u([]),g=u(""),d=u(!1),r=u(null),B=u(""),D=u(null),x=u("social"),F=[{id:"social",emoji:"💬",init:"customer",label:"Social Media (แชท)",desc:"ลูกค้าทักมาหาคุณก่อน — โทนสั้น ทักๆ ตามสไตล์แชท"},{id:"f2f_call",emoji:"📞",init:"seller",label:"พบหน้า / โทรศัพท์",desc:"คุณต้องเป็นฝ่ายเปิดการสนทนาเชิงรุกกับลีด (ผู้ฝึกทักก่อน)"},{id:"recontact",emoji:"⏳",init:"customer",label:"ลูกค้ากลับมาติดต่อ (1-3 เดือน)",desc:"เคยได้รับข้อมูลไปแล้ว ตอนนี้กลับมาติดต่อ พร้อมตัดสินใจมากขึ้น"}];function A(){Q(()=>{w.value&&(w.value.scrollTop=w.value.scrollHeight)})}const w=u(null);async function I(){_.value="chat";const t=await C.chatStart(f,k,{scenario:x.value});D.value=t.session.id,t.session.task&&(B.value=t.session.task),h.value=t.session.messages||[],h.value.length&&A()}q(async()=>{c.value=(await C.getPersona(f,k)).persona});async function N(){if(g.value.trim()){d.value=!0;try{const t=await C.chatSend(f,k,g.value.trim());h.value=t.messages,g.value="",A()}catch(t){alert(t.message)}finally{d.value=!1}}}async function E(){if(confirm(i.t("finish")+"?")){d.value=!0;try{const t=await C.chatFinish(f,k);r.value=t.debrief,h.value=t.session.messages,_.value="done"}catch(t){alert(t.message)}finally{d.value=!1}}}const M={name:"ชื่อ",tier:"ระดับ",difficulty:"ความยาก",profession:"อาชีพ",age_group:"ช่วงอายุ",location:"พื้นที่",income:"รายได้",budget:"งบประมาณ",lifestyle:"ไลฟ์สไตล์",background:"ภูมิหลัง",personality:"บุคลิก",communication_style:"สไตล์การสื่อสาร",goal:"เป้าหมาย",decision_timeline:"กรอบตัดสินใจ",pains:"ปัญหา (Pain)",objections:"ข้อโต้แย้ง",negotiation_levers:"สิ่งที่ใช้ต่อรอง",opener:"บทเปิดบทสนทนา",channel:"ช่องทาง",initiation_mode:"ใครเริ่มก่อน",product_context:"บริบทสินค้า"};function G(t){return M[t]||t}function K(t){return Array.isArray(t)?t.join(", "):t&&typeof t=="object"?JSON.stringify(t):String(t??"—")}return(t,y)=>{const V=U("router-link");return n(),l("div",null,[b(V,{to:`/groups/${a(f)}/personas`,class:"btn-back"},{default:z(()=>[b(a(X),{size:16,"stroke-width":2}),p(" "+s(a(i).t("personas")),1)]),_:1},8,["to"]),_.value==="pick"?(n(),l("div",Z,[e("h3",ee,"🎬 "+s(a(i).t("chooseScenario")),1),y[1]||(y[1]=e("p",{class:"muted",style:{"margin-top":"0"}},"เลือกสถานการณ์ที่อยากฝึกการขาย — แต่ละแบบกำหนดว่าใครทักก่อน และโทนการสนทนา",-1)),(n(),l(j,null,S(F,o=>e("div",{key:o.id,class:T(["scenario",{sel:x.value===o.id}]),onClick:v=>x.value=o.id},[e("div",te,[e("strong",null,s(o.emoji)+" "+s(o.label),1),o.init==="customer"?(n(),l("span",ae,"ลูกค้าทักก่อน")):(n(),l("span",le,"คุณต้องทักก่อน (เชิงรุก)"))]),e("div",ne,s(o.desc),1)],10,se)),64)),e("button",{class:"primary",style:{"margin-top":"16px",width:"100%"},disabled:!x.value,onClick:I}," ▶ "+s(a(i).t("start")),9,oe)])):(n(),l("div",ie,[e("div",re,[e("strong",null,[b(a(W),{size:17,"stroke-width":1.8,style:{"vertical-align":"-3px"}}),p(" "+s(a(i).t("chatGuideTitle")),1)]),e("p",ce,s(a(i).t("chatGuideText")),1)]),e("div",ue,[e("h2",de,s(c.value?c.value.name:"..."),1),e("span",{class:T(["badge",c.value&&c.value.channel])},s(c.value?c.value.channel:""),3),c.value?(n(),l("span",ve,s(c.value.profession)+" · "+s(c.value.age_group),1)):m("",!0),e("button",{class:"danger",style:{"margin-left":"auto"},onClick:E,disabled:h.value.length===0||_.value==="done"},[d.value?(n(),l("span",me)):m("",!0),p(s(a(i).t("finish")),1)],8,pe)]),e("div",{class:"thread",ref_key:"thread",ref:w},[(n(!0),l(j,null,S(h.value,(o,v)=>(n(),l("div",{key:v,class:T(["bubble",o.role==="seller"?"msg-seller":o.role==="system"?"msg-system":"msg-customer"])},s(o.text),3))),128)),d.value?(n(),l("div",_e,"...")):m("",!0)],512),_.value==="chat"?(n(),l("div",he,[H(e("input",{"onUpdate:modelValue":y[0]||(y[0]=o=>g.value=o),onKeyup:O(N,["enter"]),disabled:d.value,placeholder:a(i).t("send")},null,40,ge),[[J,g.value]]),e("button",{class:"primary",onClick:N,disabled:d.value||!g.value.trim()},s(a(i).t("send")),9,ye)])):m("",!0),_.value==="done"&&r.value?(n(),l("div",fe,[e("h3",null,s(a(i).t("debrief")),1),e("p",null,[e("span",{class:T(["badge",r.value.outcome])},s(r.value.outcome==="won"?a(i).t("won"):a(i).t("lost")),3),p(" — "+s(a(i).t("score"))+": ",1),e("strong",null,s(r.value.score),1)]),e("p",null,[e("strong",null,s(a(i).t("pain"))+":",1),p(" "+s(r.value.pain||"—"),1)]),e("p",null,[e("strong",null,s(a(i).t("why"))+":",1),p(" "+s(r.value.why),1)]),r.value.coaching&&r.value.coaching.length?(n(),l("div",be,[y[2]||(y[2]=e("strong",null,"Coaching:",-1)),e("ul",null,[(n(!0),l(j,null,S(r.value.coaching,(o,v)=>(n(),l("li",{key:v},s(o),1))),128))])])):m("",!0),e("details",null,[e("summary",null,[b(a(Y),{size:15,"stroke-width":1.8,style:{"vertical-align":"-2px"}}),p(" "+s(a(i).t("reveal")),1)]),r.value.revealed_persona?(n(),l("div",ke,[(n(!0),l(j,null,S(r.value.revealed_persona,(o,v)=>(n(),l("div",{key:v,class:"rev"},[e("span",xe,s(G(v)),1),e("span",we,s(K(o)),1)]))),128))])):m("",!0)]),b(V,{to:"/"},{default:z(()=>[e("button",Ce,s(a(i).t("dashboard")),1)]),_:1})])):m("",!0)]))])}}},Ne=$(je,[["__scopeId","data-v-310aef08"]]);export{Ne as default}; diff --git a/frontend/dist/assets/GroupBuilder-BTot24Pr.js b/frontend/dist/assets/GroupBuilder-4jGJS7rQ.js similarity index 98% rename from frontend/dist/assets/GroupBuilder-BTot24Pr.js rename to frontend/dist/assets/GroupBuilder-4jGJS7rQ.js index 475d87f..6ad67d2 100644 --- a/frontend/dist/assets/GroupBuilder-BTot24Pr.js +++ b/frontend/dist/assets/GroupBuilder-4jGJS7rQ.js @@ -1,4 +1,4 @@ -import{k as g,_ as M,c as f,m as d,n as V,a as e,u as l,j as r,t as s,i as o,w as u,v as y,D as k,e as C,x as z,r as v,s as B,g as A,o as x}from"./index-C0Orcg9B.js";import{A as P}from"./arrow-left-BoUMQ_f_.js";/** +import{k as g,_ as M,c as f,m as d,n as V,a as e,u as l,j as r,t as s,i as o,w as u,v as y,D as k,e as C,x as z,r as v,s as B,g as A,o as x}from"./index-tI5UDFg-.js";import{A as P}from"./arrow-left-C_e4n_39.js";/** * @license lucide-vue-next v1.0.0 - ISC * * This source code is licensed under the ISC license. diff --git a/frontend/dist/assets/GroupEdit-Db7E5OJj.js b/frontend/dist/assets/GroupEdit-BzqeiR8Z.js similarity index 99% rename from frontend/dist/assets/GroupEdit-Db7E5OJj.js rename to frontend/dist/assets/GroupEdit-BzqeiR8Z.js index fd09975..9f917ad 100644 --- a/frontend/dist/assets/GroupEdit-Db7E5OJj.js +++ b/frontend/dist/assets/GroupEdit-BzqeiR8Z.js @@ -1,4 +1,4 @@ -import{k as T,_ as F,E as O,o as m,c as f,a as e,t as i,w as o,v as u,D as M,e as _,G as I,r as k,l as q,m as j,n as D,u as c,j as y,i as B,B as J,F as N,q as P,H,h as R,s as z,x as K,A as E}from"./index-C0Orcg9B.js";import{U as Q}from"./users-D5qk1Nmy.js";/** +import{k as T,_ as F,E as O,o as m,c as f,a as e,t as i,w as o,v as u,D as M,e as _,G as I,r as k,l as q,m as j,n as D,u as c,j as y,i as B,B as J,F as N,q as P,H,h as R,s as z,x as K,A as E}from"./index-tI5UDFg-.js";import{U as Q}from"./users-qicnhyDs.js";/** * @license lucide-vue-next v1.0.0 - ISC * * This source code is licensed under the ISC license. diff --git a/frontend/dist/assets/Login-DhaEm1-6.js b/frontend/dist/assets/Login-BvMoAa9d.js similarity index 92% rename from frontend/dist/assets/Login-DhaEm1-6.js rename to frontend/dist/assets/Login-BvMoAa9d.js index 2d0ee62..55f677a 100644 --- a/frontend/dist/assets/Login-DhaEm1-6.js +++ b/frontend/dist/assets/Login-BvMoAa9d.js @@ -1 +1 @@ -import{_ as b,c as d,a as e,t as s,u as c,i as o,w as y,v as x,b as w,d as k,e as S,r as l,f,g as V,h as B,o as v}from"./index-C0Orcg9B.js";const C={class:"login-wrap"},D={class:"card login-card"},K={class:"pw-wrap"},E=["type"],L=["aria-label"],M={key:0,class:"error",role:"alert"},N=["disabled"],R={key:0,class:"spinner"},U={key:1},q={__name:"Login",setup(H){const g=B(),_=V(),n=l(""),u=l(""),a=l(!1),r=l(""),i=l(!1);async function m(){r.value="",i.value=!0;try{await f.login(n.value.trim(),u.value),f.mustSetup?_.push({path:"/setup"}):_.push(g.query.redirect||"/")}catch{r.value=o.t("loginError")}finally{i.value=!1}}return(h,t)=>(v(),d("div",C,[e("div",D,[e("h1",null,"🎯 "+s(c(o).t("app")),1),t[3]||(t[3]=e("p",{class:"muted",style:{"margin-top":"-8px"}},"Sales training simulator",-1)),e("label",null,s(c(o).t("username")),1),y(e("input",{"onUpdate:modelValue":t[0]||(t[0]=p=>n.value=p),type:"text",autocomplete:"username",onKeyup:w(m,["enter"])},null,544),[[x,n.value]]),e("label",null,s(c(o).t("password")),1),e("div",K,[y(e("input",{"onUpdate:modelValue":t[1]||(t[1]=p=>u.value=p),type:a.value?"text":"password",autocomplete:"current-password",onKeyup:w(m,["enter"])},null,40,E),[[k,u.value]]),e("button",{type:"button",class:"pw-toggle",onClick:t[2]||(t[2]=p=>a.value=!a.value),"aria-label":a.value?"Hide password":"Show password"},s(a.value?"🙈":"👁"),9,L)]),r.value?(v(),d("div",M,s(r.value),1)):S("",!0),e("button",{class:"primary",style:{width:"100%","margin-top":"16px"},disabled:i.value||!n.value||!u.value,onClick:m},[i.value?(v(),d("span",R)):(v(),d("span",U,s(c(o).t("login")),1))],8,N)])]))}},P=b(q,[["__scopeId","data-v-f7904d9f"]]);export{P as default}; +import{_ as b,c as d,a as e,t as s,u as c,i as o,w as y,v as x,b as w,d as k,e as S,r as l,f,g as V,h as B,o as v}from"./index-tI5UDFg-.js";const C={class:"login-wrap"},D={class:"card login-card"},K={class:"pw-wrap"},E=["type"],L=["aria-label"],M={key:0,class:"error",role:"alert"},N=["disabled"],R={key:0,class:"spinner"},U={key:1},q={__name:"Login",setup(H){const g=B(),_=V(),n=l(""),u=l(""),a=l(!1),r=l(""),i=l(!1);async function m(){r.value="",i.value=!0;try{await f.login(n.value.trim(),u.value),f.mustSetup?_.push({path:"/setup"}):_.push(g.query.redirect||"/")}catch{r.value=o.t("loginError")}finally{i.value=!1}}return(h,t)=>(v(),d("div",C,[e("div",D,[e("h1",null,"🎯 "+s(c(o).t("app")),1),t[3]||(t[3]=e("p",{class:"muted",style:{"margin-top":"-8px"}},"Sales training simulator",-1)),e("label",null,s(c(o).t("username")),1),y(e("input",{"onUpdate:modelValue":t[0]||(t[0]=p=>n.value=p),type:"text",autocomplete:"username",onKeyup:w(m,["enter"])},null,544),[[x,n.value]]),e("label",null,s(c(o).t("password")),1),e("div",K,[y(e("input",{"onUpdate:modelValue":t[1]||(t[1]=p=>u.value=p),type:a.value?"text":"password",autocomplete:"current-password",onKeyup:w(m,["enter"])},null,40,E),[[k,u.value]]),e("button",{type:"button",class:"pw-toggle",onClick:t[2]||(t[2]=p=>a.value=!a.value),"aria-label":a.value?"Hide password":"Show password"},s(a.value?"🙈":"👁"),9,L)]),r.value?(v(),d("div",M,s(r.value),1)):S("",!0),e("button",{class:"primary",style:{width:"100%","margin-top":"16px"},disabled:i.value||!n.value||!u.value,onClick:m},[i.value?(v(),d("span",R)):(v(),d("span",U,s(c(o).t("login")),1))],8,N)])]))}},P=b(q,[["__scopeId","data-v-f7904d9f"]]);export{P as default}; diff --git a/frontend/dist/assets/MyBoard-CHF7e7KE.js b/frontend/dist/assets/MyBoard-CdXG-0Ii.js similarity index 98% rename from frontend/dist/assets/MyBoard-CHF7e7KE.js rename to frontend/dist/assets/MyBoard-CdXG-0Ii.js index 970053d..3784819 100644 --- a/frontend/dist/assets/MyBoard-CHF7e7KE.js +++ b/frontend/dist/assets/MyBoard-CdXG-0Ii.js @@ -1,4 +1,4 @@ -import{k as x,_ as f,l as k,s as p,c as r,a as t,m as b,u as e,j as B,t as s,i as a,e as v,w as N,y as D,F as S,q as T,r as h,z as c,o as i,A as V}from"./index-C0Orcg9B.js";/** +import{k as x,_ as f,l as k,s as p,c as r,a as t,m as b,u as e,j as B,t as s,i as a,e as v,w as N,y as D,F as S,q as T,r as h,z as c,o as i,A as V}from"./index-tI5UDFg-.js";/** * @license lucide-vue-next v1.0.0 - ISC * * This source code is licensed under the ISC license. diff --git a/frontend/dist/assets/Personas-CRi-xjVS.js b/frontend/dist/assets/Personas-BXejtBr0.js similarity index 95% rename from frontend/dist/assets/Personas-CRi-xjVS.js rename to frontend/dist/assets/Personas-BXejtBr0.js index 10649ca..5f9c514 100644 --- a/frontend/dist/assets/Personas-CRi-xjVS.js +++ b/frontend/dist/assets/Personas-BXejtBr0.js @@ -1 +1 @@ -import{_ as T,l as $,c as r,m as d,n as m,a as t,t as s,u as e,i as n,f as g,j as _,e as w,F as y,q as p,h as L,s as N,x as V,r as b,o as a,S as I,A as h,B as A}from"./index-C0Orcg9B.js";import{T as S}from"./target-qgda8bhm.js";import{A as j}from"./arrow-left-BoUMQ_f_.js";const F={class:"row",style:{"align-items":"center"}},q={style:{margin:"0"}},D={class:"muted",style:{"margin-left":"auto"}},E={key:0,class:"card guide"},M={key:1,class:"row",style:{margin:"12px 0",gap:"10px"}},R={class:"primary"},G={class:"grid"},H={class:"row",style:{"justify-content":"space-between"}},J={class:"diff"},K={class:"muted"},O={class:"muted"},Q={class:"muted"},U={class:"muted",style:{"margin-top":"6px"}},W={class:"primary",style:{width:"100%"}},X={class:"primary",style:{width:"100%"}},Y={key:1,class:"muted",style:{"margin-top":"auto","font-size":"12px"}},Z={__name:"Personas",setup(tt){const c=L().params.gid,k=b([]),B=b(!0);async function C(){try{k.value=(await N.listPersonas(c)).personas}finally{B.value=!1}}function z(l){return k.value.filter(i=>i.tier===l)}function P(l){return n.t(l==="A"?"tierA":l==="B"?"tierB":"tierC")}function v(l){return l==="won"?n.t("won"):l==="lost"?n.t("lost"):n.t("notTried")}return $(C),(l,i)=>{const u=V("router-link");return a(),r("div",null,[d(u,{to:"/training",class:"btn-back"},{default:m(()=>[d(e(j),{size:16,"stroke-width":2}),_(" "+s(e(n).t("training")),1)]),_:1}),t("div",F,[t("h2",q,s(e(n).t("personas")),1),t("span",D,s(e(n).t("selectPersona")),1)]),e(g).isAdmin?w("",!0):(a(),r("div",E,[t("strong",null,[d(e(S),{size:17,"stroke-width":1.8,style:{"vertical-align":"-3px"}}),i[0]||(i[0]=_(" วิธีฝึก",-1))]),i[1]||(i[1]=t("ol",{style:{margin:"8px 0 0","padding-left":"20px","line-height":"1.8"}},[t("li",null,"เลือกลูกค้าจำลอง (บุคคลต้นแบบ) คนหนึ่งที่อยากฝึกด้วย"),t("li",null,"ระดับ A ง่ายสุด → ระดับ C ยากสุด (ดูจากดาว ★ ความยาก)"),t("li",null,"กดปุ่มสำหรับฝึกแชทกับลูกค้าคนนั้น (ฝึกได้คนละครั้งเท่านั้น)")],-1))])),e(g).isAdmin?(a(),r("div",M,[d(u,{to:`/admin/groups/${e(c)}/edit`},{default:m(()=>[t("button",R,[d(e(I),{size:18,"stroke-width":1.8}),_(" "+s(e(n).t("managePersonas")),1)])]),_:1},8,["to"]),i[2]||(i[2]=t("span",{class:"muted"},"Admin: จัดการรายละเอียดบุคคลต้นแบบได้ที่นี่",-1))])):w("",!0),(a(),r(y,null,p(["A","B","C"],f=>t("div",{key:f,style:{margin:"20px 0"}},[t("h4",null,s(P(f)),1),t("div",G,[(a(!0),r(y,null,p(z(f),o=>(a(),r("div",{key:o.id,class:"card pcard lift"},[t("div",H,[t("strong",null,s(o.name),1),t("span",{class:h(["badge",o.my_outcome])},s(v(o.my_outcome)),3)]),t("div",J,[(a(),r(y,null,p(5,x=>t("span",{key:x,class:h(["star",{on:x<=(o.difficulty||1)}])},"★",2)),64)),t("span",K,s(e(n).t("difficulty"))+" "+s(o.difficulty||1)+"/5",1)]),t("div",O,[_(s(o.profession)+" · "+s(o.age_group)+" · "+s(o.location),1),i[3]||(i[3]=t("br",null,null,-1)),t("span",{class:h(["badge",o.channel])},s(o.channel),3),t("span",Q," · "+s(o.initiation_mode==="seller"?e(n).t("sellerInitiated"):e(n).t("customerInitiated")),1)]),t("div",U,s(o.product_context),1),e(g).isAdmin?(a(),A(u,{key:0,to:`/groups/${e(c)}/chat/${o.id}`,style:{"margin-top":"auto"}},{default:m(()=>[t("button",W,s(e(n).t("chat")),1)]),_:1},8,["to"])):(a(),r(y,{key:1},[o.my_outcome==="not_tried"?(a(),A(u,{key:0,to:`/groups/${e(c)}/chat/${o.id}`,style:{"margin-top":"auto"}},{default:m(()=>[t("button",X,s(e(n).t("chat")),1)]),_:1},8,["to"])):(a(),r("div",Y,"✓ "+s(e(n).t("trained"))+" ("+s(v(o.my_outcome))+")",1))],64))]))),128))])])),64))])}}},at=T(Z,[["__scopeId","data-v-46d50c99"]]);export{at as default}; +import{_ as T,l as $,c as r,m as d,n as m,a as t,t as s,u as e,i as n,f as g,j as _,e as w,F as y,q as p,h as L,s as N,x as V,r as b,o as a,S as I,A as h,B as A}from"./index-tI5UDFg-.js";import{T as S}from"./target-CylhW5SK.js";import{A as j}from"./arrow-left-C_e4n_39.js";const F={class:"row",style:{"align-items":"center"}},q={style:{margin:"0"}},D={class:"muted",style:{"margin-left":"auto"}},E={key:0,class:"card guide"},M={key:1,class:"row",style:{margin:"12px 0",gap:"10px"}},R={class:"primary"},G={class:"grid"},H={class:"row",style:{"justify-content":"space-between"}},J={class:"diff"},K={class:"muted"},O={class:"muted"},Q={class:"muted"},U={class:"muted",style:{"margin-top":"6px"}},W={class:"primary",style:{width:"100%"}},X={class:"primary",style:{width:"100%"}},Y={key:1,class:"muted",style:{"margin-top":"auto","font-size":"12px"}},Z={__name:"Personas",setup(tt){const c=L().params.gid,k=b([]),B=b(!0);async function C(){try{k.value=(await N.listPersonas(c)).personas}finally{B.value=!1}}function z(l){return k.value.filter(i=>i.tier===l)}function P(l){return n.t(l==="A"?"tierA":l==="B"?"tierB":"tierC")}function v(l){return l==="won"?n.t("won"):l==="lost"?n.t("lost"):n.t("notTried")}return $(C),(l,i)=>{const u=V("router-link");return a(),r("div",null,[d(u,{to:"/training",class:"btn-back"},{default:m(()=>[d(e(j),{size:16,"stroke-width":2}),_(" "+s(e(n).t("training")),1)]),_:1}),t("div",F,[t("h2",q,s(e(n).t("personas")),1),t("span",D,s(e(n).t("selectPersona")),1)]),e(g).isAdmin?w("",!0):(a(),r("div",E,[t("strong",null,[d(e(S),{size:17,"stroke-width":1.8,style:{"vertical-align":"-3px"}}),i[0]||(i[0]=_(" วิธีฝึก",-1))]),i[1]||(i[1]=t("ol",{style:{margin:"8px 0 0","padding-left":"20px","line-height":"1.8"}},[t("li",null,"เลือกลูกค้าจำลอง (บุคคลต้นแบบ) คนหนึ่งที่อยากฝึกด้วย"),t("li",null,"ระดับ A ง่ายสุด → ระดับ C ยากสุด (ดูจากดาว ★ ความยาก)"),t("li",null,"กดปุ่มสำหรับฝึกแชทกับลูกค้าคนนั้น (ฝึกได้คนละครั้งเท่านั้น)")],-1))])),e(g).isAdmin?(a(),r("div",M,[d(u,{to:`/admin/groups/${e(c)}/edit`},{default:m(()=>[t("button",R,[d(e(I),{size:18,"stroke-width":1.8}),_(" "+s(e(n).t("managePersonas")),1)])]),_:1},8,["to"]),i[2]||(i[2]=t("span",{class:"muted"},"Admin: จัดการรายละเอียดบุคคลต้นแบบได้ที่นี่",-1))])):w("",!0),(a(),r(y,null,p(["A","B","C"],f=>t("div",{key:f,style:{margin:"20px 0"}},[t("h4",null,s(P(f)),1),t("div",G,[(a(!0),r(y,null,p(z(f),o=>(a(),r("div",{key:o.id,class:"card pcard lift"},[t("div",H,[t("strong",null,s(o.name),1),t("span",{class:h(["badge",o.my_outcome])},s(v(o.my_outcome)),3)]),t("div",J,[(a(),r(y,null,p(5,x=>t("span",{key:x,class:h(["star",{on:x<=(o.difficulty||1)}])},"★",2)),64)),t("span",K,s(e(n).t("difficulty"))+" "+s(o.difficulty||1)+"/5",1)]),t("div",O,[_(s(o.profession)+" · "+s(o.age_group)+" · "+s(o.location),1),i[3]||(i[3]=t("br",null,null,-1)),t("span",{class:h(["badge",o.channel])},s(o.channel),3),t("span",Q," · "+s(o.initiation_mode==="seller"?e(n).t("sellerInitiated"):e(n).t("customerInitiated")),1)]),t("div",U,s(o.product_context),1),e(g).isAdmin?(a(),A(u,{key:0,to:`/groups/${e(c)}/chat/${o.id}`,style:{"margin-top":"auto"}},{default:m(()=>[t("button",W,s(e(n).t("chat")),1)]),_:1},8,["to"])):(a(),r(y,{key:1},[o.my_outcome==="not_tried"?(a(),A(u,{key:0,to:`/groups/${e(c)}/chat/${o.id}`,style:{"margin-top":"auto"}},{default:m(()=>[t("button",X,s(e(n).t("chat")),1)]),_:1},8,["to"])):(a(),r("div",Y,"✓ "+s(e(n).t("trained"))+" ("+s(v(o.my_outcome))+")",1))],64))]))),128))])])),64))])}}},at=T(Z,[["__scopeId","data-v-46d50c99"]]);export{at as default}; diff --git a/frontend/dist/assets/Settings-CatYfY0I.js b/frontend/dist/assets/Settings-D5LYPIGh.js similarity index 99% rename from frontend/dist/assets/Settings-CatYfY0I.js rename to frontend/dist/assets/Settings-D5LYPIGh.js index 7bf7dd5..3be3435 100644 --- a/frontend/dist/assets/Settings-CatYfY0I.js +++ b/frontend/dist/assets/Settings-D5LYPIGh.js @@ -1,4 +1,4 @@ -import{k as z,_ as T,r as c,f as i,c as h,a as e,m as k,u as a,S as $,j as v,t as o,i as s,w as x,v as b,e as V,A as S,s as U,o as w}from"./index-C0Orcg9B.js";/** +import{k as z,_ as T,r as c,f as i,c as h,a as e,m as k,u as a,S as $,j as v,t as o,i as s,w as x,v as b,e as V,A as S,s as U,o as w}from"./index-tI5UDFg-.js";/** * @license lucide-vue-next v1.0.0 - ISC * * This source code is licensed under the ISC license. diff --git a/frontend/dist/assets/Setup-D9lCpt9C.js b/frontend/dist/assets/Setup-_cGdJHOE.js similarity index 96% rename from frontend/dist/assets/Setup-D9lCpt9C.js rename to frontend/dist/assets/Setup-_cGdJHOE.js index 83af619..796b2e4 100644 --- a/frontend/dist/assets/Setup-D9lCpt9C.js +++ b/frontend/dist/assets/Setup-_cGdJHOE.js @@ -1 +1 @@ -import{_ as x,c as v,a as e,t,u as s,i as a,j as S,f as _,w,v as f,b as y,e as V,r,g as K,o as m}from"./index-C0Orcg9B.js";const T={class:"setup-wrap"},B={class:"card setup-card"},N={class:"muted"},U={key:0,class:"error",role:"alert"},C=["disabled"],D={key:0,class:"spinner"},M={key:1},P={__name:"Setup",setup(j){const k=K(),i=r(""),l=r(""),p=r(""),o=r(""),d=r(!1);async function c(){if(o.value="",l.value.length<4){o.value=a.t("passwordTooShort");return}if(l.value!==p.value){o.value=a.t("passwordMismatch");return}d.value=!0;try{await _.finishSetup(i.value.trim(),l.value),k.push("/")}catch(h){o.value=h.message}finally{d.value=!1}}return(h,u)=>{var b,g;return m(),v("div",T,[e("div",B,[e("h1",null,"🔐 "+t(s(a).t("setupTitle")),1),e("p",N,[S(t(s(a).t("setupSubtitle"))+" ",1),e("strong",null,t(((b=s(_).user)==null?void 0:b.name)||((g=s(_).user)==null?void 0:g.username)),1)]),e("label",null,t(s(a).t("email")),1),w(e("input",{"onUpdate:modelValue":u[0]||(u[0]=n=>i.value=n),type:"email",autocomplete:"email",onKeyup:y(c,["enter"])},null,544),[[f,i.value]]),e("label",null,t(s(a).t("newPassword")),1),w(e("input",{"onUpdate:modelValue":u[1]||(u[1]=n=>l.value=n),type:"password",autocomplete:"new-password",onKeyup:y(c,["enter"])},null,544),[[f,l.value]]),e("label",null,t(s(a).t("confirmPassword")),1),w(e("input",{"onUpdate:modelValue":u[2]||(u[2]=n=>p.value=n),type:"password",autocomplete:"new-password",onKeyup:y(c,["enter"])},null,544),[[f,p.value]]),o.value?(m(),v("div",U,t(o.value),1)):V("",!0),e("button",{class:"primary",style:{width:"100%","margin-top":"16px"},disabled:d.value||!i.value||!l.value||l.value!==p.value,onClick:c},[d.value?(m(),v("span",D)):(m(),v("span",M,t(s(a).t("save")),1))],8,C)])])}}},I=x(P,[["__scopeId","data-v-f190e120"]]);export{I as default}; +import{_ as x,c as v,a as e,t,u as s,i as a,j as S,f as _,w,v as f,b as y,e as V,r,g as K,o as m}from"./index-tI5UDFg-.js";const T={class:"setup-wrap"},B={class:"card setup-card"},N={class:"muted"},U={key:0,class:"error",role:"alert"},C=["disabled"],D={key:0,class:"spinner"},M={key:1},P={__name:"Setup",setup(j){const k=K(),i=r(""),l=r(""),p=r(""),o=r(""),d=r(!1);async function c(){if(o.value="",l.value.length<4){o.value=a.t("passwordTooShort");return}if(l.value!==p.value){o.value=a.t("passwordMismatch");return}d.value=!0;try{await _.finishSetup(i.value.trim(),l.value),k.push("/")}catch(h){o.value=h.message}finally{d.value=!1}}return(h,u)=>{var b,g;return m(),v("div",T,[e("div",B,[e("h1",null,"🔐 "+t(s(a).t("setupTitle")),1),e("p",N,[S(t(s(a).t("setupSubtitle"))+" ",1),e("strong",null,t(((b=s(_).user)==null?void 0:b.name)||((g=s(_).user)==null?void 0:g.username)),1)]),e("label",null,t(s(a).t("email")),1),w(e("input",{"onUpdate:modelValue":u[0]||(u[0]=n=>i.value=n),type:"email",autocomplete:"email",onKeyup:y(c,["enter"])},null,544),[[f,i.value]]),e("label",null,t(s(a).t("newPassword")),1),w(e("input",{"onUpdate:modelValue":u[1]||(u[1]=n=>l.value=n),type:"password",autocomplete:"new-password",onKeyup:y(c,["enter"])},null,544),[[f,l.value]]),e("label",null,t(s(a).t("confirmPassword")),1),w(e("input",{"onUpdate:modelValue":u[2]||(u[2]=n=>p.value=n),type:"password",autocomplete:"new-password",onKeyup:y(c,["enter"])},null,544),[[f,p.value]]),o.value?(m(),v("div",U,t(o.value),1)):V("",!0),e("button",{class:"primary",style:{width:"100%","margin-top":"16px"},disabled:d.value||!i.value||!l.value||l.value!==p.value,onClick:c},[d.value?(m(),v("span",D)):(m(),v("span",M,t(s(a).t("save")),1))],8,C)])])}}},I=x(P,[["__scopeId","data-v-f190e120"]]);export{I as default}; diff --git a/frontend/dist/assets/Training-CH5dgqen.js b/frontend/dist/assets/Training-CHRTy9x1.js similarity index 95% rename from frontend/dist/assets/Training-CH5dgqen.js rename to frontend/dist/assets/Training-CHRTy9x1.js index 09c7552..4197604 100644 --- a/frontend/dist/assets/Training-CH5dgqen.js +++ b/frontend/dist/assets/Training-CHRTy9x1.js @@ -1 +1 @@ -import{_ as w,l as b,s as A,f as l,c as r,a as t,m as _,u as s,j as m,t as e,i as o,B as y,n as h,e as f,F as C,q as N,r as g,x as P,o as i,A as v}from"./index-C0Orcg9B.js";import{T}from"./target-qgda8bhm.js";import{P as B}from"./plus-Cw7xc8qj.js";const z={class:"row",style:{"align-items":"center","margin-bottom":"16px"}},V={style:{margin:"0"}},j={style:{"margin-left":"auto"}},F={class:"primary"},L={class:"muted"},S={key:0,class:"card",style:{"min-height":"80px"}},$={key:1,class:"card empty-state"},q={key:0},D={key:1},E={class:"grid"},G={class:"card lift train-card"},I={class:"row",style:{"justify-content":"space-between"}},M={class:"muted",style:{margin:"6px 0 12px"}},H={class:"row",style:{gap:"8px"}},J={class:"muted"},K={class:"primary",style:{width:"100%","margin-top":"12px"}},O={__name:"Training",setup(Q){const c=g([]),u=g(!0);function k(a){return a.sales_kit&&a.sales_kit.productName||a.input&&a.input.product||""}function x(a){return a.personas||a.persona_count||0}return b(async()=>{try{const a=(await A.listGroups()).groups||[];c.value=l.isAdmin?a:a.filter(d=>d.status==="ready")}finally{u.value=!1}}),(a,d)=>{const p=P("router-link");return i(),r("div",null,[t("div",z,[t("h2",V,[_(s(T),{size:22,"stroke-width":1.8,style:{"vertical-align":"-4px"}}),m(" "+e(s(o).t("training")),1)]),t("div",j,[s(l).isAdmin?(i(),y(p,{key:0,to:"/admin/new-group"},{default:h(()=>[t("button",F,[_(s(B),{size:18,"stroke-width":2}),m(" "+e(s(o).t("addProduct")),1)])]),_:1})):f("",!0)])]),t("p",L,e(s(o).t("trainingSubtitle")),1),u.value?(i(),r("div",S,[...d[0]||(d[0]=[t("div",{class:"skeleton",style:{height:"50px"}},null,-1)])])):c.value.length===0?(i(),r("div",$,[t("strong",null,e(s(o).t("noTraining")),1),s(l).isAdmin?(i(),r("span",q,'Click "'+e(s(o).t("addProduct"))+'" to create a persona group.',1)):(i(),r("span",D,"Ask an admin to create a persona group first."))])):f("",!0),t("div",E,[(i(!0),r(C,null,N(c.value,n=>(i(),y(p,{key:n.id,to:s(l).isAdmin?`/admin/groups/${n.id}/edit`:`/groups/${n.id}/personas`,style:{"text-decoration":"none"}},{default:h(()=>[t("div",G,[t("div",I,[t("strong",null,e(n.title),1),t("span",{class:v(["badge",n.status==="ready"?"ready":"draft"])},e(n.status),3)]),t("div",M,e(k(n)),1),t("div",H,[t("span",{class:v(["badge",n.channel||"line"])},e(n.channel||"line"),3),t("span",J,e(x(n))+" "+e(s(o).t("personas").toLowerCase()),1)]),t("button",K,e(s(l).isAdmin?n.status==="ready"?s(o).t("managePersonas"):s(o).t("analyze"):s(o).t("selectPersona")),1)])]),_:2},1032,["to"]))),128))])])}}},X=w(O,[["__scopeId","data-v-1e5bd92b"]]);export{X as default}; +import{_ as w,l as b,s as A,f as l,c as r,a as t,m as _,u as s,j as m,t as e,i as o,B as y,n as h,e as f,F as C,q as N,r as g,x as P,o as i,A as v}from"./index-tI5UDFg-.js";import{T}from"./target-CylhW5SK.js";import{P as B}from"./plus-DCrJlDOb.js";const z={class:"row",style:{"align-items":"center","margin-bottom":"16px"}},V={style:{margin:"0"}},j={style:{"margin-left":"auto"}},F={class:"primary"},L={class:"muted"},S={key:0,class:"card",style:{"min-height":"80px"}},$={key:1,class:"card empty-state"},q={key:0},D={key:1},E={class:"grid"},G={class:"card lift train-card"},I={class:"row",style:{"justify-content":"space-between"}},M={class:"muted",style:{margin:"6px 0 12px"}},H={class:"row",style:{gap:"8px"}},J={class:"muted"},K={class:"primary",style:{width:"100%","margin-top":"12px"}},O={__name:"Training",setup(Q){const c=g([]),u=g(!0);function k(a){return a.sales_kit&&a.sales_kit.productName||a.input&&a.input.product||""}function x(a){return a.personas||a.persona_count||0}return b(async()=>{try{const a=(await A.listGroups()).groups||[];c.value=l.isAdmin?a:a.filter(d=>d.status==="ready")}finally{u.value=!1}}),(a,d)=>{const p=P("router-link");return i(),r("div",null,[t("div",z,[t("h2",V,[_(s(T),{size:22,"stroke-width":1.8,style:{"vertical-align":"-4px"}}),m(" "+e(s(o).t("training")),1)]),t("div",j,[s(l).isAdmin?(i(),y(p,{key:0,to:"/admin/new-group"},{default:h(()=>[t("button",F,[_(s(B),{size:18,"stroke-width":2}),m(" "+e(s(o).t("addProduct")),1)])]),_:1})):f("",!0)])]),t("p",L,e(s(o).t("trainingSubtitle")),1),u.value?(i(),r("div",S,[...d[0]||(d[0]=[t("div",{class:"skeleton",style:{height:"50px"}},null,-1)])])):c.value.length===0?(i(),r("div",$,[t("strong",null,e(s(o).t("noTraining")),1),s(l).isAdmin?(i(),r("span",q,'Click "'+e(s(o).t("addProduct"))+'" to create a persona group.',1)):(i(),r("span",D,"Ask an admin to create a persona group first."))])):f("",!0),t("div",E,[(i(!0),r(C,null,N(c.value,n=>(i(),y(p,{key:n.id,to:s(l).isAdmin?`/admin/groups/${n.id}/edit`:`/groups/${n.id}/personas`,style:{"text-decoration":"none"}},{default:h(()=>[t("div",G,[t("div",I,[t("strong",null,e(n.title),1),t("span",{class:v(["badge",n.status==="ready"?"ready":"draft"])},e(n.status),3)]),t("div",M,e(k(n)),1),t("div",H,[t("span",{class:v(["badge",n.channel||"line"])},e(n.channel||"line"),3),t("span",J,e(x(n))+" "+e(s(o).t("personas").toLowerCase()),1)]),t("button",K,e(s(l).isAdmin?n.status==="ready"?s(o).t("managePersonas"):s(o).t("analyze"):s(o).t("selectPersona")),1)])]),_:2},1032,["to"]))),128))])])}}},X=w(O,[["__scopeId","data-v-1e5bd92b"]]);export{X as default}; diff --git a/frontend/dist/assets/arrow-left-BoUMQ_f_.js b/frontend/dist/assets/arrow-left-C_e4n_39.js similarity index 86% rename from frontend/dist/assets/arrow-left-BoUMQ_f_.js rename to frontend/dist/assets/arrow-left-C_e4n_39.js index bd5e1f0..9ab2310 100644 --- a/frontend/dist/assets/arrow-left-BoUMQ_f_.js +++ b/frontend/dist/assets/arrow-left-C_e4n_39.js @@ -1,4 +1,4 @@ -import{k as e}from"./index-C0Orcg9B.js";/** +import{k as e}from"./index-tI5UDFg-.js";/** * @license lucide-vue-next v1.0.0 - ISC * * This source code is licensed under the ISC license. diff --git a/frontend/dist/assets/index-C0Orcg9B.js b/frontend/dist/assets/index-tI5UDFg-.js similarity index 87% rename from frontend/dist/assets/index-C0Orcg9B.js rename to frontend/dist/assets/index-tI5UDFg-.js index b1fa3fa..4a71c86 100644 --- a/frontend/dist/assets/index-C0Orcg9B.js +++ b/frontend/dist/assets/index-tI5UDFg-.js @@ -1,4 +1,4 @@ -const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/Login-DhaEm1-6.js","assets/Login-D3svPLnW.css","assets/Setup-D9lCpt9C.js","assets/Setup-BdS_6JlN.css","assets/Analytics-CyuFLItl.js","assets/users-D5qk1Nmy.js","assets/plus-Cw7xc8qj.js","assets/Analytics-D5VrZMJ0.css","assets/MyBoard-CHF7e7KE.js","assets/MyBoard-B7ypQ1JT.css","assets/Training-CH5dgqen.js","assets/target-qgda8bhm.js","assets/Training-CeWRER6W.css","assets/Personas-CRi-xjVS.js","assets/arrow-left-BoUMQ_f_.js","assets/Personas-CskAUbZv.css","assets/Chat-Bo6laZZd.js","assets/Chat-Dq49zqgJ.css","assets/Settings-CatYfY0I.js","assets/Settings-BCN2EZQ5.css","assets/GroupBuilder-BTot24Pr.js","assets/GroupBuilder-CpbMByEC.css","assets/GroupEdit-Db7E5OJj.js","assets/GroupEdit-D9lmggXK.css","assets/AdminUsers-B7nEYj4N.js","assets/AdminUsers-B0sl0uHS.css"])))=>i.map(i=>d[i]); +const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/Login-BvMoAa9d.js","assets/Login-D3svPLnW.css","assets/Setup-_cGdJHOE.js","assets/Setup-BdS_6JlN.css","assets/Analytics-Ca8FcnIo.js","assets/users-qicnhyDs.js","assets/plus-DCrJlDOb.js","assets/Analytics-D5VrZMJ0.css","assets/MyBoard-CdXG-0Ii.js","assets/MyBoard-B7ypQ1JT.css","assets/Training-CHRTy9x1.js","assets/target-CylhW5SK.js","assets/Training-CeWRER6W.css","assets/Personas-BXejtBr0.js","assets/arrow-left-C_e4n_39.js","assets/Personas-CskAUbZv.css","assets/Chat-fU4aRozx.js","assets/Chat-BPfrVhz_.css","assets/Settings-D5LYPIGh.js","assets/Settings-BCN2EZQ5.css","assets/GroupBuilder-4jGJS7rQ.js","assets/GroupBuilder-CpbMByEC.css","assets/GroupEdit-BzqeiR8Z.js","assets/GroupEdit-D9lmggXK.css","assets/AdminUsers-Bpsek0Me.js","assets/AdminUsers-B0sl0uHS.css"])))=>i.map(i=>d[i]); (function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const r of document.querySelectorAll('link[rel="modulepreload"]'))s(r);new MutationObserver(r=>{for(const i of r)if(i.type==="childList")for(const o of i.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&s(o)}).observe(document,{childList:!0,subtree:!0});function n(r){const i={};return r.integrity&&(i.integrity=r.integrity),r.referrerPolicy&&(i.referrerPolicy=r.referrerPolicy),r.crossOrigin==="use-credentials"?i.credentials="include":r.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function s(r){if(r.ep)return;r.ep=!0;const i=n(r);fetch(r.href,i)}})();/** * @vue/shared v3.5.41 * (c) 2018-present Yuxi (Evan) You and Vue contributors @@ -79,4 +79,4 @@ const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/Login-DhaEm1-6. * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. - */const Eu=No("settings",[["path",{d:"M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915",key:"1i5ecw"}],["circle",{cx:"12",cy:"12",r:"3",key:"1v7zrd"}]]),Bs="st_token";function Mo(){return localStorage.getItem(Bs)}function ws(e){e?localStorage.setItem(Bs,e):localStorage.removeItem(Bs)}async function te(e,t,n,s=!1){const r={},i=Mo();i&&(r.Authorization=`Bearer ${i}`);let o=n;!s&&n!==void 0&&n!==null&&(r["Content-Type"]="application/json",o=JSON.stringify(n));const l=await fetch(t,{method:e,headers:r,body:o});let c=null;try{c=await l.json()}catch{}if(!l.ok){const f=c&&(c.error||c.message)||`HTTP ${l.status}`;throw new Error(f)}return c}const Ss={login:(e,t)=>te("POST","/api/auth/login",{username:e,password:t}),me:()=>te("GET","/api/auth/me"),setup:e=>te("POST","/api/auth/setup",e),updateProfile:e=>te("PATCH","/api/auth/profile",e),adminCreateUser:e=>te("POST","/api/admin/users",e),adminListUsers:()=>te("GET","/api/admin/users"),adminUpdateUser:(e,t)=>te("PUT",`/api/admin/users/${e}`,t),createGroup:e=>te("POST","/api/groups",e,!0),listGroups:()=>te("GET","/api/groups"),getGroup:e=>te("GET",`/api/groups/${e}`),analyzeGroup:e=>te("POST",`/api/groups/${e}/analyze`),listPersonas:e=>te("GET",`/api/groups/${e}/personas`),getPersona:(e,t)=>te("GET",`/api/groups/${e}/personas/${t}`),updatePersona:(e,t,n)=>te("PUT",`/api/groups/${e}/personas/${t}`,n),chatStart:(e,t)=>te("POST",`/api/chat/${e}/personas/${t}/chat/start`),chatSend:(e,t,n)=>te("POST",`/api/chat/${e}/personas/${t}/chat/send`,{text:n}),chatFinish:(e,t)=>te("POST",`/api/chat/${e}/personas/${t}/chat/finish`),mySessions:()=>te("GET","/api/chat/sessions"),myBoard:()=>te("GET","/api/me/board"),weakAreas:()=>te("GET","/api/me/weak-areas"),myPersonas:()=>te("GET","/api/me/personas"),generatePersona:e=>te("POST","/api/me/personas/generate",e),analytics:(e={})=>{const t=new URLSearchParams;e.from&&t.set("from",e.from),e.to&&t.set("to",e.to);const n=t.toString();return te("GET",`/api/analytics${n?`?${n}`:""}`)}},Ce=Jt({user:null,token:Mo(),mustSetup:!1,get role(){return this.user?this.user.role:null},get isAdmin(){return this.role==="admin"||this.role==="super_admin"},async load(){var e;if(!this.token)return null;try{const t=await Ss.me();return this.user=t.user,this.mustSetup=!!((e=t.user)!=null&&e.must_setup),this.user}catch{return this.user=null,this.mustSetup=!1,ws(null),null}},async login(e,t){const n=await Ss.login(e,t);return this.token=n.token,ws(n.token),this.user=n.user,this.mustSetup=!!n.must_setup,n.user},async finishSetup(e,t){const n=await Ss.setup({username:this.user.username||this.user.id,email:e,password:t});return this.user=n.user,this.mustSetup=!1,n.user},logout(){this.user=null,this.token=null,this.mustSetup=!1,ws(null)}}),As={en:{app:"Sales Trainer",login:"Login",logout:"Logout",username:"Username",email:"Email",password:"Password",newPassword:"New password",confirmPassword:"Confirm password",save:"Save",passwordTooShort:"Password must be at least 4 characters",passwordMismatch:"Passwords do not match",setupTitle:"Set up your account",setupSubtitle:"First login for ",loginError:"Invalid credentials",dashboard:"Dashboard",training:"Training",myDashboard:"My dashboard",adminOverview:"Admin overview",settings:"Settings",account:"Account",profile:"Profile",readonly:"read-only",name:"Display name",saveProfile:"Save profile",changePassword:"Change password",optional:"optional",saved:"Saved",total:"Total",dateRange:"Date range",apply:"Apply",clear:"Clear",hardestPersonas:"Hardest personas",closeRate:"Close rate",managePersonas:"Manage personas",difficulty:"Difficulty",trained:"Trained",addProduct:"Add product",trainingSubtitle:"Pick a product group, then choose a persona to practice closing the sale.",noTraining:"No training groups available",groups:"Persona Groups",myTraining:"My Training",adminTools:"Admin Tools",users:"Users",analytics:"Analytics",groupBuilder:"Group Builder",create:"Create",analyze:"Analyze",manual:"Manual",edit:"Edit",product:"Product",segment:"Initial customer segment (optional)",description:"Additional description / scenario (optional)",channel:"Channel",facebook:"Facebook",line:"LINE",language:"Language",thai:"Thai",english:"English",personas:"Personas",tierA:"Tier A — Ready to buy",tierB:"Tier B — Unsure",tierC:"Tier C — Not interested but has pain",selectPersona:"Select a persona to practice",chatGuideTitle:"How to practice",chatGuideText:'You are playing the salesperson. Chat with this customer and try to close the sale. Ask about their needs, solve their pain, and handle their objections. When you are done, press "Finish & get result" to see your score and coaching.',chat:"Chat",start:"Start",send:"Send",finish:"Finish & get result",debrief:"Result & Coaching",won:"Won",lost:"Lost",notTried:"Not tried",score:"Score",pain:"Pain",why:"Reason",reveal:"Revealed persona details",generatePersona:"Generate my persona",weakAreas:"My weak areas",mySessions:"My sessions",openSaleTask:"The customer did NOT message first. You must open the sale.",sellerInitiated:"You must open the sale (outbound)",customerInitiated:"The customer will message you first"},th:{app:"ระบบฝึกทักษะการขาย",login:"เข้าสู่ระบบ",logout:"ออกจากระบบ",username:"ชื่อผู้ใช้",email:"อีเมล",password:"รหัสผ่าน",newPassword:"รหัสผ่านใหม่",confirmPassword:"ยืนยันรหัสผ่าน",save:"บันทึก",passwordTooShort:"รหัสผ่านต้องมีความยาวอย่างน้อย 4 ตัวอักษร",passwordMismatch:"รหัสผ่านที่ยืนยันไม่ตรงกัน",setupTitle:"ตั้งค่าบัญชีของคุณ",setupSubtitle:"เข้าสู่ระบบครั้งแรกสำหรับ ",loginError:"ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง กรุณาลองอีกครั้ง",dashboard:"หน้าหลัก",training:"การฝึก",myDashboard:"ภาพรวมผลการฝึก",adminOverview:"ภาพรวมผู้ดูแล",settings:"การตั้งค่า",account:"บัญชี",profile:"ข้อมูลส่วนตัว",readonly:"อ่านอย่างเดียว",name:"ชื่อที่แสดง",saveProfile:"บันทึกข้อมูล",changePassword:"เปลี่ยนรหัสผ่าน",optional:"ไม่บังคับ",saved:"บันทึกเรียบร้อย",total:"ทั้งหมด",dateRange:"ช่วงเวลา",apply:"กรอง",clear:"ล้าง",hardestPersonas:"บุคคลต้นแบบที่ขายยากที่สุด",closeRate:"อัตราปิดการขาย",managePersonas:"จัดการบุคคลต้นแบบ",difficulty:"ระดับความยาก",trained:"ฝึกแล้ว",addProduct:"เพิ่มสินค้า",trainingSubtitle:"เลือกกลุ่มสินค้า แล้วเลือกบุคคลต้นแบบเพื่อฝึกปิดการขาย",noTraining:"ยังไม่มีกลุ่มฝึก",groups:"กลุ่มบุคคลต้นแบบลูกค้า (Persona)",myTraining:"ประวัติการฝึก",adminTools:"เครื่องมือผู้ดูแลระบบ",users:"ผู้ใช้งาน",analytics:"สถิติ",groupBuilder:"สร้างกลุ่มบุคคลต้นแบบ",create:"สร้าง",analyze:"วิเคราะห์",manual:"ระบุเอง",edit:"แก้ไข",product:"สินค้า/บริการ",segment:"กลุ่มลูกค้าเป้าหมายเบื้องต้น (ไม่บังคับ)",description:"คำอธิบายหรือรายละเอียดเพิ่มเติม (ไม่บังคับ)",channel:"ช่องทางติดต่อ",facebook:"Facebook",line:"LINE",language:"ภาษา",thai:"ไทย",english:"อังกฤษ",personas:"บุคคลต้นแบบ",tierA:"ระดับ A — พร้อมตัดสินใจซื้อ",tierB:"ระดับ B — ยังไม่แน่ใจ",tierC:"ระดับ C — ไม่สนใจแต่มีปัญหา",selectPersona:"เลือกบุคคลต้นแบบเพื่อฝึก",chatGuideTitle:"วิธีฝึก",chatGuideText:'คุณรับบทเป็นพนักงานขาย พูดคุยกับลูกค้าคนนี้เพื่อพยายามปิดการขายให้ได้ สอบถามความต้องการ แก้ไขปัญหาของลูกค้า และรับมือกับข้อโต้แย้ง เมื่อพอใจแล้วกด "สรุปผล" เพื่อดูคะแนนและข้อเสนอแนะ',chat:"แชท",start:"เริ่มต้น",send:"ส่ง",finish:"สรุปผล",debrief:"ผลลัพธ์และข้อเสนอแนะ",won:"ปิดการขายได้",lost:"ปิดการขายไม่ได้",notTried:"ยังไม่ได้ฝึก",score:"คะแนน",pain:"ปัญหาของลูกค้า",why:"เหตุผล",reveal:"รายละเอียดบุคคลต้นแบบที่ถูกซ่อนไว้",generatePersona:"สร้างบุคคลต้นแบบเพิ่มเติม",weakAreas:"จุดอ่อนที่ควรฝึกเพิ่มเติม",mySessions:"ประวัติการฝึกของฉัน",openSaleTask:"ลูกค้ายังไม่ได้ทักเข้ามา คุณต้องเป็นฝ่ายเริ่มบทสนทนาการขายเอง",sellerInitiated:"คุณต้องเริ่มการขายในเชิงรุก",customerInitiated:"ลูกค้าจะติดต่อเข้ามาก่อน"}},Ke=Jt({locale:localStorage.getItem("locale")||"th",t(e){return As[this.locale]&&As[this.locale][e]||As.en[e]||e},set(e){this.locale=e,localStorage.setItem("locale",e)}}),wu=(e,t)=>{const n=e.__vccOpts||e;for(const[s,r]of t)n[s]=r;return n},Su={class:"app"},Au={key:0,class:"topnav"},xu={class:"tabs"},Ru={class:"nav-right"},Tu={class:"main"},Cu={__name:"App",setup(e){const t=pu();function n(){Ke.set(Ke.locale==="th"?"en":"th")}function s(){Ce.logout(),t.push("/login")}return Wi(()=>{Ce.token&&!Ce.user&&Ce.load()}),(r,i)=>{const o=_r("router-link"),l=_r("router-view");return on(),Tr("div",Su,[ae(Ce).user?(on(),Tr("nav",Au,[le(o,{to:"/",class:"brand"},{default:Vt(()=>[Ft(At(ae(Ke).t("app")),1)]),_:1}),Rt("div",xu,[ae(Ce).isAdmin?(on(),Ds(o,{key:0,to:"/",class:jt(["tab",{active:r.$route.path==="/"}])},{default:Vt(()=>[Ft(At(ae(Ke).t("adminOverview")),1)]),_:1},8,["class"])):Cr("",!0),le(o,{to:"/my/board",class:jt(["tab",{active:r.$route.path==="/my/board"}])},{default:Vt(()=>[Ft(At(ae(Ke).t("myDashboard")),1)]),_:1},8,["class"]),le(o,{to:"/training",class:jt(["tab",{active:r.$route.path==="/training"}])},{default:Vt(()=>[Ft(At(ae(Ke).t("training")),1)]),_:1},8,["class"])]),Rt("div",Ru,[Rt("button",{onClick:n,class:"lang"},At(ae(Ke).locale==="th"?"EN":"TH"),1),le(o,{to:"/settings",class:"icon-btn",title:ae(Ke).t("settings")},{default:Vt(()=>[le(ae(Eu),{size:20,"stroke-width":1.8})]),_:1},8,["title"]),Rt("button",{onClick:s,class:"logout-btn"},[le(ae(bu),{size:18,"stroke-width":1.8}),Ft(" "+At(ae(Ke).t("logout")),1)])])])):Cr("",!0),Rt("main",Tu,[(on(),Ds(l,{key:ae(Ke).locale}))])])}}},Pu=wu(Cu,[["__scopeId","data-v-25816f6a"]]),Ou="modulepreload",Iu=function(e){return"/"+e},ai={},Te=function(t,n,s){let r=Promise.resolve();if(n&&n.length>0){document.getElementsByTagName("link");const o=document.querySelector("meta[property=csp-nonce]"),l=(o==null?void 0:o.nonce)||(o==null?void 0:o.getAttribute("nonce"));r=Promise.allSettled(n.map(c=>{if(c=Iu(c),c in ai)return;ai[c]=!0;const f=c.endsWith(".css"),u=f?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${c}"]${u}`))return;const h=document.createElement("link");if(h.rel=f?"stylesheet":Ou,f||(h.as="script"),h.crossOrigin="",h.href=c,l&&h.setAttribute("nonce",l),document.head.appendChild(h),f)return new Promise((g,m)=>{h.addEventListener("load",g),h.addEventListener("error",()=>m(new Error(`Unable to preload CSS for ${c}`)))})}))}function i(o){const l=new Event("vite:preloadError",{cancelable:!0});if(l.payload=o,window.dispatchEvent(l),!l.defaultPrevented)throw o}return r.then(o=>{for(const l of o||[])l.status==="rejected"&&i(l.reason);return t().catch(i)})},Nu=[{path:"/login",component:()=>Te(()=>import("./Login-DhaEm1-6.js"),__vite__mapDeps([0,1])),meta:{public:!0}},{path:"/setup",component:()=>Te(()=>import("./Setup-D9lCpt9C.js"),__vite__mapDeps([2,3]))},{path:"/",component:()=>Te(()=>import("./Analytics-CyuFLItl.js"),__vite__mapDeps([4,5,6,7])),meta:{admin:!0}},{path:"/my/board",component:()=>Te(()=>import("./MyBoard-CHF7e7KE.js"),__vite__mapDeps([8,9]))},{path:"/training",component:()=>Te(()=>import("./Training-CH5dgqen.js"),__vite__mapDeps([10,11,6,12]))},{path:"/groups/:gid/personas",component:()=>Te(()=>import("./Personas-CRi-xjVS.js"),__vite__mapDeps([13,11,14,15]))},{path:"/groups/:gid/chat/:pid",component:()=>Te(()=>import("./Chat-Bo6laZZd.js"),__vite__mapDeps([16,11,14,17]))},{path:"/settings",component:()=>Te(()=>import("./Settings-CatYfY0I.js"),__vite__mapDeps([18,19]))},{path:"/admin/new-group",component:()=>Te(()=>import("./GroupBuilder-BTot24Pr.js"),__vite__mapDeps([20,14,21])),meta:{admin:!0}},{path:"/admin/groups/:gid/edit",component:()=>Te(()=>import("./GroupEdit-Db7E5OJj.js"),__vite__mapDeps([22,5,23])),meta:{admin:!0}},{path:"/admin/users",component:()=>Te(()=>import("./AdminUsers-B7nEYj4N.js"),__vite__mapDeps([24,5,25])),meta:{admin:!0}},{path:"/admin/analytics",component:()=>Te(()=>import("./Analytics-CyuFLItl.js"),__vite__mapDeps([4,5,6,7])),meta:{admin:!0}}],Do=hu({history:$a(),routes:Nu});Do.beforeEach(async e=>e.meta.public?!0:(Ce.user||await Ce.load(),Ce.user?Ce.mustSetup&&e.path!=="/setup"?{path:"/setup"}:e.path==="/"&&!Ce.isAdmin?{path:"/my/board"}:e.meta.admin&&!Ce.isAdmin?{path:"/my/board"}:!0:{path:"/login",query:{redirect:e.fullPath}}));ia(Pu).use(Do).mount("#app");export{jt as A,Ds as B,Zs as C,Xc as D,Mn as E,tt as F,Jt as G,Fu as H,Eu as S,wu as _,Rt as a,Hu as b,Tr as c,Vu as d,Cr as e,Ce as f,pu as g,Uu as h,Ke as i,Ft as j,No as k,Wi as l,le as m,Vt as n,on as o,Ks as p,Du as q,dl as r,Ss as s,At as t,ae as u,jr as v,Mu as w,_r as x,Lu as y,Oe as z}; + */const Eu=No("settings",[["path",{d:"M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915",key:"1i5ecw"}],["circle",{cx:"12",cy:"12",r:"3",key:"1v7zrd"}]]),Bs="st_token";function Mo(){return localStorage.getItem(Bs)}function ws(e){e?localStorage.setItem(Bs,e):localStorage.removeItem(Bs)}async function te(e,t,n,s=!1){const r={},i=Mo();i&&(r.Authorization=`Bearer ${i}`);let o=n;!s&&n!==void 0&&n!==null&&(r["Content-Type"]="application/json",o=JSON.stringify(n));const l=await fetch(t,{method:e,headers:r,body:o});let c=null;try{c=await l.json()}catch{}if(!l.ok){const f=c&&(c.error||c.message)||`HTTP ${l.status}`;throw new Error(f)}return c}const Ss={login:(e,t)=>te("POST","/api/auth/login",{username:e,password:t}),me:()=>te("GET","/api/auth/me"),setup:e=>te("POST","/api/auth/setup",e),updateProfile:e=>te("PATCH","/api/auth/profile",e),adminCreateUser:e=>te("POST","/api/admin/users",e),adminListUsers:()=>te("GET","/api/admin/users"),adminUpdateUser:(e,t)=>te("PUT",`/api/admin/users/${e}`,t),createGroup:e=>te("POST","/api/groups",e,!0),listGroups:()=>te("GET","/api/groups"),getGroup:e=>te("GET",`/api/groups/${e}`),analyzeGroup:e=>te("POST",`/api/groups/${e}/analyze`),listPersonas:e=>te("GET",`/api/groups/${e}/personas`),getPersona:(e,t)=>te("GET",`/api/groups/${e}/personas/${t}`),updatePersona:(e,t,n)=>te("PUT",`/api/groups/${e}/personas/${t}`,n),chatStart:(e,t,n)=>te("POST",`/api/chat/${e}/personas/${t}/chat/start`,n||{}),chatSend:(e,t,n)=>te("POST",`/api/chat/${e}/personas/${t}/chat/send`,{text:n}),chatFinish:(e,t)=>te("POST",`/api/chat/${e}/personas/${t}/chat/finish`),mySessions:()=>te("GET","/api/chat/sessions"),myBoard:()=>te("GET","/api/me/board"),weakAreas:()=>te("GET","/api/me/weak-areas"),myPersonas:()=>te("GET","/api/me/personas"),generatePersona:e=>te("POST","/api/me/personas/generate",e),analytics:(e={})=>{const t=new URLSearchParams;e.from&&t.set("from",e.from),e.to&&t.set("to",e.to);const n=t.toString();return te("GET",`/api/analytics${n?`?${n}`:""}`)}},Ce=Jt({user:null,token:Mo(),mustSetup:!1,get role(){return this.user?this.user.role:null},get isAdmin(){return this.role==="admin"||this.role==="super_admin"},async load(){var e;if(!this.token)return null;try{const t=await Ss.me();return this.user=t.user,this.mustSetup=!!((e=t.user)!=null&&e.must_setup),this.user}catch{return this.user=null,this.mustSetup=!1,ws(null),null}},async login(e,t){const n=await Ss.login(e,t);return this.token=n.token,ws(n.token),this.user=n.user,this.mustSetup=!!n.must_setup,n.user},async finishSetup(e,t){const n=await Ss.setup({username:this.user.username||this.user.id,email:e,password:t});return this.user=n.user,this.mustSetup=!1,n.user},logout(){this.user=null,this.token=null,this.mustSetup=!1,ws(null)}}),As={en:{app:"Sales Trainer",login:"Login",logout:"Logout",username:"Username",email:"Email",password:"Password",newPassword:"New password",confirmPassword:"Confirm password",save:"Save",passwordTooShort:"Password must be at least 4 characters",passwordMismatch:"Passwords do not match",setupTitle:"Set up your account",setupSubtitle:"First login for ",loginError:"Invalid credentials",dashboard:"Dashboard",training:"Training",myDashboard:"My dashboard",adminOverview:"Admin overview",settings:"Settings",account:"Account",profile:"Profile",readonly:"read-only",name:"Display name",saveProfile:"Save profile",changePassword:"Change password",optional:"optional",saved:"Saved",total:"Total",dateRange:"Date range",apply:"Apply",clear:"Clear",hardestPersonas:"Hardest personas",closeRate:"Close rate",managePersonas:"Manage personas",difficulty:"Difficulty",trained:"Trained",addProduct:"Add product",trainingSubtitle:"Pick a product group, then choose a persona to practice closing the sale.",noTraining:"No training groups available",groups:"Persona Groups",myTraining:"My Training",adminTools:"Admin Tools",users:"Users",analytics:"Analytics",groupBuilder:"Group Builder",create:"Create",analyze:"Analyze",manual:"Manual",edit:"Edit",product:"Product",segment:"Initial customer segment (optional)",description:"Additional description / scenario (optional)",channel:"Channel",facebook:"Facebook",line:"LINE",language:"Language",thai:"Thai",english:"English",personas:"Personas",tierA:"Tier A — Ready to buy",tierB:"Tier B — Unsure",tierC:"Tier C — Not interested but has pain",selectPersona:"Select a persona to practice",chooseScenario:"Choose a scenario",chatGuideTitle:"How to practice",chatGuideText:'You are playing the salesperson. Chat with this customer and try to close the sale. Ask about their needs, solve their pain, and handle their objections. When you are done, press "Finish & get result" to see your score and coaching.',chat:"Chat",start:"Start",send:"Send",finish:"Finish & get result",debrief:"Result & Coaching",won:"Won",lost:"Lost",notTried:"Not tried",score:"Score",pain:"Pain",why:"Reason",reveal:"Revealed persona details",generatePersona:"Generate my persona",weakAreas:"My weak areas",mySessions:"My sessions",openSaleTask:"The customer did NOT message first. You must open the sale.",sellerInitiated:"You must open the sale (outbound)",customerInitiated:"The customer will message you first"},th:{app:"ระบบฝึกทักษะการขาย",login:"เข้าสู่ระบบ",logout:"ออกจากระบบ",username:"ชื่อผู้ใช้",email:"อีเมล",password:"รหัสผ่าน",newPassword:"รหัสผ่านใหม่",confirmPassword:"ยืนยันรหัสผ่าน",save:"บันทึก",passwordTooShort:"รหัสผ่านต้องมีความยาวอย่างน้อย 4 ตัวอักษร",passwordMismatch:"รหัสผ่านที่ยืนยันไม่ตรงกัน",setupTitle:"ตั้งค่าบัญชีของคุณ",setupSubtitle:"เข้าสู่ระบบครั้งแรกสำหรับ ",loginError:"ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง กรุณาลองอีกครั้ง",dashboard:"หน้าหลัก",training:"การฝึก",myDashboard:"ภาพรวมผลการฝึก",adminOverview:"ภาพรวมผู้ดูแล",settings:"การตั้งค่า",account:"บัญชี",profile:"ข้อมูลส่วนตัว",readonly:"อ่านอย่างเดียว",name:"ชื่อที่แสดง",saveProfile:"บันทึกข้อมูล",changePassword:"เปลี่ยนรหัสผ่าน",optional:"ไม่บังคับ",saved:"บันทึกเรียบร้อย",total:"ทั้งหมด",dateRange:"ช่วงเวลา",apply:"กรอง",clear:"ล้าง",hardestPersonas:"บุคคลต้นแบบที่ขายยากที่สุด",closeRate:"อัตราปิดการขาย",managePersonas:"จัดการบุคคลต้นแบบ",difficulty:"ระดับความยาก",trained:"ฝึกแล้ว",addProduct:"เพิ่มสินค้า",trainingSubtitle:"เลือกกลุ่มสินค้า แล้วเลือกบุคคลต้นแบบเพื่อฝึกปิดการขาย",noTraining:"ยังไม่มีกลุ่มฝึก",groups:"กลุ่มบุคคลต้นแบบลูกค้า (Persona)",myTraining:"ประวัติการฝึก",adminTools:"เครื่องมือผู้ดูแลระบบ",users:"ผู้ใช้งาน",analytics:"สถิติ",groupBuilder:"สร้างกลุ่มบุคคลต้นแบบ",create:"สร้าง",analyze:"วิเคราะห์",manual:"ระบุเอง",edit:"แก้ไข",product:"สินค้า/บริการ",segment:"กลุ่มลูกค้าเป้าหมายเบื้องต้น (ไม่บังคับ)",description:"คำอธิบายหรือรายละเอียดเพิ่มเติม (ไม่บังคับ)",channel:"ช่องทางติดต่อ",facebook:"Facebook",line:"LINE",language:"ภาษา",thai:"ไทย",english:"อังกฤษ",personas:"บุคคลต้นแบบ",tierA:"ระดับ A — พร้อมตัดสินใจซื้อ",tierB:"ระดับ B — ยังไม่แน่ใจ",tierC:"ระดับ C — ไม่สนใจแต่มีปัญหา",selectPersona:"เลือกบุคคลต้นแบบเพื่อฝึก",chooseScenario:"เลือกสถานการณ์",chatGuideTitle:"วิธีฝึก",chatGuideText:'คุณรับบทเป็นพนักงานขาย พูดคุยกับลูกค้าคนนี้เพื่อพยายามปิดการขายให้ได้ สอบถามความต้องการ แก้ไขปัญหาของลูกค้า และรับมือกับข้อโต้แย้ง เมื่อพอใจแล้วกด "สรุปผล" เพื่อดูคะแนนและข้อเสนอแนะ',chat:"แชท",start:"เริ่มต้น",send:"ส่ง",finish:"สรุปผล",debrief:"ผลลัพธ์และข้อเสนอแนะ",won:"ปิดการขายได้",lost:"ปิดการขายไม่ได้",notTried:"ยังไม่ได้ฝึก",score:"คะแนน",pain:"ปัญหาของลูกค้า",why:"เหตุผล",reveal:"รายละเอียดบุคคลต้นแบบที่ถูกซ่อนไว้",generatePersona:"สร้างบุคคลต้นแบบเพิ่มเติม",weakAreas:"จุดอ่อนที่ควรฝึกเพิ่มเติม",mySessions:"ประวัติการฝึกของฉัน",openSaleTask:"ลูกค้ายังไม่ได้ทักเข้ามา คุณต้องเป็นฝ่ายเริ่มบทสนทนาการขายเอง",sellerInitiated:"คุณต้องเริ่มการขายในเชิงรุก",customerInitiated:"ลูกค้าจะติดต่อเข้ามาก่อน"}},Ke=Jt({locale:localStorage.getItem("locale")||"th",t(e){return As[this.locale]&&As[this.locale][e]||As.en[e]||e},set(e){this.locale=e,localStorage.setItem("locale",e)}}),wu=(e,t)=>{const n=e.__vccOpts||e;for(const[s,r]of t)n[s]=r;return n},Su={class:"app"},Au={key:0,class:"topnav"},xu={class:"tabs"},Ru={class:"nav-right"},Tu={class:"main"},Cu={__name:"App",setup(e){const t=pu();function n(){Ke.set(Ke.locale==="th"?"en":"th")}function s(){Ce.logout(),t.push("/login")}return Wi(()=>{Ce.token&&!Ce.user&&Ce.load()}),(r,i)=>{const o=_r("router-link"),l=_r("router-view");return on(),Tr("div",Su,[ae(Ce).user?(on(),Tr("nav",Au,[le(o,{to:"/",class:"brand"},{default:Vt(()=>[Ft(At(ae(Ke).t("app")),1)]),_:1}),Rt("div",xu,[ae(Ce).isAdmin?(on(),Ds(o,{key:0,to:"/",class:jt(["tab",{active:r.$route.path==="/"}])},{default:Vt(()=>[Ft(At(ae(Ke).t("adminOverview")),1)]),_:1},8,["class"])):Cr("",!0),le(o,{to:"/my/board",class:jt(["tab",{active:r.$route.path==="/my/board"}])},{default:Vt(()=>[Ft(At(ae(Ke).t("myDashboard")),1)]),_:1},8,["class"]),le(o,{to:"/training",class:jt(["tab",{active:r.$route.path==="/training"}])},{default:Vt(()=>[Ft(At(ae(Ke).t("training")),1)]),_:1},8,["class"])]),Rt("div",Ru,[Rt("button",{onClick:n,class:"lang"},At(ae(Ke).locale==="th"?"EN":"TH"),1),le(o,{to:"/settings",class:"icon-btn",title:ae(Ke).t("settings")},{default:Vt(()=>[le(ae(Eu),{size:20,"stroke-width":1.8})]),_:1},8,["title"]),Rt("button",{onClick:s,class:"logout-btn"},[le(ae(bu),{size:18,"stroke-width":1.8}),Ft(" "+At(ae(Ke).t("logout")),1)])])])):Cr("",!0),Rt("main",Tu,[(on(),Ds(l,{key:ae(Ke).locale}))])])}}},Pu=wu(Cu,[["__scopeId","data-v-25816f6a"]]),Ou="modulepreload",Iu=function(e){return"/"+e},ai={},Te=function(t,n,s){let r=Promise.resolve();if(n&&n.length>0){document.getElementsByTagName("link");const o=document.querySelector("meta[property=csp-nonce]"),l=(o==null?void 0:o.nonce)||(o==null?void 0:o.getAttribute("nonce"));r=Promise.allSettled(n.map(c=>{if(c=Iu(c),c in ai)return;ai[c]=!0;const f=c.endsWith(".css"),u=f?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${c}"]${u}`))return;const h=document.createElement("link");if(h.rel=f?"stylesheet":Ou,f||(h.as="script"),h.crossOrigin="",h.href=c,l&&h.setAttribute("nonce",l),document.head.appendChild(h),f)return new Promise((g,m)=>{h.addEventListener("load",g),h.addEventListener("error",()=>m(new Error(`Unable to preload CSS for ${c}`)))})}))}function i(o){const l=new Event("vite:preloadError",{cancelable:!0});if(l.payload=o,window.dispatchEvent(l),!l.defaultPrevented)throw o}return r.then(o=>{for(const l of o||[])l.status==="rejected"&&i(l.reason);return t().catch(i)})},Nu=[{path:"/login",component:()=>Te(()=>import("./Login-BvMoAa9d.js"),__vite__mapDeps([0,1])),meta:{public:!0}},{path:"/setup",component:()=>Te(()=>import("./Setup-_cGdJHOE.js"),__vite__mapDeps([2,3]))},{path:"/",component:()=>Te(()=>import("./Analytics-Ca8FcnIo.js"),__vite__mapDeps([4,5,6,7])),meta:{admin:!0}},{path:"/my/board",component:()=>Te(()=>import("./MyBoard-CdXG-0Ii.js"),__vite__mapDeps([8,9]))},{path:"/training",component:()=>Te(()=>import("./Training-CHRTy9x1.js"),__vite__mapDeps([10,11,6,12]))},{path:"/groups/:gid/personas",component:()=>Te(()=>import("./Personas-BXejtBr0.js"),__vite__mapDeps([13,11,14,15]))},{path:"/groups/:gid/chat/:pid",component:()=>Te(()=>import("./Chat-fU4aRozx.js"),__vite__mapDeps([16,11,14,17]))},{path:"/settings",component:()=>Te(()=>import("./Settings-D5LYPIGh.js"),__vite__mapDeps([18,19]))},{path:"/admin/new-group",component:()=>Te(()=>import("./GroupBuilder-4jGJS7rQ.js"),__vite__mapDeps([20,14,21])),meta:{admin:!0}},{path:"/admin/groups/:gid/edit",component:()=>Te(()=>import("./GroupEdit-BzqeiR8Z.js"),__vite__mapDeps([22,5,23])),meta:{admin:!0}},{path:"/admin/users",component:()=>Te(()=>import("./AdminUsers-Bpsek0Me.js"),__vite__mapDeps([24,5,25])),meta:{admin:!0}},{path:"/admin/analytics",component:()=>Te(()=>import("./Analytics-Ca8FcnIo.js"),__vite__mapDeps([4,5,6,7])),meta:{admin:!0}}],Do=hu({history:$a(),routes:Nu});Do.beforeEach(async e=>e.meta.public?!0:(Ce.user||await Ce.load(),Ce.user?Ce.mustSetup&&e.path!=="/setup"?{path:"/setup"}:e.path==="/"&&!Ce.isAdmin?{path:"/my/board"}:e.meta.admin&&!Ce.isAdmin?{path:"/my/board"}:!0:{path:"/login",query:{redirect:e.fullPath}}));ia(Pu).use(Do).mount("#app");export{jt as A,Ds as B,Zs as C,Xc as D,Mn as E,tt as F,Jt as G,Fu as H,Eu as S,wu as _,Rt as a,Hu as b,Tr as c,Vu as d,Cr as e,Ce as f,pu as g,Uu as h,Ke as i,Ft as j,No as k,Wi as l,le as m,Vt as n,on as o,Ks as p,Du as q,dl as r,Ss as s,At as t,ae as u,jr as v,Mu as w,_r as x,Lu as y,Oe as z}; diff --git a/frontend/dist/assets/plus-Cw7xc8qj.js b/frontend/dist/assets/plus-DCrJlDOb.js similarity index 86% rename from frontend/dist/assets/plus-Cw7xc8qj.js rename to frontend/dist/assets/plus-DCrJlDOb.js index d2f764e..a8ab728 100644 --- a/frontend/dist/assets/plus-Cw7xc8qj.js +++ b/frontend/dist/assets/plus-DCrJlDOb.js @@ -1,4 +1,4 @@ -import{k as e}from"./index-C0Orcg9B.js";/** +import{k as e}from"./index-tI5UDFg-.js";/** * @license lucide-vue-next v1.0.0 - ISC * * This source code is licensed under the ISC license. diff --git a/frontend/dist/assets/target-qgda8bhm.js b/frontend/dist/assets/target-CylhW5SK.js similarity index 88% rename from frontend/dist/assets/target-qgda8bhm.js rename to frontend/dist/assets/target-CylhW5SK.js index 04acc22..7c028e8 100644 --- a/frontend/dist/assets/target-qgda8bhm.js +++ b/frontend/dist/assets/target-CylhW5SK.js @@ -1,4 +1,4 @@ -import{k as c}from"./index-C0Orcg9B.js";/** +import{k as c}from"./index-tI5UDFg-.js";/** * @license lucide-vue-next v1.0.0 - ISC * * This source code is licensed under the ISC license. diff --git a/frontend/dist/assets/users-D5qk1Nmy.js b/frontend/dist/assets/users-qicnhyDs.js similarity index 90% rename from frontend/dist/assets/users-D5qk1Nmy.js rename to frontend/dist/assets/users-qicnhyDs.js index 0d577c4..0028c4e 100644 --- a/frontend/dist/assets/users-D5qk1Nmy.js +++ b/frontend/dist/assets/users-qicnhyDs.js @@ -1,4 +1,4 @@ -import{k as e}from"./index-C0Orcg9B.js";/** +import{k as e}from"./index-tI5UDFg-.js";/** * @license lucide-vue-next v1.0.0 - ISC * * This source code is licensed under the ISC license. diff --git a/frontend/dist/index.html b/frontend/dist/index.html index 25332c7..fc43073 100644 --- a/frontend/dist/index.html +++ b/frontend/dist/index.html @@ -4,7 +4,7 @@ Sales Trainer - + diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index 781870a..2a85b74 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -47,7 +47,7 @@ export const api = { listPersonas: (gid) => request('GET', `/api/groups/${gid}/personas`), getPersona: (gid, pid) => request('GET', `/api/groups/${gid}/personas/${pid}`), updatePersona: (gid, pid, b) => request('PUT', `/api/groups/${gid}/personas/${pid}`, b), - chatStart: (gid, pid) => request('POST', `/api/chat/${gid}/personas/${pid}/chat/start`), + chatStart: (gid, pid, scenario) => request('POST', `/api/chat/${gid}/personas/${pid}/chat/start`, scenario || {}), chatSend: (gid, pid, text) => request('POST', `/api/chat/${gid}/personas/${pid}/chat/send`, { text }), chatFinish: (gid, pid) => request('POST', `/api/chat/${gid}/personas/${pid}/chat/finish`), mySessions: () => request('GET', '/api/chat/sessions'), diff --git a/frontend/src/i18n/index.js b/frontend/src/i18n/index.js index 4170325..6c75340 100644 --- a/frontend/src/i18n/index.js +++ b/frontend/src/i18n/index.js @@ -66,6 +66,7 @@ const messages = { tierB: 'Tier B — Unsure', tierC: 'Tier C — Not interested but has pain', selectPersona: 'Select a persona to practice', + chooseScenario: 'Choose a scenario', chatGuideTitle: 'How to practice', chatGuideText: 'You are playing the salesperson. Chat with this customer and try to close the sale. Ask about their needs, solve their pain, and handle their objections. When you are done, press "Finish & get result" to see your score and coaching.', chat: 'Chat', @@ -151,6 +152,7 @@ const messages = { tierB: 'ระดับ B — ยังไม่แน่ใจ', tierC: 'ระดับ C — ไม่สนใจแต่มีปัญหา', selectPersona: 'เลือกบุคคลต้นแบบเพื่อฝึก', + chooseScenario: 'เลือกสถานการณ์', chatGuideTitle: 'วิธีฝึก', chatGuideText: 'คุณรับบทเป็นพนักงานขาย พูดคุยกับลูกค้าคนนี้เพื่อพยายามปิดการขายให้ได้ สอบถามความต้องการ แก้ไขปัญหาของลูกค้า และรับมือกับข้อโต้แย้ง เมื่อพอใจแล้วกด "สรุปผล" เพื่อดูคะแนนและข้อเสนอแนะ', chat: 'แชท', diff --git a/frontend/src/views/Chat.vue b/frontend/src/views/Chat.vue index 1369a50..a93a2a9 100644 --- a/frontend/src/views/Chat.vue +++ b/frontend/src/views/Chat.vue @@ -2,62 +2,85 @@
{{ i18n.t('personas') }} - -
- {{ i18n.t('chatGuideTitle') }} -

{{ i18n.t('chatGuideText') }}

-
- -
-

{{ persona ? persona.name : '...' }}

- {{ persona ? persona.channel : '' }} - {{ persona.profession }} · {{ persona.age_group }} -
- -
- 📣 {{ i18n.t('sellerInitiated') }} -
{{ taskText }}
-
- - -
-
- {{ m.text }} + +
+ +
+ {{ i18n.t('chatGuideTitle') }} +

{{ i18n.t('chatGuideText') }}

-
...
-
- -
- - -
- - -
-

{{ i18n.t('debrief') }}

-

{{ debrief.outcome === 'won' ? i18n.t('won') : i18n.t('lost') }} - — {{ i18n.t('score') }}: {{ debrief.score }}

-

{{ i18n.t('pain') }}: {{ debrief.pain || '—' }}

-

{{ i18n.t('why') }}: {{ debrief.why }}

-
- Coaching: -
  • {{ c }}
+
+

{{ persona ? persona.name : '...' }}

+ {{ persona ? persona.channel : '' }} + {{ persona.profession }} · {{ persona.age_group }} +
-
- {{ i18n.t('reveal') }} -
-
- {{ fieldLabel(k) }} - {{ fmt(v) }} -
+ + +
+
{{ m.text }}
+
...
+
+ + +
+ + +
+ + +
+

{{ i18n.t('debrief') }}

+

{{ debrief.outcome === 'won' ? i18n.t('won') : i18n.t('lost') }} + — {{ i18n.t('score') }}: {{ debrief.score }}

+

{{ i18n.t('pain') }}: {{ debrief.pain || '—' }}

+

{{ i18n.t('why') }}: {{ debrief.why }}

+
+ Coaching: +
  • {{ c }}
-
- +
+ {{ i18n.t('reveal') }} +
+
+ {{ fieldLabel(k) }} + {{ fmt(v) }} +
+
+
+ +
@@ -74,13 +97,20 @@ const gid = route.params.gid const pid = route.params.pid const persona = ref(null) -const started = ref(false) +const phase = ref('pick') // 'pick' | 'chat' | 'done' const messages = ref([]) const text = ref('') const sending = ref(false) const debrief = ref(null) const taskText = ref('') const sessionId = ref(null) +const picked = ref('social') + +const scenarios = [ + { id: 'social', emoji: '💬', init: 'customer', label: 'Social Media (แชท)', desc: 'ลูกค้าทักมาหาคุณก่อน — โทนสั้น ทักๆ ตามสไตล์แชท' }, + { id: 'f2f_call', emoji: '📞', init: 'seller', label: 'พบหน้า / โทรศัพท์', desc: 'คุณต้องเป็นฝ่ายเปิดการสนทนาเชิงรุกกับลีด (ผู้ฝึกทักก่อน)' }, + { id: 'recontact', emoji: '⏳', init: 'customer', label: 'ลูกค้ากลับมาติดต่อ (1-3 เดือน)', desc: 'เคยได้รับข้อมูลไปแล้ว ตอนนี้กลับมาติดต่อ พร้อมตัดสินใจมากขึ้น' }, +] function scrollDown() { nextTick(() => { @@ -89,16 +119,17 @@ function scrollDown() { } const thread = ref(null) +async function begin() { + phase.value = 'chat' + const res = await api.chatStart(gid, pid, { scenario: picked.value }) + sessionId.value = res.session.id + if (res.session.task) taskText.value = res.session.task + messages.value = res.session.messages || [] + if (messages.value.length) scrollDown() +} + onMounted(async () => { persona.value = (await api.getPersona(gid, pid)).persona - const res = await api.chatStart(gid, pid) - sessionId.value = res.session.id - if (res.session.task) { - taskText.value = res.session.task - } - messages.value = res.session.messages || [] - started.value = true - if (messages.value.length) scrollDown() }) async function send() { @@ -123,6 +154,7 @@ async function finish() { const res = await api.chatFinish(gid, pid) debrief.value = res.debrief messages.value = res.session.messages + phase.value = 'done' } catch (e) { alert(e.message) } finally { @@ -162,12 +194,15 @@ function fmt(v) { gap: 8px; } .bubble { max-width: 72%; padding: 10px 14px; white-space: pre-wrap; word-break: break-word; } +.msg-system { align-self: center; background: #fef3c7; color: #92400e; font-size: 12px; max-width: 88%; border-radius: 999px; } .composer { display: flex; gap: 8px; margin-top: 12px; } .task { margin-bottom: 12px; background: #fff7ed; border-color: #fed7aa; } .debrief { margin-top: 16px; } -.json { background: #0f172a; color: #9ca3af; padding: 10px; border-radius: 8px; font-size: 11px; overflow: auto; max-height: 260px; } button.danger { background: var(--red); color: #fff; border: none; } .guide { background: #eef2ff; border-color: #c7d2fe; margin-bottom: 12px; } +.scenario { border: 2px solid var(--border); border-radius: 12px; padding: 12px 14px; margin: 10px 0; cursor: pointer; transition: border-color .15s ease, background .15s ease; } +.scenario:hover { border-color: var(--accent); } +.scenario.sel { border-color: var(--accent); background: #eef2ff; } .reveal-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 8px; } .rev { display: flex; flex-direction: column; background: #f8fafc; border: 1px solid var(--border); border-radius: 8px; padding: 8px 10px; } .rk { font-size: 12px; color: var(--muted); }