Subh775 commited on
Commit
0550b32
·
1 Parent(s): cc29069

model routing&response; UI/UX improvement..

Browse files
Files changed (4) hide show
  1. graph.py +38 -3
  2. static/app.js +283 -26
  3. static/index.html +192 -39
  4. static/style.css +861 -229
graph.py CHANGED
@@ -37,6 +37,26 @@ checkpointer = SqliteSaver(conn=_conn)
37
  DEFAULT_MODEL = "openai/gpt-oss-120b:free"
38
  MODELS_TTL = 10 * 60 # seconds
39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
  # ── Query Classification ─────────────────────────────────────
42
 
@@ -160,6 +180,9 @@ def _fetch_pools() -> dict:
160
  mid = m.get("id", "")
161
  if not mid.endswith(":free"):
162
  continue
 
 
 
163
 
164
  arch = m.get("architecture", {})
165
  modality = arch.get("modality", "")
@@ -320,9 +343,21 @@ def chat_node(state: ChatState, config: RunnableConfig):
320
  messages = _strip_images(messages)
321
 
322
  # 4) Build system prompt & invoke
323
- sys = SystemMessage(
324
- content=prompts.build(persona, context, language, username, profile)
325
- )
 
 
 
 
 
 
 
 
 
 
 
 
326
  print(f"[ROUTER] category={category} model={model_id} vision={actual == 'vision'}")
327
 
328
  llm = _make_llm(api_key, model_id)
 
37
  DEFAULT_MODEL = "openai/gpt-oss-120b:free"
38
  MODELS_TTL = 10 * 60 # seconds
39
 
40
+ # Models that are safety classifiers / guardrail models — they output
41
+ # "User Safety: safe\nResponse Safety: safe" instead of actual answers.
42
+ _BLACKLISTED_MODELS = frozenset([
43
+ "meta-llama/llama-guard",
44
+ "meta-llama/llama-guard-2",
45
+ "meta-llama/llama-guard-3",
46
+ "meta-llama/llama-guard-4",
47
+ ])
48
+
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
 
61
  # ── Query Classification ─────────────────────────────────────
62
 
 
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
 
187
  arch = m.get("architecture", {})
188
  modality = arch.get("modality", "")
 
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"
353
+ "The student has attached an image with their question. "
354
+ "Analyze the image carefully and respond to their question about it. "
355
+ "Describe what you see in the image if relevant, then answer their query. "
356
+ "Do NOT output safety classifications, content ratings, or moderation labels. "
357
+ "You are a teacher — respond with an educational explanation, not a safety assessment.\n"
358
+ )
359
+
360
+ sys = SystemMessage(content=base_prompt)
361
  print(f"[ROUTER] category={category} model={model_id} vision={actual == 'vision'}")
362
 
363
  llm = _make_llm(api_key, model_id)
static/app.js CHANGED
@@ -1,6 +1,8 @@
1
  /* ============================================================
2
  STEM Copilot — Application Logic
3
- Auth, BYOK, Chat, Settings, Streaming MD, PWA, Sidebar
 
 
4
  ============================================================ */
5
 
6
  /* ----- State ----- */
@@ -9,6 +11,7 @@ let currentThreadId = crypto.randomUUID();
9
  const threads = [];
10
  let isSending = false;
11
  let pendingImage = null;
 
12
  let isHeroMode = true;
13
 
14
  let currentPersona = localStorage.getItem('stemcopilot_persona') || 'nerd';
@@ -41,6 +44,7 @@ const heroInput = document.getElementById('heroInput');
41
  const heroSendBtn = document.getElementById('heroSendBtn');
42
  const heroUploadBtn = document.getElementById('heroUploadBtn');
43
  const heroImageInput = document.getElementById('heroImageInput');
 
44
 
45
  const byokInput = document.getElementById('byokInput');
46
  const byokSubmitBtn = document.getElementById('byokSubmitBtn');
@@ -79,6 +83,51 @@ const installBanner = document.getElementById('installBanner');
79
  const installBtn = document.getElementById('installBtn');
80
  const installDismiss = document.getElementById('installDismiss');
81
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
  /* ============================================================
84
  TOAST NOTIFICATION
@@ -308,6 +357,9 @@ function showApp() {
308
  opt.classList.toggle('active', opt.dataset.persona === currentPersona);
309
  });
310
 
 
 
 
311
  const userId = currentUser ? currentUser.google_id : '';
312
  fetch('/threads?user_id=' + encodeURIComponent(userId))
313
  .then(r => r.json())
@@ -337,6 +389,19 @@ function enterHeroMode() {
337
  chatContainer.innerHTML = '';
338
  chatContainer.appendChild(createWelcomeScreen());
339
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
340
  }
341
 
342
  function exitHeroMode() {
@@ -406,10 +471,8 @@ document.addEventListener('touchend', (e) => {
406
  if (window.innerWidth > 768) return;
407
  const dx = e.changedTouches[0].clientX - _touchStartX;
408
  const dy = Math.abs(e.changedTouches[0].clientY - _touchStartY);
409
- if (dy > Math.abs(dx) * 0.8) return; // too vertical
410
- // Swipe right from left 60px edge → open
411
  if (!sidebarOpen && _touchStartX < 60 && dx > 50) openSidebar();
412
- // Swipe left anywhere → close
413
  if (sidebarOpen && dx < -50) closeSidebar();
414
  }, { passive: true });
415
 
@@ -437,16 +500,29 @@ function createWelcomeScreen() {
437
  const div = document.createElement('div');
438
  div.className = 'hero-welcome';
439
  div.id = 'welcomeScreen';
 
 
 
 
 
 
440
  div.innerHTML = `
441
  <img src="/assets/bot.png" alt="STEM Copilot" class="hero-bot-icon">
442
- <h1 class="hero-title">What should we study today?</h1>
443
  <div class="hero-input-wrap">
 
 
 
 
444
  <div class="hero-input-box" id="heroInputBoxDynamic">
445
  <button class="upload-btn" id="heroUploadBtnDynamic" title="Upload image">
446
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
447
  </button>
448
  <input type="file" id="heroImageInputDynamic" accept="image/*" style="display:none;">
449
  <textarea id="heroInputDynamic" placeholder="Ask STEM Copilot..." rows="1"></textarea>
 
 
 
450
  <button class="send-btn" id="heroSendBtnDynamic">
451
  <svg viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"></path></svg>
452
  </button>
@@ -464,6 +540,10 @@ function createWelcomeScreen() {
464
  const dynSend = div.querySelector('#heroSendBtnDynamic');
465
  const dynUpload = div.querySelector('#heroUploadBtnDynamic');
466
  const dynFileInput = div.querySelector('#heroImageInputDynamic');
 
 
 
 
467
 
468
  dynInput.addEventListener('input', function() {
469
  this.style.height = '54px'; this.style.height = this.scrollHeight + 'px';
@@ -475,13 +555,31 @@ function createWelcomeScreen() {
475
  });
476
  dynSend.addEventListener('click', () => { userInput.value = dynInput.value; sendMessage(); });
477
  dynUpload.addEventListener('click', () => dynFileInput.click());
478
- dynFileInput.addEventListener('change', () => { if (dynFileInput.files[0]) handleImageFile(dynFileInput.files[0]); });
 
 
 
 
 
 
 
 
 
 
 
479
  div.querySelectorAll('.hero-pill').forEach(p => {
480
  p.addEventListener('click', () => { userInput.value = p.dataset.query; sendMessage(); });
481
  });
482
  return div;
483
  }
484
 
 
 
 
 
 
 
 
485
  function addThreadToSidebar(id, title) {
486
  threads.unshift({ id, title });
487
  renderHistory();
@@ -554,6 +652,8 @@ function closeAllMenus() {
554
  document.querySelectorAll('.options-menu.show').forEach(m => m.classList.remove('show'));
555
  document.querySelectorAll('.options-btn.menu-open').forEach(b => b.classList.remove('menu-open'));
556
  if (userMenu) userMenu.classList.remove('show');
 
 
557
  }
558
 
559
  function deleteChat(e, optionEl) {
@@ -641,10 +741,11 @@ document.querySelectorAll('.persona-option').forEach(opt => {
641
  opt.classList.add('active');
642
  currentPersona = opt.dataset.persona;
643
  localStorage.setItem('stemcopilot_persona', currentPersona);
 
 
644
  });
645
  });
646
 
647
- // Save API key — with success/error feedback
648
  if (saveApiKeyBtn) saveApiKeyBtn.addEventListener('click', () => {
649
  const key = settingsApiKeyInput.value.trim();
650
  if (!key || key.startsWith('••')) return;
@@ -682,6 +783,99 @@ if (saveProfileBtn) saveProfileBtn.addEventListener('click', () => {
682
  });
683
 
684
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
685
  /* ============================================================
686
  IMAGE UPLOAD
687
  ============================================================ */
@@ -689,7 +883,16 @@ if (saveProfileBtn) saveProfileBtn.addEventListener('click', () => {
689
  if (uploadBtn) uploadBtn.addEventListener('click', () => imageInput.click());
690
  if (imageInput) imageInput.addEventListener('change', () => { if (imageInput.files[0]) handleImageFile(imageInput.files[0]); });
691
  if (heroUploadBtn) heroUploadBtn.addEventListener('click', () => heroImageInput.click());
692
- if (heroImageInput) heroImageInput.addEventListener('change', () => { if (heroImageInput.files[0]) handleImageFile(heroImageInput.files[0]); });
 
 
 
 
 
 
 
 
 
693
 
694
  document.addEventListener('paste', (e) => {
695
  const items = e.clipboardData?.items;
@@ -706,16 +909,33 @@ function handleImageFile(file) {
706
  reader.onload = (e) => {
707
  const dataUrl = e.target.result;
708
  pendingImage = dataUrl.split(',')[1];
 
709
  if (imagePreviewThumb) imagePreviewThumb.style.backgroundImage = `url(${dataUrl})`;
710
  if (imagePreviewBar) imagePreviewBar.style.display = 'flex';
 
 
 
 
 
 
 
 
 
711
  };
712
  reader.readAsDataURL(file);
713
  }
714
 
715
- if (imagePreviewRemove) imagePreviewRemove.addEventListener('click', () => {
716
  pendingImage = null;
 
717
  if (imagePreviewBar) imagePreviewBar.style.display = 'none';
718
  if (imageInput) imageInput.value = '';
 
 
 
 
 
 
719
  });
720
 
721
 
@@ -725,8 +945,8 @@ if (imagePreviewRemove) imagePreviewRemove.addEventListener('click', () => {
725
 
726
  let currentAbortController = null;
727
  let currentStreamReader = null;
728
- let currentStreamStopped = false; // module-level so stop button can kill it
729
- let currentRenderTimer = null; // so stop button can clear pending renders
730
 
731
  function _showStopBtn() {
732
  if (sendBtn) sendBtn.style.display = 'none';
@@ -739,27 +959,21 @@ function _showSendBtn() {
739
 
740
  if (stopBtn) {
741
  stopBtn.addEventListener('click', () => {
742
- // 1) Set the flag FIRST — this immediately stops token processing
743
  currentStreamStopped = true;
744
- // 2) Clear any pending render timer
745
  if (currentRenderTimer) { clearTimeout(currentRenderTimer); currentRenderTimer = null; }
746
- // 3) Cancel the stream reader so no more data arrives
747
  if (currentStreamReader) {
748
  try { currentStreamReader.cancel(); } catch(_) {}
749
  currentStreamReader = null;
750
  }
751
- // 4) Abort the fetch connection so server detects disconnect
752
  if (currentAbortController) {
753
  currentAbortController.abort();
754
  currentAbortController = null;
755
  }
756
- // 5) Reset UI state
757
  isSending = false;
758
  _showSendBtn();
759
  const currentThinking = document.getElementById('currentThinking');
760
  if (currentThinking) currentThinking.style.display = 'none';
761
  document.querySelectorAll('.ai-avatar.pulsing').forEach(el => el.classList.remove('pulsing'));
762
- // 6) Remove the typing cursor from partial response
763
  document.querySelectorAll('.message-content.cursor').forEach(el => el.classList.remove('cursor'));
764
  });
765
  }
@@ -827,6 +1041,7 @@ function sendMessage() {
827
  function streamResponse(text) {
828
  const imageData = pendingImage || '';
829
  pendingImage = null;
 
830
  if (imagePreviewBar) imagePreviewBar.style.display = 'none';
831
  if (imageInput) imageInput.value = '';
832
 
@@ -839,6 +1054,7 @@ function streamResponse(text) {
839
  <span class="thinking-text">Reading your question...</span>
840
  </div>
841
  <div class="message-content" style="display:none;"></div>
 
842
  </div>
843
  `;
844
  chatContainer.appendChild(rowDiv);
@@ -848,9 +1064,10 @@ function streamResponse(text) {
848
  const thinkingEl = rowDiv.querySelector('#currentThinking');
849
  const thinkingText = thinkingEl.querySelector('.thinking-text');
850
  const contentEl = rowDiv.querySelector('.message-content');
 
851
  let rawText = '';
852
  let firstToken = true;
853
- currentStreamStopped = false; // reset for this new stream
854
 
855
  currentRenderTimer = null;
856
  const RENDER_INTERVAL = 120;
@@ -879,14 +1096,14 @@ function streamResponse(text) {
879
  signal: currentAbortController.signal,
880
  }).then(response => {
881
  const reader = response.body.getReader();
882
- currentStreamReader = reader; // ← store so stop button can cancel it
883
  const decoder = new TextDecoder();
884
  let buffer = '';
885
 
886
  function read() {
887
  reader.read().then(({ done, value }) => {
888
  if (done || currentStreamStopped) {
889
- if (!currentStreamStopped) finishStream(thinkingEl, contentEl, rawText, currentRenderTimer);
890
  return;
891
  }
892
 
@@ -895,11 +1112,11 @@ function streamResponse(text) {
895
  buffer = lines.pop();
896
 
897
  for (const line of lines) {
898
- if (currentStreamStopped) return; // bail immediately
899
  if (!line.startsWith('data: ')) continue;
900
  const pl = line.substring(6);
901
  if (pl === '[DONE]') {
902
- finishStream(thinkingEl, contentEl, rawText, currentRenderTimer);
903
  currentStreamStopped = true; return;
904
  }
905
  try {
@@ -912,7 +1129,7 @@ function streamResponse(text) {
912
  isSending = false; _showSendBtn(); currentStreamStopped = true; return;
913
  }
914
  if (data.token !== undefined) {
915
- if (currentStreamStopped) return; // double-check before adding token
916
  if (firstToken) {
917
  thinkingEl.style.display = 'none';
918
  contentEl.style.display = 'block';
@@ -937,7 +1154,6 @@ function streamResponse(text) {
937
  read();
938
  }).catch(err => {
939
  if (err.name === 'AbortError' || currentStreamStopped) {
940
- // User clicked stop before any response — that's fine
941
  thinkingEl.style.display = 'none';
942
  contentEl.style.display = 'block';
943
  isSending = false; _showSendBtn();
@@ -955,7 +1171,7 @@ function streamResponse(text) {
955
  });
956
  }
957
 
958
- function finishStream(thinkingEl, contentEl, rawText, timer) {
959
  if (timer) clearTimeout(timer);
960
  currentRenderTimer = null;
961
  thinkingEl.style.display = 'none';
@@ -969,6 +1185,48 @@ function finishStream(thinkingEl, contentEl, rawText, timer) {
969
  const avatar = row.querySelector('.ai-avatar');
970
  if (avatar) avatar.classList.remove('pulsing');
971
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
972
  }
973
 
974
 
@@ -1040,7 +1298,6 @@ let deferredPrompt = null;
1040
  window.addEventListener('beforeinstallprompt', (e) => {
1041
  e.preventDefault();
1042
  deferredPrompt = e;
1043
- // Show install banner immediately — works on login screen too
1044
  const dismissed = localStorage.getItem('stemcopilot_install_dismissed');
1045
  if (!dismissed && installBanner) {
1046
  installBanner.style.display = 'flex';
 
1
  /* ============================================================
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 ----- */
 
11
  const threads = [];
12
  let isSending = false;
13
  let pendingImage = null;
14
+ let pendingImageDataUrl = null;
15
  let isHeroMode = true;
16
 
17
  let currentPersona = localStorage.getItem('stemcopilot_persona') || 'nerd';
 
44
  const heroSendBtn = document.getElementById('heroSendBtn');
45
  const heroUploadBtn = document.getElementById('heroUploadBtn');
46
  const heroImageInput = document.getElementById('heroImageInput');
47
+ const heroTitle = document.getElementById('heroTitle');
48
 
49
  const byokInput = document.getElementById('byokInput');
50
  const byokSubmitBtn = document.getElementById('byokSubmitBtn');
 
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 = { 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) {
124
+ const meta = document.querySelector('meta[name="theme-color"]');
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
 
357
  opt.classList.toggle('active', opt.dataset.persona === currentPersona);
358
  });
359
 
360
+ // Sync style selector label
361
+ _syncStyleLabel();
362
+
363
  const userId = currentUser ? currentUser.google_id : '';
364
  fetch('/threads?user_id=' + encodeURIComponent(userId))
365
  .then(r => r.json())
 
389
  chatContainer.innerHTML = '';
390
  chatContainer.appendChild(createWelcomeScreen());
391
  }
392
+ // Update hero title with username
393
+ _updateHeroTitle();
394
+ }
395
+
396
+ function _updateHeroTitle() {
397
+ const el = document.getElementById('heroTitle');
398
+ if (!el) return;
399
+ const name = currentUsername || (currentUser ? currentUser.name : '');
400
+ if (name) {
401
+ el.innerHTML = `What should we study today, <span class="hero-name">${escapeHtml(name)}</span>?`;
402
+ } else {
403
+ el.textContent = 'What should we study today?';
404
+ }
405
  }
406
 
407
  function exitHeroMode() {
 
471
  if (window.innerWidth > 768) return;
472
  const dx = e.changedTouches[0].clientX - _touchStartX;
473
  const dy = Math.abs(e.changedTouches[0].clientY - _touchStartY);
474
+ if (dy > Math.abs(dx) * 0.8) return;
 
475
  if (!sidebarOpen && _touchStartX < 60 && dx > 50) openSidebar();
 
476
  if (sidebarOpen && dx < -50) closeSidebar();
477
  }, { passive: true });
478
 
 
500
  const div = document.createElement('div');
501
  div.className = 'hero-welcome';
502
  div.id = 'welcomeScreen';
503
+
504
+ const name = currentUsername || (currentUser ? currentUser.name : '');
505
+ const titleHtml = name
506
+ ? `What should we study today, <span class="hero-name">${escapeHtml(name)}</span>?`
507
+ : `What should we study today?`;
508
+
509
  div.innerHTML = `
510
  <img src="/assets/bot.png" alt="STEM Copilot" class="hero-bot-icon">
511
+ <h1 class="hero-title" id="heroTitle">${titleHtml}</h1>
512
  <div class="hero-input-wrap">
513
+ <div class="hero-image-preview" id="heroImagePreviewDynamic">
514
+ <div class="hero-image-preview-thumb" id="heroImagePreviewThumbDynamic"></div>
515
+ <button class="hero-image-preview-remove" id="heroImagePreviewRemoveDynamic">&times;</button>
516
+ </div>
517
  <div class="hero-input-box" id="heroInputBoxDynamic">
518
  <button class="upload-btn" id="heroUploadBtnDynamic" title="Upload image">
519
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
520
  </button>
521
  <input type="file" id="heroImageInputDynamic" accept="image/*" style="display:none;">
522
  <textarea id="heroInputDynamic" placeholder="Ask STEM Copilot..." rows="1"></textarea>
523
+ <button class="mic-btn" id="heroMicBtnDynamic" title="Voice input">
524
+ <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>
525
+ </button>
526
  <button class="send-btn" id="heroSendBtnDynamic">
527
  <svg viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"></path></svg>
528
  </button>
 
540
  const dynSend = div.querySelector('#heroSendBtnDynamic');
541
  const dynUpload = div.querySelector('#heroUploadBtnDynamic');
542
  const dynFileInput = div.querySelector('#heroImageInputDynamic');
543
+ const dynMic = div.querySelector('#heroMicBtnDynamic');
544
+ const dynImgPreview = div.querySelector('#heroImagePreviewDynamic');
545
+ const dynImgThumb = div.querySelector('#heroImagePreviewThumbDynamic');
546
+ const dynImgRemove = div.querySelector('#heroImagePreviewRemoveDynamic');
547
 
548
  dynInput.addEventListener('input', function() {
549
  this.style.height = '54px'; this.style.height = this.scrollHeight + 'px';
 
555
  });
556
  dynSend.addEventListener('click', () => { userInput.value = dynInput.value; sendMessage(); });
557
  dynUpload.addEventListener('click', () => dynFileInput.click());
558
+ dynFileInput.addEventListener('change', () => {
559
+ if (dynFileInput.files[0]) {
560
+ handleImageFile(dynFileInput.files[0]);
561
+ // Show preview in hero
562
+ _showHeroImagePreview(dynImgPreview, dynImgThumb);
563
+ }
564
+ });
565
+ dynImgRemove.addEventListener('click', () => {
566
+ _clearImage();
567
+ dynImgPreview.classList.remove('visible');
568
+ });
569
+ if (dynMic) _bindMic(dynMic, dynInput);
570
  div.querySelectorAll('.hero-pill').forEach(p => {
571
  p.addEventListener('click', () => { userInput.value = p.dataset.query; sendMessage(); });
572
  });
573
  return div;
574
  }
575
 
576
+ function _showHeroImagePreview(previewEl, thumbEl) {
577
+ if (pendingImageDataUrl && previewEl && thumbEl) {
578
+ thumbEl.style.backgroundImage = `url(${pendingImageDataUrl})`;
579
+ previewEl.classList.add('visible');
580
+ }
581
+ }
582
+
583
  function addThreadToSidebar(id, title) {
584
  threads.unshift({ id, title });
585
  renderHistory();
 
652
  document.querySelectorAll('.options-menu.show').forEach(m => m.classList.remove('show'));
653
  document.querySelectorAll('.options-btn.menu-open').forEach(b => b.classList.remove('menu-open'));
654
  if (userMenu) userMenu.classList.remove('show');
655
+ // Close style dropdown too
656
+ if (styleDropdown) { styleDropdown.classList.remove('show'); styleSelectorBtn?.classList.remove('open'); }
657
  }
658
 
659
  function deleteChat(e, optionEl) {
 
741
  opt.classList.add('active');
742
  currentPersona = opt.dataset.persona;
743
  localStorage.setItem('stemcopilot_persona', currentPersona);
744
+ _syncStyleLabel();
745
+ _syncStyleDropdown();
746
  });
747
  });
748
 
 
749
  if (saveApiKeyBtn) saveApiKeyBtn.addEventListener('click', () => {
750
  const key = settingsApiKeyInput.value.trim();
751
  if (!key || key.startsWith('••')) return;
 
783
  });
784
 
785
 
786
+ /* ============================================================
787
+ TEACHING STYLE SELECTOR (in input bar)
788
+ ============================================================ */
789
+
790
+ function _syncStyleLabel() {
791
+ if (styleSelectorLabel) {
792
+ styleSelectorLabel.textContent = _PERSONA_LABELS[currentPersona] || 'Nerd';
793
+ }
794
+ }
795
+
796
+ function _syncStyleDropdown() {
797
+ if (!styleDropdown) return;
798
+ styleDropdown.querySelectorAll('.style-option').forEach(opt => {
799
+ opt.classList.toggle('active', opt.dataset.persona === currentPersona);
800
+ });
801
+ }
802
+
803
+ if (styleSelectorBtn) {
804
+ styleSelectorBtn.addEventListener('click', (e) => {
805
+ e.stopPropagation();
806
+ styleDropdown.classList.toggle('show');
807
+ styleSelectorBtn.classList.toggle('open');
808
+ });
809
+ }
810
+
811
+ if (styleDropdown) {
812
+ styleDropdown.querySelectorAll('.style-option').forEach(opt => {
813
+ opt.addEventListener('click', (e) => {
814
+ e.stopPropagation();
815
+ currentPersona = opt.dataset.persona;
816
+ localStorage.setItem('stemcopilot_persona', currentPersona);
817
+ _syncStyleLabel();
818
+ _syncStyleDropdown();
819
+ // Also sync settings modal persona options
820
+ document.querySelectorAll('.persona-option').forEach(o => {
821
+ o.classList.toggle('active', o.dataset.persona === currentPersona);
822
+ });
823
+ styleDropdown.classList.remove('show');
824
+ styleSelectorBtn.classList.remove('open');
825
+ });
826
+ });
827
+ }
828
+
829
+ _syncStyleLabel();
830
+ _syncStyleDropdown();
831
+
832
+
833
+ /* ============================================================
834
+ VOICE INPUT (Web Speech API)
835
+ ============================================================ */
836
+
837
+ function _bindMic(btn, targetInput) {
838
+ if (!('webkitSpeechRecognition' in window || 'SpeechRecognition' in window)) {
839
+ btn.style.display = 'none';
840
+ return;
841
+ }
842
+
843
+ const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
844
+ let recognition = null;
845
+
846
+ btn.addEventListener('click', () => {
847
+ if (btn.classList.contains('listening')) {
848
+ if (recognition) recognition.stop();
849
+ btn.classList.remove('listening');
850
+ return;
851
+ }
852
+
853
+ recognition = new SpeechRecognition();
854
+ recognition.lang = 'en-IN';
855
+ recognition.interimResults = true;
856
+ recognition.continuous = false;
857
+
858
+ btn.classList.add('listening');
859
+
860
+ recognition.onresult = (event) => {
861
+ let transcript = '';
862
+ for (let i = event.resultIndex; i < event.results.length; i++) {
863
+ transcript += event.results[i][0].transcript;
864
+ }
865
+ targetInput.value = transcript;
866
+ targetInput.dispatchEvent(new Event('input'));
867
+ };
868
+
869
+ recognition.onend = () => btn.classList.remove('listening');
870
+ recognition.onerror = () => btn.classList.remove('listening');
871
+ recognition.start();
872
+ });
873
+ }
874
+
875
+ if (micBtn && userInput) _bindMic(micBtn, userInput);
876
+ if (heroMicBtn && heroInput) _bindMic(heroMicBtn, heroInput);
877
+
878
+
879
  /* ============================================================
880
  IMAGE UPLOAD
881
  ============================================================ */
 
883
  if (uploadBtn) uploadBtn.addEventListener('click', () => imageInput.click());
884
  if (imageInput) imageInput.addEventListener('change', () => { if (imageInput.files[0]) handleImageFile(imageInput.files[0]); });
885
  if (heroUploadBtn) heroUploadBtn.addEventListener('click', () => heroImageInput.click());
886
+ if (heroImageInput) heroImageInput.addEventListener('change', () => {
887
+ if (heroImageInput.files[0]) {
888
+ handleImageFile(heroImageInput.files[0]);
889
+ // Show hero image preview
890
+ if (heroImagePreview && heroImagePreviewThumb && pendingImageDataUrl) {
891
+ heroImagePreviewThumb.style.backgroundImage = `url(${pendingImageDataUrl})`;
892
+ heroImagePreview.classList.add('visible');
893
+ }
894
+ }
895
+ });
896
 
897
  document.addEventListener('paste', (e) => {
898
  const items = e.clipboardData?.items;
 
909
  reader.onload = (e) => {
910
  const dataUrl = e.target.result;
911
  pendingImage = dataUrl.split(',')[1];
912
+ pendingImageDataUrl = dataUrl;
913
  if (imagePreviewThumb) imagePreviewThumb.style.backgroundImage = `url(${dataUrl})`;
914
  if (imagePreviewBar) imagePreviewBar.style.display = 'flex';
915
+ // Also show in hero if currently in hero mode
916
+ if (isHeroMode) {
917
+ const hp = document.getElementById('heroImagePreviewDynamic') || heroImagePreview;
918
+ const ht = document.getElementById('heroImagePreviewThumbDynamic') || heroImagePreviewThumb;
919
+ if (hp && ht) {
920
+ ht.style.backgroundImage = `url(${dataUrl})`;
921
+ hp.classList.add('visible');
922
+ }
923
+ }
924
  };
925
  reader.readAsDataURL(file);
926
  }
927
 
928
+ function _clearImage() {
929
  pendingImage = null;
930
+ pendingImageDataUrl = null;
931
  if (imagePreviewBar) imagePreviewBar.style.display = 'none';
932
  if (imageInput) imageInput.value = '';
933
+ }
934
+
935
+ if (imagePreviewRemove) imagePreviewRemove.addEventListener('click', _clearImage);
936
+ if (heroImagePreviewRemove) heroImagePreviewRemove.addEventListener('click', () => {
937
+ _clearImage();
938
+ if (heroImagePreview) heroImagePreview.classList.remove('visible');
939
  });
940
 
941
 
 
945
 
946
  let currentAbortController = null;
947
  let currentStreamReader = null;
948
+ let currentStreamStopped = false;
949
+ let currentRenderTimer = null;
950
 
951
  function _showStopBtn() {
952
  if (sendBtn) sendBtn.style.display = 'none';
 
959
 
960
  if (stopBtn) {
961
  stopBtn.addEventListener('click', () => {
 
962
  currentStreamStopped = true;
 
963
  if (currentRenderTimer) { clearTimeout(currentRenderTimer); currentRenderTimer = null; }
 
964
  if (currentStreamReader) {
965
  try { currentStreamReader.cancel(); } catch(_) {}
966
  currentStreamReader = null;
967
  }
 
968
  if (currentAbortController) {
969
  currentAbortController.abort();
970
  currentAbortController = null;
971
  }
 
972
  isSending = false;
973
  _showSendBtn();
974
  const currentThinking = document.getElementById('currentThinking');
975
  if (currentThinking) currentThinking.style.display = 'none';
976
  document.querySelectorAll('.ai-avatar.pulsing').forEach(el => el.classList.remove('pulsing'));
 
977
  document.querySelectorAll('.message-content.cursor').forEach(el => el.classList.remove('cursor'));
978
  });
979
  }
 
1041
  function streamResponse(text) {
1042
  const imageData = pendingImage || '';
1043
  pendingImage = null;
1044
+ pendingImageDataUrl = null;
1045
  if (imagePreviewBar) imagePreviewBar.style.display = 'none';
1046
  if (imageInput) imageInput.value = '';
1047
 
 
1054
  <span class="thinking-text">Reading your question...</span>
1055
  </div>
1056
  <div class="message-content" style="display:none;"></div>
1057
+ <div class="msg-actions" style="display:none;"></div>
1058
  </div>
1059
  `;
1060
  chatContainer.appendChild(rowDiv);
 
1064
  const thinkingEl = rowDiv.querySelector('#currentThinking');
1065
  const thinkingText = thinkingEl.querySelector('.thinking-text');
1066
  const contentEl = rowDiv.querySelector('.message-content');
1067
+ const actionsEl = rowDiv.querySelector('.msg-actions');
1068
  let rawText = '';
1069
  let firstToken = true;
1070
+ currentStreamStopped = false;
1071
 
1072
  currentRenderTimer = null;
1073
  const RENDER_INTERVAL = 120;
 
1096
  signal: currentAbortController.signal,
1097
  }).then(response => {
1098
  const reader = response.body.getReader();
1099
+ currentStreamReader = reader;
1100
  const decoder = new TextDecoder();
1101
  let buffer = '';
1102
 
1103
  function read() {
1104
  reader.read().then(({ done, value }) => {
1105
  if (done || currentStreamStopped) {
1106
+ if (!currentStreamStopped) finishStream(thinkingEl, contentEl, rawText, currentRenderTimer, actionsEl);
1107
  return;
1108
  }
1109
 
 
1112
  buffer = lines.pop();
1113
 
1114
  for (const line of lines) {
1115
+ if (currentStreamStopped) return;
1116
  if (!line.startsWith('data: ')) continue;
1117
  const pl = line.substring(6);
1118
  if (pl === '[DONE]') {
1119
+ finishStream(thinkingEl, contentEl, rawText, currentRenderTimer, actionsEl);
1120
  currentStreamStopped = true; return;
1121
  }
1122
  try {
 
1129
  isSending = false; _showSendBtn(); currentStreamStopped = true; return;
1130
  }
1131
  if (data.token !== undefined) {
1132
+ if (currentStreamStopped) return;
1133
  if (firstToken) {
1134
  thinkingEl.style.display = 'none';
1135
  contentEl.style.display = 'block';
 
1154
  read();
1155
  }).catch(err => {
1156
  if (err.name === 'AbortError' || currentStreamStopped) {
 
1157
  thinkingEl.style.display = 'none';
1158
  contentEl.style.display = 'block';
1159
  isSending = false; _showSendBtn();
 
1171
  });
1172
  }
1173
 
1174
+ function finishStream(thinkingEl, contentEl, rawText, timer, actionsEl) {
1175
  if (timer) clearTimeout(timer);
1176
  currentRenderTimer = null;
1177
  thinkingEl.style.display = 'none';
 
1185
  const avatar = row.querySelector('.ai-avatar');
1186
  if (avatar) avatar.classList.remove('pulsing');
1187
  }
1188
+ // Show copy/regenerate actions
1189
+ if (actionsEl && rawText) {
1190
+ actionsEl.style.display = 'flex';
1191
+ actionsEl.classList.add('visible');
1192
+ actionsEl.innerHTML = `
1193
+ <button class="msg-action-btn" data-action="copy" title="Copy to clipboard">
1194
+ <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>
1195
+ Copy
1196
+ </button>
1197
+ <button class="msg-action-btn" data-action="regenerate" title="Regenerate response">
1198
+ <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>
1199
+ Regenerate
1200
+ </button>
1201
+ `;
1202
+ const copyBtn = actionsEl.querySelector('[data-action="copy"]');
1203
+ const regenBtn = actionsEl.querySelector('[data-action="regenerate"]');
1204
+ if (copyBtn) {
1205
+ copyBtn.addEventListener('click', () => {
1206
+ navigator.clipboard.writeText(rawText).then(() => {
1207
+ copyBtn.querySelector('svg + *').textContent = 'Copied';
1208
+ setTimeout(() => { copyBtn.querySelector('svg + *').textContent = 'Copy'; }, 1500);
1209
+ }).catch(() => showToast('Could not copy to clipboard', 'error'));
1210
+ });
1211
+ }
1212
+ if (regenBtn) {
1213
+ regenBtn.addEventListener('click', () => {
1214
+ // Find the last user message and re-send
1215
+ const userMessages = chatContainer.querySelectorAll('.message-row.user');
1216
+ if (userMessages.length) {
1217
+ const lastUser = userMessages[userMessages.length - 1];
1218
+ const mc = lastUser.querySelector('.message-content');
1219
+ if (mc) {
1220
+ // Remove the AI response row
1221
+ row.remove();
1222
+ // Resend
1223
+ const lastText = mc.textContent.trim();
1224
+ if (lastText) streamResponse(lastText);
1225
+ }
1226
+ }
1227
+ });
1228
+ }
1229
+ }
1230
  }
1231
 
1232
 
 
1298
  window.addEventListener('beforeinstallprompt', (e) => {
1299
  e.preventDefault();
1300
  deferredPrompt = e;
 
1301
  const dismissed = localStorage.getItem('stemcopilot_install_dismissed');
1302
  if (!dismissed && installBanner) {
1303
  installBanner.style.display = 'flex';
static/index.html CHANGED
@@ -1,8 +1,10 @@
1
  <!DOCTYPE html>
2
  <html lang="en">
 
3
  <head>
4
  <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
 
6
  <meta name="screen-orientation" content="portrait">
7
  <meta name="x5-orientation" content="portrait">
8
  <meta name="mobile-web-app-capable" content="yes">
@@ -23,22 +25,46 @@
23
  <link rel="stylesheet" href="/static/style.css">
24
  <!-- Google Identity Services: loaded dynamically by initGoogleAuth() in app.js -->
25
  </head>
 
26
  <body>
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  <!-- ==================== LOGIN SCREEN ==================== -->
29
  <div class="login-screen" id="loginScreen">
30
  <div class="login-card">
31
  <img src="/assets/stembotix.png" alt="STEM Copilot" class="login-logo-stembotix">
32
  <h1 class="login-title">Welcome to STEM Copilot</h1>
33
  <p class="login-subtitle">Your personal NCERT tutor for Physics, Chemistry &amp; Mathematics</p>
34
- <div id="gsi-hidden-btn" style="position:fixed;top:-9999px;left:-9999px;opacity:0;pointer-events:none;"></div>
 
35
  <div class="google-btn-wrap">
36
  <button id="customGoogleBtn" class="gsi-custom-btn" onclick="triggerGoogleSignIn()">
37
  <svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
38
- <path fill="#4285F4" d="M17.64 9.2c0-.637-.057-1.251-.164-1.84H9v3.481h4.844c-.209 1.125-.843 2.078-1.796 2.717v2.258h2.908c1.702-1.567 2.684-3.875 2.684-6.615z"/>
39
- <path fill="#34A853" d="M9 18c2.43 0 4.467-.806 5.956-2.18l-2.908-2.259c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332A8.997 8.997 0 0 0 9 18z"/>
40
- <path fill="#FBBC05" d="M3.964 10.71A5.41 5.41 0 0 1 3.682 9c0-.593.102-1.17.282-1.71V4.958H.957A8.996 8.996 0 0 0 0 9c0 1.452.348 2.827.957 4.042l3.007-2.332z"/>
41
- <path fill="#EA4335" d="M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0A8.997 8.997 0 0 0 .957 4.958L3.964 6.29C4.672 4.163 6.656 3.58 9 3.58z"/>
 
 
 
 
42
  </svg>
43
  <span>Sign in with Google</span>
44
  </button>
@@ -56,11 +82,13 @@
56
  It's free — takes 30 seconds to set up.
57
  </p>
58
  <ol class="byok-steps">
59
- <li>Go to <a href="https://openrouter.ai/workspaces/default/keys" target="_blank" rel="noopener">openrouter.ai/keys</a></li>
 
60
  <li>Create a new key (free)</li>
61
  <li>Paste it below</li>
62
  </ol>
63
- <input type="text" id="byokInput" class="settings-input" placeholder="Paste your OpenRouter API key here" autocomplete="off" spellcheck="false">
 
64
  <button class="byok-submit-btn" id="byokSubmitBtn">Start Learning</button>
65
  <p class="byok-note">Your key is stored securely and never shared.</p>
66
  </div>
@@ -95,7 +123,10 @@
95
  <img src="/assets/stembotix.png" alt="STEMbotix" class="sidebar-logo">
96
  </div>
97
  <button class="new-chat-btn" id="newChatBtn">
98
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
 
 
 
99
  New Chat
100
  </button>
101
  <div class="chat-history" id="chatHistoryList"></div>
@@ -106,14 +137,24 @@
106
  <img id="userAvatar" class="user-avatar" src="" alt="">
107
  <span id="userDisplayName" class="user-name">Student</span>
108
  </div>
109
- <!-- User menu popup — Feedback now lives in Settings → Feedback tab -->
110
  <div class="user-menu" id="userMenu">
111
  <div class="user-menu-item" id="openSettingsBtn">
112
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><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"/></svg>
 
 
 
 
 
113
  Settings
114
  </div>
115
  <div class="user-menu-item user-menu-logout" id="logoutBtn">
116
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
 
 
 
 
 
117
  Log out
118
  </div>
119
  </div>
@@ -127,7 +168,10 @@
127
  <img src="/assets/stem_black.png" alt="S" class="rail-logo">
128
  </button>
129
  <button class="rail-icon-btn" id="railNewChatBtn" title="New chat">
130
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
 
 
 
131
  </button>
132
  </div>
133
  <div class="rail-bottom">
@@ -141,7 +185,11 @@
141
  <main class="main-chat">
142
  <header class="main-header">
143
  <button class="toggle-sidebar-btn" id="toggleSidebarBtn" title="Toggle Sidebar">
144
- <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><line x1="9" y1="3" x2="9" y2="21"/></svg>
 
 
 
 
145
  </button>
146
  </header>
147
 
@@ -149,24 +197,49 @@
149
  <!-- Hero / Welcome screen -->
150
  <div class="hero-welcome" id="welcomeScreen">
151
  <img src="/assets/bot.png" alt="STEM Copilot" class="hero-bot-icon">
152
- <h1 class="hero-title">What should we study today?</h1>
153
  <div class="hero-input-wrap">
 
 
 
 
 
154
  <div class="hero-input-box" id="heroInputBox">
155
  <button class="upload-btn" id="heroUploadBtn" title="Upload image">
156
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
 
 
 
 
 
157
  </button>
158
  <input type="file" id="heroImageInput" accept="image/*" style="display:none;">
159
  <textarea id="heroInput" placeholder="Ask STEM Copilot..." rows="1"></textarea>
 
 
 
 
 
 
 
 
 
160
  <button class="send-btn" id="heroSendBtn">
161
- <svg viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"></path></svg>
 
 
162
  </button>
163
  </div>
164
  </div>
165
  <div class="hero-pills" id="welcomePills">
166
- <button class="hero-pill" data-query="Explain Coulomb's law with derivation">Coulomb's Law</button>
167
- <button class="hero-pill" data-query="What is sp3 hybridisation? Explain with examples">sp3 Hybridisation</button>
168
- <button class="hero-pill" data-query="Derive the quadratic formula step by step">Quadratic Formula</button>
169
- <button class="hero-pill" data-query="Explain Newton's laws of motion with real world examples">Newton's Laws</button>
 
 
 
 
170
  </div>
171
  </div>
172
  </div>
@@ -180,15 +253,64 @@
180
  </div>
181
  <div class="input-box" id="inputBox">
182
  <button class="upload-btn" id="uploadBtn" title="Upload image">
183
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
 
 
 
 
 
184
  </button>
185
  <input type="file" id="imageInput" accept="image/*" style="display:none;">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  <textarea id="userInput" placeholder="Ask STEM Copilot..." rows="1"></textarea>
 
 
 
 
 
 
 
 
 
187
  <button class="stop-btn" id="stopBtn" title="Stop generating">
188
- <svg viewBox="0 0 24 24" fill="currentColor"><rect x="7" y="7" width="10" height="10" rx="1"/></svg>
 
 
189
  </button>
190
  <button class="send-btn" id="sendBtn">
191
- <svg viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"></path></svg>
 
 
192
  </button>
193
  </div>
194
  <div class="disclaimer-text">
@@ -209,23 +331,44 @@
209
  <!-- Vertical Tab Navigation -->
210
  <div class="settings-nav">
211
  <button class="settings-nav-btn active" data-tab="general">
212
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><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"/></svg>
 
 
 
 
 
213
  <span>General</span>
214
  </button>
215
  <button class="settings-nav-btn" data-tab="apikeys">
216
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/></svg>
 
 
 
 
217
  <span>API Keys</span>
218
  </button>
219
  <button class="settings-nav-btn" data-tab="personalization">
220
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
 
 
 
 
221
  <span>Profile</span>
222
  </button>
223
  <button class="settings-nav-btn" data-tab="style">
224
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
 
 
 
 
225
  <span>Teaching Style</span>
226
  </button>
227
  <button class="settings-nav-btn" data-tab="feedback">
228
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg>
 
 
 
 
229
  <span>Feedback</span>
230
  </button>
231
  </div><!-- /settings-nav -->
@@ -249,19 +392,24 @@
249
  <!-- API Keys Tab -->
250
  <div class="tab-content" id="tab-apikeys">
251
  <h3 class="settings-section-title">OpenRouter API Key</h3>
252
- <input type="password" class="settings-input" id="settingsApiKeyInput" placeholder="sk-or-..." autocomplete="off" spellcheck="false">
 
253
  <button class="settings-save-btn" id="saveApiKeyBtn">Save Key</button>
254
  <p class="settings-hint">
255
- Get your free key at <a href="https://openrouter.ai/workspaces/default/keys" target="_blank" rel="noopener">openrouter.ai/keys</a>
 
256
  </p>
257
  </div>
258
 
259
  <!-- Profile Tab -->
260
  <div class="tab-content" id="tab-personalization">
261
  <h3 class="settings-section-title">Your Name</h3>
262
- <input type="text" class="settings-input" id="usernameInput" placeholder="What should I call you?" maxlength="30" autocomplete="off">
 
263
  <h3 class="settings-section-title" style="margin-top:20px;">About You</h3>
264
- <textarea class="settings-textarea" id="profileInput" placeholder="e.g. I'm in Class XII, preparing for JEE. Physics and Maths scare me. I'm good at Chemistry." rows="4"></textarea>
 
 
265
  <button class="settings-save-btn" id="saveProfileBtn">Save Profile</button>
266
  </div>
267
 
@@ -269,8 +417,9 @@
269
  <div class="tab-content" id="tab-style">
270
  <p class="settings-hint" style="margin-bottom:14px;">Pick the mood that suits you today.</p>
271
  <div class="persona-option" data-persona="noob">
272
- <div class="persona-name">Noob</div>
273
- <div class="persona-desc">Patient, step-by-step. Explains like you're seeing it for the first time.</div>
 
274
  </div>
275
  <div class="persona-option active" data-persona="nerd">
276
  <div class="persona-name">Nerd</div>
@@ -278,7 +427,8 @@
278
  </div>
279
  <div class="persona-option" data-persona="thoughtful">
280
  <div class="persona-name">Thoughtful</div>
281
- <div class="persona-desc">Connects science to the real world. Every formula has a story.</div>
 
282
  </div>
283
  <div class="persona-option" data-persona="panic">
284
  <div class="persona-name">Panic</div>
@@ -295,8 +445,10 @@
295
  <option value="other">Other</option>
296
  </select>
297
  <h3 class="settings-section-title" style="margin-top:16px;">Message</h3>
298
- <textarea class="settings-textarea" id="feedbackMessage" rows="5" placeholder="Tell us how we can improve..."></textarea>
299
- <button class="settings-save-btn" id="submitFeedbackBtn" style="width:100%;margin-top:4px;">Submit Securely</button>
 
 
300
  </div>
301
  </div><!-- /settings-panel -->
302
  </div><!-- /settings-layout -->
@@ -310,4 +462,5 @@
310
  <script defer src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
311
  <script defer src="/static/app.js"></script>
312
  </body>
313
- </html>
 
 
1
  <!DOCTYPE html>
2
  <html lang="en">
3
+
4
  <head>
5
  <meta charset="UTF-8">
6
+ <meta name="viewport"
7
+ content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
8
  <meta name="screen-orientation" content="portrait">
9
  <meta name="x5-orientation" content="portrait">
10
  <meta name="mobile-web-app-capable" content="yes">
 
25
  <link rel="stylesheet" href="/static/style.css">
26
  <!-- Google Identity Services: loaded dynamically by initGoogleAuth() in app.js -->
27
  </head>
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>
54
  <p class="login-subtitle">Your personal NCERT tutor for Physics, Chemistry &amp; Mathematics</p>
55
+ <div id="gsi-hidden-btn" style="position:fixed;top:-9999px;left:-9999px;opacity:0;pointer-events:none;">
56
+ </div>
57
  <div class="google-btn-wrap">
58
  <button id="customGoogleBtn" class="gsi-custom-btn" onclick="triggerGoogleSignIn()">
59
  <svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
60
+ <path fill="#2231D4"
61
+ d="M17.64 9.2c0-.637-.057-1.251-.164-1.84H9v3.481h4.844c-.209 1.125-.843 2.078-1.796 2.717v2.258h2.908c1.702-1.567 2.684-3.875 2.684-6.615z" />
62
+ <path fill="#34A853"
63
+ d="M9 18c2.43 0 4.467-.806 5.956-2.18l-2.908-2.259c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332A8.997 8.997 0 0 0 9 18z" />
64
+ <path fill="#FBBC05"
65
+ d="M3.964 10.71A5.41 5.41 0 0 1 3.682 9c0-.593.102-1.17.282-1.71V4.958H.957A8.996 8.996 0 0 0 0 9c0 1.452.348 2.827.957 4.042l3.007-2.332z" />
66
+ <path fill="#EA4335"
67
+ d="M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0A8.997 8.997 0 0 0 .957 4.958L3.964 6.29C4.672 4.163 6.656 3.58 9 3.58z" />
68
  </svg>
69
  <span>Sign in with Google</span>
70
  </button>
 
82
  It's free — takes 30 seconds to set up.
83
  </p>
84
  <ol class="byok-steps">
85
+ <li>Go to <a href="https://openrouter.ai/workspaces/default/keys" target="_blank"
86
+ rel="noopener">openrouter.ai/keys</a></li>
87
  <li>Create a new key (free)</li>
88
  <li>Paste it below</li>
89
  </ol>
90
+ <input type="text" id="byokInput" class="settings-input" placeholder="Paste your OpenRouter API key here"
91
+ autocomplete="off" spellcheck="false">
92
  <button class="byok-submit-btn" id="byokSubmitBtn">Start Learning</button>
93
  <p class="byok-note">Your key is stored securely and never shared.</p>
94
  </div>
 
123
  <img src="/assets/stembotix.png" alt="STEMbotix" class="sidebar-logo">
124
  </div>
125
  <button class="new-chat-btn" id="newChatBtn">
126
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
127
+ stroke-linecap="round" stroke-linejoin="round">
128
+ <path d="M12 5v14M5 12h14" />
129
+ </svg>
130
  New Chat
131
  </button>
132
  <div class="chat-history" id="chatHistoryList"></div>
 
137
  <img id="userAvatar" class="user-avatar" src="" alt="">
138
  <span id="userDisplayName" class="user-name">Student</span>
139
  </div>
140
+ <!-- User menu popup -->
141
  <div class="user-menu" id="userMenu">
142
  <div class="user-menu-item" id="openSettingsBtn">
143
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
144
+ stroke-width="2">
145
+ <circle cx="12" cy="12" r="3" />
146
+ <path
147
+ 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" />
148
+ </svg>
149
  Settings
150
  </div>
151
  <div class="user-menu-item user-menu-logout" id="logoutBtn">
152
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
153
+ stroke-width="2">
154
+ <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
155
+ <polyline points="16 17 21 12 16 7" />
156
+ <line x1="21" y1="12" x2="9" y2="12" />
157
+ </svg>
158
  Log out
159
  </div>
160
  </div>
 
168
  <img src="/assets/stem_black.png" alt="S" class="rail-logo">
169
  </button>
170
  <button class="rail-icon-btn" id="railNewChatBtn" title="New chat">
171
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
172
+ stroke-linecap="round" stroke-linejoin="round">
173
+ <path d="M12 5v14M5 12h14" />
174
+ </svg>
175
  </button>
176
  </div>
177
  <div class="rail-bottom">
 
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">
190
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
191
+ <line x1="9" y1="3" x2="9" y2="21" />
192
+ </svg>
193
  </button>
194
  </header>
195
 
 
197
  <!-- Hero / Welcome screen -->
198
  <div class="hero-welcome" id="welcomeScreen">
199
  <img src="/assets/bot.png" alt="STEM Copilot" class="hero-bot-icon">
200
+ <h1 class="hero-title" id="heroTitle">What should we study today?</h1>
201
  <div class="hero-input-wrap">
202
+ <!-- Image preview in hero -->
203
+ <div class="hero-image-preview" id="heroImagePreview">
204
+ <div class="hero-image-preview-thumb" id="heroImagePreviewThumb"></div>
205
+ <button class="hero-image-preview-remove" id="heroImagePreviewRemove">&times;</button>
206
+ </div>
207
  <div class="hero-input-box" id="heroInputBox">
208
  <button class="upload-btn" id="heroUploadBtn" title="Upload image">
209
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
210
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
211
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
212
+ <circle cx="8.5" cy="8.5" r="1.5" />
213
+ <polyline points="21 15 16 10 5 21" />
214
+ </svg>
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
+ <!-- Voice input -->
219
+ <button class="mic-btn" id="heroMicBtn" title="Voice input">
220
+ <svg viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round">
221
+ <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" />
222
+ <path d="M19 10v2a7 7 0 0 1-14 0v-2" />
223
+ <line x1="12" y1="19" x2="12" y2="23" />
224
+ <line x1="8" y1="23" x2="16" y2="23" />
225
+ </svg>
226
+ </button>
227
  <button class="send-btn" id="heroSendBtn">
228
+ <svg viewBox="0 0 24 24">
229
+ <path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"></path>
230
+ </svg>
231
  </button>
232
  </div>
233
  </div>
234
  <div class="hero-pills" id="welcomePills">
235
+ <button class="hero-pill" data-query="Explain Coulomb's law with derivation">Coulomb's
236
+ Law</button>
237
+ <button class="hero-pill" data-query="What is sp3 hybridisation? Explain with examples">sp3
238
+ Hybridisation</button>
239
+ <button class="hero-pill" data-query="Derive the quadratic formula step by step">Quadratic
240
+ Formula</button>
241
+ <button class="hero-pill"
242
+ data-query="Explain Newton's laws of motion with real world examples">Newton's Laws</button>
243
  </div>
244
  </div>
245
  </div>
 
253
  </div>
254
  <div class="input-box" id="inputBox">
255
  <button class="upload-btn" id="uploadBtn" title="Upload image">
256
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
257
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
258
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
259
+ <circle cx="8.5" cy="8.5" r="1.5" />
260
+ <polyline points="21 15 16 10 5 21" />
261
+ </svg>
262
  </button>
263
  <input type="file" id="imageInput" accept="image/*" style="display:none;">
264
+ <!-- Teaching style selector -->
265
+ <div class="style-selector-wrap" id="styleSelectorWrap">
266
+ <button class="style-selector-btn" id="styleSelectorBtn" title="Teaching style">
267
+ <span id="styleSelectorLabel">Nerd</span>
268
+ <svg viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round">
269
+ <polyline points="6 9 12 15 18 9" />
270
+ </svg>
271
+ </button>
272
+ <div class="style-dropdown" id="styleDropdown">
273
+ <div class="style-option active" data-persona="nerd">
274
+ <div class="style-option-name">Nerd</div>
275
+ <div class="style-option-desc">Deep, rigorous, first-principles. Goes beyond the
276
+ textbook.</div>
277
+ </div>
278
+ <div class="style-option" data-persona="noob">
279
+ <div class="style-option-name">Beginner</div>
280
+ <div class="style-option-desc">Patient, step-by-step. Explains like you're seeing it for
281
+ the first time.</div>
282
+ </div>
283
+ <div class="style-option" data-persona="thoughtful">
284
+ <div class="style-option-name">Thoughtful</div>
285
+ <div class="style-option-desc">Connects science to the real world. Every formula has a
286
+ story.</div>
287
+ </div>
288
+ <div class="style-option" data-persona="panic">
289
+ <div class="style-option-name">Panic</div>
290
+ <div class="style-option-desc">Exam tomorrow? Concise bullets, key formulas, no fluff.
291
+ </div>
292
+ </div>
293
+ </div>
294
+ </div>
295
  <textarea id="userInput" placeholder="Ask STEM Copilot..." rows="1"></textarea>
296
+ <!-- Voice input -->
297
+ <button class="mic-btn" id="micBtn" title="Voice input">
298
+ <svg viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round">
299
+ <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" />
300
+ <path d="M19 10v2a7 7 0 0 1-14 0v-2" />
301
+ <line x1="12" y1="19" x2="12" y2="23" />
302
+ <line x1="8" y1="23" x2="16" y2="23" />
303
+ </svg>
304
+ </button>
305
  <button class="stop-btn" id="stopBtn" title="Stop generating">
306
+ <svg viewBox="0 0 24 24" fill="currentColor">
307
+ <rect x="7" y="7" width="10" height="10" rx="1" />
308
+ </svg>
309
  </button>
310
  <button class="send-btn" id="sendBtn">
311
+ <svg viewBox="0 0 24 24">
312
+ <path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"></path>
313
+ </svg>
314
  </button>
315
  </div>
316
  <div class="disclaimer-text">
 
331
  <!-- Vertical Tab Navigation -->
332
  <div class="settings-nav">
333
  <button class="settings-nav-btn active" data-tab="general">
334
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
335
+ stroke-width="2">
336
+ <circle cx="12" cy="12" r="3" />
337
+ <path
338
+ 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" />
339
+ </svg>
340
  <span>General</span>
341
  </button>
342
  <button class="settings-nav-btn" data-tab="apikeys">
343
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
344
+ stroke-width="2">
345
+ <path
346
+ d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4" />
347
+ </svg>
348
  <span>API Keys</span>
349
  </button>
350
  <button class="settings-nav-btn" data-tab="personalization">
351
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
352
+ stroke-width="2">
353
+ <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
354
+ <circle cx="12" cy="7" r="4" />
355
+ </svg>
356
  <span>Profile</span>
357
  </button>
358
  <button class="settings-nav-btn" data-tab="style">
359
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
360
+ stroke-width="2">
361
+ <path d="M12 20h9" />
362
+ <path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" />
363
+ </svg>
364
  <span>Teaching Style</span>
365
  </button>
366
  <button class="settings-nav-btn" data-tab="feedback">
367
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
368
+ stroke-width="2">
369
+ <path
370
+ d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
371
+ </svg>
372
  <span>Feedback</span>
373
  </button>
374
  </div><!-- /settings-nav -->
 
392
  <!-- API Keys Tab -->
393
  <div class="tab-content" id="tab-apikeys">
394
  <h3 class="settings-section-title">OpenRouter API Key</h3>
395
+ <input type="password" class="settings-input" id="settingsApiKeyInput" placeholder="sk-or-..."
396
+ autocomplete="off" spellcheck="false">
397
  <button class="settings-save-btn" id="saveApiKeyBtn">Save Key</button>
398
  <p class="settings-hint">
399
+ Get your free key at <a href="https://openrouter.ai/workspaces/default/keys" target="_blank"
400
+ rel="noopener">openrouter.ai/keys</a>
401
  </p>
402
  </div>
403
 
404
  <!-- Profile Tab -->
405
  <div class="tab-content" id="tab-personalization">
406
  <h3 class="settings-section-title">Your Name</h3>
407
+ <input type="text" class="settings-input" id="usernameInput"
408
+ placeholder="What should I call you?" maxlength="30" autocomplete="off">
409
  <h3 class="settings-section-title" style="margin-top:20px;">About You</h3>
410
+ <textarea class="settings-textarea" id="profileInput"
411
+ placeholder="e.g. I'm in Class XII, preparing for JEE. Physics and Maths scare me. I'm good at Chemistry."
412
+ rows="4"></textarea>
413
  <button class="settings-save-btn" id="saveProfileBtn">Save Profile</button>
414
  </div>
415
 
 
417
  <div class="tab-content" id="tab-style">
418
  <p class="settings-hint" style="margin-bottom:14px;">Pick the mood that suits you today.</p>
419
  <div class="persona-option" data-persona="noob">
420
+ <div class="persona-name">Beginner</div>
421
+ <div class="persona-desc">Patient, step-by-step. Explains like you're seeing it for the
422
+ first time.</div>
423
  </div>
424
  <div class="persona-option active" data-persona="nerd">
425
  <div class="persona-name">Nerd</div>
 
427
  </div>
428
  <div class="persona-option" data-persona="thoughtful">
429
  <div class="persona-name">Thoughtful</div>
430
+ <div class="persona-desc">Connects science to the real world. Every formula has a story.
431
+ </div>
432
  </div>
433
  <div class="persona-option" data-persona="panic">
434
  <div class="persona-name">Panic</div>
 
445
  <option value="other">Other</option>
446
  </select>
447
  <h3 class="settings-section-title" style="margin-top:16px;">Message</h3>
448
+ <textarea class="settings-textarea" id="feedbackMessage" rows="5"
449
+ placeholder="Tell us how we can improve..."></textarea>
450
+ <button class="settings-save-btn" id="submitFeedbackBtn"
451
+ style="width:100%;margin-top:4px;">Submit Securely</button>
452
  </div>
453
  </div><!-- /settings-panel -->
454
  </div><!-- /settings-layout -->
 
462
  <script defer src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
463
  <script defer src="/static/app.js"></script>
464
  </body>
465
+
466
+ </html>
static/style.css CHANGED
@@ -1,12 +1,12 @@
1
  /* ============================================================
2
  STEM Copilot — Stylesheet
3
- Montserrat, dark theme, premium chat UI
4
  ============================================================ */
5
 
6
  :root {
7
- --brand-orange: #EB5A28;
8
- --brand-orange-hover: #cf4f22;
9
- --brand-orange-glow: rgba(235, 90, 40, 0.25);
10
  --bg-main: #000000;
11
  --bg-sidebar: #0a0a0a;
12
  --bg-input: #1a1a1a;
@@ -28,7 +28,28 @@
28
  --rail-width: 56px;
29
  }
30
 
31
- * { box-sizing: border-box; margin: 0; padding: 0; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
  html {
34
  -webkit-text-size-adjust: 100%;
@@ -47,6 +68,80 @@ body {
47
  touch-action: manipulation;
48
  -webkit-tap-highlight-color: transparent;
49
  overscroll-behavior: none;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  }
51
 
52
 
@@ -70,14 +165,6 @@ body {
70
  padding: 48px 36px;
71
  }
72
 
73
- .login-logo {
74
- width: 100px;
75
- height: 100px;
76
- margin-bottom: 28px;
77
- border-radius: 50%;
78
- object-fit: cover;
79
- }
80
-
81
  .login-logo-stembotix {
82
  height: 48px;
83
  margin-bottom: 32px;
@@ -109,22 +196,22 @@ body {
109
  display: inline-flex;
110
  align-items: center;
111
  gap: 12px;
112
- background: #1a1a1a;
113
- border: 1px solid #333;
114
  border-radius: 24px;
115
  padding: 12px 28px;
116
- color: #f0ece6;
117
  font-family: 'Montserrat', sans-serif;
118
  font-size: 13px;
119
  font-weight: 700;
120
  cursor: pointer;
121
- transition: background 0.2s, border-color 0.2s, transform 0.1s;
122
  white-space: nowrap;
123
  }
124
 
125
  .gsi-custom-btn:hover {
126
- background: #222;
127
- border-color: #555;
128
  }
129
 
130
  .gsi-custom-btn:active {
@@ -158,9 +245,18 @@ body {
158
  padding: 48px 36px;
159
  }
160
 
161
- .byok-card h2 { font-size: 22px; font-weight: 700; margin-bottom: 12px; }
 
 
 
 
162
 
163
- .byok-desc { font-size: 14px; color: var(--text-secondary); line-height: 1.6; margin-bottom: 20px; }
 
 
 
 
 
164
 
165
  .byok-steps {
166
  text-align: left;
@@ -171,14 +267,20 @@ body {
171
  color: var(--text-secondary);
172
  }
173
 
174
- .byok-steps a { color: var(--brand-orange); text-decoration: none; }
175
- .byok-steps a:hover { text-decoration: underline; }
 
 
 
 
 
 
176
 
177
  .byok-submit-btn {
178
  width: 100%;
179
  padding: 14px;
180
  margin-top: 16px;
181
- background: var(--brand-orange);
182
  color: #fff;
183
  border: none;
184
  border-radius: var(--radius);
@@ -186,11 +288,18 @@ body {
186
  font-size: 15px;
187
  font-weight: 600;
188
  cursor: pointer;
189
- transition: background var(--transition);
190
  }
191
 
192
- .byok-submit-btn:hover { background: var(--brand-orange-hover); }
193
- .byok-note { font-size: 11px; color: var(--text-muted); margin-top: 12px; }
 
 
 
 
 
 
 
194
 
195
 
196
  /* ============================================================
@@ -202,7 +311,7 @@ body {
202
  bottom: calc(24px + env(safe-area-inset-bottom));
203
  left: 50%;
204
  transform: translateX(-50%);
205
- z-index: 1000;
206
  background: var(--bg-elevated);
207
  border: 1px solid var(--border-color);
208
  border-radius: var(--radius-lg);
@@ -210,13 +319,17 @@ body {
210
  display: flex;
211
  align-items: center;
212
  gap: 12px;
213
- box-shadow: 0 12px 48px rgba(0,0,0,0.8);
214
  max-width: 400px;
215
  width: calc(100% - 32px);
216
  animation: slideUp 0.4s cubic-bezier(0.16, 1, 0.3, 1);
217
  }
218
 
219
- .install-banner-icon { width: 36px; height: 36px; border-radius: 8px; }
 
 
 
 
220
 
221
  .install-banner-text {
222
  flex: 1;
@@ -226,11 +339,17 @@ body {
226
  line-height: 1.4;
227
  }
228
 
229
- .install-banner-text strong { color: var(--text-primary); }
230
- .install-banner-text span { color: var(--text-secondary); font-size: 11px; }
 
 
 
 
 
 
231
 
232
  .install-banner-btn {
233
- background: var(--brand-orange);
234
  color: #fff;
235
  border: none;
236
  border-radius: 8px;
@@ -239,10 +358,12 @@ body {
239
  font-size: 13px;
240
  font-weight: 600;
241
  cursor: pointer;
242
- transition: background var(--transition);
243
  }
244
 
245
- .install-banner-btn:hover { background: var(--brand-orange-hover); }
 
 
246
 
247
  .install-banner-dismiss {
248
  background: none;
@@ -254,8 +375,15 @@ body {
254
  }
255
 
256
  @keyframes slideUp {
257
- from { opacity: 0; transform: translateX(-50%) translateY(20px); }
258
- to { opacity: 1; transform: translateX(-50%) translateY(0); }
 
 
 
 
 
 
 
259
  }
260
 
261
 
@@ -282,7 +410,7 @@ body {
282
  display: flex;
283
  flex-direction: column;
284
  border-right: 1px solid var(--border-color);
285
- transition: margin-left var(--transition), opacity var(--transition);
286
  position: relative;
287
  z-index: 20;
288
  }
@@ -314,8 +442,8 @@ body {
314
  .new-chat-btn {
315
  margin: 8px 14px;
316
  padding: 12px 16px;
317
- background: linear-gradient(135deg, rgba(235,90,40,0.08), rgba(235,90,40,0.02));
318
- border: 1px solid rgba(235,90,40,0.2);
319
  color: var(--text-primary);
320
  border-radius: var(--radius);
321
  cursor: pointer;
@@ -325,17 +453,17 @@ body {
325
  font-size: 14px;
326
  font-weight: 600;
327
  font-family: inherit;
328
- transition: background var(--transition), border-color var(--transition), box-shadow var(--transition);
329
  }
330
 
331
  .new-chat-btn:hover {
332
- background: linear-gradient(135deg, rgba(235,90,40,0.15), rgba(235,90,40,0.05));
333
- border-color: rgba(235,90,40,0.4);
334
- box-shadow: 0 0 16px rgba(235,90,40,0.1);
335
  }
336
 
337
  .new-chat-btn svg {
338
- color: var(--brand-orange);
339
  }
340
 
341
  .chat-history {
@@ -357,7 +485,7 @@ body {
357
  display: flex;
358
  align-items: center;
359
  gap: 10px;
360
- transition: background var(--transition), color var(--transition), transform 0.15s;
361
  }
362
 
363
  .history-item:hover {
@@ -370,9 +498,9 @@ body {
370
  }
371
 
372
  .history-item.active {
373
- background: linear-gradient(135deg, rgba(235,90,40,0.1), rgba(235,90,40,0.03));
374
  color: var(--text-primary);
375
- border: 1px solid rgba(235,90,40,0.15);
376
  }
377
 
378
  .history-item:not(.active) {
@@ -388,7 +516,7 @@ body {
388
 
389
  .history-item.active .thread-icon {
390
  opacity: 0.8;
391
- color: var(--brand-orange);
392
  }
393
 
394
  .chat-title {
@@ -399,34 +527,45 @@ body {
399
  }
400
 
401
  .options-btn {
402
- background: none; border: none;
 
403
  color: var(--text-secondary);
404
- font-size: 18px; line-height: 1;
405
- cursor: pointer; opacity: 0;
 
 
406
  padding: 0 4px;
407
  flex-shrink: 0;
408
  transition: opacity 0.2s, color 0.2s;
409
  }
410
 
411
  .history-item:hover .options-btn,
412
- .options-btn.menu-open { opacity: 1; }
413
- .options-btn:hover { color: var(--text-primary); }
 
 
 
 
 
414
 
415
  .options-menu {
416
  position: absolute;
417
- right: 10px; top: 40px;
 
418
  background: var(--bg-elevated);
419
  border: 1px solid var(--border-color);
420
  border-radius: 10px;
421
  display: none;
422
  flex-direction: column;
423
  z-index: 100;
424
- box-shadow: 0 8px 32px rgba(0,0,0,0.7);
425
  min-width: 130px;
426
  overflow: hidden;
427
  }
428
 
429
- .options-menu.show { display: flex; }
 
 
430
 
431
  .option-item {
432
  padding: 10px 14px;
@@ -439,27 +578,34 @@ body {
439
  transition: background 0.2s;
440
  }
441
 
442
- .option-item:hover { background: var(--bg-hover); }
443
- .option-item.delete { color: #ff4a4a; }
 
 
 
 
 
444
 
445
  .rename-input {
446
  width: 100%;
447
- background: transparent; border: none;
 
448
  color: var(--text-primary);
449
- font-family: inherit; font-size: 13px;
 
450
  outline: none;
451
- border-bottom: 1px solid var(--brand-orange);
452
  padding: 2px 0;
453
  }
454
 
455
 
456
- /* --- Sidebar bottom — user profile (premium look) --- */
457
 
458
  .sidebar-bottom {
459
  padding: 12px 14px 16px;
460
  border-top: 1px solid var(--border-color);
461
  position: relative;
462
- background: linear-gradient(180deg, transparent, rgba(235,90,40,0.02));
463
  }
464
 
465
  .user-profile-btn {
@@ -469,27 +615,28 @@ body {
469
  padding: 10px 12px;
470
  border-radius: 12px;
471
  cursor: pointer;
472
- transition: background var(--transition), box-shadow var(--transition);
473
  border: 1px solid transparent;
474
  }
475
 
476
  .user-profile-btn:hover {
477
  background: var(--bg-hover);
478
  border-color: var(--border-color);
479
- box-shadow: 0 2px 12px rgba(0,0,0,0.3);
480
  }
481
 
482
  .user-avatar {
483
- width: 34px; height: 34px;
 
484
  border-radius: 50%;
485
  object-fit: cover;
486
  background: var(--bg-hover);
487
  border: 2px solid var(--border-color);
488
- transition: border-color var(--transition);
489
  }
490
 
491
  .user-profile-btn:hover .user-avatar {
492
- border-color: var(--brand-orange);
493
  }
494
 
495
  .user-name {
@@ -501,21 +648,23 @@ body {
501
  text-overflow: ellipsis;
502
  }
503
 
504
- .user-menu-arrow { opacity: 0.5; transition: transform 0.2s; }
505
-
506
  .user-menu {
507
  display: none;
508
  position: absolute;
509
- bottom: 72px; left: 14px; right: 14px;
 
 
510
  background: var(--bg-elevated);
511
  border: 1px solid var(--border-color);
512
  border-radius: var(--radius);
513
- box-shadow: 0 8px 32px rgba(0,0,0,0.7);
514
  overflow: hidden;
515
  z-index: 50;
516
  }
517
 
518
- .user-menu.show { display: block; }
 
 
519
 
520
  .user-menu-item {
521
  padding: 12px 16px;
@@ -527,8 +676,13 @@ body {
527
  transition: background 0.2s;
528
  }
529
 
530
- .user-menu-item:hover { background: var(--bg-hover); }
531
- .user-menu-logout { color: #ff4a4a; }
 
 
 
 
 
532
 
533
 
534
  /* ============================================================
@@ -552,7 +706,8 @@ body {
552
  display: flex;
553
  }
554
 
555
- .rail-top, .rail-bottom {
 
556
  display: flex;
557
  flex-direction: column;
558
  align-items: center;
@@ -560,7 +715,8 @@ body {
560
  }
561
 
562
  .rail-icon-btn {
563
- width: 40px; height: 40px;
 
564
  border-radius: 10px;
565
  background: transparent;
566
  border: none;
@@ -569,7 +725,7 @@ body {
569
  display: flex;
570
  align-items: center;
571
  justify-content: center;
572
- transition: background var(--transition), color var(--transition);
573
  }
574
 
575
  .rail-icon-btn:hover {
@@ -578,13 +734,15 @@ body {
578
  }
579
 
580
  .rail-logo {
581
- width: 34px; height: 34px;
 
582
  border-radius: 6px;
583
  object-fit: contain;
584
  }
585
 
586
  .rail-avatar {
587
- width: 28px; height: 28px;
 
588
  border-radius: 50%;
589
  object-fit: cover;
590
  background: var(--bg-hover);
@@ -605,7 +763,8 @@ body {
605
 
606
  .main-header {
607
  position: absolute;
608
- top: 0; left: 0;
 
609
  width: 100%;
610
  padding: 14px 20px;
611
  display: flex;
@@ -614,7 +773,8 @@ body {
614
  }
615
 
616
  .toggle-sidebar-btn {
617
- background: transparent; border: none;
 
618
  color: var(--text-secondary);
619
  cursor: pointer;
620
  display: flex;
@@ -651,7 +811,7 @@ body {
651
  align-items: center;
652
  justify-content: center;
653
  flex: 1;
654
- gap: 24px;
655
  user-select: none;
656
  padding: 40px 20px;
657
  }
@@ -660,28 +820,66 @@ body {
660
  width: 120px;
661
  height: 120px;
662
  object-fit: contain;
663
- animation: heroFloat 3s ease-in-out infinite;
664
- filter: drop-shadow(0 8px 24px rgba(235, 90, 40, 0.15));
665
- }
666
-
667
- @keyframes heroFloat {
668
- 0%, 100% { transform: translateY(0); }
669
- 50% { transform: translateY(-8px); }
670
  }
671
 
672
  .hero-title {
673
- font-size: 32px;
674
  font-weight: 700;
675
  color: var(--text-primary);
676
  text-align: center;
677
  line-height: 1.3;
678
  }
679
 
 
 
 
 
680
  .hero-input-wrap {
681
  width: 100%;
682
  max-width: 640px;
683
  }
684
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
685
  .hero-input-box {
686
  display: flex;
687
  align-items: flex-end;
@@ -689,28 +887,33 @@ body {
689
  border: 1.5px solid var(--border-color);
690
  border-radius: var(--radius-xl);
691
  transition: border-color 0.3s, box-shadow 0.3s;
692
- box-shadow: 0 2px 12px rgba(0,0,0,0.3);
693
  }
694
 
695
  .hero-input-box:focus-within {
696
- border-color: var(--brand-orange);
697
- box-shadow: 0 0 0 3px var(--brand-orange-glow), 0 4px 20px rgba(0,0,0,0.4);
698
  }
699
 
700
  .hero-input-box textarea {
701
  flex: 1;
702
- background: transparent; border: none;
 
703
  color: var(--text-primary);
704
  padding: 16px 12px;
705
  font-family: inherit;
706
  font-size: 15px;
707
- resize: none; outline: none;
708
- height: 54px; max-height: 120px;
 
 
709
  line-height: 1.5;
710
  overflow-y: hidden;
711
  }
712
 
713
- .hero-input-box textarea::placeholder { color: var(--text-muted); }
 
 
714
 
715
  .hero-pills {
716
  display: grid;
@@ -731,14 +934,14 @@ body {
731
  font-weight: 500;
732
  cursor: pointer;
733
  text-align: left;
734
- transition: background var(--transition), color var(--transition), border-color var(--transition), box-shadow var(--transition);
735
  }
736
 
737
  .hero-pill:hover {
738
  background: var(--bg-hover);
739
- border-color: var(--brand-orange);
740
  color: var(--text-primary);
741
- box-shadow: 0 0 0 1px var(--brand-orange-glow);
742
  }
743
 
744
 
@@ -754,8 +957,13 @@ body {
754
  animation: fadeIn 0.35s ease-out forwards;
755
  }
756
 
757
- .message-row.user { justify-content: flex-end; }
758
- .message-row.ai { justify-content: flex-start; }
 
 
 
 
 
759
 
760
  .message-content {
761
  padding: 14px 18px;
@@ -779,7 +987,8 @@ body {
779
  }
780
 
781
  .ai-avatar {
782
- width: 30px; height: 30px;
 
783
  border-radius: 50%;
784
  background-image: url('/assets/bot.png');
785
  background-size: cover;
@@ -794,8 +1003,17 @@ body {
794
  }
795
 
796
  @keyframes avatarPulse {
797
- 0%, 100% { opacity: 0.6; transform: scale(1); }
798
- 50% { opacity: 1; transform: scale(1.05); }
 
 
 
 
 
 
 
 
 
799
  }
800
 
801
 
@@ -809,12 +1027,8 @@ body {
809
  animation: fadeIn 0.3s ease-out;
810
  }
811
 
812
- .thinking-indicator.hidden { display: none; }
813
-
814
- .thinking-logo {
815
- width: 22px; height: 22px;
816
- border-radius: 50%;
817
- animation: pulse 1.5s ease-in-out infinite;
818
  }
819
 
820
  .thinking-text {
@@ -823,34 +1037,54 @@ body {
823
  font-style: italic;
824
  }
825
 
826
- @keyframes pulse {
827
- 0%, 100% { opacity: 0.5; transform: scale(1); }
828
- 50% { opacity: 1; transform: scale(1.08); }
829
- }
830
-
831
 
832
  /* --- Rendered markdown in AI messages --- */
833
 
834
- .message-content h1, .message-content h2, .message-content h3 {
835
- margin-top: 16px; margin-bottom: 8px;
 
 
 
836
  font-weight: 600;
837
  }
838
- .message-content h1 { font-size: 1.25em; }
839
- .message-content h2 { font-size: 1.12em; }
840
- .message-content h3 { font-size: 1.05em; }
841
- .message-content p { margin-bottom: 10px; }
842
 
843
- .message-content ul, .message-content ol { margin: 8px 0 8px 20px; }
844
- .message-content li { margin-bottom: 4px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
845
 
846
  .message-content code {
847
- background: #1e1e1e;
848
  padding: 2px 6px;
849
  border-radius: 4px;
850
  font-family: 'Courier New', monospace;
851
  font-size: 0.88em;
852
  }
853
 
 
 
 
 
854
  .message-content pre {
855
  background: #111;
856
  border: 1px solid var(--border-color);
@@ -860,26 +1094,49 @@ body {
860
  margin: 10px 0;
861
  }
862
 
863
- .message-content pre code { background: none; padding: 0; }
 
 
864
 
865
- .message-content table { border-collapse: collapse; margin: 10px 0; width: 100%; }
866
- .message-content th, .message-content td {
 
 
 
 
 
 
 
 
 
 
 
867
  border: 1px solid var(--border-color);
868
  padding: 8px 12px;
869
  text-align: left;
870
  font-size: 14px;
871
  }
872
- .message-content th { background: var(--bg-input); font-weight: 600; }
 
 
 
 
873
 
874
  .message-content blockquote {
875
- border-left: 3px solid var(--brand-orange);
876
  padding-left: 12px;
877
  margin: 10px 0;
878
  color: var(--text-secondary);
879
  }
880
 
881
- .katex { color: var(--text-primary); }
882
- .katex-display { margin: 14px 0; overflow-x: auto; }
 
 
 
 
 
 
883
 
884
  .message-image {
885
  max-width: 240px;
@@ -888,6 +1145,55 @@ body {
888
  }
889
 
890
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
891
  /* ============================================================
892
  INPUT AREA (bottom bar during chat)
893
  ============================================================ */
@@ -915,7 +1221,8 @@ body {
915
  }
916
 
917
  .image-preview-thumb {
918
- width: 48px; height: 48px;
 
919
  border-radius: 6px;
920
  background-size: cover;
921
  background-position: center;
@@ -923,7 +1230,8 @@ body {
923
 
924
  .image-preview-remove {
925
  margin-left: auto;
926
- background: none; border: none;
 
927
  color: var(--text-secondary);
928
  font-size: 20px;
929
  cursor: pointer;
@@ -931,10 +1239,13 @@ body {
931
  transition: color 0.2s;
932
  }
933
 
934
- .image-preview-remove:hover { color: #ff4a4a; }
 
 
935
 
936
  .input-box {
937
- width: 100%; max-width: 800px;
 
938
  position: relative;
939
  display: flex;
940
  align-items: flex-end;
@@ -942,16 +1253,17 @@ body {
942
  border: 1.5px solid var(--border-color);
943
  border-radius: var(--radius-xl);
944
  transition: border-color 0.3s, box-shadow 0.3s;
945
- box-shadow: 0 2px 12px rgba(0,0,0,0.3);
946
  }
947
 
948
  .input-box:focus-within {
949
- border-color: var(--brand-orange);
950
- box-shadow: 0 0 0 3px var(--brand-orange-glow), 0 4px 20px rgba(0,0,0,0.4);
951
  }
952
 
953
  .upload-btn {
954
- background: none; border: none;
 
955
  color: var(--text-secondary);
956
  cursor: pointer;
957
  padding: 16px 4px 16px 16px;
@@ -960,25 +1272,175 @@ body {
960
  transition: color 0.2s;
961
  }
962
 
963
- .upload-btn:hover { color: var(--text-primary); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
964
 
965
  #userInput {
966
  flex: 1;
967
- background: transparent; border: none;
 
968
  color: var(--text-primary);
969
- padding: 16px 12px 16px 16px;
970
  font-family: inherit;
971
  font-size: 14px;
972
- resize: none; outline: none;
973
- height: 54px; max-height: 200px;
 
 
974
  line-height: 1.5;
975
  overflow-y: hidden;
976
  }
977
 
978
- #userInput::placeholder { color: var(--text-muted); }
 
 
979
 
980
  .send-btn {
981
- background: none; border: none;
 
982
  color: var(--text-secondary);
983
  cursor: pointer;
984
  padding: 16px 16px 16px 4px;
@@ -987,13 +1449,24 @@ body {
987
  transition: color 0.2s, transform 0.1s;
988
  }
989
 
990
- .send-btn:hover { color: var(--brand-orange); }
991
- .send-btn:active { transform: scale(0.92); }
992
- .send-btn svg { width: 20px; height: 20px; fill: currentColor; }
 
 
 
 
 
 
 
 
 
 
993
 
994
  .stop-btn {
995
- background: none; border: none;
996
- color: var(--brand-orange);
 
997
  cursor: pointer;
998
  padding: 16px 16px 16px 4px;
999
  display: none;
@@ -1001,15 +1474,27 @@ body {
1001
  transition: color 0.2s, transform 0.1s;
1002
  }
1003
 
1004
- .stop-btn.visible { display: flex; }
 
 
 
 
 
 
 
 
 
 
1005
 
1006
- .stop-btn:hover { color: #ff4a4a; }
1007
- .stop-btn:active { transform: scale(0.92); }
1008
- .stop-btn svg { width: 22px; height: 22px; fill: currentColor; }
 
 
1009
 
1010
  .disclaimer-text {
1011
  font-size: 11px;
1012
- color: rgba(255,255,255,0.5);
1013
  font-weight: 500;
1014
  margin-top: 10px;
1015
  text-align: center;
@@ -1024,13 +1509,16 @@ body {
1024
  display: none;
1025
  position: fixed;
1026
  inset: 0;
1027
- background: rgba(0,0,0,0.6);
 
1028
  z-index: 500;
1029
  align-items: center;
1030
  justify-content: center;
1031
  }
1032
 
1033
- .modal-overlay.show { display: flex; }
 
 
1034
 
1035
  .settings-modal {
1036
  background: var(--bg-sidebar);
@@ -1041,7 +1529,7 @@ body {
1041
  height: 520px;
1042
  max-height: 88vh;
1043
  overflow: hidden;
1044
- box-shadow: 0 8px 40px rgba(0,0,0,0.6);
1045
  display: flex;
1046
  flex-direction: column;
1047
  }
@@ -1055,10 +1543,14 @@ body {
1055
  flex-shrink: 0;
1056
  }
1057
 
1058
- .modal-header h2 { font-size: 18px; font-weight: 600; }
 
 
 
1059
 
1060
  .modal-close {
1061
- background: none; border: none;
 
1062
  color: var(--text-secondary);
1063
  font-size: 24px;
1064
  cursor: pointer;
@@ -1066,7 +1558,9 @@ body {
1066
  transition: color 0.2s;
1067
  }
1068
 
1069
- .modal-close:hover { color: var(--text-primary); }
 
 
1070
 
1071
  .modal-body {
1072
  padding: 0;
@@ -1103,13 +1597,21 @@ body {
1103
  font-weight: 500;
1104
  cursor: pointer;
1105
  border-radius: 8px;
1106
- transition: background var(--transition), color var(--transition);
1107
  text-align: left;
1108
  width: 100%;
1109
  }
1110
 
1111
- .settings-nav-btn:hover { background: var(--bg-hover); color: var(--text-primary); }
1112
- .settings-nav-btn.active { background: var(--bg-hover); color: var(--text-primary); font-weight: 600; }
 
 
 
 
 
 
 
 
1113
 
1114
  .settings-panel {
1115
  flex: 1;
@@ -1118,8 +1620,13 @@ body {
1118
  min-width: 0;
1119
  }
1120
 
1121
- .tab-content { display: none; }
1122
- .tab-content.active { display: block; }
 
 
 
 
 
1123
 
1124
  .settings-section-title {
1125
  font-size: 14px;
@@ -1128,7 +1635,8 @@ body {
1128
  margin-bottom: 10px;
1129
  }
1130
 
1131
- .settings-input, .settings-textarea {
 
1132
  width: 100%;
1133
  padding: 11px 14px;
1134
  background: var(--bg-input);
@@ -1141,8 +1649,15 @@ body {
1141
  transition: border-color 0.2s;
1142
  }
1143
 
1144
- .settings-input:focus, .settings-textarea:focus { border-color: var(--brand-orange); }
1145
- .settings-input::placeholder, .settings-textarea::placeholder { color: var(--text-muted); }
 
 
 
 
 
 
 
1146
 
1147
  .settings-textarea {
1148
  resize: vertical;
@@ -1169,13 +1684,19 @@ body {
1169
  transition: border-color 0.2s;
1170
  }
1171
 
1172
- .settings-select:focus { border-color: var(--brand-orange); }
1173
- .settings-select option { background: var(--bg-input); color: var(--text-primary); }
 
 
 
 
 
 
1174
 
1175
  .settings-save-btn {
1176
  margin-top: 12px;
1177
  padding: 10px 24px;
1178
- background: var(--brand-orange);
1179
  color: #fff;
1180
  border: none;
1181
  border-radius: 8px;
@@ -1183,10 +1704,12 @@ body {
1183
  font-size: 13px;
1184
  font-weight: 600;
1185
  cursor: pointer;
1186
- transition: background var(--transition);
1187
  }
1188
 
1189
- .settings-save-btn:hover { background: var(--brand-orange-hover); }
 
 
1190
 
1191
  .settings-hint {
1192
  font-size: 12px;
@@ -1195,8 +1718,14 @@ body {
1195
  line-height: 1.5;
1196
  }
1197
 
1198
- .settings-hint a { color: var(--brand-orange); text-decoration: none; }
1199
- .settings-hint a:hover { text-decoration: underline; }
 
 
 
 
 
 
1200
 
1201
  .persona-option {
1202
  display: flex;
@@ -1210,11 +1739,26 @@ body {
1210
  transition: border-color 0.2s, background 0.2s;
1211
  }
1212
 
1213
- .persona-option:hover { background: var(--bg-hover); }
1214
- .persona-option.active { border-color: var(--brand-orange); background: rgba(235,90,40,0.06); }
 
 
 
 
 
 
 
 
 
 
 
 
1215
 
1216
- .persona-name { font-size: 14px; font-weight: 600; color: var(--text-primary); }
1217
- .persona-desc { font-size: 12px; color: var(--text-secondary); line-height: 1.5; }
 
 
 
1218
 
1219
 
1220
  /* ============================================================
@@ -1222,23 +1766,54 @@ body {
1222
  ============================================================ */
1223
 
1224
  @keyframes fadeIn {
1225
- from { opacity: 0; transform: translateY(8px); }
1226
- to { opacity: 1; transform: translateY(0); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1227
  }
1228
 
1229
- ::-webkit-scrollbar { width: 5px; }
1230
- ::-webkit-scrollbar-track { background: transparent; }
1231
- ::-webkit-scrollbar-thumb { background: #333; border-radius: 4px; }
1232
- ::-webkit-scrollbar-thumb:hover { background: #555; }
 
 
 
 
 
 
 
 
 
 
 
 
1233
 
1234
  .cursor::after {
1235
  content: '\25CB';
1236
  animation: blink 1s step-start infinite;
1237
- color: var(--brand-orange);
1238
  margin-left: 2px;
1239
  }
1240
 
1241
- @keyframes blink { 50% { opacity: 0; } }
 
 
 
 
1242
 
1243
 
1244
  /* ============================================================
@@ -1249,7 +1824,7 @@ body {
1249
  color: #ff6b6b;
1250
  font-style: italic;
1251
  padding: 10px 14px;
1252
- background: rgba(255,75,75,0.08);
1253
  border-radius: 8px;
1254
  border-left: 3px solid #ff4a4a;
1255
  }
@@ -1259,34 +1834,38 @@ body {
1259
  RESPONSIVE — MOBILE
1260
  ============================================================ */
1261
 
1262
- .mobile-fab-toggle { display: none !important; }
 
 
1263
 
1264
- /* Sidebar overlay — mobile only (hidden on desktop) */
1265
  .sidebar-overlay {
1266
  display: none;
1267
  position: fixed;
1268
  inset: 0;
1269
- background: rgba(0,0,0,0.55);
1270
  z-index: 35;
1271
  -webkit-tap-highlight-color: transparent;
1272
  }
1273
- .sidebar-overlay.visible { display: block; }
 
 
 
1274
 
1275
  @media (max-width: 768px) {
1276
- /* App container */
1277
  .app-container {
1278
  overflow: hidden;
1279
  position: relative;
1280
  }
1281
 
1282
- /* Sidebar — fixed overlay on mobile */
1283
  .sidebar {
1284
- position: fixed; left: 0; top: 0;
 
 
1285
  width: var(--sidebar-width);
1286
  height: 100vh;
1287
  height: 100dvh;
1288
  z-index: 40;
1289
- transition: transform var(--transition), opacity var(--transition);
1290
  margin-left: 0 !important;
1291
  }
1292
 
@@ -1302,16 +1881,16 @@ body {
1302
  pointer-events: auto;
1303
  }
1304
 
1305
- .sidebar-rail { display: none !important; }
 
 
1306
 
1307
- /* Main chat fills full screen */
1308
  .main-chat {
1309
  width: 100vw;
1310
  min-width: 0;
1311
  flex: 1;
1312
  }
1313
 
1314
- /* Sidebar logo bigger on mobile */
1315
  .sidebar-logo {
1316
  height: 36px;
1317
  }
@@ -1320,7 +1899,6 @@ body {
1320
  padding: 20px 18px 10px;
1321
  }
1322
 
1323
- /* Settings modal */
1324
  .settings-modal {
1325
  width: 96vw;
1326
  height: 78vh;
@@ -1353,20 +1931,38 @@ body {
1353
  border-radius: 10px;
1354
  white-space: normal;
1355
  }
 
1356
  .settings-nav-btn span,
1357
- .settings-nav-btn > :not(svg) { display: none; }
1358
- .settings-nav-btn svg { width: 18px; height: 18px; flex-shrink: 0; }
 
 
 
 
 
 
 
 
1359
  .settings-nav-btn.active {
1360
  background: var(--bg-elevated);
1361
- color: var(--brand-orange);
 
 
 
 
 
 
1362
  }
1363
 
1364
- .settings-panel { padding: 14px 12px; flex: 1; min-width: 0; }
 
 
1365
 
1366
- .hero-title { font-size: 22px; }
1367
- .hero-bot-icon { width: 80px; height: 80px; }
 
 
1368
 
1369
- /* Hero welcome — scrollable so pills don't go off-screen */
1370
  .hero-welcome {
1371
  justify-content: flex-start;
1372
  padding: 20px 16px calc(20px + env(safe-area-inset-bottom));
@@ -1374,14 +1970,24 @@ body {
1374
  min-height: 0;
1375
  }
1376
 
1377
- .hero-pills { grid-template-columns: 1fr; }
1378
- .hero-pill { font-size: 12px; padding: 12px 14px; }
 
1379
 
1380
- .hero-input-wrap { max-width: 100%; }
 
 
 
1381
 
1382
- .login-title { font-size: 20px; white-space: normal; }
 
 
 
 
 
 
 
1383
 
1384
- /* Chat container */
1385
  .chat-container {
1386
  padding: 60px 10px 16px;
1387
  gap: 16px;
@@ -1395,6 +2001,7 @@ body {
1395
  .ai-avatar {
1396
  display: none;
1397
  }
 
1398
  .ai .message-content {
1399
  font-size: 13px;
1400
  line-height: 1.55;
@@ -1406,10 +2013,6 @@ body {
1406
  font-size: 13px;
1407
  }
1408
 
1409
- .thinking-indicator .thinking-logo {
1410
- display: none;
1411
- }
1412
-
1413
  .input-container {
1414
  padding: 10px 10px calc(6px + env(safe-area-inset-bottom));
1415
  }
@@ -1419,17 +2022,46 @@ body {
1419
  margin-top: 6px;
1420
  }
1421
 
1422
- /* History items — always show options on mobile (no hover) */
1423
  .history-item .options-btn {
1424
  opacity: 0.6;
1425
  }
 
 
 
 
 
 
 
 
 
 
 
 
1426
  }
1427
 
1428
  @media (max-width: 480px) {
1429
- .hero-title { font-size: 20px; }
1430
- .hero-input-box textarea { font-size: 13px; }
1431
- .ai .message-content { font-size: 12.5px; }
1432
- .user .message-content { font-size: 12.5px; }
1433
- .katex-display { overflow-x: auto; font-size: 0.95em; }
1434
- .message-content pre { font-size: 11px; }
1435
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  /* ============================================================
2
  STEM Copilot — Stylesheet
3
+ Montserrat, dark/light theme, premium chat UI
4
  ============================================================ */
5
 
6
  :root {
7
+ --brand: #2231D4;
8
+ --brand-hover: #3367D6;
9
+ --brand-glow: rgba(66, 133, 244, 0.25);
10
  --bg-main: #000000;
11
  --bg-sidebar: #0a0a0a;
12
  --bg-input: #1a1a1a;
 
28
  --rail-width: 56px;
29
  }
30
 
31
+ /* ── LIGHT MODE ── */
32
+ [data-theme="light"] {
33
+ --bg-main: #f8f9fa;
34
+ --bg-sidebar: #ffffff;
35
+ --bg-input: #ffffff;
36
+ --bg-hover: #f0f0f0;
37
+ --bg-card: #ffffff;
38
+ --bg-elevated: #f5f5f5;
39
+ --text-primary: #1a1a1a;
40
+ --text-secondary: #555555;
41
+ --text-muted: #999999;
42
+ --border-color: #e0e0e0;
43
+ --border-subtle: #eeeeee;
44
+ --msg-user: #e8f0fe;
45
+ --msg-ai: transparent;
46
+ }
47
+
48
+ * {
49
+ box-sizing: border-box;
50
+ margin: 0;
51
+ padding: 0;
52
+ }
53
 
54
  html {
55
  -webkit-text-size-adjust: 100%;
 
68
  touch-action: manipulation;
69
  -webkit-tap-highlight-color: transparent;
70
  overscroll-behavior: none;
71
+ /* Smooth theme transitions */
72
+ transition: background-color 0.4s ease, color 0.4s ease;
73
+ }
74
+
75
+ /* Smooth transitions for all themed elements */
76
+ *,
77
+ *::before,
78
+ *::after {
79
+ transition: background-color 0.4s ease, color 0.4s ease, border-color 0.4s ease, box-shadow 0.4s ease;
80
+ }
81
+
82
+ /* Exclude specific properties that shouldn't transition globally */
83
+ textarea,
84
+ input,
85
+ select {
86
+ transition: background-color 0.4s ease, color 0.4s ease, border-color 0.3s ease, box-shadow 0.3s ease;
87
+ }
88
+
89
+
90
+ /* ============================================================
91
+ THEME TOGGLE BUTTON (top-right, all screens)
92
+ ============================================================ */
93
+
94
+ .theme-toggle-btn {
95
+ position: fixed;
96
+ top: 14px;
97
+ right: 16px;
98
+ z-index: 1100;
99
+ width: 36px;
100
+ height: 36px;
101
+ border-radius: 50%;
102
+ border: 1px solid var(--border-color);
103
+ background: var(--bg-elevated);
104
+ cursor: pointer;
105
+ display: flex;
106
+ align-items: center;
107
+ justify-content: center;
108
+ transition: background 0.3s, border-color 0.3s, transform 0.2s;
109
+ }
110
+
111
+ .theme-toggle-btn:hover {
112
+ background: var(--bg-hover);
113
+ border-color: var(--brand);
114
+ transform: scale(1.08);
115
+ }
116
+
117
+ .theme-toggle-btn svg {
118
+ width: 18px;
119
+ height: 18px;
120
+ stroke: var(--text-secondary);
121
+ fill: none;
122
+ stroke-width: 2;
123
+ transition: stroke 0.3s;
124
+ }
125
+
126
+ .theme-toggle-btn:hover svg {
127
+ stroke: var(--brand);
128
+ }
129
+
130
+ /* Show sun in dark, moon in light */
131
+ .theme-toggle-btn .icon-sun {
132
+ display: block;
133
+ }
134
+
135
+ .theme-toggle-btn .icon-moon {
136
+ display: none;
137
+ }
138
+
139
+ [data-theme="light"] .theme-toggle-btn .icon-sun {
140
+ display: none;
141
+ }
142
+
143
+ [data-theme="light"] .theme-toggle-btn .icon-moon {
144
+ display: block;
145
  }
146
 
147
 
 
165
  padding: 48px 36px;
166
  }
167
 
 
 
 
 
 
 
 
 
168
  .login-logo-stembotix {
169
  height: 48px;
170
  margin-bottom: 32px;
 
196
  display: inline-flex;
197
  align-items: center;
198
  gap: 12px;
199
+ background: var(--bg-input);
200
+ border: 1px solid var(--border-color);
201
  border-radius: 24px;
202
  padding: 12px 28px;
203
+ color: var(--text-primary);
204
  font-family: 'Montserrat', sans-serif;
205
  font-size: 13px;
206
  font-weight: 700;
207
  cursor: pointer;
208
+ transition: background 0.3s, border-color 0.3s, transform 0.1s;
209
  white-space: nowrap;
210
  }
211
 
212
  .gsi-custom-btn:hover {
213
+ background: var(--bg-hover);
214
+ border-color: var(--brand);
215
  }
216
 
217
  .gsi-custom-btn:active {
 
245
  padding: 48px 36px;
246
  }
247
 
248
+ .byok-card h2 {
249
+ font-size: 22px;
250
+ font-weight: 700;
251
+ margin-bottom: 12px;
252
+ }
253
 
254
+ .byok-desc {
255
+ font-size: 14px;
256
+ color: var(--text-secondary);
257
+ line-height: 1.6;
258
+ margin-bottom: 20px;
259
+ }
260
 
261
  .byok-steps {
262
  text-align: left;
 
267
  color: var(--text-secondary);
268
  }
269
 
270
+ .byok-steps a {
271
+ color: var(--brand);
272
+ text-decoration: none;
273
+ }
274
+
275
+ .byok-steps a:hover {
276
+ text-decoration: underline;
277
+ }
278
 
279
  .byok-submit-btn {
280
  width: 100%;
281
  padding: 14px;
282
  margin-top: 16px;
283
+ background: var(--brand);
284
  color: #fff;
285
  border: none;
286
  border-radius: var(--radius);
 
288
  font-size: 15px;
289
  font-weight: 600;
290
  cursor: pointer;
291
+ transition: background 0.3s;
292
  }
293
 
294
+ .byok-submit-btn:hover {
295
+ background: var(--brand-hover);
296
+ }
297
+
298
+ .byok-note {
299
+ font-size: 11px;
300
+ color: var(--text-muted);
301
+ margin-top: 12px;
302
+ }
303
 
304
 
305
  /* ============================================================
 
311
  bottom: calc(24px + env(safe-area-inset-bottom));
312
  left: 50%;
313
  transform: translateX(-50%);
314
+ z-index: 1050;
315
  background: var(--bg-elevated);
316
  border: 1px solid var(--border-color);
317
  border-radius: var(--radius-lg);
 
319
  display: flex;
320
  align-items: center;
321
  gap: 12px;
322
+ box-shadow: 0 12px 48px rgba(0, 0, 0, 0.5);
323
  max-width: 400px;
324
  width: calc(100% - 32px);
325
  animation: slideUp 0.4s cubic-bezier(0.16, 1, 0.3, 1);
326
  }
327
 
328
+ .install-banner-icon {
329
+ width: 36px;
330
+ height: 36px;
331
+ border-radius: 8px;
332
+ }
333
 
334
  .install-banner-text {
335
  flex: 1;
 
339
  line-height: 1.4;
340
  }
341
 
342
+ .install-banner-text strong {
343
+ color: var(--text-primary);
344
+ }
345
+
346
+ .install-banner-text span {
347
+ color: var(--text-secondary);
348
+ font-size: 11px;
349
+ }
350
 
351
  .install-banner-btn {
352
+ background: var(--brand);
353
  color: #fff;
354
  border: none;
355
  border-radius: 8px;
 
358
  font-size: 13px;
359
  font-weight: 600;
360
  cursor: pointer;
361
+ transition: background 0.3s;
362
  }
363
 
364
+ .install-banner-btn:hover {
365
+ background: var(--brand-hover);
366
+ }
367
 
368
  .install-banner-dismiss {
369
  background: none;
 
375
  }
376
 
377
  @keyframes slideUp {
378
+ from {
379
+ opacity: 0;
380
+ transform: translateX(-50%) translateY(20px);
381
+ }
382
+
383
+ to {
384
+ opacity: 1;
385
+ transform: translateX(-50%) translateY(0);
386
+ }
387
  }
388
 
389
 
 
410
  display: flex;
411
  flex-direction: column;
412
  border-right: 1px solid var(--border-color);
413
+ transition: margin-left 0.25s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.25s cubic-bezier(0.4, 0, 0.2, 1);
414
  position: relative;
415
  z-index: 20;
416
  }
 
442
  .new-chat-btn {
443
  margin: 8px 14px;
444
  padding: 12px 16px;
445
+ background: linear-gradient(135deg, rgba(66, 133, 244, 0.08), rgba(66, 133, 244, 0.02));
446
+ border: 1px solid rgba(66, 133, 244, 0.2);
447
  color: var(--text-primary);
448
  border-radius: var(--radius);
449
  cursor: pointer;
 
453
  font-size: 14px;
454
  font-weight: 600;
455
  font-family: inherit;
456
+ transition: background 0.3s, border-color 0.3s, box-shadow 0.3s;
457
  }
458
 
459
  .new-chat-btn:hover {
460
+ background: linear-gradient(135deg, rgba(66, 133, 244, 0.15), rgba(66, 133, 244, 0.05));
461
+ border-color: rgba(66, 133, 244, 0.4);
462
+ box-shadow: 0 0 16px rgba(66, 133, 244, 0.1);
463
  }
464
 
465
  .new-chat-btn svg {
466
+ color: var(--brand);
467
  }
468
 
469
  .chat-history {
 
485
  display: flex;
486
  align-items: center;
487
  gap: 10px;
488
+ transition: background 0.2s, color 0.2s, transform 0.15s;
489
  }
490
 
491
  .history-item:hover {
 
498
  }
499
 
500
  .history-item.active {
501
+ background: linear-gradient(135deg, rgba(66, 133, 244, 0.1), rgba(66, 133, 244, 0.03));
502
  color: var(--text-primary);
503
+ border: 1px solid rgba(66, 133, 244, 0.15);
504
  }
505
 
506
  .history-item:not(.active) {
 
516
 
517
  .history-item.active .thread-icon {
518
  opacity: 0.8;
519
+ color: var(--brand);
520
  }
521
 
522
  .chat-title {
 
527
  }
528
 
529
  .options-btn {
530
+ background: none;
531
+ border: none;
532
  color: var(--text-secondary);
533
+ font-size: 18px;
534
+ line-height: 1;
535
+ cursor: pointer;
536
+ opacity: 0;
537
  padding: 0 4px;
538
  flex-shrink: 0;
539
  transition: opacity 0.2s, color 0.2s;
540
  }
541
 
542
  .history-item:hover .options-btn,
543
+ .options-btn.menu-open {
544
+ opacity: 1;
545
+ }
546
+
547
+ .options-btn:hover {
548
+ color: var(--text-primary);
549
+ }
550
 
551
  .options-menu {
552
  position: absolute;
553
+ right: 10px;
554
+ top: 40px;
555
  background: var(--bg-elevated);
556
  border: 1px solid var(--border-color);
557
  border-radius: 10px;
558
  display: none;
559
  flex-direction: column;
560
  z-index: 100;
561
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
562
  min-width: 130px;
563
  overflow: hidden;
564
  }
565
 
566
+ .options-menu.show {
567
+ display: flex;
568
+ }
569
 
570
  .option-item {
571
  padding: 10px 14px;
 
578
  transition: background 0.2s;
579
  }
580
 
581
+ .option-item:hover {
582
+ background: var(--bg-hover);
583
+ }
584
+
585
+ .option-item.delete {
586
+ color: #ff4a4a;
587
+ }
588
 
589
  .rename-input {
590
  width: 100%;
591
+ background: transparent;
592
+ border: none;
593
  color: var(--text-primary);
594
+ font-family: inherit;
595
+ font-size: 13px;
596
  outline: none;
597
+ border-bottom: 1px solid var(--brand);
598
  padding: 2px 0;
599
  }
600
 
601
 
602
+ /* --- Sidebar bottom — user profile --- */
603
 
604
  .sidebar-bottom {
605
  padding: 12px 14px 16px;
606
  border-top: 1px solid var(--border-color);
607
  position: relative;
608
+ background: linear-gradient(180deg, transparent, rgba(66, 133, 244, 0.02));
609
  }
610
 
611
  .user-profile-btn {
 
615
  padding: 10px 12px;
616
  border-radius: 12px;
617
  cursor: pointer;
618
+ transition: background 0.2s, box-shadow 0.2s;
619
  border: 1px solid transparent;
620
  }
621
 
622
  .user-profile-btn:hover {
623
  background: var(--bg-hover);
624
  border-color: var(--border-color);
625
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.2);
626
  }
627
 
628
  .user-avatar {
629
+ width: 34px;
630
+ height: 34px;
631
  border-radius: 50%;
632
  object-fit: cover;
633
  background: var(--bg-hover);
634
  border: 2px solid var(--border-color);
635
+ transition: border-color 0.2s;
636
  }
637
 
638
  .user-profile-btn:hover .user-avatar {
639
+ border-color: var(--brand);
640
  }
641
 
642
  .user-name {
 
648
  text-overflow: ellipsis;
649
  }
650
 
 
 
651
  .user-menu {
652
  display: none;
653
  position: absolute;
654
+ bottom: 72px;
655
+ left: 14px;
656
+ right: 14px;
657
  background: var(--bg-elevated);
658
  border: 1px solid var(--border-color);
659
  border-radius: var(--radius);
660
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
661
  overflow: hidden;
662
  z-index: 50;
663
  }
664
 
665
+ .user-menu.show {
666
+ display: block;
667
+ }
668
 
669
  .user-menu-item {
670
  padding: 12px 16px;
 
676
  transition: background 0.2s;
677
  }
678
 
679
+ .user-menu-item:hover {
680
+ background: var(--bg-hover);
681
+ }
682
+
683
+ .user-menu-logout {
684
+ color: #ff4a4a;
685
+ }
686
 
687
 
688
  /* ============================================================
 
706
  display: flex;
707
  }
708
 
709
+ .rail-top,
710
+ .rail-bottom {
711
  display: flex;
712
  flex-direction: column;
713
  align-items: center;
 
715
  }
716
 
717
  .rail-icon-btn {
718
+ width: 40px;
719
+ height: 40px;
720
  border-radius: 10px;
721
  background: transparent;
722
  border: none;
 
725
  display: flex;
726
  align-items: center;
727
  justify-content: center;
728
+ transition: background 0.2s, color 0.2s;
729
  }
730
 
731
  .rail-icon-btn:hover {
 
734
  }
735
 
736
  .rail-logo {
737
+ width: 34px;
738
+ height: 34px;
739
  border-radius: 6px;
740
  object-fit: contain;
741
  }
742
 
743
  .rail-avatar {
744
+ width: 28px;
745
+ height: 28px;
746
  border-radius: 50%;
747
  object-fit: cover;
748
  background: var(--bg-hover);
 
763
 
764
  .main-header {
765
  position: absolute;
766
+ top: 0;
767
+ left: 0;
768
  width: 100%;
769
  padding: 14px 20px;
770
  display: flex;
 
773
  }
774
 
775
  .toggle-sidebar-btn {
776
+ background: transparent;
777
+ border: none;
778
  color: var(--text-secondary);
779
  cursor: pointer;
780
  display: flex;
 
811
  align-items: center;
812
  justify-content: center;
813
  flex: 1;
814
+ gap: 20px;
815
  user-select: none;
816
  padding: 40px 20px;
817
  }
 
820
  width: 120px;
821
  height: 120px;
822
  object-fit: contain;
823
+ filter: drop-shadow(0 8px 24px rgba(66, 133, 244, 0.15));
824
+ /* NO animation — fixed, no jumping */
 
 
 
 
 
825
  }
826
 
827
  .hero-title {
828
+ font-size: 28px;
829
  font-weight: 700;
830
  color: var(--text-primary);
831
  text-align: center;
832
  line-height: 1.3;
833
  }
834
 
835
+ .hero-title .hero-name {
836
+ color: var(--brand);
837
+ }
838
+
839
  .hero-input-wrap {
840
  width: 100%;
841
  max-width: 640px;
842
  }
843
 
844
+ /* Image preview inside hero */
845
+ .hero-image-preview {
846
+ display: none;
847
+ align-items: center;
848
+ gap: 8px;
849
+ margin-bottom: 8px;
850
+ padding: 6px 10px;
851
+ background: var(--bg-card);
852
+ border: 1px solid var(--border-color);
853
+ border-radius: var(--radius);
854
+ }
855
+
856
+ .hero-image-preview.visible {
857
+ display: flex;
858
+ }
859
+
860
+ .hero-image-preview-thumb {
861
+ width: 48px;
862
+ height: 48px;
863
+ border-radius: 6px;
864
+ background-size: cover;
865
+ background-position: center;
866
+ }
867
+
868
+ .hero-image-preview-remove {
869
+ margin-left: auto;
870
+ background: none;
871
+ border: none;
872
+ color: var(--text-secondary);
873
+ font-size: 20px;
874
+ cursor: pointer;
875
+ padding: 4px 8px;
876
+ transition: color 0.2s;
877
+ }
878
+
879
+ .hero-image-preview-remove:hover {
880
+ color: #ff4a4a;
881
+ }
882
+
883
  .hero-input-box {
884
  display: flex;
885
  align-items: flex-end;
 
887
  border: 1.5px solid var(--border-color);
888
  border-radius: var(--radius-xl);
889
  transition: border-color 0.3s, box-shadow 0.3s;
890
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
891
  }
892
 
893
  .hero-input-box:focus-within {
894
+ border-color: var(--brand);
895
+ box-shadow: 0 0 0 3px var(--brand-glow), 0 4px 20px rgba(0, 0, 0, 0.2);
896
  }
897
 
898
  .hero-input-box textarea {
899
  flex: 1;
900
+ background: transparent;
901
+ border: none;
902
  color: var(--text-primary);
903
  padding: 16px 12px;
904
  font-family: inherit;
905
  font-size: 15px;
906
+ resize: none;
907
+ outline: none;
908
+ height: 54px;
909
+ max-height: 120px;
910
  line-height: 1.5;
911
  overflow-y: hidden;
912
  }
913
 
914
+ .hero-input-box textarea::placeholder {
915
+ color: var(--text-muted);
916
+ }
917
 
918
  .hero-pills {
919
  display: grid;
 
934
  font-weight: 500;
935
  cursor: pointer;
936
  text-align: left;
937
+ transition: background 0.2s, color 0.2s, border-color 0.2s, box-shadow 0.2s;
938
  }
939
 
940
  .hero-pill:hover {
941
  background: var(--bg-hover);
942
+ border-color: var(--brand);
943
  color: var(--text-primary);
944
+ box-shadow: 0 0 0 1px var(--brand-glow);
945
  }
946
 
947
 
 
957
  animation: fadeIn 0.35s ease-out forwards;
958
  }
959
 
960
+ .message-row.user {
961
+ justify-content: flex-end;
962
+ }
963
+
964
+ .message-row.ai {
965
+ justify-content: flex-start;
966
+ }
967
 
968
  .message-content {
969
  padding: 14px 18px;
 
987
  }
988
 
989
  .ai-avatar {
990
+ width: 30px;
991
+ height: 30px;
992
  border-radius: 50%;
993
  background-image: url('/assets/bot.png');
994
  background-size: cover;
 
1003
  }
1004
 
1005
  @keyframes avatarPulse {
1006
+
1007
+ 0%,
1008
+ 100% {
1009
+ opacity: 0.6;
1010
+ transform: scale(1);
1011
+ }
1012
+
1013
+ 50% {
1014
+ opacity: 1;
1015
+ transform: scale(1.05);
1016
+ }
1017
  }
1018
 
1019
 
 
1027
  animation: fadeIn 0.3s ease-out;
1028
  }
1029
 
1030
+ .thinking-indicator.hidden {
1031
+ display: none;
 
 
 
 
1032
  }
1033
 
1034
  .thinking-text {
 
1037
  font-style: italic;
1038
  }
1039
 
 
 
 
 
 
1040
 
1041
  /* --- Rendered markdown in AI messages --- */
1042
 
1043
+ .message-content h1,
1044
+ .message-content h2,
1045
+ .message-content h3 {
1046
+ margin-top: 16px;
1047
+ margin-bottom: 8px;
1048
  font-weight: 600;
1049
  }
 
 
 
 
1050
 
1051
+ .message-content h1 {
1052
+ font-size: 1.25em;
1053
+ }
1054
+
1055
+ .message-content h2 {
1056
+ font-size: 1.12em;
1057
+ }
1058
+
1059
+ .message-content h3 {
1060
+ font-size: 1.05em;
1061
+ }
1062
+
1063
+ .message-content p {
1064
+ margin-bottom: 10px;
1065
+ }
1066
+
1067
+ .message-content ul,
1068
+ .message-content ol {
1069
+ margin: 8px 0 8px 20px;
1070
+ }
1071
+
1072
+ .message-content li {
1073
+ margin-bottom: 4px;
1074
+ }
1075
 
1076
  .message-content code {
1077
+ background: rgba(0, 0, 0, 0.15);
1078
  padding: 2px 6px;
1079
  border-radius: 4px;
1080
  font-family: 'Courier New', monospace;
1081
  font-size: 0.88em;
1082
  }
1083
 
1084
+ [data-theme="light"] .message-content code {
1085
+ background: rgba(0, 0, 0, 0.06);
1086
+ }
1087
+
1088
  .message-content pre {
1089
  background: #111;
1090
  border: 1px solid var(--border-color);
 
1094
  margin: 10px 0;
1095
  }
1096
 
1097
+ [data-theme="light"] .message-content pre {
1098
+ background: #f5f5f5;
1099
+ }
1100
 
1101
+ .message-content pre code {
1102
+ background: none;
1103
+ padding: 0;
1104
+ }
1105
+
1106
+ .message-content table {
1107
+ border-collapse: collapse;
1108
+ margin: 10px 0;
1109
+ width: 100%;
1110
+ }
1111
+
1112
+ .message-content th,
1113
+ .message-content td {
1114
  border: 1px solid var(--border-color);
1115
  padding: 8px 12px;
1116
  text-align: left;
1117
  font-size: 14px;
1118
  }
1119
+
1120
+ .message-content th {
1121
+ background: var(--bg-input);
1122
+ font-weight: 600;
1123
+ }
1124
 
1125
  .message-content blockquote {
1126
+ border-left: 3px solid var(--brand);
1127
  padding-left: 12px;
1128
  margin: 10px 0;
1129
  color: var(--text-secondary);
1130
  }
1131
 
1132
+ .katex {
1133
+ color: var(--text-primary);
1134
+ }
1135
+
1136
+ .katex-display {
1137
+ margin: 14px 0;
1138
+ overflow-x: auto;
1139
+ }
1140
 
1141
  .message-image {
1142
  max-width: 240px;
 
1145
  }
1146
 
1147
 
1148
+ /* ============================================================
1149
+ AI MESSAGE ACTION BAR (copy, regenerate)
1150
+ ============================================================ */
1151
+
1152
+ .msg-actions {
1153
+ display: flex;
1154
+ gap: 6px;
1155
+ margin-top: 8px;
1156
+ padding-top: 6px;
1157
+ opacity: 0;
1158
+ transition: opacity 0.2s;
1159
+ }
1160
+
1161
+ .message-row.ai:hover .msg-actions,
1162
+ .msg-actions.visible {
1163
+ opacity: 1;
1164
+ }
1165
+
1166
+ .msg-action-btn {
1167
+ background: var(--bg-card);
1168
+ border: 1px solid var(--border-color);
1169
+ border-radius: 6px;
1170
+ padding: 4px 10px;
1171
+ font-size: 11px;
1172
+ font-family: inherit;
1173
+ font-weight: 500;
1174
+ color: var(--text-secondary);
1175
+ cursor: pointer;
1176
+ display: inline-flex;
1177
+ align-items: center;
1178
+ gap: 5px;
1179
+ transition: background 0.2s, color 0.2s, border-color 0.2s;
1180
+ }
1181
+
1182
+ .msg-action-btn:hover {
1183
+ background: var(--bg-hover);
1184
+ color: var(--text-primary);
1185
+ border-color: var(--brand);
1186
+ }
1187
+
1188
+ .msg-action-btn svg {
1189
+ width: 13px;
1190
+ height: 13px;
1191
+ stroke: currentColor;
1192
+ fill: none;
1193
+ stroke-width: 2;
1194
+ }
1195
+
1196
+
1197
  /* ============================================================
1198
  INPUT AREA (bottom bar during chat)
1199
  ============================================================ */
 
1221
  }
1222
 
1223
  .image-preview-thumb {
1224
+ width: 48px;
1225
+ height: 48px;
1226
  border-radius: 6px;
1227
  background-size: cover;
1228
  background-position: center;
 
1230
 
1231
  .image-preview-remove {
1232
  margin-left: auto;
1233
+ background: none;
1234
+ border: none;
1235
  color: var(--text-secondary);
1236
  font-size: 20px;
1237
  cursor: pointer;
 
1239
  transition: color 0.2s;
1240
  }
1241
 
1242
+ .image-preview-remove:hover {
1243
+ color: #ff4a4a;
1244
+ }
1245
 
1246
  .input-box {
1247
+ width: 100%;
1248
+ max-width: 800px;
1249
  position: relative;
1250
  display: flex;
1251
  align-items: flex-end;
 
1253
  border: 1.5px solid var(--border-color);
1254
  border-radius: var(--radius-xl);
1255
  transition: border-color 0.3s, box-shadow 0.3s;
1256
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
1257
  }
1258
 
1259
  .input-box:focus-within {
1260
+ border-color: var(--brand);
1261
+ box-shadow: 0 0 0 3px var(--brand-glow), 0 4px 20px rgba(0, 0, 0, 0.2);
1262
  }
1263
 
1264
  .upload-btn {
1265
+ background: none;
1266
+ border: none;
1267
  color: var(--text-secondary);
1268
  cursor: pointer;
1269
  padding: 16px 4px 16px 16px;
 
1272
  transition: color 0.2s;
1273
  }
1274
 
1275
+ .upload-btn:hover {
1276
+ color: var(--text-primary);
1277
+ }
1278
+
1279
+ /* Teaching style selector in input bar */
1280
+ .style-selector-wrap {
1281
+ position: relative;
1282
+ display: flex;
1283
+ align-items: center;
1284
+ }
1285
+
1286
+ .style-selector-btn {
1287
+ background: none;
1288
+ border: 1px solid var(--border-color);
1289
+ border-radius: 16px;
1290
+ padding: 6px 10px;
1291
+ font-size: 11px;
1292
+ font-family: inherit;
1293
+ font-weight: 600;
1294
+ color: var(--text-secondary);
1295
+ cursor: pointer;
1296
+ display: flex;
1297
+ align-items: center;
1298
+ gap: 4px;
1299
+ white-space: nowrap;
1300
+ transition: background 0.2s, color 0.2s, border-color 0.2s;
1301
+ margin-bottom: 12px;
1302
+ }
1303
+
1304
+ .style-selector-btn:hover {
1305
+ background: var(--bg-hover);
1306
+ color: var(--text-primary);
1307
+ border-color: var(--brand);
1308
+ }
1309
+
1310
+ .style-selector-btn svg {
1311
+ width: 12px;
1312
+ height: 12px;
1313
+ stroke: currentColor;
1314
+ fill: none;
1315
+ stroke-width: 2;
1316
+ transition: transform 0.2s;
1317
+ }
1318
+
1319
+ .style-selector-btn.open svg {
1320
+ transform: rotate(180deg);
1321
+ }
1322
+
1323
+ .style-dropdown {
1324
+ display: none;
1325
+ position: absolute;
1326
+ bottom: 44px;
1327
+ left: 0;
1328
+ background: var(--bg-elevated);
1329
+ border: 1px solid var(--border-color);
1330
+ border-radius: var(--radius);
1331
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
1332
+ z-index: 100;
1333
+ min-width: 240px;
1334
+ overflow: hidden;
1335
+ animation: fadeIn 0.15s ease-out;
1336
+ }
1337
+
1338
+ .style-dropdown.show {
1339
+ display: block;
1340
+ }
1341
+
1342
+ .style-option {
1343
+ padding: 10px 14px;
1344
+ cursor: pointer;
1345
+ transition: background 0.15s;
1346
+ border-bottom: 1px solid var(--border-subtle);
1347
+ }
1348
+
1349
+ .style-option:last-child {
1350
+ border-bottom: none;
1351
+ }
1352
+
1353
+ .style-option:hover {
1354
+ background: var(--bg-hover);
1355
+ }
1356
+
1357
+ .style-option.active {
1358
+ background: rgba(66, 133, 244, 0.08);
1359
+ }
1360
+
1361
+ .style-option-name {
1362
+ font-size: 13px;
1363
+ font-weight: 600;
1364
+ color: var(--text-primary);
1365
+ margin-bottom: 2px;
1366
+ }
1367
+
1368
+ .style-option.active .style-option-name {
1369
+ color: var(--brand);
1370
+ }
1371
+
1372
+ .style-option-desc {
1373
+ font-size: 11px;
1374
+ color: var(--text-muted);
1375
+ line-height: 1.4;
1376
+ }
1377
+
1378
+ /* Voice mic button */
1379
+ .mic-btn {
1380
+ background: none;
1381
+ border: none;
1382
+ color: var(--text-secondary);
1383
+ cursor: pointer;
1384
+ padding: 16px 4px;
1385
+ display: flex;
1386
+ align-items: center;
1387
+ transition: color 0.2s;
1388
+ }
1389
+
1390
+ .mic-btn:hover {
1391
+ color: var(--text-primary);
1392
+ }
1393
+
1394
+ .mic-btn.listening {
1395
+ color: #EA4335 !important;
1396
+ animation: micPulse 1.5s infinite ease-in-out;
1397
+ }
1398
+
1399
+ @keyframes micPulse {
1400
+
1401
+ 0%,
1402
+ 100% {
1403
+ transform: scale(1);
1404
+ opacity: 1;
1405
+ }
1406
+
1407
+ 50% {
1408
+ transform: scale(1.15);
1409
+ opacity: 0.8;
1410
+ }
1411
+ }
1412
+
1413
+ .mic-btn svg {
1414
+ width: 20px;
1415
+ height: 20px;
1416
+ stroke: currentColor;
1417
+ fill: none;
1418
+ stroke-width: 2;
1419
+ }
1420
 
1421
  #userInput {
1422
  flex: 1;
1423
+ background: transparent;
1424
+ border: none;
1425
  color: var(--text-primary);
1426
+ padding: 16px 12px 16px 8px;
1427
  font-family: inherit;
1428
  font-size: 14px;
1429
+ resize: none;
1430
+ outline: none;
1431
+ height: 54px;
1432
+ max-height: 200px;
1433
  line-height: 1.5;
1434
  overflow-y: hidden;
1435
  }
1436
 
1437
+ #userInput::placeholder {
1438
+ color: var(--text-muted);
1439
+ }
1440
 
1441
  .send-btn {
1442
+ background: none;
1443
+ border: none;
1444
  color: var(--text-secondary);
1445
  cursor: pointer;
1446
  padding: 16px 16px 16px 4px;
 
1449
  transition: color 0.2s, transform 0.1s;
1450
  }
1451
 
1452
+ .send-btn:hover {
1453
+ color: var(--brand);
1454
+ }
1455
+
1456
+ .send-btn:active {
1457
+ transform: scale(0.92);
1458
+ }
1459
+
1460
+ .send-btn svg {
1461
+ width: 20px;
1462
+ height: 20px;
1463
+ fill: currentColor;
1464
+ }
1465
 
1466
  .stop-btn {
1467
+ background: none;
1468
+ border: none;
1469
+ color: var(--brand);
1470
  cursor: pointer;
1471
  padding: 16px 16px 16px 4px;
1472
  display: none;
 
1474
  transition: color 0.2s, transform 0.1s;
1475
  }
1476
 
1477
+ .stop-btn.visible {
1478
+ display: flex;
1479
+ }
1480
+
1481
+ .stop-btn:hover {
1482
+ color: #ff4a4a;
1483
+ }
1484
+
1485
+ .stop-btn:active {
1486
+ transform: scale(0.92);
1487
+ }
1488
 
1489
+ .stop-btn svg {
1490
+ width: 22px;
1491
+ height: 22px;
1492
+ fill: currentColor;
1493
+ }
1494
 
1495
  .disclaimer-text {
1496
  font-size: 11px;
1497
+ color: var(--text-muted);
1498
  font-weight: 500;
1499
  margin-top: 10px;
1500
  text-align: center;
 
1509
  display: none;
1510
  position: fixed;
1511
  inset: 0;
1512
+ background: rgba(0, 0, 0, 0.5);
1513
+ backdrop-filter: blur(4px);
1514
  z-index: 500;
1515
  align-items: center;
1516
  justify-content: center;
1517
  }
1518
 
1519
+ .modal-overlay.show {
1520
+ display: flex;
1521
+ }
1522
 
1523
  .settings-modal {
1524
  background: var(--bg-sidebar);
 
1529
  height: 520px;
1530
  max-height: 88vh;
1531
  overflow: hidden;
1532
+ box-shadow: 0 8px 40px rgba(0, 0, 0, 0.4);
1533
  display: flex;
1534
  flex-direction: column;
1535
  }
 
1543
  flex-shrink: 0;
1544
  }
1545
 
1546
+ .modal-header h2 {
1547
+ font-size: 18px;
1548
+ font-weight: 600;
1549
+ }
1550
 
1551
  .modal-close {
1552
+ background: none;
1553
+ border: none;
1554
  color: var(--text-secondary);
1555
  font-size: 24px;
1556
  cursor: pointer;
 
1558
  transition: color 0.2s;
1559
  }
1560
 
1561
+ .modal-close:hover {
1562
+ color: var(--text-primary);
1563
+ }
1564
 
1565
  .modal-body {
1566
  padding: 0;
 
1597
  font-weight: 500;
1598
  cursor: pointer;
1599
  border-radius: 8px;
1600
+ transition: background 0.2s, color 0.2s;
1601
  text-align: left;
1602
  width: 100%;
1603
  }
1604
 
1605
+ .settings-nav-btn:hover {
1606
+ background: var(--bg-hover);
1607
+ color: var(--text-primary);
1608
+ }
1609
+
1610
+ .settings-nav-btn.active {
1611
+ background: var(--bg-hover);
1612
+ color: var(--text-primary);
1613
+ font-weight: 600;
1614
+ }
1615
 
1616
  .settings-panel {
1617
  flex: 1;
 
1620
  min-width: 0;
1621
  }
1622
 
1623
+ .tab-content {
1624
+ display: none;
1625
+ }
1626
+
1627
+ .tab-content.active {
1628
+ display: block;
1629
+ }
1630
 
1631
  .settings-section-title {
1632
  font-size: 14px;
 
1635
  margin-bottom: 10px;
1636
  }
1637
 
1638
+ .settings-input,
1639
+ .settings-textarea {
1640
  width: 100%;
1641
  padding: 11px 14px;
1642
  background: var(--bg-input);
 
1649
  transition: border-color 0.2s;
1650
  }
1651
 
1652
+ .settings-input:focus,
1653
+ .settings-textarea:focus {
1654
+ border-color: var(--brand);
1655
+ }
1656
+
1657
+ .settings-input::placeholder,
1658
+ .settings-textarea::placeholder {
1659
+ color: var(--text-muted);
1660
+ }
1661
 
1662
  .settings-textarea {
1663
  resize: vertical;
 
1684
  transition: border-color 0.2s;
1685
  }
1686
 
1687
+ .settings-select:focus {
1688
+ border-color: var(--brand);
1689
+ }
1690
+
1691
+ .settings-select option {
1692
+ background: var(--bg-input);
1693
+ color: var(--text-primary);
1694
+ }
1695
 
1696
  .settings-save-btn {
1697
  margin-top: 12px;
1698
  padding: 10px 24px;
1699
+ background: var(--brand);
1700
  color: #fff;
1701
  border: none;
1702
  border-radius: 8px;
 
1704
  font-size: 13px;
1705
  font-weight: 600;
1706
  cursor: pointer;
1707
+ transition: background 0.3s;
1708
  }
1709
 
1710
+ .settings-save-btn:hover {
1711
+ background: var(--brand-hover);
1712
+ }
1713
 
1714
  .settings-hint {
1715
  font-size: 12px;
 
1718
  line-height: 1.5;
1719
  }
1720
 
1721
+ .settings-hint a {
1722
+ color: var(--brand);
1723
+ text-decoration: none;
1724
+ }
1725
+
1726
+ .settings-hint a:hover {
1727
+ text-decoration: underline;
1728
+ }
1729
 
1730
  .persona-option {
1731
  display: flex;
 
1739
  transition: border-color 0.2s, background 0.2s;
1740
  }
1741
 
1742
+ .persona-option:hover {
1743
+ background: var(--bg-hover);
1744
+ }
1745
+
1746
+ .persona-option.active {
1747
+ border-color: var(--brand);
1748
+ background: rgba(66, 133, 244, 0.06);
1749
+ }
1750
+
1751
+ .persona-name {
1752
+ font-size: 14px;
1753
+ font-weight: 600;
1754
+ color: var(--text-primary);
1755
+ }
1756
 
1757
+ .persona-desc {
1758
+ font-size: 12px;
1759
+ color: var(--text-secondary);
1760
+ line-height: 1.5;
1761
+ }
1762
 
1763
 
1764
  /* ============================================================
 
1766
  ============================================================ */
1767
 
1768
  @keyframes fadeIn {
1769
+ from {
1770
+ opacity: 0;
1771
+ transform: translateY(8px);
1772
+ }
1773
+
1774
+ to {
1775
+ opacity: 1;
1776
+ transform: translateY(0);
1777
+ }
1778
+ }
1779
+
1780
+ ::-webkit-scrollbar {
1781
+ width: 5px;
1782
+ }
1783
+
1784
+ ::-webkit-scrollbar-track {
1785
+ background: transparent;
1786
  }
1787
 
1788
+ ::-webkit-scrollbar-thumb {
1789
+ background: #333;
1790
+ border-radius: 4px;
1791
+ }
1792
+
1793
+ ::-webkit-scrollbar-thumb:hover {
1794
+ background: #555;
1795
+ }
1796
+
1797
+ [data-theme="light"] ::-webkit-scrollbar-thumb {
1798
+ background: #ccc;
1799
+ }
1800
+
1801
+ [data-theme="light"] ::-webkit-scrollbar-thumb:hover {
1802
+ background: #aaa;
1803
+ }
1804
 
1805
  .cursor::after {
1806
  content: '\25CB';
1807
  animation: blink 1s step-start infinite;
1808
+ color: var(--brand);
1809
  margin-left: 2px;
1810
  }
1811
 
1812
+ @keyframes blink {
1813
+ 50% {
1814
+ opacity: 0;
1815
+ }
1816
+ }
1817
 
1818
 
1819
  /* ============================================================
 
1824
  color: #ff6b6b;
1825
  font-style: italic;
1826
  padding: 10px 14px;
1827
+ background: rgba(255, 75, 75, 0.08);
1828
  border-radius: 8px;
1829
  border-left: 3px solid #ff4a4a;
1830
  }
 
1834
  RESPONSIVE — MOBILE
1835
  ============================================================ */
1836
 
1837
+ .mobile-fab-toggle {
1838
+ display: none !important;
1839
+ }
1840
 
 
1841
  .sidebar-overlay {
1842
  display: none;
1843
  position: fixed;
1844
  inset: 0;
1845
+ background: rgba(0, 0, 0, 0.45);
1846
  z-index: 35;
1847
  -webkit-tap-highlight-color: transparent;
1848
  }
1849
+
1850
+ .sidebar-overlay.visible {
1851
+ display: block;
1852
+ }
1853
 
1854
  @media (max-width: 768px) {
 
1855
  .app-container {
1856
  overflow: hidden;
1857
  position: relative;
1858
  }
1859
 
 
1860
  .sidebar {
1861
+ position: fixed;
1862
+ left: 0;
1863
+ top: 0;
1864
  width: var(--sidebar-width);
1865
  height: 100vh;
1866
  height: 100dvh;
1867
  z-index: 40;
1868
+ transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.25s cubic-bezier(0.4, 0, 0.2, 1);
1869
  margin-left: 0 !important;
1870
  }
1871
 
 
1881
  pointer-events: auto;
1882
  }
1883
 
1884
+ .sidebar-rail {
1885
+ display: none !important;
1886
+ }
1887
 
 
1888
  .main-chat {
1889
  width: 100vw;
1890
  min-width: 0;
1891
  flex: 1;
1892
  }
1893
 
 
1894
  .sidebar-logo {
1895
  height: 36px;
1896
  }
 
1899
  padding: 20px 18px 10px;
1900
  }
1901
 
 
1902
  .settings-modal {
1903
  width: 96vw;
1904
  height: 78vh;
 
1931
  border-radius: 10px;
1932
  white-space: normal;
1933
  }
1934
+
1935
  .settings-nav-btn span,
1936
+ .settings-nav-btn> :not(svg) {
1937
+ display: none;
1938
+ }
1939
+
1940
+ .settings-nav-btn svg {
1941
+ width: 18px;
1942
+ height: 18px;
1943
+ flex-shrink: 0;
1944
+ }
1945
+
1946
  .settings-nav-btn.active {
1947
  background: var(--bg-elevated);
1948
+ color: var(--brand);
1949
+ }
1950
+
1951
+ .settings-panel {
1952
+ padding: 14px 12px;
1953
+ flex: 1;
1954
+ min-width: 0;
1955
  }
1956
 
1957
+ .hero-title {
1958
+ font-size: 22px;
1959
+ }
1960
 
1961
+ .hero-bot-icon {
1962
+ width: 80px;
1963
+ height: 80px;
1964
+ }
1965
 
 
1966
  .hero-welcome {
1967
  justify-content: flex-start;
1968
  padding: 20px 16px calc(20px + env(safe-area-inset-bottom));
 
1970
  min-height: 0;
1971
  }
1972
 
1973
+ .hero-pills {
1974
+ grid-template-columns: 1fr;
1975
+ }
1976
 
1977
+ .hero-pill {
1978
+ font-size: 12px;
1979
+ padding: 12px 14px;
1980
+ }
1981
 
1982
+ .hero-input-wrap {
1983
+ max-width: 100%;
1984
+ }
1985
+
1986
+ .login-title {
1987
+ font-size: 20px;
1988
+ white-space: normal;
1989
+ }
1990
 
 
1991
  .chat-container {
1992
  padding: 60px 10px 16px;
1993
  gap: 16px;
 
2001
  .ai-avatar {
2002
  display: none;
2003
  }
2004
+
2005
  .ai .message-content {
2006
  font-size: 13px;
2007
  line-height: 1.55;
 
2013
  font-size: 13px;
2014
  }
2015
 
 
 
 
 
2016
  .input-container {
2017
  padding: 10px 10px calc(6px + env(safe-area-inset-bottom));
2018
  }
 
2022
  margin-top: 6px;
2023
  }
2024
 
 
2025
  .history-item .options-btn {
2026
  opacity: 0.6;
2027
  }
2028
+
2029
+ .theme-toggle-btn {
2030
+ top: 10px;
2031
+ right: 10px;
2032
+ width: 32px;
2033
+ height: 32px;
2034
+ }
2035
+
2036
+ .theme-toggle-btn svg {
2037
+ width: 16px;
2038
+ height: 16px;
2039
+ }
2040
  }
2041
 
2042
  @media (max-width: 480px) {
2043
+ .hero-title {
2044
+ font-size: 20px;
2045
+ }
2046
+
2047
+ .hero-input-box textarea {
2048
+ font-size: 13px;
2049
+ }
2050
+
2051
+ .ai .message-content {
2052
+ font-size: 12.5px;
2053
+ }
2054
+
2055
+ .user .message-content {
2056
+ font-size: 12.5px;
2057
+ }
2058
+
2059
+ .katex-display {
2060
+ overflow-x: auto;
2061
+ font-size: 0.95em;
2062
+ }
2063
+
2064
+ .message-content pre {
2065
+ font-size: 11px;
2066
+ }
2067
+ }