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