update auto save for arduino velxio
parent
c466bc8bd4
commit
998472f996
|
|
@ -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 && auth.isLoggedIn && !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;
|
||||||
|
|
||||||
|
|
@ -474,8 +542,22 @@
|
||||||
velxioReady = true;
|
velxioReady = true;
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
velxioBridge.setEmbedMode({ hideAuth: true, hideComponentPicker: true });
|
velxioBridge.setEmbedMode({ hideAuth: true, hideComponentPicker: true });
|
||||||
if (data.velxio_circuit) velxioBridge.loadCircuit(data.velxio_circuit);
|
|
||||||
if (data.initial_code_arduino) {
|
// 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 }]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -487,31 +569,41 @@
|
||||||
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({ type: 'elemes:set_embed_mode', hideAuth: true, hideComponentPicker: true }, '*');
|
||||||
if (data.velxio_circuit) {
|
|
||||||
|
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 +615,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 +625,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');
|
|
||||||
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 {
|
} else {
|
||||||
dbg.push('[direct] editor store has no files');
|
dbg.push('[!] Gagal mengakses simulator state');
|
||||||
}
|
|
||||||
|
|
||||||
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 ===
|
||||||
|
|
@ -890,7 +922,15 @@
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="velxio-toolbar">
|
<div class="velxio-toolbar">
|
||||||
<!-- Submit button removed for cleaner workspace -->
|
<button type="button" class="btn btn-secondary btn-sm" onclick={handleReset}>Reset</button>
|
||||||
|
{#if auth.isLoggedIn}
|
||||||
|
<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}
|
||||||
</div>
|
</div>
|
||||||
<!-- svelte-ignore a11y_missing_attribute -->
|
<!-- svelte-ignore a11y_missing_attribute -->
|
||||||
<iframe
|
<iframe
|
||||||
|
|
@ -1211,6 +1251,27 @@
|
||||||
border-bottom: 1px solid var(--color-border);
|
border-bottom: 1px solid var(--color-border);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
.storage-indicator-inline {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
pointer-events: none;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
.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 {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue