Shresthh03 commited on
Commit
cb00eb0
·
verified ·
1 Parent(s): 04c7f03

Update it.

Browse files
Files changed (1) hide show
  1. index.html +104 -159
index.html CHANGED
@@ -3,24 +3,25 @@
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; }
@@ -43,10 +44,8 @@
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>
@@ -72,15 +71,13 @@
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");
@@ -95,23 +92,21 @@ 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');
@@ -121,132 +116,94 @@ function appendBubble(who, text){
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;
@@ -255,51 +212,39 @@ async function sendMessage(){
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>
 
3
  <head>
4
  <meta charset="utf-8" />
5
  <meta name="viewport" content="width=device-width,initial-scale=1" />
6
+ <title>Serenity — Emotional Companion</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: breatheOrb 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: breatheHum 5s ease-in-out infinite; display:none; }
15
+ @keyframes breatheOrb { 0%{transform:scale(1);opacity:0.9;}50%{transform:scale(1.08);opacity:1;}100%{transform:scale(1);opacity:0.92;} }
16
+ @keyframes breatheHum { 0%{transform:scale(1);opacity:0.9;}50%{transform:scale(1.06);opacity:1;}100%{transform:scale(1);opacity:0.92;} }
17
  .status { font-size:14px; color:#334155; text-align:center; }
18
  .controls { display:flex; gap:8px; margin-top:6px; flex-wrap:wrap; }
19
+ .controls button, .controls select { padding:8px 12px; border-radius:10px; border:none; cursor:pointer; font-weight:600; background:rgba(255,255,255,0.9); }
20
  .main { flex:1; display:flex; flex-direction:column; gap:12px; }
21
+ .chatWindow { height:520px; background:rgba(255,255,255,0.9); border-radius:12px; padding:18px; overflow:auto; box-shadow: inset 0 1px 0 rgba(255,255,255,0.6); }
22
  .bubble { max-width:72%; padding:12px 14px; border-radius:14px; margin:8px 0; line-height:1.35; }
23
+ .bubble.user { margin-left:auto; background:linear-gradient(135deg,#e6f0ff,#d7e9ff); color:#07235a; border-bottom-right-radius:4px; text-align:right; }
24
+ .bubble.bot { margin-right:auto; background:linear-gradient(135deg,#fdf7ff,#fff1f7); color:#2b2b2b; border-bottom-left-radius:4px; text-align:left; }
25
  .typeIndicator { font-style:italic; color:#64748b; padding:6px 0; text-align:left; }
26
  .inputRow { display:flex; gap:8px; margin-top:10px; }
27
  .inputRow input { flex:1; padding:12px 14px; border-radius:12px; border:1px solid rgba(10,10,10,0.06); outline:none; font-size:16px; }
 
44
  </div>
45
 
46
  <div class="controls">
47
+ <button id="switchStyle" class="">Switch Style</button>
48
+ <select id="voiceSelect" class="rounded-lg" title="Choose voice/personality"></select>
 
 
49
  </div>
50
 
51
  <div class="small">Tip: Allow microphone access to speak</div>
 
71
  </div>
72
 
73
  <script>
74
+ /* ---------- Session + DOM refs ---------- */
75
  const sessionKey = "serenity_session";
76
  let sessionId = localStorage.getItem(sessionKey);
77
  if(!sessionId){
 
78
  sessionId = 's_' + Math.random().toString(36).slice(2,12);
79
  localStorage.setItem(sessionKey, sessionId);
80
  }
 
81
  const chatWindow = document.getElementById("chatWindow");
82
  const messageInput = document.getElementById("messageInput");
83
  const sendBtn = document.getElementById("sendBtn");
 
92
  const newChatBtn = document.getElementById("newChat");
93
  const feedbackBtn = document.getElementById("feedback");
94
 
 
95
  let recognition = null;
96
+ let listening = false;
97
  let voices = [];
98
+ const VOICES = [
99
+ {id:'calm_male', label:'Calm Male', pitch:0.85, rate:0.95},
100
+ {id:'deep_male', label:'Deep Male', pitch:0.6, rate:0.9},
101
+ {id:'soothing_male', label:'Soothing Male', pitch:0.9, rate:0.85},
102
+ {id:'gentle_female', label:'Gentle Loving Female', pitch:1.3, rate:0.95},
103
+ {id:'feminine_female', label:'Feminine Female', pitch:1.45, rate:1.0},
104
+ {id:'deep_female', label:'Deep & Soar Female', pitch:0.9, rate:0.9},
105
+ {id:'soothing_female', label:'Soothing Female', pitch:1.2, rate:0.85},
106
+ {id:'neutral', label:'Neutral Soothing', pitch:1.0, rate:1.0}
 
 
 
107
  ];
108
 
109
+ /* ---------- small UI helpers ---------- */
110
  function appendBubble(who, text){
111
  const d = document.createElement("div");
112
  d.className = 'bubble ' + (who === "user" ? 'user' : 'bot');
 
116
  }
117
  function escapeHtml(str){ return (str||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
118
 
119
+ /* ---------- voice list population ---------- */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  function populateVoiceList(){
121
+ const avail = speechSynthesis.getVoices() || [];
 
122
  voiceSelect.innerHTML = "";
123
+ VOICES.forEach(v=>{
 
124
  const opt = document.createElement("option");
125
+ opt.value = v.id;
126
+ opt.text = v.label;
127
  voiceSelect.appendChild(opt);
128
  });
129
+ const saved = localStorage.getItem("serenity_voice") || "neutral";
130
+ voiceSelect.value = saved;
131
  }
132
  populateVoiceList();
133
+ if(typeof speechSynthesis !== "undefined"){
134
  speechSynthesis.onvoiceschanged = populateVoiceList;
135
  }
136
+ voiceSelect.addEventListener("change", ()=> localStorage.setItem("serenity_voice", voiceSelect.value));
 
 
137
 
138
+ /* ---------- typing / avatar ---------- */
139
+ function showTyping(){ typingIndicator.style.display = "block"; orb.style.transform = "scale(1.08)"; }
140
+ function hideTyping(){ typingIndicator.style.display = "none"; orb.style.transform = ""; }
141
+
142
+ function speakText(text){
143
+ if(!window.speechSynthesis) return;
144
+ const utter = new SpeechSynthesisUtterance(text);
145
+ const profileId = voiceSelect.value || "neutral";
146
+ const profile = VOICES.find(v => v.id === profileId) || VOICES[7];
147
+ utter.pitch = profile.pitch;
148
+ utter.rate = profile.rate;
149
+ // choose a matching voice if possible
150
+ const avail = speechSynthesis.getVoices() || [];
151
+ let match = null;
152
+ if(profileId.includes("male")) {
153
+ match = avail.find(v=>/male|man|david|john|alex|daniel|microsoft/i.test(v.name));
154
+ } else if(profileId.includes("female")) {
155
+ match = avail.find(v=>/female|zira|susan|sarah|microsoft|victoria/i.test(v.name));
156
+ }
157
+ if(!match) match = avail.find(v=>/en-|en_/.test(v.lang)) || avail[0];
158
+ if(match) utter.voice = match;
159
+ speechSynthesis.cancel();
160
+ speechSynthesis.speak(utter);
161
  }
162
 
163
+ /* ---------- background gentle cycling ---------- */
164
+ const SOFT_GRADIENTS = [
165
+ "linear-gradient(135deg,#EFF6FF,#FFF0F6)",
166
+ "linear-gradient(135deg,#F0FDF4,#ECFEFF)",
167
+ "linear-gradient(135deg,#FEF3C7,#FCE7F3)",
168
+ "linear-gradient(135deg,#F3E8FF,#FEF3F3)",
169
+ "linear-gradient(135deg,#FDF2F8,#EEF2FF)"
170
+ ];
171
+ let bgIndex = 0;
172
+ setInterval(()=>{ bgIndex=(bgIndex+1)%SOFT_GRADIENTS.length; document.body.style.background = SOFT_GRADIENTS[bgIndex]; }, 18000);
173
+
174
+ /* ---------- mic controls ---------- */
175
+ if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window){
176
  const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
177
  recognition = new SR();
178
  recognition.lang = "en-US";
179
  recognition.interimResults = false;
180
+ recognition.onresult = (e)=> {
 
181
  const txt = e.results[e.results.length-1][0].transcript;
182
  messageInput.value = txt;
183
  sendMessage();
184
  };
185
+ recognition.onend = ()=> { listening=false; micStart.disabled=false; micStop.disabled=true; orb.style.transform=""; }
186
+ recognition.onerror = ()=> { listening=false; micStart.disabled=false; micStop.disabled=true; orb.style.transform=""; }
187
+ micStart.onclick = ()=> { try{ recognition.start(); listening=true; micStart.disabled=true; micStop.disabled=false; orb.style.transform="scale(1.08)"; }catch(e){} };
188
+ micStop.onclick = ()=> { if(recognition) recognition.stop(); listening=false; micStart.disabled=false; micStop.disabled=true; speechSynthesis.cancel(); orb.style.transform=""; };
189
+ } else { micStart.disabled=true; micStop.disabled=true; }
190
+
191
+ /* ---------- avatar switch ---------- */
192
+ switchStyle.onclick = ()=> {
193
+ if(orb.style.display === "none" || getComputedStyle(orb).display === "none") { orb.style.display=""; hum.style.display="none"; }
194
+ else { orb.style.display="none"; hum.style.display=""; }
 
 
 
 
 
195
  }
196
 
197
+ /* ---------- reset session ---------- */
198
+ newChatBtn.onclick = async ()=> {
199
  chatWindow.innerHTML = "";
200
+ try{
201
+ await fetch("/reset_session", {method:"POST", headers:{"Content-Type":"application/json"}, body: JSON.stringify({session:sessionId})});
202
+ }catch(e){}
203
+ appendBubble("bot","New conversation started. How are you feeling today?");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  }
205
 
206
+ /* ---------- send message ---------- */
207
  async function sendMessage(){
208
  const txt = messageInput.value.trim();
209
  if(!txt) return;
 
212
  showTyping();
213
 
214
  try{
215
+ const body = { session: sessionId, message: txt, personality: voiceSelect.value || "neutral" };
216
+ const res = await fetch("/chat", {method:"POST", headers:{"Content-Type":"application/json"}, body: JSON.stringify(body)});
 
 
 
217
  const j = await res.json();
218
  hideTyping();
219
+ appendBubble("bot", j.reply || j.response || "Sorry — I couldn't form a reply.");
220
+ assistantMood.textContent = ((j.emotion||"Calm").charAt(0).toUpperCase() + (j.emotion||"Calm").slice(1)) + " • Supportive";
221
+ // gentle background changes by emotion
222
+ const emotion = (j.emotion||"neutral").toLowerCase();
223
+ if(emotion==="sadness") document.body.style.background="linear-gradient(135deg,#dbeafe,#eef2ff)";
224
+ else if(emotion==="joy"||emotion==="love") document.body.style.background="linear-gradient(135deg,#fff7ed,#fff1f2)";
225
+ else if(emotion==="anger") document.body.style.background="linear-gradient(135deg,#fff1f0,#ffe7e8)";
226
+ else if(emotion==="crisis") document.body.style.background="linear-gradient(135deg,#f8fafc,#fef2f2)";
227
  else document.body.style.background = SOFT_GRADIENTS[bgIndex];
228
+ // speak
 
229
  speakText(j.reply || j.response || "");
 
 
230
  }catch(err){
231
  hideTyping();
232
+ appendBubble("bot","Oops — couldn't connect. Try again in a moment.");
233
  console.error(err);
234
  }
235
  }
 
236
  sendBtn.addEventListener("click", sendMessage);
237
+ messageInput.addEventListener("keydown", (e)=> { if(e.key==="Enter") sendMessage(); });
238
 
239
+ /* ---------- init greeting (ask name if needed) ---------- */
240
  (async function init(){
 
241
  try{
242
+ const res = await fetch("/chat", {method:"POST", headers:{"Content-Type":"application/json"}, body: JSON.stringify({session:sessionId, init:true})});
 
 
 
 
243
  const j = await res.json();
244
+ appendBubble("bot", j.reply || "Hey there I'm Serenity. What's your name?");
245
+ assistantMood.textContent = (j.emotion || "Calm") + " • Supportive";
246
  }catch(e){
247
+ appendBubble("bot","Hey there I'm Serenity. What's your name?");
248
  }
249
  })();
250
  </script>