build-small-hackathon/microfactory-lab
2
1"""The Chief Engineer — Gradio app (four workspaces: Build, Slice, Print, Review).2 3STUDIO: define the job (part + material + simulated room). BUILD: slice + the4engineer's pre-flight read (precedent, risks, Spine veto, second opinion). PRINT:5run the closed loop (quality compounds fail->clean, the Inspector grades each run,6then log a real print). REVIEW: the compounding made visible (ledger + verdict).7 8UI follows the walkthrough spec: no emojis (custom inline-SVG icons only), one9consolidated custom loader then progressive reveal, a small primary action in the10same top-right spot on every tab with a persistent Reset, grouped contained blocks,11mirrored header/footer.12 13Local-first. Real Ollama calls (gemma4:e4b), deterministic fallback so the demo14never crashes. Run: `ollama serve` + `make run` (= `uv run python app.py`).15"""16 17from __future__ import annotations18 19import os20import random21import time22import uuid23from pathlib import Path24 25import gradio as gr26 27try:28 import spaces # ZeroGPU @spaces.GPU decorator — HF-provided on the Space.29except ImportError: # not installed locally (base env / offline); decorator no-ops.30 class _SpacesShim:31 @staticmethod32 def GPU(fn=None, **_kw):33 return fn if fn is not None else (lambda f: f)34 35 spaces = _SpacesShim() # type: ignore36 37from core import deliberation_log38from core import field_log39from core import inspector40from core import llm41from core import seed_lessons42from core import signups43from core.theme import (44 THEME, CSS, rule, command_bar, footer_bar, inspector_panel, icon, loader, tab_intro,45 CLOCK_JS,46)47from core.widgets import virtual_printer_html, layer_image, SCRUB_LAYERS, VP_HEAD48from core.chief_engineer import advise49from core.ledger import LedgerManager50from core.models import Advice, BED_POSITIONS, Environment, Job, MATERIALS, PrintSettings51from core.nodes import render_node_cards52from core.reflect import reflect_on_job53from core.spine import SpineValidator54from learn.loop import run_iteration, SessionResult55from learn.policy import LearnedPolicy, cell_key56from core.viewer import (57 GEO_READS,58 benchy_mesh,59 gcode_panel_html,60 generate_primitive,61 infer_geometry,62 iteration_log_html,63 placement_callout,64 policy_cell_html,65 precedent_eval_html,66 quality_curve_html,67 risk_callouts_html,68 settings_panel_html,69 steepest_overhang_hint,70)71 72try:73 from ingest.distill import reference_block74except Exception: # ingestion is optional / removable75 def reference_block(material): # type: ignore76 return []77 78LEDGER = LedgerManager()79SPINE = SpineValidator()80POLICY = LearnedPolicy()81_loaded = seed_lessons.ensure_seeded(LEDGER)82 83# On the Space (backend=zerogpu), import the inference module at startup so its84# @spaces.GPU function (core/llm_zerogpu._generate) is registered — ZeroGPU requires85# at least one detected GPU function, and build_job is deliberately no longer one.86if __import__("os").environ.get("CHIEF_ENGINEER_BACKEND") == "zerogpu":87 try:88 import core.llm_zerogpu # noqa: F401 (registers @spaces.GPU on import)89 import core.llm_zerogpu_lora # noqa: F401 (LoRA-aware variant)90 except Exception:91 pass92 93# Model switcher: maps UI labels to backend configuration94# Space-compatible backends only (ZeroGPU + Modal). Local Ollama options removed —95# they only work in local development, not on the Space.96MODEL_OPTIONS = [97 "LoRA v3 (QAT E4B)",98 "LoRA v2 (Standard E4B)",99 "Base Gemma 4 E4B (ZeroGPU)",100 "Modal API (remote)",101]102 103MODEL_LORA_MAP = {104 "LoRA v2 (Standard E4B)": "kylebrodeur/microfactory-node-lora-v2",105 "LoRA v3 (QAT E4B)": "kylebrodeur/microfactory-node-lora-v3-qat",106}107 108def _apply_model_choice(model_choice: str):109 """Set environment variables so the next advise() call uses the chosen backend."""110 if model_choice == "Base Gemma 4 E4B (ZeroGPU)":111 os.environ.pop("CHIEF_ENGINEER_LORA_REPO", None)112 os.environ["CHIEF_ENGINEER_BACKEND"] = "zerogpu"113 elif model_choice in MODEL_LORA_MAP:114 os.environ["CHIEF_ENGINEER_LORA_REPO"] = MODEL_LORA_MAP[model_choice]115 os.environ["CHIEF_ENGINEER_BACKEND"] = "zerogpu"116 elif model_choice == "Modal API (remote)":117 os.environ.pop("CHIEF_ENGINEER_LORA_REPO", None)118 os.environ["CHIEF_ENGINEER_BACKEND"] = "modal"119 # No module reload needed: core.llm reads the backend + adapter env dynamically120 # (llm._backend() / CHIEF_ENGINEER_LORA_REPO), so the switch takes effect on the121 # next call. (reload(__import__("core.llm")) reloaded the package, not the submodule,122 # so it was a no-op for routing anyway.)123 124# Default to the winning LoRA v3 (QAT) model — best loss, best quantization quality125_apply_model_choice("LoRA v3 (QAT E4B)")126 127# Astrometrics OS visual layer lives in core/theme.py (THEME + CSS + helpers) so128# the Off-Brand skin stays a single removable module. See ../DESIGN.md.129 130PRINTER = "Creality Ender 3 V2"131 132 133def _delib_ctx(job, env) -> dict:134 """Context columns shared by every deliberation-log turn for a job."""135 return {"material": job.material, "geometry": job.geometry_type,136 "bed_position": job.bed_position, "env_temp": env.temp, "env_humidity": env.humidity}137_SCROLL_TOP = "() => { window.scrollTo({ top: 0, behavior: 'smooth' }); }"138 139 140def ledger_html() -> str:141 c = LEDGER.count()142 entries = LEDGER.all()[-8:][::-1]143 rows = []144 for e in entries:145 tag = "SEED" if e.source == "seed" else "EARNED"146 col = "var(--ao-outline)" if e.source == "seed" else "var(--ao-green)"147 rows.append(148 f"<div style='border-left:3px solid {col};background:var(--ao-surface);padding:6px 10px;"149 f"margin:4px 0;font-family:ui-monospace,monospace;font-size:11px;'>"150 f"<span style='color:{col};font-weight:700;'>{tag}</span> "151 f"<span style='color:var(--ao-orange);'>{e.material}/{e.geometry_type}</span> "152 f"<span style='color:var(--ao-outline);'>@ {e.env_temp:.0f}°C/{e.env_humidity:.0f}% → {e.outcome}</span>"153 f"<div style='color:var(--ao-text);'>{e.lesson}</div></div>"154 )155 head = (156 f"<div style='color:var(--ao-orange);font-family:ui-monospace,monospace;'>"157 f"LEDGER · {c['total']} lessons ({c['seed']} seed · {c['earned']} earned)</div>"158 )159 return head + "".join(rows)160 161 162def studio_log_html() -> str:163 """Job log near the top of Studio: what is stored, and where (studio-14/17)."""164 c = LEDGER.count()165 return (166 "<div class='ce-card'>"167 f"<div style='color:var(--ao-orange);font-weight:700;letter-spacing:1.5px;font-size:11px;'>"168 f"{icon('book')} JOB LOG · {c['total']} LESSONS "169 f"<span style='color:var(--ao-outline);font-weight:400;'>({c['seed']} seed · {c['earned']} earned)</span></div>"170 "<div class='ce-sub' style='margin-top:4px;'>Every build, print, and recorded outcome is stored "171 "to <b>data/lessons.jsonl</b> (durable) and the learned policy to <b>data/policy.json</b>. "172 "This session's runs append live; <b>Reset to Baseline</b> restores the curated seed + ingested set.</div>"173 "</div>"174 )175 176 177def reset_learnings():178 """Reset the live ledger + learned policy to the curated baseline (seed + ingested),179 clearing only this session's runtime lessons and the learned policy file. Does NOT180 touch: seed_lessons.jsonl, references.jsonl, HF trace datasets (ledger/deliberation/181 field-log), or any recorded Space logs. Also clears the loaded part and hides the182 result groups so the next run starts from a clean LOAD tab."""183 removed = LEDGER.reset_to_baseline()184 POLICY.reset()185 gr.Info(f"Reset to baseline — cleared {removed} runtime lesson(s) + the learned policy. "186 f"Seed/ingested traces and HF logs were not touched.")187 return (188 ledger_html(), # ledger_panel189 render_node_cards(Environment(temp=22, humidity=45)), # node_cards190 "<div class='ce-sub'>Reset to baseline. Run the Print loop for a fresh verdict.</div>", # review_summary191 "", "", "", # p_curve, p_policy, p_log192 "", # p_headline193 "", # outcome_panel194 gr.update(visible=False), # print results_group (re-hide)195 "", # real_log_msg196 studio_log_html(), # studio_log197 {"geometry": None, "mesh": None, "label": None, "read": None}, # part state198 gr.update(value=None, visible=False), # model3d199 gr.update(visible=True), # part_placeholder200 "", # part_status201 gr.update(visible=False), # build_results202 gr.update(visible=False), # read_results203 gr.Tabs(selected="studio"), # tabs back to LOAD204 review_record_html(None), # review_record (clear)205 )206 207 208def _viewer_placeholder() -> str:209 """Custom empty-state for the blank part/model viewer (no generic Gradio canvas)."""210 return (211 "<div class='ce-viewer'>"212 f"<div class='ce-viewer-ico'>{icon('layers', 40)}</div>"213 "<div class='ce-viewer-title'>NO PART LOADED</div>"214 "<div class='ce-viewer-copy'>Define the part, set the material and the room, then "215 "<b>SLICE</b> (Build) and <b>PRINT</b>. You give it the part, the material, and the room; "216 "it infers what kind of part this is.</div>"217 "<div class='ce-viewer-hint'>quick-load Benchy · drop a mesh · or generate a primitive →</div>"218 "</div>"219 )220 221 222def _set_part(geometry: str, mesh, label: str, read: str | None = None):223 """Shared preview update → (part_state, model3d, status, placeholder). The user never224 picks the class — the engineer infers it from the mesh (see infer_geometry). Loading a225 part reveals the 3D model and hides the custom placeholder."""226 read = read or GEO_READS.get(geometry, geometry)227 return (228 {"geometry": geometry, "mesh": mesh, "label": label, "read": read},229 gr.update(value=mesh, visible=True),230 f"ACTIVE PART · **{label}** · the engineer reads this as *{read}* → reasons about `{geometry}`",231 gr.update(visible=False),232 )233 234 235def build_start(part):236 """Instant feedback when BUILD is clicked: jump to Build, reveal the slicer +237 virtual print preview immediately, and show the read panel in a loading state238 while the model runs. No part → stay put."""239 if not (part and part.get("geometry")):240 gr.Warning("Load a part on the BUILD tab first — quick-load Benchy, generate a primitive, or drop a mesh.")241 return (gr.update(),) * 13242 mesh = part.get("mesh")243 label = part.get("label") or "PART"244 vp_html = virtual_printer_html(mesh, caption=f"{label} · virtual print preview")245 layer_img = layer_image(mesh, 1)246 return (247 gr.Tabs(selected="build"), # tabs248 gr.update(visible=True), # build_results (reveal)249 gr.update(visible=True), # read_results (reveal, loading state)250 loader("READING THE PLAN · O'Brien is checking precedent and proposing settings"),251 # read_loader252 vp_html, # vprint253 gr.update(value=1), # vp_slider reset254 layer_img, # vp_layer255 gr.update(interactive=True), # to_print_btn (un-gate)256 gr.update(visible=False), # override_btn (hide)257 "", # second_opinion_panel (clear stale)258 gr.update(value="Engineer's Read"), # read_toggle (reset to the read)259 gr.update(visible=True), # eng_read_group260 gr.update(visible=False), # second_op_group261 )262 263 264def load_benchy():265 mesh = benchy_mesh()266 if not mesh:267 return ({"geometry": None, "mesh": None, "label": None, "read": None},268 gr.update(value=None), "**BENCHY MISSING** — add assets/benchy.glb", gr.update())269 geo, read = infer_geometry(mesh)270 return _set_part(geo, mesh, "3DBENCHY (CC0)", read)271 272 273def generate_part(kind, size):274 mesh, geo = generate_primitive(kind, size or 30)275 return _set_part(geo, mesh, f"GENERATED {str(kind).upper()} · {float(size or 30):.0f}MM")276 277 278def upload_part(f):279 if not f:280 return (gr.update(), gr.update(), gr.update(), gr.update())281 from pathlib import Path as _P282 geo, read = infer_geometry(f.name)283 return _set_part(geo, f.name, f"UPLOAD · {_P(f.name).name[:26]}", read)284 285 286def scrub_layer(idx, part):287 """Render one cross-section layer at full fidelity for the scrubber slider."""288 mesh = (part or {}).get("mesh")289 return layer_image(mesh, idx)290 291 292# ── model warm-up + live status + switcher (Live / LoRA / QAT) ────────────────293def _status_html() -> str:294 return f"<div class='ce-sub' style='font-size:12px;'>MODEL · {llm.backend_status()}</div>"295 296 297def warm_up_pending() -> str:298 """Header icon stays; the warm-up intent is signaled by a toast."""299 gr.Info("Warming up the model (first load can take ~30s on ZeroGPU)…")300 return _model_icon()301 302 303def warm_up_cb() -> str:304 """After warm-up, keep the info icon in the header."""305 return _model_icon()306 307 308def _model_icon() -> str:309 """Small info-icon callout for the header row. No text — the dropdown + warm-up310 button already give enough context; the tooltip explains the backends."""311 return (312 "<span class='ce-callout' tabindex='0'>" + icon("info") + ""313 "<span class='ce-tip'>The <b>LoRA v2/v3</b> adapters serve via ZeroGPU on the Space; "314 "<b>Base</b> is the stock Gemma 4 E4B; <b>Modal API</b> is the remote endpoint. "315 "Local users: pull the LoRA from HF Hub "316 "(kylebrodeur/microfactory-node-lora-v2 / -v3-qat) or via ollama.</span></span>"317 )318 319 320def _model_callout() -> str:321 """Legacy full-width model callout (used when more explanatory text is wanted)."""322 return (323 "<span class='ce-callout' tabindex='0'>" + icon("info") + " MODEL INFO"324 "<span class='ce-tip'>The <b>LoRA v2/v3</b> adapters serve via ZeroGPU on the Space; "325 "<b>Base</b> is the stock Gemma 4 E4B; <b>Modal API</b> is the remote endpoint. "326 "Local users: pull the LoRA from HF Hub "327 "(kylebrodeur/microfactory-node-lora-v2 / -v3-qat) or via ollama.</span></span>"328 )329 330 331def select_model(choice: str) -> str:332 """Model switcher: apply the chosen backend, then return the info icon so the333 header stays compact."""334 choice = choice or MODEL_OPTIONS[0]335 try:336 _apply_model_choice(choice)337 except Exception:338 pass339 return _model_icon()340 341 342# ── simulated environment (this is a sim lab — conditions are generated, overridable) ──343def _sensor_readout(t, h, pos) -> str:344 """Compact value line for the envbar. The 'ENVIRONMENT (SIMULATED)' label is345 rendered separately in app.py so it matches the POSITION / MATERIAL labels."""346 return (f"{icon('thermo')} <b style='color:var(--ao-blue);'>{float(t):.0f}°C</b> · "347 f"{icon('droplet')} <b style='color:var(--ao-blue);'>{float(h):.0f}%RH</b> · "348 f"{icon('target')} <b style='color:var(--ao-blue);'>{pos}</b> · "349 f"{icon('printer')} <b style='color:var(--ao-outline);'>{PRINTER}</b>")350 351 352def status_footer(part, material, t, h, pos):353 """Live job context for the sticky footer strip."""354 p = part or {}355 label = p.get("label") or "no part"356 geo = p.get("geometry") or "—"357 return footer_bar(job=f"{label} · {material} · {geo}",358 env=f"{float(t):.0f}°C / {float(h):.0f}%RH · {pos} · {PRINTER}")359 360 361def randomize_sensors():362 """Roll a plausible ambient + plate position + material — the lab's simulated sensor feed."""363 t = random.choice([18, 20, 22, 24, 26, 28, 30, 32])364 h = random.choice([30, 38, 45, 52, 60, 68])365 pos = random.choices(BED_POSITIONS, weights=[3, 2, 1])[0] # center most common366 mat = random.choice(MATERIALS)367 return (gr.update(value=t), gr.update(value=h), gr.update(value=pos),368 gr.update(value=mat), _sensor_readout(t, h, pos))369 370 371def sync_readout(t, h, pos):372 return _sensor_readout(t, h, pos)373 374 375def build_job(part, material, description, temp, humidity, bed_position, model_choice):376 # NOTE: deliberately NOT @spaces.GPU. The GPU window lives on the inference377 # function only (core/llm_zerogpu._generate). Decorating the whole handler made378 # a ZeroGPU quota/error reject the ENTIRE build (slicer, retrieval, fallback) →379 # "Error" on the Space with no graceful fallback.380 if not (part and part.get("geometry")): # guard: empty start, no part chosen381 return ("", "", "**Load a part on the BUILD tab** (quick-load Benchy, generate, or drop a mesh) "382 "before building.", "", "", "", "", gr.update(visible=False),383 gr.update(), {}, "", gr.update(), gr.update(), "", gr.update(visible=False))384 385 # Apply model choice before inference386 _apply_model_choice(model_choice or "LoRA v3 (QAT E4B)")387 geometry_type, mesh = part["geometry"], part.get("mesh")388 job = Job(geometry_type=geometry_type, material=material, description=description or "",389 bed_position=bed_position or "center", mesh_path=mesh)390 env = Environment(temp=float(temp), humidity=float(humidity))391 392 retrieved = LEDGER.retrieve(material, geometry_type, env.temp, env.humidity)393 references = reference_block(material)394 policy_note = POLICY.policy_note(material, geometry_type, env)395 rec = advise(job, env, retrieved, references, policy_note)396 spine = SPINE.check(rec.advice.settings, material)397 hint = steepest_overhang_hint(mesh) if geometry_type in ("overhang", "bridge") else None398 vp_html = virtual_printer_html(mesh, settings=spine.settings,399 caption=f"{material} · {geometry_type}") # init on BUILD400 401 precedent = precedent_eval_html(retrieved, env)402 if retrieved:403 rows = "".join(404 f"<div style='font-family:ui-monospace,monospace;font-size:11px;color:var(--ao-outline);'>"405 f"• {e.job_id} ({e.source}) {e.outcome} @ {e.env_temp:.0f}°C/{e.env_humidity:.0f}% "406 f"(dist {dist:.2f})</div>"407 for e, dist in retrieved408 )409 precedent += f"<div style='margin-top:4px;'>{rows}</div>"410 411 fb = " · deterministic fallback" if rec.used_fallback else ""412 spine_md = (f"**{icon('shield')} Spine veto:** " + " \n".join(spine.vetoes)) if spine.vetoes else ""413 confirm_vis = gr.update(visible=spine.requires_approval)414 approval_md = ("**HITL gate:** the Spine clamped a boundary setting — review, then **Confirm & Print**."415 if spine.requires_approval else "Within safe envelope — ready when you are.")416 spine_notes_text = f"{spine_md}\n\n{approval_md}" if spine_md else approval_md417 session_id = uuid.uuid4().hex418 state = {"job": job.model_dump(), "env": env.model_dump(), "settings": spine.settings.model_dump(),419 "advice": rec.advice.model_dump(), "label": part.get("label"), "session_id": session_id,420 "spine_notes": spine_notes_text}421 422 # ── field log (Space only — gated on HF_TOKEN; local/offline no-ops) ──423 field_log.log_build(424 job=state["job"], env=state["env"], settings=state["settings"],425 advice=state["advice"], backend=rec.backend, used_fallback=rec.used_fallback,426 )427 # ── deliberation log: O'Brien proposes -> Spine vetoes (same gate) ──428 s = spine.settings429 deliberation_log.log_turns(session_id, "preflight", [430 {"agent": "O'Brien", "act": "propose",431 "content": f"{rec.advice.reasoning} Proposed: nozzle {s.nozzle_temp:.0f}°C, bed "432 f"{s.bed_temp:.0f}°C, fan {s.fan_pct:.0f}%, retraction {s.retraction_mm:.1f}mm."},433 {"agent": "Spine", "act": "veto",434 "stance": "clamped" if spine.requires_approval else "clear",435 "content": ("Clamped: " + " · ".join(spine.vetoes)) if spine.vetoes436 else "Within the safe envelope for this material — no clamp."},437 ], _delib_ctx(job, env))438 439 return (440 f"{rec.backend}{fb}", # backend status441 precedent, # precedent442 f"**Chief Engineer O'Brien:** {rec.advice.reasoning}", # reasoning443 risk_callouts_html(rec.advice.risks, hint) + placement_callout(material, bed_position), # risks444 settings_panel_html(spine.settings, material), # settings (LCARS panel)445 spine_notes_text, # spine notes446 gcode_panel_html(spine.settings, material), # g-code (LCARS panel)447 confirm_vis, # confirm visibility448 render_node_cards(env, working=True), # node cards449 state, # state450 virtual_printer_html(mesh, settings=spine.settings,451 caption=f"{material} · {geometry_type}"), # vprint (refined caption)452 gr.update(value=1), # reset layer scrubber453 layer_image(mesh, 1), # initial scrubbed layer454 "", # read_loader (clear)455 )456 457 458def _compute_second_opinion(state):459 """Run the Inspector (La Forge) critique once. Returns the raw tuple used by460 the UI: (panel_html, to_print_update, override_update)."""461 if not state or "advice" not in state:462 return ("<div class='ce-sub'>Build a job first — then I'll give the plan a second look.</div>",463 gr.update(interactive=True), gr.update(visible=False))464 job = Job(**state["job"])465 env = Environment(**state["env"])466 settings = PrintSettings(**state["settings"])467 advice = Advice(**state["advice"])468 verdict = inspector.second_opinion(job, env, settings, advice)469 field_log.log_event("second_opinion", {"material": job.material, "geometry": job.geometry_type,470 "inspector_stance": verdict.stance,471 "inspector_headline": verdict.headline})472 deliberation_log.log_turns(state.get("session_id"), "preflight", [473 {"agent": "La Forge", "act": "second_opinion", "stance": verdict.stance,474 "content": f"{verdict.headline} — {verdict.detail}"},475 ], _delib_ctx(job, env))476 panel = inspector_panel(verdict, label="LA FORGE · SECOND OPINION (PRE-PRINT)")477 if verdict.stance.lower() == "dispute":478 panel += ("<div style='margin-top:6px;padding:6px 10px;border-left:3px solid var(--ao-red,#d9534f);"479 "background:var(--ao-surface);font-family:ui-monospace,monospace;font-size:12px;"480 "color:var(--ao-text);'>" + icon('alert') + " <b>The Inspector disputes this plan.</b> "481 "→ PRINT is held. Review the objection, then acknowledge to proceed anyway.</div>")482 return panel, gr.update(interactive=False), gr.update(visible=True)483 return panel, gr.update(interactive=True), gr.update(visible=False)484 485 486def second_opinion(state):487 """Idempotent wrapper: if La Forge already weighed in on this build, return the488 cached verdict; otherwise compute it once and store it in state."""489 if state and state.get("second_opinion"):490 return state["second_opinion"]491 result = _compute_second_opinion(state)492 if state:493 state["second_opinion"] = result494 return result495 496 497def toggle_read(choice, state):498 """Segmented toggle on Build: flip between Engineer's Read and Second Opinion,499 showing one panel at a time. The opinion is computed lazily on first reveal and500 then cached for the rest of the build. While it computes, the Second Opinion panel501 shows a mini-loader so the user knows La Forge is reviewing the plan."""502 if str(choice).lower().startswith("engineer"):503 yield (gr.update(visible=True), gr.update(visible=False),504 "", gr.update(), gr.update(), state)505 return506 cached = state.get("second_opinion") if state else None507 if cached:508 panel, to_print, override = cached509 yield (gr.update(visible=False), gr.update(visible=True), panel, to_print, override, state)510 return511 # show in-place loader while the Inspector runs512 yield (gr.update(visible=False), gr.update(visible=True),513 loader("CONSULTING LA FORGE", stages=[514 "reading the engineer's plan",515 "checking precedent and risks",516 "comparing settings to the safe envelope",517 "drafting the pre-print verdict",518 ]), gr.update(interactive=True), gr.update(visible=False), state)519 panel, to_print, override = _compute_second_opinion(state)520 state = (state or {}) | {"second_opinion": (panel, to_print, override)}521 yield (gr.update(visible=False), gr.update(visible=True), panel, to_print, override, state)522 523 524def ack_override(state):525 """Human overrides the Inspector's dispute — re-open → PRINT (on the operator's call)."""526 if state and state.get("session_id"):527 deliberation_log.log_turns(state["session_id"], "preflight", [528 {"agent": "Operator", "act": "override", "stance": "override",529 "content": "Acknowledged La Forge's objection. Proceeding to print on the operator's call."},530 ], _delib_ctx(Job(**state["job"]), Environment(**state["env"])))531 return gr.update(interactive=True), gr.update(visible=False)532 533 534def job_readout(state):535 """The job Print inherits from Studio/Build (no re-picking — it prints THIS job)."""536 if not state or "job" not in state:537 return ("<div class='ce-sub'>No job built yet. Go to <b>BUILD</b> → define a part → "538 "<b>SLICE</b>, then return here to print it.</div>")539 j, e = state["job"], state["env"]540 return (f"<div class='ce-sub' style='font-size:13px;'>PRINTING · "541 f"<b style='color:var(--ao-orange);'>{state.get('label') or j['geometry_type']}</b> · "542 f"{j['material']}/{j['geometry_type']} · {icon('target')} {j.get('bed_position','center')} · "543 f"{icon('thermo')} {e['temp']:.0f}°C / {icon('droplet')} {e['humidity']:.0f}%RH · "544 f"{icon('printer')} {PRINTER}</div>")545 546 547def plan_card_html(state):548 """THE PLAN card: Spine-validated proposed settings + Spine notes for the current job."""549 if not state or "settings" not in state:550 return ("<div class='ce-sub'>No plan built yet. Go to <b>BUILD</b> → define a part → "551 "<b>SLICE</b>, then return here to print it.</div>")552 settings = PrintSettings(**state["settings"])553 material = state["job"]["material"]554 html = settings_panel_html(settings, material)555 notes = state.get("spine_notes") or ""556 if notes:557 html += ("<div class='ce-sub' style='margin-top:6px;'>" +558 notes.replace("\n", "<br>") + "</div>")559 return html560 561 562def plan_verdict_html(state):563 """La Forge's pre-print stance if already computed on the SLICE tab."""564 if not state or "settings" not in state:565 return ("<div class='ce-sub'>Build a job first to get La Forge's pre-print stance.</div>")566 cached = state.get("second_opinion")567 if cached:568 return cached[0]569 return ("<div class='ce-sub'>" + icon("search") +570 " Run the Inspector (<b>Second Opinion</b>) on the SLICE tab for a pre-print stance.</div>")571 572 573def plan_testing_html(state):574 """THE PLAN header: restate plainly what this run tests (job + conditions + question)."""575 if not state or "job" not in state:576 return ("<div class='ce-sub'>No job yet — build one on <b>BUILD → SLICE</b>, then return "577 "here to print it.</div>")578 j, e = state["job"], state["env"]579 return ("<div class='ce-sub' style='font-size:13px;'>Testing the engineer's plan for "580 f"<b style='color:var(--ao-orange);'>{state.get('label') or j['geometry_type']}</b> "581 f"({j['material']}/{j['geometry_type']}) at {e['temp']:.0f}°C / {e['humidity']:.0f}%RH on "582 f"{PRINTER}: <i>does it print clean, and does the policy improve across the run?</i></div>")583 584 585def _next_steps_html(state):586 """NEXT STEPS for the REVIEW record — what the run earned and what to do next."""587 run = state.get("run") if state else None588 j = state["job"]589 steps = []590 if run:591 if run.get("first"):592 steps.append(f"Converged to a clean print by iteration <b>{run['first']}</b> — the learned "593 f"policy for {j['material']}/{j['geometry_type']} at these conditions is now stored "594 "and will pre-bias the next similar job.")595 else:596 steps.append("No clean print this run — the job is genuinely hard for these conditions or the "597 "policy is saturated. Worth a human look, an <b>OVERRIDE</b>, or logging a real outcome.")598 steps.append("Print this on the real machine and use <b>LOG A REAL PRINT</b> to feed the true "599 "outcome back into the ledger.")600 else:601 steps.append("Run the <b>PRINT</b> loop to simulate this job and learn from the outcome.")602 return "".join(f"<div class='ce-sub' style='margin:3px 0;'>{icon('arrow')} {s}</div>" for s in steps)603 604 605def review_record_html(state):606 """The full session record on REVIEW: inputs → O'Brien's read → La Forge's607 pre-print stance → the simulated run → outcome → next steps. Assembled from608 state so the whole story of this job lives in one place."""609 if not state or "job" not in state:610 return ("<div class='ce-sub'>No session yet. Build a job (<b>BUILD → SLICE</b>), get the read "611 "and a second opinion, then <b>PRINT</b> — the full record assembles here.</div>")612 j, e = state["job"], state["env"]613 p = []614 p.append(rule("INPUTS · THE JOB"))615 p.append("<div class='ce-sub' style='font-size:13px;'>"616 f"<b style='color:var(--ao-orange);'>{state.get('label') or j['geometry_type']}</b> · "617 f"{j['material']}/{j['geometry_type']} · {icon('target')} {j.get('bed_position','center')} · "618 f"{icon('thermo')} {e['temp']:.0f}°C / {icon('droplet')} {e['humidity']:.0f}%RH · "619 f"{icon('printer')} {PRINTER}</div>")620 p.append(rule("CHIEF ENGINEER O'BRIEN · THE READ"))621 adv = state.get("advice") or {}622 p.append(f"<div class='ce-sub'>{adv.get('reasoning') or '(no read captured this session)'}</div>")623 p.append(rule("LA FORGE · PRE-PRINT SECOND OPINION"))624 cached = state.get("second_opinion")625 p.append(cached[0] if cached else626 "<div class='ce-sub'>No second opinion captured — run it on the SLICE tab (THE READ → Second Opinion).</div>")627 run = state.get("run")628 p.append(rule("SIMULATED PRINT RUN"))629 if run:630 p.append(run["curve_html"])631 p.append(run["log_html"])632 p.append(rule("OUTCOME · WHAT HAPPENED"))633 p.append(run["outcome_html"])634 else:635 p.append("<div class='ce-sub'>Not printed yet — run the PRINT loop to see the simulated run + outcome.</div>")636 p.append(rule("NEXT STEPS"))637 p.append(_next_steps_html(state))638 return "".join(p)639 640 641 642def _print_plan_values(state):643 """Return the Engineer's proposed settings values (or safe defaults) for VARY sliders."""644 defaults = {"nozzle_temp": 200, "bed_temp": 60, "fan_pct": 80, "retraction_mm": 4.5}645 if not state or "settings" not in state:646 return defaults647 s = state["settings"]648 return {k: s.get(k, defaults[k]) for k in defaults}649 650 651def apply_overrides(state, nozzle, bed, fan, retract):652 """Compare VARY slider values to the Engineer's plan. If anything changed, build an653 override PrintSettings and log that the operator defied the plan. Returns the override654 dict (or None when the plan is unchanged)."""655 if not state or "settings" not in state:656 return None657 plan = PrintSettings(**state["settings"])658 diffs = {}659 if abs(float(nozzle) - plan.nozzle_temp) > 1e-6:660 diffs["nozzle_temp"] = float(nozzle)661 if abs(float(bed) - plan.bed_temp) > 1e-6:662 diffs["bed_temp"] = float(bed)663 if abs(float(fan) - plan.fan_pct) > 1e-6:664 diffs["fan_pct"] = float(fan)665 if abs(float(retract) - plan.retraction_mm) > 1e-6:666 diffs["retraction_mm"] = float(retract)667 if not diffs:668 state.pop("print_overrides", None)669 return None670 overrides = plan.model_copy(update=diffs)671 state["print_overrides"] = overrides.model_dump()672 job = Job(**state["job"])673 env = Environment(**state["env"])674 field_log.log_print_override(job.model_dump(), env.model_dump(), overrides.model_dump())675 if state.get("session_id"):676 deliberation_log.log_turns(state["session_id"], "preflight", [677 {"agent": "Operator", "act": "override",678 "content": (f"Overrode the Engineer: nozzle {overrides.nozzle_temp:.0f}°C, "679 f"bed {overrides.bed_temp:.0f}°C, fan {overrides.fan_pct:.0f}%, "680 f"retraction {overrides.retraction_mm:.1f}mm.")},681 ], _delib_ctx(job, env))682 return overrides.model_dump()683 684 685def _simulated_result_panel(sess, run_summary, material, geometry_type, env, label) -> str:686 """Two-zone outcome — the dominant SIMULATED RESULT zone (the compact LOG A REAL687 PRINT zone is static UI below). Shows the final outcome, the climb, whether the688 Inspector's prediction held, and La Forge's run verdict."""689 traj = sess.trajectory690 final = sess.records[-1].result691 first = sess.first_success692 passed = final.outcome == "success"693 col = "var(--ao-green)" if passed else "var(--ao-red)"694 badge = "PASS" if passed else "FAIL"695 climb = (f"first clean print at iteration <b>{first}</b>" if first696 else f"still improving — best <b>{max(traj):.2f}</b>")697 return (698 "<div style='font-family:ui-monospace,monospace;background:var(--ao-void);"699 "border:1px solid var(--ao-outline-dim);border-left:3px solid var(--ao-orange);padding:10px 12px;'>"700 f"<div style='color:var(--ao-orange);font-weight:700;letter-spacing:2px;font-size:11px;'>"701 f"{icon('flask')} SIMULATED RESULT <span style='color:var(--ao-outline);font-weight:400;'>"702 "(deterministic world — stand-in for printer + sensors)</span></div>"703 f"<div class='ce-sub' style='margin-top:6px;'>WHAT WAS SIMULATED · {material}/{geometry_type} "704 f"· {env.temp:.0f}°C/{env.humidity:.0f}%RH · {PRINTER}</div>"705 f"<div style='margin-top:4px;font-size:15px;'>FINAL · "706 f"<span style='color:{col};font-weight:700;'>[{badge}] {final.detail}</span></div>"707 f"<div class='ce-sub'>Started at quality <b>{traj[0]:.2f}</b>; {climb}; now <b>{traj[-1]:.2f}</b> "708 f"over {len(traj)} runs.</div></div>"709 + inspector_panel(run_summary, label="LA FORGE · RUN VERDICT")710 )711 712 713def run_print(state, iterations, nozzle, bed, fan, retract, progress=gr.Progress()):714 """PRINT: run THIS job (inherited from Build) through the closed loop.715 Now a generator so the iteration log + quality chart + policy cell fill in716 live as each iteration completes. Per-iteration timing is shown next to each row."""717 if not state or "job" not in state:718 gr.Warning("Build a job first (BUILD → SLICE), then print it here.")719 yield (gr.update(),) * 9720 return721 job = Job(**state["job"])722 env = Environment(**state["env"])723 material, geometry_type = job.material, job.geometry_type724 key = cell_key(material, geometry_type, env)725 before_html = policy_cell_html(POLICY.cell_stats(material, geometry_type, env), key)726 727 overrides_dict = apply_overrides(state, nozzle, bed, fan, retract)728 overrides = PrintSettings(**overrides_dict) if overrides_dict else None729 730 records, verdicts, timings = [], [], []731 n_iters = int(iterations)732 start_iter = time.perf_counter()733 for i in range(1, n_iters + 1):734 progress(i / n_iters, desc=f"Iteration {i}/{n_iters}")735 record = run_iteration(job, env, POLICY, LEDGER, i, overrides=overrides)736 verdict = inspector.grade_iteration(geometry_type, record.result)737 elapsed = time.perf_counter() - start_iter738 start_iter = time.perf_counter()739 records.append(record)740 verdicts.append(verdict)741 timings.append(elapsed)742 yield (743 gr.update(visible=True), # results_group744 "", # outcome_panel745 "", # p_headline746 quality_curve_html([r.result.quality for r in records]), # p_curve747 iteration_log_html(records, verdicts, timings), # p_log748 policy_cell_html(POLICY.cell_stats(material, geometry_type, env), key), # p_policy749 gr.update(), # ledger_panel750 gr.update(), # node_cards751 "", # review_summary752 )753 754 after = POLICY.cell_stats(material, geometry_type, env)755 run_summary = inspector.summarize_run(records, material=material, geometry=geometry_type)756 757 traj = [r.result.quality for r in records]758 first = next((r.n for r in records if r.result.outcome == "success"), None)759 field_log.log_event("print_run", {"material": material, "geometry": geometry_type,760 "env_temp": env.temp, "env_humidity": env.humidity,761 "iterations": len(records), "q_start": round(traj[0], 3),762 "q_end": round(traj[-1], 3), "first_clean": first,763 "inspector_stance": run_summary.stance,764 "used_override": overrides is not None,765 "override_settings": overrides.model_dump() if overrides else None})766 # ── deliberation log: World simulates -> La Forge grades, per iteration; then verdict ──767 ctx = _delib_ctx(job, env)768 loop_turns = []769 for r, g in zip(records, verdicts):770 clamp = " (Spine clamped a setting)" if r.clamped else ""771 loop_turns.append({"agent": "World", "act": "simulate", "stance": r.result.outcome,772 "content": f"Iteration {r.n}: {r.result.detail}.{clamp} Policy: {r.learned}."})773 loop_turns.append({"agent": "La Forge", "act": "grade", "stance": g.stance,774 "content": f"{g.headline} — {g.detail}"})775 deliberation_log.log_turns(state.get("session_id"), "print-loop", loop_turns, ctx)776 deliberation_log.log_turns(state.get("session_id"), "review", [777 {"agent": "La Forge", "act": "verdict", "stance": run_summary.stance,778 "content": f"{run_summary.headline} — {run_summary.detail}"},779 ], ctx)780 headline = (781 f"**{state.get('label') or geometry_type} · {material} @ {env.temp:.0f}°C / {env.humidity:.0f}% RH** — "782 f"started at quality **{traj[0]:.2f}** ({records[0].result.outcome}); "783 + (f"first clean print at **iteration {first}**, now **{traj[-1]:.2f}**."784 if first else f"still improving — best **{max(traj):.2f}** after {len(traj)} runs.")785 + " The Engineer proposed; a separate simulated world reported the outcome; the **Inspector** "786 "graded each run; the policy and ledger learned. *(Simulated — see [SIMULATION.md](docs/reference/SIMULATION.md).)*"787 )788 policy_html = (f"{before_html}<div style='text-align:center;color:var(--ao-orange);font-size:11px;"789 f"letter-spacing:2px;'>{icon('arrow')} LEARNED</div>{policy_cell_html(after, key)}")790 outcome = _simulated_result_panel(791 SessionResult(job=job, env=env, records=records),792 run_summary, material, geometry_type, env, state.get("label"))793 # Stash the run on state so the REVIEW tab can assemble the full session record.794 state["run"] = {795 "iterations": len(records), "traj": traj, "first": first,796 "outcome": records[-1].result.outcome, "detail": records[-1].result.detail,797 "headline": headline, "curve_html": quality_curve_html(traj),798 "log_html": iteration_log_html(records, verdicts, timings),799 "outcome_html": outcome, "verdict_stance": run_summary.stance,800 }801 yield (802 gr.update(visible=True), # results_group803 outcome, # outcome_panel804 headline, # p_headline805 quality_curve_html(traj), # p_curve806 iteration_log_html(records, verdicts, timings), # p_log807 policy_html, # p_policy808 ledger_html(), # ledger_panel809 render_node_cards(env, working=False), # node_cards810 inspector_panel(run_summary, label="LA FORGE · RUN VERDICT"), # review_summary811 )812 813 814def record_outcome(outcome, state):815 """LOG A REAL PRINT: a human reports what actually happened on the real machine,816 feeding a real outcome back into the ledger (use the tool today, then teach it)."""817 if not state or "job" not in state:818 gr.Warning("Build a job first (BUILD → SLICE), then record a real outcome here.")819 return gr.update(), ledger_html(), render_node_cards(Environment(temp=22, humidity=45))820 job = Job(**state["job"])821 env = Environment(**state["env"])822 settings = PrintSettings(**state["settings"])823 entry = reflect_on_job(job, env, settings, outcome, LEDGER)824 field_log.log_event("record", {"material": job.material, "geometry": job.geometry_type,825 "env_temp": env.temp, "env_humidity": env.humidity, "outcome": outcome})826 msg = (f"<div class='ce-sub'>{icon('book')} Real outcome logged (earned): "827 f"<i>{entry.lesson}</i></div>")828 return msg, ledger_html(), render_node_cards(env, working=False)829 830 831def launch(**kw):832 """Single launch entrypoint so the Astrometrics theme/CSS/clock apply833 everywhere the app is started (Gradio 6 takes these on launch, not Blocks)."""834 return build().queue().launch(theme=THEME, css=CSS, head=VP_HEAD, **kw)835 836 837def _action_bar(reset_btn_label="RESET", primary_label=None, primary_variant="primary",838 primary_id=None, primary_arrow=True):839 """Build the consistent top-right action bar (small primary + persistent Reset).840 Both buttons are scale=0 so they DON'T stretch full-width. Primary buttons get841 a right-side arrow icon by default to signal "proceed to next stage".842 If primary_id is omitted, a stable kebab-case id is derived from the label."""843 with gr.Row(elem_classes=["ce-actionbar"]):844 reset = gr.Button(reset_btn_label, elem_classes=["ce-pillbtn", "ce-act"], scale=0)845 primary = None846 if primary_label:847 primary_classes = ["ce-act"]848 if primary_arrow:849 primary_classes.append("ce-icon-arrow-after")850 if primary_id is None:851 primary_id = "ce-" + primary_label.lower().replace(" ", "-").replace("(", "").replace(")", "").replace("'", "")852 primary = gr.Button(primary_label, variant=primary_variant,853 elem_classes=primary_classes, elem_id=primary_id, scale=0)854 return reset, primary855 856 857def refresh_tabs(state):858 """Refresh on every tab visit: the PRINT plan readouts, the override sliders859 (seeded to the Engineer's plan), and the REVIEW session record."""860 vals = _print_plan_values(state)861 return (job_readout(state), plan_testing_html(state), plan_card_html(state),862 plan_verdict_html(state),863 gr.update(value=vals["nozzle_temp"]), gr.update(value=vals["bed_temp"]),864 gr.update(value=vals["fan_pct"]), gr.update(value=vals["retraction_mm"]),865 review_record_html(state))866 867 868def seed_plan_sliders(state):869 """Reset the override sliders back to the Engineer's proposed plan."""870 vals = _print_plan_values(state)871 return (gr.update(value=vals["nozzle_temp"]), gr.update(value=vals["bed_temp"]),872 gr.update(value=vals["fan_pct"]), gr.update(value=vals["retraction_mm"]))873 874 875# outputs touched by Reset (shared by the persistent Reset on every tab)876def _reset_outputs(ledger_panel, node_cards, review_summary, p_curve, p_policy, p_log,877 p_headline, outcome_panel, results_group, real_log_msg, studio_log,878 part, model3d, part_placeholder, part_status, build_results,879 read_results, tabs, review_record):880 return [ledger_panel, node_cards, review_summary, p_curve, p_policy, p_log,881 p_headline, outcome_panel, results_group, real_log_msg, studio_log,882 part, model3d, part_placeholder, part_status, build_results, read_results, tabs,883 review_record]884 885 886def build() -> gr.Blocks:887 with gr.Blocks(title="Microfactory Node: 3D Printer") as demo:888 gr.HTML(command_bar(llm.backend_status()))889 # header row: dropdown + warm button + info icon only (NO background tints, NO stretching)890 with gr.Row(elem_id="ce-modelswitch"):891 model_select = gr.Dropdown(MODEL_OPTIONS, value=MODEL_OPTIONS[0],892 show_label=False, container=False,893 elem_classes=["ce-modeldd"], scale=0)894 warm_btn = gr.Button("WARM UP", elem_id="ce-warm",895 elem_classes=["ce-pillbtn", "ce-icon-bolt"], scale=0)896 model_status = gr.HTML(_model_icon(), elem_classes=["ce-status-inline"])897 898 state = gr.State()899 part = gr.State({"geometry": None, "mesh": None, "label": None, "read": None})900 901 with gr.Tabs() as tabs:902 # ───────────────────────── BUILD · define the job ────────────────────────903 with gr.Tab("LOAD", id="studio"):904 # Top row: ENVIRONMENT + OVERRIDE + POSITION + MATERIAL grouped on the left;905 # RANDOMIZE / RESET / SLICE grouped on the right.906 with gr.Row(elem_classes=["ce-actionbar", "ce-envbar"], equal_height=False):907 with gr.Column(elem_classes=["ce-inline-group", "ce-envbar-left"], scale=0):908 with gr.Row(elem_classes=["ce-inline-pills"]):909 with gr.Column(elem_classes=["ce-inline-group"], scale=0):910 gr.HTML("<div class='ce-inline-label'>ENVIRONMENT (SIMULATED)</div>")911 sensors_readout = gr.HTML(elem_classes=["ce-envbar-readout"])912 gr.HTML("<button class='ce-pillbtn ce-icon-sliders' "913 "data-popup-trigger='override' type='button' "914 "id='ce-override' "915 "style='background:var(--ao-surface);color:var(--ao-orange);"916 "border:none;padding:5px 15px;border-radius:999px;"917 "text-transform:uppercase;letter-spacing:.5px;font-size:12px;"918 "font-weight:700;cursor:pointer;display:inline-flex;"919 "align-items:center;gap:6px;'>OVERRIDE</button>")920 with gr.Column(elem_classes=["ce-inline-group"], scale=0):921 gr.HTML("<div class='ce-inline-label'>POSITION"922 "<span class='ce-callout' tabindex='0'>" + icon("info", size=10) +923 "<span class='ce-tip'>edges/corners run cooler → warp/adhesion risk</span>"924 "</span></div>")925 bed_position = gr.Radio(BED_POSITIONS, value="center", show_label=False,926 elem_classes=["ce-pills"])927 with gr.Column(elem_classes=["ce-inline-group"], scale=0):928 gr.HTML("<div class='ce-inline-label'>MATERIAL</div>")929 material = gr.Radio(MATERIALS, value="PLA", show_label=False, elem_classes=["ce-pills"])930 # spacer pushes the action group to the right edge931 gr.HTML("<div style='flex:1;'></div>")932 with gr.Column(elem_classes=["ce-inline-group", "ce-envbar-actions"], scale=0):933 gr.HTML("<div class='ce-inline-label'>​</div>") # invisible label for alignment934 with gr.Row(elem_classes=["ce-inline-pills"]):935 roll_btn = gr.Button("RANDOMIZE", elem_id="ce-randomize",936 elem_classes=["ce-pillbtn", "ce-icon-shuffle"], scale=0)937 reset_s = gr.Button("RESET", elem_id="ce-reset-load",938 elem_classes=["ce-pillbtn", "ce-act"], scale=0)939 run_btn = gr.Button("SLICE", variant="primary",940 elem_classes=["ce-act", "ce-icon-arrow-after"], elem_id="ce-run", scale=0)941 942 # OVERRIDE ENVIRONMENT popup (hidden by default; toggled via JS)943 gr.HTML("<div class='ce-popup-backdrop' data-popup-backdrop='override'></div>")944 with gr.Group(elem_classes=["ce-popup", "ce-popup-override"]):945 gr.HTML("<div class='ce-popup-title'>OVERRIDE ENVIRONMENT"946 "<span class='ce-popup-close'>✕</span></div>")947 with gr.Row():948 temp = gr.Number(value=22, label="AMBIENT °C", elem_classes=["ce-num"], scale=1)949 humidity = gr.Number(value=45, label="HUMIDITY %RH", elem_classes=["ce-num"], scale=1)950 gr.HTML("<div class='ce-sub' style='margin-top:6px;'>Override the simulated "951 "environment to test the engineer's response to specific conditions.</div>")952 953 # PART card — intro + JOB LOG copy lives INSIDE here (not at top)954 with gr.Group(elem_classes=["ce-part-card"]):955 gr.HTML(rule("PART"))956 part_status = gr.Markdown("", elem_classes=["ce-pad"])957 with gr.Row(equal_height=False, elem_classes=["ce-part-row"]):958 # LEFT: 3D viewer with intro + job-log copy below it (moved from top)959 with gr.Column(scale=3, elem_classes=["ce-part-viewer-col"]):960 part_placeholder = gr.HTML(_viewer_placeholder())961 model3d = gr.Model3D(value=None, label="", height=360, visible=False,962 interactive=False)963 # intro + job log moved DOWN into the part area964 gr.HTML(tab_intro("Load the part, set the material and the room, then "965 "<b>SLICE</b> to read the engineer's pre-flight check. "966 "You give it the part, the material, and the room; it "967 "infers what kind of part this is."))968 studio_log = gr.HTML(studio_log_html())969 # RIGHT: dropzone + mesh-source buttons + NOTES970 with gr.Column(scale=1, elem_classes=["ce-part-actions-col"]):971 mesh_in = gr.File(file_types=[".stl", ".glb", ".obj"],972 label="UPLOAD MESH", show_label=False,973 elem_classes=["ce-drop"])974 benchy_btn = gr.Button("QUICK-LOAD BENCHY", elem_id="ce-benchy",975 elem_classes=["ce-pillbtn", "ce-icon-anchor",976 "ce-mesh-source"], scale=0)977 # GENERATE A PRIMITIVE: styled to match QUICK-LOAD BENCHY (ce-mesh-source)978 with gr.Accordion("GENERATE A PRIMITIVE", open=False,979 elem_classes=["ce-mesh-source", "ce-accordion-pill"]):980 gen_kind = gr.Radio(["box", "cylinder", "cone", "sphere"], value="box",981 show_label=False, elem_classes=["ce-pills"])982 gen_size = gr.Number(value=30, label="SIZE (mm)", elem_classes=["ce-num"])983 gen_btn = gr.Button("GENERATE", elem_id="ce-generate",984 elem_classes=["ce-pillbtn"], scale=0)985 description = gr.Textbox(label="NOTES (OPTIONAL)",986 placeholder="e.g. 45° bracket, 60mm tall",987 elem_classes=["ce-notes-inline"])988 989 # ───────────────── SLICE · slice + analyze + pre-flight check ─────────────990 with gr.Tab("SLICE", id="build"):991 reset_b, to_print_btn = _action_bar(primary_label="PRINT (RUN ITERATIONS)")992 gr.HTML(tab_intro("The pre-flight check, <b>before it prints</b>: slice the part, read "993 "precedent, flag failures, and get a second opinion. Then → PRINT."))994 with gr.Group(visible=False, elem_classes=["ce-part-card"]) as build_results:995 # slice + motion preview side by side — equal width columns996 gr.HTML(rule("SLICE · CROSS-SECTION + MOTION PREVIEW"))997 with gr.Row(equal_height=False, elem_classes=["ce-part-row", "ce-slice-viz"]):998 with gr.Column(scale=1, elem_classes=["ce-slice-col"]):999 vp_layer = gr.Image(label="", height=360, show_label=False,1000 interactive=False)1001 with gr.Column(scale=1, elem_classes=["ce-slice-col", "ce-vp"]):1002 vprint = gr.HTML()1003 with gr.Column(elem_classes=["ce-hslider"]):1004 vp_slider = gr.Slider(1, SCRUB_LAYERS, value=1, step=1,1005 show_label=False, container=False)1006 gr.HTML("<div class='ce-sub' style='padding:0 12px;'>Scrub through real cross-sections of "1007 "<i>this</i> part at full mesh fidelity. The preview animates the layer rise "1008 "while the read loads below.</div>")1009 1010 # Engineer's Read ↔ Second Opinion (one panel at a time)1011 with gr.Group(visible=False, elem_classes=["ce-card"]) as read_results:1012 backend = gr.Markdown()1013 gr.HTML(rule("THE READ"))1014 read_loader = gr.HTML()1015 read_toggle = gr.Radio(["Engineer's Read", "Second Opinion"],1016 value="Engineer's Read", show_label=False,1017 elem_classes=["ce-seg"])1018 with gr.Group(visible=True, elem_classes=["ce-card"]) as eng_read_group:1019 precedent = gr.HTML(elem_id="ce-precedent")1020 reasoning = gr.Markdown(elem_id="ce-reasoning")1021 risks = gr.HTML()1022 gr.HTML(rule("VALIDATION + G-CODE"))1023 spine_notes = gr.Markdown()1024 with gr.Row(equal_height=False):1025 settings_html = gr.HTML()1026 gcode_html = gr.HTML()1027 confirm_btn = gr.Button("CONFIRM & PRINT", elem_id="ce-confirm", visible=False)1028 with gr.Group(visible=False, elem_classes=["ce-card"]) as second_op_group:1029 gr.HTML("<div class='ce-sub' style='padding:0 12px;'>A separate inspector — <b>La Forge</b> — reviews "1030 "the plan before it prints: O'Brien is an optimist, La Forge is not.</div>")1031 second_opinion_panel = gr.HTML()1032 override_btn = gr.Button("PRINT ANYWAY (I'VE REVIEWED THE OBJECTION)",1033 visible=False, elem_classes=["ce-pillbtn"])1034 1035 # ──────────────────── PRINT · run it, iterate, grade ─────────────────────1036 with gr.Tab("PRINT", id="print"):1037 reset_p, p_run = _action_bar(primary_label="PRINT")1038 gr.HTML(tab_intro("Print <b>this job</b> (inherited from Build). The Engineer proposes → "1039 "the Spine vetoes → a <b>simulated world</b> prints → the <b>Inspector "1040 "grades</b> → policy + ledger learn. Quality compounds fail→clean."))1041 1042 # ENVBAR: what's printing (inherited from Build)1043 with gr.Row(elem_classes=["ce-actionbar", "ce-envbar"]):1044 p_job = gr.HTML(job_readout(None), elem_classes=["ce-envbar-readout"])1045 1046 # THE PLAN — what we're testing · the engineer's settings · what they expect1047 with gr.Group(elem_classes=["ce-card"]) as plan_card:1048 gr.HTML(rule("THE PLAN · WHAT WE'RE TESTING"))1049 plan_testing = gr.HTML(plan_testing_html(None))1050 gr.HTML("<div class='ce-sub' style='margin-top:8px;color:var(--ao-orange-soft);"1051 "letter-spacing:1px;'>ENGINEER'S PROPOSED SETTINGS · Spine-validated</div>")1052 plan_settings = gr.HTML()1053 gr.HTML("<div class='ce-sub' style='margin-top:8px;color:var(--ao-orange-soft);"1054 "letter-spacing:1px;'>WHAT THE ENGINEER EXPECTS · La Forge pre-print stance</div>")1055 plan_verdict = gr.HTML()1056 gr.HTML("<div style='margin-top:10px;display:flex;align-items:center;gap:10px;"1057 "flex-wrap:wrap;'>"1058 "<button class='ce-pillbtn ce-icon-sliders' data-popup-trigger='plan' "1059 "id='ce-override-plan' type='button' style='background:var(--ao-surface);"1060 "color:var(--ao-orange);border:none;padding:5px 15px;border-radius:999px;"1061 "text-transform:uppercase;letter-spacing:.5px;font-size:12px;font-weight:700;"1062 "cursor:pointer;display:inline-flex;align-items:center;gap:6px;'>"1063 "OVERRIDE PLAN</button>"1064 "<span class='ce-sub'>change the settings and print against your own values "1065 "instead of the engineer's plan</span></div>")1066 1067 # ITERATIONS — compact control + plain explainer1068 with gr.Row(elem_classes=["ce-iter-row"]):1069 gr.HTML("<div class='ce-inline-label' style='align-self:center;"1070 "margin:0 6px 0 2px;'>ITERATIONS</div>")1071 p_iters = gr.Slider(1, 16, value=8, step=1, show_label=False, container=False,1072 elem_classes=["ce-iter-slider"])1073 p_iter_readout = gr.HTML(1074 "<div class='ce-sub' style='align-self:center;white-space:nowrap;'>"1075 "<b>8</b> runs</div>")1076 gr.HTML("<div class='ce-sub' style='margin:2px 0 10px;'>How many times to print this job "1077 "in the simulated world — each run feeds the next. 1 = single print · "1078 "4 = quick convergence · 8 = balanced climb · 16 = full convergence run.</div>")1079 1080 # OVERRIDE PLAN popup — same component as OVERRIDE ENVIRONMENT on the BUILD tab1081 gr.HTML("<div class='ce-popup-backdrop' data-popup-backdrop='plan'></div>")1082 with gr.Group(elem_classes=["ce-popup", "ce-popup-plan"]):1083 gr.HTML("<div class='ce-popup-title'>OVERRIDE THE ENGINEER'S PLAN"1084 "<span class='ce-popup-close'>✕</span></div>")1085 gr.HTML("<div class='ce-sub' style='border-left:3px solid var(--ao-red);"1086 "padding:6px 10px;margin-bottom:8px;'>" + icon("alert") + " The run will use "1087 "these settings instead of the Spine-validated proposal.</div>")1088 with gr.Row():1089 p_nozzle = gr.Slider(150, 300, value=200, step=1,1090 label="NOZZLE °C", elem_classes=["ce-num"])1091 p_bed = gr.Slider(40, 120, value=60, step=1,1092 label="BED °C", elem_classes=["ce-num"])1093 with gr.Row():1094 p_fan = gr.Slider(0, 100, value=80, step=1,1095 label="FAN %", elem_classes=["ce-num"])1096 p_retract = gr.Slider(0, 10, value=4.5, step=0.1,1097 label="RETRACTION mm", elem_classes=["ce-num"])1098 reset_plan_btn = gr.Button("RESET TO ENGINEER'S PLAN", elem_classes=["ce-pillbtn"])1099 1100 with gr.Group(visible=False) as results_group:1101 gr.HTML(rule("OUTCOME · WHAT HAPPENED"))1102 outcome_panel = gr.HTML()1103 p_headline = gr.Markdown()1104 gr.HTML(rule("QUALITY PER ITERATION"))1105 p_curve = gr.HTML()1106 gr.HTML(rule("ITERATION LOG"))1107 p_log = gr.HTML()1108 gr.HTML(rule("LEARNED POLICY CELL"))1109 p_policy = gr.HTML()1110 # compact secondary zone (~20%): log a REAL print back into the ledger1111 with gr.Row(elem_classes=["ce-card"]):1112 with gr.Column(scale=1):1113 gr.HTML("<div class='ce-sub'>LOG A REAL PRINT · printed this on your machine? "1114 "Record what actually happened — it feeds the ledger.</div>")1115 with gr.Row(elem_id="ce-outcomes"):1116 b_clean = gr.Button("PRINTED CLEAN")1117 b_sag = gr.Button("SAGGED")1118 b_string = gr.Button("STRINGING")1119 real_log_msg = gr.HTML()1120 1121 # ───────────────── REVIEW · compounding + agent verdicts ─────────────────1122 with gr.Tab("REVIEW", id="review"):1123 reset_r, refresh = _action_bar(reset_btn_label="RESET TO BASELINE",1124 primary_label="REFRESH LEDGER",1125 primary_variant="secondary")1126 gr.HTML(tab_intro("The whole job, end to end — inputs, O'Brien's read, La Forge's "1127 "second opinion, the simulated run + outcome, and what the node learned. "1128 "Plus the live ledger and the capability mesh."))1129 gr.HTML(rule("LA FORGE · RUN VERDICT"))1130 review_summary = gr.HTML("<div class='ce-sub'>Run the Print loop to get the Inspector's "1131 "verdict on the whole run.</div>")1132 # Full session record — the complete story of this job in one place.1133 with gr.Group(elem_classes=["ce-card"]):1134 gr.HTML(rule("SESSION RECORD"))1135 review_record = gr.HTML(review_record_html(None))1136 with gr.Row():1137 with gr.Column(elem_classes=["ce-card"]):1138 gr.HTML(rule("LESSON LEDGER"))1139 ledger_panel = gr.HTML(ledger_html())1140 with gr.Column(elem_classes=["ce-card"]):1141 with gr.Accordion("CAPABILITY MESH · NODE NETWORK (outlook view)", open=True):1142 node_cards = gr.HTML(render_node_cards(Environment(temp=22, humidity=45)))1143 1144 footer = gr.HTML(footer_bar())1145 privacy_line = gr.HTML(visible=field_log.is_active())1146 1147 # Email signup for Microfactory updates (opt-in, privacy-first)1148 with gr.Group(elem_classes=["ce-card"]):1149 gr.HTML(rule("STAY IN THE LOOP"))1150 gr.HTML("<div class='ce-sub'>Get Microfactory updates: new nodes, build notes, and when this thing learns to stream g-code straight to the printer. One email. No spam. Unsub any time.</div>")1151 with gr.Row():1152 signup_email = gr.Textbox(placeholder="you@example.com", label="EMAIL", show_label=False,1153 elem_classes=["ce-notes-inline"], scale=3)1154 signup_consent = gr.Checkbox(label="Yes, email me Microfactory updates", scale=2)1155 signup_btn = gr.Button("SUBSCRIBE", elem_classes=["ce-pillbtn"])1156 signup_status = gr.HTML()1157 signup_privacy = gr.HTML(signups.privacy_notice())1158 1159 # ── wiring ──1160 reset_outs = _reset_outputs(ledger_panel, node_cards, review_summary, p_curve, p_policy,1161 p_log, p_headline, outcome_panel, results_group, real_log_msg,1162 studio_log, part, model3d, part_placeholder, part_status,1163 build_results, read_results, tabs, review_record)1164 preview_outs = [part, model3d, part_status, part_placeholder]1165 foot_in = [part, material, temp, humidity, bed_position]1166 benchy_btn.click(load_benchy, None, preview_outs).then(status_footer, foot_in, [footer])1167 gen_btn.click(generate_part, [gen_kind, gen_size], preview_outs).then(status_footer, foot_in, [footer])1168 mesh_in.upload(upload_part, [mesh_in], preview_outs).then(status_footer, foot_in, [footer])1169 material.change(status_footer, foot_in, [footer])1170 1171 # Model warm-up + switcher1172 warm_btn.click(warm_up_pending, None, [model_status]).then(warm_up_cb, None, [model_status])1173 model_select.change(select_model, [model_select], [model_status])1174 1175 # Simulated environment: roll on load, re-roll on demand, keep readout + footer in sync.1176 sensor_outs = [temp, humidity, bed_position, material, sensors_readout]1177 demo.load(randomize_sensors, None, sensor_outs).then(status_footer, foot_in, [footer])1178 # CLOCK_JS wires the LCARS clock, popup toggles, and loader stage cycling.1179 demo.load(None, None, None, js=CLOCK_JS)1180 demo.load(lambda: field_log.privacy_notice() if field_log.is_active() else "",1181 None, [privacy_line])1182 signup_btn.click(signups.record_signup,1183 [signup_email, signup_consent, gr.State("space" if signups.is_active() else "local")],1184 [signup_status])1185 roll_btn.click(randomize_sensors, None, sensor_outs).then(status_footer, foot_in, [footer])1186 for c in (temp, humidity, bed_position, material):1187 c.change(sync_readout, [temp, humidity, bed_position], [sensors_readout]).then(1188 status_footer, foot_in, [footer])1189 1190 # Two-step BUILD: instant loader + tab-switch, then the heavy model call (reveals1191 # the results), then refresh the inherited-job readout on the Print tab.1192 build_start_outs = [tabs, build_results, read_results, read_loader, vprint, vp_slider,1193 vp_layer, to_print_btn, override_btn, second_opinion_panel,1194 read_toggle, eng_read_group, second_op_group]1195 build_outs = [backend, precedent, reasoning, risks, settings_html, spine_notes,1196 gcode_html, confirm_btn, node_cards, state, vprint, vp_slider, vp_layer,1197 read_loader]1198 # Readouts on the PRINT plan + REVIEW record + override sliders, populated on1199 # every path that lands a job (build completion, SLICE→PRINT, and tab visits).1200 plan_outs = [p_job, plan_testing, plan_settings, plan_verdict,