broadfield-dev commited on
Commit
5a39165
Β·
verified Β·
1 Parent(s): b390278

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +476 -131
app.py CHANGED
@@ -93,18 +93,25 @@ def _clean_filename(filename_line_content):
93
  path_match = re.match(r'^([\w\-\.\s\/\\]+)', text) # Adjusted to be more general
94
  if path_match:
95
  # Further clean if it looks like "path/to/file (description)"
96
- return re.split(r'\s*\(', path_match.group(1).strip(), 1).strip() # Corrected split index
 
 
97
 
98
  # Fallback for more complex lines, like "### File: `src/app.py` (main application)"
99
  backtick_match = re.search(r'`([^`]+)`', text)
100
  if backtick_match:
101
  potential_fn = backtick_match.group(1).strip()
102
- cleaned_fn = re.split(r'\s*\(|\s{2,}', potential_fn, 1).strip() # Corrected split index
 
 
 
103
  if cleaned_fn: return cleaned_fn
104
 
105
- filename_candidate = re.split(r'\s*\(|\s{2,}', text, 1).strip() # Corrected split index
 
 
106
  filename_candidate = filename_candidate.strip('`\'":;,') # Clean common wrapping chars
107
- return filename_candidate if filename_candidate else text # Fallback to original if all else fails
108
 
109
 
110
  def _parse_chat_stream_logic(chat_json_string, existing_files_state=None):
@@ -120,27 +127,57 @@ def _parse_chat_stream_logic(chat_json_string, existing_files_state=None):
120
  except json.JSONDecodeError as e: results["error_message"] = f"JSON Parsing Error: {e}."; return results
121
  except ValueError as e: results["error_message"] = str(e); return results
122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  file_pattern = re.compile(r"### File:\s*(?P<filename_line>[^\n]+)\n(?:```(?P<lang>[\w\.\-\+]*)\n(?P<code>[\s\S]*?)\n```|(?P<binary_msg>\[Binary file(?: - [^\]]+)?\]))")
124
  structure_pattern = re.compile(r"## File Structure\n```(?:(?P<struct_lang>[\w.-]*)\n)?(?P<structure_code>[\s\S]*?)\n```")
125
 
126
- for message_obj in ai_chat_history:
127
- if not isinstance(message_obj, dict): continue
128
- role, content = message_obj.get("role", "").lower(), message_obj.get("content", "")
129
- if role == BOT_ROLE_NAME:
130
- structure_match = structure_pattern.search(content)
131
- if structure_match:
132
- latest_blocks_dict["File Structure (original)"] = {"filename": "File Structure (original)", "language": structure_match.group("struct_lang") or "plaintext", "code": structure_match.group("structure_code").strip(), "is_binary": False, "is_structure_block": True}
133
- for match in file_pattern.finditer(content):
134
- filename = _clean_filename(match.group("filename_line"))
135
- if not filename: continue
136
- lang, code_block, binary_msg = match.group("lang"), match.group("code"), match.group("binary_msg")
137
- item_data = {"filename": filename, "is_binary": False, "is_structure_block": False}
138
- if code_block is not None:
139
- item_data["code"], item_data["language"] = code_block.strip(), (lang.strip().lower() if lang else _infer_lang_from_filename(filename))
140
- elif binary_msg is not None:
141
- item_data["code"], item_data["language"], item_data["is_binary"] = binary_msg.strip(), "binary", True
142
- else: continue
143
- latest_blocks_dict[filename] = item_data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
 
145
  current_parsed_blocks = list(latest_blocks_dict.values())
146
  current_parsed_blocks.sort(key=lambda b: (0, b["filename"]) if b.get("is_structure_block") else (1, b["filename"]))
@@ -152,9 +189,15 @@ def _parse_chat_stream_logic(chat_json_string, existing_files_state=None):
152
  def _export_selected_logic(selected_filenames, space_line_name_for_md, parsed_blocks_for_export):
153
  results = {"output_str": "", "error_message": None, "download_filepath": None}
154
  global parsed_code_blocks_state_cache
155
- if not parsed_blocks_for_export: parsed_blocks_for_export = parsed_code_blocks_state_cache
 
 
156
 
157
- if not selected_filenames and not any(b.get("is_structure_block") for b in parsed_blocks_for_export):
 
 
 
 
158
  results["output_str"] = f"# Space: {space_line_name_for_md}\n## File Structure\n{bbb}\nπŸ“ Root\n{bbb}\n\n*No files to list in structure or export.*"
159
  try:
160
  with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".md", encoding='utf-8') as tmpfile:
@@ -162,27 +205,43 @@ def _export_selected_logic(selected_filenames, space_line_name_for_md, parsed_bl
162
  except Exception as e: print(f"Error creating temp file for empty export: {e}")
163
  return results
164
 
165
- output_lines = [f"# Space: {space_line_name_for_md}", "## File Structure", bbb, "πŸ“ Root"]
166
- filenames_for_structure = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
  if selected_filenames:
168
- filenames_for_structure = sorted(list(set(f for f in selected_filenames if not any(b["filename"] == f and b.get("is_structure_block") for b in parsed_blocks_for_export))))
169
  else:
170
- filenames_for_structure = sorted(list(set(b["filename"] for b in parsed_blocks_for_export if not b.get("is_structure_block") and not b.get("is_binary"))))
171
 
172
- for fname in filenames_for_structure: output_lines.append(f"πŸ“„ {fname}")
173
- output_lines.extend([bbb, "\nBelow are the contents of all files in the space:\n"])
174
- exported_content = False
175
- for block in parsed_blocks_for_export:
176
- is_selected_for_content = (selected_filenames and block["filename"] in selected_filenames) or \
177
- (not selected_filenames and not block.get("is_structure_block") and not block.get("is_binary"))
178
- if is_selected_for_content and not block.get("is_structure_block"):
179
- output_lines.append(f"### File: {block['filename']}")
180
- if block.get('is_binary'): output_lines.append(block['code'])
181
- else: output_lines.extend([f"{bbb}{block.get('language', 'plaintext') or 'plaintext'}", block.get('code',''), bbb])
182
- output_lines.append(""); exported_content = True
183
-
184
- if not exported_content and not filenames_for_structure : output_lines.append("*No file content selected or available for export.*")
185
- elif not exported_content and filenames_for_structure: output_lines.append("*Selected files have no content blocks defined by AI or are binary.*")
186
  final_output_str = "\n".join(output_lines)
187
  results["output_str"] = final_output_str
188
  try:
@@ -195,20 +254,30 @@ def _convert_gr_history_to_api_messages(system_prompt, gr_history, current_user_
195
  messages = [{"role": "system", "content": system_prompt}] if system_prompt else []
196
  for user_msg, bot_msg in gr_history:
197
  if user_msg: messages.append({"role": "user", "content": user_msg})
198
- if bot_msg: messages.append({"role": BOT_ROLE_NAME, "content": bot_msg})
 
199
  if current_user_message: messages.append({"role": "user", "content": current_user_message})
200
  return messages
201
 
202
  def get_latest_bot_message_as_json(gr_history):
203
- if not gr_history or not gr_history[-1] or len(gr_history[-1]) < 2 or not gr_history[-1]: # Check bot message at index 1
204
- return json.dumps([])
205
- return json.dumps([{"role": BOT_ROLE_NAME, "content": gr_history[-1]}], indent=2)
 
 
 
 
 
 
 
 
 
206
 
207
 
208
  def _generate_ui_outputs_from_cache(owner, space_name):
209
  global parsed_code_blocks_state_cache
210
  preview_md_val = "*No files in cache to display.*"
211
- formatted_md_val = f"# Space: {owner}/{space_name}\n## File Structure\n{bbb}\nπŸ“ Root\n{bbb}\n\n*No files in cache.*"
212
  download_file = None
213
 
214
  if parsed_code_blocks_state_cache:
@@ -217,211 +286,473 @@ def _generate_ui_outputs_from_cache(owner, space_name):
217
  preview_md_lines.append(f"\n----\n**File:** `{escape_html_for_markdown(block['filename'])}`")
218
  if block.get('is_structure_block'): preview_md_lines.append(f" (Original File Structure from AI)\n")
219
  elif block.get('is_binary'): preview_md_lines.append(f" (Binary File)\n")
220
- elif block.get('language'): preview_md_lines.append(f" (Language: `{block['language']}`)\n")
221
  else: preview_md_lines.append("\n")
222
- if block.get('is_binary'): preview_md_lines.append(f"\n`{escape_html_for_markdown(block.get('code',''))}`\n")
223
- else: preview_md_lines.append(f"\n{bbb}{block.get('language', 'plaintext') or 'plaintext'}\n{block.get('code','')}\n{bbb}\n")
 
 
 
 
 
 
 
 
 
224
  preview_md_val = "\n".join(preview_md_lines)
225
  space_line_name = f"{owner}/{space_name}" if owner and space_name else (owner or space_name or "your-space")
226
- valid_filenames_for_export = [b["filename"] for b in parsed_code_blocks_state_cache if not b.get("is_structure_block") and not b.get("is_binary") and not (b.get("code", "").startswith("[Error loading content:") or b.get("code", "").startswith("[Binary or Skipped file]"))]
227
- export_result = _export_selected_logic(valid_filenames_for_export, space_line_name, parsed_code_blocks_state_cache)
 
228
  formatted_md_val = export_result["output_str"]
229
  download_file = export_result["download_filepath"]
230
- else:
231
- preview_md_val = "No files loaded or detected yet."
232
- formatted_md_val = f"# Space: {owner or 'owner'}/{space_name or 'space'}\n## File Structure\n{bbb}\nπŸ“ Root\n{bbb}\n\n*No files found or defined for this Space.*" if owner and space_name else "*Load or define a Space to see its Markdown structure.*"
233
  return formatted_md_val, preview_md_val, gr.update(value=download_file, interactive=download_file is not None)
234
 
235
  def handle_groq_chat_submit(user_message, chat_history, api_key_input, model_select, system_prompt, hf_owner_name, hf_repo_name, _current_formatted_markdown):
236
  global parsed_code_blocks_state_cache
237
  _chat_msg_in, _chat_hist, _status = "", list(chat_history), "Initializing..."
238
  _detected_files_update, _formatted_output_update, _download_btn_update = gr.update(), gr.update(), gr.update(interactive=False, value=None)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  if not user_message.strip():
240
  _status = "Cannot send an empty message."
241
  yield (user_message, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update); return
242
  _chat_hist.append((user_message, None)); _status = "Sending to AI..."
243
  yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update)
 
244
  effective_api_key = api_key_input or os.environ.get("GROQ_API_KEY")
245
  if not effective_api_key:
246
  _chat_hist[-1] = (user_message, "Error: Groq API Key not set."); _status = "API Key missing."
247
  yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update); return
 
248
  current_sys_prompt = system_prompt.strip() or DEFAULT_SYSTEM_PROMPT
249
- api_msgs = _convert_gr_history_to_api_messages(current_sys_prompt, _chat_hist[:-1], user_message)
250
- headers, payload = {"Authorization": f"Bearer {effective_api_key}", "Content-Type": "application/json"}, {"model": model_select, "messages": api_msgs}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
251
  try:
252
  _status = f"Waiting for {model_select}..."; yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update)
253
- response = requests.post(GROQ_API_ENDPOINT, headers=headers, json=payload, timeout=180); response.raise_for_status()
 
254
  api_resp_json = response.json()
255
-
256
  bot_response_actual = None
257
- if api_resp_json.get("choices") and len(api_resp_json["choices"]) > 0:
258
- message_obj = api_resp_json["choices"].get("message")
259
- if message_obj and isinstance(message_obj, dict): # Check if message_obj is a dict
260
- bot_response_actual = message_obj.get("content")
 
 
 
 
261
 
262
  if bot_response_actual:
 
263
  _chat_hist[-1] = (user_message, bot_response_actual); _status = "AI response received. Processing files..."
264
  yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update)
265
- latest_bot_message_json = get_latest_bot_message_as_json(_chat_hist)
 
 
 
266
  parsing_res = _parse_chat_stream_logic(latest_bot_message_json, existing_files_state=parsed_code_blocks_state_cache)
 
267
  if parsing_res["error_message"]:
268
  _status = f"Parsing Error: {parsing_res['error_message']}"
 
 
269
  _detected_files_update = gr.Markdown(f"## Parsing Error\n`{escape_html_for_markdown(parsing_res['error_message'])}`")
270
  else:
 
271
  _formatted_output_update, _detected_files_update, _download_btn_update = _generate_ui_outputs_from_cache(hf_owner_name, hf_repo_name)
272
  _status = "Processing complete. Previews updated."
 
273
  yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update); return
274
  else:
 
275
  error_msg = f"API Error: Unexpected response structure or empty message content. Response: {json.dumps(api_resp_json)}"
276
-
277
- _chat_hist[-1] = (user_message, error_msg); _status = error_msg
278
  except requests.exceptions.HTTPError as e: error_msg = f"API HTTP Error: {e} - {e.response.text if e.response else 'No details'}"
279
  except requests.exceptions.RequestException as e: error_msg = f"API Request Error: {e}"
280
  except Exception as e: error_msg = f"Unexpected error in chat submit: {e}"
281
-
282
- if _chat_hist and len(_chat_hist[-1]) == 2 and _chat_hist[-1] is None:
283
- _chat_hist[-1] = (user_message, error_msg)
 
 
 
 
 
 
 
284
  _status = error_msg
285
  yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update)
286
 
 
287
  def handle_load_existing_space(hf_api_key_ui, ui_owner_name, ui_space_name):
288
  global parsed_code_blocks_state_cache
289
  _formatted_md_val, _detected_preview_val, _status_val = "*Loading files...*", "*Loading files...*", f"Loading Space: {ui_owner_name}/{ui_space_name}..."
290
  _file_browser_update, _iframe_html_update, _download_btn_update = gr.update(visible=False, choices=[], value=None), gr.update(value=None, visible=False), gr.update(interactive=False, value=None)
291
  _build_status_clear, _edit_status_clear, _runtime_status_clear = "*Build status will appear here.*", "*Select a file to load or delete.*", "*Space runtime status will appear here after refresh.*"
 
 
292
  yield (_formatted_md_val, _detected_preview_val, _status_val, _file_browser_update, gr.update(value=ui_owner_name), gr.update(value=ui_space_name), _iframe_html_update, _download_btn_update, _build_status_clear, _edit_status_clear, _runtime_status_clear)
 
293
  owner_to_use, updated_owner_name_val = ui_owner_name, ui_owner_name
 
 
294
  if not owner_to_use:
295
  token, token_err = build_logic_get_api_token(hf_api_key_ui)
296
- if token_err or not token: _status_val = f"Error: {token_err or 'Cannot determine owner.'}"
 
297
  else:
298
  try:
299
  user_info = build_logic_whoami(token=token)
300
- if user_info and 'name' in user_info: owner_to_use, updated_owner_name_val = user_info['name'], user_info['name']; _status_val += f" (Auto-detected owner: {owner_to_use})"
301
- else: _status_val = "Error: Could not auto-detect owner."
302
- except Exception as e: _status_val = f"Error auto-detecting owner: {e}"
303
- if "Error:" in _status_val: yield ("*Error*", "*Error*", _status_val, _file_browser_update, updated_owner_name_val, ui_space_name, _iframe_html_update, _download_btn_update, _build_status_clear, _edit_status_clear, _runtime_status_clear); return
 
 
 
304
  if not owner_to_use or not ui_space_name:
305
- _status_val = "Error: Owner and Space Name are required."
306
- yield ("*Error*", "*Error*", _status_val, _file_browser_update, updated_owner_name_val, ui_space_name, _iframe_html_update, _download_btn_update, _build_status_clear, _edit_status_clear, _runtime_status_clear); return
 
 
 
 
307
 
308
  sdk_for_iframe, file_list, err_list_files = get_space_repository_info(hf_api_key_ui, ui_space_name, owner_to_use)
309
- sub_owner, sub_repo = re.sub(r'[^a-z0-9\-]+', '-', owner_to_use.lower()).strip('-'), re.sub(r'[^a-z0-9\-]+', '-', ui_space_name.lower()).strip('-')
 
 
 
310
  iframe_url = f"https://{sub_owner}-{sub_repo}{'.static.hf.space' if sdk_for_iframe == 'static' else '.hf.space'}"
311
  _iframe_html_update = gr.update(value=f'<iframe src="{iframe_url}?__theme=light&embed=true" width="100%" height="500px" style="border:1px solid #eee; border-radius:8px;"></iframe>', visible=True)
 
 
312
  if err_list_files and not file_list:
313
  _status_val = f"File List Error: {err_list_files}"
314
- yield (f"*Error: {err_list_files}*", "*Error*", _status_val, _file_browser_update, updated_owner_name_val, ui_space_name, _iframe_html_update, _download_btn_update, _build_status_clear, _edit_status_clear, _runtime_status_clear); return
 
 
 
 
 
315
  if not file_list:
316
- _status_val = f"Loaded Space: {owner_to_use}/{ui_space_name}. No files found ({err_list_files or 'No files'})."
317
  parsed_code_blocks_state_cache = []
318
  _formatted_md_val, _detected_preview_val, _download_btn_update = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name)
319
  _file_browser_update = gr.update(visible=True, choices=[], value="No files found")
320
- yield (_formatted_md_val, _detected_preview_val, _status_val, _file_browser_update, updated_owner_name_val, ui_space_name, _iframe_html_update, _download_btn_update, _build_status_clear, _edit_status_clear, _runtime_status_clear); return
 
 
321
 
322
  loaded_files_for_parse = []
323
- _status_val = f"Loading {len(file_list)} files from {owner_to_use}/{ui_space_name} (SDK: {sdk_for_iframe or 'unknown'})..."; yield (_formatted_md_val, _detected_preview_val, _status_val, _file_browser_update, updated_owner_name_val, ui_space_name, _iframe_html_update, _download_btn_update, _build_status_clear, _edit_status_clear, _runtime_status_clear)
 
 
 
 
324
  for file_path in file_list:
 
 
325
  _, ext = os.path.splitext(file_path)
326
- if ext.lower() in [".png",".jpg",".jpeg",".gif",".ico",".svg",".pt",".bin",".safetensors",".onnx",".woff",".woff2",".ttf",".eot"] or ".git" in file_path or (file_path.startswith(".") and file_path not in [".env", ".gitignore", ".gitattributes"]):
 
 
 
 
 
 
 
327
  loaded_files_for_parse.append({"filename": file_path, "code": "[Binary or Skipped file]", "language": "binary", "is_binary": True, "is_structure_block": False}); continue
328
- content, err_get = get_space_file_content(hf_api_key_ui, ui_space_name, owner_to_use, file_path)
329
- if err_get: loaded_files_for_parse.append({"filename": file_path, "code": f"[Error loading content: {err_get}]", "language": _infer_lang_from_filename(file_path), "is_binary": False, "is_structure_block": False}); print(f"Error loading {file_path}: {err_get}"); continue
330
- loaded_files_for_parse.append({"filename": file_path, "code": content, "language": _infer_lang_from_filename(file_path), "is_binary": False, "is_structure_block": False})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
  parsed_code_blocks_state_cache = loaded_files_for_parse
332
  _formatted_md_val, _detected_preview_val, _download_btn_update = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name)
333
  _status_val = f"Successfully loaded Space: {owner_to_use}/{ui_space_name}. Markdown ready."
334
- _file_browser_update = gr.update(visible=True, choices=sorted(file_list or []), value=None)
 
335
  yield (_formatted_md_val, _detected_preview_val, _status_val, _file_browser_update, updated_owner_name_val, ui_space_name, _iframe_html_update, _download_btn_update, _build_status_clear, _edit_status_clear, _runtime_status_clear)
336
 
 
337
  def handle_build_space_button(hf_api_key_ui, ui_space_name_part, ui_owner_name_part, space_sdk_ui, formatted_markdown_content):
338
  _build_status, _iframe_html, _file_browser_update = "Starting space build process...", gr.update(value=None, visible=False), gr.update(visible=False, choices=[], value=None)
339
- yield _build_status, _iframe_html, _file_browser_update
340
  if not ui_space_name_part or "/" in ui_space_name_part: _build_status = f"Build Error: HF Space Name '{ui_space_name_part}' must be repo name only (no '/')."; yield _build_status, _iframe_html, _file_browser_update; return
341
  final_owner_for_build = ui_owner_name_part
342
  if not final_owner_for_build:
343
- token_for_whoami, _ = build_logic_get_api_token(hf_api_key_ui)
 
344
  if token_for_whoami:
345
- try: user_info = build_logic_whoami(token=token_for_whoami); final_owner_for_build = user_info['name'] if user_info and 'name' in user_info else final_owner_for_build
346
- except Exception as e: _build_status += f"\n(Warn: Could not auto-detect owner for build: {e})"
347
- else: _build_status += "\n(Warn: Owner not specified and no token to auto-detect for build.)"
348
- if not final_owner_for_build: _build_status = "Build Error: HF Owner Name could not be determined."; yield _build_status, _iframe_html, _file_browser_update; return
 
 
 
 
 
349
  result_message = build_logic_create_space(hf_api_key_ui, ui_space_name_part, final_owner_for_build, space_sdk_ui, formatted_markdown_content)
350
  _build_status = f"Build Process: {result_message}"
 
351
  if "Successfully" in result_message:
352
- sub_owner, sub_repo = re.sub(r'[^a-z0-9\-]+', '-', final_owner_for_build.lower()).strip('-'), re.sub(r'[^a-z0-9\-]+', '-', ui_space_name_part.lower()).strip('-')
 
353
  iframe_url = f"https://{sub_owner}-{sub_repo}{'.static.hf.space' if space_sdk_ui == 'static' else '.hf.space'}"
354
  _iframe_html = gr.update(value=f'<iframe src="{iframe_url}?__theme=light&embed=true" width="100%" height="700px" style="border:1px solid #eee; border-radius:8px;"></iframe>', visible=True)
355
- _build_status += f"\nSpace live at: {iframe_url} (Repo: https://huggingface.co/spaces/{final_owner_for_build}/{ui_space_name_part})"
 
 
356
  file_list, err_list = list_space_files_for_browsing(hf_api_key_ui, ui_space_name_part, final_owner_for_build)
357
  if err_list: _build_status += f"\nFile list refresh error after build: {err_list}"; _file_browser_update = gr.update(visible=True, choices=[], value="Error refreshing files")
358
  else: _file_browser_update = gr.update(visible=True, choices=sorted(file_list or []), value=None if file_list else "No files found")
 
359
  yield _build_status, _iframe_html, _file_browser_update
360
 
 
361
  def handle_load_file_for_editing(hf_api_key_ui, ui_space_name_part, ui_owner_name_part, selected_file_path):
362
- _file_content_val, _edit_status_val, _commit_msg_val, _lang_update = "", "Error: No file selected.", gr.update(), gr.update()
363
- if not selected_file_path or selected_file_path in ["No files found", "Error refreshing files"]: yield _file_content_val, _edit_status_val, _commit_msg_val, _lang_update; return
 
 
 
364
  owner_to_use = ui_owner_name_part
365
  if not owner_to_use:
366
- token, _ = build_logic_get_api_token(hf_api_key_ui)
 
367
  if token:
368
- try: user_info = build_logic_whoami(token=token); owner_to_use = user_info['name'] if user_info and 'name' in user_info else owner_to_use
369
- except Exception as e: print(f"Owner auto-detect fail for editing file: {e}")
 
 
370
  else: _edit_status_val = "Error: HF Owner Name not set and no token to auto-detect."; yield (_file_content_val, _edit_status_val, _commit_msg_val, _lang_update); return
 
371
  if not owner_to_use or not ui_space_name_part: _edit_status_val = "Error: HF Owner and/or Space Name is missing."; yield (_file_content_val, _edit_status_val, _commit_msg_val, _lang_update); return
 
 
 
 
372
  content, err = get_space_file_content(hf_api_key_ui, ui_space_name_part, owner_to_use, selected_file_path)
373
- if err: _edit_status_val, _commit_msg_val = f"Error loading '{selected_file_path}': {err}", f"Error loading {selected_file_path}"; yield (f"Error loading {selected_file_path}:\n{err}", _edit_status_val, _commit_msg_val, _lang_update); return
374
- _file_content_val, _edit_status_val, _commit_msg_val, _lang_update = content or "", f"Loaded {selected_file_path} for editing.", f"Update {selected_file_path} via AI Space Editor", gr.update(language=_infer_lang_from_filename(selected_file_path))
 
 
 
 
 
 
 
 
 
 
 
 
375
  yield _file_content_val, _edit_status_val, _commit_msg_val, _lang_update
376
 
377
  def handle_commit_file_changes(hf_api_key_ui, ui_space_name_part, ui_owner_name_part, file_to_edit_path, edited_content, commit_message):
378
  global parsed_code_blocks_state_cache
379
- _edit_status_val, _file_browser_update_val, _formatted_md_out, _detected_preview_out, _download_btn_out = "Error: No file selected.", gr.update(), gr.update(), gr.update(), gr.update()
380
- if not file_to_edit_path or file_to_edit_path == "No files found": yield _edit_status_val, _file_browser_update_val, _formatted_md_out, _detected_preview_out, _download_btn_out; return
 
 
 
 
 
 
 
 
381
  owner_to_use = ui_owner_name_part
382
  if not owner_to_use:
383
- token, _ = build_logic_get_api_token(hf_api_key_ui)
 
384
  if token:
385
- try: user_info = build_logic_whoami(token=token); owner_to_use = user_info['name'] if user_info and 'name' in user_info else owner_to_use
386
- except Exception as e: print(f"Owner auto-detect fail for committing file: {e}")
387
- else: _edit_status_val = "Error: HF Owner Name not set and no token to auto-detect."; yield (_edit_status_val, _file_browser_update_val, _formatted_md_out, _detected_preview_out, _download_btn_out); return
388
- if not owner_to_use or not ui_space_name_part: _edit_status_val = "Error: HF Owner and/or Space Name is missing."; yield (_edit_status_val, _file_browser_update_val, _formatted_md_out, _detected_preview_out, _download_btn_out); return
 
 
 
 
389
  status_msg = update_space_file(hf_api_key_ui, ui_space_name_part, owner_to_use, file_to_edit_path, edited_content, commit_message)
390
  _edit_status_val = status_msg
 
391
  if "Successfully updated" in status_msg:
 
392
  found_in_cache = False
393
  for block in parsed_code_blocks_state_cache:
394
- if block["filename"] == file_to_edit_path: block["code"], block["language"], block["is_binary"], found_in_cache = edited_content, _infer_lang_from_filename(file_to_edit_path), False, True; break
395
- if not found_in_cache: parsed_code_blocks_state_cache.append({"filename": file_to_edit_path, "code": edited_content, "language": _infer_lang_from_filename(file_to_edit_path), "is_binary": False, "is_structure_block": False}); parsed_code_blocks_state_cache.sort(key=lambda b: (0, b["filename"]) if b.get("is_structure_block") else (1, b["filename"]))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
396
  _formatted_md_out, _detected_preview_out, _download_btn_out = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name_part)
397
- _file_browser_update_val = gr.update(value=file_to_edit_path)
 
 
 
 
 
 
 
 
398
  yield _edit_status_val, _file_browser_update_val, _formatted_md_out, _detected_preview_out, _download_btn_out
399
 
 
400
  def handle_delete_file(hf_api_key_ui, ui_space_name_part, ui_owner_name_part, file_to_delete_path):
401
  global parsed_code_blocks_state_cache
402
- _edit_status_val, _file_browser_choices_update, _file_browser_value_update, _file_content_editor_update = "Processing deletion...", gr.update(), gr.update(), gr.update(value="")
 
 
 
 
 
403
  _formatted_md_out, _detected_preview_out, _download_btn_out = gr.update(), gr.update(), gr.update()
404
- yield (_edit_status_val, _file_browser_choices_update, _file_browser_value_update, _file_content_editor_update, _formatted_md_out, _detected_preview_out, _download_btn_out)
405
- if not file_to_delete_path or file_to_delete_path in ["No files found", "Error refreshing files"]: _edit_status_val = "Error: No file selected for deletion."; yield (_edit_status_val, gr.update(), gr.update(), _file_content_editor_update, gr.update(), gr.update(), gr.update()); return
 
 
 
 
 
 
406
  owner_to_use = ui_owner_name_part
407
  if not owner_to_use:
408
- token, _ = build_logic_get_api_token(hf_api_key_ui)
 
409
  if token:
410
- try: user_info = build_logic_whoami(token=token); owner_to_use = user_info['name'] if user_info and 'name' in user_info else owner_to_use
411
- except Exception as e: print(f"Owner auto-detect fail for deleting file: {e}"); _edit_status_val = "Error: Could not auto-detect owner."; yield (_edit_status_val, gr.update(), gr.update(), _file_content_editor_update, gr.update(), gr.update(), gr.update()); return
412
- else: _edit_status_val = "Error: HF Token needed to auto-detect owner."; yield (_edit_status_val, gr.update(), gr.update(), _file_content_editor_update, gr.update(), gr.update(), gr.update()); return
413
- if not owner_to_use or not ui_space_name_part: _edit_status_val = "Error: Owner and Space Name are required."; yield (_edit_status_val, gr.update(), gr.update(), _file_content_editor_update, gr.update(), gr.update(), gr.update()); return
 
 
 
 
 
414
  deletion_status_msg = build_logic_delete_space_file(hf_api_key_ui, ui_space_name_part, owner_to_use, file_to_delete_path)
415
  _edit_status_val = deletion_status_msg
 
416
  if "Successfully deleted" in deletion_status_msg:
 
417
  parsed_code_blocks_state_cache = [b for b in parsed_code_blocks_state_cache if b["filename"] != file_to_delete_path]
 
 
418
  _formatted_md_out, _detected_preview_out, _download_btn_out = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name_part)
 
 
419
  new_file_list, err_list = list_space_files_for_browsing(hf_api_key_ui, ui_space_name_part, owner_to_use)
420
- if err_list: _edit_status_val += f"\nFile list refresh error: {err_list}"; _file_browser_choices_update = gr.update(choices=[], value="Error refreshing files")
421
- else: _file_browser_choices_update = gr.update(choices=sorted(new_file_list or []), value=None)
422
- _file_browser_value_update = None
423
- else: new_file_list, _ = list_space_files_for_browsing(hf_api_key_ui, ui_space_name_part, owner_to_use); _file_browser_choices_update = gr.update(choices=sorted(new_file_list or []), value=file_to_delete_path)
424
- yield (_edit_status_val, _file_browser_choices_update, _file_browser_value_update, _file_content_editor_update, _formatted_md_out, _detected_preview_out, _download_btn_out)
 
 
 
 
 
 
 
 
 
 
 
425
 
426
  def handle_refresh_space_status(hf_api_key_ui, ui_owner_name, ui_space_name):
427
  yield "*Fetching space status...*" # Initial feedback
@@ -433,25 +764,29 @@ def handle_refresh_space_status(hf_api_key_ui, ui_owner_name, ui_space_name):
433
  except Exception as e: yield f"**Error auto-detecting owner:** {e}"; return
434
  if not owner_to_use or not ui_space_name: yield "**Error:** Owner and Space Name are required."; return
435
  status_details, error_msg = get_space_runtime_status(hf_api_key_ui, ui_space_name, owner_to_use)
436
- if error_msg: _status_display_md = f"**Error fetching status for {owner_to_use}/{ui_space_name}:**\n\n`{error_msg}`"
437
  elif status_details:
438
  stage, hardware, error, log_link = status_details.get('stage','N/A'), status_details.get('hardware','N/A'), status_details.get('error_message'), status_details.get('full_log_link','#')
439
  md_lines = [f"### Space Status: {owner_to_use}/{ui_space_name}", f"- **Stage:** `{stage}`", f"- **Current Hardware:** `{hardware}`"]
440
  if status_details.get('requested_hardware') and status_details.get('requested_hardware') != hardware: md_lines.append(f"- **Requested Hardware:** `{status_details.get('requested_hardware')}`")
441
  if error: md_lines.append(f"- **Error:** <span style='color:red;'>`{escape_html_for_markdown(error)}`</span>")
442
  md_lines.append(f"- [View Full Logs on Hugging Face]({log_link})")
443
- # md_lines.append(f"\n<details><summary>Raw Status Data (JSON)</summary>\n```json\n{json.dumps(status_details.get('raw_data', {}), indent=2)}\n```\n</details>")
 
 
 
444
  _status_display_md = "\n".join(md_lines)
445
  else: _status_display_md = "Could not retrieve status details."
446
  yield _status_display_md
447
 
 
448
  with gr.Blocks(theme=gr.themes.Soft(primary_hue="teal", secondary_hue="orange")) as demo:
449
  gr.Markdown("# πŸ€– AI Code & Space Generator")
450
  gr.Markdown("Configure settings, chat with AI to generate/modify Hugging Face Spaces, then build, preview, and edit.")
451
  with gr.Row():
452
  with gr.Sidebar():
453
  gr.Markdown("## βš™οΈ Configuration")
454
- with gr.Group(): gr.Markdown("### API Keys & Tokens"); groq_api_key_input = gr.Textbox(label="Groq API Key", type="password", placeholder="gsk_..."); hf_api_key_input = gr.Textbox(label="Hugging Face Token", type="password", placeholder="hf_...")
455
  with gr.Group(): gr.Markdown("### Hugging Face Space"); owner_name_input = gr.Textbox(label="HF Owner Name", placeholder="e.g., your-username"); space_name_input = gr.Textbox(label="HF Space Name", value="my-ai-space", placeholder="e.g., my-cool-app"); space_sdk_select = gr.Dropdown(label="Space SDK", choices=["gradio", "streamlit", "docker", "static"], value="gradio", info="Used for new/build."); load_space_button = gr.Button("πŸ”„ Load Existing Space", variant="secondary", size="sm")
456
  with gr.Group(): gr.Markdown("### AI Model Settings"); groq_model_select = gr.Dropdown(label="Groq Model", choices=["mixtral-8x7b-32768", "llama3-8b-8192", "llama3-70b-8192", "gemma-7b-it"], value="llama3-8b-8192"); groq_system_prompt_input = gr.Textbox(label="System Prompt", lines=8, value=DEFAULT_SYSTEM_PROMPT, interactive=True)
457
  with gr.Column(scale=3):
@@ -462,12 +797,12 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="teal", secondary_hue="orange"))
462
  gr.Markdown("---")
463
  with gr.Tabs():
464
  with gr.TabItem("πŸ“ Formatted Space Markdown"): gr.Markdown("Complete Markdown definition for your Space."); formatted_space_output_display = gr.Textbox(label="Current Space Definition", lines=15, interactive=True, show_copy_button=True, value="*Space definition...*"); download_button = gr.DownloadButton(label="Download .md", interactive=False, size="sm")
465
- with gr.TabItem("πŸ” Detected Files Preview"): gr.Markdown("Preview of files and content from AI/loaded Space."); detected_files_preview = gr.Markdown(value="*Files preview...*")
466
  gr.Markdown("---")
467
  with gr.Tabs():
468
  with gr.TabItem("πŸš€ Build & Preview Space"):
469
  with gr.Row(): build_space_button = gr.Button("Build / Update Space on HF", variant="primary", scale=2); refresh_status_button = gr.Button("πŸ”„ Refresh Space Status", scale=1)
470
- build_status_display = gr.Textbox(label="Build Operation Status", interactive=False, lines=2, value="*Build status...*"); gr.Markdown("---"); space_runtime_status_display = gr.Markdown("*Space runtime status...*"); gr.Markdown("---"); space_iframe_display = gr.HTML(value="<!-- Space Iframe -->", visible=False)
471
  with gr.TabItem("✏️ Edit Space Files"):
472
  gr.Markdown("Select a file to view, edit, or delete. Changes are committed to HF Hub.")
473
  file_browser_dropdown = gr.Dropdown(label="Select File in Space", choices=[], interactive=True, visible=False, info="Load/build Space first.")
@@ -477,19 +812,29 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="teal", secondary_hue="orange"))
477
  edit_status_display = gr.Textbox(label="File Edit/Delete Status", interactive=False, lines=2, value="*Select file...*")
478
 
479
  chat_outputs = [groq_chat_message_input, groq_chatbot_display, groq_status_output, detected_files_preview, formatted_space_output_display, download_button]
480
- chat_inputs = [groq_chat_message_input, groq_chatbot_display, groq_api_key_input, groq_model_select, groq_system_prompt_input, owner_name_input, space_name_input, formatted_space_output_display]
481
  groq_send_chat_button.click(fn=handle_groq_chat_submit, inputs=chat_inputs, outputs=chat_outputs)
482
  groq_chat_message_input.submit(fn=handle_groq_chat_submit, inputs=chat_inputs, outputs=chat_outputs)
 
 
483
  load_space_outputs = [formatted_space_output_display, detected_files_preview, groq_status_output, file_browser_dropdown, owner_name_input, space_name_input, space_iframe_display, download_button, build_status_display, edit_status_display, space_runtime_status_display]
484
  load_space_button.click(fn=handle_load_existing_space, inputs=[hf_api_key_input, owner_name_input, space_name_input], outputs=load_space_outputs)
 
485
  build_outputs = [build_status_display, space_iframe_display, file_browser_dropdown]
486
  build_space_button.click(fn=handle_build_space_button, inputs=[hf_api_key_input, space_name_input, owner_name_input, space_sdk_select, formatted_space_output_display], outputs=build_outputs)
 
 
487
  file_edit_load_outputs = [file_content_editor, edit_status_display, commit_message_input, file_content_editor]
488
  file_browser_dropdown.change(fn=handle_load_file_for_editing, inputs=[hf_api_key_input, space_name_input, owner_name_input, file_browser_dropdown], outputs=file_edit_load_outputs)
 
 
489
  commit_file_outputs = [edit_status_display, file_browser_dropdown, formatted_space_output_display, detected_files_preview, download_button]
490
  update_file_button.click(fn=handle_commit_file_changes, inputs=[hf_api_key_input, space_name_input, owner_name_input, file_browser_dropdown, file_content_editor, commit_message_input], outputs=commit_file_outputs)
491
- delete_file_outputs = [edit_status_display, file_browser_dropdown, file_browser_dropdown, file_content_editor, formatted_space_output_display, detected_files_preview, download_button]
 
 
492
  delete_file_button.click(fn=handle_delete_file, inputs=[hf_api_key_input, space_name_input, owner_name_input, file_browser_dropdown], outputs=delete_file_outputs)
 
493
  refresh_status_button.click(fn=handle_refresh_space_status, inputs=[hf_api_key_input, owner_name_input, space_name_input], outputs=[space_runtime_status_display])
494
 
495
  if __name__ == "__main__":
 
93
  path_match = re.match(r'^([\w\-\.\s\/\\]+)', text) # Adjusted to be more general
94
  if path_match:
95
  # Further clean if it looks like "path/to/file (description)"
96
+ # Corrected split index was already correct in the original code, just ensure it's applied
97
+ parts = re.split(r'\s*\(', path_match.group(1).strip(), 1)
98
+ return parts[0].strip() if parts else ""
99
 
100
  # Fallback for more complex lines, like "### File: `src/app.py` (main application)"
101
  backtick_match = re.search(r'`([^`]+)`', text)
102
  if backtick_match:
103
  potential_fn = backtick_match.group(1).strip()
104
+ # Corrected split index was already correct
105
+ parts = re.split(r'\s*\(|\s{2,}', potential_fn, 1)
106
+ cleaned_fn = parts[0].strip() if parts else ""
107
+ cleaned_fn = cleaned_fn.strip('`\'":;,') # Clean common wrapping chars
108
  if cleaned_fn: return cleaned_fn
109
 
110
+ # Final fallback
111
+ parts = re.split(r'\s*\(|\s{2,}', text, 1)
112
+ filename_candidate = parts[0].strip() if parts else text.strip()
113
  filename_candidate = filename_candidate.strip('`\'":;,') # Clean common wrapping chars
114
+ return filename_candidate if filename_candidate else text.strip()
115
 
116
 
117
  def _parse_chat_stream_logic(chat_json_string, existing_files_state=None):
 
127
  except json.JSONDecodeError as e: results["error_message"] = f"JSON Parsing Error: {e}."; return results
128
  except ValueError as e: results["error_message"] = str(e); return results
129
 
130
+ # Ensure the bot's message is the last one and is the only one being parsed for new/updated files
131
+ # This prevents reprocessing the entire history on every new AI turn
132
+ message_obj = None
133
+ if ai_chat_history and isinstance(ai_chat_history[-1], dict) and ai_chat_history[-1].get("role", "").lower() == BOT_ROLE_NAME:
134
+ message_obj = ai_chat_history[-1]
135
+
136
+ if not message_obj:
137
+ # If the last message isn't the bot's or is malformed, don't update state based on it
138
+ results["parsed_code_blocks"] = list(latest_blocks_dict.values()) # Return existing state
139
+ results["default_selected_filenames"] = [b["filename"] for b in results["parsed_code_blocks"] if not b.get("is_structure_block")]
140
+ # results["error_message"] = "No bot message found in the last entry for parsing." # Optional: add a debug message
141
+ return results
142
+
143
+
144
+ role, content = message_obj.get("role", "").lower(), message_obj.get("content", "")
145
+
146
  file_pattern = re.compile(r"### File:\s*(?P<filename_line>[^\n]+)\n(?:```(?P<lang>[\w\.\-\+]*)\n(?P<code>[\s\S]*?)\n```|(?P<binary_msg>\[Binary file(?: - [^\]]+)?\]))")
147
  structure_pattern = re.compile(r"## File Structure\n```(?:(?P<struct_lang>[\w.-]*)\n)?(?P<structure_code>[\s\S]*?)\n```")
148
 
149
+ # Process only the *last* bot message for updates to file blocks
150
+ if role == BOT_ROLE_NAME:
151
+ structure_match = structure_pattern.search(content)
152
+ if structure_match:
153
+ # Overwrite or add the structure block from the latest response
154
+ latest_blocks_dict["File Structure (original)"] = {"filename": "File Structure (original)", "language": structure_match.group("struct_lang") or "plaintext", "code": structure_match.group("structure_code").strip(), "is_binary": False, "is_structure_block": True}
155
+
156
+ # Find all file blocks in the latest message
157
+ current_message_file_blocks = {}
158
+ for match in file_pattern.finditer(content):
159
+ filename = _clean_filename(match.group("filename_line"))
160
+ if not filename: continue
161
+ lang, code_block, binary_msg = match.group("lang"), match.group("code"), match.group("binary_msg")
162
+ item_data = {"filename": filename, "is_binary": False, "is_structure_block": False}
163
+ if code_block is not None:
164
+ item_data["code"], item_data["language"] = code_block.strip(), (lang.strip().lower() if lang else _infer_lang_from_filename(filename))
165
+ elif binary_msg is not None:
166
+ item_data["code"], item_data["language"], item_data["is_binary"] = binary_msg.strip(), "binary", True
167
+ else: continue
168
+ current_message_file_blocks[filename] = item_data
169
+
170
+ # Update latest_blocks_dict with blocks from the current message
171
+ # Any file mentioned in the latest message replaces its old version
172
+ # Any file *not* mentioned in the latest message but present previously is kept
173
+ # If the AI response implies deletion (e.g., by omitting a file it previously listed),
174
+ # the user needs to signal this intention in a way the prompt covers (e.g., "omit it from your next full ### File: list").
175
+ # Our current logic keeps files unless explicitly removed from the *entire* chat history's final list of blocks.
176
+ # A more sophisticated approach might look at AI language indicating deletion, but simple omission
177
+ # in a single turn's output isn't a reliable delete signal with complex interaction.
178
+ # For this implementation, we'll just update based on the latest message's defined blocks.
179
+ latest_blocks_dict.update(current_message_file_blocks)
180
+
181
 
182
  current_parsed_blocks = list(latest_blocks_dict.values())
183
  current_parsed_blocks.sort(key=lambda b: (0, b["filename"]) if b.get("is_structure_block") else (1, b["filename"]))
 
189
  def _export_selected_logic(selected_filenames, space_line_name_for_md, parsed_blocks_for_export):
190
  results = {"output_str": "", "error_message": None, "download_filepath": None}
191
  global parsed_code_blocks_state_cache
192
+ # Use parsed_blocks_for_export if provided, otherwise use cache
193
+ if parsed_blocks_for_export is None:
194
+ parsed_blocks_for_export = parsed_code_blocks_state_cache
195
 
196
+ # Filter out structure blocks and binary/error files for file listing/export
197
+ exportable_blocks = [b for b in parsed_blocks_for_export if not b.get("is_structure_block") and not b.get("is_binary") and not (b.get("code", "").startswith("[Error loading content:") or b.get("code", "").startswith("[Binary or Skipped file]"))]
198
+ binary_blocks = [b for b in parsed_blocks_for_export if b.get("is_binary") or b.get("code", "").startswith("[Binary or Skipped file]")]
199
+
200
+ if not exportable_blocks and not binary_blocks and not any(b.get("is_structure_block") for b in parsed_blocks_for_export):
201
  results["output_str"] = f"# Space: {space_line_name_for_md}\n## File Structure\n{bbb}\nπŸ“ Root\n{bbb}\n\n*No files to list in structure or export.*"
202
  try:
203
  with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".md", encoding='utf-8') as tmpfile:
 
205
  except Exception as e: print(f"Error creating temp file for empty export: {e}")
206
  return results
207
 
208
+ output_lines = [f"# Space: {space_line_name_for_md}"]
209
+
210
+ # Add File Structure block if it exists in parsed blocks
211
+ structure_block = next((b for b in parsed_blocks_for_export if b.get("is_structure_block")), None)
212
+ if structure_block:
213
+ output_lines.extend(["## File Structure", bbb, structure_block["code"].strip(), bbb, ""])
214
+ else:
215
+ # If no structure block from AI, generate a simple one from detected files
216
+ output_lines.extend(["## File Structure", bbb, "πŸ“ Root"])
217
+ filenames_for_structure_list = sorted(list(set(b["filename"] for b in exportable_blocks + binary_blocks))) # Include binary files in structure list
218
+ if filenames_for_structure_list:
219
+ for fname in filenames_for_structure_list: output_lines.append(f" πŸ“„ {fname}")
220
+ output_lines.extend([bbb, ""])
221
+
222
+ output_lines.append("Below are the contents of all files in the space:\n")
223
+ exported_content = False
224
+
225
+ # Determine which files to export content for based on selection or default
226
+ files_to_export_content = []
227
  if selected_filenames:
228
+ files_to_export_content = [b for b in parsed_blocks_for_export if b["filename"] in selected_filenames and not b.get("is_structure_block")]
229
  else:
230
+ files_to_export_content = [b for b in parsed_blocks_for_export if not b.get("is_structure_block")]
231
 
232
+ # Sort blocks for consistent output order
233
+ files_to_export_content.sort(key=lambda b: (0, b["filename"]) if b.get("is_binary") else (1, b["filename"]))
234
+
235
+ for block in files_to_export_content:
236
+ output_lines.append(f"### File: {block['filename']}")
237
+ if block.get('is_binary') or block.get("code", "").startswith("["): # Treat errors/skipped as binary for output format
238
+ output_lines.append(block.get('code',''))
239
+ else:
240
+ output_lines.extend([f"{bbb}{block.get('language', 'plaintext') or 'plaintext'}", block.get('code',''), bbb])
241
+ output_lines.append(""); exported_content = True
242
+
243
+ if not exported_content and not filenames_for_structure_list: output_lines.append("*No file content selected or available for export.*")
244
+ elif not exported_content and filenames_for_structure_list: output_lines.append("*Selected files have no content blocks defined by AI, are binary, or encountered loading errors.*")
 
245
  final_output_str = "\n".join(output_lines)
246
  results["output_str"] = final_output_str
247
  try:
 
254
  messages = [{"role": "system", "content": system_prompt}] if system_prompt else []
255
  for user_msg, bot_msg in gr_history:
256
  if user_msg: messages.append({"role": "user", "content": user_msg})
257
+ # Ensure bot_msg is not None or empty before adding
258
+ if bot_msg and isinstance(bot_msg, str): messages.append({"role": BOT_ROLE_NAME, "content": bot_msg})
259
  if current_user_message: messages.append({"role": "user", "content": current_user_message})
260
  return messages
261
 
262
  def get_latest_bot_message_as_json(gr_history):
263
+ # Find the last non-empty bot message
264
+ latest_bot_msg_content = None
265
+ for user_msg, bot_msg in reversed(gr_history):
266
+ if bot_msg and isinstance(bot_msg, str):
267
+ latest_bot_msg_content = bot_msg
268
+ break # Found the latest, stop searching
269
+
270
+ if latest_bot_msg_content is None:
271
+ return json.dumps([]) # Return empty list if no bot message found
272
+
273
+ # Return JSON containing only the latest bot message for parsing
274
+ return json.dumps([{"role": BOT_ROLE_NAME, "content": latest_bot_msg_content}], indent=2)
275
 
276
 
277
  def _generate_ui_outputs_from_cache(owner, space_name):
278
  global parsed_code_blocks_state_cache
279
  preview_md_val = "*No files in cache to display.*"
280
+ formatted_md_val = f"# Space: {owner}/{space_name}\n## File Structure\n{bbb}\nπŸ“ Root\n{bbb}\n\n*No files in cache.*" if owner or space_name else "*Load or define a Space to see its Markdown structure.*"
281
  download_file = None
282
 
283
  if parsed_code_blocks_state_cache:
 
286
  preview_md_lines.append(f"\n----\n**File:** `{escape_html_for_markdown(block['filename'])}`")
287
  if block.get('is_structure_block'): preview_md_lines.append(f" (Original File Structure from AI)\n")
288
  elif block.get('is_binary'): preview_md_lines.append(f" (Binary File)\n")
289
+ elif block.get('language') and block.get('language') != 'binary': preview_md_lines.append(f" (Language: `{block['language']}`)\n")
290
  else: preview_md_lines.append("\n")
291
+
292
+ # Handle content display
293
+ content = block.get('code', '')
294
+ if block.get('is_binary') or content.startswith("[") : # Treat errors/skipped as binary for preview display
295
+ preview_md_lines.append(f"\n`{escape_html_for_markdown(content)}`\n")
296
+ elif block.get('is_structure_block'):
297
+ preview_md_lines.append(f"\n{bbb}{block.get('language', 'plaintext') or 'plaintext'}\n{content}\n{bbb}\n")
298
+ else:
299
+ preview_md_lines.append(f"\n{bbb}{block.get('language', 'plaintext') or 'plaintext'}\n{content}\n{bbb}\n")
300
+
301
+
302
  preview_md_val = "\n".join(preview_md_lines)
303
  space_line_name = f"{owner}/{space_name}" if owner and space_name else (owner or space_name or "your-space")
304
+
305
+ # _export_selected_logic handles selecting which files to include in the export MD
306
+ export_result = _export_selected_logic(None, space_line_name, parsed_code_blocks_state_cache) # Passing None means export all non-structure/non-binary/non-error content
307
  formatted_md_val = export_result["output_str"]
308
  download_file = export_result["download_filepath"]
309
+
 
 
310
  return formatted_md_val, preview_md_val, gr.update(value=download_file, interactive=download_file is not None)
311
 
312
  def handle_groq_chat_submit(user_message, chat_history, api_key_input, model_select, system_prompt, hf_owner_name, hf_repo_name, _current_formatted_markdown):
313
  global parsed_code_blocks_state_cache
314
  _chat_msg_in, _chat_hist, _status = "", list(chat_history), "Initializing..."
315
  _detected_files_update, _formatted_output_update, _download_btn_update = gr.update(), gr.update(), gr.update(interactive=False, value=None)
316
+
317
+ # --- Before sending to AI: Parse existing files from the current formatted markdown ---
318
+ # This ensures the AI is aware of the *current* state including user edits
319
+ # or files loaded from HF, before it generates its response.
320
+ # Only do this on a new user message
321
+ if user_message and _current_formatted_markdown:
322
+ try:
323
+ parsed_from_md = build_logic_parse_markdown(_current_formatted_markdown)
324
+ # Update cache only with files parsed from the markdown, preserving structure block if any
325
+ structure_block = next((b for b in parsed_code_blocks_state_cache if b.get("is_structure_block")), None)
326
+ parsed_code_blocks_state_cache = []
327
+ if structure_block:
328
+ parsed_code_blocks_state_cache.append(structure_block)
329
+
330
+ for f_info in parsed_from_md.get("files", []):
331
+ # Only add if it has content and isn't the structure block representation
332
+ if f_info.get("path") and f_info.get("path") != "File Structure (original)":
333
+ # Check if it's a binary representation string
334
+ is_binary_repr = isinstance(f_info.get("content"), str) and (f_info["content"].startswith("[Binary file") or f_info["content"].startswith("[Error loading content:") or f_info["content"].startswith("[Binary or Skipped file]"))
335
+ parsed_code_blocks_state_cache.append({
336
+ "filename": f_info["path"],
337
+ "code": f_info.get("content", ""),
338
+ "language": "binary" if is_binary_repr else _infer_lang_from_filename(f_info["path"]),
339
+ "is_binary": is_binary_repr,
340
+ "is_structure_block": False
341
+ })
342
+ parsed_code_blocks_state_cache.sort(key=lambda b: (0, b["filename"]) if b.get("is_structure_block") else (1, b["filename"]))
343
+ # print(f"Parsed {len(parsed_code_blocks_state_cache)} blocks from current Markdown.")
344
+ # print(parsed_code_blocks_state_cache)
345
+
346
+ except Exception as e:
347
+ # Log error but don't block chat submission
348
+ print(f"Error parsing formatted markdown before chat submit: {e}")
349
+ # Optionally update status: _status = f"Warning: Error parsing current files: {e}. Sending message anyway."
350
+
351
+ # --- End of pre-chat parsing ---
352
+
353
+
354
  if not user_message.strip():
355
  _status = "Cannot send an empty message."
356
  yield (user_message, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update); return
357
  _chat_hist.append((user_message, None)); _status = "Sending to AI..."
358
  yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update)
359
+
360
  effective_api_key = api_key_input or os.environ.get("GROQ_API_KEY")
361
  if not effective_api_key:
362
  _chat_hist[-1] = (user_message, "Error: Groq API Key not set."); _status = "API Key missing."
363
  yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update); return
364
+
365
  current_sys_prompt = system_prompt.strip() or DEFAULT_SYSTEM_PROMPT
366
+
367
+ # Include current file contents in the prompt as context
368
+ current_files_context = ""
369
+ if parsed_code_blocks_state_cache:
370
+ current_files_context = "\n\n## Current Files in Space\n"
371
+ for block in parsed_code_blocks_state_cache:
372
+ if block.get("is_structure_block"):
373
+ current_files_context += f"### File: {block['filename']}\n{bbb}\n{block['code']}\n{bbb}\n"
374
+ else:
375
+ current_files_context += f"### File: {block['filename']}\n"
376
+ if block.get("is_binary"):
377
+ current_files_context += f"{block['code']}\n" # e.g. [Binary file...]
378
+ else:
379
+ current_files_context += f"{bbb}{block.get('language', 'plaintext') or 'plaintext'}\n{block.get('code','')}\n{bbb}\n"
380
+ current_files_context += "\n"
381
+
382
+ # Append current file context to the user message
383
+ user_message_with_context = user_message.strip()
384
+ if current_files_context.strip():
385
+ user_message_with_context = user_message_with_context + current_files_context + "Based on the current files above and our chat history, please provide updated file contents using the `### File: ...\n```...\n```\n` format for any files you are creating, modifying, or want to include in the final output. Omit files you want to delete from your response."
386
+
387
+
388
+ api_msgs = _convert_gr_history_to_api_messages(current_sys_prompt, _chat_hist[:-1], user_message_with_context)
389
+
390
+ headers = {"Authorization": f"Bearer {effective_api_key}", "Content-Type": "application/json"}
391
+ payload = {"model": model_select, "messages": api_msgs}
392
+
393
  try:
394
  _status = f"Waiting for {model_select}..."; yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update)
395
+ response = requests.post(GROQ_API_ENDPOINT, headers=headers, json=payload, timeout=180)
396
+ response.raise_for_status()
397
  api_resp_json = response.json()
398
+
399
  bot_response_actual = None
400
+ # --- Corrected logic to safely access the message content ---
401
+ if api_resp_json and isinstance(api_resp_json, dict) and api_resp_json.get("choices") and isinstance(api_resp_json["choices"], list) and len(api_resp_json["choices"]) > 0:
402
+ first_choice = api_resp_json["choices"][0] # Access the first item in the 'choices' list
403
+ if isinstance(first_choice, dict) and first_choice.get("message"): # Ensure it's a dict and has 'message' key
404
+ message_obj = first_choice["message"] # Get the 'message' dictionary from the choice
405
+ if isinstance(message_obj, dict): # Ensure message_obj is a dictionary
406
+ bot_response_actual = message_obj.get("content") # Get content from the message dictionary
407
+ # --- End corrected logic ---
408
 
409
  if bot_response_actual:
410
+ # If bot_response_actual was successfully extracted
411
  _chat_hist[-1] = (user_message, bot_response_actual); _status = "AI response received. Processing files..."
412
  yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update)
413
+
414
+ # Pass the *existing* state (which was updated from markdown at the start of the function)
415
+ # to the parsing logic, so it knows about files already present.
416
+ latest_bot_message_json = json.dumps([{"role": BOT_ROLE_NAME, "content": bot_response_actual}], indent=2)
417
  parsing_res = _parse_chat_stream_logic(latest_bot_message_json, existing_files_state=parsed_code_blocks_state_cache)
418
+
419
  if parsing_res["error_message"]:
420
  _status = f"Parsing Error: {parsing_res['error_message']}"
421
+ # Append parsing error to the bot's response in chat? Or show separately?
422
+ # For now, update status and detected files area with error
423
  _detected_files_update = gr.Markdown(f"## Parsing Error\n`{escape_html_for_markdown(parsing_res['error_message'])}`")
424
  else:
425
+ # parsed_code_blocks_state_cache is updated inside _parse_chat_stream_logic
426
  _formatted_output_update, _detected_files_update, _download_btn_update = _generate_ui_outputs_from_cache(hf_owner_name, hf_repo_name)
427
  _status = "Processing complete. Previews updated."
428
+
429
  yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update); return
430
  else:
431
+ # Handle cases where bot_response_actual is None (e.g., malformed API response)
432
  error_msg = f"API Error: Unexpected response structure or empty message content. Response: {json.dumps(api_resp_json)}"
433
+ # This error_msg will be handled by the final error block below
434
+
435
  except requests.exceptions.HTTPError as e: error_msg = f"API HTTP Error: {e} - {e.response.text if e.response else 'No details'}"
436
  except requests.exceptions.RequestException as e: error_msg = f"API Request Error: {e}"
437
  except Exception as e: error_msg = f"Unexpected error in chat submit: {e}"
438
+
439
+ # If an error occurred (and bot_response_actual was not set or an exception happened),
440
+ # update chat history with the error message.
441
+ # Assumes the last entry is the one for the current turn, added as (user_message, None).
442
+ # It's safer to append a new message if the last one isn't the one we expected to update.
443
+ if _chat_hist and len(_chat_hist) > 0 and _chat_hist[-1][1] is None:
444
+ _chat_hist[-1] = (_chat_hist[-1][0], error_msg) # Keep user message, add error as bot message
445
+ else:
446
+ _chat_hist.append((user_message, error_msg)) # Append as a new user/bot turn if structure unexpected
447
+
448
  _status = error_msg
449
  yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update)
450
 
451
+
452
  def handle_load_existing_space(hf_api_key_ui, ui_owner_name, ui_space_name):
453
  global parsed_code_blocks_state_cache
454
  _formatted_md_val, _detected_preview_val, _status_val = "*Loading files...*", "*Loading files...*", f"Loading Space: {ui_owner_name}/{ui_space_name}..."
455
  _file_browser_update, _iframe_html_update, _download_btn_update = gr.update(visible=False, choices=[], value=None), gr.update(value=None, visible=False), gr.update(interactive=False, value=None)
456
  _build_status_clear, _edit_status_clear, _runtime_status_clear = "*Build status will appear here.*", "*Select a file to load or delete.*", "*Space runtime status will appear here after refresh.*"
457
+
458
+ # Yield initial state to update UI
459
  yield (_formatted_md_val, _detected_preview_val, _status_val, _file_browser_update, gr.update(value=ui_owner_name), gr.update(value=ui_space_name), _iframe_html_update, _download_btn_update, _build_status_clear, _edit_status_clear, _runtime_status_clear)
460
+
461
  owner_to_use, updated_owner_name_val = ui_owner_name, ui_owner_name
462
+ error_occurred = False
463
+
464
  if not owner_to_use:
465
  token, token_err = build_logic_get_api_token(hf_api_key_ui)
466
+ if token_err or not token:
467
+ _status_val = f"Error: {token_err or 'Cannot determine owner from token.'}"; error_occurred = True
468
  else:
469
  try:
470
  user_info = build_logic_whoami(token=token)
471
+ if user_info and 'name' in user_info:
472
+ owner_to_use, updated_owner_name_val = user_info['name'], user_info['name']; _status_val += f" (Auto-detected owner: {owner_to_use})"
473
+ else:
474
+ _status_val = "Error: Could not auto-detect owner from token."; error_occurred = True
475
+ except Exception as e:
476
+ _status_val = f"Error auto-detecting owner: {e}"; error_occurred = True
477
+
478
  if not owner_to_use or not ui_space_name:
479
+ if not error_occurred: _status_val = "Error: Owner and Space Name are required."; error_occurred = True
480
+
481
+ if error_occurred:
482
+ # Yield error state
483
+ yield (f"*Error: {_status_val}*", f"*Error: {_status_val}*", _status_val, _file_browser_update, updated_owner_name_val, ui_space_name, _iframe_html_update, _download_btn_update, _build_status_clear, _edit_status_clear, _runtime_status_clear)
484
+ return # Stop execution
485
 
486
  sdk_for_iframe, file_list, err_list_files = get_space_repository_info(hf_api_key_ui, ui_space_name, owner_to_use)
487
+
488
+ # Construct iframe URL early, even if file listing fails
489
+ sub_owner = re.sub(r'[^a-z0-9\-]+', '-', owner_to_use.lower()).strip('-') or 'owner' # Fallback owner
490
+ sub_repo = re.sub(r'[^a-z0-9\-]+', '-', ui_space_name.lower()).strip('-') or 'space' # Fallback repo
491
  iframe_url = f"https://{sub_owner}-{sub_repo}{'.static.hf.space' if sdk_for_iframe == 'static' else '.hf.space'}"
492
  _iframe_html_update = gr.update(value=f'<iframe src="{iframe_url}?__theme=light&embed=true" width="100%" height="500px" style="border:1px solid #eee; border-radius:8px;"></iframe>', visible=True)
493
+
494
+
495
  if err_list_files and not file_list:
496
  _status_val = f"File List Error: {err_list_files}"
497
+ parsed_code_blocks_state_cache = [] # Clear cache on error
498
+ _formatted_md_val, _detected_preview_val, _download_btn_update = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name)
499
+ _file_browser_update = gr.update(visible=True, choices=[], value="Error loading files") # Update file browser with error state
500
+ yield (f"*Error: {err_list_files}*", "*Error loading files*", _status_val, _file_browser_update, updated_owner_name_val, ui_space_name, _iframe_html_update, _download_btn_update, _build_status_clear, _edit_status_clear, _runtime_status_clear)
501
+ return # Stop execution
502
+
503
  if not file_list:
504
+ _status_val = f"Loaded Space: {owner_to_use}/{ui_space_name}. No files found ({err_list_files or 'Repository is empty'})."
505
  parsed_code_blocks_state_cache = []
506
  _formatted_md_val, _detected_preview_val, _download_btn_update = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name)
507
  _file_browser_update = gr.update(visible=True, choices=[], value="No files found")
508
+ yield (_formatted_md_val, _detected_preview_val, _status_val, _file_browser_update, updated_owner_name_val, ui_space_name, _iframe_html_update, _download_btn_update, _build_status_clear, _edit_status_clear, _runtime_status_clear)
509
+ return # Stop execution
510
+
511
 
512
  loaded_files_for_parse = []
513
+ _status_val = f"Loading {len(file_list)} files from {owner_to_use}/{ui_space_name} (SDK: {sdk_for_iframe or 'unknown'})...";
514
+ # Yield intermediate status while loading files
515
+ yield (_formatted_md_val, _detected_preview_val, _status_val, gr.update(visible=True, choices=sorted(file_list or []), value=None), updated_owner_name_val, ui_space_name, _iframe_html_update, _download_btn_update, _build_status_clear, _edit_status_clear, _runtime_status_clear)
516
+
517
+
518
  for file_path in file_list:
519
+ # Skip files that are likely binary or not user-editable code/text
520
+ # Added more extensions and common non-code files like lock files
521
  _, ext = os.path.splitext(file_path)
522
+ if ext.lower() in [".png",".jpg",".jpeg",".gif",".ico",".svg",".pt",".bin",".safetensors",".onnx",".woff",".woff2",".ttf",".eot",".zip",".tar",".gz",". هفΨͺ",".pdf",".mp4",".avi",".mov",".mp3",".wav",".ogg"] or \
523
+ file_path.startswith(".git") or "/.git/" in file_path or \
524
+ file_path in ["requirements.txt", "environment.yml", "setup.py", "Pipfile", "pyproject.toml", "package.json", "yarn.lock", "pnpm-lock.yaml", "poetry.lock"] or \
525
+ file_path.endswith(".lock") or \
526
+ file_path.startswith("__pycache__/") or "/__pycache__/" in file_path or \
527
+ file_path.startswith("node_modules/") or "/node_modules/" in file_path or \
528
+ file_path.startswith("venv/") or "/venv/" in file_path or \
529
+ file_path.startswith(".venv/") or "/.venv/" in file_path:
530
  loaded_files_for_parse.append({"filename": file_path, "code": "[Binary or Skipped file]", "language": "binary", "is_binary": True, "is_structure_block": False}); continue
531
+
532
+ # Handle potential issues with reading large files or non-utf8 files
533
+ try:
534
+ content, err_get = get_space_file_content(hf_api_key_ui, ui_space_name, owner_to_use, file_path)
535
+ if err_get:
536
+ # If there's an error getting content, record it but don't fail the whole load
537
+ loaded_files_for_parse.append({"filename": file_path, "code": f"[Error loading content: {err_get}]", "language": _infer_lang_from_filename(file_path), "is_binary": False, "is_structure_block": False})
538
+ print(f"Error loading {file_path}: {err_get}");
539
+ continue
540
+ # If content is successfully loaded
541
+ loaded_files_for_parse.append({"filename": file_path, "code": content, "language": _infer_lang_from_filename(file_path), "is_binary": False, "is_structure_block": False})
542
+ except Exception as content_ex:
543
+ # Catch any other unexpected exceptions during file content fetching
544
+ loaded_files_for_parse.append({"filename": file_path, "code": f"[Unexpected error loading content: {content_ex}]", "language": _infer_lang_from_filename(file_path), "is_binary": False, "is_structure_block": False})
545
+ print(f"Unexpected error loading {file_path}: {content_ex}")
546
+ continue
547
+
548
+
549
  parsed_code_blocks_state_cache = loaded_files_for_parse
550
  _formatted_md_val, _detected_preview_val, _download_btn_update = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name)
551
  _status_val = f"Successfully loaded Space: {owner_to_use}/{ui_space_name}. Markdown ready."
552
+ _file_browser_update = gr.update(visible=True, choices=sorted(file_list or []), value=None) # Use the full file list for the dropdown
553
+
554
  yield (_formatted_md_val, _detected_preview_val, _status_val, _file_browser_update, updated_owner_name_val, ui_space_name, _iframe_html_update, _download_btn_update, _build_status_clear, _edit_status_clear, _runtime_status_clear)
555
 
556
+
557
  def handle_build_space_button(hf_api_key_ui, ui_space_name_part, ui_owner_name_part, space_sdk_ui, formatted_markdown_content):
558
  _build_status, _iframe_html, _file_browser_update = "Starting space build process...", gr.update(value=None, visible=False), gr.update(visible=False, choices=[], value=None)
559
+ yield _build_status, _iframe_html, _file_browser_update # Yield initial status
560
  if not ui_space_name_part or "/" in ui_space_name_part: _build_status = f"Build Error: HF Space Name '{ui_space_name_part}' must be repo name only (no '/')."; yield _build_status, _iframe_html, _file_browser_update; return
561
  final_owner_for_build = ui_owner_name_part
562
  if not final_owner_for_build:
563
+ token_for_whoami, token_err = build_logic_get_api_token(hf_api_key_ui)
564
+ if token_err: _build_status = f"Build Error: {token_err}"; yield _build_status, _iframe_html, _file_browser_update; return
565
  if token_for_whoami:
566
+ try:
567
+ user_info = build_logic_whoami(token=token_for_whoami)
568
+ final_owner_for_build = user_info['name'] if user_info and 'name' in user_info else final_owner_for_build
569
+ if not final_owner_for_build: _build_status += "\n(Warning: Could not auto-detect owner from token for build. Please specify.)"
570
+ except Exception as e: _build_status += f"\n(Warning: Could not auto-detect owner for build: {e}. Please specify.)"
571
+ else: _build_status += "\n(Warning: Owner not specified and no token to auto-detect for build. Please specify owner or provide a token.)"
572
+
573
+ if not final_owner_for_build: _build_status = "Build Error: HF Owner Name could not be determined. Please specify it."; yield _build_status, _iframe_html, _file_browser_update; return
574
+
575
  result_message = build_logic_create_space(hf_api_key_ui, ui_space_name_part, final_owner_for_build, space_sdk_ui, formatted_markdown_content)
576
  _build_status = f"Build Process: {result_message}"
577
+
578
  if "Successfully" in result_message:
579
+ sub_owner = re.sub(r'[^a-z0-9\-]+', '-', final_owner_for_build.lower()).strip('-') or 'owner'
580
+ sub_repo = re.sub(r'[^a-z0-9\-]+', '-', ui_space_name_part.lower()).strip('-') or 'space'
581
  iframe_url = f"https://{sub_owner}-{sub_repo}{'.static.hf.space' if space_sdk_ui == 'static' else '.hf.space'}"
582
  _iframe_html = gr.update(value=f'<iframe src="{iframe_url}?__theme=light&embed=true" width="100%" height="700px" style="border:1px solid #eee; border-radius:8px;"></iframe>', visible=True)
583
+ _build_status += f"\nSpace live at: [Link]({iframe_url}) (Repo: https://huggingface.co/spaces/{final_owner_for_build}/{ui_space_name_part})"
584
+
585
+ # Refresh file list after successful build
586
  file_list, err_list = list_space_files_for_browsing(hf_api_key_ui, ui_space_name_part, final_owner_for_build)
587
  if err_list: _build_status += f"\nFile list refresh error after build: {err_list}"; _file_browser_update = gr.update(visible=True, choices=[], value="Error refreshing files")
588
  else: _file_browser_update = gr.update(visible=True, choices=sorted(file_list or []), value=None if file_list else "No files found")
589
+
590
  yield _build_status, _iframe_html, _file_browser_update
591
 
592
+
593
  def handle_load_file_for_editing(hf_api_key_ui, ui_space_name_part, ui_owner_name_part, selected_file_path):
594
+ _file_content_val, _edit_status_val, _commit_msg_val, _lang_update = "", "Error: No file selected.", gr.update(value=""), gr.update(language="plaintext") # Reset values
595
+ if not selected_file_path or selected_file_path in ["No files found", "Error loading files", "Error refreshing files"]:
596
+ yield _file_content_val, "Select a file from the dropdown.", _commit_msg_val, _lang_update # Clear editor and status
597
+ return
598
+
599
  owner_to_use = ui_owner_name_part
600
  if not owner_to_use:
601
+ token, token_err = build_logic_get_api_token(hf_api_key_ui)
602
+ if token_err: _edit_status_val = f"Error: {token_err}"; yield (_file_content_val, _edit_status_val, _commit_msg_val, _lang_update); return
603
  if token:
604
+ try:
605
+ user_info = build_logic_whoami(token=token); owner_to_use = user_info['name'] if user_info and 'name' in user_info else owner_to_use
606
+ if not owner_to_use: _edit_status_val = "Error: Could not auto-detect owner from token."; yield (_file_content_val, _edit_status_val, _commit_msg_val, _lang_update); return
607
+ except Exception as e: _edit_status_val = f"Error auto-detecting owner for editing file: {e}"; yield (_file_content_val, _edit_status_val, _commit_msg_val, _lang_update); return
608
  else: _edit_status_val = "Error: HF Owner Name not set and no token to auto-detect."; yield (_file_content_val, _edit_status_val, _commit_msg_val, _lang_update); return
609
+
610
  if not owner_to_use or not ui_space_name_part: _edit_status_val = "Error: HF Owner and/or Space Name is missing."; yield (_file_content_val, _edit_status_val, _commit_msg_val, _lang_update); return
611
+
612
+ _edit_status_val = f"Loading {selected_file_path}..."
613
+ yield gr.update(value=""), _edit_status_val, gr.update(value=""), gr.update(language="plaintext") # Yield loading state
614
+
615
  content, err = get_space_file_content(hf_api_key_ui, ui_space_name_part, owner_to_use, selected_file_path)
616
+
617
+ if err:
618
+ _edit_status_val = f"Error loading '{selected_file_path}': {err}"
619
+ _commit_msg_val = f"Error loading {selected_file_path}"
620
+ _file_content_val = f"Error loading {selected_file_path}:\n{err}"
621
+ _lang_update = gr.update(language="plaintext")
622
+ yield _file_content_val, _edit_status_val, _commit_msg_val, _lang_update
623
+ return
624
+
625
+ _file_content_val = content or ""
626
+ _edit_status_val = f"Loaded {selected_file_path} for editing."
627
+ _commit_msg_val = f"Update {selected_file_path} via AI Space Editor"
628
+ _lang_update = gr.update(language=_infer_lang_from_filename(selected_file_path))
629
+
630
  yield _file_content_val, _edit_status_val, _commit_msg_val, _lang_update
631
 
632
  def handle_commit_file_changes(hf_api_key_ui, ui_space_name_part, ui_owner_name_part, file_to_edit_path, edited_content, commit_message):
633
  global parsed_code_blocks_state_cache
634
+ _edit_status_val = "Processing commit..."
635
+ _file_browser_update_val = gr.update()
636
+ _formatted_md_out, _detected_preview_out, _download_btn_out = gr.update(), gr.update(), gr.update()
637
+
638
+ yield _edit_status_val, _file_browser_update_val, _formatted_md_out, _detected_preview_out, _download_btn_out # Yield initial status
639
+
640
+ if not file_to_edit_path or file_to_edit_path in ["No files found", "Error loading files", "Error refreshing files"]:
641
+ _edit_status_val = "Error: No valid file selected for commit.";
642
+ yield _edit_status_val, gr.update(), gr.update(), gr.update(), gr.update(); return
643
+
644
  owner_to_use = ui_owner_name_part
645
  if not owner_to_use:
646
+ token, token_err = build_logic_get_api_token(hf_api_key_ui)
647
+ if token_err: _edit_status_val = f"Error: {token_err}"; yield (_edit_status_val, gr.update(), gr.update(), gr.update(), gr.update()); return
648
  if token:
649
+ try:
650
+ user_info = build_logic_whoami(token=token); owner_to_use = user_info['name'] if user_info and 'name' in user_info else owner_to_use
651
+ if not owner_to_use: _edit_status_val = "Error: Could not auto-detect owner from token."; yield (_edit_status_val, gr.update(), gr.update(), gr.update(), gr.update()); return
652
+ except Exception as e: _edit_status_val = f"Error auto-detecting owner for committing file: {e}"; yield (_edit_status_val, gr.update(), gr.update(), gr.update(), gr.update()); return
653
+ else: _edit_status_val = "Error: HF Owner Name not set and no token to auto-detect."; yield (_edit_status_val, gr.update(), gr.update(), gr.update(), gr.update()); return
654
+
655
+ if not owner_to_use or not ui_space_name_part: _edit_status_val = "Error: HF Owner and/or Space Name is missing."; yield (_edit_status_val, gr.update(), gr.update(), gr.update(), gr.update()); return
656
+
657
  status_msg = update_space_file(hf_api_key_ui, ui_space_name_part, owner_to_use, file_to_edit_path, edited_content, commit_message)
658
  _edit_status_val = status_msg
659
+
660
  if "Successfully updated" in status_msg:
661
+ # Update the cache with the new content
662
  found_in_cache = False
663
  for block in parsed_code_blocks_state_cache:
664
+ if block["filename"] == file_to_edit_path:
665
+ block["code"] = edited_content
666
+ block["language"] = _infer_lang_from_filename(file_to_edit_path)
667
+ block["is_binary"] = False # Assume user edited text content
668
+ block["is_structure_block"] = False # Ensure it's not marked as structure
669
+ found_in_cache = True
670
+ break
671
+ if not found_in_cache:
672
+ # If file was added via editor or wasn't in initial load cache (e.g. binary/error), add it
673
+ parsed_code_blocks_state_cache.append({
674
+ "filename": file_to_edit_path,
675
+ "code": edited_content,
676
+ "language": _infer_lang_from_filename(file_to_edit_path),
677
+ "is_binary": False,
678
+ "is_structure_block": False
679
+ })
680
+ # Re-sort the cache to maintain consistent order
681
+ parsed_code_blocks_state_cache.sort(key=lambda b: (0, b["filename"]) if b.get("is_structure_block") else (1, b["filename"]))
682
+
683
+ # Regenerate markdown and preview from the updated cache
684
  _formatted_md_out, _detected_preview_out, _download_btn_out = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name_part)
685
+
686
+ # Refresh file list choices and keep the current file selected
687
+ new_file_list, err_list = list_space_files_for_browsing(hf_api_key_ui, ui_space_name_part, owner_to_use)
688
+ if err_list:
689
+ _edit_status_val += f"\nFile list refresh error: {err_list}"
690
+ _file_browser_update_val = gr.update(choices=sorted(new_file_list or []), value="Error refreshing files")
691
+ else:
692
+ _file_browser_update_val = gr.update(choices=sorted(new_file_list or []), value=file_to_edit_path) # Keep current file selected
693
+
694
  yield _edit_status_val, _file_browser_update_val, _formatted_md_out, _detected_preview_out, _download_btn_out
695
 
696
+
697
  def handle_delete_file(hf_api_key_ui, ui_space_name_part, ui_owner_name_part, file_to_delete_path):
698
  global parsed_code_blocks_state_cache
699
+ _edit_status_val = "Processing deletion..."
700
+ _file_browser_choices_update = gr.update()
701
+ _file_browser_value_update = None # Clear selected file
702
+ _file_content_editor_update = gr.update(value="") # Clear editor content
703
+ _commit_msg_update = gr.update(value="") # Clear commit message
704
+ _lang_update = gr.update(language="plaintext") # Reset editor language
705
  _formatted_md_out, _detected_preview_out, _download_btn_out = gr.update(), gr.update(), gr.update()
706
+
707
+ yield (_edit_status_val, _file_browser_choices_update, _file_browser_value_update, _file_content_editor_update, _commit_msg_update, _lang_update, _formatted_md_out, _detected_preview_out, _download_btn_out) # Yield initial status
708
+
709
+
710
+ if not file_to_delete_path or file_to_delete_path in ["No files found", "Error loading files", "Error refreshing files"]:
711
+ _edit_status_val = "Error: No valid file selected for deletion.";
712
+ yield (_edit_status_val, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()); return
713
+
714
  owner_to_use = ui_owner_name_part
715
  if not owner_to_use:
716
+ token, token_err = build_logic_get_api_token(hf_api_key_ui)
717
+ if token_err: _edit_status_val = f"API Token Error: {token_err}"; yield (_edit_status_val, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()); return
718
  if token:
719
+ try:
720
+ user_info = build_logic_whoami(token=token); owner_to_use = user_info['name'] if user_info and 'name' in user_info else owner_to_use
721
+ if not owner_to_use: _edit_status_val = "Error: Could not auto-detect owner from token."; yield (_edit_status_val, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()); return
722
+ except Exception as e: _edit_status_val = f"Error auto-detecting owner for deleting file: {e}"; yield (_edit_status_val, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()); return
723
+ else: _edit_status_val = "Error: HF Token needed to auto-detect owner."; yield (_edit_status_val, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()); return
724
+
725
+
726
+ if not owner_to_use or not ui_space_name_part: _edit_status_val = "Error: Owner and Space Name are required."; yield (_edit_status_val, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()); return
727
+
728
  deletion_status_msg = build_logic_delete_space_file(hf_api_key_ui, ui_space_name_part, owner_to_use, file_to_delete_path)
729
  _edit_status_val = deletion_status_msg
730
+
731
  if "Successfully deleted" in deletion_status_msg:
732
+ # Remove the file from the cache
733
  parsed_code_blocks_state_cache = [b for b in parsed_code_blocks_state_cache if b["filename"] != file_to_delete_path]
734
+
735
+ # Regenerate markdown and preview from the updated cache
736
  _formatted_md_out, _detected_preview_out, _download_btn_out = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name_part)
737
+
738
+ # Refresh file list choices and clear selected file
739
  new_file_list, err_list = list_space_files_for_browsing(hf_api_key_ui, ui_space_name_part, owner_to_use)
740
+ if err_list:
741
+ _edit_status_val += f"\nFile list refresh error: {err_list}"
742
+ _file_browser_choices_update = gr.update(choices=sorted(new_file_list or []), value="Error refreshing files")
743
+ else:
744
+ _file_browser_choices_update = gr.update(choices=sorted(new_file_list or []), value=None) # Clear selection
745
+
746
+ _file_browser_value_update = None # Explicitly set value to None to clear selection visual
747
+
748
+ else:
749
+ # If deletion failed, refresh the file list but keep the file selected
750
+ new_file_list, _ = list_space_files_for_browsing(hf_api_key_ui, ui_space_name_part, owner_to_use)
751
+ _file_browser_choices_update = gr.update(choices=sorted(new_file_list or []), value=file_to_delete_path)
752
+ _file_browser_value_update = file_to_delete_path # Keep the file selected
753
+
754
+ yield (_edit_status_val, _file_browser_choices_update, _file_browser_value_update, _file_content_editor_update, _commit_msg_update, _lang_update, _formatted_md_out, _detected_preview_out, _download_btn_out)
755
+
756
 
757
  def handle_refresh_space_status(hf_api_key_ui, ui_owner_name, ui_space_name):
758
  yield "*Fetching space status...*" # Initial feedback
 
764
  except Exception as e: yield f"**Error auto-detecting owner:** {e}"; return
765
  if not owner_to_use or not ui_space_name: yield "**Error:** Owner and Space Name are required."; return
766
  status_details, error_msg = get_space_runtime_status(hf_api_key_ui, ui_space_name, owner_to_use)
767
+ if error_msg: _status_display_md = f"**Error fetching status for {owner_to_use}/{ui_space_name}:**\n\n`{escape_html_for_markdown(error_msg)}`"
768
  elif status_details:
769
  stage, hardware, error, log_link = status_details.get('stage','N/A'), status_details.get('hardware','N/A'), status_details.get('error_message'), status_details.get('full_log_link','#')
770
  md_lines = [f"### Space Status: {owner_to_use}/{ui_space_name}", f"- **Stage:** `{stage}`", f"- **Current Hardware:** `{hardware}`"]
771
  if status_details.get('requested_hardware') and status_details.get('requested_hardware') != hardware: md_lines.append(f"- **Requested Hardware:** `{status_details.get('requested_hardware')}`")
772
  if error: md_lines.append(f"- **Error:** <span style='color:red;'>`{escape_html_for_markdown(error)}`</span>")
773
  md_lines.append(f"- [View Full Logs on Hugging Face]({log_link})")
774
+ if status_details.get('raw_data'):
775
+ # Add raw data in a collapsible section for debugging
776
+ md_lines.append(f"\n<details><summary>Raw Status Data (JSON)</summary>\n\n```json\n{json.dumps(status_details.get('raw_data', {}), indent=2)}\n```\n</details>")
777
+
778
  _status_display_md = "\n".join(md_lines)
779
  else: _status_display_md = "Could not retrieve status details."
780
  yield _status_display_md
781
 
782
+
783
  with gr.Blocks(theme=gr.themes.Soft(primary_hue="teal", secondary_hue="orange")) as demo:
784
  gr.Markdown("# πŸ€– AI Code & Space Generator")
785
  gr.Markdown("Configure settings, chat with AI to generate/modify Hugging Face Spaces, then build, preview, and edit.")
786
  with gr.Row():
787
  with gr.Sidebar():
788
  gr.Markdown("## βš™οΈ Configuration")
789
+ with gr.Group(): gr.Markdown("### API Keys & Tokens"); groq_api_key_input = gr.Textbox(label="Groq API Key", type="password", placeholder="gsk_...", allow_paste=True); hf_api_key_input = gr.Textbox(label="Hugging Face Token", type="password", placeholder="hf_...", allow_paste=True)
790
  with gr.Group(): gr.Markdown("### Hugging Face Space"); owner_name_input = gr.Textbox(label="HF Owner Name", placeholder="e.g., your-username"); space_name_input = gr.Textbox(label="HF Space Name", value="my-ai-space", placeholder="e.g., my-cool-app"); space_sdk_select = gr.Dropdown(label="Space SDK", choices=["gradio", "streamlit", "docker", "static"], value="gradio", info="Used for new/build."); load_space_button = gr.Button("πŸ”„ Load Existing Space", variant="secondary", size="sm")
791
  with gr.Group(): gr.Markdown("### AI Model Settings"); groq_model_select = gr.Dropdown(label="Groq Model", choices=["mixtral-8x7b-32768", "llama3-8b-8192", "llama3-70b-8192", "gemma-7b-it"], value="llama3-8b-8192"); groq_system_prompt_input = gr.Textbox(label="System Prompt", lines=8, value=DEFAULT_SYSTEM_PROMPT, interactive=True)
792
  with gr.Column(scale=3):
 
797
  gr.Markdown("---")
798
  with gr.Tabs():
799
  with gr.TabItem("πŸ“ Formatted Space Markdown"): gr.Markdown("Complete Markdown definition for your Space."); formatted_space_output_display = gr.Textbox(label="Current Space Definition", lines=15, interactive=True, show_copy_button=True, value="*Space definition...*"); download_button = gr.DownloadButton(label="Download .md", interactive=False, size="sm")
800
+ with gr.TabItem("πŸ” Detected Files Preview"): gr.Markdown(value="*Files preview...*")
801
  gr.Markdown("---")
802
  with gr.Tabs():
803
  with gr.TabItem("πŸš€ Build & Preview Space"):
804
  with gr.Row(): build_space_button = gr.Button("Build / Update Space on HF", variant="primary", scale=2); refresh_status_button = gr.Button("πŸ”„ Refresh Space Status", scale=1)
805
+ build_status_display = gr.Textbox(label="Build Operation Status", interactive=False, lines=2, value="*Build status will appear here.*"); gr.Markdown("---"); space_runtime_status_display = gr.Markdown("*Space runtime status will appear here after refresh.*"); gr.Markdown("---"); space_iframe_display = gr.HTML(value="<!-- Space Iframe -->", visible=False)
806
  with gr.TabItem("✏️ Edit Space Files"):
807
  gr.Markdown("Select a file to view, edit, or delete. Changes are committed to HF Hub.")
808
  file_browser_dropdown = gr.Dropdown(label="Select File in Space", choices=[], interactive=True, visible=False, info="Load/build Space first.")
 
812
  edit_status_display = gr.Textbox(label="File Edit/Delete Status", interactive=False, lines=2, value="*Select file...*")
813
 
814
  chat_outputs = [groq_chat_message_input, groq_chatbot_display, groq_status_output, detected_files_preview, formatted_space_output_display, download_button]
815
+ chat_inputs = [groq_chat_message_input, groq_chatbot_display, groq_api_key_input, groq_model_select, groq_system_prompt_input, owner_name_input, space_name_input, formatted_space_output_display] # Added formatted_space_output_display to inputs
816
  groq_send_chat_button.click(fn=handle_groq_chat_submit, inputs=chat_inputs, outputs=chat_outputs)
817
  groq_chat_message_input.submit(fn=handle_groq_chat_submit, inputs=chat_inputs, outputs=chat_outputs)
818
+
819
+ # Added outputs for clearing/updating edit status when loading a space
820
  load_space_outputs = [formatted_space_output_display, detected_files_preview, groq_status_output, file_browser_dropdown, owner_name_input, space_name_input, space_iframe_display, download_button, build_status_display, edit_status_display, space_runtime_status_display]
821
  load_space_button.click(fn=handle_load_existing_space, inputs=[hf_api_key_input, owner_name_input, space_name_input], outputs=load_space_outputs)
822
+
823
  build_outputs = [build_status_display, space_iframe_display, file_browser_dropdown]
824
  build_space_button.click(fn=handle_build_space_button, inputs=[hf_api_key_input, space_name_input, owner_name_input, space_sdk_select, formatted_space_output_display], outputs=build_outputs)
825
+
826
+ # Added commit message and language update to file edit load outputs
827
  file_edit_load_outputs = [file_content_editor, edit_status_display, commit_message_input, file_content_editor]
828
  file_browser_dropdown.change(fn=handle_load_file_for_editing, inputs=[hf_api_key_input, space_name_input, owner_name_input, file_browser_dropdown], outputs=file_edit_load_outputs)
829
+
830
+ # Added formatted_space_output_display, detected_files_preview, download_button to commit outputs to refresh previews
831
  commit_file_outputs = [edit_status_display, file_browser_dropdown, formatted_space_output_display, detected_files_preview, download_button]
832
  update_file_button.click(fn=handle_commit_file_changes, inputs=[hf_api_key_input, space_name_input, owner_name_input, file_browser_dropdown, file_content_editor, commit_message_input], outputs=commit_file_outputs)
833
+
834
+ # Added file_content_editor, commit_message_input, language update, formatted_space_output_display, detected_files_preview, download_button to delete outputs
835
+ delete_file_outputs = [edit_status_display, file_browser_dropdown, file_browser_dropdown, file_content_editor, commit_message_input, file_content_editor, formatted_space_output_display, detected_files_preview, download_button]
836
  delete_file_button.click(fn=handle_delete_file, inputs=[hf_api_key_input, space_name_input, owner_name_input, file_browser_dropdown], outputs=delete_file_outputs)
837
+
838
  refresh_status_button.click(fn=handle_refresh_space_status, inputs=[hf_api_key_input, owner_name_input, space_name_input], outputs=[space_runtime_status_display])
839
 
840
  if __name__ == "__main__":