goctests0 commited on
Commit
744644d
·
verified ·
1 Parent(s): 5b9a030

Upload patient.js

Browse files
Files changed (1) hide show
  1. static/js/patient.js +148 -19
static/js/patient.js CHANGED
@@ -32,9 +32,24 @@
32
  const historyCard = document.getElementById("history-card");
33
  const historyList = document.getElementById("history-list");
34
  const chatThread = document.getElementById("chat-thread");
35
- const waitingText = document.getElementById("waiting-text");
36
  const toast = document.getElementById("toast");
37
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  function showStep(stepNumber) {
39
  document.querySelectorAll(".step").forEach(function (section) {
40
  section.classList.toggle("active", section.dataset.step === String(stepNumber));
@@ -77,6 +92,12 @@
77
  el.dataset.defaultText = el.textContent;
78
  });
79
 
 
 
 
 
 
 
80
  // Step 1: language selection
81
  languageGrid.addEventListener("click", function (event) {
82
  const option = event.target.closest(".language-option");
@@ -100,6 +121,7 @@
100
  listeningPanel.style.display = "none";
101
  inputPanel.style.display = "block";
102
  recordedBlob = null;
 
103
  showStep(1);
104
  });
105
 
@@ -183,6 +205,7 @@
183
 
184
  reviewText.value = data.patient_text;
185
  reviewPanel.style.display = "block";
 
186
  reviewText.focus();
187
  } catch (error) {
188
  transcribingPanel.style.display = "none";
@@ -197,6 +220,7 @@
197
  recordedBlob = null;
198
  inputPanel.style.display = "block";
199
  recordStatus.textContent = "";
 
200
  });
201
 
202
  // Step 2 submit
@@ -236,11 +260,11 @@
236
  // page is reused for a second message later in the same session.
237
  chatThread.querySelectorAll(".chat-bubble.from-provider").forEach(function (el) { el.remove(); });
238
  document.getElementById("response-card").style.display = "none";
239
- waitingText.style.display = "block";
240
- checkResponseBtn.style.display = "inline-flex";
241
 
242
  showStep(3);
243
  loadHistory();
 
244
  } catch (error) {
245
  showError("Couldn't reach the server. Check your connection and try again.");
246
  } finally {
@@ -248,46 +272,145 @@
248
  }
249
  });
250
 
251
- // Step 3: check for response
252
- checkResponseBtn.addEventListener("click", async function () {
 
 
253
  if (!currentInteractionId) return;
254
 
255
- clearBanner();
256
- setButtonLoading(checkResponseBtn, true, "Checking...");
 
 
257
 
258
  try {
259
  const response = await fetch("/interaction/" + currentInteractionId);
260
  const data = await response.json();
261
 
262
  if (data.translated_response) {
 
 
263
  const bubble = document.createElement("div");
264
  bubble.className = "chat-bubble from-provider list-item-in";
265
  bubble.innerHTML = '<span class="chat-label">Provider</span>' + escapeHtml(data.translated_response);
266
  chatThread.appendChild(bubble);
267
 
268
- waitingText.style.display = "none";
269
- checkResponseBtn.style.display = "none";
270
 
271
  if (data.audio_url) {
272
  document.getElementById("response-card").style.display = "block";
273
- document.getElementById("response-audio").src = data.audio_url;
274
  }
275
- } else {
 
 
 
 
276
  showError("No reply yet, try again in a moment.");
277
  }
278
  } catch (error) {
279
- showError("Couldn't reach the server. Check your connection and try again.");
 
 
280
  } finally {
281
- setButtonLoading(checkResponseBtn, false, "Checking...");
282
  }
 
 
 
 
283
  });
284
 
285
- // Past messages for this session
286
- async function loadHistory() {
287
- historyCard.style.display = "block";
288
- historyList.innerHTML =
289
- '<div class="skeleton-row"></div><div class="skeleton-row"></div>';
290
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
  try {
292
  const response = await fetch("/history");
293
  const items = await response.json();
@@ -297,6 +420,7 @@
297
  return;
298
  }
299
 
 
300
  historyList.innerHTML = "";
301
  items.forEach(function (item, index) {
302
  const row = document.createElement("div");
@@ -313,7 +437,6 @@
313
  '<div class="result-text">' + escapeHtml(item.input_text) + "</div>";
314
  historyList.appendChild(row);
315
  });
316
- historyCard.style.display = "block";
317
  } catch (error) {
318
  historyCard.style.display = "none";
319
  }
@@ -322,6 +445,8 @@
322
  // Start a new conversation: ends the current session server-side and
323
  // resets the page back to language selection.
324
  newConversationBtn.addEventListener("click", async function () {
 
 
325
  try {
326
  await fetch("/end-session", { method: "POST" });
327
  } catch (error) {
@@ -333,10 +458,14 @@
333
  currentInteractionId = null;
334
  document.getElementById("patient-text").value = "";
335
  document.getElementById("response-card").style.display = "none";
 
 
 
336
  reviewPanel.style.display = "none";
337
  transcribingPanel.style.display = "none";
338
  listeningPanel.style.display = "none";
339
  inputPanel.style.display = "block";
 
340
  languageGrid.querySelectorAll(".language-option").forEach(function (el) {
341
  el.setAttribute("aria-pressed", "false");
342
  });
 
32
  const historyCard = document.getElementById("history-card");
33
  const historyList = document.getElementById("history-list");
34
  const chatThread = document.getElementById("chat-thread");
35
+ const waitingCard = document.getElementById("waiting-card");
36
  const toast = document.getElementById("toast");
37
 
38
+ // Audio player controls (for the provider's spoken reply)
39
+ const responseAudioEl = document.getElementById("response-audio");
40
+ const audioPlayBtn = document.getElementById("audio-play-btn");
41
+ const audioReplayBtn = document.getElementById("audio-replay-btn");
42
+ const audioSeek = document.getElementById("audio-seek");
43
+ const audioCurrentTimeEl = document.getElementById("audio-current-time");
44
+ const audioDurationEl = document.getElementById("audio-duration");
45
+ const audioDownloadBtn = document.getElementById("audio-download-btn");
46
+ const iconPlay = audioPlayBtn.querySelector(".icon-play");
47
+ const iconPause = audioPlayBtn.querySelector(".icon-pause");
48
+
49
+ // Auto-refresh for step 3: polls for the provider's reply in the
50
+ // background so the patient doesn't have to keep tapping "Check now".
51
+ let pollTimer = null;
52
+
53
  function showStep(stepNumber) {
54
  document.querySelectorAll(".step").forEach(function (section) {
55
  section.classList.toggle("active", section.dataset.step === String(stepNumber));
 
92
  el.dataset.defaultText = el.textContent;
93
  });
94
 
95
+ function setSubmitLabel(text) {
96
+ const label = submitInputBtn.querySelector(".btn-label");
97
+ label.textContent = text;
98
+ label.dataset.defaultText = text;
99
+ }
100
+
101
  // Step 1: language selection
102
  languageGrid.addEventListener("click", function (event) {
103
  const option = event.target.closest(".language-option");
 
121
  listeningPanel.style.display = "none";
122
  inputPanel.style.display = "block";
123
  recordedBlob = null;
124
+ setSubmitLabel("Send to provider");
125
  showStep(1);
126
  });
127
 
 
205
 
206
  reviewText.value = data.patient_text;
207
  reviewPanel.style.display = "block";
208
+ setSubmitLabel("Confirm and send");
209
  reviewText.focus();
210
  } catch (error) {
211
  transcribingPanel.style.display = "none";
 
220
  recordedBlob = null;
221
  inputPanel.style.display = "block";
222
  recordStatus.textContent = "";
223
+ setSubmitLabel("Send to provider");
224
  });
225
 
226
  // Step 2 submit
 
260
  // page is reused for a second message later in the same session.
261
  chatThread.querySelectorAll(".chat-bubble.from-provider").forEach(function (el) { el.remove(); });
262
  document.getElementById("response-card").style.display = "none";
263
+ waitingCard.style.display = "block";
 
264
 
265
  showStep(3);
266
  loadHistory();
267
+ startPolling();
268
  } catch (error) {
269
  showError("Couldn't reach the server. Check your connection and try again.");
270
  } finally {
 
272
  }
273
  });
274
 
275
+ // Step 3: check for response (shared by the manual button and the
276
+ // background poll below, so there's exactly one place that decides
277
+ // what "answered" means and what happens next).
278
+ async function checkForResponse(isManual) {
279
  if (!currentInteractionId) return;
280
 
281
+ if (isManual) {
282
+ clearBanner();
283
+ setButtonLoading(checkResponseBtn, true, "Checking...");
284
+ }
285
 
286
  try {
287
  const response = await fetch("/interaction/" + currentInteractionId);
288
  const data = await response.json();
289
 
290
  if (data.translated_response) {
291
+ stopPolling();
292
+
293
  const bubble = document.createElement("div");
294
  bubble.className = "chat-bubble from-provider list-item-in";
295
  bubble.innerHTML = '<span class="chat-label">Provider</span>' + escapeHtml(data.translated_response);
296
  chatThread.appendChild(bubble);
297
 
298
+ waitingCard.style.display = "none";
 
299
 
300
  if (data.audio_url) {
301
  document.getElementById("response-card").style.display = "block";
302
+ setAudioSource(data.audio_url);
303
  }
304
+
305
+ // Keep the history list's "Waiting for reply" / "Answered" badge
306
+ // in sync now, rather than only on the next full page load.
307
+ loadHistory();
308
+ } else if (isManual) {
309
  showError("No reply yet, try again in a moment.");
310
  }
311
  } catch (error) {
312
+ // A background poll fails silently and just retries next interval;
313
+ // only a manual tap surfaces a connection error.
314
+ if (isManual) showError("Couldn't reach the server. Check your connection and try again.");
315
  } finally {
316
+ if (isManual) setButtonLoading(checkResponseBtn, false, "Checking...");
317
  }
318
+ }
319
+
320
+ checkResponseBtn.addEventListener("click", function () {
321
+ checkForResponse(true);
322
  });
323
 
324
+ function startPolling() {
325
+ stopPolling();
326
+ pollTimer = setInterval(function () { checkForResponse(false); }, 15000);
327
+ }
 
328
 
329
+ function stopPolling() {
330
+ if (pollTimer) {
331
+ clearInterval(pollTimer);
332
+ pollTimer = null;
333
+ }
334
+ }
335
+
336
+ // Don't burn the patient's data plan polling in the background if they've
337
+ // switched away to another tab or app; catch back up when they return.
338
+ document.addEventListener("visibilitychange", function () {
339
+ if (document.hidden) {
340
+ stopPolling();
341
+ } else if (currentInteractionId && waitingCard.style.display !== "none") {
342
+ checkForResponse(false);
343
+ startPolling();
344
+ }
345
+ });
346
+
347
+ // Audio player for the provider's spoken reply.
348
+ function formatAudioTime(totalSeconds) {
349
+ if (!isFinite(totalSeconds) || totalSeconds < 0) return "0:00";
350
+ const minutes = Math.floor(totalSeconds / 60);
351
+ const seconds = Math.floor(totalSeconds % 60);
352
+ return minutes + ":" + String(seconds).padStart(2, "0");
353
+ }
354
+
355
+ function setAudioSource(url) {
356
+ responseAudioEl.src = url;
357
+ audioDownloadBtn.href = url;
358
+ audioSeek.value = 0;
359
+ audioCurrentTimeEl.textContent = "0:00";
360
+ audioDurationEl.textContent = "0:00";
361
+ iconPlay.style.display = "inline";
362
+ iconPause.style.display = "none";
363
+ }
364
+
365
+ audioPlayBtn.addEventListener("click", function () {
366
+ if (responseAudioEl.paused) {
367
+ responseAudioEl.play();
368
+ } else {
369
+ responseAudioEl.pause();
370
+ }
371
+ });
372
+
373
+ audioReplayBtn.addEventListener("click", function () {
374
+ responseAudioEl.currentTime = 0;
375
+ responseAudioEl.play();
376
+ });
377
+
378
+ responseAudioEl.addEventListener("play", function () {
379
+ iconPlay.style.display = "none";
380
+ iconPause.style.display = "inline";
381
+ });
382
+
383
+ responseAudioEl.addEventListener("pause", function () {
384
+ iconPlay.style.display = "inline";
385
+ iconPause.style.display = "none";
386
+ });
387
+
388
+ responseAudioEl.addEventListener("ended", function () {
389
+ iconPlay.style.display = "inline";
390
+ iconPause.style.display = "none";
391
+ });
392
+
393
+ responseAudioEl.addEventListener("loadedmetadata", function () {
394
+ audioSeek.max = responseAudioEl.duration || 0;
395
+ audioDurationEl.textContent = formatAudioTime(responseAudioEl.duration);
396
+ });
397
+
398
+ responseAudioEl.addEventListener("timeupdate", function () {
399
+ audioSeek.value = responseAudioEl.currentTime;
400
+ audioCurrentTimeEl.textContent = formatAudioTime(responseAudioEl.currentTime);
401
+ });
402
+
403
+ audioSeek.addEventListener("input", function () {
404
+ responseAudioEl.currentTime = Number(audioSeek.value);
405
+ });
406
+
407
+ // Past messages for this session.
408
+ // Deliberately does NOT show the card or a loading skeleton up front:
409
+ // a brand new visitor (or anyone right after "Start a new conversation")
410
+ // has no history yet, so an optimistic reveal would just flash open and
411
+ // immediately hide itself once the empty response came back. The card
412
+ // only appears once a response has confirmed there's something to show.
413
+ async function loadHistory() {
414
  try {
415
  const response = await fetch("/history");
416
  const items = await response.json();
 
420
  return;
421
  }
422
 
423
+ historyCard.style.display = "block";
424
  historyList.innerHTML = "";
425
  items.forEach(function (item, index) {
426
  const row = document.createElement("div");
 
437
  '<div class="result-text">' + escapeHtml(item.input_text) + "</div>";
438
  historyList.appendChild(row);
439
  });
 
440
  } catch (error) {
441
  historyCard.style.display = "none";
442
  }
 
445
  // Start a new conversation: ends the current session server-side and
446
  // resets the page back to language selection.
447
  newConversationBtn.addEventListener("click", async function () {
448
+ stopPolling();
449
+
450
  try {
451
  await fetch("/end-session", { method: "POST" });
452
  } catch (error) {
 
458
  currentInteractionId = null;
459
  document.getElementById("patient-text").value = "";
460
  document.getElementById("response-card").style.display = "none";
461
+ waitingCard.style.display = "block";
462
+ responseAudioEl.pause();
463
+ responseAudioEl.removeAttribute("src");
464
  reviewPanel.style.display = "none";
465
  transcribingPanel.style.display = "none";
466
  listeningPanel.style.display = "none";
467
  inputPanel.style.display = "block";
468
+ setSubmitLabel("Send to provider");
469
  languageGrid.querySelectorAll(".language-option").forEach(function (el) {
470
  el.setAttribute("aria-pressed", "false");
471
  });