Spaces:
Sleeping
Sleeping
| /******************************* | |
| * Interview Q&A Frontend JS * | |
| *******************************/ | |
| const recordBtn = document.getElementById("record-button"); | |
| const screenshotBtn = document.getElementById("screenshot-button"); | |
| const fileInput = document.getElementById("file-input"); | |
| const questionEl = document.getElementById("question-output"); | |
| const answerEl = document.getElementById("answer-output"); | |
| const editBtn = document.getElementById("edit-btn"); | |
| /* ─────────────────── Typing effect utility ─────────────────── */ | |
| function typeEffect(el, text, speed = 30) { | |
| el.textContent = ""; | |
| let idx = 0; | |
| const timer = setInterval(() => { | |
| el.textContent += text.charAt(idx); | |
| idx++; | |
| if (idx >= text.length) clearInterval(timer); | |
| }, speed); | |
| } | |
| /* ─────────────────── Abort-controller wrapper ───────────────── */ | |
| let currentController = null; | |
| function fetchWithAbort(url, opts = {}) { | |
| if (currentController) currentController.abort(); // cancel previous req | |
| currentController = new AbortController(); | |
| return fetch(url, { ...opts, signal: currentController.signal }); | |
| } | |
| /* ─────────────────── Audio recording setup ─────────────────── */ | |
| let mediaRecorder, chunks = [], isRecording = false; | |
| async function initMedia() { | |
| const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); | |
| mediaRecorder = new MediaRecorder(stream); | |
| mediaRecorder.ondataavailable = e => chunks.push(e.data); | |
| mediaRecorder.onstop = async () => { | |
| recordBtn.textContent = "🎤 Start Recording"; | |
| isRecording = false; | |
| const audioBlob = new Blob(chunks, { type: "audio/wav" }); | |
| chunks = []; | |
| const form = new FormData(); | |
| form.append("file", audioBlob, "record.wav"); | |
| questionEl.textContent = "⌛ Transcribing…"; | |
| answerEl.innerHTML = ""; | |
| try { | |
| const res = await fetchWithAbort("/voice-transcribe", { method: "POST", body: form }); | |
| if (!res.ok) throw new Error(`HTTP ${res.status}`); | |
| const data = await res.json(); | |
| displayQa(data); | |
| } catch (err) { | |
| answerEl.textContent = "❌ " + err.message; | |
| } | |
| }; | |
| } | |
| /* ─────────────── Click-to-record UX ─────────────── */ | |
| recordBtn.addEventListener("click", () => { | |
| if (!mediaRecorder) return; | |
| if (isRecording) { | |
| mediaRecorder.stop(); | |
| } else { | |
| chunks = []; | |
| mediaRecorder.start(); | |
| recordBtn.textContent = "🎤 Stop Recording"; | |
| isRecording = true; | |
| } | |
| }); | |
| /* ─────────────── Screenshot upload ─────────────── */ | |
| fileInput.addEventListener("change", async (e) => { | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| const form = new FormData(); | |
| form.append("file", file); | |
| questionEl.textContent = "⌛ Processing screenshot…"; | |
| answerEl.innerHTML = ""; | |
| try { | |
| const res = await fetchWithAbort("/image-question", { method: "POST", body: form }); | |
| if (!res.ok) throw new Error(`HTTP ${res.status}`); | |
| const data = await res.json(); | |
| displayQa(data); | |
| } catch (err) { | |
| answerEl.textContent = "❌ " + err.message; | |
| } finally { | |
| fileInput.value = ""; // reset for next upload | |
| } | |
| }); | |
| screenshotBtn.addEventListener("click", () => fileInput.click()); | |
| /* ─────────────── Editable question block ─────────────── */ | |
| function enableEdit() { | |
| questionEl.contentEditable = "true"; | |
| questionEl.classList.add("editing"); | |
| questionEl.focus(); | |
| } | |
| async function sendEditedQuestion(text) { | |
| questionEl.contentEditable = "false"; | |
| questionEl.classList.remove("editing"); | |
| answerEl.textContent = "⌛ Thinking…"; | |
| try { | |
| const res = await fetchWithAbort("/text-question", { | |
| method : "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body : JSON.stringify({ question: text }) | |
| }); | |
| if (!res.ok) throw new Error(`HTTP ${res.status}`); | |
| const data = await res.json(); | |
| displayQa(data); | |
| } catch (err) { | |
| answerEl.textContent = "❌ " + err.message; | |
| } | |
| } | |
| editBtn.addEventListener("click", () => enableEdit()); | |
| questionEl.addEventListener("keydown", (e) => { | |
| if (e.key === "Enter") { | |
| e.preventDefault(); | |
| const text = questionEl.innerText.trim(); | |
| if (text) sendEditedQuestion(text); | |
| } | |
| }); | |
| /* ─────────────── render helpers ─────────────── */ | |
| function displayQa(data) { | |
| let qHtml = "", aHtml = ""; | |
| const qaList = Array.isArray(data) ? data : [data]; | |
| qaList.forEach((item, idx) => { | |
| const q = item.question || "[no question]"; | |
| const a = item.answer || "[no answer]"; | |
| qHtml += `Q${idx + 1}: ${q}\n`; | |
| aHtml += `<strong>Q${idx + 1}:</strong> ${DOMPurify.sanitize(marked.parseInline(q))}<br>`; | |
| aHtml += `<strong>A${idx + 1}:</strong> ${DOMPurify.sanitize(marked.parse(a))}<hr>`; | |
| }); | |
| typeEffect(questionEl, qHtml.trim()); | |
| setTimeout(() => { answerEl.innerHTML = aHtml.trim(); }, 400); | |
| } | |
| /* ─────────────── init ─────────────── */ | |
| window.addEventListener("DOMContentLoaded", async () => { | |
| try { | |
| await initMedia(); | |
| } catch { | |
| alert("Microphone permission is required."); | |
| } | |
| }); | |