This commit is contained in:
113
G102-sequence/sdk/package/dist/kit/GameDataHandler.d.ts
vendored
Normal file
113
G102-sequence/sdk/package/dist/kit/GameDataHandler.d.ts
vendored
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* GameDataHandler - Data Sanitizer & Scorer
|
||||
*
|
||||
* Game Codes:
|
||||
* - G001: Quiz text-text
|
||||
* - G002: Quiz audio-text (audio question, text answer)
|
||||
* - G003: Quiz text-audio (text question, audio answer)
|
||||
* - G004: Quiz image-text (image question, text answer)
|
||||
* - G005: Quiz text-image (text question, image answer)
|
||||
*
|
||||
* - G110: Sequence Word - no audio
|
||||
* - G111: Sequence Word - có audio, missing_letter_count từ item
|
||||
* - G112: Sequence Word - có audio, missing_letter_count từ item
|
||||
* - G113: Sequence Word - có audio, missing_letter_count từ item
|
||||
*
|
||||
* - G120: Sequence Sentence - no audio
|
||||
* - G121: Sequence Sentence - có audio, missing_letter_count từ item
|
||||
* - G122: Sequence Sentence - có audio, missing_letter_count từ item
|
||||
* - G123: Sequence Sentence - có audio, missing_letter_count từ item
|
||||
*/
|
||||
export type GameCode = 'G001' | 'G002' | 'G003' | 'G004' | 'G005' | 'G110' | 'G111' | 'G112' | 'G113' | 'G120' | 'G121' | 'G122' | 'G123';
|
||||
export declare const GAME_CODES: {
|
||||
readonly G001: {
|
||||
readonly name: "Quiz Text-Text";
|
||||
readonly category: "quiz";
|
||||
readonly hasAudio: false;
|
||||
readonly hasImage: false;
|
||||
};
|
||||
readonly G002: {
|
||||
readonly name: "Quiz Audio-Text";
|
||||
readonly category: "quiz";
|
||||
readonly hasAudio: true;
|
||||
readonly hasImage: false;
|
||||
};
|
||||
readonly G003: {
|
||||
readonly name: "Quiz Text-Audio";
|
||||
readonly category: "quiz";
|
||||
readonly hasAudio: true;
|
||||
readonly hasImage: false;
|
||||
};
|
||||
readonly G004: {
|
||||
readonly name: "Quiz Image-Text";
|
||||
readonly category: "quiz";
|
||||
readonly hasAudio: false;
|
||||
readonly hasImage: true;
|
||||
};
|
||||
readonly G005: {
|
||||
readonly name: "Quiz Text-Image";
|
||||
readonly category: "quiz";
|
||||
readonly hasAudio: false;
|
||||
readonly hasImage: true;
|
||||
};
|
||||
readonly G110: {
|
||||
readonly name: "Sequence Word";
|
||||
readonly category: "sequence_word";
|
||||
readonly hasAudio: false;
|
||||
};
|
||||
readonly G111: {
|
||||
readonly name: "Sequence Word Audio";
|
||||
readonly category: "sequence_word";
|
||||
readonly hasAudio: true;
|
||||
};
|
||||
readonly G112: {
|
||||
readonly name: "Sequence Word Audio";
|
||||
readonly category: "sequence_word";
|
||||
readonly hasAudio: true;
|
||||
};
|
||||
readonly G113: {
|
||||
readonly name: "Sequence Word Audio";
|
||||
readonly category: "sequence_word";
|
||||
readonly hasAudio: true;
|
||||
};
|
||||
readonly G120: {
|
||||
readonly name: "Sequence Sentence";
|
||||
readonly category: "sequence_sentence";
|
||||
readonly hasAudio: false;
|
||||
};
|
||||
readonly G121: {
|
||||
readonly name: "Sequence Sentence Audio";
|
||||
readonly category: "sequence_sentence";
|
||||
readonly hasAudio: true;
|
||||
};
|
||||
readonly G122: {
|
||||
readonly name: "Sequence Sentence Audio";
|
||||
readonly category: "sequence_sentence";
|
||||
readonly hasAudio: true;
|
||||
};
|
||||
readonly G123: {
|
||||
readonly name: "Sequence Sentence Audio";
|
||||
readonly category: "sequence_sentence";
|
||||
readonly hasAudio: true;
|
||||
};
|
||||
};
|
||||
export declare function getGameCategory(code: GameCode): string;
|
||||
/**
|
||||
* Sanitize game data before sending to iframe
|
||||
* CRITICAL: Never send answers/correct data to client
|
||||
*/
|
||||
export declare function sanitizeForClient(code: GameCode, items: any[]): any[];
|
||||
export interface AnswerCheckResult {
|
||||
isCorrect: boolean;
|
||||
score: number;
|
||||
feedback?: string;
|
||||
}
|
||||
/**
|
||||
* Check if player's answer is correct
|
||||
*
|
||||
* @param code - Game code (G001, G110, etc.)
|
||||
* @param originalItem - Original item from server (has answer field!)
|
||||
* @param playerAnswer - Player's answer (text for quiz, array for sequence)
|
||||
*/
|
||||
export declare function checkAnswer(code: GameCode, originalItem: any, playerAnswer: any): AnswerCheckResult;
|
||||
//# sourceMappingURL=GameDataHandler.d.ts.map
|
||||
1
G102-sequence/sdk/package/dist/kit/GameDataHandler.d.ts.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/kit/GameDataHandler.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"GameDataHandler.d.ts","sourceRoot":"","sources":["../../src/kit/GameDataHandler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAMH,MAAM,MAAM,QAAQ,GAEd,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAE1C,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAEjC,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAGxC,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmBb,CAAC;AAEX,wBAAgB,eAAe,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAEtD;AAMD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,GAAG,EAAE,CAkDrE;AAwSD,MAAM,WAAW,iBAAiB;IAC9B,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,QAAQ,EAAE,YAAY,EAAE,GAAG,EAAE,YAAY,EAAE,GAAG,GAAG,iBAAiB,CA4BnG"}
|
||||
450
G102-sequence/sdk/package/dist/kit/GameDataHandler.js
vendored
Normal file
450
G102-sequence/sdk/package/dist/kit/GameDataHandler.js
vendored
Normal file
@@ -0,0 +1,450 @@
|
||||
"use strict";
|
||||
/**
|
||||
* GameDataHandler - Data Sanitizer & Scorer
|
||||
*
|
||||
* Game Codes:
|
||||
* - G001: Quiz text-text
|
||||
* - G002: Quiz audio-text (audio question, text answer)
|
||||
* - G003: Quiz text-audio (text question, audio answer)
|
||||
* - G004: Quiz image-text (image question, text answer)
|
||||
* - G005: Quiz text-image (text question, image answer)
|
||||
*
|
||||
* - G110: Sequence Word - no audio
|
||||
* - G111: Sequence Word - có audio, missing_letter_count từ item
|
||||
* - G112: Sequence Word - có audio, missing_letter_count từ item
|
||||
* - G113: Sequence Word - có audio, missing_letter_count từ item
|
||||
*
|
||||
* - G120: Sequence Sentence - no audio
|
||||
* - G121: Sequence Sentence - có audio, missing_letter_count từ item
|
||||
* - G122: Sequence Sentence - có audio, missing_letter_count từ item
|
||||
* - G123: Sequence Sentence - có audio, missing_letter_count từ item
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.GAME_CODES = void 0;
|
||||
exports.getGameCategory = getGameCategory;
|
||||
exports.sanitizeForClient = sanitizeForClient;
|
||||
exports.checkAnswer = checkAnswer;
|
||||
// Game code metadata
|
||||
exports.GAME_CODES = {
|
||||
// Quiz
|
||||
G001: { name: 'Quiz Text-Text', category: 'quiz', hasAudio: false, hasImage: false },
|
||||
G002: { name: 'Quiz Audio-Text', category: 'quiz', hasAudio: true, hasImage: false },
|
||||
G003: { name: 'Quiz Text-Audio', category: 'quiz', hasAudio: true, hasImage: false },
|
||||
G004: { name: 'Quiz Image-Text', category: 'quiz', hasAudio: false, hasImage: true },
|
||||
G005: { name: 'Quiz Text-Image', category: 'quiz', hasAudio: false, hasImage: true },
|
||||
// Sequence Word
|
||||
G110: { name: 'Sequence Word', category: 'sequence_word', hasAudio: false },
|
||||
G111: { name: 'Sequence Word Audio', category: 'sequence_word', hasAudio: true },
|
||||
G112: { name: 'Sequence Word Audio', category: 'sequence_word', hasAudio: true },
|
||||
G113: { name: 'Sequence Word Audio', category: 'sequence_word', hasAudio: true },
|
||||
// Sequence Sentence
|
||||
G120: { name: 'Sequence Sentence', category: 'sequence_sentence', hasAudio: false },
|
||||
G121: { name: 'Sequence Sentence Audio', category: 'sequence_sentence', hasAudio: true },
|
||||
G122: { name: 'Sequence Sentence Audio', category: 'sequence_sentence', hasAudio: true },
|
||||
G123: { name: 'Sequence Sentence Audio', category: 'sequence_sentence', hasAudio: true },
|
||||
};
|
||||
function getGameCategory(code) {
|
||||
return exports.GAME_CODES[code]?.category || 'unknown';
|
||||
}
|
||||
// =============================================================================
|
||||
// SANITIZE DATA FOR CLIENT (REMOVE ANSWERS)
|
||||
// =============================================================================
|
||||
/**
|
||||
* Sanitize game data before sending to iframe
|
||||
* CRITICAL: Never send answers/correct data to client
|
||||
*/
|
||||
function sanitizeForClient(code, items) {
|
||||
if (!Array.isArray(items))
|
||||
return [];
|
||||
switch (code) {
|
||||
// ===== QUIZ VARIANTS =====
|
||||
case 'G001': // Quiz text-text
|
||||
return sanitizeQuizTextText(items);
|
||||
case 'G002': // Quiz audio-text
|
||||
return sanitizeQuizAudioText(items);
|
||||
case 'G003': // Quiz text-audio
|
||||
return sanitizeQuizTextAudio(items);
|
||||
case 'G004': // Quiz image-text
|
||||
return sanitizeQuizImageText(items);
|
||||
case 'G005': // Quiz text-image
|
||||
return sanitizeQuizTextImage(items);
|
||||
// ===== SEQUENCE WORD VARIANTS =====
|
||||
case 'G110': // Sequence word
|
||||
return sanitizeSequenceWord(items);
|
||||
case 'G111': // Sequence word
|
||||
return sanitizeSequenceWord(items);
|
||||
case 'G112': // Sequence word
|
||||
return sanitizeSequenceWord(items);
|
||||
case 'G113': // Sequence word
|
||||
return sanitizeSequenceWord(items);
|
||||
// ===== SEQUENCE SENTENCE VARIANTS =====
|
||||
case 'G120': // Sequence sentence
|
||||
return sanitizeSequenceSentence(items);
|
||||
case 'G121': // Sequence sentence
|
||||
return sanitizeSequenceSentence(items);
|
||||
case 'G122': // Sequence sentence
|
||||
return sanitizeSequenceSentence(items);
|
||||
case 'G123': // Sequence sentence
|
||||
return sanitizeSequenceSentence(items);
|
||||
default:
|
||||
console.warn(`[GameDataHandler] Unknown game code: ${code}, returning raw data`);
|
||||
return items;
|
||||
}
|
||||
}
|
||||
// =============================================================================
|
||||
// QUIZ SANITIZERS
|
||||
// =============================================================================
|
||||
/**
|
||||
* G001: Quiz Text-Text
|
||||
* Client receives: id, question, options (shuffled)
|
||||
* Client does NOT receive: answer
|
||||
*/
|
||||
function sanitizeQuizTextText(items) {
|
||||
return items.map(item => {
|
||||
// Normalize options to {text: string}
|
||||
const options = (item.options || []).map((o) => {
|
||||
if (typeof o === 'string') {
|
||||
return { text: o };
|
||||
}
|
||||
if (o && typeof o === 'object') {
|
||||
return { text: String(o.text ?? '') };
|
||||
}
|
||||
return { text: String(o ?? '') };
|
||||
});
|
||||
// Shuffle to hide answer position
|
||||
shuffleArray(options);
|
||||
// Save shuffled text order for SDK to resolve index
|
||||
const shuffledTexts = options.map((o) => String(o.text ?? ''));
|
||||
return {
|
||||
id: item.id,
|
||||
question: item.question,
|
||||
options: options,
|
||||
__shuffledOptions: shuffledTexts, // SDK internal
|
||||
};
|
||||
});
|
||||
}
|
||||
/**
|
||||
* G002: Quiz Audio-Text
|
||||
* Client receives: id, question (audio URL), options (shuffled)
|
||||
* Client does NOT receive: answer
|
||||
*/
|
||||
function sanitizeQuizAudioText(items) {
|
||||
return items.map(item => {
|
||||
const options = (item.options || []).map((o) => {
|
||||
if (typeof o === 'string') {
|
||||
return { text: o };
|
||||
}
|
||||
if (o && typeof o === 'object') {
|
||||
return { text: String(o.text ?? '') };
|
||||
}
|
||||
return { text: String(o ?? '') };
|
||||
});
|
||||
shuffleArray(options);
|
||||
const shuffledTexts = options.map((o) => String(o.text ?? ''));
|
||||
return {
|
||||
id: item.id,
|
||||
question: item.audio || item.audio_url,
|
||||
options: options,
|
||||
__shuffledOptions: shuffledTexts,
|
||||
};
|
||||
});
|
||||
}
|
||||
/**
|
||||
* G003: Quiz Text-Audio
|
||||
* Client receives: id, question (text), options (audio URLs shuffled)
|
||||
* Client does NOT receive: answer
|
||||
*/
|
||||
function sanitizeQuizTextAudio(items) {
|
||||
return items.map(item => {
|
||||
const options = (item.options || []).map((o) => {
|
||||
if (typeof o === 'string') {
|
||||
return { audio: o };
|
||||
}
|
||||
if (o && typeof o === 'object') {
|
||||
const audioUrl = o.audio || o.audio_url || '';
|
||||
return { audio: String(audioUrl) };
|
||||
}
|
||||
return { audio: String(o ?? '') };
|
||||
});
|
||||
shuffleArray(options);
|
||||
const shuffledAudios = options.map((o) => String(o.audio ?? ''));
|
||||
return {
|
||||
id: item.id,
|
||||
question: item.question,
|
||||
options: options,
|
||||
__shuffledOptions: shuffledAudios,
|
||||
};
|
||||
});
|
||||
}
|
||||
/**
|
||||
* G004: Quiz Image-Text
|
||||
* Client receives: id, image_url, question (hint), options (shuffled)
|
||||
* Client does NOT receive: answer
|
||||
*/
|
||||
function sanitizeQuizImageText(items) {
|
||||
return items.map(item => {
|
||||
const options = (item.options || []).map((o) => {
|
||||
if (typeof o === 'string') {
|
||||
return { text: o };
|
||||
}
|
||||
if (o && typeof o === 'object') {
|
||||
return { text: String(o.text ?? '') };
|
||||
}
|
||||
return { text: String(o ?? '') };
|
||||
});
|
||||
shuffleArray(options);
|
||||
const shuffledTexts = options.map((o) => String(o.text ?? ''));
|
||||
return {
|
||||
id: item.id,
|
||||
image_url: item.image_url,
|
||||
question: item.question,
|
||||
options: options,
|
||||
__shuffledOptions: shuffledTexts,
|
||||
};
|
||||
});
|
||||
}
|
||||
/**
|
||||
* G005: Quiz Text-Image
|
||||
* Client receives: id, question (text), options (image URLs shuffled)
|
||||
* Client does NOT receive: answer
|
||||
*/
|
||||
function sanitizeQuizTextImage(items) {
|
||||
return items.map(item => {
|
||||
const options = (item.options || []).map((o) => {
|
||||
if (typeof o === 'string') {
|
||||
return { image_url: o };
|
||||
}
|
||||
if (o && typeof o === 'object') {
|
||||
const imageUrl = o.image_url || o.image || '';
|
||||
return { image_url: String(imageUrl) };
|
||||
}
|
||||
return { image_url: String(o ?? '') };
|
||||
});
|
||||
shuffleArray(options);
|
||||
const shuffledUrls = options.map((o) => String(o.image_url ?? ''));
|
||||
return {
|
||||
id: item.id,
|
||||
question: item.question,
|
||||
options: options, // Each option has {image_url: ...}
|
||||
__shuffledOptions: shuffledUrls,
|
||||
};
|
||||
});
|
||||
}
|
||||
// =============================================================================
|
||||
// SEQUENCE WORD SANITIZERS
|
||||
// =============================================================================
|
||||
/**
|
||||
* Sequence Word (G110-G113)
|
||||
* Client receives: id, question (array with blanks), options (missing letters shuffled), audio_url (optional)
|
||||
* Client does NOT receive: word, parts, answer, missing_letter_count
|
||||
*
|
||||
* Logic:
|
||||
* 1. Read missing_letter_count from item (count of letters to blank out)
|
||||
* 2. Randomly select positions to blank
|
||||
* 3. question: array with blanks at selected positions
|
||||
* 4. options: extracted missing letters (shuffled)
|
||||
*/
|
||||
function sanitizeSequenceWord(items) {
|
||||
return items.map(item => {
|
||||
const parts = item.answer || item.parts || [];
|
||||
const missingCount = item.missing_letter_count || 0;
|
||||
if (missingCount === 0 || parts.length === 0) {
|
||||
// No missing - all visible
|
||||
return {
|
||||
id: item.id,
|
||||
question: [...parts],
|
||||
options: [],
|
||||
...(item.audio_url && { audio_url: item.audio_url })
|
||||
};
|
||||
}
|
||||
// Randomly select which positions to blank
|
||||
const allIndices = Array.from({ length: parts.length }, (_, i) => i);
|
||||
const blankIndices = new Set();
|
||||
const count = Math.min(missingCount, parts.length);
|
||||
while (blankIndices.size < count) {
|
||||
const randomIdx = Math.floor(Math.random() * allIndices.length);
|
||||
const actualIdx = allIndices[randomIdx];
|
||||
blankIndices.add(actualIdx);
|
||||
allIndices.splice(randomIdx, 1);
|
||||
}
|
||||
// Build question array with blanks at random positions
|
||||
const question = parts.map((p, i) => blankIndices.has(i) ? "" : String(p));
|
||||
// Extract missing letters and shuffle
|
||||
const missingLetters = Array.from(blankIndices).map(i => String(parts[i]));
|
||||
shuffleArray(missingLetters);
|
||||
const result = {
|
||||
id: item.id,
|
||||
question, // e.g. ["H", "", "L", "", "O"]
|
||||
options: missingLetters, // e.g. ["L", "E"] - shuffled
|
||||
__shuffledOptions: [...missingLetters] // SDK internal: to resolve indices
|
||||
};
|
||||
if (item.audio_url) {
|
||||
result.audio_url = item.audio_url;
|
||||
}
|
||||
// CRITICAL: Do NOT send word, parts, answer, missing_letter_count
|
||||
return result;
|
||||
});
|
||||
}
|
||||
// =============================================================================
|
||||
// SEQUENCE SENTENCE SANITIZERS
|
||||
// =============================================================================
|
||||
/**
|
||||
* Sequence Sentence (G120-G123)
|
||||
* Client receives: id, question (array with blanks), options (missing words shuffled), audio_url (optional)
|
||||
* Client does NOT receive: sentence, parts, answer, missing_letter_count
|
||||
*
|
||||
* Logic: Same as Sequence Word
|
||||
* 1. Read missing_letter_count from item
|
||||
* 2. Randomly select positions to blank
|
||||
* 3. question: array with blanks
|
||||
* 4. options: extracted missing words (shuffled)
|
||||
*/
|
||||
function sanitizeSequenceSentence(items) {
|
||||
return items.map(item => {
|
||||
const parts = item.answer || item.parts || [];
|
||||
const missingCount = item.missing_letter_count || 0;
|
||||
if (missingCount === 0 || parts.length === 0) {
|
||||
// No missing - all visible
|
||||
return {
|
||||
id: item.id,
|
||||
question: [...parts],
|
||||
options: [],
|
||||
...(item.audio_url && { audio_url: item.audio_url })
|
||||
};
|
||||
}
|
||||
// Randomly select which positions to blank
|
||||
const allIndices = Array.from({ length: parts.length }, (_, i) => i);
|
||||
const blankIndices = new Set();
|
||||
const count = Math.min(missingCount, parts.length);
|
||||
while (blankIndices.size < count) {
|
||||
const randomIdx = Math.floor(Math.random() * allIndices.length);
|
||||
const actualIdx = allIndices[randomIdx];
|
||||
blankIndices.add(actualIdx);
|
||||
allIndices.splice(randomIdx, 1);
|
||||
}
|
||||
// Build question array with blanks at random positions
|
||||
const question = parts.map((p, i) => blankIndices.has(i) ? "" : String(p));
|
||||
// Extract missing words and shuffle
|
||||
const missingWords = Array.from(blankIndices).map(i => String(parts[i]));
|
||||
shuffleArray(missingWords);
|
||||
const result = {
|
||||
id: item.id,
|
||||
question, // e.g. ["I", "", "reading", ""]
|
||||
options: missingWords, // e.g. ["love", "books"] - shuffled
|
||||
__shuffledOptions: [...missingWords] // SDK internal
|
||||
};
|
||||
if (item.audio_url) {
|
||||
result.audio_url = item.audio_url;
|
||||
}
|
||||
// CRITICAL: Do NOT send sentence, parts, answer, missing_letter_count
|
||||
return result;
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Check if player's answer is correct
|
||||
*
|
||||
* @param code - Game code (G001, G110, etc.)
|
||||
* @param originalItem - Original item from server (has answer field!)
|
||||
* @param playerAnswer - Player's answer (text for quiz, array for sequence)
|
||||
*/
|
||||
function checkAnswer(code, originalItem, playerAnswer) {
|
||||
switch (code) {
|
||||
// ===== QUIZ VARIANTS (G001-G005) =====
|
||||
case 'G001': // Quiz Text-Text
|
||||
case 'G002': // Quiz Audio-Text
|
||||
case 'G003': // Quiz Text-Audio
|
||||
case 'G004': // Quiz Image-Text
|
||||
case 'G005': // Quiz Text-Image
|
||||
return checkQuizAnswer(originalItem, playerAnswer);
|
||||
// ===== SEQUENCE WORD VARIANTS (G110-G113) =====
|
||||
case 'G110': // Sequence Word
|
||||
case 'G111': // Sequence Word Audio
|
||||
case 'G112': // Sequence Word Audio
|
||||
case 'G113': // Sequence Word Audio
|
||||
return checkSequenceAnswer(originalItem, playerAnswer);
|
||||
// ===== SEQUENCE SENTENCE VARIANTS (G120-G123) =====
|
||||
case 'G120': // Sequence Sentence
|
||||
case 'G121': // Sequence Sentence Audio
|
||||
case 'G122': // Sequence Sentence Audio
|
||||
case 'G123': // Sequence Sentence Audio
|
||||
return checkSequenceAnswer(originalItem, playerAnswer);
|
||||
default:
|
||||
console.warn(`[GameDataHandler] Unknown game code for scoring: ${code}`);
|
||||
return { isCorrect: false, score: 0 };
|
||||
}
|
||||
}
|
||||
// Quiz Scoring
|
||||
function checkQuizAnswer(item, answerChoice) {
|
||||
const correctAnswer = String(item.answer || '');
|
||||
if (!correctAnswer) {
|
||||
return { isCorrect: false, score: 0, feedback: 'No correct answer defined' };
|
||||
}
|
||||
let playerAnswerText;
|
||||
if (typeof answerChoice === 'number') {
|
||||
// Index: resolve from original options
|
||||
if (Array.isArray(item.options)) {
|
||||
const v = item.options[answerChoice];
|
||||
if (typeof v === 'string') {
|
||||
playerAnswerText = v;
|
||||
}
|
||||
else if (v && typeof v === 'object' && 'text' in v) {
|
||||
playerAnswerText = String(v.text ?? '');
|
||||
}
|
||||
else {
|
||||
playerAnswerText = String(v ?? '');
|
||||
}
|
||||
}
|
||||
else {
|
||||
return { isCorrect: false, score: 0, feedback: 'Invalid question options' };
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Direct text answer
|
||||
if (answerChoice && typeof answerChoice === 'object' && 'text' in answerChoice) {
|
||||
playerAnswerText = String(answerChoice.text ?? '');
|
||||
}
|
||||
else {
|
||||
playerAnswerText = String(answerChoice ?? '');
|
||||
}
|
||||
}
|
||||
const isCorrect = playerAnswerText.toLowerCase().trim() === correctAnswer.toLowerCase().trim();
|
||||
return {
|
||||
isCorrect,
|
||||
score: isCorrect ? 1 : 0,
|
||||
feedback: isCorrect
|
||||
? `✅ Correct! "${playerAnswerText}" matches answer "${correctAnswer}"`
|
||||
: `❌ Wrong. You chose "${playerAnswerText}" but correct answer is "${correctAnswer}"`
|
||||
};
|
||||
}
|
||||
// Sequence Scoring
|
||||
function checkSequenceAnswer(item, answer) {
|
||||
const correctOrder = item.answer || item.parts;
|
||||
if (!Array.isArray(answer) || !Array.isArray(correctOrder)) {
|
||||
return { isCorrect: false, score: 0 };
|
||||
}
|
||||
const isCorrect = arraysEqual(answer, correctOrder);
|
||||
return {
|
||||
isCorrect,
|
||||
score: isCorrect ? 1 : 0,
|
||||
};
|
||||
}
|
||||
// =============================================================================
|
||||
// UTILITIES
|
||||
// =============================================================================
|
||||
function shuffleArray(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]];
|
||||
}
|
||||
}
|
||||
function arraysEqual(a, b) {
|
||||
if (a.length !== b.length)
|
||||
return false;
|
||||
return a.every((val, idx) => {
|
||||
if (typeof val === 'string' && typeof b[idx] === 'string') {
|
||||
return val.toLowerCase().trim() === b[idx].toLowerCase().trim();
|
||||
}
|
||||
return val === b[idx];
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=GameDataHandler.js.map
|
||||
1
G102-sequence/sdk/package/dist/kit/GameDataHandler.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/kit/GameDataHandler.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
19
G102-sequence/sdk/package/dist/kit/api.d.ts
vendored
Normal file
19
G102-sequence/sdk/package/dist/kit/api.d.ts
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Game API Client Kit
|
||||
* Standardized API client for communicating with Game Backend
|
||||
*/
|
||||
export interface GameApiConfig {
|
||||
baseUrl: string;
|
||||
getHeaders?: () => Record<string, string>;
|
||||
}
|
||||
export declare class GameApiClient {
|
||||
private config;
|
||||
constructor(config: GameApiConfig);
|
||||
private request;
|
||||
getGameWithProgress(assignmentId: number | string, studentId: string, refresh?: boolean): Promise<any>;
|
||||
startLiveSession(assignmentId: number | string, studentId: string, refresh?: boolean): Promise<any>;
|
||||
submitAnswer(assignmentId: number | string, studentId: string, questionId: string, answer: any, timeSpent?: number, isTimeout?: boolean): Promise<any>;
|
||||
completeSession(assignmentId: number | string, studentId: string): Promise<any>;
|
||||
getLeaderboard(assignmentId: number | string, studentId: string): Promise<any>;
|
||||
}
|
||||
//# sourceMappingURL=api.d.ts.map
|
||||
1
G102-sequence/sdk/package/dist/kit/api.d.ts.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/kit/api.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/kit/api.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,aAAa;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7C;AAED,qBAAa,aAAa;IACV,OAAO,CAAC,MAAM;gBAAN,MAAM,EAAE,aAAa;YAE3B,OAAO;IA6Cf,mBAAmB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,UAAQ;IAIrF,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,UAAQ;IAOlF,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,SAAI,EAAE,SAAS,UAAQ;IAWhI,eAAe,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE,SAAS,EAAE,MAAM;IAOhE,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE,SAAS,EAAE,MAAM;CAGxE"}
|
||||
86
G102-sequence/sdk/package/dist/kit/api.js
vendored
Normal file
86
G102-sequence/sdk/package/dist/kit/api.js
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
"use strict";
|
||||
/**
|
||||
* Game API Client Kit
|
||||
* Standardized API client for communicating with Game Backend
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.GameApiClient = void 0;
|
||||
class GameApiClient {
|
||||
constructor(config) {
|
||||
this.config = config;
|
||||
}
|
||||
async request(method, endpoint, body) {
|
||||
const url = `${this.config.baseUrl}${endpoint}`;
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...(this.config.getHeaders ? this.config.getHeaders() : {})
|
||||
};
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers,
|
||||
body: body ? JSON.stringify(body) : undefined
|
||||
});
|
||||
if (!res.ok) {
|
||||
const errorBody = await res.text();
|
||||
let errorMessage = `API Error ${res.status}: ${res.statusText}`;
|
||||
let errorCode;
|
||||
try {
|
||||
const jsonError = JSON.parse(errorBody);
|
||||
// Capture error code from response
|
||||
if (jsonError.code !== undefined) {
|
||||
errorCode = jsonError.code;
|
||||
}
|
||||
if (jsonError.message)
|
||||
errorMessage += ` - ${jsonError.message}`;
|
||||
else if (jsonError.error)
|
||||
errorMessage += ` - ${jsonError.error}`;
|
||||
}
|
||||
catch (e) {
|
||||
if (errorBody && errorBody.length < 200)
|
||||
errorMessage += ` - ${errorBody}`;
|
||||
}
|
||||
// Throw error object with code and message
|
||||
const error = new Error(errorMessage);
|
||||
error.code = errorCode;
|
||||
error.httpStatus = res.status;
|
||||
throw error;
|
||||
}
|
||||
return await res.json();
|
||||
}
|
||||
catch (error) {
|
||||
console.error('[GameApiClient] Request failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
async getGameWithProgress(assignmentId, studentId, refresh = false) {
|
||||
return this.request('GET', `/submissions/live/init/${assignmentId}/${studentId}${refresh ? '?refresh=1' : ''}`);
|
||||
}
|
||||
async startLiveSession(assignmentId, studentId, refresh = false) {
|
||||
return this.request('POST', `/submissions/live/start${refresh ? '?refresh=1' : ''}`, {
|
||||
assignment_id: assignmentId,
|
||||
student_id: studentId
|
||||
});
|
||||
}
|
||||
async submitAnswer(assignmentId, studentId, questionId, answer, timeSpent = 5, isTimeout = false) {
|
||||
return this.request('POST', '/submissions/live/answer', {
|
||||
assignment_id: assignmentId,
|
||||
student_id: studentId,
|
||||
question_id: questionId,
|
||||
selected_answer: answer,
|
||||
time_spent: timeSpent,
|
||||
is_timeout: isTimeout
|
||||
});
|
||||
}
|
||||
async completeSession(assignmentId, studentId) {
|
||||
return this.request('POST', '/submissions/live/complete', {
|
||||
assignment_id: assignmentId,
|
||||
student_id: studentId
|
||||
});
|
||||
}
|
||||
async getLeaderboard(assignmentId, studentId) {
|
||||
return this.request('GET', `/submissions/leaderboard/${assignmentId}?student_id=${studentId}`);
|
||||
}
|
||||
}
|
||||
exports.GameApiClient = GameApiClient;
|
||||
//# sourceMappingURL=api.js.map
|
||||
1
G102-sequence/sdk/package/dist/kit/api.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/kit/api.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/kit/api.ts"],"names":[],"mappings":"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"}
|
||||
6
G102-sequence/sdk/package/dist/kit/index.d.ts
vendored
Normal file
6
G102-sequence/sdk/package/dist/kit/index.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from './mappers';
|
||||
export * from './api';
|
||||
export * from './GameDataHandler';
|
||||
export * from './react/GamePlayer';
|
||||
export * from './react/GameTestPlayer';
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
1
G102-sequence/sdk/package/dist/kit/index.d.ts.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/kit/index.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/kit/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,OAAO,CAAC;AACtB,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC"}
|
||||
22
G102-sequence/sdk/package/dist/kit/index.js
vendored
Normal file
22
G102-sequence/sdk/package/dist/kit/index.js
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
||||
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
__exportStar(require("./mappers"), exports);
|
||||
__exportStar(require("./api"), exports);
|
||||
__exportStar(require("./GameDataHandler"), exports);
|
||||
__exportStar(require("./react/GamePlayer"), exports);
|
||||
__exportStar(require("./react/GameTestPlayer"), exports);
|
||||
//# sourceMappingURL=index.js.map
|
||||
1
G102-sequence/sdk/package/dist/kit/index.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/kit/index.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/kit/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,4CAA0B;AAC1B,wCAAsB;AACtB,oDAAkC;AAClC,qDAAmC;AACnC,yDAAuC"}
|
||||
31
G102-sequence/sdk/package/dist/kit/mappers.d.ts
vendored
Normal file
31
G102-sequence/sdk/package/dist/kit/mappers.d.ts
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Mappers Kit
|
||||
* Helper functions to transform data between Client App and Game Iframe
|
||||
*/
|
||||
export interface GamePayloadOptions {
|
||||
gameId: string | number;
|
||||
userId: string;
|
||||
gameData: any;
|
||||
answeredQuestions?: any[];
|
||||
extraData?: Record<string, any>;
|
||||
endTimeIso?: string;
|
||||
}
|
||||
export interface IframeGamePayload {
|
||||
game_id: string | number;
|
||||
user_id: string;
|
||||
data: any[];
|
||||
completed_question_ids: {
|
||||
id: string;
|
||||
result: number;
|
||||
}[];
|
||||
end_time_iso?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
export declare function prepareCompletedQuestions(answeredQuestions: any[]): {
|
||||
id: string;
|
||||
result: number;
|
||||
}[];
|
||||
export declare function createGamePayload(options: GamePayloadOptions): IframeGamePayload;
|
||||
export declare function createLeaderboardPayload(apiData: any): any;
|
||||
export declare function normalizeAnswerReport(data: any): any;
|
||||
//# sourceMappingURL=mappers.d.ts.map
|
||||
1
G102-sequence/sdk/package/dist/kit/mappers.d.ts.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/kit/mappers.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"mappers.d.ts","sourceRoot":"","sources":["../../src/kit/mappers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,kBAAkB;IAC/B,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC;IACd,iBAAiB,CAAC,EAAE,GAAG,EAAE,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,iBAAiB;IAC9B,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,GAAG,EAAE,CAAC;IACZ,sBAAsB,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACzD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACtB;AAED,wBAAgB,yBAAyB,CAAC,iBAAiB,EAAE,GAAG,EAAE,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EAAE,CAKpG;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,iBAAiB,CA+BhF;AAED,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,GAAG,GAAG,GAAG,CAsB1D;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,GAAG,GAAG,GAAG,CAWpD"}
|
||||
81
G102-sequence/sdk/package/dist/kit/mappers.js
vendored
Normal file
81
G102-sequence/sdk/package/dist/kit/mappers.js
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
"use strict";
|
||||
/**
|
||||
* Mappers Kit
|
||||
* Helper functions to transform data between Client App and Game Iframe
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.prepareCompletedQuestions = prepareCompletedQuestions;
|
||||
exports.createGamePayload = createGamePayload;
|
||||
exports.createLeaderboardPayload = createLeaderboardPayload;
|
||||
exports.normalizeAnswerReport = normalizeAnswerReport;
|
||||
function prepareCompletedQuestions(answeredQuestions) {
|
||||
return (answeredQuestions || []).map(a => ({
|
||||
id: a.id || a.questionId,
|
||||
result: (a.isCorrect || a.result === 1) ? 1 : 0,
|
||||
}));
|
||||
}
|
||||
function createGamePayload(options) {
|
||||
const { gameId, userId, gameData, answeredQuestions = [], endTimeIso } = options;
|
||||
const completed_question_ids = prepareCompletedQuestions(answeredQuestions);
|
||||
// Ưu tiên lấy field .questions hoặc .data, hoặc dùng chính gameData nếu nó là mảng
|
||||
let data = [];
|
||||
if (Array.isArray(gameData)) {
|
||||
data = gameData;
|
||||
}
|
||||
else if (gameData && Array.isArray(gameData.questions)) {
|
||||
data = gameData.questions;
|
||||
}
|
||||
else if (gameData && Array.isArray(gameData.data)) {
|
||||
data = gameData.data;
|
||||
}
|
||||
const payload = {
|
||||
game_id: gameId,
|
||||
user_id: userId,
|
||||
data: data,
|
||||
completed_question_ids: completed_question_ids,
|
||||
// Merge các field metadata khác
|
||||
...(typeof gameData === 'object' && !Array.isArray(gameData) ? gameData : {}),
|
||||
// Merge extraData
|
||||
...(options.extraData || {})
|
||||
};
|
||||
// Inject end_time_iso (absolute timestamp for accurate sync)
|
||||
if (endTimeIso) {
|
||||
payload.end_time_iso = endTimeIso;
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
function createLeaderboardPayload(apiData) {
|
||||
const topPlayers = apiData.topPlayers || [];
|
||||
const userRank = apiData.userRank || null;
|
||||
return {
|
||||
top_players: topPlayers.map((p) => ({
|
||||
rank: p.rank,
|
||||
name: p.name || p.studentName || p.user_id,
|
||||
score: p.score ?? p.finalScore ?? 0,
|
||||
student_id: p.studentId || p.userId,
|
||||
time_spent: p.timeSpent ?? p.time_spent ?? 0,
|
||||
completed_at: p.completedAt
|
||||
})),
|
||||
user_rank: userRank ? {
|
||||
rank: userRank.rank,
|
||||
name: userRank.name || userRank.studentName,
|
||||
score: userRank.score ?? userRank.finalScore ?? 0,
|
||||
student_id: userRank.studentId || userRank.userId,
|
||||
time_spent: userRank.timeSpent ?? userRank.time_spent ?? 0,
|
||||
completed_at: userRank.completedAt
|
||||
} : null,
|
||||
};
|
||||
}
|
||||
function normalizeAnswerReport(data) {
|
||||
// Simplified per user request
|
||||
// Input: { question_id: "Q1", result: 1, choice: "2" }
|
||||
return {
|
||||
question_id: data.question_id || data.questionId || data.id,
|
||||
choice: data.choice ?? data.selected_answer ?? data.selectedAnswer,
|
||||
result: data.result ?? (data.is_correct ? 1 : 0),
|
||||
is_correct: !!(data.result === 1 || data.is_correct === true),
|
||||
time_spent: data.time_spent ?? 5,
|
||||
is_timeout: !!data.is_timeout
|
||||
};
|
||||
}
|
||||
//# sourceMappingURL=mappers.js.map
|
||||
1
G102-sequence/sdk/package/dist/kit/mappers.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/kit/mappers.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"mappers.js","sourceRoot":"","sources":["../../src/kit/mappers.ts"],"names":[],"mappings":";AAAA;;;GAGG;;AAoBH,8DAKC;AAED,8CA+BC;AAED,4DAsBC;AAED,sDAWC;AA3ED,SAAgB,yBAAyB,CAAC,iBAAwB;IAC9D,OAAO,CAAC,iBAAiB,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACvC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,UAAU;QACxB,MAAM,EAAE,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;KAClD,CAAC,CAAC,CAAC;AACR,CAAC;AAED,SAAgB,iBAAiB,CAAC,OAA2B;IACzD,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,iBAAiB,GAAG,EAAE,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC;IACjF,MAAM,sBAAsB,GAAG,yBAAyB,CAAC,iBAAiB,CAAC,CAAC;IAE5E,mFAAmF;IACnF,IAAI,IAAI,GAAU,EAAE,CAAC;IACrB,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC1B,IAAI,GAAG,QAAQ,CAAC;IACpB,CAAC;SAAM,IAAI,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;QACvD,IAAI,GAAG,QAAQ,CAAC,SAAS,CAAC;IAC9B,CAAC;SAAM,IAAI,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QAClD,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC;IACzB,CAAC;IAED,MAAM,OAAO,GAAsB;QAC/B,OAAO,EAAE,MAAM;QACf,OAAO,EAAE,MAAM;QACf,IAAI,EAAE,IAAI;QACV,sBAAsB,EAAE,sBAAsB;QAC9C,gCAAgC;QAChC,GAAG,CAAC,OAAO,QAAQ,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7E,kBAAkB;QAClB,GAAG,CAAC,OAAO,CAAC,SAAS,IAAI,EAAE,CAAC;KAC/B,CAAC;IAEF,6DAA6D;IAC7D,IAAI,UAAU,EAAE,CAAC;QACb,OAAO,CAAC,YAAY,GAAG,UAAU,CAAC;IACtC,CAAC;IAED,OAAO,OAAO,CAAC;AACnB,CAAC;AAED,SAAgB,wBAAwB,CAAC,OAAY;IACjD,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,EAAE,CAAC;IAC5C,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,IAAI,CAAC;IAE1C,OAAO;QACH,WAAW,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC;YACrC,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,IAAI,EAAE,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,WAAW,IAAI,CAAC,CAAC,OAAO;YAC1C,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,UAAU,IAAI,CAAC;YACnC,UAAU,EAAE,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,MAAM;YACnC,UAAU,EAAE,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,UAAU,IAAI,CAAC;YAC5C,YAAY,EAAE,CAAC,CAAC,WAAW;SAC9B,CAAC,CAAC;QACH,SAAS,EAAE,QAAQ,CAAC,CAAC,CAAC;YAClB,IAAI,EAAE,QAAQ,CAAC,IAAI;YACnB,IAAI,EAAE,QAAQ,CAAC,IAAI,IAAI,QAAQ,CAAC,WAAW;YAC3C,KAAK,EAAE,QAAQ,CAAC,KAAK,IAAI,QAAQ,CAAC,UAAU,IAAI,CAAC;YACjD,UAAU,EAAE,QAAQ,CAAC,SAAS,IAAI,QAAQ,CAAC,MAAM;YACjD,UAAU,EAAE,QAAQ,CAAC,SAAS,IAAI,QAAQ,CAAC,UAAU,IAAI,CAAC;YAC1D,YAAY,EAAE,QAAQ,CAAC,WAAW;SACrC,CAAC,CAAC,CAAC,IAAI;KACX,CAAC;AACN,CAAC;AAED,SAAgB,qBAAqB,CAAC,IAAS;IAC3C,8BAA8B;IAC9B,uDAAuD;IACvD,OAAO;QACH,WAAW,EAAE,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,EAAE;QAC3D,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,eAAe,IAAI,IAAI,CAAC,cAAc;QAClE,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAChD,UAAU,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,UAAU,KAAK,IAAI,CAAC;QAC7D,UAAU,EAAE,IAAI,CAAC,UAAU,IAAI,CAAC;QAChC,UAAU,EAAE,CAAC,CAAC,IAAI,CAAC,UAAU;KAChC,CAAC;AACN,CAAC"}
|
||||
47
G102-sequence/sdk/package/dist/kit/react/GamePlayer.d.ts
vendored
Normal file
47
G102-sequence/sdk/package/dist/kit/react/GamePlayer.d.ts
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import { GameApiConfig } from '../api';
|
||||
export interface SessionStatus {
|
||||
type: 'new' | 'resumed' | 'timeout' | 'completed' | 'assignment_ended' | 'not_started';
|
||||
message: string;
|
||||
data?: {
|
||||
answered?: number;
|
||||
total?: number;
|
||||
score?: number;
|
||||
remainingTime?: number;
|
||||
startedAt?: string;
|
||||
};
|
||||
}
|
||||
export interface GamePlayerError {
|
||||
type: 'network' | 'api' | 'timeout' | 'session' | 'not_started' | 'unknown';
|
||||
code?: number;
|
||||
message: string;
|
||||
details?: any;
|
||||
canRetry?: boolean;
|
||||
}
|
||||
export declare const SessionErrorCodes: {
|
||||
readonly SESSION_NOT_STARTED: -60;
|
||||
readonly SESSION_ENDED: -61;
|
||||
readonly SESSION_COMPLETED: -62;
|
||||
readonly SESSION_TIMEOUT: -63;
|
||||
readonly SESSION_NOT_FOUND: -64;
|
||||
readonly SESSION_ALREADY_ANSWERED: -65;
|
||||
};
|
||||
export interface GamePlayerProps {
|
||||
apiConfig: GameApiConfig;
|
||||
assignmentId: number;
|
||||
studentId: string;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
debug?: boolean;
|
||||
onComplete?: (result: any) => void;
|
||||
onError?: (error: GamePlayerError) => void;
|
||||
onGameLoaded?: (gameInfo: any) => void;
|
||||
extraGameData?: Record<string, any>;
|
||||
onLog?: (message: string, type?: 'info' | 'success' | 'error' | 'warning') => void;
|
||||
onSessionStatus?: (status: SessionStatus) => void;
|
||||
renderLoading?: () => React.ReactNode;
|
||||
renderError?: (error: GamePlayerError, retry: () => void) => React.ReactNode;
|
||||
loadingTimeout?: number;
|
||||
}
|
||||
export declare const GamePlayer: React.FC<GamePlayerProps>;
|
||||
//# sourceMappingURL=GamePlayer.d.ts.map
|
||||
1
G102-sequence/sdk/package/dist/kit/react/GamePlayer.d.ts.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/kit/react/GamePlayer.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"GamePlayer.d.ts","sourceRoot":"","sources":["../../../src/kit/react/GamePlayer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmD,MAAM,OAAO,CAAC;AAExE,OAAO,EAAiB,aAAa,EAAE,MAAM,QAAQ,CAAC;AAItD,MAAM,WAAW,aAAa;IAC1B,IAAI,EAAE,KAAK,GAAG,SAAS,GAAG,SAAS,GAAG,WAAW,GAAG,kBAAkB,GAAG,aAAa,CAAC;IACvF,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE;QACH,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,SAAS,CAAC,EAAE,MAAM,CAAC;KACtB,CAAC;CACL;AAED,MAAM,WAAW,eAAe;IAC5B,IAAI,EAAE,SAAS,GAAG,KAAK,GAAG,SAAS,GAAG,SAAS,GAAG,aAAa,GAAG,SAAS,CAAC;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,GAAG,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;CACtB;AAGD,eAAO,MAAM,iBAAiB;;;;;;;CAOpB,CAAC;AAEX,MAAM,WAAW,eAAe;IAC5B,SAAS,EAAE,aAAa,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,IAAI,CAAC;IACnC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;IAC3C,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,IAAI,CAAC;IACvC,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACpC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,KAAK,IAAI,CAAC;IACnF,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC;IAElD,aAAa,CAAC,EAAE,MAAM,KAAK,CAAC,SAAS,CAAC;IACtC,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,IAAI,KAAK,KAAK,CAAC,SAAS,CAAC;IAC7E,cAAc,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC,EAAE,CAAC,eAAe,CA8oBhD,CAAC"}
|
||||
583
G102-sequence/sdk/package/dist/kit/react/GamePlayer.js
vendored
Normal file
583
G102-sequence/sdk/package/dist/kit/react/GamePlayer.js
vendored
Normal file
@@ -0,0 +1,583 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.GamePlayer = exports.SessionErrorCodes = void 0;
|
||||
const jsx_runtime_1 = require("react/jsx-runtime");
|
||||
const react_1 = require("react");
|
||||
const useGameIframeSDK_1 = require("../../useGameIframeSDK");
|
||||
const api_1 = require("../api");
|
||||
const mappers_1 = require("../mappers");
|
||||
// Session Error Codes (synced with backend)
|
||||
exports.SessionErrorCodes = {
|
||||
SESSION_NOT_STARTED: -60,
|
||||
SESSION_ENDED: -61,
|
||||
SESSION_COMPLETED: -62,
|
||||
SESSION_TIMEOUT: -63,
|
||||
SESSION_NOT_FOUND: -64,
|
||||
SESSION_ALREADY_ANSWERED: -65,
|
||||
};
|
||||
const GamePlayer = ({ apiConfig, assignmentId, studentId, className, style, debug = false, onComplete, onError, onGameLoaded, extraGameData, onLog, onSessionStatus, renderLoading, renderError, loadingTimeout = 30000 }) => {
|
||||
const iframeRef = (0, react_1.useRef)(null);
|
||||
const [gameUrl, setGameUrl] = (0, react_1.useState)('');
|
||||
const [gameState, setGameState] = (0, react_1.useState)(null);
|
||||
const [loading, setLoading] = (0, react_1.useState)(true);
|
||||
const [error, setError] = (0, react_1.useState)(null);
|
||||
const [hasLoadedGame, setHasLoadedGame] = (0, react_1.useState)(false);
|
||||
const [apiClient] = (0, react_1.useState)(() => new api_1.GameApiClient(apiConfig));
|
||||
// Refs to prevent infinite loops
|
||||
const sendLeaderboardRef = (0, react_1.useRef)(null);
|
||||
const hasInitRef = (0, react_1.useRef)(false); // Track if init has been called
|
||||
const callbacksRef = (0, react_1.useRef)({ onGameLoaded, onLog, onSessionStatus, onError }); // Stable callback refs
|
||||
// Update callback refs on each render
|
||||
callbacksRef.current = { onGameLoaded, onLog, onSessionStatus, onError };
|
||||
// Helper: Create structured error
|
||||
const createError = (0, react_1.useCallback)((err) => {
|
||||
const errorMsg = err?.message || err?.error || String(err);
|
||||
const errorCode = err?.code; // Error code from API response
|
||||
// Check error code first (more reliable than message parsing)
|
||||
if (errorCode !== undefined) {
|
||||
// Session not started
|
||||
if (errorCode === exports.SessionErrorCodes.SESSION_NOT_STARTED) {
|
||||
return {
|
||||
type: 'not_started',
|
||||
code: errorCode,
|
||||
message: errorMsg || 'Bài tập chưa bắt đầu',
|
||||
details: err,
|
||||
canRetry: false
|
||||
};
|
||||
}
|
||||
// Session ended
|
||||
if (errorCode === exports.SessionErrorCodes.SESSION_ENDED) {
|
||||
return {
|
||||
type: 'session',
|
||||
code: errorCode,
|
||||
message: errorMsg || 'Bài tập đã kết thúc',
|
||||
details: err,
|
||||
canRetry: false
|
||||
};
|
||||
}
|
||||
// Session completed
|
||||
if (errorCode === exports.SessionErrorCodes.SESSION_COMPLETED) {
|
||||
return {
|
||||
type: 'session',
|
||||
code: errorCode,
|
||||
message: errorMsg || 'Bạn đã hoàn thành bài tập này rồi',
|
||||
details: err,
|
||||
canRetry: false
|
||||
};
|
||||
}
|
||||
// Session timeout
|
||||
if (errorCode === exports.SessionErrorCodes.SESSION_TIMEOUT) {
|
||||
return {
|
||||
type: 'timeout',
|
||||
code: errorCode,
|
||||
message: errorMsg || 'Đã hết thời gian làm bài',
|
||||
details: err,
|
||||
canRetry: false
|
||||
};
|
||||
}
|
||||
}
|
||||
// Fallback: Parse error message
|
||||
// API errors
|
||||
if (errorMsg.includes('API Error') || errorMsg.includes('Failed to fetch')) {
|
||||
// Check if it contains session-related message
|
||||
if (errorMsg.includes('chưa bắt đầu')) {
|
||||
return {
|
||||
type: 'not_started',
|
||||
message: 'Bài tập chưa bắt đầu',
|
||||
details: err,
|
||||
canRetry: false
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: 'api',
|
||||
message: errorMsg,
|
||||
details: err,
|
||||
canRetry: true
|
||||
};
|
||||
}
|
||||
// Network errors
|
||||
if (errorMsg.includes('network') || errorMsg.includes('ECONNREFUSED')) {
|
||||
return {
|
||||
type: 'network',
|
||||
message: 'Lỗi kết nối mạng. Vui lòng kiểm tra internet.',
|
||||
details: err,
|
||||
canRetry: true
|
||||
};
|
||||
}
|
||||
// Session errors (message-based fallback)
|
||||
if (errorMsg.includes('chưa bắt đầu')) {
|
||||
return {
|
||||
type: 'not_started',
|
||||
message: errorMsg || 'Bài tập chưa bắt đầu',
|
||||
details: err,
|
||||
canRetry: false
|
||||
};
|
||||
}
|
||||
if (errorMsg.includes('hết thời gian') || errorMsg.includes('đã kết thúc') ||
|
||||
errorMsg.includes('đã hoàn thành') || errorMsg.includes('session')) {
|
||||
return {
|
||||
type: 'session',
|
||||
message: errorMsg,
|
||||
details: err,
|
||||
canRetry: false
|
||||
};
|
||||
}
|
||||
// Timeout errors
|
||||
if (errorMsg.includes('timeout') || errorMsg.includes('timed out')) {
|
||||
return {
|
||||
type: 'timeout',
|
||||
message: 'Kết nối quá lâu. Vui lòng thử lại.',
|
||||
details: err,
|
||||
canRetry: true
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: 'unknown',
|
||||
message: errorMsg || 'Đã xảy ra lỗi không xác định',
|
||||
details: err,
|
||||
canRetry: true
|
||||
};
|
||||
}, []);
|
||||
// Helper: Handle error with callback
|
||||
const handleError = (0, react_1.useCallback)((err) => {
|
||||
const gameError = createError(err);
|
||||
setError(gameError);
|
||||
setLoading(false);
|
||||
if (onError) {
|
||||
onError(gameError);
|
||||
}
|
||||
if (onLog) {
|
||||
onLog(`[SDK ERR] ${gameError.type}: ${gameError.message}`, 'error');
|
||||
}
|
||||
}, [createError, onError, onLog]);
|
||||
// Retry function
|
||||
const retryInit = (0, react_1.useCallback)(() => {
|
||||
if (callbacksRef.current.onLog)
|
||||
callbacksRef.current.onLog('[SDK] Retrying initialization...', 'info');
|
||||
hasInitRef.current = false; // Allow re-init
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
setGameUrl('');
|
||||
setGameState(null);
|
||||
}, []);
|
||||
// Define Handlers
|
||||
const handleAnswer = (0, react_1.useCallback)(async (data) => {
|
||||
try {
|
||||
if (onLog)
|
||||
onLog(`[SDK RECV] Answer Raw: ${JSON.stringify(data)}`, 'info');
|
||||
const report = (0, mappers_1.normalizeAnswerReport)(data);
|
||||
if (onLog)
|
||||
onLog(`[SDK] Processed Answer: ID ${report.question_id} | Choice: ${report.choice} | ${report.is_correct ? 'Correct' : 'Wrong'}`, 'info');
|
||||
if (onLog)
|
||||
onLog(`[API REQ] Submitting Answer ID ${report.question_id}...`, 'info');
|
||||
const res = await apiClient.submitAnswer(assignmentId, studentId, report.question_id, report.choice, report.time_spent, report.is_timeout);
|
||||
if (onLog)
|
||||
onLog(`[API RES] Submit Result: ${JSON.stringify(res)}`, 'success');
|
||||
}
|
||||
catch (err) {
|
||||
console.error('[GamePlayer] Submit error:', err);
|
||||
if (onLog)
|
||||
onLog(`[API ERR] Submit Failed: ${err.message}`, 'error');
|
||||
}
|
||||
}, [apiClient, assignmentId, studentId, onLog]);
|
||||
const handleFinalResult = (0, react_1.useCallback)(async (data) => {
|
||||
try {
|
||||
if (onLog)
|
||||
onLog(`[SDK RECV] Final Result Raw: ${JSON.stringify(data)}`, 'info');
|
||||
if (onLog)
|
||||
onLog('[SDK] Game Complete. Calling API...', 'info');
|
||||
const res = await apiClient.completeSession(assignmentId, studentId);
|
||||
if (onLog)
|
||||
onLog(`[API RES] Session Completed. Score: ${res.data?.finalScore}`, 'success');
|
||||
// Auto-refresh leaderboard after completion to ensure user rank is included
|
||||
if (onLog)
|
||||
onLog('[SDK] Auto-refreshing leaderboard after completion...', 'info');
|
||||
await new Promise(resolve => setTimeout(resolve, 500)); // Small delay for backend to process
|
||||
try {
|
||||
const lbRes = await apiClient.getLeaderboard(assignmentId, studentId);
|
||||
if (onLog)
|
||||
onLog(`[API RES] Post-completion Leaderboard: ${JSON.stringify(lbRes)}`, 'success');
|
||||
if (lbRes.success && lbRes.data && sendLeaderboardRef.current) {
|
||||
const leaderboardData = {
|
||||
top_players: (lbRes.data.topPlayers || []).map((p) => ({
|
||||
rank: p.rank,
|
||||
name: p.name || p.studentName || p.userId,
|
||||
score: p.score ?? p.finalScore ?? 0,
|
||||
student_id: p.studentId || p.userId,
|
||||
time_spent: p.timeSpent ?? p.time_spent ?? 0,
|
||||
completed_at: p.completedAt
|
||||
})),
|
||||
user_rank: lbRes.data.userRank ? {
|
||||
rank: lbRes.data.userRank.rank,
|
||||
name: lbRes.data.userRank.name || lbRes.data.userRank.studentName,
|
||||
score: lbRes.data.userRank.score ?? lbRes.data.userRank.finalScore ?? 0,
|
||||
student_id: lbRes.data.userRank.studentId || lbRes.data.userRank.userId,
|
||||
time_spent: lbRes.data.userRank.timeSpent ?? lbRes.data.userRank.time_spent ?? 0,
|
||||
completed_at: lbRes.data.userRank.completedAt
|
||||
} : null
|
||||
};
|
||||
if (onLog)
|
||||
onLog(`[SDK SEND] Updated Leaderboard: ${JSON.stringify(leaderboardData)}`, 'info');
|
||||
sendLeaderboardRef.current(leaderboardData);
|
||||
}
|
||||
}
|
||||
catch (lbErr) {
|
||||
if (onLog)
|
||||
onLog(`[API ERR] Leaderboard refresh failed: ${lbErr.message}`, 'warning');
|
||||
}
|
||||
if (onComplete)
|
||||
onComplete(res);
|
||||
}
|
||||
catch (err) {
|
||||
console.error('[GamePlayer] Complete error:', err);
|
||||
if (onLog)
|
||||
onLog(`[API ERR] Complete Failed: ${err.message}`, 'error');
|
||||
if (onError)
|
||||
onError(err);
|
||||
}
|
||||
}, [apiClient, assignmentId, studentId, onComplete, onError, onLog]);
|
||||
// SDK Hook
|
||||
const { isReady, sendGameData, sendLeaderboard } = (0, useGameIframeSDK_1.useGameIframeSDK)({
|
||||
iframeRef,
|
||||
iframeOrigin: '*',
|
||||
debug,
|
||||
onGameReady: () => {
|
||||
if (onLog)
|
||||
onLog('[SDK RECV] Iframe Ready', 'success');
|
||||
},
|
||||
onAnswerReport: handleAnswer,
|
||||
onFinalResult: handleFinalResult,
|
||||
onLeaderboardRequest: async (top) => {
|
||||
try {
|
||||
if (onLog)
|
||||
onLog(`[SDK RECV] Leaderboard Request Raw: top=${top}`, 'info');
|
||||
if (onLog)
|
||||
onLog(`[API REQ] Fetching Leaderboard (top ${top})...`, 'info');
|
||||
const res = await apiClient.getLeaderboard(assignmentId, studentId);
|
||||
if (onLog)
|
||||
onLog(`[API RES] Leaderboard Raw: ${JSON.stringify(res)}`, 'success');
|
||||
if (res.success && res.data) {
|
||||
const leaderboardData = {
|
||||
top_players: (res.data.topPlayers || []).map((p) => ({
|
||||
rank: p.rank,
|
||||
name: p.name || p.studentName || p.userId,
|
||||
score: p.score ?? p.finalScore ?? 0,
|
||||
student_id: p.studentId || p.userId,
|
||||
time_spent: p.timeSpent ?? p.time_spent ?? 0,
|
||||
completed_at: p.completedAt
|
||||
})),
|
||||
user_rank: res.data.userRank ? {
|
||||
rank: res.data.userRank.rank,
|
||||
name: res.data.userRank.name || res.data.userRank.studentName,
|
||||
score: res.data.userRank.score ?? res.data.userRank.finalScore ?? 0,
|
||||
student_id: res.data.userRank.studentId || res.data.userRank.userId,
|
||||
time_spent: res.data.userRank.timeSpent ?? res.data.userRank.time_spent ?? 0,
|
||||
completed_at: res.data.userRank.completedAt
|
||||
} : null
|
||||
};
|
||||
if (onLog)
|
||||
onLog(`[SDK SEND] Leaderboard Payload: ${JSON.stringify(leaderboardData)}`, 'info');
|
||||
const sent = sendLeaderboard(leaderboardData);
|
||||
if (onLog)
|
||||
onLog(sent ? '[SDK] Leaderboard sent successfully' : '[SDK ERR] Failed to send leaderboard', sent ? 'success' : 'error');
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error('[GamePlayer] Leaderboard error:', err);
|
||||
if (onLog)
|
||||
onLog(`[API ERR] Leaderboard Failed: ${err.message}`, 'error');
|
||||
}
|
||||
}
|
||||
});
|
||||
// Update ref after hook
|
||||
sendLeaderboardRef.current = sendLeaderboard;
|
||||
// 1. Fetch Game Data & Start Session
|
||||
(0, react_1.useEffect)(() => {
|
||||
let mounted = true;
|
||||
let loadingTimeoutId;
|
||||
const initGame = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
if (onLog)
|
||||
onLog('[SDK] Initializing Game...', 'info');
|
||||
// Set loading timeout
|
||||
loadingTimeoutId = setTimeout(() => {
|
||||
if (mounted && loading) {
|
||||
if (onLog)
|
||||
onLog('[SDK WARN] Loading taking longer than expected...', 'warning');
|
||||
}
|
||||
}, loadingTimeout);
|
||||
// getGameWithProgress đã tự động tạo/restore session trong backend
|
||||
// thông qua getOrCreateSubmissionCache - không cần gọi startLiveSession riêng
|
||||
const res = await apiClient.getGameWithProgress(assignmentId, studentId);
|
||||
if (!res.success || !res.data) {
|
||||
throw new Error(res.message || 'Failed to load game');
|
||||
}
|
||||
if (mounted) {
|
||||
// Check session status TRƯỚC để quyết định có load game hay không
|
||||
const session = res.data.session;
|
||||
const progress = res.data.progress;
|
||||
// Case 1: Already completed
|
||||
if (res.data.isFinished || session?.completed) {
|
||||
// CHỈ set error nếu CHƯA load game (lần đầu vào)
|
||||
if (!hasLoadedGame) {
|
||||
const errorObj = {
|
||||
type: 'session',
|
||||
message: 'Bạn đã hoàn thành bài tập này rồi',
|
||||
details: { score: progress?.score, answered: progress?.answered, total: progress?.total },
|
||||
canRetry: false
|
||||
};
|
||||
setError(errorObj);
|
||||
setLoading(false);
|
||||
clearTimeout(loadingTimeoutId);
|
||||
if (onError)
|
||||
onError(errorObj);
|
||||
if (onLog)
|
||||
onLog(`[SDK] ${errorObj.message}`, 'warning');
|
||||
}
|
||||
// Luôn gọi callback (cho cả 2 trường hợp)
|
||||
if (onSessionStatus) {
|
||||
onSessionStatus({
|
||||
type: 'completed',
|
||||
message: 'Bạn đã hoàn thành bài tập này',
|
||||
data: {
|
||||
answered: progress?.answered || 0,
|
||||
total: progress?.total || 0,
|
||||
score: progress?.score || 0
|
||||
}
|
||||
});
|
||||
}
|
||||
// Nếu CHƯA load game → STOP
|
||||
// Nếu ĐÃ load game → tiếp tục (cho game hiển thị leaderboard)
|
||||
if (!hasLoadedGame) {
|
||||
return; // ❌ STOP - không load game
|
||||
}
|
||||
}
|
||||
// Case 2: Session timeout
|
||||
if (session && session.remainingTime !== null && session.remainingTime <= 0) {
|
||||
// CHỈ set error nếu CHƯA load game
|
||||
if (!hasLoadedGame) {
|
||||
const errorObj = {
|
||||
type: 'timeout',
|
||||
message: 'Thời gian làm bài đã hết',
|
||||
details: { answered: progress?.answered, total: progress?.total },
|
||||
canRetry: false
|
||||
};
|
||||
setError(errorObj);
|
||||
setLoading(false);
|
||||
clearTimeout(loadingTimeoutId);
|
||||
if (onError)
|
||||
onError(errorObj);
|
||||
if (onLog)
|
||||
onLog(`[SDK] ${errorObj.message}`, 'warning');
|
||||
}
|
||||
// Luôn gọi callback
|
||||
if (onSessionStatus) {
|
||||
onSessionStatus({
|
||||
type: 'timeout',
|
||||
message: 'Thời gian làm bài đã hết',
|
||||
data: {
|
||||
answered: progress?.answered || 0,
|
||||
total: progress?.total || 0,
|
||||
score: progress?.score || 0,
|
||||
remainingTime: 0
|
||||
}
|
||||
});
|
||||
}
|
||||
// Nếu CHƯA load game → STOP
|
||||
if (!hasLoadedGame) {
|
||||
return; // ❌ STOP - không load game
|
||||
}
|
||||
}
|
||||
// Case 3: Valid session - LOAD game
|
||||
setGameState(res.data);
|
||||
setGameUrl(res.data.gameUrl);
|
||||
setLoading(false);
|
||||
setHasLoadedGame(true); // ✅ Đánh dấu đã load game
|
||||
clearTimeout(loadingTimeoutId);
|
||||
if (onGameLoaded)
|
||||
onGameLoaded(res.data);
|
||||
if (onLog)
|
||||
onLog(`[API RES] Game Loaded: ${res.data.title || res.data.gameType || assignmentId}`, 'success');
|
||||
// Notify session status for valid sessions
|
||||
if (session && onSessionStatus) {
|
||||
// Resumed session (có câu đã trả lời)
|
||||
if (progress && progress.answered > 0) {
|
||||
onSessionStatus({
|
||||
type: 'resumed',
|
||||
message: `Tiếp tục làm bài (Đã làm ${progress.answered}/${progress.total} câu)`,
|
||||
data: {
|
||||
answered: progress.answered,
|
||||
total: progress.total,
|
||||
score: progress.score,
|
||||
remainingTime: session.remainingTime,
|
||||
startedAt: session.startedAt
|
||||
}
|
||||
});
|
||||
}
|
||||
// New session
|
||||
else {
|
||||
onSessionStatus({
|
||||
type: 'new',
|
||||
message: 'Bắt đầu làm bài mới',
|
||||
data: {
|
||||
answered: 0,
|
||||
total: progress?.total || 0,
|
||||
score: 0,
|
||||
remainingTime: session.remainingTime,
|
||||
startedAt: session.startedAt
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// Log session info
|
||||
if (session && onLog) {
|
||||
const sessionInfo = `Session started at ${session.startedAt}, remaining: ${session.remainingTime}s`;
|
||||
onLog(`[SDK] ${sessionInfo}`, 'info');
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error('[GamePlayer] Init error:', err);
|
||||
if (mounted) {
|
||||
handleError(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
if (assignmentId && studentId && !hasInitRef.current) {
|
||||
hasInitRef.current = true; // Prevent re-init
|
||||
initGame();
|
||||
}
|
||||
return () => {
|
||||
mounted = false;
|
||||
hasInitRef.current = false; // ✅ Reset for StrictMode remount
|
||||
if (loadingTimeoutId)
|
||||
clearTimeout(loadingTimeoutId);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [assignmentId, studentId]); // ✅ Chỉ re-init khi assignmentId hoặc studentId thay đổi
|
||||
// 2. Auto Send Data when Ready
|
||||
(0, react_1.useEffect)(() => {
|
||||
if (isReady && gameState) {
|
||||
// Get expiresAt from session (absolute timestamp for accurate sync)
|
||||
const getEndTimeIso = () => {
|
||||
const session = gameState.session;
|
||||
if (!session)
|
||||
return undefined;
|
||||
// Ưu tiên dùng expiresAt
|
||||
if (session.expiresAt) {
|
||||
return session.expiresAt;
|
||||
}
|
||||
// Fallback: tính từ startedAt + duration
|
||||
if (session.startedAt && session.duration) {
|
||||
const startTimeMs = new Date(session.startedAt).getTime();
|
||||
const expiresAtMs = startTimeMs + (session.duration * 1000);
|
||||
return new Date(expiresAtMs).toISOString();
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
const payload = (0, mappers_1.createGamePayload)({
|
||||
gameId: gameState.id,
|
||||
userId: studentId,
|
||||
gameData: gameState.jsonData,
|
||||
answeredQuestions: gameState.completed_question_ids,
|
||||
extraData: extraGameData,
|
||||
endTimeIso: getEndTimeIso() // ✅ Absolute timestamp
|
||||
});
|
||||
if (onLog)
|
||||
onLog(`[SDK SEND] Game Payload: ${JSON.stringify(payload)}`, 'info');
|
||||
sendGameData(payload);
|
||||
}
|
||||
}, [isReady, gameState, studentId, sendGameData, extraGameData, onLog]);
|
||||
// ===== RENDER =====
|
||||
// 1. Error State
|
||||
if (error) {
|
||||
if (renderError) {
|
||||
return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: renderError(error, retryInit) });
|
||||
}
|
||||
// Default error UI
|
||||
return ((0, jsx_runtime_1.jsxs)("div", { className: "game-player-error", style: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
padding: '2rem',
|
||||
textAlign: 'center',
|
||||
backgroundColor: '#f8f9fa',
|
||||
...style
|
||||
}, children: [(0, jsx_runtime_1.jsx)("div", { style: {
|
||||
fontSize: '3rem',
|
||||
marginBottom: '1rem'
|
||||
}, children: error.type === 'network' ? '🌐' :
|
||||
error.type === 'timeout' ? '⏱️' :
|
||||
error.type === 'session' ? '🔒' :
|
||||
error.type === 'not_started' ? '📅' : '⚠️' }), (0, jsx_runtime_1.jsx)("h2", { style: {
|
||||
fontSize: '1.5rem',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '0.5rem',
|
||||
color: error.type === 'not_started' ? '#fd7e14' : '#dc3545'
|
||||
}, children: error.type === 'network' ? 'Lỗi Kết Nối' :
|
||||
error.type === 'timeout' ? 'Hết Giờ Làm Bài' :
|
||||
error.type === 'session' ? 'Phiên Làm Bài Đã Kết Thúc' :
|
||||
error.type === 'not_started' ? 'Bài Tập Chưa Bắt Đầu' :
|
||||
'Đã Xảy Ra Lỗi' }), (0, jsx_runtime_1.jsx)("p", { style: {
|
||||
fontSize: '1rem',
|
||||
color: '#6c757d',
|
||||
marginBottom: '1.5rem',
|
||||
maxWidth: '500px'
|
||||
}, children: error.message }), error.canRetry && ((0, jsx_runtime_1.jsx)("button", { onClick: retryInit, style: {
|
||||
padding: '0.75rem 2rem',
|
||||
fontSize: '1rem',
|
||||
fontWeight: 'bold',
|
||||
color: '#fff',
|
||||
backgroundColor: '#007bff',
|
||||
border: 'none',
|
||||
borderRadius: '0.5rem',
|
||||
cursor: 'pointer',
|
||||
transition: 'background-color 0.2s'
|
||||
}, onMouseEnter: (e) => e.currentTarget.style.backgroundColor = '#0056b3', onMouseLeave: (e) => e.currentTarget.style.backgroundColor = '#007bff', children: "\uD83D\uDD04 Th\u1EED L\u1EA1i" })), !error.canRetry && ((0, jsx_runtime_1.jsx)("p", { style: {
|
||||
fontSize: '0.875rem',
|
||||
color: '#6c757d',
|
||||
marginTop: '1rem'
|
||||
}, children: "Vui l\u00F2ng li\u00EAn h\u1EC7 gi\u00E1o vi\u00EAn ho\u1EB7c quay l\u1EA1i trang ch\u1EE7" }))] }));
|
||||
}
|
||||
// 2. Loading State
|
||||
if (loading || !gameUrl) {
|
||||
if (renderLoading) {
|
||||
return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: renderLoading() });
|
||||
}
|
||||
// Default loading UI
|
||||
return ((0, jsx_runtime_1.jsxs)("div", { className: "game-player-loading", style: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
backgroundColor: '#f8f9fa',
|
||||
...style
|
||||
}, children: [(0, jsx_runtime_1.jsx)("div", { style: {
|
||||
width: '50px',
|
||||
height: '50px',
|
||||
border: '5px solid #e9ecef',
|
||||
borderTop: '5px solid #007bff',
|
||||
borderRadius: '50%',
|
||||
animation: 'spin 1s linear infinite',
|
||||
marginBottom: '1rem'
|
||||
} }), (0, jsx_runtime_1.jsx)("p", { style: {
|
||||
fontSize: '1.125rem',
|
||||
color: '#6c757d',
|
||||
fontWeight: '500'
|
||||
}, children: "\u0110ang t\u1EA3i tr\u00F2 ch\u01A1i..." }), (0, jsx_runtime_1.jsx)("style", { children: `
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
` })] }));
|
||||
}
|
||||
// 3. Game Iframe
|
||||
return ((0, jsx_runtime_1.jsx)("iframe", { ref: iframeRef, src: gameUrl, className: className, style: { width: '100%', height: '100%', border: 'none', ...style }, allowFullScreen: true }));
|
||||
};
|
||||
exports.GamePlayer = GamePlayer;
|
||||
//# sourceMappingURL=GamePlayer.js.map
|
||||
1
G102-sequence/sdk/package/dist/kit/react/GamePlayer.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/kit/react/GamePlayer.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
96
G102-sequence/sdk/package/dist/kit/react/GameTestPlayer.d.ts
vendored
Normal file
96
G102-sequence/sdk/package/dist/kit/react/GameTestPlayer.d.ts
vendored
Normal file
@@ -0,0 +1,96 @@
|
||||
import React from 'react';
|
||||
export interface GameTestPlayerProps {
|
||||
/**
|
||||
* URL của game iframe
|
||||
*/
|
||||
gameUrl: string;
|
||||
/**
|
||||
* Data game (array of questions)
|
||||
*/
|
||||
gameData: any[];
|
||||
/**
|
||||
* User ID (optional, default 'test_user')
|
||||
*/
|
||||
userId?: string;
|
||||
/**
|
||||
* Game ID (optional, default 'test_game')
|
||||
*/
|
||||
gameId?: string | number;
|
||||
/**
|
||||
* Extra data to pass to iframe
|
||||
*/
|
||||
extraData?: Record<string, any>;
|
||||
/**
|
||||
* End time ISO (optional - for countdown)
|
||||
*/
|
||||
endTimeIso?: string;
|
||||
/**
|
||||
* CSS class
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* CSS style
|
||||
*/
|
||||
style?: React.CSSProperties;
|
||||
/**
|
||||
* Debug mode
|
||||
*/
|
||||
debug?: boolean;
|
||||
/**
|
||||
* Callback khi nhận answer từ iframe
|
||||
*/
|
||||
onAnswer?: (data: any) => void;
|
||||
/**
|
||||
* Callback khi game hoàn thành
|
||||
*/
|
||||
onComplete?: (result: any) => void;
|
||||
/**
|
||||
* Callback log
|
||||
*/
|
||||
onLog?: (message: string, type?: 'info' | 'success' | 'error' | 'warning') => void;
|
||||
/**
|
||||
* Callback khi iframe yêu cầu leaderboard
|
||||
*/
|
||||
onLeaderboardRequest?: (top: number) => void;
|
||||
/**
|
||||
* Mock leaderboard data (optional - sẽ tự gửi khi iframe request)
|
||||
*/
|
||||
mockLeaderboard?: {
|
||||
top_players: Array<{
|
||||
rank: number;
|
||||
name: string;
|
||||
score: number;
|
||||
time_spent?: number;
|
||||
}>;
|
||||
user_rank?: {
|
||||
rank: number;
|
||||
name: string;
|
||||
score: number;
|
||||
time_spent?: number;
|
||||
} | null;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* GameTestPlayer - Component test đơn giản
|
||||
*
|
||||
* Chỉ load game data vào iframe, KHÔNG gọi API
|
||||
* Dùng để test game iframe locally
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <GameTestPlayer
|
||||
* gameUrl="http://localhost:3000/game"
|
||||
* gameData={[
|
||||
* { id: 1, question: "What is 2+2?", options: ["3","4","5"], answer: "4" },
|
||||
* { id: 2, question: "Capital of France?", options: ["London","Paris","Berlin"], answer: "Paris" }
|
||||
* ]}
|
||||
* debug={true}
|
||||
* onLog={(msg, type) => console.log(`[${type}] ${msg}`)}
|
||||
* onAnswer={(data) => console.log('Answer:', data)}
|
||||
* onComplete={(result) => console.log('Complete:', result)}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
export declare const GameTestPlayer: React.FC<GameTestPlayerProps>;
|
||||
export default GameTestPlayer;
|
||||
//# sourceMappingURL=GameTestPlayer.d.ts.map
|
||||
1
G102-sequence/sdk/package/dist/kit/react/GameTestPlayer.d.ts.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/kit/react/GameTestPlayer.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"GameTestPlayer.d.ts","sourceRoot":"","sources":["../../../src/kit/react/GameTestPlayer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAsC,MAAM,OAAO,CAAC;AAI3D,MAAM,WAAW,mBAAmB;IAChC;;OAEG;IACH,OAAO,EAAE,MAAM,CAAC;IAEhB;;OAEG;IACH,QAAQ,EAAE,GAAG,EAAE,CAAC;IAEhB;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAEzB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAEhC;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAE5B;;OAEG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB;;OAEG;IACH,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,IAAI,CAAC;IAE/B;;OAEG;IACH,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,IAAI,CAAC;IAEnC;;OAEG;IACH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,KAAK,IAAI,CAAC;IAEnF;;OAEG;IACH,oBAAoB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAE7C;;OAEG;IACH,eAAe,CAAC,EAAE;QACd,WAAW,EAAE,KAAK,CAAC;YACf,IAAI,EAAE,MAAM,CAAC;YACb,IAAI,EAAE,MAAM,CAAC;YACb,KAAK,EAAE,MAAM,CAAC;YACd,UAAU,CAAC,EAAE,MAAM,CAAC;SACvB,CAAC,CAAC;QACH,SAAS,CAAC,EAAE;YACR,IAAI,EAAE,MAAM,CAAC;YACb,IAAI,EAAE,MAAM,CAAC;YACb,KAAK,EAAE,MAAM,CAAC;YACd,UAAU,CAAC,EAAE,MAAM,CAAC;SACvB,GAAG,IAAI,CAAC;KACZ,CAAC;CACL;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,cAAc,EAAE,KAAK,CAAC,EAAE,CAAC,mBAAmB,CA2HxD,CAAC;AAEF,eAAe,cAAc,CAAC"}
|
||||
116
G102-sequence/sdk/package/dist/kit/react/GameTestPlayer.js
vendored
Normal file
116
G102-sequence/sdk/package/dist/kit/react/GameTestPlayer.js
vendored
Normal file
@@ -0,0 +1,116 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.GameTestPlayer = void 0;
|
||||
const jsx_runtime_1 = require("react/jsx-runtime");
|
||||
const react_1 = require("react");
|
||||
const useGameIframeSDK_1 = require("../../useGameIframeSDK");
|
||||
/**
|
||||
* GameTestPlayer - Component test đơn giản
|
||||
*
|
||||
* Chỉ load game data vào iframe, KHÔNG gọi API
|
||||
* Dùng để test game iframe locally
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <GameTestPlayer
|
||||
* gameUrl="http://localhost:3000/game"
|
||||
* gameData={[
|
||||
* { id: 1, question: "What is 2+2?", options: ["3","4","5"], answer: "4" },
|
||||
* { id: 2, question: "Capital of France?", options: ["London","Paris","Berlin"], answer: "Paris" }
|
||||
* ]}
|
||||
* debug={true}
|
||||
* onLog={(msg, type) => console.log(`[${type}] ${msg}`)}
|
||||
* onAnswer={(data) => console.log('Answer:', data)}
|
||||
* onComplete={(result) => console.log('Complete:', result)}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
const GameTestPlayer = ({ gameUrl, gameData, userId = 'test_user', gameId = 'test_game', extraData, endTimeIso, className, style, debug = false, onAnswer, onComplete, onLog, onLeaderboardRequest, mockLeaderboard }) => {
|
||||
const iframeRef = (0, react_1.useRef)(null);
|
||||
const [isLoading, setIsLoading] = (0, react_1.useState)(true);
|
||||
// SDK Hook
|
||||
const { isReady, sendGameData, sendLeaderboard } = (0, useGameIframeSDK_1.useGameIframeSDK)({
|
||||
iframeRef,
|
||||
iframeOrigin: '*',
|
||||
debug,
|
||||
onGameReady: () => {
|
||||
if (onLog)
|
||||
onLog('[TEST] Iframe Ready', 'success');
|
||||
setIsLoading(false);
|
||||
},
|
||||
onAnswerReport: (data) => {
|
||||
if (onLog)
|
||||
onLog(`[TEST] Answer: ${JSON.stringify(data)}`, 'info');
|
||||
if (onAnswer)
|
||||
onAnswer(data);
|
||||
},
|
||||
onFinalResult: (result) => {
|
||||
if (onLog)
|
||||
onLog(`[TEST] Complete: ${JSON.stringify(result)}`, 'success');
|
||||
if (onComplete)
|
||||
onComplete(result);
|
||||
},
|
||||
onLeaderboardRequest: (top) => {
|
||||
if (onLog)
|
||||
onLog(`[TEST] Leaderboard Request: top=${top}`, 'info');
|
||||
if (onLeaderboardRequest)
|
||||
onLeaderboardRequest(top);
|
||||
// Auto send mock leaderboard if provided
|
||||
if (mockLeaderboard) {
|
||||
if (onLog)
|
||||
onLog(`[TEST] Sending mock leaderboard`, 'info');
|
||||
sendLeaderboard(mockLeaderboard);
|
||||
}
|
||||
}
|
||||
});
|
||||
// Auto send game data when ready
|
||||
(0, react_1.useEffect)(() => {
|
||||
if (isReady && gameData) {
|
||||
const payload = {
|
||||
game_id: String(gameId),
|
||||
user_id: userId,
|
||||
data: gameData,
|
||||
completed_question_ids: [],
|
||||
...(extraData || {})
|
||||
};
|
||||
if (endTimeIso) {
|
||||
payload.end_time_iso = endTimeIso;
|
||||
}
|
||||
if (onLog)
|
||||
onLog(`[TEST] Sending Game Data: ${gameData.length} items`, 'info');
|
||||
sendGameData(payload);
|
||||
}
|
||||
}, [isReady, gameData, gameId, userId, extraData, endTimeIso, sendGameData, onLog]);
|
||||
return ((0, jsx_runtime_1.jsxs)("div", { style: { position: 'relative', width: '100%', height: '100%', ...style }, children: [isLoading && ((0, jsx_runtime_1.jsx)("div", { style: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'rgba(255,255,255,0.9)',
|
||||
zIndex: 10
|
||||
}, children: (0, jsx_runtime_1.jsxs)("div", { style: { textAlign: 'center' }, children: [(0, jsx_runtime_1.jsx)("div", { style: {
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
border: '4px solid #e9ecef',
|
||||
borderTop: '4px solid #007bff',
|
||||
borderRadius: '50%',
|
||||
animation: 'spin 1s linear infinite',
|
||||
margin: '0 auto 1rem'
|
||||
} }), (0, jsx_runtime_1.jsx)("p", { style: { color: '#6c757d' }, children: "Loading game..." }), (0, jsx_runtime_1.jsx)("style", { children: `
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
` })] }) })), (0, jsx_runtime_1.jsx)("iframe", { ref: iframeRef, src: gameUrl, className: className, style: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
border: 'none'
|
||||
}, allowFullScreen: true })] }));
|
||||
};
|
||||
exports.GameTestPlayer = GameTestPlayer;
|
||||
exports.default = exports.GameTestPlayer;
|
||||
//# sourceMappingURL=GameTestPlayer.js.map
|
||||
1
G102-sequence/sdk/package/dist/kit/react/GameTestPlayer.js.map
vendored
Normal file
1
G102-sequence/sdk/package/dist/kit/react/GameTestPlayer.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"GameTestPlayer.js","sourceRoot":"","sources":["../../../src/kit/react/GameTestPlayer.tsx"],"names":[],"mappings":";;;;AAAA,iCAA2D;AAC3D,6DAA0D;AAwF1D;;;;;;;;;;;;;;;;;;;;GAoBG;AACI,MAAM,cAAc,GAAkC,CAAC,EAC1D,OAAO,EACP,QAAQ,EACR,MAAM,GAAG,WAAW,EACpB,MAAM,GAAG,WAAW,EACpB,SAAS,EACT,UAAU,EACV,SAAS,EACT,KAAK,EACL,KAAK,GAAG,KAAK,EACb,QAAQ,EACR,UAAU,EACV,KAAK,EACL,oBAAoB,EACpB,eAAe,EAClB,EAAE,EAAE;IACD,MAAM,SAAS,GAAG,IAAA,cAAM,EAAoB,IAAI,CAAC,CAAC;IAClD,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,IAAA,gBAAQ,EAAC,IAAI,CAAC,CAAC;IAEjD,WAAW;IACX,MAAM,EACF,OAAO,EACP,YAAY,EACZ,eAAe,EAClB,GAAG,IAAA,mCAAgB,EAAC;QACjB,SAAS;QACT,YAAY,EAAE,GAAG;QACjB,KAAK;QACL,WAAW,EAAE,GAAG,EAAE;YACd,IAAI,KAAK;gBAAE,KAAK,CAAC,qBAAqB,EAAE,SAAS,CAAC,CAAC;YACnD,YAAY,CAAC,KAAK,CAAC,CAAC;QACxB,CAAC;QACD,cAAc,EAAE,CAAC,IAAI,EAAE,EAAE;YACrB,IAAI,KAAK;gBAAE,KAAK,CAAC,kBAAkB,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;YACnE,IAAI,QAAQ;gBAAE,QAAQ,CAAC,IAAI,CAAC,CAAC;QACjC,CAAC;QACD,aAAa,EAAE,CAAC,MAAM,EAAE,EAAE;YACtB,IAAI,KAAK;gBAAE,KAAK,CAAC,oBAAoB,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC;YAC1E,IAAI,UAAU;gBAAE,UAAU,CAAC,MAAM,CAAC,CAAC;QACvC,CAAC;QACD,oBAAoB,EAAE,CAAC,GAAG,EAAE,EAAE;YAC1B,IAAI,KAAK;gBAAE,KAAK,CAAC,mCAAmC,GAAG,EAAE,EAAE,MAAM,CAAC,CAAC;YACnE,IAAI,oBAAoB;gBAAE,oBAAoB,CAAC,GAAG,CAAC,CAAC;YAEpD,yCAAyC;YACzC,IAAI,eAAe,EAAE,CAAC;gBAClB,IAAI,KAAK;oBAAE,KAAK,CAAC,iCAAiC,EAAE,MAAM,CAAC,CAAC;gBAC5D,eAAe,CAAC,eAAe,CAAC,CAAC;YACrC,CAAC;QACL,CAAC;KACJ,CAAC,CAAC;IAEH,iCAAiC;IACjC,IAAA,iBAAS,EAAC,GAAG,EAAE;QACX,IAAI,OAAO,IAAI,QAAQ,EAAE,CAAC;YACtB,MAAM,OAAO,GAAoB;gBAC7B,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC;gBACvB,OAAO,EAAE,MAAM;gBACf,IAAI,EAAE,QAAQ;gBACd,sBAAsB,EAAE,EAAE;gBAC1B,GAAG,CAAC,SAAS,IAAI,EAAE,CAAC;aACvB,CAAC;YAEF,IAAI,UAAU,EAAE,CAAC;gBACb,OAAO,CAAC,YAAY,GAAG,UAAU,CAAC;YACtC,CAAC;YAED,IAAI,KAAK;gBAAE,KAAK,CAAC,6BAA6B,QAAQ,CAAC,MAAM,QAAQ,EAAE,MAAM,CAAC,CAAC;YAC/E,YAAY,CAAC,OAAO,CAAC,CAAC;QAC1B,CAAC;IACL,CAAC,EAAE,CAAC,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,YAAY,EAAE,KAAK,CAAC,CAAC,CAAC;IAEpF,OAAO,CACH,iCAAK,KAAK,EAAE,EAAE,QAAQ,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,KAAK,EAAE,aAExE,SAAS,IAAI,CACV,gCAAK,KAAK,EAAE;oBACR,QAAQ,EAAE,UAAU;oBACpB,GAAG,EAAE,CAAC;oBACN,IAAI,EAAE,CAAC;oBACP,KAAK,EAAE,CAAC;oBACR,MAAM,EAAE,CAAC;oBACT,OAAO,EAAE,MAAM;oBACf,UAAU,EAAE,QAAQ;oBACpB,cAAc,EAAE,QAAQ;oBACxB,eAAe,EAAE,uBAAuB;oBACxC,MAAM,EAAE,EAAE;iBACb,YACG,iCAAK,KAAK,EAAE,EAAE,SAAS,EAAE,QAAQ,EAAE,aAC/B,gCAAK,KAAK,EAAE;gCACR,KAAK,EAAE,MAAM;gCACb,MAAM,EAAE,MAAM;gCACd,MAAM,EAAE,mBAAmB;gCAC3B,SAAS,EAAE,mBAAmB;gCAC9B,YAAY,EAAE,KAAK;gCACnB,SAAS,EAAE,yBAAyB;gCACpC,MAAM,EAAE,aAAa;6BACxB,GAAI,EACL,8BAAG,KAAK,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,gCAAqB,EACnD,4CAAQ;;;;;yBAKP,GAAS,IACR,GACJ,CACT,EAGD,mCACI,GAAG,EAAE,SAAS,EACd,GAAG,EAAE,OAAO,EACZ,SAAS,EAAE,SAAS,EACpB,KAAK,EAAE;oBACH,KAAK,EAAE,MAAM;oBACb,MAAM,EAAE,MAAM;oBACd,MAAM,EAAE,MAAM;iBACjB,EACD,eAAe,SACjB,IACA,CACT,CAAC;AACN,CAAC,CAAC;AA3HW,QAAA,cAAc,kBA2HzB;AAEF,kBAAe,sBAAc,CAAC"}
|
||||
Reference in New Issue
Block a user