首次提交:初始化项目代码
This commit is contained in:
@@ -0,0 +1,482 @@
|
||||
<div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
||||
<div class="border-b border-slate-200 px-6 py-4 bg-slate-50">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="font-semibold text-slate-800 flex items-center gap-2">
|
||||
<i class="fas fa-robot text-orange-500"></i> AI生成题目
|
||||
</h3>
|
||||
<p class="text-sm text-slate-500 mt-1">基于简历或关键词智能生成面试题目</p>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button onclick="goBack()" class="btn btn-secondary btn-sm">
|
||||
<i class="fas fa-arrow-left"></i> 返回
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
||||
<div class="lg:col-span-1">
|
||||
<div class="bg-slate-50 rounded-lg p-4 sticky top-6">
|
||||
<h4 class="font-medium text-slate-700 mb-4 flex items-center gap-2">
|
||||
<i class="fas fa-sliders-h text-primary-500"></i> 生成设置
|
||||
</h4>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">选择简历(可选)</label>
|
||||
<select id="quizResumeId" class="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500 text-sm">
|
||||
<option value="">不选择简历(使用自定义关键词)</option>
|
||||
{{ range .Resumes }}
|
||||
<option value="{{.ID}}">{{.BasicInfo.Name}} - {{.BasicInfo.Title}}</option>
|
||||
{{ end }}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">自定义关键词(可选)</label>
|
||||
<textarea id="quizKeywords" rows="3" class="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500 text-sm resize-none" placeholder="如:Go,MySQL,微服务,Redis 不选简历时必填"></textarea>
|
||||
</div>
|
||||
<button onclick="doGenerateQuestions()" id="generateBtn" class="w-full btn btn-primary">
|
||||
<i class="fas fa-robot"></i> 生成题目
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lg:col-span-3">
|
||||
<div id="questionsContainer" class="hidden">
|
||||
<div class="bg-slate-50 rounded-lg p-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h4 class="font-medium text-slate-700 flex items-center gap-2">
|
||||
<i class="fas fa-question-circle text-primary-500"></i> 请回答以下问题
|
||||
</h4>
|
||||
<span class="text-sm text-slate-500">共 <span id="questionCount">0</span> 题</span>
|
||||
</div>
|
||||
<div id="questionsList" class="space-y-6"></div>
|
||||
<div class="mt-6 flex gap-3">
|
||||
<button onclick="goBack()" class="flex-1 btn btn-secondary">取消</button>
|
||||
<button onclick="submitAnswers()" class="flex-1 btn btn-primary">提交答案</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="emptyState" class="bg-slate-50 rounded-lg p-12 text-center">
|
||||
<div class="w-20 h-20 mx-auto mb-4 rounded-full bg-orange-100 flex items-center justify-center">
|
||||
<i class="fas fa-robot text-orange-500 text-4xl"></i>
|
||||
</div>
|
||||
<h4 class="font-medium text-slate-700 mb-2">开始生成题目</h4>
|
||||
<p class="text-sm text-slate-500">选择简历或输入关键词,点击生成按钮开始</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" id="user-id" value="{{ with .User }}{{.ID}}{{ end }}">
|
||||
<script>
|
||||
var currentQuestions = [];
|
||||
|
||||
function goBack() {
|
||||
window.history.back();
|
||||
}
|
||||
|
||||
function doGenerateQuestions() {
|
||||
const resumeId = document.getElementById('quizResumeId').value;
|
||||
const keywords = document.getElementById('quizKeywords').value.trim();
|
||||
|
||||
if (!resumeId && !keywords) {
|
||||
showToast('请选择简历或输入关键词', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('generateBtn');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 生成中...';
|
||||
|
||||
fetch('/api/ai/agent/generate-questions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
resume_id: resumeId,
|
||||
keywords: keywords,
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-robot"></i> 生成题目';
|
||||
|
||||
const resultData = data.data || data;
|
||||
if (data.success && resultData.questions && resultData.questions.length > 0) {
|
||||
currentQuestions = resultData.questions;
|
||||
renderQuestions(resultData.questions);
|
||||
document.getElementById('emptyState').classList.add('hidden');
|
||||
document.getElementById('questionsContainer').classList.remove('hidden');
|
||||
} else {
|
||||
showToast(data.message || '生成题目失败', 'error');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-robot"></i> 生成题目';
|
||||
showToast('生成题目失败', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function renderQuestions(questions) {
|
||||
const container = document.getElementById('questionsList');
|
||||
const countSpan = document.getElementById('questionCount');
|
||||
container.innerHTML = '';
|
||||
countSpan.textContent = questions.length;
|
||||
|
||||
questions.forEach((q, index) => {
|
||||
const qDiv = document.createElement('div');
|
||||
qDiv.className = 'bg-white p-5 rounded-lg border border-slate-200 shadow-sm';
|
||||
|
||||
let typeLabel = '';
|
||||
let typeColor = '';
|
||||
switch(q.type) {
|
||||
case 'mcq': typeLabel = '选择题'; typeColor = 'bg-blue-100 text-blue-700'; break;
|
||||
case 'fill': typeLabel = '填空题'; typeColor = 'bg-purple-100 text-purple-700'; break;
|
||||
case 'sa': typeLabel = '简答题'; typeColor = 'bg-green-100 text-green-700'; break;
|
||||
case 'algo': typeLabel = '算法题'; typeColor = 'bg-orange-100 text-orange-700'; break;
|
||||
default: typeLabel = '问答题'; typeColor = 'bg-gray-100 text-gray-700';
|
||||
}
|
||||
|
||||
const category = q.category || '综合能力';
|
||||
const difficulty = q.difficulty || '初级';
|
||||
const hints = q.hints || [];
|
||||
|
||||
let hintsHtml = '';
|
||||
if (hints.length > 0) {
|
||||
hintsHtml = '<div class="mt-3 p-3 bg-yellow-50 rounded-lg border border-yellow-200">' +
|
||||
'<div class="flex items-center gap-2 text-sm font-medium text-yellow-700 mb-2">' +
|
||||
'<i class="fas fa-lightbulb"></i> 回答要点提示</div>' +
|
||||
'<ul class="text-xs text-yellow-600 space-y-1">';
|
||||
hints.forEach(h => {
|
||||
hintsHtml += '<li class="flex items-start gap-2"><span class="text-yellow-500">•</span>' + escapeHtml(h) + '</li>';
|
||||
});
|
||||
hintsHtml += '</ul></div>';
|
||||
}
|
||||
|
||||
let optionsHtml = '';
|
||||
if (q.options && q.options.length > 0) {
|
||||
optionsHtml = '<div class="space-y-2 mt-3">';
|
||||
q.options.forEach((opt, optIndex) => {
|
||||
const letter = String.fromCharCode(65 + optIndex);
|
||||
optionsHtml +=
|
||||
'<label class="flex items-center gap-3 p-3 rounded-lg hover:bg-slate-50 cursor-pointer border border-slate-100 transition-colors">' +
|
||||
'<input type="radio" name="q' + index + '" value="' + letter + '" class="w-4 h-4 text-primary-600">' +
|
||||
'<span class="text-sm text-slate-700">' + letter + '. ' + escapeHtml(opt) + '</span>' +
|
||||
'</label>';
|
||||
});
|
||||
optionsHtml += '</div>';
|
||||
} else if (q.type === 'fill') {
|
||||
optionsHtml =
|
||||
'<div class="mt-3">' +
|
||||
'<input type="text" id="ans' + index + '" class="w-full px-4 py-2.5 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500 text-sm" placeholder="请输入答案" autocomplete="off" >' +
|
||||
'</div>';
|
||||
} else {
|
||||
optionsHtml =
|
||||
'<div class="mt-3">' +
|
||||
'<textarea id="ans' + index + '" rows="4" class="w-full px-4 py-2.5 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500 text-sm resize-none" placeholder="请输入您的回答"></textarea>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
qDiv.innerHTML =
|
||||
'<div class="flex flex-wrap items-center justify-between mb-3">' +
|
||||
'<div class="flex flex-wrap items-center gap-2">' +
|
||||
'<span class="px-2.5 py-1 ' + typeColor + ' rounded-full text-xs font-medium">' + typeLabel + '</span>' +
|
||||
'<span class="px-2.5 py-1 ' + getDifficultyColor(difficulty) + ' rounded-full text-xs font-medium">' + difficulty + '</span>' +
|
||||
'<span class="px-2.5 py-1 bg-indigo-100 text-indigo-700 rounded-full text-xs font-medium">' + category + '</span>' +
|
||||
'<span class="text-xs text-slate-400">第 ' + (index + 1) + ' 题 · ' + q.score + ' 分</span>' +
|
||||
'</div>' +
|
||||
'<button onclick="toggleQuestionFavorite(' + index + ')" id="favBtn' + index + '" class="btn btn-xs btn-yellow flex items-center gap-1 hover:bg-yellow-100 transition-colors" title="收藏本题">' +
|
||||
'<i class="fas fa-star"></i> 收藏' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'<p class="text-slate-800 font-medium leading-relaxed">' + escapeHtml(q.text) + '</p>' +
|
||||
optionsHtml +
|
||||
hintsHtml;
|
||||
container.appendChild(qDiv);
|
||||
});
|
||||
}
|
||||
|
||||
var favoriteQuestions = [];
|
||||
|
||||
function toggleQuestionFavorite(index) {
|
||||
const q = currentQuestions[index];
|
||||
const btn = document.getElementById(`favBtn${index}`);
|
||||
|
||||
const idx = favoriteQuestions.indexOf(index);
|
||||
if (idx > -1) {
|
||||
favoriteQuestions.splice(idx, 1);
|
||||
btn.classList.remove('bg-yellow-100', 'text-yellow-700');
|
||||
btn.innerHTML = '<i class="fas fa-star"></i> 收藏';
|
||||
} else {
|
||||
favoriteQuestions.push(index);
|
||||
btn.classList.add('bg-yellow-100', 'text-yellow-700');
|
||||
btn.innerHTML = '<i class="fas fa-star"></i> 已收藏';
|
||||
showToast('题目已收藏', 'success');
|
||||
}
|
||||
}
|
||||
|
||||
function submitAnswers() {
|
||||
const answers = {};
|
||||
const unanswered = [];
|
||||
|
||||
currentQuestions.forEach((q, index) => {
|
||||
let answer = '';
|
||||
if (q.type === 'mcq') {
|
||||
const radios = document.getElementsByName('q' + index);
|
||||
for (let radio of radios) {
|
||||
if (radio.checked) {
|
||||
answer = radio.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const input = document.getElementById('ans' + index);
|
||||
answer = input ? input.value.trim() : '';
|
||||
}
|
||||
|
||||
if (!answer) {
|
||||
unanswered.push(index + 1);
|
||||
}
|
||||
answers['q' + index] = answer;
|
||||
});
|
||||
|
||||
if (unanswered.length > 0) {
|
||||
const unansweredList = unanswered.join('、');
|
||||
showCustomConfirm(
|
||||
'确认提交',
|
||||
`您还有 ${unanswered.length} 道题未作答:第 ${unansweredList} 题。\n\n确定要继续提交吗?未作答的题目将不计分。`,
|
||||
function(confirmed) {
|
||||
if (confirmed) {
|
||||
doSubmit(answers);
|
||||
}
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
doSubmit(answers);
|
||||
}
|
||||
|
||||
function doSubmit(answers) {
|
||||
let score = 0;
|
||||
let correctCount = 0;
|
||||
let unansweredCount = 0;
|
||||
|
||||
currentQuestions.forEach((q, index) => {
|
||||
const userAnswer = answers['q' + index];
|
||||
if (!userAnswer) {
|
||||
unansweredCount++;
|
||||
return;
|
||||
}
|
||||
|
||||
if (isAnswerCorrect(q, userAnswer)) {
|
||||
score += q.score;
|
||||
correctCount++;
|
||||
}
|
||||
});
|
||||
|
||||
const resultData = {
|
||||
questions: currentQuestions,
|
||||
user_answers: answers,
|
||||
score: score,
|
||||
correct_count: correctCount,
|
||||
total_count: currentQuestions.length,
|
||||
unanswered_count: unansweredCount
|
||||
};
|
||||
|
||||
fetch('/api/ai/agent/submit-answers', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(resultData)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
saveQuizRecord(answers, score, correctCount, currentQuestions.length, unansweredCount);
|
||||
} else {
|
||||
showToast(data.error || '提交失败', 'error');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
showToast('提交失败', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
function isAnswerCorrect(question, userAnswer) {
|
||||
const userAns = userAnswer.trim().toLowerCase();
|
||||
const correctAns = String(question.answer).trim().toLowerCase();
|
||||
|
||||
if (question.type === 'mcq') {
|
||||
return userAns === correctAns;
|
||||
}
|
||||
|
||||
if (question.type === 'fill') {
|
||||
return userAns === correctAns;
|
||||
}
|
||||
|
||||
if (question.type === 'sa' || question.type === 'algo') {
|
||||
return userAns.length > 0;
|
||||
}
|
||||
|
||||
return userAns === correctAns;
|
||||
}
|
||||
|
||||
function saveQuizRecord(answers, score, correctCount, totalCount, unansweredCount) {
|
||||
const resumeId = document.getElementById('quizResumeId').value;
|
||||
const keywords = document.getElementById('quizKeywords').value.trim();
|
||||
|
||||
const recordId = 'quiz-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
|
||||
|
||||
const userAnswers = {};
|
||||
currentQuestions.forEach((q, index) => {
|
||||
userAnswers[index] = answers['q' + index] || '';
|
||||
});
|
||||
|
||||
const now = new Date();
|
||||
const title = now.toLocaleString('zh-CN', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
|
||||
const userId = document.getElementById('user-id').value;
|
||||
const recordData = {
|
||||
id: recordId,
|
||||
user_id: userId,
|
||||
resume_id: resumeId,
|
||||
resume_route: resumeId ? '' : 'custom-quiz',
|
||||
title: '答题记录 - ' + title,
|
||||
questions: currentQuestions.map(q => ({
|
||||
type: q.type,
|
||||
text: q.text,
|
||||
options: q.options || [],
|
||||
answer: q.answer,
|
||||
analysis: q.analysis || '',
|
||||
score: q.score,
|
||||
keywords: q.keywords || []
|
||||
})),
|
||||
user_answers: userAnswers,
|
||||
score: score,
|
||||
correct_count: correctCount,
|
||||
total_count: totalCount
|
||||
};
|
||||
|
||||
fetch('/api/quiz/record', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(recordData)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
try {
|
||||
saveFavoriteAndWrongQuestions(resumeId, answers);
|
||||
} catch (e) {
|
||||
console.error('Failed to save favorite/wrong questions:', e);
|
||||
}
|
||||
|
||||
let message = `答题完成!`;
|
||||
if (unansweredCount > 0) {
|
||||
message += `\n得分:${score}分(${correctCount}/${totalCount - unansweredCount}正确,${unansweredCount}题未作答)`;
|
||||
} else {
|
||||
message += `\n得分:${score}分(${correctCount}/${totalCount}正确)`;
|
||||
}
|
||||
showToast(message, 'success');
|
||||
|
||||
setTimeout(() => {
|
||||
const url = new URL(window.location.href);
|
||||
url.pathname = url.pathname.replace('/quiz/generate', '/quiz');
|
||||
url.searchParams.set('record', recordId);
|
||||
window.location.href = url.toString();
|
||||
}, 2000);
|
||||
})
|
||||
.catch(() => {
|
||||
let message = `答题完成!`;
|
||||
if (unansweredCount > 0) {
|
||||
message += `\n得分:${score}分(${correctCount}/${totalCount - unansweredCount}正确,${unansweredCount}题未作答)`;
|
||||
} else {
|
||||
message += `\n得分:${score}分(${correctCount}/${totalCount}正确)`;
|
||||
}
|
||||
showToast(message, 'success');
|
||||
|
||||
setTimeout(() => {
|
||||
const url = new URL(window.location.href);
|
||||
url.pathname = url.pathname.replace('/quiz/generate', '/quiz');
|
||||
url.searchParams.set('record', recordId);
|
||||
window.location.href = url.toString();
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
function saveFavoriteAndWrongQuestions(resumeId, answers) {
|
||||
const userId = document.getElementById('user-id').value;
|
||||
currentQuestions.forEach((q, index) => {
|
||||
const userAnswer = answers['q' + index];
|
||||
const isCorrect = userAnswer && isAnswerCorrect(q, userAnswer);
|
||||
const isFavorite = favoriteQuestions.includes(index);
|
||||
|
||||
if (!isCorrect || isFavorite) {
|
||||
const favData = {
|
||||
id: generateUUID(),
|
||||
user_id: userId,
|
||||
resume_id: resumeId,
|
||||
resume_route: '',
|
||||
question: {
|
||||
type: q.type,
|
||||
text: q.text,
|
||||
options: q.options || [],
|
||||
answer: q.answer,
|
||||
analysis: q.analysis || '',
|
||||
score: q.score,
|
||||
keywords: q.keywords || [],
|
||||
difficulty: q.difficulty || '初级',
|
||||
category: q.category || '综合能力'
|
||||
},
|
||||
user_answer: userAnswer || '',
|
||||
is_correct: isCorrect,
|
||||
is_favorite: isFavorite
|
||||
};
|
||||
|
||||
fetch('/api/favorite', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(favData)
|
||||
}).catch(() => {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function generateUUID() {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
function getDifficultyColor(difficulty) {
|
||||
switch(difficulty) {
|
||||
case '入门': return 'bg-green-100 text-green-700';
|
||||
case '初级': return 'bg-blue-100 text-blue-700';
|
||||
case '中级': return 'bg-yellow-100 text-yellow-700';
|
||||
case '进阶': return 'bg-orange-100 text-orange-700';
|
||||
case '高级': return 'bg-red-100 text-red-700';
|
||||
default: return 'bg-gray-100 text-gray-700';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user