- 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.
125 lines
6.2 KiB
Vue
125 lines
6.2 KiB
Vue
<template>
|
||
<div>
|
||
<router-link to="/training" class="btn-back">← {{ i18n.t('training') }}</router-link>
|
||
<div class="row" style="align-items:center;margin-bottom:8px">
|
||
<h2 style="margin:0"><Users :size="22" :stroke-width="1.8" style="vertical-align:-4px" /> {{ i18n.t('managePersonas') }}</h2>
|
||
<div class="row" style="margin-left:auto;gap:10px">
|
||
<button @click="analyze" :disabled="busy" class="primary">
|
||
<span v-if="busy" class="spinner"></span><Sparkles v-else :size="18" :stroke-width="1.8" /> {{ busy ? 'กำลังสร้าง…' : 'สร้างบุคคลต้นแบบเพิ่มเติม' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<p class="muted" v-if="group">สินค้า: <strong>{{ group.title }}</strong></p>
|
||
|
||
<div class="error" v-if="error" style="margin:12px 0">{{ error }}</div>
|
||
|
||
<!-- Step guidance -->
|
||
<div class="card guide">
|
||
<strong>วิธีใช้หน้านี้</strong>
|
||
<ol style="margin:8px 0 0;padding-left:20px;line-height:1.8">
|
||
<li>ระบบสร้างบุคคลต้นแบบ (ลูกค้าจำลอง) ให้อัตโนมัติแล้ว หลังกดสร้าง</li>
|
||
<li>แต่ละการ์ดคือลูกค้าจำลอง 1 คน — กด <strong>✏️ แก้ไข</strong> เพื่อปรับรายละเอียดให้ตรงใจ</li>
|
||
<li>อยากได้ลูกค้าจำลองเพิ่ม กด <strong>สร้างบุคคลต้นแบบเพิ่มเติม</strong></li>
|
||
<li><strong>วิธีเริ่มฝึก:</strong> ไปที่แท็บ <strong>การฝึก</strong> → เลือกสินค้านี้ → เลือกลูกค้าจำลอง 1 คน → กด <strong>แชท</strong> → เลือกสถานการณ์ → เริ่มคุย</li>
|
||
</ol>
|
||
</div>
|
||
|
||
<div v-if="personas.length === 0 && !busy" class="card empty-state">
|
||
<strong>ยังไม่มีบุคคลต้นแบบ</strong>
|
||
<span>กด "สร้างบุคคลต้นแบบเพิ่มเติม" เพื่อให้ระบบสร้างลูกค้าจำลองให้</span>
|
||
</div>
|
||
|
||
<!-- Persona list -->
|
||
<div v-for="tier in ['A','B','C']" :key="tier" style="margin-bottom:20px">
|
||
<h4>{{ tierLabel(tier) }}</h4>
|
||
<div class="grid">
|
||
<div v-for="p in byTier(tier)" :key="p.id" class="card pcard">
|
||
<div class="row" style="justify-content:space-between;align-items:center">
|
||
<strong>{{ p.name }}</strong>
|
||
<span class="badge" :class="tierClass(p.tier)">{{ p.tier }}</span>
|
||
</div>
|
||
<div class="diff">
|
||
<span v-for="n in 5" :key="n" class="star" :class="{ on: n <= (p.difficulty || 1) }">★</span>
|
||
</div>
|
||
<div class="muted">{{ p.profession }} · {{ p.age_group }} · {{ p.location }}</div>
|
||
<div class="muted">{{ p.personality }}</div>
|
||
<div class="row" style="gap:8px;margin-top:auto">
|
||
<router-link :to="`/groups/${gid}/chat/${p.id}`" style="flex:1;text-decoration:none">
|
||
<button class="primary" style="width:100%"><MessageSquare :size="16" :stroke-width="1.8" /> {{ i18n.t('chat') }}</button>
|
||
</router-link>
|
||
<button class="edit-btn" @click="openEdit(p)"><Pencil :size="15" :stroke-width="1.8" /> {{ i18n.t('edit') }}</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Edit modal -->
|
||
<div v-if="editing" class="modal-backdrop" @click.self="editing=null">
|
||
<div class="modal">
|
||
<PersonaForm :persona="editing" @save="savePersona" @cancel="editing=null" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { onMounted, ref } from 'vue'
|
||
import { useRoute } from 'vue-router'
|
||
import { Sparkles, Users, Pencil, MessageSquare } from 'lucide-vue-next'
|
||
import { api } from '../api'
|
||
import { i18n } from '../i18n'
|
||
import PersonaForm from '../components/PersonaForm.vue'
|
||
|
||
const route = useRoute()
|
||
const gid = route.params.gid
|
||
const group = ref(null)
|
||
const personas = ref([])
|
||
const busy = ref(false)
|
||
const error = ref('')
|
||
const editing = ref(null)
|
||
|
||
async function load() {
|
||
const data = await api.getGroup(gid)
|
||
group.value = data.group
|
||
personas.value = (await api.listPersonas(gid)).personas
|
||
}
|
||
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 tierClass(t) { return { A: 'tier-a', B: 'tier-b', C: 'tier-c' }[t] || '' }
|
||
async function analyze() {
|
||
busy.value = true; error.value = ''
|
||
try {
|
||
// Append more personas (keeps existing ones), per the 'create more personas' flow.
|
||
const data = await api.analyzeGroup(gid, { append: true })
|
||
personas.value = data.personas
|
||
await load()
|
||
} catch (e) { error.value = e.message }
|
||
finally { busy.value = false }
|
||
}
|
||
function openEdit(p) { editing.value = JSON.parse(JSON.stringify(p)) }
|
||
async function savePersona(payload) {
|
||
busy.value = true
|
||
try {
|
||
await api.updatePersona(gid, editing.value.id, payload)
|
||
editing.value = null
|
||
await load()
|
||
} catch (e) { error.value = e.message }
|
||
finally { busy.value = false }
|
||
}
|
||
onMounted(load)
|
||
</script>
|
||
|
||
<style scoped>
|
||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 14px; }
|
||
.pcard { display: flex; flex-direction: column; }
|
||
.edit-btn { margin-top: auto; display: inline-flex; align-items: center; gap: 6px; }
|
||
.star { color: #d8dbe3; }
|
||
.star.on { color: #f59e0b; }
|
||
.badge.tier-a { background: #dcfce7; color: #166534; }
|
||
.badge.tier-b { background: #fef9c3; color: #854d0e; }
|
||
.badge.tier-c { background: #fee2e2; color: #991b1b; }
|
||
.guide { background: #eef2ff; border-color: #c7d2fe; margin-bottom: 16px; }
|
||
.modal-backdrop { position: fixed; inset: 0; background: rgba(15,23,42,.5); display: flex; justify-content: center; align-items: flex-start; padding: 40px 16px; z-index: 50; overflow: auto; }
|
||
.modal { background: #fff; border-radius: 14px; padding: 24px; width: 100%; max-width: 720px; box-shadow: 0 20px 50px rgba(0,0,0,.25); }
|
||
</style>
|