some implementetd stuff

This commit is contained in:
Livio Meuli 2026-05-22 14:25:22 +02:00
parent 1a5bd6633a
commit 241c6580f3
5 changed files with 241 additions and 59 deletions

View File

@ -21,6 +21,7 @@ class ChatManager:
self.api_port = os.getenv("PORT") self.api_port = os.getenv("PORT")
self.api_key = os.getenv("API_KEY") self.api_key = os.getenv("API_KEY")
self.model = os.getenv("MODEL") self.model = os.getenv("MODEL")
self.max_tokens = 2000
# API endpoint URL (OpenAI-compatible format) # API endpoint URL (OpenAI-compatible format)
self.api_url = f"http://{self.api_host}:{self.api_port}/v1/chat/completions" self.api_url = f"http://{self.api_host}:{self.api_port}/v1/chat/completions"
@ -64,7 +65,7 @@ class ChatManager:
"model": self.model, "model": self.model,
"messages": self.chat_history, "messages": self.chat_history,
"temperature": 0.7, "temperature": 0.7,
"max_tokens": 2000, "max_tokens": self.max_tokens,
"stream": False, "stream": False,
} }

View File

@ -1,11 +1,12 @@
"""Chat view — renders both the normal chat interface and the Coding Agent mode.""" """Chat view — renders both the normal chat interface and the Coding Agent mode."""
import asyncio
from pathlib import Path
import streamlit as st import streamlit as st
from backend.managers.chat_manager import ChatManager from backend.managers.chat_manager import ChatManager
from backend.managers.system_prompter import SystemPrompter from backend.managers.system_prompter import SystemPrompter
import asyncio
# ── Agent Mode helpers ──────────────────────────────────────────────────────── # ── Agent Mode helpers ────────────────────────────────────────────────────────
def _run_async(coro): def _run_async(coro):
@ -210,39 +211,119 @@ def render_agent_mode():
st.rerun() 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 ─────────────────────────────────────────────────────────────── # ── Normal Chat ───────────────────────────────────────────────────────────────
def render_normal_chat(): def render_normal_chat():
"""Render the standard multi-turn chat interface. """Render the standard multi-turn chat interface.
On the first message the system prompt is injected into the history. Execution order on every rerun:
Each subsequent message appends to the same conversation so the AI retains 1. Apply model/token settings from the Settings panel (5e)
full context throughout the session. 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). # Replay the conversation history as chat bubbles (skip system messages).
for message in st.session_state.chat_history: for message in st.session_state.chat_history:
role = message["role"] if message["role"] == "system":
if role == "system":
continue continue
with st.chat_message(role): with st.chat_message(message["role"]):
st.markdown(message["content"]) 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...") user_input = st.chat_input("Type your message here...")
if user_input: if user_input:
chat_manager = st.session_state.chat_manager
# On the very first user message, prepend the system prompt so the AI # 5g — System-prompt logic: inject on first message, update on file change.
# knows it is a code assistant embedded in an editor.
if not chat_manager.get_history(): if not chat_manager.get_history():
system_prompt = SystemPrompter.generate_prompt() custom_prompt = st.session_state.get("custom_system_prompt", "").strip()
chat_manager.add_message("system", system_prompt) 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"): with st.chat_message("user"):
st.markdown(user_input) st.markdown(user_input)
# Call the AI and show its response with a spinner while waiting.
with st.chat_message("assistant"): with st.chat_message("assistant"):
with st.spinner("Thinking..."): with st.spinner("Thinking..."):
try: try:
@ -251,15 +332,44 @@ def render_normal_chat():
ai_response = f"Error: {e}" ai_response = f"Error: {e}"
st.markdown(ai_response) 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.session_state.chat_history.append({"role": "assistant", "content": ai_response})
st.rerun() st.rerun()
# Rendered in the normal flow; JS above clones them to fixed positions # 5d — Clear Chat opens a confirmation dialog instead of deleting immediately.
# and hides these originals. if st.button("🗑️ Clear Chat"):
_clear_chat_dialog()
st.toggle("Agent Mode", key="agent_mode") 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 ─────────────────────────────────────────────────────────────── # ── Entry point ───────────────────────────────────────────────────────────────

View File

@ -1,5 +1,7 @@
"""Code Editor view — renders the Ace editor, file tabs, and execution output.""" """Code Editor view — renders the Ace editor, file tabs, and execution output."""
import ast
import streamlit as st import streamlit as st
import streamlit_ace as st_ace import streamlit_ace as st_ace
from pathlib import Path from pathlib import Path
@ -85,9 +87,13 @@ def _delete_dialog(abs_file_path: str):
def run_active_file(): def run_active_file():
"""Execute the currently active file and store the result in session_state. """Execute the currently active file and store the result in exec_results[file_path].
Returns the execution result dict {stdout, stderr, return_code}, or None
if no active file is set. 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 active_file = st.session_state.active_file
@ -97,10 +103,20 @@ def run_active_file():
execution_engine = ExecutionEngine() execution_engine = ExecutionEngine()
debug_logger = DebugLogger() debug_logger = DebugLogger()
debug_logger.clear() debug_logger.clear()
debug_logger.log(f"Executing code from {active_file}...") 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}..."): with st.spinner(f"Running {Path(active_file).name}..."):
output = execution_engine.run_code(Path(active_file)) output = execution_engine.run_code(Path(active_file))
@ -109,12 +125,13 @@ def run_active_file():
else: else:
debug_logger.log_error(f"Execution failed with exit code {output['rc']}.") debug_logger.log_error(f"Execution failed with exit code {output['rc']}.")
st.session_state.code_execution_output = { result = {
"stdout": output["stdout"], "stdout": output["stdout"],
"stderr": output["stderr"], "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 return result
def render_editor(): def render_editor():
@ -132,6 +149,24 @@ def render_editor():
tab_names = [Path(f).name for f in st.session_state.open_files] tab_names = [Path(f).name for f in st.session_state.open_files]
tabs = st.tabs(tab_names) 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"""<script>
(function() {{
setTimeout(function() {{
const tabs = window.parent.document
.querySelectorAll('button[data-baseweb="tab"]');
if (tabs[{idx}]) tabs[{idx}].click();
}}, 100);
}})();
</script>""",
height=0,
)
for idx, file_path in enumerate(st.session_state.open_files): for idx, file_path in enumerate(st.session_state.open_files):
with tabs[idx]: with tabs[idx]:
# Load file content from disk on first open; afterwards use the cached version. # 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}"): if st.button("Delete File", key=f"delete_{file_path}"):
_delete_dialog(file_path) _delete_dialog(file_path)
if st.button("▶ Run Code", key="run_code"): # ── Run + Output ──────────────────────────────────────────────────
result = run_active_file() if st.button("▶ Run Code", key=f"run_code_{file_path}", type="primary"):
if not result: run_active_file()
st.stop() 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.get("ast_error"):
if result["return_code"] == 0: st.warning("⚠️ Syntax Error detected before execution — code was not run.")
st.success(f"Exit code: {result['return_code']}") elif result["return_code"] == 0:
else: st.success(f"✅ Exit code: 0")
st.error(f"Exit code: {result['return_code']}") else:
st.error(f"❌ Exit code: {result['return_code']}")
if result["stdout"]: # Debug with AI — only shown when there is an error or stderr output.
st.text_area( if result["return_code"] != 0 or result.get("stderr"):
"Standard Output", if st.button("🐛 Debug with AI", key=f"debug_with_ai_{file_path}", type="primary"):
value=result["stdout"], file_name = Path(file_path).name
height=200, error_text = result.get("stderr", "") or f"Exit code: {result['return_code']}"
disabled=True, code_content = st.session_state.files_content.get(file_path, "")
key="run_stdout") 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"]: if result.get("stdout"):
st.text_area( st.text_area("Standard Output", value=result["stdout"], height=200,
"Standard Error", disabled=True, key=f"run_stdout_{file_path}")
value=result["stderr"], if result.get("stderr"):
height=200, st.text_area("Standard Error", value=result["stderr"], height=200,
disabled=True, disabled=True, key=f"run_stderr_{file_path}")
key="run_stderr") if not result.get("stdout") and not result.get("stderr"):
if not result["stdout"] and not result["stderr"]: st.info("No output produced by the code execution.")
st.info("No output produced by the code execution.")

View File

@ -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 st.button("Delete", type="primary", use_container_width=True):
if fm.delete_file(relative_file_path): if fm.delete_file(relative_file_path):
abs_file_path = str(Path(fm.base_path) / 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.open_files.remove(abs_file_path)
st.session_state.files_content.pop(abs_file_path, None) st.session_state.files_content.pop(abs_file_path, None)
if st.session_state.active_file == abs_file_path: if st.session_state.active_file == abs_file_path:
@ -257,7 +254,16 @@ def render_sidebar():
Tree click handling: Tree click handling:
- Clicking a file → appended to open_files, set as active_file - Clicking a file → appended to open_files, set as active_file
- Clicking a folder → stored in selected_folder so the action bar appears - 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") st.sidebar.title("Navigation")
navigation_section = st.sidebar.container() navigation_section = st.sidebar.container()
@ -294,13 +300,16 @@ def render_sidebar():
abs_path = fm.base_path / selected_path abs_path = fm.base_path / selected_path
if abs_path.is_file(): 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 = None
st.session_state.selected_folder_rel = None st.session_state.selected_folder_rel = None
file_str = str(abs_path) file_str = str(abs_path)
if file_str not in st.session_state.open_files: if file_str not in st.session_state.open_files:
st.session_state.open_files.append(file_str) st.session_state.open_files.append(file_str)
st.session_state.active_file = 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() st.rerun()
elif abs_path.is_dir(): elif abs_path.is_dir():

View File

@ -88,6 +88,21 @@ def init_state():
if "agent_pending_action" not in st.session_state: if "agent_pending_action" not in st.session_state:
st.session_state.agent_pending_action = None 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__": if __name__ == "__main__":