| import { state } from './state.js'; |
| import * as api from './api.js'; |
| import * as ui from './ui.js'; |
| import * as audio from './audio.js'; |
| import * as profiles from './profiles.js'; |
| import * as session from './session.js'; |
| import * as auth from './auth.js'; |
|
|
| |
| const startButton = document.getElementById("startButton"); |
| const voiceModeBtn = document.getElementById("voiceModeBtn"); |
| const textModeBtn = document.getElementById("textModeBtn"); |
| const voiceInput = document.getElementById("voiceInput"); |
| const textInput = document.getElementById("textInput"); |
| const chatInput = document.getElementById("chatInput"); |
| const sendBtn = document.getElementById("sendBtn"); |
| const micBtn = document.getElementById("micBtn"); |
|
|
| |
| |
| |
| function bindEvents() { |
| |
| window.addEventListener("popstate", (event) => { |
| if (event.state && event.state.screen) { |
| ui.switchScreen(event.state.screen, false); |
| } else { |
| const path = window.location.pathname; |
| if (path === "/dashboard" || path === "/session") { |
| if (!state.patientId) { |
| profiles.fetchPatients(); |
| } else { |
| ui.switchScreen(path === "/dashboard" ? "dashboardScreen" : "mainApp", false); |
| } |
| } else { |
| ui.switchScreen("loadingScreen", false); |
| } |
| } |
| }); |
|
|
| |
| window.addEventListener("unauthorized", () => { |
| ui.switchScreen("authScreen", false); |
| }); |
|
|
| |
| const loginFormContainer = document.getElementById("loginFormContainer"); |
| const signupFormContainer = document.getElementById("signupFormContainer"); |
| const forgotPwdFormContainer = document.getElementById("forgotPwdFormContainer"); |
| const otpStepContainer = document.getElementById("otpStepContainer"); |
| const newPwdStepContainer = document.getElementById("newPwdStepContainer"); |
| const signupOtpStepContainer = document.getElementById("signupOtpStepContainer"); |
|
|
| |
| let _otpVerifiedEmail = null; |
|
|
| function showAuthPanel(panelId) { |
| |
| const panels = [loginFormContainer, signupFormContainer, forgotPwdFormContainer, otpStepContainer, newPwdStepContainer, signupOtpStepContainer]; |
| panels.forEach(p => { |
| if(p) { |
| p.classList.add('hidden'); |
| p.classList.remove('block'); |
| } |
| }); |
| const target = document.getElementById(panelId); |
| if (target) { |
| target.classList.remove('hidden'); |
| target.classList.add('block'); |
| } else { |
| console.error("Auth panel not found:", panelId); |
| } |
| } |
|
|
| document.getElementById("showSignupBtn")?.addEventListener("click", () => showAuthPanel("signupFormContainer")); |
| document.getElementById("showForgotPwdBtn")?.addEventListener("click", () => showAuthPanel("forgotPwdFormContainer")); |
| document.getElementById("backToLoginBtn1")?.addEventListener("click", () => showAuthPanel("loginFormContainer")); |
| document.getElementById("backToLoginBtn2")?.addEventListener("click", () => showAuthPanel("loginFormContainer")); |
| document.getElementById("backToForgotBtn")?.addEventListener("click", () => showAuthPanel("forgotPwdFormContainer")); |
| document.getElementById("backToSignupBtn")?.addEventListener("click", () => showAuthPanel("signupFormContainer")); |
|
|
| |
| const countries = [ |
| "India", "Afghanistan", "Albania", "Algeria", "Andorra", "Angola", "Antigua and Barbuda", |
| "Argentina", "Armenia", "Australia", "Austria", "Azerbaijan", "Bahamas", "Bahrain", "Bangladesh", |
| "Barbados", "Belarus", "Belgium", "Belize", "Benin", "Bhutan", "Bolivia", |
| "Bosnia and Herzegovina", "Botswana", "Brazil", "Brunei", "Bulgaria", "Burkina Faso", |
| "Burundi", "Côte d'Ivoire", "Cabo Verde", "Cambodia", "Cameroon", "Canada", "Central African Republic", |
| "Chad", "Chile", "China", "Colombia", "Comoros", "Congo (Congo-Brazzaville)", "Costa Rica", |
| "Croatia", "Cuba", "Cyprus", "Czechia (Czech Republic)", "Democratic Republic of the Congo", |
| "Denmark", "Djibouti", "Dominica", "Dominican Republic", "Ecuador", "Egypt", "El Salvador", |
| "Equatorial Guinea", "Eritrea", "Estonia", "Eswatini", "Ethiopia", "Fiji", "Finland", |
| "France", "Gabon", "Gambia", "Georgia", "Germany", "Ghana", "Greece", "Grenada", |
| "Guatemala", "Guinea", "Guinea-Bissau", "Guyana", "Haiti", "Holy See", "Honduras", |
| "Hungary", "Iceland", "Indonesia", "Iran", "Iraq", "Ireland", "Israel", "Italy", "Jamaica", |
| "Japan", "Jordan", "Kazakhstan", "Kenya", "Kiribati", "Kuwait", "Kyrgyzstan", "Laos", |
| "Latvia", "Lebanon", "Lesotho", "Liberia", "Libya", "Liechtenstein", "Lithuania", "Luxembourg", |
| "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands", "Mauritania", |
| "Mauritius", "Mexico", "Micronesia", "Moldova", "Monaco", "Mongolia", "Montenegro", "Morocco", |
| "Mozambique", "Myanmar (formerly Burma)", "Namibia", "Nauru", "Nepal", "Netherlands", "New Zealand", |
| "Nicaragua", "Niger", "Nigeria", "North Korea", "North Macedonia", "Norway", "Oman", "Pakistan", |
| "Palau", "Palestine State", "Panama", "Papua New Guinea", "Paraguay", "Peru", "Philippines", "Poland", |
| "Portugal", "Qatar", "Romania", "Russia", "Rwanda", "Saint Kitts and Nevis", "Saint Lucia", |
| "Saint Vincent and the Grenadines", "Samoa", "San Marino", "Sao Tome and Principe", "Saudi Arabia", |
| "Senegal", "Serbia", "Seychelles", "Sierra Leone", "Singapore", "Slovakia", "Slovenia", |
| "Solomon Islands", "Somalia", "South Africa", "South Korea", "South Sudan", "Spain", "Sri Lanka", |
| "Sudan", "Suriname", "Sweden", "Switzerland", "Syria", "Tajikistan", "Tanzania", "Thailand", |
| "Timor-Leste", "Togo", "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey", "Turkmenistan", |
| "Tuvalu", "Uganda", "Ukraine", "United Arab Emirates", "United Kingdom", "United States", "Uruguay", |
| "Uzbekistan", "Vanuatu", "Venezuela", "Vietnam", "Yemen", "Zambia", "Zimbabwe" |
| ]; |
| |
| const signupNationality = document.getElementById("signupNationality"); |
| const nationalityDropdown = document.getElementById("nationalityDropdown"); |
| |
| if (signupNationality && nationalityDropdown) { |
| function renderCountries(filterText = "") { |
| nationalityDropdown.innerHTML = ""; |
| const filtered = countries.filter(c => c.toLowerCase().includes(filterText.toLowerCase())); |
| filtered.forEach(country => { |
| const div = document.createElement("div"); |
| div.className = "p-2 hover:bg-moss/20 cursor-pointer text-sm text-ink"; |
| div.textContent = country; |
| div.onmousedown = (e) => { |
| e.preventDefault(); |
| signupNationality.value = country; |
| nationalityDropdown.classList.add("hidden"); |
| }; |
| nationalityDropdown.appendChild(div); |
| }); |
| if (filtered.length === 0) { |
| const div = document.createElement("div"); |
| div.className = "p-2 text-sm text-clay italic"; |
| div.textContent = "No matches"; |
| nationalityDropdown.appendChild(div); |
| } |
| } |
|
|
| signupNationality.addEventListener("focus", () => { |
| renderCountries(signupNationality.value); |
| nationalityDropdown.classList.remove("hidden"); |
| }); |
|
|
| signupNationality.addEventListener("input", (e) => { |
| renderCountries(e.target.value); |
| nationalityDropdown.classList.remove("hidden"); |
| }); |
|
|
| signupNationality.addEventListener("blur", () => { |
| nationalityDropdown.classList.add("hidden"); |
| }); |
| } |
|
|
| |
| function setupOtpInputStripper(fieldId) { |
| const field = document.getElementById(fieldId); |
| if (field) { |
| field.addEventListener('input', (e) => { |
| |
| e.target.value = e.target.value.replace(/\s/g, ''); |
| }); |
| } |
| } |
|
|
| |
| setupOtpInputStripper("signupOtpCode"); |
| setupOtpInputStripper("resetOtpCode"); |
| setupOtpInputStripper("deleteAccountOtpCode"); |
|
|
| document.getElementById("loginBtn")?.addEventListener("click", async () => { |
| const user = document.getElementById("loginUsername").value.trim(); |
| const pass = document.getElementById("loginPassword").value; |
| if (!user || !pass) return ui.showAlert("Login Error", "Please enter both username/email and password"); |
| |
| try { |
| document.getElementById("loginBtn").textContent = "Logging In..."; |
| await auth.login(user, pass); |
| document.getElementById("loginBtn").textContent = "Log In"; |
| await profiles.fetchPatients(); |
| } catch (e) { |
| ui.showAlert("Login Failed", e.message); |
| document.getElementById("loginBtn").textContent = "Log In"; |
| } |
| }); |
|
|
| document.getElementById("signupCompleteBtn")?.addEventListener("click", async () => { |
| const payload = { |
| name: document.getElementById("signupName").value.trim(), |
| username: document.getElementById("signupUsername").value.trim(), |
| email: document.getElementById("signupEmail").value.trim(), |
| password: document.getElementById("signupPassword").value, |
| gender: document.getElementById("signupGender").value, |
| age: parseInt(document.getElementById("signupAge").value) || null, |
| nationality: document.getElementById("signupNationality").value, |
| primary_concern: document.getElementById("signupConcern").value, |
| emergency_contact_name: document.getElementById("signupEmergencyName").value.trim() || null, |
| emergency_contact_phone: document.getElementById("signupEmergencyPhone").value.trim() || null |
| }; |
|
|
| if (!payload.name || !payload.username || !payload.email || !payload.password || !payload.gender || !payload.nationality || !payload.primary_concern) { |
| return ui.showAlert("Signup Error", "Please fill out all required fields (*)."); |
| } |
| |
| if (payload.password.length < 8) { |
| return ui.showAlert("Signup Error", "Password must be at least 8 characters long."); |
| } |
| |
| if (payload.age !== null && (payload.age < 5 || payload.age > 99)) { |
| return ui.showAlert("Signup Error", "Age must be between 5 and 99."); |
| } |
|
|
| try { |
| document.getElementById("signupCompleteBtn").textContent = "Creating Account..."; |
| document.getElementById("signupCompleteBtn").disabled = true; |
| |
| await auth.signup(payload); |
| |
| ui.showAlert("Success", "Account created successfully!"); |
| showAuthPanel("loginFormContainer"); |
| |
| await profiles.fetchPatients(); |
| } catch (e) { |
| ui.showAlert("Signup Failed", e.message); |
| } finally { |
| document.getElementById("signupCompleteBtn").textContent = "Create Account"; |
| document.getElementById("signupCompleteBtn").disabled = false; |
| } |
| }); |
|
|
| document.getElementById("submitResetBtn")?.addEventListener("click", async () => { |
| const email = document.getElementById("forgotPwdEmail").value.trim(); |
| if (!email) return ui.showAlert("Reset Password", "Please enter your email."); |
| const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; |
| if (!emailRegex.test(email)) return ui.showAlert("Reset Password", "Please enter a valid email address."); |
|
|
| const newPass = document.getElementById("resetNewPassword").value; |
| if (!newPass || newPass.length < 8) return ui.showAlert("Reset Password", "New password must be at least 8 characters long."); |
| try { |
| document.getElementById("submitResetBtn").textContent = "Updating..."; |
| document.getElementById("submitResetBtn").disabled = true; |
| await auth.resetPassword(email, newPass); |
| ui.showAlert("Success", "Password reset successfully! Please log in."); |
| showAuthPanel("loginFormContainer"); |
| } catch (e) { |
| ui.showAlert("Reset Failed", e.message); |
| } finally { |
| document.getElementById("submitResetBtn").textContent = "Update Password"; |
| document.getElementById("submitResetBtn").disabled = false; |
| } |
| }); |
|
|
| |
|
|
| const patientDropdown = document.getElementById("patientDropdown"); |
| if (patientDropdown) patientDropdown.onchange = profiles.toggleSelectButton; |
|
|
| const dashboardLogoutBtn = document.getElementById("dashboardLogoutBtn"); |
| if (dashboardLogoutBtn) { |
| dashboardLogoutBtn.onclick = () => { |
| auth.logout(); |
| ui.switchScreen("authScreen"); |
| }; |
| } |
|
|
| const dashboardResetBtn = document.getElementById("dashboardResetBtn"); |
| if (dashboardResetBtn) { |
| dashboardResetBtn.onclick = () => { |
| ui.showConfirm( |
| "Reset Profile?", |
| "Are you sure you want to reset this profile? All sessions, messages, and analysis will be permanently deleted. The patient record will remain.", |
| async () => { |
| try { |
| await api.resetPatientProfile(state.patientId); |
| } catch (e) { |
| console.error("Error resetting profile:", e); |
| } |
| document.getElementById("dashboardSessionsList").innerHTML = `<p class="text-clay text-sm italic">No previous sessions found.</p>`; |
| document.getElementById("dashboardDomains").innerHTML = ` |
| <h3 class="font-utility uppercase text-clay text-sm border-b border-ink/30 pb-2">Clinical Profile</h3> |
| <p class="text-clay text-sm italic mt-4">Profile has been reset.</p> |
| `; |
| } |
| ); |
| }; |
| } |
|
|
| const dashboardDeleteBtn = document.getElementById("dashboardDeleteBtn"); |
| if (dashboardDeleteBtn) { |
| dashboardDeleteBtn.onclick = () => { |
| document.getElementById("deleteAccountConfirmModal").style.display = "flex"; |
| }; |
| } |
|
|
| const deleteAccountCancelBtn = document.getElementById("deleteAccountCancelBtn"); |
| if (deleteAccountCancelBtn) { |
| deleteAccountCancelBtn.onclick = () => { |
| document.getElementById("deleteAccountConfirmModal").style.display = "none"; |
| }; |
| } |
|
|
| const deleteAccountConfirmBtn = document.getElementById("deleteAccountConfirmBtn"); |
| if (deleteAccountConfirmBtn) { |
| deleteAccountConfirmBtn.onclick = async () => { |
| try { |
| deleteAccountConfirmBtn.textContent = "Deleting..."; |
| deleteAccountConfirmBtn.disabled = true; |
| |
| await auth.deleteAccount(); |
| |
| document.getElementById("deleteAccountConfirmModal").style.display = "none"; |
| auth.logout(); |
| ui.switchScreen("authScreen"); |
| |
| } catch (e) { |
| console.error("Error deleting account:", e); |
| ui.showAlert("Error", e.message); |
| } finally { |
| deleteAccountConfirmBtn.textContent = "DELETE"; |
| deleteAccountConfirmBtn.disabled = false; |
| } |
| }; |
| } |
| const dashboardStartBtn = document.getElementById("dashboardStartBtn"); |
| if (dashboardStartBtn) { |
| dashboardStartBtn.onclick = async () => { |
| ui.switchScreen("loadingScreen", false); |
| startButton.textContent = "Checking Session..."; |
| startButton.disabled = true; |
|
|
| try { |
| const active = await api.getActiveSession(state.patientId); |
| if (active && active.session_id) { |
| ui.switchScreen("dashboardScreen"); |
| const modal = document.getElementById("continueSessionModal"); |
| modal.style.display = "flex"; |
|
|
| document.getElementById("continueSessionBtn").onclick = async () => { |
| modal.style.display = "none"; |
| ui.switchScreen("loadingScreen", false); |
| startButton.textContent = "Resuming Session..."; |
| await session.continueActualSession(active.session_id); |
| }; |
|
|
| document.getElementById("endAndStartNewBtn").onclick = async () => { |
| modal.style.display = "none"; |
| ui.switchScreen("loadingScreen", false); |
| startButton.textContent = "Loading Session..."; |
| state.setSessionId(null); |
| await api.endSessionReq(active.session_id, state.patientId); |
| await session.startActualSession(state.patientId); |
| }; |
| } else { |
| startButton.textContent = "Loading Session..."; |
| await session.startActualSession(state.patientId); |
| } |
| } catch (e) { |
| console.error(e); |
| startButton.textContent = "Loading Session..."; |
| await session.startActualSession(state.patientId); |
| } |
| }; |
| } |
|
|
| const selectProfileBtn = document.getElementById("selectProfileBtn"); |
| if (selectProfileBtn) { |
| selectProfileBtn.onclick = async () => { |
| state.setPatientId(patientDropdown.value); |
| await profiles.showDashboard(state.patientId); |
| }; |
| } |
|
|
| const createProfileBtn = document.getElementById("createProfileBtn"); |
| if (createProfileBtn) { |
| createProfileBtn.onclick = async () => { |
| const nameInput = document.getElementById("newPatientName"); |
| const ageInput = document.getElementById("newPatientAge"); |
| const genderInput = document.getElementById("newPatientGender"); |
| const occupationInput = document.getElementById("newPatientOccupation"); |
| const concernInput = document.getElementById("newPatientConcern"); |
| |
| const name = nameInput.value.trim(); |
| const age = ageInput.value; |
| const gender = genderInput ? genderInput.value : null; |
| const occupation = occupationInput ? occupationInput.value.trim() : null; |
|
|
| if (!name || !age || !gender || !occupation) { |
| ui.showAlert("Missing Information", "Please fill out all 4 required fields (Name, Age, Gender, and Occupation) to create a new profile."); |
| return; |
| } |
| |
| const parsedAge = parseInt(age); |
| if (isNaN(parsedAge) || parsedAge < 5 || parsedAge > 99) { |
| ui.showAlert("Invalid Age", "Age must be between 5 and 99."); |
| return; |
| } |
|
|
| try { |
| const data = { |
| name: name, |
| age: parseInt(age), |
| gender: gender, |
| occupation: occupation, |
| primary_concern: concernInput ? concernInput.value.trim() : null |
| }; |
| const pData = await api.createPatientProfile(data); |
| state.setPatientId(pData.patient_id); |
| await profiles.showDashboard(state.patientId); |
| } catch (e) { |
| console.error("Failed to create profile:", e); |
| ui.showAlert("Error", "Failed to create patient profile. Please try again."); |
| } |
| }; |
| } |
|
|
| if (startButton) { |
| startButton.onclick = async () => { |
| const text = startButton.textContent.trim(); |
| if (text.includes("Start Consultation")) { |
| if (!auth.isAuthenticated()) { |
| ui.switchScreen("authScreen", false); |
| return; |
| } |
| |
| startButton.textContent = "Loading..."; |
| startButton.disabled = true; |
| try { |
| await profiles.fetchPatients(); |
| } catch(e) { |
| console.error(e); |
| ui.showAlert("Error", "Failed to load dashboard. Check console."); |
| } finally { |
| startButton.textContent = "Start Consultation"; |
| startButton.disabled = false; |
| } |
| } else if (text.includes("Retry")) { |
| startButton.textContent = "Initializing..."; |
| startButton.disabled = true; |
| session.initializeSession(); |
| } |
| }; |
| } |
|
|
| |
|
|
| if (voiceModeBtn) { |
| voiceModeBtn.onclick = (e) => { |
| state.isVoiceMode = true; |
| voiceModeBtn.classList.add("active"); |
| textModeBtn.classList.remove("active"); |
| voiceInput.style.display = "flex"; |
| textInput.style.display = "none"; |
| |
| |
| if (!state.isAudioOutputEnabled) { |
| session.setAudioOutput(true); |
| } |
| e.currentTarget.blur(); |
| }; |
| } |
|
|
| if (textModeBtn) { |
| textModeBtn.onclick = (e) => { |
| state.isVoiceMode = false; |
| textModeBtn.classList.add("active"); |
| voiceModeBtn.classList.remove("active"); |
| voiceInput.style.display = "none"; |
| textInput.style.display = "block"; |
| chatInput.focus(); |
| }; |
| } |
|
|
| const voiceSelect = document.getElementById("voiceSelect"); |
| if (voiceSelect) { |
| voiceSelect.addEventListener('change', (e) => e.target.blur()); |
| } |
|
|
| const audioOutputToggleMobile = document.getElementById("audioOutputToggleMobile"); |
| if (audioOutputToggleMobile) { |
| audioOutputToggleMobile.addEventListener('change', (e) => { |
| session.setAudioOutput(e.target.checked); |
| e.target.blur(); |
| }); |
| } |
|
|
| const audioOutputToggleDesktop = document.getElementById("audioOutputToggleDesktop"); |
| if (audioOutputToggleDesktop) { |
| audioOutputToggleDesktop.addEventListener('click', (e) => { |
| session.setAudioOutput(!state.isAudioOutputEnabled); |
| e.currentTarget.blur(); |
| }); |
| } |
|
|
| if (sendBtn) { |
| sendBtn.onclick = () => { |
| const message = chatInput.value.trim(); |
| if (message) { |
| chatInput.value = ""; |
| session.primeTTS(); |
| session.sendTextMessage(message); |
| } |
| }; |
| } |
|
|
| if (chatInput) { |
| chatInput.onkeypress = (e) => { |
| if (e.key === 'Enter') sendBtn.onclick(); |
| }; |
| } |
|
|
| |
|
|
| if (micBtn) { |
| micBtn.addEventListener('pointerdown', (e) => { |
| e.preventDefault(); |
| if (!state.isVoiceMode || state.isAISpeaking) return; |
| if (!state.isListening && !state.isRecordingPending) { |
| micBtn.style.transform = 'scale(0.95)'; |
| audio.startRecording(session.sendTextMessage); |
| } |
| }); |
|
|
| const stopMicAction = (e) => { |
| e.preventDefault(); |
| if (state.isVoiceMode && (state.isListening || state.isRecordingPending)) { |
| micBtn.style.transform = 'none'; |
| session.primeTTS(); |
| audio.stopRecording(); |
| } |
| }; |
|
|
| micBtn.addEventListener('pointerup', stopMicAction); |
| micBtn.addEventListener('pointercancel', stopMicAction); |
| micBtn.addEventListener('pointerleave', stopMicAction); |
| } |
|
|
| |
| window.addEventListener('keydown', (e) => { |
| if (e.code !== 'Space' || e.repeat) return; |
| if (!state.isVoiceMode) return; |
| if (['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement.tagName)) return; |
| |
| e.preventDefault(); |
| e.stopPropagation(); |
| |
| if (state.isAISpeaking) return; |
| if (!state.isListening && !state.isRecordingPending) { |
| audio.startRecording(session.sendTextMessage); |
| } |
| }, { capture: true }); |
|
|
| window.addEventListener('keyup', (e) => { |
| if (e.code !== 'Space') return; |
| if (!state.isVoiceMode) return; |
| if (['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement.tagName)) return; |
| |
| e.preventDefault(); |
| e.stopPropagation(); |
| |
| session.primeTTS(); |
| audio.stopRecording(); |
| }, { capture: true }); |
|
|
| const endSessionBtn = document.getElementById("endSessionBtn"); |
| if (endSessionBtn) { |
| endSessionBtn.onclick = async () => { |
| if (!state.sessionId) return; |
|
|
| if (window.speechSynthesis.speaking) window.speechSynthesis.cancel(); |
| ui.setStatusText("Ending session..."); |
|
|
| try { |
| await api.endSessionReq(state.sessionId, state.patientId); |
| state.setSessionId(null); |
| window.location.href = "/"; |
| } catch (err) { |
| console.error("Failed to end session:", err); |
| state.setSessionId(null); |
| window.location.href = "/"; |
| } |
| }; |
| } |
| } |
|
|
| |
| api.pingHealth().catch(() => console.log("Wakeup ping sent.")); |
| audio.loadVoices(); |
| bindEvents(); |
| window.onload = () => session.initializeSession(); |
|
|