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
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
case "$1" in
|
||||
stop | run | runbuild)
|
||||
echo "Stop Container..."
|
||||
podman-compose --env-file ../.env down
|
||||
;;&
|
||||
runbuild)
|
||||
echo "Build and Run Container..."
|
||||
podman-compose --env-file ../.env up --build --force-recreate -d
|
||||
;;&
|
||||
run)
|
||||
echo "Run Container..."
|
||||
podman-compose --env-file ../.env up -d
|
||||
;;&
|
||||
stop)
|
||||
podman-compose --env-file ../.env down
|
||||
generatetoken)
|
||||
echo "Generating tokens_siswa.csv from content..."
|
||||
python3 "$SCRIPT_DIR/generate_tokens.py"
|
||||
;;&
|
||||
*)
|
||||
echo "elemes.sh ( run | runbuild | stop | generatetoken )"
|
||||
;;
|
||||
esac
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ export interface LessonContent {
|
|||
lesson_content: string;
|
||||
exercise_content: string;
|
||||
expected_output: string;
|
||||
expected_circuit_output: string;
|
||||
key_text_circuit: string;
|
||||
lesson_info: string;
|
||||
initial_code: string;
|
||||
initial_code_c: string;
|
||||
|
|
|
|||
|
|
@ -38,6 +38,16 @@
|
|||
let circuitLoading = $state(false);
|
||||
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)
|
||||
let compiling = $derived(codeLoading || circuitLoading);
|
||||
|
||||
|
|
@ -90,6 +100,8 @@
|
|||
circuitOutput = '';
|
||||
circuitError = '';
|
||||
circuitSuccess = null;
|
||||
codePassed = false;
|
||||
circuitPassed = false;
|
||||
showSolution = false;
|
||||
if (lesson.lesson_info) activeTab = 'info';
|
||||
else if (lesson.exercise_content) activeTab = 'exercise';
|
||||
|
|
@ -135,6 +147,22 @@
|
|||
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() {
|
||||
if (!data || !circuitEditor) return;
|
||||
|
|
@ -153,10 +181,15 @@
|
|||
activeTab = 'output';
|
||||
|
||||
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;
|
||||
try {
|
||||
if (data.expected_output) {
|
||||
expectedState = JSON.parse(data.expected_output);
|
||||
if (circuitExpected) {
|
||||
expectedState = JSON.parse(circuitExpected);
|
||||
}
|
||||
} catch (e) {
|
||||
circuitError = "Format EXPECTED_OUTPUT tidak valid (Harus JSON).";
|
||||
|
|
@ -199,7 +232,11 @@
|
|||
// GWT getInfo() returns Java array yang sulit di-parse dari JS.
|
||||
|
||||
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) {
|
||||
allPassed = false;
|
||||
messages.push(`❌ Komponen wajib belum lengkap (lihat instruksi).`);
|
||||
|
|
@ -209,17 +246,20 @@
|
|||
circuitSuccess = allPassed;
|
||||
|
||||
if (allPassed) {
|
||||
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);
|
||||
circuitPassed = true;
|
||||
if (isHybrid) {
|
||||
circuitOutput += '\n✅ Rangkaian benar!';
|
||||
if (!codePassed) {
|
||||
circuitOutput += '\n⏳ Selesaikan juga tantangan kode untuk menyelesaikan pelajaran ini.';
|
||||
}
|
||||
}
|
||||
if (checkAllPassed()) {
|
||||
await completeLesson();
|
||||
setTimeout(() => {
|
||||
showCelebration = false;
|
||||
activeTab = 'circuit';
|
||||
}, 3000);
|
||||
}
|
||||
setTimeout(() => {
|
||||
showCelebration = false;
|
||||
activeTab = 'circuit';
|
||||
}, 3000);
|
||||
}
|
||||
} catch (err: any) {
|
||||
circuitError = `Evaluasi gagal: ${err.message}`;
|
||||
|
|
@ -253,22 +293,23 @@
|
|||
const outputMatch = res.output.trim() === data.expected_output.trim();
|
||||
const keyTextMatch = checkKeyText(code, data.key_text ?? '');
|
||||
if (outputMatch && keyTextMatch) {
|
||||
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);
|
||||
codePassed = true;
|
||||
if (isHybrid && !circuitPassed) {
|
||||
codeOutput += '\n✅ Kode benar!';
|
||||
codeOutput += '\n⏳ Selesaikan juga tantangan rangkaian untuk menyelesaikan pelajaran ini.';
|
||||
}
|
||||
// Auto-show solution after celebration
|
||||
if (data.solution_code) {
|
||||
showSolution = true;
|
||||
editor?.setCode(data.solution_code);
|
||||
if (checkAllPassed()) {
|
||||
await completeLesson();
|
||||
// Auto-show solution after celebration
|
||||
if (data.solution_code) {
|
||||
showSolution = true;
|
||||
editor?.setCode(data.solution_code);
|
||||
}
|
||||
setTimeout(() => {
|
||||
showCelebration = false;
|
||||
activeTab = 'editor';
|
||||
}, 3000);
|
||||
}
|
||||
setTimeout(() => {
|
||||
showCelebration = false;
|
||||
activeTab = 'editor';
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,20 +1,25 @@
|
|||
#!/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 glob
|
||||
import uuid
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
|
||||
# Configuration
|
||||
CONTENT_DIR = '../content'
|
||||
TOKENS_FILE = '../tokens_siswa.csv'
|
||||
|
||||
|
||||
def get_lesson_names():
|
||||
"""Get all lesson names from the content directory (excluding home.md)"""
|
||||
# First, try to get the lesson order from home.md
|
||||
"""Get all lesson names from the content directory (excluding home.md)."""
|
||||
home_file_path = os.path.join(CONTENT_DIR, "home.md")
|
||||
lesson_names = []
|
||||
|
||||
|
|
@ -22,68 +27,89 @@ def get_lesson_names():
|
|||
with open(home_file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Split content to get only the lesson list part
|
||||
parts = content.split('---Available_Lessons---')
|
||||
if len(parts) > 1:
|
||||
lesson_list_content = parts[1]
|
||||
|
||||
# Find lesson links in the lesson list content
|
||||
import re
|
||||
# Look for markdown links that point to lessons
|
||||
lesson_links = re.findall(r'\[([^\]]+)\]\(lesson/([^\)]+)\)', lesson_list_content)
|
||||
|
||||
lesson_links = re.findall(
|
||||
r'\[([^\]]+)\]\(lesson/([^\)]+)\)', lesson_list_content
|
||||
)
|
||||
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', ''))
|
||||
|
||||
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"))
|
||||
|
||||
for file_path in lesson_files:
|
||||
filename = os.path.basename(file_path)
|
||||
# Skip home.md as it's not a lesson
|
||||
if filename == "home.md":
|
||||
continue
|
||||
lesson_names.append(filename.replace('.md', ''))
|
||||
|
||||
# Sort alphabetically to have consistent order
|
||||
lesson_names.sort()
|
||||
|
||||
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
|
||||
headers = ['token', 'nama_siswa'] + lesson_names
|
||||
|
||||
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
|
||||
def _set_permissions(path):
|
||||
"""Set rw-rw-rw- so the container can update the file."""
|
||||
try:
|
||||
import stat
|
||||
import os
|
||||
current_permissions = os.stat(TOKENS_FILE).st_mode
|
||||
os.chmod(TOKENS_FILE, current_permissions | stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IWGRP | stat.S_IROTH | stat.S_IWOTH)
|
||||
print(f"Set permissions for {TOKENS_FILE} to allow container access")
|
||||
cur = os.stat(path).st_mode
|
||||
os.chmod(path, cur | stat.S_IRUSR | stat.S_IWUSR
|
||||
| stat.S_IRGRP | stat.S_IWGRP
|
||||
| stat.S_IROTH | stat.S_IWOTH)
|
||||
print(f"Set permissions for {path} to allow container access")
|
||||
except Exception as 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.")
|
||||
print("An example row has been added with token 'dummy_token_12345' for reference.")
|
||||
print("To add new students, add new rows with format: token;nama_siswa;lesson1_status;lesson2_status;...")
|
||||
|
||||
def generate_tokens_csv():
|
||||
"""Generate or update the tokens CSV file."""
|
||||
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__':
|
||||
generate_tokens_csv()
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ def api_lesson(filename):
|
|||
lesson_html = parsed_data['lesson_html']
|
||||
exercise_html = parsed_data['exercise_html']
|
||||
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']
|
||||
initial_code = parsed_data['initial_code']
|
||||
solution_code = parsed_data['solution_code']
|
||||
|
|
@ -95,6 +97,7 @@ def api_lesson(filename):
|
|||
'lesson_content': lesson_html,
|
||||
'exercise_content': exercise_html,
|
||||
'expected_output': expected_output,
|
||||
'expected_circuit_output': expected_circuit_output,
|
||||
'lesson_info': lesson_info,
|
||||
'initial_code': initial_code,
|
||||
'initial_circuit': initial_circuit,
|
||||
|
|
@ -104,6 +107,7 @@ def api_lesson(filename):
|
|||
'solution_code': solution_code,
|
||||
'solution_circuit': solution_circuit,
|
||||
'key_text': key_text,
|
||||
'key_text_circuit': key_text_circuit,
|
||||
'active_tabs': active_tabs,
|
||||
'lesson_title': full_filename.replace('.md', '').replace('_', ' ').title(),
|
||||
'lesson_completed': lesson_completed,
|
||||
|
|
|
|||
|
|
@ -278,9 +278,15 @@ def render_markdown_content(file_path):
|
|||
expected_output, lesson_content = _extract_section(
|
||||
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(
|
||||
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 = ""
|
||||
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,
|
||||
'exercise_html': exercise_html,
|
||||
'expected_output': expected_output,
|
||||
'expected_circuit_output': expected_circuit_output,
|
||||
'lesson_info': lesson_info_html,
|
||||
'initial_code': initial_code,
|
||||
'solution_code': solution_code,
|
||||
'solution_circuit': solution_circuit,
|
||||
'key_text': key_text,
|
||||
'key_text_circuit': key_text_circuit,
|
||||
'initial_code_c': initial_code_c,
|
||||
'initial_python': initial_python,
|
||||
'initial_circuit': initial_circuit,
|
||||
|
|
|
|||
Loading…
Reference in New Issue