File size: 13,123 Bytes
fe42caa
18c5dec
 
 
 
 
 
fe42caa
 
a2ea4b0
 
fe42caa
18c5dec
fe42caa
a2ea4b0
 
 
 
18c5dec
a2ea4b0
18c5dec
 
 
 
 
 
a2ea4b0
 
fe42caa
 
 
18c5dec
 
 
a2ea4b0
18c5dec
 
 
 
 
 
 
 
fe42caa
 
18c5dec
 
 
 
 
 
 
 
a2ea4b0
 
18c5dec
fe42caa
18c5dec
 
 
 
a2ea4b0
fe42caa
 
a2ea4b0
 
 
18c5dec
a2ea4b0
fe42caa
18c5dec
a2ea4b0
fe42caa
18c5dec
 
 
 
 
 
 
 
 
 
a2ea4b0
18c5dec
 
a2ea4b0
fe42caa
 
18c5dec
a2ea4b0
18c5dec
fe42caa
18c5dec
a2ea4b0
fe42caa
18c5dec
 
 
 
 
 
a2ea4b0
fe42caa
 
18c5dec
 
 
 
a2ea4b0
 
 
 
18c5dec
 
 
 
 
 
 
fe42caa
18c5dec
 
 
 
 
fe42caa
 
18c5dec
a2ea4b0
18c5dec
 
 
 
a2ea4b0
 
 
 
 
 
 
18c5dec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fe42caa
 
18c5dec
 
 
 
 
 
 
fe42caa
18c5dec
 
 
 
 
 
 
 
 
 
 
 
fe42caa
18c5dec
a2ea4b0
18c5dec
 
a2ea4b0
18c5dec
a2ea4b0
fe42caa
18c5dec
 
fe42caa
 
18c5dec
 
a2ea4b0
18c5dec
 
fe42caa
a2ea4b0
18c5dec
 
 
a2ea4b0
18c5dec
a2ea4b0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18c5dec
a2ea4b0
 
 
 
 
18c5dec
 
 
fe42caa
18c5dec
 
 
 
 
 
 
 
 
 
 
 
a2ea4b0
 
 
 
 
 
 
 
 
18c5dec
 
 
a2ea4b0
 
18c5dec
a2ea4b0
18c5dec
a2ea4b0
18c5dec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a2ea4b0
 
 
 
 
18c5dec
 
 
 
 
 
 
fe42caa
18c5dec
 
 
 
 
fe42caa
 
a2ea4b0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
import gradio as gr
from huggingface_hub import HfApi
from huggingface_hub.utils import HfHubHTTPError, RepositoryNotFoundError
import os
import uuid

# --- State Management and API Client ---

def get_hf_api(token):
    """Initializes the HfApi client. Allows read-only operations if no token is provided."""
    return HfApi(token=token if token else None)

# --- UI Functions ---

def handle_token_change(token, current_author):
    """
    Called when the token is entered. Fetches user info and updates UI interactivity.
    """
    if not token:
        # No token, disable write actions and clear user-specific info
        update_dict = {
            manage_files_btn: gr.update(interactive=False),
            delete_repo_btn: gr.update(interactive=False),
            commit_btn: gr.update(interactive=False),
            whoami_output: gr.update(value=None, visible=False)
        }
        # Do not clear the author field if the user typed it manually
        return (None, current_author, *update_dict.values())

    try:
        api = get_hf_api(token)
        user_info = api.whoami()
        username = user_info.get('name')
        
        # Token is valid, enable write actions and set author
        update_dict = {
            manage_files_btn: gr.update(interactive=True),
            delete_repo_btn: gr.update(interactive=True),
            commit_btn: gr.update(interactive=True),
            author_input: gr.update(value=username),
            whoami_output: gr.update(value=user_info, visible=True)
        }
        return (token, username, *update_dict.values())

    except HfHubHTTPError as e:
        # Token is invalid
        gr.Warning(f"Invalid Token: {e}. You can only perform read-only actions.")
        update_dict = {
            manage_files_btn: gr.update(interactive=False),
            delete_repo_btn: gr.update(interactive=False),
            commit_btn: gr.update(interactive=False),
            whoami_output: gr.update(value=None, visible=False)
        }
        # Clear username but keep author field as is
        return (token, current_author, *update_dict.values())


def list_repos(token, author, repo_type):
    """Lists repositories for a given author and type."""
    if not author:
        gr.Info("Please enter an author (username or organization) to list repositories.")
        return gr.update(choices=[], value=None), gr.update(visible=False), gr.update(visible=False)
    try:
        api = get_hf_api(token)
        # Use the dedicated list functions for clarity
        list_fn = getattr(api, f"list_{repo_type}s")
        repos = list_fn(author=author)
        repo_ids = [repo.id for repo in repos]
        return gr.update(choices=repo_ids, value=None), gr.update(visible=False), gr.update(visible=False)
    except HfHubHTTPError as e:
        gr.Error(f"Could not list repositories: {e}")
        return gr.update(choices=[], value=None), gr.update(visible=False), gr.update(visible=False)

def handle_repo_selection(repo_id):
    """Called when a repo is selected. Makes action buttons visible."""
    if repo_id:
        return gr.update(visible=True), gr.update(visible=False) # Show actions, hide editor
    return gr.update(visible=False), gr.update(visible=False) # Hide everything

def delete_repo(token, repo_id, repo_type):
    """Deletes the selected repository."""
    if not token:
        gr.Error("A write-enabled Hugging Face token is required to delete a repository.")
        return repo_id, gr.update(visible=True), gr.update(visible=False)
    if not repo_id:
        gr.Warning("No repository selected to delete.")
        return repo_id, gr.update(visible=True), gr.update(visible=False)
    try:
        api = get_hf_api(token)
        api.delete_repo(repo_id=repo_id, repo_type=repo_type)
        gr.Info(f"Successfully deleted '{repo_id}'. Listing updated repositories.")
        return None, gr.update(visible=False), gr.update(visible=False)
    except HfHubHTTPError as e:
        gr.Error(f"Failed to delete repository: {e}")
        return repo_id, gr.update(visible=True), gr.update(visible=False)

# --- File Editor Functions ---

def show_file_manager(token, repo_id, repo_type):
    """Lists files in the selected repo and shows the editor panel."""
    if not repo_id:
        gr.Warning("No repository selected.")
        return gr.update(visible=False), gr.update(), gr.update(), gr.update()
    try:
        api = get_hf_api(token)
        repo_files = api.list_repo_files(repo_id=repo_id, repo_type=repo_type)
        filtered_files = [f for f in repo_files if not f.startswith('.')]
        
        return (
            gr.update(visible=True),
            gr.update(choices=filtered_files, value=None),
            gr.update(value="## Select a file to view or edit.", language='markdown'),
            ""
        )
    except RepositoryNotFoundError:
        gr.Error(f"Repository '{repo_id}' not found. It might be private and require a token.")
        return gr.update(visible=False), gr.update(), gr.update(), gr.update()
    except Exception as e:
        gr.Error(f"Could not list files: {e}")
        return gr.update(visible=False), gr.update(), gr.update(), gr.update()


def load_file_content(token, repo_id, repo_type, filepath):
    """Downloads and displays the content of a selected file."""
    if not filepath:
        return gr.update(value="## Select a file to view its content.", language='markdown')
    try:
        api = get_hf_api(token)
        local_path = api.hf_hub_download(
            repo_id=repo_id, repo_type=repo_type, filename=filepath, token=token
        )
        with open(local_path, 'r', encoding='utf-8') as f:
            content = f.read()
        
        language = os.path.splitext(filepath)[1].lstrip('.').lower()
        supported_langs = ['python', 'typescript', 'css', 'json', 'markdown', 'html', 'javascript']
        if language == 'py': language = 'python'
        if language == 'js': language = 'javascript'
        if language == 'md': language = 'markdown'
        
        return gr.update(value=content, language=language if language in supported_langs else 'plaintext')

    except Exception as e:
        gr.Error(f"Could not load file '{filepath}': {e}")
        return gr.update(value=f"Error loading file: {e}", language='plaintext')

def commit_file(token, repo_id, repo_type, filepath, content, commit_message):
    """Commits the edited file content back to the repository."""
    if not token:
        gr.Error("A write-enabled token is required to commit changes.")
        return
    if not filepath:
        gr.Warning("No file is selected to commit.")
        return
    if not commit_message:
        gr.Warning("Commit message cannot be empty.")
        return

    try:
        temp_dir = "hf_temp_files"
        os.makedirs(temp_dir, exist_ok=True)
        temp_file_path = os.path.join(temp_dir, f"{uuid.uuid4()}_{os.path.basename(filepath)}")
        
        with open(temp_file_path, "w", encoding="utf-8") as f:
            f.write(content)

        api = get_hf_api(token)
        api.upload_file(
            path_or_fileobj=temp_file_path,
            path_in_repo=filepath,
            repo_id=repo_id,
            repo_type=repo_type,
            commit_message=commit_message,
        )
        os.remove(temp_file_path)
        gr.Info(f"Successfully committed '{filepath}' to '{repo_id}'!")
    except Exception as e:
        gr.Error(f"Failed to commit file: {e}")


# --- Gradio UI Layout ---
with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue"), title="Hugging Face Hub Toolkit") as demo:
    # State management
    hf_token_state = gr.State(None)
    author_state = gr.State("")
    selected_repo_id = gr.State(None)
    selected_repo_type = gr.State("space") # Default

    gr.Markdown("# Hugging Face Hub Dashboard")
    gr.Markdown("An intuitive interface to manage your Hugging Face repositories. **Enter a write-token for full access.**")

    with gr.Row():
        hf_token = gr.Textbox(
            label="Hugging Face API Token (write permission recommended)",
            type="password", placeholder="hf_...", scale=3,
        )
        whoami_output = gr.JSON(label="Authenticated User", visible=False, scale=1)

    with gr.Row(equal_height=False):
        # PANEL 1: List and Select Repos
        with gr.Column(scale=1):
            gr.Markdown("### 1. Select a Repository")
            author_input = gr.Textbox(label="Author (Username or Org)", interactive=True)
            
            repo_selector = gr.Radio(label="Select a Repository", interactive=True, value=None)
            
            # This is the corrected function definition. It now takes UI elements as arguments.
            def create_repo_lister(repo_type, label, repo_selector_el, action_panel_el, editor_panel_el):
                with gr.Tab(label, id=repo_type):
                    btn = gr.Button(f"List {label}")
                    btn.click(
                        fn=list_repos,
                        inputs=[hf_token_state, author_input, gr.State(repo_type)],
                        outputs=[repo_selector_el, action_panel_el, editor_panel_el]
                    ).then(
                        # After listing, update the author state
                        fn=lambda author: author,
                        inputs=author_input,
                        outputs=author_state
                    )

            with gr.Tabs() as repo_type_tabs:
                # The action and editor panels are defined later but referenced here.
                # We will define placeholder variables for them now.
                action_panel_ref = gr.Column()
                editor_panel_ref = gr.Column()

        # PANEL 2 & 3: Actions and Editor
        with gr.Column(scale=3):
            with gr.Row():
                with gr.Column(scale=1, visible=False) as action_panel:
                    gr.Markdown("### 2. Choose an Action")
                    manage_files_btn = gr.Button("Manage Files", interactive=False)
                    delete_repo_btn = gr.Button("Delete this Repo", variant="stop", interactive=False)
                
                with gr.Column(scale=3, visible=False) as editor_panel:
                    gr.Markdown("### 3. Edit Files")
                    file_selector = gr.Dropdown(label="Select File", interactive=True)
                    code_editor = gr.Code(label="File Content", language="markdown", interactive=True)
                    commit_message_input = gr.Textbox(label="Commit Message", placeholder="e.g., Update README.md", interactive=True)
                    commit_btn = gr.Button("Commit Changes", variant="primary", interactive=False)

    # --- Post-Layout UI Wiring ---
    # Now that action_panel and editor_panel are fully defined, we can wire them up
    # in the create_repo_lister function calls within the Tabs context.
    with repo_type_tabs:
        create_repo_lister("space", "Spaces", repo_selector, action_panel, editor_panel)
        create_repo_lister("model", "Models", repo_selector, action_panel, editor_panel)
        create_repo_lister("dataset", "Datasets", repo_selector, action_panel, editor_panel)

    # --- Event Handlers ---

    hf_token.change(
        fn=handle_token_change,
        inputs=[hf_token, author_state],
        outputs=[hf_token_state, author_state, manage_files_btn, delete_repo_btn, commit_btn, author_input, whoami_output]
    )
    
    repo_type_tabs.select(
        fn=lambda rt: (rt, None, gr.update(choices=[], value=None), gr.update(visible=False), gr.update(visible=False)),
        inputs=repo_type_tabs,
        outputs=[selected_repo_type, selected_repo_id, repo_selector, action_panel, editor_panel]
    )

    repo_selector.select(
        fn=lambda repo_id: (repo_id, *handle_repo_selection(repo_id)),
        inputs=repo_selector,
        outputs=[selected_repo_id, action_panel, editor_panel]
    )
    
    manage_files_btn.click(
        fn=show_file_manager,
        inputs=[hf_token_state, selected_repo_id, selected_repo_type],
        outputs=[editor_panel, file_selector, code_editor, commit_message_input]
    )
    
    delete_repo_btn.click(
        fn=delete_repo,
        inputs=[hf_token_state, selected_repo_id, selected_repo_type],
        outputs=[selected_repo_id, action_panel, editor_panel],
        js="() => confirm('Are you sure you want to permanently delete this repository? This action cannot be undone.')"
    ).then(
        # After attempting deletion, refresh the list
        fn=list_repos,
        inputs=lambda: (hf_token_state.value, author_state.value, selected_repo_type.value),
        outputs=[repo_selector, action_panel, editor_panel]
    )
    
    file_selector.change(
        fn=load_file_content,
        inputs=[hf_token_state, selected_repo_id, selected_repo_type, file_selector],
        outputs=code_editor
    )

    commit_btn.click(
        fn=commit_file,
        inputs=[hf_token_state, selected_repo_id, selected_repo_type, file_selector, code_editor, commit_message_input],
        outputs=[]
    )

if __name__ == "__main__":
    demo.launch()