diff --git a/.gitignore b/.gitignore index ea5a08b..7309b3c 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,6 @@ htmlcov/ # Data data/processed/ data/raw/ + +# Workspace +workspace/ diff --git a/backend/managers/debug_logger.py b/backend/managers/debug_logger.py index e69de29..477dc8f 100644 --- a/backend/managers/debug_logger.py +++ b/backend/managers/debug_logger.py @@ -0,0 +1,9 @@ +class DebugLogger: + def __init__(self): + self.logs = [] + + def log(self, message): + self.logs.append(message) + + def get_logs(self): + return self.logs \ No newline at end of file diff --git a/backend/managers/execution_engine.py b/backend/managers/execution_engine.py index e69de29..0b1bd92 100644 --- a/backend/managers/execution_engine.py +++ b/backend/managers/execution_engine.py @@ -0,0 +1,41 @@ +import subprocess +from pathlib import Path + +RUN_TIMEOUT = 30 # seconds + +class ExecutionEngine: + def __init__(self): + pass + + def run_code(self, active_file: Path) -> dict: + suffix = active_file.suffix + current_dir = active_file.parent.resolve() + + if suffix == ".py": + cmd = ["py", active_file.name] + elif suffix == ".tex": + cmd = [ + "pdflatex", + "-interaction=nonstopmode", + f"-output-directory={current_dir}", + active_file.name, + ] + else: + return {"stdout": "", "stderr": f"Unsupported file type: {suffix}", "rc": 1} + + try: + proc = subprocess.run( + cmd, + cwd=current_dir, + capture_output=True, + text=True, + timeout=RUN_TIMEOUT, + ) + return {"stdout": proc.stdout, "stderr": proc.stderr, "rc": proc.returncode} + + except subprocess.TimeoutExpired: + return {"stdout": "", "stderr": f"Timed out after {RUN_TIMEOUT}s", "rc": -1} + except FileNotFoundError as e: + return {"stdout": "", "stderr": str(e), "rc": -1} + except Exception as e: + return {"stdout": "", "stderr": str(e), "rc": -1} \ No newline at end of file diff --git a/backend/managers/file_manager.py b/backend/managers/file_manager.py index e392dfc..a25e8d8 100644 --- a/backend/managers/file_manager.py +++ b/backend/managers/file_manager.py @@ -5,13 +5,150 @@ WORKSPACE = Path("workspace") WORKSPACE.mkdir(exist_ok=True) class FileManager: - def __init__(self, base_path=Path("workspace")): + def __init__(self, base_path=Path("workspace")) -> None: self.base_path = Path(base_path) self.base_path.mkdir(exist_ok=True) - # read_file content + def create_folder(self, relative_path: str, name: str) -> bool: + if not name: + st.error(f"Invalid folder name: {name}") + return False + + if "/" in name or "\\" in name: + st.error(f"Invalid folder name (no slashes allowed): {name}") + return False + + name = Path(name) + if relative_path: + relative_path = Path(relative_path) + else: + relative_path = Path() - # save_file content + folder_path = (self.base_path / relative_path / name).resolve() + + if not str(folder_path).startswith(str(self.base_path.resolve())): + st.error(f"Access denied: {relative_path}") + return False + + try: + folder_path.mkdir(exist_ok=False) + return True + except FileExistsError: + st.warning(f"Folder already exists: {relative_path}") + return False + except Exception as e: + st.error(f"Error creating folder {relative_path}: {str(e)}") + return False + + def create_file(self, relative_path: str, name: str) -> bool: + if not name or name.strip() == "" : + st.error(f"Invalid file name: {name}") + return False + + name = Path(name) + if not name.suffix: + name = name.with_suffix(".txt") # Default to .txt if no extension provided + + if relative_path: + relative_path = Path(relative_path) + else: + relative_path = Path() + + file_path = (self.base_path / relative_path / name).resolve() + + if not str(file_path).startswith(str(self.base_path.resolve())): + st.error(f"Access denied: {relative_path}") + return False + + try: + file_path.touch(exist_ok=False) + return True + except FileExistsError: + st.warning(f"File already exists: {relative_path}") + return False + except Exception as e: + st.error(f"Error creating file {relative_path}: {str(e)}") + return False + + def read_file(self, relative_path: Path) -> str: + file_path = (relative_path).resolve() + + if not file_path.exists(): + st.error(f"File not found: {relative_path}") + return "" + if not file_path.is_file(): + st.error(f"Path is not a file: {relative_path}") + return "" + if not str(file_path).startswith(str(self.base_path.resolve())): + st.error(f"Access denied: {relative_path}") + return "" + + try: + with open(file_path, "r") as f: + return f.read() + except FileNotFoundError: + st.error(f"File not found: {relative_path}") + return "" + except Exception as e: + st.error(f"Error reading file {relative_path}: {str(e)}") + return "" + + def save_file(self, relative_path: str, content: str): + file_path = (Path(relative_path)).resolve() + + if not str(file_path).startswith(str(self.base_path.resolve())): + st.error(f"Access denied: {relative_path}") + return False + + try: + with open(file_path, "w") as f: + f.write(content) + return True + except Exception as e: + st.error(f"Error saving file {relative_path}: {str(e)}") + return False + + def rename_file(self, old_relative_path: str, new_name: str) -> bool: + if not new_name or new_name.strip() == "": + st.error(f"Invalid file name: {new_name}") + return False + + file_type = Path(old_relative_path).suffix + new_name = Path(new_name) + + if not Path(new_name).suffix == file_type: + new_name = Path(new_name).with_suffix(file_type) # Ensure the file extension remains the same + + old_file_path = (self.base_path / Path(old_relative_path)).resolve() + new_file_path = old_file_path.parent / new_name + + if not str(old_file_path).startswith(str(self.base_path.resolve())) or not str(new_file_path).startswith(str(self.base_path.resolve())): + st.error(f"Access denied: {old_relative_path}") + return False + + try: + old_file_path.rename(new_file_path) + return True + except FileNotFoundError: + st.error(f"File not found: {old_relative_path}") + return False + + def delete_file(self, relative_path): + file_path = (self.base_path / relative_path).resolve() + + if not str(file_path).startswith(str(self.base_path.resolve())): + st.error(f"Access denied: {relative_path}") + return False + + try: + file_path.unlink() + return True + except FileNotFoundError: + st.error(f"File not found: {relative_path}") + return False + except Exception as e: + st.error(f"Error deleting file {relative_path}: {str(e)}") + return False def get_file_tree(self): def build_tree(path: Path): diff --git a/frontend/editor.py b/frontend/editor.py index 7a22c59..79f9b98 100644 --- a/frontend/editor.py +++ b/frontend/editor.py @@ -1,15 +1,43 @@ import streamlit as st import streamlit_ace as st_ace from pathlib import Path -from backend.managers.file_manager import FileManager -def render_editor(): - LANG_MAP = { +from backend.managers.file_manager import FileManager +from backend.managers.execution_engine import ExecutionEngine +from backend.managers.debug_logger import DebugLogger + +LANG_MAP = { ".py": "python", ".tex": "latex", ".js": "javascript", ".html": "html", ".css": "css", ".sh": "bash", ".json": "json", ".yaml": "yaml", ".yml": "yaml" } +def run_active_file(): + active_file = st.session_state.active_file + + if not active_file: + st.warning("No active file to run.") + return + + execution_engine = ExecutionEngine() + debug_logger = DebugLogger() + + debug_logger.log(f"Executing code from {active_file}...") + + with st.spinner(f"Running {Path(active_file).name}..."): + output = execution_engine.run_code(Path(active_file)) + + debug_logger.log("Execution completed.") + + st.session_state.code_execution_output = { + "stdout": output["stdout"], + "stderr": output["stderr"], + "return_code": output["rc"] + } + result = st.session_state.code_execution_output + return result + +def render_editor(): st.subheader("Code Editor") if not st.session_state.open_files: @@ -26,26 +54,34 @@ def render_editor(): st.session_state.active_file = file_path if file_path not in st.session_state.files_content: - st.session_state.files_content[file_path] = fm.read_file(file_path) + st.session_state.files_content[file_path] = fm.read_file(Path(file_path)) - file_language = LANG_MAP.get(file_path.suffix, "text") + file_language = LANG_MAP.get(Path(file_path).suffix, "text") - code = st_ace( + code = st_ace.st_ace( value=st.session_state.files_content[file_path], language=file_language, theme="monokai", - key=f"code_editor_{file_path}" + key=f"code_editor_{file_path}", + auto_update=True, + height=400, + tab_size=4, + font_size=14, + show_gutter=True, + show_print_margin=False, + wrap=True ) st.session_state.files_content[file_path] = code - col1, col2 = st.columns([1, 1]) - with col1: + cols = st.columns([1, 1, 1, 1]) + with cols[0]: if st.button("Save Changes", key=f"save_{file_path}"): - fm.save_file(file_path, st.session_state.files_content[file_path]) - st.success("File saved successfully!") + content = st.session_state.files_content[file_path] + if fm.save_file(file_path, content): + st.success("File saved successfully!") - with col2: + with cols[1]: if st.button("Close File", key=f"close_{file_path}"): st.session_state.open_files.remove(file_path) del st.session_state.files_content[file_path] @@ -59,6 +95,58 @@ def render_editor(): ) st.rerun() # Refresh the page to update the UI + with cols[2]: + if st.button("Rename File", key=f"rename_{file_path}"): + new_name = st.text_input("New File Name", key=f"new_name_{file_path}") + if "/" in new_name or "\\" in new_name: + st.warning("Do not include slashes in names!") + elif new_name: + try: + fm.rename_file(file_path, new_name) + st.success(f"File renamed to '{new_name}' successfully!") + st.rerun() + except Exception as e: + st.error(f"Error renaming file: {e}") + + with cols[3]: + if st.button("Delete File", key=f"delete_{file_path}"): + try: + fm.delete_file(file_path) + st.success(f"File '{Path(file_path).name}' deleted successfully!") + st.rerun() + except Exception as e: + st.error(f"Error deleting file: {e}") + + if st.button("▶ Run Code", key="run_code"): + result = run_active_file() + if not result: + st.stop() + + st.subheader("Execution Output") + + if result["return_code"] == 0: + st.success(f"Exit code: {result['return_code']}") + else: + st.error(f"Exit code: {result['return_code']}") + + if result["stdout"]: + st.text_area( + "Standard Output", + value=result["stdout"], + height=200, + disabled=True, + key="run_stdout") + + if result["stderr"]: + st.text_area( + "Standard Error", + value=result["stderr"], + height=200, + disabled=True, + key="run_stderr") + if not result["stdout"] and not result["stderr"]: + st.info("No output produced by the code execution.") + if __name__ == "__main__": diff --git a/frontend/sidebar.py b/frontend/sidebar.py index 98dfd26..f0720d3 100644 --- a/frontend/sidebar.py +++ b/frontend/sidebar.py @@ -4,100 +4,168 @@ from backend.managers.file_manager import FileManager fm = FileManager() +SUFFIX_MAP = { + ".py": "🐍", # Python + ".js": "🟨", # JavaScript (Gelbes Quadrat/Logo) + ".html": "🌐", # HTML (Web) + ".css": "🎨", # CSS (Styling) + ".json": "📦", # JSON (Datenpaket) + ".yaml": "⚙️", # YAML (Konfiguration) + ".yml": "⚙️", # YAML + ".sh": "🐚", # Bash/Shell (Shell-Icon) + ".md": "📝", # Markdown + ".txt": "📄", # Text + ".tex": "📑", # LaTeX + ".c": "🔵", # C (Blaues Icon) + ".cpp": "🔷", # C++ + ".java": "☕", # Java + "folder": "📁", # Ordner + "default": "📄" # Unbekannt +} + def render_filetree(tree, parent_path=Path()): for name, content in sorted(tree.items()): full_path = parent_path / name if isinstance(content, dict): # Directory - with st.sidebar.expander(f"📁 {name}", expanded=False): - render_filetree(content, full_path) - else: # File - cols= st.sidebar.columns([3, 1]) - with cols[0]: - if st.sidebar.button(f"📄 {name}", key=str(full_path)): - abs_path = fm.base_path / full_path - - if str(abs_path) not in st.session_state.open_files: - st.session_state.open_files.append(str(abs_path)) - - st.session_state.active_file = str(abs_path) + disp_name = name if len(name) <= 13 else (name[:10] + "...") - with cols[1]: - show_options_key = f"show_options_{full_path}" - if st.button("...", key=show_options_key): - st.session_state[show_options_key] = not st.session_state.get(show_options_key, False) - - if st.session_state.get(show_options_key, False): - action = st.selectbox("", ["", "Rename", "Delete"], key=f"action_{full_path}", index=0) + with st.expander(f"📁 {disp_name}", expanded=False): + render_filetree(content, full_path) + with st.popover("+", key=f"popover_{full_path}"): + st.write(f"**Create in {name}**") + action = st.radio("Action", ["New File", "New Folder"], key=f"radio_{full_path}", label_visibility="collapsed") + new_name = st.text_input("Name", key=f"input_{full_path}") - if action == "Delete": - st.warning("Are you sure you want to delete this file?") - if st.button("Yes, delete file", key=f"confirm_delete_{full_path}"): - try: - (fm.base_path / full_path).unlink() - st.experimental_rerun() - except Exception as e: - st.error(f"Error deleting file: {e}") - - elif action == "Rename": - new_name = st.text_input("New Name", key=f"rename_{full_path}") + if st.button("Create", key=f"btn_{full_path}"): if "/" in new_name or "\\" in new_name: st.warning("Do not include slashes in names!") - elif new_name: - new_path = (fm.base_path / full_path.parent / new_name) - try: - (fm.base_path / full_path).rename(new_path) - st.experimental_rerun() - except Exception as e: - st.error(f"Error renaming file: {e}") + elif new_name and action == "New Folder": + fm.create_folder(str(full_path), new_name) + st.success(f"Folder {new_name} created!") + st.rerun() + elif new_name and action == "New File": + fm.create_file(str(full_path), new_name) + st.success(f"File {new_name} created!") + st.rerun() + + else: # File + suffix = Path(name).suffix + icon = SUFFIX_MAP.get(suffix, SUFFIX_MAP["default"]) + disp_name = name if len(name) <= 13 else (name[:10] + "...") + + col_file, col_opt = st.columns([0.85, 0.15], gap="small") + + with col_file: + if st.button(f"{icon} {disp_name}", key=str(full_path)): + abs_path = fm.base_path / full_path + + if str(abs_path) not in st.session_state.open_files: + st.session_state.open_files.append(str(abs_path)) + + st.session_state.active_file = str(abs_path) + st.rerun() + + with col_opt: + with st.popover("⋮", key=f"opt_{full_path}"): + + delete_key = f"confirm_delete_{full_path}" + if delete_key not in st.session_state: + st.session_state[delete_key] = False + + if st.button(" **Delete** 🗑️", key=f"del_{full_path}"): + st.session_state[delete_key] = True + + if st.session_state[delete_key]: + st.warning(f"Delete {name}?") + + confirm_col, deny_col = st.columns([1, 1], gap="small") + with confirm_col: + if st.button("✔", key=f"yes_{full_path}"): + try: + fm.delete_file(str(full_path)) + st.session_state[delete_key] = False + st.rerun() + except Exception as e: + st.error(f"Error deleting file: {e}") + with deny_col: + if st.button("✖", key=f"cancel_{full_path}"): + st.session_state[delete_key] = False + + rename_key = f"rename_mode_{full_path}" + if rename_key not in st.session_state: + st.session_state[rename_key] = False + + if st.button("**Rename** ✏️", key=f"ren_{full_path}"): + st.session_state[rename_key] = True + + if st.session_state[rename_key]: + new_name = st.text_input("New Name", key=f"input_ren_{full_path}") + + apply_col, cancel_col = st.columns([1, 1], gap="small") + with apply_col: + if st.button("Apply", key=f"apply_ren_{full_path}"): + try: + fm.rename_file(str(full_path), new_name) + st.session_state[rename_key] = False + st.rerun() + except Exception as e: + st.error(f"Error renaming file: {e}") + + with cancel_col: + if st.button("Cancel", key=f"cancel_ren_{full_path}"): + st.session_state[rename_key] = False def render_sidebar(): st.sidebar.title("Navigation") - navigation_section = st.sidebar.container() file_explorer_section = st.sidebar.container() + navigation_section = st.sidebar.container() + + with file_explorer_section: + st.subheader("File Explorer") + tree = fm.get_file_tree() + workspace = st.container() + add_more = st.container() + with workspace: + if not tree: + st.info("Workspace is empty.") + + else: + render_filetree(tree) + + with add_more: + with st.popover("⚙️ Explorer_Options", key=f"popover_options"): + action = st.radio( + "Action", + ["New File", "New Folder"], + key=f"radio_explorer_options", + label_visibility="collapsed") + + new_name = st.text_input("Name", key=f"input_explorer_options") + + if st.button("Create", key=f"btn_create_file_or_folder"): + if "/" in new_name or "\\" in new_name: + st.warning("Do not include slashes in names!") + elif new_name: + if action == "New Folder": + fm.create_folder("", new_name) + else: + fm.create_file("", new_name) + st.success(f"{action} created!") + st.rerun() + + st.sidebar.markdown("---") + with navigation_section: + st.sidebar.subheader("Options") options = ["Code Editor", "Chat with AI Assistant"] choice = [] for option in options: if st.sidebar.checkbox(option): choice.append(option) - - with file_explorer_section: - st.sidebar.subheader("File Explorer") - tree = fm.get_file_tree() - if not tree: - st.sidebar.info("Workspace is empty.") - else: - render_filetree(tree) - - st.sidebar.markdown("---") - st.sidebar.write("Manage Files & Folders") - - # Create Folder - new_folder_name = st.sidebar.text_input("New Folder Name", key="new_folder_name") - if "/" in new_folder_name or "\\" in new_folder_name: - st.warning("Do not include slashes in names!") - elif st.sidebar.button("Create Folder", key="create_folder") and new_folder_name: - new_folder_path = fm.base_path / new_folder_name - new_folder_path.mkdir(exist_ok=True) - st.success(f"Folder '{new_folder_name}' created successfully!") - st.experimental_rerun() - - # Create File - new_file_name = st.sidebar.text_input("New File Name", key="new_file_name") - if "/" in new_file_name or "\\" in new_file_name: - st.warning("Do not include slashes in names!") - elif st.sidebar.button("Create File", key="create_file") and new_file_name: - new_file_path = fm.base_path / new_file_name - new_file_path.touch(exist_ok=True) - st.success(f"File '{new_file_name}' created successfully!") - st.experimental_rerun() - - - return choice if __name__ == "__main__": diff --git a/frontend/state.py b/frontend/state.py index 0b0f27d..4090932 100644 --- a/frontend/state.py +++ b/frontend/state.py @@ -1,6 +1,9 @@ import streamlit as st def init_state(): + # Sidebar state initialization + + # Editor state initialization if "open_files" not in st.session_state: st.session_state.open_files = [] @@ -16,6 +19,9 @@ def init_state(): if "code_suggestions" not in st.session_state: st.session_state.code_suggestions = [] + + if "code_execution_output" not in st.session_state: + st.session_state.code_execution_output = "" # Chat state initialization if "chat_history" not in st.session_state: