"""HTML builders for the Workbench shell. Every function here returns a plain HTML string for a ``gr.HTML`` component. They only format data already produced by ``biolmnet.data`` / ``biolmnet.training`` / ``biolmnet.artifacts`` — none of them compute anything scientific. """ from __future__ import annotations import html from typing import Iterable, Sequence esc = html.escape CORNER_MARKS = ( '' '' ) def blueprint_div(inner_html: str, *, extra_class: str = "", style: str = "") -> str: """Wrap ``inner_html`` in a blueprint frame (hairline border + corner marks).""" cls = f"blueprint {extra_class}".strip() style_attr = f' style="{style}"' if style else "" return f'
{CORNER_MARKS}{inner_html}
' # ── rail ───────────────────────────────────────────────────────────────── def brand_block_html() -> str: return ( '
BioLM-NET' '
Workbench
' '
Workflow
' ) def rail_row_html(number: int, label: str, state: str, status_word: str) -> str: """One workflow rail row. ``state`` is one of done/on/next/off.""" return ( f'
' f'{number:02d}' f'{esc(label)}' f'{esc(status_word)}' f"
" ) def kv_row(key: str, value: str, tone: str | None = None) -> str: cls = f"num {tone}" if tone else "num" return f'
{esc(key)}{esc(value)}
' def run_state_plate(rows: Sequence[tuple[str, str, str | None]]) -> str: """``rows``: (key, value, tone) where tone is None/accent/error/muted.""" inner = "".join(kv_row(k, v, tone) for k, v, tone in rows) return blueprint_div(inner, extra_class="plate") def kv_plain_row(key: str, value: str, *, last: bool = False) -> str: cls = "kv-plain last" if last else "kv-plain" return f'
{esc(key)}{esc(value)}
' def footnote_html(line1: str, line2: str) -> str: return f'
{esc(line1)}
{esc(line2)}
' # ── topbar / page head ────────────────────────────────────────────────── def topbar_html(stage_no: int, total: int, stage_name: str, session_id: str, right_text: str) -> str: # Only the left "STAGE 0n / 05" label is uppercase; the session/device # note on the right stays mixed-case (a lowercase-hex session id reading # upper-cased is just noise). return ( '
' f'
Stage {stage_no:02d} / {total:02d} — {esc(stage_name)}
' f'
' f"Session {esc(session_id)} · {esc(right_text)}
" "
" ) def title_block_html(title: str, description: str) -> str: return f"

{esc(title)}

" f'
{esc(description)}
' def mono_meta_html(lines: Iterable[str], *, align_right: bool = True) -> str: align = "text-align:right;" if align_right else "" body = "
".join(esc(line) for line in lines) return f'
{body}
' # ── stat plates ────────────────────────────────────────────────────────── def stat_plate(value: str, label: str, *, label_first: bool = False) -> str: if label_first: inner = f'
{esc(label)}
{esc(value)}
' else: inner = f'
{esc(value)}
{esc(label)}
' return blueprint_div(inner, extra_class="stat") def mini_stat_html(value: str, label: str) -> str: """A label+value pair with no frame of its own (used inside a shared blueprint panel).""" return ( f'
{esc(label)}
' f'
{esc(value)}
' ) # ── bars ───────────────────────────────────────────────────────────────── def bar_cell(pct: float, *, height: int = 8) -> str: pct = max(0.0, min(100.0, pct)) return f'
' def labeled_bar_row( label: str, pct: float, value_text: str, *, label_width: int = 78, bar_height: int = 12, value_width: int = 52 ) -> str: return ( '
' f'{esc(label)}' f"{bar_cell(pct, height=bar_height)}" f'{esc(value_text)}' "
" ) # ── tables ─────────────────────────────────────────────────────────────── def table_html( headers: Sequence[str], rows: Sequence[Sequence[str]], *, aligns: Sequence[str] | None = None, numeric_cols: Sequence[bool] | None = None, widths: Sequence[str | None] | None = None, ) -> str: """Build a ``.tbl`` table. Cell values are raw HTML — escape plain text with :data:`esc` before passing it in.""" n_cols = len(headers) aligns = list(aligns) if aligns else ["left"] * n_cols numeric_cols = list(numeric_cols) if numeric_cols else [False] * n_cols widths = list(widths) if widths else [None] * n_cols head_cells = [] for header, align, width in zip(headers, aligns, widths): style_parts = [] if width: style_parts.append(f"width:{width}") if align == "right": style_parts.append("text-align:right") attr = f' style="{";".join(style_parts)}"' if style_parts else "" head_cells.append(f"{esc(header)}") body_rows = [] for row in rows: cells = [] for value, align, numeric in zip(row, aligns, numeric_cols): cls_attr = ' class="n"' if numeric else "" style_attr = ' style="text-align:right"' if align == "right" else "" cells.append(f"{value}") body_rows.append(f"{''.join(cells)}") return ( '' + "".join(head_cells) + "" + "".join(body_rows) + "
" ) def confusion_matrix_html(matrix, labels: Sequence[str]) -> str: """A tinted CSS grid confusion matrix (rows true, columns predicted).""" n = len(labels) row_totals = [max(sum(row), 1) for row in matrix] def cell_style(row_index: int, count: float) -> str: ratio = count / row_totals[row_index] if ratio >= 0.7: return "background:var(--color-accent-800);color:#f2f2f3" if ratio >= 0.4: return "background:var(--color-accent-700);color:#f2f2f3" if ratio >= 0.15: return "background:var(--color-accent-200)" return "background:var(--color-accent-100)" header_cells = "".join( f'
{esc(label)}
' for label in labels ) row_label_cells = "".join( f'
{esc(label)}
' for label in labels ) body_cells = "".join( f'
{int(matrix[i][j])}
' for i in range(n) for j in range(n) ) return ( '
' "
" f'
{header_cells}
' f'
{row_label_cells}
' f'
{body_cells}
' "
" ) # ── status / validation strips ─────────────────────────────────────────── def simple_status_html(message_html: str, *, error: bool = False) -> str: cls = "error" if error else "status" return f'
{message_html}
' def strip_text_html(kicker: str, message: str) -> str: """The text half of a bordered strip; pair with a real ``gr.Button`` in the same ``elem_classes=["strip", ...]`` row for the ghost action.""" return f'
{esc(kicker)}
{esc(message)}
' def empty_note_html(message: str) -> str: return f'
{esc(message)}
' # ── introduction cards ──────────────────────────────────────────────────── INTRO_CARDS = [ ( "Introduction · 01 / 06", "A network wired along known biology", ( "BioLM-NET classifies tumour samples from paired gene expression and DNA methylation. " "Instead of connecting every gene to every neuron, it only makes the connections biology supports — " "KEGG pathways, transcription-factor targets, protein interactions — so trained weights can be read " "back as pathways rather than as a black box." ), "This workbench runs the model end to end in five stages. On the bundled BRCA example it takes a few minutes.", "", ), ( "Introduction · 02 / 06", "Five stages, in order", ( "The rail on the left is the spine of the app. Each stage consumes the previous one's output, so a " "stage stays locked until its prerequisite exists — you cannot export a model you have not trained." ), ( '
' "
01Data & Priors — assemble the masked graph
" "
02Train Model — fit it, watch the epochs
" "
03Export Artifacts — save a reusable bundle
" "
04Predict — score a new cohort
" "
05Results — metrics and pathway attention
" "
" ), "The plate under the rail always says what state the model is in", ), ( "Introduction · 03 / 06 · Stage 01", "Data & Priors", ( "Point the workbench at paired omics — a bundled example, a GitHub folder, or your own upload — with " "samples in rows and HGNC symbols in columns. The two matrices are aligned on their shared " "samples, and every file is reported with the shape it was read at rather than failing silently." ), ( "The priors on the right are the biology that becomes wiring: pathway annotations, the enrichment " "cutoff, the GenePT context and the interaction sources. Defaults follow the paper." ), "Rebuilding clears any trained model", ), ( "Introduction · 04 / 06 · Stage 02", "Train Model", ( "Hyperparameters sit on the left as a spec sheet. The run panel on the right reports epoch, train " "and validation loss, accuracy and ETA as the fit progresses." ), ( "Controls lock for the duration of a run and the primary button reads Training on ZeroGPU " "until it finishes; Cancel stays available. The GPU is allocated on demand, so the first epoch may " "wait for its reservation." ), "Paper defaults are one click away", ), ( "Introduction · 05 / 06 · Stages 03–04", "Export, then predict", ( "Export writes one bundle carrying the weights, the fitted preprocessing, the mask and the config — " "enough to score new samples later without rebuilding the graph. Its manifest lists what each entry " "reproduces, with a checksum." ), ( "Predict scores a new cohort with either the session model or an uploaded bundle. Features are " "aligned to the artifact first; if a required column is missing, inference is blocked and the " "check that failed is named." ), "Bundles are temp files — download before the Space restarts", ), ( "Introduction · 06 / 06 · Stage 05", "Reading the results", ( "Results collects everything a run produced: validation metrics, the training curve, the confusion " "matrix, an audit of how sparse each layer actually is, and the pathway attention weights — which is " "the part the architecture exists to give you." ), ( "Treat it as model output, not as biology. Check cohort composition, preprocessing and class " "balance before drawing conclusions, and remember that attention weights rank pathways within " "this fit rather than proving mechanism." ), "Research use only · not for clinical decisions", ), ] def intro_card_html(index: int) -> str: step, title, body_1, body_2, _ = INTRO_CARDS[index] return ( f'
{esc(step)}
' f"

{esc(title)}

" f"

{body_1}

" f"

{body_2}

" ) def intro_dots_html(index: int, total: int = 6) -> str: return '
' + "".join( f'' for i in range(total) ) + "
" # ── misc ───────────────────────────────────────────────────────────────── def human_bytes(n: float) -> str: n = float(n) for unit in ("B", "KB", "MB"): if n < 1024 or unit == "MB": return f"{int(n)} {unit}" if unit == "B" else f"{n:.1f} {unit}" n /= 1024 return f"{n / 1024:.1f} GB"