AccelaReader
Paste your text and train with RSVP. (URL & AI are premium)
Words: 0
RSVP Reader
Use ↑/↓ or +/– (desktop).
0 / 0 words • 0%
/* --- AccelaReader containment & spill guards --- */
#accela-reader { max-width: 980px; margin: 0 auto; overflow: visible; }
#accela-reader .ar-card { overflow: hidden; }
/* Kill auto
/ spill directly after the app */
#accela-reader + pre,
#accela-reader + code,
#accela-reader + .wpb_text_column pre,
#accela-reader + .wpb_text_column code {
display: none !important;
}
/* Neutralize theme wpautop spacing inside our wrapper */
#accela-reader p:empty { display:none; }
#accela-reader p > code,
#accela-reader p > pre { white-space: normal; }
/* Prevent long words/URLs from blowing layout */
#accela-reader, #accela-reader * { word-wrap: break-word; overflow-wrap: anywhere; }
/* AccelaReader — MVP with Premium Locks */
(function(){
var tries=0; function ready(){ var root=document.getElementById('accela-reader'); if(root) init(root); else if(tries++<200) setTimeout(ready,50) }
(document.readyState==='loading')?document.addEventListener('DOMContentLoaded',ready):ready();
function init(root){
var $ = s => root.querySelector(s), $$ = s => Array.from(root.querySelectorAll(s));
// Elements
var el = {
tabPaste:$('#tabPaste'), tabUrl:$('#tabUrl'), tabAI:$('#tabAI'),
panePaste:$('#panePaste'), paneUrl:$('#paneUrl'), paneAI:$('#paneAI'),
paste:$('#arPaste'), stats:$('#arStats'), sample:$('#arSample'), useText:$('#arUseText'),
wpm:$('#arWpm'), wpmNum:$('#arWpmNum'), chunk:$('#arChunk'),
pauseStop:$('#pauseStop'), pauseComma:$('#pauseComma'), pauseDash:$('#pauseDash'),
prog:$('#arProg'), hudText:$('#arHudText'), chunkView:$('#arChunkView'),
prev:$('#arPrev'), next:$('#arNext'), start:$('#arStart'), pause:$('#arPause'), restart:$('#arRestart'),
wpmDown:$('#arWpmDown'), wpmUp:$('#arWpmUp'),
themeBtn:$('#arTheme'),
backToPasteUrl:$('#backToPasteUrl'), backToPasteAI:$('#backToPasteAI'),
ctaPremiumUrl:$('#ctaPremiumUrl'), ctaPremiumAI:$('#ctaPremiumAI')
};
// State
var state = {
words:[], i:0, running:false, timer:null,
wpm: parseInt(localStorage.getItem('ar_wpm')||'350',10),
chunk: parseInt(localStorage.getItem('ar_chunk')||'2',10),
pauses:{ stop:true, comma:true, dash:true }
};
// Utils
function clamp(n,a,b){ return Math.max(a,Math.min(b,n)) }
function esc(s){ return s.replace(/[&<>\"']/g,m=>({'&':'&','<':'>','>':'>','"':'"'}[m]||m)).replace(/'/g,''') }
function tokenize(txt){ return (txt||'').trim().replace(/\s+/g,' ').split(' ').filter(Boolean) }
// Tabs
function setTab(name){
// Only allow 'paste' in MVP
if(name!=='paste'){ name='paste'; }
el.tabPaste.classList.toggle('active',name==='paste');
el.tabUrl.classList.toggle('active',false);
el.tabAI.classList.toggle('active',false);
el.panePaste.hidden = (name!=='paste');
el.paneUrl.hidden = true;
el.paneAI.hidden = true;
}
el.tabPaste.addEventListener('click',()=>setTab('paste'));
// Premium-locked tabs: show a gentle nudge
function lockedToast(msg){ alert(msg || 'This is a premium feature. Paste text to start reading now.'); }
el.tabUrl.addEventListener('click',()=>lockedToast('URL import is premium. Paste your text to begin.'));
el.tabAI.addEventListener('click',()=>lockedToast('AI generation is premium. Paste your text to begin.'));
// Locked pane "Back to Paste"
el.backToPasteUrl?.addEventListener('click',()=>setTab('paste'));
el.backToPasteAI?.addEventListener('click',()=>setTab('paste'));
// Locked CTAs: wire to your pricing or signup page if you have one
el.ctaPremiumUrl?.addEventListener('click', (e)=>{ e.preventDefault(); lockedToast('To unlock, contact IRIS Reading.'); });
el.ctaPremiumAI?.addEventListener('click', (e)=>{ e.preventDefault(); lockedToast('To unlock, contact IRIS Reading.'); });
// Paste stats
function updateStats(){ el.stats.textContent = 'Words: ' + tokenize(el.paste.value||'').length }
var SAMPLE='Speed reading is less about rushing and more about directing attention. In an RSVP reader, words or small phrases appear at the same spot on the screen, reducing eye movements. Start near three hundred words per minute, with chunk size of two. Use punctuation pauses like breath marks—slightly longer at periods and shorter at commas. As accuracy holds, nudge speed upward.';
el.sample.addEventListener('click',()=>{ el.paste.value=SAMPLE; updateStats(); });
el.paste.addEventListener('input',updateStats); updateStats();
// Use in Reader
function useText(txt){
var t=(txt||'').trim();
if(!t){ alert('No text found.'); return; }
state.words = tokenize(t); state.i=0;
el.chunkView.textContent='Ready?'; renderHUD();
localStorage.setItem('ar_last_text', t.slice(0,20000));
}
el.useText.addEventListener('click',()=>useText(el.paste.value));
// Controls — WPM & Chunk
function syncWpm(fromSlider){
var v = fromSlider ? parseInt(el.wpm.value,10) : parseInt(el.wpmNum.value,10);
if(!Number.isFinite(v)) v=350;
v = clamp(v,100, (fromSlider?1200:2000));
el.wpm.value = clamp(v,100,1200);
el.wpmNum.value = v;
state.wpm = v;
localStorage.setItem('ar_wpm', String(v));
}
el.wpm.addEventListener('input',()=>syncWpm(true));
el.wpmNum.addEventListener('input',()=>syncWpm(false));
el.chunk.value = String(state.chunk);
el.wpm.value = clamp(state.wpm,100,1200);
el.wpmNum.value = state.wpm;
el.chunk.addEventListener('change',()=>{
state.chunk = clamp(parseInt(el.chunk.value,10)||2,1,5);
localStorage.setItem('ar_chunk', String(state.chunk));
});
// Pauses
function pStop(){ return state.pauses.stop?1.6:1 }
function pComma(){ return state.pauses.comma?1.25:1 }
function pDash(){ return state.pauses.dash?1.15:1 }
el.pauseStop.addEventListener('change',()=>state.pauses.stop=el.pauseStop.checked);
el.pauseComma.addEventListener('change',()=>state.pauses.comma=el.pauseComma.checked);
el.pauseDash.addEventListener('change',()=>state.pauses.dash=el.pauseDash.checked);
// Reader core
function nextDelayFor(chunkText){
var last = chunkText.slice(-1);
var mult = 1;
if(/[.!?]/.test(last)) mult = pStop();
else if(/[,:;]/.test(last)) mult = pComma();
else if(/[–—-]/.test(last)) mult = pDash();
var words = chunkText.trim().split(/\s+/).filter(Boolean).length || 1;
var base = (words / state.wpm) * 60000;
return Math.max(30, base * mult);
}
function showChunk(){
if(state.i >= state.words.length){ el.chunkView.textContent='Finished ✓'; stop(); return; }
var c = state.words.slice(state.i, state.i + state.chunk).join(' ');
state.i += state.chunk;
el.chunkView.innerHTML = esc(c);
renderHUD();
return nextDelayFor(c);
}
function tick(){
if(!state.running) return;
var wait = showChunk();
if(wait==null) return;
state.timer = setTimeout(tick, wait);
}
function start(){
if(!state.words.length){
var last = localStorage.getItem('ar_last_text') || '';
if(last){ useText(last); } else { alert('Paste your text first.'); return; }
}
if(state.running) return;
state.running = true; tick();
}
function stop(){ state.running=false; if(state.timer){ clearTimeout(state.timer); state.timer=null; } }
function pause(){ stop(); }
function restart(){ stop(); state.i=0; el.chunkView.textContent='Ready?'; renderHUD(); }
// HUD
function renderHUD(){
var total = state.words.length, done = Math.min(total, state.i);
var pct = total? Math.round((done/total)*100):0;
el.hudText.textContent = done+' / '+total+' words • '+pct+'%';
el.prog.style.width = pct+'%';
}
// Buttons
el.start.addEventListener('click', start);
el.pause.addEventListener('click', pause);
el.restart.addEventListener('click', restart);
el.prev.addEventListener('click', ()=>{ pause(); state.i = Math.max(0, state.i - state.chunk*2); showChunk(); });
el.next.addEventListener('click', ()=>{ pause(); showChunk(); });
el.wpmDown.addEventListener('click', ()=>{ el.wpmNum.value = clamp(parseInt(el.wpmNum.value,10)-25,100,2000); syncWpm(false); });
el.wpmUp.addEventListener('click', ()=>{ el.wpmNum.value = clamp(parseInt(el.wpmNum.value,10)+25,100,2000); syncWpm(false); });
// Keyboard controls (desktop)
window.addEventListener('keydown', function(e){
const k=e.key;
if([' ','ArrowLeft','ArrowRight','ArrowUp','ArrowDown','+','-','=','[',']'].includes(k)){ e.preventDefault(); }
if(k===' ') (state.running?pause():start());
if(k==='ArrowLeft') el.prev.click();
if(k==='ArrowRight') el.next.click();
if(k==='ArrowUp' || k==='+') el.wpmUp.click();
if(k==='ArrowDown' || k==='-') el.wpmDown.click();
if(k==='[') { state.chunk = clamp(state.chunk-1,1,5); el.chunk.value=String(state.chunk); }
if(k===']') { state.chunk = clamp(state.chunk+1,1,5); el.chunk.value=String(state.chunk); }
}, {passive:false});
// Theme toggle
var theme = localStorage.getItem('ar_theme') || 'dark';
function applyTheme(){ root.classList.toggle('light', theme==='light'); el.themeBtn.textContent = (theme==='light')?'☀️':'🌙'; localStorage.setItem('ar_theme', theme); }
el.themeBtn.addEventListener('click',()=>{ theme = (theme==='light')?'dark':'light'; applyTheme(); });
applyTheme();
// Preload last text if present
var last = localStorage.getItem('ar_last_text'); if(last){ el.paste.value = last; updateStats(); }
}
})();
// --- Mount sanity check ---
(function(){
var root = document.getElementById('accela-reader');
if(!root){ console.warn('[AccelaReader] #accela-reader not found. Place HTML block above JS.'); return; }
var hasCSS = !!document.getElementById('accela-reader-css');
if(!hasCSS) console.warn('[AccelaReader] CSS style tag missing. Place CSS block (Raw HTML) above JS.');
})();