feat(aihr): switch interview and case flows to real-model-first pipeline
AI 面试 /start 动态出题、/finish 按真实回答结构化评分(未配模型用本地 Rubric 估分);案例沉淀 /upload 改 multipart 真实语音+ASR 转写, /organize 用真实 transcript 调 chat 模型整理;seed service 重构为 AihrInterviewService/AihrCaseService;前端面试/案例/对练页与移动端、 demo-check、验收文档同步真实链路口径。
This commit is contained in:
@@ -30,6 +30,7 @@ export type CaseUploadResponse = {
|
||||
fileName: string;
|
||||
transcript: string;
|
||||
tags: CaseTag[];
|
||||
source: string;
|
||||
};
|
||||
|
||||
export type CaseOrganizeResponse = {
|
||||
@@ -37,6 +38,7 @@ export type CaseOrganizeResponse = {
|
||||
summary: CaseSummaryItem[];
|
||||
tags: CaseTag[];
|
||||
aiSummary: string;
|
||||
source: string;
|
||||
};
|
||||
|
||||
export type CaseCurateResponse = {
|
||||
@@ -45,7 +47,7 @@ export type CaseCurateResponse = {
|
||||
sampleHint: string;
|
||||
};
|
||||
|
||||
export function uploadCaseAudio(data: { fileName: string; projectExtOrgId: string }): Promise<ApiResult<CaseUploadResponse>> {
|
||||
export function uploadCaseAudio(data: FormData): Promise<ApiResult<CaseUploadResponse>> {
|
||||
return request({
|
||||
url: '/api/knowledge/case/upload',
|
||||
method: 'post',
|
||||
|
||||
@@ -27,6 +27,7 @@ export type InterviewStartResponse = {
|
||||
candidateName: string;
|
||||
positionCode: string;
|
||||
questions: InterviewQuestionResponse[];
|
||||
source: string;
|
||||
};
|
||||
|
||||
export type InterviewAnswerRequest = {
|
||||
@@ -44,6 +45,7 @@ export type InterviewAnswerResponse = {
|
||||
|
||||
export type InterviewFinishRequest = {
|
||||
sessionId: string;
|
||||
answers: Record<string, string>;
|
||||
};
|
||||
|
||||
export type InterviewFinishResponse = {
|
||||
@@ -65,6 +67,7 @@ export type InterviewFinishResponse = {
|
||||
result: string;
|
||||
summary: string;
|
||||
}>;
|
||||
source: string;
|
||||
};
|
||||
|
||||
export function startInterview(data: InterviewStartRequest): Promise<ApiResult<InterviewStartResponse>> {
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
<template>
|
||||
<div class="cases-page">
|
||||
<input ref="audioInput" class="hidden-input" type="file" accept="audio/*,.mp3,.wav,.m4a,.webm,.ogg" @change="handleAudioPicked" />
|
||||
<section class="page-head">
|
||||
<div>
|
||||
<h1>案例语音整理</h1>
|
||||
<p>语音上传或选择样例录音,完成 ASR 转写、AI 结构化整理、送审入库。</p>
|
||||
<p>上传真实语音,完成 ASR 转写、AI 结构化整理、送审入库。</p>
|
||||
</div>
|
||||
<div class="head-actions">
|
||||
<span class="status-pill" :class="apiPill.type">{{ apiPill.label }}</span>
|
||||
<span class="status-pill" :class="flowStatus.type">{{ flowStatus.label }}</span>
|
||||
<el-button @click="router.push('/index')">返回首页</el-button>
|
||||
<el-button type="danger" @click="selectSample">选择样例录音</el-button>
|
||||
<el-button type="danger" @click="audioInput?.click()">上传语音</el-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -19,18 +20,18 @@
|
||||
<el-icon><Microphone /></el-icon>
|
||||
<h2>上传与转写</h2>
|
||||
</div>
|
||||
<div class="upload-box" @click="selectSample">
|
||||
<div class="upload-box" @click="audioInput?.click()">
|
||||
<el-icon><FolderChecked /></el-icon>
|
||||
<strong>{{ sampleSelected ? displayFileName : '拖入语音 / 选择样例录音' }}</strong>
|
||||
<span>{{ sampleSelected ? '已加载样例录音并完成 ASR 转写。' : 'Demo 暂用样例转写文本,ASR 接入后替换。' }}</span>
|
||||
<strong>{{ audioUploaded ? displayFileName : '选择语音文件' }}</strong>
|
||||
<span>{{ audioUploaded ? '真实语音已完成 ASR 转写。' : '支持 mp3/wav/m4a/webm/ogg,需先配置 ASR 模型。' }}</span>
|
||||
</div>
|
||||
<div v-if="sampleSelected" class="transcript-card">
|
||||
<div v-if="audioUploaded" class="transcript-card">
|
||||
<span>ASR 转写结果</span>
|
||||
<p>{{ displayTranscript }}</p>
|
||||
</div>
|
||||
<div v-else class="empty-state">先选择样例录音,页面会展示转写结果和后续整理动作。</div>
|
||||
<div v-else class="empty-state">先上传真实语音,页面会展示转写结果和后续整理动作。</div>
|
||||
<div class="action-row">
|
||||
<el-button type="danger" @click="organizeCase">AI 整理</el-button>
|
||||
<el-button type="danger" :disabled="!audioUploaded" @click="organizeCase">AI 整理</el-button>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
@@ -105,74 +106,59 @@ import { curateKnowledgeCase, organizeKnowledgeCase, uploadCaseAudio, type CaseR
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const seedCase = {
|
||||
fileName: '车库积水投诉_20260702.wav',
|
||||
transcript: '业主反映地下车库长期积水,担心车辆受损。管家先确认车位号与积水时间,安排工程人员排查排水沟,并在业主群同步处理进度。',
|
||||
tags: [
|
||||
{ text: '投诉处理', type: 'pill-success' },
|
||||
{ text: '车库', type: 'pill-primary' },
|
||||
{ text: '高情绪', type: 'pill-warning' }
|
||||
],
|
||||
summary: [
|
||||
{ label: '背景', text: '雨后车库积水,业主担心车辆与安全问题。' },
|
||||
{ label: '处理', text: '确认位置、派工排查、群内同步、次日复盘。' },
|
||||
{ label: '结果', text: '排水沟堵塞已清理,业主确认问题解决。' },
|
||||
{ label: '亮点', text: '响应快、过程透明、主动同步进度。' }
|
||||
]
|
||||
} as const;
|
||||
|
||||
const sampleSelected = ref(false);
|
||||
const audioInput = ref<HTMLInputElement>();
|
||||
const audioUploaded = ref(false);
|
||||
const organized = ref(false);
|
||||
const reviewed = ref(false);
|
||||
const stored = ref(false);
|
||||
const sampleVisible = ref(false);
|
||||
const caseId = ref('');
|
||||
const apiMode = ref<'seed' | 'api' | 'fallback'>('seed');
|
||||
const apiMode = ref<'idle' | 'api' | 'error'>('idle');
|
||||
const apiFileName = ref('');
|
||||
const apiTranscript = ref('');
|
||||
const apiSummary = ref<CaseSummaryItem[] | null>(null);
|
||||
const apiTags = ref<CaseTag[] | null>(null);
|
||||
const apiSummary = ref<CaseSummaryItem[]>([]);
|
||||
const apiTags = ref<CaseTag[]>([]);
|
||||
const apiRecord = ref<CaseRecord | null>(null);
|
||||
const apiSampleHint = ref('');
|
||||
|
||||
const displayFileName = computed(() => apiFileName.value || seedCase.fileName);
|
||||
const displayTranscript = computed(() => apiTranscript.value || seedCase.transcript);
|
||||
const displaySummary = computed(() => apiSummary.value ?? seedCase.summary);
|
||||
const displayTags = computed(() => apiTags.value ?? seedCase.tags);
|
||||
const displayFileName = computed(() => apiFileName.value || '未选择文件');
|
||||
const displayTranscript = computed(() => apiTranscript.value || '暂无转写结果');
|
||||
const displaySummary = computed(() => apiSummary.value);
|
||||
const displayTags = computed(() => apiTags.value);
|
||||
const summary = computed(() => (organized.value ? displaySummary.value : []));
|
||||
|
||||
const apiPill = computed(() => {
|
||||
if (apiMode.value === 'api') return { label: '后端 API seed', type: 'pill-primary' };
|
||||
if (apiMode.value === 'fallback') return { label: 'API 失败回退', type: 'pill-warning' };
|
||||
return { label: '本地 seed 演示', type: 'pill-primary' };
|
||||
if (apiMode.value === 'api') return { label: '真实 ASR/API', type: 'pill-primary' };
|
||||
if (apiMode.value === 'error') return { label: '接口失败', type: 'pill-warning' };
|
||||
return { label: '待上传语音', type: 'pill-primary' };
|
||||
});
|
||||
|
||||
const flowStatus = computed(() => {
|
||||
if (stored.value) return { label: '已完成闭环', type: 'pill-success' };
|
||||
if (reviewed.value) return { label: '已送审待入库', type: 'pill-primary' };
|
||||
if (organized.value) return { label: '已生成案例稿', type: 'pill-warning' };
|
||||
if (sampleSelected.value) return { label: '已完成 ASR', type: 'pill-primary' };
|
||||
return { label: '样片兜底', type: 'pill-warning' };
|
||||
if (audioUploaded.value) return { label: '已完成 ASR', type: 'pill-primary' };
|
||||
return { label: '等待语音', type: 'pill-warning' };
|
||||
});
|
||||
|
||||
const pipeline = computed(() => [
|
||||
{
|
||||
name: '语音上传',
|
||||
count: sampleSelected.value ? '1 条' : '0 条',
|
||||
note: sampleSelected.value ? '样例录音已选择' : '等待上传',
|
||||
state: sampleSelected.value ? 'done' : 'todo'
|
||||
count: audioUploaded.value ? '1 条' : '0 条',
|
||||
note: audioUploaded.value ? '真实音频已上传' : '等待上传',
|
||||
state: audioUploaded.value ? 'done' : 'todo'
|
||||
},
|
||||
{
|
||||
name: 'ASR 转写',
|
||||
count: sampleSelected.value ? '1 条' : '0 条',
|
||||
note: sampleSelected.value ? '本地 seed 转写完成' : '等待语音',
|
||||
state: sampleSelected.value ? 'done' : 'todo'
|
||||
count: audioUploaded.value ? '1 条' : '0 条',
|
||||
note: audioUploaded.value ? '真实 ASR 已转写' : '等待语音',
|
||||
state: audioUploaded.value ? 'done' : 'todo'
|
||||
},
|
||||
{
|
||||
name: 'AI 整理',
|
||||
count: organized.value ? '1 条' : '0 条',
|
||||
note: organized.value ? '结构化案例稿已生成' : '等待整理',
|
||||
state: organized.value ? 'done' : sampleSelected.value ? 'active' : 'todo'
|
||||
state: organized.value ? 'done' : audioUploaded.value ? 'active' : 'todo'
|
||||
},
|
||||
{
|
||||
name: '审核入库',
|
||||
@@ -215,68 +201,64 @@ const baseRecords = [
|
||||
}
|
||||
];
|
||||
|
||||
const seedRecord = {
|
||||
title: '车库积水投诉处理 · seed入库',
|
||||
type: '投诉处理',
|
||||
status: '已入库',
|
||||
time: '07-02 10:50',
|
||||
owner: '培训组',
|
||||
summary: 'ASR 转写后自动整理为案例稿,完成送审与入库,可用于三角色对练复用。'
|
||||
};
|
||||
|
||||
const records = ref([...baseRecords]);
|
||||
|
||||
async function selectSample() {
|
||||
sampleSelected.value = true;
|
||||
async function handleAudioPicked(event: Event) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const file = input.files?.[0];
|
||||
input.value = '';
|
||||
if (!file) return;
|
||||
await uploadAudio(file);
|
||||
}
|
||||
|
||||
async function uploadAudio(file: File) {
|
||||
audioUploaded.value = false;
|
||||
organized.value = false;
|
||||
reviewed.value = false;
|
||||
stored.value = false;
|
||||
sampleVisible.value = false;
|
||||
apiSummary.value = null;
|
||||
apiSummary.value = [];
|
||||
apiRecord.value = null;
|
||||
apiSampleHint.value = '';
|
||||
apiTranscript.value = '';
|
||||
apiTags.value = [];
|
||||
try {
|
||||
const { data } = await uploadCaseAudio({
|
||||
fileName: seedCase.fileName,
|
||||
projectExtOrgId: 'seed-project'
|
||||
});
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
form.append('projectExtOrgId', 'mobile-upload');
|
||||
const { data } = await uploadCaseAudio(form);
|
||||
caseId.value = data.caseId;
|
||||
apiMode.value = 'api';
|
||||
apiFileName.value = data.fileName;
|
||||
apiTranscript.value = data.transcript;
|
||||
apiTags.value = data.tags;
|
||||
ElMessage.success('已通过后端 API 加载样例录音并完成 ASR 转写');
|
||||
audioUploaded.value = true;
|
||||
ElMessage.success('真实语音已完成 ASR 转写');
|
||||
} catch {
|
||||
caseId.value = 'case-garage-water';
|
||||
apiMode.value = 'fallback';
|
||||
apiFileName.value = '';
|
||||
apiTranscript.value = '';
|
||||
apiTags.value = null;
|
||||
ElMessage.warning('后端案例上传 API 不可用,已回退本地 seed 转写');
|
||||
caseId.value = '';
|
||||
apiMode.value = 'error';
|
||||
apiFileName.value = file.name;
|
||||
ElMessage.error('语音转写失败,请检查 ASR 模型配置或音频格式');
|
||||
}
|
||||
}
|
||||
|
||||
async function organizeCase() {
|
||||
if (!sampleSelected.value) {
|
||||
await selectSample();
|
||||
}
|
||||
if (!audioUploaded.value || !caseId.value) return;
|
||||
if (apiMode.value === 'api') {
|
||||
try {
|
||||
const { data } = await organizeKnowledgeCase({ caseId: caseId.value });
|
||||
apiSummary.value = data.summary;
|
||||
apiTags.value = data.tags;
|
||||
organized.value = true;
|
||||
ElMessage.success('结构化案例稿已由后端 API 返回');
|
||||
ElMessage.success(data.source === 'real-llm' ? '结构化案例稿已由真实模型返回' : '结构化案例稿已按真实转写生成');
|
||||
return;
|
||||
} catch {
|
||||
apiMode.value = 'fallback';
|
||||
apiSummary.value = null;
|
||||
apiTags.value = null;
|
||||
ElMessage.warning('后端案例整理 API 不可用,已回退本地 seed 案例稿');
|
||||
apiMode.value = 'error';
|
||||
apiSummary.value = [];
|
||||
apiTags.value = [];
|
||||
ElMessage.error('后端案例整理 API 不可用');
|
||||
}
|
||||
}
|
||||
organized.value = true;
|
||||
ElMessage.success('AI 已生成结构化案例稿');
|
||||
}
|
||||
|
||||
function submitReview() {
|
||||
@@ -295,31 +277,24 @@ async function storeCase() {
|
||||
}
|
||||
reviewed.value = true;
|
||||
stored.value = true;
|
||||
if (apiMode.value === 'api') {
|
||||
try {
|
||||
const { data } = await curateKnowledgeCase({
|
||||
caseId: caseId.value,
|
||||
criteria: '可复用培训案例',
|
||||
limit: 1
|
||||
});
|
||||
apiRecord.value = data.records?.[0] ?? null;
|
||||
apiSampleHint.value = data.sampleHint;
|
||||
if (apiRecord.value && !records.value.some((item) => item.title === apiRecord.value?.title)) {
|
||||
records.value.unshift(apiRecord.value);
|
||||
}
|
||||
ElMessage.success('案例已由后端 API 入库,可用于训练复用');
|
||||
return;
|
||||
} catch {
|
||||
apiMode.value = 'fallback';
|
||||
apiRecord.value = null;
|
||||
apiSampleHint.value = '';
|
||||
ElMessage.warning('后端案例入库 API 不可用,已回退本地 seed 入库');
|
||||
try {
|
||||
const { data } = await curateKnowledgeCase({
|
||||
caseId: caseId.value,
|
||||
criteria: '可复用培训案例',
|
||||
limit: 1
|
||||
});
|
||||
apiRecord.value = data.records?.[0] ?? null;
|
||||
apiSampleHint.value = data.sampleHint;
|
||||
if (apiRecord.value && !records.value.some((item) => item.title === apiRecord.value?.title)) {
|
||||
records.value.unshift(apiRecord.value);
|
||||
}
|
||||
ElMessage.success('案例已由后端 API 入库,可用于训练复用');
|
||||
} catch {
|
||||
apiMode.value = 'error';
|
||||
apiRecord.value = null;
|
||||
apiSampleHint.value = '';
|
||||
ElMessage.error('后端案例入库 API 不可用');
|
||||
}
|
||||
if (!records.value.some((item) => item.title === seedRecord.title)) {
|
||||
records.value.unshift(seedRecord);
|
||||
}
|
||||
ElMessage.success('案例已入库,可用于训练复用');
|
||||
}
|
||||
|
||||
function viewSample() {
|
||||
@@ -336,6 +311,10 @@ function viewSample() {
|
||||
color: #162033;
|
||||
}
|
||||
|
||||
.hidden-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page-head {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<section class="page-head">
|
||||
<div>
|
||||
<h1>AI 面试出题</h1>
|
||||
<p>岗位画像驱动出题、候选人作答、Rubric 评分、面试记录沉淀的本地 seed 演示闭环。</p>
|
||||
<p>岗位画像驱动出题、候选人作答、Rubric 评分、面试记录沉淀。</p>
|
||||
</div>
|
||||
<div class="head-actions">
|
||||
<span class="status-pill" :class="apiPill.className">{{ apiPill.text }}</span>
|
||||
@@ -79,7 +79,7 @@
|
||||
|
||||
<div v-if="!generated" class="empty-state">
|
||||
<strong>等待生成面试题</strong>
|
||||
<p>点击左侧“生成题目”,系统会按当前岗位画像加载 3 道 seed 面试题。</p>
|
||||
<p>点击左侧“生成题目”,系统会按当前岗位画像生成 3 道面试题。</p>
|
||||
<el-button type="danger" :icon="DocumentChecked" @click="generateQuestions">生成题目</el-button>
|
||||
</div>
|
||||
|
||||
@@ -105,21 +105,21 @@
|
||||
<div class="answer-box">
|
||||
<div class="answer-head">
|
||||
<span>候选人回答 · {{ activeQuestion.scenario }}</span>
|
||||
<el-button text type="danger" @click="fillSampleAnswer">填入样例回答</el-button>
|
||||
<el-button text type="danger" @click="fillSampleAnswer">填入参考回答</el-button>
|
||||
</div>
|
||||
<el-input
|
||||
v-model="answers[activeQuestion.id]"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
resize="none"
|
||||
placeholder="输入候选人回答,或点击“填入样例回答”快速推进演示"
|
||||
placeholder="输入候选人真实回答,或点击“填入参考回答”快速推进测试"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="action-row question-actions">
|
||||
<el-button @click="moveQuestion(-1)">上一题</el-button>
|
||||
<el-button @click="moveQuestion(1)">下一题</el-button>
|
||||
<el-button @click="fillAllSampleAnswers">填满 seed 回答</el-button>
|
||||
<el-button @click="fillAllSampleAnswers">填满参考回答</el-button>
|
||||
<el-button @click="saveDraft">保存草稿</el-button>
|
||||
<el-button type="danger" :disabled="!canScore" @click="finishScore">完成评分</el-button>
|
||||
</div>
|
||||
@@ -225,7 +225,7 @@ const candidates: CandidateSeed[] = [
|
||||
id: 'zhang-ming',
|
||||
name: '张明',
|
||||
project: '银城花园 / 住宅',
|
||||
mode: '文本演示,语音待接入',
|
||||
mode: '文本面试',
|
||||
experience: '3 年客服与案场接待',
|
||||
risk: '催缴冲突经验不足',
|
||||
job: {
|
||||
@@ -280,7 +280,7 @@ const candidates: CandidateSeed[] = [
|
||||
id: 'li-na',
|
||||
name: '李娜',
|
||||
project: '铂悦府 / 高端住宅',
|
||||
mode: '文本演示,语音待接入',
|
||||
mode: '文本面试',
|
||||
experience: '2 年呼叫中心经验',
|
||||
risk: '现场协调经验偏弱',
|
||||
job: {
|
||||
@@ -362,13 +362,14 @@ const scored = ref(false);
|
||||
const showRubric = ref(false);
|
||||
const activeQuestionId = ref('');
|
||||
const sessionId = ref('');
|
||||
const apiMode = ref<'seed' | 'api' | 'fallback'>('seed');
|
||||
const apiMode = ref<'idle' | 'api' | 'fallback'>('idle');
|
||||
const apiQuestions = ref<InterviewQuestion[]>([]);
|
||||
const apiScoreResult = ref<CandidateSeed['scoreResult'] | null>(null);
|
||||
const answers = reactive<Record<string, string>>({});
|
||||
const records = ref([...baseRecords]);
|
||||
|
||||
const selectedCandidate = computed(() => candidates.find((candidate) => candidate.id === candidateId.value) ?? candidates[0]);
|
||||
const questions = computed(() => selectedCandidate.value.questions);
|
||||
const questions = computed(() => (apiQuestions.value.length > 0 ? apiQuestions.value : selectedCandidate.value.questions));
|
||||
const activeQuestion = computed(() => questions.value.find((question) => question.id === activeQuestionId.value) ?? questions.value[0]);
|
||||
const scoreResult = computed(() => apiScoreResult.value ?? selectedCandidate.value.scoreResult);
|
||||
|
||||
@@ -385,12 +386,12 @@ const answeredCount = computed(() => questions.value.filter((question) => answer
|
||||
const canScore = computed(() => generated.value && answeredCount.value === questions.value.length);
|
||||
const apiPill = computed(() => {
|
||||
if (apiMode.value === 'api') {
|
||||
return { text: '后端 API seed', className: 'pill-info' };
|
||||
return { text: '真实 API', className: 'pill-info' };
|
||||
}
|
||||
if (apiMode.value === 'fallback') {
|
||||
return { text: 'API 失败回退', className: 'pill-warning' };
|
||||
return { text: '本地 Rubric', className: 'pill-warning' };
|
||||
}
|
||||
return { text: '本地 seed 演示', className: 'pill-info' };
|
||||
return { text: '待调用 API', className: 'pill-info' };
|
||||
});
|
||||
const flowPill = computed(() => {
|
||||
if (!generated.value) {
|
||||
@@ -425,7 +426,8 @@ const resetFlow = () => {
|
||||
showRubric.value = false;
|
||||
activeQuestionId.value = '';
|
||||
sessionId.value = '';
|
||||
apiMode.value = 'seed';
|
||||
apiMode.value = 'idle';
|
||||
apiQuestions.value = [];
|
||||
apiScoreResult.value = null;
|
||||
resetAnswers();
|
||||
records.value = [...baseRecords];
|
||||
@@ -434,23 +436,33 @@ const resetFlow = () => {
|
||||
const generateQuestions = async () => {
|
||||
resetAnswers();
|
||||
scored.value = false;
|
||||
apiQuestions.value = [];
|
||||
apiScoreResult.value = null;
|
||||
records.value = [...baseRecords];
|
||||
try {
|
||||
const { data } = await startInterview({
|
||||
candidateId: selectedCandidate.value.id,
|
||||
positionCode: selectedCandidate.value.job.title,
|
||||
mode: 'seed'
|
||||
mode: 'real'
|
||||
});
|
||||
sessionId.value = data.sessionId;
|
||||
apiMode.value = 'api';
|
||||
activeQuestionId.value = data.questions?.[0]?.questionId ?? questions.value[0].id;
|
||||
ElMessage.success(`已通过后端 API 生成 ${questions.value.length} 道 seed 面试题`);
|
||||
apiQuestions.value = (data.questions || []).map((question) => ({
|
||||
id: question.questionId,
|
||||
seq: question.seq,
|
||||
text: question.questionText,
|
||||
point: question.point,
|
||||
scenario: question.scenario,
|
||||
sampleAnswer: question.sampleAnswer
|
||||
}));
|
||||
activeQuestionId.value = apiQuestions.value[0]?.id ?? questions.value[0].id;
|
||||
ElMessage.success(data.source === 'real-llm' ? '已由真实模型生成面试题' : '模型未配置,已使用本地 Rubric 题库');
|
||||
} catch {
|
||||
sessionId.value = '';
|
||||
apiMode.value = 'fallback';
|
||||
apiQuestions.value = [];
|
||||
activeQuestionId.value = questions.value[0].id;
|
||||
ElMessage.warning('后端 API 不可用,已回退本地 seed 题库');
|
||||
ElMessage.warning('后端 API 不可用,已使用本地 Rubric 题库');
|
||||
}
|
||||
generated.value = true;
|
||||
};
|
||||
@@ -463,7 +475,7 @@ const fillAllSampleAnswers = () => {
|
||||
questions.value.forEach((question) => {
|
||||
answers[question.id] = question.sampleAnswer;
|
||||
});
|
||||
ElMessage.success('已填入全部 seed 样例回答');
|
||||
ElMessage.success('已填入全部参考回答');
|
||||
};
|
||||
|
||||
const moveQuestion = (offset: number) => {
|
||||
@@ -485,11 +497,11 @@ const saveDraft = async () => {
|
||||
questionId: activeQuestion.value.id,
|
||||
answerText
|
||||
});
|
||||
ElMessage.success('回答已提交后端 seed API');
|
||||
ElMessage.success('回答已提交后端 API');
|
||||
return;
|
||||
} catch {
|
||||
apiMode.value = 'fallback';
|
||||
ElMessage.warning('后端暂存 API 不可用,已回退本地草稿');
|
||||
ElMessage.warning('后端暂存 API 不可用,已保留本地草稿');
|
||||
}
|
||||
}
|
||||
ElMessage.success('草稿已暂存到本地演示状态');
|
||||
@@ -519,14 +531,14 @@ const finishScore = async () => {
|
||||
};
|
||||
if (apiMode.value === 'api' && sessionId.value) {
|
||||
try {
|
||||
const { data } = await finishInterview({ sessionId: sessionId.value });
|
||||
const { data } = await finishInterview({ sessionId: sessionId.value, answers: { ...answers } });
|
||||
apiScoreResult.value = mapApiScoreResult(data);
|
||||
nextRecord = data.records?.[0] ?? nextRecord;
|
||||
ElMessage.success('AI 面试评分已由后端 API 返回');
|
||||
ElMessage.success(data.source === 'real-llm' ? 'AI 面试评分已由真实模型返回' : 'AI 面试评分已由本地 Rubric 返回');
|
||||
} catch {
|
||||
apiMode.value = 'fallback';
|
||||
apiScoreResult.value = null;
|
||||
ElMessage.warning('后端评分 API 不可用,已回退本地 seed 评分');
|
||||
ElMessage.warning('后端评分 API 不可用,已回退本地 Rubric 评分');
|
||||
}
|
||||
} else {
|
||||
apiScoreResult.value = null;
|
||||
@@ -534,7 +546,7 @@ const finishScore = async () => {
|
||||
scored.value = true;
|
||||
records.value = [nextRecord, ...baseRecords];
|
||||
if (apiMode.value !== 'api') {
|
||||
ElMessage.success('AI 面试评分已生成,并写入本地演示记录');
|
||||
ElMessage.success('AI 面试评分已生成,并写入本地记录');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
<div class="reply-box">
|
||||
<div class="reply-head">
|
||||
<span>学员下一句</span>
|
||||
<el-button text type="danger" :disabled="finished" @click="fillSeedReply">填入 seed 回复</el-button>
|
||||
<el-button text type="danger" :disabled="finished" @click="fillReferenceReply">填入参考回复</el-button>
|
||||
</div>
|
||||
<el-input
|
||||
v-model="draftReply"
|
||||
@@ -95,7 +95,7 @@
|
||||
:rows="5"
|
||||
resize="none"
|
||||
:disabled="finished"
|
||||
placeholder="输入学员回复,或点击“填入 seed 回复”推进演示"
|
||||
placeholder="输入学员回复,或点击“填入参考回复”推进测试"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -276,7 +276,7 @@ const showGoal = ref(false);
|
||||
const activeRound = ref(0);
|
||||
const draftReply = ref('');
|
||||
const sessionId = ref('');
|
||||
const apiMode = ref<'seed' | 'api' | 'fallback'>('seed');
|
||||
const apiMode = ref<'local' | 'api' | 'fallback'>('local');
|
||||
const apiScores = ref<ScenarioSeed['scores'] | null>(null);
|
||||
const apiTotal = ref<number | null>(null);
|
||||
const apiRewrite = ref('');
|
||||
@@ -306,12 +306,12 @@ const trustScore = computed(() => {
|
||||
});
|
||||
const apiPill = computed(() => {
|
||||
if (apiMode.value === 'api') {
|
||||
return { text: '后端 API seed', className: 'pill-info' };
|
||||
return { text: '真实 API', className: 'pill-info' };
|
||||
}
|
||||
if (apiMode.value === 'fallback') {
|
||||
return { text: 'API 失败回退', className: 'pill-warning' };
|
||||
return { text: '本地剧本', className: 'pill-warning' };
|
||||
}
|
||||
return { text: '本地 seed 演示', className: 'pill-info' };
|
||||
return { text: '待调用 API', className: 'pill-info' };
|
||||
});
|
||||
const flowPill = computed(() => {
|
||||
if (!started.value) {
|
||||
@@ -354,7 +354,7 @@ const resetPractice = () => {
|
||||
activeRound.value = 0;
|
||||
draftReply.value = '';
|
||||
sessionId.value = '';
|
||||
apiMode.value = 'seed';
|
||||
apiMode.value = 'local';
|
||||
apiScores.value = null;
|
||||
apiTotal.value = null;
|
||||
apiRewrite.value = '';
|
||||
@@ -371,23 +371,23 @@ const startPractice = async () => {
|
||||
const { data } = await startPracticeSession({
|
||||
extPartyId: selectedScenario.value.trainee,
|
||||
scenarioId: selectedScenario.value.id,
|
||||
mode: 'seed'
|
||||
mode: 'real'
|
||||
});
|
||||
sessionId.value = data.sessionId;
|
||||
apiMode.value = 'api';
|
||||
apiTrust.value = data.trust;
|
||||
appendTurn('customer', data.customerText || fallbackCustomer);
|
||||
ElMessage.success(`已通过后端 API 进入 ${selectedScenario.value.name} seed 对练`);
|
||||
ElMessage.success(`已通过后端 API 进入 ${selectedScenario.value.name} 对练`);
|
||||
} catch {
|
||||
sessionId.value = '';
|
||||
apiMode.value = 'fallback';
|
||||
appendTurn('customer', fallbackCustomer);
|
||||
ElMessage.warning('后端对练 API 不可用,已回退本地 seed 对练');
|
||||
ElMessage.warning('后端对练 API 不可用,已回退本地剧本');
|
||||
}
|
||||
started.value = true;
|
||||
};
|
||||
|
||||
const fillSeedReply = () => {
|
||||
const fillReferenceReply = () => {
|
||||
draftReply.value = selectedScenario.value.rounds[activeRound.value].sampleReply;
|
||||
};
|
||||
|
||||
@@ -420,7 +420,7 @@ const continueRound = async () => {
|
||||
return;
|
||||
} catch {
|
||||
apiMode.value = 'fallback';
|
||||
ElMessage.warning('后端回合 API 不可用,已回退本地 seed 推进');
|
||||
ElMessage.warning('后端回合 API 不可用,已回退本地剧本推进');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,7 +460,7 @@ const finishScore = async () => {
|
||||
apiTotal.value = null;
|
||||
apiRewrite.value = '';
|
||||
apiSummary.value = '';
|
||||
ElMessage.warning('后端评分 API 不可用,已回退本地 seed 评分');
|
||||
ElMessage.warning('后端评分 API 不可用,已回退本地评分');
|
||||
}
|
||||
} else {
|
||||
apiScores.value = null;
|
||||
|
||||
Reference in New Issue
Block a user