Subh775 commited on
Commit
c5b24e6
·
1 Parent(s): ee3ac6d

mobile pov-ixes;cdns;etc..

Browse files
Files changed (4) hide show
  1. static/app.js +41 -28
  2. static/index.html +12 -4
  3. static/manifest.json +1 -1
  4. static/style.css +10 -0
static/app.js CHANGED
@@ -724,7 +724,9 @@ if (imagePreviewRemove) imagePreviewRemove.addEventListener('click', () => {
724
  ============================================================ */
725
 
726
  let currentAbortController = null;
727
- let currentStreamReader = null; // ← key: we keep a ref to cancel it
 
 
728
 
729
  function _showStopBtn() {
730
  if (sendBtn) sendBtn.style.display = 'none';
@@ -737,23 +739,27 @@ function _showSendBtn() {
737
 
738
  if (stopBtn) {
739
  stopBtn.addEventListener('click', () => {
740
- // 1) Cancel the stream reader so no more tokens arrive
 
 
 
 
741
  if (currentStreamReader) {
742
  try { currentStreamReader.cancel(); } catch(_) {}
743
  currentStreamReader = null;
744
  }
745
- // 2) Abort the fetch connection so server detects disconnect
746
  if (currentAbortController) {
747
  currentAbortController.abort();
748
  currentAbortController = null;
749
  }
750
- // 3) Reset UI state
751
  isSending = false;
752
  _showSendBtn();
753
  const currentThinking = document.getElementById('currentThinking');
754
  if (currentThinking) currentThinking.style.display = 'none';
755
  document.querySelectorAll('.ai-avatar.pulsing').forEach(el => el.classList.remove('pulsing'));
756
- // 4) Remove the typing cursor from partial response
757
  document.querySelectorAll('.message-content.cursor').forEach(el => el.classList.remove('cursor'));
758
  });
759
  }
@@ -844,15 +850,15 @@ function streamResponse(text) {
844
  const contentEl = rowDiv.querySelector('.message-content');
845
  let rawText = '';
846
  let firstToken = true;
847
- let stopped = false;
848
 
849
- let renderTimer = null;
850
  const RENDER_INTERVAL = 120;
851
  function scheduleRender() {
852
- if (renderTimer || stopped) return;
853
- renderTimer = setTimeout(() => {
854
- renderTimer = null;
855
- if (!stopped) { renderFinalContent(contentEl, rawText); scrollToBottom(); }
856
  }, RENDER_INTERVAL);
857
  }
858
 
@@ -879,8 +885,8 @@ function streamResponse(text) {
879
 
880
  function read() {
881
  reader.read().then(({ done, value }) => {
882
- if (done || stopped) {
883
- if (!stopped) finishStream(thinkingEl, contentEl, rawText, renderTimer);
884
  return;
885
  }
886
 
@@ -888,24 +894,25 @@ function streamResponse(text) {
888
  const lines = buffer.split('\n');
889
  buffer = lines.pop();
890
 
891
- lines.forEach(line => {
892
- if (stopped) return;
893
- if (!line.startsWith('data: ')) return;
894
  const pl = line.substring(6);
895
  if (pl === '[DONE]') {
896
- finishStream(thinkingEl, contentEl, rawText, renderTimer);
897
- stopped = true; return;
898
  }
899
  try {
900
  const data = JSON.parse(pl);
901
- if (data.status === 'thinking') { thinkingText.textContent = data.message; return; }
902
  if (data.error) {
903
  thinkingEl.style.display = 'none';
904
  contentEl.style.display = 'block';
905
  contentEl.innerHTML = `<div class="error-message">${escapeHtml(data.error)}</div>`;
906
- isSending = false; _showSendBtn(); stopped = true; return;
907
  }
908
  if (data.token !== undefined) {
 
909
  if (firstToken) {
910
  thinkingEl.style.display = 'none';
911
  contentEl.style.display = 'block';
@@ -916,13 +923,11 @@ function streamResponse(text) {
916
  scheduleRender();
917
  }
918
  } catch (_) {}
919
- });
920
 
921
- if (!stopped) read();
922
  }).catch(err => {
923
- // reader.read() rejected happens on abort/cancel
924
- if (err.name === 'AbortError' || stopped) return;
925
- // Genuine error
926
  thinkingEl.style.display = 'none';
927
  contentEl.style.display = 'block';
928
  if (!rawText) contentEl.innerHTML = '<div class="error-message">Connection lost. Please try again.</div>';
@@ -931,7 +936,7 @@ function streamResponse(text) {
931
  }
932
  read();
933
  }).catch(err => {
934
- if (err.name === 'AbortError') {
935
  // User clicked stop before any response — that's fine
936
  thinkingEl.style.display = 'none';
937
  contentEl.style.display = 'block';
@@ -950,8 +955,9 @@ function streamResponse(text) {
950
  });
951
  }
952
 
953
- function finishStream(thinkingEl, contentEl, rawText, renderTimer) {
954
- if (renderTimer) clearTimeout(renderTimer);
 
955
  thinkingEl.style.display = 'none';
956
  contentEl.style.display = 'block';
957
  contentEl.classList.remove('cursor');
@@ -1064,6 +1070,13 @@ if (installDismiss) installDismiss.addEventListener('click', () => {
1064
  // Ensure sidebar starts collapsed in DOM (matches CSS default)
1065
  if (sidebar) sidebar.classList.add('collapsed');
1066
 
 
 
 
 
 
 
 
1067
  window.addEventListener('load', () => {
1068
  checkExistingSession();
1069
  _bindFeedbackSubmit();
 
724
  ============================================================ */
725
 
726
  let currentAbortController = null;
727
+ let currentStreamReader = null;
728
+ let currentStreamStopped = false; // module-level so stop button can kill it
729
+ let currentRenderTimer = null; // so stop button can clear pending renders
730
 
731
  function _showStopBtn() {
732
  if (sendBtn) sendBtn.style.display = 'none';
 
739
 
740
  if (stopBtn) {
741
  stopBtn.addEventListener('click', () => {
742
+ // 1) Set the flag FIRST this immediately stops token processing
743
+ currentStreamStopped = true;
744
+ // 2) Clear any pending render timer
745
+ if (currentRenderTimer) { clearTimeout(currentRenderTimer); currentRenderTimer = null; }
746
+ // 3) Cancel the stream reader so no more data arrives
747
  if (currentStreamReader) {
748
  try { currentStreamReader.cancel(); } catch(_) {}
749
  currentStreamReader = null;
750
  }
751
+ // 4) Abort the fetch connection so server detects disconnect
752
  if (currentAbortController) {
753
  currentAbortController.abort();
754
  currentAbortController = null;
755
  }
756
+ // 5) Reset UI state
757
  isSending = false;
758
  _showSendBtn();
759
  const currentThinking = document.getElementById('currentThinking');
760
  if (currentThinking) currentThinking.style.display = 'none';
761
  document.querySelectorAll('.ai-avatar.pulsing').forEach(el => el.classList.remove('pulsing'));
762
+ // 6) Remove the typing cursor from partial response
763
  document.querySelectorAll('.message-content.cursor').forEach(el => el.classList.remove('cursor'));
764
  });
765
  }
 
850
  const contentEl = rowDiv.querySelector('.message-content');
851
  let rawText = '';
852
  let firstToken = true;
853
+ currentStreamStopped = false; // reset for this new stream
854
 
855
+ currentRenderTimer = null;
856
  const RENDER_INTERVAL = 120;
857
  function scheduleRender() {
858
+ if (currentRenderTimer || currentStreamStopped) return;
859
+ currentRenderTimer = setTimeout(() => {
860
+ currentRenderTimer = null;
861
+ if (!currentStreamStopped) { renderFinalContent(contentEl, rawText); scrollToBottom(); }
862
  }, RENDER_INTERVAL);
863
  }
864
 
 
885
 
886
  function read() {
887
  reader.read().then(({ done, value }) => {
888
+ if (done || currentStreamStopped) {
889
+ if (!currentStreamStopped) finishStream(thinkingEl, contentEl, rawText, currentRenderTimer);
890
  return;
891
  }
892
 
 
894
  const lines = buffer.split('\n');
895
  buffer = lines.pop();
896
 
897
+ for (const line of lines) {
898
+ if (currentStreamStopped) return; // bail immediately
899
+ if (!line.startsWith('data: ')) continue;
900
  const pl = line.substring(6);
901
  if (pl === '[DONE]') {
902
+ finishStream(thinkingEl, contentEl, rawText, currentRenderTimer);
903
+ currentStreamStopped = true; return;
904
  }
905
  try {
906
  const data = JSON.parse(pl);
907
+ if (data.status === 'thinking') { thinkingText.textContent = data.message; continue; }
908
  if (data.error) {
909
  thinkingEl.style.display = 'none';
910
  contentEl.style.display = 'block';
911
  contentEl.innerHTML = `<div class="error-message">${escapeHtml(data.error)}</div>`;
912
+ isSending = false; _showSendBtn(); currentStreamStopped = true; return;
913
  }
914
  if (data.token !== undefined) {
915
+ if (currentStreamStopped) return; // double-check before adding token
916
  if (firstToken) {
917
  thinkingEl.style.display = 'none';
918
  contentEl.style.display = 'block';
 
923
  scheduleRender();
924
  }
925
  } catch (_) {}
926
+ }
927
 
928
+ if (!currentStreamStopped) read();
929
  }).catch(err => {
930
+ if (err.name === 'AbortError' || currentStreamStopped) return;
 
 
931
  thinkingEl.style.display = 'none';
932
  contentEl.style.display = 'block';
933
  if (!rawText) contentEl.innerHTML = '<div class="error-message">Connection lost. Please try again.</div>';
 
936
  }
937
  read();
938
  }).catch(err => {
939
+ if (err.name === 'AbortError' || currentStreamStopped) {
940
  // User clicked stop before any response — that's fine
941
  thinkingEl.style.display = 'none';
942
  contentEl.style.display = 'block';
 
955
  });
956
  }
957
 
958
+ function finishStream(thinkingEl, contentEl, rawText, timer) {
959
+ if (timer) clearTimeout(timer);
960
+ currentRenderTimer = null;
961
  thinkingEl.style.display = 'none';
962
  contentEl.style.display = 'block';
963
  contentEl.classList.remove('cursor');
 
1070
  // Ensure sidebar starts collapsed in DOM (matches CSS default)
1071
  if (sidebar) sidebar.classList.add('collapsed');
1072
 
1073
+ // Lock screen to portrait on mobile
1074
+ try {
1075
+ if (screen.orientation && screen.orientation.lock) {
1076
+ screen.orientation.lock('portrait').catch(() => {});
1077
+ }
1078
+ } catch(_) {}
1079
+
1080
  window.addEventListener('load', () => {
1081
  checkExistingSession();
1082
  _bindFeedbackSubmit();
static/index.html CHANGED
@@ -2,18 +2,26 @@
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=Montserrat:wght@400;500;600;700;800&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: loaded dynamically by initGoogleAuth() in app.js
16
- to guarantee initialize() is called only after the script is ready. -->
17
  </head>
18
  <body>
19
 
 
2
  <html lang="en">
3
  <head>
4
  <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
6
+ <meta name="screen-orientation" content="portrait">
7
+ <meta name="x5-orientation" content="portrait">
8
+ <meta name="mobile-web-app-capable" content="yes">
9
+ <meta name="apple-mobile-web-app-capable" content="yes">
10
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
11
  <title>STEM Copilot</title>
12
  <meta name="description" content="AI-powered NCERT tutor for Class XI and XII Physics, Chemistry, and Mathematics.">
13
  <meta name="theme-color" content="#0a0a0a">
14
  <link rel="icon" type="image/png" href="/assets/bot.png">
15
  <link rel="manifest" href="/manifest.json">
16
  <link rel="apple-touch-icon" href="/assets/stem_black.png">
17
+ <!-- Preconnect for faster font/CDN loading -->
18
+ <link rel="preconnect" href="https://fonts.googleapis.com">
19
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
20
+ <link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
21
+ <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet">
22
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css">
23
  <link rel="stylesheet" href="/static/style.css">
24
+ <!-- Google Identity Services: loaded dynamically by initGoogleAuth() in app.js -->
 
25
  </head>
26
  <body>
27
 
static/manifest.json CHANGED
@@ -7,7 +7,7 @@
7
  "display": "standalone",
8
  "background_color": "#000000",
9
  "theme_color": "#000000",
10
- "orientation": "any",
11
  "prefer_related_applications": false,
12
  "icons": [
13
  {
 
7
  "display": "standalone",
8
  "background_color": "#000000",
9
  "theme_color": "#000000",
10
+ "orientation": "portrait",
11
  "prefer_related_applications": false,
12
  "icons": [
13
  {
static/style.css CHANGED
@@ -30,6 +30,13 @@
30
 
31
  * { box-sizing: border-box; margin: 0; padding: 0; }
32
 
 
 
 
 
 
 
 
33
  body {
34
  font-family: 'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
35
  background-color: var(--bg-main);
@@ -37,6 +44,9 @@ body {
37
  height: 100vh;
38
  height: 100dvh;
39
  overflow: hidden;
 
 
 
40
  }
41
 
42
 
 
30
 
31
  * { box-sizing: border-box; margin: 0; padding: 0; }
32
 
33
+ html {
34
+ -webkit-text-size-adjust: 100%;
35
+ -ms-text-size-adjust: 100%;
36
+ touch-action: manipulation;
37
+ overscroll-behavior: none;
38
+ }
39
+
40
  body {
41
  font-family: 'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
42
  background-color: var(--bg-main);
 
44
  height: 100vh;
45
  height: 100dvh;
46
  overflow: hidden;
47
+ touch-action: manipulation;
48
+ -webkit-tap-highlight-color: transparent;
49
+ overscroll-behavior: none;
50
  }
51
 
52