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; | |
} | |
@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; | |
} | |
} | |
/* Hide default audio controls */ | |
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">3</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.close()"> | |
Complete Interview | |
</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; | |
this.totalQuestions = 3; | |
this.isRecording = false; | |
this.mediaRecorder = null; | |
this.audioChunks = []; | |
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() { | |
this.micButton.addEventListener('mousedown', () => this.startRecording()); | |
this.micButton.addEventListener('mouseup', () => this.stopRecording()); | |
this.micButton.addEventListener('mouseleave', () => this.stopRecording()); | |
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 { | |
const response = await fetch('/api/start_interview', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json' | |
}, | |
body: JSON.stringify({ job_id: JOB_ID }) | |
}); | |
const data = await response.json(); | |
if (data.success) { | |
this.displayQuestion(data.question, data.audioUrl); | |
this.interviewData.questions.push(data.question); | |
} else { | |
this.showError('Failed to start interview. Please try again.'); | |
} | |
} catch (error) { | |
console.error('Error starting interview:', error); | |
this.showError('Connection error. Please check your internet connection.'); | |
} | |
} | |
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 talking">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) { | |
this.playQuestionAudio(audioUrl); | |
} else { | |
// Enable controls if no audio | |
setTimeout(() => this.enableControls(), 1000); | |
} | |
} | |
playQuestionAudio(audioUrl) { | |
this.ttsAudio.src = audioUrl; | |
this.ttsAudio.play().catch(error => { | |
console.error('Audio play error:', error); | |
this.enableControls(); | |
}); | |
} | |
enableControls() { | |
this.micButton.disabled = false; | |
this.recordingStatus.textContent = 'Click and hold to record your answer'; | |
// Remove talking animation from avatar | |
const avatars = this.chatArea.querySelectorAll('.ai-avatar'); | |
avatars.forEach(avatar => avatar.classList.remove('talking')); | |
} | |
async startRecording() { | |
if (this.isRecording) return; | |
try { | |
const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); | |
this.mediaRecorder = new MediaRecorder(stream); | |
this.audioChunks = []; | |
this.mediaRecorder.ondataavailable = (event) => { | |
this.audioChunks.push(event.data); | |
}; | |
this.mediaRecorder.onstop = () => { | |
this.processRecording(); | |
stream.getTracks().forEach(track => track.stop()); | |
}; | |
this.mediaRecorder.start(); | |
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.'; | |
} | |
} | |
stopRecording() { | |
if (!this.isRecording || !this.mediaRecorder) return; | |
this.mediaRecorder.stop(); | |
this.isRecording = false; | |
// Update UI | |
this.micButton.classList.remove('recording'); | |
this.micIcon.textContent = '🎤'; | |
this.recordingStatus.textContent = 'Processing audio...'; | |
} | |
async processRecording() { | |
const audioBlob = new Blob(this.audioChunks, { type: 'audio/wav' }); | |
const formData = new FormData(); | |
formData.append('audio', audioBlob, 'recording.wav'); | |
try { | |
const response = await fetch('/api/transcribe_audio', { | |
method: 'POST', | |
body: formData | |
}); | |
const data = await response.json(); | |
if (data.success && data.transcript) { | |
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.'; | |
} else { | |
this.recordingStatus.textContent = 'Transcription failed. Please try recording again.'; | |
} | |
} catch (error) { | |
console.error('Error processing recording:', error); | |
this.recordingStatus.textContent = 'Error processing audio. Please try again.'; | |
} | |
} | |
resetRecording() { | |
this.transcriptArea.textContent = ''; | |
this.confirmButton.disabled = true; | |
this.retryButton.style.display = 'none'; | |
this.recordingStatus.textContent = 'Click and hold to record your answer'; | |
} | |
async submitAnswer() { | |
const answer = this.transcriptArea.textContent.trim(); | |
if (!answer) return; | |
// 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 | |
}) | |
}); | |
const data = await response.json(); | |
if (data.success) { | |
this.interviewData.answers.push(answer); | |
this.interviewData.evaluations.push(data.evaluation); | |
if (data.isComplete) { | |
this.showInterviewSummary(data.summary); | |
} else { | |
this.currentQuestionIndex++; | |
this.displayQuestion(data.nextQuestion, data.audioUrl); | |
this.interviewData.questions.push(data.nextQuestion); | |
this.resetForNextQuestion(); | |
} | |
} else { | |
this.showError('Failed to process answer. Please try again.'); | |
} | |
} 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.micButton.disabled = true; | |
} | |
showInterviewSummary(summaryData) { | |
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) { | |
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', () => { | |
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> |