import { getAuthUserId } from '@convex-dev/auth/server'; import { v } from 'convex/values'; import OpenAI from 'openai'; import { api } from './_generated/api'; import { action, mutation, query } from './_generated/server'; const openai = new OpenAI({ baseURL: process.env.OPENAI_BASE_URL, apiKey: process.env.OPENAI_API_KEY, }); export const getAllQuestions = query({ args: {}, handler: async (ctx) => { const userId = await getAuthUserId(ctx); if (!userId) throw new Error('Not authenticated'); return await ctx.db.query('questions').collect(); }, }); export const getQuestionsByTopic = query({ args: { topic: v.string() }, handler: async (ctx, args) => { const userId = await getAuthUserId(ctx); if (!userId) throw new Error('Not authenticated'); return await ctx.db .query('questions') .withIndex('by_topic', (q) => q.eq('topic', args.topic)) .collect(); }, }); export const addQuestion = mutation({ args: { question: v.string(), options: v.array(v.string()), correctAnswer: v.number(), topic: v.string(), difficulty: v.optional(v.string()), explanation: v.optional(v.string()), }, handler: async (ctx, args) => { const userId = await getAuthUserId(ctx); if (!userId) throw new Error('Not authenticated'); return await ctx.db.insert('questions', { question: args.question, options: args.options, correctAnswer: args.correctAnswer, topic: args.topic, difficulty: args.difficulty, explanation: args.explanation, }); }, }); export const generateQuestions = action({ args: { topic: v.string(), count: v.number(), }, handler: async (ctx, args) => { const userId = await getAuthUserId(ctx); if (!userId) throw new Error('Not authenticated'); const prompt = `Generate ${args.count} multiple choice questions for the CompTIA Network+ exam on the topic: "${args.topic}". Return ONLY a valid JSON array with this exact structure: [ { "question": "Question text here?", "options": ["Option A", "Option B", "Option C", "Option D"], "correctAnswer": 0, "topic": "${args.topic}", "difficulty": "medium", "explanation": "Brief explanation of the correct answer" } ] Rules: - Each question must have exactly 4 options - correctAnswer is the index (0-3) of the correct option - Make questions realistic for Network+ certification - Include technical details and scenarios - Return ONLY the JSON array, no other text`; const response = await openai.chat.completions.create({ model: 'gpt-4o-mini', messages: [{ role: 'user', content: prompt }], temperature: 0.8, }); const content = response.choices[0].message.content; if (!content) throw new Error('No response from AI'); let questions; try { questions = JSON.parse(content); } catch (e) { throw new Error('Failed to parse AI response as JSON'); } const questionIds = []; for (const q of questions) { const id: string = await ctx.runMutation(api.questions.addQuestion, { question: q.question, options: q.options, correctAnswer: q.correctAnswer, topic: q.topic, difficulty: q.difficulty, explanation: q.explanation, }); questionIds.push(id); } return questionIds; }, });