"""Harbor Visualiser — a Gradio Space for browsing Harbor task specs. Run locally: pip install -r requirements.txt python app.py Or deploy to a Hugging Face Space — the `README.md` frontmatter pins `sdk: gradio` and `app_file: app.py`, so the Space picks this up directly. URL prefill: https:///?dataset=owner/name https:///?dataset=hf://owner/name@rev https:///?dataset=gh://owner/repo https:///?d=owner/name (short alias) """ from __future__ import annotations import logging from pathlib import Path import gradio as gr from viewer import ( DatasetSource, HarborTask, fetch_dataset, list_tasks, load_task, parse_dataset_uri, ) logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s %(message)s") logger = logging.getLogger("harbor-visualiser") # --------------------------------------------------------------------------- # Backend handlers # --------------------------------------------------------------------------- def load_dataset_action(uri: str): """Top-level "Load" button handler. Returns a tuple matching the `outputs=` of the button binding: (status_md, source_state, root_state, task_choices, first_task, *task_panel_outputs) """ if not uri or not uri.strip(): return _empty_state("Enter a dataset URI to begin.") try: source = parse_dataset_uri(uri) except ValueError as exc: return _empty_state(f"❌ {exc}") try: root = fetch_dataset(source) except Exception as exc: logger.exception("fetch failed") return _empty_state(f"❌ fetch failed: {exc}") tasks = list_tasks(root) if not tasks: return _empty_state( f"⚠ No `task.toml` files found in `{source.display}`. " f"Looked under `{root}` for `/task.toml` and `tasks//task.toml`." ) first = tasks[0] task = load_task(root, first) status = ( f"✅ Loaded **{source.display}** — {len(tasks)} task" f"{'s' if len(tasks) != 1 else ''} found." ) return ( status, source.display, str(root), gr.update(choices=tasks, value=first, label=f"Tasks ({len(tasks)})"), *_render_task(task), ) def select_task_action(task_id: str, root: str): """Radio-button onchange — switch which task's detail tab content is shown.""" if not task_id or not root: return _render_task(None) try: task = load_task(Path(root), task_id) except Exception as exc: logger.exception("load_task failed") return _render_task(None, error=str(exc)) return _render_task(task) def init_from_url(request: gr.Request): """Read `?dataset=` (or `?d=`) on page load and prefill the input.""" if request is None: return "" qs = dict(request.query_params or {}) return (qs.get("dataset") or qs.get("d") or "").strip() # --------------------------------------------------------------------------- # Rendering helpers # --------------------------------------------------------------------------- _EMPTY_OUTPUTS_COUNT = 7 # overview + instruction + patch + test + dockerfile + solve + raw def _empty_state(status: str): """Reset the UI when no dataset is loaded.""" return ( status, "", # source_state "", # root_state gr.update(choices=[], value=None, label="Tasks"), *_render_task(None), ) def _render_task(task: HarborTask | None, error: str | None = None): """Compute the 7 panel outputs for a single selected task.""" if task is None: msg = error or "Pick a task from the list." return (msg, "", "", "", "", "", "") overview_md = _overview_markdown(task) instruction = task.instruction_md or task.instruction_inline or "_(no instruction.md)_" patch = task.oracle_patch or "(no solution/patch.diff)" test = task.test_sh or "(no tests/test.sh)" dockerfile = task.dockerfile or "(no environment/Dockerfile)" solve = task.solve_sh or "(no solution/solve.sh)" raw = task.task_toml_raw or "(no task.toml)" return (overview_md, instruction, patch, test, dockerfile, solve, raw) def _overview_markdown(task: HarborTask) -> str: """Render the task's metadata as a clean markdown table.""" rows: list[tuple[str, str]] = [] rows.append(("task id", f"`{task.id}`")) if task.name: rows.append(("name", f"`{task.name}`")) if task.version: rows.append(("spec version", task.version)) if task.description: rows.append(("description", task.description)) if task.difficulty: rows.append(("difficulty", task.difficulty)) if task.category: rows.append(("category", task.category)) if task.keywords: rows.append(("keywords", ", ".join(f"`{k}`" for k in task.keywords))) if task.agent_timeout_sec is not None: rows.append(("agent timeout", f"{task.agent_timeout_sec}s")) if task.verifier_timeout_sec is not None: rows.append(("verifier timeout", f"{task.verifier_timeout_sec}s")) md = ["| Field | Value |", "|---|---|"] for k, v in rows: md.append(f"| **{k}** | {v} |") if task.repo2env: md.append("\n### `[metadata.repo2env]` extension (Repo2RLEnv)\n") md.append("| Field | Value |") md.append("|---|---|") for k, v in sorted(task.repo2env.items()): if isinstance(v, dict): # Nested pipeline-specific block — flatten one level md.append(f"| **{k}** | _(nested — see below)_ |") for kk, vv in sorted(v.items()): md.append(f"|   `{kk}` | `{_short(vv)}` |") else: md.append(f"| **{k}** | `{_short(v)}` |") return "\n".join(md) def _short(v) -> str: """Truncate long values for the metadata table cells.""" if isinstance(v, list): return ", ".join(str(x) for x in v) s = str(v) return s if len(s) < 110 else s[:107] + "…" # --------------------------------------------------------------------------- # UI # --------------------------------------------------------------------------- _INTRO_MD = """# Harbor Visualiser Browse [Harbor](https://www.harborframework.com/) task spec datasets — Hugging Face, GitHub, Harbor registry, or local.""" _FOOTER_MD = """Built with [Gradio](https://www.gradio.app/) · Source on [GitHub](https://github.com/adithya-s-k/harbor-visualiser) · Harbor framework [docs](https://www.harborframework.com/)""" # A small set of popular / known-working datasets surfaced as one-click examples. # Each tuple is (label, uri). Order matters — most useful first. _EXAMPLES: list[tuple[str, str]] = [ ("cookbook/test (Harbor)", "harbor://cookbook/test"), ("SWE-Atlas QnA (Harbor)", "harbor://scale-ai/swe-atlas-qna"), ("SWE-Bench Pro (Harbor)", "harbor://cais/swebenchpro"), ("Click PRs (HF / Repo2RLEnv)", "AdithyaSK/click-r2e-v082post1"), ("Click PRs (GitHub demo)", "https://github.com/adithya-s-k/harbor-tasks-demo"), ] # Minimal monochrome aesthetic — sharp, no rounded buttons, mono-font for code, # slim borders. Tuned for the Soft-replacement requested in v0.1 design. _CUSTOM_CSS = """ .gradio-container { font-family: ui-sans-serif, system-ui, -apple-system, sans-serif; } .gradio-container * { letter-spacing: 0; } h1, h2, h3 { font-weight: 600; } button.primary { background: #111 !important; color: white !important; border: 1px solid #111 !important; } button.primary:hover { background: #333 !important; } .tabs { border: 1px solid #e5e5e5; } #task-list label { font-weight: 500; } footer { display: none !important; } """ def build_ui() -> gr.Blocks: with gr.Blocks(title="Harbor Visualiser") as demo: gr.Markdown(_INTRO_MD) with gr.Row(): uri_input = gr.Textbox( label="Dataset", placeholder="owner/name | gh://owner/repo | harbor://org/name | https://github.com/owner/repo", lines=1, scale=8, ) load_btn = gr.Button("Load", variant="primary", scale=1, min_width=80) # Quick-access popular examples. Click → populates the input + auto-loads. with gr.Row(): example_btns: list[gr.Button] = [] for label, _ in _EXAMPLES: example_btns.append(gr.Button(label, size="sm", variant="secondary")) status = gr.Markdown("Enter a dataset URI to begin.") source_state = gr.State("") root_state = gr.State("") with gr.Row(): with gr.Column(scale=2, min_width=240): task_list = gr.Radio( choices=[], label="Tasks", value=None, interactive=True, elem_id="task-list", ) with gr.Column(scale=7): with gr.Tabs(): with gr.Tab("Overview"): overview_md = gr.Markdown() with gr.Tab("Instruction"): instruction_md = gr.Markdown() with gr.Tab("Patch"): patch_code = gr.Code( language="python", # Gradio Prism has no "diff" but python catches +/- well label="solution/patch.diff", interactive=False, ) with gr.Tab("test.sh"): test_code = gr.Code( language="shell", label="tests/test.sh", interactive=False, ) with gr.Tab("Dockerfile"): dockerfile_code = gr.Code( language="dockerfile", label="environment/Dockerfile", interactive=False, ) with gr.Tab("solve.sh"): solve_code = gr.Code( language="shell", label="solution/solve.sh", interactive=False, ) with gr.Tab("Raw task.toml"): raw_code = gr.Code( language="yaml", # closest Prism token to TOML label="task.toml", interactive=False, ) gr.Markdown(_FOOTER_MD) # --- event wiring --- panel_outputs = [ overview_md, instruction_md, patch_code, test_code, dockerfile_code, solve_code, raw_code, ] load_btn.click( fn=load_dataset_action, inputs=[uri_input], outputs=[status, source_state, root_state, task_list, *panel_outputs], ) uri_input.submit( fn=load_dataset_action, inputs=[uri_input], outputs=[status, source_state, root_state, task_list, *panel_outputs], ) task_list.change( fn=select_task_action, inputs=[task_list, root_state], outputs=panel_outputs, ) # Wire each example button → set the input AND auto-load. # Capturing `uri` by default-arg avoids the classic late-binding trap. for btn, (_, uri_value) in zip(example_btns, _EXAMPLES, strict=True): btn.click( fn=lambda u=uri_value: u, outputs=uri_input, ).then( fn=load_dataset_action, inputs=[uri_input], outputs=[status, source_state, root_state, task_list, *panel_outputs], ) # On page load: read ?dataset= from URL → prefill input → auto-load demo.load(fn=init_from_url, inputs=None, outputs=uri_input).then( fn=lambda u: load_dataset_action(u) if u else _empty_state("Enter a dataset URI to begin."), inputs=[uri_input], outputs=[status, source_state, root_state, task_list, *panel_outputs], ) return demo if __name__ == "__main__": # Minimal black-monochrome aesthetic. Gradio's built-in Monochrome theme # is grayscale by design; combined with our css overrides above it lands # at "sharp, no color, mono-ish" without rolling our own theme from scratch. theme = gr.themes.Monochrome( radius_size=gr.themes.sizes.radius_sm, spacing_size=gr.themes.sizes.spacing_md, text_size=gr.themes.sizes.text_md, ) demo = build_ui() demo.queue(default_concurrency_limit=4).launch( server_name="0.0.0.0", theme=theme, css=_CUSTOM_CSS, )