diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -4,7 +4,7 @@ import json import os import tempfile import shlex -from huggingface_hub import HfApi +from huggingface_hub import HfApi, HfHubHTTPError # Import HfHubHTTPError from build_logic import ( _get_api_token as build_logic_get_api_token, @@ -24,7 +24,7 @@ from build_logic import ( build_logic_create_pull_request, build_logic_add_comment, build_logic_like_space, - build_logic_create_space + build_logic_create_space ) from model_logic import ( @@ -77,7 +77,7 @@ The system will parse these actions from your response and present them to the u Available commands: - `CREATE_SPACE / --sdk --private `: 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. -- `DUPLICATE_SPACE / --private `: 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. +- `DUPLICATE_SPACE / --private `: 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. 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. - `DELETE_FILE path/to/file.ext`: Deletes a specific file from the current space. - `SET_PRIVATE `: Sets the privacy for the current space. - `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. @@ -374,12 +374,12 @@ def generate_and_stage_changes(ai_response_content, current_files_state, hf_owne changeset.append({"type": "CREATE_PR", "source_repo_id": f"{hf_owner_name}/{hf_repo_name}", "target_repo_id": target_repo_id, "title": title, "body": body}) print(f"Staged CREATE_PR action to {target_repo_id}") else: - print("Warning: CREATE_PR requires target_repo_id and --title.") - changeset.append({"type": "Error", "message": "CREATE_PR action requires target_repo_id and --title arguments."}) + # Changed from appending error to changeset to just printing error + # Action staging errors shouldn't block file changes if any were proposed + print("Warning: CREATE_PR requires target_repo_id and --title.") except Exception as e: print(f"Error parsing CREATE_PR arguments: {e}") - changeset.append({"type": "Error", "message": f"Error parsing CREATE_PR arguments: {e}"}) - + # Changed from appending error to changeset to just printing error elif command == "ADD_COMMENT" and args: comment_text = None @@ -395,12 +395,12 @@ def generate_and_stage_changes(ai_response_content, current_files_state, hf_owne changeset.append({"type": "ADD_COMMENT", "repo_id": f"{hf_owner_name}/{hf_repo_name}", "comment": comment_text}) print(f"Staged ADD_COMMENT action on {hf_owner_name}/{hf_repo_name}") else: + # Changed from appending error to changeset to just printing error print("Warning: ADD_COMMENT requires comment text.") - changeset.append({"type": "Error", "message": "ADD_COMMENT action requires comment text (in quotes)."}) except Exception as e: print(f"Error parsing ADD_COMMENT arguments: {e}") - changeset.append({"type": "Error", "message": f"Error parsing ADD_COMMENT arguments: {e}"}) + # Changed from appending error to changeset to just printing error elif command == "LIKE_SPACE": @@ -411,27 +411,30 @@ def generate_and_stage_changes(ai_response_content, current_files_state, hf_owne print(f"Warning: Cannot stage LIKE_SPACE action, no Space currently loaded.") changeset.append({"type": "Error", "message": "Cannot like space, no Space currently loaded."}) - for file_info in ai_proposed_files_list: - filename = file_info["path"] - proposed_content = file_info["content"] + # Process file changes only if not an exclusive duplicate action + if not duplicate_action: + for file_info in ai_proposed_files_list: + filename = file_info["path"] + proposed_content = file_info["content"] - if filename in current_files_dict: - current_content = current_files_dict[filename]["code"] - is_proposed_placeholder = proposed_content.startswith(("[Binary file", "[Error loading content:", "[Binary or Skipped file]")) + if filename in current_files_dict: + current_content = current_files_dict[filename].get("code", "") + is_proposed_placeholder = proposed_content.startswith(("[Binary file", "[Error loading content:", "[Binary or Skipped file]")) - if not is_proposed_placeholder and proposed_content != current_content: - lang = current_files_dict[filename].get("language") or _infer_lang_from_filename(filename) - changeset.append({"type": "UPDATE_FILE", "path": filename, "content": proposed_content, "lang": lang}) - elif is_proposed_placeholder: - print(f"Skipping staging update for {filename}: Proposed content is a placeholder.") + if not is_proposed_placeholder and proposed_content != current_content: + lang = current_files_dict[filename].get("language") or _infer_lang_from_filename(filename) + changeset.append({"type": "UPDATE_FILE", "path": filename, "content": proposed_content, "lang": lang}) + elif is_proposed_placeholder: + print(f"Skipping staging update for {filename}: Proposed content is a placeholder.") - else: - proposed_content = file_info["content"] - if not (proposed_content.startswith("[Binary file") or proposed_content.startswith("[Error loading content:") or proposed_content.startswith("[Binary or Skipped file]")): - lang = _infer_lang_from_filename(filename) - changeset.append({"type": "CREATE_FILE", "path": filename, "content": proposed_content, "lang": lang}) else: - print(f"Skipping staging create for {filename}: Proposed content is a placeholder.") + # Creating a new file + proposed_content = file_info["content"] + if not (proposed_content.startswith("[Binary file") or proposed_content.startswith("[Error loading content:") or proposed_content.startswith("[Binary or Skipped file]")): + lang = _infer_lang_from_filename(filename) + changeset.append({"type": "CREATE_FILE", "path": filename, "content": proposed_content, "lang": lang}) + else: + print(f"Skipping staging create for {filename}: Proposed content is a placeholder.") if not changeset: @@ -457,7 +460,7 @@ def generate_and_stage_changes(ai_response_content, current_files_state, hf_owne elif change["type"] == "CREATE_PR": md_summary.append(f"- **🤝 Create Pull Request:** From `{change.get('source_repo_id', '...')}` to `{change.get('target_repo_id', '...')}`") md_summary.append(f" - Title: `{change.get('title', '...')}`") - if change.get('body'): md_summary.append(f" - Body: `{change.get('body', '...')}`") + if change.get('body'): md_summary.append(f" - Body: `{change.get('body', '...')[:100]}{'...' if len(change.get('body', '')) > 100 else ''}`") elif change["type"] == "ADD_COMMENT": md_summary.append(f"- **💬 Add Comment:** On `{change.get('repo_id', '...')}`") md_summary.append(f" - Comment: `{change.get('comment', '...')[:100]}{'...' if len(change.get('comment', '')) > 100 else ''}`") @@ -487,16 +490,20 @@ def handle_chat_submit(user_message, chat_history, hf_api_key_input, provider_ap _chat_hist = list(chat_history) _chat_hist.append((user_message, None)) + # Yield initial state to show thinking and clear UI elements + # Need to match the 11 chat_outputs yield ( - "", - _chat_hist, - "Initializing...", - gr.update(), - gr.update(), - gr.update(interactive=False), - [], - gr.update(value="*No changes proposed.*"), - gr.update(visible=False), gr.update(visible=False), gr.update(visible=False) + "", # chat_message_input + _chat_hist, # chatbot_display + "Thinking...", # status_output + gr.update(), # detected_files_preview + gr.update(), # formatted_space_output_display + gr.update(interactive=False), # download_button + [], # changeset_state (clear old changes) + gr.update(value="*Processing AI response...*"), # changeset_display + gr.update(visible=False), # confirm_accordion + gr.update(visible=False), # confirm_button + gr.update(visible=False) # cancel_button ) current_sys_prompt = system_prompt.strip() or DEFAULT_SYSTEM_PROMPT @@ -515,94 +522,145 @@ def handle_chat_submit(user_message, chat_history, hf_api_key_input, provider_ap if chunk is None: continue full_bot_response_content += str(chunk) _chat_hist[-1] = (user_message, full_bot_response_content) + # Yield during streaming yield ( - gr.update(), - _chat_hist, - f"Streaming from {model_select}...", - gr.update(), - gr.update(), - gr.update(), - gr.update(), - gr.update(), - gr.update(), gr.update(), gr.update() + gr.update(), # chat_message_input (keep empty) + _chat_hist, # chatbot_display (update chat history) + f"Streaming from {model_select}...", # status_output + gr.update(), # detected_files_preview (no update during stream) + gr.update(), # formatted_space_output_display (no update during stream) + gr.update(), # download_button (no update during stream) + gr.update(), # changeset_state (no update during stream) + gr.update(value=f"*Processing AI response...*\n\n{full_bot_response_content[-200:]}..."), # changeset_display (show partial AI output) + gr.update(visible=False), gr.update(visible=False), gr.update(visible=False) # confirm buttons hidden ) if full_bot_response_content.startswith("Error:") or full_bot_response_content.startswith("API HTTP Error"): _status = full_bot_response_content + _chat_hist[-1] = (user_message, _status) # Update chat with full error yield ( gr.update(), _chat_hist, _status, gr.update(), gr.update(), gr.update(), - [], "*Error occurred, changes plan cleared.*", + [], "*Error occurred, changes plan cleared.*", # Clear staged changes on error gr.update(visible=False), gr.update(visible=False), gr.update(visible=False) ) return parsed_code_blocks_state_cache, proposed_filenames_in_turn = _parse_and_update_state_cache(full_bot_response_content, parsed_code_blocks_state_cache) - _formatted, _detected, _download = _generate_ui_outputs_from_cache(hf_owner_name, hf_repo_name) + _formatted, _detected, _download_update = _generate_ui_outputs_from_cache(hf_owner_name, hf_repo_name) staged_changeset, summary_md = generate_and_stage_changes(full_bot_response_content, parsed_code_blocks_state_cache, hf_owner_name, hf_repo_name) + _chat_hist[-1] = (user_message, full_bot_response_content) # Final update to chat history with full response + if not staged_changeset: - _status = summary_md + _status = "AI finished. No changes proposed." yield ( - gr.update(), - _chat_hist, _status, - _detected, _formatted, _download, - [], summary_md, - gr.update(visible=False), gr.update(visible=False), gr.update(visible=False) + gr.update(), # chat_message_input + _chat_hist, # chatbot_display + _status, # status_output + gr.update(value=_detected), # detected_files_preview + gr.update(value=_formatted), # formatted_space_output_display + _download_update, # download_button + [], # changeset_state (explicitly clear if no changes were staged) + gr.update(value="*No changes proposed.*"), # changeset_display + gr.update(visible=False), # confirm_accordion + gr.update(visible=False), # confirm_button + gr.update(visible=False) # cancel_button ) else: - _status = "Change plan generated. Please review and confirm below." + _status = "AI finished. Change plan generated. Please review and confirm below." yield ( - gr.update(), - _chat_hist, _status, - _detected, _formatted, _download, - staged_changeset, summary_md, - gr.update(visible=True), - gr.update(visible=True), - gr.update(visible=True) + gr.update(), # chat_message_input + _chat_hist, # chatbot_display + _status, # status_output + gr.update(value=_detected), # detected_files_preview + gr.update(value=_formatted), # formatted_space_output_display + _download_update, # download_button + staged_changeset, # changeset_state + gr.update(value=summary_md), # changeset_display + gr.update(visible=True), # confirm_accordion + gr.update(visible=True), # confirm_button + gr.update(visible=True) # cancel_button ) except Exception as e: - error_msg = f"An unexpected error occurred during chat submission: {e}" + error_msg = f"An unexpected error occurred during chat submission: {type(e).__name__}: {e}" print(f"Error in handle_chat_submit: {e}") import traceback traceback.print_exc() - if _chat_hist: - try: - idx = next(i for i in reversed(range(len(_chat_hist))) if _chat_hist[i] and _chat_hist[i][0] == user_message) - current_bot_response = _chat_hist[idx][1] or "" - _chat_hist[idx] = (user_message, (current_bot_response + "\n\n" if current_bot_response else "") + error_msg) - except (StopIteration, IndexError): - _chat_hist.append((user_message, error_msg)) + # Ensure chat history is updated with error message + if _chat_hist and _chat_hist[-1][0] == user_message: + current_bot_response = _chat_hist[-1][1] or "" + _chat_hist[-1] = (user_message, (current_bot_response + "\n\n" if current_bot_response else "") + error_msg) + else: + _chat_hist.append((user_message, error_msg)) + - _formatted, _detected, _download = _generate_ui_outputs_from_cache(hf_owner_name, hf_repo_name) + # Generate final UI state on error + _formatted, _detected, _download_update = _generate_ui_outputs_from_cache(hf_owner_name, hf_repo_name) yield ( - gr.update(), - _chat_hist, error_msg, - _detected, _formatted, _download, - [], "*Error occurred, changes plan cleared.*", - gr.update(visible=False), gr.update(visible=False), gr.update(visible=False) + gr.update(), # chat_message_input + _chat_hist, # chatbot_display + error_msg, # status_output + gr.update(value=_detected), # detected_files_preview + gr.update(value=_formatted), # formatted_space_output_display + _download_update, # download_button + [], # changeset_state (clear staged changes) + gr.update(value="*Error occurred, changes plan cleared.*"), # changeset_display + gr.update(visible=False), # confirm_accordion + gr.update(visible=False), # confirm_button + gr.update(visible=False) # cancel_button ) def handle_confirm_changes(hf_api_key, owner_name, space_name, changeset): global parsed_code_blocks_state_cache + # Outputs for confirm_button.click (14 outputs) + # [status_output, formatted_space_output_display, detected_files_preview, download_button, + # confirm_accordion, confirm_button, cancel_button, changeset_state, changeset_display, + # owner_name_input, space_name_input, file_browser_dropdown, space_iframe_display, space_runtime_status_display] + _status = "Applying changes..." - yield _status, gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(value="*Applying changes...*") - # Dummy yields for components that might be updated later in the generator - yield gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update() + # Initial yield: Show loading, hide confirm UI, clear changeset display + yield ( + gr.update(value=_status), # status_output + gr.update(), # formatted_space_output_display + gr.update(), # detected_files_preview + gr.update(interactive=False), # download_button + gr.update(visible=False), # confirm_accordion + gr.update(visible=False), # confirm_button + gr.update(visible=False), # cancel_button + [], # changeset_state (clear staged changes immediately) + gr.update(value="*Applying changes...*"), # changeset_display + gr.update(), # owner_name_input + gr.update(), # space_name_input + gr.update(), # file_browser_dropdown + gr.update(), # space_iframe_display + gr.update(value="*Applying changes...*") # space_runtime_status_display + ) if not changeset: - return "No changes to apply.", gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(value="No changes were staged.") + # This case should be prevented by hiding the button, but handle defensively + yield ( + gr.update(value="No changes to apply."), # status_output + gr.update(), gr.update(), gr.update(), # Keep displays as they were + gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), # Hide confirm UI + [], # Clear changeset state + gr.update(value="*No changes were staged.*"), # Clear changeset display + gr.update(), gr.update(), gr.update(), gr.update(), gr.update() # Keep others as they were + ) + return first_action = changeset[0] if changeset else None is_exclusive_duplicate = first_action and first_action.get('type') == 'DUPLICATE_SPACE' is_exclusive_delete = first_action and first_action.get('type') == 'DELETE_SPACE' + final_overall_status = "Changes applied." + reload_error = None if is_exclusive_duplicate: change = first_action @@ -613,8 +671,7 @@ def handle_confirm_changes(hf_api_key, owner_name, space_name, changeset): target_repo_id=change["target_repo_id"], private=change.get("private", False) ) - _status_reload = f"{status_message} | Attempting to load the new Space [{change['target_repo_id']}]..." - yield gr.update(value=_status_reload) + final_overall_status = status_message target_repo_id = change['target_repo_id'] new_owner, new_space_name = target_repo_id.split('/', 1) if '/' in target_repo_id else (None, target_repo_id) @@ -630,12 +687,24 @@ def handle_confirm_changes(hf_api_key, owner_name, space_name, changeset): print(f"Error auto-detecting owner from token for new space: {e}") new_owner = None + # Yield status update before reloading + yield ( + gr.update(value=f"{final_overall_status} | Attempting to load the new Space [{target_repo_id}]..."), # status_output + gr.update(), gr.update(), gr.update(), # Keep displays as they are + gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), # Hide confirm UI + [], # Clear changeset state + gr.update(value="*Reloading new space state...*"), # Clear changeset display + gr.update(value=new_owner), gr.update(value=new_space_name), # Update owner/space fields + gr.update(), gr.update(), gr.update() # Keep others as they are (will be updated after reload) + ) if new_owner and new_space_name: sdk, file_list, err_list = get_space_repository_info(hf_api_key, new_space_name, new_owner) if err_list: reload_error = f"Error reloading file list after duplication: {err_list}" parsed_code_blocks_state_cache = [] + file_browser_update = gr.update(visible=False, choices=[], value=None) + iframe_update = gr.update(value=None, visible=False) else: loaded_files = [] for file_path in file_list: @@ -646,51 +715,54 @@ def handle_confirm_changes(hf_api_key, owner_name, space_name, changeset): loaded_files.append({"filename": file_path, "code": code, "language": lang, "is_binary": is_binary, "is_structure_block": False}) parsed_code_blocks_state_cache = loaded_files - _formatted, _detected, _download = _generate_ui_outputs_from_cache(new_owner, new_space_name) - 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.") - - owner_update = gr.update(value=new_owner) - space_update = gr.update(value=new_space_name) - 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) + 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) - if new_owner and new_space_name: - sub_owner = re.sub(r'[^a-z0-9\-]+', '-', new_owner.lower()).strip('-') or 'owner' - sub_repo = re.sub(r'[^a-z0-9\-]+', '-', new_space_name.lower()).strip('-') or 'space' - iframe_url = f"https://{sub_owner}-{sub_repo}{'.static.hf.space' if sdk == 'static' else '.hf.space'}" - iframe_update = gr.update(value=f'', visible=True) - else: - iframe_update = gr.update(value=None, visible=False) + if new_owner and new_space_name: + sub_owner = re.sub(r'[^a-z0-9\-]+', '-', new_owner.lower()).strip('-') or 'owner' + sub_repo = re.sub(r'[^a-z0-9\-]+', '-', new_space_name.lower()).strip('-') or 'space' + iframe_url = f"https://{sub_owner}-{sub_repo}{'.static.hf.space' if sdk == 'static' else '.hf.space'}" + iframe_update = gr.update(value=f'', visible=True) + else: + iframe_update = gr.update(value=None, visible=False) runtime_status_md = handle_refresh_space_status(hf_api_key, new_owner, new_space_name) + runtime_status_update = gr.update(value=runtime_status_md) - yield ( - gr.update(value=final_overall_status), gr.update(value=_formatted), gr.update(value=_detected), _download, - gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), - [], gr.update(value="*No changes proposed.*"), - owner_update, space_update, file_browser_update, iframe_update, - gr.update(value=runtime_status_md) - ) else: reload_error = "Cannot load new Space state: Owner or Space Name missing after duplication." - _formatted, _detected, _download = _generate_ui_outputs_from_cache(None, None) - final_overall_status = status_message + f" | Reload Status: {reload_error}" - yield ( - gr.update(value=final_overall_status), gr.update(value=_formatted), gr.update(value=_detected), _download, - gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), - [], gr.update(value="*No changes proposed.*"), - gr.update(), gr.update(), gr.update(visible=False, choices=[], value=None), gr.update(value=None, visible=False), - gr.update(value="*Runtime status unavailable for new space.*") - ) + parsed_code_blocks_state_cache = [] # Clear cache if reload fails + file_browser_update = gr.update(visible=False, choices=[], value=None) + iframe_update = gr.update(value=None, visible=False) + runtime_status_update = gr.update(value="*Runtime status unavailable after duplication reload failure.*") + + + _formatted, _detected, _download = _generate_ui_outputs_from_cache(new_owner, new_space_name) + final_overall_status_with_reload = final_overall_status + (f" | Reload Status: {reload_error}" if reload_error else f" | Reload Status: New Space [{new_owner}/{new_space_name}] state refreshed.") + + yield ( + gr.update(value=final_overall_status_with_reload), # status_output + gr.update(value=_formatted), # formatted_space_output_display + gr.update(value=_detected), # detected_files_preview + _download, # download_button + gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), # Hide confirm UI + [], # changeset_state + gr.update(value="*No changes proposed.*"), # changeset_display + gr.update(value=new_owner), gr.update(value=new_space_name), # Update owner/space fields + file_browser_update, iframe_update, runtime_status_update # Update file browser, iframe, status + ) + else: final_overall_status = "Duplicate Action Error: Invalid duplicate changeset format." - _formatted, _detected, _download = _generate_ui_outputs_from_cache(owner_name, space_name) + _formatted, _detected, _download = _generate_ui_outputs_from_cache(owner_name, space_name) # Stay on original space state + runtime_status_md = handle_refresh_space_status(hf_api_key, owner_name, space_name) yield ( - gr.update(value=final_overall_status), gr.update(value=_formatted), gr.update(value=_detected), _download, - gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), - [], gr.update(value="*No changes proposed.*"), - gr.update(), gr.update(), gr.update(), gr.update(), gr.update() + gr.update(value=final_overall_status), # status_output + gr.update(value=_formatted), gr.update(value=_detected), _download, # Keep current space display + gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), # Hide confirm UI + [], gr.update(value="*No changes proposed.*"), # Clear changeset + gr.update(), gr.update(), gr.update(), gr.update(), gr.update(value=runtime_status_md) # Keep other fields ) elif is_exclusive_delete: @@ -703,18 +775,26 @@ def handle_confirm_changes(hf_api_key, owner_name, space_name, changeset): if not delete_repo_id_target or delete_repo_id_target != current_repo_id: final_overall_status = f"DELETE_SPACE Error: Action blocked. Cannot delete '{delete_repo_id_target}'. Only deletion of the currently loaded space ('{current_repo_id}') is permitted via AI action." print(f"Blocked DELETE_SPACE action via confirm: requested '{delete_repo_id_target}', current '{current_repo_id}'.") + # Stay on the current space state if deletion is blocked + _formatted, _detected, _download = _generate_ui_outputs_from_cache(owner_name, space_name) + runtime_status_update = handle_refresh_space_status(hf_api_key, owner_name, space_name) + owner_update = gr.update() + space_update = gr.update() + file_browser_update = gr.update() + iframe_update = gr.update() else: status_message = build_logic_delete_space(hf_api_key, delete_owner, delete_space) final_overall_status = status_message if "Successfully" in status_message: - parsed_code_blocks_state_cache = [] - _formatted, _detected, _download = _generate_ui_outputs_from_cache(None, None) + parsed_code_blocks_state_cache = [] # Clear cache on successful delete + _formatted, _detected, _download = _generate_ui_outputs_from_cache(None, None) # Clear UI displays owner_update = gr.update(value="") space_update = gr.update(value="") file_browser_update = gr.update(visible=False, choices=[], value=None) iframe_update = gr.update(value=None, visible=False) runtime_status_update = gr.update(value="*Space deleted, runtime status unavailable.*") else: + # Stay on the current space state if deletion fails _formatted, _detected, _download = _generate_ui_outputs_from_cache(owner_name, space_name) owner_update = gr.update() space_update = gr.update() @@ -723,77 +803,101 @@ def handle_confirm_changes(hf_api_key, owner_name, space_name, changeset): runtime_status_update = handle_refresh_space_status(hf_api_key, owner_name, space_name) - cleared_changeset = [] yield ( - gr.update(value=final_overall_status), gr.update(value=_formatted), gr.update(value=_detected), _download, - gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), - cleared_changeset, gr.update(value="*No changes proposed.*"), - owner_update, space_update, file_browser_update, iframe_update, runtime_status_update + gr.update(value=final_overall_status), # status_output + gr.update(value=_formatted), gr.update(value=_detected), _download, # Update displays based on success/failure + gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), # Hide confirm UI + [], gr.update(value="*No changes proposed.*"), # Clear changeset + owner_update, space_update, file_browser_update, iframe_update, runtime_status_update # Update fields based on success/failure ) - else: + else: # Non-exclusive actions (combination of space actions and file changes) action_status_messages = [] file_change_operations = [] + errors_occurred = False for change in changeset: try: + # Execute space actions first if change['type'] == 'CREATE_SPACE': repo_id_to_create = change.get('repo_id') if repo_id_to_create: - msg = build_logic_create_space(hf_api_key, repo_id_to_create.split('/')[1], repo_id_to_create.split('/')[0] if '/' in repo_id_to_create else None, change.get('sdk', 'gradio'), "", change.get('private', False)) # Note: Manual create requires markdown, passing empty string + # Note: manual create space function takes markdown. AI generated changes don't provide markdown directly for create space action. + # We'll rely on subsequent file operations in the changeset for content. + # For now, just create the empty repo or ensure it exists. + msg = build_logic_create_space(hf_api_key, repo_id_to_create.split('/')[1], repo_id_to_create.split('/')[0] if '/' in repo_id_to_create else None, change.get('sdk', 'gradio'), "", change.get('private', False)) action_status_messages.append(f"CREATE_SPACE: {msg}") - # If creation was successful, potentially update the UI fields? No, AI might create a *different* space. - # Just report success/failure. The AI can propose loading it next. else: - action_status_messages.append("CREATE_SPACE Error: Target repo_id not specified.") + msg = "CREATE_SPACE Error: Target repo_id not specified." + action_status_messages.append(msg) + errors_occurred = True + elif change['type'] == 'SET_PRIVACY': repo_id_to_set_privacy = change.get('repo_id') or f"{owner_name}/{space_name}" - if repo_id_to_set_privacy and owner_name and space_name: + if repo_id_to_set_privacy and owner_name and space_name: # Only allow setting privacy on the currently loaded space via AI action msg = build_logic_set_space_privacy(hf_api_key, repo_id_to_set_privacy, change['private']) action_status_messages.append(f"SET_PRIVACY: {msg}") else: - action_status_messages.append("SET_PRIVACY Error: Cannot set privacy, no Space currently loaded.") + msg = "SET_PRIVACY Error: Cannot set privacy, no Space currently loaded." + action_status_messages.append(msg) + errors_occurred = True # Treat failure to apply action as error + elif change['type'] == 'CREATE_PR': source_repo = f"{owner_name}/{space_name}" if owner_name and space_name else None if source_repo and change.get('target_repo_id') and change.get('title'): msg = build_logic_create_pull_request(hf_api_key, source_repo, change['target_repo_id'], change['title'], change.get('body', '')) action_status_messages.append(f"CREATE_PR: {msg}") else: - action_status_messages.append("CREATE_PR Error: Source/Target repo, title, or body missing or no Space loaded.") + msg = "CREATE_PR Error: Source/Target repo, title, or body missing or no Space loaded." + action_status_messages.append(msg) + errors_occurred = True + elif change['type'] == 'ADD_COMMENT': repo_id_to_comment = change.get('repo_id') or f"{owner_name}/{space_name}" - if repo_id_to_comment and owner_name and space_name and change.get('comment'): + if repo_id_to_comment and owner_name and space_name and change.get('comment'): # Only allow adding comment on currently loaded space msg = build_logic_add_comment(hf_api_key, repo_id_to_comment, change['comment']) action_status_messages.append(f"ADD_COMMENT: {msg}") else: - action_status_messages.append("ADD_COMMENT Error: Cannot add comment, no Space loaded or comment text missing.") + msg = "ADD_COMMENT Error: Cannot add comment, no Space loaded or comment text missing." + action_status_messages.append(msg) + errors_occurred = True + elif change['type'] == 'LIKE_SPACE': repo_id_to_like = change.get('repo_id') or f"{owner_name}/{space_name}" - if repo_id_to_like and owner_name and space_name: + if repo_id_to_like and owner_name and space_name: # Only allow liking currently loaded space msg = build_logic_like_space(hf_api_key, repo_id_to_like) action_status_messages.append(f"LIKE_SPACE: {msg}") else: - action_status_messages.append("LIKE_SPACE Error: Cannot like space, no Space currently loaded.") + msg = "LIKE_SPACE Error: Cannot like space, no Space currently loaded." + action_status_messages.append(msg) + errors_occurred = True + elif change['type'] == 'Error': - action_status_messages.append(f"Staging Error: {change.get('message', 'Unknown error')}") + msg = f"Staging Error: {change.get('message', 'Unknown error')}" + action_status_messages.append(msg) + errors_occurred = True # Treat staging error as execution error elif change['type'] in ['CREATE_FILE', 'UPDATE_FILE', 'DELETE_FILE']: file_change_operations.append(change) except Exception as e: - action_status_messages.append(f"Error processing action {change.get('type', 'Unknown')}: {e}") + msg = f"Error processing action {change.get('type', 'Unknown')}: {e}" + action_status_messages.append(msg) print(f"Error processing action {change.get('type', 'Unknown')}: {e}") import traceback traceback.print_exc() + errors_occurred = True + file_commit_status = "" if file_change_operations: if owner_name and space_name: file_commit_status = apply_staged_file_changes(hf_api_key, owner_name, space_name, file_change_operations) + if "Error" in file_commit_status: errors_occurred = True else: file_commit_status = "File Commit Error: Cannot commit file changes, no Space currently loaded." - action_status_messages.append(file_commit_status) + errors_occurred = True else: file_commit_status = "No file changes (create/update/delete) to commit." @@ -805,10 +909,19 @@ def handle_confirm_changes(hf_api_key, owner_name, space_name, changeset): if not final_overall_status: final_overall_status = "No operations were applied." _status_reload = f"{final_overall_status} | Reloading Space state..." - yield gr.update(value=_status_reload) + # Yield status update before reloading + yield ( + gr.update(value=_status_reload), # status_output + gr.update(), gr.update(), gr.update(), # Keep displays as they are + gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), # Hide confirm UI + [], # Clear changeset state + gr.update(value="*Reloading space state...*"), # Clear changeset display + gr.update(), gr.update(), gr.update(), gr.update(), gr.update() # Keep others as they are (will be updated after reload) + ) + + # Reload Space state regardless of errors, to show current state refreshed_file_list = [] - reload_error = None repo_id_for_reload = f"{owner_name}/{space_name}" if owner_name and space_name else None if repo_id_for_reload: @@ -816,6 +929,8 @@ def handle_confirm_changes(hf_api_key, owner_name, space_name, changeset): if err_list: reload_error = f"Error reloading file list after changes: {err_list}" parsed_code_blocks_state_cache = [] + file_browser_update = gr.update(visible=False, choices=[], value=None) + iframe_update = gr.update(value=None, visible=False) else: refreshed_file_list = file_list loaded_files = [] @@ -827,20 +942,23 @@ def handle_confirm_changes(hf_api_key, owner_name, space_name, changeset): loaded_files.append({"filename": file_path, "code": code, "language": lang, "is_binary": is_binary, "is_structure_block": False}) parsed_code_blocks_state_cache = loaded_files - file_browser_update = gr.update(visible=True, choices=sorted(refreshed_file_list or []), value=None) - if owner_name and space_name: - sub_owner = re.sub(r'[^a-z0-9\-]+', '-', owner_name.lower()).strip('-') or 'owner' - sub_repo = re.sub(r'[^a-z0-9\-]+', '-', space_name.lower()).strip('-') or 'space' - iframe_url = f"https://{sub_owner}-{sub_repo}{'.static.hf.space' if sdk == 'static' else '.hf.space'}" - iframe_update = gr.update(value=f'', visible=True) - else: - iframe_update = gr.update(value=None, visible=False) + file_browser_update = gr.update(visible=True, choices=sorted(refreshed_file_list or []), value=None) - runtime_status_update = handle_refresh_space_status(hf_api_key, owner_name, space_name) + if owner_name and space_name: + sub_owner = re.sub(r'[^a-z0-9\-]+', '-', owner_name.lower()).strip('-') or 'owner' + sub_repo = re.sub(r'[^a-z0-9\-]+', '-', space_name.lower()).strip('-') or 'space' + iframe_url = f"https://{sub_owner}-{sub_repo}{'.static.hf.space' if sdk == 'static' else '.hf.space'}" + iframe_update = gr.update(value=f'', visible=True) + else: + iframe_update = gr.update(value=None, visible=False) + + runtime_status_md = handle_refresh_space_status(hf_api_key, owner_name, space_name) + runtime_status_update = gr.update(value=runtime_status_md) else: reload_error = "Cannot reload Space state: Owner or Space Name missing." + parsed_code_blocks_state_cache = [] # Clear cache if reload fails file_browser_update = gr.update(visible=False, choices=[], value=None) iframe_update = gr.update(value=None, visible=False) runtime_status_update = gr.update(value="*Runtime status unavailable after reload failure.*") @@ -849,27 +967,30 @@ def handle_confirm_changes(hf_api_key, owner_name, space_name, changeset): _formatted, _detected, _download = _generate_ui_outputs_from_cache(owner_name, space_name) final_overall_status_with_reload = final_overall_status + (f" | Reload Status: {reload_error}" if reload_error else " | Reload Status: Space state refreshed.") - - cleared_changeset = [] + if errors_occurred: final_overall_status_with_reload = "Errors occurred during application: " + final_overall_status_with_reload yield ( - gr.update(value=final_overall_status_with_reload), - gr.update(value=_formatted), gr.update(value=_detected), _download, - gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), - cleared_changeset, gr.update(value="*No changes proposed.*"), - gr.update(), gr.update(), file_browser_update, iframe_update, runtime_status_update + gr.update(value=final_overall_status_with_reload), # status_output + gr.update(value=_formatted), # formatted_space_output_display + gr.update(value=_detected), # detected_files_preview + _download, # download_button + gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), # Hide confirm UI + [], gr.update(value="*No changes proposed.*"), # Clear changeset + gr.update(), gr.update(), file_browser_update, iframe_update, runtime_status_update # Update fields ) def handle_cancel_changes(): global parsed_code_blocks_state_cache + # Outputs for cancel_button.click (6 outputs) + # [status_output, changeset_state, changeset_display, confirm_accordion, confirm_button, cancel_button] return ( - "Changes cancelled.", - [], - gr.update(value="*No changes proposed.*"), - gr.update(visible=False), - gr.update(visible=False), - gr.update(visible=False) + gr.update(value="Changes cancelled."), # status_output + [], # changeset_state + gr.update(value="*No changes proposed.*"), # changeset_display + gr.update(visible=False), # confirm_accordion + gr.update(visible=False), # confirm_button + gr.update(visible=False) # cancel_button ) @@ -881,6 +1002,9 @@ def update_models_dropdown(provider_select): return gr.update(choices=models, value=selected_value) def handle_detect_user(hf_api_key_ui, owner_name_ui): + # Outputs for detect_user_button.click (3 outputs) + # [status_output, owner_name_input, list_spaces_display] + _status = "Detecting user from token..." # Yield initial state to show loading status and clear potentially outdated owner/spaces list yield gr.update(value=_status), gr.update(value=""), gr.update(value="*Listing spaces...*") @@ -899,7 +1023,7 @@ def handle_detect_user(hf_api_key_ui, owner_name_ui): owner_update = gr.update(value=owner_name) # Immediately trigger listing spaces using the detected owner - list_spaces_md, list_err = build_logic_list_user_spaces(hf_api_key=token, owner=owner_name) + list_spaces_md, list_err = build_logic_list_user_spaces(hf_api_key=token, owner=owner_name) # Pass token directly if list_err: list_spaces_update = gr.update(value=f"List Spaces Error: {list_err}") _status += f" | List Spaces Error: {list_err}" @@ -918,6 +1042,12 @@ def handle_detect_user(hf_api_key_ui, owner_name_ui): def handle_load_existing_space(hf_api_key_ui, ui_owner_name, ui_space_name): global parsed_code_blocks_state_cache + # Outputs for load_space_button.click (20 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, changeset_state, changeset_display, confirm_accordion, + # confirm_button, cancel_button, list_spaces_display, target_owner_input, target_space_name_input, target_private_checkbox] + _formatted_md_val, _detected_preview_val, _status_val = "*Loading files...*", "*Loading files...*", f"Loading Space: {ui_owner_name}/{ui_space_name}..." _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) _build_status_clear, _edit_status_clear, _runtime_status_clear = "*Manual build status...*", "*Select a file...*", "*Runtime/Repo status will appear here.*" @@ -927,52 +1057,92 @@ def handle_load_existing_space(hf_api_key_ui, ui_owner_name, ui_space_name): _list_spaces_display_clear = gr.update() # Don't clear list spaces display on load, it's controlled by its own button _target_owner_clear, _target_space_clear, _target_private_clear = gr.update(value=""), gr.update(value=""), gr.update(value=False) - + # Initial yield to show loading state yield ( - gr.update(value=_formatted_md_val), gr.update(value=_detected_preview_val), gr.update(value=_status_val), _file_browser_update, - gr.update(value=ui_owner_name), gr.update(value=ui_space_name), - _iframe_html_update, _download_btn_update, gr.update(value=_build_status_clear), - gr.update(value=_edit_status_clear), gr.update(value=_runtime_status_clear), - _changeset_clear, gr.update(value=_changeset_summary_clear), _confirm_ui_hidden, _confirm_ui_hidden, _confirm_ui_hidden, - _list_spaces_display_clear, _target_owner_clear, _target_space_clear, _target_private_clear + gr.update(value=_formatted_md_val), # formatted_space_output_display + gr.update(value=_detected_preview_val), # detected_files_preview + gr.update(value=_status_val), # status_output + _file_browser_update, # file_browser_dropdown + gr.update(value=ui_owner_name), # owner_name_input (show requested owner) + gr.update(value=ui_space_name), # space_name_input (show requested space) + _iframe_html_update, # space_iframe_display + _download_btn_update, # download_button + gr.update(value=_build_status_clear), # build_status_display + gr.update(value=_edit_status_clear), # edit_status_display + gr.update(value=_runtime_status_clear), # space_runtime_status_display + _changeset_clear, # changeset_state (clear changes from previous space) + gr.update(value=_changeset_summary_clear), # changeset_display (clear changes from previous space) + _confirm_ui_hidden, _confirm_ui_hidden, _confirm_ui_hidden, # Hide confirm UI + _list_spaces_display_clear, # list_spaces_display + _target_owner_clear, # target_owner_input (clear target fields) + _target_space_clear, # target_space_name_input (clear target fields) + _target_private_clear # target_private_checkbox (clear target fields) ) owner_to_use = ui_owner_name token, token_err = build_logic_get_api_token(hf_api_key_ui) if token_err: _status_val = f"Load Error: {token_err}" - yield gr.update(value=_status_val), + yield ( # Yield 20 elements + gr.update(), gr.update(), gr.update(value=_status_val), gr.update(), + gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), + gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), + gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), + gr.update() + ) return if not owner_to_use: try: user_info = build_logic_whoami(token=token) owner_to_use = user_info.get('name') if not owner_to_use: raise Exception("Could not find user name from token.") - yield gr.update(value=owner_to_use), gr.update(value=f"Loading Space: {owner_to_use}/{ui_space_name} (Auto-detected owner)...") + # Yield update for auto-detected owner + yield ( # Yield 20 elements + gr.update(), gr.update(), gr.update(value=f"Loading Space: {owner_to_use}/{ui_space_name} (Auto-detected owner)..."), gr.update(), + gr.update(value=owner_to_use), gr.update(), gr.update(), gr.update(), gr.update(), + gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), + gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), + gr.update() + ) except Exception as e: _status_val = f"Load Error: Error auto-detecting owner: {e}" - yield gr.update(value=_status_val), + yield ( # Yield 20 elements + gr.update(), gr.update(), gr.update(value=_status_val), gr.update(), + gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), + gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), + gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), + gr.update() + ) return if not owner_to_use or not ui_space_name: _status_val = "Load Error: Owner and Space Name are required." - yield gr.update(value=_status_val), + yield ( # Yield 20 elements + gr.update(), gr.update(), gr.update(value=_status_val), gr.update(), + gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), + gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), + gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), + gr.update() + ) return sdk, file_list, err = get_space_repository_info(hf_api_key_ui, ui_space_name, owner_to_use) - yield gr.update(value=owner_to_use), gr.update(value=ui_space_name), - if err: _status_val = f"Load Error: {err}" parsed_code_blocks_state_cache = [] _formatted, _detected, _download = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name) - yield ( - gr.update(value=_formatted), gr.update(value=_detected), gr.update(value=_status_val), - gr.update(visible=False, choices=[], value=None), - gr.update(), gr.update(), - gr.update(value=None, visible=False), - _download, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update() + yield ( # Yield 20 elements + gr.update(value=_formatted), # formatted_space_output_display + gr.update(value=_detected), # detected_files_preview + gr.update(value=_status_val), # status_output + gr.update(visible=False, choices=[], value=None), # file_browser_dropdown + gr.update(value=owner_to_use), gr.update(value=ui_space_name), # Keep owner/space fields + gr.update(value=None, visible=False), # space_iframe_display + _download, # download_button + gr.update(), gr.update(), gr.update(value="*Runtime/Repo status unavailable after load error.*"), # Keep other status fields, clear runtime status + [], gr.update(value="*No changes proposed.*"), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), # Clear changeset and hide confirm UI + gr.update(), gr.update(), gr.update(), gr.update() # Keep other fields ) return @@ -991,105 +1161,159 @@ def handle_load_existing_space(hf_api_key_ui, ui_owner_name, ui_space_name): file_browser_update = gr.update(visible=True, choices=sorted(file_list or []), value=None) + iframe_update = gr.update(value=None, visible=False) # Default to hidden if owner_to_use and ui_space_name: sub_owner = re.sub(r'[^a-z0-9\-]+', '-', owner_to_use.lower()).strip('-') or 'owner' sub_repo = re.sub(r'[^a-z0-9\-]+', '-', ui_space_name.lower()).strip('-') or 'space' iframe_url = f"https://{sub_owner}-{sub_repo}{'.static.hf.space' if sdk == 'static' else '.hf.space'}" iframe_update = gr.update(value=f'', visible=True) - else: - iframe_update = gr.update(value=None, visible=False) - runtime_status_md = handle_refresh_space_status(hf_api_key_ui, owner_to_use, ui_space_name) - yield ( - gr.update(value=_formatted), gr.update(value=_detected), gr.update(value=_status_val), - file_browser_update, - gr.update(), gr.update(), - iframe_update, - _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(), gr.update() + runtime_status_md = handle_refresh_space_status(hf_api_key_ui, owner_to_use, ui_space_name) + runtime_status_update = gr.update(value=runtime_status_md) + + + yield ( # Final yield with 20 elements + gr.update(value=_formatted), # formatted_space_output_display + gr.update(value=_detected), # detected_files_preview + gr.update(value=_status_val), # status_output + file_browser_update, # file_browser_dropdown + gr.update(value=owner_to_use), gr.update(value=ui_space_name), # Ensure fields are set in case of auto-detection + iframe_update, # space_iframe_display + _download, # download_button + gr.update(), gr.update(), runtime_status_update, # Keep other status fields + [], gr.update(value="*No changes proposed.*"), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), # Clear changeset and hide confirm UI + gr.update(), gr.update(value=""), gr.update(value=""), gr.update(value=False) # Reset target fields and list spaces display (no update) ) 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): global parsed_code_blocks_state_cache + # Outputs for build_space_button.click (14 outputs) + # [build_status_display, space_iframe_display, file_browser_dropdown, owner_name_input, space_name_input, + # changeset_state, changeset_display, confirm_accordion, confirm_button, cancel_button, + # formatted_space_output_display, detected_files_preview, download_button, space_runtime_status_display] + _build_status = "Starting manual space build process..." _iframe_html, _file_browser_update = gr.update(value=None, visible=False), gr.update(visible=False, choices=[], value=None) _changeset_clear = [] _changeset_summary_clear = "*Manual build initiated, changes plan cleared.*" _confirm_ui_hidden = gr.update(visible=False) + _formatted_md = formatted_markdown_content # Keep the source markdown + _detected_preview = "*Loading files after build...*" # Placeholder during build - yield (gr.update(value=_build_status), _iframe_html, _file_browser_update, gr.update(value=ui_owner_name_part), gr.update(value=ui_space_name_part), - _changeset_clear, gr.update(value=_changeset_summary_clear), _confirm_ui_hidden, _confirm_ui_hidden, _confirm_ui_hidden, - gr.update(), gr.update(), gr.update(), gr.update()) + # Initial yield: Show loading state + yield ( + gr.update(value=_build_status), # build_status_display + _iframe_html, # space_iframe_display + _file_browser_update, # file_browser_dropdown + gr.update(value=ui_owner_name_part), # owner_name_input + gr.update(value=ui_space_name_part), # space_name_input + _changeset_clear, # changeset_state (clear changes) + gr.update(value=_changeset_summary_clear), # changeset_display (clear changes) + _confirm_ui_hidden, _confirm_ui_hidden, _confirm_ui_hidden, # Hide confirm UI + gr.update(value=_formatted_md), # formatted_space_output_display (keep markdown) + gr.update(value=_detected_preview), # detected_files_preview (placeholder) + gr.update(interactive=False), # download_button (disable during build) + gr.update(value="*Building space, runtime status updating...*") # space_runtime_status_display + ) if not ui_space_name_part or "/" in ui_space_name_part: - _build_status = f"Build Error: Invalid Space Name '{ui_space_name_part}'." - yield gr.update(value=_build_status), + _build_status = f"Build Error: Invalid Space Name '{ui_space_name_part}'. Should not contain '/'." + yield ( # Yield 14 elements + gr.update(value=_build_status), gr.update(), gr.update(), gr.update(), gr.update(), + gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), + gr.update(), gr.update(), gr.update(), gr.update() + ) return - parsed_content = build_logic_parse_markdown(formatted_markdown_content) - proposed_files_list = parsed_content.get("files", []) - - manual_changeset = [] - if ui_owner_name_part and ui_space_name_part: - manual_changeset.append({"type": "CREATE_SPACE", "repo_id": f"{ui_owner_name_part}/{ui_space_name_part}", "sdk": space_sdk_ui, "private": is_private_ui}) - for file_info in proposed_files_list: - manual_changeset.append({"type": "CREATE_FILE", "path": file_info["path"], "content": file_info["content"], "lang": _infer_lang_from_filename(file_info["path"])}) - - if not manual_changeset: - _build_status = "Build Error: No target space specified or no files parsed from markdown." - yield gr.update(value=_build_status), - return + owner_to_use = ui_owner_name_part + space_to_use = ui_space_name_part + repo_id_target = f"{owner_to_use}/{space_to_use}" if owner_to_use else space_to_use # Handle case where owner is not specified token, token_err = build_logic_get_api_token(hf_api_key_ui) if token_err: _build_status = f"Build Error: API Token Error: {token_err}" - yield gr.update(value=_build_status), + yield ( # Yield 14 elements + gr.update(value=_build_status), gr.update(), gr.update(), gr.update(), gr.update(), + gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), + gr.update(), gr.update(), gr.update(), gr.update() + ) return - api = HfApi(token=token) - repo_id_target = f"{ui_owner_name_part}/{ui_space_name_part}" + # Auto-detect owner if not specified + if not owner_to_use: + try: + user_info = build_logic_whoami(token=token) + owner_to_use = user_info.get('name') + repo_id_target = f"{owner_to_use}/{space_to_use}" + if not owner_to_use: raise Exception("Could not find user name from token.") + # Yield update for auto-detected owner + yield ( # Yield 14 elements + gr.update(), gr.update(), gr.update(), gr.update(value=owner_to_use), gr.update(), + gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), + gr.update(), gr.update(), gr.update(), gr.update() + ) + except Exception as e: + _build_status = f"Build Error: Error auto-detecting owner: {e}" + yield ( # Yield 14 elements + gr.update(value=_build_status), gr.update(), gr.update(), gr.update(), gr.update(), + gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), + gr.update(), gr.update(), gr.update(), gr.update() + ) + return + + api = HfApi(token=token) build_status_messages = [] + errors_occurred = False + + # Step 1: Create the space or ensure it exists try: - api.create_repo(repo_id=repo_id_target, repo_type="space", space_sdk=space_sdk_select, private=is_private_ui, exist_ok=True) + api.create_repo(repo_id=repo_id_target, repo_type="space", space_sdk=space_sdk_ui, private=is_private_ui, exist_ok=True) build_status_messages.append(f"CREATE_SPACE: Successfully created or ensured space [{repo_id_target}](https://huggingface.co/spaces/{repo_id_target}) exists.") except HfHubHTTPError as e_http: - build_status_messages.append(f"CREATE_SPACE HTTP Error ({e_http.response.status_code if e_http.response else 'N/A'}): {e_http.response.text if e_http.response else str(e_http)}. Check logs.") + msg = f"CREATE_SPACE HTTP Error ({e_http.response.status_code if e_http.response else 'N/A'}): {e_http.response.text if e_http.response else str(e_http)}. Check logs." + build_status_messages.append(msg) + errors_occurred = True if e_http.response and e_http.response.status_code in (401, 403): - _build_status = f"Build Error: {build_status_messages[-1]}. Cannot proceed with file upload."; yield gr.update(value=_build_status); return + _build_status = f"Build Error: {msg}. Cannot proceed with file upload." + yield ( gr.update(value=_build_status), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update() ); return except Exception as e: - build_status_messages.append(f"CREATE_SPACE Error: {e}") - _build_status = f"Build Error: {build_status_messages[-1]}. Cannot proceed with file upload."; yield gr.update(value=_build_status); return + msg = f"CREATE_SPACE Error: {e}" + build_status_messages.append(msg) + errors_occurred = True + _build_status = f"Build Error: {msg}. Cannot proceed with file upload." + yield ( gr.update(value=_build_status), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update() ); return + + _build_status = " | ".join(build_status_messages) + yield ( gr.update(value=_build_status), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update() ) + # Step 2: Apply file changes parsed from markdown + parsed_content = build_logic_parse_markdown(formatted_markdown_content) + proposed_files_list = parsed_content.get("files", []) file_changes_only_changeset = [{"type": "CREATE_FILE", "path": f["path"], "content": f["content"], "lang": _infer_lang_from_filename(f["path"])} for f in proposed_files_list] + if file_changes_only_changeset: - file_commit_status = apply_staged_file_changes(hf_api_key_ui, ui_owner_name_part, ui_space_name_part, file_changes_only_changeset) + file_commit_status = apply_staged_file_changes(hf_api_key_ui, owner_to_use, space_to_use, file_changes_only_changeset) build_status_messages.append(file_commit_status) + if "Error" in file_commit_status: errors_occurred = True else: build_status_messages.append("No files parsed from markdown to upload/update.") - _build_status = " | ".join(build_status_messages) + yield ( gr.update(value=_build_status), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update() ) - owner_to_use = ui_owner_name_part - space_to_use = ui_space_name_part - _formatted_md = formatted_markdown_content - _detected_preview_val = "*Loading files after build...*" - _download_btn_update = gr.update(interactive=False, value=None) + # Step 3: Reload the space state after building + _build_status_reload = f"{_build_status} | Reloading Space state..." + yield ( gr.update(value=_build_status_reload), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(value="*Reloading after build...*") ) - yield ( - gr.update(value=_build_status), _iframe_html, _file_browser_update, - gr.update(value=owner_to_use), gr.update(value=space_to_use), - _changeset_clear, gr.update(value=_changeset_summary_clear), _confirm_ui_hidden, _confirm_ui_hidden, _confirm_ui_hidden, - gr.update(value=_formatted_md), gr.update(value=_detected_preview_val), _download_btn_update, gr.update() - ) sdk_built, file_list, err_list = get_space_repository_info(hf_api_key_ui, space_to_use, owner_to_use) if err_list: - _build_status += f" | Error reloading file list after build: {err_list}" + reload_error = f"Error reloading file list after build: {err_list}" parsed_code_blocks_state_cache = [] _file_browser_update = gr.update(visible=False, choices=[], value=None) _iframe_html_update = gr.update(value=None, visible=False) @@ -1105,37 +1329,66 @@ def handle_build_space_button(hf_api_key_ui, ui_space_name_part, ui_owner_name_p _file_browser_update = gr.update(visible=True, choices=sorted(file_list or []), value=None) + _iframe_html_update = gr.update(value=None, visible=False) # Default to hidden if owner_to_use and space_to_use: sub_owner = re.sub(r'[^a-z0-9\-]+', '-', owner_to_use.lower()).strip('-') or 'owner' sub_repo = re.sub(r'[^a-z0-9\-]+', '-', space_to_use.lower()).strip('-') or 'space' iframe_url = f"https://{sub_owner}-{sub_repo}{'.static.hf.space' if sdk_built == 'static' else '.hf.space'}" _iframe_html_update = gr.update(value=f'', visible=True) - else: - _iframe_html_update = gr.update(value=None, visible=False) + runtime_status_md = handle_refresh_space_status(hf_api_key_ui, owner_to_use, space_to_use) + runtime_status_update = gr.update(value=runtime_status_md) - yield ( - gr.update(value=_build_status), _iframe_html_update, _file_browser_update, - gr.update(value=owner_to_use), gr.update(value=space_to_use), - _changeset_clear, gr.update(value=_changeset_summary_clear), _confirm_ui_hidden, _confirm_ui_hidden, _confirm_ui_hidden, - gr.update(value=_formatted_md), gr.update(value=_detected_preview), _download, gr.update(value=runtime_status_md) + + _build_status_final = _build_status + (f" | Reload Status: {reload_error}" if reload_error else " | Reload Status: Space state refreshed.") + if errors_occurred: _build_status_final = "Errors occurred during build: " + _build_status_final + + + _formatted, _detected, _download = _generate_ui_outputs_from_cache(owner_to_use, space_to_use) + + yield ( # Final yield with 14 elements + gr.update(value=_build_status_final), # build_status_display + _iframe_html_update, # space_iframe_display + _file_browser_update, # file_browser_dropdown + gr.update(value=owner_to_use), gr.update(value=space_to_use), # Ensure fields are set + [], gr.update(value="*No changes proposed.*"), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), # Clear changeset and hide confirm UI + gr.update(value=_formatted), gr.update(value=_detected), _download, runtime_status_update # Update displays ) def handle_load_file_for_editing(hf_api_key_ui, ui_space_name_part, ui_owner_name_part, selected_file_path): + # Outputs for file_browser_dropdown.change (3 outputs) - Corrected list + # [file_content_editor, edit_status_display, commit_message_input] + if not selected_file_path: - return "", "Select a file.", "", gr.update(language="plaintext") + # Return gr.update() for all outputs to clear/reset them + return ( + gr.update(value="", language="plaintext"), # file_content_editor + gr.update(value="Select a file."), # edit_status_display + gr.update(value="") # commit_message_input + ) content, err = get_space_file_content(hf_api_key_ui, ui_space_name_part, ui_owner_name_part, selected_file_path) if err: - return "", f"Load Error: {err}", "", gr.update(language="plaintext") + return ( + gr.update(value=f"[Error loading content: {err}]", language="plaintext"), # file_content_editor + gr.update(value=f"Load Error: {err}"), # edit_status_display + gr.update(value="") # commit_message_input + ) lang = _infer_lang_from_filename(selected_file_path) commit_msg = f"Update {selected_file_path}" - return content, f"Loaded `{selected_file_path}`", commit_msg, gr.update(language=lang) + return ( + gr.update(value=content or "", language=lang), # file_content_editor (ensure value is not None) + gr.update(value=f"Loaded `{selected_file_path}`"), # edit_status_display + gr.update(value=commit_msg) # commit_message_input + ) 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): + # Outputs for update_file_button.click (5 outputs) + # [edit_status_display, file_browser_dropdown, formatted_space_output_display, detected_files_preview, download_button] + if not ui_owner_name_part or not ui_space_name_part: return "Commit Error: Cannot commit changes. Please load a space first.", gr.update(), gr.update(), gr.update(), gr.update() if not file_to_edit_path: @@ -1155,6 +1408,7 @@ def handle_commit_file_changes(hf_api_key_ui, ui_space_name_part, ui_owner_name_ found = True break if not found: + # If the file was newly created via manual edit (not AI) parsed_code_blocks_state_cache.append({ "filename": file_to_edit_path, "code": edited_content, @@ -1164,6 +1418,7 @@ def handle_commit_file_changes(hf_api_key_ui, ui_space_name_part, ui_owner_name_ }) parsed_code_blocks_state_cache.sort(key=lambda b: (0, b["filename"]) if b.get("is_structure_block") else (1, b["filename"])) + # Refresh the file browser choices to include the new/updated file if necessary file_list, _ = list_space_files_for_browsing(hf_api_key_ui, ui_space_name_part, ui_owner_name_part) file_browser_update = gr.update(choices=sorted(file_list or [])) @@ -1171,10 +1426,14 @@ def handle_commit_file_changes(hf_api_key_ui, ui_space_name_part, ui_owner_name_ return status_msg, file_browser_update, gr.update(value=_formatted), gr.update(value=_detected), _download def handle_delete_file(hf_api_key_ui, ui_space_name_part, ui_owner_name_part, file_to_delete_path): + # Outputs for delete_file_button.click (7 outputs) - Corrected list + # [edit_status_display, file_browser_dropdown, file_content_editor, commit_message_input, + # formatted_space_output_display, detected_files_preview, download_button] + if not ui_owner_name_part or not ui_space_name_part: - return "Delete Error: Cannot delete file. Please load a space first.", gr.update(), "", "", "plaintext", gr.update(), gr.update(), gr.update() + return "Delete Error: Cannot delete file. Please load a space first.", gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update() if not file_to_delete_path: - return "Delete Error: No file selected to delete.", gr.update(), "", "", "plaintext", gr.update(), gr.update(), gr.update() + return "Delete Error: No file selected to delete.", gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update() status_msg = build_logic_delete_space_file(hf_api_key_ui, ui_space_name_part, ui_owner_name_part, file_to_delete_path) @@ -1182,28 +1441,29 @@ def handle_delete_file(hf_api_key_ui, ui_space_name_part, ui_owner_name_part, fi file_browser_update = gr.update() file_content_editor_update = gr.update() commit_message_update = gr.update() - editor_lang_update = gr.update() if "Successfully" in status_msg: + # Remove the deleted file from the cache parsed_code_blocks_state_cache = [b for b in parsed_code_blocks_state_cache if b["filename"] != file_to_delete_path] - file_content_editor_update = gr.update(value="") + # Clear editor fields as the file is gone + file_content_editor_update = gr.update(value="", language="plaintext") # Also reset language commit_message_update = gr.update(value="") - editor_lang_update = gr.update(language="plaintext") + # Refresh the file browser choices file_list, _ = list_space_files_for_browsing(hf_api_key_ui, ui_space_name_part, ui_owner_name_part) - file_browser_update = gr.update(choices=sorted(file_list or []), value=None) + file_browser_update = gr.update(choices=sorted(file_list or []), value=None) # Reset selected value _formatted, _detected, _download = _generate_ui_outputs_from_cache(ui_owner_name_part, ui_space_name_part) return ( - status_msg, - file_browser_update, - file_content_editor_update, - commit_message_update, - editor_lang_update, - gr.update(value=_formatted), - gr.update(value=_detected), - _download + status_msg, # edit_status_display + file_browser_update, # file_browser_dropdown + file_content_editor_update, # file_content_editor + commit_message_update, # commit_message_input + gr.update(value=_formatted), # formatted_space_output_display + gr.update(value=_detected), # detected_files_preview + _download # download_button ) + def handle_refresh_space_status(hf_api_key_ui, ui_owner_name, ui_space_name): if not ui_owner_name or not ui_space_name: return "**Status Error:** Owner and Space Name must be provided to get status." @@ -1211,10 +1471,13 @@ def handle_refresh_space_status(hf_api_key_ui, ui_owner_name, ui_space_name): status_details, err = get_space_runtime_status(hf_api_key_ui, ui_space_name, ui_owner_name) repo_info = None repo_info_err = None - if not err: - sdk, file_list, repo_info_err = get_space_repository_info(hf_api_key_ui, ui_space_name, ui_owner_name) - if not repo_info_err: - repo_info = {"sdk": sdk, "file_count": len(file_list) if file_list is not None else 'N/A'} + + # Get repo info regardless of runtime status error + sdk = None + file_list = None + sdk, file_list, repo_info_err = get_space_repository_info(hf_api_key_ui, ui_space_name, ui_owner_name) + if not repo_info_err: + repo_info = {"sdk": sdk, "file_count": len(file_list) if file_list is not None else 'N/A'} md_lines = [] @@ -1234,6 +1497,13 @@ def handle_refresh_space_status(hf_api_key_ui, ui_owner_name, ui_space_name): if error_msg: md_lines.append(f"- **Error:** `{escape_html_for_markdown(error_msg)}`\n") log_link = status_details.get('full_log_link') if log_link and log_link != "#": md_lines.append(f"- [View Full Logs]({log_link})\n") + # Add build logs link if available (common pattern in Space status) + if owner_name and ui_space_name: + sub_owner = re.sub(r'[^a-z0-9\-]+', '-', ui_owner_name.lower()).strip('-') or 'owner' + sub_repo = re.sub(r'[^a-z0-9\-]+', '-', ui_space_name.lower()).strip('-') or 'space' + build_logs_url = f"https://huggingface.co/spaces/{ui_owner_name}/{ui_space_name}/build-logs" + md_lines.append(f"- [View Build Logs]({build_logs_url})\n") + md_lines.append("---") md_lines.append("### Repository Info\n") @@ -1242,6 +1512,10 @@ def handle_refresh_space_status(hf_api_key_ui, ui_owner_name, ui_space_name): elif repo_info: md_lines.append(f"- **SDK:** `{repo_info.get('sdk', 'N/A')}`\n") md_lines.append(f"- **File Count:** `{repo_info.get('file_count', 'N/A')}`\n") + if owner_name and ui_space_name: # Add repo link + repo_url = f"https://huggingface.co/spaces/{ui_owner_name}/{ui_space_name}/tree/main" + md_lines.append(f"- [View Repository Files]({repo_url})\n") + else: md_lines.append("*Could not retrieve repository info.*") @@ -1249,6 +1523,8 @@ def handle_refresh_space_status(hf_api_key_ui, ui_owner_name, ui_space_name): return "\n".join(md_lines) def handle_list_spaces(hf_api_key_ui, ui_owner_name): + # Outputs for list_spaces_button.click (1 output) + # [list_spaces_display] token, token_err = build_logic_get_api_token(hf_api_key_ui) if token_err: return f"**List Spaces Error:** {token_err}" @@ -1265,7 +1541,7 @@ def handle_list_spaces(hf_api_key_ui, ui_owner_name): if not owner_to_list: return "**List Spaces Error:** Owner could not be determined. Please specify it in the Owner field." - spaces_list, err = build_logic_list_user_spaces(hf_api_key=token, owner=owner_to_list) + spaces_list, err = build_logic_list_user_spaces(hf_api_key=token, owner=owner_to_list) # Pass token directly if err: return f"**List Spaces Error:** {err}" @@ -1280,71 +1556,106 @@ def handle_list_spaces(hf_api_key_ui, ui_owner_name): return md def handle_manual_duplicate_space(hf_api_key_ui, source_owner, source_space_name, target_owner, target_space_name, target_private): + # Outputs for duplicate_space_button.click (9 outputs) + # [status_output, owner_name_input, space_name_input, + # formatted_space_output_display, detected_files_preview, download_button, + # file_browser_dropdown, space_iframe_display, space_runtime_status_display] + + global parsed_code_blocks_state_cache + if not source_owner or not source_space_name: return "Duplicate Error: Please load a Space first to duplicate.", gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update() if not target_owner or not target_space_name: return "Duplicate Error: Target Owner and Target Space Name are required.", gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update() if "/" in target_space_name: 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(), gr.update(), gr.update() - global parsed_code_blocks_state_cache - source_repo_id = f"{source_owner}/{source_space_name}" target_repo_id = f"{target_owner}/{target_space_name}" status_msg = f"Attempting to duplicate `{source_repo_id}` to `{target_repo_id}`..." - yield status_msg, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update() + # Initial yield + yield ( + gr.update(value=status_msg), # status_output + gr.update(), gr.update(), # Keep owner/space fields + gr.update(), gr.update(), gr.update(interactive=False), # Keep file displays, disable download + gr.update(visible=False, choices=[], value=None), gr.update(value=None, visible=False), # Clear file browser and iframe + gr.update(value="*Duplicating space...*") # Clear runtime status + ) result_message = build_logic_duplicate_space(hf_api_key_ui, source_repo_id, target_repo_id, target_private) status_msg = f"Duplication Result: {result_message}" + errors_occurred = "Error" in status_msg or "Failed" in status_msg + + # Yield status update before attempting reload _status_reload = f"{status_msg} | Attempting to load the new Space [{target_repo_id}]..." - yield gr.update(value=_status_reload) + yield ( + gr.update(value=_status_reload), # status_output + gr.update(value=target_owner), gr.update(value=target_space_name), # Update fields to new space + gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(value="*Reloading new space...*") # Keep displays, clear runtime status + ) + # Attempt to load the new space state regardless of duplication success, to show current state new_owner = target_owner new_space_name = target_space_name + reload_error = None + sdk = None - sdk, file_list, err_list = get_space_repository_info(hf_api_key_ui, new_space_name, new_owner) + if not errors_occurred: # Only attempt reload if duplication was successful + sdk, file_list, err_list = get_space_repository_info(hf_api_key_ui, new_space_name, new_owner) - if err_list: - reload_error = f"Error reloading file list after duplication: {err_list}" - parsed_code_blocks_state_cache = [] - _file_browser_update = gr.update(visible=False, choices=[], value=None) - _iframe_html_update = gr.update(value=None, visible=False) - else: - loaded_files = [] - for file_path in file_list: - content, err_get = get_space_file_content(hf_api_key_ui, new_space_name, new_owner, file_path) - lang = _infer_lang_from_filename(file_path) - is_binary = lang == "binary" or (err_get is not None) - code = f"[Error loading content: {err_get}]" if err_get else (content or "") - loaded_files.append({"filename": file_path, "code": code, "language": lang, "is_binary": is_binary, "is_structure_block": False}) - parsed_code_blocks_state_cache = loaded_files - - _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) - if new_owner and new_space_name: - sub_owner = re.sub(r'[^a-z0-9\-]+', '-', new_owner.lower()).strip('-') or 'owner' - sub_repo = re.sub(r'[^a-z0-9\-]+', '-', new_space_name.lower()).strip('-') or 'space' - iframe_url = f"https://{sub_owner}-{sub_repo}{'.static.hf.space' if sdk == 'static' else '.hf.space'}" - _iframe_html_update = gr.update(value=f'', visible=True) + if err_list: + reload_error = f"Error reloading file list after duplication: {err_list}" + parsed_code_blocks_state_cache = [] # Clear cache if reload fails + _file_browser_update = gr.update(visible=False, choices=[], value=None) + _iframe_html_update = gr.update(value=None, visible=False) else: - _iframe_html_update = gr.update(value=None, visible=False) - - _formatted, _detected, _download = _generate_ui_outputs_from_cache(new_owner, new_space_name) - 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.") - - runtime_status_md = handle_refresh_space_status(hf_api_key_ui, new_owner, new_space_name) - - owner_update = gr.update(value=new_owner) - space_update = gr.update(value=new_space_name) - - - yield ( - gr.update(value=final_overall_status), - owner_update, space_update, - gr.update(value=_formatted), gr.update(value=_detected), _download, - _file_browser_update, _iframe_html_update, gr.update(value=runtime_status_md) + loaded_files = [] + for file_path in file_list: + content, err_get = get_space_file_content(hf_api_key_ui, new_space_name, new_owner, file_path) + lang = _infer_lang_from_filename(file_path) + is_binary = lang == "binary" or (err_get is not None) + code = f"[Error loading content: {err_get}]" if err_get else (content or "") + loaded_files.append({"filename": file_path, "code": code, "language": lang, "is_binary": is_binary, "is_structure_block": False}) + parsed_code_blocks_state_cache = loaded_files + + _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) + _iframe_html_update = gr.update(value=None, visible=False) # Default to hidden + if new_owner and new_space_name: + sub_owner = re.sub(r'[^a-z0-9\-]+', '-', new_owner.lower()).strip('-') or 'owner' + sub_repo = re.sub(r'[^a-z0-9\-]+', '-', new_space_name.lower()).strip('-') or 'space' + iframe_url = f"https://{sub_owner}-{sub_repo}{'.static.hf.space' if sdk == 'static' else '.hf.space'}" + _iframe_html_update = gr.update(value=f'', visible=True) + + runtime_status_md = handle_refresh_space_status(hf_api_key_ui, new_owner, new_space_name) + runtime_status_update = gr.update(value=runtime_status_md) + + else: # If duplication had errors, don't attempt reload of new space, stay on current + reload_error = "Reload skipped due to duplication errors." + _formatted, _detected, _download = _generate_ui_outputs_from_cache(source_owner, source_space_name) # Regenerate UI based on source space + _file_browser_update = gr.update() # Keep current file browser state + _iframe_html_update = gr.update() # Keep current iframe state + runtime_status_update = handle_refresh_space_status(hf_api_key_ui, source_owner, source_space_name) # Refresh status of source space + new_owner = source_owner # Revert fields to source space + new_space_name = source_space_name + # Need to re-generate _formatted, _detected, _download based on source space if errors occurred + _formatted, _detected, _download = _generate_ui_outputs_from_cache(source_owner, source_space_name) + + + # Generate final UI based on successful/failed reload + if not errors_occurred: + _formatted, _detected, _download = _generate_ui_outputs_from_cache(new_owner, new_space_name) + + final_overall_status_with_reload = status_msg + (f" | Reload Status: {reload_error}" if reload_error else f" | Reload Status: Space state refreshed ({new_owner}/{new_space_name}).") + if errors_occurred: final_overall_status_with_reload = "Errors occurred during duplication: " + final_overall_status_with_reload + + yield ( # Final yield with 9 elements + gr.update(value=final_overall_status_with_reload), # status_output + gr.update(value=new_owner), gr.update(value=new_space_name), # Update owner/space fields + gr.update(value=_formatted), gr.update(value=_detected), _download, # Update file displays + _file_browser_update, _iframe_html_update, runtime_status_update # Update browser, iframe, status ) @@ -1393,7 +1704,7 @@ with gr.Blocks(theme=custom_theme, css=custom_css) as demo: default_provider = 'Groq' if default_provider not in available_providers: default_provider = available_providers[0] if available_providers else None - elif len(available_providers) < 3: + elif len(available_providers) < 3: # Adjust default if less than 3 providers available default_provider = available_providers[0] if available_providers else None initial_models = get_models_for_provider(default_provider) if default_provider else [] @@ -1433,7 +1744,7 @@ with gr.Blocks(theme=custom_theme, css=custom_css) as demo: with gr.Column(scale=2): gr.Markdown("## 💬 AI Assistant Chat") - chatbot_display = gr.Chatbot(label="AI Chat", height=500, bubble_full_width=False, avatar_images=(None)) + chatbot_display = gr.Chatbot(label="AI Chat", height=500, bubble_full_width=False, avatar_images=(None)) # Removed avatar tuple as per doc 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) @@ -1482,87 +1793,77 @@ with gr.Blocks(theme=custom_theme, css=custom_css) as demo: provider_select.change(update_models_dropdown, inputs=provider_select, outputs=model_select) - chat_inputs = [ - chat_message_input, chatbot_display, hf_api_key_input, - provider_api_key_input, provider_select, model_select, system_prompt_input, - owner_name_input, space_name_input - ] - chat_outputs = [ - chat_message_input, chatbot_display, status_output, - detected_files_preview, formatted_space_output_display, download_button, - changeset_state, changeset_display, confirm_accordion, confirm_button, cancel_button - ] + # chat_inputs = [chat_message_input, chatbot_display, hf_api_key_input, + # provider_api_key_input, provider_select, model_select, system_prompt_input, + # owner_name_input, space_name_input] # 9 inputs + # chat_outputs = [chat_message_input, chatbot_display, status_output, + # detected_files_preview, formatted_space_output_display, download_button, + # changeset_state, changeset_display, confirm_accordion, confirm_button, cancel_button] # 11 outputs + send_chat_button.click(handle_chat_submit, inputs=chat_inputs, outputs=chat_outputs) chat_message_input.submit(handle_chat_submit, inputs=chat_inputs, outputs=chat_outputs) - confirm_inputs = [hf_api_key_input, owner_name_input, space_name_input, changeset_state] - confirm_outputs = [ - status_output, formatted_space_output_display, detected_files_preview, download_button, - confirm_accordion, confirm_button, cancel_button, changeset_state, changeset_display, - owner_name_input, space_name_input, file_browser_dropdown, space_iframe_display, space_runtime_status_display - ] + # confirm_inputs = [hf_api_key_input, owner_name_input, space_name_input, changeset_state] # 4 inputs + # confirm_outputs = [status_output, formatted_space_output_display, detected_files_preview, download_button, + # confirm_accordion, confirm_button, cancel_button, changeset_state, changeset_display, + # owner_name_input, space_name_input, file_browser_dropdown, space_iframe_display, space_runtime_status_display] # 14 outputs confirm_button.click(handle_confirm_changes, inputs=confirm_inputs, outputs=confirm_outputs) - cancel_outputs = [ - status_output, changeset_state, changeset_display, - confirm_accordion, confirm_button, cancel_button - ] + # cancel_outputs = [status_output, changeset_state, changeset_display, confirm_accordion, confirm_button, cancel_button] # 6 outputs cancel_button.click(handle_cancel_changes, inputs=None, outputs=cancel_outputs) - 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, - changeset_state, changeset_display, confirm_accordion, confirm_button, cancel_button, - list_spaces_display, target_owner_input, target_space_name_input, target_private_checkbox - ] + # 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, changeset_state, changeset_display, confirm_accordion, + # confirm_button, cancel_button, list_spaces_display, target_owner_input, target_space_name_input, target_private_checkbox] # 20 outputs load_space_button.click( fn=handle_load_existing_space, - inputs=[hf_api_key_input, owner_name_input, space_name_input], + inputs=[hf_api_key_input, owner_name_input, space_name_input], # 3 inputs outputs=load_space_outputs ) - detect_user_outputs = [status_output, owner_name_input, list_spaces_display] - detect_user_button.click(fn=handle_detect_user, inputs=[hf_api_key_input, owner_name_input], outputs=detect_user_outputs) - - build_outputs = [ - build_status_display, space_iframe_display, file_browser_dropdown, - owner_name_input, space_name_input, - changeset_state, changeset_display, confirm_accordion, confirm_button, cancel_button, - formatted_space_output_display, detected_files_preview, download_button, space_runtime_status_display - ] - build_inputs = [ - hf_api_key_input, space_name_input, owner_name_input, space_sdk_select, - space_private_checkbox, formatted_space_output_display - ] + # detect_user_outputs = [status_output, owner_name_input, list_spaces_display] # 3 outputs + detect_user_button.click(fn=handle_detect_user, inputs=[hf_api_key_input, owner_name_input], outputs=detect_user_outputs) # 2 inputs + + # build_outputs = [build_status_display, space_iframe_display, file_browser_dropdown, owner_name_input, space_name_input, + # changeset_state, changeset_display, confirm_accordion, confirm_button, cancel_button, + # formatted_space_output_display, detected_files_preview, download_button, space_runtime_status_display] # 14 outputs + # build_inputs = [hf_api_key_input, space_name_input, owner_name_input, space_sdk_select, + # space_private_checkbox, formatted_space_output_display] # 6 inputs build_space_button.click(fn=handle_build_space_button, inputs=build_inputs, outputs=build_outputs) - file_edit_load_outputs = [file_content_editor, edit_status_display, commit_message_input, file_content_editor] - 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) + # file_edit_load_outputs = [file_content_editor, edit_status_display, commit_message_input] # Corrected: 3 outputs + file_browser_dropdown.change( + fn=handle_load_file_for_editing, + inputs=[hf_api_key_input, space_name_input, owner_name_input, file_browser_dropdown], # 4 inputs + outputs=file_edit_load_outputs + ) - commit_file_outputs = [edit_status_display, file_browser_dropdown, formatted_space_output_display, detected_files_preview, download_button] - 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) + # commit_file_outputs = [edit_status_display, file_browser_dropdown, formatted_space_output_display, detected_files_preview, download_button] # 5 outputs + 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], # 6 inputs + outputs=commit_file_outputs + ) - 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 - ] - 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) + # delete_file_outputs = [edit_status_display, file_browser_dropdown, file_content_editor, commit_message_input, + # formatted_space_output_display, detected_files_preview, download_button] # Corrected: 7 outputs + delete_file_button.click( + fn=handle_delete_file, + inputs=[hf_api_key_input, space_name_input, owner_name_input, file_browser_dropdown], # 4 inputs + outputs=delete_file_outputs + ) - 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]) + 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]) # 3 inputs, 1 output - manual_duplicate_inputs = [hf_api_key_input, owner_name_input, space_name_input, target_owner_input, target_space_name_input, target_private_checkbox] - manual_duplicate_outputs = [ - status_output, owner_name_input, space_name_input, - formatted_space_output_display, detected_files_preview, download_button, - file_browser_dropdown, space_iframe_display, space_runtime_status_display - ] + # manual_duplicate_inputs = [hf_api_key_input, owner_name_input, space_name_input, target_owner_input, target_space_name_input, target_private_checkbox] # 6 inputs + # manual_duplicate_outputs = [status_output, owner_name_input, space_name_input, + # formatted_space_output_display, detected_files_preview, download_button, + # file_browser_dropdown, space_iframe_display, space_runtime_status_display] # 9 outputs duplicate_space_button.click(fn=handle_manual_duplicate_space, inputs=manual_duplicate_inputs, outputs=manual_duplicate_outputs) - list_spaces_button.click(fn=handle_list_spaces, inputs=[hf_api_key_input, owner_name_input], outputs=[list_spaces_display]) + list_spaces_button.click(fn=handle_list_spaces, inputs=[hf_api_key_input, owner_name_input], outputs=[list_spaces_display]) # 2 inputs, 1 output if __name__ == "__main__": - demo.launch(debug=False, mcp_server=True) \ No newline at end of file + demo.launch(debug=False, mcp_server=True) # Set debug=True for more detailed errors \ No newline at end of file