From 241c6580f35b27e229646ece67650cc2699b285a Mon Sep 17 00:00:00 2001 From: Livio Meuli Date: Fri, 22 May 2026 14:25:22 +0200 Subject: [PATCH] some implementetd stuff --- backend/managers/chat_manager.py | 3 +- frontend/chat.py | 152 ++++++++++++++++++++++++++----- frontend/editor.py | 113 ++++++++++++++++------- frontend/sidebar.py | 17 +++- frontend/state.py | 15 +++ 5 files changed, 241 insertions(+), 59 deletions(-) diff --git a/backend/managers/chat_manager.py b/backend/managers/chat_manager.py index a10faab..e39d911 100644 --- a/backend/managers/chat_manager.py +++ b/backend/managers/chat_manager.py @@ -21,6 +21,7 @@ class ChatManager: self.api_port = os.getenv("PORT") self.api_key = os.getenv("API_KEY") self.model = os.getenv("MODEL") + self.max_tokens = 2000 # API endpoint URL (OpenAI-compatible format) self.api_url = f"http://{self.api_host}:{self.api_port}/v1/chat/completions" @@ -64,7 +65,7 @@ class ChatManager: "model": self.model, "messages": self.chat_history, "temperature": 0.7, - "max_tokens": 2000, + "max_tokens": self.max_tokens, "stream": False, } diff --git a/frontend/chat.py b/frontend/chat.py index 43ab8bc..5db855e 100644 --- a/frontend/chat.py +++ b/frontend/chat.py @@ -1,11 +1,12 @@ """Chat view — renders both the normal chat interface and the Coding Agent mode.""" +import asyncio +from pathlib import Path + import streamlit as st from backend.managers.chat_manager import ChatManager from backend.managers.system_prompter import SystemPrompter -import asyncio - # ── Agent Mode helpers ──────────────────────────────────────────────────────── def _run_async(coro): @@ -210,39 +211,119 @@ def render_agent_mode(): st.rerun() +# ── Normal Chat helpers ─────────────────────────────────────────────────────── + +def _build_file_context() -> dict | None: + """Return file context for the system prompt if a file is open and context is enabled. + + Reads from files_content cache first; falls back to FileManager if the file + has not been loaded into the editor yet. + """ + if not st.session_state.get("include_file_context", True): + return None + active_file = st.session_state.get("active_file") + if not active_file: + return None + content = st.session_state.get("files_content", {}).get(active_file, "") + if not content: + try: + from backend.managers.file_manager import FileManager + fm = FileManager() + content = fm.read_file(Path(active_file)) or "" + except Exception: + return None + return {"name": Path(active_file).name, "content": content} + + +@st.dialog("Clear Chat") +def _clear_chat_dialog(): + """Confirmation dialog before wiping the full conversation history.""" + st.warning("All messages will be deleted. This cannot be undone.") + col1, col2 = st.columns(2) + with col1: + if st.button("Clear", type="primary", use_container_width=True): + st.session_state.chat_manager.clear_history() + st.session_state.chat_history = [] + st.rerun() + with col2: + if st.button("Cancel", use_container_width=True): + st.rerun() + + # ── Normal Chat ─────────────────────────────────────────────────────────────── def render_normal_chat(): """Render the standard multi-turn chat interface. - On the first message the system prompt is injected into the history. - Each subsequent message appends to the same conversation so the AI retains - full context throughout the session. + Execution order on every rerun: + 1. Apply model/token settings from the Settings panel (5e) + 2. Consume any pending debug message from the editor (5f) + 3. Replay chat history + 4. Handle chat input with updated system-prompt logic (5g) + 5. Render Clear Chat button and Settings expander (5d, 5h) """ + chat_manager: ChatManager = st.session_state.chat_manager + + # 5e — 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 + if "chat_max_tokens" in st.session_state: + chat_manager.max_tokens = st.session_state.chat_max_tokens + + # 5f — Consume a debug message forwarded from the editor's "Debug with AI" button. + pending_debug = st.session_state.pop("pending_debug_message", None) + if pending_debug: + if not chat_manager.get_history(): + custom_prompt = st.session_state.get("custom_system_prompt", "").strip() + if custom_prompt: + chat_manager.add_message("system", custom_prompt) + else: + file_ctx = _build_file_context() + system_prompt = SystemPrompter.generate_prompt(file_ctx) + chat_manager.add_message("system", system_prompt) + + with st.spinner("Sending debug info to AI..."): + try: + ai_response = chat_manager.send_message(pending_debug) + except Exception as e: + ai_response = f"Error: {e}" + + st.session_state.chat_history.append({"role": "user", "content": pending_debug}) + st.session_state.chat_history.append({"role": "assistant", "content": ai_response}) + st.rerun() + return + # Replay the conversation history as chat bubbles (skip system messages). for message in st.session_state.chat_history: - role = message["role"] - if role == "system": + if message["role"] == "system": continue - with st.chat_message(role): + with st.chat_message(message["role"]): st.markdown(message["content"]) - # Chat input — Enter to send, no extra button needed + # Chat input — Enter to send, no extra button needed. user_input = st.chat_input("Type your message here...") if user_input: - chat_manager = st.session_state.chat_manager - # On the very first user message, prepend the system prompt so the AI - # knows it is a code assistant embedded in an editor. + # 5g — System-prompt logic: inject on first message, update on file change. if not chat_manager.get_history(): - system_prompt = SystemPrompter.generate_prompt() - chat_manager.add_message("system", system_prompt) + custom_prompt = st.session_state.get("custom_system_prompt", "").strip() + if custom_prompt: + chat_manager.add_message("system", custom_prompt) + else: + file_ctx = _build_file_context() + system_prompt = SystemPrompter.generate_prompt(file_ctx) + chat_manager.add_message("system", system_prompt) + elif st.session_state.get("active_file") and st.session_state.get("include_file_context", True): + # Follow-up messages: refresh the system prompt when the active file changes. + history = chat_manager.get_history() + if history and history[0]["role"] == "system": + file_ctx = _build_file_context() + if file_ctx: + history[0]["content"] = SystemPrompter.generate_prompt(file_ctx) - # Show user message immediately without waiting for response. with st.chat_message("user"): st.markdown(user_input) - # Call the AI and show its response with a spinner while waiting. with st.chat_message("assistant"): with st.spinner("Thinking..."): try: @@ -251,15 +332,44 @@ def render_normal_chat(): ai_response = f"Error: {e}" st.markdown(ai_response) - st.session_state.chat_history.append({"role": "user", "content": user_input}) + st.session_state.chat_history.append({"role": "user", "content": user_input}) st.session_state.chat_history.append({"role": "assistant", "content": ai_response}) st.rerun() - # Rendered in the normal flow; JS above clones them to fixed positions - # and hides these originals. + # 5d — Clear Chat opens a confirmation dialog instead of deleting immediately. + if st.button("🗑️ Clear Chat"): + _clear_chat_dialog() + st.toggle("Agent Mode", key="agent_mode") - with st.expander("Settings", expanded=False): - st.toggle("Use debug system prompt", key="use_system_prompt", value=True) + + # 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 ─────────────────────────────────────────────────────────────── diff --git a/frontend/editor.py b/frontend/editor.py index 9f9c8b8..805e1d7 100644 --- a/frontend/editor.py +++ b/frontend/editor.py @@ -1,5 +1,7 @@ """Code Editor view — renders the Ace editor, file tabs, and execution output.""" +import ast + import streamlit as st import streamlit_ace as st_ace from pathlib import Path @@ -85,9 +87,13 @@ def _delete_dialog(abs_file_path: str): def run_active_file(): - """Execute the currently active file and store the result in session_state. - Returns the execution result dict {stdout, stderr, return_code}, or None - if no active file is set. + """Execute the currently active file and store the result in exec_results[file_path]. + + Runs an ast.parse() check first — if the syntax is invalid the file is not + executed and ast_error=True is stored so the UI can show a targeted warning. + + Returns: + The result dict, or None if no active file is set. """ active_file = st.session_state.active_file @@ -97,10 +103,20 @@ def run_active_file(): execution_engine = ExecutionEngine() debug_logger = DebugLogger() - debug_logger.clear() debug_logger.log(f"Executing code from {active_file}...") + # ast check — only for Python files + if Path(active_file).suffix == ".py": + source = st.session_state.get("files_content", {}).get(active_file, "") + try: + ast.parse(source) + except SyntaxError as e: + result = {"stdout": "", "stderr": str(e), "return_code": -1, "ast_error": True} + st.session_state.exec_results[active_file] = result + debug_logger.log_error(f"Syntax error: {e}") + return result + with st.spinner(f"Running {Path(active_file).name}..."): output = execution_engine.run_code(Path(active_file)) @@ -109,12 +125,13 @@ def run_active_file(): else: debug_logger.log_error(f"Execution failed with exit code {output['rc']}.") - st.session_state.code_execution_output = { + result = { "stdout": output["stdout"], "stderr": output["stderr"], - "return_code": output["rc"] + "return_code": output["rc"], + "ast_error": False, } - result = st.session_state.code_execution_output + st.session_state.exec_results[active_file] = result return result def render_editor(): @@ -132,6 +149,24 @@ def render_editor(): tab_names = [Path(f).name for f in st.session_state.open_files] tabs = st.tabs(tab_names) + # Tab-Sprung via JavaScript — pop() verhindert Loop bei jedem Rerun. + # Wenn _jump_to_tab gesetzt ist, klickt das Script den richtigen Tab an. + jump_target = st.session_state.pop("_jump_to_tab", None) + if jump_target and jump_target in st.session_state.open_files: + idx = st.session_state.open_files.index(jump_target) + st.components.v1.html( + f"""""", + height=0, + ) + for idx, file_path in enumerate(st.session_state.open_files): with tabs[idx]: # Load file content from disk on first open; afterwards use the cached version. @@ -179,36 +214,48 @@ def render_editor(): if st.button("Delete File", key=f"delete_{file_path}"): _delete_dialog(file_path) - if st.button("▶ Run Code", key="run_code"): - result = run_active_file() - if not result: - st.stop() + # ── Run + Output ────────────────────────────────────────────────── + if st.button("▶ Run Code", key=f"run_code_{file_path}", type="primary"): + run_active_file() + st.rerun() - st.subheader("Execution Output") + result = st.session_state.get("exec_results", {}).get(file_path) + if result: + st.subheader("Execution Output") - # Green on exit code 0 (success), red on anything else (error/crash). - if result["return_code"] == 0: - st.success(f"Exit code: {result['return_code']}") - else: - st.error(f"Exit code: {result['return_code']}") + if result.get("ast_error"): + st.warning("⚠️ Syntax Error detected before execution — code was not run.") + elif result["return_code"] == 0: + st.success(f"✅ Exit code: 0") + 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") + # Debug with AI — only shown when there is an error or stderr output. + if result["return_code"] != 0 or result.get("stderr"): + if st.button("🐛 Debug with AI", key=f"debug_with_ai_{file_path}", type="primary"): + file_name = Path(file_path).name + error_text = result.get("stderr", "") or f"Exit code: {result['return_code']}" + code_content = st.session_state.files_content.get(file_path, "") + lang = LANG_MAP.get(Path(file_path).suffix, "python") + debug_message = ( + f"I got an error while running **{file_name}**:\n\n" + f"**Error:** {error_text.strip()}\n" + f"**Exit Code:** {result['return_code']}\n\n" + f"**Here is the code:**\n```{lang}\n{code_content}\n```\n\n" + f"Can you help me fix this?" + ) + st.session_state.pending_debug_message = debug_message + st.session_state["_navigate_to_chat"] = True + st.rerun() - 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 result.get("stdout"): + st.text_area("Standard Output", value=result["stdout"], height=200, + disabled=True, key=f"run_stdout_{file_path}") + if result.get("stderr"): + st.text_area("Standard Error", value=result["stderr"], height=200, + disabled=True, key=f"run_stderr_{file_path}") + if not result.get("stdout") and not result.get("stderr"): + st.info("No output produced by the code execution.") diff --git a/frontend/sidebar.py b/frontend/sidebar.py index 1a6b9d4..f496190 100644 --- a/frontend/sidebar.py +++ b/frontend/sidebar.py @@ -173,9 +173,6 @@ def _delete_file_dialog(relative_file_path: str, file_name: str): 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: @@ -257,7 +254,16 @@ def render_sidebar(): 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 + + Navigation flags (_navigate_to_editor, _navigate_to_chat) are consumed here + at the very top — before any widget is rendered — to avoid StreamlitAPIException. """ + # Consume navigation flags before any widget renders. + if st.session_state.pop("_navigate_to_editor", False): + st.session_state.radio_interface_options = "Code Editor" + if st.session_state.pop("_navigate_to_chat", False): + st.session_state.radio_interface_options = "Chat with AI Assistant" + st.sidebar.title("Navigation") navigation_section = st.sidebar.container() @@ -294,13 +300,16 @@ def render_sidebar(): abs_path = fm.base_path / selected_path if abs_path.is_file(): - # Open the file in the editor. + # Open the file in the editor, jump to its tab, + # and switch the view to the Editor pane. 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.session_state._jump_to_tab = file_str + st.session_state._navigate_to_editor = True st.rerun() elif abs_path.is_dir(): diff --git a/frontend/state.py b/frontend/state.py index 2a4b25d..b5c7b7b 100644 --- a/frontend/state.py +++ b/frontend/state.py @@ -88,6 +88,21 @@ def init_state(): if "agent_pending_action" not in st.session_state: st.session_state.agent_pending_action = None + # Whether to inject the currently open file as context into the system prompt + if "include_file_context" not in st.session_state: + st.session_state.include_file_context = True + + # Optional custom system prompt entered by the user in Settings (overrides default) + if "custom_system_prompt" not in st.session_state: + st.session_state.custom_system_prompt = "" + + # Holds a pre-built debug message to be sent to the AI on the next chat render + if "pending_debug_message" not in st.session_state: + st.session_state.pending_debug_message = None + + # Per-file execution results: {file_path: {stdout, stderr, return_code, ast_error}} + if "exec_results" not in st.session_state: + st.session_state.exec_results = {} if __name__ == "__main__":