| <!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; |
| max-width: 900px; |
| margin: 0 auto; |
| padding: 2rem; |
| line-height: 1.6; |
| background-color: #fdfbf7; |
| 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; |
| } |
| |
| |
| 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 { |
| transition: font-weight 0.2s; |
| } |
| |
| .highlighted { |
| font-weight: 800; |
| 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> |
|
|
| |
| <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"> |
| |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="study-tab" class="tab-content active"> |
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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;"> |
| |
| </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> |
|
|
| |
| <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;"> |
| |
| </div> |
| <button id="submit-answers-btn">Submit Answers</button> |
| </div> |
|
|
| |
| <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> |
|
|
| |
| <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;"> |
| |
| <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> |
| |
| <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> |
| |
| <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'); |
| } |
| } |
| |
| |
| |
| |
| 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 = ''; |
| |
| |
| 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 }); |
| } |
| |
| |
| words.forEach(wordObj => { |
| const isWordHighlighted = wordObj.maxScore >= threshold; |
| |
| wordObj.tokens.forEach(item => { |
| const span = document.createElement('span'); |
| span.className = 'token'; |
| |
| if (useGradient) { |
| |
| 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 { |
| |
| if (isWordHighlighted) span.classList.add('highlighted'); |
| } |
| |
| span.textContent = item.token; |
| resultContainer.appendChild(span); |
| }); |
| }); |
| } |
| |
| |
| |
| |
| 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; |
| |
| |
| 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 { |
| |
| const res = await fetch('/api/study/texts'); |
| const data = await res.json(); |
| studyTexts = data.texts; |
| |
| |
| conditionOrder = shuffle(['plain', 'flowread', 'gradient']); |
| |
| |
| 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; |
| |
| |
| 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'; |
| |
| readingStartTime = performance.now(); |
| } |
| |
| document.getElementById('done-reading-btn').addEventListener('click', () => { |
| |
| 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; |
| |
| |
| 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..."; |
| |
| |
| 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) { |
| |
| showReadingScreen(); |
| } else { |
| |
| document.getElementById('study-preference').style.display = 'block'; |
| } |
| }); |
| |
| |
| document.querySelectorAll('.pref-btn').forEach(btn => { |
| btn.addEventListener('click', async (e) => { |
| const preference = e.target.getAttribute('data-pref'); |
| |
| |
| 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> |