Files
biblical-guide.com/SPEED_READING_MODE_PLAN.md
Andrei 9b5c0ed8bb 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>
2025-11-11 20:38:01 +00:00

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

  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

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