- 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
157 lines
6.5 KiB
Vue
157 lines
6.5 KiB
Vue
<template>
|
|
<div>
|
|
<router-link to="/my/board" class="btn-back">← {{ i18n.t('myDashboard') }}</router-link>
|
|
<div v-if="loading" class="card"><div class="skeleton" style="height:120px"></div></div>
|
|
<div v-else-if="error" class="card error">{{ error }}</div>
|
|
<template v-else-if="session">
|
|
<div class="row" style="align-items:center;margin-bottom:12px">
|
|
<h2 style="margin:0">{{ session.persona_name || i18n.t('session') }}</h2>
|
|
<span class="badge" :class="statusClass">{{ statusLabel }}</span>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3 style="margin-top:0">{{ i18n.t('mySessions') }}</h3>
|
|
<div class="thread">
|
|
<div
|
|
v-for="(message, index) in session.messages || []"
|
|
:key="index"
|
|
class="bubble"
|
|
:class="message.role === 'seller' ? 'msg-seller' : message.role === 'system' ? 'msg-system' : 'msg-customer'"
|
|
>{{ message.text }}</div>
|
|
<div v-if="!(session.messages || []).length" class="muted">ยังไม่มีข้อความใน session นี้</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="session.debrief" class="card debrief">
|
|
<h3 style="margin-top:0">{{ i18n.t('debrief') }}</h3>
|
|
<p><span class="badge" :class="session.debrief.outcome">{{ outcomeLabel(session.debrief.outcome) }}</span>
|
|
— {{ i18n.t('score') }}: <strong>{{ session.debrief.score }}</strong></p>
|
|
<p><strong>{{ i18n.t('pain') }}:</strong> {{ session.debrief.pain || '—' }}</p>
|
|
<p><strong>{{ i18n.t('why') }}:</strong> {{ session.debrief.why || '—' }}</p>
|
|
<div v-if="(session.debrief.failurePoints || []).length">
|
|
<strong>{{ i18n.t('failurePoints') }}</strong>
|
|
<ul><li v-for="(item, index) in session.debrief.failurePoints" :key="index">{{ item }}</li></ul>
|
|
</div>
|
|
<div v-if="(session.debrief.coaching || []).length">
|
|
<strong>{{ i18n.t('coaching') }}</strong>
|
|
<ul><li v-for="(item, index) in session.debrief.coaching" :key="index">{{ item }}</li></ul>
|
|
</div>
|
|
<details v-if="session.debrief.revealed_persona">
|
|
<summary>{{ i18n.t('reveal') }}</summary>
|
|
<div class="reveal-grid">
|
|
<div v-for="(value, key) in session.debrief.revealed_persona" :key="key" class="rev">
|
|
<span class="rk">{{ fieldLabel(key) }}</span>
|
|
<span class="rv">{{ formatValue(value) }}</span>
|
|
</div>
|
|
</div>
|
|
</details>
|
|
|
|
<button
|
|
v-if="session.group_id && session.persona_id"
|
|
class="soft variant-btn"
|
|
@click="makeVariant"
|
|
:disabled="creating"
|
|
>
|
|
{{ creating ? i18n.t('creating') : i18n.t('createVariant') }}
|
|
</button>
|
|
<div v-if="variantMsg" class="muted" style="margin-top:8px">{{ variantMsg }}</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, onMounted, ref } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { api } from '../api'
|
|
import { i18n } from '../i18n'
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const session = ref(null)
|
|
const loading = ref(true)
|
|
const error = ref('')
|
|
const creating = ref(false)
|
|
const variantMsg = ref('')
|
|
|
|
const statusClass = computed(() => session.value?.status === 'active' ? 'active' : session.value?.outcome || 'not_tried')
|
|
const statusLabel = computed(() => {
|
|
if (!session.value || session.value.status === 'active') return i18n.t('inProgress')
|
|
return outcomeLabel(session.value.outcome)
|
|
})
|
|
|
|
function outcomeLabel(outcome) {
|
|
if (outcome === 'won') return i18n.t('won')
|
|
if (outcome === 'lost') return i18n.t('lost')
|
|
return i18n.t('inProgress')
|
|
}
|
|
function fieldLabel(key) {
|
|
const labels = {
|
|
pains: 'ปัญหา (Pain)', income: 'รายได้', personality: 'บุคลิก', budget: 'งบประมาณ',
|
|
negotiation_levers: 'สิ่งที่ใช้ต่อรอง', opener: 'บทเปิดบทสนทนา', background: 'ภูมิหลัง',
|
|
}
|
|
return labels[key] || key
|
|
}
|
|
function formatValue(value) {
|
|
if (Array.isArray(value)) {
|
|
// Arrays may hold plain strings OR objects (e.g. pains [{title,description}]).
|
|
// Never let join() stringify an object into "[object Object]".
|
|
return value
|
|
.map((item) => {
|
|
if (item && typeof item === 'object') {
|
|
const readable = item.description ?? item.title ?? item.text
|
|
return readable && typeof readable === 'string' && readable.trim()
|
|
? readable.trim()
|
|
: JSON.stringify(item)
|
|
}
|
|
return String(item)
|
|
})
|
|
.join(', ')
|
|
}
|
|
if (value && typeof value === 'object') return JSON.stringify(value)
|
|
return String(value == null ? '—' : value)
|
|
}
|
|
|
|
async function makeVariant() {
|
|
if (!session.value?.group_id || !session.value?.persona_id) return
|
|
creating.value = true
|
|
variantMsg.value = ''
|
|
try {
|
|
const created = await api.createPersonaVariant(session.value.group_id, session.value.persona_id)
|
|
const target = created && typeof created === 'object' && created.group_id
|
|
? created.group_id
|
|
: session.value.group_id
|
|
// Trainee variants land in the private group; admin variants in the shared group.
|
|
await router.push(`/groups/${target}/personas`)
|
|
} catch (e) {
|
|
variantMsg.value = i18n.t('requestFailed')
|
|
} finally {
|
|
creating.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(async () => {
|
|
try {
|
|
session.value = (await api.getSession(route.params.sid)).session
|
|
} catch (e) {
|
|
error.value = i18n.t('requestFailed')
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.thread { background:#eceff4; border:1px solid var(--border); border-radius:var(--radius); padding:16px; min-height:220px; max-height:60vh; overflow-y:auto; display:flex; flex-direction:column; gap:8px; }
|
|
.bubble { max-width:72%; padding:10px 14px; white-space:pre-wrap; word-break:break-word; }
|
|
.msg-system { align-self:center; background:#fef3c7; color:#92400e; font-size:12px; max-width:88%; border-radius:999px; }
|
|
.debrief { margin-top:16px; }
|
|
.variant-btn { margin-top:14px; }
|
|
.reveal-grid { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-top:8px; }
|
|
.rev { display:flex; flex-direction:column; background:#f8fafc; border:1px solid var(--border); border-radius:8px; padding:8px 10px; }
|
|
.rk { font-size:12px; color:var(--muted); }
|
|
.rv { font-size:13px; color:var(--ink); margin-top:2px; }
|
|
.badge.active { background:#e0e7ff; color:#3730a3; }
|
|
@media (max-width:640px) { .reveal-grid { grid-template-columns:1fr; } }
|
|
</style>
|