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">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.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; | |
| 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 }) | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`HTTP error! status: ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| if (data.error) { | |
| this.showError(data.error); | |
| return; | |
| } | |
| 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 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 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 | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`HTTP error! status: ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| if (data.error) { | |
| this.recordingStatus.textContent = data.error; | |
| 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.'; | |
| } else { | |
| this.recordingStatus.textContent = 'No speech detected. 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 | |
| }) | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`HTTP error! status: ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| if (data.error) { | |
| this.showError(data.error); | |
| return; | |
| } | |
| if (data.success) { | |
| this.interviewData.answers.push(answer); | |
| this.interviewData.evaluations.push(data.evaluation); | |
| if (data.isComplete) { | |
| this.showInterviewSummary(); | |
| } 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() { | |
| 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) { | |
| // 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', () => { | |
| 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> | |