feat(teacher): integrate full Velxio editor with bi-directional circuit and code sync
This commit is contained in:
parent
2a152c8559
commit
c3fc8dc8f1
|
|
@ -0,0 +1,316 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { getVelxioState } from '$services/velxio-manager';
|
||||
|
||||
let {
|
||||
initialCode = '',
|
||||
initialCircuit = '',
|
||||
onSave
|
||||
}: {
|
||||
initialCode?: string;
|
||||
initialCircuit?: string;
|
||||
onSave?: (state: { code: string; circuit: string }) => void;
|
||||
} = $props();
|
||||
|
||||
let iframeRef = $state<HTMLIFrameElement | null>(null);
|
||||
let isReady = $state(false);
|
||||
let saveFeedback = $state<string | null>(null);
|
||||
let feedbackTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const velxioSrc = '/velxio/editor?embed=true';
|
||||
|
||||
export function getCurrentState(): { code: string; circuit: string } | null {
|
||||
return getVelxioState(iframeRef);
|
||||
}
|
||||
|
||||
export function loadStateToSimulator(code: string, circuit: string) {
|
||||
if (!iframeRef?.contentWindow) return;
|
||||
const win = iframeRef.contentWindow;
|
||||
|
||||
// Teacher mode: embed mode with components and editor fully editable
|
||||
win.postMessage(
|
||||
{
|
||||
type: 'elemes:set_embed_mode',
|
||||
hideAuth: true,
|
||||
hideComponentPicker: false,
|
||||
lockComponents: false
|
||||
},
|
||||
'*'
|
||||
);
|
||||
|
||||
if (circuit) {
|
||||
try {
|
||||
const circuitData = typeof circuit === 'string' ? JSON.parse(circuit) : circuit;
|
||||
win.postMessage({ type: 'elemes:load_circuit', ...circuitData }, '*');
|
||||
} catch (err) {
|
||||
console.error('[TeacherVelxioEditor] Error parsing circuit JSON:', err);
|
||||
}
|
||||
}
|
||||
|
||||
if (code) {
|
||||
win.postMessage(
|
||||
{
|
||||
type: 'elemes:load_code',
|
||||
files: [{ name: 'sketch.ino', content: code }]
|
||||
},
|
||||
'*'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function reloadFromDocument() {
|
||||
loadStateToSimulator(initialCode, initialCircuit);
|
||||
showFeedback('Dimuat ulang dari dokumen');
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
const state = getCurrentState();
|
||||
if (!state) {
|
||||
showFeedback('Gagal membaca state simulator');
|
||||
return;
|
||||
}
|
||||
|
||||
if (onSave) {
|
||||
onSave(state);
|
||||
}
|
||||
showFeedback('Tersimpan ke dokumen!');
|
||||
}
|
||||
|
||||
function showFeedback(msg: string) {
|
||||
saveFeedback = msg;
|
||||
if (feedbackTimeout) clearTimeout(feedbackTimeout);
|
||||
feedbackTimeout = setTimeout(() => {
|
||||
saveFeedback = null;
|
||||
}, 2500);
|
||||
}
|
||||
|
||||
function handleIframeLoad() {
|
||||
checkIframeReady();
|
||||
}
|
||||
|
||||
function checkIframeReady() {
|
||||
try {
|
||||
const win = iframeRef?.contentWindow as any;
|
||||
if (!win || !win.document) return;
|
||||
const root = win.document.getElementById('root');
|
||||
if (root && root.children.length > 0) {
|
||||
if (!isReady) {
|
||||
isReady = true;
|
||||
loadStateToSimulator(initialCode, initialCircuit);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Cross-origin fallback or not yet loaded
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const onMessage = (e: MessageEvent) => {
|
||||
if (e.data?.type === 'velxio:ready') {
|
||||
isReady = true;
|
||||
loadStateToSimulator(initialCode, initialCircuit);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', onMessage);
|
||||
|
||||
const poll = setInterval(() => {
|
||||
if (isReady) {
|
||||
clearInterval(poll);
|
||||
return;
|
||||
}
|
||||
checkIframeReady();
|
||||
}, 600);
|
||||
|
||||
const pollTimeout = setTimeout(() => {
|
||||
clearInterval(poll);
|
||||
}, 15000);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('message', onMessage);
|
||||
clearInterval(poll);
|
||||
clearTimeout(pollTimeout);
|
||||
if (feedbackTimeout) clearTimeout(feedbackTimeout);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="teacher-velxio-container">
|
||||
<div class="teacher-velxio-toolbar">
|
||||
<div class="toolbar-status">
|
||||
<span class="status-dot" class:ready={isReady}></span>
|
||||
<span class="status-text">
|
||||
{isReady ? 'Simulator Siap (Mode Lengkap)' : 'Menghubungkan Simulator...'}
|
||||
</span>
|
||||
{#if saveFeedback}
|
||||
<span class="save-feedback">{saveFeedback}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="toolbar-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="toolbar-btn btn-reload"
|
||||
onclick={reloadFromDocument}
|
||||
title="Muat ulang rangkaian & kode dari dokumen markdown"
|
||||
disabled={!isReady}
|
||||
>
|
||||
<svg class="icon" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M23 4v6h-6"></path>
|
||||
<path d="M1 20v-6h6"></path>
|
||||
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path>
|
||||
</svg>
|
||||
<span>Muat Ulang</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="toolbar-btn btn-save"
|
||||
onclick={handleSave}
|
||||
title="Simpan rangkaian dan kode saat ini langsung ke dokumen markdown"
|
||||
disabled={!isReady}
|
||||
>
|
||||
<svg class="icon" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path>
|
||||
<polyline points="17 21 17 13 7 13 7 21"></polyline>
|
||||
<polyline points="7 3 7 8 15 8"></polyline>
|
||||
</svg>
|
||||
<span>Simpan ke Dokumen</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<iframe
|
||||
bind:this={iframeRef}
|
||||
class="velxio-iframe"
|
||||
src={velxioSrc}
|
||||
title="Velxio Arduino Simulator & Editor"
|
||||
onload={handleIframeLoad}
|
||||
allow="cross-origin-isolated; fullscreen"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.teacher-velxio-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: var(--color-bg, #ffffff);
|
||||
}
|
||||
|
||||
.teacher-velxio-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 14px;
|
||||
background: var(--color-bg-secondary, #f8f9fa);
|
||||
border-bottom: 1px solid var(--color-border, #dee2e6);
|
||||
font-size: 0.82rem;
|
||||
min-height: 40px;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.toolbar-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 50%;
|
||||
background: #ffc107;
|
||||
display: inline-block;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.status-dot.ready {
|
||||
background: #198754;
|
||||
box-shadow: 0 0 6px rgba(25, 135, 84, 0.4);
|
||||
}
|
||||
|
||||
.status-text {
|
||||
color: var(--color-text-muted, #555555);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.save-feedback {
|
||||
margin-left: 8px;
|
||||
padding: 2px 8px;
|
||||
background: rgba(25, 135, 84, 0.12);
|
||||
color: #198754;
|
||||
border-radius: 4px;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
animation: fadeIn 0.2s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.toolbar-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--color-border, #dee2e6);
|
||||
background: var(--color-bg, #ffffff);
|
||||
color: var(--color-text, #111111);
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.toolbar-btn:hover:not(:disabled) {
|
||||
background: var(--color-bg-secondary, #f0f0f0);
|
||||
}
|
||||
|
||||
.toolbar-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-save {
|
||||
background: var(--color-primary, #0d6efd);
|
||||
color: #ffffff;
|
||||
border-color: var(--color-primary, #0d6efd);
|
||||
}
|
||||
|
||||
.btn-save:hover:not(:disabled) {
|
||||
background: var(--color-primary-dark, #0a58ca);
|
||||
border-color: var(--color-primary-dark, #0a58ca);
|
||||
}
|
||||
|
||||
.icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.velxio-iframe {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -23,3 +23,31 @@ export function extractSection(
|
|||
|
||||
return [extracted, remaining];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the inner content between startMarker and endMarker in content,
|
||||
* or append the section block at the end if the markers are not present.
|
||||
*/
|
||||
export function upsertSection(
|
||||
content: string,
|
||||
startMarker: string,
|
||||
endMarker: string,
|
||||
newInnerContent: string
|
||||
): string {
|
||||
const trimmedInner = newInnerContent.trim();
|
||||
const startIdx = content.indexOf(startMarker);
|
||||
const endIdx = content.indexOf(endMarker);
|
||||
|
||||
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
||||
return (
|
||||
content.substring(0, startIdx + startMarker.length) +
|
||||
'\n' +
|
||||
trimmedInner +
|
||||
'\n' +
|
||||
content.substring(endIdx)
|
||||
);
|
||||
} else {
|
||||
const trimmedContent = content.trimEnd();
|
||||
return `${trimmedContent}\n\n${startMarker}\n${trimmedInner}\n${endMarker}\n`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -161,3 +161,5 @@ export function renderMarkdownPreview(rawMarkdown: string): RenderedPreview {
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
export { extractSection, upsertSection } from './extractSections';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { extractSection } from './extractSections';
|
||||
import { extractSection, upsertSection } from './extractSections';
|
||||
import { detectActiveTabs } from './detectActiveTabs';
|
||||
import { processCircuitEmbeds, processFlowchartEmbeds, processEmbedEmbeds } from './processEmbeds';
|
||||
import { parseSlides } from './parseSlides';
|
||||
|
|
@ -23,6 +23,20 @@ describe('Markdown Renderer & Extractor Tests', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('upsertSection', () => {
|
||||
it('should replace existing section content while preserving surrounding text', () => {
|
||||
const input = 'Header\n---INITIAL_CODE_ARDUINO---\nold code\n---END_INITIAL_CODE_ARDUINO---\nFooter';
|
||||
const result = upsertSection(input, '---INITIAL_CODE_ARDUINO---', '---END_INITIAL_CODE_ARDUINO---', 'new code\nvoid setup() {}');
|
||||
expect(result).toBe('Header\n---INITIAL_CODE_ARDUINO---\nnew code\nvoid setup() {}\n---END_INITIAL_CODE_ARDUINO---\nFooter');
|
||||
});
|
||||
|
||||
it('should append section to content if markers do not exist', () => {
|
||||
const input = 'Header content';
|
||||
const result = upsertSection(input, '---VELXIO_CIRCUIT---', '---END_VELXIO_CIRCUIT---', '{"board": "arduino:avr:uno"}');
|
||||
expect(result).toBe('Header content\n\n---VELXIO_CIRCUIT---\n{"board": "arduino:avr:uno"}\n---END_VELXIO_CIRCUIT---\n');
|
||||
});
|
||||
});
|
||||
|
||||
describe('detectActiveTabs', () => {
|
||||
it('should detect velxio mode exclusively if INITIAL_CODE_ARDUINO is present', () => {
|
||||
const input = '---INITIAL_CODE_ARDUINO---\nvoid setup() {}\n---END_INITIAL_CODE_ARDUINO---\n---INITIAL_CODE---\nint main() {}\n---END_INITIAL_CODE---';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,15 @@
|
|||
import { VelxioBridge } from '$services/velxio-bridge';
|
||||
import type { LessonContent } from '$types/lesson';
|
||||
|
||||
const BOARD_KIND_MAP: Record<string, string> = {
|
||||
'arduino-uno': 'arduino:avr:uno',
|
||||
'arduino-nano': 'arduino:avr:nano',
|
||||
'arduino-mega': 'arduino:avr:mega',
|
||||
'raspberry-pi-pico': 'rp2040:rp2040:rp2040',
|
||||
'rp2040-pico': 'rp2040:rp2040:rp2040',
|
||||
'esp32': 'esp32:esp32:esp32'
|
||||
};
|
||||
|
||||
export function getVelxioState(velxioIframe: HTMLIFrameElement | null): { code: string; circuit: string } | null {
|
||||
if (!velxioIframe) return null;
|
||||
try {
|
||||
|
|
@ -12,28 +21,57 @@ export function getVelxioState(velxioIframe: HTMLIFrameElement | null): { code:
|
|||
|
||||
if (!editorStore || !simStore) return null;
|
||||
|
||||
// Extract code
|
||||
const code = (editorStore.files as any[] || []).map((f: any) => f.content).join('\n');
|
||||
// Extract active board and determine FQBN / board string
|
||||
const activeBoard = simStore.boards?.find((b: any) => b.id === simStore.activeBoardId) ?? simStore.boards?.[0];
|
||||
const boardKind = activeBoard?.boardKind || simStore.activeBoardId || 'arduino-uno';
|
||||
const board = BOARD_KIND_MAP[boardKind] || (typeof boardKind === 'string' && boardKind.includes(':') ? boardKind : 'arduino:avr:uno');
|
||||
|
||||
// Extract code from active board file group or fallback to primary .ino / files
|
||||
const activeGroupId = activeBoard?.activeFileGroupId ?? '';
|
||||
const activeFiles = (editorStore.fileGroups?.[activeGroupId]?.length
|
||||
? editorStore.fileGroups[activeGroupId]
|
||||
: editorStore.files) ?? editorStore.files ?? [];
|
||||
|
||||
const code = activeFiles.find((f: any) => f.name?.endsWith('.ino'))?.content
|
||||
?? activeFiles[0]?.content
|
||||
?? (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: any) => ({
|
||||
const components = (simStore.components as any[] || []).map((c: any) => ({
|
||||
type: c.metadataId,
|
||||
id: c.id,
|
||||
x: c.x,
|
||||
y: c.y,
|
||||
x: Math.round((c.x || 0) * 10) / 10,
|
||||
y: Math.round((c.y || 0) * 10) / 10,
|
||||
rotation: c.properties?.rotation || 0,
|
||||
props: { ...c.properties }
|
||||
})),
|
||||
wires: simStore.wires || []
|
||||
}));
|
||||
|
||||
const wires = (simStore.wires as any[] || [])
|
||||
.filter((w: any) => w && w.start && w.end)
|
||||
.map((w: any) => ({
|
||||
start: {
|
||||
componentId: w.start?.componentId,
|
||||
pinName: w.start?.pinName
|
||||
},
|
||||
end: {
|
||||
componentId: w.end?.componentId,
|
||||
pinName: w.end?.pinName
|
||||
}
|
||||
}));
|
||||
|
||||
const circuit = {
|
||||
board,
|
||||
components,
|
||||
wires
|
||||
};
|
||||
|
||||
return {
|
||||
code,
|
||||
circuit: JSON.stringify(circuit)
|
||||
circuit: JSON.stringify(circuit, null, 2)
|
||||
};
|
||||
} catch {
|
||||
} catch (e) {
|
||||
console.error('[velxio-manager] Error getting Velxio state:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,18 +4,21 @@
|
|||
import { createFloatingPanel } from '$actions/floatingPanel.svelte';
|
||||
import CodeEditor from '$components/CodeEditor.svelte';
|
||||
import CircuitEditor from '$components/CircuitEditor.svelte';
|
||||
import TeacherVelxioEditor from '$components/TeacherVelxioEditor.svelte';
|
||||
import ContentFileTree from '$components/ContentFileTree.svelte';
|
||||
import LessonContentView from '$components/LessonContentView.svelte';
|
||||
import QuizPreviewReadonly from '$components/QuizPreviewReadonly.svelte';
|
||||
import { ContentEditorManager } from '$services/contentEditor.svelte';
|
||||
import { getTree, createFolder, createFile, renameEntry, deleteEntry, uploadAsset } from '$services/contentEditor';
|
||||
import type { TreeNode } from '$services/contentEditor';
|
||||
import { upsertSection } from '$services/markdown';
|
||||
|
||||
const mgr = new ContentEditorManager();
|
||||
const float = createFloatingPanel();
|
||||
|
||||
let editorRef = $state<any>(null);
|
||||
let circuitEditorRef = $state<any>(null);
|
||||
let velxioEditorRef = $state<any>(null);
|
||||
|
||||
// Mobile: show tree or editor
|
||||
let mobileShowTree = $state(true);
|
||||
|
|
@ -83,6 +86,8 @@
|
|||
return codes;
|
||||
})());
|
||||
|
||||
let extractedCircuitVelxio = $derived(extractCode(mgr.body, 'VELXIO_CIRCUIT'));
|
||||
|
||||
// Tab label map for chrome tabs
|
||||
let allTabs = $derived([
|
||||
{ id: 'editor', label: 'Editor' },
|
||||
|
|
@ -167,7 +172,66 @@
|
|||
}
|
||||
}
|
||||
|
||||
function syncVelxioToMarkdown(): boolean {
|
||||
if (!velxioEditorRef) return false;
|
||||
const state = velxioEditorRef.getCurrentState();
|
||||
if (!state) return false;
|
||||
applyVelxioToBody(state.code, state.circuit);
|
||||
return true;
|
||||
}
|
||||
|
||||
function applyVelxioToBody(code: string, circuitJson: string) {
|
||||
let currentBody = mgr.body;
|
||||
|
||||
if (code !== undefined) {
|
||||
currentBody = upsertSection(
|
||||
currentBody,
|
||||
'---INITIAL_CODE_ARDUINO---',
|
||||
'---END_INITIAL_CODE_ARDUINO---',
|
||||
code.trim()
|
||||
);
|
||||
}
|
||||
|
||||
if (circuitJson !== undefined) {
|
||||
currentBody = upsertSection(
|
||||
currentBody,
|
||||
'---VELXIO_CIRCUIT---',
|
||||
'---END_VELXIO_CIRCUIT---',
|
||||
circuitJson.trim()
|
||||
);
|
||||
}
|
||||
|
||||
if (currentBody !== mgr.body) {
|
||||
mgr.onBodyChange(currentBody);
|
||||
if (editorRef) {
|
||||
editorRef.setCode(currentBody);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveVelxioFromTab(state: { code: string; circuit: string }) {
|
||||
applyVelxioToBody(state.code, state.circuit);
|
||||
await mgr.handleSave();
|
||||
}
|
||||
|
||||
async function handleSaveAction() {
|
||||
if (mgr.activeTab === 'velxio') {
|
||||
syncVelxioToMarkdown();
|
||||
}
|
||||
await mgr.handleSave();
|
||||
}
|
||||
|
||||
async function handlePublishAction() {
|
||||
if (mgr.activeTab === 'velxio') {
|
||||
syncVelxioToMarkdown();
|
||||
}
|
||||
await mgr.handlePublish();
|
||||
}
|
||||
|
||||
function handleTabClick(tab: string) {
|
||||
if (mgr.activeTab === 'velxio' && tab !== 'velxio') {
|
||||
syncVelxioToMarkdown();
|
||||
}
|
||||
mgr.activeTab = tab;
|
||||
if (mgr.isMobile && mgr.mobileMode === 'hidden') {
|
||||
mgr.mobileMode = 'h50';
|
||||
|
|
@ -584,10 +648,10 @@
|
|||
{#snippet saveActions()}
|
||||
{#if mgr.activePath && !isAssetFile}
|
||||
<button class="panel-btn toolbar-toggle" class:active={featureSidebarOpen} onclick={() => featureSidebarOpen = !featureSidebarOpen} title="Tampilkan/sembunyikan panel Fitur">🧩</button>
|
||||
<button class="panel-btn save-btn" onclick={() => mgr.handleSave()} disabled={mgr.saving || !mgr.dirty} title="Simpan">
|
||||
<button class="panel-btn save-btn" onclick={() => handleSaveAction()} disabled={mgr.saving || !mgr.dirty} title="Simpan">
|
||||
{mgr.saving ? '⏳' : '💾'}
|
||||
</button>
|
||||
<button class="panel-btn publish-btn" onclick={() => mgr.handlePublish()} disabled={mgr.publishing || !mgr.draftId} title="Publish">
|
||||
<button class="panel-btn publish-btn" onclick={() => handlePublishAction()} disabled={mgr.publishing || !mgr.draftId} title="Publish">
|
||||
{mgr.publishing ? '⏳' : '🚀'}
|
||||
</button>
|
||||
{/if}
|
||||
|
|
@ -802,10 +866,12 @@
|
|||
</div>
|
||||
{:else if dynTab.id === 'velxio'}
|
||||
<div class="velxio-tab-content">
|
||||
<iframe class="velxio-iframe" src="/velxio/editor?embed=true&hideEditor=true&lockComponents=true" title="Velxio Arduino Editor"
|
||||
allow="cross-origin-isolated; fullscreen"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
<TeacherVelxioEditor
|
||||
bind:this={velxioEditorRef}
|
||||
initialCode={extractedCodes.velxio}
|
||||
initialCircuit={extractedCircuitVelxio}
|
||||
onSave={handleSaveVelxioFromTab}
|
||||
/>
|
||||
</div>
|
||||
{:else if dynTab.id === 'flowchart'}
|
||||
<div class="flowchart-tab-content">
|
||||
|
|
|
|||
Loading…
Reference in New Issue