1- import React , { useState , useEffect } from 'react' ;
1+ import React , { useState , useEffect , useRef } from 'react' ;
22import CodeMirror from '@uiw/react-codemirror' ;
33import { python } from '@codemirror/lang-python' ;
44import { oneDark } from '@codemirror/theme-one-dark' ;
@@ -7,23 +7,64 @@ import styles from './styles.module.css';
77export default function InteractivePython ( { children } ) {
88 const initialCode = children . props . children . trim ( ) ;
99 const [ code , setCode ] = useState ( initialCode ) ;
10- const [ output , setOutput ] = useState ( "" ) ;
1110 const [ isSkulptReady , setIsSkulptReady ] = useState ( false ) ;
1211 const [ isRunning , setIsRunning ] = useState ( false ) ;
1312
13+ const outputRef = useRef ( null ) ;
14+ const inputContainerRef = useRef ( null ) ;
15+ const inputPromptRef = useRef ( null ) ;
16+ const inputFieldRef = useRef ( null ) ;
17+ const resolveInputRef = useRef ( null ) ;
18+
19+ const appendOutput = ( text ) => {
20+ if ( outputRef . current ) outputRef . current . textContent += text ;
21+ } ;
22+
23+ const clearOutput = ( ) => {
24+ if ( outputRef . current ) outputRef . current . textContent = '' ;
25+ } ;
26+
27+ const showInput = ( prompt ) => {
28+ if ( ! inputContainerRef . current ) return ;
29+ inputPromptRef . current . textContent = prompt ;
30+ inputFieldRef . current . value = '' ;
31+ inputContainerRef . current . style . display = 'flex' ;
32+ inputFieldRef . current . focus ( ) ;
33+ } ;
34+
35+ const hideInput = ( ) => {
36+ if ( ! inputContainerRef . current ) return ;
37+ inputContainerRef . current . style . display = 'none' ;
38+ } ;
39+
40+ const submitInput = ( ) => {
41+ const value = inputFieldRef . current . value ;
42+ const prompt = inputPromptRef . current . textContent ;
43+ hideInput ( ) ;
44+ appendOutput ( prompt + value + "\n" ) ;
45+ if ( resolveInputRef . current ) {
46+ const resolve = resolveInputRef . current ;
47+ resolveInputRef . current = null ;
48+ resolve ( value ) ;
49+ }
50+ } ;
51+
52+ useEffect ( ( ) => {
53+ if ( inputContainerRef . current ) {
54+ inputContainerRef . current . style . display = 'none' ;
55+ }
56+ } , [ ] ) ;
57+
1458 useEffect ( ( ) => {
15- const loadScript = ( src ) => {
16- return new Promise ( ( resolve , reject ) => {
17- const script = document . createElement ( 'script' ) ;
18- script . src = src ;
19- script . async = true ;
20- script . onload = resolve ;
21- script . onerror = reject ;
22- document . body . appendChild ( script ) ;
23- } ) ;
24- } ;
25-
26- // Sequential loading to prevent "Sk is not defined"
59+ const loadScript = ( src ) => new Promise ( ( resolve , reject ) => {
60+ const script = document . createElement ( 'script' ) ;
61+ script . src = src ;
62+ script . async = true ;
63+ script . onload = resolve ;
64+ script . onerror = reject ;
65+ document . body . appendChild ( script ) ;
66+ } ) ;
67+
2768 async function initSkulpt ( ) {
2869 try {
2970 if ( ! window . Sk ) {
@@ -35,81 +76,93 @@ export default function InteractivePython({ children }) {
3576 console . error ( "Failed to load Skulpt scripts" , err ) ;
3677 }
3778 }
38-
3979 initSkulpt ( ) ;
4080 } , [ ] ) ;
4181
4282 const runCode = ( ) => {
43- if ( ! isSkulptReady || ! window . Sk ) {
44- setOutput ( "Engine is still loading..." ) ;
45- return ;
46- }
83+ if ( ! isSkulptReady || ! window . Sk ) return ;
4784
48- setOutput ( "" ) ;
85+ clearOutput ( ) ;
86+ hideInput ( ) ;
4987 setIsRunning ( true ) ;
88+ resolveInputRef . current = null ;
5089
5190 window . Sk . configure ( {
52- output : ( text ) => setOutput ( ( prev ) => prev + text ) ,
53- read : ( x ) => {
91+ output : ( text ) => appendOutput ( text ) ,
92+ read : ( x ) => {
5493 if ( window . Sk . builtinFiles === undefined || window . Sk . builtinFiles [ "files" ] [ x ] === undefined )
55- throw "File not found: '" + x + "'" ;
94+ throw "File not found: '" + x + "'" ;
5695 return window . Sk . builtinFiles [ "files" ] [ x ] ;
57- } ,
58- // Adding yieldLimit helps prevent the browser from freezing on infinite loops
59- yieldLimit : 100 ,
60- execLimit : 5000 , // 5 second timeout safety
61- inputfun : ( prompt ) => window . prompt ( prompt ) ,
62- inputfunTakesPrompt : true ,
96+ } ,
97+ inputfunTakesPrompt : true ,
98+ inputfun : ( prompt ) => {
99+ return new Promise ( ( resolve ) => {
100+ resolveInputRef . current = resolve ;
101+ showInput ( prompt ) ;
102+ } ) ;
103+ } ,
104+ __future__ : window . Sk . python3
63105 } ) ;
64106
65- try {
66- // We execute the code
67- const result = window . Sk . importMainWithBody ( "<stdin>" , false , code , true ) ;
68-
69- // Skulpt returns a 'suspension' or a promise-like object.
70- // We use Promise.resolve to normalize it so .then() always works.
71- Promise . resolve ( result ) . then (
72- ( ) => {
73- setIsRunning ( false ) ;
74- console . log ( "Execution complete." ) ;
75- } ,
76- ( err ) => {
77- setOutput ( ( prev ) => prev + "\n" + err . toString ( ) ) ;
78- setIsRunning ( false ) ;
79- }
80- ) ;
81- } catch ( e ) {
82- setOutput ( e . toString ( ) ) ;
107+ window . Sk . misceval . asyncToPromise ( ( ) =>
108+ window . Sk . importMainWithBody ( "<stdin>" , false , code , true )
109+ ) . then (
110+ ( ) => { setIsRunning ( false ) ; hideInput ( ) ; } ,
111+ ( err ) => {
112+ appendOutput ( "\n" + err . toString ( ) ) ;
83113 setIsRunning ( false ) ;
84- }
85- } ;
114+ hideInput ( ) ;
115+ }
116+ ) ;
117+ } ;
86118
87119 return (
88120 < div className = { styles . wrapper } >
121+ { /* ── Header ── */ }
89122 < div className = { styles . codeHeader } >
90- Python Ledger Editor { isSkulptReady ? "✅ Ready" : "⏳ Loading..." }
123+ < span className = { styles . codeHeaderText } > Python Ledger Editor</ span >
124+ < span className = { `${ styles . codeHeaderStatus } ${ isSkulptReady ? styles . ready : '' } ` } >
125+ { isSkulptReady ? '● READY' : '○ LOADING...' }
126+ </ span >
91127 </ div >
92-
128+
93129 < CodeMirror
94130 value = { code }
95131 theme = { oneDark }
96132 extensions = { [ python ( ) ] }
97133 onChange = { ( value ) => setCode ( value ) }
98134 />
99-
100- < button
101- className = { styles . runButton }
102- onClick = { runCode }
103- disabled = { ! isSkulptReady }
135+
136+ { /* ── Run Button ── */ }
137+ < button
138+ className = { styles . runButton }
139+ onClick = { runCode }
140+ disabled = { ! isSkulptReady || isRunning }
104141 >
105- { isSkulptReady ? "▶ Run Code" : "Loading Engine..." }
142+ { isRunning ? '⏳ Running...' : '▶ Execute Program' }
106143 </ button >
107144
108- { output && (
109- < div className = { styles . console } >
110- < pre className = { styles . output } > { output } </ pre >
145+ { /* ── Console ── */ }
146+ < div className = { styles . console } >
147+ < pre ref = { outputRef } className = { styles . output } />
148+ < div ref = { inputContainerRef } className = { styles . inputForm } >
149+ < span ref = { inputPromptRef } className = { styles . promptText } />
150+ < input
151+ ref = { inputFieldRef }
152+ type = "text"
153+ className = { styles . terminalInput }
154+ onKeyDown = { ( e ) => {
155+ if ( e . key === 'Enter' ) {
156+ e . preventDefault ( ) ;
157+ submitInput ( ) ;
158+ }
159+ } }
160+ />
161+ < button type = "button" className = { styles . submitInputBtn } onClick = { submitInput } >
162+ Enter ↵
163+ </ button >
111164 </ div >
112- ) }
165+ </ div >
113166 </ div >
114167 ) ;
115168}
0 commit comments