| 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 auth from './auth.js'; |
|
|
| const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); |
|
|
| |
| |
| |
| |
| |
| |
| |
| export async function handleAssistantResponses(message) { |
| if (!message) return; |
|
|
| const parts = message.split("&&").map(p => p.trim()).filter(Boolean); |
|
|
| ui.addMessage(parts[0], "assistant"); |
| await audio.speak(parts[0]); |
|
|
| if (parts[1]) { |
| ui.setStatusText("Assistant is thinking..."); |
| await sleep(500); |
|
|
| ui.addTyping(false, "Thinking..."); |
| await sleep(500); |
| ui.removeTyping(); |
|
|
| ui.addMessage(parts[1], "assistant"); |
| await audio.speak(parts[1]); |
| } |
| } |
|
|
| |
| |
| |
| export async function endSession() { |
| if (state.sessionInterval) { |
| clearInterval(state.sessionInterval); |
| state.sessionInterval = null; |
| } |
| state.setSessionId(null); |
| state.setPatientId(null); |
| ui.switchScreen("dashboard"); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export async function recordMessage(text) { |
| return await sendTextMessage(text); |
| } |
|
|
| export async function sendTextMessage(message, emotion = null) { |
| ui.addMessage(message, "user"); |
| ui.addTyping(false, "Analyzing..."); |
|
|
| try { |
| const data = await api.sendChatText(state.sessionId, state.patientId, message, emotion); |
| ui.removeTyping(); |
| |
| if (data.session_id && data.session_id !== state.sessionId) { |
| state.setSessionId(data.session_id); |
| } |
| |
| await handleAssistantResponses(data.assistant_message); |
|
|
| |
| |
| if (!state.isAudioOutputEnabled) { |
| ui.setStatusText(state.isVoiceMode ? "Hold Space to speak" : "Type your message"); |
| } |
|
|
| if (data.risk_flagged) { |
| ui.showSafetyBanner(); |
| } else { |
| ui.hideSafetyBanner(); |
| } |
| } catch (err) { |
| ui.removeTyping(); |
| if (err.message && (err.message.includes("Tokens Exhausted") || err.message.includes("429"))) { |
| ui.showAlert("Tokens Exhausted", "Your LLM provider tokens have been exhausted. Please wait or upgrade your plan."); |
| } else { |
| ui.addMessage("I apologize, but I'm having trouble connecting right now.", "assistant"); |
| } |
| console.error("Error:", err); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export async function startActualSession(pId) { |
| try { |
| const data = await api.startSessionReq(pId); |
| state.setSessionId(data.session_id); |
| state.setPatientId(data.patient_id); |
|
|
| ui.switchScreen("mainApp"); |
| document.getElementById("chat").innerHTML = ""; |
|
|
| state.sessionStartTime = Date.now(); |
| state.exchangeCount = 0; |
| ui.updateSessionStats(); |
| if (state.sessionInterval) clearInterval(state.sessionInterval); |
| state.sessionInterval = setInterval(ui.updateSessionStats, 60000); |
|
|
| handleAssistantResponses(data.assistant_message); |
| } catch (e) { |
| console.error("Failed to start session:", e); |
| ui.showAlert("Session Failed", e.message || "Failed to start session. Please try again."); |
| ui.switchScreen("dashboardScreen"); |
| const startButton = document.getElementById("dashboardStartBtn"); |
| if (startButton) { |
| startButton.textContent = "START CONSULTATION"; |
| startButton.disabled = false; |
| } |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| export async function continueActualSession(sessionId) { |
| try { |
| state.setSessionId(sessionId); |
| ui.switchScreen("mainApp"); |
| |
| document.getElementById("chat").innerHTML = ""; |
| |
| const data = await api.getSessionMessages(sessionId); |
| if (data.messages) { |
| data.messages.forEach(msg => { |
| if (msg.role !== "system") { |
| if (msg.role === 'assistant') { |
| const parts = msg.content.split("&&").map(p => p.trim()).filter(Boolean); |
| parts.forEach(part => ui.addMessage(part, 'assistant')); |
| } else { |
| ui.addMessage(msg.content, msg.role); |
| } |
| } |
| }); |
| } |
|
|
| state.sessionStartTime = Date.now(); |
| state.exchangeCount = Math.floor((data.messages || []).length / 2); |
| ui.updateSessionStats(); |
| if (state.sessionInterval) clearInterval(state.sessionInterval); |
| state.sessionInterval = setInterval(ui.updateSessionStats, 60000); |
| } catch (e) { |
| console.error("Failed to continue session:", e); |
| alert("Failed to continue session. Please try again."); |
| location.reload(); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export async function initializeSession(retryCount = 0) { |
| const startButton = document.getElementById("startButton"); |
| const wakeupText = document.getElementById("wakeupText"); |
| |
| if (retryCount === 0) { |
| if (wakeupText) wakeupText.textContent = "Waking up backend (this may take up to a minute)..."; |
| if (startButton) { |
| startButton.disabled = false; |
| startButton.textContent = "Start Consultation"; |
| } |
| } |
| try { |
| await api.pingHealth(); |
| document.getElementById("loadingDots").style.display = "none"; |
| if (wakeupText) wakeupText.style.display = "none"; |
|
|
| if (startButton) { |
| startButton.disabled = false; |
| startButton.textContent = "Start Consultation"; |
| } |
|
|
| |
| if (window.location.hash) { |
| const hash = window.location.hash.substring(1); |
| let newPath = "/"; |
| if (hash === "dashboardScreen") newPath = "/dashboard"; |
| else if (hash === "mainApp") newPath = "/session"; |
| |
| if (newPath !== "/") { |
| window.history.replaceState(null, "", newPath); |
| } |
| } |
|
|
| |
| const path = window.location.pathname; |
| if (path === "/dashboard") { |
| |
| await profiles.fetchPatients(); |
| } else if (path === "/session") { |
| if (!state.sessionId) { |
| window.location.href = "/"; |
| } else { |
| ui.switchScreen("mainApp", false); |
| } |
| } else { |
| |
| } |
| } catch (error) { |
| console.error(`Backend not ready (attempt ${retryCount + 1}):`, error); |
| if (retryCount < 15) { |
| const delay = Math.min(2000 * Math.pow(1.3, retryCount), 30000); |
| setTimeout(() => initializeSession(retryCount + 1), delay); |
| } else { |
| document.getElementById("loadingDots").style.display = "none"; |
| if (startButton) { |
| startButton.disabled = false; |
| startButton.textContent = "Retry Connection"; |
| } |
| } |
| } |
| } |
|
|
| |
| |
| |
| |
| export function primeTTS() { |
| if (state.isAudioOutputEnabled && window.speechSynthesis) { |
| const primer = new SpeechSynthesisUtterance(' '); |
| primer.volume = 0; |
| primer.rate = 10; |
| window.speechSynthesis.speak(primer); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| export function setAudioOutput(enabled) { |
| state.isAudioOutputEnabled = enabled; |
| |
| const mobileToggle = document.getElementById("audioOutputToggleMobile"); |
| const track = document.getElementById("audioToggleTrack"); |
| const dot = document.getElementById("audioToggleDot"); |
| const iconMobile = document.getElementById("audioToggleIconMobile"); |
| |
| const desktopToggle = document.getElementById("audioOutputToggleDesktop"); |
| const labelDesktop = document.getElementById("audioToggleLabelDesktop"); |
| |
| if (mobileToggle && mobileToggle.checked !== enabled) { |
| mobileToggle.checked = enabled; |
| } |
| |
| if (enabled) { |
| |
| if (window.speechSynthesis) { |
| window.speechSynthesis.getVoices(); |
| const primer = new SpeechSynthesisUtterance(' '); |
| primer.volume = 0; |
| primer.rate = 10; |
| window.speechSynthesis.speak(primer); |
| } |
| |
| |
| if (track) { track.classList.remove("bg-ink/30"); track.classList.add("bg-moss"); } |
| if (dot) { dot.classList.add("translate-x-4"); } |
| if (iconMobile) { iconMobile.classList.remove("text-ink/50"); iconMobile.classList.add("text-moss"); } |
| |
| |
| if (desktopToggle) { |
| desktopToggle.classList.add("bg-moss", "border-moss", "text-paper"); |
| desktopToggle.classList.remove("text-ink", "border-ink"); |
| } |
| if (labelDesktop) labelDesktop.textContent = "Audio On"; |
| |
| } else { |
| if (window.speechSynthesis && window.speechSynthesis.speaking) { |
| window.speechSynthesis.cancel(); |
| } |
| state.isAISpeaking = false; |
| |
| |
| if (track) { track.classList.remove("bg-moss"); track.classList.add("bg-ink/30"); } |
| if (dot) { dot.classList.remove("translate-x-4"); } |
| if (iconMobile) { iconMobile.classList.remove("text-moss"); iconMobile.classList.add("text-ink/50"); } |
| |
| |
| if (desktopToggle) { |
| desktopToggle.classList.remove("bg-moss", "border-moss", "text-paper"); |
| desktopToggle.classList.add("text-ink", "border-ink"); |
| } |
| if (labelDesktop) labelDesktop.textContent = "Audio Off"; |
| } |
| } |
|
|