Compare commits

...

5 Commits

11 changed files with 488 additions and 181 deletions

View File

@ -148,22 +148,40 @@ void loop() {
"board": "arduino:avr:uno",
"components": [
{
"type": "wokwi-pushbutton",
"id": "button-1",
"x": 400,
"y": -100,
"rotation": 0,
"props": {}
},
{
"type": "wokwi-led",
"id": "led-1",
"x": 400,
"y": -250,
"type": "led",
"id": "led-builtin",
"x": 956.2066448009604,
"y": -15.285273464964305,
"rotation": 0,
"props": {
"color": "green",
"pin": 13
"color": "red",
"pin": 13,
"state": false,
"value": false
}
},
{
"type": "resistor",
"id": "resistor-1776299815587-z0q0jzny8",
"x": 808.6031046903261,
"y": 44.96592399354325,
"rotation": 0,
"props": {
"value": false,
"state": false
}
},
{
"type": "pushbutton",
"id": "pushbutton-1776299852057-07xh6qh7g",
"x": 829.2414304859469,
"y": 230.19576790585754,
"rotation": 0,
"props": {
"color": "red",
"pressed": false,
"label": "",
"xray": false
}
}
],
@ -179,20 +197,54 @@ PRESSED
{
"wires": [
{
"start": { "componentId": "arduino-uno", "pinName": "2" },
"end": { "componentId": "button-1", "pinName": "1.l" }
"start": {
"componentId": "resistor-1776299815587-z0q0jzny8",
"pinName": "2"
},
"end": {
"componentId": "led-builtin",
"pinName": "A"
}
},
{
"start": { "componentId": "button-1", "pinName": "2.l" },
"end": { "componentId": "arduino-uno", "pinName": "GND" }
"start": {
"componentId": "arduino-uno",
"pinName": "GND.1"
},
"end": {
"componentId": "led-builtin",
"pinName": "C"
}
},
{
"start": { "componentId": "arduino-uno", "pinName": "13" },
"end": { "componentId": "led-1", "pinName": "A" }
"start": {
"componentId": "resistor-1776299815587-z0q0jzny8",
"pinName": "1"
},
"end": {
"componentId": "arduino-uno",
"pinName": "13"
}
},
{
"start": { "componentId": "led-1", "pinName": "C" },
"end": { "componentId": "arduino-uno", "pinName": "GND" }
"start": {
"componentId": "pushbutton-1776299852057-07xh6qh7g",
"pinName": "1.l"
},
"end": {
"componentId": "arduino-uno",
"pinName": "2"
}
},
{
"start": {
"componentId": "arduino-uno",
"pinName": "GND.3"
},
"end": {
"componentId": "pushbutton-1776299852057-07xh6qh7g",
"pinName": "2.l"
}
}
]
}

View File

@ -147,40 +147,81 @@ void loop() {
"board": "arduino:avr:uno",
"components": [
{
"type": "wokwi-led",
"type": "led",
"id": "led-red",
"x": 370,
"y": -300,
"x": 409.6091884525257,
"y": 65.40692157090741,
"rotation": 0,
"props": {
"color": "red",
"pin": 13
"pin": 13,
"state": true,
"value": true
}
},
{
"type": "wokwi-led",
"type": "led",
"id": "led-yellow",
"x": 370,
"y": -200,
"x": 466.09101803924966,
"y": 66.43228050989252,
"rotation": 0,
"props": {
"color": "yellow",
"pin": 12
"pin": 12,
"state": false,
"value": false
}
},
{
"type": "wokwi-led",
"type": "led",
"id": "led-green",
"x": 370,
"y": -100,
"x": 522.0366933470766,
"y": 63.613964067867315,
"rotation": 0,
"props": {
"color": "green",
"pin": 11
"pin": 11,
"state": false,
"value": false
}
},
{
"type": "resistor",
"id": "resistor-1776300566703-7pi4wei9j",
"x": 275.08055011716567,
"y": 133.79325981817132,
"rotation": 0,
"props": {
"value": true,
"state": true
}
},
{
"type": "resistor",
"id": "resistor-1776300590088-k4l87u1rf",
"x": 273.3198126609919,
"y": 178.95644554504224,
"rotation": 0,
"props": {
"value": false,
"state": false
}
},
{
"type": "resistor",
"id": "resistor-1776300593575-rklkixiwy",
"x": 270.5981563595655,
"y": 220.2759558909028,
"rotation": 0,
"props": {
"value": false,
"state": false
}
}
],
"wires": []
"wires": [
]
}
---END_VELXIO_CIRCUIT---
@ -194,29 +235,95 @@ YELLOW
---EXPECTED_WIRING---
{
"wires": [
{
"start": { "componentId": "arduino-uno", "pinName": "13" },
"end": { "componentId": "led-red", "pinName": "A" }
{
"start": {
"componentId": "resistor-1776300566703-7pi4wei9j",
"pinName": "2"
},
"end": {
"componentId": "led-red",
"pinName": "A"
}
},
{
"start": { "componentId": "led-red", "pinName": "C" },
"end": { "componentId": "arduino-uno", "pinName": "GND" }
"start": {
"componentId": "resistor-1776300590088-k4l87u1rf",
"pinName": "2"
},
"end": {
"componentId": "led-yellow",
"pinName": "A"
}
},
{
"start": { "componentId": "arduino-uno", "pinName": "12" },
"end": { "componentId": "led-yellow", "pinName": "A" }
"start": {
"componentId": "resistor-1776300593575-rklkixiwy",
"pinName": "2"
},
"end": {
"componentId": "led-green",
"pinName": "A"
}
},
{
"start": { "componentId": "led-yellow", "pinName": "C" },
"end": { "componentId": "arduino-uno", "pinName": "GND" }
"start": {
"componentId": "arduino-uno",
"pinName": "GND.3"
},
"end": {
"componentId": "led-red",
"pinName": "C"
}
},
{
"start": { "componentId": "arduino-uno", "pinName": "11" },
"end": { "componentId": "led-green", "pinName": "A" }
"start": {
"componentId": "arduino-uno",
"pinName": "GND.3"
},
"end": {
"componentId": "led-yellow",
"pinName": "C"
}
},
{
"start": { "componentId": "led-green", "pinName": "C" },
"end": { "componentId": "arduino-uno", "pinName": "GND" }
"start": {
"componentId": "arduino-uno",
"pinName": "GND.3"
},
"end": {
"componentId": "led-green",
"pinName": "C"
}
},
{
"start": {
"componentId": "resistor-1776300566703-7pi4wei9j",
"pinName": "1"
},
"end": {
"componentId": "arduino-uno",
"pinName": "13"
}
},
{
"start": {
"componentId": "resistor-1776300590088-k4l87u1rf",
"pinName": "1"
},
"end": {
"componentId": "arduino-uno",
"pinName": "12"
}
},
{
"start": {
"componentId": "resistor-1776300593575-rklkixiwy",
"pinName": "1"
},
"end": {
"componentId": "arduino-uno",
"pinName": "11"
}
}
]
}
@ -229,3 +336,9 @@ const int
Serial
delay
---END_KEY_TEXT---
---EVALUATION_CONFIG---
{
"timeout_ms": 10000
}
---END_EVALUATION_CONFIG---

View File

@ -27,7 +27,8 @@
const LONG_PRESS_MS = 400;
const DOUBLE_TAP_MS = 300;
const HOLDING_TIMEOUT_MS = 5000;
const CURSOR_OFFSET_Y = -(parseInt(env.PUBLIC_CURSOR_OFFSET_Y || '50', 10));
const BASE_OFFSET_Y = -(parseInt(env.PUBLIC_CURSOR_OFFSET_Y || '50', 10));
let currentOffset = $derived(isTouchDevice ? BASE_OFFSET_Y : 0);
/**
* State machine:
@ -46,12 +47,18 @@
$effect(() => {
if (typeof window === 'undefined') return;
const mql = window.matchMedia('(hover: none) and (pointer: coarse)');
isTouchDevice = mql.matches;
const handler = (e: MediaQueryListEvent) => {
isTouchDevice = e.matches;
const checkTouch = () => {
isTouchDevice = mql.matches && window.innerWidth < 768;
};
checkTouch();
mql.addEventListener('change', checkTouch);
window.addEventListener('resize', checkTouch);
return () => {
mql.removeEventListener('change', checkTouch);
window.removeEventListener('resize', checkTouch);
};
mql.addEventListener('change', handler);
return () => mql.removeEventListener('change', handler);
});
function getIframeTarget(x: number, y: number): Element | null {
@ -142,7 +149,7 @@
if (!overlayEl) return;
const rect = overlayEl.getBoundingClientRect();
crosshairX = viewportX - rect.left;
crosshairY = viewportY - rect.top + CURSOR_OFFSET_Y;
crosshairY = viewportY - rect.top + currentOffset;
}
function getCrosshairViewport(): { x: number; y: number } {

View File

@ -31,7 +31,7 @@
}
</script>
<nav class="navbar">
<nav class="navbar" onclick={() => auth.recordActivity()}>
<div class="container navbar-inner">
{#if $lessonContext}
<!-- Lesson mode -->

View File

@ -94,7 +94,7 @@ export class VelxioBridge {
}
}
setEmbedMode(options: { hideEditor?: boolean; hideAuth?: boolean; hideComponentPicker?: boolean }) {
setEmbedMode(options: { hideEditor?: boolean; hideAuth?: boolean; hideComponentPicker?: boolean; lockComponents?: boolean }) {
this.send('elemes:set_embed_mode', options);
}

View File

@ -11,12 +11,38 @@ import { writable, get } from 'svelte/store';
import { validateToken, login as apiLogin, logout as apiLogout } from '$services/api';
const STORAGE_KEY = 'student_token';
const LAST_ACTIVE_KEY = 'student_last_active';
const MAX_INACTIVITY = 24 * 60 * 60 * 1000; // 1 day in ms
export const authToken = writable('');
export const authStudentName = writable('');
export const authLoggedIn = writable(false);
export const authIsTeacher = writable(false);
function clearAllCookies() {
if (typeof document === 'undefined') return;
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i];
const eqPos = cookie.indexOf('=');
const name = eqPos > -1 ? cookie.substr(0, eqPos).trim() : cookie.trim();
document.cookie = name + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/';
}
}
function clearAuthData() {
clearAllCookies();
localStorage.removeItem(STORAGE_KEY);
localStorage.removeItem(LAST_ACTIVE_KEY);
sessionStorage.clear();
}
function updateLastActive() {
if (typeof window !== 'undefined') {
localStorage.setItem(LAST_ACTIVE_KEY, Date.now().toString());
}
}
export const auth = {
/** Current token value (non-reactive). */
get token() { return get(authToken); },
@ -25,9 +51,21 @@ export const auth = {
/** Restore session from localStorage on app mount. */
async init() {
if (typeof window === 'undefined') return;
const saved = localStorage.getItem(STORAGE_KEY);
const lastActive = localStorage.getItem(LAST_ACTIVE_KEY);
if (!saved) return;
// Check for 1 day inactivity
if (lastActive) {
const inactiveTime = Date.now() - parseInt(lastActive, 10);
if (inactiveTime > MAX_INACTIVITY) {
clearAuthData();
return;
}
}
try {
const res = await validateToken(saved);
if (res.success && res.student_name) {
@ -35,17 +73,18 @@ export const auth = {
authStudentName.set(res.student_name);
authLoggedIn.set(true);
authIsTeacher.set(res.is_teacher ?? false);
updateLastActive();
} else {
localStorage.removeItem(STORAGE_KEY);
sessionStorage.clear();
clearAuthData();
}
} catch {
localStorage.removeItem(STORAGE_KEY);
sessionStorage.clear();
clearAuthData();
}
},
async login(inputToken: string) {
clearAuthData();
const res = await apiLogin(inputToken);
if (res.success && res.student_name) {
authToken.set(inputToken);
@ -53,18 +92,29 @@ export const auth = {
authLoggedIn.set(true);
authIsTeacher.set(res.is_teacher ?? false);
localStorage.setItem(STORAGE_KEY, inputToken);
sessionStorage.clear();
updateLastActive();
}
return res;
},
async logout() {
await apiLogout();
try {
await apiLogout();
} catch {
// ignore logout failure, proceed to clear local state
}
authToken.set('');
authStudentName.set('');
authLoggedIn.set(false);
authIsTeacher.set(false);
localStorage.removeItem(STORAGE_KEY);
sessionStorage.clear();
clearAuthData();
location.reload();
},
/** Update activity timestamp. Call this on user interactions. */
recordActivity() {
if (get(authLoggedIn)) {
updateLastActive();
}
}
};

View File

@ -33,4 +33,5 @@ export interface LessonContent {
language: string;
language_display_name: string;
active_tabs: string[];
evaluation_config: Record<string, any>;
}

View File

@ -51,11 +51,89 @@
let isVelxio = $derived(data?.active_tabs?.includes('velxio') ?? false);
let velxioBridge = $state<VelxioBridge | null>(null);
let velxioReady = $state(false);
let velxioSaving = $state(false);
let velxioError = $state(false);
let velxioIframe = $state<HTMLIFrameElement | null>(null);
let velxioOut = $state(freshOutput());
let hasArduinoCode = $derived(!!data?.initial_code_arduino);
// Velxio storage keys
let arduinoCodeKey = $derived(`elemes_arduino_code_${slug}`);
let arduinoCircuitKey = $derived(`elemes_arduino_circuit_${slug}`);
/** Try to directly read Velxio Zustand stores from iframe (same-origin). */
function getVelxioState(): { code: string; circuit: string } | null {
if (!velxioIframe) return null;
try {
const win = velxioIframe.contentWindow as any;
if (!win) return null;
const editorStore = win.__VELXIO_EDITOR_STORE__?.getState?.();
const simStore = win.__VELXIO_SIMULATOR_STORE__?.getState?.();
if (!editorStore || !simStore) return null;
// Extract code
const code = (editorStore.files as any[] || []).map((f: any) => f.content).join('\n');
// Extract circuit state (Diagram format compatible with elemes:load_circuit)
const circuit = {
board: simStore.activeBoardId,
components: (simStore.components as any[] || []).map(c => ({
type: c.metadataId,
id: c.id,
x: c.x,
y: c.y,
rotation: c.properties?.rotation || 0,
props: { ...c.properties }
})),
wires: simStore.wires || []
};
return {
code,
circuit: JSON.stringify(circuit)
};
} catch {
return null;
}
}
// Auto-save Velxio state periodically
$effect(() => {
if (velxioReady && $authLoggedIn && !showSolution) {
const interval = setInterval(() => {
const state = getVelxioState();
if (!state) return;
let changed = false;
// 1. Source Code
const savedCode = localStorage.getItem(arduinoCodeKey);
if (state.code && state.code !== savedCode) {
console.log('[Velxio Auto-save] Saving code changes (Zustand)');
localStorage.setItem(arduinoCodeKey, state.code);
changed = true;
}
// 2. Circuit (Diagram + Wires)
const savedCircuit = localStorage.getItem(arduinoCircuitKey);
if (state.circuit && state.circuit !== savedCircuit) {
console.log('[Velxio Auto-save] Saving circuit changes (Zustand)');
localStorage.setItem(arduinoCircuitKey, state.circuit);
changed = true;
}
if (changed) {
velxioSaving = true;
setTimeout(() => { velxioSaving = false; }, 1500);
}
}, 7000); // Poll every 7 seconds
return () => clearInterval(interval);
}
});
// Derived: is this a hybrid lesson (has both code and circuit)?
let isHybrid = $derived(
(data?.active_tabs?.includes('c') || data?.active_tabs?.includes('python')) &&
@ -379,6 +457,17 @@
if (activeTab === 'circuit') {
circuitEditor?.setCircuitText(data.initial_circuit || data.initial_code);
Object.assign(circuitOut, freshOutput());
} else if (activeTab === 'velxio') {
console.log('[Velxio Reset] Clearing drafts and reloading initial state');
localStorage.removeItem(arduinoCodeKey);
localStorage.removeItem(arduinoCircuitKey);
if (data.initial_code_arduino) {
velxioBridge?.loadCode([{ name: 'sketch.ino', content: data.initial_code_arduino }]);
}
if (data.velxio_circuit) {
velxioBridge?.loadCircuit(data.velxio_circuit);
}
Object.assign(velxioOut, freshOutput());
} else {
const resetCode = currentLanguage === 'python'
? (data.initial_python || '')
@ -419,13 +508,7 @@
}
}
// === 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.
*/
/** Match serial output using subsequence matching. */
function matchSerialSubsequence(actual: string, expected: string): boolean {
if (!expected) return true;
if (!actual) return false;
@ -436,7 +519,6 @@
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++;
}
@ -446,20 +528,6 @@
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 {
const win = iframe.contentWindow as any;
if (!win) return null;
// Zustand stores expose getState() on the hook; we look for the global store refs
// that Velxio's EmbedBridge.ts imports. As a fallback, walk __ZUSTAND__ if available.
const editorState = win.__VELXIO_EDITOR_STORE__?.getState?.();
const simState = win.__VELXIO_SIMULATOR_STORE__?.getState?.();
if (editorState && simState) return { editor: editorState, simulator: simState };
return null;
} catch { return null; }
}
function initVelxioBridge(iframe: HTMLIFrameElement) {
velxioIframe = iframe;
@ -473,45 +541,79 @@
velxioBridge = new VelxioBridge(iframe);
velxioReady = true;
if (!data) return;
velxioBridge.setEmbedMode({ hideAuth: true, hideComponentPicker: true });
if (data.velxio_circuit) velxioBridge.loadCircuit(data.velxio_circuit);
if (data.initial_code_arduino) {
velxioBridge.setEmbedMode({
hideAuth: true,
hideComponentPicker: true,
lockComponents: true
});
// Priority: Restore from localStorage if available, otherwise use data from backend
const savedCircuit = localStorage.getItem(arduinoCircuitKey);
const savedCode = localStorage.getItem(arduinoCodeKey);
if (savedCircuit) {
console.log('[Velxio Bridge] Restoring circuit from draft');
velxioBridge.loadCircuit(savedCircuit);
} else if (data.velxio_circuit) {
velxioBridge.loadCircuit(data.velxio_circuit);
}
if (savedCode) {
console.log('[Velxio Bridge] Restoring code from draft');
velxioBridge.loadCode([{ name: 'sketch.ino', content: savedCode }]);
} else if (data.initial_code_arduino) {
velxioBridge.loadCode([{ name: 'sketch.ino', content: data.initial_code_arduino }]);
}
}
if (type === 'velxio:compile_result' && e.data.success) {
setTimeout(() => handleVelxioSubmit(), 5000);
const timeout = data?.evaluation_config?.timeout_ms ?? 5000;
setTimeout(() => handleVelxioSubmit(), timeout);
}
};
window.addEventListener('message', onMessage);
// Fallback: if PostMessage bridge never connects, try direct iframe access (same-origin)
// and also use it to send initial data via postMessage directly
const pollReady = setInterval(() => {
if (settled) { clearInterval(pollReady); return; }
try {
const win = iframe.contentWindow as any;
if (!win || !win.document) return;
// Check if React app loaded by looking for the root element with content
const root = win.document.getElementById('root');
if (!root || !root.children.length) return;
// Iframe loaded — mark as ready even without velxio:ready message
settled = true;
clearInterval(pollReady);
velxioReady = true;
// Try sending commands directly via postMessage (same-origin, should work)
if (data) {
win.postMessage({ type: 'elemes:set_embed_mode', hideAuth: true, hideComponentPicker: true }, '*');
if (data.velxio_circuit) {
win.postMessage({
type: 'elemes:set_embed_mode',
hideAuth: true,
hideComponentPicker: true,
lockComponents: true
}, '*');
const savedCircuit = localStorage.getItem(arduinoCircuitKey);
const savedCode = localStorage.getItem(arduinoCodeKey);
if (savedCircuit) {
try {
console.log('[Velxio Fallback] Restoring circuit from draft');
const circuitData = JSON.parse(savedCircuit);
win.postMessage({ type: 'elemes:load_circuit', ...circuitData }, '*');
} catch {}
} else if (data.velxio_circuit) {
try {
const circuitData = JSON.parse(data.velxio_circuit);
win.postMessage({ type: 'elemes:load_circuit', ...circuitData }, '*');
} catch {}
}
if (data.initial_code_arduino) {
if (savedCode) {
console.log('[Velxio Fallback] Restoring code from draft');
win.postMessage({ type: 'elemes:load_code', files: [{ name: 'sketch.ino', content: savedCode }] }, '*');
} else if (data.initial_code_arduino) {
win.postMessage({ type: 'elemes:load_code', files: [{ name: 'sketch.ino', content: data.initial_code_arduino }] }, '*');
}
}
@ -523,8 +625,6 @@
if (settled) return;
settled = true;
window.removeEventListener('message', onMessage);
// Don't show error — the Submit button + direct access still works
// velxioError = true;
}, 30_000);
}
@ -535,87 +635,29 @@
activeTab = 'output';
try {
// === Gather data: try PostMessage bridge first, fall back to direct iframe access ===
// === Gather data ===
let sourceCode = '';
let serialLog = '';
let wireList: { start: { componentId: string; pinName: string }; end: { componentId: string; pinName: string } }[] = [];
let wireList: any[] = [];
const dbg: string[] = [];
// Try PostMessage bridge first for serial log (since it's harder to get from store)
if (velxioBridge) {
// Try bridge (PostMessage)
dbg.push('[metode: PostMessage bridge]');
const srcResp = await velxioBridge['request']('elemes:get_source_code', 'velxio:source_code');
if (srcResp) sourceCode = (srcResp.files as any[]).map((f: any) => f.content).join('\n');
else dbg.push('[!] get_source_code timeout');
const serResp = await velxioBridge['request']('elemes:get_serial_log', 'velxio:serial_log');
if (serResp) serialLog = serResp.log as string;
else dbg.push('[!] get_serial_log timeout');
const wireResp = await velxioBridge['request']('elemes:get_wires', 'velxio:wires');
if (wireResp) wireList = wireResp.wires as any[];
else dbg.push('[!] get_wires timeout');
}
// Fallback: direct iframe store access (same-origin)
if (!sourceCode && velxioIframe) {
dbg.push('[fallback: direct iframe access]');
// Use getVelxioState for code and wires (more reliable/complete)
const state = getVelxioState();
if (state) {
sourceCode = state.code;
try {
const win = velxioIframe.contentWindow as any;
// 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 (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}`);
}
const circuit = JSON.parse(state.circuit);
wireList = circuit.wires || [];
} catch {}
dbg.push('[metode: Zustand store]');
} else {
dbg.push('[!] Gagal mengakses simulator state');
}
// === Evaluate ===
@ -889,13 +931,18 @@
Hubungi guru jika masalah berlanjut.
</div>
{:else}
<div class="velxio-toolbar">
<!-- Submit button removed for cleaner workspace -->
</div>
{#if $authLoggedIn}
<div class="storage-indicator-inline" title={velxioSaving ? "Menyimpan draf..." : "Draf tersimpan di browser"}>
<span class="indicator-icon" class:saving={velxioSaving}>
{velxioSaving ? '●' : '☁'}
</span>
<span class="indicator-text">Auto-save</span>
</div>
{/if}
<!-- svelte-ignore a11y_missing_attribute -->
<iframe
class="velxio-iframe"
src="/velxio/editor?embed=true{hasArduinoCode ? '' : '&hideEditor=true'}"
src="/velxio/editor?embed=true{hasArduinoCode ? '' : '&hideEditor=true'}&lockComponents=true"
onload={(e) => initVelxioBridge(e.currentTarget as HTMLIFrameElement)}
allow="cross-origin-isolated"
></iframe>
@ -1203,13 +1250,35 @@
}
/* ── Velxio (Arduino simulator) ─────────────────────── */
.velxio-toolbar {
.storage-indicator-inline {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.4rem 0.5rem;
border-bottom: 1px solid var(--color-border);
flex-shrink: 0;
gap: 6px;
font-size: 0.75rem;
color: var(--color-text-muted);
background: var(--color-bg-secondary);
padding: 3px 10px;
border-radius: 12px;
border: 1px solid var(--color-border);
position: absolute;
bottom: 1rem;
right: 1.5rem;
z-index: 10;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);
pointer-events: none;
opacity: 0.8;
}
.storage-indicator-inline .indicator-icon {
line-height: 1;
font-size: 0.8rem;
color: var(--color-success);
}
.storage-indicator-inline .indicator-icon.saving {
color: var(--color-primary);
animation: pulse 1s infinite;
}
.storage-indicator-inline .indicator-text {
font-weight: 500;
}
.velxio-panel {
@ -1217,6 +1286,7 @@
flex-direction: column;
flex: 1;
min-height: 0;
position: relative;
}
.velxio-panel.tab-hidden {
display: none;

View File

@ -69,6 +69,15 @@ def api_lesson(filename):
velxio_circuit = parsed_data.get('velxio_circuit', '')
expected_serial_output = parsed_data.get('expected_serial_output', '')
expected_wiring = parsed_data.get('expected_wiring', '')
evaluation_config_raw = parsed_data.get('evaluation_config', '')
evaluation_config = {}
if evaluation_config_raw:
import json
try:
evaluation_config = json.loads(evaluation_config_raw)
except Exception:
pass
if not initial_code:
initial_code = (
@ -119,6 +128,7 @@ def api_lesson(filename):
'velxio_circuit': velxio_circuit,
'expected_serial_output': expected_serial_output,
'expected_wiring': expected_wiring,
'evaluation_config': evaluation_config,
'solution_code': solution_code,
'solution_circuit': solution_circuit,
'solution_python': solution_python,

View File

@ -340,6 +340,9 @@ def render_markdown_content(file_path):
expected_wiring, lesson_content = _extract_section(
lesson_content, '---EXPECTED_WIRING---', '---END_EXPECTED_WIRING---')
evaluation_config, lesson_content = _extract_section(
lesson_content, '---EVALUATION_CONFIG---', '---END_EVALUATION_CONFIG---')
# Just use whichever initial code matched as the generic 'initial_code' for simplicity
# if only one type exists, but return all as dictionary values.
# Typically frontend uses 'initial_code' for legacy.
@ -381,6 +384,7 @@ def render_markdown_content(file_path):
'velxio_circuit': velxio_circuit,
'expected_serial_output': expected_serial_output,
'expected_wiring': expected_wiring,
'evaluation_config': evaluation_config,
'active_tabs': active_tabs
}

2
velxio

@ -1 +1 @@
Subproject commit eb7f87dfbca01b1cbcd7ee05fdf90c10f10ad0fb
Subproject commit 9402b6c40f338b4c65ea275e42fb94341cd354db