diff options
author | polwex <polwex@sortug.com> | 2025-05-29 15:16:41 +0700 |
---|---|---|
committer | polwex <polwex@sortug.com> | 2025-05-29 15:16:41 +0700 |
commit | 8e0965f5274635f609972ef85802675af64df0f4 (patch) | |
tree | cc82db5928d49bede5c162cd22ab2a4e36cbdc6b /src | |
parent | 490388360a0852bcf8ee054e96fa90e166df5792 (diff) |
this is mostly me
Diffstat (limited to 'src')
-rw-r--r-- | src/components/Flashcard/StudySession.tsx | 75 | ||||
-rw-r--r-- | src/components/srs/LessonSelector.tsx | 76 | ||||
-rw-r--r-- | src/lib/db/index.ts | 26 | ||||
-rw-r--r-- | src/lib/services/srs_study.ts | 211 | ||||
-rw-r--r-- | src/pages/study.tsx | 124 |
5 files changed, 296 insertions, 216 deletions
diff --git a/src/components/Flashcard/StudySession.tsx b/src/components/Flashcard/StudySession.tsx index 1f79e09..c58531b 100644 --- a/src/components/Flashcard/StudySession.tsx +++ b/src/components/Flashcard/StudySession.tsx @@ -13,37 +13,43 @@ import { cn } from "@/lib/utils"; interface StudySessionProps { userId: number; lessonId: number; - initialData?: DeckResponse; + initialData: DeckResponse; } -export default function StudySession({ userId, lessonId, initialData }: StudySessionProps) { - const [deckData, setDeckData] = useState<DeckResponse | null>(initialData || null); +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) { + + if ("error" in result) { setError(result.error); setDeckData(null); } else { @@ -56,7 +62,7 @@ export default function StudySession({ userId, lessonId, initialData }: StudySes setIsLoading(false); } }; - + // Load user stats const loadStats = async () => { try { @@ -66,12 +72,12 @@ export default function StudySession({ userId, lessonId, initialData }: StudySes console.error("Error loading stats:", error); } }; - + // Handle card completion const handleCardComplete = (updatedCard: CardResponse) => { // Add to reviewed cards - setReviewedCards(prev => [...prev, updatedCard]); - + setReviewedCards((prev) => [...prev, updatedCard]); + // Move to next card if (deckData && currentCardIndex < deckData.cards.length - 1) { setCurrentCardIndex(currentCardIndex + 1); @@ -79,18 +85,18 @@ export default function StudySession({ userId, lessonId, initialData }: StudySes // 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); @@ -98,19 +104,20 @@ export default function StudySession({ userId, lessonId, initialData }: StudySes 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]; + if (!deckData || !deckData.cards || deckData.cards.length === 0) + return null; + return deckData.cards[currentCardIndex]!; }; - + // Render loading state if (isLoading) { return ( @@ -128,7 +135,7 @@ export default function StudySession({ userId, lessonId, initialData }: StudySes </div> ); } - + // Render error state if (error) { return ( @@ -140,16 +147,20 @@ export default function StudySession({ userId, lessonId, initialData }: StudySes </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> + <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> + <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> @@ -169,29 +180,27 @@ export default function StudySession({ userId, lessonId, initialData }: StudySes </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> + <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 @@ -200,7 +209,7 @@ export default function StudySession({ userId, lessonId, initialData }: StudySes Skip </Button> </div> - + {stats && ( <div className="mt-8 p-4 bg-gray-50 rounded-lg"> <h3 className="font-medium mb-2">Your Progress</h3> @@ -226,4 +235,4 @@ export default function StudySession({ userId, lessonId, initialData }: StudySes )} </div> ); -}
\ No newline at end of file +} diff --git a/src/components/srs/LessonSelector.tsx b/src/components/srs/LessonSelector.tsx new file mode 100644 index 0000000..8c4e8dd --- /dev/null +++ b/src/components/srs/LessonSelector.tsx @@ -0,0 +1,76 @@ +"use client"; +import { useState } from "react"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +// 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> + ); +} + +export default LessonSelector; diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts index 6bd417c..a767f70 100644 --- a/src/lib/db/index.ts +++ b/src/lib/db/index.ts @@ -348,11 +348,13 @@ class DatabaseHandler { page?: number; random?: boolean; }): Result<DeckResponse> { + console.time("fetchLesson-total"); const p = page ? page : 1; const size = count ? count : PAGE_SIZE; const offset = getDBOffset(p, size); const tomorrow = new Date(); tomorrow.setDate(tomorrow.getDate() + 1); + console.time("fetchLesson-query"); const queryString = ` SELECT l.name, l.description, ll.lang as llang, cards.text, cards.note, cards.id as cid, @@ -403,10 +405,18 @@ class DatabaseHandler { // LIMIT 10; const query = this.db.query(queryString); const res = query.all(userId, lessonId, tomorrow.getTime(), size, offset); + console.timeEnd("fetchLesson-query"); + // console.log("cards", res.length); - if (res.length === 0) return { error: "Lesson not found" }; + if (res.length === 0) { + console.timeEnd("fetchLesson-total"); + return { error: "Lesson not found" }; + } + const row: any = res[0]; - console.log({ row }); + // console.log({ row }); + + console.time("fetchLesson-process"); const lesson = { id: lessonId, name: row.name, @@ -414,10 +424,11 @@ class DatabaseHandler { language: row.llang, cardCount: row.total_card_count, }; - // TODO IPA, prosody, senses... should we unify the format on the wikisource standard? + + // Process the cards + console.time("fetchLesson-json-processing"); const cards = res.map((row: any) => { - // TODO parse here...? - // console.log({ row }); + // JSON parsing is often expensive const sense_array = JSON.parse(row.senses_array); const senses = sense_array.map((s: any) => { const senses = JSON.parse(s.senses); @@ -457,6 +468,10 @@ class DatabaseHandler { }; return card; }); + console.timeEnd("fetchLesson-json-processing"); + + console.timeEnd("fetchLesson-process"); + console.timeEnd("fetchLesson-total"); return { ok: { lesson, cards } }; } fetchCard(cid: number, userid: number) { @@ -794,4 +809,5 @@ type ExpressionSearchParams = { type ExpressionType = "syllable" | "word" | "expression"; const db = new DatabaseHandler(); +export type { DatabaseHandler }; export default db; diff --git a/src/lib/services/srs_study.ts b/src/lib/services/srs_study.ts index 6e8f6f5..9223722 100644 --- a/src/lib/services/srs_study.ts +++ b/src/lib/services/srs_study.ts @@ -1,4 +1,5 @@ -import { DatabaseHandler } from "../db/db"; +import type { DatabaseHandler } from "@/lib/db"; +import { Result } from "../types"; import { CardResponse, DeckResponse } from "../types/cards"; export interface SRSConfiguration { @@ -14,7 +15,7 @@ export const DEFAULT_CONFIG: SRSConfiguration = { difficultyDecay: -0.5, easyBonus: 1.3, newCardsPerDay: 10, - maxNewCards: 20 + maxNewCards: 20, }; /** @@ -38,37 +39,45 @@ export function calculateNextReview( // Adjusted tiered multiplier based on accuracy const multiplier = - recallAccuracy >= 0.95 ? 3 : - recallAccuracy >= 0.85 ? 2 : - recallAccuracy >= 0.75 ? 1.5 : 1.2; + 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) { + } 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)); + 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); } @@ -81,19 +90,19 @@ export function calculateNextReview( */ export function calculateDifficulty( previousDifficulty: number = 2.5, - recallAccuracy: number + 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); - + 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; @@ -138,32 +147,34 @@ export class SRSStudyService { * @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 } { + startStudySession( + userId: number, + lessonId: number, + random: boolean = true, + ): Result<DeckResponse> { // Fetch the lesson with its due cards const deckResponse = this.db.fetchLesson({ userId, lessonId, - random + random, }); - - if ('error' in deckResponse) { - return { error: deckResponse.error }; - } - + + if ("error" in deckResponse) return deckResponse; + 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)) { + + if (newCardsResponse && !("error" in newCardsResponse)) { return newCardsResponse; } - + return { error: "No cards due for review and no new cards available" }; } - - return deckResponse.ok; + + return deckResponse; } /** @@ -173,7 +184,11 @@ export class SRSStudyService { * @param random Whether to randomize card order * @returns Deck response with new cards */ - fetchNewCards(userId: number, lessonId: number, random: boolean = true): DeckResponse | { error: string } { + fetchNewCards( + userId: number, + lessonId: number, + random: boolean = true, + ): Result<DeckResponse> { // Get new cards that don't have progress records const query = this.db.db.query(` SELECT cards.id @@ -184,16 +199,16 @@ export class SRSStudyService { ${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(','); - + const cardIds = results.map((row: any) => row.id).join(","); + // Fetch full card data for these IDs const deckResponse = this.db.fetchLesson({ userId, @@ -201,14 +216,14 @@ export class SRSStudyService { // 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 + page: 1, }); - - if ('error' in deckResponse) { + + if ("error" in deckResponse) { return { error: deckResponse.error }; } - - return deckResponse.ok; + + return deckResponse; } /** @@ -217,24 +232,27 @@ export class SRSStudyService { * @param reviewResult Review result data * @returns Updated card data */ - processReview(userId: number, reviewResult: ReviewResult): CardResponse | { error: string } { + 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); @@ -249,18 +267,22 @@ export class SRSStudyService { } 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); - + const newInterval = calculateNextReview( + progress.interval, + accuracy, + this.config, + ); + // Calculate next review date - const nextReviewDate = now + (newInterval * 24 * 60 * 60 * 1000); // Convert days to ms - + 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 @@ -273,7 +295,7 @@ export class SRSStudyService { is_mastered = ? WHERE user_id = ? AND card_id = ? `); - + updateQuery.run( newEaseFactor, newInterval, @@ -281,30 +303,32 @@ export class SRSStudyService { now, isMastered ? 1 : 0, userId, - cardId + 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 + page: 1, }); - - if ('error' in updatedDeckResponse) { + + 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); + const updatedCard = updatedDeckResponse.ok.cards.find( + (c) => c.id === cardId, + ); if (!updatedCard) { return { error: "Failed to retrieve updated card" }; } - + return updatedCard; } @@ -317,7 +341,7 @@ export class SRSStudyService { const query = this.db.db.query(` SELECT * FROM cards WHERE id = ? `); - + return query.get(cardId); } @@ -329,15 +353,15 @@ export class SRSStudyService { */ initializeProgress(userId: number, cardId: number): number { const now = Date.now(); - const tomorrow = now + (24 * 60 * 60 * 1000); // Add 1 day in milliseconds - + 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); } @@ -351,19 +375,19 @@ export class SRSStudyService { * @returns ID of the created attempt record */ recordAttempt( - userId: number, - cardId: number, + userId: number, + cardId: number, good: number, - reviewTime: 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); } @@ -375,8 +399,8 @@ export class SRSStudyService { */ resetProgress(userId: number, cardId: number): void { const now = Date.now(); - const tomorrow = now + (24 * 60 * 60 * 1000); // Add 1 day in milliseconds - + const tomorrow = now + 24 * 60 * 60 * 1000; // Add 1 day in milliseconds + const query = this.db.db.query(` UPDATE user_progress SET @@ -387,7 +411,7 @@ export class SRSStudyService { is_mastered = 0 WHERE user_id = ? AND card_id = ? `); - + query.run(tomorrow, userId, cardId); } @@ -408,29 +432,29 @@ export class SRSStudyService { 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 ( @@ -450,14 +474,14 @@ export class SRSStudyService { 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 + streakDays: streakResult?.streak_days || 0, }; } @@ -467,7 +491,10 @@ export class SRSStudyService { * @param lessonId Lesson ID * @returns Lesson progress statistics */ - getLessonProgress(userId: number, lessonId: number): { + getLessonProgress( + userId: number, + lessonId: number, + ): { totalCards: number; masteredCards: number; dueCards: number; @@ -483,20 +510,20 @@ export class SRSStudyService { 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 + progress, }; } @@ -528,16 +555,16 @@ export class SRSStudyService { 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, @@ -545,8 +572,8 @@ export class SRSStudyService { totalCards, masteredCards, dueCards: row.due_cards || 0, - progress + progress, }; }); } -}
\ No newline at end of file +} diff --git a/src/pages/study.tsx b/src/pages/study.tsx index f818b4b..68f781e 100644 --- a/src/pages/study.tsx +++ b/src/pages/study.tsx @@ -1,12 +1,10 @@ import { getContextData } from "waku/middleware/context"; -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"; +import LessonSelector from "@/components/srs/LessonSelector"; // This is a server component that gets the initial data export default async function StudyPage({ @@ -15,8 +13,8 @@ export default async function StudyPage({ searchParams: { lessonId?: string }; }) { const { user } = getContextData() as any; - // const state = getState(null); const userId = user?.id; + // const state = getState(null); // If not logged in, show login required message if (!userId) { @@ -35,101 +33,55 @@ export default async function StudyPage({ ); } - const lessonId = searchParams.lessonId + 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 - } - /> + <Inner userId={userId} lessonId={lessonId} /> </div> ); } -// Client component for selecting a lesson -function LessonSelector({ userId }: { userId: number }) { - const [lessonId, setLessonId] = useState<string>(""); - +async function Inner({ + userId, + lessonId, +}: { + userId: number; + lessonId: number | null; +}) { 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> + <> + {lessonId ? ( + <StudySessionOuter userId={userId} lessonId={Number(lessonId)} /> + ) : ( + <LessonSelector userId={userId} /> + )} + </> ); } +async function StudySessionOuter({ + userId, + lessonId, +}: { + userId: number; + lessonId: number; +}) { + const initialData = await startStudySession(userId, lessonId, true); + if ("ok" in initialData) + return ( + <> + <StudySession + userId={userId} + lessonId={lessonId} + initialData={initialData.ok} + /> + </> + ); + else return <p>idk</p>; +} |