File size: 5,517 Bytes
e7b1e22
 
 
3cb4983
 
 
 
 
 
e7b1e22
3cb4983
e7b1e22
 
 
 
 
 
 
 
 
 
3cb4983
 
 
aeade20
3cb4983
 
 
 
 
aeade20
 
e7b1e22
3cb4983
 
 
 
 
aeade20
 
3cb4983
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aeade20
 
 
 
 
 
 
 
 
 
 
 
 
 
3cb4983
 
 
 
 
 
 
 
 
e7b1e22
3cb4983
 
 
 
e7b1e22
3cb4983
 
 
e7b1e22
3cb4983
 
e7b1e22
aeade20
3cb4983
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aeade20
3cb4983
61d1de3
 
 
 
 
 
 
 
 
 
 
3cb4983
 
aeade20
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
152
153
154
/*******************************
 * 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.");
  }
});