Subh775 commited on
Commit
0666741
·
1 Parent(s): 73d6834
Files changed (8) hide show
  1. .gitattributes +3 -0
  2. app.py +13 -0
  3. assets/stembotix.png +0 -0
  4. static/app.js +303 -116
  5. static/index.html +136 -74
  6. static/manifest.json +24 -0
  7. static/style.css +339 -119
  8. sw.js +39 -0
.gitattributes CHANGED
@@ -35,3 +35,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  stem_black.png filter=lfs diff=lfs merge=lfs -text
37
  assets/stem_black.png filter=lfs diff=lfs merge=lfs -text
 
 
 
 
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  stem_black.png filter=lfs diff=lfs merge=lfs -text
37
  assets/stem_black.png filter=lfs diff=lfs merge=lfs -text
38
+ *.png filter=lfs diff=lfs merge=lfs -text
39
+ *.jpeg filter=lfs diff=lfs merge=lfs -text
40
+ *.jpg filter=lfs diff=lfs merge=lfs -text
app.py CHANGED
@@ -155,6 +155,19 @@ def save_profile(req: ProfileRequest):
155
 
156
  # --- Core routes ---
157
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  @app.get("/")
159
  def serve_index():
160
  return FileResponse(os.path.join(STATIC_DIR, "index.html"))
 
155
 
156
  # --- Core routes ---
157
 
158
+ APP_DIR = os.path.dirname(os.path.abspath(__file__))
159
+
160
+ @app.get("/sw.js")
161
+ def serve_sw():
162
+ """Service worker must be served from root for PWA scope."""
163
+ return FileResponse(os.path.join(APP_DIR, "sw.js"), media_type="application/javascript")
164
+
165
+
166
+ @app.get("/manifest.json")
167
+ def serve_manifest():
168
+ return FileResponse(os.path.join(STATIC_DIR, "manifest.json"), media_type="application/manifest+json")
169
+
170
+
171
  @app.get("/")
172
  def serve_index():
173
  return FileResponse(os.path.join(STATIC_DIR, "index.html"))
assets/stembotix.png CHANGED

Git LFS Details

  • SHA256: 85e85e397990f085d574b7363c4b83906a1f93276d72d6770ab0e39172a8ce43
  • Pointer size: 130 Bytes
  • Size of remote file: 55 kB
static/app.js CHANGED
@@ -1,64 +1,86 @@
1
  /* ============================================================
2
- Stem Copilot — Application Logic
3
- Auth, BYOK, Chat, Settings, Image Upload, Streaming
4
  ============================================================ */
5
 
6
  /* ----- State ----- */
7
- let currentUser = null; // { google_id, email, name, picture }
8
  let currentThreadId = crypto.randomUUID();
9
  const threads = [];
10
  let isSending = false;
11
- let pendingImage = null; // base64 string
 
12
 
13
- let currentPersona = localStorage.getItem('anstem_persona') || 'nerd';
14
- let currentLanguage = localStorage.getItem('anstem_language') || 'auto';
15
- let currentUsername = localStorage.getItem('anstem_username') || '';
16
 
17
 
18
  /* ============================================================
19
  DOM REFERENCES
20
  ============================================================ */
21
 
22
- const loginScreen = document.getElementById('loginScreen');
23
- const byokScreen = document.getElementById('byokScreen');
24
- const appContainer = document.getElementById('appContainer');
25
- const sidebar = document.getElementById('sidebar');
26
- const toggleSidebarBtn= document.getElementById('toggleSidebarBtn');
27
- const chatHistoryList = document.getElementById('chatHistoryList');
28
- const chatContainer = document.getElementById('chatContainer');
29
- const welcomeScreen = document.getElementById('welcomeScreen');
30
- const userInput = document.getElementById('userInput');
31
- const sendBtn = document.getElementById('sendBtn');
32
- const newChatBtn = document.getElementById('newChatBtn');
 
 
 
 
 
 
 
 
 
 
33
 
34
  // Auth
35
- const byokInput = document.getElementById('byokInput');
36
- const byokSubmitBtn = document.getElementById('byokSubmitBtn');
37
 
38
  // User menu
39
- const userProfileBtn = document.getElementById('userProfileBtn');
40
- const userMenu = document.getElementById('userMenu');
41
- const userAvatar = document.getElementById('userAvatar');
42
- const userDisplayName = document.getElementById('userDisplayName');
43
- const logoutBtn = document.getElementById('logoutBtn');
 
 
 
 
 
 
44
 
45
  // Settings
46
- const settingsOverlay = document.getElementById('settingsOverlay');
47
- const openSettingsBtn = document.getElementById('openSettingsBtn');
48
- const settingsCloseBtn= document.getElementById('settingsCloseBtn');
49
- const usernameInput = document.getElementById('usernameInput');
50
- const languageSelect = document.getElementById('languageSelect');
51
- const profileInput = document.getElementById('profileInput');
52
- const saveProfileBtn = document.getElementById('saveProfileBtn');
53
  const settingsApiKeyInput = document.getElementById('settingsApiKeyInput');
54
- const saveApiKeyBtn = document.getElementById('saveApiKeyBtn');
55
 
56
- // Image upload
57
- const uploadBtn = document.getElementById('uploadBtn');
58
- const imageInput = document.getElementById('imageInput');
59
- const imagePreviewBar = document.getElementById('imagePreviewBar');
60
  const imagePreviewThumb = document.getElementById('imagePreviewThumb');
61
- const imagePreviewRemove = document.getElementById('imagePreviewRemove');
 
 
 
 
 
62
 
63
 
64
  /* ============================================================
@@ -70,7 +92,6 @@ function initGoogleAuth() {
70
  .then(r => r.json())
71
  .then(data => {
72
  if (!data.client_id) {
73
- // No Google Client ID configured — skip login for dev
74
  showApp();
75
  return;
76
  }
@@ -80,13 +101,10 @@ function initGoogleAuth() {
80
  });
81
  google.accounts.id.renderButton(
82
  document.getElementById('googleSignInBtn'),
83
- { theme: 'filled_black', size: 'large', width: 280, text: 'signin_with' }
84
  );
85
  })
86
- .catch(() => {
87
- // Fallback: skip login
88
- showApp();
89
- });
90
  }
91
 
92
  function handleGoogleCredential(response) {
@@ -97,47 +115,32 @@ function handleGoogleCredential(response) {
97
  })
98
  .then(r => r.json())
99
  .then(data => {
100
- if (data.error) {
101
- alert('Login failed: ' + data.error);
102
- return;
103
- }
104
  currentUser = data.user;
105
- localStorage.setItem('anstem_user', JSON.stringify(currentUser));
106
  currentUsername = currentUser.name;
107
- localStorage.setItem('anstem_username', currentUsername);
108
-
109
- if (!data.has_api_key) {
110
- showByok();
111
- } else {
112
- showApp();
113
- }
114
  })
115
  .catch(() => alert('Login failed. Please try again.'));
116
  }
117
 
118
- // Check if user was already logged in
119
  function checkExistingSession() {
120
- const saved = localStorage.getItem('anstem_user');
121
  if (saved) {
122
  currentUser = JSON.parse(saved);
123
  currentUsername = currentUser.name;
124
- // Verify user still exists on backend
125
  fetch('/auth/me?user_id=' + encodeURIComponent(currentUser.google_id))
126
  .then(r => r.json())
127
  .then(data => {
128
  if (data.error) {
129
- // Session invalid, show login
130
- localStorage.removeItem('anstem_user');
131
  currentUser = null;
132
  initGoogleAuth();
133
  return;
134
  }
135
  currentUser = data.user;
136
- if (!data.has_api_key) {
137
- showByok();
138
- } else {
139
- showApp();
140
- }
141
  })
142
  .catch(() => initGoogleAuth());
143
  } else {
@@ -147,7 +150,7 @@ function checkExistingSession() {
147
 
148
 
149
  /* ============================================================
150
- BYOK — Bring Your Own Key
151
  ============================================================ */
152
 
153
  function showByok() {
@@ -159,7 +162,6 @@ function showByok() {
159
  byokSubmitBtn.addEventListener('click', () => {
160
  const key = byokInput.value.trim();
161
  if (!key) { byokInput.focus(); return; }
162
-
163
  fetch('/user/apikey', {
164
  method: 'POST',
165
  headers: { 'Content-Type': 'application/json' },
@@ -180,13 +182,12 @@ function showApp() {
180
  byokScreen.style.display = 'none';
181
  appContainer.style.display = 'flex';
182
 
183
- // Populate user info in sidebar
184
  if (currentUser) {
185
  userDisplayName.textContent = currentUser.name || 'Student';
186
  if (currentUser.picture) {
187
  userAvatar.src = currentUser.picture;
 
188
  }
189
- // Load user-specific profile data
190
  fetch('/auth/me?user_id=' + encodeURIComponent(currentUser.google_id))
191
  .then(r => r.json())
192
  .then(data => {
@@ -197,7 +198,6 @@ function showApp() {
197
  }).catch(() => {});
198
  }
199
 
200
- // Load settings from localStorage
201
  usernameInput.value = currentUsername;
202
  languageSelect.value = currentLanguage;
203
 
@@ -215,6 +215,33 @@ function showApp() {
215
  renderHistory();
216
  })
217
  .catch(() => {});
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
  }
219
 
220
 
@@ -222,34 +249,110 @@ function showApp() {
222
  SIDEBAR
223
  ============================================================ */
224
 
225
- toggleSidebarBtn.addEventListener('click', () => sidebar.classList.toggle('collapsed'));
 
 
 
 
 
 
226
 
227
- newChatBtn.addEventListener('click', () => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  currentThreadId = crypto.randomUUID();
229
  chatContainer.innerHTML = '';
230
  chatContainer.appendChild(createWelcomeScreen());
 
231
  renderHistory();
232
- });
233
 
234
  function createWelcomeScreen() {
235
  const div = document.createElement('div');
236
- div.className = 'welcome-screen';
237
  div.id = 'welcomeScreen';
238
  div.innerHTML = `
239
- <h1 class="welcome-title">What should we study today?</h1>
240
- <div class="welcome-pills">
241
- <button class="pill" data-query="Explain Coulomb's law with derivation">Coulomb's Law</button>
242
- <button class="pill" data-query="What is sp3 hybridisation? Explain with examples">sp3 Hybridisation</button>
243
- <button class="pill" data-query="Derive the quadratic formula step by step">Quadratic Formula</button>
244
- <button class="pill" data-query="Explain Newton's laws of motion with real world examples">Newton's Laws</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
245
  </div>
246
  `;
247
- div.querySelectorAll('.pill').forEach(p => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
  p.addEventListener('click', () => {
249
  userInput.value = p.dataset.query;
250
  sendMessage();
251
  });
252
  });
 
253
  return div;
254
  }
255
 
@@ -290,13 +393,23 @@ function loadThread(threadId) {
290
  currentThreadId = threadId;
291
  renderHistory();
292
  chatContainer.innerHTML = '';
 
 
293
  fetch('/history/' + threadId)
294
  .then(res => res.json())
295
  .then(data => {
296
  data.messages.forEach(msg => {
297
  const sender = msg.role === 'user' ? 'user' : 'ai';
298
- const el = appendMessage(sender, msg.content);
299
- if (sender === 'ai') renderFinalContent(el, msg.content);
 
 
 
 
 
 
 
 
300
  });
301
  });
302
  }
@@ -390,16 +503,16 @@ settingsOverlay.addEventListener('click', (e) => {
390
  });
391
 
392
  logoutBtn.addEventListener('click', () => {
393
- localStorage.removeItem('anstem_user');
394
- localStorage.removeItem('anstem_username');
395
  currentUser = null;
396
  location.reload();
397
  });
398
 
399
- // Settings tabs
400
- document.querySelectorAll('.tab-btn').forEach(btn => {
401
  btn.addEventListener('click', () => {
402
- document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
403
  document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
404
  btn.classList.add('active');
405
  document.getElementById('tab-' + btn.dataset.tab).classList.add('active');
@@ -409,13 +522,13 @@ document.querySelectorAll('.tab-btn').forEach(btn => {
409
  // Username
410
  usernameInput.addEventListener('input', () => {
411
  currentUsername = usernameInput.value.trim();
412
- localStorage.setItem('anstem_username', currentUsername);
413
  });
414
 
415
  // Language
416
  languageSelect.addEventListener('change', () => {
417
  currentLanguage = languageSelect.value;
418
- localStorage.setItem('anstem_language', currentLanguage);
419
  });
420
 
421
  // Persona selection
@@ -424,11 +537,11 @@ document.querySelectorAll('.persona-option').forEach(opt => {
424
  document.querySelectorAll('.persona-option').forEach(o => o.classList.remove('active'));
425
  opt.classList.add('active');
426
  currentPersona = opt.dataset.persona;
427
- localStorage.setItem('anstem_persona', currentPersona);
428
  });
429
  });
430
 
431
- // Save API key from settings
432
  saveApiKeyBtn.addEventListener('click', () => {
433
  const key = settingsApiKeyInput.value.trim();
434
  if (!key || key.startsWith('••')) return;
@@ -437,9 +550,7 @@ saveApiKeyBtn.addEventListener('click', () => {
437
  method: 'POST',
438
  headers: { 'Content-Type': 'application/json' },
439
  body: JSON.stringify({ user_id: currentUser.google_id, key: key }),
440
- }).then(() => {
441
- settingsApiKeyInput.value = '••••••••••••';
442
- });
443
  });
444
 
445
  // Save student profile
@@ -454,7 +565,7 @@ saveProfileBtn.addEventListener('click', () => {
454
 
455
 
456
  /* ============================================================
457
- IMAGE UPLOAD (button + Ctrl+V paste)
458
  ============================================================ */
459
 
460
  uploadBtn.addEventListener('click', () => imageInput.click());
@@ -464,6 +575,13 @@ imageInput.addEventListener('change', () => {
464
  if (file) handleImageFile(file);
465
  });
466
 
 
 
 
 
 
 
 
467
  // Ctrl+V / Cmd+V paste
468
  document.addEventListener('paste', (e) => {
469
  const items = e.clipboardData?.items;
@@ -481,7 +599,7 @@ function handleImageFile(file) {
481
  const reader = new FileReader();
482
  reader.onload = (e) => {
483
  const dataUrl = e.target.result;
484
- pendingImage = dataUrl.split(',')[1]; // base64 part
485
  imagePreviewThumb.style.backgroundImage = `url(${dataUrl})`;
486
  imagePreviewBar.style.display = 'flex';
487
  };
@@ -496,7 +614,7 @@ imagePreviewRemove.addEventListener('click', () => {
496
 
497
 
498
  /* ============================================================
499
- CHAT & STREAMING
500
  ============================================================ */
501
 
502
  userInput.addEventListener('input', function () {
@@ -513,25 +631,44 @@ userInput.addEventListener('keydown', function (e) {
513
 
514
  sendBtn.addEventListener('click', sendMessage);
515
 
516
- // Welcome pills
517
- document.querySelectorAll('.pill').forEach(p => {
518
  p.addEventListener('click', () => {
519
  userInput.value = p.dataset.query;
520
  sendMessage();
521
  });
522
  });
523
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
524
  function sendMessage() {
525
  const text = userInput.value.trim();
526
  if (!text || isSending) return;
527
 
528
- // Remove welcome screen
529
- const ws = document.getElementById('welcomeScreen');
530
- if (ws) ws.remove();
531
 
532
  isSending = true;
533
 
534
- // Show user message (with image if attached)
535
  if (pendingImage) {
536
  const rowDiv = document.createElement('div');
537
  rowDiv.classList.add('message-row', 'user');
@@ -563,7 +700,6 @@ function sendMessage() {
563
 
564
  function streamResponse(text) {
565
  const imageData = pendingImage || '';
566
- // Clear pending image
567
  pendingImage = null;
568
  imagePreviewBar.style.display = 'none';
569
  imageInput.value = '';
@@ -575,7 +711,7 @@ function streamResponse(text) {
575
  <div class="ai-avatar"></div>
576
  <div style="flex:1; min-width:0;">
577
  <div class="thinking-indicator" id="currentThinking">
578
- <img src="/assets/stem_black.png" class="thinking-logo" />
579
  <span class="thinking-text">Reading your question...</span>
580
  </div>
581
  <div class="message-content" style="display:none;"></div>
@@ -590,6 +726,19 @@ function streamResponse(text) {
590
  let rawText = '';
591
  let firstToken = true;
592
 
 
 
 
 
 
 
 
 
 
 
 
 
 
593
  const payload = {
594
  message: text,
595
  thread_id: currentThreadId,
@@ -612,7 +761,7 @@ function streamResponse(text) {
612
  function read() {
613
  reader.read().then(({ done, value }) => {
614
  if (done) {
615
- finishStream(thinkingEl, contentEl, rawText);
616
  return;
617
  }
618
 
@@ -625,20 +774,18 @@ function streamResponse(text) {
625
  const payload = line.substring(6);
626
 
627
  if (payload === '[DONE]') {
628
- finishStream(thinkingEl, contentEl, rawText);
629
  return;
630
  }
631
 
632
  try {
633
  const data = JSON.parse(payload);
634
 
635
- // Status event
636
  if (data.status === 'thinking') {
637
  thinkingText.textContent = data.message;
638
  return;
639
  }
640
 
641
- // Error event
642
  if (data.error) {
643
  thinkingEl.style.display = 'none';
644
  contentEl.style.display = 'block';
@@ -647,7 +794,6 @@ function streamResponse(text) {
647
  return;
648
  }
649
 
650
- // Token event
651
  if (data.token !== undefined) {
652
  if (firstToken) {
653
  thinkingEl.style.display = 'none';
@@ -656,8 +802,8 @@ function streamResponse(text) {
656
  firstToken = false;
657
  }
658
  rawText += data.token;
659
- contentEl.textContent = rawText;
660
- scrollToBottom();
661
  }
662
  } catch (e) { /* ignore parse errors */ }
663
  });
@@ -675,7 +821,8 @@ function streamResponse(text) {
675
  });
676
  }
677
 
678
- function finishStream(thinkingEl, contentEl, rawText) {
 
679
  thinkingEl.style.display = 'none';
680
  contentEl.style.display = 'block';
681
  contentEl.classList.remove('cursor');
@@ -705,6 +852,7 @@ function scrollToBottom() {
705
  }
706
 
707
  function escapeHtml(text) {
 
708
  const div = document.createElement('div');
709
  div.textContent = text;
710
  return div.innerHTML;
@@ -770,11 +918,50 @@ function renderFinalContent(element, rawText) {
770
  }
771
 
772
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
773
  /* ============================================================
774
  INIT
775
  ============================================================ */
776
 
777
- // Wait for Google Identity Services to load
778
  window.addEventListener('load', () => {
779
  checkExistingSession();
780
  });
 
1
  /* ============================================================
2
+ STEM Copilot — Application Logic
3
+ Auth, BYOK, Chat, Settings, Streaming MD, PWA, Sidebar Rail
4
  ============================================================ */
5
 
6
  /* ----- State ----- */
7
+ let currentUser = null;
8
  let currentThreadId = crypto.randomUUID();
9
  const threads = [];
10
  let isSending = false;
11
+ let pendingImage = null;
12
+ let isHeroMode = true; // hero visible = true
13
 
14
+ let currentPersona = localStorage.getItem('stemcopilot_persona') || 'nerd';
15
+ let currentLanguage = localStorage.getItem('stemcopilot_language') || 'auto';
16
+ let currentUsername = localStorage.getItem('stemcopilot_username') || '';
17
 
18
 
19
  /* ============================================================
20
  DOM REFERENCES
21
  ============================================================ */
22
 
23
+ const loginScreen = document.getElementById('loginScreen');
24
+ const byokScreen = document.getElementById('byokScreen');
25
+ const appContainer = document.getElementById('appContainer');
26
+ const sidebar = document.getElementById('sidebar');
27
+ const sidebarRail = document.getElementById('sidebarRail');
28
+ const toggleSidebarBtn = document.getElementById('toggleSidebarBtn');
29
+ const chatHistoryList = document.getElementById('chatHistoryList');
30
+ const chatContainer = document.getElementById('chatContainer');
31
+ const welcomeScreen = document.getElementById('welcomeScreen');
32
+ const bottomInputContainer = document.getElementById('bottomInputContainer');
33
+
34
+ // Bottom input bar
35
+ const userInput = document.getElementById('userInput');
36
+ const sendBtn = document.getElementById('sendBtn');
37
+ const newChatBtn = document.getElementById('newChatBtn');
38
+
39
+ // Hero input bar
40
+ 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
  // Auth
46
+ const byokInput = document.getElementById('byokInput');
47
+ const byokSubmitBtn = document.getElementById('byokSubmitBtn');
48
 
49
  // User menu
50
+ const userProfileBtn = document.getElementById('userProfileBtn');
51
+ const userMenu = document.getElementById('userMenu');
52
+ const userAvatar = document.getElementById('userAvatar');
53
+ const userDisplayName = document.getElementById('userDisplayName');
54
+ const logoutBtn = document.getElementById('logoutBtn');
55
+
56
+ // Rail
57
+ const railExpandBtn = document.getElementById('railExpandBtn');
58
+ const railNewChatBtn = document.getElementById('railNewChatBtn');
59
+ const railProfileBtn = document.getElementById('railProfileBtn');
60
+ const railAvatar = document.getElementById('railAvatar');
61
 
62
  // Settings
63
+ const settingsOverlay = document.getElementById('settingsOverlay');
64
+ const openSettingsBtn = document.getElementById('openSettingsBtn');
65
+ const settingsCloseBtn = document.getElementById('settingsCloseBtn');
66
+ const usernameInput = document.getElementById('usernameInput');
67
+ const languageSelect = document.getElementById('languageSelect');
68
+ const profileInput = document.getElementById('profileInput');
69
+ const saveProfileBtn = document.getElementById('saveProfileBtn');
70
  const settingsApiKeyInput = document.getElementById('settingsApiKeyInput');
71
+ const saveApiKeyBtn = document.getElementById('saveApiKeyBtn');
72
 
73
+ // Image upload (bottom bar)
74
+ const uploadBtn = document.getElementById('uploadBtn');
75
+ const imageInput = document.getElementById('imageInput');
76
+ const imagePreviewBar = document.getElementById('imagePreviewBar');
77
  const imagePreviewThumb = document.getElementById('imagePreviewThumb');
78
+ const imagePreviewRemove= document.getElementById('imagePreviewRemove');
79
+
80
+ // PWA
81
+ const installBanner = document.getElementById('installBanner');
82
+ const installBtn = document.getElementById('installBtn');
83
+ const installDismiss = document.getElementById('installDismiss');
84
 
85
 
86
  /* ============================================================
 
92
  .then(r => r.json())
93
  .then(data => {
94
  if (!data.client_id) {
 
95
  showApp();
96
  return;
97
  }
 
101
  });
102
  google.accounts.id.renderButton(
103
  document.getElementById('googleSignInBtn'),
104
+ { theme: 'filled_black', size: 'large', width: 280, text: 'signin_with', shape: 'pill' }
105
  );
106
  })
107
+ .catch(() => showApp());
 
 
 
108
  }
109
 
110
  function handleGoogleCredential(response) {
 
115
  })
116
  .then(r => r.json())
117
  .then(data => {
118
+ if (data.error) { alert('Login failed: ' + data.error); return; }
 
 
 
119
  currentUser = data.user;
120
+ localStorage.setItem('stemcopilot_user', JSON.stringify(currentUser));
121
  currentUsername = currentUser.name;
122
+ localStorage.setItem('stemcopilot_username', currentUsername);
123
+ if (!data.has_api_key) { showByok(); } else { showApp(); }
 
 
 
 
 
124
  })
125
  .catch(() => alert('Login failed. Please try again.'));
126
  }
127
 
 
128
  function checkExistingSession() {
129
+ const saved = localStorage.getItem('stemcopilot_user');
130
  if (saved) {
131
  currentUser = JSON.parse(saved);
132
  currentUsername = currentUser.name;
 
133
  fetch('/auth/me?user_id=' + encodeURIComponent(currentUser.google_id))
134
  .then(r => r.json())
135
  .then(data => {
136
  if (data.error) {
137
+ localStorage.removeItem('stemcopilot_user');
 
138
  currentUser = null;
139
  initGoogleAuth();
140
  return;
141
  }
142
  currentUser = data.user;
143
+ if (!data.has_api_key) { showByok(); } else { showApp(); }
 
 
 
 
144
  })
145
  .catch(() => initGoogleAuth());
146
  } else {
 
150
 
151
 
152
  /* ============================================================
153
+ BYOK
154
  ============================================================ */
155
 
156
  function showByok() {
 
162
  byokSubmitBtn.addEventListener('click', () => {
163
  const key = byokInput.value.trim();
164
  if (!key) { byokInput.focus(); return; }
 
165
  fetch('/user/apikey', {
166
  method: 'POST',
167
  headers: { 'Content-Type': 'application/json' },
 
182
  byokScreen.style.display = 'none';
183
  appContainer.style.display = 'flex';
184
 
 
185
  if (currentUser) {
186
  userDisplayName.textContent = currentUser.name || 'Student';
187
  if (currentUser.picture) {
188
  userAvatar.src = currentUser.picture;
189
+ railAvatar.src = currentUser.picture;
190
  }
 
191
  fetch('/auth/me?user_id=' + encodeURIComponent(currentUser.google_id))
192
  .then(r => r.json())
193
  .then(data => {
 
198
  }).catch(() => {});
199
  }
200
 
 
201
  usernameInput.value = currentUsername;
202
  languageSelect.value = currentLanguage;
203
 
 
215
  renderHistory();
216
  })
217
  .catch(() => {});
218
+
219
+ // Show hero mode
220
+ enterHeroMode();
221
+ }
222
+
223
+
224
+ /* ============================================================
225
+ HERO / BOTTOM INPUT MODE SWITCHING
226
+ ============================================================ */
227
+
228
+ function enterHeroMode() {
229
+ isHeroMode = true;
230
+ if (welcomeScreen) welcomeScreen.style.display = '';
231
+ bottomInputContainer.style.display = 'none';
232
+ // Rebuild hero welcome if needed
233
+ const existing = document.getElementById('welcomeScreen');
234
+ if (!existing) {
235
+ chatContainer.innerHTML = '';
236
+ chatContainer.appendChild(createWelcomeScreen());
237
+ }
238
+ }
239
+
240
+ function exitHeroMode() {
241
+ isHeroMode = false;
242
+ const ws = document.getElementById('welcomeScreen');
243
+ if (ws) ws.remove();
244
+ bottomInputContainer.style.display = '';
245
  }
246
 
247
 
 
249
  SIDEBAR
250
  ============================================================ */
251
 
252
+ let sidebarCollapsed = false;
253
+
254
+ function collapseSidebar() {
255
+ sidebarCollapsed = true;
256
+ sidebar.classList.add('collapsed');
257
+ sidebarRail.classList.add('visible');
258
+ }
259
 
260
+ function expandSidebar() {
261
+ sidebarCollapsed = false;
262
+ sidebar.classList.remove('collapsed');
263
+ sidebarRail.classList.remove('visible');
264
+ }
265
+
266
+ toggleSidebarBtn.addEventListener('click', () => {
267
+ if (sidebarCollapsed) expandSidebar();
268
+ else collapseSidebar();
269
+ });
270
+
271
+ railExpandBtn.addEventListener('click', expandSidebar);
272
+
273
+ railNewChatBtn.addEventListener('click', () => {
274
+ startNewChat();
275
+ });
276
+
277
+ railProfileBtn.addEventListener('click', () => {
278
+ expandSidebar();
279
+ setTimeout(() => userProfileBtn.click(), 150);
280
+ });
281
+
282
+ newChatBtn.addEventListener('click', startNewChat);
283
+
284
+ function startNewChat() {
285
  currentThreadId = crypto.randomUUID();
286
  chatContainer.innerHTML = '';
287
  chatContainer.appendChild(createWelcomeScreen());
288
+ enterHeroMode();
289
  renderHistory();
290
+ }
291
 
292
  function createWelcomeScreen() {
293
  const div = document.createElement('div');
294
+ div.className = 'hero-welcome';
295
  div.id = 'welcomeScreen';
296
  div.innerHTML = `
297
+ <img src="/assets/bot.png" alt="STEM Copilot" class="hero-bot-icon">
298
+ <h1 class="hero-title">What should we study today?</h1>
299
+ <div class="hero-input-wrap">
300
+ <div class="hero-input-box" id="heroInputBoxDynamic">
301
+ <button class="upload-btn" id="heroUploadBtnDynamic" title="Upload image">
302
+ <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>
303
+ </button>
304
+ <input type="file" id="heroImageInputDynamic" accept="image/*" style="display:none;">
305
+ <textarea id="heroInputDynamic" placeholder="Ask anything about Physics, Chemistry or Maths..." rows="1"></textarea>
306
+ <button class="send-btn" id="heroSendBtnDynamic">
307
+ <svg viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"></path></svg>
308
+ </button>
309
+ </div>
310
+ </div>
311
+ <div class="hero-pills">
312
+ <button class="hero-pill" data-query="Explain Coulomb's law with derivation">⚡ Coulomb's Law</button>
313
+ <button class="hero-pill" data-query="What is sp3 hybridisation? Explain with examples">🧪 sp3 Hybridisation</button>
314
+ <button class="hero-pill" data-query="Derive the quadratic formula step by step">📐 Quadratic Formula</button>
315
+ <button class="hero-pill" data-query="Explain Newton's laws of motion with real world examples">🍎 Newton's Laws</button>
316
  </div>
317
  `;
318
+
319
+ // Attach events to dynamic elements
320
+ const dynInput = div.querySelector('#heroInputDynamic');
321
+ const dynSend = div.querySelector('#heroSendBtnDynamic');
322
+ const dynUpload = div.querySelector('#heroUploadBtnDynamic');
323
+ const dynFileInput = div.querySelector('#heroImageInputDynamic');
324
+
325
+ dynInput.addEventListener('input', function() {
326
+ this.style.height = '54px';
327
+ this.style.height = this.scrollHeight + 'px';
328
+ });
329
+
330
+ dynInput.addEventListener('keydown', function(e) {
331
+ if (e.key === 'Enter' && !e.shiftKey) {
332
+ e.preventDefault();
333
+ userInput.value = dynInput.value;
334
+ sendMessage();
335
+ }
336
+ });
337
+
338
+ dynSend.addEventListener('click', () => {
339
+ userInput.value = dynInput.value;
340
+ sendMessage();
341
+ });
342
+
343
+ dynUpload.addEventListener('click', () => dynFileInput.click());
344
+ dynFileInput.addEventListener('change', () => {
345
+ const file = dynFileInput.files[0];
346
+ if (file) handleImageFile(file);
347
+ });
348
+
349
+ div.querySelectorAll('.hero-pill').forEach(p => {
350
  p.addEventListener('click', () => {
351
  userInput.value = p.dataset.query;
352
  sendMessage();
353
  });
354
  });
355
+
356
  return div;
357
  }
358
 
 
393
  currentThreadId = threadId;
394
  renderHistory();
395
  chatContainer.innerHTML = '';
396
+ exitHeroMode();
397
+
398
  fetch('/history/' + threadId)
399
  .then(res => res.json())
400
  .then(data => {
401
  data.messages.forEach(msg => {
402
  const sender = msg.role === 'user' ? 'user' : 'ai';
403
+ // Handle multimodal messages (array content with text + image_url)
404
+ let textContent = msg.content;
405
+ if (Array.isArray(msg.content)) {
406
+ textContent = msg.content
407
+ .filter(part => part.type === 'text')
408
+ .map(part => part.text)
409
+ .join('');
410
+ }
411
+ const el = appendMessage(sender, textContent);
412
+ if (sender === 'ai') renderFinalContent(el, textContent);
413
  });
414
  });
415
  }
 
503
  });
504
 
505
  logoutBtn.addEventListener('click', () => {
506
+ localStorage.removeItem('stemcopilot_user');
507
+ localStorage.removeItem('stemcopilot_username');
508
  currentUser = null;
509
  location.reload();
510
  });
511
 
512
+ // Settings tabs (vertical nav)
513
+ document.querySelectorAll('.settings-nav-btn').forEach(btn => {
514
  btn.addEventListener('click', () => {
515
+ document.querySelectorAll('.settings-nav-btn').forEach(b => b.classList.remove('active'));
516
  document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
517
  btn.classList.add('active');
518
  document.getElementById('tab-' + btn.dataset.tab).classList.add('active');
 
522
  // Username
523
  usernameInput.addEventListener('input', () => {
524
  currentUsername = usernameInput.value.trim();
525
+ localStorage.setItem('stemcopilot_username', currentUsername);
526
  });
527
 
528
  // Language
529
  languageSelect.addEventListener('change', () => {
530
  currentLanguage = languageSelect.value;
531
+ localStorage.setItem('stemcopilot_language', currentLanguage);
532
  });
533
 
534
  // Persona selection
 
537
  document.querySelectorAll('.persona-option').forEach(o => o.classList.remove('active'));
538
  opt.classList.add('active');
539
  currentPersona = opt.dataset.persona;
540
+ localStorage.setItem('stemcopilot_persona', currentPersona);
541
  });
542
  });
543
 
544
+ // Save API key
545
  saveApiKeyBtn.addEventListener('click', () => {
546
  const key = settingsApiKeyInput.value.trim();
547
  if (!key || key.startsWith('••')) return;
 
550
  method: 'POST',
551
  headers: { 'Content-Type': 'application/json' },
552
  body: JSON.stringify({ user_id: currentUser.google_id, key: key }),
553
+ }).then(() => { settingsApiKeyInput.value = '••••••••••••'; });
 
 
554
  });
555
 
556
  // Save student profile
 
565
 
566
 
567
  /* ============================================================
568
+ IMAGE UPLOAD
569
  ============================================================ */
570
 
571
  uploadBtn.addEventListener('click', () => imageInput.click());
 
575
  if (file) handleImageFile(file);
576
  });
577
 
578
+ // Hero upload buttons (static — for initial page load)
579
+ if (heroUploadBtn) heroUploadBtn.addEventListener('click', () => heroImageInput.click());
580
+ if (heroImageInput) heroImageInput.addEventListener('change', () => {
581
+ const file = heroImageInput.files[0];
582
+ if (file) handleImageFile(file);
583
+ });
584
+
585
  // Ctrl+V / Cmd+V paste
586
  document.addEventListener('paste', (e) => {
587
  const items = e.clipboardData?.items;
 
599
  const reader = new FileReader();
600
  reader.onload = (e) => {
601
  const dataUrl = e.target.result;
602
+ pendingImage = dataUrl.split(',')[1];
603
  imagePreviewThumb.style.backgroundImage = `url(${dataUrl})`;
604
  imagePreviewBar.style.display = 'flex';
605
  };
 
614
 
615
 
616
  /* ============================================================
617
+ CHAT & STREAMING WITH LIVE MARKDOWN RENDERING
618
  ============================================================ */
619
 
620
  userInput.addEventListener('input', function () {
 
631
 
632
  sendBtn.addEventListener('click', sendMessage);
633
 
634
+ // Hero pills (static — for initial page load)
635
+ document.querySelectorAll('.hero-pill').forEach(p => {
636
  p.addEventListener('click', () => {
637
  userInput.value = p.dataset.query;
638
  sendMessage();
639
  });
640
  });
641
 
642
+ // Hero send (static)
643
+ if (heroSendBtn) heroSendBtn.addEventListener('click', () => {
644
+ userInput.value = heroInput.value;
645
+ sendMessage();
646
+ });
647
+
648
+ if (heroInput) {
649
+ heroInput.addEventListener('input', function() {
650
+ this.style.height = '54px';
651
+ this.style.height = this.scrollHeight + 'px';
652
+ });
653
+ heroInput.addEventListener('keydown', function(e) {
654
+ if (e.key === 'Enter' && !e.shiftKey) {
655
+ e.preventDefault();
656
+ userInput.value = heroInput.value;
657
+ sendMessage();
658
+ }
659
+ });
660
+ }
661
+
662
  function sendMessage() {
663
  const text = userInput.value.trim();
664
  if (!text || isSending) return;
665
 
666
+ // Exit hero mode
667
+ if (isHeroMode) exitHeroMode();
 
668
 
669
  isSending = true;
670
 
671
+ // Show user message
672
  if (pendingImage) {
673
  const rowDiv = document.createElement('div');
674
  rowDiv.classList.add('message-row', 'user');
 
700
 
701
  function streamResponse(text) {
702
  const imageData = pendingImage || '';
 
703
  pendingImage = null;
704
  imagePreviewBar.style.display = 'none';
705
  imageInput.value = '';
 
711
  <div class="ai-avatar"></div>
712
  <div style="flex:1; min-width:0;">
713
  <div class="thinking-indicator" id="currentThinking">
714
+ <img src="/assets/bot.png" class="thinking-logo" />
715
  <span class="thinking-text">Reading your question...</span>
716
  </div>
717
  <div class="message-content" style="display:none;"></div>
 
726
  let rawText = '';
727
  let firstToken = true;
728
 
729
+ // Throttled live markdown rendering
730
+ let renderTimer = null;
731
+ const RENDER_INTERVAL = 120; // ms
732
+
733
+ function scheduleRender() {
734
+ if (renderTimer) return;
735
+ renderTimer = setTimeout(() => {
736
+ renderTimer = null;
737
+ renderFinalContent(contentEl, rawText);
738
+ scrollToBottom();
739
+ }, RENDER_INTERVAL);
740
+ }
741
+
742
  const payload = {
743
  message: text,
744
  thread_id: currentThreadId,
 
761
  function read() {
762
  reader.read().then(({ done, value }) => {
763
  if (done) {
764
+ finishStream(thinkingEl, contentEl, rawText, renderTimer);
765
  return;
766
  }
767
 
 
774
  const payload = line.substring(6);
775
 
776
  if (payload === '[DONE]') {
777
+ finishStream(thinkingEl, contentEl, rawText, renderTimer);
778
  return;
779
  }
780
 
781
  try {
782
  const data = JSON.parse(payload);
783
 
 
784
  if (data.status === 'thinking') {
785
  thinkingText.textContent = data.message;
786
  return;
787
  }
788
 
 
789
  if (data.error) {
790
  thinkingEl.style.display = 'none';
791
  contentEl.style.display = 'block';
 
794
  return;
795
  }
796
 
 
797
  if (data.token !== undefined) {
798
  if (firstToken) {
799
  thinkingEl.style.display = 'none';
 
802
  firstToken = false;
803
  }
804
  rawText += data.token;
805
+ // Live render markdown as tokens stream
806
+ scheduleRender();
807
  }
808
  } catch (e) { /* ignore parse errors */ }
809
  });
 
821
  });
822
  }
823
 
824
+ function finishStream(thinkingEl, contentEl, rawText, renderTimer) {
825
+ if (renderTimer) clearTimeout(renderTimer);
826
  thinkingEl.style.display = 'none';
827
  contentEl.style.display = 'block';
828
  contentEl.classList.remove('cursor');
 
852
  }
853
 
854
  function escapeHtml(text) {
855
+ if (typeof text !== 'string') return '';
856
  const div = document.createElement('div');
857
  div.textContent = text;
858
  return div.innerHTML;
 
918
  }
919
 
920
 
921
+ /* ============================================================
922
+ PWA — Service Worker + Install Prompt
923
+ ============================================================ */
924
+
925
+ // Register service worker
926
+ if ('serviceWorker' in navigator) {
927
+ navigator.serviceWorker.register('/sw.js').catch(() => {});
928
+ }
929
+
930
+ // Capture beforeinstallprompt for custom install banner
931
+ let deferredPrompt = null;
932
+
933
+ window.addEventListener('beforeinstallprompt', (e) => {
934
+ e.preventDefault();
935
+ deferredPrompt = e;
936
+ // Only show on mobile
937
+ if (/Mobi|Android/i.test(navigator.userAgent)) {
938
+ const dismissed = localStorage.getItem('stemcopilot_install_dismissed');
939
+ if (!dismissed) {
940
+ installBanner.style.display = 'flex';
941
+ }
942
+ }
943
+ });
944
+
945
+ installBtn.addEventListener('click', () => {
946
+ if (deferredPrompt) {
947
+ deferredPrompt.prompt();
948
+ deferredPrompt.userChoice.then(() => {
949
+ deferredPrompt = null;
950
+ installBanner.style.display = 'none';
951
+ });
952
+ }
953
+ });
954
+
955
+ installDismiss.addEventListener('click', () => {
956
+ installBanner.style.display = 'none';
957
+ localStorage.setItem('stemcopilot_install_dismissed', '1');
958
+ });
959
+
960
+
961
  /* ============================================================
962
  INIT
963
  ============================================================ */
964
 
 
965
  window.addEventListener('load', () => {
966
  checkExistingSession();
967
  });
static/index.html CHANGED
@@ -2,11 +2,14 @@
2
  <html lang="en">
3
  <head>
4
  <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Stem Copilot</title>
7
  <meta name="description" content="AI-powered NCERT tutor for Class XI and XII Physics, Chemistry, and Mathematics.">
8
- <link rel="icon" type="image/png" href="/assets/stem_black.png">
9
- <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet">
 
 
 
10
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css">
11
  <link rel="stylesheet" href="/static/style.css">
12
  <!-- Google Identity Services -->
@@ -17,8 +20,8 @@
17
  <!-- ==================== LOGIN SCREEN ==================== -->
18
  <div class="login-screen" id="loginScreen">
19
  <div class="login-card">
20
- <img src="/assets/stembotix.png" alt="STEMbotix" class="login-logo">
21
- <h1 class="login-title">Welcome to Stem Copilot</h1>
22
  <p class="login-subtitle">Your personal NCERT tutor for Physics, Chemistry & Mathematics</p>
23
  <div id="googleSignInBtn" class="google-btn-wrap"></div>
24
  <p class="login-footer">By signing in, you agree to our Terms of Service.</p>
@@ -30,7 +33,7 @@
30
  <div class="byok-card">
31
  <h2>One Last Step</h2>
32
  <p class="byok-desc">
33
- Stem Copilot uses your own OpenRouter API key to power the tutor.
34
  It's free — takes 30 seconds to set up.
35
  </p>
36
  <ol class="byok-steps">
@@ -44,12 +47,26 @@
44
  </div>
45
  </div>
46
 
 
 
 
 
 
 
 
 
 
 
 
47
  <!-- ==================== MAIN APP ==================== -->
48
  <div class="app-container" id="appContainer" style="display:none;">
49
 
50
- <!-- Sidebar -->
51
  <aside class="sidebar" id="sidebar">
52
  <div class="sidebar-top">
 
 
 
53
  <button class="new-chat-btn" id="newChatBtn">
54
  <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>
55
  New Chat
@@ -77,6 +94,23 @@
77
  </div>
78
  </aside>
79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  <!-- Main -->
81
  <main class="main-chat">
82
  <header class="main-header">
@@ -86,19 +120,33 @@
86
  </header>
87
 
88
  <div class="chat-container" id="chatContainer">
89
- <!-- Welcome screen -->
90
- <div class="welcome-screen" id="welcomeScreen">
91
- <h1 class="welcome-title">What should we study today?</h1>
92
- <div class="welcome-pills" id="welcomePills">
93
- <button class="pill" data-query="Explain Coulomb's law with derivation">Coulomb's Law</button>
94
- <button class="pill" data-query="What is sp3 hybridisation? Explain with examples">sp3 Hybridisation</button>
95
- <button class="pill" data-query="Derive the quadratic formula step by step">Quadratic Formula</button>
96
- <button class="pill" data-query="Explain Newton's laws of motion with real world examples">Newton's Laws</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  </div>
98
  </div>
99
  </div>
100
 
101
- <div class="input-container">
 
102
  <!-- Image preview -->
103
  <div class="image-preview-bar" id="imagePreviewBar" style="display:none;">
104
  <div class="image-preview-thumb" id="imagePreviewThumb"></div>
@@ -109,13 +157,13 @@
109
  <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>
110
  </button>
111
  <input type="file" id="imageInput" accept="image/*" style="display:none;">
112
- <textarea id="userInput" placeholder="Ask Stem Copilot..." rows="1"></textarea>
113
  <button class="send-btn" id="sendBtn">
114
  <svg viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"></path></svg>
115
  </button>
116
  </div>
117
  <div class="disclaimer-text">
118
- Stem Copilot is an AI tutor grounded in NCERT material. It can still make mistakes.
119
  </div>
120
  </div>
121
  </main>
@@ -128,66 +176,80 @@
128
  <h2>Settings</h2>
129
  <button class="modal-close" id="settingsCloseBtn">&times;</button>
130
  </div>
131
- <div class="modal-body">
132
- <!-- Tab Navigation -->
133
- <div class="settings-tabs">
134
- <button class="tab-btn active" data-tab="general">General</button>
135
- <button class="tab-btn" data-tab="apikeys">API Keys</button>
136
- <button class="tab-btn" data-tab="personalization">Personalization</button>
137
- <button class="tab-btn" data-tab="style">Teaching Style</button>
138
- </div>
139
-
140
- <!-- General Tab -->
141
- <div class="tab-content active" id="tab-general">
142
- <label class="settings-label">Language</label>
143
- <select class="settings-select" id="languageSelect">
144
- <option value="auto">Auto-detect</option>
145
- <option value="english">English</option>
146
- <option value="hinglish">Hinglish</option>
147
- <option value="hindi">Hindi</option>
148
- <option value="gujarati">Gujarati</option>
149
- <option value="marathi">Marathi</option>
150
- </select>
151
- </div>
152
-
153
- <!-- API Keys Tab -->
154
- <div class="tab-content" id="tab-apikeys">
155
- <label class="settings-label">OpenRouter API Key</label>
156
- <input type="password" class="settings-input" id="settingsApiKeyInput" placeholder="sk-or-..." autocomplete="off" spellcheck="false">
157
- <button class="settings-save-btn" id="saveApiKeyBtn">Save Key</button>
158
- <p class="settings-hint">
159
- Get your free key at <a href="https://openrouter.ai/workspaces/default/keys" target="_blank" rel="noopener">openrouter.ai/keys</a>
160
- </p>
161
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
 
163
- <!-- Personalization Tab -->
164
- <div class="tab-content" id="tab-personalization">
165
- <label class="settings-label">Your Name</label>
166
- <input type="text" class="settings-input" id="usernameInput" placeholder="What should I call you?" maxlength="30" autocomplete="off">
 
 
 
 
 
167
 
168
- <label class="settings-label" style="margin-top:16px;">About You</label>
169
- <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>
170
- <button class="settings-save-btn" id="saveProfileBtn">Save Profile</button>
171
- </div>
172
 
173
- <!-- Teaching Style Tab -->
174
- <div class="tab-content" id="tab-style">
175
- <p class="settings-hint" style="margin-bottom:12px;">All styles are equally effective. Pick the mood that suits you today.</p>
176
- <div class="persona-option" data-persona="noob">
177
- <div class="persona-name">Noob</div>
178
- <div class="persona-desc">Patient, step-by-step. Explains like you're seeing it for the first time. Uses daily-life analogies.</div>
179
  </div>
180
- <div class="persona-option active" data-persona="nerd">
181
- <div class="persona-name">Nerd</div>
182
- <div class="persona-desc">Deep, rigorous, first-principles. Goes beyond the textbook. For the kitabi keeda in you.</div>
183
- </div>
184
- <div class="persona-option" data-persona="thoughtful">
185
- <div class="persona-name">Thoughtful</div>
186
- <div class="persona-desc">Connects science to the real world. Every formula has a story. Warm, curious, like a mentor over chai.</div>
187
- </div>
188
- <div class="persona-option" data-persona="panic">
189
- <div class="persona-name">Panic</div>
190
- <div class="persona-desc">Exam tomorrow? Concise bullets, key formulas, mnemonics, and quick examples. No fluff, just results.</div>
 
 
 
 
 
 
 
 
 
191
  </div>
192
  </div>
193
  </div>
 
2
  <html lang="en">
3
  <head>
4
  <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
6
+ <title>STEM Copilot</title>
7
  <meta name="description" content="AI-powered NCERT tutor for Class XI and XII Physics, Chemistry, and Mathematics.">
8
+ <meta name="theme-color" content="#0a0a0a">
9
+ <link rel="icon" type="image/png" href="/assets/bot.png">
10
+ <link rel="manifest" href="/manifest.json">
11
+ <link rel="apple-touch-icon" href="/assets/stem_black.png">
12
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
13
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css">
14
  <link rel="stylesheet" href="/static/style.css">
15
  <!-- Google Identity Services -->
 
20
  <!-- ==================== LOGIN SCREEN ==================== -->
21
  <div class="login-screen" id="loginScreen">
22
  <div class="login-card">
23
+ <img src="/assets/bot.png" alt="STEM Copilot" class="login-logo">
24
+ <h1 class="login-title">Welcome to STEM Copilot</h1>
25
  <p class="login-subtitle">Your personal NCERT tutor for Physics, Chemistry & Mathematics</p>
26
  <div id="googleSignInBtn" class="google-btn-wrap"></div>
27
  <p class="login-footer">By signing in, you agree to our Terms of Service.</p>
 
33
  <div class="byok-card">
34
  <h2>One Last Step</h2>
35
  <p class="byok-desc">
36
+ STEM Copilot uses your own OpenRouter API key to power the tutor.
37
  It's free — takes 30 seconds to set up.
38
  </p>
39
  <ol class="byok-steps">
 
47
  </div>
48
  </div>
49
 
50
+ <!-- ==================== PWA INSTALL BANNER ==================== -->
51
+ <div class="install-banner" id="installBanner" style="display:none;">
52
+ <img src="/assets/bot.png" alt="" class="install-banner-icon">
53
+ <div class="install-banner-text">
54
+ <strong>STEM Copilot</strong>
55
+ <span>Install as app for faster access</span>
56
+ </div>
57
+ <button class="install-banner-btn" id="installBtn">Install</button>
58
+ <button class="install-banner-dismiss" id="installDismiss">&times;</button>
59
+ </div>
60
+
61
  <!-- ==================== MAIN APP ==================== -->
62
  <div class="app-container" id="appContainer" style="display:none;">
63
 
64
+ <!-- Sidebar (full) -->
65
  <aside class="sidebar" id="sidebar">
66
  <div class="sidebar-top">
67
+ <div class="sidebar-logo-row">
68
+ <img src="/assets/stembotix.png" alt="STEMbotix" class="sidebar-logo">
69
+ </div>
70
  <button class="new-chat-btn" id="newChatBtn">
71
  <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>
72
  New Chat
 
94
  </div>
95
  </aside>
96
 
97
+ <!-- Sidebar rail (collapsed) -->
98
+ <aside class="sidebar-rail" id="sidebarRail">
99
+ <div class="rail-top">
100
+ <button class="rail-icon-btn" id="railExpandBtn" title="Open sidebar">
101
+ <img src="/assets/stem_black.png" alt="S" class="rail-logo">
102
+ </button>
103
+ <button class="rail-icon-btn" id="railNewChatBtn" title="New chat">
104
+ <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>
105
+ </button>
106
+ </div>
107
+ <div class="rail-bottom">
108
+ <button class="rail-icon-btn" id="railProfileBtn" title="Profile">
109
+ <img id="railAvatar" class="rail-avatar" src="" alt="">
110
+ </button>
111
+ </div>
112
+ </aside>
113
+
114
  <!-- Main -->
115
  <main class="main-chat">
116
  <header class="main-header">
 
120
  </header>
121
 
122
  <div class="chat-container" id="chatContainer">
123
+ <!-- Hero / Welcome screen -->
124
+ <div class="hero-welcome" id="welcomeScreen">
125
+ <img src="/assets/bot.png" alt="STEM Copilot" class="hero-bot-icon">
126
+ <h1 class="hero-title">What should we study today?</h1>
127
+ <div class="hero-input-wrap">
128
+ <div class="hero-input-box" id="heroInputBox">
129
+ <button class="upload-btn" id="heroUploadBtn" title="Upload image">
130
+ <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>
131
+ </button>
132
+ <input type="file" id="heroImageInput" accept="image/*" style="display:none;">
133
+ <textarea id="heroInput" placeholder="Ask anything about Physics, Chemistry or Maths..." rows="1"></textarea>
134
+ <button class="send-btn" id="heroSendBtn">
135
+ <svg viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"></path></svg>
136
+ </button>
137
+ </div>
138
+ </div>
139
+ <div class="hero-pills" id="welcomePills">
140
+ <button class="hero-pill" data-query="Explain Coulomb's law with derivation">⚡ Coulomb's Law</button>
141
+ <button class="hero-pill" data-query="What is sp3 hybridisation? Explain with examples">🧪 sp3 Hybridisation</button>
142
+ <button class="hero-pill" data-query="Derive the quadratic formula step by step">📐 Quadratic Formula</button>
143
+ <button class="hero-pill" data-query="Explain Newton's laws of motion with real world examples">🍎 Newton's Laws</button>
144
  </div>
145
  </div>
146
  </div>
147
 
148
+ <!-- Bottom input (hidden during hero, shown during chat) -->
149
+ <div class="input-container" id="bottomInputContainer" style="display:none;">
150
  <!-- Image preview -->
151
  <div class="image-preview-bar" id="imagePreviewBar" style="display:none;">
152
  <div class="image-preview-thumb" id="imagePreviewThumb"></div>
 
157
  <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>
158
  </button>
159
  <input type="file" id="imageInput" accept="image/*" style="display:none;">
160
+ <textarea id="userInput" placeholder="Ask STEM Copilot..." rows="1"></textarea>
161
  <button class="send-btn" id="sendBtn">
162
  <svg viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"></path></svg>
163
  </button>
164
  </div>
165
  <div class="disclaimer-text">
166
+ STEM Copilot is an AI tutor grounded in NCERT material. It can still make mistakes.
167
  </div>
168
  </div>
169
  </main>
 
176
  <h2>Settings</h2>
177
  <button class="modal-close" id="settingsCloseBtn">&times;</button>
178
  </div>
179
+ <div class="modal-body settings-layout">
180
+ <!-- Vertical Tab Navigation -->
181
+ <div class="settings-nav">
182
+ <button class="settings-nav-btn active" data-tab="general">
183
+ <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>
184
+ General
185
+ </button>
186
+ <button class="settings-nav-btn" data-tab="apikeys">
187
+ <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>
188
+ API Keys
189
+ </button>
190
+ <button class="settings-nav-btn" data-tab="personalization">
191
+ <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>
192
+ Profile
193
+ </button>
194
+ <button class="settings-nav-btn" data-tab="style">
195
+ <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>
196
+ Teaching Style
197
+ </button>
 
 
 
 
 
 
 
 
 
 
 
198
  </div>
199
+ <!-- Settings Content Panel -->
200
+ <div class="settings-panel">
201
+ <!-- General Tab -->
202
+ <div class="tab-content active" id="tab-general">
203
+ <h3 class="settings-section-title">Language</h3>
204
+ <select class="settings-select" id="languageSelect">
205
+ <option value="auto">Auto-detect</option>
206
+ <option value="english">English</option>
207
+ <option value="hinglish">Hinglish</option>
208
+ <option value="hindi">Hindi</option>
209
+ <option value="gujarati">Gujarati</option>
210
+ <option value="marathi">Marathi</option>
211
+ </select>
212
+ </div>
213
 
214
+ <!-- API Keys Tab -->
215
+ <div class="tab-content" id="tab-apikeys">
216
+ <h3 class="settings-section-title">OpenRouter API Key</h3>
217
+ <input type="password" class="settings-input" id="settingsApiKeyInput" placeholder="sk-or-..." autocomplete="off" spellcheck="false">
218
+ <button class="settings-save-btn" id="saveApiKeyBtn">Save Key</button>
219
+ <p class="settings-hint">
220
+ Get your free key at <a href="https://openrouter.ai/workspaces/default/keys" target="_blank" rel="noopener">openrouter.ai/keys</a>
221
+ </p>
222
+ </div>
223
 
224
+ <!-- Personalization Tab -->
225
+ <div class="tab-content" id="tab-personalization">
226
+ <h3 class="settings-section-title">Your Name</h3>
227
+ <input type="text" class="settings-input" id="usernameInput" placeholder="What should I call you?" maxlength="30" autocomplete="off">
228
 
229
+ <h3 class="settings-section-title" style="margin-top:20px;">About You</h3>
230
+ <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>
231
+ <button class="settings-save-btn" id="saveProfileBtn">Save Profile</button>
 
 
 
232
  </div>
233
+
234
+ <!-- Teaching Style Tab -->
235
+ <div class="tab-content" id="tab-style">
236
+ <p class="settings-hint" style="margin-bottom:14px;">Pick the mood that suits you today.</p>
237
+ <div class="persona-option" data-persona="noob">
238
+ <div class="persona-name">Noob</div>
239
+ <div class="persona-desc">Patient, step-by-step. Explains like you're seeing it for the first time.</div>
240
+ </div>
241
+ <div class="persona-option active" data-persona="nerd">
242
+ <div class="persona-name">Nerd</div>
243
+ <div class="persona-desc">Deep, rigorous, first-principles. Goes beyond the textbook.</div>
244
+ </div>
245
+ <div class="persona-option" data-persona="thoughtful">
246
+ <div class="persona-name">Thoughtful</div>
247
+ <div class="persona-desc">Connects science to the real world. Every formula has a story.</div>
248
+ </div>
249
+ <div class="persona-option" data-persona="panic">
250
+ <div class="persona-name">Panic</div>
251
+ <div class="persona-desc">Exam tomorrow? Concise bullets, key formulas, no fluff.</div>
252
+ </div>
253
  </div>
254
  </div>
255
  </div>
static/manifest.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "STEM Copilot",
3
+ "short_name": "STEM Copilot",
4
+ "description": "AI-powered NCERT tutor for Class XI & XII — Physics, Chemistry, Mathematics",
5
+ "start_url": "/",
6
+ "display": "standalone",
7
+ "background_color": "#0a0a0a",
8
+ "theme_color": "#0a0a0a",
9
+ "orientation": "any",
10
+ "icons": [
11
+ {
12
+ "src": "/assets/stem_black.png",
13
+ "sizes": "512x512",
14
+ "type": "image/png",
15
+ "purpose": "any maskable"
16
+ },
17
+ {
18
+ "src": "/assets/bot.png",
19
+ "sizes": "192x192",
20
+ "type": "image/png",
21
+ "purpose": "any"
22
+ }
23
+ ]
24
+ }
static/style.css CHANGED
@@ -1,31 +1,37 @@
1
  /* ============================================================
2
- Stem Copilot — Stylesheet
3
- Montserrat, dark theme, ChatGPT-inspired layout
4
  ============================================================ */
5
 
6
  :root {
7
  --brand-orange: #EB5A28;
8
  --brand-orange-hover: #cf4f22;
 
9
  --bg-main: #0a0a0a;
10
  --bg-sidebar: #0f0f0f;
11
  --bg-input: #1a1a1a;
12
  --bg-hover: #1f1f1f;
13
  --bg-card: #141414;
 
14
  --text-primary: #ececec;
15
  --text-secondary: #888888;
16
  --text-muted: #555555;
17
  --border-color: #262626;
 
18
  --msg-user: #2a2a2a;
19
  --msg-ai: transparent;
20
  --radius: 12px;
21
  --radius-lg: 16px;
 
22
  --transition: 0.25s cubic-bezier(0.4, 0, 0.2, 1);
 
 
23
  }
24
 
25
  * { box-sizing: border-box; margin: 0; padding: 0; }
26
 
27
  body {
28
- font-family: 'Montserrat', sans-serif;
29
  background-color: var(--bg-main);
30
  color: var(--text-primary);
31
  height: 100vh;
@@ -54,15 +60,19 @@ body {
54
  }
55
 
56
  .login-logo {
57
- max-width: 200px;
 
58
  margin-bottom: 28px;
 
 
59
  }
60
 
61
  .login-title {
62
- font-size: 26px;
63
  font-weight: 700;
64
  margin-bottom: 8px;
65
  color: var(--text-primary);
 
66
  }
67
 
68
  .login-subtitle {
@@ -105,18 +115,9 @@ body {
105
  padding: 48px 36px;
106
  }
107
 
108
- .byok-card h2 {
109
- font-size: 22px;
110
- font-weight: 700;
111
- margin-bottom: 12px;
112
- }
113
 
114
- .byok-desc {
115
- font-size: 14px;
116
- color: var(--text-secondary);
117
- line-height: 1.6;
118
- margin-bottom: 20px;
119
- }
120
 
121
  .byok-steps {
122
  text-align: left;
@@ -127,11 +128,7 @@ body {
127
  color: var(--text-secondary);
128
  }
129
 
130
- .byok-steps a {
131
- color: var(--brand-orange);
132
- text-decoration: none;
133
- }
134
-
135
  .byok-steps a:hover { text-decoration: underline; }
136
 
137
  .byok-submit-btn {
@@ -142,7 +139,7 @@ body {
142
  color: #fff;
143
  border: none;
144
  border-radius: var(--radius);
145
- font-family: 'Montserrat', sans-serif;
146
  font-size: 15px;
147
  font-weight: 600;
148
  cursor: pointer;
@@ -150,16 +147,77 @@ body {
150
  }
151
 
152
  .byok-submit-btn:hover { background: var(--brand-orange-hover); }
 
153
 
154
- .byok-note {
155
- font-size: 11px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  color: var(--text-muted);
157
- margin-top: 12px;
 
 
 
 
 
 
 
158
  }
159
 
160
 
161
  /* ============================================================
162
- APP CONTAINER (post-login)
163
  ============================================================ */
164
 
165
  .app-container {
@@ -170,22 +228,26 @@ body {
170
 
171
 
172
  /* ============================================================
173
- SIDEBAR
174
  ============================================================ */
175
 
176
  .sidebar {
177
- width: 260px;
178
  flex-shrink: 0;
179
  background: var(--bg-sidebar);
180
  display: flex;
181
  flex-direction: column;
182
  border-right: 1px solid var(--border-color);
183
- transition: margin-left var(--transition);
184
  position: relative;
185
  z-index: 20;
186
  }
187
 
188
- .sidebar.collapsed { margin-left: -260px; }
 
 
 
 
189
 
190
  .sidebar-top {
191
  flex: 1;
@@ -194,8 +256,19 @@ body {
194
  overflow: hidden;
195
  }
196
 
 
 
 
 
 
 
 
 
 
 
 
197
  .new-chat-btn {
198
- margin: 16px 14px 8px;
199
  padding: 11px 14px;
200
  background: transparent;
201
  border: 1px solid var(--border-color);
@@ -207,11 +280,11 @@ body {
207
  gap: 10px;
208
  font-size: 14px;
209
  font-weight: 500;
210
- font-family: 'Montserrat', sans-serif;
211
  transition: background var(--transition), border-color var(--transition);
212
  }
213
 
214
- .new-chat-btn:hover { background: var(--bg-hover); }
215
 
216
  .chat-history {
217
  flex: 1;
@@ -264,7 +337,7 @@ body {
264
  .options-menu {
265
  position: absolute;
266
  right: 10px; top: 35px;
267
- background: var(--bg-input);
268
  border: 1px solid var(--border-color);
269
  border-radius: 8px;
270
  display: none;
@@ -338,16 +411,13 @@ body {
338
  text-overflow: ellipsis;
339
  }
340
 
341
- .user-menu-arrow {
342
- opacity: 0.5;
343
- transition: transform 0.2s;
344
- }
345
 
346
  .user-menu {
347
  display: none;
348
  position: absolute;
349
  bottom: 68px; left: 14px; right: 14px;
350
- background: var(--bg-input);
351
  border: 1px solid var(--border-color);
352
  border-radius: var(--radius);
353
  box-shadow: 0 4px 20px rgba(0,0,0,0.7);
@@ -371,6 +441,66 @@ body {
371
  .user-menu-logout { color: #ff4a4a; }
372
 
373
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
374
  /* ============================================================
375
  MAIN CHAT AREA
376
  ============================================================ */
@@ -421,50 +551,104 @@ body {
421
  }
422
 
423
 
424
- /* --- Welcome screen --- */
 
 
425
 
426
- .welcome-screen {
427
  display: flex;
428
  flex-direction: column;
429
  align-items: center;
430
  justify-content: center;
431
  flex: 1;
432
- gap: 28px;
433
  user-select: none;
 
434
  }
435
 
436
- .welcome-title {
437
- font-size: 26px;
438
- font-weight: 600;
439
- color: var(--text-secondary);
 
 
 
 
 
 
 
 
 
 
 
 
 
440
  text-align: center;
 
 
 
 
 
 
441
  }
442
 
443
- .welcome-pills {
444
  display: flex;
445
- flex-wrap: wrap;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
446
  gap: 10px;
447
- justify-content: center;
448
- max-width: 600px;
449
  }
450
 
451
- .pill {
452
- padding: 10px 18px;
453
  background: var(--bg-card);
454
  border: 1px solid var(--border-color);
455
- border-radius: 20px;
456
  color: var(--text-secondary);
457
- font-family: 'Montserrat', sans-serif;
458
  font-size: 13px;
459
  font-weight: 500;
460
  cursor: pointer;
461
- transition: background var(--transition), color var(--transition), border-color var(--transition);
 
462
  }
463
 
464
- .pill:hover {
465
  background: var(--bg-hover);
466
  border-color: var(--brand-orange);
467
  color: var(--text-primary);
 
468
  }
469
 
470
 
@@ -487,7 +671,7 @@ body {
487
  padding: 14px 18px;
488
  border-radius: var(--radius);
489
  font-size: 15px;
490
- line-height: 1.7;
491
  word-wrap: break-word;
492
  }
493
 
@@ -504,9 +688,9 @@ body {
504
  }
505
 
506
  .ai-avatar {
507
- width: 28px; height: 28px;
508
  border-radius: 50%;
509
- background-image: url('/assets/stem_black.png');
510
  background-size: cover;
511
  background-position: center;
512
  margin-right: 14px;
@@ -597,7 +781,6 @@ body {
597
  .katex { color: var(--text-primary); }
598
  .katex-display { margin: 14px 0; overflow-x: auto; }
599
 
600
- /* --- User uploaded image in message --- */
601
  .message-image {
602
  max-width: 240px;
603
  border-radius: 8px;
@@ -606,7 +789,7 @@ body {
606
 
607
 
608
  /* ============================================================
609
- INPUT AREA
610
  ============================================================ */
611
 
612
  .input-container {
@@ -656,12 +839,16 @@ body {
656
  display: flex;
657
  align-items: flex-end;
658
  background: var(--bg-input);
659
- border: 1px solid var(--border-color);
660
- border-radius: var(--radius-lg);
661
- transition: border-color 0.3s;
 
662
  }
663
 
664
- .input-box:focus-within { border-color: var(--brand-orange); }
 
 
 
665
 
666
  .upload-btn {
667
  background: none; border: none;
@@ -680,7 +867,7 @@ textarea {
680
  background: transparent; border: none;
681
  color: var(--text-primary);
682
  padding: 16px 12px;
683
- font-family: 'Montserrat', sans-serif;
684
  font-size: 15px;
685
  resize: none; outline: none;
686
  height: 54px; max-height: 200px;
@@ -700,13 +887,14 @@ textarea::placeholder { color: var(--text-muted); }
700
  transition: color 0.2s, transform 0.1s;
701
  }
702
 
703
- .send-btn:hover { color: var(--text-primary); }
704
- .send-btn:active { transform: scale(0.95); }
705
  .send-btn svg { width: 20px; height: 20px; fill: currentColor; }
706
 
707
  .disclaimer-text {
708
  font-size: 11px;
709
- color: var(--text-muted);
 
710
  margin-top: 10px;
711
  text-align: center;
712
  }
@@ -726,19 +914,19 @@ textarea::placeholder { color: var(--text-muted); }
726
  justify-content: center;
727
  }
728
 
729
- .modal-overlay.show {
730
- display: flex;
731
- }
732
 
733
  .settings-modal {
734
  background: var(--bg-sidebar);
735
  border: 1px solid var(--border-color);
736
  border-radius: var(--radius-lg);
737
- width: 520px;
738
  max-width: 92vw;
739
  max-height: 85vh;
740
- overflow-y: auto;
741
  box-shadow: 0 8px 40px rgba(0,0,0,0.6);
 
 
742
  }
743
 
744
  .modal-header {
@@ -747,12 +935,10 @@ textarea::placeholder { color: var(--text-muted); }
747
  align-items: center;
748
  padding: 20px 24px;
749
  border-bottom: 1px solid var(--border-color);
 
750
  }
751
 
752
- .modal-header h2 {
753
- font-size: 18px;
754
- font-weight: 600;
755
- }
756
 
757
  .modal-close {
758
  background: none; border: none;
@@ -766,42 +952,61 @@ textarea::placeholder { color: var(--text-muted); }
766
  .modal-close:hover { color: var(--text-primary); }
767
 
768
  .modal-body {
769
- padding: 20px 24px 28px;
 
770
  }
771
 
772
- .settings-tabs {
773
  display: flex;
774
- gap: 4px;
775
- margin-bottom: 20px;
776
- border-bottom: 1px solid var(--border-color);
777
- padding-bottom: 12px;
778
  }
779
 
780
- .tab-btn {
781
- padding: 8px 16px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
782
  background: none;
783
  border: none;
784
  color: var(--text-secondary);
785
- font-family: 'Montserrat', sans-serif;
786
  font-size: 13px;
787
  font-weight: 500;
788
  cursor: pointer;
789
- border-radius: 6px;
790
  transition: background var(--transition), color var(--transition);
 
 
791
  }
792
 
793
- .tab-btn:hover { background: var(--bg-hover); color: var(--text-primary); }
794
- .tab-btn.active { background: var(--bg-hover); color: var(--text-primary); font-weight: 600; }
 
 
 
 
 
 
795
 
796
  .tab-content { display: none; }
797
  .tab-content.active { display: block; }
798
 
799
- .settings-label {
800
- display: block;
801
- font-size: 13px;
802
  font-weight: 600;
803
  color: var(--text-primary);
804
- margin-bottom: 8px;
805
  }
806
 
807
  .settings-input, .settings-textarea {
@@ -811,7 +1016,7 @@ textarea::placeholder { color: var(--text-muted); }
811
  border: 1px solid var(--border-color);
812
  border-radius: 8px;
813
  color: var(--text-primary);
814
- font-family: 'Montserrat', sans-serif;
815
  font-size: 14px;
816
  outline: none;
817
  transition: border-color 0.2s;
@@ -833,7 +1038,7 @@ textarea::placeholder { color: var(--text-muted); }
833
  border: 1px solid var(--border-color);
834
  border-radius: 8px;
835
  color: var(--text-primary);
836
- font-family: 'Montserrat', sans-serif;
837
  font-size: 14px;
838
  outline: none;
839
  cursor: pointer;
@@ -855,7 +1060,7 @@ textarea::placeholder { color: var(--text-muted); }
855
  color: #fff;
856
  border: none;
857
  border-radius: 8px;
858
- font-family: 'Montserrat', sans-serif;
859
  font-size: 13px;
860
  font-weight: 600;
861
  cursor: pointer;
@@ -871,11 +1076,7 @@ textarea::placeholder { color: var(--text-muted); }
871
  line-height: 1.5;
872
  }
873
 
874
- .settings-hint a {
875
- color: var(--brand-orange);
876
- text-decoration: none;
877
- }
878
-
879
  .settings-hint a:hover { text-decoration: underline; }
880
 
881
  .persona-option {
@@ -893,17 +1094,8 @@ textarea::placeholder { color: var(--text-muted); }
893
  .persona-option:hover { background: var(--bg-hover); }
894
  .persona-option.active { border-color: var(--brand-orange); background: rgba(235,90,40,0.06); }
895
 
896
- .persona-name {
897
- font-size: 14px;
898
- font-weight: 600;
899
- color: var(--text-primary);
900
- }
901
-
902
- .persona-desc {
903
- font-size: 12px;
904
- color: var(--text-secondary);
905
- line-height: 1.5;
906
- }
907
 
908
 
909
  /* ============================================================
@@ -923,7 +1115,7 @@ textarea::placeholder { color: var(--text-muted); }
923
  .cursor::after {
924
  content: '\25CB';
925
  animation: blink 1s step-start infinite;
926
- color: var(--text-secondary);
927
  margin-left: 2px;
928
  }
929
 
@@ -931,7 +1123,7 @@ textarea::placeholder { color: var(--text-muted); }
931
 
932
 
933
  /* ============================================================
934
- ERROR MESSAGE IN STREAM
935
  ============================================================ */
936
 
937
  .error-message {
@@ -949,9 +1141,37 @@ textarea::placeholder { color: var(--text-muted); }
949
  ============================================================ */
950
 
951
  @media (max-width: 768px) {
952
- .sidebar { position: fixed; left: 0; top: 0; height: 100vh; z-index: 30; }
953
- .sidebar.collapsed { margin-left: -260px; }
 
 
 
 
 
 
954
  .settings-modal { width: 95vw; }
955
- .welcome-title { font-size: 20px; }
956
- .pill { font-size: 12px; padding: 8px 14px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
957
  }
 
1
  /* ============================================================
2
+ STEM Copilot — Stylesheet
3
+ Inter, 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: #0a0a0a;
11
  --bg-sidebar: #0f0f0f;
12
  --bg-input: #1a1a1a;
13
  --bg-hover: #1f1f1f;
14
  --bg-card: #141414;
15
+ --bg-elevated: #1c1c1c;
16
  --text-primary: #ececec;
17
  --text-secondary: #888888;
18
  --text-muted: #555555;
19
  --border-color: #262626;
20
+ --border-subtle: #1e1e1e;
21
  --msg-user: #2a2a2a;
22
  --msg-ai: transparent;
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: 260px;
28
+ --rail-width: 56px;
29
  }
30
 
31
  * { box-sizing: border-box; margin: 0; padding: 0; }
32
 
33
  body {
34
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
35
  background-color: var(--bg-main);
36
  color: var(--text-primary);
37
  height: 100vh;
 
60
  }
61
 
62
  .login-logo {
63
+ width: 100px;
64
+ height: 100px;
65
  margin-bottom: 28px;
66
+ border-radius: 50%;
67
+ object-fit: cover;
68
  }
69
 
70
  .login-title {
71
+ font-size: 24px;
72
  font-weight: 700;
73
  margin-bottom: 8px;
74
  color: var(--text-primary);
75
+ white-space: nowrap;
76
  }
77
 
78
  .login-subtitle {
 
115
  padding: 48px 36px;
116
  }
117
 
118
+ .byok-card h2 { font-size: 22px; font-weight: 700; margin-bottom: 12px; }
 
 
 
 
119
 
120
+ .byok-desc { font-size: 14px; color: var(--text-secondary); line-height: 1.6; margin-bottom: 20px; }
 
 
 
 
 
121
 
122
  .byok-steps {
123
  text-align: left;
 
128
  color: var(--text-secondary);
129
  }
130
 
131
+ .byok-steps a { color: var(--brand-orange); text-decoration: none; }
 
 
 
 
132
  .byok-steps a:hover { text-decoration: underline; }
133
 
134
  .byok-submit-btn {
 
139
  color: #fff;
140
  border: none;
141
  border-radius: var(--radius);
142
+ font-family: inherit;
143
  font-size: 15px;
144
  font-weight: 600;
145
  cursor: pointer;
 
147
  }
148
 
149
  .byok-submit-btn:hover { background: var(--brand-orange-hover); }
150
+ .byok-note { font-size: 11px; color: var(--text-muted); margin-top: 12px; }
151
 
152
+
153
+ /* ============================================================
154
+ PWA INSTALL BANNER
155
+ ============================================================ */
156
+
157
+ .install-banner {
158
+ position: fixed;
159
+ bottom: 16px;
160
+ left: 50%;
161
+ transform: translateX(-50%);
162
+ z-index: 900;
163
+ background: var(--bg-elevated);
164
+ border: 1px solid var(--border-color);
165
+ border-radius: var(--radius);
166
+ padding: 12px 16px;
167
+ display: flex;
168
+ align-items: center;
169
+ gap: 12px;
170
+ box-shadow: 0 8px 32px rgba(0,0,0,0.6);
171
+ max-width: 400px;
172
+ width: calc(100% - 32px);
173
+ animation: slideUp 0.4s cubic-bezier(0.16, 1, 0.3, 1);
174
+ }
175
+
176
+ .install-banner-icon { width: 36px; height: 36px; border-radius: 8px; }
177
+
178
+ .install-banner-text {
179
+ flex: 1;
180
+ display: flex;
181
+ flex-direction: column;
182
+ font-size: 13px;
183
+ line-height: 1.4;
184
+ }
185
+
186
+ .install-banner-text strong { color: var(--text-primary); }
187
+ .install-banner-text span { color: var(--text-secondary); font-size: 11px; }
188
+
189
+ .install-banner-btn {
190
+ background: var(--brand-orange);
191
+ color: #fff;
192
+ border: none;
193
+ border-radius: 8px;
194
+ padding: 8px 16px;
195
+ font-family: inherit;
196
+ font-size: 13px;
197
+ font-weight: 600;
198
+ cursor: pointer;
199
+ transition: background var(--transition);
200
+ }
201
+
202
+ .install-banner-btn:hover { background: var(--brand-orange-hover); }
203
+
204
+ .install-banner-dismiss {
205
+ background: none;
206
+ border: none;
207
  color: var(--text-muted);
208
+ font-size: 18px;
209
+ cursor: pointer;
210
+ padding: 0 4px;
211
+ }
212
+
213
+ @keyframes slideUp {
214
+ from { opacity: 0; transform: translateX(-50%) translateY(20px); }
215
+ to { opacity: 1; transform: translateX(-50%) translateY(0); }
216
  }
217
 
218
 
219
  /* ============================================================
220
+ APP CONTAINER
221
  ============================================================ */
222
 
223
  .app-container {
 
228
 
229
 
230
  /* ============================================================
231
+ SIDEBAR (full)
232
  ============================================================ */
233
 
234
  .sidebar {
235
+ width: var(--sidebar-width);
236
  flex-shrink: 0;
237
  background: var(--bg-sidebar);
238
  display: flex;
239
  flex-direction: column;
240
  border-right: 1px solid var(--border-color);
241
+ transition: margin-left var(--transition), opacity var(--transition);
242
  position: relative;
243
  z-index: 20;
244
  }
245
 
246
+ .sidebar.collapsed {
247
+ margin-left: calc(-1 * var(--sidebar-width));
248
+ opacity: 0;
249
+ pointer-events: none;
250
+ }
251
 
252
  .sidebar-top {
253
  flex: 1;
 
256
  overflow: hidden;
257
  }
258
 
259
+ .sidebar-logo-row {
260
+ padding: 16px 14px 4px;
261
+ display: flex;
262
+ align-items: center;
263
+ }
264
+
265
+ .sidebar-logo {
266
+ height: 28px;
267
+ object-fit: contain;
268
+ }
269
+
270
  .new-chat-btn {
271
+ margin: 8px 14px;
272
  padding: 11px 14px;
273
  background: transparent;
274
  border: 1px solid var(--border-color);
 
280
  gap: 10px;
281
  font-size: 14px;
282
  font-weight: 500;
283
+ font-family: inherit;
284
  transition: background var(--transition), border-color var(--transition);
285
  }
286
 
287
+ .new-chat-btn:hover { background: var(--bg-hover); border-color: var(--text-muted); }
288
 
289
  .chat-history {
290
  flex: 1;
 
337
  .options-menu {
338
  position: absolute;
339
  right: 10px; top: 35px;
340
+ background: var(--bg-elevated);
341
  border: 1px solid var(--border-color);
342
  border-radius: 8px;
343
  display: none;
 
411
  text-overflow: ellipsis;
412
  }
413
 
414
+ .user-menu-arrow { opacity: 0.5; transition: transform 0.2s; }
 
 
 
415
 
416
  .user-menu {
417
  display: none;
418
  position: absolute;
419
  bottom: 68px; left: 14px; right: 14px;
420
+ background: var(--bg-elevated);
421
  border: 1px solid var(--border-color);
422
  border-radius: var(--radius);
423
  box-shadow: 0 4px 20px rgba(0,0,0,0.7);
 
441
  .user-menu-logout { color: #ff4a4a; }
442
 
443
 
444
+ /* ============================================================
445
+ SIDEBAR RAIL (collapsed mode)
446
+ ============================================================ */
447
+
448
+ .sidebar-rail {
449
+ width: var(--rail-width);
450
+ flex-shrink: 0;
451
+ background: var(--bg-sidebar);
452
+ border-right: 1px solid var(--border-color);
453
+ display: none;
454
+ flex-direction: column;
455
+ justify-content: space-between;
456
+ align-items: center;
457
+ padding: 12px 0;
458
+ z-index: 20;
459
+ }
460
+
461
+ .sidebar-rail.visible {
462
+ display: flex;
463
+ }
464
+
465
+ .rail-top, .rail-bottom {
466
+ display: flex;
467
+ flex-direction: column;
468
+ align-items: center;
469
+ gap: 4px;
470
+ }
471
+
472
+ .rail-icon-btn {
473
+ width: 40px; height: 40px;
474
+ border-radius: 10px;
475
+ background: transparent;
476
+ border: none;
477
+ color: var(--text-secondary);
478
+ cursor: pointer;
479
+ display: flex;
480
+ align-items: center;
481
+ justify-content: center;
482
+ transition: background var(--transition), color var(--transition);
483
+ }
484
+
485
+ .rail-icon-btn:hover {
486
+ background: var(--bg-hover);
487
+ color: var(--text-primary);
488
+ }
489
+
490
+ .rail-logo {
491
+ width: 28px; height: 28px;
492
+ border-radius: 6px;
493
+ object-fit: cover;
494
+ }
495
+
496
+ .rail-avatar {
497
+ width: 28px; height: 28px;
498
+ border-radius: 50%;
499
+ object-fit: cover;
500
+ background: var(--bg-hover);
501
+ }
502
+
503
+
504
  /* ============================================================
505
  MAIN CHAT AREA
506
  ============================================================ */
 
551
  }
552
 
553
 
554
+ /* ============================================================
555
+ HERO / WELCOME SCREEN
556
+ ============================================================ */
557
 
558
+ .hero-welcome {
559
  display: flex;
560
  flex-direction: column;
561
  align-items: center;
562
  justify-content: center;
563
  flex: 1;
564
+ gap: 24px;
565
  user-select: none;
566
+ padding: 40px 20px;
567
  }
568
 
569
+ .hero-bot-icon {
570
+ width: 120px;
571
+ height: 120px;
572
+ object-fit: contain;
573
+ animation: heroFloat 3s ease-in-out infinite;
574
+ filter: drop-shadow(0 8px 24px rgba(235, 90, 40, 0.15));
575
+ }
576
+
577
+ @keyframes heroFloat {
578
+ 0%, 100% { transform: translateY(0); }
579
+ 50% { transform: translateY(-8px); }
580
+ }
581
+
582
+ .hero-title {
583
+ font-size: 32px;
584
+ font-weight: 700;
585
+ color: var(--text-primary);
586
  text-align: center;
587
+ line-height: 1.3;
588
+ }
589
+
590
+ .hero-input-wrap {
591
+ width: 100%;
592
+ max-width: 640px;
593
  }
594
 
595
+ .hero-input-box {
596
  display: flex;
597
+ align-items: flex-end;
598
+ background: var(--bg-input);
599
+ border: 1.5px solid var(--border-color);
600
+ border-radius: var(--radius-xl);
601
+ transition: border-color 0.3s, box-shadow 0.3s;
602
+ box-shadow: 0 2px 12px rgba(0,0,0,0.3);
603
+ }
604
+
605
+ .hero-input-box:focus-within {
606
+ border-color: var(--brand-orange);
607
+ box-shadow: 0 0 0 3px var(--brand-orange-glow), 0 4px 20px rgba(0,0,0,0.4);
608
+ }
609
+
610
+ .hero-input-box textarea {
611
+ flex: 1;
612
+ background: transparent; border: none;
613
+ color: var(--text-primary);
614
+ padding: 16px 12px;
615
+ font-family: inherit;
616
+ font-size: 15px;
617
+ resize: none; outline: none;
618
+ height: 54px; max-height: 120px;
619
+ line-height: 1.5;
620
+ overflow-y: hidden;
621
+ }
622
+
623
+ .hero-input-box textarea::placeholder { color: var(--text-muted); }
624
+
625
+ .hero-pills {
626
+ display: grid;
627
+ grid-template-columns: 1fr 1fr;
628
  gap: 10px;
629
+ max-width: 560px;
630
+ width: 100%;
631
  }
632
 
633
+ .hero-pill {
634
+ padding: 14px 16px;
635
  background: var(--bg-card);
636
  border: 1px solid var(--border-color);
637
+ border-radius: var(--radius);
638
  color: var(--text-secondary);
639
+ font-family: inherit;
640
  font-size: 13px;
641
  font-weight: 500;
642
  cursor: pointer;
643
+ text-align: left;
644
+ transition: background var(--transition), color var(--transition), border-color var(--transition), box-shadow var(--transition);
645
  }
646
 
647
+ .hero-pill:hover {
648
  background: var(--bg-hover);
649
  border-color: var(--brand-orange);
650
  color: var(--text-primary);
651
+ box-shadow: 0 0 0 1px var(--brand-orange-glow);
652
  }
653
 
654
 
 
671
  padding: 14px 18px;
672
  border-radius: var(--radius);
673
  font-size: 15px;
674
+ line-height: 1.75;
675
  word-wrap: break-word;
676
  }
677
 
 
688
  }
689
 
690
  .ai-avatar {
691
+ width: 30px; height: 30px;
692
  border-radius: 50%;
693
+ background-image: url('/assets/bot.png');
694
  background-size: cover;
695
  background-position: center;
696
  margin-right: 14px;
 
781
  .katex { color: var(--text-primary); }
782
  .katex-display { margin: 14px 0; overflow-x: auto; }
783
 
 
784
  .message-image {
785
  max-width: 240px;
786
  border-radius: 8px;
 
789
 
790
 
791
  /* ============================================================
792
+ INPUT AREA (bottom bar during chat)
793
  ============================================================ */
794
 
795
  .input-container {
 
839
  display: flex;
840
  align-items: flex-end;
841
  background: var(--bg-input);
842
+ border: 1.5px solid var(--border-color);
843
+ border-radius: var(--radius-xl);
844
+ transition: border-color 0.3s, box-shadow 0.3s;
845
+ box-shadow: 0 2px 12px rgba(0,0,0,0.3);
846
  }
847
 
848
+ .input-box:focus-within {
849
+ border-color: var(--brand-orange);
850
+ box-shadow: 0 0 0 3px var(--brand-orange-glow), 0 4px 20px rgba(0,0,0,0.4);
851
+ }
852
 
853
  .upload-btn {
854
  background: none; border: none;
 
867
  background: transparent; border: none;
868
  color: var(--text-primary);
869
  padding: 16px 12px;
870
+ font-family: inherit;
871
  font-size: 15px;
872
  resize: none; outline: none;
873
  height: 54px; max-height: 200px;
 
887
  transition: color 0.2s, transform 0.1s;
888
  }
889
 
890
+ .send-btn:hover { color: var(--brand-orange); }
891
+ .send-btn:active { transform: scale(0.92); }
892
  .send-btn svg { width: 20px; height: 20px; fill: currentColor; }
893
 
894
  .disclaimer-text {
895
  font-size: 11px;
896
+ color: rgba(255,255,255,0.5);
897
+ font-weight: 500;
898
  margin-top: 10px;
899
  text-align: center;
900
  }
 
914
  justify-content: center;
915
  }
916
 
917
+ .modal-overlay.show { display: flex; }
 
 
918
 
919
  .settings-modal {
920
  background: var(--bg-sidebar);
921
  border: 1px solid var(--border-color);
922
  border-radius: var(--radius-lg);
923
+ width: 600px;
924
  max-width: 92vw;
925
  max-height: 85vh;
926
+ overflow: hidden;
927
  box-shadow: 0 8px 40px rgba(0,0,0,0.6);
928
+ display: flex;
929
+ flex-direction: column;
930
  }
931
 
932
  .modal-header {
 
935
  align-items: center;
936
  padding: 20px 24px;
937
  border-bottom: 1px solid var(--border-color);
938
+ flex-shrink: 0;
939
  }
940
 
941
+ .modal-header h2 { font-size: 18px; font-weight: 600; }
 
 
 
942
 
943
  .modal-close {
944
  background: none; border: none;
 
952
  .modal-close:hover { color: var(--text-primary); }
953
 
954
  .modal-body {
955
+ padding: 0;
956
+ overflow: hidden;
957
  }
958
 
959
+ .settings-layout {
960
  display: flex;
961
+ min-height: 380px;
 
 
 
962
  }
963
 
964
+ .settings-nav {
965
+ width: 160px;
966
+ flex-shrink: 0;
967
+ background: var(--bg-main);
968
+ padding: 16px 8px;
969
+ display: flex;
970
+ flex-direction: column;
971
+ gap: 2px;
972
+ border-right: 1px solid var(--border-color);
973
+ }
974
+
975
+ .settings-nav-btn {
976
+ display: flex;
977
+ align-items: center;
978
+ gap: 8px;
979
+ padding: 10px 12px;
980
  background: none;
981
  border: none;
982
  color: var(--text-secondary);
983
+ font-family: inherit;
984
  font-size: 13px;
985
  font-weight: 500;
986
  cursor: pointer;
987
+ border-radius: 8px;
988
  transition: background var(--transition), color var(--transition);
989
+ text-align: left;
990
+ width: 100%;
991
  }
992
 
993
+ .settings-nav-btn:hover { background: var(--bg-hover); color: var(--text-primary); }
994
+ .settings-nav-btn.active { background: var(--bg-hover); color: var(--text-primary); font-weight: 600; }
995
+
996
+ .settings-panel {
997
+ flex: 1;
998
+ padding: 24px;
999
+ overflow-y: auto;
1000
+ }
1001
 
1002
  .tab-content { display: none; }
1003
  .tab-content.active { display: block; }
1004
 
1005
+ .settings-section-title {
1006
+ font-size: 14px;
 
1007
  font-weight: 600;
1008
  color: var(--text-primary);
1009
+ margin-bottom: 10px;
1010
  }
1011
 
1012
  .settings-input, .settings-textarea {
 
1016
  border: 1px solid var(--border-color);
1017
  border-radius: 8px;
1018
  color: var(--text-primary);
1019
+ font-family: inherit;
1020
  font-size: 14px;
1021
  outline: none;
1022
  transition: border-color 0.2s;
 
1038
  border: 1px solid var(--border-color);
1039
  border-radius: 8px;
1040
  color: var(--text-primary);
1041
+ font-family: inherit;
1042
  font-size: 14px;
1043
  outline: none;
1044
  cursor: pointer;
 
1060
  color: #fff;
1061
  border: none;
1062
  border-radius: 8px;
1063
+ font-family: inherit;
1064
  font-size: 13px;
1065
  font-weight: 600;
1066
  cursor: pointer;
 
1076
  line-height: 1.5;
1077
  }
1078
 
1079
+ .settings-hint a { color: var(--brand-orange); text-decoration: none; }
 
 
 
 
1080
  .settings-hint a:hover { text-decoration: underline; }
1081
 
1082
  .persona-option {
 
1094
  .persona-option:hover { background: var(--bg-hover); }
1095
  .persona-option.active { border-color: var(--brand-orange); background: rgba(235,90,40,0.06); }
1096
 
1097
+ .persona-name { font-size: 14px; font-weight: 600; color: var(--text-primary); }
1098
+ .persona-desc { font-size: 12px; color: var(--text-secondary); line-height: 1.5; }
 
 
 
 
 
 
 
 
 
1099
 
1100
 
1101
  /* ============================================================
 
1115
  .cursor::after {
1116
  content: '\25CB';
1117
  animation: blink 1s step-start infinite;
1118
+ color: var(--brand-orange);
1119
  margin-left: 2px;
1120
  }
1121
 
 
1123
 
1124
 
1125
  /* ============================================================
1126
+ ERROR MESSAGE
1127
  ============================================================ */
1128
 
1129
  .error-message {
 
1141
  ============================================================ */
1142
 
1143
  @media (max-width: 768px) {
1144
+ .sidebar {
1145
+ position: fixed; left: 0; top: 0;
1146
+ height: 100vh; z-index: 30;
1147
+ }
1148
+ .sidebar.collapsed { margin-left: calc(-1 * var(--sidebar-width)); }
1149
+
1150
+ .sidebar-rail { display: none !important; }
1151
+
1152
  .settings-modal { width: 95vw; }
1153
+
1154
+ .settings-layout { flex-direction: column; }
1155
+ .settings-nav {
1156
+ width: 100%;
1157
+ flex-direction: row;
1158
+ overflow-x: auto;
1159
+ padding: 8px;
1160
+ border-right: none;
1161
+ border-bottom: 1px solid var(--border-color);
1162
+ }
1163
+ .settings-nav-btn { white-space: nowrap; }
1164
+ .settings-panel { padding: 16px; }
1165
+
1166
+ .hero-title { font-size: 22px; }
1167
+ .hero-bot-icon { width: 80px; height: 80px; }
1168
+ .hero-pills { grid-template-columns: 1fr; }
1169
+ .hero-pill { font-size: 12px; padding: 12px 14px; }
1170
+
1171
+ .login-title { font-size: 20px; white-space: normal; }
1172
+ }
1173
+
1174
+ @media (max-width: 480px) {
1175
+ .hero-title { font-size: 20px; }
1176
+ .hero-input-box textarea { font-size: 14px; }
1177
  }
sw.js ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // STEM Copilot — Service Worker (PWA installability + basic cache)
2
+ const CACHE_NAME = 'stemcopilot-v1';
3
+ const SHELL_URLS = [
4
+ '/',
5
+ '/static/style.css',
6
+ '/static/app.js',
7
+ '/assets/bot.png',
8
+ '/assets/stem_black.png',
9
+ '/assets/stembotix.png',
10
+ ];
11
+
12
+ self.addEventListener('install', (e) => {
13
+ e.waitUntil(
14
+ caches.open(CACHE_NAME).then((cache) => cache.addAll(SHELL_URLS))
15
+ );
16
+ self.skipWaiting();
17
+ });
18
+
19
+ self.addEventListener('activate', (e) => {
20
+ e.waitUntil(
21
+ caches.keys().then((keys) =>
22
+ Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))
23
+ )
24
+ );
25
+ self.clients.claim();
26
+ });
27
+
28
+ self.addEventListener('fetch', (e) => {
29
+ // Network-first for API calls, cache-first for shell assets
30
+ if (e.request.url.includes('/chat') || e.request.url.includes('/auth') ||
31
+ e.request.url.includes('/threads') || e.request.url.includes('/history') ||
32
+ e.request.url.includes('/user') || e.request.url.includes('/export') ||
33
+ e.request.url.includes('/health')) {
34
+ return; // Let these go straight to network
35
+ }
36
+ e.respondWith(
37
+ caches.match(e.request).then((cached) => cached || fetch(e.request))
38
+ );
39
+ });