Amogh1221 commited on
Commit
5c3dc21
·
verified ·
1 Parent(s): edf601e

Delete static

Browse files
static/css/style.css DELETED
@@ -1,277 +0,0 @@
1
- @import url('https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300..900;1,9..144,300..900&family=IBM+Plex+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&family=Public+Sans:ital,wght@0,100..900;1,100..900&display=swap');
2
-
3
- :root {
4
- /* Colors */
5
- --color-paper: #F6F3EE;
6
- --color-ink: #262521;
7
- --color-moss: #4B6358;
8
- --color-clay: #9C7A5B;
9
- --color-signal: #B4483C;
10
-
11
- /* Typography */
12
- --font-display: 'Fraunces', serif;
13
- --font-body: 'Public Sans', sans-serif;
14
- --font-utility: 'IBM Plex Mono', monospace;
15
- }
16
-
17
- body {
18
- background-color: var(--color-paper);
19
- color: var(--color-ink);
20
- font-family: var(--font-body);
21
- }
22
-
23
- /* Base resets to clear generic Tailwind */
24
- h1,
25
- h2,
26
- h3,
27
- h4,
28
- h5,
29
- h6 {
30
- font-family: var(--font-display);
31
- }
32
-
33
- .font-display {
34
- font-family: var(--font-display);
35
- }
36
-
37
- .font-utility {
38
- font-family: var(--font-utility);
39
- }
40
-
41
- .font-body {
42
- font-family: var(--font-body);
43
- }
44
-
45
- /* Remove previous animations and float effects */
46
- .fade-in {
47
- animation: fadeIn 0.15s ease-out;
48
- }
49
-
50
- @keyframes fadeIn {
51
- from {
52
- opacity: 0;
53
- transform: translateY(4px);
54
- }
55
-
56
- to {
57
- opacity: 1;
58
- transform: translateY(0);
59
- }
60
- }
61
-
62
- @media (prefers-reduced-motion: reduce) {
63
- .fade-in {
64
- animation: none;
65
- }
66
- }
67
-
68
- .custom-scrollbar::-webkit-scrollbar {
69
- width: 6px;
70
- }
71
-
72
- .custom-scrollbar::-webkit-scrollbar-track {
73
- background: var(--color-paper);
74
- }
75
-
76
- .custom-scrollbar::-webkit-scrollbar-thumb {
77
- background: var(--color-clay);
78
- border-radius: 3px;
79
- }
80
-
81
- .custom-scrollbar::-webkit-scrollbar-thumb:hover {
82
- background: var(--color-moss);
83
- }
84
-
85
- /* Loading Screen */
86
- #loadingScreen {
87
- position: fixed;
88
- top: 0;
89
- left: 0;
90
- width: 100%;
91
- height: 100%;
92
- background: var(--color-paper);
93
- display: flex;
94
- flex-direction: column;
95
- align-items: center;
96
- justify-content: center;
97
- z-index: 9999;
98
- transition: opacity 0.5s ease-out;
99
- }
100
-
101
- #loadingScreen.hidden {
102
- opacity: 0;
103
- pointer-events: none;
104
- }
105
-
106
- .logo-text {
107
- font-family: var(--font-display);
108
- font-size: clamp(3rem, 10vw, 5rem);
109
- font-weight: 400;
110
- color: var(--color-ink);
111
- margin-bottom: 1rem;
112
- }
113
-
114
- .loading-dots {
115
- display: flex;
116
- gap: 0.5rem;
117
- margin-top: 1rem;
118
- }
119
-
120
- .loading-dots span {
121
- width: 8px;
122
- height: 8px;
123
- border-radius: 50%;
124
- background: var(--color-ink);
125
- animation: breathe 2s infinite ease-in-out both;
126
- }
127
-
128
- @keyframes breathe {
129
-
130
- 0%,
131
- 100% {
132
- transform: scale(0.8);
133
- opacity: 0.3;
134
- }
135
-
136
- 50% {
137
- transform: scale(1.2);
138
- opacity: 0.8;
139
- }
140
- }
141
-
142
- /* Panels */
143
- .panel {
144
- background: var(--color-paper);
145
- border: 1px solid var(--color-ink);
146
- box-shadow: none;
147
- }
148
-
149
- /* Chat Input */
150
- .chat-input-container {
151
- display: flex;
152
- gap: 0.5rem;
153
- align-items: center;
154
- background: transparent;
155
- padding: 0.75rem;
156
- border: 1px solid var(--color-ink);
157
- border-radius: 0;
158
- }
159
-
160
- .chat-input {
161
- flex: 1;
162
- background: transparent;
163
- border: none;
164
- outline: none;
165
- color: var(--color-ink);
166
- font-family: var(--font-body);
167
- font-size: 0.95rem;
168
- padding: 0.5rem 1rem;
169
- }
170
-
171
- .chat-input::placeholder {
172
- color: var(--color-clay);
173
- }
174
-
175
- /* Buttons */
176
- .btn-primary {
177
- background: var(--color-moss);
178
- color: var(--color-paper);
179
- border: 1px solid var(--color-moss);
180
- padding: 0.5rem 1rem;
181
- font-family: var(--font-body);
182
- cursor: pointer;
183
- transition: background 0.2s ease, color 0.2s ease;
184
- }
185
-
186
- .btn-primary:hover {
187
- background: transparent;
188
- color: var(--color-moss);
189
- }
190
-
191
- .btn-primary:disabled {
192
- opacity: 0.5;
193
- cursor: not-allowed;
194
- }
195
-
196
- .btn-toggle {
197
- background: transparent;
198
- color: var(--color-ink);
199
- border: none;
200
- border-bottom: 2px solid transparent;
201
- padding: 0.5rem;
202
- font-family: var(--font-body);
203
- cursor: pointer;
204
- }
205
-
206
- .btn-toggle.active {
207
- border-bottom-color: var(--color-moss);
208
- font-weight: 600;
209
- }
210
-
211
- /* Chat Messages */
212
- .message-user {
213
- text-align: left;
214
- font-family: var(--font-body);
215
- color: var(--color-ink);
216
- padding: 1rem 0;
217
- }
218
-
219
- .message-assistant {
220
- text-align: left;
221
- font-family: var(--font-body);
222
- color: var(--color-ink);
223
- padding: 1rem 0;
224
- }
225
-
226
- .assistant-label {
227
- font-family: var(--font-utility);
228
- color: var(--color-clay);
229
- font-size: 0.8rem;
230
- text-transform: uppercase;
231
- letter-spacing: 0.05em;
232
- display: block;
233
- margin-bottom: 0.5rem;
234
- }
235
-
236
- .safety-banner {
237
- background: var(--color-paper);
238
- border: 1px solid var(--color-signal);
239
- color: var(--color-ink);
240
- padding: 1rem;
241
- margin: 1rem 0;
242
- }
243
-
244
- .safety-banner-title {
245
- color: var(--color-signal);
246
- font-family: var(--font-display);
247
- margin-bottom: 0.5rem;
248
- }
249
-
250
- .typing-indicator span {
251
- display: inline-block;
252
- width: 6px;
253
- height: 6px;
254
- border-radius: 50%;
255
- background: var(--color-ink);
256
- animation: breathe 1.4s infinite;
257
- }
258
-
259
- .typing-indicator span:nth-child(2) {
260
- animation-delay: 0.2s;
261
- }
262
-
263
- .typing-indicator span:nth-child(3) {
264
- animation-delay: 0.4s;
265
- }
266
-
267
- .dashboard-label {
268
- font-family: var(--font-utility);
269
- color: var(--color-clay);
270
- font-size: 0.85rem;
271
- text-transform: uppercase;
272
- }
273
-
274
- .dashboard-value {
275
- font-family: var(--font-body);
276
- color: var(--color-ink);
277
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
static/icons/logo.svg DELETED
static/js/api.js DELETED
@@ -1,147 +0,0 @@
1
- export const BACKEND_URL = "https://amogh1221-serinity.hf.space";
2
-
3
- // Helper to get token
4
- export function getToken() {
5
- return localStorage.getItem('serinity_token');
6
- }
7
-
8
- // Global fetch wrapper for auth
9
- export async function authFetch(url, options = {}) {
10
- const token = getToken();
11
- const headers = { ...options.headers };
12
-
13
- if (token) {
14
- headers['Authorization'] = `Bearer ${token}`;
15
- }
16
-
17
- // Don't override FormData headers
18
- if (!(options.body instanceof FormData) && !headers['Content-Type']) {
19
- headers['Content-Type'] = 'application/json';
20
- }
21
-
22
- const res = await fetch(url, { ...options, headers });
23
-
24
- // Handle unauthorized globaly
25
- if (res.status === 401) {
26
- localStorage.removeItem('serinity_token');
27
- window.dispatchEvent(new Event('unauthorized'));
28
- throw new Error('Unauthorized');
29
- }
30
-
31
- return res;
32
- }
33
-
34
- /**
35
- * Fetches the list of patients associated with the logged-in user.
36
- * @returns {Promise<Array>} List of patient objects.
37
- */
38
- export async function fetchPatientsList() {
39
- const res = await authFetch(`${BACKEND_URL}/patients`, { cache: "no-store" });
40
- return res.json();
41
- }
42
-
43
- /**
44
- * Loads the dashboard data for a specific patient.
45
- * @param {string} patientId
46
- * @returns {Promise<Object>} Dashboard overview data.
47
- */
48
- export async function loadDashboard(patientId) {
49
- const res = await authFetch(`${BACKEND_URL}/patients/${patientId}/dashboard`, { cache: "no-store" });
50
- if (!res.ok) throw new Error("Dashboard load failed");
51
- return res.json();
52
- }
53
-
54
- /**
55
- * Resets a patient's historical profile and sessions.
56
- * @param {string} patientId
57
- */
58
- export async function resetPatientProfile(patientId) {
59
- return authFetch(`${BACKEND_URL}/patients/${patientId}/reset`, { method: "POST" });
60
- }
61
-
62
- /**
63
- * Deletes a patient profile completely.
64
- * @param {string} patientId
65
- */
66
- export async function deletePatientProfile(patientId) {
67
- return authFetch(`${BACKEND_URL}/patients/${patientId}`, { method: "DELETE" });
68
- }
69
-
70
- /**
71
- * Creates a new patient profile linked to the user.
72
- * @param {Object} data
73
- * @returns {Promise<Object>} The created patient data.
74
- */
75
- export async function createPatientProfile(data) {
76
- const res = await authFetch(`${BACKEND_URL}/patients/create`, {
77
- method: "POST",
78
- body: JSON.stringify(data)
79
- });
80
- return res.json();
81
- }
82
-
83
- export async function pingHealth() {
84
- // health doesn't need auth, but authFetch is safe to use
85
- return fetch(`${BACKEND_URL}/health`);
86
- }
87
-
88
- export async function startSessionReq(patientId) {
89
- const res = await authFetch(`${BACKEND_URL}/start`, {
90
- method: "POST",
91
- body: JSON.stringify({ patient_id: patientId })
92
- });
93
- return res.json();
94
- }
95
-
96
- export async function sendChatText(sessionId, patientId, message, emotion) {
97
- const res = await authFetch(`${BACKEND_URL}/chat_text`, {
98
- method: "POST",
99
- body: JSON.stringify({
100
- message: message,
101
- session_id: sessionId,
102
- patient_id: patientId,
103
- emotion: emotion
104
- })
105
- });
106
- if (!res.ok) {
107
- let errorDetail = `HTTP Error ${res.status}`;
108
- try {
109
- const data = await res.json();
110
- errorDetail = data.detail || errorDetail;
111
- } catch(e) {}
112
- throw new Error(errorDetail);
113
- }
114
- return res.json();
115
- }
116
-
117
- export async function transcribeAudio(audioBlob) {
118
- const formData = new FormData();
119
- formData.append('audio', audioBlob, 'recording.webm');
120
-
121
- // Custom headers because authFetch handles FormData correctly if Content-Type is omitted
122
- const res = await authFetch(`${BACKEND_URL}/transcribe`, {
123
- method: "POST",
124
- body: formData
125
- });
126
- return res.json();
127
- }
128
-
129
- export async function endSessionReq(sessionId, patientId) {
130
- return authFetch(`${BACKEND_URL}/end_session`, {
131
- method: "POST",
132
- body: JSON.stringify({
133
- session_id: sessionId,
134
- patient_id: patientId
135
- })
136
- });
137
- }
138
-
139
- export async function getActiveSession(patientId) {
140
- const res = await authFetch(`${BACKEND_URL}/patients/${patientId}/active_session`, { cache: "no-store" });
141
- return res.json();
142
- }
143
-
144
- export async function getSessionMessages(sessionId) {
145
- const res = await authFetch(`${BACKEND_URL}/sessions/${sessionId}/messages`, { cache: "no-store" });
146
- return res.json();
147
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
static/js/app.js DELETED
@@ -1,717 +0,0 @@
1
- import { state } from './state.js';
2
- import * as api from './api.js';
3
- import * as ui from './ui.js';
4
- import * as audio from './audio.js';
5
- import * as profiles from './profiles.js';
6
- import * as session from './session.js';
7
- import * as auth from './auth.js';
8
-
9
- // DOM Elements specific to top-level app logic
10
- const startButton = document.getElementById("startButton");
11
- const voiceModeBtn = document.getElementById("voiceModeBtn");
12
- const textModeBtn = document.getElementById("textModeBtn");
13
- const voiceInput = document.getElementById("voiceInput");
14
- const textInput = document.getElementById("textInput");
15
- const chatInput = document.getElementById("chatInput");
16
- const sendBtn = document.getElementById("sendBtn");
17
- const micBtn = document.getElementById("micBtn");
18
-
19
- /**
20
- * Binds all global events to the DOM (button clicks, form submits, keypresses).
21
- */
22
- function bindEvents() {
23
- // Handle browser back/forward buttons
24
- window.addEventListener("popstate", (event) => {
25
- if (event.state && event.state.screen) {
26
- ui.switchScreen(event.state.screen, false);
27
- } else {
28
- const path = window.location.pathname;
29
- if (path === "/dashboard" || path === "/session") {
30
- if (!state.patientId) {
31
- profiles.fetchPatients();
32
- } else {
33
- ui.switchScreen(path === "/dashboard" ? "dashboardScreen" : "mainApp", false);
34
- }
35
- } else {
36
- ui.switchScreen("loadingScreen", false);
37
- }
38
- }
39
- });
40
-
41
- // Global Unauthorized Handler
42
- window.addEventListener("unauthorized", () => {
43
- ui.switchScreen("authScreen", false);
44
- });
45
-
46
- // --- Auth Events ---
47
- const loginFormContainer = document.getElementById("loginFormContainer");
48
- const signupFormContainer = document.getElementById("signupFormContainer");
49
- const forgotPwdFormContainer = document.getElementById("forgotPwdFormContainer");
50
- const otpStepContainer = document.getElementById("otpStepContainer");
51
- const newPwdStepContainer = document.getElementById("newPwdStepContainer");
52
- const signupOtpStepContainer = document.getElementById("signupOtpStepContainer");
53
-
54
- // Track verified OTP state in memory (not persisted — refresh = back to login)
55
- let _otpVerifiedEmail = null;
56
-
57
- function showAuthPanel(panelId) {
58
- // Only hide/show panels that exist
59
- const panels = [loginFormContainer, signupFormContainer, forgotPwdFormContainer, otpStepContainer, newPwdStepContainer, signupOtpStepContainer];
60
- panels.forEach(p => {
61
- if(p) {
62
- p.classList.add('hidden');
63
- p.classList.remove('block');
64
- }
65
- });
66
- const target = document.getElementById(panelId);
67
- if (target) {
68
- target.classList.remove('hidden');
69
- target.classList.add('block');
70
- } else {
71
- console.error("Auth panel not found:", panelId);
72
- }
73
- }
74
-
75
- document.getElementById("showSignupBtn")?.addEventListener("click", () => showAuthPanel("signupFormContainer"));
76
- document.getElementById("showForgotPwdBtn")?.addEventListener("click", () => showAuthPanel("forgotPwdFormContainer"));
77
- document.getElementById("backToLoginBtn1")?.addEventListener("click", () => showAuthPanel("loginFormContainer"));
78
- document.getElementById("backToLoginBtn2")?.addEventListener("click", () => showAuthPanel("loginFormContainer"));
79
- document.getElementById("backToForgotBtn")?.addEventListener("click", () => showAuthPanel("forgotPwdFormContainer"));
80
- document.getElementById("backToSignupBtn")?.addEventListener("click", () => showAuthPanel("signupFormContainer"));
81
-
82
- // Populate Nationality Dropdown
83
- const countries = [
84
- "India", "Afghanistan", "Albania", "Algeria", "Andorra", "Angola", "Antigua and Barbuda",
85
- "Argentina", "Armenia", "Australia", "Austria", "Azerbaijan", "Bahamas", "Bahrain", "Bangladesh",
86
- "Barbados", "Belarus", "Belgium", "Belize", "Benin", "Bhutan", "Bolivia",
87
- "Bosnia and Herzegovina", "Botswana", "Brazil", "Brunei", "Bulgaria", "Burkina Faso",
88
- "Burundi", "Côte d'Ivoire", "Cabo Verde", "Cambodia", "Cameroon", "Canada", "Central African Republic",
89
- "Chad", "Chile", "China", "Colombia", "Comoros", "Congo (Congo-Brazzaville)", "Costa Rica",
90
- "Croatia", "Cuba", "Cyprus", "Czechia (Czech Republic)", "Democratic Republic of the Congo",
91
- "Denmark", "Djibouti", "Dominica", "Dominican Republic", "Ecuador", "Egypt", "El Salvador",
92
- "Equatorial Guinea", "Eritrea", "Estonia", "Eswatini", "Ethiopia", "Fiji", "Finland",
93
- "France", "Gabon", "Gambia", "Georgia", "Germany", "Ghana", "Greece", "Grenada",
94
- "Guatemala", "Guinea", "Guinea-Bissau", "Guyana", "Haiti", "Holy See", "Honduras",
95
- "Hungary", "Iceland", "Indonesia", "Iran", "Iraq", "Ireland", "Israel", "Italy", "Jamaica",
96
- "Japan", "Jordan", "Kazakhstan", "Kenya", "Kiribati", "Kuwait", "Kyrgyzstan", "Laos",
97
- "Latvia", "Lebanon", "Lesotho", "Liberia", "Libya", "Liechtenstein", "Lithuania", "Luxembourg",
98
- "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands", "Mauritania",
99
- "Mauritius", "Mexico", "Micronesia", "Moldova", "Monaco", "Mongolia", "Montenegro", "Morocco",
100
- "Mozambique", "Myanmar (formerly Burma)", "Namibia", "Nauru", "Nepal", "Netherlands", "New Zealand",
101
- "Nicaragua", "Niger", "Nigeria", "North Korea", "North Macedonia", "Norway", "Oman", "Pakistan",
102
- "Palau", "Palestine State", "Panama", "Papua New Guinea", "Paraguay", "Peru", "Philippines", "Poland",
103
- "Portugal", "Qatar", "Romania", "Russia", "Rwanda", "Saint Kitts and Nevis", "Saint Lucia",
104
- "Saint Vincent and the Grenadines", "Samoa", "San Marino", "Sao Tome and Principe", "Saudi Arabia",
105
- "Senegal", "Serbia", "Seychelles", "Sierra Leone", "Singapore", "Slovakia", "Slovenia",
106
- "Solomon Islands", "Somalia", "South Africa", "South Korea", "South Sudan", "Spain", "Sri Lanka",
107
- "Sudan", "Suriname", "Sweden", "Switzerland", "Syria", "Tajikistan", "Tanzania", "Thailand",
108
- "Timor-Leste", "Togo", "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey", "Turkmenistan",
109
- "Tuvalu", "Uganda", "Ukraine", "United Arab Emirates", "United Kingdom", "United States", "Uruguay",
110
- "Uzbekistan", "Vanuatu", "Venezuela", "Vietnam", "Yemen", "Zambia", "Zimbabwe"
111
- ];
112
-
113
- const signupNationality = document.getElementById("signupNationality");
114
- const nationalityDropdown = document.getElementById("nationalityDropdown");
115
-
116
- if (signupNationality && nationalityDropdown) {
117
- function renderCountries(filterText = "") {
118
- nationalityDropdown.innerHTML = "";
119
- const filtered = countries.filter(c => c.toLowerCase().includes(filterText.toLowerCase()));
120
- filtered.forEach(country => {
121
- const div = document.createElement("div");
122
- div.className = "p-2 hover:bg-moss/20 cursor-pointer text-sm text-ink";
123
- div.textContent = country;
124
- div.onmousedown = (e) => {
125
- e.preventDefault(); // prevent blur
126
- signupNationality.value = country;
127
- nationalityDropdown.classList.add("hidden");
128
- };
129
- nationalityDropdown.appendChild(div);
130
- });
131
- if (filtered.length === 0) {
132
- const div = document.createElement("div");
133
- div.className = "p-2 text-sm text-clay italic";
134
- div.textContent = "No matches";
135
- nationalityDropdown.appendChild(div);
136
- }
137
- }
138
-
139
- signupNationality.addEventListener("focus", () => {
140
- renderCountries(signupNationality.value);
141
- nationalityDropdown.classList.remove("hidden");
142
- });
143
-
144
- signupNationality.addEventListener("input", (e) => {
145
- renderCountries(e.target.value);
146
- nationalityDropdown.classList.remove("hidden");
147
- });
148
-
149
- signupNationality.addEventListener("blur", () => {
150
- nationalityDropdown.classList.add("hidden");
151
- });
152
- }
153
-
154
- // Helper function to strip spaces from OTP input
155
- function setupOtpInputStripper(fieldId) {
156
- const field = document.getElementById(fieldId);
157
- if (field) {
158
- field.addEventListener('input', (e) => {
159
- // Remove all spaces from the input value
160
- e.target.value = e.target.value.replace(/\s/g, '');
161
- });
162
- }
163
- }
164
-
165
- // Setup OTP input strippers for all OTP fields (signup, forgot password, delete account)
166
- setupOtpInputStripper("signupOtpCode");
167
- setupOtpInputStripper("resetOtpCode");
168
- setupOtpInputStripper("deleteAccountOtpCode");
169
-
170
- document.getElementById("loginBtn")?.addEventListener("click", async () => {
171
- const user = document.getElementById("loginUsername").value.trim();
172
- const pass = document.getElementById("loginPassword").value;
173
- if (!user || !pass) return ui.showAlert("Login Error", "Please enter both username/email and password");
174
-
175
- try {
176
- document.getElementById("loginBtn").textContent = "Logging In...";
177
- await auth.login(user, pass);
178
- document.getElementById("loginBtn").textContent = "Log In";
179
- await profiles.fetchPatients();
180
- } catch (e) {
181
- ui.showAlert("Login Failed", e.message);
182
- document.getElementById("loginBtn").textContent = "Log In";
183
- }
184
- });
185
-
186
- let _signupPayload = null;
187
-
188
- document.getElementById("signupGetOtpBtn")?.addEventListener("click", async () => {
189
- const payload = {
190
- name: document.getElementById("signupName").value.trim(),
191
- username: document.getElementById("signupUsername").value.trim(),
192
- email: document.getElementById("signupEmail").value.trim(),
193
- password: document.getElementById("signupPassword").value,
194
- gender: document.getElementById("signupGender").value,
195
- age: parseInt(document.getElementById("signupAge").value) || null,
196
- nationality: document.getElementById("signupNationality").value,
197
- primary_concern: document.getElementById("signupConcern").value,
198
- emergency_contact_name: document.getElementById("signupEmergencyName").value.trim() || null,
199
- emergency_contact_phone: document.getElementById("signupEmergencyPhone").value.trim() || null
200
- };
201
-
202
- if (!payload.name || !payload.username || !payload.email || !payload.password || !payload.gender || !payload.nationality || !payload.primary_concern) {
203
- return ui.showAlert("Signup Error", "Please fill out all required fields (*).");
204
- }
205
-
206
- if (payload.password.length < 8) {
207
- return ui.showAlert("Signup Error", "Password must be at least 8 characters long.");
208
- }
209
-
210
- if (payload.age !== null && (payload.age < 5 || payload.age > 99)) {
211
- return ui.showAlert("Signup Error", "Age must be between 5 and 99.");
212
- }
213
-
214
- try {
215
- document.getElementById("signupGetOtpBtn").textContent = "Sending OTP...";
216
- document.getElementById("signupGetOtpBtn").disabled = true;
217
-
218
- await auth.requestSignupOtp(payload.email, payload.username);
219
-
220
- _signupPayload = payload;
221
-
222
- // Clear OTP code and switch to signup OTP panel
223
- const signupOtpCodeField = document.getElementById("signupOtpCode");
224
- if (signupOtpCodeField) {
225
- signupOtpCodeField.value = "";
226
- }
227
- showAuthPanel("signupOtpStepContainer");
228
-
229
- } catch (e) {
230
- ui.showAlert("Signup Failed", e.message);
231
- } finally {
232
- document.getElementById("signupGetOtpBtn").textContent = "Get OTP";
233
- document.getElementById("signupGetOtpBtn").disabled = false;
234
- }
235
- });
236
-
237
- document.getElementById("signupCompleteBtn")?.addEventListener("click", async () => {
238
- if (!_signupPayload) {
239
- return ui.showAlert("Signup Error", "Missing signup details. Please refresh and try again.");
240
- }
241
-
242
- const signupOtpCodeField = document.getElementById("signupOtpCode");
243
- if (!signupOtpCodeField) {
244
- return ui.showAlert("Signup Error", "OTP code field not found. Please refresh and try again.");
245
- }
246
-
247
- const otpCode = signupOtpCodeField.value.trim();
248
- if (otpCode.length !== 6) {
249
- return ui.showAlert("Signup Error", "Please enter the 6-digit OTP code.");
250
- }
251
-
252
- try {
253
- const completeBtn = document.getElementById("signupCompleteBtn");
254
- if (completeBtn) {
255
- completeBtn.textContent = "Creating Account...";
256
- completeBtn.disabled = true;
257
- }
258
-
259
- const payload = {
260
- ..._signupPayload,
261
- otp_code: otpCode
262
- };
263
-
264
- await auth.signup(payload);
265
-
266
- _signupPayload = null;
267
- if (signupOtpCodeField) {
268
- signupOtpCodeField.value = "";
269
- }
270
-
271
- // Account created successfully, go back to login
272
- ui.showAlert("Success", "Account created successfully!");
273
- showAuthPanel("loginFormContainer");
274
-
275
- await profiles.fetchPatients();
276
- } catch (e) {
277
- ui.showAlert("Signup Failed", e.message);
278
- } finally {
279
- const completeBtn = document.getElementById("signupCompleteBtn");
280
- if (completeBtn) {
281
- completeBtn.textContent = "Complete Signup";
282
- completeBtn.disabled = false;
283
- }
284
- }
285
- });
286
-
287
- document.getElementById("requestOtpBtn")?.addEventListener("click", async () => {
288
- const email = document.getElementById("forgotPwdEmail").value.trim();
289
- if (!email) return ui.showAlert("Reset Password", "Please enter your email.");
290
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
291
- if (!emailRegex.test(email)) return ui.showAlert("Reset Password", "Please enter a valid email address.");
292
- try {
293
- document.getElementById("requestOtpBtn").textContent = "Sending...";
294
- document.getElementById("requestOtpBtn").disabled = true;
295
- await auth.forgotPassword(email);
296
- document.getElementById("resetOtpCode").value = "";
297
- showAuthPanel("otpStepContainer");
298
- } catch(e) {
299
- ui.showAlert("Reset Password", e.message);
300
- } finally {
301
- document.getElementById("requestOtpBtn").textContent = "Send Code";
302
- document.getElementById("requestOtpBtn").disabled = false;
303
- }
304
- });
305
-
306
- document.getElementById("verifyOtpBtn")?.addEventListener("click", async () => {
307
- const email = document.getElementById("forgotPwdEmail").value.trim();
308
- const otp = document.getElementById("resetOtpCode").value.trim();
309
- if (otp.length !== 6) return ui.showAlert("Reset Password", "Please enter the 6-digit code.");
310
- try {
311
- document.getElementById("verifyOtpBtn").textContent = "Verifying...";
312
- document.getElementById("verifyOtpBtn").disabled = true;
313
- await auth.verifyOtp(email, otp);
314
- _otpVerifiedEmail = email;
315
- document.getElementById("resetNewPassword").value = "";
316
- showAuthPanel("newPwdStepContainer");
317
- } catch(e) {
318
- ui.showAlert("Verification Failed", e.message);
319
- } finally {
320
- document.getElementById("verifyOtpBtn").textContent = "Verify Code";
321
- document.getElementById("verifyOtpBtn").disabled = false;
322
- }
323
- });
324
-
325
- document.getElementById("submitResetBtn")?.addEventListener("click", async () => {
326
- // If _otpVerifiedEmail is null, this means a refresh happened — redirect to login
327
- if (!_otpVerifiedEmail) {
328
- showAuthPanel("loginFormContainer");
329
- return;
330
- }
331
- const newPass = document.getElementById("resetNewPassword").value;
332
- if (!newPass || newPass.length < 8) return ui.showAlert("Reset Password", "New password must be at least 8 characters long.");
333
- try {
334
- document.getElementById("submitResetBtn").textContent = "Updating...";
335
- document.getElementById("submitResetBtn").disabled = true;
336
- const otp = document.getElementById("resetOtpCode").value.trim();
337
- await auth.resetPassword(_otpVerifiedEmail, otp, newPass);
338
- _otpVerifiedEmail = null;
339
- ui.showAlert("Success", "Password reset successfully! Please log in.");
340
- showAuthPanel("loginFormContainer");
341
- } catch (e) {
342
- ui.showAlert("Reset Failed", e.message);
343
- } finally {
344
- document.getElementById("submitResetBtn").textContent = "Update Password";
345
- document.getElementById("submitResetBtn").disabled = false;
346
- }
347
- });
348
-
349
- // --- Profiles & Dashboard Events ---
350
-
351
- const patientDropdown = document.getElementById("patientDropdown");
352
- if (patientDropdown) patientDropdown.onchange = profiles.toggleSelectButton;
353
-
354
- const dashboardLogoutBtn = document.getElementById("dashboardLogoutBtn");
355
- if (dashboardLogoutBtn) {
356
- dashboardLogoutBtn.onclick = () => {
357
- auth.logout();
358
- ui.switchScreen("authScreen");
359
- };
360
- }
361
-
362
- const dashboardResetBtn = document.getElementById("dashboardResetBtn");
363
- if (dashboardResetBtn) {
364
- dashboardResetBtn.onclick = () => {
365
- ui.showConfirm(
366
- "Reset Profile?",
367
- "Are you sure you want to reset this profile? All sessions, messages, and analysis will be permanently deleted. The patient record will remain.",
368
- async () => {
369
- try {
370
- await api.resetPatientProfile(state.patientId);
371
- } catch (e) {
372
- console.error("Error resetting profile:", e);
373
- }
374
- document.getElementById("dashboardSessionsList").innerHTML = `<p class="text-clay text-sm italic">No previous sessions found.</p>`;
375
- document.getElementById("dashboardDomains").innerHTML = `
376
- <h3 class="font-utility uppercase text-clay text-sm border-b border-ink/30 pb-2">Clinical Profile</h3>
377
- <p class="text-clay text-sm italic mt-4">Profile has been reset.</p>
378
- `;
379
- }
380
- );
381
- };
382
- }
383
-
384
- const dashboardDeleteBtn = document.getElementById("dashboardDeleteBtn");
385
- if (dashboardDeleteBtn) {
386
- dashboardDeleteBtn.onclick = () => {
387
- document.getElementById("deleteAccountConfirmModal").style.display = "flex";
388
- };
389
- }
390
-
391
- const deleteAccountCancelBtn = document.getElementById("deleteAccountCancelBtn");
392
- if (deleteAccountCancelBtn) {
393
- deleteAccountCancelBtn.onclick = () => {
394
- document.getElementById("deleteAccountConfirmModal").style.display = "none";
395
- };
396
- }
397
-
398
- const deleteAccountSendOtpBtn = document.getElementById("deleteAccountSendOtpBtn");
399
- if (deleteAccountSendOtpBtn) {
400
- deleteAccountSendOtpBtn.onclick = async () => {
401
- try {
402
- deleteAccountSendOtpBtn.textContent = "Sending OTP...";
403
- deleteAccountSendOtpBtn.disabled = true;
404
-
405
- await auth.requestDeleteAccountOtp();
406
-
407
- // Close confirmation modal and show OTP modal
408
- document.getElementById("deleteAccountConfirmModal").style.display = "none";
409
- document.getElementById("deleteAccountOtpCode").value = "";
410
- document.getElementById("deleteAccountOtpModal").style.display = "flex";
411
-
412
- } catch (e) {
413
- console.error("Error requesting OTP:", e);
414
- ui.showAlert("Error", e.message);
415
- } finally {
416
- deleteAccountSendOtpBtn.textContent = "SEND OTP";
417
- deleteAccountSendOtpBtn.disabled = false;
418
- }
419
- };
420
- }
421
-
422
- const closeDeleteOtpModalBtn = document.getElementById("closeDeleteOtpModalBtn");
423
- if (closeDeleteOtpModalBtn) {
424
- closeDeleteOtpModalBtn.onclick = () => {
425
- document.getElementById("deleteAccountOtpModal").style.display = "none";
426
- };
427
- }
428
-
429
- const submitDeleteOtpBtn = document.getElementById("submitDeleteOtpBtn");
430
- if (submitDeleteOtpBtn) {
431
- submitDeleteOtpBtn.onclick = async () => {
432
- const code = document.getElementById("deleteAccountOtpCode").value.trim();
433
- if (code.length !== 6) return ui.showAlert("Error", "Please enter a valid 6-digit code.");
434
- try {
435
- submitDeleteOtpBtn.textContent = "Verifying...";
436
- submitDeleteOtpBtn.disabled = true;
437
-
438
- await auth.verifyDeleteAccount(code);
439
-
440
- document.getElementById("deleteAccountOtpModal").style.display = "none";
441
- auth.logout();
442
- ui.switchScreen("authScreen");
443
-
444
- } catch (e) {
445
- ui.showAlert("Error", e.message);
446
- } finally {
447
- submitDeleteOtpBtn.textContent = "Verify & Delete";
448
- submitDeleteOtpBtn.disabled = false;
449
- }
450
- };
451
- }
452
- const dashboardStartBtn = document.getElementById("dashboardStartBtn");
453
- if (dashboardStartBtn) {
454
- dashboardStartBtn.onclick = async () => {
455
- ui.switchScreen("loadingScreen", false);
456
- startButton.textContent = "Checking Session...";
457
- startButton.disabled = true;
458
-
459
- try {
460
- const active = await api.getActiveSession(state.patientId);
461
- if (active && active.session_id) {
462
- ui.switchScreen("dashboardScreen");
463
- const modal = document.getElementById("continueSessionModal");
464
- modal.style.display = "flex";
465
-
466
- document.getElementById("continueSessionBtn").onclick = async () => {
467
- modal.style.display = "none";
468
- ui.switchScreen("loadingScreen", false);
469
- startButton.textContent = "Resuming Session...";
470
- await session.continueActualSession(active.session_id);
471
- };
472
-
473
- document.getElementById("endAndStartNewBtn").onclick = async () => {
474
- modal.style.display = "none";
475
- ui.switchScreen("loadingScreen", false);
476
- startButton.textContent = "Loading Session...";
477
- state.setSessionId(null);
478
- await api.endSessionReq(active.session_id, state.patientId);
479
- await session.startActualSession(state.patientId);
480
- };
481
- } else {
482
- startButton.textContent = "Loading Session...";
483
- await session.startActualSession(state.patientId);
484
- }
485
- } catch (e) {
486
- console.error(e);
487
- startButton.textContent = "Loading Session...";
488
- await session.startActualSession(state.patientId);
489
- }
490
- };
491
- }
492
-
493
- const selectProfileBtn = document.getElementById("selectProfileBtn");
494
- if (selectProfileBtn) {
495
- selectProfileBtn.onclick = async () => {
496
- state.setPatientId(patientDropdown.value);
497
- await profiles.showDashboard(state.patientId);
498
- };
499
- }
500
-
501
- const createProfileBtn = document.getElementById("createProfileBtn");
502
- if (createProfileBtn) {
503
- createProfileBtn.onclick = async () => {
504
- const nameInput = document.getElementById("newPatientName");
505
- const ageInput = document.getElementById("newPatientAge");
506
- const genderInput = document.getElementById("newPatientGender");
507
- const occupationInput = document.getElementById("newPatientOccupation");
508
- const concernInput = document.getElementById("newPatientConcern");
509
-
510
- const name = nameInput.value.trim();
511
- const age = ageInput.value;
512
- const gender = genderInput ? genderInput.value : null;
513
- const occupation = occupationInput ? occupationInput.value.trim() : null;
514
-
515
- if (!name || !age || !gender || !occupation) {
516
- ui.showAlert("Missing Information", "Please fill out all 4 required fields (Name, Age, Gender, and Occupation) to create a new profile.");
517
- return;
518
- }
519
-
520
- const parsedAge = parseInt(age);
521
- if (isNaN(parsedAge) || parsedAge < 5 || parsedAge > 99) {
522
- ui.showAlert("Invalid Age", "Age must be between 5 and 99.");
523
- return;
524
- }
525
-
526
- try {
527
- const data = {
528
- name: name,
529
- age: parseInt(age),
530
- gender: gender,
531
- occupation: occupation,
532
- primary_concern: concernInput ? concernInput.value.trim() : null
533
- };
534
- const pData = await api.createPatientProfile(data);
535
- state.setPatientId(pData.patient_id);
536
- await profiles.showDashboard(state.patientId);
537
- } catch (e) {
538
- console.error("Failed to create profile:", e);
539
- ui.showAlert("Error", "Failed to create patient profile. Please try again.");
540
- }
541
- };
542
- }
543
-
544
- if (startButton) {
545
- startButton.onclick = async () => {
546
- const text = startButton.textContent.trim();
547
- if (text.includes("Start Consultation")) {
548
- if (!auth.isAuthenticated()) {
549
- ui.switchScreen("authScreen", false);
550
- return;
551
- }
552
-
553
- startButton.textContent = "Loading...";
554
- startButton.disabled = true;
555
- try {
556
- await profiles.fetchPatients();
557
- } catch(e) {
558
- console.error(e);
559
- ui.showAlert("Error", "Failed to load dashboard. Check console.");
560
- } finally {
561
- startButton.textContent = "Start Consultation";
562
- startButton.disabled = false;
563
- }
564
- } else if (text.includes("Retry")) {
565
- startButton.textContent = "Initializing...";
566
- startButton.disabled = true;
567
- session.initializeSession();
568
- }
569
- };
570
- }
571
-
572
- // --- Session UI Events (Toggle Text/Voice, etc) ---
573
-
574
- if (voiceModeBtn) {
575
- voiceModeBtn.onclick = (e) => {
576
- state.isVoiceMode = true;
577
- voiceModeBtn.classList.add("active");
578
- textModeBtn.classList.remove("active");
579
- voiceInput.style.display = "flex";
580
- textInput.style.display = "none";
581
-
582
- // Auto-enable audio output when switching to voice mode
583
- if (!state.isAudioOutputEnabled) {
584
- session.setAudioOutput(true);
585
- }
586
- e.currentTarget.blur();
587
- };
588
- }
589
-
590
- if (textModeBtn) {
591
- textModeBtn.onclick = (e) => {
592
- state.isVoiceMode = false;
593
- textModeBtn.classList.add("active");
594
- voiceModeBtn.classList.remove("active");
595
- voiceInput.style.display = "none";
596
- textInput.style.display = "block";
597
- chatInput.focus();
598
- };
599
- }
600
-
601
- const voiceSelect = document.getElementById("voiceSelect");
602
- if (voiceSelect) {
603
- voiceSelect.addEventListener('change', (e) => e.target.blur());
604
- }
605
-
606
- const audioOutputToggleMobile = document.getElementById("audioOutputToggleMobile");
607
- if (audioOutputToggleMobile) {
608
- audioOutputToggleMobile.addEventListener('change', (e) => {
609
- session.setAudioOutput(e.target.checked);
610
- e.target.blur();
611
- });
612
- }
613
-
614
- const audioOutputToggleDesktop = document.getElementById("audioOutputToggleDesktop");
615
- if (audioOutputToggleDesktop) {
616
- audioOutputToggleDesktop.addEventListener('click', (e) => {
617
- session.setAudioOutput(!state.isAudioOutputEnabled);
618
- e.currentTarget.blur();
619
- });
620
- }
621
-
622
- if (sendBtn) {
623
- sendBtn.onclick = () => {
624
- const message = chatInput.value.trim();
625
- if (message) {
626
- chatInput.value = "";
627
- session.primeTTS(); // Renew activation synchronously
628
- session.sendTextMessage(message);
629
- }
630
- };
631
- }
632
-
633
- if (chatInput) {
634
- chatInput.onkeypress = (e) => {
635
- if (e.key === 'Enter') sendBtn.onclick();
636
- };
637
- }
638
-
639
- // --- Audio Recording Events ---
640
-
641
- if (micBtn) {
642
- micBtn.addEventListener('pointerdown', (e) => {
643
- e.preventDefault();
644
- if (!state.isVoiceMode || state.isAISpeaking) return;
645
- if (!state.isListening && !state.isRecordingPending) {
646
- micBtn.style.transform = 'scale(0.95)';
647
- audio.startRecording(session.sendTextMessage);
648
- }
649
- });
650
-
651
- const stopMicAction = (e) => {
652
- e.preventDefault();
653
- if (state.isVoiceMode && (state.isListening || state.isRecordingPending)) {
654
- micBtn.style.transform = 'none';
655
- session.primeTTS(); // Renew activation synchronously
656
- audio.stopRecording();
657
- }
658
- };
659
-
660
- micBtn.addEventListener('pointerup', stopMicAction);
661
- micBtn.addEventListener('pointercancel', stopMicAction);
662
- micBtn.addEventListener('pointerleave', stopMicAction);
663
- }
664
-
665
- // Spacebar hold-to-record (capture phase to intercept before browser synthetic clicks)
666
- window.addEventListener('keydown', (e) => {
667
- if (e.code !== 'Space' || e.repeat) return;
668
- if (!state.isVoiceMode) return;
669
- if (['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement.tagName)) return;
670
-
671
- e.preventDefault();
672
- e.stopPropagation();
673
-
674
- if (state.isAISpeaking) return;
675
- if (!state.isListening && !state.isRecordingPending) {
676
- audio.startRecording(session.sendTextMessage);
677
- }
678
- }, { capture: true });
679
-
680
- window.addEventListener('keyup', (e) => {
681
- if (e.code !== 'Space') return;
682
- if (!state.isVoiceMode) return;
683
- if (['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement.tagName)) return;
684
-
685
- e.preventDefault();
686
- e.stopPropagation();
687
-
688
- session.primeTTS(); // Renew activation synchronously
689
- audio.stopRecording();
690
- }, { capture: true });
691
-
692
- const endSessionBtn = document.getElementById("endSessionBtn");
693
- if (endSessionBtn) {
694
- endSessionBtn.onclick = async () => {
695
- if (!state.sessionId) return;
696
-
697
- if (window.speechSynthesis.speaking) window.speechSynthesis.cancel();
698
- ui.setStatusText("Ending session...");
699
-
700
- try {
701
- await api.endSessionReq(state.sessionId, state.patientId);
702
- state.setSessionId(null);
703
- window.location.href = "/";
704
- } catch (err) {
705
- console.error("Failed to end session:", err);
706
- state.setSessionId(null);
707
- window.location.href = "/";
708
- }
709
- };
710
- }
711
- }
712
-
713
- // Bootstrap application
714
- api.pingHealth().catch(() => console.log("Wakeup ping sent."));
715
- audio.loadVoices();
716
- bindEvents();
717
- window.onload = () => session.initializeSession();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
static/js/audio.js DELETED
@@ -1,218 +0,0 @@
1
- import { state } from './state.js';
2
- import { setStatusText, toggleMicButtonVisuals, addTyping, removeTyping } from './ui.js';
3
- import { transcribeAudio } from './api.js';
4
-
5
- let systemVoices = [];
6
- let mediaRecorder = null;
7
- let audioChunks = [];
8
-
9
- // Keep a global reference so Chrome doesn't garbage-collect the utterance mid-speech
10
- let currentUtterance = null;
11
-
12
- export function loadVoices() {
13
- const voiceSelect = document.getElementById("voiceSelect");
14
- if (!voiceSelect) return;
15
-
16
- systemVoices = window.speechSynthesis.getVoices();
17
- voiceSelect.innerHTML = "";
18
-
19
- // Add a reliable default option that doesn't force a specific voice object
20
- const defaultOption = document.createElement("option");
21
- defaultOption.value = "";
22
- defaultOption.textContent = "Default OS Voice (Reliable)";
23
- defaultOption.selected = true;
24
- voiceSelect.appendChild(defaultOption);
25
-
26
- const englishVoices = systemVoices.filter(v => v.lang.includes('en'));
27
-
28
- englishVoices.forEach(voice => {
29
- const option = document.createElement("option");
30
- option.value = voice.name;
31
- option.textContent = `${voice.name} (${voice.localService ? 'Local' : 'Network'})`;
32
- voiceSelect.appendChild(option);
33
- });
34
-
35
- const googleUSEnglish = englishVoices.find(v => v.name === "Google US English");
36
- if (googleUSEnglish) {
37
- voiceSelect.value = googleUSEnglish.name;
38
- }
39
- }
40
-
41
- if (window.speechSynthesis.onvoiceschanged !== undefined) {
42
- window.speechSynthesis.onvoiceschanged = loadVoices;
43
- }
44
-
45
- /**
46
- * Speak text using the browser's SpeechSynthesis API.
47
- * Only speaks if audio output is enabled (state.isAudioOutputEnabled).
48
- * This is independent of input mode (text vs voice).
49
- */
50
- export function speak(text) {
51
- return new Promise((resolve) => {
52
- if (!state.isAudioOutputEnabled) {
53
- resolve();
54
- return;
55
- }
56
-
57
- // Ensure voices are loaded
58
- if (systemVoices.length === 0) {
59
- systemVoices = window.speechSynthesis.getVoices();
60
- }
61
-
62
- // Strip markdown symbols before speaking
63
- const cleanText = text.replace(/[*_~`#]/g, '').replace(/\[.*?\]/g, '').replace(/\(.*?\)/g, '').trim();
64
- if (!cleanText) {
65
- console.warn("TTS Skipped: Text was empty after stripping markdown");
66
- resolve();
67
- return;
68
- }
69
-
70
- setStatusText("Assistant is responding...");
71
-
72
- // No cancel(), no setTimeout. Just create and speak.
73
- currentUtterance = new SpeechSynthesisUtterance(cleanText);
74
-
75
- // Select voice
76
- const voiceSelect = document.getElementById("voiceSelect");
77
- const selectedVoiceName = voiceSelect ? voiceSelect.value : "";
78
-
79
- // Only set the voice if the user explicitly selected one of the specific named voices.
80
- // Otherwise, leave it undefined so the browser uses its ultra-reliable native default.
81
- if (selectedVoiceName !== "") {
82
- const preferredVoice = systemVoices.find(v => v.name === selectedVoiceName);
83
- if (preferredVoice) {
84
- currentUtterance.voice = preferredVoice;
85
- }
86
- }
87
- currentUtterance.rate = 1.0;
88
- currentUtterance.pitch = 1.0;
89
- currentUtterance.volume = 1.0;
90
-
91
- currentUtterance.onstart = () => {
92
- state.isAISpeaking = true;
93
- setStatusText("Assistant is speaking... (Audio Started)");
94
- };
95
-
96
- currentUtterance.onend = () => {
97
- state.isAISpeaking = false;
98
- setStatusText(state.isVoiceMode ? "Hold Space to speak" : "Type your message");
99
- resolve();
100
- };
101
-
102
- currentUtterance.onerror = (e) => {
103
- console.error("TTS error:", e.error);
104
- state.isAISpeaking = false;
105
- setStatusText(`TTS Error: ${e.error}`);
106
- setTimeout(() => {
107
- setStatusText(state.isVoiceMode ? "Hold Space to speak" : "Type your message");
108
- }, 3000);
109
- resolve();
110
- };
111
-
112
- setStatusText("Attempting to speak...");
113
-
114
- // Fallback timeout in case onstart/onend never fire
115
- setTimeout(() => {
116
- if (state.isAISpeaking) {
117
- // It started, but maybe stuck?
118
- } else {
119
- setStatusText("Audio failed to start (Timeout)");
120
- setTimeout(() => {
121
- setStatusText(state.isVoiceMode ? "Hold Space to speak" : "Type your message");
122
- }, 3000);
123
- resolve();
124
- }
125
- }, 4000);
126
-
127
- window.speechSynthesis.speak(currentUtterance);
128
- });
129
- }
130
-
131
- /**
132
- * Starts recording the user's voice via MediaRecorder.
133
- * Called only via spacebar hold.
134
- */
135
- export async function startRecording(onTranscriptionDone) {
136
- if (state.isRecordingPending || state.isListening) return;
137
- state.isRecordingPending = true;
138
-
139
- try {
140
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
141
-
142
- // If user released space before mic permission was granted
143
- if (!state.isRecordingPending) {
144
- stream.getTracks().forEach(track => track.stop());
145
- return;
146
- }
147
-
148
- mediaRecorder = new MediaRecorder(stream);
149
- audioChunks = [];
150
-
151
- mediaRecorder.ondataavailable = (event) => {
152
- if (event.data.size > 0) {
153
- audioChunks.push(event.data);
154
- }
155
- };
156
-
157
- mediaRecorder.onstop = async () => {
158
- // Stop all mic tracks immediately
159
- stream.getTracks().forEach(track => track.stop());
160
-
161
- const audioBlob = new Blob(audioChunks, { type: mediaRecorder.mimeType || 'audio/webm' });
162
-
163
- if (audioBlob.size < 100) {
164
- setStatusText("Recording was too short. Hold Space longer.");
165
- return;
166
- }
167
-
168
- setStatusText("Transcribing your voice...");
169
- addTyping(true, "Transcribing...");
170
-
171
- try {
172
- const data = await transcribeAudio(audioBlob);
173
- removeTyping();
174
-
175
- if (data.text && data.text.trim() !== "") {
176
- onTranscriptionDone(data.text, data.emotion);
177
- } else {
178
- setStatusText("I didn't catch that. Try again?");
179
- }
180
- } catch (e) {
181
- removeTyping();
182
- console.error("Transcription failed", e);
183
- setStatusText("Hold Space to try again");
184
- }
185
- };
186
-
187
- mediaRecorder.start();
188
- state.isListening = true;
189
- state.isRecordingPending = false;
190
- toggleMicButtonVisuals(true);
191
- setStatusText("Listening... (release Space to stop)");
192
- } catch (err) {
193
- console.error("Microphone access denied", err);
194
- setStatusText("Microphone access denied");
195
- state.isRecordingPending = false;
196
- }
197
- }
198
-
199
- export function stopRecording() {
200
- state.isRecordingPending = false;
201
-
202
- if (mediaRecorder && state.isListening) {
203
- try {
204
- if (mediaRecorder.state === 'recording') {
205
- mediaRecorder.stop();
206
- }
207
- } catch(e) {
208
- console.error("Error stopping media recorder", e);
209
- }
210
-
211
- state.isListening = false;
212
- toggleMicButtonVisuals(false);
213
- setStatusText("Processing...");
214
- } else {
215
- state.isListening = false;
216
- toggleMicButtonVisuals(false);
217
- }
218
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
static/js/auth.js DELETED
@@ -1,135 +0,0 @@
1
- import { BACKEND_URL } from './api.js';
2
-
3
- export async function login(username, password) {
4
- const res = await fetch(`${BACKEND_URL}/api/auth/login`, {
5
- method: "POST",
6
- headers: { "Content-Type": "application/json" },
7
- body: JSON.stringify({ username, password })
8
- });
9
-
10
- const data = await res.json();
11
- if (!res.ok) {
12
- throw new Error(data.detail || "Login failed");
13
- }
14
-
15
- localStorage.setItem('serinity_token', data.access_token);
16
- return data;
17
- }
18
-
19
- export async function requestSignupOtp(email, username) {
20
- const res = await fetch(`${BACKEND_URL}/api/auth/signup/request-otp`, {
21
- method: "POST",
22
- headers: { "Content-Type": "application/json" },
23
- body: JSON.stringify({ email, username })
24
- });
25
-
26
- const data = await res.json();
27
- if (!res.ok) {
28
- if (Array.isArray(data.detail)) {
29
- throw new Error(data.detail[0].msg || "Validation error");
30
- }
31
- throw new Error(data.detail || "Failed to request OTP");
32
- }
33
- return data;
34
- }
35
-
36
- export async function signup(payload) {
37
- const res = await fetch(`${BACKEND_URL}/api/auth/signup`, {
38
- method: "POST",
39
- headers: { "Content-Type": "application/json" },
40
- body: JSON.stringify(payload)
41
- });
42
-
43
- const data = await res.json();
44
- if (!res.ok) {
45
- if (Array.isArray(data.detail)) {
46
- throw new Error(data.detail[0].msg || "Validation error");
47
- }
48
- throw new Error(data.detail || "Signup failed");
49
- }
50
-
51
- localStorage.setItem('serinity_token', data.access_token);
52
- return data;
53
- }
54
-
55
- export async function forgotPassword(email) {
56
- const res = await fetch(`${BACKEND_URL}/api/auth/forgot-password`, {
57
- method: "POST",
58
- headers: { "Content-Type": "application/json" },
59
- body: JSON.stringify({ email })
60
- });
61
-
62
- const data = await res.json();
63
- if (!res.ok) {
64
- // Pydantic validation errors return detail as an array
65
- if (res.status === 422 || Array.isArray(data.detail)) {
66
- throw new Error("Enter a valid email address.");
67
- }
68
- throw new Error(data.detail || "Failed to request OTP");
69
- }
70
- return data;
71
- }
72
-
73
- export async function verifyOtp(email, otp_code) {
74
- const res = await fetch(`${BACKEND_URL}/api/auth/verify-otp`, {
75
- method: "POST",
76
- headers: { "Content-Type": "application/json" },
77
- body: JSON.stringify({ email, otp_code })
78
- });
79
- const data = await res.json();
80
- if (!res.ok) throw new Error(data.detail || "Invalid OTP");
81
- return data;
82
- }
83
-
84
- export async function resetPassword(email, otp_code, new_password) {
85
- const res = await fetch(`${BACKEND_URL}/api/auth/reset-password`, {
86
- method: "POST",
87
- headers: { "Content-Type": "application/json" },
88
- body: JSON.stringify({ email, otp_code, new_password })
89
- });
90
-
91
- const data = await res.json();
92
- if (!res.ok) {
93
- if (Array.isArray(data.detail)) {
94
- throw new Error(data.detail[0].msg || "Validation error");
95
- }
96
- throw new Error(data.detail || "Failed to reset password");
97
- }
98
- return data;
99
- }
100
- export async function requestDeleteAccountOtp() {
101
- const token = localStorage.getItem('serinity_token');
102
- const res = await fetch(`${BACKEND_URL}/api/auth/delete-account/request-otp`, {
103
- method: "POST",
104
- headers: {
105
- "Content-Type": "application/json",
106
- "Authorization": `Bearer ${token}`
107
- }
108
- });
109
- const data = await res.json();
110
- if (!res.ok) throw new Error(data.detail || "Failed to request OTP");
111
- return data;
112
- }
113
-
114
- export async function verifyDeleteAccount(otp_code) {
115
- const token = localStorage.getItem('serinity_token');
116
- const res = await fetch(`${BACKEND_URL}/api/auth/delete-account/verify`, {
117
- method: "POST",
118
- headers: {
119
- "Content-Type": "application/json",
120
- "Authorization": `Bearer ${token}`
121
- },
122
- body: JSON.stringify({ otp_code })
123
- });
124
- const data = await res.json();
125
- if (!res.ok) throw new Error(data.detail || "Failed to delete account");
126
- return data;
127
- }
128
-
129
- export function logout() {
130
- localStorage.removeItem('serinity_token');
131
- }
132
-
133
- export function isAuthenticated() {
134
- return !!localStorage.getItem('serinity_token');
135
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
static/js/profiles.js DELETED
@@ -1,99 +0,0 @@
1
- import { state } from './state.js';
2
- import * as api from './api.js';
3
- import * as ui from './ui.js';
4
-
5
- export async function fetchPatients() {
6
- try {
7
- const patients = await api.fetchPatientsList();
8
- if (patients.length > 0) {
9
- state.patientId = patients[0].patient_id;
10
- await showDashboard(state.patientId);
11
- } else {
12
- // This should never happen now since the backend auto-creates a profile, but just in case:
13
- ui.showAlert("Profile Error", "No patient profile found. Please contact support.");
14
- }
15
- } catch (err) {
16
- console.error("Failed to load patient profile:", err);
17
- }
18
- }
19
-
20
- /**
21
- * Fetches patient dashboard data (past sessions, domains, etc.) and renders the dashboard screen.
22
- *
23
- * @param {string} pId - The patient ID to load dashboard for.
24
- */
25
- export async function showDashboard(pId) {
26
- try {
27
- const data = await api.loadDashboard(pId);
28
-
29
- document.getElementById("dashboardPatientName").textContent = data.patient.name;
30
- document.getElementById("dashboardPatientAge").textContent = data.patient.age ? `Age: ${data.patient.age}` : "Age: --";
31
-
32
- const list = document.getElementById("dashboardSessionsList");
33
- list.innerHTML = "";
34
-
35
- const dashStartBtn = document.getElementById("dashboardStartBtn");
36
- dashStartBtn.textContent = "NEW SESSION";
37
-
38
- if (data.sessions && data.sessions.length > 0) {
39
- data.sessions.forEach(sess => {
40
- const d = new Date(sess.created_at).toLocaleString();
41
- const summary = sess.rolling_summary || "No summary available.";
42
- list.innerHTML += `
43
- <details class="group p-4 border border-ink bg-paper shadow-sm mb-3">
44
- <summary class="text-xs text-moss font-utility tracking-widest uppercase cursor-pointer select-none group-open:mb-2 flex justify-between">
45
- ${d}
46
- <span class="text-ink transition-transform group-open:rotate-180">▼</span>
47
- </summary>
48
- <div class="text-sm text-ink whitespace-pre-wrap mt-2 pl-2 border-l-2 border-clay">${summary}</div>
49
- </details>
50
- `;
51
- });
52
- } else {
53
- list.innerHTML = `<p class="text-clay text-sm italic">No previous sessions found.</p>`;
54
- }
55
-
56
- const domainsDiv = document.getElementById("dashboardDomains");
57
- if (domainsDiv && data.profile) {
58
- domainsDiv.innerHTML = `<h3 class="font-utility uppercase text-clay text-sm border-b border-ink/30 pb-2">Clinical Profile</h3>`;
59
- const renderDomain = (title, items) => {
60
- if (!items || items.length === 0) return;
61
- const html = `
62
- <details class="group mt-4 border-b border-clay/30 pb-2 cursor-pointer">
63
- <summary class="dashboard-label mb-1 select-none flex justify-between">
64
- ${title}
65
- <span class="text-ink text-xs transition-transform group-open:rotate-180">▼</span>
66
- </summary>
67
- <ul class="list-disc pl-5 text-sm dashboard-value space-y-1 mt-2 text-ink">
68
- ${items.map(i => `<li>${i}</li>`).join('')}
69
- </ul>
70
- </details>
71
- `;
72
- domainsDiv.innerHTML += html;
73
- };
74
-
75
- renderDomain("Emotional Themes", data.profile.emotional_themes);
76
- renderDomain("Thinking Patterns", data.profile.thinking_patterns);
77
- renderDomain("Stressors", data.profile.stressors);
78
- renderDomain("Protective Factors", data.profile.protective_factors);
79
-
80
- if (data.profile.risk_assessment) {
81
- domainsDiv.innerHTML += `
82
- <details class="group mt-4 border-b border-clay/30 pb-2 cursor-pointer">
83
- <summary class="dashboard-label mb-1 select-none flex justify-between">
84
- Risk Assessment
85
- <span class="text-ink text-xs transition-transform group-open:rotate-180">▼</span>
86
- </summary>
87
- <p class="text-sm dashboard-value mt-2 text-ink">${data.profile.risk_assessment}</p>
88
- </details>
89
- `;
90
- }
91
- }
92
-
93
- ui.switchScreen("dashboardScreen");
94
-
95
- } catch (err) {
96
- console.error("Dashboard error", err);
97
- alert("Could not load patient dashboard.");
98
- }
99
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
static/js/session.js DELETED
@@ -1,314 +0,0 @@
1
- import { state } from './state.js';
2
- import * as api from './api.js';
3
- import * as ui from './ui.js';
4
- import * as audio from './audio.js';
5
- import * as profiles from './profiles.js';
6
- import * as auth from './auth.js';
7
-
8
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
9
-
10
- /**
11
- * Processes the assistant's split message response.
12
- * Displays and speaks the empathetic acknowledgment first,
13
- * then pauses before delivering the follow-up clinical question.
14
- *
15
- * @param {string} message - The raw message containing the && separator.
16
- */
17
- export async function handleAssistantResponses(message) {
18
- if (!message) return;
19
-
20
- const parts = message.split("&&").map(p => p.trim()).filter(Boolean);
21
-
22
- ui.addMessage(parts[0], "assistant");
23
- await audio.speak(parts[0]);
24
-
25
- if (parts[1]) {
26
- ui.setStatusText("Assistant is thinking...");
27
- await sleep(500);
28
-
29
- ui.addTyping(false, "Thinking...");
30
- await sleep(500);
31
- ui.removeTyping();
32
-
33
- ui.addMessage(parts[1], "assistant");
34
- await audio.speak(parts[1]);
35
- }
36
- }
37
-
38
- /**
39
- * Ends the current active session, clears intervals, and returns the user to the dashboard.
40
- */
41
- export async function endSession() {
42
- if (state.sessionInterval) {
43
- clearInterval(state.sessionInterval);
44
- state.sessionInterval = null;
45
- }
46
- state.setSessionId(null);
47
- state.setPatientId(null);
48
- ui.switchScreen("dashboard");
49
- }
50
-
51
- /**
52
- * Sends a text message to the backend and handles the response.
53
- *
54
- * @param {string} message - The text message to send.
55
- * @param {string|null} emotion - Optional emotion tag.
56
- */
57
- /**
58
- * Submits a new text message to the active session and waits for the AI response.
59
- * @param {string} text - The user's input message.
60
- * @returns {Promise<boolean>} True if successful, false otherwise.
61
- */
62
- export async function recordMessage(text) {
63
- return await sendTextMessage(text);
64
- }
65
-
66
- export async function sendTextMessage(message, emotion = null) {
67
- ui.addMessage(message, "user");
68
- ui.addTyping(false, "Analyzing...");
69
-
70
- try {
71
- const data = await api.sendChatText(state.sessionId, state.patientId, message, emotion);
72
- ui.removeTyping();
73
-
74
- if (data.session_id && data.session_id !== state.sessionId) {
75
- state.setSessionId(data.session_id);
76
- }
77
-
78
- await handleAssistantResponses(data.assistant_message);
79
-
80
- // Only reset status text if audio is NOT enabled,
81
- // otherwise audio.js will handle resetting it when speech finishes.
82
- if (!state.isAudioOutputEnabled) {
83
- ui.setStatusText(state.isVoiceMode ? "Hold Space to speak" : "Type your message");
84
- }
85
-
86
- if (data.risk_flagged) {
87
- ui.showSafetyBanner();
88
- } else {
89
- ui.hideSafetyBanner();
90
- }
91
- } catch (err) {
92
- ui.removeTyping();
93
- if (err.message && (err.message.includes("Tokens Exhausted") || err.message.includes("429"))) {
94
- ui.showAlert("Tokens Exhausted", "Your LLM provider tokens have been exhausted. Please wait or upgrade your plan.");
95
- } else {
96
- ui.addMessage("I apologize, but I'm having trouble connecting right now.", "assistant");
97
- }
98
- console.error("Error:", err);
99
- }
100
- }
101
-
102
- /**
103
- * Starts a new session for the given patient ID.
104
- * Transitions UI to main app and starts session timers.
105
- *
106
- * @param {string} pId - The patient ID to start the session for.
107
- */
108
- /**
109
- * Starts a new chat session with the AI for the currently selected patient.
110
- * Emits an initial context message to the backend.
111
- */
112
- export async function startActualSession(pId) {
113
- try {
114
- const data = await api.startSessionReq(pId);
115
- state.setSessionId(data.session_id);
116
- state.setPatientId(data.patient_id);
117
-
118
- ui.switchScreen("mainApp");
119
- document.getElementById("chat").innerHTML = "";
120
-
121
- state.sessionStartTime = Date.now();
122
- state.exchangeCount = 0;
123
- ui.updateSessionStats();
124
- if (state.sessionInterval) clearInterval(state.sessionInterval);
125
- state.sessionInterval = setInterval(ui.updateSessionStats, 60000);
126
-
127
- handleAssistantResponses(data.assistant_message);
128
- } catch (e) {
129
- console.error("Failed to start session:", e);
130
- alert("Failed to start session. Please try again.");
131
- location.reload();
132
- }
133
- }
134
-
135
- /**
136
- * Resumes an existing active session.
137
- *
138
- * @param {string} sessionId - The ID of the session to resume.
139
- */
140
- export async function continueActualSession(sessionId) {
141
- try {
142
- state.setSessionId(sessionId);
143
- ui.switchScreen("mainApp");
144
-
145
- document.getElementById("chat").innerHTML = "";
146
-
147
- const data = await api.getSessionMessages(sessionId);
148
- if (data.messages) {
149
- data.messages.forEach(msg => {
150
- if (msg.role !== "system") {
151
- if (msg.role === 'assistant') {
152
- const parts = msg.content.split("&&").map(p => p.trim()).filter(Boolean);
153
- parts.forEach(part => ui.addMessage(part, 'assistant'));
154
- } else {
155
- ui.addMessage(msg.content, msg.role);
156
- }
157
- }
158
- });
159
- }
160
-
161
- state.sessionStartTime = Date.now();
162
- state.exchangeCount = Math.floor((data.messages || []).length / 2);
163
- ui.updateSessionStats();
164
- if (state.sessionInterval) clearInterval(state.sessionInterval);
165
- state.sessionInterval = setInterval(ui.updateSessionStats, 60000);
166
- } catch (e) {
167
- console.error("Failed to continue session:", e);
168
- alert("Failed to continue session. Please try again.");
169
- location.reload();
170
- }
171
- }
172
-
173
- /**
174
- * Pings the backend to check health and initializes routing based on URL path.
175
- * Retries with exponential backoff if backend is not ready.
176
- *
177
- * @param {number} retryCount - Number of initialization retries so far.
178
- */
179
- export async function initializeSession(retryCount = 0) {
180
- const startButton = document.getElementById("startButton");
181
- const wakeupText = document.getElementById("wakeupText");
182
-
183
- if (retryCount === 0) {
184
- if (wakeupText) wakeupText.textContent = "Waking up backend (this may take up to a minute)...";
185
- if (startButton) {
186
- startButton.disabled = false;
187
- startButton.textContent = "Start Consultation";
188
- }
189
- }
190
- try {
191
- await api.pingHealth();
192
- document.getElementById("loadingDots").style.display = "none";
193
- if (wakeupText) wakeupText.style.display = "none";
194
-
195
- if (startButton) {
196
- startButton.disabled = false;
197
- startButton.textContent = "Start Consultation";
198
- }
199
-
200
- // Handle hash routing from OAuth or direct links
201
- if (window.location.hash) {
202
- const hash = window.location.hash.substring(1);
203
- let newPath = "/";
204
- if (hash === "dashboardScreen") newPath = "/dashboard";
205
- else if (hash === "mainApp") newPath = "/session";
206
-
207
- if (newPath !== "/") {
208
- window.history.replaceState(null, "", newPath);
209
- }
210
- }
211
-
212
- // Handle deep linking / routing on load
213
- const path = window.location.pathname;
214
- if (path === "/dashboard") {
215
- // Always re-fetch on refresh — state.patientId is in-memory and lost on reload
216
- await profiles.fetchPatients();
217
- } else if (path === "/session") {
218
- if (!state.sessionId) {
219
- window.location.href = "/";
220
- } else {
221
- ui.switchScreen("mainApp", false);
222
- }
223
- } else {
224
- // Allow user to click the start button
225
- }
226
- } catch (error) {
227
- console.error(`Backend not ready (attempt ${retryCount + 1}):`, error);
228
- if (retryCount < 15) {
229
- const delay = Math.min(2000 * Math.pow(1.3, retryCount), 30000);
230
- setTimeout(() => initializeSession(retryCount + 1), delay);
231
- } else {
232
- document.getElementById("loadingDots").style.display = "none";
233
- if (startButton) {
234
- startButton.disabled = false;
235
- startButton.textContent = "Retry Connection";
236
- }
237
- }
238
- }
239
- }
240
-
241
- /**
242
- * Renews the browser's TTS user-activation token synchronously.
243
- * Must be called directly inside an onclick or keydown/keyup handler.
244
- */
245
- export function primeTTS() {
246
- if (state.isAudioOutputEnabled && window.speechSynthesis) {
247
- const primer = new SpeechSynthesisUtterance(' ');
248
- primer.volume = 0;
249
- primer.rate = 10;
250
- window.speechSynthesis.speak(primer);
251
- }
252
- }
253
-
254
- /**
255
- * Toggle audio output on/off and update the button visuals.
256
- *
257
- * @param {boolean} enabled - Whether audio should be enabled.
258
- */
259
- export function setAudioOutput(enabled) {
260
- state.isAudioOutputEnabled = enabled;
261
-
262
- const mobileToggle = document.getElementById("audioOutputToggleMobile");
263
- const track = document.getElementById("audioToggleTrack");
264
- const dot = document.getElementById("audioToggleDot");
265
- const iconMobile = document.getElementById("audioToggleIconMobile");
266
-
267
- const desktopToggle = document.getElementById("audioOutputToggleDesktop");
268
- const labelDesktop = document.getElementById("audioToggleLabelDesktop");
269
-
270
- if (mobileToggle && mobileToggle.checked !== enabled) {
271
- mobileToggle.checked = enabled;
272
- }
273
-
274
- if (enabled) {
275
- // Prime the TTS engine with a silent utterance linked to this click gesture.
276
- if (window.speechSynthesis) {
277
- window.speechSynthesis.getVoices();
278
- const primer = new SpeechSynthesisUtterance(' ');
279
- primer.volume = 0;
280
- primer.rate = 10;
281
- window.speechSynthesis.speak(primer);
282
- }
283
-
284
- // Update mobile UI
285
- if (track) { track.classList.remove("bg-ink/30"); track.classList.add("bg-moss"); }
286
- if (dot) { dot.classList.add("translate-x-4"); }
287
- if (iconMobile) { iconMobile.classList.remove("text-ink/50"); iconMobile.classList.add("text-moss"); }
288
-
289
- // Update desktop UI
290
- if (desktopToggle) {
291
- desktopToggle.classList.add("bg-moss", "border-moss", "text-paper");
292
- desktopToggle.classList.remove("text-ink", "border-ink");
293
- }
294
- if (labelDesktop) labelDesktop.textContent = "Audio On";
295
-
296
- } else {
297
- if (window.speechSynthesis && window.speechSynthesis.speaking) {
298
- window.speechSynthesis.cancel();
299
- }
300
- state.isAISpeaking = false;
301
-
302
- // Update mobile UI
303
- if (track) { track.classList.remove("bg-moss"); track.classList.add("bg-ink/30"); }
304
- if (dot) { dot.classList.remove("translate-x-4"); }
305
- if (iconMobile) { iconMobile.classList.remove("text-moss"); iconMobile.classList.add("text-ink/50"); }
306
-
307
- // Update desktop UI
308
- if (desktopToggle) {
309
- desktopToggle.classList.remove("bg-moss", "border-moss", "text-paper");
310
- desktopToggle.classList.add("text-ink", "border-ink");
311
- }
312
- if (labelDesktop) labelDesktop.textContent = "Audio Off";
313
- }
314
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
static/js/state.js DELETED
@@ -1,30 +0,0 @@
1
- export const state = {
2
- sessionId: sessionStorage.getItem("serinity_session_id") || null,
3
- patientId: localStorage.getItem("serinity_patient_id") || null,
4
- isListening: false,
5
- isRecordingPending: false,
6
- isVoiceMode: false,
7
- isAudioOutputEnabled: false,
8
- isAISpeaking: false,
9
- exchangeCount: 0,
10
- sessionStartTime: null,
11
- sessionInterval: null,
12
-
13
- setPatientId(id) {
14
- this.patientId = id;
15
- if (id) {
16
- localStorage.setItem("serinity_patient_id", id);
17
- } else {
18
- localStorage.removeItem("serinity_patient_id");
19
- }
20
- },
21
-
22
- setSessionId(id) {
23
- this.sessionId = id;
24
- if (id) {
25
- sessionStorage.setItem("serinity_session_id", id);
26
- } else {
27
- sessionStorage.removeItem("serinity_session_id");
28
- }
29
- }
30
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
static/js/ui.js DELETED
@@ -1,242 +0,0 @@
1
- import { state } from './state.js';
2
-
3
- // Cache DOM elements that exist on load
4
- const uiElements = {
5
- chat: document.getElementById("chat"),
6
- chatWrapper: document.getElementById("chatWrapper"),
7
- statusText: document.getElementById("status"),
8
- micBtn: document.getElementById("micBtn"),
9
- sessionStats: document.getElementById("sessionStats"),
10
- voiceModeBtn: document.getElementById("voiceModeBtn"),
11
- textModeBtn: document.getElementById("textModeBtn"),
12
- voiceInput: document.getElementById("voiceInput"),
13
- textInput: document.getElementById("textInput"),
14
- chatInput: document.getElementById("chatInput")
15
- };
16
-
17
- let typingNode = null;
18
-
19
- const screens = ["loadingScreen", "authScreen", "dashboardScreen", "mainApp"];
20
-
21
- export function getPathForScreen(screen) {
22
- const map = {
23
- "loadingScreen": "/",
24
- "authScreen": "/login",
25
- "dashboardScreen": "/dashboard",
26
- "mainApp": "/session"
27
- };
28
- return map[screen] || "/";
29
- }
30
-
31
- export function switchScreen(targetScreen, pushToHistory = true) {
32
- window.currentScreen = targetScreen;
33
- screens.forEach(s => {
34
- const el = document.getElementById(s);
35
- if (el) {
36
- if (s === targetScreen) {
37
- if (s === 'mainApp' || s === 'dashboardScreen') el.classList.remove("hidden");
38
- el.style.display = "flex";
39
- } else {
40
- el.style.display = "none";
41
- }
42
- }
43
- });
44
-
45
- if (pushToHistory) {
46
- history.pushState({ screen: targetScreen }, "", getPathForScreen(targetScreen));
47
- }
48
- }
49
-
50
- export function scrollToBottom() {
51
- setTimeout(() => {
52
- if (uiElements.chatWrapper) {
53
- uiElements.chatWrapper.scrollTop = uiElements.chatWrapper.scrollHeight;
54
- }
55
- }, 100);
56
- }
57
-
58
- export function updateSessionStats() {
59
- if (!state.sessionStartTime) return;
60
- if (!uiElements.sessionStats) return;
61
- const elapsed = Math.floor((Date.now() - state.sessionStartTime) / 60000);
62
- uiElements.sessionStats.textContent = `Session · ${elapsed} min · ${state.exchangeCount} exchanges`;
63
- }
64
-
65
- export function addMessage(text, role) {
66
- const messageDiv = document.createElement("div");
67
- messageDiv.className = `flex ${role === 'user' ? 'justify-end' : 'justify-start'} fade-in w-full mb-6`;
68
-
69
- const contentDiv = document.createElement("div");
70
-
71
- if (role === 'assistant') {
72
- const label = document.createElement("span");
73
- label.className = "assistant-label";
74
- label.textContent = "ASSISTANT ·";
75
- contentDiv.appendChild(label);
76
- contentDiv.className = "pl-4 border-l-2 border-moss max-w-[85%] sm:max-w-[75%]";
77
- } else {
78
- const label = document.createElement("span");
79
- label.className = "assistant-label text-right block";
80
- const dashName = document.getElementById("dashboardPatientName");
81
- const userName = dashName ? dashName.innerText : "USER";
82
- label.textContent = "· " + userName.toUpperCase();
83
- contentDiv.appendChild(label);
84
- contentDiv.className = "pr-4 border-r-2 border-clay text-right max-w-[85%] sm:max-w-[75%]";
85
- }
86
-
87
- const textNode = document.createElement("p");
88
- textNode.className = "text-ink whitespace-pre-wrap";
89
- textNode.textContent = text;
90
- contentDiv.appendChild(textNode);
91
-
92
- messageDiv.appendChild(contentDiv);
93
- if (uiElements.chat) {
94
- uiElements.chat.appendChild(messageDiv);
95
- scrollToBottom();
96
- }
97
-
98
- if (role === 'user') {
99
- state.exchangeCount++;
100
- updateSessionStats();
101
- }
102
- }
103
-
104
- export function addTyping(isUser = false, customText = null) {
105
- if (typingNode) return;
106
-
107
- const messageDiv = document.createElement("div");
108
- messageDiv.className = `flex ${isUser ? 'justify-end' : 'justify-start'} fade-in w-full mb-6`;
109
-
110
- const contentDiv = document.createElement("div");
111
-
112
- if (!isUser) {
113
- const label = document.createElement("span");
114
- label.className = "assistant-label";
115
- label.textContent = "ASSISTANT ·";
116
- contentDiv.appendChild(label);
117
- contentDiv.className = "pl-4 border-l-2 border-moss max-w-[85%] sm:max-w-[75%]";
118
- } else {
119
- const label = document.createElement("span");
120
- label.className = "assistant-label text-right block";
121
- const dashName = document.getElementById("dashboardPatientName");
122
- const userName = dashName ? dashName.innerText : "USER";
123
- label.textContent = "· " + userName.toUpperCase();
124
- contentDiv.appendChild(label);
125
- contentDiv.className = "pr-4 border-r-2 border-clay text-right max-w-[85%] sm:max-w-[75%]";
126
- }
127
-
128
- if (customText) {
129
- const textNode = document.createElement("p");
130
- textNode.className = "text-moss text-sm italic font-utility animate-pulse";
131
- textNode.textContent = customText;
132
- contentDiv.appendChild(textNode);
133
- } else {
134
- const indicator = document.createElement("div");
135
- indicator.className = "typing-indicator flex gap-1 mt-2" + (isUser ? " justify-end" : "");
136
- indicator.innerHTML = `<span></span><span></span><span></span>`;
137
- contentDiv.appendChild(indicator);
138
- }
139
-
140
- messageDiv.appendChild(contentDiv);
141
-
142
- typingNode = messageDiv;
143
- if (uiElements.chat) {
144
- uiElements.chat.appendChild(messageDiv);
145
- scrollToBottom();
146
- }
147
- }
148
-
149
- export function removeTyping() {
150
- if (!typingNode) return;
151
- typingNode.remove();
152
- typingNode = null;
153
- }
154
-
155
- export function setStatusText(text) {
156
- if (uiElements.statusText) {
157
- uiElements.statusText.textContent = text;
158
- }
159
- }
160
-
161
- export function toggleMicButtonVisuals(isRecording) {
162
- if (!uiElements.micBtn) return;
163
- if (isRecording) {
164
- uiElements.micBtn.classList.add("bg-moss", "border-moss", "text-paper");
165
- } else {
166
- uiElements.micBtn.classList.remove("bg-moss", "border-moss", "text-paper");
167
- }
168
- }
169
-
170
- export function showSafetyBanner() {
171
- if (!document.getElementById("safetyBanner")) {
172
- const banner = document.createElement("div");
173
- banner.id = "safetyBanner";
174
- banner.className = "bg-signal text-paper p-4 shadow-md z-50 fixed top-0 left-0 right-0 fade-in";
175
- banner.innerHTML = `
176
- <div class="max-w-5xl mx-auto flex w-full justify-between items-start">
177
- <div>
178
- <h3 class="font-utility uppercase tracking-widest text-sm mb-1 font-bold">Emergency Support</h3>
179
- <p class="text-sm">You are not alone. Free, confidential support is available right now.</p>
180
- <ul class="list-none text-sm font-utility space-y-1 mt-2">
181
- <li>iCall (TISS): 9152987821</li>
182
- <li>Kiran: 1800-599-0019</li>
183
- <li>Vandrevala: 1860-2662-345</li>
184
- </ul>
185
- </div>
186
- <button onclick="document.getElementById('safetyBanner').remove()" class="text-paper hover:opacity-70 text-2xl font-bold p-2 cursor-pointer leading-none">&times;</button>
187
- </div>
188
- `;
189
- document.body.appendChild(banner);
190
- }
191
- }
192
-
193
- export function hideSafetyBanner() {
194
- const banner = document.getElementById("safetyBanner");
195
- if (banner) {
196
- banner.remove();
197
- }
198
- }
199
-
200
- export function showConfirm(title, text, callback) {
201
- const confirmationModal = document.getElementById("confirmationModal");
202
- const confirmModalTitle = document.getElementById("confirmModalTitle");
203
- const confirmModalText = document.getElementById("confirmModalText");
204
- const confirmModalCancelBtn = document.getElementById("confirmModalCancelBtn");
205
- const confirmModalConfirmBtn = document.getElementById("confirmModalConfirmBtn");
206
-
207
- confirmModalTitle.textContent = title;
208
- confirmModalText.textContent = text;
209
-
210
- confirmModalCancelBtn.onclick = () => {
211
- confirmationModal.style.display = "none";
212
- };
213
-
214
- confirmModalConfirmBtn.onclick = () => {
215
- confirmationModal.style.display = "none";
216
- if (callback) callback();
217
- };
218
-
219
- confirmationModal.style.display = "flex";
220
- }
221
-
222
- export function showAlert(title, text) {
223
- const alertModal = document.getElementById("alertModal");
224
- if (!alertModal) return alert(text); // Fallback
225
-
226
- document.getElementById("alertModalTitle").textContent = title;
227
- document.getElementById("alertModalText").textContent = text;
228
-
229
- const closeModal = () => {
230
- alertModal.style.display = "none";
231
- };
232
-
233
- document.getElementById("alertModalOkBtn").onclick = closeModal;
234
-
235
- alertModal.onclick = (e) => {
236
- if (e.target === alertModal) {
237
- closeModal();
238
- }
239
- };
240
-
241
- alertModal.style.display = "flex";
242
- }