Includes all Phase 1 features: - Search-first navigation with auto-complete - Responsive reading interface (desktop/tablet/mobile) - 4 customization presets + full fine-tuning controls - Layered details panel with notes, bookmarks, highlights - Smart offline caching with IndexedDB and auto-sync - Full accessibility (WCAG 2.1 AA) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
20 KiB
20 KiB
Speed Reading Mode - Implementation Plan
📋 Overview
Implement a speed reading mode using RSVP (Rapid Serial Visual Presentation) technique, allowing users to consume Bible content at accelerated rates while maintaining comprehension through guided visual training.
Status: Planning Phase Priority: 🟡 Medium Estimated Time: 2 weeks (80 hours) Target Completion: TBD
🎯 Goals & Objectives
Primary Goals
- Enable users to read at 200-1000+ words per minute
- Reduce eye movement and increase focus
- Track reading speed progress over time
- Provide comprehension exercises
- Offer customizable display modes
User Value Proposition
- For busy professionals: Read more in less time
- For students: Cover more material quickly
- For speed reading enthusiasts: Practice technique
- For information seekers: Rapid content consumption
- For skill builders: Measurable improvement tracking
✨ Feature Specifications
1. RSVP Configuration
interface RSVPConfig {
// Speed
wordsPerMinute: number // 200-1000+
autoAdjust: boolean // Automatically adjust based on comprehension
// Display
displayMode: 'single' | 'dual' | 'triple' // Words shown at once
chunkSize: number // 1-3 words
fontSize: number // 16-48px
fontFamily: string
backgroundColor: string
textColor: string
highlightColor: string
// Timing
pauseOnPunctuation: boolean
pauseDuration: { comma: number; period: number; question: number } // ms
pauseBetweenVerses: number // ms
// Focus
showFixationPoint: boolean
fixationStyle: 'center' | 'orpAlgorithm' | 'custom'
showWordPosition: boolean // Current word out of total
showProgress: boolean
// Comprehension
enableQuizzes: boolean
quizFrequency: number // Every N verses
requirePassToContinue: boolean
}
2. RSVP Display Component
const RSVPReader: React.FC<{
content: string[]
config: RSVPConfig
onComplete: () => void
onPause: () => void
}> = ({ content, config, onComplete, onPause }) => {
const [isPlaying, setIsPlaying] = useState(false)
const [currentIndex, setCurrentIndex] = useState(0)
const [words, setWords] = useState<string[]>([])
useEffect(() => {
// Parse content into words
const allWords = content.join(' ').split(/\s+/)
setWords(allWords)
}, [content])
// Main playback logic
useEffect(() => {
if (!isPlaying || currentIndex >= words.length) return
const currentWord = words[currentIndex]
const delay = calculateDelay(currentWord, config)
const timer = setTimeout(() => {
setCurrentIndex(prev => prev + 1)
// Check if completed
if (currentIndex + 1 >= words.length) {
setIsPlaying(false)
onComplete()
}
}, delay)
return () => clearTimeout(timer)
}, [isPlaying, currentIndex, words, config])
const calculateDelay = (word: string, config: RSVPConfig): number => {
const baseDelay = (60 / config.wordsPerMinute) * 1000
// Adjust for punctuation
if (config.pauseOnPunctuation) {
if (word.endsWith(',')) return baseDelay + config.pauseDuration.comma
if (word.endsWith('.') || word.endsWith('!')) return baseDelay + config.pauseDuration.period
if (word.endsWith('?')) return baseDelay + config.pauseDuration.question
}
// Adjust for word length (longer words take slightly longer)
const lengthMultiplier = 1 + (Math.max(0, word.length - 6) * 0.02)
return baseDelay * lengthMultiplier
}
const getDisplayWords = (): string[] => {
if (config.displayMode === 'single') {
return [words[currentIndex]]
} else if (config.displayMode === 'dual') {
return [words[currentIndex], words[currentIndex + 1]].filter(Boolean)
} else {
return [words[currentIndex], words[currentIndex + 1], words[currentIndex + 2]].filter(Boolean)
}
}
const displayWords = getDisplayWords()
const progress = (currentIndex / words.length) * 100
return (
<Box className="rsvp-reader" sx={{
height: '100vh',
display: 'flex',
flexDirection: 'column',
bgcolor: config.backgroundColor
}}>
{/* Header - Controls */}
<Box sx={{ p: 2, borderBottom: 1, borderColor: 'divider' }}>
<SpeedReadingControls
isPlaying={isPlaying}
onPlay={() => setIsPlaying(true)}
onPause={() => {
setIsPlaying(false)
onPause()
}}
onRestart={() => setCurrentIndex(0)}
config={config}
/>
</Box>
{/* Main Display Area */}
<Box sx={{
flex: 1,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
position: 'relative'
}}>
{/* Fixation Point Guide */}
{config.showFixationPoint && (
<Box sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
zIndex: 0
}}>
<FixationGuide style={config.fixationStyle} />
</Box>
)}
{/* Word Display */}
<Box sx={{
fontSize: `${config.fontSize}px`,
fontFamily: config.fontFamily,
color: config.textColor,
textAlign: 'center',
minHeight: '100px',
display: 'flex',
alignItems: 'center',
gap: 2,
zIndex: 1
}}>
{displayWords.map((word, index) => {
const isActive = index === 0
const fixationIndex = calculateFixationPoint(word)
return (
<span
key={`${currentIndex}-${index}`}
style={{
fontWeight: isActive ? 700 : 400,
opacity: isActive ? 1 : 0.6,
transition: 'opacity 0.1s ease'
}}
>
{word.split('').map((char, charIndex) => (
<span
key={charIndex}
style={{
color: charIndex === fixationIndex && isActive
? config.highlightColor
: 'inherit',
fontWeight: charIndex === fixationIndex && isActive
? 800
: 'inherit'
}}
>
{char}
</span>
))}
</span>
)
})}
</Box>
{/* Word Position Indicator */}
{config.showWordPosition && (
<Typography variant="caption" color="text.secondary" sx={{ mt: 4 }}>
Word {currentIndex + 1} of {words.length}
</Typography>
)}
</Box>
{/* Footer - Progress */}
{config.showProgress && (
<Box sx={{ p: 2, borderTop: 1, borderColor: 'divider' }}>
<LinearProgress variant="determinate" value={progress} sx={{ mb: 1 }} />
<Box display="flex" justifyContent="space-between">
<Typography variant="caption">
{Math.round(progress)}% Complete
</Typography>
<Typography variant="caption">
{config.wordsPerMinute} WPM
</Typography>
</Box>
</Box>
)}
</Box>
)
}
// ORP (Optimal Recognition Point) Algorithm
const calculateFixationPoint = (word: string): number => {
const length = word.length
if (length <= 1) return 0
if (length <= 5) return 1
if (length <= 9) return 2
if (length <= 13) return 3
return Math.floor(length * 0.3)
}
3. Speed Reading Controls
const SpeedReadingControls: React.FC<{
isPlaying: boolean
onPlay: () => void
onPause: () => void
onRestart: () => void
config: RSVPConfig
}> = ({ isPlaying, onPlay, onPause, onRestart, config }) => {
const [showSettings, setShowSettings] = useState(false)
return (
<Box display="flex" gap={2} alignItems="center">
{/* Playback Controls */}
<ButtonGroup>
<IconButton onClick={onRestart} title="Restart">
<RestartAltIcon />
</IconButton>
<IconButton
onClick={isPlaying ? onPause : onPlay}
color="primary"
size="large"
>
{isPlaying ? <PauseIcon /> : <PlayArrowIcon />}
</IconButton>
</ButtonGroup>
{/* Speed Adjustment */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, minWidth: 200 }}>
<IconButton size="small" onClick={() => adjustSpeed(-25)}>
<RemoveIcon />
</IconButton>
<Box sx={{ flex: 1, textAlign: 'center' }}>
<Typography variant="body2" fontWeight="600">
{config.wordsPerMinute} WPM
</Typography>
<Slider
value={config.wordsPerMinute}
onChange={(_, value) => updateSpeed(value as number)}
min={100}
max={1000}
step={25}
size="small"
/>
</Box>
<IconButton size="small" onClick={() => adjustSpeed(25)}>
<AddIcon />
</IconButton>
</Box>
{/* Quick Speed Presets */}
<ButtonGroup size="small">
<Button onClick={() => updateSpeed(200)}>Slow</Button>
<Button onClick={() => updateSpeed(350)}>Normal</Button>
<Button onClick={() => updateSpeed(500)}>Fast</Button>
<Button onClick={() => updateSpeed(700)}>Very Fast</Button>
</ButtonGroup>
<Box sx={{ flex: 1 }} />
{/* Settings */}
<IconButton onClick={() => setShowSettings(true)}>
<SettingsIcon />
</IconButton>
{/* Settings Dialog */}
<RSVPSettingsDialog
open={showSettings}
onClose={() => setShowSettings(false)}
config={config}
/>
</Box>
)
}
4. Fixation Guide
const FixationGuide: React.FC<{ style: string }> = ({ style }) => {
if (style === 'center') {
return (
<Box sx={{
width: 2,
height: 60,
bgcolor: 'primary.main',
opacity: 0.3
}} />
)
}
if (style === 'orpAlgorithm') {
return (
<Box sx={{ display: 'flex', gap: '2px' }}>
<Box sx={{ width: 1, height: 40, bgcolor: 'grey.400', opacity: 0.2 }} />
<Box sx={{ width: 1, height: 50, bgcolor: 'grey.400', opacity: 0.2 }} />
<Box sx={{ width: 2, height: 60, bgcolor: 'primary.main', opacity: 0.4 }} />
<Box sx={{ width: 1, height: 50, bgcolor: 'grey.400', opacity: 0.2 }} />
<Box sx={{ width: 1, height: 40, bgcolor: 'grey.400', opacity: 0.2 }} />
</Box>
)
}
return null
}
5. Comprehension Quiz
interface ComprehensionQuiz {
id: string
verseReference: string
question: string
options: string[]
correctAnswer: number
explanation?: string
}
const ComprehensionQuiz: React.FC<{
quiz: ComprehensionQuiz
onAnswer: (correct: boolean) => void
}> = ({ quiz, onAnswer }) => {
const [selectedAnswer, setSelectedAnswer] = useState<number | null>(null)
const [showResult, setShowResult] = useState(false)
const handleSubmit = () => {
const isCorrect = selectedAnswer === quiz.correctAnswer
setShowResult(true)
setTimeout(() => {
onAnswer(isCorrect)
}, 2000)
}
return (
<Dialog open maxWidth="sm" fullWidth>
<DialogTitle>Comprehension Check</DialogTitle>
<DialogContent>
<Typography variant="body2" color="text.secondary" gutterBottom>
{quiz.verseReference}
</Typography>
<Typography variant="h6" sx={{ mb: 3 }}>
{quiz.question}
</Typography>
<RadioGroup value={selectedAnswer} onChange={(e) => setSelectedAnswer(Number(e.target.value))}>
{quiz.options.map((option, index) => (
<FormControlLabel
key={index}
value={index}
control={<Radio />}
label={option}
disabled={showResult}
sx={{
p: 1,
borderRadius: 1,
bgcolor: showResult
? index === quiz.correctAnswer
? 'success.light'
: index === selectedAnswer
? 'error.light'
: 'transparent'
: 'transparent'
}}
/>
))}
</RadioGroup>
{showResult && quiz.explanation && (
<Alert severity={selectedAnswer === quiz.correctAnswer ? 'success' : 'info'} sx={{ mt: 2 }}>
{quiz.explanation}
</Alert>
)}
</DialogContent>
<DialogActions>
<Button
onClick={handleSubmit}
disabled={selectedAnswer === null || showResult}
variant="contained"
>
Submit Answer
</Button>
</DialogActions>
</Dialog>
)
}
6. Progress Tracking
interface ReadingSession {
id: string
userId: string
startTime: Date
endTime: Date
wordsRead: number
averageWPM: number
peakWPM: number
comprehensionScore: number // 0-100%
book: string
chapter: number
}
const ProgressTracker: React.FC = () => {
const [sessions, setSessions] = useState<ReadingSession[]>([])
const [stats, setStats] = useState<any>(null)
useEffect(() => {
loadSessions()
loadStats()
}, [])
return (
<Box sx={{ p: 3 }}>
<Typography variant="h5" gutterBottom>
Speed Reading Progress
</Typography>
{/* Summary Stats */}
<Grid container spacing={2} sx={{ mb: 4 }}>
<Grid item xs={6} sm={3}>
<StatCard
title="Current Speed"
value={`${stats?.currentWPM || 0} WPM`}
icon={<SpeedIcon />}
/>
</Grid>
<Grid item xs={6} sm={3}>
<StatCard
title="Improvement"
value={`+${stats?.improvement || 0}%`}
icon={<TrendingUpIcon />}
/>
</Grid>
<Grid item xs={6} sm={3}>
<StatCard
title="Total Words"
value={formatNumber(stats?.totalWords || 0)}
icon={<MenuBookIcon />}
/>
</Grid>
<Grid item xs={6} sm={3}>
<StatCard
title="Avg Comprehension"
value={`${stats?.avgComprehension || 0}%`}
icon={<CheckCircleIcon />}
/>
</Grid>
</Grid>
{/* Progress Chart */}
<Paper sx={{ p: 2, mb: 3 }}>
<Typography variant="h6" gutterBottom>
Reading Speed Over Time
</Typography>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={sessions}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis />
<Tooltip />
<Legend />
<Line type="monotone" dataKey="averageWPM" stroke="#8884d8" name="Average WPM" />
<Line type="monotone" dataKey="peakWPM" stroke="#82ca9d" name="Peak WPM" />
</LineChart>
</ResponsiveContainer>
</Paper>
{/* Session History */}
<Paper sx={{ p: 2 }}>
<Typography variant="h6" gutterBottom>
Recent Sessions
</Typography>
<TableContainer>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Date</TableCell>
<TableCell>Passage</TableCell>
<TableCell>Words</TableCell>
<TableCell>Avg WPM</TableCell>
<TableCell>Comprehension</TableCell>
</TableRow>
</TableHead>
<TableBody>
{sessions.map(session => (
<TableRow key={session.id}>
<TableCell>{formatDate(session.startTime)}</TableCell>
<TableCell>{session.book} {session.chapter}</TableCell>
<TableCell>{session.wordsRead}</TableCell>
<TableCell>{session.averageWPM}</TableCell>
<TableCell>
<Chip
label={`${session.comprehensionScore}%`}
color={session.comprehensionScore >= 80 ? 'success' : 'warning'}
size="small"
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Paper>
</Box>
)
}
7. Training Exercises
const SpeedReadingTraining: React.FC = () => {
const [currentExercise, setCurrentExercise] = useState(0)
const exercises = [
{
name: 'Word Recognition',
description: 'Practice recognizing words at increasing speeds',
component: <WordRecognitionExercise />
},
{
name: 'Peripheral Vision',
description: 'Expand your field of vision',
component: <PeripheralVisionExercise />
},
{
name: 'Chunking Practice',
description: 'Read multiple words at once',
component: <ChunkingExercise />
},
{
name: 'Speed Progression',
description: 'Gradually increase reading speed',
component: <ProgressionExercise />
}
]
return (
<Box sx={{ p: 3 }}>
<Typography variant="h5" gutterBottom>
Speed Reading Training
</Typography>
<Stepper activeStep={currentExercise} sx={{ mb: 4 }}>
{exercises.map((exercise, index) => (
<Step key={exercise.name}>
<StepLabel>{exercise.name}</StepLabel>
</Step>
))}
</Stepper>
<Paper sx={{ p: 3 }}>
{exercises[currentExercise].component}
</Paper>
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'space-between' }}>
<Button
disabled={currentExercise === 0}
onClick={() => setCurrentExercise(prev => prev - 1)}
>
Previous
</Button>
<Button
variant="contained"
onClick={() => setCurrentExercise(prev => Math.min(prev + 1, exercises.length - 1))}
>
{currentExercise === exercises.length - 1 ? 'Finish' : 'Next'}
</Button>
</Box>
</Box>
)
}
🗄️ Database Schema
model SpeedReadingSession {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id])
startTime DateTime
endTime DateTime
wordsRead Int
averageWPM Int
peakWPM Int
lowestWPM Int
book String
chapter Int
startVerse Int
endVerse Int
comprehensionScore Float? // 0-100
quizzesTaken Int @default(0)
quizzesCorrect Int @default(0)
config Json // RSVPConfig snapshot
createdAt DateTime @default(now())
@@index([userId, createdAt])
}
model SpeedReadingStats {
id String @id @default(cuid())
userId String @unique
user User @relation(fields: [userId], references: [id])
totalSessions Int @default(0)
totalWords BigInt @default(0)
totalMinutes Int @default(0)
currentWPM Int @default(200)
startingWPM Int @default(200)
peakWPM Int @default(200)
avgComprehension Float @default(0)
lastSessionAt DateTime?
updatedAt DateTime @updatedAt
}
📅 Implementation Timeline
Week 1: Core RSVP
Day 1-2: Foundation
- RSVP display component
- Word timing logic
- Basic controls
Day 3-4: Features
- Fixation point
- Speed adjustment
- Multiple display modes
Day 5: Testing
- Performance optimization
- User testing
- Bug fixes
Week 2: Advanced
Day 1-2: Comprehension
- Quiz system
- Auto-adjustment
- Results tracking
Day 3-4: Analytics
- Progress tracking
- Statistics dashboard
- Training exercises
Day 5: Launch
- Final polish
- Documentation
- Deployment
Document Version: 1.0 Last Updated: 2025-10-13 Status: Ready for Implementation