Update app.py
Browse files
app.py
CHANGED
@@ -1,3 +1,5 @@
|
|
|
|
|
|
1 |
import gradio as gr
|
2 |
import re
|
3 |
import json
|
@@ -17,6 +19,10 @@ from build_logic import (
|
|
17 |
delete_space_file as build_logic_delete_space_file,
|
18 |
get_space_runtime_status,
|
19 |
apply_staged_changes,
|
|
|
|
|
|
|
|
|
20 |
)
|
21 |
|
22 |
from model_logic import (
|
@@ -69,11 +75,12 @@ The system will parse these actions from your response and present them to the u
|
|
69 |
|
70 |
Available commands:
|
71 |
- `CREATE_SPACE <owner>/<repo_name> --sdk <sdk> --private <true|false>`: Creates a new, empty space. SDK can be gradio, streamlit, docker, or static. Private is optional and defaults to false. Use this ONLY when the user explicitly asks to create a *new* space with a specific name. When using this action, also provide the initial file structure (e.g., app.py, README.md) using `### File:` blocks in the same response. The system will apply these files to the new space.
|
|
|
72 |
- `DELETE_FILE path/to/file.ext`: Deletes a specific file from the current space.
|
73 |
- `SET_PRIVATE <true|false>`: Sets the privacy for the current space.
|
74 |
- `DELETE_SPACE`: Deletes the entire current space. THIS IS PERMANENT AND REQUIRES CAUTION. Only use this if the user explicitly and clearly asks to delete the space.
|
75 |
|
76 |
-
You can issue multiple file updates and action commands in a single response. The system will process all of them into a single change plan.
|
77 |
|
78 |
**Current Space Context:**
|
79 |
You will be provided with the current state of the files in the Space the user is interacting with. Use this information to understand the current project structure and content before proposing changes or actions. This context will appear after the user's message, starting with "## Current Space Context:". Do NOT include this context in your response. Only generate your response based on the user's request and the formatting rules above.
|
@@ -259,40 +266,100 @@ def generate_and_stage_changes(ai_response_content, current_files_state, hf_owne
|
|
259 |
ai_proposed_files_dict = {f["path"]: f for f in ai_proposed_files_list}
|
260 |
|
261 |
action_pattern = re.compile(r"### HF_ACTION:\s*(?P<command_line>[^\n]+)", re.MULTILINE)
|
|
|
|
|
262 |
for match in action_pattern.finditer(ai_response_content):
|
263 |
try:
|
264 |
cmd_parts = shlex.split(match.group("command_line").strip())
|
265 |
if not cmd_parts: continue
|
266 |
command, args = cmd_parts[0].upper(), cmd_parts[1:]
|
267 |
-
|
268 |
-
if command == "CREATE_SPACE" and args:
|
269 |
-
repo_id = args[0]
|
270 |
-
sdk = "gradio"
|
271 |
-
private = False
|
272 |
-
if '--sdk' in args:
|
273 |
-
try: sdk = args[args.index('--sdk') + 1]
|
274 |
-
except IndexError: print("Warning: CREATE_SPACE --sdk requires an argument.")
|
275 |
-
if '--private' in args:
|
276 |
-
try: private_str = args[args.index('--private') + 1].lower()
|
277 |
-
except IndexError: print("Warning: CREATE_SPACE --private requires an argument.")
|
278 |
-
else: private = private_str == 'true'
|
279 |
-
changeset.append({"type": "CREATE_SPACE", "repo_id": repo_id, "sdk": sdk, "private": private})
|
280 |
-
|
281 |
-
elif command == "DELETE_FILE" and args:
|
282 |
-
file_path = args[0]
|
283 |
-
changeset.append({"type": "DELETE_FILE", "path": file_path})
|
284 |
-
|
285 |
-
elif command == "SET_PRIVATE" and args:
|
286 |
-
private = args[0].lower() == 'true'
|
287 |
-
changeset.append({"type": "SET_PRIVACY", "private": private, "repo_id": f"{hf_owner_name}/{hf_repo_name}"})
|
288 |
-
|
289 |
-
elif command == "DELETE_SPACE":
|
290 |
-
changeset.append({"type": "DELETE_SPACE", "owner": hf_owner_name, "space_name": hf_repo_name})
|
291 |
-
|
292 |
except Exception as e:
|
293 |
print(f"Error parsing HF_ACTION line '{match.group('command_line').strip()}': {e}")
|
294 |
|
295 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
296 |
for file_info in ai_proposed_files_list:
|
297 |
filename = file_info["path"]
|
298 |
proposed_content = file_info["content"]
|
@@ -315,6 +382,7 @@ def generate_and_stage_changes(ai_response_content, current_files_state, hf_owne
|
|
315 |
else:
|
316 |
print(f"Skipping staging create for {filename}: Proposed content is a placeholder.")
|
317 |
|
|
|
318 |
if not changeset:
|
319 |
md_summary = ["### π Proposed Changes Plan", "\nThe AI did not propose any specific changes to files or the space.\n"]
|
320 |
else:
|
@@ -350,21 +418,20 @@ def generate_and_stage_changes(ai_response_content, current_files_state, hf_owne
|
|
350 |
|
351 |
def handle_chat_submit(user_message, chat_history, hf_api_key_input, provider_api_key_input, provider_select, model_select, system_prompt, hf_owner_name, hf_repo_name):
|
352 |
global parsed_code_blocks_state_cache
|
353 |
-
_chat_msg_in = user_message
|
354 |
_chat_hist = list(chat_history)
|
355 |
-
_chat_hist.append((user_message, None))
|
356 |
|
357 |
-
# Initial yield: Show loading state, clear input, clear/hide changes
|
358 |
yield (
|
359 |
-
"",
|
360 |
-
_chat_hist,
|
361 |
-
"Initializing...",
|
362 |
-
gr.update(),
|
363 |
-
gr.update(),
|
364 |
-
gr.update(interactive=False),
|
365 |
-
[],
|
366 |
-
"*No changes proposed.*",
|
367 |
-
gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
|
368 |
)
|
369 |
|
370 |
current_sys_prompt = system_prompt.strip() or DEFAULT_SYSTEM_PROMPT
|
@@ -383,59 +450,53 @@ def handle_chat_submit(user_message, chat_history, hf_api_key_input, provider_ap
|
|
383 |
if chunk is None: continue
|
384 |
full_bot_response_content += str(chunk)
|
385 |
_chat_hist[-1] = (user_message, full_bot_response_content)
|
386 |
-
# Yield during streaming - ONLY update chatbot and status
|
387 |
yield (
|
388 |
-
gr.update(),
|
389 |
-
_chat_hist,
|
390 |
-
f"Streaming from {model_select}...",
|
391 |
-
gr.update(),
|
392 |
-
gr.update(),
|
393 |
-
gr.update(),
|
394 |
-
gr.update(),
|
395 |
-
gr.update(),
|
396 |
-
gr.update(), gr.update(), gr.update()
|
397 |
)
|
398 |
|
399 |
-
# --- Post-streaming: Parse AI output, update cache, stage changes, FINAL UI UPDATE ---
|
400 |
-
|
401 |
if full_bot_response_content.startswith("Error:") or full_bot_response_content.startswith("API HTTP Error"):
|
402 |
_status = full_bot_response_content
|
403 |
-
# Update chatbot with error in the last message, update status, clear/hide changes
|
404 |
yield (
|
405 |
gr.update(), _chat_hist, _status,
|
406 |
-
gr.update(), gr.update(), gr.update(),
|
407 |
-
[], "*Error occurred, changes plan cleared.*",
|
408 |
-
gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
|
409 |
)
|
410 |
-
return
|
411 |
|
412 |
-
# Parse AI output and update state cache
|
413 |
parsed_code_blocks_state_cache, proposed_filenames_in_turn = _parse_and_update_state_cache(full_bot_response_content, parsed_code_blocks_state_cache)
|
414 |
|
415 |
-
# Regenerate UI previews based on the updated cache
|
416 |
_formatted, _detected, _download = _generate_ui_outputs_from_cache(hf_owner_name, hf_repo_name)
|
417 |
|
418 |
-
# Generate changeset and summary
|
419 |
staged_changeset, summary_md = generate_and_stage_changes(full_bot_response_content, parsed_code_blocks_state_cache, hf_owner_name, hf_repo_name)
|
420 |
|
421 |
-
# Final yield based on whether changes were staged
|
422 |
if not staged_changeset:
|
423 |
-
_status = summary_md
|
424 |
yield (
|
425 |
-
gr.update(),
|
426 |
-
_chat_hist, _status,
|
427 |
-
_detected, _formatted, _download,
|
428 |
-
[], summary_md,
|
429 |
-
gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
|
430 |
)
|
431 |
else:
|
432 |
_status = "Change plan generated. Please review and confirm below."
|
433 |
yield (
|
434 |
-
gr.update(),
|
435 |
-
_chat_hist, _status,
|
436 |
-
_detected, _formatted, _download,
|
437 |
-
staged_changeset, summary_md,
|
438 |
-
gr.update(visible=True),
|
|
|
|
|
439 |
)
|
440 |
|
441 |
except Exception as e:
|
@@ -443,26 +504,22 @@ def handle_chat_submit(user_message, chat_history, hf_api_key_input, provider_ap
|
|
443 |
print(f"Error in handle_chat_submit: {e}")
|
444 |
import traceback
|
445 |
traceback.print_exc()
|
446 |
-
# Update chatbot with error
|
447 |
if _chat_hist:
|
448 |
-
# Find the last message matching the user's input and append the error
|
449 |
try:
|
450 |
idx = next(i for i in reversed(range(len(_chat_hist))) if _chat_hist[i] and _chat_hist[i][0] == user_message)
|
451 |
current_bot_response = _chat_hist[idx][1] or ""
|
452 |
_chat_hist[idx] = (user_message, (current_bot_response + "\n\n" if current_bot_response else "") + error_msg)
|
453 |
except (StopIteration, IndexError):
|
454 |
-
_chat_hist.append((user_message, error_msg))
|
455 |
|
456 |
-
# Regenerate previews from potentially partially updated cache
|
457 |
_formatted, _detected, _download = _generate_ui_outputs_from_cache(hf_owner_name, hf_repo_name)
|
458 |
|
459 |
-
# Final error yield: Update chatbot, status, file previews, clear/hide changes
|
460 |
yield (
|
461 |
-
gr.update(),
|
462 |
-
_chat_hist, error_msg,
|
463 |
-
_detected, _formatted, _download,
|
464 |
-
[], "*Error occurred, changes plan cleared.*",
|
465 |
-
gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
|
466 |
)
|
467 |
|
468 |
|
@@ -475,51 +532,159 @@ def handle_confirm_changes(hf_api_key, owner_name, space_name, changeset):
|
|
475 |
if not changeset:
|
476 |
return "No changes to apply.", gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(value="No changes were staged.")
|
477 |
|
478 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
479 |
|
480 |
-
|
481 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
482 |
|
483 |
-
refreshed_file_list = []
|
484 |
-
reload_error = None
|
485 |
-
repo_id_for_reload = f"{owner_name}/{space_name}" if owner_name and space_name else None
|
486 |
|
487 |
-
if repo_id_for_reload:
|
488 |
-
sdk, file_list, err_list = get_space_repository_info(hf_api_key, space_name, owner_name)
|
489 |
-
if err_list:
|
490 |
-
reload_error = f"Error reloading file list after changes: {err_list}"
|
491 |
-
parsed_code_blocks_state_cache = []
|
492 |
else:
|
493 |
-
|
494 |
-
|
495 |
-
|
496 |
-
|
497 |
-
|
498 |
-
|
499 |
-
|
500 |
-
|
501 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
502 |
|
503 |
-
else:
|
504 |
-
|
505 |
|
506 |
-
|
|
|
507 |
|
508 |
-
|
|
|
|
|
509 |
|
510 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
511 |
|
512 |
-
|
513 |
-
|
514 |
-
|
515 |
-
|
516 |
-
|
517 |
-
|
518 |
-
|
519 |
-
|
520 |
-
|
521 |
-
|
522 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
523 |
|
524 |
|
525 |
def handle_cancel_changes():
|
@@ -545,36 +710,30 @@ def handle_load_existing_space(hf_api_key_ui, ui_owner_name, ui_space_name):
|
|
545 |
global parsed_code_blocks_state_cache
|
546 |
_formatted_md_val, _detected_preview_val, _status_val = "*Loading files...*", "*Loading files...*", f"Loading Space: {ui_owner_name}/{ui_space_name}..."
|
547 |
_file_browser_update, _iframe_html_update, _download_btn_update = gr.update(visible=False, choices=[], value=None), gr.update(value=None, visible=False), gr.update(interactive=False, value=None)
|
548 |
-
_build_status_clear, _edit_status_clear, _runtime_status_clear = "*
|
549 |
_changeset_clear = []
|
550 |
_changeset_summary_clear = "*No changes proposed.*"
|
551 |
_confirm_ui_hidden = gr.update(visible=False)
|
|
|
552 |
|
553 |
-
|
554 |
-
|
555 |
-
|
556 |
-
space_iframe_display, download_button, build_status_display,
|
557 |
-
edit_status_display, space_runtime_status_display,
|
558 |
-
changeset_state, changeset_display, confirm_accordion, confirm_button, cancel_button
|
559 |
-
]
|
560 |
-
# Use gr.update for components that are not state variables
|
561 |
-
initial_updates = [
|
562 |
gr.update(value=_formatted_md_val), gr.update(value=_detected_preview_val), gr.update(value=_status_val), _file_browser_update,
|
563 |
gr.update(value=ui_owner_name), gr.update(value=ui_space_name),
|
564 |
_iframe_html_update, _download_btn_update, gr.update(value=_build_status_clear),
|
565 |
gr.update(value=_edit_status_clear), gr.update(value=_runtime_status_clear),
|
566 |
-
_changeset_clear, gr.update(value=_changeset_summary_clear), _confirm_ui_hidden, _confirm_ui_hidden, _confirm_ui_hidden
|
567 |
-
|
568 |
-
|
569 |
-
yield initial_updates
|
570 |
|
571 |
owner_to_use = ui_owner_name
|
|
|
|
|
|
|
|
|
|
|
572 |
if not owner_to_use:
|
573 |
-
token, token_err = build_logic_get_api_token(hf_api_key_ui)
|
574 |
-
if token_err:
|
575 |
-
_status_val = f"Load Error: {token_err}"
|
576 |
-
yield gr.update(value=_status_val), # Only update status on early error
|
577 |
-
return
|
578 |
try:
|
579 |
user_info = build_logic_whoami(token=token)
|
580 |
owner_to_use = user_info.get('name')
|
@@ -582,7 +741,7 @@ def handle_load_existing_space(hf_api_key_ui, ui_owner_name, ui_space_name):
|
|
582 |
yield gr.update(value=owner_to_use), gr.update(value=f"Loading Space: {owner_to_use}/{ui_space_name} (Auto-detected owner)...") # Update owner and status
|
583 |
except Exception as e:
|
584 |
_status_val = f"Load Error: Error auto-detecting owner: {e}"
|
585 |
-
yield gr.update(value=_status_val),
|
586 |
return
|
587 |
|
588 |
if not owner_to_use or not ui_space_name:
|
@@ -592,20 +751,19 @@ def handle_load_existing_space(hf_api_key_ui, ui_owner_name, ui_space_name):
|
|
592 |
|
593 |
sdk, file_list, err = get_space_repository_info(hf_api_key_ui, ui_space_name, owner_to_use)
|
594 |
|
595 |
-
# Always update owner/space inputs even on error
|
596 |
-
|
597 |
-
yield gr.update(value=owner_to_use), gr.update(value=ui_space_name), # Update owner and space textboxes
|
598 |
|
599 |
if err:
|
600 |
_status_val = f"Load Error: {err}"
|
601 |
parsed_code_blocks_state_cache = []
|
602 |
_formatted, _detected, _download = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name)
|
603 |
yield (
|
604 |
-
gr.update(value=_formatted), gr.update(value=_detected), gr.update(value=_status_val),
|
605 |
-
gr.update(visible=False, choices=[], value=None),
|
606 |
-
gr.update(), gr.update(),
|
607 |
-
gr.update(value=None, visible=False),
|
608 |
-
_download, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
|
609 |
)
|
610 |
return
|
611 |
|
@@ -632,13 +790,17 @@ def handle_load_existing_space(hf_api_key_ui, ui_owner_name, ui_space_name):
|
|
632 |
else:
|
633 |
iframe_update = gr.update(value=None, visible=False)
|
634 |
|
|
|
|
|
|
|
|
|
635 |
# Final yield after success
|
636 |
yield (
|
637 |
-
gr.update(value=_formatted), gr.update(value=_detected), gr.update(value=_status_val),
|
638 |
-
file_browser_update,
|
639 |
-
gr.update(), gr.update(),
|
640 |
-
iframe_update,
|
641 |
-
_download, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
|
642 |
)
|
643 |
|
644 |
def handle_build_space_button(hf_api_key_ui, ui_space_name_part, ui_owner_name_part, space_sdk_ui, is_private_ui, formatted_markdown_content):
|
@@ -649,20 +811,9 @@ def handle_build_space_button(hf_api_key_ui, ui_space_name_part, ui_owner_name_p
|
|
649 |
_changeset_summary_clear = "*Manual build initiated, changes plan cleared.*"
|
650 |
_confirm_ui_hidden = gr.update(visible=False)
|
651 |
|
652 |
-
|
653 |
-
|
654 |
-
|
655 |
-
changeset_state, changeset_display, confirm_accordion, confirm_button, cancel_button,
|
656 |
-
formatted_space_output_display, detected_files_preview, download_button
|
657 |
-
]
|
658 |
-
# Initial yield state
|
659 |
-
initial_updates = [
|
660 |
-
gr.update(value=_build_status), _iframe_html, _file_browser_update,
|
661 |
-
gr.update(value=ui_owner_name_part), gr.update(value=ui_space_name_part),
|
662 |
-
_changeset_clear, gr.update(value=_changeset_summary_clear), _confirm_ui_hidden, _confirm_ui_hidden, _confirm_ui_hidden,
|
663 |
-
gr.update(), gr.update(), gr.update() # Don't update markdown/download yet
|
664 |
-
]
|
665 |
-
yield initial_updates
|
666 |
|
667 |
|
668 |
if not ui_space_name_part or "/" in ui_space_name_part:
|
@@ -681,29 +832,25 @@ def handle_build_space_button(hf_api_key_ui, ui_space_name_part, ui_owner_name_p
|
|
681 |
|
682 |
if not manual_changeset:
|
683 |
_build_status = "Build Error: No target space specified or no files parsed from markdown."
|
684 |
-
yield gr.update(value=_build_status),
|
685 |
return
|
686 |
|
687 |
-
|
688 |
result_message = apply_staged_changes(hf_api_key_ui, ui_owner_name_part, ui_space_name_part, manual_changeset)
|
689 |
_build_status = f"Manual Build/Update Result: {result_message}"
|
690 |
|
691 |
owner_to_use = ui_owner_name_part
|
692 |
space_to_use = ui_space_name_part
|
693 |
-
# Keep manual markdown content as is, clear/disable preview/download initially
|
694 |
_formatted_md = formatted_markdown_content
|
695 |
_detected_preview_val = "*Loading files after build...*"
|
696 |
_download_btn_update = gr.update(interactive=False, value=None)
|
697 |
|
698 |
-
# Yield intermediate state with build status and placeholders for reloading
|
699 |
yield (
|
700 |
-
gr.update(value=_build_status), _iframe_html, _file_browser_update,
|
701 |
-
gr.update(value=owner_to_use), gr.update(value=space_to_use),
|
702 |
-
_changeset_clear, gr.update(value=_changeset_summary_clear), _confirm_ui_hidden, _confirm_ui_hidden, _confirm_ui_hidden,
|
703 |
-
gr.update(value=_formatted_md), gr.update(value=_detected_preview_val), _download_btn_update
|
704 |
)
|
705 |
|
706 |
-
# Reload space state to reflect actual content after build attempt
|
707 |
sdk_built, file_list, err_list = get_space_repository_info(hf_api_key_ui, space_to_use, owner_to_use)
|
708 |
|
709 |
if err_list:
|
@@ -725,22 +872,23 @@ def handle_build_space_button(hf_api_key_ui, ui_space_name_part, ui_owner_name_p
|
|
725 |
|
726 |
if owner_to_use and space_to_use:
|
727 |
sub_owner = re.sub(r'[^a-z0-9\-]+', '-', owner_to_use.lower()).strip('-') or 'owner'
|
728 |
-
sub_repo = re.sub(r'[^a-z0-9\-]+', '-',
|
729 |
iframe_url = f"https://{sub_owner}-{sub_repo}{'.static.hf.space' if sdk_built == 'static' else '.hf.space'}"
|
730 |
_iframe_html_update = gr.update(value=f'<iframe src="{iframe_url}?__theme=light&embed=true" width="100%" height="700px"></iframe>', visible=True)
|
731 |
else:
|
732 |
_iframe_html_update = gr.update(value=None, visible=False)
|
733 |
|
|
|
734 |
|
735 |
-
|
|
|
736 |
|
737 |
|
738 |
-
# Final yield after reloading
|
739 |
yield (
|
740 |
-
gr.update(value=_build_status), _iframe_html_update, _file_browser_update,
|
741 |
-
gr.update(value=owner_to_use), gr.update(value=space_to_use),
|
742 |
-
_changeset_clear, gr.update(value=_changeset_summary_clear), _confirm_ui_hidden, _confirm_ui_hidden, _confirm_ui_hidden,
|
743 |
-
gr.update(value=_formatted_md), gr.update(value=_detected_preview), _download
|
744 |
)
|
745 |
|
746 |
|
@@ -844,8 +992,122 @@ def handle_refresh_space_status(hf_api_key_ui, ui_owner_name, ui_space_name):
|
|
844 |
log_link = status_details.get('full_log_link')
|
845 |
if log_link and log_link != "#": md += f"- [View Full Logs]({log_link})\n"
|
846 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
847 |
return md
|
848 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
849 |
|
850 |
custom_theme = gr.themes.Base(primary_hue="teal", secondary_hue="purple", neutral_hue="zinc", text_size="sm", spacing_size="md", radius_size="sm", font=["System UI", "sans-serif"])
|
851 |
custom_css = """
|
@@ -913,6 +1175,21 @@ with gr.Blocks(theme=custom_theme, css=custom_css) as demo:
|
|
913 |
provider_api_key_input = gr.Textbox(label="Model Provider API Key (Optional)", type="password", placeholder="sk_... (overrides backend settings)")
|
914 |
system_prompt_input = gr.Textbox(label="System Prompt", lines=10, value=DEFAULT_SYSTEM_PROMPT, elem_id="system-prompt")
|
915 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
916 |
with gr.Column(scale=2):
|
917 |
gr.Markdown("## π¬ AI Assistant Chat")
|
918 |
chatbot_display = gr.Chatbot(label="AI Chat", height=500, bubble_full_width=False, avatar_images=(None))
|
@@ -940,8 +1217,8 @@ with gr.Blocks(theme=custom_theme, css=custom_css) as demo:
|
|
940 |
build_space_button = gr.Button("π Build / Update Space from Markdown", variant="primary")
|
941 |
build_status_display = gr.Textbox(label="Manual Build/Update Status", interactive=False, value="*Manual build status...*")
|
942 |
gr.Markdown("---")
|
943 |
-
refresh_status_button = gr.Button("π Refresh Runtime Status")
|
944 |
-
space_runtime_status_display = gr.Markdown("*Runtime status will appear here.*")
|
945 |
|
946 |
with gr.TabItem("π Files Preview"):
|
947 |
detected_files_preview = gr.Markdown(value="*A preview of the latest file versions will appear here.*")
|
@@ -961,6 +1238,7 @@ with gr.Blocks(theme=custom_theme, css=custom_css) as demo:
|
|
961 |
gr.Markdown("### Live Space Preview")
|
962 |
space_iframe_display = gr.HTML(value="", visible=True)
|
963 |
|
|
|
964 |
provider_select.change(update_models_dropdown, inputs=provider_select, outputs=model_select)
|
965 |
|
966 |
chat_inputs = [
|
@@ -976,10 +1254,12 @@ with gr.Blocks(theme=custom_theme, css=custom_css) as demo:
|
|
976 |
send_chat_button.click(handle_chat_submit, inputs=chat_inputs, outputs=chat_outputs)
|
977 |
chat_message_input.submit(handle_chat_submit, inputs=chat_inputs, outputs=chat_outputs)
|
978 |
|
|
|
979 |
confirm_inputs = [hf_api_key_input, owner_name_input, space_name_input, changeset_state]
|
980 |
confirm_outputs = [
|
981 |
status_output, formatted_space_output_display, detected_files_preview, download_button,
|
982 |
-
confirm_accordion, confirm_button, cancel_button, changeset_state, changeset_display
|
|
|
983 |
]
|
984 |
confirm_button.click(handle_confirm_changes, inputs=confirm_inputs, outputs=confirm_outputs)
|
985 |
|
@@ -989,12 +1269,14 @@ with gr.Blocks(theme=custom_theme, css=custom_css) as demo:
|
|
989 |
]
|
990 |
cancel_button.click(handle_cancel_changes, inputs=None, outputs=cancel_outputs)
|
991 |
|
|
|
992 |
load_space_outputs = [
|
993 |
formatted_space_output_display, detected_files_preview, status_output,
|
994 |
file_browser_dropdown, owner_name_input, space_name_input,
|
995 |
space_iframe_display, download_button, build_status_display,
|
996 |
edit_status_display, space_runtime_status_display,
|
997 |
-
changeset_state, changeset_display, confirm_accordion, confirm_button, cancel_button
|
|
|
998 |
]
|
999 |
load_space_button.click(
|
1000 |
fn=handle_load_existing_space,
|
@@ -1002,11 +1284,12 @@ with gr.Blocks(theme=custom_theme, css=custom_css) as demo:
|
|
1002 |
outputs=load_space_outputs
|
1003 |
)
|
1004 |
|
|
|
1005 |
build_outputs = [
|
1006 |
build_status_display, space_iframe_display, file_browser_dropdown,
|
1007 |
owner_name_input, space_name_input,
|
1008 |
changeset_state, changeset_display, confirm_accordion, confirm_button, cancel_button,
|
1009 |
-
formatted_space_output_display, detected_files_preview, download_button
|
1010 |
]
|
1011 |
build_inputs = [
|
1012 |
hf_api_key_input, space_name_input, owner_name_input, space_sdk_select,
|
@@ -1029,5 +1312,18 @@ with gr.Blocks(theme=custom_theme, css=custom_css) as demo:
|
|
1029 |
|
1030 |
refresh_status_button.click(fn=handle_refresh_space_status, inputs=[hf_api_key_input, owner_name_input, space_name_input], outputs=[space_runtime_status_display])
|
1031 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1032 |
if __name__ == "__main__":
|
1033 |
demo.launch(debug=False, mcp_server=True)
|
|
|
1 |
+
### File: app.py
|
2 |
+
```python
|
3 |
import gradio as gr
|
4 |
import re
|
5 |
import json
|
|
|
19 |
delete_space_file as build_logic_delete_space_file,
|
20 |
get_space_runtime_status,
|
21 |
apply_staged_changes,
|
22 |
+
duplicate_space as build_logic_duplicate_space,
|
23 |
+
list_user_spaces as build_logic_list_user_spaces,
|
24 |
+
build_logic_set_space_privacy,
|
25 |
+
build_logic_delete_space
|
26 |
)
|
27 |
|
28 |
from model_logic import (
|
|
|
75 |
|
76 |
Available commands:
|
77 |
- `CREATE_SPACE <owner>/<repo_name> --sdk <sdk> --private <true|false>`: Creates a new, empty space. SDK can be gradio, streamlit, docker, or static. Private is optional and defaults to false. Use this ONLY when the user explicitly asks to create a *new* space with a specific name. When using this action, also provide the initial file structure (e.g., app.py, README.md) using `### File:` blocks in the same response. The system will apply these files to the new space.
|
78 |
+
- `DUPLICATE_SPACE <new_owner>/<new_repo_name> --private <true|false>`: Duplicates the *currently loaded* space to a new space. Private is optional and defaults to false. This action is destructive if the target space already exists. Use this ONLY when the user explicitly asks to duplicate the space. If this action is staged, it will be the *only* action applied in that changeset.
|
79 |
- `DELETE_FILE path/to/file.ext`: Deletes a specific file from the current space.
|
80 |
- `SET_PRIVATE <true|false>`: Sets the privacy for the current space.
|
81 |
- `DELETE_SPACE`: Deletes the entire current space. THIS IS PERMANENT AND REQUIRES CAUTION. Only use this if the user explicitly and clearly asks to delete the space.
|
82 |
|
83 |
+
You can issue multiple file updates and action commands in a single response. The system will process all of them into a single change plan. Note that the `DUPLICATE_SPACE` command, if present, will override any other file or space actions in the same response.
|
84 |
|
85 |
**Current Space Context:**
|
86 |
You will be provided with the current state of the files in the Space the user is interacting with. Use this information to understand the current project structure and content before proposing changes or actions. This context will appear after the user's message, starting with "## Current Space Context:". Do NOT include this context in your response. Only generate your response based on the user's request and the formatting rules above.
|
|
|
266 |
ai_proposed_files_dict = {f["path"]: f for f in ai_proposed_files_list}
|
267 |
|
268 |
action_pattern = re.compile(r"### HF_ACTION:\s*(?P<command_line>[^\n]+)", re.MULTILINE)
|
269 |
+
# Collect all potential actions first
|
270 |
+
potential_actions = []
|
271 |
for match in action_pattern.finditer(ai_response_content):
|
272 |
try:
|
273 |
cmd_parts = shlex.split(match.group("command_line").strip())
|
274 |
if not cmd_parts: continue
|
275 |
command, args = cmd_parts[0].upper(), cmd_parts[1:]
|
276 |
+
potential_actions.append({"command": command, "args": args, "raw_line": match.group("command_line").strip()})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
277 |
except Exception as e:
|
278 |
print(f"Error parsing HF_ACTION line '{match.group('command_line').strip()}': {e}")
|
279 |
|
280 |
|
281 |
+
# Check for exclusive actions (like DUPLICATE_SPACE)
|
282 |
+
duplicate_action = next((act for act in potential_actions if act["command"] == "DUPLICATE_SPACE"), None)
|
283 |
+
|
284 |
+
if duplicate_action:
|
285 |
+
# If DUPLICATE_SPACE is present, only stage that action
|
286 |
+
cmd_parts = duplicate_action["args"]
|
287 |
+
if cmd_parts:
|
288 |
+
target_repo_id = cmd_parts[0]
|
289 |
+
private = False
|
290 |
+
if '--private' in cmd_parts:
|
291 |
+
try: private_str = cmd_parts[cmd_parts.index('--private') + 1].lower()
|
292 |
+
except IndexError: print("Warning: DUPLICATE_SPACE --private requires an argument.")
|
293 |
+
else: private = private_str == 'true'
|
294 |
+
# Source repo is the currently loaded one
|
295 |
+
source_repo_id = f"{hf_owner_name}/{hf_repo_name}" if hf_owner_name and hf_repo_name else None
|
296 |
+
|
297 |
+
if source_repo_id:
|
298 |
+
changeset.append({
|
299 |
+
"type": "DUPLICATE_SPACE",
|
300 |
+
"source_repo_id": source_repo_id,
|
301 |
+
"target_repo_id": target_repo_id,
|
302 |
+
"private": private
|
303 |
+
})
|
304 |
+
print(f"Staged exclusive DUPLICATE_SPACE action from {source_repo_id} to {target_repo_id}")
|
305 |
+
else:
|
306 |
+
print(f"Warning: Cannot stage DUPLICATE_SPACE action, no Space currently loaded.")
|
307 |
+
# Maybe add an error message to the summary?
|
308 |
+
changeset.append({"type": "Error", "message": "Cannot duplicate space, no Space currently loaded."})
|
309 |
+
|
310 |
+
else:
|
311 |
+
print(f"Warning: DUPLICATE_SPACE action requires a target repo_id.")
|
312 |
+
changeset.append({"type": "Error", "message": "DUPLICATE_SPACE action requires a target repo_id (<new_owner>/<new_repo_name>)."})
|
313 |
+
|
314 |
+
# If duplication is staged (or failed to stage due to syntax/missing source), skip other actions
|
315 |
+
if changeset:
|
316 |
+
md_summary = ["### π Proposed Changes Plan (Exclusive Action)\n"]
|
317 |
+
if changeset[0]["type"] == "DUPLICATE_SPACE":
|
318 |
+
change = changeset[0]
|
319 |
+
md_summary.append(f"- **π Duplicate Space:** Duplicate `{change.get('source_repo_id', '...')}` to `{change.get('target_repo_id', '...')}` (Private: {change.get('private', False)})")
|
320 |
+
md_summary.append("\n**Warning:** This will overwrite the target space if it exists. No other file or space actions proposed by the AI in this turn will be applied.")
|
321 |
+
else: # Handle the staging error case
|
322 |
+
md_summary.append(f"- **Error staging DUPLICATE_SPACE:** {changeset[0].get('message', 'Unknown error')}")
|
323 |
+
md_summary.append("\nNo changes were staged due to the error in the DUPLICATE_SPACE command.")
|
324 |
+
changeset = [] # Clear changeset if the duplicate action itself was malformed/blocked
|
325 |
+
|
326 |
+
return changeset, "\n".join(md_summary)
|
327 |
+
|
328 |
+
|
329 |
+
# If no exclusive action, process other actions and file changes
|
330 |
+
for action in potential_actions:
|
331 |
+
command, args = action["command"], action["args"]
|
332 |
+
|
333 |
+
if command == "CREATE_SPACE" and args:
|
334 |
+
repo_id = args[0]
|
335 |
+
sdk = "gradio"
|
336 |
+
private = False
|
337 |
+
if '--sdk' in args:
|
338 |
+
try: sdk = args[args.index('--sdk') + 1]
|
339 |
+
except IndexError: print("Warning: CREATE_SPACE --sdk requires an argument.")
|
340 |
+
if '--private' in args:
|
341 |
+
try: private_str = args[args.index('--private') + 1].lower()
|
342 |
+
except IndexError: print("Warning: CREATE_SPACE --private requires an argument.")
|
343 |
+
else: private = private_str == 'true'
|
344 |
+
changeset.append({"type": "CREATE_SPACE", "repo_id": repo_id, "sdk": sdk, "private": private})
|
345 |
+
print(f"Staged CREATE_SPACE action for {repo_id}")
|
346 |
+
|
347 |
+
elif command == "DELETE_FILE" and args:
|
348 |
+
file_path = args[0]
|
349 |
+
changeset.append({"type": "DELETE_FILE", "path": file_path})
|
350 |
+
print(f"Staged DELETE_FILE action for {file_path}")
|
351 |
+
|
352 |
+
elif command == "SET_PRIVATE" and args:
|
353 |
+
private = args[0].lower() == 'true'
|
354 |
+
changeset.append({"type": "SET_PRIVACY", "private": private, "repo_id": f"{hf_owner_name}/{hf_repo_name}"})
|
355 |
+
print(f"Staged SET_PRIVACY action for {hf_owner_name}/{hf_repo_name} to {private}")
|
356 |
+
|
357 |
+
elif command == "DELETE_SPACE":
|
358 |
+
changeset.append({"type": "DELETE_SPACE", "owner": hf_owner_name, "space_name": hf_repo_name})
|
359 |
+
print(f"Staged DELETE_SPACE action for {hf_owner_name}/{hf_repo_name}")
|
360 |
+
# Note: DUPLICATE_SPACE is handled before this loop
|
361 |
+
|
362 |
+
|
363 |
for file_info in ai_proposed_files_list:
|
364 |
filename = file_info["path"]
|
365 |
proposed_content = file_info["content"]
|
|
|
382 |
else:
|
383 |
print(f"Skipping staging create for {filename}: Proposed content is a placeholder.")
|
384 |
|
385 |
+
|
386 |
if not changeset:
|
387 |
md_summary = ["### π Proposed Changes Plan", "\nThe AI did not propose any specific changes to files or the space.\n"]
|
388 |
else:
|
|
|
418 |
|
419 |
def handle_chat_submit(user_message, chat_history, hf_api_key_input, provider_api_key_input, provider_select, model_select, system_prompt, hf_owner_name, hf_repo_name):
|
420 |
global parsed_code_blocks_state_cache
|
421 |
+
_chat_msg_in = user_message
|
422 |
_chat_hist = list(chat_history)
|
423 |
+
_chat_hist.append((user_message, None))
|
424 |
|
|
|
425 |
yield (
|
426 |
+
"",
|
427 |
+
_chat_hist,
|
428 |
+
"Initializing...",
|
429 |
+
gr.update(),
|
430 |
+
gr.update(),
|
431 |
+
gr.update(interactive=False),
|
432 |
+
[],
|
433 |
+
gr.update(value="*No changes proposed.*"),
|
434 |
+
gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
|
435 |
)
|
436 |
|
437 |
current_sys_prompt = system_prompt.strip() or DEFAULT_SYSTEM_PROMPT
|
|
|
450 |
if chunk is None: continue
|
451 |
full_bot_response_content += str(chunk)
|
452 |
_chat_hist[-1] = (user_message, full_bot_response_content)
|
|
|
453 |
yield (
|
454 |
+
gr.update(),
|
455 |
+
_chat_hist,
|
456 |
+
f"Streaming from {model_select}...",
|
457 |
+
gr.update(),
|
458 |
+
gr.update(),
|
459 |
+
gr.update(),
|
460 |
+
gr.update(),
|
461 |
+
gr.update(),
|
462 |
+
gr.update(), gr.update(), gr.update()
|
463 |
)
|
464 |
|
|
|
|
|
465 |
if full_bot_response_content.startswith("Error:") or full_bot_response_content.startswith("API HTTP Error"):
|
466 |
_status = full_bot_response_content
|
|
|
467 |
yield (
|
468 |
gr.update(), _chat_hist, _status,
|
469 |
+
gr.update(), gr.update(), gr.update(),
|
470 |
+
[], "*Error occurred, changes plan cleared.*",
|
471 |
+
gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
|
472 |
)
|
473 |
+
return
|
474 |
|
|
|
475 |
parsed_code_blocks_state_cache, proposed_filenames_in_turn = _parse_and_update_state_cache(full_bot_response_content, parsed_code_blocks_state_cache)
|
476 |
|
|
|
477 |
_formatted, _detected, _download = _generate_ui_outputs_from_cache(hf_owner_name, hf_repo_name)
|
478 |
|
|
|
479 |
staged_changeset, summary_md = generate_and_stage_changes(full_bot_response_content, parsed_code_blocks_state_cache, hf_owner_name, hf_repo_name)
|
480 |
|
|
|
481 |
if not staged_changeset:
|
482 |
+
_status = summary_md
|
483 |
yield (
|
484 |
+
gr.update(),
|
485 |
+
_chat_hist, _status,
|
486 |
+
_detected, _formatted, _download,
|
487 |
+
[], summary_md,
|
488 |
+
gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
|
489 |
)
|
490 |
else:
|
491 |
_status = "Change plan generated. Please review and confirm below."
|
492 |
yield (
|
493 |
+
gr.update(),
|
494 |
+
_chat_hist, _status,
|
495 |
+
_detected, _formatted, _download,
|
496 |
+
staged_changeset, summary_md,
|
497 |
+
gr.update(visible=True),
|
498 |
+
gr.update(visible=True),
|
499 |
+
gr.update(visible=True)
|
500 |
)
|
501 |
|
502 |
except Exception as e:
|
|
|
504 |
print(f"Error in handle_chat_submit: {e}")
|
505 |
import traceback
|
506 |
traceback.print_exc()
|
|
|
507 |
if _chat_hist:
|
|
|
508 |
try:
|
509 |
idx = next(i for i in reversed(range(len(_chat_hist))) if _chat_hist[i] and _chat_hist[i][0] == user_message)
|
510 |
current_bot_response = _chat_hist[idx][1] or ""
|
511 |
_chat_hist[idx] = (user_message, (current_bot_response + "\n\n" if current_bot_response else "") + error_msg)
|
512 |
except (StopIteration, IndexError):
|
513 |
+
_chat_hist.append((user_message, error_msg))
|
514 |
|
|
|
515 |
_formatted, _detected, _download = _generate_ui_outputs_from_cache(hf_owner_name, hf_repo_name)
|
516 |
|
|
|
517 |
yield (
|
518 |
+
gr.update(),
|
519 |
+
_chat_hist, error_msg,
|
520 |
+
_detected, _formatted, _download,
|
521 |
+
[], "*Error occurred, changes plan cleared.*",
|
522 |
+
gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
|
523 |
)
|
524 |
|
525 |
|
|
|
532 |
if not changeset:
|
533 |
return "No changes to apply.", gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(value="No changes were staged.")
|
534 |
|
535 |
+
# Check if the first action is an exclusive action like DUPLICATE_SPACE
|
536 |
+
first_action_type = changeset[0].get('type') if changeset else None
|
537 |
+
is_exclusive_action = first_action_type == 'DUPLICATE_SPACE'
|
538 |
+
|
539 |
+
if is_exclusive_action:
|
540 |
+
change = changeset[0]
|
541 |
+
if change.get("source_repo_id") and change.get("target_repo_id"):
|
542 |
+
status_message = build_logic_duplicate_space(
|
543 |
+
hf_api_key=hf_api_key,
|
544 |
+
source_repo_id=change["source_repo_id"],
|
545 |
+
target_repo_id=change["target_repo_id"],
|
546 |
+
private=change.get("private", False)
|
547 |
+
)
|
548 |
+
# After duplication, attempt to load the *new* space
|
549 |
+
_status_reload = f"{status_message} | Attempting to load the new Space [{change['target_repo_id']}]..."
|
550 |
+
yield _status_reload, gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(value="*Loading new Space...*")
|
551 |
+
|
552 |
+
# Extract new owner and space name from target_repo_id
|
553 |
+
new_owner, new_space_name = change["target_repo_id"].split('/', 1) if '/' in change["target_repo_id"] else (None, change["target_repo_id"])
|
554 |
+
if not new_owner: # Try to auto-detect owner if target was just a name
|
555 |
+
token, token_err = build_logic_get_api_token(hf_api_key)
|
556 |
+
if token_err: new_owner = None # Cannot auto-detect
|
557 |
+
else:
|
558 |
+
try: user_info = build_logic_whoami(token=token); new_owner = user_info.get('name')
|
559 |
+
except: new_owner = None
|
560 |
+
|
561 |
+
# Trigger the load logic for the new space
|
562 |
+
if new_owner and new_space_name:
|
563 |
+
# Call the load handler directly or replicate its logic
|
564 |
+
# Replicating is safer for yield chains
|
565 |
+
sdk, file_list, err_list = get_space_repository_info(hf_api_key, new_space_name, new_owner)
|
566 |
+
if err_list:
|
567 |
+
reload_error = f"Error reloading file list after duplication: {err_list}"
|
568 |
+
parsed_code_blocks_state_cache = []
|
569 |
+
else:
|
570 |
+
loaded_files = []
|
571 |
+
for file_path in file_list:
|
572 |
+
content, err_get = get_space_file_content(hf_api_key, new_space_name, new_owner, file_path)
|
573 |
+
lang = _infer_lang_from_filename(file_path)
|
574 |
+
is_binary = lang == "binary" or (err_get is not None)
|
575 |
+
code = f"[Error loading content: {err_get}]" if err_get else (content or "")
|
576 |
+
loaded_files.append({"filename": file_path, "code": code, "language": lang, "is_binary": is_binary, "is_structure_block": False})
|
577 |
+
parsed_code_blocks_state_cache = loaded_files
|
578 |
+
|
579 |
+
_formatted, _detected, _download = _generate_ui_outputs_from_cache(new_owner, new_space_name)
|
580 |
+
final_overall_status = status_message + (f" | Reload Status: {reload_error}" if reload_error else f" | Reload Status: New Space [{new_owner}/{new_space_name}] state refreshed.")
|
581 |
+
|
582 |
+
# Update UI fields to reflect the newly loaded space
|
583 |
+
owner_update = gr.update(value=new_owner)
|
584 |
+
space_update = gr.update(value=new_space_name)
|
585 |
+
file_browser_update = gr.update(visible=True, choices=sorted([f["filename"] for f in parsed_code_blocks_state_cache if not f.get("is_structure_block")] or []), value=None)
|
586 |
+
|
587 |
+
# Update iframe preview for the new space
|
588 |
+
if new_owner and new_space_name:
|
589 |
+
sub_owner = re.sub(r'[^a-z0-9\-]+', '-', new_owner.lower()).strip('-') or 'owner'
|
590 |
+
sub_repo = re.sub(r'[^a-z0-9\-]+', '-', new_space_name.lower()).strip('-') or 'space'
|
591 |
+
iframe_url = f"https://{sub_owner}-{sub_repo}{'.static.hf.space' if sdk == 'static' else '.hf.space'}"
|
592 |
+
iframe_update = gr.update(value=f'<iframe src="{iframe_url}?__theme=light&embed=true" width="100%" height="500px"></iframe>', visible=True)
|
593 |
+
else:
|
594 |
+
iframe_update = gr.update(value=None, visible=False)
|
595 |
+
|
596 |
|
597 |
+
else:
|
598 |
+
reload_error = "Cannot load new Space state: Owner or Space Name missing after duplication."
|
599 |
+
_formatted, _detected, _download = _generate_ui_outputs_from_cache(None, None) # Clear previews if new space couldn't be loaded
|
600 |
+
final_overall_status = status_message + f" | Reload Status: {reload_error}"
|
601 |
+
owner_update = gr.update() # Keep old value
|
602 |
+
space_update = gr.update() # Keep old value
|
603 |
+
file_browser_update = gr.update(visible=False, choices=[], value=None) # Clear file browser
|
604 |
+
iframe_update = gr.update(value=None, visible=False) # Hide iframe
|
605 |
|
|
|
|
|
|
|
606 |
|
|
|
|
|
|
|
|
|
|
|
607 |
else:
|
608 |
+
# Should not happen if staged_changeset exists and type is DUPLICATE_SPACE, but as safeguard
|
609 |
+
final_overall_status = "Duplicate Action Error: Invalid duplicate changeset format."
|
610 |
+
# No state change, just report error
|
611 |
+
_formatted, _detected, _download = _generate_ui_outputs_from_cache(owner_name, space_name)
|
612 |
+
owner_update = gr.update()
|
613 |
+
space_update = gr.update()
|
614 |
+
file_browser_update = gr.update()
|
615 |
+
iframe_update = gr.update()
|
616 |
+
|
617 |
+
|
618 |
+
cleared_changeset = []
|
619 |
+
# Return updates including potentially new space info and file list
|
620 |
+
yield (
|
621 |
+
final_overall_status,
|
622 |
+
gr.update(value=_formatted), gr.update(value=_detected), _download,
|
623 |
+
gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), # Hide confirm UI
|
624 |
+
cleared_changeset, gr.update(value="*No changes proposed.*"), # Clear changeset state and display
|
625 |
+
owner_update, space_update, file_browser_update, iframe_update # Update space/owner fields, file browser, iframe
|
626 |
+
)
|
627 |
|
628 |
+
else: # Not an exclusive action, proceed with standard apply_staged_changes
|
629 |
+
status_message = apply_staged_changes(hf_api_key, owner_name, space_name, changeset)
|
630 |
|
631 |
+
_status_reload = f"{status_message} | Reloading Space state..."
|
632 |
+
yield _status_reload, gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(value="*Reloading Space state...*")
|
633 |
|
634 |
+
refreshed_file_list = []
|
635 |
+
reload_error = None
|
636 |
+
repo_id_for_reload = f"{owner_name}/{space_name}" if owner_name and space_name else None
|
637 |
|
638 |
+
if repo_id_for_reload:
|
639 |
+
sdk, file_list, err_list = get_space_repository_info(hf_api_key, space_name, owner_name)
|
640 |
+
if err_list:
|
641 |
+
reload_error = f"Error reloading file list after changes: {err_list}"
|
642 |
+
parsed_code_blocks_state_cache = []
|
643 |
+
else:
|
644 |
+
refreshed_file_list = file_list
|
645 |
+
loaded_files = []
|
646 |
+
for file_path in refreshed_file_list:
|
647 |
+
content, err_get = get_space_file_content(hf_api_key, space_name, owner_name, file_path)
|
648 |
+
lang = _infer_lang_from_filename(file_path)
|
649 |
+
is_binary = lang == "binary" or (err_get is not None)
|
650 |
+
code = f"[Error loading content: {err_get}]" if err_get else (content or "")
|
651 |
+
loaded_files.append({"filename": file_path, "code": code, "language": lang, "is_binary": is_binary, "is_structure_block": False})
|
652 |
+
parsed_code_blocks_state_cache = loaded_files
|
653 |
+
|
654 |
+
# Update file browser dropdown with refreshed list
|
655 |
+
file_browser_update = gr.update(visible=True, choices=sorted(refreshed_file_list or []), value=None)
|
656 |
+
# Update iframe preview for the current space (might not have changed URL, but status may update)
|
657 |
+
if owner_name and space_name:
|
658 |
+
sub_owner = re.sub(r'[^a-z0-9\-]+', '-', owner_name.lower()).strip('-') or 'owner'
|
659 |
+
sub_repo = re.sub(r'[^a-z0-9\-]+', '-', space_name.lower()).strip('-') or 'space'
|
660 |
+
iframe_url = f"https://{sub_owner}-{sub_repo}{'.static.hf.space' if sdk == 'static' else '.hf.space'}"
|
661 |
+
iframe_update = gr.update(value=f'<iframe src="{iframe_url}?__theme=light&embed=true" width="100%" height="500px"></iframe>', visible=True)
|
662 |
+
else:
|
663 |
+
iframe_update = gr.update(value=None, visible=False)
|
664 |
+
|
665 |
+
else:
|
666 |
+
reload_error = "Cannot reload Space state: Owner or Space Name missing."
|
667 |
+
# Clear UI elements related to files if reload fails
|
668 |
+
file_browser_update = gr.update(visible=False, choices=[], value=None)
|
669 |
+
iframe_update = gr.update(value=None, visible=False)
|
670 |
|
671 |
+
|
672 |
+
_formatted, _detected, _download = _generate_ui_outputs_from_cache(owner_name, space_name)
|
673 |
+
|
674 |
+
final_overall_status = status_message + (f" | Reload Status: {reload_error}" if reload_error else " | Reload Status: Space state refreshed.")
|
675 |
+
|
676 |
+
cleared_changeset = []
|
677 |
+
|
678 |
+
# Return updated UI elements and hide confirmation UI
|
679 |
+
yield (
|
680 |
+
final_overall_status,
|
681 |
+
gr.update(value=_formatted),
|
682 |
+
gr.update(value=_detected),
|
683 |
+
_download,
|
684 |
+
gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), # Hide confirm UI
|
685 |
+
cleared_changeset, gr.update(value="*No changes proposed.*"), # Clear changeset state and display
|
686 |
+
gr.update(), gr.update(), file_browser_update, iframe_update # Keep owner/space, update file browser and iframe
|
687 |
+
)
|
688 |
|
689 |
|
690 |
def handle_cancel_changes():
|
|
|
710 |
global parsed_code_blocks_state_cache
|
711 |
_formatted_md_val, _detected_preview_val, _status_val = "*Loading files...*", "*Loading files...*", f"Loading Space: {ui_owner_name}/{ui_space_name}..."
|
712 |
_file_browser_update, _iframe_html_update, _download_btn_update = gr.update(visible=False, choices=[], value=None), gr.update(value=None, visible=False), gr.update(interactive=False, value=None)
|
713 |
+
_build_status_clear, _edit_status_clear, _runtime_status_clear = "*Manual build status...*", "*Select a file...*", "*Runtime status...*"
|
714 |
_changeset_clear = []
|
715 |
_changeset_summary_clear = "*No changes proposed.*"
|
716 |
_confirm_ui_hidden = gr.update(visible=False)
|
717 |
+
_list_spaces_display_clear = "*List of spaces will appear here.*"
|
718 |
|
719 |
+
|
720 |
+
# Initial yield to show loading state
|
721 |
+
yield (
|
|
|
|
|
|
|
|
|
|
|
|
|
722 |
gr.update(value=_formatted_md_val), gr.update(value=_detected_preview_val), gr.update(value=_status_val), _file_browser_update,
|
723 |
gr.update(value=ui_owner_name), gr.update(value=ui_space_name),
|
724 |
_iframe_html_update, _download_btn_update, gr.update(value=_build_status_clear),
|
725 |
gr.update(value=_edit_status_clear), gr.update(value=_runtime_status_clear),
|
726 |
+
_changeset_clear, gr.update(value=_changeset_summary_clear), _confirm_ui_hidden, _confirm_ui_hidden, _confirm_ui_hidden,
|
727 |
+
gr.update(), gr.update(), gr.update() # For list spaces updates
|
728 |
+
)
|
|
|
729 |
|
730 |
owner_to_use = ui_owner_name
|
731 |
+
token, token_err = build_logic_get_api_token(hf_api_key_ui)
|
732 |
+
if token_err:
|
733 |
+
_status_val = f"Load Error: {token_err}"
|
734 |
+
yield gr.update(value=_status_val), # Only update status on early error
|
735 |
+
return
|
736 |
if not owner_to_use:
|
|
|
|
|
|
|
|
|
|
|
737 |
try:
|
738 |
user_info = build_logic_whoami(token=token)
|
739 |
owner_to_use = user_info.get('name')
|
|
|
741 |
yield gr.update(value=owner_to_use), gr.update(value=f"Loading Space: {owner_to_use}/{ui_space_name} (Auto-detected owner)...") # Update owner and status
|
742 |
except Exception as e:
|
743 |
_status_val = f"Load Error: Error auto-detecting owner: {e}"
|
744 |
+
yield gr.update(value=_status_val),
|
745 |
return
|
746 |
|
747 |
if not owner_to_use or not ui_space_name:
|
|
|
751 |
|
752 |
sdk, file_list, err = get_space_repository_info(hf_api_key_ui, ui_space_name, owner_to_use)
|
753 |
|
754 |
+
# Always update owner/space inputs even on error
|
755 |
+
yield gr.update(value=owner_to_use), gr.update(value=ui_space_name),
|
|
|
756 |
|
757 |
if err:
|
758 |
_status_val = f"Load Error: {err}"
|
759 |
parsed_code_blocks_state_cache = []
|
760 |
_formatted, _detected, _download = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name)
|
761 |
yield (
|
762 |
+
gr.update(value=_formatted), gr.update(value=_detected), gr.update(value=_status_val),
|
763 |
+
gr.update(visible=False, choices=[], value=None),
|
764 |
+
gr.update(), gr.update(),
|
765 |
+
gr.update(value=None, visible=False),
|
766 |
+
_download, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
|
767 |
)
|
768 |
return
|
769 |
|
|
|
790 |
else:
|
791 |
iframe_update = gr.update(value=None, visible=False)
|
792 |
|
793 |
+
# Also fetch and display runtime status after loading
|
794 |
+
runtime_status_md = handle_refresh_space_status(hf_api_key_ui, owner_to_use, ui_space_name)
|
795 |
+
|
796 |
+
|
797 |
# Final yield after success
|
798 |
yield (
|
799 |
+
gr.update(value=_formatted), gr.update(value=_detected), gr.update(value=_status_val),
|
800 |
+
file_browser_update,
|
801 |
+
gr.update(), gr.update(),
|
802 |
+
iframe_update,
|
803 |
+
_download, gr.update(), gr.update(), gr.update(value=runtime_status_md), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
|
804 |
)
|
805 |
|
806 |
def handle_build_space_button(hf_api_key_ui, ui_space_name_part, ui_owner_name_part, space_sdk_ui, is_private_ui, formatted_markdown_content):
|
|
|
811 |
_changeset_summary_clear = "*Manual build initiated, changes plan cleared.*"
|
812 |
_confirm_ui_hidden = gr.update(visible=False)
|
813 |
|
814 |
+
yield (_build_status, _iframe_html, _file_browser_update, gr.update(value=ui_owner_name_part), gr.update(value=ui_space_name_part),
|
815 |
+
_changeset_clear, gr.update(value=_changeset_summary_clear), _confirm_ui_hidden, _confirm_ui_hidden, _confirm_ui_hidden,
|
816 |
+
gr.update(), gr.update(), gr.update())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
817 |
|
818 |
|
819 |
if not ui_space_name_part or "/" in ui_space_name_part:
|
|
|
832 |
|
833 |
if not manual_changeset:
|
834 |
_build_status = "Build Error: No target space specified or no files parsed from markdown."
|
835 |
+
yield gr.update(value=_build_status),
|
836 |
return
|
837 |
|
|
|
838 |
result_message = apply_staged_changes(hf_api_key_ui, ui_owner_name_part, ui_space_name_part, manual_changeset)
|
839 |
_build_status = f"Manual Build/Update Result: {result_message}"
|
840 |
|
841 |
owner_to_use = ui_owner_name_part
|
842 |
space_to_use = ui_space_name_part
|
|
|
843 |
_formatted_md = formatted_markdown_content
|
844 |
_detected_preview_val = "*Loading files after build...*"
|
845 |
_download_btn_update = gr.update(interactive=False, value=None)
|
846 |
|
|
|
847 |
yield (
|
848 |
+
gr.update(value=_build_status), _iframe_html, _file_browser_update,
|
849 |
+
gr.update(value=owner_to_use), gr.update(value=space_to_use),
|
850 |
+
_changeset_clear, gr.update(value=_changeset_summary_clear), _confirm_ui_hidden, _confirm_ui_hidden, _confirm_ui_hidden,
|
851 |
+
gr.update(value=_formatted_md), gr.update(value=_detected_preview_val), _download_btn_update
|
852 |
)
|
853 |
|
|
|
854 |
sdk_built, file_list, err_list = get_space_repository_info(hf_api_key_ui, space_to_use, owner_to_use)
|
855 |
|
856 |
if err_list:
|
|
|
872 |
|
873 |
if owner_to_use and space_to_use:
|
874 |
sub_owner = re.sub(r'[^a-z0-9\-]+', '-', owner_to_use.lower()).strip('-') or 'owner'
|
875 |
+
sub_repo = re.sub(r'[^a-z0-9\-]+', '-', space_to_use.lower()).strip('-') or 'space'
|
876 |
iframe_url = f"https://{sub_owner}-{sub_repo}{'.static.hf.space' if sdk_built == 'static' else '.hf.space'}"
|
877 |
_iframe_html_update = gr.update(value=f'<iframe src="{iframe_url}?__theme=light&embed=true" width="100%" height="700px"></iframe>', visible=True)
|
878 |
else:
|
879 |
_iframe_html_update = gr.update(value=None, visible=False)
|
880 |
|
881 |
+
_formatted_md, _detected_preview, _download = _generate_ui_outputs_from_cache(owner_to_use, space_to_use)
|
882 |
|
883 |
+
# Also fetch and display runtime status after build
|
884 |
+
runtime_status_md = handle_refresh_space_status(hf_api_key_ui, owner_to_use, space_to_use)
|
885 |
|
886 |
|
|
|
887 |
yield (
|
888 |
+
gr.update(value=_build_status), _iframe_html_update, _file_browser_update,
|
889 |
+
gr.update(value=owner_to_use), gr.update(value=space_to_use),
|
890 |
+
_changeset_clear, gr.update(value=_changeset_summary_clear), _confirm_ui_hidden, _confirm_ui_hidden, _confirm_ui_hidden,
|
891 |
+
gr.update(value=_formatted_md), gr.update(value=_detected_preview), _download, gr.update(value=runtime_status_md)
|
892 |
)
|
893 |
|
894 |
|
|
|
992 |
log_link = status_details.get('full_log_link')
|
993 |
if log_link and log_link != "#": md += f"- [View Full Logs]({log_link})\n"
|
994 |
|
995 |
+
# Get general repo info as well
|
996 |
+
sdk, file_list, repo_info_err = get_space_repository_info(hf_api_key_ui, ui_space_name, ui_owner_name)
|
997 |
+
if repo_info_err:
|
998 |
+
md += f"- **Repo Info Error:** {repo_info_err}\n"
|
999 |
+
else:
|
1000 |
+
md += f"- **SDK:** `{sdk or 'N/A'}`\n"
|
1001 |
+
md += f"- **File Count:** `{len(file_list) if file_list is not None else 'N/A'}`\n"
|
1002 |
+
# Add more repo info if needed from get_space_repository_info result
|
1003 |
+
|
1004 |
+
return md
|
1005 |
+
|
1006 |
+
def handle_list_spaces(hf_api_key_ui, ui_owner_name):
|
1007 |
+
token, token_err = build_logic_get_api_token(hf_api_key_ui)
|
1008 |
+
if token_err:
|
1009 |
+
return f"**List Spaces Error:** {token_err}"
|
1010 |
+
|
1011 |
+
owner_to_list = ui_owner_name
|
1012 |
+
if not owner_to_list:
|
1013 |
+
try:
|
1014 |
+
user_info = build_logic_whoami(token=token)
|
1015 |
+
owner_to_list = user_info.get('name')
|
1016 |
+
if not owner_to_list: raise Exception("Could not find user name from token.")
|
1017 |
+
except Exception as e:
|
1018 |
+
return f"**List Spaces Error:** Error auto-detecting owner: {e}. Please specify Owner field."
|
1019 |
+
|
1020 |
+
if not owner_to_list:
|
1021 |
+
return "**List Spaces Error:** Owner could not be determined. Please specify it in the Owner field."
|
1022 |
+
|
1023 |
+
spaces_list, err = build_logic_list_user_spaces(hf_api_key=token, owner=owner_to_list)
|
1024 |
+
|
1025 |
+
if err:
|
1026 |
+
return f"**List Spaces Error:** {err}"
|
1027 |
+
|
1028 |
+
if not spaces_list:
|
1029 |
+
return f"*No spaces found for user/org `{owner_to_list}`.*"
|
1030 |
+
|
1031 |
+
md = f"### Spaces for `{owner_to_list}`\n"
|
1032 |
+
for space in sorted(spaces_list):
|
1033 |
+
md += f"- `{space}`\n"
|
1034 |
+
|
1035 |
return md
|
1036 |
|
1037 |
+
def handle_manual_duplicate_space(hf_api_key_ui, source_owner, source_space_name, target_owner, target_space_name, target_private):
|
1038 |
+
if not source_owner or not source_space_name:
|
1039 |
+
return "Duplicate Error: Please load a Space first to duplicate.", gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
|
1040 |
+
if not target_owner or not target_space_name:
|
1041 |
+
return "Duplicate Error: Target Owner and Target Space Name are required.", gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
|
1042 |
+
if "/" in target_space_name:
|
1043 |
+
return "Duplicate Error: Target Space Name should not contain '/'. Use Target Owner field for the owner part.", gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
|
1044 |
+
|
1045 |
+
|
1046 |
+
source_repo_id = f"{source_owner}/{source_space_name}"
|
1047 |
+
target_repo_id = f"{target_owner}/{target_space_name}"
|
1048 |
+
|
1049 |
+
status_msg = f"Attempting to duplicate `{source_repo_id}` to `{target_repo_id}`..."
|
1050 |
+
yield status_msg, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update() # Update status immediately
|
1051 |
+
|
1052 |
+
result_message = build_logic_duplicate_space(hf_api_key_ui, source_repo_id, target_repo_id, target_private)
|
1053 |
+
status_msg = f"Duplication Result: {result_message}"
|
1054 |
+
|
1055 |
+
# Attempt to load the new space after duplication attempt
|
1056 |
+
_status_reload = f"{status_msg} | Attempting to load the new Space [{target_repo_id}]..."
|
1057 |
+
yield _status_reload, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
|
1058 |
+
|
1059 |
+
# Replicate load logic for the new space
|
1060 |
+
# Note: This replicates parts of handle_load_existing_space, could refactor common parts
|
1061 |
+
new_owner = target_owner # Use the provided target owner
|
1062 |
+
new_space_name = target_space_name # Use the provided target space name
|
1063 |
+
|
1064 |
+
sdk, file_list, err_list = get_space_repository_info(hf_api_key_ui, new_space_name, new_owner)
|
1065 |
+
|
1066 |
+
if err_list:
|
1067 |
+
reload_error = f"Error reloading file list after duplication: {err_list}"
|
1068 |
+
global parsed_code_blocks_state_cache
|
1069 |
+
parsed_code_blocks_state_cache = []
|
1070 |
+
_file_browser_update = gr.update(visible=False, choices=[], value=None)
|
1071 |
+
_iframe_html_update = gr.update(value=None, visible=False)
|
1072 |
+
else:
|
1073 |
+
loaded_files = []
|
1074 |
+
for file_path in file_list:
|
1075 |
+
content, err_get = get_space_file_content(hf_api_key_ui, new_space_name, new_owner, file_path)
|
1076 |
+
lang = _infer_lang_from_filename(file_path)
|
1077 |
+
is_binary = lang == "binary" or (err_get is not None)
|
1078 |
+
code = f"[Error loading content: {err_get}]" if err_get else (content or "")
|
1079 |
+
loaded_files.append({"filename": file_path, "code": code, "language": lang, "is_binary": is_binary, "is_structure_block": False})
|
1080 |
+
global parsed_code_blocks_state_cache
|
1081 |
+
parsed_code_blocks_state_cache = loaded_files
|
1082 |
+
|
1083 |
+
_file_browser_update = gr.update(visible=True, choices=sorted([f["filename"] for f in parsed_code_blocks_state_cache if not f.get("is_structure_block")] or []), value=None)
|
1084 |
+
if new_owner and new_space_name:
|
1085 |
+
sub_owner = re.sub(r'[^a-z0-9\-]+', '-', new_owner.lower()).strip('-') or 'owner'
|
1086 |
+
sub_repo = re.sub(r'[^a-z0-9\-]+', '-', new_space_name.lower()).strip('-') or 'space'
|
1087 |
+
iframe_url = f"https://{sub_owner}-{sub_repo}{'.static.hf.space' if sdk == 'static' else '.hf.space'}"
|
1088 |
+
_iframe_html_update = gr.update(value=f'<iframe src="{iframe_url}?__theme=light&embed=true" width="100%" height="500px"></iframe>', visible=True)
|
1089 |
+
else:
|
1090 |
+
_iframe_html_update = gr.update(value=None, visible=False)
|
1091 |
+
|
1092 |
+
|
1093 |
+
_formatted, _detected, _download = _generate_ui_outputs_from_cache(new_owner, new_space_name)
|
1094 |
+
final_overall_status = status_msg + (f" | Reload Status: {reload_error}" if reload_error else f" | Reload Status: New Space [{new_owner}/{new_space_name}] state refreshed.")
|
1095 |
+
|
1096 |
+
# Fetch runtime status for the new space
|
1097 |
+
runtime_status_md = handle_refresh_space_status(hf_api_key_ui, new_owner, new_space_name)
|
1098 |
+
|
1099 |
+
# Update UI fields to reflect the newly loaded space
|
1100 |
+
owner_update = gr.update(value=new_owner)
|
1101 |
+
space_update = gr.update(value=new_space_name)
|
1102 |
+
|
1103 |
+
|
1104 |
+
return (
|
1105 |
+
final_overall_status,
|
1106 |
+
owner_update, space_update,
|
1107 |
+
_formatted, _detected, _download,
|
1108 |
+
_file_browser_update, _iframe_html_update, gr.update(value=runtime_status_md)
|
1109 |
+
)
|
1110 |
+
|
1111 |
|
1112 |
custom_theme = gr.themes.Base(primary_hue="teal", secondary_hue="purple", neutral_hue="zinc", text_size="sm", spacing_size="md", radius_size="sm", font=["System UI", "sans-serif"])
|
1113 |
custom_css = """
|
|
|
1175 |
provider_api_key_input = gr.Textbox(label="Model Provider API Key (Optional)", type="password", placeholder="sk_... (overrides backend settings)")
|
1176 |
system_prompt_input = gr.Textbox(label="System Prompt", lines=10, value=DEFAULT_SYSTEM_PROMPT, elem_id="system-prompt")
|
1177 |
|
1178 |
+
with gr.Accordion("ποΈ Space Management", open=True):
|
1179 |
+
gr.Markdown("### Manual Actions")
|
1180 |
+
with gr.Group():
|
1181 |
+
gr.Markdown("Duplicate Current Space To:")
|
1182 |
+
target_owner_input = gr.Textbox(label="Target Owner Name", placeholder="e.g., new-username")
|
1183 |
+
target_space_name_input = gr.Textbox(label="Target Space Name", placeholder="e.g., new-space")
|
1184 |
+
target_private_checkbox = gr.Checkbox(label="Make Target Private", value=False)
|
1185 |
+
duplicate_space_button = gr.Button("π Duplicate Space", variant="secondary")
|
1186 |
+
|
1187 |
+
gr.Markdown("---")
|
1188 |
+
gr.Markdown("### List Spaces")
|
1189 |
+
list_spaces_button = gr.Button("π List My Spaces", variant="secondary")
|
1190 |
+
list_spaces_display = gr.Markdown("*List of spaces will appear here.*")
|
1191 |
+
|
1192 |
+
|
1193 |
with gr.Column(scale=2):
|
1194 |
gr.Markdown("## π¬ AI Assistant Chat")
|
1195 |
chatbot_display = gr.Chatbot(label="AI Chat", height=500, bubble_full_width=False, avatar_images=(None))
|
|
|
1217 |
build_space_button = gr.Button("π Build / Update Space from Markdown", variant="primary")
|
1218 |
build_status_display = gr.Textbox(label="Manual Build/Update Status", interactive=False, value="*Manual build status...*")
|
1219 |
gr.Markdown("---")
|
1220 |
+
refresh_status_button = gr.Button("π Refresh Runtime/Repo Status")
|
1221 |
+
space_runtime_status_display = gr.Markdown("*Runtime/Repo status will appear here.*")
|
1222 |
|
1223 |
with gr.TabItem("π Files Preview"):
|
1224 |
detected_files_preview = gr.Markdown(value="*A preview of the latest file versions will appear here.*")
|
|
|
1238 |
gr.Markdown("### Live Space Preview")
|
1239 |
space_iframe_display = gr.HTML(value="", visible=True)
|
1240 |
|
1241 |
+
|
1242 |
provider_select.change(update_models_dropdown, inputs=provider_select, outputs=model_select)
|
1243 |
|
1244 |
chat_inputs = [
|
|
|
1254 |
send_chat_button.click(handle_chat_submit, inputs=chat_inputs, outputs=chat_outputs)
|
1255 |
chat_message_input.submit(handle_chat_submit, inputs=chat_inputs, outputs=chat_outputs)
|
1256 |
|
1257 |
+
# Note: handle_confirm_changes yields multiple sets of outputs for intermediate steps
|
1258 |
confirm_inputs = [hf_api_key_input, owner_name_input, space_name_input, changeset_state]
|
1259 |
confirm_outputs = [
|
1260 |
status_output, formatted_space_output_display, detected_files_preview, download_button,
|
1261 |
+
confirm_accordion, confirm_button, cancel_button, changeset_state, changeset_display,
|
1262 |
+
owner_name_input, space_name_input, file_browser_dropdown, space_iframe_display # Added outputs for DUPLICATE_SPACE load
|
1263 |
]
|
1264 |
confirm_button.click(handle_confirm_changes, inputs=confirm_inputs, outputs=confirm_outputs)
|
1265 |
|
|
|
1269 |
]
|
1270 |
cancel_button.click(handle_cancel_changes, inputs=None, outputs=cancel_outputs)
|
1271 |
|
1272 |
+
# Note: handle_load_existing_space yields multiple sets of outputs for intermediate steps
|
1273 |
load_space_outputs = [
|
1274 |
formatted_space_output_display, detected_files_preview, status_output,
|
1275 |
file_browser_dropdown, owner_name_input, space_name_input,
|
1276 |
space_iframe_display, download_button, build_status_display,
|
1277 |
edit_status_display, space_runtime_status_display,
|
1278 |
+
changeset_state, changeset_display, confirm_accordion, confirm_button, cancel_button,
|
1279 |
+
target_owner_input, target_space_name_input, target_private_checkbox # Added outputs for List Spaces
|
1280 |
]
|
1281 |
load_space_button.click(
|
1282 |
fn=handle_load_existing_space,
|
|
|
1284 |
outputs=load_space_outputs
|
1285 |
)
|
1286 |
|
1287 |
+
# Note: handle_build_space_button yields multiple sets of outputs for intermediate steps
|
1288 |
build_outputs = [
|
1289 |
build_status_display, space_iframe_display, file_browser_dropdown,
|
1290 |
owner_name_input, space_name_input,
|
1291 |
changeset_state, changeset_display, confirm_accordion, confirm_button, cancel_button,
|
1292 |
+
formatted_space_output_display, detected_files_preview, download_button, space_runtime_status_display # Added runtime status output
|
1293 |
]
|
1294 |
build_inputs = [
|
1295 |
hf_api_key_input, space_name_input, owner_name_input, space_sdk_select,
|
|
|
1312 |
|
1313 |
refresh_status_button.click(fn=handle_refresh_space_status, inputs=[hf_api_key_input, owner_name_input, space_name_input], outputs=[space_runtime_status_display])
|
1314 |
|
1315 |
+
# Manual Duplicate Space Button logic - Note it yields
|
1316 |
+
manual_duplicate_inputs = [hf_api_key_input, owner_name_input, space_name_input, target_owner_input, target_space_name_input, target_private_checkbox]
|
1317 |
+
manual_duplicate_outputs = [
|
1318 |
+
status_output, owner_name_input, space_name_input,
|
1319 |
+
formatted_space_output_display, detected_files_preview, download_button,
|
1320 |
+
file_browser_dropdown, space_iframe_display, space_runtime_status_display # Outputs needed after loading the new space
|
1321 |
+
]
|
1322 |
+
duplicate_space_button.click(fn=handle_manual_duplicate_space, inputs=manual_duplicate_inputs, outputs=manual_duplicate_outputs)
|
1323 |
+
|
1324 |
+
# List Spaces Button logic
|
1325 |
+
list_spaces_button.click(fn=handle_list_spaces, inputs=[hf_api_key_input, owner_name_input], outputs=[list_spaces_display])
|
1326 |
+
|
1327 |
+
|
1328 |
if __name__ == "__main__":
|
1329 |
demo.launch(debug=False, mcp_server=True)
|