Compare commits

..

No commits in common. "26b97a1cb4019890cd23464f9237c9bf962d22c9" and "ee38227d348d4cde57961c8119720760877e58b4" have entirely different histories.

11 changed files with 211 additions and 794 deletions

View File

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

Binary file not shown.

View File

@ -1,72 +1,12 @@
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 = []
# ---------------- INTERNAL ---------------- def log(self, message):
def _entry(self, level: str, message: str) -> dict: self.logs.append(message)
entry = {
"level": level,
"message": message,
"time": datetime.now().strftime("%H:%M:%S"),
}
self.logs.append(entry)
return entry
# ---------------- PUBLIC API ---------------- def get_logs(self):
def log(self, message: str): return self.logs
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,198 +1,12 @@
import ast
import subprocess
import sys
from pathlib import Path
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: class ExecutionEngine:
"""
Platzhalter für die Code-Ausführung [1]
"""
def __init__(self): def __init__(self):
self.logger = DebugLogger() pass
# ---------------- SAFETY CHECK ---------------- def execute_code(self):
def _check_safe(self, filepath: str) -> None:
""" """
Raises SafetyError if the file contains dangerous patterns. Führt Code nicht aus, sondern gibt einen Status aus [1]
Only runs on .py files (other types are blocked outright).
""" """
path = Path(filepath) return "Code wurde nicht ausgeführt (Platzhalter)"
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,72 +1,50 @@
import os
from pathlib import Path from pathlib import Path
class FileManager: class FileManager:
""" """
Handles all filesystem operations: read, write, create, delete, and change detection. Verwaltet das Laden, Speichern und Auflisten von Code-Dateien inklusive Ordnerstruktur [1]
""" """
def __init__(self, root_path="."): def __init__(self, root_path="."):
self.root_path = Path(root_path).resolve() self.root_path = Path(root_path).resolve()
SUPPORTED_EXTENSIONS = {'.py', '.js', '.html', '.css', '.json', '.md'} def list_files(self, current_path=None):
"""
# ---------------- READ / WRITE ---------------- Gibt eine verschachtelte Struktur zurück, um Ordner und Dateien darzustellen [1]
def read_file(self, file_path: str) -> str: """
if current_path is None:
current_path = self.root_path
elif not isinstance(current_path, Path):
current_path = Path(current_path)
items = []
try: try:
with open(file_path, "r", encoding="utf-8") as f: 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:
return f.read() return f.read()
except Exception: except Exception as e:
return "" return None
def save_file(self, file_path: str, content: str) -> bool: def save_file(self, file_path, content):
"""
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,115 +1,72 @@
import sys import sys
import os import os
from pathlib import Path 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)
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
# ---------------- PAGE CONFIG ---------------- # Backend-Imports
st.set_page_config( from src.backend.file_manager import FileManager
page_title="AI Code Editor", from src.backend.chat_manager import ChatManager
page_icon="💻", from src.backend.execution_engine import ExecutionEngine
layout="wide"
)
st.markdown(""" # UI-Imports
<style> from src.ui.navigation import FileNavigation
.stMainBlockContainer { padding-top: 2rem !important; } from src.ui.editor import CodeEditor
.stAppDeployButton, #MainMenu { display: none !important; } from src.ui.chat import ChatInterface
.main .block-container { padding-top: 1rem !important; } from src.ui.output import OutputDisplay
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 "active_file" not in st.session_state: if "file_history" not in st.session_state:
st.session_state.active_file = None st.session_state.file_history = []
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 ---------------- # Sidebar für Dateinavigation
selected_file = navigation.render_sidebar() st.sidebar.title("📁 Datei-Explorer")
if selected_file: st.sidebar.subheader("Wähle eine Datei aus")
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,6 +3,4 @@
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,116 +1,45 @@
import streamlit as st import streamlit as st
class ChatInterface: class ChatInterface:
""" """
Chat Interface for AI interaction, including the right-side slide-out panel. Chat Interface für KI-Interaktion [1]
""" """
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).""" """
st.title("AI Assistant") Zeigt die Chat-Schnittstelle mit Konversationsverlauf [1]
"""
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"])
if prompt := st.chat_input("Ask about your code..."): # User Input
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})
# ---------------- HELPERS ---------------- def add_message(self, role, content):
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,54 +1,30 @@
from streamlit_ace import st_ace
import streamlit as st import streamlit as st
class CodeEditor: class CodeEditor:
"""
def detect_language(self, filename: str) -> str: Code Editor & Anzeige Komponente [1]
mapping = { """
".py": "python", def __init__(self):
".js": "javascript", self.language = "python"
".html": "html", self.show_line_numbers = True
".css": "css",
".json": "json", def display_code(self, code_content):
".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:
""" """
Renders a toolbar row (Apply is built into st_ace, Run is next to it) Zeigt den Code-Editor mit Inhalt an
then the ACE editor below.
Returns (new_value, run_clicked) where new_value is None until Apply is hit.
""" """
language = self.detect_language(filename) if code_content:
st.code(code_content, language=self.language)
# --- Toolbar: filename label + Run button aligned right --- else:
label_col, run_col = st.columns([6, 1]) st.info("Bitte wählen Sie eine Datei aus der Sidebar aus")
with label_col:
st.caption(f" {filename.split('/')[-1] or filename}" if filename else "") def get_code_content(self):
with run_col: """
run_clicked = st.button( Liest den aktuellen Code-Inhalt vom Editor
"▶ RUN CODE", """
key=f"run_{filename}", return st.session_state.get("code_content", "")
type="primary",
use_container_width=True def update_code_content(self, new_content):
) """
Aktualisiert den Code-Inhalt im Session State
new_value = st_ace( """
value=code_content, st.session_state.code_content = new_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,188 +1,30 @@
import tkinter as tk import os
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:
""" """
Builds a VS Code-like file tree using streamlit-antd-components. Navigation für die Dateiauswahl in der Sidebar [1]
All filesystem ops delegated to FileManager.
""" """
def __init__(self): def __init__(self):
self.file_manager = FileManager() self.supported_extensions = {'.py', '.js', '.html', '.css', '.json', '.md'}
self.supported_extensions = FileManager.SUPPORTED_EXTENSIONS
def list_files(self, path=None):
# ---------------- FOLDER PICKER ---------------- """
def pick_folder(self) -> str | None: Listet alle Code-Dateien im aktuellen Verzeichnis auf [1]
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 = []
def build_node(current_path: Path) -> sac.TreeItem: for root, dirs, files in os.walk(path):
is_dir = current_path.is_dir() for file in files:
if is_dir: if os.path.splitext(file)[1] in self.supported_extensions:
children = [] file_list.append(file)
try: return sorted(file_list)
for item in sorted(
current_path.iterdir(), def get_full_path(self, filename):
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: """
children.append(build_node(item)) return Path(filename)
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,53 +1,42 @@
import streamlit as st import streamlit as st
class OutputDisplay: class OutputDisplay:
""" """
Renders structured execution results and debug logs. Display für Execution Results und Debugging [1]
""" """
# ---------------- MAIN RENDERER ---------------- def __init__(self):
def render_output(self, result: dict): self.execution_output = []
success = result["returncode"] == 0
status_icon = "" if success else "" def display_execution(self, execution_engine):
st.markdown(f"**{status_icon} Exited {result['returncode']}**") """
Zeigt die Ergebnisse der Code-Ausführung an [1]
if result["stdout"].strip(): """
st.caption("stdout") st.title("🔍 Execution Output")
st.code(result["stdout"].rstrip(), language="text")
# Ausgabe-Fenster erstellen
if result["stderr"].strip(): output_area = st.empty()
st.caption("stderr")
st.code(result["stderr"].rstrip(), language="text") # Code Execution Button
if st.button("▶️ Code ausführen"):
if result.get("logs"): with output_area:
with st.expander("🪲 Debug log", expanded=False): with st.spinner("Code wird ausgeführt..."):
level_colors = { result = execution_engine.execute_code()
"INFO": "#6c7086", "OUTPUT": "#a6e3a1", "ERROR": "#f38ba8", output_area.code(result, language="python")
"SYNTAX": "#fab387", "EXCEPTION": "#f38ba8",
"SUCCESS": "#a6e3a1", "FAIL": "#f38ba8", # Fehler-Logging anzeigen
} if st.checkbox("Debug Logs anzeigen"):
icons = { st.subheader("🐛 Debug Logs")
"INFO": "", "OUTPUT": "", "ERROR": "", st.text_area("Logs", height=200)
"SYNTAX": "", "EXCEPTION": "💥", "SUCCESS": "", "FAIL": "",
} def display_error(self, error_message):
lines = [] """
for e in result["logs"]: Zeigt Fehlermeldungen an
color = level_colors.get(e["level"], "#cdd6f4") """
icon = icons.get(e["level"], "") st.error(f"❌ Fehler: {error_message}")
lines.append(
f'<span style="color:#585b70">[{e["time"]}]</span> ' def display_warning(self, warning_message):
f'<span style="color:{color}">{icon} <b>{e["level"]} :</b></span> ' """
f'<span style="color:#444444">{e["message"]}</span>' Zeigt Warnungen an
) """
st.markdown("<br>".join(lines), unsafe_allow_html=True) st.warning(f"⚠️ Warnung: {warning_message}")
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}")