import { v } from "convex/values"; import { query, mutation, action } from "./_generated/server"; import { getAuthUserId } from "@convex-dev/auth/server"; import { api } from "./_generated/api"; import OpenAI from "openai"; 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; }, });