feat: improve evaluation system and update circuit editor to use localStorage for persistence
This commit is contained in:
parent
74a8d87853
commit
997ab78f56
|
|
@ -43,6 +43,8 @@ Penjelasan:
|
|||
- Tidak perlu `\n` — `print()` otomatis menambahkan baris baru
|
||||
- Tidak perlu `return 0` atau titik koma
|
||||
|
||||
|
||||
|
||||
---EXERCISE---
|
||||
### Latihan
|
||||
Buat program yang mencetak teks berikut:
|
||||
|
|
@ -50,14 +52,16 @@ Buat program yang mencetak teks berikut:
|
|||
```
|
||||
Halo Dunia
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
---INITIAL_CODE---
|
||||
#include <stdio.h>
|
||||
|
||||
int main() {
|
||||
// Tulis kode kamu di sini
|
||||
|
||||
printf("Halo Dunia\\n");
|
||||
return 0;
|
||||
}
|
||||
---END_INITIAL_CODE---
|
||||
|
|
@ -66,11 +70,29 @@ int main() {
|
|||
Halo Dunia
|
||||
---END_EXPECTED_OUTPUT---
|
||||
|
||||
---EXPECTED_OUTPUT_PYTHON---
|
||||
Halo Dunia
|
||||
---END_EXPECTED_OUTPUT_PYTHON---
|
||||
|
||||
---INITIAL_PYTHON---
|
||||
# Tulis kode kamu di sini
|
||||
|
||||
print("Halo Dunia")
|
||||
---END_INITIAL_PYTHON---
|
||||
|
||||
---SOLUTION_CODE---
|
||||
#include <stdio.h>
|
||||
|
||||
int main() {
|
||||
printf("Halo Dunia\\n");
|
||||
return 0;
|
||||
}
|
||||
---END_SOLUTION_CODE---
|
||||
|
||||
---SOLUTION_PYTHON---
|
||||
print("Halo Dunia")
|
||||
---END_SOLUTION_PYTHON---
|
||||
|
||||
---KEY_TEXT---
|
||||
printf
|
||||
print
|
||||
---END_KEY_TEXT---
|
||||
|
|
|
|||
|
|
@ -19,8 +19,12 @@ Semua materi bisa dikerjakan langsung di browser!
|
|||
### Elektronika (Hybrid)
|
||||
3. [Rangkaian Dasar](lesson/rangkaian_dasar.md)
|
||||
|
||||
### Arduino (Velxio)
|
||||
4. [LED Blink](lesson/led_blink_arduino.md)
|
||||
|
||||
----Available_Lessons----
|
||||
|
||||
1. [Hello, World!](lesson/hello_world.md)
|
||||
2. [Variabel](lesson/variabel.md)
|
||||
3. [Rangkaian Dasar](lesson/rangkaian_dasar.md)
|
||||
4. [LED Blink](lesson/led_blink_arduino.md)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,147 @@
|
|||
---LESSON_INFO---
|
||||
Pelajaran Arduino: Mengedipkan LED menggunakan simulator Velxio.
|
||||
|
||||
**Learning Objectives:**
|
||||
- Memahami fungsi `pinMode()`, `digitalWrite()`, dan `delay()`
|
||||
- Menghubungkan LED dan resistor ke Arduino Uno
|
||||
- Menggunakan `Serial.print()` untuk debugging
|
||||
|
||||
**Prerequisites:**
|
||||
- Hello, World!
|
||||
---END_LESSON_INFO---
|
||||
|
||||
# LED Blink dengan Arduino
|
||||
|
||||
Proyek pertama setiap programmer Arduino: **mengedipkan LED!**
|
||||
|
||||
## Konsep Dasar
|
||||
|
||||
### Digital Output
|
||||
|
||||
Arduino Uno memiliki 14 pin digital (0–13). Setiap pin bisa menjadi **OUTPUT** atau **INPUT**:
|
||||
|
||||
```
|
||||
pinMode(13, OUTPUT); // Set pin 13 sebagai output
|
||||
digitalWrite(13, HIGH); // Nyalakan (5V)
|
||||
digitalWrite(13, LOW); // Matikan (0V)
|
||||
```
|
||||
|
||||
### Rangkaian LED
|
||||
|
||||
LED membutuhkan **resistor pembatas arus** agar tidak rusak:
|
||||
|
||||
```
|
||||
Pin 13 → Resistor 220Ω → LED (Anode) → LED (Cathode) → GND
|
||||
```
|
||||
|
||||
Tanpa resistor, arus terlalu besar dan LED bisa terbakar!
|
||||
|
||||
### Serial Monitor
|
||||
|
||||
`Serial.println()` mengirim teks ke Serial Monitor — sangat berguna untuk debugging:
|
||||
|
||||
```
|
||||
Serial.begin(9600); // Mulai komunikasi serial
|
||||
Serial.println("LED ON"); // Cetak teks + newline
|
||||
```
|
||||
|
||||
---EXERCISE---
|
||||
### Tantangan
|
||||
|
||||
**Kode Arduino:**
|
||||
Tulis program yang mengedipkan LED di **pin 13** dengan interval 1 detik.
|
||||
Program harus mencetak `LED ON` saat LED menyala dan `LED OFF` saat LED mati ke Serial Monitor.
|
||||
|
||||
**Rangkaian:**
|
||||
Hubungkan komponen-komponen berikut:
|
||||
- Pin 13 Arduino → Resistor (pin 1)
|
||||
- Resistor (pin 2) → LED Anode (A)
|
||||
- LED Cathode (C) → GND Arduino
|
||||
|
||||
Setelah selesai, tekan **Compile & Run** untuk menjalankan program dan tunggu beberapa detik agar Serial output muncul.
|
||||
---
|
||||
|
||||
---INITIAL_CODE_ARDUINO---
|
||||
// LED Blink - Tugas Pertama Arduino
|
||||
// Mengedipkan LED di pin 13 dengan resistor pembatas arus
|
||||
|
||||
void setup() {
|
||||
pinMode(13, OUTPUT);
|
||||
Serial.begin(9600);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
digitalWrite(13, HIGH);
|
||||
Serial.println("LED ON");
|
||||
|
||||
delay(1000);
|
||||
|
||||
digitalWrite(13, LOW);
|
||||
Serial.println("LED OFF");
|
||||
|
||||
delay(1000);
|
||||
}
|
||||
---END_INITIAL_CODE_ARDUINO---
|
||||
|
||||
---VELXIO_CIRCUIT---
|
||||
{
|
||||
"board": "arduino:avr:uno",
|
||||
"components": [
|
||||
{
|
||||
"type": "led",
|
||||
"id": "led-builtin",
|
||||
"x": 394,
|
||||
"y": -208,
|
||||
"rotation": 0,
|
||||
"props": {
|
||||
"color": "red",
|
||||
"pin": 13,
|
||||
"state": true,
|
||||
"value": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "resistor",
|
||||
"id": "resistor-1775728124959-5rknpronw",
|
||||
"x": 292,
|
||||
"y": -159,
|
||||
"rotation": 0,
|
||||
"props": {
|
||||
"value": true,
|
||||
"state": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"wires": []
|
||||
}
|
||||
---END_VELXIO_CIRCUIT---
|
||||
|
||||
---EXPECTED_SERIAL_OUTPUT---
|
||||
LED ON
|
||||
LED OFF
|
||||
---END_EXPECTED_SERIAL_OUTPUT---
|
||||
|
||||
---EXPECTED_WIRING---
|
||||
{
|
||||
"wires": [
|
||||
{
|
||||
"start": { "componentId": "arduino-uno", "pinName": "13" },
|
||||
"end": { "componentId": "resistor-1775728124959-5rknpronw", "pinName": "1" }
|
||||
},
|
||||
{
|
||||
"start": { "componentId": "resistor-1775728124959-5rknpronw", "pinName": "2" },
|
||||
"end": { "componentId": "led-builtin", "pinName": "A" }
|
||||
},
|
||||
{
|
||||
"start": { "componentId": "arduino-uno", "pinName": "GND" },
|
||||
"end": { "componentId": "led-builtin", "pinName": "C" }
|
||||
}
|
||||
]
|
||||
}
|
||||
---END_EXPECTED_WIRING---
|
||||
|
||||
---KEY_TEXT---
|
||||
pinMode
|
||||
digitalWrite
|
||||
Serial
|
||||
---END_KEY_TEXT---
|
||||
|
|
@ -73,17 +73,31 @@ Lengkapi rangkaian agar tegangan di **Vout** bernilai **2.5V**.
|
|||
#include <stdio.h>
|
||||
|
||||
int main() {
|
||||
// Hitung voltage divider: Vout = Vin * R2 / (R1 + R2)
|
||||
// Vin=5, R1=1000, R2=1000
|
||||
|
||||
float vin = 5.0;
|
||||
float r1 = 1000.0;
|
||||
float r2 = 1000.0;
|
||||
float vout = vin * r2 / (r1 + r2);
|
||||
printf("Vout = %.2fV\n", vout);
|
||||
return 0;
|
||||
}
|
||||
---END_INITIAL_CODE---
|
||||
|
||||
---INITIAL_PYTHON---
|
||||
# Hitung voltage divider: Vout = Vin * R2 / (R1 + R2)
|
||||
# Vin=5, R1=1000, R2=1000
|
||||
vin = 5
|
||||
r1 = 1000
|
||||
r2 = 1000
|
||||
|
||||
vout = vin * (r2 / (r1 + r2))
|
||||
print(f"Vout = {vout:.2f}V")
|
||||
---END_INITIAL_PYTHON---
|
||||
|
||||
---INITIAL_CIRCUIT---
|
||||
<cir f="1" ts="0.000005" ic="10.20027730826997" cb="50" pb="50" vr="5" mts="5e-11">
|
||||
<v x="80 200 80 112" f="0" wf="0" maxv="5"/>
|
||||
<r x="80 112 176 112" f="0" r="1000"/>
|
||||
<r x="176 112 176 200" f="0" r="1000"/>
|
||||
<w x="176 200 80 200" f="0"/>
|
||||
<ln x="176 112 208 32" f="0" te="Vout"/>
|
||||
</cir>
|
||||
|
|
@ -101,14 +115,42 @@ Vout = 2.50V
|
|||
}
|
||||
---END_EXPECTED_CIRCUIT_OUTPUT---
|
||||
|
||||
---INITIAL_PYTHON---
|
||||
# Hitung voltage divider: Vout = Vin * R2 / (R1 + R2)
|
||||
# Vin=5, R1=1000, R2=1000
|
||||
---SOLUTION_CODE---
|
||||
#include <stdio.h>
|
||||
|
||||
---END_INITIAL_PYTHON---
|
||||
int main() {
|
||||
float vin = 5.0;
|
||||
float r1 = 1000.0;
|
||||
float r2 = 1000.0;
|
||||
float vout = vin * r2 / (r1 + r2);
|
||||
printf("Vout = %.2fV\n", vout);
|
||||
return 0;
|
||||
}
|
||||
---END_SOLUTION_CODE---
|
||||
|
||||
---SOLUTION_PYTHON---
|
||||
vin = 5
|
||||
r1 = 1000
|
||||
r2 = 1000
|
||||
|
||||
vout = vin * (r2 / (r1 + r2))
|
||||
print(f"Vout = {vout:.2f}V")
|
||||
---END_SOLUTION_PYTHON---
|
||||
|
||||
---SOLUTION_CIRCUIT---
|
||||
<cir f="1" ts="0.000005" ic="10.20027730826997" cb="50" pb="50" vr="5" mts="5e-11">
|
||||
<v x="80 200 80 112" f="0" wf="0" maxv="5"/>
|
||||
<r x="80 112 176 112" f="0" r="1000"/>
|
||||
<r x="176 112 176 200" f="0" r="1000"/>
|
||||
<w x="176 200 80 200" f="0"/>
|
||||
<ln x="176 112 208 32" f="0" te="Vout"/>
|
||||
</cir>
|
||||
---END_SOLUTION_CIRCUIT---
|
||||
|
||||
---KEY_TEXT---
|
||||
printf
|
||||
float
|
||||
print(f"
|
||||
---END_KEY_TEXT---
|
||||
|
||||
---KEY_TEXT_CIRCUIT---
|
||||
|
|
|
|||
|
|
@ -78,8 +78,8 @@ Panjang nama: 10
|
|||
#include <stdio.h>
|
||||
|
||||
int main() {
|
||||
// Deklarasikan variabel nama_panjang bertipe int
|
||||
// Cetak hasilnya menggunakan printf
|
||||
int nama_panjang = 10;
|
||||
printf("Panjang nama: %d\n", nama_panjang);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -93,9 +93,30 @@ Panjang nama: 10
|
|||
# Deklarasikan variabel nama_panjang
|
||||
# Cetak hasilnya menggunakan print
|
||||
|
||||
nama_panjang = 10
|
||||
print(f"Panjang nama: {nama_panjang}")
|
||||
---END_INITIAL_PYTHON---
|
||||
|
||||
---SOLUTION_CODE---
|
||||
#include <stdio.h>
|
||||
|
||||
int main() {
|
||||
int nama_panjang = 10;
|
||||
printf("Panjang nama: %d\n", nama_panjang);
|
||||
|
||||
return 0;
|
||||
}
|
||||
---END_SOLUTION_CODE---
|
||||
|
||||
---SOLUTION_PYTHON---
|
||||
nama_panjang = 10
|
||||
print(f"Panjang nama: {nama_panjang}")
|
||||
---END_SOLUTION_PYTHON---
|
||||
|
||||
---KEY_TEXT---
|
||||
int
|
||||
printf
|
||||
print(f"
|
||||
{
|
||||
}
|
||||
---END_KEY_TEXT---
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
token;nama_siswa;hello_world;variabel;rangkaian_dasar
|
||||
GURU001;Pak Budi;not_started;not_started;not_started
|
||||
SISWA001;Andi Pratama;not_started;not_started;not_started
|
||||
SISWA002;Siti Nurhaliza;not_started;not_started;not_started
|
||||
SISWA003;Rizky Firmansyah;not_started;not_started;not_started
|
||||
token;nama_siswa;hello_world;variabel;rangkaian_dasar;led_blink_arduino
|
||||
121312;Anggoro Dwi;completed;completed;completed;completed
|
||||
dummy_token_12345;Example Student;completed;not_started;not_started;not_started
|
||||
|
|
|
|||
|
|
|
@ -23,7 +23,7 @@
|
|||
saving = true;
|
||||
clearTimeout(saveTimeout);
|
||||
saveTimeout = setTimeout(() => {
|
||||
sessionStorage.setItem(storageKey, text);
|
||||
localStorage.setItem(storageKey, text);
|
||||
saving = false;
|
||||
}, 1000);
|
||||
}
|
||||
|
|
@ -54,7 +54,7 @@
|
|||
// Load initial circuit or draft
|
||||
let toLoad = initialCircuit;
|
||||
if (storageKey) {
|
||||
const saved = sessionStorage.getItem(storageKey);
|
||||
const saved = localStorage.getItem(storageKey);
|
||||
if (saved) {
|
||||
toLoad = saved;
|
||||
}
|
||||
|
|
@ -69,19 +69,6 @@
|
|||
}
|
||||
|
||||
if (onready) onready(simApi);
|
||||
|
||||
// Setup auto-save polling
|
||||
if (storageKey) {
|
||||
autoSaveInterval = setInterval(() => {
|
||||
if (simApi && ready) {
|
||||
const currentText = simApi.exportCircuit();
|
||||
const saved = sessionStorage.getItem(storageKey);
|
||||
if (currentText && currentText !== saved && currentText.trim().length > 10) {
|
||||
saveToStorage(currentText);
|
||||
}
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
} else {
|
||||
console.error("CircuitEditor: Could not find CircuitJS1 API object");
|
||||
}
|
||||
|
|
@ -109,7 +96,39 @@
|
|||
$effect(() => {
|
||||
if (simApi && ready && initialCircuit && initialCircuit !== lastLoadedCircuit) {
|
||||
lastLoadedCircuit = initialCircuit;
|
||||
loadCircuitToSim(initialCircuit);
|
||||
const saved = storageKey ? localStorage.getItem(storageKey) : null;
|
||||
if (!saved) {
|
||||
loadCircuitToSim(initialCircuit);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Load draft reactively when storageKey is set (fixes delayed auth problem)
|
||||
$effect(() => {
|
||||
if (storageKey && storageKey !== lastStorageKey) {
|
||||
lastStorageKey = storageKey;
|
||||
if (!ready || !simApi) return;
|
||||
|
||||
const saved = localStorage.getItem(storageKey);
|
||||
if (saved) {
|
||||
loadCircuitToSim(saved);
|
||||
} else if (initialCircuit) {
|
||||
loadCircuitToSim(initialCircuit);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Reactively poll to sync auto-save, ensures it gets setup even if storageKey is delayed
|
||||
$effect(() => {
|
||||
if (simApi && ready && storageKey) {
|
||||
const interval = setInterval(() => {
|
||||
const currentText = simApi.exportCircuit();
|
||||
const saved = localStorage.getItem(storageKey);
|
||||
if (currentText && currentText !== saved && currentText.trim().length > 10) {
|
||||
saveToStorage(currentText);
|
||||
}
|
||||
}, 5000);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -26,11 +26,13 @@
|
|||
let saveTimeout: any;
|
||||
|
||||
function saveToStorage(value: string) {
|
||||
if (!storageKey) return;
|
||||
if (!lastStorageKey && !storageKey) return;
|
||||
const activeKey = lastStorageKey || storageKey;
|
||||
if (!activeKey) return;
|
||||
saving = true;
|
||||
clearTimeout(saveTimeout);
|
||||
saveTimeout = setTimeout(() => {
|
||||
sessionStorage.setItem(storageKey, value);
|
||||
localStorage.setItem(activeKey, value);
|
||||
saving = false;
|
||||
}, 1000);
|
||||
}
|
||||
|
|
@ -89,7 +91,11 @@
|
|||
if (update.docChanged) {
|
||||
const val = update.state.doc.toString();
|
||||
onchange?.(val);
|
||||
saveToStorage(val);
|
||||
|
||||
// Use lastStorageKey ref or dynamic check since storageKey prop might be stale in closure
|
||||
if (lastStorageKey) {
|
||||
saveToStorage(val);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
|
@ -184,7 +190,7 @@
|
|||
|
||||
let initialCode = code;
|
||||
if (storageKey) {
|
||||
const saved = sessionStorage.getItem(storageKey);
|
||||
const saved = localStorage.getItem(storageKey);
|
||||
if (saved !== null) {
|
||||
initialCode = saved;
|
||||
}
|
||||
|
|
@ -267,7 +273,7 @@
|
|||
lastStorageKey = storageKey;
|
||||
if (!ready || !view || !storageKey) return;
|
||||
|
||||
const saved = sessionStorage.getItem(storageKey);
|
||||
const saved = localStorage.getItem(storageKey);
|
||||
if (saved !== null) {
|
||||
setCode(saved);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
error?: string;
|
||||
loading?: boolean;
|
||||
success?: boolean | null;
|
||||
debug?: string[];
|
||||
}
|
||||
|
||||
export interface OutputEntry {
|
||||
|
|
@ -21,6 +22,8 @@
|
|||
|
||||
let { sections = [] }: Props = $props();
|
||||
|
||||
let showDebug = $state<Record<string, boolean>>({});
|
||||
|
||||
let anyLoading = $derived(sections.some(s => s.data.loading));
|
||||
|
||||
let overallSuccess = $derived.by(() => {
|
||||
|
|
@ -55,8 +58,16 @@
|
|||
{:else if sec.data.success === false}
|
||||
<span class="section-badge error">Error</span>
|
||||
{/if}
|
||||
{#if sec.data.debug && sec.data.debug.length > 0}
|
||||
<label class="debug-toggle">
|
||||
<input type="checkbox" bind:checked={showDebug[sec.key]} /> Debug
|
||||
</label>
|
||||
{/if}
|
||||
</div>
|
||||
<pre class="output-body">{#if sec.data.loading}{sec.loadingText}{:else if sec.data.error}{sec.data.error}{:else if sec.data.output}{sec.data.output}{:else}<span class="placeholder">{sec.placeholder}</span>{/if}</pre>
|
||||
<pre class="output-body">{#if sec.data.loading}{sec.loadingText}{:else if sec.data.error}{sec.data.error}{:else if sec.data.output}{sec.data.output}{:else}<span class="placeholder">{sec.placeholder}</span>{/if}{#if sec.data.debug && showDebug[sec.key]}
|
||||
|
||||
── Debug ──
|
||||
{sec.data.debug.join('\n')}{/if}</pre>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
|
|
@ -153,4 +164,13 @@
|
|||
color: var(--color-text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
.debug-toggle {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
font-size: 0.7rem;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ interface VelxioWire {
|
|||
interface EvaluationExpected {
|
||||
key_text?: string;
|
||||
serial_output?: string;
|
||||
wiring?: [string, string][];
|
||||
wiring?: [string, string][] | { wires: Array<{ start: { componentId: string; pinName: string }; end: { componentId: string; pinName: string } }> };
|
||||
}
|
||||
|
||||
export interface EvaluationResult {
|
||||
|
|
@ -34,6 +34,7 @@ export interface EvaluationResult {
|
|||
serial?: boolean;
|
||||
wiring?: boolean;
|
||||
messages: string[];
|
||||
debug?: string[];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -145,12 +146,25 @@ export class VelxioBridge {
|
|||
const edges = studentWires.map(w =>
|
||||
`${w.start.componentId}:${w.start.pinName}↔${w.end.componentId}:${w.end.pinName}`
|
||||
);
|
||||
result.wiring = this.matchWiring(studentWires, expected.wiring);
|
||||
|
||||
// Extract expected wires array from both formats
|
||||
let expectedWires: any[] = [];
|
||||
if (Array.isArray(expected.wiring)) {
|
||||
expectedWires = expected.wiring;
|
||||
} else if (expected.wiring.wires && Array.isArray(expected.wiring.wires)) {
|
||||
expectedWires = expected.wiring.wires;
|
||||
}
|
||||
|
||||
result.wiring = this.matchWiring(studentWires, expectedWires);
|
||||
result.messages.push(result.wiring
|
||||
? '✅ Rangkaian wiring benar'
|
||||
: '❌ Wiring belum sesuai. Periksa kembali koneksi komponen');
|
||||
dbg.push(`[DBG wiring] ${studentWires.length} wires: ${edges.join(' | ')}`);
|
||||
dbg.push(`[DBG wiring] expected: ${expected.wiring.map(p => p.join('↔')).join(' | ')}`);
|
||||
dbg.push(`[DBG wiring] expected: ${expectedWires.map((w: any) => {
|
||||
if (Array.isArray(w) && w.length === 2) return w.join('↔');
|
||||
if (w.start && w.end) return `${w.start.componentId}:${w.start.pinName}↔${w.end.componentId}:${w.end.pinName}`;
|
||||
return JSON.stringify(w);
|
||||
}).join(' | ')}`);
|
||||
dbg.push(`[DBG wiring] → ${result.wiring}`);
|
||||
} else {
|
||||
result.wiring = false;
|
||||
|
|
@ -163,8 +177,8 @@ export class VelxioBridge {
|
|||
const checks = [result.key_text, result.serial, result.wiring].filter(v => v !== undefined);
|
||||
result.pass = checks.length > 0 && checks.every(Boolean);
|
||||
|
||||
// Append debug info to messages so it's visible in the output panel
|
||||
result.messages.push('', '── Debug ──', ...dbg);
|
||||
// Store debug info separately
|
||||
result.debug = dbg;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
@ -204,34 +218,188 @@ export class VelxioBridge {
|
|||
|
||||
/** Subsequence match: expected lines must appear in order within actual. */
|
||||
private matchSerial(actual: string, expected: string): boolean {
|
||||
const actualLines = actual.trim().split('\n').map(l => l.trim());
|
||||
const expectedLines = expected.trim().split('\n').map(l => l.trim());
|
||||
let j = 0;
|
||||
for (const line of actualLines) {
|
||||
if (j < expectedLines.length && line === expectedLines[j]) j++;
|
||||
if (j === expectedLines.length) return true;
|
||||
if (!expected.trim()) return true;
|
||||
if (!actual.trim()) return false;
|
||||
|
||||
const actualLines = actual.split('\n').map(l => l.trim()).filter(l => l.length > 0);
|
||||
const expectedLines = expected.split('\n').map(l => l.trim()).filter(l => l.length > 0);
|
||||
|
||||
let expectedIdx = 0;
|
||||
for (const actualLine of actualLines) {
|
||||
if (expectedIdx < expectedLines.length) {
|
||||
// Check if expected line is a substring of actual line (case-insensitive)
|
||||
if (actualLine.toLowerCase().includes(expectedLines[expectedIdx].toLowerCase())) {
|
||||
expectedIdx++;
|
||||
}
|
||||
}
|
||||
if (expectedIdx === expectedLines.length) return true;
|
||||
}
|
||||
return j === expectedLines.length;
|
||||
return expectedIdx === expectedLines.length;
|
||||
}
|
||||
|
||||
/** Lenient wiring check: all expected edges must exist in student wires. */
|
||||
private matchWiring(studentWires: VelxioWire[], expectedPairs: [string, string][]): boolean {
|
||||
/**
|
||||
* Net-based wiring check: groups pins into electrical nets using
|
||||
* Union-Find (Disjoint Set Union), then compares net composition.
|
||||
*
|
||||
* This is robust against:
|
||||
* - Node order swaps (A↔B vs B↔A)
|
||||
* - Transitive connections (A-B-C vs A-C, B in middle)
|
||||
* - Extra student wires (lenient - only missing connections fail)
|
||||
*/
|
||||
private matchWiring(studentWires: VelxioWire[], expectedPairs: [string, string][] | any[]): boolean {
|
||||
// Normalize power pin names (e.g., GND.2 → GND, VCC.1 → VCC)
|
||||
const normalizePin = (pin: string) => pin.replace(/^(GND|VCC|5V|3V3|3\.3V)\.\d+$/i, '$1');
|
||||
|
||||
const norm = (a: string, b: string) => {
|
||||
const normA = a.replace(/:(.+)$/, (_, pin) => ':' + normalizePin(pin));
|
||||
const normB = b.replace(/:(.+)$/, (_, pin) => ':' + normalizePin(pin));
|
||||
return [normA, normB].sort().join('↔');
|
||||
const normalizePin = (pin: string) => {
|
||||
return pin.replace(/^(GND|VCC|5V|3V3|3\.3V|POWER)\.\d+$/i, '$1');
|
||||
};
|
||||
const studentEdges = new Set(
|
||||
studentWires.map(w =>
|
||||
norm(
|
||||
`${w.start.componentId}:${w.start.pinName}`,
|
||||
`${w.end.componentId}:${w.end.pinName}`
|
||||
)
|
||||
)
|
||||
);
|
||||
return expectedPairs.every(([a, b]) => studentEdges.has(norm(a, b)));
|
||||
|
||||
const normPin = (pin: string) => {
|
||||
const [comp, name] = pin.includes(':') ? pin.split(':') : ['', pin];
|
||||
return `${comp}:${normalizePin(name)}`;
|
||||
};
|
||||
|
||||
// Union-Find (DSU) implementation
|
||||
const parent = new Map<string, string>();
|
||||
const rank = new Map<string, string>();
|
||||
|
||||
const find = (x: string): string => {
|
||||
if (!parent.has(x)) {
|
||||
parent.set(x, x);
|
||||
rank.set(x, x);
|
||||
return x;
|
||||
}
|
||||
// Path compression
|
||||
if (parent.get(x) !== x) {
|
||||
parent.set(x, find(parent.get(x)!));
|
||||
}
|
||||
return parent.get(x)!;
|
||||
};
|
||||
|
||||
const union = (a: string, b: string) => {
|
||||
const rootA = find(a);
|
||||
const rootB = find(b);
|
||||
if (rootA === rootB) return;
|
||||
// Union by rank
|
||||
const rankA = rank.get(rootA) || '';
|
||||
const rankB = rank.get(rootB) || '';
|
||||
if (rankA < rankB) {
|
||||
parent.set(rootA, rootB);
|
||||
} else if (rankA > rankB) {
|
||||
parent.set(rootB, rootA);
|
||||
} else {
|
||||
parent.set(rootB, rootA);
|
||||
rank.set(rootA, rankA + '1');
|
||||
}
|
||||
};
|
||||
|
||||
// Build student nets
|
||||
for (const wire of studentWires) {
|
||||
const start = normPin(`${wire.start.componentId}:${wire.start.pinName}`);
|
||||
const end = normPin(`${wire.end.componentId}:${wire.end.pinName}`);
|
||||
union(start, end);
|
||||
}
|
||||
|
||||
// Collect all pins and group into nets for student
|
||||
const allStudentPins = new Set<string>();
|
||||
for (const wire of studentWires) {
|
||||
allStudentPins.add(normPin(`${wire.start.componentId}:${wire.start.pinName}`));
|
||||
allStudentPins.add(normPin(`${wire.end.componentId}:${wire.end.pinName}`));
|
||||
}
|
||||
|
||||
const studentNets = new Map<string, Set<string>>();
|
||||
for (const pin of allStudentPins) {
|
||||
const root = find(pin);
|
||||
if (!studentNets.has(root)) {
|
||||
studentNets.set(root, new Set());
|
||||
}
|
||||
studentNets.get(root)!.add(pin);
|
||||
}
|
||||
|
||||
// Build expected nets
|
||||
const expectedWires: Array<{ start: string; end: string }> = [];
|
||||
for (const expected of expectedPairs) {
|
||||
if (Array.isArray(expected) && expected.length === 2) {
|
||||
expectedWires.push({ start: normPin(expected[0]), end: normPin(expected[1]) });
|
||||
} else if (expected.start && expected.end) {
|
||||
expectedWires.push({
|
||||
start: normPin(`${expected.start.componentId}:${expected.start.pinName}`),
|
||||
end: normPin(`${expected.end.componentId}:${expected.end.pinName}`)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const expectedParent = new Map<string, string>();
|
||||
const expectedRank = new Map<string, string>();
|
||||
|
||||
const expectedFind = (x: string): string => {
|
||||
if (!expectedParent.has(x)) {
|
||||
expectedParent.set(x, x);
|
||||
expectedRank.set(x, x);
|
||||
return x;
|
||||
}
|
||||
if (expectedParent.get(x) !== x) {
|
||||
expectedParent.set(x, expectedFind(expectedParent.get(x)!));
|
||||
}
|
||||
return expectedParent.get(x)!;
|
||||
};
|
||||
|
||||
const expectedUnion = (a: string, b: string) => {
|
||||
const rootA = expectedFind(a);
|
||||
const rootB = expectedFind(b);
|
||||
if (rootA === rootB) return;
|
||||
const rankA = expectedRank.get(rootA) || '';
|
||||
const rankB = expectedRank.get(rootB) || '';
|
||||
if (rankA < rankB) {
|
||||
expectedParent.set(rootA, rootB);
|
||||
} else if (rankA > rankB) {
|
||||
expectedParent.set(rootB, rootA);
|
||||
} else {
|
||||
expectedParent.set(rootB, rootA);
|
||||
expectedRank.set(rootA, rankA + '1');
|
||||
}
|
||||
};
|
||||
|
||||
for (const wire of expectedWires) {
|
||||
expectedUnion(wire.start, wire.end);
|
||||
}
|
||||
|
||||
const allExpectedPins = new Set<string>();
|
||||
for (const wire of expectedWires) {
|
||||
allExpectedPins.add(wire.start);
|
||||
allExpectedPins.add(wire.end);
|
||||
}
|
||||
|
||||
const expectedNets = new Map<string, Set<string>>();
|
||||
for (const pin of allExpectedPins) {
|
||||
const root = expectedFind(pin);
|
||||
if (!expectedNets.has(root)) {
|
||||
expectedNets.set(root, new Set());
|
||||
}
|
||||
expectedNets.get(root)!.add(pin);
|
||||
}
|
||||
|
||||
// Compare: every expected net must have a matching student net
|
||||
// that contains ALL pins from the expected net
|
||||
for (const [, expectedNet] of expectedNets) {
|
||||
let found = false;
|
||||
for (const [, studentNet] of studentNets) {
|
||||
// Check if student net contains all pins from expected net
|
||||
let allPresent = true;
|
||||
for (const pin of expectedNet) {
|
||||
if (!studentNet.has(pin)) {
|
||||
allPresent = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (allPresent) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ export interface LessonContent {
|
|||
expected_serial_output: string;
|
||||
expected_wiring: string;
|
||||
solution_code: string;
|
||||
solution_circuit: string;
|
||||
solution_python: string;
|
||||
key_text: string;
|
||||
lesson_title: string;
|
||||
lesson_completed: boolean;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
import { createFloatingPanel } from '$actions/floatingPanel.svelte';
|
||||
import { highlightAllCode } from '$actions/highlightCode';
|
||||
import { renderCircuitEmbeds } from '$actions/renderCircuitEmbeds';
|
||||
import { tick } from 'svelte';
|
||||
import { tick, untrack } from 'svelte';
|
||||
import type { LessonContent } from '$types/lesson';
|
||||
|
||||
// Data from +page.ts load function (SSR + client)
|
||||
|
|
@ -35,7 +35,7 @@
|
|||
let pythonCode = $state('');
|
||||
|
||||
// Output state per language + circuit
|
||||
const freshOutput = () => ({ output: '', error: '', loading: false, success: null as boolean | null });
|
||||
const freshOutput = () => ({ output: '', error: '', loading: false, success: null as boolean | null, debug: undefined as string[] | undefined });
|
||||
let cOut = $state(freshOutput());
|
||||
let pyOut = $state(freshOutput());
|
||||
let circuitOut = $state(freshOutput());
|
||||
|
|
@ -43,8 +43,8 @@
|
|||
// Helper: get the active code output object for current language
|
||||
function getCodeOut() { return currentLanguage === 'python' ? pyOut : cOut; }
|
||||
|
||||
// AND-logic: track whether each exercise type has passed (persists across runs)
|
||||
let codePassed = $state(false);
|
||||
let cPassed = $state(false);
|
||||
let pythonPassed = $state(false);
|
||||
let circuitPassed = $state(false);
|
||||
|
||||
// Velxio (Arduino simulator) state
|
||||
|
|
@ -79,7 +79,7 @@
|
|||
secs.push({ key: 'circuit', label: 'Circuit', icon: '\u26A1', data: circuitOut, placeholder: 'Klik "Cek Rangkaian" untuk mengevaluasi', loadingText: 'Mengevaluasi rangkaian...' });
|
||||
}
|
||||
if (tabs.includes('velxio')) {
|
||||
secs.push({ key: 'velxio', label: 'Arduino', icon: '\u{1F4DF}', data: velxioOut, placeholder: 'Klik "Submit" untuk mengevaluasi', loadingText: 'Mengevaluasi...' });
|
||||
secs.push({ key: 'velxio', label: 'Arduino', icon: '\u{1F4DF}', data: velxioOut, placeholder: 'Klik "Compile & Run" untuk menjalankan kode', loadingText: 'Mengevaluasi...' });
|
||||
}
|
||||
return secs;
|
||||
});
|
||||
|
|
@ -123,9 +123,11 @@
|
|||
|
||||
// Sync lesson data when navigating between lessons
|
||||
$effect(() => {
|
||||
if (lesson) {
|
||||
data = lesson;
|
||||
lessonCompleted = lesson.lesson_completed;
|
||||
const currentLesson = lesson; // capture dependency
|
||||
if (currentLesson) {
|
||||
untrack(() => {
|
||||
data = currentLesson;
|
||||
lessonCompleted = currentLesson.lesson_completed;
|
||||
|
||||
// Initialize per-language code
|
||||
cCode = lesson.initial_code_c || '';
|
||||
|
|
@ -143,7 +145,8 @@
|
|||
pyOut = freshOutput();
|
||||
circuitOut = freshOutput();
|
||||
velxioOut = freshOutput();
|
||||
codePassed = false;
|
||||
cPassed = false;
|
||||
pythonPassed = false;
|
||||
circuitPassed = false;
|
||||
showSolution = false;
|
||||
|
||||
|
|
@ -164,7 +167,8 @@
|
|||
title: lesson.lesson_title,
|
||||
completed: lesson.lesson_completed,
|
||||
prevLesson: lesson.prev_lesson,
|
||||
nextLesson: lesson.next_lesson
|
||||
nextLesson: currentLesson.next_lesson
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -217,10 +221,19 @@
|
|||
}
|
||||
}
|
||||
|
||||
/** For hybrid lessons, check if all exercise types have passed (AND logic). */
|
||||
/** Check if all exercise types for this lesson have passed (AND logic). */
|
||||
function checkAllPassed(): boolean {
|
||||
if (!isHybrid) return true; // not hybrid, each evaluator handles itself
|
||||
return codePassed && circuitPassed;
|
||||
const needsC = data?.active_tabs?.includes('c');
|
||||
const needsPython = data?.active_tabs?.includes('python');
|
||||
const needsCircuit = data?.active_tabs?.includes('circuit');
|
||||
|
||||
if (!data?.active_tabs?.length) return true;
|
||||
|
||||
if (needsC && !cPassed) return false;
|
||||
if (needsPython && !pythonPassed) return false;
|
||||
if (needsCircuit && !circuitPassed) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function evaluateCircuit() {
|
||||
|
|
@ -271,7 +284,7 @@
|
|||
circuitPassed = true;
|
||||
if (isHybrid) {
|
||||
circuitOut.output += '\n✅ Rangkaian benar!';
|
||||
if (!codePassed) circuitOut.output += '\n⏳ Selesaikan juga tantangan kode untuk menyelesaikan pelajaran ini.';
|
||||
if (!checkAllPassed()) circuitOut.output += '\n⏳ Selesaikan juga tantangan kode untuk menyelesaikan pelajaran ini.';
|
||||
}
|
||||
if (checkAllPassed()) {
|
||||
await completeLesson();
|
||||
|
|
@ -306,15 +319,28 @@
|
|||
out.success = true;
|
||||
|
||||
if (data.expected_output) {
|
||||
const passed = res.output.trim() === data.expected_output.trim() && checkKeyText(code, data.key_text ?? '');
|
||||
const currentCCode = currentLanguage === 'c' ? code : cCode;
|
||||
const currentPythonCode = currentLanguage === 'python' ? code : pythonCode;
|
||||
const mergedCode = currentCCode + '\n' + currentPythonCode;
|
||||
const passed = res.output.trim() === data.expected_output.trim() && checkKeyText(mergedCode, data.key_text ?? '');
|
||||
if (passed) {
|
||||
codePassed = true;
|
||||
if (isHybrid && !circuitPassed) {
|
||||
out.output += '\n✅ Kode benar!\n⏳ Selesaikan juga tantangan rangkaian untuk menyelesaikan pelajaran ini.';
|
||||
if (currentLanguage === 'c') cPassed = true;
|
||||
else if (currentLanguage === 'python') pythonPassed = true;
|
||||
|
||||
if (!checkAllPassed()) {
|
||||
out.output += '\n✅ Kode benar!\n⏳ Selesaikan juga tantangan di tab lainnya untuk menyelesaikan pelajaran ini.';
|
||||
} else {
|
||||
out.output += '\n🎉 Semuanya benar!';
|
||||
}
|
||||
|
||||
if (checkAllPassed()) {
|
||||
await completeLesson();
|
||||
if (data.solution_code) { showSolution = true; editor?.setCode(data.solution_code); }
|
||||
if (data.solution_code || data.solution_python || data.solution_circuit) {
|
||||
showSolution = true;
|
||||
handleShowSolution();
|
||||
// To avoid logic flipping
|
||||
showSolution = true;
|
||||
}
|
||||
setTimeout(() => { showCelebration = false; activeTab = 'editor'; }, 3000);
|
||||
}
|
||||
}
|
||||
|
|
@ -344,18 +370,28 @@
|
|||
}
|
||||
|
||||
function handleShowSolution() {
|
||||
if (!data?.solution_code) return;
|
||||
if (!data) return;
|
||||
if (!data.solution_code && !data.solution_circuit && !data.solution_python) return;
|
||||
|
||||
showSolution = !showSolution;
|
||||
if (showSolution) {
|
||||
if (activeTab === 'circuit' || data.active_tabs?.includes('circuit')) {
|
||||
circuitEditor?.setCircuitText(data.solution_code);
|
||||
} else {
|
||||
// Update Circuit if exists
|
||||
if (data.active_tabs?.includes('circuit') && data.solution_circuit) {
|
||||
circuitEditor?.setCircuitText(data.solution_circuit);
|
||||
}
|
||||
// Update Code Editor
|
||||
if (currentLanguage === 'python' && data.solution_python) {
|
||||
editor?.setCode(data.solution_python);
|
||||
} else if (data.solution_code) {
|
||||
editor?.setCode(data.solution_code);
|
||||
}
|
||||
} else {
|
||||
if (activeTab === 'circuit' || data.active_tabs?.includes('circuit')) {
|
||||
circuitEditor?.setCircuitText(data.initial_code);
|
||||
} else {
|
||||
// Restore Circuit if exists
|
||||
if (data.active_tabs?.includes('circuit') && data.initial_circuit) {
|
||||
circuitEditor?.setCircuitText(data.initial_circuit);
|
||||
}
|
||||
// Restore Code Editor to their working code
|
||||
if (currentLanguage === 'python' || currentLanguage === 'c' || data.initial_code) {
|
||||
editor?.setCode(currentCode);
|
||||
}
|
||||
}
|
||||
|
|
@ -363,6 +399,31 @@
|
|||
|
||||
// === Velxio (Arduino simulator) ===
|
||||
|
||||
/**
|
||||
* Match serial output using subsequence matching.
|
||||
* Expected lines must appear in order within actual output (not necessarily consecutive).
|
||||
* This is more robust than exact line matching.
|
||||
*/
|
||||
function matchSerialSubsequence(actual: string, expected: string): boolean {
|
||||
if (!expected) return true;
|
||||
if (!actual) return false;
|
||||
|
||||
const actualLines = actual.split('\n').map(l => l.trim()).filter(l => l.length > 0);
|
||||
const expectedLines = expected.split('\n').map(l => l.trim()).filter(l => l.length > 0);
|
||||
|
||||
let expectedIdx = 0;
|
||||
for (const actualLine of actualLines) {
|
||||
if (expectedIdx < expectedLines.length) {
|
||||
// Check if expected line is a substring of actual line (case-insensitive)
|
||||
if (actualLine.toLowerCase().includes(expectedLines[expectedIdx].toLowerCase())) {
|
||||
expectedIdx++;
|
||||
}
|
||||
}
|
||||
if (expectedIdx === expectedLines.length) return true;
|
||||
}
|
||||
return expectedIdx === expectedLines.length;
|
||||
}
|
||||
|
||||
/** Try to directly read Velxio Zustand stores from iframe (same-origin). */
|
||||
function getVelxioStores(iframe: HTMLIFrameElement): { editor: any; simulator: any } | null {
|
||||
try {
|
||||
|
|
@ -479,26 +540,56 @@
|
|||
dbg.push('[fallback: direct iframe access]');
|
||||
try {
|
||||
const win = velxioIframe.contentWindow as any;
|
||||
// Access Zustand stores via window globals (we'll expose them)
|
||||
// Or try to find the stores on the module scope
|
||||
const editorStore = win.__VELXIO_EDITOR_STORE__?.getState?.();
|
||||
const simStore = win.__VELXIO_SIMULATOR_STORE__?.getState?.();
|
||||
|
||||
if (editorStore?.files) {
|
||||
sourceCode = editorStore.files.map((f: any) => f.content).join('\n');
|
||||
dbg.push(`[direct] source: ${sourceCode.length} chars`);
|
||||
} else {
|
||||
dbg.push('[direct] editor store not found');
|
||||
|
||||
// Check if stores are exposed
|
||||
if (!win.__VELXIO_EDITOR_STORE__ || !win.__VELXIO_SIMULATOR_STORE__) {
|
||||
dbg.push('[direct] WARNING: Stores not exposed on window');
|
||||
dbg.push('[direct] Trying alternative access methods...');
|
||||
|
||||
// Alternative: try to find Zustand stores via other means
|
||||
// Some bundlers expose stores differently
|
||||
if (win.__ZUSTAND__) {
|
||||
dbg.push('[direct] Found __ZUSTAND__, searching for stores...');
|
||||
// Try to locate editor and simulator stores
|
||||
for (const key of Object.keys(win.__ZUSTAND__)) {
|
||||
const store = win.__ZUSTAND__[key];
|
||||
if (store?.getState) {
|
||||
const state = store.getState();
|
||||
if (state?.files && !sourceCode) {
|
||||
sourceCode = state.files.map((f: any) => f.content).join('\n');
|
||||
dbg.push(`[direct] source from __ZUSTAND__: ${sourceCode.length} chars`);
|
||||
}
|
||||
if (state?.wires !== undefined && wireList.length === 0) {
|
||||
wireList = state.wires;
|
||||
dbg.push(`[direct] wires from __ZUSTAND__: ${wireList.length}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Primary method: use exposed stores
|
||||
if (!sourceCode || wireList.length === 0) {
|
||||
const editorStore = win.__VELXIO_EDITOR_STORE__?.getState?.();
|
||||
const simStore = win.__VELXIO_SIMULATOR_STORE__?.getState?.();
|
||||
|
||||
if (simStore) {
|
||||
const board = simStore.boards?.find((b: any) => b.id === simStore.activeBoardId);
|
||||
serialLog = board?.serialOutput ?? simStore.serialOutput ?? '';
|
||||
dbg.push(`[direct] serial: ${serialLog.length} chars`);
|
||||
wireList = simStore.wires ?? [];
|
||||
dbg.push(`[direct] wires: ${wireList.length}`);
|
||||
} else {
|
||||
dbg.push('[direct] simulator store not found');
|
||||
if (editorStore?.files) {
|
||||
sourceCode = editorStore.files.map((f: any) => f.content).join('\n');
|
||||
dbg.push(`[direct] source: ${sourceCode.length} chars`);
|
||||
} else {
|
||||
dbg.push('[direct] editor store has no files');
|
||||
}
|
||||
|
||||
if (simStore) {
|
||||
// Try to get serial output from active board
|
||||
const board = simStore.boards?.find((b: any) => b.id === simStore.activeBoardId);
|
||||
serialLog = board?.serialOutput ?? simStore.serialOutput ?? '';
|
||||
dbg.push(`[direct] serial: ${serialLog.length} chars`);
|
||||
wireList = simStore.wires ?? [];
|
||||
dbg.push(`[direct] wires: ${wireList.length}`);
|
||||
} else {
|
||||
dbg.push('[direct] simulator store not found');
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
dbg.push(`[direct] error: ${e.message}`);
|
||||
|
|
@ -523,41 +614,105 @@
|
|||
|
||||
// 2. Serial output
|
||||
if (data.expected_serial_output) {
|
||||
const actualLines = serialLog.trim().split('\n').map(l => l.trim());
|
||||
const expectedLines = data.expected_serial_output.trim().split('\n').map(l => l.trim());
|
||||
let j = 0;
|
||||
for (const line of actualLines) {
|
||||
if (j < expectedLines.length && line === expectedLines[j]) j++;
|
||||
if (j === expectedLines.length) break;
|
||||
}
|
||||
serialPass = j === expectedLines.length;
|
||||
const actualLog = serialLog.trim();
|
||||
const expectedLog = data.expected_serial_output.trim();
|
||||
|
||||
// Use subsequence matching: expected lines must appear in order in actual output
|
||||
serialPass = matchSerialSubsequence(actualLog, expectedLog);
|
||||
|
||||
messages.push(serialPass
|
||||
? '✅ Serial output sesuai'
|
||||
: '❌ Serial output belum sesuai');
|
||||
|
||||
const preview = serialLog.substring(0, 150).replace(/\n/g, '↵');
|
||||
dbg.push(`[serial] actual(${serialLog.length}ch)="${preview}" → ${serialPass}`);
|
||||
dbg.push(`[serial] actual(${serialLog.length}ch)="${preview}"`);
|
||||
dbg.push(`[serial] expected="${expectedLog.substring(0, 150).replace(/\n/g, '↵')}" → ${serialPass}`);
|
||||
}
|
||||
|
||||
// 3. Wiring
|
||||
if (data.expected_wiring) {
|
||||
let expectedPairs: [string, string][] = [];
|
||||
try { expectedPairs = JSON.parse(data.expected_wiring); } catch {}
|
||||
let expectedWires: any[] = [];
|
||||
try {
|
||||
const parsed = JSON.parse(data.expected_wiring);
|
||||
// Support both old format (array of pairs) and new format (object with wires array)
|
||||
if (Array.isArray(parsed)) {
|
||||
expectedWires = parsed;
|
||||
} else if (parsed.wires && Array.isArray(parsed.wires)) {
|
||||
expectedWires = parsed.wires;
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// Detect non-polarized component types (pins are interchangeable)
|
||||
const nonPolarizedTypes = new Set(['resistor']);
|
||||
|
||||
// Normalize power pin names (e.g., GND.2 → GND, VCC.1 → VCC)
|
||||
const normalizePin = (pin: string) => pin.replace(/^(GND|VCC|5V|3V3|3\.3V)\.\d+$/i, '$1');
|
||||
|
||||
const norm = (a: string, b: string) => {
|
||||
const normA = a.replace(/:(.+)$/, (_, pin) => ':' + normalizePin(pin));
|
||||
const normB = b.replace(/:(.+)$/, (_, pin) => ':' + normalizePin(pin));
|
||||
return [normA, normB].sort().join('↔');
|
||||
// This handles multiple power pin variants
|
||||
const normalizePin = (pin: string) => {
|
||||
// Match power pins with numeric suffixes: GND.1, VCC.2, 5V.3, etc.
|
||||
return pin.replace(/^(GND|VCC|5V|3V3|3\.3V|POWER)\.\d+$/i, '$1');
|
||||
};
|
||||
|
||||
// Normalize component:pin for comparison
|
||||
// For non-polarized components (like resistors), strip pin numbers
|
||||
// so that resistor:1 and resistor:2 are treated as equivalent
|
||||
const normalizeEdge = (compId: string, pinName: string): string => {
|
||||
const normPin = normalizePin(pinName);
|
||||
// Check if this component is non-polarized by looking at expected_wires
|
||||
// We detect resistor components by their ID pattern or type
|
||||
const isNonPolarized = nonPolarizedTypes.has(compId.split('-')[0]) ||
|
||||
compId.startsWith('resistor');
|
||||
if (isNonPolarized && /^\d+$/.test(normPin)) {
|
||||
// For non-polarized components with numeric pins, use a generic pin name
|
||||
return `${compId}:PIN`;
|
||||
}
|
||||
return `${compId}:${normPin}`;
|
||||
};
|
||||
|
||||
const norm = (a: string, b: string) => {
|
||||
// Parse component:pin format
|
||||
const [compA, pinA] = a.split(':');
|
||||
const [compB, pinB] = b.split(':');
|
||||
const normA = `${compA}:${normalizePin(pinA || '')}`;
|
||||
const normB = `${compB}:${normalizePin(pinB || '')}`;
|
||||
// For non-polarized components, normalize numeric pins to generic
|
||||
const finalA = nonPolarizedTypes.has(compA) || compA.startsWith('resistor')
|
||||
? `${compA}:PIN` : normA;
|
||||
const finalB = nonPolarizedTypes.has(compB) || compB.startsWith('resistor')
|
||||
? `${compB}:PIN` : normB;
|
||||
// Sort to make edge comparison order-independent
|
||||
return [finalA, finalB].sort().join('↔');
|
||||
};
|
||||
|
||||
const studentEdges = new Set(
|
||||
wireList.map(w => norm(
|
||||
`${w.start.componentId}:${w.start.pinName}`,
|
||||
`${w.end.componentId}:${w.end.pinName}`
|
||||
))
|
||||
);
|
||||
wiringPass = expectedPairs.every(([a, b]) => studentEdges.has(norm(a, b)));
|
||||
|
||||
// Check all expected connections exist
|
||||
wiringPass = expectedWires.every((expected: any) => {
|
||||
let edgeKey: string;
|
||||
// Handle both formats
|
||||
if (Array.isArray(expected) && expected.length === 2) {
|
||||
// Old format: ["component:pin", "component:pin"]
|
||||
edgeKey = norm(expected[0], expected[1]);
|
||||
} else if (expected.start && expected.end) {
|
||||
// New format: { start: { componentId, pinName }, end: { componentId, pinName } }
|
||||
const startPin = `${expected.start.componentId}:${expected.start.pinName}`;
|
||||
const endPin = `${expected.end.componentId}:${expected.end.pinName}`;
|
||||
edgeKey = norm(startPin, endPin);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
const exists = studentEdges.has(edgeKey);
|
||||
if (!exists) {
|
||||
dbg.push(`[wiring] MISSING: ${edgeKey}`);
|
||||
}
|
||||
return exists;
|
||||
});
|
||||
|
||||
messages.push(wiringPass
|
||||
? '✅ Rangkaian wiring benar'
|
||||
: '❌ Wiring belum sesuai');
|
||||
|
|
@ -565,17 +720,20 @@
|
|||
const edgesStr = wireList.map(w =>
|
||||
`${w.start.componentId}:${w.start.pinName}↔${w.end.componentId}:${w.end.pinName}`
|
||||
);
|
||||
dbg.push(`[wiring] student: ${edgesStr.join(' | ') || '(kosong)'}`);
|
||||
dbg.push(`[wiring] expected: ${expectedPairs.map(p => p.join('↔')).join(' | ')}`);
|
||||
dbg.push(`[wiring] → ${wiringPass}`);
|
||||
dbg.push(`[wiring] student (${wireList.length} wires): ${edgesStr.join(' | ') || '(kosong)'}`);
|
||||
dbg.push(`[wiring] expected (${expectedWires.length} connections): ${expectedWires.map((w: any) => {
|
||||
if (Array.isArray(w) && w.length === 2) return w.join('↔');
|
||||
if (w.start && w.end) return `${w.start.componentId}:${w.start.pinName}↔${w.end.componentId}:${w.end.pinName}`;
|
||||
return JSON.stringify(w);
|
||||
}).join(' | ')}`);
|
||||
dbg.push(`[wiring] result → ${wiringPass}`);
|
||||
}
|
||||
|
||||
const checks = [keyTextPass, serialPass, wiringPass].filter(v => v !== undefined);
|
||||
const pass = checks.length > 0 && checks.every(Boolean);
|
||||
|
||||
messages.push('', '── Debug ──', ...dbg);
|
||||
|
||||
velxioOut.output = messages.join('\n');
|
||||
velxioOut.debug = dbg;
|
||||
velxioOut.success = pass;
|
||||
|
||||
if (pass) {
|
||||
|
|
@ -710,13 +868,7 @@
|
|||
</div>
|
||||
{:else}
|
||||
<div class="velxio-toolbar">
|
||||
<button type="button" class="btn btn-success" onclick={handleVelxioSubmit}
|
||||
disabled={velxioOut.loading}>
|
||||
{velxioOut.loading ? 'Mengevaluasi...' : '✓ Submit'}
|
||||
</button>
|
||||
<span class="velxio-status">
|
||||
{velxioReady ? '🟢 Bridge' : '🔵 Direct'}
|
||||
</span>
|
||||
<!-- Submit button removed for cleaner workspace -->
|
||||
</div>
|
||||
<!-- svelte-ignore a11y_missing_attribute -->
|
||||
<iframe
|
||||
|
|
@ -1027,10 +1179,7 @@
|
|||
border-bottom: 1px solid var(--color-border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.velxio-status {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.velxio-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
|||
1357
proposal.md
1357
proposal.md
File diff suppressed because it is too large
Load Diff
|
|
@ -54,6 +54,7 @@ def api_lesson(filename):
|
|||
initial_code = parsed_data['initial_code']
|
||||
solution_code = parsed_data['solution_code']
|
||||
solution_circuit = parsed_data.get('solution_circuit', '')
|
||||
solution_python = parsed_data.get('solution_python', '')
|
||||
key_text = parsed_data['key_text']
|
||||
active_tabs = parsed_data['active_tabs']
|
||||
|
||||
|
|
@ -120,6 +121,7 @@ def api_lesson(filename):
|
|||
'expected_wiring': expected_wiring,
|
||||
'solution_code': solution_code,
|
||||
'solution_circuit': solution_circuit,
|
||||
'solution_python': solution_python,
|
||||
'key_text': key_text,
|
||||
'key_text_circuit': key_text_circuit,
|
||||
'active_tabs': active_tabs,
|
||||
|
|
|
|||
|
|
@ -311,6 +311,9 @@ def render_markdown_content(file_path):
|
|||
solution_circuit, lesson_content = _extract_section(
|
||||
lesson_content, '---SOLUTION_CIRCUIT---', '---END_SOLUTION_CIRCUIT---')
|
||||
|
||||
solution_python, lesson_content = _extract_section(
|
||||
lesson_content, '---SOLUTION_PYTHON---', '---END_SOLUTION_PYTHON---')
|
||||
|
||||
# Initial codes (C, Python, Circuit, Quiz)
|
||||
initial_code_c, lesson_content = _extract_section(
|
||||
lesson_content, '---INITIAL_CODE---', '---END_INITIAL_CODE---')
|
||||
|
|
@ -367,6 +370,7 @@ def render_markdown_content(file_path):
|
|||
'initial_code': initial_code,
|
||||
'solution_code': solution_code,
|
||||
'solution_circuit': solution_circuit,
|
||||
'solution_python': solution_python,
|
||||
'key_text': key_text,
|
||||
'key_text_circuit': key_text_circuit,
|
||||
'initial_code_c': initial_code_c,
|
||||
|
|
|
|||
2
velxio
2
velxio
|
|
@ -1 +1 @@
|
|||
Subproject commit 52321dffe839481eb849a92ec02dd3727246adef
|
||||
Subproject commit a11dd87d5ad3a7239fb577dc6c2133050484c0a7
|
||||
Loading…
Reference in New Issue