Compare commits
5 Commits
c466bc8bd4
...
7c069660f6
| Author | SHA1 | Date |
|---|---|---|
|
|
7c069660f6 | |
|
|
7acce0b610 | |
|
|
405b6d8a6d | |
|
|
65c12bc716 | |
|
|
998472f996 |
|
|
@ -148,22 +148,40 @@ void loop() {
|
||||||
"board": "arduino:avr:uno",
|
"board": "arduino:avr:uno",
|
||||||
"components": [
|
"components": [
|
||||||
{
|
{
|
||||||
"type": "wokwi-pushbutton",
|
"type": "led",
|
||||||
"id": "button-1",
|
"id": "led-builtin",
|
||||||
"x": 400,
|
"x": 956.2066448009604,
|
||||||
"y": -100,
|
"y": -15.285273464964305,
|
||||||
"rotation": 0,
|
|
||||||
"props": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "wokwi-led",
|
|
||||||
"id": "led-1",
|
|
||||||
"x": 400,
|
|
||||||
"y": -250,
|
|
||||||
"rotation": 0,
|
"rotation": 0,
|
||||||
"props": {
|
"props": {
|
||||||
"color": "green",
|
"color": "red",
|
||||||
"pin": 13
|
"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": [
|
"wires": [
|
||||||
{
|
{
|
||||||
"start": { "componentId": "arduino-uno", "pinName": "2" },
|
"start": {
|
||||||
"end": { "componentId": "button-1", "pinName": "1.l" }
|
"componentId": "resistor-1776299815587-z0q0jzny8",
|
||||||
|
"pinName": "2"
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"componentId": "led-builtin",
|
||||||
|
"pinName": "A"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"start": { "componentId": "button-1", "pinName": "2.l" },
|
"start": {
|
||||||
"end": { "componentId": "arduino-uno", "pinName": "GND" }
|
"componentId": "arduino-uno",
|
||||||
|
"pinName": "GND.1"
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"componentId": "led-builtin",
|
||||||
|
"pinName": "C"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"start": { "componentId": "arduino-uno", "pinName": "13" },
|
"start": {
|
||||||
"end": { "componentId": "led-1", "pinName": "A" }
|
"componentId": "resistor-1776299815587-z0q0jzny8",
|
||||||
|
"pinName": "1"
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"componentId": "arduino-uno",
|
||||||
|
"pinName": "13"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"start": { "componentId": "led-1", "pinName": "C" },
|
"start": {
|
||||||
"end": { "componentId": "arduino-uno", "pinName": "GND" }
|
"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",
|
"board": "arduino:avr:uno",
|
||||||
"components": [
|
"components": [
|
||||||
{
|
{
|
||||||
"type": "wokwi-led",
|
"type": "led",
|
||||||
"id": "led-red",
|
"id": "led-red",
|
||||||
"x": 370,
|
"x": 409.6091884525257,
|
||||||
"y": -300,
|
"y": 65.40692157090741,
|
||||||
"rotation": 0,
|
"rotation": 0,
|
||||||
"props": {
|
"props": {
|
||||||
"color": "red",
|
"color": "red",
|
||||||
"pin": 13
|
"pin": 13,
|
||||||
|
"state": true,
|
||||||
|
"value": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "wokwi-led",
|
"type": "led",
|
||||||
"id": "led-yellow",
|
"id": "led-yellow",
|
||||||
"x": 370,
|
"x": 466.09101803924966,
|
||||||
"y": -200,
|
"y": 66.43228050989252,
|
||||||
"rotation": 0,
|
"rotation": 0,
|
||||||
"props": {
|
"props": {
|
||||||
"color": "yellow",
|
"color": "yellow",
|
||||||
"pin": 12
|
"pin": 12,
|
||||||
|
"state": false,
|
||||||
|
"value": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "wokwi-led",
|
"type": "led",
|
||||||
"id": "led-green",
|
"id": "led-green",
|
||||||
"x": 370,
|
"x": 522.0366933470766,
|
||||||
"y": -100,
|
"y": 63.613964067867315,
|
||||||
"rotation": 0,
|
"rotation": 0,
|
||||||
"props": {
|
"props": {
|
||||||
"color": "green",
|
"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---
|
---END_VELXIO_CIRCUIT---
|
||||||
|
|
||||||
|
|
@ -194,29 +235,95 @@ YELLOW
|
||||||
---EXPECTED_WIRING---
|
---EXPECTED_WIRING---
|
||||||
{
|
{
|
||||||
"wires": [
|
"wires": [
|
||||||
{
|
{
|
||||||
"start": { "componentId": "arduino-uno", "pinName": "13" },
|
"start": {
|
||||||
"end": { "componentId": "led-red", "pinName": "A" }
|
"componentId": "resistor-1776300566703-7pi4wei9j",
|
||||||
|
"pinName": "2"
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"componentId": "led-red",
|
||||||
|
"pinName": "A"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"start": { "componentId": "led-red", "pinName": "C" },
|
"start": {
|
||||||
"end": { "componentId": "arduino-uno", "pinName": "GND" }
|
"componentId": "resistor-1776300590088-k4l87u1rf",
|
||||||
|
"pinName": "2"
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"componentId": "led-yellow",
|
||||||
|
"pinName": "A"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"start": { "componentId": "arduino-uno", "pinName": "12" },
|
"start": {
|
||||||
"end": { "componentId": "led-yellow", "pinName": "A" }
|
"componentId": "resistor-1776300593575-rklkixiwy",
|
||||||
|
"pinName": "2"
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"componentId": "led-green",
|
||||||
|
"pinName": "A"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"start": { "componentId": "led-yellow", "pinName": "C" },
|
"start": {
|
||||||
"end": { "componentId": "arduino-uno", "pinName": "GND" }
|
"componentId": "arduino-uno",
|
||||||
|
"pinName": "GND.3"
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"componentId": "led-red",
|
||||||
|
"pinName": "C"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"start": { "componentId": "arduino-uno", "pinName": "11" },
|
"start": {
|
||||||
"end": { "componentId": "led-green", "pinName": "A" }
|
"componentId": "arduino-uno",
|
||||||
|
"pinName": "GND.3"
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"componentId": "led-yellow",
|
||||||
|
"pinName": "C"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"start": { "componentId": "led-green", "pinName": "C" },
|
"start": {
|
||||||
"end": { "componentId": "arduino-uno", "pinName": "GND" }
|
"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
|
Serial
|
||||||
delay
|
delay
|
||||||
---END_KEY_TEXT---
|
---END_KEY_TEXT---
|
||||||
|
|
||||||
|
---EVALUATION_CONFIG---
|
||||||
|
{
|
||||||
|
"timeout_ms": 10000
|
||||||
|
}
|
||||||
|
---END_EVALUATION_CONFIG---
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,8 @@
|
||||||
const LONG_PRESS_MS = 400;
|
const LONG_PRESS_MS = 400;
|
||||||
const DOUBLE_TAP_MS = 300;
|
const DOUBLE_TAP_MS = 300;
|
||||||
const HOLDING_TIMEOUT_MS = 5000;
|
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:
|
* State machine:
|
||||||
|
|
@ -46,12 +47,18 @@
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
const mql = window.matchMedia('(hover: none) and (pointer: coarse)');
|
const mql = window.matchMedia('(hover: none) and (pointer: coarse)');
|
||||||
isTouchDevice = mql.matches;
|
const checkTouch = () => {
|
||||||
const handler = (e: MediaQueryListEvent) => {
|
isTouchDevice = mql.matches && window.innerWidth < 768;
|
||||||
isTouchDevice = e.matches;
|
};
|
||||||
|
|
||||||
|
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 {
|
function getIframeTarget(x: number, y: number): Element | null {
|
||||||
|
|
@ -142,7 +149,7 @@
|
||||||
if (!overlayEl) return;
|
if (!overlayEl) return;
|
||||||
const rect = overlayEl.getBoundingClientRect();
|
const rect = overlayEl.getBoundingClientRect();
|
||||||
crosshairX = viewportX - rect.left;
|
crosshairX = viewportX - rect.left;
|
||||||
crosshairY = viewportY - rect.top + CURSOR_OFFSET_Y;
|
crosshairY = viewportY - rect.top + currentOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCrosshairViewport(): { x: number; y: number } {
|
function getCrosshairViewport(): { x: number; y: number } {
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<nav class="navbar">
|
<nav class="navbar" onclick={() => auth.recordActivity()}>
|
||||||
<div class="container navbar-inner">
|
<div class="container navbar-inner">
|
||||||
{#if $lessonContext}
|
{#if $lessonContext}
|
||||||
<!-- Lesson mode -->
|
<!-- 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);
|
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';
|
import { validateToken, login as apiLogin, logout as apiLogout } from '$services/api';
|
||||||
|
|
||||||
const STORAGE_KEY = 'student_token';
|
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 authToken = writable('');
|
||||||
export const authStudentName = writable('');
|
export const authStudentName = writable('');
|
||||||
export const authLoggedIn = writable(false);
|
export const authLoggedIn = writable(false);
|
||||||
export const authIsTeacher = 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 = {
|
export const auth = {
|
||||||
/** Current token value (non-reactive). */
|
/** Current token value (non-reactive). */
|
||||||
get token() { return get(authToken); },
|
get token() { return get(authToken); },
|
||||||
|
|
@ -25,9 +51,21 @@ export const auth = {
|
||||||
/** Restore session from localStorage on app mount. */
|
/** Restore session from localStorage on app mount. */
|
||||||
async init() {
|
async init() {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
const saved = localStorage.getItem(STORAGE_KEY);
|
const saved = localStorage.getItem(STORAGE_KEY);
|
||||||
|
const lastActive = localStorage.getItem(LAST_ACTIVE_KEY);
|
||||||
|
|
||||||
if (!saved) return;
|
if (!saved) return;
|
||||||
|
|
||||||
|
// Check for 1 day inactivity
|
||||||
|
if (lastActive) {
|
||||||
|
const inactiveTime = Date.now() - parseInt(lastActive, 10);
|
||||||
|
if (inactiveTime > MAX_INACTIVITY) {
|
||||||
|
clearAuthData();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await validateToken(saved);
|
const res = await validateToken(saved);
|
||||||
if (res.success && res.student_name) {
|
if (res.success && res.student_name) {
|
||||||
|
|
@ -35,17 +73,18 @@ export const auth = {
|
||||||
authStudentName.set(res.student_name);
|
authStudentName.set(res.student_name);
|
||||||
authLoggedIn.set(true);
|
authLoggedIn.set(true);
|
||||||
authIsTeacher.set(res.is_teacher ?? false);
|
authIsTeacher.set(res.is_teacher ?? false);
|
||||||
|
updateLastActive();
|
||||||
} else {
|
} else {
|
||||||
localStorage.removeItem(STORAGE_KEY);
|
clearAuthData();
|
||||||
sessionStorage.clear();
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
localStorage.removeItem(STORAGE_KEY);
|
clearAuthData();
|
||||||
sessionStorage.clear();
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async login(inputToken: string) {
|
async login(inputToken: string) {
|
||||||
|
clearAuthData();
|
||||||
|
|
||||||
const res = await apiLogin(inputToken);
|
const res = await apiLogin(inputToken);
|
||||||
if (res.success && res.student_name) {
|
if (res.success && res.student_name) {
|
||||||
authToken.set(inputToken);
|
authToken.set(inputToken);
|
||||||
|
|
@ -53,18 +92,29 @@ export const auth = {
|
||||||
authLoggedIn.set(true);
|
authLoggedIn.set(true);
|
||||||
authIsTeacher.set(res.is_teacher ?? false);
|
authIsTeacher.set(res.is_teacher ?? false);
|
||||||
localStorage.setItem(STORAGE_KEY, inputToken);
|
localStorage.setItem(STORAGE_KEY, inputToken);
|
||||||
sessionStorage.clear();
|
updateLastActive();
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
|
|
||||||
async logout() {
|
async logout() {
|
||||||
await apiLogout();
|
try {
|
||||||
|
await apiLogout();
|
||||||
|
} catch {
|
||||||
|
// ignore logout failure, proceed to clear local state
|
||||||
|
}
|
||||||
authToken.set('');
|
authToken.set('');
|
||||||
authStudentName.set('');
|
authStudentName.set('');
|
||||||
authLoggedIn.set(false);
|
authLoggedIn.set(false);
|
||||||
authIsTeacher.set(false);
|
authIsTeacher.set(false);
|
||||||
localStorage.removeItem(STORAGE_KEY);
|
clearAuthData();
|
||||||
sessionStorage.clear();
|
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: string;
|
||||||
language_display_name: string;
|
language_display_name: string;
|
||||||
active_tabs: string[];
|
active_tabs: string[];
|
||||||
|
evaluation_config: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,11 +51,89 @@
|
||||||
let isVelxio = $derived(data?.active_tabs?.includes('velxio') ?? false);
|
let isVelxio = $derived(data?.active_tabs?.includes('velxio') ?? false);
|
||||||
let velxioBridge = $state<VelxioBridge | null>(null);
|
let velxioBridge = $state<VelxioBridge | null>(null);
|
||||||
let velxioReady = $state(false);
|
let velxioReady = $state(false);
|
||||||
|
let velxioSaving = $state(false);
|
||||||
let velxioError = $state(false);
|
let velxioError = $state(false);
|
||||||
let velxioIframe = $state<HTMLIFrameElement | null>(null);
|
let velxioIframe = $state<HTMLIFrameElement | null>(null);
|
||||||
let velxioOut = $state(freshOutput());
|
let velxioOut = $state(freshOutput());
|
||||||
let hasArduinoCode = $derived(!!data?.initial_code_arduino);
|
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)?
|
// Derived: is this a hybrid lesson (has both code and circuit)?
|
||||||
let isHybrid = $derived(
|
let isHybrid = $derived(
|
||||||
(data?.active_tabs?.includes('c') || data?.active_tabs?.includes('python')) &&
|
(data?.active_tabs?.includes('c') || data?.active_tabs?.includes('python')) &&
|
||||||
|
|
@ -379,6 +457,17 @@
|
||||||
if (activeTab === 'circuit') {
|
if (activeTab === 'circuit') {
|
||||||
circuitEditor?.setCircuitText(data.initial_circuit || data.initial_code);
|
circuitEditor?.setCircuitText(data.initial_circuit || data.initial_code);
|
||||||
Object.assign(circuitOut, freshOutput());
|
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 {
|
} else {
|
||||||
const resetCode = currentLanguage === 'python'
|
const resetCode = currentLanguage === 'python'
|
||||||
? (data.initial_python || '')
|
? (data.initial_python || '')
|
||||||
|
|
@ -419,13 +508,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Velxio (Arduino simulator) ===
|
/** Match serial output using subsequence matching. */
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 {
|
function matchSerialSubsequence(actual: string, expected: string): boolean {
|
||||||
if (!expected) return true;
|
if (!expected) return true;
|
||||||
if (!actual) return false;
|
if (!actual) return false;
|
||||||
|
|
@ -436,7 +519,6 @@
|
||||||
let expectedIdx = 0;
|
let expectedIdx = 0;
|
||||||
for (const actualLine of actualLines) {
|
for (const actualLine of actualLines) {
|
||||||
if (expectedIdx < expectedLines.length) {
|
if (expectedIdx < expectedLines.length) {
|
||||||
// Check if expected line is a substring of actual line (case-insensitive)
|
|
||||||
if (actualLine.toLowerCase().includes(expectedLines[expectedIdx].toLowerCase())) {
|
if (actualLine.toLowerCase().includes(expectedLines[expectedIdx].toLowerCase())) {
|
||||||
expectedIdx++;
|
expectedIdx++;
|
||||||
}
|
}
|
||||||
|
|
@ -446,20 +528,6 @@
|
||||||
return expectedIdx === expectedLines.length;
|
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) {
|
function initVelxioBridge(iframe: HTMLIFrameElement) {
|
||||||
velxioIframe = iframe;
|
velxioIframe = iframe;
|
||||||
|
|
||||||
|
|
@ -473,45 +541,79 @@
|
||||||
velxioBridge = new VelxioBridge(iframe);
|
velxioBridge = new VelxioBridge(iframe);
|
||||||
velxioReady = true;
|
velxioReady = true;
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
velxioBridge.setEmbedMode({ hideAuth: true, hideComponentPicker: true });
|
velxioBridge.setEmbedMode({
|
||||||
if (data.velxio_circuit) velxioBridge.loadCircuit(data.velxio_circuit);
|
hideAuth: true,
|
||||||
if (data.initial_code_arduino) {
|
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 }]);
|
velxioBridge.loadCode([{ name: 'sketch.ino', content: data.initial_code_arduino }]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'velxio:compile_result' && e.data.success) {
|
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);
|
window.addEventListener('message', onMessage);
|
||||||
|
|
||||||
// Fallback: if PostMessage bridge never connects, try direct iframe access (same-origin)
|
// 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(() => {
|
const pollReady = setInterval(() => {
|
||||||
if (settled) { clearInterval(pollReady); return; }
|
if (settled) { clearInterval(pollReady); return; }
|
||||||
try {
|
try {
|
||||||
const win = iframe.contentWindow as any;
|
const win = iframe.contentWindow as any;
|
||||||
if (!win || !win.document) return;
|
if (!win || !win.document) return;
|
||||||
// Check if React app loaded by looking for the root element with content
|
|
||||||
const root = win.document.getElementById('root');
|
const root = win.document.getElementById('root');
|
||||||
if (!root || !root.children.length) return;
|
if (!root || !root.children.length) return;
|
||||||
|
|
||||||
// Iframe loaded — mark as ready even without velxio:ready message
|
|
||||||
settled = true;
|
settled = true;
|
||||||
clearInterval(pollReady);
|
clearInterval(pollReady);
|
||||||
velxioReady = true;
|
velxioReady = true;
|
||||||
|
|
||||||
// Try sending commands directly via postMessage (same-origin, should work)
|
|
||||||
if (data) {
|
if (data) {
|
||||||
win.postMessage({ type: 'elemes:set_embed_mode', hideAuth: true, hideComponentPicker: true }, '*');
|
win.postMessage({
|
||||||
if (data.velxio_circuit) {
|
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 {
|
try {
|
||||||
const circuitData = JSON.parse(data.velxio_circuit);
|
const circuitData = JSON.parse(data.velxio_circuit);
|
||||||
win.postMessage({ type: 'elemes:load_circuit', ...circuitData }, '*');
|
win.postMessage({ type: 'elemes:load_circuit', ...circuitData }, '*');
|
||||||
} catch {}
|
} 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 }] }, '*');
|
win.postMessage({ type: 'elemes:load_code', files: [{ name: 'sketch.ino', content: data.initial_code_arduino }] }, '*');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -523,8 +625,6 @@
|
||||||
if (settled) return;
|
if (settled) return;
|
||||||
settled = true;
|
settled = true;
|
||||||
window.removeEventListener('message', onMessage);
|
window.removeEventListener('message', onMessage);
|
||||||
// Don't show error — the Submit button + direct access still works
|
|
||||||
// velxioError = true;
|
|
||||||
}, 30_000);
|
}, 30_000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -535,87 +635,29 @@
|
||||||
activeTab = 'output';
|
activeTab = 'output';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// === Gather data: try PostMessage bridge first, fall back to direct iframe access ===
|
// === Gather data ===
|
||||||
let sourceCode = '';
|
let sourceCode = '';
|
||||||
let serialLog = '';
|
let serialLog = '';
|
||||||
let wireList: { start: { componentId: string; pinName: string }; end: { componentId: string; pinName: string } }[] = [];
|
let wireList: any[] = [];
|
||||||
const dbg: string[] = [];
|
const dbg: string[] = [];
|
||||||
|
|
||||||
|
// Try PostMessage bridge first for serial log (since it's harder to get from store)
|
||||||
if (velxioBridge) {
|
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');
|
const serResp = await velxioBridge['request']('elemes:get_serial_log', 'velxio:serial_log');
|
||||||
if (serResp) serialLog = serResp.log as string;
|
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)
|
// Use getVelxioState for code and wires (more reliable/complete)
|
||||||
if (!sourceCode && velxioIframe) {
|
const state = getVelxioState();
|
||||||
dbg.push('[fallback: direct iframe access]');
|
if (state) {
|
||||||
|
sourceCode = state.code;
|
||||||
try {
|
try {
|
||||||
const win = velxioIframe.contentWindow as any;
|
const circuit = JSON.parse(state.circuit);
|
||||||
|
wireList = circuit.wires || [];
|
||||||
// Check if stores are exposed
|
} catch {}
|
||||||
if (!win.__VELXIO_EDITOR_STORE__ || !win.__VELXIO_SIMULATOR_STORE__) {
|
dbg.push('[metode: Zustand store]');
|
||||||
dbg.push('[direct] WARNING: Stores not exposed on window');
|
} else {
|
||||||
dbg.push('[direct] Trying alternative access methods...');
|
dbg.push('[!] Gagal mengakses simulator state');
|
||||||
|
|
||||||
// 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}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Evaluate ===
|
// === Evaluate ===
|
||||||
|
|
@ -889,13 +931,18 @@
|
||||||
Hubungi guru jika masalah berlanjut.
|
Hubungi guru jika masalah berlanjut.
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="velxio-toolbar">
|
{#if $authLoggedIn}
|
||||||
<!-- Submit button removed for cleaner workspace -->
|
<div class="storage-indicator-inline" title={velxioSaving ? "Menyimpan draf..." : "Draf tersimpan di browser"}>
|
||||||
</div>
|
<span class="indicator-icon" class:saving={velxioSaving}>
|
||||||
|
{velxioSaving ? '●' : '☁'}
|
||||||
|
</span>
|
||||||
|
<span class="indicator-text">Auto-save</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<!-- svelte-ignore a11y_missing_attribute -->
|
<!-- svelte-ignore a11y_missing_attribute -->
|
||||||
<iframe
|
<iframe
|
||||||
class="velxio-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)}
|
onload={(e) => initVelxioBridge(e.currentTarget as HTMLIFrameElement)}
|
||||||
allow="cross-origin-isolated"
|
allow="cross-origin-isolated"
|
||||||
></iframe>
|
></iframe>
|
||||||
|
|
@ -1203,13 +1250,35 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Velxio (Arduino simulator) ─────────────────────── */
|
/* ── Velxio (Arduino simulator) ─────────────────────── */
|
||||||
.velxio-toolbar {
|
.storage-indicator-inline {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 6px;
|
||||||
padding: 0.4rem 0.5rem;
|
font-size: 0.75rem;
|
||||||
border-bottom: 1px solid var(--color-border);
|
color: var(--color-text-muted);
|
||||||
flex-shrink: 0;
|
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 {
|
.velxio-panel {
|
||||||
|
|
@ -1217,6 +1286,7 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
.velxio-panel.tab-hidden {
|
.velxio-panel.tab-hidden {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,15 @@ def api_lesson(filename):
|
||||||
velxio_circuit = parsed_data.get('velxio_circuit', '')
|
velxio_circuit = parsed_data.get('velxio_circuit', '')
|
||||||
expected_serial_output = parsed_data.get('expected_serial_output', '')
|
expected_serial_output = parsed_data.get('expected_serial_output', '')
|
||||||
expected_wiring = parsed_data.get('expected_wiring', '')
|
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:
|
if not initial_code:
|
||||||
initial_code = (
|
initial_code = (
|
||||||
|
|
@ -119,6 +128,7 @@ def api_lesson(filename):
|
||||||
'velxio_circuit': velxio_circuit,
|
'velxio_circuit': velxio_circuit,
|
||||||
'expected_serial_output': expected_serial_output,
|
'expected_serial_output': expected_serial_output,
|
||||||
'expected_wiring': expected_wiring,
|
'expected_wiring': expected_wiring,
|
||||||
|
'evaluation_config': evaluation_config,
|
||||||
'solution_code': solution_code,
|
'solution_code': solution_code,
|
||||||
'solution_circuit': solution_circuit,
|
'solution_circuit': solution_circuit,
|
||||||
'solution_python': solution_python,
|
'solution_python': solution_python,
|
||||||
|
|
|
||||||
|
|
@ -340,6 +340,9 @@ def render_markdown_content(file_path):
|
||||||
expected_wiring, lesson_content = _extract_section(
|
expected_wiring, lesson_content = _extract_section(
|
||||||
lesson_content, '---EXPECTED_WIRING---', '---END_EXPECTED_WIRING---')
|
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
|
# Just use whichever initial code matched as the generic 'initial_code' for simplicity
|
||||||
# if only one type exists, but return all as dictionary values.
|
# if only one type exists, but return all as dictionary values.
|
||||||
# Typically frontend uses 'initial_code' for legacy.
|
# Typically frontend uses 'initial_code' for legacy.
|
||||||
|
|
@ -381,6 +384,7 @@ def render_markdown_content(file_path):
|
||||||
'velxio_circuit': velxio_circuit,
|
'velxio_circuit': velxio_circuit,
|
||||||
'expected_serial_output': expected_serial_output,
|
'expected_serial_output': expected_serial_output,
|
||||||
'expected_wiring': expected_wiring,
|
'expected_wiring': expected_wiring,
|
||||||
|
'evaluation_config': evaluation_config,
|
||||||
'active_tabs': active_tabs
|
'active_tabs': active_tabs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
2
velxio
2
velxio
|
|
@ -1 +1 @@
|
||||||
Subproject commit eb7f87dfbca01b1cbcd7ee05fdf90c10f10ad0fb
|
Subproject commit 9402b6c40f338b4c65ea275e42fb94341cd354db
|
||||||
Loading…
Reference in New Issue