@thanh01.pmt/interactive-quiz-kit 1.0.8 → 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1346,6 +1346,8 @@ var emptyQuiz = {
1346
1346
  description: "",
1347
1347
  questions: [],
1348
1348
  settings: {
1349
+ language: "English",
1350
+ // <-- ĐÃ THÊM
1349
1351
  shuffleQuestions: false,
1350
1352
  shuffleOptions: false,
1351
1353
  showCorrectAnswers: "end_of_quiz",
@@ -1408,35 +1410,257 @@ var exportQuizAsSCORMZip = async (quiz, options) => {
1408
1410
  }
1409
1411
  };
1410
1412
 
1411
- // src/ai/genkit.ts
1412
- import { genkit } from "genkit";
1413
- import { googleAI } from "@genkit-ai/googleai";
1414
- var ai = genkit({
1415
- plugins: [googleAI()],
1416
- model: "googleai/gemini-2.0-flash"
1417
- });
1413
+ // src/services/APIKeyService.ts
1414
+ var GEMINI_API_KEY_SERVICE_NAME = "gemini";
1415
+ var LOCAL_STORAGE_PREFIX = "iqk_api_keys_";
1416
+ function _encode(data) {
1417
+ if (typeof window !== "undefined" && typeof window.btoa === "function") {
1418
+ try {
1419
+ return window.btoa(data);
1420
+ } catch (e) {
1421
+ console.error("Base64 encoding (btoa) failed:", e);
1422
+ return data;
1423
+ }
1424
+ }
1425
+ return data;
1426
+ }
1427
+ function _decode(data) {
1428
+ if (typeof window !== "undefined" && typeof window.atob === "function") {
1429
+ try {
1430
+ return window.atob(data);
1431
+ } catch (e) {
1432
+ console.error("Base64 decoding (atob) failed:", e);
1433
+ return data;
1434
+ }
1435
+ }
1436
+ return data;
1437
+ }
1438
+ var APIKeyService = class {
1439
+ static getStorageKey(serviceName) {
1440
+ return `${LOCAL_STORAGE_PREFIX}${serviceName}`;
1441
+ }
1442
+ /**
1443
+ * Saves an API key to localStorage. The key is mildly obfuscated using Base64.
1444
+ * @param serviceName - The name of the service (e.g., 'gemini').
1445
+ * @param apiKey - The API key to save.
1446
+ */
1447
+ static saveAPIKey(serviceName, apiKey) {
1448
+ if (typeof window !== "undefined" && window.localStorage) {
1449
+ try {
1450
+ const encodedKey = _encode(apiKey);
1451
+ localStorage.setItem(this.getStorageKey(serviceName), encodedKey);
1452
+ } catch (e) {
1453
+ console.error(`Error saving API key for ${serviceName} to localStorage:`, e);
1454
+ }
1455
+ } else {
1456
+ console.warn("localStorage is not available. APIKeyService cannot save keys.");
1457
+ }
1458
+ }
1459
+ /**
1460
+ * Retrieves an API key from localStorage.
1461
+ * @param serviceName - The name of the service.
1462
+ * @returns The decoded API key, or null if not found or if localStorage is unavailable.
1463
+ */
1464
+ static getAPIKey(serviceName) {
1465
+ if (typeof window !== "undefined" && window.localStorage) {
1466
+ try {
1467
+ const storedKey = localStorage.getItem(this.getStorageKey(serviceName));
1468
+ if (storedKey) {
1469
+ return _decode(storedKey);
1470
+ }
1471
+ } catch (e) {
1472
+ console.error(`Error retrieving API key for ${serviceName} from localStorage:`, e);
1473
+ }
1474
+ } else {
1475
+ }
1476
+ return null;
1477
+ }
1478
+ /**
1479
+ * Removes an API key from localStorage.
1480
+ * @param serviceName - The name of the service.
1481
+ */
1482
+ static removeAPIKey(serviceName) {
1483
+ if (typeof window !== "undefined" && window.localStorage) {
1484
+ try {
1485
+ localStorage.removeItem(this.getStorageKey(serviceName));
1486
+ } catch (e) {
1487
+ console.error(`Error removing API key for ${serviceName} from localStorage:`, e);
1488
+ }
1489
+ } else {
1490
+ }
1491
+ }
1492
+ /**
1493
+ * Checks if an API key exists in localStorage for the given service.
1494
+ * @param serviceName - The name of the service.
1495
+ * @returns True if a key exists, false otherwise.
1496
+ */
1497
+ static hasAPIKey(serviceName) {
1498
+ return this.getAPIKey(serviceName) !== null;
1499
+ }
1500
+ };
1501
+
1502
+ // src/services/QuizEditorService.ts
1503
+ var QuizEditorService = class {
1504
+ constructor(initialQuiz) {
1505
+ this.quiz = JSON.parse(JSON.stringify(initialQuiz));
1506
+ }
1507
+ /**
1508
+ * Returns the current state of the quiz configuration.
1509
+ * @returns The current QuizConfig object.
1510
+ */
1511
+ getQuiz() {
1512
+ return this.quiz;
1513
+ }
1514
+ /**
1515
+ * Creates a new, "empty" question object based on the specified question type.
1516
+ * @param type The type of question to create.
1517
+ * @returns A new QuizQuestion object with default values.
1518
+ */
1519
+ static createNewQuestionTemplate(type) {
1520
+ const baseNewQuestion = {
1521
+ id: generateUniqueId(`new_${type}_`),
1522
+ // 'new_' prefix indicates it's a new, unsaved question
1523
+ questionType: type,
1524
+ prompt: "",
1525
+ points: 10,
1526
+ difficulty: "medium"
1527
+ };
1528
+ switch (type) {
1529
+ case "true_false":
1530
+ return __spreadProps(__spreadValues({}, baseNewQuestion), { questionType: "true_false", correctAnswer: true });
1531
+ case "multiple_choice":
1532
+ return __spreadProps(__spreadValues({}, baseNewQuestion), { questionType: "multiple_choice", options: [], correctAnswerId: "" });
1533
+ case "multiple_response":
1534
+ return __spreadProps(__spreadValues({}, baseNewQuestion), { questionType: "multiple_response", options: [], correctAnswerIds: [] });
1535
+ case "short_answer":
1536
+ return __spreadProps(__spreadValues({}, baseNewQuestion), { questionType: "short_answer", acceptedAnswers: [""], isCaseSensitive: false });
1537
+ case "numeric":
1538
+ return __spreadProps(__spreadValues({}, baseNewQuestion), { questionType: "numeric", answer: 0 });
1539
+ case "fill_in_the_blanks": {
1540
+ const blankId = generateUniqueId("blank_");
1541
+ return __spreadProps(__spreadValues({}, baseNewQuestion), {
1542
+ questionType: "fill_in_the_blanks",
1543
+ segments: [
1544
+ { type: "text", content: "Your text before " },
1545
+ { type: "blank", id: blankId },
1546
+ { type: "text", content: " and after." }
1547
+ ],
1548
+ answers: [{ blankId, acceptedValues: [""] }],
1549
+ isCaseSensitive: false
1550
+ });
1551
+ }
1552
+ case "sequence":
1553
+ return __spreadProps(__spreadValues({}, baseNewQuestion), { questionType: "sequence", items: [], correctOrder: [] });
1554
+ case "matching":
1555
+ return __spreadProps(__spreadValues({}, baseNewQuestion), { questionType: "matching", prompts: [], options: [], correctAnswerMap: [], shuffleOptions: true });
1556
+ case "drag_and_drop":
1557
+ return __spreadProps(__spreadValues({}, baseNewQuestion), { questionType: "drag_and_drop", draggableItems: [], dropZones: [], answerMap: [] });
1558
+ case "hotspot":
1559
+ return __spreadProps(__spreadValues({}, baseNewQuestion), { questionType: "hotspot", imageUrl: "", hotspots: [], correctHotspotIds: [] });
1560
+ case "blockly_programming":
1561
+ return __spreadProps(__spreadValues({}, baseNewQuestion), {
1562
+ questionType: "blockly_programming",
1563
+ toolboxDefinition: '<xml xmlns="https://developers.google.com/blockly/xml"></xml>',
1564
+ initialWorkspace: "",
1565
+ solutionWorkspaceXML: "",
1566
+ solutionGeneratedCode: ""
1567
+ });
1568
+ case "scratch_programming":
1569
+ return __spreadProps(__spreadValues({}, baseNewQuestion), {
1570
+ questionType: "scratch_programming",
1571
+ toolboxDefinition: '<xml xmlns="https://developers.google.com/blockly/xml"></xml>',
1572
+ initialWorkspace: "",
1573
+ solutionWorkspaceXML: "",
1574
+ solutionGeneratedCode: ""
1575
+ });
1576
+ default:
1577
+ const _exhaustiveCheck = type;
1578
+ throw new Error(`Question type "${_exhaustiveCheck}" is not supported for creation.`);
1579
+ }
1580
+ }
1581
+ /**
1582
+ * Adds a new question to the quiz. If the question ID is temporary, a new permanent ID is generated.
1583
+ * @param question The question object to add.
1584
+ * @returns The updated QuizConfig.
1585
+ */
1586
+ addQuestion(question) {
1587
+ const newQuestion = __spreadValues({}, question);
1588
+ if (newQuestion.id.startsWith("new_")) {
1589
+ newQuestion.id = generateUniqueId(`${newQuestion.questionType}_`);
1590
+ }
1591
+ this.quiz.questions.push(newQuestion);
1592
+ return this.quiz;
1593
+ }
1594
+ /**
1595
+ * Updates an existing question in the quiz.
1596
+ * @param updatedQuestion The full question object with changes.
1597
+ * @returns The updated QuizConfig.
1598
+ */
1599
+ updateQuestion(updatedQuestion) {
1600
+ const questionIndex = this.quiz.questions.findIndex((q) => q.id === updatedQuestion.id);
1601
+ if (questionIndex === -1) {
1602
+ throw new Error(`Question with ID "${updatedQuestion.id}" not found.`);
1603
+ }
1604
+ this.quiz.questions[questionIndex] = updatedQuestion;
1605
+ return this.quiz;
1606
+ }
1607
+ /**
1608
+ * Deletes a question from the quiz by its index.
1609
+ * @param index The index of the question to delete.
1610
+ * @returns The updated QuizConfig.
1611
+ */
1612
+ deleteQuestionByIndex(index) {
1613
+ if (index < 0 || index >= this.quiz.questions.length) {
1614
+ throw new Error(`Invalid index ${index} for question deletion.`);
1615
+ }
1616
+ this.quiz.questions.splice(index, 1);
1617
+ return this.quiz;
1618
+ }
1619
+ /**
1620
+ * Moves a question from one position to another.
1621
+ * @param fromIndex The current index of the question.
1622
+ * @param toIndex The target index for the question.
1623
+ * @returns The updated QuizConfig.
1624
+ */
1625
+ moveQuestion(fromIndex, toIndex) {
1626
+ if (fromIndex < 0 || fromIndex >= this.quiz.questions.length || toIndex < 0 || toIndex >= this.quiz.questions.length) {
1627
+ throw new Error("Invalid index for moving question.");
1628
+ }
1629
+ const [movedItem] = this.quiz.questions.splice(fromIndex, 1);
1630
+ this.quiz.questions.splice(toIndex, 0, movedItem);
1631
+ return this.quiz;
1632
+ }
1633
+ };
1418
1634
 
1419
1635
  // src/ai/flows/generate-fitb-question.ts
1420
- import { z } from "genkit";
1421
- var GenerateFillInTheBlanksQuestionInputSchema = z.object({
1636
+ import { z } from "zod";
1637
+ import { genkit } from "genkit";
1638
+ import { googleAI, gemini20Flash } from "@genkit-ai/googleai";
1639
+ function extractJsonFromMarkdown(text) {
1640
+ const match = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
1641
+ return match ? match[1].trim() : text.trim();
1642
+ }
1643
+ var GenerateFillInTheBlanksQuestionClientInputSchema = z.object({
1422
1644
  topic: z.string().describe("The topic for the question."),
1645
+ language: z.string().optional().default("English").describe('The language for the generated question (e.g., "Vietnamese", "English").'),
1646
+ // <-- ĐÃ THÊM
1423
1647
  difficulty: z.enum(["easy", "medium", "hard"]).optional().default("medium"),
1424
1648
  numberOfBlanks: z.number().int().min(1).max(5).optional().default(1).describe("Number of blanks to include (1-5)."),
1425
1649
  isCaseSensitive: z.boolean().optional().default(false).describe("Whether answers should be case-sensitive."),
1426
- contextDescription: z.string().optional().describe("A specific context or scenario for the question, complementing the main topic."),
1427
- selectedContextId: z.string().optional().describe("The ID of the selected context, if any.")
1650
+ contextDescription: z.string().optional().describe("A specific context or scenario for the question."),
1651
+ selectedContextId: z.string().optional().describe("The ID of the selected context.")
1428
1652
  });
1429
1653
  var AIFillInTheBlanksOutputFieldsSchema = z.object({
1430
- prompt: z.string().describe("The overall instruction for the question (e.g., 'Fill in the blanks in the sentence below.')."),
1431
- sentenceWithPlaceholders: z.string().describe("The sentence containing placeholders for blanks, e.g., 'The capital of {{country}} is {{city}}.' or 'Water is made of {{element1}} and {{element2}}.' Placeholders should be distinct and clearly marked, e.g., using {{placeholder_name}} format."),
1654
+ prompt: z.string().describe("The overall instruction for the question."),
1655
+ sentenceWithPlaceholders: z.string().describe("The sentence containing placeholders like {{placeholder_name}}."),
1432
1656
  blanks: z.array(
1433
1657
  z.object({
1434
- placeholder: z.string().describe("The exact placeholder string used in 'sentenceWithPlaceholders' (e.g., 'country', 'element1'). Do not include the curly braces."),
1435
- acceptedAnswers: z.array(z.string()).min(1).describe("An array of acceptable answers for this blank.")
1658
+ placeholder: z.string().describe("The placeholder name (without curly braces)."),
1659
+ acceptedAnswers: z.array(z.string().min(1)).min(1).describe("Array of acceptable answers.")
1436
1660
  })
1437
- ).min(1).describe("An array defining each placeholder and its accepted answers."),
1438
- isCaseSensitive: z.boolean().optional().describe("Should answer evaluation be case sensitive? Defaults to input or false."),
1439
- explanation: z.string().optional().describe("Explanation for the correct answer(s)."),
1661
+ ).min(1),
1662
+ isCaseSensitive: z.boolean().optional(),
1663
+ explanation: z.string().optional(),
1440
1664
  points: z.number().optional().default(10),
1441
1665
  difficulty: z.enum(["easy", "medium", "hard"]).optional(),
1442
1666
  topic: z.string().optional()
@@ -1444,149 +1668,169 @@ var AIFillInTheBlanksOutputFieldsSchema = z.object({
1444
1668
  var FillInTheBlanksQuestionZodSchema = z.object({
1445
1669
  id: z.string(),
1446
1670
  questionType: z.literal("fill_in_the_blanks"),
1447
- prompt: z.string(),
1448
- segments: z.array(z.object({ type: z.enum(["text", "blank"]), content: z.string().optional(), id: z.string().optional() })),
1449
- answers: z.array(z.object({ blankId: z.string(), acceptedValues: z.array(z.string()) })),
1671
+ prompt: z.string().min(1),
1672
+ segments: z.array(z.object({
1673
+ type: z.enum(["text", "blank"]),
1674
+ content: z.string().optional(),
1675
+ // Only for 'text' type
1676
+ id: z.string().optional()
1677
+ // Only for 'blank' type
1678
+ })).min(1),
1679
+ answers: z.array(z.object({
1680
+ blankId: z.string(),
1681
+ acceptedValues: z.array(z.string().min(1)).min(1)
1682
+ })).min(1),
1450
1683
  isCaseSensitive: z.boolean().optional(),
1451
- points: z.number().optional(),
1452
- explanation: z.string().optional(),
1453
- learningObjective: z.string().optional(),
1454
- glossary: z.array(z.string()).optional(),
1455
- bloomLevel: z.string().optional(),
1456
- difficulty: z.enum(["easy", "medium", "hard"]).optional(),
1457
- contextCode: z.string().optional(),
1458
- gradeBand: z.string().optional(),
1459
- course: z.string().optional(),
1460
- category: z.string().optional(),
1461
- topic: z.string().optional()
1684
+ points: z.number().min(0).optional(),
1685
+ explanation: z.string().optional()
1686
+ // ... other fields
1687
+ }).refine((data) => {
1688
+ const segmentBlankIds = new Set(data.segments.filter((s) => s.type === "blank").map((s) => s.id));
1689
+ const answerBlankIds = new Set(data.answers.map((a) => a.blankId));
1690
+ if (segmentBlankIds.size !== answerBlankIds.size) return false;
1691
+ for (const id of segmentBlankIds) {
1692
+ if (!answerBlankIds.has(id || "")) return false;
1693
+ }
1694
+ return true;
1695
+ }, {
1696
+ message: "There must be a 1-to-1 correspondence between blank segments and answer definitions.",
1697
+ path: ["answers"]
1462
1698
  });
1463
1699
  var GenerateFillInTheBlanksQuestionOutputSchema = z.object({
1464
- question: FillInTheBlanksQuestionZodSchema.optional().describe("The generated Fill-In-The-Blanks question.")
1700
+ question: FillInTheBlanksQuestionZodSchema.optional().describe("The generated question.")
1465
1701
  });
1466
- async function generateFillInTheBlanksQuestion(input) {
1467
- return generateFillInTheBlanksQuestionFlow(input);
1702
+ async function generateFillInTheBlanksQuestion(clientInput, apiKey) {
1703
+ var _a;
1704
+ try {
1705
+ const ai = genkit({
1706
+ plugins: [googleAI({ apiKey })],
1707
+ model: gemini20Flash
1708
+ });
1709
+ const promptText = `You are an expert quiz question writer.
1710
+ Generate a single Fill-In-The-Blanks question in ${clientInput.language} with approximately ${clientInput.numberOfBlanks} blank(s).
1711
+
1712
+ IMPORTANT: Return the response as JSON with this EXACT format:
1713
+ {
1714
+ "prompt": "Complete the famous saying.",
1715
+ "sentenceWithPlaceholders": "Roses are {{color1}}, violets are {{color2}}.",
1716
+ "blanks": [
1717
+ { "placeholder": "color1", "acceptedAnswers": ["red"] },
1718
+ { "placeholder": "color2", "acceptedAnswers": ["blue"] }
1719
+ ],
1720
+ "isCaseSensitive": false,
1721
+ "explanation": "This is a classic nursery rhyme.",
1722
+ "points": 10,
1723
+ "difficulty": "easy",
1724
+ "topic": "Nursery Rhymes"
1468
1725
  }
1469
- var fitbAIPrompt = ai.definePrompt({
1470
- name: "fitbQuestionGeneratorPrompt",
1471
- input: { schema: GenerateFillInTheBlanksQuestionInputSchema },
1472
- output: { schema: AIFillInTheBlanksOutputFieldsSchema },
1473
- prompt: `You are an expert quiz question writer.
1474
- Generate a single Fill-In-The-Blanks question with approximately {{{numberOfBlanks}}} blanks.
1475
- Provide:
1476
- 1. An overall 'prompt' or instruction.
1477
- 2. A 'sentenceWithPlaceholders' where blanks are clearly marked with double curly braces, e.g., "{{blank_name}}". Each placeholder name inside the braces must be unique.
1478
- 3. A 'blanks' array, where each object defines a 'placeholder' (matching a name from the sentence, without braces) and its 'acceptedAnswers'.
1479
- 4. Specify 'isCaseSensitive' (defaulting to {{{isCaseSensitive}}}).
1480
- 5. Optional 'explanation', 'points' (default 10), refined 'topic', and 'difficulty'.
1481
-
1482
- Topic: {{{topic}}}
1483
- {{#if contextDescription}}
1484
- Context: {{{contextDescription}}}
1485
- {{/if}}
1486
- Difficulty: {{{difficulty}}}
1487
- Target Number of Blanks: {{{numberOfBlanks}}}
1488
- Case Sensitive: {{{isCaseSensitive}}}
1489
-
1490
- Example for 'sentenceWithPlaceholders': "The {{color_of_sky}} sky is beautiful, and the {{type_of_grass}} grass is green."
1491
- Example for 'blanks' array entry for the above: { "placeholder": "color_of_sky", "acceptedAnswers": ["blue"] }
1492
-
1493
- Ensure your output strictly matches the requested JSON schema. The number of entries in the 'blanks' array should correspond to the number of unique placeholders in 'sentenceWithPlaceholders'.
1494
- `
1495
- });
1496
- var generateFillInTheBlanksQuestionFlow = ai.defineFlow(
1497
- {
1498
- name: "generateFillInTheBlanksQuestionFlow",
1499
- inputSchema: GenerateFillInTheBlanksQuestionInputSchema,
1500
- outputSchema: GenerateFillInTheBlanksQuestionOutputSchema
1501
- },
1502
- async (input) => {
1503
- try {
1504
- const { output: aiGeneratedContent } = await fitbAIPrompt(input);
1505
- if (aiGeneratedContent) {
1506
- const segments = [];
1507
- const answers = [];
1508
- const placeholderToBlankIdMap = {};
1509
- aiGeneratedContent.blanks.forEach((blankInfo) => {
1510
- const blankId = generateUniqueId("blank_");
1511
- placeholderToBlankIdMap[blankInfo.placeholder] = blankId;
1512
- answers.push({
1513
- blankId,
1514
- acceptedValues: blankInfo.acceptedAnswers
1515
- });
1726
+
1727
+ Requirements:
1728
+ - Use double curly braces for placeholders, like {{placeholder_name}}.
1729
+ - Each placeholder name inside the braces must be unique.
1730
+ - The 'blanks' array must define the accepted answers for every unique placeholder in the sentence.
1731
+ - The 'placeholder' value in the 'blanks' array should NOT include the curly braces.
1732
+ - The content of 'prompt', 'sentenceWithPlaceholders', 'blanks.acceptedAnswers', 'explanation', and 'topic' must be in ${clientInput.language}.
1733
+
1734
+ Topic: ${clientInput.topic}
1735
+ Language: ${clientInput.language}
1736
+ Difficulty: ${clientInput.difficulty}
1737
+ Target Number of Blanks: ${clientInput.numberOfBlanks}
1738
+ Case Sensitive: ${clientInput.isCaseSensitive}
1739
+
1740
+ Return only the JSON response.`;
1741
+ const response = await ai.generate(promptText);
1742
+ const rawText = response.text;
1743
+ const jsonText = extractJsonFromMarkdown(rawText);
1744
+ console.log("AI Response:", jsonText);
1745
+ const aiGeneratedContent = JSON.parse(jsonText);
1746
+ if (aiGeneratedContent) {
1747
+ const segments = [];
1748
+ const answers = [];
1749
+ const placeholderToBlankIdMap = {};
1750
+ aiGeneratedContent.blanks.forEach((blankInfo) => {
1751
+ const blankId = generateUniqueId("blank_");
1752
+ placeholderToBlankIdMap[blankInfo.placeholder] = blankId;
1753
+ answers.push({
1754
+ blankId,
1755
+ acceptedValues: blankInfo.acceptedAnswers
1516
1756
  });
1517
- let remainingSentence = aiGeneratedContent.sentenceWithPlaceholders;
1518
- const placeholderRegex = /\{\{([^}]+)\}\}/g;
1519
- let lastIndex = 0;
1520
- let match;
1521
- while ((match = placeholderRegex.exec(remainingSentence)) !== null) {
1522
- const placeholderName = match[1];
1523
- const blankId = placeholderToBlankIdMap[placeholderName];
1524
- if (match.index > lastIndex) {
1525
- segments.push({ type: "text", content: remainingSentence.substring(lastIndex, match.index) });
1526
- }
1527
- if (blankId) {
1528
- segments.push({ type: "blank", id: blankId });
1529
- } else {
1530
- console.warn(`Placeholder {{${placeholderName}}} found in sentence but not in blanks definition. Treating as text.`);
1531
- segments.push({ type: "text", content: match[0] });
1532
- }
1533
- lastIndex = placeholderRegex.lastIndex;
1534
- }
1535
- if (lastIndex < remainingSentence.length) {
1536
- segments.push({ type: "text", content: remainingSentence.substring(lastIndex) });
1537
- }
1538
- if (answers.length === 0 && segments.some((s) => s.type === "blank")) {
1539
- throw new Error("AI generated blank segments but no corresponding answer definitions.");
1757
+ });
1758
+ const placeholderRegex = /\{\{([^}]+)\}\}/g;
1759
+ let lastIndex = 0;
1760
+ let match;
1761
+ while ((match = placeholderRegex.exec(aiGeneratedContent.sentenceWithPlaceholders)) !== null) {
1762
+ const placeholderName = match[1];
1763
+ const blankId = placeholderToBlankIdMap[placeholderName];
1764
+ if (match.index > lastIndex) {
1765
+ segments.push({ type: "text", content: aiGeneratedContent.sentenceWithPlaceholders.substring(lastIndex, match.index) });
1540
1766
  }
1541
- if (segments.filter((s) => s.type === "blank").length !== answers.length) {
1542
- console.warn(`Mismatch between number of blank segments (${segments.filter((s) => s.type === "blank").length}) and answer definitions (${answers.length}). Review AI output.`);
1543
- if (answers.length === 0 && segments.filter((s) => s.type === "blank").length > 0) {
1544
- throw new Error("AI generated blank segments but no answer definitions for them.");
1545
- }
1767
+ if (blankId) {
1768
+ segments.push({ type: "blank", id: blankId });
1769
+ } else {
1770
+ console.warn(`Placeholder {{${placeholderName}}} found in sentence but not defined in blanks array. Treating as literal text.`);
1771
+ segments.push({ type: "text", content: match[0] });
1546
1772
  }
1547
- const completeQuestion = {
1548
- id: generateUniqueId("fitb_ai_"),
1549
- questionType: "fill_in_the_blanks",
1550
- prompt: aiGeneratedContent.prompt,
1551
- segments,
1552
- answers,
1553
- isCaseSensitive: aiGeneratedContent.isCaseSensitive === void 0 ? input.isCaseSensitive : aiGeneratedContent.isCaseSensitive,
1554
- explanation: aiGeneratedContent.explanation,
1555
- points: aiGeneratedContent.points,
1556
- topic: aiGeneratedContent.topic || input.topic,
1557
- difficulty: aiGeneratedContent.difficulty || input.difficulty,
1558
- contextCode: input.contextDescription ? input.selectedContextId : void 0
1559
- };
1560
- return { question: completeQuestion };
1561
- } else {
1562
- throw new Error("AI did not return content for the Fill-In-The-Blanks question.");
1773
+ lastIndex = placeholderRegex.lastIndex;
1563
1774
  }
1564
- } catch (error) {
1565
- console.error("Error generating Fill-In-The-Blanks question in flow:", error);
1566
- throw new Error(`Failed to generate Fill-In-The-Blanks question: ${error.message}`);
1775
+ if (lastIndex < aiGeneratedContent.sentenceWithPlaceholders.length) {
1776
+ segments.push({ type: "text", content: aiGeneratedContent.sentenceWithPlaceholders.substring(lastIndex) });
1777
+ }
1778
+ const completeQuestion = {
1779
+ id: generateUniqueId("fitb_ai_"),
1780
+ questionType: "fill_in_the_blanks",
1781
+ prompt: aiGeneratedContent.prompt,
1782
+ segments,
1783
+ answers,
1784
+ isCaseSensitive: (_a = aiGeneratedContent.isCaseSensitive) != null ? _a : clientInput.isCaseSensitive,
1785
+ explanation: aiGeneratedContent.explanation,
1786
+ points: aiGeneratedContent.points,
1787
+ topic: aiGeneratedContent.topic || clientInput.topic,
1788
+ difficulty: aiGeneratedContent.difficulty || clientInput.difficulty,
1789
+ contextCode: clientInput.contextDescription ? clientInput.selectedContextId : void 0
1790
+ };
1791
+ try {
1792
+ const validatedQuestion = FillInTheBlanksQuestionZodSchema.parse(completeQuestion);
1793
+ return { question: validatedQuestion };
1794
+ } catch (validationError) {
1795
+ console.error("Question validation failed:", validationError);
1796
+ throw new Error(`Generated question failed validation: ${validationError}`);
1797
+ }
1798
+ } else {
1799
+ throw new Error("AI did not return content for the Fill-In-The-Blanks question.");
1567
1800
  }
1801
+ } catch (error) {
1802
+ console.error("Error generating Fill-In-The-Blanks question:", error);
1803
+ throw new Error(`Failed to generate Fill-In-The-Blanks question: ${error.message}`);
1568
1804
  }
1569
- );
1805
+ }
1570
1806
 
1571
1807
  // src/ai/flows/generate-matching-question.ts
1572
- import { z as z2 } from "genkit";
1573
- var GenerateMatchingQuestionInputSchema = z2.object({
1808
+ import { z as z2 } from "zod";
1809
+ import { genkit as genkit2 } from "genkit";
1810
+ import { googleAI as googleAI2, gemini20Flash as gemini20Flash2 } from "@genkit-ai/googleai";
1811
+ function extractJsonFromMarkdown2(text) {
1812
+ const match = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
1813
+ return match ? match[1].trim() : text.trim();
1814
+ }
1815
+ var GenerateMatchingQuestionClientInputSchema = z2.object({
1574
1816
  topic: z2.string().describe("The topic for the question."),
1817
+ language: z2.string().optional().default("English").describe('The language for the generated question (e.g., "Vietnamese", "English").'),
1818
+ // <-- ĐÃ THÊM
1575
1819
  difficulty: z2.enum(["easy", "medium", "hard"]).optional().default("medium"),
1576
1820
  numberOfPairs: z2.number().int().min(2).max(8).optional().default(4).describe("Number of pairs to match (2-8)."),
1577
- shuffleOptions: z2.boolean().optional().default(true).describe("Whether the options should be shuffled for the user."),
1578
- contextDescription: z2.string().optional().describe("A specific context or scenario for the question, complementing the main topic."),
1579
- selectedContextId: z2.string().optional().describe("The ID of the selected context, if any.")
1821
+ shuffleOptions: z2.boolean().optional().default(true).describe("Whether the options should be shuffled."),
1822
+ contextDescription: z2.string().optional().describe("A specific context or scenario for the question."),
1823
+ selectedContextId: z2.string().optional().describe("The ID of the selected context.")
1580
1824
  });
1581
1825
  var AIMatchingOutputFieldsSchema = z2.object({
1582
1826
  prompt: z2.string().describe("The overall instruction (e.g., 'Match the terms to their definitions.')."),
1583
1827
  correctPairs: z2.array(
1584
1828
  z2.object({
1585
- promptText: z2.string().describe("The text for a prompt item (e.g., a term)."),
1586
- optionText: z2.string().describe("The text for the corresponding option item (e.g., its definition).")
1829
+ promptText: z2.string().min(1).describe("The text for a prompt item (e.g., a term)."),
1830
+ optionText: z2.string().min(1).describe("The text for the corresponding option item (e.g., its definition).")
1587
1831
  })
1588
- ).min(2).describe("An array of objects, each representing a correct prompt-option pair."),
1589
- explanation: z2.string().optional().describe("General explanation if needed."),
1832
+ ).min(2),
1833
+ explanation: z2.string().optional(),
1590
1834
  points: z2.number().optional().default(10),
1591
1835
  difficulty: z2.enum(["easy", "medium", "hard"]).optional(),
1592
1836
  topic: z2.string().optional()
@@ -1594,121 +1838,130 @@ var AIMatchingOutputFieldsSchema = z2.object({
1594
1838
  var MatchingQuestionZodSchema = z2.object({
1595
1839
  id: z2.string(),
1596
1840
  questionType: z2.literal("matching"),
1597
- prompt: z2.string(),
1598
- prompts: z2.array(z2.object({ id: z2.string(), content: z2.string() })),
1599
- options: z2.array(z2.object({ id: z2.string(), content: z2.string() })),
1600
- correctAnswerMap: z2.array(z2.object({ promptId: z2.string(), optionId: z2.string() })),
1841
+ prompt: z2.string().min(1),
1842
+ prompts: z2.array(z2.object({ id: z2.string(), content: z2.string().min(1) })).min(2),
1843
+ options: z2.array(z2.object({ id: z2.string(), content: z2.string().min(1) })).min(2),
1844
+ correctAnswerMap: z2.array(z2.object({ promptId: z2.string(), optionId: z2.string() })).min(2),
1601
1845
  shuffleOptions: z2.boolean().optional(),
1602
- points: z2.number().optional(),
1603
- explanation: z2.string().optional(),
1604
- learningObjective: z2.string().optional(),
1605
- glossary: z2.array(z2.string()).optional(),
1606
- bloomLevel: z2.string().optional(),
1607
- difficulty: z2.enum(["easy", "medium", "hard"]).optional(),
1608
- contextCode: z2.string().optional(),
1609
- gradeBand: z2.string().optional(),
1610
- course: z2.string().optional(),
1611
- category: z2.string().optional(),
1612
- topic: z2.string().optional()
1846
+ points: z2.number().min(0).optional(),
1847
+ explanation: z2.string().optional()
1848
+ // ... other fields
1849
+ }).refine((data) => {
1850
+ const promptIds = new Set(data.prompts.map((p) => p.id));
1851
+ const optionIds = new Set(data.options.map((o) => o.id));
1852
+ return data.correctAnswerMap.every(
1853
+ (map) => promptIds.has(map.promptId) && optionIds.has(map.optionId)
1854
+ );
1855
+ }, {
1856
+ message: "All IDs in correctAnswerMap must exist in the prompts and options arrays.",
1857
+ path: ["correctAnswerMap"]
1858
+ }).refine((data) => {
1859
+ const mappedPromptIds = new Set(data.correctAnswerMap.map((m) => m.promptId));
1860
+ return mappedPromptIds.size === data.prompts.length && data.correctAnswerMap.length === data.prompts.length;
1861
+ }, {
1862
+ message: "Each prompt must be mapped exactly once in the correctAnswerMap.",
1863
+ path: ["correctAnswerMap"]
1613
1864
  });
1614
1865
  var GenerateMatchingQuestionOutputSchema = z2.object({
1615
1866
  question: MatchingQuestionZodSchema.optional().describe("The generated Matching question.")
1616
1867
  });
1617
- async function generateMatchingQuestion(input) {
1618
- return generateMatchingQuestionFlow(input);
1868
+ async function generateMatchingQuestion(clientInput, apiKey) {
1869
+ try {
1870
+ const ai = genkit2({
1871
+ plugins: [googleAI2({ apiKey })],
1872
+ model: gemini20Flash2
1873
+ });
1874
+ const promptText = `You are an expert quiz question writer.
1875
+ Generate a single Matching question in ${clientInput.language} with exactly ${clientInput.numberOfPairs} correct pairs.
1876
+
1877
+ IMPORTANT: Return the response as JSON with this EXACT format:
1878
+ {
1879
+ "prompt": "Match each country to its capital city.",
1880
+ "correctPairs": [
1881
+ { "promptText": "France", "optionText": "Paris" },
1882
+ { "promptText": "Japan", "optionText": "Tokyo" },
1883
+ { "promptText": "Egypt", "optionText": "Cairo" },
1884
+ { "promptText": "Brazil", "optionText": "Bras\xEDlia" }
1885
+ ],
1886
+ "explanation": "These are the capital cities for the respective countries.",
1887
+ "points": 10,
1888
+ "difficulty": "medium",
1889
+ "topic": "World Geography"
1619
1890
  }
1620
- var matchingAIPrompt = ai.definePrompt({
1621
- name: "matchingQuestionGeneratorPrompt",
1622
- input: { schema: GenerateMatchingQuestionInputSchema },
1623
- output: { schema: AIMatchingOutputFieldsSchema },
1624
- prompt: `You are an expert quiz question writer.
1625
- Generate a single Matching question with exactly {{{numberOfPairs}}} correct pairs.
1626
- Provide:
1627
- 1. An overall 'prompt' or instruction.
1628
- 2. A 'correctPairs' array: each object in this array should contain a 'promptText' and its corresponding 'optionText'. These represent items that correctly match.
1629
- 3. Optional 'explanation', 'points' (default 10), refined 'topic', and 'difficulty'.
1630
-
1631
- Topic: {{{topic}}}
1632
- {{#if contextDescription}}
1633
- Context: {{{contextDescription}}}
1634
- {{/if}}
1635
- Difficulty: {{{difficulty}}}
1636
- Number of Pairs: {{{numberOfPairs}}}
1637
-
1638
- Ensure your output strictly matches the requested JSON schema. 'correctPairs' must have {{{numberOfPairs}}} elements.
1639
- The promptText and optionText within each pair should be distinct and meaningful for a matching question.
1640
- `
1641
- });
1642
- var generateMatchingQuestionFlow = ai.defineFlow(
1643
- {
1644
- name: "generateMatchingQuestionFlow",
1645
- inputSchema: GenerateMatchingQuestionInputSchema,
1646
- outputSchema: GenerateMatchingQuestionOutputSchema
1647
- },
1648
- async (input) => {
1649
- try {
1650
- const { output: aiGeneratedContent } = await matchingAIPrompt(input);
1651
- if (aiGeneratedContent) {
1652
- if (aiGeneratedContent.correctPairs.length !== input.numberOfPairs) {
1653
- throw new Error(`AI generated ${aiGeneratedContent.correctPairs.length} pairs, but ${input.numberOfPairs} were requested.`);
1654
- }
1655
- const finalPrompts = [];
1656
- const finalOptions = [];
1657
- const finalCorrectAnswerMap = [];
1658
- const promptTextToId = {};
1659
- const optionTextToId = {};
1660
- aiGeneratedContent.correctPairs.forEach((pair) => {
1661
- let promptId = promptTextToId[pair.promptText];
1662
- if (!promptId) {
1663
- promptId = generateUniqueId("m_p_");
1664
- promptTextToId[pair.promptText] = promptId;
1665
- finalPrompts.push({ id: promptId, content: pair.promptText });
1666
- }
1667
- let optionId = optionTextToId[pair.optionText];
1668
- if (!optionId) {
1669
- optionId = generateUniqueId("m_o_");
1670
- optionTextToId[pair.optionText] = optionId;
1671
- finalOptions.push({ id: optionId, content: pair.optionText });
1672
- }
1673
- if (!finalCorrectAnswerMap.find((m) => m.promptId === promptId && m.optionId === optionId)) {
1674
- finalCorrectAnswerMap.push({ promptId, optionId });
1675
- }
1676
- });
1677
- if (finalPrompts.length < input.numberOfPairs || finalOptions.length < input.numberOfPairs) {
1678
- console.warn(`AI generated ${finalPrompts.length} unique prompts and ${finalOptions.length} unique options for ${input.numberOfPairs} requested pairs. This might lead to an unbalanced matching question or indicate duplicate content from AI.`);
1679
- }
1680
- if (finalCorrectAnswerMap.length !== input.numberOfPairs) {
1681
- throw new Error(`Could not form the required ${input.numberOfPairs} unique mappings from AI output. AI provided ${finalCorrectAnswerMap.length} valid mappings.`);
1682
- }
1683
- const completeQuestion = {
1684
- id: generateUniqueId("match_ai_"),
1685
- questionType: "matching",
1686
- prompt: aiGeneratedContent.prompt,
1687
- prompts: finalPrompts,
1688
- options: finalOptions,
1689
- correctAnswerMap: finalCorrectAnswerMap,
1690
- shuffleOptions: input.shuffleOptions,
1691
- explanation: aiGeneratedContent.explanation,
1692
- points: aiGeneratedContent.points,
1693
- topic: aiGeneratedContent.topic || input.topic,
1694
- difficulty: aiGeneratedContent.difficulty || input.difficulty,
1695
- contextCode: input.contextDescription ? input.selectedContextId : void 0
1696
- };
1697
- return { question: completeQuestion };
1698
- } else {
1699
- throw new Error("AI did not return content for the Matching question.");
1891
+
1892
+ Requirements:
1893
+ - The 'correctPairs' array must contain exactly ${clientInput.numberOfPairs} objects.
1894
+ - Each object in 'correctPairs' must have a 'promptText' and its corresponding 'optionText'.
1895
+ - Ensure the content of all 'promptText' values are unique.
1896
+ - Ensure the content of all 'optionText' values are unique.
1897
+ - The content of 'prompt', 'correctPairs.promptText', 'correctPairs.optionText', 'explanation', and 'topic' must be in ${clientInput.language}.
1898
+
1899
+ Topic: ${clientInput.topic}
1900
+ Language: ${clientInput.language}
1901
+ Difficulty: ${clientInput.difficulty}
1902
+ Number of Pairs: ${clientInput.numberOfPairs}
1903
+
1904
+ Return only the JSON response.`;
1905
+ const response = await ai.generate(promptText);
1906
+ const rawText = response.text;
1907
+ const jsonText = extractJsonFromMarkdown2(rawText);
1908
+ console.log("AI Response:", jsonText);
1909
+ const aiGeneratedContent = JSON.parse(jsonText);
1910
+ if (aiGeneratedContent) {
1911
+ if (aiGeneratedContent.correctPairs.length !== clientInput.numberOfPairs) {
1912
+ throw new Error(`AI generated ${aiGeneratedContent.correctPairs.length} pairs, but ${clientInput.numberOfPairs} were requested.`);
1700
1913
  }
1701
- } catch (error) {
1702
- console.error("Error generating Matching question in flow:", error);
1703
- throw new Error(`Failed to generate Matching question: ${error.message}`);
1914
+ const finalPrompts = [];
1915
+ const finalOptions = [];
1916
+ const finalCorrectAnswerMap = [];
1917
+ aiGeneratedContent.correctPairs.forEach((pair) => {
1918
+ const promptId = generateUniqueId("m_p_");
1919
+ const optionId = generateUniqueId("m_o_");
1920
+ finalPrompts.push({ id: promptId, content: pair.promptText });
1921
+ finalOptions.push({ id: optionId, content: pair.optionText });
1922
+ finalCorrectAnswerMap.push({ promptId, optionId });
1923
+ });
1924
+ const completeQuestion = {
1925
+ id: generateUniqueId("match_ai_"),
1926
+ questionType: "matching",
1927
+ prompt: aiGeneratedContent.prompt,
1928
+ prompts: finalPrompts,
1929
+ options: finalOptions,
1930
+ correctAnswerMap: finalCorrectAnswerMap,
1931
+ shuffleOptions: clientInput.shuffleOptions,
1932
+ explanation: aiGeneratedContent.explanation,
1933
+ points: aiGeneratedContent.points,
1934
+ topic: aiGeneratedContent.topic || clientInput.topic,
1935
+ difficulty: aiGeneratedContent.difficulty || clientInput.difficulty,
1936
+ contextCode: clientInput.contextDescription ? clientInput.selectedContextId : void 0
1937
+ };
1938
+ try {
1939
+ const validatedQuestion = MatchingQuestionZodSchema.parse(completeQuestion);
1940
+ return { question: validatedQuestion };
1941
+ } catch (validationError) {
1942
+ console.error("Question validation failed:", validationError);
1943
+ throw new Error(`Generated question failed validation: ${validationError}`);
1944
+ }
1945
+ } else {
1946
+ throw new Error("AI did not return content for the Matching question.");
1704
1947
  }
1948
+ } catch (error) {
1949
+ console.error("Error generating Matching question:", error);
1950
+ throw new Error(`Failed to generate Matching question: ${error.message}`);
1705
1951
  }
1706
- );
1952
+ }
1707
1953
 
1708
1954
  // src/ai/flows/generate-mcq-question.ts
1709
- import { z as z3 } from "genkit";
1710
- var GenerateMCQQuestionInputSchema = z3.object({
1955
+ import { z as z3 } from "zod";
1956
+ import { genkit as genkit3 } from "genkit";
1957
+ import { googleAI as googleAI3, gemini20Flash as gemini20Flash3 } from "@genkit-ai/googleai";
1958
+ function extractJsonFromMarkdown3(text) {
1959
+ return text.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "");
1960
+ }
1961
+ var GenerateMCQQuestionClientInputSchema = z3.object({
1711
1962
  topic: z3.string().describe("The topic for the question."),
1963
+ language: z3.string().optional().default("English").describe('The language for the generated question (e.g., "Vietnamese", "English").'),
1964
+ // <-- ĐÃ THÊM
1712
1965
  difficulty: z3.enum(["easy", "medium", "hard"]).optional().default("medium"),
1713
1966
  numberOfOptions: z3.number().int().min(2).max(6).optional().default(4).describe("Number of answer options to generate (2-6)."),
1714
1967
  contextDescription: z3.string().optional().describe("A specific context or scenario for the question, complementing the main topic."),
@@ -1721,7 +1974,7 @@ var AIMCQOutputFieldsSchema = z3.object({
1721
1974
  tempId: z3.string().describe("A temporary, unique identifier for this option (e.g., 'A', 'B', '1', '2')."),
1722
1975
  text: z3.string().describe("The text content of this answer option.")
1723
1976
  })
1724
- ).min(2).max(6).describe("An array of answer choices that matches the numberOfOptions specified in the input."),
1977
+ ).min(2).max(6),
1725
1978
  correctTempOptionId: z3.string().describe("The temporary ID of the correct option from the generated options array."),
1726
1979
  explanation: z3.string().optional().describe("A brief explanation of why the answer is correct."),
1727
1980
  points: z3.number().optional().default(10).describe("Points for a correct answer."),
@@ -1731,10 +1984,10 @@ var AIMCQOutputFieldsSchema = z3.object({
1731
1984
  var MultipleChoiceQuestionZodSchema = z3.object({
1732
1985
  id: z3.string(),
1733
1986
  questionType: z3.literal("multiple_choice"),
1734
- prompt: z3.string(),
1735
- options: z3.array(z3.object({ id: z3.string(), text: z3.string() })),
1987
+ prompt: z3.string().min(1),
1988
+ options: z3.array(z3.object({ id: z3.string(), text: z3.string().min(1) })).min(2).max(10),
1736
1989
  correctAnswerId: z3.string(),
1737
- points: z3.number().optional(),
1990
+ points: z3.number().min(0).optional(),
1738
1991
  explanation: z3.string().optional(),
1739
1992
  learningObjective: z3.string().optional(),
1740
1993
  glossary: z3.array(z3.string()).optional(),
@@ -1745,97 +1998,129 @@ var MultipleChoiceQuestionZodSchema = z3.object({
1745
1998
  course: z3.string().optional(),
1746
1999
  category: z3.string().optional(),
1747
2000
  topic: z3.string().optional()
2001
+ }).refine((data) => {
2002
+ return data.options.some((option) => option.id === data.correctAnswerId);
2003
+ }, {
2004
+ message: "correctAnswerId must match one of the option IDs",
2005
+ path: ["correctAnswerId"]
2006
+ }).refine((data) => {
2007
+ const optionIds = data.options.map((opt) => opt.id);
2008
+ return optionIds.length === new Set(optionIds).size;
2009
+ }, {
2010
+ message: "All option IDs must be unique",
2011
+ path: ["options"]
1748
2012
  });
1749
2013
  var GenerateMCQQuestionOutputSchema = z3.object({
1750
2014
  question: MultipleChoiceQuestionZodSchema.optional().describe("The generated Multiple Choice question.")
1751
2015
  });
1752
- async function generateMCQQuestion(input) {
1753
- return generateMCQQuestionFlow(input);
2016
+ async function generateMCQQuestion(clientInput, apiKey) {
2017
+ try {
2018
+ const ai = genkit3({
2019
+ plugins: [googleAI3({ apiKey })],
2020
+ model: gemini20Flash3
2021
+ });
2022
+ const promptText = `You are an expert quiz question writer.
2023
+ Generate a single Multiple Choice question in ${clientInput.language} based on the following inputs.
2024
+
2025
+ IMPORTANT: Return the response as JSON with this EXACT format:
2026
+ {
2027
+ "prompt": "Your question here",
2028
+ "options": [
2029
+ { "tempId": "A", "text": "First option text" },
2030
+ { "tempId": "B", "text": "Second option text" },
2031
+ { "tempId": "C", "text": "Third option text" },
2032
+ { "tempId": "D", "text": "Fourth option text" }
2033
+ ],
2034
+ "correctTempOptionId": "C",
2035
+ "explanation": "Brief explanation",
2036
+ "points": 10,
2037
+ "difficulty": "medium",
2038
+ "topic": "refined topic"
1754
2039
  }
1755
- var mcqAIPrompt = ai.definePrompt({
1756
- name: "mcqQuestionGeneratorPrompt",
1757
- input: { schema: GenerateMCQQuestionInputSchema },
1758
- output: { schema: AIMCQOutputFieldsSchema },
1759
- prompt: `You are an expert quiz question writer.
1760
- Generate a single Multiple Choice question based on the following inputs.
1761
- Provide the question statement (prompt), an array of options (each with a unique temporary ID like 'A', 'B', 'C' or 'option_1', 'option_2' and its text), the temporary ID of the correct option, an optional explanation, optional points (default 10), and optionally refine the topic and difficulty.
1762
- Ensure there are exactly {{{numberOfOptions}}} options. The temporary IDs for options must be unique within the 'options' array and one of them must match 'correctTempOptionId'.
1763
-
1764
- Topic: {{{topic}}}
1765
- {{#if contextDescription}}
1766
- Context: {{{contextDescription}}}
1767
- {{/if}}
1768
- Difficulty: {{{difficulty}}}
1769
- Number of Options: {{{numberOfOptions}}}
1770
-
1771
- Ensure your output strictly matches the requested JSON schema.
1772
- `
1773
- });
1774
- var generateMCQQuestionFlow = ai.defineFlow(
1775
- {
1776
- name: "generateMCQQuestionFlow",
1777
- inputSchema: GenerateMCQQuestionInputSchema,
1778
- outputSchema: GenerateMCQQuestionOutputSchema
1779
- },
1780
- async (input) => {
1781
- try {
1782
- const { output: aiGeneratedContent } = await mcqAIPrompt(input);
1783
- if (aiGeneratedContent) {
1784
- const finalOptions = [];
1785
- const tempIdToFinalIdMap = {};
1786
- aiGeneratedContent.options.forEach((aiOption) => {
1787
- const finalId = generateUniqueId("opt_");
1788
- finalOptions.push({ id: finalId, text: aiOption.text });
1789
- tempIdToFinalIdMap[aiOption.tempId] = finalId;
1790
- });
1791
- const finalCorrectAnswerId = tempIdToFinalIdMap[aiGeneratedContent.correctTempOptionId];
1792
- if (!finalCorrectAnswerId) {
1793
- const correctAiOption = aiGeneratedContent.options.find((o) => o.tempId === aiGeneratedContent.correctTempOptionId);
1794
- if (correctAiOption) {
1795
- } else {
1796
- const index = parseInt(aiGeneratedContent.correctTempOptionId);
1797
- if (!isNaN(index) && index >= 0 && index < finalOptions.length && finalOptions[index].id) {
1798
- } else {
1799
- throw new Error(`Correct option ID '${aiGeneratedContent.correctTempOptionId}' is invalid or does not match any generated option tempId.`);
1800
- }
1801
- }
1802
- if (!finalCorrectAnswerId) {
1803
- throw new Error(`Correct option ID '${aiGeneratedContent.correctTempOptionId}' is invalid or does not match any generated option tempId.`);
1804
- }
2040
+
2041
+ Requirements:
2042
+ - Generate exactly ${clientInput.numberOfOptions} options.
2043
+ - Use tempId values like "A", "B", "C", "D" or "option_1", "option_2", etc.
2044
+ - Make sure correctTempOptionId matches one of the tempId values in options array.
2045
+ - Each option must have both "tempId" and "text" fields.
2046
+ - The content of 'prompt', 'options.text', 'explanation', and 'topic' must be in ${clientInput.language}.
2047
+
2048
+ Topic: ${clientInput.topic}
2049
+ Language: ${clientInput.language}
2050
+ ${clientInput.contextDescription ? `Context: ${clientInput.contextDescription}` : ""}
2051
+ Difficulty: ${clientInput.difficulty}
2052
+ Number of Options: ${clientInput.numberOfOptions}
2053
+
2054
+ Return only the JSON response.`;
2055
+ const response = await ai.generate(promptText);
2056
+ const rawText = response.text;
2057
+ const jsonText = extractJsonFromMarkdown3(rawText);
2058
+ console.log("AI Response:", jsonText);
2059
+ let aiGeneratedContent = JSON.parse(jsonText);
2060
+ if (aiGeneratedContent.options && Array.isArray(aiGeneratedContent.options)) {
2061
+ const normalizedOptions = aiGeneratedContent.options.map((option, index) => {
2062
+ if (typeof option === "object" && !option.tempId) {
2063
+ const key = Object.keys(option)[0];
2064
+ const text = option[key];
2065
+ return { tempId: key, text };
1805
2066
  }
1806
- const completeQuestion = {
1807
- id: generateUniqueId("mcq_ai_"),
1808
- questionType: "multiple_choice",
1809
- prompt: aiGeneratedContent.prompt,
1810
- options: finalOptions,
1811
- correctAnswerId: finalCorrectAnswerId,
1812
- explanation: aiGeneratedContent.explanation,
1813
- points: aiGeneratedContent.points,
1814
- topic: aiGeneratedContent.topic || input.topic,
1815
- difficulty: aiGeneratedContent.difficulty || input.difficulty,
1816
- learningObjective: void 0,
1817
- glossary: void 0,
1818
- bloomLevel: void 0,
1819
- contextCode: input.contextDescription ? input.selectedContextId : void 0,
1820
- gradeBand: void 0,
1821
- course: void 0,
1822
- category: void 0
1823
- };
1824
- return { question: completeQuestion };
1825
- } else {
1826
- throw new Error("AI did not return content for the MCQ question.");
2067
+ return option;
2068
+ });
2069
+ aiGeneratedContent.options = normalizedOptions;
2070
+ }
2071
+ console.log("Normalized AI Content:", JSON.stringify(aiGeneratedContent, null, 2));
2072
+ if (aiGeneratedContent) {
2073
+ const finalOptions = [];
2074
+ const tempIdToFinalIdMap = {};
2075
+ aiGeneratedContent.options.forEach((aiOption) => {
2076
+ const finalId = generateUniqueId("opt_");
2077
+ finalOptions.push({ id: finalId, text: aiOption.text });
2078
+ tempIdToFinalIdMap[aiOption.tempId] = finalId;
2079
+ });
2080
+ const finalCorrectAnswerId = tempIdToFinalIdMap[aiGeneratedContent.correctTempOptionId];
2081
+ if (!finalCorrectAnswerId) {
2082
+ throw new Error(`Correct option ID '${aiGeneratedContent.correctTempOptionId}' is invalid or does not match any generated option tempId.`);
1827
2083
  }
1828
- } catch (error) {
1829
- console.error("Error generating MCQ question in flow:", error);
1830
- throw new Error(`Failed to generate MCQ question: ${error.message}`);
2084
+ const completeQuestion = {
2085
+ id: generateUniqueId("mcq_ai_"),
2086
+ questionType: "multiple_choice",
2087
+ prompt: aiGeneratedContent.prompt,
2088
+ options: finalOptions,
2089
+ correctAnswerId: finalCorrectAnswerId,
2090
+ explanation: aiGeneratedContent.explanation,
2091
+ points: aiGeneratedContent.points,
2092
+ topic: aiGeneratedContent.topic || clientInput.topic,
2093
+ difficulty: aiGeneratedContent.difficulty || clientInput.difficulty,
2094
+ contextCode: clientInput.contextDescription ? clientInput.selectedContextId : void 0
2095
+ };
2096
+ try {
2097
+ const validatedQuestion = MultipleChoiceQuestionZodSchema.parse(completeQuestion);
2098
+ return { question: validatedQuestion };
2099
+ } catch (validationError) {
2100
+ console.error("Question validation failed:", validationError);
2101
+ throw new Error(`Generated question failed validation: ${validationError}`);
2102
+ }
2103
+ } else {
2104
+ throw new Error("AI did not return content for the MCQ question.");
1831
2105
  }
2106
+ } catch (error) {
2107
+ console.error("Error generating MCQ question:", error);
2108
+ throw new Error(`Failed to generate MCQ question: ${error.message}`);
1832
2109
  }
1833
- );
2110
+ }
1834
2111
 
1835
2112
  // src/ai/flows/generate-mrq-question.ts
1836
- import { z as z4 } from "genkit";
1837
- var GenerateMRQQuestionInputSchema = z4.object({
2113
+ import { z as z4 } from "zod";
2114
+ import { genkit as genkit4 } from "genkit";
2115
+ import { googleAI as googleAI4, gemini20Flash as gemini20Flash4 } from "@genkit-ai/googleai";
2116
+ function extractJsonFromMarkdown4(text) {
2117
+ const match = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
2118
+ return match ? match[1].trim() : text.trim();
2119
+ }
2120
+ var GenerateMRQQuestionClientInputSchema = z4.object({
1838
2121
  topic: z4.string().describe("The topic for the question."),
2122
+ language: z4.string().optional().default("English").describe('The language for the generated question (e.g., "Vietnamese", "English").'),
2123
+ // <-- ĐÃ THÊM
1839
2124
  difficulty: z4.enum(["easy", "medium", "hard"]).optional().default("medium"),
1840
2125
  numberOfOptions: z4.number().int().min(2).max(8).optional().default(5).describe("Number of answer options to generate (2-8)."),
1841
2126
  minCorrectAnswers: z4.number().int().min(1).optional().default(2).describe("Minimum number of correct answers among the options."),
@@ -1850,8 +2135,8 @@ var AIMRQOutputFieldsSchema = z4.object({
1850
2135
  tempId: z4.string().describe("A temporary, unique identifier for this option (e.g., 'A', 'B')."),
1851
2136
  text: z4.string().describe("The text content of this answer option.")
1852
2137
  })
1853
- ).min(2).max(8).describe("An array of answer choices."),
1854
- correctTempOptionIds: z4.array(z4.string()).min(1).describe("An array of temporary IDs of the correct options from the generated options array."),
2138
+ ).min(2).max(8),
2139
+ correctTempOptionIds: z4.array(z4.string()).min(1).describe("An array of temporary IDs of the correct options."),
1855
2140
  explanation: z4.string().optional().describe("A brief explanation of why the answers are correct."),
1856
2141
  points: z4.number().optional().default(10).describe("Points for a correct answer."),
1857
2142
  difficulty: z4.enum(["easy", "medium", "hard"]).optional().describe("Assessed difficulty."),
@@ -1860,10 +2145,10 @@ var AIMRQOutputFieldsSchema = z4.object({
1860
2145
  var MultipleResponseQuestionZodSchema = z4.object({
1861
2146
  id: z4.string(),
1862
2147
  questionType: z4.literal("multiple_response"),
1863
- prompt: z4.string(),
1864
- options: z4.array(z4.object({ id: z4.string(), text: z4.string() })),
1865
- correctAnswerIds: z4.array(z4.string()),
1866
- points: z4.number().optional(),
2148
+ prompt: z4.string().min(1),
2149
+ options: z4.array(z4.object({ id: z4.string(), text: z4.string().min(1) })).min(2).max(10),
2150
+ correctAnswerIds: z4.array(z4.string()).min(1),
2151
+ points: z4.number().min(0).optional(),
1867
2152
  explanation: z4.string().optional(),
1868
2153
  learningObjective: z4.string().optional(),
1869
2154
  glossary: z4.array(z4.string()).optional(),
@@ -1874,105 +2159,148 @@ var MultipleResponseQuestionZodSchema = z4.object({
1874
2159
  course: z4.string().optional(),
1875
2160
  category: z4.string().optional(),
1876
2161
  topic: z4.string().optional()
2162
+ }).refine((data) => {
2163
+ const optionIds = new Set(data.options.map((option) => option.id));
2164
+ return data.correctAnswerIds.every((correctId) => optionIds.has(correctId));
2165
+ }, {
2166
+ message: "All correctAnswerIds must match one of the option IDs",
2167
+ path: ["correctAnswerIds"]
2168
+ }).refine((data) => {
2169
+ const optionIds = data.options.map((opt) => opt.id);
2170
+ return optionIds.length === new Set(optionIds).size;
2171
+ }, {
2172
+ message: "All option IDs must be unique",
2173
+ path: ["options"]
1877
2174
  });
1878
2175
  var GenerateMRQQuestionOutputSchema = z4.object({
1879
2176
  question: MultipleResponseQuestionZodSchema.optional().describe("The generated Multiple Response question.")
1880
2177
  });
1881
- async function generateMRQQuestion(input) {
1882
- if (input.minCorrectAnswers > input.maxCorrectAnswers) {
1883
- throw new Error("Minimum correct answers cannot exceed maximum correct answers.");
1884
- }
1885
- if (input.maxCorrectAnswers > input.numberOfOptions) {
1886
- throw new Error("Maximum correct answers cannot exceed the total number of options.");
1887
- }
1888
- return generateMRQQuestionFlow(input);
2178
+ async function generateMRQQuestion(clientInput, apiKey) {
2179
+ try {
2180
+ if (clientInput.minCorrectAnswers > clientInput.maxCorrectAnswers) {
2181
+ throw new Error("Minimum correct answers cannot exceed maximum correct answers.");
2182
+ }
2183
+ if (clientInput.maxCorrectAnswers >= clientInput.numberOfOptions) {
2184
+ throw new Error("Maximum correct answers must be less than the total number of options.");
2185
+ }
2186
+ const ai = genkit4({
2187
+ plugins: [googleAI4({ apiKey })],
2188
+ model: gemini20Flash4
2189
+ });
2190
+ const promptText = `You are an expert quiz question writer specializing in Multiple Response questions.
2191
+ Generate a single Multiple Response question in ${clientInput.language} based on the following inputs.
2192
+
2193
+ IMPORTANT: Return the response as JSON with this EXACT format:
2194
+ {
2195
+ "prompt": "Your question here (e.g., 'Which of the following are primary colors?')",
2196
+ "options": [
2197
+ { "tempId": "A", "text": "First option" },
2198
+ { "tempId": "B", "text": "Second option" },
2199
+ { "tempId": "C", "text": "Third option" },
2200
+ { "tempId": "D", "text": "Fourth option" },
2201
+ { "tempId": "E", "text": "Fifth option" }
2202
+ ],
2203
+ "correctTempOptionIds": ["A", "C"],
2204
+ "explanation": "Brief explanation of all correct answers.",
2205
+ "points": 10,
2206
+ "difficulty": "medium",
2207
+ "topic": "refined topic"
1889
2208
  }
1890
- var mrqAIPrompt = ai.definePrompt({
1891
- name: "mrqQuestionGeneratorPrompt",
1892
- input: { schema: GenerateMRQQuestionInputSchema },
1893
- output: { schema: AIMRQOutputFieldsSchema },
1894
- prompt: `You are an expert quiz question writer.
1895
- Generate a single Multiple Response question based on the following inputs.
1896
- Provide the question statement (prompt), an array of options (each with a unique temporary ID and its text), an array of the temporary IDs of the correct options, an optional explanation, optional points (default 10), and optionally refine the topic and difficulty.
1897
- Ensure there are exactly {{{numberOfOptions}}} options.
1898
- Ensure there are between {{{minCorrectAnswers}}} and {{{maxCorrectAnswers}}} correct options.
1899
- The temporary IDs for options must be unique within the 'options' array and all IDs in 'correctTempOptionIds' must match one of them.
1900
-
1901
- Topic: {{{topic}}}
1902
- {{#if contextDescription}}
1903
- Context: {{{contextDescription}}}
1904
- {{/if}}
1905
- Difficulty: {{{difficulty}}}
1906
- Number of Options: {{{numberOfOptions}}}
1907
- Min Correct Answers: {{{minCorrectAnswers}}}
1908
- Max Correct Answers: {{{maxCorrectAnswers}}}
1909
-
1910
- Ensure your output strictly matches the requested JSON schema.
1911
- `
1912
- });
1913
- var generateMRQQuestionFlow = ai.defineFlow(
1914
- {
1915
- name: "generateMRQQuestionFlow",
1916
- inputSchema: GenerateMRQQuestionInputSchema,
1917
- outputSchema: GenerateMRQQuestionOutputSchema
1918
- },
1919
- async (input) => {
1920
- try {
1921
- const { output: aiGeneratedContent } = await mrqAIPrompt(input);
1922
- if (aiGeneratedContent) {
1923
- const finalOptions = [];
1924
- const tempIdToFinalIdMap = {};
1925
- aiGeneratedContent.options.forEach((aiOption) => {
1926
- const finalId = generateUniqueId("opt_mr_");
1927
- finalOptions.push({ id: finalId, text: aiOption.text });
1928
- tempIdToFinalIdMap[aiOption.tempId] = finalId;
1929
- });
1930
- const finalCorrectAnswerIds = aiGeneratedContent.correctTempOptionIds.map((tempId) => {
1931
- const finalId = tempIdToFinalIdMap[tempId];
1932
- if (!finalId) {
1933
- throw new Error(`AI provided an invalid correctTempOptionId ('${tempId}') that does not map to any generated option.`);
1934
- }
1935
- return finalId;
1936
- });
1937
- if (finalCorrectAnswerIds.length < input.minCorrectAnswers || finalCorrectAnswerIds.length > input.maxCorrectAnswers) {
1938
- throw new Error(`AI generated ${finalCorrectAnswerIds.length} correct answers, which is outside the requested range of ${input.minCorrectAnswers}-${input.maxCorrectAnswers}.`);
2209
+
2210
+ Requirements:
2211
+ - Generate exactly ${clientInput.numberOfOptions} options.
2212
+ - Generate between ${clientInput.minCorrectAnswers} and ${clientInput.maxCorrectAnswers} correct answers.
2213
+ - Use tempId values like "A", "B", "C", etc.
2214
+ - Make sure all IDs in correctTempOptionIds match a tempId in the options array.
2215
+ - Each option must have both "tempId" and "text" fields.
2216
+ - The content of 'prompt', 'options.text', 'explanation', and 'topic' must be in ${clientInput.language}.
2217
+
2218
+ Topic: ${clientInput.topic}
2219
+ Language: ${clientInput.language}
2220
+ ${clientInput.contextDescription ? `Context: ${clientInput.contextDescription}` : ""}
2221
+ Difficulty: ${clientInput.difficulty}
2222
+ Number of Options: ${clientInput.numberOfOptions}
2223
+ Min Correct Answers: ${clientInput.minCorrectAnswers}
2224
+ Max Correct Answers: ${clientInput.maxCorrectAnswers}
2225
+
2226
+ Return only the JSON response.`;
2227
+ const response = await ai.generate(promptText);
2228
+ const rawText = response.text;
2229
+ const jsonText = extractJsonFromMarkdown4(rawText);
2230
+ console.log("AI Response:", jsonText);
2231
+ let aiGeneratedContent = JSON.parse(jsonText);
2232
+ if (aiGeneratedContent.options && Array.isArray(aiGeneratedContent.options)) {
2233
+ const normalizedOptions = aiGeneratedContent.options.map((option) => {
2234
+ if (typeof option === "object" && !option.tempId && Object.keys(option).length === 1) {
2235
+ const key = Object.keys(option)[0];
2236
+ return { tempId: key, text: option[key] };
1939
2237
  }
1940
- if (finalOptions.length !== input.numberOfOptions) {
1941
- throw new Error(`AI generated ${finalOptions.length} options, but ${input.numberOfOptions} were requested.`);
2238
+ return option;
2239
+ });
2240
+ aiGeneratedContent.options = normalizedOptions;
2241
+ }
2242
+ console.log("Normalized AI Content:", JSON.stringify(aiGeneratedContent, null, 2));
2243
+ if (aiGeneratedContent) {
2244
+ const finalOptions = [];
2245
+ const tempIdToFinalIdMap = {};
2246
+ aiGeneratedContent.options.forEach((aiOption) => {
2247
+ const finalId = generateUniqueId("opt_mr_");
2248
+ finalOptions.push({ id: finalId, text: aiOption.text });
2249
+ tempIdToFinalIdMap[aiOption.tempId] = finalId;
2250
+ });
2251
+ const finalCorrectAnswerIds = aiGeneratedContent.correctTempOptionIds.map((tempId) => {
2252
+ const finalId = tempIdToFinalIdMap[tempId];
2253
+ if (!finalId) {
2254
+ throw new Error(`AI provided an invalid correctTempOptionId ('${tempId}') that does not map to any generated option.`);
1942
2255
  }
1943
- const completeQuestion = {
1944
- id: generateUniqueId("mrq_ai_"),
1945
- questionType: "multiple_response",
1946
- prompt: aiGeneratedContent.prompt,
1947
- options: finalOptions,
1948
- correctAnswerIds: finalCorrectAnswerIds,
1949
- explanation: aiGeneratedContent.explanation,
1950
- points: aiGeneratedContent.points,
1951
- topic: aiGeneratedContent.topic || input.topic,
1952
- difficulty: aiGeneratedContent.difficulty || input.difficulty,
1953
- learningObjective: void 0,
1954
- glossary: void 0,
1955
- bloomLevel: void 0,
1956
- contextCode: input.contextDescription ? input.selectedContextId : void 0,
1957
- gradeBand: void 0,
1958
- course: void 0,
1959
- category: void 0
1960
- };
1961
- return { question: completeQuestion };
1962
- } else {
1963
- throw new Error("AI did not return content for the MRQ question.");
2256
+ return finalId;
2257
+ });
2258
+ if (finalCorrectAnswerIds.length < clientInput.minCorrectAnswers || finalCorrectAnswerIds.length > clientInput.maxCorrectAnswers) {
2259
+ throw new Error(`AI generated ${finalCorrectAnswerIds.length} correct answers, which is outside the requested range of ${clientInput.minCorrectAnswers}-${clientInput.maxCorrectAnswers}.`);
1964
2260
  }
1965
- } catch (error) {
1966
- console.error("Error generating MRQ question in flow:", error);
1967
- throw new Error(`Failed to generate MRQ question: ${error.message}`);
2261
+ if (finalOptions.length !== clientInput.numberOfOptions) {
2262
+ throw new Error(`AI generated ${finalOptions.length} options, but ${clientInput.numberOfOptions} were requested.`);
2263
+ }
2264
+ const completeQuestion = {
2265
+ id: generateUniqueId("mrq_ai_"),
2266
+ questionType: "multiple_response",
2267
+ prompt: aiGeneratedContent.prompt,
2268
+ options: finalOptions,
2269
+ correctAnswerIds: finalCorrectAnswerIds,
2270
+ explanation: aiGeneratedContent.explanation,
2271
+ points: aiGeneratedContent.points,
2272
+ topic: aiGeneratedContent.topic || clientInput.topic,
2273
+ difficulty: aiGeneratedContent.difficulty || clientInput.difficulty,
2274
+ contextCode: clientInput.contextDescription ? clientInput.selectedContextId : void 0
2275
+ };
2276
+ try {
2277
+ const validatedQuestion = MultipleResponseQuestionZodSchema.parse(completeQuestion);
2278
+ return { question: validatedQuestion };
2279
+ } catch (validationError) {
2280
+ console.error("Question validation failed:", validationError);
2281
+ throw new Error(`Generated question failed validation: ${validationError}`);
2282
+ }
2283
+ } else {
2284
+ throw new Error("AI did not return content for the MRQ question.");
1968
2285
  }
2286
+ } catch (error) {
2287
+ console.error("Error generating MRQ question:", error);
2288
+ throw new Error(`Failed to generate MRQ question: ${error.message}`);
1969
2289
  }
1970
- );
2290
+ }
1971
2291
 
1972
2292
  // src/ai/flows/generate-numeric-question.ts
1973
- import { z as z5 } from "genkit";
1974
- var GenerateNumericQuestionInputSchema = z5.object({
2293
+ import { z as z5 } from "zod";
2294
+ import { genkit as genkit5 } from "genkit";
2295
+ import { googleAI as googleAI5, gemini20Flash as gemini20Flash5 } from "@genkit-ai/googleai";
2296
+ function extractJsonFromMarkdown5(text) {
2297
+ const match = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
2298
+ return match ? match[1].trim() : text.trim();
2299
+ }
2300
+ var GenerateNumericQuestionClientInputSchema = z5.object({
1975
2301
  topic: z5.string().describe("The topic for the question."),
2302
+ language: z5.string().optional().default("English").describe('The language for the generated question (e.g., "Vietnamese", "English").'),
2303
+ // <-- ĐÃ THÊM
1976
2304
  difficulty: z5.enum(["easy", "medium", "hard"]).optional().default("medium"),
1977
2305
  allowDecimals: z5.boolean().optional().default(true).describe("Whether the answer can be a decimal."),
1978
2306
  minRange: z5.number().optional().describe("Optional minimum value for the answer."),
@@ -1983,7 +2311,7 @@ var GenerateNumericQuestionInputSchema = z5.object({
1983
2311
  var AINumericOutputFieldsSchema = z5.object({
1984
2312
  prompt: z5.string().describe("The question statement that expects a numerical answer."),
1985
2313
  answer: z5.number().describe("The precise numerical correct answer."),
1986
- tolerance: z5.number().optional().default(0).describe("The acceptable range of error (plus or minus). E.g., if answer is 10 and tolerance is 0.5, answers between 9.5 and 10.5 are correct. Default is 0 for exact match."),
2314
+ tolerance: z5.number().min(0).optional().default(0).describe("The acceptable range of error (plus or minus). Default is 0 for exact match."),
1987
2315
  explanation: z5.string().optional().describe("Explanation for the correct answer."),
1988
2316
  points: z5.number().optional().default(10),
1989
2317
  difficulty: z5.enum(["easy", "medium", "hard"]).optional(),
@@ -1992,10 +2320,10 @@ var AINumericOutputFieldsSchema = z5.object({
1992
2320
  var NumericQuestionZodSchema = z5.object({
1993
2321
  id: z5.string(),
1994
2322
  questionType: z5.literal("numeric"),
1995
- prompt: z5.string(),
2323
+ prompt: z5.string().min(1),
1996
2324
  answer: z5.number(),
1997
- tolerance: z5.number().optional(),
1998
- points: z5.number().optional(),
2325
+ tolerance: z5.number().min(0).optional(),
2326
+ points: z5.number().min(0).optional(),
1999
2327
  explanation: z5.string().optional(),
2000
2328
  learningObjective: z5.string().optional(),
2001
2329
  glossary: z5.array(z5.string()).optional(),
@@ -2010,75 +2338,96 @@ var NumericQuestionZodSchema = z5.object({
2010
2338
  var GenerateNumericQuestionOutputSchema = z5.object({
2011
2339
  question: NumericQuestionZodSchema.optional().describe("The generated Numeric question.")
2012
2340
  });
2013
- async function generateNumericQuestion(input) {
2014
- return generateNumericQuestionFlow(input);
2341
+ async function generateNumericQuestion(clientInput, apiKey) {
2342
+ try {
2343
+ if (clientInput.minRange !== void 0 && clientInput.maxRange !== void 0 && clientInput.minRange > clientInput.maxRange) {
2344
+ throw new Error("minRange cannot be greater than maxRange.");
2345
+ }
2346
+ const ai = genkit5({
2347
+ plugins: [googleAI5({ apiKey })],
2348
+ model: gemini20Flash5
2349
+ });
2350
+ const promptText = `You are an expert quiz question writer.
2351
+ Generate a single Numeric question in ${clientInput.language} based on the following inputs. The question must require a numerical answer.
2352
+
2353
+ IMPORTANT: Return the response as JSON with this EXACT format:
2354
+ {
2355
+ "prompt": "Your question here (e.g., 'What is the value of Pi rounded to two decimal places?')",
2356
+ "answer": 3.14,
2357
+ "tolerance": 0.01,
2358
+ "explanation": "Pi is an irrational number, approximately 3.14159. Rounding to two decimal places gives 3.14.",
2359
+ "points": 10,
2360
+ "difficulty": "easy",
2361
+ "topic": "Mathematics"
2015
2362
  }
2016
- var numericAIPrompt = ai.definePrompt({
2017
- name: "numericQuestionGeneratorPrompt",
2018
- input: { schema: GenerateNumericQuestionInputSchema },
2019
- output: { schema: AINumericOutputFieldsSchema },
2020
- prompt: `You are an expert quiz question writer.
2021
- Generate a single Numeric question based on the topic and difficulty.
2022
- The question should require a numerical answer.
2023
- Provide the question prompt, the correct numerical answer, an optional tolerance (default 0 for exact match), an optional explanation, points (default 10), and optionally refine the topic/difficulty.
2024
- {{#if minRange}}The answer should ideally be greater than or equal to {{{minRange}}}.{{/if}}
2025
- {{#if maxRange}}The answer should ideally be less than or equal to {{{maxRange}}}.{{/if}}
2026
- The answer should be an integer if 'allowDecimals' is false, otherwise it can be a decimal. Current 'allowDecimals': {{{allowDecimals}}}.
2027
-
2028
- Topic: {{{topic}}}
2029
- {{#if contextDescription}}
2030
- Context: {{{contextDescription}}}
2031
- {{/if}}
2032
- Difficulty: {{{difficulty}}}
2033
-
2034
- Ensure your output strictly matches the requested JSON schema.
2035
- `
2036
- });
2037
- var generateNumericQuestionFlow = ai.defineFlow(
2038
- {
2039
- name: "generateNumericQuestionFlow",
2040
- inputSchema: GenerateNumericQuestionInputSchema,
2041
- outputSchema: GenerateNumericQuestionOutputSchema
2042
- },
2043
- async (input) => {
2044
- try {
2045
- const { output: aiGeneratedContent } = await numericAIPrompt(input);
2046
- if (aiGeneratedContent) {
2047
- let finalAnswer = aiGeneratedContent.answer;
2048
- if (!input.allowDecimals) {
2049
- finalAnswer = Math.round(finalAnswer);
2050
- }
2051
- if (input.minRange !== void 0 && finalAnswer < input.minRange) {
2052
- console.warn(`AI generated answer ${finalAnswer} below minRange ${input.minRange}. Adjusting or re-prompt might be needed for stricter adherence.`);
2053
- }
2054
- if (input.maxRange !== void 0 && finalAnswer > input.maxRange) {
2055
- console.warn(`AI generated answer ${finalAnswer} above maxRange ${input.maxRange}. Adjusting or re-prompt might be needed.`);
2056
- }
2057
- const completeQuestion = {
2058
- id: generateUniqueId("numq_ai_"),
2059
- questionType: "numeric",
2060
- prompt: aiGeneratedContent.prompt,
2061
- answer: finalAnswer,
2062
- tolerance: aiGeneratedContent.tolerance,
2063
- explanation: aiGeneratedContent.explanation,
2064
- points: aiGeneratedContent.points,
2065
- topic: aiGeneratedContent.topic || input.topic,
2066
- difficulty: aiGeneratedContent.difficulty || input.difficulty,
2067
- contextCode: input.contextDescription ? input.selectedContextId : void 0
2068
- };
2069
- return { question: completeQuestion };
2070
- } else {
2071
- throw new Error("AI did not return content for the Numeric question.");
2363
+
2364
+ Requirements:
2365
+ - The 'answer' must be a number.
2366
+ - If 'allowDecimals' is false, the 'answer' must be an integer.
2367
+ - 'tolerance' is the acceptable error range (+/-). A tolerance of 0 means the answer must be exact.
2368
+ - The content of 'prompt', 'explanation', and 'topic' must be in ${clientInput.language}.
2369
+ ${clientInput.minRange !== void 0 ? `- The answer should ideally be >= ${clientInput.minRange}.` : ""}
2370
+ ${clientInput.maxRange !== void 0 ? `- The answer should ideally be <= ${clientInput.maxRange}.` : ""}
2371
+
2372
+ Topic: ${clientInput.topic}
2373
+ Language: ${clientInput.language}
2374
+ ${clientInput.contextDescription ? `Context: ${clientInput.contextDescription}` : ""}
2375
+ Difficulty: ${clientInput.difficulty}
2376
+ Allow Decimals: ${clientInput.allowDecimals}
2377
+
2378
+ Return only the JSON response.`;
2379
+ const response = await ai.generate(promptText);
2380
+ const rawText = response.text;
2381
+ const jsonText = extractJsonFromMarkdown5(rawText);
2382
+ console.log("AI Response:", jsonText);
2383
+ const aiGeneratedContent = JSON.parse(jsonText);
2384
+ if (aiGeneratedContent) {
2385
+ let finalAnswer = aiGeneratedContent.answer;
2386
+ if (!clientInput.allowDecimals) {
2387
+ finalAnswer = Math.round(finalAnswer);
2072
2388
  }
2073
- } catch (error) {
2074
- console.error("Error generating Numeric question in flow:", error);
2075
- throw new Error(`Failed to generate Numeric question: ${error.message}`);
2389
+ if (clientInput.minRange !== void 0 && finalAnswer < clientInput.minRange) {
2390
+ console.warn(`AI generated answer ${finalAnswer} is below the requested minRange of ${clientInput.minRange}.`);
2391
+ }
2392
+ if (clientInput.maxRange !== void 0 && finalAnswer > clientInput.maxRange) {
2393
+ console.warn(`AI generated answer ${finalAnswer} is above the requested maxRange of ${clientInput.maxRange}.`);
2394
+ }
2395
+ const completeQuestion = {
2396
+ id: generateUniqueId("num_ai_"),
2397
+ questionType: "numeric",
2398
+ prompt: aiGeneratedContent.prompt,
2399
+ answer: finalAnswer,
2400
+ tolerance: aiGeneratedContent.tolerance,
2401
+ explanation: aiGeneratedContent.explanation,
2402
+ points: aiGeneratedContent.points,
2403
+ topic: aiGeneratedContent.topic || clientInput.topic,
2404
+ difficulty: aiGeneratedContent.difficulty || clientInput.difficulty,
2405
+ contextCode: clientInput.contextDescription ? clientInput.selectedContextId : void 0
2406
+ };
2407
+ try {
2408
+ const validatedQuestion = NumericQuestionZodSchema.parse(completeQuestion);
2409
+ return { question: validatedQuestion };
2410
+ } catch (validationError) {
2411
+ console.error("Question validation failed:", validationError);
2412
+ throw new Error(`Generated question failed validation: ${validationError}`);
2413
+ }
2414
+ } else {
2415
+ throw new Error("AI did not return content for the Numeric question.");
2076
2416
  }
2417
+ } catch (error) {
2418
+ console.error("Error generating Numeric question:", error);
2419
+ throw new Error(`Failed to generate Numeric question: ${error.message}`);
2077
2420
  }
2078
- );
2421
+ }
2079
2422
 
2080
- // src/ai/flows/generate-quiz-plan-flow.ts
2081
- import { z as z6 } from "genkit";
2423
+ // src/ai/flows/generate-quiz-plan.ts
2424
+ import { z as z6 } from "zod";
2425
+ import { genkit as genkit6 } from "genkit";
2426
+ import { googleAI as googleAI6, gemini20Flash as gemini20Flash6 } from "@genkit-ai/googleai";
2427
+ function extractJsonFromMarkdown6(text) {
2428
+ const match = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
2429
+ return match ? match[1].trim() : text.trim();
2430
+ }
2082
2431
  var fullQuizSupportedQuestionTypesArray = [
2083
2432
  "true_false",
2084
2433
  "multiple_choice",
@@ -2087,139 +2436,122 @@ var fullQuizSupportedQuestionTypesArray = [
2087
2436
  "numeric",
2088
2437
  "fill_in_the_blanks",
2089
2438
  "sequence",
2090
- "matching",
2091
- "drag_and_drop"
2439
+ "matching"
2092
2440
  ];
2093
2441
  var BloomLevelStringsEnum = z6.enum(["remembering", "understanding", "applying"]);
2094
- var GenerateQuizPlanInputSchema = z6.object({
2095
- totalQuestions: z6.number().int().min(1).max(100).describe("Total number of questions for the quiz (1-100)."),
2096
- topics: z6.array(
2097
- z6.object({
2098
- topic: z6.string().min(1, { message: "Topic name cannot be empty." }).describe("Name of the topic."),
2099
- ratio: z6.number().min(0).max(100).describe("Percentage of questions for this topic (0-100).")
2100
- })
2101
- ).min(1, { message: "At least one topic is required." }).describe("List of topics and their desired percentage distribution."),
2102
- bloomLevels: z6.array(
2103
- z6.object({
2104
- level: BloomLevelStringsEnum.describe("Bloom's taxonomy level."),
2105
- ratio: z6.number().min(0).max(100).describe("Percentage of questions for this Bloom level (0-100).")
2106
- })
2107
- ).min(1, { message: "At least one Bloom level distribution is required." }).describe("Desired distribution of Bloom's levels across the quiz."),
2108
- selectedContextIds: z6.array(z6.string()).optional().describe('Array of IDs for contexts relevant to the quiz (e.g., "THEO_ABS", "REAL_PROB"). These are hints for the AI.'),
2109
- selectedQuestionTypes: z6.array(z6.enum(fullQuizSupportedQuestionTypesArray)).min(1, { message: "At least one question type must be selected." }).describe("Array of question types to be used in the quiz. Must not include Hotspot or Blockly types.")
2442
+ var GenerateQuizPlanClientInputSchema = z6.object({
2443
+ language: z6.string().optional().default("English").describe('The language for the quiz plan (e.g., "Vietnamese", "English").'),
2444
+ // <-- ĐÃ THÊM
2445
+ totalQuestions: z6.number().int().min(1).max(50),
2446
+ topics: z6.array(z6.object({
2447
+ topic: z6.string().min(1),
2448
+ ratio: z6.number().min(0).max(100)
2449
+ })).min(1),
2450
+ bloomLevels: z6.array(z6.object({
2451
+ level: BloomLevelStringsEnum,
2452
+ ratio: z6.number().min(0).max(100)
2453
+ })).min(1),
2454
+ selectedContextIds: z6.array(z6.string()).optional(),
2455
+ selectedQuestionTypes: z6.array(z6.enum(fullQuizSupportedQuestionTypesArray)).min(1)
2110
2456
  });
2111
2457
  var PlannedQuestionSchema = z6.object({
2112
- plannedTopic: z6.string().describe("The specific topic for this question, derived from the input topics and ratios."),
2113
- plannedQuestionType: z6.enum(fullQuizSupportedQuestionTypesArray).describe("The specific question type chosen for this question from the allowed types."),
2114
- plannedBloomLevel: BloomLevelStringsEnum.describe("The Bloom's level assigned to this question based on overall distribution.")
2458
+ plannedTopic: z6.string().min(1).describe("The specific topic for this question."),
2459
+ plannedQuestionType: z6.enum(fullQuizSupportedQuestionTypesArray).describe("The specific question type chosen."),
2460
+ plannedBloomLevel: BloomLevelStringsEnum.describe("The Bloom's level assigned.")
2115
2461
  });
2116
2462
  var GenerateQuizPlanOutputSchema = z6.object({
2117
- quizPlan: z6.array(PlannedQuestionSchema).describe("A detailed plan for each question in the quiz, specifying topic, type, and Bloom level.")
2463
+ quizPlan: z6.array(PlannedQuestionSchema).describe("A detailed plan for each question in the quiz.")
2118
2464
  });
2119
- async function generateQuizPlan(input) {
2120
- const totalTopicRatio = input.topics.reduce((sum, t) => sum + t.ratio, 0);
2121
- if (Math.abs(totalTopicRatio - 100) > 0.1 && input.topics.length > 0) {
2122
- throw new Error(`Total topic ratio must be 100%. Current sum: ${totalTopicRatio.toFixed(1)}%`);
2123
- }
2124
- const totalBloomRatio = input.bloomLevels.reduce((sum, b) => sum + b.ratio, 0);
2125
- if (Math.abs(totalBloomRatio - 100) > 0.1) {
2126
- throw new Error(`Total Bloom level ratio must be 100%. Current sum: ${totalBloomRatio.toFixed(1)}%`);
2127
- }
2128
- if (input.totalQuestions <= 0 || input.totalQuestions > 100) {
2129
- throw new Error("Total questions must be between 1 and 100.");
2130
- }
2131
- if (input.selectedQuestionTypes.length === 0) {
2132
- throw new Error("At least one question type must be selected.");
2133
- }
2134
- if (input.selectedQuestionTypes.some((qt) => qt === "hotspot" || qt === "blockly_programming" || qt === "scratch_programming")) {
2135
- throw new Error("Selected question types must not include Hotspot, Blockly Programming, or Scratch Programming for this flow.");
2136
- }
2137
- return generateQuizPlanFlow(input);
2138
- }
2139
- var quizPlannerAIPrompt = ai.definePrompt({
2140
- name: "quizPlannerAIPrompt",
2141
- input: { schema: GenerateQuizPlanInputSchema },
2142
- output: { schema: GenerateQuizPlanOutputSchema },
2143
- prompt: `You are an expert educational content planner specializing in creating balanced and effective quiz blueprints.
2144
- Your task is to generate a detailed plan for a quiz consisting of exactly {{{totalQuestions}}} questions.
2145
- The plan should be an array of objects, where each object represents one question and specifies its 'plannedTopic', 'plannedQuestionType', and 'plannedBloomLevel'.
2465
+ async function generateQuizPlan(clientInput, apiKey) {
2466
+ try {
2467
+ const totalTopicRatio = clientInput.topics.reduce((sum, t) => sum + t.ratio, 0);
2468
+ if (Math.abs(totalTopicRatio - 100) > 1) {
2469
+ throw new Error(`Total topic ratio must be 100%. Current sum: ${totalTopicRatio.toFixed(1)}%`);
2470
+ }
2471
+ const totalBloomRatio = clientInput.bloomLevels.reduce((sum, b) => sum + b.ratio, 0);
2472
+ if (Math.abs(totalBloomRatio - 100) > 1) {
2473
+ throw new Error(`Total Bloom level ratio must be 100%. Current sum: ${totalBloomRatio.toFixed(1)}%`);
2474
+ }
2475
+ const ai = genkit6({
2476
+ plugins: [googleAI6({ apiKey })],
2477
+ model: gemini20Flash6
2478
+ });
2479
+ const topicsDistribution = clientInput.topics.map((t) => `- Topic: "${t.topic}", Ratio: ${t.ratio}%`).join("\n ");
2480
+ const bloomDistribution = clientInput.bloomLevels.map((b) => `- Level: "${b.level}", Ratio: ${b.ratio}%`).join("\n ");
2481
+ const allowedQuestionTypes = clientInput.selectedQuestionTypes.map((t) => `'${t}'`).join(", ");
2482
+ const promptText = `You are an expert educational content planner. Your task is to generate a detailed plan for a quiz in ${clientInput.language}.
2146
2483
 
2147
- Constraints and Guidelines:
2148
- 1. **Total Questions**: The output 'quizPlan' array must contain exactly {{{totalQuestions}}} elements.
2149
- 2. **Topic Distribution**: Distribute the questions across the following topics according to their specified ratios. Aim to match these ratios as closely as possible.
2150
- {{#each topics}}
2151
- - Topic: "{{this.topic}}", Ratio: {{this.ratio}}%
2152
- {{/each}}
2153
- 3. **Bloom Level Distribution**: Distribute the questions across the following Bloom's Taxonomy levels according to their specified ratios. This distribution applies to the entire quiz.
2154
- {{#each bloomLevels}}
2155
- - Level: "{{this.level}}", Ratio: {{this.ratio}}%
2156
- {{/each}}
2157
- 4. **Question Types**: For each planned question, select a 'plannedQuestionType' from the following allowed types: {{#each selectedQuestionTypes}}{{{this}}}{{#unless @last}}, {{/unless}}{{/each}}. Try to use a variety of these selected types throughout the quiz, if appropriate for the topics and Bloom levels. The available types are: ${fullQuizSupportedQuestionTypesArray.map((q) => `'${q}'`).join(", ")}.
2158
- 5. **Contexts (Informational)**: The user has indicated the following general contexts are relevant to the quiz: {{#if selectedContextIds}}{{#each selectedContextIds}}{{{this}}}{{#unless @last}}, {{/unless}}{{/each}}{{else}}None specified{{/if}}. You don't need to explicitly assign a context ID to each planned question in your output, but keep these in mind when deciding on topic nuances or type suitability, if applicable. The actual detailed question generation later will handle context.
2159
- 6. **Output Format**: Ensure your output is a JSON object containing a single key "quizPlan", which is an array of question plan objects. Each question plan object must have "plannedTopic" (string), "plannedQuestionType" (one of the allowed enum values specified in guideline 4), and "plannedBloomLevel" (one of the allowed enum values: 'remembering', 'understanding', 'applying').
2160
-
2161
- Example of a single element in the 'quizPlan' array:
2484
+ IMPORTANT: Return the response as JSON with this EXACT format:
2162
2485
  {
2163
- "plannedTopic": "Cellular Respiration Stages",
2164
- "plannedQuestionType": "multiple_choice",
2165
- "plannedBloomLevel": "understanding"
2486
+ "quizPlan": [
2487
+ { "plannedTopic": "Specific Topic A", "plannedQuestionType": "multiple_choice", "plannedBloomLevel": "remembering" },
2488
+ { "plannedTopic": "Specific Topic B", "plannedQuestionType": "true_false", "plannedBloomLevel": "understanding" }
2489
+ ]
2166
2490
  }
2167
2491
 
2168
- Carefully calculate the number of questions for each topic and Bloom level based on the total number of questions and the provided ratios. Strive for a balanced and logical distribution.
2169
- If ratios lead to fractional questions, round to the nearest whole number in a way that maintains the total number of questions.
2170
- The 'plannedTopic' for each question should be specific and directly related to one of the user-provided topics.
2171
- The final 'quizPlan' array must have exactly {{{totalQuestions}}} elements.
2172
- `
2173
- });
2174
- var generateQuizPlanFlow = ai.defineFlow(
2175
- {
2176
- name: "generateQuizPlanFlow",
2177
- inputSchema: GenerateQuizPlanInputSchema,
2178
- outputSchema: GenerateQuizPlanOutputSchema
2179
- },
2180
- async (input) => {
2181
- try {
2182
- const { output } = await quizPlannerAIPrompt(input);
2183
- if (!output || !output.quizPlan) {
2184
- throw new Error("AI did not return a quiz plan.");
2185
- }
2186
- if (output.quizPlan.length !== input.totalQuestions) {
2187
- console.warn(`AI generated a plan for ${output.quizPlan.length} questions, but ${input.totalQuestions} were requested. This is an error in AI adherence.`);
2188
- throw new Error(`AI planned for ${output.quizPlan.length} questions, but ${input.totalQuestions} were requested. The plan must match the total number of questions.`);
2492
+ Constraints and Guidelines:
2493
+ 1. **Total Questions**: The output 'quizPlan' array must contain exactly ${clientInput.totalQuestions} elements.
2494
+ 2. **Topic Distribution**: Distribute the questions across the following topics according to their specified ratios. Match these ratios as closely as possible.
2495
+ ${topicsDistribution}
2496
+ 3. **Bloom Level Distribution**: Distribute the questions across the following Bloom's Taxonomy levels according to their specified ratios.
2497
+ ${bloomDistribution}
2498
+ 4. **Allowed Question Types**: For each planned question, 'plannedQuestionType' must be one of these types: ${allowedQuestionTypes}. Use a variety of these types.
2499
+ 5. **Topic Specificity**: The 'plannedTopic' for each question should be a specific sub-topic or aspect related to one of the main topics provided, and must be in ${clientInput.language}.
2500
+
2501
+ Carefully calculate the number of questions for each topic and Bloom level based on the total number of questions and the provided ratios. If ratios lead to fractional questions, round to the nearest whole number while ensuring the total number of questions remains exactly ${clientInput.totalQuestions}.
2502
+ The final 'quizPlan' array must have exactly ${clientInput.totalQuestions} elements.
2503
+
2504
+ Return only the JSON response.`;
2505
+ const response = await ai.generate(promptText);
2506
+ const rawText = response.text;
2507
+ const jsonText = extractJsonFromMarkdown6(rawText);
2508
+ console.log("AI Response:", jsonText);
2509
+ const aiGeneratedContent = JSON.parse(jsonText);
2510
+ if (!aiGeneratedContent || !aiGeneratedContent.quizPlan) {
2511
+ throw new Error("AI did not return a valid quiz plan structure.");
2512
+ }
2513
+ if (aiGeneratedContent.quizPlan.length !== clientInput.totalQuestions) {
2514
+ throw new Error(`AI planned for ${aiGeneratedContent.quizPlan.length} questions, but ${clientInput.totalQuestions} were requested. The plan must match the total number of questions.`);
2515
+ }
2516
+ aiGeneratedContent.quizPlan.forEach((item, index) => {
2517
+ if (!clientInput.selectedQuestionTypes.includes(item.plannedQuestionType)) {
2518
+ throw new Error(`AI planned question ${index + 1} with a disallowed question type: '${item.plannedQuestionType}'`);
2189
2519
  }
2190
- output.quizPlan.forEach((item, index) => {
2191
- if (!input.selectedQuestionTypes.includes(item.plannedQuestionType)) {
2192
- throw new Error(`AI planned question ${index + 1} with an invalid or disallowed question type: ${item.plannedQuestionType}`);
2193
- }
2194
- const validBloomLevels = ["remembering", "understanding", "applying"];
2195
- if (!validBloomLevels.includes(item.plannedBloomLevel)) {
2196
- throw new Error(`AI planned question ${index + 1} with an invalid Bloom level: ${item.plannedBloomLevel}`);
2197
- }
2198
- if (!input.topics.some((t) => t.topic === item.plannedTopic)) {
2199
- console.warn(`AI planned question ${index + 1} with topic "${item.plannedTopic}" which was not in the input topics. This might be an AI interpretation or error.`);
2200
- }
2201
- });
2202
- return output;
2203
- } catch (error) {
2204
- console.error("Error generating Quiz Plan in flow:", error);
2205
- throw new Error(`Failed to generate Quiz Plan: ${error.message}`);
2520
+ });
2521
+ try {
2522
+ const validatedPlan = GenerateQuizPlanOutputSchema.parse(aiGeneratedContent);
2523
+ return validatedPlan;
2524
+ } catch (validationError) {
2525
+ console.error("Quiz plan validation failed:", validationError);
2526
+ throw new Error(`Generated quiz plan failed validation: ${validationError}`);
2206
2527
  }
2528
+ } catch (error) {
2529
+ console.error("Error generating Quiz Plan:", error);
2530
+ throw new Error(`Failed to generate Quiz Plan: ${error.message}`);
2207
2531
  }
2208
- );
2532
+ }
2209
2533
 
2210
2534
  // src/ai/flows/generate-sequence-question.ts
2211
- import { z as z7 } from "genkit";
2212
- var GenerateSequenceQuestionInputSchema = z7.object({
2535
+ import { z as z7 } from "zod";
2536
+ import { genkit as genkit7 } from "genkit";
2537
+ import { googleAI as googleAI7, gemini20Flash as gemini20Flash7 } from "@genkit-ai/googleai";
2538
+ function extractJsonFromMarkdown7(text) {
2539
+ const match = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
2540
+ return match ? match[1].trim() : text.trim();
2541
+ }
2542
+ var GenerateSequenceQuestionClientInputSchema = z7.object({
2213
2543
  topic: z7.string().describe("The topic for the question."),
2544
+ language: z7.string().optional().default("English").describe('The language for the generated question (e.g., "Vietnamese", "English").'),
2545
+ // <-- ĐÃ THÊM
2214
2546
  difficulty: z7.enum(["easy", "medium", "hard"]).optional().default("medium"),
2215
2547
  numberOfItems: z7.number().int().min(2).max(10).optional().default(4).describe("Number of items to sequence (2-10)."),
2216
2548
  contextDescription: z7.string().optional().describe("A specific context or scenario for the question, complementing the main topic."),
2217
2549
  selectedContextId: z7.string().optional().describe("The ID of the selected context, if any.")
2218
2550
  });
2219
2551
  var AISequenceOutputFieldsSchema = z7.object({
2220
- prompt: z7.string().describe("The instruction for sequencing (e.g., 'Arrange the following steps in chronological order.')."),
2221
- itemsContent: z7.array(z7.string()).min(2).describe("An array of strings, where each string is the content of an item to be sequenced. The order here can be random or pre-sorted by AI, it doesn't matter for this field."),
2222
- correctOrderContent: z7.array(z7.string()).min(2).describe("An array of strings representing the content of the items in the correct sequence. Each string here MUST match one of the strings in 'itemsContent'."),
2552
+ prompt: z7.string().describe("The instruction for sequencing."),
2553
+ itemsContent: z7.array(z7.string().min(1)).min(2).describe("An array of strings for each item to be sequenced."),
2554
+ correctOrderContent: z7.array(z7.string().min(1)).min(2).describe("An array of the same strings from 'itemsContent', but in the correct sequence."),
2223
2555
  explanation: z7.string().optional().describe("Explanation for the correct sequence."),
2224
2556
  points: z7.number().optional().default(10),
2225
2557
  difficulty: z7.enum(["easy", "medium", "hard"]).optional(),
@@ -2228,11 +2560,11 @@ var AISequenceOutputFieldsSchema = z7.object({
2228
2560
  var SequenceQuestionZodSchema = z7.object({
2229
2561
  id: z7.string(),
2230
2562
  questionType: z7.literal("sequence"),
2231
- prompt: z7.string(),
2232
- items: z7.array(z7.object({ id: z7.string(), content: z7.string() })),
2233
- correctOrder: z7.array(z7.string()),
2563
+ prompt: z7.string().min(1),
2564
+ items: z7.array(z7.object({ id: z7.string(), content: z7.string().min(1) })).min(2),
2565
+ correctOrder: z7.array(z7.string()).min(2),
2234
2566
  // Array of item IDs
2235
- points: z7.number().optional(),
2567
+ points: z7.number().min(0).optional(),
2236
2568
  explanation: z7.string().optional(),
2237
2569
  learningObjective: z7.string().optional(),
2238
2570
  glossary: z7.array(z7.string()).optional(),
@@ -2243,92 +2575,136 @@ var SequenceQuestionZodSchema = z7.object({
2243
2575
  course: z7.string().optional(),
2244
2576
  category: z7.string().optional(),
2245
2577
  topic: z7.string().optional()
2578
+ }).refine((data) => {
2579
+ const itemIds = new Set(data.items.map((item) => item.id));
2580
+ return data.correctOrder.every((id) => itemIds.has(id));
2581
+ }, {
2582
+ message: "Every ID in correctOrder must correspond to an item in the items array.",
2583
+ path: ["correctOrder"]
2584
+ }).refine((data) => {
2585
+ return new Set(data.correctOrder).size === data.items.length && data.correctOrder.length === data.items.length;
2586
+ }, {
2587
+ message: "The correctOrder array must contain all item IDs exactly once.",
2588
+ path: ["correctOrder"]
2589
+ }).refine((data) => {
2590
+ const itemIds = data.items.map((item) => item.id);
2591
+ return itemIds.length === new Set(itemIds).size;
2592
+ }, {
2593
+ message: "All item IDs must be unique.",
2594
+ path: ["items"]
2246
2595
  });
2247
2596
  var GenerateSequenceQuestionOutputSchema = z7.object({
2248
2597
  question: SequenceQuestionZodSchema.optional().describe("The generated Sequence question.")
2249
2598
  });
2250
- async function generateSequenceQuestion(input) {
2251
- return generateSequenceQuestionFlow(input);
2599
+ async function generateSequenceQuestion(clientInput, apiKey) {
2600
+ try {
2601
+ const ai = genkit7({
2602
+ plugins: [googleAI7({ apiKey })],
2603
+ model: gemini20Flash7
2604
+ });
2605
+ const promptText = `You are an expert quiz question writer specializing in Sequence questions.
2606
+ Generate a single Sequence question in ${clientInput.language} based on the following inputs.
2607
+
2608
+ IMPORTANT: Return the response as JSON with this EXACT format:
2609
+ {
2610
+ "prompt": "Arrange the following events of World War II in chronological order.",
2611
+ "itemsContent": [
2612
+ "D-Day (Normandy Landings)",
2613
+ "Invasion of Poland",
2614
+ "Attack on Pearl Harbor",
2615
+ "Battle of Stalingrad"
2616
+ ],
2617
+ "correctOrderContent": [
2618
+ "Invasion of Poland",
2619
+ "Attack on Pearl Harbor",
2620
+ "Battle of Stalingrad",
2621
+ "D-Day (Normandy Landings)"
2622
+ ],
2623
+ "explanation": "The Invasion of Poland started the war in Europe, followed by the US entry after Pearl Harbor, the turning point at Stalingrad, and finally the D-Day invasion.",
2624
+ "points": 10,
2625
+ "difficulty": "medium",
2626
+ "topic": "World War II History"
2252
2627
  }
2253
- var sequenceAIPrompt = ai.definePrompt({
2254
- name: "sequenceQuestionGeneratorPrompt",
2255
- input: { schema: GenerateSequenceQuestionInputSchema },
2256
- output: { schema: AISequenceOutputFieldsSchema },
2257
- prompt: `You are an expert quiz question writer.
2258
- Generate a single Sequence question with exactly {{{numberOfItems}}} items to be ordered.
2259
- Provide:
2260
- 1. A 'prompt' or instruction.
2261
- 2. An 'itemsContent' array: strings for each item (e.g., ["Event A", "Event B", "Event C"]). The order here doesn't matter yet.
2262
- 3. A 'correctOrderContent' array: strings from 'itemsContent' but in the correct sequence.
2263
- 4. Optional 'explanation', 'points' (default 10), refined 'topic', and 'difficulty'.
2264
-
2265
- Topic: {{{topic}}}
2266
- {{#if contextDescription}}
2267
- Context: {{{contextDescription}}}
2268
- {{/if}}
2269
- Difficulty: {{{difficulty}}}
2270
- Number of Items: {{{numberOfItems}}}
2271
-
2272
- Ensure 'itemsContent' and 'correctOrderContent' contain the same set of strings, and 'correctOrderContent' has exactly {{{numberOfItems}}} elements.
2273
- Ensure your output strictly matches the requested JSON schema.
2274
- `
2275
- });
2276
- var generateSequenceQuestionFlow = ai.defineFlow(
2277
- {
2278
- name: "generateSequenceQuestionFlow",
2279
- inputSchema: GenerateSequenceQuestionInputSchema,
2280
- outputSchema: GenerateSequenceQuestionOutputSchema
2281
- },
2282
- async (input) => {
2283
- try {
2284
- const { output: aiGeneratedContent } = await sequenceAIPrompt(input);
2285
- if (aiGeneratedContent) {
2286
- if (aiGeneratedContent.itemsContent.length !== input.numberOfItems || aiGeneratedContent.correctOrderContent.length !== input.numberOfItems) {
2287
- throw new Error(`AI generated ${aiGeneratedContent.itemsContent.length} itemsContent and ${aiGeneratedContent.correctOrderContent.length} correctOrderContent, but ${input.numberOfItems} were requested.`);
2288
- }
2289
- const contentToIdMap = {};
2290
- const finalItems = aiGeneratedContent.itemsContent.map((content) => {
2291
- const id = generateUniqueId("seqi_");
2292
- contentToIdMap[content] = id;
2293
- return { id, content };
2294
- });
2295
- const finalCorrectOrder = aiGeneratedContent.correctOrderContent.map((content) => {
2296
- const id = contentToIdMap[content];
2297
- if (!id) {
2298
- throw new Error(`Content "${content}" in 'correctOrderContent' does not match any content in 'itemsContent'.`);
2299
- }
2300
- return id;
2301
- });
2302
- if (new Set(finalCorrectOrder).size !== finalItems.length) {
2303
- throw new Error("Correct order does not contain all items from itemsContent uniquely or items are missing/duplicated.");
2628
+
2629
+ Requirements:
2630
+ - Generate exactly ${clientInput.numberOfItems} items to be sequenced.
2631
+ - The 'itemsContent' array should contain the text for each item.
2632
+ - The 'correctOrderContent' array must contain the exact same strings as 'itemsContent', but arranged in the correct sequence.
2633
+ - Both arrays must have the same number of elements.
2634
+ - The content of 'prompt', 'itemsContent', 'correctOrderContent', 'explanation', and 'topic' must be in ${clientInput.language}.
2635
+
2636
+ Topic: ${clientInput.topic}
2637
+ Language: ${clientInput.language}
2638
+ ${clientInput.contextDescription ? `Context: ${clientInput.contextDescription}` : ""}
2639
+ Difficulty: ${clientInput.difficulty}
2640
+ Number of Items: ${clientInput.numberOfItems}
2641
+
2642
+ Return only the JSON response.`;
2643
+ const response = await ai.generate(promptText);
2644
+ const rawText = response.text;
2645
+ const jsonText = extractJsonFromMarkdown7(rawText);
2646
+ console.log("AI Response:", jsonText);
2647
+ const aiGeneratedContent = JSON.parse(jsonText);
2648
+ if (aiGeneratedContent) {
2649
+ if (aiGeneratedContent.itemsContent.length !== clientInput.numberOfItems || aiGeneratedContent.correctOrderContent.length !== clientInput.numberOfItems) {
2650
+ throw new Error(`AI generated an incorrect number of items. Requested: ${clientInput.numberOfItems}, Got: ${aiGeneratedContent.itemsContent.length} items and ${aiGeneratedContent.correctOrderContent.length} in correct order.`);
2651
+ }
2652
+ if (new Set(aiGeneratedContent.itemsContent).size !== new Set(aiGeneratedContent.correctOrderContent).size) {
2653
+ throw new Error("The set of items in 'itemsContent' and 'correctOrderContent' do not match.");
2654
+ }
2655
+ const contentToIdMap = {};
2656
+ const finalItems = aiGeneratedContent.itemsContent.map((content) => {
2657
+ const id = generateUniqueId("seqi_");
2658
+ contentToIdMap[content] = id;
2659
+ return { id, content };
2660
+ });
2661
+ const finalCorrectOrder = aiGeneratedContent.correctOrderContent.map((content) => {
2662
+ const id = contentToIdMap[content];
2663
+ if (!id) {
2664
+ throw new Error(`Content "${content}" from 'correctOrderContent' was not found in 'itemsContent'.`);
2304
2665
  }
2305
- const completeQuestion = {
2306
- id: generateUniqueId("seq_ai_"),
2307
- questionType: "sequence",
2308
- prompt: aiGeneratedContent.prompt,
2309
- items: finalItems,
2310
- correctOrder: finalCorrectOrder,
2311
- explanation: aiGeneratedContent.explanation,
2312
- points: aiGeneratedContent.points,
2313
- topic: aiGeneratedContent.topic || input.topic,
2314
- difficulty: aiGeneratedContent.difficulty || input.difficulty,
2315
- contextCode: input.contextDescription ? input.selectedContextId : void 0
2316
- };
2317
- return { question: completeQuestion };
2318
- } else {
2319
- throw new Error("AI did not return content for the Sequence question.");
2666
+ return id;
2667
+ });
2668
+ const completeQuestion = {
2669
+ id: generateUniqueId("seq_ai_"),
2670
+ questionType: "sequence",
2671
+ prompt: aiGeneratedContent.prompt,
2672
+ items: finalItems,
2673
+ correctOrder: finalCorrectOrder,
2674
+ explanation: aiGeneratedContent.explanation,
2675
+ points: aiGeneratedContent.points,
2676
+ topic: aiGeneratedContent.topic || clientInput.topic,
2677
+ difficulty: aiGeneratedContent.difficulty || clientInput.difficulty,
2678
+ contextCode: clientInput.contextDescription ? clientInput.selectedContextId : void 0
2679
+ };
2680
+ try {
2681
+ const validatedQuestion = SequenceQuestionZodSchema.parse(completeQuestion);
2682
+ return { question: validatedQuestion };
2683
+ } catch (validationError) {
2684
+ console.error("Question validation failed:", validationError);
2685
+ throw new Error(`Generated question failed validation: ${validationError}`);
2320
2686
  }
2321
- } catch (error) {
2322
- console.error("Error generating Sequence question in flow:", error);
2323
- throw new Error(`Failed to generate Sequence question: ${error.message}`);
2687
+ } else {
2688
+ throw new Error("AI did not return content for the Sequence question.");
2324
2689
  }
2690
+ } catch (error) {
2691
+ console.error("Error generating Sequence question:", error);
2692
+ throw new Error(`Failed to generate Sequence question: ${error.message}`);
2325
2693
  }
2326
- );
2694
+ }
2327
2695
 
2328
2696
  // src/ai/flows/generate-short-answer-question.ts
2329
- import { z as z8 } from "genkit";
2330
- var GenerateShortAnswerQuestionInputSchema = z8.object({
2697
+ import { z as z8 } from "zod";
2698
+ import { genkit as genkit8 } from "genkit";
2699
+ import { googleAI as googleAI8, gemini20Flash as gemini20Flash8 } from "@genkit-ai/googleai";
2700
+ function extractJsonFromMarkdown8(text) {
2701
+ const match = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
2702
+ return match ? match[1].trim() : text.trim();
2703
+ }
2704
+ var GenerateShortAnswerQuestionClientInputSchema = z8.object({
2331
2705
  topic: z8.string().describe("The topic for the question."),
2706
+ language: z8.string().optional().default("English").describe('The language for the generated question (e.g., "Vietnamese", "English").'),
2707
+ // <-- ĐÃ THÊM
2332
2708
  difficulty: z8.enum(["easy", "medium", "hard"]).optional().default("medium"),
2333
2709
  isCaseSensitive: z8.boolean().optional().default(false).describe("Whether the answer should be case-sensitive."),
2334
2710
  contextDescription: z8.string().optional().describe("A specific context or scenario for the question, complementing the main topic."),
@@ -2336,8 +2712,8 @@ var GenerateShortAnswerQuestionInputSchema = z8.object({
2336
2712
  });
2337
2713
  var AIShortAnswerOutputFieldsSchema = z8.object({
2338
2714
  prompt: z8.string().describe("The question statement."),
2339
- acceptedAnswers: z8.array(z8.string()).min(1).describe("An array of acceptable short answers. Provide variations if appropriate."),
2340
- isCaseSensitive: z8.boolean().optional().describe("Should the answer evaluation be case sensitive? Defaults to input or false."),
2715
+ acceptedAnswers: z8.array(z8.string().min(1)).min(1).describe("An array of acceptable short answers."),
2716
+ isCaseSensitive: z8.boolean().optional().describe("Should the answer evaluation be case sensitive?"),
2341
2717
  explanation: z8.string().optional().describe("Explanation for the correct answer(s)."),
2342
2718
  points: z8.number().optional().default(10),
2343
2719
  difficulty: z8.enum(["easy", "medium", "hard"]).optional(),
@@ -2346,10 +2722,10 @@ var AIShortAnswerOutputFieldsSchema = z8.object({
2346
2722
  var ShortAnswerQuestionZodSchema = z8.object({
2347
2723
  id: z8.string(),
2348
2724
  questionType: z8.literal("short_answer"),
2349
- prompt: z8.string(),
2350
- acceptedAnswers: z8.array(z8.string()),
2725
+ prompt: z8.string().min(1),
2726
+ acceptedAnswers: z8.array(z8.string().min(1)).min(1),
2351
2727
  isCaseSensitive: z8.boolean().optional(),
2352
- points: z8.number().optional(),
2728
+ points: z8.number().min(0).optional(),
2353
2729
  explanation: z8.string().optional(),
2354
2730
  learningObjective: z8.string().optional(),
2355
2731
  glossary: z8.array(z8.string()).optional(),
@@ -2364,83 +2740,105 @@ var ShortAnswerQuestionZodSchema = z8.object({
2364
2740
  var GenerateShortAnswerQuestionOutputSchema = z8.object({
2365
2741
  question: ShortAnswerQuestionZodSchema.optional().describe("The generated Short Answer question.")
2366
2742
  });
2367
- async function generateShortAnswerQuestion(input) {
2368
- return generateShortAnswerQuestionFlow(input);
2743
+ async function generateShortAnswerQuestion(clientInput, apiKey) {
2744
+ var _a;
2745
+ try {
2746
+ const ai = genkit8({
2747
+ plugins: [googleAI8({ apiKey })],
2748
+ model: gemini20Flash8
2749
+ });
2750
+ const promptText = `You are an expert quiz question writer.
2751
+ Generate a single Short Answer question in ${clientInput.language} based on the following inputs.
2752
+
2753
+ IMPORTANT: Return the response as JSON with this EXACT format:
2754
+ {
2755
+ "prompt": "What is the capital of France?",
2756
+ "acceptedAnswers": ["Paris"],
2757
+ "isCaseSensitive": false,
2758
+ "explanation": "Paris has been the capital of France since the 10th century.",
2759
+ "points": 5,
2760
+ "difficulty": "easy",
2761
+ "topic": "World Capitals"
2369
2762
  }
2370
- var shortAnswerAIPrompt = ai.definePrompt({
2371
- name: "shortAnswerQuestionGeneratorPrompt",
2372
- input: { schema: GenerateShortAnswerQuestionInputSchema },
2373
- output: { schema: AIShortAnswerOutputFieldsSchema },
2374
- prompt: `You are an expert quiz question writer.
2375
- Generate a single Short Answer question based on the following inputs.
2376
- Provide the question prompt, an array of accepted answer strings, and specify if case sensitivity is important (default to {{{isCaseSensitive}}}).
2377
- Also include an optional explanation, points (default 10), and optionally refine the topic/difficulty.
2378
-
2379
- Topic: {{{topic}}}
2380
- {{#if contextDescription}}
2381
- Context: {{{contextDescription}}}
2382
- {{/if}}
2383
- Difficulty: {{{difficulty}}}
2384
- Case Sensitive: {{{isCaseSensitive}}}
2385
-
2386
- Ensure your output strictly matches the requested JSON schema.
2387
- `
2388
- });
2389
- var generateShortAnswerQuestionFlow = ai.defineFlow(
2390
- {
2391
- name: "generateShortAnswerQuestionFlow",
2392
- inputSchema: GenerateShortAnswerQuestionInputSchema,
2393
- outputSchema: GenerateShortAnswerQuestionOutputSchema
2394
- },
2395
- async (input) => {
2396
- try {
2397
- const { output: aiGeneratedContent } = await shortAnswerAIPrompt(input);
2398
- if (aiGeneratedContent) {
2399
- const completeQuestion = {
2400
- id: generateUniqueId("saq_ai_"),
2401
- questionType: "short_answer",
2402
- prompt: aiGeneratedContent.prompt,
2403
- acceptedAnswers: aiGeneratedContent.acceptedAnswers,
2404
- isCaseSensitive: aiGeneratedContent.isCaseSensitive === void 0 ? input.isCaseSensitive : aiGeneratedContent.isCaseSensitive,
2405
- explanation: aiGeneratedContent.explanation,
2406
- points: aiGeneratedContent.points,
2407
- topic: aiGeneratedContent.topic || input.topic,
2408
- difficulty: aiGeneratedContent.difficulty || input.difficulty,
2409
- contextCode: input.contextDescription ? input.selectedContextId : void 0
2410
- };
2411
- return { question: completeQuestion };
2412
- } else {
2413
- throw new Error("AI did not return content for the Short Answer question.");
2763
+
2764
+ Requirements:
2765
+ - The 'acceptedAnswers' array must contain at least one possible correct answer.
2766
+ - If there are multiple common ways to phrase the answer (e.g., "USA", "United States"), include them in the array.
2767
+ - Set 'isCaseSensitive' to true or false based on the nature of the answer. Default to the input value.
2768
+ - The content of 'prompt', 'acceptedAnswers', 'explanation', and 'topic' must be in ${clientInput.language}.
2769
+
2770
+ Topic: ${clientInput.topic}
2771
+ Language: ${clientInput.language}
2772
+ ${clientInput.contextDescription ? `Context: ${clientInput.contextDescription}` : ""}
2773
+ Difficulty: ${clientInput.difficulty}
2774
+ Case Sensitive: ${clientInput.isCaseSensitive}
2775
+
2776
+ Return only the JSON response.`;
2777
+ const response = await ai.generate(promptText);
2778
+ const rawText = response.text;
2779
+ const jsonText = extractJsonFromMarkdown8(rawText);
2780
+ console.log("AI Response:", jsonText);
2781
+ const aiGeneratedContent = JSON.parse(jsonText);
2782
+ if (aiGeneratedContent) {
2783
+ const completeQuestion = {
2784
+ id: generateUniqueId("saq_ai_"),
2785
+ questionType: "short_answer",
2786
+ prompt: aiGeneratedContent.prompt,
2787
+ acceptedAnswers: aiGeneratedContent.acceptedAnswers,
2788
+ // Ưu tiên giá trị từ AI, nếu không có thì dùng giá trị đầu vào
2789
+ isCaseSensitive: (_a = aiGeneratedContent.isCaseSensitive) != null ? _a : clientInput.isCaseSensitive,
2790
+ explanation: aiGeneratedContent.explanation,
2791
+ points: aiGeneratedContent.points,
2792
+ topic: aiGeneratedContent.topic || clientInput.topic,
2793
+ difficulty: aiGeneratedContent.difficulty || clientInput.difficulty,
2794
+ contextCode: clientInput.contextDescription ? clientInput.selectedContextId : void 0
2795
+ };
2796
+ try {
2797
+ const validatedQuestion = ShortAnswerQuestionZodSchema.parse(completeQuestion);
2798
+ return { question: validatedQuestion };
2799
+ } catch (validationError) {
2800
+ console.error("Question validation failed:", validationError);
2801
+ throw new Error(`Generated question failed validation: ${validationError}`);
2414
2802
  }
2415
- } catch (error) {
2416
- console.error("Error generating Short Answer question in flow:", error);
2417
- throw new Error(`Failed to generate Short Answer question: ${error.message}`);
2803
+ } else {
2804
+ throw new Error("AI did not return content for the Short Answer question.");
2418
2805
  }
2806
+ } catch (error) {
2807
+ console.error("Error generating Short Answer question:", error);
2808
+ throw new Error(`Failed to generate Short Answer question: ${error.message}`);
2419
2809
  }
2420
- );
2810
+ }
2421
2811
 
2422
2812
  // src/ai/flows/generate-true-false-question.ts
2423
- import { z as z9 } from "genkit";
2424
- var GenerateTrueFalseQuestionInputSchema = z9.object({
2425
- topic: z9.string().describe("The topic for which to generate a True/False question."),
2426
- difficulty: z9.enum(["easy", "medium", "hard"]).optional().default("medium").describe("The difficulty level of the question (optional)."),
2427
- contextDescription: z9.string().optional().describe("A specific context or scenario for the question, complementing the main topic."),
2428
- selectedContextId: z9.string().optional().describe("The ID of the selected context, if any.")
2813
+ import { z as z9 } from "zod";
2814
+ import { genkit as genkit9 } from "genkit";
2815
+ import { googleAI as googleAI9, gemini20Flash as gemini20Flash9 } from "@genkit-ai/googleai";
2816
+ function extractJsonFromMarkdown9(text) {
2817
+ const match = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
2818
+ return match ? match[1].trim() : text.trim();
2819
+ }
2820
+ var GenerateTrueFalseQuestionClientInputSchema = z9.object({
2821
+ topic: z9.string().describe("The topic for the question."),
2822
+ language: z9.string().optional().default("English").describe('The language for the generated question (e.g., "Vietnamese", "English").'),
2823
+ // <-- ĐÃ THÊM
2824
+ difficulty: z9.enum(["easy", "medium", "hard"]).optional().default("medium"),
2825
+ contextDescription: z9.string().optional().describe("A specific context or scenario for the question."),
2826
+ selectedContextId: z9.string().optional().describe("The ID of the selected context.")
2429
2827
  });
2430
- var AIQuestionOutputFieldsSchema = z9.object({
2431
- prompt: z9.string().describe("The question text itself. This is the statement to be evaluated as true or false."),
2432
- correctAnswer: z9.boolean().describe("The correct answer for the statement (true if the statement is true, false if it is false)."),
2828
+ var AITrueFalseOutputFieldsSchema = z9.object({
2829
+ prompt: z9.string().describe("The statement to be evaluated as true or false."),
2830
+ correctAnswer: z9.boolean().describe("The correct answer (true or false)."),
2433
2831
  explanation: z9.string().optional().describe("A brief explanation of why the answer is correct."),
2434
- points: z9.number().optional().default(10).describe("Points awarded for a correct answer. Defaults to 10."),
2435
- difficulty: z9.enum(["easy", "medium", "hard"]).optional().describe("The assessed difficulty of the generated question."),
2436
- topic: z9.string().optional().describe("The specific topic of the generated question, potentially refined from the input topic.")
2832
+ points: z9.number().optional().default(10),
2833
+ difficulty: z9.enum(["easy", "medium", "hard"]).optional(),
2834
+ topic: z9.string().optional()
2437
2835
  });
2438
2836
  var TrueFalseQuestionZodSchema = z9.object({
2439
2837
  id: z9.string(),
2440
2838
  questionType: z9.literal("true_false"),
2441
- prompt: z9.string(),
2839
+ prompt: z9.string().min(1),
2442
2840
  correctAnswer: z9.boolean(),
2443
- points: z9.number().optional(),
2841
+ points: z9.number().min(0).optional(),
2444
2842
  explanation: z9.string().optional(),
2445
2843
  learningObjective: z9.string().optional(),
2446
2844
  glossary: z9.array(z9.string()).optional(),
@@ -2455,68 +2853,73 @@ var TrueFalseQuestionZodSchema = z9.object({
2455
2853
  var GenerateTrueFalseQuestionOutputSchema = z9.object({
2456
2854
  question: TrueFalseQuestionZodSchema.optional().describe("The generated True/False question.")
2457
2855
  });
2458
- async function generateTrueFalseQuestion(input) {
2459
- return generateTrueFalseQuestionFlow(input);
2856
+ async function generateTrueFalseQuestion(clientInput, apiKey) {
2857
+ try {
2858
+ const ai = genkit9({
2859
+ plugins: [googleAI9({ apiKey })],
2860
+ model: gemini20Flash9
2861
+ });
2862
+ const promptText = `You are an expert quiz question writer.
2863
+ Generate a single True/False question in ${clientInput.language} based on the following inputs.
2864
+
2865
+ IMPORTANT: Return the response as JSON with this EXACT format:
2866
+ {
2867
+ "prompt": "The Earth is the fourth planet from the Sun.",
2868
+ "correctAnswer": false,
2869
+ "explanation": "The Earth is the third planet from the Sun. Mars is the fourth.",
2870
+ "points": 10,
2871
+ "difficulty": "easy",
2872
+ "topic": "Astronomy"
2460
2873
  }
2461
- var trueFalseAIPrompt = ai.definePrompt({
2462
- name: "trueFalseQuestionGeneratorPrompt",
2463
- input: { schema: GenerateTrueFalseQuestionInputSchema },
2464
- output: { schema: AIQuestionOutputFieldsSchema },
2465
- prompt: `You are an expert quiz question writer.
2466
- Generate a single True/False question based on the following inputs.
2467
- The question should be a statement that can be clearly evaluated as true or false.
2468
- Provide the question statement (prompt), the correct answer (true or false), an optional explanation, optional points (default to 10), and optionally refine the topic and difficulty.
2469
-
2470
- Topic: {{{topic}}}
2471
- {{#if contextDescription}}
2472
- Context: {{{contextDescription}}}
2473
- {{/if}}
2474
- Difficulty: {{{difficulty}}}
2475
-
2476
- Ensure your output strictly matches the requested JSON schema.
2477
- `
2478
- });
2479
- var generateTrueFalseQuestionFlow = ai.defineFlow(
2480
- {
2481
- name: "generateTrueFalseQuestionFlow",
2482
- inputSchema: GenerateTrueFalseQuestionInputSchema,
2483
- outputSchema: GenerateTrueFalseQuestionOutputSchema
2484
- },
2485
- async (input) => {
2486
- try {
2487
- const { output: aiGeneratedContent } = await trueFalseAIPrompt(input);
2488
- if (aiGeneratedContent) {
2489
- const completeQuestion = {
2490
- prompt: aiGeneratedContent.prompt,
2491
- correctAnswer: aiGeneratedContent.correctAnswer,
2492
- explanation: aiGeneratedContent.explanation,
2493
- points: aiGeneratedContent.points,
2494
- topic: aiGeneratedContent.topic || input.topic,
2495
- difficulty: aiGeneratedContent.difficulty || input.difficulty,
2496
- id: generateUniqueId("tf_ai_"),
2497
- questionType: "true_false",
2498
- learningObjective: void 0,
2499
- glossary: void 0,
2500
- bloomLevel: void 0,
2501
- contextCode: input.contextDescription ? input.selectedContextId : void 0,
2502
- gradeBand: void 0,
2503
- course: void 0,
2504
- category: void 0
2505
- };
2506
- return { question: completeQuestion };
2507
- } else {
2508
- throw new Error("AI did not return content for the question.");
2874
+
2875
+ Requirements:
2876
+ - The 'prompt' must be a clear statement that is definitively true or false.
2877
+ - 'correctAnswer' must be a boolean value (true or false).
2878
+ - The content of 'prompt', 'explanation', and 'topic' must be in ${clientInput.language}.
2879
+
2880
+ Topic: ${clientInput.topic}
2881
+ Language: ${clientInput.language}
2882
+ ${clientInput.contextDescription ? `Context: ${clientInput.contextDescription}` : ""}
2883
+ Difficulty: ${clientInput.difficulty}
2884
+
2885
+ Return only the JSON response.`;
2886
+ const response = await ai.generate(promptText);
2887
+ const rawText = response.text;
2888
+ const jsonText = extractJsonFromMarkdown9(rawText);
2889
+ console.log("AI Response:", jsonText);
2890
+ const aiGeneratedContent = JSON.parse(jsonText);
2891
+ if (aiGeneratedContent) {
2892
+ const completeQuestion = {
2893
+ id: generateUniqueId("tf_ai_"),
2894
+ questionType: "true_false",
2895
+ prompt: aiGeneratedContent.prompt,
2896
+ correctAnswer: aiGeneratedContent.correctAnswer,
2897
+ explanation: aiGeneratedContent.explanation,
2898
+ points: aiGeneratedContent.points,
2899
+ topic: aiGeneratedContent.topic || clientInput.topic,
2900
+ difficulty: aiGeneratedContent.difficulty || clientInput.difficulty,
2901
+ contextCode: clientInput.contextDescription ? clientInput.selectedContextId : void 0
2902
+ };
2903
+ try {
2904
+ const validatedQuestion = TrueFalseQuestionZodSchema.parse(completeQuestion);
2905
+ return { question: validatedQuestion };
2906
+ } catch (validationError) {
2907
+ console.error("Question validation failed:", validationError);
2908
+ throw new Error(`Generated question failed validation: ${validationError}`);
2509
2909
  }
2510
- } catch (error) {
2511
- console.error("Error generating True/False question in flow:", error);
2512
- throw new Error(`Failed to generate True/False question: ${error.message}`);
2910
+ } else {
2911
+ throw new Error("AI did not return content for the True/False question.");
2513
2912
  }
2913
+ } catch (error) {
2914
+ console.error("Error generating True/False question:", error);
2915
+ throw new Error(`Failed to generate True/False question: ${error.message}`);
2514
2916
  }
2515
- );
2917
+ }
2516
2918
 
2517
- // src/ai/flows/generateQuestionsFromQuizPlanFlow.ts
2518
- import { z as z10 } from "genkit";
2919
+ // src/ai/flows/generate-questions-from-quiz-plan.ts
2920
+ import { z as z10 } from "zod";
2519
2921
  var internalContextOptions = [
2922
+ // ... (giữ nguyên mảng này)
2520
2923
  { contextId: "THEO_ABS", contextDescription: "L\xFD thuy\u1EBFt/Tr\u1EEBu t\u01B0\u1EE3ng" },
2521
2924
  { contextId: "SPEC_CASE", contextDescription: "V\xED d\u1EE5 C\u1EE5 th\u1EC3/Tr\u01B0\u1EDDng h\u1EE3p Ri\xEAng" },
2522
2925
  { contextId: "NAT_OBS", contextDescription: "Hi\u1EC7n t\u01B0\u1EE3ng T\u1EF1 nhi\xEAn/Quan s\xE1t" },
@@ -2528,38 +2931,6 @@ var internalContextOptions = [
2528
2931
  { contextId: "INTERDISC", contextDescription: "Li\xEAn ng\xE0nh (Interdisciplinary)" },
2529
2932
  { contextId: "HYPO_COMP", contextDescription: "Gi\u1EA3 \u0111\u1ECBnh/So s\xE1nh T\xECnh hu\u1ED1ng" }
2530
2933
  ];
2531
- var LocalBloomLevelStringsEnum = z10.enum(["remembering", "understanding", "applying"]);
2532
- var fullQuizSupportedQuestionTypesArrayForSchema = [
2533
- "true_false",
2534
- "multiple_choice",
2535
- "multiple_response",
2536
- "short_answer",
2537
- "numeric",
2538
- "fill_in_the_blanks",
2539
- "sequence",
2540
- "matching",
2541
- "drag_and_drop"
2542
- ];
2543
- var LocalPlannedQuestionSchema = z10.object({
2544
- plannedTopic: z10.string().describe("The specific topic for this question, derived from the input topics and ratios."),
2545
- plannedQuestionType: z10.enum(fullQuizSupportedQuestionTypesArrayForSchema).describe("The specific question type chosen for this question from the allowed types."),
2546
- plannedBloomLevel: LocalBloomLevelStringsEnum.describe("The Bloom's level assigned to this question based on overall distribution.")
2547
- });
2548
- var GenerateQuestionsFromQuizPlanInputSchema = z10.object({
2549
- quizPlan: z10.array(LocalPlannedQuestionSchema).describe("The plan for each question, from Stage 1."),
2550
- selectedContextIds: z10.array(z10.string()).optional().describe("Array of IDs for contexts relevant to the overall quiz."),
2551
- customContextInput: z10.string().optional().describe("Custom context description if provided by the user.")
2552
- });
2553
- var GenerationErrorSchema = z10.object({
2554
- plannedQuestionIndex: z10.number().describe("Index of the question in the plan that failed."),
2555
- plannedTopic: z10.string(),
2556
- plannedQuestionType: z10.string(),
2557
- error: z10.string().describe("Error message for this specific question generation.")
2558
- });
2559
- var GenerateQuestionsFromQuizPlanOutputSchema = z10.object({
2560
- generatedQuestions: z10.array(z10.custom((val) => typeof val === "object" && val !== null && "id" in val && "questionType" in val)).describe("Array of successfully generated QuizQuestion objects."),
2561
- errors: z10.array(GenerationErrorSchema).optional().describe("Array of errors encountered during generation for specific planned questions.")
2562
- });
2563
2934
  var calculateCombinedDifficulty = (contextId, bloomLevel, qType, generalCustomContextInput) => {
2564
2935
  let contextScore = 1;
2565
2936
  const selectedContext = contextId ? internalContextOptions.find((c) => c.contextId === contextId) : void 0;
@@ -2567,7 +2938,7 @@ var calculateCombinedDifficulty = (contextId, bloomLevel, qType, generalCustomCo
2567
2938
  if (["THEO_ABS", "HIST_SCI"].includes(selectedContext.contextId)) contextScore = 1;
2568
2939
  else if (["SPEC_CASE", "NAT_OBS", "DATA_MOD", "INTERDISC", "HYPO_COMP"].includes(selectedContext.contextId)) contextScore = 2;
2569
2940
  else if (["TECH_ENG", "EXP_INV", "REAL_PROB"].includes(selectedContext.contextId)) contextScore = 3;
2570
- } else if (generalCustomContextInput && generalCustomContextInput.trim()) {
2941
+ } else if (generalCustomContextInput == null ? void 0 : generalCustomContextInput.trim()) {
2571
2942
  contextScore = 2;
2572
2943
  }
2573
2944
  let bloomScore = 1;
@@ -2598,31 +2969,50 @@ var calculateCombinedDifficulty = (contextId, bloomLevel, qType, generalCustomCo
2598
2969
  if (totalScore <= 6) return "medium";
2599
2970
  return "hard";
2600
2971
  };
2601
- async function generateQuestionsFromQuizPlan(input) {
2602
- return generateQuestionsFromQuizPlanFlow(input);
2603
- }
2604
- var generateQuestionsFromQuizPlanFlow = ai.defineFlow(
2605
- {
2606
- name: "generateQuestionsFromQuizPlanFlow",
2607
- inputSchema: GenerateQuestionsFromQuizPlanInputSchema,
2608
- outputSchema: GenerateQuestionsFromQuizPlanOutputSchema
2609
- },
2610
- async ({ quizPlan, selectedContextIds, customContextInput }) => {
2611
- var _a, _b;
2612
- const generatedQuestions = [];
2613
- const errors = [];
2614
- for (let i = 0; i < quizPlan.length; i++) {
2615
- const plannedQ = quizPlan[i];
2616
- let question = void 0;
2617
- let generationError = null;
2618
- let contextDescriptionForAI = void 0;
2972
+ var PlannedQuestionSchema2 = z10.object({
2973
+ plannedTopic: z10.string().min(1),
2974
+ plannedQuestionType: z10.string(),
2975
+ // Giữ dạng string để linh hoạt, sẽ kiểm tra trong logic
2976
+ plannedBloomLevel: z10.enum(["remembering", "understanding", "applying"]),
2977
+ plannedContextId: z10.string().optional()
2978
+ });
2979
+ var GenerateQuestionsFromQuizPlanClientInputSchema = z10.object({
2980
+ quizPlan: z10.array(PlannedQuestionSchema2).min(1),
2981
+ language: z10.string().optional().default("English").describe("The language for the generated questions."),
2982
+ // <-- ĐÃ THÊM
2983
+ selectedContextIds: z10.array(z10.string()).optional(),
2984
+ customContextInput: z10.string().optional()
2985
+ });
2986
+ var GenerationErrorSchema = z10.object({
2987
+ plannedQuestionIndex: z10.number(),
2988
+ plannedTopic: z10.string(),
2989
+ plannedQuestionType: z10.string(),
2990
+ error: z10.string()
2991
+ });
2992
+ var GenerateQuestionsFromQuizPlanOutputSchema = z10.object({
2993
+ generatedQuestions: z10.array(z10.any()),
2994
+ // z.any() là thực tế vì union của tất cả các loại câu hỏi rất phức tạp
2995
+ errors: z10.array(GenerationErrorSchema).optional()
2996
+ });
2997
+ async function generateQuestionsFromQuizPlan(clientInput, apiKey) {
2998
+ var _a, _b;
2999
+ const { quizPlan, selectedContextIds, customContextInput, language } = clientInput;
3000
+ const generatedQuestions = [];
3001
+ const errors = [];
3002
+ for (let i = 0; i < quizPlan.length; i++) {
3003
+ const plannedQ = quizPlan[i];
3004
+ let generationError = null;
3005
+ try {
3006
+ let contextDescriptionForAI;
2619
3007
  let contextIdForDifficultyCalc = plannedQ.plannedContextId;
2620
3008
  if (plannedQ.plannedContextId && plannedQ.plannedContextId !== "__custom__") {
2621
3009
  contextDescriptionForAI = (_a = internalContextOptions.find((c) => c.contextId === plannedQ.plannedContextId)) == null ? void 0 : _a.contextDescription;
2622
- } else if (selectedContextIds && selectedContextIds.length > 0 && selectedContextIds[0] !== "__custom__") {
3010
+ } else if (plannedQ.plannedContextId === "__custom__") {
3011
+ contextDescriptionForAI = customContextInput == null ? void 0 : customContextInput.trim();
3012
+ } else if ((selectedContextIds == null ? void 0 : selectedContextIds[0]) && selectedContextIds[0] !== "__custom__") {
2623
3013
  contextDescriptionForAI = (_b = internalContextOptions.find((c) => c.contextId === selectedContextIds[0])) == null ? void 0 : _b.contextDescription;
2624
3014
  if (!contextIdForDifficultyCalc) contextIdForDifficultyCalc = selectedContextIds[0];
2625
- } else if (customContextInput && customContextInput.trim()) {
3015
+ } else if (customContextInput == null ? void 0 : customContextInput.trim()) {
2626
3016
  contextDescriptionForAI = customContextInput.trim();
2627
3017
  if (!contextIdForDifficultyCalc) contextIdForDifficultyCalc = "__custom__";
2628
3018
  }
@@ -2632,71 +3022,69 @@ var generateQuestionsFromQuizPlanFlow = ai.defineFlow(
2632
3022
  plannedQ.plannedQuestionType,
2633
3023
  contextDescriptionForAI
2634
3024
  );
2635
- const baseQuestionInput = {
3025
+ const baseClientInput = {
2636
3026
  topic: plannedQ.plannedTopic,
3027
+ language,
3028
+ // <-- TRUYỀN `language` VÀO
2637
3029
  difficulty: difficultyForAI,
2638
3030
  contextDescription: contextDescriptionForAI,
2639
- selectedContextId: plannedQ.plannedContextId
3031
+ selectedContextId: plannedQ.plannedContextId || contextIdForDifficultyCalc
2640
3032
  };
2641
- try {
2642
- switch (plannedQ.plannedQuestionType) {
2643
- case "true_false":
2644
- ({ question } = await generateTrueFalseQuestion(baseQuestionInput));
2645
- break;
2646
- case "multiple_choice":
2647
- ({ question } = await generateMCQQuestion(__spreadProps(__spreadValues({}, baseQuestionInput), { numberOfOptions: 4 })));
2648
- break;
2649
- case "multiple_response":
2650
- ({ question } = await generateMRQQuestion(__spreadProps(__spreadValues({}, baseQuestionInput), { numberOfOptions: 5, minCorrectAnswers: 2, maxCorrectAnswers: 3 })));
2651
- break;
2652
- case "short_answer":
2653
- ({ question } = await generateShortAnswerQuestion(__spreadProps(__spreadValues({}, baseQuestionInput), { isCaseSensitive: false })));
2654
- break;
2655
- case "numeric":
2656
- ({ question } = await generateNumericQuestion(__spreadProps(__spreadValues({}, baseQuestionInput), { allowDecimals: true })));
2657
- break;
2658
- case "fill_in_the_blanks":
2659
- ({ question } = await generateFillInTheBlanksQuestion(__spreadProps(__spreadValues({}, baseQuestionInput), { numberOfBlanks: 2, isCaseSensitive: false })));
2660
- break;
2661
- case "sequence":
2662
- ({ question } = await generateSequenceQuestion(__spreadProps(__spreadValues({}, baseQuestionInput), { numberOfItems: 4 })));
2663
- break;
2664
- case "matching":
2665
- ({ question } = await generateMatchingQuestion(__spreadProps(__spreadValues({}, baseQuestionInput), { numberOfPairs: 4 })));
2666
- break;
2667
- case "drag_and_drop":
2668
- generationError = `Question type "drag_and_drop" is planned but generation is not yet supported.`;
2669
- console.warn(`Skipping Drag and Drop question generation for topic: ${plannedQ.plannedTopic}. Not implemented.`);
2670
- break;
2671
- default:
2672
- generationError = `Question type "${plannedQ.plannedQuestionType}" is not supported for automated generation in this flow.`;
2673
- }
2674
- if (question) {
2675
- question.topic = plannedQ.plannedTopic;
2676
- question.bloomLevel = plannedQ.plannedBloomLevel;
2677
- question.difficulty = difficultyForAI;
2678
- question.contextCode = plannedQ.plannedContextId || (contextDescriptionForAI ? (selectedContextIds == null ? void 0 : selectedContextIds[0]) || "__custom__" : void 0);
2679
- if (question.points === void 0) question.points = 10;
2680
- generatedQuestions.push(question);
2681
- } else if (!generationError) {
2682
- generationError = `AI did not return a question object for ${plannedQ.plannedQuestionType}.`;
2683
- }
2684
- } catch (e) {
2685
- generationError = e.message || `Unknown error generating ${plannedQ.plannedQuestionType} question for topic ${plannedQ.plannedTopic}.`;
3033
+ let result = {};
3034
+ switch (plannedQ.plannedQuestionType) {
3035
+ case "true_false":
3036
+ result = await generateTrueFalseQuestion(baseClientInput, apiKey);
3037
+ break;
3038
+ case "multiple_choice":
3039
+ result = await generateMCQQuestion(__spreadProps(__spreadValues({}, baseClientInput), { numberOfOptions: 4 }), apiKey);
3040
+ break;
3041
+ case "multiple_response":
3042
+ result = await generateMRQQuestion(__spreadProps(__spreadValues({}, baseClientInput), { numberOfOptions: 5, minCorrectAnswers: 2, maxCorrectAnswers: 3 }), apiKey);
3043
+ break;
3044
+ case "short_answer":
3045
+ result = await generateShortAnswerQuestion(__spreadProps(__spreadValues({}, baseClientInput), { isCaseSensitive: false }), apiKey);
3046
+ break;
3047
+ case "numeric":
3048
+ result = await generateNumericQuestion(__spreadProps(__spreadValues({}, baseClientInput), { allowDecimals: true }), apiKey);
3049
+ break;
3050
+ case "fill_in_the_blanks":
3051
+ result = await generateFillInTheBlanksQuestion(__spreadProps(__spreadValues({}, baseClientInput), { numberOfBlanks: 2, isCaseSensitive: false }), apiKey);
3052
+ break;
3053
+ case "sequence":
3054
+ result = await generateSequenceQuestion(__spreadProps(__spreadValues({}, baseClientInput), { numberOfItems: 4 }), apiKey);
3055
+ break;
3056
+ case "matching":
3057
+ result = await generateMatchingQuestion(__spreadProps(__spreadValues({}, baseClientInput), { numberOfPairs: 4, shuffleOptions: true }), apiKey);
3058
+ break;
3059
+ default:
3060
+ generationError = `Question type "${plannedQ.plannedQuestionType}" is not supported for automated generation.`;
2686
3061
  }
2687
- if (generationError) {
2688
- console.error(`Error during Stage 2 generation for planned question index ${i}: ${generationError}`);
2689
- errors.push({
2690
- plannedQuestionIndex: i,
2691
- plannedTopic: plannedQ.plannedTopic,
2692
- plannedQuestionType: plannedQ.plannedQuestionType,
2693
- error: generationError
2694
- });
3062
+ if (result.question) {
3063
+ const question = result.question;
3064
+ question.topic = plannedQ.plannedTopic;
3065
+ question.bloomLevel = plannedQ.plannedBloomLevel;
3066
+ question.difficulty = difficultyForAI;
3067
+ question.contextCode = baseClientInput.selectedContextId;
3068
+ if (question.points === void 0) question.points = 10;
3069
+ generatedQuestions.push(question);
3070
+ } else if (!generationError) {
3071
+ generationError = `AI did not return a question object for type '${plannedQ.plannedQuestionType}'.`;
2695
3072
  }
3073
+ } catch (e) {
3074
+ generationError = e.message || `An unknown error occurred.`;
3075
+ }
3076
+ if (generationError) {
3077
+ console.error(`Error generating question at index ${i} (Topic: ${plannedQ.plannedTopic}): ${generationError}`);
3078
+ errors.push({
3079
+ plannedQuestionIndex: i,
3080
+ plannedTopic: plannedQ.plannedTopic,
3081
+ plannedQuestionType: plannedQ.plannedQuestionType,
3082
+ error: generationError
3083
+ });
2696
3084
  }
2697
- return { generatedQuestions, errors: errors.length > 0 ? errors : void 0 };
2698
3085
  }
2699
- );
3086
+ return { generatedQuestions, errors: errors.length > 0 ? errors : void 0 };
3087
+ }
2700
3088
 
2701
3089
  // src/utils/utils.ts
2702
3090
  import { clsx } from "clsx";
@@ -2705,6 +3093,9 @@ function cn(...inputs) {
2705
3093
  return twMerge(clsx(inputs));
2706
3094
  }
2707
3095
  export {
3096
+ APIKeyService,
3097
+ GEMINI_API_KEY_SERVICE_NAME,
3098
+ QuizEditorService,
2708
3099
  QuizEngine,
2709
3100
  SCORMService,
2710
3101
  cn,