Merge pull request 'dev-leart' (#1) from dev-leart into master

Reviewed-on: #1
This commit is contained in:
Thomas Hollenstein 2026-05-26 22:08:47 +02:00
commit 26b97a1cb4
11 changed files with 795 additions and 212 deletions

6
.streamlit/config.toml Normal file
View File

@ -0,0 +1,6 @@
[theme]
base = "light"
primaryColor = "#4A90D9"
backgroundColor = "#ffffff"
secondaryBackgroundColor = "#f5f5f5"
textColor = "#1a1a1a"

Binary file not shown.

View File

@ -1,12 +1,72 @@
import ast
import traceback
from datetime import datetime
class DebugLogger: class DebugLogger:
"""
Platzhalter für das Logging [1]
"""
def __init__(self): def __init__(self):
self.logs = [] self.logs = []
def log(self, message): # ---------------- INTERNAL ----------------
self.logs.append(message) 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): # ---------------- PUBLIC API ----------------
return self.logs 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)

View File

@ -1,12 +1,198 @@
class ExecutionEngine: import ast
""" import subprocess
Platzhalter für die Code-Ausführung [1] import sys
""" from pathlib import Path
def __init__(self):
pass
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)" 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

View File

@ -1,50 +1,72 @@
import os
from pathlib import Path from pathlib import Path
class FileManager: 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="."): def __init__(self, root_path="."):
self.root_path = Path(root_path).resolve() self.root_path = Path(root_path).resolve()
def list_files(self, current_path=None): SUPPORTED_EXTENSIONS = {'.py', '.js', '.html', '.css', '.json', '.md'}
"""
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
def read_file(self, file_path): # ---------------- READ / WRITE ----------------
""" def read_file(self, file_path: str) -> str:
Liest und gibt den Inhalt einer Datei zurück [1]
"""
try: try:
with open(file_path, 'r', encoding='utf-8') as f: with open(file_path, "r", encoding="utf-8") as f:
return f.read() return f.read()
except Exception as e: except Exception:
return None return ""
def save_file(self, file_path, content): def save_file(self, file_path: str, content: str) -> bool:
"""
Schreibt Inhalt in eine Datei [1]
"""
try: try:
with open(file_path, 'w', encoding='utf-8') as f: with open(file_path, "w", encoding="utf-8") as f:
f.write(content) f.write(content)
return True return True
except Exception: except Exception:
return False 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()

View File

@ -1,72 +1,115 @@
import sys import sys
import os import os
project_root = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) from pathlib import Path
if project_root not in sys.path:
sys.path.insert(0, project_root)
import streamlit as st 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 # ---------------- PAGE CONFIG ----------------
from src.backend.file_manager import FileManager st.set_page_config(
from src.backend.chat_manager import ChatManager page_title="AI Code Editor",
from src.backend.execution_engine import ExecutionEngine page_icon="💻",
layout="wide"
)
# UI-Imports st.markdown("""
from src.ui.navigation import FileNavigation <style>
from src.ui.editor import CodeEditor .stMainBlockContainer { padding-top: 2rem !important; }
from src.ui.chat import ChatInterface .stAppDeployButton, #MainMenu { display: none !important; }
from src.ui.output import OutputDisplay .main .block-container { padding-top: 1rem !important; }
body.chat-open .main .block-container { margin-right: 400px; }
iframe[height="1"] { display: none !important; }
[data-testid="stSidebar"] { z-index: 99 !important; }
</style>
""", unsafe_allow_html=True)
# ---------------- MAIN ----------------
def main(): def main():
st.set_page_config(
page_title="AI Code Editor",
page_icon="💻",
layout="wide"
)
# Initialisiere Manager-Klassen
file_manager = FileManager() file_manager = FileManager()
navigation = FileNavigation()
chat = ChatInterface()
chat_manager = ChatManager() chat_manager = ChatManager()
execution_engine = ExecutionEngine() 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: if "selected_file" not in st.session_state:
st.session_state.selected_file = None st.session_state.selected_file = None
if "file_history" not in st.session_state: if "active_file" not in st.session_state:
st.session_state.file_history = [] 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 # ---------------- SIDEBAR ----------------
st.sidebar.title("📁 Datei-Explorer") selected_file = navigation.render_sidebar()
st.sidebar.subheader("Wähle eine Datei aus") if selected_file:
st.session_state.selected_file = selected_file
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)
st.sidebar.markdown("---") 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__": if __name__ == "__main__":
main() main()

View File

@ -3,4 +3,6 @@
from .navigation import FileNavigation from .navigation import FileNavigation
from .editor import CodeEditor from .editor import CodeEditor
from .chat import ChatInterface from .chat import ChatInterface
from .output import OutputDisplay from .output import OutputDisplay
# Test

View File

@ -1,45 +1,116 @@
import streamlit as st import streamlit as st
class ChatInterface: class ChatInterface:
""" """
Chat Interface für KI-Interaktion [1] Chat Interface for AI interaction, including the right-side slide-out panel.
""" """
def __init__(self): def __init__(self):
self.chat_messages = [] 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"""
<script>
const win = window.parent;
const doc = win.document;
const old = doc.getElementById('ai-chat-panel');
if (old) old.remove();
doc.body.classList.remove('chat-open');
if ({'true' if is_open else 'false'}) {{
doc.body.classList.add('chat-open');
const panel = doc.createElement('div');
panel.id = 'ai-chat-panel';
panel.style.cssText = `
position: fixed;
top: 0;
right: 0;
width: 380px;
height: 100vh;
background: #ffffff;
border-left: 1px solid #e0e0e0;
z-index: 999999;
display: flex;
flex-direction: column;
box-shadow: -4px 0 24px rgba(0,0,0,0.08);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
`;
panel.innerHTML = `
<div style="padding:1rem 1.25rem; border-bottom:1px solid #e0e0e0; display:flex; align-items:center;">
<span style="color:#1a1a1a; font-weight:600; font-size:0.95rem;">💬 Ask AI</span>
</div>
<div id="ai-messages" style="flex:1; overflow-y:auto; padding:1rem; display:flex; flex-direction:column; gap:0.75rem;">
<div style="color:#888; font-size:0.82rem; text-align:center;">Ask anything about your code.</div>
</div>
<div style="padding:0.75rem 1rem; border-top:1px solid #e0e0e0; display:flex; gap:0.5rem;">
<input id="ai-input" type="text" placeholder="Ask about your code…"
style="flex:1; background:#f5f5f5; border:1px solid #ddd; border-radius:6px;
color:#1a1a1a; padding:0.5rem 0.75rem; font-size:0.85rem; outline:none;" />
<button onclick="sendMessage()"
style="background:#1a1a1a; color:#fff; border:none; border-radius:6px;
padding:0.5rem 0.9rem; font-size:0.85rem; font-weight:600; cursor:pointer;">
Send
</button>
</div>
`;
doc.body.appendChild(panel);
win.sendMessage = function() {{
const input = doc.getElementById('ai-input');
const msgs = doc.getElementById('ai-messages');
const text = input.value.trim();
if (!text) return;
const userMsg = doc.createElement('div');
userMsg.style.cssText = 'background:#1a1a1a; border-radius:8px 8px 2px 8px; padding:0.5rem 0.75rem; color:#fff; font-size:0.85rem; align-self:flex-end; max-width:85%;';
userMsg.textContent = text;
msgs.appendChild(userMsg);
input.value = '';
msgs.scrollTop = msgs.scrollHeight;
const aiMsg = doc.createElement('div');
aiMsg.style.cssText = 'background:#f0f0f0; border-radius:8px 8px 8px 2px; padding:0.5rem 0.75rem; color:#1a1a1a; font-size:0.85rem; align-self:flex-start; max-width:85%;';
aiMsg.textContent = '';
msgs.appendChild(aiMsg);
msgs.scrollTop = msgs.scrollHeight;
}};
doc.getElementById('ai-input').addEventListener('keydown', function(e) {{
if (e.key === 'Enter') win.sendMessage();
}});
}}
</script>
""", height=1)
# ---------------- CHAT DISPLAY ----------------
def display_chat(self, chat_manager): def display_chat(self, chat_manager):
""" """Renders the Streamlit chat interface (used when not in panel mode)."""
Zeigt die Chat-Schnittstelle mit Konversationsverlauf [1] st.title("AI Assistant")
"""
st.title("🤖 KI-Assistent")
# Chat History anzeigen
for message in self.chat_messages: for message in self.chat_messages:
with st.chat_message(message["role"]): with st.chat_message(message["role"]):
st.markdown(message["content"]) st.markdown(message["content"])
# User Input if prompt := st.chat_input("Ask about your code..."):
if prompt := st.chat_input("Frage zum Code stellen..."):
# User Nachricht anzeigen
self.chat_messages.append({"role": "user", "content": prompt}) self.chat_messages.append({"role": "user", "content": prompt})
with st.chat_message("user"): with st.chat_message("user"):
st.markdown(prompt) st.markdown(prompt)
# AI Antwort generieren (via ChatManager)
with st.chat_message("assistant"): with st.chat_message("assistant"):
response = chat_manager.generate_response(prompt) response = chat_manager.generate_response(prompt)
st.markdown(response) st.markdown(response)
self.chat_messages.append({"role": "assistant", "content": response}) self.chat_messages.append({"role": "assistant", "content": response})
def add_message(self, role, content): # ---------------- HELPERS ----------------
""" def add_message(self, role: str, content: str):
Fügt eine Nachricht zur Konversation hinzu
"""
self.chat_messages.append({"role": role, "content": content}) self.chat_messages.append({"role": role, "content": content})
def clear_history(self): def clear_history(self):
"""
Löscht den Chat-Verlauf
"""
self.chat_messages.clear() self.chat_messages.clear()

View File

@ -1,30 +1,54 @@
from streamlit_ace import st_ace
import streamlit as st import streamlit as st
class CodeEditor: class CodeEditor:
"""
Code Editor & Anzeige Komponente [1] def detect_language(self, filename: str) -> str:
""" mapping = {
def __init__(self): ".py": "python",
self.language = "python" ".js": "javascript",
self.show_line_numbers = True ".html": "html",
".css": "css",
def display_code(self, code_content): ".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: language = self.detect_language(filename)
st.code(code_content, language=self.language)
else: # --- Toolbar: filename label + Run button aligned right ---
st.info("Bitte wählen Sie eine Datei aus der Sidebar aus") label_col, run_col = st.columns([6, 1])
with label_col:
def get_code_content(self): st.caption(f" {filename.split('/')[-1] or filename}" if filename else "")
""" with run_col:
Liest den aktuellen Code-Inhalt vom Editor run_clicked = st.button(
""" "▶ RUN CODE",
return st.session_state.get("code_content", "") key=f"run_{filename}",
type="primary",
def update_code_content(self, new_content): use_container_width=True
""" )
Aktualisiert den Code-Inhalt im Session State
""" new_value = st_ace(
st.session_state.code_content = new_content 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

View File

@ -1,30 +1,188 @@
import os import tkinter as tk
from tkinter import filedialog
from pathlib import Path from pathlib import Path
import streamlit as st
import streamlit_antd_components as sac
from backend.file_manager import FileManager
class FileNavigation: 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): def __init__(self):
self.supported_extensions = {'.py', '.js', '.html', '.css', '.json', '.md'} self.file_manager = FileManager()
self.supported_extensions = FileManager.SUPPORTED_EXTENSIONS
def list_files(self, path=None):
""" # ---------------- FOLDER PICKER ----------------
Listet alle Code-Dateien im aktuellen Verzeichnis auf [1] 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: if path is None:
path = Path.cwd() path = Path.cwd()
path = Path(path)
file_list = []
for root, dirs, files in os.walk(path): def build_node(current_path: Path) -> sac.TreeItem:
for file in files: is_dir = current_path.is_dir()
if os.path.splitext(file)[1] in self.supported_extensions: if is_dir:
file_list.append(file) children = []
return sorted(file_list) try:
for item in sorted(
def get_full_path(self, filename): current_path.iterdir(),
""" key=lambda x: (not x.is_dir(), x.name.lower())
Gibt den vollständigen Pfad der Datei zurück ):
""" if item.is_dir() or item.suffix in self.supported_extensions:
return Path(filename) 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

View File

@ -1,42 +1,53 @@
import streamlit as st import streamlit as st
class OutputDisplay: class OutputDisplay:
""" """
Display für Execution Results und Debugging [1] Renders structured execution results and debug logs.
""" """
def __init__(self): # ---------------- MAIN RENDERER ----------------
self.execution_output = [] def render_output(self, result: dict):
success = result["returncode"] == 0
def display_execution(self, execution_engine): status_icon = "" if success else ""
""" st.markdown(f"**{status_icon} Exited {result['returncode']}**")
Zeigt die Ergebnisse der Code-Ausführung an [1]
""" if result["stdout"].strip():
st.title("🔍 Execution Output") st.caption("stdout")
st.code(result["stdout"].rstrip(), language="text")
# Ausgabe-Fenster erstellen
output_area = st.empty() if result["stderr"].strip():
st.caption("stderr")
# Code Execution Button st.code(result["stderr"].rstrip(), language="text")
if st.button("▶️ Code ausführen"):
with output_area: if result.get("logs"):
with st.spinner("Code wird ausgeführt..."): with st.expander("🪲 Debug log", expanded=False):
result = execution_engine.execute_code() level_colors = {
output_area.code(result, language="python") "INFO": "#6c7086", "OUTPUT": "#a6e3a1", "ERROR": "#f38ba8",
"SYNTAX": "#fab387", "EXCEPTION": "#f38ba8",
# Fehler-Logging anzeigen "SUCCESS": "#a6e3a1", "FAIL": "#f38ba8",
if st.checkbox("Debug Logs anzeigen"): }
st.subheader("🐛 Debug Logs") icons = {
st.text_area("Logs", height=200) "INFO": "", "OUTPUT": "", "ERROR": "",
"SYNTAX": "", "EXCEPTION": "💥", "SUCCESS": "", "FAIL": "",
def display_error(self, error_message): }
""" lines = []
Zeigt Fehlermeldungen an for e in result["logs"]:
""" color = level_colors.get(e["level"], "#cdd6f4")
st.error(f"❌ Fehler: {error_message}") icon = icons.get(e["level"], "")
lines.append(
def display_warning(self, warning_message): f'<span style="color:#585b70">[{e["time"]}]</span> '
""" f'<span style="color:{color}">{icon} <b>{e["level"]} :</b></span> '
Zeigt Warnungen an f'<span style="color:#444444">{e["message"]}</span>'
""" )
st.warning(f"⚠️ Warnung: {warning_message}") st.markdown("<br>".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}")