118 lines
3.3 KiB
TypeScript
118 lines
3.3 KiB
TypeScript
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;
|
|
},
|
|
});
|