Files
sentence1/G120-sequence/sena_sdk.js
lubukhu b68589e782
All checks were successful
Deploy to Production / deploy (push) Successful in 6s
up
2026-01-27 17:18:17 +07:00

600 lines
24 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Sena SDK Constructor
* @param {Object} config - Configuration object for the SDK
* @param {Object} config.data - Quiz data containing question, options, and answer
*/
function SenaSDK(gid = 'G2510S1T30') {
// Initialize data
this.data = null;
this.correctAnswer = null;
this.gameCode = gid;
// Initialize properties
this.timeLimit = 0;
this.shuffle = true;
// tracking time in game
this.startTime = 0;
this.endTime = 0;
// Initialize Web Speech API
this.speechSynthesis = window.speechSynthesis;
this.currentUtterance = null;
this.voiceSettings = {
lang: 'en-US',
rate: 1.0, // Speed (0.1 to 10)
pitch: 1.0, // Pitch (0 to 2)
volume: 1.0 // Volume (0 to 1)
};
}
/**
* Shuffle array using Fisher-Yates algorithm
* @param {Array} array - Array to shuffle
*/
SenaSDK.prototype.shuffleArray = function(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
};
SenaSDK.prototype.load = function(callback,template = 'G2510S1T30') {
let self = this;
// get parameter LID from URL
const urlParams = new URLSearchParams(window.location.search);
const LID = urlParams.get('LID');
if (LID) {
self.gameCode = LID;
};
fetch(`https://senaai.tech/sample/${self.gameCode}.json`)
.then(response => response.json())
.then(data => {
self.data = data.data;
self.correctAnswer = data.answer || null;
// based on game code, set timeLimit and shuffle
const gameCode = self.gameCode || template;
const regex = /^G([1-5])([2-9])([0-2])([0-2])(?:S([0-1]))?(?:T(\d+))?$/;
const match = gameCode.match(regex);
if (match) {
const shuffleFlag = match[5] !== undefined ? match[5] : '1';
const timeStr = match[6] !== undefined ? match[6] : '0';
self.shuffle = (shuffleFlag === '1');
self.timeLimit = parseInt(timeStr, 10);
}
if (callback) callback(true);
})
.catch(error => {
console.error('Error loading LID data:', error);
if (callback) callback(false);
});
};
/**
* Generate comprehensive developer guide based on game code
* @returns {string} Developer guide with implementation instructions
*/
SenaSDK.prototype.guide = function() {
let self = this;
const gameCode = self.gameCode || 'G2510S1T30';
const data = self.data || {};
/**
* Regex giải thích:
* ^G([1-5])([2-9])([0-2])([0-2]) : Bắt buộc (Loại, Số lượng, Q, O)
* (?:S([0-1]))? : Không bắt buộc, mặc định S1
* (?:T(\d+))? : Không bắt buộc, mặc định T0
*/
const regex = /^G([1-5])([2-9])([0-2])([0-2])(?:S([0-1]))?(?:T(\d+))?$/;
const match = gameCode.match(regex);
if (!match) return "Mã game không hợp lệ! Định dạng chuẩn: Gxxxx hoặc GxxxxSxTxx";
const category = match[1];
const count = match[2];
const qIdx = match[3];
const oIdx = match[4];
const shuffle = match[5] !== undefined ? match[5] : '1';
const time = match[6] !== undefined ? match[6] : '0';
const types = { '0': 'Text', '1': 'Image', '2': 'Audio' };
let guide = '';
// Header
guide += `╔════════════════════════════════════════════════════════════════════════════╗\n`;
guide += `║ SENA SDK - DEVELOPER GUIDE: ${gameCode.padEnd(37)}\n`;
guide += `╚════════════════════════════════════════════════════════════════════════════╝\n\n`;
// Game Analysis
guide += `📊 GAME ANALYSIS\n`;
guide += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
let gameName = '', instruction = '', displayMode = '';
switch(category) {
case '1':
gameName = "Quiz (Trắc nghiệm)";
instruction = `Người dùng chọn 1 đáp án đúng trong ${count} options`;
displayMode = `Question: ${types[qIdx]} → Options: ${types[oIdx]}`;
break;
case '2':
if (qIdx === '0' && oIdx === '0') {
gameName = "Sort Word (Sắp xếp từ)";
instruction = `Sắp xếp ${count} từ/ký tự thành chuỗi hoàn chỉnh`;
} else {
gameName = "Sequences (Sắp xếp chuỗi)";
instruction = `Sắp xếp ${count} items theo đúng thứ tự`;
}
displayMode = `Hint: ${types[qIdx]} → Items: ${types[oIdx]}`;
break;
case '3':
if (qIdx === oIdx) {
gameName = "Memory Card (Trí nhớ)";
instruction = `Lật và ghép ${count} cặp thẻ giống nhau`;
} else {
gameName = "Matching (Nối cặp)";
instruction = `Nối ${count} items từ 2 nhóm với nhau`;
}
displayMode = `Group A: ${types[qIdx]} ↔ Group B: ${types[oIdx]}`;
break;
}
guide += `Game Type : ${gameName}\n`;
guide += `Objective : ${instruction}\n`;
guide += `Display Mode : ${displayMode}\n`;
guide += `Items Count : ${count}\n`;
guide += `Shuffle : ${shuffle === '1' ? 'YES (call sdk.start() to shuffle)' : 'NO (S0)'}\n`;
guide += `Time Limit : ${time === '0' ? 'Unlimited' : time + ' seconds'}\n`;
if (data.hint && data.hint.type) {
guide += `Hint Type : ${data.hint.type}\n`;
guide += `Hint Count : ${Array.isArray(data.hint.value) ? data.hint.value.length : '1'}\n`;
}
guide += `\n🔧 IMPLEMENTATION STEPS\n`;
guide += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n`;
// Step 1: Initialize
guide += `1⃣ INITIALIZE SDK\n`;
guide += ` var sdk = new SenaSDK('${gameCode}');\n`;
guide += ` sdk.load(function(success) {\n`;
guide += ` if (success) sdk.start();\n`;
guide += ` });\n\n`;
// Step 2: Display based on game type
guide += `2⃣ DISPLAY UI\n`;
if (category === '1') {
// Quiz
guide += ` // Display Question\n`;
if (qIdx === '0') {
guide += ` var questionText = sdk.getQuestionValue();\n`;
guide += ` displayText(questionText); // Show text\n`;
} else if (qIdx === '1') {
guide += ` var questionImg = sdk.getQuestionValue();\n`;
guide += ` displayImage(questionImg); // Show image URL\n`;
} else if (qIdx === '2') {
guide += ` sdk.playVoice('question'); // Auto play audio\n`;
}
guide += `\n // Display Options\n`;
guide += ` var optionsCount = sdk.getOptionsCount(); // ${count} items\n`;
guide += ` for (var i = 0; i < optionsCount; i++) {\n`;
if (oIdx === '0') {
guide += ` var optionText = sdk.getOptionsValue(i).text;\n`;
guide += ` createButton(optionText, i); // Show text button\n`;
} else if (oIdx === '1') {
guide += ` var optionImg = sdk.getOptionsValue(i).image;\n`;
guide += ` createImageButton(optionImg, i); // Show image button\n`;
} else if (oIdx === '2') {
guide += ` createAudioButton(i); // Button to play audio\n`;
guide += ` // onClick: sdk.playVoice('option' + (i+1));\n`;
}
guide += ` }\n`;
} else if (category === '2') {
// Sort/Sequences
guide += ` // Display Hint (if exists)\n`;
if (qIdx === '0') {
guide += ` var hintText = sdk.getQuestionValue() || sdk.getRequestValue();\n`;
guide += ` if (hintText) displayHint(hintText);\n`;
} else if (qIdx === '1') {
guide += ` var hintImg = sdk.getQuestionValue();\n`;
guide += ` if (hintImg) displayHintImage(hintImg);\n`;
}
guide += `\n // Display Draggable Items\n`;
guide += ` var itemsCount = sdk.getOptionsCount();\n`;
guide += ` for (var i = 0; i < itemsCount; i++) {\n`;
if (oIdx === '0') {
guide += ` var itemText = sdk.getOptionsValue(i).text;\n`;
guide += ` createDraggableText(itemText, i);\n`;
} else if (oIdx === '1') {
guide += ` var itemImg = sdk.getOptionsValue(i).image;\n`;
guide += ` createDraggableImage(itemImg, i);\n`;
}
guide += ` }\n`;
guide += ` // User drags to reorder items\n`;
} else if (category === '3') {
// Memory/Matching
guide += ` var itemsCount = sdk.getOptionsCount();\n`;
if (qIdx === oIdx) {
guide += ` // Memory Card - Create pairs\n`;
guide += ` var allCards = []; // Duplicate items for pairs\n`;
guide += ` for (var i = 0; i < itemsCount; i++) {\n`;
guide += ` allCards.push(sdk.getOptionsValue(i));\n`;
guide += ` allCards.push(sdk.getOptionsValue(i)); // Duplicate\n`;
guide += ` }\n`;
guide += ` shuffleArray(allCards);\n`;
guide += ` // Create face-down cards, flip on click\n`;
} else {
guide += ` // Matching - Create two groups\n`;
guide += ` for (var i = 0; i < itemsCount; i++) {\n`;
if (qIdx === '0') {
guide += ` var leftText = sdk.getOptionsValue(i).text;\n`;
guide += ` createLeftItem(leftText, i);\n`;
} else if (qIdx === '1') {
guide += ` var leftImg = sdk.getOptionsValue(i).image;\n`;
guide += ` createLeftItem(leftImg, i);\n`;
}
if (oIdx === '0') {
guide += ` var rightText = sdk.getOptionsValue(i).text; // Matching pair\n`;
guide += ` createRightItem(rightText, i);\n`;
} else if (oIdx === '1') {
guide += ` var rightImg = sdk.getOptionsValue(i).image;\n`;
guide += ` createRightItem(rightImg, i);\n`;
}
guide += ` }\n`;
guide += ` // User draws lines to match pairs\n`;
}
}
// Step 3: Handle Hint
if (data.hint && data.hint.type) {
guide += `\n3⃣ HANDLE HINT (Optional)\n`;
guide += ` var hintType = sdk.getHintType(); // "${data.hint.type}"\n`;
if (data.hint.type === 'display') {
guide += ` var hintCount = sdk.getHintCount();\n`;
guide += ` for (var i = 0; i < hintCount; i++) {\n`;
guide += ` var hintItem = sdk.getHintValue(i);\n`;
guide += ` displayHintItem(hintItem, i); // Show each hint\n`;
guide += ` }\n`;
} else if (data.hint.type === 'audio') {
guide += ` var hintAudio = sdk.getHintValue();\n`;
guide += ` createHintButton(hintAudio); // Play audio hint\n`;
} else if (data.hint.type === 'text') {
guide += ` var hintText = sdk.getHintValue();\n`;
guide += ` displayHintText(hintText);\n`;
}
}
// Step 4: Check Answer
const stepNum = data.hint ? '4⃣' : '3⃣';
guide += `\n${stepNum} CHECK ANSWER\n`;
if (category === '1') {
guide += ` // User clicks an option\n`;
guide += ` function onOptionClick(selectedIndex) {\n`;
guide += ` var userAnswer = sdk.getOptionsValue(selectedIndex).text;\n`;
guide += ` var result = sdk.end(userAnswer, function(isCorrect) {\n`;
guide += ` if (isCorrect) showSuccess();\n`;
guide += ` else showError();\n`;
guide += ` });\n`;
guide += ` }\n`;
} else if (category === '2') {
guide += ` // User finishes sorting\n`;
guide += ` function onSubmitOrder(orderedArray) {\n`;
guide += ` var answerStr = orderedArray.map(item => item.text).join('|');\n`;
guide += ` sdk.end(answerStr, function(isCorrect) {\n`;
guide += ` if (isCorrect) showSuccess();\n`;
guide += ` else showCorrectOrder();\n`;
guide += ` });\n`;
guide += ` }\n`;
} else if (category === '3') {
guide += ` // User completes all matches/pairs\n`;
guide += ` function onAllMatched(matchedPairs) {\n`;
guide += ` var answerStr = matchedPairs.map(p => p.text).join('|');\n`;
guide += ` sdk.end(answerStr, function(isCorrect) {\n`;
guide += ` showResult(isCorrect);\n`;
guide += ` });\n`;
guide += ` }\n`;
}
// Step 5: Timer
if (time !== '0') {
const nextStep = data.hint ? '5⃣' : '4⃣';
guide += `\n${nextStep} TIMER COUNTDOWN\n`;
guide += ` var timeLimit = sdk.timeLimit; // ${time} seconds\n`;
guide += ` startCountdown(timeLimit);\n`;
guide += ` // If time runs out before sdk.end(), user fails\n`;
}
// API Reference
guide += `\n\n📚 KEY API METHODS\n`;
guide += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
guide += `sdk.load(callback, template) - Load data from server\n`;
guide += `sdk.start() - Start game, shuffle if needed\n`;
guide += `sdk.getQuestionValue() - Get question text/image/audio\n`;
guide += `sdk.getQuestionType() - Get question type (text/image/audio)\n`;
guide += `sdk.getOptionsCount() - Get number of options\n`;
guide += `sdk.getOptionsValue(index) - Get option object at index\n`;
guide += `sdk.getOptionsType() - Get options type\n`;
guide += `sdk.getHintType() - Get hint type\n`;
guide += `sdk.getHintValue(index) - Get hint value/array item\n`;
guide += `sdk.getHintCount() - Get hint items count\n`;
guide += `sdk.playVoice(type) - Play TTS (question/option1/hint)\n`;
guide += `sdk.end(answer, callback) - Check answer & return result\n`;
guide += `sdk.timeLimit - Time limit in seconds\n`;
guide += `sdk.shuffle - Whether to shuffle options\n`;
guide += `\n\n💡 TIPS\n`;
guide += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
if (shuffle === '1') {
guide += `• Options are shuffled after sdk.start() - display in new order\n`;
}
if (time !== '0') {
guide += `• Implement timer UI and auto-submit when time expires\n`;
}
guide += `• Use Web Speech API: sdk.playVoice() for TTS in English\n`;
guide += `• Multiple answers format: "answer1|answer2|answer3"\n`;
guide += `• sdk.end() returns: {isCorrect, duration, correctAnswer, userAnswer}\n`;
guide += `\n${'═'.repeat(76)}\n`;
return guide;
};
/**
* Get the question text
* @returns {string} Question or request text
*/
SenaSDK.prototype.getQuestionValue = function() {
if (this.data.question && this.data.question !== "") {
return this.data.question;
} else {
return "";
}
};
/**
* Get the question type
* @returns {string} Question type (text, image, audio)
*/
SenaSDK.prototype.getQuestionType = function() {
let self = this;
// based on game code, determine question type
const gameCode = self.gameCode || 'G2510S1T30';
const regex = /^G([1-5])([2-9])([0-2])([0-2])(?:S([0-1]))?(?:T(\d+))?$/;
const match = gameCode.match(regex);
if (match) {
const qIdx = match[3];
const types = { '0': 'text', '1': 'image', '2': 'audio' };
return types[qIdx] || 'text';
}
return 'text';
};
/**
* Get the request value
* @returns {string} Request text
*/
SenaSDK.prototype.getRequestValue = function() {
if (this.data && this.data.request && this.data.request !== "") {
return this.data.request;
} else {
return "";
}
};
/**
* Get the request type (same as question type)
* @returns {string} Request type (text, image, audio)
*/
SenaSDK.prototype.getRequestType = function() {
return this.getQuestionType();
};
/**
* Get total number of options
* @returns {number} Number of options
*/
SenaSDK.prototype.getOptionsCount = function() {
return this.data.options.length;
};
/**
* Get options type based on game code
* @returns {string} Options type (text, image, audio)
*/
SenaSDK.prototype.getOptionsType = function() {
let self = this;
// based on game code, determine options type
const gameCode = self.gameCode || 'G2510S1T30';
const regex = /^G([1-5])([2-9])([0-2])([0-2])(?:S([0-1]))?(?:T(\d+))?$/;
const match = gameCode.match(regex);
if (match) {
const oIdx = match[4];
const types = { '0': 'text', '1': 'image', '2': 'audio' };
return types[oIdx] || 'text';
}
return 'text';
};
/**
* Get option value by index based on options type from game code
* @param {number} index - Option index
* @returns {string} Option value (text, image URL, or audio URL based on game code)
*/
SenaSDK.prototype.getOptionsValue = function(index) {
return this.data.options[index];
};
/**
* Get hint type
* @returns {string} Hint type (e.g., 'display', 'audio', 'text', or empty string if no hint)
*/
SenaSDK.prototype.getHintType = function() {
if (this.data && this.data.hint && this.data.hint.type) {
return this.data.hint.type;
}
return "";
};
/**
* Get hint count (number of elements if hint is an array, particularly for display type)
* @returns {number} Number of elements in hint array, or 1 if not an array, or 0 if no hint
*/
SenaSDK.prototype.getHintCount = function() {
const hintValue = this.getHintValue();
if (hintValue === null) {
return 0;
}
if (Array.isArray(hintValue)) {
return hintValue.length;
}
return 1;
};
/**
* Get hint value
* @param {number} index - Optional index for array hints (display type)
* @returns {*} Hint value (string, array element, or null if no hint)
*/
SenaSDK.prototype.getHintValue = function(index) {
if (this.data && this.data.hint && this.data.hint.value !== undefined) {
const hintValue = this.data.hint.value;
const hintType = this.getHintType();
// If hint type is display and value is array, return specific index
if (hintType === 'display' && Array.isArray(hintValue)) {
if (index !== undefined && index >= 0 && index < hintValue.length) {
return hintValue[index];
}
// If no index provided or invalid, return the whole array
return hintValue;
}
// For audio or text type, return the value directly
return hintValue;
}
return null;
};
/**
* Start the quiz - resets index and shuffles options
*/
SenaSDK.prototype.start = function() {
let self = this;
self.curIndex = 0;
if (self.shuffle) {
self.shuffleArray(self.data.options);
}
self.startTime = Date.now();
// Additional logic for tracking can be added here if needed
};
/**
* End the game and check answer
* @param {string} answer - User's answer (single text or multiple answers separated by |)
* @returns {Object} Result object with isCorrect, duration, correctAnswer, and userAnswer
*/
SenaSDK.prototype.end = function(answer , callback) {
let self = this;
self.endTime = Date.now();
const duration = (self.endTime - self.startTime) / 1000;
// Parse user answer - split by | for multiple answers
const userAnswers = answer.includes('|') ? answer.split('|').map(a => a.trim().toLowerCase()) : [answer.trim().toLowerCase()];
// Get correct answer(s) from data
let correctAnswers = [];
if (self.correctAnswer) {
// Check if answer is an array (multiple answers) or single answer
if (Array.isArray(self.correctAnswer)) {
correctAnswers = self.correctAnswer.map(a => {
if (typeof a === 'string') return a.toLowerCase();
if (a.text) return a.text.toLowerCase();
return '';
});
} else if (typeof self.correctAnswer === 'string') {
correctAnswers = [self.correctAnswer.toLowerCase()];
} else if (self.correctAnswer.text) {
correctAnswers = [self.correctAnswer.text.toLowerCase()];
}
}
// Check if answer is correct
let isCorrect = false;
if (userAnswers.length === correctAnswers.length) {
// For ordered multiple answers
isCorrect = userAnswers.every((ans, index) => ans === correctAnswers[index]);
} else if (userAnswers.length === 1 && correctAnswers.length === 1) {
// For single answer
isCorrect = userAnswers[0] === correctAnswers[0];
}
const result = {
isCorrect: isCorrect,
duration: duration,
correctAnswer: correctAnswers.join(' | '),
userAnswer: userAnswers.join(' | ')
};
// if time spent more than time limit, mark as incorrect
if (self.timeLimit > 0 && duration > self.timeLimit) {
result.isCorrect = false;
}
console.log(`Time spent in game: ${duration} seconds`);
console.log(`Result: ${isCorrect ? 'CORRECT' : 'INCORRECT'}`);
if (callback) callback(result.isCorrect);
};
SenaSDK.prototype.playVoice = function(type) {
let self = this;
// type: 'question', 'optionA', 'optionB', ...
// if type is options, get corresponding option text like option0 -> index 0
let textToSpeak = '';
if (type.startsWith('option')) {
const optionIndex = parseInt(type.slice(6)) - 1;
textToSpeak = self.getOptionsValue(optionIndex);
} else if (type === 'question') {
textToSpeak = self.getQuestionValue();
} else if (type === 'request') {
textToSpeak = self.getRequestValue();
} else if (type === 'hint') {
const hintValue = self.getHintValue();
if (typeof hintValue === 'string') {
textToSpeak = hintValue;
}
}
if(textToSpeak == "") return;
if (window['audioPlayer']) {
window['audioPlayer'].pause();
window['audioPlayer'].src = textToSpeak;
window['audioPlayer'].play();
} else {
window['audioPlayer'] = new Audio(textToSpeak);
window['audioPlayer'].play();
}
};
SenaSDK.prototype.helper = {};
SenaSDK.prototype.helper.CalcObjectPositions = function(n, objectWidth, margin, maxWidth) {
let self = this;
self.positions = [];
const totalWidth = n * objectWidth + (n - 1) * margin;
const startX = (maxWidth - totalWidth) / 2;
let positions = [];
for (let i = 0; i < n; i++) {
positions.push(startX + i * (objectWidth + margin));
}
positions.map(pos => pos + objectWidth / 2); // Adjusting to center the objects
self.positions = positions;
};
SenaSDK.prototype.helper.getPosXbyIndex = function(index) {
let self = this;
if (index < 0 || index >= self.positions.length) {
return null; // Return null if index is out of bounds
}
return self.positions[index];
};
// Export for different module systems
if (typeof module !== 'undefined' && module.exports) {
module.exports = SenaSDK;
} else if (typeof define === 'function' && define.amd) {
define([], function() { return SenaSDK; });
} else {
window.SenaSDK = SenaSDK;
}