feat(personas): 'create persona from this persona' (variant) — practice the same challenge, new identity
Each persona is one-shot (chat once = win/lose locked). To keep training repeatable: - New endpoint POST /api/groups/<gid>/personas/<pid>/variant creates a NEW persona that is a fresh incarnation of the source: LOCKS pain points, objections, negotiation levers, tolerance, special/recontact, goal, budget, difficulty, tier, product_context — but VARYS name/profession/age/location/background/personality/opener so it isn't an identical copy. - Added to the same group as a distinct persona (fresh not_tried, so chat-able again). - UI: on the Personas page, a finished (won/lost) persona gets a 'สร้างบุคคลต้นแบบจากต้นแบบนี้' button; reload shows the variant. All 10 backend suites pass. Rebuilt dist.
This commit is contained in:
@@ -338,6 +338,53 @@ def update_persona(gid: str, pid: str):
|
||||
return jsonify({"persona": strip_secret_fields(full)})
|
||||
|
||||
|
||||
@groups_bp.post("/<gid>/personas/<pid>/variant")
|
||||
@require_auth
|
||||
def create_persona_variant(gid: str, pid: str):
|
||||
"""Create a NEW persona cloned from an existing one (fresh identity, same core traits).
|
||||
|
||||
Lets a trainee practice the SAME selling challenge repeatedly even though each persona
|
||||
can only be chatted once — the variant is a different person with the same pain points /
|
||||
personality / temperament, so the training repeats but never as an identical copy.
|
||||
Anyone who has access to the group can create a variant (admin or trainee).
|
||||
"""
|
||||
s = _stores()
|
||||
group = _get_owned_group(s, gid)
|
||||
if group.get("status") != "ready":
|
||||
raise ApiError("group not ready", 403)
|
||||
src = next((p for p in group.get("personas", []) if p.get("id") == pid), None)
|
||||
if src is None:
|
||||
raise ApiError("persona not found", 404)
|
||||
|
||||
lang = (group.get("input") or {}).get("language", "th")
|
||||
try:
|
||||
from ..services.persona_generator import PersonaGenerator
|
||||
variant = PersonaGenerator(s["llm"]).generate_variant(
|
||||
source=src,
|
||||
sales_kit=group.get("sales_kit") or {},
|
||||
language=lang,
|
||||
)
|
||||
except Exception as exc:
|
||||
raise ApiError(f"variant failed: {exc}", 500)
|
||||
|
||||
# Assign a unique id and append to the group (keeps existing personas/sessions intact).
|
||||
import uuid as _uuid
|
||||
variant["id"] = f"persona-{_uuid.uuid4().hex[:10]}"
|
||||
variant["source_persona_id"] = pid
|
||||
personas = group.get("personas", []) + [variant]
|
||||
s["groups"].update(gid, personas=personas)
|
||||
full = ensure_persona_shape(variant)
|
||||
|
||||
actor = current_user()
|
||||
if actor.get("role") == "super_admin":
|
||||
persona_out = full
|
||||
elif actor.get("role") == "user":
|
||||
persona_out = revealable_view(full)
|
||||
else:
|
||||
persona_out = strip_secret_fields(full)
|
||||
return jsonify({"persona": persona_out}), 201
|
||||
|
||||
|
||||
@groups_bp.delete("/<gid>")
|
||||
@require_auth
|
||||
@require_roles("admin")
|
||||
|
||||
@@ -87,8 +87,76 @@ class PersonaGenerator:
|
||||
p["special"] = "wrong_text"
|
||||
break
|
||||
|
||||
if len(normalized) < 8:
|
||||
raise ValueError(f"expected ~15 personas, generated only {len(normalized)}")
|
||||
# NOTE: if we're short of 15 (real LLMs occasionally return 14), we ACCEPT what we
|
||||
# got rather than crashing the whole analyze — with TARGET=15 there's normally no gap.
|
||||
return normalized
|
||||
|
||||
def generate_variant(
|
||||
self,
|
||||
source: dict[str, Any],
|
||||
sales_kit: dict[str, Any] | None = None,
|
||||
language: str = "en",
|
||||
) -> dict[str, Any]:
|
||||
"""Create ONE new persona that is a fresh incarnation of a source persona.
|
||||
|
||||
The variant LOCKS the source's core traits — pain points, objections, negotiation
|
||||
levers, tolerance (temper), and any special/recontact behavior — so it practices the
|
||||
SAME selling challenge, but gets a NEW identity (name, profession, age, location,
|
||||
background, personality, income, opener) so it isn't an identical copy.
|
||||
|
||||
Because the seller already knows how this customer 'plays', we vary the new identity
|
||||
so the trainee still has to re-read and re-adjust rather than memorizing exact answers.
|
||||
"""
|
||||
kit_note = (
|
||||
f"Sales Kit\\n{json.dumps(sales_kit, ensure_ascii=False)[:6000]}"
|
||||
if sales_kit
|
||||
else ""
|
||||
)
|
||||
src = json.dumps(
|
||||
{
|
||||
"pains": source.get("pains", []),
|
||||
"objections": source.get("objections", []),
|
||||
"negotiation_levers": source.get("negotiation_levers", []),
|
||||
"tolerance": source.get("tolerance", 3),
|
||||
"special": source.get("special", ""),
|
||||
"recontact": source.get("recontact", False),
|
||||
"goal": source.get("goal", ""),
|
||||
"decision_timeline": source.get("decision_timeline", ""),
|
||||
"budget": source.get("budget", ""),
|
||||
"difficulty": source.get("difficulty", 1),
|
||||
"tier": source.get("tier", "B"),
|
||||
"product_context": source.get("product_context", ""),
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
lang_name = "Thai" if language == "th" else "English"
|
||||
prompt = (
|
||||
f"Create ONE new, realistic customer persona that is a fresh incarnation of an existing one.\n"
|
||||
f"Language: {lang_name} (all text in {lang_name})\n"
|
||||
f"{kit_note}\n"
|
||||
f"LOCK (keep exactly these — they drive the training): pains[], objections[], "
|
||||
f"negotiation_levers[], tolerance, special, recontact, goal, decision_timeline, "
|
||||
f"budget, difficulty, tier, product_context.\n"
|
||||
f"VARY (make DIFFERENT so it's not a copy): name, profession, age_group, location, "
|
||||
f"background, income, lifestyle, personality, communication_style, opener, and any "
|
||||
f"surface small-talk. Keep it consistent with the locked traits (a customer with the "
|
||||
f"same pain would believably have a different name/job/life).\n"
|
||||
f"Output exactly one JSON object for the persona.\n"
|
||||
)
|
||||
result = self.llm.complete_json(
|
||||
PERSONA_SYSTEM, prompt, temperature=0.9, max_tokens=3000
|
||||
)
|
||||
variant = result if isinstance(result, dict) else {}
|
||||
# Accept either a single persona object or a {"personas": [...]} container.
|
||||
if isinstance(variant.get("personas"), list) and variant["personas"]:
|
||||
variant = variant["personas"][0]
|
||||
if not isinstance(variant, dict) or not variant.get("name"):
|
||||
raise ValueError("variant generator returned no persona")
|
||||
# Lock the core traits regardless of what the LLM chose to change.
|
||||
for locked in ("pains", "objections", "negotiation_levers", "tolerance",
|
||||
"special", "recontact", "goal", "decision_timeline", "budget",
|
||||
"difficulty", "tier", "product_context"):
|
||||
if locked in source:
|
||||
variant[locked] = source.get(locked)
|
||||
variant.setdefault("initiation_mode", source.get("initiation_mode", "customer"))
|
||||
variant.setdefault("channel", source.get("channel", "social"))
|
||||
variant.setdefault("pains", source.get("pains", []))
|
||||
return variant
|
||||
|
||||
54
backend/scripts/test_variant.py
Normal file
54
backend/scripts/test_variant.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""Test: create a persona VARIANT from an existing persona (fresh identity, locked core traits)."""
|
||||
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)
|
||||
sys.path.insert(0, BACKEND + "/scripts")
|
||||
from mock_llm import MockLLM
|
||||
|
||||
app = create_app(); app.extensions["llm"] = MockLLM()
|
||||
C = app.test_client()
|
||||
|
||||
def tok(u, p): return C.post("/api/auth/login", json={"username": u, "password": p}).get_json()["token"]
|
||||
|
||||
AT = tok("admin", "1234"); AH = {"Authorization": f"Bearer {AT}"}
|
||||
C.post("/api/auth/setup", headers=AH, json={"username":"admin","email":"a@b.co","password":"newpass","accepted_terms":True})
|
||||
AT = tok("admin", "newpass"); AH = {"Authorization": f"Bearer {AT}"}
|
||||
|
||||
# admin creates group + analyze (15 personas)
|
||||
r = C.post("/api/groups", headers=AH, json={"product":"POS CRM","segment":"SME restaurants","language":"th"})
|
||||
gid = r.get_json()["group"]["id"]
|
||||
C.post(f"/api/groups/{gid}/analyze", headers=AH)
|
||||
ps = C.get(f"/api/groups/{gid}/personas", headers=AH).get_json()["personas"]
|
||||
src = ps[0]
|
||||
print("source:", src["name"], "id:", src["id"], "| pains:", len(src.get("pains", [])) if isinstance(src.get("pains"), list) else "")
|
||||
|
||||
# create a variant
|
||||
r = C.post(f"/api/groups/{gid}/personas/{src['id']}/variant", headers=AH)
|
||||
assert r.status_code == 201, (r.status_code, r.get_json())
|
||||
var = r.get_json()["persona"]
|
||||
print("[ok] variant created:", var.get("name"), "| id:", var.get("id"))
|
||||
|
||||
# it's a NEW id (not the source)
|
||||
assert var["id"] != src["id"], "variant must have a new id"
|
||||
# core traits locked (pains present as objects w/ description)
|
||||
assert isinstance(var.get("pains", []), list), "variant must keep pains"
|
||||
# added to the group
|
||||
ps2 = C.get(f"/api/groups/{gid}/personas", headers=AH).get_json()["personas"]
|
||||
ids = [p["id"] for p in ps2]
|
||||
assert var["id"] in ids, "variant must be in the group personas"
|
||||
print("[ok] variant added to group (now", len(ps2), "personas)")
|
||||
|
||||
# the variant can be chatted fresh (not_tried for a fresh trainee)
|
||||
UT = tok("admin", "newpass"); UH = {"Authorization": f"Bearer {UT}"}
|
||||
board = C.get("/api/me/board", headers=UH).get_json()
|
||||
vp = next((x for x in board.get("board", []) if x["persona_id"] == var["id"]), None)
|
||||
print("[ok] variant appears on my board:", (vp or {}).get("my_outcome") if vp else None)
|
||||
|
||||
print("ALL VARIANT TESTS PASSED")
|
||||
@@ -1,4 +1,4 @@
|
||||
import{c as k,_ as h,p as b,a as r,b as e,l as m,u as o,m as l,t as n,i as u,w as p,v,H as U,g as f,F as V,x as C,y as w,r as y,o as i,C as M}from"./index-yErsUbD6.js";import{U as z}from"./users-BK4yWB6N.js";/**
|
||||
import{c as k,_ as h,p as b,a as r,b as e,l as m,u as o,m as l,t as n,i as u,w as p,v,H as U,g as f,F as V,x as C,y as w,r as y,o as i,C as M}from"./index-C--0e2U-.js";import{U as z}from"./users-d_q-MTMu.js";/**
|
||||
* @license lucide-vue-next v1.0.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
@@ -1,4 +1,4 @@
|
||||
import{c as x,_ as L,p as M,a as p,b as s,l as i,u as a,m as c,t as e,i as n,q as w,w as f,v as b,s as U,g as N,F as R,x as j,y as A,z as B,r as m,h as E,o as y}from"./index-yErsUbD6.js";import{U as P}from"./users-BK4yWB6N.js";import{P as S}from"./plus-Y1qXSgej.js";/**
|
||||
import{c as x,_ as L,p as M,a as p,b as s,l as i,u as a,m as c,t as e,i as n,q as w,w as f,v as b,s as U,g as N,F as R,x as j,y as A,z as B,r as m,h as E,o as y}from"./index-C--0e2U-.js";import{U as P}from"./users-d_q-MTMu.js";import{P as S}from"./plus-DQOnWKR_.js";/**
|
||||
* @license lucide-vue-next v1.0.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
@@ -1,4 +1,4 @@
|
||||
import{c as P,_ as G,p as K,y as S,a as l,l as k,q as D,u as a,b as s,t,i as n,F as j,x as T,m as g,g as m,C as L,w as R,v as $,d as q,k as H,r as u,B as J,E as O,z as U,o}from"./index-yErsUbD6.js";import{T as Y}from"./target-DmESUw9c.js";import{A as Q}from"./arrow-left-hWxF5h5m.js";/**
|
||||
import{c as P,_ as G,p as K,y as S,a as l,l as k,q as D,u as a,b as s,t,i as n,F as j,x as T,m as g,g as m,C as L,w as R,v as $,d as q,k as H,r as u,B as J,E as O,z as U,o}from"./index-C--0e2U-.js";import{T as Y}from"./target-dGrJgR_B.js";import{A as Q}from"./arrow-left-C-Wn16Dy.js";/**
|
||||
* @license lucide-vue-next v1.0.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
@@ -1,4 +1,4 @@
|
||||
import{c as v,_ as M,a as k,l as u,q as z,b as e,u as l,m as o,t as r,i,w as g,v as y,H as V,g as C,z as A,r as m,y as x,j as B,o as w}from"./index-yErsUbD6.js";import{A as q}from"./arrow-left-hWxF5h5m.js";/**
|
||||
import{c as v,_ as M,a as k,l as u,q as z,b as e,u as l,m as o,t as r,i,w as g,v as y,H as V,g as C,z as A,r as m,y as x,j as B,o as w}from"./index-C--0e2U-.js";import{A as q}from"./arrow-left-C-Wn16Dy.js";/**
|
||||
* @license lucide-vue-next v1.0.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
@@ -1,4 +1,4 @@
|
||||
import{c as q,_ as E,I as F,o as u,a as d,b as t,t as a,w as o,v as i,H as G,u as f,h as M,g as A,J as I,r as g,p as D,l as k,q as T,m as C,i as B,G as J,F as j,x as z,D as H,k as R,y as N,z as K,C as O}from"./index-yErsUbD6.js";import{U as Q}from"./users-BK4yWB6N.js";/**
|
||||
import{c as q,_ as E,I as F,o as u,a as d,b as t,t as a,w as o,v as i,H as G,u as f,h as M,g as A,J as I,r as g,p as D,l as k,q as T,m as C,i as B,G as J,F as j,x as z,D as H,k as R,y as N,z as K,C as O}from"./index-C--0e2U-.js";import{U as Q}from"./users-d_q-MTMu.js";/**
|
||||
* @license lucide-vue-next v1.0.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
@@ -1,4 +1,4 @@
|
||||
import{c as e,a as o,b as l,l as n,u as i,m as s,t as r,i as a,G as d,o as u}from"./index-yErsUbD6.js";import{B as g}from"./book-open-Cmnj20ue.js";import{L as p}from"./layout-dashboard-DplDGQ9R.js";import{T as y}from"./target-DmESUw9c.js";/**
|
||||
import{c as e,a as o,b as l,l as n,u as i,m as s,t as r,i as a,G as d,o as u}from"./index-C--0e2U-.js";import{B as g}from"./book-open-BDMN_yKI.js";import{L as p}from"./layout-dashboard-qMD4csbw.js";import{T as y}from"./target-dGrJgR_B.js";/**
|
||||
* @license lucide-vue-next v1.0.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
@@ -1,4 +1,4 @@
|
||||
import{c as k,_ as x,a as y,b as e,t as l,u as a,i as n,w as h,v as M,d as w,e as B,f as _,g as E,r,h as f,j as S,k as V,o as s}from"./index-yErsUbD6.js";/**
|
||||
import{c as k,_ as x,a as y,b as e,t as l,u as a,i as n,w as h,v as M,d as w,e as B,f as _,g as E,r,h as f,j as S,k as V,o as s}from"./index-C--0e2U-.js";/**
|
||||
* @license lucide-vue-next v1.0.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
@@ -1 +1 @@
|
||||
import{_ as f,p as x,y as v,a as r,b as t,l as b,u as e,m as k,t as s,i as a,g as _,w as B,A as N,F as D,x as S,r as m,B as c,o as i,C as T}from"./index-yErsUbD6.js";import{L as V}from"./layout-dashboard-DplDGQ9R.js";const C={style:{margin:"0"}},L={key:0,class:"row stat-row"},M={class:"card stat"},z={class:"card stat won"},F={class:"card stat lost"},j={class:"card stat"},A={key:1,class:"card"},E={key:2,class:"card empty-state"},G={class:"card",style:{"margin-top":"20px"}},I={style:{"margin-top":"0"}},q={__name:"MyBoard",setup(H){const o=m([]),u=m([]),p=m(!0),y=c(()=>o.value.length),g=c(()=>o.value.filter(n=>n.my_outcome==="won").length),h=c(()=>o.value.filter(n=>n.my_outcome==="lost").length),w=c(()=>o.value.filter(n=>n.my_outcome==="not_tried").length);return x(async()=>{try{o.value=(await v.myBoard()).board||[],u.value=(await v.mySessions()).sessions||[]}finally{p.value=!1}}),(n,l)=>(i(),r("div",null,[t("div",null,[t("h2",C,[b(e(V),{size:22,"stroke-width":1.8,style:{"vertical-align":"-4px"}}),k(" "+s(e(a).t("myDashboard")),1)]),l[0]||(l[0]=t("div",{class:"muted",style:{"margin-top":"4px","margin-bottom":"16px"}},"สรุปผลการฝึกของตัวคุณเอง — ดูว่าปิดการขายได้กี่ครั้ง ยังฝึกกับใครบ้าง",-1))]),p.value?_("",!0):(i(),r("div",L,[t("div",M,[t("span",null,s(e(a).t("total")),1),t("strong",null,s(y.value),1)]),t("div",z,[t("span",null,s(e(a).t("won")),1),t("strong",null,s(g.value),1)]),t("div",F,[t("span",null,s(e(a).t("lost")),1),t("strong",null,s(h.value),1)]),t("div",j,[t("span",null,s(e(a).t("notTried")),1),t("strong",null,s(w.value),1)])])),p.value?(i(),r("div",A,[...l[1]||(l[1]=[t("div",{class:"skeleton",style:{height:"50px"}},null,-1)])])):o.value.length===0?(i(),r("div",E,[...l[2]||(l[2]=[t("strong",null,"No practice yet",-1),t("span",null,"Go to Training and try closing a sale with a persona.",-1)])])):_("",!0),B(t("div",G,[t("h3",I,s(e(a).t("mySessions")),1),(i(!0),r(D,null,S(u.value,d=>(i(),r("div",{key:d.id,style:{display:"flex","justify-content":"space-between",padding:"8px 0","border-bottom":"1px solid var(--border)"}},[t("span",null,s(d.persona_name),1),t("span",{class:T(["badge",d.outcome])},s(d.outcome==="won"?e(a).t("won"):e(a).t("lost")),3)]))),128))],512),[[N,u.value.length]])]))}},O=f(q,[["__scopeId","data-v-6767b6b8"]]);export{O as default};
|
||||
import{_ as f,p as x,y as v,a as r,b as t,l as b,u as e,m as k,t as s,i as a,g as _,w as B,A as N,F as D,x as S,r as m,B as c,o as i,C as T}from"./index-C--0e2U-.js";import{L as V}from"./layout-dashboard-qMD4csbw.js";const C={style:{margin:"0"}},L={key:0,class:"row stat-row"},M={class:"card stat"},z={class:"card stat won"},F={class:"card stat lost"},j={class:"card stat"},A={key:1,class:"card"},E={key:2,class:"card empty-state"},G={class:"card",style:{"margin-top":"20px"}},I={style:{"margin-top":"0"}},q={__name:"MyBoard",setup(H){const o=m([]),u=m([]),p=m(!0),y=c(()=>o.value.length),g=c(()=>o.value.filter(n=>n.my_outcome==="won").length),h=c(()=>o.value.filter(n=>n.my_outcome==="lost").length),w=c(()=>o.value.filter(n=>n.my_outcome==="not_tried").length);return x(async()=>{try{o.value=(await v.myBoard()).board||[],u.value=(await v.mySessions()).sessions||[]}finally{p.value=!1}}),(n,l)=>(i(),r("div",null,[t("div",null,[t("h2",C,[b(e(V),{size:22,"stroke-width":1.8,style:{"vertical-align":"-4px"}}),k(" "+s(e(a).t("myDashboard")),1)]),l[0]||(l[0]=t("div",{class:"muted",style:{"margin-top":"4px","margin-bottom":"16px"}},"สรุปผลการฝึกของตัวคุณเอง — ดูว่าปิดการขายได้กี่ครั้ง ยังฝึกกับใครบ้าง",-1))]),p.value?_("",!0):(i(),r("div",L,[t("div",M,[t("span",null,s(e(a).t("total")),1),t("strong",null,s(y.value),1)]),t("div",z,[t("span",null,s(e(a).t("won")),1),t("strong",null,s(g.value),1)]),t("div",F,[t("span",null,s(e(a).t("lost")),1),t("strong",null,s(h.value),1)]),t("div",j,[t("span",null,s(e(a).t("notTried")),1),t("strong",null,s(w.value),1)])])),p.value?(i(),r("div",A,[...l[1]||(l[1]=[t("div",{class:"skeleton",style:{height:"50px"}},null,-1)])])):o.value.length===0?(i(),r("div",E,[...l[2]||(l[2]=[t("strong",null,"No practice yet",-1),t("span",null,"Go to Training and try closing a sale with a persona.",-1)])])):_("",!0),B(t("div",G,[t("h3",I,s(e(a).t("mySessions")),1),(i(!0),r(D,null,S(u.value,d=>(i(),r("div",{key:d.id,style:{display:"flex","justify-content":"space-between",padding:"8px 0","border-bottom":"1px solid var(--border)"}},[t("span",null,s(d.persona_name),1),t("span",{class:T(["badge",d.outcome])},s(d.outcome==="won"?e(a).t("won"):e(a).t("lost")),3)]))),128))],512),[[N,u.value.length]])]))}},O=f(q,[["__scopeId","data-v-6767b6b8"]]);export{O as default};
|
||||
1
frontend/dist/assets/Personas-CTGLZyPW.js
vendored
Normal file
1
frontend/dist/assets/Personas-CTGLZyPW.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import{_ as T,p as L,a as r,l as c,q as y,b as t,t as s,u as a,i as n,h as p,m as d,g as w,F as _,x as h,k as N,y as x,z as S,r as A,o as l,S as F,C,f as B}from"./index-C--0e2U-.js";import{T as j}from"./target-dGrJgR_B.js";import{A as q}from"./arrow-left-C-Wn16Dy.js";const D={class:"row",style:{"align-items":"center"}},E={style:{margin:"0"}},I={class:"muted",style:{"margin-left":"auto"}},M={key:0,class:"card guide"},R={key:1,class:"row",style:{margin:"12px 0",gap:"10px"}},G={class:"primary"},H={class:"grid"},J={class:"row",style:{"justify-content":"space-between"}},K={class:"diff"},O={class:"muted"},Q={class:"muted"},U={class:"muted"},W={class:"muted",style:{"margin-top":"6px"}},X={class:"primary",style:{width:"100%"}},Y={class:"primary",style:{width:"100%"}},Z={class:"muted",style:{"margin-top":"auto","font-size":"12px"}},tt=["onClick","disabled"],st={__name:"Personas",setup(et){const u=N().params.gid,k=A([]),z=A(!0);async function b(){try{k.value=(await x.listPersonas(u)).personas}finally{z.value=!1}}function P(o){return k.value.filter(i=>i.tier===o)}function V(o){return n.t(o==="A"?"tierA":o==="B"?"tierB":"tierC")}function v(o){return o==="won"?n.t("won"):o==="lost"?n.t("lost"):o==="not_tried"?n.t("notTried"):o||"-"}async function $(o){o._busy=!0;try{await x.createPersonaVariant(u,o.id),await b()}catch(i){alert(i.message)}finally{o._busy=!1}}return L(b),(o,i)=>{const m=S("router-link");return l(),r("div",null,[c(m,{to:"/training",class:"btn-back"},{default:y(()=>[c(a(q),{size:16,"stroke-width":2}),d(" "+s(a(n).t("training")),1)]),_:1}),t("div",D,[t("h2",E,s(a(n).t("personas")),1),t("span",I,s(a(n).t("selectPersona")),1)]),a(p).isAdmin?w("",!0):(l(),r("div",M,[t("strong",null,[c(a(j),{size:17,"stroke-width":1.8,style:{"vertical-align":"-3px"}}),i[0]||(i[0]=d(" วิธีฝึก",-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,[d("กด "),t("strong",null,"แชท"),d(" → เลือกสถานการณ์ (โซเชียล / พบหน้า-โทร) → เริ่มคุยกับลูกค้า")]),t("li",null,"ลูกค้าจะตัดสินใจเองว่าซื้อหรือไม่ซื้อ (ฝึกได้คนละครั้งเท่านั้น)")],-1))])),a(p).isAdmin?(l(),r("div",R,[c(m,{to:`/admin/groups/${a(u)}/edit`},{default:y(()=>[t("button",G,[c(a(F),{size:18,"stroke-width":1.8}),d(" "+s(a(n).t("managePersonas")),1)])]),_:1},8,["to"]),i[2]||(i[2]=t("span",{class:"muted"},"Admin: จัดการรายละเอียดบุคคลต้นแบบได้ที่นี่",-1))])):w("",!0),(l(),r(_,null,h(["A","B","C"],f=>t("div",{key:f,style:{margin:"20px 0"}},[t("h4",null,s(V(f)),1),t("div",H,[(l(!0),r(_,null,h(P(f),e=>(l(),r("div",{key:e.id,class:"card pcard lift"},[t("div",J,[t("strong",null,s(e.name),1),t("span",{class:C(["badge",e.my_outcome])},s(v(e.my_outcome)),3)]),t("div",K,[(l(),r(_,null,h(5,g=>t("span",{key:g,class:C(["star",{on:g<=(e.difficulty||1)}])},"★",2)),64)),t("span",O,s(a(n).t("difficulty"))+" "+s(e.difficulty||1)+"/5",1)]),t("div",Q,[d(s(e.profession)+" · "+s(e.age_group)+" · "+s(e.location),1),i[3]||(i[3]=t("br",null,null,-1)),t("span",U,s(a(n).t("difficulty"))+" "+s(e.difficulty||1)+"/5",1)]),t("div",W,s(e.product_context),1),a(p).isAdmin?(l(),B(m,{key:0,to:`/groups/${a(u)}/chat/${e.id}`,style:{"margin-top":"auto"}},{default:y(()=>[t("button",X,s(a(n).t("chat")),1)]),_:1},8,["to"])):(l(),r(_,{key:1},[e.my_outcome==="not_tried"?(l(),B(m,{key:0,to:`/groups/${a(u)}/chat/${e.id}`,style:{"margin-top":"auto"}},{default:y(()=>[t("button",Y,s(a(n).t("chat")),1)]),_:1},8,["to"])):(l(),r(_,{key:1},[t("div",Z,"✓ "+s(a(n).t("trained"))+" ("+s(v(e.my_outcome))+")",1),t("button",{class:"soft",style:{width:"100%","margin-top":"8px"},onClick:g=>$(e),disabled:e._busy},s(e._busy?"กำลังสร้าง…":"สร้างบุคคลต้นแบบจากต้นแบบนี้"),9,tt)],64))],64))]))),128))])])),64))])}}},lt=T(st,[["__scopeId","data-v-20adbc50"]]);export{lt as default};
|
||||
1
frontend/dist/assets/Personas-CjtqghLF.js
vendored
1
frontend/dist/assets/Personas-CjtqghLF.js
vendored
@@ -1 +0,0 @@
|
||||
import{_ as T,p as $,a as l,l as u,q as _,b as t,t as s,u as e,i as n,h as g,m as d,g as x,F as y,x as p,k as L,y as N,z as V,r as w,o as a,S,C as b,f as A}from"./index-yErsUbD6.js";import{T as F}from"./target-DmESUw9c.js";import{A as j}from"./arrow-left-hWxF5h5m.js";const q={class:"row",style:{"align-items":"center"}},D={style:{margin:"0"}},E={class:"muted",style:{"margin-left":"auto"}},I={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,h=w([]),C=w(!0);async function B(){try{h.value=(await N.listPersonas(c)).personas}finally{C.value=!1}}function z(r){return h.value.filter(i=>i.tier===r)}function P(r){return n.t(r==="A"?"tierA":r==="B"?"tierB":"tierC")}function k(r){return r==="won"?n.t("won"):r==="lost"?n.t("lost"):n.t("notTried")}return $(B),(r,i)=>{const m=V("router-link");return a(),l("div",null,[u(m,{to:"/training",class:"btn-back"},{default:_(()=>[u(e(j),{size:16,"stroke-width":2}),d(" "+s(e(n).t("training")),1)]),_:1}),t("div",q,[t("h2",D,s(e(n).t("personas")),1),t("span",E,s(e(n).t("selectPersona")),1)]),e(g).isAdmin?x("",!0):(a(),l("div",I,[t("strong",null,[u(e(F),{size:17,"stroke-width":1.8,style:{"vertical-align":"-3px"}}),i[0]||(i[0]=d(" วิธีฝึก",-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,[d("กด "),t("strong",null,"แชท"),d(" → เลือกสถานการณ์ (โซเชียล / พบหน้า-โทร) → เริ่มคุยกับลูกค้า")]),t("li",null,"ลูกค้าจะตัดสินใจเองว่าซื้อหรือไม่ซื้อ (ฝึกได้คนละครั้งเท่านั้น)")],-1))])),e(g).isAdmin?(a(),l("div",M,[u(m,{to:`/admin/groups/${e(c)}/edit`},{default:_(()=>[t("button",R,[u(e(S),{size:18,"stroke-width":1.8}),d(" "+s(e(n).t("managePersonas")),1)])]),_:1},8,["to"]),i[2]||(i[2]=t("span",{class:"muted"},"Admin: จัดการรายละเอียดบุคคลต้นแบบได้ที่นี่",-1))])):x("",!0),(a(),l(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),l(y,null,p(z(f),o=>(a(),l("div",{key:o.id,class:"card pcard lift"},[t("div",H,[t("strong",null,s(o.name),1),t("span",{class:b(["badge",o.my_outcome])},s(k(o.my_outcome)),3)]),t("div",J,[(a(),l(y,null,p(5,v=>t("span",{key:v,class:b(["star",{on:v<=(o.difficulty||1)}])},"★",2)),64)),t("span",K,s(e(n).t("difficulty"))+" "+s(o.difficulty||1)+"/5",1)]),t("div",O,[d(s(o.profession)+" · "+s(o.age_group)+" · "+s(o.location),1),i[3]||(i[3]=t("br",null,null,-1)),t("span",Q,s(e(n).t("difficulty"))+" "+s(o.difficulty||1)+"/5",1)]),t("div",U,s(o.product_context),1),e(g).isAdmin?(a(),A(m,{key:0,to:`/groups/${e(c)}/chat/${o.id}`,style:{"margin-top":"auto"}},{default:_(()=>[t("button",W,s(e(n).t("chat")),1)]),_:1},8,["to"])):(a(),l(y,{key:1},[o.my_outcome==="not_tried"?(a(),A(m,{key:0,to:`/groups/${e(c)}/chat/${o.id}`,style:{"margin-top":"auto"}},{default:_(()=>[t("button",X,s(e(n).t("chat")),1)]),_:1},8,["to"])):(a(),l("div",Y,"✓ "+s(e(n).t("trained"))+" ("+s(k(o.my_outcome))+")",1))],64))]))),128))])])),64))])}}},at=T(Z,[["__scopeId","data-v-ec851b01"]]);export{at as default};
|
||||
1
frontend/dist/assets/Personas-CmLjB15L.css
vendored
Normal file
1
frontend/dist/assets/Personas-CmLjB15L.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.grid[data-v-20adbc50]{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:14px}.pcard[data-v-20adbc50]{display:flex;flex-direction:column;min-height:190px}.diff[data-v-20adbc50]{margin:8px 0}.star[data-v-20adbc50]{color:#d8dbe3}.star.on[data-v-20adbc50]{color:#f59e0b}.badge.won[data-v-20adbc50]{background:#dcfce7;color:#166534}.badge.lost[data-v-20adbc50]{background:#fee2e2;color:#991b1b}.badge.not_tried[data-v-20adbc50]{background:#eef2ff;color:#4338ca}.guide[data-v-20adbc50]{background:#eef2ff;border-color:#c7d2fe;margin:12px 0 16px}
|
||||
1
frontend/dist/assets/Personas-cgqlaBh7.css
vendored
1
frontend/dist/assets/Personas-cgqlaBh7.css
vendored
@@ -1 +0,0 @@
|
||||
.grid[data-v-ec851b01]{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:14px}.pcard[data-v-ec851b01]{display:flex;flex-direction:column;min-height:190px}.diff[data-v-ec851b01]{margin:8px 0}.star[data-v-ec851b01]{color:#d8dbe3}.star.on[data-v-ec851b01]{color:#f59e0b}.badge.won[data-v-ec851b01]{background:#dcfce7;color:#166534}.badge.lost[data-v-ec851b01]{background:#fee2e2;color:#991b1b}.badge.not_tried[data-v-ec851b01]{background:#eef2ff;color:#4338ca}.guide[data-v-ec851b01]{background:#eef2ff;border-color:#c7d2fe;margin:12px 0 16px}
|
||||
@@ -1,4 +1,4 @@
|
||||
import{c as U,_ as T,r as c,h as u,a as h,b as e,l as k,u as a,S as $,m as v,t as o,i as s,w as b,v as x,g as V,C as S,y as M,o as g}from"./index-yErsUbD6.js";import{L as D}from"./lock-CCBV2bwv.js";/**
|
||||
import{c as U,_ as T,r as c,h as u,a as h,b as e,l as k,u as a,S as $,m as v,t as o,i as s,w as b,v as x,g as V,C as S,y as M,o as g}from"./index-C--0e2U-.js";import{L as D}from"./lock-CdhbyMuc.js";/**
|
||||
* @license lucide-vue-next v1.0.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
@@ -1 +1 @@
|
||||
import{_ as U,a as v,b as e,l as K,u as s,m,t as a,i as l,h as _,w,v as b,d as h,n as N,g as T,r,j as B,o as f}from"./index-yErsUbD6.js";import{L as C}from"./lock-CCBV2bwv.js";const M={class:"setup-wrap"},D={class:"card setup-card"},L={class:"muted"},P={class:"terms"},j={key:0,class:"error",role:"alert"},z=["disabled"],E={key:0,class:"spinner"},I={key:1},V="/legal",R={__name:"Setup",setup(q){const S=B(),i=r(""),o=r(""),p=r(""),y=r(!1),u=r(""),d=r(!1);async function c(){if(u.value="",o.value.length<4){u.value=l.t("passwordTooShort");return}if(o.value!==p.value){u.value=l.t("passwordMismatch");return}d.value=!0;try{await _.finishSetup(i.value.trim(),o.value,!0),S.push("/")}catch(k){u.value=k.message}finally{d.value=!1}}return(k,t)=>{var g,x;return f(),v("div",M,[e("div",D,[e("h1",null,[K(s(C),{size:22,"stroke-width":1.8,style:{"vertical-align":"-4px"}}),m(" "+a(s(l).t("setupTitle")),1)]),e("p",L,[m(a(s(l).t("setupSubtitle"))+" ",1),e("strong",null,a(((g=s(_).user)==null?void 0:g.name)||((x=s(_).user)==null?void 0:x.username)),1)]),e("label",null,a(s(l).t("email")),1),w(e("input",{"onUpdate:modelValue":t[0]||(t[0]=n=>i.value=n),type:"email",autocomplete:"email",class:"wide",onKeyup:h(c,["enter"])},null,544),[[b,i.value]]),e("label",null,a(s(l).t("newPassword")),1),w(e("input",{"onUpdate:modelValue":t[1]||(t[1]=n=>o.value=n),type:"password",autocomplete:"new-password",class:"wide",onKeyup:h(c,["enter"])},null,544),[[b,o.value]]),e("label",null,a(s(l).t("confirmPassword")),1),w(e("input",{"onUpdate:modelValue":t[2]||(t[2]=n=>p.value=n),type:"password",autocomplete:"new-password",class:"wide",onKeyup:h(c,["enter"])},null,544),[[b,p.value]]),e("label",P,[w(e("input",{"onUpdate:modelValue":t[3]||(t[3]=n=>y.value=n),type:"checkbox"},null,512),[[N,y.value]]),e("span",null,[t[4]||(t[4]=m("ฉันยอมรับ ",-1)),e("a",{href:V,target:"_blank",rel:"noopener"},"ข้อกำหนดการใช้งาน"),t[5]||(t[5]=m(" และ ",-1)),e("a",{href:V,target:"_blank",rel:"noopener"},"นโยบายความเป็นส่วนตัว")])]),u.value?(f(),v("div",j,a(u.value),1)):T("",!0),e("button",{class:"primary",style:{width:"100%","margin-top":"16px"},disabled:d.value||!i.value||!o.value||o.value!==p.value||!y.value,onClick:c},[d.value?(f(),v("span",E)):(f(),v("span",I,a(s(l).t("save")),1))],8,z)])])}}},G=U(R,[["__scopeId","data-v-a3748c0a"]]);export{G as default};
|
||||
import{_ as U,a as v,b as e,l as K,u as s,m,t as a,i as l,h as _,w,v as b,d as h,n as N,g as T,r,j as B,o as f}from"./index-C--0e2U-.js";import{L as C}from"./lock-CdhbyMuc.js";const M={class:"setup-wrap"},D={class:"card setup-card"},L={class:"muted"},P={class:"terms"},j={key:0,class:"error",role:"alert"},z=["disabled"],E={key:0,class:"spinner"},I={key:1},V="/legal",R={__name:"Setup",setup(q){const S=B(),i=r(""),o=r(""),p=r(""),y=r(!1),u=r(""),d=r(!1);async function c(){if(u.value="",o.value.length<4){u.value=l.t("passwordTooShort");return}if(o.value!==p.value){u.value=l.t("passwordMismatch");return}d.value=!0;try{await _.finishSetup(i.value.trim(),o.value,!0),S.push("/")}catch(k){u.value=k.message}finally{d.value=!1}}return(k,t)=>{var g,x;return f(),v("div",M,[e("div",D,[e("h1",null,[K(s(C),{size:22,"stroke-width":1.8,style:{"vertical-align":"-4px"}}),m(" "+a(s(l).t("setupTitle")),1)]),e("p",L,[m(a(s(l).t("setupSubtitle"))+" ",1),e("strong",null,a(((g=s(_).user)==null?void 0:g.name)||((x=s(_).user)==null?void 0:x.username)),1)]),e("label",null,a(s(l).t("email")),1),w(e("input",{"onUpdate:modelValue":t[0]||(t[0]=n=>i.value=n),type:"email",autocomplete:"email",class:"wide",onKeyup:h(c,["enter"])},null,544),[[b,i.value]]),e("label",null,a(s(l).t("newPassword")),1),w(e("input",{"onUpdate:modelValue":t[1]||(t[1]=n=>o.value=n),type:"password",autocomplete:"new-password",class:"wide",onKeyup:h(c,["enter"])},null,544),[[b,o.value]]),e("label",null,a(s(l).t("confirmPassword")),1),w(e("input",{"onUpdate:modelValue":t[2]||(t[2]=n=>p.value=n),type:"password",autocomplete:"new-password",class:"wide",onKeyup:h(c,["enter"])},null,544),[[b,p.value]]),e("label",P,[w(e("input",{"onUpdate:modelValue":t[3]||(t[3]=n=>y.value=n),type:"checkbox"},null,512),[[N,y.value]]),e("span",null,[t[4]||(t[4]=m("ฉันยอมรับ ",-1)),e("a",{href:V,target:"_blank",rel:"noopener"},"ข้อกำหนดการใช้งาน"),t[5]||(t[5]=m(" และ ",-1)),e("a",{href:V,target:"_blank",rel:"noopener"},"นโยบายความเป็นส่วนตัว")])]),u.value?(f(),v("div",j,a(u.value),1)):T("",!0),e("button",{class:"primary",style:{width:"100%","margin-top":"16px"},disabled:d.value||!i.value||!o.value||o.value!==p.value||!y.value,onClick:c},[d.value?(f(),v("span",E)):(f(),v("span",I,a(s(l).t("save")),1))],8,z)])])}}},G=U(R,[["__scopeId","data-v-a3748c0a"]]);export{G as default};
|
||||
@@ -1,4 +1,4 @@
|
||||
import{c as x,_ as b,p as C,y as f,h as l,a as d,b as s,l as c,u as t,m,t as o,i as n,q as _,f as A,g as h,F as z,x as M,r as v,z as T,o as r,C as B,D as N}from"./index-yErsUbD6.js";import{T as P}from"./target-DmESUw9c.js";import{B as V}from"./book-open-Cmnj20ue.js";import{P as $}from"./plus-Y1qXSgej.js";/**
|
||||
import{c as x,_ as b,p as C,y as f,h as l,a as d,b as s,l as c,u as t,m,t as o,i as n,q as _,f as A,g as h,F as z,x as M,r as v,z as T,o as r,C as B,D as N}from"./index-C--0e2U-.js";import{T as P}from"./target-dGrJgR_B.js";import{B as V}from"./book-open-BDMN_yKI.js";import{P as $}from"./plus-DQOnWKR_.js";/**
|
||||
* @license lucide-vue-next v1.0.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
@@ -1,4 +1,4 @@
|
||||
import{c as e}from"./index-yErsUbD6.js";/**
|
||||
import{c as e}from"./index-C--0e2U-.js";/**
|
||||
* @license lucide-vue-next v1.0.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
@@ -1,4 +1,4 @@
|
||||
import{c as a}from"./index-yErsUbD6.js";/**
|
||||
import{c as a}from"./index-C--0e2U-.js";/**
|
||||
* @license lucide-vue-next v1.0.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
||||
import{c as t}from"./index-yErsUbD6.js";/**
|
||||
import{c as t}from"./index-C--0e2U-.js";/**
|
||||
* @license lucide-vue-next v1.0.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
@@ -1,4 +1,4 @@
|
||||
import{c as e}from"./index-yErsUbD6.js";/**
|
||||
import{c as e}from"./index-C--0e2U-.js";/**
|
||||
* @license lucide-vue-next v1.0.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
@@ -1,4 +1,4 @@
|
||||
import{c as e}from"./index-yErsUbD6.js";/**
|
||||
import{c as e}from"./index-C--0e2U-.js";/**
|
||||
* @license lucide-vue-next v1.0.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
@@ -1,4 +1,4 @@
|
||||
import{c}from"./index-yErsUbD6.js";/**
|
||||
import{c}from"./index-C--0e2U-.js";/**
|
||||
* @license lucide-vue-next v1.0.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
@@ -1,4 +1,4 @@
|
||||
import{c as e}from"./index-yErsUbD6.js";/**
|
||||
import{c as e}from"./index-C--0e2U-.js";/**
|
||||
* @license lucide-vue-next v1.0.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
2
frontend/dist/index.html
vendored
2
frontend/dist/index.html
vendored
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Sales Trainer</title>
|
||||
<script type="module" crossorigin src="/assets/index-yErsUbD6.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-C--0e2U-.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-B9WjPRES.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -47,6 +47,7 @@ export const api = {
|
||||
analyzeGroup: (id, opts = {}) => request('POST', `/api/groups/${id}/analyze${opts.append ? '?append=true' : ''}`),
|
||||
listPersonas: (gid) => request('GET', `/api/groups/${gid}/personas`),
|
||||
getPersona: (gid, pid) => request('GET', `/api/groups/${gid}/personas/${pid}`),
|
||||
createPersonaVariant: (gid, pid) => request('POST', `/api/groups/${gid}/personas/${pid}/variant`),
|
||||
updatePersona: (gid, pid, b) => request('PUT', `/api/groups/${gid}/personas/${pid}`, b),
|
||||
chatStart: (gid, pid, scenario, locale) => request('POST', `/api/chat/${gid}/personas/${pid}/chat/start`, { scenario, locale }),
|
||||
chatResume: (gid, pid) => request('GET', `/api/chat/${gid}/personas/${pid}/chat/resume`),
|
||||
|
||||
@@ -48,7 +48,12 @@
|
||||
<router-link v-if="p.my_outcome === 'not_tried'" :to="`/groups/${gid}/chat/${p.id}`" style="margin-top:auto">
|
||||
<button class="primary" style="width:100%">{{ i18n.t('chat') }}</button>
|
||||
</router-link>
|
||||
<div v-else class="muted" style="margin-top:auto;font-size:12px">✓ {{ i18n.t('trained') }} ({{ outcomeLabel(p.my_outcome) }})</div>
|
||||
<template v-else>
|
||||
<div class="muted" style="margin-top:auto;font-size:12px">✓ {{ i18n.t('trained') }} ({{ outcomeLabel(p.my_outcome) }})</div>
|
||||
<button class="soft" style="width:100%;margin-top:8px" @click="makeVariant(p)" :disabled="p._busy">
|
||||
{{ p._busy ? 'กำลังสร้าง…' : 'สร้างบุคคลต้นแบบจากต้นแบบนี้' }}
|
||||
</button>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -76,7 +81,19 @@ async function load() {
|
||||
function byTier(t) { return personas.value.filter((p) => p.tier === t) }
|
||||
function tierLabel(t) { return i18n.t(t === 'A' ? 'tierA' : t === 'B' ? 'tierB' : 'tierC') }
|
||||
function outcomeLabel(o) {
|
||||
return o === 'won' ? i18n.t('won') : o === 'lost' ? i18n.t('lost') : i18n.t('notTried')
|
||||
return o === 'won' ? i18n.t('won') : o === 'lost' ? i18n.t('lost') : o === 'not_tried' ? i18n.t('notTried') : o || '-'
|
||||
}
|
||||
|
||||
async function makeVariant(p) {
|
||||
p._busy = true
|
||||
try {
|
||||
await api.createPersonaVariant(gid, p.id)
|
||||
await load() // reload so the new variant shows up in the list
|
||||
} catch (e) {
|
||||
alert(e.message)
|
||||
} finally {
|
||||
p._busy = false
|
||||
}
|
||||
}
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user