315 lines
9.3 KiB
TypeScript
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; |