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:
@@ -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 面试评分已生成,并写入本地记录');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user