| from __future__ import annotations |
|
|
| import json |
| from pathlib import Path |
|
|
| import gradio as gr |
|
|
| try: |
| from .repo_ops import DEFAULT_REPO_ID, allocate_next_task_id, create_dataset_pr, list_existing_task_ids, load_hf_token |
| from .validator import ( |
| DOMAINS, |
| PreparedSubmission, |
| SubmissionMetadata, |
| ValidationError, |
| build_public_report, |
| cleanup_work_dir, |
| normalize_domain_token, |
| validate_and_prepare_submission, |
| ) |
| except ImportError: |
| from repo_ops import DEFAULT_REPO_ID, allocate_next_task_id, create_dataset_pr, list_existing_task_ids, load_hf_token |
| from validator import ( |
| DOMAINS, |
| PreparedSubmission, |
| SubmissionMetadata, |
| ValidationError, |
| build_public_report, |
| cleanup_work_dir, |
| normalize_domain_token, |
| validate_and_prepare_submission, |
| ) |
|
|
|
|
| SPACE_TITLE = 'ResearchClawBench Task Submission' |
| GITHUB_REPO_URL = 'https://github.com/InternScience/ResearchClawBench' |
| DATASET_URL = f'https://huggingface.co/datasets/{DEFAULT_REPO_ID}' |
| SPACE_URL = 'https://huggingface.co/spaces/InternScience/ResearchClawBench-Task-Submit' |
|
|
| CSS = """ |
| @import url('https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&display=swap'); |
| |
| :root { |
| --page-text: #0f172a; |
| --page-muted: #526075; |
| --page-line: rgba(15, 23, 42, 0.12); |
| --page-surface: rgba(255, 255, 255, 0.78); |
| --page-surface-strong: #ffffff; |
| } |
| |
| body { |
| background: |
| radial-gradient(circle at top left, rgba(54, 107, 245, 0.12), transparent 34%), |
| radial-gradient(circle at top right, rgba(15, 118, 110, 0.08), transparent 28%), |
| linear-gradient(180deg, #f8fafc 0%, #f3f6fb 55%, #f6f8fb 100%); |
| color: var(--page-text); |
| } |
| |
| body, |
| button, |
| input, |
| textarea { |
| font-family: 'Manrope', 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif !important; |
| } |
| |
| .gradio-container { |
| max-width: 1220px !important; |
| margin: 0 auto !important; |
| padding: 34px 28px 56px !important; |
| --block-background-fill: transparent; |
| --block-border-width: 0px; |
| --block-border-color: transparent; |
| --block-label-background-fill: transparent; |
| --block-label-border-width: 0px; |
| --panel-background-fill: transparent; |
| --panel-border-width: 0px; |
| --panel-border-color: transparent; |
| --background-fill-secondary: transparent; |
| --body-background-fill: transparent; |
| } |
| |
| .page-shell { |
| margin-top: 26px; |
| padding: 30px 34px 34px; |
| background: #ffffff; |
| border: 1px solid rgba(15, 23, 42, 0.08); |
| border-radius: 22px; |
| box-shadow: 0 18px 48px rgba(15, 23, 42, 0.05); |
| } |
| |
| .hero { |
| padding: 38px 42px 34px; |
| border-radius: 24px; |
| color: #f8fbff; |
| background: |
| radial-gradient(circle at 14% 18%, rgba(255, 255, 255, 0.16), transparent 18%), |
| linear-gradient(135deg, #0f274d 0%, #133c7c 46%, #124f75 100%); |
| box-shadow: 0 26px 60px rgba(15, 39, 77, 0.18); |
| } |
| |
| .hero h1 { |
| margin: 0; |
| font-size: 2.4rem; |
| line-height: 1.02; |
| letter-spacing: -0.04em; |
| color: #f8fbff !important; |
| text-shadow: 0 1px 12px rgba(0, 0, 0, 0.14); |
| } |
| |
| .hero-copy { |
| margin-top: 16px; |
| max-width: 860px; |
| font-size: 1.04rem; |
| line-height: 1.72; |
| color: rgba(248, 251, 255, 0.9) !important; |
| } |
| |
| .hero-links { |
| display: flex; |
| gap: 14px; |
| flex-wrap: wrap; |
| margin-top: 22px; |
| } |
| |
| .hero-links a { |
| color: #f8fbff !important; |
| text-decoration: none; |
| font-weight: 700; |
| letter-spacing: -0.01em; |
| } |
| |
| .hero-links a:hover { |
| text-decoration: underline; |
| } |
| |
| .hero-meta { |
| margin-top: 18px; |
| font-size: 0.93rem; |
| color: rgba(248, 251, 255, 0.72) !important; |
| } |
| |
| .section-row { |
| margin-top: 30px; |
| } |
| |
| .section-row, |
| .section-row > div, |
| .section-copy, |
| .section-copy > div, |
| .main-form, |
| .side-notes { |
| background: transparent !important; |
| border: 0 !important; |
| box-shadow: none !important; |
| } |
| |
| .section-copy h2 { |
| margin: 0 0 10px; |
| font-size: 1.2rem; |
| letter-spacing: -0.03em; |
| } |
| |
| .section-copy h3 { |
| margin: 24px 0 8px; |
| font-size: 1rem; |
| } |
| |
| .section-copy p, |
| .section-copy li { |
| color: #5a667a; |
| line-height: 1.72; |
| } |
| |
| .section-copy ul, |
| .section-copy ol { |
| margin: 10px 0 0; |
| padding-left: 1.2rem; |
| } |
| |
| .section-copy code { |
| font-size: 0.95em; |
| } |
| |
| .section-copy .prose { |
| max-width: 100%; |
| } |
| |
| .subtle-block { |
| padding-bottom: 22px; |
| border-bottom: 1px solid var(--page-line); |
| } |
| |
| .section-copy .prose, |
| .section-copy .prose *, |
| .section-copy .md, |
| .section-copy .md *, |
| .section-copy .markdown, |
| .section-copy .markdown * { |
| background: transparent !important; |
| } |
| |
| .main-form { |
| padding-right: 14px; |
| } |
| |
| .side-notes { |
| padding-left: 10px; |
| } |
| |
| .caption { |
| margin-top: 4px; |
| color: var(--page-muted); |
| font-size: 0.93rem; |
| line-height: 1.6; |
| } |
| |
| .field-label { |
| margin: 18px 0 8px; |
| color: var(--page-text); |
| font-size: 0.95rem; |
| font-weight: 700; |
| letter-spacing: -0.01em; |
| } |
| |
| .results-shell { |
| margin-top: 26px; |
| padding-top: 22px; |
| border-top: 1px solid var(--page-line); |
| } |
| |
| .action-row { |
| margin-top: 10px; |
| } |
| |
| .upload-row { |
| margin-top: 18px; |
| margin-bottom: 10px; |
| } |
| |
| .upload-button button { |
| border-radius: 12px !important; |
| min-height: 48px !important; |
| padding: 0 18px !important; |
| background: #ffffff !important; |
| color: var(--page-text) !important; |
| border: 1px solid rgba(19, 70, 162, 0.16) !important; |
| box-shadow: 0 8px 22px rgba(15, 23, 42, 0.04) !important; |
| } |
| |
| .upload-status { |
| padding-top: 10px; |
| } |
| |
| .upload-status p { |
| margin: 0 !important; |
| color: var(--page-muted) !important; |
| } |
| |
| .primary-button button, |
| .secondary-button button { |
| border-radius: 12px !important; |
| min-height: 48px !important; |
| font-weight: 700 !important; |
| letter-spacing: -0.01em; |
| } |
| |
| .primary-button button { |
| background: linear-gradient(135deg, #1346a2 0%, #155eef 100%) !important; |
| box-shadow: 0 16px 32px rgba(21, 94, 239, 0.2) !important; |
| } |
| |
| .secondary-button button { |
| background: var(--page-surface-strong) !important; |
| color: var(--page-text) !important; |
| border: 1px solid rgba(15, 23, 42, 0.12) !important; |
| } |
| |
| .gradio-container .block, |
| .gradio-container .gr-box, |
| .gradio-container .gr-form, |
| .gradio-container .gr-group, |
| .gradio-container .form, |
| .gradio-container .input-container, |
| .gradio-container .wrap, |
| .gradio-container .row, |
| .gradio-container .column, |
| .gradio-container fieldset { |
| background: transparent !important; |
| box-shadow: none !important; |
| border-color: transparent !important; |
| } |
| |
| .gradio-container input:not([type="checkbox"]), |
| .gradio-container textarea, |
| .gradio-container button[aria-haspopup="listbox"], |
| .gradio-container button[role="listbox"], |
| .gradio-container .wrap:has(input:not([type="checkbox"])), |
| .gradio-container .wrap:has(textarea), |
| .gradio-container .wrap:has(button[aria-haspopup="listbox"]), |
| .gradio-container .wrap:has(button[role="listbox"]), |
| .gradio-container .wrap:has(select), |
| .gradio-container .input-container:has(input:not([type="checkbox"])), |
| .gradio-container .input-container:has(textarea), |
| .gradio-container .input-container:has(button[aria-haspopup="listbox"]), |
| .gradio-container .input-container:has(button[role="listbox"]), |
| .gradio-container input:not([type="checkbox"]), |
| .gradio-container textarea { |
| background: var(--page-surface-strong) !important; |
| border: 1px solid rgba(19, 70, 162, 0.16) !important; |
| border-radius: 10px !important; |
| box-shadow: 0 1px 0 rgba(15, 23, 42, 0.02), 0 8px 22px rgba(15, 23, 42, 0.04) !important; |
| } |
| |
| .gradio-container .block, |
| .gradio-container .wrap, |
| .gradio-container .gr-box, |
| .gradio-container .gr-form, |
| .gradio-container .gr-panel, |
| .gradio-container .gr-group, |
| .gradio-container .form, |
| .gradio-container .input-container, |
| .gradio-container .wrap-inner { |
| overflow: visible !important; |
| } |
| |
| .gradio-container label, |
| .gradio-container .label-wrap, |
| .gradio-container .caption-label { |
| color: var(--page-text) !important; |
| } |
| |
| .link-list a { |
| color: #1346a2; |
| text-decoration: none; |
| font-weight: 600; |
| } |
| |
| .link-list a:hover { |
| text-decoration: underline; |
| } |
| |
| @media (max-width: 900px) { |
| .gradio-container { |
| padding: 22px 16px 42px !important; |
| } |
| |
| .hero { |
| padding: 28px 24px 26px; |
| border-radius: 20px; |
| } |
| |
| .hero h1 { |
| font-size: 2rem; |
| } |
| |
| .main-form, |
| .side-notes { |
| padding-right: 0; |
| padding-left: 0; |
| } |
| } |
| """ |
|
|
|
|
| def build_hero_html() -> str: |
| return f""" |
| <section class="hero"> |
| <h1>{SPACE_TITLE}</h1> |
| <p class="hero-copy"> |
| Submit a new ResearchClawBench task as a single ZIP archive. This Space validates the full task |
| structure, checks JSON fields and referenced paths, allocates the next available task ID, and then |
| opens a PR against the official Hugging Face dataset for maintainer review. |
| </p> |
| <div class="hero-links"> |
| <a href="{GITHUB_REPO_URL}" target="_blank">GitHub Repository</a> |
| <a href="{DATASET_URL}" target="_blank">Hugging Face Dataset</a> |
| <a href="{SPACE_URL}" target="_blank">Space Repository</a> |
| </div> |
| <div class="hero-meta"> |
| ZIP upload only · full task-format validation · PR to dataset repo after passing checks |
| </div> |
| </section> |
| """ |
|
|
|
|
| def field_label_html(text: str) -> str: |
| return f'<div class="field-label">{text}</div>' |
|
|
|
|
| def submission_guide_markdown() -> str: |
| return """ |
| ## Before You Upload |
| |
| 1. Put exactly one task directory at the top level of the ZIP. |
| 2. Make sure the directory contains `task_info.json`, `data/`, `related_work/`, and `target_study/`. |
| 3. Keep every data reference inside `task_info.json` in the `./data/...` format. |
| 4. Make sure every checklist image path points to `target_study/images/...`. |
| 5. Ensure that uploaded files can be redistributed through Hugging Face before submitting. |
| |
| ## Expected ZIP Layout |
| |
| ```text |
| your_submission.zip |
| └── any_folder_name/ |
| ├── task_info.json |
| ├── data/ |
| ├── related_work/ |
| └── target_study/ |
| ├── checklist.json |
| ├── paper.pdf |
| └── images/ |
| ``` |
| |
| ## What The Space Checks |
| |
| - top-level folder structure and missing or extra files |
| - `task_info.json` and `checklist.json` parseability and required keys |
| - file naming conventions such as `related_work/paper_000.pdf` |
| - whether declared data paths actually exist |
| - whether image references actually exist |
| - whether old source paths or stale `/tasks/...` references remain in descriptions |
| |
| Example task in GitHub: |
| [tasks/Astronomy_000](https://github.com/InternScience/ResearchClawBench/tree/main/tasks/Astronomy_000) |
| """ |
|
|
|
|
| def final_task_help_html() -> str: |
| return ( |
| '<div class="caption">' |
| 'The final task ID is assigned automatically after the Space scans existing <code>tasks/</code> folders. ' |
| 'You do not need to choose the numeric suffix yourself. The selected domain becomes the prefix, and if the ' |
| 'custom field is filled, it overrides the suggested domain.' |
| '</div>' |
| ) |
|
|
|
|
| def resolve_domain(selected_domain: str, custom_domain: str) -> str: |
| raw_value = (custom_domain or '').strip() or (selected_domain or '').strip() |
| normalized = normalize_domain_token(raw_value) |
| if not normalized: |
| raise ValidationError('Please select a suggested domain or provide a custom domain.') |
| return normalized |
|
|
|
|
| def handle_archive_upload(archive_path: str | None): |
| if not archive_path: |
| return '', 'No ZIP file selected yet.' |
| filename = Path(archive_path).name |
| return archive_path, f'Selected ZIP: `{filename}`' |
|
|
|
|
| def build_validation_markdown(prepared: PreparedSubmission) -> str: |
| metadata = prepared.metadata |
| return '\n'.join([ |
| '## Validation passed', |
| '', |
| f'- Final task ID: `{prepared.assigned_task_id}`', |
| '- This is the folder name that will be created under `tasks/` in the dataset repo.', |
| f'- Domain token used for allocation: `{metadata.domain}`', |
| f'- Submitter: `{metadata.submitter}`', |
| f'- Archive file count: `{prepared.archive_stats.file_count}`', |
| f'- Archive total bytes: `{prepared.archive_stats.total_bytes}`', |
| '', |
| 'You can now create a PR to the Hugging Face dataset repo.', |
| ]) |
|
|
|
|
| def build_failure_markdown(message: str) -> str: |
| items = [line.strip() for line in message.splitlines() if line.strip()] |
| bullets = '\n'.join(f'- {item}' for item in items) if items else '- Unknown validation error' |
| return f'## Validation failed\n\n{bullets}' |
|
|
|
|
| def validate_submission( |
| archive_path: str, |
| suggested_domain: str, |
| custom_domain: str, |
| submitter: str, |
| email: str, |
| paper_title: str, |
| paper_url: str, |
| notes: str, |
| current_state: dict | None, |
| ): |
| if current_state: |
| cleanup_work_dir(current_state.get('work_dir')) |
|
|
| if not archive_path: |
| return None, '', '## Validation failed\n\n- Please upload a zip file.', '{}', gr.update(interactive=False), '' |
|
|
| domain = resolve_domain(suggested_domain, custom_domain) |
| metadata = SubmissionMetadata( |
| domain=domain, |
| submitter=submitter, |
| email=email, |
| paper_title=paper_title, |
| paper_url=paper_url, |
| notes=notes or '', |
| ) |
|
|
| try: |
| existing_ids = list_existing_task_ids(repo_id=DEFAULT_REPO_ID, token=load_hf_token()) |
| assigned_task_id = allocate_next_task_id(domain, existing_ids) |
| prepared = validate_and_prepare_submission(archive_path, metadata, assigned_task_id) |
| pr_ready = bool(load_hf_token()) |
| return ( |
| prepared.to_state(), |
| prepared.assigned_task_id, |
| build_validation_markdown(prepared), |
| json.dumps(build_public_report(prepared), indent=2, ensure_ascii=False), |
| gr.update(interactive=pr_ready), |
| '' if pr_ready else 'Validation passed, but PR creation is disabled until a write token is configured.', |
| ) |
| except ValidationError as exc: |
| return ( |
| None, |
| '', |
| build_failure_markdown(str(exc)), |
| json.dumps({'status': 'error', 'errors': str(exc).splitlines()}, indent=2, ensure_ascii=False), |
| gr.update(interactive=False), |
| '', |
| ) |
| except Exception as exc: |
| return ( |
| None, |
| '', |
| build_failure_markdown(str(exc)), |
| json.dumps({'status': 'error', 'errors': [str(exc)]}, indent=2, ensure_ascii=False), |
| gr.update(interactive=False), |
| '', |
| ) |
|
|
|
|
| def create_pr(state: dict | None): |
| if not state: |
| return '## PR creation failed\n\n- Validate a submission first.' |
|
|
| prepared = PreparedSubmission.from_state(state) |
| try: |
| commit_info = create_dataset_pr(prepared, repo_id=DEFAULT_REPO_ID, token=load_hf_token()) |
| pr_url = commit_info.pr_url or commit_info.commit_url |
| return '\n'.join([ |
| '## PR created', |
| '', |
| f'- Task ID: `{prepared.assigned_task_id}`', |
| f'- PR: {pr_url}', |
| ]) |
| finally: |
| cleanup_work_dir(prepared.work_dir) |
|
|
|
|
| with gr.Blocks(title=SPACE_TITLE, theme=gr.themes.Base(), css=CSS, fill_width=True) as demo: |
| state = gr.State(None) |
| archive_state = gr.State('') |
|
|
| gr.HTML(build_hero_html()) |
|
|
| with gr.Group(elem_classes=['page-shell']): |
| with gr.Row(elem_classes=['section-row']): |
| with gr.Column(scale=7, elem_classes=['section-copy', 'main-form']): |
| gr.HTML(field_label_html('Task ZIP archive')) |
| with gr.Row(elem_classes=['upload-row']): |
| archive = gr.UploadButton( |
| 'Select ZIP file', |
| file_types=['.zip'], |
| file_count='single', |
| type='filepath', |
| variant='secondary', |
| elem_classes=['upload-button'], |
| ) |
| archive_notice = gr.Markdown('No ZIP file selected yet.', elem_classes=['upload-status']) |
| with gr.Row(): |
| with gr.Column(): |
| gr.HTML(field_label_html('Suggested domain')) |
| suggested_domain = gr.Dropdown( |
| choices=list(DOMAINS), |
| value='Astronomy', |
| show_label=False, |
| container=False, |
| ) |
| with gr.Column(): |
| gr.HTML(field_label_html('Custom domain (optional)')) |
| custom_domain = gr.Textbox( |
| placeholder='e.g. Robotics or Robot-Learning', |
| show_label=False, |
| container=False, |
| ) |
| gr.Markdown( |
| '<div class="caption">Use the custom field if your task does not belong to the suggested list. ' |
| 'If the custom field is filled, it overrides the suggested domain and becomes the prefix of the final task ID.</div>' |
| ) |
| gr.HTML(field_label_html('Submitter name or HF username')) |
| submitter = gr.Textbox( |
| placeholder='e.g. your-hf-handle', |
| show_label=False, |
| container=False, |
| ) |
| gr.HTML(field_label_html('Contact email')) |
| email = gr.Textbox( |
| placeholder='name@example.com', |
| show_label=False, |
| container=False, |
| ) |
| gr.HTML(field_label_html('Target paper title')) |
| paper_title = gr.Textbox(show_label=False, container=False) |
| gr.HTML(field_label_html('Target paper URL or DOI')) |
| paper_url = gr.Textbox( |
| placeholder='https://... or DOI', |
| show_label=False, |
| container=False, |
| ) |
| gr.HTML(field_label_html('Optional notes for reviewers')) |
| notes = gr.Textbox( |
| lines=4, |
| placeholder='Anything maintainers should know about licensing, preprocessing, or provenance.', |
| show_label=False, |
| container=False, |
| ) |
| with gr.Column(scale=5, elem_classes=['section-copy', 'side-notes']): |
| gr.Markdown(submission_guide_markdown(), elem_classes=['subtle-block']) |
|
|
| with gr.Row(elem_classes=['action-row']): |
| validate_btn = gr.Button('Validate ZIP', variant='primary', elem_classes=['primary-button']) |
| create_pr_btn = gr.Button('Create Dataset PR', interactive=False, elem_classes=['secondary-button']) |
|
|
| with gr.Column(elem_classes=['section-copy', 'results-shell']): |
| gr.HTML(field_label_html('Final task ID (assigned automatically)')) |
| assigned_task_id = gr.Textbox( |
| interactive=False, |
| show_label=False, |
| container=False, |
| ) |
| gr.Markdown(final_task_help_html()) |
| validation_md = gr.Markdown() |
| gr.HTML(field_label_html('Validation report')) |
| validation_report = gr.Code(language='json', show_label=False, container=False) |
| pr_md = gr.Markdown() |
|
|
| archive.upload(fn=handle_archive_upload, inputs=[archive], outputs=[archive_state, archive_notice]) |
|
|
| validate_btn.click( |
| fn=validate_submission, |
| inputs=[ |
| archive_state, |
| suggested_domain, |
| custom_domain, |
| submitter, |
| email, |
| paper_title, |
| paper_url, |
| notes, |
| state, |
| ], |
| outputs=[state, assigned_task_id, validation_md, validation_report, create_pr_btn, pr_md], |
| ) |
| create_pr_btn.click(fn=create_pr, inputs=[state], outputs=[pr_md]) |
|
|
|
|
| if __name__ == '__main__': |
| demo.launch() |
|
|