Add api key and sorting on scoreboard

This commit is contained in:
Joey Yakimowich-Payne 2026-01-15 14:49:10 -07:00
commit 4688a73559
No known key found for this signature in database
GPG key ID: 6BFE655FA5ABD1E1
14 changed files with 791 additions and 227 deletions

View file

@ -10,9 +10,14 @@ const getGeminiClient = (apiKey?: string) => {
return new GoogleGenAI({ apiKey: key });
};
const DEFAULT_GEMINI_MODEL = 'gemini-3-flash-preview';
const OPENROUTER_API_URL = 'https://openrouter.ai/api/v1/chat/completions';
const DEFAULT_OPENROUTER_MODEL = 'google/gemini-3-flash-preview';
const OPENAI_API_URL = 'https://api.openai.com/v1/chat/completions';
const DEFAULT_OPENAI_MODEL = 'gpt-5-mini';
const QUIZ_SCHEMA = {
type: Type.OBJECT,
properties: {
@ -173,6 +178,39 @@ const JSON_SCHEMA_FOR_OPENROUTER = {
required: ["title", "questions"]
};
const JSON_SCHEMA_FOR_OPENAI = {
type: "object",
properties: {
title: { type: "string", description: "A catchy title for the quiz" },
questions: {
type: "array",
items: {
type: "object",
properties: {
text: { type: "string", description: "The question text" },
options: {
type: "array",
items: {
type: "object",
properties: {
text: { type: "string" },
isCorrect: { type: "boolean" },
reason: { type: "string", description: "Brief explanation of why this answer is correct or incorrect" }
},
required: ["text", "isCorrect", "reason"],
additionalProperties: false
},
}
},
required: ["text", "options"],
additionalProperties: false
}
}
},
required: ["title", "questions"],
additionalProperties: false
};
async function generateQuizWithGemini(options: GenerateQuizOptions): Promise<Quiz> {
const ai = getGeminiClient(options.apiKey);
@ -200,8 +238,10 @@ async function generateQuizWithGemini(options: GenerateQuizOptions): Promise<Qui
contents = prompt;
}
const model = options.geminiModel || DEFAULT_GEMINI_MODEL;
const response = await ai.models.generateContent({
model: "gemini-3-flash-preview",
model,
contents,
config: {
responseMimeType: "application/json",
@ -294,6 +334,78 @@ async function generateQuizWithOpenRouter(options: GenerateQuizOptions): Promise
return transformToQuiz(data);
}
async function generateQuizWithOpenAI(options: GenerateQuizOptions): Promise<Quiz> {
const apiKey = options.apiKey;
if (!apiKey) {
throw new Error("OpenAI API key is missing");
}
const docs = options.documents || [];
const hasDocuments = docs.length > 0;
const prompt = buildPrompt(options, hasDocuments, true);
let fullPrompt = prompt;
if (hasDocuments) {
const textParts: string[] = [];
for (const doc of docs) {
if (doc.type === 'text') {
textParts.push(doc.content as string);
} else if (doc.type === 'native') {
console.warn('Native document type not supported with OpenAI - document will be skipped');
}
}
if (textParts.length > 0) {
fullPrompt = `Here is the content to create a quiz from:\n\n${textParts.join('\n\n---\n\n')}\n\n${prompt}`;
}
}
const model = options.openAIModel || DEFAULT_OPENAI_MODEL;
const response = await fetch(OPENAI_API_URL, {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model,
messages: [
{
role: 'user',
content: fullPrompt
}
],
response_format: {
type: 'json_schema',
json_schema: {
name: 'quiz',
strict: true,
schema: JSON_SCHEMA_FOR_OPENAI
}
}
})
});
if (!response.ok) {
const error = await response.json().catch(() => ({ error: { message: 'Unknown error' } }));
throw new Error(error.error?.message || `OpenAI API error: ${response.status}`);
}
const result = await response.json();
const content = result.choices?.[0]?.message?.content;
// console.log('[OpenAI] Raw response:', content);
if (!content) {
throw new Error("Failed to generate quiz content from OpenAI");
}
const data = JSON.parse(content);
return transformToQuiz(data);
}
export const generateQuiz = async (options: GenerateQuizOptions): Promise<Quiz> => {
const provider = options.aiProvider || 'gemini';
@ -301,5 +413,9 @@ export const generateQuiz = async (options: GenerateQuizOptions): Promise<Quiz>
return generateQuizWithOpenRouter(options);
}
if (provider === 'openai') {
return generateQuizWithOpenAI(options);
}
return generateQuizWithGemini(options);
};