Some Guy commited on
Commit
1e55f99
·
1 Parent(s): 84f2f65

UI styling updates and README

Browse files
Files changed (2) hide show
  1. README.md +47 -0
  2. static/index.html +525 -65
README.md ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # FlowRead AI 📖 (Gemma 4 Good Hackathon Submission)
2
+
3
+ **Accelerate reading comprehension using LLM attention vectors.**
4
+
5
+ FlowRead AI is an accessibility and educational tool that dynamically bolds the most semantically important words in a text. It uses the raw, internal attention vectors of the **Google Gemma 2B** model to understand what words actually matter, creating a visually guided reading path that reduces cognitive load and improves reading comprehension.
6
+
7
+ ## 🎯 The Problem
8
+ In the digital age, information overload is a massive barrier. For individuals with ADHD, dyslexia, or cognitive fatigue, reading dense blocks of text is exhausting and hinders learning. Existing solutions (like "Bionic Reading") simply bold the first half of *every* word, which is arbitrary and ignores the actual meaning of the sentence.
9
+
10
+ ## 💡 The Solution
11
+ FlowRead AI solves this by extracting the mathematical "saliency" of words. By averaging the incoming attention scores across the middle layers of Gemma 2B, we determine exactly which nouns, verbs, and adjectives anchor the semantic meaning of the sentence.
12
+
13
+ **Key Features:**
14
+ * **🧠 True Semantic Saliency:** Real mathematical LLM attention extraction, ensuring only truly important words are emphasized.
15
+ * **🎯 Intent-Driven Reading:** By preprompting Gemma (e.g., *"Focus on numbers and dates"*), the internal attention mechanism dynamically shifts, allowing the user to read texts through specific "lenses."
16
+ * **🔬 Optimal Layer Selection:** Users can explore how Gemma thinks by peeking into the middle layers (semantic understanding) vs. the last layer (next-token prediction).
17
+ * **📊 Built-in A/B User Study:** A complete embedded research tool with an SQLite backend to gather real-world empirical data on how FlowRead impacts average reading speed and comprehension accuracy.
18
+
19
+ ## 🚀 Why Gemma?
20
+ We chose **Gemma 2B** because its open weights allow direct access to the `outputs.attentions` matrices—something impossible with closed-source, API-based models. Furthermore, at 2 billion parameters, it is lightweight enough to run locally on consumer hardware (Macs with MPS) or free cloud tiers (Hugging Face Spaces), democratizing this accessibility tool.
21
+
22
+ ## 🛠️ Technical Implementation
23
+ * **Backend:** FastAPI (Python) serving a PyTorch/Hugging Face pipeline.
24
+ * **Model:** `google/gemma-2b` running in `bfloat16` precision.
25
+ * **Frontend:** Pure HTML/JS/CSS with a minimalist, distraction-free "academic paper" aesthetic.
26
+ * **Database:** SQLite for storing A/B test results.
27
+
28
+ ## ⚙️ How to Run Locally
29
+
30
+ 1. **Install Dependencies:**
31
+ ```bash
32
+ pip install -r requirements.txt
33
+ ```
34
+ 2. **Authenticate with Hugging Face:**
35
+ Gemma 2B is a gated model. You must accept the terms on Hugging Face and log in:
36
+ ```bash
37
+ huggingface-cli login
38
+ ```
39
+ 3. **Start the Server:**
40
+ ```bash
41
+ python main.py
42
+ ```
43
+ 4. **Open the Web App:**
44
+ Navigate to `http://localhost:7860/static/index.html` in your browser.
45
+
46
+ ## 📊 The User Study (Help Us Prove It Works!)
47
+ When you deploy this project to Hugging Face Spaces, users can take the built-in **2-Minute Study**. The system randomly assigns users to read plain text vs. FlowRead text, times their reading speed, and tests their comprehension. The global statistics dashboard proves the real-world impact of Gemma-powered saliency!
static/index.html CHANGED
@@ -3,42 +3,97 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Text Saliency Pro</title>
7
  <style>
8
  body {
9
- font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
10
- max-width: 800px;
11
  margin: 0 auto;
12
  padding: 2rem;
13
- line-height: 1.5;
14
- background-color: #f9fafb;
15
- color: #111827;
16
  }
17
 
18
  h1 {
19
- font-size: 2.5rem;
20
- margin-bottom: 1rem;
21
  text-align: center;
 
 
 
 
 
 
22
  }
23
 
24
- p.description {
25
  text-align: center;
26
- color: #4b5563;
 
 
 
 
 
 
 
 
27
  margin-bottom: 2rem;
28
  }
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  .container {
31
- background: white;
32
  padding: 2rem;
33
  border-radius: 0.5rem;
34
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
 
 
 
 
 
 
 
 
35
  }
36
 
37
  textarea {
38
  width: 100%;
39
  height: 150px;
40
  padding: 0.75rem;
41
- border: 1px solid #d1d5db;
42
  border-radius: 0.375rem;
43
  font-size: 1rem;
44
  resize: vertical;
@@ -65,38 +120,71 @@
65
  input[type="range"] {
66
  flex-grow: 1;
67
  max-width: 300px;
 
68
  }
69
 
70
  button {
71
- background-color: #3b82f6;
72
- color: white;
73
  border: none;
74
  padding: 0.5rem 1.5rem;
75
  font-size: 1rem;
76
  border-radius: 0.375rem;
77
  cursor: pointer;
78
  transition: background-color 0.2s;
 
79
  }
80
 
81
  button:hover {
82
- background-color: #2563eb;
83
  }
84
 
85
  button:disabled {
86
- background-color: #9ca3af;
87
  cursor: not-allowed;
88
  }
89
 
90
  #result-container {
91
  margin-top: 2rem;
92
  padding: 1.5rem;
93
- background-color: #f3f4f6;
 
94
  border-radius: 0.375rem;
95
  min-height: 100px;
96
  white-space: pre-wrap;
97
  font-size: 1.125rem;
98
  }
99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  /* Token specific styles */
101
  .token {
102
  transition: font-weight 0.2s;
@@ -107,122 +195,494 @@
107
  color: #000;
108
  }
109
 
 
 
 
 
 
110
  #loading {
111
  display: none;
112
- color: #6b7280;
113
  text-align: center;
114
  margin-top: 1rem;
 
115
  }
116
  </style>
117
  </head>
118
  <body>
119
 
120
- <h1>Text Saliency Pro</h1>
121
- <p class="description">Improve reading comprehension using LLM attention vectors.<br>Words with attention above the threshold will be bolded.</p>
122
 
123
- <div class="container">
124
- <textarea id="text-input" placeholder="Enter or paste your text here...">In this project I want to use the attention vectors of a llm to bold the most important words in an input text to improve reading comprehension.</textarea>
125
-
126
- <div class="controls">
127
- <button id="analyze-btn">Analyze Text</button>
128
- <div class="slider-group">
129
- <label for="threshold">Attention Threshold: <span id="threshold-val">0.50</span></label>
130
- <input type="range" id="threshold" min="0" max="1" step="0.01" value="0.5">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  </div>
132
  </div>
133
-
134
- <div id="loading">Analyzing attention vectors with Gemma 2B... Please wait.</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
 
136
- <div id="result-container">
137
- <!-- Processed text will appear here -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  </div>
139
  </div>
140
 
141
  <script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  const inputArea = document.getElementById('text-input');
 
143
  const analyzeBtn = document.getElementById('analyze-btn');
144
  const thresholdSlider = document.getElementById('threshold');
145
  const thresholdVal = document.getElementById('threshold-val');
 
146
  const resultContainer = document.getElementById('result-container');
147
  const loading = document.getElementById('loading');
148
 
149
- let currentTokens = []; // Array of {token: str, word: str, score: float}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
 
151
- // Update threshold display
152
  thresholdSlider.addEventListener('input', (e) => {
153
  thresholdVal.textContent = parseFloat(e.target.value).toFixed(2);
154
- renderTokens(); // Re-render instantly when slider changes
155
  });
156
 
157
- // Analyze text when button is clicked
 
158
  analyzeBtn.addEventListener('click', async () => {
159
  const text = inputArea.value.trim();
 
 
 
 
160
  if (!text) return;
 
 
 
 
161
 
162
- // Update UI state
163
  analyzeBtn.disabled = true;
164
  loading.style.display = 'block';
165
  resultContainer.innerHTML = '';
166
 
167
  try {
168
- // Call the FastAPI backend
169
  const response = await fetch('/analyze', {
170
  method: 'POST',
171
- headers: {
172
- 'Content-Type': 'application/json'
173
- },
174
- body: JSON.stringify({ text })
175
  });
176
 
177
- if (!response.ok) {
178
- throw new Error('Network response was not ok');
179
- }
180
 
181
  const data = await response.json();
182
  currentTokens = data.words || [];
183
  renderTokens();
184
 
185
  } catch (error) {
186
- console.error('Error analyzing text:', error);
187
  resultContainer.innerHTML = '<span style="color: red;">Error analyzing text. Is the backend running?</span>';
188
  } finally {
189
- // Restore UI state
190
  analyzeBtn.disabled = false;
191
  loading.style.display = 'none';
192
  }
193
  });
194
 
195
- // Render the tokens based on the current threshold
196
  function renderTokens() {
197
  if (!currentTokens.length) return;
198
-
199
  const threshold = parseFloat(thresholdSlider.value);
200
- resultContainer.innerHTML = ''; // Clear existing
 
201
 
202
  currentTokens.forEach((item, index) => {
203
- // Skip the <bos> token (usually first)
204
- if (index === 0 && (item.token.includes('<bos>') || item.word.includes('<bos>'))) {
205
- return;
206
- }
207
 
208
  const span = document.createElement('span');
209
  span.className = 'token';
210
 
211
- // Add bolding if score is above threshold
212
- if (item.score >= threshold) {
213
- span.classList.add('highlighted');
 
 
 
 
 
 
 
214
  }
215
-
216
- // If the raw token started with a space, add it here.
217
- // The backend replaced the special block char with a normal space.
218
- // Depending on the tokenizer, 'word' might be better to display if it represents whole words,
219
- // but for subwords, using the raw token with correct spacing is usually best.
220
- let displayText = item.token;
221
 
222
- span.textContent = displayText;
223
  resultContainer.appendChild(span);
224
  });
225
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
  </script>
227
  </body>
228
  </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>FlowRead AI | Saliency-Guided Reading</title>
7
  <style>
8
  body {
9
+ font-family: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif; /* More paper/academic feel */
10
+ max-width: 900px;
11
  margin: 0 auto;
12
  padding: 2rem;
13
+ line-height: 1.6;
14
+ background-color: #fdfbf7; /* Slightly beige paper background */
15
+ color: #292524;
16
  }
17
 
18
  h1 {
19
+ font-size: 2.8rem;
20
+ margin-bottom: 0.5rem;
21
  text-align: center;
22
+ font-weight: 800;
23
+ background: linear-gradient(to right, #1c1917 0%, #1c1917 40%, #a8a29e 100%);
24
+ -webkit-background-clip: text;
25
+ -webkit-text-fill-color: transparent;
26
+ background-clip: text;
27
+ color: transparent;
28
  }
29
 
30
+ p.subtitle {
31
  text-align: center;
32
+ color: #57534e;
33
+ font-size: 1.1rem;
34
+ margin-bottom: 2rem;
35
+ }
36
+
37
+ .tabs {
38
+ display: flex;
39
+ justify-content: center;
40
+ gap: 1rem;
41
  margin-bottom: 2rem;
42
  }
43
 
44
+ .tab-btn {
45
+ background-color: #e7e5df;
46
+ color: #44403c;
47
+ border: 1px solid #d6d3d1;
48
+ padding: 0.75rem 1.5rem;
49
+ font-size: 1rem;
50
+ border-radius: 999px;
51
+ cursor: pointer;
52
+ font-weight: 600;
53
+ transition: all 0.2s;
54
+ font-family: inherit;
55
+ }
56
+
57
+ .tab-btn.active {
58
+ background-color: #292524;
59
+ color: #fdfbf7;
60
+ border-color: #292524;
61
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
62
+ }
63
+
64
+ .tab-content {
65
+ display: none;
66
+ }
67
+
68
+ .tab-content.active {
69
+ display: block;
70
+ animation: fadeIn 0.3s ease-in-out;
71
+ }
72
+
73
+ @keyframes fadeIn {
74
+ from { opacity: 0; transform: translateY(10px); }
75
+ to { opacity: 1; transform: translateY(0); }
76
+ }
77
+
78
  .container {
79
+ background: #fffcf8;
80
  padding: 2rem;
81
  border-radius: 0.5rem;
82
+ border: 1px solid #e7e5df;
83
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
84
+ }
85
+
86
+ textarea, input[type="text"] {
87
+ font-family: system-ui, -apple-system, sans-serif;
88
+ background-color: #fffcf8;
89
+ border: 1px solid #d6d3d1;
90
+ color: #292524;
91
  }
92
 
93
  textarea {
94
  width: 100%;
95
  height: 150px;
96
  padding: 0.75rem;
 
97
  border-radius: 0.375rem;
98
  font-size: 1rem;
99
  resize: vertical;
 
120
  input[type="range"] {
121
  flex-grow: 1;
122
  max-width: 300px;
123
+ accent-color: #57534e;
124
  }
125
 
126
  button {
127
+ background-color: #292524;
128
+ color: #fdfbf7;
129
  border: none;
130
  padding: 0.5rem 1.5rem;
131
  font-size: 1rem;
132
  border-radius: 0.375rem;
133
  cursor: pointer;
134
  transition: background-color 0.2s;
135
+ font-family: inherit;
136
  }
137
 
138
  button:hover {
139
+ background-color: #44403c;
140
  }
141
 
142
  button:disabled {
143
+ background-color: #a8a29e;
144
  cursor: not-allowed;
145
  }
146
 
147
  #result-container {
148
  margin-top: 2rem;
149
  padding: 1.5rem;
150
+ background-color: #f5f3ef;
151
+ border: 1px solid #e7e5df;
152
  border-radius: 0.375rem;
153
  min-height: 100px;
154
  white-space: pre-wrap;
155
  font-size: 1.125rem;
156
  }
157
 
158
+ /* Round Checkboxes */
159
+ input[type="checkbox"] {
160
+ appearance: none;
161
+ background-color: #fffcf8;
162
+ margin: 0;
163
+ font: inherit;
164
+ color: currentColor;
165
+ width: 1.15em;
166
+ height: 1.15em;
167
+ border: 1px solid #a8a29e;
168
+ border-radius: 50%;
169
+ display: grid;
170
+ place-content: center;
171
+ cursor: pointer;
172
+ }
173
+
174
+ input[type="checkbox"]::before {
175
+ content: "";
176
+ width: 0.65em;
177
+ height: 0.65em;
178
+ border-radius: 50%;
179
+ transform: scale(0);
180
+ transition: 120ms transform ease-in-out;
181
+ box-shadow: inset 1em 1em #292524;
182
+ }
183
+
184
+ input[type="checkbox"]:checked::before {
185
+ transform: scale(1);
186
+ }
187
+
188
  /* Token specific styles */
189
  .token {
190
  transition: font-weight 0.2s;
 
195
  color: #000;
196
  }
197
 
198
+ .semi-highlighted {
199
+ font-weight: 600;
200
+ color: #44403c;
201
+ }
202
+
203
  #loading {
204
  display: none;
205
+ color: #78716c;
206
  text-align: center;
207
  margin-top: 1rem;
208
+ font-style: italic;
209
  }
210
  </style>
211
  </head>
212
  <body>
213
 
214
+ <h1>FlowRead</h1>
215
+ <p class="subtitle">Accelerate reading comprehension using LLM attention vectors.</p>
216
 
217
+ <div class="tabs">
218
+ <button class="tab-btn active" onclick="switchTab('study')">Take the User Study</button>
219
+ <button class="tab-btn" onclick="switchTab('sandbox')">Playground (Sandbox)</button>
220
+ </div>
221
+
222
+ <!-- Playground Tab -->
223
+ <div id="sandbox-tab" class="tab-content">
224
+ <div class="container">
225
+ <textarea id="text-input" placeholder="Enter or paste your text here...">In the year 1969, Apollo 11 launched a massive rocket into space, filled with brilliant astronauts and powerful technology. It was a terrifying, yet awe-inspiring journey that changed history forever.</textarea>
226
+
227
+ <details style="margin-bottom: 1.5rem; border: 1px solid #d6d3d1; border-radius: 0.375rem; padding: 1rem; background: #fdfbf7;">
228
+ <summary style="cursor: pointer; font-weight: bold; color: #57534e;">Advanced Saliency Settings</summary>
229
+
230
+ <div style="margin-top: 1rem;">
231
+ <label for="preprompt" style="display:block; font-weight: 600; margin-bottom: 0.5rem; color: #44403c;">
232
+ Intent-Driven Reading (Preprompt)
233
+ </label>
234
+ <p style="font-size: 0.85rem; color: #78716c; margin-top: 0; margin-bottom: 0.5rem;">
235
+ Instruct Gemma what to focus on. Examples: "Focus on numbers and dates", "Highlight emotional words".
236
+ </p>
237
+ <input type="text" id="preprompt" placeholder="e.g., Focus only on verbs and action words..." style="width: 100%; padding: 0.5rem; border: 1px solid #d6d3d1; border-radius: 0.25rem; font-size: 0.9rem; box-sizing: border-box; margin-bottom: 1rem; font-family: inherit;">
238
+ </div>
239
+
240
+ <div>
241
+ <label style="display:block; font-weight: 600; margin-bottom: 0.5rem; color: #44403c;">
242
+ Network Layers (Gemma 2B: 18 Layers)
243
+ </label>
244
+ <p style="font-size: 0.85rem; color: #78716c; margin-top: 0; margin-bottom: 0.5rem;">
245
+ Select which layers to average attention scores from. Middle layers generally capture the best semantic meaning.
246
+ </p>
247
+ <div style="display: flex; gap: 0.5rem; margin-bottom: 0.75rem; flex-wrap: wrap;">
248
+ <button class="layer-preset" onclick="setLayerPreset('middle')" style="padding: 0.25rem 0.5rem; font-size: 0.8rem; background: #e7e5df; color: #44403c; border-radius: 0.25rem;">Middle Layers</button>
249
+ <button class="layer-preset" onclick="setLayerPreset('all')" style="padding: 0.25rem 0.5rem; font-size: 0.8rem; background: #e7e5df; color: #44403c; border-radius: 0.25rem;">All Layers</button>
250
+ <button class="layer-preset" onclick="setLayerPreset('first')" style="padding: 0.25rem 0.5rem; font-size: 0.8rem; background: #e7e5df; color: #44403c; border-radius: 0.25rem;">First Layer</button>
251
+ <button class="layer-preset" onclick="setLayerPreset('last')" style="padding: 0.25rem 0.5rem; font-size: 0.8rem; background: #e7e5df; color: #44403c; border-radius: 0.25rem;">Last Layer</button>
252
+ </div>
253
+ <div id="layer-checkboxes" style="display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center;">
254
+ <!-- Checkboxes generated by JS -->
255
+ </div>
256
+ </div>
257
+ </details>
258
+
259
+ <div class="controls">
260
+ <button id="analyze-btn">Analyze Text</button>
261
+ <div class="slider-group">
262
+ <label for="threshold" title="Lower threshold highlights more words">Threshold: <span id="threshold-val">0.35</span></label>
263
+ <input type="range" id="threshold" min="0" max="1" step="0.01" value="0.35">
264
+ </div>
265
+ <div class="slider-group">
266
+ <label for="gradient-mode" style="cursor:pointer;" title="Maps attention scores to visual contrast dynamically">
267
+ <input type="checkbox" id="gradient-mode"> Gradient Mode
268
+ </label>
269
+ </div>
270
+ </div>
271
+
272
+ <div id="loading">Analyzing attention vectors with Gemma 2B... Please wait.</div>
273
+
274
+ <div id="result-container">
275
+ <!-- Processed text will appear here -->
276
  </div>
277
  </div>
278
+ </div>
279
+
280
+ <!-- Study Tab -->
281
+ <div id="study-tab" class="tab-content active">
282
+ <!-- Step 1: Intro -->
283
+ <div id="study-intro" class="container" style="text-align: center;">
284
+ <h2>Help us prove FlowRead works!</h2>
285
+ <p style="max-width: 600px; margin: 0 auto 2rem; color: #4b5563;">
286
+ We are testing if LLM-guided saliency highlighting improves reading speed, comprehension, and retention.
287
+ You will be shown two short texts (one plain, one FlowRead-enhanced) and asked a few quick questions.
288
+ </p>
289
+ <button id="start-study-btn" style="font-size: 1.25rem; padding: 1rem 2rem;">Start the 2-Minute Study</button>
290
+ </div>
291
+
292
+ <!-- Step 1.5: Loading -->
293
+ <div id="study-loading" class="container" style="display: none; text-align: center;">
294
+ <h2 id="study-loading-title">Preparing your study...</h2>
295
+ <p>Our AI is analyzing the texts. This takes just a moment.</p>
296
+ </div>
297
 
298
+ <!-- Step 2: Reading -->
299
+ <div id="study-reading" class="container" style="display: none;">
300
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
301
+ <h2 id="study-topic" style="margin: 0; color: #1f2937;">Topic</h2>
302
+ <span id="study-progress" style="color: #6b7280; font-weight: bold; background: #e5e7eb; padding: 0.25rem 0.75rem; border-radius: 999px;">Text 1 of 2</span>
303
+ </div>
304
+ <div id="study-text-content" style="font-size: 1.25rem; line-height: 1.8; margin-bottom: 2rem; padding: 1.5rem; background: #fdfbf7; border: 1px solid #d6d3d1; border-radius: 0.5rem; min-height: 150px;">
305
+ <!-- Text goes here -->
306
+ </div>
307
+ <div style="text-align: center;">
308
+ <button id="done-reading-btn" style="font-size: 1.1rem; padding: 0.75rem 2rem;">I'm Done Reading</button>
309
+ </div>
310
+ </div>
311
+
312
+ <!-- Step 3: Questions -->
313
+ <div id="study-questions" class="container" style="display: none;">
314
+ <h2>Comprehension Check</h2>
315
+ <p style="color: #6b7280; margin-bottom: 1.5rem;">Please answer based on the text you just read. (You cannot go back!)</p>
316
+ <div id="questions-container" style="margin-bottom: 2rem;">
317
+ <!-- Questions go here -->
318
+ </div>
319
+ <button id="submit-answers-btn">Submit Answers</button>
320
+ </div>
321
+
322
+ <!-- Step 4: Results Dashboard -->
323
+ <div id="study-results" class="container" style="display: none; text-align: center;">
324
+ <h2>Global Study Results</h2>
325
+ <p style="color: #4b5563; margin-bottom: 2rem;">Thank you! Here is how FlowRead impacts reading across all users.</p>
326
+
327
+ <div style="display: flex; justify-content: center; gap: 2rem; margin-bottom: 2rem; flex-wrap: wrap;">
328
+ <!-- Plain Stats -->
329
+ <div style="background: #fffcf8; padding: 1.5rem; border-radius: 0.5rem; width: 250px; border: 1px solid #e7e5df;">
330
+ <h3 style="margin-top: 0; color: #292524;">Plain Text</h3>
331
+ <p style="font-size: 2rem; font-weight: bold; margin: 0.5rem 0;" id="stat-plain-time">-- s</p>
332
+ <p style="margin: 0; color: #57534e;">Average Reading Time</p>
333
+ <p style="font-size: 1.5rem; font-weight: bold; margin: 1rem 0 0.5rem;" id="stat-plain-acc">--%</p>
334
+ <p style="margin: 0; color: #57534e;">Comprehension Accuracy</p>
335
+ </div>
336
+ <!-- FlowRead Stats -->
337
+ <div style="background: #f5f3ef; padding: 1.5rem; border-radius: 0.5rem; width: 250px; border: 2px solid #a8a29e;">
338
+ <h3 style="margin-top: 0; color: #292524;">FlowRead Text</h3>
339
+ <p style="font-size: 2rem; font-weight: bold; margin: 0.5rem 0; color: #292524;" id="stat-flow-time">-- s</p>
340
+ <p style="margin: 0; color: #57534e;">Average Reading Time</p>
341
+ <p style="font-size: 1.5rem; font-weight: bold; margin: 1rem 0 0.5rem; color: #292524;" id="stat-flow-acc">--%</p>
342
+ <p style="margin: 0; color: #57534e;">Comprehension Accuracy</p>
343
+ </div>
344
+ </div>
345
+ <p style="font-size: 0.9rem; color: #6b7280;">Based on <span id="stat-sample-size">0</span> total reading sessions.</p>
346
+ <button onclick="switchTab('sandbox')" style="margin-top: 1rem;">Try FlowRead on your own text</button>
347
  </div>
348
  </div>
349
 
350
  <script>
351
+ function switchTab(tabName) {
352
+ document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
353
+ document.querySelectorAll('.tab-btn').forEach(el => el.classList.remove('active'));
354
+
355
+ if (tabName === 'study') {
356
+ document.getElementById('study-tab').classList.add('active');
357
+ document.querySelectorAll('.tab-btn')[0].classList.add('active');
358
+ } else {
359
+ document.getElementById('sandbox-tab').classList.add('active');
360
+ document.querySelectorAll('.tab-btn')[1].classList.add('active');
361
+ }
362
+ }
363
+
364
+ // ==========================================
365
+ // PLAYGROUND LOGIC
366
+ // ==========================================
367
  const inputArea = document.getElementById('text-input');
368
+ const prepromptInput = document.getElementById('preprompt');
369
  const analyzeBtn = document.getElementById('analyze-btn');
370
  const thresholdSlider = document.getElementById('threshold');
371
  const thresholdVal = document.getElementById('threshold-val');
372
+ const gradientMode = document.getElementById('gradient-mode');
373
  const resultContainer = document.getElementById('result-container');
374
  const loading = document.getElementById('loading');
375
 
376
+ let currentTokens = [];
377
+
378
+ // Generate Layer Checkboxes
379
+ const layerContainer = document.getElementById('layer-checkboxes');
380
+ for(let i = 0; i < 18; i++) {
381
+ const label = document.createElement('label');
382
+ label.style.display = 'flex';
383
+ label.style.alignItems = 'center';
384
+ label.style.gap = '0.25rem';
385
+ label.style.fontSize = '0.85rem';
386
+ label.style.cursor = 'pointer';
387
+
388
+ const cb = document.createElement('input');
389
+ cb.type = 'checkbox';
390
+ cb.value = i;
391
+ cb.className = 'layer-cb';
392
+
393
+ label.appendChild(cb);
394
+ label.appendChild(document.createTextNode(`L${i}`));
395
+ layerContainer.appendChild(label);
396
+ }
397
+
398
+ // Presets Logic
399
+ window.setLayerPreset = function(preset) {
400
+ const checkboxes = document.querySelectorAll('.layer-cb');
401
+ checkboxes.forEach(cb => cb.checked = false);
402
+
403
+ if(preset === 'all') {
404
+ checkboxes.forEach(cb => cb.checked = true);
405
+ } else if (preset === 'first') {
406
+ checkboxes[0].checked = true;
407
+ } else if (preset === 'last') {
408
+ checkboxes[17].checked = true;
409
+ } else if (preset === 'middle') {
410
+ // Middle 50% for 18 layers is ~4 through 13
411
+ for(let i = 4; i <= 13; i++) {
412
+ checkboxes[i].checked = true;
413
+ }
414
+ }
415
+ };
416
+ // Set middle as default
417
+ setLayerPreset('middle');
418
 
 
419
  thresholdSlider.addEventListener('input', (e) => {
420
  thresholdVal.textContent = parseFloat(e.target.value).toFixed(2);
421
+ renderTokens();
422
  });
423
 
424
+ gradientMode.addEventListener('change', renderTokens);
425
+
426
  analyzeBtn.addEventListener('click', async () => {
427
  const text = inputArea.value.trim();
428
+ const preprompt = prepromptInput.value.trim();
429
+
430
+ const checkedLayers = Array.from(document.querySelectorAll('.layer-cb:checked')).map(cb => parseInt(cb.value));
431
+
432
  if (!text) return;
433
+ if (checkedLayers.length === 0) {
434
+ alert("Please select at least one layer!");
435
+ return;
436
+ }
437
 
 
438
  analyzeBtn.disabled = true;
439
  loading.style.display = 'block';
440
  resultContainer.innerHTML = '';
441
 
442
  try {
 
443
  const response = await fetch('/analyze', {
444
  method: 'POST',
445
+ headers: { 'Content-Type': 'application/json' },
446
+ body: JSON.stringify({ text, preprompt, layers: checkedLayers })
 
 
447
  });
448
 
449
+ if (!response.ok) throw new Error('Network response was not ok');
 
 
450
 
451
  const data = await response.json();
452
  currentTokens = data.words || [];
453
  renderTokens();
454
 
455
  } catch (error) {
456
+ console.error('Error:', error);
457
  resultContainer.innerHTML = '<span style="color: red;">Error analyzing text. Is the backend running?</span>';
458
  } finally {
 
459
  analyzeBtn.disabled = false;
460
  loading.style.display = 'none';
461
  }
462
  });
463
 
 
464
  function renderTokens() {
465
  if (!currentTokens.length) return;
 
466
  const threshold = parseFloat(thresholdSlider.value);
467
+ const useGradient = gradientMode.checked;
468
+ resultContainer.innerHTML = '';
469
 
470
  currentTokens.forEach((item, index) => {
471
+ if (index === 0 && (item.token.includes('<bos>') || item.word.includes('<bos>'))) return;
 
 
 
472
 
473
  const span = document.createElement('span');
474
  span.className = 'token';
475
 
476
+ if (useGradient) {
477
+ // Map score (0-1) to opacity (0.4-1.0) and font-weight (400-800)
478
+ const opacity = 0.4 + (item.score * 0.6);
479
+ const weight = 400 + Math.round(item.score * 400);
480
+ span.style.opacity = opacity;
481
+ span.style.fontWeight = weight;
482
+ } else {
483
+ // Binary threshold mode
484
+ span.style = ''; // reset inline styles
485
+ if (item.score >= threshold) span.classList.add('highlighted');
486
  }
 
 
 
 
 
 
487
 
488
+ span.textContent = item.token;
489
  resultContainer.appendChild(span);
490
  });
491
  }
492
+
493
+ // ==========================================
494
+ // USER STUDY LOGIC
495
+ // ==========================================
496
+ const userId = 'user_' + Math.random().toString(36).substr(2, 9);
497
+ let studyTexts = [];
498
+ let currentStep = 0;
499
+ let conditionOrder = [];
500
+ let readingStartTime = 0;
501
+ let flowReadHTMLs = {};
502
+ let currentReadingTimeMs = 0;
503
+
504
+ document.getElementById('start-study-btn').addEventListener('click', async () => {
505
+ document.getElementById('study-intro').style.display = 'none';
506
+ document.getElementById('study-loading').style.display = 'block';
507
+
508
+ try {
509
+ // 1. Fetch texts from backend
510
+ const res = await fetch('/api/study/texts');
511
+ const data = await res.json();
512
+ studyTexts = data.texts;
513
+
514
+ // 2. Randomize condition order for A/B testing
515
+ conditionOrder = Math.random() > 0.5 ? ['plain', 'flowread'] : ['flowread', 'plain'];
516
+
517
+ // 3. Pre-calculate FlowRead AI highlighting so the timer isn't affected by network latency
518
+ const flowReadTextIndex = conditionOrder.indexOf('flowread');
519
+ const textToHighlight = studyTexts[flowReadTextIndex].text;
520
+
521
+ const analyzeRes = await fetch('/analyze', {
522
+ method: 'POST',
523
+ headers: { 'Content-Type': 'application/json' },
524
+ body: JSON.stringify({ text: textToHighlight, layer: 'middle' })
525
+ });
526
+ const analyzeData = await analyzeRes.json();
527
+
528
+ let html = '';
529
+ const threshold = 0.35; // Fixed threshold for the study
530
+ (analyzeData.words || []).forEach((item, index) => {
531
+ if (index === 0 && (item.token.includes('<bos>') || item.word.includes('<bos>'))) return;
532
+ let className = 'token';
533
+ if (item.score >= threshold) className += ' highlighted';
534
+ html += `<span class="${className}">${item.token}</span>`;
535
+ });
536
+ flowReadHTMLs[studyTexts[flowReadTextIndex].id] = html;
537
+
538
+ // 4. Start the first reading task
539
+ document.getElementById('study-loading').style.display = 'none';
540
+ showReadingScreen();
541
+
542
+ } catch (err) {
543
+ console.error(err);
544
+ alert("Error starting study. Please try again later.");
545
+ document.getElementById('study-loading').style.display = 'none';
546
+ document.getElementById('study-intro').style.display = 'block';
547
+ }
548
+ });
549
+
550
+ function showReadingScreen() {
551
+ const currentData = studyTexts[currentStep];
552
+ const currentCondition = conditionOrder[currentStep];
553
+
554
+ document.getElementById('study-topic').textContent = currentData.topic + (currentCondition === 'flowread' ? ' (FlowRead Enhanced)' : '');
555
+ document.getElementById('study-progress').textContent = `Text ${currentStep + 1} of 2`;
556
+
557
+ const contentDiv = document.getElementById('study-text-content');
558
+ if (currentCondition === 'flowread') {
559
+ contentDiv.innerHTML = flowReadHTMLs[currentData.id];
560
+ } else {
561
+ contentDiv.textContent = currentData.text;
562
+ }
563
+
564
+ document.getElementById('study-reading').style.display = 'block';
565
+ // Record exact start time
566
+ readingStartTime = performance.now();
567
+ }
568
+
569
+ document.getElementById('done-reading-btn').addEventListener('click', () => {
570
+ // Record exact end time
571
+ currentReadingTimeMs = Math.round(performance.now() - readingStartTime);
572
+ document.getElementById('study-reading').style.display = 'none';
573
+ showQuestionsScreen();
574
+ });
575
+
576
+ function showQuestionsScreen() {
577
+ const currentData = studyTexts[currentStep];
578
+ const container = document.getElementById('questions-container');
579
+ container.innerHTML = '';
580
+
581
+ currentData.questions.forEach((q, qIndex) => {
582
+ const qDiv = document.createElement('div');
583
+ qDiv.style.marginBottom = '1.5rem';
584
+ qDiv.style.background = '#fff';
585
+ qDiv.style.padding = '1rem';
586
+ qDiv.style.borderRadius = '0.375rem';
587
+ qDiv.style.border = '1px solid #e5e7eb';
588
+
589
+ qDiv.innerHTML = `<p style="font-weight: bold; margin-top: 0; margin-bottom: 0.75rem;">${qIndex + 1}. ${q.question}</p>`;
590
+
591
+ q.options.forEach((opt, oIndex) => {
592
+ qDiv.innerHTML += `
593
+ <label style="display: block; margin-bottom: 0.5rem; cursor: pointer; padding: 0.25rem 0;">
594
+ <input type="radio" name="q${qIndex}" value="${oIndex}" style="margin-right: 0.5rem;"> ${opt}
595
+ </label>
596
+ `;
597
+ });
598
+ container.appendChild(qDiv);
599
+ });
600
+
601
+ document.getElementById('study-questions').style.display = 'block';
602
+ }
603
+
604
+ document.getElementById('submit-answers-btn').addEventListener('click', async () => {
605
+ const currentData = studyTexts[currentStep];
606
+ let score = 0;
607
+ let allAnswered = true;
608
+
609
+ // Grade questions
610
+ currentData.questions.forEach((q, qIndex) => {
611
+ const selected = document.querySelector(`input[name="q${qIndex}"]:checked`);
612
+ if (!selected) {
613
+ allAnswered = false;
614
+ } else if (parseInt(selected.value) === q.correct) {
615
+ score++;
616
+ }
617
+ });
618
+
619
+ if (!allAnswered) {
620
+ alert("Please answer all questions before proceeding.");
621
+ return;
622
+ }
623
+
624
+ document.getElementById('submit-answers-btn').disabled = true;
625
+ document.getElementById('submit-answers-btn').textContent = "Submitting...";
626
+
627
+ // Send result to backend
628
+ try {
629
+ await fetch('/api/study/submit', {
630
+ method: 'POST',
631
+ headers: { 'Content-Type': 'application/json' },
632
+ body: JSON.stringify({
633
+ user_id: userId,
634
+ text_id: currentData.id,
635
+ condition: conditionOrder[currentStep],
636
+ reading_time_ms: currentReadingTimeMs,
637
+ score: score,
638
+ total_questions: currentData.questions.length
639
+ })
640
+ });
641
+ } catch (err) {
642
+ console.error("Failed to submit result:", err);
643
+ }
644
+
645
+ document.getElementById('study-questions').style.display = 'none';
646
+ document.getElementById('submit-answers-btn').disabled = false;
647
+ document.getElementById('submit-answers-btn').textContent = "Submit Answers";
648
+
649
+ currentStep++;
650
+ if (currentStep < 2) {
651
+ // Next text
652
+ showReadingScreen();
653
+ } else {
654
+ // Done! Show stats
655
+ showResultsScreen();
656
+ }
657
+ });
658
+
659
+ async function showResultsScreen() {
660
+ document.getElementById('study-loading').style.display = 'block';
661
+ document.getElementById('study-loading-title').textContent = "Loading global statistics...";
662
+
663
+ try {
664
+ const res = await fetch('/api/study/stats');
665
+ const stats = await res.json();
666
+
667
+ const formatTime = ms => ms ? (ms / 1000).toFixed(1) + ' s' : '-- s';
668
+ const formatAcc = pct => pct ? Math.round(pct) + '%' : '--%';
669
+
670
+ document.getElementById('stat-plain-time').textContent = formatTime(stats.plain.avg_reading_time_ms);
671
+ document.getElementById('stat-plain-acc').textContent = formatAcc(stats.plain.avg_accuracy_percent);
672
+
673
+ document.getElementById('stat-flow-time').textContent = formatTime(stats.flowread.avg_reading_time_ms);
674
+ document.getElementById('stat-flow-acc').textContent = formatAcc(stats.flowread.avg_accuracy_percent);
675
+
676
+ document.getElementById('stat-sample-size').textContent = stats.plain.sample_size + stats.flowread.sample_size;
677
+
678
+ document.getElementById('study-loading').style.display = 'none';
679
+ document.getElementById('study-results').style.display = 'block';
680
+
681
+ } catch (err) {
682
+ console.error(err);
683
+ alert("Error loading stats.");
684
+ }
685
+ }
686
  </script>
687
  </body>
688
  </html>