Spaces:
Running
Running
/******************************* | |
* 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 += `<strong>Q${idx + 1}:</strong> ${DOMPurify.sanitize(marked.parseInline(q))}<br>`; | |
aHtml += `<strong>A${idx + 1}:</strong> ${DOMPurify.sanitize(marked.parse(a))}<hr>`; | |
}); | |
// 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."); | |
} | |
}); | |