Amogh1221 commited on
Commit
72daa2c
·
verified ·
1 Parent(s): bb799ec

Upload 11 files

Browse files
static/css/style.css ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
static/js/api.js ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,717 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,314 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }
templates/index.html ADDED
@@ -0,0 +1,456 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Serinity - Mental Healthcare</title>
7
+ <link rel="icon" type="image/svg+xml" href="/static/icons/logo.svg">
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <link rel="stylesheet" href="/static/css/style.css">
10
+ <script>
11
+ tailwind.config = {
12
+ theme: {
13
+ extend: {
14
+ colors: {
15
+ paper: '#F6F3EE',
16
+ ink: '#262521',
17
+ moss: '#4B6358',
18
+ clay: '#9C7A5B',
19
+ signal: '#B4483C'
20
+ }
21
+ }
22
+ }
23
+ }
24
+ </script>
25
+ </head>
26
+ <body class="bg-paper text-ink min-h-screen flex flex-col font-body">
27
+
28
+ <!-- Loading Screen -->
29
+ <div id="loadingScreen" class="fixed inset-0 bg-paper z-50 flex flex-col items-center justify-center transition-opacity duration-500">
30
+ <h1 class="logo-text">Serinity</h1>
31
+ <div class="loading-dots" id="loadingDots">
32
+ <span></span>
33
+ <span></span>
34
+ <span></span>
35
+ </div>
36
+ <button class="btn-primary mt-8" id="startButton" disabled>
37
+ Initializing...
38
+ </button>
39
+ <p id="wakeupText" class="text-sm text-clay mt-4 text-center max-w-xs mx-auto font-utility">
40
+ Validating credentials...
41
+ </p>
42
+ </div>
43
+
44
+ <!-- Auth Screen -->
45
+ <div id="authScreen" style="display: none;" class="fixed inset-0 bg-paper z-40 flex flex-col items-center justify-center p-4 overflow-y-auto custom-scrollbar">
46
+ <div class="panel p-8 max-w-md w-full my-auto shadow-sm">
47
+
48
+ <!-- Logo/Branding -->
49
+ <div class="flex items-center gap-3 mb-8 justify-center">
50
+ <img src="/static/icons/logo.svg" alt="Serinity Logo" class="w-10 h-10">
51
+ <h2 class="text-3xl font-display text-ink">Serinity</h2>
52
+ </div>
53
+
54
+ <!-- Login Form -->
55
+ <div id="loginFormContainer" class="block space-y-5">
56
+ <div>
57
+ <label class="block text-xs font-utility text-clay uppercase mb-1">Username / Email *</label>
58
+ <input type="text" id="loginUsername" placeholder="Username or Email" class="w-full bg-paper border border-ink p-3 text-ink focus:outline-none focus:border-moss transition">
59
+ </div>
60
+ <div>
61
+ <label class="block text-xs font-utility text-clay uppercase mb-1">Password</label>
62
+ <input type="password" id="loginPassword" class="w-full bg-paper border border-ink p-3 text-ink focus:outline-none focus:border-moss transition" placeholder="Enter password">
63
+ </div>
64
+ <div class="flex justify-between items-center text-xs font-utility uppercase tracking-widest mt-2">
65
+ <button id="showForgotPwdBtn" class="text-clay hover:text-ink transition">Forgot Password?</button>
66
+ </div>
67
+ <button id="loginBtn" class="w-full btn-primary py-3 font-utility uppercase tracking-widest text-sm mt-4">
68
+ Log In
69
+ </button>
70
+ <div class="text-center mt-6 border-t border-ink/20 pt-6">
71
+ <p class="text-xs text-clay font-utility uppercase tracking-widest mb-3">New Patient?</p>
72
+ <button id="showSignupBtn" class="w-full bg-transparent border border-ink text-ink hover:bg-ink hover:text-paper transition py-3 font-utility uppercase tracking-widest text-sm">
73
+ Create Account
74
+ </button>
75
+ </div>
76
+ </div>
77
+
78
+ <!-- Signup Form -->
79
+ <div id="signupFormContainer" class="hidden space-y-4">
80
+ <div>
81
+ <label class="block text-xs font-utility text-clay uppercase mb-1">Full Name *</label>
82
+ <input type="text" id="signupName" class="w-full bg-paper border border-ink p-2 text-ink focus:outline-none focus:border-moss" placeholder="Jane Doe">
83
+ </div>
84
+ <div class="grid grid-cols-3 gap-4">
85
+ <div>
86
+ <label class="block text-xs font-utility text-clay uppercase mb-1">Username *</label>
87
+ <input type="text" id="signupUsername" class="w-full bg-paper border border-ink p-2 text-ink focus:outline-none focus:border-moss">
88
+ </div>
89
+ <div>
90
+ <label class="block text-xs font-utility text-clay uppercase mb-1">Gender *</label>
91
+ <select id="signupGender" class="w-full bg-paper border border-ink p-2 text-ink focus:outline-none focus:border-moss">
92
+ <option value="">Select</option>
93
+ <option value="Male">Male</option>
94
+ <option value="Female">Female</option>
95
+ <option value="Other">Other</option>
96
+ </select>
97
+ </div>
98
+ <div>
99
+ <label class="block text-xs font-utility text-clay uppercase mb-1">Age</label>
100
+ <input type="number" id="signupAge" min="5" max="99" class="w-full bg-paper border border-ink p-2 text-ink focus:outline-none focus:border-moss">
101
+ </div>
102
+ </div>
103
+ <div>
104
+ <label class="block text-xs font-utility text-clay uppercase mb-1">Email *</label>
105
+ <input type="email" id="signupEmail" class="w-full bg-paper border border-ink p-2 text-ink focus:outline-none focus:border-moss" placeholder="jane@example.com">
106
+ </div>
107
+ <div>
108
+ <label class="block text-xs font-utility text-clay uppercase mb-1">Password *</label>
109
+ <input type="password" id="signupPassword" class="w-full bg-paper border border-ink p-2 text-ink focus:outline-none focus:border-moss">
110
+ </div>
111
+ <div class="grid grid-cols-2 gap-4">
112
+ <div class="relative" id="nationalityWrapper">
113
+ <label class="block text-xs font-utility text-clay uppercase mb-1" title="For cultural context">Nationality *</label>
114
+ <input type="text" id="signupNationality" autocomplete="off" class="w-full bg-paper border border-ink p-2 text-ink focus:outline-none focus:border-moss" placeholder="e.g. India">
115
+ <div id="nationalityDropdown" class="hidden absolute left-0 right-0 top-full mt-1 bg-paper border border-ink z-50 max-h-48 overflow-y-auto shadow-lg">
116
+ <!-- Populated by JS -->
117
+ </div>
118
+ </div>
119
+ <div>
120
+ <label class="block text-xs font-utility text-clay uppercase mb-1">Primary Concern *</label>
121
+ <select id="signupConcern" class="w-full bg-paper border border-ink p-2 text-ink focus:outline-none focus:border-moss">
122
+ <option value="">Select...</option>
123
+ <option value="Anxiety">Anxiety</option>
124
+ <option value="Depression">Depression</option>
125
+ <option value="Stress">Stress</option>
126
+ <option value="Sleep">Sleep Issues</option>
127
+ <option value="General">General Wellness</option>
128
+ <option value="Other">Other</option>
129
+ </select>
130
+ </div>
131
+ </div>
132
+ <div class="grid grid-cols-2 gap-4">
133
+ <div>
134
+ <label class="block text-xs font-utility text-clay uppercase mb-1">Emergency Contact Name</label>
135
+ <input type="text" id="signupEmergencyName" class="w-full bg-paper border border-ink p-2 text-ink focus:outline-none focus:border-moss" placeholder="John Doe">
136
+ </div>
137
+ <div>
138
+ <label class="block text-xs font-utility text-clay uppercase mb-1">Emergency Contact Phone</label>
139
+ <input type="tel" id="signupEmergencyPhone" class="w-full bg-paper border border-ink p-2 text-ink focus:outline-none focus:border-moss" placeholder="+1 234 567 8900">
140
+ </div>
141
+ </div>
142
+
143
+ <button id="signupGetOtpBtn" class="w-full btn-primary py-3 font-utility uppercase tracking-widest text-sm mt-4">
144
+ Get OTP
145
+ </button>
146
+
147
+ <button id="backToLoginBtn1" class="w-full bg-transparent text-clay hover:text-ink transition py-3 font-utility uppercase tracking-widest text-xs mt-2">
148
+ &larr; Back to Login
149
+ </button>
150
+ </div>
151
+
152
+ <!-- Forgot Password Form -->
153
+ <div id="forgotPwdFormContainer" class="hidden space-y-4">
154
+ <p class="text-sm text-clay font-body mb-4">Enter your email address and we will send you a 6-digit verification code to reset your password.</p>
155
+ <div>
156
+ <label class="block text-xs font-utility text-clay uppercase mb-1">Email Address</label>
157
+ <input type="email" id="forgotPwdEmail" class="w-full bg-paper border border-ink p-3 text-ink focus:outline-none focus:border-moss transition" placeholder="jane@example.com">
158
+ </div>
159
+ <button id="requestOtpBtn" class="w-full btn-primary py-3 font-utility uppercase tracking-widest text-sm mt-2">
160
+ Send Code
161
+ </button>
162
+ <button id="backToLoginBtn2" class="w-full bg-transparent text-clay hover:text-ink transition py-3 font-utility uppercase tracking-widest text-xs mt-2">
163
+ &larr; Back to Login
164
+ </button>
165
+ </div>
166
+
167
+ <!-- Reset Password Step 1: OTP Verification -->
168
+ <div id="otpStepContainer" class="hidden space-y-4">
169
+ <p class="text-sm text-clay font-body mb-4 text-center">We've sent a 6-digit code to your email.</p>
170
+ <div>
171
+ <label class="block text-xs font-utility text-clay uppercase mb-1">6-Digit Code</label>
172
+ <input type="text" id="resetOtpCode" class="w-full bg-paper border border-ink p-3 text-ink focus:outline-none focus:border-moss text-center tracking-widest font-mono text-lg" placeholder="000000" maxlength="6" inputmode="numeric">
173
+ </div>
174
+ <button id="verifyOtpBtn" class="w-full btn-primary py-3 font-utility uppercase tracking-widest text-sm mt-2">
175
+ Verify Code
176
+ </button>
177
+ <button id="backToForgotBtn" class="w-full bg-transparent text-clay hover:text-ink transition py-3 font-utility uppercase tracking-widest text-xs mt-2">
178
+ &larr; Back
179
+ </button>
180
+ </div>
181
+
182
+ <!-- Reset Password Step 2: New Password (no back button) -->
183
+ <div id="newPwdStepContainer" class="hidden space-y-4">
184
+ <p class="text-sm text-clay font-body mb-4 text-center">OTP verified. Enter your new password below.</p>
185
+ <div>
186
+ <label class="block text-xs font-utility text-clay uppercase mb-1">New Password</label>
187
+ <input type="password" id="resetNewPassword" class="w-full bg-paper border border-ink p-3 text-ink focus:outline-none focus:border-moss" placeholder="Min. 8 characters">
188
+ </div>
189
+ <button id="submitResetBtn" class="w-full btn-primary py-3 font-utility uppercase tracking-widest text-sm mt-2">
190
+ Update Password
191
+ </button>
192
+ </div>
193
+
194
+ <!-- Signup Step 2: OTP Verification -->
195
+ <div id="signupOtpStepContainer" class="hidden space-y-4">
196
+ <p class="text-sm text-clay font-body mb-4 text-center">We've sent a 6-digit code to your email.</p>
197
+ <div>
198
+ <label class="block text-xs font-utility text-clay uppercase mb-1">6-Digit Code *</label>
199
+ <input type="text" id="signupOtpCode" class="w-full bg-paper border border-ink p-3 text-ink focus:outline-none focus:border-moss text-center tracking-widest font-mono text-lg" placeholder="000000" maxlength="6" inputmode="numeric">
200
+ </div>
201
+ <button id="signupCompleteBtn" class="w-full btn-primary py-3 font-utility uppercase tracking-widest text-sm mt-2">
202
+ Complete Signup
203
+ </button>
204
+ <button id="backToSignupBtn" class="w-full bg-transparent text-clay hover:text-ink transition py-3 font-utility uppercase tracking-widest text-xs mt-2">
205
+ &larr; Back to Signup
206
+ </button>
207
+ </div>
208
+
209
+ </div>
210
+ </div>
211
+
212
+ <!-- Profile Selection Screen Removed -->
213
+
214
+ <!-- Patient Dashboard -->
215
+ <div class="hidden max-w-5xl mx-auto w-full h-screen p-4 flex-col fade-in overflow-hidden" id="dashboardScreen">
216
+ <div class="bg-paper border border-ink p-6 lg:p-10 flex-1 flex flex-col min-h-0 h-full">
217
+
218
+ <!-- Header -->
219
+ <div class="flex justify-between items-start border-b border-ink/30 pb-6 mb-6 shrink-0">
220
+ <div>
221
+ <h2 class="text-3xl font-display text-ink" id="dashboardPatientName">Patient Name</h2>
222
+ <p class="text-xs font-utility tracking-widest uppercase text-clay mt-2" id="dashboardPatientAge">Age: --</p>
223
+ </div>
224
+ <button class="btn-primary" id="dashboardStartBtn">NEW SESSION</button>
225
+ </div>
226
+
227
+ <!-- Content -->
228
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-10 flex-1 min-h-0 overflow-hidden" id="dashboardContent">
229
+ <!-- Profile left -->
230
+ <div class="space-y-6 overflow-y-auto pr-4 custom-scrollbar" id="dashboardDomains">
231
+ <h3 class="font-utility uppercase text-clay text-sm border-b border-ink/30 pb-2">Clinical Profile</h3>
232
+ <p class="text-clay text-sm italic">Loading profile data...</p>
233
+ </div>
234
+
235
+ <!-- Column 2: Session History -->
236
+ <div class="flex flex-col min-h-0 h-full">
237
+ <h3 class="font-utility uppercase text-clay text-sm border-b border-ink/30 pb-2 mb-4 shrink-0">Session History</h3>
238
+ <div id="dashboardSessionsList" class="flex-1 overflow-y-auto pr-4 space-y-4 custom-scrollbar">
239
+ <p class="text-clay text-sm italic">Loading previous sessions...</p>
240
+ </div>
241
+ </div>
242
+ </div>
243
+
244
+ <div class="mt-8 pt-6 border-t border-ink/30 shrink-0 flex flex-wrap justify-between items-center gap-4">
245
+ <button class="text-xs font-utility tracking-widest text-clay hover:text-ink uppercase transition flex items-center gap-2" id="dashboardLogoutBtn">
246
+ <span>&larr;</span> LOGOUT
247
+ </button>
248
+ <div class="flex gap-4">
249
+ <button class="text-xs font-utility tracking-widest text-signal border border-signal hover:bg-signal hover:text-paper uppercase transition px-4 py-2 flex items-center gap-2" id="dashboardResetBtn">
250
+ RESET DATA
251
+ </button>
252
+ <button class="text-xs font-utility tracking-widest text-paper bg-signal hover:bg-signal/90 uppercase transition px-4 py-2 flex items-center gap-2" id="dashboardDeleteBtn">
253
+ DELETE ACCOUNT
254
+ </button>
255
+ </div>
256
+ </div>
257
+ </div>
258
+ </div>
259
+
260
+ <!-- Main App (Hidden Initially) -->
261
+ <div id="mainApp" style="display: none;" class="flex flex-col h-screen">
262
+ <!-- Header -->
263
+ <header class="bg-paper border-b border-ink sticky top-0 z-50 py-2 md:py-3 shadow-sm">
264
+ <div class="w-full px-3 md:px-12 mx-auto flex items-center justify-between gap-1 md:gap-2">
265
+
266
+ <!-- Left: Logo & Title -->
267
+ <div class="flex items-center gap-2 md:gap-4 shrink-0 min-w-0">
268
+ <img src="/static/icons/logo.svg" alt="Serinity Logo" class="w-8 h-8 md:w-10 md:h-10 opacity-90 shrink-0">
269
+ <div class="flex flex-col justify-center min-w-0">
270
+ <h1 class="text-xl md:text-3xl font-display text-ink leading-none truncate" id="headerName">Serinity</h1>
271
+ <p class="text-[10px] md:text-xs font-utility text-clay tracking-widest uppercase mt-0.5 truncate hidden sm:block">Clinical Session</p>
272
+ </div>
273
+ </div>
274
+
275
+ <!-- Right: Controls -->
276
+ <div class="flex items-center gap-2 shrink-0">
277
+ <span id="sessionStats" class="text-xs font-utility text-moss uppercase hidden lg:block"></span>
278
+
279
+ <select
280
+ id="voiceSelect"
281
+ class="bg-paper border border-ink text-ink text-xs md:text-sm font-utility px-1 py-1.5 outline-none focus:border-moss w-[70px] sm:w-[140px] md:w-[200px] cursor-pointer hover:bg-ink/5 truncate shrink-0"
282
+ >
283
+ <option value="">Voice: Off</option>
284
+ </select>
285
+
286
+ <!-- Mobile Slider Toggle -->
287
+ <label class="flex sm:hidden items-center cursor-pointer gap-1.5 shrink-0" title="Toggle Audio Response">
288
+ <div class="relative flex items-center">
289
+ <input type="checkbox" id="audioOutputToggleMobile" class="sr-only">
290
+ <div id="audioToggleTrack" class="block bg-ink/30 w-10 h-6 rounded-full transition-colors duration-200"></div>
291
+ <div id="audioToggleDot" class="dot absolute left-1 top-1 bg-paper w-4 h-4 rounded-full transition-transform duration-200"></div>
292
+ </div>
293
+ <svg id="audioToggleIconMobile" class="w-4 h-4 text-ink/50 transition-colors duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
294
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.536 8.464a5 5 0 010 7.072M17.95 6.05a8 8 0 010 11.9M6.5 8.5H4a1 1 0 00-1 1v5a1 1 0 001 1h2.5l4 4V4.5l-4 4z"></path>
295
+ </svg>
296
+ </label>
297
+
298
+ <!-- Desktop Button Toggle -->
299
+ <button
300
+ id="audioOutputToggleDesktop"
301
+ class="hidden sm:flex border border-ink text-ink hover:bg-moss hover:border-moss hover:text-paper transition-all items-center justify-center px-2 py-1.5 md:px-3 shrink-0 gap-1"
302
+ title="Toggle Audio Response"
303
+ >
304
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
305
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.536 8.464a5 5 0 010 7.072M17.95 6.05a8 8 0 010 11.9M6.5 8.5H4a1 1 0 00-1 1v5a1 1 0 001 1h2.5l4 4V4.5l-4 4z"></path>
306
+ </svg>
307
+ <span class="text-xs font-utility uppercase tracking-wide" id="audioToggleLabelDesktop">Audio Off</span>
308
+ </button>
309
+
310
+ <button
311
+ id="endSessionBtn"
312
+ class="text-signal hover:text-signal/70 transition-colors border border-signal flex items-center justify-center px-2 py-1.5 md:px-3 shrink-0"
313
+ title="End Session"
314
+ >
315
+ <span class="text-xs md:text-sm font-utility uppercase tracking-wide md:hidden">End</span>
316
+ <span class="hidden md:inline text-sm font-utility uppercase tracking-wide">End Session</span>
317
+ </button>
318
+ </div>
319
+
320
+ </div>
321
+ </header>
322
+
323
+ <!-- Chat Container -->
324
+ <div class="flex-1 overflow-hidden flex flex-col w-full">
325
+ <div id="chatWrapper" class="flex-1 overflow-y-auto px-6 md:px-12 py-8 custom-scrollbar">
326
+ <div id="chat" class="flex flex-col gap-6 max-w-3xl mx-auto pb-8"></div>
327
+ </div>
328
+ </div>
329
+
330
+ <!-- Fixed Bottom Controls -->
331
+ <div class="bg-paper border-t border-ink sticky bottom-0">
332
+ <div class="w-full px-6 md:px-12 mx-auto py-4">
333
+ <!-- Input Mode Toggle -->
334
+ <div class="flex justify-center gap-4 mb-4">
335
+ <button id="textModeBtn" class="btn-toggle active uppercase tracking-wider text-xs">Text</button>
336
+ <button id="voiceModeBtn" class="btn-toggle uppercase tracking-wider text-xs">Audio</button>
337
+ </div>
338
+
339
+ <!-- Voice Input -->
340
+ <div id="voiceInput" class="hidden flex-col items-center gap-3">
341
+ <button
342
+ id="micBtn"
343
+ class="w-14 h-14 border border-ink rounded-full flex items-center justify-center transition-all duration-200 hover:bg-moss hover:border-moss hover:text-paper group text-ink"
344
+ style="touch-action: none;"
345
+ >
346
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
347
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z"></path>
348
+ </svg>
349
+ </button>
350
+ <p id="status" class="text-xs font-utility text-clay uppercase tracking-widest">Hold Space to speak</p>
351
+ </div>
352
+
353
+ <!-- Text Input -->
354
+ <div id="textInput" class="block">
355
+ <div class="chat-input-container max-w-3xl mx-auto">
356
+ <input
357
+ type="text"
358
+ id="chatInput"
359
+ class="chat-input"
360
+ placeholder="Enter message..."
361
+ autocomplete="off"
362
+ />
363
+ <button id="sendBtn" class="px-4 py-2 bg-ink text-paper text-sm font-utility uppercase transition-colors hover:bg-moss">
364
+ Submit
365
+ </button>
366
+ </div>
367
+ </div>
368
+ </div>
369
+ </div>
370
+ </div>
371
+
372
+ <!-- Confirmation Modal -->
373
+ <div id="confirmationModal" style="display: none;" class="fixed inset-0 bg-ink/50 backdrop-blur-sm z-[60] flex flex-col items-center justify-center p-4 fade-in">
374
+ <div class="bg-paper border border-signal p-8 max-w-sm w-full shadow-lg">
375
+ <h2 class="text-xl font-display text-signal mb-2" id="confirmModalTitle">Are you sure?</h2>
376
+ <p class="text-clay text-sm mb-6" id="confirmModalText">This action cannot be undone.</p>
377
+
378
+ <div class="flex gap-4">
379
+ <button id="confirmModalCancelBtn" class="flex-1 bg-transparent border border-ink text-ink hover:bg-ink hover:text-paper transition py-2 font-utility uppercase tracking-widest text-sm">
380
+ CANCEL
381
+ </button>
382
+ <button id="confirmModalConfirmBtn" class="flex-1 bg-signal text-paper hover:bg-signal/80 transition py-2 font-utility uppercase tracking-widest text-sm">
383
+ CONFIRM
384
+ </button>
385
+ </div>
386
+ </div>
387
+ </div>
388
+
389
+ <!-- Continue Session Modal -->
390
+ <div id="continueSessionModal" style="display: none;" class="fixed inset-0 bg-ink/50 backdrop-blur-sm z-[60] flex flex-col items-center justify-center p-4 fade-in">
391
+ <div class="bg-paper border border-signal p-8 max-w-sm w-full shadow-lg">
392
+ <h2 class="text-xl font-display text-signal mb-2">Active Session Found</h2>
393
+ <p class="text-clay text-sm mb-6">You have a previous session that is still open. Would you like to pick up where you left off, or end it and start a new one?</p>
394
+
395
+ <div class="flex flex-col gap-3">
396
+ <button id="continueSessionBtn" class="w-full bg-signal text-paper hover:bg-signal/80 transition py-2 font-utility uppercase tracking-widest text-sm">
397
+ Continue Session
398
+ </button>
399
+ <button id="endAndStartNewBtn" class="w-full bg-transparent border border-ink text-ink hover:bg-ink hover:text-paper transition py-2 font-utility uppercase tracking-widest text-sm">
400
+ End & Start New
401
+ </button>
402
+ </div>
403
+ </div>
404
+ </div>
405
+
406
+ <!-- Alert Modal -->
407
+ <div id="alertModal" style="display: none;" class="fixed inset-0 bg-ink/50 backdrop-blur-sm z-[70] flex flex-col items-center justify-center p-4 fade-in">
408
+ <div class="bg-paper border border-ink p-8 max-w-sm w-full shadow-lg">
409
+ <h2 class="text-xl font-display text-ink mb-2" id="alertModalTitle">Notice</h2>
410
+ <p class="text-clay text-sm mb-6" id="alertModalText"></p>
411
+ <button id="alertModalOkBtn" class="w-full bg-ink text-paper hover:bg-moss transition py-2 font-utility uppercase tracking-widest text-sm">
412
+ OK
413
+ </button>
414
+ </div>
415
+ </div>
416
+
417
+ <!-- Delete Account Confirmation Modal -->
418
+ <div id="deleteAccountConfirmModal" style="display: none;" class="fixed inset-0 bg-ink/50 backdrop-blur-sm z-50 flex flex-col items-center justify-center p-4">
419
+ <div class="panel p-8 max-w-sm w-full shadow-lg">
420
+ <h2 class="text-xl font-display text-signal mb-2">Delete Account?</h2>
421
+ <p class="text-clay text-sm mb-6">Are you sure you want to completely delete your account? This action is irreversible and all data will be erased. An OTP will be sent to your email to verify.</p>
422
+
423
+ <div class="flex gap-4">
424
+ <button id="deleteAccountCancelBtn" class="flex-1 bg-transparent border border-ink text-ink hover:bg-ink hover:text-paper transition py-2 font-utility uppercase tracking-widest text-sm">
425
+ CANCEL
426
+ </button>
427
+ <button id="deleteAccountSendOtpBtn" class="flex-1 bg-signal text-paper hover:bg-signal/80 transition py-2 font-utility uppercase tracking-widest text-sm">
428
+ SEND OTP
429
+ </button>
430
+ </div>
431
+ </div>
432
+ </div>
433
+
434
+ <!-- Delete Account OTP Modal -->
435
+ <div id="deleteAccountOtpModal" style="display: none;" class="fixed inset-0 bg-ink/50 backdrop-blur-sm z-50 flex flex-col items-center justify-center p-4">
436
+ <div class="panel p-8 max-w-sm w-full shadow-lg">
437
+ <div class="flex justify-between items-center mb-6">
438
+ <h2 class="text-xl font-display text-signal">Delete Account</h2>
439
+ <button id="closeDeleteOtpModalBtn" class="text-clay hover:text-ink transition">&times;</button>
440
+ </div>
441
+ <p class="text-clay text-sm mb-6">An OTP has been sent to your email. Enter it below to permanently delete your account and all data.</p>
442
+ <div class="space-y-4">
443
+ <div>
444
+ <label class="block text-xs font-utility text-clay uppercase mb-1">6-Digit Code</label>
445
+ <input type="text" id="deleteAccountOtpCode" class="w-full bg-paper border border-ink p-3 text-ink focus:outline-none focus:border-moss text-center tracking-widest font-mono text-lg" placeholder="000000" maxlength="6">
446
+ </div>
447
+ <button id="submitDeleteOtpBtn" class="w-full bg-signal text-paper hover:bg-signal/90 transition py-3 font-utility uppercase tracking-widest text-sm mt-2">
448
+ Verify & Delete
449
+ </button>
450
+ </div>
451
+ </div>
452
+ </div>
453
+
454
+ <script type="module" src="/static/js/app.js"></script>
455
+ </body>
456
+ </html>