jnm-itb commited on
Commit
ceceae5
·
1 Parent(s): 1ff1bca

Refactor code structure for improved readability and maintainability

Browse files
Files changed (3) hide show
  1. index.html +6 -0
  2. quiz_questions.json +82 -0
  3. unet.html +1310 -0
index.html CHANGED
@@ -295,6 +295,12 @@
295
  <p class="link-description">Model vehicle movement and traffic light systems.</p>
296
  <a href="#" class="link-url">Observe Traffic</a>
297
  </div>
 
 
 
 
 
 
298
  </div>
299
 
300
  <footer>
 
295
  <p class="link-description">Model vehicle movement and traffic light systems.</p>
296
  <a href="#" class="link-url">Observe Traffic</a>
297
  </div>
298
+
299
+ <div class="link-card">
300
+ <h3 class="link-title"><i class="fas fa-brain link-icon"></i> Simulasi Arsitektur U-Net</h3>
301
+ <p class="link-description">Visualisasi arsitektur U-Net yang umum digunakan untuk segmentasi gambar biomedis.</p>
302
+ <a href="unet.html" class="link-url">Lihat Simulasi</a>
303
+ </div>
304
  </div>
305
 
306
  <footer>
quiz_questions.json ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "question": "Apa fungsi utama dari U-Net?",
4
+ "options": [
5
+ "Klasifikasi gambar",
6
+ "Segmentasi gambar",
7
+ "Pengenalan wajah",
8
+ "Kompresi gambar"
9
+ ],
10
+ "correctAnswers": [1]
11
+ },
12
+ {
13
+ "question": "Apa karakteristik arsitektur U-Net?",
14
+ "options": [
15
+ "Memiliki jalur contracting dan expanding",
16
+ "Menggunakan skip connections",
17
+ "Berbentuk seperti huruf U",
18
+ "Didesain khusus untuk segmentasi citra medis"
19
+ ],
20
+ "correctAnswers": [0, 1, 2, 3]
21
+ },
22
+ {
23
+ "question": "Apa fungsi dari skip connections pada U-Net?",
24
+ "options": [
25
+ "Mengurangi jumlah parameter",
26
+ "Mempercepat proses training",
27
+ "Menggabungkan informasi kontekstual dan spasial",
28
+ "Mencegah hilangnya informasi spasial"
29
+ ],
30
+ "correctAnswers": [2, 3]
31
+ },
32
+ {
33
+ "question": "Apa tujuan utama dari 'Contracting Path' (Encoder) dalam arsitektur U-Net?",
34
+ "options": [
35
+ "Meningkatkan resolusi spasial gambar.",
36
+ "Mengekstrak fitur kontekstual dan mengurangi resolusi spasial.",
37
+ "Menggabungkan fitur dari encoder dengan decoder.",
38
+ "Menghasilkan peta segmentasi akhir."
39
+ ],
40
+ "correctAnswers": [1]
41
+ },
42
+ {
43
+ "question": "Apa fungsi dari 'Skip Connections' pada U-Net?",
44
+ "options": [
45
+ "Mempercepat proses training.",
46
+ "Mengurangi jumlah parameter model.",
47
+ "Membantu decoder memulihkan detail spasial halus yang hilang selama down-sampling.",
48
+ "Hanya digunakan untuk visualisasi."
49
+ ],
50
+ "correctAnswers": [2]
51
+ },
52
+ {
53
+ "question": "Lapisan apa yang biasanya digunakan di bagian 'Expanding Path' (Decoder) untuk meningkatkan resolusi spasial?",
54
+ "options": [
55
+ "Max Pooling",
56
+ "Convolution",
57
+ "Up-Convolution (Transposed Convolution)",
58
+ "ReLU Activation"
59
+ ],
60
+ "correctAnswers": [2]
61
+ },
62
+ {
63
+ "question": "Manakah dari berikut ini yang merupakan karakteristik lapisan 'Bottleneck' pada U-Net?",
64
+ "options": [
65
+ "Memiliki resolusi spasial tertinggi.",
66
+ "Menggabungkan fitur dari input asli.",
67
+ "Menghubungkan Contracting Path dan Expanding Path.",
68
+ "Memiliki jumlah fitur (channels) paling sedikit."
69
+ ],
70
+ "correctAnswers": [2]
71
+ },
72
+ {
73
+ "question": "Operasi apa yang biasanya dilakukan setelah menggabungkan (concatenate) output Up-Convolution dengan fitur dari Skip Connection di Decoder?",
74
+ "options": [
75
+ "Max Pooling lagi",
76
+ "Satu atau lebih lapisan Konvolusi (+ ReLU)",
77
+ "Konvolusi 1x1 untuk output",
78
+ "Normalisasi Batch saja"
79
+ ],
80
+ "correctAnswers": [1]
81
+ }
82
+ ]
unet.html ADDED
@@ -0,0 +1,1310 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="id">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Simulasi Interaktif U-Net</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
9
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/OrbitControls.js"></script>
10
+ <style>
11
+ /* Custom styles */
12
+ body { font-family: 'Inter', sans-serif; overflow: hidden; /* Prevent scrolling */ }
13
+ #canvas-container { width: 100%; height: 100vh; position: relative; } /* Full viewport height */
14
+ #info-panel {
15
+ position: absolute;
16
+ top: 1rem;
17
+ left: 1rem;
18
+ background-color: rgba(0, 0, 0, 0.7);
19
+ color: white;
20
+ padding: 0.75rem;
21
+ border-radius: 0.5rem;
22
+ max-width: 250px; /* Limit width */
23
+ font-size: 0.875rem; /* text-sm */
24
+ z-index: 10;
25
+ }
26
+ #controls {
27
+ position: absolute;
28
+ bottom: 1rem;
29
+ left: 50%;
30
+ transform: translateX(-50%);
31
+ background-color: rgba(0, 0, 0, 0.7);
32
+ padding: 0.5rem 1rem;
33
+ border-radius: 0.5rem;
34
+ display: flex;
35
+ gap: 0.5rem; /* Space between buttons */
36
+ z-index: 10;
37
+ }
38
+ .control-button {
39
+ background-color: #4a5568; /* gray-700 */
40
+ color: white;
41
+ padding: 0.5rem 1rem;
42
+ border-radius: 0.375rem; /* rounded-md */
43
+ border: none;
44
+ cursor: pointer;
45
+ transition: background-color 0.2s;
46
+ }
47
+ .control-button:hover {
48
+ background-color: #2d3748; /* gray-800 */
49
+ }
50
+ .control-button:disabled {
51
+ background-color: #a0aec0; /* gray-500 */
52
+ cursor: not-allowed;
53
+ }
54
+ /* Style for highlighting active block */
55
+ .active-block {
56
+ /* Example styles - choose what looks best */
57
+ border: 2px solid yellow; /* Tambahkan border kuning */
58
+ box-shadow: 0 0 10px yellow; /* Tambahkan efek glow kuning */
59
+ /* Anda juga bisa mengubah background-color atau properti lainnya */
60
+ }
61
+ /* Style for skip connection visualization (Handled by Three.js) */
62
+ #feature-map-display {
63
+ position: absolute;
64
+ top: 1rem;
65
+ right: 1rem;
66
+ background-color: rgba(255, 255, 255, 0.9);
67
+ border: 1px solid #ccc;
68
+ border-radius: 0.5rem;
69
+ padding: 0.5rem;
70
+ width: 150px; /* Adjust as needed */
71
+ height: 150px; /* Adjust as needed */
72
+ z-index: 10;
73
+ display: none; /* Hidden by default */
74
+ font-size: 0.75rem;
75
+ color: #333;
76
+ text-align: center;
77
+ }
78
+ #feature-map-display canvas {
79
+ display: block;
80
+ margin: 5px auto 0;
81
+ max-width: 100%;
82
+ height: auto;
83
+ }
84
+ /* Style for Tooltip */
85
+ #tooltip {
86
+ position: absolute;
87
+ display: none; /* Hidden by default */
88
+ background-color: rgba(0, 0, 0, 0.8);
89
+ color: white;
90
+ padding: 5px 10px;
91
+ border-radius: 4px;
92
+ font-size: 0.8rem;
93
+ white-space: pre-wrap; /* Allow line breaks */
94
+ z-index: 100; /* Ensure it's on top */
95
+ pointer-events: none; /* Prevent tooltip from blocking mouse events */
96
+ max-width: 200px;
97
+ }
98
+ /* Style for Modal */
99
+ #detail-modal-backdrop {
100
+ position: fixed;
101
+ top: 0;
102
+ left: 0;
103
+ width: 100%;
104
+ height: 100%;
105
+ background-color: rgba(0, 0, 0, 0.6); /* Semi-transparent black */
106
+ z-index: 40; /* Below modal, above other content */
107
+ display: none; /* Hidden by default */
108
+ }
109
+ #detail-modal {
110
+ position: fixed;
111
+ top: 50%;
112
+ left: 50%;
113
+ transform: translate(-50%, -50%);
114
+ background-color: #2d3748; /* gray-800 */
115
+ color: white;
116
+ padding: 1.5rem; /* p-6 */
117
+ border-radius: 0.5rem; /* rounded-lg */
118
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); /* shadow-lg */
119
+ z-index: 50; /* Above backdrop */
120
+ display: none; /* Hidden by default */
121
+ min-width: 300px;
122
+ max-width: 90%; /* Responsive max width */
123
+ }
124
+ #detail-modal h4 {
125
+ font-size: 1.125rem; /* text-lg */
126
+ font-weight: 600; /* font-semibold */
127
+ margin-bottom: 1rem; /* mb-4 */
128
+ }
129
+ #detail-modal p {
130
+ font-size: 0.875rem; /* text-sm */
131
+ margin-bottom: 0.5rem; /* mb-2 */
132
+ white-space: pre-wrap; /* Respect newlines in info */
133
+ }
134
+ #modal-close-button {
135
+ position: absolute;
136
+ top: 0.5rem; /* top-2 */
137
+ right: 0.5rem; /* right-2 */
138
+ background: none;
139
+ border: none;
140
+ color: #a0aec0; /* gray-500 */
141
+ font-size: 1.5rem; /* text-2xl */
142
+ line-height: 1;
143
+ cursor: pointer;
144
+ }
145
+ #modal-close-button:hover {
146
+ color: #e2e8f0; /* gray-300 */
147
+ }
148
+ /* Style for Quiz Modal */
149
+ #quiz-modal-backdrop {
150
+ position: fixed;
151
+ top: 0;
152
+ left: 0;
153
+ width: 100%;
154
+ height: 100%;
155
+ background-color: rgba(0, 0, 0, 0.7); /* Darker backdrop */
156
+ z-index: 60; /* Above detail modal backdrop */
157
+ display: none; /* Hidden by default */
158
+ }
159
+ #quiz-modal {
160
+ position: fixed;
161
+ top: 50%;
162
+ left: 50%;
163
+ transform: translate(-50%, -50%);
164
+ background-color: #1a202c; /* gray-900 */
165
+ color: white;
166
+ padding: 1.5rem 2rem; /* p-6 px-8 */
167
+ border-radius: 0.75rem; /* rounded-xl */
168
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.2), 0 4px 6px -2px rgba(0, 0, 0, 0.1); /* shadow-lg */
169
+ z-index: 70; /* Above backdrop */
170
+ display: none; /* Hidden by default */
171
+ min-width: 400px;
172
+ max-width: 600px; /* Wider for questions */
173
+ width: 90%;
174
+ }
175
+ #quiz-modal h4 {
176
+ font-size: 1.25rem; /* text-xl */
177
+ font-weight: 600; /* font-semibold */
178
+ margin-bottom: 1.5rem; /* mb-6 */
179
+ text-align: center;
180
+ color: #a0aec0; /* gray-400 */
181
+ }
182
+ #quiz-question-container {
183
+ margin-bottom: 1.5rem; /* mb-6 */
184
+ }
185
+ #quiz-question {
186
+ font-size: 1rem; /* text-base */
187
+ margin-bottom: 1rem; /* mb-4 */
188
+ line-height: 1.5;
189
+ }
190
+ #quiz-options {
191
+ display: flex;
192
+ flex-direction: column;
193
+ gap: 0.75rem; /* space-y-3 */
194
+ }
195
+ .quiz-option label {
196
+ display: flex;
197
+ align-items: center;
198
+ background-color: #2d3748; /* gray-800 */
199
+ padding: 0.75rem 1rem; /* py-3 px-4 */
200
+ border-radius: 0.375rem; /* rounded-md */
201
+ cursor: pointer;
202
+ transition: background-color 0.2s;
203
+ font-size: 0.875rem; /* text-sm */
204
+ }
205
+ .quiz-option label:hover {
206
+ background-color: #4a5568; /* gray-700 */
207
+ }
208
+ .quiz-option input[type="checkbox"] {
209
+ margin-right: 0.75rem; /* mr-3 */
210
+ accent-color: #4299e1; /* blue-500 */
211
+ width: 1rem;
212
+ height: 1rem;
213
+ }
214
+ #quiz-feedback {
215
+ margin-top: 1rem; /* mt-4 */
216
+ font-size: 0.875rem; /* text-sm */
217
+ min-height: 1.25rem; /* Ensure space for feedback */
218
+ text-align: center;
219
+ }
220
+ #quiz-feedback.correct {
221
+ color: #48bb78; /* green-500 */
222
+ font-weight: 600;
223
+ }
224
+ #quiz-feedback.incorrect {
225
+ color: #f56565; /* red-500 */
226
+ font-weight: 600;
227
+ }
228
+ #quiz-score {
229
+ text-align: right;
230
+ font-size: 0.875rem; /* text-sm */
231
+ color: #a0aec0; /* gray-500 */
232
+ margin-bottom: 1rem; /* mb-4 */
233
+ }
234
+ #quiz-controls {
235
+ display: flex;
236
+ justify-content: space-between; /* Adjust layout */
237
+ align-items: center;
238
+ margin-top: 1.5rem; /* mt-6 */
239
+ }
240
+ /* #quiz-submit-button uses .control-button class */
241
+ #quiz-close-button {
242
+ background: none;
243
+ border: none;
244
+ color: #a0aec0; /* gray-500 */
245
+ font-size: 1rem;
246
+ cursor: pointer;
247
+ }
248
+ #quiz-close-button:hover {
249
+ color: #e2e8f0; /* gray-300 */
250
+ }
251
+ #quiz-results {
252
+ text-align: center;
253
+ margin-top: 2rem; /* mt-8 */
254
+ }
255
+ #quiz-results h5 {
256
+ font-size: 1.125rem; /* text-lg */
257
+ font-weight: 600;
258
+ margin-bottom: 0.5rem; /* mb-2 */
259
+ }
260
+ #quiz-results p {
261
+ font-size: 1rem; /* text-base */
262
+ color: #cbd5e0; /* gray-400 */
263
+ }
264
+
265
+ </style>
266
+ <link rel="preconnect" href="https://fonts.googleapis.com">
267
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
268
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet">
269
+ </head>
270
+ <body class="bg-gray-900">
271
+
272
+ <div id="canvas-container">
273
+ <div id="info-panel">
274
+ <h3 class="text-lg font-semibold mb-2">Informasi Simulasi</h3>
275
+ <p><strong>Input Gambar:</strong> <span id="input-name">-</span></p>
276
+ <p><strong>Langkah Saat Ini:</strong> <span id="current-step">-</span></p>
277
+ <p><strong>Dimensi Data:</strong> <span id="data-dimensions">-</span></p>
278
+ <p class="mt-2 text-xs">Gunakan mouse untuk memutar (klik kiri), zoom (scroll), dan pan (klik kanan).</p>
279
+ <p class="mt-1 text-xs">Klik pada blok untuk info detail.</p>
280
+ </div>
281
+
282
+ <div id="controls">
283
+ <select id="input-select" class="control-button bg-gray-700 hover:bg-gray-800 text-white rounded-md px-3 py-1.5 text-sm">
284
+ <option value="sample1">Input Sampel 1</option>
285
+ <option value="sample2">Input Sampel 2</option>
286
+ </select>
287
+ <button id="reset-button" class="control-button">Reset</button>
288
+ <button id="prev-step-button" class="control-button" disabled>← Sebelumnya</button>
289
+ <button id="next-step-button" class="control-button">Selanjutnya →</button>
290
+ <button id="run-auto-button" class="control-button">Jalankan Otomatis</button>
291
+ <button id="quiz-button" class="control-button">Quiz</button> <!-- New Quiz Button -->
292
+ </div>
293
+
294
+ <div id="feature-map-display">
295
+ <p>Feature Map (Visualisasi)</p>
296
+ <canvas id="feature-map-canvas" width="128" height="128"></canvas>
297
+ </div>
298
+
299
+ </div>
300
+
301
+ <!-- Modal Structure -->
302
+ <div id="detail-modal-backdrop"></div>
303
+ <div id="detail-modal">
304
+ <button id="modal-close-button">&times;</button>
305
+ <h4 id="modal-title">Detail Blok</h4>
306
+ <p><strong>Nama:</strong> <span id="modal-block-name">-</span></p>
307
+ <p><strong>Dimensi Data:</strong> <span id="modal-block-dimensions">-</span></p>
308
+ <p><strong>Deskripsi:</strong></p>
309
+ <p id="modal-block-description">-</p>
310
+ <!-- Add more detail fields if needed -->
311
+ </div>
312
+
313
+ <!-- Quiz Modal Structure -->
314
+ <div id="quiz-modal-backdrop"></div>
315
+ <div id="quiz-modal">
316
+ <h4>U-Net Quiz</h4>
317
+ <div id="quiz-score">Skor: 0/0</div>
318
+ <div id="quiz-question-container">
319
+ <p id="quiz-question">Memuat pertanyaan...</p>
320
+ <div id="quiz-options">
321
+ <!-- Options will be loaded here -->
322
+ </div>
323
+ <div id="quiz-feedback"></div>
324
+ </div>
325
+ <div id="quiz-results" style="display: none;">
326
+ <h5>Quiz Selesai!</h5>
327
+ <p id="quiz-final-score"></p>
328
+ </div>
329
+ <div id="quiz-controls">
330
+ <button id="quiz-close-button">Tutup</button>
331
+ <div> <!-- Group navigation/submit buttons -->
332
+ <button id="quiz-back-button" class="control-button" disabled>← Back</button>
333
+ <button id="quiz-submit-button" class="control-button">Submit Jawaban</button>
334
+ <button id="quiz-next-button" class="control-button">Next →</button>
335
+ </div>
336
+ </div>
337
+ </div>
338
+
339
+ <!-- Tooltip Element -->
340
+ <div id="tooltip"></div>
341
+
342
+ <script>
343
+ // --- Konfigurasi Dasar Three.js ---
344
+ let scene, camera, renderer, controls;
345
+ let raycaster, mouse;
346
+ const container = document.getElementById('canvas-container');
347
+ const infoPanel = {
348
+ step: document.getElementById('current-step'),
349
+ dimensions: document.getElementById('data-dimensions'),
350
+ inputName: document.getElementById('input-name'),
351
+ };
352
+ const featureMapDisplay = document.getElementById('feature-map-display');
353
+ const featureMapCanvas = document.getElementById('feature-map-canvas');
354
+ const featureMapCtx = featureMapCanvas.getContext('2d');
355
+ let tooltipElement; // Variable for the tooltip
356
+ let modalBackdrop, modalElement, modalTitle, modalBlockName, modalBlockDimensions, modalBlockDescription, modalCloseButton;
357
+ let quizModalBackdrop, quizModal, quizButton, quizQuestionEl, quizOptionsEl, quizFeedbackEl, quizScoreEl, quizSubmitButton, quizCloseButton, quizResultsEl, quizFinalScoreEl, quizQuestionContainer; // Added quizQuestionContainer
358
+ let quizBackButton, quizNextButton; // Add variables for new buttons
359
+
360
+ // --- State Simulasi ---
361
+ let currentStepIndex = -1; // Belum dimulai
362
+ let simulationSteps = []; // Akan diisi dengan info tiap langkah
363
+ let networkObjects = {}; // Menyimpan objek Three.js untuk tiap layer/blok
364
+ let skipConnectionLines = []; // Menyimpan garis untuk skip connections
365
+ let isAutoRunning = false;
366
+ let autoRunInterval = null;
367
+ const defaultBlockColor = 0x0077cc; // Biru
368
+ const activeBlockColor = 0xffcc00; // Kuning
369
+ const skipConnectionColor = 0x00ff00; // Hijau
370
+ let isQuizActive = false;
371
+ let currentQuizQuestionIndex = 0;
372
+ let quizScore = 0;
373
+ let quizQuestions = []; // Will be populated in init
374
+ let quizAnswerSubmitted = false; // Track if current answer was submitted
375
+
376
+ // --- Data Sampel (Placeholder) ---
377
+ const sampleInputs = {
378
+ sample1: { name: "Sel Darah (Contoh)", initialDim: "128x128x3" },
379
+ sample2: { name: "Citra Medis (Contoh)", initialDim: "256x256x1" },
380
+ };
381
+ let currentInput = sampleInputs.sample1; // Default
382
+
383
+ // --- Elemen Kontrol ---
384
+ const inputSelect = document.getElementById('input-select');
385
+ const resetButton = document.getElementById('reset-button');
386
+ const prevStepButton = document.getElementById('prev-step-button');
387
+ const nextStepButton = document.getElementById('next-step-button');
388
+ const runAutoButton = document.getElementById('run-auto-button');
389
+
390
+ // --- Inisialisasi ---
391
+ function init() {
392
+ // 1. Scene
393
+ scene = new THREE.Scene();
394
+ scene.background = new THREE.Color(0x1a202c); // Warna latar belakang gelap (gray-900)
395
+
396
+ // 2. Camera
397
+ camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
398
+ camera.position.z = 15; // Mundurkan kamera
399
+ camera.position.y = 5; // Naikkan sedikit
400
+
401
+ // 3. Renderer
402
+ renderer = new THREE.WebGLRenderer({ antialias: true });
403
+ renderer.setSize(container.clientWidth, container.clientHeight);
404
+ container.appendChild(renderer.domElement);
405
+
406
+ // 4. Controls
407
+ controls = new THREE.OrbitControls(camera, renderer.domElement);
408
+ controls.enableDamping = true; // Efek halus saat rotasi
409
+ controls.dampingFactor = 0.1;
410
+ controls.screenSpacePanning = false; // Batasi panning pada bidang xz
411
+
412
+ // 5. Lighting
413
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
414
+ scene.add(ambientLight);
415
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
416
+ directionalLight.position.set(5, 10, 7.5);
417
+ scene.add(directionalLight);
418
+
419
+ // 6. Raycaster for interaction
420
+ raycaster = new THREE.Raycaster();
421
+ mouse = new THREE.Vector2();
422
+
423
+ // --- MOVED: Get Control, Tooltip and Modal Elements ---
424
+ // Ensure these are defined before resetSimulation is called
425
+ tooltipElement = document.getElementById('tooltip');
426
+ modalBackdrop = document.getElementById('detail-modal-backdrop');
427
+ modalElement = document.getElementById('detail-modal');
428
+ modalTitle = document.getElementById('modal-title');
429
+ modalBlockName = document.getElementById('modal-block-name');
430
+ modalBlockDimensions = document.getElementById('modal-block-dimensions');
431
+ modalBlockDescription = document.getElementById('modal-block-description');
432
+ modalCloseButton = document.getElementById('modal-close-button');
433
+
434
+ quizModalBackdrop = document.getElementById('quiz-modal-backdrop');
435
+ quizModal = document.getElementById('quiz-modal');
436
+ quizButton = document.getElementById('quiz-button'); // Assign quizButton here
437
+ quizQuestionEl = document.getElementById('quiz-question');
438
+ quizOptionsEl = document.getElementById('quiz-options');
439
+ quizFeedbackEl = document.getElementById('quiz-feedback');
440
+ quizScoreEl = document.getElementById('quiz-score');
441
+ quizSubmitButton = document.getElementById('quiz-submit-button');
442
+ quizCloseButton = document.getElementById('quiz-close-button');
443
+ quizResultsEl = document.getElementById('quiz-results');
444
+ quizFinalScoreEl = document.getElementById('quiz-final-score');
445
+ quizQuestionContainer = document.getElementById('quiz-question-container'); // Get question container
446
+ quizBackButton = document.getElementById('quiz-back-button'); // Get back button
447
+ quizNextButton = document.getElementById('quiz-next-button'); // Get next button
448
+ // --- END MOVED SECTION ---
449
+
450
+ // --- Add Listeners for Modals (Detail & Quiz Internal) ---
451
+ // Add listeners to close detail modal
452
+ modalCloseButton.addEventListener('click', hideModal);
453
+ modalBackdrop.addEventListener('click', hideModal); // Close on backdrop click
454
+
455
+ // Add Quiz Modal Internal Listeners (Added only ONCE here)
456
+ quizCloseButton.addEventListener('click', endQuiz);
457
+ quizModalBackdrop.addEventListener('click', endQuiz); // Also close on backdrop click
458
+ quizSubmitButton.addEventListener('click', handleQuizSubmit);
459
+ quizBackButton.addEventListener('click', handleQuizBack); // Add listener for back
460
+ quizNextButton.addEventListener('click', handleQuizNext); // Add listener for next
461
+ // --- END Add Listeners ---
462
+
463
+
464
+ // Disable Quiz button initially
465
+ if (quizButton) { // Check if quizButton exists before disabling
466
+ quizButton.disabled = true;
467
+ quizButton.textContent = "Memuat Quiz..."; // Indicate loading
468
+ }
469
+
470
+
471
+ // 7. Build the U-Net visualization & Set Initial State
472
+ buildUNetArchitecture();
473
+ addLabels(); // Panggil fungsi untuk menambahkan label setelah arsitektur dibuat
474
+ defineSimulationSteps(); // Definisikan langkah simulasi setelah arsitektur dibuat
475
+ resetSimulation(); // Set ke state awal - NOW safe to call updateButtonStates
476
+
477
+ // 8. Event Listeners (Simulation Controls)
478
+ window.addEventListener('resize', onWindowResize, false);
479
+ container.addEventListener('click', onCanvasClick, false); // Re-enable click listener
480
+ container.addEventListener('mousemove', onCanvasMouseMove, false); // Add mousemove listener
481
+ resetButton.addEventListener('click', resetSimulation);
482
+ prevStepButton.addEventListener('click', previousStep);
483
+ nextStepButton.addEventListener('click', nextStep);
484
+ runAutoButton.addEventListener('click', toggleAutoRun);
485
+ inputSelect.addEventListener('change', handleInputChange);
486
+
487
+ // REMOVED Quiz Listeners from here - Moved up or added in fetch
488
+
489
+ // --- Load Quiz Questions ---
490
+ fetch('quiz_questions.json') // Path relative to the HTML file
491
+ .then(response => {
492
+ if (!response.ok) {
493
+ throw new Error(`HTTP error! status: ${response.status}`);
494
+ }
495
+ return response.json();
496
+ })
497
+ .then(data => {
498
+ quizQuestions = data;
499
+ console.log("Quiz questions loaded successfully:", quizQuestions.length, "questions");
500
+ // Enable Quiz button and set up its main listener only after loading
501
+ if (quizButton && quizQuestions.length > 0) {
502
+ quizButton.disabled = false;
503
+ quizButton.textContent = "Quiz";
504
+ quizButton.addEventListener('click', startQuiz); // Add listener for the main quiz button HERE
505
+ updateQuizScoreDisplay(); // Initialize score display (0/N)
506
+ } else if (quizButton) {
507
+ quizButton.textContent = "Quiz Kosong";
508
+ console.warn("Quiz questions loaded, but the array is empty.");
509
+ }
510
+ })
511
+ .catch(error => {
512
+ console.error("Error loading quiz questions:", error);
513
+ if (quizButton) {
514
+ quizButton.textContent = "Quiz Gagal Dimuat";
515
+ // Keep button disabled
516
+ }
517
+ // Optionally display an error message to the user in the quiz modal later
518
+ quizQuestionEl.textContent = "Gagal memuat pertanyaan quiz. Periksa file 'quiz_questions.json'.";
519
+ });
520
+
521
+ // Start the animation loop
522
+ animate();
523
+ }
524
+
525
+ // --- Helper Function to Create Text Labels (Sprites) ---
526
+ function createLabel(text, position, color = '#ffffff', fontSize = 48, backgroundPadding = 10) {
527
+ const canvas = document.createElement('canvas');
528
+ const context = canvas.getContext('2d');
529
+ context.font = `Bold ${fontSize}px Arial`;
530
+
531
+ // Ukur teks untuk menentukan ukuran canvas
532
+ const textMetrics = context.measureText(text);
533
+ const textWidth = textMetrics.width;
534
+ canvas.width = textWidth + backgroundPadding * 2;
535
+ canvas.height = fontSize + backgroundPadding * 2;
536
+
537
+ // Gambar background (opsional, bisa transparan)
538
+ // context.fillStyle = 'rgba(0, 0, 0, 0.5)'; // Background semi-transparan
539
+ // context.fillRect(0, 0, canvas.width, canvas.height);
540
+
541
+ // Gambar teks
542
+ context.font = `Bold ${fontSize}px Arial`; // Set ulang font setelah resize canvas
543
+ context.fillStyle = color;
544
+ context.textAlign = 'center';
545
+ context.textBaseline = 'middle';
546
+ context.fillText(text, canvas.width / 2, canvas.height / 2);
547
+
548
+ const texture = new THREE.CanvasTexture(canvas);
549
+ texture.needsUpdate = true;
550
+
551
+ const material = new THREE.SpriteMaterial({ map: texture, transparent: true });
552
+ const sprite = new THREE.Sprite(material);
553
+
554
+ // Skala sprite agar sesuai dengan scene
555
+ const scaleFactor = 0.01; // Sesuaikan nilai ini untuk ukuran label
556
+ sprite.scale.set(canvas.width * scaleFactor, canvas.height * scaleFactor, 1);
557
+
558
+ sprite.position.copy(position);
559
+ scene.add(sprite);
560
+ return sprite;
561
+ }
562
+
563
+ // --- Menambahkan Label ke Scene ---
564
+ function addLabels() {
565
+ const labelYOffset = 4; // Jarak vertikal label dari blok teratas
566
+ const encoderTopY = networkObjects['Encoder_1'].position.y;
567
+ const decoderTopY = networkObjects['Decoder_4'].position.y;
568
+ const inputY = networkObjects['Input_Image'].position.y;
569
+ const outputY = networkObjects['Output_Segmentation'].position.y;
570
+
571
+ // Label Input
572
+ createLabel("Input", new THREE.Vector3(
573
+ networkObjects['Input_Image'].position.x,
574
+ inputY + labelYOffset,
575
+ 0
576
+ ));
577
+
578
+ // Label Encoder Path
579
+ createLabel("Encoder Path", new THREE.Vector3(
580
+ networkObjects['Encoder_1'].position.x, // X dari blok encoder pertama
581
+ encoderTopY + labelYOffset, // Y di atas blok encoder pertama
582
+ 0
583
+ ));
584
+
585
+ // Label Bottleneck
586
+ createLabel("Bottleneck", new THREE.Vector3(
587
+ networkObjects['Bottleneck'].position.x,
588
+ networkObjects['Bottleneck'].position.y + labelYOffset * 0.6, // Sedikit lebih dekat ke bottleneck
589
+ 0
590
+ ));
591
+
592
+ // Label Decoder Path
593
+ createLabel("Decoder Path", new THREE.Vector3(
594
+ networkObjects['Decoder_4'].position.x, // X dari blok decoder terakhir (paling atas)
595
+ decoderTopY + labelYOffset, // Y di atas blok decoder terakhir
596
+ 0
597
+ ));
598
+
599
+ // Label Output
600
+ createLabel("Output", new THREE.Vector3(
601
+ networkObjects['Output_Segmentation'].position.x,
602
+ outputY + labelYOffset,
603
+ 0
604
+ ));
605
+ }
606
+
607
+ // --- Membangun Visualisasi Arsitektur U-Net ---
608
+ function buildUNetArchitecture() {
609
+ // Ukuran dan posisi relatif (bisa disesuaikan)
610
+ const blockDepth = 0.5;
611
+ const levelHeight = 3; // Jarak vertikal antar level
612
+ const horizontalSpacing = 4; // Jarak horizontal antar blok di level yang sama
613
+ const contractingPathX = -horizontalSpacing * 1.5;
614
+ const expandingPathX = horizontalSpacing * 1.5;
615
+
616
+ // Helper function to create a block (layer group)
617
+ function createBlock(name, width, height, color = defaultBlockColor) {
618
+ const geometry = new THREE.BoxGeometry(width, height, blockDepth);
619
+ const material = new THREE.MeshStandardMaterial({ color: color, metalness: 0.3, roughness: 0.6 });
620
+ const mesh = new THREE.Mesh(geometry, material);
621
+ mesh.name = name; // Nama untuk identifikasi
622
+
623
+ // --- Enhanced userData.info ---
624
+ let description = "Informasi detail belum tersedia.";
625
+ if (name.startsWith("Encoder")) {
626
+ description = `Blok Encoder (${name.split('_')[1]}):\n- Bagian ini (Contracting Path) secara bertahap mengekstrak fitur dari gambar input sambil mengurangi ukurannya (resolusi spasial).\n- Dalam simulasi ini: Melakukan 2x (Konvolusi 3x3 + ReLU) untuk deteksi pola, diikuti Max Pooling 2x2 (kecuali blok pertama) untuk mengurangi dimensi spasial (misal, 128x128 -> 64x64) dan biasanya menggandakan jumlah fitur (channels).\n- Fitur yang diekstrak di level ini disimpan untuk Skip Connection ke blok Decoder yang sesuai.`;
627
+ } else if (name === "Bottleneck") {
628
+ description = "Bottleneck:\n- Lapisan terdalam dan terkecil dalam arsitektur U-Net, menghubungkan Encoder dan Decoder.\n- Dalam simulasi ini: Menerima output dari Encoder terakhir setelah Max Pooling, lalu melakukan 2x (Konvolusi 3x3 + ReLU).\n- Menangkap informasi kontekstual tingkat tinggi (fitur paling abstrak) pada resolusi terendah (misal, 8x8).";
629
+ } else if (name.startsWith("Decoder")) {
630
+ description = `Blok Decoder (${name.split('_')[1]}):\n- Bagian ini (Expanding Path) secara bertahap membangun kembali peta segmentasi, meningkatkan resolusi spasial.\n- Dalam simulasi ini: Melakukan Up-Convolution 2x2 (Transposed Convolution) untuk menggandakan dimensi spasial (misal, 16x16 -> 32x32) dan biasanya mengurangi separuh jumlah fitur.\n- Menggabungkan (concatenate) hasilnya dengan fitur dari Skip Connection Encoder yang sesuai (misal, Decoder 1 dengan Encoder 4).\n- Melakukan 2x (Konvolusi 3x3 + ReLU) pada fitur gabungan untuk menyempurnakan representasi.`;
631
+ } else if (name === "Output_Segmentation") {
632
+ description = "Output Layer:\n- Lapisan terakhir dari U-Net yang menghasilkan peta segmentasi akhir.\n- Dalam simulasi ini: Menerima input dari blok Decoder terakhir (misal, 128x128x64) dan melakukan Konvolusi 1x1 untuk memetakan fitur ke jumlah kelas yang diinginkan (disini 1 channel untuk segmentasi biner, menghasilkan output 128x128x1).\n- Setiap piksel pada output mewakili probabilitas kelas (misal, objek vs latar belakang).";
633
+ } else if (name === "Input_Image") {
634
+ description = "Input:\n- Gambar asli yang dimasukkan ke dalam jaringan U-Net.\n- Dalam simulasi ini: Dimensi awal (misal, 128x128x3 atau 256x256x1) ditentukan oleh pilihan input sampel.";
635
+ }
636
+
637
+ mesh.userData = {
638
+ info: description, // Use the detailed description
639
+ dimensions: "N/A" // Akan diupdate saat simulasi
640
+ };
641
+ scene.add(mesh);
642
+ networkObjects[name] = mesh;
643
+ return mesh;
644
+ }
645
+
646
+ // --- Contracting Path (Encoder) ---
647
+ const enc1 = createBlock("Encoder_1", 3, 3);
648
+ enc1.position.set(contractingPathX, levelHeight * 2, 0);
649
+ const enc2 = createBlock("Encoder_2", 2.5, 2.5);
650
+ enc2.position.set(contractingPathX, levelHeight * 1, 0);
651
+ const enc3 = createBlock("Encoder_3", 2, 2);
652
+ enc3.position.set(contractingPathX, levelHeight * 0, 0);
653
+ const enc4 = createBlock("Encoder_4", 1.5, 1.5);
654
+ enc4.position.set(contractingPathX, -levelHeight * 1, 0);
655
+
656
+ // --- Bottleneck ---
657
+ const bottleneck = createBlock("Bottleneck", 1, 1);
658
+ bottleneck.position.set(0, -levelHeight * 2, 0);
659
+
660
+ // --- Expanding Path (Decoder) ---
661
+ const dec1 = createBlock("Decoder_1", 1.5, 1.5);
662
+ dec1.position.set(expandingPathX, -levelHeight * 1, 0);
663
+ const dec2 = createBlock("Decoder_2", 2, 2);
664
+ dec2.position.set(expandingPathX, levelHeight * 0, 0);
665
+ const dec3 = createBlock("Decoder_3", 2.5, 2.5);
666
+ dec3.position.set(expandingPathX, levelHeight * 1, 0);
667
+ const dec4 = createBlock("Decoder_4", 3, 3);
668
+ dec4.position.set(expandingPathX, levelHeight * 2, 0);
669
+
670
+ // --- Output Layer ---
671
+ const output = createBlock("Output_Segmentation", 3, 3, 0x999999); // Warna abu-abu
672
+ output.position.set(expandingPathX + horizontalSpacing, levelHeight * 2, 0);
673
+
674
+ // --- Input Layer (Visual Placeholder) ---
675
+ const inputVis = createBlock("Input_Image", 3, 3, 0xcccccc); // Warna abu-abu terang
676
+ inputVis.position.set(contractingPathX - horizontalSpacing, levelHeight * 2, 0);
677
+ networkObjects["Input_Image"] = inputVis; // Tambahkan ke networkObjects
678
+
679
+ // Atur posisi kamera agar fokus ke tengah arsitektur
680
+ camera.lookAt(scene.position);
681
+ }
682
+
683
+ // --- Mendefinisikan Langkah-langkah Simulasi ---
684
+ function defineSimulationSteps() {
685
+ // Urutan nama blok sesuai aliran data
686
+ // Format: { blockName: "NamaBlok", dimension: "HxWx C", featureMapVis: function() }
687
+ simulationSteps = [
688
+ { blockName: "Input_Image", dimension: currentInput.initialDim, info: "Gambar Input Awal", featureMapVis: generateSimpleFeatureMap },
689
+ { blockName: "Encoder_1", dimension: "128x128x64", info: "Encoder Blok 1 (Conv, ReLU)", featureMapVis: generateSimpleFeatureMap },
690
+ { blockName: "Encoder_2", dimension: "64x64x128", info: "Encoder Blok 2 (MaxPool, Conv, ReLU)", featureMapVis: generateSimpleFeatureMap },
691
+ { blockName: "Encoder_3", dimension: "32x32x256", info: "Encoder Blok 3 (MaxPool, Conv, ReLU)", featureMapVis: generateSimpleFeatureMap },
692
+ { blockName: "Encoder_4", dimension: "16x16x512", info: "Encoder Blok 4 (MaxPool, Conv, ReLU)", featureMapVis: generateSimpleFeatureMap },
693
+ { blockName: "Bottleneck", dimension: "8x8x1024", info: "Bottleneck (MaxPool, Conv, ReLU)", featureMapVis: generateSimpleFeatureMap },
694
+ // Skip connection 1 (Enc4 -> Dec1)
695
+ { blockName: "Decoder_1", skipSource: "Encoder_4", dimension: "16x16x512", info: "Decoder Blok 1 (UpConv, Concat, Conv, ReLU)", featureMapVis: generateSimpleFeatureMap },
696
+ // Skip connection 2 (Enc3 -> Dec2)
697
+ { blockName: "Decoder_2", skipSource: "Encoder_3", dimension: "32x32x256", info: "Decoder Blok 2 (UpConv, Concat, Conv, ReLU)", featureMapVis: generateSimpleFeatureMap },
698
+ // Skip connection 3 (Enc2 -> Dec3)
699
+ { blockName: "Decoder_3", skipSource: "Encoder_2", dimension: "64x64x128", info: "Decoder Blok 3 (UpConv, Concat, Conv, ReLU)", featureMapVis: generateSimpleFeatureMap },
700
+ // Skip connection 4 (Enc1 -> Dec4)
701
+ { blockName: "Decoder_4", skipSource: "Encoder_1", dimension: "128x128x64", info: "Decoder Blok 4 (UpConv, Concat, Conv, ReLU)", featureMapVis: generateSimpleFeatureMap },
702
+ { blockName: "Output_Segmentation", dimension: "128x128x1", info: "Output Akhir (Segmentasi)", featureMapVis: generateSegmentationMap } // Misal 1 channel output
703
+ ];
704
+ }
705
+
706
+ // --- Fungsi Simulasi ---
707
+ function resetSimulation() {
708
+ if (isQuizActive) return; // Don't reset if quiz is active
709
+ stopAutoRun(); // Hentikan auto run jika sedang berjalan
710
+ currentStepIndex = -1; // Kembali ke sebelum start
711
+ clearHighlightsAndConnections();
712
+ updateInfoPanel();
713
+ // Set input visual dimension
714
+ if (networkObjects["Input_Image"]) {
715
+ networkObjects["Input_Image"].userData.dimensions = currentInput.initialDim;
716
+ }
717
+ featureMapDisplay.style.display = 'none'; // Sembunyikan display feature map
718
+ // Also reset quiz state if needed, though endQuiz handles most of it
719
+ isQuizActive = false; // Ensure quiz is marked as inactive
720
+ hideQuizModal(); // Ensure quiz modal is hidden on full reset
721
+ updateButtonStates(); // Update buttons after ensuring quiz is inactive
722
+ }
723
+
724
+ function nextStep() {
725
+ if (isQuizActive) return; // Don't allow simulation steps during quiz
726
+ if (currentStepIndex < simulationSteps.length - 1) {
727
+ currentStepIndex++;
728
+ updateSimulationState();
729
+ }
730
+ updateButtonStates();
731
+ }
732
+
733
+ function previousStep() {
734
+ if (isQuizActive) return; // Don't allow simulation steps during quiz
735
+ if (currentStepIndex > 0) { // Tidak bisa kembali sebelum langkah pertama
736
+ currentStepIndex--;
737
+ updateSimulationState();
738
+ } else if (currentStepIndex === 0) { // Kembali dari langkah pertama ke state awal
739
+ resetSimulation(); // Reset penuh jika kembali dari langkah pertama
740
+ }
741
+ updateButtonStates();
742
+ }
743
+
744
+ function updateSimulationState() {
745
+ clearHighlightsAndConnections(); // Hapus highlight & garis sebelumnya
746
+
747
+ const step = simulationSteps[currentStepIndex];
748
+ const currentBlock = networkObjects[step.blockName];
749
+
750
+ if (currentBlock) {
751
+ // 1. Highlight block aktif (Hanya dengan glow)
752
+ // currentBlock.material.color.setHex(activeBlockColor); // <-- Tidak mengubah warna dasar
753
+ currentBlock.material.emissive.setHex(activeBlockColor); // Tetap buat memancarkan cahaya kuning
754
+ currentBlock.material.emissiveIntensity = 0.6; // Naikkan intensitas glow sedikit
755
+
756
+ // 2. Update info dimensi di userData block (untuk hover/klik nanti)
757
+ currentBlock.userData.dimensions = step.dimension;
758
+
759
+ // 3. Update info panel utama
760
+ updateInfoPanel(step);
761
+
762
+ // 4. Tampilkan visualisasi feature map
763
+ if (step.featureMapVis) {
764
+ step.featureMapVis(featureMapCtx, step.dimension); // Panggil fungsi visualisasi
765
+ featureMapDisplay.style.display = 'block';
766
+ } else {
767
+ featureMapDisplay.style.display = 'none';
768
+ }
769
+
770
+ // 5. Gambar skip connection jika ada
771
+ if (step.skipSource) {
772
+ const sourceBlock = networkObjects[step.skipSource];
773
+ if (sourceBlock) {
774
+ drawSkipConnection(sourceBlock, currentBlock);
775
+ }
776
+ }
777
+ } else {
778
+ console.warn(`Objek untuk blok "${step.blockName}" tidak ditemukan.`);
779
+ featureMapDisplay.style.display = 'none'; // Sembunyikan jika blok tidak ada
780
+ }
781
+ }
782
+
783
+ function clearHighlightsAndConnections() {
784
+ // Reset warna dan glow semua block
785
+ for (const name in networkObjects) {
786
+ const obj = networkObjects[name];
787
+ // Set warna dasar berdasarkan tipe blok (pastikan ini benar)
788
+ if (name === "Input_Image") {
789
+ obj.material.color.setHex(0xcccccc); // Abu-abu terang untuk input
790
+ } else if (name === "Output_Segmentation") {
791
+ obj.material.color.setHex(0x999999); // Abu-abu untuk output
792
+ } else {
793
+ obj.material.color.setHex(defaultBlockColor); // Biru default untuk blok proses
794
+ }
795
+ obj.material.emissive.setHex(0x000000); // Matikan glow
796
+ obj.material.emissiveIntensity = 0;
797
+ }
798
+ // Hapus garis skip connection
799
+ skipConnectionLines.forEach(line => scene.remove(line));
800
+ skipConnectionLines = [];
801
+ }
802
+
803
+ function drawSkipConnection(sourceBlock, targetBlock) {
804
+ const points = [];
805
+ // Titik awal sedikit di depan source block
806
+ const startPoint = sourceBlock.position.clone();
807
+ startPoint.z += 0.3; // Sedikit offset Z
808
+ // Titik akhir sedikit di depan target block
809
+ const endPoint = targetBlock.position.clone();
810
+ endPoint.z += 0.3; // Sedikit offset Z
811
+
812
+ // Titik tengah untuk membuat lengkungan (opsional)
813
+ const midPointX = (startPoint.x + endPoint.x) / 2;
814
+ const midPointY = (startPoint.y + endPoint.y) / 2;
815
+ const midPointZ = startPoint.z + 2; // Lengkungkan ke depan
816
+
817
+ // Gunakan kurva untuk garis melengkung
818
+ const curve = new THREE.QuadraticBezierCurve3(
819
+ startPoint,
820
+ new THREE.Vector3(midPointX, midPointY, midPointZ),
821
+ endPoint
822
+ );
823
+
824
+ const curvePoints = curve.getPoints( 50 ); // Jumlah segmen garis
825
+ const geometry = new THREE.BufferGeometry().setFromPoints( curvePoints );
826
+ const material = new THREE.LineBasicMaterial( { color: skipConnectionColor, linewidth: 2 } ); // Buat garis lebih tebal
827
+
828
+ const line = new THREE.Line( geometry, material );
829
+ scene.add(line);
830
+ skipConnectionLines.push(line); // Simpan referensi untuk dihapus nanti
831
+ }
832
+
833
+ function updateInfoPanel(step = null) {
834
+ if (step) {
835
+ infoPanel.step.textContent = `${currentStepIndex + 1}. ${step.info || step.blockName}`;
836
+ infoPanel.dimensions.textContent = step.dimension;
837
+ } else { // Kondisi reset
838
+ infoPanel.step.textContent = "Belum dimulai";
839
+ infoPanel.dimensions.textContent = "-";
840
+ }
841
+ infoPanel.inputName.textContent = currentInput.name;
842
+ }
843
+
844
+ function updateButtonStates() {
845
+ const simActive = !isQuizActive; // Simulation is active if quiz is NOT active
846
+ prevStepButton.disabled = !simActive || currentStepIndex <= -1;
847
+ nextStepButton.disabled = !simActive || currentStepIndex >= simulationSteps.length - 1;
848
+ runAutoButton.textContent = isAutoRunning ? "Hentikan Otomatis" : "Jalankan Otomatis";
849
+ runAutoButton.disabled = !simActive || isAutoRunning; // Disable if quiz active OR if already auto-running
850
+ resetButton.disabled = !simActive;
851
+ inputSelect.disabled = !simActive;
852
+
853
+ // Only update quizButton state if it has been successfully loaded and enabled initially
854
+ if (quizButton && quizQuestions.length > 0) { // Check if quiz is loadable
855
+ quizButton.disabled = isAutoRunning || isQuizActive; // Disable quiz if auto-run active OR quiz already active
856
+ } else if (quizButton) {
857
+ quizButton.disabled = true; // Keep disabled if loading failed or no questions
858
+ }
859
+ }
860
+
861
+ function toggleAutoRun() {
862
+ if (isQuizActive) return; // Don't toggle if quiz is active
863
+ if (isAutoRunning) {
864
+ stopAutoRun();
865
+ } else {
866
+ startAutoRun();
867
+ }
868
+ // updateButtonStates() is called within start/stopAutoRun
869
+ }
870
+
871
+ function startAutoRun() {
872
+ if (isQuizActive) return; // Don't start if quiz is active
873
+ // ... (rest of startAutoRun logic remains largely the same) ...
874
+ isAutoRunning = true;
875
+ // runAutoButton.textContent = "Hentikan Otomatis"; // Set by updateButtonStates
876
+ // Disable tombol lain saat auto run (handled by updateButtonStates)
877
+ // prevStepButton.disabled = true;
878
+ // nextStepButton.disabled = true;
879
+ // resetButton.disabled = true;
880
+ // inputSelect.disabled = true;
881
+ // quizButton.disabled = true; // Handled by updateButtonStates
882
+
883
+ // Ensure the first step runs immediately if starting from reset state
884
+ if (currentStepIndex === -1) {
885
+ nextStep(); // This already calls updateButtonStates
886
+ } else {
887
+ updateButtonStates(); // Update buttons if not starting from -1
888
+ }
889
+
890
+
891
+ autoRunInterval = setInterval(() => {
892
+ // Stop condition check moved inside nextStep/resetSimulation logic
893
+ // if (!isAutoRunning || isQuizActive) { // Stop if quiz starts or manually stopped
894
+ // stopAutoRun(); // Ensure interval is cleared
895
+ // return;
896
+ // }
897
+
898
+ if (currentStepIndex < simulationSteps.length - 1) {
899
+ nextStep(); // Calls updateButtonStates
900
+ } else {
901
+ // Reached the end, loop back to the beginning
902
+ currentStepIndex = -1; // Reset index before next step
903
+ clearHighlightsAndConnections(); // Clear visuals before starting over
904
+ updateInfoPanel(); // Update panel to initial state briefly
905
+ // No need to call resetSimulation() fully, just loop
906
+ setTimeout(nextStep, 100); // Small delay before starting the first step again
907
+ }
908
+ }, 1000); // Interval 1 detik per langkah
909
+ // updateButtonStates(); // Called after first nextStep or immediately if not starting from -1
910
+ }
911
+
912
+ function stopAutoRun() {
913
+ if (autoRunInterval) {
914
+ clearInterval(autoRunInterval);
915
+ autoRunInterval = null; // Clear interval ID
916
+ }
917
+ isAutoRunning = false;
918
+ // runAutoButton.textContent = "Jalankan Otomatis"; // Set in updateButtonStates
919
+ // Enable tombol lain lagi (handled by updateButtonStates)
920
+ updateButtonStates(); // Update buttons state after stopping
921
+ }
922
+
923
+ function handleInputChange(event) {
924
+ if (isQuizActive) return; // Don't change input during quiz
925
+ const selectedValue = event.target.value;
926
+ currentInput = sampleInputs[selectedValue];
927
+ // Perbarui definisi langkah simulasi karena dimensi awal berubah
928
+ defineSimulationSteps();
929
+ resetSimulation(); // Reset simulasi dengan input baru (calls updateButtonStates)
930
+ }
931
+
932
+
933
+ // --- Visualisasi Feature Map Sederhana (Placeholder) ---
934
+ function generateSimpleFeatureMap(ctx, dimensions) {
935
+ const size = 128; // Ukuran canvas
936
+ ctx.clearRect(0, 0, size, size);
937
+ ctx.fillStyle = '#f0f0f0'; // Latar belakang
938
+ ctx.fillRect(0, 0, size, size);
939
+
940
+ // Parse dimensi (contoh: "128x128x64")
941
+ const dims = dimensions.split('x').map(Number);
942
+ const channels = dims[2] || 1;
943
+
944
+ // Gambar grid acak sederhana untuk representasi
945
+ const cellSize = 4;
946
+ for (let y = 0; y < size; y += cellSize) {
947
+ for (let x = 0; x < size; x += cellSize) {
948
+ // Warna acak berdasarkan channel (simplifikasi)
949
+ const grayValue = Math.floor(Math.random() * 200) + 55; // Nilai abu-abu acak
950
+ const blueTint = Math.min(255, Math.floor(Math.random() * channels * 0.5)); // Sedikit warna biru berdasarkan channel
951
+ ctx.fillStyle = `rgb(${grayValue}, ${grayValue}, ${Math.min(255, grayValue + blueTint)})`;
952
+ ctx.fillRect(x, y, cellSize, cellSize);
953
+ }
954
+ }
955
+ ctx.strokeStyle = '#ccc';
956
+ ctx.lineWidth = 0.5;
957
+ // Optional: Draw grid lines
958
+ // for (let i = 0; i <= size; i += cellSize*2) {
959
+ // ctx.beginPath();
960
+ // ctx.moveTo(i, 0);
961
+ // ctx.lineTo(i, size);
962
+ // ctx.stroke();
963
+ // ctx.beginPath();
964
+ // ctx.moveTo(0, i);
965
+ // ctx.lineTo(size, i);
966
+ // ctx.stroke();
967
+ // }
968
+ }
969
+
970
+ // --- Visualisasi Output Segmentasi (Placeholder) ---
971
+ function generateSegmentationMap(ctx, dimensions) {
972
+ const size = 128;
973
+ ctx.clearRect(0, 0, size, size);
974
+
975
+ // Gambar pola sederhana (misal: lingkaran di tengah)
976
+ ctx.fillStyle = '#333'; // Background
977
+ ctx.fillRect(0, 0, size, size);
978
+ ctx.fillStyle = '#00ff00'; // Warna segmentasi (hijau)
979
+ ctx.beginPath();
980
+ ctx.arc(size / 2, size / 2, size / 3, 0, Math.PI * 2); // Lingkaran
981
+ ctx.fill();
982
+ ctx.fillStyle = '#555';
983
+ ctx.font = '10px Arial';
984
+ ctx.textAlign = 'center';
985
+ ctx.fillText('Contoh Output', size/2, size - 5);
986
+ }
987
+
988
+
989
+ // --- Event Handling ---
990
+ function onWindowResize() {
991
+ camera.aspect = container.clientWidth / container.clientHeight;
992
+ camera.updateProjectionMatrix();
993
+ renderer.setSize(container.clientWidth, container.clientHeight);
994
+ }
995
+
996
+ // Function to handle click - Now opens the detail modal
997
+ function onCanvasClick(event) {
998
+ if (isQuizActive) return; // Ignore clicks if quiz is active
999
+
1000
+ // Calculate mouse position in normalized device coordinates (-1 to +1)
1001
+ mouse.x = (event.clientX / container.clientWidth) * 2 - 1;
1002
+ mouse.y = - (event.clientY / container.clientHeight) * 2 + 1;
1003
+
1004
+ // Update the picking ray
1005
+ raycaster.setFromCamera(mouse, camera);
1006
+
1007
+ // Calculate objects intersecting the picking ray
1008
+ const intersects = raycaster.intersectObjects(Object.values(networkObjects), false); // Only check network blocks
1009
+
1010
+ if (intersects.length > 0) {
1011
+ const intersectedObject = intersects[0].object;
1012
+ if (tooltipElement) tooltipElement.style.display = 'none'; // Hide tooltip on click
1013
+ showModal(intersectedObject); // Call function to show detail modal
1014
+ } else {
1015
+ hideModal(); // Hide detail modal if clicking outside blocks (optional)
1016
+ }
1017
+ }
1018
+
1019
+ // Function to handle mouse move for tooltip
1020
+ function onCanvasMouseMove(event) {
1021
+ if (isQuizActive || modalElement.style.display === 'block') { // Hide tooltip if quiz active or detail modal shown
1022
+ if (tooltipElement) tooltipElement.style.display = 'none';
1023
+ return;
1024
+ }
1025
+ // Calculate mouse position relative to the container
1026
+ const rect = container.getBoundingClientRect();
1027
+ const canvasX = event.clientX - rect.left;
1028
+ const canvasY = event.clientY - rect.top;
1029
+
1030
+ // Calculate mouse position in normalized device coordinates (-1 to +1)
1031
+ mouse.x = (canvasX / container.clientWidth) * 2 - 1;
1032
+ mouse.y = - (canvasY / container.clientHeight) * 2 + 1;
1033
+
1034
+ // Update the picking ray
1035
+ raycaster.setFromCamera(mouse, camera);
1036
+
1037
+ // Calculate objects intersecting the picking ray
1038
+ const intersects = raycaster.intersectObjects(Object.values(networkObjects), false); // Only check network blocks
1039
+
1040
+ if (intersects.length > 0) {
1041
+ const intersectedObject = intersects[0].object;
1042
+
1043
+ // Update tooltip content and position
1044
+ if (tooltipElement) {
1045
+ tooltipElement.style.display = 'block';
1046
+ tooltipElement.style.left = `${event.clientX + 15}px`; // Position tooltip near cursor
1047
+ tooltipElement.style.top = `${event.clientY + 15}px`;
1048
+ // Keep tooltip concise - add click instruction
1049
+ tooltipElement.textContent = `Blok: ${intersectedObject.name}\nDimensi: ${intersectedObject.userData.dimensions}\n(Klik untuk detail)`;
1050
+ }
1051
+
1052
+ } else {
1053
+ // Hide tooltip if not hovering over a block
1054
+ if (tooltipElement) tooltipElement.style.display = 'none';
1055
+ }
1056
+ }
1057
+
1058
+ // --- Modal Functions (Detail Modal) ---
1059
+ function showModal(blockObject) {
1060
+ modalTitle.textContent = `Detail Blok: ${blockObject.name}`;
1061
+ modalBlockName.textContent = blockObject.name;
1062
+ modalBlockDimensions.textContent = blockObject.userData.dimensions || "N/A";
1063
+ modalBlockDescription.textContent = blockObject.userData.info || "Tidak ada deskripsi.";
1064
+
1065
+ modalElement.style.display = 'block';
1066
+ modalBackdrop.style.display = 'block';
1067
+ if (tooltipElement) tooltipElement.style.display = 'none'; // Hide tooltip when modal opens
1068
+ }
1069
+
1070
+ function hideModal() {
1071
+ modalElement.style.display = 'none';
1072
+ modalBackdrop.style.display = 'none';
1073
+ }
1074
+
1075
+
1076
+ // --- Quiz Functions ---
1077
+ function startQuiz() {
1078
+ console.log("startQuiz called.");
1079
+ if (isAutoRunning || quizQuestions.length === 0 || isQuizActive) {
1080
+ console.log("startQuiz aborted: isAutoRunning=", isAutoRunning, "quizQuestions.length=", quizQuestions.length, "isQuizActive=", isQuizActive);
1081
+ return;
1082
+ }
1083
+
1084
+ // Stop simulation if running
1085
+ if (isAutoRunning) {
1086
+ stopAutoRun();
1087
+ console.log("stopAutoRun called from startQuiz.");
1088
+ }
1089
+
1090
+ isQuizActive = true;
1091
+ currentQuizQuestionIndex = 0;
1092
+ quizScore = 0;
1093
+ quizAnswerSubmitted = false; // Reset submitted flag globally (will be handled per question display)
1094
+
1095
+ // --- HIDE SIMULATION VIEW ---
1096
+ console.log("Hiding simulation container...");
1097
+ container.style.display = 'none'; // Hide the main canvas container
1098
+ if (tooltipElement) {
1099
+ tooltipElement.style.display = 'none'; // Ensure tooltip is hidden
1100
+ }
1101
+ // --- END HIDE SIMULATION VIEW ---
1102
+
1103
+
1104
+ updateButtonStates(); // Disable simulation controls
1105
+ updateQuizScoreDisplay();
1106
+ displayQuizQuestion(currentQuizQuestionIndex); // Display first question (handles button states)
1107
+ quizResultsEl.style.display = 'none'; // Hide results area
1108
+ quizQuestionContainer.style.display = 'block'; // Show question area
1109
+ quizSubmitButton.onclick = handleQuizSubmit; // Ensure submit handler is attached
1110
+
1111
+ // Ensure correct buttons are visible/hidden initially
1112
+ quizSubmitButton.style.display = 'inline-block';
1113
+ quizBackButton.style.display = 'inline-block';
1114
+ quizNextButton.style.display = 'inline-block';
1115
+ quizCloseButton.style.display = 'inline-block'; // Keep close button always visible
1116
+
1117
+ console.log("Showing quiz modal and backdrop...");
1118
+ quizModal.style.display = 'block';
1119
+ quizModalBackdrop.style.display = 'block';
1120
+ // ... logging ...
1121
+ }
1122
+
1123
+ function endQuiz() {
1124
+ console.log("endQuiz called.");
1125
+ isQuizActive = false;
1126
+ hideQuizModal();
1127
+
1128
+ // --- SHOW SIMULATION VIEW ---
1129
+ console.log("Showing simulation container...");
1130
+ container.style.display = 'block'; // Show the main canvas container again
1131
+ // --- END SHOW SIMULATION VIEW ---
1132
+
1133
+ updateButtonStates(); // Re-enable simulation controls
1134
+ }
1135
+
1136
+ function hideQuizModal() {
1137
+ console.log("hideQuizModal called.");
1138
+ quizModal.style.display = 'none';
1139
+ quizModalBackdrop.style.display = 'none';
1140
+ }
1141
+
1142
+ function displayQuizQuestion(index) {
1143
+ // ... existing checks for question loading and index bounds ...
1144
+ if (index >= quizQuestions.length) {
1145
+ showQuizResults();
1146
+ return;
1147
+ }
1148
+
1149
+ const q = quizQuestions[index];
1150
+ console.log(`Displaying question ${index + 1}:`, q.question);
1151
+ quizQuestionEl.textContent = `Pertanyaan ${index + 1}: ${q.question}`;
1152
+ quizOptionsEl.innerHTML = ''; // Clear previous options
1153
+ quizFeedbackEl.textContent = ''; // Clear feedback
1154
+ quizFeedbackEl.className = 'quiz-feedback'; // Reset feedback style
1155
+ quizAnswerSubmitted = false; // Reset submitted flag when displaying/navigating
1156
+
1157
+ q.options.forEach((option, i) => {
1158
+ const optionId = `q${index}_option${i}`;
1159
+ const div = document.createElement('div');
1160
+ div.className = 'quiz-option';
1161
+ div.innerHTML = `
1162
+ <label for="${optionId}">
1163
+ <input type="checkbox" id="${optionId}" name="quizOption${index}" value="${i}">
1164
+ ${option}
1165
+ </label>
1166
+ `;
1167
+ quizOptionsEl.appendChild(div);
1168
+ });
1169
+
1170
+ // Update button states
1171
+ quizSubmitButton.textContent = "Submit Jawaban";
1172
+ quizSubmitButton.disabled = false; // Enable submit by default when navigating
1173
+ quizBackButton.disabled = (index === 0); // Disable back if first question
1174
+ quizNextButton.disabled = (index >= quizQuestions.length - 1); // Disable next if last question
1175
+
1176
+ quizQuestionContainer.style.display = 'block'; // Ensure question area is visible
1177
+ quizResultsEl.style.display = 'none'; // Ensure results area is hidden
1178
+ }
1179
+
1180
+ function handleQuizBack() {
1181
+ if (!isQuizActive || currentQuizQuestionIndex <= 0) return;
1182
+ currentQuizQuestionIndex--;
1183
+ console.log("Navigating back to question:", currentQuizQuestionIndex + 1);
1184
+ displayQuizQuestion(currentQuizQuestionIndex);
1185
+ }
1186
+
1187
+ function handleQuizNext() {
1188
+ if (!isQuizActive || currentQuizQuestionIndex >= quizQuestions.length - 1) return;
1189
+ currentQuizQuestionIndex++;
1190
+ console.log("Navigating next to question:", currentQuizQuestionIndex + 1);
1191
+ displayQuizQuestion(currentQuizQuestionIndex);
1192
+ }
1193
+
1194
+
1195
+ function handleQuizSubmit() {
1196
+ if (!isQuizActive || quizAnswerSubmitted) return; // Don't submit if already submitted for this view
1197
+ console.log("handleQuizSubmit called. currentQuestionIndex:", currentQuizQuestionIndex);
1198
+
1199
+ // Submit the current answer
1200
+ const selectedOptions = quizOptionsEl.querySelectorAll('input[type="checkbox"]:checked');
1201
+ if (selectedOptions.length === 0) {
1202
+ quizFeedbackEl.textContent = "Silakan pilih setidaknya satu jawaban.";
1203
+ quizFeedbackEl.className = 'quiz-feedback incorrect';
1204
+ return; // Don't proceed if no answer selected
1205
+ }
1206
+
1207
+ const selectedAnswers = Array.from(selectedOptions).map(input => parseInt(input.value));
1208
+ const currentQuestion = quizQuestions[currentQuizQuestionIndex];
1209
+ console.log("Submitting answer for question", currentQuizQuestionIndex + 1, "Selected:", selectedAnswers);
1210
+
1211
+ const isCorrect = checkQuizAnswer(selectedAnswers, currentQuestion.correctAnswers);
1212
+
1213
+ if (isCorrect) {
1214
+ quizScore++; // Increment score only on the first correct submission for a question (needs better state if re-answering allowed)
1215
+ quizFeedbackEl.textContent = "Benar!";
1216
+ quizFeedbackEl.className = 'quiz-feedback correct';
1217
+ console.log("Answer correct. Score:", quizScore);
1218
+ } else {
1219
+ quizFeedbackEl.textContent = `Salah. Jawaban yang benar: ${formatCorrectAnswers(currentQuestion)}`;
1220
+ quizFeedbackEl.className = 'quiz-feedback incorrect';
1221
+ console.log("Answer incorrect.");
1222
+ }
1223
+
1224
+ quizAnswerSubmitted = true; // Mark as submitted for this question view
1225
+ updateQuizScoreDisplay();
1226
+ quizSubmitButton.disabled = true; // Disable submit after answering for this view
1227
+ // quizSubmitButton.textContent = "Jawaban Terkirim"; // Optional: Change text
1228
+
1229
+ // Disable checkboxes after submitting
1230
+ const allCheckboxes = quizOptionsEl.querySelectorAll('input[type="checkbox"]');
1231
+ allCheckboxes.forEach(cb => cb.disabled = true);
1232
+
1233
+ // Note: Score calculation might need adjustment if users can go back and change answers.
1234
+ // Current simple approach: score increments only once per question index if correct on first submit shown.
1235
+ }
1236
+
1237
+ // Missing functions - add these before showQuizResults
1238
+ function checkQuizAnswer(selectedAnswers, correctAnswers) {
1239
+ // Check if arrays have the same length and same values (regardless of order)
1240
+ if (selectedAnswers.length !== correctAnswers.length) {
1241
+ return false;
1242
+ }
1243
+
1244
+ // Make copies to avoid modifying the originals
1245
+ const selected = [...selectedAnswers].sort();
1246
+ const correct = [...correctAnswers].sort();
1247
+
1248
+ // Compare each element
1249
+ for (let i = 0; i < selected.length; i++) {
1250
+ if (selected[i] !== correct[i]) {
1251
+ return false;
1252
+ }
1253
+ }
1254
+
1255
+ return true;
1256
+ }
1257
+
1258
+ function formatCorrectAnswers(question) {
1259
+ // Format the correct answers as text for feedback
1260
+ const correctIndices = question.correctAnswers;
1261
+ const correctOptions = correctIndices.map(index =>
1262
+ String.fromCharCode(65 + index) + ": " + question.options[index]
1263
+ );
1264
+
1265
+ return correctOptions.join(", ");
1266
+ }
1267
+
1268
+ function updateQuizScoreDisplay() {
1269
+ // Update the quiz score display element
1270
+ if (quizScoreEl) {
1271
+ quizScoreEl.textContent = `Skor: ${quizScore}/${quizQuestions.length}`;
1272
+ }
1273
+ }
1274
+
1275
+ function showQuizResults() {
1276
+ console.log("showQuizResults called.");
1277
+ // ... existing checks ...
1278
+
1279
+ quizQuestionContainer.style.display = 'none'; // Hide question area
1280
+ quizResultsEl.style.display = 'block'; // Show results area
1281
+ quizFinalScoreEl.textContent = `Skor Akhir Anda: ${quizScore} dari ${quizQuestions.length} soal.`;
1282
+
1283
+ // Hide navigation/submit buttons, keep only Close
1284
+ quizSubmitButton.style.display = 'none';
1285
+ quizBackButton.style.display = 'none';
1286
+ quizNextButton.style.display = 'none';
1287
+ quizCloseButton.style.display = 'inline-block'; // Ensure close is visible
1288
+
1289
+ // No need to change submit button's onclick here, just hide it.
1290
+ console.log("Quiz results shown. Navigation/Submit hidden.");
1291
+ }
1292
+
1293
+
1294
+ // --- Animation Loop ---
1295
+ function animate() {
1296
+ requestAnimationFrame(animate);
1297
+ // Only update OrbitControls if the simulation is visible and quiz is not active
1298
+ if (!isQuizActive && container.style.display !== 'none') {
1299
+ controls.update();
1300
+ }
1301
+ renderer.render(scene, camera);
1302
+ }
1303
+
1304
+ // --- Mulai Aplikasi ---
1305
+ init();
1306
+
1307
+ </script>
1308
+
1309
+ </body>
1310
+ </html>