last commit before trying out opencode on repo
This commit is contained in:
@@ -7,9 +7,9 @@ A query function that takes two arguments looks like:
|
||||
|
||||
```ts
|
||||
// functions.js
|
||||
import { v } from "convex/values";
|
||||
import { v } from 'convex/values';
|
||||
|
||||
import { query } from "./_generated/server";
|
||||
import { query } from './_generated/server';
|
||||
|
||||
export const myQueryFunction = query({
|
||||
// Validators for arguments.
|
||||
@@ -22,7 +22,7 @@ export const myQueryFunction = query({
|
||||
handler: async (ctx, args) => {
|
||||
// Read the database as many times as you need here.
|
||||
// See https://docs.convex.dev/database/reading-data.
|
||||
const documents = await ctx.db.query("tablename").collect();
|
||||
const documents = await ctx.db.query('tablename').collect();
|
||||
|
||||
// Arguments passed from the client are properties of the args object.
|
||||
console.log(args.first, args.second);
|
||||
@@ -39,7 +39,7 @@ Using this query function in a React component looks like:
|
||||
```ts
|
||||
const data = useQuery(api.functions.myQueryFunction, {
|
||||
first: 10,
|
||||
second: "hello",
|
||||
second: 'hello',
|
||||
});
|
||||
```
|
||||
|
||||
@@ -47,9 +47,9 @@ A mutation function looks like:
|
||||
|
||||
```ts
|
||||
// functions.js
|
||||
import { v } from "convex/values";
|
||||
import { v } from 'convex/values';
|
||||
|
||||
import { mutation } from "./_generated/server";
|
||||
import { mutation } from './_generated/server';
|
||||
|
||||
export const myMutationFunction = mutation({
|
||||
// Validators for arguments.
|
||||
@@ -64,7 +64,7 @@ export const myMutationFunction = mutation({
|
||||
// Mutations can also read from the database like queries.
|
||||
// See https://docs.convex.dev/database/writing-data.
|
||||
const message = { body: args.first, author: args.second };
|
||||
const id = await ctx.db.insert("messages", message);
|
||||
const id = await ctx.db.insert('messages', message);
|
||||
|
||||
// Optionally, return a value from your mutation.
|
||||
return await ctx.db.get(id);
|
||||
@@ -78,10 +78,10 @@ Using this mutation function in a React component looks like:
|
||||
const mutation = useMutation(api.functions.myMutationFunction);
|
||||
function handleButtonPress() {
|
||||
// fire and forget, the most common way to use mutations
|
||||
mutation({ first: "Hello!", second: "me" });
|
||||
mutation({ first: 'Hello!', second: 'me' });
|
||||
// OR
|
||||
// use the result once the mutation has completed
|
||||
mutation({ first: "Hello!", second: "me" }).then((result) =>
|
||||
mutation({ first: 'Hello!', second: 'me' }).then((result) =>
|
||||
console.log(result),
|
||||
);
|
||||
}
|
||||
|
||||
4
packages/backend/convex/_generated/api.d.ts
vendored
4
packages/backend/convex/_generated/api.d.ts
vendored
@@ -14,7 +14,7 @@ import type * as custom_auth_providers_password from "../custom/auth/providers/p
|
||||
import type * as custom_auth_providers_usesend from "../custom/auth/providers/usesend.js";
|
||||
import type * as http from "../http.js";
|
||||
import type * as questions from "../questions.js";
|
||||
import type * as utils from "../utils.js";
|
||||
import type * as quiz from "../quiz.js";
|
||||
|
||||
import type {
|
||||
ApiFromModules,
|
||||
@@ -29,7 +29,7 @@ declare const fullApi: ApiFromModules<{
|
||||
"custom/auth/providers/usesend": typeof custom_auth_providers_usesend;
|
||||
http: typeof http;
|
||||
questions: typeof questions;
|
||||
utils: typeof utils;
|
||||
quiz: typeof quiz;
|
||||
}>;
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@ export default {
|
||||
providers: [
|
||||
{
|
||||
domain: process.env.CONVEX_SITE_URL,
|
||||
applicationID: "convex",
|
||||
applicationID: 'convex',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,32 +1,25 @@
|
||||
import { ConvexError, v } from 'convex/values';
|
||||
import Authentik from '@auth/core/providers/authentik';
|
||||
import {
|
||||
convexAuth,
|
||||
getAuthUserId,
|
||||
retrieveAccount,
|
||||
modifyAccountCredentials,
|
||||
retrieveAccount,
|
||||
} from '@convex-dev/auth/server';
|
||||
import { api } from './_generated/api';
|
||||
import { ConvexError, v } from 'convex/values';
|
||||
|
||||
import type { Doc, Id } from './_generated/dataModel';
|
||||
import {
|
||||
action,
|
||||
mutation,
|
||||
query,
|
||||
type MutationCtx,
|
||||
type QueryCtx,
|
||||
} from './_generated/server';
|
||||
import Authentik from '@auth/core/providers/authentik';
|
||||
import type { MutationCtx, QueryCtx } from './_generated/server';
|
||||
import { api } from './_generated/api';
|
||||
import { action, mutation, query } from './_generated/server';
|
||||
import { Password, validatePassword } from './custom/auth';
|
||||
|
||||
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
|
||||
providers: [
|
||||
Authentik({ allowDangerousEmailAccountLinking: true }),
|
||||
Password,
|
||||
],
|
||||
providers: [Authentik({ allowDangerousEmailAccountLinking: true }), Password],
|
||||
});
|
||||
|
||||
const getUserById = async (
|
||||
ctx: QueryCtx,
|
||||
userId: Id<'users'>
|
||||
userId: Id<'users'>,
|
||||
): Promise<Doc<'users'>> => {
|
||||
const user = await ctx.db.get(userId);
|
||||
if (!user) throw new ConvexError('User not found.');
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Password as DefaultPassword } from '@convex-dev/auth/providers/Password';
|
||||
import { DataModel } from '../../../_generated/dataModel';
|
||||
import { UseSendOTP, UseSendOTPPasswordReset } from '..';
|
||||
import { ConvexError } from 'convex/values';
|
||||
|
||||
import { UseSendOTP, UseSendOTPPasswordReset } from '..';
|
||||
import { DataModel } from '../../../_generated/dataModel';
|
||||
|
||||
export const Password = DefaultPassword<DataModel>({
|
||||
profile(params, ctx) {
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { EmailConfig, EmailUserConfig } from '@auth/core/providers/email';
|
||||
import { alphabet } from 'oslo/crypto';
|
||||
import { generateRandomString, RandomReader } from '@oslojs/crypto/random';
|
||||
import { alphabet } from 'oslo/crypto';
|
||||
import { UseSend } from 'usesend-js';
|
||||
|
||||
export default function UseSendProvider(config: EmailUserConfig): EmailConfig {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { httpRouter } from 'convex/server';
|
||||
|
||||
import { auth } from './auth';
|
||||
|
||||
const http = httpRouter();
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
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";
|
||||
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,
|
||||
@@ -13,8 +14,8 @@ 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();
|
||||
if (!userId) throw new Error('Not authenticated');
|
||||
return await ctx.db.query('questions').collect();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -22,10 +23,10 @@ export const getQuestionsByTopic = query({
|
||||
args: { topic: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await getAuthUserId(ctx);
|
||||
if (!userId) throw new Error("Not authenticated");
|
||||
if (!userId) throw new Error('Not authenticated');
|
||||
return await ctx.db
|
||||
.query("questions")
|
||||
.withIndex("by_topic", (q) => q.eq("topic", args.topic))
|
||||
.query('questions')
|
||||
.withIndex('by_topic', (q) => q.eq('topic', args.topic))
|
||||
.collect();
|
||||
},
|
||||
});
|
||||
@@ -41,8 +42,8 @@ export const addQuestion = mutation({
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await getAuthUserId(ctx);
|
||||
if (!userId) throw new Error("Not authenticated");
|
||||
return await ctx.db.insert("questions", {
|
||||
if (!userId) throw new Error('Not authenticated');
|
||||
return await ctx.db.insert('questions', {
|
||||
question: args.question,
|
||||
options: args.options,
|
||||
correctAnswer: args.correctAnswer,
|
||||
@@ -60,7 +61,7 @@ export const generateQuestions = action({
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await getAuthUserId(ctx);
|
||||
if (!userId) throw new Error("Not authenticated");
|
||||
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:
|
||||
@@ -83,19 +84,19 @@ Rules:
|
||||
- Return ONLY the JSON array, no other text`;
|
||||
|
||||
const response = await openai.chat.completions.create({
|
||||
model: "gpt-4o-mini",
|
||||
messages: [{ role: "user", content: prompt }],
|
||||
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");
|
||||
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");
|
||||
throw new Error('Failed to parse AI response as JSON');
|
||||
}
|
||||
|
||||
const questionIds = [];
|
||||
|
||||
236
packages/backend/convex/quiz.ts
Normal file
236
packages/backend/convex/quiz.ts
Normal file
@@ -0,0 +1,236 @@
|
||||
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,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defineSchema, defineTable } from "convex/server";
|
||||
import { v, VId } from "convex/values";
|
||||
import { authTables } from '@convex-dev/auth/server';
|
||||
import { defineSchema, defineTable } from 'convex/server';
|
||||
import { v, VId } from 'convex/values';
|
||||
|
||||
const applicationTables = {
|
||||
questions: defineTable({
|
||||
@@ -10,8 +10,7 @@ const applicationTables = {
|
||||
topic: v.string(),
|
||||
difficulty: v.optional(v.string()),
|
||||
explanation: v.optional(v.string()),
|
||||
})
|
||||
.index('by_topic', ['topic']),
|
||||
}).index('by_topic', ['topic']),
|
||||
|
||||
userProgress: defineTable({
|
||||
userId: v.id('users'),
|
||||
@@ -30,8 +29,7 @@ const applicationTables = {
|
||||
completedQuestions: v.array(v.id('questions')),
|
||||
currentQuestionIndex: v.number(),
|
||||
isActive: v.boolean(),
|
||||
})
|
||||
.index('by_user_and_active', ['userId', 'isActive']),
|
||||
}).index('by_user_and_active', ['userId', 'isActive']),
|
||||
};
|
||||
|
||||
export default defineSchema({
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
export function missingEnvVariableUrl(envVarName: string, whereToGet: string) {
|
||||
const deployment = deploymentName();
|
||||
if (!deployment) return `Missing ${envVarName} in environment variables.`;
|
||||
return (
|
||||
`\n Missing ${envVarName} in environment variables.\n\n` +
|
||||
` Get it from ${whereToGet} .\n Paste it on the Convex dashboard:\n` +
|
||||
` https://dashboard.convex.dev/d/${deployment}/settings?var=${envVarName}`
|
||||
);
|
||||
}
|
||||
|
||||
export function deploymentName() {
|
||||
const url = process.env.CONVEX_CLOUD_URL;
|
||||
if (!url) return undefined;
|
||||
const regex = new RegExp("https://(.+).convex.cloud");
|
||||
return regex.exec(url)?.[1];
|
||||
}
|
||||
Reference in New Issue
Block a user