import { getAuthUserId } from '@convex-dev/auth/server'; import { v } from 'convex/values'; import { mutation, query } from './_generated/server'; export const startQuizSession = mutation({ args: { questionIds: v.optional(v.array(v.id('questions'))), }, handler: async (ctx, args) => { const userId = await getAuthUserId(ctx); if (!userId) throw new Error('Not authenticated'); // End any active sessions const activeSessions = await ctx.db .query('quizSessions') .withIndex('by_user_and_active', (q) => q.eq('userId', userId).eq('isActive', true), ) .collect(); for (const session of activeSessions) { await ctx.db.patch(session._id, { isActive: false }); } // Get questions let questionIds = args.questionIds; if (!questionIds || questionIds.length === 0) { const allQuestions = await ctx.db.query('questions').collect(); questionIds = allQuestions.map((q) => q._id); } // Shuffle questions const shuffled = [...questionIds].sort(() => Math.random() - 0.5); return await ctx.db.insert('quizSessions', { userId, activeQuestions: shuffled, missedQuestions: [], completedQuestions: [], currentQuestionIndex: 0, isActive: true, }); }, }); export const getActiveSession = query({ args: {}, handler: async (ctx) => { const userId = await getAuthUserId(ctx); if (!userId) throw new Error('Not authenticated'); const session = await ctx.db .query('quizSessions') .withIndex('by_user_and_active', (q) => q.eq('userId', userId).eq('isActive', true), ) .first(); return session; }, }); export const getCurrentQuestion = query({ args: {}, handler: async (ctx) => { const userId = await getAuthUserId(ctx); if (!userId) throw new Error('Not authenticated'); const session = await ctx.db .query('quizSessions') .withIndex('by_user_and_active', (q) => q.eq('userId', userId).eq('isActive', true), ) .first(); if (!session) return null; const allQuestions = [ ...session.activeQuestions, ...session.missedQuestions, ]; if (allQuestions.length === 0) return null; const currentQuestionId = allQuestions[session.currentQuestionIndex]; const question = await ctx.db.get(currentQuestionId); return { question, sessionId: session._id, progress: { current: session.currentQuestionIndex + 1, total: allQuestions.length, completed: session.completedQuestions.length, }, }; }, }); export const answerQuestion = mutation({ args: { sessionId: v.id('quizSessions'), questionId: v.id('questions'), selectedAnswer: v.number(), }, handler: async (ctx, args) => { const userId = await getAuthUserId(ctx); if (!userId) throw new Error('Not authenticated'); const session = await ctx.db.get(args.sessionId); if (!session || session.userId !== userId) { throw new Error('Invalid session'); } const question = await ctx.db.get(args.questionId); if (!question) throw new Error('Question not found'); const isCorrect = args.selectedAnswer === question.correctAnswer; // Update user progress const existingProgress = await ctx.db .query('userProgress') .withIndex('by_user_and_question', (q) => q.eq('userId', userId).eq('questionId', args.questionId), ) .first(); if (existingProgress) { await ctx.db.patch(existingProgress._id, { correctCount: existingProgress.correctCount + (isCorrect ? 1 : 0), incorrectCount: existingProgress.incorrectCount + (isCorrect ? 0 : 1), lastAttempted: Date.now(), }); } else { await ctx.db.insert('userProgress', { userId, questionId: args.questionId, correctCount: isCorrect ? 1 : 0, incorrectCount: isCorrect ? 0 : 1, lastAttempted: Date.now(), }); } // Update session const allQuestions = [ ...session.activeQuestions, ...session.missedQuestions, ]; let newActiveQuestions = [...session.activeQuestions]; let newMissedQuestions = [...session.missedQuestions]; let newCompletedQuestions = [...session.completedQuestions]; // Remove current question from active list const currentIndex = newActiveQuestions.indexOf(args.questionId); if (currentIndex !== -1) { newActiveQuestions.splice(currentIndex, 1); } else { const missedIndex = newMissedQuestions.indexOf(args.questionId); if (missedIndex !== -1) { newMissedQuestions.splice(missedIndex, 1); } } if (isCorrect) { newCompletedQuestions.push(args.questionId); } else { newMissedQuestions.push(args.questionId); } const newAllQuestions = [...newActiveQuestions, ...newMissedQuestions]; const newIndex = newAllQuestions.length > 0 ? 0 : session.currentQuestionIndex; await ctx.db.patch(args.sessionId, { activeQuestions: newActiveQuestions, missedQuestions: newMissedQuestions, completedQuestions: newCompletedQuestions, currentQuestionIndex: newIndex, }); return { isCorrect, correctAnswer: question.correctAnswer, explanation: question.explanation, }; }, }); export const endQuizSession = mutation({ args: { sessionId: v.id('quizSessions'), }, handler: async (ctx, args) => { const userId = await getAuthUserId(ctx); if (!userId) throw new Error('Not authenticated'); const session = await ctx.db.get(args.sessionId); if (!session || session.userId !== userId) { throw new Error('Invalid session'); } await ctx.db.patch(args.sessionId, { isActive: false }); return null; }, }); export const getUserStats = query({ args: {}, handler: async (ctx) => { const userId = await getAuthUserId(ctx); if (!userId) throw new Error('Not authenticated'); const progress = await ctx.db .query('userProgress') .withIndex('by_user', (q) => q.eq('userId', userId)) .collect(); const totalQuestions = progress.length; const totalCorrect = progress.reduce((sum, p) => sum + p.correctCount, 0); const totalIncorrect = progress.reduce( (sum, p) => sum + p.incorrectCount, 0, ); const totalAttempts = totalCorrect + totalIncorrect; return { totalQuestions, totalAttempts, totalCorrect, totalIncorrect, accuracy: totalAttempts > 0 ? (totalCorrect / totalAttempts) * 100 : 0, }; }, });