import React, { useState, useEffect, useRef } from 'react';
import {
BookOpen, Target, Layers, FileText, List, Loader2, Printer,
CheckCircle2, AlertCircle, Copy, Settings, GraduationCap,
BrainCircuit, LayoutDashboard, Plus, Trash2, Edit3, Save,
RefreshCw, Download, ChevronRight, FileCheck, CheckSquare,
AlignLeft, Eye, History, Library, Menu, X, FileQuestion, Sparkles
} from 'lucide-react';
export default function App() {
// Navigation & Layout State
const [currentView, setCurrentView] = useState('home'); // home, form, result, history, bank
const [sidebarOpen, setSidebarOpen] = useState(false);
// Storage State (Local Storage)
const [history, setHistory] = useState([]);
const [bank, setBank] = useState([]);
const [apiKey, setApiKey] = useState('');
const [showApiModal, setShowApiModal] = useState(false);
// Form State
const [formData, setFormData] = useState({
jenjang: 'SMK',
kelas: 'X',
mapel: 'Kimia',
materi: 'Reaksi Redoks',
tp: 'Peserta didik mampu menjelaskan konsep oksidasi dan reduksi, menentukan bilangan oksidasi, serta mengidentifikasi zat yang mengalami oksidasi dan reduksi.',
jenis: 'Sumatif',
qTypes: { pg: 5, uraian: 2, pGKompleks: 0, benarSalah: 0, menjodohkan: 0, isian: 0, kasus: 0 },
kognitif: ['AUTO'],
kesulitan: 'Otomatis',
karakter: ['Kontekstual', 'HOTS', 'Konteks dunia industri'],
visual: 'Modern Edukatif'
});
// Generation State
const [loading, setLoading] = useState(false);
const [loadingStep, setLoadingStep] = useState('');
const [error, setError] = useState('');
const [assessment, setAssessment] = useState(null);
const [activeTab, setActiveTab] = useState('SOAL'); // SOAL, KISI, KUNCI, RUBRIK
const printRef = useRef(null);
// Load from local storage on mount
useEffect(() => {
const savedHistory = localStorage.getItem('trigps_history_v2');
const savedBank = localStorage.getItem('trigps_bank_v2');
const savedApiKey = localStorage.getItem('trigps_gemini_key_v2');
if (savedHistory) setHistory(JSON.parse(savedHistory));
if (savedBank) setBank(JSON.parse(savedBank));
if (savedApiKey) setApiKey(savedApiKey);
}, []);
const saveApiKey = (key) => {
setApiKey(key);
localStorage.setItem('trigps_gemini_key_v2', key);
setShowApiModal(false);
};
const totalQuestions = Object.values(formData.qTypes).reduce((a, b) => a + (parseInt(b) || 0), 0);
const handleInputChange = (field, value) => setFormData(prev => ({ ...prev, [field]: value }));
const handleQTypeChange = (type, value) => {
setFormData(prev => ({ ...prev, qTypes: { ...prev.qTypes, [type]: parseInt(value) || 0 } }));
};
const toggleArrayItem = (field, item) => {
setFormData(prev => {
const arr = prev[field];
if (arr.includes(item)) return { ...prev, [field]: arr.filter(i => i !== item) };
if (field === 'kognitif' && item === 'AUTO') return { ...prev, [field]: ['AUTO'] };
if (field === 'kognitif' && item !== 'AUTO') return { ...prev, [field]: arr.filter(i => i !== 'AUTO').concat(item) };
return { ...prev, [field]: [...arr, item] };
});
};
const loadContohKimia = () => {
setFormData({
jenjang: 'SMK',
kelas: 'X',
mapel: 'Kimia',
materi: 'Reaksi Redoks',
tp: 'Peserta didik mampu menjelaskan konsep oksidasi dan reduksi, menentukan bilangan oksidasi, serta mengidentifikasi zat yang mengalami oksidasi dan reduksi.',
jenis: 'Sumatif',
qTypes: { pg: 10, uraian: 2, pGKompleks: 0, benarSalah: 0, menjodohkan: 0, isian: 0, kasus: 0 },
kognitif: ['AUTO'],
kesulitan: 'Otomatis',
karakter: ['Kontekstual', 'HOTS', 'Konteks dunia industri'],
visual: 'Modern Edukatif'
});
};
const generateAssessment = async (e) => {
if (e) e.preventDefault();
if (!apiKey) {
setShowApiModal(true);
return;
}
if (totalQuestions === 0) {
setError("Silakan tentukan minimal 1 jumlah soal.");
return;
}
if (!formData.tp.trim() || !formData.materi.trim()) {
setError("Materi dan Tujuan Pembelajaran wajib diisi.");
return;
}
setLoading(true);
setError('');
setCurrentView('loading');
const steps = [
"Menganalisis tujuan pembelajaran...",
"Menyelaraskan dengan level kognitif...",
"Menyusun kisi-kisi dan indikator...",
"Membangun butir soal dan distraktor...",
"Membuat kunci jawaban & rubrik penilaian...",
"Memfinalisasi desain dokumen..."
];
let currentStep = 0;
const stepInterval = setInterval(() => {
setLoadingStep(steps[currentStep]);
currentStep = Math.min(currentStep + 1, steps.length - 1);
}, 1500);
const promptText = `
Buatlah instrumen asesmen pembelajaran tingkat ${formData.jenjang} (Kelas ${formData.kelas}) untuk mata pelajaran ${formData.mapel}.
Materi/Topik: ${formData.materi}
Tujuan Pembelajaran (TP): ${formData.tp}
Jenis Asesmen: ${formData.jenis}
Struktur Soal (Total ${totalQuestions}):
${formData.qTypes.pg > 0 ? `- Pilihan Ganda: ${formData.qTypes.pg} soal (A, B, C, D${formData.jenjang === 'SMA' || formData.jenjang === 'SMK' ? ', E' : ''})` : ''}
${formData.qTypes.uraian > 0 ? `- Uraian: ${formData.qTypes.uraian} soal` : ''}
Pengaturan: Level Kognitif: ${formData.kognitif.join(', ')}. Kesulitan: ${formData.kesulitan}.
Karakteristik: ${formData.karakter.join(', ')}.
PENTING: Soal HARUS linier dengan TP. Hindari jawaban ganda yang benar pada Pilihan Ganda.
`;
const systemPrompt = `Anda adalah "AI Assessment Generator" karya Pak TriGPS. Hasilkan output murni dalam format JSON.
Format JSON WAJIB:
{
"title": "Judul Asesmen",
"validation": { "status": "SIAP DIGUNAKAN", "notes": ["catatan 1"] },
"questions": [
{
"id": 1,
"type": "Pilihan Ganda" (atau "Uraian"),
"cognitiveLevel": "C1/C2/C3/C4/C5/C6",
"difficulty": "Mudah/Sedang/Sulit",
"indicator": "Indikator soal...",
"text": "Pertanyaan lengkap...",
"options": ["A. opsi1", "B. opsi2", "C. opsi3", "D. opsi4", "E. opsi5"],
"answer": "Kunci jawaban benar (contoh: C)",
"rubric": "Rubrik skor (Hanya untuk uraian)"
}
]
}`;
try {
// Using gemini-3-flash-preview as instructed for text generation
const response = await fetch(
`https://generativelanguage.googleapis.com/v1beta/models/gemini-3-flash-preview:generateContent?key=${apiKey}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{ parts: [{ text: promptText }] }],
systemInstruction: { parts: [{ text: systemPrompt }] },
generationConfig: { responseMimeType: "application/json" }
})
}
);
if (!response.ok) {
const errData = await response.json();
throw new Error(errData.error?.message || "Gagal terhubung ke AI API.");
}
const result = await response.json();
const generatedText = result.candidates?.[0]?.content?.parts?.[0]?.text;
if (!generatedText) throw new Error("Respon kosong dari AI.");
// FIX: Safe JSON cleaning without regex literals
let cleanJSON = generatedText;
const backticks = String.fromCharCode(96, 96, 96); // ```
const prefix = backticks + "json\n";
// Remove markdown ```json prefix if present
if (cleanJSON.startsWith(prefix)) {
cleanJSON = cleanJSON.substring(prefix.length);
}
// Remove trailing ``` if present
if (cleanJSON.endsWith(backticks + "\n")) {
cleanJSON = cleanJSON.substring(0, cleanJSON.length - 4);
} else if (cleanJSON.endsWith(backticks)) {
cleanJSON = cleanJSON.substring(0, cleanJSON.length - 3);
}
cleanJSON = cleanJSON.trim();
const parsedData = JSON.parse(cleanJSON);
const newHistoryItem = {
id: Date.now(), date: new Date().toLocaleDateString('id-ID'),
...formData, assessment: parsedData
};
const updatedHistory = [newHistoryItem, ...history];
setHistory(updatedHistory);
localStorage.setItem('trigps_history_v2', JSON.stringify(updatedHistory));
clearInterval(stepInterval);
setAssessment(parsedData);
setLoading(false);
setCurrentView('result');
setActiveTab('SOAL');
} catch (err) {
clearInterval(stepInterval);
setLoading(false);
setError("Kesalahan Generate: " + err.message);
setCurrentView('form');
}
};
const saveToBank = () => {
if (!assessment) return;
const isAlreadySaved = bank.some(item => item.id === assessment.id);
if (!isAlreadySaved) {
const bankItem = {
id: Date.now(), // Generate new ID for bank
date: new Date().toLocaleDateString('id-ID'),
title: assessment.title,
mapel: formData.mapel,
kelas: formData.kelas,
assessment: assessment
};
const newBank = [bankItem, ...bank];
setBank(newBank);
localStorage.setItem('trigps_bank_v2', JSON.stringify(newBank));
alert("Asesmen berhasil disimpan ke Bank Soal!");
} else {
alert("Asesmen ini sudah ada di Bank Soal.");
}
};
const deleteFromHistory = (id) => {
const updated = history.filter(item => item.id !== id);
setHistory(updated);
localStorage.setItem('trigps_history_v2', JSON.stringify(updated));
};
const deleteFromBank = (id) => {
const updated = bank.filter(item => item.id !== id);
setBank(updated);
localStorage.setItem('trigps_bank_v2', JSON.stringify(updated));
};
const handlePrint = () => window.print();
const navigateTo = (view) => {
setCurrentView(view);
setSidebarOpen(false);
};
const Sidebar = () => (
<>
{/* Mobile Overlay */}
{sidebarOpen && (
setSidebarOpen(false)} />
)}
{/* Sidebar Content */}
Menu Utama
{[
{ id: 'home', icon: LayoutDashboard, label: 'Beranda' },
{ id: 'form', icon: Plus, label: 'Buat Asesmen Baru' },
{ id: 'bank', icon: Library, label: 'Bank Soal', badge: bank.length },
{ id: 'history', icon: History, label: 'Riwayat Asesmen' },
].map(item => (
navigateTo(item.id)}
className={`w-full flex items-center justify-between px-3 py-3 rounded-xl transition-all ${currentView === item.id ? 'bg-teal-600 text-white shadow-md' : 'text-slate-300 hover:bg-slate-800 hover:text-white'}`}
>
{item.label}
{item.badge > 0 && {item.badge} }
))}
setShowApiModal(true)} className="w-full flex items-center space-x-3 px-3 py-3 text-slate-300 hover:bg-slate-800 rounded-xl transition-all">
Pengaturan API Key
>
);
const ApiKeyModal = () => (
);
const HomeView = () => (
{/* Hero Banner */}
GENERATOR ASESMEN AI V2
Ubah Tujuan Pembelajaran Menjadi Asesmen Bermakna.
Solusi cerdas untuk guru profesional. Hasilkan instrumen asesmen yang valid, reliabel, dan siap cetak dalam hitungan detik.
navigateTo('form')} className="bg-white text-teal-900 hover:bg-teal-50 px-8 py-4 rounded-full font-bold text-lg shadow-lg transition-transform hover:-translate-y-1 flex items-center">
Buat Asesmen Baru
{/* Stats Cards */}
Total Generate
{history.length}
Soal di Bank
{bank.length}
Status API
{apiKey ? 'Terhubung' : 'Belum Diatur'}
);
const FormView = () => (
Konfigurasi Asesmen
Lengkapi parameter untuk meracik instrumen evaluasi yang presisi.
🧪 Load Data Contoh
{error && (
)}
);
const LoadingView = () => (
AI Sedang Meracik Soal
{loadingStep}
);
const ResultView = () => {
if (!assessment) return null;
const { title, validation, questions } = assessment;
return (
{/* Toolbar Top */}
{['SOAL', 'KISI-KISI', 'KUNCI JAWABAN', 'RUBRIK'].map(tab => (
setActiveTab(tab)}
className={`px-5 py-2.5 text-sm font-bold rounded-xl transition-all whitespace-nowrap ${activeTab === tab ? 'bg-white text-teal-700 shadow-sm' : 'text-slate-500 hover:text-slate-700 hover:bg-slate-200/50'}`}
>
{tab}
))}
Simpan ke Bank
Cetak (A4)
{/* Status Validation */}
{validation?.status === 'SIAP DIGUNAKAN' ?
:
}
{validation?.status || 'Validasi Selesai'}
{validation?.notes?.map((n, i) => • {n} ) || • Sesuai dengan Tujuan Pembelajaran dan Materi. }
{/* Printable Paper Canvas */}
{activeTab === 'SOAL' && (
{/* Kop Surat Resmi */}
DINAS PENDIDIKAN DAN KEBUDAYAAN
{formData.jenjang} NEGERI CONTOH
Jalan Pendidikan No. 123, Kota Cerdas, Provinsi Maju 12345
Website: www.sekolahcontoh.sch.id | Email: info@sekolahcontoh.sch.id
{/* Judul & Meta */}
LEMBAR SOAL ASESMEN {formData.jenis}
Mata Pelajaran : {formData.mapel}
Nama Siswa : .......................................
Kelas/Semester : {formData.kelas} / Genap
Nomor Absen : .......................................
Hari/Tanggal : .......................................
Nilai Akhir :
{/* Petunjuk Khusus */}
PETUNJUK UMUM:
Berdoalah sebelum mengerjakan soal.
Tuliskan nama dan nomor absen pada lembar jawaban yang tersedia.
Bacalah dengan teliti setiap soal sebelum menjawab.
Pilihlah satu jawaban yang paling benar dengan menyilang (X) pada huruf A, B, C, D, atau E untuk soal pilihan ganda.
Jawablah dengan singkat dan jelas untuk soal uraian.
{/* Konten Soal */}
{questions?.map((q, idx) => (
{idx + 1}.
{q.text}
{q.options && q.options.length > 0 && (
{q.options.map((opt, oIdx) => (
{opt}
))}
)}
{q.type.toLowerCase().includes('uraian') && (
)}
))}
*** Selamat Bekerja ***
)}
{activeTab === 'KISI-KISI' && (
KISI-KISI PENULISAN SOAL
Mata Pelajaran : {formData.mapel}
Kelas / Jenjang: {formData.kelas} / {formData.jenjang}
Jenis Asesmen : {formData.jenis}
Jumlah Soal : {questions.length} Butir
Tujuan Pembelajaran:
{formData.tp}
No
Materi Pokok
Indikator Soal
Level Kognitif
Bentuk Soal
Tingkat Kesulitan
{questions?.map((q, idx) => (
{idx + 1}
{formData.materi}
{q.indicator || "-"}
{q.cognitiveLevel}
{q.type}
{q.difficulty}
))}
)}
{activeTab === 'KUNCI JAWABAN' && (
KUNCI JAWABAN
{questions?.map((q, idx) => (
Soal No. {idx + 1}
{q.type}
{q.answer || "Uraian"}
))}
)}
{activeTab === 'RUBRIK' && (
PEDOMAN PENSKORAN & RUBRIK
{questions?.filter(q => q.rubric).length === 0 && (
Asesmen ini hanya berisi soal pilihan ganda. Rubrik otomatis menggunakan skor 1 untuk jawaban benar dan 0 untuk salah.
)}
{questions?.filter(q => q.rubric).map((q, idx) => (
Soal No. {q.id || idx + 1} (Uraian)
{q.rubric}
))}
)}
);
};
const BankView = () => (
Bank Soal
Koleksi instrumen asesmen yang telah Anda simpan.
{bank.length === 0 ? (
Bank Soal Kosong
Anda belum menyimpan asesmen apapun ke dalam bank soal.
) : (
{bank.map(item => (
{item.date}
deleteFromBank(item.id)} className="text-slate-400 hover:text-rose-500 transition-colors">
{item.title}
{item.mapel} — Kelas {item.kelas}
{ setAssessment(item.assessment); navigateTo('result'); }} className="w-full py-2.5 bg-emerald-50 hover:bg-emerald-100 text-emerald-700 rounded-xl font-bold text-sm transition-colors flex justify-center items-center">
Buka Asesmen
))}
)}
);
const HistoryView = () => (
Riwayat Generate
Log semua asesmen yang pernah dibuat menggunakan AI.
{history.length === 0 ? (
Riwayat Kosong
Buat asesmen baru untuk melihat riwayat di sini.
) : (
Tanggal
Mata Pelajaran
Materi
Aksi
{history.map(item => (
{item.date}
{item.mapel} Kls {item.kelas}
{item.materi}
{ setAssessment(item.assessment); navigateTo('result'); }} className="p-2 bg-blue-50 text-blue-600 hover:bg-blue-100 rounded-lg transition-colors" title="Buka">
deleteFromHistory(item.id)} className="p-2 bg-rose-50 text-rose-600 hover:bg-rose-100 rounded-lg transition-colors" title="Hapus">
))}
)}
);
return (
{/* Sidebar for Desktop / Hidden on mobile unless toggled */}
{/* API Key Modal */}
{showApiModal &&
}
{/* Main Content Area */}
{/* Mobile Header (Top Navbar) */}
ASESMEN AI
setSidebarOpen(true)} className="p-2 hover:bg-slate-800 rounded-lg">
{/* Dynamic Content Views */}
{currentView === 'home' && }
{currentView === 'form' && }
{currentView === 'loading' && }
{currentView === 'result' && }
{currentView === 'history' && }
{currentView === 'bank' && }
{/* Global Footer */}
{currentView !== 'loading' && currentView !== 'result' && (
Asesmen Karya Pak TriGPS • AI Assessment Generator V2
)}
);
}