parent
0e944e5224
commit
09827bf3ff
|
|
@ -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">💻</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">⚡</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,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>
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue