From 3ba942d6105b01fbaa79b1a83cbd82d195a5562f Mon Sep 17 00:00:00 2001 From: Irina Rueegg Date: Thu, 26 Mar 2026 18:02:28 +0100 Subject: [PATCH 1/5] Entwurf grobes UI-Setup --- frontend/app.py | 33 +++++++++++++++++++++++++++++++++ frontend/chat.py | 21 +++++++++++++++++++++ frontend/editor.py | 17 +++++++++++++++++ frontend/sidebar.py | 13 +++++++++++++ frontend/state.py | 20 ++++++++++++++++++++ 5 files changed, 104 insertions(+) create mode 100644 frontend/state.py diff --git a/frontend/app.py b/frontend/app.py index e69de29..fdecce1 100644 --- a/frontend/app.py +++ b/frontend/app.py @@ -0,0 +1,33 @@ +import streamlit as st + +from frontend.state import init_state +from frontend.sidebar import render_sidebar +from frontend.editor import render_editor +from frontend.chat import render_chat + + +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 choice == "Chat with AI Assistant": + render_chat() + elif choice == "Code Editor": + render_editor() + elif choice == "File Explorer": + st.subheader("File Explorer") + st.info("File Explorer functionality is not implemented yet.") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/frontend/chat.py b/frontend/chat.py index e69de29..1a06046 100644 --- a/frontend/chat.py +++ b/frontend/chat.py @@ -0,0 +1,21 @@ +import streamlit as st + +def render_chat(): + st.subheader("Chat with AI Assistant") + + 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 + +if __name__ == "__main__": + render_chat() \ No newline at end of file diff --git a/frontend/editor.py b/frontend/editor.py index e69de29..5020986 100644 --- a/frontend/editor.py +++ b/frontend/editor.py @@ -0,0 +1,17 @@ +import streamlit as st + +def render_editor(): + st.subheader("Code Editor") + + if st.session_state.selected_file: + st.text_area( + "Edit your code here:", + value=st.session_state.file_content, + height=400, + key="code_editor" + ) + else: + st.info("Please select a file to edit.") + +if __name__ == "__main__": + render_editor() \ No newline at end of file diff --git a/frontend/sidebar.py b/frontend/sidebar.py index e69de29..0c914a1 100644 --- a/frontend/sidebar.py +++ b/frontend/sidebar.py @@ -0,0 +1,13 @@ +import streamlit as st + +def render_sidebar(): + st.sidebar.title("Navigation") + + options = ["Chat with AI Assistant", "Code Editor", "File Explorer"] + choice = st.sidebar.radio("Go to:", options) + + 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..a340bb5 --- /dev/null +++ b/frontend/state.py @@ -0,0 +1,20 @@ +import streamlit as st + +def init_state(): + if "selected_file" not in st.session_state: + st.session_state.selected_file = None + + if "file_content" not in st.session_state: + st.session_state.file_content = "" + + if "is_editing" not in st.session_state: + st.session_state.is_editing = False + + if "chat_history" not in st.session_state: + st.session_state.chat_history = [] + + if "code_suggestions" not in st.session_state: + st.session_state.code_suggestions = [] + +if __name__ == "__main__": + init_state() \ No newline at end of file -- 2.30.2 From b3331215700ab9d0fc37e1d491d0a647bb38171a Mon Sep 17 00:00:00 2001 From: Livio Meuli Date: Fri, 27 Mar 2026 14:31:31 +0100 Subject: [PATCH 2/5] Working Streamlit in Browser --- frontend/app.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/frontend/app.py b/frontend/app.py index fdecce1..04b9c09 100644 --- a/frontend/app.py +++ b/frontend/app.py @@ -1,11 +1,27 @@ 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.state import init_state from frontend.sidebar import render_sidebar from frontend.editor import render_editor from frontend.chat import render_chat +def init_state(): + """Initialize session state variables.""" + if "selected_file" not in st.session_state: + st.session_state.selected_file = None + if "file_content" not in st.session_state: + st.session_state.file_content = "" + if "chat_history" not in st.session_state: + st.session_state.chat_history = [] + if "code_output" not in st.session_state: + st.session_state.code_output = "" + + def main(): st.set_page_config( page_title="Lightweight code editor", @@ -30,4 +46,4 @@ def main(): st.info("File Explorer functionality is not implemented yet.") if __name__ == "__main__": - main() \ No newline at end of file + main() -- 2.30.2 From 699c3c1dba6ed7c67cab66d20129962bb58b9840 Mon Sep 17 00:00:00 2001 From: Irina Rueegg Date: Fri, 27 Mar 2026 15:56:15 +0100 Subject: [PATCH 3/5] Anpassung Options & Sidebar --- frontend/app.py | 23 +++++++---------------- frontend/sidebar.py | 18 ++++++++++++++++-- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/frontend/app.py b/frontend/app.py index 04b9c09..bfba19c 100644 --- a/frontend/app.py +++ b/frontend/app.py @@ -8,18 +8,9 @@ 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 - -def init_state(): - """Initialize session state variables.""" - if "selected_file" not in st.session_state: - st.session_state.selected_file = None - if "file_content" not in st.session_state: - st.session_state.file_content = "" - if "chat_history" not in st.session_state: - st.session_state.chat_history = [] - if "code_output" not in st.session_state: - st.session_state.code_output = "" +init_state() def main(): @@ -37,13 +28,13 @@ def main(): choice = render_sidebar() with right: - if choice == "Chat with AI Assistant": + if "Chat with AI Assistant" in choice: render_chat() - elif choice == "Code Editor": + if "Code Editor" in choice: render_editor() - elif choice == "File Explorer": - st.subheader("File Explorer") - st.info("File Explorer functionality is not implemented yet.") + #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/sidebar.py b/frontend/sidebar.py index 0c914a1..7e26017 100644 --- a/frontend/sidebar.py +++ b/frontend/sidebar.py @@ -3,8 +3,22 @@ import streamlit as st def render_sidebar(): st.sidebar.title("Navigation") - options = ["Chat with AI Assistant", "Code Editor", "File Explorer"] - choice = st.sidebar.radio("Go to:", options) + navigation_section = st.sidebar.container() + file_explorer_section = st.sidebar.container() + + with navigation_section: + 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") + st.sidebar.info("File Explorer functionality is not implemented yet.") + # selected_file = st.sidebar.file_uploader("Upload a file", type=["py", "txt", "md"]) + return choice -- 2.30.2 From e59cd2849bfa1440142e69d4ad9630e340decd71 Mon Sep 17 00:00:00 2001 From: Irina Rueegg Date: Mon, 6 Apr 2026 15:25:53 +0200 Subject: [PATCH 4/5] feat: editor and file tree --- backend/managers/file_manager.py | 30 ++++++++++++ frontend/chat.py | 33 ++++++++----- frontend/editor.py | 66 +++++++++++++++++++++---- frontend/sidebar.py | 82 +++++++++++++++++++++++++++++++- frontend/state.py | 19 +++++--- requirements.txt | 3 ++ 6 files changed, 205 insertions(+), 28 deletions(-) diff --git a/backend/managers/file_manager.py b/backend/managers/file_manager.py index e69de29..e392dfc 100644 --- a/backend/managers/file_manager.py +++ b/backend/managers/file_manager.py @@ -0,0 +1,30 @@ +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")): + self.base_path = Path(base_path) + self.base_path.mkdir(exist_ok=True) + + # read_file content + + # save_file content + + 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/chat.py b/frontend/chat.py index 1a06046..7070c73 100644 --- a/frontend/chat.py +++ b/frontend/chat.py @@ -3,19 +3,30 @@ import streamlit as st def render_chat(): st.subheader("Chat with AI Assistant") - if st.session_state.chat_history: - for message in st.session_state.chat_history: - st.markdown(f"**{message['role'].capitalize()}:** {message['content']}") + 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 - user_input = st.text_input("Type your message here:", key="chat_input") + 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) - 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 + # 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 5020986..7a22c59 100644 --- a/frontend/editor.py +++ b/frontend/editor.py @@ -1,17 +1,65 @@ 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 = { + ".py": "python", ".tex": "latex", ".js": "javascript", + ".html": "html", ".css": "css", ".sh": "bash", + ".json": "json", ".yaml": "yaml", ".yml": "yaml" + } + st.subheader("Code Editor") - - if st.session_state.selected_file: - st.text_area( - "Edit your code here:", - value=st.session_state.file_content, - height=400, - key="code_editor" - ) - else: + + 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(file_path) + + file_language = LANG_MAP.get(file_path.suffix, "text") + + code = st_ace( + value=st.session_state.files_content[file_path], + language=file_language, + theme="monokai", + key=f"code_editor_{file_path}" + ) + + st.session_state.files_content[file_path] = code + + col1, col2 = st.columns([1, 1]) + with col1: + 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!") + + with col2: + 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 + if __name__ == "__main__": render_editor() \ No newline at end of file diff --git a/frontend/sidebar.py b/frontend/sidebar.py index 7e26017..98dfd26 100644 --- a/frontend/sidebar.py +++ b/frontend/sidebar.py @@ -1,4 +1,55 @@ import streamlit as st +from pathlib import Path +from backend.managers.file_manager import FileManager + +fm = FileManager() + +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) + + 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) + + 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 "/" 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}") def render_sidebar(): st.sidebar.title("Navigation") @@ -16,9 +67,36 @@ def render_sidebar(): with file_explorer_section: st.sidebar.subheader("File Explorer") - st.sidebar.info("File Explorer functionality is not implemented yet.") - # selected_file = st.sidebar.file_uploader("Upload a file", type=["py", "txt", "md"]) + 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 diff --git a/frontend/state.py b/frontend/state.py index a340bb5..0b0f27d 100644 --- a/frontend/state.py +++ b/frontend/state.py @@ -1,20 +1,27 @@ import streamlit as st def init_state(): - if "selected_file" not in st.session_state: - st.session_state.selected_file = None + # Editor state initialization + if "open_files" not in st.session_state: + st.session_state.open_files = [] - if "file_content" not in st.session_state: - st.session_state.file_content = "" + 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 = [] + + # Chat state initialization if "chat_history" not in st.session_state: st.session_state.chat_history = [] - if "code_suggestions" not in st.session_state: - st.session_state.code_suggestions = [] + 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 -- 2.30.2 From 3ce4ef970b5cde82cbfe21199529e7697a18bc33 Mon Sep 17 00:00:00 2001 From: Irina Rueegg Date: Mon, 6 Apr 2026 21:14:27 +0200 Subject: [PATCH 5/5] Debug feats: editor, file_manager --- .gitignore | 3 + backend/managers/debug_logger.py | 9 ++ backend/managers/execution_engine.py | 41 ++++++ backend/managers/file_manager.py | 143 +++++++++++++++++- frontend/editor.py | 112 ++++++++++++-- frontend/sidebar.py | 212 ++++++++++++++++++--------- frontend/state.py | 6 + 7 files changed, 439 insertions(+), 87 deletions(-) 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: -- 2.30.2