Shresthh03 commited on
Commit
cc3ff6c
·
verified ·
1 Parent(s): 536604a

Update it all.

Browse files
Files changed (1) hide show
  1. index.html +276 -129
index.html CHANGED
@@ -1,160 +1,307 @@
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>💛 Emotional Support Assistant</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <style>
9
- body {
10
- transition: background 3s ease-in-out;
11
- background: linear-gradient(135deg, #cce6ff, #fef6ff);
12
- }
13
- .chat-container {
14
- max-width: 500px;
15
- margin: 50px auto;
16
- background: rgba(255,255,255,0.25);
17
- backdrop-filter: blur(12px);
18
- border-radius: 20px;
19
- box-shadow: 0 8px 32px rgba(0,0,0,0.1);
20
- padding: 20px;
21
- }
22
- .avatar {
23
- width: 100px;
24
- height: 100px;
25
- border-radius: 50%;
26
- margin: auto;
27
- animation: breathe 4s ease-in-out infinite;
28
- background: radial-gradient(circle at center, #a0c4ff, #4361ee);
29
- }
30
- @keyframes breathe {
31
- 0%, 100% { transform: scale(1); opacity: 0.9; }
32
- 50% { transform: scale(1.1); opacity: 1; }
33
- }
34
- .humanoid {
35
- width: 100px; height: 100px;
36
- border-radius: 50%;
37
- background: radial-gradient(circle at 50% 30%, #f8f9fa 20%, #4361ee 80%);
38
- animation: breathe 5s infinite ease-in-out;
39
- }
40
- .hidden { display: none; }
41
  </style>
42
  </head>
43
- <body class="flex flex-col items-center justify-center h-screen">
44
-
45
- <div class="chat-container text-center">
46
- <div id="orb" class="avatar"></div>
47
- <div id="humanoid" class="humanoid hidden"></div>
48
- <button id="switchAvatar" class="text-sm mt-2 underline text-blue-700">Switch Style</button>
49
-
50
- <div id="chat" class="h-80 overflow-y-auto text-left p-3 text-gray-800"></div>
51
- <input id="userInput" class="w-full mt-4 p-2 rounded-md border" placeholder="Type your message..." />
52
- <div class="mt-3 flex justify-around">
53
- <button id="sendBtn" class="bg-blue-500 text-white px-4 py-2 rounded-md">Send</button>
54
- <button id="startBtn" class="bg-green-500 text-white px-4 py-2 rounded-md">🎙️ Start</button>
55
- <button id="stopBtn" class="bg-red-500 text-white px-4 py-2 rounded-md">⏹️ Stop</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  </div>
57
- <select id="voiceSelect" class="mt-3 w-full rounded-md p-2 border"></select>
58
  </div>
59
 
60
  <script>
61
- let recognizing = false;
62
- let recognition;
63
- let voices = [];
64
- let selectedVoice;
65
- const synth = window.speechSynthesis;
66
-
67
- function populateVoices() {
68
- voices = synth.getVoices();
69
- const voiceSelect = document.getElementById("voiceSelect");
70
- voiceSelect.innerHTML = voices.map(v => `<option>${v.name}</option>`).join("");
71
- voiceSelect.onchange = () => {
72
- selectedVoice = voices.find(v => v.name === voiceSelect.value);
73
- };
74
  }
75
- populateVoices();
76
- if (speechSynthesis.onvoiceschanged !== undefined) {
77
- speechSynthesis.onvoiceschanged = populateVoices;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  }
 
79
 
80
- document.getElementById("switchAvatar").onclick = () => {
81
- document.getElementById("orb").classList.toggle("hidden");
82
- document.getElementById("humanoid").classList.toggle("hidden");
83
- };
84
-
85
- function changeBackground(emotion) {
86
- const map = {
87
- joy: "linear-gradient(135deg,#fff9c4,#ffecb3)",
88
- sadness: "linear-gradient(135deg,#a7c7e7,#d4f1f4)",
89
- anger: "linear-gradient(135deg,#ffb3b3,#ff6666)",
90
- love: "linear-gradient(135deg,#ffc1cc,#ffb6c1)",
91
- fear: "linear-gradient(135deg,#c8d8e4,#a7bed3)",
92
- crisis: "linear-gradient(135deg,#444,#ff5555)"
93
- };
94
- document.body.style.background = map[emotion] || "linear-gradient(135deg,#cce6ff,#fef6ff)";
95
  }
 
96
 
97
- function appendMessage(sender, text) {
98
- const chat = document.getElementById("chat");
99
- const msg = document.createElement("div");
100
- msg.className = sender === "user" ? "text-right mb-2" : "text-left mb-2";
101
- msg.innerHTML = `<b>${sender}:</b> ${text}`;
102
- chat.appendChild(msg);
103
- chat.scrollTop = chat.scrollHeight;
 
 
104
  }
105
 
106
- async function sendMessage() {
107
- const input = document.getElementById("userInput");
108
- const message = input.value.trim();
109
- if (!message) return;
110
- appendMessage("You", message);
111
- input.value = "";
112
-
113
- const res = await fetch("/chat", {
114
- method: "POST",
115
- headers: { "Content-Type": "application/json" },
116
- body: JSON.stringify({ message })
117
  });
118
- const data = await res.json();
119
- appendMessage("Assistant", data.reply);
120
- changeBackground(data.emotion);
121
- speak(data.reply);
122
  }
123
-
124
- function speak(text) {
125
- if (!text) return;
126
- const utter = new SpeechSynthesisUtterance(text);
127
- utter.voice = selectedVoice || voices[0];
128
- utter.pitch = 0.8;
129
- utter.rate = 1.0;
130
- synth.speak(utter);
131
  }
 
 
 
132
 
133
- document.getElementById("sendBtn").onclick = sendMessage;
 
 
 
 
 
 
 
 
134
 
135
- if ("webkitSpeechRecognition" in window) {
136
- recognition = new webkitSpeechRecognition();
137
- recognition.continuous = false;
 
138
  recognition.lang = "en-US";
 
 
139
  recognition.onresult = (e) => {
140
- const transcript = e.results[0][0].transcript;
141
- document.getElementById("userInput").value = transcript;
142
  sendMessage();
143
  };
144
- document.getElementById("startBtn").onclick = () => {
145
- if (!recognizing) {
146
- recognizing = true;
147
- recognition.start();
148
- }
149
- };
150
- document.getElementById("stopBtn").onclick = () => {
151
- if (recognizing) {
152
- recognizing = false;
153
- recognition.stop();
154
- synth.cancel();
155
- }
156
- };
157
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  </script>
159
  </body>
160
  </html>
 
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" />
6
+ <title>Serenity Emotional Support</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <style>
9
+ body { transition: background 1.2s ease; background: linear-gradient(135deg,#f2f6ff,#fff0f6); font-family: Inter, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue"; }
10
+ .card { max-width:960px; margin:28px auto; display:flex; gap:20px; align-items:flex-start; padding:20px; border-radius:18px; background: rgba(255,255,255,0.55); box-shadow: 0 10px 30px rgba(12,12,20,0.08); backdrop-filter: blur(8px); }
11
+ .left { width:220px; display:flex; flex-direction:column; align-items:center; gap:12px; }
12
+ .avatarOrb, .avatarHum { width:150px; height:150px; border-radius:50%; display:flex; align-items:center; justify-content:center; }
13
+ .avatarOrb { background: radial-gradient(circle at 30% 30%, rgba(255,255,255,0.5), rgba(255,255,255,0.05)), radial-gradient(circle at 60% 60%, rgba(120,170,255,0.5), rgba(80,100,255,0.2)); box-shadow: 0 6px 30px rgba(80,110,255,0.18); animation: breathe 4.5s ease-in-out infinite; }
14
+ .avatarHum { background: radial-gradient(circle at 30% 25%, rgba(255,255,255,0.7), rgba(255,255,255,0.05)), linear-gradient(135deg,#fef3c7,#fbcfe8); box-shadow: 0 6px 30px rgba(200,120,200,0.12); animation: breathe 5s ease-in-out infinite; display:none; }
15
+ @keyframes breathe { 0%{transform:scale(1);opacity:0.9;}50%{transform:scale(1.08);opacity:1;}100%{transform:scale(1);opacity:0.92;} }
16
+ .status { font-size:14px; color:#334155; text-align:center; }
17
+ .controls { display:flex; gap:8px; margin-top:6px; flex-wrap:wrap; }
18
+ .controls button, .controls select { padding:8px 12px; border-radius:10px; border:none; cursor:pointer; font-weight:600; }
19
+ .main { flex:1; display:flex; flex-direction:column; gap:12px; }
20
+ .chatWindow { height:520px; background:rgba(255,255,255,0.85); border-radius:12px; padding:18px; overflow:auto; box-shadow: inset 0 1px 0 rgba(255,255,255,0.6); }
21
+ .bubble { max-width:72%; padding:12px 14px; border-radius:14px; margin:8px 0; line-height:1.35; }
22
+ .bubble.user { margin-left:auto; background:linear-gradient(135deg,#e6f0ff,#d7e9ff); color:#07235a; border-bottom-right-radius:4px; }
23
+ .bubble.bot { margin-right:auto; background:linear-gradient(135deg,#fdf7ff,#fff1f7); color:#2b2b2b; border-bottom-left-radius:4px; }
24
+ .typeIndicator { font-style:italic; color:#64748b; padding:6px 0; text-align:left; }
25
+ .inputRow { display:flex; gap:8px; margin-top:10px; }
26
+ .inputRow input { flex:1; padding:12px 14px; border-radius:12px; border:1px solid rgba(10,10,10,0.06); outline:none; font-size:16px; }
27
+ .inputRow button { padding:10px 14px; border-radius:10px; border:none; cursor:pointer; font-weight:700; }
28
+ .footerBtns { margin-top:12px; display:flex; gap:8px; justify-content:flex-end; align-items:center; }
29
+ .small { font-size:12px; color:#475569; }
30
+ @media (max-width:820px) { .card{flex-direction:column; padding:14px;} .left{width:100%; flex-direction:row; justify-content:space-between;} .avatarOrb,.avatarHum{width:80px;height:80px;} .chatWindow{height:340px;} }
 
 
 
 
 
 
 
 
 
 
31
  </style>
32
  </head>
33
+ <body>
34
+
35
+ <div class="card">
36
+ <div class="left">
37
+ <div id="orb" class="avatarOrb" aria-hidden="true"></div>
38
+ <div id="hum" class="avatarHum" aria-hidden="true"></div>
39
+
40
+ <div class="status">
41
+ <div id="assistantName" class="text-lg font-semibold">Serenity</div>
42
+ <div id="assistantMood" class="small">Calm Supportive</div>
43
+ </div>
44
+
45
+ <div class="controls">
46
+ <button id="switchStyle" class="bg-white/80">Switch Style</button>
47
+ <select id="voiceSelect" class="rounded-lg">
48
+ <!-- options populated by JS -->
49
+ </select>
50
+ </div>
51
+
52
+ <div class="small">Tip: Allow microphone access to speak</div>
53
+ </div>
54
+
55
+ <div class="main">
56
+ <div id="chatWindow" class="chatWindow" role="log" aria-live="polite"></div>
57
+
58
+ <div id="typing" class="typeIndicator" style="display:none">Serenity is typing…</div>
59
+
60
+ <div class="inputRow">
61
+ <input id="messageInput" placeholder="Type your message..." autocomplete="off" />
62
+ <button id="sendBtn" class="bg-indigo-600 text-white">Send</button>
63
+ <button id="micStart" class="bg-green-500 text-white">🎙️</button>
64
+ <button id="micStop" class="bg-red-500 text-white">⏹️</button>
65
+ </div>
66
+
67
+ <div class="footerBtns">
68
+ <button id="newChat" class="bg-gray-100 px-3">New Chat</button>
69
+ <button id="feedback" class="bg-amber-100 px-3">Feedback</button>
70
+ </div>
71
  </div>
 
72
  </div>
73
 
74
  <script>
75
+ /* ---------- Utilities and session setup ---------- */
76
+ const sessionKey = "serenity_session";
77
+ let sessionId = localStorage.getItem(sessionKey);
78
+ if(!sessionId){
79
+ // create simple random id
80
+ sessionId = 's_' + Math.random().toString(36).slice(2,12);
81
+ localStorage.setItem(sessionKey, sessionId);
 
 
 
 
 
 
82
  }
83
+
84
+ const chatWindow = document.getElementById("chatWindow");
85
+ const messageInput = document.getElementById("messageInput");
86
+ const sendBtn = document.getElementById("sendBtn");
87
+ const typingIndicator = document.getElementById("typing");
88
+ const orb = document.getElementById("orb");
89
+ const hum = document.getElementById("hum");
90
+ const switchStyle = document.getElementById("switchStyle");
91
+ const assistantMood = document.getElementById("assistantMood");
92
+ const voiceSelect = document.getElementById("voiceSelect");
93
+ const micStart = document.getElementById("micStart");
94
+ const micStop = document.getElementById("micStop");
95
+ const newChatBtn = document.getElementById("newChat");
96
+ const feedbackBtn = document.getElementById("feedback");
97
+
98
+ let listening = false;
99
+ let recognition = null;
100
+ let voices = [];
101
+ let chosenVoice = null;
102
+
103
+ /* ---------- Voice profiles list (names represent target style; actual voice chosen from browser) ---------- */
104
+ const VOICE_PROFILES = [
105
+ { id:'calm_male', label:'Calm Male', pitch:0.85, rate:0.95 },
106
+ { id:'deep_male', label:'Deep Male', pitch:0.6, rate:0.9 },
107
+ { id:'soothing_male', label:'Soothing Male', pitch:0.9, rate:0.85 },
108
+ { id:'gentle_female', label:'Gentle Loving Female', pitch:1.3, rate:0.95 },
109
+ { id:'feminine_female', label:'Feminine Female', pitch:1.45, rate:1.0 },
110
+ { id:'deep_female', label:'Deep & Soar Female', pitch:0.9, rate:0.9 },
111
+ { id:'soothing_female', label:'Soothing Female', pitch:1.2, rate:0.85 },
112
+ { id:'neutral', label:'Neutral Soothing', pitch:1.0, rate:1.0 }
113
+ ];
114
+
115
+ function appendBubble(who, text){
116
+ const d = document.createElement("div");
117
+ d.className = 'bubble ' + (who === "user" ? 'user' : 'bot');
118
+ d.innerHTML = `<strong>${who === "user" ? "You" : "Serenity"}:</strong> ${escapeHtml(text)}`;
119
+ chatWindow.appendChild(d);
120
+ chatWindow.scrollTop = chatWindow.scrollHeight;
121
  }
122
+ function escapeHtml(str){ return (str||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
123
 
124
+ /* ---------- color transitions (soft palette) ---------- */
125
+ const SOFT_GRADIENTS = [
126
+ "linear-gradient(135deg,#EFF6FF,#FFF0F6)", // blue->pink
127
+ "linear-gradient(135deg,#F0FDF4,#ECFEFF)", // green -> aqua
128
+ "linear-gradient(135deg,#FEF3C7,#FCE7F3)", // warm
129
+ "linear-gradient(135deg,#F3E8FF,#FEF3F3)", // lavender
130
+ "linear-gradient(135deg,#FDF2F8,#EEF2FF)" // pastel
131
+ ];
132
+ let bgIndex = 0;
133
+ function cycleBackground() {
134
+ bgIndex = (bgIndex + 1) % SOFT_GRADIENTS.length;
135
+ document.body.style.background = SOFT_GRADIENTS[bgIndex];
 
 
 
136
  }
137
+ setInterval(cycleBackground, 18000); // every 18s
138
 
139
+ /* ---------- Avatar breathing + thinking animation control ---------- */
140
+ function setAvatarThinking(on){
141
+ if(on){
142
+ orb.style.transform = "scale(1.08)";
143
+ orb.style.boxShadow = "0 10px 40px rgba(80,110,255,0.28)";
144
+ } else {
145
+ orb.style.transform = "";
146
+ orb.style.boxShadow = "";
147
+ }
148
  }
149
 
150
+ /* ---------- Populate voice select using available browser voices ---------- */
151
+ function populateVoiceList(){
152
+ voices = speechSynthesis.getVoices() || [];
153
+ // create optgroups: prefer names that contain "male"/"female" etc but fallback to first N
154
+ voiceSelect.innerHTML = "";
155
+ // add a "best match" option for each profile
156
+ VOICE_PROFILES.forEach(profile=>{
157
+ const opt = document.createElement("option");
158
+ opt.value = profile.id;
159
+ opt.text = profile.label;
160
+ voiceSelect.appendChild(opt);
161
  });
162
+ // choose default
163
+ voiceSelect.value = localStorage.getItem("serenity_voice") || "neutral";
 
 
164
  }
165
+ populateVoiceList();
166
+ if (typeof speechSynthesis !== "undefined") {
167
+ speechSynthesis.onvoiceschanged = populateVoiceList;
 
 
 
 
 
168
  }
169
+ voiceSelect.addEventListener("change", ()=>{
170
+ localStorage.setItem("serenity_voice", voiceSelect.value);
171
+ });
172
 
173
+ /* ---------- Typing indicator helpers ---------- */
174
+ function showTyping(){
175
+ typingIndicator.style.display = "block";
176
+ setAvatarThinking(true);
177
+ }
178
+ function hideTyping(){
179
+ typingIndicator.style.display = "none";
180
+ setAvatarThinking(false);
181
+ }
182
 
183
+ /* ---------- Microphone: Web Speech API ---------- */
184
+ if ("webkitSpeechRecognition" in window || "SpeechRecognition" in window){
185
+ const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
186
+ recognition = new SR();
187
  recognition.lang = "en-US";
188
+ recognition.interimResults = false;
189
+ recognition.continuous = false;
190
  recognition.onresult = (e) => {
191
+ const txt = e.results[e.results.length-1][0].transcript;
192
+ messageInput.value = txt;
193
  sendMessage();
194
  };
195
+ recognition.onend = () => { listening = false; micStart.disabled=false; micStop.disabled=true; setAvatarThinking(false); }
196
+ recognition.onerror = (ev) => { listening = false; micStart.disabled=false; micStop.disabled=true; setAvatarThinking(false); console.warn("speech error",ev); }
197
+ micStart.onclick = () => { try { recognition.start(); listening=true; micStart.disabled=true; micStop.disabled=false; setAvatarThinking(true);} catch(e){ console.warn(e);} };
198
+ micStop.onclick = () => { if(recognition) recognition.stop(); listening=false; micStart.disabled=false; micStop.disabled=true; setAvatarThinking(false); speechSynthesis.cancel(); };
199
+ } else {
200
+ micStart.disabled = true; micStop.disabled = true;
 
 
 
 
 
 
 
201
  }
202
+
203
+ /* ---------- Avatar switch ---------- */
204
+ switchStyle.onclick = () => {
205
+ if(orb.style.display === "none" || getComputedStyle(orb).display === "none"){
206
+ orb.style.display = ""; hum.style.display = "none";
207
+ } else {
208
+ orb.style.display = "none"; hum.style.display = "";
209
+ }
210
+ }
211
+
212
+ /* ---------- New chat / feedback ---------- */
213
+ newChatBtn.onclick = () => {
214
+ chatWindow.innerHTML = "";
215
+ // clear memory for session
216
+ fetch("/reset_session", {method:"POST", headers:{"Content-Type":"application/json"}, body:JSON.stringify({session:sessionId})}).catch(()=>{});
217
+ appendBubbleLocal("Serenity","New conversation started. How are you feeling today?");
218
+ }
219
+ feedbackBtn.onclick = ()=>{ window.alert("Thanks — we appreciate feedback. ❤️"); }
220
+
221
+ /* append local bot message */
222
+ function appendBubbleLocal(name,text){ appendBubble("bot", text); }
223
+
224
+ /* ---------- Speak text with profile mapping ---------- */
225
+ function speakText(text){
226
+ if(!text) return;
227
+ // build utterance
228
+ const utter = new SpeechSynthesisUtterance(text);
229
+ const profileId = voiceSelect.value || "neutral";
230
+ const profile = VOICE_PROFILES.find(p=>p.id===profileId) || VOICE_PROFILES[7];
231
+ utter.pitch = profile.pitch;
232
+ utter.rate = profile.rate;
233
+ // try to select an actual voice that matches gender/character
234
+ const available = speechSynthesis.getVoices();
235
+ let match = null;
236
+ const prefer = ["male","microsoft","david","mark","john","alex","daniel","robert","mike"];
237
+ const preferF = ["female","zira","sarah","linda","kate","mary","victoria","eva"];
238
+ if(profileId.includes("male")){
239
+ match = available.find(v => prefer.some(p=>v.name.toLowerCase().includes(p)));
240
+ } else if(profileId.includes("female")){
241
+ match = available.find(v => preferF.some(p=>v.name.toLowerCase().includes(p)));
242
+ }
243
+ if(!match) match = available.find(v=>v.lang && v.lang.toLowerCase().includes("en")) || available[0];
244
+ if(match) utter.voice = match;
245
+ speechSynthesis.cancel();
246
+ speechSynthesis.speak(utter);
247
+ }
248
+
249
+ /* ---------- Send message to backend ---------- */
250
+ async function sendMessage(){
251
+ const txt = messageInput.value.trim();
252
+ if(!txt) return;
253
+ appendBubble("user", txt);
254
+ messageInput.value = "";
255
+ showTyping();
256
+
257
+ try{
258
+ const res = await fetch("/chat", {
259
+ method:"POST",
260
+ headers:{"Content-Type":"application/json"},
261
+ body: JSON.stringify({ session: sessionId, message: txt })
262
+ });
263
+ const j = await res.json();
264
+ hideTyping();
265
+ appendBubble("bot", j.reply || j.response || "Sorry — something went wrong.");
266
+ assistantMood.textContent = (j.emotion ? j.emotion.charAt(0).toUpperCase()+j.emotion.slice(1) : "Calm") + " • Supportive";
267
+ // gentle background change per emotion
268
+ const e = (j.emotion || "neutral").toLowerCase();
269
+ if(e==="sadness") document.body.style.background="linear-gradient(135deg,#dbeafe,#eef2ff)";
270
+ else if(e==="joy"||e==="love") document.body.style.background="linear-gradient(135deg,#fff7ed,#fff1f2)";
271
+ else if(e==="anger") document.body.style.background="linear-gradient(135deg,#fff1f0,#ffe7e8)";
272
+ else if(e==="crisis") document.body.style.background="linear-gradient(135deg,#f8fafc,#fef2f2)";
273
+ else document.body.style.background = SOFT_GRADIENTS[bgIndex];
274
+ // avatar thinking pulse while speaking
275
+ setAvatarThinking(true);
276
+ speakText(j.reply || j.response || "");
277
+ // stop thinking after speak ends
278
+ setTimeout(()=> setAvatarThinking(false), 900);
279
+ }catch(err){
280
+ hideTyping();
281
+ appendBubble("bot","Oops — I couldn't connect. Try again in a moment.");
282
+ console.error(err);
283
+ }
284
+ }
285
+
286
+ sendBtn.addEventListener("click", sendMessage);
287
+ messageInput.addEventListener("keydown", (e)=>{ if(e.key==="Enter") sendMessage(); });
288
+
289
+ /* ---------- On first load, request init greeting once ---------- */
290
+ (async function init(){
291
+ // call backend with init flag so it responds only once
292
+ try{
293
+ const res = await fetch("/chat", {
294
+ method:"POST",
295
+ headers:{"Content-Type":"application/json"},
296
+ body: JSON.stringify({ session: sessionId, init: true })
297
+ });
298
+ const j = await res.json();
299
+ appendBubble("bot", j.reply || "Hey there! I'm Serenity — what's your name?");
300
+ assistantMood.textContent = (j.emotion ? j.emotion : "Calm") + " • Supportive";
301
+ }catch(e){
302
+ appendBubble("bot","Hey there! I'm Serenity — what's your name?");
303
+ }
304
+ })();
305
  </script>
306
  </body>
307
  </html>