LMS-BGN/src/components/quiz/InteractiveQuizRenderer.tsx

315 lines
9.3 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import { useExam } from '../../contexts/ExamContext';
import { InteractiveQuestion } from '../../types';
import { cn } from '../../utils/cn';
import {
Brain,
Puzzle,
ChefHat,
MousePointer,
Timer,
Star,
AlertCircle,
CheckCircle,
HelpCircle,
Image,
Zap,
Gamepad2,
Heart
} from 'lucide-react';
// Import komponen kuis interaktif
import EnhancedMCQComponent from './EnhancedMCQComponent';
import PuzzleQuestion from './PuzzleQuestion';
import ScenarioSimulation from './ScenarioSimulation';
import PuzzleGambarIconMatch from './PuzzleGambarIconMatch';
import TrueFalseCepatTepat from './TrueFalseCepatTepat';
import MiniSimulationRPG from './MiniSimulationRPG';
import MiniSurveyReflection from './MiniSurveyReflection';
import VideoScenario from './VideoScenario';
import InteractiveImageHotspot from './InteractiveImageHotspot';
import MediaGallery from './MediaGallery';
interface InteractiveQuizRendererProps {
question: InteractiveQuestion;
questionIndex: number;
adminConfig?: any;
onAnswerChange?: (answer: any) => void;
onConfidenceChange?: (confidence: number) => void;
className?: string;
}
const InteractiveQuizRenderer: React.FC<InteractiveQuizRendererProps> = ({
question,
questionIndex,
adminConfig,
onAnswerChange,
onConfidenceChange,
className
}) => {
const {
state,
setInteractiveState,
setConfidenceRating,
trackHesitation,
trackAnswerChange,
behaviorTrackingEnabled,
adminLanguage
} = useExam();
const [startTime] = useState(Date.now());
const [lastInteraction, setLastInteraction] = useState(Date.now());
// Guard clause untuk question yang undefined
if (!question) {
return (
<div className="p-6 text-center text-gray-500">
<AlertCircle className="mx-auto mb-2" size={48} />
<p>Pertanyaan tidak ditemukan</p>
</div>
);
}
// Tracking perilaku pengguna
useEffect(() => {
// Behavior tracking is disabled for now
if (false && behaviorTrackingEnabled && question?.id) {
setInteractiveState({
currentQuestionType: question.type
});
}
}, [question?.id, behaviorTrackingEnabled, setInteractiveState, question?.type]);
// Handle perubahan jawaban dengan tracking
const handleAnswerChange = (answer: any) => {
const now = Date.now();
const timeSinceLastInteraction = now - lastInteraction;
// Track hesitation jika ada jeda yang lama (disabled for now)
if (false && timeSinceLastInteraction > 3000 && behaviorTrackingEnabled) {
trackHesitation(questionIndex, timeSinceLastInteraction);
}
// Track perubahan jawaban (disabled for now)
if (false && behaviorTrackingEnabled) {
trackAnswerChange(questionIndex);
}
setLastInteraction(now);
onAnswerChange?.(answer);
};
// Render ikon berdasarkan tipe pertanyaan
const renderQuestionTypeIcon = () => {
const iconProps = { size: 20, className: "text-blue-600" };
switch (question.type) {
case 'enhanced_mcq':
return <Brain {...iconProps} />;
case 'puzzle':
return <Puzzle {...iconProps} />;
case 'scenario':
return <ChefHat {...iconProps} />;
case 'puzzle_gambar_icon':
return <Image {...iconProps} />;
case 'true_false_cepat':
return <Zap {...iconProps} />;
case 'mini_simulation_rpg':
return <Gamepad2 {...iconProps} />;
case 'mini_survey_reflection':
return <Heart {...iconProps} />;
case 'video_scenario':
return <ChefHat {...iconProps} />;
case 'image_hotspot':
return <MousePointer {...iconProps} />;
case 'media_gallery':
return <Image {...iconProps} />;
default:
return <HelpCircle {...iconProps} />;
}
};
// Render komponen berdasarkan tipe pertanyaan
const renderQuestionComponent = () => {
const commonProps = {
question,
questionIndex,
onAnswerChange: handleAnswerChange,
adminLanguage
};
switch (question.type) {
case 'enhanced_mcq':
return <EnhancedMCQComponent {...commonProps} />;
case 'puzzle':
return <PuzzleQuestion {...commonProps} />;
case 'scenario':
return <ScenarioSimulation {...commonProps} />;
case 'puzzle_gambar_icon':
return <PuzzleGambarIconMatch {...commonProps} />;
case 'true_false_cepat':
return <TrueFalseCepatTepat {...commonProps} />;
case 'mini_simulation_rpg':
return <MiniSimulationRPG {...commonProps} />;
case 'mini_survey_reflection':
return <MiniSurveyReflection {...commonProps} />;
case 'video_scenario':
return (
<VideoScenario
videoSrc={question.content?.media?.src || ''}
title={question.question || ''}
description={question.content?.instructions || ''}
scenarios={question.scenarios || []}
onScenarioComplete={(results) => {
if (onAnswerChange) {
onAnswerChange(results);
}
}}
/>
);
case 'image_hotspot':
return (
<InteractiveImageHotspot
imageSrc={question.content?.media?.src || ''}
title={question.question || ''}
description={question.content?.instructions || ''}
hotspots={question.hotspots || []}
onComplete={(results) => {
if (onAnswerChange) {
onAnswerChange(results);
}
}}
showHints={true}
allowMultipleAttempts={true}
/>
);
case 'media_gallery':
return <MediaGallery {...commonProps} />;
default:
return (
<div className="p-6 text-center text-gray-500">
<AlertCircle className="mx-auto mb-2" size={48} />
<p>Tipe pertanyaan tidak didukung: {question.type}</p>
</div>
);
}
};
// Render difficulty indicator
const renderDifficultyIndicator = () => {
if (!question.difficulty) return null;
const difficultyColors = {
easy: 'text-green-600 bg-green-100',
medium: 'text-yellow-600 bg-yellow-100',
hard: 'text-red-600 bg-red-100'
};
const difficultyLabels = {
easy: adminLanguage === 'indonesia' ? 'Mudah' : 'Easy',
medium: adminLanguage === 'indonesia' ? 'Sedang' : 'Medium',
hard: adminLanguage === 'indonesia' ? 'Sulit' : 'Hard'
};
return (
<span className={cn(
'inline-flex items-center px-2 py-1 rounded-full text-xs font-medium',
difficultyColors[question.difficulty]
)}>
{difficultyLabels[question.difficulty]}
</span>
);
};
// Render time limit indicator
const renderTimeLimit = () => {
if (!question.timeLimit) return null;
return (
<div className="flex items-center text-sm text-gray-600">
<Timer size={16} className="mr-1" />
<span>
{adminLanguage === 'indonesia' ? 'Batas waktu: ' : 'Time limit: '}
{Math.floor(question.timeLimit / 60)}:{(question.timeLimit % 60).toString().padStart(2, '0')}
</span>
</div>
);
};
return (
<div className={cn(
'bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden',
className
)}>
{/* Header dengan informasi pertanyaan */}
<div className="px-6 py-4 bg-gray-50 border-b border-gray-200">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
{renderQuestionTypeIcon()}
<div>
<h3 className="text-lg font-semibold text-gray-900">
{adminLanguage === 'indonesia' ? 'Pertanyaan' : 'Question'} {questionIndex + 1}
</h3>
<div className="flex items-center space-x-3 mt-1">
{renderDifficultyIndicator()}
{renderTimeLimit()}
</div>
</div>
</div>
{/* Status indikator */}
<div className="flex items-center space-x-2">
{state.answers[questionIndex] && (
<CheckCircle className="text-green-600" size={20} />
)}
{state.flaggedQuestions.has(questionIndex) && (
<Star className="text-yellow-600" size={20} />
)}
</div>
</div>
</div>
{/* Konten pertanyaan */}
<div className="p-6">
{/* Teks pertanyaan */}
<div className="mb-6">
<p className="text-lg text-gray-900 leading-relaxed">
{question.text || question.content?.question}
</p>
{/* Instruksi tambahan */}
{question.content?.instructions && (
<div className="mt-3 p-3 bg-blue-50 border-l-4 border-blue-400 rounded-r">
<p className="text-sm text-blue-800">
<strong>
{adminLanguage === 'indonesia' ? 'Instruksi: ' : 'Instructions: '}
</strong>
{question.content.instructions}
</p>
</div>
)}
</div>
{/* Render komponen pertanyaan */}
{renderQuestionComponent()}
</div>
</div>
);
};
export default InteractiveQuizRenderer;