import gradio as gr import os # For os.path.relpath, os.path.isfile, os.path.basename, os.sep from pathlib import Path # For path manipulations if needed from app_logic import ( create_space, # view_space_files, # Optional: can be removed update_space_file, load_token_from_image_and_set_env, KEYLOCK_DECODE_AVAILABLE, get_space_local_clone_path, read_file_from_local_path, ) # Gradio interface def main_ui(): with gr.Blocks(theme=gr.themes.Soft(primary_hue=gr.themes.colors.blue, secondary_hue=gr.themes.colors.sky), title="Hugging Face Space Builder") as demo: gr.Markdown( """ # 🛠️ Hugging Face Space Builder Create, view, and manage Hugging Face Spaces. Provide your Hugging Face API token directly or load it from a KeyLock Wallet image. """ ) # --- Authentication Section (Terminology updated) --- with gr.Accordion("🔑 Authentication Methods", open=True): gr.Markdown( """ **Token Precedence:** 1. If a token is successfully loaded from a KeyLock Wallet image, it will be used. 2. Otherwise, the token entered in the 'Enter API Token Directly' textbox will be used. """ ) gr.Markdown("---") gr.Markdown("### Method 1: Enter API Token Directly") api_token_ui_input = gr.Textbox( label="Hugging Face API Token (hf_xxx)", type="password", placeholder="Enter your HF token OR load from KeyLock Wallet image below", info="Get from hf.co/settings/tokens. Needs 'write' access." ) if KEYLOCK_DECODE_AVAILABLE: gr.Markdown("---") gr.Markdown("### Method 2: Load API Token from KeyLock Wallet Image") with gr.Row(): keylock_image_input = gr.Image( label="KeyLock Wallet Image (PNG containing HF_TOKEN)", type="pil", image_mode="RGBA", # Recommended ) keylock_password_input = gr.Textbox(label="Image Password", type="password") keylock_decode_button = gr.Button("Load Token from Wallet Image", variant="secondary") keylock_status_output = gr.Markdown(label="Wallet Image Decoding Status", value="Status will appear here.") keylock_decode_button.click( fn=load_token_from_image_and_set_env, inputs=[keylock_image_input, keylock_password_input], outputs=[keylock_status_output] ) else: gr.Markdown("_(KeyLock Wallet image decoding is disabled as the library could not be imported.)_") # --- State for File Browser/Editor --- current_clone_root_path_state = gr.State(None) current_editing_file_relative_path_state = gr.State(None) # --- Main Application Tabs --- with gr.Tabs(): with gr.TabItem("🚀 Create New Space"): with gr.Row(): space_name_create_input = gr.Textbox(label="Space Name", placeholder="my-awesome-app (no slashes)", scale=2) owner_create_input = gr.Textbox(label="Owner Username/Org", placeholder="Leave blank for your HF username", scale=1) sdk_create_input = gr.Dropdown(label="Space SDK", choices=["gradio", "streamlit", "docker", "static"], value="gradio") markdown_input_create = gr.Textbox( label="Markdown File Structure & Content", placeholder="""Example: ### File: app.py # ```python print("Hello World!") # ``` ### File: README.md # ```markdown # My App This is a README. # ```""", lines=15, interactive=True, info="Define files using '### File: path/to/your/file.ext'." ) create_btn = gr.Button("Create Space", variant="primary") create_output_md = gr.Markdown(label="Result") # --- Revamped "Browse & Edit Files" Tab --- with gr.TabItem("📂 Browse & Edit Files"): gr.Markdown("Browse the file structure of a Space, view, and edit files.") with gr.Row(): browse_space_name_input = gr.Textbox(label="Space Name", placeholder="my-target-app", scale=2) browse_owner_input = gr.Textbox(label="Owner Username/Org", placeholder="Leave blank if it's your space", scale=1) with gr.Row(): browse_files_button = gr.Button("Load Space Files for Browsing", variant="secondary") force_refresh_clone_checkbox = gr.Checkbox(label="Force Refresh Clone", value=False, info="Re-download the space.") browse_status_output = gr.Markdown(label="Browsing Status", value="Status will appear here.") gr.Markdown("---") gr.Markdown("### File Explorer (Select a file to view/edit)") file_explorer_component = gr.FileExplorer(label="Space File Tree", file_count="single", interactive=True, glob="**/*") # glob for all files gr.Markdown("---") gr.Markdown("### File Editor") # Display for current file being edited current_file_display_ro = gr.Textbox(label="Currently Editing File (Relative Path):", interactive=False, placeholder="No file selected.") file_editor_textbox = gr.Textbox( label="File Content (Editable)", lines=20, interactive=True, placeholder="Select a file from the explorer above to view/edit its content." ) edit_commit_message_input = gr.Textbox(label="Commit Message for Update", placeholder="e.g., Fix typo in README.md") update_edited_file_button = gr.Button("Update File in Space", variant="primary") edit_update_status_output = gr.Markdown(label="File Update Result", value="Result will appear here.") # --- Event Handlers for Browse & Edit Tab --- def handle_browse_space_files(token_from_ui, space_name, owner_name, force_refresh): if not space_name: return { browse_status_output: gr.Markdown("Error: Space Name cannot be empty."), file_explorer_component: gr.FileExplorer(value=None), current_clone_root_path_state: None, file_editor_textbox: gr.Textbox(value=""), # Clear editor current_file_display_ro: gr.Textbox(value="No file selected."), current_editing_file_relative_path_state: None } new_clone_root_path, error_msg = get_space_local_clone_path(token_from_ui, space_name, owner_name, force_refresh) if error_msg: return { browse_status_output: gr.Markdown(f"Error: {error_msg}"), file_explorer_component: gr.FileExplorer(value=None), current_clone_root_path_state: None, file_editor_textbox: gr.Textbox(value=""), current_file_display_ro: gr.Textbox(value="No file selected."), current_editing_file_relative_path_state: None } return { browse_status_output: gr.Markdown(f"Space '{owner_name}/{space_name}' files loaded. Local clone: `{new_clone_root_path}`"), file_explorer_component: gr.FileExplorer(value=new_clone_root_path), current_clone_root_path_state: new_clone_root_path, file_editor_textbox: gr.Textbox(value=""), current_file_display_ro: gr.Textbox(value="No file selected."), current_editing_file_relative_path_state: None } browse_files_button.click( fn=handle_browse_space_files, inputs=[api_token_ui_input, browse_space_name_input, browse_owner_input, force_refresh_clone_checkbox], outputs=[browse_status_output, file_explorer_component, current_clone_root_path_state, file_editor_textbox, current_file_display_ro, current_editing_file_relative_path_state] ) def handle_file_selected_in_explorer(selected_file_abs_path_evt: gr.SelectData, clone_root_path_from_state): if not selected_file_abs_path_evt or not selected_file_abs_path_evt.value: return { file_editor_textbox: gr.Textbox(value=""), current_file_display_ro: gr.Textbox(value="No file selected."), current_editing_file_relative_path_state: None, browse_status_output: gr.Markdown("File selection cleared or invalid.") } selected_file_abs_path = selected_file_abs_path_evt.value[0] # FileExplorer with file_count="single" returns a list of one item if not clone_root_path_from_state: return { file_editor_textbox: gr.Textbox(value=""), current_file_display_ro: gr.Textbox(value="Error: Clone root path not set."), current_editing_file_relative_path_state: None, browse_status_output: gr.Markdown("Error: Clone root path state is not set. Please load space files first.") } if not os.path.isfile(selected_file_abs_path): return { file_editor_textbox: gr.Textbox(value=""), current_file_display_ro: gr.Textbox(value="Selected item is not a file."), current_editing_file_relative_path_state: None, browse_status_output: gr.Markdown(f"'{os.path.basename(selected_file_abs_path)}' is a directory. Please select a file.") } content, error_msg = read_file_from_local_path(selected_file_abs_path) if error_msg: return { file_editor_textbox: gr.Textbox(value=f"Error reading file: {error_msg}"), current_file_display_ro: gr.Textbox(value="Error reading file."), current_editing_file_relative_path_state: None, browse_status_output: gr.Markdown(f"Error loading content for {os.path.basename(selected_file_abs_path)}.") } try: relative_path = os.path.relpath(selected_file_abs_path, start=clone_root_path_from_state) relative_path = relative_path.replace(os.sep, '/') # Normalize to forward slashes for HF Hub except ValueError as e: return { file_editor_textbox: gr.Textbox(value=f"Error: Could not determine file's relative path. {e}"), current_file_display_ro: gr.Textbox(value="Error: Path calculation failed."), current_editing_file_relative_path_state: None, browse_status_output: gr.Markdown("Error: File path calculation issue.") } return { file_editor_textbox: gr.Textbox(value=content), current_file_display_ro: gr.Textbox(value=relative_path), current_editing_file_relative_path_state: relative_path, browse_status_output: gr.Markdown(f"Loaded content for: {relative_path}") } file_explorer_component.select( # .select() is correct for FileExplorer fn=handle_file_selected_in_explorer, inputs=[current_clone_root_path_state], outputs=[file_editor_textbox, current_file_display_ro, current_editing_file_relative_path_state, browse_status_output] ) update_edited_file_button.click( fn=update_space_file, inputs=[ api_token_ui_input, browse_space_name_input, browse_owner_input, current_editing_file_relative_path_state, file_editor_textbox, edit_commit_message_input ], outputs=[edit_update_status_output] ) # --- Event handlers for Create Space Tab (unchanged) --- create_btn.click( fn=create_space, inputs=[api_token_ui_input, space_name_create_input, owner_create_input, sdk_create_input, markdown_input_create], outputs=create_output_md, ) return demo if __name__ == "__main__": demo = main_ui() demo.launch()