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:
77
frontend/src/views/GenPersona.vue
Normal file
77
frontend/src/views/GenPersona.vue
Normal 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>
|
||||
Reference in New Issue
Block a user