Some Guy commited on
Commit ·
1e55f99
1
Parent(s): 84f2f65
UI styling updates and README
Browse files- README.md +47 -0
- 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>
|
| 7 |
<style>
|
| 8 |
body {
|
| 9 |
-
font-family:
|
| 10 |
-
max-width:
|
| 11 |
margin: 0 auto;
|
| 12 |
padding: 2rem;
|
| 13 |
-
line-height: 1.
|
| 14 |
-
background-color: #
|
| 15 |
-
color: #
|
| 16 |
}
|
| 17 |
|
| 18 |
h1 {
|
| 19 |
-
font-size: 2.
|
| 20 |
-
margin-bottom:
|
| 21 |
text-align: center;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
}
|
| 23 |
|
| 24 |
-
p.
|
| 25 |
text-align: center;
|
| 26 |
-
color: #
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
margin-bottom: 2rem;
|
| 28 |
}
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
.container {
|
| 31 |
-
background:
|
| 32 |
padding: 2rem;
|
| 33 |
border-radius: 0.5rem;
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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: #
|
| 72 |
-
color:
|
| 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: #
|
| 83 |
}
|
| 84 |
|
| 85 |
button:disabled {
|
| 86 |
-
background-color: #
|
| 87 |
cursor: not-allowed;
|
| 88 |
}
|
| 89 |
|
| 90 |
#result-container {
|
| 91 |
margin-top: 2rem;
|
| 92 |
padding: 1.5rem;
|
| 93 |
-
background-color: #
|
|
|
|
| 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: #
|
| 113 |
text-align: center;
|
| 114 |
margin-top: 1rem;
|
|
|
|
| 115 |
}
|
| 116 |
</style>
|
| 117 |
</head>
|
| 118 |
<body>
|
| 119 |
|
| 120 |
-
<h1>
|
| 121 |
-
<p class="
|
| 122 |
|
| 123 |
-
<div class="
|
| 124 |
-
<
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
</div>
|
| 132 |
</div>
|
| 133 |
-
|
| 134 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
|
| 136 |
-
<
|
| 137 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 = [];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
|
| 151 |
-
// Update threshold display
|
| 152 |
thresholdSlider.addEventListener('input', (e) => {
|
| 153 |
thresholdVal.textContent = parseFloat(e.target.value).toFixed(2);
|
| 154 |
-
renderTokens();
|
| 155 |
});
|
| 156 |
|
| 157 |
-
|
|
|
|
| 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 |
-
|
| 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
|
| 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 |
-
|
|
|
|
| 201 |
|
| 202 |
currentTokens.forEach((item, index) => {
|
| 203 |
-
|
| 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 |
-
|
| 212 |
-
|
| 213 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 =
|
| 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>
|