diff --git a/.streamlit/config.toml b/.streamlit/config.toml new file mode 100644 index 0000000..c369fe4 --- /dev/null +++ b/.streamlit/config.toml @@ -0,0 +1,6 @@ +[theme] +base = "light" +primaryColor = "#4A90D9" +backgroundColor = "#ffffff" +secondaryBackgroundColor = "#f5f5f5" +textColor = "#1a1a1a" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 120898d..497c1bc 100644 Binary files a/requirements.txt and b/requirements.txt differ 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 67cfd1b..9469d30 100644 --- a/src/backend/execution_engine.py +++ b/src/backend/execution_engine.py @@ -1,12 +1,198 @@ -class ExecutionEngine: - """ - Platzhalter für die Code-Ausführung [1] - """ - def __init__(self): - pass +import ast +import subprocess +import sys +from pathlib import Path - def execute_code(self): +from backend.debug_logger import DebugLogger + +# ---------------- BLOCKED PATTERNS ---------------- +# These AST node patterns are refused before execution +BLOCKED_IMPORTS = { + "winreg", "ctypes", "msvcrt", # Windows internals +} + +BLOCKED_CALLS = { + ("os", "system"), ("os", "popen"), + ("os", "remove"), ("os", "unlink"), + ("os", "rmdir"), ("os", "removedirs"), + ("shutil", "rmtree"), ("shutil", "move"), + ("subprocess", "call"), ("subprocess", "Popen"), + ("subprocess", "run"), +} + +BLOCKED_BUILTINS = {"__import__", "eval", "exec", "compile"} + +# Paths that are never allowed as cwd or in file arguments +BLOCKED_PATH_PREFIXES = [ + "C:\\Windows", + "C:\\System32", + "/etc", + "/bin", + "/sbin", + "/usr/bin", + "/usr/sbin", + "/boot", + "/sys", + "/proc", +] + + +class SafetyError(Exception): + pass + + +class ExecutionEngine: + def __init__(self): + self.logger = DebugLogger() + + # ---------------- SAFETY CHECK ---------------- + def _check_safe(self, filepath: str) -> None: """ - Führt Code nicht aus, sondern gibt einen Status aus [1] + Raises SafetyError if the file contains dangerous patterns. + Only runs on .py files (other types are blocked outright). """ - return "Code wurde nicht ausgeführt (Platzhalter)" \ No newline at end of file + path = Path(filepath) + source = path.read_text(encoding="utf-8") + try: + tree = ast.parse(source, filename=str(path)) + except SyntaxError: + return # syntax errors handled separately + + for node in ast.walk(tree): + + # Block dangerous imports + if isinstance(node, (ast.Import, ast.ImportFrom)): + names = ( + [a.name for a in node.names] + if isinstance(node, ast.Import) + else [node.module or ""] + ) + for name in names: + root = name.split(".")[0] + if root in BLOCKED_IMPORTS: + raise SafetyError(f"Blocked import: '{name}'") + + # Block dangerous attribute calls like os.remove(...) + if isinstance(node, ast.Call): + func = node.func + if isinstance(func, ast.Attribute): + if isinstance(func.value, ast.Name): + pair = (func.value.id, func.attr) + if pair in BLOCKED_CALLS: + raise SafetyError( + f"Blocked call: '{func.value.id}.{func.attr}()'" + ) + + # Block dangerous builtins: eval(), exec(), __import__() + if isinstance(func, ast.Name) and func.id in BLOCKED_BUILTINS: + raise SafetyError(f"Blocked builtin: '{func.id}()'") + + # Block absolute paths pointing at system directories in string literals + for node in ast.walk(tree): + if isinstance(node, ast.Constant) and isinstance(node.value, str): + for prefix in BLOCKED_PATH_PREFIXES: + if node.value.lower().startswith(prefix.lower()): + raise SafetyError( + f"Blocked system path in code: '{node.value}'" + ) + + # ---------------- SYNTAX CHECK ---------------- + def check_syntax(self, filepath: str) -> bool: + path = Path(filepath) + if path.suffix.lower() != ".py": + return True + 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) + + # --- file exists --- + if not path.exists(): + self.logger.log_stderr(f"File not found: {filepath}") + return self._result("", f"File not found: {filepath}", -1) + + ext = path.suffix.lower() + + # --- only allow known safe types --- + if ext not in (".py", ".js", ".sh"): + msg = f"Unsupported file type: {ext}" + self.logger.log_stderr(msg) + return self._result("", msg, -1) + + # --- block shell scripts on Windows (too risky) --- + if ext == ".sh" and sys.platform == "win32": + msg = "Shell scripts are not supported on Windows." + self.logger.log_stderr(msg) + return self._result("", msg, -1) + + # --- Python: syntax + safety check --- + if ext == ".py": + if not self.check_syntax(filepath): + return self._result("", self.logger.format(), -1) + try: + self._check_safe(filepath) + except SafetyError as e: + msg = f"Safety check failed: {e}" + self.logger.log_stderr(msg) + return self._result("", msg, -1) + cmd = [sys.executable, str(path)] + + elif ext == ".js": + cmd = ["node", str(path)] + + elif ext == ".sh": + cmd = ["bash", str(path)] + + # --- execute --- + self.logger.log(f"Running: {path.name}") + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=30, + cwd=str(path.parent), + ) + 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: + msg = "Timed out after 30 seconds." + self.logger.log_stderr(msg) + return self._result("", msg, -1) + except FileNotFoundError as e: + msg = f"Runtime not found: {e}" + self.logger.log_stderr(msg) + return self._result("", msg, -1) + except Exception as e: + 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/backend/file_manager.py b/src/backend/file_manager.py index 8c9532e..36bfec8 100644 --- a/src/backend/file_manager.py +++ b/src/backend/file_manager.py @@ -1,50 +1,72 @@ -import os from pathlib import Path + class FileManager: """ - Verwaltet das Laden, Speichern und Auflisten von Code-Dateien inklusive Ordnerstruktur [1] + Handles all filesystem operations: read, write, create, delete, and change detection. """ + def __init__(self, root_path="."): self.root_path = Path(root_path).resolve() - def list_files(self, current_path=None): - """ - Gibt eine verschachtelte Struktur zurück, um Ordner und Dateien darzustellen [1] - """ - if current_path is None: - current_path = self.root_path - elif not isinstance(current_path, Path): - current_path = Path(current_path) - - items = [] - try: - for item in current_path.iterdir(): - if item.is_dir(): - items.append({"type": "folder", "name": item.name, "path": str(item)}) - elif item.is_file() and item.suffix in {'.py', '.js', '.html', '.css', '.md'}: - items.append({"type": "file", "name": item.name, "path": str(item)}) - except PermissionError: - pass - return items + SUPPORTED_EXTENSIONS = {'.py', '.js', '.html', '.css', '.json', '.md'} - def read_file(self, file_path): - """ - Liest und gibt den Inhalt einer Datei zurück [1] - """ + # ---------------- READ / WRITE ---------------- + def read_file(self, file_path: str) -> str: try: - with open(file_path, 'r', encoding='utf-8') as f: + with open(file_path, "r", encoding="utf-8") as f: return f.read() - except Exception as e: - return None + except Exception: + return "" - def save_file(self, file_path, content): - """ - Schreibt Inhalt in eine Datei [1] - """ + def save_file(self, file_path: str, content: str) -> bool: try: - with open(file_path, 'w', encoding='utf-8') as f: + with open(file_path, "w", encoding="utf-8") as f: f.write(content) return True except Exception: - return False \ No newline at end of file + return False + + # ---------------- CREATE ---------------- + def create_file(self, target_dir: str, name: str) -> tuple: + try: + path = Path(target_dir) / name.strip() + if path.exists(): + return False, f"'{name}' already exists." + if path.suffix.lower() not in self.SUPPORTED_EXTENSIONS: + return False, f"Unsupported extension. Use: {', '.join(sorted(self.SUPPORTED_EXTENSIONS))}" + path.touch() + return True, f"Created '{name}'." + except Exception as e: + return False, str(e) + + def create_folder(self, target_dir: str, name: str) -> tuple: + try: + path = Path(target_dir) / name.strip() + if path.exists(): + return False, f"'{name}' already exists." + path.mkdir(parents=False) + return True, f"Created folder '{name}'." + except Exception as e: + return False, str(e) + + # ---------------- LISTING ---------------- + def list_all_dirs(self, root_folder: str) -> list: + """Return all directories (root first, then subdirs sorted).""" + root = Path(root_folder) + dirs = [root] + for p in sorted(root.rglob("*")): + if p.is_dir(): + dirs.append(p) + return dirs + + # ---------------- CHANGE DETECTION ---------------- + def snapshot(self, root_folder: str) -> frozenset: + """Snapshot of all paths for change detection.""" + try: + return frozenset( + (str(p), p.is_dir()) + for p in Path(root_folder).rglob("*") + ) + except Exception: + return frozenset() \ No newline at end of file diff --git a/src/main.py b/src/main.py index ef431ac..95d0179 100644 --- a/src/main.py +++ b/src/main.py @@ -1,72 +1,115 @@ import sys import os -project_root = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) -if project_root not in sys.path: - sys.path.insert(0, project_root) - +from pathlib import Path import streamlit as st +# ---------------- PATH FIX ---------------- +sys.path.append(os.path.dirname(os.path.abspath(__file__))) +# ---------------- BACKEND ---------------- +from backend.file_manager import FileManager +from ui.navigation import FileNavigation +from ui.chat import ChatInterface +from ui.editor import CodeEditor +from ui.output import OutputDisplay +from backend.chat_manager import ChatManager +from backend.execution_engine import ExecutionEngine -# Backend-Imports -from src.backend.file_manager import FileManager -from src.backend.chat_manager import ChatManager -from src.backend.execution_engine import ExecutionEngine +# ---------------- PAGE CONFIG ---------------- +st.set_page_config( + page_title="AI Code Editor", + page_icon="💻", + layout="wide" +) -# UI-Imports -from src.ui.navigation import FileNavigation -from src.ui.editor import CodeEditor -from src.ui.chat import ChatInterface -from src.ui.output import OutputDisplay +st.markdown(""" + +""", unsafe_allow_html=True) +# ---------------- MAIN ---------------- def main(): - st.set_page_config( - page_title="AI Code Editor", - page_icon="💻", - layout="wide" - ) - - # Initialisiere Manager-Klassen file_manager = FileManager() + navigation = FileNavigation() + chat = ChatInterface() chat_manager = ChatManager() execution_engine = ExecutionEngine() - + output_display = OutputDisplay() + # ---------------- STATE ---------------- + if "root_folder" not in st.session_state: + st.session_state.root_folder = None if "selected_file" not in st.session_state: st.session_state.selected_file = None - if "file_history" not in st.session_state: - st.session_state.file_history = [] + if "active_file" not in st.session_state: + st.session_state.active_file = None + if "editor_content" not in st.session_state: + st.session_state.editor_content = "" + if "last_applied_content" not in st.session_state: + st.session_state.last_applied_content = "" + if "run_result" not in st.session_state: + st.session_state.run_result = None + if "chat_open" not in st.session_state: + st.session_state.chat_open = False - # Sidebar für Dateinavigation - st.sidebar.title("📁 Datei-Explorer") - st.sidebar.subheader("Wähle eine Datei aus") - - navigation = FileNavigation() - file_list = navigation.list_files() - - if file_list: - selected_file = st.sidebar.selectbox("Dateien", file_list) - if selected_file: - content = file_manager.read_file(selected_file) - st.session_state.selected_file = selected_file - - col1, col2 = st.columns([3, 2]) - - with col1: - editor = CodeEditor() - editor.display_code(content) - - if st.button("💾 Speichern"): - file_manager.save_file(selected_file, content) - st.success("Datei gespeichert!") - - with col2: - chat = ChatInterface() - chat.display_chat(chat_manager) - - output = OutputDisplay() - output.display_execution(execution_engine) + # ---------------- SIDEBAR ---------------- + selected_file = navigation.render_sidebar() + if selected_file: + st.session_state.selected_file = selected_file st.sidebar.markdown("---") - st.sidebar.caption("AISE501 – AI in Software Engineering") + + current_file = st.session_state.selected_file + + # ---------------- FILE SWITCH ---------------- + if current_file != st.session_state.active_file: + st.session_state.active_file = current_file + st.session_state.run_result = None + if current_file: + content = file_manager.read_file(current_file) + st.session_state.editor_content = content + st.session_state.last_applied_content = content + else: + st.session_state.editor_content = "" + + chat.inject_panel(st.session_state.chat_open) + + # ---------------- MAIN UI ---------------- + if st.session_state.active_file: + st.subheader(f"✏️ {Path(st.session_state.active_file).name}") + + editor = CodeEditor() + new_value, run_clicked = editor.display_code( + st.session_state.editor_content, + filename=st.session_state.active_file + ) + + # ---------------- APPLY DETECTION ---------------- + if new_value is not None: + if new_value != st.session_state.last_applied_content: + st.session_state.editor_content = new_value + st.session_state.last_applied_content = new_value + file_manager.save_file(st.session_state.active_file, new_value) + st.success("Saved ✔") + + # ---------------- RUN ---------------- + if run_clicked: + with st.spinner("Running…"): + st.session_state.run_result = execution_engine.run_file( + st.session_state.active_file + ) + + if st.session_state.run_result is not None: + st.markdown("---") + st.caption("Output") + output_display.render_output(st.session_state.run_result) + + else: + st.info("Select a file from the explorer") if __name__ == "__main__": main() \ No newline at end of file diff --git a/src/ui/__init__.py b/src/ui/__init__.py index 6003886..2314a11 100644 --- a/src/ui/__init__.py +++ b/src/ui/__init__.py @@ -3,4 +3,6 @@ from .navigation import FileNavigation from .editor import CodeEditor from .chat import ChatInterface -from .output import OutputDisplay \ No newline at end of file +from .output import OutputDisplay + +# Test \ No newline at end of file diff --git a/src/ui/chat.py b/src/ui/chat.py index a5711fd..c88dcc8 100644 --- a/src/ui/chat.py +++ b/src/ui/chat.py @@ -1,45 +1,116 @@ import streamlit as st + class ChatInterface: """ - Chat Interface für KI-Interaktion [1] + Chat Interface for AI interaction, including the right-side slide-out panel. """ - + def __init__(self): self.chat_messages = [] - + + # ---------------- PANEL INJECTION ---------------- + def inject_panel(self, is_open: bool): + """Injects or removes the fixed right-side AI chat panel via st.iframe.""" + st.iframe(f""" + + """, height=1) + + # ---------------- CHAT DISPLAY ---------------- def display_chat(self, chat_manager): - """ - Zeigt die Chat-Schnittstelle mit Konversationsverlauf [1] - """ - st.title("🤖 KI-Assistent") - - # Chat History anzeigen + """Renders the Streamlit chat interface (used when not in panel mode).""" + st.title("AI Assistant") + for message in self.chat_messages: with st.chat_message(message["role"]): st.markdown(message["content"]) - - # User Input - if prompt := st.chat_input("Frage zum Code stellen..."): - # User Nachricht anzeigen + + if prompt := st.chat_input("Ask about your code..."): self.chat_messages.append({"role": "user", "content": prompt}) with st.chat_message("user"): st.markdown(prompt) - - # AI Antwort generieren (via ChatManager) + with st.chat_message("assistant"): response = chat_manager.generate_response(prompt) st.markdown(response) self.chat_messages.append({"role": "assistant", "content": response}) - - def add_message(self, role, content): - """ - Fügt eine Nachricht zur Konversation hinzu - """ + + # ---------------- HELPERS ---------------- + def add_message(self, role: str, content: str): self.chat_messages.append({"role": role, "content": content}) - + def clear_history(self): - """ - Löscht den Chat-Verlauf - """ self.chat_messages.clear() \ No newline at end of file diff --git a/src/ui/editor.py b/src/ui/editor.py index 63f9ec2..24b391b 100644 --- a/src/ui/editor.py +++ b/src/ui/editor.py @@ -1,30 +1,54 @@ +from streamlit_ace import st_ace import streamlit as st + class CodeEditor: - """ - Code Editor & Anzeige Komponente [1] - """ - def __init__(self): - self.language = "python" - self.show_line_numbers = True - - def display_code(self, code_content): + + def detect_language(self, filename: str) -> str: + mapping = { + ".py": "python", + ".js": "javascript", + ".html": "html", + ".css": "css", + ".json": "json", + ".md": "markdown", + } + for ext, lang in mapping.items(): + if filename.endswith(ext): + return lang + return "text" + + def display_code(self, code_content: str, filename: str = "") -> tuple: """ - Zeigt den Code-Editor mit Inhalt an + Renders a toolbar row (Apply is built into st_ace, Run is next to it) + then the ACE editor below. + Returns (new_value, run_clicked) where new_value is None until Apply is hit. """ - if code_content: - st.code(code_content, language=self.language) - else: - st.info("Bitte wählen Sie eine Datei aus der Sidebar aus") - - def get_code_content(self): - """ - Liest den aktuellen Code-Inhalt vom Editor - """ - return st.session_state.get("code_content", "") - - def update_code_content(self, new_content): - """ - Aktualisiert den Code-Inhalt im Session State - """ - st.session_state.code_content = new_content \ No newline at end of file + language = self.detect_language(filename) + + # --- Toolbar: filename label + Run button aligned right --- + label_col, run_col = st.columns([6, 1]) + with label_col: + st.caption(f" {filename.split('/')[-1] or filename}" if filename else "") + with run_col: + run_clicked = st.button( + "▶ RUN CODE", + key=f"run_{filename}", + type="primary", + use_container_width=True + ) + + new_value = st_ace( + value=code_content, + language=language, + theme="chrome", + key=f"ace_{filename}", + font_size=14, + tab_size=4, + wrap=False, + auto_update=False, + show_gutter=True, + show_print_margin=False, + ) + + return new_value, run_clicked \ No newline at end of file diff --git a/src/ui/navigation.py b/src/ui/navigation.py index 81d4bd8..9a328d1 100644 --- a/src/ui/navigation.py +++ b/src/ui/navigation.py @@ -1,30 +1,188 @@ -import os +import tkinter as tk +from tkinter import filedialog from pathlib import Path +import streamlit as st +import streamlit_antd_components as sac + +from backend.file_manager import FileManager + class FileNavigation: """ - Navigation für die Dateiauswahl in der Sidebar [1] + Builds a VS Code-like file tree using streamlit-antd-components. + All filesystem ops delegated to FileManager. """ - + def __init__(self): - self.supported_extensions = {'.py', '.js', '.html', '.css', '.json', '.md'} - - def list_files(self, path=None): - """ - Listet alle Code-Dateien im aktuellen Verzeichnis auf [1] - """ + self.file_manager = FileManager() + self.supported_extensions = FileManager.SUPPORTED_EXTENSIONS + + # ---------------- FOLDER PICKER ---------------- + def pick_folder(self) -> str | None: + root = tk.Tk() + root.withdraw() + root.wm_attributes("-topmost", 1) + folder = filedialog.askdirectory(title="Select project folder") + root.destroy() + return folder if folder else None + + # ---------------- TREE BUILDER ---------------- + def build_tree(self, path=None) -> list: if path is None: path = Path.cwd() - - file_list = [] - for root, dirs, files in os.walk(path): - for file in files: - if os.path.splitext(file)[1] in self.supported_extensions: - file_list.append(file) - return sorted(file_list) - - def get_full_path(self, filename): - """ - Gibt den vollständigen Pfad der Datei zurück - """ - return Path(filename) \ No newline at end of file + path = Path(path) + + def build_node(current_path: Path) -> sac.TreeItem: + is_dir = current_path.is_dir() + if is_dir: + children = [] + try: + for item in sorted( + current_path.iterdir(), + key=lambda x: (not x.is_dir(), x.name.lower()) + ): + if item.is_dir() or item.suffix in self.supported_extensions: + children.append(build_node(item)) + except PermissionError: + pass + return sac.TreeItem( + label=current_path.name, + icon="folder", + children=children if children else None, + ) + else: + return sac.TreeItem( + label=current_path.name, + icon="file-earmark-code", + ) + + return [build_node(path)] + + # ---------------- DIALOG ---------------- + @st.dialog("New file or folder") + def _new_item_dialog(self, root_folder: str): + root = Path(root_folder) + all_dirs = self.file_manager.list_all_dirs(root_folder) + + def dir_label(p: Path) -> str: + rel = p.relative_to(root) + return "/ (root)" if str(rel) == "." else str(rel) + + dir_labels = [dir_label(d) for d in all_dirs] + dir_paths = [str(d) for d in all_dirs] + + mode = st.segmented_control( + "Type", + options=["📄 File", "📁 Folder"], + default="📄 File", + label_visibility="collapsed" + ) + is_file = mode == "📄 File" + + st.markdown("---") + + selected_label = st.selectbox("Location", options=dir_labels, index=0) + target_dir = dir_paths[dir_labels.index(selected_label)] + + name = st.text_input( + "File name" if is_file else "Folder name", + placeholder="e.g. main.py" if is_file else "e.g. utils", + ) + + if name and name.strip(): + sep = "" if target_dir.endswith("/") else "/" + st.caption(f"`{target_dir}{sep}{name.strip()}`") + + st.markdown("---") + + col_cancel, col_create = st.columns(2) + with col_cancel: + if st.button("Cancel", use_container_width=True): + st.rerun() + with col_create: + if st.button( + "Create file" if is_file else "Create folder", + type="primary", + use_container_width=True, + disabled=not (name and name.strip()) + ): + if is_file: + ok, msg = self.file_manager.create_file(target_dir, name) + else: + ok, msg = self.file_manager.create_folder(target_dir, name) + if ok: + st.success(msg) + st.rerun() + else: + st.error(msg) + + # ---------------- SIDEBAR RENDERER ---------------- + def render_sidebar(self) -> str | None: + st.sidebar.title("File Explorer") + root_folder = st.session_state.get("root_folder") + + # --- Auto-refresh: detect external filesystem changes --- + if root_folder: + current_snapshot = self.file_manager.snapshot(root_folder) + last_snapshot = st.session_state.get("_folder_snapshot") + if last_snapshot is None: + st.session_state._folder_snapshot = current_snapshot + elif current_snapshot != last_snapshot: + st.session_state._folder_snapshot = current_snapshot + st.rerun() + + with st.sidebar: + btn_left, btn_right = st.columns(2) + with btn_left: + if st.button("📂 Folder", key="pick_folder", use_container_width=True): + picked = self.pick_folder() + if picked: + st.session_state.root_folder = picked + st.session_state._folder_snapshot = None + st.rerun() + with btn_right: + if st.button("💬 Ask AI", key="toggle_chat", use_container_width=True): + st.session_state.chat_open = not st.session_state.get("chat_open", False) + st.rerun() + + st.markdown("---") + + if root_folder: + if st.button("Add new File/Folder", key="new_item_btn", use_container_width=True): + self._new_item_dialog(root_folder) + + try: + root_path = Path(root_folder) + if root_path.exists() and root_path.is_dir(): + tree_data = self.build_tree(root_path) + + selected = sac.tree( + items=tree_data, + label=" ", + index=None, + align="left", + size="sm", + icon="folder2", + open_all=False, + checkbox=False, + show_line=False, + key=f"tree_{root_folder}", + ) + + if selected: + return self._resolve_path(root_path, selected) + else: + st.warning("Folder not found.") + except Exception as e: + st.error(f"Could not load folder: {e}") + else: + st.text("No folder selected yet.") + + return None + + # ---------------- PATH RESOLVER ---------------- + def _resolve_path(self, root_path: Path, selected_label: str) -> str | None: + for p in root_path.rglob("*"): + if p.name == selected_label and p.suffix in self.supported_extensions: + return str(p) + return None \ No newline at end of file diff --git a/src/ui/output.py b/src/ui/output.py index b0e16df..05bc61c 100644 --- a/src/ui/output.py +++ b/src/ui/output.py @@ -1,42 +1,53 @@ import streamlit as st + class OutputDisplay: """ - Display für Execution Results und Debugging [1] + Renders structured execution results and debug logs. """ - - def __init__(self): - self.execution_output = [] - - def display_execution(self, execution_engine): - """ - Zeigt die Ergebnisse der Code-Ausführung an [1] - """ - st.title("🔍 Execution Output") - - # Ausgabe-Fenster erstellen - output_area = st.empty() - - # Code Execution Button - if st.button("▶️ Code ausführen"): - with output_area: - with st.spinner("Code wird ausgeführt..."): - result = execution_engine.execute_code() - output_area.code(result, language="python") - - # Fehler-Logging anzeigen - if st.checkbox("Debug Logs anzeigen"): - st.subheader("🐛 Debug Logs") - st.text_area("Logs", height=200) - - def display_error(self, error_message): - """ - Zeigt Fehlermeldungen an - """ - st.error(f"❌ Fehler: {error_message}") - - def display_warning(self, warning_message): - """ - Zeigt Warnungen an - """ - st.warning(f"⚠️ Warnung: {warning_message}") \ No newline at end of file + + # ---------------- MAIN RENDERER ---------------- + def render_output(self, result: dict): + success = result["returncode"] == 0 + status_icon = "✅" if success else "❌" + st.markdown(f"**{status_icon} Exited {result['returncode']}**") + + if result["stdout"].strip(): + st.caption("stdout") + st.code(result["stdout"].rstrip(), language="text") + + if result["stderr"].strip(): + st.caption("stderr") + st.code(result["stderr"].rstrip(), language="text") + + 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.") + + # ---------------- HELPERS ---------------- + def display_error(self, error_message: str): + st.error(f"❌ {error_message}") + + def display_warning(self, warning_message: str): + st.warning(f"⚠️ {warning_message}") \ No newline at end of file