2026-01-07 16:30:31 +08:00
|
|
|
|
let currentFeature = null;
|
2026-01-08 20:45:40 +08:00
|
|
|
|
let interviewId = localStorage.getItem('interviewId') || null;
|
2026-01-07 16:30:31 +08:00
|
|
|
|
let conversationKey = 'default';
|
|
|
|
|
|
|
|
|
|
|
|
function showFeature(feature) {
|
|
|
|
|
|
document.querySelectorAll('.tool-section').forEach(section => {
|
|
|
|
|
|
section.style.display = 'none';
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
document.querySelectorAll('.feature-card').forEach(card => {
|
|
|
|
|
|
card.style.transform = '';
|
|
|
|
|
|
card.style.boxShadow = '';
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (feature === null) {
|
|
|
|
|
|
currentFeature = null;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const featureCard = document.querySelector(`.feature-card[onclick="showFeature('${feature}')"]`);
|
|
|
|
|
|
if (featureCard) {
|
|
|
|
|
|
featureCard.style.transform = 'translateY(-5px)';
|
|
|
|
|
|
featureCard.style.boxShadow = '0 10px 30px rgba(102, 126, 234, 0.3)';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
currentFeature = feature;
|
|
|
|
|
|
|
|
|
|
|
|
const sectionMap = {
|
|
|
|
|
|
'resume': 'resume-section',
|
|
|
|
|
|
'interview': 'interview-section',
|
|
|
|
|
|
'feedback': 'interview-section'
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const targetSection = sectionMap[feature];
|
|
|
|
|
|
if (targetSection) {
|
|
|
|
|
|
document.getElementById(targetSection).style.display = 'block';
|
|
|
|
|
|
|
2026-01-08 20:45:40 +08:00
|
|
|
|
if (feature === 'resume') {
|
2026-01-07 16:30:31 +08:00
|
|
|
|
document.getElementById('resume-result').style.display = 'none';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function showLoading(show) {
|
|
|
|
|
|
document.getElementById('loading-overlay').style.display = show ? 'flex' : 'none';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function optimizeResume() {
|
|
|
|
|
|
const targetPosition = document.getElementById('target-position').value.trim();
|
|
|
|
|
|
const resumeContent = document.getElementById('resume-content').value.trim();
|
|
|
|
|
|
|
|
|
|
|
|
if (!resumeContent) {
|
|
|
|
|
|
alert('请输入简历内容');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
showLoading(true);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/resume/optimize', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
resume_content: resumeContent,
|
|
|
|
|
|
target_position: targetPosition
|
|
|
|
|
|
})
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.error) {
|
|
|
|
|
|
alert(data.error);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const suggestionsDiv = document.getElementById('resume-suggestions');
|
|
|
|
|
|
suggestionsDiv.innerHTML = formatSuggestions(data.suggestions);
|
|
|
|
|
|
document.getElementById('resume-result').style.display = 'block';
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
alert('优化失败:' + error.message);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
showLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatSuggestions(text) {
|
|
|
|
|
|
if (!text) return '<p>未能生成优化建议,请重试。</p>';
|
|
|
|
|
|
|
|
|
|
|
|
let html = text
|
|
|
|
|
|
.replace(/\n\n/g, '</p><p>')
|
|
|
|
|
|
.replace(/\n/g, '<br>');
|
|
|
|
|
|
|
|
|
|
|
|
return '<p>' + html + '</p>';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function startInterview() {
|
|
|
|
|
|
const jobPosition = document.getElementById('job-position').value.trim();
|
|
|
|
|
|
|
|
|
|
|
|
if (!jobPosition) {
|
|
|
|
|
|
alert('请输入目标岗位');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const difficulty = document.querySelector('input[name="difficulty"]:checked').value;
|
|
|
|
|
|
|
|
|
|
|
|
showLoading(true);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/interview/start', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
job_position: jobPosition,
|
|
|
|
|
|
difficulty: difficulty
|
|
|
|
|
|
})
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.error) {
|
|
|
|
|
|
alert(data.error);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interviewId = data.interview_id;
|
2026-01-08 20:45:40 +08:00
|
|
|
|
localStorage.setItem('interviewId', interviewId);
|
2026-01-07 16:30:31 +08:00
|
|
|
|
|
|
|
|
|
|
document.getElementById('interview-setup').style.display = 'none';
|
|
|
|
|
|
document.getElementById('interview-active').style.display = 'flex';
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById('current-position').textContent = '目标岗位:' + data.job_position;
|
|
|
|
|
|
updatePhaseBadge(data.phase);
|
|
|
|
|
|
|
|
|
|
|
|
const messagesContainer = document.getElementById('interview-messages');
|
|
|
|
|
|
messagesContainer.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
|
|
addInterviewMessage(data.question, 'interviewer');
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
alert('开始面试失败:' + error.message);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
showLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function updatePhaseBadge(phase) {
|
|
|
|
|
|
const phaseMap = {
|
|
|
|
|
|
'intro': '自我介绍',
|
|
|
|
|
|
'professional': '专业能力',
|
|
|
|
|
|
'scenario': '情景假设',
|
|
|
|
|
|
'career': '职业规划',
|
|
|
|
|
|
'closing': '面试结束'
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById('current-phase').textContent = phaseMap[phase] || phase;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function addInterviewMessage(content, type) {
|
|
|
|
|
|
if (!content) return;
|
|
|
|
|
|
|
|
|
|
|
|
const messagesContainer = document.getElementById('interview-messages');
|
|
|
|
|
|
const messageDiv = document.createElement('div');
|
|
|
|
|
|
messageDiv.className = 'message ' + type;
|
|
|
|
|
|
messageDiv.textContent = content;
|
|
|
|
|
|
messagesContainer.appendChild(messageDiv);
|
|
|
|
|
|
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function submitAnswer() {
|
|
|
|
|
|
const answerInput = document.getElementById('answer-input');
|
|
|
|
|
|
const answer = answerInput.value.trim();
|
|
|
|
|
|
|
|
|
|
|
|
if (!answer) {
|
|
|
|
|
|
alert('请输入你的回答');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
addInterviewMessage(answer, 'candidate');
|
|
|
|
|
|
answerInput.value = '';
|
|
|
|
|
|
|
|
|
|
|
|
showLoading(true);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/interview/answer', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
interview_id: interviewId,
|
|
|
|
|
|
answer: answer,
|
|
|
|
|
|
request_feedback: false
|
|
|
|
|
|
})
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.error) {
|
|
|
|
|
|
alert(data.error);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (data.ended) {
|
2026-01-08 20:45:40 +08:00
|
|
|
|
// 面试结束,调用endInterview获取生成的面试反馈
|
|
|
|
|
|
endInterview();
|
2026-01-07 16:30:31 +08:00
|
|
|
|
} else if (data.question) {
|
|
|
|
|
|
addInterviewMessage(data.question, 'interviewer');
|
|
|
|
|
|
updatePhaseBadge(data.phase);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
alert('提交回答失败:' + error.message);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
showLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function requestFeedback() {
|
|
|
|
|
|
const answerInput = document.getElementById('answer-input');
|
|
|
|
|
|
const answer = answerInput.value.trim();
|
|
|
|
|
|
|
|
|
|
|
|
if (!answer) {
|
|
|
|
|
|
alert('请先输入你的回答');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
addInterviewMessage(answer, 'candidate');
|
|
|
|
|
|
answerInput.value = '';
|
|
|
|
|
|
|
|
|
|
|
|
showLoading(true);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/interview/answer', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
interview_id: interviewId,
|
|
|
|
|
|
answer: answer,
|
|
|
|
|
|
request_feedback: true
|
|
|
|
|
|
})
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.error) {
|
|
|
|
|
|
alert(data.error);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (data.feedback) {
|
|
|
|
|
|
addInterviewMessage(data.feedback, 'feedback');
|
2026-01-08 20:45:40 +08:00
|
|
|
|
|
|
|
|
|
|
// 添加返回面试的按钮
|
|
|
|
|
|
const messagesContainer = document.getElementById('interview-messages');
|
|
|
|
|
|
const returnButton = document.createElement('button');
|
|
|
|
|
|
returnButton.className = 'return-interview-btn';
|
|
|
|
|
|
returnButton.textContent = '返回面试';
|
|
|
|
|
|
returnButton.onclick = function() {
|
|
|
|
|
|
// 移除按钮
|
|
|
|
|
|
this.remove();
|
|
|
|
|
|
// 可以在这里添加额外的逻辑,比如重新启用输入框等
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const buttonContainer = document.createElement('div');
|
|
|
|
|
|
buttonContainer.className = 'return-button-container';
|
|
|
|
|
|
buttonContainer.appendChild(returnButton);
|
|
|
|
|
|
messagesContainer.appendChild(buttonContainer);
|
|
|
|
|
|
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
2026-01-07 16:30:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
alert('获取反馈失败:' + error.message);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
showLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function endInterview() {
|
|
|
|
|
|
const messages = [];
|
|
|
|
|
|
document.querySelectorAll('.interview-messages .message').forEach(msg => {
|
|
|
|
|
|
messages.push({
|
|
|
|
|
|
role: msg.classList.contains('interviewer') ? 'assistant' : 'user',
|
|
|
|
|
|
content: msg.textContent
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
showLoading(true);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/interview/feedback', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
interview_id: interviewId,
|
|
|
|
|
|
conversation_history: messages
|
|
|
|
|
|
})
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.error) {
|
|
|
|
|
|
alert(data.error);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
showFinalFeedback(data.feedback);
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
2026-01-08 20:45:40 +08:00
|
|
|
|
console.error('获取反馈失败:', error);
|
|
|
|
|
|
alert('获取反馈失败,请稍后重试');
|
2026-01-07 16:30:31 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
showLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function showFinalFeedback(feedbackData) {
|
|
|
|
|
|
document.getElementById('interview-active').style.display = 'none';
|
|
|
|
|
|
document.getElementById('interview-feedback').style.display = 'block';
|
|
|
|
|
|
|
|
|
|
|
|
const feedbackContent = document.getElementById('feedback-content');
|
|
|
|
|
|
|
|
|
|
|
|
if (typeof feedbackData === 'string') {
|
2026-01-08 20:45:40 +08:00
|
|
|
|
// 将Markdown风格的列表和标题转换为HTML
|
|
|
|
|
|
let formattedFeedback = feedbackData
|
|
|
|
|
|
// 转换标题
|
|
|
|
|
|
.replace(/^###\s+(.*)$/gm, '<h4>$1</h4>')
|
|
|
|
|
|
.replace(/^##\s+(.*)$/gm, '<h3>$1</h3>')
|
|
|
|
|
|
// 转换列表
|
|
|
|
|
|
.replace(/^\*\s+(.*)$/gm, '<li>$1</li>')
|
|
|
|
|
|
.replace(/(?:<li>.*<\/li>\n?)+/gm, '<ul>$&</ul>')
|
|
|
|
|
|
// 转换换行
|
|
|
|
|
|
.replace(/\n\n/g, '</p><p>')
|
|
|
|
|
|
.replace(/\n/g, '<br>');
|
|
|
|
|
|
|
|
|
|
|
|
feedbackContent.innerHTML = `<div class="feedback-container">${formattedFeedback}</div>`;
|
2026-01-07 16:30:31 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
let html = '<h4>面试综合评估</h4>';
|
|
|
|
|
|
feedbackData.forEach(msg => {
|
|
|
|
|
|
html += `<p><strong>${msg.role === 'assistant' ? '面试官' : '你'}:</strong>${msg.content}</p>`;
|
|
|
|
|
|
});
|
|
|
|
|
|
feedbackContent.innerHTML = html;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resetInterview() {
|
|
|
|
|
|
interviewId = null;
|
2026-01-08 20:45:40 +08:00
|
|
|
|
localStorage.removeItem('interviewId');
|
2026-01-07 16:30:31 +08:00
|
|
|
|
document.getElementById('interview-setup').style.display = 'block';
|
|
|
|
|
|
document.getElementById('interview-active').style.display = 'none';
|
|
|
|
|
|
document.getElementById('interview-feedback').style.display = 'none';
|
|
|
|
|
|
document.getElementById('interview-messages').innerHTML = '';
|
|
|
|
|
|
document.getElementById('answer-input').value = '';
|
|
|
|
|
|
document.getElementById('job-position').value = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function sendChatMessage() {
|
|
|
|
|
|
const chatInput = document.getElementById('chat-input');
|
|
|
|
|
|
const message = chatInput.value.trim();
|
|
|
|
|
|
|
|
|
|
|
|
if (!message) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const messagesContainer = document.getElementById('chat-messages');
|
|
|
|
|
|
|
|
|
|
|
|
const userMessageDiv = document.createElement('div');
|
|
|
|
|
|
userMessageDiv.className = 'chat-message user';
|
|
|
|
|
|
userMessageDiv.textContent = message;
|
|
|
|
|
|
messagesContainer.appendChild(userMessageDiv);
|
|
|
|
|
|
|
|
|
|
|
|
chatInput.value = '';
|
|
|
|
|
|
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|
|
|
|
|
|
|
|
|
|
|
showLoading(true);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/chat', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
message: message,
|
|
|
|
|
|
system_type: 'general_assistant',
|
|
|
|
|
|
conversation_key: conversationKey
|
|
|
|
|
|
})
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.error) {
|
|
|
|
|
|
alert(data.error);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const assistantMessageDiv = document.createElement('div');
|
|
|
|
|
|
assistantMessageDiv.className = 'chat-message assistant';
|
|
|
|
|
|
assistantMessageDiv.textContent = data.response;
|
|
|
|
|
|
messagesContainer.appendChild(assistantMessageDiv);
|
|
|
|
|
|
|
|
|
|
|
|
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
alert('发送消息失败:' + error.message);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
showLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
|
|
document.getElementById('chat-input').addEventListener('keypress', function(e) {
|
|
|
|
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
sendChatMessage();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById('answer-input').addEventListener('keypress', function(e) {
|
|
|
|
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
submitAnswer();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|