Sales Trainer v0.1: corporate sales-training simulator (Flask+Vue, 15 personas, chat simulator, judge, analytics)

- Auth/roles (no self-reg), admin user provision, JWT
- Analyze: sales kit + initial pain-fit from form/upload
- Persona generator: 15 personas (5/tier) w/ pain variety, negotiation, init mode, channel, latent/revealable, wrong_text special
- Chat simulator: per-mode initiation, one-shot, hidden signals, judge-LLM debrief+coaching
- Trainee loop: win/lose board, weak-areas, user-generated personas
- Admin analytics; EN+TH Vue SPA served by Flask
- Deploy: Dockerfile, docker-compose, README, eng-log + HANDOFF
- Tests (mock LLM): m0/m1/routes/e2e all pass
This commit is contained in:
Macky
2026-08-07 15:31:06 +07:00
commit c3d31c06e2
70 changed files with 6135 additions and 0 deletions

View File

@@ -0,0 +1,77 @@
<template>
<div>
<h2>{{ i18n.t('generatePersona') }}</h2>
<div class="card" style="margin-bottom:16px">
<label>Mode</label>
<div class="row">
<button :class="{ active: mode === 'manual' }" @click="mode = 'manual'"> {{ i18n.t('manual') }}</button>
<button :class="{ active: mode === 'weak-area' }" @click="mode = 'weak-area'">🔒 Weak-area lock</button>
</div>
<template v-if="mode === 'manual'">
<label>Describe the persona you want to practice against</label>
<textarea v-model="spec" rows="4" placeholder="e.g. a price-hardball restaurant owner on LINE who stalls when I bring up costs"></textarea>
</template>
<template v-else>
<p class="muted">The system will analyze your losses and auto-generate a harder persona targeting your weak points.</p>
</template>
<button class="primary" style="margin-top:16px" :disabled="busy || (mode === 'manual' && !spec.trim())" @click="gen">
{{ busy ? '...' : i18n.t('generatePersona') }}
</button>
<div class="error" v-if="error">{{ error }}</div>
</div>
<h3>My personas</h3>
<div class="grid">
<div v-for="p in mine" :key="p.id" class="card pcard">
<strong>{{ p.name }}</strong>
<span class="badge" :class="p.tier">Tier {{ p.tier }}</span>
<div class="muted">{{ p.profession }} · {{ p.age_group }}</div>
<router-link :to="`/groups/${myGid}/chat/${p.id}`" style="margin-top:auto">
<button class="primary" style="width:100%">{{ i18n.t('chat') }}</button>
</router-link>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import { api } from '../api'
import { i18n } from '../i18n'
const route = useRoute()
const mode = ref(route.query.mode === 'weak' ? 'weak-area' : 'manual')
const spec = ref('')
const busy = ref(false)
const error = ref('')
const mine = ref([])
const myGid = ref(null)
async function loadMine() {
const d = await api.myPersonas()
myGid.value = d.group.id
mine.value = d.personas
}
onMounted(loadMine)
async function gen() {
busy.value = true
error.value = ''
try {
const body = mode.value === 'weak-area'
? { mode: 'weak-area', spec: {} }
: { mode: 'manual', spec: { description: spec.value } }
await api.generatePersona(body)
await loadMine()
} catch (e) { error.value = e.message }
finally { busy.value = false }
}
</script>
<style scoped>
button.active { background: var(--accent); color: #fff; border-color: var(--accent); }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 14px; }
.pcard { display: flex; flex-direction: column; min-height: 140px; }
</style>