"""Turn the project graph into a Mermaid diagram — built from real edges. The architecture map is **not** drawn by the model. It's generated directly from `ProjectModel` (the resolved import edges, grouped by plain-English role), so it is always faithful to the actual code. We render it client-side with mermaid.js inside a Gradio HTML component. """ from __future__ import annotations import html as _html import posixpath import config from schema import ProjectModel # Order roles top-to-bottom so the map reads like a journey: front door at top. _ROLE_ORDER = ("entry", "frontend", "backend", "data", "config", "util", "test", "other") def _node_id(idx: int) -> str: return f"n{idx}" def build_mermaid(model: ProjectModel) -> str: """Return Mermaid `flowchart` source for the project.""" if not model.files: return "flowchart TD\n empty[No code files found]" ids = {f.path: _node_id(i) for i, f in enumerate(model.files)} lines: list[str] = ["flowchart TD"] # Group nodes into subgraphs by role. by_role: dict[str, list] = {} for f in model.files: by_role.setdefault(f.role, []).append(f) for role in _ROLE_ORDER: group = by_role.get(role) if not group: continue label = config.ROLE_LABEL.get(role, role) lines.append(f' subgraph {role}["{label}"]') for f in group: base = posixpath.basename(f.path) lines.append(f' {ids[f.path]}["{_esc(base)}"]') lines.append(" end") # Edges: A --> B means "A uses B". for f in model.files: for dep in f.depends_on: if dep in ids: lines.append(f" {ids[f.path]} --> {ids[dep]}") # Colour each node by role (safe-to-edit is its own panel; here we show roles). for role in _ROLE_ORDER: if role in by_role: color = config.ROLE_COLOR.get(role, "#9aa0a6") lines.append(f" classDef {role} fill:{color},stroke:#1d2430,color:#fff,rx:8,ry:8;") for f in model.files: lines.append(f" class {ids[f.path]} {f.role};") return "\n".join(lines) def _esc(text: str) -> str: # Mermaid node labels: keep it simple, strip quotes/brackets that break parsing. return text.replace('"', "").replace("[", "(").replace("]", ")") # Loaded once in the page
(see ui.theme.MERMAID_HEAD). A MutationObserver # renders any `.mermaid` block Gradio swaps in after an update — the reliable way # to get diagrams to (re)draw on dynamic content in a Gradio Space. def render_html(model: ProjectModel) -> str: """The mermaid block for a gr.HTML component (loader lives in the page head).""" safe = _html.escape(build_mermaid(model)) # `key` forces a fresh, unprocessed node each render so the observer redraws it. import time key = int(time.time() * 1000) return (f'{safe}