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:
"""
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):
# ---------------- 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)

View File

@ -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)"
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
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)
SUPPORTED_EXTENSIONS = {'.py', '.js', '.html', '.css', '.json', '.md'}
items = []
# ---------------- READ / WRITE ----------------
def read_file(self, file_path: str) -> str:
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):
"""
Liest und gibt den Inhalt einer Datei zurück [1]
"""
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
# ---------------- 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 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("""
<style>
.stMainBlockContainer { padding-top: 2rem !important; }
.stAppDeployButton, #MainMenu { display: none !important; }
.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():
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()

View File

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

View File

@ -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 = []
def display_chat(self, chat_manager):
"""
Zeigt die Chat-Schnittstelle mit Konversationsverlauf [1]
"""
st.title("🤖 KI-Assistent")
# ---------------- 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):
"""Renders the Streamlit chat interface (used when not in panel mode)."""
st.title("AI Assistant")
# Chat History anzeigen
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()

View File

@ -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):
"""
Zeigt den Code-Editor mit Inhalt an
"""
if code_content:
st.code(code_content, language=self.language)
else:
st.info("Bitte wählen Sie eine Datei aus der Sidebar aus")
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 get_code_content(self):
def display_code(self, code_content: str, filename: str = "") -> tuple:
"""
Liest den aktuellen Code-Inhalt vom Editor
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.
"""
return st.session_state.get("code_content", "")
language = self.detect_language(filename)
def update_code_content(self, new_content):
"""
Aktualisiert den Code-Inhalt im Session State
"""
st.session_state.code_content = new_content
# --- 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

View File

@ -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'}
self.file_manager = FileManager()
self.supported_extensions = FileManager.SUPPORTED_EXTENSIONS
def list_files(self, path=None):
"""
Listet alle Code-Dateien im aktuellen Verzeichnis auf [1]
"""
# ---------------- 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()
path = Path(path)
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 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",
)
def get_full_path(self, filename):
"""
Gibt den vollständigen Pfad der Datei zurück
"""
return Path(filename)
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
class OutputDisplay:
"""
Display für Execution Results und Debugging [1]
Renders structured execution results and debug logs.
"""
def __init__(self):
self.execution_output = []
# ---------------- 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']}**")
def display_execution(self, execution_engine):
"""
Zeigt die Ergebnisse der Code-Ausführung an [1]
"""
st.title("🔍 Execution Output")
if result["stdout"].strip():
st.caption("stdout")
st.code(result["stdout"].rstrip(), language="text")
# Ausgabe-Fenster erstellen
output_area = st.empty()
if result["stderr"].strip():
st.caption("stderr")
st.code(result["stderr"].rstrip(), language="text")
# 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")
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'<span style="color:#585b70">[{e["time"]}]</span> '
f'<span style="color:{color}">{icon} <b>{e["level"]} :</b></span> '
f'<span style="color:#444444">{e["message"]}</span>'
)
st.markdown("<br>".join(lines), unsafe_allow_html=True)
# Fehler-Logging anzeigen
if st.checkbox("Debug Logs anzeigen"):
st.subheader("🐛 Debug Logs")
st.text_area("Logs", height=200)
if not result["stdout"].strip() and not result["stderr"].strip():
st.caption("No output.")
def display_error(self, error_message):
"""
Zeigt Fehlermeldungen an
"""
st.error(f"❌ Fehler: {error_message}")
# ---------------- HELPERS ----------------
def display_error(self, error_message: str):
st.error(f"{error_message}")
def display_warning(self, warning_message):
"""
Zeigt Warnungen an
"""
st.warning(f"⚠️ Warnung: {warning_message}")
def display_warning(self, warning_message: str):
st.warning(f"⚠️ {warning_message}")