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:
|
||||
"""
|
||||
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)
|
||||
@ -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
|
||||
@ -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()
|
||||
149
src/main.py
149
src/main.py
@ -1,72 +1,115 @@
|
||||
import sys
|
||||
import os
|
||||
project_root = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
|
||||
if project_root not in sys.path:
|
||||
sys.path.insert(0, project_root)
|
||||
|
||||
from pathlib import Path
|
||||
import streamlit as st
|
||||
# ---------------- PATH FIX ----------------
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
# ---------------- BACKEND ----------------
|
||||
from backend.file_manager import FileManager
|
||||
from ui.navigation import FileNavigation
|
||||
from ui.chat import ChatInterface
|
||||
from ui.editor import CodeEditor
|
||||
from ui.output import OutputDisplay
|
||||
from backend.chat_manager import ChatManager
|
||||
from backend.execution_engine import ExecutionEngine
|
||||
|
||||
# Backend-Imports
|
||||
from src.backend.file_manager import FileManager
|
||||
from src.backend.chat_manager import ChatManager
|
||||
from src.backend.execution_engine import ExecutionEngine
|
||||
# ---------------- PAGE CONFIG ----------------
|
||||
st.set_page_config(
|
||||
page_title="AI Code Editor",
|
||||
page_icon="💻",
|
||||
layout="wide"
|
||||
)
|
||||
|
||||
# UI-Imports
|
||||
from src.ui.navigation import FileNavigation
|
||||
from src.ui.editor import CodeEditor
|
||||
from src.ui.chat import ChatInterface
|
||||
from src.ui.output import OutputDisplay
|
||||
st.markdown("""
|
||||
<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()
|
||||
@ -4,3 +4,5 @@ from .navigation import FileNavigation
|
||||
from .editor import CodeEditor
|
||||
from .chat import ChatInterface
|
||||
from .output import OutputDisplay
|
||||
|
||||
# Test
|
||||
107
src/ui/chat.py
107
src/ui/chat.py
@ -1,45 +1,116 @@
|
||||
import streamlit as st
|
||||
|
||||
|
||||
class ChatInterface:
|
||||
"""
|
||||
Chat Interface für KI-Interaktion [1]
|
||||
Chat Interface for AI interaction, including the right-side slide-out panel.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.chat_messages = []
|
||||
|
||||
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()
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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}")
|
||||
Loading…
x
Reference in New Issue
Block a user