summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/components/Flashcard/StudySession.tsx75
-rw-r--r--src/components/srs/LessonSelector.tsx76
-rw-r--r--src/lib/db/index.ts26
-rw-r--r--src/lib/services/srs_study.ts211
-rw-r--r--src/pages/study.tsx124
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>;
+}