fix(Docker): ship prebuilt frontend/dist, drop npm from image (removes 'vite not found')

The Vue SPA is now built locally and its frontend/dist/ committed (force-added).
Dockerfile is a single-stage runtime: copy prebuilt dist + python:3.11 + Flask.
No node/npm inside the image -> deterministic, no npm/esbuild postinstall flakiness.
To update UI: edit src, npm run build, commit frontend/dist/.
This commit is contained in:
Macky
2026-08-07 19:46:20 +07:00
parent 4827481da3
commit c19b23d44d
30 changed files with 80 additions and 13 deletions

View File

@@ -3,7 +3,6 @@ venv/
**/__pycache__/ **/__pycache__/
*.pyc *.pyc
**/node_modules/ **/node_modules/
frontend/dist/
backend/data/ backend/data/
data/ data/
.env .env

View File

@@ -1,27 +1,29 @@
# Sales Trainer — single-container build (Vue frontend built + served by Flask) # Sales Trainer — single-container runtime (Python 3.11 + Flask)
FROM python:3.11-slim AS backend #
# The Vue frontend is BUILT LOCALLY (`cd frontend && npm run build`) and the
# resulting `frontend/dist/` is committed to the repo. This image does NOT run
# npm/node at all — it just copies the prebuilt SPA and serves it via Flask.
# This eliminates `vite: not found` / npm postinstall flakiness from the build.
#
# To update the UI: edit frontend/src, run `npm run build` locally, commit dist/.
# Node 20 for building the Vue frontend FROM python:3.11-slim
# Runtime deps (none required beyond Python + pip packages; curl for healthcheck)
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y --no-install-recommends curl ca-certificates \ && apt-get install -y --no-install-recommends curl ca-certificates \
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
ENV NODE_ENV=production
WORKDIR /app WORKDIR /app
# 1) Build frontend # 1) Copy prebuilt frontend SPA (built locally, committed to repo)
COPY frontend/package.json frontend/package-lock.json* ./frontend/ COPY frontend/dist/ ./frontend/dist/
RUN cd frontend && npm install
COPY frontend/ ./frontend/
RUN cd frontend && npm run build
# 2) Install backend deps # 2) Install backend deps
COPY backend/requirements.txt ./backend/ COPY backend/requirements.txt ./backend/
RUN pip install --no-cache-dir -r backend/requirements.txt RUN pip install --no-cache-dir -r backend/requirements.txt
# 3) Copy backend source # 3) Copy backend source (and stub frontend dir so static serving finds dist)
COPY backend/ ./backend/ COPY backend/ ./backend/
# Run # Run

View File

@@ -0,0 +1 @@
import{k as x,c as r,a,t,u,i as d,w as n,v as m,C as g,e as _,F as U,n as k,p as f,r as p,o as i,x as V}from"./index-B1lw4PRH.js";const b={class:"card",style:{"margin-bottom":"16px"}},C={class:"row"},h={key:0,class:"error"},B={style:{flex:"1"}},M={class:"badge"},E={__name:"AdminUsers",setup(N){const v=p([]),o=p(""),s=p({name:"",email:"",password:"",role:"user"});async function c(){v.value=(await f.adminListUsers()).users}async function w(){o.value="";try{await f.adminCreateUser({...s.value}),s.value={name:"",email:"",password:"",role:"user"},await c()}catch(y){o.value=y.message}}return x(c),(y,l)=>(i(),r("div",null,[a("h2",null,t(u(d).t("users")),1),a("div",b,[a("h4",null,"+ "+t(u(d).t("create"))+" user",1),a("div",C,[n(a("input",{"onUpdate:modelValue":l[0]||(l[0]=e=>s.value.name=e),placeholder:"Name",style:{flex:"1"}},null,512),[[m,s.value.name]]),n(a("input",{"onUpdate:modelValue":l[1]||(l[1]=e=>s.value.email=e),placeholder:"Email",style:{flex:"1"}},null,512),[[m,s.value.email]]),n(a("input",{"onUpdate:modelValue":l[2]||(l[2]=e=>s.value.password=e),type:"password",placeholder:"Temp password",style:{flex:"1"}},null,512),[[m,s.value.password]]),n(a("select",{"onUpdate:modelValue":l[3]||(l[3]=e=>s.value.role=e),style:{flex:"1"}},[...l[4]||(l[4]=[a("option",{value:"user"},"user",-1),a("option",{value:"admin"},"admin",-1)])],512),[[g,s.value.role]]),a("button",{class:"primary",onClick:w},t(u(d).t("create")),1)]),o.value?(i(),r("div",h,t(o.value),1)):_("",!0)]),(i(!0),r(U,null,k(v.value,e=>(i(),r("div",{class:"card",key:e.id,style:{"margin-bottom":"8px",display:"flex","align-items":"center",gap:"12px"}},[a("strong",B,t(e.name)+" ("+t(e.email)+")",1),a("span",M,t(e.role),1),a("span",{class:V(["badge",e.active?"won":"lost"])},t(e.active?"active":"inactive"),3)]))),128))]))}};export{E as default};

View File

@@ -0,0 +1 @@
.stat[data-v-8828276e]{text-align:center;min-width:110px}.stat div[data-v-8828276e]{color:var(--muted);font-size:12px}.stat strong[data-v-8828276e]{font-size:20px}

View File

@@ -0,0 +1 @@
import{k as i,p as d,c as l,a as s,t as a,u as v,i as _,F as u,n as c,r as p,o as n}from"./index-B1lw4PRH.js";import{_ as g}from"./_plugin-vue_export-helper-DlAUqK2U.js";const m={class:"row",style:{gap:"16px",margin:"16px 0"}},y={class:"card stat"},f={class:"card stat"},w={style:{color:"var(--green)"}},x={class:"card stat"},h={style:{color:"var(--red)"}},k={class:"card stat"},b={class:"card stat"},A={class:"badge lost"},B={class:"badge won"},L={class:"muted"},F={__name:"Analytics",setup(S){const o=p({overall:{total_sessions:0,wins:0,losses:0,close_rate:0,avg_score:0},trainee_count:0,hardest_personas:[]});return i(async()=>{o.value=await d.analytics()}),(W,t)=>(n(),l("div",null,[s("h2",null,a(v(_).t("analytics")),1),s("div",m,[s("div",y,[t[0]||(t[0]=s("div",null,"Sessions",-1)),s("strong",null,a(o.value.overall.total_sessions),1)]),s("div",f,[t[1]||(t[1]=s("div",null,"Wins",-1)),s("strong",w,a(o.value.overall.wins),1)]),s("div",x,[t[2]||(t[2]=s("div",null,"Losses",-1)),s("strong",h,a(o.value.overall.losses),1)]),s("div",k,[t[3]||(t[3]=s("div",null,"Close rate",-1)),s("strong",null,a(o.value.overall.close_rate)+"%",1)]),s("div",b,[t[4]||(t[4]=s("div",null,"Avg score",-1)),s("strong",null,a(o.value.overall.avg_score),1)])]),s("h3",null,"Trainees: "+a(o.value.trainee_count),1),t[5]||(t[5]=s("h3",null,"Hardest personas",-1)),(n(!0),l(u,null,c(o.value.hardest_personas,(e,r)=>(n(),l("div",{class:"card",key:r,style:{"margin-bottom":"8px"}},[s("strong",null,a(e.persona_name),1),s("span",A,a(e.losses)+"L",1),s("span",B,a(e.wins)+"W",1),s("span",L,"· avg "+a(e.avg_score),1)]))),128))]))}},E=g(F,[["__scopeId","data-v-8828276e"]]);export{E as default};

1
frontend/dist/assets/Chat-D-rkKKd4.js vendored Normal file
View File

@@ -0,0 +1 @@
import{k as K,p as k,c as t,l as B,m as D,u as l,a as s,t as e,x as C,e as i,j as _,i as n,F,n as I,w as M,v as q,b as E,h as H,r as c,z as J,q as L,o}from"./index-B1lw4PRH.js";import{_ as O}from"./_plugin-vue_export-helper-DlAUqK2U.js";const P={class:"row",style:{"align-items":"center","margin-bottom":"12px"}},R={style:{margin:"0"}},U={key:0,class:"muted"},$=["disabled"],A={key:0,class:"spinner",style:{"margin-right":"4px"}},G={key:0,class:"card task"},Q={key:0},W={key:0,class:"bubble msg-customer muted"},X={key:2,class:"composer"},Y=["disabled","placeholder"],Z=["disabled"],ee={key:3,class:"card debrief"},se={key:0},ae={class:"json"},te={class:"primary",style:{"margin-top":"12px"}},le={__name:"Chat",setup(ne){const N=H(),m=N.params.gid,g=N.params.pid,r=c(null),f=c(!1),v=c([]),p=c(""),d=c(!1),u=c(null),w=c(""),j=c(null);function T(){J(()=>{y.value&&(y.value.scrollTop=y.value.scrollHeight)})}const y=c(null);K(async()=>{r.value=(await k.getPersona(m,g)).persona;const a=await k.chatStart(m,g);j.value=a.session.id,a.session.task&&(w.value=a.session.task),v.value=a.session.messages||[],f.value=!0,v.value.length&&T()});async function V(){if(p.value.trim()){d.value=!0;try{const a=await k.chatSend(m,g,p.value.trim());v.value=a.messages,p.value="",T()}catch(a){alert(a.message)}finally{d.value=!1}}}async function z(){if(confirm(n.t("finish")+"?")){d.value=!0;try{const a=await k.chatFinish(m,g);u.value=a.debrief,v.value=a.session.messages}catch(a){alert(a.message)}finally{d.value=!1}}}return(a,b)=>{const S=L("router-link");return o(),t("div",null,[B(S,{to:`/groups/${l(m)}/personas`,class:"btn-back"},{default:D(()=>[_("← "+e(l(n).t("personas")),1)]),_:1},8,["to"]),s("div",P,[s("h2",R,e(r.value?r.value.name:"..."),1),s("span",{class:C(["badge",r.value&&r.value.channel])},e(r.value?r.value.channel:""),3),r.value?(o(),t("span",U,e(r.value.profession)+" · "+e(r.value.age_group),1)):i("",!0),s("button",{class:"danger",style:{"margin-left":"auto"},onClick:z,disabled:v.value.length===0||!!u.value},[d.value?(o(),t("span",A)):i("",!0),_(e(l(n).t("finish")),1)],8,$)]),f.value?i("",!0):(o(),t("div",G,[s("strong",null,"📣 "+e(l(n).t("sellerInitiated")),1),w.value?(o(),t("div",Q,e(w.value),1)):i("",!0)])),f.value?(o(),t("div",{key:1,class:"thread",ref_key:"thread",ref:y},[(o(!0),t(F,null,I(v.value,(h,x)=>(o(),t("div",{key:x,class:C(["bubble",h.role==="seller"?"msg-seller":"msg-customer"])},e(h.text),3))),128)),d.value?(o(),t("div",W,"...")):i("",!0)],512)):i("",!0),f.value&&!u.value?(o(),t("div",X,[M(s("input",{"onUpdate:modelValue":b[0]||(b[0]=h=>p.value=h),onKeyup:E(V,["enter"]),disabled:d.value,placeholder:l(n).t("send")},null,40,Y),[[q,p.value]]),s("button",{class:"primary",onClick:V,disabled:d.value||!p.value.trim()},e(l(n).t("send")),9,Z)])):i("",!0),u.value?(o(),t("div",ee,[s("h3",null,e(l(n).t("debrief")),1),s("p",null,[s("span",{class:C(["badge",u.value.outcome])},e(u.value.outcome==="won"?l(n).t("won"):l(n).t("lost")),3),_(" — "+e(l(n).t("score"))+": ",1),s("strong",null,e(u.value.score),1)]),s("p",null,[s("strong",null,e(l(n).t("pain"))+":",1),_(" "+e(u.value.pain||"—"),1)]),s("p",null,[s("strong",null,e(l(n).t("why"))+":",1),_(" "+e(u.value.why),1)]),u.value.coaching&&u.value.coaching.length?(o(),t("div",se,[b[1]||(b[1]=s("strong",null,"Coaching:",-1)),s("ul",null,[(o(!0),t(F,null,I(u.value.coaching,(h,x)=>(o(),t("li",{key:x},e(h),1))),128))])])):i("",!0),s("details",null,[s("summary",null,e(l(n).t("reveal")),1),s("pre",ae,e(JSON.stringify(u.value.revealed_persona,null,2)),1)]),B(S,{to:"/"},{default:D(()=>[s("button",te,e(l(n).t("dashboard")),1)]),_:1})])):i("",!0)])}}},re=O(le,[["__scopeId","data-v-4f54526a"]]);export{re as default};

View File

@@ -0,0 +1 @@
.thread[data-v-4f54526a]{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-4f54526a]{max-width:72%;padding:10px 14px;white-space:pre-wrap;word-break:break-word}.composer[data-v-4f54526a]{display:flex;gap:8px;margin-top:12px}.task[data-v-4f54526a]{margin-bottom:12px;background:#fff7ed;border-color:#fed7aa}.debrief[data-v-4f54526a]{margin-top:16px}.json[data-v-4f54526a]{background:#0f172a;color:#9ca3af;padding:10px;border-radius:8px;font-size:11px;overflow:auto;max-height:260px}button.danger[data-v-4f54526a]{background:var(--red);color:#fff;border:none}

View File

@@ -0,0 +1 @@
import{k as T,c as o,a as s,t as e,u as i,i as r,f as V,l as A,m as B,e as p,w,v as k,F as y,n as C,p as F,q as S,r as c,o as n}from"./index-B1lw4PRH.js";const W={class:"row",style:{"margin-bottom":"16px","align-items":"center"}},q={style:{margin:"0"}},M={class:"muted",style:{margin:"4px 0 0","font-size":"13px"}},N={key:0,style:{"margin-left":"auto"}},P={class:"primary"},U={class:"filter-bar"},z={key:0,class:"card",style:{"min-height":"120px"}},j={class:"stat-grid",style:{"margin-bottom":"20px"}},E={class:"stat-card"},H={class:"stat-value"},Q={class:"stat-card"},R={class:"stat-value",style:{color:"var(--green)"}},$={class:"stat-card"},G={class:"stat-value",style:{color:"var(--red)"}},I={class:"stat-card"},J={class:"stat-value"},K={class:"stat-card"},O={class:"stat-value"},X={class:"stat-card"},Y={class:"stat-value",style:{color:"var(--accent)"}},Z={class:"card",style:{padding:"8px 20px"}},ss={style:{width:"100%","border-collapse":"collapse"}},ts={style:{padding:"8px 0","font-weight":"600"}},es={style:{color:"var(--green)"}},as={style:{color:"var(--red)"}},ls={key:0},os={colspan:"5",class:"muted",style:{padding:"12px 0"}},ns={class:"row",style:{"justify-content":"space-between","align-items":"center"}},is={class:"row",style:{gap:"6px"}},rs={class:"badge won"},ds={class:"badge lost"},us={class:"badge not_tried"},vs={key:0,class:"card empty-state"},ps={__name:"Dashboard",setup(cs){const l=c({overall:{total_sessions:0,wins:0,losses:0,close_rate:0,avg_score:0},trainee_count:0,hardest_personas:[],per_user:[]}),d=c(""),u=c(""),_=c(!0);async function m(){_.value=!0;try{const v=new URLSearchParams;d.value&&v.set("from",d.value),u.value&&v.set("to",u.value);const t=v.toString();l.value=await F.analyticsWithQuery(t?`?${t}`:"")}finally{_.value=!1}}return T(m),(v,t)=>{var g,h,x,b,f;const D=S("router-link");return n(),o("div",null,[s("div",W,[s("div",null,[s("h2",q,e(i(r).t("tabAdminDash")),1),s("p",M,e(i(r).t("analytics")),1)]),i(V).isAdmin?(n(),o("div",N,[A(D,{to:"/admin/new-group"},{default:B(()=>[s("button",P,"+ "+e(i(r).t("groupBuilder")),1)]),_:1})])):p("",!0)]),s("div",U,[t[2]||(t[2]=s("label",{style:{margin:"0"}},"From",-1)),w(s("input",{type:"date","onUpdate:modelValue":t[0]||(t[0]=a=>d.value=a)},null,512),[[k,d.value]]),t[3]||(t[3]=s("label",{style:{margin:"0"}},"To",-1)),w(s("input",{type:"date","onUpdate:modelValue":t[1]||(t[1]=a=>u.value=a)},null,512),[[k,u.value]]),s("button",{class:"primary",onClick:m,style:{"min-height":"44px"}},"Apply")]),_.value?(n(),o("div",z,[...t[4]||(t[4]=[s("div",{class:"skeleton",style:{height:"60px"}},null,-1),s("div",{class:"skeleton",style:{height:"60px","margin-top":"10px"}},null,-1)])])):(n(),o(y,{key:1},[s("div",j,[s("div",E,[t[5]||(t[5]=s("div",{class:"stat-label"},"Sessions",-1)),s("div",H,e(((g=l.value.overall)==null?void 0:g.total_sessions)||0),1)]),s("div",Q,[t[6]||(t[6]=s("div",{class:"stat-label"},"Wins",-1)),s("div",R,e(((h=l.value.overall)==null?void 0:h.wins)||0),1)]),s("div",$,[t[7]||(t[7]=s("div",{class:"stat-label"},"Losses",-1)),s("div",G,e(((x=l.value.overall)==null?void 0:x.losses)||0),1)]),s("div",I,[t[8]||(t[8]=s("div",{class:"stat-label"},"Close rate",-1)),s("div",J,e((b=l.value.overall)==null?void 0:b.close_rate)+"%",1)]),s("div",K,[t[9]||(t[9]=s("div",{class:"stat-label"},"Avg score",-1)),s("div",O,e((f=l.value.overall)==null?void 0:f.avg_score),1)]),s("div",X,[t[10]||(t[10]=s("div",{class:"stat-label"},"Trainees",-1)),s("div",Y,e(l.value.trainee_count||0),1)])]),t[12]||(t[12]=s("h3",{style:{margin:"20px 0 12px"}},"Per trainee",-1)),s("div",Z,[s("table",ss,[t[11]||(t[11]=s("thead",null,[s("tr",{class:"muted",style:{"font-size":"12px","text-align":"left"}},[s("th",{style:{padding:"8px 0"}},"Trainee"),s("th",null,"Played"),s("th",null,"Won"),s("th",null,"Lost"),s("th",null,"Close %")])],-1)),s("tbody",null,[(n(!0),o(y,null,C(l.value.per_user||[],a=>(n(),o("tr",{key:a.user_id,style:{"border-top":"1px solid var(--border)"}},[s("td",ts,e(a.name),1),s("td",null,e(a.sessions),1),s("td",es,e(a.wins),1),s("td",as,e(a.losses),1),s("td",null,e(a.sessions?Math.round(a.wins/a.sessions*100):0)+"%",1)]))),128)),(l.value.per_user||[]).length?p("",!0):(n(),o("tr",ls,[s("td",os,e(i(r).t("noData")),1)]))])])]),t[13]||(t[13]=s("h3",{style:{margin:"20px 0 12px"}},"Hardest personas",-1)),(n(!0),o(y,null,C(l.value.hardest_personas||[],(a,L)=>(n(),o("div",{class:"card",key:L,style:{"margin-bottom":"8px"}},[s("div",ns,[s("strong",null,e(a.persona_name),1),s("div",is,[s("span",rs,e(a.wins)+"W",1),s("span",ds,e(a.losses)+"L",1),s("span",us,"avg "+e(a.avg_score),1)])])]))),128)),(l.value.hardest_personas||[]).length?p("",!0):(n(),o("div",vs,[s("strong",null,e(i(r).t("noData")),1)]))],64))])}}};export{ps as default};

View File

@@ -0,0 +1 @@
import{r as l,k as x,c as r,a as e,t,u as m,i as v,x as p,F as k,w as C,v as P,e as M,n as N,h as V,p as w,q as $,o as u,l as B,m as D}from"./index-B1lw4PRH.js";import{_ as G}from"./_plugin-vue_export-helper-DlAUqK2U.js";const I={class:"card",style:{"margin-bottom":"16px"}},T={class:"row"},q={key:1,class:"muted"},z=["disabled"],E={key:2,class:"error"},F={class:"grid"},L={class:"muted"},R={class:"primary",style:{width:"100%"}},S={__name:"GenPersona",setup(U){const b=V(),o=l(b.query.mode==="weak"?"weak-area":"manual"),i=l(""),d=l(!1),c=l(""),y=l([]),g=l(null);async function _(){const n=await w.myPersonas();g.value=n.group.id,y.value=n.personas}x(_);async function f(){d.value=!0,c.value="";try{const n=o.value==="weak-area"?{mode:"weak-area",spec:{}}:{mode:"manual",spec:{description:i.value}};await w.generatePersona(n),await _()}catch(n){c.value=n.message}finally{d.value=!1}}return(n,a)=>{const h=$("router-link");return u(),r("div",null,[e("h2",null,t(m(v).t("generatePersona")),1),e("div",I,[a[4]||(a[4]=e("label",null,"Mode",-1)),e("div",T,[e("button",{class:p({active:o.value==="manual"}),onClick:a[0]||(a[0]=s=>o.value="manual")},"✏️ "+t(m(v).t("manual")),3),e("button",{class:p({active:o.value==="weak-area"}),onClick:a[1]||(a[1]=s=>o.value="weak-area")},"🔒 Weak-area lock",2)]),o.value==="manual"?(u(),r(k,{key:0},[a[3]||(a[3]=e("label",null,"Describe the persona you want to practice against",-1)),C(e("textarea",{"onUpdate:modelValue":a[2]||(a[2]=s=>i.value=s),rows:"4",placeholder:"e.g. a price-hardball restaurant owner on LINE who stalls when I bring up costs"},null,512),[[P,i.value]])],64)):(u(),r("p",q,"The system will analyze your losses and auto-generate a harder persona targeting your weak points.")),e("button",{class:"primary",style:{"margin-top":"16px"},disabled:d.value||o.value==="manual"&&!i.value.trim(),onClick:f},t(d.value?"...":m(v).t("generatePersona")),9,z),c.value?(u(),r("div",E,t(c.value),1)):M("",!0)]),a[5]||(a[5]=e("h3",null,"My personas",-1)),e("div",F,[(u(!0),r(k,null,N(y.value,s=>(u(),r("div",{key:s.id,class:"card pcard"},[e("strong",null,t(s.name),1),e("span",{class:p(["badge",s.tier])},"Tier "+t(s.tier),3),e("div",L,t(s.profession)+" · "+t(s.age_group),1),B(h,{to:`/groups/${g.value}/chat/${s.id}`,style:{"margin-top":"auto"}},{default:D(()=>[e("button",R,t(m(v).t("chat")),1)]),_:1},8,["to"])]))),128))])])}}},A=G(S,[["__scopeId","data-v-d299d1b3"]]);export{A as default};

View File

@@ -0,0 +1 @@
button.active[data-v-d299d1b3]{background:var(--accent);color:#fff;border-color:var(--accent)}.grid[data-v-d299d1b3]{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:14px}.pcard[data-v-d299d1b3]{display:flex;flex-direction:column;min-height:140px}

View File

@@ -0,0 +1 @@
import{c as g,a as e,t as l,u as o,i as n,w as r,v,C as m,e as x,r as p,g as k,o as f,p as w}from"./index-B1lw4PRH.js";const V={class:"card"},B={class:"row"},C={style:{flex:"1"}},U={value:"facebook"},D={value:"line"},F={style:{flex:"1"}},S={value:"th"},E={value:"en"},G={key:0,class:"error"},M=["disabled"],O={__name:"GroupBuilder",setup(N){const h=k(),t=p({product:"",segment:"",description:"",channel:"facebook",language:"th"}),c=p([]),d=p(""),i=p(!1);function _(a){c.value=Array.from(a.target.files||[])}async function b(){i.value=!0,d.value="";try{const a=new FormData;a.append("product",t.value.product),a.append("segment",t.value.segment),a.append("description",t.value.description),a.append("channel",t.value.channel),a.append("language",t.value.language),c.value.forEach(y=>a.append("files",y));const u=(await w.createGroup(a)).group.id;h.push(`/admin/groups/${u}/edit`)}catch(a){d.value=a.message}finally{i.value=!1}}return(a,s)=>(f(),g("div",V,[e("h2",null,l(o(n).t("groupBuilder")),1),e("label",null,l(o(n).t("product")),1),r(e("textarea",{"onUpdate:modelValue":s[0]||(s[0]=u=>t.value.product=u),rows:"3",placeholder:"e.g. Cloud POS for small restaurants"},null,512),[[v,t.value.product]]),e("label",null,l(o(n).t("segment")),1),r(e("input",{"onUpdate:modelValue":s[1]||(s[1]=u=>t.value.segment=u)},null,512),[[v,t.value.segment]]),e("label",null,l(o(n).t("description")),1),r(e("textarea",{"onUpdate:modelValue":s[2]||(s[2]=u=>t.value.description=u),rows:"3"},null,512),[[v,t.value.description]]),e("div",B,[e("div",C,[e("label",null,l(o(n).t("channel")),1),r(e("select",{"onUpdate:modelValue":s[3]||(s[3]=u=>t.value.channel=u)},[e("option",U,l(o(n).t("facebook")),1),e("option",D,l(o(n).t("line")),1)],512),[[m,t.value.channel]])]),e("div",F,[e("label",null,l(o(n).t("language")),1),r(e("select",{"onUpdate:modelValue":s[4]||(s[4]=u=>t.value.language=u)},[e("option",S,l(o(n).t("thai")),1),e("option",E,l(o(n).t("english")),1)],512),[[m,t.value.language]])])]),e("label",null,"📎 Files (.pdf/.md/.txt) — "+l(o(n).t("product"))+" can come from here",1),e("input",{type:"file",multiple:"",accept:".pdf,.md,.txt",onChange:_},null,32),d.value?(f(),g("div",G,l(d.value),1)):x("",!0),e("button",{class:"primary",style:{"margin-top":"16px"},disabled:i.value||!t.value.product&&!c.value.length,onClick:b},l(i.value?"...":o(n).t("create")),9,M)]))}};export{O as default};

View File

@@ -0,0 +1 @@
.grid[data-v-ee1c1944]{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:14px}.pcard[data-v-ee1c1944]{display:flex;flex-direction:column}.pcard button[data-v-ee1c1944]{margin-top:auto}.json[data-v-ee1c1944]{background:#0f172a;color:#9ca3af;padding:10px;border-radius:8px;font-size:11px;overflow:auto;max-height:220px}

View File

@@ -0,0 +1 @@
import{k as S,c as n,l as E,m as J,a as t,t as e,u as d,i,e as m,j as k,F as b,n as x,h as O,p as y,q as z,r as v,o}from"./index-B1lw4PRH.js";import{_ as G}from"./_plugin-vue_export-helper-DlAUqK2U.js";const V={class:"row",style:{"align-items":"center","margin-bottom":"16px"}},j={style:{margin:"0"}},A=["disabled"],P={key:0,class:"spinner",style:{"margin-right":"4px"}},F={key:0,class:"error"},I={key:1,class:"card empty-state"},L={class:"grid"},T={class:"row"},q={key:0,class:"badge"},D={class:"muted"},M={class:"muted",style:{"margin-top":"4px"}},R={style:{"margin-top":"8px"},open:""},$={class:"json"},H=["onClick"],K={__name:"GroupEdit",setup(Q){const _=O().params.gid,g=v(null),p=v([]),l=v(!1),u=v("");async function f(){const s=await y.getGroup(_);g.value=s.group,p.value=(await y.listPersonas(_)).personas}function N(s){return p.value.filter(r=>r.tier===s)}function w(s){return i.t(s==="A"?"tierA":s==="B"?"tierB":"tierC")}async function C(){l.value=!0,u.value="";try{const s=await y.analyzeGroup(_);p.value=s.personas,await f()}catch(s){u.value=s.message}finally{l.value=!1}}function B(s){const r=prompt("Edit persona JSON (full fields):",JSON.stringify(s,null,2));if(r)try{const c=JSON.parse(r);y.updatePersona(_,s.id,c).then(f)}catch(c){u.value="Invalid JSON: "+c.message}}return S(f),(s,r)=>{const c=z("router-link");return o(),n("div",null,[E(c,{to:"/",class:"btn-back"},{default:J(()=>[k("← "+e(d(i).t("dashboard")),1)]),_:1}),t("div",V,[t("h2",j,e(d(i).t("groupBuilder"))+" — "+e(g.value&&g.value.title),1),t("button",{class:"primary",style:{"margin-left":"auto"},onClick:C,disabled:l.value},[l.value?(o(),n("span",P)):m("",!0),k(e(d(i).t("analyze")),1)],8,A)]),u.value?(o(),n("div",F,e(u.value),1)):m("",!0),p.value.length===0&&!l.value?(o(),n("div",I,[r[0]||(r[0]=t("strong",null,"No personas yet",-1)),t("span",null,"Click "+e(d(i).t("analyze"))+" to generate the 15 personas (5 per tier).",1)])):m("",!0),(o(),n(b,null,x(["A","B","C"],h=>t("div",{key:h,style:{"margin-bottom":"20px"}},[t("h4",null,e(w(h)),1),t("div",L,[(o(!0),n(b,null,x(N(h),a=>(o(),n("div",{key:a.id,class:"card pcard"},[t("div",T,[t("strong",null,e(a.name),1),a.special==="wrong_text"?(o(),n("span",q,"⚠️ wrong_text")):m("",!0)]),t("div",D,e(a.profession)+" · "+e(a.age_group)+" · "+e(a.channel)+" · "+e(a.initiation_mode),1),t("div",M,"diff "+e(a.difficulty)+" · "+e(a.income)+" · "+e(a.personality),1),t("details",R,[t("summary",null,e(d(i).t("reveal")),1),t("pre",$,e(JSON.stringify(a,null,2)),1)]),t("button",{onClick:W=>B(a)},"✏️ Edit",8,H)]))),128))])])),64))])}}},Z=G(K,[["__scopeId","data-v-ee1c1944"]]);export{Z as default};

View File

@@ -0,0 +1 @@
import{c as d,a as e,t as s,u as c,i as o,w as y,v as b,b as w,d as x,e as k,r as l,f,g as S,h as V,o as v}from"./index-B1lw4PRH.js";import{_ as B}from"./_plugin-vue_export-helper-DlAUqK2U.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=V(),_=S(),n=l(""),r=l(""),a=l(!1),u=l(""),i=l(!1);async function m(){u.value="",i.value=!0;try{await f.login(n.value.trim(),r.value),f.mustSetup?_.push({path:"/setup"}):_.push(g.query.redirect||"/")}catch{u.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),[[b,n.value]]),e("label",null,s(c(o).t("password")),1),e("div",K,[y(e("input",{"onUpdate:modelValue":t[1]||(t[1]=p=>r.value=p),type:a.value?"text":"password",autocomplete:"current-password",onKeyup:w(m,["enter"])},null,40,E),[[x,r.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)]),u.value?(v(),d("div",M,s(u.value),1)):k("",!0),e("button",{class:"primary",style:{width:"100%","margin-top":"16px"},disabled:i.value||!n.value||!r.value,onClick:m},[i.value?(v(),d("span",R)):(v(),d("span",U,s(c(o).t("login")),1))],8,N)])]))}},T=B(q,[["__scopeId","data-v-f7904d9f"]]);export{T as default};

View File

@@ -0,0 +1 @@
.login-wrap[data-v-f7904d9f]{display:flex;justify-content:center;padding-top:10vh}.login-card[data-v-f7904d9f]{width:360px}h1[data-v-f7904d9f]{margin-top:0}.pw-wrap[data-v-f7904d9f]{position:relative}.pw-toggle[data-v-f7904d9f]{position:absolute;right:4px;top:50%;transform:translateY(-50%);background:transparent;border:none;padding:6px;min-height:36px;cursor:pointer}

View File

@@ -0,0 +1 @@
import{k as B,p as x,c as n,a as t,t as s,u as l,i as e,F as y,n as v,e as b,r as u,s as f,o as i,x as z}from"./index-B1lw4PRH.js";const D={style:{"margin-bottom":"4px"}},L={class:"muted",style:{margin:"0 0 20px","font-size":"13px"}},A={class:"stat-grid",style:{"margin-bottom":"20px"}},S={class:"stat-card"},C={class:"stat-label"},M={class:"stat-value",style:{color:"var(--ink)"}},j={class:"stat-card"},F={class:"stat-label"},N={class:"stat-value",style:{color:"var(--green)"}},T={class:"stat-card"},V={class:"stat-label"},E={class:"stat-value",style:{color:"var(--red)"}},G={class:"stat-card"},q={class:"stat-label"},H={class:"stat-value",style:{color:"var(--accent)"}},I={style:{margin:"20px 0 12px"}},J={key:0,class:"card",style:{"min-height":"60px"}},K={key:1,class:"card",style:{"margin-bottom":"12px"}},O={style:{"font-weight":"700","font-size":"14px","margin-bottom":"6px"}},P={class:"muted",style:{"font-size":"13px"}},Q={class:"badge lost"},R={key:0,class:"muted",style:{"font-size":"12px"}},U={key:2,class:"card empty-state",style:{"margin-bottom":"8px"}},W={style:{margin:"20px 0 12px"}},X={key:3,class:"card",style:{"min-height":"80px"}},Y={key:4,class:"card",style:{padding:"4px 20px"}},Z={class:"muted",style:{"font-size":"12px"}},$={key:0,class:"muted",style:{padding:"16px 0","text-align":"center"}},et={__name:"MyBoard",setup(tt){const p=u([]),d=u({}),h=u(!0),m=u(!0),k=f(()=>{var a;return(a=d.value.top_loss_personas)!=null&&a.length,""}),g=f(()=>{var o,c,_;const a=d.value,r=[];return(o=a.by_tier)!=null&&o.length&&r.push({title:"By tier · losses",items:a.by_tier}),(c=a.by_initiation)!=null&&c.length&&r.push({title:"By initiation · losses",items:a.by_initiation}),(_=a.by_channel)!=null&&_.length&&r.push({title:"By channel · losses",items:a.by_channel}),r});function w(a){return a==="won"?e.t("won"):a==="lost"?e.t("lost"):e.t("notTried")}return B(async()=>{try{p.value=(await x.myBoard()).board}catch{}finally{h.value=!1}try{d.value=(await x.weakAreas()).insight||{}}catch{}finally{m.value=!1}}),(a,r)=>(i(),n("div",null,[t("h2",D,s(l(e).t("tabMyDash")),1),t("p",L,s(l(e).t("weakAreas"))+" · "+s(l(e).t("mySessions")),1),t("div",A,[t("div",S,[t("div",C,s(l(e).t("mySessions")),1),t("div",M,s(d.value.total_sessions||0),1)]),t("div",j,[t("div",F,s(l(e).t("won")),1),t("div",N,s(d.value.wins||0),1)]),t("div",T,[t("div",V,s(l(e).t("lost")),1),t("div",E,s(d.value.losses||0),1)]),t("div",G,[t("div",q,s(l(e).t("score")),1),t("div",H,s(k.value)+s(d.value.total_sessions?"%":""),1)])]),t("h3",I,s(l(e).t("weakAreas")),1),m.value?(i(),n("div",J,[...r[0]||(r[0]=[t("div",{class:"skeleton",style:{height:"40px"}},null,-1)])])):g.value.length?(i(),n("div",K,[(i(!0),n(y,null,v(g.value,o=>(i(),n("div",{key:o.title,style:{padding:"10px 0","border-bottom":"1px solid var(--border)"}},[t("div",O,s(o.title),1),(i(!0),n(y,null,v(o.items,(c,_)=>(i(),n("div",{key:_,class:"row",style:{"justify-content":"space-between",padding:"2px 0","align-items":"center"}},[t("span",P,s(c.value),1),t("span",Q,s(c.losses)+" "+s(l(e).t("lost").toLowerCase()),1)]))),128)),o.items.length?b("",!0):(i(),n("div",R,s(l(e).t("noData")),1))]))),128))])):(i(),n("div",U,[t("strong",null,s(l(e).t("noData")),1),t("span",null,s(l(e).t("weakAreas")),1)])),t("h3",W,s(l(e).t("myTraining")),1),h.value?(i(),n("div",X,[...r[1]||(r[1]=[t("div",{class:"skeleton",style:{height:"60px"}},null,-1)])])):(i(),n("div",Y,[(i(!0),n(y,null,v(p.value,o=>(i(),n("div",{key:o.group_id+o.persona_id,class:"row",style:{"justify-content":"space-between",padding:"9px 0","border-bottom":"1px solid var(--border)"}},[t("div",null,[t("strong",null,s(o.persona_name),1),t("span",Z," — "+s(o.group_title)+" · "+s(o.tier),1)]),t("span",{class:z(["badge",o.my_outcome])},s(w(o.my_outcome)),3)]))),128)),p.value.length===0?(i(),n("div",$,s(l(e).t("noData")),1)):b("",!0)]))]))}};export{et as default};

View File

@@ -0,0 +1 @@
import{k as l,p as c,c as a,a as s,t,u as d,i as u,F as m,n as p,e as i,r as _,o as n,x as y}from"./index-B1lw4PRH.js";const g={class:"row",style:{"justify-content":"space-between"}},f={class:"muted"},v={key:0,class:"muted",style:{"margin-top":"4px"}},k={key:0,class:"card empty-state"},x={__name:"MySessions",setup(b){const o=_([]);return l(async()=>{o.value=(await c.mySessions()).sessions}),(h,r)=>(n(),a("div",null,[s("h2",null,t(d(u).t("myTraining")),1),(n(!0),a(m,null,p(o.value,e=>(n(),a("div",{class:"card",key:e.id,style:{"margin-bottom":"10px"}},[s("div",g,[s("strong",null,t(e.persona_name),1),s("span",{class:y(["badge",e.outcome||"not_tried"])},t(e.outcome||"—"),3)]),s("div",f,t(e.persona_id)+" · "+t(new Date(e.created_at).toLocaleString()),1),e.debrief?(n(),a("div",v," Score "+t(e.debrief.score)+" — "+t(e.debrief.why),1)):i("",!0)]))),128)),o.value.length===0?(n(),a("div",k,[...r[0]||(r[0]=[s("strong",null,"No training sessions yet",-1),s("span",null,"Pick a persona from a group and practice closing a sale.",-1)])])):i("",!0)]))}};export{x as default};

View File

@@ -0,0 +1 @@
.grid[data-v-88c74e17]{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:14px}.pcard[data-v-88c74e17]{display:flex;flex-direction:column;min-height:170px}

View File

@@ -0,0 +1 @@
import{k as B,c as l,l as C,m as y,a as t,t as s,u as r,i as o,F as p,n as f,h as L,p as T,q as A,r as g,o as i,j as h,x as v,y as I}from"./index-B1lw4PRH.js";import{_ as N}from"./_plugin-vue_export-helper-DlAUqK2U.js";const P={class:"row",style:{"align-items":"center"}},V={style:{margin:"0"}},z={class:"grid"},F={class:"row"},$={class:"muted"},j={class:"muted"},q={class:"muted",style:{"margin-top":"6px"}},D={class:"primary",style:{width:"100%"}},E={key:1,class:"muted",style:{"margin-top":"auto","font-size":"12px"}},M={__name:"Personas",setup(R){const d=L().params.gid,u=g([]),k=g(!0);async function b(){try{u.value=(await T.listPersonas(d)).personas}finally{k.value=!1}}function x(a){return u.value.filter(n=>n.tier===a)}function w(a){return o.t(a==="A"?"tierA":a==="B"?"tierB":"tierC")}function _(a){return a==="won"?o.t("won"):a==="lost"?o.t("lost"):o.t("notTried")}return B(b),(a,n)=>{const m=A("router-link");return i(),l("div",null,[C(m,{to:"/",class:"btn-back"},{default:y(()=>[h("← "+s(r(o).t("dashboard")),1)]),_:1}),t("div",P,[t("h2",V,s(r(o).t("personas")),1),n[0]||(n[0]=t("span",{class:"muted",style:{"margin-left":"auto"}},"Levels: choose one to practice (one-shot)",-1))]),(i(),l(p,null,f(["A","B","C"],c=>t("div",{key:c,style:{margin:"20px 0"}},[t("h4",null,s(w(c)),1),t("div",z,[(i(!0),l(p,null,f(x(c),e=>(i(),l("div",{key:e.id,class:"card pcard lift"},[t("div",F,[t("strong",null,s(e.name),1),t("span",{class:v(["badge",e.my_outcome])},s(_(e.my_outcome)),3)]),t("div",$,[h(s(e.profession)+" · "+s(e.age_group)+" · "+s(e.location),1),n[1]||(n[1]=t("br",null,null,-1)),t("span",{class:v(["badge",e.channel])},s(e.channel),3),t("span",j," · "+s(e.initiation_mode==="seller"?r(o).t("sellerInitiated"):r(o).t("customerInitiated")),1)]),t("div",q,s(e.product_context),1),e.my_outcome==="not_tried"?(i(),I(m,{key:0,to:`/groups/${r(d)}/chat/${e.id}`,style:{"margin-top":"auto"}},{default:y(()=>[t("button",D,s(r(o).t("chat")),1)]),_:1},8,["to"])):(i(),l("div",E,"✓ Trained ("+s(_(e.my_outcome))+")",1))]))),128))])])),64))])}}},J=N(M,[["__scopeId","data-v-88c74e17"]]);export{J as default};

View File

@@ -0,0 +1 @@
import{k as E,f as i,c as g,a as e,t,u as a,i as o,A as V,w as v,v as _,x as y,e as M,B as U,r as p,s as k,o as b,p as B}from"./index-B1lw4PRH.js";const T={class:"row",style:{margin:"0 0 20px"}},A={style:{margin:"0"}},j={class:"setting-grid"},q={class:"card settings-section"},F={class:"s-title"},G={class:"s-desc"},H={class:"form-grid"},I={for:"p-name"},J=["placeholder"],K={for:"p-email"},L=["placeholder"],O=["disabled"],Q={class:"card settings-section"},R={class:"s-title"},W={class:"s-desc"},X={class:"form-grid"},Y={for:"pw-current"},Z={for:"pw-new"},$={for:"pw-confirm"},ee=["disabled"],se={class:"card settings-section"},te={class:"s-title"},ae={class:"s-desc"},oe={class:"row",style:{gap:"10px","align-items":"center"}},ne={class:"avatar",style:{width:"44px",height:"44px","font-size":"18px"}},le={style:{"font-weight":"700"}},ie={class:"muted",style:{"font-size":"13px"}},ue={__name:"Settings",setup(re){const r=U({name:"",email:""}),l=U({current:"",newp:"",confirm:""}),w=p(!1),d=p(!1),m=p(""),h=p(!1),u=p(""),f=p(!1),C=k(()=>{var n,s;return(((n=i.user)==null?void 0:n.name)||((s=i.user)==null?void 0:s.username)||"?").slice(0,1).toUpperCase()}),D=k(()=>{var n,s;return((n=i.user)==null?void 0:n.name)||((s=i.user)==null?void 0:s.username)||""});E(()=>{var n,s;r.name=((n=i.user)==null?void 0:n.name)||"",r.email=((s=i.user)==null?void 0:s.email)||""});async function N(){w.value=!0,h.value=!1,m.value="";try{const n={};r.name.trim()&&(n.name=r.name.trim()),r.email.trim()&&(n.email=r.email.trim());const s=await B.updateProfile(n);i.user=s.user,m.value=o.t("profileSaved")}catch(n){h.value=!0,m.value=n.message}finally{w.value=!1}}async function z(){if(d.value=!0,f.value=!1,u.value="",l.newp.length<4){f.value=!0,u.value=o.t("passwordTooShort"),d.value=!1;return}if(l.newp!==l.confirm){f.value=!0,u.value=o.t("passwordMismatch"),d.value=!1;return}try{await B.changePassword({current_password:l.current,new_password:l.newp}),l.current="",l.newp="",l.confirm="",u.value=o.t("passwordChanged")}catch(n){f.value=!0,u.value=n.message}finally{d.value=!1}}return(n,s)=>{var x,P,S;return b(),g("div",null,[e("div",T,[e("h2",A,t(a(o).t("settingsTitle")),1)]),e("div",j,[e("section",q,[e("h3",F,t(a(o).t("settingsProfile")),1),e("p",G,t(a(o).t("settingsProfileDesc")),1),e("form",{onSubmit:V(N,["prevent"])},[e("div",H,[e("label",I,t(a(o).t("displayName")),1),v(e("input",{id:"p-name","onUpdate:modelValue":s[0]||(s[0]=c=>r.name=c),placeholder:((x=a(i).user)==null?void 0:x.name)||""},null,8,J),[[_,r.name]]),e("label",K,t(a(o).t("email")),1),v(e("input",{id:"p-email","onUpdate:modelValue":s[1]||(s[1]=c=>r.email=c),type:"email",placeholder:((P=a(i).user)==null?void 0:P.email)||""},null,8,L),[[_,r.email]]),e("button",{class:"primary",type:"submit",disabled:w.value,style:{"margin-top":"16px"}},t(w.value?"…":a(o).t("save")),9,O)])],32),m.value?(b(),g("p",{key:0,class:y(h.value?"error":""),style:{"margin-top":"10px"}},t(m.value),3)):M("",!0)]),e("section",Q,[e("h3",R,t(a(o).t("settingsSecurity")),1),e("p",W,t(a(o).t("settingsSecurityDesc")),1),e("form",{onSubmit:V(z,["prevent"])},[e("div",X,[e("label",Y,t(a(o).t("currentPassword")),1),v(e("input",{id:"pw-current","onUpdate:modelValue":s[2]||(s[2]=c=>l.current=c),type:"password",autocomplete:"current-password"},null,512),[[_,l.current]]),e("label",Z,t(a(o).t("newPassword")),1),v(e("input",{id:"pw-new","onUpdate:modelValue":s[3]||(s[3]=c=>l.newp=c),type:"password",autocomplete:"new-password"},null,512),[[_,l.newp]]),e("label",$,t(a(o).t("confirmPassword")),1),v(e("input",{id:"pw-confirm","onUpdate:modelValue":s[4]||(s[4]=c=>l.confirm=c),type:"password",autocomplete:"new-password"},null,512),[[_,l.confirm]]),e("button",{class:"primary",type:"submit",disabled:d.value,style:{"margin-top":"16px"}},t(d.value?"…":a(o).t("changePassword")),9,ee)])],32),u.value?(b(),g("p",{key:0,class:y(f.value?"error":""),style:{"margin-top":"10px"}},t(u.value),3)):M("",!0)]),e("section",se,[e("h3",te,t(a(o).t("account")),1),e("p",ae,t(a(o).t("role"))+" / "+t(a(o).t("status")),1),e("div",oe,[e("span",ne,t(C.value),1),e("div",null,[e("div",le,t(D.value),1),e("div",ie,t((S=a(i).user)==null?void 0:S.username),1)]),e("span",{class:y(["badge",a(i).role])},t(a(i).role),3)])])])])}}};export{ue as default};

View File

@@ -0,0 +1 @@
import{c as v,a as e,t,u as s,i as a,j as x,f as _,w as f,v as w,b as y,e as S,r,g as V,o as m}from"./index-B1lw4PRH.js";import{_ as K}from"./_plugin-vue_export-helper-DlAUqK2U.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=V(),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,[x(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),f(e("input",{"onUpdate:modelValue":u[0]||(u[0]=n=>i.value=n),type:"email",autocomplete:"email",onKeyup:y(c,["enter"])},null,544),[[w,i.value]]),e("label",null,t(s(a).t("newPassword")),1),f(e("input",{"onUpdate:modelValue":u[1]||(u[1]=n=>l.value=n),type:"password",autocomplete:"new-password",onKeyup:y(c,["enter"])},null,544),[[w,l.value]]),e("label",null,t(s(a).t("confirmPassword")),1),f(e("input",{"onUpdate:modelValue":u[2]||(u[2]=n=>p.value=n),type:"password",autocomplete:"new-password",onKeyup:y(c,["enter"])},null,544),[[w,p.value]]),o.value?(m(),v("div",U,t(o.value),1)):S("",!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)])])}}},R=K(P,[["__scopeId","data-v-f190e120"]]);export{R as default};

View File

@@ -0,0 +1 @@
.setup-wrap[data-v-f190e120]{display:flex;justify-content:center;padding-top:8vh}.setup-card[data-v-f190e120]{width:380px}h1[data-v-f190e120]{margin-top:0}

View File

@@ -0,0 +1 @@
import{k as x,p as f,c as i,a as e,t,u as s,i as o,f as l,l as c,m as p,e as _,F as v,n as k,r as h,q as b,o as n,x as w,y as B}from"./index-B1lw4PRH.js";import{_ as N}from"./_plugin-vue_export-helper-DlAUqK2U.js";const A={class:"row",style:{"margin-bottom":"16px","align-items":"center"}},z={style:{margin:"0"}},C={class:"muted",style:{margin:"4px 0 0","font-size":"13px"}},$={key:0,style:{"margin-left":"auto"}},P={class:"primary"},T={key:0,class:"card",style:{"min-height":"120px"}},V={key:1,class:"card empty-state"},F={key:0},j={key:1},q={class:"grid"},D={class:"row",style:{"justify-content":"space-between","align-items":"center"}},E={key:0,class:"row",style:{gap:"6px"}},G={style:{padding:"6px 12px","min-height":"38px"}},I={style:{padding:"6px 12px","min-height":"38px"}},L={style:{margin:"10px 0 4px","font-weight":"700","font-size":"15px"}},M={class:"muted",style:{"font-size":"13px","margin-bottom":"12px"}},S={key:0,class:"row",style:{gap:"8px","flex-wrap":"wrap","margin-top":"8px"}},H={class:"primary"},J={class:"primary"},K={key:2,class:"muted",style:{"font-size":"12px","margin-top":"8px"}},O={__name:"Training",setup(Q){const u=h([]),m=h(!0);function g(r){return r.sales_kit&&r.sales_kit.productName||r.input&&r.input.product||""}return x(async()=>{try{u.value=(await f.listGroups()).groups}finally{m.value=!1}}),(r,y)=>{const d=b("router-link");return n(),i("div",null,[e("div",A,[e("div",null,[e("h2",z,t(s(o).t("tabTraining")),1),e("p",C,t(s(o).t("selectPersona")),1)]),s(l).isAdmin?(n(),i("div",$,[c(d,{to:"/admin/new-group"},{default:p(()=>[e("button",P,"+ "+t(s(o).t("groupBuilder"))+" / "+t(s(o).t("groupBuilder"))+" "+t(s(o).t("create")),1)]),_:1})])):_("",!0)]),m.value?(n(),i("div",T,[...y[0]||(y[0]=[e("div",{class:"skeleton",style:{height:"60px"}},null,-1),e("div",{class:"skeleton",style:{height:"60px","margin-top":"10px"}},null,-1)])])):u.value.length===0?(n(),i("div",V,[e("strong",null,t(s(l).isAdmin?"No products yet":"No training products available"),1),s(l).isAdmin?(n(),i("span",F,t(s(o).t("groupBuilder"))+" to add a "+t(s(o).t("product"))+" + personas.",1)):(n(),i("span",j,"Ask an admin to add a "+t(s(o).t("product"))+".",1))])):_("",!0),e("div",q,[(n(!0),i(v,null,k(u.value,a=>(n(),i("div",{key:a.id,class:"card group-card lift"},[e("div",D,[e("span",{class:w(["badge",a.status])},t(a.status),3),s(l).isAdmin?(n(),i("span",E,[c(d,{to:`/groups/${a.id}/personas`},{default:p(()=>[e("button",G,t(s(o).t("personas"))+" ("+t((a.personas||[]).length)+")",1)]),_:2},1032,["to"]),c(d,{to:`/admin/groups/${a.id}/edit`},{default:p(()=>[e("button",I,t(s(o).t("edit")),1)]),_:1},8,["to"])])):_("",!0)]),e("div",L,t(a.title),1),e("div",M,t(g(a)),1),s(l).isAdmin?(n(),i("div",S,[c(d,{to:`/groups/${a.id}/personas`},{default:p(()=>[e("button",H,t(s(o).t("selectPersona")),1)]),_:1},8,["to"])])):a.status==="ready"?(n(),B(d,{key:1,to:`/groups/${a.id}/personas`},{default:p(()=>[e("button",J,t(s(o).t("selectPersona")),1)]),_:1},8,["to"])):(n(),i("div",K,"⏳ "+t(s(o).t("status"))+": "+t(a.status),1))]))),128))])])}}},W=N(O,[["__scopeId","data-v-d28970f8"]]);export{W as default};

View File

@@ -0,0 +1 @@
.grid[data-v-d28970f8]{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px}.group-card[data-v-d28970f8]{display:flex;flex-direction:column}.group-card>a[data-v-d28970f8]{margin-top:auto}

View File

@@ -0,0 +1 @@
.stat[data-v-dd8a8071]{text-align:center;min-width:100px}.stat div[data-v-dd8a8071]{color:var(--muted);font-size:12px}.stat strong[data-v-dd8a8071]{font-size:22px}

View File

@@ -0,0 +1 @@
import{k as _,p as m,c as l,a as s,t,u as c,i as v,l as y,m as g,F as r,n as i,e as k,r as x,q as w,o as n,x as f,j as d}from"./index-B1lw4PRH.js";import{_ as b}from"./_plugin-vue_export-helper-DlAUqK2U.js";const N={class:"row",style:{"align-items":"center"}},h={style:{margin:"0"}},A={class:"row",style:{gap:"16px",margin:"16px 0"}},C={class:"card stat"},T={class:"card stat"},V={class:"card stat"},B={key:0,class:"card muted"},W={class:"muted"},F={__name:"WeakAreas",setup(L){const a=x({wins:0,losses:0,total_sessions:0,by_tier:[],top_loss_personas:[]});return _(async()=>{a.value=(await m.weakAreas()).insight}),(j,e)=>{const u=w("router-link");return n(),l("div",null,[s("div",N,[s("h2",h,t(c(v).t("weakAreas")),1),y(u,{to:"/my/generate?mode=weak",style:{"margin-left":"auto"}},{default:g(()=>[...e[0]||(e[0]=[s("button",{class:"primary"},"🔒 Generate a lock persona",-1)])]),_:1})]),s("div",A,[s("div",C,[e[1]||(e[1]=s("div",null,"Wins",-1)),s("strong",null,t(a.value.wins),1)]),s("div",T,[e[2]||(e[2]=s("div",null,"Losses",-1)),s("strong",null,t(a.value.losses),1)]),s("div",V,[e[3]||(e[3]=s("div",null,"Total",-1)),s("strong",null,t(a.value.total_sessions),1)])]),(n(!0),l(r,null,i(a.value.by_tier,o=>(n(),l("div",{key:o.value,class:"card",style:{"margin-bottom":"8px"}},[s("span",{class:f(["badge",o.value])},"Tier "+t(o.value),3),d(" — "+t(o.losses)+" losses ",1)]))),128)),e[5]||(e[5]=s("h3",{style:{"margin-top":"20px"}},"Top loss personas",-1)),!a.value.top_loss_personas||!a.value.top_loss_personas.length?(n(),l("div",B,"No losses yet — 🎉")):k("",!0),(n(!0),l(r,null,i(a.value.top_loss_personas,(o,p)=>(n(),l("div",{class:"card",key:p,style:{"margin-bottom":"8px"}},[s("strong",null,t(o.persona_name),1),d(" — score "+t(o.score),1),e[4]||(e[4]=s("br",null,null,-1)),s("span",W,t(o.why),1)]))),128))])}}},D=b(F,[["__scopeId","data-v-dd8a8071"]]);export{D as default};

View File

@@ -0,0 +1 @@
const s=(t,r)=>{const o=t.__vccOpts||t;for(const[c,e]of r)o[c]=e;return o};export{s as _};

27
frontend/dist/assets/index-B1lw4PRH.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

13
frontend/dist/index.html vendored Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<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-B1lw4PRH.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-SkBDFfY3.css">
</head>
<body>
<div id="app"></div>
</body>
</html>