broadfield-dev commited on
Commit
aee4cb7
Β·
verified Β·
1 Parent(s): c477253

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +480 -184
app.py CHANGED
@@ -1,3 +1,5 @@
 
 
1
  import gradio as gr
2
  import re
3
  import json
@@ -17,6 +19,10 @@ from build_logic import (
17
  delete_space_file as build_logic_delete_space_file,
18
  get_space_runtime_status,
19
  apply_staged_changes,
 
 
 
 
20
  )
21
 
22
  from model_logic import (
@@ -69,11 +75,12 @@ The system will parse these actions from your response and present them to the u
69
 
70
  Available commands:
71
  - `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. Use this ONLY when the user explicitly asks to create a *new* space with a specific name. When using this action, also provide the initial file structure (e.g., app.py, README.md) using `### File:` blocks in the same response. The system will apply these files to the new space.
 
72
  - `DELETE_FILE path/to/file.ext`: Deletes a specific file from the current space.
73
  - `SET_PRIVATE <true|false>`: Sets the privacy for the current space.
74
  - `DELETE_SPACE`: Deletes the entire current space. THIS IS PERMANENT AND REQUIRES CAUTION. Only use this if the user explicitly and clearly asks to delete the space.
75
 
76
- You can issue multiple file updates and action commands in a single response. The system will process all of them into a single change plan.
77
 
78
  **Current Space Context:**
79
  You will be provided with the current state of the files in the Space the user is interacting with. Use this information to understand the current project structure and content before proposing changes or actions. This context will appear after the user's message, starting with "## Current Space Context:". Do NOT include this context in your response. Only generate your response based on the user's request and the formatting rules above.
@@ -259,40 +266,100 @@ def generate_and_stage_changes(ai_response_content, current_files_state, hf_owne
259
  ai_proposed_files_dict = {f["path"]: f for f in ai_proposed_files_list}
260
 
261
  action_pattern = re.compile(r"### HF_ACTION:\s*(?P<command_line>[^\n]+)", re.MULTILINE)
 
 
262
  for match in action_pattern.finditer(ai_response_content):
263
  try:
264
  cmd_parts = shlex.split(match.group("command_line").strip())
265
  if not cmd_parts: continue
266
  command, args = cmd_parts[0].upper(), cmd_parts[1:]
267
-
268
- if command == "CREATE_SPACE" and args:
269
- repo_id = args[0]
270
- sdk = "gradio"
271
- private = False
272
- if '--sdk' in args:
273
- try: sdk = args[args.index('--sdk') + 1]
274
- except IndexError: print("Warning: CREATE_SPACE --sdk requires an argument.")
275
- if '--private' in args:
276
- try: private_str = args[args.index('--private') + 1].lower()
277
- except IndexError: print("Warning: CREATE_SPACE --private requires an argument.")
278
- else: private = private_str == 'true'
279
- changeset.append({"type": "CREATE_SPACE", "repo_id": repo_id, "sdk": sdk, "private": private})
280
-
281
- elif command == "DELETE_FILE" and args:
282
- file_path = args[0]
283
- changeset.append({"type": "DELETE_FILE", "path": file_path})
284
-
285
- elif command == "SET_PRIVATE" and args:
286
- private = args[0].lower() == 'true'
287
- changeset.append({"type": "SET_PRIVACY", "private": private, "repo_id": f"{hf_owner_name}/{hf_repo_name}"})
288
-
289
- elif command == "DELETE_SPACE":
290
- changeset.append({"type": "DELETE_SPACE", "owner": hf_owner_name, "space_name": hf_repo_name})
291
-
292
  except Exception as e:
293
  print(f"Error parsing HF_ACTION line '{match.group('command_line').strip()}': {e}")
294
 
295
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
  for file_info in ai_proposed_files_list:
297
  filename = file_info["path"]
298
  proposed_content = file_info["content"]
@@ -315,6 +382,7 @@ def generate_and_stage_changes(ai_response_content, current_files_state, hf_owne
315
  else:
316
  print(f"Skipping staging create for {filename}: Proposed content is a placeholder.")
317
 
 
318
  if not changeset:
319
  md_summary = ["### πŸ“‹ Proposed Changes Plan", "\nThe AI did not propose any specific changes to files or the space.\n"]
320
  else:
@@ -350,21 +418,20 @@ def generate_and_stage_changes(ai_response_content, current_files_state, hf_owne
350
 
351
  def handle_chat_submit(user_message, chat_history, hf_api_key_input, provider_api_key_input, provider_select, model_select, system_prompt, hf_owner_name, hf_repo_name):
352
  global parsed_code_blocks_state_cache
353
- _chat_msg_in = user_message # Keep user input for clearing later
354
  _chat_hist = list(chat_history)
355
- _chat_hist.append((user_message, None)) # Append user message immediately
356
 
357
- # Initial yield: Show loading state, clear input, clear/hide changes
358
  yield (
359
- "", # Clear chat_message_input
360
- _chat_hist, # Update chat_history with user message
361
- "Initializing...", # status_output
362
- gr.update(), # Keep detected_files_preview
363
- gr.update(), # Keep formatted_space_output_display
364
- gr.update(interactive=False), # download_button (disable)
365
- [], # changeset_state (clear)
366
- "*No changes proposed.*", # changeset_display (clear text)
367
- gr.update(visible=False), gr.update(visible=False), gr.update(visible=False) # Hide confirm UI
368
  )
369
 
370
  current_sys_prompt = system_prompt.strip() or DEFAULT_SYSTEM_PROMPT
@@ -383,59 +450,53 @@ def handle_chat_submit(user_message, chat_history, hf_api_key_input, provider_ap
383
  if chunk is None: continue
384
  full_bot_response_content += str(chunk)
385
  _chat_hist[-1] = (user_message, full_bot_response_content)
386
- # Yield during streaming - ONLY update chatbot and status
387
  yield (
388
- gr.update(), # Keep chat_message_input cleared
389
- _chat_hist, # Update chat_history with new chunk
390
- f"Streaming from {model_select}...", # status_output
391
- gr.update(), # Keep detected_files_preview
392
- gr.update(), # Keep formatted_space_output_display
393
- gr.update(), # Keep download_button
394
- gr.update(), # Keep changeset_state
395
- gr.update(), # Keep changeset_display
396
- gr.update(), gr.update(), gr.update() # Keep confirm UI visibility
397
  )
398
 
399
- # --- Post-streaming: Parse AI output, update cache, stage changes, FINAL UI UPDATE ---
400
-
401
  if full_bot_response_content.startswith("Error:") or full_bot_response_content.startswith("API HTTP Error"):
402
  _status = full_bot_response_content
403
- # Update chatbot with error in the last message, update status, clear/hide changes
404
  yield (
405
  gr.update(), _chat_hist, _status,
406
- gr.update(), gr.update(), gr.update(), # Keep file previews as they were
407
- [], "*Error occurred, changes plan cleared.*", # Clear changeset state and display
408
- gr.update(visible=False), gr.update(visible=False), gr.update(visible=False) # Hide confirm UI
409
  )
410
- return # Exit handler
411
 
412
- # Parse AI output and update state cache
413
  parsed_code_blocks_state_cache, proposed_filenames_in_turn = _parse_and_update_state_cache(full_bot_response_content, parsed_code_blocks_state_cache)
414
 
415
- # Regenerate UI previews based on the updated cache
416
  _formatted, _detected, _download = _generate_ui_outputs_from_cache(hf_owner_name, hf_repo_name)
417
 
418
- # Generate changeset and summary
419
  staged_changeset, summary_md = generate_and_stage_changes(full_bot_response_content, parsed_code_blocks_state_cache, hf_owner_name, hf_repo_name)
420
 
421
- # Final yield based on whether changes were staged
422
  if not staged_changeset:
423
- _status = summary_md # Will be "No changes proposed" message
424
  yield (
425
- gr.update(), # Keep input box cleared
426
- _chat_hist, _status, # Update chatbot (final), status
427
- _detected, _formatted, _download, # Update file previews
428
- [], summary_md, # Clear changeset state, display summary
429
- gr.update(visible=False), gr.update(visible=False), gr.update(visible=False) # Hide confirm UI
430
  )
431
  else:
432
  _status = "Change plan generated. Please review and confirm below."
433
  yield (
434
- gr.update(), # Keep input box cleared
435
- _chat_hist, _status, # Update chatbot (final), status
436
- _detected, _formatted, _download, # Update file previews
437
- staged_changeset, summary_md, # Set changeset state, display summary
438
- gr.update(visible=True), gr.update(visible=True), gr.update(visible=True) # Show confirm UI
 
 
439
  )
440
 
441
  except Exception as e:
@@ -443,26 +504,22 @@ def handle_chat_submit(user_message, chat_history, hf_api_key_input, provider_ap
443
  print(f"Error in handle_chat_submit: {e}")
444
  import traceback
445
  traceback.print_exc()
446
- # Update chatbot with error
447
  if _chat_hist:
448
- # Find the last message matching the user's input and append the error
449
  try:
450
  idx = next(i for i in reversed(range(len(_chat_hist))) if _chat_hist[i] and _chat_hist[i][0] == user_message)
451
  current_bot_response = _chat_hist[idx][1] or ""
452
  _chat_hist[idx] = (user_message, (current_bot_response + "\n\n" if current_bot_response else "") + error_msg)
453
  except (StopIteration, IndexError):
454
- _chat_hist.append((user_message, error_msg)) # Fallback if user message not found
455
 
456
- # Regenerate previews from potentially partially updated cache
457
  _formatted, _detected, _download = _generate_ui_outputs_from_cache(hf_owner_name, hf_repo_name)
458
 
459
- # Final error yield: Update chatbot, status, file previews, clear/hide changes
460
  yield (
461
- gr.update(), # Keep input box content
462
- _chat_hist, error_msg, # Update chatbot, status
463
- _detected, _formatted, _download, # Update file previews based on state before error
464
- [], "*Error occurred, changes plan cleared.*", # Clear changeset state and display
465
- gr.update(visible=False), gr.update(visible=False), gr.update(visible=False) # Hide confirm UI
466
  )
467
 
468
 
@@ -475,51 +532,159 @@ def handle_confirm_changes(hf_api_key, owner_name, space_name, changeset):
475
  if not changeset:
476
  return "No changes to apply.", gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(value="No changes were staged.")
477
 
478
- status_message = apply_staged_changes(hf_api_key, owner_name, space_name, changeset)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
479
 
480
- _status_reload = f"{status_message} | Reloading Space state..."
481
- yield _status_reload, gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(value="*Reloading Space state...*")
 
 
 
 
 
 
482
 
483
- refreshed_file_list = []
484
- reload_error = None
485
- repo_id_for_reload = f"{owner_name}/{space_name}" if owner_name and space_name else None
486
 
487
- if repo_id_for_reload:
488
- sdk, file_list, err_list = get_space_repository_info(hf_api_key, space_name, owner_name)
489
- if err_list:
490
- reload_error = f"Error reloading file list after changes: {err_list}"
491
- parsed_code_blocks_state_cache = []
492
  else:
493
- refreshed_file_list = file_list
494
- loaded_files = []
495
- for file_path in refreshed_file_list:
496
- content, err_get = get_space_file_content(hf_api_key, space_name, owner_name, file_path)
497
- lang = _infer_lang_from_filename(file_path)
498
- is_binary = lang == "binary" or (err_get is not None)
499
- code = f"[Error loading content: {err_get}]" if err_get else (content or "")
500
- loaded_files.append({"filename": file_path, "code": code, "language": lang, "is_binary": is_binary, "is_structure_block": False})
501
- parsed_code_blocks_state_cache = loaded_files
 
 
 
 
 
 
 
 
 
 
502
 
503
- else:
504
- reload_error = "Cannot reload Space state: Owner or Space Name missing."
505
 
506
- _formatted, _detected, _download = _generate_ui_outputs_from_cache(owner_name, space_name)
 
507
 
508
- final_overall_status = status_message + (f" | Reload Status: {reload_error}" if reload_error else " | Reload Status: Space state refreshed.")
 
 
509
 
510
- cleared_changeset = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
511
 
512
- return (
513
- final_overall_status,
514
- _formatted,
515
- _detected,
516
- _download,
517
- gr.update(visible=False),
518
- gr.update(visible=False),
519
- gr.update(visible=False),
520
- cleared_changeset,
521
- gr.update(value="*No changes proposed.*")
522
- )
 
 
 
 
 
 
523
 
524
 
525
  def handle_cancel_changes():
@@ -545,36 +710,30 @@ def handle_load_existing_space(hf_api_key_ui, ui_owner_name, ui_space_name):
545
  global parsed_code_blocks_state_cache
546
  _formatted_md_val, _detected_preview_val, _status_val = "*Loading files...*", "*Loading files...*", f"Loading Space: {ui_owner_name}/{ui_space_name}..."
547
  _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)
548
- _build_status_clear, _edit_status_clear, _runtime_status_clear = "*Build status...*", "*Select a file...*", "*Runtime status...*"
549
  _changeset_clear = []
550
  _changeset_summary_clear = "*No changes proposed.*"
551
  _confirm_ui_hidden = gr.update(visible=False)
 
552
 
553
- outputs = [
554
- formatted_space_output_display, detected_files_preview, status_output,
555
- file_browser_dropdown, owner_name_input, space_name_input,
556
- space_iframe_display, download_button, build_status_display,
557
- edit_status_display, space_runtime_status_display,
558
- changeset_state, changeset_display, confirm_accordion, confirm_button, cancel_button
559
- ]
560
- # Use gr.update for components that are not state variables
561
- initial_updates = [
562
  gr.update(value=_formatted_md_val), gr.update(value=_detected_preview_val), gr.update(value=_status_val), _file_browser_update,
563
  gr.update(value=ui_owner_name), gr.update(value=ui_space_name),
564
  _iframe_html_update, _download_btn_update, gr.update(value=_build_status_clear),
565
  gr.update(value=_edit_status_clear), gr.update(value=_runtime_status_clear),
566
- _changeset_clear, gr.update(value=_changeset_summary_clear), _confirm_ui_hidden, _confirm_ui_hidden, _confirm_ui_hidden
567
- ]
568
-
569
- yield initial_updates
570
 
571
  owner_to_use = ui_owner_name
 
 
 
 
 
572
  if not owner_to_use:
573
- token, token_err = build_logic_get_api_token(hf_api_key_ui)
574
- if token_err:
575
- _status_val = f"Load Error: {token_err}"
576
- yield gr.update(value=_status_val), # Only update status on early error
577
- return
578
  try:
579
  user_info = build_logic_whoami(token=token)
580
  owner_to_use = user_info.get('name')
@@ -582,7 +741,7 @@ def handle_load_existing_space(hf_api_key_ui, ui_owner_name, ui_space_name):
582
  yield gr.update(value=owner_to_use), gr.update(value=f"Loading Space: {owner_to_use}/{ui_space_name} (Auto-detected owner)...") # Update owner and status
583
  except Exception as e:
584
  _status_val = f"Load Error: Error auto-detecting owner: {e}"
585
- yield gr.update(value=_status_val), # Only update status on early error
586
  return
587
 
588
  if not owner_to_use or not ui_space_name:
@@ -592,20 +751,19 @@ def handle_load_existing_space(hf_api_key_ui, ui_owner_name, ui_space_name):
592
 
593
  sdk, file_list, err = get_space_repository_info(hf_api_key_ui, ui_space_name, owner_to_use)
594
 
595
- # Always update owner/space inputs even on error, as user entered them
596
- # These were updated in the initial yield, but we might need to update again if owner was auto-detected
597
- yield gr.update(value=owner_to_use), gr.update(value=ui_space_name), # Update owner and space textboxes
598
 
599
  if err:
600
  _status_val = f"Load Error: {err}"
601
  parsed_code_blocks_state_cache = []
602
  _formatted, _detected, _download = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name)
603
  yield (
604
- gr.update(value=_formatted), gr.update(value=_detected), gr.update(value=_status_val), # Update markdown, preview, status
605
- gr.update(visible=False, choices=[], value=None), # Hide file browser
606
- gr.update(), gr.update(), # Keep owner/space
607
- gr.update(value=None, visible=False), # Hide iframe
608
- _download, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update() # Rest are updates
609
  )
610
  return
611
 
@@ -632,13 +790,17 @@ def handle_load_existing_space(hf_api_key_ui, ui_owner_name, ui_space_name):
632
  else:
633
  iframe_update = gr.update(value=None, visible=False)
634
 
 
 
 
 
635
  # Final yield after success
636
  yield (
637
- gr.update(value=_formatted), gr.update(value=_detected), gr.update(value=_status_val), # Update markdown, preview, status
638
- file_browser_update, # Update file browser
639
- gr.update(), gr.update(), # Keep owner/space
640
- iframe_update, # Update iframe
641
- _download, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update() # Rest are updates
642
  )
643
 
644
  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):
@@ -649,20 +811,9 @@ def handle_build_space_button(hf_api_key_ui, ui_space_name_part, ui_owner_name_p
649
  _changeset_summary_clear = "*Manual build initiated, changes plan cleared.*"
650
  _confirm_ui_hidden = gr.update(visible=False)
651
 
652
- outputs = [
653
- build_status_display, space_iframe_display, file_browser_dropdown,
654
- owner_name_input, space_name_input,
655
- changeset_state, changeset_display, confirm_accordion, confirm_button, cancel_button,
656
- formatted_space_output_display, detected_files_preview, download_button
657
- ]
658
- # Initial yield state
659
- initial_updates = [
660
- gr.update(value=_build_status), _iframe_html, _file_browser_update,
661
- gr.update(value=ui_owner_name_part), gr.update(value=ui_space_name_part),
662
- _changeset_clear, gr.update(value=_changeset_summary_clear), _confirm_ui_hidden, _confirm_ui_hidden, _confirm_ui_hidden,
663
- gr.update(), gr.update(), gr.update() # Don't update markdown/download yet
664
- ]
665
- yield initial_updates
666
 
667
 
668
  if not ui_space_name_part or "/" in ui_space_name_part:
@@ -681,29 +832,25 @@ def handle_build_space_button(hf_api_key_ui, ui_space_name_part, ui_owner_name_p
681
 
682
  if not manual_changeset:
683
  _build_status = "Build Error: No target space specified or no files parsed from markdown."
684
- yield gr.update(value=_build_status), # Only update status
685
  return
686
 
687
-
688
  result_message = apply_staged_changes(hf_api_key_ui, ui_owner_name_part, ui_space_name_part, manual_changeset)
689
  _build_status = f"Manual Build/Update Result: {result_message}"
690
 
691
  owner_to_use = ui_owner_name_part
692
  space_to_use = ui_space_name_part
693
- # Keep manual markdown content as is, clear/disable preview/download initially
694
  _formatted_md = formatted_markdown_content
695
  _detected_preview_val = "*Loading files after build...*"
696
  _download_btn_update = gr.update(interactive=False, value=None)
697
 
698
- # Yield intermediate state with build status and placeholders for reloading
699
  yield (
700
- gr.update(value=_build_status), _iframe_html, _file_browser_update, # Build status, iframe, file browser (initial clear state)
701
- gr.update(value=owner_to_use), gr.update(value=space_to_use), # Update owner/space textboxes
702
- _changeset_clear, gr.update(value=_changeset_summary_clear), _confirm_ui_hidden, _confirm_ui_hidden, _confirm_ui_hidden, # Changeset clear/hide
703
- gr.update(value=_formatted_md), gr.update(value=_detected_preview_val), _download_btn_update # Update markdown/preview/download
704
  )
705
 
706
- # Reload space state to reflect actual content after build attempt
707
  sdk_built, file_list, err_list = get_space_repository_info(hf_api_key_ui, space_to_use, owner_to_use)
708
 
709
  if err_list:
@@ -725,22 +872,23 @@ def handle_build_space_button(hf_api_key_ui, ui_space_name_part, ui_owner_name_p
725
 
726
  if owner_to_use and space_to_use:
727
  sub_owner = re.sub(r'[^a-z0-9\-]+', '-', owner_to_use.lower()).strip('-') or 'owner'
728
- sub_repo = re.sub(r'[^a-z0-9\-]+', '-', ui_space_name_part.lower()).strip('-') or 'space'
729
  iframe_url = f"https://{sub_owner}-{sub_repo}{'.static.hf.space' if sdk_built == 'static' else '.hf.space'}"
730
  _iframe_html_update = gr.update(value=f'<iframe src="{iframe_url}?__theme=light&embed=true" width="100%" height="700px"></iframe>', visible=True)
731
  else:
732
  _iframe_html_update = gr.update(value=None, visible=False)
733
 
 
734
 
735
- _formatted_md, _detected_preview, _download = _generate_ui_outputs_from_cache(owner_to_use, space_to_use) # Regenerate previews from reloaded cache
 
736
 
737
 
738
- # Final yield after reloading
739
  yield (
740
- gr.update(value=_build_status), _iframe_html_update, _file_browser_update, # Build status, iframe, file browser
741
- gr.update(value=owner_to_use), gr.update(value=space_to_use), # Update owner/space textboxes
742
- _changeset_clear, gr.update(value=_changeset_summary_clear), _confirm_ui_hidden, _confirm_ui_hidden, _confirm_ui_hidden, # Changeset clear/hide
743
- gr.update(value=_formatted_md), gr.update(value=_detected_preview), _download # Update markdown/preview/download
744
  )
745
 
746
 
@@ -844,8 +992,122 @@ def handle_refresh_space_status(hf_api_key_ui, ui_owner_name, ui_space_name):
844
  log_link = status_details.get('full_log_link')
845
  if log_link and log_link != "#": md += f"- [View Full Logs]({log_link})\n"
846
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
847
  return md
848
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
849
 
850
  custom_theme = gr.themes.Base(primary_hue="teal", secondary_hue="purple", neutral_hue="zinc", text_size="sm", spacing_size="md", radius_size="sm", font=["System UI", "sans-serif"])
851
  custom_css = """
@@ -913,6 +1175,21 @@ with gr.Blocks(theme=custom_theme, css=custom_css) as demo:
913
  provider_api_key_input = gr.Textbox(label="Model Provider API Key (Optional)", type="password", placeholder="sk_... (overrides backend settings)")
914
  system_prompt_input = gr.Textbox(label="System Prompt", lines=10, value=DEFAULT_SYSTEM_PROMPT, elem_id="system-prompt")
915
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
916
  with gr.Column(scale=2):
917
  gr.Markdown("## πŸ’¬ AI Assistant Chat")
918
  chatbot_display = gr.Chatbot(label="AI Chat", height=500, bubble_full_width=False, avatar_images=(None))
@@ -940,8 +1217,8 @@ with gr.Blocks(theme=custom_theme, css=custom_css) as demo:
940
  build_space_button = gr.Button("πŸš€ Build / Update Space from Markdown", variant="primary")
941
  build_status_display = gr.Textbox(label="Manual Build/Update Status", interactive=False, value="*Manual build status...*")
942
  gr.Markdown("---")
943
- refresh_status_button = gr.Button("πŸ”„ Refresh Runtime Status")
944
- space_runtime_status_display = gr.Markdown("*Runtime status will appear here.*")
945
 
946
  with gr.TabItem("πŸ” Files Preview"):
947
  detected_files_preview = gr.Markdown(value="*A preview of the latest file versions will appear here.*")
@@ -961,6 +1238,7 @@ with gr.Blocks(theme=custom_theme, css=custom_css) as demo:
961
  gr.Markdown("### Live Space Preview")
962
  space_iframe_display = gr.HTML(value="", visible=True)
963
 
 
964
  provider_select.change(update_models_dropdown, inputs=provider_select, outputs=model_select)
965
 
966
  chat_inputs = [
@@ -976,10 +1254,12 @@ with gr.Blocks(theme=custom_theme, css=custom_css) as demo:
976
  send_chat_button.click(handle_chat_submit, inputs=chat_inputs, outputs=chat_outputs)
977
  chat_message_input.submit(handle_chat_submit, inputs=chat_inputs, outputs=chat_outputs)
978
 
 
979
  confirm_inputs = [hf_api_key_input, owner_name_input, space_name_input, changeset_state]
980
  confirm_outputs = [
981
  status_output, formatted_space_output_display, detected_files_preview, download_button,
982
- confirm_accordion, confirm_button, cancel_button, changeset_state, changeset_display
 
983
  ]
984
  confirm_button.click(handle_confirm_changes, inputs=confirm_inputs, outputs=confirm_outputs)
985
 
@@ -989,12 +1269,14 @@ with gr.Blocks(theme=custom_theme, css=custom_css) as demo:
989
  ]
990
  cancel_button.click(handle_cancel_changes, inputs=None, outputs=cancel_outputs)
991
 
 
992
  load_space_outputs = [
993
  formatted_space_output_display, detected_files_preview, status_output,
994
  file_browser_dropdown, owner_name_input, space_name_input,
995
  space_iframe_display, download_button, build_status_display,
996
  edit_status_display, space_runtime_status_display,
997
- changeset_state, changeset_display, confirm_accordion, confirm_button, cancel_button
 
998
  ]
999
  load_space_button.click(
1000
  fn=handle_load_existing_space,
@@ -1002,11 +1284,12 @@ with gr.Blocks(theme=custom_theme, css=custom_css) as demo:
1002
  outputs=load_space_outputs
1003
  )
1004
 
 
1005
  build_outputs = [
1006
  build_status_display, space_iframe_display, file_browser_dropdown,
1007
  owner_name_input, space_name_input,
1008
  changeset_state, changeset_display, confirm_accordion, confirm_button, cancel_button,
1009
- formatted_space_output_display, detected_files_preview, download_button
1010
  ]
1011
  build_inputs = [
1012
  hf_api_key_input, space_name_input, owner_name_input, space_sdk_select,
@@ -1029,5 +1312,18 @@ with gr.Blocks(theme=custom_theme, css=custom_css) as demo:
1029
 
1030
  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])
1031
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1032
  if __name__ == "__main__":
1033
  demo.launch(debug=False, mcp_server=True)
 
1
+ ### File: app.py
2
+ ```python
3
  import gradio as gr
4
  import re
5
  import json
 
19
  delete_space_file as build_logic_delete_space_file,
20
  get_space_runtime_status,
21
  apply_staged_changes,
22
+ duplicate_space as build_logic_duplicate_space,
23
+ list_user_spaces as build_logic_list_user_spaces,
24
+ build_logic_set_space_privacy,
25
+ build_logic_delete_space
26
  )
27
 
28
  from model_logic import (
 
75
 
76
  Available commands:
77
  - `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. Use this ONLY when the user explicitly asks to create a *new* space with a specific name. When using this action, also provide the initial file structure (e.g., app.py, README.md) using `### File:` blocks in the same response. The system will apply these files to the new space.
78
+ - `DUPLICATE_SPACE <new_owner>/<new_repo_name> --private <true|false>`: Duplicates the *currently loaded* space to a new space. Private is optional and defaults to false. This action is destructive if the target space already exists. Use this ONLY when the user explicitly asks to duplicate the space. If this action is staged, it will be the *only* action applied in that changeset.
79
  - `DELETE_FILE path/to/file.ext`: Deletes a specific file from the current space.
80
  - `SET_PRIVATE <true|false>`: Sets the privacy for the current space.
81
  - `DELETE_SPACE`: Deletes the entire current space. THIS IS PERMANENT AND REQUIRES CAUTION. Only use this if the user explicitly and clearly asks to delete the space.
82
 
83
+ You can issue multiple file updates and action commands in a single response. The system will process all of them into a single change plan. Note that the `DUPLICATE_SPACE` command, if present, will override any other file or space actions in the same response.
84
 
85
  **Current Space Context:**
86
  You will be provided with the current state of the files in the Space the user is interacting with. Use this information to understand the current project structure and content before proposing changes or actions. This context will appear after the user's message, starting with "## Current Space Context:". Do NOT include this context in your response. Only generate your response based on the user's request and the formatting rules above.
 
266
  ai_proposed_files_dict = {f["path"]: f for f in ai_proposed_files_list}
267
 
268
  action_pattern = re.compile(r"### HF_ACTION:\s*(?P<command_line>[^\n]+)", re.MULTILINE)
269
+ # Collect all potential actions first
270
+ potential_actions = []
271
  for match in action_pattern.finditer(ai_response_content):
272
  try:
273
  cmd_parts = shlex.split(match.group("command_line").strip())
274
  if not cmd_parts: continue
275
  command, args = cmd_parts[0].upper(), cmd_parts[1:]
276
+ potential_actions.append({"command": command, "args": args, "raw_line": match.group("command_line").strip()})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
277
  except Exception as e:
278
  print(f"Error parsing HF_ACTION line '{match.group('command_line').strip()}': {e}")
279
 
280
 
281
+ # Check for exclusive actions (like DUPLICATE_SPACE)
282
+ duplicate_action = next((act for act in potential_actions if act["command"] == "DUPLICATE_SPACE"), None)
283
+
284
+ if duplicate_action:
285
+ # If DUPLICATE_SPACE is present, only stage that action
286
+ cmd_parts = duplicate_action["args"]
287
+ if cmd_parts:
288
+ target_repo_id = cmd_parts[0]
289
+ private = False
290
+ if '--private' in cmd_parts:
291
+ try: private_str = cmd_parts[cmd_parts.index('--private') + 1].lower()
292
+ except IndexError: print("Warning: DUPLICATE_SPACE --private requires an argument.")
293
+ else: private = private_str == 'true'
294
+ # Source repo is the currently loaded one
295
+ source_repo_id = f"{hf_owner_name}/{hf_repo_name}" if hf_owner_name and hf_repo_name else None
296
+
297
+ if source_repo_id:
298
+ changeset.append({
299
+ "type": "DUPLICATE_SPACE",
300
+ "source_repo_id": source_repo_id,
301
+ "target_repo_id": target_repo_id,
302
+ "private": private
303
+ })
304
+ print(f"Staged exclusive DUPLICATE_SPACE action from {source_repo_id} to {target_repo_id}")
305
+ else:
306
+ print(f"Warning: Cannot stage DUPLICATE_SPACE action, no Space currently loaded.")
307
+ # Maybe add an error message to the summary?
308
+ changeset.append({"type": "Error", "message": "Cannot duplicate space, no Space currently loaded."})
309
+
310
+ else:
311
+ print(f"Warning: DUPLICATE_SPACE action requires a target repo_id.")
312
+ changeset.append({"type": "Error", "message": "DUPLICATE_SPACE action requires a target repo_id (<new_owner>/<new_repo_name>)."})
313
+
314
+ # If duplication is staged (or failed to stage due to syntax/missing source), skip other actions
315
+ if changeset:
316
+ md_summary = ["### πŸ“‹ Proposed Changes Plan (Exclusive Action)\n"]
317
+ if changeset[0]["type"] == "DUPLICATE_SPACE":
318
+ change = changeset[0]
319
+ md_summary.append(f"- **πŸ“‚ Duplicate Space:** Duplicate `{change.get('source_repo_id', '...')}` to `{change.get('target_repo_id', '...')}` (Private: {change.get('private', False)})")
320
+ md_summary.append("\n**Warning:** This will overwrite the target space if it exists. No other file or space actions proposed by the AI in this turn will be applied.")
321
+ else: # Handle the staging error case
322
+ md_summary.append(f"- **Error staging DUPLICATE_SPACE:** {changeset[0].get('message', 'Unknown error')}")
323
+ md_summary.append("\nNo changes were staged due to the error in the DUPLICATE_SPACE command.")
324
+ changeset = [] # Clear changeset if the duplicate action itself was malformed/blocked
325
+
326
+ return changeset, "\n".join(md_summary)
327
+
328
+
329
+ # If no exclusive action, process other actions and file changes
330
+ for action in potential_actions:
331
+ command, args = action["command"], action["args"]
332
+
333
+ if command == "CREATE_SPACE" and args:
334
+ repo_id = args[0]
335
+ sdk = "gradio"
336
+ private = False
337
+ if '--sdk' in args:
338
+ try: sdk = args[args.index('--sdk') + 1]
339
+ except IndexError: print("Warning: CREATE_SPACE --sdk requires an argument.")
340
+ if '--private' in args:
341
+ try: private_str = args[args.index('--private') + 1].lower()
342
+ except IndexError: print("Warning: CREATE_SPACE --private requires an argument.")
343
+ else: private = private_str == 'true'
344
+ changeset.append({"type": "CREATE_SPACE", "repo_id": repo_id, "sdk": sdk, "private": private})
345
+ print(f"Staged CREATE_SPACE action for {repo_id}")
346
+
347
+ elif command == "DELETE_FILE" and args:
348
+ file_path = args[0]
349
+ changeset.append({"type": "DELETE_FILE", "path": file_path})
350
+ print(f"Staged DELETE_FILE action for {file_path}")
351
+
352
+ elif command == "SET_PRIVATE" and args:
353
+ private = args[0].lower() == 'true'
354
+ changeset.append({"type": "SET_PRIVACY", "private": private, "repo_id": f"{hf_owner_name}/{hf_repo_name}"})
355
+ print(f"Staged SET_PRIVACY action for {hf_owner_name}/{hf_repo_name} to {private}")
356
+
357
+ elif command == "DELETE_SPACE":
358
+ changeset.append({"type": "DELETE_SPACE", "owner": hf_owner_name, "space_name": hf_repo_name})
359
+ print(f"Staged DELETE_SPACE action for {hf_owner_name}/{hf_repo_name}")
360
+ # Note: DUPLICATE_SPACE is handled before this loop
361
+
362
+
363
  for file_info in ai_proposed_files_list:
364
  filename = file_info["path"]
365
  proposed_content = file_info["content"]
 
382
  else:
383
  print(f"Skipping staging create for {filename}: Proposed content is a placeholder.")
384
 
385
+
386
  if not changeset:
387
  md_summary = ["### πŸ“‹ Proposed Changes Plan", "\nThe AI did not propose any specific changes to files or the space.\n"]
388
  else:
 
418
 
419
  def handle_chat_submit(user_message, chat_history, hf_api_key_input, provider_api_key_input, provider_select, model_select, system_prompt, hf_owner_name, hf_repo_name):
420
  global parsed_code_blocks_state_cache
421
+ _chat_msg_in = user_message
422
  _chat_hist = list(chat_history)
423
+ _chat_hist.append((user_message, None))
424
 
 
425
  yield (
426
+ "",
427
+ _chat_hist,
428
+ "Initializing...",
429
+ gr.update(),
430
+ gr.update(),
431
+ gr.update(interactive=False),
432
+ [],
433
+ gr.update(value="*No changes proposed.*"),
434
+ gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
435
  )
436
 
437
  current_sys_prompt = system_prompt.strip() or DEFAULT_SYSTEM_PROMPT
 
450
  if chunk is None: continue
451
  full_bot_response_content += str(chunk)
452
  _chat_hist[-1] = (user_message, full_bot_response_content)
 
453
  yield (
454
+ gr.update(),
455
+ _chat_hist,
456
+ f"Streaming from {model_select}...",
457
+ gr.update(),
458
+ gr.update(),
459
+ gr.update(),
460
+ gr.update(),
461
+ gr.update(),
462
+ gr.update(), gr.update(), gr.update()
463
  )
464
 
 
 
465
  if full_bot_response_content.startswith("Error:") or full_bot_response_content.startswith("API HTTP Error"):
466
  _status = full_bot_response_content
 
467
  yield (
468
  gr.update(), _chat_hist, _status,
469
+ gr.update(), gr.update(), gr.update(),
470
+ [], "*Error occurred, changes plan cleared.*",
471
+ gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
472
  )
473
+ return
474
 
 
475
  parsed_code_blocks_state_cache, proposed_filenames_in_turn = _parse_and_update_state_cache(full_bot_response_content, parsed_code_blocks_state_cache)
476
 
 
477
  _formatted, _detected, _download = _generate_ui_outputs_from_cache(hf_owner_name, hf_repo_name)
478
 
 
479
  staged_changeset, summary_md = generate_and_stage_changes(full_bot_response_content, parsed_code_blocks_state_cache, hf_owner_name, hf_repo_name)
480
 
 
481
  if not staged_changeset:
482
+ _status = summary_md
483
  yield (
484
+ gr.update(),
485
+ _chat_hist, _status,
486
+ _detected, _formatted, _download,
487
+ [], summary_md,
488
+ gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
489
  )
490
  else:
491
  _status = "Change plan generated. Please review and confirm below."
492
  yield (
493
+ gr.update(),
494
+ _chat_hist, _status,
495
+ _detected, _formatted, _download,
496
+ staged_changeset, summary_md,
497
+ gr.update(visible=True),
498
+ gr.update(visible=True),
499
+ gr.update(visible=True)
500
  )
501
 
502
  except Exception as e:
 
504
  print(f"Error in handle_chat_submit: {e}")
505
  import traceback
506
  traceback.print_exc()
 
507
  if _chat_hist:
 
508
  try:
509
  idx = next(i for i in reversed(range(len(_chat_hist))) if _chat_hist[i] and _chat_hist[i][0] == user_message)
510
  current_bot_response = _chat_hist[idx][1] or ""
511
  _chat_hist[idx] = (user_message, (current_bot_response + "\n\n" if current_bot_response else "") + error_msg)
512
  except (StopIteration, IndexError):
513
+ _chat_hist.append((user_message, error_msg))
514
 
 
515
  _formatted, _detected, _download = _generate_ui_outputs_from_cache(hf_owner_name, hf_repo_name)
516
 
 
517
  yield (
518
+ gr.update(),
519
+ _chat_hist, error_msg,
520
+ _detected, _formatted, _download,
521
+ [], "*Error occurred, changes plan cleared.*",
522
+ gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
523
  )
524
 
525
 
 
532
  if not changeset:
533
  return "No changes to apply.", gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(value="No changes were staged.")
534
 
535
+ # Check if the first action is an exclusive action like DUPLICATE_SPACE
536
+ first_action_type = changeset[0].get('type') if changeset else None
537
+ is_exclusive_action = first_action_type == 'DUPLICATE_SPACE'
538
+
539
+ if is_exclusive_action:
540
+ change = changeset[0]
541
+ if change.get("source_repo_id") and change.get("target_repo_id"):
542
+ status_message = build_logic_duplicate_space(
543
+ hf_api_key=hf_api_key,
544
+ source_repo_id=change["source_repo_id"],
545
+ target_repo_id=change["target_repo_id"],
546
+ private=change.get("private", False)
547
+ )
548
+ # After duplication, attempt to load the *new* space
549
+ _status_reload = f"{status_message} | Attempting to load the new Space [{change['target_repo_id']}]..."
550
+ yield _status_reload, gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(value="*Loading new Space...*")
551
+
552
+ # Extract new owner and space name from target_repo_id
553
+ new_owner, new_space_name = change["target_repo_id"].split('/', 1) if '/' in change["target_repo_id"] else (None, change["target_repo_id"])
554
+ if not new_owner: # Try to auto-detect owner if target was just a name
555
+ token, token_err = build_logic_get_api_token(hf_api_key)
556
+ if token_err: new_owner = None # Cannot auto-detect
557
+ else:
558
+ try: user_info = build_logic_whoami(token=token); new_owner = user_info.get('name')
559
+ except: new_owner = None
560
+
561
+ # Trigger the load logic for the new space
562
+ if new_owner and new_space_name:
563
+ # Call the load handler directly or replicate its logic
564
+ # Replicating is safer for yield chains
565
+ sdk, file_list, err_list = get_space_repository_info(hf_api_key, new_space_name, new_owner)
566
+ if err_list:
567
+ reload_error = f"Error reloading file list after duplication: {err_list}"
568
+ parsed_code_blocks_state_cache = []
569
+ else:
570
+ loaded_files = []
571
+ for file_path in file_list:
572
+ content, err_get = get_space_file_content(hf_api_key, new_space_name, new_owner, file_path)
573
+ lang = _infer_lang_from_filename(file_path)
574
+ is_binary = lang == "binary" or (err_get is not None)
575
+ code = f"[Error loading content: {err_get}]" if err_get else (content or "")
576
+ loaded_files.append({"filename": file_path, "code": code, "language": lang, "is_binary": is_binary, "is_structure_block": False})
577
+ parsed_code_blocks_state_cache = loaded_files
578
+
579
+ _formatted, _detected, _download = _generate_ui_outputs_from_cache(new_owner, new_space_name)
580
+ final_overall_status = status_message + (f" | Reload Status: {reload_error}" if reload_error else f" | Reload Status: New Space [{new_owner}/{new_space_name}] state refreshed.")
581
+
582
+ # Update UI fields to reflect the newly loaded space
583
+ owner_update = gr.update(value=new_owner)
584
+ space_update = gr.update(value=new_space_name)
585
+ file_browser_update = gr.update(visible=True, choices=sorted([f["filename"] for f in parsed_code_blocks_state_cache if not f.get("is_structure_block")] or []), value=None)
586
+
587
+ # Update iframe preview for the new space
588
+ if new_owner and new_space_name:
589
+ sub_owner = re.sub(r'[^a-z0-9\-]+', '-', new_owner.lower()).strip('-') or 'owner'
590
+ sub_repo = re.sub(r'[^a-z0-9\-]+', '-', new_space_name.lower()).strip('-') or 'space'
591
+ iframe_url = f"https://{sub_owner}-{sub_repo}{'.static.hf.space' if sdk == 'static' else '.hf.space'}"
592
+ iframe_update = gr.update(value=f'<iframe src="{iframe_url}?__theme=light&embed=true" width="100%" height="500px"></iframe>', visible=True)
593
+ else:
594
+ iframe_update = gr.update(value=None, visible=False)
595
+
596
 
597
+ else:
598
+ reload_error = "Cannot load new Space state: Owner or Space Name missing after duplication."
599
+ _formatted, _detected, _download = _generate_ui_outputs_from_cache(None, None) # Clear previews if new space couldn't be loaded
600
+ final_overall_status = status_message + f" | Reload Status: {reload_error}"
601
+ owner_update = gr.update() # Keep old value
602
+ space_update = gr.update() # Keep old value
603
+ file_browser_update = gr.update(visible=False, choices=[], value=None) # Clear file browser
604
+ iframe_update = gr.update(value=None, visible=False) # Hide iframe
605
 
 
 
 
606
 
 
 
 
 
 
607
  else:
608
+ # Should not happen if staged_changeset exists and type is DUPLICATE_SPACE, but as safeguard
609
+ final_overall_status = "Duplicate Action Error: Invalid duplicate changeset format."
610
+ # No state change, just report error
611
+ _formatted, _detected, _download = _generate_ui_outputs_from_cache(owner_name, space_name)
612
+ owner_update = gr.update()
613
+ space_update = gr.update()
614
+ file_browser_update = gr.update()
615
+ iframe_update = gr.update()
616
+
617
+
618
+ cleared_changeset = []
619
+ # Return updates including potentially new space info and file list
620
+ yield (
621
+ final_overall_status,
622
+ gr.update(value=_formatted), gr.update(value=_detected), _download,
623
+ gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), # Hide confirm UI
624
+ cleared_changeset, gr.update(value="*No changes proposed.*"), # Clear changeset state and display
625
+ owner_update, space_update, file_browser_update, iframe_update # Update space/owner fields, file browser, iframe
626
+ )
627
 
628
+ else: # Not an exclusive action, proceed with standard apply_staged_changes
629
+ status_message = apply_staged_changes(hf_api_key, owner_name, space_name, changeset)
630
 
631
+ _status_reload = f"{status_message} | Reloading Space state..."
632
+ yield _status_reload, gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(value="*Reloading Space state...*")
633
 
634
+ refreshed_file_list = []
635
+ reload_error = None
636
+ repo_id_for_reload = f"{owner_name}/{space_name}" if owner_name and space_name else None
637
 
638
+ if repo_id_for_reload:
639
+ sdk, file_list, err_list = get_space_repository_info(hf_api_key, space_name, owner_name)
640
+ if err_list:
641
+ reload_error = f"Error reloading file list after changes: {err_list}"
642
+ parsed_code_blocks_state_cache = []
643
+ else:
644
+ refreshed_file_list = file_list
645
+ loaded_files = []
646
+ for file_path in refreshed_file_list:
647
+ content, err_get = get_space_file_content(hf_api_key, space_name, owner_name, file_path)
648
+ lang = _infer_lang_from_filename(file_path)
649
+ is_binary = lang == "binary" or (err_get is not None)
650
+ code = f"[Error loading content: {err_get}]" if err_get else (content or "")
651
+ loaded_files.append({"filename": file_path, "code": code, "language": lang, "is_binary": is_binary, "is_structure_block": False})
652
+ parsed_code_blocks_state_cache = loaded_files
653
+
654
+ # Update file browser dropdown with refreshed list
655
+ file_browser_update = gr.update(visible=True, choices=sorted(refreshed_file_list or []), value=None)
656
+ # Update iframe preview for the current space (might not have changed URL, but status may update)
657
+ if owner_name and space_name:
658
+ sub_owner = re.sub(r'[^a-z0-9\-]+', '-', owner_name.lower()).strip('-') or 'owner'
659
+ sub_repo = re.sub(r'[^a-z0-9\-]+', '-', space_name.lower()).strip('-') or 'space'
660
+ iframe_url = f"https://{sub_owner}-{sub_repo}{'.static.hf.space' if sdk == 'static' else '.hf.space'}"
661
+ iframe_update = gr.update(value=f'<iframe src="{iframe_url}?__theme=light&embed=true" width="100%" height="500px"></iframe>', visible=True)
662
+ else:
663
+ iframe_update = gr.update(value=None, visible=False)
664
+
665
+ else:
666
+ reload_error = "Cannot reload Space state: Owner or Space Name missing."
667
+ # Clear UI elements related to files if reload fails
668
+ file_browser_update = gr.update(visible=False, choices=[], value=None)
669
+ iframe_update = gr.update(value=None, visible=False)
670
 
671
+
672
+ _formatted, _detected, _download = _generate_ui_outputs_from_cache(owner_name, space_name)
673
+
674
+ final_overall_status = status_message + (f" | Reload Status: {reload_error}" if reload_error else " | Reload Status: Space state refreshed.")
675
+
676
+ cleared_changeset = []
677
+
678
+ # Return updated UI elements and hide confirmation UI
679
+ yield (
680
+ final_overall_status,
681
+ gr.update(value=_formatted),
682
+ gr.update(value=_detected),
683
+ _download,
684
+ gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), # Hide confirm UI
685
+ cleared_changeset, gr.update(value="*No changes proposed.*"), # Clear changeset state and display
686
+ gr.update(), gr.update(), file_browser_update, iframe_update # Keep owner/space, update file browser and iframe
687
+ )
688
 
689
 
690
  def handle_cancel_changes():
 
710
  global parsed_code_blocks_state_cache
711
  _formatted_md_val, _detected_preview_val, _status_val = "*Loading files...*", "*Loading files...*", f"Loading Space: {ui_owner_name}/{ui_space_name}..."
712
  _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)
713
+ _build_status_clear, _edit_status_clear, _runtime_status_clear = "*Manual build status...*", "*Select a file...*", "*Runtime status...*"
714
  _changeset_clear = []
715
  _changeset_summary_clear = "*No changes proposed.*"
716
  _confirm_ui_hidden = gr.update(visible=False)
717
+ _list_spaces_display_clear = "*List of spaces will appear here.*"
718
 
719
+
720
+ # Initial yield to show loading state
721
+ yield (
 
 
 
 
 
 
722
  gr.update(value=_formatted_md_val), gr.update(value=_detected_preview_val), gr.update(value=_status_val), _file_browser_update,
723
  gr.update(value=ui_owner_name), gr.update(value=ui_space_name),
724
  _iframe_html_update, _download_btn_update, gr.update(value=_build_status_clear),
725
  gr.update(value=_edit_status_clear), gr.update(value=_runtime_status_clear),
726
+ _changeset_clear, gr.update(value=_changeset_summary_clear), _confirm_ui_hidden, _confirm_ui_hidden, _confirm_ui_hidden,
727
+ gr.update(), gr.update(), gr.update() # For list spaces updates
728
+ )
 
729
 
730
  owner_to_use = ui_owner_name
731
+ token, token_err = build_logic_get_api_token(hf_api_key_ui)
732
+ if token_err:
733
+ _status_val = f"Load Error: {token_err}"
734
+ yield gr.update(value=_status_val), # Only update status on early error
735
+ return
736
  if not owner_to_use:
 
 
 
 
 
737
  try:
738
  user_info = build_logic_whoami(token=token)
739
  owner_to_use = user_info.get('name')
 
741
  yield gr.update(value=owner_to_use), gr.update(value=f"Loading Space: {owner_to_use}/{ui_space_name} (Auto-detected owner)...") # Update owner and status
742
  except Exception as e:
743
  _status_val = f"Load Error: Error auto-detecting owner: {e}"
744
+ yield gr.update(value=_status_val),
745
  return
746
 
747
  if not owner_to_use or not ui_space_name:
 
751
 
752
  sdk, file_list, err = get_space_repository_info(hf_api_key_ui, ui_space_name, owner_to_use)
753
 
754
+ # Always update owner/space inputs even on error
755
+ yield gr.update(value=owner_to_use), gr.update(value=ui_space_name),
 
756
 
757
  if err:
758
  _status_val = f"Load Error: {err}"
759
  parsed_code_blocks_state_cache = []
760
  _formatted, _detected, _download = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name)
761
  yield (
762
+ gr.update(value=_formatted), gr.update(value=_detected), gr.update(value=_status_val),
763
+ gr.update(visible=False, choices=[], value=None),
764
+ gr.update(), gr.update(),
765
+ gr.update(value=None, visible=False),
766
+ _download, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
767
  )
768
  return
769
 
 
790
  else:
791
  iframe_update = gr.update(value=None, visible=False)
792
 
793
+ # Also fetch and display runtime status after loading
794
+ runtime_status_md = handle_refresh_space_status(hf_api_key_ui, owner_to_use, ui_space_name)
795
+
796
+
797
  # Final yield after success
798
  yield (
799
+ gr.update(value=_formatted), gr.update(value=_detected), gr.update(value=_status_val),
800
+ file_browser_update,
801
+ gr.update(), gr.update(),
802
+ iframe_update,
803
+ _download, gr.update(), gr.update(), gr.update(value=runtime_status_md), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
804
  )
805
 
806
  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):
 
811
  _changeset_summary_clear = "*Manual build initiated, changes plan cleared.*"
812
  _confirm_ui_hidden = gr.update(visible=False)
813
 
814
+ yield (_build_status, _iframe_html, _file_browser_update, gr.update(value=ui_owner_name_part), gr.update(value=ui_space_name_part),
815
+ _changeset_clear, gr.update(value=_changeset_summary_clear), _confirm_ui_hidden, _confirm_ui_hidden, _confirm_ui_hidden,
816
+ gr.update(), gr.update(), gr.update())
 
 
 
 
 
 
 
 
 
 
 
817
 
818
 
819
  if not ui_space_name_part or "/" in ui_space_name_part:
 
832
 
833
  if not manual_changeset:
834
  _build_status = "Build Error: No target space specified or no files parsed from markdown."
835
+ yield gr.update(value=_build_status),
836
  return
837
 
 
838
  result_message = apply_staged_changes(hf_api_key_ui, ui_owner_name_part, ui_space_name_part, manual_changeset)
839
  _build_status = f"Manual Build/Update Result: {result_message}"
840
 
841
  owner_to_use = ui_owner_name_part
842
  space_to_use = ui_space_name_part
 
843
  _formatted_md = formatted_markdown_content
844
  _detected_preview_val = "*Loading files after build...*"
845
  _download_btn_update = gr.update(interactive=False, value=None)
846
 
 
847
  yield (
848
+ gr.update(value=_build_status), _iframe_html, _file_browser_update,
849
+ gr.update(value=owner_to_use), gr.update(value=space_to_use),
850
+ _changeset_clear, gr.update(value=_changeset_summary_clear), _confirm_ui_hidden, _confirm_ui_hidden, _confirm_ui_hidden,
851
+ gr.update(value=_formatted_md), gr.update(value=_detected_preview_val), _download_btn_update
852
  )
853
 
 
854
  sdk_built, file_list, err_list = get_space_repository_info(hf_api_key_ui, space_to_use, owner_to_use)
855
 
856
  if err_list:
 
872
 
873
  if owner_to_use and space_to_use:
874
  sub_owner = re.sub(r'[^a-z0-9\-]+', '-', owner_to_use.lower()).strip('-') or 'owner'
875
+ sub_repo = re.sub(r'[^a-z0-9\-]+', '-', space_to_use.lower()).strip('-') or 'space'
876
  iframe_url = f"https://{sub_owner}-{sub_repo}{'.static.hf.space' if sdk_built == 'static' else '.hf.space'}"
877
  _iframe_html_update = gr.update(value=f'<iframe src="{iframe_url}?__theme=light&embed=true" width="100%" height="700px"></iframe>', visible=True)
878
  else:
879
  _iframe_html_update = gr.update(value=None, visible=False)
880
 
881
+ _formatted_md, _detected_preview, _download = _generate_ui_outputs_from_cache(owner_to_use, space_to_use)
882
 
883
+ # Also fetch and display runtime status after build
884
+ runtime_status_md = handle_refresh_space_status(hf_api_key_ui, owner_to_use, space_to_use)
885
 
886
 
 
887
  yield (
888
+ gr.update(value=_build_status), _iframe_html_update, _file_browser_update,
889
+ gr.update(value=owner_to_use), gr.update(value=space_to_use),
890
+ _changeset_clear, gr.update(value=_changeset_summary_clear), _confirm_ui_hidden, _confirm_ui_hidden, _confirm_ui_hidden,
891
+ gr.update(value=_formatted_md), gr.update(value=_detected_preview), _download, gr.update(value=runtime_status_md)
892
  )
893
 
894
 
 
992
  log_link = status_details.get('full_log_link')
993
  if log_link and log_link != "#": md += f"- [View Full Logs]({log_link})\n"
994
 
995
+ # Get general repo info as well
996
+ sdk, file_list, repo_info_err = get_space_repository_info(hf_api_key_ui, ui_space_name, ui_owner_name)
997
+ if repo_info_err:
998
+ md += f"- **Repo Info Error:** {repo_info_err}\n"
999
+ else:
1000
+ md += f"- **SDK:** `{sdk or 'N/A'}`\n"
1001
+ md += f"- **File Count:** `{len(file_list) if file_list is not None else 'N/A'}`\n"
1002
+ # Add more repo info if needed from get_space_repository_info result
1003
+
1004
+ return md
1005
+
1006
+ def handle_list_spaces(hf_api_key_ui, ui_owner_name):
1007
+ token, token_err = build_logic_get_api_token(hf_api_key_ui)
1008
+ if token_err:
1009
+ return f"**List Spaces Error:** {token_err}"
1010
+
1011
+ owner_to_list = ui_owner_name
1012
+ if not owner_to_list:
1013
+ try:
1014
+ user_info = build_logic_whoami(token=token)
1015
+ owner_to_list = user_info.get('name')
1016
+ if not owner_to_list: raise Exception("Could not find user name from token.")
1017
+ except Exception as e:
1018
+ return f"**List Spaces Error:** Error auto-detecting owner: {e}. Please specify Owner field."
1019
+
1020
+ if not owner_to_list:
1021
+ return "**List Spaces Error:** Owner could not be determined. Please specify it in the Owner field."
1022
+
1023
+ spaces_list, err = build_logic_list_user_spaces(hf_api_key=token, owner=owner_to_list)
1024
+
1025
+ if err:
1026
+ return f"**List Spaces Error:** {err}"
1027
+
1028
+ if not spaces_list:
1029
+ return f"*No spaces found for user/org `{owner_to_list}`.*"
1030
+
1031
+ md = f"### Spaces for `{owner_to_list}`\n"
1032
+ for space in sorted(spaces_list):
1033
+ md += f"- `{space}`\n"
1034
+
1035
  return md
1036
 
1037
+ def handle_manual_duplicate_space(hf_api_key_ui, source_owner, source_space_name, target_owner, target_space_name, target_private):
1038
+ if not source_owner or not source_space_name:
1039
+ return "Duplicate Error: Please load a Space first to duplicate.", gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
1040
+ if not target_owner or not target_space_name:
1041
+ return "Duplicate Error: Target Owner and Target Space Name are required.", gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
1042
+ if "/" in target_space_name:
1043
+ return "Duplicate Error: Target Space Name should not contain '/'. Use Target Owner field for the owner part.", gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
1044
+
1045
+
1046
+ source_repo_id = f"{source_owner}/{source_space_name}"
1047
+ target_repo_id = f"{target_owner}/{target_space_name}"
1048
+
1049
+ status_msg = f"Attempting to duplicate `{source_repo_id}` to `{target_repo_id}`..."
1050
+ yield status_msg, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update() # Update status immediately
1051
+
1052
+ result_message = build_logic_duplicate_space(hf_api_key_ui, source_repo_id, target_repo_id, target_private)
1053
+ status_msg = f"Duplication Result: {result_message}"
1054
+
1055
+ # Attempt to load the new space after duplication attempt
1056
+ _status_reload = f"{status_msg} | Attempting to load the new Space [{target_repo_id}]..."
1057
+ yield _status_reload, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
1058
+
1059
+ # Replicate load logic for the new space
1060
+ # Note: This replicates parts of handle_load_existing_space, could refactor common parts
1061
+ new_owner = target_owner # Use the provided target owner
1062
+ new_space_name = target_space_name # Use the provided target space name
1063
+
1064
+ sdk, file_list, err_list = get_space_repository_info(hf_api_key_ui, new_space_name, new_owner)
1065
+
1066
+ if err_list:
1067
+ reload_error = f"Error reloading file list after duplication: {err_list}"
1068
+ global parsed_code_blocks_state_cache
1069
+ parsed_code_blocks_state_cache = []
1070
+ _file_browser_update = gr.update(visible=False, choices=[], value=None)
1071
+ _iframe_html_update = gr.update(value=None, visible=False)
1072
+ else:
1073
+ loaded_files = []
1074
+ for file_path in file_list:
1075
+ content, err_get = get_space_file_content(hf_api_key_ui, new_space_name, new_owner, file_path)
1076
+ lang = _infer_lang_from_filename(file_path)
1077
+ is_binary = lang == "binary" or (err_get is not None)
1078
+ code = f"[Error loading content: {err_get}]" if err_get else (content or "")
1079
+ loaded_files.append({"filename": file_path, "code": code, "language": lang, "is_binary": is_binary, "is_structure_block": False})
1080
+ global parsed_code_blocks_state_cache
1081
+ parsed_code_blocks_state_cache = loaded_files
1082
+
1083
+ _file_browser_update = gr.update(visible=True, choices=sorted([f["filename"] for f in parsed_code_blocks_state_cache if not f.get("is_structure_block")] or []), value=None)
1084
+ if new_owner and new_space_name:
1085
+ sub_owner = re.sub(r'[^a-z0-9\-]+', '-', new_owner.lower()).strip('-') or 'owner'
1086
+ sub_repo = re.sub(r'[^a-z0-9\-]+', '-', new_space_name.lower()).strip('-') or 'space'
1087
+ iframe_url = f"https://{sub_owner}-{sub_repo}{'.static.hf.space' if sdk == 'static' else '.hf.space'}"
1088
+ _iframe_html_update = gr.update(value=f'<iframe src="{iframe_url}?__theme=light&embed=true" width="100%" height="500px"></iframe>', visible=True)
1089
+ else:
1090
+ _iframe_html_update = gr.update(value=None, visible=False)
1091
+
1092
+
1093
+ _formatted, _detected, _download = _generate_ui_outputs_from_cache(new_owner, new_space_name)
1094
+ final_overall_status = status_msg + (f" | Reload Status: {reload_error}" if reload_error else f" | Reload Status: New Space [{new_owner}/{new_space_name}] state refreshed.")
1095
+
1096
+ # Fetch runtime status for the new space
1097
+ runtime_status_md = handle_refresh_space_status(hf_api_key_ui, new_owner, new_space_name)
1098
+
1099
+ # Update UI fields to reflect the newly loaded space
1100
+ owner_update = gr.update(value=new_owner)
1101
+ space_update = gr.update(value=new_space_name)
1102
+
1103
+
1104
+ return (
1105
+ final_overall_status,
1106
+ owner_update, space_update,
1107
+ _formatted, _detected, _download,
1108
+ _file_browser_update, _iframe_html_update, gr.update(value=runtime_status_md)
1109
+ )
1110
+
1111
 
1112
  custom_theme = gr.themes.Base(primary_hue="teal", secondary_hue="purple", neutral_hue="zinc", text_size="sm", spacing_size="md", radius_size="sm", font=["System UI", "sans-serif"])
1113
  custom_css = """
 
1175
  provider_api_key_input = gr.Textbox(label="Model Provider API Key (Optional)", type="password", placeholder="sk_... (overrides backend settings)")
1176
  system_prompt_input = gr.Textbox(label="System Prompt", lines=10, value=DEFAULT_SYSTEM_PROMPT, elem_id="system-prompt")
1177
 
1178
+ with gr.Accordion("πŸ—„οΈ Space Management", open=True):
1179
+ gr.Markdown("### Manual Actions")
1180
+ with gr.Group():
1181
+ gr.Markdown("Duplicate Current Space To:")
1182
+ target_owner_input = gr.Textbox(label="Target Owner Name", placeholder="e.g., new-username")
1183
+ target_space_name_input = gr.Textbox(label="Target Space Name", placeholder="e.g., new-space")
1184
+ target_private_checkbox = gr.Checkbox(label="Make Target Private", value=False)
1185
+ duplicate_space_button = gr.Button("πŸ“‚ Duplicate Space", variant="secondary")
1186
+
1187
+ gr.Markdown("---")
1188
+ gr.Markdown("### List Spaces")
1189
+ list_spaces_button = gr.Button("πŸ“„ List My Spaces", variant="secondary")
1190
+ list_spaces_display = gr.Markdown("*List of spaces will appear here.*")
1191
+
1192
+
1193
  with gr.Column(scale=2):
1194
  gr.Markdown("## πŸ’¬ AI Assistant Chat")
1195
  chatbot_display = gr.Chatbot(label="AI Chat", height=500, bubble_full_width=False, avatar_images=(None))
 
1217
  build_space_button = gr.Button("πŸš€ Build / Update Space from Markdown", variant="primary")
1218
  build_status_display = gr.Textbox(label="Manual Build/Update Status", interactive=False, value="*Manual build status...*")
1219
  gr.Markdown("---")
1220
+ refresh_status_button = gr.Button("πŸ”„ Refresh Runtime/Repo Status")
1221
+ space_runtime_status_display = gr.Markdown("*Runtime/Repo status will appear here.*")
1222
 
1223
  with gr.TabItem("πŸ” Files Preview"):
1224
  detected_files_preview = gr.Markdown(value="*A preview of the latest file versions will appear here.*")
 
1238
  gr.Markdown("### Live Space Preview")
1239
  space_iframe_display = gr.HTML(value="", visible=True)
1240
 
1241
+
1242
  provider_select.change(update_models_dropdown, inputs=provider_select, outputs=model_select)
1243
 
1244
  chat_inputs = [
 
1254
  send_chat_button.click(handle_chat_submit, inputs=chat_inputs, outputs=chat_outputs)
1255
  chat_message_input.submit(handle_chat_submit, inputs=chat_inputs, outputs=chat_outputs)
1256
 
1257
+ # Note: handle_confirm_changes yields multiple sets of outputs for intermediate steps
1258
  confirm_inputs = [hf_api_key_input, owner_name_input, space_name_input, changeset_state]
1259
  confirm_outputs = [
1260
  status_output, formatted_space_output_display, detected_files_preview, download_button,
1261
+ confirm_accordion, confirm_button, cancel_button, changeset_state, changeset_display,
1262
+ owner_name_input, space_name_input, file_browser_dropdown, space_iframe_display # Added outputs for DUPLICATE_SPACE load
1263
  ]
1264
  confirm_button.click(handle_confirm_changes, inputs=confirm_inputs, outputs=confirm_outputs)
1265
 
 
1269
  ]
1270
  cancel_button.click(handle_cancel_changes, inputs=None, outputs=cancel_outputs)
1271
 
1272
+ # Note: handle_load_existing_space yields multiple sets of outputs for intermediate steps
1273
  load_space_outputs = [
1274
  formatted_space_output_display, detected_files_preview, status_output,
1275
  file_browser_dropdown, owner_name_input, space_name_input,
1276
  space_iframe_display, download_button, build_status_display,
1277
  edit_status_display, space_runtime_status_display,
1278
+ changeset_state, changeset_display, confirm_accordion, confirm_button, cancel_button,
1279
+ target_owner_input, target_space_name_input, target_private_checkbox # Added outputs for List Spaces
1280
  ]
1281
  load_space_button.click(
1282
  fn=handle_load_existing_space,
 
1284
  outputs=load_space_outputs
1285
  )
1286
 
1287
+ # Note: handle_build_space_button yields multiple sets of outputs for intermediate steps
1288
  build_outputs = [
1289
  build_status_display, space_iframe_display, file_browser_dropdown,
1290
  owner_name_input, space_name_input,
1291
  changeset_state, changeset_display, confirm_accordion, confirm_button, cancel_button,
1292
+ formatted_space_output_display, detected_files_preview, download_button, space_runtime_status_display # Added runtime status output
1293
  ]
1294
  build_inputs = [
1295
  hf_api_key_input, space_name_input, owner_name_input, space_sdk_select,
 
1312
 
1313
  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])
1314
 
1315
+ # Manual Duplicate Space Button logic - Note it yields
1316
+ manual_duplicate_inputs = [hf_api_key_input, owner_name_input, space_name_input, target_owner_input, target_space_name_input, target_private_checkbox]
1317
+ manual_duplicate_outputs = [
1318
+ status_output, owner_name_input, space_name_input,
1319
+ formatted_space_output_display, detected_files_preview, download_button,
1320
+ file_browser_dropdown, space_iframe_display, space_runtime_status_display # Outputs needed after loading the new space
1321
+ ]
1322
+ duplicate_space_button.click(fn=handle_manual_duplicate_space, inputs=manual_duplicate_inputs, outputs=manual_duplicate_outputs)
1323
+
1324
+ # List Spaces Button logic
1325
+ list_spaces_button.click(fn=handle_list_spaces, inputs=[hf_api_key_input, owner_name_input], outputs=[list_spaces_display])
1326
+
1327
+
1328
  if __name__ == "__main__":
1329
  demo.launch(debug=False, mcp_server=True)