Spaces:
Sleeping
Sleeping
| """Prefab UI for a research job.""" | |
| from __future__ import annotations | |
| from copy import deepcopy | |
| from typing import Any | |
| from prefab_ui.actions import ( | |
| CloseOverlay, | |
| Fetch, | |
| OpenLink, | |
| SetInterval, | |
| SetState, | |
| ShowToast, | |
| ) | |
| from prefab_ui.actions.mcp import CallTool, SendMessage, UpdateContext | |
| from prefab_ui.app import PrefabApp | |
| from prefab_ui.components import ( | |
| Badge, | |
| Button, | |
| Column, | |
| Dialog, | |
| Div, | |
| Heading, | |
| If, | |
| Image, | |
| Markdown, | |
| Row, | |
| Separator, | |
| Text, | |
| ) | |
| from prefab_ui.components.control_flow import ForEach | |
| from prefab_ui.rx import RESULT, STATE | |
| from .hf_design import ( | |
| HF_DESIGN_CSS, | |
| HF_FONT_STYLESHEET, | |
| HF_LOGO_DATA_URI, | |
| HFDesign, | |
| design_css_class, | |
| validate_design, | |
| ) | |
| from .report_preview import markdown_only_preview | |
| BROADSHEET_CSS = """ | |
| .dispatch-app { | |
| --dispatch-accent: color-mix(in oklab, var(--warning) 68%, var(--foreground)); | |
| --dispatch-live: color-mix(in oklab, var(--success) 76%, #4a8a63); | |
| --dispatch-panel: color-mix(in oklab, var(--card) 76%, var(--muted)); | |
| width: 100%; | |
| min-height: 760px; | |
| display: flex; | |
| justify-content: center; | |
| background: var(--muted); | |
| color: var(--foreground); | |
| } | |
| .dispatch-sheet { | |
| width: min(100%, 800px); | |
| height: 760px; | |
| overflow: hidden; | |
| display: flex; | |
| flex-direction: column; | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| background: var(--background); | |
| box-shadow: 0 3px 12px color-mix(in oklab, var(--foreground) 10%, transparent); | |
| } | |
| .dispatch-header { | |
| flex: none; | |
| padding: 22px 36px 14px; | |
| } | |
| .dispatch-kicker, | |
| .dispatch-section-label, | |
| .dispatch-time, | |
| .dispatch-source, | |
| .dispatch-meta, | |
| .dispatch-log-line, | |
| .dispatch-query-toggle, | |
| .dispatch-status { | |
| font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; | |
| } | |
| .dispatch-topbar { | |
| min-width: 0; | |
| align-items: center; | |
| } | |
| .dispatch-kicker { | |
| flex: none; | |
| color: var(--dispatch-accent); | |
| font-size: 11px; | |
| letter-spacing: .14em; | |
| text-transform: uppercase; | |
| } | |
| .dispatch-status-controls { | |
| min-width: 0; | |
| margin-left: 10px; | |
| align-items: center; | |
| justify-content: flex-end; | |
| flex-wrap: wrap; | |
| } | |
| .dispatch-stats { | |
| min-width: 0; | |
| margin-left: auto; | |
| align-items: center; | |
| color: var(--muted-foreground); | |
| } | |
| .dispatch-stat { | |
| font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; | |
| font-size: 11px; | |
| font-variant-numeric: tabular-nums; | |
| letter-spacing: .035em; | |
| text-transform: uppercase; | |
| white-space: nowrap; | |
| } | |
| .dispatch-status { | |
| min-height: 25px; | |
| padding: 4px 11px; | |
| border-radius: 3px; | |
| font-size: 11px; | |
| font-weight: 500; | |
| letter-spacing: .03em; | |
| } | |
| .dispatch-status-running { | |
| background: color-mix(in oklab, var(--warning) 17%, var(--background)); | |
| color: color-mix(in oklab, var(--warning) 46%, var(--foreground)); | |
| } | |
| .dispatch-status-complete { | |
| background: color-mix(in oklab, var(--warning) 30%, var(--background)); | |
| color: color-mix(in oklab, var(--warning) 28%, var(--foreground)); | |
| } | |
| .dispatch-status-failed { | |
| border: 1px solid var(--muted-foreground); | |
| color: var(--muted-foreground); | |
| } | |
| .dispatch-status-cancelled { | |
| background: var(--muted); | |
| color: var(--muted-foreground); | |
| } | |
| .dispatch-status-dot { | |
| width: 7px; | |
| height: 7px; | |
| border-radius: 999px; | |
| background: var(--dispatch-live); | |
| animation: dispatch-pulse 1.4s ease-in-out infinite; | |
| } | |
| .dispatch-header-separator { | |
| margin: 14px 0 16px; | |
| } | |
| .dispatch-query-row { | |
| min-width: 0; | |
| align-items: baseline; | |
| } | |
| .dispatch-section-label { | |
| flex: none; | |
| color: var(--muted-foreground); | |
| font-size: 10px; | |
| letter-spacing: .14em; | |
| text-transform: uppercase; | |
| } | |
| .dispatch-query-wrap { | |
| min-width: 0; | |
| flex: 1; | |
| } | |
| .dispatch-headline { | |
| margin: 0; | |
| font-family: ui-serif, Georgia, Cambria, "Times New Roman", serif; | |
| font-size: clamp(26px, 4vw, 34px); | |
| font-weight: 500; | |
| line-height: 1.12; | |
| letter-spacing: -.02em; | |
| text-wrap: balance; | |
| } | |
| .dispatch-original-label { | |
| display: block; | |
| margin-top: 16px; | |
| } | |
| .dispatch-query { | |
| margin: 6px 0 0; | |
| font-family: ui-serif, Georgia, Cambria, "Times New Roman", serif; | |
| color: var(--muted-foreground); | |
| font-size: 15px; | |
| font-weight: 400; | |
| line-height: 1.45; | |
| letter-spacing: 0; | |
| text-wrap: pretty; | |
| } | |
| .dispatch-query-clamped { | |
| display: -webkit-box; | |
| overflow: hidden; | |
| -webkit-box-orient: vertical; | |
| -webkit-line-clamp: 2; | |
| } | |
| .dispatch-query-toggle { | |
| height: auto; | |
| min-height: 0; | |
| margin-top: 6px; | |
| padding: 0; | |
| color: var(--dispatch-accent); | |
| font-size: 11px; | |
| letter-spacing: .06em; | |
| } | |
| .dispatch-body { | |
| flex: 1; | |
| min-height: 0; | |
| overflow-x: hidden; | |
| overflow-y: auto; | |
| padding: 0 36px; | |
| scrollbar-color: var(--muted-foreground) transparent; | |
| scrollbar-width: thin; | |
| } | |
| .dispatch-body::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| .dispatch-body::-webkit-scrollbar-thumb { | |
| border-radius: 4px; | |
| background: var(--muted-foreground); | |
| } | |
| .dispatch-report-actions { | |
| display: grid; | |
| grid-template-columns: repeat(2, minmax(0, 1fr)); | |
| gap: 10px; | |
| margin: 12px 0 22px; | |
| } | |
| .dispatch-report-action { | |
| width: 100%; | |
| color: var(--dispatch-accent); | |
| } | |
| .dispatch-markdown-report { | |
| margin-top: 16px; | |
| padding: 22px; | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| background: var(--card); | |
| } | |
| .dispatch-markdown-body { | |
| margin-top: 14px; | |
| color: var(--foreground); | |
| font-size: 14px; | |
| line-height: 1.65; | |
| } | |
| .dispatch-markdown-body > :first-child { | |
| margin-top: 0; | |
| } | |
| .dispatch-markdown-body > :last-child { | |
| margin-bottom: 0; | |
| } | |
| .dispatch-markdown-image { | |
| display: block; | |
| max-width: 100%; | |
| height: auto; | |
| margin: 18px auto; | |
| border-radius: 8px; | |
| } | |
| .dispatch-log { | |
| padding: 12px 16px; | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| background: var(--dispatch-panel); | |
| } | |
| .dispatch-log-body { | |
| margin-top: 24px; | |
| } | |
| .dispatch-log-footer { | |
| margin: 0; | |
| padding-bottom: 0; | |
| border: 0; | |
| background: transparent; | |
| } | |
| .dispatch-log-line { | |
| min-width: 0; | |
| align-items: baseline; | |
| color: var(--muted-foreground); | |
| font-size: 12px; | |
| line-height: 1.35; | |
| } | |
| .dispatch-log-line + .dispatch-log-line { | |
| margin-top: 5px; | |
| } | |
| .dispatch-log-message { | |
| min-width: 0; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| } | |
| .dispatch-current-block { | |
| margin-top: 22px; | |
| } | |
| .dispatch-current-meta { | |
| align-items: baseline; | |
| } | |
| .dispatch-time { | |
| flex: none; | |
| color: var(--muted-foreground); | |
| font-size: 12px; | |
| font-variant-numeric: tabular-nums; | |
| } | |
| .dispatch-source { | |
| min-width: 0; | |
| overflow: hidden; | |
| color: var(--dispatch-accent); | |
| font-size: 11px; | |
| letter-spacing: .02em; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| } | |
| .dispatch-current { | |
| margin-top: 9px; | |
| align-items: flex-start; | |
| } | |
| .dispatch-current-copy { | |
| max-width: 60ch; | |
| font-family: ui-serif, Georgia, Cambria, "Times New Roman", serif; | |
| font-size: 22px; | |
| line-height: 1.34; | |
| letter-spacing: -.005em; | |
| text-wrap: pretty; | |
| } | |
| .dispatch-current-copy > :first-child, | |
| .dispatch-event-message > :first-child { | |
| margin-top: 0; | |
| } | |
| .dispatch-current-copy > :last-child, | |
| .dispatch-event-message > :last-child { | |
| margin-bottom: 0; | |
| } | |
| .dispatch-live-dot { | |
| position: relative; | |
| width: 11px; | |
| height: 11px; | |
| flex: none; | |
| margin-top: 8px; | |
| border-radius: 999px; | |
| background: var(--dispatch-live); | |
| animation: dispatch-pulse 1.4s ease-in-out infinite; | |
| } | |
| .dispatch-live-dot::after { | |
| content: ""; | |
| position: absolute; | |
| inset: 0; | |
| border-radius: inherit; | |
| background: inherit; | |
| animation: dispatch-ring 1.6s ease-out infinite; | |
| } | |
| .dispatch-rule { | |
| height: 2px; | |
| margin-top: 16px; | |
| overflow: hidden; | |
| border-radius: 2px; | |
| background: var(--border); | |
| } | |
| .dispatch-rule-running::after { | |
| content: ""; | |
| display: block; | |
| width: 40%; | |
| height: 100%; | |
| background: var(--dispatch-accent); | |
| animation: dispatch-progress 1.5s ease-in-out infinite; | |
| } | |
| .dispatch-rule-completed { | |
| background: var(--dispatch-accent); | |
| } | |
| .dispatch-rule-failed { | |
| height: 0; | |
| border-top: 2px dashed var(--muted-foreground); | |
| border-radius: 0; | |
| background: transparent; | |
| } | |
| .dispatch-rule-cancelled { | |
| background: color-mix(in oklab, var(--muted-foreground) 62%, transparent); | |
| } | |
| .dispatch-history { | |
| margin-top: 26px; | |
| padding-bottom: 26px; | |
| } | |
| .dispatch-event { | |
| min-width: 0; | |
| padding: 12px 0; | |
| border-top: 1px solid var(--border); | |
| opacity: .72; | |
| } | |
| .dispatch-event-copy { | |
| min-width: 0; | |
| } | |
| .dispatch-event-message { | |
| margin-top: 3px; | |
| font-family: ui-serif, Georgia, Cambria, "Times New Roman", serif; | |
| font-size: 15px; | |
| line-height: 1.5; | |
| text-wrap: pretty; | |
| } | |
| .dispatch-read-confirmation { | |
| margin: -12px 0 18px; | |
| color: var(--muted-foreground); | |
| font-size: 11px; | |
| } | |
| .dispatch-footer { | |
| flex: none; | |
| border-top: 1px solid var(--border); | |
| } | |
| .dispatch-footer-log { | |
| padding: 8px 36px 0; | |
| } | |
| .dispatch-log-toggle { | |
| min-height: 24px; | |
| align-items: center; | |
| } | |
| .dispatch-log-toggle-button { | |
| height: auto; | |
| min-height: 0; | |
| margin-left: auto; | |
| padding: 2px 0; | |
| color: var(--muted-foreground); | |
| font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; | |
| font-size: 10px; | |
| letter-spacing: .04em; | |
| } | |
| .dispatch-session { | |
| min-width: 0; | |
| padding: 12px 36px; | |
| align-items: center; | |
| } | |
| .dispatch-meta { | |
| flex: none; | |
| color: var(--muted-foreground); | |
| font-size: 11px; | |
| letter-spacing: .1em; | |
| text-transform: uppercase; | |
| } | |
| .dispatch-trace { | |
| min-width: 0; | |
| overflow: hidden; | |
| color: var(--muted-foreground); | |
| font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; | |
| font-size: 11px; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| } | |
| .dispatch-build { | |
| flex: none; | |
| margin-left: auto; | |
| color: var(--muted-foreground); | |
| font-size: 9px; | |
| opacity: .6; | |
| } | |
| .dispatch-archive-link { | |
| height: auto; | |
| min-height: 0; | |
| margin-left: auto; | |
| padding: 0; | |
| color: var(--muted-foreground); | |
| font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; | |
| font-size: 10px; | |
| } | |
| .dispatch-archive-link + .dispatch-build { | |
| margin-left: 8px; | |
| } | |
| .dispatch-dialog-actions { | |
| margin-top: 8px; | |
| justify-content: flex-end; | |
| } | |
| @keyframes dispatch-progress { | |
| from { transform: translateX(-110%); } | |
| to { transform: translateX(250%); } | |
| } | |
| @keyframes dispatch-ring { | |
| from { transform: scale(1); opacity: .45; } | |
| to { transform: scale(2.6); opacity: 0; } | |
| } | |
| @keyframes dispatch-pulse { | |
| 0%, 100% { opacity: 1; transform: scale(1); } | |
| 50% { opacity: .4; transform: scale(.78); } | |
| } | |
| @media (max-width: 720px) { | |
| .dispatch-sheet { | |
| border-right: 0; | |
| border-left: 0; | |
| border-radius: 0; | |
| box-shadow: none; | |
| } | |
| .dispatch-header { | |
| padding: 18px 20px 12px; | |
| } | |
| .dispatch-query-row { | |
| align-items: flex-start; | |
| } | |
| .dispatch-query-row { | |
| flex-direction: column; | |
| } | |
| .dispatch-query { | |
| margin-top: 8px; | |
| font-size: 14px; | |
| } | |
| .dispatch-body { | |
| padding: 0 20px; | |
| } | |
| .dispatch-log-footer { | |
| margin: 0; | |
| } | |
| .dispatch-footer-log { | |
| padding-right: 20px; | |
| padding-left: 20px; | |
| } | |
| .dispatch-session { | |
| padding: 12px 20px; | |
| } | |
| .dispatch-event { | |
| gap: 12px; | |
| } | |
| .dispatch-build { | |
| display: none; | |
| } | |
| } | |
| @media (max-width: 560px) { | |
| .dispatch-topbar { | |
| align-items: flex-start; | |
| flex-wrap: wrap; | |
| } | |
| .dispatch-stats { | |
| order: 3; | |
| width: 100%; | |
| margin: 9px 0 0; | |
| } | |
| .dispatch-status-controls { | |
| margin-left: auto; | |
| } | |
| } | |
| @media (max-width: 460px) { | |
| .dispatch-report-actions { | |
| grid-template-columns: 1fr; | |
| } | |
| .dispatch-status-controls { | |
| gap: 8px; | |
| } | |
| .dispatch-stats { justify-content: space-between; } | |
| .dispatch-current-copy { | |
| font-size: 20px; | |
| } | |
| } | |
| @media (prefers-reduced-motion: reduce) { | |
| .dispatch-live-dot, | |
| .dispatch-live-dot::after, | |
| .dispatch-status-dot, | |
| .dispatch-rule-running::after { | |
| animation: none; | |
| } | |
| } | |
| """ | |
| def _display_source(source: str | None) -> str: | |
| return (source or "researcher/agent_loop").replace("/", " / ") | |
| def _ui_snapshot(snapshot: dict[str, Any]) -> dict[str, Any]: | |
| """Add display-only fields without mutating the retained job snapshot.""" | |
| prepared = deepcopy(snapshot) | |
| prepared.setdefault("headline", "Briefing the researcher") | |
| prepared.setdefault("workspace_id", None) | |
| prepared.setdefault( | |
| "markdown_report_ready", | |
| bool(prepared.get("markdown_report_uri") or prepared.get("markdown_report")), | |
| ) | |
| prepared.setdefault( | |
| "activity_source_label", _display_source(prepared.get("activity_source")) | |
| ) | |
| for summary in prepared.get("recent_summaries", []): | |
| summary.setdefault("source_label", _display_source(summary.get("source"))) | |
| return prepared | |
| def build_research_ui( | |
| topic: str, | |
| snapshot: dict[str, Any], | |
| *, | |
| build_id: str = "dev", | |
| live: bool = True, | |
| design: HFDesign | None = None, | |
| status_url: str | None = None, | |
| recovery_url: str | None = None, | |
| ) -> PrefabApp: | |
| snapshot = _ui_snapshot(snapshot) | |
| if design is not None: | |
| design = validate_design(design) | |
| on_mount = None | |
| if live: | |
| report_is_current = RESULT.markdown_report_revision == STATE.report_revision | |
| poll_status = CallTool( | |
| "research_status", | |
| arguments={"job_id": STATE.job_id}, | |
| on_success=[ | |
| SetState( | |
| "report_loaded", | |
| report_is_current.then(STATE.report_loaded, False), | |
| ), | |
| SetState("job", RESULT), | |
| ], | |
| ) | |
| poll_while = ~STATE.job.done | |
| if status_url is not None: | |
| poll_status = Fetch( | |
| STATE.status_url, | |
| on_success=[ | |
| SetState( | |
| "report_loaded", | |
| report_is_current.then(STATE.report_loaded, False), | |
| ), | |
| SetState("job", RESULT), | |
| SetState("report_blocks", RESULT.report_blocks), | |
| SetState("report_loaded", RESULT.report_ready), | |
| SetState( | |
| "report_revision", | |
| RESULT.markdown_report_revision, | |
| ), | |
| ], | |
| on_error=[ | |
| SetState("status_unavailable", True), | |
| ShowToast( | |
| "Live status is unavailable", | |
| description=( | |
| "The research may still be running. " | |
| "Reconnect the app to restore updates." | |
| ), | |
| variant="warning", | |
| ), | |
| ], | |
| ) | |
| poll_while = ( | |
| (STATE.status_url != "") | |
| & ~STATE.job.done | |
| & ~STATE.status_unavailable | |
| ) | |
| on_mount = SetInterval( | |
| duration=4000, | |
| while_=poll_while, | |
| on_tick=poll_status, | |
| ) | |
| cancel_action = CloseOverlay() | |
| if live: | |
| cancel_action = CallTool( | |
| "cancel_research", | |
| arguments={"job_id": STATE.job_id}, | |
| on_success=[ | |
| SetState("job", RESULT), | |
| SetState("cancel_requested", False), | |
| CloseOverlay(), | |
| ShowToast( | |
| "Cancellation requested", | |
| description="The active research session is being closed.", | |
| variant="warning", | |
| ), | |
| ], | |
| on_error=[ | |
| SetState("cancel_requested", False), | |
| ShowToast( | |
| "Could not cancel research", | |
| description="The job may already have finished.", | |
| variant="error", | |
| ), | |
| ], | |
| ) | |
| read_report = CallTool( | |
| "research_chat_context", | |
| arguments={"job_id": STATE.job_id}, | |
| on_success=[ | |
| UpdateContext(content=RESULT.markdown), | |
| SendMessage( | |
| RESULT.message, | |
| on_success=SetState("chat_sent", True), | |
| on_error=ShowToast( | |
| "This host could not send the chat message.", | |
| variant="error", | |
| ), | |
| ), | |
| ], | |
| on_error=ShowToast( | |
| "Could not load the Markdown report.", | |
| variant="error", | |
| ), | |
| ) | |
| if not live: | |
| read_report = SetState("chat_sent", True) | |
| load_report = CallTool( | |
| "research_report_preview", | |
| arguments={"job_id": STATE.job_id}, | |
| on_success=[ | |
| SetState( | |
| "report_blocks", | |
| (RESULT.revision == STATE.job.markdown_report_revision).then( | |
| RESULT.blocks, | |
| STATE.report_blocks, | |
| ), | |
| ), | |
| SetState( | |
| "report_loaded", | |
| RESULT.revision == STATE.job.markdown_report_revision, | |
| ), | |
| SetState( | |
| "report_revision", | |
| (RESULT.revision == STATE.job.markdown_report_revision).then( | |
| RESULT.revision, | |
| STATE.report_revision, | |
| ), | |
| ), | |
| ], | |
| on_error=[ | |
| SetState("report_loaded", True), | |
| SetState( | |
| "report_revision", | |
| STATE.job.markdown_report_revision, | |
| ), | |
| ShowToast( | |
| "Could not load the in-app report preview.", | |
| description="The Markdown and HTML artifacts remain available.", | |
| variant="error", | |
| ), | |
| ], | |
| ) | |
| css_class = "dispatch-app" | |
| css = [BROADSHEET_CSS] | |
| stylesheets = None | |
| if design is not None: | |
| css_class = design_css_class(design) | |
| css.append(HF_DESIGN_CSS) | |
| stylesheets = [HF_FONT_STYLESHEET] | |
| with PrefabApp( | |
| title="Research Dispatch", | |
| css_class=css_class, | |
| css=css, | |
| stylesheets=stylesheets, | |
| state={ | |
| "job": snapshot, | |
| "report_blocks": ( | |
| snapshot.get("markdown_report_blocks") | |
| or ( | |
| markdown_only_preview(snapshot["markdown_report"]) | |
| if snapshot.get("markdown_report") | |
| else [] | |
| ) | |
| ), | |
| "report_loaded": bool( | |
| snapshot.get("markdown_report_blocks") | |
| or snapshot.get("markdown_report") | |
| ), | |
| "report_revision": snapshot.get("markdown_report_revision", 0), | |
| "topic": topic, | |
| "job_id": snapshot["job_id"], | |
| "cancel_requested": False, | |
| "query_expanded": False, | |
| "query_toggleable": len(topic) > 90, | |
| "event_log_expanded": False, | |
| "chat_sent": False, | |
| "app_version": f"build {build_id}", | |
| "status_url": status_url or "", | |
| "recovery_url": recovery_url or "", | |
| "status_unavailable": False, | |
| }, | |
| ) as ui: | |
| with Div(css_class="dispatch-sheet", on_mount=on_mount): | |
| with Column(css_class="dispatch-header", gap=0): | |
| with Row(css_class="dispatch-topbar", gap=4): | |
| if design is None: | |
| Text("Research Dispatch", css_class="dispatch-kicker") | |
| else: | |
| with Row(css_class="hf-brand", gap=2): | |
| Image( | |
| src=HF_LOGO_DATA_URI, | |
| alt="Hugging Face", | |
| css_class="hf-brand-logo", | |
| ) | |
| with Column(css_class="hf-brand-copy", gap=0): | |
| Text("Hugging Face", css_class="hf-brand-name") | |
| Text( | |
| "Researcher", | |
| css_class="hf-brand-product", | |
| ) | |
| with Row(css_class="dispatch-stats", gap=2): | |
| Text(STATE.job.elapsed, css_class="dispatch-stat") | |
| Text("·", css_class="dispatch-stat") | |
| Text( | |
| "{{ job.event_count + ' events' }}", | |
| css_class="dispatch-stat", | |
| ) | |
| Text("·", css_class="dispatch-stat") | |
| Text( | |
| "{{ job.turn_count + ' turns' }}", | |
| css_class="dispatch-stat", | |
| ) | |
| with Row(css_class="dispatch-status-controls", gap=3): | |
| with If( | |
| (STATE.job.status == "queued") | |
| | (STATE.job.status == "running") | |
| | (STATE.job.status == "finalizing") | |
| | (STATE.job.status == "cancelling") | |
| ): | |
| with Badge( | |
| css_class=("dispatch-status dispatch-status-running") | |
| ): | |
| Div(css_class="dispatch-status-dot") | |
| Text( | |
| "{{ job.status == 'cancelling' ? " | |
| "'Cancelling' : job.status == 'finalizing' ? " | |
| "'Finalizing' : job.phase == 'reporting' ? " | |
| "'Building report' : job.phase == 'wrapping_up' ? " | |
| "'Wrapping up' : 'Working' }}" | |
| ) | |
| with If(STATE.job.status == "completed"): | |
| Badge( | |
| "Complete", | |
| css_class=("dispatch-status dispatch-status-complete"), | |
| ) | |
| with If(STATE.job.status == "failed"): | |
| Badge( | |
| "Failed", | |
| variant="outline", | |
| css_class=("dispatch-status dispatch-status-failed"), | |
| ) | |
| with If( | |
| (STATE.job.status == "cancelled") | |
| | (STATE.job.status == "expired") | |
| ): | |
| Badge( | |
| "{{ job.status == 'expired' ? " | |
| "'Unavailable' : 'Cancelled' }}", | |
| css_class=("dispatch-status dispatch-status-cancelled"), | |
| ) | |
| with If(STATE.job.cancellable): | |
| with Dialog( | |
| title="Cancel this research run?", | |
| description=( | |
| "The agent will stop where it is. Partial " | |
| "work and the session trace collected so far " | |
| "will be kept, but no final report will be " | |
| "produced." | |
| ), | |
| ): | |
| Button("Cancel", variant="outline", size="sm") | |
| with Row(css_class="dispatch-dialog-actions", gap=2): | |
| Button( | |
| "Keep running", | |
| variant="outline", | |
| onClick=CloseOverlay(), | |
| ) | |
| Button( | |
| "Cancel research", | |
| variant="destructive", | |
| disabled=STATE.cancel_requested, | |
| onClick=[ | |
| SetState("cancel_requested", True), | |
| cancel_action, | |
| ], | |
| ) | |
| Separator(css_class="dispatch-header-separator") | |
| with Div(css_class="dispatch-query-wrap"): | |
| Text("Research brief", css_class="dispatch-section-label") | |
| Heading( | |
| STATE.job.headline, | |
| level=1, | |
| css_class="dispatch-headline", | |
| ) | |
| Text( | |
| "Original query", | |
| css_class=("dispatch-section-label dispatch-original-label"), | |
| ) | |
| with Div(): | |
| with If(STATE.query_toggleable): | |
| with If(~STATE.query_expanded): | |
| Heading( | |
| STATE.topic, | |
| level=1, | |
| css_class="dispatch-query dispatch-query-clamped", | |
| ) | |
| Button( | |
| "Show full query", | |
| variant="link", | |
| size="xs", | |
| css_class="dispatch-query-toggle", | |
| onClick=SetState("query_expanded", True), | |
| ) | |
| with If(STATE.query_expanded): | |
| Heading( | |
| STATE.topic, | |
| level=1, | |
| css_class="dispatch-query", | |
| ) | |
| Button( | |
| "Show less", | |
| variant="link", | |
| size="xs", | |
| css_class="dispatch-query-toggle", | |
| onClick=SetState("query_expanded", False), | |
| ) | |
| with If(~STATE.query_toggleable): | |
| Heading( | |
| STATE.topic, | |
| level=1, | |
| css_class="dispatch-query", | |
| ) | |
| with Div(css_class="dispatch-body"): | |
| with If( | |
| STATE.job.markdown_report_ready | |
| | (STATE.job.phase == "reporting") | |
| | (STATE.job.phase == "wrapping_up") | |
| | (STATE.job.status == "finalizing") | |
| | (STATE.job.status == "completed") | |
| ): | |
| with If(STATE.job.markdown_report_ready & ~STATE.report_loaded): | |
| Div(on_mount=load_report) | |
| with If(STATE.report_loaded): | |
| with Div(css_class="dispatch-markdown-report"): | |
| Text( | |
| "Markdown report", | |
| css_class="dispatch-section-label", | |
| ) | |
| with ForEach("report_blocks") as block: | |
| with If(block.kind == "markdown"): | |
| Markdown( | |
| block.content, | |
| css_class="dispatch-markdown-body", | |
| ) | |
| with If(block.kind == "image"): | |
| Image( | |
| src=block.src, | |
| alt=block.alt, | |
| width="100%", | |
| height="auto", | |
| css_class="dispatch-markdown-image", | |
| ) | |
| with Div(css_class="dispatch-report-actions"): | |
| Button( | |
| "{{ job.markdown_report_ready ? 'Add to chat' : " | |
| "'Preparing Markdown…' }}", | |
| variant="outline", | |
| css_class="dispatch-report-action", | |
| disabled=( | |
| STATE.chat_sent | ~STATE.job.markdown_report_ready | |
| ), | |
| onClick=read_report, | |
| ) | |
| Button( | |
| "{{ job.html_report_ready ? 'Open HTML report' : " | |
| "job.done ? 'HTML report unavailable' : " | |
| "'Building HTML report…' }}", | |
| variant="outline", | |
| css_class="dispatch-report-action", | |
| disabled=~STATE.job.html_report_ready, | |
| onClick=OpenLink(STATE.job.html_report_url), | |
| ) | |
| with If(STATE.chat_sent): | |
| Text( | |
| "Added to chat.", | |
| css_class="dispatch-read-confirmation", | |
| ) | |
| with If("{{ job.done && job.activity_roll.length > 0 }}"): | |
| with Div(css_class="dispatch-log dispatch-log-body"): | |
| with ForEach("job.activity_roll") as event: | |
| with Row(css_class="dispatch-log-line", gap=3): | |
| Text(event.elapsed, css_class="dispatch-time") | |
| Text( | |
| event.message, | |
| css_class="dispatch-log-message", | |
| ) | |
| with Div(css_class="dispatch-current-block"): | |
| with Row(css_class="dispatch-current-meta", gap=3): | |
| Text(STATE.job.elapsed, css_class="dispatch-time") | |
| Text( | |
| STATE.job.activity_source_label, | |
| css_class="dispatch-source", | |
| ) | |
| with Row(css_class="dispatch-current", gap=3): | |
| with If("{{ !job.done }}"): | |
| Div(css_class="dispatch-live-dot") | |
| Markdown( | |
| STATE.job.activity_summary, | |
| css_class="dispatch-current-copy", | |
| ) | |
| with If("{{ !job.done }}"): | |
| Div(css_class="dispatch-rule dispatch-rule-running") | |
| with If(STATE.job.status == "completed"): | |
| Div(css_class="dispatch-rule dispatch-rule-completed") | |
| with If(STATE.job.status == "failed"): | |
| Div(css_class="dispatch-rule dispatch-rule-failed") | |
| with If( | |
| (STATE.job.status == "cancelled") | |
| | (STATE.job.status == "expired") | |
| ): | |
| Div(css_class="dispatch-rule dispatch-rule-cancelled") | |
| with If("{{ job.recent_summaries.length > 0 }}"): | |
| with Div(css_class="dispatch-history"): | |
| Text("Before this", css_class="dispatch-section-label") | |
| with ForEach("job.recent_summaries") as event: | |
| with Row(css_class="dispatch-event", gap=4): | |
| Text(event.elapsed, css_class="dispatch-time") | |
| with Column(css_class="dispatch-event-copy", gap=0): | |
| Text( | |
| event.source_label, | |
| css_class="dispatch-source", | |
| ) | |
| Markdown( | |
| event.message, | |
| css_class="dispatch-event-message", | |
| ) | |
| with Div(css_class="dispatch-footer"): | |
| with If("{{ !job.done && job.activity_roll.length > 0 }}"): | |
| with Div(css_class="dispatch-footer-log"): | |
| with Row(css_class="dispatch-log-toggle"): | |
| Text("Agent events", css_class="dispatch-meta") | |
| with If(STATE.event_log_expanded): | |
| Button( | |
| "Hide log", | |
| icon="chevron-down", | |
| variant="link", | |
| size="xs", | |
| css_class="dispatch-log-toggle-button", | |
| onClick=SetState("event_log_expanded", False), | |
| ) | |
| with If(~STATE.event_log_expanded): | |
| Button( | |
| "Show log", | |
| icon="chevron-up", | |
| variant="link", | |
| size="xs", | |
| css_class="dispatch-log-toggle-button", | |
| onClick=SetState("event_log_expanded", True), | |
| ) | |
| with If(STATE.event_log_expanded): | |
| with Div(css_class="dispatch-log dispatch-log-footer"): | |
| with ForEach("job.activity_roll") as event: | |
| with Row(css_class="dispatch-log-line", gap=3): | |
| Text(event.elapsed, css_class="dispatch-time") | |
| Text( | |
| event.message, | |
| css_class="dispatch-log-message", | |
| ) | |
| with Row(css_class="dispatch-session", gap=3): | |
| Text("Session", css_class="dispatch-meta") | |
| Text( | |
| "{{ job.trace_path || 'Trace pending' }}", | |
| css_class="dispatch-trace", | |
| ) | |
| with If(STATE.job.archive_space_url): | |
| Button( | |
| "Archive ↗", | |
| variant="link", | |
| size="xs", | |
| css_class="dispatch-archive-link", | |
| onClick=OpenLink(STATE.job.archive_space_url), | |
| ) | |
| Text(STATE.app_version, css_class="dispatch-build") | |
| return ui | |