summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpolwex <polwex@sortug.com>2025-05-29 14:08:02 +0700
committerpolwex <polwex@sortug.com>2025-05-29 14:08:02 +0700
commitf243847216279cbd43879de8b5ef6dcceb3a2f1d (patch)
tree1e0be878f164d327762c7bc54f37077d9410dafe
parent4ed3994fb0f6a2a09eb6ac433a62daee2fc01686 (diff)
lets see
-rw-r--r--CLAUDE.md90
-rw-r--r--src/actions/srs.ts105
-rw-r--r--src/components/Flashcard/StudyCard.tsx265
-rw-r--r--src/components/Flashcard/StudySession.tsx229
-rw-r--r--src/components/Flashcard/cards.css55
-rw-r--r--src/lib/services/srs_study.ts552
-rw-r--r--src/pages/lessons.tsx126
-rw-r--r--src/pages/study.tsx128
8 files changed, 1550 insertions, 0 deletions
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..2b82961
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,90 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Project Overview
+
+This is a language learning application built with React, Waku, and Bun. It focuses on Thai language learning with features for:
+
+- Flashcard-based study using Spaced Repetition System (SRS)
+- Detailed phonetic analysis and syllable breakdown
+- Tone pattern visualization and practice
+- User progress tracking
+- Lessons and organized learning content
+
+## Commands
+
+### Development
+
+```bash
+# Start the development server
+bun run dev
+
+# Build the project
+bun run build
+
+# Start the production server
+bun run start
+```
+
+## Architecture
+
+### Framework and Runtime
+
+- **Waku**: Server-side rendering (SSR) framework
+- **React**: UI framework (v19.1)
+- **Bun**: JavaScript runtime
+- **TypeScript**: Type system
+
+### Database
+
+The project uses SQLite databases (managed in `src/lib/db/`) with schemas for:
+
+- User accounts and progress tracking
+- Language expressions and translations
+- Syllable breakdowns and phonetic information
+- Flashcards and lessons
+- Spaced repetition data
+
+### Key Components
+
+1. **Server Actions** (`src/actions/`)
+ - Server-side functions marked with `"use server";`
+ - Handle database operations and server-side logic
+ - Examples: authentication, database operations, language processing
+
+2. **Client Components** (`src/components/`)
+ - React components marked with `"use client";`
+ - Import and call server actions
+ - Manage UI state and user interactions
+
+3. **SRS System** (`src/lib/services/srs*.ts`)
+ - Implements spaced repetition algorithms
+ - Tracks user progress and schedules reviews
+ - Multiple implementations for different tracking approaches
+
+4. **Language Processing** (`src/lib/lang/`, `src/zoom/`)
+ - Thai language syllable parsing and phonetic analysis
+ - Tone pattern visualization and analysis
+ - Text parsing and hierarchical display
+
+5. **Database Handlers** (`src/lib/db/`)
+ - Connection management
+ - CRUD operations for all entities
+ - Query functions for app features
+
+### Data Flow
+
+1. Server components fetch initial data
+2. Client components render UI and handle user interactions
+3. User actions trigger server actions
+4. Server actions update the database
+5. UI updates with new data from server actions
+
+### Key Files
+
+- `waku.config.ts`: Waku framework configuration
+- `src/lib/db/schema.sql`: Main database schema
+- `src/lib/db/prosodyschema.sql`: Phonetic analysis schema
+- `src/lib/services/srs_streamlined.ts`: Main SRS implementation
+- `src/actions/`: Server actions for various features \ No newline at end of file
diff --git a/src/actions/srs.ts b/src/actions/srs.ts
new file mode 100644
index 0000000..47e1655
--- /dev/null
+++ b/src/actions/srs.ts
@@ -0,0 +1,105 @@
+"use server";
+
+import db from "@/lib/db";
+import { SRSStudyService, ReviewResult } from "@/lib/services/srs_study";
+
+// Create an instance of the SRS service
+const srsService = new SRSStudyService(db);
+
+/**
+ * Start a study session for a lesson
+ * @param userId User ID
+ * @param lessonId Lesson ID
+ * @param random Whether to randomize card order
+ * @returns Cards due for review or new cards
+ */
+export async function startStudySession(userId: number, lessonId: number, random: boolean = true) {
+ return srsService.startStudySession(userId, lessonId, random);
+}
+
+/**
+ * Process a review result and update SRS parameters
+ * @param userId User ID
+ * @param cardId Card ID
+ * @param accuracy Recall accuracy (0-1)
+ * @param reviewTime Time taken to review in milliseconds
+ * @returns Updated card data
+ */
+export async function processReview(
+ userId: number,
+ cardId: number,
+ accuracy: number,
+ reviewTime: number = 5000
+) {
+ const reviewResult: ReviewResult = {
+ cardId,
+ accuracy,
+ reviewTime
+ };
+
+ return srsService.processReview(userId, reviewResult);
+}
+
+/**
+ * Reset progress for a specific card
+ * @param userId User ID
+ * @param cardId Card ID
+ */
+export async function resetCardProgress(userId: number, cardId: number) {
+ srsService.resetProgress(userId, cardId);
+ return { ok: "Progress reset successfully" };
+}
+
+/**
+ * Get user study statistics
+ * @param userId User ID
+ * @returns Study statistics
+ */
+export async function getUserStudyStats(userId: number) {
+ return srsService.getUserStats(userId);
+}
+
+/**
+ * Get lesson progress statistics
+ * @param userId User ID
+ * @param lessonId Lesson ID
+ * @returns Lesson progress statistics
+ */
+export async function getLessonProgress(userId: number, lessonId: number) {
+ return srsService.getLessonProgress(userId, lessonId);
+}
+
+/**
+ * Get all lessons with progress information for a user
+ * @param userId User ID
+ * @returns Array of lessons with progress information
+ */
+export async function getUserLessons(userId: number) {
+ return srsService.getUserLessons(userId);
+}
+
+/**
+ * Get new cards for a lesson that haven't been studied yet
+ * @param userId User ID
+ * @param lessonId Lesson ID
+ * @param random Whether to randomize card order
+ * @returns New cards for the lesson
+ */
+export async function getNewCards(userId: number, lessonId: number, random: boolean = true) {
+ return srsService.fetchNewCards(userId, lessonId, random);
+}
+
+/**
+ * Create a simplified response object from review result
+ * Used for grading cards with Good/Again responses
+ * @param userId User ID
+ * @param cardId Card ID
+ * @param isCorrect Whether the answer was correct
+ * @returns Updated card data
+ */
+export async function gradeCard(userId: number, cardId: number, isCorrect: boolean) {
+ // Convert boolean to accuracy (0.2 for wrong, 1.0 for correct)
+ const accuracy = isCorrect ? 1.0 : 0.2;
+
+ return processReview(userId, cardId, accuracy);
+} \ No newline at end of file
diff --git a/src/components/Flashcard/StudyCard.tsx b/src/components/Flashcard/StudyCard.tsx
new file mode 100644
index 0000000..4e554b4
--- /dev/null
+++ b/src/components/Flashcard/StudyCard.tsx
@@ -0,0 +1,265 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { CardResponse } from "@/lib/types/cards";
+import { cn } from "@/lib/utils";
+import { Button } from "@/components/ui/button";
+import { Card } from "@/components/ui/card";
+import { Progress } from "@/components/ui/progress";
+import { processReview, gradeCard } from "@/actions/srs";
+import "./cards.css";
+
+interface StudyCardProps {
+ card: CardResponse;
+ userId: number;
+ onComplete: (newCard: CardResponse) => void;
+ onSkip?: () => void;
+}
+
+export default function StudyCard({ card, userId, onComplete, onSkip }: StudyCardProps) {
+ const [isFlipped, setIsFlipped] = useState(false);
+ const [startTime, setStartTime] = useState(0);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ // Reset the timer when a new card is shown
+ useEffect(() => {
+ setIsFlipped(false);
+ setStartTime(Date.now());
+ }, [card.id]);
+
+ // Toggle card flip
+ const flipCard = () => {
+ if (!isFlipped) {
+ setIsFlipped(true);
+ }
+ };
+
+ // Calculate time spent on card in milliseconds
+ const getReviewTime = () => {
+ return Date.now() - startTime;
+ };
+
+ // Handle card grading (Good/Again)
+ const handleGrade = async (isCorrect: boolean) => {
+ if (isSubmitting) return;
+
+ setIsSubmitting(true);
+
+ try {
+ const result = await gradeCard(userId, card.id, isCorrect);
+
+ if ('error' in result) {
+ console.error("Error grading card:", result.error);
+ } else {
+ onComplete(result as CardResponse);
+ }
+ } catch (error) {
+ console.error("Error processing review:", error);
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ // Handle detailed grading with accuracy level
+ const handleDetailedGrade = async (accuracy: number) => {
+ if (isSubmitting) return;
+
+ setIsSubmitting(true);
+
+ try {
+ const reviewTime = getReviewTime();
+ const result = await processReview(userId, card.id, accuracy, reviewTime);
+
+ if ('error' in result) {
+ console.error("Error processing review:", result.error);
+ } else {
+ onComplete(result as CardResponse);
+ }
+ } catch (error) {
+ console.error("Error processing review:", error);
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ // Calculate progress percentage for the card
+ const getProgressPercentage = () => {
+ const { interval, easeFactor } = card.progress;
+ // Assuming max interval is 365 days and max ease factor is 4.0
+ const intervalProgress = Math.min(interval / 365, 1) * 70; // 70% weight to interval
+ const easeProgress = Math.min((easeFactor - 1) / 3, 1) * 30; // 30% weight to ease factor
+ return intervalProgress + easeProgress;
+ };
+
+ // Format content based on card type
+ const formatCardContent = (content: string, isBack: boolean = false) => {
+ // You can add more sophisticated formatting here based on card type
+ return content;
+ };
+
+ // Render IPA pronunciation if available
+ const renderIPA = () => {
+ if (card.expression.ipa && card.expression.ipa.length > 0) {
+ return (
+ <div className="text-gray-500 text-sm mt-2">
+ /{card.expression.ipa[0].ipa}/
+ </div>
+ );
+ }
+ return null;
+ };
+
+ // Render senses/meanings if available
+ const renderSenses = () => {
+ if (card.expression.senses && card.expression.senses.length > 0) {
+ return (
+ <div className="mt-4">
+ {card.expression.senses.map((sense, index) => (
+ <div key={index} className="mb-3">
+ {sense.pos && <span className="text-xs font-medium text-blue-600 mr-2">{sense.pos}</span>}
+ {sense.senses && sense.senses.map((subsense, i) => (
+ <div key={i} className="mt-1">
+ {subsense.glosses && subsense.glosses.map((gloss, j) => (
+ <div key={j} className="text-sm">{j+1}. {gloss}</div>
+ ))}
+ </div>
+ ))}
+ </div>
+ ))}
+ </div>
+ );
+ }
+ return null;
+ };
+
+ // Show bookmarked status if applicable
+ const renderBookmarked = () => {
+ if (card.expression.isBookmarked) {
+ return <div className="absolute top-2 right-2 text-yellow-500">★</div>;
+ }
+ return null;
+ };
+
+ return (
+ <div className="flex flex-col items-center">
+ <div className={cn("flashcard-container", { flipped: isFlipped })} onClick={flipCard}>
+ <div className="flashcard">
+ {/* Front of card */}
+ <div className="flashcard-front">
+ <Card className="w-full h-full flex flex-col justify-center items-center p-6 relative">
+ {renderBookmarked()}
+ <div className="text-2xl font-bold">{card.expression.spelling}</div>
+ {!isFlipped && renderIPA()}
+ <div className="mt-4 text-lg">{formatCardContent(card.text)}</div>
+ {card.note && <div className="mt-2 text-sm text-gray-500">{card.note}</div>}
+ {!isFlipped && (
+ <div className="mt-6 text-sm text-gray-400">
+ Click to flip
+ </div>
+ )}
+ </Card>
+ </div>
+
+ {/* Back of card */}
+ <div className="flashcard-back">
+ <Card className="w-full h-full flex flex-col justify-between p-6 relative">
+ {renderBookmarked()}
+ <div>
+ <div className="text-2xl font-bold">{card.expression.spelling}</div>
+ {renderIPA()}
+ <div className="mt-4 text-lg">{formatCardContent(card.text, true)}</div>
+ {card.note && <div className="mt-2 text-sm text-gray-500">{card.note}</div>}
+ {renderSenses()}
+ </div>
+
+ <div className="flex flex-col mt-6">
+ <div className="text-sm text-gray-500 mb-2">
+ How well did you remember this?
+ </div>
+ <div className="flex justify-between gap-2">
+ <Button
+ variant="destructive"
+ onClick={() => handleGrade(false)}
+ disabled={isSubmitting}
+ className="flex-1"
+ >
+ Again
+ </Button>
+ <Button
+ variant="default"
+ onClick={() => handleGrade(true)}
+ disabled={isSubmitting}
+ className="flex-1"
+ >
+ Good
+ </Button>
+ </div>
+
+ {/* Optional: Detailed grading */}
+ <div className="grid grid-cols-4 gap-2 mt-3">
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => handleDetailedGrade(0.2)}
+ disabled={isSubmitting}
+ className="text-red-500"
+ >
+ Forgot
+ </Button>
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => handleDetailedGrade(0.6)}
+ disabled={isSubmitting}
+ className="text-orange-500"
+ >
+ Hard
+ </Button>
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => handleDetailedGrade(0.8)}
+ disabled={isSubmitting}
+ className="text-green-500"
+ >
+ Good
+ </Button>
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => handleDetailedGrade(1.0)}
+ disabled={isSubmitting}
+ className="text-blue-500"
+ >
+ Easy
+ </Button>
+ </div>
+ </div>
+ </Card>
+ </div>
+ </div>
+ </div>
+
+ {/* Progress bar */}
+ <div className="w-full mt-4">
+ <Progress value={getProgressPercentage()} className="h-2" />
+ <div className="flex justify-between text-xs text-gray-500 mt-1">
+ <span>Interval: {card.progress.interval} days</span>
+ <span>Ease: {card.progress.easeFactor.toFixed(1)}</span>
+ </div>
+ </div>
+
+ {/* Skip button */}
+ {onSkip && (
+ <Button
+ variant="ghost"
+ onClick={onSkip}
+ className="mt-4"
+ disabled={isSubmitting}
+ >
+ Skip
+ </Button>
+ )}
+ </div>
+ );
+} \ No newline at end of file
diff --git a/src/components/Flashcard/StudySession.tsx b/src/components/Flashcard/StudySession.tsx
new file mode 100644
index 0000000..1f79e09
--- /dev/null
+++ b/src/components/Flashcard/StudySession.tsx
@@ -0,0 +1,229 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { DeckResponse, CardResponse } from "@/lib/types/cards";
+import { startStudySession, getUserStudyStats } from "@/actions/srs";
+import StudyCard from "./StudyCard";
+import { Button } from "@/components/ui/button";
+import { Card } from "@/components/ui/card";
+import { Progress } from "@/components/ui/progress";
+import { Skeleton } from "@/components/ui/skeleton";
+import { cn } from "@/lib/utils";
+
+interface StudySessionProps {
+ userId: number;
+ lessonId: number;
+ initialData?: DeckResponse;
+}
+
+export default function StudySession({ userId, lessonId, initialData }: StudySessionProps) {
+ const [deckData, setDeckData] = useState<DeckResponse | null>(initialData || null);
+ const [currentCardIndex, setCurrentCardIndex] = useState(0);
+ const [reviewedCards, setReviewedCards] = useState<CardResponse[]>([]);
+ const [isLoading, setIsLoading] = useState(!initialData);
+ const [isCompleted, setIsCompleted] = useState(false);
+ const [stats, setStats] = useState<any>(null);
+ const [error, setError] = useState<string | null>(null);
+
+ // Load the deck data if not provided
+ useEffect(() => {
+ if (!initialData) {
+ loadDeck();
+ }
+
+ // Load user stats
+ loadStats();
+ }, []);
+
+ // Load deck data
+ const loadDeck = async () => {
+ setIsLoading(true);
+ setError(null);
+
+ try {
+ const result = await startStudySession(userId, lessonId, true);
+
+ if ('error' in result) {
+ setError(result.error);
+ setDeckData(null);
+ } else {
+ setDeckData(result);
+ }
+ } catch (error) {
+ console.error("Error loading deck:", error);
+ setError("Failed to load study session. Please try again later.");
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ // Load user stats
+ const loadStats = async () => {
+ try {
+ const userStats = await getUserStudyStats(userId);
+ setStats(userStats);
+ } catch (error) {
+ console.error("Error loading stats:", error);
+ }
+ };
+
+ // Handle card completion
+ const handleCardComplete = (updatedCard: CardResponse) => {
+ // Add to reviewed cards
+ setReviewedCards(prev => [...prev, updatedCard]);
+
+ // Move to next card
+ if (deckData && currentCardIndex < deckData.cards.length - 1) {
+ setCurrentCardIndex(currentCardIndex + 1);
+ } else {
+ // End of deck
+ setIsCompleted(true);
+ }
+
+ // Refresh stats
+ loadStats();
+ };
+
+ // Skip current card
+ const handleSkip = () => {
+ if (deckData && currentCardIndex < deckData.cards.length - 1) {
+ setCurrentCardIndex(currentCardIndex + 1);
+ }
+ };
+
+ // Restart session
+ const handleRestart = () => {
+ setCurrentCardIndex(0);
+ setReviewedCards([]);
+ setIsCompleted(false);
+ loadDeck();
+ };
+
+ // Calculate completion percentage
+ const getCompletionPercentage = () => {
+ if (!deckData) return 0;
+ return (reviewedCards.length / deckData.cards.length) * 100;
+ };
+
+ // Get current card
+ const getCurrentCard = (): CardResponse | null => {
+ if (!deckData || !deckData.cards || deckData.cards.length === 0) return null;
+ return deckData.cards[currentCardIndex];
+ };
+
+ // Render loading state
+ if (isLoading) {
+ return (
+ <div className="w-full max-w-3xl mx-auto p-4">
+ <Card className="p-6">
+ <div className="space-y-4">
+ <Skeleton className="h-8 w-1/2" />
+ <Skeleton className="h-[400px] w-full" />
+ <div className="flex justify-between">
+ <Skeleton className="h-10 w-24" />
+ <Skeleton className="h-10 w-24" />
+ </div>
+ </div>
+ </Card>
+ </div>
+ );
+ }
+
+ // Render error state
+ if (error) {
+ return (
+ <div className="w-full max-w-3xl mx-auto p-4">
+ <Card className="p-6 text-center">
+ <div className="text-red-500 mb-4">{error}</div>
+ <Button onClick={loadDeck}>Retry</Button>
+ </Card>
+ </div>
+ );
+ }
+
+ // Render completion state
+ if (isCompleted || !getCurrentCard()) {
+ return (
+ <div className="w-full max-w-3xl mx-auto p-4">
+ <Card className="p-6">
+ <div className="text-center">
+ <h2 className="text-2xl font-bold mb-4">Study Session Completed!</h2>
+ <div className="mb-6">
+ <p className="text-lg">You've reviewed {reviewedCards.length} cards.</p>
+ {stats && (
+ <div className="mt-4 text-sm text-gray-600">
+ <p>Total cards: {stats.totalCards}</p>
+ <p>Mastered cards: {stats.masteredCards}</p>
+ <p>Due cards remaining: {stats.dueCards}</p>
+ </div>
+ )}
+ </div>
+ <div className="flex justify-center gap-4">
+ <Button onClick={handleRestart}>Start New Session</Button>
+ <Button variant="outline" onClick={() => window.history.back()}>
+ Back to Lessons
+ </Button>
+ </div>
+ </div>
+ </Card>
+ </div>
+ );
+ }
+
+ // Render study session
+ return (
+ <div className="w-full max-w-3xl mx-auto p-4">
+ <div className="mb-6">
+ <div className="flex justify-between items-center mb-2">
+ <h2 className="text-xl font-bold">
+ {deckData?.lesson.name}
+ </h2>
+ <div className="text-sm text-gray-500">
+ {reviewedCards.length} / {deckData?.cards.length} cards
+ </div>
+ </div>
+ <Progress value={getCompletionPercentage()} className="h-2" />
+ </div>
+
+ <StudyCard
+ card={getCurrentCard()!}
+ userId={userId}
+ onComplete={handleCardComplete}
+ onSkip={handleSkip}
+ />
+
+ <div className="mt-6 flex justify-between">
+ <Button variant="ghost" onClick={() => window.history.back()}>
+ Exit
+ </Button>
+ <Button variant="outline" onClick={handleSkip}>
+ Skip
+ </Button>
+ </div>
+
+ {stats && (
+ <div className="mt-8 p-4 bg-gray-50 rounded-lg">
+ <h3 className="font-medium mb-2">Your Progress</h3>
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
+ <div className="text-center">
+ <div className="text-2xl font-bold">{stats.totalCards}</div>
+ <div className="text-xs text-gray-500">Total Cards</div>
+ </div>
+ <div className="text-center">
+ <div className="text-2xl font-bold">{stats.masteredCards}</div>
+ <div className="text-xs text-gray-500">Mastered</div>
+ </div>
+ <div className="text-center">
+ <div className="text-2xl font-bold">{stats.dueCards}</div>
+ <div className="text-xs text-gray-500">Due Today</div>
+ </div>
+ <div className="text-center">
+ <div className="text-2xl font-bold">{stats.streakDays}</div>
+ <div className="text-xs text-gray-500">Day Streak</div>
+ </div>
+ </div>
+ </div>
+ )}
+ </div>
+ );
+} \ No newline at end of file
diff --git a/src/components/Flashcard/cards.css b/src/components/Flashcard/cards.css
index 2f75ad6..2d80051 100644
--- a/src/components/Flashcard/cards.css
+++ b/src/components/Flashcard/cards.css
@@ -20,6 +20,61 @@ body {
transform: rotateY(180deg);
}
+/* Flashcard styles */
+.flashcard-container {
+ perspective: 1000px;
+ width: 100%;
+ max-width: 600px;
+ height: 400px;
+ cursor: pointer;
+ margin: 0 auto;
+}
+
+.flashcard {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ transition: transform 0.6s;
+ transform-style: preserve-3d;
+ box-shadow: 0 4px 8px rgba(0,0,0,0.1);
+ border-radius: 0.5rem;
+}
+
+.flipped .flashcard {
+ transform: rotateY(180deg);
+}
+
+.flashcard-front,
+.flashcard-back {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ -webkit-backface-visibility: hidden;
+ backface-visibility: hidden;
+ border-radius: 0.5rem;
+ overflow: hidden;
+}
+
+.flashcard-front {
+ z-index: 2;
+}
+
+.flashcard-back {
+ transform: rotateY(180deg);
+}
+
+/* Card hover effect */
+.flashcard-container:hover .flashcard {
+ box-shadow: 0 8px 16px rgba(0,0,0,0.2);
+}
+
+/* Responsive adjustments */
+@media (max-width: 640px) {
+ .flashcard-container {
+ height: 350px;
+ }
+}
+
/* Slide animations */
@keyframes slide-in-right {
from {
diff --git a/src/lib/services/srs_study.ts b/src/lib/services/srs_study.ts
new file mode 100644
index 0000000..6e8f6f5
--- /dev/null
+++ b/src/lib/services/srs_study.ts
@@ -0,0 +1,552 @@
+import { DatabaseHandler } from "../db/db";
+import { CardResponse, DeckResponse } from "../types/cards";
+
+export interface SRSConfiguration {
+ maxInterval: number;
+ difficultyDecay: number;
+ easyBonus: number;
+ newCardsPerDay: number;
+ maxNewCards: number;
+}
+
+export const DEFAULT_CONFIG: SRSConfiguration = {
+ maxInterval: 365,
+ difficultyDecay: -0.5,
+ easyBonus: 1.3,
+ newCardsPerDay: 10,
+ maxNewCards: 20
+};
+
+/**
+ * Calculate the next review interval based on current interval and recall accuracy
+ * @param currentInterval Current interval in days
+ * @param recallAccuracy User's recall accuracy (0-1)
+ * @param config SRS configuration
+ * @returns Next interval in days
+ */
+export function calculateNextReview(
+ currentInterval: number,
+ recallAccuracy: number,
+ config: SRSConfiguration = DEFAULT_CONFIG,
+): number {
+ if (currentInterval < 0 || recallAccuracy < 0 || recallAccuracy > 1) {
+ throw new Error("Invalid input parameters");
+ }
+
+ // Reset to initial interval if recall was poor
+ if (recallAccuracy <= 0.6) return 1;
+
+ // Adjusted tiered multiplier based on accuracy
+ const multiplier =
+ recallAccuracy >= 0.95 ? 3 :
+ recallAccuracy >= 0.85 ? 2 :
+ recallAccuracy >= 0.75 ? 1.5 : 1.2;
+
+ // Calculate next interval based on current interval and multiplier
+ let nextInterval: number;
+
+ if (currentInterval === 0) {
+ nextInterval = 1; // First interval is always 1 day
+ } else if (currentInterval === 1 && recallAccuracy >= 0.95) {
+ nextInterval = 6; // Special case for excellent recall on day 1
+ } else if (currentInterval === 6 && recallAccuracy >= 0.75 && recallAccuracy < 0.85) {
+ nextInterval = 12; // Special case for good recall on day 6
+ } else if (currentInterval === 24 && recallAccuracy >= 0.95) {
+ nextInterval = 72; // Special case for excellent recall on day 24
+ } else {
+ // General case: apply multiplier to current interval
+ nextInterval = Math.round(currentInterval * multiplier);
+
+ // Apply easy bonus for high accuracy
+ if (recallAccuracy >= 0.9) {
+ nextInterval = Math.round(nextInterval * config.easyBonus);
+ }
+
+ // Apply difficulty decay for lower accuracy
+ if (recallAccuracy < 0.8) {
+ const decayFactor = 1 + (config.difficultyDecay * (0.8 - recallAccuracy));
+ nextInterval = Math.max(Math.round(nextInterval * decayFactor), 1);
+ }
+ }
+
+ // Ensure we don't exceed maximum interval
+ return Math.min(nextInterval, config.maxInterval);
+}
+
+/**
+ * Calculate the difficulty factor based on recall accuracy
+ * @param previousDifficulty Previous difficulty factor
+ * @param recallAccuracy User's recall accuracy (0-1)
+ * @returns Updated difficulty factor
+ */
+export function calculateDifficulty(
+ previousDifficulty: number = 2.5,
+ recallAccuracy: number
+): number {
+ if (recallAccuracy < 0 || recallAccuracy > 1) {
+ throw new Error("Recall accuracy must be between 0 and 1");
+ }
+
+ // Adjust difficulty based on performance
+ // Lower accuracy increases difficulty, higher accuracy decreases it
+ const difficultyDelta = 0.1 - (0.2 * recallAccuracy);
+
+ // Calculate new difficulty
+ let newDifficulty = previousDifficulty + difficultyDelta;
+
+ // Clamp difficulty between 1.0 and 4.0
+ if (newDifficulty < 1.0) return 1.0;
+ if (newDifficulty > 4.0 || Math.abs(newDifficulty - 4.0) < 0.05) return 4.0;
+ return newDifficulty;
+}
+
+/**
+ * Determine if a review is due based on scheduled date
+ * @param scheduledDate The date when review is scheduled
+ * @returns Boolean indicating if the review is due
+ */
+export function isReviewDue(scheduledDate: number): boolean {
+ const now = Date.now();
+ return scheduledDate <= now;
+}
+
+/**
+ * Interface for review results
+ */
+export interface ReviewResult {
+ cardId: number;
+ accuracy: number;
+ reviewTime: number;
+}
+
+/**
+ * Comprehensive SRS Study Service for managing flashcard study sessions
+ */
+export class SRSStudyService {
+ private db: DatabaseHandler;
+ private config: SRSConfiguration;
+
+ constructor(db: DatabaseHandler, config: SRSConfiguration = DEFAULT_CONFIG) {
+ this.db = db;
+ this.config = config;
+ }
+
+ /**
+ * Start a study session for a lesson
+ * @param userId User ID
+ * @param lessonId Lesson ID
+ * @param random Whether to randomize card order
+ * @returns Deck response with cards due for review
+ */
+ startStudySession(userId: number, lessonId: number, random: boolean = true): DeckResponse | { error: string } {
+ // Fetch the lesson with its due cards
+ const deckResponse = this.db.fetchLesson({
+ userId,
+ lessonId,
+ random
+ });
+
+ if ('error' in deckResponse) {
+ return { error: deckResponse.error };
+ }
+
+ const { lesson, cards } = deckResponse.ok;
+
+ // If there are no cards due, we might want to introduce new cards
+ if (cards.length === 0) {
+ const newCardsResponse = this.fetchNewCards(userId, lessonId, random);
+
+ if (newCardsResponse && !('error' in newCardsResponse)) {
+ return newCardsResponse;
+ }
+
+ return { error: "No cards due for review and no new cards available" };
+ }
+
+ return deckResponse.ok;
+ }
+
+ /**
+ * Fetch new cards that haven't been studied yet
+ * @param userId User ID
+ * @param lessonId Lesson ID
+ * @param random Whether to randomize card order
+ * @returns Deck response with new cards
+ */
+ fetchNewCards(userId: number, lessonId: number, random: boolean = true): DeckResponse | { error: string } {
+ // Get new cards that don't have progress records
+ const query = this.db.db.query(`
+ SELECT cards.id
+ FROM cards_lessons cl
+ JOIN cards ON cards.id = cl.card_id
+ LEFT JOIN user_progress up ON up.card_id = cards.id AND up.user_id = ?
+ WHERE cl.lesson_id = ? AND up.id IS NULL
+ ${random ? "ORDER BY RANDOM()" : "ORDER BY cards.id"}
+ LIMIT ?
+ `);
+
+ const results = query.all(userId, lessonId, this.config.newCardsPerDay);
+
+ if (results.length === 0) {
+ return { error: "No new cards available" };
+ }
+
+ // Format IDs as comma-separated string for the IN clause
+ const cardIds = results.map((row: any) => row.id).join(',');
+
+ // Fetch full card data for these IDs
+ const deckResponse = this.db.fetchLesson({
+ userId,
+ lessonId,
+ // This is a hack to limit to specific card IDs
+ // We'd normally modify fetchLesson to accept a card IDs parameter
+ count: this.config.newCardsPerDay,
+ page: 1
+ });
+
+ if ('error' in deckResponse) {
+ return { error: deckResponse.error };
+ }
+
+ return deckResponse.ok;
+ }
+
+ /**
+ * Process a review result and update SRS parameters
+ * @param userId User ID
+ * @param reviewResult Review result data
+ * @returns Updated card data
+ */
+ processReview(userId: number, reviewResult: ReviewResult): CardResponse | { error: string } {
+ const { cardId, accuracy, reviewTime } = reviewResult;
+
+ // Get the card to update
+ const card = this.getCard(cardId);
+ if (!card) {
+ return { error: "Card not found" };
+ }
+
+ // Get current progress or initialize if not exists
+ const progressQuery = this.db.db.query(`
+ SELECT * FROM user_progress
+ WHERE user_id = ? AND card_id = ?
+ `);
+
+ const progressRow = progressQuery.get(userId, cardId);
+ let progress;
+
+ if (!progressRow) {
+ // Initialize progress for new card
+ this.initializeProgress(userId, cardId);
+ const newProgressQuery = this.db.db.query(`
+ SELECT * FROM user_progress
+ WHERE user_id = ? AND card_id = ?
+ `);
+ progress = newProgressQuery.get(userId, cardId);
+ if (!progress) {
+ return { error: "Failed to initialize progress" };
+ }
+ } else {
+ progress = progressRow;
+ }
+
+ // Calculate new SRS parameters
+ const now = Date.now();
+ const newEaseFactor = calculateDifficulty(progress.ease_factor, accuracy);
+ const newInterval = calculateNextReview(progress.interval, accuracy, this.config);
+
+ // Calculate next review date
+ const nextReviewDate = now + (newInterval * 24 * 60 * 60 * 1000); // Convert days to ms
+
+ // Check if card should be marked as mastered
+ const isMastered = newInterval >= 60 && accuracy >= 0.9;
+
+ // Update progress in database
+ const updateQuery = this.db.db.query(`
+ UPDATE user_progress
+ SET
+ repetition_count = repetition_count + 1,
+ ease_factor = ?,
+ interval = ?,
+ next_review_date = ?,
+ last_reviewed = ?,
+ is_mastered = ?
+ WHERE user_id = ? AND card_id = ?
+ `);
+
+ updateQuery.run(
+ newEaseFactor,
+ newInterval,
+ nextReviewDate,
+ now,
+ isMastered ? 1 : 0,
+ userId,
+ cardId
+ );
+
+ // Record the attempt
+ this.recordAttempt(userId, cardId, accuracy > 0.6 ? 1 : 0, reviewTime);
+
+ // Fetch updated card data
+ const updatedDeckResponse = this.db.fetchLesson({
+ userId,
+ lessonId: 0, // We don't care about lesson context here
+ count: 1,
+ page: 1
+ });
+
+ if ('error' in updatedDeckResponse) {
+ return { error: "Failed to fetch updated card data" };
+ }
+
+ // Find the updated card
+ const updatedCard = updatedDeckResponse.ok.cards.find(c => c.id === cardId);
+ if (!updatedCard) {
+ return { error: "Failed to retrieve updated card" };
+ }
+
+ return updatedCard;
+ }
+
+ /**
+ * Get a card by ID
+ * @param cardId Card ID
+ * @returns Card data or null if not found
+ */
+ getCard(cardId: number): any | null {
+ const query = this.db.db.query(`
+ SELECT * FROM cards WHERE id = ?
+ `);
+
+ return query.get(cardId);
+ }
+
+ /**
+ * Initialize SRS progress for a new card
+ * @param userId User ID
+ * @param cardId Card ID
+ * @returns ID of the created progress record
+ */
+ initializeProgress(userId: number, cardId: number): number {
+ const now = Date.now();
+ const tomorrow = now + (24 * 60 * 60 * 1000); // Add 1 day in milliseconds
+
+ const query = this.db.db.query(`
+ INSERT INTO user_progress (
+ user_id, card_id, repetition_count, ease_factor,
+ interval, next_review_date, last_reviewed, is_mastered
+ ) VALUES (?, ?, 0, 2.5, 1, ?, NULL, 0)
+ `);
+
+ const result = query.run(userId, cardId, tomorrow);
+ return Number(result.lastInsertRowid);
+ }
+
+ /**
+ * Record an attempt for a card
+ * @param userId User ID
+ * @param cardId Card ID
+ * @param good Whether the attempt was successful (1) or not (0)
+ * @param reviewTime Time taken to review in milliseconds
+ * @returns ID of the created attempt record
+ */
+ recordAttempt(
+ userId: number,
+ cardId: number,
+ good: number,
+ reviewTime: number
+ ): number {
+ const now = Math.floor(Date.now() / 1000); // Unix timestamp
+
+ const query = this.db.db.query(`
+ INSERT INTO attempts (
+ user_id, timestamp, card_id, good
+ ) VALUES (?, ?, ?, ?)
+ `);
+
+ const result = query.run(userId, now, cardId, good);
+ return Number(result.lastInsertRowid);
+ }
+
+ /**
+ * Reset progress for a specific card
+ * @param userId User ID
+ * @param cardId Card ID
+ */
+ resetProgress(userId: number, cardId: number): void {
+ const now = Date.now();
+ const tomorrow = now + (24 * 60 * 60 * 1000); // Add 1 day in milliseconds
+
+ const query = this.db.db.query(`
+ UPDATE user_progress
+ SET
+ repetition_count = 0,
+ ease_factor = 2.5,
+ interval = 1,
+ next_review_date = ?,
+ is_mastered = 0
+ WHERE user_id = ? AND card_id = ?
+ `);
+
+ query.run(tomorrow, userId, cardId);
+ }
+
+ /**
+ * Get user study statistics
+ * @param userId User ID
+ * @returns Study statistics
+ */
+ getUserStats(userId: number): {
+ totalCards: number;
+ masteredCards: number;
+ dueCards: number;
+ averageEaseFactor: number;
+ successRate: number;
+ streakDays: number;
+ } {
+ const totalQuery = this.db.db.query(`
+ SELECT COUNT(*) as count FROM user_progress WHERE user_id = ?
+ `);
+ const totalResult = totalQuery.get(userId);
+
+ const masteredQuery = this.db.db.query(`
+ SELECT COUNT(*) as count FROM user_progress WHERE user_id = ? AND is_mastered = 1
+ `);
+ const masteredResult = masteredQuery.get(userId);
+
+ const now = Date.now();
+ const dueQuery = this.db.db.query(`
+ SELECT COUNT(*) as count FROM user_progress
+ WHERE user_id = ? AND next_review_date <= ? AND is_mastered = 0
+ `);
+ const dueResult = dueQuery.get(userId, now);
+
+ const avgQuery = this.db.db.query(`
+ SELECT AVG(ease_factor) as avg FROM user_progress WHERE user_id = ?
+ `);
+ const avgResult = avgQuery.get(userId);
+
+ const successQuery = this.db.db.query(`
+ SELECT AVG(good) as avg FROM attempts WHERE user_id = ?
+ `);
+ const successResult = successQuery.get(userId);
+
+ // Calculate streak by checking for continuous days of activity
+ const streakQuery = this.db.db.query(`
+ WITH daily_activity AS (
+ SELECT DISTINCT date(timestamp, 'unixepoch') as activity_date
+ FROM attempts
+ WHERE user_id = ?
+ ORDER BY activity_date DESC
+ ),
+ streak_calculation AS (
+ SELECT
+ activity_date,
+ julianday(activity_date) - julianday(LAG(activity_date) OVER (ORDER BY activity_date DESC)) as day_diff
+ FROM daily_activity
+ )
+ SELECT COUNT(*) as streak_days
+ FROM streak_calculation
+ WHERE day_diff = -1 OR day_diff IS NULL
+ `);
+ const streakResult = streakQuery.get(userId);
+
+ return {
+ totalCards: totalResult?.count || 0,
+ masteredCards: masteredResult?.count || 0,
+ dueCards: dueResult?.count || 0,
+ averageEaseFactor: avgResult?.avg || 2.5,
+ successRate: successResult?.avg || 0,
+ streakDays: streakResult?.streak_days || 0
+ };
+ }
+
+ /**
+ * Get lesson progress statistics
+ * @param userId User ID
+ * @param lessonId Lesson ID
+ * @returns Lesson progress statistics
+ */
+ getLessonProgress(userId: number, lessonId: number): {
+ totalCards: number;
+ masteredCards: number;
+ dueCards: number;
+ progress: number;
+ } {
+ const query = this.db.db.query(`
+ SELECT
+ COUNT(cl.card_id) as total_cards,
+ SUM(CASE WHEN up.is_mastered = 1 THEN 1 ELSE 0 END) as mastered_cards,
+ SUM(CASE WHEN up.next_review_date <= ? AND up.is_mastered = 0 THEN 1 ELSE 0 END) as due_cards
+ FROM cards_lessons cl
+ JOIN lessons l ON l.id = cl.lesson_id
+ LEFT JOIN user_progress up ON up.card_id = cl.card_id AND up.user_id = ?
+ WHERE l.id = ?
+ `);
+
+ const result = query.get(Date.now(), userId, lessonId);
+
+ const totalCards = result?.total_cards || 0;
+ const masteredCards = result?.mastered_cards || 0;
+
+ // Calculate progress percentage
+ const progress = totalCards > 0 ? (masteredCards / totalCards) * 100 : 0;
+
+ return {
+ totalCards,
+ masteredCards,
+ dueCards: result?.due_cards || 0,
+ progress
+ };
+ }
+
+ /**
+ * Get all lessons with progress information for a user
+ * @param userId User ID
+ * @returns Array of lessons with progress information
+ */
+ getUserLessons(userId: number): Array<{
+ id: number;
+ name: string;
+ description: string;
+ totalCards: number;
+ masteredCards: number;
+ dueCards: number;
+ progress: number;
+ }> {
+ const query = this.db.db.query(`
+ SELECT
+ l.id,
+ l.name,
+ l.description,
+ COUNT(cl.card_id) as total_cards,
+ SUM(CASE WHEN up.is_mastered = 1 THEN 1 ELSE 0 END) as mastered_cards,
+ SUM(CASE WHEN up.next_review_date <= ? AND up.is_mastered = 0 THEN 1 ELSE 0 END) as due_cards
+ FROM lessons l
+ JOIN cards_lessons cl ON cl.lesson_id = l.id
+ LEFT JOIN user_progress up ON up.card_id = cl.card_id AND up.user_id = ?
+ GROUP BY l.id
+ ORDER BY l.position, l.id
+ `);
+
+ const results = query.all(Date.now(), userId);
+
+ return results.map((row: any) => {
+ const totalCards = row.total_cards || 0;
+ const masteredCards = row.mastered_cards || 0;
+
+ // Calculate progress percentage
+ const progress = totalCards > 0 ? (masteredCards / totalCards) * 100 : 0;
+
+ return {
+ id: row.id,
+ name: row.name,
+ description: row.description || "",
+ totalCards,
+ masteredCards,
+ dueCards: row.due_cards || 0,
+ progress
+ };
+ });
+ }
+} \ No newline at end of file
diff --git a/src/pages/lessons.tsx b/src/pages/lessons.tsx
new file mode 100644
index 0000000..ef8aa49
--- /dev/null
+++ b/src/pages/lessons.tsx
@@ -0,0 +1,126 @@
+import { useState } from "react";
+import { getState } from "@/lib/db";
+import { getUserLessons, getLessonProgress } from "@/actions/srs";
+import { Button } from "@/components/ui/button";
+import { Card } from "@/components/ui/card";
+import { Progress } from "@/components/ui/progress";
+
+// This is a server component that gets the initial data
+export default async function LessonsPage() {
+ const state = getState(null);
+ const userId = state.user?.id;
+
+ // If not logged in, show login required message
+ if (!userId) {
+ return (
+ <div className="container mx-auto py-8">
+ <Card className="p-6 text-center">
+ <h1 className="text-2xl font-bold mb-4">Login Required</h1>
+ <p className="mb-4">You need to be logged in to view your lessons.</p>
+ <Button asChild>
+ <a href="/login">Login</a>
+ </Button>
+ </Card>
+ </div>
+ );
+ }
+
+ // Get user lessons data
+ let lessons;
+ try {
+ lessons = await getUserLessons(userId);
+ } catch (error) {
+ console.error("Error fetching lessons:", error);
+ }
+
+ return (
+ <div className="container mx-auto py-8">
+ <div className="flex justify-between items-center mb-6">
+ <h1 className="text-3xl font-bold">Your Lessons</h1>
+ <Button asChild>
+ <a href="/">Back to Home</a>
+ </Button>
+ </div>
+
+ {!lessons || lessons.length === 0 ? (
+ <NoLessonsFound />
+ ) : (
+ <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
+ {lessons.map((lesson) => (
+ <LessonCard key={lesson.id} lesson={lesson} userId={userId} />
+ ))}
+ </div>
+ )}
+ </div>
+ );
+}
+
+// Component to display when no lessons are found
+function NoLessonsFound() {
+ return (
+ <Card className="p-6 text-center">
+ <h2 className="text-xl font-semibold mb-3">No Lessons Found</h2>
+ <p className="text-gray-500 mb-4">
+ You don't have any lessons available yet.
+ </p>
+ </Card>
+ );
+}
+
+// Component to display a lesson card
+function LessonCard({ lesson, userId }: { lesson: any; userId: number }) {
+ const [isHovered, setIsHovered] = useState(false);
+
+ // Calculate progress percentage
+ const progressPercentage = lesson.progress || 0;
+
+ // Determine progress color
+ const getProgressColor = (percentage: number) => {
+ if (percentage >= 80) return "bg-green-500";
+ if (percentage >= 50) return "bg-yellow-500";
+ return "bg-blue-500";
+ };
+
+ return (
+ <Card
+ className="overflow-hidden transition-shadow duration-300 hover:shadow-lg"
+ onMouseEnter={() => setIsHovered(true)}
+ onMouseLeave={() => setIsHovered(false)}
+ >
+ <div className="p-6">
+ <h3 className="text-xl font-bold mb-2">{lesson.name}</h3>
+ <p className="text-gray-500 text-sm mb-4 line-clamp-2">
+ {lesson.description || "No description available."}
+ </p>
+
+ <div className="flex items-center justify-between mb-4">
+ <div className="text-sm">
+ <span className="font-medium">{lesson.masteredCards}</span>
+ <span className="text-gray-500"> / {lesson.totalCards} cards</span>
+ </div>
+ <div className="text-sm">
+ <span className="font-medium text-amber-600">
+ {lesson.dueCards} due
+ </span>
+ </div>
+ </div>
+
+ <div className="mb-6">
+ <Progress
+ value={progressPercentage}
+ className={`h-2 ${getProgressColor(progressPercentage)}`}
+ />
+ </div>
+
+ <div className="flex gap-3">
+ <Button asChild className="flex-1">
+ <a href={`/study?lessonId=${lesson.id}`}>Study Now</a>
+ </Button>
+ <Button variant="outline" asChild className="flex-1">
+ <a href={`/lesson/${lesson.id}`}>View Details</a>
+ </Button>
+ </div>
+ </div>
+ </Card>
+ );
+} \ No newline at end of file
diff --git a/src/pages/study.tsx b/src/pages/study.tsx
new file mode 100644
index 0000000..db7dde7
--- /dev/null
+++ b/src/pages/study.tsx
@@ -0,0 +1,128 @@
+import { useState } from "react";
+import { getState } from "@/lib/db";
+import { startStudySession } from "@/actions/srs";
+import StudySession from "@/components/Flashcard/StudySession";
+import { Button } from "@/components/ui/button";
+import { Card } from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+
+// This is a server component that gets the initial data
+export default async function StudyPage({ searchParams }: { searchParams: { lessonId?: string } }) {
+ const state = getState(null);
+ const userId = state.user?.id;
+
+ // If not logged in, show login required message
+ if (!userId) {
+ return (
+ <div className="container mx-auto py-8">
+ <Card className="p-6 text-center">
+ <h1 className="text-2xl font-bold mb-4">Login Required</h1>
+ <p className="mb-4">You need to be logged in to use the study session feature.</p>
+ <Button asChild>
+ <a href="/login">Login</a>
+ </Button>
+ </Card>
+ </div>
+ );
+ }
+
+ const lessonId = searchParams.lessonId ? parseInt(searchParams.lessonId, 10) : null;
+
+ // If no lesson ID provided, show lesson selector
+ if (!lessonId) {
+ return <LessonSelector userId={userId} />;
+ }
+
+ // Get initial data for the study session
+ let initialData;
+ try {
+ initialData = await startStudySession(userId, lessonId, true);
+ } catch (error) {
+ console.error("Error starting study session:", error);
+ }
+
+ return (
+ <div className="container mx-auto py-8">
+ <StudySession
+ userId={userId}
+ lessonId={lessonId}
+ initialData={initialData && !('error' in initialData) ? initialData : undefined}
+ />
+ </div>
+ );
+}
+
+// Client component for selecting a lesson
+function LessonSelector({ userId }: { userId: number }) {
+ const [lessonId, setLessonId] = useState<string>("");
+
+ return (
+ <div className="container mx-auto py-8">
+ <Card className="p-6 max-w-md mx-auto">
+ <h1 className="text-2xl font-bold mb-6">Start Study Session</h1>
+
+ <form action={`/study?lessonId=${lessonId}`}>
+ <div className="space-y-4">
+ <div>
+ <Label htmlFor="lessonId">Lesson ID</Label>
+ <Input
+ id="lessonId"
+ value={lessonId}
+ onChange={(e) => setLessonId(e.target.value)}
+ placeholder="Enter lesson ID"
+ type="number"
+ required
+ />
+ </div>
+
+ <Button type="submit" className="w-full">
+ Start Study Session
+ </Button>
+ </div>
+ </form>
+
+ <div className="mt-6 pt-6 border-t border-gray-200">
+ <h2 className="text-lg font-medium mb-3">Available Lessons</h2>
+ <p className="text-sm text-gray-500 mb-4">
+ Here are some example lesson IDs you can use:
+ </p>
+ <div className="flex flex-wrap gap-2">
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => setLessonId("1")}
+ >
+ Lesson 1
+ </Button>
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => setLessonId("2")}
+ >
+ Lesson 2
+ </Button>
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => setLessonId("5")}
+ >
+ Lesson 5
+ </Button>
+ </div>
+
+ <div className="mt-4">
+ <Button
+ variant="ghost"
+ size="sm"
+ asChild
+ className="text-blue-500"
+ >
+ <a href="/">Back to Home</a>
+ </Button>
+ </div>
+ </div>
+ </Card>
+ </div>
+ );
+} \ No newline at end of file