diff --git a/src/main.py b/src/main.py index 65eb1bf..51df815 100644 --- a/src/main.py +++ b/src/main.py @@ -1,10 +1,7 @@ import sys import os -import tkinter as tk -from tkinter import filedialog from pathlib import Path import streamlit as st -from streamlit_tree_select import tree_select # ---------------- PATH FIX ---------------- sys.path.append(os.path.dirname(os.path.abspath(__file__))) # ---------------- BACKEND ---------------- @@ -12,6 +9,7 @@ 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 @@ -22,112 +20,20 @@ st.set_page_config( layout="wide" ) -# ---------------- FOLDER PICKER ---------------- -def pick_folder(): - 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 - -# ---------------- SIDEBAR ---------------- -def render_sidebar(): - st.sidebar.title("File Explorer") - - root_folder = st.session_state.get("root_folder") - - with st.sidebar: - if st.button("📂 Choose Folder", key="pick_folder"): - picked = pick_folder() - if picked: - st.session_state.root_folder = picked - st.rerun() - - if root_folder: - st.caption(f" {Path(root_folder).name}") - - st.markdown("---") - - if root_folder: - try: - root_path = Path(root_folder) - if root_path.exists() and root_path.is_dir(): - navigation = FileNavigation() - tree_data = navigation.build_tree(root_path) - selection = tree_select( - tree_data, - check_model="leaf", - only_leaf_checkboxes=True - ) - if selection and selection.get("checked"): - return selection["checked"][0] - else: - st.warning("Folder not found.") - except Exception: - st.error("Could not load folder.") - else: - st.info("No folder selected yet.") - - return None - -# ---------------- OUTPUT PANEL ---------------- -def render_output(result: dict): - """Render structured execution output from ExecutionEngine.""" - success = result["returncode"] == 0 - status_icon = "✅" if success else "❌" - status_text = f"Exited {result['returncode']}" - - st.markdown(f"**{status_icon} {status_text}**") - - # stdout - if result["stdout"].strip(): - st.caption("stdout") - st.code(result["stdout"].rstrip(), language="text") - - # stderr - if result["stderr"].strip(): - st.caption("stderr") - st.code(result["stderr"].rstrip(), language="text") - - # debug log entries (collapsed by default) - 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'[{e["time"]}] ' - f'{icon} {e["level"]} ' - f'{e["message"]}' - ) - st.markdown( - "
".join(lines), - unsafe_allow_html=True - ) - - if not result["stdout"].strip() and not result["stderr"].strip(): - st.caption("No output.") +st.markdown(""" + +""", unsafe_allow_html=True) # ---------------- MAIN ---------------- def main(): file_manager = FileManager() + navigation = FileNavigation() chat_manager = ChatManager() execution_engine = ExecutionEngine() + output_display = OutputDisplay() # ---------------- STATE ---------------- if "root_folder" not in st.session_state: @@ -144,7 +50,7 @@ def main(): st.session_state.run_result = None # ---------------- FILE SELECT ---------------- - selected_file = render_sidebar() + selected_file = navigation.render_sidebar() if selected_file: st.session_state.selected_file = selected_file @@ -200,7 +106,7 @@ def main(): if st.session_state.run_result is not None: st.markdown("---") st.caption("Output") - render_output(st.session_state.run_result) + output_display.render_output(st.session_state.run_result) else: st.info("Select a file from the explorer") diff --git a/src/ui/navigation.py b/src/ui/navigation.py index 985c9ba..5722b65 100644 --- a/src/ui/navigation.py +++ b/src/ui/navigation.py @@ -1,30 +1,45 @@ +import tkinter as tk +from tkinter import filedialog from pathlib import Path +import streamlit as st +from streamlit_tree_select import tree_select + class FileNavigation: """ Builds a VS Code-like file tree for streamlit_tree_select + and handles folder picking. """ def __init__(self): self.supported_extensions = {'.py', '.js', '.html', '.css', '.json', '.md'} - def build_tree(self, path=None): + # ---------------- FOLDER PICKER ---------------- + def pick_folder(self) -> str | None: + """Open a native OS folder picker and return the selected path.""" + 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: + """Build a VS Code-style tree structure for streamlit_tree_select.""" if path is None: path = Path.cwd() path = Path(path) - def build_node(current_path: Path): + def build_node(current_path: Path) -> dict: is_dir = current_path.is_dir() - node = { "label": ("📁 " if is_dir else "📄 ") + current_path.name, "value": str(current_path), } - if is_dir: children = [] - try: for item in sorted( current_path.iterdir(), @@ -34,10 +49,49 @@ class FileNavigation: children.append(build_node(item)) except PermissionError: pass - node["children"] = children - return node - # IMPORTANT: must return a LIST - return [build_node(path)] \ No newline at end of file + return [build_node(path)] + + # ---------------- SIDEBAR RENDERER ---------------- + def render_sidebar(self) -> str | None: + """ + Renders the full sidebar: folder button, folder caption, divider, and tree. + Returns the selected file path or None. + """ + st.sidebar.title("File Explorer") + root_folder = st.session_state.get("root_folder") + + with st.sidebar: + if st.button("📂 Choose Folder", key="pick_folder"): + picked = self.pick_folder() + if picked: + st.session_state.root_folder = picked + st.rerun() + + if root_folder: + st.caption(f" {Path(root_folder).name}") + + st.markdown("---") + + if root_folder: + try: + root_path = Path(root_folder) + if root_path.exists() and root_path.is_dir(): + tree_data = self.build_tree(root_path) + selection = tree_select( + tree_data, + check_model="leaf", + only_leaf_checkboxes=True + ) + if selection and selection.get("checked"): + return selection["checked"][0] + else: + st.warning("Folder not found.") + except Exception: + st.error("Could not load folder.") + else: + st.info("No folder selected yet.") + + return None \ No newline at end of file diff --git a/src/ui/output.py b/src/ui/output.py index b0e16df..b06b43d 100644 --- a/src/ui/output.py +++ b/src/ui/output.py @@ -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 = [] - - def display_execution(self, execution_engine): - """ - Zeigt die Ergebnisse der Code-Ausführung an [1] - """ - st.title("🔍 Execution Output") - - # Ausgabe-Fenster erstellen - output_area = st.empty() - - # 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") - - # Fehler-Logging anzeigen - if st.checkbox("Debug Logs anzeigen"): - st.subheader("🐛 Debug Logs") - st.text_area("Logs", height=200) - - def display_error(self, error_message): - """ - Zeigt Fehlermeldungen an - """ - st.error(f"❌ Fehler: {error_message}") - - def display_warning(self, warning_message): - """ - Zeigt Warnungen an - """ - st.warning(f"⚠️ Warnung: {warning_message}") \ No newline at end of file + + # ---------------- 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']}**") + + if result["stdout"].strip(): + st.caption("stdout") + st.code(result["stdout"].rstrip(), language="text") + + if result["stderr"].strip(): + st.caption("stderr") + st.code(result["stderr"].rstrip(), language="text") + + 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'[{e["time"]}] ' + f'{icon} {e["level"]} ' + f'{e["message"]}' + ) + st.markdown("
".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}") \ No newline at end of file