960 lines
35 KiB
TypeScript
960 lines
35 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useRef } from 'react';
|
|
import {
|
|
Plus,
|
|
Minus,
|
|
Upload,
|
|
Image,
|
|
Video,
|
|
Music,
|
|
File,
|
|
X,
|
|
Eye,
|
|
Settings,
|
|
Puzzle,
|
|
Gamepad2,
|
|
Brain,
|
|
Timer,
|
|
Star,
|
|
Heart,
|
|
Zap,
|
|
MousePointer,
|
|
Shuffle,
|
|
Target,
|
|
CheckCircle,
|
|
AlertCircle,
|
|
HelpCircle,
|
|
Play,
|
|
Pause
|
|
} from 'lucide-react';
|
|
import { MultimediaManager, MultimediaFile } from './MultimediaManager';
|
|
|
|
export interface InteractiveQuestionOption {
|
|
id: string;
|
|
text: string;
|
|
isCorrect: boolean;
|
|
multimedia?: MultimediaFile;
|
|
explanation?: string;
|
|
weight?: number; // for weighted scoring
|
|
}
|
|
|
|
export interface InteractiveQuestionData {
|
|
id?: string;
|
|
title: string;
|
|
type: 'multiple_choice' | 'essay' | 'true_false' | 'enhanced_mcq' | 'puzzle' | 'scenario' | 'puzzle_gambar_icon' | 'true_false_cepat' | 'mini_simulation_rpg' | 'mini_survey_reflection';
|
|
category: string;
|
|
difficulty: 'easy' | 'medium' | 'hard';
|
|
points: number;
|
|
explanation?: string;
|
|
status: 'draft' | 'active';
|
|
|
|
// Standard question data
|
|
options?: InteractiveQuestionOption[];
|
|
essayAnswer?: string;
|
|
trueFalseAnswer?: boolean;
|
|
|
|
// Interactive features
|
|
multimedia?: MultimediaFile[];
|
|
interactiveConfig?: {
|
|
// Enhanced MCQ
|
|
allowMultipleAnswers?: boolean;
|
|
enableHints?: boolean;
|
|
hints?: string[];
|
|
|
|
// Puzzle features
|
|
puzzleType?: 'drag_drop' | 'sequencing' | 'matching' | 'visual_matching';
|
|
puzzleItems?: Array<{
|
|
id: string;
|
|
content: string;
|
|
position?: { x: number; y: number };
|
|
matchTarget?: string;
|
|
multimedia?: MultimediaFile;
|
|
}>;
|
|
|
|
// Scenario simulation
|
|
scenarioStages?: Array<{
|
|
id: string;
|
|
title: string;
|
|
description: string;
|
|
choices: Array<{
|
|
id: string;
|
|
text: string;
|
|
consequence: string;
|
|
nextStage?: string;
|
|
}>;
|
|
multimedia?: MultimediaFile;
|
|
}>;
|
|
|
|
// RPG simulation
|
|
rpgConfig?: {
|
|
character?: {
|
|
name: string;
|
|
attributes: { [key: string]: number };
|
|
};
|
|
storyline?: Array<{
|
|
id: string;
|
|
text: string;
|
|
choices: Array<{
|
|
id: string;
|
|
text: string;
|
|
attributeEffects?: { [key: string]: number };
|
|
nextScene?: string;
|
|
}>;
|
|
}>;
|
|
};
|
|
|
|
// Reflection survey
|
|
reflectionConfig?: {
|
|
emojiScale?: {
|
|
min: number;
|
|
max: number;
|
|
labels: string[];
|
|
};
|
|
textPrompts?: string[];
|
|
allowSkip?: boolean;
|
|
};
|
|
|
|
// Timing and behavior
|
|
timeLimit?: number;
|
|
showTimer?: boolean;
|
|
trackBehavior?: boolean;
|
|
allowReview?: boolean;
|
|
};
|
|
}
|
|
|
|
interface EnhancedQuestionBuilderProps {
|
|
initialData?: Partial<InteractiveQuestionData>;
|
|
onSave: (questionData: InteractiveQuestionData) => void;
|
|
onCancel: () => void;
|
|
categories: string[];
|
|
className?: string;
|
|
}
|
|
|
|
export const EnhancedQuestionBuilder: React.FC<EnhancedQuestionBuilderProps> = ({
|
|
initialData,
|
|
onSave,
|
|
onCancel,
|
|
categories,
|
|
className = ''
|
|
}) => {
|
|
const [questionData, setQuestionData] = useState<InteractiveQuestionData>({
|
|
title: '',
|
|
type: 'multiple_choice',
|
|
category: '',
|
|
difficulty: 'medium',
|
|
points: 10,
|
|
explanation: '',
|
|
status: 'draft',
|
|
options: [
|
|
{ id: '1', text: '', isCorrect: false },
|
|
{ id: '2', text: '', isCorrect: false },
|
|
{ id: '3', text: '', isCorrect: false },
|
|
{ id: '4', text: '', isCorrect: false }
|
|
],
|
|
multimedia: [],
|
|
interactiveConfig: {
|
|
allowMultipleAnswers: false,
|
|
enableHints: false,
|
|
hints: [],
|
|
timeLimit: 0,
|
|
showTimer: false,
|
|
trackBehavior: false,
|
|
allowReview: true
|
|
},
|
|
...initialData
|
|
});
|
|
|
|
const [activeTab, setActiveTab] = useState<'basic' | 'content' | 'interactive' | 'multimedia' | 'preview'>('basic');
|
|
const [showMultimediaManager, setShowMultimediaManager] = useState(false);
|
|
const [previewMode, setPreviewMode] = useState(false);
|
|
|
|
const questionTypes = [
|
|
{ value: 'multiple_choice', label: 'Pilihan Ganda', icon: CheckCircle, description: 'Soal pilihan ganda standar' },
|
|
{ value: 'enhanced_mcq', label: 'Pilihan Ganda Plus', icon: Star, description: 'Pilihan ganda dengan fitur tambahan' },
|
|
{ value: 'essay', label: 'Essay', icon: File, description: 'Soal essay dengan jawaban terbuka' },
|
|
{ value: 'true_false', label: 'Benar/Salah', icon: Target, description: 'Soal benar atau salah' },
|
|
{ value: 'true_false_cepat', label: 'Benar/Salah Cepat', icon: Zap, description: 'Benar/salah dengan tracking waktu reaksi' },
|
|
{ value: 'puzzle', label: 'Puzzle', icon: Puzzle, description: 'Soal puzzle interaktif' },
|
|
{ value: 'puzzle_gambar_icon', label: 'Puzzle Visual', icon: Image, description: 'Puzzle dengan gambar dan ikon' },
|
|
{ value: 'scenario', label: 'Simulasi Skenario', icon: Brain, description: 'Simulasi skenario dengan pilihan bertingkat' },
|
|
{ value: 'mini_simulation_rpg', label: 'Mini RPG', icon: Gamepad2, description: 'Simulasi RPG mini dengan karakter' },
|
|
{ value: 'mini_survey_reflection', label: 'Refleksi Survey', icon: Heart, description: 'Survey refleksi dengan skala emoji' }
|
|
];
|
|
|
|
const handleInputChange = (field: keyof InteractiveQuestionData, value: any) => {
|
|
setQuestionData(prev => ({
|
|
...prev,
|
|
[field]: value
|
|
}));
|
|
};
|
|
|
|
const handleInteractiveConfigChange = (field: string, value: any) => {
|
|
setQuestionData(prev => ({
|
|
...prev,
|
|
interactiveConfig: {
|
|
...prev.interactiveConfig,
|
|
[field]: value
|
|
}
|
|
}));
|
|
};
|
|
|
|
const handleOptionChange = (optionId: string, field: keyof InteractiveQuestionOption, value: any) => {
|
|
setQuestionData(prev => ({
|
|
...prev,
|
|
options: prev.options?.map(option =>
|
|
option.id === optionId ? { ...option, [field]: value } : option
|
|
)
|
|
}));
|
|
};
|
|
|
|
const addOption = () => {
|
|
const newId = ((questionData.options?.length || 0) + 1).toString();
|
|
setQuestionData(prev => ({
|
|
...prev,
|
|
options: [...(prev.options || []), { id: newId, text: '', isCorrect: false }]
|
|
}));
|
|
};
|
|
|
|
const removeOption = (optionId: string) => {
|
|
if ((questionData.options?.length || 0) > 2) {
|
|
setQuestionData(prev => ({
|
|
...prev,
|
|
options: prev.options?.filter(option => option.id !== optionId)
|
|
}));
|
|
}
|
|
};
|
|
|
|
const addHint = () => {
|
|
const hints = questionData.interactiveConfig?.hints || [];
|
|
handleInteractiveConfigChange('hints', [...hints, '']);
|
|
};
|
|
|
|
const updateHint = (index: number, value: string) => {
|
|
const hints = questionData.interactiveConfig?.hints || [];
|
|
const newHints = [...hints];
|
|
newHints[index] = value;
|
|
handleInteractiveConfigChange('hints', newHints);
|
|
};
|
|
|
|
const removeHint = (index: number) => {
|
|
const hints = questionData.interactiveConfig?.hints || [];
|
|
handleInteractiveConfigChange('hints', hints.filter((_, i) => i !== index));
|
|
};
|
|
|
|
const handleMultimediaUpload = async (files: File[]): Promise<MultimediaFile[]> => {
|
|
// Simulate file upload - in real implementation, this would upload to server
|
|
const uploadedFiles: MultimediaFile[] = files.map(file => ({
|
|
id: Math.random().toString(36).substr(2, 9),
|
|
name: file.name,
|
|
type: file.type.startsWith('image/') ? 'image' :
|
|
file.type.startsWith('video/') ? 'video' :
|
|
file.type.startsWith('audio/') ? 'audio' : 'document',
|
|
url: URL.createObjectURL(file),
|
|
size: file.size,
|
|
uploadedAt: new Date()
|
|
}));
|
|
|
|
setQuestionData(prev => ({
|
|
...prev,
|
|
multimedia: [...(prev.multimedia || []), ...uploadedFiles]
|
|
}));
|
|
|
|
return uploadedFiles;
|
|
};
|
|
|
|
const handleMultimediaDelete = async (fileId: string) => {
|
|
setQuestionData(prev => ({
|
|
...prev,
|
|
multimedia: prev.multimedia?.filter(file => file.id !== fileId)
|
|
}));
|
|
};
|
|
|
|
const validateQuestion = (): string[] => {
|
|
const errors: string[] = [];
|
|
|
|
if (!questionData.title.trim()) {
|
|
errors.push('Judul soal harus diisi');
|
|
}
|
|
|
|
if (!questionData.category) {
|
|
errors.push('Kategori harus dipilih');
|
|
}
|
|
|
|
if (['multiple_choice', 'enhanced_mcq'].includes(questionData.type)) {
|
|
const hasCorrectAnswer = questionData.options?.some(option => option.isCorrect);
|
|
const hasEmptyOption = questionData.options?.some(option => !option.text.trim());
|
|
|
|
if (hasEmptyOption) {
|
|
errors.push('Semua pilihan jawaban harus diisi');
|
|
}
|
|
|
|
if (!hasCorrectAnswer) {
|
|
errors.push('Pilih jawaban yang benar');
|
|
}
|
|
}
|
|
|
|
if (questionData.type === 'essay' && !questionData.essayAnswer?.trim()) {
|
|
errors.push('Kunci jawaban essay harus diisi');
|
|
}
|
|
|
|
return errors;
|
|
};
|
|
|
|
const handleSave = () => {
|
|
const errors = validateQuestion();
|
|
if (errors.length > 0) {
|
|
alert('Terdapat kesalahan:\n' + errors.join('\n'));
|
|
return;
|
|
}
|
|
|
|
onSave(questionData);
|
|
};
|
|
|
|
const renderBasicTab = () => (
|
|
<div className="space-y-6">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Judul Soal *
|
|
</label>
|
|
<textarea
|
|
value={questionData.title}
|
|
onChange={(e) => handleInputChange('title', e.target.value)}
|
|
rows={3}
|
|
placeholder="Masukkan pertanyaan soal..."
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Tipe Soal *
|
|
</label>
|
|
<div className="space-y-2">
|
|
{questionTypes.map(type => {
|
|
const Icon = type.icon;
|
|
return (
|
|
<label key={type.value} className="flex items-center p-3 border rounded-lg cursor-pointer hover:bg-gray-50">
|
|
<input
|
|
type="radio"
|
|
name="questionType"
|
|
value={type.value}
|
|
checked={questionData.type === type.value}
|
|
onChange={(e) => handleInputChange('type', e.target.value)}
|
|
className="w-4 h-4 text-blue-600 focus:ring-blue-500"
|
|
/>
|
|
<Icon className="w-5 h-5 ml-3 mr-2 text-gray-600" />
|
|
<div>
|
|
<div className="font-medium text-gray-900">{type.label}</div>
|
|
<div className="text-sm text-gray-500">{type.description}</div>
|
|
</div>
|
|
</label>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Kategori *
|
|
</label>
|
|
<select
|
|
value={questionData.category}
|
|
onChange={(e) => handleInputChange('category', e.target.value)}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
required
|
|
>
|
|
<option value="">Pilih Kategori</option>
|
|
{categories.map(category => (
|
|
<option key={category} value={category}>{category}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Tingkat Kesulitan
|
|
</label>
|
|
<select
|
|
value={questionData.difficulty}
|
|
onChange={(e) => handleInputChange('difficulty', e.target.value)}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
>
|
|
<option value="easy">Mudah</option>
|
|
<option value="medium">Sedang</option>
|
|
<option value="hard">Sulit</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Poin
|
|
</label>
|
|
<input
|
|
type="number"
|
|
value={questionData.points}
|
|
onChange={(e) => handleInputChange('points', parseInt(e.target.value))}
|
|
min="1"
|
|
max="100"
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Status
|
|
</label>
|
|
<select
|
|
value={questionData.status}
|
|
onChange={(e) => handleInputChange('status', e.target.value)}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
>
|
|
<option value="draft">Draft</option>
|
|
<option value="active">Aktif</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const renderContentTab = () => (
|
|
<div className="space-y-6">
|
|
{/* Standard Multiple Choice */}
|
|
{['multiple_choice', 'enhanced_mcq'].includes(questionData.type) && (
|
|
<div>
|
|
<h3 className="text-lg font-medium text-gray-900 mb-4">Pilihan Jawaban</h3>
|
|
<div className="space-y-4">
|
|
{questionData.options?.map((option, index) => (
|
|
<div key={option.id} className="flex items-start space-x-3 p-4 border rounded-lg">
|
|
<input
|
|
type={questionData.interactiveConfig?.allowMultipleAnswers ? "checkbox" : "radio"}
|
|
name="correctAnswer"
|
|
checked={option.isCorrect}
|
|
onChange={() => {
|
|
if (questionData.interactiveConfig?.allowMultipleAnswers) {
|
|
handleOptionChange(option.id, 'isCorrect', !option.isCorrect);
|
|
} else {
|
|
// Single answer - uncheck others
|
|
setQuestionData(prev => ({
|
|
...prev,
|
|
options: prev.options?.map(opt => ({
|
|
...opt,
|
|
isCorrect: opt.id === option.id
|
|
}))
|
|
}));
|
|
}
|
|
}}
|
|
className="mt-1 w-4 h-4 text-blue-600 focus:ring-blue-500"
|
|
/>
|
|
<div className="flex-1 space-y-2">
|
|
<input
|
|
type="text"
|
|
value={option.text}
|
|
onChange={(e) => handleOptionChange(option.id, 'text', e.target.value)}
|
|
placeholder={`Pilihan ${String.fromCharCode(65 + index)}`}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
/>
|
|
{questionData.type === 'enhanced_mcq' && (
|
|
<input
|
|
type="text"
|
|
value={option.explanation || ''}
|
|
onChange={(e) => handleOptionChange(option.id, 'explanation', e.target.value)}
|
|
placeholder="Penjelasan untuk pilihan ini (opsional)"
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm"
|
|
/>
|
|
)}
|
|
</div>
|
|
{(questionData.options?.length || 0) > 2 && (
|
|
<button
|
|
type="button"
|
|
onClick={() => removeOption(option.id)}
|
|
className="text-red-600 hover:text-red-800 p-1"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
))}
|
|
|
|
<button
|
|
type="button"
|
|
onClick={addOption}
|
|
className="flex items-center gap-2 text-blue-600 hover:text-blue-800 text-sm font-medium"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
Tambah Pilihan
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Essay */}
|
|
{questionData.type === 'essay' && (
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Kunci Jawaban / Rubrik Penilaian
|
|
</label>
|
|
<textarea
|
|
value={questionData.essayAnswer || ''}
|
|
onChange={(e) => handleInputChange('essayAnswer', e.target.value)}
|
|
rows={5}
|
|
placeholder="Masukkan kunci jawaban atau rubrik penilaian..."
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* True/False */}
|
|
{['true_false', 'true_false_cepat'].includes(questionData.type) && (
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Jawaban yang Benar
|
|
</label>
|
|
<div className="flex space-x-4">
|
|
<label className="flex items-center">
|
|
<input
|
|
type="radio"
|
|
name="trueFalseAnswer"
|
|
checked={questionData.trueFalseAnswer === true}
|
|
onChange={() => handleInputChange('trueFalseAnswer', true)}
|
|
className="w-4 h-4 text-blue-600 focus:ring-blue-500"
|
|
/>
|
|
<span className="ml-2">Benar</span>
|
|
</label>
|
|
<label className="flex items-center">
|
|
<input
|
|
type="radio"
|
|
name="trueFalseAnswer"
|
|
checked={questionData.trueFalseAnswer === false}
|
|
onChange={() => handleInputChange('trueFalseAnswer', false)}
|
|
className="w-4 h-4 text-blue-600 focus:ring-blue-500"
|
|
/>
|
|
<span className="ml-2">Salah</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Explanation */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Penjelasan (Opsional)
|
|
</label>
|
|
<textarea
|
|
value={questionData.explanation || ''}
|
|
onChange={(e) => handleInputChange('explanation', e.target.value)}
|
|
rows={4}
|
|
placeholder="Berikan penjelasan untuk jawaban yang benar..."
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const renderInteractiveTab = () => (
|
|
<div className="space-y-6">
|
|
{/* Enhanced MCQ Features */}
|
|
{questionData.type === 'enhanced_mcq' && (
|
|
<div className="space-y-4">
|
|
<h3 className="text-lg font-medium text-gray-900">Fitur Enhanced MCQ</h3>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<label className="flex items-center">
|
|
<input
|
|
type="checkbox"
|
|
checked={questionData.interactiveConfig?.allowMultipleAnswers || false}
|
|
onChange={(e) => handleInteractiveConfigChange('allowMultipleAnswers', e.target.checked)}
|
|
className="w-4 h-4 text-blue-600 focus:ring-blue-500"
|
|
/>
|
|
<span className="ml-2">Izinkan Multiple Answers</span>
|
|
</label>
|
|
|
|
<label className="flex items-center">
|
|
<input
|
|
type="checkbox"
|
|
checked={questionData.interactiveConfig?.enableHints || false}
|
|
onChange={(e) => handleInteractiveConfigChange('enableHints', e.target.checked)}
|
|
className="w-4 h-4 text-blue-600 focus:ring-blue-500"
|
|
/>
|
|
<span className="ml-2">Aktifkan Hints</span>
|
|
</label>
|
|
</div>
|
|
|
|
{/* Hints Management */}
|
|
{questionData.interactiveConfig?.enableHints && (
|
|
<div>
|
|
<h4 className="font-medium text-gray-900 mb-2">Hints</h4>
|
|
<div className="space-y-2">
|
|
{questionData.interactiveConfig.hints?.map((hint, index) => (
|
|
<div key={index} className="flex items-center gap-2">
|
|
<input
|
|
type="text"
|
|
value={hint}
|
|
onChange={(e) => updateHint(index, e.target.value)}
|
|
placeholder={`Hint ${index + 1}`}
|
|
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => removeHint(index)}
|
|
className="text-red-600 hover:text-red-800 p-1"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
))}
|
|
<button
|
|
type="button"
|
|
onClick={addHint}
|
|
className="flex items-center gap-2 text-blue-600 hover:text-blue-800 text-sm font-medium"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
Tambah Hint
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Timing and Behavior */}
|
|
<div className="space-y-4">
|
|
<h3 className="text-lg font-medium text-gray-900">Pengaturan Waktu & Perilaku</h3>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Batas Waktu (detik, 0 = tidak terbatas)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
value={questionData.interactiveConfig?.timeLimit || 0}
|
|
onChange={(e) => handleInteractiveConfigChange('timeLimit', parseInt(e.target.value))}
|
|
min="0"
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<label className="flex items-center">
|
|
<input
|
|
type="checkbox"
|
|
checked={questionData.interactiveConfig?.showTimer || false}
|
|
onChange={(e) => handleInteractiveConfigChange('showTimer', e.target.checked)}
|
|
className="w-4 h-4 text-blue-600 focus:ring-blue-500"
|
|
/>
|
|
<span className="ml-2">Tampilkan Timer</span>
|
|
</label>
|
|
|
|
<label className="flex items-center">
|
|
<input
|
|
type="checkbox"
|
|
checked={questionData.interactiveConfig?.trackBehavior || false}
|
|
onChange={(e) => handleInteractiveConfigChange('trackBehavior', e.target.checked)}
|
|
className="w-4 h-4 text-blue-600 focus:ring-blue-500"
|
|
/>
|
|
<span className="ml-2">Lacak Perilaku</span>
|
|
</label>
|
|
|
|
<label className="flex items-center">
|
|
<input
|
|
type="checkbox"
|
|
checked={questionData.interactiveConfig?.allowReview || false}
|
|
onChange={(e) => handleInteractiveConfigChange('allowReview', e.target.checked)}
|
|
className="w-4 h-4 text-blue-600 focus:ring-blue-500"
|
|
/>
|
|
<span className="ml-2">Izinkan Review</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Interactive Type Specific Settings */}
|
|
{questionData.type === 'true_false_cepat' && (
|
|
<div className="p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
|
|
<h4 className="font-medium text-yellow-800 mb-2">Pengaturan True/False Cepat</h4>
|
|
<p className="text-sm text-yellow-700">
|
|
Soal ini akan melacak waktu reaksi siswa dan memberikan feedback berdasarkan kecepatan jawaban.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{questionData.type === 'mini_survey_reflection' && (
|
|
<div className="space-y-4">
|
|
<h4 className="font-medium text-gray-900">Pengaturan Survey Refleksi</h4>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Skala Emoji Min
|
|
</label>
|
|
<input
|
|
type="number"
|
|
value={questionData.interactiveConfig?.reflectionConfig?.emojiScale?.min || 1}
|
|
onChange={(e) => handleInteractiveConfigChange('reflectionConfig', {
|
|
...questionData.interactiveConfig?.reflectionConfig,
|
|
emojiScale: {
|
|
...questionData.interactiveConfig?.reflectionConfig?.emojiScale,
|
|
min: parseInt(e.target.value)
|
|
}
|
|
})}
|
|
min="1"
|
|
max="5"
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Skala Emoji Max
|
|
</label>
|
|
<input
|
|
type="number"
|
|
value={questionData.interactiveConfig?.reflectionConfig?.emojiScale?.max || 4}
|
|
onChange={(e) => handleInteractiveConfigChange('reflectionConfig', {
|
|
...questionData.interactiveConfig?.reflectionConfig,
|
|
emojiScale: {
|
|
...questionData.interactiveConfig?.reflectionConfig?.emojiScale,
|
|
max: parseInt(e.target.value)
|
|
}
|
|
})}
|
|
min="2"
|
|
max="10"
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
const renderMultimediaTab = () => (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-lg font-medium text-gray-900">Multimedia Files</h3>
|
|
<button
|
|
onClick={() => setShowMultimediaManager(!showMultimediaManager)}
|
|
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
|
>
|
|
<Upload className="w-4 h-4" />
|
|
{showMultimediaManager ? 'Tutup' : 'Kelola'} Multimedia
|
|
</button>
|
|
</div>
|
|
|
|
{showMultimediaManager && (
|
|
<MultimediaManager
|
|
files={questionData.multimedia || []}
|
|
onFileUpload={handleMultimediaUpload}
|
|
onFileDelete={handleMultimediaDelete}
|
|
className="border-2 border-dashed border-gray-300 rounded-lg"
|
|
/>
|
|
)}
|
|
|
|
{/* Display current multimedia files */}
|
|
{(questionData.multimedia?.length || 0) > 0 && (
|
|
<div>
|
|
<h4 className="font-medium text-gray-900 mb-3">File Terlampir ({questionData.multimedia?.length})</h4>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
{questionData.multimedia?.map(file => {
|
|
const Icon = file.type === 'image' ? Image :
|
|
file.type === 'video' ? Video :
|
|
file.type === 'audio' ? Music : File;
|
|
return (
|
|
<div key={file.id} className="border rounded-lg p-3">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<Icon className="w-4 h-4 text-gray-600" />
|
|
<span className="text-sm font-medium truncate">{file.name}</span>
|
|
</div>
|
|
{file.type === 'image' && (
|
|
<img src={file.url} alt={file.name} className="w-full h-20 object-cover rounded" />
|
|
)}
|
|
<div className="text-xs text-gray-500 mt-1">
|
|
{(file.size / 1024).toFixed(1)} KB
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
const renderPreviewTab = () => (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-lg font-medium text-gray-900">Preview Soal</h3>
|
|
<button
|
|
onClick={() => setPreviewMode(!previewMode)}
|
|
className="flex items-center gap-2 px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors"
|
|
>
|
|
<Eye className="w-4 h-4" />
|
|
{previewMode ? 'Edit Mode' : 'Preview Mode'}
|
|
</button>
|
|
</div>
|
|
|
|
<div className="border-2 border-gray-200 rounded-lg p-6 bg-gray-50">
|
|
<div className="bg-white rounded-lg p-6 shadow-sm">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div className="flex items-center gap-2">
|
|
<span className="px-2 py-1 bg-blue-100 text-blue-800 text-xs font-medium rounded">
|
|
{questionTypes.find(t => t.value === questionData.type)?.label}
|
|
</span>
|
|
<span className="px-2 py-1 bg-gray-100 text-gray-800 text-xs font-medium rounded">
|
|
{questionData.difficulty === 'easy' ? 'Mudah' :
|
|
questionData.difficulty === 'medium' ? 'Sedang' : 'Sulit'}
|
|
</span>
|
|
<span className="px-2 py-1 bg-green-100 text-green-800 text-xs font-medium rounded">
|
|
{questionData.points} poin
|
|
</span>
|
|
</div>
|
|
{questionData.interactiveConfig?.timeLimit && questionData.interactiveConfig.timeLimit > 0 && (
|
|
<div className="flex items-center gap-1 text-sm text-gray-600">
|
|
<Timer className="w-4 h-4" />
|
|
{questionData.interactiveConfig.timeLimit}s
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<h4 className="text-lg font-medium text-gray-900 mb-4">
|
|
{questionData.title || 'Judul soal akan muncul di sini...'}
|
|
</h4>
|
|
|
|
{/* Preview based on question type */}
|
|
{['multiple_choice', 'enhanced_mcq'].includes(questionData.type) && (
|
|
<div className="space-y-3">
|
|
{questionData.options?.map((option, index) => (
|
|
<label key={option.id} className="flex items-center p-3 border rounded-lg cursor-pointer hover:bg-gray-50">
|
|
<input
|
|
type={questionData.interactiveConfig?.allowMultipleAnswers ? "checkbox" : "radio"}
|
|
name="preview"
|
|
className="w-4 h-4 text-blue-600 focus:ring-blue-500"
|
|
disabled
|
|
/>
|
|
<span className="ml-3">{option.text || `Pilihan ${String.fromCharCode(65 + index)}`}</span>
|
|
{option.isCorrect && (
|
|
<CheckCircle className="w-4 h-4 text-green-600 ml-auto" />
|
|
)}
|
|
</label>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{questionData.type === 'essay' && (
|
|
<textarea
|
|
placeholder="Area untuk jawaban essay siswa..."
|
|
rows={5}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
|
|
disabled
|
|
/>
|
|
)}
|
|
|
|
{['true_false', 'true_false_cepat'].includes(questionData.type) && (
|
|
<div className="flex gap-4">
|
|
<label className="flex items-center p-3 border rounded-lg cursor-pointer hover:bg-gray-50">
|
|
<input type="radio" name="preview" className="w-4 h-4 text-blue-600" disabled />
|
|
<span className="ml-3">Benar</span>
|
|
{questionData.trueFalseAnswer === true && (
|
|
<CheckCircle className="w-4 h-4 text-green-600 ml-auto" />
|
|
)}
|
|
</label>
|
|
<label className="flex items-center p-3 border rounded-lg cursor-pointer hover:bg-gray-50">
|
|
<input type="radio" name="preview" className="w-4 h-4 text-blue-600" disabled />
|
|
<span className="ml-3">Salah</span>
|
|
{questionData.trueFalseAnswer === false && (
|
|
<CheckCircle className="w-4 h-4 text-green-600 ml-auto" />
|
|
)}
|
|
</label>
|
|
</div>
|
|
)}
|
|
|
|
{questionData.explanation && (
|
|
<div className="mt-4 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
|
|
<p className="text-sm font-medium text-yellow-800 mb-1">Penjelasan:</p>
|
|
<p className="text-sm text-yellow-700">{questionData.explanation}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Interactive features preview */}
|
|
{questionData.interactiveConfig?.enableHints && questionData.interactiveConfig.hints && questionData.interactiveConfig.hints.length > 0 && (
|
|
<div className="mt-4">
|
|
<button className="flex items-center gap-2 text-blue-600 hover:text-blue-800 text-sm">
|
|
<HelpCircle className="w-4 h-4" />
|
|
Lihat Hint ({questionData.interactiveConfig.hints.length})
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const tabs = [
|
|
{ id: 'basic', label: 'Informasi Dasar', icon: Settings },
|
|
{ id: 'content', label: 'Konten Soal', icon: File },
|
|
{ id: 'interactive', label: 'Fitur Interaktif', icon: Zap },
|
|
{ id: 'multimedia', label: 'Multimedia', icon: Image },
|
|
{ id: 'preview', label: 'Preview', icon: Eye }
|
|
];
|
|
|
|
return (
|
|
<div className={`bg-white rounded-lg shadow-sm border ${className}`}>
|
|
{/* Header */}
|
|
<div className="p-6 border-b">
|
|
<h2 className="text-xl font-semibold text-gray-900">Enhanced Question Builder</h2>
|
|
<p className="text-gray-600 mt-1">Buat soal interaktif dengan fitur multimedia dan konfigurasi lanjutan</p>
|
|
</div>
|
|
|
|
{/* Tabs */}
|
|
<div className="border-b">
|
|
<nav className="flex space-x-8 px-6">
|
|
{tabs.map(tab => {
|
|
const Icon = tab.icon;
|
|
return (
|
|
<button
|
|
key={tab.id}
|
|
onClick={() => setActiveTab(tab.id as any)}
|
|
className={`flex items-center gap-2 py-4 px-1 border-b-2 font-medium text-sm transition-colors ${
|
|
activeTab === tab.id
|
|
? 'border-blue-500 text-blue-600'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
|
}`}
|
|
>
|
|
<Icon className="w-4 h-4" />
|
|
{tab.label}
|
|
</button>
|
|
);
|
|
})}
|
|
</nav>
|
|
</div>
|
|
|
|
{/* Tab Content */}
|
|
<div className="p-6">
|
|
{activeTab === 'basic' && renderBasicTab()}
|
|
{activeTab === 'content' && renderContentTab()}
|
|
{activeTab === 'interactive' && renderInteractiveTab()}
|
|
{activeTab === 'multimedia' && renderMultimediaTab()}
|
|
{activeTab === 'preview' && renderPreviewTab()}
|
|
</div>
|
|
|
|
{/* Footer Actions */}
|
|
<div className="p-6 border-t bg-gray-50 flex justify-end space-x-4">
|
|
<button
|
|
onClick={onCancel}
|
|
className="px-6 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors"
|
|
>
|
|
Batal
|
|
</button>
|
|
<button
|
|
onClick={handleSave}
|
|
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
|
>
|
|
Simpan Soal
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}; |