Merge pull request 'dev-leart' (#1) from dev-leart into master
Reviewed-on: #1
This commit is contained in:
commit
26b97a1cb4
6
.streamlit/config.toml
Normal file
6
.streamlit/config.toml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
[theme]
|
||||||
|
base = "light"
|
||||||
|
primaryColor = "#4A90D9"
|
||||||
|
backgroundColor = "#ffffff"
|
||||||
|
secondaryBackgroundColor = "#f5f5f5"
|
||||||
|
textColor = "#1a1a1a"
|
||||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
@ -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)
|
||||||
@ -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
|
||||||
@ -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()
|
||||||
149
src/main.py
149
src/main.py
@ -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()
|
||||||
@ -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
|
||||||
119
src/ui/chat.py
119
src/ui/chat.py
@ -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()
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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}")
|
||||||
Loading…
x
Reference in New Issue
Block a user