feat(aihr): add coach-hint style regeneration buttons to practice turn
- /turn accepts optional regenerate+style (serious/gentle/detailed/professional); regenerate short-circuits before rememberTraineeReply — no round advance, no trainee reply write, no scoring impact; only same-round coachHint evidence is overwritten - Style instruction injected into existing LLM turn prompt; LLM missing/failed returns original hint with explicit message (no fake data) - Mobile practice page renders 再严肃/再温柔/更细致/更专业 pills under coach hints; success replaces text with a style note, failure toasts and keeps original; old clients unaffected (fields appended)
This commit is contained in:
+95
-4
@@ -690,6 +690,18 @@
|
||||
<b>{{ practiceTurnDuration(turn.role, index) }}</b>
|
||||
</small>
|
||||
</p>
|
||||
<div v-if="turn.role === 'coach' && turn.roundIndex !== undefined" class="coach-style-actions">
|
||||
<small v-if="turn.styleNote" class="coach-style-note">{{ turn.styleNote }}</small>
|
||||
<button
|
||||
v-for="option in practiceStyleOptions"
|
||||
:key="option.key"
|
||||
type="button"
|
||||
:disabled="Boolean(practiceRegeneratingKey)"
|
||||
@click="regeneratePracticeCoach(index, option)"
|
||||
>
|
||||
{{ practiceRegeneratingKey === `${index}:${option.key}` ? '调整中' : option.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dialogue-memory">
|
||||
@@ -1243,11 +1255,14 @@ interface RoleHome {
|
||||
type PracticeStatus = 'idle' | 'starting' | 'active' | 'submitting' | 'finished';
|
||||
type InterviewStatus = PracticeStatus;
|
||||
type PracticeRole = 'customer' | 'trainee' | 'coach';
|
||||
type PracticeStyleKey = 'serious' | 'gentle' | 'detailed' | 'professional';
|
||||
|
||||
interface PracticeTurn {
|
||||
role: PracticeRole;
|
||||
label: string;
|
||||
text: string;
|
||||
roundIndex?: number;
|
||||
styleNote?: string;
|
||||
}
|
||||
|
||||
interface PracticeStartResponse {
|
||||
@@ -1267,6 +1282,7 @@ interface PracticePrepCardResponse {
|
||||
interface PracticeTurnResponse {
|
||||
customerText: string;
|
||||
coachHint: string;
|
||||
message?: string;
|
||||
emotion?: number;
|
||||
trust?: number;
|
||||
redFlag?: boolean;
|
||||
@@ -1693,10 +1709,17 @@ const practiceRoundLabel = computed(() => Math.max(1, Math.min(practiceRoundInde
|
||||
const practiceEmotion = ref<number | null>(null);
|
||||
const practiceEmotionScore = computed(() => practiceEmotion.value ?? Math.max(45, 76 - practiceRoundIndex.value * 8));
|
||||
const waveBars = [12, 20, 28, 16, 34, 24, 18, 32, 22, 30, 14, 26, 18, 35, 20, 12];
|
||||
const practiceStyleOptions: { key: PracticeStyleKey; label: string }[] = [
|
||||
{ key: 'serious', label: '再严肃' },
|
||||
{ key: 'gentle', label: '再温柔' },
|
||||
{ key: 'detailed', label: '更细致' },
|
||||
{ key: 'professional', label: '更专业' }
|
||||
];
|
||||
const practiceDraft = ref('');
|
||||
const practiceTurns = ref<PracticeTurn[]>([]);
|
||||
const practiceResult = ref<PracticeFinishResponse | null>(null);
|
||||
const practiceMessage = ref('');
|
||||
const practiceRegeneratingKey = ref('');
|
||||
const latestPracticeAudioUrl = ref('');
|
||||
const latestPracticeAudioOssId = ref<number | null>(null);
|
||||
const practicePrepCard = ref<PracticePrepCardResponse | null>(null);
|
||||
@@ -2952,13 +2975,13 @@ const submitMaterial = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const appendPracticeTurn = (role: PracticeRole, text: string) => {
|
||||
const appendPracticeTurn = (role: PracticeRole, text: string, meta: Partial<PracticeTurn> = {}) => {
|
||||
const labels: Record<PracticeRole, string> = {
|
||||
customer: 'AI业主',
|
||||
trainee: '我的回复',
|
||||
coach: 'AI教练'
|
||||
};
|
||||
practiceTurns.value.push({ role, label: labels[role], text });
|
||||
practiceTurns.value.push({ role, label: labels[role], text, ...meta });
|
||||
if (role === 'customer') {
|
||||
void playCustomerVoice(text);
|
||||
}
|
||||
@@ -3017,6 +3040,7 @@ const startMobilePractice = async () => {
|
||||
practiceDraft.value = '';
|
||||
latestPracticeAudioUrl.value = '';
|
||||
latestPracticeAudioOssId.value = null;
|
||||
practiceRegeneratingKey.value = '';
|
||||
practiceTurns.value = [];
|
||||
practiceRoundIndex.value = 0;
|
||||
practiceHelp.value = null;
|
||||
@@ -3062,6 +3086,7 @@ const submitMobilePractice = async () => {
|
||||
const traineeText = practiceDraft.value;
|
||||
const traineeAudioUrl = latestPracticeAudioUrl.value;
|
||||
const traineeAudioOssId = latestPracticeAudioOssId.value;
|
||||
const submittedRoundIndex = practiceRoundIndex.value;
|
||||
practiceDraft.value = '';
|
||||
practiceMessage.value = '';
|
||||
practiceStatus.value = 'submitting';
|
||||
@@ -3071,7 +3096,7 @@ const submitMobilePractice = async () => {
|
||||
await fetch('/dev-api/api/train/practice/turn', {
|
||||
method: 'POST',
|
||||
headers: apiHeaders(),
|
||||
body: JSON.stringify({ sessionId: practiceSessionId.value, roundIndex: practiceRoundIndex.value, traineeText, traineeAudioUrl, traineeAudioOssId })
|
||||
body: JSON.stringify({ sessionId: practiceSessionId.value, roundIndex: submittedRoundIndex, traineeText, traineeAudioUrl, traineeAudioOssId })
|
||||
})
|
||||
);
|
||||
latestPracticeAudioUrl.value = '';
|
||||
@@ -3079,7 +3104,7 @@ const submitMobilePractice = async () => {
|
||||
practiceEmotion.value = data.emotion ?? practiceEmotion.value;
|
||||
const coachHint = data.redFlag ? data.coachHint || '红线纠偏:先承接诉求,再给责任人与反馈节点。' : data.coachHint;
|
||||
if (coachHint) {
|
||||
appendPracticeTurn('coach', coachHint);
|
||||
appendPracticeTurn('coach', coachHint, { roundIndex: submittedRoundIndex });
|
||||
}
|
||||
if (data.finished) {
|
||||
await finishMobilePractice();
|
||||
@@ -3094,6 +3119,41 @@ const submitMobilePractice = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const regeneratePracticeCoach = async (index: number, option: { key: PracticeStyleKey; label: string }) => {
|
||||
const turn = practiceTurns.value[index];
|
||||
if (!turn || turn.role !== 'coach' || turn.roundIndex === undefined || practiceRegeneratingKey.value) return;
|
||||
const key = `${index}:${option.key}`;
|
||||
practiceRegeneratingKey.value = key;
|
||||
practiceMessage.value = '';
|
||||
try {
|
||||
const data = await readApi<PracticeTurnResponse>(
|
||||
await fetch('/dev-api/api/train/practice/turn', {
|
||||
method: 'POST',
|
||||
headers: apiHeaders(),
|
||||
body: JSON.stringify({ sessionId: practiceSessionId.value, roundIndex: turn.roundIndex, regenerate: true, style: option.key })
|
||||
})
|
||||
);
|
||||
if (data.message) {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
const coachHint = data.redFlag ? data.coachHint || '红线纠偏:先承接诉求,再给责任人与反馈节点。' : data.coachHint;
|
||||
if (!coachHint) {
|
||||
throw new Error('教练建议重生成失败');
|
||||
}
|
||||
practiceTurns.value[index] = { ...turn, text: coachHint, styleNote: `已按“${option.label}”调整` };
|
||||
} catch (error) {
|
||||
toastMessage.value = error instanceof Error ? error.message : '教练建议调整失败';
|
||||
if (toastTimer) window.clearTimeout(toastTimer);
|
||||
toastTimer = window.setTimeout(() => {
|
||||
toastMessage.value = '';
|
||||
}, 1800);
|
||||
} finally {
|
||||
if (practiceRegeneratingKey.value === key) {
|
||||
practiceRegeneratingKey.value = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const startCandidateInterview = async () => {
|
||||
activeUserPanel.value = 'tasks';
|
||||
activeTabLabel.value = '面试';
|
||||
@@ -6629,6 +6689,37 @@ p {
|
||||
background: #f3f8ff;
|
||||
}
|
||||
|
||||
.coach-style-actions {
|
||||
grid-column: 2;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 7px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.coach-style-actions button {
|
||||
min-height: 30px;
|
||||
padding: 0 12px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(87, 154, 255, 0.18);
|
||||
background: #ffffff;
|
||||
color: #3769a9;
|
||||
font-size: 12px;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.coach-style-actions button:disabled {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.coach-style-note {
|
||||
width: 100%;
|
||||
color: #4e77ad;
|
||||
font-size: 12px;
|
||||
line-height: 1.25;
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.practice-wave {
|
||||
margin-top: 10px;
|
||||
min-height: 34px;
|
||||
|
||||
Reference in New Issue
Block a user