File size: 5,856 Bytes
e7b1e22
 
 
3cb4983
 
 
 
 
 
e7b1e22
3cb4983
e7b1e22
 
 
 
 
 
 
 
 
 
3cb4983
 
 
 
 
 
 
 
 
243b6fb
e7b1e22
3cb4983
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e7b1e22
3cb4983
 
 
 
e7b1e22
3cb4983
 
 
e7b1e22
3cb4983
 
e7b1e22
3cb4983
e7b1e22
 
 
 
3cb4983
e7b1e22
 
3cb4983
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61d1de3
 
 
 
 
 
 
 
 
 
 
 
 
3cb4983
 
61d1de3
3cb4983
e7b1e22
3cb4983
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
/*******************************
 * 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.");
  }
});