Files
sales-trainer/frontend/src/views/GroupEdit.vue
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

125 lines
6.2 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>