import React, { useState, useEffect } from 'react'; import { Download, Plus, Trash2, Send, FileSpreadsheet, ClipboardCheck, BrainCircuit, User, School, GraduationCap, Gauge, BookOpen, Image as ImageIcon, Sparkles, LayoutList, CheckSquare, AlignJustify, ToggleLeft, FileText, Bookmark, Printer, FileText as FileTextIcon, Bot, Target, AlertTriangle, ThumbsUp, Edit2, LayoutGrid, ListOrdered } from 'lucide-react'; // --- HELPER UNTUK FETCH API DENGAN EXPONENTIAL BACKOFF --- const fetchWithRetry = async (url, options, retries = 5) => { let delay = 1000; for (let i = 0; i < retries; i++) { try { const response = await fetch(url, options); if (response.ok) { return response; } // Jika ini adalah percobaan terakhir, lempar error sesuai status HTTP if (i === retries - 1) { if (response.status === 401 || response.status === 403) { throw new Error(`Autentikasi API ditolak (Status: ${response.status}). Pastikan API Key Anda dikonfigurasi dengan benar.`); } throw new Error(`Server AI sibuk atau menolak permintaan (Status: ${response.status}).`); } } catch (error) { if (i === retries - 1) throw error; } // Tunggu dengan jeda yang meningkat eksponensial (1s, 2s, 4s, 8s, 16s) await new Promise(res => setTimeout(res, delay)); delay *= 2; } }; // --- HELPER UNTUK MERENDER GAMBAR & TEKS ARAB (RTL) --- const renderText = (text) => { if (!text || typeof text !== 'string') return text; // Pisahkan string berdasarkan tag [GAMBAR:...] dan [ARABIC]...[/ARABIC] const parts = text.split(/(\[GAMBAR:.*?\]|\[ARABIC\][\s\S]*?\[\/ARABIC\])/g); return parts.map((part, index) => { if (part.startsWith('[GAMBAR:')) { return (

{part}

(Ganti kotak ini dengan gambar yang sesuai di MS Word)

); } else if (part.startsWith('[ARABIC]')) { const arabicText = part.replace('[ARABIC]', '').replace('[/ARABIC]', ''); return (
{arabicText}
); } else { // Render teks biasa, dukung enter (\n) return {part.split('\n').map((line, i) => {line}{i !== part.split('\n').length - 1 &&
}
)}
; } }); }; // --- HELPER FORMAT EXPORT WORD & PDF --- const processExportText = (t) => { if (!t) return ''; let str = String(t).replace(/\n/g, '
'); str = str.replace(/\[GAMBAR:(.*?)\]/g, '
[Tempat Gambar: $1]
'); str = str.replace(/\[ARABIC\]([\s\S]*?)\[\/ARABIC\]/g, '
$1
'); return str; }; // --- HELPER FORMAT KUNCI JAWABAN (ARRAY/OBJECT KE TEKS) --- const formatKunciJawaban = (kunci, separator = ', ') => { if (kunci === null || kunci === undefined) return '-'; if (typeof kunci === 'string' || typeof kunci === 'number') return String(kunci); // Jika AI merespons Kunci berupa Array (cth: soal Benar-Salah) if (Array.isArray(kunci)) { return kunci.map((item, index) => { if (typeof item === 'object') return `${index + 1}. ${item.jawaban || item.kunci || JSON.stringify(item)}`; return `${index + 1}. ${item}`; }).join(separator); } // Jika AI merespons Kunci berupa Object (cth: Menjodohkan / Kategori) if (typeof kunci === 'object') { return Object.entries(kunci).map(([k, v]) => `${k}: ${v}`).join(separator); } return String(kunci); }; // --- KOMPONEN RENDER SOAL (WEB PREVIEW) --- const RenderSoal = ({ soal }) => { if (!soal || typeof soal !== 'object') return null; try { switch (soal.bentuk) { case 'Pilihan Ganda': case 'PG': return (
{soal.stimulus_content &&
{renderText(soal.stimulus_content)}
}
{renderText(soal.pertanyaan)}
{soal.opsi && typeof soal.opsi === 'object' && !Array.isArray(soal.opsi) ? ( Object.entries(soal.opsi).map(([key, value]) => (
{key}. {renderText(String(value))}
)) ) : (

Error: Format opsi PG harus object (A: ..., B: ...)

)}
); case 'PG Kompleks': case 'Pilihan Ganda Kompleks': const opsiPGK = Array.isArray(soal.opsi) ? soal.opsi : (typeof soal.opsi === 'object' && soal.opsi !== null ? Object.values(soal.opsi) : []); return (
{soal.stimulus_content &&
{renderText(soal.stimulus_content)}
}
{renderText(soal.pertanyaan)}
{opsiPGK.length > 0 ? ( opsiPGK.map((opsi, idx) => (
{renderText(typeof opsi === 'object' ? opsi.teks : opsi)}
)) ) : (

Error: Format opsi PG Kompleks tidak valid

)}
); case 'Menjodohkan': return (
{soal.stimulus_content &&
{renderText(soal.stimulus_content)}
}
{renderText(soal.pertanyaan)}
{/* Kolom Kiri */}
Lajur Kiri
    {Array.isArray(soal.premis_kiri) && soal.premis_kiri.map((item, i) => (
  • {String.fromCharCode(65 + i)}. {renderText(String(item))}
  • ))}
{/* Kolom Kanan */}
Lajur Kanan
    {Array.isArray(soal.respon_kanan) && soal.respon_kanan.map((item, i) => (
  • {i + 1}. {renderText(String(item))}
  • ))}
); case 'Benar Salah': return (
{soal.stimulus_content &&
{renderText(soal.stimulus_content)}
}
{renderText(soal.pertanyaan)}
{Array.isArray(soal.pernyataan) && soal.pernyataan.map((item, idx) => ( ))}
No Pernyataan Benar Salah
{idx + 1} {renderText(typeof item === 'object' ? item.teks : item)}
); case 'Setuju-Tidak Setuju': return (
{soal.stimulus_content &&
{renderText(soal.stimulus_content)}
}
{renderText(soal.pertanyaan)}
{Array.isArray(soal.pernyataan) && soal.pernyataan.map((item, idx) => ( ))}
No Pernyataan Setuju Tidak Setuju
{idx + 1} {renderText(typeof item === 'object' ? item.teks : item)}
); case 'Jawaban Singkat': return (
{soal.stimulus_content &&
{renderText(soal.stimulus_content)}
}
{renderText(soal.pertanyaan)}
); case 'Isian Singkat': return (
{soal.stimulus_content &&
{renderText(soal.stimulus_content)}
}
{renderText(soal.pertanyaan)}
(1 kata)
); case 'Uraian/ Essay': return (
{soal.stimulus_content &&
{renderText(soal.stimulus_content)}
}
{renderText(soal.pertanyaan)}
); case 'Mengkategorikan': return (
{soal.stimulus_content &&
{renderText(soal.stimulus_content)}
}
{renderText(soal.pertanyaan)}
{soal.kategori && soal.item_kategori ? ( {soal.kategori.map((kat, kIdx) => ( ))} {soal.item_kategori.map((item, idx) => ( {soal.kategori.map((_, kIdx) => ( ))} ))}
Item / Pernyataan{kat}
{renderText(item)}
) :

Error: Format kategori tidak sesuai.

}
); case 'Mengurutkan/ Menyusun': return (
{soal.stimulus_content &&
{renderText(soal.stimulus_content)}
}
{renderText(soal.pertanyaan)}
{Array.isArray(soal.item_acak) && soal.item_acak.map((item, idx) => (
( ... )
{renderText(item)}
))}
); default: return
Format soal {soal.bentuk} tidak dikenali
; } } catch (err) { return
Gagal merender soal ini. Data tidak valid.
; } }; const App = () => { const [loading, setLoading] = useState(false); const [loadingProgress, setLoadingProgress] = useState(''); const [loadingMateri, setLoadingMateri] = useState(false); const [loadingTP, setLoadingTP] = useState(false); const [config, setConfig] = useState({ sekolah: 'SMP Negeri 3 Cikarang Timur', tingkatSekolah: 'SMP', mapel: 'Pendidikan Agama Islam dan Budi Pekerti', tp: '', bab: '', subBab: '', materiLengkap: '', namaGuru: 'Asep Saefullah', kelas: 'Kelas 9 (Fase D)', kurikulum: 'Kurikulum Merdeka', jumlahPerBentuk: { 'Pilihan Ganda': 10 }, levelKognitif: { L1: 3, L2: 4, L3: 3 }, jumlahGambar: 0, }); const [generatedData, setGeneratedData] = useState(null); const [activeTab, setActiveTab] = useState('input'); const [errorMsg, setErrorMsg] = useState(''); const apiKey = ""; // --- SETUP AWAL & PROTEKSI COPY/KLIK KANAN --- useEffect(() => { // 1. Injeksi Tailwind CSS CDN agar tampilan tetap rapi saat di-deploy ke Netlify/Vercel if (!document.getElementById('tailwind-cdn-script')) { const script = document.createElement('script'); script.id = 'tailwind-cdn-script'; script.src = 'https://cdn.tailwindcss.com'; document.head.appendChild(script); } // 2. Mencegah aksi klik kanan dan copy-paste const preventAction = (e) => { // Izinkan aksi pada input dan textarea agar form tetap bisa digunakan untuk mengetik & menempel teks const isInput = e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA'; if (!isInput) { e.preventDefault(); } }; // Mencegah klik kanan (Context Menu) document.addEventListener('contextmenu', preventAction); // Mencegah Copy dan Cut dari keyboard document.addEventListener('copy', preventAction); document.addEventListener('cut', preventAction); // Mencegah drag/tarik elemen (teks/gambar) document.addEventListener('dragstart', preventAction); return () => { document.removeEventListener('contextmenu', preventAction); document.removeEventListener('copy', preventAction); document.removeEventListener('cut', preventAction); document.removeEventListener('dragstart', preventAction); }; }, []); const getKelasOptions = (tingkat) => { switch (tingkat) { case 'SD': return ["Kelas 1 (Fase A)", "Kelas 2 (Fase A)", "Kelas 3 (Fase B)", "Kelas 4 (Fase B)", "Kelas 5 (Fase C)", "Kelas 6 (Fase C)"]; case 'SMP': return ["Kelas 7 (Fase D)", "Kelas 8 (Fase D)", "Kelas 9 (Fase D)"]; case 'SMA': case 'SMK': return ["Kelas 10 (Fase E)", "Kelas 11 (Fase F)", "Kelas 12 (Fase F)"]; default: return ["Pilih Jenjang Dulu"]; } }; const handleTingkatChange = (e) => { const newTingkat = e.target.value; const options = getKelasOptions(newTingkat); setConfig(prev => ({ ...prev, tingkatSekolah: newTingkat, kelas: options[0] })); }; const totalSoal = Object.values(config.jumlahPerBentuk).reduce((acc, curr) => acc + (parseInt(curr) || 0), 0); const totalLevel = (parseInt(config.levelKognitif.L1) || 0) + (parseInt(config.levelKognitif.L2) || 0) + (parseInt(config.levelKognitif.L3) || 0); const isConfigValid = totalSoal > 0 && totalSoal <= 100 && totalSoal === totalLevel; const handleBentukChange = (type, value) => { const val = parseInt(value); setConfig(prev => { const newBentuk = { ...prev.jumlahPerBentuk }; if (isNaN(val) || val <= 0) { delete newBentuk[type]; } else { newBentuk[type] = val; } return { ...prev, jumlahPerBentuk: newBentuk }; }); }; const generateTPOtomatis = async () => { if (!config.mapel || !config.bab) { alert("Mohon isi 'Mata Pelajaran' dan 'BAB' terlebih dahulu."); return; } setLoadingTP(true); const userQuery = `Rumuskan Tujuan Pembelajaran (TP) yang spesifik dan terukur dalam 1-2 kalimat pendek. Konteks: Jenjang ${config.tingkatSekolah}, Mapel ${config.mapel}, ${config.kelas}, BAB ${config.bab}, Topik ${config.subBab}. Materi Referensi (Wajib Relevan): """${config.materiLengkap || "Sesuai standar kurikulum"}""" Instruksi: Gunakan KKO sesuai taksonomi Bloom. TP HARUS mencerminkan materi referensi di atas. Hapus simbol markdown (*, #).`; try { const response = await fetchWithRetry(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${apiKey}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents: [{ parts: [{ text: userQuery }] }] }) }); const data = await response.json(); if (data.candidates?.[0]?.content?.parts?.[0]?.text) setConfig(prev => ({ ...prev, tp: data.candidates[0].content.parts[0].text.replace(/[*#]/g, '').trim() })); } catch (error) { console.error(error); alert("Gagal generate TP: " + error.message); } finally { setLoadingTP(false); } }; const generateMateriOtomatis = async () => { if (!config.mapel || !config.bab) { alert("Mohon isi 'Mata Pelajaran' dan 'BAB' terlebih dahulu."); return; } setLoadingMateri(true); const userQuery = `Buatkan ringkasan materi pelajaran SANGAT PADAT (Max 150 kata). Identitas: ${config.tingkatSekolah}, ${config.mapel}, ${config.kelas}, BAB ${config.bab}, Sub ${config.subBab}. Instruksi: Paragraf narasi biasa. HAPUS semua markdown (*, #, -). Hapus enter berlebih.`; try { const response = await fetchWithRetry(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${apiKey}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents: [{ parts: [{ text: userQuery }] }] }) }); const data = await response.json(); if (data.candidates?.[0]?.content?.parts?.[0]?.text) setConfig(prev => ({ ...prev, materiLengkap: data.candidates[0].content.parts[0].text.replace(/[*#]/g, '').replace(/\n\s*\n/g, '\n').trim() })); } catch (error) { console.error(error); alert("Gagal generate materi: " + error.message); } finally { setLoadingMateri(false); } }; const generateContent = async () => { setLoading(true); setErrorMsg(''); setGeneratedData(null); setLoadingProgress('Mempersiapkan data...'); // 1. CHUNKING LOGIC UNTUK MENGHINDARI OUTPUT LIMIT // Membagi jumlah soal menjadi potongan per 5 soal agar AI tidak memotong JSON const chunks = []; let currentChunk = {}; let currentCount = 0; for (const [bentuk, jumlah] of Object.entries(config.jumlahPerBentuk)) { let remaining = parseInt(jumlah) || 0; while (remaining > 0) { // Mengurangi jumlah soal per request dari 10 menjadi 5 untuk mencegah pemotongan JSON const take = Math.min(remaining, 5 - currentCount); currentChunk[bentuk] = (currentChunk[bentuk] || 0) + take; currentCount += take; remaining -= take; if (currentCount >= 5) { chunks.push(currentChunk); currentChunk = {}; currentCount = 0; } } } if (currentCount > 0) chunks.push(currentChunk); // 2. DISTRIBUSI LEVEL KOGNITIF & GAMBAR SECARA PROPORSIONAL PER CHUNK let remainingL1 = parseInt(config.levelKognitif.L1) || 0; let remainingL2 = parseInt(config.levelKognitif.L2) || 0; let remainingL3 = parseInt(config.levelKognitif.L3) || 0; let remainingGambar = parseInt(config.jumlahGambar) || 0; const chunkData = chunks.map(chunk => { const totalChunk = Object.values(chunk).reduce((a, b) => a + b, 0); let cL1 = 0, cL2 = 0, cL3 = 0, cGambar = 0; for (let i = 0; i < totalChunk; i++) { if (remainingL1 > 0) { cL1++; remainingL1--; } else if (remainingL2 > 0) { cL2++; remainingL2--; } else if (remainingL3 > 0) { cL3++; remainingL3--; } } const takeImg = Math.min(remainingGambar, totalChunk); cGambar = takeImg; remainingGambar -= takeImg; return { bentuk: chunk, total: totalChunk, L1: cL1, L2: cL2, L3: cL3, gambar: cGambar }; }); let allKisi = []; let allSoal = []; try { // PROSES SETIAP CHUNK SECARA BERURUTAN for (let i = 0; i < chunkData.length; i++) { const c = chunkData[i]; setLoadingProgress(`Meracik Soal: Bagian ${i + 1} dari ${chunkData.length} ...`); const bentukString = Object.entries(c.bentuk).map(([k, v]) => `${v} soal ${k}`).join(', '); const cLevelString = `L1 (Pemahaman/C1-C2): ${c.L1} soal, L2 (Aplikasi/C3): ${c.L2} soal, L3 (Penalaran/HOTS/C4-C6): ${c.L3} soal`; const systemPrompt = `Anda adalah pakar evaluasi pendidikan jenjang ${config.tingkatSekolah}. Tugas: Susun Kisi-kisi & Soal yang valid. REFERENSI UTAMA: """${config.materiLengkap || "Gunakan pengetahuan umum kurikulum yang sesuai."}""" ATURAN KONTEN SANGAT PENTING: 1. **STIMULUS LITERASI & NUMERASI (WAJIB)**: SETIAP soal TANPA TERKECUALI WAJIB memiliki 'stimulus_content'. Stimulus harus menuntut siswa untuk berliterasi (wacana/artikel/kasus) atau bernumerasi (tabel/angka/data). 2. **Pertanyaan**: Harus terkait logis dengan stimulus yang diberikan. 3. **Distribusi Level Kognitif**: Sesuaikan persis dengan target: ${cLevelString}. 4. **Menjodohkan**: Gunakan format dua lajur. Lajur Kiri berisi premis, Lajur Kanan berisi pasangan. 5. **Gambar**: Buat ${c.gambar} soal yang WAJIB memiliki stimulus berupa deskripsi gambar (Gunakan format: [GAMBAR: Deskripsi detil visual gambar...]). 6. **Kunci Jawaban**: PG cukup huruf kunci. Menjodohkan pasangkan kode. 7. **Format Tambahan Khusus**: - Setuju-Tidak Setuju / Benar Salah: Gunakan array "pernyataan". - Mengkategorikan: Gunakan array "kategori" dan "item_kategori". - Mengurutkan/ Menyusun: Gunakan array "item_acak". Kunci berisi urutan benar. - Isian Singkat: Kunci jawaban WAJIB HANYA 1 KATA. 8. **Integrasi Ayat Al-Quran & Hadits**: Jika materi terkait Agama Islam/BTA, sertakan teks Arab diapit tag [ARABIC] dan [/ARABIC] (contoh: [ARABIC] بِسْمِ اللَّهِ [/ARABIC]). ATURAN JSON STRICT: 1. WAJIB KEMBALIKAN JSON VALID. Jangan terpotong. 2. Opsi Pilihan Ganda harus OBJECT dengan 4 opsi pasti {"A": "...", "B": "...", "C": "...", "D": "..."}. 3. Opsi PG Kompleks harus ARRAY string ["...", "...", "...", "..."] (Bukan object). FORMAT JSON: { "kisi_kisi": [{ "tp": "...", "materi": "...", "level": "L3", "indikator": "...", "nomor": 1, "bentuk": "...", "kunci": "..." }], "soal": [{ "nomor": 1, "bentuk": "...", "pertanyaan": "...", "stimulus_content": "...", "opsi": {"A": "..."}, "kunci": "...", "premis_kiri": ["..."], "respon_kanan": ["..."], "pernyataan": ["..."] }] }`; const userQuery = `Buat instrumen penilaian ${config.tingkatSekolah} Mapel ${config.mapel} ${config.kelas}. BAB: ${config.bab}, Sub: ${config.subBab}. TP: ${config.tp}. Bentuk & Jumlah Soal: ${bentukString}. Total: ${c.total} soal. Total Bergambar: ${c.gambar} soal. Guru: ${config.namaGuru}, Sekolah: ${config.sekolah}.`; const response = await fetchWithRetry(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${apiKey}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents: [{ parts: [{ text: userQuery }] }], systemInstruction: { parts: [{ text: systemPrompt }] }, generationConfig: { responseMimeType: "application/json", maxOutputTokens: 8192 } }) }); const data = await response.json(); if (!data.candidates || !data.candidates[0]) { throw new Error(`Gagal memuat bagian ${i + 1}. AI tidak merespons dengan format yang benar. Coba lagi.`); } const rawText = data.candidates[0].content.parts[0].text; // Bersihkan teks dari markdown let cleanText = rawText.replace(/```json/gi, '').replace(/```/g, '').trim(); // Ekstrak JSON utuh const firstBrace = cleanText.indexOf('{'); const lastBrace = cleanText.lastIndexOf('}'); if (firstBrace !== -1 && lastBrace !== -1) { cleanText = cleanText.substring(firstBrace, lastBrace + 1); } let result; try { result = JSON.parse(cleanText); } catch (e) { console.error("Parse JSON Error:", e.message, "\nRaw Text Length:", cleanText.length); throw new Error(`Terjadi pemotongan teks dari AI pada bagian ke-${i + 1}. Mohon kurangi detail materi yang terlalu panjang atau coba generate ulang.`); } // NORMALISASI NAMA BENTUK SOAL DARI AI const normalisasiBentuk = (dataArray) => { if(!dataArray) return; dataArray.forEach(item => { if(!item.bentuk) return; const b = item.bentuk.toLowerCase(); if (b.includes('uraian') || b.includes('essay')) item.bentuk = 'Uraian/ Essay'; else if (b.includes('urut') || b.includes('susun')) item.bentuk = 'Mengurutkan/ Menyusun'; else if (b.includes('benar') && b.includes('salah')) item.bentuk = 'Benar Salah'; else if (b.includes('setuju')) item.bentuk = 'Setuju-Tidak Setuju'; else if (b.includes('kategori')) item.bentuk = 'Mengkategorikan'; else if (b.includes('kompleks')) item.bentuk = 'Pilihan Ganda Kompleks'; }); }; if (result.kisi_kisi) { normalisasiBentuk(result.kisi_kisi); allKisi.push(...result.kisi_kisi); } if (result.soal) { normalisasiBentuk(result.soal); allSoal.push(...result.soal); } } setLoadingProgress('Menyusun dan merapikan urutan...'); // --- MENGURUTKAN SELURUH SOAL DAN KISI-KISI BERDASARKAN BENTUK --- const urutanBentuk = [ 'pilihan ganda', 'pg', 'pilihan ganda kompleks', 'pg kompleks', 'menjodohkan', 'benar salah', 'setuju-tidak setuju', 'mengkategorikan', 'mengurutkan/ menyusun', 'isian singkat', 'jawaban singkat', 'uraian/ essay' ]; const getOrderIndex = (bentuk) => { if (!bentuk) return 999; const idx = urutanBentuk.indexOf(bentuk.toLowerCase()); return idx === -1 ? 999 : idx; }; allSoal.sort((a, b) => getOrderIndex(a.bentuk) - getOrderIndex(b.bentuk)); allKisi.sort((a, b) => getOrderIndex(a.bentuk) - getOrderIndex(b.bentuk)); // Sinkronisasi ulang nomor urut agar rapi dari 1 hingga N allSoal.forEach((s, i) => s.nomor = i + 1); allKisi.forEach((k, i) => k.nomor = i + 1); setGeneratedData({ kisi_kisi: allKisi, soal: allSoal }); setActiveTab('preview'); } catch (error) { console.error(error); setErrorMsg(error.message); } finally { setLoading(false); setLoadingProgress(''); } }; // --- RENDER KE WORD (DIPERBAIKI UNTUK CETAK RAPI) --- const handleDownloadWord = () => { if (!generatedData) return; let html = ` Soal Naskah

NASKAH PENILAIAN HARIAN

${config.sekolah}

Mata Pelajaran:${config.mapel} Nama Siswa:
Kelas/Fase:${config.kelas} No. Absen:
Jawablah pertanyaan di bawah ini dengan benar dan teliti!
`; generatedData.soal.forEach(s => { html += ``; html += ``; html += `
${s.nomor}.`; if (s.stimulus_content) { html += `
${processExportText(s.stimulus_content)}
`; } html += `
${processExportText(s.pertanyaan)}
`; // RENDER BERDASARKAN BENTUK if (['Pilihan Ganda', 'PG'].includes(s.bentuk) && s.opsi) { html += ``; Object.entries(s.opsi).forEach(([k, v]) => { html += ``; }); html += `
${k}.${processExportText(v)}
`; } else if (['PG Kompleks', 'Pilihan Ganda Kompleks'].includes(s.bentuk) && s.opsi) { html += ``; const opsiArray = Array.isArray(s.opsi) ? s.opsi : Object.values(s.opsi); opsiArray.forEach(o => { html += ``; }); html += `
${processExportText(typeof o === 'object' ? o.teks : o)}
`; } else if (['Menjodohkan'].includes(s.bentuk)) { html += `
Lajur Kiri `; (s.premis_kiri || []).forEach((i, idx) => { html += ``; }); html += `
${String.fromCharCode(65 + idx)}.${processExportText(i)}
  Lajur Kanan `; (s.respon_kanan || []).forEach((i, idx) => { html += ``; }); html += `
${idx+1}.${processExportText(i)}
`; } else if (['Benar Salah'].includes(s.bentuk)) { html += ``; (s.pernyataan || []).forEach((p, idx) => { html += ``; }); html += `
NoPernyataanBenarSalah
${idx+1}${processExportText(typeof p === 'object' ? p.teks : p)}
`; } else if (['Setuju-Tidak Setuju'].includes(s.bentuk)) { html += ``; (s.pernyataan || []).forEach((p, idx) => { html += ``; }); html += `
NoPernyataanSetujuTidak Setuju
${idx+1}${processExportText(typeof p === 'object' ? p.teks : p)}
`; } else if (['Mengkategorikan'].includes(s.bentuk) && s.kategori && s.item_kategori) { html += ``; s.kategori.forEach(k => html += ``); html += ``; s.item_kategori.forEach(item => { html += ``; s.kategori.forEach(() => html += ``); html += ``; }); html += `
Item / Pernyataan${k}
${processExportText(item)}
`; } else if (['Mengurutkan/ Menyusun'].includes(s.bentuk) && Array.isArray(s.item_acak)) { html += ``; s.item_acak.forEach(item => { html += ``; }); html += `
(....)${processExportText(item)}
`; } else if (['Isian Singkat'].includes(s.bentuk)) { html += `
Jawaban:  
`; } else if (['Jawaban Singkat'].includes(s.bentuk)) { html += `
Jawaban:  
`; } else if (['Uraian/ Essay'].includes(s.bentuk)) { html += `
`; } html += `
`; }); // KUNCI JAWABAN html += `

KUNCI JAWABAN

${config.mapel}

`; generatedData.soal.forEach(s => { // Menggunakan Helper Kunci Jawaban dengan tag
agar baris baru di Word html += ``; }); html += `
NoJawaban
${s.nomor}${formatKunciJawaban(s.kunci, '
')}
`; const blob = new Blob([html], { type: 'application/msword' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = `Naskah_Soal_${config.mapel.replace(/ /g, '_')}.doc`; link.click(); }; const handleDownloadExcel = () => { if (!generatedData) return; let html = ` `; generatedData.kisi_kisi.forEach((item, idx) => { html += ``; }); html += `
KISI-KISI SOAL ${config.mapel.toUpperCase()}
Sekolah: ${config.sekolah}
Kelas: ${config.kelas}
NO TP MATERI LEVEL INDIKATOR BENTUK NO SOAL KUNCI
${idx+1} ${item.tp} ${item.materi} ${item.level} ${item.indikator} ${item.bentuk} ${item.nomor} ${formatKunciJawaban(item.kunci, ', ')}
`; const blob = new Blob([html], { type: 'application/vnd.ms-excel' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = `Kisi_Kisi_${config.mapel.replace(/ /g, '_')}.xls`; link.click(); }; const handlePrintPDF = () => { if (!generatedData) return; const printWindow = window.open('', '_blank'); printWindow.document.write(`Print Naskah Soal

NASKAH PENILAIAN HARIAN

${config.sekolah}

Mapel:${config.mapel} Nama:
Kelas:${config.kelas} No. Absen:
${generatedData.soal.map(s => `
${s.nomor}. ${s.stimulus_content ? `
${processExportText(s.stimulus_content)}
` : ''}
${processExportText(s.pertanyaan)}
${['Pilihan Ganda', 'PG'].includes(s.bentuk) && s.opsi ? `` + Object.entries(s.opsi).map(([k,v]) => ``).join('') + `
${k}.${processExportText(v)}
` : ''} ${['PG Kompleks', 'Pilihan Ganda Kompleks'].includes(s.bentuk) && s.opsi ? `` + (Array.isArray(s.opsi) ? s.opsi : Object.values(s.opsi)).map(o => ``).join('') + `
${processExportText(typeof o === 'object' ? o.teks : o)}
` : ''} ${['Menjodohkan'].includes(s.bentuk) ? `
Lajur Kiri
${(s.premis_kiri || []).map((i, idx) => ``).join('')}
${String.fromCharCode(65+idx)}.${processExportText(i)}
Lajur Kanan
${(s.respon_kanan || []).map((i, idx) => ``).join('')}
${idx+1}.${processExportText(i)}
` : ''} ${['Benar Salah'].includes(s.bentuk) ? `${(s.pernyataan || []).map((p, idx) => ``).join('')}
NoPernyataanBenarSalah
${idx+1}${processExportText(typeof p === 'object' ? p.teks : p)}
` : ''} ${['Setuju-Tidak Setuju'].includes(s.bentuk) ? `${(s.pernyataan || []).map((p, idx) => ``).join('')}
NoPernyataanSetujuTidak Setuju
${idx+1}${processExportText(typeof p === 'object' ? p.teks : p)}
` : ''} ${['Mengkategorikan'].includes(s.bentuk) && s.kategori ? `${s.kategori.map(k => ``).join('')}${(s.item_kategori || []).map(item => `${s.kategori.map(() => ``).join('')}`).join('')}
Item${k}
${processExportText(item)}
` : ''} ${['Mengurutkan/ Menyusun'].includes(s.bentuk) ? `${(s.item_acak || []).map(item => ``).join('')}
(....)${processExportText(item)}
` : ''} ${['Isian Singkat'].includes(s.bentuk) ? `
Jawaban:  
` : ''} ${['Jawaban Singkat'].includes(s.bentuk) ? `
Jawaban:  
` : ''} ${['Uraian/ Essay'].includes(s.bentuk) ? `
` : ''}
`).join('')} `); printWindow.document.close(); }; return (
{/* HEADER */}

Generator Soal dan Kisi-Kisi

Dikembangkan Oleh : Asep Saefullah || SMP Negeri 3 Cikarang Timur

{errorMsg && (

Error

{errorMsg}

)} {activeTab === 'input' ? (
{/* KOLOM KIRI: Identitas */}

Identitas & Materi

{/* JENJANG & KELAS DINAMIS */}
{/* MATERI */}

Detail Materi

setConfig({...config, bab: e.target.value})} />
setConfig({...config, subBab: e.target.value})} />