File size: 10,537 Bytes
88779f3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d897d25
88779f3
d897d25
88779f3
 
 
d897d25
88779f3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d897d25
 
88779f3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
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 = '<div style="display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 10px;">'
    for fp in unique_sources:
        basename = os.path.basename(fp) if "/" in fp else fp
        chips_html += f"""
        <div class="source-chip">
            πŸ“„ {basename}
        </div>
        """
    chips_html += '</div>'
    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))