FlowRead / static /index.html
jobbler's picture
Fix tokenizer extra_special_tokens crash and stabilize transformers
bd01d05
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FlowRead AI | Saliency-Guided Reading</title>
<style>
body {
font-family: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif; /* More paper/academic feel */
max-width: 900px;
margin: 0 auto;
padding: 2rem;
line-height: 1.6;
background-color: #fdfbf7; /* Slightly beige paper background */
color: #292524;
}
h1 {
font-size: 2.8rem;
margin-bottom: 0.5rem;
text-align: center;
font-weight: 800;
color: #1c1917;
}
p.subtitle {
text-align: center;
color: #57534e;
font-size: 1.1rem;
margin-bottom: 2rem;
}
.tabs {
display: flex;
justify-content: center;
gap: 1rem;
margin-bottom: 2rem;
}
.tab-btn {
background-color: #e7e5df;
color: #44403c;
border: 1px solid #d6d3d1;
padding: 0.75rem 1.5rem;
font-size: 1rem;
border-radius: 999px;
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
font-family: inherit;
}
.tab-btn.active {
background-color: #292524;
color: #fdfbf7;
border-color: #292524;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
animation: fadeIn 0.3s ease-in-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.container {
background: #fffcf8;
padding: 2rem;
border-radius: 0.5rem;
border: 1px solid #e7e5df;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
}
textarea, input[type="text"] {
font-family: system-ui, -apple-system, sans-serif;
background-color: #fffcf8;
border: 1px solid #d6d3d1;
color: #292524;
}
textarea {
width: 100%;
height: 150px;
padding: 0.75rem;
border-radius: 0.375rem;
font-size: 1rem;
resize: vertical;
margin-bottom: 1rem;
box-sizing: border-box;
}
.controls {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1.5rem;
flex-wrap: wrap;
gap: 1rem;
}
.slider-group {
display: flex;
align-items: center;
gap: 1rem;
flex-grow: 1;
}
input[type="range"] {
flex-grow: 1;
max-width: 300px;
accent-color: #57534e;
}
button {
background-color: #292524;
color: #fdfbf7;
border: none;
padding: 0.5rem 1.5rem;
font-size: 1rem;
border-radius: 0.375rem;
cursor: pointer;
transition: background-color 0.2s;
font-family: inherit;
}
button:hover {
background-color: #44403c;
}
button:disabled {
background-color: #a8a29e;
cursor: not-allowed;
}
#result-container {
margin-top: 2rem;
padding: 1.5rem;
background-color: #f5f3ef;
border: 1px solid #e7e5df;
border-radius: 0.375rem;
min-height: 100px;
white-space: pre-wrap;
font-size: 1.125rem;
}
/* Round Checkboxes */
input[type="checkbox"] {
appearance: none;
background-color: #fffcf8;
margin: 0;
font: inherit;
color: currentColor;
width: 1.15em;
height: 1.15em;
border: 1px solid #a8a29e;
border-radius: 50%;
display: grid;
place-content: center;
cursor: pointer;
}
input[type="checkbox"]::before {
content: "";
width: 0.65em;
height: 0.65em;
border-radius: 50%;
transform: scale(0);
transition: 120ms transform ease-in-out;
box-shadow: inset 1em 1em #292524;
}
input[type="checkbox"]:checked::before {
transform: scale(1);
}
/* Token specific styles */
.token {
transition: font-weight 0.2s;
}
.highlighted {
font-weight: 800; /* Extra bold */
color: #000;
}
.semi-highlighted {
font-weight: 600;
color: #44403c;
}
#loading {
display: none;
color: #78716c;
text-align: center;
margin-top: 1rem;
font-style: italic;
}
</style>
</head>
<body>
<h1>Flow<span style="color: #a8a29e; font-weight: 500;">Read</span></h1>
<p class="subtitle">Accelerate reading comprehension using LLM attention vectors.</p>
<div class="tabs">
<button class="tab-btn active" onclick="switchTab('study')">Take the User Study</button>
<button class="tab-btn" onclick="switchTab('sandbox')">Playground (Sandbox)</button>
</div>
<!-- Playground Tab -->
<div id="sandbox-tab" class="tab-content">
<div class="container">
<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>
<details style="margin-bottom: 1.5rem; border: 1px solid #d6d3d1; border-radius: 0.375rem; padding: 1rem; background: #fdfbf7;">
<summary style="cursor: pointer; font-weight: bold; color: #57534e;">Advanced Saliency Settings</summary>
<div style="margin-top: 1rem; margin-bottom: 1rem;">
<label for="saliency-mode" style="display:block; font-weight: 600; margin-bottom: 0.5rem; color: #44403c;">
Saliency Calculation Mode
</label>
<p style="font-size: 0.85rem; color: #78716c; margin-top: 0; margin-bottom: 0.5rem;">
Choose whether importance is calculated relative to this text alone, or absolutely across all texts.
</p>
<select id="saliency-mode" style="width: 100%; padding: 0.5rem; border: 1px solid #d6d3d1; border-radius: 0.25rem; font-size: 0.9rem; box-sizing: border-box; font-family: inherit;">
<option value="local">Local Mode (Relative Min/Max)</option>
<option value="global">Global Mode (Absolute Values with Penalty)</option>
</select>
</div>
<div style="margin-top: 1rem;">
<label for="preprompt" style="display:block; font-weight: 600; margin-bottom: 0.5rem; color: #44403c;">
Intent-Driven Reading (Preprompt)
</label>
<p style="font-size: 0.85rem; color: #78716c; margin-top: 0; margin-bottom: 0.5rem;">
Instruct Gemma what to focus on. Examples: "Focus on numbers and dates", "Highlight emotional words".
</p>
<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;">
</div>
<div>
<label for="layer-preset" style="display:block; font-weight: 600; margin-bottom: 0.5rem; color: #44403c;">
Network Layers
</label>
<p style="font-size: 0.85rem; color: #78716c; margin-top: 0; margin-bottom: 0.5rem;">
Select which section of the network's layers to extract attention scores from. Middle layers generally capture the best semantic meaning.
</p>
<select id="layer-preset" style="width: 100%; padding: 0.5rem; border: 1px solid #d6d3d1; border-radius: 0.25rem; font-size: 0.9rem; box-sizing: border-box; font-family: inherit;">
<option value="middle" selected>Middle Layers (Semantic focus)</option>
<option value="first">First Few Layers (Lexical/Syntax focus)</option>
<option value="last">Last Few Layers (Output formatting focus)</option>
<option value="all">All Layers (Averaged)</option>
</select>
</div>
</details>
<div class="controls">
<button id="analyze-btn">Analyze Text</button>
<div class="slider-group">
<label for="threshold" title="Lower threshold highlights more words">Threshold: <span id="threshold-val">0.35</span></label>
<input type="range" id="threshold" min="0" max="1" step="0.01" value="0.35">
</div>
<div class="slider-group" style="display: flex; align-items: center; justify-content: flex-start; gap: 0.5rem;">
<input type="checkbox" id="gradient-mode" style="margin: 0;">
<label for="gradient-mode" style="cursor:pointer; margin: 0; white-space: nowrap;" title="Maps attention scores to visual contrast dynamically">
Gradient Mode
</label>
</div>
</div>
<div id="loading">Analyzing attention vectors with Gemma 2B... Please wait.</div>
<div id="result-container">
<!-- Processed text will appear here -->
</div>
</div>
</div>
<!-- Study Tab -->
<div id="study-tab" class="tab-content active">
<!-- Step 1: Intro -->
<div id="study-intro" class="container" style="text-align: center;">
<h2>Help us prove FlowRead works!</h2>
<p style="max-width: 600px; margin: 0 auto 2rem; color: #4b5563;">
We are testing if LLM-guided saliency highlighting improves reading speed, comprehension, and retention.
You will be shown three short texts (one plain, one FlowRead bolding, one FlowRead gradient) and asked a few quick questions.
</p>
<button id="start-study-btn" style="font-size: 1.25rem; padding: 1rem 2rem;">Start the 3-Minute Study</button>
</div>
<!-- Step 1.5: Loading -->
<div id="study-loading" class="container" style="display: none; text-align: center;">
<h2 id="study-loading-title">Preparing your study...</h2>
<p>Our AI is analyzing the texts. This takes just a moment.</p>
</div>
<!-- Step 2: Reading -->
<div id="study-reading" class="container" style="display: none;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<h2 id="study-topic" style="margin: 0; color: #1f2937;">Topic</h2>
<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>
</div>
<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;">
<!-- Text goes here -->
</div>
<div style="text-align: center;">
<button id="done-reading-btn" style="font-size: 1.1rem; padding: 0.75rem 2rem;">I'm Done Reading</button>
</div>
</div>
<!-- Step 3: Questions -->
<div id="study-questions" class="container" style="display: none;">
<h2>Comprehension Check</h2>
<p style="color: #6b7280; margin-bottom: 1.5rem;">Please answer based on the text you just read. (You cannot go back!)</p>
<div id="questions-container" style="margin-bottom: 2rem;">
<!-- Questions go here -->
</div>
<button id="submit-answers-btn">Submit Answers</button>
</div>
<!-- Step 3.5: Preference -->
<div id="study-preference" class="container" style="display: none; text-align: center;">
<h2>Which text style did you prefer?</h2>
<p style="color: #6b7280; margin-bottom: 2rem;">Please select the reading experience you liked the most.</p>
<div style="display: flex; flex-direction: column; gap: 1rem; max-width: 400px; margin: 0 auto 2rem;">
<button class="pref-btn" data-pref="plain" style="padding: 1rem; font-size: 1.1rem; background: #fffcf8; color: #292524; border: 1px solid #e7e5df;">Plain Text</button>
<button class="pref-btn" data-pref="flowread" style="padding: 1rem; font-size: 1.1rem; background: #f5f3ef; color: #292524; border: 1px solid #a8a29e;">FlowRead (bolding)</button>
<button class="pref-btn" data-pref="gradient" style="padding: 1rem; font-size: 1.1rem; background: #eef2ff; color: #1e3a8a; border: 1px solid #c7d2fe;">FlowRead (gradient)</button>
</div>
</div>
<!-- Step 4: Results Dashboard -->
<div id="study-results" class="container" style="display: none; text-align: center;">
<h2>Global Study Results</h2>
<p style="color: #4b5563; margin-bottom: 2rem;">Thank you! Here is how FlowRead impacts reading across all users.</p>
<div style="display: flex; justify-content: center; gap: 2rem; margin-bottom: 2rem; flex-wrap: wrap;">
<!-- Plain Stats -->
<div style="background: #fffcf8; padding: 1.5rem; border-radius: 0.5rem; width: 250px; border: 1px solid #e7e5df;">
<h3 style="margin-top: 0; color: #292524;">Plain Text</h3>
<p style="font-size: 2rem; font-weight: bold; margin: 0.5rem 0;" id="stat-plain-time">-- s</p>
<p style="margin: 0; color: #57534e;">Average Reading Time</p>
<p style="font-size: 1.5rem; font-weight: bold; margin: 1rem 0 0.5rem;" id="stat-plain-acc">--%</p>
<p style="margin: 0; color: #57534e;">Comprehension Accuracy</p>
</div>
<!-- FlowRead Stats -->
<div style="background: #f5f3ef; padding: 1.5rem; border-radius: 0.5rem; width: 250px; border: 2px solid #a8a29e;">
<h3 style="margin-top: 0; color: #292524;">FlowRead (bolding)</h3>
<p style="font-size: 2rem; font-weight: bold; margin: 0.5rem 0; color: #292524;" id="stat-flow-time">-- s</p>
<p style="margin: 0; color: #57534e;">Average Reading Time</p>
<p style="font-size: 1.5rem; font-weight: bold; margin: 1rem 0 0.5rem; color: #292524;" id="stat-flow-acc">--%</p>
<p style="margin: 0; color: #57534e;">Comprehension Accuracy</p>
</div>
<!-- Gradient Stats -->
<div style="background: #eef2ff; padding: 1.5rem; border-radius: 0.5rem; width: 250px; border: 2px solid #a5b4fc;">
<h3 style="margin-top: 0; color: #1e3a8a;">FlowRead (gradient)</h3>
<p style="font-size: 2rem; font-weight: bold; margin: 0.5rem 0; color: #1e3a8a;" id="stat-grad-time">-- s</p>
<p style="margin: 0; color: #3730a3;">Average Reading Time</p>
<p style="font-size: 1.5rem; font-weight: bold; margin: 1rem 0 0.5rem; color: #1e3a8a;" id="stat-grad-acc">--%</p>
<p style="margin: 0; color: #3730a3;">Comprehension Accuracy</p>
</div>
</div>
<div style="margin-bottom: 2rem;">
<h3 style="color: #4b5563;">User Preferences</h3>
<p>Plain: <span id="pref-plain">0</span> | FlowRead (bolding): <span id="pref-flowread">0</span> | FlowRead (gradient): <span id="pref-gradient">0</span></p>
</div>
<p style="font-size: 0.9rem; color: #6b7280;">Based on <span id="stat-sample-size">0</span> total reading sessions.</p>
<button onclick="switchTab('sandbox')" style="margin-top: 1rem;">Try FlowRead on your own text</button>
</div>
</div>
<script>
function switchTab(tabName) {
document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
document.querySelectorAll('.tab-btn').forEach(el => el.classList.remove('active'));
if (tabName === 'study') {
document.getElementById('study-tab').classList.add('active');
document.querySelectorAll('.tab-btn')[0].classList.add('active');
} else {
document.getElementById('sandbox-tab').classList.add('active');
document.querySelectorAll('.tab-btn')[1].classList.add('active');
}
}
// ==========================================
// PLAYGROUND LOGIC
// ==========================================
const inputArea = document.getElementById('text-input');
const prepromptInput = document.getElementById('preprompt');
const saliencyModeInput = document.getElementById('saliency-mode');
const layerPresetInput = document.getElementById('layer-preset');
const analyzeBtn = document.getElementById('analyze-btn');
const thresholdSlider = document.getElementById('threshold');
const thresholdVal = document.getElementById('threshold-val');
const gradientMode = document.getElementById('gradient-mode');
const resultContainer = document.getElementById('result-container');
const loading = document.getElementById('loading');
let currentTokens = [];
thresholdSlider.addEventListener('input', (e) => {
thresholdVal.textContent = parseFloat(e.target.value).toFixed(2);
renderTokens();
});
gradientMode.addEventListener('change', renderTokens);
analyzeBtn.addEventListener('click', async () => {
const text = inputArea.value.trim();
const preprompt = prepromptInput.value.trim();
const saliencyMode = saliencyModeInput.value;
const layerPreset = layerPresetInput.value;
const modelVersion = "2b";
if (!text) return;
analyzeBtn.disabled = true;
loading.style.display = 'block';
loading.textContent = 'Analyzing text with Gemma 4...';
resultContainer.innerHTML = '';
let isFetching = true;
const pollStatus = async () => {
while(isFetching) {
try {
const statusRes = await fetch('/status');
if (statusRes.ok) {
const statusData = await statusRes.json();
if (statusData[modelVersion] && statusData[modelVersion].startsWith("downloading")) {
const parts = statusData[modelVersion].split(": ");
const progress = parts.length > 1 ? parts[1] : "...";
loading.textContent = `Downloading Gemma 4 (${modelVersion}) Model ${progress}... this takes a few minutes.`;
}
}
} catch(e) {}
await new Promise(r => setTimeout(r, 2000));
}
};
pollStatus();
try {
const response = await fetch(`/analyze/${modelVersion}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, preprompt, layer_preset: layerPreset, saliency_mode: saliencyMode })
});
isFetching = false;
if (!response.ok) throw new Error('Network response was not ok');
const data = await response.json();
currentTokens = data.words || [];
renderTokens();
} catch (error) {
console.error('Error:', error);
resultContainer.innerHTML = '<span style="color: red;">Error analyzing text. Is the backend running?</span>';
} finally {
analyzeBtn.disabled = false;
loading.style.display = 'none';
}
});
function renderTokens() {
if (!currentTokens.length) return;
const threshold = parseFloat(thresholdSlider.value);
const useGradient = gradientMode.checked;
resultContainer.innerHTML = '';
// Group tokens into words based on the SentencePiece space prefix
let words = [];
let currentWordTokens = [];
let currentWordMaxScore = 0;
currentTokens.forEach((item, index) => {
if (index === 0 && (item.token.includes('<bos>') || item.word.includes('<bos>'))) return;
const isWhitespace = item.token.trim() === '';
const prevIsWhitespace = currentWordTokens.length > 0 && currentWordTokens[currentWordTokens.length - 1].token.trim() === '';
if (item.token.startsWith(' ') || (currentWordTokens.length > 0 && isWhitespace !== prevIsWhitespace)) {
if (currentWordTokens.length > 0) {
words.push({ tokens: currentWordTokens, maxScore: currentWordMaxScore });
}
currentWordTokens = [item];
currentWordMaxScore = item.score;
} else {
currentWordTokens.push(item);
currentWordMaxScore = Math.max(currentWordMaxScore, item.score);
}
});
if (currentWordTokens.length > 0) {
words.push({ tokens: currentWordTokens, maxScore: currentWordMaxScore });
}
// Render words
words.forEach(wordObj => {
const isWordHighlighted = wordObj.maxScore >= threshold;
wordObj.tokens.forEach(item => {
const span = document.createElement('span');
span.className = 'token';
if (useGradient) {
// In gradient mode, we still render token-by-token detail
const opacity = 0.4 + (item.score * 0.6);
const weight = 400 + Math.round(item.score * 400);
span.style.opacity = opacity;
span.style.fontWeight = weight;
} else {
// Binary mode: entire word gets highlighted if ANY part of it crossed threshold
if (isWordHighlighted) span.classList.add('highlighted');
}
span.textContent = item.token;
resultContainer.appendChild(span);
});
});
}
// ==========================================
// USER STUDY LOGIC
// ==========================================
const userId = 'user_' + Math.random().toString(36).substr(2, 9);
let studyTexts = [];
let currentStep = 0;
let conditionOrder = [];
let readingStartTime = 0;
let flowReadHTMLs = {};
let flowReadGradientHTMLs = {};
let currentReadingTimeMs = 0;
// Helper to shuffle array
function shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
document.getElementById('start-study-btn').addEventListener('click', async () => {
currentStep = 0;
document.getElementById('study-intro').style.display = 'none';
document.getElementById('study-loading').style.display = 'block';
try {
// 1. Fetch texts from backend
const res = await fetch('/api/study/texts');
const data = await res.json();
studyTexts = data.texts;
// 2. Randomize condition order for A/B/C testing
conditionOrder = shuffle(['plain', 'flowread', 'gradient']);
// 3. Load HTMLs into dicts
const flowReadTextIndex = conditionOrder.indexOf('flowread');
flowReadHTMLs[studyTexts[flowReadTextIndex].id] = studyTexts[flowReadTextIndex].flowread_html;
const gradientTextIndex = conditionOrder.indexOf('gradient');
flowReadGradientHTMLs[studyTexts[gradientTextIndex].id] = studyTexts[gradientTextIndex].flowread_gradient_html;
// 4. Start the first reading task
document.getElementById('study-loading').style.display = 'none';
showReadingScreen();
} catch (err) {
console.error(err);
alert("Error starting study. Please try again later.");
document.getElementById('study-loading').style.display = 'none';
document.getElementById('study-intro').style.display = 'block';
}
});
function showReadingScreen() {
const currentData = studyTexts[currentStep];
const currentCondition = conditionOrder[currentStep];
let topicText = currentData.topic;
if (currentCondition === 'flowread') topicText += ' (FlowRead bolding)';
if (currentCondition === 'gradient') topicText += ' (FlowRead gradient)';
document.getElementById('study-topic').textContent = topicText;
document.getElementById('study-progress').textContent = `Text ${currentStep + 1} of 3`;
const contentDiv = document.getElementById('study-text-content');
if (currentCondition === 'flowread') {
contentDiv.innerHTML = flowReadHTMLs[currentData.id];
} else if (currentCondition === 'gradient') {
contentDiv.innerHTML = flowReadGradientHTMLs[currentData.id];
} else {
contentDiv.textContent = currentData.text;
}
document.getElementById('study-reading').style.display = 'block';
// Record exact start time
readingStartTime = performance.now();
}
document.getElementById('done-reading-btn').addEventListener('click', () => {
// Record exact end time
currentReadingTimeMs = Math.round(performance.now() - readingStartTime);
document.getElementById('study-reading').style.display = 'none';
showQuestionsScreen();
});
function showQuestionsScreen() {
const currentData = studyTexts[currentStep];
const container = document.getElementById('questions-container');
container.innerHTML = '';
currentData.questions.forEach((q, qIndex) => {
const qDiv = document.createElement('div');
qDiv.style.marginBottom = '1.5rem';
qDiv.style.background = '#fff';
qDiv.style.padding = '1rem';
qDiv.style.borderRadius = '0.375rem';
qDiv.style.border = '1px solid #e5e7eb';
qDiv.innerHTML = `<p style="font-weight: bold; margin-top: 0; margin-bottom: 0.75rem;">${qIndex + 1}. ${q.question}</p>`;
q.options.forEach((opt, oIndex) => {
qDiv.innerHTML += `
<label style="display: block; margin-bottom: 0.5rem; cursor: pointer; padding: 0.25rem 0;">
<input type="radio" name="q${qIndex}" value="${oIndex}" style="margin-right: 0.5rem;"> ${opt}
</label>
`;
});
container.appendChild(qDiv);
});
document.getElementById('study-questions').style.display = 'block';
}
document.getElementById('submit-answers-btn').addEventListener('click', async () => {
const currentData = studyTexts[currentStep];
let score = 0;
let allAnswered = true;
// Grade questions
currentData.questions.forEach((q, qIndex) => {
const selected = document.querySelector(`input[name="q${qIndex}"]:checked`);
if (!selected) {
allAnswered = false;
} else if (parseInt(selected.value) === q.correct) {
score++;
}
});
if (!allAnswered) {
alert("Please answer all questions before proceeding.");
return;
}
document.getElementById('submit-answers-btn').disabled = true;
document.getElementById('submit-answers-btn').textContent = "Submitting...";
// Send result to backend
try {
await fetch('/api/study/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
user_id: userId,
text_id: currentData.id,
condition: conditionOrder[currentStep],
reading_time_ms: currentReadingTimeMs,
score: score,
total_questions: currentData.questions.length
})
});
} catch (err) {
console.error("Failed to submit result:", err);
}
document.getElementById('study-questions').style.display = 'none';
document.getElementById('submit-answers-btn').disabled = false;
document.getElementById('submit-answers-btn').textContent = "Submit Answers";
currentStep++;
if (currentStep < 3) {
// Next text
showReadingScreen();
} else {
// Done! Show preference questionnaire
document.getElementById('study-preference').style.display = 'block';
}
});
// Handle preference selection
document.querySelectorAll('.pref-btn').forEach(btn => {
btn.addEventListener('click', async (e) => {
const preference = e.target.getAttribute('data-pref');
// Disable buttons
document.querySelectorAll('.pref-btn').forEach(b => b.disabled = true);
e.target.textContent = "Submitting...";
try {
await fetch('/api/study/preference', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
user_id: userId,
preference: preference
})
});
} catch (err) {
console.error("Failed to submit preference:", err);
}
document.getElementById('study-preference').style.display = 'none';
showResultsScreen();
});
});
async function showResultsScreen() {
document.getElementById('study-loading').style.display = 'block';
document.getElementById('study-loading-title').textContent = "Loading global statistics...";
try {
const res = await fetch('/api/study/stats');
const stats = await res.json();
const formatTime = ms => ms ? (ms / 1000).toFixed(1) + ' s' : '-- s';
const formatAcc = pct => pct ? Math.round(pct) + '%' : '--%';
document.getElementById('stat-plain-time').textContent = formatTime(stats.plain.avg_reading_time_ms);
document.getElementById('stat-plain-acc').textContent = formatAcc(stats.plain.avg_accuracy_percent);
document.getElementById('stat-flow-time').textContent = formatTime(stats.flowread.avg_reading_time_ms);
document.getElementById('stat-flow-acc').textContent = formatAcc(stats.flowread.avg_accuracy_percent);
document.getElementById('stat-grad-time').textContent = formatTime(stats.gradient.avg_reading_time_ms);
document.getElementById('stat-grad-acc').textContent = formatAcc(stats.gradient.avg_accuracy_percent);
document.getElementById('pref-plain').textContent = stats.preferences.plain || 0;
document.getElementById('pref-flowread').textContent = stats.preferences.flowread || 0;
document.getElementById('pref-gradient').textContent = stats.preferences.gradient || 0;
const totalSessions = (stats.plain.sample_size || 0) + (stats.flowread.sample_size || 0) + (stats.gradient.sample_size || 0);
document.getElementById('stat-sample-size').textContent = totalSessions;
document.getElementById('study-loading').style.display = 'none';
document.getElementById('study-results').style.display = 'block';
} catch (err) {
console.error(err);
alert("Failed to analyze text.");
isFetching = false;
} finally {
analyzeBtn.disabled = false;
loading.style.display = 'none';
}
}
</script>
</body>
</html>