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