fix python code

master v2.4
a2nr 2026-04-01 12:56:27 +07:00
parent 0e944e5224
commit 09827bf3ff
5 changed files with 224 additions and 247 deletions

View File

@ -1,34 +1,32 @@
<script lang="ts"> <script lang="ts">
interface OutputSection { export interface OutputSection {
output?: string; output?: string;
error?: string; error?: string;
loading?: boolean; loading?: boolean;
success?: boolean | null; success?: boolean | null;
} }
interface Props { export interface OutputEntry {
code?: OutputSection; key: string;
circuit?: OutputSection; label: string;
hasCode?: boolean; icon: string;
hasCircuit?: boolean; data: OutputSection;
placeholder: string;
loadingText: string;
} }
let { interface Props {
code = { output: '', error: '', loading: false, success: null }, sections?: OutputEntry[];
circuit = { output: '', error: '', loading: false, success: null }, }
hasCode = true,
hasCircuit = false,
}: Props = $props();
// Determine overall loading state for header badge let { sections = [] }: Props = $props();
let anyLoading = $derived(code.loading || circuit.loading);
let anyLoading = $derived(sections.some(s => s.data.loading));
// Determine overall success state: null if idle, true if all ran successfully, false if any error
let overallSuccess = $derived.by(() => { let overallSuccess = $derived.by(() => {
const codeRan = code.success !== null && code.success !== undefined; const ran = sections.filter(s => s.data.success !== null && s.data.success !== undefined);
const circuitRan = circuit.success !== null && circuit.success !== undefined; if (ran.length === 0) return null;
if (!codeRan && !circuitRan) return null; if (ran.some(s => s.data.success === false)) return false;
if ((codeRan && code.success === false) || (circuitRan && circuit.success === false)) return false;
return true; return true;
}); });
</script> </script>
@ -46,42 +44,23 @@
</div> </div>
<div class="output-sections"> <div class="output-sections">
<!-- Code output section --> {#each sections as sec (sec.key)}
{#if hasCode} <div class="output-section" class:has-error={!!sec.data.error} class:has-success={sec.data.success === true}>
<div class="output-section" class:has-error={!!code.error} class:has-success={code.success === true}>
<div class="section-label"> <div class="section-label">
<span class="section-icon">&#x1F4BB;</span> Code <span class="section-icon">{sec.icon}</span> {sec.label}
{#if code.loading} {#if sec.data.loading}
<span class="section-badge running">Compiling...</span> <span class="section-badge running">{sec.loadingText}</span>
{:else if code.success === true} {:else if sec.data.success === true}
<span class="section-badge success">OK</span> <span class="section-badge success">OK</span>
{:else if code.success === false} {:else if sec.data.success === false}
<span class="section-badge error">Error</span> <span class="section-badge error">Error</span>
{/if} {/if}
</div> </div>
<pre class="output-body">{#if code.loading}Mengompilasi dan menjalankan kode...{:else if code.error}{code.error}{:else if code.output}{code.output}{:else}<span class="placeholder">Klik "Run" untuk menjalankan kode</span>{/if}</pre> <pre class="output-body">{#if sec.data.loading}{sec.loadingText}{:else if sec.data.error}{sec.data.error}{:else if sec.data.output}{sec.data.output}{:else}<span class="placeholder">{sec.placeholder}</span>{/if}</pre>
</div> </div>
{/if} {/each}
<!-- Circuit output section --> {#if sections.length === 0}
{#if hasCircuit}
<div class="output-section" class:has-error={!!circuit.error} class:has-success={circuit.success === true}>
<div class="section-label">
<span class="section-icon">&#x26A1;</span> Circuit
{#if circuit.loading}
<span class="section-badge running">Evaluating...</span>
{:else if circuit.success === true}
<span class="section-badge success">OK</span>
{:else if circuit.success === false}
<span class="section-badge error">Error</span>
{/if}
</div>
<pre class="output-body">{#if circuit.loading}Mengevaluasi rangkaian...{:else if circuit.error}{circuit.error}{:else if circuit.output}{circuit.output}{:else}<span class="placeholder">Klik "Cek Rangkaian" untuk mengevaluasi</span>{/if}</pre>
</div>
{/if}
<!-- Fallback when neither section has run -->
{#if !hasCode && !hasCircuit}
<pre class="output-body"><span class="placeholder">Tidak ada output</span></pre> <pre class="output-body"><span class="placeholder">Tidak ada output</span></pre>
{/if} {/if}
</div> </div>

View File

@ -5,6 +5,7 @@
isMobile: boolean; isMobile: boolean;
mobileMode: 'hidden' | 'half' | 'full'; mobileMode: 'hidden' | 'half' | 'full';
activeTab: TabType; activeTab: TabType;
currentLanguage: string;
hasInfo: boolean; hasInfo: boolean;
hasExercise: boolean; hasExercise: boolean;
activeTabs: string[]; activeTabs: string[];
@ -20,6 +21,7 @@
isMobile, isMobile,
mobileMode = $bindable(), mobileMode = $bindable(),
activeTab = $bindable(), activeTab = $bindable(),
currentLanguage = $bindable(),
hasInfo, hasInfo,
hasExercise, hasExercise,
activeTabs, activeTabs,
@ -33,9 +35,12 @@
let touchStartY = 0; let touchStartY = 0;
const hasC = $derived(activeTabs?.includes('c') ?? false);
const hasPython = $derived(activeTabs?.includes('python') ?? false);
const hasCodeEditor = $derived( const hasCodeEditor = $derived(
!activeTabs || activeTabs.length === 0 || activeTabs.includes('c') || activeTabs.includes('python') !activeTabs || activeTabs.length === 0 || hasC || hasPython
); );
const hasMultiLang = $derived(hasC && hasPython);
const hasCircuit = $derived(activeTabs?.includes('circuit') ?? false); const hasCircuit = $derived(activeTabs?.includes('circuit') ?? false);
function cycleMobileSheet() { function cycleMobileSheet() {
@ -70,7 +75,12 @@
<button class="chrome-tab" class:active={activeTab === 'exercise'} onclick={() => (activeTab = 'exercise')}>Exercise</button> <button class="chrome-tab" class:active={activeTab === 'exercise'} onclick={() => (activeTab = 'exercise')}>Exercise</button>
{/if} {/if}
{#if hasCodeEditor} {#if hasCodeEditor}
<button class="chrome-tab" class:active={activeTab === 'editor'} onclick={() => (activeTab = 'editor')}>Code</button> {#if hasMultiLang}
<button class="chrome-tab" class:active={activeTab === 'editor' && currentLanguage === 'c'} onclick={() => { activeTab = 'editor'; currentLanguage = 'c'; }}>C</button>
<button class="chrome-tab" class:active={activeTab === 'editor' && currentLanguage === 'python'} onclick={() => { activeTab = 'editor'; currentLanguage = 'python'; }}>Python</button>
{:else}
<button class="chrome-tab" class:active={activeTab === 'editor'} onclick={() => (activeTab = 'editor')}>Code</button>
{/if}
{/if} {/if}
{#if hasCircuit} {#if hasCircuit}
<button class="chrome-tab" class:active={activeTab === 'circuit'} onclick={() => (activeTab = 'circuit')}>Circuit</button> <button class="chrome-tab" class:active={activeTab === 'circuit'} onclick={() => (activeTab = 'circuit')}>Circuit</button>

View File

@ -0,0 +1,44 @@
/**
* Pure helper functions for exercise evaluation.
*/
import type { CircuitJSApi } from '$types/circuitjs';
/** Check if code contains all required key_text keywords (one per line). */
export function checkKeyText(code: string, keyText: string): boolean {
if (!keyText.trim()) return true;
const keys = keyText.split('\n').map(k => k.trim()).filter(k => k.length > 0);
return keys.every(key => code.includes(key));
}
export interface NodeResult {
passed: boolean;
message: string;
}
/** Validate circuit node voltages against expected state. */
export function validateNodes(
simApi: CircuitJSApi,
nodes: Record<string, { voltage: number; tolerance?: number }>
): { allPassed: boolean; messages: string[] } {
let allPassed = true;
const messages: string[] = [];
for (const [nodeName, criteria] of Object.entries(nodes)) {
const actualV = simApi.getNodeVoltage(nodeName);
if (actualV === undefined || actualV === null) {
allPassed = false;
messages.push(`❌ Node '${nodeName}' tidak ditemukan.`);
continue;
}
const tol = criteria.tolerance || 0.1;
if (Math.abs(actualV - criteria.voltage) <= tol) {
messages.push(`✅ Node '${nodeName}': Tegangan ${actualV.toFixed(2)}V (Sesuai)`);
} else {
allPassed = false;
messages.push(`❌ Node '${nodeName}': Tegangan ${actualV.toFixed(2)}V (Harusnya ~${criteria.voltage}V)`);
}
}
return { allPassed, messages };
}

View File

@ -3,11 +3,12 @@
import { beforeNavigate } from '$app/navigation'; import { beforeNavigate } from '$app/navigation';
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 OutputPanel from '$components/OutputPanel.svelte'; import OutputPanel, { type OutputEntry } from '$components/OutputPanel.svelte';
import CelebrationOverlay from '$components/CelebrationOverlay.svelte'; import CelebrationOverlay from '$components/CelebrationOverlay.svelte';
import WorkspaceHeader from '$components/WorkspaceHeader.svelte'; import WorkspaceHeader from '$components/WorkspaceHeader.svelte';
import LessonList from '$components/LessonList.svelte'; import LessonList from '$components/LessonList.svelte';
import { compileCode, trackProgress } from '$services/api'; import { compileCode, trackProgress } from '$services/api';
import { checkKeyText, validateNodes } from '$services/exercise';
import { auth, authLoggedIn } from '$stores/auth'; import { auth, authLoggedIn } from '$stores/auth';
import { lessonContext } from '$stores/lessonContext'; import { lessonContext } from '$stores/lessonContext';
import { noSelect } from '$actions/noSelect'; import { noSelect } from '$actions/noSelect';
@ -26,17 +27,20 @@
let data = $state<LessonContent | null>(null); let data = $state<LessonContent | null>(null);
let lessonCompleted = $state(false); let lessonCompleted = $state(false);
let currentCode = $state(''); let currentCode = $state('');
let currentLanguage = $state<string>('c');
// Separate output state for code and circuit // Per-language code tracking
let codeOutput = $state(''); let cCode = $state('');
let codeError = $state(''); let pythonCode = $state('');
let codeLoading = $state(false);
let codeSuccess = $state<boolean | null>(null);
let circuitOutput = $state(''); // Output state per language + circuit
let circuitError = $state(''); const freshOutput = () => ({ output: '', error: '', loading: false, success: null as boolean | null });
let circuitLoading = $state(false); let cOut = $state(freshOutput());
let circuitSuccess = $state<boolean | null>(null); let pyOut = $state(freshOutput());
let circuitOut = $state(freshOutput());
// Helper: get the active code output object for current language
function getCodeOut() { return currentLanguage === 'python' ? pyOut : cOut; }
// AND-logic: track whether each exercise type has passed (persists across runs) // AND-logic: track whether each exercise type has passed (persists across runs)
let codePassed = $state(false); let codePassed = $state(false);
@ -49,7 +53,23 @@
); );
// Derived: any loading state (for disabling Run button) // Derived: any loading state (for disabling Run button)
let compiling = $derived(codeLoading || circuitLoading); let compiling = $derived(cOut.loading || pyOut.loading || circuitOut.loading);
// Build output sections for OutputPanel
let outputSections = $derived.by(() => {
const tabs = data?.active_tabs ?? [];
const secs: OutputEntry[] = [];
if (tabs.includes('c') || (!tabs.length && !tabs.includes('python'))) {
secs.push({ key: 'c', label: 'C', icon: '\u{1F4BB}', data: cOut, placeholder: 'Klik "Run" untuk menjalankan kode C', loadingText: 'Mengompilasi C...' });
}
if (tabs.includes('python')) {
secs.push({ key: 'python', label: 'Python', icon: '\u{1F40D}', data: pyOut, placeholder: 'Klik "Run" untuk menjalankan kode Python', loadingText: 'Menjalankan Python...' });
}
if (tabs.includes('circuit')) {
secs.push({ key: 'circuit', label: 'Circuit', icon: '\u26A1', data: circuitOut, placeholder: 'Klik "Cek Rangkaian" untuk mengevaluasi', loadingText: 'Mengevaluasi rangkaian...' });
}
return secs;
});
// UI state // UI state
let showSolution = $state(false); let showSolution = $state(false);
@ -93,19 +113,28 @@
if (lesson) { if (lesson) {
data = lesson; data = lesson;
lessonCompleted = lesson.lesson_completed; lessonCompleted = lesson.lesson_completed;
currentCode = lesson.initial_code_c || lesson.initial_python || lesson.initial_code || '';
codeOutput = ''; // Initialize per-language code
codeError = ''; cCode = lesson.initial_code_c || '';
codeSuccess = null; pythonCode = lesson.initial_python || '';
circuitOutput = '';
circuitError = ''; // Determine initial language (use local var to avoid reactive dependency on currentLanguage)
circuitSuccess = null; const hasC = lesson.active_tabs?.includes('c');
const hasPython = lesson.active_tabs?.includes('python');
const initLang = (hasPython && !hasC) ? 'python' : 'c';
currentLanguage = initLang;
prevLanguage = initLang;
currentCode = initLang === 'python' ? pythonCode : (cCode || lesson.initial_code || '');
cOut = freshOutput();
pyOut = freshOutput();
circuitOut = freshOutput();
codePassed = false; codePassed = false;
circuitPassed = 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';
else if (lesson.active_tabs?.includes('circuit') && !lesson.active_tabs?.includes('c') && !lesson.active_tabs?.includes('python')) activeTab = 'circuit'; else if (lesson.active_tabs?.includes('circuit') && !hasC && !hasPython) activeTab = 'circuit';
else activeTab = 'editor'; else activeTab = 'editor';
mobileMode = 'half'; mobileMode = 'half';
@ -119,6 +148,21 @@
} }
}); });
// Switch editor content when language tab changes
let prevLanguage = $state<string>('c');
$effect(() => {
if (!data || currentLanguage === prevLanguage) return;
// Save current code to the previous language slot
const code = editor?.getCode() ?? currentCode;
if (prevLanguage === 'c') cCode = code;
else if (prevLanguage === 'python') pythonCode = code;
// Load code for the new language
const newCode = currentLanguage === 'python' ? pythonCode : (cCode || data.initial_code || '');
currentCode = newCode;
editor?.setCode(newCode);
prevLanguage = currentLanguage;
});
// Clear lesson context when leaving page // Clear lesson context when leaving page
beforeNavigate(() => { beforeNavigate(() => {
lessonContext.set(null); lessonContext.set(null);
@ -140,13 +184,6 @@
} }
}); });
/** Check if student code contains all required key_text keywords. */
function checkKeyText(code: string, keyText: string): boolean {
if (!keyText.trim()) return true;
const keys = keyText.split('\n').map(k => k.trim()).filter(k => k.length > 0);
return keys.every(key => code.includes(key));
}
/** Mark lesson as complete: track progress + celebration. Called when ALL exercises pass. */ /** Mark lesson as complete: track progress + celebration. Called when ALL exercises pass. */
async function completeLesson() { async function completeLesson() {
showCelebration = true; showCelebration = true;
@ -168,159 +205,102 @@
if (!data || !circuitEditor) return; if (!data || !circuitEditor) return;
const simApi = circuitEditor.getApi(); const simApi = circuitEditor.getApi();
if (!simApi) { if (!simApi) {
circuitError = "Simulator belum siap."; Object.assign(circuitOut, { error: "Simulator belum siap.", success: false });
circuitSuccess = false;
activeTab = 'output'; activeTab = 'output';
return; return;
} }
circuitLoading = true; Object.assign(circuitOut, { loading: true, output: 'Mengevaluasi rangkaian...', error: '', success: null });
circuitOutput = 'Mengevaluasi rangkaian...';
circuitError = '';
circuitSuccess = null;
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) const circuitExpected = (isHybrid && data.expected_circuit_output)
? data.expected_circuit_output ? data.expected_circuit_output
: data.expected_output; : data.expected_output;
let expectedState: any = null; let expectedState: any = null;
try { try {
if (circuitExpected) { if (circuitExpected) expectedState = JSON.parse(circuitExpected);
expectedState = JSON.parse(circuitExpected); } catch {
} Object.assign(circuitOut, { error: "Format EXPECTED_OUTPUT tidak valid (Harus JSON).", success: false, loading: false });
} catch (e) {
circuitError = "Format EXPECTED_OUTPUT tidak valid (Harus JSON).";
circuitSuccess = false;
circuitLoading = false;
return; return;
} }
if (!expectedState) { if (!expectedState) {
circuitOutput = "Tidak ada kriteria evaluasi yang ditetapkan."; Object.assign(circuitOut, { output: "Tidak ada kriteria evaluasi yang ditetapkan.", success: true, loading: false });
circuitSuccess = true;
circuitLoading = false;
return; return;
} }
let allPassed = true; let { allPassed, messages } = expectedState.nodes
let messages: string[] = []; ? validateNodes(simApi, expectedState.nodes)
: { allPassed: true, messages: [] as string[] };
if (expectedState.nodes) {
for (const [nodeName, criteria] of Object.entries<any>(expectedState.nodes)) {
const actualV = simApi.getNodeVoltage(nodeName);
if (actualV === undefined || actualV === null) {
allPassed = false;
messages.push(`❌ Node '${nodeName}' tidak ditemukan.`);
continue;
}
const expectedV = criteria.voltage;
const tol = criteria.tolerance || 0.1;
if (Math.abs(actualV - expectedV) <= tol) {
messages.push(`✅ Node '${nodeName}': Tegangan ${actualV.toFixed(2)}V (Sesuai)`);
} else {
allPassed = false;
messages.push(`❌ Node '${nodeName}': Tegangan ${actualV.toFixed(2)}V (Harusnya ~${expectedV}V)`);
}
}
}
// TODO: Element-level checking (e.g. expectedState.elements) belum diimplementasi.
// GWT getInfo() returns Java array yang sulit di-parse dari JS.
const circuitText = circuitEditor.getCircuitText(); const circuitText = circuitEditor.getCircuitText();
// 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 circuitKeyText = (isHybrid && data.key_text_circuit) if (!checkKeyText(circuitText, circuitKeyText ?? '')) {
? data.key_text_circuit
: data.key_text;
const keyTextMatch = checkKeyText(circuitText, circuitKeyText ?? '');
if (!keyTextMatch) {
allPassed = false; allPassed = false;
messages.push(`❌ Komponen wajib belum lengkap (lihat instruksi).`); messages.push(`❌ Komponen wajib belum lengkap (lihat instruksi).`);
} }
circuitOutput = messages.join('\n'); circuitOut.output = messages.join('\n');
circuitSuccess = allPassed; circuitOut.success = allPassed;
if (allPassed) { if (allPassed) {
circuitPassed = true; circuitPassed = true;
if (isHybrid) { if (isHybrid) {
circuitOutput += '\n✅ Rangkaian benar!'; circuitOut.output += '\n✅ Rangkaian benar!';
if (!codePassed) { if (!codePassed) circuitOut.output += '\n⏳ Selesaikan juga tantangan kode untuk menyelesaikan pelajaran ini.';
circuitOutput += '\n⏳ Selesaikan juga tantangan kode untuk menyelesaikan pelajaran ini.';
}
} }
if (checkAllPassed()) { if (checkAllPassed()) {
await completeLesson(); await completeLesson();
setTimeout(() => { setTimeout(() => { showCelebration = false; activeTab = 'circuit'; }, 3000);
showCelebration = false;
activeTab = 'circuit';
}, 3000);
} }
} }
} catch (err: any) { } catch (err: any) {
circuitError = `Evaluasi gagal: ${err.message}`; Object.assign(circuitOut, { error: `Evaluasi gagal: ${err.message}`, success: false });
circuitSuccess = false;
} finally { } finally {
circuitLoading = false; circuitOut.loading = false;
} }
} }
async function handleRun() { async function handleRun() {
if (activeTab === 'circuit') { if (activeTab === 'circuit') { await evaluateCircuit(); return; }
await evaluateCircuit();
return;
}
if (!data) return; if (!data) return;
codeLoading = true;
codeOutput = ''; const out = getCodeOut();
codeError = ''; Object.assign(out, { loading: true, output: '', error: '', success: null });
codeSuccess = null;
activeTab = 'output'; activeTab = 'output';
try { try {
const code = editor?.getCode() ?? currentCode; const code = editor?.getCode() ?? currentCode;
const res = await compileCode({ code, language: data.language }); const res = await compileCode({ code, language: currentLanguage });
if (res.success) { if (!res.success) {
codeOutput = res.output; Object.assign(out, { error: res.error || 'Compilation failed', success: false });
codeSuccess = true; return;
}
if (data.expected_output) { out.output = res.output;
const outputMatch = res.output.trim() === data.expected_output.trim(); out.success = true;
const keyTextMatch = checkKeyText(code, data.key_text ?? '');
if (outputMatch && keyTextMatch) { if (data.expected_output) {
codePassed = true; const passed = res.output.trim() === data.expected_output.trim() && checkKeyText(code, data.key_text ?? '');
if (isHybrid && !circuitPassed) { if (passed) {
codeOutput += '\n✅ Kode benar!'; codePassed = true;
codeOutput += '\n⏳ Selesaikan juga tantangan rangkaian untuk menyelesaikan pelajaran ini.'; if (isHybrid && !circuitPassed) {
} out.output += '\n✅ Kode benar!\n⏳ Selesaikan juga tantangan rangkaian untuk menyelesaikan pelajaran ini.';
if (checkAllPassed()) { }
await completeLesson(); if (checkAllPassed()) {
// Auto-show solution after celebration await completeLesson();
if (data.solution_code) { if (data.solution_code) { showSolution = true; editor?.setCode(data.solution_code); }
showSolution = true; setTimeout(() => { showCelebration = false; activeTab = 'editor'; }, 3000);
editor?.setCode(data.solution_code);
}
setTimeout(() => {
showCelebration = false;
activeTab = 'editor';
}, 3000);
}
} }
} }
} else {
codeError = res.error || 'Compilation failed';
codeSuccess = false;
} }
} catch { } catch {
codeError = 'Gagal terhubung ke server'; Object.assign(out, { error: 'Gagal terhubung ke server', success: false });
codeSuccess = false;
} finally { } finally {
codeLoading = false; out.loading = false;
} }
} }
@ -328,15 +308,16 @@
if (!data) return; if (!data) return;
if (activeTab === 'circuit') { if (activeTab === 'circuit') {
circuitEditor?.setCircuitText(data.initial_circuit || data.initial_code); circuitEditor?.setCircuitText(data.initial_circuit || data.initial_code);
circuitOutput = ''; Object.assign(circuitOut, freshOutput());
circuitError = '';
circuitSuccess = null;
} else { } else {
currentCode = data.initial_code; const resetCode = currentLanguage === 'python'
editor?.setCode(data.initial_code); ? (data.initial_python || '')
codeOutput = ''; : (data.initial_code_c || data.initial_code || '');
codeError = ''; currentCode = resetCode;
codeSuccess = null; if (currentLanguage === 'c') cCode = resetCode;
else pythonCode = resetCode;
editor?.setCode(resetCode);
Object.assign(getCodeOut(), freshOutput());
} }
} }
@ -403,6 +384,7 @@
{isMobile} {isMobile}
bind:mobileMode bind:mobileMode
bind:activeTab bind:activeTab
bind:currentLanguage
hasInfo={!!data.lesson_info} hasInfo={!!data.lesson_info}
hasExercise={!!data.exercise_content} hasExercise={!!data.exercise_content}
activeTabs={data.active_tabs ?? []} activeTabs={data.active_tabs ?? []}
@ -481,18 +463,20 @@
{showSolution ? 'Sembunyikan Solusi' : 'Lihat Solusi'} {showSolution ? 'Sembunyikan Solusi' : 'Lihat Solusi'}
</button> </button>
{/if} {/if}
<span class="lang-label">{data.language_display_name}</span> <span class="lang-label">{currentLanguage === 'python' ? 'Python' : 'C'}</span>
</div> </div>
<div class="panel"> <div class="panel">
{#key currentLanguage}
<CodeEditor <CodeEditor
bind:this={editor} bind:this={editor}
code={currentCode} code={currentCode}
language={data.language} language={currentLanguage}
noPaste={true} noPaste={true}
storageKey={($authLoggedIn && !showSolution) ? `elemes_draft_${slug}` : undefined} storageKey={($authLoggedIn && !showSolution) ? `elemes_draft_${slug}_${currentLanguage}` : undefined}
onchange={(val) => { if (!showSolution) currentCode = val; }} onchange={(val) => { if (!showSolution) currentCode = val; }}
/> />
{/key}
</div> </div>
{#if data.expected_output} {#if data.expected_output}
@ -507,12 +491,7 @@
<!-- Output tab panel --> <!-- Output tab panel -->
<div class="tab-panel" class:tab-hidden={activeTab !== 'output'}> <div class="tab-panel" class:tab-hidden={activeTab !== 'output'}>
<OutputPanel <OutputPanel sections={outputSections} />
code={{ output: codeOutput, error: codeError, loading: codeLoading, success: codeSuccess }}
circuit={{ output: circuitOutput, error: circuitError, loading: circuitLoading, success: circuitSuccess }}
hasCode={!data.active_tabs || data.active_tabs.length === 0 || data.active_tabs.includes('c') || data.active_tabs.includes('python')}
hasCircuit={data.active_tabs?.includes('circuit') ?? false}
/>
</div> </div>
</div> </div>
@ -523,26 +502,35 @@
{/if} {/if}
<style> <style>
/* ── Shared rich-text styles (.prose + .tab-content) ──── */
.tab-content { .tab-content {
font-size: 0.85rem; font-size: 0.85rem;
padding: 0.75rem 0.5rem; padding: 0.75rem 0.5rem;
line-height: 1.65; line-height: 1.65;
} }
.tab-heading {
color: var(--color-primary);
font-size: 1.1rem;
margin-top: 0;
}
.prose :global(pre),
.tab-content :global(pre) { .tab-content :global(pre) {
background: var(--color-bg-secondary); background: var(--color-bg-secondary);
padding: 0.75rem; padding: 0.75rem;
border-radius: var(--radius); border-radius: var(--radius);
overflow-x: auto; overflow-x: auto;
} }
.prose :global(code),
.tab-content :global(code) { .tab-content :global(code) {
font-family: var(--font-mono); font-family: var(--font-mono);
font-size: 0.8rem; font-size: 0.85rem;
} }
.prose :global(p),
.tab-content :global(p) { .tab-content :global(p) {
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
} }
.tab-content :global(h2), .prose :global(h2), .prose :global(h3),
.tab-content :global(h3) { .tab-content :global(h2), .tab-content :global(h3) {
margin-top: 1.25rem; margin-top: 1.25rem;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
@ -554,11 +542,6 @@
.tab-content :global(li) { .tab-content :global(li) {
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
} }
.tab-heading {
color: var(--color-primary);
font-size: 1.1rem;
margin-top: 0;
}
/* ── Two-column layout ─────────────────────────────────── */ /* ── Two-column layout ─────────────────────────────────── */
.lesson-layout { .lesson-layout {
@ -577,25 +560,6 @@
-webkit-touch-callout: none; -webkit-touch-callout: none;
} }
.prose :global(pre) {
background: var(--color-bg-secondary);
padding: 0.75rem;
border-radius: var(--radius);
overflow-x: auto;
}
.prose :global(code) {
font-family: var(--font-mono);
font-size: 0.85rem;
}
.prose :global(p) {
margin-bottom: 0.75rem;
}
.prose :global(h2),
.prose :global(h3) {
margin-top: 1.25rem;
margin-bottom: 0.5rem;
}
/* ── Editor area (docked mode) ──────────────────────────── */ /* ── Editor area (docked mode) ──────────────────────────── */
.editor-area { .editor-area {
position: sticky; position: sticky;
@ -741,25 +705,11 @@
overscroll-behavior: contain; overscroll-behavior: contain;
} }
/* ── Mobile full: expand content to fill ────────────── */ /* ── Mobile full: expand content to fill ────────────── */
.editor-area.mobile-full .editor-body { .editor-area.mobile-full .editor-body,
flex: 1; .editor-area.mobile-full .tab-panel:not(.tab-hidden),
display: flex; .editor-area.mobile-full .panel,
flex-direction: column; .editor-area.mobile-full :global(.circuit-container),
min-height: 0; .editor-area.mobile-full :global(.editor-wrapper) {
}
.editor-area.mobile-full .tab-panel:not(.tab-hidden) {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.editor-area.mobile-full .panel {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.editor-area.mobile-full :global(.circuit-container) {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -769,12 +719,6 @@
flex: 1; flex: 1;
height: auto; height: auto;
} }
.editor-area.mobile-full :global(.editor-wrapper) {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.editor-area.mobile-full :global(.cm-editor) { .editor-area.mobile-full :global(.cm-editor) {
flex: 1; flex: 1;
max-height: none; max-height: none;

View File

@ -90,8 +90,8 @@ def api_lesson(filename):
prev_lesson = all_lessons[current_idx - 1] if current_idx > 0 else None prev_lesson = all_lessons[current_idx - 1] if current_idx > 0 else None
next_lesson = all_lessons[current_idx + 1] if 0 <= current_idx < len(all_lessons) - 1 else None next_lesson = all_lessons[current_idx + 1] if 0 <= current_idx < len(all_lessons) - 1 else None
# Derive language from active_tabs instead of global env var # Derive default language from active_tabs (frontend manages switching)
if 'python' in active_tabs: if 'python' in active_tabs and 'c' not in active_tabs:
programming_language = 'python' programming_language = 'python'
else: else:
programming_language = 'c' programming_language = 'c'