build-space / app.py
broadfield-dev's picture
Update app.py
e69109f verified
raw
history blame
13.7 kB
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()