Subh775 commited on
Commit
44d4f7a
·
1 Parent(s): 1236464

UI/UX;backend optim..;bug fix..

Browse files
Files changed (5) hide show
  1. config.py +1 -0
  2. graph.py +23 -31
  3. static/app.js +369 -530
  4. static/index.html +163 -57
  5. static/style.css +620 -768
config.py CHANGED
@@ -31,3 +31,4 @@ DB_PATH = "/data/stemgraph.db" if os.path.isdir("/data") else "stemgraph.db"
31
  # --- Defaults ---
32
  DEFAULT_PERSONA = "nerd"
33
  RETRIEVAL_TOP_K = 5
 
 
31
  # --- Defaults ---
32
  DEFAULT_PERSONA = "nerd"
33
  RETRIEVAL_TOP_K = 5
34
+ LLM_TIMEOUT = 60 # seconds — prevents indefinite hangs on slow models
graph.py CHANGED
@@ -13,7 +13,7 @@ and rotated per-category to avoid per-model rate limits.
13
 
14
  from langgraph.graph import StateGraph, START, END
15
  from langgraph.graph.message import add_messages
16
- from langchain_core.messages import BaseMessage, SystemMessage, HumanMessage
17
  from langchain_core.runnables import RunnableConfig
18
  from langchain_openrouter import ChatOpenRouter
19
  from typing import TypedDict, Annotated
@@ -23,8 +23,9 @@ import time
23
  import json
24
  import urllib.request
25
  import threading
 
26
  from langgraph.checkpoint.sqlite import SqliteSaver
27
- from config import OPENROUTER_API_KEY, DB_PATH
28
  import prompts
29
 
30
 
@@ -49,12 +50,10 @@ _BLACKLISTED_MODELS = frozenset([
49
  def _is_blacklisted(model_id: str) -> bool:
50
  """Check if model is a known guardrail/safety classifier."""
51
  mid = model_id.lower()
52
- # Exact prefix matches
53
  for bl in _BLACKLISTED_MODELS:
54
  if mid.startswith(bl):
55
  return True
56
- # Heuristic: any model with 'guard' or 'shield' in its name
57
- base = mid.split(':')[0] # strip ':free'
58
  return any(kw in base for kw in ('guard', 'shield', 'safety', 'moderation'))
59
 
60
 
@@ -180,7 +179,6 @@ def _fetch_pools() -> dict:
180
  mid = m.get("id", "")
181
  if not mid.endswith(":free"):
182
  continue
183
- # Skip known safety/guardrail/classifier models
184
  if _is_blacklisted(mid):
185
  continue
186
 
@@ -194,16 +192,15 @@ def _fetch_pools() -> dict:
194
  if has_vision:
195
  vision_all.append(entry)
196
 
197
- # Sort high→low for reasoning, low→high for casual
198
  text_all.sort(key=lambda x: x["score"], reverse=True)
199
  vision_all.sort(key=lambda x: x["score"], reverse=True)
200
 
201
  pools = {
202
- "reasoning": text_all[:6], # best 6
203
- "science": text_all[:8], # best 8
204
- "general": text_all[:10], # best 10
205
  "casual": text_all[-5:][::-1] if len(text_all) >= 5 else text_all[:3],
206
- "vision": vision_all, # all vision-capable
207
  }
208
 
209
  with _lock:
@@ -242,8 +239,6 @@ def _pick(category: str, has_image: bool = False) -> tuple[str, str]:
242
  idx = _counters.get("vision", 0) % len(v)
243
  _counters["vision"] = idx + 1
244
  return v[idx]["id"], "vision"
245
- # No vision models — caller must strip images
246
- # Fall through to text routing
247
 
248
  pool = pools.get(category) or pools.get("general") or []
249
  if not pool:
@@ -282,7 +277,6 @@ def _strip_images(messages: list) -> list:
282
  text = " ".join(t for t in text_parts if t).strip()
283
  if not text:
284
  text = "(user sent an image)"
285
- # Preserve original message class (HumanMessage, AIMessage, etc.)
286
  out.append(msg.__class__(content=text))
287
  else:
288
  out.append(msg)
@@ -302,7 +296,8 @@ def _make_llm(api_key: str, model_id: str):
302
  openrouter_api_key=key,
303
  temperature=0.5,
304
  max_tokens=4096,
305
- max_retries=3,
 
306
  streaming=True,
307
  )
308
 
@@ -334,19 +329,14 @@ def chat_node(state: ChatState, config: RunnableConfig):
334
  else:
335
  model_id, actual = _pick(category, has_image=has_image)
336
 
337
- # 3) ALWAYS strip images from history when model is not vision-capable.
338
- # LangGraph checkpoint may contain old image messages from prior turns
339
- # that would cause "No endpoints found that support image input" on
340
- # text-only models.
341
  messages = state["messages"]
342
  if actual != "vision":
343
  messages = _strip_images(messages)
344
 
345
- # 4) Build system prompt & invoke
346
  base_prompt = prompts.build(persona, context, language, username, profile)
347
 
348
- # If the user sent an image, add explicit vision instruction so the model
349
- # doesn't just output safety labels or ignore the image.
350
  if has_image:
351
  base_prompt += (
352
  "\n\nIMAGE HANDLING:\n"
@@ -360,24 +350,26 @@ def chat_node(state: ChatState, config: RunnableConfig):
360
  sys = SystemMessage(content=base_prompt)
361
  print(f"[ROUTER] category={category} model={model_id} vision={actual == 'vision'}")
362
 
363
- # Retry loop for rate limits or transient errors
364
  max_attempts = 3
 
365
  for attempt in range(max_attempts):
366
  try:
367
  llm = _make_llm(api_key, model_id)
368
  resp = llm.invoke([sys] + messages)
369
  return {"messages": [resp]}
370
  except Exception as e:
371
- err_str = str(e)
372
- print(f"[ROUTER] Model {model_id} failed on attempt {attempt+1}/{max_attempts}: {err_str[:100]}")
 
 
373
  if attempt < max_attempts - 1:
374
- # Pick a different model and try again
375
  model_id, actual = _pick(category, has_image=has_image)
376
- print(f"[ROUTER] Retrying with new model {model_id}")
377
- else:
378
- # Last attempt failed
379
- from langchain_core.messages import AIMessage
380
- return {"messages": [AIMessage(content="I'm experiencing high traffic right now. Could you please try again in a moment?")]}
381
 
382
 
383
  # ── Compile graph ─────────────────────────────────────────────
 
13
 
14
  from langgraph.graph import StateGraph, START, END
15
  from langgraph.graph.message import add_messages
16
+ from langchain_core.messages import BaseMessage, SystemMessage, HumanMessage, AIMessage
17
  from langchain_core.runnables import RunnableConfig
18
  from langchain_openrouter import ChatOpenRouter
19
  from typing import TypedDict, Annotated
 
23
  import json
24
  import urllib.request
25
  import threading
26
+ import traceback
27
  from langgraph.checkpoint.sqlite import SqliteSaver
28
+ from config import OPENROUTER_API_KEY, DB_PATH, LLM_TIMEOUT
29
  import prompts
30
 
31
 
 
50
  def _is_blacklisted(model_id: str) -> bool:
51
  """Check if model is a known guardrail/safety classifier."""
52
  mid = model_id.lower()
 
53
  for bl in _BLACKLISTED_MODELS:
54
  if mid.startswith(bl):
55
  return True
56
+ base = mid.split(':')[0]
 
57
  return any(kw in base for kw in ('guard', 'shield', 'safety', 'moderation'))
58
 
59
 
 
179
  mid = m.get("id", "")
180
  if not mid.endswith(":free"):
181
  continue
 
182
  if _is_blacklisted(mid):
183
  continue
184
 
 
192
  if has_vision:
193
  vision_all.append(entry)
194
 
 
195
  text_all.sort(key=lambda x: x["score"], reverse=True)
196
  vision_all.sort(key=lambda x: x["score"], reverse=True)
197
 
198
  pools = {
199
+ "reasoning": text_all[:6],
200
+ "science": text_all[:8],
201
+ "general": text_all[:10],
202
  "casual": text_all[-5:][::-1] if len(text_all) >= 5 else text_all[:3],
203
+ "vision": vision_all,
204
  }
205
 
206
  with _lock:
 
239
  idx = _counters.get("vision", 0) % len(v)
240
  _counters["vision"] = idx + 1
241
  return v[idx]["id"], "vision"
 
 
242
 
243
  pool = pools.get(category) or pools.get("general") or []
244
  if not pool:
 
277
  text = " ".join(t for t in text_parts if t).strip()
278
  if not text:
279
  text = "(user sent an image)"
 
280
  out.append(msg.__class__(content=text))
281
  else:
282
  out.append(msg)
 
296
  openrouter_api_key=key,
297
  temperature=0.5,
298
  max_tokens=4096,
299
+ max_retries=1,
300
+ timeout=LLM_TIMEOUT,
301
  streaming=True,
302
  )
303
 
 
329
  else:
330
  model_id, actual = _pick(category, has_image=has_image)
331
 
332
+ # 3) Strip images from history when model is not vision-capable
 
 
 
333
  messages = state["messages"]
334
  if actual != "vision":
335
  messages = _strip_images(messages)
336
 
337
+ # 4) Build system prompt
338
  base_prompt = prompts.build(persona, context, language, username, profile)
339
 
 
 
340
  if has_image:
341
  base_prompt += (
342
  "\n\nIMAGE HANDLING:\n"
 
350
  sys = SystemMessage(content=base_prompt)
351
  print(f"[ROUTER] category={category} model={model_id} vision={actual == 'vision'}")
352
 
353
+ # 5) Invoke with model rotation on failure
354
  max_attempts = 3
355
+ last_error = None
356
  for attempt in range(max_attempts):
357
  try:
358
  llm = _make_llm(api_key, model_id)
359
  resp = llm.invoke([sys] + messages)
360
  return {"messages": [resp]}
361
  except Exception as e:
362
+ last_error = e
363
+ print(f"[ROUTER] Model {model_id} failed (attempt {attempt+1}/{max_attempts}):")
364
+ print(f"[ROUTER] {type(e).__name__}: {str(e)[:200]}")
365
+ traceback.print_exc()
366
  if attempt < max_attempts - 1:
 
367
  model_id, actual = _pick(category, has_image=has_image)
368
+ print(f"[ROUTER] Rotating to model: {model_id}")
369
+
370
+ # All attempts failed — raise so app.py SSE handler can show the error
371
+ print(f"[ROUTER] All {max_attempts} attempts failed. Raising last error.")
372
+ raise last_error
373
 
374
 
375
  # ── Compile graph ─────────────────────────────────────────────
static/app.js CHANGED
@@ -2,10 +2,12 @@
2
  STEM Copilot — Application Logic
3
  Auth, BYOK, Chat, Settings, Streaming MD, PWA, Sidebar,
4
  Theme Toggle, Teaching Style Selector, Voice Input,
5
- Copy/Regenerate actions, Image Preview
 
6
  ============================================================ */
7
 
8
- /* ----- State ----- */
 
9
  let currentUser = null;
10
  let currentThreadId = crypto.randomUUID();
11
  const threads = [];
@@ -18,10 +20,10 @@ let currentPersona = localStorage.getItem('stemcopilot_persona') || 'vidyut';
18
  let currentLanguage = localStorage.getItem('stemcopilot_language') || 'auto';
19
  let currentUsername = localStorage.getItem('stemcopilot_username') || '';
20
 
 
21
 
22
- /* ============================================================
23
- DOM REFERENCES
24
- ============================================================ */
25
 
26
  const loginScreen = document.getElementById('loginScreen');
27
  const byokScreen = document.getElementById('byokScreen');
@@ -36,12 +38,12 @@ const welcomeScreen = document.getElementById('welcomeScreen');
36
  const bottomInputContainer = document.getElementById('bottomInputContainer');
37
 
38
  const userInput = document.getElementById('userInput');
39
- const sendBtn = document.getElementById('sendBtn');
40
  const newChatBtn = document.getElementById('newChatBtn');
41
  const stopBtn = document.getElementById('stopBtn');
 
42
 
43
  const heroInput = document.getElementById('heroInput');
44
- const heroSendBtn = document.getElementById('heroSendBtn');
45
  const heroUploadBtn = document.getElementById('heroUploadBtn');
46
  const heroImageInput = document.getElementById('heroImageInput');
47
  const heroTitle = document.getElementById('heroTitle');
@@ -63,8 +65,6 @@ const railAvatar = document.getElementById('railAvatar');
63
  const settingsOverlay = document.getElementById('settingsOverlay');
64
  const openSettingsBtn = document.getElementById('openSettingsBtn');
65
  const settingsCloseBtn = document.getElementById('settingsCloseBtn');
66
- const settingsModal = document.getElementById('settingsModal');
67
- const feedbackMenuBtn = document.getElementById('feedbackMenuBtn');
68
 
69
  const usernameInput = document.getElementById('usernameInput');
70
  const languageSelect = document.getElementById('languageSelect');
@@ -83,41 +83,60 @@ const installBanner = document.getElementById('installBanner');
83
  const installBtn = document.getElementById('installBtn');
84
  const installDismiss = document.getElementById('installDismiss');
85
 
86
- const themeToggleBtn = document.getElementById('themeToggleBtn');
87
-
88
- // Hero image preview
89
  const heroImagePreview = document.getElementById('heroImagePreview');
90
  const heroImagePreviewThumb = document.getElementById('heroImagePreviewThumb');
91
  const heroImagePreviewRemove = document.getElementById('heroImagePreviewRemove');
92
 
93
- // Style selector in input bar
94
  const styleSelectorBtn = document.getElementById('styleSelectorBtn');
95
  const styleSelectorLabel = document.getElementById('styleSelectorLabel');
96
  const styleDropdown = document.getElementById('styleDropdown');
97
 
98
- // Mic buttons
99
- const micBtn = document.getElementById('micBtn');
100
- const heroMicBtn = document.getElementById('heroMicBtn');
101
-
102
-
103
- /* ============================================================
104
- THEME TOGGLE Smooth dark/light
105
- ============================================================ */
106
-
107
- const _PERSONA_LABELS = { vidyut: 'Vidyut', nerd: 'Nerd', noob: 'Beginner', thoughtful: 'Thoughtful', panic: 'Panic' };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
 
109
  function _initTheme() {
110
  const saved = localStorage.getItem('stemcopilot_theme') || 'dark';
111
  document.documentElement.setAttribute('data-theme', saved);
112
  _updateThemeColor(saved);
 
 
 
 
 
 
 
 
113
  }
114
 
115
  function _toggleTheme() {
116
  const current = document.documentElement.getAttribute('data-theme') || 'dark';
117
- const next = current === 'dark' ? 'light' : 'dark';
118
- document.documentElement.setAttribute('data-theme', next);
119
- localStorage.setItem('stemcopilot_theme', next);
120
- _updateThemeColor(next);
121
  }
122
 
123
  function _updateThemeColor(theme) {
@@ -125,13 +144,19 @@ function _updateThemeColor(theme) {
125
  if (meta) meta.content = theme === 'light' ? '#f8f9fa' : '#0a0a0a';
126
  }
127
 
 
 
 
 
 
128
  _initTheme();
129
- if (themeToggleBtn) themeToggleBtn.addEventListener('click', _toggleTheme);
 
 
130
 
131
 
132
- /* ============================================================
133
- TOAST NOTIFICATION
134
- ============================================================ */
135
  function showToast(msg, type = 'info') {
136
  const t = document.createElement('div');
137
  t.textContent = msg;
@@ -144,18 +169,15 @@ function showToast(msg, type = 'info') {
144
  });
145
  document.body.appendChild(t);
146
  requestAnimationFrame(() => t.style.opacity = '1');
147
- setTimeout(() => {
148
- t.style.opacity = '0';
149
- setTimeout(() => t.remove(), 300);
150
- }, 2500);
151
  }
152
 
153
 
154
- /* ============================================================
155
- FEEDBACK
156
- ============================================================ */
157
  function openFeedbackInSettings() {
158
- userMenu.classList.remove('show');
 
159
  document.querySelectorAll('.settings-nav-btn').forEach(b => b.classList.remove('active'));
160
  document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
161
  const fbBtn = document.querySelector('.settings-nav-btn[data-tab="feedback"]');
@@ -164,17 +186,16 @@ function openFeedbackInSettings() {
164
  if (fbTab) fbTab.classList.add('active');
165
  settingsOverlay.classList.add('show');
166
  }
167
- if (feedbackMenuBtn) feedbackMenuBtn.addEventListener('click', openFeedbackInSettings);
168
 
169
  function _bindFeedbackSubmit() {
170
- const submitFeedbackBtn = document.getElementById('submitFeedbackBtn');
171
- if (!submitFeedbackBtn) return;
172
- submitFeedbackBtn.addEventListener('click', () => {
173
  const cat = document.getElementById('feedbackCategory').value;
174
  const msg = document.getElementById('feedbackMessage').value;
175
  if (!msg.trim()) { document.getElementById('feedbackMessage').focus(); return; }
176
- submitFeedbackBtn.disabled = true;
177
- submitFeedbackBtn.textContent = 'Submitting...';
178
  fetch('/feedback', {
179
  method: 'POST',
180
  headers: { 'Content-Type': 'application/json' },
@@ -182,53 +203,39 @@ function _bindFeedbackSubmit() {
182
  user_id: currentUser ? currentUser.google_id : 'anonymous',
183
  category: cat, message: msg
184
  })
185
- }).then(r => {
186
- if (!r.ok) throw new Error('Server error');
187
- return r.json();
188
- }).then(() => {
189
- settingsOverlay.classList.remove('show');
190
- document.getElementById('feedbackMessage').value = '';
191
- showToast('Feedback submitted. Thank you!', 'success');
192
- }).catch(() => {
193
- showToast('Could not submit feedback. Please try again.', 'error');
194
- }).finally(() => {
195
- submitFeedbackBtn.disabled = false;
196
- submitFeedbackBtn.textContent = 'Submit Securely';
197
- });
198
  });
199
  }
200
 
201
 
202
- /* ============================================================
203
- AUTH — Google Sign-In
204
- ============================================================ */
205
 
206
  let _gsiInitialized = false;
207
 
208
  function initGoogleAuth() {
209
  return new Promise((resolve) => {
210
  if (_gsiInitialized) { resolve(); return; }
211
- fetch('/auth/client_id')
212
- .then(r => r.json())
213
- .then(data => {
214
- if (!data.client_id) { showApp(); resolve(); return; }
215
- const script = document.createElement('script');
216
- script.src = 'https://accounts.google.com/gsi/client';
217
- script.async = true; script.defer = true;
218
- script.onload = () => {
219
- google.accounts.id.initialize({
220
- client_id: data.client_id,
221
- callback: handleGoogleCredential,
222
- auto_select: false, cancel_on_tap_outside: false,
223
- });
224
- _renderHiddenGoogleBtn();
225
- _gsiInitialized = true;
226
- resolve();
227
- };
228
- script.onerror = () => { showApp(); resolve(); };
229
- document.head.appendChild(script);
230
- })
231
- .catch(() => { showApp(); resolve(); });
232
  });
233
  }
234
 
@@ -246,13 +253,8 @@ function _renderHiddenGoogleBtn(cb) {
246
  function triggerGoogleSignIn() {
247
  const tryClick = () => {
248
  const realBtn = document.querySelector('#gsi-hidden-btn [role="button"]');
249
- if (realBtn) { realBtn.click(); }
250
- else {
251
- _renderHiddenGoogleBtn(() => {
252
- const btn = document.querySelector('#gsi-hidden-btn [role="button"]');
253
- if (btn) btn.click();
254
- });
255
- }
256
  };
257
  if (_gsiInitialized) tryClick();
258
  else initGoogleAuth().then(tryClick);
@@ -260,23 +262,17 @@ function triggerGoogleSignIn() {
260
 
261
  function handleGoogleCredential(response) {
262
  fetch('/auth/google', {
263
- method: 'POST',
264
- headers: { 'Content-Type': 'application/json' },
265
  body: JSON.stringify({ token: response.credential }),
266
- })
267
- .then(r => {
268
- if (!r.ok) throw new Error('Auth failed');
269
- return r.json();
270
- })
271
- .then(data => {
272
- if (data.error) { showToast('Login failed: ' + data.error, 'error'); return; }
273
- currentUser = data.user;
274
- localStorage.setItem('stemcopilot_user', JSON.stringify(currentUser));
275
- currentUsername = currentUser.name;
276
- localStorage.setItem('stemcopilot_username', currentUsername);
277
- if (!data.has_api_key) showByok(); else showApp();
278
- })
279
- .catch(() => showToast('Login failed. Check your connection and try again.', 'error'));
280
  }
281
 
282
  function checkExistingSession() {
@@ -287,25 +283,15 @@ function checkExistingSession() {
287
  fetch('/auth/me?user_id=' + encodeURIComponent(currentUser.google_id))
288
  .then(r => r.json())
289
  .then(data => {
290
- if (data.error) {
291
- localStorage.removeItem('stemcopilot_user');
292
- currentUser = null;
293
- initGoogleAuth();
294
- return;
295
- }
296
  currentUser = data.user;
297
  if (!data.has_api_key) showByok(); else showApp();
298
- })
299
- .catch(() => initGoogleAuth());
300
- } else {
301
- initGoogleAuth();
302
- }
303
  }
304
 
305
 
306
- /* ============================================================
307
- BYOK
308
- ============================================================ */
309
 
310
  function showByok() {
311
  loginScreen.style.display = 'none';
@@ -316,20 +302,14 @@ function showByok() {
316
  if (byokSubmitBtn) byokSubmitBtn.addEventListener('click', () => {
317
  const key = byokInput.value.trim();
318
  if (!key) { byokInput.focus(); return; }
319
- fetch('/user/apikey', {
320
- method: 'POST',
321
- headers: { 'Content-Type': 'application/json' },
322
- body: JSON.stringify({ user_id: currentUser.google_id, key: key }),
323
- })
324
  .then(r => { if (!r.ok) throw new Error(); return r.json(); })
325
  .then(() => showApp())
326
- .catch(() => showToast('Failed to save key. Try again.', 'error'));
327
  });
328
 
329
 
330
- /* ============================================================
331
- SHOW MAIN APP
332
- ============================================================ */
333
 
334
  function showApp() {
335
  loginScreen.style.display = 'none';
@@ -337,11 +317,17 @@ function showApp() {
337
  appContainer.style.display = 'flex';
338
 
339
  if (currentUser) {
340
- userDisplayName.textContent = currentUser.name || 'Student';
341
- if (currentUser.picture) {
342
- userAvatar.src = currentUser.picture;
343
- if (railAvatar) railAvatar.src = currentUser.picture;
 
 
 
344
  }
 
 
 
345
  fetch('/auth/me?user_id=' + encodeURIComponent(currentUser.google_id))
346
  .then(r => r.json())
347
  .then(data => {
@@ -349,38 +335,25 @@ function showApp() {
349
  if (data.user.student_profile && profileInput) profileInput.value = data.user.student_profile;
350
  if (data.user.openrouter_key && settingsApiKeyInput) settingsApiKeyInput.value = '••••••••••••';
351
  }
352
- }).catch(() => { });
353
  }
354
 
355
  if (usernameInput) usernameInput.value = currentUsername;
356
  if (languageSelect) languageSelect.value = currentLanguage;
357
-
358
- document.querySelectorAll('.persona-option').forEach(opt => {
359
- opt.classList.toggle('active', opt.dataset.persona === currentPersona);
360
- });
361
-
362
- // Sync style selector label
363
  _syncStyleLabel();
364
 
365
  const userId = currentUser ? currentUser.google_id : '';
366
  fetch('/threads?user_id=' + encodeURIComponent(userId))
367
  .then(r => r.json())
368
- .then(data => {
369
- data.threads.forEach(t => threads.push({ id: t.id, title: t.title }));
370
- renderHistory();
371
- })
372
- .catch(() => { });
373
 
374
- // Ensure sidebar starts collapsed on mobile
375
  if (window.innerWidth <= 768) closeSidebar();
376
-
377
  enterHeroMode();
378
  }
379
 
380
 
381
- /* ============================================================
382
- HERO / BOTTOM INPUT MODE SWITCHING
383
- ============================================================ */
384
 
385
  function enterHeroMode() {
386
  isHeroMode = true;
@@ -391,7 +364,6 @@ function enterHeroMode() {
391
  chatContainer.innerHTML = '';
392
  chatContainer.appendChild(createWelcomeScreen());
393
  }
394
- // Update hero title with username
395
  _updateHeroTitle();
396
  }
397
 
@@ -399,11 +371,8 @@ function _updateHeroTitle() {
399
  const el = document.getElementById('heroTitle');
400
  if (!el) return;
401
  const name = currentUsername || (currentUser ? currentUser.name : '');
402
- if (name) {
403
- el.innerHTML = `What should we study today, <span class="hero-name">${escapeHtml(name)}</span>?`;
404
- } else {
405
- el.textContent = 'What should we study today?';
406
- }
407
  }
408
 
409
  function exitHeroMode() {
@@ -414,27 +383,12 @@ function exitHeroMode() {
414
  }
415
 
416
 
417
- /* ============================================================
418
- SIDEBAR
419
- ============================================================ */
420
 
421
  let sidebarOpen = false;
422
 
423
- function _cleanSidebarStyles() {
424
- sidebar.style.transition = '';
425
- sidebar.style.transform = '';
426
- sidebar.style.opacity = '';
427
- sidebar.style.display = '';
428
- if (sidebarOverlay) {
429
- sidebarOverlay.style.transition = '';
430
- sidebarOverlay.style.opacity = '';
431
- sidebarOverlay.style.display = '';
432
- }
433
- }
434
-
435
  function openSidebar() {
436
  sidebarOpen = true;
437
- _cleanSidebarStyles();
438
  sidebar.classList.remove('collapsed');
439
  if (sidebarOverlay && window.innerWidth <= 768) sidebarOverlay.classList.add('visible');
440
  if (sidebarRail) sidebarRail.classList.remove('visible');
@@ -442,42 +396,19 @@ function openSidebar() {
442
 
443
  function closeSidebar() {
444
  sidebarOpen = false;
445
- _cleanSidebarStyles();
446
  sidebar.classList.add('collapsed');
447
  if (sidebarOverlay) sidebarOverlay.classList.remove('visible');
448
  if (sidebarRail && window.innerWidth > 768) sidebarRail.classList.add('visible');
449
  }
450
 
451
- function toggleSidebar() {
452
- if (sidebarOpen) closeSidebar(); else openSidebar();
453
- }
454
 
455
  if (toggleSidebarBtn) toggleSidebarBtn.addEventListener('click', toggleSidebar);
456
-
457
  if (sidebarOverlay) {
458
  sidebarOverlay.addEventListener('click', closeSidebar);
459
- sidebarOverlay.addEventListener('touchstart', (e) => {
460
- e.preventDefault(); closeSidebar();
461
- }, { passive: false });
462
  }
463
 
464
- // Edge swipe — 60px zone, mid-screen horizontal swipe
465
- let _touchStartX = 0, _touchStartY = 0;
466
- document.addEventListener('touchstart', (e) => {
467
- if (window.innerWidth > 768) return;
468
- _touchStartX = e.changedTouches[0].clientX;
469
- _touchStartY = e.changedTouches[0].clientY;
470
- }, { passive: true });
471
-
472
- document.addEventListener('touchend', (e) => {
473
- if (window.innerWidth > 768) return;
474
- const dx = e.changedTouches[0].clientX - _touchStartX;
475
- const dy = Math.abs(e.changedTouches[0].clientY - _touchStartY);
476
- if (dy > Math.abs(dx) * 0.8) return;
477
- if (!sidebarOpen && _touchStartX < 60 && dx > 50) openSidebar();
478
- if (sidebarOpen && dx < -50) closeSidebar();
479
- }, { passive: true });
480
-
481
  if (railExpandBtn) railExpandBtn.addEventListener('click', openSidebar);
482
  if (railNewChatBtn) railNewChatBtn.addEventListener('click', () => startNewChat());
483
  if (railProfileBtn) railProfileBtn.addEventListener('click', () => {
@@ -485,8 +416,6 @@ if (railProfileBtn) railProfileBtn.addEventListener('click', () => {
485
  setTimeout(() => { if (userProfileBtn) userProfileBtn.click(); }, 150);
486
  });
487
 
488
- const mobileFabToggle = document.getElementById('mobileFabToggle');
489
- if (mobileFabToggle) mobileFabToggle.addEventListener('click', toggleSidebar);
490
  if (newChatBtn) newChatBtn.addEventListener('click', startNewChat);
491
 
492
  function startNewChat() {
@@ -495,19 +424,98 @@ function startNewChat() {
495
  chatContainer.appendChild(createWelcomeScreen());
496
  enterHeroMode();
497
  renderHistory();
498
- if (window.innerWidth <= 768) closeSidebar();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
499
  }
500
 
 
 
 
501
  function createWelcomeScreen() {
502
  const div = document.createElement('div');
503
  div.className = 'hero-welcome';
504
  div.id = 'welcomeScreen';
505
 
506
  const name = currentUsername || (currentUser ? currentUser.name : '');
507
- const titleHtml = name
508
- ? `What should we study today, <span class="hero-name">${escapeHtml(name)}</span>?`
509
- : `What should we study today?`;
510
-
511
  const styleLabel = _PERSONA_LABELS[currentPersona] || 'Vidyut';
512
 
513
  div.innerHTML = `
@@ -530,87 +538,54 @@ function createWelcomeScreen() {
530
  <svg viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9" /></svg>
531
  </button>
532
  <div class="style-dropdown" id="heroStyleDropdownDynamic">
533
- <div class="style-option ${currentPersona === 'vidyut' ? 'active' : ''}" data-persona="vidyut">
534
- <div class="style-option-name">Vidyut</div>
535
- <div class="style-option-desc">Calm, clear, step-by-step master teacher. Makes hard concepts obvious.</div>
536
- </div>
537
- <div class="style-option ${currentPersona === 'nerd' ? 'active' : ''}" data-persona="nerd">
538
- <div class="style-option-name">Nerd</div>
539
- <div class="style-option-desc">Deep, rigorous, first-principles. Goes beyond the textbook.</div>
540
- </div>
541
- <div class="style-option ${currentPersona === 'noob' ? 'active' : ''}" data-persona="noob">
542
- <div class="style-option-name">Beginner</div>
543
- <div class="style-option-desc">Patient, step-by-step. Explains like you're seeing it for the first time.</div>
544
- </div>
545
- <div class="style-option ${currentPersona === 'thoughtful' ? 'active' : ''}" data-persona="thoughtful">
546
- <div class="style-option-name">Thoughtful</div>
547
- <div class="style-option-desc">Connects science to the real world. Every formula has a story.</div>
548
- </div>
549
- <div class="style-option ${currentPersona === 'panic' ? 'active' : ''}" data-persona="panic">
550
- <div class="style-option-name">Panic</div>
551
- <div class="style-option-desc">Exam tomorrow? Concise bullets, key formulas, no fluff.</div>
552
- </div>
553
  </div>
554
  </div>
555
- <button class="mic-btn" id="heroMicBtnDynamic" title="Voice input">
556
- <svg viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>
557
- </button>
558
- <button class="send-btn" id="heroSendBtnDynamic">
559
- <svg viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"></path></svg>
560
  </button>
561
  </div>
562
  </div>
563
- <div class="hero-pills">
564
- <button class="hero-pill" data-query="What is the photoelectric effect, and why did Einstein win the Nobel Prize for it?">What is the photoelectric effect and why did Einstein win the Nobel Prize for it?</button>
565
- <button class="hero-pill" data-query="Derive the relation between Kp and Kc for a general gaseous equilibrium reaction">Derive the relation between Kp and Kc for gaseous equilibrium</button>
566
- <button class="hero-pill" data-query="Solve: A ball is thrown vertically upward with velocity 20 m/s. Find maximum height and time of flight. Take g = 10 m/s^2">A ball thrown up at 20 m/s -- find max height and time of flight</button>
567
- <button class="hero-pill" data-query="Explain the concept of limits in calculus with a real-world example. How is it different from the actual value of a function?">Explain limits in calculus with a real-world example</button>
568
- </div>
569
  `;
570
 
571
  const dynInput = div.querySelector('#heroInputDynamic');
572
- const dynSend = div.querySelector('#heroSendBtnDynamic');
573
  const dynUpload = div.querySelector('#heroUploadBtnDynamic');
574
  const dynFileInput = div.querySelector('#heroImageInputDynamic');
575
- const dynMic = div.querySelector('#heroMicBtnDynamic');
576
  const dynImgPreview = div.querySelector('#heroImagePreviewDynamic');
577
  const dynImgThumb = div.querySelector('#heroImagePreviewThumbDynamic');
578
  const dynImgRemove = div.querySelector('#heroImagePreviewRemoveDynamic');
579
-
580
- // Style selector in dynamic hero
581
  const dynStyleBtn = div.querySelector('#heroStyleSelectorBtnDynamic');
582
  const dynStyleDropdown = div.querySelector('#heroStyleDropdownDynamic');
583
  const dynStyleLabel = div.querySelector('#heroStyleSelectorLabelDynamic');
584
 
 
585
  dynInput.addEventListener('input', function () {
586
- this.style.height = '54px'; this.style.height = this.scrollHeight + 'px';
 
587
  });
 
 
 
 
588
  dynInput.addEventListener('keydown', function (e) {
589
- if (e.key === 'Enter' && !e.shiftKey) {
590
- e.preventDefault(); userInput.value = dynInput.value; sendMessage();
591
- }
592
  });
593
- dynSend.addEventListener('click', () => { userInput.value = dynInput.value; sendMessage(); });
594
  dynUpload.addEventListener('click', () => dynFileInput.click());
595
  dynFileInput.addEventListener('change', () => {
596
- if (dynFileInput.files[0]) {
597
- handleImageFile(dynFileInput.files[0]);
598
- _showHeroImagePreview(dynImgPreview, dynImgThumb);
599
- }
600
- });
601
- dynImgRemove.addEventListener('click', () => {
602
- _clearImage();
603
- dynImgPreview.classList.remove('visible');
604
  });
605
- if (dynMic) _bindMic(dynMic, dynInput);
606
 
607
- // Style selector events
608
  if (dynStyleBtn) {
609
- dynStyleBtn.addEventListener('click', (e) => {
610
- e.stopPropagation();
611
- dynStyleDropdown.classList.toggle('show');
612
- dynStyleBtn.classList.toggle('open');
613
- });
614
  }
615
  if (dynStyleDropdown) {
616
  dynStyleDropdown.querySelectorAll('.style-option').forEach(opt => {
@@ -620,19 +595,25 @@ function createWelcomeScreen() {
620
  dynStyleDropdown.classList.remove('show');
621
  dynStyleBtn.classList.remove('open');
622
  dynStyleLabel.textContent = _PERSONA_LABELS[currentPersona] || 'Vidyut';
623
- dynStyleDropdown.querySelectorAll('.style-option').forEach(o => {
624
- o.classList.toggle('active', o.dataset.persona === currentPersona);
625
- });
626
  });
627
  });
628
  }
629
 
630
- div.querySelectorAll('.hero-pill').forEach(p => {
631
- p.addEventListener('click', () => { userInput.value = p.dataset.query; sendMessage(); });
632
- });
633
  return div;
634
  }
635
 
 
 
 
 
 
 
 
 
 
 
 
636
  function _showHeroImagePreview(previewEl, thumbEl) {
637
  if (pendingImageDataUrl && previewEl && thumbEl) {
638
  thumbEl.style.backgroundImage = `url(${pendingImageDataUrl})`;
@@ -640,12 +621,16 @@ function _showHeroImagePreview(previewEl, thumbEl) {
640
  }
641
  }
642
 
 
 
 
643
  function addThreadToSidebar(id, title) {
644
  threads.unshift({ id, title });
645
  renderHistory();
646
  }
647
 
648
  function renderHistory() {
 
649
  chatHistoryList.innerHTML = '';
650
  threads.forEach(thread => {
651
  const item = document.createElement('div');
@@ -681,38 +666,32 @@ function loadThread(threadId) {
681
  exitHeroMode();
682
  if (window.innerWidth <= 768) closeSidebar();
683
 
684
- fetch('/history/' + threadId)
685
- .then(res => res.json())
686
- .then(data => {
687
- data.messages.forEach(msg => {
688
- const sender = msg.role === 'user' ? 'user' : 'ai';
689
- let textContent = msg.content;
690
- if (Array.isArray(msg.content)) {
691
- textContent = msg.content.filter(p => p.type === 'text').map(p => p.text).join('');
692
- }
693
- const el = appendMessage(sender, textContent);
694
- if (sender === 'ai') renderFinalContent(el, textContent);
695
- });
696
  });
 
697
  }
698
 
699
 
700
- /* ============================================================
701
- OPTIONS MENU (Rename / Delete)
702
- ============================================================ */
703
 
704
  function toggleMenu(e, btn) {
705
  e.stopPropagation(); closeAllMenus();
706
  btn.nextElementSibling.classList.add('show');
707
  btn.classList.add('menu-open');
708
  }
 
709
  document.addEventListener('click', closeAllMenus);
710
 
711
  function closeAllMenus() {
712
  document.querySelectorAll('.options-menu.show').forEach(m => m.classList.remove('show'));
713
  document.querySelectorAll('.options-btn.menu-open').forEach(b => b.classList.remove('menu-open'));
714
  if (userMenu) userMenu.classList.remove('show');
715
- // Close style dropdown too
716
  if (styleDropdown) { styleDropdown.classList.remove('show'); styleSelectorBtn?.classList.remove('open'); }
717
  }
718
 
@@ -744,10 +723,7 @@ function renameChat(e, optionEl) {
744
  titleSpan.innerText = newTitle; input.replaceWith(titleSpan);
745
  const thread = threads.find(t => t.id === threadId);
746
  if (thread) thread.title = newTitle;
747
- fetch('/rename', {
748
- method: 'POST', headers: { 'Content-Type': 'application/json' },
749
- body: JSON.stringify({ thread_id: threadId, title: newTitle })
750
- });
751
  }
752
  input.addEventListener('blur', saveRename);
753
  input.addEventListener('keydown', evt => {
@@ -758,25 +734,13 @@ function renameChat(e, optionEl) {
758
  }
759
 
760
 
761
- /* ============================================================
762
- USER MENU & SETTINGS
763
- ============================================================ */
764
 
765
- if (userProfileBtn) userProfileBtn.addEventListener('click', (e) => {
766
- e.stopPropagation(); userMenu.classList.toggle('show');
767
- });
768
- if (openSettingsBtn) openSettingsBtn.addEventListener('click', () => {
769
- userMenu.classList.remove('show'); settingsOverlay.classList.add('show');
770
- });
771
  if (settingsCloseBtn) settingsCloseBtn.addEventListener('click', () => settingsOverlay.classList.remove('show'));
772
- if (settingsOverlay) settingsOverlay.addEventListener('click', (e) => {
773
- if (e.target === settingsOverlay) settingsOverlay.classList.remove('show');
774
- });
775
- if (logoutBtn) logoutBtn.addEventListener('click', () => {
776
- localStorage.removeItem('stemcopilot_user');
777
- localStorage.removeItem('stemcopilot_username');
778
- currentUser = null; location.reload();
779
- });
780
 
781
  document.querySelectorAll('.settings-nav-btn').forEach(btn => {
782
  btn.addEventListener('click', () => {
@@ -788,100 +752,56 @@ document.querySelectorAll('.settings-nav-btn').forEach(btn => {
788
  });
789
  });
790
 
791
- if (usernameInput) usernameInput.addEventListener('input', () => {
792
- currentUsername = usernameInput.value.trim();
793
- localStorage.setItem('stemcopilot_username', currentUsername);
794
- });
795
- if (languageSelect) languageSelect.addEventListener('change', () => {
796
- currentLanguage = languageSelect.value;
797
- localStorage.setItem('stemcopilot_language', currentLanguage);
798
- });
799
-
800
- document.querySelectorAll('.persona-option').forEach(opt => {
801
- opt.addEventListener('click', () => {
802
- document.querySelectorAll('.persona-option').forEach(o => o.classList.remove('active'));
803
- opt.classList.add('active');
804
- currentPersona = opt.dataset.persona;
805
- localStorage.setItem('stemcopilot_persona', currentPersona);
806
- _syncStyleLabel();
807
- _syncStyleDropdown();
808
- });
809
- });
810
 
811
  if (saveApiKeyBtn) saveApiKeyBtn.addEventListener('click', () => {
812
  const key = settingsApiKeyInput.value.trim();
813
  if (!key || key.startsWith('••')) return;
814
  if (!currentUser) return;
815
- saveApiKeyBtn.disabled = true;
816
- saveApiKeyBtn.textContent = 'Saving...';
817
- fetch('/user/apikey', {
818
- method: 'POST',
819
- headers: { 'Content-Type': 'application/json' },
820
- body: JSON.stringify({ user_id: currentUser.google_id, key: key }),
821
- }).then(r => {
822
- if (!r.ok) throw new Error();
823
- return r.json();
824
- }).then(() => {
825
- settingsApiKeyInput.value = '••••••••••••';
826
- showToast('API key saved successfully!', 'success');
827
- }).catch(() => {
828
- showToast('Failed to save API key. Please try again.', 'error');
829
- }).finally(() => {
830
- saveApiKeyBtn.disabled = false;
831
- saveApiKeyBtn.textContent = 'Save Key';
832
- });
833
  });
834
 
835
  if (saveProfileBtn) saveProfileBtn.addEventListener('click', () => {
836
  if (!currentUser) return;
837
- fetch('/user/profile', {
838
- method: 'POST',
839
- headers: { 'Content-Type': 'application/json' },
840
- body: JSON.stringify({ user_id: currentUser.google_id, profile: profileInput.value }),
841
- }).then(r => {
842
- if (!r.ok) throw new Error();
843
- showToast('Profile saved!', 'success');
844
- }).catch(() => showToast('Failed to save profile.', 'error'));
845
  });
846
 
847
 
848
- /* ============================================================
849
- TEACHING STYLE SELECTOR (in input bar)
850
- ============================================================ */
 
 
 
 
 
851
 
852
  function _syncStyleLabel() {
853
- if (styleSelectorLabel) {
854
- styleSelectorLabel.textContent = _PERSONA_LABELS[currentPersona] || 'Vidyut';
855
- }
856
  }
857
 
858
  function _syncStyleDropdown() {
859
  if (!styleDropdown) return;
860
- styleDropdown.querySelectorAll('.style-option').forEach(opt => {
861
- opt.classList.toggle('active', opt.dataset.persona === currentPersona);
862
- });
863
  }
864
 
865
  if (styleSelectorBtn) {
866
- styleSelectorBtn.addEventListener('click', (e) => {
867
- e.stopPropagation();
868
- styleDropdown.classList.toggle('show');
869
- styleSelectorBtn.classList.toggle('open');
870
- });
871
  }
872
 
873
  if (styleDropdown) {
874
  styleDropdown.querySelectorAll('.style-option').forEach(opt => {
875
  opt.addEventListener('click', (e) => {
876
  e.stopPropagation();
877
- currentPersona = opt.dataset.persona;
878
- localStorage.setItem('stemcopilot_persona', currentPersona);
879
- _syncStyleLabel();
880
- _syncStyleDropdown();
881
- // Also sync settings modal persona options
882
- document.querySelectorAll('.persona-option').forEach(o => {
883
- o.classList.toggle('active', o.dataset.persona === currentPersona);
884
- });
885
  styleDropdown.classList.remove('show');
886
  styleSelectorBtn.classList.remove('open');
887
  });
@@ -892,20 +812,33 @@ _syncStyleLabel();
892
  _syncStyleDropdown();
893
 
894
 
895
- /* ============================================================
896
- VOICE INPUT (Web Speech API)
897
- ============================================================ */
898
 
899
- function _bindMic(btn, targetInput) {
900
- if (!('webkitSpeechRecognition' in window || 'SpeechRecognition' in window)) {
901
- btn.style.display = 'none';
902
- return;
903
- }
 
 
904
 
 
905
  const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
906
  let recognition = null;
907
 
908
  btn.addEventListener('click', () => {
 
 
 
 
 
 
 
 
 
 
 
 
909
  if (btn.classList.contains('listening')) {
910
  if (recognition) recognition.stop();
911
  btn.classList.remove('listening');
@@ -916,16 +849,13 @@ function _bindMic(btn, targetInput) {
916
  recognition.lang = 'en-IN';
917
  recognition.interimResults = true;
918
  recognition.continuous = false;
919
-
920
  btn.classList.add('listening');
921
 
922
  recognition.onresult = (event) => {
923
  let transcript = '';
924
- for (let i = event.resultIndex; i < event.results.length; i++) {
925
- transcript += event.results[i][0].transcript;
926
- }
927
- targetInput.value = transcript;
928
- targetInput.dispatchEvent(new Event('input'));
929
  };
930
 
931
  recognition.onend = () => btn.classList.remove('listening');
@@ -934,13 +864,14 @@ function _bindMic(btn, targetInput) {
934
  });
935
  }
936
 
937
- if (micBtn && userInput) _bindMic(micBtn, userInput);
938
- if (heroMicBtn && heroInput) _bindMic(heroMicBtn, heroInput);
939
 
 
 
940
 
941
- /* ============================================================
942
- IMAGE UPLOAD
943
- ============================================================ */
944
 
945
  if (uploadBtn) uploadBtn.addEventListener('click', () => imageInput.click());
946
  if (imageInput) imageInput.addEventListener('change', () => { if (imageInput.files[0]) handleImageFile(imageInput.files[0]); });
@@ -948,7 +879,6 @@ if (heroUploadBtn) heroUploadBtn.addEventListener('click', () => heroImageInput.
948
  if (heroImageInput) heroImageInput.addEventListener('change', () => {
949
  if (heroImageInput.files[0]) {
950
  handleImageFile(heroImageInput.files[0]);
951
- // Show hero image preview
952
  if (heroImagePreview && heroImagePreviewThumb && pendingImageDataUrl) {
953
  heroImagePreviewThumb.style.backgroundImage = `url(${pendingImageDataUrl})`;
954
  heroImagePreview.classList.add('visible');
@@ -960,9 +890,7 @@ document.addEventListener('paste', (e) => {
960
  const items = e.clipboardData?.items;
961
  if (!items) return;
962
  for (const item of items) {
963
- if (item.type.startsWith('image/')) {
964
- e.preventDefault(); handleImageFile(item.getAsFile()); return;
965
- }
966
  }
967
  });
968
 
@@ -974,14 +902,10 @@ function handleImageFile(file) {
974
  pendingImageDataUrl = dataUrl;
975
  if (imagePreviewThumb) imagePreviewThumb.style.backgroundImage = `url(${dataUrl})`;
976
  if (imagePreviewBar) imagePreviewBar.style.display = 'flex';
977
- // Also show in hero if currently in hero mode
978
  if (isHeroMode) {
979
  const hp = document.getElementById('heroImagePreviewDynamic') || heroImagePreview;
980
  const ht = document.getElementById('heroImagePreviewThumbDynamic') || heroImagePreviewThumb;
981
- if (hp && ht) {
982
- ht.style.backgroundImage = `url(${dataUrl})`;
983
- hp.classList.add('visible');
984
- }
985
  }
986
  };
987
  reader.readAsDataURL(file);
@@ -995,15 +919,10 @@ function _clearImage() {
995
  }
996
 
997
  if (imagePreviewRemove) imagePreviewRemove.addEventListener('click', _clearImage);
998
- if (heroImagePreviewRemove) heroImagePreviewRemove.addEventListener('click', () => {
999
- _clearImage();
1000
- if (heroImagePreview) heroImagePreview.classList.remove('visible');
1001
- });
1002
 
1003
 
1004
- /* ============================================================
1005
- CHAT & STREAMING — with REAL stop button
1006
- ============================================================ */
1007
 
1008
  let currentAbortController = null;
1009
  let currentStreamReader = null;
@@ -1011,62 +930,33 @@ let currentStreamStopped = false;
1011
  let currentRenderTimer = null;
1012
 
1013
  function _showStopBtn() {
1014
- if (sendBtn) sendBtn.style.display = 'none';
1015
  if (stopBtn) { stopBtn.style.display = 'flex'; stopBtn.classList.add('visible'); }
1016
  }
1017
- function _showSendBtn() {
 
1018
  if (stopBtn) { stopBtn.style.display = 'none'; stopBtn.classList.remove('visible'); }
1019
- if (sendBtn) sendBtn.style.display = 'flex';
1020
  }
1021
 
1022
  if (stopBtn) {
1023
  stopBtn.addEventListener('click', () => {
1024
  currentStreamStopped = true;
1025
  if (currentRenderTimer) { clearTimeout(currentRenderTimer); currentRenderTimer = null; }
1026
- if (currentStreamReader) {
1027
- try { currentStreamReader.cancel(); } catch (_) { }
1028
- currentStreamReader = null;
1029
- }
1030
- if (currentAbortController) {
1031
- currentAbortController.abort();
1032
- currentAbortController = null;
1033
- }
1034
  isSending = false;
1035
- _showSendBtn();
1036
- const currentThinking = document.getElementById('currentThinking');
1037
- if (currentThinking) currentThinking.style.display = 'none';
1038
  document.querySelectorAll('.ai-avatar.pulsing').forEach(el => el.classList.remove('pulsing'));
1039
  document.querySelectorAll('.message-content.cursor').forEach(el => el.classList.remove('cursor'));
1040
  });
1041
  }
1042
 
1043
  if (userInput) {
1044
- userInput.addEventListener('input', function () {
1045
- this.style.height = '54px'; this.style.height = this.scrollHeight + 'px';
1046
- });
1047
- userInput.addEventListener('keydown', function (e) {
1048
- if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
1049
- });
1050
- }
1051
-
1052
- if (sendBtn) sendBtn.addEventListener('click', sendMessage);
1053
-
1054
- document.querySelectorAll('.hero-pill').forEach(p => {
1055
- p.addEventListener('click', () => { userInput.value = p.dataset.query; sendMessage(); });
1056
- });
1057
-
1058
- if (heroSendBtn) heroSendBtn.addEventListener('click', () => {
1059
- userInput.value = heroInput.value; sendMessage();
1060
- });
1061
- if (heroInput) {
1062
- heroInput.addEventListener('input', function () {
1063
- this.style.height = '54px'; this.style.height = this.scrollHeight + 'px';
1064
- });
1065
- heroInput.addEventListener('keydown', function (e) {
1066
- if (e.key === 'Enter' && !e.shiftKey) {
1067
- e.preventDefault(); userInput.value = heroInput.value; sendMessage();
1068
- }
1069
- });
1070
  }
1071
 
1072
  function sendMessage() {
@@ -1083,11 +973,12 @@ function sendMessage() {
1083
  <div>${escapeHtml(text)}</div>
1084
  </div>`;
1085
  chatContainer.appendChild(rowDiv);
1086
- } else {
1087
- appendMessage('user', text);
1088
- }
1089
 
1090
- userInput.value = ''; userInput.style.height = '54px';
 
 
 
1091
 
1092
  const exists = threads.find(t => t.id === currentThreadId);
1093
  if (!exists) {
@@ -1102,8 +993,7 @@ function sendMessage() {
1102
 
1103
  function streamResponse(text) {
1104
  const imageData = pendingImage || '';
1105
- pendingImage = null;
1106
- pendingImageDataUrl = null;
1107
  if (imagePreviewBar) imagePreviewBar.style.display = 'none';
1108
  if (imageInput) imageInput.value = '';
1109
 
@@ -1130,8 +1020,8 @@ function streamResponse(text) {
1130
  let rawText = '';
1131
  let firstToken = true;
1132
  currentStreamStopped = false;
1133
-
1134
  currentRenderTimer = null;
 
1135
  const RENDER_INTERVAL = 120;
1136
  function scheduleRender() {
1137
  if (currentRenderTimer || currentStreamStopped) return;
@@ -1152,10 +1042,8 @@ function streamResponse(text) {
1152
  currentAbortController = new AbortController();
1153
 
1154
  fetch('/chat', {
1155
- method: 'POST',
1156
- headers: { 'Content-Type': 'application/json' },
1157
- body: JSON.stringify(payload),
1158
- signal: currentAbortController.signal,
1159
  }).then(response => {
1160
  const reader = response.body.getReader();
1161
  currentStreamReader = reader;
@@ -1177,10 +1065,7 @@ function streamResponse(text) {
1177
  if (currentStreamStopped) return;
1178
  if (!line.startsWith('data: ')) continue;
1179
  const pl = line.substring(6);
1180
- if (pl === '[DONE]') {
1181
- finishStream(thinkingEl, contentEl, rawText, currentRenderTimer, actionsEl);
1182
- currentStreamStopped = true; return;
1183
- }
1184
  try {
1185
  const data = JSON.parse(pl);
1186
  if (data.status === 'thinking') { thinkingText.textContent = data.message; continue; }
@@ -1188,46 +1073,32 @@ function streamResponse(text) {
1188
  thinkingEl.style.display = 'none';
1189
  contentEl.style.display = 'block';
1190
  contentEl.innerHTML = `<div class="error-message">${escapeHtml(data.error)}</div>`;
1191
- isSending = false; _showSendBtn(); currentStreamStopped = true; return;
1192
  }
1193
  if (data.token !== undefined) {
1194
  if (currentStreamStopped) return;
1195
- if (firstToken) {
1196
- thinkingEl.style.display = 'none';
1197
- contentEl.style.display = 'block';
1198
- contentEl.classList.add('cursor');
1199
- firstToken = false;
1200
- }
1201
  rawText += data.token;
1202
  scheduleRender();
1203
  }
1204
- } catch (_) { }
1205
  }
1206
-
1207
  if (!currentStreamStopped) read();
1208
  }).catch(err => {
1209
  if (err.name === 'AbortError' || currentStreamStopped) return;
1210
- thinkingEl.style.display = 'none';
1211
- contentEl.style.display = 'block';
1212
  if (!rawText) contentEl.innerHTML = '<div class="error-message">Connection lost. Please try again.</div>';
1213
- isSending = false; _showSendBtn();
1214
  });
1215
  }
1216
  read();
1217
  }).catch(err => {
1218
- if (err.name === 'AbortError' || currentStreamStopped) {
1219
- thinkingEl.style.display = 'none';
1220
- contentEl.style.display = 'block';
1221
- isSending = false; _showSendBtn();
1222
- return;
1223
- }
1224
- thinkingEl.style.display = 'none';
1225
- contentEl.style.display = 'block';
1226
- contentEl.innerHTML = '<div class="error-message">Could not connect to the server. Please try again.</div>';
1227
- isSending = false; _showSendBtn();
1228
  }).finally(() => {
1229
- currentAbortController = null;
1230
- currentStreamReader = null;
1231
  const avatar = rowDiv.querySelector('#avatarThinking');
1232
  if (avatar) avatar.classList.remove('pulsing');
1233
  });
@@ -1241,22 +1112,19 @@ function finishStream(thinkingEl, contentEl, rawText, timer, actionsEl) {
1241
  contentEl.classList.remove('cursor');
1242
  renderFinalContent(contentEl, rawText);
1243
  isSending = false;
1244
- _showSendBtn();
1245
  const row = thinkingEl.closest('.message-row');
1246
- if (row) {
1247
- const avatar = row.querySelector('.ai-avatar');
1248
- if (avatar) avatar.classList.remove('pulsing');
1249
- }
1250
- // Show copy/regenerate actions
1251
  if (actionsEl && rawText) {
1252
  actionsEl.style.display = 'flex';
1253
  actionsEl.classList.add('visible');
1254
  actionsEl.innerHTML = `
1255
- <button class="msg-action-btn" data-action="copy" title="Copy to clipboard">
1256
  <svg viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
1257
  Copy
1258
  </button>
1259
- <button class="msg-action-btn" data-action="regenerate" title="Regenerate response">
1260
  <svg viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>
1261
  Regenerate
1262
  </button>
@@ -1268,23 +1136,16 @@ function finishStream(thinkingEl, contentEl, rawText, timer, actionsEl) {
1268
  navigator.clipboard.writeText(rawText).then(() => {
1269
  copyBtn.querySelector('svg + *').textContent = 'Copied';
1270
  setTimeout(() => { copyBtn.querySelector('svg + *').textContent = 'Copy'; }, 1500);
1271
- }).catch(() => showToast('Could not copy to clipboard', 'error'));
1272
  });
1273
  }
1274
  if (regenBtn) {
1275
  regenBtn.addEventListener('click', () => {
1276
- // Find the last user message and re-send
1277
  const userMessages = chatContainer.querySelectorAll('.message-row.user');
1278
  if (userMessages.length) {
1279
  const lastUser = userMessages[userMessages.length - 1];
1280
  const mc = lastUser.querySelector('.message-content');
1281
- if (mc) {
1282
- // Remove the AI response row
1283
- row.remove();
1284
- // Resend
1285
- const lastText = mc.textContent.trim();
1286
- if (lastText) streamResponse(lastText);
1287
- }
1288
  }
1289
  });
1290
  }
@@ -1292,9 +1153,7 @@ function finishStream(thinkingEl, contentEl, rawText, timer, actionsEl) {
1292
  }
1293
 
1294
 
1295
- /* ============================================================
1296
- MESSAGE HELPERS
1297
- ============================================================ */
1298
 
1299
  function appendMessage(sender, text) {
1300
  const rowDiv = document.createElement('div');
@@ -1318,9 +1177,7 @@ function escapeHtml(text) {
1318
  }
1319
 
1320
 
1321
- /* ============================================================
1322
- RENDERER — Markdown + KaTeX + mhchem
1323
- ============================================================ */
1324
 
1325
  function renderFinalContent(element, rawText) {
1326
  if (!rawText) return;
@@ -1347,32 +1204,22 @@ function renderFinalContent(element, rawText) {
1347
  }
1348
 
1349
 
1350
- /* ============================================================
1351
- PWA — Service Worker + Install Prompt
1352
- ============================================================ */
1353
 
1354
- if ('serviceWorker' in navigator) {
1355
- navigator.serviceWorker.register('/sw.js').catch(() => { });
1356
- }
1357
 
1358
  let deferredPrompt = null;
1359
 
1360
  window.addEventListener('beforeinstallprompt', (e) => {
1361
- e.preventDefault();
1362
- deferredPrompt = e;
1363
  const dismissed = localStorage.getItem('stemcopilot_install_dismissed');
1364
- if (!dismissed && installBanner) {
1365
- installBanner.style.display = 'flex';
1366
- }
1367
  });
1368
 
1369
  if (installBtn) installBtn.addEventListener('click', () => {
1370
  if (deferredPrompt) {
1371
  deferredPrompt.prompt();
1372
- deferredPrompt.userChoice.then(() => {
1373
- deferredPrompt = null;
1374
- if (installBanner) installBanner.style.display = 'none';
1375
- });
1376
  }
1377
  });
1378
 
@@ -1382,19 +1229,11 @@ if (installDismiss) installDismiss.addEventListener('click', () => {
1382
  });
1383
 
1384
 
1385
- /* ============================================================
1386
- INIT
1387
- ============================================================ */
1388
 
1389
- // Ensure sidebar starts collapsed in DOM (matches CSS default)
1390
  if (sidebar) sidebar.classList.add('collapsed');
1391
 
1392
- // Lock screen to portrait on mobile
1393
- try {
1394
- if (screen.orientation && screen.orientation.lock) {
1395
- screen.orientation.lock('portrait').catch(() => { });
1396
- }
1397
- } catch (_) { }
1398
 
1399
  window.addEventListener('load', () => {
1400
  checkExistingSession();
 
2
  STEM Copilot — Application Logic
3
  Auth, BYOK, Chat, Settings, Streaming MD, PWA, Sidebar,
4
  Theme Toggle, Teaching Style Selector, Voice Input,
5
+ Copy/Regenerate, Image Preview, Mobile Overlays,
6
+ Adaptive Mic/Send Button
7
  ============================================================ */
8
 
9
+ /* ── State ──────────────────────────────────────────────────── */
10
+
11
  let currentUser = null;
12
  let currentThreadId = crypto.randomUUID();
13
  const threads = [];
 
20
  let currentLanguage = localStorage.getItem('stemcopilot_language') || 'auto';
21
  let currentUsername = localStorage.getItem('stemcopilot_username') || '';
22
 
23
+ const _PERSONA_LABELS = { vidyut: 'Vidyut', nerd: 'Nerd', noob: 'Beginner', thoughtful: 'Thoughtful', panic: 'Panic' };
24
 
25
+
26
+ /* ── DOM References ─────────────────────────────────────────── */
 
27
 
28
  const loginScreen = document.getElementById('loginScreen');
29
  const byokScreen = document.getElementById('byokScreen');
 
38
  const bottomInputContainer = document.getElementById('bottomInputContainer');
39
 
40
  const userInput = document.getElementById('userInput');
 
41
  const newChatBtn = document.getElementById('newChatBtn');
42
  const stopBtn = document.getElementById('stopBtn');
43
+ const adaptiveBtn = document.getElementById('adaptiveBtn');
44
 
45
  const heroInput = document.getElementById('heroInput');
46
+ const heroAdaptiveBtn = document.getElementById('heroAdaptiveBtn');
47
  const heroUploadBtn = document.getElementById('heroUploadBtn');
48
  const heroImageInput = document.getElementById('heroImageInput');
49
  const heroTitle = document.getElementById('heroTitle');
 
65
  const settingsOverlay = document.getElementById('settingsOverlay');
66
  const openSettingsBtn = document.getElementById('openSettingsBtn');
67
  const settingsCloseBtn = document.getElementById('settingsCloseBtn');
 
 
68
 
69
  const usernameInput = document.getElementById('usernameInput');
70
  const languageSelect = document.getElementById('languageSelect');
 
83
  const installBtn = document.getElementById('installBtn');
84
  const installDismiss = document.getElementById('installDismiss');
85
 
 
 
 
86
  const heroImagePreview = document.getElementById('heroImagePreview');
87
  const heroImagePreviewThumb = document.getElementById('heroImagePreviewThumb');
88
  const heroImagePreviewRemove = document.getElementById('heroImagePreviewRemove');
89
 
 
90
  const styleSelectorBtn = document.getElementById('styleSelectorBtn');
91
  const styleSelectorLabel = document.getElementById('styleSelectorLabel');
92
  const styleDropdown = document.getElementById('styleDropdown');
93
 
94
+ // Mobile elements
95
+ const mobileTopbar = document.getElementById('mobileTopbar');
96
+ const topbarProfileBtn = document.getElementById('topbarProfileBtn');
97
+ const topbarAvatar = document.getElementById('topbarAvatar');
98
+ const topbarHistoryBtn = document.getElementById('topbarHistoryBtn');
99
+ const topbarNewChatBtn = document.getElementById('topbarNewChatBtn');
100
+ const accountOverlay = document.getElementById('accountOverlay');
101
+ const accountOverlayClose = document.getElementById('accountOverlayClose');
102
+ const historyOverlay = document.getElementById('historyOverlay');
103
+ const historyOverlayClose = document.getElementById('historyOverlayClose');
104
+ const overlayHistoryList = document.getElementById('overlayHistoryList');
105
+ const overlayHistoryEmpty = document.getElementById('overlayHistoryEmpty');
106
+ const overlaySettingsBtn = document.getElementById('overlaySettingsBtn');
107
+ const overlayProfileBtn = document.getElementById('overlayProfileBtn');
108
+ const overlayLogoutBtn = document.getElementById('overlayLogoutBtn');
109
+ const overlayUserAvatar = document.getElementById('overlayUserAvatar');
110
+ const overlayUserName = document.getElementById('overlayUserName');
111
+ const overlayUserEmail = document.getElementById('overlayUserEmail');
112
+
113
+ // Login theme toggle
114
+ const loginThemeBtn = document.getElementById('loginThemeBtn');
115
+
116
+ // Settings theme buttons
117
+ const themeDarkBtn = document.getElementById('themeDarkBtn');
118
+ const themeLightBtn = document.getElementById('themeLightBtn');
119
+
120
+
121
+ /* ── Theme ──────────────────────────────────────────────────── */
122
 
123
  function _initTheme() {
124
  const saved = localStorage.getItem('stemcopilot_theme') || 'dark';
125
  document.documentElement.setAttribute('data-theme', saved);
126
  _updateThemeColor(saved);
127
+ _syncThemeButtons(saved);
128
+ }
129
+
130
+ function _setTheme(theme) {
131
+ document.documentElement.setAttribute('data-theme', theme);
132
+ localStorage.setItem('stemcopilot_theme', theme);
133
+ _updateThemeColor(theme);
134
+ _syncThemeButtons(theme);
135
  }
136
 
137
  function _toggleTheme() {
138
  const current = document.documentElement.getAttribute('data-theme') || 'dark';
139
+ _setTheme(current === 'dark' ? 'light' : 'dark');
 
 
 
140
  }
141
 
142
  function _updateThemeColor(theme) {
 
144
  if (meta) meta.content = theme === 'light' ? '#f8f9fa' : '#0a0a0a';
145
  }
146
 
147
+ function _syncThemeButtons(theme) {
148
+ if (themeDarkBtn) themeDarkBtn.classList.toggle('active', theme === 'dark');
149
+ if (themeLightBtn) themeLightBtn.classList.toggle('active', theme === 'light');
150
+ }
151
+
152
  _initTheme();
153
+ if (loginThemeBtn) loginThemeBtn.addEventListener('click', _toggleTheme);
154
+ if (themeDarkBtn) themeDarkBtn.addEventListener('click', () => _setTheme('dark'));
155
+ if (themeLightBtn) themeLightBtn.addEventListener('click', () => _setTheme('light'));
156
 
157
 
158
+ /* ── Toast ──────────────────────────────────────────────────── */
159
+
 
160
  function showToast(msg, type = 'info') {
161
  const t = document.createElement('div');
162
  t.textContent = msg;
 
169
  });
170
  document.body.appendChild(t);
171
  requestAnimationFrame(() => t.style.opacity = '1');
172
+ setTimeout(() => { t.style.opacity = '0'; setTimeout(() => t.remove(), 300); }, 2500);
 
 
 
173
  }
174
 
175
 
176
+ /* ── Feedback ───────────────────────────────────────────────── */
177
+
 
178
  function openFeedbackInSettings() {
179
+ if (userMenu) userMenu.classList.remove('show');
180
+ _closeAllOverlays();
181
  document.querySelectorAll('.settings-nav-btn').forEach(b => b.classList.remove('active'));
182
  document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
183
  const fbBtn = document.querySelector('.settings-nav-btn[data-tab="feedback"]');
 
186
  if (fbTab) fbTab.classList.add('active');
187
  settingsOverlay.classList.add('show');
188
  }
 
189
 
190
  function _bindFeedbackSubmit() {
191
+ const btn = document.getElementById('submitFeedbackBtn');
192
+ if (!btn) return;
193
+ btn.addEventListener('click', () => {
194
  const cat = document.getElementById('feedbackCategory').value;
195
  const msg = document.getElementById('feedbackMessage').value;
196
  if (!msg.trim()) { document.getElementById('feedbackMessage').focus(); return; }
197
+ btn.disabled = true;
198
+ btn.textContent = 'Submitting...';
199
  fetch('/feedback', {
200
  method: 'POST',
201
  headers: { 'Content-Type': 'application/json' },
 
203
  user_id: currentUser ? currentUser.google_id : 'anonymous',
204
  category: cat, message: msg
205
  })
206
+ }).then(r => { if (!r.ok) throw new Error(); return r.json(); })
207
+ .then(() => { settingsOverlay.classList.remove('show'); document.getElementById('feedbackMessage').value = ''; showToast('Feedback submitted. Thank you!', 'success'); })
208
+ .catch(() => showToast('Could not submit feedback.', 'error'))
209
+ .finally(() => { btn.disabled = false; btn.textContent = 'Submit Securely'; });
 
 
 
 
 
 
 
 
 
210
  });
211
  }
212
 
213
 
214
+ /* ── Auth — Google Sign-In ──────────────────────────────────── */
 
 
215
 
216
  let _gsiInitialized = false;
217
 
218
  function initGoogleAuth() {
219
  return new Promise((resolve) => {
220
  if (_gsiInitialized) { resolve(); return; }
221
+ fetch('/auth/client_id').then(r => r.json()).then(data => {
222
+ if (!data.client_id) { showApp(); resolve(); return; }
223
+ const script = document.createElement('script');
224
+ script.src = 'https://accounts.google.com/gsi/client';
225
+ script.async = true; script.defer = true;
226
+ script.onload = () => {
227
+ google.accounts.id.initialize({
228
+ client_id: data.client_id,
229
+ callback: handleGoogleCredential,
230
+ auto_select: false, cancel_on_tap_outside: false,
231
+ });
232
+ _renderHiddenGoogleBtn();
233
+ _gsiInitialized = true;
234
+ resolve();
235
+ };
236
+ script.onerror = () => { showApp(); resolve(); };
237
+ document.head.appendChild(script);
238
+ }).catch(() => { showApp(); resolve(); });
 
 
 
239
  });
240
  }
241
 
 
253
  function triggerGoogleSignIn() {
254
  const tryClick = () => {
255
  const realBtn = document.querySelector('#gsi-hidden-btn [role="button"]');
256
+ if (realBtn) realBtn.click();
257
+ else _renderHiddenGoogleBtn(() => { const btn = document.querySelector('#gsi-hidden-btn [role="button"]'); if (btn) btn.click(); });
 
 
 
 
 
258
  };
259
  if (_gsiInitialized) tryClick();
260
  else initGoogleAuth().then(tryClick);
 
262
 
263
  function handleGoogleCredential(response) {
264
  fetch('/auth/google', {
265
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
 
266
  body: JSON.stringify({ token: response.credential }),
267
+ }).then(r => { if (!r.ok) throw new Error('Auth failed'); return r.json(); })
268
+ .then(data => {
269
+ if (data.error) { showToast('Login failed: ' + data.error, 'error'); return; }
270
+ currentUser = data.user;
271
+ localStorage.setItem('stemcopilot_user', JSON.stringify(currentUser));
272
+ currentUsername = currentUser.name;
273
+ localStorage.setItem('stemcopilot_username', currentUsername);
274
+ if (!data.has_api_key) showByok(); else showApp();
275
+ }).catch(() => showToast('Login failed. Check your connection.', 'error'));
 
 
 
 
 
276
  }
277
 
278
  function checkExistingSession() {
 
283
  fetch('/auth/me?user_id=' + encodeURIComponent(currentUser.google_id))
284
  .then(r => r.json())
285
  .then(data => {
286
+ if (data.error) { localStorage.removeItem('stemcopilot_user'); currentUser = null; initGoogleAuth(); return; }
 
 
 
 
 
287
  currentUser = data.user;
288
  if (!data.has_api_key) showByok(); else showApp();
289
+ }).catch(() => initGoogleAuth());
290
+ } else initGoogleAuth();
 
 
 
291
  }
292
 
293
 
294
+ /* ── BYOK ───────────────────────────────────────────────────── */
 
 
295
 
296
  function showByok() {
297
  loginScreen.style.display = 'none';
 
302
  if (byokSubmitBtn) byokSubmitBtn.addEventListener('click', () => {
303
  const key = byokInput.value.trim();
304
  if (!key) { byokInput.focus(); return; }
305
+ fetch('/user/apikey', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_id: currentUser.google_id, key: key }) })
 
 
 
 
306
  .then(r => { if (!r.ok) throw new Error(); return r.json(); })
307
  .then(() => showApp())
308
+ .catch(() => showToast('Failed to save key.', 'error'));
309
  });
310
 
311
 
312
+ /* ── Show Main App ──────────────────────────────────────────── */
 
 
313
 
314
  function showApp() {
315
  loginScreen.style.display = 'none';
 
317
  appContainer.style.display = 'flex';
318
 
319
  if (currentUser) {
320
+ if (userDisplayName) userDisplayName.textContent = currentUser.name || 'Student';
321
+ const pic = currentUser.picture || '';
322
+ if (pic) {
323
+ if (userAvatar) userAvatar.src = pic;
324
+ if (railAvatar) railAvatar.src = pic;
325
+ if (topbarAvatar) topbarAvatar.src = pic;
326
+ if (overlayUserAvatar) overlayUserAvatar.src = pic;
327
  }
328
+ if (overlayUserName) overlayUserName.textContent = currentUser.name || 'Student';
329
+ if (overlayUserEmail) overlayUserEmail.textContent = currentUser.email || '';
330
+
331
  fetch('/auth/me?user_id=' + encodeURIComponent(currentUser.google_id))
332
  .then(r => r.json())
333
  .then(data => {
 
335
  if (data.user.student_profile && profileInput) profileInput.value = data.user.student_profile;
336
  if (data.user.openrouter_key && settingsApiKeyInput) settingsApiKeyInput.value = '••••••••••••';
337
  }
338
+ }).catch(() => {});
339
  }
340
 
341
  if (usernameInput) usernameInput.value = currentUsername;
342
  if (languageSelect) languageSelect.value = currentLanguage;
 
 
 
 
 
 
343
  _syncStyleLabel();
344
 
345
  const userId = currentUser ? currentUser.google_id : '';
346
  fetch('/threads?user_id=' + encodeURIComponent(userId))
347
  .then(r => r.json())
348
+ .then(data => { data.threads.forEach(t => threads.push({ id: t.id, title: t.title })); renderHistory(); })
349
+ .catch(() => {});
 
 
 
350
 
 
351
  if (window.innerWidth <= 768) closeSidebar();
 
352
  enterHeroMode();
353
  }
354
 
355
 
356
+ /* ── Hero / Bottom Input Switching ──────────────────────────── */
 
 
357
 
358
  function enterHeroMode() {
359
  isHeroMode = true;
 
364
  chatContainer.innerHTML = '';
365
  chatContainer.appendChild(createWelcomeScreen());
366
  }
 
367
  _updateHeroTitle();
368
  }
369
 
 
371
  const el = document.getElementById('heroTitle');
372
  if (!el) return;
373
  const name = currentUsername || (currentUser ? currentUser.name : '');
374
+ if (name) el.innerHTML = `What should we study today, <span class="hero-name">${escapeHtml(name)}</span>?`;
375
+ else el.textContent = 'What should we study today?';
 
 
 
376
  }
377
 
378
  function exitHeroMode() {
 
383
  }
384
 
385
 
386
+ /* ── Sidebar (Desktop) ──────────────────────────────────────── */
 
 
387
 
388
  let sidebarOpen = false;
389
 
 
 
 
 
 
 
 
 
 
 
 
 
390
  function openSidebar() {
391
  sidebarOpen = true;
 
392
  sidebar.classList.remove('collapsed');
393
  if (sidebarOverlay && window.innerWidth <= 768) sidebarOverlay.classList.add('visible');
394
  if (sidebarRail) sidebarRail.classList.remove('visible');
 
396
 
397
  function closeSidebar() {
398
  sidebarOpen = false;
 
399
  sidebar.classList.add('collapsed');
400
  if (sidebarOverlay) sidebarOverlay.classList.remove('visible');
401
  if (sidebarRail && window.innerWidth > 768) sidebarRail.classList.add('visible');
402
  }
403
 
404
+ function toggleSidebar() { if (sidebarOpen) closeSidebar(); else openSidebar(); }
 
 
405
 
406
  if (toggleSidebarBtn) toggleSidebarBtn.addEventListener('click', toggleSidebar);
 
407
  if (sidebarOverlay) {
408
  sidebarOverlay.addEventListener('click', closeSidebar);
409
+ sidebarOverlay.addEventListener('touchstart', (e) => { e.preventDefault(); closeSidebar(); }, { passive: false });
 
 
410
  }
411
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
412
  if (railExpandBtn) railExpandBtn.addEventListener('click', openSidebar);
413
  if (railNewChatBtn) railNewChatBtn.addEventListener('click', () => startNewChat());
414
  if (railProfileBtn) railProfileBtn.addEventListener('click', () => {
 
416
  setTimeout(() => { if (userProfileBtn) userProfileBtn.click(); }, 150);
417
  });
418
 
 
 
419
  if (newChatBtn) newChatBtn.addEventListener('click', startNewChat);
420
 
421
  function startNewChat() {
 
424
  chatContainer.appendChild(createWelcomeScreen());
425
  enterHeroMode();
426
  renderHistory();
427
+ if (window.innerWidth <= 768) { closeSidebar(); _closeAllOverlays(); }
428
+ }
429
+
430
+
431
+ /* ── Mobile Overlays ───────────��────────────────────────────── */
432
+
433
+ function _closeAllOverlays() {
434
+ if (accountOverlay) accountOverlay.classList.remove('open');
435
+ if (historyOverlay) historyOverlay.classList.remove('open');
436
+ }
437
+
438
+ if (topbarProfileBtn) topbarProfileBtn.addEventListener('click', () => {
439
+ _closeAllOverlays();
440
+ if (accountOverlay) accountOverlay.classList.add('open');
441
+ });
442
+
443
+ if (accountOverlayClose) accountOverlayClose.addEventListener('click', () => {
444
+ if (accountOverlay) accountOverlay.classList.remove('open');
445
+ });
446
+
447
+ if (topbarHistoryBtn) topbarHistoryBtn.addEventListener('click', () => {
448
+ _closeAllOverlays();
449
+ _renderOverlayHistory();
450
+ if (historyOverlay) historyOverlay.classList.add('open');
451
+ });
452
+
453
+ if (historyOverlayClose) historyOverlayClose.addEventListener('click', () => {
454
+ if (historyOverlay) historyOverlay.classList.remove('open');
455
+ });
456
+
457
+ if (topbarNewChatBtn) topbarNewChatBtn.addEventListener('click', () => {
458
+ _closeAllOverlays();
459
+ startNewChat();
460
+ });
461
+
462
+ if (overlaySettingsBtn) overlaySettingsBtn.addEventListener('click', () => {
463
+ _closeAllOverlays();
464
+ settingsOverlay.classList.add('show');
465
+ });
466
+
467
+ if (overlayProfileBtn) overlayProfileBtn.addEventListener('click', () => {
468
+ _closeAllOverlays();
469
+ // Open settings on Profile tab
470
+ document.querySelectorAll('.settings-nav-btn').forEach(b => b.classList.remove('active'));
471
+ document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
472
+ const btn = document.querySelector('.settings-nav-btn[data-tab="personalization"]');
473
+ const tab = document.getElementById('tab-personalization');
474
+ if (btn) btn.classList.add('active');
475
+ if (tab) tab.classList.add('active');
476
+ settingsOverlay.classList.add('show');
477
+ });
478
+
479
+ if (overlayLogoutBtn) overlayLogoutBtn.addEventListener('click', () => {
480
+ localStorage.removeItem('stemcopilot_user');
481
+ localStorage.removeItem('stemcopilot_username');
482
+ currentUser = null;
483
+ location.reload();
484
+ });
485
+
486
+ function _renderOverlayHistory() {
487
+ if (!overlayHistoryList) return;
488
+ overlayHistoryList.innerHTML = '';
489
+ if (threads.length === 0) {
490
+ if (overlayHistoryEmpty) overlayHistoryEmpty.style.display = 'flex';
491
+ return;
492
+ }
493
+ if (overlayHistoryEmpty) overlayHistoryEmpty.style.display = 'none';
494
+ threads.forEach(thread => {
495
+ const item = document.createElement('button');
496
+ item.className = `overlay-history-item ${thread.id === currentThreadId ? 'active' : ''}`;
497
+ item.innerHTML = `
498
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
499
+ <span class="overlay-history-title">${escapeHtml(thread.title)}</span>
500
+ `;
501
+ item.addEventListener('click', () => {
502
+ _closeAllOverlays();
503
+ loadThread(thread.id);
504
+ });
505
+ overlayHistoryList.appendChild(item);
506
+ });
507
  }
508
 
509
+
510
+ /* ── Create Welcome Screen (Dynamic) ───────────────────────── */
511
+
512
  function createWelcomeScreen() {
513
  const div = document.createElement('div');
514
  div.className = 'hero-welcome';
515
  div.id = 'welcomeScreen';
516
 
517
  const name = currentUsername || (currentUser ? currentUser.name : '');
518
+ const titleHtml = name ? `What should we study today, <span class="hero-name">${escapeHtml(name)}</span>?` : `What should we study today?`;
 
 
 
519
  const styleLabel = _PERSONA_LABELS[currentPersona] || 'Vidyut';
520
 
521
  div.innerHTML = `
 
538
  <svg viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9" /></svg>
539
  </button>
540
  <div class="style-dropdown" id="heroStyleDropdownDynamic">
541
+ ${['vidyut','nerd','noob','thoughtful','panic'].map(p => `
542
+ <div class="style-option ${currentPersona === p ? 'active' : ''}" data-persona="${p}">
543
+ <div class="style-option-name">${_PERSONA_LABELS[p]}</div>
544
+ <div class="style-option-desc">${_personaDesc(p)}</div>
545
+ </div>`).join('')}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
546
  </div>
547
  </div>
548
+ <button class="adaptive-action-btn" id="heroAdaptiveBtnDynamic" title="Voice input">
549
+ <svg class="icon-mic" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>
550
+ <svg class="icon-send" viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"></path></svg>
 
 
551
  </button>
552
  </div>
553
  </div>
 
 
 
 
 
 
554
  `;
555
 
556
  const dynInput = div.querySelector('#heroInputDynamic');
557
+ const dynAdaptive = div.querySelector('#heroAdaptiveBtnDynamic');
558
  const dynUpload = div.querySelector('#heroUploadBtnDynamic');
559
  const dynFileInput = div.querySelector('#heroImageInputDynamic');
 
560
  const dynImgPreview = div.querySelector('#heroImagePreviewDynamic');
561
  const dynImgThumb = div.querySelector('#heroImagePreviewThumbDynamic');
562
  const dynImgRemove = div.querySelector('#heroImagePreviewRemoveDynamic');
 
 
563
  const dynStyleBtn = div.querySelector('#heroStyleSelectorBtnDynamic');
564
  const dynStyleDropdown = div.querySelector('#heroStyleDropdownDynamic');
565
  const dynStyleLabel = div.querySelector('#heroStyleSelectorLabelDynamic');
566
 
567
+ // Auto-resize
568
  dynInput.addEventListener('input', function () {
569
+ this.style.height = '54px';
570
+ this.style.height = this.scrollHeight + 'px';
571
  });
572
+
573
+ // Adaptive button logic
574
+ _bindAdaptiveBtn(dynAdaptive, dynInput);
575
+
576
  dynInput.addEventListener('keydown', function (e) {
577
+ if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); userInput.value = dynInput.value; sendMessage(); }
 
 
578
  });
579
+
580
  dynUpload.addEventListener('click', () => dynFileInput.click());
581
  dynFileInput.addEventListener('change', () => {
582
+ if (dynFileInput.files[0]) { handleImageFile(dynFileInput.files[0]); _showHeroImagePreview(dynImgPreview, dynImgThumb); }
 
 
 
 
 
 
 
583
  });
584
+ dynImgRemove.addEventListener('click', () => { _clearImage(); dynImgPreview.classList.remove('visible'); });
585
 
586
+ // Style selector
587
  if (dynStyleBtn) {
588
+ dynStyleBtn.addEventListener('click', (e) => { e.stopPropagation(); dynStyleDropdown.classList.toggle('show'); dynStyleBtn.classList.toggle('open'); });
 
 
 
 
589
  }
590
  if (dynStyleDropdown) {
591
  dynStyleDropdown.querySelectorAll('.style-option').forEach(opt => {
 
595
  dynStyleDropdown.classList.remove('show');
596
  dynStyleBtn.classList.remove('open');
597
  dynStyleLabel.textContent = _PERSONA_LABELS[currentPersona] || 'Vidyut';
598
+ dynStyleDropdown.querySelectorAll('.style-option').forEach(o => o.classList.toggle('active', o.dataset.persona === currentPersona));
 
 
599
  });
600
  });
601
  }
602
 
 
 
 
603
  return div;
604
  }
605
 
606
+ function _personaDesc(p) {
607
+ const descs = {
608
+ vidyut: 'Calm, clear, step-by-step master teacher. Makes hard concepts obvious.',
609
+ nerd: 'Deep, rigorous, first-principles. Goes beyond the textbook.',
610
+ noob: 'Patient, step-by-step. Explains like you\'re seeing it for the first time.',
611
+ thoughtful: 'Connects science to the real world. Every formula has a story.',
612
+ panic: 'Exam tomorrow? Concise bullets, key formulas, no fluff.',
613
+ };
614
+ return descs[p] || '';
615
+ }
616
+
617
  function _showHeroImagePreview(previewEl, thumbEl) {
618
  if (pendingImageDataUrl && previewEl && thumbEl) {
619
  thumbEl.style.backgroundImage = `url(${pendingImageDataUrl})`;
 
621
  }
622
  }
623
 
624
+
625
+ /* ── Chat History ───────────────────────────────────────────── */
626
+
627
  function addThreadToSidebar(id, title) {
628
  threads.unshift({ id, title });
629
  renderHistory();
630
  }
631
 
632
  function renderHistory() {
633
+ if (!chatHistoryList) return;
634
  chatHistoryList.innerHTML = '';
635
  threads.forEach(thread => {
636
  const item = document.createElement('div');
 
666
  exitHeroMode();
667
  if (window.innerWidth <= 768) closeSidebar();
668
 
669
+ fetch('/history/' + threadId).then(res => res.json()).then(data => {
670
+ data.messages.forEach(msg => {
671
+ const sender = msg.role === 'user' ? 'user' : 'ai';
672
+ let textContent = msg.content;
673
+ if (Array.isArray(msg.content)) textContent = msg.content.filter(p => p.type === 'text').map(p => p.text).join('');
674
+ const el = appendMessage(sender, textContent);
675
+ if (sender === 'ai') renderFinalContent(el, textContent);
 
 
 
 
 
676
  });
677
+ });
678
  }
679
 
680
 
681
+ /* ── Options Menu ───────────────────────────────────────────── */
 
 
682
 
683
  function toggleMenu(e, btn) {
684
  e.stopPropagation(); closeAllMenus();
685
  btn.nextElementSibling.classList.add('show');
686
  btn.classList.add('menu-open');
687
  }
688
+
689
  document.addEventListener('click', closeAllMenus);
690
 
691
  function closeAllMenus() {
692
  document.querySelectorAll('.options-menu.show').forEach(m => m.classList.remove('show'));
693
  document.querySelectorAll('.options-btn.menu-open').forEach(b => b.classList.remove('menu-open'));
694
  if (userMenu) userMenu.classList.remove('show');
 
695
  if (styleDropdown) { styleDropdown.classList.remove('show'); styleSelectorBtn?.classList.remove('open'); }
696
  }
697
 
 
723
  titleSpan.innerText = newTitle; input.replaceWith(titleSpan);
724
  const thread = threads.find(t => t.id === threadId);
725
  if (thread) thread.title = newTitle;
726
+ fetch('/rename', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ thread_id: threadId, title: newTitle }) });
 
 
 
727
  }
728
  input.addEventListener('blur', saveRename);
729
  input.addEventListener('keydown', evt => {
 
734
  }
735
 
736
 
737
+ /* ── User Menu & Settings ───────────────────────────────────── */
 
 
738
 
739
+ if (userProfileBtn) userProfileBtn.addEventListener('click', (e) => { e.stopPropagation(); userMenu.classList.toggle('show'); });
740
+ if (openSettingsBtn) openSettingsBtn.addEventListener('click', () => { userMenu.classList.remove('show'); settingsOverlay.classList.add('show'); });
 
 
 
 
741
  if (settingsCloseBtn) settingsCloseBtn.addEventListener('click', () => settingsOverlay.classList.remove('show'));
742
+ if (settingsOverlay) settingsOverlay.addEventListener('click', (e) => { if (e.target === settingsOverlay) settingsOverlay.classList.remove('show'); });
743
+ if (logoutBtn) logoutBtn.addEventListener('click', () => { localStorage.removeItem('stemcopilot_user'); localStorage.removeItem('stemcopilot_username'); currentUser = null; location.reload(); });
 
 
 
 
 
 
744
 
745
  document.querySelectorAll('.settings-nav-btn').forEach(btn => {
746
  btn.addEventListener('click', () => {
 
752
  });
753
  });
754
 
755
+ if (usernameInput) usernameInput.addEventListener('input', () => { currentUsername = usernameInput.value.trim(); localStorage.setItem('stemcopilot_username', currentUsername); });
756
+ if (languageSelect) languageSelect.addEventListener('change', () => { currentLanguage = languageSelect.value; localStorage.setItem('stemcopilot_language', currentLanguage); });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
757
 
758
  if (saveApiKeyBtn) saveApiKeyBtn.addEventListener('click', () => {
759
  const key = settingsApiKeyInput.value.trim();
760
  if (!key || key.startsWith('••')) return;
761
  if (!currentUser) return;
762
+ saveApiKeyBtn.disabled = true; saveApiKeyBtn.textContent = 'Saving...';
763
+ fetch('/user/apikey', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_id: currentUser.google_id, key: key }) })
764
+ .then(r => { if (!r.ok) throw new Error(); return r.json(); })
765
+ .then(() => { settingsApiKeyInput.value = '••••••••••••'; showToast('API key saved!', 'success'); })
766
+ .catch(() => showToast('Failed to save API key.', 'error'))
767
+ .finally(() => { saveApiKeyBtn.disabled = false; saveApiKeyBtn.textContent = 'Save Key'; });
 
 
 
 
 
 
 
 
 
 
 
 
768
  });
769
 
770
  if (saveProfileBtn) saveProfileBtn.addEventListener('click', () => {
771
  if (!currentUser) return;
772
+ fetch('/user/profile', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_id: currentUser.google_id, profile: profileInput.value }) })
773
+ .then(r => { if (!r.ok) throw new Error(); showToast('Profile saved!', 'success'); })
774
+ .catch(() => showToast('Failed to save profile.', 'error'));
 
 
 
 
 
775
  });
776
 
777
 
778
+ /* ── Teaching Style Selector ────────────────────────────────── */
779
+
780
+ function _setPersona(persona) {
781
+ currentPersona = persona;
782
+ localStorage.setItem('stemcopilot_persona', currentPersona);
783
+ _syncStyleLabel();
784
+ _syncStyleDropdown();
785
+ }
786
 
787
  function _syncStyleLabel() {
788
+ if (styleSelectorLabel) styleSelectorLabel.textContent = _PERSONA_LABELS[currentPersona] || 'Vidyut';
 
 
789
  }
790
 
791
  function _syncStyleDropdown() {
792
  if (!styleDropdown) return;
793
+ styleDropdown.querySelectorAll('.style-option').forEach(opt => opt.classList.toggle('active', opt.dataset.persona === currentPersona));
 
 
794
  }
795
 
796
  if (styleSelectorBtn) {
797
+ styleSelectorBtn.addEventListener('click', (e) => { e.stopPropagation(); styleDropdown.classList.toggle('show'); styleSelectorBtn.classList.toggle('open'); });
 
 
 
 
798
  }
799
 
800
  if (styleDropdown) {
801
  styleDropdown.querySelectorAll('.style-option').forEach(opt => {
802
  opt.addEventListener('click', (e) => {
803
  e.stopPropagation();
804
+ _setPersona(opt.dataset.persona);
 
 
 
 
 
 
 
805
  styleDropdown.classList.remove('show');
806
  styleSelectorBtn.classList.remove('open');
807
  });
 
812
  _syncStyleDropdown();
813
 
814
 
815
+ /* ── Adaptive Mic/Send Button ───────────────────────────────── */
 
 
816
 
817
+ function _bindAdaptiveBtn(btn, inputEl) {
818
+ if (!btn || !inputEl) return;
819
+
820
+ // Watch input to toggle mic ↔ send
821
+ inputEl.addEventListener('input', () => {
822
+ btn.classList.toggle('has-text', inputEl.value.trim().length > 0);
823
+ });
824
 
825
+ // Mic / Send / Voice logic
826
  const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
827
  let recognition = null;
828
 
829
  btn.addEventListener('click', () => {
830
+ const hasText = inputEl.value.trim().length > 0;
831
+
832
+ if (hasText) {
833
+ // Act as send
834
+ if (inputEl === userInput) sendMessage();
835
+ else { userInput.value = inputEl.value; sendMessage(); }
836
+ return;
837
+ }
838
+
839
+ // Act as mic
840
+ if (!SpeechRecognition) { showToast('Voice input not supported in this browser.', 'error'); return; }
841
+
842
  if (btn.classList.contains('listening')) {
843
  if (recognition) recognition.stop();
844
  btn.classList.remove('listening');
 
849
  recognition.lang = 'en-IN';
850
  recognition.interimResults = true;
851
  recognition.continuous = false;
 
852
  btn.classList.add('listening');
853
 
854
  recognition.onresult = (event) => {
855
  let transcript = '';
856
+ for (let i = event.resultIndex; i < event.results.length; i++) transcript += event.results[i][0].transcript;
857
+ inputEl.value = transcript;
858
+ inputEl.dispatchEvent(new Event('input'));
 
 
859
  };
860
 
861
  recognition.onend = () => btn.classList.remove('listening');
 
864
  });
865
  }
866
 
867
+ // Bind bottom input adaptive button
868
+ _bindAdaptiveBtn(adaptiveBtn, userInput);
869
 
870
+ // Bind hero adaptive button (static HTML version)
871
+ _bindAdaptiveBtn(heroAdaptiveBtn, heroInput);
872
 
873
+
874
+ /* ── Image Upload ───────────────────────────────────────────── */
 
875
 
876
  if (uploadBtn) uploadBtn.addEventListener('click', () => imageInput.click());
877
  if (imageInput) imageInput.addEventListener('change', () => { if (imageInput.files[0]) handleImageFile(imageInput.files[0]); });
 
879
  if (heroImageInput) heroImageInput.addEventListener('change', () => {
880
  if (heroImageInput.files[0]) {
881
  handleImageFile(heroImageInput.files[0]);
 
882
  if (heroImagePreview && heroImagePreviewThumb && pendingImageDataUrl) {
883
  heroImagePreviewThumb.style.backgroundImage = `url(${pendingImageDataUrl})`;
884
  heroImagePreview.classList.add('visible');
 
890
  const items = e.clipboardData?.items;
891
  if (!items) return;
892
  for (const item of items) {
893
+ if (item.type.startsWith('image/')) { e.preventDefault(); handleImageFile(item.getAsFile()); return; }
 
 
894
  }
895
  });
896
 
 
902
  pendingImageDataUrl = dataUrl;
903
  if (imagePreviewThumb) imagePreviewThumb.style.backgroundImage = `url(${dataUrl})`;
904
  if (imagePreviewBar) imagePreviewBar.style.display = 'flex';
 
905
  if (isHeroMode) {
906
  const hp = document.getElementById('heroImagePreviewDynamic') || heroImagePreview;
907
  const ht = document.getElementById('heroImagePreviewThumbDynamic') || heroImagePreviewThumb;
908
+ if (hp && ht) { ht.style.backgroundImage = `url(${dataUrl})`; hp.classList.add('visible'); }
 
 
 
909
  }
910
  };
911
  reader.readAsDataURL(file);
 
919
  }
920
 
921
  if (imagePreviewRemove) imagePreviewRemove.addEventListener('click', _clearImage);
922
+ if (heroImagePreviewRemove) heroImagePreviewRemove.addEventListener('click', () => { _clearImage(); if (heroImagePreview) heroImagePreview.classList.remove('visible'); });
 
 
 
923
 
924
 
925
+ /* ── Chat & Streaming ───────────────────────────────────────── */
 
 
926
 
927
  let currentAbortController = null;
928
  let currentStreamReader = null;
 
930
  let currentRenderTimer = null;
931
 
932
  function _showStopBtn() {
933
+ if (adaptiveBtn) adaptiveBtn.style.display = 'none';
934
  if (stopBtn) { stopBtn.style.display = 'flex'; stopBtn.classList.add('visible'); }
935
  }
936
+
937
+ function _showAdaptiveBtn() {
938
  if (stopBtn) { stopBtn.style.display = 'none'; stopBtn.classList.remove('visible'); }
939
+ if (adaptiveBtn) adaptiveBtn.style.display = 'flex';
940
  }
941
 
942
  if (stopBtn) {
943
  stopBtn.addEventListener('click', () => {
944
  currentStreamStopped = true;
945
  if (currentRenderTimer) { clearTimeout(currentRenderTimer); currentRenderTimer = null; }
946
+ if (currentStreamReader) { try { currentStreamReader.cancel(); } catch (_) {} currentStreamReader = null; }
947
+ if (currentAbortController) { currentAbortController.abort(); currentAbortController = null; }
 
 
 
 
 
 
948
  isSending = false;
949
+ _showAdaptiveBtn();
950
+ const ct = document.getElementById('currentThinking');
951
+ if (ct) ct.style.display = 'none';
952
  document.querySelectorAll('.ai-avatar.pulsing').forEach(el => el.classList.remove('pulsing'));
953
  document.querySelectorAll('.message-content.cursor').forEach(el => el.classList.remove('cursor'));
954
  });
955
  }
956
 
957
  if (userInput) {
958
+ userInput.addEventListener('input', function () { this.style.height = '54px'; this.style.height = this.scrollHeight + 'px'; });
959
+ userInput.addEventListener('keydown', function (e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
960
  }
961
 
962
  function sendMessage() {
 
973
  <div>${escapeHtml(text)}</div>
974
  </div>`;
975
  chatContainer.appendChild(rowDiv);
976
+ } else appendMessage('user', text);
 
 
977
 
978
+ userInput.value = '';
979
+ userInput.style.height = '54px';
980
+ // Reset adaptive btn state
981
+ if (adaptiveBtn) adaptiveBtn.classList.remove('has-text');
982
 
983
  const exists = threads.find(t => t.id === currentThreadId);
984
  if (!exists) {
 
993
 
994
  function streamResponse(text) {
995
  const imageData = pendingImage || '';
996
+ pendingImage = null; pendingImageDataUrl = null;
 
997
  if (imagePreviewBar) imagePreviewBar.style.display = 'none';
998
  if (imageInput) imageInput.value = '';
999
 
 
1020
  let rawText = '';
1021
  let firstToken = true;
1022
  currentStreamStopped = false;
 
1023
  currentRenderTimer = null;
1024
+
1025
  const RENDER_INTERVAL = 120;
1026
  function scheduleRender() {
1027
  if (currentRenderTimer || currentStreamStopped) return;
 
1042
  currentAbortController = new AbortController();
1043
 
1044
  fetch('/chat', {
1045
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
1046
+ body: JSON.stringify(payload), signal: currentAbortController.signal,
 
 
1047
  }).then(response => {
1048
  const reader = response.body.getReader();
1049
  currentStreamReader = reader;
 
1065
  if (currentStreamStopped) return;
1066
  if (!line.startsWith('data: ')) continue;
1067
  const pl = line.substring(6);
1068
+ if (pl === '[DONE]') { finishStream(thinkingEl, contentEl, rawText, currentRenderTimer, actionsEl); currentStreamStopped = true; return; }
 
 
 
1069
  try {
1070
  const data = JSON.parse(pl);
1071
  if (data.status === 'thinking') { thinkingText.textContent = data.message; continue; }
 
1073
  thinkingEl.style.display = 'none';
1074
  contentEl.style.display = 'block';
1075
  contentEl.innerHTML = `<div class="error-message">${escapeHtml(data.error)}</div>`;
1076
+ isSending = false; _showAdaptiveBtn(); currentStreamStopped = true; return;
1077
  }
1078
  if (data.token !== undefined) {
1079
  if (currentStreamStopped) return;
1080
+ if (firstToken) { thinkingEl.style.display = 'none'; contentEl.style.display = 'block'; contentEl.classList.add('cursor'); firstToken = false; }
 
 
 
 
 
1081
  rawText += data.token;
1082
  scheduleRender();
1083
  }
1084
+ } catch (_) {}
1085
  }
 
1086
  if (!currentStreamStopped) read();
1087
  }).catch(err => {
1088
  if (err.name === 'AbortError' || currentStreamStopped) return;
1089
+ thinkingEl.style.display = 'none'; contentEl.style.display = 'block';
 
1090
  if (!rawText) contentEl.innerHTML = '<div class="error-message">Connection lost. Please try again.</div>';
1091
+ isSending = false; _showAdaptiveBtn();
1092
  });
1093
  }
1094
  read();
1095
  }).catch(err => {
1096
+ if (err.name === 'AbortError' || currentStreamStopped) { thinkingEl.style.display = 'none'; contentEl.style.display = 'block'; isSending = false; _showAdaptiveBtn(); return; }
1097
+ thinkingEl.style.display = 'none'; contentEl.style.display = 'block';
1098
+ contentEl.innerHTML = '<div class="error-message">Could not connect to the server.</div>';
1099
+ isSending = false; _showAdaptiveBtn();
 
 
 
 
 
 
1100
  }).finally(() => {
1101
+ currentAbortController = null; currentStreamReader = null;
 
1102
  const avatar = rowDiv.querySelector('#avatarThinking');
1103
  if (avatar) avatar.classList.remove('pulsing');
1104
  });
 
1112
  contentEl.classList.remove('cursor');
1113
  renderFinalContent(contentEl, rawText);
1114
  isSending = false;
1115
+ _showAdaptiveBtn();
1116
  const row = thinkingEl.closest('.message-row');
1117
+ if (row) { const avatar = row.querySelector('.ai-avatar'); if (avatar) avatar.classList.remove('pulsing'); }
1118
+
 
 
 
1119
  if (actionsEl && rawText) {
1120
  actionsEl.style.display = 'flex';
1121
  actionsEl.classList.add('visible');
1122
  actionsEl.innerHTML = `
1123
+ <button class="msg-action-btn" data-action="copy" title="Copy">
1124
  <svg viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
1125
  Copy
1126
  </button>
1127
+ <button class="msg-action-btn" data-action="regenerate" title="Regenerate">
1128
  <svg viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>
1129
  Regenerate
1130
  </button>
 
1136
  navigator.clipboard.writeText(rawText).then(() => {
1137
  copyBtn.querySelector('svg + *').textContent = 'Copied';
1138
  setTimeout(() => { copyBtn.querySelector('svg + *').textContent = 'Copy'; }, 1500);
1139
+ }).catch(() => showToast('Could not copy.', 'error'));
1140
  });
1141
  }
1142
  if (regenBtn) {
1143
  regenBtn.addEventListener('click', () => {
 
1144
  const userMessages = chatContainer.querySelectorAll('.message-row.user');
1145
  if (userMessages.length) {
1146
  const lastUser = userMessages[userMessages.length - 1];
1147
  const mc = lastUser.querySelector('.message-content');
1148
+ if (mc) { row.remove(); const lastText = mc.textContent.trim(); if (lastText) streamResponse(lastText); }
 
 
 
 
 
 
1149
  }
1150
  });
1151
  }
 
1153
  }
1154
 
1155
 
1156
+ /* ── Message Helpers ────────────────────────────────────────── */
 
 
1157
 
1158
  function appendMessage(sender, text) {
1159
  const rowDiv = document.createElement('div');
 
1177
  }
1178
 
1179
 
1180
+ /* ── Renderer — Markdown + KaTeX + mhchem ───────────────────── */
 
 
1181
 
1182
  function renderFinalContent(element, rawText) {
1183
  if (!rawText) return;
 
1204
  }
1205
 
1206
 
1207
+ /* ── PWA ────────────────────────────────────────────────────── */
 
 
1208
 
1209
+ if ('serviceWorker' in navigator) navigator.serviceWorker.register('/sw.js').catch(() => {});
 
 
1210
 
1211
  let deferredPrompt = null;
1212
 
1213
  window.addEventListener('beforeinstallprompt', (e) => {
1214
+ e.preventDefault(); deferredPrompt = e;
 
1215
  const dismissed = localStorage.getItem('stemcopilot_install_dismissed');
1216
+ if (!dismissed && installBanner) installBanner.style.display = 'flex';
 
 
1217
  });
1218
 
1219
  if (installBtn) installBtn.addEventListener('click', () => {
1220
  if (deferredPrompt) {
1221
  deferredPrompt.prompt();
1222
+ deferredPrompt.userChoice.then(() => { deferredPrompt = null; if (installBanner) installBanner.style.display = 'none'; });
 
 
 
1223
  }
1224
  });
1225
 
 
1229
  });
1230
 
1231
 
1232
+ /* ── Init ───────────────────────────────────────────────────── */
 
 
1233
 
 
1234
  if (sidebar) sidebar.classList.add('collapsed');
1235
 
1236
+ try { if (screen.orientation && screen.orientation.lock) screen.orientation.lock('portrait').catch(() => {}); } catch (_) {}
 
 
 
 
 
1237
 
1238
  window.addEventListener('load', () => {
1239
  checkExistingSession();
static/index.html CHANGED
@@ -28,26 +28,26 @@
28
 
29
  <body>
30
 
31
- <!-- ==================== THEME TOGGLE (persistent, all screens) ==================== -->
32
- <button class="theme-toggle-btn" id="themeToggleBtn" title="Toggle dark / light mode">
33
- <svg class="icon-sun" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round">
34
- <circle cx="12" cy="12" r="5" />
35
- <line x1="12" y1="1" x2="12" y2="3" />
36
- <line x1="12" y1="21" x2="12" y2="23" />
37
- <line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
38
- <line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
39
- <line x1="1" y1="12" x2="3" y2="12" />
40
- <line x1="21" y1="12" x2="23" y2="12" />
41
- <line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
42
- <line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
43
- </svg>
44
- <svg class="icon-moon" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round">
45
- <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
46
- </svg>
47
- </button>
48
-
49
  <!-- ==================== LOGIN SCREEN ==================== -->
50
  <div class="login-screen" id="loginScreen">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  <div class="login-card">
52
  <img src="/assets/stembotix.png" alt="STEM Copilot" class="login-logo-stembotix">
53
  <h1 class="login-title">Welcome to STEM Copilot</h1>
@@ -105,18 +105,94 @@
105
  <button class="install-banner-dismiss" id="installDismiss">&times;</button>
106
  </div>
107
 
108
- <!-- Mobile sidebar overlay -->
109
- <div class="sidebar-overlay" id="sidebarOverlay"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
- <!-- Floating Mobile Sidebar Toggle (hidden via CSS) -->
112
- <button class="mobile-fab-toggle" id="mobileFabToggle">
113
- <img src="/assets/stembotix.png" alt="Menu">
114
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
  <!-- ==================== MAIN APP ==================== -->
117
  <div class="app-container" id="appContainer" style="display:none;">
118
 
119
- <!-- Sidebar (full) -->
120
  <aside class="sidebar collapsed" id="sidebar">
121
  <div class="sidebar-top">
122
  <div class="sidebar-logo-row">
@@ -183,7 +259,8 @@
183
 
184
  <!-- Main -->
185
  <main class="main-chat">
186
- <header class="main-header">
 
187
  <button class="toggle-sidebar-btn" id="toggleSidebarBtn" title="Toggle Sidebar">
188
  <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
189
  stroke-linecap="round" stroke-linejoin="round">
@@ -215,14 +292,18 @@
215
  </button>
216
  <input type="file" id="heroImageInput" accept="image/*" style="display:none;">
217
  <textarea id="heroInput" placeholder="Ask STEM Copilot..." rows="1"></textarea>
218
- <!-- Teaching style selector in hero -->
219
  <div class="style-selector-wrap" id="heroStyleSelectorWrap">
220
  <button class="style-selector-btn" id="heroStyleSelectorBtn" title="Teaching style">
221
- <span id="heroStyleSelectorLabel">Nerd</span>
222
  <svg viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9" /></svg>
223
  </button>
224
  <div class="style-dropdown" id="heroStyleDropdown">
225
- <div class="style-option active" data-persona="nerd">
 
 
 
 
226
  <div class="style-option-name">Nerd</div>
227
  <div class="style-option-desc">Deep, rigorous, first-principles. Goes beyond the textbook.</div>
228
  </div>
@@ -240,21 +321,14 @@
240
  </div>
241
  </div>
242
  </div>
243
- <!-- Voice input -->
244
- <button class="mic-btn" id="heroMicBtn" title="Voice input">
245
- <svg viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" /><path d="M19 10v2a7 7 0 0 1-14 0v-2" /><line x1="12" y1="19" x2="12" y2="23" /><line x1="8" y1="23" x2="16" y2="23" /></svg>
246
- </button>
247
- <button class="send-btn" id="heroSendBtn">
248
- <svg viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"></path></svg>
249
  </button>
250
  </div>
251
  </div>
252
- <div class="hero-pills" id="welcomePills">
253
- <button class="hero-pill" data-query="What is the photoelectric effect, and why did Einstein win the Nobel Prize for it?">What is the photoelectric effect and why did Einstein win the Nobel Prize for it?</button>
254
- <button class="hero-pill" data-query="Derive the relation between Kp and Kc for a general gaseous equilibrium reaction">Derive the relation between Kp and Kc for gaseous equilibrium</button>
255
- <button class="hero-pill" data-query="Solve: A ball is thrown vertically upward with velocity 20 m/s. Find maximum height and time of flight. Take g = 10 m/s^2">A ball thrown up at 20 m/s -- find max height and time of flight</button>
256
- <button class="hero-pill" data-query="Explain the concept of limits in calculus with a real-world example. How is it different from the actual value of a function?">Explain limits in calculus with a real-world example</button>
257
- </div>
258
  </div>
259
  </div>
260
 
@@ -276,10 +350,10 @@
276
  </button>
277
  <input type="file" id="imageInput" accept="image/*" style="display:none;">
278
  <textarea id="userInput" placeholder="Ask STEM Copilot..." rows="1"></textarea>
279
- <!-- Teaching style selector (RIGHT side, before mic) -->
280
  <div class="style-selector-wrap" id="styleSelectorWrap">
281
  <button class="style-selector-btn" id="styleSelectorBtn" title="Teaching style">
282
- <span id="styleSelectorLabel">Nerd</span>
283
  <svg viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9" /></svg>
284
  </button>
285
  <div class="style-dropdown" id="styleDropdown">
@@ -305,25 +379,16 @@
305
  </div>
306
  </div>
307
  </div>
308
- <!-- Voice input -->
309
- <button class="mic-btn" id="micBtn" title="Voice input">
310
- <svg viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round">
311
- <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" />
312
- <path d="M19 10v2a7 7 0 0 1-14 0v-2" />
313
- <line x1="12" y1="19" x2="12" y2="23" />
314
- <line x1="8" y1="23" x2="16" y2="23" />
315
- </svg>
316
  </button>
317
  <button class="stop-btn" id="stopBtn" title="Stop generating">
318
  <svg viewBox="0 0 24 24" fill="currentColor">
319
  <rect x="7" y="7" width="10" height="10" rx="1" />
320
  </svg>
321
  </button>
322
- <button class="send-btn" id="sendBtn">
323
- <svg viewBox="0 0 24 24">
324
- <path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"></path>
325
- </svg>
326
- </button>
327
  </div>
328
  <div class="disclaimer-text">
329
  STEM Copilot is an AI tutor grounded in NCERT material. It can still make mistakes.
@@ -351,6 +416,21 @@
351
  </svg>
352
  <span>General</span>
353
  </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
354
  <button class="settings-nav-btn" data-tab="apikeys">
355
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
356
  stroke-width="2">
@@ -393,6 +473,33 @@
393
  </select>
394
  </div>
395
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
396
  <!-- API Keys Tab -->
397
  <div class="tab-content" id="tab-apikeys">
398
  <h3 class="settings-section-title">OpenRouter API Key</h3>
@@ -417,7 +524,6 @@
417
  <button class="settings-save-btn" id="saveProfileBtn">Save Profile</button>
418
  </div>
419
 
420
- <!-- Teaching Style Tab (REMOVED - now in input bar) -->
421
  <!-- Feedback Tab -->
422
  <div class="tab-content" id="tab-feedback">
423
  <h3 class="settings-section-title">Category</h3>
 
28
 
29
  <body>
30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  <!-- ==================== LOGIN SCREEN ==================== -->
32
  <div class="login-screen" id="loginScreen">
33
+ <div class="login-top-actions">
34
+ <button class="login-theme-btn" id="loginThemeBtn" title="Toggle dark / light mode">
35
+ <svg class="icon-sun" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round">
36
+ <circle cx="12" cy="12" r="5" />
37
+ <line x1="12" y1="1" x2="12" y2="3" />
38
+ <line x1="12" y1="21" x2="12" y2="23" />
39
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
40
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
41
+ <line x1="1" y1="12" x2="3" y2="12" />
42
+ <line x1="21" y1="12" x2="23" y2="12" />
43
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
44
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
45
+ </svg>
46
+ <svg class="icon-moon" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round">
47
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
48
+ </svg>
49
+ </button>
50
+ </div>
51
  <div class="login-card">
52
  <img src="/assets/stembotix.png" alt="STEM Copilot" class="login-logo-stembotix">
53
  <h1 class="login-title">Welcome to STEM Copilot</h1>
 
105
  <button class="install-banner-dismiss" id="installDismiss">&times;</button>
106
  </div>
107
 
108
+ <!-- ==================== MOBILE TOP NAV BAR ==================== -->
109
+ <div class="mobile-topbar" id="mobileTopbar">
110
+ <button class="topbar-profile-btn" id="topbarProfileBtn" title="Account">
111
+ <img id="topbarAvatar" class="topbar-avatar" src="/assets/bot.png" alt="">
112
+ </button>
113
+ <img src="/assets/stembotix.png" alt="STEM Copilot" class="topbar-logo">
114
+ <div class="topbar-actions">
115
+ <button class="topbar-icon-btn" id="topbarHistoryBtn" title="Chat history">
116
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
117
+ stroke-linecap="round" stroke-linejoin="round">
118
+ <circle cx="12" cy="12" r="10" />
119
+ <polyline points="12 6 12 12 16 14" />
120
+ </svg>
121
+ </button>
122
+ <button class="topbar-icon-btn" id="topbarNewChatBtn" title="New chat">
123
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
124
+ stroke-linecap="round" stroke-linejoin="round">
125
+ <path d="M12 5v14M5 12h14" />
126
+ </svg>
127
+ </button>
128
+ </div>
129
+ </div>
130
 
131
+ <!-- ==================== MOBILE ACCOUNT OVERLAY ==================== -->
132
+ <div class="fullscreen-overlay" id="accountOverlay">
133
+ <div class="overlay-header">
134
+ <h2>Account</h2>
135
+ <button class="overlay-close-btn" id="accountOverlayClose">&times;</button>
136
+ </div>
137
+ <div class="overlay-body">
138
+ <div class="overlay-user-info" id="overlayUserInfo">
139
+ <img id="overlayUserAvatar" class="overlay-avatar" src="/assets/bot.png" alt="">
140
+ <div class="overlay-user-details">
141
+ <span class="overlay-user-name" id="overlayUserName">Student</span>
142
+ <span class="overlay-user-email" id="overlayUserEmail"></span>
143
+ </div>
144
+ </div>
145
+ <div class="overlay-menu">
146
+ <button class="overlay-menu-item" id="overlaySettingsBtn">
147
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
148
+ <circle cx="12" cy="12" r="3" />
149
+ <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.6 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.6a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" />
150
+ </svg>
151
+ Settings
152
+ </button>
153
+ <button class="overlay-menu-item" id="overlayProfileBtn">
154
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
155
+ <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
156
+ <circle cx="12" cy="7" r="4" />
157
+ </svg>
158
+ Profile
159
+ </button>
160
+ <button class="overlay-menu-item overlay-menu-logout" id="overlayLogoutBtn">
161
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
162
+ <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
163
+ <polyline points="16 17 21 12 16 7" />
164
+ <line x1="21" y1="12" x2="9" y2="12" />
165
+ </svg>
166
+ Log out
167
+ </button>
168
+ </div>
169
+ </div>
170
+ </div>
171
+
172
+ <!-- ==================== MOBILE HISTORY OVERLAY ==================== -->
173
+ <div class="fullscreen-overlay" id="historyOverlay">
174
+ <div class="overlay-header">
175
+ <h2>Chat History</h2>
176
+ <button class="overlay-close-btn" id="historyOverlayClose">&times;</button>
177
+ </div>
178
+ <div class="overlay-body">
179
+ <div class="overlay-history-list" id="overlayHistoryList"></div>
180
+ <div class="overlay-empty-state" id="overlayHistoryEmpty">
181
+ <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="opacity:0.3">
182
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
183
+ </svg>
184
+ <p>No conversations yet</p>
185
+ </div>
186
+ </div>
187
+ </div>
188
+
189
+ <!-- Mobile sidebar overlay (kept for edge-swipe on mobile) -->
190
+ <div class="sidebar-overlay" id="sidebarOverlay"></div>
191
 
192
  <!-- ==================== MAIN APP ==================== -->
193
  <div class="app-container" id="appContainer" style="display:none;">
194
 
195
+ <!-- Sidebar (full — desktop only) -->
196
  <aside class="sidebar collapsed" id="sidebar">
197
  <div class="sidebar-top">
198
  <div class="sidebar-logo-row">
 
259
 
260
  <!-- Main -->
261
  <main class="main-chat">
262
+ <!-- Desktop header -->
263
+ <header class="main-header" id="mainHeader">
264
  <button class="toggle-sidebar-btn" id="toggleSidebarBtn" title="Toggle Sidebar">
265
  <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
266
  stroke-linecap="round" stroke-linejoin="round">
 
292
  </button>
293
  <input type="file" id="heroImageInput" accept="image/*" style="display:none;">
294
  <textarea id="heroInput" placeholder="Ask STEM Copilot..." rows="1"></textarea>
295
+ <!-- Teaching style selector (scrollable) -->
296
  <div class="style-selector-wrap" id="heroStyleSelectorWrap">
297
  <button class="style-selector-btn" id="heroStyleSelectorBtn" title="Teaching style">
298
+ <span id="heroStyleSelectorLabel">Vidyut</span>
299
  <svg viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9" /></svg>
300
  </button>
301
  <div class="style-dropdown" id="heroStyleDropdown">
302
+ <div class="style-option active" data-persona="vidyut">
303
+ <div class="style-option-name">Vidyut</div>
304
+ <div class="style-option-desc">Calm, clear, step-by-step master teacher. Makes hard concepts obvious.</div>
305
+ </div>
306
+ <div class="style-option" data-persona="nerd">
307
  <div class="style-option-name">Nerd</div>
308
  <div class="style-option-desc">Deep, rigorous, first-principles. Goes beyond the textbook.</div>
309
  </div>
 
321
  </div>
322
  </div>
323
  </div>
324
+ <!-- Adaptive action button: mic when empty, send when text present -->
325
+ <button class="adaptive-action-btn" id="heroAdaptiveBtn" title="Voice input">
326
+ <svg class="icon-mic" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" /><path d="M19 10v2a7 7 0 0 1-14 0v-2" /><line x1="12" y1="19" x2="12" y2="23" /><line x1="8" y1="23" x2="16" y2="23" /></svg>
327
+ <svg class="icon-send" viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"></path></svg>
 
 
328
  </button>
329
  </div>
330
  </div>
331
+ <!-- Hero pills REMOVED per todo item 3 -->
 
 
 
 
 
332
  </div>
333
  </div>
334
 
 
350
  </button>
351
  <input type="file" id="imageInput" accept="image/*" style="display:none;">
352
  <textarea id="userInput" placeholder="Ask STEM Copilot..." rows="1"></textarea>
353
+ <!-- Teaching style selector -->
354
  <div class="style-selector-wrap" id="styleSelectorWrap">
355
  <button class="style-selector-btn" id="styleSelectorBtn" title="Teaching style">
356
+ <span id="styleSelectorLabel">Vidyut</span>
357
  <svg viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9" /></svg>
358
  </button>
359
  <div class="style-dropdown" id="styleDropdown">
 
379
  </div>
380
  </div>
381
  </div>
382
+ <!-- Adaptive action button + stop -->
383
+ <button class="adaptive-action-btn" id="adaptiveBtn" title="Voice input">
384
+ <svg class="icon-mic" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" /><path d="M19 10v2a7 7 0 0 1-14 0v-2" /><line x1="12" y1="19" x2="12" y2="23" /><line x1="8" y1="23" x2="16" y2="23" /></svg>
385
+ <svg class="icon-send" viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"></path></svg>
 
 
 
 
386
  </button>
387
  <button class="stop-btn" id="stopBtn" title="Stop generating">
388
  <svg viewBox="0 0 24 24" fill="currentColor">
389
  <rect x="7" y="7" width="10" height="10" rx="1" />
390
  </svg>
391
  </button>
 
 
 
 
 
392
  </div>
393
  <div class="disclaimer-text">
394
  STEM Copilot is an AI tutor grounded in NCERT material. It can still make mistakes.
 
416
  </svg>
417
  <span>General</span>
418
  </button>
419
+ <button class="settings-nav-btn" data-tab="appearance">
420
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
421
+ stroke-width="2">
422
+ <circle cx="12" cy="12" r="5" />
423
+ <line x1="12" y1="1" x2="12" y2="3" />
424
+ <line x1="12" y1="21" x2="12" y2="23" />
425
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
426
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
427
+ <line x1="1" y1="12" x2="3" y2="12" />
428
+ <line x1="21" y1="12" x2="23" y2="12" />
429
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
430
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
431
+ </svg>
432
+ <span>Appearance</span>
433
+ </button>
434
  <button class="settings-nav-btn" data-tab="apikeys">
435
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
436
  stroke-width="2">
 
473
  </select>
474
  </div>
475
 
476
+ <!-- Appearance Tab -->
477
+ <div class="tab-content" id="tab-appearance">
478
+ <h3 class="settings-section-title">Theme</h3>
479
+ <div class="theme-option-group">
480
+ <button class="theme-option-btn" data-theme-val="dark" id="themeDarkBtn">
481
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
482
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
483
+ </svg>
484
+ Dark
485
+ </button>
486
+ <button class="theme-option-btn" data-theme-val="light" id="themeLightBtn">
487
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
488
+ <circle cx="12" cy="12" r="5" />
489
+ <line x1="12" y1="1" x2="12" y2="3" />
490
+ <line x1="12" y1="21" x2="12" y2="23" />
491
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
492
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
493
+ <line x1="1" y1="12" x2="3" y2="12" />
494
+ <line x1="21" y1="12" x2="23" y2="12" />
495
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
496
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
497
+ </svg>
498
+ Light
499
+ </button>
500
+ </div>
501
+ </div>
502
+
503
  <!-- API Keys Tab -->
504
  <div class="tab-content" id="tab-apikeys">
505
  <h3 class="settings-section-title">OpenRouter API Key</h3>
 
524
  <button class="settings-save-btn" id="saveProfileBtn">Save Profile</button>
525
  </div>
526
 
 
527
  <!-- Feedback Tab -->
528
  <div class="tab-content" id="tab-feedback">
529
  <h3 class="settings-section-title">Category</h3>
static/style.css CHANGED
@@ -1,12 +1,15 @@
1
  /* ============================================================
2
  STEM Copilot — Stylesheet
3
  Montserrat, dark/light theme, premium chat UI
 
4
  ============================================================ */
5
 
 
 
6
  :root {
7
- --brand: #EB5A28;
8
- --brand-hover: #cf4f22;
9
- --brand-glow: rgba(235, 90, 40, 0.25);
10
  --bg-main: #000000;
11
  --bg-sidebar: #0a0a0a;
12
  --bg-input: #1a1a1a;
@@ -26,13 +29,14 @@
26
  --transition: 0.35s cubic-bezier(0.4, 0, 0.2, 1);
27
  --sidebar-width: 280px;
28
  --rail-width: 56px;
 
29
  }
30
 
31
- /* ── LIGHT MODE ── */
32
  [data-theme="light"] {
33
- --brand: #4285F4;
34
- --brand-hover: #3367D6;
35
- --brand-glow: rgba(66, 133, 244, 0.25);
36
  --bg-main: #f8f9fa;
37
  --bg-sidebar: #ffffff;
38
  --bg-input: #ffffff;
@@ -48,6 +52,9 @@
48
  --msg-ai: transparent;
49
  }
50
 
 
 
 
51
  * {
52
  box-sizing: border-box;
53
  margin: 0;
@@ -59,6 +66,7 @@ html {
59
  -ms-text-size-adjust: 100%;
60
  touch-action: manipulation;
61
  overscroll-behavior: none;
 
62
  }
63
 
64
  body {
@@ -68,98 +76,80 @@ body {
68
  height: 100vh;
69
  height: 100dvh;
70
  overflow: hidden;
 
71
  touch-action: manipulation;
72
  -webkit-tap-highlight-color: transparent;
73
  overscroll-behavior: none;
74
- /* Smooth theme transitions — uniform 0.35s for all components */
75
  transition: background-color 0.35s ease, color 0.35s ease;
76
  }
77
 
78
- /* Smooth transitions for all themed elements — UNIFORM timing */
79
- *,
80
- *::before,
81
- *::after {
82
  transition: background-color 0.35s ease, color 0.35s ease, border-color 0.35s ease, box-shadow 0.35s ease, fill 0.35s ease, stroke 0.35s ease;
83
  }
84
 
85
- textarea,
86
- input,
87
- select {
88
  transition: background-color 0.35s ease, color 0.35s ease, border-color 0.35s ease, box-shadow 0.35s ease;
89
  }
90
 
91
 
92
  /* ============================================================
93
- THEME TOGGLE BUTTON (top-right, all screens)
94
  ============================================================ */
95
 
96
- .theme-toggle-btn {
 
 
 
 
 
 
 
 
 
 
97
  position: fixed;
98
  top: 14px;
99
  right: 16px;
100
  z-index: 1100;
 
 
 
 
 
101
  width: 36px;
102
  height: 36px;
103
  border-radius: 50%;
104
- border: 1px solid var(--border-color);
105
- background: var(--bg-elevated);
106
  cursor: pointer;
107
  display: flex;
108
  align-items: center;
109
  justify-content: center;
110
- transition: background 0.3s, border-color 0.3s, transform 0.2s;
111
  }
112
 
113
- .theme-toggle-btn:hover {
114
- background: var(--bg-hover);
115
- border-color: var(--brand);
116
- transform: scale(1.08);
117
  }
118
 
119
- .theme-toggle-btn svg {
120
- width: 18px;
121
- height: 18px;
122
  stroke: var(--text-secondary);
123
  fill: none;
124
  stroke-width: 2;
125
  transition: stroke 0.3s;
126
  }
127
 
128
- .theme-toggle-btn:hover svg {
129
  stroke: var(--brand);
130
  }
131
 
132
- /* Show sun in dark, moon in light */
133
- .theme-toggle-btn .icon-sun {
134
- display: block;
135
- }
136
-
137
- .theme-toggle-btn .icon-moon {
138
- display: none;
139
- }
140
-
141
- [data-theme="light"] .theme-toggle-btn .icon-sun {
142
- display: none;
143
- }
144
-
145
- [data-theme="light"] .theme-toggle-btn .icon-moon {
146
- display: block;
147
- }
148
-
149
-
150
- /* ============================================================
151
- LOGIN SCREEN
152
- ============================================================ */
153
-
154
- .login-screen {
155
- position: fixed;
156
- inset: 0;
157
- z-index: 1000;
158
- background: var(--bg-main);
159
- display: flex;
160
- align-items: center;
161
- justify-content: center;
162
- }
163
 
164
  .login-card {
165
  text-align: center;
@@ -269,14 +259,8 @@ select {
269
  color: var(--text-secondary);
270
  }
271
 
272
- .byok-steps a {
273
- color: var(--brand);
274
- text-decoration: none;
275
- }
276
-
277
- .byok-steps a:hover {
278
- text-decoration: underline;
279
- }
280
 
281
  .byok-submit-btn {
282
  width: 100%;
@@ -293,9 +277,7 @@ select {
293
  transition: background 0.3s;
294
  }
295
 
296
- .byok-submit-btn:hover {
297
- background: var(--brand-hover);
298
- }
299
 
300
  .byok-note {
301
  font-size: 11px;
@@ -327,11 +309,7 @@ select {
327
  animation: slideUp 0.4s cubic-bezier(0.16, 1, 0.3, 1);
328
  }
329
 
330
- .install-banner-icon {
331
- width: 36px;
332
- height: 36px;
333
- border-radius: 8px;
334
- }
335
 
336
  .install-banner-text {
337
  flex: 1;
@@ -341,14 +319,8 @@ select {
341
  line-height: 1.4;
342
  }
343
 
344
- .install-banner-text strong {
345
- color: var(--text-primary);
346
- }
347
-
348
- .install-banner-text span {
349
- color: var(--text-secondary);
350
- font-size: 11px;
351
- }
352
 
353
  .install-banner-btn {
354
  background: var(--brand);
@@ -363,29 +335,310 @@ select {
363
  transition: background 0.3s;
364
  }
365
 
366
- .install-banner-btn:hover {
367
- background: var(--brand-hover);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
368
  }
369
 
370
- .install-banner-dismiss {
371
- background: none;
372
- border: none;
373
- color: var(--text-muted);
374
- font-size: 18px;
375
- cursor: pointer;
376
- padding: 0 4px;
377
  }
378
 
379
- @keyframes slideUp {
380
- from {
381
- opacity: 0;
382
- transform: translateX(-50%) translateY(20px);
383
- }
384
 
385
- to {
386
- opacity: 1;
387
- transform: translateX(-50%) translateY(0);
388
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389
  }
390
 
391
 
@@ -398,11 +651,12 @@ select {
398
  height: 100vh;
399
  height: 100dvh;
400
  overflow: hidden;
 
401
  }
402
 
403
 
404
  /* ============================================================
405
- SIDEBAR (full) — premium look
406
  ============================================================ */
407
 
408
  .sidebar {
@@ -441,11 +695,12 @@ select {
441
  object-fit: contain;
442
  }
443
 
 
444
  .new-chat-btn {
445
  margin: 8px 14px;
446
  padding: 12px 16px;
447
- background: linear-gradient(135deg, rgba(235, 90, 40, 0.08), rgba(235, 90, 40, 0.02));
448
- border: 1px solid rgba(235, 90, 40, 0.2);
449
  color: var(--text-primary);
450
  border-radius: var(--radius);
451
  cursor: pointer;
@@ -459,9 +714,9 @@ select {
459
  }
460
 
461
  .new-chat-btn:hover {
462
- background: linear-gradient(135deg, rgba(235, 90, 40, 0.15), rgba(235, 90, 40, 0.05));
463
- border-color: rgba(235, 90, 40, 0.4);
464
- box-shadow: 0 0 16px rgba(235, 90, 40, 0.1);
465
  }
466
 
467
  .new-chat-btn svg {
@@ -490,36 +745,19 @@ select {
490
  transition: background 0.2s, color 0.2s, transform 0.15s;
491
  }
492
 
493
- .history-item:hover {
494
- background: var(--bg-hover);
495
- color: var(--text-primary);
496
- }
497
-
498
- .history-item:active {
499
- transform: scale(0.98);
500
- }
501
 
502
  .history-item.active {
503
- background: linear-gradient(135deg, rgba(235, 90, 40, 0.1), rgba(235, 90, 40, 0.03));
504
  color: var(--text-primary);
505
- border: 1px solid rgba(235, 90, 40, 0.15);
506
- }
507
-
508
- .history-item:not(.active) {
509
- border: 1px solid transparent;
510
  }
511
 
512
- .thread-icon {
513
- flex-shrink: 0;
514
- opacity: 0.4;
515
- width: 16px;
516
- height: 16px;
517
- }
518
 
519
- .history-item.active .thread-icon {
520
- opacity: 0.8;
521
- color: var(--brand);
522
- }
523
 
524
  .chat-title {
525
  flex: 1;
@@ -541,14 +779,8 @@ select {
541
  transition: opacity 0.2s, color 0.2s;
542
  }
543
 
544
- .history-item:hover .options-btn,
545
- .options-btn.menu-open {
546
- opacity: 1;
547
- }
548
-
549
- .options-btn:hover {
550
- color: var(--text-primary);
551
- }
552
 
553
  .options-menu {
554
  position: absolute;
@@ -565,9 +797,7 @@ select {
565
  overflow: hidden;
566
  }
567
 
568
- .options-menu.show {
569
- display: flex;
570
- }
571
 
572
  .option-item {
573
  padding: 10px 14px;
@@ -580,13 +810,8 @@ select {
580
  transition: background 0.2s;
581
  }
582
 
583
- .option-item:hover {
584
- background: var(--bg-hover);
585
- }
586
-
587
- .option-item.delete {
588
- color: #ff4a4a;
589
- }
590
 
591
  .rename-input {
592
  width: 100%;
@@ -600,14 +825,11 @@ select {
600
  padding: 2px 0;
601
  }
602
 
603
-
604
- /* --- Sidebar bottom — user profile --- */
605
-
606
  .sidebar-bottom {
607
  padding: 12px 14px 16px;
608
  border-top: 1px solid var(--border-color);
609
  position: relative;
610
- background: linear-gradient(180deg, transparent, rgba(235, 90, 40, 0.02));
611
  }
612
 
613
  .user-profile-btn {
@@ -637,9 +859,7 @@ select {
637
  transition: border-color 0.2s;
638
  }
639
 
640
- .user-profile-btn:hover .user-avatar {
641
- border-color: var(--brand);
642
- }
643
 
644
  .user-name {
645
  flex: 1;
@@ -665,10 +885,7 @@ select {
665
  backdrop-filter: blur(12px);
666
  }
667
 
668
- .user-menu.show {
669
- display: block;
670
- animation: fadeIn 0.15s ease-out;
671
- }
672
 
673
  .user-menu-item {
674
  padding: 12px 18px;
@@ -682,33 +899,16 @@ select {
682
  color: var(--text-primary);
683
  }
684
 
685
- .user-menu-item:hover {
686
- background: var(--bg-hover);
687
- color: var(--brand);
688
- }
689
-
690
- .user-menu-item svg {
691
- opacity: 0.6;
692
- transition: opacity 0.2s;
693
- }
694
-
695
- .user-menu-item:hover svg {
696
- opacity: 1;
697
- }
698
-
699
- .user-menu-logout {
700
- color: #ff4a4a;
701
- border-top: 1px solid var(--border-subtle);
702
- }
703
 
704
- .user-menu-logout:hover {
705
- color: #ff4a4a;
706
- background: rgba(255, 74, 74, 0.06);
707
- }
708
 
709
 
710
  /* ============================================================
711
- SIDEBAR RAIL (collapsed mode — desktop only)
712
  ============================================================ */
713
 
714
  .sidebar-rail {
@@ -724,12 +924,9 @@ select {
724
  z-index: 20;
725
  }
726
 
727
- .sidebar-rail.visible {
728
- display: flex;
729
- }
730
 
731
- .rail-top,
732
- .rail-bottom {
733
  display: flex;
734
  flex-direction: column;
735
  align-items: center;
@@ -750,25 +947,10 @@ select {
750
  transition: background 0.2s, color 0.2s;
751
  }
752
 
753
- .rail-icon-btn:hover {
754
- background: var(--bg-hover);
755
- color: var(--text-primary);
756
- }
757
-
758
- .rail-logo {
759
- width: 34px;
760
- height: 34px;
761
- border-radius: 6px;
762
- object-fit: contain;
763
- }
764
 
765
- .rail-avatar {
766
- width: 28px;
767
- height: 28px;
768
- border-radius: 50%;
769
- object-fit: cover;
770
- background: var(--bg-hover);
771
- }
772
 
773
 
774
  /* ============================================================
@@ -807,14 +989,12 @@ select {
807
  transition: background 0.2s, color 0.2s;
808
  }
809
 
810
- .toggle-sidebar-btn:hover {
811
- background: var(--bg-hover);
812
- color: var(--text-primary);
813
- }
814
 
815
  .chat-container {
816
  flex: 1;
817
  overflow-y: auto;
 
818
  padding: 64px 20px 20px;
819
  display: flex;
820
  flex-direction: column;
@@ -842,8 +1022,7 @@ select {
842
  width: 120px;
843
  height: 120px;
844
  object-fit: contain;
845
- filter: drop-shadow(0 8px 24px rgba(235, 90, 40, 0.15));
846
- /* NO animation — fixed, no jumping */
847
  }
848
 
849
  .hero-title {
@@ -854,9 +1033,7 @@ select {
854
  line-height: 1.3;
855
  }
856
 
857
- .hero-title .hero-name {
858
- color: var(--brand);
859
- }
860
 
861
  .hero-input-wrap {
862
  width: 100%;
@@ -875,9 +1052,7 @@ select {
875
  border-radius: var(--radius);
876
  }
877
 
878
- .hero-image-preview.visible {
879
- display: flex;
880
- }
881
 
882
  .hero-image-preview-thumb {
883
  width: 48px;
@@ -898,9 +1073,7 @@ select {
898
  transition: color 0.2s;
899
  }
900
 
901
- .hero-image-preview-remove:hover {
902
- color: #ff4a4a;
903
- }
904
 
905
  .hero-input-box {
906
  display: flex;
@@ -931,40 +1104,12 @@ select {
931
  max-height: 120px;
932
  line-height: 1.5;
933
  overflow-y: hidden;
 
 
934
  }
935
 
936
- .hero-input-box textarea::placeholder {
937
- color: var(--text-muted);
938
- }
939
-
940
- .hero-pills {
941
- display: grid;
942
- grid-template-columns: 1fr 1fr;
943
- gap: 10px;
944
- max-width: 560px;
945
- width: 100%;
946
- }
947
-
948
- .hero-pill {
949
- padding: 14px 16px;
950
- background: var(--bg-card);
951
- border: 1px solid var(--border-color);
952
- border-radius: var(--radius);
953
- color: var(--text-secondary);
954
- font-family: inherit;
955
- font-size: 13px;
956
- font-weight: 500;
957
- cursor: pointer;
958
- text-align: left;
959
- transition: background 0.2s, color 0.2s, border-color 0.2s, box-shadow 0.2s;
960
- }
961
-
962
- .hero-pill:hover {
963
- background: var(--bg-hover);
964
- border-color: var(--brand);
965
- color: var(--text-primary);
966
- box-shadow: 0 0 0 1px var(--brand-glow);
967
- }
968
 
969
 
970
  /* ============================================================
@@ -979,13 +1124,8 @@ select {
979
  animation: fadeIn 0.35s ease-out forwards;
980
  }
981
 
982
- .message-row.user {
983
- justify-content: flex-end;
984
- }
985
-
986
- .message-row.ai {
987
- justify-content: flex-start;
988
- }
989
 
990
  .message-content {
991
  padding: 14px 18px;
@@ -1003,10 +1143,7 @@ select {
1003
  color: var(--text-primary);
1004
  }
1005
 
1006
- .ai .message-content {
1007
- max-width: 100%;
1008
- background: var(--msg-ai);
1009
- }
1010
 
1011
  .ai-avatar {
1012
  width: 30px;
@@ -1020,27 +1157,14 @@ select {
1020
  margin-top: 4px;
1021
  }
1022
 
1023
- .ai-avatar.pulsing {
1024
- animation: avatarPulse 1.5s ease-in-out infinite;
1025
- }
1026
 
1027
  @keyframes avatarPulse {
1028
-
1029
- 0%,
1030
- 100% {
1031
- opacity: 0.6;
1032
- transform: scale(1);
1033
- }
1034
-
1035
- 50% {
1036
- opacity: 1;
1037
- transform: scale(1.05);
1038
- }
1039
  }
1040
 
1041
-
1042
- /* --- Thinking indicator --- */
1043
-
1044
  .thinking-indicator {
1045
  display: flex;
1046
  align-items: center;
@@ -1049,51 +1173,23 @@ select {
1049
  animation: fadeIn 0.3s ease-out;
1050
  }
1051
 
1052
- .thinking-indicator.hidden {
1053
- display: none;
1054
- }
1055
-
1056
- .thinking-text {
1057
- font-size: 13px;
1058
- color: var(--text-secondary);
1059
- font-style: italic;
1060
- }
1061
-
1062
 
1063
- /* --- Rendered markdown in AI messages --- */
1064
 
1065
- .message-content h1,
1066
- .message-content h2,
1067
- .message-content h3 {
1068
  margin-top: 16px;
1069
  margin-bottom: 8px;
1070
  font-weight: 600;
1071
  }
1072
 
1073
- .message-content h1 {
1074
- font-size: 1.25em;
1075
- }
1076
-
1077
- .message-content h2 {
1078
- font-size: 1.12em;
1079
- }
1080
-
1081
- .message-content h3 {
1082
- font-size: 1.05em;
1083
- }
1084
-
1085
- .message-content p {
1086
- margin-bottom: 10px;
1087
- }
1088
-
1089
- .message-content ul,
1090
- .message-content ol {
1091
- margin: 8px 0 8px 20px;
1092
- }
1093
-
1094
- .message-content li {
1095
- margin-bottom: 4px;
1096
- }
1097
 
1098
  .message-content code {
1099
  background: rgba(0, 0, 0, 0.15);
@@ -1103,9 +1199,7 @@ select {
1103
  font-size: 0.88em;
1104
  }
1105
 
1106
- [data-theme="light"] .message-content code {
1107
- background: rgba(0, 0, 0, 0.06);
1108
- }
1109
 
1110
  .message-content pre {
1111
  background: #111;
@@ -1116,33 +1210,18 @@ select {
1116
  margin: 10px 0;
1117
  }
1118
 
1119
- [data-theme="light"] .message-content pre {
1120
- background: #f5f5f5;
1121
- }
1122
-
1123
- .message-content pre code {
1124
- background: none;
1125
- padding: 0;
1126
- }
1127
-
1128
- .message-content table {
1129
- border-collapse: collapse;
1130
- margin: 10px 0;
1131
- width: 100%;
1132
- }
1133
 
1134
- .message-content th,
1135
- .message-content td {
1136
  border: 1px solid var(--border-color);
1137
  padding: 8px 12px;
1138
  text-align: left;
1139
  font-size: 14px;
1140
  }
1141
 
1142
- .message-content th {
1143
- background: var(--bg-input);
1144
- font-weight: 600;
1145
- }
1146
 
1147
  .message-content blockquote {
1148
  border-left: 3px solid var(--brand);
@@ -1151,24 +1230,13 @@ select {
1151
  color: var(--text-secondary);
1152
  }
1153
 
1154
- .katex {
1155
- color: var(--text-primary);
1156
- }
1157
-
1158
- .katex-display {
1159
- margin: 14px 0;
1160
- overflow-x: auto;
1161
- }
1162
-
1163
- .message-image {
1164
- max-width: 240px;
1165
- border-radius: 8px;
1166
- margin-bottom: 8px;
1167
- }
1168
 
1169
 
1170
  /* ============================================================
1171
- AI MESSAGE ACTION BAR (copy, regenerate)
1172
  ============================================================ */
1173
 
1174
  .msg-actions {
@@ -1180,10 +1248,7 @@ select {
1180
  transition: opacity 0.2s;
1181
  }
1182
 
1183
- .message-row.ai:hover .msg-actions,
1184
- .msg-actions.visible {
1185
- opacity: 1;
1186
- }
1187
 
1188
  .msg-action-btn {
1189
  background: var(--bg-card);
@@ -1207,13 +1272,7 @@ select {
1207
  border-color: var(--brand);
1208
  }
1209
 
1210
- .msg-action-btn svg {
1211
- width: 13px;
1212
- height: 13px;
1213
- stroke: currentColor;
1214
- fill: none;
1215
- stroke-width: 2;
1216
- }
1217
 
1218
 
1219
  /* ============================================================
@@ -1261,9 +1320,7 @@ select {
1261
  transition: color 0.2s;
1262
  }
1263
 
1264
- .image-preview-remove:hover {
1265
- color: #ff4a4a;
1266
- }
1267
 
1268
  .input-box {
1269
  width: 100%;
@@ -1294,16 +1351,13 @@ select {
1294
  transition: color 0.2s;
1295
  }
1296
 
1297
- .upload-btn:hover {
1298
- color: var(--text-primary);
1299
- }
1300
 
1301
- /* Teaching style selector in input bar — right side before mic */
1302
  .style-selector-wrap {
1303
  position: relative;
1304
  display: flex;
1305
  align-items: center;
1306
- order: 10; /* push to right */
1307
  }
1308
 
1309
  .style-selector-btn {
@@ -1339,9 +1393,7 @@ select {
1339
  transition: transform 0.2s;
1340
  }
1341
 
1342
- .style-selector-btn.open svg {
1343
- transform: rotate(180deg);
1344
- }
1345
 
1346
  .style-dropdown {
1347
  display: none;
@@ -1354,121 +1406,49 @@ select {
1354
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
1355
  z-index: 100;
1356
  min-width: 240px;
1357
- overflow: hidden;
 
1358
  animation: fadeIn 0.15s ease-out;
1359
  }
1360
 
1361
- .style-dropdown.show {
1362
- display: block;
1363
- }
1364
-
1365
- .style-option {
1366
- padding: 10px 14px;
1367
- cursor: pointer;
1368
- transition: background 0.15s;
1369
- border-bottom: 1px solid var(--border-subtle);
1370
- }
1371
-
1372
- .style-option:last-child {
1373
- border-bottom: none;
1374
- }
1375
-
1376
- .style-option:hover {
1377
- background: var(--bg-hover);
1378
- }
1379
-
1380
- .style-option.active {
1381
- background: rgba(235, 90, 40, 0.08);
1382
- }
1383
-
1384
- [data-theme="light"] .style-option.active {
1385
- background: rgba(66, 133, 244, 0.08);
1386
- }
1387
-
1388
- .style-option-name {
1389
- font-size: 13px;
1390
- font-weight: 600;
1391
- color: var(--text-primary);
1392
- margin-bottom: 2px;
1393
- }
1394
-
1395
- .style-option.active .style-option-name {
1396
- color: var(--brand);
1397
- }
1398
-
1399
- .style-option-desc {
1400
- font-size: 11px;
1401
- color: var(--text-muted);
1402
- line-height: 1.4;
1403
- }
1404
-
1405
- /* Voice mic button */
1406
- .mic-btn {
1407
- background: none;
1408
- border: none;
1409
- color: var(--text-secondary);
1410
- cursor: pointer;
1411
- padding: 16px 4px;
1412
- display: flex;
1413
- align-items: center;
1414
- transition: color 0.2s;
1415
- }
1416
-
1417
- .mic-btn:hover {
1418
- color: var(--text-primary);
1419
- }
1420
-
1421
- .mic-btn.listening {
1422
- color: #EA4335 !important;
1423
- animation: micPulse 1.5s infinite ease-in-out;
1424
- }
1425
-
1426
- @keyframes micPulse {
1427
-
1428
- 0%,
1429
- 100% {
1430
- transform: scale(1);
1431
- opacity: 1;
1432
- }
1433
-
1434
- 50% {
1435
- transform: scale(1.15);
1436
- opacity: 0.8;
1437
- }
1438
- }
1439
-
1440
- .mic-btn svg {
1441
- width: 20px;
1442
- height: 20px;
1443
- stroke: currentColor;
1444
- fill: none;
1445
- stroke-width: 2;
1446
  }
1447
 
1448
- #userInput {
1449
- flex: 1;
1450
- background: transparent;
1451
- border: none;
 
 
 
 
1452
  color: var(--text-primary);
1453
- padding: 16px 12px 16px 8px;
1454
- font-family: inherit;
1455
- font-size: 14px;
1456
- resize: none;
1457
- outline: none;
1458
- height: 54px;
1459
- max-height: 200px;
1460
- line-height: 1.5;
1461
- overflow-y: hidden;
1462
  }
1463
 
1464
- #userInput::placeholder {
 
 
 
1465
  color: var(--text-muted);
 
1466
  }
1467
 
1468
- .send-btn {
 
 
 
 
 
1469
  background: none;
1470
  border: none;
1471
- color: var(--text-secondary);
1472
  cursor: pointer;
1473
  padding: 16px 16px 16px 4px;
1474
  display: flex;
@@ -1476,20 +1456,42 @@ select {
1476
  transition: color 0.2s, transform 0.1s;
1477
  }
1478
 
1479
- .send-btn:hover {
1480
- color: var(--brand);
 
 
 
 
 
1481
  }
1482
 
1483
- .send-btn:active {
1484
- transform: scale(0.92);
 
 
1485
  }
1486
 
1487
- .send-btn svg {
1488
- width: 20px;
1489
- height: 20px;
1490
  fill: currentColor;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1491
  }
1492
 
 
1493
  .stop-btn {
1494
  background: none;
1495
  border: none;
@@ -1501,23 +1503,32 @@ select {
1501
  transition: color 0.2s, transform 0.1s;
1502
  }
1503
 
1504
- .stop-btn.visible {
1505
- display: flex;
1506
- }
1507
-
1508
- .stop-btn:hover {
1509
- color: #ff4a4a;
1510
- }
1511
 
1512
- .stop-btn:active {
1513
- transform: scale(0.92);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1514
  }
1515
 
1516
- .stop-btn svg {
1517
- width: 22px;
1518
- height: 22px;
1519
- fill: currentColor;
1520
- }
1521
 
1522
  .disclaimer-text {
1523
  font-size: 11px;
@@ -1543,9 +1554,7 @@ select {
1543
  justify-content: center;
1544
  }
1545
 
1546
- .modal-overlay.show {
1547
- display: flex;
1548
- }
1549
 
1550
  .settings-modal {
1551
  background: var(--bg-sidebar);
@@ -1570,10 +1579,7 @@ select {
1570
  flex-shrink: 0;
1571
  }
1572
 
1573
- .modal-header h2 {
1574
- font-size: 18px;
1575
- font-weight: 600;
1576
- }
1577
 
1578
  .modal-close {
1579
  background: none;
@@ -1585,14 +1591,9 @@ select {
1585
  transition: color 0.2s;
1586
  }
1587
 
1588
- .modal-close:hover {
1589
- color: var(--text-primary);
1590
- }
1591
 
1592
- .modal-body {
1593
- padding: 0;
1594
- overflow: hidden;
1595
- }
1596
 
1597
  .settings-layout {
1598
  display: flex;
@@ -1629,10 +1630,7 @@ select {
1629
  width: 100%;
1630
  }
1631
 
1632
- .settings-nav-btn:hover {
1633
- background: var(--bg-hover);
1634
- color: var(--text-primary);
1635
- }
1636
 
1637
  .settings-nav-btn.active {
1638
  background: var(--bg-hover);
@@ -1647,13 +1645,8 @@ select {
1647
  min-width: 0;
1648
  }
1649
 
1650
- .tab-content {
1651
- display: none;
1652
- }
1653
-
1654
- .tab-content.active {
1655
- display: block;
1656
- }
1657
 
1658
  .settings-section-title {
1659
  font-size: 14px;
@@ -1662,8 +1655,7 @@ select {
1662
  margin-bottom: 10px;
1663
  }
1664
 
1665
- .settings-input,
1666
- .settings-textarea {
1667
  width: 100%;
1668
  padding: 11px 14px;
1669
  background: var(--bg-input);
@@ -1676,21 +1668,10 @@ select {
1676
  transition: border-color 0.2s;
1677
  }
1678
 
1679
- .settings-input:focus,
1680
- .settings-textarea:focus {
1681
- border-color: var(--brand);
1682
- }
1683
-
1684
- .settings-input::placeholder,
1685
- .settings-textarea::placeholder {
1686
- color: var(--text-muted);
1687
- }
1688
 
1689
- .settings-textarea {
1690
- resize: vertical;
1691
- min-height: 80px;
1692
- line-height: 1.5;
1693
- }
1694
 
1695
  .settings-select {
1696
  width: 100%;
@@ -1711,14 +1692,8 @@ select {
1711
  transition: border-color 0.2s;
1712
  }
1713
 
1714
- .settings-select:focus {
1715
- border-color: var(--brand);
1716
- }
1717
-
1718
- .settings-select option {
1719
- background: var(--bg-input);
1720
- color: var(--text-primary);
1721
- }
1722
 
1723
  .settings-save-btn {
1724
  margin-top: 12px;
@@ -1734,9 +1709,7 @@ select {
1734
  transition: background 0.3s;
1735
  }
1736
 
1737
- .settings-save-btn:hover {
1738
- background: var(--brand-hover);
1739
- }
1740
 
1741
  .settings-hint {
1742
  font-size: 12px;
@@ -1745,50 +1718,39 @@ select {
1745
  line-height: 1.5;
1746
  }
1747
 
1748
- .settings-hint a {
1749
- color: var(--brand);
1750
- text-decoration: none;
1751
- }
1752
 
1753
- .settings-hint a:hover {
1754
- text-decoration: underline;
 
 
1755
  }
1756
 
1757
- .persona-option {
 
1758
  display: flex;
1759
- flex-direction: column;
1760
- gap: 4px;
1761
- padding: 14px 16px;
 
1762
  border-radius: var(--radius);
1763
- border: 1px solid var(--border-color);
 
 
 
 
 
1764
  cursor: pointer;
1765
- margin-bottom: 8px;
1766
- transition: border-color 0.2s, background 0.2s;
1767
  }
1768
 
1769
- .persona-option:hover {
1770
- background: var(--bg-hover);
1771
- }
1772
 
1773
- .persona-option.active {
1774
  border-color: var(--brand);
1775
- background: rgba(235, 90, 40, 0.06);
1776
- }
1777
-
1778
- [data-theme="light"] .persona-option.active {
1779
- background: rgba(66, 133, 244, 0.06);
1780
- }
1781
-
1782
- .persona-name {
1783
- font-size: 14px;
1784
- font-weight: 600;
1785
- color: var(--text-primary);
1786
- }
1787
-
1788
- .persona-desc {
1789
- font-size: 12px;
1790
- color: var(--text-secondary);
1791
- line-height: 1.5;
1792
  }
1793
 
1794
 
@@ -1797,41 +1759,17 @@ select {
1797
  ============================================================ */
1798
 
1799
  @keyframes fadeIn {
1800
- from {
1801
- opacity: 0;
1802
- transform: translateY(8px);
1803
- }
1804
-
1805
- to {
1806
- opacity: 1;
1807
- transform: translateY(0);
1808
- }
1809
- }
1810
-
1811
- ::-webkit-scrollbar {
1812
- width: 5px;
1813
- }
1814
-
1815
- ::-webkit-scrollbar-track {
1816
- background: transparent;
1817
- }
1818
-
1819
- ::-webkit-scrollbar-thumb {
1820
- background: #333;
1821
- border-radius: 4px;
1822
  }
1823
 
1824
- ::-webkit-scrollbar-thumb:hover {
1825
- background: #555;
1826
- }
1827
-
1828
- [data-theme="light"] ::-webkit-scrollbar-thumb {
1829
- background: #ccc;
1830
- }
1831
 
1832
- [data-theme="light"] ::-webkit-scrollbar-thumb:hover {
1833
- background: #aaa;
1834
- }
1835
 
1836
  .cursor::after {
1837
  content: '\25CB';
@@ -1840,11 +1778,7 @@ select {
1840
  margin-left: 2px;
1841
  }
1842
 
1843
- @keyframes blink {
1844
- 50% {
1845
- opacity: 0;
1846
- }
1847
- }
1848
 
1849
 
1850
  /* ============================================================
@@ -1862,13 +1796,9 @@ select {
1862
 
1863
 
1864
  /* ============================================================
1865
- RESPONSIVE MOBILE
1866
  ============================================================ */
1867
 
1868
- .mobile-fab-toggle {
1869
- display: none !important;
1870
- }
1871
-
1872
  .sidebar-overlay {
1873
  display: none;
1874
  position: fixed;
@@ -1878,58 +1808,69 @@ select {
1878
  -webkit-tap-highlight-color: transparent;
1879
  }
1880
 
1881
- .sidebar-overlay.visible {
1882
- display: block;
1883
- }
 
 
 
1884
 
1885
  @media (max-width: 768px) {
 
 
 
 
 
 
 
 
 
1886
  .app-container {
1887
  overflow: hidden;
 
1888
  position: relative;
 
1889
  }
1890
 
1891
- .sidebar {
1892
- position: fixed;
1893
- left: 0;
1894
- top: 0;
1895
- width: var(--sidebar-width);
1896
- height: 100vh;
1897
- height: 100dvh;
1898
- z-index: 40;
1899
- transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.25s cubic-bezier(0.4, 0, 0.2, 1);
1900
- margin-left: 0 !important;
1901
  }
1902
 
1903
- .sidebar.collapsed {
1904
- transform: translateX(calc(-1 * var(--sidebar-width)));
1905
- opacity: 0;
1906
- pointer-events: none;
 
1907
  }
1908
 
1909
- .sidebar:not(.collapsed) {
1910
- transform: translateX(0);
1911
- opacity: 1;
1912
- pointer-events: auto;
1913
- }
1914
 
1915
- .sidebar-rail {
1916
- display: none !important;
 
 
1917
  }
1918
 
1919
- .main-chat {
1920
- width: 100vw;
1921
- min-width: 0;
1922
- flex: 1;
1923
  }
1924
 
1925
- .sidebar-logo {
1926
- height: 36px;
1927
  }
1928
 
1929
- .sidebar-logo-row {
1930
- padding: 20px 18px 10px;
 
1931
  }
1932
 
 
1933
  .settings-modal {
1934
  width: 96vw;
1935
  height: 78vh;
@@ -1963,16 +1904,9 @@ select {
1963
  white-space: normal;
1964
  }
1965
 
1966
- .settings-nav-btn span,
1967
- .settings-nav-btn> :not(svg) {
1968
- display: none;
1969
- }
1970
 
1971
- .settings-nav-btn svg {
1972
- width: 18px;
1973
- height: 18px;
1974
- flex-shrink: 0;
1975
- }
1976
 
1977
  .settings-nav-btn.active {
1978
  background: var(--bg-elevated);
@@ -1985,114 +1919,32 @@ select {
1985
  min-width: 0;
1986
  }
1987
 
1988
- .hero-title {
1989
- font-size: 22px;
1990
- }
1991
-
1992
- .hero-bot-icon {
1993
- width: 80px;
1994
- height: 80px;
1995
- }
1996
 
1997
  .hero-welcome {
1998
  justify-content: center;
1999
- padding: 40px 16px calc(20px + env(safe-area-inset-bottom));
2000
  overflow-y: auto;
2001
  min-height: 0;
2002
  }
2003
 
2004
- .hero-pills {
2005
- grid-template-columns: 1fr;
2006
- }
2007
-
2008
- .hero-pill {
2009
- font-size: 12px;
2010
- padding: 12px 14px;
2011
- }
2012
-
2013
- .hero-input-wrap {
2014
- max-width: 100%;
2015
- }
2016
-
2017
- .login-title {
2018
- font-size: 20px;
2019
- white-space: normal;
2020
- }
2021
-
2022
- .chat-container {
2023
- padding: 60px 10px 16px;
2024
- gap: 16px;
2025
- height: 100%;
2026
- }
2027
-
2028
- .message-row {
2029
- max-width: 100%;
2030
- }
2031
-
2032
- .ai-avatar {
2033
- display: none;
2034
- }
2035
-
2036
- .ai .message-content {
2037
- font-size: 13px;
2038
- line-height: 1.55;
2039
- padding: 0 2px;
2040
- }
2041
-
2042
- .user .message-content {
2043
- max-width: 88%;
2044
- font-size: 13px;
2045
- }
2046
-
2047
- .input-container {
2048
- padding: 10px 10px calc(6px + env(safe-area-inset-bottom));
2049
- }
2050
-
2051
- .disclaimer-text {
2052
- font-size: 10px;
2053
- margin-top: 6px;
2054
- }
2055
-
2056
- .history-item .options-btn {
2057
- opacity: 0.6;
2058
- }
2059
 
2060
- .theme-toggle-btn {
2061
- top: 10px;
2062
- right: 10px;
2063
- width: 32px;
2064
- height: 32px;
2065
- }
2066
 
2067
- .theme-toggle-btn svg {
2068
- width: 16px;
2069
- height: 16px;
 
2070
  }
2071
  }
2072
 
2073
  @media (max-width: 480px) {
2074
- .hero-title {
2075
- font-size: 20px;
2076
- }
2077
-
2078
- .hero-input-box textarea {
2079
- font-size: 13px;
2080
- }
2081
-
2082
- .ai .message-content {
2083
- font-size: 12.5px;
2084
- }
2085
-
2086
- .user .message-content {
2087
- font-size: 12.5px;
2088
- }
2089
-
2090
- .katex-display {
2091
- overflow-x: auto;
2092
- font-size: 0.95em;
2093
- }
2094
-
2095
- .message-content pre {
2096
- font-size: 11px;
2097
- }
2098
  }
 
1
  /* ============================================================
2
  STEM Copilot — Stylesheet
3
  Montserrat, dark/light theme, premium chat UI
4
+ Theme: Dark=#EC642B, Light=#2C2872
5
  ============================================================ */
6
 
7
+ /* ── CSS Variables ─────────────────────────────────────────── */
8
+
9
  :root {
10
+ --brand: #EC642B;
11
+ --brand-hover: #d4561f;
12
+ --brand-glow: rgba(236, 100, 43, 0.25);
13
  --bg-main: #000000;
14
  --bg-sidebar: #0a0a0a;
15
  --bg-input: #1a1a1a;
 
29
  --transition: 0.35s cubic-bezier(0.4, 0, 0.2, 1);
30
  --sidebar-width: 280px;
31
  --rail-width: 56px;
32
+ --topbar-height: 52px;
33
  }
34
 
35
+ /* ── Light Mode ── */
36
  [data-theme="light"] {
37
+ --brand: #2C2872;
38
+ --brand-hover: #221f5e;
39
+ --brand-glow: rgba(44, 40, 114, 0.25);
40
  --bg-main: #f8f9fa;
41
  --bg-sidebar: #ffffff;
42
  --bg-input: #ffffff;
 
52
  --msg-ai: transparent;
53
  }
54
 
55
+
56
+ /* ── Reset & Base ─────────────────────────────────────────── */
57
+
58
  * {
59
  box-sizing: border-box;
60
  margin: 0;
 
66
  -ms-text-size-adjust: 100%;
67
  touch-action: manipulation;
68
  overscroll-behavior: none;
69
+ overflow-x: hidden;
70
  }
71
 
72
  body {
 
76
  height: 100vh;
77
  height: 100dvh;
78
  overflow: hidden;
79
+ overflow-x: hidden;
80
  touch-action: manipulation;
81
  -webkit-tap-highlight-color: transparent;
82
  overscroll-behavior: none;
 
83
  transition: background-color 0.35s ease, color 0.35s ease;
84
  }
85
 
86
+ /* Smooth theme transitions */
87
+ *, *::before, *::after {
 
 
88
  transition: background-color 0.35s ease, color 0.35s ease, border-color 0.35s ease, box-shadow 0.35s ease, fill 0.35s ease, stroke 0.35s ease;
89
  }
90
 
91
+ textarea, input, select {
 
 
92
  transition: background-color 0.35s ease, color 0.35s ease, border-color 0.35s ease, box-shadow 0.35s ease;
93
  }
94
 
95
 
96
  /* ============================================================
97
+ LOGIN SCREEN
98
  ============================================================ */
99
 
100
+ .login-screen {
101
+ position: fixed;
102
+ inset: 0;
103
+ z-index: 1000;
104
+ background: var(--bg-main);
105
+ display: flex;
106
+ align-items: center;
107
+ justify-content: center;
108
+ }
109
+
110
+ .login-top-actions {
111
  position: fixed;
112
  top: 14px;
113
  right: 16px;
114
  z-index: 1100;
115
+ display: flex;
116
+ gap: 8px;
117
+ }
118
+
119
+ .login-theme-btn {
120
  width: 36px;
121
  height: 36px;
122
  border-radius: 50%;
123
+ border: none;
124
+ background: transparent;
125
  cursor: pointer;
126
  display: flex;
127
  align-items: center;
128
  justify-content: center;
129
+ transition: transform 0.2s;
130
  }
131
 
132
+ .login-theme-btn:hover {
133
+ transform: scale(1.1);
 
 
134
  }
135
 
136
+ .login-theme-btn svg {
137
+ width: 20px;
138
+ height: 20px;
139
  stroke: var(--text-secondary);
140
  fill: none;
141
  stroke-width: 2;
142
  transition: stroke 0.3s;
143
  }
144
 
145
+ .login-theme-btn:hover svg {
146
  stroke: var(--brand);
147
  }
148
 
149
+ .login-theme-btn .icon-sun { display: block; }
150
+ .login-theme-btn .icon-moon { display: none; }
151
+ [data-theme="light"] .login-theme-btn .icon-sun { display: none; }
152
+ [data-theme="light"] .login-theme-btn .icon-moon { display: block; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
 
154
  .login-card {
155
  text-align: center;
 
259
  color: var(--text-secondary);
260
  }
261
 
262
+ .byok-steps a { color: var(--brand); text-decoration: none; }
263
+ .byok-steps a:hover { text-decoration: underline; }
 
 
 
 
 
 
264
 
265
  .byok-submit-btn {
266
  width: 100%;
 
277
  transition: background 0.3s;
278
  }
279
 
280
+ .byok-submit-btn:hover { background: var(--brand-hover); }
 
 
281
 
282
  .byok-note {
283
  font-size: 11px;
 
309
  animation: slideUp 0.4s cubic-bezier(0.16, 1, 0.3, 1);
310
  }
311
 
312
+ .install-banner-icon { width: 36px; height: 36px; border-radius: 8px; }
 
 
 
 
313
 
314
  .install-banner-text {
315
  flex: 1;
 
319
  line-height: 1.4;
320
  }
321
 
322
+ .install-banner-text strong { color: var(--text-primary); }
323
+ .install-banner-text span { color: var(--text-secondary); font-size: 11px; }
 
 
 
 
 
 
324
 
325
  .install-banner-btn {
326
  background: var(--brand);
 
335
  transition: background 0.3s;
336
  }
337
 
338
+ .install-banner-btn:hover { background: var(--brand-hover); }
339
+
340
+ .install-banner-dismiss {
341
+ background: none;
342
+ border: none;
343
+ color: var(--text-muted);
344
+ font-size: 18px;
345
+ cursor: pointer;
346
+ padding: 0 4px;
347
+ }
348
+
349
+ @keyframes slideUp {
350
+ from { opacity: 0; transform: translateX(-50%) translateY(20px); }
351
+ to { opacity: 1; transform: translateX(-50%) translateY(0); }
352
+ }
353
+
354
+
355
+ /* ============================================================
356
+ MOBILE TOP NAV BAR
357
+ ============================================================ */
358
+
359
+ .mobile-topbar {
360
+ display: none; /* shown via media query */
361
+ position: fixed;
362
+ top: 0;
363
+ left: 0;
364
+ right: 0;
365
+ height: var(--topbar-height);
366
+ background: var(--bg-sidebar);
367
+ border-bottom: 1px solid var(--border-color);
368
+ z-index: 100;
369
+ align-items: center;
370
+ justify-content: space-between;
371
+ padding: 0 12px;
372
+ }
373
+
374
+ .topbar-profile-btn {
375
+ background: none;
376
+ border: none;
377
+ cursor: pointer;
378
+ padding: 4px;
379
+ display: flex;
380
+ align-items: center;
381
+ }
382
+
383
+ .topbar-avatar {
384
+ width: 32px;
385
+ height: 32px;
386
+ border-radius: 50%;
387
+ object-fit: cover;
388
+ border: 2px solid var(--border-color);
389
+ transition: border-color 0.2s;
390
+ }
391
+
392
+ .topbar-profile-btn:hover .topbar-avatar,
393
+ .topbar-profile-btn:active .topbar-avatar {
394
+ border-color: var(--brand);
395
+ }
396
+
397
+ .topbar-logo {
398
+ height: 28px;
399
+ object-fit: contain;
400
+ position: absolute;
401
+ left: 50%;
402
+ transform: translateX(-50%);
403
+ }
404
+
405
+ .topbar-actions {
406
+ display: flex;
407
+ gap: 4px;
408
+ }
409
+
410
+ .topbar-icon-btn {
411
+ width: 36px;
412
+ height: 36px;
413
+ border-radius: 10px;
414
+ background: transparent;
415
+ border: none;
416
+ color: var(--text-secondary);
417
+ cursor: pointer;
418
+ display: flex;
419
+ align-items: center;
420
+ justify-content: center;
421
+ transition: background 0.2s, color 0.2s;
422
+ }
423
+
424
+ .topbar-icon-btn:hover,
425
+ .topbar-icon-btn:active {
426
+ background: var(--bg-hover);
427
+ color: var(--brand);
428
+ }
429
+
430
+
431
+ /* ============================================================
432
+ FULL-SCREEN OVERLAYS (Account & History — Mobile)
433
+ ============================================================ */
434
+
435
+ .fullscreen-overlay {
436
+ position: fixed;
437
+ inset: 0;
438
+ z-index: 500;
439
+ background: var(--bg-main);
440
+ display: none;
441
+ flex-direction: column;
442
+ animation: overlaySlideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1);
443
+ }
444
+
445
+ .fullscreen-overlay.open {
446
+ display: flex;
447
+ }
448
+
449
+ @keyframes overlaySlideIn {
450
+ from { opacity: 0; transform: translateY(20px); }
451
+ to { opacity: 1; transform: translateY(0); }
452
+ }
453
+
454
+ .overlay-header {
455
+ display: flex;
456
+ justify-content: space-between;
457
+ align-items: center;
458
+ padding: 16px 20px;
459
+ border-bottom: 1px solid var(--border-color);
460
+ flex-shrink: 0;
461
+ }
462
+
463
+ .overlay-header h2 {
464
+ font-size: 18px;
465
+ font-weight: 600;
466
+ }
467
+
468
+ .overlay-close-btn {
469
+ width: 36px;
470
+ height: 36px;
471
+ border-radius: 50%;
472
+ border: none;
473
+ background: var(--bg-hover);
474
+ color: var(--text-primary);
475
+ font-size: 22px;
476
+ cursor: pointer;
477
+ display: flex;
478
+ align-items: center;
479
+ justify-content: center;
480
+ transition: background 0.2s, color 0.2s;
481
+ }
482
+
483
+ .overlay-close-btn:hover {
484
+ background: var(--brand);
485
+ color: #fff;
486
+ }
487
+
488
+ .overlay-body {
489
+ flex: 1;
490
+ overflow-y: auto;
491
+ padding: 20px;
492
+ }
493
+
494
+ /* Account overlay */
495
+ .overlay-user-info {
496
+ display: flex;
497
+ align-items: center;
498
+ gap: 14px;
499
+ padding: 16px;
500
+ background: var(--bg-card);
501
+ border-radius: var(--radius);
502
+ border: 1px solid var(--border-color);
503
+ margin-bottom: 20px;
504
+ }
505
+
506
+ .overlay-avatar {
507
+ width: 48px;
508
+ height: 48px;
509
+ border-radius: 50%;
510
+ object-fit: cover;
511
+ border: 2px solid var(--brand);
512
+ }
513
+
514
+ .overlay-user-details {
515
+ display: flex;
516
+ flex-direction: column;
517
+ gap: 2px;
518
+ }
519
+
520
+ .overlay-user-name {
521
+ font-size: 15px;
522
+ font-weight: 600;
523
+ color: var(--text-primary);
524
+ }
525
+
526
+ .overlay-user-email {
527
+ font-size: 12px;
528
+ color: var(--text-secondary);
529
+ }
530
+
531
+ .overlay-menu {
532
+ display: flex;
533
+ flex-direction: column;
534
+ gap: 4px;
535
+ }
536
+
537
+ .overlay-menu-item {
538
+ display: flex;
539
+ align-items: center;
540
+ gap: 14px;
541
+ padding: 14px 16px;
542
+ border-radius: var(--radius);
543
+ border: none;
544
+ background: none;
545
+ color: var(--text-primary);
546
+ font-family: inherit;
547
+ font-size: 14px;
548
+ font-weight: 500;
549
+ cursor: pointer;
550
+ width: 100%;
551
+ text-align: left;
552
+ transition: background 0.2s, color 0.2s;
553
+ }
554
+
555
+ .overlay-menu-item:hover,
556
+ .overlay-menu-item:active {
557
+ background: var(--bg-hover);
558
+ color: var(--brand);
559
+ }
560
+
561
+ .overlay-menu-item svg {
562
+ opacity: 0.6;
563
+ flex-shrink: 0;
564
+ }
565
+
566
+ .overlay-menu-item:hover svg { opacity: 1; }
567
+
568
+ .overlay-menu-logout {
569
+ color: #ff4a4a;
570
+ border-top: 1px solid var(--border-subtle);
571
+ margin-top: 8px;
572
+ padding-top: 16px;
573
+ border-radius: 0 0 var(--radius) var(--radius);
574
+ }
575
+
576
+ .overlay-menu-logout:hover { color: #ff4a4a; background: rgba(255, 74, 74, 0.06); }
577
+
578
+ /* History overlay */
579
+ .overlay-history-list {
580
+ display: flex;
581
+ flex-direction: column;
582
+ gap: 4px;
583
+ }
584
+
585
+ .overlay-history-item {
586
+ padding: 12px 14px;
587
+ border-radius: 10px;
588
+ cursor: pointer;
589
+ color: var(--text-secondary);
590
+ font-size: 14px;
591
+ display: flex;
592
+ align-items: center;
593
+ gap: 10px;
594
+ transition: background 0.2s, color 0.2s;
595
+ border: none;
596
+ background: none;
597
+ width: 100%;
598
+ text-align: left;
599
+ font-family: inherit;
600
+ }
601
+
602
+ .overlay-history-item:hover,
603
+ .overlay-history-item:active {
604
+ background: var(--bg-hover);
605
+ color: var(--text-primary);
606
  }
607
 
608
+ .overlay-history-item.active {
609
+ background: rgba(236, 100, 43, 0.08);
610
+ color: var(--text-primary);
 
 
 
 
611
  }
612
 
613
+ [data-theme="light"] .overlay-history-item.active {
614
+ background: rgba(44, 40, 114, 0.08);
615
+ }
 
 
616
 
617
+ .overlay-history-item svg {
618
+ flex-shrink: 0;
619
+ opacity: 0.4;
620
+ }
621
+
622
+ .overlay-history-item.active svg {
623
+ opacity: 0.8;
624
+ color: var(--brand);
625
+ }
626
+
627
+ .overlay-history-title {
628
+ flex: 1;
629
+ white-space: nowrap;
630
+ overflow: hidden;
631
+ text-overflow: ellipsis;
632
+ }
633
+
634
+ .overlay-empty-state {
635
+ display: flex;
636
+ flex-direction: column;
637
+ align-items: center;
638
+ gap: 12px;
639
+ padding: 60px 20px;
640
+ color: var(--text-muted);
641
+ font-size: 14px;
642
  }
643
 
644
 
 
651
  height: 100vh;
652
  height: 100dvh;
653
  overflow: hidden;
654
+ overflow-x: hidden;
655
  }
656
 
657
 
658
  /* ============================================================
659
+ SIDEBAR (desktop only)
660
  ============================================================ */
661
 
662
  .sidebar {
 
695
  object-fit: contain;
696
  }
697
 
698
+ /* New chat btn — no orange gradient */
699
  .new-chat-btn {
700
  margin: 8px 14px;
701
  padding: 12px 16px;
702
+ background: var(--bg-hover);
703
+ border: 1px solid var(--border-color);
704
  color: var(--text-primary);
705
  border-radius: var(--radius);
706
  cursor: pointer;
 
714
  }
715
 
716
  .new-chat-btn:hover {
717
+ background: var(--bg-elevated);
718
+ border-color: var(--brand);
719
+ box-shadow: 0 0 12px var(--brand-glow);
720
  }
721
 
722
  .new-chat-btn svg {
 
745
  transition: background 0.2s, color 0.2s, transform 0.15s;
746
  }
747
 
748
+ .history-item:hover { background: var(--bg-hover); color: var(--text-primary); }
749
+ .history-item:active { transform: scale(0.98); }
 
 
 
 
 
 
750
 
751
  .history-item.active {
752
+ background: var(--bg-hover);
753
  color: var(--text-primary);
754
+ border: 1px solid var(--border-color);
 
 
 
 
755
  }
756
 
757
+ .history-item:not(.active) { border: 1px solid transparent; }
 
 
 
 
 
758
 
759
+ .thread-icon { flex-shrink: 0; opacity: 0.4; width: 16px; height: 16px; }
760
+ .history-item.active .thread-icon { opacity: 0.8; color: var(--brand); }
 
 
761
 
762
  .chat-title {
763
  flex: 1;
 
779
  transition: opacity 0.2s, color 0.2s;
780
  }
781
 
782
+ .history-item:hover .options-btn, .options-btn.menu-open { opacity: 1; }
783
+ .options-btn:hover { color: var(--text-primary); }
 
 
 
 
 
 
784
 
785
  .options-menu {
786
  position: absolute;
 
797
  overflow: hidden;
798
  }
799
 
800
+ .options-menu.show { display: flex; }
 
 
801
 
802
  .option-item {
803
  padding: 10px 14px;
 
810
  transition: background 0.2s;
811
  }
812
 
813
+ .option-item:hover { background: var(--bg-hover); }
814
+ .option-item.delete { color: #ff4a4a; }
 
 
 
 
 
815
 
816
  .rename-input {
817
  width: 100%;
 
825
  padding: 2px 0;
826
  }
827
 
828
+ /* Sidebar bottom */
 
 
829
  .sidebar-bottom {
830
  padding: 12px 14px 16px;
831
  border-top: 1px solid var(--border-color);
832
  position: relative;
 
833
  }
834
 
835
  .user-profile-btn {
 
859
  transition: border-color 0.2s;
860
  }
861
 
862
+ .user-profile-btn:hover .user-avatar { border-color: var(--brand); }
 
 
863
 
864
  .user-name {
865
  flex: 1;
 
885
  backdrop-filter: blur(12px);
886
  }
887
 
888
+ .user-menu.show { display: block; animation: fadeIn 0.15s ease-out; }
 
 
 
889
 
890
  .user-menu-item {
891
  padding: 12px 18px;
 
899
  color: var(--text-primary);
900
  }
901
 
902
+ .user-menu-item:hover { background: var(--bg-hover); color: var(--brand); }
903
+ .user-menu-item svg { opacity: 0.6; transition: opacity 0.2s; }
904
+ .user-menu-item:hover svg { opacity: 1; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
905
 
906
+ .user-menu-logout { color: #ff4a4a; border-top: 1px solid var(--border-subtle); }
907
+ .user-menu-logout:hover { color: #ff4a4a; background: rgba(255, 74, 74, 0.06); }
 
 
908
 
909
 
910
  /* ============================================================
911
+ SIDEBAR RAIL (collapsed — desktop only)
912
  ============================================================ */
913
 
914
  .sidebar-rail {
 
924
  z-index: 20;
925
  }
926
 
927
+ .sidebar-rail.visible { display: flex; }
 
 
928
 
929
+ .rail-top, .rail-bottom {
 
930
  display: flex;
931
  flex-direction: column;
932
  align-items: center;
 
947
  transition: background 0.2s, color 0.2s;
948
  }
949
 
950
+ .rail-icon-btn:hover { background: var(--bg-hover); color: var(--text-primary); }
 
 
 
 
 
 
 
 
 
 
951
 
952
+ .rail-logo { width: 34px; height: 34px; border-radius: 6px; object-fit: contain; }
953
+ .rail-avatar { width: 28px; height: 28px; border-radius: 50%; object-fit: cover; background: var(--bg-hover); }
 
 
 
 
 
954
 
955
 
956
  /* ============================================================
 
989
  transition: background 0.2s, color 0.2s;
990
  }
991
 
992
+ .toggle-sidebar-btn:hover { background: var(--bg-hover); color: var(--text-primary); }
 
 
 
993
 
994
  .chat-container {
995
  flex: 1;
996
  overflow-y: auto;
997
+ overflow-x: hidden;
998
  padding: 64px 20px 20px;
999
  display: flex;
1000
  flex-direction: column;
 
1022
  width: 120px;
1023
  height: 120px;
1024
  object-fit: contain;
1025
+ filter: drop-shadow(0 8px 24px var(--brand-glow));
 
1026
  }
1027
 
1028
  .hero-title {
 
1033
  line-height: 1.3;
1034
  }
1035
 
1036
+ .hero-title .hero-name { color: var(--brand); }
 
 
1037
 
1038
  .hero-input-wrap {
1039
  width: 100%;
 
1052
  border-radius: var(--radius);
1053
  }
1054
 
1055
+ .hero-image-preview.visible { display: flex; }
 
 
1056
 
1057
  .hero-image-preview-thumb {
1058
  width: 48px;
 
1073
  transition: color 0.2s;
1074
  }
1075
 
1076
+ .hero-image-preview-remove:hover { color: #ff4a4a; }
 
 
1077
 
1078
  .hero-input-box {
1079
  display: flex;
 
1104
  max-height: 120px;
1105
  line-height: 1.5;
1106
  overflow-y: hidden;
1107
+ -ms-overflow-style: none;
1108
+ scrollbar-width: none;
1109
  }
1110
 
1111
+ .hero-input-box textarea::-webkit-scrollbar { display: none; }
1112
+ .hero-input-box textarea::placeholder { color: var(--text-muted); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1113
 
1114
 
1115
  /* ============================================================
 
1124
  animation: fadeIn 0.35s ease-out forwards;
1125
  }
1126
 
1127
+ .message-row.user { justify-content: flex-end; }
1128
+ .message-row.ai { justify-content: flex-start; }
 
 
 
 
 
1129
 
1130
  .message-content {
1131
  padding: 14px 18px;
 
1143
  color: var(--text-primary);
1144
  }
1145
 
1146
+ .ai .message-content { max-width: 100%; background: var(--msg-ai); }
 
 
 
1147
 
1148
  .ai-avatar {
1149
  width: 30px;
 
1157
  margin-top: 4px;
1158
  }
1159
 
1160
+ .ai-avatar.pulsing { animation: avatarPulse 1.5s ease-in-out infinite; }
 
 
1161
 
1162
  @keyframes avatarPulse {
1163
+ 0%, 100% { opacity: 0.6; transform: scale(1); }
1164
+ 50% { opacity: 1; transform: scale(1.05); }
 
 
 
 
 
 
 
 
 
1165
  }
1166
 
1167
+ /* Thinking indicator */
 
 
1168
  .thinking-indicator {
1169
  display: flex;
1170
  align-items: center;
 
1173
  animation: fadeIn 0.3s ease-out;
1174
  }
1175
 
1176
+ .thinking-indicator.hidden { display: none; }
 
 
 
 
 
 
 
 
 
1177
 
1178
+ .thinking-text { font-size: 13px; color: var(--text-secondary); font-style: italic; }
1179
 
1180
+ /* Rendered markdown */
1181
+ .message-content h1, .message-content h2, .message-content h3 {
 
1182
  margin-top: 16px;
1183
  margin-bottom: 8px;
1184
  font-weight: 600;
1185
  }
1186
 
1187
+ .message-content h1 { font-size: 1.25em; }
1188
+ .message-content h2 { font-size: 1.12em; }
1189
+ .message-content h3 { font-size: 1.05em; }
1190
+ .message-content p { margin-bottom: 10px; }
1191
+ .message-content ul, .message-content ol { margin: 8px 0 8px 20px; }
1192
+ .message-content li { margin-bottom: 4px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1193
 
1194
  .message-content code {
1195
  background: rgba(0, 0, 0, 0.15);
 
1199
  font-size: 0.88em;
1200
  }
1201
 
1202
+ [data-theme="light"] .message-content code { background: rgba(0, 0, 0, 0.06); }
 
 
1203
 
1204
  .message-content pre {
1205
  background: #111;
 
1210
  margin: 10px 0;
1211
  }
1212
 
1213
+ [data-theme="light"] .message-content pre { background: #f5f5f5; }
1214
+ .message-content pre code { background: none; padding: 0; }
 
 
 
 
 
 
 
 
 
 
 
 
1215
 
1216
+ .message-content table { border-collapse: collapse; margin: 10px 0; width: 100%; }
1217
+ .message-content th, .message-content td {
1218
  border: 1px solid var(--border-color);
1219
  padding: 8px 12px;
1220
  text-align: left;
1221
  font-size: 14px;
1222
  }
1223
 
1224
+ .message-content th { background: var(--bg-input); font-weight: 600; }
 
 
 
1225
 
1226
  .message-content blockquote {
1227
  border-left: 3px solid var(--brand);
 
1230
  color: var(--text-secondary);
1231
  }
1232
 
1233
+ .katex { color: var(--text-primary); }
1234
+ .katex-display { margin: 14px 0; overflow-x: auto; }
1235
+ .message-image { max-width: 240px; border-radius: 8px; margin-bottom: 8px; }
 
 
 
 
 
 
 
 
 
 
 
1236
 
1237
 
1238
  /* ============================================================
1239
+ AI MESSAGE ACTION BAR
1240
  ============================================================ */
1241
 
1242
  .msg-actions {
 
1248
  transition: opacity 0.2s;
1249
  }
1250
 
1251
+ .message-row.ai:hover .msg-actions, .msg-actions.visible { opacity: 1; }
 
 
 
1252
 
1253
  .msg-action-btn {
1254
  background: var(--bg-card);
 
1272
  border-color: var(--brand);
1273
  }
1274
 
1275
+ .msg-action-btn svg { width: 13px; height: 13px; stroke: currentColor; fill: none; stroke-width: 2; }
 
 
 
 
 
 
1276
 
1277
 
1278
  /* ============================================================
 
1320
  transition: color 0.2s;
1321
  }
1322
 
1323
+ .image-preview-remove:hover { color: #ff4a4a; }
 
 
1324
 
1325
  .input-box {
1326
  width: 100%;
 
1351
  transition: color 0.2s;
1352
  }
1353
 
1354
+ .upload-btn:hover { color: var(--text-primary); }
 
 
1355
 
1356
+ /* Teaching style selector */
1357
  .style-selector-wrap {
1358
  position: relative;
1359
  display: flex;
1360
  align-items: center;
 
1361
  }
1362
 
1363
  .style-selector-btn {
 
1393
  transition: transform 0.2s;
1394
  }
1395
 
1396
+ .style-selector-btn.open svg { transform: rotate(180deg); }
 
 
1397
 
1398
  .style-dropdown {
1399
  display: none;
 
1406
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
1407
  z-index: 100;
1408
  min-width: 240px;
1409
+ max-height: 280px;
1410
+ overflow-y: auto;
1411
  animation: fadeIn 0.15s ease-out;
1412
  }
1413
 
1414
+ .style-dropdown.show { display: block; }
1415
+
1416
+ .style-option {
1417
+ padding: 10px 14px;
1418
+ cursor: pointer;
1419
+ transition: background 0.15s;
1420
+ border-bottom: 1px solid var(--border-subtle);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1421
  }
1422
 
1423
+ .style-option:last-child { border-bottom: none; }
1424
+ .style-option:hover { background: var(--bg-hover); }
1425
+
1426
+ .style-option.active { background: var(--brand-glow); }
1427
+
1428
+ .style-option-name {
1429
+ font-size: 13px;
1430
+ font-weight: 600;
1431
  color: var(--text-primary);
1432
+ margin-bottom: 2px;
 
 
 
 
 
 
 
 
1433
  }
1434
 
1435
+ .style-option.active .style-option-name { color: var(--brand); }
1436
+
1437
+ .style-option-desc {
1438
+ font-size: 11px;
1439
  color: var(--text-muted);
1440
+ line-height: 1.4;
1441
  }
1442
 
1443
+
1444
+ /* ============================================================
1445
+ ADAPTIVE ACTION BUTTON (mic ↔ send)
1446
+ ============================================================ */
1447
+
1448
+ .adaptive-action-btn {
1449
  background: none;
1450
  border: none;
1451
+ color: var(--brand);
1452
  cursor: pointer;
1453
  padding: 16px 16px 16px 4px;
1454
  display: flex;
 
1456
  transition: color 0.2s, transform 0.1s;
1457
  }
1458
 
1459
+ .adaptive-action-btn:hover { color: var(--brand-hover); }
1460
+ .adaptive-action-btn:active { transform: scale(0.92); }
1461
+
1462
+ .adaptive-action-btn svg {
1463
+ width: 20px;
1464
+ height: 20px;
1465
+ transition: opacity 0.2s, transform 0.2s;
1466
  }
1467
 
1468
+ .adaptive-action-btn .icon-mic {
1469
+ stroke: currentColor;
1470
+ fill: none;
1471
+ stroke-width: 2;
1472
  }
1473
 
1474
+ .adaptive-action-btn .icon-send {
 
 
1475
  fill: currentColor;
1476
+ display: none;
1477
+ }
1478
+
1479
+ /* When text is present */
1480
+ .adaptive-action-btn.has-text .icon-mic { display: none; }
1481
+ .adaptive-action-btn.has-text .icon-send { display: block; }
1482
+
1483
+ /* Listening state */
1484
+ .adaptive-action-btn.listening {
1485
+ color: #EA4335 !important;
1486
+ animation: micPulse 1.5s infinite ease-in-out;
1487
+ }
1488
+
1489
+ @keyframes micPulse {
1490
+ 0%, 100% { transform: scale(1); opacity: 1; }
1491
+ 50% { transform: scale(1.15); opacity: 0.8; }
1492
  }
1493
 
1494
+ /* Stop button */
1495
  .stop-btn {
1496
  background: none;
1497
  border: none;
 
1503
  transition: color 0.2s, transform 0.1s;
1504
  }
1505
 
1506
+ .stop-btn.visible { display: flex; }
1507
+ .stop-btn:hover { color: #ff4a4a; }
1508
+ .stop-btn:active { transform: scale(0.92); }
1509
+ .stop-btn svg { width: 22px; height: 22px; fill: currentColor; }
 
 
 
1510
 
1511
+ /* User input textarea */
1512
+ #userInput {
1513
+ flex: 1;
1514
+ background: transparent;
1515
+ border: none;
1516
+ color: var(--text-primary);
1517
+ padding: 16px 12px 16px 8px;
1518
+ font-family: inherit;
1519
+ font-size: 14px;
1520
+ resize: none;
1521
+ outline: none;
1522
+ height: 54px;
1523
+ max-height: 200px;
1524
+ line-height: 1.5;
1525
+ overflow-y: hidden;
1526
+ -ms-overflow-style: none;
1527
+ scrollbar-width: none;
1528
  }
1529
 
1530
+ #userInput::-webkit-scrollbar { display: none; }
1531
+ #userInput::placeholder { color: var(--text-muted); }
 
 
 
1532
 
1533
  .disclaimer-text {
1534
  font-size: 11px;
 
1554
  justify-content: center;
1555
  }
1556
 
1557
+ .modal-overlay.show { display: flex; }
 
 
1558
 
1559
  .settings-modal {
1560
  background: var(--bg-sidebar);
 
1579
  flex-shrink: 0;
1580
  }
1581
 
1582
+ .modal-header h2 { font-size: 18px; font-weight: 600; }
 
 
 
1583
 
1584
  .modal-close {
1585
  background: none;
 
1591
  transition: color 0.2s;
1592
  }
1593
 
1594
+ .modal-close:hover { color: var(--text-primary); }
 
 
1595
 
1596
+ .modal-body { padding: 0; overflow: hidden; }
 
 
 
1597
 
1598
  .settings-layout {
1599
  display: flex;
 
1630
  width: 100%;
1631
  }
1632
 
1633
+ .settings-nav-btn:hover { background: var(--bg-hover); color: var(--text-primary); }
 
 
 
1634
 
1635
  .settings-nav-btn.active {
1636
  background: var(--bg-hover);
 
1645
  min-width: 0;
1646
  }
1647
 
1648
+ .tab-content { display: none; }
1649
+ .tab-content.active { display: block; }
 
 
 
 
 
1650
 
1651
  .settings-section-title {
1652
  font-size: 14px;
 
1655
  margin-bottom: 10px;
1656
  }
1657
 
1658
+ .settings-input, .settings-textarea {
 
1659
  width: 100%;
1660
  padding: 11px 14px;
1661
  background: var(--bg-input);
 
1668
  transition: border-color 0.2s;
1669
  }
1670
 
1671
+ .settings-input:focus, .settings-textarea:focus { border-color: var(--brand); }
1672
+ .settings-input::placeholder, .settings-textarea::placeholder { color: var(--text-muted); }
 
 
 
 
 
 
 
1673
 
1674
+ .settings-textarea { resize: vertical; min-height: 80px; line-height: 1.5; }
 
 
 
 
1675
 
1676
  .settings-select {
1677
  width: 100%;
 
1692
  transition: border-color 0.2s;
1693
  }
1694
 
1695
+ .settings-select:focus { border-color: var(--brand); }
1696
+ .settings-select option { background: var(--bg-input); color: var(--text-primary); }
 
 
 
 
 
 
1697
 
1698
  .settings-save-btn {
1699
  margin-top: 12px;
 
1709
  transition: background 0.3s;
1710
  }
1711
 
1712
+ .settings-save-btn:hover { background: var(--brand-hover); }
 
 
1713
 
1714
  .settings-hint {
1715
  font-size: 12px;
 
1718
  line-height: 1.5;
1719
  }
1720
 
1721
+ .settings-hint a { color: var(--brand); text-decoration: none; }
1722
+ .settings-hint a:hover { text-decoration: underline; }
 
 
1723
 
1724
+ /* Appearance tab — theme selector */
1725
+ .theme-option-group {
1726
+ display: flex;
1727
+ gap: 10px;
1728
  }
1729
 
1730
+ .theme-option-btn {
1731
+ flex: 1;
1732
  display: flex;
1733
+ align-items: center;
1734
+ justify-content: center;
1735
+ gap: 8px;
1736
+ padding: 14px;
1737
  border-radius: var(--radius);
1738
+ border: 2px solid var(--border-color);
1739
+ background: var(--bg-card);
1740
+ color: var(--text-secondary);
1741
+ font-family: inherit;
1742
+ font-size: 14px;
1743
+ font-weight: 600;
1744
  cursor: pointer;
1745
+ transition: border-color 0.2s, background 0.2s, color 0.2s;
 
1746
  }
1747
 
1748
+ .theme-option-btn:hover { border-color: var(--text-muted); }
 
 
1749
 
1750
+ .theme-option-btn.active {
1751
  border-color: var(--brand);
1752
+ color: var(--brand);
1753
+ background: var(--brand-glow);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1754
  }
1755
 
1756
 
 
1759
  ============================================================ */
1760
 
1761
  @keyframes fadeIn {
1762
+ from { opacity: 0; transform: translateY(8px); }
1763
+ to { opacity: 1; transform: translateY(0); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1764
  }
1765
 
1766
+ ::-webkit-scrollbar { width: 5px; }
1767
+ ::-webkit-scrollbar-track { background: transparent; }
1768
+ ::-webkit-scrollbar-thumb { background: #333; border-radius: 4px; }
1769
+ ::-webkit-scrollbar-thumb:hover { background: #555; }
 
 
 
1770
 
1771
+ [data-theme="light"] ::-webkit-scrollbar-thumb { background: #ccc; }
1772
+ [data-theme="light"] ::-webkit-scrollbar-thumb:hover { background: #aaa; }
 
1773
 
1774
  .cursor::after {
1775
  content: '\25CB';
 
1778
  margin-left: 2px;
1779
  }
1780
 
1781
+ @keyframes blink { 50% { opacity: 0; } }
 
 
 
 
1782
 
1783
 
1784
  /* ============================================================
 
1796
 
1797
 
1798
  /* ============================================================
1799
+ SIDEBAR OVERLAY (mobile edge-swipe)
1800
  ============================================================ */
1801
 
 
 
 
 
1802
  .sidebar-overlay {
1803
  display: none;
1804
  position: fixed;
 
1808
  -webkit-tap-highlight-color: transparent;
1809
  }
1810
 
1811
+ .sidebar-overlay.visible { display: block; }
1812
+
1813
+
1814
+ /* ============================================================
1815
+ RESPONSIVE — MOBILE (≤768px)
1816
+ ============================================================ */
1817
 
1818
  @media (max-width: 768px) {
1819
+
1820
+ /* Show mobile top bar */
1821
+ .mobile-topbar { display: flex; }
1822
+
1823
+ /* Hide desktop sidebar elements */
1824
+ .sidebar { display: none !important; }
1825
+ .sidebar-rail { display: none !important; }
1826
+ .main-header { display: none !important; }
1827
+
1828
  .app-container {
1829
  overflow: hidden;
1830
+ overflow-x: hidden;
1831
  position: relative;
1832
+ flex-direction: column;
1833
  }
1834
 
1835
+ .main-chat {
1836
+ width: 100vw;
1837
+ max-width: 100vw;
1838
+ min-width: 0;
1839
+ flex: 1;
1840
+ overflow-x: hidden;
 
 
 
 
1841
  }
1842
 
1843
+ .chat-container {
1844
+ padding: calc(var(--topbar-height) + 10px) 10px 16px;
1845
+ gap: 16px;
1846
+ height: 100%;
1847
+ overflow-x: hidden;
1848
  }
1849
 
1850
+ .message-row { max-width: 100%; }
1851
+ .ai-avatar { display: none; }
 
 
 
1852
 
1853
+ .ai .message-content {
1854
+ font-size: 13px;
1855
+ line-height: 1.55;
1856
+ padding: 0 2px;
1857
  }
1858
 
1859
+ .user .message-content {
1860
+ max-width: 88%;
1861
+ font-size: 13px;
 
1862
  }
1863
 
1864
+ .input-container {
1865
+ padding: 10px 10px calc(6px + env(safe-area-inset-bottom));
1866
  }
1867
 
1868
+ .disclaimer-text {
1869
+ font-size: 10px;
1870
+ margin-top: 6px;
1871
  }
1872
 
1873
+ /* Settings modal responsive */
1874
  .settings-modal {
1875
  width: 96vw;
1876
  height: 78vh;
 
1904
  white-space: normal;
1905
  }
1906
 
1907
+ .settings-nav-btn span, .settings-nav-btn > :not(svg) { display: none; }
 
 
 
1908
 
1909
+ .settings-nav-btn svg { width: 18px; height: 18px; flex-shrink: 0; }
 
 
 
 
1910
 
1911
  .settings-nav-btn.active {
1912
  background: var(--bg-elevated);
 
1919
  min-width: 0;
1920
  }
1921
 
1922
+ .hero-title { font-size: 22px; }
1923
+ .hero-bot-icon { width: 80px; height: 80px; }
 
 
 
 
 
 
1924
 
1925
  .hero-welcome {
1926
  justify-content: center;
1927
+ padding: calc(var(--topbar-height) + 10px) 16px calc(20px + env(safe-area-inset-bottom));
1928
  overflow-y: auto;
1929
  min-height: 0;
1930
  }
1931
 
1932
+ .hero-input-wrap { max-width: 100%; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1933
 
1934
+ .login-title { font-size: 20px; white-space: normal; }
 
 
 
 
 
1935
 
1936
+ /* Horizontal lock */
1937
+ html, body, .app-container, .main-chat, .chat-container {
1938
+ max-width: 100vw;
1939
+ overflow-x: hidden;
1940
  }
1941
  }
1942
 
1943
  @media (max-width: 480px) {
1944
+ .hero-title { font-size: 20px; }
1945
+ .hero-input-box textarea { font-size: 13px; }
1946
+ .ai .message-content { font-size: 12.5px; }
1947
+ .user .message-content { font-size: 12.5px; }
1948
+ .katex-display { overflow-x: auto; font-size: 0.95em; }
1949
+ .message-content pre { font-size: 11px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1950
  }