feat: Enhance lesson functionality with circuit output and key text support, update token generation script for better CSV handling
parent
d29c2f2e3e
commit
1e6c6a884c
16
elemes.sh
16
elemes.sh
|
|
@ -1,13 +1,25 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
case "$1" in
|
case "$1" in
|
||||||
|
stop | run | runbuild)
|
||||||
|
echo "Stop Container..."
|
||||||
|
podman-compose --env-file ../.env down
|
||||||
|
;;&
|
||||||
runbuild)
|
runbuild)
|
||||||
|
echo "Build and Run Container..."
|
||||||
podman-compose --env-file ../.env up --build --force-recreate -d
|
podman-compose --env-file ../.env up --build --force-recreate -d
|
||||||
;;&
|
;;&
|
||||||
run)
|
run)
|
||||||
|
echo "Run Container..."
|
||||||
podman-compose --env-file ../.env up -d
|
podman-compose --env-file ../.env up -d
|
||||||
;;&
|
;;&
|
||||||
stop)
|
generatetoken)
|
||||||
podman-compose --env-file ../.env down
|
echo "Generating tokens_siswa.csv from content..."
|
||||||
|
python3 "$SCRIPT_DIR/generate_tokens.py"
|
||||||
|
;;&
|
||||||
|
*)
|
||||||
|
echo "elemes.sh ( run | runbuild | stop | generatetoken )"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ export interface LessonContent {
|
||||||
lesson_content: string;
|
lesson_content: string;
|
||||||
exercise_content: string;
|
exercise_content: string;
|
||||||
expected_output: string;
|
expected_output: string;
|
||||||
|
expected_circuit_output: string;
|
||||||
|
key_text_circuit: string;
|
||||||
lesson_info: string;
|
lesson_info: string;
|
||||||
initial_code: string;
|
initial_code: string;
|
||||||
initial_code_c: string;
|
initial_code_c: string;
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,16 @@
|
||||||
let circuitLoading = $state(false);
|
let circuitLoading = $state(false);
|
||||||
let circuitSuccess = $state<boolean | null>(null);
|
let circuitSuccess = $state<boolean | null>(null);
|
||||||
|
|
||||||
|
// AND-logic: track whether each exercise type has passed (persists across runs)
|
||||||
|
let codePassed = $state(false);
|
||||||
|
let circuitPassed = $state(false);
|
||||||
|
|
||||||
|
// Derived: is this a hybrid lesson (has both code and circuit)?
|
||||||
|
let isHybrid = $derived(
|
||||||
|
(data?.active_tabs?.includes('c') || data?.active_tabs?.includes('python')) &&
|
||||||
|
data?.active_tabs?.includes('circuit')
|
||||||
|
);
|
||||||
|
|
||||||
// Derived: any loading state (for disabling Run button)
|
// Derived: any loading state (for disabling Run button)
|
||||||
let compiling = $derived(codeLoading || circuitLoading);
|
let compiling = $derived(codeLoading || circuitLoading);
|
||||||
|
|
||||||
|
|
@ -90,6 +100,8 @@
|
||||||
circuitOutput = '';
|
circuitOutput = '';
|
||||||
circuitError = '';
|
circuitError = '';
|
||||||
circuitSuccess = null;
|
circuitSuccess = null;
|
||||||
|
codePassed = false;
|
||||||
|
circuitPassed = false;
|
||||||
showSolution = false;
|
showSolution = false;
|
||||||
if (lesson.lesson_info) activeTab = 'info';
|
if (lesson.lesson_info) activeTab = 'info';
|
||||||
else if (lesson.exercise_content) activeTab = 'exercise';
|
else if (lesson.exercise_content) activeTab = 'exercise';
|
||||||
|
|
@ -135,7 +147,23 @@
|
||||||
return keys.every(key => code.includes(key));
|
return keys.every(key => code.includes(key));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Mark lesson as complete: track progress + celebration. Called when ALL exercises pass. */
|
||||||
|
async function completeLesson() {
|
||||||
|
showCelebration = true;
|
||||||
|
if (auth.isLoggedIn) {
|
||||||
|
const lessonName = slug.replace('.md', '');
|
||||||
|
await trackProgress(auth.token, lessonName);
|
||||||
|
lessonCompleted = true;
|
||||||
|
lessonContext.update(ctx => ctx ? { ...ctx, completed: true } : ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** For hybrid lessons, check if all exercise types have passed (AND logic). */
|
||||||
|
function checkAllPassed(): boolean {
|
||||||
|
if (!isHybrid) return true; // not hybrid, each evaluator handles itself
|
||||||
|
return codePassed && circuitPassed;
|
||||||
|
}
|
||||||
|
|
||||||
async function evaluateCircuit() {
|
async function evaluateCircuit() {
|
||||||
if (!data || !circuitEditor) return;
|
if (!data || !circuitEditor) return;
|
||||||
const simApi = circuitEditor.getApi();
|
const simApi = circuitEditor.getApi();
|
||||||
|
|
@ -153,10 +181,15 @@
|
||||||
activeTab = 'output';
|
activeTab = 'output';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// For hybrid lessons, use expected_circuit_output; otherwise fallback to expected_output
|
||||||
|
const circuitExpected = (isHybrid && data.expected_circuit_output)
|
||||||
|
? data.expected_circuit_output
|
||||||
|
: data.expected_output;
|
||||||
|
|
||||||
let expectedState: any = null;
|
let expectedState: any = null;
|
||||||
try {
|
try {
|
||||||
if (data.expected_output) {
|
if (circuitExpected) {
|
||||||
expectedState = JSON.parse(data.expected_output);
|
expectedState = JSON.parse(circuitExpected);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
circuitError = "Format EXPECTED_OUTPUT tidak valid (Harus JSON).";
|
circuitError = "Format EXPECTED_OUTPUT tidak valid (Harus JSON).";
|
||||||
|
|
@ -199,7 +232,11 @@
|
||||||
// GWT getInfo() returns Java array yang sulit di-parse dari JS.
|
// GWT getInfo() returns Java array yang sulit di-parse dari JS.
|
||||||
|
|
||||||
const circuitText = circuitEditor.getCircuitText();
|
const circuitText = circuitEditor.getCircuitText();
|
||||||
const keyTextMatch = checkKeyText(circuitText, data.key_text ?? '');
|
// For hybrid lessons, use key_text_circuit; otherwise fallback to key_text
|
||||||
|
const circuitKeyText = (isHybrid && data.key_text_circuit)
|
||||||
|
? data.key_text_circuit
|
||||||
|
: data.key_text;
|
||||||
|
const keyTextMatch = checkKeyText(circuitText, circuitKeyText ?? '');
|
||||||
if (!keyTextMatch) {
|
if (!keyTextMatch) {
|
||||||
allPassed = false;
|
allPassed = false;
|
||||||
messages.push(`❌ Komponen wajib belum lengkap (lihat instruksi).`);
|
messages.push(`❌ Komponen wajib belum lengkap (lihat instruksi).`);
|
||||||
|
|
@ -209,17 +246,20 @@
|
||||||
circuitSuccess = allPassed;
|
circuitSuccess = allPassed;
|
||||||
|
|
||||||
if (allPassed) {
|
if (allPassed) {
|
||||||
showCelebration = true;
|
circuitPassed = true;
|
||||||
if (auth.isLoggedIn) {
|
if (isHybrid) {
|
||||||
const lessonName = slug.replace('.md', '');
|
circuitOutput += '\n✅ Rangkaian benar!';
|
||||||
await trackProgress(auth.token, lessonName);
|
if (!codePassed) {
|
||||||
lessonCompleted = true;
|
circuitOutput += '\n⏳ Selesaikan juga tantangan kode untuk menyelesaikan pelajaran ini.';
|
||||||
lessonContext.update(ctx => ctx ? { ...ctx, completed: true } : ctx);
|
}
|
||||||
|
}
|
||||||
|
if (checkAllPassed()) {
|
||||||
|
await completeLesson();
|
||||||
|
setTimeout(() => {
|
||||||
|
showCelebration = false;
|
||||||
|
activeTab = 'circuit';
|
||||||
|
}, 3000);
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
|
||||||
showCelebration = false;
|
|
||||||
activeTab = 'circuit';
|
|
||||||
}, 3000);
|
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
circuitError = `Evaluasi gagal: ${err.message}`;
|
circuitError = `Evaluasi gagal: ${err.message}`;
|
||||||
|
|
@ -253,22 +293,23 @@
|
||||||
const outputMatch = res.output.trim() === data.expected_output.trim();
|
const outputMatch = res.output.trim() === data.expected_output.trim();
|
||||||
const keyTextMatch = checkKeyText(code, data.key_text ?? '');
|
const keyTextMatch = checkKeyText(code, data.key_text ?? '');
|
||||||
if (outputMatch && keyTextMatch) {
|
if (outputMatch && keyTextMatch) {
|
||||||
showCelebration = true;
|
codePassed = true;
|
||||||
if (auth.isLoggedIn) {
|
if (isHybrid && !circuitPassed) {
|
||||||
const lessonName = slug.replace('.md', '');
|
codeOutput += '\n✅ Kode benar!';
|
||||||
await trackProgress(auth.token, lessonName);
|
codeOutput += '\n⏳ Selesaikan juga tantangan rangkaian untuk menyelesaikan pelajaran ini.';
|
||||||
lessonCompleted = true;
|
|
||||||
lessonContext.update(ctx => ctx ? { ...ctx, completed: true } : ctx);
|
|
||||||
}
|
}
|
||||||
// Auto-show solution after celebration
|
if (checkAllPassed()) {
|
||||||
if (data.solution_code) {
|
await completeLesson();
|
||||||
showSolution = true;
|
// Auto-show solution after celebration
|
||||||
editor?.setCode(data.solution_code);
|
if (data.solution_code) {
|
||||||
|
showSolution = true;
|
||||||
|
editor?.setCode(data.solution_code);
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
showCelebration = false;
|
||||||
|
activeTab = 'editor';
|
||||||
|
}, 3000);
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
|
||||||
showCelebration = false;
|
|
||||||
activeTab = 'editor';
|
|
||||||
}, 3000);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,25 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Script to generate the tokens CSV file based on lessons in the content directory
|
Script to generate/update the tokens CSV file based on lessons in the content directory.
|
||||||
|
|
||||||
|
If the CSV already exists, new lesson columns are added (with 'not_started')
|
||||||
|
and existing student data is preserved. If it doesn't exist, a fresh file
|
||||||
|
is created with a dummy example row.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
import csv
|
import csv
|
||||||
import glob
|
import glob
|
||||||
import uuid
|
import os
|
||||||
|
import re
|
||||||
|
import stat
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
CONTENT_DIR = '../content'
|
CONTENT_DIR = '../content'
|
||||||
TOKENS_FILE = '../tokens_siswa.csv'
|
TOKENS_FILE = '../tokens_siswa.csv'
|
||||||
|
|
||||||
|
|
||||||
def get_lesson_names():
|
def get_lesson_names():
|
||||||
"""Get all lesson names from the content directory (excluding home.md)"""
|
"""Get all lesson names from the content directory (excluding home.md)."""
|
||||||
# First, try to get the lesson order from home.md
|
|
||||||
home_file_path = os.path.join(CONTENT_DIR, "home.md")
|
home_file_path = os.path.join(CONTENT_DIR, "home.md")
|
||||||
lesson_names = []
|
lesson_names = []
|
||||||
|
|
||||||
|
|
@ -22,68 +27,89 @@ def get_lesson_names():
|
||||||
with open(home_file_path, 'r', encoding='utf-8') as f:
|
with open(home_file_path, 'r', encoding='utf-8') as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
|
|
||||||
# Split content to get only the lesson list part
|
|
||||||
parts = content.split('---Available_Lessons---')
|
parts = content.split('---Available_Lessons---')
|
||||||
if len(parts) > 1:
|
if len(parts) > 1:
|
||||||
lesson_list_content = parts[1]
|
lesson_list_content = parts[1]
|
||||||
|
lesson_links = re.findall(
|
||||||
# Find lesson links in the lesson list content
|
r'\[([^\]]+)\]\(lesson/([^\)]+)\)', lesson_list_content
|
||||||
import re
|
)
|
||||||
# Look for markdown links that point to lessons
|
|
||||||
lesson_links = re.findall(r'\[([^\]]+)\]\(lesson/([^\)]+)\)', lesson_list_content)
|
|
||||||
|
|
||||||
if lesson_links:
|
if lesson_links:
|
||||||
# Create ordered list based on home.md
|
for _link_text, filename in lesson_links:
|
||||||
for link_text, filename in lesson_links:
|
|
||||||
lesson_names.append(filename.replace('.md', ''))
|
lesson_names.append(filename.replace('.md', ''))
|
||||||
|
|
||||||
return lesson_names
|
return lesson_names
|
||||||
|
|
||||||
# If no specific order is defined in home.md, fall back to alphabetical order
|
# Fallback: alphabetical order
|
||||||
lesson_files = glob.glob(os.path.join(CONTENT_DIR, "*.md"))
|
lesson_files = glob.glob(os.path.join(CONTENT_DIR, "*.md"))
|
||||||
|
|
||||||
for file_path in lesson_files:
|
for file_path in lesson_files:
|
||||||
filename = os.path.basename(file_path)
|
filename = os.path.basename(file_path)
|
||||||
# Skip home.md as it's not a lesson
|
|
||||||
if filename == "home.md":
|
if filename == "home.md":
|
||||||
continue
|
continue
|
||||||
lesson_names.append(filename.replace('.md', ''))
|
lesson_names.append(filename.replace('.md', ''))
|
||||||
|
|
||||||
# Sort alphabetically to have consistent order
|
|
||||||
lesson_names.sort()
|
lesson_names.sort()
|
||||||
|
|
||||||
return lesson_names
|
return lesson_names
|
||||||
|
|
||||||
def generate_tokens_csv():
|
|
||||||
"""Generate the tokens CSV file with headers and lesson columns"""
|
|
||||||
lesson_names = get_lesson_names()
|
|
||||||
|
|
||||||
# Create the file with headers
|
def _set_permissions(path):
|
||||||
headers = ['token', 'nama_siswa'] + lesson_names
|
"""Set rw-rw-rw- so the container can update the file."""
|
||||||
|
|
||||||
with open(TOKENS_FILE, 'w', newline='', encoding='utf-8') as csvfile:
|
|
||||||
writer = csv.writer(csvfile, delimiter=';')
|
|
||||||
writer.writerow(headers)
|
|
||||||
|
|
||||||
# Add a dummy example row for reference
|
|
||||||
dummy_row = ['dummy_token_12345', 'Example Student'] + ['not_started'] * len(lesson_names)
|
|
||||||
writer.writerow(dummy_row)
|
|
||||||
|
|
||||||
# Set file permissions to allow read/write access for all users
|
|
||||||
# This ensures the container can update the file when progress is tracked
|
|
||||||
try:
|
try:
|
||||||
import stat
|
cur = os.stat(path).st_mode
|
||||||
import os
|
os.chmod(path, cur | stat.S_IRUSR | stat.S_IWUSR
|
||||||
current_permissions = os.stat(TOKENS_FILE).st_mode
|
| stat.S_IRGRP | stat.S_IWGRP
|
||||||
os.chmod(TOKENS_FILE, current_permissions | stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IWGRP | stat.S_IROTH | stat.S_IWOTH)
|
| stat.S_IROTH | stat.S_IWOTH)
|
||||||
print(f"Set permissions for {TOKENS_FILE} to allow container access")
|
print(f"Set permissions for {path} to allow container access")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Warning: Could not set file permissions: {e}")
|
print(f"Warning: Could not set file permissions: {e}")
|
||||||
|
|
||||||
print(f"Created tokens file: {TOKENS_FILE} with headers: {headers}")
|
|
||||||
print("Teachers can now add student tokens and names directly to this file.")
|
def generate_tokens_csv():
|
||||||
print("An example row has been added with token 'dummy_token_12345' for reference.")
|
"""Generate or update the tokens CSV file."""
|
||||||
print("To add new students, add new rows with format: token;nama_siswa;lesson1_status;lesson2_status;...")
|
lesson_names = get_lesson_names()
|
||||||
|
new_headers = ['token', 'nama_siswa'] + lesson_names
|
||||||
|
|
||||||
|
if os.path.exists(TOKENS_FILE):
|
||||||
|
# ── Merge mode: preserve existing data, add new columns ──
|
||||||
|
with open(TOKENS_FILE, 'r', newline='', encoding='utf-8') as f:
|
||||||
|
reader = csv.DictReader(f, delimiter=';')
|
||||||
|
old_headers = list(reader.fieldnames) if reader.fieldnames else []
|
||||||
|
rows = list(reader)
|
||||||
|
|
||||||
|
added_cols = [h for h in new_headers if h not in old_headers]
|
||||||
|
removed_cols = [h for h in old_headers if h not in new_headers
|
||||||
|
and h not in ('token', 'nama_siswa')]
|
||||||
|
|
||||||
|
# Rebuild each row with the new header order
|
||||||
|
merged_rows = []
|
||||||
|
for row in rows:
|
||||||
|
new_row = {}
|
||||||
|
for h in new_headers:
|
||||||
|
new_row[h] = row.get(h, 'not_started')
|
||||||
|
merged_rows.append(new_row)
|
||||||
|
|
||||||
|
with open(TOKENS_FILE, 'w', newline='', encoding='utf-8') as f:
|
||||||
|
writer = csv.DictWriter(f, fieldnames=new_headers, delimiter=';')
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerows(merged_rows)
|
||||||
|
|
||||||
|
if added_cols:
|
||||||
|
print(f"Kolom baru ditambahkan: {added_cols}")
|
||||||
|
if removed_cols:
|
||||||
|
print(f"Kolom lama dihapus: {removed_cols}")
|
||||||
|
print(f"Updated {TOKENS_FILE} — {len(merged_rows)} siswa dipertahankan.")
|
||||||
|
|
||||||
|
else:
|
||||||
|
# ── Fresh mode: create new file ──
|
||||||
|
with open(TOKENS_FILE, 'w', newline='', encoding='utf-8') as f:
|
||||||
|
writer = csv.writer(f, delimiter=';')
|
||||||
|
writer.writerow(new_headers)
|
||||||
|
dummy_row = ['dummy_token_12345', 'Example Student'] + \
|
||||||
|
['not_started'] * len(lesson_names)
|
||||||
|
writer.writerow(dummy_row)
|
||||||
|
print(f"Created tokens file: {TOKENS_FILE}")
|
||||||
|
print("Tambahkan siswa dengan format: token;nama_siswa;status;...")
|
||||||
|
|
||||||
|
_set_permissions(TOKENS_FILE)
|
||||||
|
print(f"Headers: {new_headers}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
generate_tokens_csv()
|
generate_tokens_csv()
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,8 @@ def api_lesson(filename):
|
||||||
lesson_html = parsed_data['lesson_html']
|
lesson_html = parsed_data['lesson_html']
|
||||||
exercise_html = parsed_data['exercise_html']
|
exercise_html = parsed_data['exercise_html']
|
||||||
expected_output = parsed_data['expected_output']
|
expected_output = parsed_data['expected_output']
|
||||||
|
expected_circuit_output = parsed_data.get('expected_circuit_output', '')
|
||||||
|
key_text_circuit = parsed_data.get('key_text_circuit', '')
|
||||||
lesson_info = parsed_data['lesson_info']
|
lesson_info = parsed_data['lesson_info']
|
||||||
initial_code = parsed_data['initial_code']
|
initial_code = parsed_data['initial_code']
|
||||||
solution_code = parsed_data['solution_code']
|
solution_code = parsed_data['solution_code']
|
||||||
|
|
@ -95,6 +97,7 @@ def api_lesson(filename):
|
||||||
'lesson_content': lesson_html,
|
'lesson_content': lesson_html,
|
||||||
'exercise_content': exercise_html,
|
'exercise_content': exercise_html,
|
||||||
'expected_output': expected_output,
|
'expected_output': expected_output,
|
||||||
|
'expected_circuit_output': expected_circuit_output,
|
||||||
'lesson_info': lesson_info,
|
'lesson_info': lesson_info,
|
||||||
'initial_code': initial_code,
|
'initial_code': initial_code,
|
||||||
'initial_circuit': initial_circuit,
|
'initial_circuit': initial_circuit,
|
||||||
|
|
@ -104,6 +107,7 @@ def api_lesson(filename):
|
||||||
'solution_code': solution_code,
|
'solution_code': solution_code,
|
||||||
'solution_circuit': solution_circuit,
|
'solution_circuit': solution_circuit,
|
||||||
'key_text': key_text,
|
'key_text': key_text,
|
||||||
|
'key_text_circuit': key_text_circuit,
|
||||||
'active_tabs': active_tabs,
|
'active_tabs': active_tabs,
|
||||||
'lesson_title': full_filename.replace('.md', '').replace('_', ' ').title(),
|
'lesson_title': full_filename.replace('.md', '').replace('_', ' ').title(),
|
||||||
'lesson_completed': lesson_completed,
|
'lesson_completed': lesson_completed,
|
||||||
|
|
|
||||||
|
|
@ -278,9 +278,15 @@ def render_markdown_content(file_path):
|
||||||
expected_output, lesson_content = _extract_section(
|
expected_output, lesson_content = _extract_section(
|
||||||
lesson_content, '---EXPECTED_OUTPUT---', '---END_EXPECTED_OUTPUT---')
|
lesson_content, '---EXPECTED_OUTPUT---', '---END_EXPECTED_OUTPUT---')
|
||||||
|
|
||||||
|
expected_circuit_output, lesson_content = _extract_section(
|
||||||
|
lesson_content, '---EXPECTED_CIRCUIT_OUTPUT---', '---END_EXPECTED_CIRCUIT_OUTPUT---')
|
||||||
|
|
||||||
key_text, lesson_content = _extract_section(
|
key_text, lesson_content = _extract_section(
|
||||||
lesson_content, '---KEY_TEXT---', '---END_KEY_TEXT---')
|
lesson_content, '---KEY_TEXT---', '---END_KEY_TEXT---')
|
||||||
|
|
||||||
|
key_text_circuit, lesson_content = _extract_section(
|
||||||
|
lesson_content, '---KEY_TEXT_CIRCUIT---', '---END_KEY_TEXT_CIRCUIT---')
|
||||||
|
|
||||||
# Lesson info has a special fallback for old format
|
# Lesson info has a special fallback for old format
|
||||||
lesson_info = ""
|
lesson_info = ""
|
||||||
if '---LESSON_INFO---' in lesson_content and '---END_LESSON_INFO---' in lesson_content:
|
if '---LESSON_INFO---' in lesson_content and '---END_LESSON_INFO---' in lesson_content:
|
||||||
|
|
@ -336,11 +342,13 @@ def render_markdown_content(file_path):
|
||||||
'lesson_html': lesson_html,
|
'lesson_html': lesson_html,
|
||||||
'exercise_html': exercise_html,
|
'exercise_html': exercise_html,
|
||||||
'expected_output': expected_output,
|
'expected_output': expected_output,
|
||||||
|
'expected_circuit_output': expected_circuit_output,
|
||||||
'lesson_info': lesson_info_html,
|
'lesson_info': lesson_info_html,
|
||||||
'initial_code': initial_code,
|
'initial_code': initial_code,
|
||||||
'solution_code': solution_code,
|
'solution_code': solution_code,
|
||||||
'solution_circuit': solution_circuit,
|
'solution_circuit': solution_circuit,
|
||||||
'key_text': key_text,
|
'key_text': key_text,
|
||||||
|
'key_text_circuit': key_text_circuit,
|
||||||
'initial_code_c': initial_code_c,
|
'initial_code_c': initial_code_c,
|
||||||
'initial_python': initial_python,
|
'initial_python': initial_python,
|
||||||
'initial_circuit': initial_circuit,
|
'initial_circuit': initial_circuit,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue