"""Sidebar — navigation radio, logo, and the workspace file explorer.""" import streamlit as st from streamlit_arborist import tree_view from pathlib import Path from backend.managers.file_manager import FileManager # Shared FileManager instance for all sidebar operations. fm = FileManager() # Maps file extensions (and special keys "folder"/"default") to display emojis # shown next to each entry in the file tree. 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 } # ── Modals ──────────────────────────────────────────────────────────────────── @st.dialog("Delete Folder") def _delete_folder_dialog(folder_rel: str, folder_name: str): """Confirmation dialog before permanently deleting a folder and its contents.""" st.warning(f"Delete **{folder_name}** and all its contents? This cannot be undone.") col1, col2 = st.columns(2) with col1: if st.button("Delete", type="primary", use_container_width=True): if fm.delete_folder(folder_rel): # Clear the selected-folder state so the action bar disappears. st.session_state.selected_folder = None st.session_state.selected_folder_rel = None st.rerun() else: st.error("Delete failed.") with col2: if st.button("Cancel", use_container_width=True): st.rerun() @st.dialog("Add File") def _add_file_dialog(parent_path: str = ""): with st.form("add_file_form"): name = st.text_input("File name:", placeholder="e.g. script.py") col1, col2 = st.columns(2) with col1: submitted = st.form_submit_button("Create", type="primary", use_container_width=True) with col2: cancel = st.form_submit_button("Cancel", use_container_width=True) if submitted: if not name.strip(): st.warning("Please enter a file name.") elif "/" in name or "\\" in name: st.warning("Name must not contain slashes.") else: if fm.create_file(parent_path, name.strip()): st.rerun() if cancel: st.rerun() @st.dialog("Add Folder") def _add_folder_dialog(parent_path: str = ""): with st.form("add_folder_form"): name = st.text_input("Folder name:", placeholder="e.g. utils") col1, col2 = st.columns(2) with col1: submitted = st.form_submit_button("Create", type="primary", use_container_width=True) with col2: cancel = st.form_submit_button("Cancel", use_container_width=True) if submitted: if not name.strip(): st.warning("Please enter a folder name.") elif "/" in name or "\\" in name: st.warning("Name must not contain slashes.") else: if fm.create_folder(parent_path, name.strip()): st.rerun() if cancel: st.rerun() @st.dialog("Rename File") def _rename_file_dialog(relative_file_path: str, file_name: str): """ Dialog to rename a file. Args: relative_file_path (str): The current relative path (without base path) to the file to rename, including the file name. file_name (str): The current name of the file, including the extension. """ st.write(f"Current name: **{file_name}**") with st.form("rename_file_form"): new_name = st.text_input( "New name:", value=Path(relative_file_path).stem ) col1, col2 = st.columns(2) with col1: submitted = st.form_submit_button( "Confirm", type="primary", use_container_width=True ) with col2: cancel = st.form_submit_button( "Cancel", use_container_width=True ) if submitted: if not new_name.strip(): st.warning("Please enter a name.") elif "/" in new_name or "\\" in new_name or "." in new_name: st.warning("Name must not contain slashes.") else: if fm.rename_file(relative_file_path, new_name.strip()): absolute_file_path = str(Path(fm.base_path / relative_file_path)) ext = Path(absolute_file_path).suffix new_file_path = str( Path(absolute_file_path).parent / (Path(new_name.strip()).stem + ext) ) if absolute_file_path in st.session_state.open_files: i = st.session_state.open_files.index(absolute_file_path) st.session_state.open_files[i] = new_file_path if absolute_file_path in st.session_state.files_content: st.session_state.files_content[new_file_path] = \ st.session_state.files_content.pop(absolute_file_path) if st.session_state.active_file == absolute_file_path: st.session_state.active_file = new_file_path st.rerun() else: st.error("Rename failed. Check that the file still exists.") st.error(f"Attempted to rename: {relative_file_path} to {new_name.strip()}") if cancel: st.rerun() @st.dialog("Delete File") def _delete_file_dialog(relative_file_path: str, file_name: str): st.warning(f"Delete **{file_name}**? This cannot be undone.") col1, col2 = st.columns(2) with col1: if st.button("Delete", type="primary", use_container_width=True): if fm.delete_file(relative_file_path): abs_file_path = str(Path(fm.base_path) / relative_file_path) print(f"Deleting file at absolute path: {abs_file_path}") # Debugging info print(f"Current open files before deletion: {st.session_state.open_files}") # Debugging info st.session_state.open_files.remove(abs_file_path) st.session_state.files_content.pop(abs_file_path, None) if st.session_state.active_file == abs_file_path: st.session_state.active_file = ( st.session_state.open_files[0] if st.session_state.open_files else None ) st.rerun() else: st.error("Delete failed. Check that the file still exists.") with col2: if st.button("Cancel", use_container_width=True): st.rerun() # ── File tree ───────────────────────────────────────────────────────────────── def build_arborist_tree(tree, parent_path=Path()): """Convert the FileManager dict tree into the node format expected by streamlit-arborist. Folders become nodes with a "children" list; files become leaf nodes with an emoji prefix derived from their extension. Args: tree: Nested dict from FileManager.get_file_tree() parent_path: Accumulates the relative path while recursing. Returns: List of node dicts accepted by tree_view(). """ nodes = [] for name, content in sorted(tree.items()): full_path = parent_path / name node_id = str(full_path.as_posix()) # forward-slash IDs work cross-platform if isinstance(content, dict): # Directory — recurse to build child nodes. nodes.append({ "id": node_id, "name": f"{name}", "children": build_arborist_tree(content, full_path) }) else: # File — pick an emoji based on extension, fall back to default. suffix = Path(name).suffix icon = SUFFIX_MAP.get(suffix, SUFFIX_MAP["default"]) nodes.append({ "id": node_id, "name": f"{icon} {name}", "key": f"key_{node_id}" }) return nodes def render_filetree_arborist(tree): """Render the interactive file tree and return the currently selected node dict.""" data = build_arborist_tree(tree) selected = tree_view( data=data, icons={"open": "📂", "closed": "📁"}, height=200, selection=None, select_internal_nodes=True, # allow clicking folder names, not just files open_by_default=False ) return selected # ── Sidebar ─────────────────────────────────────────────────────────────────── def render_sidebar(): """Render the full sidebar: navigation radio and workspace file explorer. Tree click handling: - Clicking a file → appended to open_files, set as active_file - Clicking a folder → stored in selected_folder so the action bar appears """ st.sidebar.title("Navigation") navigation_section = st.sidebar.container() file_explorer_section = st.sidebar.container() with navigation_section: st.radio( "Options", ["Chat with AI Assistant", "Code Editor"], key="radio_interface_options") st.markdown("---") with file_explorer_section: st.subheader("File Explorer") tree = fm.get_file_tree() workspace = st.container() add_more = st.container() with workspace: with st.container(border=False): if not tree: st.info("Workspace is empty.") else: selected = render_filetree_arborist(tree) if selected: selected_path = selected.get("id") # Only react when the user clicks a *different* node to # avoid re-running on every Streamlit rerender. if st.session_state.last_selected != selected_path: st.session_state.last_selected = selected_path abs_path = fm.base_path / selected_path if abs_path.is_file(): # Open the file in the editor. st.session_state.selected_folder = None st.session_state.selected_folder_rel = None file_str = str(abs_path) if file_str not in st.session_state.open_files: st.session_state.open_files.append(file_str) st.session_state.active_file = file_str st.rerun() elif abs_path.is_dir(): # Select the folder so its action bar appears below. st.session_state.selected_folder = str(abs_path) st.session_state.selected_folder_rel = selected_path st.rerun() # Folder action bar — rendered unconditionally outside the selection # block so it persists across reruns even when no new click happens. if st.session_state.get("selected_folder"): folder_name = Path(st.session_state.selected_folder).name folder_rel = st.session_state.selected_folder_rel with st.container(border=True): st.write(f"**Folder actions:** 📁 {folder_name}") if st.button("Add File", key="btn_add_file_in_folder", use_container_width=True): _add_file_dialog(folder_rel) if st.button("Add Folder", key="btn_add_folder_in_folder", use_container_width=True): _add_folder_dialog(folder_rel) if st.button("Delete Folder", key="btn_delete_folder", use_container_width=True): _delete_folder_dialog(folder_rel, folder_name) if st.session_state.get("active_file"): active_file_name = Path(st.session_state.active_file).name file_rel = str(Path(st.session_state.active_file).relative_to(fm.base_path)) with st.container(border=True): st.write(f"**File actions:** {active_file_name}") if st.button("Rename File", key="btn_rename_file", use_container_width=True): _rename_file_dialog(file_rel, active_file_name) if st.button("Delete File", key="btn_delete_active_file", use_container_width=True): _delete_file_dialog(file_rel, active_file_name) with add_more: # Popover for workspace-root actions (not tied to any selected folder). with st.popover("⚙️ Explorer Options", key="popover_options", use_container_width=True): if st.button("Add File", key="btn_add_file", use_container_width=True): _add_file_dialog("") if st.button("Add Folder", key="btn_add_folder", use_container_width=True): _add_folder_dialog("") return if __name__ == "__main__": render_sidebar()