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];
|
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 { describe, it, expect } from 'vitest';
|
||||||
import { extractSection } from './extractSections';
|
import { extractSection, upsertSection } from './extractSections';
|
||||||
import { detectActiveTabs } from './detectActiveTabs';
|
import { detectActiveTabs } from './detectActiveTabs';
|
||||||
import { processCircuitEmbeds, processFlowchartEmbeds, processEmbedEmbeds } from './processEmbeds';
|
import { processCircuitEmbeds, processFlowchartEmbeds, processEmbedEmbeds } from './processEmbeds';
|
||||||
import { parseSlides } from './parseSlides';
|
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', () => {
|
describe('detectActiveTabs', () => {
|
||||||
it('should detect velxio mode exclusively if INITIAL_CODE_ARDUINO is present', () => {
|
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---';
|
const input = '---INITIAL_CODE_ARDUINO---\nvoid setup() {}\n---END_INITIAL_CODE_ARDUINO---\n---INITIAL_CODE---\nint main() {}\n---END_INITIAL_CODE---';
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,77 @@
|
||||||
import { VelxioBridge } from '$services/velxio-bridge';
|
import { VelxioBridge } from '$services/velxio-bridge';
|
||||||
import type { LessonContent } from '$types/lesson';
|
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 {
|
export function getVelxioState(velxioIframe: HTMLIFrameElement | null): { code: string; circuit: string } | null {
|
||||||
if (!velxioIframe) return null;
|
if (!velxioIframe) return null;
|
||||||
try {
|
try {
|
||||||
const win = velxioIframe.contentWindow as any;
|
const win = velxioIframe.contentWindow as any;
|
||||||
if (!win) return null;
|
if (!win) return null;
|
||||||
|
|
||||||
const editorStore = win.__VELXIO_EDITOR_STORE__?.getState?.();
|
const editorStore = win.__VELXIO_EDITOR_STORE__?.getState?.();
|
||||||
const simStore = win.__VELXIO_SIMULATOR_STORE__?.getState?.();
|
const simStore = win.__VELXIO_SIMULATOR_STORE__?.getState?.();
|
||||||
|
|
||||||
if (!editorStore || !simStore) return null;
|
if (!editorStore || !simStore) return null;
|
||||||
|
|
||||||
// Extract code
|
// Extract active board and determine FQBN / board string
|
||||||
const code = (editorStore.files as any[] || []).map((f: any) => f.content).join('\n');
|
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)
|
// Extract circuit state (Diagram format compatible with elemes:load_circuit)
|
||||||
|
const components = (simStore.components as any[] || []).map((c: any) => ({
|
||||||
|
type: c.metadataId,
|
||||||
|
id: c.id,
|
||||||
|
x: Math.round((c.x || 0) * 10) / 10,
|
||||||
|
y: Math.round((c.y || 0) * 10) / 10,
|
||||||
|
rotation: c.properties?.rotation || 0,
|
||||||
|
props: { ...c.properties }
|
||||||
|
}));
|
||||||
|
|
||||||
|
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 = {
|
const circuit = {
|
||||||
board: simStore.activeBoardId,
|
board,
|
||||||
components: (simStore.components as any[] || []).map((c: any) => ({
|
components,
|
||||||
type: c.metadataId,
|
wires
|
||||||
id: c.id,
|
|
||||||
x: c.x,
|
|
||||||
y: c.y,
|
|
||||||
rotation: c.properties?.rotation || 0,
|
|
||||||
props: { ...c.properties }
|
|
||||||
})),
|
|
||||||
wires: simStore.wires || []
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
code,
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,18 +4,21 @@
|
||||||
import { createFloatingPanel } from '$actions/floatingPanel.svelte';
|
import { createFloatingPanel } from '$actions/floatingPanel.svelte';
|
||||||
import CodeEditor from '$components/CodeEditor.svelte';
|
import CodeEditor from '$components/CodeEditor.svelte';
|
||||||
import CircuitEditor from '$components/CircuitEditor.svelte';
|
import CircuitEditor from '$components/CircuitEditor.svelte';
|
||||||
|
import TeacherVelxioEditor from '$components/TeacherVelxioEditor.svelte';
|
||||||
import ContentFileTree from '$components/ContentFileTree.svelte';
|
import ContentFileTree from '$components/ContentFileTree.svelte';
|
||||||
import LessonContentView from '$components/LessonContentView.svelte';
|
import LessonContentView from '$components/LessonContentView.svelte';
|
||||||
import QuizPreviewReadonly from '$components/QuizPreviewReadonly.svelte';
|
import QuizPreviewReadonly from '$components/QuizPreviewReadonly.svelte';
|
||||||
import { ContentEditorManager } from '$services/contentEditor.svelte';
|
import { ContentEditorManager } from '$services/contentEditor.svelte';
|
||||||
import { getTree, createFolder, createFile, renameEntry, deleteEntry, uploadAsset } from '$services/contentEditor';
|
import { getTree, createFolder, createFile, renameEntry, deleteEntry, uploadAsset } from '$services/contentEditor';
|
||||||
import type { TreeNode } from '$services/contentEditor';
|
import type { TreeNode } from '$services/contentEditor';
|
||||||
|
import { upsertSection } from '$services/markdown';
|
||||||
|
|
||||||
const mgr = new ContentEditorManager();
|
const mgr = new ContentEditorManager();
|
||||||
const float = createFloatingPanel();
|
const float = createFloatingPanel();
|
||||||
|
|
||||||
let editorRef = $state<any>(null);
|
let editorRef = $state<any>(null);
|
||||||
let circuitEditorRef = $state<any>(null);
|
let circuitEditorRef = $state<any>(null);
|
||||||
|
let velxioEditorRef = $state<any>(null);
|
||||||
|
|
||||||
// Mobile: show tree or editor
|
// Mobile: show tree or editor
|
||||||
let mobileShowTree = $state(true);
|
let mobileShowTree = $state(true);
|
||||||
|
|
@ -83,6 +86,8 @@
|
||||||
return codes;
|
return codes;
|
||||||
})());
|
})());
|
||||||
|
|
||||||
|
let extractedCircuitVelxio = $derived(extractCode(mgr.body, 'VELXIO_CIRCUIT'));
|
||||||
|
|
||||||
// Tab label map for chrome tabs
|
// Tab label map for chrome tabs
|
||||||
let allTabs = $derived([
|
let allTabs = $derived([
|
||||||
{ id: 'editor', label: 'Editor' },
|
{ 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) {
|
function handleTabClick(tab: string) {
|
||||||
|
if (mgr.activeTab === 'velxio' && tab !== 'velxio') {
|
||||||
|
syncVelxioToMarkdown();
|
||||||
|
}
|
||||||
mgr.activeTab = tab;
|
mgr.activeTab = tab;
|
||||||
if (mgr.isMobile && mgr.mobileMode === 'hidden') {
|
if (mgr.isMobile && mgr.mobileMode === 'hidden') {
|
||||||
mgr.mobileMode = 'h50';
|
mgr.mobileMode = 'h50';
|
||||||
|
|
@ -584,10 +648,10 @@
|
||||||
{#snippet saveActions()}
|
{#snippet saveActions()}
|
||||||
{#if mgr.activePath && !isAssetFile}
|
{#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 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 ? '⏳' : '💾'}
|
{mgr.saving ? '⏳' : '💾'}
|
||||||
</button>
|
</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 ? '⏳' : '🚀'}
|
{mgr.publishing ? '⏳' : '🚀'}
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -802,10 +866,12 @@
|
||||||
</div>
|
</div>
|
||||||
{:else if dynTab.id === 'velxio'}
|
{:else if dynTab.id === 'velxio'}
|
||||||
<div class="velxio-tab-content">
|
<div class="velxio-tab-content">
|
||||||
<iframe class="velxio-iframe" src="/velxio/editor?embed=true&hideEditor=true&lockComponents=true" title="Velxio Arduino Editor"
|
<TeacherVelxioEditor
|
||||||
allow="cross-origin-isolated; fullscreen"
|
bind:this={velxioEditorRef}
|
||||||
allowfullscreen
|
initialCode={extractedCodes.velxio}
|
||||||
></iframe>
|
initialCircuit={extractedCircuitVelxio}
|
||||||
|
onSave={handleSaveVelxioFromTab}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{:else if dynTab.id === 'flowchart'}
|
{:else if dynTab.id === 'flowchart'}
|
||||||
<div class="flowchart-tab-content">
|
<div class="flowchart-tab-content">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue