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 e69de29..a25e8d8 100644 --- a/backend/managers/file_manager.py +++ b/backend/managers/file_manager.py @@ -0,0 +1,167 @@ +import streamlit as st +from pathlib import Path + +WORKSPACE = Path("workspace") +WORKSPACE.mkdir(exist_ok=True) + +class FileManager: + def __init__(self, base_path=Path("workspace")) -> None: + self.base_path = Path(base_path) + self.base_path.mkdir(exist_ok=True) + + 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() + + 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): + + tree = {} + + for item in sorted(path.iterdir()): + if item.is_dir(): + tree[item.name] = build_tree(item) + else: + tree[item.name] = None + return tree + return build_tree(self.base_path) + +if __name__ == "__main__": + FileManager() \ No newline at end of file diff --git a/frontend/app.py b/frontend/app.py index e69de29..bfba19c 100644 --- a/frontend/app.py +++ b/frontend/app.py @@ -0,0 +1,40 @@ +import streamlit as st +import sys +from pathlib import Path + +# Add project root to Python path for imports +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from frontend.sidebar import render_sidebar +from frontend.editor import render_editor +from frontend.chat import render_chat +from frontend.state import init_state + +init_state() + + +def main(): + st.set_page_config( + page_title="Lightweight code editor", + layout="wide") + + st.title("Lightweight code editor") + + init_state() + + left, right = st.columns([1, 3]) + + with left: + choice = render_sidebar() + + with right: + if "Chat with AI Assistant" in choice: + render_chat() + if "Code Editor" in choice: + render_editor() + #elif choice == "File Explorer": + # st.subheader("File Explorer") + # st.info("File Explorer functionality is not implemented yet.") + +if __name__ == "__main__": + main() diff --git a/frontend/chat.py b/frontend/chat.py index e69de29..7070c73 100644 --- a/frontend/chat.py +++ b/frontend/chat.py @@ -0,0 +1,32 @@ +import streamlit as st + +def render_chat(): + st.subheader("Chat with AI Assistant") + + chat_section = st.container() + setup_section = st.container() + + with chat_section: + if st.session_state.chat_history: + for message in st.session_state.chat_history: + st.markdown(f"**{message['role'].capitalize()}:** {message['content']}") + + user_input = st.text_input("Type your message here:", key="chat_input") + + if st.button("Send", key="send_button") and user_input: + st.session_state.chat_history.append({"role": "user", "content": user_input}) + # Here you would typically call your AI assistant to get a response + # For demonstration, we'll just echo the user's message + ai_response = f"Echo: {user_input}" + st.session_state.chat_history.append({"role": "assistant", "content": ai_response}) + st.session_state.chat_input = "" # Clear input after sending + + with setup_section: + st.info("This is where you can set up your AI assistant. For now, this section is just a placeholder.") + st.toggle("Use debug system prompt", key="use_system_prompt", value=True) + + # Here you could add options to configure the AI assistant, such as selecting a model, setting parameters, etc. + + +if __name__ == "__main__": + render_chat() \ No newline at end of file diff --git a/frontend/editor.py b/frontend/editor.py index e69de29..79f9b98 100644 --- a/frontend/editor.py +++ b/frontend/editor.py @@ -0,0 +1,153 @@ +import streamlit as st +import streamlit_ace as st_ace +from pathlib import Path + +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: + st.info("Please select a file to edit.") + return + + fm = FileManager() + + tab_names = [Path(f).name for f in st.session_state.open_files] + tabs = st.tabs(tab_names) + + for idx, file_path in enumerate(st.session_state.open_files): + with tabs[idx]: + 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(Path(file_path)) + + file_language = LANG_MAP.get(Path(file_path).suffix, "text") + + code = st_ace.st_ace( + value=st.session_state.files_content[file_path], + language=file_language, + theme="monokai", + 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 + + cols = st.columns([1, 1, 1, 1]) + with cols[0]: + if st.button("Save Changes", key=f"save_{file_path}"): + content = st.session_state.files_content[file_path] + if fm.save_file(file_path, content): + st.success("File saved successfully!") + + 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] + st.success("File closed successfully!") + + if st.session_state.active_file == file_path: + st.session_state.active_file = ( + st.session_state.open_files[0] + if st.session_state.open_files + else None + ) + + 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__": + render_editor() \ No newline at end of file diff --git a/frontend/sidebar.py b/frontend/sidebar.py index e69de29..f0720d3 100644 --- a/frontend/sidebar.py +++ b/frontend/sidebar.py @@ -0,0 +1,173 @@ +import streamlit as st +from pathlib import Path +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 + disp_name = name if len(name) <= 13 else (name[:10] + "...") + + 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 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 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") + + 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) + return choice + +if __name__ == "__main__": + render_sidebar() + \ No newline at end of file diff --git a/frontend/state.py b/frontend/state.py new file mode 100644 index 0000000..4090932 --- /dev/null +++ b/frontend/state.py @@ -0,0 +1,33 @@ +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 = [] + + if "files_content" not in st.session_state: + st.session_state.files_content = {} + + if "active_file" not in st.session_state: + st.session_state.active_file = None + + if "is_editing" not in st.session_state: + st.session_state.is_editing = False + + 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: + st.session_state.chat_history = [] + + + +if __name__ == "__main__": + init_state() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 5882de9..ada1cab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,3 +18,6 @@ pytest-cov>=4.0.0 # Development & Utilities python-dotenv>=1.0.0 + +#For code editor functionality +streamlit-ace>=0.1.0