ui-setup #3

Merged
meulilivio merged 5 commits from ui-setup into main 2026-04-09 14:16:27 +02:00
10 changed files with 654 additions and 0 deletions

3
.gitignore vendored
View File

@ -47,3 +47,6 @@ htmlcov/
# Data
data/processed/
data/raw/
# Workspace
workspace/

View File

@ -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

View File

@ -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}

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

33
frontend/state.py Normal file
View File

@ -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()

View File

@ -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