Spaces:
Paused
Paused
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>AI Interview - Interactive Session</title> | |
<style> | |
* { | |
margin: 0; | |
padding: 0; | |
box-sizing: border-box; | |
} | |
body { | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
min-height: 100vh; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
padding: 20px; | |
} | |
.interview-container { | |
background: white; | |
border-radius: 20px; | |
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.1); | |
width: 100%; | |
max-width: 800px; | |
min-height: 600px; | |
display: flex; | |
flex-direction: column; | |
overflow: hidden; | |
} | |
.header { | |
background: linear-gradient(45deg, #4CAF50, #45a049); | |
color: white; | |
padding: 20px; | |
text-align: center; | |
position: relative; | |
} | |
.header h1 { | |
font-size: 1.8rem; | |
margin-bottom: 5px; | |
} | |
.header p { | |
opacity: 0.9; | |
font-size: 0.9rem; | |
} | |
.chat-area { | |
flex: 1; | |
padding: 20px; | |
display: flex; | |
flex-direction: column; | |
gap: 20px; | |
max-height: 400px; | |
overflow-y: auto; | |
} | |
.ai-message { | |
display: flex; | |
align-items: flex-start; | |
gap: 15px; | |
animation: slideIn 0.5s ease-out; | |
} | |
.ai-avatar { | |
width: 60px; | |
height: 60px; | |
border-radius: 50%; | |
background: linear-gradient(45deg, #667eea, #764ba2); | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
color: white; | |
font-weight: bold; | |
font-size: 1.2rem; | |
position: relative; | |
flex-shrink: 0; | |
} | |
.ai-avatar.talking { | |
animation: pulse 1.5s infinite; | |
} | |
.ai-avatar.talking::before { | |
content: ''; | |
position: absolute; | |
width: 100%; | |
height: 100%; | |
border-radius: 50%; | |
background: rgba(102, 126, 234, 0.3); | |
animation: ripple 1.5s infinite; | |
} | |
.message-bubble { | |
background: #f8f9fa; | |
border-radius: 15px; | |
padding: 15px 20px; | |
max-width: 70%; | |
position: relative; | |
} | |
.message-bubble::before { | |
content: ''; | |
position: absolute; | |
left: -10px; | |
top: 15px; | |
width: 0; | |
height: 0; | |
border-top: 10px solid transparent; | |
border-bottom: 10px solid transparent; | |
border-right: 10px solid #f8f9fa; | |
} | |
.user-message { | |
display: flex; | |
justify-content: flex-end; | |
animation: slideIn 0.5s ease-out; | |
} | |
.user-bubble { | |
background: linear-gradient(45deg, #4CAF50, #45a049); | |
color: white; | |
border-radius: 15px; | |
padding: 15px 20px; | |
max-width: 70%; | |
position: relative; | |
} | |
.user-bubble::before { | |
content: ''; | |
position: absolute; | |
right: -10px; | |
top: 15px; | |
width: 0; | |
height: 0; | |
border-top: 10px solid transparent; | |
border-bottom: 10px solid transparent; | |
border-left: 10px solid #4CAF50; | |
} | |
.controls-area { | |
background: #f8f9fa; | |
padding: 20px; | |
border-top: 1px solid #e9ecef; | |
} | |
.recording-section { | |
display: flex; | |
flex-direction: column; | |
gap: 15px; | |
} | |
.mic-container { | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
gap: 20px; | |
} | |
.mic-button { | |
width: 80px; | |
height: 80px; | |
border-radius: 50%; | |
border: none; | |
background: linear-gradient(45deg, #ff6b6b, #ee5a52); | |
color: white; | |
font-size: 1.8rem; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
position: relative; | |
} | |
.mic-button:hover { | |
transform: scale(1.05); | |
box-shadow: 0 8px 25px rgba(255, 107, 107, 0.3); | |
} | |
.mic-button.recording { | |
background: linear-gradient(45deg, #ff4757, #ff3742); | |
animation: recordPulse 1s infinite; | |
} | |
.mic-button.recording::before { | |
content: ''; | |
position: absolute; | |
width: 100%; | |
height: 100%; | |
border-radius: 50%; | |
background: rgba(255, 71, 87, 0.3); | |
animation: ripple 1s infinite; | |
} | |
.recording-status { | |
font-size: 0.9rem; | |
color: #666; | |
text-align: center; | |
} | |
.transcript-area { | |
background: white; | |
border: 2px solid #e9ecef; | |
border-radius: 10px; | |
padding: 15px; | |
font-size: 0.9rem; | |
color: #333; | |
min-height: 60px; | |
max-height: 120px; | |
overflow-y: auto; | |
transition: border-color 0.3s ease; | |
} | |
.transcript-area:focus { | |
outline: none; | |
border-color: #4CAF50; | |
} | |
.action-buttons { | |
display: flex; | |
gap: 10px; | |
justify-content: center; | |
margin-top: 15px; | |
} | |
.btn { | |
padding: 12px 24px; | |
border: none; | |
border-radius: 8px; | |
cursor: pointer; | |
font-size: 1rem; | |
font-weight: 500; | |
transition: all 0.3s ease; | |
display: flex; | |
align-items: center; | |
gap: 8px; | |
} | |
.btn-primary { | |
background: linear-gradient(45deg, #4CAF50, #45a049); | |
color: white; | |
} | |
.btn-primary:hover { | |
transform: translateY(-2px); | |
box-shadow: 0 5px 15px rgba(76, 175, 80, 0.3); | |
} | |
.btn-secondary { | |
background: #6c757d; | |
color: white; | |
} | |
.btn-secondary:hover { | |
background: #545b62; | |
transform: translateY(-2px); | |
} | |
.btn:disabled { | |
opacity: 0.6; | |
cursor: not-allowed; | |
transform: none; | |
} | |
.question-counter { | |
position: absolute; | |
top: 15px; | |
right: 20px; | |
background: rgba(255, 255, 255, 0.2); | |
padding: 5px 12px; | |
border-radius: 20px; | |
font-size: 0.8rem; | |
} | |
.loading { | |
display: inline-block; | |
width: 20px; | |
height: 20px; | |
border: 3px solid rgba(255, 255, 255, 0.3); | |
border-radius: 50%; | |
border-top-color: white; | |
animation: spin 1s ease-in-out infinite; | |
} | |
.summary-panel { | |
display: none; | |
background: white; | |
border-radius: 15px; | |
padding: 30px; | |
margin: 20px; | |
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); | |
} | |
.summary-panel h2 { | |
color: #333; | |
margin-bottom: 20px; | |
text-align: center; | |
} | |
.summary-item { | |
background: #f8f9fa; | |
border-radius: 10px; | |
padding: 15px; | |
margin-bottom: 15px; | |
border-left: 4px solid #4CAF50; | |
} | |
.summary-item h4 { | |
color: #333; | |
margin-bottom: 8px; | |
} | |
.summary-item p { | |
color: #666; | |
margin-bottom: 5px; | |
line-height: 1.4; | |
} | |
.evaluation-score { | |
display: inline-block; | |
background: #4CAF50; | |
color: white; | |
padding: 4px 8px; | |
border-radius: 15px; | |
font-size: 0.8rem; | |
font-weight: bold; | |
} | |
.error-message { | |
background: #ff4757; | |
color: white; | |
padding: 10px; | |
border-radius: 5px; | |
margin: 10px 0; | |
text-align: center; | |
} | |
@keyframes slideIn { | |
from { | |
opacity: 0; | |
transform: translateY(20px); | |
} | |
to { | |
opacity: 1; | |
transform: translateY(0); | |
} | |
} | |
@keyframes pulse { | |
0%, | |
100% { | |
transform: scale(1); | |
} | |
50% { | |
transform: scale(1.1); | |
} | |
} | |
@keyframes ripple { | |
0% { | |
transform: scale(1); | |
opacity: 1; | |
} | |
100% { | |
transform: scale(1.4); | |
opacity: 0; | |
} | |
} | |
@keyframes recordPulse { | |
0%, | |
100% { | |
transform: scale(1); | |
} | |
50% { | |
transform: scale(1.05); | |
} | |
} | |
@keyframes spin { | |
to { | |
transform: rotate(360deg); | |
} | |
} | |
@media (max-width: 768px) { | |
.interview-container { | |
margin: 10px; | |
min-height: 90vh; | |
} | |
.mic-button { | |
width: 70px; | |
height: 70px; | |
font-size: 1.5rem; | |
} | |
.message-bubble, | |
.user-bubble { | |
max-width: 85%; | |
} | |
.header h1 { | |
font-size: 1.5rem; | |
} | |
} | |
audio { | |
display: none; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="interview-container"> | |
<div class="header"> | |
<div class="question-counter"> | |
Question <span id="currentQuestionNum">1</span> of <span id="totalQuestions">{{ job.num_questions }}</span> | |
</div> | |
<h1>🤖 AI Interview Assistant</h1> | |
<p>Answer thoughtfully and take your time</p> | |
</div> | |
<div class="chat-area" id="chatArea"> | |
<div class="ai-message"> | |
<div class="ai-avatar" id="aiAvatar">AI</div> | |
<div class="message-bubble"> | |
<div id="loadingMessage"> | |
<div class="loading"></div> | |
<span style="margin-left: 10px;">Generating your first question...</span> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="controls-area"> | |
<div class="recording-section"> | |
<div class="mic-container"> | |
<button class="mic-button" id="micButton" disabled> | |
<span id="micIcon">🎤</span> | |
</button> | |
</div> | |
<div class="recording-status" id="recordingStatus"> | |
Click the microphone to record your answer | |
</div> | |
<div class="transcript-area" id="transcriptArea" contenteditable="true" | |
placeholder="Your transcribed answer will appear here..."></div> | |
<div class="action-buttons"> | |
<button class="btn btn-primary" id="confirmButton" disabled> | |
<span>Confirm Answer</span> | |
<span id="confirmLoading" style="display: none;"> | |
<div class="loading" style="width: 16px; height: 16px; border-width: 2px;"></div> | |
</span> | |
</button> | |
<button class="btn btn-secondary" id="retryRecording" style="display: none;"> | |
🔄 Re-record | |
</button> | |
</div> | |
</div> | |
</div> | |
<!-- Summary Panel (hidden initially) --> | |
<div class="summary-panel" id="summaryPanel"> | |
<h2>📋 Interview Summary</h2> | |
<div id="summaryContent"></div> | |
<div style="text-align: center; margin-top: 30px;"> | |
<button class="btn btn-primary" onclick="window.location.href='/jobs'"> | |
Back to Jobs | |
</button> | |
</div> | |
</div> | |
</div> | |
<!-- Hidden audio element for TTS playback --> | |
<audio id="ttsAudio" preload="auto"></audio> | |
<script> | |
const JOB_ID = {{ job.id }}; | |
class AIInterviewer { | |
constructor() { | |
this.currentQuestionIndex = 0; | |
// Set the total number of questions based on the job configuration. | |
// The value is read from the DOM element #totalQuestions, which | |
// has been populated server‑side using ``job.num_questions``. | |
const totalSpan = document.getElementById('totalQuestions'); | |
const parsedTotal = parseInt(totalSpan && totalSpan.textContent); | |
this.totalQuestions = Number.isNaN(parsedTotal) || parsedTotal <= 0 ? 4 : parsedTotal; | |
// Ensure the counter display shows the correct total number of | |
// questions. Without this update the span would retain the | |
// server‑rendered value even if the fallback above changed | |
// ``this.totalQuestions``. | |
if (totalSpan) { | |
totalSpan.textContent = this.totalQuestions; | |
} | |
this.isRecording = false; | |
this.mediaRecorder = null; | |
this.audioChunks = []; | |
this.currentQuestion = ""; | |
this.interviewData = { | |
questions: [], | |
answers: [], | |
evaluations: [] | |
}; | |
this.initializeElements(); | |
this.initializeInterview(); | |
} | |
initializeElements() { | |
this.chatArea = document.getElementById('chatArea'); | |
this.micButton = document.getElementById('micButton'); | |
this.micIcon = document.getElementById('micIcon'); | |
this.recordingStatus = document.getElementById('recordingStatus'); | |
this.transcriptArea = document.getElementById('transcriptArea'); | |
this.confirmButton = document.getElementById('confirmButton'); | |
this.confirmLoading = document.getElementById('confirmLoading'); | |
this.retryButton = document.getElementById('retryRecording'); | |
this.aiAvatar = document.getElementById('aiAvatar'); | |
this.ttsAudio = document.getElementById('ttsAudio'); | |
this.summaryPanel = document.getElementById('summaryPanel'); | |
this.currentQuestionNum = document.getElementById('currentQuestionNum'); | |
this.totalQuestionsSpan = document.getElementById('totalQuestions'); | |
this.bindEvents(); | |
} | |
bindEvents() { | |
// Mouse events for desktop | |
this.micButton.addEventListener('mousedown', (e) => { | |
e.preventDefault(); | |
this.startRecording(); | |
}); | |
this.micButton.addEventListener('mouseup', (e) => { | |
e.preventDefault(); | |
this.stopRecording(); | |
}); | |
this.micButton.addEventListener('mouseleave', (e) => { | |
e.preventDefault(); | |
this.stopRecording(); | |
}); | |
// Touch events for mobile | |
this.micButton.addEventListener('touchstart', (e) => { | |
e.preventDefault(); | |
this.startRecording(); | |
}); | |
this.micButton.addEventListener('touchend', (e) => { | |
e.preventDefault(); | |
this.stopRecording(); | |
}); | |
this.confirmButton.addEventListener('click', () => this.submitAnswer()); | |
this.retryButton.addEventListener('click', () => this.resetRecording()); | |
this.transcriptArea.addEventListener('input', () => { | |
const hasText = this.transcriptArea.textContent.trim().length > 0; | |
this.confirmButton.disabled = !hasText; | |
}); | |
this.ttsAudio.addEventListener('play', () => { | |
this.aiAvatar.classList.add('talking'); | |
}); | |
this.ttsAudio.addEventListener('ended', () => { | |
this.aiAvatar.classList.remove('talking'); | |
this.enableControls(); | |
}); | |
this.ttsAudio.addEventListener('error', (e) => { | |
console.error('Audio playback error:', e); | |
this.aiAvatar.classList.remove('talking'); | |
this.enableControls(); | |
}); | |
} | |
async initializeInterview() { | |
try { | |
console.log('Starting interview...'); | |
const response = await fetch('/api/start_interview', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json' | |
}, | |
body: JSON.stringify({ job_id: JOB_ID }) | |
}); | |
if (!response.ok) { | |
const errorText = await response.text(); | |
console.error('Server response:', response.status, errorText); | |
throw new Error(`HTTP error! status: ${response.status}`); | |
} | |
const data = await response.json(); | |
console.log('Received interview data:', data); | |
if (data.error) { | |
this.showError(data.error); | |
return; | |
} | |
// Store the current question for evaluation | |
this.currentQuestion = data.question; | |
this.displayQuestion(data.question, data.audio_url); | |
this.interviewData.questions.push(data.question); | |
} catch (error) { | |
console.error('Error starting interview:', error); | |
this.showError('Failed to start interview. Please check your connection and try again.'); | |
} | |
} | |
displayQuestion(question, audioUrl = null) { | |
// Remove loading message | |
const loadingMsg = document.getElementById('loadingMessage'); | |
if (loadingMsg) { | |
loadingMsg.remove(); | |
} | |
// Create question message | |
const messageDiv = document.createElement('div'); | |
messageDiv.className = 'ai-message'; | |
messageDiv.innerHTML = ` | |
<div class="ai-avatar">AI</div> | |
<div class="message-bubble"> | |
<p>${question}</p> | |
</div> | |
`; | |
this.chatArea.appendChild(messageDiv); | |
this.chatArea.scrollTop = this.chatArea.scrollHeight; | |
// Update question counter | |
this.currentQuestionNum.textContent = this.currentQuestionIndex + 1; | |
// Play audio if available | |
if (audioUrl) { | |
console.log('Playing audio:', audioUrl); | |
this.playQuestionAudio(audioUrl); | |
} else { | |
console.log('No audio URL provided, enabling controls'); | |
setTimeout(() => this.enableControls(), 1000); | |
} | |
} | |
playQuestionAudio(audioUrl) { | |
// Add talking animation immediately | |
const avatars = this.chatArea.querySelectorAll('.ai-avatar'); | |
avatars.forEach(avatar => avatar.classList.add('talking')); | |
this.ttsAudio.src = audioUrl; | |
this.ttsAudio.load(); // Ensure audio is loaded | |
this.ttsAudio.play().catch(error => { | |
console.error('Audio play error:', error); | |
avatars.forEach(avatar => avatar.classList.remove('talking')); | |
this.enableControls(); | |
}); | |
} | |
enableControls() { | |
this.micButton.disabled = false; | |
this.recordingStatus.textContent = 'Click and hold to record your answer'; | |
// Remove talking animation from all avatars | |
const avatars = this.chatArea.querySelectorAll('.ai-avatar'); | |
avatars.forEach(avatar => avatar.classList.remove('talking')); | |
} | |
async startRecording() { | |
if (this.isRecording || this.micButton.disabled) return; | |
try { | |
console.log('Starting recording...'); | |
const stream = await navigator.mediaDevices.getUserMedia({ | |
audio: { | |
echoCancellation: true, | |
noiseSuppression: true, | |
autoGainControl: true, | |
sampleRate: 16000 | |
} | |
}); | |
// Use webm format with opus codec for better compatibility | |
const options = { | |
mimeType: 'audio/webm;codecs=opus' | |
}; | |
// Fallback for browsers that don't support webm | |
if (!MediaRecorder.isTypeSupported(options.mimeType)) { | |
options.mimeType = 'audio/webm'; | |
} | |
if (!MediaRecorder.isTypeSupported(options.mimeType)) { | |
delete options.mimeType; | |
} | |
this.mediaRecorder = new MediaRecorder(stream, { | |
mimeType: 'audio/webm;codecs=opus' | |
}); | |
this.audioChunks = []; | |
this.mediaRecorder.ondataavailable = (event) => { | |
if (event.data.size > 0) { | |
this.audioChunks.push(event.data); | |
console.log('Audio chunk received:', event.data.size, 'bytes'); | |
} | |
}; | |
this.mediaRecorder.onstop = () => { | |
console.log('Recording stopped, processing...'); | |
stream.getTracks().forEach(track => track.stop()); | |
this.processRecording(); | |
}; | |
this.mediaRecorder.onerror = (event) => { | |
console.error('MediaRecorder error:', event.error); | |
this.recordingStatus.textContent = 'Recording error. Please try again.'; | |
stream.getTracks().forEach(track => track.stop()); | |
}; | |
this.mediaRecorder.start(1000); // Collect data every second | |
this.isRecording = true; | |
// Update UI | |
this.micButton.classList.add('recording'); | |
this.micIcon.textContent = '🔴'; | |
this.recordingStatus.textContent = 'Recording... Release to stop'; | |
} catch (error) { | |
console.error('Error starting recording:', error); | |
this.recordingStatus.textContent = 'Microphone access denied. Please allow microphone access and try again.'; | |
this.recordingStatus.style.color = '#ff4757'; | |
} | |
} | |
stopRecording() { | |
if (!this.isRecording || !this.mediaRecorder) return; | |
console.log('Stopping recording...'); | |
this.mediaRecorder.stop(); | |
this.isRecording = false; | |
// Update UI | |
this.micButton.classList.remove('recording'); | |
this.micIcon.textContent = '🎤'; | |
this.recordingStatus.textContent = 'Processing audio...'; | |
this.recordingStatus.style.color = '#666'; | |
} | |
async processRecording() { | |
try { | |
if (this.audioChunks.length === 0) { | |
console.error('No audio chunks recorded'); | |
this.recordingStatus.textContent = 'No audio recorded. Please try again.'; | |
return; | |
} | |
console.log('Processing', this.audioChunks.length, 'audio chunks'); | |
// Create blob from audio chunks | |
const audioBlob = new Blob(this.audioChunks, { type: 'audio/webm' }); | |
// Initialise a new FormData instance AFTER creating the blob | |
// and append the audio. Previously a call to `formData.append` was | |
// made before the variable was declared, causing a reference | |
// error and triggering the generic "Error processing audio" message. | |
const formData = new FormData(); | |
formData.append('audio', audioBlob, 'recording.webm'); | |
console.log('Created audio blob:', audioBlob.size, 'bytes'); | |
if (audioBlob.size === 0) { | |
console.error('Audio blob is empty'); | |
this.recordingStatus.textContent = 'No audio data captured. Please try again.'; | |
return; | |
} | |
// formData has already been created and the audio appended above. | |
// Do not recreate or re-append here. | |
console.log('Sending audio for transcription...'); | |
const response = await fetch('/api/transcribe_audio', { | |
method: 'POST', | |
body: formData | |
}); | |
if (!response.ok) { | |
const errorText = await response.text(); | |
console.error('Transcription error:', response.status, errorText); | |
throw new Error(`HTTP error! status: ${response.status}`); | |
} | |
const data = await response.json(); | |
console.log('Transcription response:', data); | |
if (data.error) { | |
this.recordingStatus.textContent = data.error; | |
this.recordingStatus.style.color = '#ff4757'; | |
return; | |
} | |
if (data.transcript && data.transcript.trim()) { | |
this.transcriptArea.textContent = data.transcript; | |
this.confirmButton.disabled = false; | |
this.retryButton.style.display = 'inline-flex'; | |
this.recordingStatus.textContent = 'Transcription complete. Review and confirm your answer.'; | |
this.recordingStatus.style.color = '#4CAF50'; | |
} else { | |
this.recordingStatus.textContent = 'No speech detected. Please try recording again.'; | |
this.recordingStatus.style.color = '#ff4757'; | |
} | |
} catch (error) { | |
console.error('Error processing recording:', error); | |
this.recordingStatus.textContent = 'Error processing audio. Please try again.'; | |
this.recordingStatus.style.color = '#ff4757'; | |
} | |
} | |
resetRecording() { | |
this.transcriptArea.textContent = ''; | |
this.confirmButton.disabled = true; | |
this.retryButton.style.display = 'none'; | |
this.recordingStatus.textContent = 'Click and hold to record your answer'; | |
this.recordingStatus.style.color = '#666'; | |
} | |
async submitAnswer() { | |
const answer = this.transcriptArea.textContent.trim(); | |
if (!answer) return; | |
console.log('Submitting answer:', answer); | |
// Show loading state | |
this.confirmButton.disabled = true; | |
this.confirmLoading.style.display = 'inline-block'; | |
this.confirmButton.querySelector('span').style.display = 'none'; | |
// Add user message to chat | |
this.addUserMessage(answer); | |
try { | |
const response = await fetch('/api/process_answer', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json' | |
}, | |
body: JSON.stringify({ | |
answer: answer, | |
questionIndex: this.currentQuestionIndex, | |
current_question: this.currentQuestion, | |
job_id: JOB_ID | |
}) | |
}); | |
if (!response.ok) { | |
const errorText = await response.text(); | |
console.error('Process answer error:', response.status, errorText); | |
throw new Error(`HTTP error! status: ${response.status}`); | |
} | |
const data = await response.json(); | |
console.log('Process answer response:', data); | |
if (!data.success) { | |
this.showError(data.error || 'Failed to process answer. Please try again.'); | |
return; | |
} | |
// Record the user's answer and its evaluation | |
this.interviewData.answers.push(answer); | |
this.interviewData.evaluations.push(data.evaluation || {}); | |
if (data.is_complete) { | |
console.log('Interview completed'); | |
if (data.redirect_url) { | |
window.location.href = data.redirect_url; | |
} | |
this.showInterviewSummary(); | |
} else { | |
console.log('Moving to next question'); | |
this.currentQuestionIndex++; | |
this.currentQuestion = data.next_question; | |
this.displayQuestion(data.next_question, data.audio_url); | |
this.interviewData.questions.push(data.next_question); | |
this.resetForNextQuestion(); | |
} | |
} catch (error) { | |
console.error('Error submitting answer:', error); | |
this.showError('Connection error. Please try again.'); | |
} finally { | |
// Reset button state | |
this.confirmLoading.style.display = 'none'; | |
this.confirmButton.querySelector('span').style.display = 'inline'; | |
} | |
} | |
addUserMessage(message) { | |
const messageDiv = document.createElement('div'); | |
messageDiv.className = 'user-message'; | |
messageDiv.innerHTML = ` | |
<div class="user-bubble"> | |
<p>${message}</p> | |
</div> | |
`; | |
this.chatArea.appendChild(messageDiv); | |
this.chatArea.scrollTop = this.chatArea.scrollHeight; | |
} | |
resetForNextQuestion() { | |
this.transcriptArea.textContent = ''; | |
this.confirmButton.disabled = true; | |
this.retryButton.style.display = 'none'; | |
this.recordingStatus.textContent = 'Wait for the next question...'; | |
this.recordingStatus.style.color = '#666'; | |
this.micButton.disabled = true; | |
} | |
showInterviewSummary() { | |
const summaryContent = document.getElementById('summaryContent'); | |
let summaryHtml = ''; | |
this.interviewData.questions.forEach((question, index) => { | |
const answer = this.interviewData.answers[index] || 'No answer provided'; | |
const evaluation = this.interviewData.evaluations[index] || {}; | |
summaryHtml += ` | |
<div class="summary-item"> | |
<h4>Question ${index + 1}:</h4> | |
<p><strong>Q:</strong> ${question}</p> | |
<p><strong>A:</strong> ${answer}</p> | |
<p><strong>Score:</strong> <span class="evaluation-score">${evaluation.score || 'N/A'}</span></p> | |
<p><strong>Feedback:</strong> ${evaluation.feedback || 'No feedback provided'}</p> | |
</div> | |
`; | |
}); | |
summaryContent.innerHTML = summaryHtml; | |
// Hide main interface and show summary | |
document.querySelector('.interview-container').style.display = 'none'; | |
this.summaryPanel.style.display = 'block'; | |
} | |
showError(message) { | |
console.error('Showing error:', message); | |
// Create error message element | |
const errorDiv = document.createElement('div'); | |
errorDiv.className = 'error-message'; | |
errorDiv.textContent = message; | |
// Insert at the top of chat area | |
this.chatArea.insertBefore(errorDiv, this.chatArea.firstChild); | |
// Remove after 5 seconds | |
setTimeout(() => { | |
if (errorDiv.parentNode) { | |
errorDiv.parentNode.removeChild(errorDiv); | |
} | |
}, 5000); | |
// Also update recording status | |
this.recordingStatus.textContent = message; | |
this.recordingStatus.style.color = '#ff4757'; | |
setTimeout(() => { | |
this.recordingStatus.style.color = '#666'; | |
}, 3000); | |
} | |
} | |
// Initialize the interview when page loads | |
document.addEventListener('DOMContentLoaded', () => { | |
console.log('DOM loaded, initializing AI Interviewer...'); | |
new AIInterviewer(); | |
}); | |
// Add placeholder attribute support for contenteditable | |
document.addEventListener('DOMContentLoaded', function () { | |
const transcriptArea = document.getElementById('transcriptArea'); | |
const placeholder = transcriptArea.getAttribute('placeholder'); | |
function checkPlaceholder() { | |
if (transcriptArea.textContent.trim() === '') { | |
transcriptArea.style.color = '#999'; | |
transcriptArea.textContent = placeholder; | |
} else if (transcriptArea.textContent === placeholder) { | |
transcriptArea.style.color = '#333'; | |
transcriptArea.textContent = ''; | |
} | |
} | |
transcriptArea.addEventListener('focus', function () { | |
if (transcriptArea.textContent === placeholder) { | |
transcriptArea.textContent = ''; | |
transcriptArea.style.color = '#333'; | |
} | |
}); | |
transcriptArea.addEventListener('blur', function () { | |
if (transcriptArea.textContent.trim() === '') { | |
transcriptArea.style.color = '#999'; | |
transcriptArea.textContent = placeholder; | |
} | |
}); | |
// Initial check | |
checkPlaceholder(); | |
}); | |
</script> | |
</body> | |
</html> |