Compare commits
5 Commits
c466bc8bd4
...
7c069660f6
| Author | SHA1 | Date |
|---|---|---|
|
|
7c069660f6 | |
|
|
7acce0b610 | |
|
|
405b6d8a6d | |
|
|
65c12bc716 | |
|
|
998472f996 |
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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---
|
||||
|
|
|
|||
|
|
@ -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 } {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<nav class="navbar">
|
||||
<nav class="navbar" onclick={() => auth.recordActivity()}>
|
||||
<div class="container navbar-inner">
|
||||
{#if $lessonContext}
|
||||
<!-- Lesson mode -->
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -33,4 +33,5 @@ export interface LessonContent {
|
|||
language: string;
|
||||
language_display_name: string;
|
||||
active_tabs: string[];
|
||||
evaluation_config: Record<string, any>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
2
velxio
|
|
@ -1 +1 @@
|
|||
Subproject commit eb7f87dfbca01b1cbcd7ee05fdf90c10f10ad0fb
|
||||
Subproject commit 9402b6c40f338b4c65ea275e42fb94341cd354db
|
||||
Loading…
Reference in New Issue