build: production build with Phase 1 2025 Bible Reader implementation complete
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>
This commit is contained in:
733
SPEED_READING_MODE_PLAN.md
Normal file
733
SPEED_READING_MODE_PLAN.md
Normal file
@@ -0,0 +1,733 @@
|
||||
# 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
|
||||
1. Enable users to read at 200-1000+ words per minute
|
||||
2. Reduce eye movement and increase focus
|
||||
3. Track reading speed progress over time
|
||||
4. Provide comprehension exercises
|
||||
5. 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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```prisma
|
||||
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
|
||||
Reference in New Issue
Block a user