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.
This commit is contained in:
@@ -48,6 +48,7 @@ export const api = {
|
||||
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 || {}),
|
||||
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`),
|
||||
mySessions: () => request('GET', '/api/chat/sessions'),
|
||||
|
||||
@@ -37,9 +37,9 @@
|
||||
<h2 style="margin:0">{{ persona ? persona.name : '...' }}</h2>
|
||||
<span class="badge" :class="persona && persona.channel">{{ persona ? persona.channel : '' }}</span>
|
||||
<span class="muted" v-if="persona">{{ persona.profession }} · {{ persona.age_group }}</span>
|
||||
<button class="danger" style="margin-left:auto" @click="finish" :disabled="messages.length === 0 || phase === 'done'">
|
||||
<span v-if="sending" class="spinner" style="margin-right:4px"></span>{{ i18n.t('finish') }}
|
||||
</button>
|
||||
<span v-if="phase === 'done'" class="badge" :class="debrief && debrief.outcome">
|
||||
{{ debrief && debrief.outcome === 'won' ? i18n.t('won') : i18n.t('lost') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Chat thread -->
|
||||
@@ -102,7 +102,6 @@ const messages = ref([])
|
||||
const text = ref('')
|
||||
const sending = ref(false)
|
||||
const debrief = ref(null)
|
||||
const taskText = ref('')
|
||||
const sessionId = ref(null)
|
||||
const picked = ref('social')
|
||||
|
||||
@@ -123,13 +122,33 @@ async function begin() {
|
||||
phase.value = 'chat'
|
||||
const res = await api.chatStart(gid, pid, { scenario: picked.value })
|
||||
sessionId.value = res.session.id
|
||||
if (res.session.task) taskText.value = res.session.task
|
||||
messages.value = res.session.messages || []
|
||||
if (messages.value.length) scrollDown()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
persona.value = (await api.getPersona(gid, pid)).persona
|
||||
// Resume: if there's an unfinished session for this persona, continue it instead of restarting.
|
||||
try {
|
||||
const r = await api.chatResume(gid, pid)
|
||||
sessionId.value = r.session.id
|
||||
messages.value = r.session.messages || []
|
||||
if (messages.value.length && !r.session.debrief) {
|
||||
phase.value = 'chat'
|
||||
// re-derive which scenario was active
|
||||
const sc = scenarios.find((s) => s.id === r.scenario)
|
||||
if (sc) picked.value = r.scenario
|
||||
// if session has a debrief already, it's done
|
||||
} else if (r.session.debrief) {
|
||||
debrief.value = r.session.debrief
|
||||
messages.value = r.session.messages || []
|
||||
phase.value = 'done'
|
||||
}
|
||||
if (messages.value.length) scrollDown()
|
||||
} catch (e) {
|
||||
// no active session -> show the scenario picker
|
||||
phase.value = 'pick'
|
||||
}
|
||||
})
|
||||
|
||||
async function send() {
|
||||
@@ -139,6 +158,10 @@ async function send() {
|
||||
const res = await api.chatSend(gid, pid, text.value.trim())
|
||||
messages.value = res.messages
|
||||
text.value = ''
|
||||
if (res.finished) {
|
||||
debrief.value = res.debrief || null
|
||||
phase.value = 'done'
|
||||
}
|
||||
scrollDown()
|
||||
} catch (e) {
|
||||
alert(e.message)
|
||||
@@ -147,21 +170,6 @@ async function send() {
|
||||
}
|
||||
}
|
||||
|
||||
async function finish() {
|
||||
if (!confirm(i18n.t('finish') + '?')) return
|
||||
sending.value = true
|
||||
try {
|
||||
const res = await api.chatFinish(gid, pid)
|
||||
debrief.value = res.debrief
|
||||
messages.value = res.session.messages
|
||||
phase.value = 'done'
|
||||
} catch (e) {
|
||||
alert(e.message)
|
||||
} finally {
|
||||
sending.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const FIELD_LABELS = {
|
||||
name: 'ชื่อ', tier: 'ระดับ', difficulty: 'ความยาก', profession: 'อาชีพ',
|
||||
age_group: 'ช่วงอายุ', location: 'พื้นที่', income: 'รายได้', budget: 'งบประมาณ',
|
||||
|
||||
Reference in New Issue
Block a user