feat(flowchart): implement text-based logic with auto-layout and migrate evaluation to frontend

This commit is contained in:
a2nr 2026-05-02 18:47:46 +07:00
parent 0ff56ed9d2
commit 433c095394
11 changed files with 511 additions and 38 deletions

View File

@ -16,6 +16,8 @@
"svelte": "^5.55.4",
"svelte-check": "^4.4.6",
"typescript": "~6.0.2",
"vite": "^8.0.10"
"vite": "^8.0.10",
"dagre": "^0.8.5",
"@types/dagre": "^0.7.52"
}
}

View File

@ -5,6 +5,8 @@
import Properties from './lib/Properties.svelte';
import Canvas from './lib/Canvas.svelte';
import { fcState } from './lib/flowchartState.svelte';
import { parseFlowchartText, exportToFlowchartText } from './lib/parser';
import { applyAutoLayout } from './lib/layout';
import { onMount } from 'svelte';
onMount(() => {
@ -21,26 +23,42 @@
window.addEventListener('message', (event) => {
if (event.data?.type === 'FLOWCHART_LOAD') {
try {
const payload = JSON.parse(event.data.payload);
const payload = typeof event.data.payload === 'string' ? JSON.parse(event.data.payload) : event.data.payload;
function processData(data: any) {
if (typeof data === 'string' && !data.trim().startsWith('{')) {
const { shapes, arrows } = parseFlowchartText(data);
return applyAutoLayout(shapes, arrows);
}
return data;
}
// Legacy format (just data object)
if (!payload.initialData && !payload.draftData && payload.shapes) {
fcState.initialData = payload;
fcState.loadData(payload);
if (!payload.initialData && !payload.draftData && (payload.shapes || typeof payload === 'string')) {
const data = processData(payload);
fcState.initialData = data;
fcState.loadData(data);
} else {
// New format { initialData, draftData }
if (payload.initialData) {
fcState.initialData = payload.initialData;
fcState.initialData = processData(payload.initialData);
}
if (payload.draftData) {
fcState.loadData(payload.draftData);
fcState.loadData(processData(payload.draftData));
} else if (payload.initialData) {
fcState.loadData(payload.initialData);
fcState.loadData(processData(payload.initialData));
}
}
} catch(e) {
console.error("Invalid data format", e);
}
} else if (event.data?.type === 'FLOWCHART_GET_TEXT') {
const text = exportToFlowchartText(fcState.shapes, fcState.arrows);
window.parent.postMessage({
type: 'FLOWCHART_TEXT_RESPONSE',
requestId: event.data.requestId,
payload: text
}, '*');
}
});

View File

@ -55,7 +55,7 @@
<span class="label-text">Warna Isi</span>
<div class="color-palette">
{#each predefinedColors as color}
<button class="color-btn" style="background: {color};" class:active={sharedFillColor === color} onclick={() => updateShape('fillColor', color)}></button>
<button class="color-btn" title={color} style="background: {color};" class:active={sharedFillColor === color} onclick={() => updateShape('fillColor', color)}></button>
{/each}
</div>
</div>
@ -87,7 +87,7 @@
<span class="label-text">Warna Garis</span>
<div class="color-palette">
{#each predefinedColors as color}
<button class="color-btn" style="background: {color};" class:active={sharedStrokeColor === color} onclick={() => {
<button class="color-btn" title={color} style="background: {color};" class:active={sharedStrokeColor === color} onclick={() => {
if (selectedShapes.length) updateShape('strokeColor', color);
if (selectedArrows.length) updateArrow('strokeColor', color);
}}></button>

View File

@ -1,5 +1,7 @@
<script lang="ts">
import { fcState } from './flowchartState.svelte';
import { parseFlowchartText, exportToFlowchartText } from './parser';
import { applyAutoLayout } from './layout';
let fileInput: HTMLInputElement;
@ -27,9 +29,12 @@
}
function handleNew() {
if (fcState.isIframeMode && fcState.initialData) {
if (fcState.isIframeMode) {
if (confirm('Kembalikan flowchart ke kondisi awal? Perubahan yang belum disimpan akan hilang.')) {
fcState.loadData(fcState.initialData);
// Notify parent to reset and re-send initial data
if (window.parent && window.parent !== window) {
window.parent.postMessage({ type: 'FLOWCHART_RESET' }, '*');
}
}
} else {
if (confirm('Buat flowchart baru? Perubahan yang belum disimpan akan hilang.')) {
@ -47,14 +52,8 @@
}
function handleExport() {
const data = JSON.stringify({
shapes: fcState.shapes,
arrows: fcState.arrows,
zoom: fcState.zoom,
panX: fcState.panX,
panY: fcState.panY
});
const blob = new Blob([data], { type: 'application/json' });
const text = exportToFlowchartText(fcState.shapes, fcState.arrows);
const blob = new Blob([text], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
@ -75,9 +74,18 @@
reader.onload = (ev) => {
try {
const content = ev.target?.result as string;
const data = JSON.parse(content);
fcState.loadData(data);
if (content.trim().startsWith('{')) {
// Legacy JSON support
const data = JSON.parse(content);
fcState.loadData(data);
} else {
// New Text-based parsing with Auto-Layout
const { shapes, arrows } = parseFlowchartText(content);
const layouted = applyAutoLayout(shapes, arrows);
fcState.loadData(layouted);
}
} catch (err) {
console.error(err);
alert("Format file tidak valid!");
}
};
@ -149,7 +157,7 @@
<!-- MOBILE MENU ACTION -->
<div class="topbar-actions mobile-only">
<div class="dropdown-container">
<button class="topbar-btn" onclick={(e) => { e.stopPropagation(); isMenuOpen = !isMenuOpen; }}>
<button class="topbar-btn" title="Menu" onclick={(e) => { e.stopPropagation(); isMenuOpen = !isMenuOpen; }}>
<svg><use href="#icon-menu"/></svg>
</button>
{#if isMenuOpen}

View File

@ -0,0 +1,55 @@
import dagre from 'dagre';
import type { Shape, Arrow } from './flowchartState.svelte';
export function applyAutoLayout(shapes: Shape[], arrows: Arrow[]): { shapes: Shape[], arrows: Arrow[] } {
const g = new dagre.graphlib.Graph();
// Set an object for the graph label
g.setGraph({
rankdir: 'TB',
nodesep: 50,
ranksep: 70,
marginx: 50,
marginy: 50
});
// Default to assigning a new object as a label for each edge.
g.setDefaultEdgeLabel(() => ({}));
// Add nodes to the graph
shapes.forEach(shape => {
// Estimate width based on text length if it's too long
const estimatedW = Math.max(shape.w, shape.text.length * 8 + 40);
g.setNode(shape.id, { width: estimatedW, height: shape.h });
// Update shape width for the canvas
shape.w = estimatedW;
});
// Add edges to the graph
arrows.forEach(arrow => {
g.setEdge(arrow.fromId, arrow.toId);
});
// Calculate layout
dagre.layout(g);
// Update shape coordinates
shapes.forEach(shape => {
const node = g.node(shape.id);
if (node) {
// Dagre uses center coordinates, we use top-left
shape.x = node.x - node.width / 2;
shape.y = node.y - node.height / 2;
}
});
// For arrows, we let the canvas logic or a simple straight line handle it for now
// unless we want to use dagre's edge points.
// Straight lines are easier to manage with manual dragging later.
arrows.forEach(arrow => {
arrow.points = []; // Reset points for straight lines
arrow.routing = 'straight';
});
return { shapes, arrows };
}

109
flowchart/src/lib/parser.ts Normal file
View File

@ -0,0 +1,109 @@
import type { Shape, Arrow } from './flowchartState.svelte';
export interface ParsedData {
shapes: Shape[];
arrows: Arrow[];
}
export function parseFlowchartText(text: string): ParsedData {
const lines = text.split('\n');
const shapes: Shape[] = [];
const arrows: Arrow[] = [];
const shapeMap = new Map<string, Shape>();
// Regex patterns
// Node: id[type] "Label"
const nodeRegex = /^(\w+)(?:\[(\w+)\])?(?:\s+"([^"]+)")?$/;
// Edge: from --> to "Label"
const edgeRegex = /^(\w+)\s*-->\s*(\w+)(?:\s+"([^"]+)")?$/;
let nextAutoId = 1;
function getOrCreateShape(id: string, typeStr?: string, label?: string): Shape {
if (shapeMap.has(id)) {
const existing = shapeMap.get(id)!;
if (typeStr) existing.type = typeStr as any;
if (label) existing.text = label;
return existing;
}
const shape: Shape = {
id,
type: (typeStr as any) || 'rect',
x: 0,
y: 0,
w: 180,
h: 50,
r: typeStr === 'roundrect' ? 25 : 0,
text: label || id,
fillColor: '#ffffff',
strokeColor: '#000000',
strokeWidth: 2
};
// Adjust defaults based on type
if (shape.type === 'circle') {
shape.w = 80;
shape.h = 80;
} else if (shape.type === 'diamond') {
shape.w = 150;
shape.h = 80;
}
shapes.push(shape);
shapeMap.set(id, shape);
return shape;
}
for (let line of lines) {
line = line.trim();
if (!line || line.startsWith('#') || line.startsWith('//')) continue;
const edgeMatch = line.match(edgeRegex);
if (edgeMatch) {
const [, fromId, toId, label] = edgeMatch;
getOrCreateShape(fromId);
getOrCreateShape(toId);
arrows.push({
id: `a_${nextAutoId++}`,
type: 'arrow',
fromId,
toId,
label: label || '',
strokeColor: '#000000',
strokeWidth: 2,
routing: 'straight',
points: []
});
continue;
}
const nodeMatch = line.match(nodeRegex);
if (nodeMatch) {
const [, id, type, label] = nodeMatch;
getOrCreateShape(id, type, label);
}
}
return { shapes, arrows };
}
export function exportToFlowchartText(shapes: Shape[], arrows: Arrow[]): string {
let text = "";
// Export Nodes
shapes.forEach(s => {
text += `${s.id}[${s.type}] "${s.text.replace(/"/g, '\\"')}"\n`;
});
if (shapes.length > 0) text += "\n";
// Export Edges
arrows.forEach(a => {
const label = a.label ? ` "${a.label.replace(/"/g, '\\"')}"` : "";
text += `${a.fromId} --> ${a.toId}${label}\n`;
});
return text;
}

View File

@ -0,0 +1,127 @@
export interface FlowchartEvalResult {
pass: boolean;
score: number;
output: string;
feedback: string[];
}
interface ParsedGraph {
nodes: Map<string, { type: string; text: string }>;
edges: Array<{ from: string; to: string; label: string }>;
}
function parseToGraph(text: string): ParsedGraph {
const nodes = new Map<string, { type: string; text: string }>();
const edges: Array<{ from: string; to: string; label: string }> = [];
const nodeRegex = /^(\w+)(?:\[(\w+)\])?(?:\s+"([^"]+)")?$/;
const edgeRegex = /^(\w+)\s*-->\s*(\w+)(?:\s+"([^"]+)")?$/;
const lines = text.split('\n');
for (let line of lines) {
line = line.trim();
if (!line || line.startsWith('#') || line.startsWith('//')) continue;
const edgeMatch = line.match(edgeRegex);
if (edgeMatch) {
const [, u, v, label] = edgeMatch;
edges.push({ from: u, to: v, label: (label || "").toLowerCase().trim() });
continue;
}
const nodeMatch = line.match(nodeRegex);
if (nodeMatch) {
const [, id, type, nodeText] = nodeMatch;
nodes.set(id, {
type: type || "rect",
text: (nodeText || id).toLowerCase().trim()
});
}
}
return { nodes, edges };
}
/**
* Evaluates student flowchart against reference using keyword and connection matching.
*/
export function evaluateFlowchartSubmission(
studentText: string,
referenceText: string
): FlowchartEvalResult {
if (!referenceText) {
return { pass: true, score: 100, output: "Luar biasa! Alur logika Anda sempurna.", feedback: [] };
}
const student = parseToGraph(studentText);
const reference = parseToGraph(referenceText);
let totalPoints = 0;
const feedback: string[] = [];
// 1. Keyword Matching (40 points)
const refTexts = Array.from(reference.nodes.values()).map(n => n.text);
const stuTexts = Array.from(student.nodes.values()).map(n => n.text);
let foundKeywords = 0;
for (const refText of refTexts) {
const found = stuTexts.some(st => st.includes(refText) || refText.includes(st));
if (found) {
foundKeywords++;
} else {
feedback.push(`❌ Kurang instruksi: "${refText}"`);
}
}
const keywordScore = refTexts.length > 0 ? (foundKeywords / refTexts.length) * 40 : 40;
totalPoints += keywordScore;
// 2. Connection Matching (60 points)
// Map student IDs to reference IDs based on keyword matching
const idMap = new Map<string, string>();
for (const [sId, sNode] of student.nodes.entries()) {
for (const [rId, rNode] of reference.nodes.entries()) {
if (sNode.text.includes(rNode.text) || rNode.text.includes(sNode.text)) {
idMap.set(sId, rId);
break;
}
}
}
const stuEdgesMapped = student.edges
.filter(e => idMap.has(e.from) && idMap.has(e.to))
.map(e => ({ from: idMap.get(e.from)!, to: idMap.get(e.to)!, label: e.label }));
let foundEdges = 0;
for (const refEdge of reference.edges) {
const found = stuEdgesMapped.some(se =>
se.from === refEdge.from && se.to === refEdge.to
);
if (found) {
foundEdges++;
} else {
const uText = reference.nodes.get(refEdge.from)?.text || refEdge.from;
const vText = reference.nodes.get(refEdge.to)?.text || refEdge.to;
feedback.push(`❌ Alur terputus: "${uText}" harus terhubung ke "${vText}"`);
}
}
const connectionScore = reference.edges.length > 0 ? (foundEdges / reference.edges.length) * 60 : 60;
totalPoints += connectionScore;
const finalScore = Math.round(totalPoints);
const pass = finalScore >= 70;
if (pass && feedback.length === 0) {
feedback.push("✅ Luar biasa! Alur logika Anda sempurna.");
} else if (pass) {
feedback.push("⚠️ Alur logika cukup baik, tapi ada beberapa bagian yang kurang tepat.");
}
return {
pass,
score: finalScore,
output: feedback.join('\n'),
feedback
};
}

View File

@ -15,6 +15,7 @@
import { compileCode, trackProgress } from '$services/api';
import { checkKeyText, validateNodes } from '$services/exercise';
import { evaluateVelxioSubmission } from '$services/velxio-evaluator';
import { evaluateFlowchartSubmission } from '$services/flowchart-evaluator';
import { evaluateCircuitSubmission, processLanguageEvaluation } from '$services/evaluators';
import { getVelxioState, initVelxioBridge } from '$services/velxio-manager';
import { VelxioBridge, type EvaluationResult } from '$services/velxio-bridge';
@ -56,6 +57,7 @@
let cPassed = $state(false);
let pythonPassed = $state(false);
let circuitPassed = $state(false);
let flowchartPassed = $state(false);
// Velxio (Arduino simulator) state
let isVelxio = $derived(data?.active_tabs?.includes('velxio') ?? false);
@ -65,9 +67,11 @@
let velxioError = $state(false);
let velxioIframe = $state<HTMLIFrameElement | null>(null);
let velxioOut = $state(freshOutput());
let flowchartOut = $state(freshOutput());
let hasArduinoCode = $derived(!!data?.initial_code_arduino);
let isFlowchart = $derived(data?.active_tabs?.includes('flowchart') ?? false);
let flowchartStorageKey = $derived(`elemes_flowchart_draft_${slug}`);
let flowchartTab = $state<any>(null);
// Velxio storage keys
let arduinoCodeKey = $derived(`elemes_arduino_code_${slug}`);
@ -115,7 +119,7 @@
);
// Derived: any loading state (for disabling Run button)
let compiling = $derived(cOut.loading || pyOut.loading || circuitOut.loading);
let compiling = $derived(cOut.loading || pyOut.loading || circuitOut.loading || flowchartOut.loading);
// Build output sections for OutputPanel
let outputSections = $derived.by(() => {
@ -133,6 +137,9 @@
if (tabs.includes('velxio')) {
secs.push({ key: 'velxio', label: 'Arduino', icon: '\u{1F4DF}', data: velxioOut, placeholder: 'Klik "Compile & Run" untuk menjalankan kode', loadingText: 'Mengevaluasi...' });
}
if (tabs.includes('flowchart')) {
secs.push({ key: 'flowchart', label: 'Flowchart', icon: '\u{1F531}', data: flowchartOut, placeholder: 'Klik "Cek Flowchart" untuk mengevaluasi alur logika', loadingText: 'Mengevaluasi alur...' });
}
return secs;
});
@ -317,16 +324,59 @@
const needsC = data?.active_tabs?.includes('c');
const needsPython = data?.active_tabs?.includes('python');
const needsCircuit = data?.active_tabs?.includes('circuit');
const needsFlowchart = data?.active_tabs?.includes('flowchart');
if (!data?.active_tabs?.length) return true;
if (needsC && !cPassed) return false;
if (needsPython && !pythonPassed) return false;
if (needsCircuit && !circuitPassed) return false;
if (needsFlowchart && !flowchartPassed) return false;
return true;
}
async function evaluateFlowchart() {
if (!pageData?.lesson || !flowchartTab) return;
Object.assign(flowchartOut, { loading: true, output: 'Mengevaluasi alur...', error: '', success: null });
activeTab = 'output';
try {
const flowchartText = await flowchartTab.getFlowchartText();
if (!flowchartText) {
Object.assign(flowchartOut, { error: 'Gagal mengambil data flowchart.', success: false });
return;
}
const expectedFlowchart = pageData.lesson.expected_flowchart || '';
if (!expectedFlowchart) {
Object.assign(flowchartOut, { error: 'Kunci jawaban tidak tersedia untuk pelajaran ini.', success: false });
return;
}
// Perform evaluation in frontend
const result = evaluateFlowchartSubmission(flowchartText, expectedFlowchart);
flowchartOut.output = result.output;
flowchartOut.success = result.pass;
if (flowchartOut.success) {
flowchartPassed = true;
if (checkAllPassed()) {
await completeLesson();
setTimeout(() => { showCelebration = false; activeTab = 'flowchart'; }, 3000);
}
}
} catch (err: any) {
console.error('[ERROR] Flowchart evaluation failed:', err);
Object.assign(flowchartOut, { error: `Terjadi kesalahan saat evaluasi: ${err.message}`, success: false });
} finally {
flowchartOut.loading = false;
}
}
async function evaluateCircuit() {
if (!data || !circuitEditor) return;
const simApi = circuitEditor.getApi();
@ -416,6 +466,7 @@
async function handleRun() {
if (activeTab === 'circuit') { await evaluateCircuit(); return; }
if (activeTab === 'flowchart') { await evaluateFlowchart(); return; }
if (!data) return;
activeTab = 'output';
@ -434,6 +485,7 @@
if (hasPython) await evaluateLanguage('python');
if (tabs.includes('circuit')) await evaluateCircuit();
if (tabs.includes('velxio')) await handleVelxioSubmit();
if (tabs.includes('flowchart')) await evaluateFlowchart();
}
function handleReset() {
@ -453,8 +505,10 @@
}
Object.assign(velxioOut, freshOutput());
} else if (activeTab === 'flowchart') {
localStorage.removeItem(flowchartStorageKey);
// Draft cleared. A proper reset might require iframe reload or postMessage
if (storageKey) localStorage.removeItem(flowchartStorageKey);
if (flowchartTab && typeof flowchartTab.handleLoad === 'function') {
flowchartTab.handleLoad(true);
}
} else {
const resetCode = currentLanguage === 'python'
? (data.initial_python || '')
@ -682,8 +736,12 @@
{#if isFlowchart}
<div class="tab-panel flowchart-panel" class:tab-hidden={activeTab !== 'flowchart'}>
<FlowchartTab
bind:this={flowchartTab}
storageKey={flowchartStorageKey}
initialData={data.initial_flowchart}
onRun={handleRun}
onReset={handleReset}
compiling={compiling}
/>
</div>
{/if}

View File

@ -2,9 +2,13 @@
let {
storageKey,
initialData,
onRun,
compiling
}: {
storageKey?: string;
initialData?: any;
onRun?: () => void;
compiling?: boolean;
} = $props();
let iframe = $state<HTMLIFrameElement>(null!);
@ -19,7 +23,15 @@
localStorage.setItem(storageKey, event.data.payload);
}
setTimeout(() => saving = false, 1000);
} else if (event.data?.type === 'FLOWCHART_RESET') {
if (storageKey) {
localStorage.removeItem(storageKey);
}
handleLoad(true);
} else if (event.data?.type === 'FLOWCHART_READY') {
// If READY is received, it means the iframe just loaded or was reset.
// We check if the last message from iframe was a request to reset.
// For simplicity, we can just send the data.
handleLoad();
}
}
@ -27,10 +39,10 @@
return () => window.removeEventListener('message', handleMessage);
});
function handleLoad() {
export function handleLoad(ignoreDraft = false) {
if (iframe && iframe.contentWindow) {
let draftData = null;
if (storageKey) {
if (storageKey && !ignoreDraft) {
const draft = localStorage.getItem(storageKey);
if (draft) {
try {
@ -39,22 +51,59 @@
}
}
if (initialData || draftData) {
iframe.contentWindow.postMessage({
type: 'FLOWCHART_LOAD',
payload: JSON.stringify({
initialData: initialData,
draftData: draftData
})
}, '*');
}
iframe.contentWindow.postMessage({
type: 'FLOWCHART_LOAD',
payload: JSON.stringify({
initialData: initialData,
draftData: draftData
})
}, '*');
}
}
// Gunakan cache-busting sederhana agar tidak membuka Nginx default yang tersimpan di cache
let cb = $state(Date.now());
export function getFlowchartText(): Promise<string> {
return new Promise((resolve) => {
const requestId = Math.random().toString(36).substring(7);
const handler = (event: MessageEvent) => {
if (event.data?.type === 'FLOWCHART_TEXT_RESPONSE' && event.data?.requestId === requestId) {
window.removeEventListener('message', handler);
resolve(event.data.payload);
}
};
window.addEventListener('message', handler);
if (iframe && iframe.contentWindow) {
iframe.contentWindow.postMessage({
type: 'FLOWCHART_GET_TEXT',
requestId: requestId
}, '*');
} else {
window.removeEventListener('message', handler);
resolve('');
}
// Timeout fallback
setTimeout(() => {
window.removeEventListener('message', handler);
resolve('');
}, 2000);
});
}
</script>
<div class="flowchart-container">
<button
type="button"
class="floating-action-btn"
onclick={onRun}
disabled={compiling}
title="Evaluasi alur logika Anda"
>
<span class="btn-icon">{compiling ? '⌛' : '▶'}</span>
<span class="btn-text">{compiling ? 'Mengevaluasi...' : 'Cek Flowchart'}</span>
</button>
{#if storageKey}
<div class="storage-indicator-inline" title={saving ? "Menyimpan draf..." : "Draf tersimpan di browser"}>
<span class="indicator-icon" class:saving>
@ -92,6 +141,47 @@
border: none;
}
.floating-action-btn {
position: absolute;
top: 64px;
left: 12px;
z-index: 10;
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: #10b981;
color: white;
border: none;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.floating-action-btn:hover:not(:disabled) {
background: #059669;
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(16, 185, 129, 0.4);
}
.floating-action-btn:active:not(:disabled) {
transform: translateY(0);
}
.floating-action-btn:disabled {
background: #9ca3af;
cursor: not-allowed;
box-shadow: none;
}
.btn-icon {
font-size: 1rem;
line-height: 1;
}
.storage-indicator-inline {
position: absolute;
bottom: 1rem;

View File

@ -73,6 +73,7 @@ def api_lesson(filename):
velxio_circuit = parsed_data.get('velxio_circuit', '')
expected_serial_output = parsed_data.get('expected_serial_output', '')
expected_wiring = parsed_data.get('expected_wiring', '')
expected_flowchart = parsed_data.get('expected_flowchart', '')
evaluation_config_raw = parsed_data.get('evaluation_config', '')
evaluation_config = {}
@ -134,6 +135,7 @@ def api_lesson(filename):
'velxio_circuit': velxio_circuit,
'expected_serial_output': expected_serial_output,
'expected_wiring': expected_wiring,
'expected_flowchart': expected_flowchart,
'evaluation_config': evaluation_config,
'solution_code': solution_code,
'solution_circuit': solution_circuit,

View File

@ -363,6 +363,9 @@ def render_markdown_content(file_path):
expected_wiring, lesson_content = _extract_section(
lesson_content, '---EXPECTED_WIRING---', '---END_EXPECTED_WIRING---')
expected_flowchart, lesson_content = _extract_section(
lesson_content, '---EXPECTED_FLOWCHART---', '---END_EXPECTED_FLOWCHART---')
evaluation_config, lesson_content = _extract_section(
lesson_content, '---EVALUATION_CONFIG---', '---END_EVALUATION_CONFIG---')
@ -393,6 +396,7 @@ def render_markdown_content(file_path):
'expected_output': expected_output,
'expected_output_python': expected_output_python,
'expected_circuit_output': expected_circuit_output,
'expected_flowchart': expected_flowchart,
'lesson_info': lesson_info_html,
'initial_code': initial_code,
'solution_code': solution_code,
@ -403,7 +407,7 @@ def render_markdown_content(file_path):
'initial_code_c': initial_code_c,
'initial_python': initial_python,
'initial_circuit': initial_circuit,
'initial_flowchart': initial_flowchart,
'initial_flowchart': initial_flowchart_str or initial_flowchart,
'initial_quiz': initial_quiz,
'initial_code_arduino': initial_code_arduino,
'velxio_circuit': velxio_circuit,