Compare commits
No commits in common. "09827bf3ffaf97c7cc5896b04a33625ef820ba94" and "2505cd0977abbd7194ab44dfe7e75e362a9d889e" have entirely different histories.
09827bf3ff
...
2505cd0977
|
|
@ -29,20 +29,6 @@ Penjelasan:
|
||||||
- `\n` — membuat baris baru
|
- `\n` — membuat baris baru
|
||||||
- `return 0` — menandakan program selesai tanpa error
|
- `return 0` — menandakan program selesai tanpa error
|
||||||
|
|
||||||
## Versi Python
|
|
||||||
|
|
||||||
Dalam Python, mencetak teks jauh lebih sederhana:
|
|
||||||
|
|
||||||
```python
|
|
||||||
print("Hello, World!")
|
|
||||||
```
|
|
||||||
|
|
||||||
Penjelasan:
|
|
||||||
- Tidak perlu `#include` — Python sudah menyediakan `print()` secara bawaan
|
|
||||||
- Tidak perlu fungsi `main()` — kode langsung dijalankan dari atas ke bawah
|
|
||||||
- Tidak perlu `\n` — `print()` otomatis menambahkan baris baru
|
|
||||||
- Tidak perlu `return 0` atau titik koma
|
|
||||||
|
|
||||||
---EXERCISE---
|
---EXERCISE---
|
||||||
### Latihan
|
### Latihan
|
||||||
Buat program yang mencetak teks berikut:
|
Buat program yang mencetak teks berikut:
|
||||||
|
|
@ -66,11 +52,6 @@ int main() {
|
||||||
Halo Dunia
|
Halo Dunia
|
||||||
---END_EXPECTED_OUTPUT---
|
---END_EXPECTED_OUTPUT---
|
||||||
|
|
||||||
---INITIAL_PYTHON---
|
|
||||||
# Tulis kode kamu di sini
|
|
||||||
|
|
||||||
---END_INITIAL_PYTHON---
|
|
||||||
|
|
||||||
---KEY_TEXT---
|
---KEY_TEXT---
|
||||||
printf
|
printf
|
||||||
---END_KEY_TEXT---
|
---END_KEY_TEXT---
|
||||||
|
|
|
||||||
|
|
@ -43,24 +43,6 @@ Berikut rangkaian voltage divider sederhana:
|
||||||
|
|
||||||
Perhatikan tegangan di **Vout** adalah ~2.5V.
|
Perhatikan tegangan di **Vout** adalah ~2.5V.
|
||||||
|
|
||||||
## Versi Python
|
|
||||||
|
|
||||||
Perhitungan yang sama bisa dilakukan dengan Python:
|
|
||||||
|
|
||||||
```python
|
|
||||||
vin = 5
|
|
||||||
r1 = 1000
|
|
||||||
r2 = 1000
|
|
||||||
|
|
||||||
vout = vin * (r2 / (r1 + r2))
|
|
||||||
print(f"Vout = {vout:.2f}V")
|
|
||||||
```
|
|
||||||
|
|
||||||
Perbedaan utama:
|
|
||||||
- Tidak perlu mendeklarasikan tipe data
|
|
||||||
- Python menggunakan **float division** secara default (di C, `1000/2000` menghasilkan `0` karena integer division)
|
|
||||||
- f-string `{vout:.2f}` setara dengan `%.2f` di C
|
|
||||||
|
|
||||||
---EXERCISE---
|
---EXERCISE---
|
||||||
### Tantangan 1: Pemrograman C
|
### Tantangan 1: Pemrograman C
|
||||||
Buat program yang mencetak hasil perhitungan voltage divider.
|
Buat program yang mencetak hasil perhitungan voltage divider.
|
||||||
|
|
@ -101,12 +83,6 @@ Vout = 2.50V
|
||||||
}
|
}
|
||||||
---END_EXPECTED_CIRCUIT_OUTPUT---
|
---END_EXPECTED_CIRCUIT_OUTPUT---
|
||||||
|
|
||||||
---INITIAL_PYTHON---
|
|
||||||
# Hitung voltage divider: Vout = Vin * R2 / (R1 + R2)
|
|
||||||
# Vin=5, R1=1000, R2=1000
|
|
||||||
|
|
||||||
---END_INITIAL_PYTHON---
|
|
||||||
|
|
||||||
---KEY_TEXT---
|
---KEY_TEXT---
|
||||||
printf
|
printf
|
||||||
---END_KEY_TEXT---
|
---END_KEY_TEXT---
|
||||||
|
|
|
||||||
|
|
@ -44,25 +44,6 @@ Format specifier untuk `printf()`:
|
||||||
- `%f` — float (gunakan `%.1f` untuk 1 desimal)
|
- `%f` — float (gunakan `%.1f` untuk 1 desimal)
|
||||||
- `%c` — character
|
- `%c` — character
|
||||||
|
|
||||||
## Versi Python
|
|
||||||
|
|
||||||
Python tidak perlu mendeklarasikan tipe data secara eksplisit:
|
|
||||||
|
|
||||||
```python
|
|
||||||
umur = 17
|
|
||||||
tinggi = 165.5
|
|
||||||
huruf = 'A'
|
|
||||||
|
|
||||||
print(f"Umur: {umur} tahun")
|
|
||||||
print(f"Tinggi: {tinggi:.1f} cm")
|
|
||||||
print(f"Huruf: {huruf}")
|
|
||||||
```
|
|
||||||
|
|
||||||
Perbedaan utama:
|
|
||||||
- Tidak perlu menuliskan tipe (`int`, `float`, `char`) — Python mengenali otomatis
|
|
||||||
- Gunakan **f-string** (`f"..."`) untuk menyisipkan variabel ke dalam teks
|
|
||||||
- `{tinggi:.1f}` sama fungsinya dengan `%.1f` di C
|
|
||||||
|
|
||||||
---EXERCISE---
|
---EXERCISE---
|
||||||
### Latihan
|
### Latihan
|
||||||
Buat program yang mendeklarasikan variabel `nama_panjang` bertipe `int` dengan nilai `10`,
|
Buat program yang mendeklarasikan variabel `nama_panjang` bertipe `int` dengan nilai `10`,
|
||||||
|
|
@ -89,12 +70,6 @@ int main() {
|
||||||
Panjang nama: 10
|
Panjang nama: 10
|
||||||
---END_EXPECTED_OUTPUT---
|
---END_EXPECTED_OUTPUT---
|
||||||
|
|
||||||
---INITIAL_PYTHON---
|
|
||||||
# Deklarasikan variabel nama_panjang
|
|
||||||
# Cetak hasilnya menggunakan print
|
|
||||||
|
|
||||||
---END_INITIAL_PYTHON---
|
|
||||||
|
|
||||||
---KEY_TEXT---
|
---KEY_TEXT---
|
||||||
int
|
int
|
||||||
printf
|
printf
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,34 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export interface OutputSection {
|
interface OutputSection {
|
||||||
output?: string;
|
output?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
success?: boolean | null;
|
success?: boolean | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OutputEntry {
|
|
||||||
key: string;
|
|
||||||
label: string;
|
|
||||||
icon: string;
|
|
||||||
data: OutputSection;
|
|
||||||
placeholder: string;
|
|
||||||
loadingText: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
sections?: OutputEntry[];
|
code?: OutputSection;
|
||||||
|
circuit?: OutputSection;
|
||||||
|
hasCode?: boolean;
|
||||||
|
hasCircuit?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { sections = [] }: Props = $props();
|
let {
|
||||||
|
code = { output: '', error: '', loading: false, success: null },
|
||||||
|
circuit = { output: '', error: '', loading: false, success: null },
|
||||||
|
hasCode = true,
|
||||||
|
hasCircuit = false,
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
let anyLoading = $derived(sections.some(s => s.data.loading));
|
// Determine overall loading state for header badge
|
||||||
|
let anyLoading = $derived(code.loading || circuit.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 ran = sections.filter(s => s.data.success !== null && s.data.success !== undefined);
|
const codeRan = code.success !== null && code.success !== undefined;
|
||||||
if (ran.length === 0) return null;
|
const circuitRan = circuit.success !== null && circuit.success !== undefined;
|
||||||
if (ran.some(s => s.data.success === false)) return false;
|
if (!codeRan && !circuitRan) return null;
|
||||||
|
if ((codeRan && code.success === false) || (circuitRan && circuit.success === false)) return false;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -44,23 +46,42 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="output-sections">
|
<div class="output-sections">
|
||||||
{#each sections as sec (sec.key)}
|
<!-- Code output section -->
|
||||||
<div class="output-section" class:has-error={!!sec.data.error} class:has-success={sec.data.success === true}>
|
{#if hasCode}
|
||||||
|
<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">{sec.icon}</span> {sec.label}
|
<span class="section-icon">💻</span> Code
|
||||||
{#if sec.data.loading}
|
{#if code.loading}
|
||||||
<span class="section-badge running">{sec.loadingText}</span>
|
<span class="section-badge running">Compiling...</span>
|
||||||
{:else if sec.data.success === true}
|
{:else if code.success === true}
|
||||||
<span class="section-badge success">OK</span>
|
<span class="section-badge success">OK</span>
|
||||||
{:else if sec.data.success === false}
|
{:else if code.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 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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/if}
|
||||||
|
|
||||||
{#if sections.length === 0}
|
<!-- Circuit output section -->
|
||||||
|
{#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">⚡</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>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@
|
||||||
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[];
|
||||||
|
|
@ -21,7 +20,6 @@
|
||||||
isMobile,
|
isMobile,
|
||||||
mobileMode = $bindable(),
|
mobileMode = $bindable(),
|
||||||
activeTab = $bindable(),
|
activeTab = $bindable(),
|
||||||
currentLanguage = $bindable(),
|
|
||||||
hasInfo,
|
hasInfo,
|
||||||
hasExercise,
|
hasExercise,
|
||||||
activeTabs,
|
activeTabs,
|
||||||
|
|
@ -35,12 +33,9 @@
|
||||||
|
|
||||||
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 || hasC || hasPython
|
!activeTabs || activeTabs.length === 0 || activeTabs.includes('c') || activeTabs.includes('python')
|
||||||
);
|
);
|
||||||
const hasMultiLang = $derived(hasC && hasPython);
|
|
||||||
const hasCircuit = $derived(activeTabs?.includes('circuit') ?? false);
|
const hasCircuit = $derived(activeTabs?.includes('circuit') ?? false);
|
||||||
|
|
||||||
function cycleMobileSheet() {
|
function cycleMobileSheet() {
|
||||||
|
|
@ -75,12 +70,7 @@
|
||||||
<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}
|
||||||
{#if hasMultiLang}
|
<button class="chrome-tab" class:active={activeTab === 'editor'} onclick={() => (activeTab = 'editor')}>Code</button>
|
||||||
<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>
|
||||||
|
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
/**
|
|
||||||
* 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 };
|
|
||||||
}
|
|
||||||
|
|
@ -3,12 +3,11 @@
|
||||||
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, { type OutputEntry } from '$components/OutputPanel.svelte';
|
import OutputPanel 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';
|
||||||
|
|
@ -27,20 +26,17 @@
|
||||||
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');
|
|
||||||
|
|
||||||
// Per-language code tracking
|
// Separate output state for code and circuit
|
||||||
let cCode = $state('');
|
let codeOutput = $state('');
|
||||||
let pythonCode = $state('');
|
let codeError = $state('');
|
||||||
|
let codeLoading = $state(false);
|
||||||
|
let codeSuccess = $state<boolean | null>(null);
|
||||||
|
|
||||||
// Output state per language + circuit
|
let circuitOutput = $state('');
|
||||||
const freshOutput = () => ({ output: '', error: '', loading: false, success: null as boolean | null });
|
let circuitError = $state('');
|
||||||
let cOut = $state(freshOutput());
|
let circuitLoading = $state(false);
|
||||||
let pyOut = $state(freshOutput());
|
let circuitSuccess = $state<boolean | null>(null);
|
||||||
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);
|
||||||
|
|
@ -53,23 +49,7 @@
|
||||||
);
|
);
|
||||||
|
|
||||||
// Derived: any loading state (for disabling Run button)
|
// Derived: any loading state (for disabling Run button)
|
||||||
let compiling = $derived(cOut.loading || pyOut.loading || circuitOut.loading);
|
let compiling = $derived(codeLoading || circuitLoading);
|
||||||
|
|
||||||
// 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);
|
||||||
|
|
@ -113,28 +93,19 @@
|
||||||
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 || '';
|
||||||
// Initialize per-language code
|
codeOutput = '';
|
||||||
cCode = lesson.initial_code_c || '';
|
codeError = '';
|
||||||
pythonCode = lesson.initial_python || '';
|
codeSuccess = null;
|
||||||
|
circuitOutput = '';
|
||||||
// Determine initial language (use local var to avoid reactive dependency on currentLanguage)
|
circuitError = '';
|
||||||
const hasC = lesson.active_tabs?.includes('c');
|
circuitSuccess = null;
|
||||||
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') && !hasC && !hasPython) activeTab = 'circuit';
|
else if (lesson.active_tabs?.includes('circuit') && !lesson.active_tabs?.includes('c') && !lesson.active_tabs?.includes('python')) activeTab = 'circuit';
|
||||||
else activeTab = 'editor';
|
else activeTab = 'editor';
|
||||||
mobileMode = 'half';
|
mobileMode = 'half';
|
||||||
|
|
||||||
|
|
@ -148,21 +119,6 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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);
|
||||||
|
|
@ -184,6 +140,13 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** 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;
|
||||||
|
|
@ -205,102 +168,159 @@
|
||||||
if (!data || !circuitEditor) return;
|
if (!data || !circuitEditor) return;
|
||||||
const simApi = circuitEditor.getApi();
|
const simApi = circuitEditor.getApi();
|
||||||
if (!simApi) {
|
if (!simApi) {
|
||||||
Object.assign(circuitOut, { error: "Simulator belum siap.", success: false });
|
circuitError = "Simulator belum siap.";
|
||||||
|
circuitSuccess = false;
|
||||||
activeTab = 'output';
|
activeTab = 'output';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.assign(circuitOut, { loading: true, output: 'Mengevaluasi rangkaian...', error: '', success: null });
|
circuitLoading = true;
|
||||||
|
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) expectedState = JSON.parse(circuitExpected);
|
if (circuitExpected) {
|
||||||
} catch {
|
expectedState = JSON.parse(circuitExpected);
|
||||||
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) {
|
||||||
Object.assign(circuitOut, { output: "Tidak ada kriteria evaluasi yang ditetapkan.", success: true, loading: false });
|
circuitOutput = "Tidak ada kriteria evaluasi yang ditetapkan.";
|
||||||
|
circuitSuccess = true;
|
||||||
|
circuitLoading = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { allPassed, messages } = expectedState.nodes
|
let allPassed = true;
|
||||||
? validateNodes(simApi, expectedState.nodes)
|
let messages: string[] = [];
|
||||||
: { 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();
|
||||||
const circuitKeyText = (isHybrid && data.key_text_circuit) ? data.key_text_circuit : data.key_text;
|
// For hybrid lessons, use key_text_circuit; otherwise fallback to key_text
|
||||||
if (!checkKeyText(circuitText, circuitKeyText ?? '')) {
|
const circuitKeyText = (isHybrid && data.key_text_circuit)
|
||||||
|
? 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).`);
|
||||||
}
|
}
|
||||||
|
|
||||||
circuitOut.output = messages.join('\n');
|
circuitOutput = messages.join('\n');
|
||||||
circuitOut.success = allPassed;
|
circuitSuccess = allPassed;
|
||||||
|
|
||||||
if (allPassed) {
|
if (allPassed) {
|
||||||
circuitPassed = true;
|
circuitPassed = true;
|
||||||
if (isHybrid) {
|
if (isHybrid) {
|
||||||
circuitOut.output += '\n✅ Rangkaian benar!';
|
circuitOutput += '\n✅ Rangkaian benar!';
|
||||||
if (!codePassed) circuitOut.output += '\n⏳ Selesaikan juga tantangan kode untuk menyelesaikan pelajaran ini.';
|
if (!codePassed) {
|
||||||
|
circuitOutput += '\n⏳ Selesaikan juga tantangan kode untuk menyelesaikan pelajaran ini.';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (checkAllPassed()) {
|
if (checkAllPassed()) {
|
||||||
await completeLesson();
|
await completeLesson();
|
||||||
setTimeout(() => { showCelebration = false; activeTab = 'circuit'; }, 3000);
|
setTimeout(() => {
|
||||||
|
showCelebration = false;
|
||||||
|
activeTab = 'circuit';
|
||||||
|
}, 3000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
Object.assign(circuitOut, { error: `Evaluasi gagal: ${err.message}`, success: false });
|
circuitError = `Evaluasi gagal: ${err.message}`;
|
||||||
|
circuitSuccess = false;
|
||||||
} finally {
|
} finally {
|
||||||
circuitOut.loading = false;
|
circuitLoading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleRun() {
|
async function handleRun() {
|
||||||
if (activeTab === 'circuit') { await evaluateCircuit(); return; }
|
if (activeTab === 'circuit') {
|
||||||
|
await evaluateCircuit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
codeLoading = true;
|
||||||
const out = getCodeOut();
|
codeOutput = '';
|
||||||
Object.assign(out, { loading: true, output: '', error: '', success: null });
|
codeError = '';
|
||||||
|
codeSuccess = null;
|
||||||
activeTab = 'output';
|
activeTab = 'output';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const code = editor?.getCode() ?? currentCode;
|
const code = editor?.getCode() ?? currentCode;
|
||||||
const res = await compileCode({ code, language: currentLanguage });
|
const res = await compileCode({ code, language: data.language });
|
||||||
|
|
||||||
if (!res.success) {
|
if (res.success) {
|
||||||
Object.assign(out, { error: res.error || 'Compilation failed', success: false });
|
codeOutput = res.output;
|
||||||
return;
|
codeSuccess = true;
|
||||||
}
|
|
||||||
|
|
||||||
out.output = res.output;
|
if (data.expected_output) {
|
||||||
out.success = true;
|
const outputMatch = res.output.trim() === data.expected_output.trim();
|
||||||
|
const keyTextMatch = checkKeyText(code, data.key_text ?? '');
|
||||||
if (data.expected_output) {
|
if (outputMatch && keyTextMatch) {
|
||||||
const passed = res.output.trim() === data.expected_output.trim() && checkKeyText(code, data.key_text ?? '');
|
codePassed = true;
|
||||||
if (passed) {
|
if (isHybrid && !circuitPassed) {
|
||||||
codePassed = true;
|
codeOutput += '\n✅ Kode benar!';
|
||||||
if (isHybrid && !circuitPassed) {
|
codeOutput += '\n⏳ Selesaikan juga tantangan rangkaian untuk menyelesaikan pelajaran ini.';
|
||||||
out.output += '\n✅ Kode benar!\n⏳ Selesaikan juga tantangan rangkaian untuk menyelesaikan pelajaran ini.';
|
}
|
||||||
}
|
if (checkAllPassed()) {
|
||||||
if (checkAllPassed()) {
|
await completeLesson();
|
||||||
await completeLesson();
|
// Auto-show solution after celebration
|
||||||
if (data.solution_code) { showSolution = true; editor?.setCode(data.solution_code); }
|
if (data.solution_code) {
|
||||||
setTimeout(() => { showCelebration = false; activeTab = 'editor'; }, 3000);
|
showSolution = true;
|
||||||
|
editor?.setCode(data.solution_code);
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
showCelebration = false;
|
||||||
|
activeTab = 'editor';
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
codeError = res.error || 'Compilation failed';
|
||||||
|
codeSuccess = false;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
Object.assign(out, { error: 'Gagal terhubung ke server', success: false });
|
codeError = 'Gagal terhubung ke server';
|
||||||
|
codeSuccess = false;
|
||||||
} finally {
|
} finally {
|
||||||
out.loading = false;
|
codeLoading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -308,16 +328,15 @@
|
||||||
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);
|
||||||
Object.assign(circuitOut, freshOutput());
|
circuitOutput = '';
|
||||||
|
circuitError = '';
|
||||||
|
circuitSuccess = null;
|
||||||
} else {
|
} else {
|
||||||
const resetCode = currentLanguage === 'python'
|
currentCode = data.initial_code;
|
||||||
? (data.initial_python || '')
|
editor?.setCode(data.initial_code);
|
||||||
: (data.initial_code_c || data.initial_code || '');
|
codeOutput = '';
|
||||||
currentCode = resetCode;
|
codeError = '';
|
||||||
if (currentLanguage === 'c') cCode = resetCode;
|
codeSuccess = null;
|
||||||
else pythonCode = resetCode;
|
|
||||||
editor?.setCode(resetCode);
|
|
||||||
Object.assign(getCodeOut(), freshOutput());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -384,7 +403,6 @@
|
||||||
{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 ?? []}
|
||||||
|
|
@ -463,20 +481,18 @@
|
||||||
{showSolution ? 'Sembunyikan Solusi' : 'Lihat Solusi'}
|
{showSolution ? 'Sembunyikan Solusi' : 'Lihat Solusi'}
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
<span class="lang-label">{currentLanguage === 'python' ? 'Python' : 'C'}</span>
|
<span class="lang-label">{data.language_display_name}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
{#key currentLanguage}
|
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
bind:this={editor}
|
bind:this={editor}
|
||||||
code={currentCode}
|
code={currentCode}
|
||||||
language={currentLanguage}
|
language={data.language}
|
||||||
noPaste={true}
|
noPaste={true}
|
||||||
storageKey={($authLoggedIn && !showSolution) ? `elemes_draft_${slug}_${currentLanguage}` : undefined}
|
storageKey={($authLoggedIn && !showSolution) ? `elemes_draft_${slug}` : undefined}
|
||||||
onchange={(val) => { if (!showSolution) currentCode = val; }}
|
onchange={(val) => { if (!showSolution) currentCode = val; }}
|
||||||
/>
|
/>
|
||||||
{/key}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if data.expected_output}
|
{#if data.expected_output}
|
||||||
|
|
@ -491,7 +507,12 @@
|
||||||
|
|
||||||
<!-- 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 sections={outputSections} />
|
<OutputPanel
|
||||||
|
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>
|
||||||
|
|
||||||
|
|
@ -502,35 +523,26 @@
|
||||||
{/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.85rem;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
.prose :global(p),
|
|
||||||
.tab-content :global(p) {
|
.tab-content :global(p) {
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
.prose :global(h2), .prose :global(h3),
|
.tab-content :global(h2),
|
||||||
.tab-content :global(h2), .tab-content :global(h3) {
|
.tab-content :global(h3) {
|
||||||
margin-top: 1.25rem;
|
margin-top: 1.25rem;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
@ -542,6 +554,11 @@
|
||||||
.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 {
|
||||||
|
|
@ -560,6 +577,25 @@
|
||||||
-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;
|
||||||
|
|
@ -705,11 +741,25 @@
|
||||||
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 {
|
||||||
.editor-area.mobile-full .tab-panel:not(.tab-hidden),
|
flex: 1;
|
||||||
.editor-area.mobile-full .panel,
|
display: flex;
|
||||||
.editor-area.mobile-full :global(.circuit-container),
|
flex-direction: column;
|
||||||
.editor-area.mobile-full :global(.editor-wrapper) {
|
min-height: 0;
|
||||||
|
}
|
||||||
|
.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;
|
||||||
|
|
@ -719,6 +769,12 @@
|
||||||
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;
|
||||||
|
|
|
||||||
|
|
@ -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 default language from active_tabs (frontend manages switching)
|
# Derive language from active_tabs instead of global env var
|
||||||
if 'python' in active_tabs and 'c' not in active_tabs:
|
if 'python' in active_tabs:
|
||||||
programming_language = 'python'
|
programming_language = 'python'
|
||||||
else:
|
else:
|
||||||
programming_language = 'c'
|
programming_language = 'c'
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue