From f589e22e4e8d9e06b488aeb7b9bd6763b042fb68 Mon Sep 17 00:00:00 2001 From: leart-ramushi <113895250+leart-ramushi@users.noreply.github.com> Date: Tue, 26 May 2026 16:36:09 +0200 Subject: [PATCH] debug_logger and execution update --- src/backend/debug_logger.py | 74 +++++++++++++++++++++++++++--- src/backend/execution_engine.py | 78 +++++++++++++++++++++++++++----- src/main.py | 79 ++++++++++++++++++++++++--------- 3 files changed, 192 insertions(+), 39 deletions(-) diff --git a/src/backend/debug_logger.py b/src/backend/debug_logger.py index d45e9a2..00be55c 100644 --- a/src/backend/debug_logger.py +++ b/src/backend/debug_logger.py @@ -1,12 +1,72 @@ +import ast +import traceback +from datetime import datetime + + class DebugLogger: - """ - Platzhalter für das Logging [1] - """ def __init__(self): self.logs = [] - def log(self, message): - self.logs.append(message) + # ---------------- INTERNAL ---------------- + def _entry(self, level: str, message: str) -> dict: + entry = { + "level": level, + "message": message, + "time": datetime.now().strftime("%H:%M:%S"), + } + self.logs.append(entry) + return entry - def get_logs(self): - return self.logs \ No newline at end of file + # ---------------- PUBLIC API ---------------- + def log(self, message: str): + self._entry("INFO", message) + + def log_stdout(self, output: str): + if output.strip(): + self._entry("OUTPUT", output.rstrip()) + + def log_stderr(self, error: str): + if error.strip(): + self._entry("ERROR", error.rstrip()) + + def log_syntax_error(self, filepath: str, error: SyntaxError): + msg = f"SyntaxError in {filepath} (line {error.lineno}): {error.msg}" + self._entry("SYNTAX", msg) + + def log_result(self, returncode: int): + level = "SUCCESS" if returncode == 0 else "FAIL" + msg = f"Process exited with code {returncode}" + self._entry(level, msg) + + def log_exception(self, exc: Exception): + msg = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__)) + self._entry("EXCEPTION", msg.rstrip()) + + # ---------------- ACCESSORS ---------------- + def get_logs(self) -> list: + return self.logs + + def get_logs_by_level(self, level: str) -> list: + return [e for e in self.logs if e["level"] == level.upper()] + + def has_errors(self) -> bool: + return any(e["level"] in ("ERROR", "SYNTAX", "EXCEPTION", "FAIL") for e in self.logs) + + def clear(self): + self.logs = [] + + def format(self) -> str: + lines = [] + icons = { + "INFO": "ℹ", + "OUTPUT": "▶", + "ERROR": "✖", + "SYNTAX": "⚠", + "EXCEPTION": "💥", + "SUCCESS": "✔", + "FAIL": "✖", + } + for e in self.logs: + icon = icons.get(e["level"], "•") + lines.append(f"[{e['time']}] {icon} {e['message']}") + return "\n".join(lines) \ No newline at end of file diff --git a/src/backend/execution_engine.py b/src/backend/execution_engine.py index 9ab6723..2bc891c 100644 --- a/src/backend/execution_engine.py +++ b/src/backend/execution_engine.py @@ -1,25 +1,62 @@ +import ast import subprocess import sys from pathlib import Path +from backend.debug_logger import DebugLogger + class ExecutionEngine: + def __init__(self): + self.logger = DebugLogger() + + # ---------------- SYNTAX CHECK ---------------- + def check_syntax(self, filepath: str) -> bool: + """Parse the file with ast. Returns True if valid, False if not.""" + path = Path(filepath) + if path.suffix.lower() != ".py": + return True # skip non-Python files + + try: + source = path.read_text(encoding="utf-8") + ast.parse(source, filename=str(path)) + self.logger.log(f"Syntax OK: {path.name}") + return True + except SyntaxError as e: + self.logger.log_syntax_error(str(path), e) + return False + except Exception as e: + self.logger.log_exception(e) + return False + + # ---------------- RUN ---------------- def run_file(self, filepath: str) -> dict: + self.logger.clear() path = Path(filepath) + # --- guards --- if not path.exists(): - return {"stdout": "", "stderr": f"File not found: {filepath}", "returncode": -1} + self.logger.log_stderr(f"File not found: {filepath}") + return self._result("", f"File not found: {filepath}", -1) ext = path.suffix.lower() if ext == ".py": + # syntax check before running + if not self.check_syntax(filepath): + err = self.logger.format() + return self._result("", err, -1) cmd = [sys.executable, str(path)] elif ext == ".js": cmd = ["node", str(path)] elif ext == ".sh": cmd = ["bash", str(path)] else: - return {"stdout": "", "stderr": f"Unsupported file type: {ext}", "returncode": -1} + msg = f"Unsupported file type: {ext}" + self.logger.log_stderr(msg) + return self._result("", msg, -1) + # --- execute --- + self.logger.log(f"Running: {path.name}") try: result = subprocess.run( cmd, @@ -28,14 +65,35 @@ class ExecutionEngine: timeout=30, cwd=str(path.parent), ) - return { - "stdout": result.stdout, - "stderr": result.stderr, - "returncode": result.returncode, - } + self.logger.log_stdout(result.stdout) + self.logger.log_stderr(result.stderr) + self.logger.log_result(result.returncode) + + return self._result(result.stdout, result.stderr, result.returncode) + except subprocess.TimeoutExpired: - return {"stdout": "", "stderr": "Timed out after 30 seconds.", "returncode": -1} + msg = "Timed out after 30 seconds." + self.logger.log_stderr(msg) + return self._result("", msg, -1) except FileNotFoundError as e: - return {"stdout": "", "stderr": f"Runtime not found: {e}", "returncode": -1} + msg = f"Runtime not found: {e}" + self.logger.log_stderr(msg) + return self._result("", msg, -1) except Exception as e: - return {"stdout": "", "stderr": f"Error: {e}", "returncode": -1} \ No newline at end of file + self.logger.log_exception(e) + return self._result("", str(e), -1) + + # ---------------- HELPERS ---------------- + def _result(self, stdout: str, stderr: str, returncode: int) -> dict: + return { + "stdout": stdout, + "stderr": stderr, + "returncode": returncode, + "success": returncode == 0, + "logs": self.logger.get_logs(), + "log_text": self.logger.format(), + "has_errors": self.logger.has_errors(), + } + + def get_logger(self) -> DebugLogger: + return self.logger \ No newline at end of file diff --git a/src/main.py b/src/main.py index bcdfb63..65eb1bf 100644 --- a/src/main.py +++ b/src/main.py @@ -19,7 +19,7 @@ from backend.execution_engine import ExecutionEngine st.set_page_config( page_title="AI Code Editor", page_icon="💻", - layout="wide", + layout="wide" ) # ---------------- FOLDER PICKER ---------------- @@ -38,7 +38,6 @@ def render_sidebar(): root_folder = st.session_state.get("root_folder") with st.sidebar: - # --- Button on top --- if st.button("📂 Choose Folder", key="pick_folder"): picked = pick_folder() if picked: @@ -50,7 +49,6 @@ def render_sidebar(): st.markdown("---") - # --- Tree below --- if root_folder: try: root_path = Path(root_folder) @@ -73,6 +71,58 @@ def render_sidebar(): return None +# ---------------- OUTPUT PANEL ---------------- +def render_output(result: dict): + """Render structured execution output from ExecutionEngine.""" + success = result["returncode"] == 0 + status_icon = "✅" if success else "❌" + status_text = f"Exited {result['returncode']}" + + st.markdown(f"**{status_icon} {status_text}**") + + # stdout + if result["stdout"].strip(): + st.caption("stdout") + st.code(result["stdout"].rstrip(), language="text") + + # stderr + if result["stderr"].strip(): + st.caption("stderr") + st.code(result["stderr"].rstrip(), language="text") + + # debug log entries (collapsed by default) + if result.get("logs"): + with st.expander("🪲 Debug log", expanded=False): + level_colors = { + "INFO": "#6c7086", + "OUTPUT": "#a6e3a1", + "ERROR": "#f38ba8", + "SYNTAX": "#fab387", + "EXCEPTION": "#f38ba8", + "SUCCESS": "#a6e3a1", + "FAIL": "#f38ba8", + } + icons = { + "INFO": "ℹ", "OUTPUT": "▶", "ERROR": "✖", + "SYNTAX": "⚠", "EXCEPTION": "💥", "SUCCESS": "✔", "FAIL": "✖", + } + lines = [] + for e in result["logs"]: + color = level_colors.get(e["level"], "#cdd6f4") + icon = icons.get(e["level"], "•") + lines.append( + f'[{e["time"]}] ' + f'{icon} {e["level"]} ' + f'{e["message"]}' + ) + st.markdown( + "
".join(lines), + unsafe_allow_html=True + ) + + if not result["stdout"].strip() and not result["stderr"].strip(): + st.caption("No output.") + # ---------------- MAIN ---------------- def main(): file_manager = FileManager() @@ -103,7 +153,7 @@ def main(): # ---------------- FILE SWITCH ---------------- if current_file != st.session_state.active_file: st.session_state.active_file = current_file - st.session_state.run_result = None # clear output on file switch + st.session_state.run_result = None if current_file: content = file_manager.read_file(current_file) st.session_state.editor_content = content @@ -119,7 +169,6 @@ def main(): col1, col2 = st.columns([3, 2]) with col1: - # --- File name + Run button on same row --- title_col, btn_col = st.columns([5, 1]) with title_col: st.subheader(f"✏️ {Path(st.session_state.active_file).name}") @@ -138,7 +187,6 @@ def main(): file_manager.save_file(st.session_state.active_file, new_value) st.success("Saved ✔") - # --- Run execution --- if run_clicked: with st.spinner("Running…"): st.session_state.run_result = execution_engine.run_file( @@ -149,23 +197,10 @@ def main(): chat = ChatInterface() chat.display_chat(chat_manager) - # --- Output panel --- - result = st.session_state.run_result - if result is not None: + if st.session_state.run_result is not None: st.markdown("---") - success = result["returncode"] == 0 - - status = "✅ Exited 0" if success else f"❌ Exited {result['returncode']}" - st.caption(f"Output — {status}") - - if result["stdout"]: - st.code(result["stdout"], language="text") - - if result["stderr"]: - st.code(result["stderr"], language="text") - - if not result["stdout"] and not result["stderr"]: - st.caption("No output.") + st.caption("Output") + render_output(st.session_state.run_result) else: st.info("Select a file from the explorer")