Skip to content

Commit 9ee091c

Browse files
committed
Actualización de la terminal para hacerla interactiva y en tiempo real
1 parent 0e02690 commit 9ee091c

3 files changed

Lines changed: 161 additions & 100 deletions

File tree

core/executor.py

Lines changed: 88 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,19 @@
55
"""
66

77
import io
8+
import sys
9+
import eel
810
import traceback
911
import threading
1012
import queue
1113
import importlib
1214
from contextlib import redirect_stdout
1315
from typing import Dict, Any
1416

17+
18+
input_queue = queue.Queue()
19+
is_waiting_for_user = False
20+
1521
# -------------------------------------
1622
# CONFIGURACIÓN DE SEGURIDAD
1723

@@ -43,6 +49,18 @@ def safe_import(name, globals=None, locals=None, fromlist=(), level=0):
4349
# ---------------------------------------------
4450
# EJECUTOR PRINCIPAL
4551

52+
class RealTimeStdout:
53+
"""Intercepta los prints y los envía a la interfaz de React al instante."""
54+
def write(self, text):
55+
# Filtramos los saltos de línea vacíos que hace print por defecto
56+
if text and text != '\n':
57+
try:
58+
eel.api_realtime_log(str(text))()
59+
except Exception:
60+
pass
61+
def flush(self):
62+
pass
63+
4664
def sanitize_code(code: str) -> str:
4765
"""Evita palabras clave peligrosas antes de la ejecución."""
4866
forbidden = [
@@ -58,114 +76,100 @@ def sanitize_code(code: str) -> str:
5876
return code
5977

6078
def execute_user_code(code: str, timeout: int = 5) -> Dict[str, Any]:
61-
"""
62-
Ejecuta el código de usuario en un hilo separado con stdout capturado.
63-
64-
Args:
65-
code (str): Código Python a ejecutar.
66-
timeout (int): Tiempo máximo de ejecución en segundos.
67-
68-
Returns:
69-
dict: {'success': bool, 'output': str, 'error': str}
70-
"""
79+
global is_waiting_for_user
7180
result = {'success': False, 'output': '', 'error': ''}
7281

82+
# Limpiar cualquier residuo de inputs de ejecuciones pasadas
83+
while not input_queue.empty():
84+
input_queue.get_nowait()
85+
7386
def target(q):
74-
buffer = io.StringIO()
87+
global is_waiting_for_user
7588
try:
76-
# Saneamiento básico
7789
safe_code = sanitize_code(code)
78-
79-
def dummy_input(prompt=""):
80-
# Imprime el mensaje del estudiante de forma natural y limpia
81-
if prompt:
82-
print(prompt)
83-
# Retornamos "" silenciosamente para que la ejecución no se detenga
84-
# (Útil por si luego conectan esto a un nodo de matemáticas)
85-
return ""
8690

87-
# Configuración del entorno restringido (Sandbox)
88-
# Definimos qué funciones 'built-in' puede ver el estudiante
89-
safe_builtins_map = {
90-
# Básicos
91-
'print': print,
92-
'input': dummy_input,
93-
'range': range,
94-
'len': len,
95-
'int': int,
96-
'float': float,
97-
'str': str,
98-
'list': list,
99-
'dict': dict,
100-
'set': set,
101-
'tuple': tuple,
102-
'bool': bool,
91+
def interactive_input(prompt=""):
92+
global is_waiting_for_user
93+
if prompt:
94+
try: eel.api_realtime_log(str(prompt))()
95+
except: pass
10396

104-
# Matemáticas y Utilidades
105-
'abs': abs,
106-
'min': min,
107-
'max': max,
108-
'sum': sum,
109-
'round': round,
110-
'zip': zip,
111-
'map': map,
112-
'filter': filter,
113-
'sorted': sorted,
114-
'enumerate': enumerate,
97+
# Avisar a React que habilite la caja de texto
98+
try: eel.trigger_frontend_input()()
99+
except: pass
115100

116-
# Excepciones
117-
'Exception': Exception,
118-
'ValueError': ValueError,
119-
'TypeError': TypeError,
101+
# Bloquear el hilo de Python hasta que React responda
102+
is_waiting_for_user = True
103+
user_response = input_queue.get()
104+
is_waiting_for_user = False
120105

121-
# Soporte para Clases y POO
122-
'__build_class__': __build_class__,
123-
'object': object, # Necesario para herencia base
124-
'super': super, # Necesario para llamar al padre
125-
'classmethod': classmethod, # Decorador útil para enseñar POO
126-
'staticmethod': staticmethod, # Decorador útil para enseñar POO
127-
'property': property, # Getters/Setters pythonicos
128-
'type': type, # Para introspección de tipos
129-
'isinstance': isinstance, # Validación de tipos
106+
# Hacer eco de la respuesta en la terminal visual
107+
try: eel.api_realtime_log(f"❯ {user_response}")()
108+
except: pass
130109

131-
# Sistema
110+
return str(user_response)
111+
112+
safe_builtins_map = {
113+
'print': print,
114+
'input': interactive_input, # <- Nueva función interactiva
115+
'range': range, 'len': len, 'int': int, 'float': float, 'str': str,
116+
'list': list, 'dict': dict, 'set': set, 'tuple': tuple, 'bool': bool,
117+
'abs': abs, 'min': min, 'max': max, 'sum': sum, 'round': round,
118+
'zip': zip, 'map': map, 'filter': filter, 'sorted': sorted, 'enumerate': enumerate,
119+
'Exception': Exception, 'ValueError': ValueError, 'TypeError': TypeError,
120+
'__build_class__': __build_class__, # Vital para los nodos Class
121+
'object': object,
122+
'super': super,
123+
'classmethod': classmethod,
124+
'staticmethod': staticmethod,
125+
'property': property,
126+
'type': type,
127+
'isinstance': isinstance,
132128
'__import__': safe_import,
133-
'__name__': '__main__' # Evita errores en algunos contextos de ejecución
129+
'__name__': '__main__'
134130
}
135131

136-
# Entorno global inicial
137132
env = {'__builtins__': safe_builtins_map}
138133

139-
# Ejecución
140-
with redirect_stdout(buffer):
134+
# Aplicar la redirección de consola en tiempo real
135+
original_stdout = sys.stdout
136+
sys.stdout = RealTimeStdout()
137+
138+
try:
141139
exec(safe_code, env)
140+
q.put({'success': True, 'output': '', 'error': ''})
141+
finally:
142+
sys.stdout = original_stdout # Restaurar stdout por seguridad
142143

143-
q.put({'success': True, 'output': buffer.getvalue(), 'error': ''})
144-
144+
#Captura específica de errores
145145
except SyntaxError as se:
146-
q.put({'success': False, 'output': buffer.getvalue(), 'error': f"Error de Sintaxis: {se}"})
146+
q.put({'success': False, 'output': '', 'error': f"Error de Sintaxis: {se}"})
147147
except ImportError as ie:
148-
q.put({'success': False, 'output': buffer.getvalue(), 'error': f"Error de Importación: {ie}"})
148+
q.put({'success': False, 'output': '', 'error': f"Error de Importación: {ie}"})
149149
except Exception:
150-
q.put({'success': False, 'output': buffer.getvalue(), 'error': traceback.format_exc()})
150+
q.put({'success': False, 'output': '', 'error': traceback.format_exc()})
151151

152-
# Gestión de Hilos (Threading) para evitar bloqueos infinitos (while True)
152+
# Ejecución en hilo secundario
153153
q = queue.Queue()
154154
thread = threading.Thread(target=target, args=(q,))
155155
thread.start()
156156

157-
try:
158-
thread.join(timeout)
159-
if thread.is_alive():
160-
result['error'] = "Tiempo de ejecución excedido (Timeout). ¿Tienes un bucle infinito?"
161-
# Nota: Python threads no se pueden matar forzosamente de forma segura,
162-
# pero el frontend dejará de esperar.
157+
# TIMEOUT INTELIGENTE
158+
time_elapsed = 0.0
159+
while thread.is_alive() and time_elapsed < timeout:
160+
# eel.sleep cede el control temporalmente a los WebSockets
161+
# permitiendo que la interfaz reaccione en tiempo real sin bloquear el hilo principal.
162+
eel.sleep(0.1)
163+
164+
if not is_waiting_for_user:
165+
time_elapsed += 0.1
166+
167+
if thread.is_alive():
168+
result['error'] = "Tiempo de ejecución excedido (Timeout). ¿Tienes un bucle infinito?"
169+
else:
170+
if not q.empty():
171+
result = q.get()
163172
else:
164-
if not q.empty():
165-
result = q.get()
166-
else:
167-
result['error'] = "Error desconocido: No se recibió respuesta del hilo."
168-
except Exception as e:
169-
result['error'] = str(e)
170-
173+
result['error'] = "Error desconocido: No se recibió respuesta del hilo."
174+
171175
return result

main.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from core.ml_struct_rules import block_to_struct as ml_block_to_struct
2424
from storage import ml_exporter
2525
from storage import file_handler
26-
from core.executor import execute_user_code
26+
from core.executor import execute_user_code, input_queue
2727
from core import ml_manager
2828
from estimators import memory_estimator
2929
# import maker_edu.auth
@@ -638,6 +638,11 @@ def api_get_grades():
638638
except Exception as e:
639639
return {'success': False, 'error': str(e)}
640640

641+
@eel.expose
642+
def api_provide_input(user_text):
643+
"""Recibe la respuesta de la terminal visual y desbloquea el hilo de Python"""
644+
input_queue.put(user_text)
645+
641646
# --------------------------------------------
642647
# PUNTO DE ENTRADA PRINCIPAL
643648

web/index.html

Lines changed: 67 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -382,17 +382,33 @@ <h2 className="text-white font-bold flex items-center gap-2">
382382
);
383383
};
384384

385-
const TerminalLog = ({ logs }) => {
385+
const TerminalLog = ({ logs, isWaitingInput, terminalInput, setTerminalInput, handleTerminalSubmit }) => {
386386
const endRef = useRef(null);
387-
useEffect(() => endRef.current?.scrollIntoView({ behavior: "smooth" }), [logs]);
387+
useEffect(() => endRef.current?.scrollIntoView({ behavior: "smooth" }), [logs, isWaitingInput]);
388388
return (
389-
<div className="vscode-terminal p-3 h-full overflow-y-auto text-sm">
390-
{logs.map((log, i) => {
391-
let cl = 'text-gray-400';
392-
if (log.type === 'error') cl = 'text-red-400'; else if (log.type === 'warn') cl = 'text-yellow-400'; else if (log.type === 'success') cl = 'text-green-400'; else if (log.type === 'output') cl = 'text-white';
393-
return <div key={i} className={`whitespace-pre-wrap break-words mb-0.5 ${cl}`}>{log.type !== 'output' && <span className="opacity-50 text-xs mr-2 select-none">[{log.time}]</span>}<span>{log.msg}</span></div>;
394-
})}
395-
<div ref={endRef} />
389+
<div className="vscode-terminal p-3 h-full overflow-y-auto text-sm flex flex-col">
390+
<div className="flex-1">
391+
{logs.map((log, i) => {
392+
let cl = 'text-gray-400';
393+
if (log.type === 'error') cl = 'text-red-400'; else if (log.type === 'warn') cl = 'text-yellow-400'; else if (log.type === 'success') cl = 'text-green-400'; else if (log.type === 'output') cl = 'text-white';
394+
return <div key={i} className={`whitespace-pre-wrap break-words mb-0.5 ${cl}`}>{log.type !== 'output' && <span className="opacity-50 text-xs mr-2 select-none">[{log.time}]</span>}<span>{log.msg}</span></div>;
395+
})}
396+
{isWaitingInput && (
397+
<div className="flex items-center mt-1 animate-fade-in">
398+
<span className="text-blue-400 font-bold mr-2 animate-pulse"></span>
399+
<input
400+
autoFocus
401+
type="text"
402+
className="flex-1 bg-transparent border-none outline-none text-yellow-300 font-mono"
403+
value={terminalInput}
404+
onChange={e => setTerminalInput(e.target.value)}
405+
onKeyDown={handleTerminalSubmit}
406+
placeholder="Escribe tu respuesta y presiona Enter..."
407+
/>
408+
</div>
409+
)}
410+
<div ref={endRef} />
411+
</div>
396412
</div>
397413
);
398414
};
@@ -913,6 +929,8 @@ <h3 className="text-red-400 font-bold uppercase text-xs mb-3 flex items-center g
913929
const [logs, setLogs] = useState([{ time: new Date().toLocaleTimeString(), msg: `IDE Python Listo`, type: 'info' }]);
914930
const [activeTab, setActiveTab] = useState('forest');
915931
const [pythonCode, setPythonCode] = useState("# El código generado aparecerá aquí...");
932+
const [isWaitingInput, setIsWaitingInput] = useState(false);
933+
const [terminalInput, setTerminalInput] = useState('');
916934
const codeRef = useRef(null);
917935

918936
const reactFlowWrapper = useRef(null);
@@ -924,6 +942,30 @@ <h3 className="text-red-400 font-bold uppercase text-xs mb-3 flex items-center g
924942
const [showSettings, setShowSettings] = useState(false);
925943
const [showTeacherDashboard, setShowTeacherDashboard] = useState(false);
926944

945+
946+
useEffect(() => {
947+
// Escuchando los prints de Python en tiempo real
948+
window.eel.expose(api_realtime_log, 'api_realtime_log');
949+
function api_realtime_log(text) {
950+
setLogs(p => [...p, { time: new Date().toLocaleTimeString(), msg: text, type: 'output' }]);
951+
}
952+
953+
// Escuchando la solicitud de Input de Python
954+
window.eel.expose(trigger_frontend_input, 'trigger_frontend_input');
955+
function trigger_frontend_input() {
956+
setIsWaitingInput(true);
957+
}
958+
}, []);
959+
960+
const handleTerminalSubmit = async (e) => {
961+
if (e.key === 'Enter') {
962+
const val = terminalInput;
963+
setTerminalInput('');
964+
setIsWaitingInput(false);
965+
await window.eel.api_provide_input(val)();
966+
}
967+
};
968+
927969
useEffect(() => {
928970
const root = document.documentElement;
929971
root.style.setProperty('--editor-font-size', `${prefs.editorFontSize}px`);
@@ -1012,20 +1054,18 @@ <h3 className="text-red-400 font-bold uppercase text-xs mb-3 flex items-center g
10121054
};
10131055

10141056
const runPipeline = async () => {
1015-
addLog("Ejecutando...", 'warn');
1057+
setLogs([{ time: new Date().toLocaleTimeString(), msg: "Iniciando Sandbox...", type: 'warn' }]);
10161058
setEdges(eds => eds.map(e => ({ ...e, data: { status: 'active' } })));
10171059
const blocks = serializeNodesLinear(nodes, edges);
10181060
try {
10191061
if (window.eel) {
10201062
const res = await window.eel.api_execute(blocks)();
10211063
if (res.success) {
1022-
addLog("Finalizado.", 'success');
1023-
if(res.output) addLog(res.output, 'output');
1024-
setTimeout(() => { setEdges(eds => eds.map(e => ({ ...e, data: { status: 'idle' } }))); }, 500);
1064+
addLog("Ejecución finalizada.", 'success');
10251065
} else {
10261066
addLog(`Error: ${res.error}`, 'error');
1027-
setEdges(eds => eds.map(e => ({ ...e, data: { status: 'error' } })));
10281067
}
1068+
setEdges(eds => eds.map(e => ({ ...e, data: { status: 'idle' } })));
10291069
}
10301070
} catch (e) {
10311071
addLog(`Error: ${e}`, 'error');
@@ -1145,7 +1185,19 @@ <h3 className="text-red-400 font-bold uppercase text-xs mb-3 flex items-center g
11451185
}
11461186
</div>
11471187
</div>
1148-
<div className="h-40 border-t border-gray-700 flex flex-col bg-black shrink-0"><div className="bg-gray-800 text-[10px] uppercase font-bold tracking-wider px-3 py-1 text-gray-400 border-b border-gray-700 flex justify-between"><span>Terminal</span><button onClick={()=>setLogs([])} className="hover:text-white">Clear</button></div><TerminalLog logs={logs} /></div>
1188+
<div className="h-40 border-t border-gray-700 flex flex-col bg-black shrink-0">
1189+
<div className="bg-gray-800 text-[10px] uppercase font-bold tracking-wider px-3 py-1 text-gray-400 border-b border-gray-700 flex justify-between">
1190+
<span>Terminal Interactiva</span>
1191+
<button onClick={()=>setLogs([])} className="hover:text-white">Clear</button>
1192+
</div>
1193+
<TerminalLog
1194+
logs={logs}
1195+
isWaitingInput={isWaitingInput}
1196+
terminalInput={terminalInput}
1197+
setTerminalInput={setTerminalInput}
1198+
handleTerminalSubmit={handleTerminalSubmit}
1199+
/>
1200+
</div>
11491201
<SettingsModal isOpen={showSettings} onClose={() => setShowSettings(false)} prefs={prefs} setPrefs={setPrefs}/>
11501202
{showTeacherDashboard && <TeacherDashboard onClose={() => setShowTeacherDashboard(false)} />}
11511203
<CustomModal {...modalConfig} />

0 commit comments

Comments
 (0)