Compare commits

..

No commits in common. "main" and "finetuning" have entirely different histories.

14 changed files with 260 additions and 240 deletions

View File

@ -3,9 +3,11 @@
# NOTE: Never commit .env with real secrets to version control!
# Silicon Server Configuration
HOST=
PORT=
HOST=silicon.fhgr.ch
PORT=7080
API_KEY=EMPTY
MODEL=
MODEL=qwen3.5-35b-a3b
# Optional: Add more configuration variables as needed
# DEBUG=False
# LOG_LEVEL=INFO

156
README.md
View File

@ -4,15 +4,6 @@ KI-unterstützter Lightweight Code Editor auf Basis von Streamlit (AISE501 Sprin
---
## Projektinformationen
| | |
|---|---|
| **Modul** | AI in Software Engineering 1 (AISE501) |
| **Autoren** | Irina Rüegg & Livio Meuli |
| **Semester** | Spring 2026 |
---
## Inhaltsverzeichnis
1. [Projektstruktur](#projektstruktur)
@ -80,124 +71,52 @@ AISE_AIAgent/
## Schnellstart
> Die folgenden Schritte funktionieren auf **Windows** und **macOS** — abweichende Befehle sind jeweils mit dem Betriebssystem gekennzeichnet.
---
### Schritt 1 — Voraussetzungen prüfen
**Python 3.10 oder neuer** muss installiert sein.
### 1. Repository klonen
```bash
# Windows (PowerShell)
python --version
# macOS (Terminal)
python3 --version
git clone https://gitea.fhgr.ch/meulilivio/AISE1_Project.git
cd AISE1_Project
```
Falls Python nicht installiert ist:
- **Windows:** [python.org/downloads](https://www.python.org/downloads/) herunterladen und installieren. Bei der Installation „Add Python to PATH" aktivieren.
- **macOS:** [python.org/downloads](https://www.python.org/downloads/) herunterladen und installieren, **oder** via Homebrew: `brew install python3`
**Git** muss ebenfalls installiert sein:
```bash
git --version
```
Falls nicht vorhanden: [git-scm.com](https://git-scm.com/downloads) (Windows) bzw. `brew install git` (macOS).
---
### Schritt 2 — Repository klonen
### 2. Virtuelle Umgebung aktivieren
```bash
git clone https://gitea.fhgr.ch/meulilivio/AISE1_Project_Irina_Livio.git
cd AISE1_Project_Irina_Livio
```
---
### Schritt 3 — Virtuelle Umgebung erstellen
Eine virtuelle Umgebung isoliert die Projekt-Abhängigkeiten vom restlichen System. Sie muss einmalig erstellt werden.
```bash
# Windows (PowerShell)
python -m venv .venv
# macOS (Terminal)
python3 -m venv .venv
```
---
### Schritt 4 — Virtuelle Umgebung aktivieren
Die Umgebung muss **jedes Mal neu aktiviert** werden, wenn ein neues Terminal geöffnet wird.
```bash
# Windows (PowerShell)
# Windows
.\.venv\Scripts\Activate.ps1
```
> Falls PowerShell die Ausführung blockiert, einmalig folgenden Befehl ausführen und danach erneut versuchen:
> ```powershell
> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
> ```
```bash
# macOS (Terminal)
# macOS / Linux
source .venv/bin/activate
```
Nach erfolgreicher Aktivierung erscheint `(.venv)` am Anfang der Eingabezeile.
---
### Schritt 5 — Abhängigkeiten installieren
### 3. Abhängigkeiten installieren
```bash
pip install -r requirements.txt
```
Dieser Schritt lädt alle benötigten Pakete herunter (~2–5 Minuten je nach Internetverbindung). Er muss nur einmal ausgeführt werden.
---
### Schritt 6 — Umgebungsvariablen konfigurieren
### 4. Umgebungsvariablen konfigurieren
```bash
# Windows (PowerShell)
copy .env.example .env
# macOS (Terminal)
cp .env.example .env
# .env öffnen und HOST, PORT, API_KEY, MODEL eintragen
```
Danach die Datei `.env` in einem Texteditor öffnen und die Werte eintragen welche per Mail mitgeteilt wurden (HOST, PORT, API_KEY, MODEL).
---
### Schritt 7 — App starten
### 5. App starten
```bash
# Windows & macOS
streamlit run frontend/app.py
```
Streamlit öffnet die App automatisch im Standard-Browser unter `http://localhost:8501`.
Falls der Browser nicht automatisch aufgeht, die URL manuell eingeben.
Zum **Beenden** der App im Terminal `Ctrl + C` drücken.
---
### Schritt 8 — Tests ausführen (optional)
### 6. Tests ausführen
```bash
pytest tests/ -v
```
**Optionale Abhängigkeit:** Die LaTeX-Unterstützung (`.tex`-Ausführung) erfordert `pdflatex`
im System-`PATH`. Ohne `pdflatex` funktioniert der Editor weiterhin; beim Ausführen einer
`.tex`-Datei erscheint ein `FileNotFoundError` im stderr-Panel.
---
## Frontend
@ -252,12 +171,10 @@ vorausgefüllten Debug-Nachricht.
Zwei sich gegenseitig ausschliessende Ansichten, umgeschaltet via `st.toggle("Agent Mode")`:
**Normaler Chat** (`render_normal_chat()`):
- Kompakte 4-spaltige Toolbar direkt über dem Chat-Input:
`[● Agent Mode]` `[🔍 Search]` `[🗑️ Clear]` `[⚙️ Settings]`
— Web-Suche und Einstellungen jeweils als `st.popover`, Clear öffnet einen Bestätigungs-Dialog.
- Websuch-Panel oben — öffnet sich automatisch, wenn Suchergebnisse aktiv sind.
- System-Prompt wird vor jeder ausgehenden Nachricht neu generiert (`_set_system_prompt()`).
- Unterstützt Slash-Befehle `/search <Abfrage>` und `/search clear`.
- Settings-Popover: Dateikontext-Toggle, Modell-Auswahl, Max-Token-Slider,
- Einstellungs-Expander: Dateikontext-Toggle, Modell-Auswahl, Max-Token-Slider,
benutzerdefinierter System-Prompt.
- «Mit KI debuggen»-Nachrichten vom Editor werden über `pending_debug_message` im
Session-State weitergeleitet.
@ -311,9 +228,7 @@ Generiert kontextbewusste System-Prompts. Signatur:
SystemPrompter.generate_prompt(
user_message="",
file_context=None, # {"name": str, "content": str}
search_context=None, # list[{"title", "url", "snippet"}] — in system_prompter.py implementiert,
# aber in chat.py nicht verwendet: dort wird Search-Kontext direkt
# als <search_context>-Block vor die Nachricht eingefügt
search_context=None, # reserviert, noch nicht verdrahtet
task_type="default", # "debug" | "explain" | "optimize" | "default"
)
```
@ -326,7 +241,8 @@ Funktion oder Klasse zurückzugeben, nach der der Benutzer fragt, anstatt die ge
### `execution_engine.py`
Führt Dateien in einem Subprocess mit `capture_output=True`, `text=True` und einem
`RUN_TIMEOUT` von 30 Sekunden aus. Aktuell unterstützt:
- `.py` — via `sys.executable` (plattformübergreifend; zeigt auf den aktuell aktiven Python-Interpreter)
- `.py` — via `py`-Launcher (Windows) / `sys.executable`
- `.tex` — via `pdflatex -interaction=nonstopmode` (erfordert pdflatex im PATH)
Rückgabe: `{"stdout": str, "stderr": str, "rc": int}`.
@ -381,7 +297,7 @@ Wichtige Methoden:
| `start_task(task)` | Setzt den gesamten Zustand zurück, befüllt History mit System + Aufgabe |
| `propose_next_action()` | Ruft das LLM auf, parst JSON, speichert als `pending_action` |
| `approve()` | Führt das ausstehende Tool via `dispatch_tool()` aus, loggt Ergebnis |
| `reject(feedback)` | Injiziert Feedback + Replan-Tag in die History; `propose_next_action()` wird danach separat aufgerufen |
| `reject(feedback)` | Injiziert Feedback + Replan-Tag; schlägt neue Aktion vor |
| `follow_up(question)` | Fügt nach «done» eine Folgefrage ein, setzt Schleife fort |
Hilfsfunktionen (Modul-Ebene):
@ -392,7 +308,7 @@ Hilfsfunktionen (Modul-Ebene):
| `trim_messages(msgs)` | Entfernt alte Turns, wenn History `MAX_HISTORY_CHARS` (80 000 Zeichen) überschreitet; System-Nachricht + Original-Aufgabe bleiben immer erhalten |
| `_strip_code_fences(text)` | Entfernt ` ```json `- / ` ``` `-Wrapper aus LLM-Antworten |
| `dispatch_tool(name, arguments)` | Leitet weiter an `MCPToolAdapter.call_tool()` |
| `build_all_tool_description()` | Erstellt eine menschenlesbare Tool-Liste für den System-Prompt |
| `get_tool_descriptions()` | Erstellt eine menschenlesbare Tool-Liste für den System-Prompt |
### `mcp_server_adapter.py`
Liest `mcp_server_config.json`, startet jeden Server als stdio-Subprocess (immer mit
@ -419,11 +335,12 @@ Alle drei Server sind FastMCP-Applikationen, die über stdio kommunizieren.
- Beide Tools nutzen SSRF-Schutz (gleiche URL-Validierung wie `search_manager.py`)
**`mcp_server_code_execution.py`** — Sandbox-Python-Analyse:
- `check_code_safety(code)` — statische Analyse; blockiert gefährliche Imports/Builtins
- `analyse_structure(code)` — AST-basierte Strukturzusammenfassung
- `lint_code(code)` — pyflakes-Analyse
- `python_code_validation(code)` — Sicherheits- + Syntaxprüfung ohne Ausführung
- `run_python_sandboxed(code)` — Ausführung in einem Subprocess mit `PYTHONIOENCODING=utf-8`,
15 s Timeout, Output begrenzt auf `MAX_OUTPUT_LENGTH`
10 s Timeout, Output begrenzt auf `MAX_OUTPUT_LENGTH`
---
@ -610,28 +527,3 @@ beteiligt ist.
Die App funktioniert mit jedem OpenAI-kompatiblen API-Endpunkt (vLLM, Ollama mit
OpenAI-Shim, OpenAI selbst usw.).
---
## Einsatz von KI-Werkzeugen
Während der Entwicklung wurden KI-Assistenten (Claude, GitHub Copilot) als
Werkzeuge eingesetzt — vergleichbar mit der Nutzung von Dokumentation, Stack Overflow
oder einer IDE mit Autocomplete.
Konkret bedeutet das:
- **Eigenständige Konzeption und Architektur**: Die Gesamtarchitektur (Schichtentrennung
Frontend / Manager / Agent), die Designentscheidungen und die Aufteilung in Komponenten
wurden selbst erarbeitet und geplant.
- **Implementierung mit Unterstützung**: Boilerplate-Code, Docstrings und einzelne
Hilfsfunktionen wurden teils mit KI-Unterstützung geschrieben, verstanden und
anschliessend in das Projekt integriert.
- **MCP-Integration und Chat-Logik**: Für das Model Context Protocol und den
Chat-Assistenten haben wir uns an den Kursbeispielen des Dozenten orientiert und
diese als Ausgangsbasis adaptiert und erweitert.
- **Debugging und Refactoring**: KI wurde als Gesprächspartner genutzt, um Fehler zu
analysieren und Lösungsansätze zu diskutieren — die Entscheidungen wurden jedoch
eigenständig getroffen und umgesetzt.
Der gesamte Code wurde von uns gelesen, verstanden und bewusst eingesetzt.
Unkritisch übernommener oder nicht verstandener Code wurde nicht ins Projekt aufgenommen.

View File

@ -167,7 +167,6 @@ Example:
- After validation passes, run it with run_python to verify correctness.
- If an error occurs, analyse it and try to fix it (up to 3 retries).
- Stay within the workspace directory.
- Never use emojis, umlauts (ä, ö, ü, Ä, Ö, Ü, ß), or any non-ASCII characters in string literals or print() calls — the execution environment uses cp1252 encoding which cannot handle them.
- When the task is fully complete, call the "done" tool.
- If you receive a <human_message>, acknowledge it and adjust your plan.
- If you receive a <replan> tag, revise your plan before choosing the next tool.

View File

@ -6,7 +6,6 @@ processes from blocking the UI indefinitely.
"""
import subprocess
import sys
from pathlib import Path
from backend.managers.debug_logger import get_logger
@ -41,7 +40,15 @@ class ExecutionEngine:
# Build the shell command depending on file type
if suffix == ".py":
cmd = [sys.executable, active_file.name]
cmd = ["py", active_file.name]
elif suffix == ".tex":
# pdflatex in non-interactive mode so it never waits for input
cmd = [
"pdflatex",
"-interaction=nonstopmode",
f"-output-directory={current_dir}",
active_file.name,
]
else:
return {"stdout": "", "stderr": f"Unsupported file type: {suffix}", "rc": 1}

View File

@ -9,38 +9,26 @@ logger = get_logger(__name__)
MAX_FILE_CHARS = 4000
# Per-task base prompts — selected via the task_type parameter.
# Appended to every prompt — ensures generated code is safe to run on Windows
# where the console encoding is cp1252 and cannot handle emojis or non-ASCII chars.
_CODE_SAFETY_NOTE = (
" When writing or suggesting code, never use emojis, umlauts (ä, ö, ü, Ä, Ö, Ü, ß), "
"or any non-ASCII characters in string literals or print statements, "
"as the execution environment uses cp1252 encoding which cannot handle them."
)
_TASK_PROMPTS: dict[str, str] = {
"debug": (
"You are a debugging expert integrated into a lightweight code editor. "
"Focus on identifying and fixing errors. "
"Be concise and precise. Use markdown and fenced code blocks where appropriate."
+ _CODE_SAFETY_NOTE
),
"explain": (
"You are a code explainer integrated into a lightweight code editor. "
"Use simple language and examples. "
"Be concise and precise. Use markdown and fenced code blocks where appropriate."
+ _CODE_SAFETY_NOTE
),
"optimize": (
"You are a code optimization expert integrated into a lightweight code editor. "
"Focus on performance and readability. "
"Be concise and precise. Use markdown and fenced code blocks where appropriate."
+ _CODE_SAFETY_NOTE
),
"default": (
"You are an expert code assistant integrated into a lightweight code editor. "
"Help the user with code suggestions, debugging, explanations, and improvements. "
"Be concise and precise. Use markdown and fenced code blocks where appropriate."
+ _CODE_SAFETY_NOTE
),
}

View File

View File

View File

@ -38,13 +38,15 @@ def main():
st.markdown(
"""
<style>
.block-container { padding-top: 4rem; }
[data-testid="stSidebarContent"] { padding-top: 0rem; }
.block-container { padding-top: 1rem; }
[data-testid="stSidebarContent"] { padding-top: 1rem; }
</style>
""",
unsafe_allow_html=True,
)
st.title("Lightweight code editor")
render_sidebar()
# Switch between the two main views based on the sidebar radio button

View File

@ -85,12 +85,6 @@ def _reject_action(feedback: str):
st.session_state.agent_pending_action = next_action
st.session_state.agent_status = "waiting_approval"
def _handle_reject():
feedback = st.session_state.agent_reject_feedback
with st.spinner("Agent is replanning..."):
_reject_action(feedback)
st.session_state.agent_reject_feedback = ""
def _followup_agent(question: str):
"""Continue a finished task by injecting a follow-up question and resuming the loop."""
@ -194,15 +188,14 @@ def render_agent_mode():
st.caption(f"Thought: {step['thought']}")
if step.get("arguments"):
_render_arguments(step["arguments"])
with st.expander("➡️ Result", expanded=False):
result_text = step.get("result", "")
# Colour the result based on whether the tool succeeded or failed.
if result_text.startswith("ERROR") or result_text.startswith("SYNTAX ERROR"):
st.error(result_text)
elif result_text.startswith("OK") or result_text.startswith("DONE"):
st.success(result_text)
else:
st.code(result_text, language=None)
result_text = step.get("result", "")
# Colour the result based on whether the tool succeeded or failed.
if result_text.startswith("ERROR") or result_text.startswith("SYNTAX ERROR"):
st.error(result_text)
elif result_text.startswith("OK") or result_text.startswith("DONE"):
st.success(result_text)
else:
st.code(result_text, language=None)
# ── Idle: task input ──────────────────────────────────────────────────────
if agent_status == "idle":
@ -232,10 +225,7 @@ def render_agent_mode():
if args:
_render_arguments(args)
if "agent_reject_feedback" not in st.session_state:
st.session_state.agent_reject_feedback = ""
st.text_input(
feedback = st.text_input(
"Rejection feedback (optional):",
key="agent_reject_feedback",
placeholder="e.g. Use a different approach...",
@ -248,11 +238,10 @@ def render_agent_mode():
_approve_action()
st.rerun()
with col2:
st.button(
"Reject",
use_container_width=True,
on_click=_handle_reject,
)
if st.button("Reject", use_container_width=True):
with st.spinner("Agent is replanning..."):
_reject_action(feedback)
st.rerun()
with col3:
if st.button("Abort Task", use_container_width=True):
_reset_agent()
@ -379,7 +368,7 @@ def _clear_chat_dialog():
# ── Normal Chat ───────────────────────────────────────────────────────────────
def _render_search_panel():
"""Render the collapsible web search panel above the chat toolbar.
"""Render the collapsible web search panel above the chat history.
Stores results in session_state.search_results so they are automatically
injected as context into the next message the user sends.
@ -435,6 +424,9 @@ def render_normal_chat():
logger.info("Chat mode")
chat_manager: ChatManager = st.session_state.chat_manager
# Search panel always rendered at the top — expands automatically when results are active.
_render_search_panel()
# Apply model/token overrides from the Settings panel before any API call.
if st.session_state.get("selected_model"):
chat_manager.model = st.session_state.selected_model
@ -464,58 +456,7 @@ def render_normal_chat():
with st.chat_message(message["role"]):
st.markdown(message["content"])
# ── Toolbar — directly above the sticky chat input ────────────────────────
# In Streamlit, st.chat_input is a fixed footer. Elements placed BEFORE it
# in code appear in the scrollable content area right above the input bar.
_render_search_panel()
col_clear, col_agent, col_settings = st.columns([1, 1, 1])
with col_clear:
if st.button("🗑️ Clear Chat", use_container_width=True):
_clear_chat_dialog()
with col_agent:
st.toggle("Agent Mode", key="agent_mode")
with col_settings:
with st.popover("⚙️ Settings", use_container_width=True):
current_file = st.session_state.get("active_file")
if current_file:
st.toggle(
f"Include current file as context: **{Path(current_file).name}**",
key="include_file_context",
value=True,
)
else:
st.toggle(
"Include current file as context",
key="include_file_context",
value=True,
)
st.divider()
default_model = chat_manager.model or ""
model_options = [default_model] if default_model else []
st.selectbox("Model", model_options, key="selected_model")
st.slider(
"Max Response Tokens",
min_value=256,
max_value=8000,
value=chat_manager.max_tokens,
step=256,
key="chat_max_tokens",
)
st.divider()
st.text_area(
"Custom System Prompt (overrides default if set)",
key="custom_system_prompt",
height=120,
placeholder="Leave empty to use the default assistant prompt with optional file context.",
)
# Chat input — sticky footer, always at the very bottom of the viewport.
# Chat input — Enter to send, no extra button needed.
# Supports /search <query> and /search clear as special commands.
user_input = st.chat_input("Type a message or /search <query>...")
if user_input:
@ -594,6 +535,42 @@ def render_normal_chat():
st.session_state.chat_history.append({"role": "assistant", "content": ai_response})
st.rerun()
# 5d — Clear Chat opens a confirmation dialog instead of deleting immediately.
if st.button("🗑️ Clear Chat"):
_clear_chat_dialog()
# Toggle to switch to Agent Mode (render_normal_chat and render_agent_mode are
# mutually exclusive, so the same key here causes no DuplicateWidgetID conflict).
st.toggle("Agent Mode", key="agent_mode")
# 5h — Settings expander: file context toggle, model, token limit, custom prompt.
with st.expander("⚙️ Settings", expanded=False):
st.toggle("Include current file as context", key="include_file_context", value=True)
st.divider()
default_model = chat_manager.model or ""
model_options = [default_model] if default_model else []
for m in ["claude-3-5-sonnet-20241022", "claude-3-haiku-20240307", "gpt-4o", "gpt-4o-mini"]:
if m not in model_options:
model_options.append(m)
st.selectbox("Model", model_options, key="selected_model")
st.slider(
"Max Response Tokens",
min_value=256, max_value=8000,
value=chat_manager.max_tokens,
step=256, key="chat_max_tokens",
)
st.divider()
st.text_area(
"Custom System Prompt (overrides default if set)",
key="custom_system_prompt",
height=120,
placeholder="Leave empty to use the default assistant prompt with optional file context.",
)
# ── Entry point ───────────────────────────────────────────────────────────────

View File

@ -13,12 +13,13 @@ logger = get_logger(__name__)
# Maps file extensions to Ace editor language modes for syntax highlighting.
LANG_MAP = {
".py": "python", ".js": "javascript",
".py": "python", ".tex": "latex", ".js": "javascript",
".html": "html", ".css": "css", ".sh": "bash",
".json": "json", ".yaml": "yaml", ".yml": "yaml"
}
# ── Modals ────────────────────────────────────────────────────────────────────
@st.dialog("Rename File")
@ -208,7 +209,7 @@ class FileViewer:
with cols[1]:
st.download_button(
label="Download",
label="⬇ Download",
data=st.session_state.files_content.get(file_path, ""),
file_name=Path(file_path).name,
mime="text/plain",

View File

@ -110,6 +110,106 @@ def _add_folder_dialog(parent_path: str = ""):
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):
"""Confirmation dialog before permanently deleting a file.
After a successful delete the file is also removed from the editor's
open-files list and content cache so it cannot be saved back to disk.
Args:
relative_file_path: Workspace-relative path to the file (used by FileManager).
file_name: Display name shown in the warning message.
"""
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)
st.session_state.open_files.remove(abs_file_path)
st.session_state.files_content.pop(abs_file_path, None)
# Fall back to the first remaining open file, or None if all tabs are closed.
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()):
@ -174,10 +274,10 @@ def render_filetree_arborist(tree):
selected = tree_view(
data=data,
icons={"open": "📂", "closed": "📁"},
height=350,
height=400,
selection=active_selection,
select_internal_nodes=True, # allow clicking folder names, not just files
open_by_default=True,
open_by_default=True
)
return selected
@ -269,6 +369,17 @@ def render_sidebar():
_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).
@ -296,5 +407,6 @@ def render_sidebar():
st.success(f"'{uploaded.name}' uploaded successfully.")
if __name__ == "__main__":
render_sidebar()

BIN
project_exercise.pdf Normal file

Binary file not shown.

View File

@ -1,5 +1,5 @@
# Core Framework
streamlit==1.57.0
streamlit>=1.28.0
streamlit_arborist>=0.1.0
# AI/LLM Integration

View File

@ -1,4 +1,3 @@
import sys
import pytest
import subprocess
from unittest.mock import Mock, patch
@ -62,6 +61,26 @@ def test_run_python_file_error(mock_run, engine, tmp_path):
assert "ZeroDivisionError" in result["stderr"]
# ---------------------------------------------------------
# 3. LaTeX-Datei wird kompiliert
# ---------------------------------------------------------
@patch("subprocess.run")
def test_run_tex_file_success(mock_run, engine, tmp_path):
file = tmp_path / "doc.tex"
file.write_text("\\documentclass{article}")
mock_run.return_value = Mock(
stdout="PDF created",
stderr="",
returncode=0
)
result = engine.run_code(file)
assert result["rc"] == 0
assert "PDF created" in result["stdout"]
# ---------------------------------------------------------
# 4. Unsupported File Type
@ -308,13 +327,34 @@ def test_run_unicode_filename(mock_run, engine, tmp_path):
assert result["rc"] == 0
# ---------------------------------------------------------
# 17. .py nutzt sys.executable als Interpreter
# 16. .tex nutzt pdflatex
# ---------------------------------------------------------
@patch("subprocess.run")
def test_python_uses_sys_executable(mock_run, engine, tmp_path):
def test_tex_uses_pdflatex(mock_run, engine, tmp_path):
file = tmp_path / "doc.tex"
file.write_text("x")
mock_run.return_value = Mock(
stdout="",
stderr="",
returncode=0
)
engine.run_code(file)
args, _ = mock_run.call_args
assert args[0][0] == "pdflatex"
# ---------------------------------------------------------
# 17. .py nutzt py Interpreter
# ---------------------------------------------------------
@patch("subprocess.run")
def test_python_uses_py_interpreter(mock_run, engine, tmp_path):
file = tmp_path / "main.py"
file.write_text("print(1)")
@ -328,7 +368,7 @@ def test_python_uses_sys_executable(mock_run, engine, tmp_path):
args, _ = mock_run.call_args
assert args[0][0] == sys.executable
assert args[0][0] == "py"
# ---------------------------------------------------------