mgbam commited on
Commit
5477235
Β·
verified Β·
1 Parent(s): 3a80282

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +129 -142
app.py CHANGED
@@ -5,11 +5,11 @@ import io
5
  import json
6
  import os
7
  from pathlib import Path
8
- import time # Added for simulating mock delay
9
 
10
  # --- Configuration ---
11
  GEMINI_MODEL_NAME = "gemini-2.5-pro-preview-03-25"
12
- MAX_PROMPT_TOKENS_ESTIMATE = 800000 # Adjust as needed
13
 
14
  AVAILABLE_ANALYSES = {
15
  "generate_docs": "Generate Missing Docstrings/Comments",
@@ -19,17 +19,20 @@ AVAILABLE_ANALYSES = {
19
  "suggest_refactoring": "Suggest Refactoring Opportunities"
20
  }
21
 
22
- CODE_EXTENSIONS = {'.py', '.js', '.java', '.c', '.cpp', '.h', '.cs', '.go', '.rb', '.php', '.swift', '.kt', '.ts', '.html', '.css', '.scss', '.sql'}
 
 
 
23
 
24
  # --- Session State Initialization ---
25
- # Initialize session state for mock mode toggle if it doesn't exist
26
  if 'mock_api_call' not in st.session_state:
27
- st.session_state.mock_api_call = False # Default to using the real API
28
 
29
  # --- Gemini API Setup ---
30
- # Defer full initialization until needed if mock mode might be used first
31
  model = None
 
32
  def initialize_gemini_model():
 
33
  global model
34
  if model is None and not st.session_state.mock_api_call:
35
  try:
@@ -46,21 +49,28 @@ def initialize_gemini_model():
46
  return False
47
  elif st.session_state.mock_api_call:
48
  print("Running in Mock Mode. Skipping Gemini initialization.")
49
- return True # Allow proceeding in mock mode
50
  elif model is not None:
51
- print("Gemini Model already initialized.")
52
- return True
53
  return False
54
 
55
-
56
  # --- Helper Functions ---
57
 
58
  def estimate_token_count(text):
59
- """Roughly estimate token count (3-4 chars per token)."""
60
  return len(text) // 3
61
 
62
  def process_zip_file(uploaded_file):
63
- """Extracts code files and their content from the uploaded zip file."""
 
 
 
 
 
 
 
 
64
  code_files = {}
65
  total_chars = 0
66
  file_count = 0
@@ -69,6 +79,7 @@ def process_zip_file(uploaded_file):
69
  try:
70
  with zipfile.ZipFile(io.BytesIO(uploaded_file.getvalue()), 'r') as zip_ref:
71
  for member in zip_ref.infolist():
 
72
  if member.is_dir() or any(part.startswith('.') for part in Path(member.filename).parts) or '__' in member.filename:
73
  continue
74
 
@@ -82,17 +93,17 @@ def process_zip_file(uploaded_file):
82
  try:
83
  content = file.read().decode('latin-1')
84
  except Exception as decode_err:
85
- ignored_files.append(f"{member.filename} (Decode Error: {decode_err})")
86
- continue
87
 
88
  code_files[member.filename] = content
89
  total_chars += len(content)
90
  file_count += 1
91
  except Exception as read_err:
92
- ignored_files.append(f"{member.filename} (Read Error: {read_err})")
93
  else:
94
  # Only add to ignored if it's not explicitly ignored by path rules above
95
- if not (any(part.startswith('.') for part in Path(member.filename).parts) or '__' in member.filename):
96
  ignored_files.append(f"{member.filename} (Skipped Extension: {file_path.suffix})")
97
 
98
  except zipfile.BadZipFile:
@@ -105,7 +116,13 @@ def process_zip_file(uploaded_file):
105
  return code_files, total_chars, file_count, ignored_files
106
 
107
  def construct_analysis_prompt(code_files_dict, requested_analyses):
108
- """Constructs the prompt for Gemini, including code content and JSON structure request."""
 
 
 
 
 
 
109
  prompt_content = "Analyze the following codebase provided as a collection of file paths and their content.\n\n"
110
  current_token_estimate = estimate_token_count(prompt_content)
111
  included_files = []
@@ -132,8 +149,8 @@ def construct_analysis_prompt(code_files_dict, requested_analyses):
132
 
133
  prompt_content += concatenated_code
134
 
 
135
  json_structure_description = "{\n"
136
- # Dynamically build the JSON structure based on selection
137
  structure_parts = []
138
  if "generate_docs" in requested_analyses:
139
  structure_parts.append(' "documentation_suggestions": [{"file": "path/to/file", "line": number, "suggestion": "Suggested docstring/comment"}]')
@@ -145,7 +162,6 @@ def construct_analysis_prompt(code_files_dict, requested_analyses):
145
  structure_parts.append(' "module_summaries": [{"file": "path/to/file", "summary": "One-paragraph summary of the file purpose/functionality"}]')
146
  if "suggest_refactoring" in requested_analyses:
147
  structure_parts.append(' "refactoring_suggestions": [{"file": "path/to/file", "line": number, "area": "e.g., function name, class name", "suggestion": "Description of refactoring suggestion"}]')
148
-
149
  json_structure_description += ",\n".join(structure_parts)
150
  json_structure_description += "\n}"
151
 
@@ -161,57 +177,44 @@ Respond ONLY with a single, valid JSON object adhering strictly to the following
161
  **JSON Output Only:**
162
  """
163
  full_prompt = prompt_content + prompt_footer
164
- # print(f"--- PROMPT (First 500 chars): ---\n{full_prompt[:500]}\n--------------------------")
165
- # print(f"--- PROMPT (Last 500 chars): ---\n{full_prompt[-500:]}\n--------------------------")
166
  return full_prompt, included_files
167
 
168
-
169
  def call_gemini_api(prompt):
170
- """Calls the Gemini API or returns mock data based on session state."""
 
 
 
 
 
 
171
  if not prompt:
172
  return None, "Prompt generation failed."
173
 
174
  # --- MOCK MODE LOGIC ---
175
  if st.session_state.mock_api_call:
176
  st.info(" MOCK MODE: Simulating API call...")
177
- time.sleep(2) # Simulate network/processing delay
178
 
179
- # --- CHOOSE YOUR MOCK RESPONSE ---
180
- # Option 1: Simulate successful response with some data
181
  mock_json_response = json.dumps({
182
  "documentation_suggestions": [{"file": "mock/core.py", "line": 15, "suggestion": "def process_data(data):\n \"\"\"Processes the input data using mock logic.\"\"\""}],
183
- "potential_bugs": [{"file":"mock/utils.py", "line": 22, "description":"Potential division by zero if denominator is not checked.", "severity":"Medium"}],
184
  "style_issues": [{"file": "mock/core.py", "line": 5, "description": "Variable 'varName' does not follow snake_case convention."}],
185
- "module_summaries": [{"file": "mock/core.py", "summary": "This file contains the core mock processing logic."}, {"file":"mock/utils.py", "summary": "Utility functions for mocking."}],
186
- "refactoring_suggestions": [{"file":"mock/utils.py", "line": 30, "area":"calculate_metrics function", "suggestion": "Function is too long (> 50 lines), consider breaking it down."}]
 
 
 
187
  })
188
  st.success("Mock response generated successfully.")
189
- return json.loads(mock_json_response), None # Return insights, no error
190
-
191
- # Option 2: Simulate API error
192
- # st.error("Simulating API error.")
193
- # return None, "MOCK ERROR: Simulated API Quota Exceeded."
194
-
195
- # Option 3: Simulate invalid JSON response
196
- # st.warning("Simulating invalid JSON response from AI.")
197
- # return {"raw_response": "{malformed json'"}, "AI response was not valid JSON, showing raw text."
198
- #
199
- # Option 4: Simulate empty results
200
- # mock_empty_json = json.dumps({
201
- # "documentation_suggestions": [], "potential_bugs": [], "style_issues": [],
202
- # "module_summaries": [], "refactoring_suggestions": []
203
- # })
204
- # st.success("Mock response generated (empty results).")
205
- # return json.loads(mock_empty_json), None
206
- # --- END MOCK MODE LOGIC ---
207
-
208
 
209
  # --- REAL API CALL LOGIC ---
210
  else:
211
- if not initialize_gemini_model(): # Ensure model is ready
212
- return None, "Gemini Model Initialization Failed."
213
- if model is None: # Should not happen if initialize check passed, but safeguard
214
- return None, "Gemini model not available."
215
 
216
  try:
217
  st.write(f"πŸ“‘ Sending request to {GEMINI_MODEL_NAME}...")
@@ -227,21 +230,17 @@ def call_gemini_api(prompt):
227
  )
228
  st.write("βœ… Response received from AI.")
229
 
230
- # Debug: Print raw response text
231
- # print(f"--- RAW API RESPONSE ---\n{response.text}\n------------------------")
232
-
233
  try:
234
- # Try to extract JSON robustly
235
  json_response_text = response.text.strip()
236
- # Handle potential markdown code block fences
237
  if json_response_text.startswith("```json"):
238
  json_response_text = json_response_text[7:]
239
- if json_response_text.startswith("```"): # Handle case where ```json wasn't used
240
- json_response_text = json_response_text[3:]
241
  if json_response_text.endswith("```"):
242
  json_response_text = json_response_text[:-3]
243
 
244
- # Find the first '{' and the last '}'
245
  json_start = json_response_text.find('{')
246
  json_end = json_response_text.rfind('}') + 1
247
 
@@ -259,42 +258,40 @@ def call_gemini_api(prompt):
259
  st.code(response.text, language='text')
260
  return None, f"AI response was not valid JSON: {json_err}"
261
  except AttributeError:
262
- # Handle cases where response structure might be different (e.g. blocked)
263
- st.error(f"🚨 Unexpected API response structure.")
264
- st.code(f"Response object: {response}", language='text') # Log the problematic response
265
- # Try to get blocked reason if available
266
- try:
267
- block_reason = response.prompt_feedback.block_reason
268
- if block_reason:
269
- return None, f"Content blocked by API. Reason: {block_reason}"
270
- except Exception:
271
- pass # Ignore if feedback structure isn't as expected
272
- return None, "Unexpected response structure from API."
273
  except Exception as e:
274
  st.error(f"🚨 Unexpected issue processing AI response: {e}")
275
- try: st.code(f"Response object: {response}", language='text')
276
- except: pass
 
 
277
  return None, f"Unexpected response structure: {e}"
278
 
279
  except Exception as e:
280
  st.error(f"🚨 An error occurred during API call: {e}")
281
  error_msg = f"API call failed: {e}"
282
- # Improved error identification
283
- if hasattr(e, 'message'): # For google.api_core.exceptions
284
- if "429" in e.message:
285
- error_msg = "API Quota Exceeded or Rate Limit hit. Check your Google Cloud/AI Studio dashboard."
286
- elif "API key not valid" in e.message:
287
- error_msg = "Invalid Gemini API Key. Please check `.streamlit/secrets.toml`."
288
- elif "blocked" in e.message.lower(): # General check for safety blocks
289
- error_msg = "Content blocked due to safety settings. Review input code or adjust safety settings if appropriate."
290
- elif "block_reason: SAFETY" in str(e): # Fallback check
291
- error_msg = "Content blocked due to safety settings. Review input code or adjust safety settings if appropriate."
292
 
293
  return None, error_msg
294
 
295
-
296
  def display_results(results_json, requested_analyses):
297
- """Renders the analysis results in Streamlit."""
298
  st.header("πŸ“Š Analysis Report")
299
 
300
  if not isinstance(results_json, dict):
@@ -307,52 +304,54 @@ def display_results(results_json, requested_analyses):
307
  st.code(results_json["raw_response"], language='text')
308
  return
309
 
310
- # Define display functions for clarity
311
  def display_list_items(items, fields):
312
  if items:
313
  for item in items:
314
  details = []
315
  for field_key, field_label in fields.items():
316
  value = item.get(field_key, 'N/A')
317
- if value != 'N/A': # Only show if value exists
318
- details.append(f"**{field_label}:** {value}")
319
  st.markdown("- " + " - ".join(details))
320
- # Handle specific multi-line outputs like suggestions/summaries
321
  if 'suggestion' in item:
322
- st.code(item['suggestion'], language='text')
323
  elif 'description' in item:
324
- st.markdown(f" > {item['description']}") # Indent description
325
  elif 'summary' in item:
326
- st.markdown(f" > {item['summary']}") # Indent summary
327
  else:
328
  st.markdown("_No items found for this category._")
329
  st.divider()
330
 
331
- # Map keys to display configurations
332
  display_config = {
333
  "generate_docs": {
334
- "key": "documentation_suggestions", "title": AVAILABLE_ANALYSES["generate_docs"],
335
- "fields": {"file": "File", "line": "Line"} # Suggestion shown by st.code
 
336
  },
337
  "find_bugs": {
338
- "key": "potential_bugs", "title": AVAILABLE_ANALYSES["find_bugs"],
339
- "fields": {"file": "File", "line": "Line", "severity": "Severity"} # Description shown separately
 
340
  },
341
  "check_style": {
342
- "key": "style_issues", "title": AVAILABLE_ANALYSES["check_style"],
343
- "fields": {"file": "File", "line": "Line"} # Description shown separately
 
344
  },
345
  "summarize_modules": {
346
- "key": "module_summaries", "title": AVAILABLE_ANALYSES["summarize_modules"],
347
- "fields": {"file": "File"} # Summary shown separately
 
348
  },
349
  "suggest_refactoring": {
350
- "key": "refactoring_suggestions", "title": AVAILABLE_ANALYSES["suggest_refactoring"],
351
- "fields": {"file": "File", "line": "Line", "area": "Area"} # Suggestion shown separately
 
352
  }
353
  }
354
 
355
- # Iterate and display selected sections
356
  any_results = False
357
  for analysis_key in requested_analyses:
358
  if analysis_key in display_config:
@@ -360,18 +359,18 @@ def display_results(results_json, requested_analyses):
360
  st.subheader(config["title"])
361
  items = results_json.get(config["key"], [])
362
  display_list_items(items, config["fields"])
363
- if items: any_results = True
 
364
 
365
  if not any_results:
366
- st.info("No specific findings were identified in the analysis based on your selections.")
367
 
368
- # Download button
369
  st.download_button(
370
- label="Download Full Report (JSON)",
371
- data=json.dumps(results_json, indent=4),
372
- file_name="code_audit_report.json",
373
- mime="application/json"
374
- )
375
 
376
  # --- Streamlit App Main Interface ---
377
  st.set_page_config(page_title="Codebase Audit Assistant", layout="wide")
@@ -382,9 +381,11 @@ st.markdown(f"Upload your codebase (`.zip`) for analysis using **{GEMINI_MODEL_N
382
  # Sidebar controls
383
  with st.sidebar:
384
  st.header("βš™οΈ Analysis Controls")
385
- # Mock Mode Toggle
386
- st.session_state.mock_api_call = st.toggle("πŸ§ͺ Enable Mock API Mode (for Testing)", value=st.session_state.mock_api_call,
387
- help="If enabled, uses fake data instead of calling the real Gemini API. Saves cost during testing.")
 
 
388
  if st.session_state.mock_api_call:
389
  st.info("Mock API Mode ACTIVE")
390
  else:
@@ -409,77 +410,63 @@ with st.sidebar:
409
  "7. Review the report."
410
  )
411
  st.info(f"**Note:** Only files with common code extensions ({', '.join(CODE_EXTENSIONS)}) are processed. Analysis might be limited (~{MAX_PROMPT_TOKENS_ESTIMATE:,} est. tokens).")
412
-
413
  st.divider()
414
  st.warning("⚠️ **Privacy:** Code content is sent to the Google Gemini API if Mock Mode is OFF. Do not upload sensitive code if uncomfortable.")
415
 
416
-
417
  # Main content area
418
  uploaded_file = st.file_uploader("πŸ“ Upload Codebase ZIP File", type=['zip'], key="file_uploader")
419
-
420
  analysis_triggered = False
421
- results_cache = None # To store results briefly
422
 
423
  if uploaded_file:
424
  st.success(f"βœ… File '{uploaded_file.name}' uploaded.")
425
-
426
  with st.spinner("Inspecting ZIP file..."):
427
  code_files, total_chars, file_count, ignored_files = process_zip_file(uploaded_file)
428
 
429
  if code_files is not None:
430
  st.info(f"Found **{file_count}** relevant code files ({total_chars:,} characters). Est. tokens: ~{estimate_token_count(total_chars):,}")
431
  if ignored_files:
432
- with st.expander(f"View {len(ignored_files)} Skipped/Ignored Files"):
433
- # Use st.code for better formatting of list
434
- st.code("\n".join(ignored_files), language='text')
435
 
436
  analyze_button_disabled = (not selected_analyses or file_count == 0)
437
  analyze_button_label = "Analyze Codebase" if not analyze_button_disabled else "Select Analyses or Upload Valid Code"
438
  if st.button(analyze_button_label, type="primary", disabled=analyze_button_disabled):
439
  analysis_triggered = True
440
-
441
  if not selected_analyses:
442
- st.warning("Please select at least one analysis type from the sidebar.")
443
  elif file_count == 0:
444
- st.warning("No relevant code files found in the ZIP archive to analyze.")
445
  else:
446
  st.divider()
447
  with st.spinner(f"πŸš€ Preparing prompt & contacting AI ({'Mock Mode' if st.session_state.mock_api_call else GEMINI_MODEL_NAME})... This may take time."):
448
- # 1. Construct Prompt
449
  analysis_prompt, included_files_in_prompt = construct_analysis_prompt(code_files, selected_analyses)
450
-
451
  if analysis_prompt and included_files_in_prompt:
452
  st.write(f"Analyzing {len(included_files_in_prompt)} files...")
453
- # 2. Call API (Real or Mock)
454
  results_json, error_message = call_gemini_api(analysis_prompt)
455
- results_cache = (results_json, error_message) # Store results
456
  elif not included_files_in_prompt:
457
  results_cache = (None, "Could not proceed: No files included in prompt (check token limits/errors).")
458
  else:
459
- results_cache = (None, "Failed to generate analysis prompt.")
460
-
461
- else: # Error during zip processing
462
- pass # Error message already shown
463
 
464
- # Display results outside the button click block if analysis was triggered
465
  if analysis_triggered and results_cache:
466
  results_json, error_message = results_cache
467
  st.divider()
468
  if error_message:
469
  st.error(f"Analysis Failed: {error_message}")
470
- # Display partial results if available (e.g., raw response on JSON error)
471
  if results_json and isinstance(results_json, dict) and "raw_response" in results_json:
472
- st.subheader("Raw AI Response")
473
- st.code(results_json["raw_response"], language='text')
474
-
475
  elif results_json:
476
  display_results(results_json, selected_analyses)
477
  else:
478
  st.error("Analysis did not return results or an unknown error occurred.")
479
-
480
-
481
  elif not uploaded_file:
482
  st.info("Upload a ZIP file containing your source code to begin.")
483
 
484
  st.divider()
485
- st.markdown("_Assistant powered by Google Gemini._")
 
5
  import json
6
  import os
7
  from pathlib import Path
8
+ import time # Added for simulating mock delay
9
 
10
  # --- Configuration ---
11
  GEMINI_MODEL_NAME = "gemini-2.5-pro-preview-03-25"
12
+ MAX_PROMPT_TOKENS_ESTIMATE = 800000 # Adjust as needed
13
 
14
  AVAILABLE_ANALYSES = {
15
  "generate_docs": "Generate Missing Docstrings/Comments",
 
19
  "suggest_refactoring": "Suggest Refactoring Opportunities"
20
  }
21
 
22
+ CODE_EXTENSIONS = {
23
+ '.py', '.js', '.java', '.c', '.cpp', '.h', '.cs', '.go',
24
+ '.rb', '.php', '.swift', '.kt', '.ts', '.html', '.css', '.scss', '.sql'
25
+ }
26
 
27
  # --- Session State Initialization ---
 
28
  if 'mock_api_call' not in st.session_state:
29
+ st.session_state.mock_api_call = False # Default to using the real API
30
 
31
  # --- Gemini API Setup ---
 
32
  model = None
33
+
34
  def initialize_gemini_model():
35
+ """Initializes the Gemini model if not in mock mode."""
36
  global model
37
  if model is None and not st.session_state.mock_api_call:
38
  try:
 
49
  return False
50
  elif st.session_state.mock_api_call:
51
  print("Running in Mock Mode. Skipping Gemini initialization.")
52
+ return True # Allow proceeding in mock mode
53
  elif model is not None:
54
+ print("Gemini Model already initialized.")
55
+ return True
56
  return False
57
 
 
58
  # --- Helper Functions ---
59
 
60
  def estimate_token_count(text):
61
+ """Roughly estimate token count (assumes ~3-4 characters per token)."""
62
  return len(text) // 3
63
 
64
  def process_zip_file(uploaded_file):
65
+ """
66
+ Extracts code files and their content from the uploaded ZIP file.
67
+
68
+ Returns:
69
+ code_files (dict): Mapping of file paths to content.
70
+ total_chars (int): Total number of characters in included files.
71
+ file_count (int): Count of processed code files.
72
+ ignored_files (list): List of files skipped or not processed.
73
+ """
74
  code_files = {}
75
  total_chars = 0
76
  file_count = 0
 
79
  try:
80
  with zipfile.ZipFile(io.BytesIO(uploaded_file.getvalue()), 'r') as zip_ref:
81
  for member in zip_ref.infolist():
82
+ # Skip directories, hidden files, and files with '__' in the name
83
  if member.is_dir() or any(part.startswith('.') for part in Path(member.filename).parts) or '__' in member.filename:
84
  continue
85
 
 
93
  try:
94
  content = file.read().decode('latin-1')
95
  except Exception as decode_err:
96
+ ignored_files.append(f"{member.filename} (Decode Error: {decode_err})")
97
+ continue
98
 
99
  code_files[member.filename] = content
100
  total_chars += len(content)
101
  file_count += 1
102
  except Exception as read_err:
103
+ ignored_files.append(f"{member.filename} (Read Error: {read_err})")
104
  else:
105
  # Only add to ignored if it's not explicitly ignored by path rules above
106
+ if not (any(part.startswith('.') for part in Path(member.filename).parts) or '__' in member.filename):
107
  ignored_files.append(f"{member.filename} (Skipped Extension: {file_path.suffix})")
108
 
109
  except zipfile.BadZipFile:
 
116
  return code_files, total_chars, file_count, ignored_files
117
 
118
  def construct_analysis_prompt(code_files_dict, requested_analyses):
119
+ """
120
+ Constructs the prompt for Gemini, including code content and a JSON structure request.
121
+
122
+ Returns:
123
+ full_prompt (str): The complete prompt.
124
+ included_files (list): List of file names included in the prompt.
125
+ """
126
  prompt_content = "Analyze the following codebase provided as a collection of file paths and their content.\n\n"
127
  current_token_estimate = estimate_token_count(prompt_content)
128
  included_files = []
 
149
 
150
  prompt_content += concatenated_code
151
 
152
+ # Build the expected JSON structure dynamically based on the selected analyses
153
  json_structure_description = "{\n"
 
154
  structure_parts = []
155
  if "generate_docs" in requested_analyses:
156
  structure_parts.append(' "documentation_suggestions": [{"file": "path/to/file", "line": number, "suggestion": "Suggested docstring/comment"}]')
 
162
  structure_parts.append(' "module_summaries": [{"file": "path/to/file", "summary": "One-paragraph summary of the file purpose/functionality"}]')
163
  if "suggest_refactoring" in requested_analyses:
164
  structure_parts.append(' "refactoring_suggestions": [{"file": "path/to/file", "line": number, "area": "e.g., function name, class name", "suggestion": "Description of refactoring suggestion"}]')
 
165
  json_structure_description += ",\n".join(structure_parts)
166
  json_structure_description += "\n}"
167
 
 
177
  **JSON Output Only:**
178
  """
179
  full_prompt = prompt_content + prompt_footer
 
 
180
  return full_prompt, included_files
181
 
 
182
  def call_gemini_api(prompt):
183
+ """
184
+ Calls the Gemini API (or simulates it in mock mode) with the provided prompt.
185
+
186
+ Returns:
187
+ insights (dict): The parsed JSON response from the API.
188
+ error_message (str): An error message if something went wrong.
189
+ """
190
  if not prompt:
191
  return None, "Prompt generation failed."
192
 
193
  # --- MOCK MODE LOGIC ---
194
  if st.session_state.mock_api_call:
195
  st.info(" MOCK MODE: Simulating API call...")
196
+ time.sleep(2) # Simulate network/processing delay
197
 
198
+ # Simulated successful response
 
199
  mock_json_response = json.dumps({
200
  "documentation_suggestions": [{"file": "mock/core.py", "line": 15, "suggestion": "def process_data(data):\n \"\"\"Processes the input data using mock logic.\"\"\""}],
201
+ "potential_bugs": [{"file": "mock/utils.py", "line": 22, "description": "Potential division by zero if denominator is not checked.", "severity": "Medium"}],
202
  "style_issues": [{"file": "mock/core.py", "line": 5, "description": "Variable 'varName' does not follow snake_case convention."}],
203
+ "module_summaries": [
204
+ {"file": "mock/core.py", "summary": "This file contains the core mock processing logic."},
205
+ {"file": "mock/utils.py", "summary": "Utility functions for mocking."}
206
+ ],
207
+ "refactoring_suggestions": [{"file": "mock/utils.py", "line": 30, "area": "calculate_metrics function", "suggestion": "Function is too long (> 50 lines), consider breaking it down."}]
208
  })
209
  st.success("Mock response generated successfully.")
210
+ return json.loads(mock_json_response), None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
 
212
  # --- REAL API CALL LOGIC ---
213
  else:
214
+ if not initialize_gemini_model():
215
+ return None, "Gemini Model Initialization Failed."
216
+ if model is None:
217
+ return None, "Gemini model not available."
218
 
219
  try:
220
  st.write(f"πŸ“‘ Sending request to {GEMINI_MODEL_NAME}...")
 
230
  )
231
  st.write("βœ… Response received from AI.")
232
 
 
 
 
233
  try:
 
234
  json_response_text = response.text.strip()
235
+ # Remove potential markdown code block fences
236
  if json_response_text.startswith("```json"):
237
  json_response_text = json_response_text[7:]
238
+ if json_response_text.startswith("```"):
239
+ json_response_text = json_response_text[3:]
240
  if json_response_text.endswith("```"):
241
  json_response_text = json_response_text[:-3]
242
 
243
+ # Extract JSON object boundaries
244
  json_start = json_response_text.find('{')
245
  json_end = json_response_text.rfind('}') + 1
246
 
 
258
  st.code(response.text, language='text')
259
  return None, f"AI response was not valid JSON: {json_err}"
260
  except AttributeError:
261
+ st.error("🚨 Unexpected API response structure.")
262
+ st.code(f"Response object: {response}", language='text')
263
+ try:
264
+ block_reason = response.prompt_feedback.block_reason
265
+ if block_reason:
266
+ return None, f"Content blocked by API. Reason: {block_reason}"
267
+ except Exception:
268
+ pass
269
+ return None, "Unexpected response structure from API."
 
 
270
  except Exception as e:
271
  st.error(f"🚨 Unexpected issue processing AI response: {e}")
272
+ try:
273
+ st.code(f"Response object: {response}", language='text')
274
+ except Exception:
275
+ pass
276
  return None, f"Unexpected response structure: {e}"
277
 
278
  except Exception as e:
279
  st.error(f"🚨 An error occurred during API call: {e}")
280
  error_msg = f"API call failed: {e}"
281
+ if hasattr(e, 'message'):
282
+ if "429" in e.message:
283
+ error_msg = "API Quota Exceeded or Rate Limit hit. Check your Google Cloud/AI Studio dashboard."
284
+ elif "API key not valid" in e.message:
285
+ error_msg = "Invalid Gemini API Key. Please check `.streamlit/secrets.toml`."
286
+ elif "blocked" in e.message.lower():
287
+ error_msg = "Content blocked due to safety settings. Review input code or adjust safety settings if appropriate."
288
+ elif "block_reason: SAFETY" in str(e):
289
+ error_msg = "Content blocked due to safety settings. Review input code or adjust safety settings if appropriate."
 
290
 
291
  return None, error_msg
292
 
 
293
  def display_results(results_json, requested_analyses):
294
+ """Renders the analysis results in the Streamlit interface."""
295
  st.header("πŸ“Š Analysis Report")
296
 
297
  if not isinstance(results_json, dict):
 
304
  st.code(results_json["raw_response"], language='text')
305
  return
306
 
 
307
  def display_list_items(items, fields):
308
  if items:
309
  for item in items:
310
  details = []
311
  for field_key, field_label in fields.items():
312
  value = item.get(field_key, 'N/A')
313
+ if value != 'N/A':
314
+ details.append(f"**{field_label}:** {value}")
315
  st.markdown("- " + " - ".join(details))
316
+ # Display multi-line outputs when applicable
317
  if 'suggestion' in item:
318
+ st.code(item['suggestion'], language='text')
319
  elif 'description' in item:
320
+ st.markdown(f" > {item['description']}")
321
  elif 'summary' in item:
322
+ st.markdown(f" > {item['summary']}")
323
  else:
324
  st.markdown("_No items found for this category._")
325
  st.divider()
326
 
 
327
  display_config = {
328
  "generate_docs": {
329
+ "key": "documentation_suggestions",
330
+ "title": AVAILABLE_ANALYSES["generate_docs"],
331
+ "fields": {"file": "File", "line": "Line"}
332
  },
333
  "find_bugs": {
334
+ "key": "potential_bugs",
335
+ "title": AVAILABLE_ANALYSES["find_bugs"],
336
+ "fields": {"file": "File", "line": "Line", "severity": "Severity"}
337
  },
338
  "check_style": {
339
+ "key": "style_issues",
340
+ "title": AVAILABLE_ANALYSES["check_style"],
341
+ "fields": {"file": "File", "line": "Line"}
342
  },
343
  "summarize_modules": {
344
+ "key": "module_summaries",
345
+ "title": AVAILABLE_ANALYSES["summarize_modules"],
346
+ "fields": {"file": "File"}
347
  },
348
  "suggest_refactoring": {
349
+ "key": "refactoring_suggestions",
350
+ "title": AVAILABLE_ANALYSES["suggest_refactoring"],
351
+ "fields": {"file": "File", "line": "Line", "area": "Area"}
352
  }
353
  }
354
 
 
355
  any_results = False
356
  for analysis_key in requested_analyses:
357
  if analysis_key in display_config:
 
359
  st.subheader(config["title"])
360
  items = results_json.get(config["key"], [])
361
  display_list_items(items, config["fields"])
362
+ if items:
363
+ any_results = True
364
 
365
  if not any_results:
366
+ st.info("No specific findings were identified in the analysis based on your selections.")
367
 
 
368
  st.download_button(
369
+ label="Download Full Report (JSON)",
370
+ data=json.dumps(results_json, indent=4),
371
+ file_name="code_audit_report.json",
372
+ mime="application/json"
373
+ )
374
 
375
  # --- Streamlit App Main Interface ---
376
  st.set_page_config(page_title="Codebase Audit Assistant", layout="wide")
 
381
  # Sidebar controls
382
  with st.sidebar:
383
  st.header("βš™οΈ Analysis Controls")
384
+ st.session_state.mock_api_call = st.toggle(
385
+ "πŸ§ͺ Enable Mock API Mode (for Testing)",
386
+ value=st.session_state.mock_api_call,
387
+ help="If enabled, uses fake data instead of calling the real Gemini API. Saves cost during testing."
388
+ )
389
  if st.session_state.mock_api_call:
390
  st.info("Mock API Mode ACTIVE")
391
  else:
 
410
  "7. Review the report."
411
  )
412
  st.info(f"**Note:** Only files with common code extensions ({', '.join(CODE_EXTENSIONS)}) are processed. Analysis might be limited (~{MAX_PROMPT_TOKENS_ESTIMATE:,} est. tokens).")
 
413
  st.divider()
414
  st.warning("⚠️ **Privacy:** Code content is sent to the Google Gemini API if Mock Mode is OFF. Do not upload sensitive code if uncomfortable.")
415
 
 
416
  # Main content area
417
  uploaded_file = st.file_uploader("πŸ“ Upload Codebase ZIP File", type=['zip'], key="file_uploader")
 
418
  analysis_triggered = False
419
+ results_cache = None # To store results briefly
420
 
421
  if uploaded_file:
422
  st.success(f"βœ… File '{uploaded_file.name}' uploaded.")
 
423
  with st.spinner("Inspecting ZIP file..."):
424
  code_files, total_chars, file_count, ignored_files = process_zip_file(uploaded_file)
425
 
426
  if code_files is not None:
427
  st.info(f"Found **{file_count}** relevant code files ({total_chars:,} characters). Est. tokens: ~{estimate_token_count(total_chars):,}")
428
  if ignored_files:
429
+ with st.expander(f"View {len(ignored_files)} Skipped/Ignored Files"):
430
+ st.code("\n".join(ignored_files), language='text')
 
431
 
432
  analyze_button_disabled = (not selected_analyses or file_count == 0)
433
  analyze_button_label = "Analyze Codebase" if not analyze_button_disabled else "Select Analyses or Upload Valid Code"
434
  if st.button(analyze_button_label, type="primary", disabled=analyze_button_disabled):
435
  analysis_triggered = True
 
436
  if not selected_analyses:
437
+ st.warning("Please select at least one analysis type from the sidebar.")
438
  elif file_count == 0:
439
+ st.warning("No relevant code files found in the ZIP archive to analyze.")
440
  else:
441
  st.divider()
442
  with st.spinner(f"πŸš€ Preparing prompt & contacting AI ({'Mock Mode' if st.session_state.mock_api_call else GEMINI_MODEL_NAME})... This may take time."):
 
443
  analysis_prompt, included_files_in_prompt = construct_analysis_prompt(code_files, selected_analyses)
 
444
  if analysis_prompt and included_files_in_prompt:
445
  st.write(f"Analyzing {len(included_files_in_prompt)} files...")
 
446
  results_json, error_message = call_gemini_api(analysis_prompt)
447
+ results_cache = (results_json, error_message)
448
  elif not included_files_in_prompt:
449
  results_cache = (None, "Could not proceed: No files included in prompt (check token limits/errors).")
450
  else:
451
+ results_cache = (None, "Failed to generate analysis prompt.")
452
+ else:
453
+ # Error during ZIP processing (error already displayed)
454
+ pass
455
 
 
456
  if analysis_triggered and results_cache:
457
  results_json, error_message = results_cache
458
  st.divider()
459
  if error_message:
460
  st.error(f"Analysis Failed: {error_message}")
 
461
  if results_json and isinstance(results_json, dict) and "raw_response" in results_json:
462
+ st.subheader("Raw AI Response")
463
+ st.code(results_json["raw_response"], language='text')
 
464
  elif results_json:
465
  display_results(results_json, selected_analyses)
466
  else:
467
  st.error("Analysis did not return results or an unknown error occurred.")
 
 
468
  elif not uploaded_file:
469
  st.info("Upload a ZIP file containing your source code to begin.")
470
 
471
  st.divider()
472
+ st.markdown("_Assistant powered by Google Gemini._")