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.
This commit is contained in:
@@ -47,7 +47,7 @@ export const api = {
|
||||
listPersonas: (gid) => request('GET', `/api/groups/${gid}/personas`),
|
||||
getPersona: (gid, pid) => request('GET', `/api/groups/${gid}/personas/${pid}`),
|
||||
updatePersona: (gid, pid, b) => request('PUT', `/api/groups/${gid}/personas/${pid}`, b),
|
||||
chatStart: (gid, pid, scenario) => request('POST', `/api/chat/${gid}/personas/${pid}/chat/start`, scenario || {}),
|
||||
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`),
|
||||
chatSend: (gid, pid, text) => request('POST', `/api/chat/${gid}/personas/${pid}/chat/send`, { text }),
|
||||
chatFinish: (gid, pid) => request('POST', `/api/chat/${gid}/personas/${pid}/chat/finish`),
|
||||
|
||||
@@ -21,6 +21,7 @@ const messages = {
|
||||
training: 'Training',
|
||||
myDashboard: 'My dashboard',
|
||||
adminOverview: 'Admin overview',
|
||||
guideTitle: 'How to use / คู่มือใช้งาน',
|
||||
settings: 'Settings',
|
||||
account: 'Account',
|
||||
profile: 'Profile',
|
||||
|
||||
@@ -14,6 +14,8 @@ const routes = [
|
||||
{ path: '/groups/:gid/chat/:pid', component: () => import('../views/Chat.vue') },
|
||||
// Settings
|
||||
{ path: '/settings', component: () => import('../views/Settings.vue') },
|
||||
// Guide (how to use, non-IT friendly)
|
||||
{ path: '/guide', component: () => import('../views/Guide.vue') },
|
||||
// Admin management
|
||||
{ path: '/admin/new-group', component: () => import('../views/GroupBuilder.vue'), meta: { admin: true } },
|
||||
{ path: '/admin/groups/:gid/edit', component: () => import('../views/GroupEdit.vue'), meta: { admin: true } },
|
||||
|
||||
@@ -13,12 +13,13 @@
|
||||
|
||||
<!-- Date filter -->
|
||||
<div class="card filter-bar">
|
||||
<span class="muted">🗓 {{ i18n.t('dateRange') }}</span>
|
||||
<span class="muted"><Download :size="15" :stroke-width="1.8" style="vertical-align:-2px" /> {{ i18n.t('dateRange') }}</span>
|
||||
<input type="date" v-model="from" />
|
||||
<span>–</span>
|
||||
<input type="date" v-model="to" />
|
||||
<button class="primary" @click="apply" :disabled="loading">→ {{ i18n.t('apply') }}</button>
|
||||
<button @click="clear" :disabled="loading">{{ i18n.t('clear') }}</button>
|
||||
<button class="soft" @click="download"><Download :size="16" :stroke-width="1.8" /> CSV</button>
|
||||
</div>
|
||||
|
||||
<!-- Headline metrics -->
|
||||
@@ -66,8 +67,9 @@
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { BarChart3, Users as UsersIcon, Plus } from 'lucide-vue-next'
|
||||
import { BarChart3, Users as UsersIcon, Plus, Download } from 'lucide-vue-next'
|
||||
import { api } from '../api'
|
||||
import { auth } from '../store/auth'
|
||||
import { i18n } from '../i18n'
|
||||
|
||||
const a = ref({ overall: { total_sessions: 0, wins: 0, losses: 0, close_rate: 0, avg_score: 0 }, trainee_count: 0, hardest_personas: [] })
|
||||
@@ -85,6 +87,21 @@ async function load() {
|
||||
}
|
||||
function apply() { load() }
|
||||
function clear() { from.value = ''; to.value = ''; load() }
|
||||
async function download() {
|
||||
try {
|
||||
const resp = await fetch('/api/analytics/export', { headers: { Authorization: `Bearer ${auth.token}` } })
|
||||
if (!resp.ok) throw new Error('export failed')
|
||||
const blob = await resp.blob()
|
||||
const url = URL.createObjectURL(blob)
|
||||
const aEl = document.createElement('a')
|
||||
aEl.href = url
|
||||
aEl.download = 'sales-trainer-results.csv'
|
||||
aEl.click()
|
||||
URL.revokeObjectURL(url)
|
||||
} catch (e) {
|
||||
alert(e.message)
|
||||
}
|
||||
}
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, nextTick, ref } from 'vue'
|
||||
import { computed, onMounted, nextTick, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { Target, ArrowLeft, Search } from 'lucide-vue-next'
|
||||
import { api } from '../api'
|
||||
@@ -105,11 +105,14 @@ const debrief = ref(null)
|
||||
const sessionId = ref(null)
|
||||
const picked = ref('social')
|
||||
|
||||
const scenarios = [
|
||||
{ id: 'social', emoji: '💬', init: 'customer', label: 'Social Media (แชท)', desc: 'ลูกค้าทักมาหาคุณก่อน — โทนสั้น ทักๆ ตามสไตล์แชท' },
|
||||
{ id: 'f2f_call', emoji: '📞', init: 'seller', label: 'พบหน้า / โทรศัพท์', desc: 'คุณต้องเป็นฝ่ายเปิดการสนทนาเชิงรุกกับลีด (ผู้ฝึกทักก่อน)' },
|
||||
{ id: 'recontact', emoji: '⏳', init: 'customer', label: 'ลูกค้ากลับมาติดต่อ (1-3 เดือน)', desc: 'เคยได้รับข้อมูลไปแล้ว ตอนนี้กลับมาติดต่อ พร้อมตัดสินใจมากขึ้น' },
|
||||
]
|
||||
const scenarios = computed(() => {
|
||||
const en = i18n.locale === 'en'
|
||||
return [
|
||||
{ id: 'social', emoji: '💬', init: 'customer', label: en ? 'Social Media (chat)' : 'Social Media (แชท)', desc: en ? 'The customer messages you first — short, chat-style replies' : 'ลูกค้าทักมาหาคุณก่อน — โทนสั้น ทักๆ ตามสไตล์แชท' },
|
||||
{ id: 'f2f_call', emoji: '📞', init: 'seller', label: en ? 'Face-to-face / Phone' : 'พบหน้า / โทรศัพท์', desc: en ? 'You must proactively open the conversation (seller starts)' : 'คุณต้องเป็นฝ่ายเปิดการสนทนาเชิงรุกกับลีด (ผู้ฝึกทักก่อน)' },
|
||||
{ id: 'recontact', emoji: '⏳', init: 'customer', label: en ? 'Re-contact (1-3 months)' : 'ลูกค้ากลับมาติดต่อ (1-3 เดือน)', desc: en ? 'Customer contacted you before, now returns more ready to decide' : 'เคยได้รับข้อมูลไปแล้ว ตอนนี้กลับมาติดต่อ พร้อมตัดสินใจมากขึ้น' },
|
||||
]
|
||||
})
|
||||
|
||||
function scrollDown() {
|
||||
nextTick(() => {
|
||||
@@ -120,7 +123,7 @@ const thread = ref(null)
|
||||
|
||||
async function begin() {
|
||||
phase.value = 'chat'
|
||||
const res = await api.chatStart(gid, pid, { scenario: picked.value })
|
||||
const res = await api.chatStart(gid, pid, picked.value, i18n.locale)
|
||||
sessionId.value = res.session.id
|
||||
messages.value = res.session.messages || []
|
||||
if (messages.value.length) scrollDown()
|
||||
|
||||
51
frontend/src/views/Guide.vue
Normal file
51
frontend/src/views/Guide.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2><BookOpen :size="22" :stroke-width="1.8" style="vertical-align:-4px" /> {{ i18n.t('guideTitle') }}</h2>
|
||||
<p class="muted">คู่มือสั้นๆ สำหรับพนักงานใหม่ — เข้าใจระบบได้ใน 5 นาที</p>
|
||||
|
||||
<div class="card">
|
||||
<h3 style="margin-top:0"><LayoutDashboard :size="18" :stroke-width="1.8" style="vertical-align:-3px" /> 1. หน้า 3 แท็บ (menu ด้านบน)</h3>
|
||||
<ul style="line-height:1.9">
|
||||
<li><strong>ภาพรวมผู้ดูแล</strong> — เฉพาะ admin ดูสถิติผลการฝึกของทีม + กรองตามช่วงเวลา</li>
|
||||
<li><strong>ภาพรวมผลการฝึก</strong> — ของตัวคุณเอง: ชนะ/แพ้/ยังไม่ได้ฝึก</li>
|
||||
<li><strong>การฝึก</strong> — เลือกสินค้า → เลือกลูกค้า (บุคคลต้นแบบ) → เริ่มแชท</li>
|
||||
<li><strong>การตั้งค่า (⚙︎)</strong> — แก้ชื่อ/อีเมล, เปลี่ยนรหัสผ่าน, เปลี่ยนภาษา</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3 style="margin-top:0"><Target :size="18" :stroke-width="1.8" style="vertical-align:-3px" /> 2. วิธีฝึก (start a session)</h3>
|
||||
<ol style="line-height:1.9;padding-left:20px">
|
||||
<li>เข้าแท็บ <strong>การฝึก</strong> → เลือกสินค้าที่อยากฝึก</li>
|
||||
<li>เลือกลูกค้า (บุคคลต้นแบบ) — ระดับ A ง่ายสุด / C ยากสุด (ดูดาวความยาก)</li>
|
||||
<li>เลือก <strong>สถานการณ์</strong>: โซเชียล (ลูกค้าทักก่อน) / พบหน้า-โทร (คุณทักก่อน) / กลับมาติดต่อ</li>
|
||||
<li>แชทกับลูกค้า — ฝึกปิดการขาย ลูกค้าจะตัดสินใจเองว่าซื้อหรือไม่ซื้อ</li>
|
||||
<li>หลังจบ ดูผล + ข้อเสนอแนะ + ข้อมูลลูกค้าที่ถูกซ่อนไว้</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3 style="margin-top:0"><Sparkles :size="18" :stroke-width="1.8" style="vertical-align:-3px" /> 3. เคล็ดลับ</h3>
|
||||
<ul style="line-height:1.9">
|
||||
<li>ลูกค้าแต่ละคน หรือ "บุคคลต้นแบบ" ฝึกได้ <strong>คนละครั้ง</strong> (one-shot) — ฝึกจนจบแล้วลองคนอื่น</li>
|
||||
<li>ถ้าออกกลางคัน กลับมาได้ คุยต่อจากเดิม (ยังไม่นับว่าจบ)</li>
|
||||
<li>ปิดการขาย = แก้ปัญหาจริงของลูกค้า + รับมือข้อโต้แย้ง + ปิดราคา</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3 style="margin-top:0"><Wrench :size="18" :stroke-width="1.8" style="vertical-align:-3px" /> 4. สำหรับ Admin</h3>
|
||||
<ul style="line-height:1.9">
|
||||
<li>ปุ่ม <strong>เพิ่มสินค้า</strong> (หน้า การฝึก) → สร้างกลุ่มบุคคลต้นแบบใหม่</li>
|
||||
<li>หน้า <strong>จัดการบุคคลต้นแบบ</strong> → แก้ไขรายละเอียดลูกค้าแต่ละคน แล้วเปิดใช้งาน</li>
|
||||
<li>หน้า <strong>ผู้ใช้งาน</strong> → สร้างบัญชีให้ทีม (username + รหัสผ่านเริ่มต้น)</li>
|
||||
<li>ปุ่ม <strong>CSV</strong> (หน้า ภาพรวม) → ดาวน์โหลดผลการฝึกทั้งหมด</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { BookOpen, LayoutDashboard, Target, Sparkles, Wrench } from 'lucide-vue-next'
|
||||
import { i18n } from '../i18n'
|
||||
</script>
|
||||
@@ -2,7 +2,8 @@
|
||||
<div>
|
||||
<div class="row" style="align-items:center;margin-bottom:16px">
|
||||
<h2 style="margin:0"><Target :size="22" :stroke-width="1.8" style="vertical-align:-4px" /> {{ i18n.t('training') }}</h2>
|
||||
<div style="margin-left:auto">
|
||||
<div class="row" style="margin-left:auto;gap:10px">
|
||||
<router-link to="/guide"><button class="soft"><BookOpen :size="18" :stroke-width="1.8" /> คู่มือ</button></router-link>
|
||||
<router-link v-if="auth.isAdmin" to="/admin/new-group">
|
||||
<button class="primary"><Plus :size="18" :stroke-width="2" /> {{ i18n.t('addProduct') }}</button>
|
||||
</router-link>
|
||||
@@ -46,7 +47,7 @@
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { Target, Plus, Settings as SettingsIcon } from 'lucide-vue-next'
|
||||
import { Target, Plus, BookOpen } from 'lucide-vue-next'
|
||||
import { api } from '../api'
|
||||
import { auth } from '../store/auth'
|
||||
import { i18n } from '../i18n'
|
||||
|
||||
Reference in New Issue
Block a user