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 */} ); const ApiKeyModal = () => (

Konfigurasi API Key

Masukkan Google Gemini API Key Anda. Key disimpan secara aman di browser lokal Anda (Local Storage) dan tidak dikirim ke server lain.

); 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.

{/* 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.

{error && (

{error}

)}
{/* CARD 1: Identitas */}

1. Identitas Pembelajaran

handleInputChange('mapel', e.target.value)} required/>
handleInputChange('materi', e.target.value)} required/>