MAJOR ARCHITECTURE UPDATE - C++ Style Interface Enforcement 🔒 **Strict Interface System**: - Created DRSExerciseInterface (10 required methods) - Created ProgressSystemInterface (17 required methods) - Updated ImplementationValidator with 3-phase validation - Red screen errors for missing implementations 📚 **11/11 Exercise Modules Implemented**: ✅ VocabularyModule - Local flashcard validation ✅ TextAnalysisModule - AI text comprehension ✅ GrammarAnalysisModule - AI grammar correction ✅ TranslationModule - AI translation validation ✅ OpenResponseModule - AI open-ended responses ✅ PhraseModule - Phrase comprehension ✅ AudioModule - Audio listening exercises ✅ ImageModule - Visual comprehension ✅ GrammarModule - Grammar exercises ✅ TextModule - Reading comprehension ✅ WordDiscoveryModule - Vocabulary introduction 🎯 **Required Methods (All Modules)**: - Lifecycle: init(), render(), destroy() - Exercise: validate(), getResults(), handleUserInput() - Progress: markCompleted(), getProgress() - Metadata: getExerciseType(), getExerciseConfig() 📋 **Documentation**: - Updated CLAUDE.md with complete interface hierarchy - Created DRS_IMPLEMENTATION_PLAN.md (roadmap) - Documented enforcement rules and patterns 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
688 lines
24 KiB
JavaScript
688 lines
24 KiB
JavaScript
/**
|
|
* OpenResponseModule - Free-form open response exercises with AI validation
|
|
* Allows students to write free-text responses that are evaluated by AI
|
|
* Implements DRSExerciseInterface for strict contract enforcement
|
|
*/
|
|
|
|
import DRSExerciseInterface from '../interfaces/DRSExerciseInterface.js';
|
|
|
|
class OpenResponseModule extends DRSExerciseInterface {
|
|
constructor(orchestrator, llmValidator, prerequisiteEngine, contextMemory) {
|
|
super('OpenResponseModule');
|
|
|
|
// Validate dependencies
|
|
if (!orchestrator || !llmValidator || !prerequisiteEngine || !contextMemory) {
|
|
throw new Error('OpenResponseModule requires all service dependencies');
|
|
}
|
|
|
|
this.orchestrator = orchestrator;
|
|
this.llmValidator = llmValidator;
|
|
this.prerequisiteEngine = prerequisiteEngine;
|
|
this.contextMemory = contextMemory;
|
|
|
|
// Module state
|
|
this.initialized = false;
|
|
this.container = null;
|
|
this.currentExerciseData = null;
|
|
this.currentQuestion = null;
|
|
this.userResponse = '';
|
|
this.validationInProgress = false;
|
|
this.lastValidationResult = null;
|
|
|
|
// Configuration
|
|
this.config = {
|
|
requiredProvider: 'openai', // Open response needs good comprehension
|
|
model: 'gpt-4o-mini',
|
|
temperature: 0.2, // Low for consistent evaluation
|
|
maxTokens: 1000,
|
|
timeout: 45000,
|
|
minResponseLength: 10, // Minimum characters for response
|
|
maxResponseLength: 2000, // Maximum characters for response
|
|
allowMultipleAttempts: true,
|
|
feedbackDepth: 'detailed' // detailed, brief, or minimal
|
|
};
|
|
|
|
// Progress tracking
|
|
this.progress = {
|
|
questionsAnswered: 0,
|
|
questionsCorrect: 0,
|
|
averageScore: 0,
|
|
totalAttempts: 0,
|
|
timeSpent: 0,
|
|
startTime: null
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Check if module can run with given content
|
|
*/
|
|
canRun(prerequisites, chapterContent) {
|
|
// Can run with any content - will generate open questions
|
|
return chapterContent && (
|
|
chapterContent.vocabulary ||
|
|
chapterContent.texts ||
|
|
chapterContent.phrases ||
|
|
chapterContent.grammar
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Present the open response exercise
|
|
*/
|
|
async present(container, exerciseData) {
|
|
this.container = container;
|
|
this.currentExerciseData = exerciseData;
|
|
this.progress.startTime = Date.now();
|
|
|
|
console.log('📝 Starting Open Response exercise...');
|
|
|
|
// Generate or select question
|
|
await this._generateQuestion();
|
|
|
|
// Create UI
|
|
this._createExerciseInterface();
|
|
|
|
this.initialized = true;
|
|
}
|
|
|
|
/**
|
|
* Validate user response using AI
|
|
*/
|
|
async validate(userInput, context) {
|
|
if (this.validationInProgress) {
|
|
return { score: 0, feedback: 'Validation already in progress', isCorrect: false };
|
|
}
|
|
|
|
this.validationInProgress = true;
|
|
this.userResponse = userInput.trim();
|
|
|
|
try {
|
|
// Basic validation
|
|
if (this.userResponse.length < this.config.minResponseLength) {
|
|
return {
|
|
score: 0,
|
|
feedback: `Response too short. Please provide at least ${this.config.minResponseLength} characters.`,
|
|
isCorrect: false,
|
|
suggestions: ['Try to elaborate more on your answer', 'Provide more details and examples']
|
|
};
|
|
}
|
|
|
|
if (this.userResponse.length > this.config.maxResponseLength) {
|
|
return {
|
|
score: 0,
|
|
feedback: `Response too long. Please keep it under ${this.config.maxResponseLength} characters.`,
|
|
isCorrect: false,
|
|
suggestions: ['Try to be more concise', 'Focus on the main points']
|
|
};
|
|
}
|
|
|
|
// AI validation
|
|
const aiResult = await this._validateWithAI();
|
|
|
|
// Update progress
|
|
this._updateProgress(aiResult);
|
|
|
|
this.lastValidationResult = aiResult;
|
|
return aiResult;
|
|
|
|
} catch (error) {
|
|
console.error('Open response validation error:', error);
|
|
return {
|
|
score: 0.5,
|
|
feedback: 'Unable to validate response. Please try again.',
|
|
isCorrect: false,
|
|
error: error.message
|
|
};
|
|
} finally {
|
|
this.validationInProgress = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get current progress
|
|
*/
|
|
getProgress() {
|
|
const timeSpent = this.progress.startTime ? Date.now() - this.progress.startTime : 0;
|
|
|
|
return {
|
|
type: 'open-response',
|
|
questionsAnswered: this.progress.questionsAnswered,
|
|
questionsCorrect: this.progress.questionsCorrect,
|
|
accuracy: this.progress.questionsAnswered > 0 ?
|
|
this.progress.questionsCorrect / this.progress.questionsAnswered : 0,
|
|
averageScore: this.progress.averageScore,
|
|
timeSpent: timeSpent,
|
|
currentQuestion: this.currentQuestion?.question,
|
|
responseLength: this.userResponse.length
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get module metadata
|
|
*/
|
|
getMetadata() {
|
|
return {
|
|
name: 'Open Response',
|
|
type: 'open-response',
|
|
difficulty: 'advanced',
|
|
estimatedTime: 300, // 5 minutes per question
|
|
capabilities: ['creative_writing', 'comprehension', 'analysis'],
|
|
prerequisites: ['basic_vocabulary']
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Cleanup module
|
|
*/
|
|
cleanup() {
|
|
if (this.container) {
|
|
this.container.innerHTML = '';
|
|
}
|
|
this.initialized = false;
|
|
this.validationInProgress = false;
|
|
}
|
|
|
|
// Private methods
|
|
|
|
/**
|
|
* Generate open response question
|
|
*/
|
|
async _generateQuestion() {
|
|
const chapterContent = this.currentExerciseData.chapterContent || {};
|
|
|
|
// Question types based on available content
|
|
const questionTypes = [];
|
|
|
|
if (chapterContent.vocabulary) {
|
|
questionTypes.push('vocabulary_usage', 'vocabulary_explanation');
|
|
}
|
|
|
|
if (chapterContent.texts) {
|
|
questionTypes.push('text_comprehension', 'text_analysis');
|
|
}
|
|
|
|
if (chapterContent.phrases) {
|
|
questionTypes.push('phrase_creation', 'situation_usage');
|
|
}
|
|
|
|
if (chapterContent.grammar) {
|
|
questionTypes.push('grammar_explanation', 'grammar_examples');
|
|
}
|
|
|
|
// Fallback questions
|
|
if (questionTypes.length === 0) {
|
|
questionTypes.push('general_discussion', 'opinion_question');
|
|
}
|
|
|
|
const selectedType = questionTypes[Math.floor(Math.random() * questionTypes.length)];
|
|
this.currentQuestion = await this._createQuestionByType(selectedType, chapterContent);
|
|
}
|
|
|
|
/**
|
|
* Create question based on type
|
|
*/
|
|
async _createQuestionByType(type, content) {
|
|
const questions = {
|
|
vocabulary_usage: {
|
|
question: this._createVocabularyUsageQuestion(content.vocabulary),
|
|
type: 'vocabulary',
|
|
expectedLength: 100,
|
|
criteria: ['correct_word_usage', 'context_appropriateness', 'grammar']
|
|
},
|
|
vocabulary_explanation: {
|
|
question: this._createVocabularyExplanationQuestion(content.vocabulary),
|
|
type: 'vocabulary',
|
|
expectedLength: 150,
|
|
criteria: ['accuracy', 'clarity', 'examples']
|
|
},
|
|
text_comprehension: {
|
|
question: this._createTextComprehensionQuestion(content.texts),
|
|
type: 'comprehension',
|
|
expectedLength: 200,
|
|
criteria: ['understanding', 'details', 'inference']
|
|
},
|
|
phrase_creation: {
|
|
question: this._createPhraseCreationQuestion(content.phrases),
|
|
type: 'creative',
|
|
expectedLength: 150,
|
|
criteria: ['creativity', 'relevance', 'grammar']
|
|
},
|
|
grammar_explanation: {
|
|
question: this._createGrammarExplanationQuestion(content.grammar),
|
|
type: 'grammar',
|
|
expectedLength: 180,
|
|
criteria: ['accuracy', 'examples', 'clarity']
|
|
},
|
|
general_discussion: {
|
|
question: this._createGeneralDiscussionQuestion(),
|
|
type: 'discussion',
|
|
expectedLength: 200,
|
|
criteria: ['coherence', 'development', 'language_use']
|
|
}
|
|
};
|
|
|
|
return questions[type] || questions.general_discussion;
|
|
}
|
|
|
|
_createVocabularyUsageQuestion(vocabulary) {
|
|
if (!vocabulary || Object.keys(vocabulary).length === 0) {
|
|
return "Describe your daily routine using as much detail as possible.";
|
|
}
|
|
|
|
const words = Object.keys(vocabulary);
|
|
const selectedWords = words.slice(0, 3).join(', ');
|
|
|
|
return `Write a short paragraph using these words: ${selectedWords}. Make sure to use each word correctly in context.`;
|
|
}
|
|
|
|
_createVocabularyExplanationQuestion(vocabulary) {
|
|
if (!vocabulary || Object.keys(vocabulary).length === 0) {
|
|
return "Explain the difference between 'house' and 'home' and give examples.";
|
|
}
|
|
|
|
const words = Object.keys(vocabulary);
|
|
const selectedWord = words[Math.floor(Math.random() * words.length)];
|
|
|
|
return `Explain the meaning of "${selectedWord}" and provide at least two example sentences showing how to use it.`;
|
|
}
|
|
|
|
_createTextComprehensionQuestion(texts) {
|
|
if (!texts || texts.length === 0) {
|
|
return "Describe a memorable experience you had and explain why it was important to you.";
|
|
}
|
|
|
|
return "Based on the chapter content, what are the main themes discussed and how do they relate to everyday life?";
|
|
}
|
|
|
|
_createPhraseCreationQuestion(phrases) {
|
|
if (!phrases || Object.keys(phrases).length === 0) {
|
|
return "Create a dialogue between two people meeting for the first time.";
|
|
}
|
|
|
|
return "Using the phrases from this chapter, write a short conversation that might happen in a real-life situation.";
|
|
}
|
|
|
|
_createGrammarExplanationQuestion(grammar) {
|
|
if (!grammar || Object.keys(grammar).length === 0) {
|
|
return "Explain when to use 'a' vs 'an' and give three examples of each.";
|
|
}
|
|
|
|
const concepts = Object.keys(grammar);
|
|
const selectedConcept = concepts[Math.floor(Math.random() * concepts.length)];
|
|
|
|
return `Explain the grammar rule for "${selectedConcept}" and provide examples showing correct and incorrect usage.`;
|
|
}
|
|
|
|
_createGeneralDiscussionQuestion() {
|
|
const questions = [
|
|
"What advice would you give to someone learning English for the first time?",
|
|
"Describe your ideal weekend and explain why these activities appeal to you.",
|
|
"What are the advantages and disadvantages of living in a big city?",
|
|
"How has technology changed the way people communicate?",
|
|
"What qualities make a good friend? Explain with examples."
|
|
];
|
|
|
|
return questions[Math.floor(Math.random() * questions.length)];
|
|
}
|
|
|
|
/**
|
|
* Validate response with AI
|
|
*/
|
|
async _validateWithAI() {
|
|
const prompt = this._buildValidationPrompt();
|
|
|
|
try {
|
|
const result = await this.llmValidator.validateAnswer(
|
|
this.currentQuestion.question,
|
|
this.userResponse,
|
|
{
|
|
provider: this.config.requiredProvider,
|
|
model: this.config.model,
|
|
temperature: this.config.temperature,
|
|
maxTokens: this.config.maxTokens,
|
|
timeout: this.config.timeout,
|
|
context: prompt
|
|
}
|
|
);
|
|
|
|
return this._parseAIResponse(result);
|
|
|
|
} catch (error) {
|
|
console.error('AI validation failed:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
_buildValidationPrompt() {
|
|
return `
|
|
You are evaluating an open response answer from an English language learner.
|
|
|
|
Question: "${this.currentQuestion.question}"
|
|
Student Response: "${this.userResponse}"
|
|
|
|
Evaluation Criteria:
|
|
- ${this.currentQuestion.criteria.join('\n- ')}
|
|
|
|
Expected Response Length: ~${this.currentQuestion.expectedLength} characters
|
|
Actual Response Length: ${this.userResponse.length} characters
|
|
|
|
Please evaluate the response and provide:
|
|
1. A score from 0.0 to 1.0
|
|
2. Detailed feedback on strengths and areas for improvement
|
|
3. Specific suggestions for enhancement
|
|
4. Whether the response adequately addresses the question
|
|
|
|
Format your response as:
|
|
[score]0.85
|
|
[feedback]Your response shows good understanding... [detailed feedback]
|
|
[suggestions]Consider adding more examples... [specific suggestions]
|
|
[correct]yes/no
|
|
`.trim();
|
|
}
|
|
|
|
_parseAIResponse(aiResult) {
|
|
try {
|
|
const response = aiResult.response || '';
|
|
|
|
// Extract score
|
|
const scoreMatch = response.match(/\[score\]([\d.]+)/);
|
|
const score = scoreMatch ? parseFloat(scoreMatch[1]) : 0.5;
|
|
|
|
// Extract feedback
|
|
const feedbackMatch = response.match(/\[feedback\](.*?)\[suggestions\]/s);
|
|
const feedback = feedbackMatch ? feedbackMatch[1].trim() : 'Response evaluated';
|
|
|
|
// Extract suggestions
|
|
const suggestionsMatch = response.match(/\[suggestions\](.*?)\[correct\]/s);
|
|
const suggestions = suggestionsMatch ? suggestionsMatch[1].trim().split('\n') : [];
|
|
|
|
// Extract correctness
|
|
const correctMatch = response.match(/\[correct\](yes|no)/i);
|
|
const isCorrect = correctMatch ? correctMatch[1].toLowerCase() === 'yes' : score >= 0.7;
|
|
|
|
return {
|
|
score,
|
|
feedback,
|
|
suggestions: suggestions.filter(s => s.trim().length > 0),
|
|
isCorrect,
|
|
criteria: this.currentQuestion.criteria,
|
|
questionType: this.currentQuestion.type,
|
|
aiProvider: this.config.requiredProvider
|
|
};
|
|
|
|
} catch (error) {
|
|
console.error('Error parsing AI response:', error);
|
|
return {
|
|
score: 0.5,
|
|
feedback: 'Unable to parse evaluation. Please try again.',
|
|
suggestions: [],
|
|
isCorrect: false,
|
|
error: 'parsing_error'
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create exercise interface
|
|
*/
|
|
_createExerciseInterface() {
|
|
this.container.innerHTML = `
|
|
<div class="open-response-exercise">
|
|
<div class="exercise-header">
|
|
<h2>📝 Open Response</h2>
|
|
<div class="exercise-meta">
|
|
<span class="question-type">${this.currentQuestion.type}</span>
|
|
<span class="expected-length">Target: ~${this.currentQuestion.expectedLength} chars</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="question-section">
|
|
<div class="question-text">
|
|
${this.currentQuestion.question}
|
|
</div>
|
|
|
|
<div class="criteria-info">
|
|
<strong>Evaluation criteria:</strong>
|
|
<ul>
|
|
${this.currentQuestion.criteria.map(c => `<li>${c.replace(/_/g, ' ')}</li>`).join('')}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="response-section">
|
|
<textarea
|
|
id="open-response-input"
|
|
placeholder="Write your response here..."
|
|
maxlength="${this.config.maxResponseLength}"
|
|
></textarea>
|
|
|
|
<div class="response-meta">
|
|
<span id="character-count">0 / ${this.config.maxResponseLength}</span>
|
|
<span class="min-length">Minimum: ${this.config.minResponseLength} characters</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="action-section">
|
|
<button id="validate-response" class="btn btn-primary" disabled>
|
|
Submit Response
|
|
</button>
|
|
<button id="clear-response" class="btn btn-secondary">
|
|
Clear
|
|
</button>
|
|
</div>
|
|
|
|
<div id="validation-result" class="validation-result" style="display: none;">
|
|
<!-- Validation results will appear here -->
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
this._attachEventListeners();
|
|
}
|
|
|
|
_attachEventListeners() {
|
|
const textarea = document.getElementById('open-response-input');
|
|
const validateBtn = document.getElementById('validate-response');
|
|
const clearBtn = document.getElementById('clear-response');
|
|
const charCount = document.getElementById('character-count');
|
|
|
|
if (textarea) {
|
|
textarea.addEventListener('input', (e) => {
|
|
const length = e.target.value.length;
|
|
charCount.textContent = `${length} / ${this.config.maxResponseLength}`;
|
|
|
|
// Enable/disable submit button
|
|
validateBtn.disabled = length < this.config.minResponseLength;
|
|
|
|
// Update character count color
|
|
if (length < this.config.minResponseLength) {
|
|
charCount.style.color = '#dc3545';
|
|
} else if (length > this.config.maxResponseLength * 0.9) {
|
|
charCount.style.color = '#ffc107';
|
|
} else {
|
|
charCount.style.color = '#28a745';
|
|
}
|
|
});
|
|
}
|
|
|
|
if (validateBtn) {
|
|
validateBtn.addEventListener('click', async () => {
|
|
await this._handleValidation();
|
|
});
|
|
}
|
|
|
|
if (clearBtn) {
|
|
clearBtn.addEventListener('click', () => {
|
|
textarea.value = '';
|
|
textarea.dispatchEvent(new Event('input'));
|
|
document.getElementById('validation-result').style.display = 'none';
|
|
});
|
|
}
|
|
}
|
|
|
|
async _handleValidation() {
|
|
const textarea = document.getElementById('open-response-input');
|
|
const validateBtn = document.getElementById('validate-response');
|
|
const resultDiv = document.getElementById('validation-result');
|
|
|
|
validateBtn.disabled = true;
|
|
validateBtn.textContent = 'Validating...';
|
|
|
|
try {
|
|
const result = await this.validate(textarea.value, {});
|
|
this._displayValidationResult(result);
|
|
} catch (error) {
|
|
console.error('Validation error:', error);
|
|
this._displayValidationResult({
|
|
score: 0,
|
|
feedback: 'Error validating response. Please try again.',
|
|
isCorrect: false,
|
|
suggestions: []
|
|
});
|
|
} finally {
|
|
validateBtn.disabled = false;
|
|
validateBtn.textContent = 'Submit Response';
|
|
}
|
|
}
|
|
|
|
_displayValidationResult(result) {
|
|
const resultDiv = document.getElementById('validation-result');
|
|
|
|
const scorePercentage = Math.round(result.score * 100);
|
|
const scoreClass = result.isCorrect ? 'success' : (result.score >= 0.5 ? 'warning' : 'error');
|
|
|
|
resultDiv.innerHTML = `
|
|
<div class="result-header ${scoreClass}">
|
|
<div class="score-display">
|
|
<span class="score-value">${scorePercentage}%</span>
|
|
<span class="score-label">${result.isCorrect ? 'Good Response!' : 'Needs Improvement'}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="feedback-section">
|
|
<h4>Feedback:</h4>
|
|
<p>${result.feedback}</p>
|
|
</div>
|
|
|
|
${result.suggestions && result.suggestions.length > 0 ? `
|
|
<div class="suggestions-section">
|
|
<h4>Suggestions for improvement:</h4>
|
|
<ul>
|
|
${result.suggestions.map(s => `<li>${s}</li>`).join('')}
|
|
</ul>
|
|
</div>
|
|
` : ''}
|
|
|
|
<div class="result-actions">
|
|
<button id="try-again" class="btn btn-secondary">Try Again</button>
|
|
<button id="next-question" class="btn btn-primary">Next Question</button>
|
|
</div>
|
|
`;
|
|
|
|
resultDiv.style.display = 'block';
|
|
|
|
// Attach action handlers
|
|
document.getElementById('try-again')?.addEventListener('click', () => {
|
|
resultDiv.style.display = 'none';
|
|
});
|
|
|
|
document.getElementById('next-question')?.addEventListener('click', () => {
|
|
this._nextQuestion();
|
|
});
|
|
}
|
|
|
|
async _nextQuestion() {
|
|
await this._generateQuestion();
|
|
this._createExerciseInterface();
|
|
}
|
|
|
|
_updateProgress(result) {
|
|
this.progress.questionsAnswered++;
|
|
this.progress.totalAttempts++;
|
|
|
|
if (result.isCorrect) {
|
|
this.progress.questionsCorrect++;
|
|
}
|
|
|
|
// Update average score
|
|
const previousTotal = this.progress.averageScore * (this.progress.questionsAnswered - 1);
|
|
this.progress.averageScore = (previousTotal + result.score) / this.progress.questionsAnswered;
|
|
}
|
|
|
|
// ========================================
|
|
// DRSExerciseInterface REQUIRED METHODS
|
|
// ========================================
|
|
|
|
async init(config = {}, content = {}) {
|
|
this.config = { ...this.config, ...config };
|
|
this.currentExerciseData = content;
|
|
this.startTime = Date.now();
|
|
this.initialized = true;
|
|
}
|
|
|
|
async render(container) {
|
|
if (!this.initialized) throw new Error('OpenResponseModule must be initialized before rendering');
|
|
await this.present(container, this.currentExerciseData);
|
|
}
|
|
|
|
async destroy() {
|
|
this.cleanup?.();
|
|
this.container = null;
|
|
this.initialized = false;
|
|
}
|
|
|
|
getResults() {
|
|
const totalQuestions = this.questions ? this.questions.length : 0;
|
|
const answeredQuestions = this.userResponses ? this.userResponses.length : 0;
|
|
const score = this.progress ? Math.round(this.progress.averageScore * 100) : 0;
|
|
|
|
return {
|
|
score,
|
|
attempts: answeredQuestions,
|
|
timeSpent: this.startTime ? Date.now() - this.startTime : 0,
|
|
completed: answeredQuestions >= totalQuestions,
|
|
details: {
|
|
totalQuestions,
|
|
answeredQuestions,
|
|
averageScore: this.progress?.averageScore || 0,
|
|
responses: this.userResponses || []
|
|
}
|
|
};
|
|
}
|
|
|
|
handleUserInput(event, data) {
|
|
if (event && event.type === 'input') this._handleInputChange?.(event);
|
|
if (event && event.type === 'click' && event.target.id === 'submitButton') this._handleSubmit?.(event);
|
|
}
|
|
|
|
async markCompleted(results) {
|
|
const { score, details } = results || this.getResults();
|
|
if (this.contextMemory) {
|
|
this.contextMemory.recordInteraction({
|
|
type: 'open-response',
|
|
subtype: 'completion',
|
|
content: { questions: this.questions },
|
|
userResponse: this.userResponses,
|
|
validation: { score, averageScore: details.averageScore },
|
|
context: { moduleType: 'open-response', totalQuestions: details.totalQuestions }
|
|
});
|
|
}
|
|
}
|
|
|
|
getExerciseType() {
|
|
return 'open-response';
|
|
}
|
|
|
|
getExerciseConfig() {
|
|
const questionCount = this.questions ? this.questions.length : this.config?.questionsPerExercise || 2;
|
|
return {
|
|
type: this.getExerciseType(),
|
|
difficulty: this.currentExerciseData?.difficulty || 'medium',
|
|
estimatedTime: questionCount * 3, // 3 min per question
|
|
prerequisites: [],
|
|
metadata: { ...this.config, questionCount, requiresAI: true }
|
|
};
|
|
}
|
|
}
|
|
|
|
export default OpenResponseModule; |