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_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,
}

View File

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

View File

@ -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"""<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):
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.")

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

View File

@ -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__":