2026-05-08 09:50:07 +07:00
import { page } from '$app/stores' ;
import { get } from 'svelte/store' ;
import { tick , untrack } from 'svelte' ;
import { auth , authLoggedIn } from '$stores/auth' ;
import { lessonContext } from '$stores/lessonContext' ;
import { compileCode , trackProgress } from '$services/api' ;
import { evaluateVelxioSubmission } from '$services/velxio-evaluator' ;
import { evaluateFlowchartSubmission } from '$services/flowchart-evaluator' ;
import { evaluateCircuitSubmission , processLanguageEvaluation } from '$services/evaluators' ;
import { getVelxioState , initVelxioBridge } from '$services/velxio-manager' ;
import { VelxioBridge } from '$services/velxio-bridge' ;
import type { LessonContent } from '$types/lesson' ;
import type { CodeEditor } from '$components/CodeEditor.svelte' ;
import type { CircuitEditor } from '$components/CircuitEditor.svelte' ;
export class LessonManager {
data = $state < LessonContent | null > ( null ) ;
lessonCompleted = $state ( false ) ;
isQuizMode = $state ( false ) ;
currentCode = $state ( '' ) ;
currentLanguage = $state < string > ( 'c' ) ;
// Per-language code tracking
cCode = $state ( '' ) ;
pythonCode = $state ( '' ) ;
// Output state
freshOutput = ( ) = > ( { output : '' , error : '' , loading : false , success : null as boolean | null , debug : undefined as string [ ] | undefined } ) ;
cOut = $state ( this . freshOutput ( ) ) ;
pyOut = $state ( this . freshOutput ( ) ) ;
circuitOut = $state ( this . freshOutput ( ) ) ;
velxioOut = $state ( this . freshOutput ( ) ) ;
flowchartOut = $state ( this . freshOutput ( ) ) ;
cPassed = $state ( false ) ;
pythonPassed = $state ( false ) ;
circuitPassed = $state ( false ) ;
flowchartPassed = $state ( false ) ;
velxioBridge = $state < VelxioBridge | null > ( null ) ;
velxioReady = $state ( false ) ;
velxioSaving = $state ( false ) ;
velxioError = $state ( false ) ;
velxioIframe = $state < HTMLIFrameElement | null > ( null ) ;
showSolution = $state ( false ) ;
activeTab = $state < 'info' | 'exercise' | 'editor' | 'circuit' | 'output' | 'velxio' | 'flowchart' > ( 'info' ) ;
showCelebration = $state ( false ) ;
mobileMode = $state < 'hidden' | 'half' | 'full' > ( 'hidden' ) ;
isMobile = $state ( false ) ;
// Refs (set from component)
editor = $state < CodeEditor | null > ( null ) ;
circuitEditor = $state < CircuitEditor | null > ( null ) ;
flowchartTab = $state < any > ( null ) ;
slug = $derived ( get ( page ) . params . slug ) ;
isVelxio = $derived ( this . data ? . active_tabs ? . includes ( 'velxio' ) ? ? false ) ;
isFlowchart = $derived ( this . data ? . active_tabs ? . includes ( 'flowchart' ) ? ? false ) ;
outputSections = $derived . by ( ( ) = > {
const tabs = this . data ? . active_tabs ? ? [ ] ;
const secs : any [ ] = [ ] ;
if ( tabs . includes ( 'c' ) || ( ! tabs . length && ! tabs . includes ( 'python' ) ) ) {
secs . push ( { key : 'c' , label : 'C' , icon : '\u{1F4BB}' , data : this.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 : this.pyOut , placeholder : 'Klik "Run" untuk menjalankan kode Python' , loadingText : 'Menjalankan Python...' } ) ;
}
if ( tabs . includes ( 'circuit' ) ) {
secs . push ( { key : 'circuit' , label : 'Circuit' , icon : '\u26A1' , data : this.circuitOut , placeholder : 'Klik "Cek Rangkaian" untuk mengevaluasi' , loadingText : 'Mengevaluasi rangkaian...' } ) ;
}
if ( tabs . includes ( 'velxio' ) ) {
secs . push ( { key : 'velxio' , label : 'Arduino' , icon : '\u{1F4DF}' , data : this.velxioOut , placeholder : 'Klik "Compile & Run" untuk menjalankan kode' , loadingText : 'Mengevaluasi...' } ) ;
}
if ( tabs . includes ( 'flowchart' ) ) {
secs . push ( { key : 'flowchart' , label : 'Flowchart' , icon : '\u{1F531}' , data : this.flowchartOut , placeholder : 'Klik "Cek Flowchart" untuk mengevaluasi alur logika' , loadingText : 'Mengevaluasi alur...' } ) ;
}
return secs ;
} ) ;
isHybrid = $derived (
( this . data ? . active_tabs ? . includes ( 'c' ) || this . data ? . active_tabs ? . includes ( 'python' ) ) &&
this . data ? . active_tabs ? . includes ( 'circuit' )
) ;
compiling = $derived ( this . cOut . loading || this . pyOut . loading || this . circuitOut . loading || this . flowchartOut . loading ) ;
arduinoCodeKey = $derived ( ` elemes_arduino_code_ ${ this . slug } ` ) ;
arduinoCircuitKey = $derived ( ` elemes_arduino_circuit_ ${ this . slug } ` ) ;
flowchartStorageKey = $derived ( ` elemes_flowchart_draft_ ${ this . slug } ` ) ;
constructor ( ) {
// Media query detection
if ( typeof window !== 'undefined' ) {
const mql = window . matchMedia ( '(max-width: 768px)' ) ;
this . isMobile = mql . matches ;
const handler = ( e : MediaQueryListEvent ) = > {
this . isMobile = e . matches ;
} ;
mql . addEventListener ( 'change' , handler ) ;
// Auto-save Velxio state periodically
$effect ( ( ) = > {
if ( this . velxioReady && get ( authLoggedIn ) && ! this . showSolution ) {
const interval = setInterval ( ( ) = > {
const state = getVelxioState ( this . velxioIframe ) ;
if ( ! state ) return ;
let changed = false ;
// 1. Source Code
const savedCode = localStorage . getItem ( this . arduinoCodeKey ) ;
if ( state . code && state . code !== savedCode ) {
console . log ( '[Velxio Auto-save] Saving code changes (Zustand)' ) ;
localStorage . setItem ( this . arduinoCodeKey , state . code ) ;
changed = true ;
}
// 2. Circuit (Diagram + Wires)
const savedCircuit = localStorage . getItem ( this . arduinoCircuitKey ) ;
if ( state . circuit && state . circuit !== savedCircuit ) {
console . log ( '[Velxio Auto-save] Saving circuit changes (Zustand)' ) ;
localStorage . setItem ( this . arduinoCircuitKey , state . circuit ) ;
changed = true ;
}
if ( changed ) {
this . velxioSaving = true ;
setTimeout ( ( ) = > { this . velxioSaving = false ; } , 1500 ) ;
}
} , 7000 ) ; // Poll every 7 seconds
return ( ) = > clearInterval ( interval ) ;
}
} ) ;
}
}
// Switch editor content when language tab changes
prevLanguage = $state < string > ( 'c' ) ;
setupLanguageSync() {
$effect ( ( ) = > {
if ( ! this . data || this . currentLanguage === this . prevLanguage ) return ;
// Save current code to the previous language slot
const code = this . editor ? . getCode ( ) ? ? this . currentCode ;
if ( this . prevLanguage === 'c' ) this . cCode = code ;
else if ( this . prevLanguage === 'python' ) this . pythonCode = code ;
// Load code for the new language
const newCode = this . currentLanguage === 'python' ? this . pythonCode : ( this . cCode || this . data . initial_code || '' ) ;
this . currentCode = newCode ;
this . editor ? . setCode ( newCode ) ;
this . prevLanguage = this . currentLanguage ;
} ) ;
}
init ( lesson : LessonContent ) {
untrack ( ( ) = > {
this . data = lesson ;
this . lessonCompleted = lesson . lesson_completed ;
this . isQuizMode = false ;
this . cCode = lesson . initial_code_c || '' ;
this . pythonCode = lesson . initial_python || '' ;
const hasC = lesson . active_tabs ? . includes ( 'c' ) ;
const hasPython = lesson . active_tabs ? . includes ( 'python' ) ;
const initLang = ( hasPython && ! hasC ) ? 'python' : 'c' ;
this . currentLanguage = initLang ;
this . currentCode = initLang === 'python' ? this . pythonCode : ( this . cCode || lesson . initial_code || '' ) ;
this . cOut = this . freshOutput ( ) ;
this . pyOut = this . freshOutput ( ) ;
this . circuitOut = this . freshOutput ( ) ;
this . velxioOut = this . freshOutput ( ) ;
this . cPassed = false ;
this . pythonPassed = false ;
this . circuitPassed = false ;
this . showSolution = false ;
if ( this . velxioBridge ) { this . velxioBridge . destroy ( ) ; this . velxioBridge = null ; }
this . velxioReady = false ;
this . velxioError = false ;
if ( lesson . lesson_info ) this . activeTab = 'info' ;
else if ( lesson . exercise_content ) this . activeTab = 'exercise' ;
else if ( lesson . active_tabs ? . includes ( 'velxio' ) ) this . activeTab = 'velxio' ;
else if ( lesson . active_tabs ? . includes ( 'flowchart' ) ) this . activeTab = 'flowchart' ;
else if ( lesson . active_tabs ? . includes ( 'circuit' ) && ! hasC && ! hasPython ) this . activeTab = 'circuit' ;
else this . activeTab = 'editor' ;
this . mobileMode = 'hidden' ;
lessonContext . set ( {
title : lesson.lesson_title ,
completed : lesson.lesson_completed ,
prevLesson : lesson.prev_lesson ,
nextLesson : lesson.next_lesson
} ) ;
} ) ;
}
2026-05-09 13:04:42 +07:00
async completeLesson ( status = 'completed' ) {
if ( this . lessonCompleted && status === 'completed' ) return ;
2026-05-08 09:50:07 +07:00
this . showCelebration = true ;
2026-05-09 13:04:42 +07:00
2026-05-08 09:50:07 +07:00
if ( get ( authLoggedIn ) ) {
const lessonName = this . slug . replace ( '.md' , '' ) ;
2026-05-09 13:04:42 +07:00
await trackProgress ( auth . token , lessonName , status ) ;
2026-05-08 09:50:07 +07:00
this . lessonCompleted = true ;
lessonContext . update ( ctx = > ctx ? { . . . ctx , completed : true } : ctx ) ;
}
}
checkAllPassed ( ) : boolean {
const needsC = this . data ? . active_tabs ? . includes ( 'c' ) ;
const needsPython = this . data ? . active_tabs ? . includes ( 'python' ) ;
const needsCircuit = this . data ? . active_tabs ? . includes ( 'circuit' ) ;
const needsFlowchart = this . data ? . active_tabs ? . includes ( 'flowchart' ) ;
if ( ! this . data ? . active_tabs ? . length ) return true ;
if ( needsC && ! this . cPassed ) return false ;
if ( needsPython && ! this . pythonPassed ) return false ;
if ( needsCircuit && ! this . circuitPassed ) return false ;
if ( needsFlowchart && ! this . flowchartPassed ) return false ;
return true ;
}
async evaluateFlowchart() {
if ( ! this . data || ! this . flowchartTab ) return ;
Object . assign ( this . flowchartOut , { loading : true , output : 'Mengevaluasi alur...' , error : '' , success : null } ) ;
this . activeTab = 'output' ;
try {
const flowchartText = await this . flowchartTab . getFlowchartText ( ) ;
if ( ! flowchartText ) {
Object . assign ( this . flowchartOut , { error : 'Gagal mengambil data flowchart.' , success : false } ) ;
return ;
}
const expectedFlowchart = this . data . expected_flowchart || '' ;
if ( ! expectedFlowchart ) {
Object . assign ( this . flowchartOut , { error : 'Kunci jawaban tidak tersedia untuk pelajaran ini.' , success : false } ) ;
return ;
}
const result = evaluateFlowchartSubmission ( flowchartText , expectedFlowchart ) ;
this . flowchartOut . output = result . output ;
this . flowchartOut . success = result . pass ;
if ( this . flowchartOut . success ) {
this . flowchartPassed = true ;
if ( this . checkAllPassed ( ) ) {
await this . completeLesson ( ) ;
setTimeout ( ( ) = > { this . showCelebration = false ; this . activeTab = 'flowchart' ; } , 3000 ) ;
}
}
} catch ( err : any ) {
Object . assign ( this . flowchartOut , { error : ` Terjadi kesalahan saat evaluasi: ${ err . message } ` , success : false } ) ;
} finally {
this . flowchartOut . loading = false ;
}
}
async evaluateCircuit() {
if ( ! this . data || ! this . circuitEditor ) return ;
const simApi = this . circuitEditor . getApi ( ) ;
if ( ! simApi ) {
Object . assign ( this . circuitOut , { error : "Simulator belum siap." , success : false } ) ;
this . activeTab = 'output' ;
return ;
}
Object . assign ( this . circuitOut , { loading : true , output : 'Mengevaluasi rangkaian...' , error : '' , success : null } ) ;
this . activeTab = 'output' ;
try {
const circuitText = this . circuitEditor . getCircuitText ( ) ;
const res = evaluateCircuitSubmission ( simApi , circuitText , this . isHybrid , this . data , ( ) = > this . checkAllPassed ( ) ) ;
if ( res . error ) {
Object . assign ( this . circuitOut , { error : res.error , success : false , loading : false } ) ;
return ;
}
this . circuitOut . output = res . output ;
this . circuitOut . success = res . pass ;
if ( res . pass ) {
this . circuitPassed = true ;
if ( this . checkAllPassed ( ) ) {
await this . completeLesson ( ) ;
setTimeout ( ( ) = > { this . showCelebration = false ; this . activeTab = 'circuit' ; } , 3000 ) ;
}
}
} catch ( err : any ) {
Object . assign ( this . circuitOut , { error : ` Evaluasi gagal: ${ err . message } ` , success : false } ) ;
} finally {
this . circuitOut . loading = false ;
}
}
async evaluateLanguage ( lang : 'c' | 'python' ) {
if ( ! this . data ) return ;
const out = lang === 'c' ? this . cOut : this.pyOut ;
Object . assign ( out , { loading : true , output : '' , error : '' , success : null } ) ;
this . activeTab = 'output' ;
try {
const code = ( this . currentLanguage === lang ) ? ( this . editor ? . getCode ( ) ? ? this . currentCode ) : ( lang === 'c' ? this . cCode : this.pythonCode ) ;
2026-05-09 13:04:42 +07:00
const res = await compileCode ( { code , language : lang , token : auth.token } ) ;
2026-05-08 09:50:07 +07:00
if ( ! res . success ) {
Object . assign ( out , { error : res.error || 'Compilation failed' , success : false } ) ;
return ;
}
out . output = res . output ;
out . success = true ;
if ( this . data . expected_output || this . data . expected_output_python ) {
const { isCorrect } = processLanguageEvaluation ( res . output , code , lang , this . currentLanguage , this . cCode , this . pythonCode , this . data ) ;
if ( isCorrect ) {
if ( lang === 'c' ) this . cPassed = true ;
else if ( lang === 'python' ) this . pythonPassed = true ;
if ( ! this . checkAllPassed ( ) ) {
out . output += '\n✅ Kode benar!\n⏳ Selesaikan juga tantangan di tab lainnya untuk menyelesaikan pelajaran ini.' ;
} else {
out . output += '\n🎉 Semuanya benar!' ;
}
if ( this . checkAllPassed ( ) ) {
await this . completeLesson ( ) ;
if ( this . data . solution_code || this . data . solution_python || this . data . solution_circuit ) {
this . showSolution = true ;
this . handleShowSolution ( ) ;
this . showSolution = true ;
}
setTimeout ( ( ) = > { this . showCelebration = false ; this . activeTab = 'editor' ; } , 3000 ) ;
}
}
}
} catch {
Object . assign ( out , { error : 'Gagal terhubung ke server' , success : false } ) ;
} finally {
out . loading = false ;
}
}
async handleRun() {
if ( this . activeTab === 'circuit' ) { await this . evaluateCircuit ( ) ; return ; }
if ( this . activeTab === 'flowchart' ) { await this . evaluateFlowchart ( ) ; return ; }
if ( ! this . data ) return ;
this . activeTab = 'output' ;
await this . evaluateLanguage ( this . currentLanguage as 'c' | 'python' ) ;
}
async handleRunAll() {
if ( ! this . data ) return ;
this . activeTab = 'output' ;
const tabs = this . data . active_tabs ? ? [ ] ;
if ( tabs . includes ( 'c' ) || ( ! tabs . length && ! tabs . includes ( 'python' ) ) ) await this . evaluateLanguage ( 'c' ) ;
if ( tabs . includes ( 'python' ) ) await this . evaluateLanguage ( 'python' ) ;
if ( tabs . includes ( 'circuit' ) ) await this . evaluateCircuit ( ) ;
if ( tabs . includes ( 'velxio' ) ) await this . handleVelxioSubmit ( ) ;
if ( tabs . includes ( 'flowchart' ) ) await this . evaluateFlowchart ( ) ;
}
handleReset() {
if ( ! this . data ) return ;
if ( this . activeTab === 'circuit' ) {
this . circuitEditor ? . setCircuitText ( this . data . initial_circuit || this . data . initial_code ) ;
Object . assign ( this . circuitOut , this . freshOutput ( ) ) ;
} else if ( this . activeTab === 'velxio' ) {
localStorage . removeItem ( this . arduinoCodeKey ) ;
localStorage . removeItem ( this . arduinoCircuitKey ) ;
if ( this . data . initial_code_arduino ) {
this . velxioBridge ? . loadCode ( [ { name : 'sketch.ino' , content : this.data.initial_code_arduino } ] ) ;
}
if ( this . data . velxio_circuit ) {
this . velxioBridge ? . loadCircuit ( this . data . velxio_circuit ) ;
}
Object . assign ( this . velxioOut , this . freshOutput ( ) ) ;
} else if ( this . activeTab === 'flowchart' ) {
localStorage . removeItem ( this . flowchartStorageKey ) ;
if ( this . flowchartTab && typeof this . flowchartTab . handleLoad === 'function' ) {
this . flowchartTab . handleLoad ( true ) ;
}
} else {
const resetCode = this . currentLanguage === 'python'
? ( this . data . initial_python || '' )
: ( this . data . initial_code_c || this . data . initial_code || '' ) ;
this . currentCode = resetCode ;
if ( this . currentLanguage === 'c' ) this . cCode = resetCode ;
else this . pythonCode = resetCode ;
this . editor ? . setCode ( resetCode ) ;
const out = this . currentLanguage === 'python' ? this . pyOut : this.cOut ;
Object . assign ( out , this . freshOutput ( ) ) ;
}
}
handleShowSolution() {
if ( ! this . data ) return ;
if ( ! this . data . solution_code && ! this . data . solution_circuit && ! this . data . solution_python ) return ;
this . showSolution = ! this . showSolution ;
if ( this . showSolution ) {
if ( this . data . active_tabs ? . includes ( 'circuit' ) && this . data . solution_circuit ) {
this . circuitEditor ? . setCircuitText ( this . data . solution_circuit ) ;
}
if ( this . currentLanguage === 'python' && this . data . solution_python ) {
this . editor ? . setCode ( this . data . solution_python ) ;
} else if ( this . data . solution_code ) {
this . editor ? . setCode ( this . data . solution_code ) ;
}
} else {
if ( this . data . active_tabs ? . includes ( 'circuit' ) && this . data . initial_circuit ) {
this . circuitEditor ? . setCircuitText ( this . data . initial_circuit ) ;
}
if ( this . currentLanguage === 'python' || this . currentLanguage === 'c' || this . data . initial_code ) {
this . editor ? . setCode ( this . currentCode ) ;
}
}
}
setupVelxioBridge ( iframe : HTMLIFrameElement ) {
this . velxioIframe = iframe ;
initVelxioBridge (
iframe ,
this . data ,
this . arduinoCircuitKey ,
this . arduinoCodeKey ,
( bridge ) = > {
this . velxioBridge = bridge ;
this . velxioReady = true ;
} ,
( ) = > this . handleVelxioSubmit ( )
) ;
}
async handleVelxioSubmit() {
if ( ! this . data ) return ;
Object . assign ( this . velxioOut , { loading : true , output : 'Mengevaluasi...' , error : '' , success : null } ) ;
this . activeTab = 'output' ;
try {
let sourceCode = '' ;
let serialLog = '' ;
let wireList : any [ ] = [ ] ;
const dbg : string [ ] = [ ] ;
if ( this . velxioBridge ) {
const serResp = await this . velxioBridge [ 'request' ] ( 'elemes:get_serial_log' , 'velxio:serial_log' ) ;
if ( serResp ) serialLog = serResp . log as string ;
}
const state = getVelxioState ( this . velxioIframe ) ;
if ( state ) {
sourceCode = state . code ;
try {
const circuit = JSON . parse ( state . circuit ) ;
wireList = circuit . wires || [ ] ;
} catch { }
dbg . push ( '[metode: Zustand store]' ) ;
} else {
dbg . push ( '[!] Gagal mengakses simulator state' ) ;
}
const evalRes = evaluateVelxioSubmission ( sourceCode , serialLog , wireList , this . data ) ;
this . velxioOut . output = evalRes . messages . join ( '\n' ) ;
this . velxioOut . debug = dbg . concat ( evalRes . dbg ) ;
this . velxioOut . success = evalRes . pass ;
if ( evalRes . pass ) {
await this . completeLesson ( ) ;
setTimeout ( ( ) = > { this . showCelebration = false ; this . activeTab = 'velxio' ; } , 3000 ) ;
}
} catch ( err : any ) {
Object . assign ( this . velxioOut , { error : ` Evaluasi gagal: ${ err . message } ` , success : false } ) ;
} finally {
this . velxioOut . loading = false ;
}
}
getLessonTitle ( slug : string ) {
const lesson = this . data ? . ordered_lessons ? . find ( l = > l . filename . replace ( '.md' , '' ) === slug ) ;
return lesson ? . title || slug . replace ( /_/g , ' ' ) . toUpperCase ( ) ;
}
handleTryCode ( code : string , lang : string , float : any ) {
if ( lang === 'python' || lang === 'c' ) {
this . currentLanguage = lang ;
this . currentCode = code ;
if ( lang === 'c' ) this . cCode = code ;
else this . pythonCode = code ;
this . editor ? . setCode ( code ) ;
this . activeTab = 'editor' ;
if ( ! this . isMobile ) {
float . floating = true ;
} else {
this . mobileMode = 'half' ;
tick ( ) . then ( ( ) = > {
document . querySelector ( '.editor-area' ) ? . scrollIntoView ( { behavior : 'smooth' } ) ;
} ) ;
}
} else if ( lang === 'cpp' ) {
if ( this . isVelxio && this . velxioBridge ) {
this . velxioBridge . loadCode ( [ { name : 'sketch.ino' , content : code } ] ) ;
this . activeTab = 'velxio' ;
if ( ! this . isMobile ) {
float . floating = true ;
} else {
this . mobileMode = 'half' ;
tick ( ) . then ( ( ) = > {
document . querySelector ( '.editor-area' ) ? . scrollIntoView ( { behavior : 'smooth' } ) ;
} ) ;
}
}
}
}
}