Commit Graph

72 Commits

Author SHA1 Message Date
Macky
1ad21cc8f1 test: cover product delete cascade (group + own sessions) keeping other-org sessions
Proves DELETE /api/groups/<gid> removes the product, its personas (stored on the
group), its own sessions, and uploads, while sessions of an unrelated product/
org survive. Guards the cascade against future regressions.
2026-08-26 14:04:47 +07:00
Macky
47f92b48bf fix(chat): remove manual finish button — sale auto-closes when the customer decides
The realistic sales simulation must not offer a "Finish practice / จบการฝึก"
button, because the chat is meant to auto-close on buy/walk/try (as originally
decided in ffb7cdd). The finish button and its chatFinish handler were
reintroduced in 3c22d88 and are now removed; the composer holds only Send and the
debrief renders automatically once the session finishes. Regression test asserts
no finish/summary button is rendered.
2026-08-26 14:04:47 +07:00
Macky
0903b19691 fix(ui): capitalize Persona in EN+TH UI copy and drop forced-lowercase count label
Use "Persona"/"Personas" (capital P) as the feature term across the English and
Thai UI instead of lowercase "persona". Thai now reads "Persona" with a space
separator in mixed Thai-English sentences (e.g. "เลือก Persona เพื่อฝึก",
"จัดการ Persona", "บันทึก Persona"). Remove .toLowerCase() on the persona count
label in Training so it renders "Personas"/"Persona" consistently.
2026-08-25 21:05:13 +07:00
Macky
fde094e233 docs: capitalize Persona/Personas for the product feature in review docs
Feature-word 'persona'/'personas' now reads 'Persona'/'Personas' (capital P)
across the current review checkpoint docs, matching the UI terminology. Code
identifiers, API paths, and schema fields stay lowercase.
2026-08-25 20:37:54 +07:00
Macky
6a2e6a326f fix: restore legacy training data hidden by new visibility schema (migrate-on-read)
Records written before the visibility field existed carry none; the new
fail-closed authorization treated missing visibility as invalid, making every
legacy group unlistable and unreadable. Add resolved_visibility(group) that
derives effective visibility for legacy records only (owner present => private,
absent => public), leaves explicit-malformed visibility fail-closed (None), and
never derives demo/hidden. Apply it at every list, authorization, chat, and
analytics boundary while keeping demo and hidden-preview paths raw and
owner_user_id-based private isolation intact. No persisted data is rewritten.

Backend full suite passes 517; frontend 26/26; production build passes.
2026-08-25 08:59:22 +07:00
Macky
3c22d88bcd feat: demo SaaS + training flow security hardening (8/8 review gate passed)
- Demo accounts: super_admin-only provisioning into isolated DEMO_ORG_ID tenant,
  30-day UTC trial on first login, revocable, one-time credential delivery via
  optional SES/webhook (never persisted). Adds boto3 dependency.
- Analytics/report/export/privacy: shared bounded scan budget across users/groups/
  sessions, tenant-consistent session/user/group joins, scalar-only CSV export
  (no nested persisted-value stringification).
- Ownership/tenant isolation: canonical owner-tenant predicate for list/read/chat;
  client sees is_owned only, never owner_user_id.
- Lifecycle/races: status transition validation, analyzing is an in-progress gate
  (no duplicate reanalysis), structured-ready publication, stale-variant revalidation.
- Auth/setup/consent/JWT/OAuth/config: fail-closed consent, bounded JWT lifetime,
  provider-subject atomic OAuth identity, repeated-secret rejection, strict Persona
  trait validation.
- Chat/session/privacy: pre-seller opener redaction, corrupt-session recovery,
  role-aware completed-chat dashboard routing.
- Frontend: Training→product→personas→practice flow, demo/role/demo guards,
  is_owned-based ownership display, 320×568 and 500×768 responsive E2E.
- 8 independent exact-five-key review scopes passed; backend 509, frontend 26,
  production build 1775 modules, isolated E2E 15.
2026-08-25 06:39:06 +07:00
Macky
8a632b5e6a docs: record successful production deploy of UX/SAAS redesign 2026-08-21 12:30:21 +07:00
Macky
9fd748154d feat: UX/SAAS 12-point redesign
- auth: self-registration (role=user); first-created-user becomes super_admin
- roles: super_admin may promote others; regular admin cannot see super_admin accounts
- products: user-created private groups; admin 'สินค้าขององค์กร' (shared) with hidden/public; users can create groups
- analytics: team + per-user weak areas, close-rate-by-difficulty buckets, 30-day default, weekly trend, trainee table, active users; dashboard redesigned
- files: docx + xlsx upload support (python-docx + openpyxl)
- ui: tabs การฝึก→ผลการฝึก→ภาพรวม; admin lands on ภาพรวม / user on การฝึก; guide in topbar
- consolidate: weak-areas merged into Results (10/page), my-personas merged into Training
- copy: บุคคลต้นแบบ→persona everywhere; clearer add-product form (A/B/C, upload-or-fill)
- report: remove ดูรายงาน UI entry (endpoint kept)

Backend 348 tests pass; frontend build + vitest clean.
2026-08-21 12:28:22 +07:00
Macky
2950ffad89 docs: record successful production deploy of OAuth + website removal 2026-08-20 11:39:48 +07:00
Macky
d6e7cffc84 feat(auth): Google + Facebook OAuth login/register
Public social signup into OAUTH_DEFAULT_ORG (role user, seat-checked);
email-match links existing active user instead of duplicating. Server-side
provider token validation via stdlib urllib only (no new dep): Google
tokeninfo (aud + email_verified) and Facebook app/debug-token/me (is_valid,
app_id, me.id==user_id). Fail-closed when creds unconfigured, rate-limited
per-IP + per-email, /oauth/config leaks no secrets. Frontend: login buttons
(only enabled providers), GSI + FB SDK on-demand, monochrome glyphs, TH/EN.

Login page shows social buttons only when backend reports provider enabled.

348 backend tests pass (337 + 11 new OAuth), frontend build + 4/4 unit
clean, manual security review PASS. Not pushed (push auto-deploys).
2026-08-20 10:57:04 +07:00
Macky
74de0d4d4f chore(website): remove marketing site from app repo (moved to separate project ~/Gitea/Sales Trainer Website)
Per owner: this repo should contain only the app. The marketing landing
site now lives in its own git repo (Sales Trainer Website); not pushed.
2026-08-20 09:25:20 +07:00
Macky
ece187be44 docs: record successful production deploy of UX redesign + marketing site
Verified live: backend booted (JWT_SECRET set, no crash-loop), new bundle
hashes + new Login design served, website/ in repo (not yet deployed
publicly; pricing placeholders pending).
2026-08-19 18:34:41 +07:00
Macky
c263cc81df feat(ui): app UX/UI redesign + marketing landing site
App (Vue SPA) — 100% presentational, global design-token rebuild in
style.css + polished App/Login/Chat/MyBoard/Personas/Setup/Training.
No logic/API/route/i18n changes (verified: build clean, 4/4 vitest,
independent review PASS 5/5).

website/ — new self-contained static marketing landing site (TH-primary
+ EN toggle, CSS chat mockup, Hallmark theme), verified responsive at
320-768 (no horizontal scroll). Pricing uses placeholders pending owner.

Docs: engineering-log 2026-08-19 + HANDOFF updated.

NOT pushed (push to main auto-deploys to EasyPanel production;
operator approval + JWT_SECRET check required before deploy).
2026-08-19 15:46:21 +07:00
Macky
4bff7db098 fix(persona): openers must ASK or say สนใจ — never 'ลองดู/ลอง/แค่มาดู'
- persona_prompts OPENER RULE: openers must be a direct question OR declare
  interest ('สนใจ/สอบถาม'); explicitly forbid 'ลองดู / ลอง / มาลอง / แค่มาดู /
  กดดู / แวะดู' (trying/browsing) phrasing which Thai customers don't use
- fix_openers.py: rewrite browse/try openers into a polite natural interest
  opener ('สวัสดีครับ/ค่ะ สนใจสอบถามสินค้าครับ ขอรายละเอียดได้ไหม'), leaving
  openers that already ask a question or say สนใจ untouched
2026-08-19 10:19:29 +07:00
Macky
7fcef45649 fix(persona): forbid dismissive openers + fix script for '..' and negative tails
- persona_prompts OPENER RULE: never open with dots/ellipsis and never end
  an opener by dismissing/belittling the product (but ไม่คิดจะซื้อ, แค่มาดูเล่น,
  ก็แค่มาเมิน, ไม่ได้จะซื้อ...) — Thai openers stay politely interested
- fix_openers.py: strip leading dots("..") and remove trailing dismissive
  clauses ('ตามไม่คิดจะซื้อหรอกนะ' etc.) so existing personas are normalized
  to a natural interest opener; update docstring
2026-08-19 10:12:56 +07:00
Macky
165cd840cf fix(script): bootstrap sys.path so 'app' imports regardless of cwd in fix_openers 2026-08-19 10:05:45 +07:00
Macky
9b79e0c53e feat(script): one-time fix_existing personas' openers to natural Thai
Deterministic, conservative: only rewrites openers that lead with a casual
interjection (เฮ้/เอ้/เอ่อ/เอ๊ะ/อืม/เออ/นี่ๆ/ว่าไง/hey/hi/hiya/yo) or a bare
politeness word (ครับ/ค่ะ). Leaves natural Thai and English openers untouched.
Prints before -> after. Run in the container console:
  cd /app/backend && python3 scripts/fix_openers.py
2026-08-19 09:30:35 +07:00
Macky
07e80be952 fix(persona): natural Thai openers - 'สวัสดีครับ/ค่ะ' + direct question, no 'เฮ้/เอ้'
- persona_prompts OPENER RULE: mandate polite Thai openers that start
  with สวัสดีครับ/ค่ะ and go straight to the question or 'สนใจ/สอบถาม';
  explicitly forbid casual English fillers (เฮ้/เอ้/เอ่อ/hey) and
  singsong slang for Thai
- chat_routes: default opener fallback is now locale-aware - Thai locale
  uses 'สวัสดีครับ สนใจสินค้าของคุณครับ...' instead of the English one
2026-08-19 09:26:19 +07:00
Macky
4b5a14079b [verified] all roles train for real: remove preview, 1 persona = 1 chat per user
- listPersonas: compute per-user my_outcome for ALL roles; always set the
  key (default 'not_tried') so the all-roles UI never sees undefined
  (fixes global super_admin without org showing every persona as trained)
- start_session/mode lookups (send/finish/resume/authorize): no preview
  mode; every role creates one-shot 'trainee' sessions. The one-shot lock
  (1 persona chat per user; many users per persona) now applies to all roles
- Chat.vue: always start in trainee mode, remove preview label
- Personas.vue: unified branches - show 'แชท' when untrained, 'สรุปผล'+variant
  when trained, for all roles
- Rewrite test_admin_preview.py to assert the new no-preview one-shot behavior
2026-08-19 08:32:33 +07:00
Macky
ab5fff0bd9 [verified] IP-protect pain (hide from all roles) + persona summary button + topbar user dropdown
- Backend: strip pain/painProgress/revealed_persona.pains from debrief
  serializer and remove pain/initialPainFit from admin report so the
  coaching formula never leaks to any user-facing role (persona keeps
  pains internally to drive the judge/training)
- Fix [object Object] array-of-objects rendering in Chat/SessionDetail
- Personas: trained persona shows 'สรุปผล/Summary' button -> chat page
  with past result (chat already loads finished session)
- Top bar: username dropdown containing Settings + Logout (was separate
  logout button); variant-from-base already preserves tier/difficulty
- Updated debrief allowlist tests to reflect new redaction
2026-08-19 07:26:21 +07:00
Macky
711e058b24 [verified] fix debrief pain display ([object Object]) + add 'create persona from this one' button
- formatValue(): render arrays of objects (e.g. pains) as readable text
  instead of '[object Object], [object Object]'
- SessionDetail: new 'สร้างบุคคลต้นแบบจากต้นแบบนี้' button calls the
  existing group persona-variant endpoint so trainees/orders can regenerate
  a persona based on the revealed one after a session
2026-08-18 22:01:27 +07:00
Macky
19e9fd2328 docs: record JWT_SECRET boot-crash root cause for 2026-08-18 deploy 2026-08-18 14:41:00 +07:00
Macky
5febcc83c4 docs: note deploy-not-live follow-up for 2026-08-18 UX fixes (force webhook re-trigger) 2026-08-18 12:04:11 +07:00
Macky
ffb7cdda31 [verified] fix live-QA UX: no JSON leak in chat, greeting openers (wrong_text cools off post-first-reply), auto-close+summarize on buy/walk/try, remove Finish button 2026-08-18 09:42:05 +07:00
Macky
2c40fc7502 feat(import): commit S4.4 JSON→PostgreSQL importer + API error-handler hardening
Re-verified staged increment from a clean requirements.lock.txt venv:
- 330 backend tests pass (17/17 in new error_handlers + json_import tests)
- compileall + frontend npm build clean
- git diff --check clean; no secrets in diff
- importer CLI dry-run bootstrap works

Includes JSON HTTPException handler under /api/* and parse-safe static 404
via abort. JSON stores remain runtime-authoritative; production operation
still gated behind operator approval.
2026-08-16 07:42:32 +07:00
Macky
dbfce9aa54 [verified] harden Sales Trainer and add PostgreSQL foundation 2026-08-16 02:41:10 +07:00
Macky
6811dc1db9 docs: refresh HANDOFF + add 2026-08-09 engineering-log entry (idea-flow, live QA, LLM-judge decisions, persona variant) 2026-08-09 19:18:10 +07:00
Macky
c92400b195 refactor(chat): decide buy/walk by per-turn LLM judge (not fixed keywords)
Removed the fixed-value text detector. persona_reply no longer forces JSON meta; instead a
per-turn evaluate_turn() calls the judge LLM after every customer reply to read the persona's
current mood + whether it has decided (buy/walk/pending) + score_delta + reason. send_message
consumes that context-based decision to (a) end the chat as won/lost and (b) move the score.

This is what the user asked: the system evaluates EVERY turn and decides at the moment it's
truly committed — not keyword matching (so 'ซื้อไม่ไหว แต่ว่ามีผ่อนไหม?' stays pending).
Mock updated: judge returns buy on first send (keeps E2E deterministic). 11/11 suites pass.
2026-08-09 13:19:15 +07:00
Macky
94e75238a3 fix(chat): resume unfinished session without re-picking scenario; detect win/loss from real-LLM text
- start_session now RESUMES an existing ACTIVE session for the persona instead of creating
  a new one / forcing re-pick of the scenario (keep original scenario+messages).
- send_message falls back to a text-based decision detector (_detect_customer_decision)
  because real LLMs rarely emit structured meta.decision — so a customer who says
  'ซื้อไม่ไหว'/'no thanks' now actually ENDS the chat as lost (was stuck active forever).
  Fragments handled in TH + EN; buy + walk.
All 11 backend suites pass. Rebuilt dist.
2026-08-09 13:12:08 +07:00
Macky
10c7d01236 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.
2026-08-09 12:43:00 +07:00
Macky
8771438aef fix(scenario): recontact chats normally first then re-engages mid-chat; channel default=social; remove 'add more' button (15 personas auto)
- recontact persona now opens as a NORMAL customer (no 'I asked before' in the opening);
  the mid-chat time-lapse system note (turn 2) makes them re-engage warmer instead.
- channel default changed facebook->social everywhere (create/analyze/store/simulator/me).
- 15 personas auto-generated (TARGET=15); removed the 'สร้างบุคคลต้นแบบเพิ่มเติม' button/guide.
All 9 backend suites pass. Rebuilt dist.
2026-08-09 12:37:43 +07:00
Macky
2b414d13b7 fix(analyze): don't hard-fail when LLM generates <15 personas (retry + accept)
Live test found: deepseek returned 14/15 personas -> whole analyze 500'd, breaking the
'กดสร้าง -> auto-analyze' flow. Now generate() retries up to 3x with a nudge, and accepts
a short result (>=8 personas) instead of crashing — admin can top up the rest with
'สร้างบุคคลต้นแบบเพิ่มเติม'. Also default tolerance/recontact on generated personas.
All backend suites pass.
2026-08-09 12:23:15 +07:00
Macky
479d77757f fix(chat): map customer/seller roles for OpenAI-compat LLM; re-contact = mid-chat time-lapse
- llm.complete_conversation now maps internal roles (customer->assistant, seller->user,
  system->system) before the API call — fixes 'Unknown role: customer' (501).
- Re-contact personality: the customer now chats normally, at turn 2 goes quiet and a
  system time-lapse note is shown (' ผ่านไป 2-3 สัปดาห์...'), then re-engages warmer —
  instead of 'pretending you asked before' at start. Driven by persona.recontact trait.
All 9 backend suites pass. Rebuilt dist.
2026-08-09 11:25:50 +07:00
Macky
fd2cae9e62 refactor(scenario): only 2 scenarios (social / face-to-face); 're-contact' becomes a persona trait
- Scenario picker now has only social + f2f_call (removed recontact) in backend
  _scenarios, start_session validation, and Chat.vue picker.
- 'ลูกค้ากลับมาติดต่อ' is no longer a scenario: it's now a PERSONA trait. Persona prompt
  generates ~1-in-4 personas with recontact=true (asked before, now returns warmer/ready);
  store shape gets recontact field; simulator injects a re-contact note into the persona's
  system prompt so it plays as a returning customer naturally.
- test_scenario updated: unknown scenario defaults to social; recontact shown as a
  generated persona trait.
All 9 backend suites pass. Rebuilt dist.
2026-08-09 11:21:34 +07:00
Macky
cb2642a659 fix(ui): no raw JSON on Training list, pain no longer [object Object], add Chat button on GroupEdit, drop facebook/line from persona
- list_groups returns lightweight summaries (id/title/status/persona_count + product) —
  never the full personas array, so Training no longer shows a long JSON blob.
- PersonaForm toLines extracts .description/.text from pain/objection objects (fixes
  '[object Object]'); save re-wraps pains as {description}.
- GroupEdit persona cards get a 'แชท' (MessageSquare) button so you can start chatting
  right where you land after creating (chat was only on Personas page before).
- channel: default neutral 'social', removed facebook/line validation + removed from
  revealable_view (scenario now sets the tone, not fb/line).
All 9 backend suites pass. Rebuilt dist.
2026-08-09 11:15:13 +07:00
Macky
f8719d77e3 ux(flow): remove facebook/line channel everywhere (scenario chosen at chat); clarify 'how to start chatting'
- Removed channel + initiation displays: Training card, Personas card, GroupEdit card,
  Chat header, and the channel select in PersonaForm (scenario now determines who opens).
- Clarified the start-training flow: GroupEdit guide now says go to the Training tab ->
  pick product -> pick a customer -> Chat -> choose scenario -> talk; Personas guide
  spells out the Chat + scenario step.
Rebuilt dist.
2026-08-09 11:08:14 +07:00
Macky
5b167b4546 feat(training): delete group button (admin only) on Training page
- Backend DELETE /api/groups/<gid> (admin, own-org enforced via _get_owned_group);
  cascades removal of the group's sessions. GroupStore.delete added.
- Training.vue: each group card gets a trash (Trash2 line icon) delete button shown only
  to admins (auth.isAdmin); confirm dialog; @click.stop so it doesn't navigate. Card
  restructured so the delete button is not inside the clickable router-link.
- test_user_journey: trainee cannot delete (403), admin deletes, group gone (404).
All suites pass. Rebuilt dist.
2026-08-09 10:20:10 +07:00
Macky
0b92430aab feat(idea): create group auto-generates personas; channel removed; product->product/idea
Per product-idea focus:
- GroupBuilder: 'สินค้า/บริการ/ไอเดีย' label; removed the channel select (channel is now
  chosen at chat time as a scenario, not at persona create). Create now auto-runs analyze
  so personas are generated immediately (no separate Analyze button).
- GroupEdit: analyze button becomes 'สร้างบุคคลต้นแบบเพิ่มเติม' which APPENDS more personas
  (backend analyze?append=true reuses sales kit + keeps existing instead of replacing).
- i18n product label updated.
- User-journey test asserts append adds personas (15->30) and existing kept.
All 9 backend suites pass. Rebuilt dist.
2026-08-09 10:12:40 +07:00
Macky
675cc5c73f test: add user-journey flow test (idea end-to-end, mock LLM)
Covers exact product idea: admin create group -> analyze -> personas; role/IP splits
(super_admin sees recipe, admin stripped, trainee revealable-only); trainee picks
scenario (social=customer opens) -> chat -> persona decides (buy->won) -> readable
debrief -> one-shot enforced. All pass.
2026-08-09 10:01:38 +07:00
Macky
3e1badc2d3 docs: add FUTURE_WORK.md (deferred infra/SaaS/UX items); focus shifts to using the app as designed 2026-08-09 09:51:27 +07:00
Macky
3d5c81fbd7 feat(saas): Phase 3 — plan/seats/active model, ToS consent, signed expiring export
P3a: org carries plan/seats/active/created_at; create_user enforces seats + rejects
inactive org; verify blocks login for inactive orgs; PATCH /api/admin/orgs (super_admin)
updates plan/seats/active with audit. Fixed verify swallowing its AuthError.
P3b: export/token issues a 5-min HMAC one-time CSV link; export accepts ?token=.
P3c: setup requires accepted_terms (consent stored); Setup.vue consent checkbox.
All 8 backend suites pass. Rebuilt dist.
2026-08-09 09:48:55 +07:00
Macky
056753e8cb feat(saas): multi-tenant isolation (Phase 1) + rate-limit/audit/org-scoped export (Phase 2)
Phase 1 (tenant isolation):
- g.org_id set on require_auth; assert_tenant()/current_org_id() choke-point helpers.
- Multi-org provisioning: POST /api/admin/users {new_org:true} (super_admin) creates a
  new org + its first admin; GET /api/admin/orgs (super_admin sees all, admin own).
- Fixed latent create_org double-id bug (dict id != store key).
- test_saas_tenant.py: org2 admin blocked from org1 group (403), can't list org1
  groups/users, sees only own org; super_admin sees all.

Phase 2 (hardening):
- Rate limit login (per-IP + per-username) + chat send (per-user) to protect LLM cost
  and slow brute force; services/rate_limit.py (in-memory + disk, no external deps).
- Audit log data/audit/audit.jsonl on org.create, user.promote_super_admin, analytics.export.
- CSV export now org-scoped (admin exports only own org).
All 8 backend suites pass.
2026-08-09 09:35:26 +07:00
Macky
e1d61e1e1e feat(ip): protect persona 'formula' — secret fields only super_admin sees/edits
IP protection so casual copying yields inferior results:
- SECRET_PERSONA_FIELDS (pains/objections/negotiation_levers/opener/tolerance +
  pain rootCause/resolutionConditions): only super_admin can view/edit them.
- list_personas/get_persona/update_persona/get_group strip these for role=admin (and
  hide sales_kit + pain-fit report from admins too).
- update_persona rejects admin attempts to set secret fields (403).
- PersonaForm hides the 'การขาย' recipe section for non-super-admin (shows locked note);
  auth.isSuperAdmin getter added.
Rebuilt dist. Added test_ip_protection.
2026-08-09 07:22:50 +07:00
Macky
a04bc2add8 fix(ui): widen login/setup forms so inputs don't truncate text; line icons
- Login card 360->460px (max-width:100%), wider inputs (46px, 15px, more padding),
  mobile side padding; eye-toggles -> lucide Eye/EyeOff line icons (no emoji).
- Setup card 380->460px (max-width:100%), wider inputs, Lock line icon instead of emoji.
Rebuilt dist.
2026-08-08 17:00:09 +07:00
Macky
b41bfee03b chore(ui): remove 4 orphaned views (Dashboard/GenPersona/MySessions/WeakAreas) + mobile polish
- Delete unused views not referenced by router/components (dead files from earlier builds).
- Mobile (640px/460px/380px): nav tabs wrap to a centered full-width row + hide brand at
  <380; logout shows icon-only; stat cards center when collapsing to one column
  (Moreminimore convention); chat bubbles/width handled.
Rebuilt dist.
2026-08-08 16:12:30 +07:00
Macky
15c60ae400 feat(i18n + export + guide): locale-aware scenarios/debrief; CSV export; how-to-use page
Backend:
- SCENARIOS localized (th/en): scenario preamble + persona tone adaptation follow the
  trainee's locale; start accepts {locale} and stores it on the session; send/auto-finish
  use it so debrief text (why/coaching/turning points) is in the active language.
- /api/analytics/export returns per-trainee finished-session CSV (admin).

Frontend:
- Scenario picker labels localize by i18n.locale.
- New /guide page (non-IT how-to), linked from Training.
- Analytics: date-filter bar uses line icon + 'Download CSV' button (fetch w/ auth).
Rebuilt dist.
2026-08-08 15:06:48 +07:00
Macky
aa2eb8dd37 feat(chat): persona decides to buy/walk — auto-finish + tolerance (temper) + resume
The conversation now ENDS when the persona makes a decision (option C), not when the
trainee clicks a button:
- persona_id replies carry {reply, decision(none/buy/walk), mood}; when decision is
  buy/walk the session auto-finishes (won/lost) with a debrief that reveals latent
  details + per-turn 'turning points'.
- personas have a tolerance (1-5, 'temper'): impatient personas walk away fast after
  poor answers (fed via internal.misses on mood<=-1); tough 'wrong text' cases can
  still be won by a strong, gentle response (judge realism).
- trainee 'Finish' button removed; if they leave mid-chat an active session is resumed
  via /chat/resume (continue, not restart). One-shot lock still enforced once decided.
- mock/tests updated: persona deciding buy -> send auto-finishes won.
Rebuilt dist.
2026-08-08 14:34:34 +07:00
Macky
bd6a7ffa32 feat(chat): scenario-based training — choice of channel/Situation + realistic per-turn evaluation
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.
2026-08-08 11:27:05 +07:00
Macky
8670addcc3 ui(icons): replace colored emoji with modern single-color line icons (lucide-vue-next)
Add lucide-vue-next (stroke-based, single-color, currentColor) and swap emoji icons
across nav + all main views: Settings/LogOut (App), BarChart3/Users/Plus (Analytics),
LayoutDashboard (MyBoard), Sparkles/Users/Pencil (GroupEdit), Box/ListChecks/Paperclip/
PlusCircle/ArrowLeft (GroupBuilder), Target/ArrowLeft/Search (Chat/Personas/Training),
User/Lock/Globe (Settings), Users/UserPlus/Info (AdminUsers).
- Buttons become inline-flex for icon+text alignment.
- Icons inherit currentColor (single color, modern line style).
- Rebuild dist with lucide.
2026-08-08 10:55:45 +07:00
Macky
22d3e51980 ux(ui): beginner-friendly guidance across trainee/admin flows + fix AdminUsers to username
- Chat: add 'How to practice' guide card; render revealed persona as readable
  label:value grid instead of JSON.
- AdminUsers: switch create form to username (matches username login), add step guide,
  role explanation (user=ฝึก, admin=ดูแลระบบ).
- Analytics (admin overview) + MyBoard (personal): add plain-Thai explainer subtitle.
- Rebuild dist. Verified user create with username -> 201.
2026-08-08 10:34:24 +07:00