some implementetd stuff
This commit is contained in:
parent
1a5bd6633a
commit
241c6580f3
@ -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,
|
||||
}
|
||||
|
||||
|
||||
152
frontend/chat.py
152
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 ───────────────────────────────────────────────────────────────
|
||||
|
||||
@ -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.")
|
||||
|
||||
|
||||
|
||||
|
||||
@ -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():
|
||||
|
||||
@ -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__":
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user