/******************************* * 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 = []; 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 () => { 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; } }; } /* ─────────────── Screenshot / image-question 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()); /* ─────────────────── Hold-to-record UX ─────────────────────── */ function bindRecordBtn() { recordBtn.addEventListener("mousedown", () => mediaRecorder.start()); recordBtn.addEventListener("mouseup", () => mediaRecorder.stop()); recordBtn.addEventListener("touchstart", e => { e.preventDefault(); mediaRecorder.start(); }); recordBtn.addEventListener("touchend", e => { e.preventDefault(); mediaRecorder.stop(); }); } /* ─────────────────── 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); } }); /* ─────────────────────── helpers ───────────────────────────── */ function displayQa(data) { let qHtml = "", aHtml = ""; // Parse and bind JSON now as Q&A can be an array with more than 1 component(s) 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 += `Q${idx + 1}: ${DOMPurify.sanitize(marked.parseInline(q))}
`; aHtml += `A${idx + 1}: ${DOMPurify.sanitize(marked.parse(a))}
`; }); // Type effect with trimming element typeEffect(questionEl, qHtml.trim()); setTimeout(() => { answerEl.innerHTML = aHtml.trim(); }, 400); } /* ─────────────────────── bootstrap ─────────────────────────── */ window.addEventListener("DOMContentLoaded", async () => { try { await initMedia(); bindRecordBtn(); } catch { alert("Microphone permission is required."); } });