From 57ba9eaf0b0c51c98724fe48e211a5255dbbe41d Mon Sep 17 00:00:00 2001 From: leart-ramushi <113895250+leart-ramushi@users.noreply.github.com> Date: Thu, 21 May 2026 21:59:57 +0200 Subject: [PATCH 01/11] Test --- src/backend/file_manager.py | 38 ++----------- src/main.py | 110 ++++++++++++++++++++++-------------- src/ui/navigation.py | 55 +++++++++++------- 3 files changed, 107 insertions(+), 96 deletions(-) diff --git a/src/backend/file_manager.py b/src/backend/file_manager.py index 8c9532e..011b975 100644 --- a/src/backend/file_manager.py +++ b/src/backend/file_manager.py @@ -1,49 +1,23 @@ -import os from pathlib import Path class FileManager: """ - Verwaltet das Laden, Speichern und Auflisten von Code-Dateien inklusive Ordnerstruktur [1] + Handles file reading and writing """ + 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) - - 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): - """ - 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] - """ 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: diff --git a/src/main.py b/src/main.py index ef431ac..96205d2 100644 --- a/src/main.py +++ b/src/main.py @@ -1,21 +1,45 @@ import sys import os -project_root = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) +from pathlib import Path + +import streamlit as st +from streamlit_tree_select import tree_select + +# project path fix +project_root = os.path.abspath(os.path.dirname(__file__)) if project_root not in sys.path: sys.path.insert(0, project_root) -import streamlit as st +# backend +from backend.file_manager import FileManager +from ui.navigation import FileNavigation -# Backend-Imports -from src.backend.file_manager import FileManager -from src.backend.chat_manager import ChatManager -from src.backend.execution_engine import ExecutionEngine +# your existing modules (keep yours as-is) +from backend.chat_manager import ChatManager +from backend.execution_engine import ExecutionEngine +from ui.editor import CodeEditor +from ui.chat import ChatInterface +from ui.output import OutputDisplay + + +def render_sidebar(): + st.sidebar.title("📁 File Explorer") + + navigation = FileNavigation() + tree_data = navigation.build_tree() + + with st.sidebar: + selection = tree_select( + tree_data, + check_model="leaf", + only_leaf_checkboxes=True + ) + + if selection and selection.get("checked"): + return selection["checked"][0] + + return None -# 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 def main(): st.set_page_config( @@ -24,49 +48,49 @@ def main(): layout="wide" ) - # Initialisiere Manager-Klassen file_manager = FileManager() chat_manager = ChatManager() execution_engine = ExecutionEngine() - 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 = [] - # Sidebar für Dateinavigation - st.sidebar.title("📁 Datei-Explorer") - st.sidebar.subheader("Wähle eine Datei aus") + # --- SIDEBAR --- + selected_file = render_sidebar() - 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) + if selected_file: + st.session_state.selected_file = selected_file st.sidebar.markdown("---") st.sidebar.caption("AISE501 – AI in Software Engineering") + # --- MAIN AREA --- + if st.session_state.selected_file: + + content = file_manager.read_file(st.session_state.selected_file) + + col1, col2 = st.columns([3, 2]) + + with col1: + st.subheader(f"Editing: {Path(st.session_state.selected_file).name}") + + editor = CodeEditor() + editor.display_code(content) + + if st.button("💾 Save"): + file_manager.save_file(st.session_state.selected_file, content) + st.success("File saved!") + + with col2: + chat = ChatInterface() + chat.display_chat(chat_manager) + + output = OutputDisplay() + output.display_execution(execution_engine) + + else: + st.info("Select a file from the sidebar explorer 👈") + + if __name__ == "__main__": main() \ No newline at end of file diff --git a/src/ui/navigation.py b/src/ui/navigation.py index 81d4bd8..985c9ba 100644 --- a/src/ui/navigation.py +++ b/src/ui/navigation.py @@ -1,30 +1,43 @@ -import os from pathlib import Path class FileNavigation: """ - Navigation für die Dateiauswahl in der Sidebar [1] + Builds a VS Code-like file tree for streamlit_tree_select """ - + def __init__(self): self.supported_extensions = {'.py', '.js', '.html', '.css', '.json', '.md'} - - def list_files(self, path=None): - """ - Listet alle Code-Dateien im aktuellen Verzeichnis auf [1] - """ + + def build_tree(self, path=None): if path is None: path = Path.cwd() - - 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 get_full_path(self, filename): - """ - Gibt den vollständigen Pfad der Datei zurück - """ - return Path(filename) \ No newline at end of file + + path = Path(path) + + def build_node(current_path: Path): + 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(), + 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 + + node["children"] = children + + return node + + # IMPORTANT: must return a LIST + return [build_node(path)] \ No newline at end of file From c3e37d1709a6981d623b69be8962f48a9b9b8fad Mon Sep 17 00:00:00 2001 From: leart-ramushi <113895250+leart-ramushi@users.noreply.github.com> Date: Fri, 22 May 2026 15:45:15 +0200 Subject: [PATCH 02/11] new requirements --- requirements.txt | Bin 771 -> 4426 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/requirements.txt b/requirements.txt index 120898d20f6abde906c6e3e98ac595e185ec9b0e..2817cb1058a85e24574ca29e95d3588ae0fc7fc8 100644 GIT binary patch literal 4426 zcmZ{nOK%%T5QOI(AU}nMC`x`Dd`K=v0tg6V1UVXfNuoq9X>ui+{_#ob>)Ph*5)KSn zwz{Xgy1Ke&`0qc{vMz0zmQ~r8kNWeryeVUSuFB8lC;h%DC)u`Py9wK*XFcd_E9-Mv z%JUFjwiCq>@Yr{@>x=j^otu^yd92F4ey-)W)z4kI>18DEbwr!VZzk_$`6i!f#9emC zL5=6l*|Q0clOpbgZmV-tcqJdIbcHkJT**oWW`H;!diK4d%p)VGuj4e8&ExlvvcSML zu;cMg9>kag4o>d7`c{$uTA9tmt-Sc3qdlJMY0D_^?#o8M1K;;EI@qPkcikzhbD<~m z`_7gT!9?xTh?BK*`fYc5t3058k=IV0#>dzz{*&Ub&-r0*94xrM3t8q}79yELSC$%} zTX?z8O?iksFv-Vp?o|g_TA|qKXClGF_1r5uXoqaOnySeM#c9hzDj@>aL=4NH!up4D zwB<44;l*jHzU?wS>VM-n@3U1ckCA_OfdMafog6D=M|m~!ZLqxxhWmL@8JV)ep~si- ze^Bq3tyYuqB^ZLsoF~ru6tOZ<}KCo6!Xg_n2X=#S$Fion$P^q`Ot5qQ*?f={HTHXVlUVaq@EnKW#*`v zxjhNdR^EHnR4byZX8u`Gj{1;uU0!vfPCC(yX)Tp*WG6yyL2L6aVn4@T`mBH7D*|3t zUDnR5wyJ=cYxOsqD!sojvUhyi?w_{%pmISsl)4 zRg?P*GB%C7vQvHAV64?{$i0Z#ebc2rZ_m}>kKas8-$zEGO*AJp=0P^*u1*4+W=3v` zrLry62e<)sYBlRU$+8b@uIl={haH9e`%7fmY9s$8Rb~|C`nJ;)J@)P5+l?%BOA+H! zDC+O%9n;}#MvE_+!mqzW$>h1JvB>19WNS8oW;jV!`K8}T>Oo`H#PCGU9{KOo{fF|a ze64!&gVNiDsErg2>(&4Y?pyPvQ#ESsRh9bAWJcLLyK|iW?E;cNg>~NL$*$F&e3p%B zmhs+5EJLI@b)(QbFR{K_s0>~?vs1mi4^ugHVmXTr;rjKA)yk)5FjDhulz}%Yb)l>J zigN`y%l$KWd1v~L%UjC((D7e11-Z+t|Eh9g9c2Y!W!<~1@5QasXnyjhygJYINgU1s zZ)%&<#`;xNpd8eVm2P|Az&`h~_Xw!0Ran8s&BIUjV8sOwlipsy7F(XDgW7-fx%G%((X+;@)W$>+;p~K5|(t z)OSz}&j(C7f{`4QnfbJ?vsd0W`F(-Y?4+DUX2?-@n!S1(!)M)~yf{!|MQ;_7BWq)~ zvfsU9;nq1|ozJ-IIa>F9lC$AAAU^Ev#EaKnh+u^oNX{J3nEA%UHzOF&ZEg+sv0r+P zB7a3^9)BDWUuvo^CRA844)c$MW)t&*X6qT(|9=_sO+V5}Zl0rhoNq5Sn7OtdVec|0 zZyR0{W-K-8w`A?knW^^Gdb@`p+yBId9f$!wJBhg9S2jlQ{pZs0Pdw?7Hn>^VK z%tC!N`)%Z@aqREPOI6lqy|;|(CAJHF!h`6{2fGBM**7ELHNg9Yx9%kD!})sAMDmj7 zmh`0;uXz<8x(EJSNN?2dt-Y85~)iAX(q39o||^xCpHjKqHB5M?Cxqf2fq zPzmm^tdT>Z%0lXN|DvkD`RbF~G{L|HeXIYy`o^1w0cogO#&!@F0Lb7LK4kqyz-?Km zSPOk{- Date: Mon, 25 May 2026 01:54:58 +0200 Subject: [PATCH 03/11] editor --- src/main.py | 103 +++++++++++++++++++++++++++++++++------------ src/ui/__init__.py | 4 +- src/ui/editor.py | 64 ++++++++++++++++------------ 3 files changed, 115 insertions(+), 56 deletions(-) diff --git a/src/main.py b/src/main.py index 96205d2..83744ba 100644 --- a/src/main.py +++ b/src/main.py @@ -5,28 +5,37 @@ from pathlib import Path import streamlit as st from streamlit_tree_select import tree_select -# project path fix -project_root = os.path.abspath(os.path.dirname(__file__)) -if project_root not in sys.path: - sys.path.insert(0, project_root) +# ---------------- PATH FIX ---------------- +sys.path.append(os.path.dirname(os.path.abspath(__file__))) -# backend +# ---------------- BACKEND ---------------- from backend.file_manager import FileManager -from ui.navigation import FileNavigation -# your existing modules (keep yours as-is) -from backend.chat_manager import ChatManager -from backend.execution_engine import ExecutionEngine -from ui.editor import CodeEditor +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 ---------------- +st.set_page_config( + page_title="AI Code Editor", + page_icon="💻", + layout="wide" +) + + + +# ---------------- SIDEBAR ---------------- def render_sidebar(): + st.sidebar.title("📁 File Explorer") navigation = FileNavigation() - tree_data = navigation.build_tree() + tree_data = navigation.build_tree(Path.cwd()) with st.sidebar: selection = tree_select( @@ -41,47 +50,87 @@ def render_sidebar(): return None +# ---------------- MAIN ---------------- def main(): - st.set_page_config( - page_title="AI Code Editor", - page_icon="💻", - layout="wide" - ) file_manager = FileManager() chat_manager = ChatManager() execution_engine = ExecutionEngine() + # ---------------- STATE ---------------- if "selected_file" not in st.session_state: st.session_state.selected_file = None - # --- SIDEBAR --- + 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 = "" + + # ---------------- FILE SELECT ---------------- selected_file = render_sidebar() if selected_file: st.session_state.selected_file = selected_file + current_file = st.session_state.selected_file + + + # ---------------- FILE SWITCH ---------------- + if current_file != st.session_state.active_file: + + st.session_state.active_file = current_file + + 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 = "" + + st.sidebar.markdown("---") - st.sidebar.caption("AISE501 – AI in Software Engineering") + st.sidebar.caption("AI Code Editor") - # --- MAIN AREA --- - if st.session_state.selected_file: - content = file_manager.read_file(st.session_state.selected_file) + # ---------------- MAIN UI ---------------- + if st.session_state.active_file: col1, col2 = st.columns([3, 2]) with col1: - st.subheader(f"Editing: {Path(st.session_state.selected_file).name}") + + st.subheader(f"Editing: {Path(st.session_state.active_file).name}") editor = CodeEditor() - editor.display_code(content) - if st.button("💾 Save"): - file_manager.save_file(st.session_state.selected_file, content) - st.success("File saved!") + # IMPORTANT: only updates AFTER APPLY + new_value = editor.display_code( + st.session_state.editor_content, + filename=st.session_state.active_file + ) + + # ---------------- APPLY DETECTION ---------------- + # Apply only triggers when Streamlit reruns with new value + 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 via Apply ✔") with col2: + chat = ChatInterface() chat.display_chat(chat_manager) @@ -89,7 +138,7 @@ def main(): output.display_execution(execution_engine) else: - st.info("Select a file from the sidebar explorer 👈") + st.info("Select a file from the explorer 👈") if __name__ == "__main__": diff --git a/src/ui/__init__.py b/src/ui/__init__.py index 6003886..2314a11 100644 --- a/src/ui/__init__.py +++ b/src/ui/__init__.py @@ -3,4 +3,6 @@ from .navigation import FileNavigation from .editor import CodeEditor from .chat import ChatInterface -from .output import OutputDisplay \ No newline at end of file +from .output import OutputDisplay + +# Test \ No newline at end of file diff --git a/src/ui/editor.py b/src/ui/editor.py index 63f9ec2..026d07a 100644 --- a/src/ui/editor.py +++ b/src/ui/editor.py @@ -1,30 +1,38 @@ -import streamlit as st +from streamlit_ace import st_ace 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 get_code_content(self): - """ - Liest den aktuellen Code-Inhalt vom Editor - """ - return st.session_state.get("code_content", "") - - def update_code_content(self, new_content): - """ - Aktualisiert den Code-Inhalt im Session State - """ - st.session_state.code_content = new_content \ No newline at end of file + + def detect_language(self, filename): + + 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 display_code(self, code_content, filename=""): + + language = self.detect_language(filename) + + return st_ace( + value=code_content, + language=language, + theme="chrome", + key=f"ace_{filename}", + height=500, + font_size=14, + tab_size=4, + wrap=False, + auto_update=False, + show_gutter=True, + show_print_margin=False + ) \ No newline at end of file From da1d22ec56aede968a251ab0d952d20f4c8ef6a4 Mon Sep 17 00:00:00 2001 From: leart-ramushi <113895250+leart-ramushi@users.noreply.github.com> Date: Tue, 26 May 2026 16:17:50 +0200 Subject: [PATCH 04/11] provisorisches run --- src/backend/execution_engine.py | 51 +++++++++--- src/backend/file_manager.py | 1 + src/main.py | 132 +++++++++++++++++++------------- src/ui/editor.py | 1 - 4 files changed, 121 insertions(+), 64 deletions(-) diff --git a/src/backend/execution_engine.py b/src/backend/execution_engine.py index 67cfd1b..9ab6723 100644 --- a/src/backend/execution_engine.py +++ b/src/backend/execution_engine.py @@ -1,12 +1,41 @@ -class ExecutionEngine: - """ - Platzhalter für die Code-Ausführung [1] - """ - def __init__(self): - pass +import subprocess +import sys +from pathlib import Path - def execute_code(self): - """ - Führt Code nicht aus, sondern gibt einen Status aus [1] - """ - return "Code wurde nicht ausgeführt (Platzhalter)" \ No newline at end of file + +class ExecutionEngine: + def run_file(self, filepath: str) -> dict: + path = Path(filepath) + + if not path.exists(): + return {"stdout": "", "stderr": f"File not found: {filepath}", "returncode": -1} + + ext = path.suffix.lower() + if ext == ".py": + cmd = [sys.executable, str(path)] + elif ext == ".js": + cmd = ["node", str(path)] + elif ext == ".sh": + cmd = ["bash", str(path)] + else: + return {"stdout": "", "stderr": f"Unsupported file type: {ext}", "returncode": -1} + + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=30, + cwd=str(path.parent), + ) + return { + "stdout": result.stdout, + "stderr": result.stderr, + "returncode": result.returncode, + } + except subprocess.TimeoutExpired: + return {"stdout": "", "stderr": "Timed out after 30 seconds.", "returncode": -1} + except FileNotFoundError as e: + return {"stdout": "", "stderr": f"Runtime not found: {e}", "returncode": -1} + except Exception as e: + return {"stdout": "", "stderr": f"Error: {e}", "returncode": -1} \ No newline at end of file diff --git a/src/backend/file_manager.py b/src/backend/file_manager.py index 011b975..004a9f9 100644 --- a/src/backend/file_manager.py +++ b/src/backend/file_manager.py @@ -1,5 +1,6 @@ from pathlib import Path + class FileManager: """ Handles file reading and writing diff --git a/src/main.py b/src/main.py index 83744ba..bcdfb63 100644 --- a/src/main.py +++ b/src/main.py @@ -1,89 +1,109 @@ 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 ---------------- 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 ---------------- +# ---------------- PAGE CONFIG ---------------- st.set_page_config( page_title="AI Code Editor", page_icon="💻", - layout="wide" + 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") - st.sidebar.title("📁 File Explorer") - - navigation = FileNavigation() - tree_data = navigation.build_tree(Path.cwd()) + root_folder = st.session_state.get("root_folder") with st.sidebar: - selection = tree_select( - tree_data, - check_model="leaf", - only_leaf_checkboxes=True - ) + # --- Button on top --- + if st.button("📂 Choose Folder", key="pick_folder"): + picked = pick_folder() + if picked: + st.session_state.root_folder = picked + st.rerun() - if selection and selection.get("checked"): - return selection["checked"][0] + if root_folder: + st.caption(f" {Path(root_folder).name}") + + st.markdown("---") + + # --- Tree below --- + 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 - # ---------------- MAIN ---------------- def main(): - file_manager = FileManager() chat_manager = ChatManager() execution_engine = ExecutionEngine() # ---------------- 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 "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 # ---------------- FILE SELECT ---------------- selected_file = render_sidebar() - if selected_file: st.session_state.selected_file = selected_file 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 # clear output on file switch if current_file: content = file_manager.read_file(current_file) st.session_state.editor_content = content @@ -91,55 +111,63 @@ def main(): else: st.session_state.editor_content = "" - st.sidebar.markdown("---") st.sidebar.caption("AI Code Editor") - # ---------------- MAIN UI ---------------- if st.session_state.active_file: - col1, col2 = st.columns([3, 2]) with col1: - - st.subheader(f"Editing: {Path(st.session_state.active_file).name}") + # --- File name + Run button on same row --- + title_col, btn_col = st.columns([5, 1]) + with title_col: + st.subheader(f"✏️ {Path(st.session_state.active_file).name}") + with btn_col: + run_clicked = st.button("▶ Run", type="primary", use_container_width=True) editor = CodeEditor() - - # IMPORTANT: only updates AFTER APPLY new_value = editor.display_code( st.session_state.editor_content, filename=st.session_state.active_file ) - - # ---------------- APPLY DETECTION ---------------- - # Apply only triggers when Streamlit reruns with new value 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 ✔") - file_manager.save_file( - st.session_state.active_file, - new_value + # --- Run execution --- + if run_clicked: + with st.spinner("Running…"): + st.session_state.run_result = execution_engine.run_file( + st.session_state.active_file ) - st.success("Saved via Apply ✔") - with col2: - chat = ChatInterface() chat.display_chat(chat_manager) - output = OutputDisplay() - output.display_execution(execution_engine) + # --- Output panel --- + result = st.session_state.run_result + if result is not None: + st.markdown("---") + success = result["returncode"] == 0 + status = "✅ Exited 0" if success else f"❌ Exited {result['returncode']}" + st.caption(f"Output — {status}") + + if result["stdout"]: + st.code(result["stdout"], language="text") + + if result["stderr"]: + st.code(result["stderr"], language="text") + + if not result["stdout"] and not result["stderr"]: + st.caption("No output.") else: - st.info("Select a file from the explorer 👈") - + st.info("Select a file from the explorer") if __name__ == "__main__": main() \ No newline at end of file diff --git a/src/ui/editor.py b/src/ui/editor.py index 026d07a..6798a03 100644 --- a/src/ui/editor.py +++ b/src/ui/editor.py @@ -28,7 +28,6 @@ class CodeEditor: language=language, theme="chrome", key=f"ace_{filename}", - height=500, font_size=14, tab_size=4, wrap=False, From f589e22e4e8d9e06b488aeb7b9bd6763b042fb68 Mon Sep 17 00:00:00 2001 From: leart-ramushi <113895250+leart-ramushi@users.noreply.github.com> Date: Tue, 26 May 2026 16:36:09 +0200 Subject: [PATCH 05/11] debug_logger and execution update --- src/backend/debug_logger.py | 74 +++++++++++++++++++++++++++--- src/backend/execution_engine.py | 78 +++++++++++++++++++++++++++----- src/main.py | 79 ++++++++++++++++++++++++--------- 3 files changed, 192 insertions(+), 39 deletions(-) diff --git a/src/backend/debug_logger.py b/src/backend/debug_logger.py index d45e9a2..00be55c 100644 --- a/src/backend/debug_logger.py +++ b/src/backend/debug_logger.py @@ -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): - return self.logs \ No newline at end of file + # ---------------- 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) \ No newline at end of file diff --git a/src/backend/execution_engine.py b/src/backend/execution_engine.py index 9ab6723..2bc891c 100644 --- a/src/backend/execution_engine.py +++ b/src/backend/execution_engine.py @@ -1,25 +1,62 @@ +import ast import subprocess import sys from pathlib import Path +from backend.debug_logger import DebugLogger + class ExecutionEngine: + def __init__(self): + self.logger = DebugLogger() + + # ---------------- SYNTAX CHECK ---------------- + def check_syntax(self, filepath: str) -> bool: + """Parse the file with ast. Returns True if valid, False if not.""" + path = Path(filepath) + if path.suffix.lower() != ".py": + return True # skip non-Python files + + 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) + # --- guards --- if not path.exists(): - return {"stdout": "", "stderr": f"File not found: {filepath}", "returncode": -1} + self.logger.log_stderr(f"File not found: {filepath}") + return self._result("", f"File not found: {filepath}", -1) ext = path.suffix.lower() if ext == ".py": + # syntax check before running + if not self.check_syntax(filepath): + err = self.logger.format() + return self._result("", err, -1) cmd = [sys.executable, str(path)] elif ext == ".js": cmd = ["node", str(path)] elif ext == ".sh": cmd = ["bash", str(path)] else: - return {"stdout": "", "stderr": f"Unsupported file type: {ext}", "returncode": -1} + msg = f"Unsupported file type: {ext}" + self.logger.log_stderr(msg) + return self._result("", msg, -1) + # --- execute --- + self.logger.log(f"Running: {path.name}") try: result = subprocess.run( cmd, @@ -28,14 +65,35 @@ class ExecutionEngine: timeout=30, cwd=str(path.parent), ) - return { - "stdout": result.stdout, - "stderr": result.stderr, - "returncode": result.returncode, - } + 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: - return {"stdout": "", "stderr": "Timed out after 30 seconds.", "returncode": -1} + msg = "Timed out after 30 seconds." + self.logger.log_stderr(msg) + return self._result("", msg, -1) except FileNotFoundError as e: - return {"stdout": "", "stderr": f"Runtime not found: {e}", "returncode": -1} + msg = f"Runtime not found: {e}" + self.logger.log_stderr(msg) + return self._result("", msg, -1) except Exception as e: - return {"stdout": "", "stderr": f"Error: {e}", "returncode": -1} \ No newline at end of file + 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 \ No newline at end of file diff --git a/src/main.py b/src/main.py index bcdfb63..65eb1bf 100644 --- a/src/main.py +++ b/src/main.py @@ -19,7 +19,7 @@ from backend.execution_engine import ExecutionEngine st.set_page_config( page_title="AI Code Editor", page_icon="💻", - layout="wide", + layout="wide" ) # ---------------- FOLDER PICKER ---------------- @@ -38,7 +38,6 @@ def render_sidebar(): root_folder = st.session_state.get("root_folder") with st.sidebar: - # --- Button on top --- if st.button("📂 Choose Folder", key="pick_folder"): picked = pick_folder() if picked: @@ -50,7 +49,6 @@ def render_sidebar(): st.markdown("---") - # --- Tree below --- if root_folder: try: root_path = Path(root_folder) @@ -73,6 +71,58 @@ def render_sidebar(): 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.") + # ---------------- MAIN ---------------- def main(): file_manager = FileManager() @@ -103,7 +153,7 @@ def main(): # ---------------- FILE SWITCH ---------------- if current_file != st.session_state.active_file: st.session_state.active_file = current_file - st.session_state.run_result = None # clear output on file switch + st.session_state.run_result = None if current_file: content = file_manager.read_file(current_file) st.session_state.editor_content = content @@ -119,7 +169,6 @@ def main(): col1, col2 = st.columns([3, 2]) with col1: - # --- File name + Run button on same row --- title_col, btn_col = st.columns([5, 1]) with title_col: st.subheader(f"✏️ {Path(st.session_state.active_file).name}") @@ -138,7 +187,6 @@ def main(): file_manager.save_file(st.session_state.active_file, new_value) st.success("Saved ✔") - # --- Run execution --- if run_clicked: with st.spinner("Running…"): st.session_state.run_result = execution_engine.run_file( @@ -149,23 +197,10 @@ def main(): chat = ChatInterface() chat.display_chat(chat_manager) - # --- Output panel --- - result = st.session_state.run_result - if result is not None: + if st.session_state.run_result is not None: st.markdown("---") - success = result["returncode"] == 0 - - status = "✅ Exited 0" if success else f"❌ Exited {result['returncode']}" - st.caption(f"Output — {status}") - - if result["stdout"]: - st.code(result["stdout"], language="text") - - if result["stderr"]: - st.code(result["stderr"], language="text") - - if not result["stdout"] and not result["stderr"]: - st.caption("No output.") + st.caption("Output") + render_output(st.session_state.run_result) else: st.info("Select a file from the explorer") From e3d2fe87b52c5e4e76c97f47c2ff165eeccbc0f7 Mon Sep 17 00:00:00 2001 From: leart-ramushi <113895250+leart-ramushi@users.noreply.github.com> Date: Tue, 26 May 2026 17:14:46 +0200 Subject: [PATCH 06/11] outsource to ui --- src/main.py | 116 ++++--------------------------------------- src/ui/navigation.py | 72 +++++++++++++++++++++++---- src/ui/output.py | 85 +++++++++++++++++-------------- 3 files changed, 122 insertions(+), 151 deletions(-) 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 From 49745349f6e84348b3a8cf2cef17d1daaa222b5e Mon Sep 17 00:00:00 2001 From: leart-ramushi <113895250+leart-ramushi@users.noreply.github.com> Date: Tue, 26 May 2026 17:20:15 +0200 Subject: [PATCH 07/11] added header back --- src/main.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/main.py b/src/main.py index 51df815..6a94b3e 100644 --- a/src/main.py +++ b/src/main.py @@ -20,13 +20,6 @@ st.set_page_config( layout="wide" ) -st.markdown(""" - -""", unsafe_allow_html=True) - # ---------------- MAIN ---------------- def main(): file_manager = FileManager() From 6506d366fc541ee9c649c595d69d80afcd735fd2 Mon Sep 17 00:00:00 2001 From: leart-ramushi <113895250+leart-ramushi@users.noreply.github.com> Date: Tue, 26 May 2026 18:01:31 +0200 Subject: [PATCH 08/11] chat window 1 --- src/main.py | 147 +++++++++++++++++++++++++++++++++---------- src/ui/navigation.py | 16 +++-- 2 files changed, 124 insertions(+), 39 deletions(-) diff --git a/src/main.py b/src/main.py index 6a94b3e..b8d2484 100644 --- a/src/main.py +++ b/src/main.py @@ -20,6 +20,87 @@ st.set_page_config( layout="wide" ) +st.markdown(""" """, unsafe_allow_html=True) + +# ---------------- CHAT PANEL INJECTION ---------------- +def inject_chat_panel(is_open: bool): + st.iframe(f""" + + """, height=1) + # ---------------- MAIN ---------------- def main(): file_manager = FileManager() @@ -41,12 +122,17 @@ def main(): 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 - # ---------------- FILE SELECT ---------------- + # ---------------- SIDEBAR ---------------- selected_file = navigation.render_sidebar() if selected_file: st.session_state.selected_file = selected_file + st.sidebar.markdown("---") + st.sidebar.caption("AI Code Editor") + current_file = st.session_state.selected_file # ---------------- FILE SWITCH ---------------- @@ -60,46 +146,39 @@ def main(): else: st.session_state.editor_content = "" - st.sidebar.markdown("---") - st.sidebar.caption("AI Code Editor") + inject_chat_panel(st.session_state.chat_open) # ---------------- MAIN UI ---------------- if st.session_state.active_file: - col1, col2 = st.columns([3, 2]) + title_col, btn_col = st.columns([5, 1]) + with title_col: + st.subheader(f"✏️ {Path(st.session_state.active_file).name}") + with btn_col: + run_clicked = st.button("▶ Run", type="primary", use_container_width=True) - with col1: - title_col, btn_col = st.columns([5, 1]) - with title_col: - st.subheader(f"✏️ {Path(st.session_state.active_file).name}") - with btn_col: - run_clicked = st.button("▶ Run", type="primary", use_container_width=True) + editor = CodeEditor() + new_value = editor.display_code( + st.session_state.editor_content, + filename=st.session_state.active_file + ) + 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 ✔") - editor = CodeEditor() - new_value = editor.display_code( - st.session_state.editor_content, - filename=st.session_state.active_file - ) - 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 ✔") + if run_clicked: + with st.spinner("Running…"): + st.session_state.run_result = execution_engine.run_file( + st.session_state.active_file + ) - 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) - with col2: - chat = ChatInterface() - chat.display_chat(chat_manager) - - 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") diff --git a/src/ui/navigation.py b/src/ui/navigation.py index 5722b65..0ef19dd 100644 --- a/src/ui/navigation.py +++ b/src/ui/navigation.py @@ -57,17 +57,23 @@ class FileNavigation: # ---------------- SIDEBAR RENDERER ---------------- def render_sidebar(self) -> str | None: """ - Renders the full sidebar: folder button, folder caption, divider, and tree. + Renders the full sidebar: folder button, 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 + 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.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() if root_folder: From 2f0a61e854a129b39f5dcc0f64f6fc66b5bbff88 Mon Sep 17 00:00:00 2001 From: leart-ramushi <113895250+leart-ramushi@users.noreply.github.com> Date: Tue, 26 May 2026 18:15:59 +0200 Subject: [PATCH 09/11] outsource --- src/main.py | 104 +++++++---------------------------------- src/ui/chat.py | 119 +++++++++++++++++++++++++++++++++++++---------- src/ui/editor.py | 45 ++++++++++++------ 3 files changed, 143 insertions(+), 125 deletions(-) diff --git a/src/main.py b/src/main.py index b8d2484..77b2398 100644 --- a/src/main.py +++ b/src/main.py @@ -20,91 +20,22 @@ st.set_page_config( layout="wide" ) -st.markdown(""" """, unsafe_allow_html=True) - -# ---------------- CHAT PANEL INJECTION ---------------- -def inject_chat_panel(is_open: bool): - st.iframe(f""" - - """, height=1) +st.markdown(""" + +""", unsafe_allow_html=True) # ---------------- MAIN ---------------- def main(): file_manager = FileManager() navigation = FileNavigation() + chat = ChatInterface() chat_manager = ChatManager() execution_engine = ExecutionEngine() output_display = OutputDisplay() @@ -146,21 +77,19 @@ def main(): else: st.session_state.editor_content = "" - inject_chat_panel(st.session_state.chat_open) + chat.inject_panel(st.session_state.chat_open) # ---------------- MAIN UI ---------------- if st.session_state.active_file: - title_col, btn_col = st.columns([5, 1]) - with title_col: - st.subheader(f"✏️ {Path(st.session_state.active_file).name}") - with btn_col: - run_clicked = st.button("▶ Run", type="primary", use_container_width=True) + st.subheader(f"✏️ {Path(st.session_state.active_file).name}") editor = CodeEditor() - new_value = editor.display_code( + 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 @@ -168,6 +97,7 @@ def main(): 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( diff --git a/src/ui/chat.py b/src/ui/chat.py index a5711fd..c88dcc8 100644 --- a/src/ui/chat.py +++ b/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 = [] - + + # ---------------- 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""" + + """, height=1) + + # ---------------- CHAT DISPLAY ---------------- def display_chat(self, chat_manager): - """ - Zeigt die Chat-Schnittstelle mit Konversationsverlauf [1] - """ - st.title("🤖 KI-Assistent") - - # Chat History anzeigen + """Renders the Streamlit chat interface (used when not in panel mode).""" + st.title("AI Assistant") + 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() \ No newline at end of file diff --git a/src/ui/editor.py b/src/ui/editor.py index 6798a03..ec7d1b8 100644 --- a/src/ui/editor.py +++ b/src/ui/editor.py @@ -1,29 +1,44 @@ from streamlit_ace import st_ace +import streamlit as st + class CodeEditor: - def detect_language(self, filename): - + def detect_language(self, filename: str) -> str: mapping = { - ".py": "python", - ".js": "javascript", + ".py": "python", + ".js": "javascript", ".html": "html", - ".css": "css", + ".css": "css", ".json": "json", - ".md": "markdown", + ".md": "markdown", } - for ext, lang in mapping.items(): if filename.endswith(ext): return lang - return "text" - def display_code(self, code_content, filename=""): - + 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) + then the ACE editor below. + Returns (new_value, run_clicked) where new_value is None until Apply is hit. + """ language = self.detect_language(filename) - return st_ace( + # --- 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", + key=f"run_{filename}", + type="primary", + use_container_width=True + ) + + new_value = st_ace( value=code_content, language=language, theme="chrome", @@ -31,7 +46,9 @@ class CodeEditor: font_size=14, tab_size=4, wrap=False, - auto_update=False, + auto_update=False, show_gutter=True, - show_print_margin=False - ) \ No newline at end of file + show_print_margin=False, + ) + + return new_value, run_clicked \ No newline at end of file From 2e7e2b4ff955f0fe85401339c8f25dee759166fd Mon Sep 17 00:00:00 2001 From: leart-ramushi <113895250+leart-ramushi@users.noreply.github.com> Date: Tue, 26 May 2026 18:23:40 +0200 Subject: [PATCH 10/11] cleaner debug log --- src/main.py | 1 - src/ui/navigation.py | 5 +---- src/ui/output.py | 4 ++-- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/main.py b/src/main.py index 77b2398..95d0179 100644 --- a/src/main.py +++ b/src/main.py @@ -62,7 +62,6 @@ def main(): st.session_state.selected_file = selected_file st.sidebar.markdown("---") - st.sidebar.caption("AI Code Editor") current_file = st.session_state.selected_file diff --git a/src/ui/navigation.py b/src/ui/navigation.py index 0ef19dd..1d7b771 100644 --- a/src/ui/navigation.py +++ b/src/ui/navigation.py @@ -76,9 +76,6 @@ class FileNavigation: st.session_state.chat_open = not st.session_state.get("chat_open", False) st.rerun() - if root_folder: - st.caption(f" {Path(root_folder).name}") - st.markdown("---") if root_folder: @@ -98,6 +95,6 @@ class FileNavigation: except Exception: st.error("Could not load folder.") else: - st.info("No folder selected yet.") + st.text("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 b06b43d..05bc61c 100644 --- a/src/ui/output.py +++ b/src/ui/output.py @@ -37,8 +37,8 @@ class OutputDisplay: icon = icons.get(e["level"], "•") lines.append( f'[{e["time"]}] ' - f'{icon} {e["level"]} ' - f'{e["message"]}' + f'{icon} {e["level"]} : ' + f'{e["message"]}' ) st.markdown("
".join(lines), unsafe_allow_html=True) From 4c5cbfa03536901451f7e05638fa780ad620823c Mon Sep 17 00:00:00 2001 From: leart-ramushi <113895250+leart-ramushi@users.noreply.github.com> Date: Tue, 26 May 2026 20:58:25 +0200 Subject: [PATCH 11/11] file explorer, execution engine und editor --- .streamlit/config.toml | 6 ++ requirements.txt | Bin 4426 -> 4494 bytes src/backend/execution_engine.py | 131 ++++++++++++++++++++++++++---- src/backend/file_manager.py | 55 ++++++++++++- src/ui/editor.py | 2 +- src/ui/navigation.py | 140 ++++++++++++++++++++++++++------ 6 files changed, 287 insertions(+), 47 deletions(-) create mode 100644 .streamlit/config.toml diff --git a/.streamlit/config.toml b/.streamlit/config.toml new file mode 100644 index 0000000..c369fe4 --- /dev/null +++ b/.streamlit/config.toml @@ -0,0 +1,6 @@ +[theme] +base = "light" +primaryColor = "#4A90D9" +backgroundColor = "#ffffff" +secondaryBackgroundColor = "#f5f5f5" +textColor = "#1a1a1a" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 2817cb1058a85e24574ca29e95d3588ae0fc7fc8..497c1bc3de85a47f80d42ad724d7dd0b46d680f7 100644 GIT binary patch delta 52 zcmX@5)Tg{bi*NEDUJhklhGd3(hFpdMAeqOI3dAK0#SFGUXuzPyU<|}Yn+5qSnE_-u B3poG) delta 12 TcmeBEKBcrli*Iutp8_)g9q9wu diff --git a/src/backend/execution_engine.py b/src/backend/execution_engine.py index 2bc891c..9469d30 100644 --- a/src/backend/execution_engine.py +++ b/src/backend/execution_engine.py @@ -5,18 +5,102 @@ 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: def __init__(self): self.logger = DebugLogger() + # ---------------- SAFETY CHECK ---------------- + def _check_safe(self, filepath: str) -> None: + """ + Raises SafetyError if the file contains dangerous patterns. + Only runs on .py files (other types are blocked outright). + """ + 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: - """Parse the file with ast. Returns True if valid, False if not.""" path = Path(filepath) if path.suffix.lower() != ".py": - return True # skip non-Python files - + return True try: source = path.read_text(encoding="utf-8") ast.parse(source, filename=str(path)) @@ -34,27 +118,43 @@ class ExecutionEngine: self.logger.clear() path = Path(filepath) - # --- guards --- + # --- 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() - if ext == ".py": - # syntax check before running - if not self.check_syntax(filepath): - err = self.logger.format() - return self._result("", err, -1) - cmd = [sys.executable, str(path)] - elif ext == ".js": - cmd = ["node", str(path)] - elif ext == ".sh": - cmd = ["bash", str(path)] - else: + + # --- 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: @@ -68,7 +168,6 @@ class ExecutionEngine: 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: diff --git a/src/backend/file_manager.py b/src/backend/file_manager.py index 004a9f9..36bfec8 100644 --- a/src/backend/file_manager.py +++ b/src/backend/file_manager.py @@ -3,23 +3,70 @@ from pathlib import Path class FileManager: """ - Handles file reading and writing + Handles all filesystem operations: read, write, create, delete, and change detection. """ def __init__(self, root_path="."): self.root_path = Path(root_path).resolve() - def read_file(self, file_path): + SUPPORTED_EXTENSIONS = {'.py', '.js', '.html', '.css', '.json', '.md'} + + # ---------------- READ / WRITE ---------------- + def read_file(self, file_path: str) -> str: try: with open(file_path, "r", encoding="utf-8") as f: return f.read() except Exception: return "" - def save_file(self, file_path, content): + def save_file(self, file_path: str, content: str) -> bool: try: with open(file_path, "w", encoding="utf-8") as f: f.write(content) return True except Exception: - return False \ No newline at end of file + 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() \ No newline at end of file diff --git a/src/ui/editor.py b/src/ui/editor.py index ec7d1b8..24b391b 100644 --- a/src/ui/editor.py +++ b/src/ui/editor.py @@ -32,7 +32,7 @@ class CodeEditor: st.caption(f" {filename.split('/')[-1] or filename}" if filename else "") with run_col: run_clicked = st.button( - "▶ Run", + "▶ RUN CODE", key=f"run_{filename}", type="primary", use_container_width=True diff --git a/src/ui/navigation.py b/src/ui/navigation.py index 1d7b771..9a328d1 100644 --- a/src/ui/navigation.py +++ b/src/ui/navigation.py @@ -2,21 +2,23 @@ import tkinter as tk from tkinter import filedialog from pathlib import Path import streamlit as st -from streamlit_tree_select import tree_select +import streamlit_antd_components as sac + +from backend.file_manager import FileManager class FileNavigation: """ - Builds a VS Code-like file tree for streamlit_tree_select - and handles folder picking. + 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 # ---------------- 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) @@ -26,18 +28,12 @@ class FileNavigation: # ---------------- 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) -> dict: + def build_node(current_path: Path) -> sac.TreeItem: is_dir = current_path.is_dir() - node = { - "label": ("📁 " if is_dir else "📄 ") + current_path.name, - "value": str(current_path), - } if is_dir: children = [] try: @@ -49,20 +45,92 @@ class FileNavigation: children.append(build_node(item)) except PermissionError: pass - node["children"] = children - return node + 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: - """ - Renders the full sidebar: folder button, caption, divider, and tree. - Returns the selected file path or 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: @@ -70,6 +138,7 @@ class FileNavigation: 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): @@ -79,22 +148,41 @@ class FileNavigation: 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) - selection = tree_select( - tree_data, - check_model="leaf", - only_leaf_checkboxes=True + + 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 selection and selection.get("checked"): - return selection["checked"][0] + + if selected: + return self._resolve_path(root_path, selected) else: st.warning("Folder not found.") - except Exception: - st.error("Could not load folder.") + 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 \ No newline at end of file