broadfield-dev commited on
Commit
c9018ad
Β·
verified Β·
1 Parent(s): c811532

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +337 -635
app.py CHANGED
@@ -3,44 +3,101 @@ import re
3
  import json
4
  import os
5
  import tempfile
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
- from build_logic import (
8
- create_space as build_logic_create_space,
9
- _get_api_token as build_logic_get_api_token,
10
- whoami as build_logic_whoami,
11
- list_space_files_for_browsing,
12
- get_space_repository_info,
13
- get_space_file_content,
14
- update_space_file,
15
- parse_markdown as build_logic_parse_markdown,
16
- delete_space_file as build_logic_delete_space_file,
17
- get_space_runtime_status
18
- )
19
- print("build_logic.py loaded successfully.")
20
-
21
- from model_logic import (
22
- get_available_providers,
23
- get_models_for_provider,
24
- get_default_model_for_provider,
25
- get_model_id_from_display_name,
26
- generate_stream
27
- )
28
- print("model_logic.py loaded successfully.")
29
-
30
- bbb = chr(96) * 3
31
  parsed_code_blocks_state_cache = []
32
  BOT_ROLE_NAME = "assistant"
33
 
34
- DEFAULT_SYSTEM_PROMPT = f"""You are an expert AI programmer. Your role is to generate code and file structures based on user requests, or to modify existing code provided by the user.
 
 
35
  When you provide NEW code for a file, or MODIFIED code for an existing file, use the following format exactly:
36
  ### File: path/to/filename.ext
37
  (You can add a short, optional, parenthesized description after the filename on the SAME line)
38
  {bbb}language
39
  # Your full code here
40
  {bbb}
 
41
  If the file is binary, or you cannot show its content, use this format:
42
  ### File: path/to/binaryfile.ext
43
  [Binary file - approximate_size bytes]
 
44
  When you provide a project file structure, use this format:
45
  ## File Structure
46
  {bbb}
@@ -49,16 +106,34 @@ When you provide a project file structure, use this format:
49
  πŸ“ subfolder
50
  πŸ“„ file2.js
51
  {bbb}
52
- The role name for your responses in the chat history must be '{BOT_ROLE_NAME}'.
53
- Adhere strictly to these formatting instructions.
54
- If you update a file, provide the FULL file content again under the same filename.
55
- Only the latest version of each file mentioned throughout the chat will be used for the final output.
56
- Filenames in the '### File:' line should be clean paths (e.g., 'src/app.py', 'Dockerfile') and should NOT include Markdown backticks around the filename itself.
57
- If the user provides existing code (e.g., by pasting a Markdown structure), and asks for modifications, ensure your response includes the complete, modified versions of ONLY the files that changed, using the ### File: format. Unchanged files do not need to be repeated by you. The system will merge your changes with the prior state.
58
- If the user asks to delete a file, simply omit it from your next full ### File: list.
59
- If no code is provided, assist the user with their tasks.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  """
61
 
 
62
  def escape_html_for_markdown(text):
63
  if not isinstance(text, str): return ""
64
  return text.replace("&", "&").replace("<", "<").replace(">", ">")
@@ -108,6 +183,7 @@ def _clean_filename(filename_line_content):
108
  return filename_candidate if filename_candidate else text.strip()
109
 
110
  def _parse_chat_stream_logic(latest_bot_message_content, existing_files_state=None):
 
111
  latest_blocks_dict = {}
112
  if existing_files_state:
113
  for block in existing_files_state:
@@ -124,7 +200,7 @@ def _parse_chat_stream_logic(latest_bot_message_content, existing_files_state=No
124
  if structure_match:
125
  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}
126
  else:
127
- existing_structure_block = next((b for b in (existing_files_state or []) if b.get("is_structure_block")), None)
128
  if existing_structure_block:
129
  latest_blocks_dict["File Structure (original)"] = existing_structure_block.copy()
130
 
@@ -142,19 +218,16 @@ def _parse_chat_stream_logic(latest_bot_message_content, existing_files_state=No
142
  current_message_file_blocks[filename] = item_data
143
 
144
  latest_blocks_dict.update(current_message_file_blocks)
145
-
146
  current_parsed_blocks = list(latest_blocks_dict.values())
147
  current_parsed_blocks.sort(key=lambda b: (0, b["filename"]) if b.get("is_structure_block") else (1, b["filename"]))
148
-
149
  results["parsed_code_blocks"] = current_parsed_blocks
150
  results["default_selected_filenames"] = [b["filename"] for b in current_parsed_blocks if not b.get("is_structure_block")]
151
  return results
152
 
153
  def _export_selected_logic(selected_filenames, space_line_name_for_md, parsed_blocks_for_export):
154
  results = {"output_str": "", "error_message": None, "download_filepath": None}
155
- exportable_blocks_content = [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]"))]
156
  binary_blocks_content = [b for b in parsed_blocks_for_export if b.get("is_binary") or b.get("code", "").startswith("[Binary or Skipped file]")]
157
-
158
  all_filenames_in_state = sorted(list(set(b["filename"] for b in parsed_blocks_for_export if not b.get("is_structure_block"))))
159
 
160
  if not all_filenames_in_state and not any(b.get("is_structure_block") for b in parsed_blocks_for_export):
@@ -166,7 +239,6 @@ def _export_selected_logic(selected_filenames, space_line_name_for_md, parsed_bl
166
  return results
167
 
168
  output_lines = [f"# Space: {space_line_name_for_md}"]
169
-
170
  structure_block = next((b for b in parsed_blocks_for_export if b.get("is_structure_block")), None)
171
  if structure_block:
172
  output_lines.extend(["## File Structure", bbb, structure_block["code"].strip(), bbb, ""])
@@ -177,25 +249,14 @@ def _export_selected_logic(selected_filenames, space_line_name_for_md, parsed_bl
177
  output_lines.extend([bbb, ""])
178
 
179
  output_lines.append("Below are the contents of all files in the space:\n")
180
- exported_content = False
181
-
182
- files_to_export_content = []
183
- if selected_filenames:
184
- files_to_export_content = [b for b in exportable_blocks_content if b["filename"] in selected_filenames]
185
- else:
186
- files_to_export_content = exportable_blocks_content
187
-
188
- binary_error_blocks_to_export = []
189
- if selected_filenames:
190
- binary_error_blocks_to_export = [b for b in binary_blocks_content if b["filename"] in selected_filenames]
191
- elif binary_blocks_content:
192
- binary_error_blocks_to_export = binary_blocks_content
193
-
194
  all_blocks_to_export_content = sorted(files_to_export_content + binary_error_blocks_to_export, key=lambda b: b["filename"])
195
 
 
196
  for block in all_blocks_to_export_content:
197
  output_lines.append(f"### File: {block['filename']}")
198
- if block.get('is_binary') or block.get("code", "").startswith("[Binary file") or block.get("code", "").startswith("[Error loading content:") or block.get("code", "").startswith("[Binary or Skipped file]"):
199
  output_lines.append(block.get('code','[Binary or Skipped file]'))
200
  else:
201
  output_lines.extend([f"{bbb}{block.get('language', 'plaintext') or 'plaintext'}", block.get('code',''), bbb])
@@ -209,7 +270,9 @@ def _export_selected_logic(selected_filenames, space_line_name_for_md, parsed_bl
209
  try:
210
  with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".md", encoding='utf-8') as tmpfile:
211
  tmpfile.write(final_output_str); results["download_filepath"] = tmpfile.name
212
- except Exception as e: print(f"Error creating temp file: {e}"); results["error_message"] = "Could not prepare file for download."
 
 
213
  return results
214
 
215
  def _convert_gr_history_to_api_messages(system_prompt, gr_history, current_user_message=None):
@@ -221,11 +284,9 @@ def _convert_gr_history_to_api_messages(system_prompt, gr_history, current_user_
221
  return messages
222
 
223
  def _generate_ui_outputs_from_cache(owner, space_name):
224
- # This function only reads the global variable, does not need 'global' declaration to read.
225
- # Added global for consistency, but technically reads don't require it.
226
  global parsed_code_blocks_state_cache
227
  preview_md_val = "*No files in cache to display.*"
228
- 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.*"
229
  download_file = None
230
 
231
  if parsed_code_blocks_state_cache:
@@ -234,689 +295,330 @@ def _generate_ui_outputs_from_cache(owner, space_name):
234
  preview_md_lines.append(f"\n----\n**File:** `{escape_html_for_markdown(block['filename'])}`")
235
  if block.get('is_structure_block'): preview_md_lines.append(f" (Original File Structure from AI)\n")
236
  elif block.get('is_binary'): preview_md_lines.append(f" (Binary File)\n")
237
- elif block.get('language') and block.get('language') != 'binary': preview_md_lines.append(f" (Language: `{block['language']}`)\n")
238
- else: preview_md_lines.append("\n")
239
 
240
  content = block.get('code', '')
241
- if block.get('is_binary') or content.startswith("["):
242
  preview_md_lines.append(f"\n`{escape_html_for_markdown(content)}`\n")
243
- elif block.get('is_structure_block'):
244
- preview_md_lines.append(f"\n{bbb}{block.get('language', 'plaintext') or 'plaintext'}\n{content}\n{bbb}\n")
245
  else:
246
  preview_md_lines.append(f"\n{bbb}{block.get('language', 'plaintext') or 'plaintext'}\n{content}\n{bbb}\n")
247
-
248
  preview_md_val = "\n".join(preview_md_lines)
249
  space_line_name = f"{owner}/{space_name}" if owner and space_name else (owner or space_name or "your-space")
250
-
251
  export_result = _export_selected_logic(None, space_line_name, parsed_code_blocks_state_cache)
252
  formatted_md_val = export_result["output_str"]
253
  download_file = export_result["download_filepath"]
254
 
255
  return formatted_md_val, preview_md_val, gr.update(value=download_file, interactive=download_file is not None)
256
 
257
- def handle_chat_submit(user_message, chat_history, api_key_input, provider_select, model_select, system_prompt, hf_owner_name, hf_repo_name, _current_formatted_markdown):
258
- # This global declaration is needed because the function ASSIGNS to parsed_code_blocks_state_cache later.
259
- # It must be at the very top of the function before any use or assignment.
260
  global parsed_code_blocks_state_cache
261
  _chat_msg_in = ""
262
  _chat_hist = list(chat_history)
263
  _status = "Initializing..."
264
  _detected_files_update, _formatted_output_update, _download_btn_update = gr.update(), gr.update(), gr.update(interactive=False, value=None)
265
 
266
- if user_message and _current_formatted_markdown:
267
- try:
268
- parsed_from_md = build_logic_parse_markdown(_current_formatted_markdown)
269
- new_cache_state = []
270
- # This line USES parsed_code_blocks_state_cache. It must come AFTER the global declaration.
271
- existing_structure_block = next((b for b in parsed_code_blocks_state_cache if b.get("is_structure_block")), None)
272
- if existing_structure_block:
273
- new_cache_state.append(existing_structure_block.copy())
274
-
275
- for f_info in parsed_from_md.get("files", []):
276
- if f_info.get("path") and f_info["path"] != "File Structure (original)":
277
- 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]"))
278
- found_existing = False
279
- for i, block in enumerate(new_cache_state):
280
- if block["filename"] == f_info["path"] and not block.get("is_structure_block"):
281
- new_cache_state[i] = {
282
- "filename": f_info["path"],
283
- "code": f_info.get("content", ""),
284
- "language": "binary" if is_binary_repr else _infer_lang_from_filename(f_info["path"]),
285
- "is_binary": is_binary_repr,
286
- "is_structure_block": False
287
- }
288
- found_existing = True
289
- break
290
- if not found_existing:
291
- new_cache_state.append({
292
- "filename": f_info["path"],
293
- "code": f_info.get("content", ""),
294
- "language": "binary" if is_binary_repr else _infer_lang_from_filename(f_info["path"]),
295
- "is_binary": is_binary_repr,
296
- "is_structure_block": False
297
- })
298
- new_cache_state.sort(key=lambda b: (0, b["filename"]) if b.get("is_structure_block") else (1, b["filename"]))
299
- # This line ASSIGNS to parsed_code_blocks_state_cache. This is why the global declaration is needed.
300
- parsed_code_blocks_state_cache = new_cache_state
301
-
302
- except Exception as e:
303
- print(f"Error parsing formatted markdown before chat submit: {e}")
304
-
305
  if not user_message.strip():
306
  _status = "Cannot send an empty message."
307
  yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update); return
308
- _chat_hist.append((user_message, None)); _status = f"Sending to {model_select} via {provider_select}..."
 
309
  yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update)
310
 
311
- api_key_override = api_key_input
312
  current_sys_prompt = system_prompt.strip() or DEFAULT_SYSTEM_PROMPT
 
 
 
313
 
314
- current_files_context = ""
315
- # This block USES parsed_code_blocks_state_cache. It must come AFTER the global declaration.
316
- if parsed_code_blocks_state_cache:
317
- current_files_context = "\n\n## Current Files in Space\n"
318
- for block in parsed_code_blocks_state_cache:
319
- if block.get("is_structure_block"):
320
- current_files_context += f"### File: {block['filename']}\n{bbb}\n{block['code']}\n{bbb}\n"
321
- else:
322
- current_files_context += f"### File: {block['filename']}\n"
323
- if block.get("is_binary") or block.get("code", "").startswith("["):
324
- current_files_context += f"{block['code']}\n"
325
- else:
326
- current_files_context += f"{bbb}{block.get('language', 'plaintext') or 'plaintext'}\n{block.get('code','')}\n{bbb}\n"
327
- current_files_context += "\n"
328
-
329
- user_message_with_context = user_message.strip()
330
- if current_files_context.strip():
331
- user_message_with_context = user_message_with_context + current_files_context + "\nBased 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. If you are providing a file structure, use the `## File Structure\n```\n...\n```\n` format. Omit files you want to delete from your response."
332
-
333
  api_msgs = _convert_gr_history_to_api_messages(current_sys_prompt, _chat_hist[:-1], user_message_with_context)
334
 
335
  try:
336
- _status = f"Waiting for {model_select} via {provider_select}..."; yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update)
 
337
 
338
  full_bot_response_content = ""
339
- error_during_stream = None
340
-
341
- for chunk in generate_stream(provider_select, model_select, api_key_override, api_msgs):
342
- if chunk is None: continue
343
- if isinstance(chunk, str) and (chunk.startswith("Error: ") or chunk.startswith("API HTTP Error")):
344
- full_bot_response_content = chunk
345
- error_during_stream = chunk
346
- break
347
- else:
348
- full_bot_response_content += str(chunk)
349
- _chat_hist[-1] = (user_message, full_bot_response_content)
350
- _status = f"Streaming from {model_select}..."
351
- yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update)
352
-
353
- if error_during_stream:
354
- _status = error_during_stream
355
- elif full_bot_response_content and not full_bot_response_content.startswith("Error: "):
356
- _status = f"Streaming complete. Processing files from {model_select} response..."
357
-
358
- # This line USES parsed_code_blocks_state_cache.
359
- parsing_res = _parse_chat_stream_logic(full_bot_response_content, existing_files_state=parsed_code_blocks_state_cache)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
 
 
361
  if parsing_res["error_message"]:
362
- _status = f"Parsing Error: {parsing_res['error_message']}"
363
- _detected_files_update = gr.Markdown(f"## Parsing Error\n`{escape_html_for_markdown(parsing_res['error_message'])}`")
364
  else:
365
- # REMOVE THE REDUNDANT GLOBAL DECLARATION HERE.
366
- # global parsed_code_blocks_state_cache # Redundant global, but harmless. The one at the top is effective.
367
- # This line ASSIGNS to parsed_code_blocks_state_cache.
368
  parsed_code_blocks_state_cache = parsing_res["parsed_code_blocks"]
 
 
369
 
370
- _formatted_output_update, _detected_files_update, _download_btn_update = _generate_ui_outputs_from_cache(hf_owner_name, hf_repo_name)
371
- _status = "Processing complete. Previews updated."
372
- else:
373
- if not error_during_stream:
374
- _status = "AI response complete, but returned no content."
375
- _formatted_output_update, _detected_files_update, _download_btn_update = _generate_ui_outputs_from_cache(hf_owner_name, hf_repo_name)
376
 
377
  except Exception as e:
378
- error_msg = f"An unexpected error occurred during AI generation: {e}"
379
- print(f"Unexpected error in chat submit stream: {e}")
380
- if _chat_hist and len(_chat_hist) > 0 and _chat_hist[-1][1] is None:
381
- _chat_hist[-1] = (_chat_hist[-1][0], error_msg)
382
- else:
383
- _chat_hist.append((user_message, error_msg))
384
  _status = error_msg
385
  _formatted_output_update, _detected_files_update, _download_btn_update = _generate_ui_outputs_from_cache(hf_owner_name, hf_repo_name)
386
 
387
  yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update)
388
 
389
  def update_models_dropdown(provider_select):
390
- if not provider_select:
391
- return gr.update(choices=[], value=None)
392
  models = get_models_for_provider(provider_select)
393
  default_model = get_default_model_for_provider(provider_select)
394
- if default_model and default_model in models:
395
- selected_value = default_model
396
- elif models:
397
- selected_value = models[0]
398
- else:
399
- selected_value = None
400
  return gr.update(choices=models, value=selected_value)
401
 
402
  def handle_load_existing_space(hf_api_key_ui, ui_owner_name, ui_space_name):
403
- # This global declaration is needed because the function ASSIGNS to parsed_code_blocks_state_cache later.
404
  global parsed_code_blocks_state_cache
405
  _formatted_md_val, _detected_preview_val, _status_val = "*Loading files...*", "*Loading files...*", f"Loading Space: {ui_owner_name}/{ui_space_name}..."
406
  _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)
407
- _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.*"
408
  _chat_history_clear = []
 
 
409
 
410
- 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, _chat_history_clear)
411
-
412
- owner_to_use, updated_owner_name_val = ui_owner_name, ui_owner_name
413
- error_occurred = False
414
-
415
  if not owner_to_use:
416
- token, token_err = build_logic_get_api_token(hf_api_key_ui)
417
- if token_err or not token:
418
- _status_val = f"Error: {token_err or 'Cannot determine owner from token.'}"; error_occurred = True
419
- else:
420
- try:
421
- user_info = build_logic_whoami(token=token)
422
- if user_info and 'name' in user_info:
423
- owner_to_use, updated_owner_name_val = user_info['name'], user_info['name']; _status_val += f" (Auto-detected owner: {owner_to_use})"
424
- else:
425
- _status_val = "Error: Could not auto-detect owner from token."; error_occurred = True
426
- except Exception as e:
427
- _status_val = f"Error auto-detecting owner: {e}"; error_occurred = True
428
 
429
  if not owner_to_use or not ui_space_name:
430
- if not error_occurred: _status_val = "Error: Owner and Space Name are required."; error_occurred = True
431
-
432
- if error_occurred:
433
- 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, _chat_history_clear)
434
- # This line ASSIGNS to parsed_code_blocks_state_cache.
435
- parsed_code_blocks_state_cache = []
436
- return
437
 
438
- sdk_for_iframe, file_list, err_list_files = get_space_repository_info(hf_api_key_ui, ui_space_name, owner_to_use)
 
 
 
 
 
439
 
440
  sub_owner = re.sub(r'[^a-z0-9\-]+', '-', owner_to_use.lower()).strip('-') or 'owner'
441
  sub_repo = re.sub(r'[^a-z0-9\-]+', '-', ui_space_name.lower()).strip('-') or 'space'
442
- iframe_url = f"https://{sub_owner}-{sub_repo}{'.static.hf.space' if sdk_for_iframe == 'static' else '.hf.space'}"
443
- _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)
444
-
445
- if err_list_files and not file_list:
446
- _status_val = f"File List Error: {err_list_files}"
447
- # This line ASSIGNS to parsed_code_blocks_state_cache.
448
- parsed_code_blocks_state_cache = []
449
- _formatted_md_val, _detected_preview_val, _download_btn_update = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name)
450
- _file_browser_update = gr.update(visible=True, choices=[], value="Error loading files")
451
- 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, _chat_history_clear)
452
- return
453
-
454
- if not file_list:
455
- _status_val = f"Loaded Space: {owner_to_use}/{ui_space_name}. No files found ({err_list_files or 'Repository is empty'})."
456
- # This line ASSIGNS to parsed_code_blocks_state_cache.
457
- parsed_code_blocks_state_cache = []
458
- _formatted_md_val, _detected_preview_val, _download_btn_update = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name)
459
- _file_browser_update = gr.update(visible=True, choices=[], value="No files found")
460
- 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, _chat_history_clear)
461
- return
462
-
463
- loaded_files_for_cache = []
464
- _status_val = f"Loading {len(file_list)} files from {owner_to_use}/{ui_space_name} (SDK: {sdk_for_iframe or 'unknown'})...";
465
- 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, _chat_history_clear)
466
 
 
467
  for file_path in file_list:
468
- _, ext = os.path.splitext(file_path)
469
- 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 \
470
- file_path.startswith(".git") or "/.git/" in file_path or \
471
- file_path in ["requirements.txt", "environment.yml", "setup.py", "Pipfile", "pyproject.toml", "package.json", "yarn.lock", "pnpm-lock.yaml", "poetry.lock"] or \
472
- file_path.endswith(".lock") or \
473
- file_path.startswith("__pycache__/") or "/__pycache__/" in file_path or \
474
- file_path.startswith("node_modules/") or "/node_modules/" in file_path or \
475
- file_path.startswith("venv/") or "/venv/" in file_path or \
476
- file_path.startswith(".venv/") or "/.venv/" in file_path or \
477
- file_path == "README.md" or file_path == "LICENSE":
478
- loaded_files_for_cache.append({"filename": file_path, "code": "[Binary or Skipped file]", "language": "binary", "is_binary": True, "is_structure_block": False}); continue
479
- try:
480
- content, err_get = get_space_file_content(hf_api_key_ui, ui_space_name, owner_to_use, file_path)
481
- if err_get:
482
- loaded_files_for_cache.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})
483
- print(f"Error loading {file_path}: {err_get}");
484
- continue
485
- loaded_files_for_cache.append({"filename": file_path, "code": content, "language": _infer_lang_from_filename(file_path), "is_binary": False, "is_structure_block": False})
486
- except Exception as content_ex:
487
- loaded_files_for_cache.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})
488
- print(f"Unexpected error loading {file_path}: {content_ex}")
489
- continue
490
-
491
- # This line ASSIGNS to parsed_code_blocks_state_cache.
492
- parsed_code_blocks_state_cache = loaded_files_for_cache
493
- _formatted_md_val, _detected_preview_val, _download_btn_update = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name)
494
- _status_val = f"Successfully loaded Space: {owner_to_use}/{ui_space_name}. Markdown ready. {len(file_list)} files listed."
495
- _file_browser_update = gr.update(visible=True, choices=sorted(file_list or []), value=None)
496
-
497
- 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, _chat_history_clear)
498
-
499
- def handle_build_space_button(hf_api_key_ui, ui_space_name_part, ui_owner_name_part, space_sdk_ui, formatted_markdown_content):
500
- # This global declaration is needed because the function ASSIGNS to parsed_code_blocks_state_cache later.
501
- global parsed_code_blocks_state_cache
502
  _build_status, _iframe_html, _file_browser_update = "Starting space build process...", gr.update(value=None, visible=False), gr.update(visible=False, choices=[], value=None)
503
  yield _build_status, _iframe_html, _file_browser_update, gr.update(value=ui_owner_name_part), gr.update(value=ui_space_name_part)
504
- 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, gr.update(), gr.update(); return
505
- final_owner_for_build = ui_owner_name_part
506
- if not final_owner_for_build:
507
- token_for_whoami, token_err = build_logic_get_api_token(hf_api_key_ui)
508
- if token_err: _build_status = f"Build Error: {token_err}"; yield _build_status, _iframe_html, _file_browser_update, gr.update(), gr.update(); return
509
- if token_for_whoami:
510
- try:
511
- user_info = build_logic_whoami(token=token_for_whoami)
512
- final_owner_for_build = user_info['name'] if user_info and 'name' in user_info else final_owner_for_build
513
- if not final_owner_for_build: _build_status += "\n(Warning: Could not auto-detect owner from token for build. Please specify.)"
514
- except Exception as e: _build_status += f"\n(Warning: Could not auto-detect owner for build: {e}. Please specify.)"
515
- else: _build_status += "\n(Warning: Owner not specified and no token to auto-detect for build. Please specify owner or provide a token.)"
516
-
517
- 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, gr.update(), gr.update(); return
518
-
519
- try:
520
- parsed_from_md_for_build = build_logic_parse_markdown(formatted_markdown_content)
521
- # This line ASSIGNS to parsed_code_blocks_state_cache.
522
- parsed_code_blocks_state_cache = []
523
- if parsed_from_md_for_build.get("owner_md"):
524
- ui_owner_name_part = parsed_from_md_for_build["owner_md"]
525
- if parsed_from_md_for_build.get("repo_name_md"):
526
- ui_space_name_part = parsed_from_md_for_build["repo_name_md"]
527
-
528
- structure_block_md = next((f for f in parsed_from_md_for_build.get("files", []) if f.get("path") == "File Structure (original)"), None)
529
- if structure_block_md:
530
- # This line modifies the global list.
531
- parsed_code_blocks_state_cache.append({
532
- "filename": structure_block_md["path"],
533
- "code": structure_block_md["content"],
534
- "language": "plaintext",
535
- "is_binary": False,
536
- "is_structure_block": True
537
- })
538
-
539
- for f_info in parsed_from_md_for_build.get("files", []):
540
- if f_info.get("path") and f_info["path"] != "File Structure (original)":
541
- 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]"))
542
- # This line modifies the global list.
543
- parsed_code_blocks_state_cache.append({
544
- "filename": f_info["path"],
545
- "code": f_info.get("content", ""),
546
- "language": "binary" if is_binary_repr else _infer_lang_from_filename(f_info["path"]),
547
- "is_binary": is_binary_repr,
548
- "is_structure_block": False
549
- })
550
- # This line modifies the global list.
551
- parsed_code_blocks_state_cache.sort(key=lambda b: (0, b["filename"]) if b.get("is_structure_block") else (1, b["filename"]))
552
-
553
- except Exception as e:
554
- _build_status = f"Build Error: Failed to parse Markdown structure before building: {e}";
555
- yield _build_status, _iframe_html, _file_browser_update, gr.update(value=ui_owner_name_part), gr.update(value=ui_space_name_part); return
556
 
557
- result_message = build_logic_create_space(hf_api_key_ui, ui_space_name_part, final_owner_for_build, space_sdk_ui, formatted_markdown_content)
558
  _build_status = f"Build Process: {result_message}"
559
 
560
- owner_name_output = gr.update(value=ui_owner_name_part)
561
- space_name_output = gr.update(value=ui_space_name_part)
562
-
563
  if "Successfully" in result_message:
564
- sub_owner = re.sub(r'[^a-z0-9\-]+', '-', ui_owner_name_part.lower()).strip('-') or 'owner'
565
- sub_repo = re.sub(r'[^a-z0-9\-]+', '-', ui_space_name_part.lower()).strip('-') or 'space'
566
  iframe_url = f"https://{sub_owner}-{sub_repo}{'.static.hf.space' if space_sdk_ui == 'static' else '.hf.space'}"
567
- _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)
568
- _build_status += f"\nSpace live at: [Link]({iframe_url}) (Repo: https://huggingface.co/spaces/{ui_owner_name_part}/{ui_space_name_part})"
569
-
570
- file_list, err_list = list_space_files_for_browsing(hf_api_key_ui, ui_space_name_part, ui_owner_name_part)
571
- if err_list: _build_status += f"\nFile list refresh error after build: {err_list}"; _file_browser_update = gr.update(visible=True, choices=sorted(file_list or []), value="Error refreshing files")
572
- else: _file_browser_update = gr.update(visible=True, choices=sorted(file_list or []), value=None if file_list else "No files found")
573
-
574
- yield _build_status, _iframe_html, _file_browser_update, owner_name_output, space_name_output
575
 
576
  def handle_load_file_for_editing(hf_api_key_ui, ui_space_name_part, ui_owner_name_part, selected_file_path):
577
- # This function only reads the global variable, does not strictly need global.
578
- # Added global for consistency, but technically reads don't require it.
579
- global parsed_code_blocks_state_cache
580
- _file_content_val, _edit_status_val, _commit_msg_val, _lang_update = "", "Error: No file selected.", gr.update(value=""), gr.update(language="python")
581
- if not selected_file_path or selected_file_path in ["No files found", "Error loading files", "Error refreshing files"]:
582
- yield _file_content_val, "Select a file from the dropdown.", _commit_msg_val, _lang_update
583
- return
584
-
585
- owner_to_use = ui_owner_name_part
586
- if not owner_to_use:
587
- token, token_err = build_logic_get_api_token(hf_api_key_ui)
588
- if token_err: _edit_status_val = f"Error: {token_err}"; yield (_file_content_val, _edit_status_val, _commit_msg_val, _lang_update); return
589
- if token:
590
- try:
591
- 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
592
- 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
593
- 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
594
- 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
595
-
596
- 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
597
-
598
- _edit_status_val = f"Loading {selected_file_path}..."
599
- yield gr.update(value=""), _edit_status_val, gr.update(value=""), gr.update(language="python")
600
-
601
- content, err = get_space_file_content(hf_api_key_ui, ui_space_name_part, owner_to_use, selected_file_path)
602
 
 
603
  if err:
604
- _edit_status_val = f"Error loading '{selected_file_path}': {err}"
605
- _commit_msg_val = f"Error loading {selected_file_path}"
606
- _file_content_val = f"Error loading {selected_file_path}:\n{err}"
607
- _lang_update = gr.update(language="python")
608
- yield _file_content_val, _edit_status_val, _commit_msg_val, _lang_update
609
  return
610
 
611
- _file_content_val = content or ""
612
- _edit_status_val = f"Loaded {selected_file_path} for editing."
613
- _commit_msg_val = f"Update {selected_file_path} via AI Space Editor"
614
- _lang_update = gr.update(language=_infer_lang_from_filename(selected_file_path))
615
-
616
- yield _file_content_val, _edit_status_val, _commit_msg_val, _lang_update
617
 
618
  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):
619
- # This global declaration is needed because the function ASSIGNS to parsed_code_blocks_state_cache later.
 
620
  global parsed_code_blocks_state_cache
621
- _edit_status_val = "Processing commit..."
622
- _file_browser_update_val = gr.update()
623
- _formatted_md_out = gr.update()
624
- _detected_preview_out = gr.update()
625
- _download_btn_out = gr.update()
626
-
627
- yield _edit_status_val, _file_browser_update_val, _formatted_md_out, _detected_preview_out, _download_btn_out
628
-
629
- if not file_to_edit_path or file_to_edit_path in ["No files found", "Error loading files", "Error refreshing files"]:
630
- _edit_status_val = "Error: No valid file selected for commit.";
631
- yield _edit_status_val, gr.update(), gr.update(), gr.update(), gr.update(); return
632
-
633
- owner_to_use = ui_owner_name_part
634
- if not owner_to_use:
635
- token, token_err = build_logic_get_api_token(hf_api_key_ui)
636
- if token_err: _edit_status_val = f"Error: {token_err}"; yield (_edit_status_val, gr.update(), gr.update(), gr.update(), gr.update()); return
637
- if token:
638
- try:
639
- 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
640
- 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
641
- 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
642
- 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
643
-
644
- 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
645
-
646
- status_msg = update_space_file(hf_api_key_ui, ui_space_name_part, owner_to_use, file_to_edit_path, edited_content, commit_message)
647
- _edit_status_val = status_msg
648
-
649
- if "Successfully updated" in status_msg:
650
- found_in_cache = False
651
- # This loop modifies items within the global list.
652
  for block in parsed_code_blocks_state_cache:
653
  if block["filename"] == file_to_edit_path:
654
  block["code"] = edited_content
655
- block["language"] = _infer_lang_from_filename(file_to_edit_path)
656
- block["is_binary"] = False
657
- block["is_structure_block"] = False
658
- found_in_cache = True
659
  break
660
- if not found_in_cache:
661
- # This line ASSIGNS to parsed_code_blocks_state_cache.
662
- parsed_code_blocks_state_cache = [b for b in parsed_code_blocks_state_cache if b["filename"] != file_to_edit_path]
663
- # This line modifies the global list.
664
- parsed_code_blocks_state_cache.append({
665
- "filename": file_to_edit_path,
666
- "code": edited_content,
667
- "language": _infer_lang_from_filename(file_to_edit_path),
668
- "is_binary": False,
669
- "is_structure_block": False
670
- })
671
- # This line modifies the global list.
672
- parsed_code_blocks_state_cache.sort(key=lambda b: (0, b["filename"]) if b.get("is_structure_block") else (1, b["filename"]))
673
-
674
- _formatted_md_out, _detected_preview_out, _download_btn_out = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name_part)
675
-
676
- new_file_list, err_list = list_space_files_for_browsing(hf_api_key_ui, ui_space_name_part, owner_to_use)
677
- if err_list:
678
- _edit_status_val += f"\nFile list refresh error: {err_list}"
679
- _file_browser_update_val = gr.update(choices=sorted(new_file_list or []), value="Error refreshing files")
680
- else:
681
- _file_browser_update_val = gr.update(choices=sorted(new_file_list or []), value=file_to_edit_path)
682
-
683
- yield _edit_status_val, _file_browser_update_val, _formatted_md_out, _detected_preview_out, _download_btn_out
684
 
685
  def handle_delete_file(hf_api_key_ui, ui_space_name_part, ui_owner_name_part, file_to_delete_path):
686
- # This global declaration is needed because the function ASSIGNS to parsed_code_blocks_state_cache later.
 
 
 
 
687
  global parsed_code_blocks_state_cache
688
- _edit_status_val = "Processing deletion..."
689
- _file_browser_choices_update = gr.update()
690
- _file_browser_value_update = None
691
- _file_content_editor_update = gr.update(value="")
692
- _commit_msg_update = gr.update(value="")
693
- _lang_update = gr.update(language="plaintext")
694
- _formatted_md_out = gr.update()
695
- _detected_preview_out = gr.update()
696
- _download_btn_out = gr.update()
697
-
698
- 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)
699
-
700
- if not file_to_delete_path or file_to_delete_path in ["No files found", "Error loading files", "Error refreshing files"]:
701
- _edit_status_val = "Error: No valid file selected for deletion.";
702
- yield (_edit_status_val, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()); return
703
-
704
- owner_to_use = ui_owner_name_part
705
- if not owner_to_use:
706
- token, token_err = build_logic_get_api_token(hf_api_key_ui)
707
- 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
708
- if token:
709
- try:
710
- 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
711
- 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
712
- 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
713
- 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
714
-
715
- 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
716
-
717
- deletion_status_msg = build_logic_delete_space_file(hf_api_key_ui, ui_space_name_part, owner_to_use, file_to_delete_path)
718
- _edit_status_val = deletion_status_msg
719
-
720
- new_file_list, err_list = list_space_files_for_browsing(hf_api_key_ui, ui_space_name_part, owner_to_use)
721
-
722
- if "Successfully deleted" in deletion_status_msg:
723
- # This line ASSIGNS to parsed_code_blocks_state_cache.
724
  parsed_code_blocks_state_cache = [b for b in parsed_code_blocks_state_cache if b["filename"] != file_to_delete_path]
725
- _formatted_md_out, _detected_preview_out, _download_btn_out = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name_part)
726
-
727
- if err_list:
728
- _edit_status_val += f"\nFile list refresh error: {err_list}"
729
- _file_browser_choices_update = gr.update(choices=sorted(new_file_list or []), value="Error refreshing files")
730
- else:
731
- _file_browser_choices_update = gr.update(choices=sorted(new_file_list or []), value=None)
732
-
733
- _file_browser_value_update = None
734
- else:
735
- if err_list:
736
- _edit_status_val += f"\nFile list refresh error: {err_list}"
737
- _file_browser_choices_update = gr.update(choices=sorted(new_file_list or []), value="Error refreshing files")
738
- _file_browser_value_update = "Error refreshing files"
739
- else:
740
- _file_browser_choices_update = gr.update(choices=sorted(new_file_list or []), value=file_to_delete_path)
741
- _file_browser_value_update = file_to_delete_path
742
- _formatted_md_out, _detected_preview_out, _download_btn_out = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name_part)
743
-
744
- 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)
745
 
746
  def handle_refresh_space_status(hf_api_key_ui, ui_owner_name, ui_space_name):
747
- yield "*Fetching space status...*"
748
- owner_to_use = ui_owner_name
749
- if not owner_to_use:
750
- token, token_err = build_logic_get_api_token(hf_api_key_ui)
751
- if token_err or not token: yield f"**Error:** {token_err or 'Cannot determine owner.'}"; return
752
- 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
753
- except Exception as e: yield f"**Error auto-detecting owner:** {e}"; return
754
- if not owner_to_use or not ui_space_name: yield "**Error:** Owner and Space Name are required."; return
755
- status_details, error_msg = get_space_runtime_status(hf_api_key_ui, ui_space_name, owner_to_use)
756
- 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)}`"
757
- elif status_details:
758
- 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','#')
759
- md_lines = [f"### Space Status: {owner_to_use}/{ui_space_name}", f"- **Stage:** `{stage}`", f"- **Current Hardware:** `{hardware}`"]
760
- if status_details.get('requested_hardware') and status_details.get('requested_hardware') != hardware: md_lines.append(f"- **Requested Hardware:** `{status_details.get('requested_hardware')}`")
761
- if error: md_lines.append(f"- **Error:** <span style='color:red;'>`{escape_html_for_markdown(error)}`</span>")
762
- md_lines.append(f"- [View Full Logs on Hugging Face]({log_link})")
763
- if status_details.get('raw_data'):
764
- 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>")
765
- _status_display_md = "\n".join(md_lines)
766
- else: _status_display_md = "Could not retrieve status details."
767
- yield _status_display_md
768
-
769
- custom_theme = gr.themes.Base(
770
- primary_hue="teal",
771
- secondary_hue="purple",
772
- neutral_hue="zinc",
773
- text_size="sm",
774
- spacing_size="md",
775
- radius_size="sm",
776
- font=["System UI", "sans-serif"]
777
- )
778
-
779
- custom_css = """
780
- body {
781
- background: linear-gradient(to bottom right, #2c3e50, #34495e);
782
- color: #ecf0f1;
783
- }
784
- .gradio-container {
785
- background: transparent !important;
786
- }
787
- .gr-box, .gr-panel, .gr-pill {
788
- background-color: rgba(44, 62, 80, 0.8) !important;
789
- border-color: rgba(189, 195, 199, 0.2) !important;
790
- }
791
- .gr-textbox, .gr-dropdown, .gr-button, .gr-code, .gr-chat-message {
792
- border-color: rgba(189, 195, 199, 0.3) !important;
793
- background-color: rgba(52, 73, 94, 0.9) !important;
794
- color: #ecf0f1 !important;
795
- }
796
- .gr-button.gr-button-primary {
797
- background-color: #1abc9c !important;
798
- color: white !important;
799
- border-color: #16a085 !important;
800
- }
801
- .gr-button.gr-button-secondary {
802
- background-color: #9b59b6 !important;
803
- color: white !important;
804
- border-color: #8e44ad !important;
805
- }
806
- .gr-button.gr-button-stop {
807
- background-color: #e74c3c !important;
808
- color: white !important;
809
- border-color: #c0392b !important;
810
- }
811
- .gr-markdown {
812
- background-color: rgba(44, 62, 80, 0.7) !important;
813
- padding: 10px;
814
- border-radius: 5px;
815
- }
816
- .gr-markdown h1, .gr-markdown h2, .gr-markdown h3, .gr-markdown h4, .gr-markdown h5, .gr-markdown h6 {
817
- color: #ecf0f1 !important;
818
- border-bottom-color: rgba(189, 195, 199, 0.3) !important;
819
- }
820
- .gr-markdown pre code {
821
- background-color: rgba(52, 73, 94, 0.95) !important;
822
- border-color: rgba(189, 195, 199, 0.3) !important;
823
- }
824
- .gr-chatbot {
825
- background-color: rgba(44, 62, 80, 0.7) !important;
826
- border-color: rgba(189, 195, 199, 0.2) !important;
827
- }
828
- .gr-chatbot .message {
829
- background-color: rgba(52, 73, 94, 0.9) !important;
830
- color: #ecf0f1 !important;
831
- border-color: rgba(189, 195, 199, 0.3) !important;
832
- }
833
- .gr-chatbot .message.user {
834
- background-color: rgba(46, 204, 113, 0.9) !important;
835
- color: black !important;
836
- }
837
- """
838
 
839
- available_providers = get_available_providers()
840
- default_provider = available_providers[0] if available_providers else None
841
- initial_models = get_models_for_provider(default_provider) if default_provider else []
842
- initial_default_model = get_default_model_for_provider(default_provider) if default_provider else None
843
- if initial_default_model not in initial_models and initial_models:
844
- initial_default_model = initial_models[0]
845
-
846
- with gr.Blocks(theme=custom_theme, css=custom_css) as demo:
847
- gr.Markdown("# πŸ€– AI Code & Space Generator")
848
- gr.Markdown("Configure settings, chat with AI to generate/modify Hugging Face Spaces, then build, preview, and edit.")
849
  with gr.Row():
850
- with gr.Sidebar():
851
- gr.Markdown("## βš™οΈ Configuration")
852
- with gr.Group(): gr.Markdown("### API Keys & Tokens");
853
- api_key_input = gr.Textbox(label="AI Provider API Key (Optional Override)", type="password", placeholder="Paste key here or set env var (e.g., GROQ_API_KEY)");
854
- hf_api_key_input = gr.Textbox(label="Hugging Face Token (for building/loading)", type="password", placeholder="hf_...")
855
- 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")
856
- with gr.Group(): gr.Markdown("### AI Model Settings");
857
- provider_select = gr.Dropdown(label="AI Provider", choices=available_providers, value=default_provider, info="Select an AI model provider.");
858
- model_select = gr.Dropdown(label="AI Model", choices=initial_models, value=initial_default_model, info="Select a model.");
859
- system_prompt_input = gr.Textbox(label="System Prompt", lines=8, value=DEFAULT_SYSTEM_PROMPT, interactive=True)
860
- with gr.Column(scale=3):
861
- gr.Markdown("## πŸ’¬ AI Chat & Code Generation")
862
- chatbot_display = gr.Chatbot(label="AI Chat", height=400, bubble_full_width=False, avatar_images=(None, "https://huggingface.co/datasets/huggingface/badges/resolve/main/huggingface-bot-avatar.svg"))
863
- with gr.Row(): chat_message_input = gr.Textbox(show_label=False, placeholder="Your Message...", scale=7); send_chat_button = gr.Button("Send", variant="primary", scale=1, size="lg")
864
- status_output = gr.Textbox(label="Chat/Process Status", interactive=False, lines=1, value="Ready.")
865
- gr.Markdown("---")
866
- with gr.Tabs():
867
- 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")
868
- with gr.TabItem("πŸ” Detected Files Preview"):
869
- detected_files_preview = gr.Markdown(value="*Files preview...*")
870
-
871
- gr.Markdown("---")
872
- with gr.Tabs():
873
- with gr.TabItem("πŸš€ Build & Preview Space"):
874
- 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)
875
- 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)
876
- with gr.TabItem("✏️ Edit Space Files"):
877
- gr.Markdown("Select a file to view, edit, or delete. Changes are committed to HF Hub.")
878
- file_browser_dropdown = gr.Dropdown(label="Select File in Space", choices=[], interactive=True, visible=False, info="Load/build Space first.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
879
  file_content_editor = gr.Code(label="File Content Editor", language="python", lines=15, interactive=True)
880
- commit_message_input = gr.Textbox(label="Commit Message", placeholder="e.g., Updated app.py", value="Update via AI Space Editor")
881
- with gr.Row(): update_file_button = gr.Button("Commit Changes", variant="primary", scale=2); delete_file_button = gr.Button("πŸ—‘οΈ Delete Selected File", variant="stop", scale=1)
882
- edit_status_display = gr.Textbox(label="File Edit/Delete Status", interactive=False, lines=2, value="*Select file...*")
883
-
884
- provider_select.change(
885
- fn=update_models_dropdown,
886
- inputs=provider_select,
887
- outputs=model_select
888
- )
889
-
 
 
 
890
  chat_outputs = [chat_message_input, chatbot_display, status_output, detected_files_preview, formatted_space_output_display, download_button]
891
- chat_inputs = [chat_message_input, chatbot_display, api_key_input, provider_select, model_select, system_prompt_input, owner_name_input, space_name_input, formatted_space_output_display]
892
-
893
- send_chat_button.click(
894
- fn=handle_chat_submit,
895
- inputs=chat_inputs,
896
- outputs=chat_outputs
897
- )
898
- chat_message_input.submit(
899
- fn=handle_chat_submit,
900
- inputs=chat_inputs,
901
- outputs=chat_outputs
902
- )
903
 
904
  load_space_outputs = [formatted_space_output_display, detected_files_preview, 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, chatbot_display]
905
  load_space_button.click(fn=handle_load_existing_space, inputs=[hf_api_key_input, owner_name_input, space_name_input], outputs=load_space_outputs)
906
 
907
  build_outputs = [build_status_display, space_iframe_display, file_browser_dropdown, owner_name_input, space_name_input]
908
- 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)
 
909
 
910
- file_edit_load_outputs = [file_content_editor, edit_status_display, commit_message_input, file_content_editor]
911
  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)
912
 
913
  commit_file_outputs = [edit_status_display, file_browser_dropdown, formatted_space_output_display, detected_files_preview, download_button]
914
  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)
915
 
916
- 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]
917
  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)
918
 
919
  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])
920
 
921
  if __name__ == "__main__":
922
- demo.launch(debug=True, share=False)
 
3
  import json
4
  import os
5
  import tempfile
6
+ import shlex
7
+ from huggingface_hub import HfApi
8
+
9
+ try:
10
+ from build_logic import (
11
+ create_space as build_logic_create_space,
12
+ _get_api_token as build_logic_get_api_token,
13
+ whoami as build_logic_whoami,
14
+ list_space_files_for_browsing,
15
+ get_space_repository_info,
16
+ get_space_file_content,
17
+ update_space_file,
18
+ parse_markdown as build_logic_parse_markdown,
19
+ delete_space_file as build_logic_delete_space_file,
20
+ get_space_runtime_status
21
+ )
22
+ print("build_logic.py loaded successfully.")
23
+
24
+ from model_logic import (
25
+ get_available_providers,
26
+ get_models_for_provider,
27
+ get_default_model_for_provider,
28
+ generate_stream
29
+ )
30
+ print("model_logic.py loaded successfully.")
31
+ except ImportError:
32
+ print("Warning: Local modules (build_logic.py, model_logic.py) not found. Using dummy functions.")
33
+ # Define dummy functions so the app can at least start
34
+ def get_available_providers(): return ["DummyProvider"]
35
+ def get_models_for_provider(p): return ["dummy-model"]
36
+ def get_default_model_for_provider(p): return "dummy-model"
37
+ def generate_stream(p, m, a, msgs):
38
+ yield "Error: Local modules not found. This is a dummy response."
39
+ def build_logic_create_space(*args, **kwargs): return "Error: build_logic not found."
40
+ def build_logic_get_api_token(key): return (key or os.getenv("HF_TOKEN"), None)
41
+ def build_logic_whoami(token): return {"name": "dummy_user"}
42
+ def list_space_files_for_browsing(*args): return ([], "Error: build_logic not found.")
43
+ def get_space_repository_info(*args): return (None, [], "Error: build_logic not found.")
44
+ def get_space_file_content(*args): return ("", "Error: build_logic not found.")
45
+ def update_space_file(*args): return "Error: build_logic not found."
46
+ def build_logic_parse_markdown(md): return {"files": []}
47
+ def build_logic_delete_space_file(*args): return "Error: build_logic not found."
48
+ def get_space_runtime_status(*args): return (None, "Error: build_logic not found.")
49
+
50
+
51
+ # --- New Feature Functions (can be moved to build_logic.py) ---
52
+ def build_logic_set_space_privacy(hf_api_key, repo_id, private: bool):
53
+ """Sets the privacy of a Hugging Face Space."""
54
+ print(f"[ACTION] Setting privacy for '{repo_id}' to {private}.")
55
+ try:
56
+ token, err = build_logic_get_api_token(hf_api_key)
57
+ if err or not token: return f"Error getting token: {err or 'Token not found.'}"
58
+ api = HfApi(token=token)
59
+ api.update_repo_visibility(repo_id=repo_id, private=private, repo_type='space')
60
+ return f"Successfully set privacy for {repo_id} to {private}."
61
+ except Exception as e:
62
+ print(f"Error setting privacy: {e}")
63
+ return f"Error setting privacy: {e}"
64
+
65
+ def build_logic_delete_space(hf_api_key, owner, space_name):
66
+ """Deletes an entire Hugging Face Space."""
67
+ repo_id = f"{owner}/{space_name}"
68
+ print(f"[ACTION] Deleting space '{repo_id}'. THIS IS A DESTRUCTIVE ACTION.")
69
+ try:
70
+ token, err = build_logic_get_api_token(hf_api_key)
71
+ if err or not token: return f"Error getting token: {err or 'Token not found.'}"
72
+ api = HfApi(token=token)
73
+ api.delete_repo(repo_id=repo_id, repo_type='space')
74
+ return f"Successfully deleted space {repo_id}."
75
+ except Exception as e:
76
+ print(f"Error deleting space: {e}")
77
+ return f"Error deleting space: {e}"
78
+
79
+
80
+ # --- CORE FIX: Define triple backticks safely to prevent Markdown rendering issues ---
81
+ backtick = chr(96)
82
+ bbb = f'{backtick}{backtick}{backtick}'
83
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  parsed_code_blocks_state_cache = []
85
  BOT_ROLE_NAME = "assistant"
86
 
87
+ DEFAULT_SYSTEM_PROMPT = f"""You are an expert AI programmer and Hugging Face assistant. Your role is to generate code and file structures based on user requests, or to modify existing code provided by the user.
88
+
89
+ **File and Code Formatting:**
90
  When you provide NEW code for a file, or MODIFIED code for an existing file, use the following format exactly:
91
  ### File: path/to/filename.ext
92
  (You can add a short, optional, parenthesized description after the filename on the SAME line)
93
  {bbb}language
94
  # Your full code here
95
  {bbb}
96
+
97
  If the file is binary, or you cannot show its content, use this format:
98
  ### File: path/to/binaryfile.ext
99
  [Binary file - approximate_size bytes]
100
+
101
  When you provide a project file structure, use this format:
102
  ## File Structure
103
  {bbb}
 
106
  πŸ“ subfolder
107
  πŸ“„ file2.js
108
  {bbb}
109
+
110
+ **Instructions and Rules:**
111
+ - The role name for your responses in the chat history must be '{BOT_ROLE_NAME}'.
112
+ - Adhere strictly to these formatting instructions.
113
+ - If you update a file, provide the FULL file content again under the same filename.
114
+ - Only the latest version of each file mentioned throughout the chat will be used for the final output. The system will merge your changes with the prior state.
115
+ - Filenames in the '### File:' line should be clean paths (e.g., 'src/app.py', 'Dockerfile') and should NOT include Markdown backticks.
116
+
117
+ **Hugging Face Space Actions:**
118
+ To perform direct actions on the Hugging Face Space, use the `HF_ACTION` command. This is a powerful tool to manage the repository programmatically.
119
+ The format is `### HF_ACTION: COMMAND arguments...` on a single line.
120
+
121
+ Available commands:
122
+ - `CREATE_SPACE owner/repo_name --sdk <sdk> --private <true|false>`: Creates a new, empty space. SDK can be gradio, streamlit, docker, or static. Private is optional and defaults to false.
123
+ - `DELETE_FILE path/to/file.ext`: Deletes a specific file from the current space.
124
+ - `SET_PRIVATE <true|false>`: Sets the privacy for the current space.
125
+ - `DELETE_SPACE`: Deletes the entire current space. THIS IS PERMANENT AND REQUIRES CAUTION.
126
+
127
+ You can issue multiple actions. For example, to delete a file and then add a new one:
128
+ ### HF_ACTION: DELETE_FILE old_app.py
129
+ ### File: new_app.py
130
+ {bbb}python
131
+ # new code
132
+ {bbb}
133
+ Use these actions when the user's request explicitly calls for them (e.g., "delete the readme file", "make this space private", "create a new private space called my-test-app"). If no code is provided, assist the user with their tasks.
134
  """
135
 
136
+
137
  def escape_html_for_markdown(text):
138
  if not isinstance(text, str): return ""
139
  return text.replace("&", "&").replace("<", "<").replace(">", ">")
 
183
  return filename_candidate if filename_candidate else text.strip()
184
 
185
  def _parse_chat_stream_logic(latest_bot_message_content, existing_files_state=None):
186
+ global parsed_code_blocks_state_cache
187
  latest_blocks_dict = {}
188
  if existing_files_state:
189
  for block in existing_files_state:
 
200
  if structure_match:
201
  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}
202
  else:
203
+ existing_structure_block = next((b for b in parsed_code_blocks_state_cache if b.get("is_structure_block")), None)
204
  if existing_structure_block:
205
  latest_blocks_dict["File Structure (original)"] = existing_structure_block.copy()
206
 
 
218
  current_message_file_blocks[filename] = item_data
219
 
220
  latest_blocks_dict.update(current_message_file_blocks)
 
221
  current_parsed_blocks = list(latest_blocks_dict.values())
222
  current_parsed_blocks.sort(key=lambda b: (0, b["filename"]) if b.get("is_structure_block") else (1, b["filename"]))
 
223
  results["parsed_code_blocks"] = current_parsed_blocks
224
  results["default_selected_filenames"] = [b["filename"] for b in current_parsed_blocks if not b.get("is_structure_block")]
225
  return results
226
 
227
  def _export_selected_logic(selected_filenames, space_line_name_for_md, parsed_blocks_for_export):
228
  results = {"output_str": "", "error_message": None, "download_filepath": None}
229
+ exportable_blocks_content = [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:", "[Binary or Skipped file]")))]
230
  binary_blocks_content = [b for b in parsed_blocks_for_export if b.get("is_binary") or b.get("code", "").startswith("[Binary or Skipped file]")]
 
231
  all_filenames_in_state = sorted(list(set(b["filename"] for b in parsed_blocks_for_export if not b.get("is_structure_block"))))
232
 
233
  if not all_filenames_in_state and not any(b.get("is_structure_block") for b in parsed_blocks_for_export):
 
239
  return results
240
 
241
  output_lines = [f"# Space: {space_line_name_for_md}"]
 
242
  structure_block = next((b for b in parsed_blocks_for_export if b.get("is_structure_block")), None)
243
  if structure_block:
244
  output_lines.extend(["## File Structure", bbb, structure_block["code"].strip(), bbb, ""])
 
249
  output_lines.extend([bbb, ""])
250
 
251
  output_lines.append("Below are the contents of all files in the space:\n")
252
+ files_to_export_content = [b for b in exportable_blocks_content if not selected_filenames or b["filename"] in selected_filenames]
253
+ binary_error_blocks_to_export = [b for b in binary_blocks_content if not selected_filenames or b["filename"] in selected_filenames]
 
 
 
 
 
 
 
 
 
 
 
 
254
  all_blocks_to_export_content = sorted(files_to_export_content + binary_error_blocks_to_export, key=lambda b: b["filename"])
255
 
256
+ exported_content = False
257
  for block in all_blocks_to_export_content:
258
  output_lines.append(f"### File: {block['filename']}")
259
+ if block.get('is_binary') or block.get("code", "").startswith(("[Binary file", "[Error loading content:", "[Binary or Skipped file]")):
260
  output_lines.append(block.get('code','[Binary or Skipped file]'))
261
  else:
262
  output_lines.extend([f"{bbb}{block.get('language', 'plaintext') or 'plaintext'}", block.get('code',''), bbb])
 
270
  try:
271
  with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".md", encoding='utf-8') as tmpfile:
272
  tmpfile.write(final_output_str); results["download_filepath"] = tmpfile.name
273
+ except Exception as e:
274
+ print(f"Error creating temp file: {e}")
275
+ results["error_message"] = "Could not prepare file for download."
276
  return results
277
 
278
  def _convert_gr_history_to_api_messages(system_prompt, gr_history, current_user_message=None):
 
284
  return messages
285
 
286
  def _generate_ui_outputs_from_cache(owner, space_name):
 
 
287
  global parsed_code_blocks_state_cache
288
  preview_md_val = "*No files in cache to display.*"
289
+ 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 and space_name else "*Load or define a Space to see its Markdown structure.*"
290
  download_file = None
291
 
292
  if parsed_code_blocks_state_cache:
 
295
  preview_md_lines.append(f"\n----\n**File:** `{escape_html_for_markdown(block['filename'])}`")
296
  if block.get('is_structure_block'): preview_md_lines.append(f" (Original File Structure from AI)\n")
297
  elif block.get('is_binary'): preview_md_lines.append(f" (Binary File)\n")
298
+ else: preview_md_lines.append(f" (Language: `{block['language']}`)\n")
 
299
 
300
  content = block.get('code', '')
301
+ if block.get('is_binary') or content.startswith(("[Binary file", "[Error loading content:", "[Binary or Skipped file]")):
302
  preview_md_lines.append(f"\n`{escape_html_for_markdown(content)}`\n")
 
 
303
  else:
304
  preview_md_lines.append(f"\n{bbb}{block.get('language', 'plaintext') or 'plaintext'}\n{content}\n{bbb}\n")
 
305
  preview_md_val = "\n".join(preview_md_lines)
306
  space_line_name = f"{owner}/{space_name}" if owner and space_name else (owner or space_name or "your-space")
 
307
  export_result = _export_selected_logic(None, space_line_name, parsed_code_blocks_state_cache)
308
  formatted_md_val = export_result["output_str"]
309
  download_file = export_result["download_filepath"]
310
 
311
  return formatted_md_val, preview_md_val, gr.update(value=download_file, interactive=download_file is not None)
312
 
313
+ def handle_chat_submit(user_message, chat_history, hf_api_key_input, provider_select, model_select, system_prompt, hf_owner_name, hf_repo_name, _current_formatted_markdown):
 
 
314
  global parsed_code_blocks_state_cache
315
  _chat_msg_in = ""
316
  _chat_hist = list(chat_history)
317
  _status = "Initializing..."
318
  _detected_files_update, _formatted_output_update, _download_btn_update = gr.update(), gr.update(), gr.update(interactive=False, value=None)
319
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320
  if not user_message.strip():
321
  _status = "Cannot send an empty message."
322
  yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update); return
323
+
324
+ _chat_hist.append((user_message, None)); _status = f"Sending to {model_select}..."
325
  yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update)
326
 
 
327
  current_sys_prompt = system_prompt.strip() or DEFAULT_SYSTEM_PROMPT
328
+ current_files_context = f"\n\n## Current Space Context: {hf_owner_name}/{hf_repo_name}\n"
329
+ export_result = _export_selected_logic(None, f"{hf_owner_name}/{hf_repo_name}", parsed_code_blocks_state_cache)
330
+ current_files_context += export_result["output_str"]
331
 
332
+ user_message_with_context = user_message.strip() + "\n" + current_files_context
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
  api_msgs = _convert_gr_history_to_api_messages(current_sys_prompt, _chat_hist[:-1], user_message_with_context)
334
 
335
  try:
336
+ _status = f"Waiting for {model_select}..."
337
+ yield (_chat_msg_in, _chat_hist, _status, gr.update(), gr.update(), gr.update())
338
 
339
  full_bot_response_content = ""
340
+ for chunk in generate_stream(provider_select, model_select, None, api_msgs):
341
+ if chunk is None: continue
342
+ if isinstance(chunk, str) and (chunk.startswith("Error:") or chunk.startswith("API HTTP Error")):
343
+ full_bot_response_content = chunk; break
344
+ full_bot_response_content += str(chunk)
345
+ _chat_hist[-1] = (user_message, full_bot_response_content)
346
+ _status = f"Streaming from {model_select}..."
347
+ yield (_chat_msg_in, _chat_hist, _status, gr.update(), gr.update(), gr.update())
348
+
349
+ if "Error:" in full_bot_response_content:
350
+ _status = full_bot_response_content
351
+ else:
352
+ _status = "Stream complete. Processing response..."
353
+ action_results = []
354
+ action_pattern = re.compile(r"### HF_ACTION:\s*(?P<command_line>[^\n]+)")
355
+
356
+ # Process destructive actions first
357
+ for match in action_pattern.finditer(full_bot_response_content):
358
+ cmd_parts = shlex.split(match.group("command_line").strip())
359
+ if not cmd_parts: continue
360
+ command, args = cmd_parts[0].upper(), cmd_parts[1:]
361
+ if command == "DELETE_FILE":
362
+ if not args: action_results.append("Action Failed: DELETE_FILE needs a path."); continue
363
+ filepath = args[0]
364
+ status_msg = build_logic_delete_space_file(hf_api_key_input, hf_repo_name, hf_owner_name, filepath)
365
+ action_results.append(f"DELETE '{filepath}': {status_msg}")
366
+ if "Successfully" in status_msg:
367
+ parsed_code_blocks_state_cache = [b for b in parsed_code_blocks_state_cache if b["filename"] != filepath]
368
+ elif command == "DELETE_SPACE":
369
+ status_msg = build_logic_delete_space(hf_api_key_input, hf_owner_name, hf_repo_name)
370
+ action_results.append(f"DELETE_SPACE '{hf_owner_name}/{hf_repo_name}': {status_msg}")
371
+ if "Successfully" in status_msg: parsed_code_blocks_state_cache = []
372
+
373
+ # Process other actions
374
+ for match in action_pattern.finditer(full_bot_response_content):
375
+ cmd_parts = shlex.split(match.group("command_line").strip())
376
+ if not cmd_parts: continue
377
+ command, args = cmd_parts[0].upper(), cmd_parts[1:]
378
+ if command == "SET_PRIVATE":
379
+ if not args: action_results.append("Action Failed: SET_PRIVATE needs true/false."); continue
380
+ is_private = args[0].lower() == 'true'
381
+ status_msg = build_logic_set_space_privacy(hf_api_key_input, f"{hf_owner_name}/{hf_repo_name}", private=is_private)
382
+ action_results.append(f"SET_PRIVATE to {is_private}: {status_msg}")
383
 
384
+ parsing_res = _parse_chat_stream_logic(full_bot_response_content, existing_files_state=parsed_code_blocks_state_cache)
385
  if parsing_res["error_message"]:
386
+ action_results.append(f"Parsing Error: {parsing_res['error_message']}")
 
387
  else:
 
 
 
388
  parsed_code_blocks_state_cache = parsing_res["parsed_code_blocks"]
389
+ if any(f.get("filename") for f in parsing_res["parsed_code_blocks"]):
390
+ action_results.append("File content updated from AI response.")
391
 
392
+ _status = " | ".join(action_results) if action_results else "No actions performed. Files parsed."
393
+
394
+ _formatted_output_update, _detected_files_update, _download_btn_update = _generate_ui_outputs_from_cache(hf_owner_name, hf_repo_name)
 
 
 
395
 
396
  except Exception as e:
397
+ error_msg = f"An unexpected error occurred: {e}"
398
+ print(f"Error in handle_chat_submit: {e}")
399
+ if _chat_hist:
400
+ _chat_hist[-1] = (user_message, error_msg)
 
 
401
  _status = error_msg
402
  _formatted_output_update, _detected_files_update, _download_btn_update = _generate_ui_outputs_from_cache(hf_owner_name, hf_repo_name)
403
 
404
  yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update)
405
 
406
  def update_models_dropdown(provider_select):
407
+ if not provider_select: return gr.update(choices=[], value=None)
 
408
  models = get_models_for_provider(provider_select)
409
  default_model = get_default_model_for_provider(provider_select)
410
+ selected_value = default_model if default_model in models else (models[0] if models else None)
 
 
 
 
 
411
  return gr.update(choices=models, value=selected_value)
412
 
413
  def handle_load_existing_space(hf_api_key_ui, ui_owner_name, ui_space_name):
 
414
  global parsed_code_blocks_state_cache
415
  _formatted_md_val, _detected_preview_val, _status_val = "*Loading files...*", "*Loading files...*", f"Loading Space: {ui_owner_name}/{ui_space_name}..."
416
  _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)
417
+ _build_status_clear, _edit_status_clear, _runtime_status_clear = "*Build status...*", "*Select a file...*", "*Runtime status...*"
418
  _chat_history_clear = []
419
+ outputs = [_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, _chat_history_clear]
420
+ yield outputs
421
 
422
+ owner_to_use = ui_owner_name
 
 
 
 
423
  if not owner_to_use:
424
+ token, err = build_logic_get_api_token(hf_api_key_ui)
425
+ if err or not token:
426
+ _status_val = f"Error: {err or 'Cannot determine owner from token.'}"
427
+ outputs[2] = _status_val; yield outputs; return
428
+ try:
429
+ user_info = build_logic_whoami(token=token)
430
+ owner_to_use = user_info.get('name')
431
+ if not owner_to_use: raise Exception("Could not find user name from token.")
432
+ outputs[4] = gr.update(value=owner_to_use)
433
+ _status_val += f" (Auto-detected owner: {owner_to_use})"
434
+ except Exception as e:
435
+ _status_val = f"Error auto-detecting owner: {e}"; outputs[2] = _status_val; yield outputs; return
436
 
437
  if not owner_to_use or not ui_space_name:
438
+ _status_val = "Error: Owner and Space Name are required."; outputs[2] = _status_val; yield outputs; return
 
 
 
 
 
 
439
 
440
+ sdk, file_list, err = get_space_repository_info(hf_api_key_ui, ui_space_name, owner_to_use)
441
+ if err and not file_list:
442
+ _status_val = f"File List Error: {err}"; parsed_code_blocks_state_cache = []
443
+ _formatted, _detected, _download = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name)
444
+ outputs[0], outputs[1], outputs[2], outputs[7] = _formatted, _detected, _status_val, _download
445
+ yield outputs; return
446
 
447
  sub_owner = re.sub(r'[^a-z0-9\-]+', '-', owner_to_use.lower()).strip('-') or 'owner'
448
  sub_repo = re.sub(r'[^a-z0-9\-]+', '-', ui_space_name.lower()).strip('-') or 'space'
449
+ iframe_url = f"https://{sub_owner}-{sub_repo}{'.static.hf.space' if sdk == 'static' else '.hf.space'}"
450
+ outputs[6] = gr.update(value=f'<iframe src="{iframe_url}?__theme=light&embed=true" width="100%" height="500px"></iframe>', visible=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
451
 
452
+ loaded_files = []
453
  for file_path in file_list:
454
+ content, err_get = get_space_file_content(hf_api_key_ui, ui_space_name, owner_to_use, file_path)
455
+ lang = _infer_lang_from_filename(file_path)
456
+ is_binary = lang == "binary" or err_get
457
+ code = f"[Error loading content: {err_get}]" if err_get else content
458
+ loaded_files.append({"filename": file_path, "code": code, "language": lang, "is_binary": is_binary, "is_structure_block": False})
459
+
460
+ parsed_code_blocks_state_cache = loaded_files
461
+ _formatted, _detected, _download = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name)
462
+ _status_val = f"Successfully loaded {len(file_list)} files from {owner_to_use}/{ui_space_name}."
463
+ outputs[0], outputs[1], outputs[2], outputs[7] = _formatted, _detected, _status_val, _download
464
+ outputs[3] = gr.update(visible=True, choices=sorted(file_list or []), value=None)
465
+ yield outputs
466
+
467
+ def handle_build_space_button(hf_api_key_ui, ui_space_name_part, ui_owner_name_part, space_sdk_ui, is_private_ui, formatted_markdown_content):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
468
  _build_status, _iframe_html, _file_browser_update = "Starting space build process...", gr.update(value=None, visible=False), gr.update(visible=False, choices=[], value=None)
469
  yield _build_status, _iframe_html, _file_browser_update, gr.update(value=ui_owner_name_part), gr.update(value=ui_space_name_part)
470
+ if not ui_space_name_part or "/" in ui_space_name_part:
471
+ _build_status = f"Build Error: Invalid Space Name '{ui_space_name_part}'."
472
+ yield _build_status, _iframe_html, _file_browser_update, gr.update(), gr.update(); return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
473
 
474
+ result_message = build_logic_create_space(hf_api_key=hf_api_key_ui, space_name=ui_space_name_part, owner=ui_owner_name_part, sdk=space_sdk_ui, markdown_content=formatted_markdown_content, private=is_private_ui)
475
  _build_status = f"Build Process: {result_message}"
476
 
 
 
 
477
  if "Successfully" in result_message:
478
+ sub_owner = re.sub(r'[^a-z0-9\-]+', '-', ui_owner_name_part.lower()).strip('-')
479
+ sub_repo = re.sub(r'[^a-z0-9\-]+', '-', ui_space_name_part.lower()).strip('-')
480
  iframe_url = f"https://{sub_owner}-{sub_repo}{'.static.hf.space' if space_sdk_ui == 'static' else '.hf.space'}"
481
+ _iframe_html = gr.update(value=f'<iframe src="{iframe_url}?__theme=light&embed=true" width="100%" height="700px"></iframe>', visible=True)
482
+ file_list, err = list_space_files_for_browsing(hf_api_key_ui, ui_space_name_part, ui_owner_name_part)
483
+ _file_browser_update = gr.update(visible=True, choices=sorted(file_list or []), value=None)
484
+ yield _build_status, _iframe_html, _file_browser_update, gr.update(value=ui_owner_name_part), gr.update(value=ui_space_name_part)
 
 
 
 
485
 
486
  def handle_load_file_for_editing(hf_api_key_ui, ui_space_name_part, ui_owner_name_part, selected_file_path):
487
+ if not selected_file_path:
488
+ yield gr.update(value=""), "Select a file.", gr.update(value=""), gr.update(language="plaintext")
489
+ return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
490
 
491
+ content, err = get_space_file_content(hf_api_key_ui, ui_space_name_part, ui_owner_name_part, selected_file_path)
492
  if err:
493
+ yield f"Error: {err}", f"Error loading '{selected_file_path}': {err}", "", gr.update(language="plaintext")
 
 
 
 
494
  return
495
 
496
+ lang = _infer_lang_from_filename(selected_file_path)
497
+ commit_msg = f"Update {selected_file_path}"
498
+ yield content, f"Loaded {selected_file_path}", commit_msg, gr.update(language=lang)
 
 
 
499
 
500
  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):
501
+ status_msg = update_space_file(hf_api_key_ui, ui_space_name_part, ui_owner_name_part, file_to_edit_path, edited_content, commit_message)
502
+ file_list, _ = list_space_files_for_browsing(hf_api_key_ui, ui_space_name_part, ui_owner_name_part)
503
  global parsed_code_blocks_state_cache
504
+ if "Successfully" in status_msg:
505
+ # Update cache
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
506
  for block in parsed_code_blocks_state_cache:
507
  if block["filename"] == file_to_edit_path:
508
  block["code"] = edited_content
 
 
 
 
509
  break
510
+ _formatted, _detected, _download = _generate_ui_outputs_from_cache(ui_owner_name_part, ui_space_name_part)
511
+ return status_msg, gr.update(choices=sorted(file_list or [])), _formatted, _detected, _download
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
512
 
513
  def handle_delete_file(hf_api_key_ui, ui_space_name_part, ui_owner_name_part, file_to_delete_path):
514
+ if not file_to_delete_path:
515
+ return "No file selected to delete.", gr.update(), "", "", "plaintext", gr.update(), gr.update(), gr.update()
516
+
517
+ status_msg = build_logic_delete_space_file(hf_api_key_ui, ui_space_name_part, ui_owner_name_part, file_to_delete_path)
518
+ file_list, _ = list_space_files_for_browsing(hf_api_key_ui, ui_space_name_part, ui_owner_name_part)
519
  global parsed_code_blocks_state_cache
520
+ if "Successfully" in status_msg:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
521
  parsed_code_blocks_state_cache = [b for b in parsed_code_blocks_state_cache if b["filename"] != file_to_delete_path]
522
+ _formatted, _detected, _download = _generate_ui_outputs_from_cache(ui_owner_name_part, ui_space_name_part)
523
+ return status_msg, gr.update(choices=sorted(file_list or []), value=None), "", "", "plaintext", _formatted, _detected, _download
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
524
 
525
  def handle_refresh_space_status(hf_api_key_ui, ui_owner_name, ui_space_name):
526
+ if not ui_owner_name or not ui_space_name:
527
+ return "Owner and Space Name must be provided to get status."
528
+
529
+ status, err = get_space_runtime_status(hf_api_key_ui, ui_space_name, ui_owner_name)
530
+ if err: return f"**Error:** {err}"
531
+ if not status: return "Could not retrieve status."
532
+
533
+ md = f"### Status for {ui_owner_name}/{ui_space_name}\n"
534
+ for key, val in status.items():
535
+ md += f"- **{key.replace('_', ' ').title()}:** `{val}`\n"
536
+ return md
537
+
538
+ # --- Gradio UI Definition ---
539
+ with gr.Blocks(theme=gr.themes.Soft(), css=".gradio-container {background: linear-gradient(to bottom right, #eff6ff, #dbeafe);}") as demo:
540
+ gr.Markdown("# πŸ€– AI-Powered Hugging Face Space Builder")
541
+ gr.Markdown("Use an AI assistant to create, modify, build, and manage your Hugging Face Spaces directly from this interface.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
542
 
 
 
 
 
 
 
 
 
 
 
543
  with gr.Row():
544
+ with gr.Column(scale=1):
545
+ with gr.Accordion("βš™οΈ Configuration", open=True):
546
+ hf_api_key_input = gr.Textbox(label="Hugging Face Token", type="password", placeholder="hf_... (uses env var HF_TOKEN if empty)")
547
+ owner_name_input = gr.Textbox(label="HF Owner Name", placeholder="e.g., your-username")
548
+ space_name_input = gr.Textbox(label="HF Space Name", value="my-ai-space")
549
+ load_space_button = gr.Button("πŸ”„ Load Existing Space", variant="secondary")
550
+
551
+ with gr.Accordion("πŸ€– AI Model Settings", open=True):
552
+ provider_select = gr.Dropdown(label="AI Provider", choices=get_available_providers(), value=get_default_model_for_provider(get_available_providers()[0] if get_available_providers() else None))
553
+ model_select = gr.Dropdown(label="AI Model", choices=[])
554
+ system_prompt_input = gr.Textbox(label="System Prompt", lines=10, value=DEFAULT_SYSTEM_PROMPT, elem_id="system-prompt")
555
+
556
+ with gr.Column(scale=2):
557
+ gr.Markdown("## πŸ’¬ AI Assistant Chat")
558
+ chatbot_display = gr.Chatbot(label="AI Chat", height=500, bubble_full_width=False, avatar_images=(None, "https://huggingface.co/datasets/huggingface/badges/resolve/main/huggingface-bot-avatar.svg"))
559
+ with gr.Row():
560
+ chat_message_input = gr.Textbox(show_label=False, placeholder="Your Message...", scale=7)
561
+ send_chat_button = gr.Button("Send", variant="primary", scale=1)
562
+ status_output = gr.Textbox(label="Last Action Status", interactive=False, value="Ready.")
563
+
564
+ with gr.Tabs():
565
+ with gr.TabItem("πŸ“ Generated Markdown & Build"):
566
+ with gr.Row():
567
+ with gr.Column(scale=2):
568
+ formatted_space_output_display = gr.Textbox(label="Current Space Definition (Editable)", lines=20, interactive=True, value="*Load or create a space to see its definition.*")
569
+ download_button = gr.DownloadButton(label="Download .md", interactive=False)
570
+ with gr.Column(scale=1):
571
+ gr.Markdown("### Build Controls")
572
+ space_sdk_select = gr.Dropdown(label="Space SDK", choices=["gradio", "streamlit", "docker", "static"], value="gradio")
573
+ space_private_checkbox = gr.Checkbox(label="Make Space Private", value=False)
574
+ build_space_button = gr.Button("πŸš€ Build / Update Space on HF", variant="primary")
575
+ build_status_display = gr.Textbox(label="Build Operation Status", interactive=False)
576
+ refresh_status_button = gr.Button("πŸ”„ Refresh Runtime Status")
577
+ space_runtime_status_display = gr.Markdown("*Runtime status will appear here.*")
578
+
579
+ with gr.TabItem("πŸ” Files Preview"):
580
+ detected_files_preview = gr.Markdown(value="*A preview of the latest file versions will appear here.*")
581
+
582
+ with gr.TabItem("✏️ Live File Editor & Preview"):
583
+ with gr.Row():
584
+ with gr.Column(scale=1):
585
+ gr.Markdown("### Live Editor")
586
+ file_browser_dropdown = gr.Dropdown(label="Select File in Space", choices=[], interactive=True)
587
  file_content_editor = gr.Code(label="File Content Editor", language="python", lines=15, interactive=True)
588
+ commit_message_input = gr.Textbox(label="Commit Message", placeholder="e.g., Updated app.py")
589
+ with gr.Row():
590
+ update_file_button = gr.Button("Commit Changes", variant="primary")
591
+ delete_file_button = gr.Button("πŸ—‘οΈ Delete Selected File", variant="stop")
592
+ edit_status_display = gr.Textbox(label="File Edit/Delete Status", interactive=False)
593
+ with gr.Column(scale=1):
594
+ gr.Markdown("### Live Space Preview")
595
+ space_iframe_display = gr.HTML(value="", visible=True)
596
+
597
+ # --- Event Listeners ---
598
+ provider_select.change(update_models_dropdown, inputs=provider_select, outputs=model_select)
599
+
600
+ chat_inputs = [chat_message_input, chatbot_display, hf_api_key_input, provider_select, model_select, system_prompt_input, owner_name_input, space_name_input, formatted_space_output_display]
601
  chat_outputs = [chat_message_input, chatbot_display, status_output, detected_files_preview, formatted_space_output_display, download_button]
602
+ send_chat_button.click(handle_chat_submit, inputs=chat_inputs, outputs=chat_outputs)
603
+ chat_message_input.submit(handle_chat_submit, inputs=chat_inputs, outputs=chat_outputs)
 
 
 
 
 
 
 
 
 
 
604
 
605
  load_space_outputs = [formatted_space_output_display, detected_files_preview, 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, chatbot_display]
606
  load_space_button.click(fn=handle_load_existing_space, inputs=[hf_api_key_input, owner_name_input, space_name_input], outputs=load_space_outputs)
607
 
608
  build_outputs = [build_status_display, space_iframe_display, file_browser_dropdown, owner_name_input, space_name_input]
609
+ build_inputs = [hf_api_key_input, space_name_input, owner_name_input, space_sdk_select, space_private_checkbox, formatted_space_output_display]
610
+ build_space_button.click(fn=handle_build_space_button, inputs=build_inputs, outputs=build_outputs)
611
 
612
+ file_edit_load_outputs = [file_content_editor, edit_status_display, commit_message_input, file_content_editor] # last one updates language
613
  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)
614
 
615
  commit_file_outputs = [edit_status_display, file_browser_dropdown, formatted_space_output_display, detected_files_preview, download_button]
616
  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)
617
 
618
+ delete_file_outputs = [edit_status_display, file_browser_dropdown, file_content_editor, commit_message_input, file_content_editor, formatted_space_output_display, detected_files_preview, download_button]
619
  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)
620
 
621
  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])
622
 
623
  if __name__ == "__main__":
624
+ demo.launch(debug=True)