Add document AI gen support
This commit is contained in:
parent
16007cc3aa
commit
028bab23fd
11 changed files with 1270 additions and 170 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import { GoogleGenAI, Type } from "@google/genai";
|
||||
import { Quiz, Question, AnswerOption } from "../types";
|
||||
import { GoogleGenAI, Type, createUserContent, createPartFromUri } from "@google/genai";
|
||||
import { Quiz, Question, AnswerOption, GenerateQuizOptions, ProcessedDocument } from "../types";
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const getClient = () => {
|
||||
|
|
@ -10,61 +10,62 @@ const getClient = () => {
|
|||
return new GoogleGenAI({ apiKey });
|
||||
};
|
||||
|
||||
export const generateQuiz = async (topic: string): Promise<Quiz> => {
|
||||
const ai = getClient();
|
||||
|
||||
const prompt = `Generate a trivia quiz about "${topic}". Create 10 engaging multiple-choice questions. Each question must have exactly 4 options, and exactly one correct answer. Vary the difficulty. For each option, provide a brief reason explaining why it is correct or incorrect - this helps players learn.`;
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model: "gemini-3-flash-preview",
|
||||
contents: prompt,
|
||||
config: {
|
||||
responseMimeType: "application/json",
|
||||
responseSchema: {
|
||||
const QUIZ_SCHEMA = {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
title: { type: Type.STRING, description: "A catchy title for the quiz" },
|
||||
questions: {
|
||||
type: Type.ARRAY,
|
||||
items: {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
title: { type: Type.STRING, description: "A catchy title for the quiz" },
|
||||
questions: {
|
||||
text: { type: Type.STRING, description: "The question text" },
|
||||
options: {
|
||||
type: Type.ARRAY,
|
||||
items: {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
text: { type: Type.STRING, description: "The question text" },
|
||||
options: {
|
||||
type: Type.ARRAY,
|
||||
items: {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
text: { type: Type.STRING },
|
||||
isCorrect: { type: Type.BOOLEAN },
|
||||
reason: { type: Type.STRING, description: "Brief explanation of why this answer is correct or incorrect" }
|
||||
},
|
||||
required: ["text", "isCorrect", "reason"]
|
||||
},
|
||||
}
|
||||
text: { type: Type.STRING },
|
||||
isCorrect: { type: Type.BOOLEAN },
|
||||
reason: { type: Type.STRING, description: "Brief explanation of why this answer is correct or incorrect" }
|
||||
},
|
||||
required: ["text", "options"]
|
||||
}
|
||||
required: ["text", "isCorrect", "reason"]
|
||||
},
|
||||
}
|
||||
},
|
||||
required: ["title", "questions"]
|
||||
required: ["text", "options"]
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
required: ["title", "questions"]
|
||||
};
|
||||
|
||||
if (!response.text) {
|
||||
throw new Error("Failed to generate quiz content");
|
||||
}
|
||||
|
||||
const data = JSON.parse(response.text);
|
||||
function buildPrompt(options: GenerateQuizOptions, hasDocuments: boolean): string {
|
||||
const questionCount = options.questionCount || 10;
|
||||
|
||||
const baseInstructions = `Create ${questionCount} engaging multiple-choice questions. Each question must have exactly 4 options, and exactly one correct answer. Vary the difficulty.
|
||||
|
||||
IMPORTANT: For each option's reason, write as if you are directly explaining facts - never reference "the document", "the text", "the material", or "the source". Write explanations as standalone factual statements.`;
|
||||
|
||||
if (hasDocuments) {
|
||||
const topicContext = options.topic
|
||||
? ` Focus on aspects related to "${options.topic}".`
|
||||
: '';
|
||||
return `Generate a quiz based on the provided content.${topicContext}
|
||||
|
||||
${baseInstructions}`;
|
||||
}
|
||||
|
||||
return `Generate a trivia quiz about "${options.topic}".
|
||||
|
||||
${baseInstructions}`;
|
||||
}
|
||||
|
||||
function transformToQuiz(data: any): Quiz {
|
||||
const shapes = ['triangle', 'diamond', 'circle', 'square'] as const;
|
||||
const colors = ['red', 'blue', 'yellow', 'green'] as const;
|
||||
|
||||
// Transform to our internal type with shapes/colors pre-assigned
|
||||
const questions: Question[] = data.questions.map((q: any) => {
|
||||
const shapes = ['triangle', 'diamond', 'circle', 'square'] as const;
|
||||
const colors = ['red', 'blue', 'yellow', 'green'] as const;
|
||||
|
||||
// Shuffle options so the correct one isn't always first (though Gemini usually randomizes, safety first)
|
||||
// Actually, to map shapes consistently, let's keep array order but assign props
|
||||
const options: AnswerOption[] = q.options.map((opt: any, index: number) => ({
|
||||
text: opt.text,
|
||||
isCorrect: opt.isCorrect,
|
||||
|
|
@ -85,4 +86,67 @@ export const generateQuiz = async (topic: string): Promise<Quiz> => {
|
|||
title: data.title,
|
||||
questions
|
||||
};
|
||||
}
|
||||
|
||||
async function uploadNativeDocument(ai: GoogleGenAI, doc: ProcessedDocument): Promise<{ uri: string; mimeType: string }> {
|
||||
const buffer = typeof doc.content === 'string'
|
||||
? Buffer.from(doc.content, 'base64')
|
||||
: doc.content;
|
||||
|
||||
const blob = new Blob([buffer], { type: doc.mimeType });
|
||||
|
||||
const uploadedFile = await ai.files.upload({
|
||||
file: blob,
|
||||
config: { mimeType: doc.mimeType! }
|
||||
});
|
||||
|
||||
if (!uploadedFile.uri || !uploadedFile.mimeType) {
|
||||
throw new Error("Failed to upload document to Gemini");
|
||||
}
|
||||
|
||||
return { uri: uploadedFile.uri, mimeType: uploadedFile.mimeType };
|
||||
}
|
||||
|
||||
export const generateQuiz = async (options: GenerateQuizOptions): Promise<Quiz> => {
|
||||
const ai = getClient();
|
||||
|
||||
const docs = options.documents || [];
|
||||
const hasDocuments = docs.length > 0;
|
||||
const prompt = buildPrompt(options, hasDocuments);
|
||||
|
||||
let contents: any;
|
||||
|
||||
if (hasDocuments) {
|
||||
const parts: any[] = [];
|
||||
|
||||
for (const doc of docs) {
|
||||
if (doc.type === 'native' && doc.mimeType) {
|
||||
const uploaded = await uploadNativeDocument(ai, doc);
|
||||
parts.push(createPartFromUri(uploaded.uri, uploaded.mimeType));
|
||||
} else if (doc.type === 'text') {
|
||||
parts.push({ text: doc.content as string });
|
||||
}
|
||||
}
|
||||
|
||||
parts.push({ text: prompt });
|
||||
contents = createUserContent(parts);
|
||||
} else {
|
||||
contents = prompt;
|
||||
}
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model: "gemini-3-flash-preview",
|
||||
contents,
|
||||
config: {
|
||||
responseMimeType: "application/json",
|
||||
responseSchema: QUIZ_SCHEMA
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.text) {
|
||||
throw new Error("Failed to generate quiz content");
|
||||
}
|
||||
|
||||
const data = JSON.parse(response.text);
|
||||
return transformToQuiz(data);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue