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

ui/ux; vidyut-personaa

Browse files
Files changed (6) hide show
  1. assets/bot.png +2 -2
  2. graph.py +18 -3
  3. prompts.py +25 -0
  4. static/app.js +182 -120
  5. static/index.html +44 -62
  6. static/style.css +59 -28
assets/bot.png CHANGED

Git LFS Details

  • SHA256: 147d1a69d046ff94caa60b6bda0ab9cd285a86c4a7a71ebe12b67ce6029d3df3
  • Pointer size: 131 Bytes
  • Size of remote file: 631 kB

Git LFS Details

  • SHA256: bcc836ee4b73991ba5e898fabd795387ba5b475fe507dafb1a8db0eb2419162b
  • Pointer size: 131 Bytes
  • Size of remote file: 363 kB
graph.py CHANGED
@@ -360,9 +360,24 @@ def chat_node(state: ChatState, config: RunnableConfig):
360
  sys = SystemMessage(content=base_prompt)
361
  print(f"[ROUTER] category={category} model={model_id} vision={actual == 'vision'}")
362
 
363
- llm = _make_llm(api_key, model_id)
364
- resp = llm.invoke([sys] + messages)
365
- return {"messages": [resp]}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
366
 
367
 
368
  # ── Compile graph ─────────────────────────────────────────────
 
360
  sys = SystemMessage(content=base_prompt)
361
  print(f"[ROUTER] category={category} model={model_id} vision={actual == 'vision'}")
362
 
363
+ # Retry loop for rate limits or transient errors
364
+ max_attempts = 3
365
+ for attempt in range(max_attempts):
366
+ try:
367
+ llm = _make_llm(api_key, model_id)
368
+ resp = llm.invoke([sys] + messages)
369
+ return {"messages": [resp]}
370
+ except Exception as e:
371
+ err_str = str(e)
372
+ print(f"[ROUTER] Model {model_id} failed on attempt {attempt+1}/{max_attempts}: {err_str[:100]}")
373
+ if attempt < max_attempts - 1:
374
+ # Pick a different model and try again
375
+ model_id, actual = _pick(category, has_image=has_image)
376
+ print(f"[ROUTER] Retrying with new model {model_id}")
377
+ else:
378
+ # Last attempt failed
379
+ from langchain_core.messages import AIMessage
380
+ return {"messages": [AIMessage(content="I'm experiencing high traffic right now. Could you please try again in a moment?")]}
381
 
382
 
383
  # ── Compile graph ─────────────────────────────────────────────
prompts.py CHANGED
@@ -99,6 +99,31 @@ _PERSONAS = {
99
  "- Skip derivations unless explicitly asked. Give the result and how to USE it.\n"
100
  "- End with a confidence boost: 'Quick revision done. You've got this. πŸ’ͺ'"
101
  ),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  }
103
 
104
 
 
99
  "- Skip derivations unless explicitly asked. Give the result and how to USE it.\n"
100
  "- End with a confidence boost: 'Quick revision done. You've got this. πŸ’ͺ'"
101
  ),
102
+ "vidyut": (
103
+ "TEACHING STYLE β€” Vidyut Mode:\n"
104
+ "You are that one teacher students remember for the rest of their lives β€” "
105
+ "the one who made hard things feel obvious without ever making the student feel small.\n"
106
+ "- Teach one topic at a time. Do not move ahead until the student asks.\n"
107
+ "- Open with a single sharp real-world analogy or daily-life observation β€” "
108
+ "something a sharp 17-year-old would immediately connect with. "
109
+ "Not a long story. One precise moment that makes the concept feel inevitable.\n"
110
+ "- Then define the concept cleanly in 2 lines β€” no jargon, no filler.\n"
111
+ "- Explain how it works in numbered steps. Name every variable in any formula. "
112
+ "Solve one example completely from start to finish.\n"
113
+ "- Show 2-3 places the concept appears in real life or the NCERT syllabus.\n"
114
+ "- End with one line on what the examiner expects, and one mistake to avoid.\n"
115
+ "- Tone: calm, mature, direct. No cheerleading. No over-explaining. "
116
+ "Treat the student as an intelligent person who just hasn't seen this yet.\n"
117
+ "- If a student is confused, approach from a completely different angle. "
118
+ "A different analogy, a different example β€” never the same explanation repeated.\n"
119
+ "- If the student sends an image, identify it first β€” question, diagram, or their "
120
+ "own solution β€” then respond to exactly what is in front of you.\n"
121
+ "- Hinglish is welcome if the student uses it naturally. Never force it.\n"
122
+ "- No Mermaid diagrams. ASCII art only when it genuinely clarifies something.\n"
123
+ "- Keep the session respectful at all times. No inappropriate language, "
124
+ "no off-topic conversation, no content outside academics.\n"
125
+ "- One concept taught well is worth ten taught badly. Do not rush."
126
+ ),
127
  }
128
 
129
 
static/app.js CHANGED
@@ -6,105 +6,105 @@
6
  ============================================================ */
7
 
8
  /* ----- State ----- */
9
- let currentUser = null;
10
  let currentThreadId = crypto.randomUUID();
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';
18
  let currentLanguage = localStorage.getItem('stemcopilot_language') || 'auto';
19
- let currentUsername = localStorage.getItem('stemcopilot_username') || '';
20
 
21
 
22
  /* ============================================================
23
  DOM REFERENCES
24
  ============================================================ */
25
 
26
- const loginScreen = document.getElementById('loginScreen');
27
- const byokScreen = document.getElementById('byokScreen');
28
- const appContainer = document.getElementById('appContainer');
29
- const sidebar = document.getElementById('sidebar');
30
- const sidebarOverlay = document.getElementById('sidebarOverlay');
31
- const sidebarRail = document.getElementById('sidebarRail');
32
- const toggleSidebarBtn = document.getElementById('toggleSidebarBtn');
33
- const chatHistoryList = document.getElementById('chatHistoryList');
34
- const chatContainer = document.getElementById('chatContainer');
35
- const welcomeScreen = document.getElementById('welcomeScreen');
36
  const bottomInputContainer = document.getElementById('bottomInputContainer');
37
 
38
- const userInput = document.getElementById('userInput');
39
- const sendBtn = document.getElementById('sendBtn');
40
- const newChatBtn = document.getElementById('newChatBtn');
41
- const stopBtn = document.getElementById('stopBtn');
42
-
43
- const heroInput = document.getElementById('heroInput');
44
- const heroSendBtn = document.getElementById('heroSendBtn');
45
- const heroUploadBtn = document.getElementById('heroUploadBtn');
46
- const heroImageInput = document.getElementById('heroImageInput');
47
- const heroTitle = document.getElementById('heroTitle');
48
-
49
- const byokInput = document.getElementById('byokInput');
50
- const byokSubmitBtn = document.getElementById('byokSubmitBtn');
51
-
52
- const userProfileBtn = document.getElementById('userProfileBtn');
53
- const userMenu = document.getElementById('userMenu');
54
- const userAvatar = document.getElementById('userAvatar');
55
- const userDisplayName = document.getElementById('userDisplayName');
56
- const logoutBtn = document.getElementById('logoutBtn');
57
-
58
- const railExpandBtn = document.getElementById('railExpandBtn');
59
- const railNewChatBtn = document.getElementById('railNewChatBtn');
60
- const railProfileBtn = document.getElementById('railProfileBtn');
61
- const railAvatar = document.getElementById('railAvatar');
62
-
63
- const settingsOverlay = document.getElementById('settingsOverlay');
64
- const openSettingsBtn = document.getElementById('openSettingsBtn');
65
- const settingsCloseBtn = document.getElementById('settingsCloseBtn');
66
- const settingsModal = document.getElementById('settingsModal');
67
- const feedbackMenuBtn = document.getElementById('feedbackMenuBtn');
68
-
69
- const usernameInput = document.getElementById('usernameInput');
70
- const languageSelect = document.getElementById('languageSelect');
71
- const profileInput = document.getElementById('profileInput');
72
- const saveProfileBtn = document.getElementById('saveProfileBtn');
73
  const settingsApiKeyInput = document.getElementById('settingsApiKeyInput');
74
- const saveApiKeyBtn = document.getElementById('saveApiKeyBtn');
75
 
76
- const uploadBtn = document.getElementById('uploadBtn');
77
- const imageInput = document.getElementById('imageInput');
78
- const imagePreviewBar = document.getElementById('imagePreviewBar');
79
  const imagePreviewThumb = document.getElementById('imagePreviewThumb');
80
- const imagePreviewRemove= document.getElementById('imagePreviewRemove');
81
 
82
- const installBanner = document.getElementById('installBanner');
83
- const installBtn = document.getElementById('installBtn');
84
- const installDismiss = document.getElementById('installDismiss');
85
 
86
- const themeToggleBtn = document.getElementById('themeToggleBtn');
87
 
88
  // Hero image preview
89
- const heroImagePreview = document.getElementById('heroImagePreview');
90
- const heroImagePreviewThumb = document.getElementById('heroImagePreviewThumb');
91
  const heroImagePreviewRemove = document.getElementById('heroImagePreviewRemove');
92
 
93
  // Style selector in input bar
94
- const styleSelectorBtn = document.getElementById('styleSelectorBtn');
95
  const styleSelectorLabel = document.getElementById('styleSelectorLabel');
96
- const styleDropdown = document.getElementById('styleDropdown');
97
 
98
  // Mic buttons
99
- const micBtn = document.getElementById('micBtn');
100
- const heroMicBtn = document.getElementById('heroMicBtn');
101
 
102
 
103
  /* ============================================================
104
  THEME TOGGLE β€” Smooth dark/light
105
  ============================================================ */
106
 
107
- const _PERSONA_LABELS = { nerd: 'Nerd', noob: 'Beginner', thoughtful: 'Thoughtful', panic: 'Panic' };
108
 
109
  function _initTheme() {
110
  const saved = localStorage.getItem('stemcopilot_theme') || 'dark';
@@ -247,10 +247,12 @@ function triggerGoogleSignIn() {
247
  const tryClick = () => {
248
  const realBtn = document.querySelector('#gsi-hidden-btn [role="button"]');
249
  if (realBtn) { realBtn.click(); }
250
- else { _renderHiddenGoogleBtn(() => {
251
- const btn = document.querySelector('#gsi-hidden-btn [role="button"]');
252
- if (btn) btn.click();
253
- }); }
 
 
254
  };
255
  if (_gsiInitialized) tryClick();
256
  else initGoogleAuth().then(tryClick);
@@ -262,19 +264,19 @@ function handleGoogleCredential(response) {
262
  headers: { 'Content-Type': 'application/json' },
263
  body: JSON.stringify({ token: response.credential }),
264
  })
265
- .then(r => {
266
- if (!r.ok) throw new Error('Auth failed');
267
- return r.json();
268
- })
269
- .then(data => {
270
- if (data.error) { showToast('Login failed: ' + data.error, 'error'); return; }
271
- currentUser = data.user;
272
- localStorage.setItem('stemcopilot_user', JSON.stringify(currentUser));
273
- currentUsername = currentUser.name;
274
- localStorage.setItem('stemcopilot_username', currentUsername);
275
- if (!data.has_api_key) showByok(); else showApp();
276
- })
277
- .catch(() => showToast('Login failed. Check your connection and try again.', 'error'));
278
  }
279
 
280
  function checkExistingSession() {
@@ -319,9 +321,9 @@ if (byokSubmitBtn) byokSubmitBtn.addEventListener('click', () => {
319
  headers: { 'Content-Type': 'application/json' },
320
  body: JSON.stringify({ user_id: currentUser.google_id, key: key }),
321
  })
322
- .then(r => { if (!r.ok) throw new Error(); return r.json(); })
323
- .then(() => showApp())
324
- .catch(() => showToast('Failed to save key. Try again.', 'error'));
325
  });
326
 
327
 
@@ -347,7 +349,7 @@ function showApp() {
347
  if (data.user.student_profile && profileInput) profileInput.value = data.user.student_profile;
348
  if (data.user.openrouter_key && settingsApiKeyInput) settingsApiKeyInput.value = 'β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’';
349
  }
350
- }).catch(() => {});
351
  }
352
 
353
  if (usernameInput) usernameInput.value = currentUsername;
@@ -367,7 +369,7 @@ function showApp() {
367
  data.threads.forEach(t => threads.push({ id: t.id, title: t.title }));
368
  renderHistory();
369
  })
370
- .catch(() => {});
371
 
372
  // Ensure sidebar starts collapsed on mobile
373
  if (window.innerWidth <= 768) closeSidebar();
@@ -420,13 +422,13 @@ let sidebarOpen = false;
420
 
421
  function _cleanSidebarStyles() {
422
  sidebar.style.transition = '';
423
- sidebar.style.transform = '';
424
- sidebar.style.opacity = '';
425
- sidebar.style.display = '';
426
  if (sidebarOverlay) {
427
  sidebarOverlay.style.transition = '';
428
- sidebarOverlay.style.opacity = '';
429
- sidebarOverlay.style.display = '';
430
  }
431
  }
432
 
@@ -506,6 +508,8 @@ function createWelcomeScreen() {
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>
@@ -520,6 +524,34 @@ function createWelcomeScreen() {
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>
@@ -529,10 +561,10 @@ function createWelcomeScreen() {
529
  </div>
530
  </div>
531
  <div class="hero-pills">
532
- <button class="hero-pill" data-query="Explain Coulomb's law with derivation">Coulomb's Law</button>
533
- <button class="hero-pill" data-query="What is sp3 hybridisation? Explain with examples">sp3 Hybridisation</button>
534
- <button class="hero-pill" data-query="Derive the quadratic formula step by step">Quadratic Formula</button>
535
- <button class="hero-pill" data-query="Explain Newton's laws of motion with real world examples">Newton's Laws</button>
536
  </div>
537
  `;
538
 
@@ -545,10 +577,15 @@ function createWelcomeScreen() {
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';
550
  });
551
- dynInput.addEventListener('keydown', function(e) {
552
  if (e.key === 'Enter' && !e.shiftKey) {
553
  e.preventDefault(); userInput.value = dynInput.value; sendMessage();
554
  }
@@ -558,7 +595,6 @@ function createWelcomeScreen() {
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
  });
@@ -567,6 +603,30 @@ function createWelcomeScreen() {
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
  });
@@ -684,8 +744,10 @@ function renameChat(e, optionEl) {
684
  titleSpan.innerText = newTitle; input.replaceWith(titleSpan);
685
  const thread = threads.find(t => t.id === threadId);
686
  if (thread) thread.title = newTitle;
687
- fetch('/rename', { method: 'POST', headers: { 'Content-Type': 'application/json' },
688
- body: JSON.stringify({ thread_id: threadId, title: newTitle }) });
 
 
689
  }
690
  input.addEventListener('blur', saveRename);
691
  input.addEventListener('keydown', evt => {
@@ -789,7 +851,7 @@ if (saveProfileBtn) saveProfileBtn.addEventListener('click', () => {
789
 
790
  function _syncStyleLabel() {
791
  if (styleSelectorLabel) {
792
- styleSelectorLabel.textContent = _PERSONA_LABELS[currentPersona] || 'Nerd';
793
  }
794
  }
795
 
@@ -944,9 +1006,9 @@ if (heroImagePreviewRemove) heroImagePreviewRemove.addEventListener('click', ()
944
  ============================================================ */
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';
@@ -962,7 +1024,7 @@ if (stopBtn) {
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) {
@@ -997,10 +1059,10 @@ if (heroSendBtn) heroSendBtn.addEventListener('click', () => {
997
  userInput.value = heroInput.value; sendMessage();
998
  });
999
  if (heroInput) {
1000
- heroInput.addEventListener('input', function() {
1001
  this.style.height = '54px'; this.style.height = this.scrollHeight + 'px';
1002
  });
1003
- heroInput.addEventListener('keydown', function(e) {
1004
  if (e.key === 'Enter' && !e.shiftKey) {
1005
  e.preventDefault(); userInput.value = heroInput.value; sendMessage();
1006
  }
@@ -1139,7 +1201,7 @@ function streamResponse(text) {
1139
  rawText += data.token;
1140
  scheduleRender();
1141
  }
1142
- } catch (_) {}
1143
  }
1144
 
1145
  if (!currentStreamStopped) read();
@@ -1264,10 +1326,10 @@ function renderFinalContent(element, rawText) {
1264
  if (!rawText) return;
1265
  const blocks = [];
1266
  let safeText = rawText;
1267
- safeText = safeText.replace(/\$\$[\s\S]*?\$\$/g, m => { blocks.push(m); return `%%LATEX_${blocks.length-1}%%`; });
1268
- safeText = safeText.replace(/\$[^\$\n]+?\$/g, m => { blocks.push(m); return `%%LATEX_${blocks.length-1}%%`; });
1269
- safeText = safeText.replace(/\\\[[\s\S]*?\\\]/g, m => { blocks.push(m); return `%%LATEX_${blocks.length-1}%%`; });
1270
- safeText = safeText.replace(/\\\([\s\S]*?\\\)/g, m => { blocks.push(m); return `%%LATEX_${blocks.length-1}%%`; });
1271
  let html = typeof marked !== 'undefined' ? marked.parse(safeText) : safeText;
1272
  blocks.forEach((block, i) => { html = html.replace(`%%LATEX_${i}%%`, block); });
1273
  element.innerHTML = html;
@@ -1275,7 +1337,7 @@ function renderFinalContent(element, rawText) {
1275
  renderMathInElement(element, {
1276
  delimiters: [
1277
  { left: '$$', right: '$$', display: true },
1278
- { left: '$', right: '$', display: false },
1279
  { left: '\\(', right: '\\)', display: false },
1280
  { left: '\\[', right: '\\]', display: true },
1281
  ],
@@ -1290,7 +1352,7 @@ function renderFinalContent(element, rawText) {
1290
  ============================================================ */
1291
 
1292
  if ('serviceWorker' in navigator) {
1293
- navigator.serviceWorker.register('/sw.js').catch(() => {});
1294
  }
1295
 
1296
  let deferredPrompt = null;
@@ -1330,9 +1392,9 @@ if (sidebar) sidebar.classList.add('collapsed');
1330
  // Lock screen to portrait on mobile
1331
  try {
1332
  if (screen.orientation && screen.orientation.lock) {
1333
- screen.orientation.lock('portrait').catch(() => {});
1334
  }
1335
- } catch(_) {}
1336
 
1337
  window.addEventListener('load', () => {
1338
  checkExistingSession();
 
6
  ============================================================ */
7
 
8
  /* ----- State ----- */
9
+ let currentUser = null;
10
  let currentThreadId = crypto.randomUUID();
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') || 'vidyut';
18
  let currentLanguage = localStorage.getItem('stemcopilot_language') || 'auto';
19
+ let currentUsername = localStorage.getItem('stemcopilot_username') || '';
20
 
21
 
22
  /* ============================================================
23
  DOM REFERENCES
24
  ============================================================ */
25
 
26
+ const loginScreen = document.getElementById('loginScreen');
27
+ const byokScreen = document.getElementById('byokScreen');
28
+ const appContainer = document.getElementById('appContainer');
29
+ const sidebar = document.getElementById('sidebar');
30
+ const sidebarOverlay = document.getElementById('sidebarOverlay');
31
+ const sidebarRail = document.getElementById('sidebarRail');
32
+ const toggleSidebarBtn = document.getElementById('toggleSidebarBtn');
33
+ const chatHistoryList = document.getElementById('chatHistoryList');
34
+ const chatContainer = document.getElementById('chatContainer');
35
+ const welcomeScreen = document.getElementById('welcomeScreen');
36
  const bottomInputContainer = document.getElementById('bottomInputContainer');
37
 
38
+ const userInput = document.getElementById('userInput');
39
+ const sendBtn = document.getElementById('sendBtn');
40
+ const newChatBtn = document.getElementById('newChatBtn');
41
+ const stopBtn = document.getElementById('stopBtn');
42
+
43
+ const heroInput = document.getElementById('heroInput');
44
+ const heroSendBtn = document.getElementById('heroSendBtn');
45
+ const heroUploadBtn = document.getElementById('heroUploadBtn');
46
+ const heroImageInput = document.getElementById('heroImageInput');
47
+ const heroTitle = document.getElementById('heroTitle');
48
+
49
+ const byokInput = document.getElementById('byokInput');
50
+ const byokSubmitBtn = document.getElementById('byokSubmitBtn');
51
+
52
+ const userProfileBtn = document.getElementById('userProfileBtn');
53
+ const userMenu = document.getElementById('userMenu');
54
+ const userAvatar = document.getElementById('userAvatar');
55
+ const userDisplayName = document.getElementById('userDisplayName');
56
+ const logoutBtn = document.getElementById('logoutBtn');
57
+
58
+ const railExpandBtn = document.getElementById('railExpandBtn');
59
+ const railNewChatBtn = document.getElementById('railNewChatBtn');
60
+ const railProfileBtn = document.getElementById('railProfileBtn');
61
+ const railAvatar = document.getElementById('railAvatar');
62
+
63
+ const settingsOverlay = document.getElementById('settingsOverlay');
64
+ const openSettingsBtn = document.getElementById('openSettingsBtn');
65
+ const settingsCloseBtn = document.getElementById('settingsCloseBtn');
66
+ const settingsModal = document.getElementById('settingsModal');
67
+ const feedbackMenuBtn = document.getElementById('feedbackMenuBtn');
68
+
69
+ const usernameInput = document.getElementById('usernameInput');
70
+ const languageSelect = document.getElementById('languageSelect');
71
+ const profileInput = document.getElementById('profileInput');
72
+ const saveProfileBtn = document.getElementById('saveProfileBtn');
73
  const settingsApiKeyInput = document.getElementById('settingsApiKeyInput');
74
+ const saveApiKeyBtn = document.getElementById('saveApiKeyBtn');
75
 
76
+ const uploadBtn = document.getElementById('uploadBtn');
77
+ const imageInput = document.getElementById('imageInput');
78
+ const imagePreviewBar = document.getElementById('imagePreviewBar');
79
  const imagePreviewThumb = document.getElementById('imagePreviewThumb');
80
+ const imagePreviewRemove = document.getElementById('imagePreviewRemove');
81
 
82
+ const installBanner = document.getElementById('installBanner');
83
+ const installBtn = document.getElementById('installBtn');
84
+ const installDismiss = document.getElementById('installDismiss');
85
 
86
+ const themeToggleBtn = document.getElementById('themeToggleBtn');
87
 
88
  // Hero image preview
89
+ const heroImagePreview = document.getElementById('heroImagePreview');
90
+ const heroImagePreviewThumb = document.getElementById('heroImagePreviewThumb');
91
  const heroImagePreviewRemove = document.getElementById('heroImagePreviewRemove');
92
 
93
  // Style selector in input bar
94
+ const styleSelectorBtn = document.getElementById('styleSelectorBtn');
95
  const styleSelectorLabel = document.getElementById('styleSelectorLabel');
96
+ const styleDropdown = document.getElementById('styleDropdown');
97
 
98
  // Mic buttons
99
+ const micBtn = document.getElementById('micBtn');
100
+ const heroMicBtn = document.getElementById('heroMicBtn');
101
 
102
 
103
  /* ============================================================
104
  THEME TOGGLE β€” Smooth dark/light
105
  ============================================================ */
106
 
107
+ const _PERSONA_LABELS = { vidyut: 'Vidyut', nerd: 'Nerd', noob: 'Beginner', thoughtful: 'Thoughtful', panic: 'Panic' };
108
 
109
  function _initTheme() {
110
  const saved = localStorage.getItem('stemcopilot_theme') || 'dark';
 
247
  const tryClick = () => {
248
  const realBtn = document.querySelector('#gsi-hidden-btn [role="button"]');
249
  if (realBtn) { realBtn.click(); }
250
+ else {
251
+ _renderHiddenGoogleBtn(() => {
252
+ const btn = document.querySelector('#gsi-hidden-btn [role="button"]');
253
+ if (btn) btn.click();
254
+ });
255
+ }
256
  };
257
  if (_gsiInitialized) tryClick();
258
  else initGoogleAuth().then(tryClick);
 
264
  headers: { 'Content-Type': 'application/json' },
265
  body: JSON.stringify({ token: response.credential }),
266
  })
267
+ .then(r => {
268
+ if (!r.ok) throw new Error('Auth failed');
269
+ return r.json();
270
+ })
271
+ .then(data => {
272
+ if (data.error) { showToast('Login failed: ' + data.error, 'error'); return; }
273
+ currentUser = data.user;
274
+ localStorage.setItem('stemcopilot_user', JSON.stringify(currentUser));
275
+ currentUsername = currentUser.name;
276
+ localStorage.setItem('stemcopilot_username', currentUsername);
277
+ if (!data.has_api_key) showByok(); else showApp();
278
+ })
279
+ .catch(() => showToast('Login failed. Check your connection and try again.', 'error'));
280
  }
281
 
282
  function checkExistingSession() {
 
321
  headers: { 'Content-Type': 'application/json' },
322
  body: JSON.stringify({ user_id: currentUser.google_id, key: key }),
323
  })
324
+ .then(r => { if (!r.ok) throw new Error(); return r.json(); })
325
+ .then(() => showApp())
326
+ .catch(() => showToast('Failed to save key. Try again.', 'error'));
327
  });
328
 
329
 
 
349
  if (data.user.student_profile && profileInput) profileInput.value = data.user.student_profile;
350
  if (data.user.openrouter_key && settingsApiKeyInput) settingsApiKeyInput.value = 'β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’';
351
  }
352
+ }).catch(() => { });
353
  }
354
 
355
  if (usernameInput) usernameInput.value = currentUsername;
 
369
  data.threads.forEach(t => threads.push({ id: t.id, title: t.title }));
370
  renderHistory();
371
  })
372
+ .catch(() => { });
373
 
374
  // Ensure sidebar starts collapsed on mobile
375
  if (window.innerWidth <= 768) closeSidebar();
 
422
 
423
  function _cleanSidebarStyles() {
424
  sidebar.style.transition = '';
425
+ sidebar.style.transform = '';
426
+ sidebar.style.opacity = '';
427
+ sidebar.style.display = '';
428
  if (sidebarOverlay) {
429
  sidebarOverlay.style.transition = '';
430
+ sidebarOverlay.style.opacity = '';
431
+ sidebarOverlay.style.display = '';
432
  }
433
  }
434
 
 
508
  ? `What should we study today, <span class="hero-name">${escapeHtml(name)}</span>?`
509
  : `What should we study today?`;
510
 
511
+ const styleLabel = _PERSONA_LABELS[currentPersona] || 'Vidyut';
512
+
513
  div.innerHTML = `
514
  <img src="/assets/bot.png" alt="STEM Copilot" class="hero-bot-icon">
515
  <h1 class="hero-title" id="heroTitle">${titleHtml}</h1>
 
524
  </button>
525
  <input type="file" id="heroImageInputDynamic" accept="image/*" style="display:none;">
526
  <textarea id="heroInputDynamic" placeholder="Ask STEM Copilot..." rows="1"></textarea>
527
+ <div class="style-selector-wrap" id="heroStyleSelectorWrapDynamic">
528
+ <button class="style-selector-btn" id="heroStyleSelectorBtnDynamic" title="Teaching style">
529
+ <span id="heroStyleSelectorLabelDynamic">${styleLabel}</span>
530
+ <svg viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9" /></svg>
531
+ </button>
532
+ <div class="style-dropdown" id="heroStyleDropdownDynamic">
533
+ <div class="style-option ${currentPersona === 'vidyut' ? 'active' : ''}" data-persona="vidyut">
534
+ <div class="style-option-name">Vidyut</div>
535
+ <div class="style-option-desc">Calm, clear, step-by-step master teacher. Makes hard concepts obvious.</div>
536
+ </div>
537
+ <div class="style-option ${currentPersona === 'nerd' ? 'active' : ''}" data-persona="nerd">
538
+ <div class="style-option-name">Nerd</div>
539
+ <div class="style-option-desc">Deep, rigorous, first-principles. Goes beyond the textbook.</div>
540
+ </div>
541
+ <div class="style-option ${currentPersona === 'noob' ? 'active' : ''}" data-persona="noob">
542
+ <div class="style-option-name">Beginner</div>
543
+ <div class="style-option-desc">Patient, step-by-step. Explains like you're seeing it for the first time.</div>
544
+ </div>
545
+ <div class="style-option ${currentPersona === 'thoughtful' ? 'active' : ''}" data-persona="thoughtful">
546
+ <div class="style-option-name">Thoughtful</div>
547
+ <div class="style-option-desc">Connects science to the real world. Every formula has a story.</div>
548
+ </div>
549
+ <div class="style-option ${currentPersona === 'panic' ? 'active' : ''}" data-persona="panic">
550
+ <div class="style-option-name">Panic</div>
551
+ <div class="style-option-desc">Exam tomorrow? Concise bullets, key formulas, no fluff.</div>
552
+ </div>
553
+ </div>
554
+ </div>
555
  <button class="mic-btn" id="heroMicBtnDynamic" title="Voice input">
556
  <svg viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>
557
  </button>
 
561
  </div>
562
  </div>
563
  <div class="hero-pills">
564
+ <button class="hero-pill" data-query="What is the photoelectric effect, and why did Einstein win the Nobel Prize for it?">What is the photoelectric effect and why did Einstein win the Nobel Prize for it?</button>
565
+ <button class="hero-pill" data-query="Derive the relation between Kp and Kc for a general gaseous equilibrium reaction">Derive the relation between Kp and Kc for gaseous equilibrium</button>
566
+ <button class="hero-pill" data-query="Solve: A ball is thrown vertically upward with velocity 20 m/s. Find maximum height and time of flight. Take g = 10 m/s^2">A ball thrown up at 20 m/s -- find max height and time of flight</button>
567
+ <button class="hero-pill" data-query="Explain the concept of limits in calculus with a real-world example. How is it different from the actual value of a function?">Explain limits in calculus with a real-world example</button>
568
  </div>
569
  `;
570
 
 
577
  const dynImgThumb = div.querySelector('#heroImagePreviewThumbDynamic');
578
  const dynImgRemove = div.querySelector('#heroImagePreviewRemoveDynamic');
579
 
580
+ // Style selector in dynamic hero
581
+ const dynStyleBtn = div.querySelector('#heroStyleSelectorBtnDynamic');
582
+ const dynStyleDropdown = div.querySelector('#heroStyleDropdownDynamic');
583
+ const dynStyleLabel = div.querySelector('#heroStyleSelectorLabelDynamic');
584
+
585
+ dynInput.addEventListener('input', function () {
586
  this.style.height = '54px'; this.style.height = this.scrollHeight + 'px';
587
  });
588
+ dynInput.addEventListener('keydown', function (e) {
589
  if (e.key === 'Enter' && !e.shiftKey) {
590
  e.preventDefault(); userInput.value = dynInput.value; sendMessage();
591
  }
 
595
  dynFileInput.addEventListener('change', () => {
596
  if (dynFileInput.files[0]) {
597
  handleImageFile(dynFileInput.files[0]);
 
598
  _showHeroImagePreview(dynImgPreview, dynImgThumb);
599
  }
600
  });
 
603
  dynImgPreview.classList.remove('visible');
604
  });
605
  if (dynMic) _bindMic(dynMic, dynInput);
606
+
607
+ // Style selector events
608
+ if (dynStyleBtn) {
609
+ dynStyleBtn.addEventListener('click', (e) => {
610
+ e.stopPropagation();
611
+ dynStyleDropdown.classList.toggle('show');
612
+ dynStyleBtn.classList.toggle('open');
613
+ });
614
+ }
615
+ if (dynStyleDropdown) {
616
+ dynStyleDropdown.querySelectorAll('.style-option').forEach(opt => {
617
+ opt.addEventListener('click', (e) => {
618
+ e.stopPropagation();
619
+ _setPersona(opt.dataset.persona);
620
+ dynStyleDropdown.classList.remove('show');
621
+ dynStyleBtn.classList.remove('open');
622
+ dynStyleLabel.textContent = _PERSONA_LABELS[currentPersona] || 'Vidyut';
623
+ dynStyleDropdown.querySelectorAll('.style-option').forEach(o => {
624
+ o.classList.toggle('active', o.dataset.persona === currentPersona);
625
+ });
626
+ });
627
+ });
628
+ }
629
+
630
  div.querySelectorAll('.hero-pill').forEach(p => {
631
  p.addEventListener('click', () => { userInput.value = p.dataset.query; sendMessage(); });
632
  });
 
744
  titleSpan.innerText = newTitle; input.replaceWith(titleSpan);
745
  const thread = threads.find(t => t.id === threadId);
746
  if (thread) thread.title = newTitle;
747
+ fetch('/rename', {
748
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
749
+ body: JSON.stringify({ thread_id: threadId, title: newTitle })
750
+ });
751
  }
752
  input.addEventListener('blur', saveRename);
753
  input.addEventListener('keydown', evt => {
 
851
 
852
  function _syncStyleLabel() {
853
  if (styleSelectorLabel) {
854
+ styleSelectorLabel.textContent = _PERSONA_LABELS[currentPersona] || 'Vidyut';
855
  }
856
  }
857
 
 
1006
  ============================================================ */
1007
 
1008
  let currentAbortController = null;
1009
+ let currentStreamReader = null;
1010
+ let currentStreamStopped = false;
1011
+ let currentRenderTimer = null;
1012
 
1013
  function _showStopBtn() {
1014
  if (sendBtn) sendBtn.style.display = 'none';
 
1024
  currentStreamStopped = true;
1025
  if (currentRenderTimer) { clearTimeout(currentRenderTimer); currentRenderTimer = null; }
1026
  if (currentStreamReader) {
1027
+ try { currentStreamReader.cancel(); } catch (_) { }
1028
  currentStreamReader = null;
1029
  }
1030
  if (currentAbortController) {
 
1059
  userInput.value = heroInput.value; sendMessage();
1060
  });
1061
  if (heroInput) {
1062
+ heroInput.addEventListener('input', function () {
1063
  this.style.height = '54px'; this.style.height = this.scrollHeight + 'px';
1064
  });
1065
+ heroInput.addEventListener('keydown', function (e) {
1066
  if (e.key === 'Enter' && !e.shiftKey) {
1067
  e.preventDefault(); userInput.value = heroInput.value; sendMessage();
1068
  }
 
1201
  rawText += data.token;
1202
  scheduleRender();
1203
  }
1204
+ } catch (_) { }
1205
  }
1206
 
1207
  if (!currentStreamStopped) read();
 
1326
  if (!rawText) return;
1327
  const blocks = [];
1328
  let safeText = rawText;
1329
+ safeText = safeText.replace(/\$\$[\s\S]*?\$\$/g, m => { blocks.push(m); return `%%LATEX_${blocks.length - 1}%%`; });
1330
+ safeText = safeText.replace(/\$[^\$\n]+?\$/g, m => { blocks.push(m); return `%%LATEX_${blocks.length - 1}%%`; });
1331
+ safeText = safeText.replace(/\\\[[\s\S]*?\\\]/g, m => { blocks.push(m); return `%%LATEX_${blocks.length - 1}%%`; });
1332
+ safeText = safeText.replace(/\\\([\s\S]*?\\\)/g, m => { blocks.push(m); return `%%LATEX_${blocks.length - 1}%%`; });
1333
  let html = typeof marked !== 'undefined' ? marked.parse(safeText) : safeText;
1334
  blocks.forEach((block, i) => { html = html.replace(`%%LATEX_${i}%%`, block); });
1335
  element.innerHTML = html;
 
1337
  renderMathInElement(element, {
1338
  delimiters: [
1339
  { left: '$$', right: '$$', display: true },
1340
+ { left: '$', right: '$', display: false },
1341
  { left: '\\(', right: '\\)', display: false },
1342
  { left: '\\[', right: '\\]', display: true },
1343
  ],
 
1352
  ============================================================ */
1353
 
1354
  if ('serviceWorker' in navigator) {
1355
+ navigator.serviceWorker.register('/sw.js').catch(() => { });
1356
  }
1357
 
1358
  let deferredPrompt = null;
 
1392
  // Lock screen to portrait on mobile
1393
  try {
1394
  if (screen.orientation && screen.orientation.lock) {
1395
+ screen.orientation.lock('portrait').catch(() => { });
1396
  }
1397
+ } catch (_) { }
1398
 
1399
  window.addEventListener('load', () => {
1400
  checkExistingSession();
static/index.html CHANGED
@@ -215,31 +215,45 @@
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>
@@ -261,38 +275,36 @@
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">
@@ -355,14 +367,6 @@
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">
@@ -413,29 +417,7 @@
413
  <button class="settings-save-btn" id="saveProfileBtn">Save Profile</button>
414
  </div>
415
 
416
- <!-- Teaching Style Tab -->
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>
426
- <div class="persona-desc">Deep, rigorous, first-principles. Goes beyond the textbook.</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>
435
- <div class="persona-desc">Exam tomorrow? Concise bullets, key formulas, no fluff.</div>
436
- </div>
437
- </div>
438
-
439
  <!-- Feedback Tab -->
440
  <div class="tab-content" id="tab-feedback">
441
  <h3 class="settings-section-title">Category</h3>
 
215
  </button>
216
  <input type="file" id="heroImageInput" accept="image/*" style="display:none;">
217
  <textarea id="heroInput" placeholder="Ask STEM Copilot..." rows="1"></textarea>
218
+ <!-- Teaching style selector in hero -->
219
+ <div class="style-selector-wrap" id="heroStyleSelectorWrap">
220
+ <button class="style-selector-btn" id="heroStyleSelectorBtn" title="Teaching style">
221
+ <span id="heroStyleSelectorLabel">Nerd</span>
222
+ <svg viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9" /></svg>
223
+ </button>
224
+ <div class="style-dropdown" id="heroStyleDropdown">
225
+ <div class="style-option active" data-persona="nerd">
226
+ <div class="style-option-name">Nerd</div>
227
+ <div class="style-option-desc">Deep, rigorous, first-principles. Goes beyond the textbook.</div>
228
+ </div>
229
+ <div class="style-option" data-persona="noob">
230
+ <div class="style-option-name">Beginner</div>
231
+ <div class="style-option-desc">Patient, step-by-step. Explains like you're seeing it for the first time.</div>
232
+ </div>
233
+ <div class="style-option" data-persona="thoughtful">
234
+ <div class="style-option-name">Thoughtful</div>
235
+ <div class="style-option-desc">Connects science to the real world. Every formula has a story.</div>
236
+ </div>
237
+ <div class="style-option" data-persona="panic">
238
+ <div class="style-option-name">Panic</div>
239
+ <div class="style-option-desc">Exam tomorrow? Concise bullets, key formulas, no fluff.</div>
240
+ </div>
241
+ </div>
242
+ </div>
243
  <!-- Voice input -->
244
  <button class="mic-btn" id="heroMicBtn" title="Voice input">
245
+ <svg viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" /><path d="M19 10v2a7 7 0 0 1-14 0v-2" /><line x1="12" y1="19" x2="12" y2="23" /><line x1="8" y1="23" x2="16" y2="23" /></svg>
 
 
 
 
 
246
  </button>
247
  <button class="send-btn" id="heroSendBtn">
248
+ <svg viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"></path></svg>
 
 
249
  </button>
250
  </div>
251
  </div>
252
  <div class="hero-pills" id="welcomePills">
253
+ <button class="hero-pill" data-query="What is the photoelectric effect, and why did Einstein win the Nobel Prize for it?">What is the photoelectric effect and why did Einstein win the Nobel Prize for it?</button>
254
+ <button class="hero-pill" data-query="Derive the relation between Kp and Kc for a general gaseous equilibrium reaction">Derive the relation between Kp and Kc for gaseous equilibrium</button>
255
+ <button class="hero-pill" data-query="Solve: A ball is thrown vertically upward with velocity 20 m/s. Find maximum height and time of flight. Take g = 10 m/s^2">A ball thrown up at 20 m/s -- find max height and time of flight</button>
256
+ <button class="hero-pill" data-query="Explain the concept of limits in calculus with a real-world example. How is it different from the actual value of a function?">Explain limits in calculus with a real-world example</button>
 
 
 
 
257
  </div>
258
  </div>
259
  </div>
 
275
  </svg>
276
  </button>
277
  <input type="file" id="imageInput" accept="image/*" style="display:none;">
278
+ <textarea id="userInput" placeholder="Ask STEM Copilot..." rows="1"></textarea>
279
+ <!-- Teaching style selector (RIGHT side, before mic) -->
280
  <div class="style-selector-wrap" id="styleSelectorWrap">
281
  <button class="style-selector-btn" id="styleSelectorBtn" title="Teaching style">
282
  <span id="styleSelectorLabel">Nerd</span>
283
+ <svg viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9" /></svg>
 
 
284
  </button>
285
  <div class="style-dropdown" id="styleDropdown">
286
+ <div class="style-option active" data-persona="vidyut">
287
+ <div class="style-option-name">Vidyut</div>
288
+ <div class="style-option-desc">Calm, clear, step-by-step master teacher. Makes hard concepts obvious.</div>
289
+ </div>
290
+ <div class="style-option" data-persona="nerd">
291
  <div class="style-option-name">Nerd</div>
292
+ <div class="style-option-desc">Deep, rigorous, first-principles. Goes beyond the textbook.</div>
 
293
  </div>
294
  <div class="style-option" data-persona="noob">
295
  <div class="style-option-name">Beginner</div>
296
+ <div class="style-option-desc">Patient, step-by-step. Explains like you're seeing it for the first time.</div>
 
297
  </div>
298
  <div class="style-option" data-persona="thoughtful">
299
  <div class="style-option-name">Thoughtful</div>
300
+ <div class="style-option-desc">Connects science to the real world. Every formula has a story.</div>
 
301
  </div>
302
  <div class="style-option" data-persona="panic">
303
  <div class="style-option-name">Panic</div>
304
+ <div class="style-option-desc">Exam tomorrow? Concise bullets, key formulas, no fluff.</div>
 
305
  </div>
306
  </div>
307
  </div>
 
308
  <!-- Voice input -->
309
  <button class="mic-btn" id="micBtn" title="Voice input">
310
  <svg viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round">
 
367
  </svg>
368
  <span>Profile</span>
369
  </button>
 
 
 
 
 
 
 
 
370
  <button class="settings-nav-btn" data-tab="feedback">
371
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
372
  stroke-width="2">
 
417
  <button class="settings-save-btn" id="saveProfileBtn">Save Profile</button>
418
  </div>
419
 
420
+ <!-- Teaching Style Tab (REMOVED - now in input bar) -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
421
  <!-- Feedback Tab -->
422
  <div class="tab-content" id="tab-feedback">
423
  <h3 class="settings-section-title">Category</h3>
static/style.css CHANGED
@@ -4,9 +4,9 @@
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;
@@ -23,13 +23,16 @@
23
  --radius: 12px;
24
  --radius-lg: 16px;
25
  --radius-xl: 24px;
26
- --transition: 0.25s cubic-bezier(0.4, 0, 0.2, 1);
27
  --sidebar-width: 280px;
28
  --rail-width: 56px;
29
  }
30
 
31
  /* ── LIGHT MODE ── */
32
  [data-theme="light"] {
 
 
 
33
  --bg-main: #f8f9fa;
34
  --bg-sidebar: #ffffff;
35
  --bg-input: #ffffff;
@@ -68,22 +71,21 @@ body {
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
 
@@ -442,8 +444,8 @@ select {
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;
@@ -457,9 +459,9 @@ select {
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 {
@@ -498,9 +500,9 @@ select {
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) {
@@ -605,7 +607,7 @@ select {
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 {
@@ -654,34 +656,54 @@ select {
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;
671
  font-size: 13px;
 
672
  cursor: pointer;
673
  display: flex;
674
  align-items: center;
675
- gap: 10px;
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
 
@@ -820,7 +842,7 @@ select {
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
 
@@ -1276,11 +1298,12 @@ select {
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 {
@@ -1324,7 +1347,7 @@ select {
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);
@@ -1355,6 +1378,10 @@ select {
1355
  }
1356
 
1357
  .style-option.active {
 
 
 
 
1358
  background: rgba(66, 133, 244, 0.08);
1359
  }
1360
 
@@ -1745,6 +1772,10 @@ select {
1745
 
1746
  .persona-option.active {
1747
  border-color: var(--brand);
 
 
 
 
1748
  background: rgba(66, 133, 244, 0.06);
1749
  }
1750
 
@@ -1964,8 +1995,8 @@ select {
1964
  }
1965
 
1966
  .hero-welcome {
1967
- justify-content: flex-start;
1968
- padding: 20px 16px calc(20px + env(safe-area-inset-bottom));
1969
  overflow-y: auto;
1970
  min-height: 0;
1971
  }
 
4
  ============================================================ */
5
 
6
  :root {
7
+ --brand: #EB5A28;
8
+ --brand-hover: #cf4f22;
9
+ --brand-glow: rgba(235, 90, 40, 0.25);
10
  --bg-main: #000000;
11
  --bg-sidebar: #0a0a0a;
12
  --bg-input: #1a1a1a;
 
23
  --radius: 12px;
24
  --radius-lg: 16px;
25
  --radius-xl: 24px;
26
+ --transition: 0.35s cubic-bezier(0.4, 0, 0.2, 1);
27
  --sidebar-width: 280px;
28
  --rail-width: 56px;
29
  }
30
 
31
  /* ── LIGHT MODE ── */
32
  [data-theme="light"] {
33
+ --brand: #4285F4;
34
+ --brand-hover: #3367D6;
35
+ --brand-glow: rgba(66, 133, 244, 0.25);
36
  --bg-main: #f8f9fa;
37
  --bg-sidebar: #ffffff;
38
  --bg-input: #ffffff;
 
71
  touch-action: manipulation;
72
  -webkit-tap-highlight-color: transparent;
73
  overscroll-behavior: none;
74
+ /* Smooth theme transitions β€” uniform 0.35s for all components */
75
+ transition: background-color 0.35s ease, color 0.35s ease;
76
  }
77
 
78
+ /* Smooth transitions for all themed elements β€” UNIFORM timing */
79
  *,
80
  *::before,
81
  *::after {
82
+ transition: background-color 0.35s ease, color 0.35s ease, border-color 0.35s ease, box-shadow 0.35s ease, fill 0.35s ease, stroke 0.35s ease;
83
  }
84
 
 
85
  textarea,
86
  input,
87
  select {
88
+ transition: background-color 0.35s ease, color 0.35s ease, border-color 0.35s ease, box-shadow 0.35s ease;
89
  }
90
 
91
 
 
444
  .new-chat-btn {
445
  margin: 8px 14px;
446
  padding: 12px 16px;
447
+ background: linear-gradient(135deg, rgba(235, 90, 40, 0.08), rgba(235, 90, 40, 0.02));
448
+ border: 1px solid rgba(235, 90, 40, 0.2);
449
  color: var(--text-primary);
450
  border-radius: var(--radius);
451
  cursor: pointer;
 
459
  }
460
 
461
  .new-chat-btn:hover {
462
+ background: linear-gradient(135deg, rgba(235, 90, 40, 0.15), rgba(235, 90, 40, 0.05));
463
+ border-color: rgba(235, 90, 40, 0.4);
464
+ box-shadow: 0 0 16px rgba(235, 90, 40, 0.1);
465
  }
466
 
467
  .new-chat-btn svg {
 
500
  }
501
 
502
  .history-item.active {
503
+ background: linear-gradient(135deg, rgba(235, 90, 40, 0.1), rgba(235, 90, 40, 0.03));
504
  color: var(--text-primary);
505
+ border: 1px solid rgba(235, 90, 40, 0.15);
506
  }
507
 
508
  .history-item:not(.active) {
 
607
  padding: 12px 14px 16px;
608
  border-top: 1px solid var(--border-color);
609
  position: relative;
610
+ background: linear-gradient(180deg, transparent, rgba(235, 90, 40, 0.02));
611
  }
612
 
613
  .user-profile-btn {
 
656
  bottom: 72px;
657
  left: 14px;
658
  right: 14px;
659
+ background: var(--bg-card);
660
  border: 1px solid var(--border-color);
661
  border-radius: var(--radius);
662
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
663
  overflow: hidden;
664
  z-index: 50;
665
+ backdrop-filter: blur(12px);
666
  }
667
 
668
  .user-menu.show {
669
  display: block;
670
+ animation: fadeIn 0.15s ease-out;
671
  }
672
 
673
  .user-menu-item {
674
+ padding: 12px 18px;
675
  font-size: 13px;
676
+ font-weight: 500;
677
  cursor: pointer;
678
  display: flex;
679
  align-items: center;
680
+ gap: 12px;
681
+ transition: background 0.2s, color 0.2s;
682
+ color: var(--text-primary);
683
  }
684
 
685
  .user-menu-item:hover {
686
  background: var(--bg-hover);
687
+ color: var(--brand);
688
+ }
689
+
690
+ .user-menu-item svg {
691
+ opacity: 0.6;
692
+ transition: opacity 0.2s;
693
+ }
694
+
695
+ .user-menu-item:hover svg {
696
+ opacity: 1;
697
  }
698
 
699
  .user-menu-logout {
700
  color: #ff4a4a;
701
+ border-top: 1px solid var(--border-subtle);
702
+ }
703
+
704
+ .user-menu-logout:hover {
705
+ color: #ff4a4a;
706
+ background: rgba(255, 74, 74, 0.06);
707
  }
708
 
709
 
 
842
  width: 120px;
843
  height: 120px;
844
  object-fit: contain;
845
+ filter: drop-shadow(0 8px 24px rgba(235, 90, 40, 0.15));
846
  /* NO animation β€” fixed, no jumping */
847
  }
848
 
 
1298
  color: var(--text-primary);
1299
  }
1300
 
1301
+ /* Teaching style selector in input bar β€” right side before mic */
1302
  .style-selector-wrap {
1303
  position: relative;
1304
  display: flex;
1305
  align-items: center;
1306
+ order: 10; /* push to right */
1307
  }
1308
 
1309
  .style-selector-btn {
 
1347
  display: none;
1348
  position: absolute;
1349
  bottom: 44px;
1350
+ right: 0;
1351
  background: var(--bg-elevated);
1352
  border: 1px solid var(--border-color);
1353
  border-radius: var(--radius);
 
1378
  }
1379
 
1380
  .style-option.active {
1381
+ background: rgba(235, 90, 40, 0.08);
1382
+ }
1383
+
1384
+ [data-theme="light"] .style-option.active {
1385
  background: rgba(66, 133, 244, 0.08);
1386
  }
1387
 
 
1772
 
1773
  .persona-option.active {
1774
  border-color: var(--brand);
1775
+ background: rgba(235, 90, 40, 0.06);
1776
+ }
1777
+
1778
+ [data-theme="light"] .persona-option.active {
1779
  background: rgba(66, 133, 244, 0.06);
1780
  }
1781
 
 
1995
  }
1996
 
1997
  .hero-welcome {
1998
+ justify-content: center;
1999
+ padding: 40px 16px calc(20px + env(safe-area-inset-bottom));
2000
  overflow-y: auto;
2001
  min-height: 0;
2002
  }