import streamlit as st import os import re from pathlib import Path import logging def render_chat_panel(chat_engine): """ Renders the Chat interface within the side panel. """ st.markdown("### 💬 Chat") # Initialize messages for this specific panel usage if needed # But we usually share global st.session_state.messages if "messages" not in st.session_state: st.session_state.messages = [] # Suggestion buttons (only if no messages) if not st.session_state.messages: st.markdown("#### 💡 Try asking:") col1, col2 = st.columns(2) with col1: if st.button("🔍 Explain structure", use_container_width=True, key="btn_explain"): st.session_state.pending_prompt = "Explain the project structure and main components" st.rerun() if st.button("⚡ Generate utility", use_container_width=True, key="btn_util"): st.session_state.pending_prompt = "Generate a new utility function for this project" st.rerun() with col2: if st.button("📝 List functions", use_container_width=True, key="btn_list"): st.session_state.pending_prompt = "List all the main functions and their purpose" st.rerun() if st.button("🔧 Improvements", use_container_width=True, key="btn_imp"): st.session_state.pending_prompt = "What improvements would you suggest for this code?" st.rerun() # Message Container # We use a container with fixed height to allow scrolling independent of the editor # But standard Streamlit scrolling works best if we just let it flow in the column. # Display chat history for message in st.session_state.messages: with st.chat_message(message["role"]): # Render Sources if available if "sources" in message and message["sources"]: _render_sources(message["sources"]) st.markdown(message["content"]) # Handle pending prompt prompt = st.session_state.pop("pending_prompt", None) # Chat input # key needs to be unique if we have multiple inputs, but usually only one chat input active if user_input := st.chat_input("Ask about your code...", key="chat_panel_input"): prompt = user_input if prompt: # Add user message st.session_state.messages.append({"role": "user", "content": prompt}) with st.chat_message("user"): st.markdown(prompt) # Generate response with st.chat_message("assistant"): with st.spinner("Thinking..."): try: # Blocking call answer_payload = chat_engine.chat(prompt) if isinstance(answer_payload, tuple): response, sources = answer_payload else: response = answer_payload sources = [] if sources: _render_sources(sources) st.markdown(response) st.session_state.messages.append({ "role": "assistant", "content": response, "sources": sources }) except Exception as e: error_msg = f"Error: {str(e)}" st.error(error_msg) st.session_state.messages.append({"role": "assistant", "content": error_msg}) def _render_sources(sources): unique_sources = {} for s in sources: if isinstance(s, dict): fp = s.get('file_path', 'Unknown') else: fp = str(s) if fp not in unique_sources: unique_sources[fp] = s chips_html = '
' for fp in unique_sources: basename = os.path.basename(fp) if "/" in fp else fp chips_html += f"""
📄 {basename}
""" chips_html += '
' st.markdown(chips_html, unsafe_allow_html=True) def render_search_panel(indexed_files): """ Renders the Search interface. """ st.markdown("### 🔍 Search") query = st.text_input("Search pattern", placeholder="Enter search term or regex...", key="search_query") use_regex = st.checkbox("Use regex", value=False, key="search_regex") file_types = st.multiselect( "Filter by file type", options=[".py", ".js", ".ts", ".jsx", ".tsx", ".html", ".css", ".json", ".md"], default=[], key="search_types" ) if query and st.button("Go", key="search_go", type="primary"): results = [] try: pattern = re.compile(query, re.IGNORECASE) if use_regex else None except re.error as e: st.error(f"Invalid regex: {e}") return with st.spinner("Searching..."): for file_path in indexed_files: if file_types: ext = Path(file_path).suffix.lower() if ext not in file_types: continue try: with open(file_path, "r", errors="ignore") as f: lines = f.readlines() for i, line in enumerate(lines, 1): if use_regex: if pattern.search(line): results.append({ "file": file_path, "line_num": i, "content": line.strip(), "match": pattern.search(line).group() }) else: if query.lower() in line.lower(): results.append({ "file": file_path, "line_num": i, "content": line.strip(), "match": query }) except Exception: continue st.markdown(f"**Found {len(results)} matches**") if results: # Group by file by_file = {} for r in results: f = r["file"] if f not in by_file: by_file[f] = [] by_file[f].append(r) for file_path, matches in by_file.items(): filename = os.path.basename(file_path) with st.expander(f"📄 {filename} ({len(matches)})", expanded=False): for m in matches[:5]: # Make clickable logic? Streamlit doesn't easily support clickable text to trigger state change without rerun # We use buttons if st.button(f"L{m['line_num']}: {m['content'][:40]}...", key=f"nav_{file_path}_{m['line_num']}"): st.session_state.selected_file = file_path # st.rerun() # Might not be needed if this triggers a rerun naturally else: st.info("No matches.") def render_generate_panel(chat_engine, indexed_files): """ Renders the Refactor/Generate interface. """ st.markdown("### ✨ Refactor & Generate") mode = st.radio( "Mode", ["Refactor", "New Code", "New File"], horizontal=True, label_visibility="collapsed" ) if mode == "New Code": st.caption("Generate new code snippet") description = st.text_area("Request", placeholder="e.g. Email validator function", height=100, key="gen_desc") context = st.text_input("Context", placeholder="e.g. style of utils.py", key="gen_ctx") if st.button("Generate", type="primary", key="btn_gen_new"): with st.spinner("Working..."): prompt = f"Generate code: {description}\nContext: {context}" try: resp = chat_engine.chat(prompt) # Unwrap if tuple if isinstance(resp, tuple): resp = resp[0] st.code(resp) except Exception as e: st.error(str(e)) if mode == "Refactor": st.caption("Refactor or modify existing files") # Ensure we have files if not indexed_files: st.error("No files indexed.") return # Use session state to remember selection if possible, or just default # We need a unique key selected_file = st.selectbox( "File", indexed_files, format_func=lambda x: os.path.basename(x), key="mod_file_select" ) modification = st.text_area("Instructions", placeholder="Add error handling...", height=100, key="mod_instr") if st.button("Modify", type="primary", key="btn_mod"): with st.spinner("Modifying..."): prompt = f"Modify {selected_file}: {modification}" try: resp = chat_engine.chat(prompt) if isinstance(resp, tuple): resp = resp[0] st.code(resp) except Exception as e: st.error(str(e)) elif mode == "New File": st.caption("Create new file") fname = st.text_input("Filename", placeholder="utils/helper.py", key="new_fname") desc = st.text_area("Content Description", placeholder="Functions for...", height=100, key="new_fdesc") if st.button("Create", type="primary", key="btn_create_file"): with st.spinner("Creating..."): prompt = f"Create file {fname}: {desc}" try: resp = chat_engine.chat(prompt) if isinstance(resp, tuple): resp = resp[0] st.code(resp) except Exception as e: st.error(str(e))