Spaces:
Sleeping
Sleeping
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() |