File size: 12,761 Bytes
fe42caa
18c5dec
 
 
 
 
 
fe42caa
 
 
18c5dec
 
fe42caa
 
18c5dec
fe42caa
18c5dec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fe42caa
 
 
18c5dec
 
 
 
 
 
 
 
 
 
 
 
 
fe42caa
 
18c5dec
 
 
 
 
 
 
 
 
 
 
fe42caa
18c5dec
 
 
 
 
fe42caa
 
18c5dec
 
 
fe42caa
18c5dec
 
fe42caa
18c5dec
 
 
 
 
 
 
 
 
 
 
 
 
 
fe42caa
 
18c5dec
 
 
 
fe42caa
18c5dec
 
fe42caa
18c5dec
 
 
 
 
 
 
fe42caa
 
18c5dec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fe42caa
18c5dec
 
 
 
 
fe42caa
 
18c5dec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fe42caa
 
18c5dec
 
 
 
 
 
 
 
 
fe42caa
18c5dec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fe42caa
18c5dec
 
 
 
 
 
 
fe42caa
18c5dec
 
fe42caa
 
18c5dec
 
 
 
 
 
 
fe42caa
18c5dec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fe42caa
18c5dec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fe42caa
18c5dec
 
 
 
 
fe42caa
 
18c5dec
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
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
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):
    if not token:
        # Allow read-only operations without a token
        return HfApi()
    return HfApi(token=token)

# --- UI Functions ---

def handle_token_change(token):
    """Called when the token is entered. Fetches user info and enables/disables UI elements."""
    if not token:
        # No token, so disable write actions and clear username
        update_dict = {
            # In 'Actions' panel
            manage_files_btn: gr.update(interactive=False),
            delete_repo_btn: gr.update(interactive=False),
            # In 'Editor' panel
            commit_btn: gr.update(interactive=False),
            # In 'List Repos' panel
            author_input: gr.update(value=""),
            # User info output
            whoami_output: gr.update(value=None, visible=False)
        }
        return (None, "") + (gr.update(),) * (len(update_dict))

    try:
        api = get_hf_api(token)
        user_info = api.whoami()
        username = user_info.get('name')
        
        # Token is valid, enable write actions
        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)
        }
        # The first two return values update gr.State objects
        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 the invalid token for read-only API calls
        return (token, "", *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)
    try:
        api = get_hf_api(token)
        repos = api.list_repos(author=author, repo_type=repo_type)
        repo_ids = [repo.id for repo in repos]
        return gr.update(choices=repo_ids, value=None), 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)

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
    if not repo_id:
        gr.Warning("No repository selected to delete.")
        return
    try:
        api = get_hf_api(token)
        api.delete_repo(repo_id=repo_id, repo_type=repo_type)
        gr.Info(f"Successfully deleted '{repo_id}'. Please re-list repositories.")
        # Clear selection and hide action/editor panels
        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) # Keep state on failure

# --- 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)
    try:
        api = get_hf_api(token)
        repo_files = api.list_repo_files(repo_id=repo_id, repo_type=repo_type)
        
        # Don't show .gitattributes or other hidden files by default
        filtered_files = [f for f in repo_files if not f.startswith('.')]
        
        # Update UI components for the editor
        return (
            gr.update(visible=True), # Show editor panel
            gr.update(choices=filtered_files, value=None), # Update file dropdown
            gr.update(value=f"## Select a file from the dropdown to view or edit its content.", language=None), # Clear code view
            "" # Clear commit message
        )
    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)
        # Download the file to a temporary local path
        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()
        
        # Determine language for syntax highlighting
        language = os.path.splitext(filepath)[1].lstrip('.')
        if language in ['py', 'js', 'html', 'css', 'json', 'md']:
             return gr.update(value=content, language=language)
        else:
             return gr.update(value=content, language='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:
        # Write content to a temporary file to upload it
        temp_dir = "hf_temp_files"
        os.makedirs(temp_dir, exist_ok=True)
        # Use a unique filename to avoid conflicts
        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,
        )
        
        # Clean up the temporary file
        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(), title="Hugging Face Hub Toolkit") as demo:
    # State management
    hf_token_state = gr.State(None)
    username_state = gr.State("")
    selected_repo_id = gr.State(None)
    selected_repo_type = gr.State("space") # Default to spaces

    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():
        # 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)")
            
            with gr.Tabs() as repo_type_tabs:
                # This helper function creates the list button and radio selector for a repo type
                def create_repo_lister(repo_type, label):
                    with gr.Tab(label, id=repo_type):
                        gr.Button(f"List {label}").click(
                            fn=list_repos,
                            inputs=[hf_token_state, author_input, gr.State(repo_type)],
                            outputs=[repo_selector, editor_panel]
                        )
                
                create_repo_lister("space", "Spaces")
                create_repo_lister("model", "Models")
                create_repo_lister("dataset", "Datasets")

            repo_selector = gr.Radio(label="Select a Repository", interactive=True)

        # PANEL 2 & 3: Actions and Editor
        with gr.Column(scale=3):
            with gr.Row():
                # PANEL 2: Action Buttons
                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)
                
                # PANEL 3: File Editor
                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)

    # --- Event Wiring ---

    # When token changes, update auth state and UI
    hf_token.change(
        fn=handle_token_change,
        inputs=hf_token,
        outputs=[
            hf_token_state,
            username_state,
            manage_files_btn,
            delete_repo_btn,
            commit_btn,
            author_input,
            whoami_output
        ]
    )

    # When repo type tab is changed, store the new type and clear selection
    def on_tab_change(repo_type):
        return repo_type, None, gr.update(choices=[], value=None), gr.update(visible=False), gr.update(visible=False)

    repo_type_tabs.select(
        fn=on_tab_change,
        inputs=repo_type_tabs,
        outputs=[selected_repo_type, selected_repo_id, repo_selector, action_panel, editor_panel]
    )

    # When a repo is selected, update state and show the action 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]
    )
    
    # Action button clicks
    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],
        # Add a confirmation popup before deleting
        js="() => confirm('Are you sure you want to permanently delete this repository? This action cannot be undone.')"
    )
    
    # Editor interactions
    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(debug=True)