// TalkToDoc provider page logic. // Lists interactions in three tabs (Waiting / Active / Completed), and // lets the provider open one to read full context and reply, with a real // preview of the translation and speech before actually sending. (function () { let selectedInteractionId = null; let activeItem = null; let currentTab = "waiting"; let pendingItems = []; let completedItems = []; let completedLoaded = false; // Set on a successful preview, cleared the moment the reply text // changes again - only ever valid for the exact text it was generated // from, so Send only reuses it when nothing has been edited since. let lastPreviewText = null; let lastPreviewTranslation = null; // Completed tab filters — client-side only, no new API calls needed let completedLanguageFilter = "all"; let completedSortAsc = false; // false = newest first (default) const statusBanner = document.getElementById("status-banner"); const pendingList = document.getElementById("pending-list"); const emptyPending = document.getElementById("empty-pending"); const emptyPendingTitle = document.getElementById("empty-pending-title"); const emptyPendingBody = document.getElementById("empty-pending-body"); const queueHeading = document.getElementById("queue-heading"); const queueSearchInput = document.getElementById("queue-search"); const completedControls = document.getElementById("completed-controls"); const sortToggleBtn = document.getElementById("sort-toggle-btn"); const sortLabel = document.getElementById("sort-label"); const sortIconAsc = document.getElementById("sort-icon-asc"); const sortIconDesc = document.getElementById("sort-icon-desc"); const backToListBtn = document.getElementById("back-to-list"); const backToListReadonlyBtn = document.getElementById("back-to-list-readonly"); const submitResponseBtn = document.getElementById("submit-response"); const previewBtn = document.getElementById("preview-response-btn"); const previewBlock = document.getElementById("preview-block"); const previewTextBubble = document.getElementById("preview-text-bubble"); const responseTextInput = document.getElementById("response-text-input"); const patientHistoryCard = document.getElementById("patient-history-card"); const patientHistoryList = document.getElementById("patient-history-list"); const composeArea = document.getElementById("compose-area"); const readonlyArea = document.getElementById("readonly-area"); const respondProviderBubble = document.getElementById("respond-provider-bubble"); const respondProviderReplyText = document.getElementById("respond-provider-reply-text"); const toast = document.getElementById("toast"); // Preview audio player const previewAudioEl = document.getElementById("preview-audio"); const previewPlayBtn = document.getElementById("preview-audio-play-btn"); const previewReplayBtn = document.getElementById("preview-audio-replay-btn"); const previewSeek = document.getElementById("preview-audio-seek"); const previewCurrentTimeEl = document.getElementById("preview-audio-current-time"); const previewDurationEl = document.getElementById("preview-audio-duration"); const previewIconPlay = previewPlayBtn.querySelector(".icon-play"); const previewIconPause = previewPlayBtn.querySelector(".icon-pause"); function showStep(stepName) { document.querySelectorAll(".step").forEach(function (section) { section.classList.toggle("active", section.dataset.step === stepName); }); } function showError(message) { statusBanner.innerHTML = '
"; } function clearBanner() { statusBanner.innerHTML = ""; } function escapeHtml(text) { const div = document.createElement("div"); div.textContent = text == null ? "" : String(text); return div.innerHTML; } function showToast(message) { toast.textContent = message; toast.classList.add("visible"); setTimeout(function () { toast.classList.remove("visible"); }, 2200); } function setButtonLoading(button, isLoading, loadingText) { const spinner = button.querySelector(".btn-spinner"); const icon = button.querySelector(".btn-icon"); const label = button.querySelector(".btn-label"); button.disabled = isLoading; if (spinner) spinner.style.display = isLoading ? "inline-block" : "none"; if (icon) icon.style.display = isLoading ? "none" : "inline-flex"; if (label && loadingText) label.textContent = isLoading ? loadingText : label.dataset.defaultText; } document.querySelectorAll(".btn-label").forEach(function (el) { el.dataset.defaultText = el.textContent; }); function capitalize(text) { return text ? text.charAt(0).toUpperCase() + text.slice(1) : text; } // ---- Queue (Waiting / Active / Completed tabs) ---- function getTabItems() { if (currentTab === "waiting") { return pendingItems.filter(function (item) { return !activeItem || item.id !== activeItem.id; }); } if (currentTab === "active") { return activeItem ? [activeItem] : []; } // Completed tab: apply language filter then sort let items = completedItems; if (completedLanguageFilter !== "all") { items = items.filter(function (item) { return (item.detected_language || "").toLowerCase() === completedLanguageFilter; }); } if (completedSortAsc) { items = items.slice().sort(function (a, b) { return a.timestamp < b.timestamp ? -1 : 1; }); } return items; } function matchesQuery(item, query) { if (!query) return true; const haystack = ( (item.detected_language || "") + " " + (item.translated_text || "") + " " + (item.input_text || "") ).toLowerCase(); return haystack.indexOf(query) !== -1; } function buildQueueRow(item, index) { const row = document.createElement("button"); row.type = "button"; row.className = "queue-card list-item-in"; row.style.animationDelay = (index * 50) + "ms"; const timeLabel = window.formatRelativeTime ? window.formatRelativeTime(item.timestamp) : ""; const answered = Boolean(item.translated_response); const isActive = Boolean(activeItem) && activeItem.id === item.id; const badgeClass = answered ? "answered" : "waiting"; const badgeLabel = answered ? "Answered" : (isActive ? "In progress" : "#" + item.id); // Descriptive aria-label so screen readers announce "Yoruba, just now, // [preview text]" rather than the meaningless default "button". const ariaLabel = [ capitalize(item.detected_language), timeLabel, answered ? "Answered" : "Waiting for reply", item.translated_text ].filter(Boolean).join(", "); row.setAttribute("aria-label", ariaLabel); row.innerHTML = '' + '' + '' + '' + escapeHtml(item.detected_language) + '' + '' + '' + '' + '' + '' + '' + escapeHtml(item.translated_text) + '' + ''; row.addEventListener("click", function () { openInteraction(item); }); return row; } function renderQueueList() { const query = queueSearchInput.value.trim().toLowerCase(); const items = getTabItems().filter(function (item) { return matchesQuery(item, query); }); // Show completed-controls (language filter + sort) only on the // Completed tab — filtering a short live Waiting/Active queue // adds friction rather than value. completedControls.style.display = currentTab === "completed" ? "block" : "none"; pendingList.innerHTML = ""; const isEmpty = items.length === 0; emptyPending.style.display = isEmpty ? "block" : "none"; if (isEmpty) { const hasFilter = query || (currentTab === "completed" && completedLanguageFilter !== "all"); if (hasFilter) { emptyPendingTitle.textContent = "No matches"; emptyPendingBody.textContent = "Try a different search or filter."; } else if (currentTab === "waiting") { emptyPendingTitle.textContent = "All caught up"; emptyPendingBody.textContent = "Nothing waiting right now."; } else if (currentTab === "active") { emptyPendingTitle.textContent = "Nothing active"; emptyPendingBody.textContent = "Open a message from the Waiting tab to start on it."; } else { emptyPendingTitle.textContent = "No completed consultations"; emptyPendingBody.textContent = "Completed replies will appear here."; } return; } items.forEach(function (item, index) { pendingList.appendChild(buildQueueRow(item, index)); }); } function updateTabCounts() { const waitingCount = pendingItems.filter(function (item) { return !activeItem || item.id !== activeItem.id; }).length; document.getElementById("tab-count-waiting").textContent = waitingCount; document.getElementById("tab-count-active").textContent = activeItem ? "1" : "0"; if (completedLoaded) { document.getElementById("tab-count-completed").textContent = completedItems.length; } } async function loadPending() { if (currentTab === "waiting") { pendingList.innerHTML = ''; } try { const response = await fetch("/pending-interactions"); const interactions = await response.json(); pendingItems = interactions; const newHeadingText = interactions.length === 0 ? "All caught up" : interactions.length === 1 ? "1 message waiting for you" : interactions.length + " messages waiting for you"; queueHeading.textContent = newHeadingText; queueHeading.style.animation = "none"; void queueHeading.offsetWidth; queueHeading.style.animation = ""; renderQueueList(); updateTabCounts(); } catch (error) { showError("Couldn't load pending messages. Check your connection."); } } async function loadCompleted() { pendingList.innerHTML = ''; try { const response = await fetch("/completed-interactions"); completedItems = await response.json(); completedLoaded = true; renderQueueList(); updateTabCounts(); } catch (error) { showError("Couldn't load completed messages. Check your connection."); } } document.querySelectorAll(".queue-tab").forEach(function (tabBtn) { tabBtn.addEventListener("click", function () { currentTab = tabBtn.dataset.tab; document.querySelectorAll(".queue-tab").forEach(function (t) { t.classList.toggle("active", t === tabBtn); t.setAttribute("aria-selected", t === tabBtn ? "true" : "false"); }); if (currentTab === "completed" && !completedLoaded) { loadCompleted(); } else { renderQueueList(); } }); }); queueSearchInput.addEventListener("input", function () { renderQueueList(); }); // Language filter chips (completed tab only) — set initial aria-pressed document.querySelectorAll("#language-filter-chips .filter-chip").forEach(function (c) { c.setAttribute("aria-pressed", c.classList.contains("active") ? "true" : "false"); }); document.getElementById("language-filter-chips").addEventListener("click", function (event) { const chip = event.target.closest(".filter-chip"); if (!chip) return; completedLanguageFilter = chip.dataset.filter; document.querySelectorAll("#language-filter-chips .filter-chip").forEach(function (c) { const active = c === chip; c.classList.toggle("active", active); c.setAttribute("aria-pressed", active ? "true" : "false"); }); renderQueueList(); }); // Sort toggle (completed tab only) sortToggleBtn.addEventListener("click", function () { completedSortAsc = !completedSortAsc; sortLabel.textContent = completedSortAsc ? "Oldest first" : "Newest first"; sortIconAsc.style.display = completedSortAsc ? "inline" : "none"; sortIconDesc.style.display = completedSortAsc ? "none" : "inline"; renderQueueList(); }); // ---- Workspace ---- async function loadPatientHistoryForWorkspace(item) { patientHistoryCard.style.display = "none"; try { const response = await fetch("/patient-history/" + item.user_id); const items = await response.json(); const others = items.filter(function (h) { return h.id !== item.id; }); if (!others.length) return; patientHistoryList.innerHTML = ""; others.forEach(function (h, index) { const row = document.createElement("div"); row.className = "result-block history-row-static list-item-in"; row.style.animationDelay = (index * 40) + "ms"; const answered = Boolean(h.translated_response); const badgeClass = answered ? "answered" : "waiting"; const badgeLabel = answered ? "Answered" : "Waiting for reply"; const timeLabel = window.formatRelativeTime ? window.formatRelativeTime(h.timestamp) : ""; row.innerHTML = '